Skip to content

Commit bad5a59

Browse files
authored
feat: FR-044 Plan 1 reporting vocabulary with loader validation in all five ports (metamodel 1.1) (#397)
* feat(metamodel): register the FR-044 reporting vocabulary in TypeScript (metamodel 1.1) Registers dimension.attribute, dimension.time, measure.aggregate, measure.ratio, segment.filter and object.report (the ADR-0023 list agreed for FR-044), with their node classes and the report accessors Plan 2's lowering will use. object.entity gains dimension/measure/segment child wildcards; object.report inherits the object.base children like object.projection. measure.derived is deliberately absent. metamodelVersion moves 1.0 -> 1.1; the version check classifies all nine changes ADDITIVE. The registry manifests of the other four ports are regenerated for the byte-match, but those ports do not register the vocabulary yet, so their registry-conformance is red until the fan-out lands. Also updates the derived artifacts the vocabulary changes: metamodel docs, site payload and reference, the registry coverage baseline (the six new subtypes are untested until the conformance fixtures land), the BASE_TYPES count, and the project's own requirements ledger with the harness regenerated from it. * feat(metadata): validate the FR-044 reporting vocabulary at load (TypeScript) * fix(metadata): FR-044 reporting validation — keep each inheritor's error * test(conformance): FR-044 reporting vocabulary fixtures (1 positive, inheritance, 22 error cases) * test(conformance): FR-044 fixtures — make non-temporal F2 fixture reach F2, de-collide declared field, map fixtures to a doc * feat(metadata): relative-date filter values, legal on reporting hosts only (TypeScript) A { now: "<ISO duration>" } operand survives filter desugaring as an opaque value. The operator-less shorthand { field: { now: ... } } is read as the value of an implicit eq (now is not an operator), so F2 refuses it by name instead of S1 reporting a bad op. F1's search for a relative value on a non-reporting host now walks operand values only, so a field named now is no longer misread. The TS view lowering throws on a { now } operand that bypassed the loader. * feat(csharp): register and validate the FR-044 reporting vocabulary Registers dimension.attribute/time, measure.aggregate/ratio, segment.filter and object.report in the C# port, and ports validateReporting rule-for-rule from the TypeScript reference with the same message text. - SpecMetamodel/reporting.json (new) and object.json are byte copies of the root spec; SpecMetamodelReader lists 19 files. Attr schemas declare only value type, array-ness, requiredness and allowedValues; descriptions and the structural child graph come from the embedded JSON. - ValidationPasses.ValidateReporting (new partial file) runs after the projection-filter pass: D1-D4, M1-M6, S1, R1-R7, F1/F2. One broken rule is one error; inherited members are validated per inheritor with the TS ErrorSink dedupe. F1 classifies filter attrs through the registry, since C# keeps inline attrs as values rather than typed attr nodes. - Filter desugar: a shorthand { f: { now } } becomes { eq: { now } }; an explicit-op relative value stays an opaque operand. - Four new error codes: ERR_INVALID_DIMENSION, ERR_INVALID_MEASURE, ERR_INVALID_REPORT, ERR_REPORT_FOREIGN_MEASURE. - ReportingAccessorsTests and ReportingValidationTests (exact message text, inherited-member dedupe, relative-date desugar, F1 operand-only walk). All 24 FR-044 conformance fixtures, registry-conformance and spec-embed gates pass; codegen output for the positive fixture is byte-identical with and without the reporting vocabulary. * feat(java): register and validate the FR-044 reporting vocabulary Registers dimension.attribute, dimension.time, measure.aggregate, measure.ratio and segment.filter (new reporting-types provider) and object.report (beside object.projection). Structural children and descriptions come from spec/metamodel/reporting.json and object.json, now embedded (19 spec files). Kotlin composes from the same JVM provider set and needs no code; the facade carries no requirement accessors, so none are added for reporting. ReportingValidation ports the TS validateReporting pass rule for rule (D1-D4, M1-M6, S1, R1-R7, F1/F2) with the same message text, the same pass-1/pass-2 inheritance dedupe and the same no-cascade short-circuits. FilterAttribute desugars an operator-less { now } clause to eq, as the TS desugarClause does. Two Java-specific findings: 1. The registry manifest decided array-ness by attr NAME alone. FR-044 makes @Of an array on measure.aggregate while it stays scalar on dimension.* and origin.*, so every @Of in the manifest flipped to isArray: true. The emitter now resolves the array constraint per type (own, inherited, then common attr). 2. .withEnum is decorative on attr children in this port, so the closed @agg and @Grains sets the TS registry enforces generically are checked in the pass as ERR_BAD_ATTR_VALUE. Tests: all 24 FR-044 conformance fixtures and registry-conformance pass. A new test asserts every error fixture yields exactly one finding with the TS loader's code and message. Disabling the pass fails exactly the 22 error fixtures. * fix(java): FR-044 reporting — match TS message text for closed-set values - ERR_BAD_ATTR_VALUE for @agg / @Grains now uses the short node label (type.subType 'name'), byte-identical to the TS attr-schema check; the tests assert the full message, one error per bad array element. - A bare string authored for an array attr is ONE element, commas included, as TS normalizeStringArrayAttr wraps it. The canonical parser's comma-delimited array path split it (and left stray quotes on the pieces); it is now re-set whole. ReportingAttrs no longer splits a bare string either. A JSON-array value still yields its items. - Drop the four unused FR-044 ErrorMessageConstants entries, which had orphaned the ERR_VALIDATION_FORMAT doc comment. - ValidationPhase.run javadoc lists validateReporting in call order. * feat(python): register and validate the FR-044 reporting vocabulary Registers dimension.attribute, dimension.time, measure.aggregate, measure.ratio, segment.filter and object.report in the Python port, with the MetaDimension / MetaMeasure / MetaSegment node classes and the report accessors. The spec copies (reporting.json, object.json) are byte copies of spec/metamodel/, so the registry manifest byte-matches expected-registry.json. validate_reporting ports the TypeScript loader pass rule for rule (D1-D4, M1-M6, S1, R1-R7, F1/F2) with the same message text, the same short-circuits, the same error sources (R4/R5 on the declared child) and the same pass-2 inheritance dedupe. Every attr read resolves (get_meta_attr): Python attr() is own-only (ADR-0039); the two own reads (R4, the F1 tree walk) say why. The filter desugar now matches the TypeScript reference: and/or arrays recurse (they were collapsed into an `in` clause), an object clause carrying a `now` key is the value of an implicit eq, and any other object clause is an op map kept as-is (it was wrapped in eq, which hid a bad op). All 24 FR-044 conformance fixtures pass; a TS-vs-Python probe over 96 models produced byte-identical codes and messages. * test: FR-044 reporting nodes are inert in every generator and in migrate — fixtures fixtures/codegen-noop/reporting/{with,without}: a model pair that differs only by the reporting vocabulary (with/ adds a report declaring a read-only source.rdb @kind: view, the shape that leaked in C#). Shared by the five per-port inert tests. meta.fitness.json: Program gains one dimension.time, one measure.aggregate and one segment.filter, so every port's persistence corpus and codegen-compile gate load the vocabulary. No object.report there (no lowering until Plan 2). schema.postgres.sql regenerated with bun run gen:schema: byte-identical. * fix(codegen-ts): object.report emits nothing until its lowering lands (FR-044 Plan 1) runGen dropped nothing for a report, so entity, names, barrel, queries, routes, routes-hono and hooks each emitted files for the three reports in the inert model. Reports now leave the entity set at runGen's single selection choke point (isReport in source-detect.ts, keyed on OBJECT_SUBTYPE_REPORT). reporting-inert.test.ts compares every catalog generator, alone and as one suite, with and without the reporting nodes, and checks that migrate-ts builds the same postgres schema and diff() proposes no change. * fix(codegen-ts): view lowering reads { now } as a value, matching the attr.filter desugar extract-view-spec's local desugarClause claimed to mirror metadata's but read an object with a now key as an op map, so a pre-desugar shorthand { f: { now: "-P7D" } } lowered as op "now" and assertNoRelativeDate never fired. It now applies the same now-key rule (-> eq). Tests cover the shorthand on the row-scope @filter and both forms on the origin.aggregate @filter (resolveAggregateFilter); the test's own-attr mutation names its sanctioned case. * fix(csharp): object.report emits nothing until its lowering lands (FR-044 Plan 1) A report declaring a read-only source.rdb @kind: view emitted an empty entity, names, a filter allowlist, a GET-only route and a keyless DbSet with ToView. CodegenRunner.Run now drops reports from the entity set every generator reads (MetaObject.IsReport()). ReportingInertTests runs every registered generator, alone and together. * fix(java,kotlin): object.report emits nothing until its lowering lands (FR-044 Plan 1) Java: the entity generator emitted a class and an extractor per report (and bound them in GeneratedObjectClassBindingProvider); names emitted StoreTotalsNames for the view-backed report. GeneratorUtil.getFilteredMetaData, which every direct per-object generator selects through, now drops reports via the shared GeneratorUtil.isReport predicate; the names generator, which walks the loader itself, uses the same predicate. Kotlin: only names leaked (StoreTotalsNames.kt); same predicate. ReportingInertTest in codegen-spring and codegen-kotlin runs every registry generator alone, plus the coherent combined suites. * fix(python): object.report emits nothing until its lowering lands (FR-044 Plan 1) entity wrote an empty BaseModel module per report; filter-allowlist, names and routes emitted for the view-backed report. run_gen now drops reports from the entity set every generator reads. test_reporting_inert.py runs every registered generator alone and the runnable ones together. * docs: list fixtures/codegen-noop as the 25th shared corpus The FR-044 inert model pair is run by all five ports, so the site-payload gate counts it as a corpus. CONFORMANCE.md gains its matrix row (a template-quality check, not a core promise), and the corpus count moves 24 -> 25 there, in AGENTS.md and in the site payload. * fix(docs): object.report gets no docs page until its lowering lands (FR-044 Plan 1) A report's fields are derived by its lowering, so a docs page for one today shows none of them. Every docs surface now skips it, with the FR-044 Plan 1 comment: - TS meta docs: docsFile (model pages + README index), api-model (api surface), and the docs-site link graph that every site page, index and nav list is built from. The site's coverage audit also skips the reporting vocabulary, which it renders nothing for by design until the lowering lands, instead of reporting it as a rendering gap. - C#, Java, Python api-docs builders. Kotlin's already admitted only entity/value/projection. The inert tests in all five ports now also compare docs output with and without the reporting nodes: TS runs meta docs (model, agent, requirements, site) and the api surface; the other ports render every api-docs unit page, the index and the agent page. The Java test normalizes the model tier's wall-clock "Generated On:" header line, which made the comparison flaky across a second boundary. * fix(codegen-ts): servesReadApi excludes object.report, so agent/ui.md documents no report endpoint A report declaring a read-only source.rdb @kind: view passed servesReadApi (non-abstract + any rdb source), so agent/ui.md, rendered when the UI tier is wired, documented an endpoint for StoreTotals that nothing serves. runGen already drops reports, so this only bit doors that read the model directly. The predicate now excludes reports. The inert test gains an agent-surface case (schema, ui, requirements) run with the Hono routes and the UI tier wired; the earlier --agent run emitted nothing without a gen config. It also pins the generators that cannot run from a bare model (shared-model) by name, and checks that a selection of only reports warns that there is nothing to generate. * fix: FR-044 Plan 1 review minors — only-reports warning, deferred coverage, filter host - TS and Python runners: a selection made only of object.report nodes now gets the "No entities to generate" warning instead of a silent empty run. - docs-site coverage audit: dimension/measure/segment/report are reported as deferred (FR-044 Plan 2/3) on the returned report and in its warnings, rather than dropped, so the gap stays visible without changing a rendered page. The comment says to delete it when the lowering lands. - extract-view-spec: the relative-date / int-enum guard messages name the filter host, so the origin.aggregate @filter path no longer reads as a projection's view @filter. - Python apidocs builder: one object_constants import. - Inert tests: string literals replaced with the report subtype constants; each port pins, by name, the generators that cannot run from a bare model (TS shared-model, Java extractor and template, none in C#/Kotlin/Python), so one that starts throwing cannot drop out. * docs(fr-044): reporting vocabulary feature doc and changelog (metamodel 1.1) * docs(fr-044): correct fixture count and R5/D2/R6 wording in reporting.md * fix(python): dataGrid preset filters recurse into and/or The dataGrid @filter pass read 'and'/'or' as field names, so a valid composed preset was refused ("references field 'or' which is not a filterable field") even in a model with no reporting nodes, and a relative date inside an 'or' gave two errors where TS gives one. Recurse into each composition item like the TS checkFilterClauses. * fix(java): keep distinct reporting errors on one node The validation phase's envelope dedupe keyed on code + source, so two different reporting findings on one node (two unresolved @Dimensions items; a bad dimension item and a bad @segment; two inheritors breaking one inherited dimension) collapsed to one in Java while TS, C# and Python report each. Findings from the reporting pass now add their message to the dedupe key; the pass already drops its own true repeats. * test(conformance): two reporting errors on one node; relative dates on dataGrid and origin hosts error-report-two-unresolved-dimensions gates that two distinct findings on one report are both reported in every port. The two relative-date host fixtures gate F1 on a layout.dataGrid preset and an origin.aggregate @filter, beside the existing projection host. * test(csharp): select reporting fixture children by type and name Two inherited-member tests edited BaseEvent children by index, so a fixture reorder would silently retarget them; select by (type, name). The field-named-now F1 test now asserts the model loads clean rather than only that no relative-date error appears. * fix: FR-044 R5 message names every read-only kind; pin R6 attribute-dimension collision R5 accepts any read-only @kind (view, materializedView, storedProc, tableFunction) but its message told the author to declare @kind: view. The message now names the read-only kinds, identically in all four loaders, with the pins updated (Java's fixture table, the TS test) and new pins in C# and Python. R6 for an attribute dimension (bare name, no grain suffix) colliding with a same-named measure was untested; it is now pinned in TS with mirrors in C#, Java and Python. Python also pins two unresolved @Dimensions items as two errors. * docs: FR-044 changelog states what loads differently; fixture counts The 1.1 note claimed every model that loaded under 1.0 loads the same, which the Java and Python parser fixes contradict; say which models each fix affects, describe the Python non-operator object clause accurately per pass, and record the Python dataGrid and/or fix. The reporting doc's compatibility note gets the same correction. Metamodel corpus 358 -> 361 fixtures (AGENTS.md, CONFORMANCE.md, site payload); the changelog counts 27 new reporting fixtures. * no-mistakes(document): docs: update FR-044 roadmap status and stale corpus/matrix/porting facts
1 parent 70423f9 commit bad5a59

235 files changed

Lines changed: 23005 additions & 148 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.claude/rules/cross-language-porting.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Preserve the following contracts exactly across all language ports:
1717

1818
**Metamodel subtype vocabularies (must be identical across languages):** the `registry-conformance` gate (`fixtures/registry-conformance/`) is the structural enforcer of this rule — each port emits its registry as a canonical manifest byte-matched to `expected-registry.json`. **All five ports (TS / C# / Java / Kotlin / Python) are live + green** (SP-G Java/Kotlin reconciliation complete; the JVM runners compose from the defined metamodel provider set so codegen-base/om classpath SPI does not pollute the measured vocabulary). See `fixtures/registry-conformance/README.md`.
1919
- Filter operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `like`, `isNull`
20-
- Object subtypes: `entity` (owns data: own identity, writable sources, lifecycle), `value` (pure shape: NO identity, NO source, ever; constructed — by caller/embedding — never populated; may `extends` entity fields for shape; a value-hosted field may carry `origin.passthrough` but never an assembly origin), `projection` (derived read-only representation: fields `extends`-bound / origin-derived / self-declared-under-external-assembly, all read-only at subtype level; identity optional and MUST extend an entity identity; sources restricted to read-only `@kind`s; the declared field set IS the exposure — inclusive list, fail-closed). A field carrying `origin.*` is derived ⇒ read-only wherever it lives (incl. on entities). An entity's primary source must be a writable `@kind` (read-only kinds only in read role). See [ADR-0028](spec/decisions/ADR-0028-object-taxonomy-projection-value-purity.md). (FR-024 Phase E — `object.projection`/`value` are registered in `expected-registry.json` and the projection/value validation passes [identity pass-through, value-purity, projection-licensing, `@via` inference/cardinality, extends/origin agreement, derived-field providability] are enforced cross-port in all 5 ports. The **B4b** entity-primary-source-readonly cutover [the "writable `@kind`" clause above — `ERR_ENTITY_PRIMARY_SOURCE_READONLY`] + the projection codegen fan-out (read-only DTOs for view-kind projections; FR-015 proc-callables for proc-kind projections in TypeScript, C# and Kotlin ONLY — Java and Python ship no callable generator at all, so the cross-port claim does NOT cover that clause; api-docs label `object.projection` units as `projection` and document their generated `<Name>Dto`) are now shipped cross-port; the remaining FR-024 work is the declared-API surface — tracked in #10.)
20+
- Object subtypes: `entity` (owns data: own identity, writable sources, lifecycle), `value` (pure shape: NO identity, NO source, ever; constructed — by caller/embedding — never populated; may `extends` entity fields for shape; a value-hosted field may carry `origin.passthrough` but never an assembly origin), `projection` (derived read-only representation: fields `extends`-bound / origin-derived / self-declared-under-external-assembly, all read-only at subtype level; identity optional and MUST extend an entity identity; sources restricted to read-only `@kind`s; the declared field set IS the exposure — inclusive list, fail-closed). A field carrying `origin.*` is derived ⇒ read-only wherever it lives (incl. on entities). An entity's primary source must be a writable `@kind` (read-only kinds only in read role). See [ADR-0028](spec/decisions/ADR-0028-object-taxonomy-projection-value-purity.md). (FR-024 Phase E — `object.projection`/`value` are registered in `expected-registry.json` and the projection/value validation passes [identity pass-through, value-purity, projection-licensing, `@via` inference/cardinality, extends/origin agreement, derived-field providability] are enforced cross-port in all 5 ports. The **B4b** entity-primary-source-readonly cutover [the "writable `@kind`" clause above — `ERR_ENTITY_PRIMARY_SOURCE_READONLY`] + the projection codegen fan-out (read-only DTOs for view-kind projections; FR-015 proc-callables for proc-kind projections in TypeScript, C# and Kotlin ONLY — Java and Python ship no callable generator at all, so the cross-port claim does NOT cover that clause; api-docs label `object.projection` units as `projection` and document their generated `<Name>Dto`) are now shipped cross-port; the remaining FR-024 work is the declared-API surface — tracked in #10.) **`report` (FR-044 Plan 1)** is a root object subtype registered in all five ports, with the `dimension.attribute` / `dimension.time` / `measure.aggregate` / `measure.ratio` / `segment.filter` children on `object.entity` and the relative-date filter value — loader-validated (`ERR_INVALID_DIMENSION` / `ERR_INVALID_MEASURE` / `ERR_INVALID_REPORT` / `ERR_REPORT_FOREIGN_MEASURE`, plus `ERR_BAD_ATTR_FILTER` for relative dates off a reporting host) and **inert in every generator and in `meta migrate`**: an `object.report` emits nothing until its lowering plans land, gated by the 27 `reporting` conformance fixtures and the `codegen-noop` corpus. See [docs/features/reporting.md](docs/features/reporting.md).
2121
- Source subtypes: `rdb` (paradigm; ADR-0007). The pre-v2 `dbTable`/`dbView` subtypes are RETIRED — `source.rdb` + `@kind: table|view|materializedView|storedProc|tableFunction` is the form, with read-only-ness derived from `@kind`. Multi-source via `@role` (exactly one `primary` per object). Source physical name = `@table` (NOT `@name`); field physical name = `@column` (renamed from `@dbColumn`). Referential actions on relationships: `@onDelete` / `@onUpdate`.
2222
- Origin subtypes: `passthrough`, `aggregate`, `collection`, `computed`, `first` (concrete; `base` is the abstract root). `passthrough` is legal on an `object.value`-hosted field (FR-015 parameter lineage); the four assembly origins (`aggregate`/`computed`/`collection`/`first`) live on `object.projection` only — a value-hosted assembly origin is `ERR_SUBTYPE_RULE_VIOLATION` (#210).
2323
- Relationship subtypes: `association`, `aggregation`, `composition`. Cardinality via `@cardinality: one|many`; target via `@objectRef`. **M:N (FR-018) slim vocabulary:** `@cardinality: "many"` + `@objectRef` (target) + `@through` (the junction/through entity — a third entity that MUST declare two `identity.reference` children, one per FK side). The relationship's FK fields are **derived** from those references (the `identity.reference` SSOT for FK direction), never restated. `@sourceRefField` (optional) disambiguates a *directed* self-join by naming the source-side FK field on the junction (the other reference is the target side); on a `@cardinality: one` relationship it instead names which of several `identity.reference` nodes onto the same target this relationship navigates, short-circuiting the unique-candidate/`@sourceRefField`/name-pairing ladder (#368, [ADR-0029](spec/decisions/ADR-0029-entity-child-extends-and-via-inference.md) Amendment 1) — an unresolvable 1:N reference set is `ERR_INVALID_RELATIONSHIP` at load. `@symmetric` (optional boolean) marks an *undirected* self-join (union-on-read) — valid only when `@objectRef` == the declaring entity, and mutually exclusive with `@sourceRefField`. The pre-FR-018 `@joinEntity`/`@joinFields` attrs are REMOVED. Validation errors: symmetric-on-hetero / symmetric+sourceRefField → `ERR_BAD_ATTR_VALUE`; junction-missing-two-references / sourceRefField-not-matching / M:N-attr-on-1:N / **junction-unpairable** → `ERR_INVALID_RELATIONSHIP`. **Unpairable means the junction declares its two references but neither resolves to the navigating entity, or neither to the `@objectRef` target** — declaring two references is NOT enough, and the loader now checks WHAT they point at (owner ruling 2026-09-20). It does so by running the real FK derivation and converting its failure, never a parallel re-implementation, so the loader and the derivation cannot drift; scope mirrors codegen's own iteration exactly — every CONCRETE, non-projection object crossed with its EFFECTIVE relationships, NOT deduped by declaration, because pairing is a property of the navigating entity and an inherited M:N can pair from one subtype and not another. Before the ruling this loaded clean and then diverged: TS and C# warned and emitted no traversal route (a silent 404), while Java, Kotlin and Python failed the build. Gated by `fixtures/conformance/error-relationship-m2m-junction-unpairable/`.

‎AGENTS.md‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,10 @@ number. Both are the convergent-publishing rule working as intended. **1.0 is CU
4747
1.0.5's `rc.1` through `rc.11`, 1.0.7's and 1.0.8's `rc.1`, and 1.0.9's `rc.1` through `rc.6` are
4848
superseded, and npm's `next` tag is REPOINTED onto each release — not deleted, because
4949
`dist-tag rm` 403s for every token we hold (`docs/RELEASING.md` §4).
50-
`metamodelVersion` reads **`1.0`**, frozen — C4 landed and G4 shipped it, and no 1.0.x patch
51-
has moved it (1.0.4 relaxed `@sourceRefField` onto `@cardinality: one` with a prose-only
52-
manifest footprint, and was ruled a hold). Per-release detail lives in **`CHANGELOG.md`** — it is the log, and this file does
50+
`metamodelVersion` read **`1.0`** through 1.0.13 — C4 landed and G4 shipped it, and no 1.0.x patch
51+
moved it (1.0.4 relaxed `@sourceRefField` onto `@cardinality: one` with a prose-only
52+
manifest footprint, and was ruled a hold). `main` now carries **`1.1`** for the FR-044 reporting
53+
vocabulary, so 1.0.x PATCH releases are held from `main` until 1.1 ships. Per-release detail lives in **`CHANGELOG.md`** — it is the log, and this file does
5354
not duplicate it.
5455

5556
The npm surface is **14 `@metaobjectsdev/*` packages in full lockstep**; the two `angular`
@@ -74,14 +75,14 @@ PyPI has had no product change since `0.25.0` — nothing is broken.
7475
- **Kotlin** — `codegen-kotlin` (KotlinPoet on JVM): entity + Exposed table + Spring controller + payload + relations + filter allowlist + validator + stored-proc + output-parser generators. `integration-tests-kotlin` runs the persistence-conformance corpus through Exposed against Testcontainers Postgres.
7576

7677
**Cross-port conformance corpora** (every port runs the shared corpus):
77-
- Metamodel: `fixtures/conformance/` (334 fixtures; 24 shared corpora in total — per-corpus counts + the corpus x port matrix live in `docs/CONFORMANCE.md`). TS / C# / Java / Python all green.
78+
- Metamodel: `fixtures/conformance/` (361 fixtures; 25 shared corpora in total — per-corpus counts + the corpus x port matrix live in `docs/CONFORMANCE.md`). TS / C# / Java / Python all green.
7879
- Render: `fixtures/render-conformance/`. TS / C# / Java / Kotlin / Python byte-identical.
7980
- Persistence: `fixtures/persistence-conformance/`. **Query** scenarios run on every port (TS / C# / Java / Kotlin / Python), each provisioning its test DB by executing the committed, TS-produced `canonical/schema.postgres.sql` (Postgres only — Derby dropped for the cross-port query corpus, ADR-0015). The **migration** scenarios are exercised by **TS only** (TS owns schema migrations). **The corpus now gates WRITES, not just reads (SP-H):** an `op: roundtrip` scenario type INSERTs through each port's runtime/ORM write codec (NOT raw SQL), reads the row back, and asserts the wire-normalized value. The `AllTypes` entity (`roundtrip-all-types.yaml`) carries one field of **every** persistable `field.*` subtype — string/int/long/double/float/decimal/boolean/date/time/timestamp(+tz)/currency/enum/uuid/object — plus an **array-of-VO** `field.object @isArray @storage:jsonb` column (`labels`, written as 2-element / empty-`[]` / single-element arrays across the three rows) — so every subtype write+read (incl. the array-of-value-object jsonb codec) round-trips through every port against Testcontainers PG. (`field.byte`/`field.short`/`field.class` were cut as non-functional registration-only stubs — the matrix tracks only genuinely-supported subtypes; see `fixtures/registry-conformance/README.md` → "Per-subtype write-round-trip matrix".)
8081
- API-contract: `fixtures/api-contract-conformance/`. TS / C# / Java / Kotlin / Python all green — each port runs **two lanes**: a hand-rolled reference server AND its **generated** API artifact booted over HTTP (the deployed controller/routes; TS+C# full-stack vs Testcontainers PG, Java/Kotlin/Python generated controller + in-memory repo behind the consumer seam). The generated fan-out found 10 real deployment bugs golden snapshots missed. Two sub-corpora run the **generated lane only, on all five ports** — `write-through/` and `projection/` (F22: a view-only `object.projection` serves GET list + GET by id and answers every write verb with `405 {"error": "method_not_allowed"}`). That is deliberate, not a gap: what is under test is whether a port's GENERATOR emits those routes, and a hand-rolled reference server would answer every scenario by construction. The `m2m/` sub-corpus also gates **TPH x M:N together** (base-declared, subtype-declared, abstract-mid-declared, a non-subtype source onto a subtype TARGET, and the cross-subtype source id answering `200 []`) — the two corpora were originally built disjoint (`tph/` had no relationships, `m2m/` no discriminators), which is precisely how that defect class survived.
8182
- YAML / verify corpora green across the ports that ship those layers.
8283
- **Codegen-compile gate** (all five ports; a GATE, not a corpus — it has no fixtures of its own and no row in the matrix). Every corpus above gates BEHAVIOUR; none asks whether the emitted code BUILDS, which is how four "generated code does not compile" defects shipped in 1.0.4 with the whole matrix green — `gen` exits 0 in all four cases and the adopter's build is the first thing that disagrees. Each port generates from `fixtures/persistence-conformance/canonical/meta.fitness.json` (reused deliberately: a second kitchen sink would drift from the one the other corpora already maintain) and compiles the emitted tree with its real compiler — `ts.createProgram` / Roslyn / `javac` / `KotlinCompilation` / (Python, having no static compiler) importing the generated package plus `ruff` F821. **Every port excludes its framework-bound route tier** (TS `routesFile`, C# `RoutesGenerator`, Java `SpringControllerGenerator`, Kotlin `KotlinSpringControllerGenerator`): those imports are not on an in-memory compile's classpath and stubbing them drowns the signal, so that tier is proven by the api-contract integration lane instead. One cross-port rule, not four local concessions. Found 5 further real defects on first run. Boundary detail: `docs/CONFORMANCE.md` → "Split coverage".
8384

84-
**Key cross-language features shipped:** FR5 family (a/b/c/d/e + WARN envelope-shape — actionable loader errors per ADR-0009); FR-003 (Java RDB runtime persistence + projections; schema migrations are TS-only — the Java migration engine was removed); FR-006 (template.output parser-on-receipt codegen per ADR-0010 in all 5 ports); FR-008 + FR-009 (cross-port REST API contract + the nine filter operators); FR-018 (M:N relationship codegen in all 5 ports — entity navigation + idiomatic ORM wiring [Drizzle m2m / EF Core `UsingEntity` / Spring repo+JPA / Exposed / Pydantic+route as the SQLAlchemy-secondary equivalent] + REST traversal `GET /<source-plural>/{id}/<relation>` + Tier-2 docs, gated by the shared api-contract m2m corpus in both lanes + persistence-conformance; the TanStack M:N client hook is a deferred client-ergonomics follow-up); SP-H (field-subtype end-to-end hardening: every concrete `field.*` subtype write+read round-trips cross-port via the persistence `op: roundtrip` gate; cut `field.byte`/`field.short`/`field.class` non-functional stubs; cross-port filter-op reconciliation for uuid/currency); source v2 paradigm (ADR-0007); metadata-ktx Kotlin facade; per-target output directories (TS codegen).
85+
**Key cross-language features shipped:** FR5 family (a/b/c/d/e + WARN envelope-shape — actionable loader errors per ADR-0009); FR-003 (Java RDB runtime persistence + projections; schema migrations are TS-only — the Java migration engine was removed); FR-006 (template.output parser-on-receipt codegen per ADR-0010 in all 5 ports); FR-008 + FR-009 (cross-port REST API contract + the nine filter operators); FR-018 (M:N relationship codegen in all 5 ports — entity navigation + idiomatic ORM wiring [Drizzle m2m / EF Core `UsingEntity` / Spring repo+JPA / Exposed / Pydantic+route as the SQLAlchemy-secondary equivalent] + REST traversal `GET /<source-plural>/{id}/<relation>` + Tier-2 docs, gated by the shared api-contract m2m corpus in both lanes + persistence-conformance; the TanStack M:N client hook is a deferred client-ergonomics follow-up); SP-H (field-subtype end-to-end hardening: every concrete `field.*` subtype write+read round-trips cross-port via the persistence `op: roundtrip` gate; cut `field.byte`/`field.short`/`field.class` non-functional stubs; cross-port filter-op reconciliation for uuid/currency); source v2 paradigm (ADR-0007); metadata-ktx Kotlin facade; per-target output directories (TS codegen); FR-044 Plan 1 (the reporting vocabulary — `dimension.attribute`/`dimension.time`, `measure.aggregate`/`measure.ratio`, `segment.filter`, `object.report`, plus the relative-date filter value — registered and loader-validated in all five ports behind 27 conformance fixtures + the `codegen-noop` corpus; **reports generate nothing yet**, see [docs/features/reporting.md](docs/features/reporting.md)).
8586

8687
**Latest release: 1.0.13** (2026-10-03) — npm `1.0.13`, PyPI `1.0.13`, NuGet `1.0.13`, Maven Central `8.0.13`. A PATCH: an already-plural entity name (`Stats`, `Settings`) no longer doubles in API-surface names (REST paths, hooks, finders, DbSets) in any port, while default physical table names stay frozen on the old rule; two entities that would share one API name are now a generation error; two Kotlin controller compile fixes (`field.inet` filter ops, `@dbColumnType: uuid` on a string field). Gated by a private `1.0.13-rc.1` build on the adopter estate (`rc-gate.sh` 7/7) and a full `--strict-toolchains` local CI run. The previous release, 1.0.12 (2026-10-02), added `fmt` in every CLI, a deprecated-reference `verify` advisory, the `onLocate` extract hook, and an Exposed 1.x Kotlin output mode.
8788

0 commit comments

Comments
 (0)