@metaobjectsdev/sdk 0.16.0 → 0.17.0
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.
- package/agent-context/skills/metaobjects-audit/SKILL.md +25 -12
- package/agent-context/skills/metaobjects-audit/references/capability-checklist.md +54 -20
- package/agent-context/skills/metaobjects-audit/references/csharp.md +7 -4
- package/agent-context/skills/metaobjects-audit/references/java.md +12 -10
- package/agent-context/skills/metaobjects-audit/references/kotlin.md +11 -7
- package/agent-context/skills/metaobjects-audit/references/python.md +7 -4
- package/agent-context/skills/metaobjects-audit/references/typescript.md +1 -1
- package/agent-context/skills/metaobjects-authoring/SKILL.md +171 -39
- package/agent-context/skills/metaobjects-codegen/SKILL.md +45 -16
- package/agent-context/skills/metaobjects-codegen/references/csharp.md +21 -1
- package/agent-context/skills/metaobjects-codegen/references/java.md +43 -5
- package/agent-context/skills/metaobjects-codegen/references/kotlin.md +34 -4
- package/agent-context/skills/metaobjects-codegen/references/python.md +32 -8
- package/agent-context/skills/metaobjects-codegen/references/typescript.md +19 -3
- package/agent-context/skills/metaobjects-fit-assessment/SKILL.md +55 -18
- package/agent-context/skills/metaobjects-prompts/SKILL.md +65 -4
- package/agent-context/skills/metaobjects-prompts/references/csharp.md +21 -0
- package/agent-context/skills/metaobjects-prompts/references/java.md +32 -3
- package/agent-context/skills/metaobjects-prompts/references/kotlin.md +33 -3
- package/agent-context/skills/metaobjects-prompts/references/python.md +29 -3
- package/agent-context/skills/metaobjects-prompts/references/typescript.md +34 -1
- package/agent-context/skills/metaobjects-runtime-ui/SKILL.md +1 -1
- package/agent-context/skills/metaobjects-runtime-ui/references/csharp.md +90 -0
- package/agent-context/skills/metaobjects-runtime-ui/references/python.md +94 -0
- package/agent-context/skills/metaobjects-verify/SKILL.md +50 -15
- package/agent-context/skills/metaobjects-verify/references/migration.md +67 -14
- package/dist/agent-docs/body.d.ts +1 -1
- package/dist/agent-docs/body.d.ts.map +1 -1
- package/dist/agent-docs/body.js +3 -1
- package/dist/agent-docs/body.js.map +1 -1
- package/package.json +2 -2
- 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
|
|
295
|
-
- **
|
|
296
|
-
- **
|
|
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
|
-
- **
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
333
|
-
|
|
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
|
|
449
|
-
|
|
450
|
-
-
|
|
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).
|
|
57
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
`origin.
|
|
70
|
-
|
|
71
|
-
|
|
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.*` (
|
|
126
|
+
## Origin — `origin.*` (derived fields — on projections AND entity read-views)
|
|
111
127
|
|
|
112
|
-
- **`origin.aggregate`** (`@agg`, `@of`, `@via`) —
|
|
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
|
|
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
|
|
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 +
|
|
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
|
-
(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
- **
|
|
99
|
-
API)
|
|
100
|
-
operators
|
|
101
|
-
|
|
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
|
-
- **
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
- **
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
- **
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
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
|
|
543
|
-
|
|
544
|
-
the
|
|
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
|
-
|
|
|
547
|
-
|
|
548
|
-
|
|
|
549
|
-
|
|
|
550
|
-
|
|
|
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
|
-
|
|
555
|
-
|
|
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
|
-
**
|
|
559
|
-
`
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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 references — never
|
|
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.
|
|
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
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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", "@
|
|
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`
|
|
619
|
-
identity passes through via `extends` (`identity.primary:
|
|
620
|
-
"Author.id" }`); it is read-only by construction and the
|
|
621
|
-
the exposure (fail-closed). Give it a read-only `source.rdb`
|
|
622
|
-
child (`source.rdb: { kind: view,
|
|
623
|
-
detection + view DDL off that read-only source, so without it `meta gen`
|
|
624
|
-
nothing for the projection.
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
|
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 `
|
|
806
|
+
- An unresolved reference fails with `ERR_UNRESOLVED_SUPER`.
|
|
675
807
|
|
|
676
808
|
`abstract` and `extends` are **structural keys** (bare, no `@`).
|
|
677
809
|
|