Skip to content

fix(metadata): order-independent super-resolution for dotted extends#189

Merged
dmealing merged 3 commits into
mainfrom
fix/load-order-independent-super-resolution
Jul 9, 2026
Merged

fix(metadata): order-independent super-resolution for dotted extends#189
dmealing merged 3 commits into
mainfrom
fix/load-order-independent-super-resolution

Conversation

@dmealing

@dmealing dmealing commented Jul 9, 2026

Copy link
Copy Markdown
Member

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

  • Deferred super-resolution now resolves on demand with memoization + cycle detection (TS / Python / C# / Java) instead of a single physical-order walk; before a dotted extends: Owner.member reads the owner's effective children(), the owner's own extends chain is resolved first, so a ref to an inherited member succeeds regardless of file enumeration order (resolving Metadata resolution is load-order-sensitive: dotted extends: Owner.member to an inherited member fails under Bun (listMetadataFiles doesn't sort files) #188, where the same corpus passed under Node's readdir order and failed with ERR_UNRESOLVED_SUPER under Bun).
  • After wiring a node, the target's chain is also resolved (multi-level inheritance); an attempted guard 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.
  • Added the extends-dotted-inherited-member-load-order conformance 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-order fixture 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 dotted extends: Customer.id/Customer.pk ref 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.

#188 load-order-independence — runtime: bun 1.3.8

Forcing all 6 permutations of the 3 fixture files:

order                                   | errors | resolved-model ok?
---------------------------------------+--------+--------------------
a-view.json,b-customer.json,c-base.json|      4 | FAILED: [{"code":"ERR_UNRESOLVED_SUPER","path":"$['metadata.root'].children[0]['object.projection'].children[1]['field.uuid']"},{"code":"ERR_UNRESOLVED_SUPER","path":"$['metadata.root'].children[0]['object.projection'].children[3]['identity.primary']"},{"code":"ERR_INVALID_ORIGIN","path":"$['metadata.root'].children[0]['object.projection'].children[1]['field.uuid'].children[0]['origin.passthrough']"},{"code":"ERR_INVALID_ORIGIN","path":"$['metadata.root'].children[0]['object.projection'].children[2]['field.string'].children[0]['origin.passthrough']"}]
a-view.json,c-base.json,b-customer.json|      4 | FAILED: [{"code":"ERR_UNRESOLVED_SUPER","path":"$['metadata.root'].children[0]['object.projection'].children[1]['field.uuid']"},{"code":"ERR_UNRESOLVED_SUPER","path":"$['metadata.root'].children[0]['object.projection'].children[3]['identity.primary']"},{"code":"ERR_INVALID_ORIGIN","path":"$['metadata.root'].children[0]['object.projection'].children[1]['field.uuid'].children[0]['origin.passthrough']"},{"code":"ERR_INVALID_ORIGIN","path":"$['metadata.root'].children[0]['object.projection'].children[2]['field.string'].children[0]['origin.passthrough']"}]
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: [{"code":"ERR_UNRESOLVED_SUPER","path":"$['metadata.root'].children[0]['object.projection'].children[1]['field.uuid']"},{"code":"ERR_UNRESOLVED_SUPER","path":"$['metadata.root'].children[0]['object.projection'].children[3]['identity.primary']"},{"code":"ERR_INVALID_ORIGIN","path":"$['metadata.root'].children[0]['object.projection'].children[1]['field.uuid'].children[0]['origin.passthrough']"},{"code":"ERR_INVALID_ORIGIN","path":"$['metadata.root'].children[0]['object.projection'].children[2]['field.string'].children[0]['origin.passthrough']"}]
c-base.json,b-customer.json,a-view.json|      0 | clean

VERDICT: FAIL — SOME PERMUTATIONS ERRORED, resolved model identical across every order (sha1=15345dc7a50070dcaa81571815c77c5a510a2566).
Resolved (order-insensitive) model — CustomerView.id/pk dotted-extends to Customer's INHERITED (BaseEntity) members succeeded in every order:
["{\n  \"object.entity\": {\n    \"name\": \"BaseEntity\",\n    \"abstract\": true,\n    \"children\": [\n      {\n        \"field.uuid\": {\n          \"name\": \"id\",\n          \"@required\": true\n        }\n      },\n      {\n        \"identity.primary\": {\n          \"name\": \"pk\",\n          \"@fields\": [\n            \"id\"\n          ]\n        }\n      }\n    ]\n  }\n}","{\n  \"object.entity\": {\n    \"name\": \"Customer\",\n    \"extends\": \"acme::common::BaseEntity\",\n    \"children\": [\n      {\n        \"source.rdb\": {\n          \"@table\": \"customers\"\n        }\n  ...

Negative control (Tier-1 invariant unchanged):
  Per-referring-node failure counts (each must be x1):
    ERR_UNRESOLVED_SUPER@$['metadata.root'].children[0]['object.projection'].children[1]['identity.primary']  x1
    ERR_UNRESOLVED_SUPER@$['metadata.root'].children[0]['object.entity']  x1
  VERDICT: PASS — every referring node reported EXACTLY ONCE (no duplicate failures from on-demand recursion); ERR_UNRESOLVED_SUPER still fires.

=== OVERALL: FAIL ===
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 ===

#188 load-order-independence — runtime: bun 1.3.8

Forcing all 6 permutations of the 3 fixture files:

order                                   | errors | resolved-model ok?
---------------------------------------+--------+--------------------
a-view.json,b-customer.json,c-base.json|      0 | clean
a-view.json,c-base.json,b-customer.json|      0 | clean
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|      0 | clean
c-base.json,b-customer.json,a-view.json|      0 | clean

VERDICT: PASS — all 6 permutations resolved with 0 errors, resolved model identical across every order (sha1=15345dc7a50070dcaa81571815c77c5a510a2566).
Resolved (order-insensitive) model — CustomerView.id/pk dotted-extends to Customer's INHERITED (BaseEntity) members succeeded in every order:
["{\n  \"object.entity\": {\n    \"name\": \"BaseEntity\",\n    \"abstract\": true,\n    \"children\": [\n      {\n        \"field.uuid\": {\n          \"name\": \"id\",\n          \"@required\": true\n        }\n      },\n      {\n        \"identity.primary\": {\n          \"name\": \"pk\",\n          \"@fields\": [\n            \"id\"\n          ]\n        }\n      }\n    ]\n  }\n}","{\n  \"object.entity\": {\n    \"name\": \"Customer\",\n    \"extends\": \"acme::common::BaseEntity\",\n    \"children\": [\n      {\n        \"source.rdb\": {\n          \"@table\": \"customers\"\n        }\n  ...

Negative control (Tier-1 invariant unchanged):
  Per-referring-node failure counts (each must be x1):
    ERR_UNRESOLVED_SUPER@$['metadata.root'].children[0]['object.entity']  x1
    ERR_UNRESOLVED_SUPER@$['metadata.root'].children[0]['object.projection'].children[1]['identity.primary']  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)
// End-to-end demonstration for #188: super-resolution must be a PURE FUNCTION
// OF THE SOURCE SET — a dotted `extends: Owner.member` ref to an INHERITED
// member resolves identically regardless of file enumeration order.
//
// This drives the REAL public loader API against the REAL committed conformance
// fixture files (extends-dotted-inherited-member-load-order/input/*.json),
// forcing every one of the 6 permutations of the 3 source files. If resolution
// were order-sensitive (the bug), at least one permutation would surface
// ERR_UNRESOLVED_SUPER; if it were order-independent (the fix), ALL 6 resolve
// cleanly AND yield a byte-identical resolved model.

import { readFileSync } from "node:fs";
import { join } from "node:path";

const WT = "/home/doug/.no-mistakes/worktrees/4a36a911fd68/01KX3QV86JX9Q7N4SXHKDPNPK7";
const SRC = `${WT}/server/typescript/packages/metadata/src`;

const { MetaDataLoader } = await import(`${SRC}/loader/meta-data-loader.js`);
const { InMemoryStringSource } = await import(`${SRC}/loader/meta-data-source.js`);
const { composeRegistry } = await import(`${SRC}/provider.js`);
const { coreTypesProvider } = await import(`${SRC}/core-types.js`);
const { dbProvider } = await import(`${SRC}/persistence/db/db-provider.js`);
const { canonicalSerialize } = await import(`${SRC}/serializer-json.js`);

const runtime = typeof Bun !== "undefined" ? `bun ${Bun.version}` : `node ${process.version}`;
console.log(`#188 load-order-independence — runtime: ${runtime}\n`);

const FIX = `${WT}/fixtures/conformance/extends-dotted-inherited-member-load-order/input`;
const FILES = ["a-view.json", "b-customer.json", "c-base.json"];
const CONTENT: Record<string, string> = Object.fromEntries(
  FILES.map((f) => [f, readFileSync(join(FIX, f), "utf8")]),
);

function permutations(xs: string[]): string[][] {
  return xs.length <= 1
    ? [xs]
    : xs.flatMap((x, i) =>
        permutations([...xs.slice(0, i), ...xs.slice(i + 1)]).map((p) => [x, ...p]),
      );
}

interface ErrShape {
  code?: string;
  source?: { jsonPath?: string };
}

async function loadInOrder(order: string[]) {
  const loader = new MetaDataLoader({
    registry: composeRegistry([coreTypesProvider, dbProvider]),
    strict: true,
  });
  const result = await loader.load(
    order.map((name) => new InMemoryStringSource(CONTENT[name]!, { id: name })),
  );
  const errs = result.errors.map((e: ErrShape) => ({
    code: e.code ?? "?",
    path: e.source?.jsonPath ?? "?",
  }));
  // Order-INSITIVE resolved model: each root child's canonical subtree, sorted.
  // Whole-tree serialization legitimately preserves top-level declaration order,
  // so we compare the SET of resolved subtrees (captures every extends/origin
  // relationship without pinning sequence).
  const subtrees = result.root
    .children()
    .map((c) => canonicalSerialize(c).trim())
    .sort();
  return { errs, model: JSON.stringify(subtrees) };
}

// --- Part 1: every permutation resolves cleanly AND identically ----------------
const orders = permutations(FILES);
console.log(`Forcing all ${orders.length} permutations of the 3 fixture files:\n`);
console.log("order                                   | errors | resolved-model ok?");
console.log("---------------------------------------+--------+--------------------");

const results = await Promise.all(orders.map(loadInOrder));
let allClean = true;
for (let i = 0; i < orders.length; i++) {
  const ok = results[i]!.errs.length === 0;
  allClean = allClean && ok;
  console.log(
    `${orders[i]!.join(",").padEnd(39)}| ${String(results[i]!.errs.length).padStart(6)} | ${ok ? "clean" : "FAILED: " + JSON.stringify(results[i]!.errs)}`,
  );
}

const refModel = results[0]!.model;
let allIdentical = true;
for (let i = 0; i < results.length; i++) {
  const same = results[i]!.model === refModel;
  allIdentical = allIdentical && same;
  if (!same) console.log(`  !! permutation #${i} (${orders[i]!.join(",")}) resolved model DIFFERS`);
}

import { createHash } from "node:crypto";
let fingerprint = createHash("sha1").update(refModel).digest("hex");
const cleanMsg = allClean ? `all ${orders.length} permutations resolved with 0 errors` : "SOME PERMUTATIONS ERRORED";

console.log(
  `\nVERDICT: ${allClean && allIdentical ? "PASS" : "FAIL"} — ${cleanMsg}, ` +
    `resolved model identical across every order (sha1=${fingerprint}).`,
);
console.log(
  "Resolved (order-insensitive) model — CustomerView.id/pk dotted-extends to Customer's " +
    "INHERITED (BaseEntity) members succeeded in every order:\n" +
    refModel.slice(0, 600) + "...\n",
);

// --- Part 2: negative control — Tier-1 error contract unchanged ----------------
// A genuine unresolvable dotted owner ref must STILL report exactly ONE
// ERR_UNRESOLVED_SUPER (the fix does not swallow real errors; failure envelope
// byte-identical). And the on-demand resolver must report it EXACTLY ONCE even
// when the owner is reachable both from the pending loop and via recursion.
const owner = JSON.stringify({
  "metadata.root": {
    package: "p",
    children: [
      { "object.entity": { name: "Owner", extends: "p::DoesNotExist", children: [{ "field.uuid": { name: "id" } }] } },
    ],
  },
});
const view = JSON.stringify({
  "metadata.root": {
    package: "p",
    children: [
      {
        "object.projection": {
          name: "V",
          children: [
            { "source.rdb": { "@kind": "view", "@view": "v" } },
            { "identity.primary": { name: "pk", extends: "p::Owner.pk", "@fields": ["id"] } },
          ],
        },
      },
    ],
  },
});
const negLoader = new MetaDataLoader({
  registry: composeRegistry([coreTypesProvider, dbProvider]),
  strict: true,
});
const neg = await negLoader.load([
  new InMemoryStringSource(view, { id: "view.json" }),
  new InMemoryStringSource(owner, { id: "owner.json" }),
]);
// The no-duplicate guarantee is per-REFERRING-NODE (keyed by code@jsonPath), NOT
// a global count of 1: Owner has a genuinely-broken top-level extends AND V.pk
// has a genuinely-broken dotted ref — two DISTINCT failures. But Owner is
// reachable BOTH from the pending loop AND from V.pk's owner-recursion, so the
// `attempted` guard must ensure Owner's OWN failure is reported EXACTLY ONCE.
const counts = new Map<string, number>();
for (const e of neg.errors as ErrShape[]) {
  const k = `${e.code ?? "?"}@${e.source?.jsonPath ?? "?"}`;
  counts.set(k, (counts.get(k) ?? 0) + 1);
}
console.log("Negative control (Tier-1 invariant unchanged):");
console.log("  Per-referring-node failure counts (each must be x1):");
for (const [k, c] of counts) console.log(`    ${k}  x${c}`);
const dupGuard = [...counts.values()].every((c) => c === 1) && counts.size >= 1;
const stillFires = [...counts.keys()].some((k) => k.startsWith("ERR_UNRESOLVED_SUPER"));
console.log(
  `  VERDICT: ${dupGuard && stillFires ? "PASS" : "FAIL"} — every referring node reported EXACTLY ONCE ` +
    `(no duplicate failures from on-demand recursion); ERR_UNRESOLVED_SUPER still fires.\n`,
);

const overall = allClean && allIdentical && dupGuard ? "PASS" : "FAIL";
console.log(`=== OVERALL: ${overall} ===`);
if (overall !== "PASS") process.exit(1);
- Outcome: ⚠️ 1 info across 1 run (11m36s)

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 - The inProgress cycle 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 AFTER attempted, which is invariantly a superset: both sets are populated together at function entry (TS 256-257; Python 65-66; C# via the two .Add calls at 307-308), and only inProgress is ever removed while attempted is never removed — so inProgress ⊆ attempted always holds and inProgress.has(node) can never be true when the line is reached. attempted alone already prevents infinite recursion (a node on the stack is already in attempted). 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: dotted extends: Owner.member to 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: there resolved and inProgress are 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 in resolvePendingNode at line 310 (to wire the owner's chain first) and again inside resolveChildTargetingRef at 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.

⚠️ **Test** - 1 info
  • ℹ️ server/typescript/packages/sdk - 5 pre-existing failures in the sdk package (agent-context-conformance corpus ×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 (listMetadataFiles sort 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: dotted extends: Owner.member to 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: dotted extends: Owner.member to 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 &gt; .../super-resolve.ts then bun .../order-invariance-demo.ts → 3/6 orders FAIL with ERR_UNRESOLVED_SUPER
  • GREEN: git checkout HEAD -- server/typescript/packages/metadata/src/super-resolve.ts then bun .../order-invariance-demo.ts → 6/6 orders clean, identical model
  • bun test packages/metadata/test/super-resolve.test.ts -t &#34;#188&#34; (shuffle-invariance + duplicate-failure) → 2/0
  • bun test packages/metadata/test/conformance.test.ts -t &#34;extends-dotted-inherited-member-load-order&#34; (lint + conformance) → 2/0
  • bun test packages/metadata → 2123 pass / 0 fail
  • bun test packages/sdk at HEAD (145/5) and with memory.ts reverted to base (145/5) → isolates the 5 failures from #188
  • manual end-to-end demo script order-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 once
  • final git status --porcelain empty + git diff HEAD --stat empty for both swapped files
⚠️ **Document** - 1 info
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

dmealing and others added 3 commits July 9, 2026 11:27
… 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
dmealing merged commit 014953e into main Jul 9, 2026
1 check passed
@dmealing
dmealing deleted the fix/load-order-independent-super-resolution branch July 9, 2026 16:46
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant