Skip to content

fix(codegen-ts): keep reverse @via view columns and refuse value-object aggregates - #561

Merged
dmealing merged 6 commits into
mainfrom
fm/mo-report-view-emitter-fixes
Nov 12, 2026
Merged

dmealing merged 6 commits into
mainfrom
fm/mo-report-view-emitter-fixes

Conversation

@dmealing

@dmealing dmealing commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

What Changed

  • View emitter keeps reverse @via hop columns. codegen-ts extract-view-spec.ts now resolves an origin @via that walks an identity.reference backwards (for example Project.timeEntries). Previously meta migrate dropped the origin.aggregate / origin.first columns from CREATE VIEW. The emitter joins from the holder of the reference. A Postgres integration test (origin-reverse-hop-view-pg.test.ts) creates the view and reads it.
  • origin.aggregate refuses sum/avg/min/max over a value-object field. The loader in TypeScript, Java, C#, and Python now raises ERR_INVALID_ORIGIN when @of names a field.object or an @objectRef field. The message tells the author to model the amount as a scalar field. The error-origin-aggregate-value-object conformance fixture gates this across ports. count is unchanged.
  • Agent guidance routes grouped totals and reports to object.report. The metaobjects-authoring skill, its reference docs, the always-on template, and the generated agent-context fixtures now point grouped totals, dashboard numbers, and read-only report views at object.report (declaring-reports.md) before the @sql escape. docs/features/source-kinds.md documents the scalar-@of rule. CHANGELOG records both fixes under Unreleased.

Note: this branch does not change the reporting vocabulary itself. It fixes two correctness gaps and changes what agents are told to reach for.

Validation

What the reporting vocabulary covers (smoke-test reports, Postgres)

The three reports from the large-application smoke test, rewritten with object.report / dimension.* / measure.* / @spine and loaded and migrated on Postgres. This PR adds no vocabulary; the gaps are findings.

Report Verdict How, or what is missing
Revenue by client by month Expressible, except the money sum object.report from Payment: clientId / clientName via Payment.invoice.client, receivedOn:month, a count measure. Gap: the amount is a flattened Money value object, and a dimension or measure @of cannot name a value-object member (Payment.amount.currency is ERR_INVALID_DIMENSION), so neither summing the amount nor grouping by its currency is possible. Workaround: model the amount and currency as scalar fields.
Member utilization by month Workaround object.report from TimeEntry grouped by member and workDate:month, summing total and billable hours, with weekly capacity carried as a dimension. Gaps: there is no calendar-month x member spine. @spine takes rows from one to-one dimension entity, so "a zero row for every month in which anyone logged time" needs a cross product it cannot express. Utilization % needs measure-by-dimension arithmetic (hours / capacity x weeks), and measure.ratio only divides two measures.
Project financials Hours only Hours (total, billable) via @spine: TimeEntry.project, which gives zero rows for idle projects. Gaps: cost = hours x member cost rate needs a per-row product measure. Invoiced and paid are Money sums, the value-object gap again. Budget burn reads an optional jsonb Money, and no measure reaches into a jsonb value object. Combining measures from three fact tables (time entries, invoices, payments) does not fit a report with a single @from.

A fresh agent build session on packages built from this branch read declaring-reports.md and wrote its first report as an object.report. It fell back to @sql only after the Money value-object ERR_INVALID_DIMENSION above.

Risk Assessment

✅ Low: The change is small. The docs route report wording to object.report, and the generated agent-context fixtures match the template and skill edits. The reverse-hop view emitter uses the same order as the loader (relationship, then forward reference, then a unique reverse reference) and joins from the reference holder. The new value-object aggregate refusal is the same check in all four loader ports, uses resolving accessors, and only refuses input that produced invalid SQL before.

Testing

