Repository navigation
Metadata resolution is load-order-sensitive: dotted extends: Owner.member to an inherited member fails under Bun (listMetadataFiles doesn't sort files) #188
Description
Summary
Metadata resolution is load-order-sensitive: a dotted extends: Owner.member reference (FR-024 child-targeting ref) that targets an inherited member fails with
the SuperClass 'pkg::Owner.member' does not exist (referenced by <node>)
whenever the referring file is loaded before the owner entity's file. Because the SDK loader (loadMemory → listMetadataFiles) enumerates files in raw readdir order, the same metadata set resolves on one runtime/filesystem and throws on another. In practice this passes under Node (whose readdir here returns owner-before-view) and fails under Bun (view-before-owner) — so a project builds locally under Node but breaks in a Bun-based CI/container (e.g. oven/bun:1, whose #!/usr/bin/env node shim runs the meta bin under Bun).
This is two stacked issues:
listMetadataFilesnever sorts files — only subdirectories — contradicting its own docstring.resolveDeferredSupersresolves dotted refs in a single load-order walk, so a dotted ref to an inherited member only succeeds if the owner's ownextendschain happens to be wired first.
The docstring at resolveSuperRef explicitly says inherited children should resolve, so #2 is an unintended order-dependency, not a metadata-authoring error.
Environment
@metaobjectsdev/metadata0.15.14,@metaobjectsdev/sdk(via@metaobjectsdev/cli0.15.16); reproduced againstmain(source refs below).- Runtime differentiator: Node vs Bun
readdirordering. Themetabin is#!/usr/bin/env node; a base image with no realnode(e.g.oven/bun:1) runs it under Bun.
Minimal repro
Three files under a metaobjects/ dir (generic names):
# common/base.yaml
metadata:
package: acme::common
children:
- object.entity:
name: BaseEntity
abstract: true
children:
- field.uuid: { name: id, required: true }
- identity.primary: { name: pk, fields: ["id"] }# shop/customer.yaml
metadata:
package: acme::shop
children:
- object.entity:
name: Customer
extends: acme::common::BaseEntity # inherits pk
children:
- field.string: { name: name, required: true }
- source.rdb: { table: customers }# shop/customer-view.yaml
metadata:
package: acme::shop
children:
- object.projection:
name: CustomerView
children:
- source.rdb: { kind: view, table: v_customers }
- identity.primary:
name: pk
extends: "acme::shop::Customer.pk" # dotted ref to an INHERITED member
fields: ["id"]- Load with
loadMemorywherereaddiryieldscustomer-view.yamlbeforecustomer.yaml(raw filesystem order — what Bun returns): throwsthe SuperClass 'acme::shop::Customer.pk' does not exist. - Same three files where
readdiryieldscustomer.yamlfirst (what Node returns here): resolves clean.
Empirically, on one machine with identical files + pinned versions:
node node_modules/.bin/meta gen→ successbun node_modules/.bin/meta gen→ the error above
If Customer declares pk directly (no extends), it resolves in either order — the failure is specific to a dotted ref targeting an inherited member.
Root cause (source refs, main)
Bug 1 — files not sorted. server/typescript/packages/sdk/src/memory.ts, listMetadataFiles (≈L155–178):
// docstring (L148): "Files within a directory are sorted alphabetically for deterministic load order"
for (const entry of entries) { // entries = await readdir(dir) — RAW order
...
else if (s.isFile() && isMetadataFile(entry)) paths.push(full); // files pushed in readdir order
}
for (const sub of subdirs.sort()) { // only SUBDIRS are sorted
paths.push(...(await listMetadataFiles(sub)));
}
return paths; // `paths` (files) never sortedThe docstring promises a deterministic alphabetical file order; the code delivers raw readdir order. (The metadata package's own DirectorySource in loader/sources/directory-source.ts does sort by basename — the SDK loader diverged from it.)
Bug 2 — order-sensitive dotted-ref resolution. server/typescript/packages/metadata/src/super-resolve.ts:
resolveDeferredSupers(L226) does a singlewalk(root, …)resolving each node'ssuperRefin physical/declaration (= load) order.- The dotted branch of
resolveSuperRefselects the final segment againstowner.children()(L157/L163) — effective children, which include inherited members only once the owner's ownextendschain is already wired. - Docstring at L117–121 states the dotted ref resolves "among the owner's EFFECTIVE children (so inherited children resolve)" — i.e. inherited resolution is the intended contract.
So if the walk reaches the referring node before the owner entity's super is resolved, owner.children() doesn't yet contain the inherited member and the ref throws. Load order decides the outcome.
Expected behavior
Resolving a metadata corpus should be a pure function of the set of source files, independent of enumeration/load order. A dotted extends: Owner.member targeting an inherited member should resolve regardless of the order the owner and referrer files are loaded.
Proposed fix
Durable (order-independent resolution). Make super-resolution independent of walk order: when resolving a dotted ref against an owner, ensure the owner's own extends chain is resolved first — e.g. resolve supers on demand with memoization + cycle detection (topological), rather than a single pre-order walk. Add a shuffle-invariance regression test: permute the input source order (and/or a Node-vs-Bun readdir shim) and assert an identical resolved model. This eliminates the whole class, not just the pk case.
Floor (deterministic enumeration). Independently, listMetadataFiles should sort files to match its docstring (paths.sort() before recursing, mirroring DirectorySource). This makes load order deterministic across runtimes/filesystems even before the resolver change lands.
Cross-port note: whichever lands, the same invariant should hold across all ports (Java/TS/Python/C#) and be covered by the shared conformance suite so a port can't silently disagree on ordering semantics.
Impact
Any adopter using dotted extends: Owner.member where Owner inherits the member (a common pattern for projections/views over entities that extend a shared abstract base) is exposed: green under Node, hard build failure under Bun — with an error that points at the metadata (SuperClass … does not exist) rather than the framework, so it reads as an authoring bug.
Activity
fix(metadata): order-independent super-resolution for dotted extends (#…
chore(release): 0.15.17 (npm) / 0.15.10 (PyPI) / 0.15.8 (NuGet) / 7.7…
dmealing commentedon Jul 13, 2026
Fixed and shipped in 0.15.17 (npm) / PyPI 0.15.10 / NuGet 0.15.8 / Maven 7.7.8.
Super-resolution is now a pure function of the source set, not of directory-scan order: a dotted
extends: Owner.membertargeting an inherited member resolves regardless of load order (the Node-vs-Bunreaddirdivergence that produced the spuriousERR_UNRESOLVED_SUPER). Fixed in all five ports via on-demand memoized resolution. See the 0.15.17 CHANGELOG entry.