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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ PyPI has had no product change since `0.25.0` — nothing is broken.
- YAML / verify corpora green across the ports that ship those layers.
- **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".

**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 (the reporting vocabulary — `dimension.attribute`/`dimension.time`, `measure.aggregate`/`measure.ratio`, `segment.filter`, `object.report`, `@spine` on a report to take rows from a to-one dimension entity, `@default` on a measure for empty aggregates, plus the relative-date filter value — registered and loader-validated in all five ports; a report that declares a read-only `source.rdb @kind: view` is lowered to a SQL view by `meta migrate` and read by every port, and served by a generated read-only list route in every port, gated by the api-contract `report/` sub-corpus; the TypeScript `cube-model` reference generator writes the vocabulary as Cube data-model files, gated by the `fixtures/cube-model/` corpus and a `cube` lane that loads the output into a real Cube; a served report gets the generated TanStack list hook (`use<R>List`) but no grid or form, see [docs/features/reporting.md](docs/features/reporting.md) and [docs/features/cube-export.md](docs/features/cube-export.md)).
**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 (the reporting vocabulary — `dimension.attribute`/`dimension.time`, `measure.aggregate`/`measure.ratio`, `segment.filter`, `object.report`, `@spine` on a report to take rows from a to-one dimension entity, `@default` on a measure for empty aggregates, plus the relative-date filter value — registered and loader-validated in all five ports; a report that declares a read-only `source.rdb @kind: view` is lowered to a SQL view by `meta migrate` and read by every port, and served by a generated read-only list route in every port, gated by the api-contract `report/` sub-corpus; the TypeScript `cube-model` reference generator writes the vocabulary as Cube data-model files, gated by the `fixtures/cube-model/` corpus and a `cube` lane that loads the output into a real Cube; a served report gets the generated TanStack list hook (`use<plural of R>`, or `use<R>List` when the plural is the name) but no grid or form, see [docs/features/reporting.md](docs/features/reporting.md) and [docs/features/cube-export.md](docs/features/cube-export.md)).

**Release 1.1.0** (released; npm `1.1.0`, PyPI `1.1.0`, NuGet `1.1.0` and Maven Central `8.1.0`; the latest is 1.2.0 / 8.2.0). The reporting release (FR-044): declare dimensions, measures, segments and an `object.report`, and get a SQL view, a served read-only list route in every port, a generated TanStack list hook, and a Cube data model, with `@spine` for rows that have no facts and `@default` for an empty measure. Also carries the first slice of requirements (the gate in every port's `verify`), the derived-key parity fix across the ports, and the list-hook and nested-average fixes found by the first adopter. See the `CHANGELOG.md` 1.1.0 section for the full scope, pre-release RC history, and the diffs to expect when you upgrade (ejected generators and keyless projections). It also makes documentation a set of generators in every port (ADR-0034 Amendment 9): the docs commands are removed, and `docs/features/migrations/docs-are-generators.md` is the upgrade path. The last 1.0.x release, 1.0.13 (2026-10-03), was a PATCH on 1.0.x.

Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,12 @@ Each item below is described in full under Added, Changed or Fixed; this list is

### Documentation

- **Index names and list-hook names, corrected.** The authoring reference's index examples are
table-scoped (`userByEmail`, not `byEmail`) and say an index name is schema-wide, so a repeated
one fails `meta migrate` with `ERR_DUPLICATE_SQL_NAME`. The generated list hook is
`use<plural>`, or `use<Name>List` when the plural equals the name; the reporting page had it
backwards, and the codegen and runtime-UI skills named only one form.

- **Separate-repo model sharing (FR-023 Phase 2) close-out.** The feature page's Phase 2 section is now
the shipped per-port matrix (consume, check, publish, runtime load, transports), the deferred list
names what is still not built, and the verify, codegen, authoring, audit and capability-sheet skills
Expand All @@ -518,6 +524,14 @@ Each item below is described in full under Added, Changed or Fixed; this list is

### Changed

- **Quieter `meta verify` advisories.** The anti-pattern scanner skips comment lines; it flags
minor-unit math (`* 100`, `/ 100`, `.toFixed(2)`, `parseFloat`) only when the operand is
money-named, so hours formatted to two places no longer read as money; and a `.reduce` counts
as a hand-rolled aggregate only when it accumulates with `+` outside `.tsx`/`.jsx` UI source.
Unindexed foreign keys are one finding naming every key and its `index.lookup`, not one per
key. With a `--format` payload on stdout, the text on stderr gives each section's header and a
pointer instead of repeating every finding.

- **The requirements reference pages install in every stack.** They used to install only when
`meta init` saw a `requirement.*` node, which a new project has not written yet, so a project
that declared requirements later had skills pointing at pages that were not there. `meta gen`
Expand Down Expand Up @@ -663,6 +677,22 @@ Each item below is described in full under Added, Changed or Fixed; this list is

### Fixed

- **Generated routes on Postgres answer 400, not 500, for bad uuid input.** On an entity with a
uuid foreign key, `?search=` ran ILIKE against the uuid column (the allowlist carries it as
`subType: "string", format: "uuid"`), and Postgres has no `uuid ~~* text` operator. The search
now skips string rules that carry a `format`. A malformed uuid in `:id` reached Postgres as a
literal ("invalid input syntax for type uuid"); `coerceIdForColumn` now rejects it, so GET,
PATCH and DELETE answer 400 `{"error":"invalid_id"}` as a malformed numeric id already did. A
`field.uuid` in the generated insert and update Zod schemas now checks the 8-4-4-4-12 shape, so
a malformed uuid foreign key in a POST or PATCH body answers 400 `validation`. SQLite stores a
uuid as text, so none of this showed there; a new integration test runs the stock
`routesFile()` output against Postgres with uuid keys and a uuid foreign key. Owned copies
pick the fix up when re-ejected.
- **`meta migrate` warns when a renamed column is still named by a view.** A rename never
rewrites a view body, so a view whose `@sql` still names the old column is recreated as
declared and the migration fails at apply. The plan now names the view, the column and its new
name, and the "recreated only because" note no longer lists that view among the harmless ones.

- **An enum's `@default` is what an untouched record stores**. C#: a scalar enum
started at `default(TEnum)`, not at `@default`, while the EF sentinel was `@default`, so EF
wrote it: a string-stored or int-mapped enum stored its first member, a nullable one NULL, and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,16 @@ An `identity.secondary` can index an **expression** instead of plain columns: us
index method — `gin` / `gist` / `hash`; default `btree`) and `@where` (a partial-index
predicate).

