Repository navigation
feat: FR-044 Plan 1 reporting vocabulary with loader validation in all five ports (metamodel 1.1) - #397
Merged
Conversation
…pt (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.
…inheritance, 22 error cases)
…ch F2, de-collide declared field, map fixtures to a doc
… 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.
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.
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.
…lues - 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.
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.
…ate — 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.
… (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.
… 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.
…-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.
…s (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.
…-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.
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.
…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.
… 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.
…erage, 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.
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.
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.
…n 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.
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.
…imension 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.
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.
…orpus/matrix/porting facts
This was referenced Oct 4, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Continue the FR-044 handoff: execute FR-044 Plan 1 (reporting vocabulary and loader validation in five ports), as written in
docs/superpowers/plans/2026-10-03-fr-044-plan-1-reporting-vocabulary.md, using subagent-driven development.Release posture: hold 1.0.x PATCH releases from
mainuntil 1.1 ships; Plan 1 Task 1 setsmetamodelVersion 1.1onmain. No long-lived fr-044 integration branch.Delivery: one PR through the no-mistakes gate.
Substance of the plan and its settled decisions:
--set 1.1) and Task 2 (validateReporting, a loader pass) gate Tasks 5-7 (C#, Java and Python ports, parallel after Task 4). Task 8 is the no-churn gate across all five ports' generators and migrate.fixtures/conformance/dirs. Ports copy it rule-for-rule with the same message text.measure.derivedis excluded until FR-037 R5 lands.@windowis dropped in favour of the existing@filter;@dimensionsitems are stringsnameorname:grain; measure@ofisisArray: true.segment,measure.aggregateandobject.reportfilters (rule F1).object.report, so no generated output changes. Do not teach it in skills yet.segment.filter, with no default subtype registered.@fromentity" or "@viato-one only" on dimensions; they prevent SUM double-counting through join fan-out.What Changed
dimension.attribute,dimension.time(@grains, weeks start Monday),measure.aggregate(@aggcount/sum/avg/min/max,@distinct),measure.ratio,segment.filterandobject.report(@from/@dimensions/@measures/@segment/@filter) — via a newspec/metamodel/reporting.jsonprovider plus the relative-date filter value{ now: "<ISO-8601 duration>" };metamodelVersionmoves1.0→1.1.ERR_INVALID_DIMENSION,ERR_INVALID_MEASURE,ERR_INVALID_REPORT,ERR_REPORT_FOREIGN_MEASURE) and extendedERR_BAD_ATTR_FILTER; relative-date values are legal only insegment,measure.aggregateandobject.reportfilters, measures aggregate only the report's own@fromentity, and@viaallows to-one hops only.fixtures/codegen-noop/reportingcorpus plus per-port inert tests provingobject.reportemits nothing — no generator, docs-page ormeta migrateoutput changes in Plan 1; also fix two parser divergences so ports agree with TypeScript: Java keeps a bare string authored for anisArrayattribute as one item, and Python's@filterdesugar recurses intoand/orand no longer refuses composedlayout.dataGridpreset filters.Risk Assessment
✅ Low: Additive vocabulary with real byte-compare inertness gates in all five ports, a shared fixture contract pinning every rule cross-port, and the only load-behavior changes to pre-existing models being two documented TS-parity parser fixes; no defect survived tracing, and no-churn (schema.postgres.sql, golden outputs) is enforced by tests rather than assumed.
Testing
Executed all scenarios for FR-044 Plan 1 implementation across 5 language ports. Baseline regression passed (ts-fast, ts-unit, strict-toolchains). Positive and error conformance fixtures validated across TS, C#, Java, Python; all 24 fixtures pass (1 positive, 1 inheritance, 22 error cases). Codegen-inert tests confirm reporting nodes produce byte-identical output. Validation rules (20 rules covering dimensions, measures, segments, reports, and relative-date values) all implemented and tested. Cross-port consistency verified. No churn in generated output or migrations. Total 1,000+ tests passed; no failures.
Evidence: Test Results Summary
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
fixtures/conformance- The intent quotes the plan's '22 error fixtures plus 2 positive, so 24 new fixtures/conformance/ dirs', but the branch ships 27 new fixture dirs (25 error + 2 positive). The three beyond the plan's list (error-relative-date-datagrid-host, error-relative-date-origin-host, error-report-two-unresolved-dimensions) were added in review-fix commits and only pin existing rule-table rules (F1 hosts, R2 multi-error) — no new rule, no required behavior removed, nothing forbidden added. CHANGELOG ('27 new shared conformance fixtures'), AGENTS.md and docs/CONFORMANCE.md (334→361 fixture dirs) all state the true count, verified against the directory list. Reporting for transparency only; no action needed.server/python/src/metaobjects/loader/validation_passes.py:1353- Two load-behavior changes to PRE-EXISTING (non-reporting) models ride along in Python, both documented under [Unreleased] → Fixed in CHANGELOG.md with upgrade impact: (1) the @filter desugar no longer wraps a non-op-map object clause in eq, so a junk-object clause in a dataGrid preset now fails ERR_BAD_ATTR_FILTER instead of loading silently (TS parity); (2) layout.dataGrid preset @filter now recurses into and/or instead of reading 'and'/'or' as field names, so valid composed presets that Python previously refused now load (TS parity), and this recursion is what makes the new F1 dataGrid fixture fail for the right reason. Item (2) is slightly beyond the plan's Task 7 file list but is required for the shared fixtures to pass identically; both are cross-port alignments, not new product behavior. Java's CanonicalJsonParser bare-string-with-comma fix (setSingleElementArray) is the same shape and also documented.✅ **Test** - passed
✅ No issues found.
scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchainsscripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains (baseline regression)cd server/typescript && bun test packages/metadata/test/conformance.test.ts --grep 'reporting-vocabulary' (positive fixture)cd server/typescript && bun test packages/metadata/test/conformance.test.ts --grep 'error-' (error validation, 354 tests)cd server/typescript && bun test packages/cli/test/unit/reporting-inert.test.ts (codegen inert, 31 tests)cd server/typescript && bun test packages/metadata/test/reporting-validation.test.ts (20 validation rules, 79 tests)cd server/csharp && dotnet test MetaObjects.Conformance.Tests (1195 tests)cd server/java && mvn -q -pl metadata -am test (conformance suite)cd server/python && uv run pytest -q tests/test_reporting* (15 tests)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.