@metaobjectsdev/sdk 0.16.0-rc.1 → 0.17.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/agent-context/skills/metaobjects-audit/SKILL.md +25 -12
  2. package/agent-context/skills/metaobjects-audit/references/capability-checklist.md +54 -20
  3. package/agent-context/skills/metaobjects-audit/references/csharp.md +7 -4
  4. package/agent-context/skills/metaobjects-audit/references/java.md +12 -10
  5. package/agent-context/skills/metaobjects-audit/references/kotlin.md +11 -7
  6. package/agent-context/skills/metaobjects-audit/references/python.md +7 -4
  7. package/agent-context/skills/metaobjects-audit/references/typescript.md +1 -1
  8. package/agent-context/skills/metaobjects-authoring/SKILL.md +171 -39
  9. package/agent-context/skills/metaobjects-codegen/SKILL.md +45 -16
  10. package/agent-context/skills/metaobjects-codegen/references/csharp.md +21 -1
  11. package/agent-context/skills/metaobjects-codegen/references/java.md +43 -5
  12. package/agent-context/skills/metaobjects-codegen/references/kotlin.md +34 -4
  13. package/agent-context/skills/metaobjects-codegen/references/python.md +32 -8
  14. package/agent-context/skills/metaobjects-codegen/references/typescript.md +19 -3
  15. package/agent-context/skills/metaobjects-fit-assessment/SKILL.md +55 -18
  16. package/agent-context/skills/metaobjects-prompts/SKILL.md +65 -4
  17. package/agent-context/skills/metaobjects-prompts/references/csharp.md +21 -0
  18. package/agent-context/skills/metaobjects-prompts/references/java.md +32 -3
  19. package/agent-context/skills/metaobjects-prompts/references/kotlin.md +33 -3
  20. package/agent-context/skills/metaobjects-prompts/references/python.md +29 -3
  21. package/agent-context/skills/metaobjects-prompts/references/typescript.md +34 -1
  22. package/agent-context/skills/metaobjects-runtime-ui/SKILL.md +1 -1
  23. package/agent-context/skills/metaobjects-runtime-ui/references/csharp.md +90 -0
  24. package/agent-context/skills/metaobjects-runtime-ui/references/python.md +94 -0
  25. package/agent-context/skills/metaobjects-verify/SKILL.md +50 -15
  26. package/agent-context/skills/metaobjects-verify/references/migration.md +67 -14
  27. package/dist/agent-docs/body.d.ts +1 -1
  28. package/dist/agent-docs/body.d.ts.map +1 -1
  29. package/dist/agent-docs/body.js +3 -1
  30. package/dist/agent-docs/body.js.map +1 -1
  31. package/package.json +2 -2
  32. package/src/agent-docs/body.ts +3 -1
@@ -291,9 +291,10 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend
291
291
  5. **Runtime schema patching** (`ALTER TABLE … ADD COLUMN IF NOT EXISTS`, `_ensure_schema()`) — N schema owners.
292
292
  6. **N declarations of one shape** — same entity as Drizzle table + Zod schema + Pydantic model + hand dataclass; target is 1 + N generated.
293
293
  7. **`own*()` accessor read of an effective property** (ADR-0039) — `ownAttr` / `ownFields` / `own_children` / bare Python `attr(` / `getMetaAttr(name, false)` / native `IsArray` used to read a value or iterate members outside the sanctioned subclass-emit / own-serializer / `@dbColumnType` cases → silently drops `extends`-inherited values. A **correctness defect** (axis G2), not advisory.
