fix(metadata): order-independent super-resolution for dotted extends#189
Merged
Conversation
… inherited members (#188) Metadata super-resolution was LOAD-ORDER-SENSITIVE: a dotted `extends: Owner.member` reference (FR-024 child-targeting ref) targeting an INHERITED member failed with ERR_UNRESOLVED_SUPER whenever the referring file was resolved before the owner entity's own `extends` chain was wired. The deferred-super pass walked nodes in physical-declaration (= load) order and read the owner's EFFECTIVE children (inherited members appear only once the owner's OWN super is resolved), so a dotted ref to an inherited member resolved only if the owner's chain happened to be wired first — green under Node's readdir order, a hard build failure under Bun's (the `meta` bin runs under Bun on a base image with no real node, e.g. oven/bun:1). Resolving a corpus must be a pure function of the source SET, not its enumeration order. Durable fix (all ports — TS reference + Python / C# / Java; Kotlin shares Java's loader): replace the single pre-order walk with ON-DEMAND MEMOIZED resolution with cycle detection. When resolving a dotted ref, resolve the OWNER node's own extends-chain FIRST (recursively, memoized) so its effective children include the inherited member before the ref reads it; after wiring a node, resolve the TARGET's own chain too (multi-level inheritance, e.g. Base extends AbstractRoot). An `attempted` guard makes each node resolve — and, on failure, report — EXACTLY ONCE, preserving the old single-visit walk's no-duplicate-failures guarantee now that a node is reachable from both the pending loop and owner/target recursion. Tier-1 invariants unchanged: ERR_UNRESOLVED_SUPER / ERR_EXTENDS_TARGET_MISMATCH, the failure envelope, and the target-mismatch contract are byte-identical. Floor (TS SDK only): `listMetadataFiles` now sorts files to match its docstring and the metadata package's own `DirectorySource` — deterministic enumeration across runtimes/filesystems. The other ports' DirectorySource already sorts. Gated by a new cross-port conformance fixture (`extends-dotted-inherited-member-load-order`: an abstract base with `id`+`pk`, an entity that inherits them, a projection whose `pk`/`id` carry dotted extends to those inherited members — filenames ordered so the referrer loads FIRST, reproducing the bug) plus a TS shuffle-invariance test (all 6 source-order permutations → identical resolved model) and a duplicate-failure regression test. All ports green: TS metadata 2131/0 + shuffle/dedup, Python conformance 336/0, C# conformance 720/0, Java metadata 1125/0, Kotlin 272/0. Co-Authored-By: Claude <noreply@anthropic.com>
dmealing
added a commit
that referenced
this pull request
Jul 9, 2026
….8 (Maven) Coordinated release across all four registries. Three merged efforts: - #185/#186 — BREAKING: origin.passthrough is type-preserving (+@convert) - #187 — typed value-object jsonb columns all ports (Kotlin→Jackson; C#/Java/Python array-of-VO) - #188/#189 — load-order-independent super-resolution (Node-vs-Bun build portability) npm: 14-package lockstep 0.15.17. PyPI metaobjects 0.15.10. NuGet MetaObjects* 0.15.8. Maven com.metaobjects:* 7.7.8. See CHANGELOG.md [0.15.17]. Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Fix GitHub issue #188: metadata super-resolution was LOAD-ORDER-SENSITIVE — a dotted extends: Owner.member ref to an INHERITED member failed with ERR_UNRESOLVED_SUPER when the referring file resolved before the owner's own extends chain was wired (green under Node readdir order, hard failure under Bun). Resolving a corpus must be a pure function of the source SET.
(Re-run: the prior attempt failed at PUSH on my own pre-push tsc gate — a type error in the new dedup regression test [e.source on Error]; fixed the cast, typecheck now clean, test green. No pipeline steps had run yet.)
DURABLE FIX (all ports — TS ref + Python/C#/Java; Kotlin shares Java's loader): replaced the single pre-order walk in deferred super-resolution with ON-DEMAND MEMOIZED resolution + cycle detection — resolve the OWNER's own extends-chain FIRST (memoized) so its effective children include the inherited member before the dotted ref reads it; after wiring a node, resolve the TARGET's chain too (multi-level inheritance). An 'attempted' guard makes each node resolve/report EXACTLY ONCE (no-duplicate-failures — a correctness item the simplifier altitude pass flagged, fixed + gated). FLOOR (TS SDK only): listMetadataFiles sorts files (deterministic enumeration); other ports' DirectorySource already sorts.
DELIBERATE: Tier-1 invariants unchanged (ERR_UNRESOLVED_SUPER / ERR_EXTENDS_TARGET_MISMATCH, failure envelope, target-mismatch contract byte-identical). TS/Python/C# accumulate a failure list, Java eager-throws (pre-existing). 3-angle simplifier ran; altitude clean-bill on the algorithm. Two MINOR reuse items left UNAPPLIED by choice (Python _owner_ref + Java inline dotted-parse duplicate a sibling parse helper; TS/C# already single-helper) + a minor Java resolved-set — low-risk polish, flag if you disagree.
Fixtures-first: new conformance fixture (extends-dotted-inherited-member-load-order) RED→GREEN in every port + TS shuffle-invariance (6 permutations → identical model) + duplicate-failure regression test. All green: TS 2131/0, Python 336/0, C# 720/0, Java 1125/0, Kotlin 272/0. Holds the coordinated #185+jsonb release so #188 ships together.
What Changed
extends: Owner.memberreads the owner's effectivechildren(), the owner's ownextendschain is resolved first, so a ref to an inherited member succeeds regardless of file enumeration order (resolving Metadata resolution is load-order-sensitive: dottedextends: Owner.memberto an inherited member fails under Bun (listMetadataFiles doesn't sort files) #188, where the same corpus passed under Node's readdir order and failed withERR_UNRESOLVED_SUPERunder Bun).attemptedguard makes each node resolve and report exactly once, restoring the no-duplicate-failures guarantee. Tier-1 error behavior (ERR_UNRESOLVED_SUPER/ERR_EXTENDS_TARGET_MISMATCH, failure envelope, target-mismatch contract) is unchanged.extends-dotted-inherited-member-load-orderconformance fixture plus TS shuffle-invariance and duplicate-failure regression tests; made the TS SDK metadata-file enumeration deterministic so the loader floor is order-independent too.Risk Assessment
✅ Low: Coordinated cross-port load-order fix with comprehensive tests (shuffle-invariance + duplicate-failure regression + adversarial conformance fixture); both round-1 review fixes (dead-cycle-guard removal in TS/Python/C#, Java owner threading) are correctly applied and behavior-preserving, Java's distinct cycle guard verified live (disjoint sets, not the dead-superset pattern), and no new material issues introduced.
Testing
Drove the real public loader API against the committed
extends-dotted-inherited-member-load-orderfixture across all 6 file-order permutations under Bun (the runtime where the bug surfaced). RED at base: 3/6 orders fail with ERR_UNRESOLVED_SUPER + ERR_INVALID_ORIGIN — precisely the view-before-customer orders (the dottedextends: Customer.id/Customer.pkref to an INHERITED member fails when the referrer resolves before the owner's extends chain is wired). GREEN with the fix: 6/6 orders resolve cleanly and produce a byte-identical resolved model (sha1 15345dc7…). New #188 unit tests and the auto-discovered conformance fixture pass; the full metadata suite is 2123/0 green. A negative control confirms the Tier-1 error contract is unchanged (genuine unresolvable supers still each reported exactly once). 5 sdk agent-context failures were isolated as pre-existing and unrelated (identical 145/5 with the PR's sdk change reverted to base). Working tree left clean; both temporarily-swapped files restored to HEAD exactly.Evidence: RED transcript (base super-resolve.ts) — order-sensitivity reproduced
order | errors | ok? a-view.json,b-customer.json,c-base.json| 4 | FAILED: ERR_UNRESOLVED_SUPER x2 (CustomerView.id, CustomerView.pk) + ERR_INVALID_ORIGIN x2 a-view.json,c-base.json,b-customer.json| 4 | FAILED (same) b-customer.json,a-view.json,c-base.json| 0 | clean b-customer.json,c-base.json,a-view.json| 0 | clean c-base.json,a-view.json,b-customer.json| 4 | FAILED (same) c-base.json,b-customer.json,a-view.json| 0 | clean VERDICT: FAIL — SOME PERMUTATIONS ERRORED. The 3 failing orders are exactly those where a-view.json (the dotted referrer) is processed BEFORE b-customer.json (its owner) — load-order-sensitivity reproduced.Evidence: GREEN transcript (fixed super-resolve.ts) — order-invariant
All 6 permutations: errors=0, clean. VERDICT: PASS — all 6 permutations resolved with 0 errors, resolved model identical across every order (sha1=15345dc7a50070dcaa81571815c77c5a510a2566). Negative control (Tier-1 invariant unchanged): ERR_UNRESOLVED_SUPER@…['object.entity'] (Owner→DoesNotExist) x1 ERR_UNRESOLVED_SUPER@…['object.projection']…['identity.primary'] (V.pk→Owner.pk) x1 VERDICT: PASS — every referring node reported EXACTLY ONCE (no duplicate failures from on-demand recursion); ERR_UNRESOLVED_SUPER still fires. === OVERALL: PASS ===Evidence: End-to-end order-invariance demo script (loads real fixture in 6 orders + negative control)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 2 issues found → auto-fixed ✅
server/typescript/packages/metadata/src/super-resolve.ts:255- TheinProgresscycle guard is unreachable in all three accumulate-failure ports (TS super-resolve.ts:255, Python super_resolve.py:63, C# SuperResolve.cs:308). It is checked immediately AFTERattempted, which is invariantly a superset: both sets are populated together at function entry (TS 256-257; Python 65-66; C# via the two.Addcalls at 307-308), and onlyinProgressis ever removed whileattemptedis never removed — soinProgress ⊆ attemptedalways holds andinProgress.has(node)can never be true when the line is reached.attemptedalone already prevents infinite recursion (a node on the stack is already inattempted). As a result the comments claiming 'a genuine super cycle is unresolvable → reported' are inaccurate: a real super cycle (A extends B, B extends A) is silently resolved A→B, B→A with no failure reported. This matches pre-Metadata resolution is load-order-sensitive: dottedextends: Owner.memberto an inherited member fails under Bun (listMetadataFiles doesn't sort files) #188 behavior so it is NOT a regression, but the guard is dead code and the comments overstate the behavior — a future maintainer debugging a cycle would be misled. Either remove the dead guard/fix the comments, or (if cycle-reporting is actually desired) the check order and reporting logic need rework. Note Java differs: thereresolvedandinProgressare disjoint sets checked separately (MetaDataLoader.java ~313-314), so Java's guard is live; this is an inconsistency to be aware of.server/java/metadata/src/main/java/com/metaobjects/loader/MetaDataLoader.java:310- For every deferred dotted ref,resolveOwnerObject(p)runs twice — once inresolvePendingNodeat line 310 (to wire the owner's chain first) and again insideresolveChildTargetingRefat line 489 (which re-resolves the same owner object before traversing its children). The second call repeats the full pkg-prepend → referrer-ancestry-walk → FQN lookup chain. Minor; the already-resolved owner could be threaded into resolveChildTargetingRef to skip the redo. (Adjacent to, but distinct from, the parse-helper duplication the author already acknowledged deferring.)🔧 Fix: Remove dead super-cycle guards, thread Java owner resolve
✅ Re-checked - no issues remain.
server/typescript/packages/sdk- 5 pre-existing failures in the sdk package (agent-context-conformancecorpus ×4 +bundled agent-context content×1) are unrelated to this change and not introduced by it. Proven by isolation: reverting only the PR's sdk change (listMetadataFilessort in memory.ts) back to the base commit yields an identical 145 pass / 5 fail. The PR's diff touches no agent-context test; the actual Metadata resolution is load-order-sensitive: dottedextends: Owner.memberto an inherited member fails under Bun (listMetadataFiles doesn't sort files) #188 change lives in the metadata package, which is 100% green (2123/0). Not blocking for Metadata resolution is load-order-sensitive: dottedextends: Owner.memberto an inherited member fails under Bun (listMetadataFiles doesn't sort files) #188; these failures are a stale agent-context corpus/bundle artifact.RED:git show e0334cbb:server/typescript/packages/metadata/src/super-resolve.ts > .../super-resolve.tsthenbun .../order-invariance-demo.ts→ 3/6 orders FAIL with ERR_UNRESOLVED_SUPERGREEN:git checkout HEAD -- server/typescript/packages/metadata/src/super-resolve.tsthenbun .../order-invariance-demo.ts→ 6/6 orders clean, identical modelbun test packages/metadata/test/super-resolve.test.ts -t "#188"(shuffle-invariance + duplicate-failure) → 2/0bun test packages/metadata/test/conformance.test.ts -t "extends-dotted-inherited-member-load-order"(lint + conformance) → 2/0bun test packages/metadata→ 2123 pass / 0 failbun test packages/sdkat HEAD (145/5) and with memory.ts reverted to base (145/5) → isolates the 5 failures from #188manual end-to-end demo scriptorder-invariance-demo.ts: forces all 6 permutations of the 3 real fixture input files via MetaDataLoader + InMemoryStringSource, asserts 0 errors + identical order-insensitive resolved model, plus a negative control asserting each genuinely-broken referring node is reported exactly oncefinalgit status --porcelainempty +git diff HEAD --statempty for both swapped filesdocs/CONFORMANCE.md:17- The aggregate fixture counts and grand total are broadly stale from prior corpus growth, NOT from Metadata resolution is load-order-sensitive: dottedextends: Owner.memberto an inherited member fails under Bun (listMetadataFiles doesn't sort files) #188: the metamodel corpus is 219 actual fixtures vs 91 documented, yaml-conformance is 15 vs 13, render-conformance is 15 vs 14 (verify-conformance 31 matches). Metadata resolution is load-order-sensitive: dottedextends: Owner.memberto an inherited member fails under Bun (listMetadataFiles doesn't sort files) #188 added exactly +1 to the metamodel corpus (218->219 actual). I did NOT bump the documented 91->92 / grand total 181->182 because that would still be ~127 off reality and actively misleading (a reader would assume a freshly-updated count is accurate). A correct fix requires a full recount of all six corpora plus re-verification of the per-port pass/skip ledgers (and the persistence/api-contract rows count scenarios, not top-level dirs) -- a separate maintenance effort that needs a human decision on approach, so I left it untouched rather than half-fix it.✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.