Language-neutral fixtures that define the MetaObjects standard's behavior. Every
language port runs every fixture through its own adapter. The corpus is the
contract — see spec/conformance-tests.md.
Fixtures use the canonical metadata format: every node is a one-key map
{ "<type>.<subType>": <body> } (e.g. object.entity, field.long,
identity.primary). Reserved structural body keys are name, package,
extends, abstract, overlay, isArray, and children; everything else is
an @-prefixed attribute in alphabetical order. expected.json and
expected-effective.json are the authoritative reference for the shape — when
in doubt, read one.
Port status: the metamodel loader + canonical serializer corpus is run by four ports — TypeScript, C#, Java, and Python — all green.
Kotlin runs this corpus transitively, not as a separate run — by design.
Kotlin has no distinct loader, parser, or canonical-serializer surface: the
metadata-ktx facade (com.metaobjects.metadata.ktx.Loader) forwards every
call straight to MetaDataLoader in the Java metadata module, which is the
single JVM implementation of the load/parse/canonical-serialize path this corpus
exercises. Java already runs the full corpus against that exact code
(metadata/src/test/java/com/metaobjects/conformance/ConformanceTest.java), so
wiring a parallel Kotlin metamodel-corpus run would re-invoke identical bytecode
through a Kotlin harness — redundant, with zero added coverage. Kotlin's
distinct surfaces (codegen via codegen-kotlin, runtime via Exposed) are gated
by their own corpora (render-conformance, persistence-conformance, and
api-contract-conformance, all of which Kotlin runs as a first-class port). The
shared metamodel vocabulary Kotlin emits is additionally byte-gated by
fixtures/registry-conformance/ (Kotlin is live + green there).
<scenario>/
├── input/*.json # metadata under test (one shared copy)
├── providers.json # optional — provider ids to compose (default: ["metaobjects-core-types"])
├── expected.json # optional — golden canonicalSerialize of the resolved tree
├── expected-effective.json # optional — golden canonicalSerialize of the effective tree
├── expected-errors.json # optional — list of { "code": "ERR_*" } the load must produce
└── script.json # optional — operation-script: API-surface checks
A scenario runs whatever checks its expectation files declare. A fixture with
only expected.json is a behavioral fixture; one with script.json adds
API-surface checks; a fixture may have both.
{
"operations": [
{ "navigate": ["object:Program", "field:weekCount"],
"invoke": "field.is-required",
"expect": { "scalar": false } }
]
}navigate— path segmentstype:name, ortype[subType]for nameless nodes.invoke— a capability-id,<type>.<capability>(kebab-case).args— optional flat map of scalar arguments.expect— a normalized result (see below).
| Kind | Meaning |
|---|---|
{ "names": [...] } |
ordered node-name list |
{ "name": "..." } |
a single node |
{ "absent": true } |
null / None / Optional.empty / undefined |
{ "scalar": <value> } |
string / number / boolean |
{ "subtype": "..." } |
a node's subtype |
{ "effective-tree": "<canonical string>" } |
a resolved subtree, canonical-serialized |
{ "error": { "code": "ERR_*" } } |
the invocation surfaced an error |
Fixtures assert error codes, never message prose. Codes are registered in
ERROR-CODES.json. Adding a code is an additive edit to that file.
error-filterable-array-field— afield.string @isArray @filterablefailsERR_FILTERABLE_UNSUPPORTED_SUBTYPE: no FR-009 operator (eq/ne/gt/gte/lt/lte/in/like/isNull) applies to a collection column.error-sortable-array-field— the same shape with@sortablein place of@filterablefailsERR_SORTABLE_UNSUPPORTED_SUBTYPE: a collection column has no ordering.
A structural scan of the whole repo (1321 JSON + 124 YAML files) found zero
fields carrying both isArray: true and @filterable/@sortable: true before
these two fixtures were added — they are the only cases in the corpus that
exercise an array field through the filter/sort tier, so they are what makes the
rule (added to the TypeScript loader for #335) a cross-port contract rather than
a TS-only unit test.
origin.aggregate @agg:collect with @of omitted is a whole-object rollup: the
carrying field must be a field.object @isArray @objectRef, and the origin
assembles the related rows as an array of that declared value object — each
member matched by name against the @via path's TERMINAL entity — instead of
an array of one scalar column.
collect-whole-object— the positive case: afield.object @isArray @objectRefcollects the related entity's rows as an array of the declared value object. This is the corpus's only whole-object rollup — every other@agg:collectfixture (origin-agg-collect,error-agg-collect-*) collects a single scalar column via@of.error-collect-no-object-ref— the carrying field declares no@objectRef. GatesERR_INVALID_ORIGIN(the whole-object-rollup rule itself) andERR_OBJECT_FIELD_WITHOUT_OBJECT_REF(afield.objectalways requires@objectRef, independent of collect) — both fire on the same shape, so both are asserted.error-collect-ref-not-value—@objectRefresolves to anobject.entityinstead of anobject.value. GatesERR_SUBTYPE_RULE_VIOLATION(#210, ADR-0028: a whole-object rollup must target a value).error-collect-no-via—@viais absent, and with no@ofthere is no entity to infer the relationship from. GatesERR_INVALID_ORIGIN.error-collect-via-to-one— every hop on@viais to-one (a passthrough shape, not a rollup). GatesERR_ORIGIN_CARDINALITY.error-collect-distinct—@distinctis declared on a whole-object rollup (a no-op once the value object carries the primary key). GatesERR_INVALID_ORIGIN.error-collect-member-unresolved— a declared value-object member has no matching field, by name, on the@viaterminal entity. GatesERR_COLLECT_MEMBER_UNRESOLVED— failing open here is how #270 turned a curated value object into the full entity, invisible in a diff.error-collect-member-type-mismatch— a declared value-object member'sfield.<subType>differs from the matched terminal-entity field's. GatesERR_INVALID_ORIGIN(a whole-object rollup preserves each member's declared type).error-collect-orderby-not-terminal—@orderBynames a field that exists on a middle hop of a multi-hop@viapath but not on the path's TERMINAL entity. GatesERR_INVALID_ORIGIN—@orderBykeys always resolve against the terminal, never the head or an intermediate hop.
Create a directory; add input/ and expectation files. No runner code changes —
discovery is automatic. A new fixture a port cannot yet pass goes in that port's
conformance-expected-failures.json ledger.
A corpus that stops exercising a path emits no diagnostic — removing a fixture is silent. So every removal records what it covered and where that coverage went.
FR-037 R2 (#336) — origin.collection retired to reserved-not-registered:
| Removed | What it covered | Where the coverage lives now |
|---|---|---|
origin-collection-simple |
the subtype parses + round-trips on a projection host | nothing to cover: the subtype is deregistered. Replaced by error-origin-collection-retired, which asserts the same declaration on the same legal host now fails ERR_UNKNOWN_SUBTYPE — the retirement is proved where the use was previously VALID |
error-value-origin-collection |
#210 — an assembly origin is illegal on an object.value host |
its three siblings, all present and green: error-value-origin-aggregate, error-value-origin-computed, error-value-origin-first. The rule is a property of the shared ASSEMBLY_ORIGIN_SUBTYPES constant, not of a per-subtype branch |
Coverage genuinely lost, stated rather than faked: flattened-kitchen-sink
dropped its supplierBriefs field, the corpus's only array-of-value-object
carrying an origin. No surviving origin expresses a whole-object rollup along a
relationship — origin.aggregate @agg:collect reduces a column via @of. RESTORED by #335,
which makes @of optional on collect — see "Whole-object rollup (#335)" below.
The coverage lives in a DEDICATED fixture, collect-whole-object, rather than
back in flattened-kitchen-sink: that fixture's concern is @storage: flattened,
and a whole-object rollup exercises none of it, so folding the two would make
each harder to read in a five-language investigation. collect-whole-object is
an array-of-value-object carrying an origin — the exact shape recorded as lost —
and it is round-tripped against real Postgres and SQLite in
server/typescript/packages/integration-tests/test/view-lifecycle-{pg,sqlite}.test.ts.
generated-* directories are produced by the differential fixture generator
(conformance generate <corpusRoot> <count> [startSeed]). Each fixture's
expected.json golden is captured from the TS reference implementation at
generation time (spec §7.2).
Important (spec §7.5): a port diverging on a generated-* fixture does
not automatically mean the port is wrong. The TS reference is the oracle only
insofar as it is correct. If a port disagrees with a generated golden,
investigate both sides: the port may be right and the TS reference may have a
bug. Do not auto-conform ports to a generated golden without understanding the
divergence.