Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 62 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ it until 1.1 ships._
nothing for a report before. Java `gen` now joins Kotlin and C# in refusing a served report
with a derived field over a `field.object`. In C# a report's enum dimension is sortable (an
entity's enum field still is not). A decimal column (`avg`, a ratio, a `sum` of a decimal)
has no cross-port JSON spelling: each port sends its own, and TypeScript sends a string.
has no cross-port JSON spelling: each port sends its own, and TypeScript sends a string (on SQLite as well as Postgres: see **Fixed**).
Gated by a new api-contract sub-corpus, `fixtures/api-contract-conformance/report/` (13
scenarios, generated lane, all five ports; the corpus goes from 61 scenarios to 78, the
`projection/` sub-corpus gaining four), which
Expand All @@ -196,7 +196,8 @@ it until 1.1 ships._
routes generator passes them, and its output fails typecheck against the older
`MountReadOnlyOptions`. A hand-written generator that gates on `servesReadApi`, or on
`!isAbstract` as the `meta generator` scaffold does, now receives a served report's read
model: gate UI output on `servesClientTier`.
model: gate grid or form output on `servesClientTier` (a hook is wanted for a report: see the
list hook below).
- **Reports have model and API pages in `meta docs` (FR-044).** Every report gets a model page
(its `@from`, its view or "Not served" with the reason, its row scope, and a column table
with a definition per column) listed under `## Reports` on the model index, and a page on the
Expand All @@ -206,14 +207,26 @@ it until 1.1 ships._
gets none. `EntityDocData` gains four optional keys for an owned `docs/entity-page.md`
template: `hasReport`, `reportBlock`, `hasReporting`, `reportingBlock`. A model with no
reporting nodes renders the same model pages as before.
- **No client hook or other UI-tier output is generated for a report yet (FR-044).** No
TanStack hook, grid, grid hook or form, no Angular service or grid, and `agent/ui.md` lists
no report. In TypeScript the UI-tier generators now gate on a new exported predicate,
`servesClientTier` (`servesReadApi` and not a report); `servesReadApi` is true for a served
report so that its routes and queries emit. **If you own an ejected hook or grid generator
that gates on `servesReadApi`, it will emit for every served report: switch it to
`servesClientTier`.** `hasItemRoute`, `isReport`, `servedReport` and `generatableObjects` are
exported from `@metaobjectsdev/codegen-ts` beside it.
- **A served report gets a generated TanStack list hook in TypeScript (FR-044).** `tanstackQuery()`
writes `<R>.hooks.ts` and the `<R>.meta.ts` descriptor it imports for a served report:
`use<R>List` (or `use<R>s` when the name is not already plural), typed with the report's row
type, its filter type and its sort, and the `<r>Keys` query-key factory with `all`, `lists` and
`list`. It writes no detail hook, no mutation hook, no form and no grid: a report has no item
route and no write, and a grid, form or dashboard over reports waits for the `reporting`
library. `agent/ui.md` lists the report, with a line saying so. An adopter whose pages read
everything through generated hooks no longer swaps a generated hook for a hand-written one
when it replaces a projection with a report. TypeScript is the only port with a generated
client tier, so no other port gains a file. Two predicates, both exported from
`@metaobjectsdev/codegen-ts`: the hook generator gates on `servesClientHooks` (`servesReadApi`,
true for a served report) and the grid generators, the Angular service and grid and the
Angular barrel gate on `servesClientTier` (`servesReadApi` and not a report). `hasItemRoute`,
`isReport`, `servedReport` and `generatableObjects` are exported beside them.
**Upgrading an owned `hooks` generator.** A copy ejected before this release keeps the gate it
was copied with, so it writes no hook for a report until you resync it (`meta eject hooks
--force`, or merge the reference by hand: its filter is now `servesClientHooks(e)`). Nothing
else breaks: an owned `grid`, `grid-hook` or `form` copy needs no change, and a hand-written
generator that gates on `servesReadApi` receives a served report's read model and emits
its list hook, which is what a hook generator wants; gate a grid or form on `servesClientTier`.

### Changed

Expand Down Expand Up @@ -348,6 +361,45 @@ until you regenerate.

### Fixed