The earlier timeout is resolved: the server suite was rerun per package, every package exited 0, and integration-tests ran file by file, all 65 files exit 0 (api-contract report/generated/projection/m2m, report views on PG, MySQL and SQLite, and the origin reverse-hop view). All other server packages report 0 failures. forge and test-generators contain no test files (bun exits 1 with "0 test files matching"), so they have nothing to run. Live checks on disposable projects confirmed the value-object aggregate guard (the Java-side @agg guard in fe55b6d was not driven here). The docs-routing scenario has live evidence from the fresh agent session: it reached the reports reference and wrote an object.report first. It then fell back to @sql on an unrelated Money value-object @of gap (ERR_INVALID_DIMENSION), which is outside this change. No browser or UI surface exists, so no screenshots. Evidence logs are in the evidence directory. The worktree is clean and no background jobs remain.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
CLI meta on a disposable project refuses sum/avg/max aggregate over a field.object field with ERR_INVALID_ORIGIN, while count and scalar aggregates still load ✅ pass live loader-variants.log
A view column fed by a reverse identity.reference @via hop is emitted and works on Postgres (Testcontainers) ✅ pass live reverse-hop-view-pg.log; it/summary.txt (origin-reverse-hop-view-pg exit=0)
Report generation, views and served report routes still work across PG, MySQL and SQLite (api-contract report corpus, report-views, generated lane) ✅ pass live it/api-contract-report*.log, it/report-views-*.log all exit 0
Fresh agent build session is routed to object.report docs and writes a report as object.report first ✅ pass live Live agent session on packages built from this branch (1.3.1-rrd.2, fresh meta init). Fell back to @sql only on an unrelated Money value-object @of gap (ERR_INVALID_DIMENSION).
Full server TypeScript regression suite, run per package, has no failures ✅ pass live regression-summary.txt and it/summary.txt
Evidence: per-package regression summary
ai-runtime exit=0  21 pass  0 fail
cli exit=0  2011 pass  0 fail
codegen-ts exit=0  3445 pass  0 fail
codegen-ts-angular exit=0  23 pass  0 fail
codegen-ts-react exit=0  63 pass  0 fail
codegen-ts-tanstack exit=0  156 pass  0 fail
conformance exit=0  58 pass  0 fail
docs-site exit=0  89 pass  0 fail
forge exit=1 
integration-tests exit=124 
metadata exit=0  3984 pass  0 fail
migrate-ts exit=0  1203 pass  0 fail
render exit=0  472 pass  0 fail
runtime-ts exit=0  978 pass  0 fail
sdk exit=0  443 pass  0 fail
test-generators exit=1 
DONE
Evidence: integration-tests per-file results
test/ai-trace-sti-persistence.test.ts exit=0
test/api-contract-enum-default.test.ts exit=0
test/api-contract-duration.test.ts exit=0
test/api-contract-jsonb.test.ts exit=0
test/api-contract-decimal.test.ts exit=0
test/api-contract-openapi-check.test.ts exit=0
test/api-contract-projection.test.ts exit=0
test/api-contract-generated.test.ts exit=0
test/api-contract-report-corpus.test.ts exit=0
test/api-contract-m2m.test.ts exit=0
test/api-contract-report-mysql.test.ts exit=0
test/api-contract-scenario.test.ts exit=0
test/api-contract-report-sqlite.test.ts exit=0
test/api-contract-secret-fields.test.ts exit=0
test/api-contract-report.test.ts exit=0
test/api-contract-write-through.test.ts exit=0
test/contract-oasdiff.test.ts exit=0
test/d1-243-view-over-rebuilt-table.test.ts exit=0
test/dependency-imported-table-pg.test.ts exit=0
test/drop-column-view-pg.test.ts exit=0
test/drop-column-view-sqlite.test.ts exit=0
test/engine-wire.test.ts exit=0
test/enum-default-pg.test.ts exit=0
test/enum-intvaluemap-array.test.ts exit=0
test/api-contract-tph.test.ts exit=0
test/enum-intvaluemap-pg.test.ts exit=0
test/generated-uuid-routes-pg.test.ts exit=0
test/feature-combinations-pg.test.ts exit=0
test/independent-oracle-pg.test.ts exit=0
test/independent-oracle-relations.test.ts exit=0
test/jsonb-to-array-type-change-pg.test.ts exit=0
test/load-metadata-config.test.ts exit=0
test/load-project-model.test.ts exit=0
test/llm-call-persistence.test.ts exit=0
test/api-contract.test.ts exit=0
test/mysql-generated-app.test.ts exit=0
test/normalization.test.ts exit=0
test/origin-reverse-hop-view-pg.test.ts exit=0
test/pk-identity-cross-door.test.ts exit=0
test/mysql-object-manager.test.ts exit=0
test/migration.test.ts exit=0
test/query-d1.test.ts exit=0
test/query-sqlite.test.ts exit=0
test/query-mysql.test.ts exit=0
test/regen-reports-divergence.test.ts exit=0
test/report-shapes-artifact.test.ts exit=0
test/reaper-kill.test.ts exit=0
test/report-views-mysql.test.ts exit=0
test/report-views-pg.test.ts exit=0
test/runtime-return-types.test.ts exit=0
test/schema-artifact-engines.test.ts exit=0
test/schema-artifact.test.ts exit=0
test/temporal-parsers.test.ts exit=0
test/validation-conformance-cross-field.test.ts exit=0
test/validation-conformance-runtime.test.ts exit=0
test/validation-conformance.test.ts exit=0
test/report-views-sqlite.test.ts exit=0
test/view-lifecycle-sqlite.test.ts exit=0
test/view-lifecycle-pg.test.ts exit=0
test/view-recreate-drift-pg.test.ts exit=0
test/view-recreate-drift-sqlite.test.ts exit=0
test/view-recreate-grants-pg.test.ts exit=0
test/view-replace-probe-pg.test.ts exit=0
test/query.test.ts exit=0
DONE
Evidence: value-object aggregate guard CLI transcript
== count
summary: 1 of 2 gate(s) failed (codegen); 1 advisory finding(s)
notRepresented[2]: "per-gate drift detail for every gate but schema (which template variable drifted, which generated file differs, which migration failed to replay) — printed as text on stderr; this payload carries each gate's pass/fail verdict, and the schema gate's differences in schemaDrift",the loader's own warnings and the agent-context/manifest advisories — printed as text on stderr
exit=2
== avg
meta: failed to load metadata: ERR_INVALID_ORIGIN: origin.aggregate @agg:avg on ProjectInvoicing.invoicedTotal: @of "Invoice.total" is a value-object field (field.object), which has no single column to aggregate. Aggregate a scalar field instead: model the amount as its own field (and group by its currency).
error: "failed to load metadata: origin.aggregate @agg:avg on ProjectInvoicing.invoicedTotal: @of \"Invoice.total\" is a value-object field (field.object), which has no single column to aggregate. Aggregate a scalar field instead: model the amount as its own field (and group by its currency)."
code: ERR_INVALID_ORIGIN
exit=1
== maxvo
meta: failed to load metadata: ERR_INVALID_ORIGIN: origin.aggregate @agg:max on ProjectInvoicing.invoicedTotal: @of "Invoice.total" is a value-object field (field.object), which has no single column to aggregate. Aggregate a scalar field instead: model the amount as its own field (and group by its currency).
error: "failed to load metadata: origin.aggregate @agg:max on ProjectInvoicing.invoicedTotal: @of \"Invoice.total\" is a value-object field (field.object), which has no single column to aggregate. Aggregate a scalar field instead: model the amount as its own field (and group by its currency)."
code: ERR_INVALID_ORIGIN
exit=1
== scalar
summary: 1 of 2 gate(s) failed (codegen); 1 advisory finding(s)
notRepresented[2]: "per-gate drift detail for every gate but schema (which template variable drifted, which generated file differs, which migration failed to replay) — printed as text on stderr; this payload carries each gate's pass/fail verdict, and the schema gate's differences in schemaDrift",the loader's own warnings and the agent-context/manifest advisories — printed as text on stderr
exit=2
Evidence: reverse-hop view on Postgres
bun test v1.3.14 (0d9b296a)

 1 pass
 0 fail
 2 expect() calls