**Name every `identity.secondary` and `index.lookup` after its table.** The name is used
verbatim as the database index name, and index names are unique across the whole schema,
not per table. Two entities that both declare `byName` load and generate cleanly, then
`meta migrate` stops with `ERR_DUPLICATE_SQL_NAME`. Prefix the entity (`clientByName`,
`tagByName`).

```json
{ "identity.primary": { "name": "id", "@fields": ["id"], "@generation": "increment" } }
{ "identity.secondary": { "name": "byEmail", "@fields": ["email"] } }
{ "identity.secondary": { "name": "byEmailCI", "@expr": "lower(email)" } }
{ "identity.secondary": { "name": "userByEmail", "@fields": ["email"] } }
{ "identity.secondary": { "name": "userByEmailCI", "@expr": "lower(email)" } }
{ "identity.reference": { "name": "fkAuthor", "@fields": ["authorId"], "@references": "Author", "@enforce": true } }
```

Expand Down Expand Up @@ -73,10 +79,10 @@ The db provider contributes physical-tuning attrs alongside either form: `@order
`btree`), and `@where` (partial-index predicate).

```json
{ "index.lookup": { "name": "byCreatedAt", "@fields": ["createdAt"], "@orders": ["desc"] } }
{ "index.lookup": { "name": "byStatusCreatedAt", "@fields": ["status", "createdAt"] } }
{ "index.lookup": { "name": "byEmailCI", "@expr": "lower(email)" } }
{ "identity.secondary": { "name": "uniqLowerEmail", "@expr": "lower(email)" } }
{ "index.lookup": { "name": "orderByCreatedAt", "@fields": ["createdAt"], "@orders": ["desc"] } }
{ "index.lookup": { "name": "orderByStatusCreatedAt", "@fields": ["status", "createdAt"] } }
{ "index.lookup": { "name": "userByEmailCI", "@expr": "lower(email)" } }
{ "identity.secondary": { "name": "userUniqLowerEmail", "@expr": "lower(email)" } }
```

`index.lookup` is a sibling of `identity.*` — declare it as a direct child of an `object.entity`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ 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. The UI tier
writes the list hook and nothing else: `<R>.hooks.ts` (`use<R>List`, typed with the report's
writes the list hook and nothing else: `<R>.hooks.ts` (`use` + the plural of the name (`useMemberUtilizations` for `MemberUtilization`), or `use<R>List` when the name's plural is the name itself (`useSeriesList`), 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,
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 @@ -115,8 +115,9 @@ An `object.projection` uses the same rule, so `OrderSummary` is at
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, in
TypeScript, a generated list hook (`use<Report>List`); it has no grid, form or detail
hook.
TypeScript, a generated list hook named `use` + the plural of the report's name
(`useMemberUtilizations` for `MemberUtilization`; `use<Report>List` only when the plural is
the name itself, as in `useSeriesList`); 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
1 change: 1 addition & 0 deletions docs/features/api-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ or Hono `onError` the adopter installed, answer exactly as they did before.
| Response | When |
|---|---|
| malformed JSON body | a `POST`/`PATCH`/`PUT` body that does not parse as JSON (an empty body sent as `application/json` included) → HTTP 400 `{ "error": "invalid_json" }`. An EMPTY body on a method that takes none (`GET`, `DELETE`, `HEAD`) is accepted even when the request declares `content-type: application/json`, as many clients do on every request; a non-empty `DELETE` body is still parsed. Before, Fastify answered its own `{ "statusCode": 400, "code": "FST_ERR_CTP_INVALID_JSON_BODY", … }` and Hono a Zod `validation` error about a missing object |
| malformed id | a `:id` the key column cannot hold (a malformed uuid on a Postgres uuid key, a non-numeric id on an integer key) → HTTP 400 `{ "error": "invalid_id" }` on GET, PATCH and DELETE. A malformed uuid in a POST or PATCH body's foreign key → HTTP 400 `{ "error": "validation" }`, from the insert/update schema's `8-4-4-4-12` hex check. Postgres used to answer 500 for both. The check is TS-only |
| malformed page bound | a `?limit=` or `?offset=` that is not a non-negative integer (`abc`, `-5`, `1.5`, an empty value) → HTTP 400 `{ "error": "pagination.invalid_value", "param": "limit", "value": "abc", "expected": "a non-negative integer (0 or more)" }`. Before, a non-numeric bound was dropped (every row came back) and a negative or fractional one reached SQL. There is no page-size maximum on an entity or a projection with declared columns; an opaque view (no declared columns, served by raw SQL) pages to at most 1000 rows, so there a `limit` above 1000 is refused too, with `"expected": "an integer from 0 to 1000"` (it used to be clamped silently). The same answer comes from the Drizzle mounts and the ObjectManager Fastify mount. The corpus sends a malformed bound only to a report (`report/bad-page-bound`), where every port answers this same envelope and also refuses `limit=0`; on an entity or a projection the other ports answer it as their framework does |
| database constraint violation | a write the database itself rejects → `{ "error": "constraint_violation", "constraint": "<kind>" }`, HTTP **409** for `unique` and `foreign_key` (a conflict with another row) and HTTP **400** for `check` and `not_null` (the request's own value), per the status rule under "Error response". The body names the constraint KIND only — never the SQL, the table or the bound values. A `CHECK` the model derives (an enum, a `validator.numeric`/`length`/`regex` range) is normally refused earlier by the route's own validation, as the corpus's `{ "error": "validation" }` 400; this row is the database's backstop |
| schema validation failure | a body that fails the insert/update schema → HTTP 400 `{ "error": "validation", "issues": [...] }`, each issue as Zod reports it **minus `pattern`**: Zod 4 attaches a failed `.regex()` check's source there, which for a generated `field.date` / `field.time` / `field.timestamp` check is a ~300-character calendar regex (and an authored `validator.regex` pattern is the server's to keep). The generated temporal checks carry a short `message` naming the expected format instead (`"must be an ISO date (YYYY-MM-DD)"`) |
Expand Down
6 changes: 5 additions & 1 deletion docs/features/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -583,9 +583,13 @@ the construct that replaces it:
| Hand-rolled pattern | Suggested construct |
|---|---|
| an aggregate computed by hand (SQL `AVG`/`SUM`, a summing `.reduce(...)`) | For grouped, served or reported figures: `object.report` (dimensions + measures, read-only list route). For a per-row figure on one entity: `object.projection` with `origin.aggregate` and a generated query. |
| money as a float / hand-rolled minor units (a money-named field with `* 100`, `/ 100`, `.toFixed(2)`, `parseFloat`) | `field.currency` |
| money as a float / hand-rolled minor units (`* 100`, `/ 100`, `.toFixed(2)`, `parseFloat` on a money-named operand) | `field.currency` |
| a fixed value set enforced by a SQL `CHECK (... IN (...))` | `field.enum` |

The scan skips comment lines. A `.reduce` counts only when it accumulates into its first
parameter with `+`, and `.tsx`/`.jsx` UI files are not scanned for it, so on-screen totals
are not flagged.

It is **warnings only** — it never changes the exit code (bias to under-flagging;
a >15% false-positive rate is a project kill criterion). This pass is
**Node-`meta`-specific**; the C#/Java/Kotlin/Python codegen surfaces do not run it.
Expand Down
6 changes: 6 additions & 0 deletions docs/features/migrations-and-drift.md
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,12 @@ the case of an `@sql` body that joins it. `meta migrate` finds it by name in the
stored definition, and recreates the view only when that definition also names the changed
column. The same two costs apply.

A rename never rewrites a view body. When a migration renames a column and a view's `@sql`
still names the old one, that view is recreated as declared and the migration fails at apply.
`meta migrate` prints an advisory for it (`view <name> still names <table>.<column>, which this
migration renames to <new>`), so the view, column and new name are all shown. The advisory does not block the plan. Change the view's `@sql` to the
new name before you apply.

SQLite and D1 have no `CREATE OR REPLACE VIEW`: every replaced view is written as `DROP VIEW`
and `CREATE VIEW` there, so there is no refusal to avoid and no probe. They refuse something
else: a `DROP COLUMN`, or the rebuild of a table, while a view that reads the table stands. So
Expand Down
Loading
Loading