- **TypeScript: a report's decimal fields reach the wire as strings on SQLite, as on Postgres
(FR-044).** A ratio is typed `decimal`, the TypeScript read schema types a decimal as
`string`, and SQLite has no decimal: the view computes a `REAL`, which the driver hands the
route as a JS number. A generated report route on SQLite therefore answered
`"paidShare": 0.4` where Postgres answers `"paidShare": "0.4"`, and the row type said
`string` for both, so `.toFixed(1)` compiled on neither and ran on one. The route is now
corrected, not the type. **Behaviour note:** on SQLite a report's `avg`, ratio and
`sum` of a decimal field arrive as strings (`"0.4"`, `"1.6666666666666667"`, null stays
null); convert with `Number(x)` where you did arithmetic or formatting on the number. The
generated report route passes the decimal field names to the mount as `decimalColumns`
(`mountReadOnlyCrudRoutes` in the Fastify and Hono adapters, new optional option), which sends
a number under one of them as its string. Filtering and sorting on the column are unchanged
and stay numeric, because the view column is still a REAL. The digits are JavaScript's
shortest spelling of the double, not Postgres' fixed scale, and a decimal's digits stay
outside the cross-port contract. Postgres and MySQL are untouched, and so is a projection on
SQLite (released behaviour; its decimals still arrive as numbers). **Upgrading an owned
`routes` or `routes-hono` generator:** a copy ejected before this release passes no
`decimalColumns`, so a SQLite report keeps answering a number until you resync it
(`meta eject routes --force`, `meta eject routes-hono --force`, which also refreshes the
adapter copy under `codegen/runtime/`). Gated by a new TypeScript lane,
`integration-tests/test/api-contract-report-sqlite.test.ts`, which boots the emitted report
routes on a real SQLite and runs the thirteen shared report scenarios as well as the
decimal's wire type; the cross-port corpus still runs TypeScript on Postgres only.
- **The spec's worked example for the nested average counted the wrong thing (FR-044).**
`daysEngaged` was declared as a distinct count of `(programId, weekNumber, dayNumber)` and
`avgDaysPerStarter` as `daysEngaged / starters`. Grouped by program, that numerator is the
number of distinct days anyone did, not the sum over customers of the days each did: for one
customer with three days and two customers sharing one day it gives 3 / 3 = 1.0 where the
answer is (3 + 1 + 1) / 3 = 1.667. The tuple now includes the customer:
`(programId, customerEmail, weekNumber, dayNumber)`. `docs/superpowers/specs/2026-10-02-fr-044-core-reporting-design.md`
states why. The same declaration was copied into the conformance fixtures, so the positive
fixture `reporting-vocabulary`, its canonical `expected.json`, the `codegen-noop/reporting`
model and the error-fixture models derived from it carry the four-column tuple, and the
per-port accessor tests and the Java tuple-size message ("@of lists 4 columns") follow. No
loader rule changes, no vocabulary moves and `metamodelVersion` stays `1.1`; a model that
used the three-column form still loads, and keeps counting the distinct days anyone did.
A value test on a real SQLite and a real Postgres holds the example's numbers (5 customer-days
over 3 starters, 1.667), and its slip form (3 / 3) as the thing it replaces.