Ran 1 test across 1 file. [4.36s]
  • Outcome: 🔧 2 issues found → no changes applied ✅ across 2 runs (1h42m13s)

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

🔧 **Test** - 2 issues found → no changes applied ✅
  • ⚠️ server/typescript - Full server regression suite (bun test in server/typescript) timed out at 1500s with no result. Regression is unproven. Rerun per package or with a longer timeout before the PR gate.
  • ⚠️ live validation verdict: inconclusive (5 of 7 scenarios were driven live against the product); untested: Full server regression suite, Reporting skill/docs routing (agent-context guidance to route report wording to object.report)
  • Live validation: ⚠️ inconclusive - 5 of 7 scenarios driven live against the product
Scenario Result Live Evidence
Sum over value-object field refused at load (meta verify on disposable project, @Of Invoice.total field.object) ✅ pass live loader-variants.log; fixture input meta.billing.json; output ERR_INVALID_ORIGIN with message naming ProjectInvoicing.invoicedTotal
avg and max over value-object field also refused (adversarial: other reducing aggregates) ✅ pass live loader-variants.log, variants avg and maxvo
count over value-object field still loads (adversarial: guard must not over-reach) ✅ pass live loader-variants.log, variant count: no ERR_ load error, only codegen gate drift because no config exists
sum over scalar field still loads (adversarial: guard must not refuse scalar sums) ✅ pass live loader-variants.log, variant scalar
Reverse identity.reference @via hop view emits aggregate, first and count columns and joins from holder (Postgres) ✅ pass live reverse-hop-view-pg.log: 1 pass, 0 fail
Full server regression suite ⏸️ untested no bun test run in server/typescript hit the 1500s cap with no output, so no pass/fail. Rerun per package to get a verdict.
Reporting skill/docs routing (agent-context guidance to route report wording to object.report) ⏸️ untested no Markdown and skill text with no runtime product surface; the intent's question about better skills is a documentation judgment, not an executable behavior.
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • bun install at repo root (dependency materialize)
  • bun server/typescript/packages/cli/bin/meta.ts verify --codegen on disposable project with fixture error-origin-aggregate-value-object input (sum over field.object Invoice.total)
  • Variants of the same project: @agg avg and max over value-object field; @agg count over value-object field; @agg sum over new scalar field Invoice.amount
  • bun test test/origin-reverse-hop-view-pg.test.ts in server/typescript/packages/integration-tests (Testcontainers Postgres: migrate, insert, read view)
  • bun test in server/typescript (full server regression suite) - killed at timeout 1500s, no result
  • Not run: pre-fix reproduction of reverse-hop view bug (would require reverting product code)

