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
52 changes: 32 additions & 20 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,9 @@ it until 1.1 ships._
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.
Gated by a new api-contract sub-corpus, `fixtures/api-contract-conformance/report/` (12
scenarios, generated lane, all five ports; the corpus goes from 61 scenarios to 73), which
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
is also the first to assert a `field.date` literally. Anyone who declared a view-sourced
report under the unreleased 1.1 vocabulary will see these files on the next `gen`. See
[docs/features/reporting.md](docs/features/reporting.md#how-a-report-is-served).
Expand Down Expand Up @@ -144,28 +145,32 @@ it until 1.1 ships._

### Changed

Seven corrections that shipped with report serving and reach models that declare no report.
Eight corrections that shipped with report serving and reach models that declare no report.
Each one changes generated code on the next `gen`, or generated docs on the next `meta docs`
or Python api-docs build, as its entry says. A drift gate that covers that output reports it
until you regenerate.

- **TypeScript and Python: a read-only projection with no declared identity and no field named
`id` loses its item surface.** It no longer gets `GET /{id}` or the three item-verb refusals,
its by-id query (`find<Name>ById` in TypeScript, `find_by_id` on the Python repository
Protocol) or, in TypeScript, its detail hook and `detail` query keys. That surface could not
address a row: TypeScript built the query with no `WHERE` and answered the view's first row,
and Python bound an `id: int` to nothing. **Unchanged by this entry:** a projection with a
declared identity (a composite one still binds its first field; the next entry covers one
keyed on a field not named `id`), and a projection with an `id` field and no declared
identity. C#, Java and Kotlin are unchanged and remain stricter: they mount
`/{id}` only for a declared single-column identity, so a projection with an `id` field and no
declared identity has item routes in TypeScript and Python and none in the other three. The
TypeScript mounts (`mountReadOnlyCrudRoutes`, Fastify and Hono) take a new `itemRoutes: false`
option for this; it defaults to mounting them. **Upgrading:** an owned `routes` or
`routes-hono` generator ejected before this release keeps mounting the item routes for this
shape until you re-eject it or add `itemRoutes: false` to its read-only mount options, and
an owned `mount-read-only.ts` needs the re-sync described under the served-report entry
above.
- **TypeScript and Python: a read-only projection with no declared identity loses its item
surface, even when it has a field named `id`.** It no longer gets `GET /{id}` or the three
item-verb refusals, its by-id query (`find<Name>ById` in TypeScript, `find_by_id` on the Python
repository Protocol) or, in TypeScript, its detail hook and `detail` query keys. This is a
**named behaviour change for an adopter whose view-only projection has an `id` field and no
`identity.primary`**: that shape kept working in these two ports (TypeScript answered the row
whose `id` matched), and now answers the framework's `404` for `/{id}`. To keep the item
route, declare the identity: `identity.primary` extending the base entity's, with a
pass-through field for its key. A field named `id` is a convention, not a key, and C#, Java
and Kotlin never mounted item routes for it, so all five ports now mount `/{id}` for exactly
the projections that declare an identity. Without any `id` field the same surface was already
unaddressable: TypeScript built the by-id query with no `WHERE` and answered the view's first
row, and Python bound an `id: int` to nothing. **Unchanged by this entry:** a projection with
a declared identity (a composite one still binds its first field in these two ports; the next
entry covers one keyed on a field not named `id`). The TypeScript mounts
(`mountReadOnlyCrudRoutes`, Fastify and Hono) take a new `itemRoutes: false` option for this;
it defaults to mounting them. **Upgrading:** an owned `routes` or `routes-hono` generator
ejected before this release keeps mounting the item routes for this shape until you re-eject
it or add `itemRoutes: false` to its read-only mount options, and an owned
`mount-read-only.ts` needs the re-sync described under the served-report entry above. Gated
in all five ports by `projection/keyless-no-item-route.yaml`.
- **TypeScript: a read-only projection whose identity is on a field not named `id` now
addresses that field.** The shape: a view-only `object.projection` whose
`identity.primary` names a single field such as `code`, or a composite identity whose first
Expand Down Expand Up @@ -197,6 +202,13 @@ until you regenerate.
`GET <path>/{id}` only when the projection has an item route (the rule above), and the
filter allowlist. No write verb is listed, since the router answers those with `405`.
Entities and write-through objects are unchanged. Regenerate the docs to pick the pages up.
- **Java and Kotlin API docs: a read-only projection's page lists the reads only.** The
api-docs builders listed `POST`, `PATCH`, `PUT` and `DELETE` on a read-only projection, each
described as a `405` refusal, beside `GET`. The other three ports never documented a write
verb on one, and a refusal is not an operation a caller can use, so the page now lists `GET
<path>` and, when the projection declares an identity, `GET <path>/{id}`. The generated
controllers are unchanged. Regenerate the docs to pick it up. Gated in all five ports by
`projection/docs-routes.json`.
- **Java: a filter allowlist with more than ten filterable fields now compiles.**
`SpringFilterAllowlistGenerator` spelled `OPS_BY_FIELD` with `Map.of`, which has no overload
past ten pairs, so an entity or projection with eleven or more `@filterable` fields generated
Expand Down
7 changes: 3 additions & 4 deletions agent-context/skills/metaobjects-codegen/references/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,9 @@ Its REST surface is generated and READ-ONLY (F22): GET list + GET by id, the sam
projection's OWN declared field set, and `POST` / `PATCH` / `PUT` / `DELETE` each
answering `405 {"error": "method_not_allowed"}` — 405 and not 404 because the same
path answers GET. A KEYLESS projection mounts no `/{id}` route at all, so it refuses only
the collection verb, and its repository Protocol has no `find_by_id`. In Python keyless
means no `identity.primary` AND no field named `id`: a projection with an `id` field and
no declared identity keeps its item routes (C#, Java and Kotlin are stricter and need a
declared single-column identity).
the collection verb, and its repository Protocol has no `find_by_id`. Keyless means no
declared `identity.primary`: a field that is merely named `id` is a convention, not a key, so
a projection with an `id` field and no declared identity is keyless too (in every port).
The read-only router is a separate assembly from the writable one, sharing only the
emitters they genuinely have in common; `router_generator` and
`filter_allowlist_generator` ask one shared `emits_router()` predicate.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,9 @@ Its REST surface is generated and READ-ONLY (F22): GET list + GET by id, the sam
projection's OWN declared field set, and `POST` / `PATCH` / `PUT` / `DELETE` each
answering `405 {"error": "method_not_allowed"}` — 405 and not 404 because the same
path answers GET. A KEYLESS projection mounts no `/:id` route at all, so it refuses only
the collection verb, and it gets no `find…ById` query and no detail hook. In TypeScript
keyless means no `identity.primary` AND no field named `id`: a projection with an `id`
field and no declared identity keeps its item routes (C#, Java and Kotlin are stricter and
need a declared single-column identity).
the collection verb, and it gets no `find…ById` query and no detail hook. Keyless means no
declared `identity.primary`: a field that is merely named `id` is a convention, not a key, so
a projection with an `id` field and no declared identity is keyless too (in every port).
`routesFile()` mounts it through `mountReadOnlyCrudRoutes` from the drizzle-fastify
adapter (your `codegen/runtime/` copy once ejected), which is where the refusals live.
A `field.decimal` in a view's read schema is `z.string()`: the driver reads `numeric` as
Expand Down
14 changes: 8 additions & 6 deletions docs/CONFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ regenerate with `ls -d fixtures/<corpus>/*/ | wc -l` for directory-shaped corpor
| [`fixtures/extract-conformance/`](../fixtures/extract-conformance/) | 48 | ✓ | ✓ | inherits the shared JVM engine | ✓ | ✓ |
| [`fixtures/output-prompt-conformance/`](../fixtures/output-prompt-conformance/) | 17 | ✓ | ✓ | ✓ | ✓ | ✓ |
| [`fixtures/persistence-conformance/`](../fixtures/persistence-conformance/) | 39 (33 query + 6 migration) | all 39 | 33 query (migrations TS-only, ADR-0015) | 33 query (via Exposed) | 33 query | 33 query |
| [`fixtures/api-contract-conformance/`](../fixtures/api-contract-conformance/) | 73 (31 core + 10 tph + 9 m2m + 2 jsonb + 2 write-through + 7 projection + 12 report) | ✓ (Fastify reference + generated lane) | ✓ (embedded HTTP + JDBC) | ✓ (embedded HTTP + Exposed) | ✓ (HttpListener + Npgsql) | ✓ (FastAPI + pg8000) |
| [`fixtures/api-contract-conformance/`](../fixtures/api-contract-conformance/) | 78 (31 core + 10 tph + 9 m2m + 2 jsonb + 2 write-through + 11 projection + 13 report) | ✓ (Fastify reference + generated lane) | ✓ (embedded HTTP + JDBC) | ✓ (embedded HTTP + Exposed) | ✓ (HttpListener + Npgsql) | ✓ (FastAPI + pg8000) |
| [`fixtures/validation-conformance/`](../fixtures/validation-conformance/) | 42 cases | ✓ (generated Zod + run-time `runValidators`) | ✓ | ✓ | ✓ | ✓ (generated Pydantic + run-time `run_validators`) |
| [`fixtures/registry-conformance/`](../fixtures/registry-conformance/) | 1 canonical manifest | ✓ (reference emitter) | ✓ | ✓ | ✓ | ✓ |
| [`fixtures/object-model-conformance/`](../fixtures/object-model-conformance/) | 1 shared metadata fixture (per-port scenarios) | ✓ | ✓ | ✓ | ✓ | ✓ |
Expand Down Expand Up @@ -257,9 +257,9 @@ All 31 fixtures → [features/migrations-and-drift.md](features/migrations-and-d
- `migrations/*` (6) → [features/migrations-and-drift.md](features/migrations-and-drift.md) (schema migration section)
- `queries/*` (33) → [features/source-kinds.md](features/source-kinds.md) (query semantics against `source.rdb`)

### `fixtures/api-contract-conformance/` (73)
### `fixtures/api-contract-conformance/` (78)

All 73 scenarios → [features/api-contract.md](features/api-contract.md) (cross-port
All 78 scenarios → [features/api-contract.md](features/api-contract.md) (cross-port
REST API URL grammar + JSON wire format). Verifies every backend's emitted CRUD
routes answer identically over HTTP — list / get / create / patch+put / delete,
plus pagination (`limit`/`offset`), sort (`sort=field:dir`), the `withCount=1`
Expand All @@ -274,8 +274,10 @@ scenarios the corpus carries six sub-corpora — `tph/` (10, single-table
inheritance), `m2m/` (9 — 3 plain, 5 gating TPH x M:N together, the combination
each corpus alone could not reach, and 1 pinning the collection-URL spelling),
`jsonb/` (2, typed value-object columns),
`write-through/` (2, table-write + view-read entities), `projection/` (7, a
read-only view answers reads and refuses writes with 405) and `report/` (12,
`write-through/` (2, table-write + view-read entities), `projection/` (11, a
read-only view answers reads and refuses writes with 405; a projection with no declared
identity has no item route; a projection keyed on a field not named `id` is addressed by
it; decimal and float fields filter) and `report/` (13,
FR-044: a view-backed `object.report` is listed, filtered, sorted and paged on
its derived fields, answers `POST` with 405 and mounts no `/{id}`). All 5 ports — TS, Java,
Kotlin, C#, Python — run it in BOTH lanes: a hand-rolled reference server and
Expand Down Expand Up @@ -405,7 +407,7 @@ own those two functions), and
## Orphaned fixtures (tested but not yet documented)

The fixtures in the nine corpora mapped above (metamodel 364 + yaml 16 + verify 31
+ render 15 + persistence 39 + api-contract 73 + source-resolution 25 + scope 10 +
+ render 15 + persistence 39 + api-contract 78 + source-resolution 25 + scope 10 +
dependency 23) each map to a feature doc. None are orphaned today. The remaining
corpora in the totals table gate tooling contracts (registry manifests, provider
composition, agent context, docs emit) rather than user-facing metamodel behaviour,
Expand Down
34 changes: 18 additions & 16 deletions docs/features/api-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,21 +425,23 @@ The surface:
on the collection, `PATCH` / `PUT` / `DELETE` on the item. 405 rather than 404
because the resource plainly exists: the same path answers `GET`. `message` is free
prose and is not part of the contract.
- A **keyless** projection mounts no `/{id}` route at all, so it refuses only the
collection verb — refusing an item verb would advertise an address the port never
serves. What counts as keyless differs by port, and the corpus does not gate it:

| Port | Item routes are mounted when |
|---|---|
| C#, Java, Kotlin | the projection declares or inherits a **single-column** `identity.primary` |
| TypeScript, Python | it declares or inherits an `identity.primary` (a composite one binds its first field), **or** it declares none and has a field named `id` |

So a projection with an `id` field and no declared identity has item routes in
TypeScript and Python and none in the other three. Until FR-044, TypeScript and Python
mounted the item routes for **every** read-only projection, including one with no
identity and no `id` field, where they could not address a row (TypeScript answered the
view's first row). That one shape lost its `/{id}` routes, its by-id query and, in
TypeScript, its detail hook; every other projection is unchanged.
- A **keyless** projection, one that declares and inherits no `identity.primary`, mounts no
`/{id}` route of any verb in any port, so it refuses only the collection verb —
refusing an item verb would advertise an address the port never serves. A field that
happens to be named `id` is a convention, not a key: a projection with an `id` field and
no declared identity is keyless. `GET /<plural>/1` on one answers the framework's own
`404`, never a row. A projection with a declared identity addresses a row by the field
that identity names, which need not be called `id` and which the view's column must
match: `GET /<plural>/{key}` answers that row, or the `404 {"error": "not_found"}`
envelope. One difference remains, and the corpus does not gate it: C#, Java and Kotlin
mount item routes only for a **single-column** identity, while TypeScript and Python
bind a composite one to its first field.
- Filters apply to a projection's `field.decimal` and `field.float` fields as they do to
an entity's. A decimal's wire spelling is port-specific and is not asserted.
- **The api docs list the same routes.** A projection's api page documents `GET <path>`,
and `GET <path>/{id}` only when the item route exists. The `405` refusals are not
operations a caller can use, so no write verb is documented. Pinned in every port by
`projection/docs-routes.json`.

Every port mounts those refusals **explicitly**. Left to the framework, ASP.NET and
Spring each answer an unmatched method on a matched path with an empty-bodied 405 and
Expand Down Expand Up @@ -472,7 +474,7 @@ entity's. No request parameter picks dimensions, measures or a grain. No port ge
client hook, grid or form for a report yet.

Gated by [`fixtures/api-contract-conformance/report/`](../../fixtures/api-contract-conformance/report/)
(12 scenarios), **generated lane only, on all five ports**, for the reason `projection/`
(13 scenarios), **generated lane only, on all five ports**, for the reason `projection/`
gives. The columns, their types and the per-port generated files are in
[reporting.md](reporting.md#how-a-report-is-served).

Expand Down
Loading
Loading