294
- 8. **Hand-written `CREATE VIEW` / read-only SQL standing in for a projection (view-necessity test).** Grep migrations, checked-in `.sql`, and repository/query code for `CREATE [OR REPLACE] [MATERIALIZED] VIEW`, and for hand-rolled read-only queries that mirror a read model — a pure-`SELECT` repository/service method with joins or `GROUP BY` feeding a DTO, or a raw-SQL escape (`db.execute(sql…)`, `FromSqlRaw`, a JPA @Query with hand-written SQL). For each, run the **necessity test** — can `object.projection` + origins express this shape? It can when every output column is (a) a base-entity or relationship-joined column → `origin.passthrough` (`@from` / `@via`), (b) a count/sum/avg/min/max over related rows → `origin.aggregate` (`@agg` / `@of` / `@via`, optionally row-scoped with `@filter`), (c) a child collection → `origin.collection` (`@via`), or (d) a column borrowed via `extends` — and the joins follow declared relationships / `identity.reference` FKs.
295
- - **ExpressibleCODEGEN CANDIDATE (high):** convert to an `object.projection` with a read-only `source.rdb` `@kind: view` child, let `meta migrate` emit the `CREATE VIEW`, and consume the generated read-only query the hand-written view is a second source of truth for a derivable shape. Flag in the finding that **`meta verify --db` cannot catch this**: an unmodeled DB view is *unmanaged*, so this audit is the only gate that sees it. Parity-gate: the generated view returns row-identical results before the hand-written SQL is deleted.
296
- - **Not expressible → BESPOKE (keep), with a NAMED justification:** record the construct that makes it irreducible (recursive CTE, window function / `OVER`, `UNION` / `INTERSECT` / `EXCEPT`, `DISTINCT ON`, lateral join, non-aggregate expression column). "It's an aggregation" is NOT a justification plain count/sum/avg/min/max rollups are `origin.aggregate`.
294
+ 8. **Hand-written `CREATE VIEW` / read-only SQL standing in for a projection or entity read-view (view-necessity test).** Grep migrations, checked-in `.sql`, and repository/query code for `CREATE [OR REPLACE] [MATERIALIZED] VIEW`, and for hand-rolled read-only queries that mirror a read model — a pure-`SELECT` repository/service method with joins or `GROUP BY` feeding a DTO, or a raw-SQL escape (`db.execute(sql…)`, `FromSqlRaw`, a JPA @Query with hand-written SQL). For each, run the **necessity test** — can origins express this shape? A column is derivable when it is (a) a base-entity or relationship-joined column → `origin.passthrough` (`@from` / `@via`), (b) a count/sum/avg/min/max over related rows → `origin.aggregate` (`@agg` / `@of` / `@via`), or an `EXISTS` / `array_agg` → `origin.aggregate` `any` / `all` / `collect` — any of them optionally row-scoped with `@filter`, (c) a child collection → `origin.collection` (`@via`), (d) a computed scalar / **non-aggregate expression column** → `origin.computed` (`@expr`, #195), (e) one related row's column picked by an ordering — an **argmax / `DISTINCT ON … ORDER BY` / correlated `ORDER BY … LIMIT 1`** → `origin.first` (`@of` / `@via` / `@orderBy`, #195), or (f) a column borrowed via `extends` — and the joins follow declared relationships / `identity.reference` FKs.
295
+ - **Entity-shaped (`SELECT own.* + derived`) an entity read-view, NOT a projection (#214).** The single most common legacy view is an entity's OWN columns plus a joined/derived extra (`SELECT o.*, c.name AS customer_name`). This is the `Order` **entity with a read route**, not an exposure contract — route it to an **entity read-view**: keep the writable `@role: primary` `@kind: table` source and add a **non-primary `@role: replica` `@kind: view`** source, declaring only the *extra* as a derived `origin.*` field on the entity (the own field set already covers `o.*`). Reads route to the replica view, writes to the table. Reach for a **projection** instead only when the view renames base columns or **row-filters** (`WHERE status='active'`, soft-delete) — the latter is a projection with an object-level `@filter` (#207) that lowers to the outer `WHERE`.
296
+ - **Exposure contract, expressible → CODEGEN CANDIDATE (high):** a subset / renamed / versioned / multi-base read model convert to an `object.projection` with a read-only `source.rdb` `@kind: view` child, let `meta migrate` emit the `CREATE VIEW`, and consume the generated read-only query the hand-written view is a second source of truth for a derivable shape. Parity-gate: the generated view returns row-identical results before the hand-written SQL is deleted.
297
+ - **Not expressible → carry it in `@sql` or `@unmanaged`, never a hand-edited migration (#208, ADR-0043).** When a NAMED irreducible construct blocks origin authoring — recursive CTE, window function / `OVER`, `UNION` / `INTERSECT` / `EXCEPT`, lateral join — the body still belongs in the metadata: carry the hand-written SQL in the `source.rdb` **`@sql`** escape — a read-only-`@kind` body the tool REGISTERS, fingerprints, and drift-checks (adopt a pre-existing view with `meta migrate --allow adopt-view`); `@sql` forbids `origin.*` children (two sources of truth). A DB object whose DDL is owned **entirely elsewhere** (Flyway / a hand-migration) → mark its source **`@unmanaged: true`** (legal on any `@kind` incl. `table`); `meta migrate` never creates/drops/drift-checks it and `verify --db` reports it as external. `@sql` and `@unmanaged` are mutually exclusive. **Only a view left *undeclared*** — neither modeled, nor `@sql`, nor `@unmanaged` — is truly *unmanaged*, invisible to `meta verify --db`, so this audit is the only gate that sees it. "It's an aggregation" is NOT an irreducibility justification (plain count/sum/avg/min/max rollups are `origin.aggregate`); nor is a `DISTINCT ON` pick-one-row (`origin.first`) or a non-aggregate expression column (`origin.computed`).
297
298
  9. **A closed variant-set hand-modeled per instance** — N sibling modules / classes / config blocks, one per channel / provider / target, sharing a payload + config shape and diverging only by transport. Grep for sibling-file families and switch-on-a-string dispatch; verify the set is closed and recurring (never a one-off). → axis I "New-vocabulary OPPORTUNITY" (VOCAB CANDIDATE, advisory).
298
299
 
299
300
  ---
@@ -313,10 +314,12 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend
313
314
  own + customize / author a template-spec / fix upstream / stopgap.
314
315
  - **Verify the DB artifact, not just the types** — computed view columns may appear in the
315
316
  contract but be dropped from the view DDL; the contract may lie.
316
- - **A hand-authored DB view is invisible to `meta verify --db`.** An unmodeled view is
317
- *unmanaged* (informational only never actionable drift, never auto-dropped), so a
318
- hand-written view standing in for an expressible `object.projection` can never be
319
- outsourced to the drift gate; hunt it here (drift signature 8, below).
317
+ - **An *undeclared* hand-authored DB view is invisible to `meta verify --db`.** A view that is
318
+ neither modeled nor carried in the `source.rdb` `@sql` / `@unmanaged` escapes is *unmanaged*
319
+ (informational only never actionable drift, never auto-dropped), so a hand-written view
320
+ standing in for an expressible `object.projection` / entity read-view can never be outsourced
321
+ to the drift gate; hunt it here (drift signature 8, below). Once carried in `@sql` (#208) it IS
322
+ registered, fingerprinted, and drift-checked — no longer audit-only.
320
323
  - **Version skew:** check *actually-resolved* package versions, not declared; consuming a fix
321
324
  requires a coordinated lockstep bump, not a source-file copy.
322
325
 
@@ -329,8 +332,9 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend
329
332
  `object.value` with `origin.*` (`passthrough` / `aggregate` / `collection`) fields.
330
333
  - Silent-degradation hack (`try/except KeyError` or `?? ''` around formatting) — flag every instance.
331
334
  - Hand-rolled output parsing (regex / XML / ad-hoc JSON) vs declared `template.output` +
332
- generated `parse*` / `safeParse*` / `extract*` parser. **Java hand-writes the Jackson
333
- one-liner do NOT flag it** Calibration).
335
+ generated `parse*` / `safeParse*` / `extract*` parser **generated in all five ports**
336
+ (Java's generated `<Name>Parser` owns the Jackson `readValue`); flag a hand-rolled parser
337
+ in a **non-generated** file where a `template.output` node exists.
334
338
  - Engine-side formatting breaking byte-identical render (prompt-cache exact-prefix hits
335
339
  depend on byte-stability).
336
340
  - `template.toolcall` candidates: LLM tool schemas hand-defined per call vs modeled
@@ -445,9 +449,18 @@ The audit never edits code. Pattern: **dry-run → review the diff → apply**.
445
449
 
446
450
  ## Calibration — port gaps & non-defects (do NOT flag these as adopter fault)
447
451
 
448
- - **Filter-operator route codegen** is full only in **TS**; Java/Kotlin/C#/Python generate
449
- pagination/sort/`withCount` but defer filter ops — do not flag hand-added filter handling.
450
- - **Output-parser codegen** ships TS/C#/Python/Kotlin; **Java hand-writes the Jackson parse** not a defect.
452
+ - **Filter-operator route codegen the CORE grammar ships in all five ports.** The
453
+ `?filter[field][op]=value` grammar (all 9 operators `eq/ne/gt/gte/lt/lte/in/like/isNull`,
454
+ implicit-AND across params, the generated `<Entity>FilterAllowlist` + `invalid-field` /
455
+ `invalid-op` / `in`-over-cap 400s) is **generated in every port** (Java `SpringControllerGenerator`,
456
+ C# `RoutesGenerator`, Python `router_generator`, Kotlin `KotlinSpringControllerGenerator`, TS) —
457
+ gated by the api-contract corpus in BOTH lanes. **Flag hand-rolled filter parsing anywhere.**
458
+ Only the *richer* surface is genuinely **TS-only**: free-text `?search=`, the explicit
459
+ `filter[or][N]` / `filter[and][N]` nested boolean combinators (+ their nesting-depth cap), and
460
+ leading-wildcard gating — do NOT flag the absence of those in a non-TS port.
461
+ - **Output-parser codegen** ships in **all five ports** — Java's `SpringOutputParserGenerator`
462
+ *generates* the `<Name>Parser` (the Jackson `readValue` lives inside that generated file). A
463
+ hand-rolled parser in a **non-generated** file where a `template.output` node exists IS a finding.
451
464
  - **Python** still hand-wires the FastAPI router + repository impl around a generated
452
465
  `APIRouter`; relationship / non-`table` source-kind / `field.object flattened` codegen is partial.
453
466
  - **C#** has no ObjectManager runtime tier (EF Core is the runtime) — hand services over the generated `DbContext` are expected.
@@ -53,8 +53,10 @@ classify it (using the classification scheme in `SKILL.md`) and route the cutove
53
53
  assignment), `@required` (hand presence checks), `@unique` (hand uniqueness), `@readOnly`
54
54
  (hand write-guards), `@filterable` / `@sortable` (hand filter/sort allowlists),
55
55
  `@dbColumnType` (hand native-type override), `@example` / `@instruction` (hand prompt
56
- hints), `@xmlText` (hand XML-text mapping). Indexed-without-filter suppression is the
57
- `db.indexed` attr (cite without the `@` sigil — it is a dotted attr name).
56
+ hints), `@xmlText` (hand XML-text mapping). The `@db.indexed` attr suppresses the
57
+ *`@filterable`-without-index* Loader warning (you assert the column is indexed by other
58
+ means); it is a dotted attr name but canonical JSON still authors it WITH the sigil:
59
+ `"@db.indexed": true`.
58
60
  - **CALIBRATION — cut subtypes:** `field.byte`, `field.short`, `field.class` are
59
61
  non-functional removed stubs. **Do NOT audit for them and never recommend them.**
60
62
 
@@ -63,15 +65,29 @@ classify it (using the classification scheme in `SKILL.md`) and route the cutove
63
65
  - **`source.rdb`** (`@table`, `@schema`) — hunt hard-coded physical table/schema names that
64
66
  diverge from the default naming the source models.
65
67
  - **`@kind` = `view` / `materializedView`** — hunt hand-written SQL views where an authored
66
- projection source (read-only `@kind`) belongs. Apply the **view-necessity test** (SKILL.md,
67
- drift signature 8): a hand-written `CREATE VIEW` (or read-only SQL mirroring a read model) is a
68
- CODEGEN CANDIDATE when its shape is expressible via `origin.passthrough` / `origin.aggregate` /
69
- `origin.collection` + `extends` convert it to an `object.projection` so `meta migrate` emits
70
- the view DDL. It is BESPOKE-keep only when a NAMED irreducible construct (recursive CTE, window
71
- function, set op) blocks projection authoring. `meta verify --db` can't see an unmodeled view,
68
+ read-only source belongs. Apply the **view-necessity test** (SKILL.md, drift signature 8): a
69
+ hand-written `CREATE VIEW` (or read-only SQL mirroring a read model) is a CODEGEN CANDIDATE when
70
+ its shape is expressible via `origin.passthrough` / `origin.aggregate` / `origin.collection` /
71
+ `origin.computed` / `origin.first` + `extends`. Route by shape: an entity's OWN columns plus an
72
+ extra (`SELECT o.*, …`) an **entity read-view** (#214: a `@role: replica` view beside the
73
+ writable `table`); a subset / renamed / row-filtered exposure an `object.projection` (row-scope
74
+ with an object-level `@filter`, #207) — so `meta migrate` emits the view DDL. A genuinely
75
+ irreducible body (recursive CTE, window function, set op) belongs in the `source.rdb` `@sql`
76
+ escape (#208), not a hand migration. Only an *undeclared* view is invisible to `meta verify --db`,
72
77
  so this is audit-only.
73
78
  - **`@kind` = `storedProc` / `tableFunction`** (`@parameterRef`) — hunt hand-called procs /
74
79
  table functions that a modeled callable source with `@parameterRef` already describes.
80
+ - **`@sql`** (read-only `@kind` only; mutually exclusive with `@unmanaged`) — a hand-written SQL
81
+ body the tool REGISTERS, fingerprints, and drift-checks but never authors or parses (#208,
82
+ ADR-0043). The **escape valve for a genuinely irreducible view** origins can't express (recursive
83
+ CTE, window function, set op): carry the body here rather than in a hand-edited migration where it
84
+ goes accidentally unmanaged. Forbids `origin.*` children (two sources of truth); adopt a
85
+ pre-existing view with `meta migrate --allow adopt-view`. Hunt a hand-written irreducible
86
+ `CREATE VIEW` in a migration / `.sql` that should be carried in `@sql`.
87
+ - **`@unmanaged`** (any `@kind` incl. `table`; mutually exclusive with `@sql`) — marks a DB object
88
+ whose DDL is owned entirely elsewhere (Flyway / a hand-migration). `meta migrate` never creates,
89
+ drops, or drift-checks it; `verify --db` reports it as external (declared). Hunt an
90
+ externally-owned table/view the metadata silently omits instead of declaring `@unmanaged: true`.
75
91
  - **`@role` = `primary`** (multi-source write-through) — hunt manual CQRS / write-through
76
92
  wiring; exactly one `primary` source per object models it.
77
93
  - **`source.base`** — abstract source base (no audit target of its own).
@@ -107,16 +123,28 @@ classify it (using the classification scheme in `SKILL.md`) and route the cutove
107
123
  a NON-unique retrieval index (uniqueness is what distinguishes it from `identity.secondary`);
108
124
  hunt hand-created lookup / recency indexes (`CREATE INDEX …`) it models.
109
125
 
110
- ## Origin — `origin.*` (projection-field derivation)
126
+ ## Origin — `origin.*` (derived fields — on projections AND entity read-views)
111
127
 
112
- - **`origin.aggregate`** (`@agg`, `@of`, `@via`) — hunt hand `COUNT` / `SUM` / `AVG`
128
+ - **`origin.aggregate`** (`@agg`, `@of`, `@via`, `@filter`) — `@agg`: `count`/`sum`/`avg`/`min`/`max`
129
+ (numeric reduces over `@of`), `any`/`all` (predicate quantifiers over `@filter`; `@of` forbidden),
130
+ `collect` (array rollup of `@of` into an `isArray` field; `@distinct`/`@orderBy` collect-only).
131
+ Any aggregate may be row-scoped with `@filter`. Hunt hand `COUNT`/`SUM`/`AVG`/`EXISTS`/`array_agg`
113
132
  subqueries or in-app rollups a derived aggregate field models.
114
- - **`origin.passthrough`** (`@from`, `@via`) — hunt denormalized-by-hand copied fields that a
115
- passthrough origin pulls across a relationship.
133
+ - **`origin.passthrough`** (`@from`, `@via`, `@convert`) — hunt denormalized-by-hand copied fields
134
+ that a passthrough origin pulls across a relationship.
116
135
  - **`origin.collection`** (`@via`) — hunt hand-assembled child-collection loading a collection
117
136
  origin derives.
137
+ - **`origin.computed`** (`@expr` — a closed `attr.expression` grammar) — hunt a hand-computed derived
138
+ scalar (a formula over other fields) a computed origin models.
139
+ - **`origin.first`** (`@of`, `@via`, `@orderBy`, `@filter`; `@orderBy` REQUIRED) — hunt a hand
140
+ argmax-style "one related row's column" projection — a `DISTINCT ON … ORDER BY` or correlated
141
+ `ORDER BY … LIMIT 1` — a first origin models (nullable).
118
142
  - **`origin.base`** — abstract base.
119
143
 
144
+ Distinct from the per-aggregate `@filter` above, an **object-level `@filter` on
145
+ `object.projection`** (#207) row-scopes the WHOLE view (outer `WHERE`) — hunt a hand-written
146
+ soft-delete / status / type view it models.
147
+
120
148
  ## Validator — `validator.*`
121
149
 
122
150
  - **`validator.required` / `validator.length` / `validator.numeric` / `validator.array` /
@@ -144,12 +172,14 @@ classify it (using the classification scheme in `SKILL.md`) and route the cutove
144
172
  ## Template — `template.*` (prompt pillar)
145
173
 
146
174
  - **`template.prompt`** (`@payloadRef`, `@textRef`, `@responseRef`, `@requiredSlots`,
147
- `@maxTokens`, `@maxChars`, `@format`, `@model`, `@promptStyle`) — hunt prompt strings
175
+ `@requiredTags`, `@maxTokens`, `@maxChars`, `@format`, `@model`) — hunt prompt strings
148
176
  assembled inline in services, payloads built ad-hoc, output parsing without a typed
149
177
  `@responseRef`, or token/char budgets enforced by hand.
150
178
  - **`template.output`** (`@kind` = `document` | `email`; `@subjectRef`, `@htmlBodyRef`,
151
- `@textBodyRef`) — hunt hand-built document/email rendering + hand-written
152
- parse-on-receipt the output template + generated render helper/parser cover.
179
+ `@textBodyRef`, `@promptStyle`, `@requiredTags`) — hunt hand-built document/email rendering +
180
+ hand-written parse-on-receipt the output template + generated render helper/parser cover.
181
+ (`@promptStyle` — the FR-010 output-format presentation — is on `template.output` ONLY;
182
+ authoring it on `template.prompt` fails load with `ERR_UNKNOWN_ATTR`.)
153
183
  - **`template.toolcall`** (`@toolName`, `@payloadRef`) — hunt hand-declared LLM tool schemas
154
184
  a modeled tool call describes.
155
185
  - **`template.base`** — abstract base.
@@ -185,11 +215,15 @@ classify it (using the classification scheme in `SKILL.md`) and route the cutove
185
215
  `origin.*` never inherits).
186
216
  - **Filter + sort + pagination REST layer** — hunt hand-written query parsing, `LIMIT`/
187
217
  `OFFSET` pagination, total-count queries, and filter/sort handling the generated CRUD layer
188
- (8 filter operators + sort + `?limit=N&offset=N` + count) already provides.
189
- - **CALIBRATION — per-port codegen gaps:** filter-operator route codegen is full only in
190
- **TS**; Java / Kotlin / C# / Python generate pagination/sort/count but **defer filter
191
- ops** do NOT flag hand-added filter handling there. Output-parser codegen ships
192
- TS/C#/Python/Kotlin; **Java hand-writes** the Jackson parse (acceptable). **Python**
218
+ (9 filter operators `eq/ne/gt/gte/lt/lte/in/like/isNull` + sort + `?limit=N&offset=N` + count)
219
+ already provides.
220
+ - **CALIBRATION per-port codegen gaps:** the core `?filter[field][op]` filter grammar (all
221
+ 9 operators + the generated allowlist + `invalid-field` / `invalid-op` / `in`-over-cap 400s)
222
+ is **generated in all five ports** (api-contract corpus, both lanes) **flag hand-rolled
223
+ filter parsing anywhere.** Only the richer surface (`?search=`, explicit
224
+ `filter[or][N]` / `filter[and][N]` combinators, leading-wildcard gating) is TS-only.
225
+ Output-parser codegen also ships in **all five ports** — Java's `SpringOutputParserGenerator`
226
+ *generates* the parser (the Jackson `readValue` lives inside that generated file). **Python**
193
227
  still hand-wires the FastAPI router around a generated `APIRouter` (relationship /
194
228
  non-`table` source-kind / flattened-object codegen is partial). **C#** has no
195
229
  ObjectManager runtime tier (EF Core *is* the runtime) — hand services over the generated
@@ -95,10 +95,13 @@ version across these packages is an intra-port skew finding.
95
95
  pattern, not a defect.
96
96
  - **No C# migrate command.** Schema migration is Node-`meta`-owned for every port
97
97
  (ADR-0015). `dotnet meta` has no migrate subcommand; `meta migrate` is correct.
98
- - **Filter-operator route codegen deferred.** The generated `<Entity>Routes.cs` (Minimal
99
- API) supports `?sort`, `?limit`/`?offset`, and `?withCount=1` envelope, but filter
100
- operators (`?filter[field][op]=value`) are not yet generated. Do NOT flag hand-added
101
- C# filter handling as an adopter defect.
98
+ - **Core filter-operator codegen ships in C# — do NOT treat it as deferred.** The `routes`
99
+ generator (`<Entity>Routes.cs`, Minimal API) generates the `?filter[field][op]=value` grammar
100
+ (all 9 operators `eq/ne/gt/gte/lt/lte/in/like/isNull`) via the runtime `FilterParser` +
101
+ `EfCoreFilterDispatch`, validated against the generated filter allowlist (api-contract corpus,
102
+ both lanes). **Flag a hand-rolled filter parser as a finding.** Only the richer surface
103
+ (`?search=`, `filter[or][N]` / `filter[and][N]` combinators, leading-wildcard gating) is
104
+ TS-only — do NOT flag its absence in C#.
102
105
  - **Output-parser codegen ships in C#.** `output-parser` / `extractor` / `render-helper`
103
106
  generators are available (`dotnet meta gen --generators output-parser`). Absence of
104
107
  wired output parsers where `template.output` nodes exist IS a finding.
@@ -88,16 +88,18 @@ versioning — **do not flag it** (only intra-port skew matters).
88
88
 
89
89
  ## Calibration gaps (do NOT flag these)
90
90
 
91
- - **Hand-written output parser is expected.** The `SpringOutputParserGenerator` emits
92
- a typed `<Name>Parser` class, but the Jackson `readValue` one-liner inside it is
93
- hand-written by the generator — it is NOT a defect to see Jackson deserialization in
94
- generated `*Parser.java` files. **Do not flag Jackson `readValue` calls in
95
- `*Parser.java` files.** Only flag hand-rolled parsers in *non*-generated files where
96
- a `template.output` node exists.
97
- - **Filter-operator route codegen deferred.** The generated `<Entity>Controller.java`
98
- supports `?sort`, `?limit`/`?offset`, and `?withCount=1` envelope but defers
99
- filter-operator validation (`?filter[field][op]=value`). Do NOT flag hand-added
100
- Java filter handling as an adopter defect.
91
+ - **Jackson `readValue` inside a generated parser is expected.** `SpringOutputParserGenerator`
92
+ *generates* the typed `<Name>Parser` class; the Jackson `readValue` call lives inside that
93
+ generated file. It is NOT a defect to see Jackson deserialization in a generated `*Parser.java`
94
+ file. **Do not flag Jackson `readValue` calls in generated `*Parser.java` files.** DO flag a
95
+ hand-rolled parser in a *non*-generated file where a `template.output` node exists.
96
+ - **Core filter-operator codegen ships in Java — do NOT treat it as deferred.**
97
+ `SpringControllerGenerator` generates the `?filter[field][op]=value` grammar (all 9 operators
98
+ `eq/ne/gt/gte/lt/lte/in/like/isNull`): it parses via the runtime `FilterParser`, validates
99
+ against the generated `<Entity>FilterAllowlist`, and 400s on unknown field / disallowed op /
100
+ over-cap `in`-list (api-contract corpus, both lanes). **Flag a hand-rolled filter parser as a
101
+ finding.** Only the *richer* surface is TS-only — `?search=`, `filter[or][N]` / `filter[and][N]`
102
+ combinators, leading-wildcard gating — do NOT flag the absence of those in Java.
101
103
  - **Repository interface is a hand-implemented stub.** `SpringRepositoryGenerator`
102
104
  emits `<Entity>Repository.java` as a stub `interface`; the consumer hand-writes the
103
105
  implementation against OMDB (or any persistence layer). Hand-written repository
@@ -97,12 +97,16 @@ cross-port versioning — **do not flag it** (only intra-port skew matters).
97
97
  emits the `Table` column definitions; the consumer hand-writes the (typically trivial)
98
98
  `transaction(db) { ... }` bodies that query and mutate those tables. This is the expected
99
99
  Kotlin runtime pattern — do NOT flag hand-written Exposed transactions as a defect.
100
- - **Hand-written output parser is expected.** `KotlinOutputParserGenerator` emits a typed
101
- parser class, but the deserialization body inside it uses a hand-written kotlinx.serialization
102
- call do NOT flag this. Only flag hand-rolled parsers in non-generated files where a
103
- `template.output` node exists.
104
- - **Filter-operator route codegen deferred.** The generated `<Entity>Controller.kt` supports
105
- `?sort`, `?limit`/`?offset`, and `?withCount=1` envelope but defers filter-operator
106
- validation. Do NOT flag hand-added Kotlin filter handling as an adopter defect.
100
+ - **Output-parser codegen ships in Kotlin do NOT treat it as deferred.**
101
+ `KotlinOutputParserGenerator` generates the typed parser class AND its deserialization body
102
+ (a kotlinx `Json.decodeFromString` call inside the generated file kotlinx, not Jackson: the
103
+ #187 Jackson move was jsonb-codec-only, prompt payloads/parsers stay on kotlinx). Only flag
104
+ a hand-rolled parser in a NON-generated file where a `template.output` node exists.
105
+ - **Core filter-operator codegen ships in Kotlin — do NOT treat it as deferred.**
106
+ `KotlinSpringControllerGenerator` generates the `?filter[field][op]=value` grammar (all 9
107
+ operators `eq/ne/gt/gte/lt/lte/in/like/isNull`) validated against the generated filter allowlist
108
+ (api-contract corpus, both lanes). **Flag a hand-rolled filter parser as a finding.** Only the
109
+ richer surface (`?search=`, `filter[or][N]` / `filter[and][N]` combinators, leading-wildcard
110
+ gating) is TS-only — do NOT flag its absence in Kotlin.
107
111
  - **No JVM migrate goal.** Schema migration is Node-`meta`-owned for every port
108
112
  (ADR-0015). The Maven plugin has no migrate goal; `meta migrate` is correct.
@@ -98,10 +98,13 @@ another) matters.
98
98
  generated router are typed `dict[str, Any]` and responses return `Any`. Tightening
99
99
  to the typed Pydantic model is a hand-edit the adopter may choose — do NOT flag
100
100
  the `Any` as an adopter fault.
101
- - **Filter-operator route codegen deferred.** The generated `APIRouter` supports
102
- `?sort`, `?limit`/`?offset`, and `?withCount=1` envelope but defers filter-operator
103
- validation (`?filter[field][op]=value`). Do NOT flag hand-added Python filter
104
- handling as an adopter defect.
101
+ - **Core filter-operator codegen ships in Python — do NOT treat it as deferred.** The `routes`
102
+ generator emits an `APIRouter` whose list route parses the `?filter[field][op]=value` grammar
103
+ (all 9 operators `eq/ne/gt/gte/lt/lte/in/like/isNull`) via the generated `filter_parser` module,
104
+ validated against the generated filter allowlist (api-contract corpus, both lanes). **Flag a
105
+ hand-rolled filter parser as a finding.** Only the richer surface (`?search=`,
106
+ `filter[or][N]` / `filter[and][N]` combinators, leading-wildcard gating) is TS-only — do NOT
107
+ flag its absence in Python.
105
108
  - **Partial relationship + flattened-object codegen.** Relationship navigation,
106
109
  non-`table` source kinds, and `field.object @storage: flattened` codegen are
107
110
  partially implemented in the Python port. Do NOT flag these gaps as adopter defects.
@@ -106,7 +106,7 @@ via `metamodelVersion` (see the Phase 0 cross-language consistency item).
106
106
 
107
107
  TypeScript is the reference implementation — it ships the full feature set:
108
108
 
109
- - Filter-operator codegen: **complete** (`<Entity>FilterAllowlist`, 8 operators, all four CRUD routes validate against it).
109
+ - Filter-operator codegen: **complete** — the core `?filter[field][op]` grammar (all 9 operators `eq/ne/gt/gte/lt/lte/in/like/isNull`, `<Entity>FilterAllowlist`) generates in every port; TS additionally ships the richer surface (`?search=`, `filter[or][N]` / `filter[and][N]` combinators, leading-wildcard gating).
110
110
  - Output-parser codegen: **complete** (`outputParser()` generator; `parse*`/`safeParse*`/`extract*` per `template.output`).
111
111
  - ObjectManager: **complete** (`@metaobjectsdev/runtime-ts`; `kyselyDriver` / `inMemoryDriver`).
112
112
  - Schema migrate / `meta verify --db`: **complete** (TS owns the shared migrate engine — ADR-0015).
@@ -198,18 +198,23 @@ name package extends abstract overlay isArray children value
198
198
  | `field.string` | text | `@maxLength` drives `varchar(N)` |
199
199
  | `field.int` | 32-bit integer | |
200
200
  | `field.long` | 64-bit integer | |
201
- | `field.double` | float | |
201
+ | `field.double` | double float | approximate; not for money |
202
+ | `field.float` | single-precision float | native double/number (TS has no distinct float); DB `REAL`; not for money |
202
203
  | `field.boolean` | true/false | |
203
204
  | `field.date` | calendar date | ISO 8601 `YYYY-MM-DD` on the wire |
205
+ | `field.time` | time-of-day | no calendar date; DB `TIME` |
204
206
  | `field.timestamp` | instant (tz-aware) | ISO 8601 with timezone on the wire; `@localTime: true` for a naive wall-clock value |
205
207
  | `field.decimal` | exact decimal | `@precision` / `@scale`; lossless money/quantity |
206
208
  | `field.currency` | integer minor units | see Currency below |
207
209
  | `field.enum` | string member | `@values` required; see Enum below |
208
210
  | `field.uuid` | UUID | canonical lowercase hex on the wire |
209
211
  | `field.object` | embedded value object | `@objectRef` + `@storage`; see below |
212
+ | `field.map` | open-keyed map | one jsonb column; string keys; `@valueType` (scalar subtype) XOR `@objectRef` (value object); `isArray` does not apply |
210
213
 
211
214
  Common field attributes: `@required`, `@maxLength`, `@column` (physical column
212
- name), `@default`, `@filterable`, `@sortable`.
215
+ name), `@default`, `@filterable`, `@sortable`. On temporal fields
216
+ (`field.date`/`field.time`/`field.timestamp`) `@autoSet` stamps the value
217
+ automatically — see the `@autoSet` callout under Timestamps below.
213
218
 
214
219
  ### Choosing the right shape — the general decision procedure (ADR-0037)
215
220
 
@@ -277,7 +282,7 @@ Canonical form for common field needs — reach for these before inventing anyth
277
282
  | IDs / unique keys / **any UUID column** | `field.uuid` | native UUID type. **NEVER `field.string` + `@dbColumnType: uuid`** — see the smell callout below |
278
283
  | Money | `field.currency` | integer minor units; never a float |
279
284
  | Closed set of symbols | `field.enum` | `@values` required |
280
- | Instant / event time (created/updated) | `field.timestamp` | instant / tz-aware by default (Postgres `timestamptz`; native `Instant`/`DateTimeOffset`/aware `datetime`) |
285
+ | Instant / event time (created/updated) | `field.timestamp` + `@autoSet` | instant / tz-aware by default (Postgres `timestamptz`; native `Instant`/`DateTimeOffset`/aware `datetime`); `@autoSet: onCreate` for `createdAt`, `@autoSet: onUpdate` for `updatedAt` — never hand-stamp |
281
286
  | Naive wall-clock value (store-open time, birthday-with-time) | `field.timestamp` + `@localTime: true` | `timestamp without time zone` — opt out of zone-awareness only for a genuine wall-clock value |
282
287
  | A list of anything | `isArray: true` | on the base subtype (e.g. `field.string` + `isArray`) — there is **no** array `@dbColumnType` (retired) |
283
288
  | Long / unbounded text | bare `field.string` | add `@maxLength` only when you want `varchar(N)` |
@@ -322,6 +327,20 @@ lives in `field.timestamp` (instant by default) + the `@localTime` naive opt-out
322
327
  { "field.timestamp": { "name": "opensAt", "@localTime": true } }
323
328
  ```
324
329
 
330
+ **`@autoSet` — let the runtime stamp created/updated times; never hand-set them.**
331
+ `@autoSet` is registered on the temporal subtypes (`field.date` / `field.time` /
332
+ `field.timestamp`) and takes **`onCreate`** (stamp on insert) or **`onUpdate`**
333
+ (stamp on every write). This is the model-first way to express audit timestamps —
334
+ declare it and the generated write path stamps the column, so you never hand-write
335
+ `createdAt = now()` in application code (the exact hand-stamping anti-pattern). A
336
+ `@required` field carrying `@autoSet: onCreate` is correctly *optional* on POST (the
337
+ server supplies it).
338
+
339
+ ```json
340
+ { "field.timestamp": { "name": "createdAt", "@autoSet": "onCreate", "@required": true } }
341
+ { "field.timestamp": { "name": "updatedAt", "@autoSet": "onUpdate" } }
342
+ ```
343
+
325
344
  **String-shaped natives & validated strings (ADR-0036 Wave 3).** A URL/URI is its own
326
345
  native type with URL behavior → **`field.uri`** (subtype, step 2a), not a validated
327
346
  string. An IP address likewise → **`field.inet`**. An email or hostname is a *plain
@@ -539,30 +558,72 @@ at the same level as fields and identities.
539
558
 
540
559
  ## Relationships
541
560
 
542
- `relationship.composition` is the "this entity owns / aggregates instances of
543
- that entity" side; `identity.reference` (above) is the FK-column side. They are
544
- the two halves of one FK.
561
+ A `relationship.*` child is the navigation / ownership side of a link to another
562
+ entity; `identity.reference` (above) is the FK-column side. They are the two halves
563
+ of one FK. **Choose the subtype by ownership semantics — it decides the default
564
+ referential action**, so modeling every FK as `composition` silently arms unintended
565
+ `CASCADE` deletes:
545
566
 
546
- | Attr | On | Values |
547
- |---|---|---|
548
- | `@objectRef` | composition | target entity name |
549
- | `@cardinality` | composition | `one` / `many` |
550
- | `@onDelete` / `@onUpdate` | `relationship.*` only | `cascade` / `set-null` / `restrict` / `no-action` |
567
+ | Subtype | Ownership | Default `@onDelete` | Use when |
568
+ |---|---|---|---|
569
+ | `relationship.composition` | owns the target's lifecycle | `cascade` | children are owned and deleted with the parent |
570
+ | `relationship.aggregation` | groups, does NOT own | `set-null` | children outlive the parent; delete nulls the FK |
571
+ | `relationship.association` | plain reference, no ownership | `restrict` | you just point at an independent entity |
572
+
573
+ Common attrs (on any subtype): `@objectRef` (target entity name / FQN),
574
+ `@cardinality` (`one` / `many`), and `@onDelete` / `@onUpdate`
575
+ (`cascade` / `set-null` / `restrict` / `no-action`). `@onDelete` defaults **per
576
+ subtype** as in the table; `@onUpdate` defaults to `cascade` on every subtype.
551
577
 
552
578
  ```json
553
- { "relationship.composition": {
554
- "name": "posts", "@objectRef": "Post",
555
- "@cardinality": "many", "@onDelete": "cascade" } }
579
+ { "relationship.composition": { "name": "posts", "@objectRef": "Post", "@cardinality": "many" } }
580
+ { "relationship.aggregation": { "name": "members", "@objectRef": "User", "@cardinality": "many" } }
581
+ { "relationship.association": { "name": "author", "@objectRef": "User", "@cardinality": "one" } }
556
582
  ```
557
583
 
558
- **Adoption footgunpin BOTH actions.** `@onDelete` and `@onUpdate` each default to
559
- `cascade` when omitted, but a plain SQL foreign key is `NO ACTION` on both. If you're
560
- adopting an existing database (matching metadata to a live schema), omitting these
561
- makes the metadata declare `CASCADE` where the DB has `NO ACTION` a perpetual
562
- `verify --db` drift. Pin **both** explicitly to the DB's real behavior:
584
+ **Many-to-many (FR-018)`@through` a junction entity.** Model an M:N link with
585
+ `@cardinality: "many"` + `@objectRef` (the target) + **`@through`** (the junction
586
+ entity). The junction MUST declare **two `identity.reference` children**, one per FK
587
+ side; the relationship's FK fields are **derived** from those referencesnever
588
+ restated. Two optional attrs handle self-joins:
589
+
590
+ - `@sourceRefField` — names the source-side FK field on the junction, disambiguating a
591
+ **directed** self-join (e.g. `follows`, where both references point at `User`).
592
+ - `@symmetric: true` — marks an **undirected** self-join (union-on-read). Valid only
593
+ when `@objectRef` is the declaring entity itself, and **mutually exclusive** with
594
+ `@sourceRefField`.
595
+
596
+ (Any relationship subtype carries the M:N attrs; the conformance fixtures author them
597
+ on `relationship.association`.)
563
598
 
564
599
  ```json
565
- { "relationship.composition": { "name": "author", "@objectRef": "User",
600
+ { "relationship.association": {
601
+ "name": "tags", "@cardinality": "many",
602
+ "@objectRef": "Tag", "@through": "PostTag" } }
603
+ ```
604
+
605
+ The `PostTag` junction supplies the FK direction via its two references:
606
+
607
+ ```json
608
+ { "object.entity": { "name": "PostTag", "children": [
609
+ { "field.long": { "name": "id" } },
610
+ { "field.long": { "name": "postId" } },
611
+ { "field.long": { "name": "tagId" } },
612
+ { "identity.primary": { "name": "id", "@fields": "id" } },
613
+ { "identity.reference": { "name": "postRef", "@fields": "postId", "@references": "Post" } },
614
+ { "identity.reference": { "name": "tagRef", "@fields": "tagId", "@references": "Tag" } }
615
+ ] } }
616
+ ```
617
+
618
+ **Adoption footgun — pin BOTH actions.** `@onDelete` defaults *per subtype* (above)
619
+ and `@onUpdate` defaults to `cascade` — but a plain SQL foreign key is `NO ACTION` on
620
+ both. If you're adopting an existing database (matching metadata to a live schema),
621
+ leaving these implicit makes the metadata declare a referential action the DB doesn't
622
+ have — a perpetual `verify --db` drift. Pin **both** explicitly to the DB's real
623
+ behavior:
624
+
625
+ ```json
626
+ { "relationship.association": { "name": "author", "@objectRef": "User",
566
627
  "@cardinality": "one", "@onDelete": "no-action", "@onUpdate": "no-action" } }
567
628
  ```
568
629
 
@@ -601,13 +662,17 @@ These are children of `object.entity`, alongside its fields and identities.
601
662
  | `storedProc` | yes | – |
602
663
  | `tableFunction` | yes | – |
603
664
 
604
- The physical name is `@table` (NOT `@name`). The physical column name on a field
605
- is `@column`. `@schema` namespaces the DB schema (Postgres default `public`;
606
- SQLite rejects non-default values). Multi-source: multiple `source.rdb` children,
607
- each with a `@role`, exactly one `primary`.
665
+ The physical name attr is **kind-matched** never `@name`: `@table` for a table,
666
+ `@view` for a view, `@materializedView`, `@proc` (storedProc), `@function`
667
+ (tableFunction). (`@table` on a non-table kind is a pre-1.0 legacy spelling the
668
+ canonical serializer rewrites to the kind's alias; author the kind-matched attr
669
+ directly.) The physical column name on a field is `@column`. `@schema` namespaces
670
+ the DB schema (Postgres default `public`; SQLite rejects non-default values).
671
+ Multi-source: multiple `source.rdb` children, each with a `@role`, exactly one
672
+ `primary`.
608
673
 
609
674
  ```json
610
- { "source.rdb": { "@kind": "view", "@table": "v_author", "@schema": "blog" } }
675
+ { "source.rdb": { "@kind": "view", "@view": "v_author", "@schema": "blog" } }
611
676
  ```
612
677
 
613
678
  **An entity's PRIMARY source must be writable** (`table`) — read-only kinds are
@@ -615,18 +680,83 @@ legal only in non-primary roles (e.g. table `primary` + view `replica` for
615
680
  read-through). A derived read model over a view/proc is an **`object.projection`**
616
681
  (FR-024): its fields `extends` entity fields (`extends: "Author.id"` — dotted
617
682
  child traversal, package only on the root segment) and/or carry `origin.*`
618
- children (`passthrough` / `aggregate` / `collection`) declaring assembly; its
619
- identity passes through via `extends` (`identity.primary: { name: id, extends:
620
- "Author.id" }`); it is read-only by construction and the declared field set IS
621
- the exposure (fail-closed). Give it a read-only `source.rdb` `@kind: view`
622
- child (`source.rdb: { kind: view, table: v_author }`) — codegen keys projection
623
- detection + view DDL off that read-only source, so without it `meta gen` emits
624
- nothing for the projection. **The `CREATE VIEW` body is generated from those
625
- `origin.*` children by the Node `meta migrate` — never hand-author view SQL for a
626
- shape origins can express** (an unmodeled view is *unmanaged*, so `meta verify --db`
627
- can't even catch the drift). Hand-write a view only when a construct origins can't
628
- express (recursive CTE, window function, set operation) blocks projection authoring,
629
- and keep that DDL in a hand-edited migration file.
683
+ children (`passthrough` / `aggregate` / `collection` / `computed` / `first`)
684
+ declaring assembly; its identity passes through via `extends` (`identity.primary:
685
+ { name: id, extends: "Author.id" }`); it is read-only by construction and the
686
+ declared field set IS the exposure (fail-closed). Give it a read-only `source.rdb`
687
+ `@kind: view` child (`source.rdb: { kind: view, view: v_author }`) — codegen keys
688
+ projection detection + view DDL off that read-only source, so without it `meta gen`
689
+ emits nothing for the projection.
690
+
691
+ **Origin vocabulary (#195).** `origin.aggregate @agg` takes `count`/`sum`/`avg`/`min`/`max`
692
+ (numeric reduces over `@of`), `any`/`all` (predicate quantifiers over a `@filter`; `@of`
693
+ forbidden; empty set `any=false`, `all=true`), and `collect` (an array rollup of `@of`
694
+ into an `isArray` field, with optional `@distinct` / `@orderBy`). Any aggregate may be
695
+ row-scoped with `@filter`. `origin.computed` carries a closed structured `@expr` tree (a
696
+ derived scalar). `origin.first` picks one related row's column (`@of`) along `@via`,
697
+ ordered by a **required `@orderBy`** (`["field:asc|desc", …]`, with the PK as tie-break) —
698
+ the ordering is what makes `origin.first` express "latest / earliest X"; it may be
699
+ row-scoped with `@filter` and is nullable. A field carrying any `origin.*` is derived ⇒
700
+ read-only.
701
+
702
+ `@expr` and `@filter` are **structured objects, not SQL strings** — guessing a string
703
+ body fails the load:
704
+
705
+ - **`@expr`** (on `origin.computed`) is a closed operation tree; a string body is a load
706
+ error (`ERR_BAD_ATTR_VALUE`):
707
+ ```json
708
+ { "origin.computed": { "@expr": { "op": "isNotNull", "arg": { "field": "payloadJson" } } } }
709
+ ```
710
+ - **`@filter`** (on `origin.aggregate` / `origin.first`, and object-level on a projection)
711
+ is an `attr.filter` object — a field→predicate map. A bare value is `eq` shorthand; an
712
+ operator map spells the op:
713
+ ```json
714
+ { "origin.aggregate": { "@agg": "any", "@via": "Session.turns", "@filter": { "success": false } } }
715
+ { "@filter": { "status": { "ne": "archived" } } }
716
+ ```
717
+
718
+ **Projection row-scope `@filter` (#207).** An object-level `@filter` on `object.projection`
719
+ (the same `attr.filter` object shown above) scopes the WHOLE view's rows — it lowers to
720
+ the view's outer `WHERE`. This is the metadata-managed way to author a soft-delete /
721
+ status / type view without hand-writing SQL. It may reference **only declared,
722
+ non-aggregate-derived** projection fields: naming an undeclared field fails the load
723
+ (`ERR_BAD_ATTR_FILTER`), and so does naming a field whose value comes from an
724
+ `origin.aggregate`.
725
+
726
+ ```json
727
+ { "object.projection": { "name": "ActiveOrders",
728
+ "@filter": { "status": { "ne": "archived" } }, "children": [ … ] } }
729
+ ```
730
+
731
+ **The `CREATE VIEW` body is generated from those `origin.*` children by the Node
732
+ `meta migrate` — never hand-author view SQL for a shape origins can express** (an unmodeled
733
+ view is *unmanaged*, so `meta verify --db` can't even catch the drift). For a genuinely
734
+ irreducible body (recursive CTE, window function, set operation) that origins can't express,
735
+ carry it in the `source.rdb` **`@sql`** escape (#208, ADR-0043) — a hand-written body the
736
+ tool registers, fingerprints, and drift-checks (adopt a pre-existing view with
737
+ `meta migrate --allow adopt-view`) — rather than a hand-edited migration file where it goes
738
+ accidentally unmanaged. For a DB object owned entirely elsewhere (Flyway), mark its source
739
+ **`@unmanaged: true`** (view or table); migrate/verify then never touch it.
740
+
741
+ **`@sql` fail-closed rules — an `@sql` body is a *second* source of truth, so the loader
742
+ walls it off (#208).** Author an `@sql` source under these constraints or the load fails:
743
+
744
+ - `@sql` is legal **only on a read-only `@kind`** (view / materializedView / storedProc /
745
+ tableFunction) — never on a writable `@kind: table` (`ERR_SQL_BODY_ON_WRITABLE_KIND`).
746
+ - **No `origin.*`-bearing field may live under an `@sql` host** — the body already *is*
747
+ the derivation, so an origin alongside it is a double-declaration
748
+ (`ERR_ORIGIN_UNDER_SQL_BODY`). If you add an `@sql` body to a projection, **move its
749
+ derived fields out** (into plain declared fields the body computes) rather than keeping
750
+ their `origin.*` children.
751
+ - The object-level `@filter` (#207) is **mutually exclusive with `@sql`** (same error) —
752
+ fold the predicate into the `@sql` body's own `WHERE`.
753
+ - `@sql` and `@unmanaged` are **mutually exclusive** (`ERR_SQL_BODY_WITH_UNMANAGED`); an
754
+ empty/whitespace `@sql` is rejected (`ERR_BAD_ATTR_VALUE`).
755
+ - Under **`@unmanaged`**, an `origin.*`-bearing field only **warns** (the marker acts on
756
+ nothing, so a documented-but-unacted-on lineage is benign) — the asymmetry with `@sql`
757
+ is deliberate.
758
+ - Today `meta migrate` **lowers `@sql` only on `@kind: view`**; on matview / storedProc /
759
+ tableFunction it is registered but not yet migrate-managed (mark those `@unmanaged`).
630
760
 
631
761
  **A `passthrough` field must match its `@from` source's type.** A passthrough
632
762
  forwards the source value unchanged, so the projection field's `field.<subType>`
@@ -668,10 +798,12 @@ Resolution facts:
668
798
 
669
799
  - **Deferred.** `extends:` resolves *after all files load* — abstracts can live in
670
800
  any file, forward references are fine.
671
- - **Multi-level chains flatten** (`Author extends BaseEntity extends Auditable`).
801
+ - **Multi-level chains resolve through the whole chain** (`Author extends BaseEntity
802
+ extends Auditable`) — each `extends` is a super-*reference*, so resolution walks the
803
+ full chain (it does not flatten inherited members onto the child; see ADR-0039 below).
672
804
  - **Cross-package** refs use the fully-qualified name (`extends: "shared::auditable"`);
673
805
  same-package refs use the bare name.
674
- - An unresolved reference fails with `ERR_UNKNOWN_EXTENDS`.
806
+ - An unresolved reference fails with `ERR_UNRESOLVED_SUPER`.
675
807
 
676
808
  `abstract` and `extends` are **structural keys** (bare, no `@`).
677
809