🔧 No changes applied.
✅ Re-checked - no issues remain.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
CLI meta on a disposable project refuses sum/avg/max aggregate over a field.object field with ERR_INVALID_ORIGIN, while count and scalar aggregates still load ✅ pass live loader-variants.log
A view column fed by a reverse identity.reference @via hop is emitted and works on Postgres (Testcontainers) ✅ pass live reverse-hop-view-pg.log; it/summary.txt (origin-reverse-hop-view-pg exit=0)
Report generation, views and served report routes still work across PG, MySQL and SQLite (api-contract report corpus, report-views, generated lane) ✅ pass live it/api-contract-report*.log, it/report-views-*.log all exit 0
Fresh agent build session is routed to object.report docs and writes a report as object.report first ✅ pass live Live agent session on packages built from this branch (1.3.1-rrd.2, fresh meta init). Fell back to @sql only on an unrelated Money value-object @of gap (ERR_INVALID_DIMENSION).
Full server TypeScript regression suite, run per package, has no failures ✅ pass live regression-summary.txt and it/summary.txt
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains (baseline, passed)
  • meta CLI on disposable projects: origin.aggregate sum/avg/max over a field.object field fails with ERR_INVALID_ORIGIN, count and scalar aggregates load (loader-variants.log)
  • origin-reverse-hop-view-pg.test.ts against Testcontainers Postgres (reverse identity.reference @via hop view column)
  • Server regression per package, bun test from inside each package: metadata, cli, codegen-ts, codegen-ts-react, codegen-ts-tanstack, codegen-ts-angular, conformance, docs-site, migrate-ts, render, runtime-ts, sdk, ai-runtime
  • integration-tests: each of the ~65 test/*.test.ts files run separately (Postgres, MySQL and SQLite containers/engines), all exit 0
  • Fresh agent build session on packages built from this branch (1.3.1-rrd.2, fresh meta init): opened declaring-reports.md, saw the routing line, wrote its first report as object.report
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

The meta init agent guide gains one rule: a grouped total, dashboard number or
read-only report view is an object.report over dimension.* / measure.*, with a
hand-written @SQL view body as the last resort. The metaobjects-authoring
routing table sends report, dashboard, totals, GROUP BY and per-month wording to
declaring-reports.md first, and the @SQL sections of read-views-and-projections.md
and sources.md point an aggregating body at it and say what @SQL is for.
…@via hop

An origin @via hop that walks an identity.reference backwards (#560) loaded, but the
projection view emitter resolved hops only through relationships and forward references
and silently dropped the aggregate and first columns from CREATE VIEW. It now falls back
to the same reverse-hop lookup the loader uses and joins from the reference holder.
Gated by a Postgres test that migrates, inserts and reads the view.
…bject field

@Of naming a field.object (flattened or jsonb) loaded and emitted SUM(<field>), a column
that does not exist. All four loaders (Kotlin through Java) now refuse it with
ERR_INVALID_ORIGIN: a value object is several columns or a document, and summing a
Money-shaped value would add amounts in different currencies. count is unaffected.
Gated by the new error-origin-aggregate-value-object conformance fixture.
Fixtures, the metamodel spec and its generated per-port copies, the generated reference
site and the showcase payload.
List.of(...).contains(null) throws, so an origin.aggregate with no @agg over a
resolvable @Of crashed the loader instead of reporting the missing attribute.
@dmealing
dmealing merged commit 57a7c6f into main Nov 12, 2026
15 of 17 checks passed
@dmealing
dmealing deleted the fm/mo-report-view-emitter-fixes branch November 12, 2026 16:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant