A structural gate for the cross-language contract that the metamodel subtype
vocabularies must be identical across languages (see CLAUDE.md →
"Cross-language porting"). Each of the five ports (TypeScript, C#, Java, Kotlin,
Python) walks its own type registry and serializes the logical metamodel
vocabulary as a canonical, fully-sorted, byte-stable JSON manifest. A
conformance runner asserts that manifest is byte-identical to the single
committed canonical, expected-registry.json.
The "vocabularies identical across languages" rule was previously enforced only
by manual discipline ("add to the TS constants first, then mirror in the other
ports") plus incidental behavioral coverage — a drifted attr was caught only
if some fixture happened to exercise it. That misses things: the SP-C work found
the Java validator vocabulary had drifted for weeks (@mask vs @pattern, a
NumericValidator with no min/max, an ArrayValidator with @minSize, and the
Python loader not registering length/regex/numeric/array at all) — all
silent, because no behavioral fixture exercised that vocabulary. There was no
test comparing the registries themselves. This is that test.
This is the same proven pattern as render-conformance and
output-prompt-conformance: byte-exact to a single committed canonical —
strongest signal, simplest model, no escape-hatch ledger. The logical
vocabulary must have zero per-port divergence by contract.
The manifest carries the cross-port logical vocabulary plus (FR-033 Task 5)
the documentation surface (per-type / per-attr description, optional rules /
example / whenToUse) and the structural constraint graph (each type's
children from its child rules — with optional cardinality — and optional
parents).
- 2-space indentation.
- Object key order is fixed by construction:
types,commonAttrs,defaultSubTypes.- Each attr:
name,valueType,isArray,required,description, then optionalrules,example,whenToUse(each omitted when absent). - Each type:
type,subType,description, then optionalrules,example,whenToUse(omitted when absent), thenattrs,children, then optionalparents(omitted when absent/empty). - Each child (constraint graph):
childType,childSubType,childName, then optionalmin,max,named(each emitted ONLY when defined on the rule — legacy wildcard rules omit all three;max: nullis emitted when the rule sets it null, omitted when undefined).
- Each attr:
- Everything sorted (locale-independent, ASCII codepoint compare):
typesby"<type>.<subType>"; eachattrsarray byname; each type'schildrenby the tuple(childType, childSubTypeKey, childName)wherechildSubTypeKeyis the string or the comma-joined list;parentsascending;commonAttrsbyname;defaultSubTypeskeys sorted. valueTypeis the attr's scalar value-type (string/int/boolean/…), ornull(literal) for polymorphic/untyped attrs (e.g.@default, whose value type follows its owning field's subtype).isArrayis a boolean:truefor an array-valued attr (a list of the scalarvalueType),falseotherwise. Array-ness is modeled as a single orthogonal axis — there is nostringarrayvalue-type token and noattr.stringarraysubtype. A polymorphic attr (valueType: null) isisArray: false.descriptionis required and non-empty on every type and attr — a coverage gate (registry-conformance.test.ts) hard-fails CI on any empty/missing description (mirrors ADR-0023 strict provenance for the documentation surface). Generic descriptions are acceptable for now; FR-033 Phase 2 improves the non-fieldproviders and regenerates.- A single trailing newline.
FR-033 Task 5 grew the canonical (descriptions + constraint graph). This is a purely-additive growth (every pre-existing field —
type/subType, attrname/valueType/isArray/required— is byte-unchanged; only new keys were added). Per the fix-at-source rule below, the grown canonical RED-flags the other four ports' registry-conformance until they reconcile in Phase 4 — the intended, documented intermediate state (the same lifecycle as the FR-032 sweep). The TS reference is correctly advanced; the ports reconcile later.
The TS emitter (server/typescript/packages/metadata/src/registry-manifest.ts,
emitRegistryManifest) is the reference implementation; the TS constants are
the documented source of truth.
The guiding rule: include a facet only if it is part of the cross-port logical contract AND all five ports can emit it identically. A smaller airtight manifest beats a bigger flaky one — when unsure, exclude from v1 and document the deferral.
How the in/out decision is made (Wave 3b — principled, not a tautology). The
boundary used to be a bare exclusion name-list: an attr was OUT iff its name
happened to be hand-added to the set, IN otherwise. That is a tautology — it
records the names someone remembered, and the next port-private attr nobody adds
silently enters the cross-port contract. The decision is now an explicit
classification (classifyPerTypeAttr / classifyTypeSubType, uniform across
all four emitters — TS / C# / Java / Python; Kotlin reuses the JVM emitter). Each
per-type facet the emitter encounters resolves to exactly one of:
INCLUDED— logical cross-port vocabulary (this is what the gate measures; MUST be identical in every port). Note "logical" is not "non-physical": the physical-DB attrscolumn/dbColumnType/db.indexed/precision/scale/maxLength/uniqueARE logical here — they are the agreed cross-port persistence vocabulary (every port emits the same DDL contract from them). The real axis is cross-port-CONTRACT vs port-PRIVATE-mechanism, not abstract-vs-physical.- An
ExclusionReason— carved OUT of the contract for a declared category, never a bare name. The categories are:NATIVE_BINDING(factories / native type classes / ADR-0001@object/ ADR-0005@objectAdapter),STRUCTURAL_KEYWORD(isArray/isAbstract/extends/implements/isInterface),COMMON_ATTR_DUP(descriptionre-registered per-type — it lives in thecommonAttrsblock),INHERITANCE_ANCHOR(metadata.base),PRESENTATION_ONLY(the genericview.*controls).
The classification is total — there is no "unclassified" third state that
silently defaults to IN, so a port can never let an unreasoned facet through
undetected (classifyPerTypeAttr returns INCLUDED or a reason, and every port's
conformance runner asserts the classification is total and that each carve-out
carries its declared reason).
Why inclusion-by-default is sound rather than the old tautology: ADR-0023
seals each port's composed metamodel registry after the agreed-provider
bootstrap, so every registered (type, subType, attr) is, by construction,
deliberately-agreed metamodel vocabulary — there is no accidental registration to
silently let in. The classification's job is therefore to carve the small,
declared set of agreed-but-port-private facets OUT of that agreed vocabulary,
each with a reason; the cross-port byte-canonical is the backstop that a carve-out
hasn't gone stale (a dead rule would surface as a manifest diff).
The manifest measures each (type, subType) row's own attr set as physically
present on that registry row — i.e. the resolved attrs at that row, NOT a
separately-computed inheritsFrom walk. The reference port (TS) registers the
full common attr set directly onto the base AND every concrete leaf (the
registration loops in core-types.ts do this), so each row is self-contained and
the manifest can read each row's own attrs uniformly. The JVM port resolves
getChildRequirements() (which includes inherited requirements) to the same
own-resolved set, de-duped by name. Consequence, made explicit so it is not read
as an inconsistency:
- A family whose shared attrs are registered on the base (e.g.
field.*,relationship.*,object.*) shows those attrs on the base row AND every leaf row (each row carries its complete resolved set). - A family whose attrs are declared only on the concrete subtype (e.g.
source.rdb,template.output, theorigin.*leaves) shows an empty base row and the attrs on the leaf.
Both are the same rule — "emit each row's resolved own-attrs" — applied to two
different (and legitimate) registration choices. inheritsFrom itself is NOT
surfaced (it remains the deferred facet below): the manifest deliberately does not
try to attribute an attr to its declaring level, only to report the resolved set
present at each level, which every port computes identically and the byte-gate
verifies.
- Every registered
(type, subType), each with itsdescription(FR-033 — required + non-empty) and optionalrules/example/whenToUse. - Each type's declared attrs as
{ name, valueType, isArray, required, description, rules?, example?, whenToUse? }(valueType: nullfor polymorphic/untyped attrs;isArray: truefor array-valued attrs — the scalarvalueTypeplus the orthogonal array flag, NOT a conflatedstringarraytoken, and NOattr.stringarraysubtype;descriptionrequired + non-empty; the rest emitted only when present). - Each type's structural constraint graph (FR-033):
children— the type's child rules as{ childType, childSubType, childName, min?, max?, named? }(cardinality emitted only when defined on the rule;childSubTypemay be a single subtype,"*", or a list) — and optionalparents(the child-side placement claim, emitted only when present + non-empty).
index.lookup+identity.secondarykey-purity (ADR-0040).identity.secondaryis a unique alternate key — its@uniqueattr is removed (uniqueness is the type's meaning; a legacy@uniqueisERR_UNKNOWN_ATTR). Non-unique retrieval is the separateindextype; onlyindex.lookupis registered.index.fulltext/index.vector/index.spatialare reserved on the axis but NOT registered (added additively when a real adopter needs one — an unregistered subtype fails closed rather than silently no-op'ing). The physical index escapes (@using/@expr/@where/@orders) are contributed by the db provider to bothindex.lookupandidentity.secondary.
commonAttrs— theregisterCommonAttribute/registerCommonAttrsset (the doc attrs:aliases,deprecated,description,notes,replacedBy,seeAlso,title), each carrying itsdescription.defaultSubTypes— the per-type designated default subType for bare-key YAML authoring sugar (metadata→root,object→entity).
Each bullet below is one of the ExclusionReason categories above. They are
not "things whose names we listed" — they are facets that are genuinely
per-port mechanism rather than cross-port contract, and the emitter classifies
them as such with a declared reason.
NATIVE_BINDING— Node factory /NodeConstructor/ JavaClass; native type bindings (JavaDataTypesvalue-class, TS native TS-type, EF/CLR types, the coarseDataTypeclassification); and the per-port type-binding facets@object+@objectAdapter—@objectis the ADR-0001 class-FQN type binding for OO ports (the Java runtime resolves an object's native class from this attr) and@objectAdapteris the ADR-0005 hybrid value-access seam. All are per-port BINDING mechanisms, not cross-port logical vocabulary. Java registers@object/@objectAdapteras per-type attrs onobject.*(load-bearing runtime mechanisms read by the value-access representation, IO readers, and OMDB — kept registered in Java); the classification carves them out of the manifest (uniform across all four emitters; a no-op for TS/C#/Python, which never register them). See SP-G Unit 6b-finish.- Codegen targets/options; the TS-only
D1dialect and any other documented port-unique surface; ordering (we sort everything) — all out of scope. STRUCTURAL_KEYWORD(isArray,isAbstract,extends,implements,isInterface) — bare structural / OO-shape body keywords (peers ofname/children), not attributes. Java additionally registers them as ordinary per-type attrs (inherited everywhere); the other ports do not. The classification carves them out of each type'sattrslist (uniform across all four emitters; a no-op for TS/C#/Python). See SP-G analysis C-2/C-3.COMMON_ATTR_DUP(description) —descriptionis acommonAttr(emitted in thecommonAttrsblock), not a per-type attribute. Java registers it per-type too (a duplicate); the classification carves out the per-type occurrence (it stays incommonAttrs). See SP-G analysis C-3.INHERITANCE_ANCHOR(metadata.base) — Java registers an internal abstract anchor (metadata.base) that all types inherit from; the other ports register only the concrete tree root (metadata.root). It is the not-universally-trackedinheritsFromanchor this manifest already defers, so the(metadata, base)row is carved out. See SP-G analysis C-5.PRESENTATION_ONLY— the 13 genericview.*controls (checkbox,date,dropdown,hidden,hotlink,image,month,number,password,radio,text,textarea,web) — a TS-web-PRESENTATION facet, like the TS-onlyD1dialect. They have zero backend / codegen / render consumers; only the TS web client (client/web/packages/{tanstack,angular}/src/) + TS form codegen (server/typescript/packages/codegen-ts/src/templates/field-meta.ts) consume them. They are registered in every port — TypeScript, C#, Java (so Kotlin) and Python — so one metadata document loads everywhere (an authoredview.dropdownis not anERR_UNKNOWN_SUBTYPEin a backend port; gated by theview-text-basicconformance fixture), and excluded from the manifest by all four emitters, because registration there is for LOADING only. Onlyview.base+view.currency(the cross-port currency@localewire contract) are manifest rows. See SP-G analysis B-2; C#, Java and Python originally left them unregistered, which made a document shared between a TS web client and a backend port unloadable in the backend.
These were evaluated against the three "open boundary calls" in the design spec and excluded because they are not tracked identically on all five registries. They are intentional deferrals, to be revisited as a follow-on — NOT silent omissions.
-
allowedValues/defaulton attr schemas. TS, C#, and Python carry both on theirAttrSchema. Java does not: in Java an attribute is modeled as aChildRequirement(expectedType="attr"), which has noallowedValues/defaultfields. Including these would require a per-port-conditional manifest (forbidden) or a Java registry change beyond the scope of a detection gate. Still excluded. -
Superseded by FR-033. The manifest now emits a type'sinheritsFrom(the declared parenttype.subType).parents(the FR-033 child-side placement claim) whenever theTypeDefinitiondeclares them. This is the cross-port additive grammar (an extension provider names a parent without editing the parent's definition), not the prior Java-onlyparentType/parentSubTypeinheritance facet. The TS reference advances first; the other ports reconcile in Phase 4. -
Now INCLUDED (FR-033 Task 5). The manifest emits each type'schildRules(structural child-type vocabulary).children—{ childType, childSubType, childName, min?, max?, named? }, sorted and with cardinality emitted only when defined on the rule. The TS / C# / Python cleanChildRulemodel maps directly; reconciling Java's conflatedChildRequirementlist (where an attr is aChildRequirementwithexpectedType="attr"and a child-type rule is one withname="*", alongside placement/validation-constraint variants) to the same{childType, childSubType, childName, …}shape is the cross-port reconciliation work for the JVM ports in Phase 4 (the grown canonical RED-flags them until then).
When a port's emitter first runs, it may NOT match the canonical — that is the point (this is a drift-finding gate). Each mismatch is a real registry divergence:
- Fix the diverging port's registration to match the cross-port contract — do NOT loosen the canonical to accommodate drift.
- The only exception: if a divergence reveals that TS itself is wrong
versus the documented contract (TS is the reference, but it is not infallible),
fix TS, regenerate
expected-registry.json, and re-verify every port. Note the escalation. - Never make the manifest per-port-conditional. A facet that cannot be emitted identically by all five ports is excluded from v1 and documented here (see above), never fudged.
| Port | Status | Runner | Runs in scripts/ci-local.sh via |
|---|---|---|---|
| TypeScript | live + green (reference emitter; produces the canonical) | packages/metadata/test/registry-conformance.test.ts |
its ts-fast section (scoped bun test) |
| C# | live + green (byte-identical) | MetaObjects.Conformance.Tests/RegistryManifestConformanceTests.cs |
its csharp section (whole MetaObjects.Conformance.Tests project) |
| Python | live + green (byte-identical) | tests/conformance/test_registry_conformance.py |
its python section (the full tests/ suite) |
| Java | live + green (byte-identical; reconciled SP-G Units 4-7, gate re-enabled Unit 8) | metadata/src/test/java/com/metaobjects/registry/RegistryManifestConformanceTest.java |
its java section (in the metadata -Dtest= list) |
| Kotlin | live + green (byte-identical; composes the metamodel provider set) | codegen-kotlin/src/test/kotlin/com/metaobjects/generator/kotlin/RegistryManifestConformanceTest.kt |
its java section (in the codegen-kotlin -Dtest= list) |
scripts/ci-local.sh is where each port's runner is wired (its ts-fast, csharp, java-fast — Java and Kotlin — and python lanes); .github/workflows/conformance.yml is a thin wrapper that calls the script once per lane, and --quick covers TypeScript only. TS / C# / Python were live from the start; Java + Kotlin were re-enabled in SP-G Unit 8 after the Java metamodel-vocabulary reconciliation (Units 4-7) landed (see the divergence analysis:
docs/superpowers/specs/2026-06-02-sp-g-java-registry-divergence-analysis.md and the
reconciliation plan).
Why the Java + Kotlin runners compose from a defined provider set. Both JVM
runners build their registry from the explicit metamodel provider set
(RegistryManifest.composeMetamodelRegistry() — mirroring the TS reference's
composeRegistry(coreProviders)), NOT from the process-global
MetaDataRegistry.getInstance() singleton. The metadata module's classpath holds
only the metamodel providers, but a downstream module that also runs the gate
(codegen-kotlin) has the om + codegen-base modules on its test classpath —
whose SPI providers register an extra object.managed subtype and ~22
codegen-tooling attrs (ai* / json* / has*, self-registered by the doc
generators MetaDataAIDocumentationGenerator / MetaDataFileJsonSchemaGenerator).
Those are per-port codegen tooling, NOT the cross-port logical metamodel
vocabulary the gate measures (the same category as the EXCLUDED native type
bindings / codegen targets). Composing from the defined provider set makes every
module's runner measure the identical vocabulary — the gate stays meaningful (it
still catches a real attr/subtype divergence in any metamodel provider) while
being immune to classpath pollution.
Detail per port:
- TypeScript — reference emitter; green (produces the canonical).
- C# — green, byte-identical.
- Python — green, byte-identical (
emit_registry_manifestinserver/python/src/metaobjects/registry_manifest.py; runnertests/conformance/test_registry_conformance.py). Running the gate surfaced targeted drift (now reconciled at source): the attr value-type /attr.*subtype spelledstringArrayinstead of the cross-portstringarray; missing field attrsdb.indexed/readOnly(andcurrencyonfield.currency); missing subtypesfield.base|byte|short,object.base,template.base, and 9view.*control kinds; missing attrsdiscriminator/discriminatorValueonobject.*, the M:N slim vocabularycardinality/through/sourceRefField/symmetric/objectRef(FR-018 — the removedjoinEntity/joinFieldsare no longer in the canonical) onrelationship.*,parameterRefonsource.rdb,uniqueonidentity.secondary,filterableonlayout.dataGrid,localeonview.currency; andtemplate.*payloadRefdeclared optional + carried ontemplate.base(the shared attrs now live only on the concrete subtypes withpayloadRefrequired, matching TS — the manual prompt-payloadRef check was removed in favor of the generic required-attr schema check). Two dead, never-registered constants (RELATIONSHIP_ATTR_FK_FIELD/_PARENT_FIELD) were removed. No structural (Java-class) divergence — Python uses the same attr vocabulary and registry model. - Java + Kotlin (shared JVM emitter) — emitter (
RegistryManifest.emit,metadatamodule) + runners (RegistryManifestConformanceTestinmetadataandcodegen-kotlin) are live + green (both re-enabled in SP-G Unit 8; each composes from the defined metamodel provider set — see "Why the Java + Kotlin runners compose from a defined provider set" above). The historical divergence inventory below records what the gate surfaced and how it was reconciled. SP-G Phase 1 (Units 2-3) settled three of the divergences cross-port via the uniform emitter exclusions documented in "EXCLUDED" above (no Java behavior change): the structural keywordsisArray/isAbstract+ thedescriptionper-type duplicate are filtered from theattrslist, themetadata.baseanchor row is skipped, and the genericview.*controls are cut from the manifest (every port registers them for loading only). The REMAINING (Phase 2) divergence between Java's registry and the cross-port logical vocabulary that TS, C#, and Python agree on: Java carries a parallel physical-DB attr vocabulary (dbType/dbIndex/dbLength/dbNullable/dbForeignKey/dbPrecision/dbScale/dbUnique/previousName/dbSequenceName/dbIndexName/dbTablespace) instead ofcolumn/db.indexed/dbColumnType+maxLength/precision/scale/unique; is missing the logical field attrsautoSet/filterable/sortable/sortableDefaultOrder/readOnly/storage; carries Java-specific feature attrs the contract has no peer for (minLength/pattern/maxValue/minValue/format/dateFormat/maxDate/minDate/defaultView, validatormsg/mask/maxSize/minSize); modelsobject.*with Java OO attrs (extends/implements/object/objectAdapter/isInterface/value*/data*) instead ofdiscriminator/discriminatorValue. (The structural keyword /description/metadata.base/ genericview.*divergences were settled in Phase 1 — see above.) Reconciling this at source means rewriting Java's metamodel attribute layer to the cross-port vocabulary — a change that ripples through the loader's validation, OMDB,codegen-spring, andcodegen-kotlin(all consume the current Java attrs). It is tracked as a dedicated follow-on, NOT a silent omission, and the canonical is not edited (it is correct). Update (Phase 2 complete): every divergence above has since been reconciled unit-by-unit — the final piece, the physicaldb*vocabulary, was converged in SP-G Unit 7 (the logical-equivalent attrsdbType/dbIndex/dbLength/dbNullable/dbPrecision/dbScale/dbUniquemapped onto the cross-portdbColumnType/db.indexed/maxLength/required-ness/precision/scale/unique- owned-object
@storage="jsonb", with consumers inomdb+codegen-mustachemigrated; the DDL/migration-only remnantsdbForeignKey/previousName/dbIndexName/dbSequenceName/dbTablespacedropped as dead under ADR-0015). The Java manifest now byte-matches the canonical (residual EMPTY); SP-G Unit 8 re-enabled the gate (dropped@Ignore/@Disabled), constrained both JVM runners to the defined metamodel provider set, and wired both into CI.
- owned-object
The canonical is generated from the TS reference emitter. After an intended TS registry change, regenerate and reconcile the other ports:
import { composeRegistry, coreProviders, emitRegistryManifest } from "@metaobjectsdev/metadata";
process.stdout.write(emitRegistryManifest(composeRegistry(coreProviders)));(write the output to fixtures/registry-conformance/expected-registry.json),
then run every port's registry-conformance runner to confirm they still
byte-match — reconciling any newly-surfaced divergence at the source.
The registry-conformance gate above proves the vocabulary is identical across
ports. It does NOT prove the vocabulary is exercised at all — a registered
(type, subType) or attr that no fixture ever uses is only caught incidentally,
which is exactly the meta-gap that let the SP-C drift hide for weeks (a drifted
member was invisible because no behavioral fixture happened to touch it).
The coverage report closes that gap. It cross-references the registered
vocabulary (this manifest) against the conformance fixture corpora and
surfaces every registered (type, subType) — and each declared attr on an
exercised subtype — that NO fixture exercises.
- Module:
server/typescript/packages/metadata/src/registry-coverage.ts(pure + testable:computeCoverage(manifest, corpusRoots)). - Test:
server/typescript/packages/metadata/test/registry-coverage.test.ts— scansfixtures/conformance/(the primary vocabulary exerciser) unioned with the render / persistence / api-contract / output-prompt / extract corpora (so a member exercised in any corpus is not falsely flagged untested). A fixture node keyed"<type>.<subType>"exercises that subtype; an@attr(or reserved bare key) set on it exercises that attr;childrenare walked recursively. - Snapshot:
coverage-report.json— a sorted, deterministic, committed snapshot of the untested sets + counts.
Monotonic ratchet — hard-fails on a coverage REGRESSION (Wave 4a). The
untested set today is a legitimate pre-existing backlog — many subtypes
(view.* controls, the validator.* family, the abstract *.base anchors, the
attr.* value-type subtypes) and many attrs are not yet exercised by a fixture.
Hard-failing CI on that whole backlog would block everything for no gain. So the
committed coverage-report.json is treated as a baseline, not an exact
expectation, and the test enforces a one-way ratchet: coverage may improve
freely but must never regress. The build hard-fails when
- a registered
(type, subType)that the baseline had exercised becomes unexercised (a NEW entry inuntestedSubTypes), or - an attr on an exercised subtype that the baseline had exercised becomes
unexercised (a NEW entry in that key's
untestedAttrs).
Adding an exercising fixture (an item LEAVES an untested set) is an improvement and is always allowed — the test prints a hint suggesting you tighten the baseline so a future regression of the newly-exercised item is caught too.
The comparison is set-based, not count-based: an integer-only check would let
a regression hide behind a simultaneous improvement (one item newly exercised, a
different one regressing, with the net count unchanged). The ratchet compares the
untested sets directly, so each individual regression is named in the failure
message (which item regressed + how to fix it). Implemented by checkRatchet /
formatRatchetFailure in
server/typescript/packages/metadata/src/registry-coverage.ts; a self-contained
negative test feeds a synthetic regression and asserts the ratchet bites, so the
gate is provably live (not vacuous). It runs in ONE port (TS reference) — the
coverage measurement is over the shared canonical vocabulary, so the ratchet is
not fanned into all five runners.
When coverage IMPROVES (an item is newly exercised; the baseline can be made stricter), or a member is legitimately removed from the vocabulary, regenerate the baseline in one step:
cd server/typescript
MO_UPDATE_COVERAGE_SNAPSHOT=1 bun test packages/metadata/test/registry-coverage.test.ts
Fully burn down later. Once the backlog empties (the untested-subtype set goes to zero), this can be tightened further to hard-fail on ANY untested subtype — forcing a fixture for every vocabulary member, not merely forbidding regressions.
The registry-conformance gate (above) proves the field-subtype vocabulary is
identical across ports, and the metamodel fixtures/conformance/ corpus
proves each subtype loads. SP-H closed the remaining axis: every concrete
field.* subtype must also write+read round-trip through each port's
runtime/ORM — not just be seeded by raw SQL and read back. That gate lives in
the persistence corpus, not here, but it is recorded here so a future subtype
added without a write-round-trip is visible:
field.* subtype |
metamodel corpus | persistence write-round-trip | codegen |
|---|---|---|---|
| string | ✅ | ✅ | ✅ |
| int | ✅ | ✅ | ✅ |
| long | ✅ | ✅ | ✅ |
| double | ✅ | ✅ | ✅ |
| float | ✅ | ✅ | ✅ |
| decimal | ✅ | ✅ | ✅ |
| boolean | ✅ | ✅ | ✅ |
| date | ✅ | ✅ | ✅ |
| time | ✅ | ✅ | ✅ |
| timestamp | ✅ | ✅ | ✅ |
| currency | ✅ | ✅ | ✅ |
| enum | ✅ | ✅ | ✅ |
| uuid | ✅ | ✅ | ✅ |
| uri | ✅ | ✅ | ✅ |
| inet | ✅ | ✅ | ✅ |
| object | ✅ | ✅ | ✅ |
- persistence write-round-trip = the
op: roundtripscenario type infixtures/persistence-conformance/queries/roundtrip-all-types.yaml: itsAllTypesentity carries one field of every persistable subtype, INSERTed through each port's runtime/ORM write codec (NOT raw SQL) then read back and asserted against the wire-normalizedexpect. Run on all five ports (TS / C# / Java / Kotlin / Python) against Testcontainers Postgres. - codegen = each port emits the
AllTypesentity + itsall_typestable (the committed TS-producedcanonical/schema.postgres.sql), and the api-contract corpus boots each port's generated artifact.
field.byte / field.short / field.class are absent by design — they
were cut as non-functional registration-only stubs (byte/short in 29057ad5,
class in SP-H), so the matrix tracks only genuinely-supported subtypes. Adding a
new persistable field.* subtype means adding a column to AllTypes (and a row
here) so the write gate covers it from day one.
{ "types": [ // sorted by "type.subType" { "type": "validator", "subType": "regex", "description": "...", // FR-033 — required, non-empty // optional: "rules", "example", "whenToUse" (emitted only when present) "attrs": [ // sorted by name { "name": "max", "valueType": "int", "isArray": false, "required": false, "description": "..." }, { "name": "min", "valueType": "int", "isArray": false, "required": false, "description": "..." }, { "name": "pattern", "valueType": "string", "isArray": false, "required": false, "description": "..." } ], "children": [] // FR-033 — sorted constraint graph (possibly empty) }, { "type": "object", "subType": "entity", "description": "...", "attrs": [ /* ... each carrying a "description"; "rules"/"example"/"whenToUse" only when present ... */ ], "children": [ // sorted by (childType, childSubTypeKey, childName) { "childType": "field", "childSubType": "*", "childName": "*" }, // a child rule MAY carry cardinality (FR-033) — emitted ONLY when defined: { "childType": "identity", "childSubType": ["primary", "secondary"], "childName": "*", "min": 1, "max": null, "named": true } // "childSubType" is a string, "*" (any), OR a string[] (a list of admitted subtypes). // "max": null means unbounded; "min"/"max"/"named" are OMITTED when undefined. ] // optional: "parents": ["object.entity"] — emitted only when present + non-empty, sorted ASCII } ], "commonAttrs": [ // sorted by name — the registerCommonAttribute set { "name": "description", "valueType": "string", "isArray": false, "required": false, "description": "..." } ], "defaultSubTypes": { "metadata": "root", "object": "entity" } // sorted keys }