- **TypeScript, Kotlin, C#: a read-only projection whose key is renamed and whose identity omits
`@fields` now serves its item route.** The shape: a view-only `object.projection` that passes
the base entity's key through on a field with another name (`regNo` extending `Invoice.id`) and
Expand Down
2 changes: 1 addition & 1 deletion agent-context/skills/metaobjects-authoring/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,7 @@ no `source.*` is checked at load and generates nothing.
A report `@from` a TPH subtype is refused when its view is derived (the subtype shares its
base's table): declare it `@from` the base with an `@filter` on the discriminator field.

What does not exist: no client hook, grid or form for a report yet, no way to narrow which
What does not exist: no grid or form for a report (TypeScript generates a list hook; no other port has a client tier), no way to narrow which
derived fields are filterable, no `measure.derived`
(arithmetic between measures beyond `measure.ratio`), no query-time choice of dimensions or
measures (a report is a fixed, compiled combination), and no time-zone vocabulary (grains and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,4 @@ The files each port generates are in the `metaobjects-codegen` skill's reference

## What a report does not have

No client hook, grid or form is generated for a report in any port yet; you get the route and the row type. There is no `measure.derived`, no query-time choice of dimensions or measures, and no time-zone vocabulary.
In TypeScript a served report gets a generated list hook; no port generates a grid or a form for a report, and the other ports have no client tier, so there you get the route and the row type. There is no `measure.derived`, no query-time choice of dimensions or measures, and no time-zone vocabulary.
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,15 @@ served like a keyless projection, from a detached read model of its derived fiel
view binding, Zod read schema, row type, descriptor, filter and sort allowlists),
`<R>.queries.ts` (`list…` only, no by-id), `<R>.routes.ts` (and `<R>.routes.hono.ts` from
`routesFileHono()`): GET list, `POST` answers 405, no `/:id`; `<R>.names.ts`; and the barrel
export. Every derived field with filter operators is filterable and sortable. Nothing
from the UI tier is written (no hooks, grid, grid hook or form), and `agent/ui.md` lists
no report: those generators gate on `servesClientTier`, which is false for a report,
while `servesReadApi` is true. An ejected hook generator that still gates on
`servesReadApi` emits a list hook for a report; switch it to `servesClientTier`. A
report with no view source, or an abstract one, generates nothing. The route and
export. Every derived field with filter operators is filterable and sortable. The UI tier
writes the list hook and nothing else: `<R>.hooks.ts` (`use<R>List`, typed with the report's
filter, no detail or mutation hook) and its `<R>.meta.ts` descriptor, and `agent/ui.md` lists
the report. The hook generator gates on `servesClientHooks`, which is true for a served
report; the grid generators gate on `servesClientTier`, which is false, so there is no grid,
grid hook or form. An ejected hook generator keeps the gate it was copied with and writes no
report hook until you resync it (`meta eject`). On SQLite a decimal field of a report (an
`avg`, a ratio) is sent as a string, as on Postgres: the route passes `decimalColumns` to the
mount. A report with no view source, or an abstract one, generates nothing. The route and
contract: `references/reporting.md` in the `metaobjects-authoring` skill.

The `CREATE VIEW` DDL is generated by `meta migrate`
Expand Down
5 changes: 3 additions & 2 deletions agent-context/skills/metaobjects-runtime-ui/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,9 @@ An `object.projection` uses the same rule, so `OrderSummary` is at
`/order_summaries` either way. The generated web-client hooks and grids build
their fetch URLs from the TypeScript `$path`, and every backend now mounts that
same spelling, so a React/TanStack client works against any port's server.
A view-backed `object.report` has a generated list route at the same rule and no
generated hook, grid or form yet.
A view-backed `object.report` has a generated list route at the same rule and, in
TypeScript, a generated list hook (`use<Report>List`); it has no grid, form or detail
hook.

**This is a change, and it was a breaking one.** Each port used to spell the
segment differently and they agreed only on single regular words — which was
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ this tree is documentation, not the source of truth.
| Build on a metadata model another repository publishes (`dependencies`, `meta deps sync`, overlay/extend across the boundary) | [`features/metadata-dependencies.md`](features/metadata-dependencies.md) |
| Adopt a design MetaObjects already ships — users/groups/roles, an LLM trace envelope — instead of authoring it (`libraries`, `meta eject <library>`) | [`features/libraries.md`](features/libraries.md) |
| Record what the system is supposed to do, and stop agents reviving retired features | [`features/requirements.md`](features/requirements.md) |
| Declare what a dashboard groups by and counts (`dimension`, `measure`, `segment`, `object.report`; load-time checked; a view-backed report becomes a SQL view served by a generated read-only list route; no client hook yet) | [`features/reporting.md`](features/reporting.md) |
| Declare what a dashboard groups by and counts (`dimension`, `measure`, `segment`, `object.report`; load-time checked; a view-backed report becomes a SQL view served by a generated read-only list route and a TypeScript list hook) | [`features/reporting.md`](features/reporting.md) |
| Wire prompt construction (FR-004) | [`features/templates-and-payloads.md`](features/templates-and-payloads.md) |
| Share a metadata shape across multiple instances (abstracts, `extends:`) | [`features/abstracts-and-inheritance.md`](features/abstracts-and-inheritance.md) |
| Add a custom metamodel subtype or attribute to a downstream project | [`features/extending-with-providers.md`](features/extending-with-providers.md) + [`recipes/extending-metaobjects-with-providers.md`](recipes/extending-metaobjects-with-providers.md) |
Expand Down
10 changes: 10 additions & 0 deletions docs/features/migrations/upgrading-within-1.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,15 @@ cross.
`origin.*` `@via` path follows is now the one the metadata names. A view that used to join on
the other key is rewritten by the next `meta migrate`, and it can return different rows. Read
that migration before applying it.
- **Reports (1.1).** An owned (ejected) generator keeps the logic it was copied with, so a copy
from before 1.1 produces no report output, or the wrong output, until you resync it. For a
served `object.report` that means: `entity`, `queries`, `names` and `barrel` write no report
files; `routes` and `routes-hono` mount `GET /:id` and the item refusals on a report, and on
SQLite send a ratio as a number; `hooks` writes no report hook, so a page that calls
`use<Report>List` has nothing to import. `meta eject --list` shows which copies differ;
`meta eject <name> --force` takes the new reference, or three-way merge a copy you edited. An
owned `mount-read-only.ts` under `codegen/runtime/` needs the same resync (`itemRoutes`,
`resource` and `decimalColumns`). Only TypeScript has a generated client tier; the other
ports gain a route and a row type and no client file.
- **New advisories (1.0.9).** `meta verify` lists foreign keys with no covering index. It never
fails a build; add an `index.lookup` (and a migration) where the join matters.
Loading
Loading