@metaobjectsdev/sdk 0.14.1 → 0.15.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.
@@ -69,7 +69,35 @@ code behind a grep hit; a "duplicate" validator's *divergence* is the finding.
69
69
  (ADR-0019); runtime reflection to resolve a type from FQN vs generated static imports /
70
70
  FQN registry (ADR-0001 / 0017); process-global registry vs per-loader (ADR-0014); code
71
71
  that **mutates the loaded metadata tree** (read-only after load); JVM/Kotlin missing
72
- startup validator; writes not routed to the `@role: primary` source.
72
+ startup validator; writes not routed to the `@role: primary` source; **`own*()`
73
+ accessor reads of effective properties / own-only member iteration (ADR-0039 — see
74
+ the active check below).**
75
+
76
+ - [ ] **G2. `own*()` accessor discipline (ADR-0039 — CORRECTNESS DEFECT, not advisory).**
77
+ In any custom generator, metamodel provider, or runtime path (NOT the sanctioned cases
78
+ below), **flag every read of a field/node's effective property, or own-only member
79
+ iteration, done through an own-only accessor** — it silently drops everything inherited
80
+ via `extends` (a super-reference, not a flatten), corrupting codegen and runtime. This
81
+ is exactly the class of bug that broke Kotlin's array-type derivation (a concrete field
82
+ inheriting an array flag from an abstract parent generated a scalar) and, per the audit,
83
+ is latent cross-port. Grep for the own-only accessors and verify each hit:
84
+ - **TS:** `ownAttr(`, `ownChildren(`, `ownFields(`, a raw `isArray` field flag read →
85
+ should be `attr(` / `children()` / `fields()` unless emitting a subclass's own members.
86
+ - **Python:** `own_children(`, `own_fields(`, and the **inverted** bare `attr(` (Python
87
+ `attr()` is OWN; the resolving form is `attrs().get(`) → flag `attr(` used to read an
88
+ effective value.
89
+ - **Java / Kotlin:** `getMetaAttr(name, false)` (the `,false` own overload), own-only
90
+ child walks (e.g. an own-only `filterIsInstance<…>()` source lookup that emits nothing
91
+ for an entity inheriting its source).
92
+ - **C#:** a native `IsArray` flag read, `OwnChildren()`, own attr reads.
93
+
94
+ **Sanctioned (do NOT flag):** (a) a generator emitting a generated **subclass** that
95
+ iterates `ownFields()` so inherited members aren't re-emitted (the generated base
96
+ declares them — the `class Sub extends Base` / TPH pattern); (b) the own-mode canonical
97
+ serializer + overlay-merge + super-resolution walks (library-internal); (c) the single
98
+ deliberately-own attribute `@dbColumnType` (a physical column-type override, never
99
+ inherited). Any own read that carries a comment naming one of these cases is fine; an
100
+ uncommented own read of an effective property is the defect.
73
101
  - [ ] **H. Authoring-correctness / ADR-conformance (deep).** Invented/unregistered
74
102
  `@`-attrs or post-bootstrap registration (ADR-0023 — custom attrs belong in a registered
75
103
  provider or `attr.properties`); retired source-v2 forms (`source.dbTable` / `@name` /
@@ -80,10 +108,70 @@ code behind a grep hit; a "duplicate" validator's *divergence* is the finding.
80
108
  in committed canonical JSON (ADR-0032); DB-type-as-logical-subtype (ADR-0013); per-port
81
109
  migration engine where schema is Node-`meta`-owned (ADR-0015).
82
110
 
111
+ - [ ] **I. Vocabulary hygiene / modernization (ADVISORY).** Flag already-retired or
112
+ deprecated authoring patterns and recommend the canonical form (see § Vocabulary
113
+ hygiene). Advisory severity — scored as modernization opportunities, **never a
114
+ failing finding**.
115
+
83
116
  **Phase 4 — Synthesize** into the tiered roadmap and populate both artifacts.
84
117
 
85
118
  ---
86
119
 
120
+ ## Vocabulary hygiene / modernization (axis I — ADVISORY)
121
+
122
+ Per ADR-0037, vocabulary expansion follows ONE ordered test (derivable → derive;
123
+ physical-only → `@dbColumnType`; logical: different native type → subtype, same kind +
124
+ modifier → attribute). This axis surfaces authoring that predates or contradicts that
125
+ framework. **All findings here are advisory** — modernization opportunities scored as
126
+ such, surfaced in the roadmap, but **non-failing** (the code works; the form is dated).
127
+
128
+ **Already-retired / deprecated forms → recommend the canonical form:**
129
+
130
+ - `@dbColumnType: uuid_array` / `@dbColumnType: text_array` (a physical array column
131
+ type) → **`isArray: true`** on the base subtype. Array-ness is logical and derivable;
132
+ the array column type is retired.
133
+ - The `@kind: text` hack (forcing text via a kind override) → **bare `field.string`**
134
+ (text is the default; no override needed).
135
+ - `@dbColumnType: uuid` where a native UUID type is actually wanted → **`field.uuid`**
136
+ (a distinct native type is a subtype, not a physical override). Keep `@dbColumnType:
137
+ uuid` ONLY for the deliberate string-over-uuid-column case.
138
+ - `@dbColumnType: timestamp_with_tz` (ADR-0036 Wave 2) → **drop it.** `field.timestamp` is
139
+ instant / timezone-aware **by default** now; the `timestamp_with_tz` column-type override
140
+ is **retired**. Timezone-awareness lives in `field.timestamp` + the `@localTime` opt-out.
141
+ - A bare `field.timestamp` that is **semantically a wall-clock value** (a store-open time, a
142
+ birthday-with-time, a recurring local schedule) → recommend **`@localTime: true`** (naive
143
+ `timestamp without time zone`), or confirm it is genuinely meant to be an instant. Default
144
+ `field.timestamp` is an instant; only flag when the field's meaning is clearly wall-clock.
145
+ - A `validator.regex` (or a plain `field.string`) validating an **email** shape → recommend
146
+ **`@stringFormat: email`** on the `field.string` (ADR-0036 Wave 3). The native type stays
147
+ `string`; the per-port codegen emits the idiomatic email check — don't hand-roll the regex.
148
+ - A `field.string` that **holds a URL / URI** → recommend **`field.uri`** (native `URI`/`Uri`,
149
+ URL validation) — a distinct native type + behavior is a subtype, not a validated string.
150
+ - A `field.string` that **holds an IP address** → recommend **`field.inet`** (native IP type;
151
+ Postgres `inet` column).
152
+
153
+ **Custom-provider vocabulary (adopters who register their own types/attrs):** check
154
+ new/custom vocab against the ADR-0037 procedure (advisory) — e.g. *a custom subtype
155
+ that differs from an existing one only by a property should be an attribute, not a
156
+ subtype*; an email/hostname string-validation is an **attribute** (`@stringFormat`,
157
+ native type unchanged), while a URL or IP is a **native type** (`field.uri` /
158
+ `field.inet`, a subtype). Recommend re-shaping against the ordered test.
159
+
160
+ **Hand-rolled reverse-query repository methods (ADR-0038) → recommend the generated reverse
161
+ FK finder.** Reverse navigation — *"find all the rows that reference this one"* — is now
162
+ codegen. Flag a hand-written reverse-query method (a `findByParentId` / `findBy<Parent>`
163
+ repository finder, or a manual `WHERE fk = ?` query helper — exactly what a JVM adopter
164
+ hand-writes in its `SceneRepository`) and recommend the **generated reverse FK finder**
165
+ instead: codegen now emits `find<Source>By<FkField>` plus a batched `…In(ids)` variant from
166
+ the FK metadata, idiomatic per port (Spring repository finder / EF query method / Python or
167
+ TS query function). It is **performant** (one indexed query, no N+1) and **framework-free**
168
+ (no lazy collections / proxies). When an entity has two FKs to the same target, the codegen
169
+ emits two distinct finders (named by the FK field) automatically — no annotation needed; the
170
+ reverse finder is a **codegen feature, not an attribute** (there is no reverse-nav `@`-attr
171
+ to author). Advisory severity — a modernization opportunity, not a failing finding.
172
+
173
+ ---
174
+
87
175
  ## Classification scheme (every surface; classify on codegen AND runtime)
88
176
 
89
177
  | Class | Meaning | Action |
@@ -112,6 +200,7 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend
112
200
  4. **Drift-admitting comments** — grep: `"keep in sync with"` / `"mirrors the"` / `"matching the"`.
113
201
  5. **Runtime schema patching** (`ALTER TABLE … ADD COLUMN IF NOT EXISTS`, `_ensure_schema()`) — N schema owners.
114
202
  6. **N declarations of one shape** — same entity as Drizzle table + Zod schema + Pydantic model + hand dataclass; target is 1 + N generated.
203
+ 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.
115
204
 
116
205
  ---
117
206
 
@@ -183,6 +272,10 @@ one-consumer need, read it codegen-locally.
183
272
  Coarse bands only (none / some / most / all). Worst-of within a pillar. On re-run, grade
184
273
  the delta. Lead with gaps, not the grade.
185
274
 
275
+ **Vocabulary hygiene (axis I) is advisory** — it surfaces as modernization
276
+ opportunities in the roadmap, scored as such, and **never gates a tier or fails the
277
+ audit.** Dated-but-working vocabulary is a quality nudge, not a defect.
278
+
186
279
  ---
187
280
 
188
281
  ## Report
@@ -151,7 +151,7 @@ name package extends abstract overlay isArray children value
151
151
  | `field.double` | float | |
152
152
  | `field.boolean` | true/false | |
153
153
  | `field.date` | calendar date | ISO 8601 `YYYY-MM-DD` on the wire |
154
- | `field.timestamp` | instant | ISO 8601 with timezone on the wire |
154
+ | `field.timestamp` | instant (tz-aware) | ISO 8601 with timezone on the wire; `@localTime: true` for a naive wall-clock value |
155
155
  | `field.decimal` | exact decimal | `@precision` / `@scale`; lossless money/quantity |
156
156
  | `field.currency` | integer minor units | see Currency below |
157
157
  | `field.enum` | string member | `@values` required; see Enum below |
@@ -161,6 +161,140 @@ name package extends abstract overlay isArray children value
161
161
  Common field attributes: `@required`, `@maxLength`, `@column` (physical column
162
162
  name), `@default`, `@filterable`, `@sortable`.
163
163
 
164
+ ### Choosing the right shape — the general decision procedure (ADR-0037)
165
+
166
+ This procedure decides the shape of **any** concept entering the metamodel — a
167
+ field need today, or new vocabulary you register as a custom provider. It is not a
168
+ lookup table of specific answers; it is the routing an LLM re-derives on its own
169
+ for a concept it has never seen.
170
+
171
+ **Ask what the concept *does*, never how it stores.** The guiding question is
172
+ **semantic behavior, not surface storage**: never ask *"is X a string / a number /
173
+ a date?"* — ask *"what does X **do**? Does it have its own native type, behavior,
174
+ or attributes (a **thing** → subtype)? Is it a structural variant of an existing
175
+ thing (a **kind**)? Or does it just modify, validate, or configure an existing type
176
+ (an **attribute**)?"* Shape follows behavior. Don't be misled by tools (JSON
177
+ Schema, Zod) that call everything a "string format" — they only do so because
178
+ JS/JSON has no native types; MetaObjects binds metadata→native types across five
179
+ languages, so the call is behavioral.
180
+
181
+ Run the steps **in order; the first that matches decides:**
182
+
183
+ | # | Test | If yes → | Examples (existing vocab) |
184
+ |---|---|---|---|
185
+ | 0 | **Derivable** from the existing subtype + attrs (`isArray`, `@maxLength`) + structure (`identity.reference`, relationships) + naming? | **derive it in codegen — add NOTHING** | `text[]` ← `field.string` + `isArray`; `varchar(n)` ← `@maxLength`; FK columns ← `identity.reference` |
186
+ | 1 | **Physical-only** — pure DB-storage detail, native type *and* meaning unchanged? | narrow **`@dbColumnType`** escape hatch (sparingly; not a logical type) | open JSON bag → `field.string` + `@dbColumnType: jsonb` |
187
+ | 2a | Its **own thing** — has its own native type, **or** its own behavior, **or** its own attributes? | **SUBTYPE** (the extension point — owns custom codegen, validation, child attrs) | `field.uuid` (native UUID), `field.currency` (minor-unit money behavior), `field.decimal` (exact) |
188
+ | 2b | A **structural variant within** a subtype that already earned 2a — same native type/behavior, different generated *shape*? | **`@kind`** (the one chartered structural-variant axis) | `source.rdb @kind`: table/view/materializedView/storedProc/tableFunction; `template.output @kind`: document/email |
189
+ | 2c | Otherwise it **modifies / validates / configures** an existing type | **ATTRIBUTE** (boolean flag · closed enum · validation · config) | `@localTime` (boolean exception-flag); `@maxLength`/`@precision`/`@scale` (config) |
190
+
191
+ **Reading step 2 (the load-bearing split):**
192
+ - **2a — subtype** is the metamodel's *extension point*: the only shape that owns
193
+ custom logic. Litmus: *"would I plausibly want to attach behavior or extra
194
+ attributes to this later?"* If yes → subtype. A value that merely *serializes* as
195
+ a string is still a subtype if the **concept** has a native type or behavior of
196
+ its own. (General rule, stated abstractly so it survives un-built vocab: *a
197
+ concept with a native type or its own behavior becomes a subtype; a plain string
198
+ that just needs validating becomes a validation attribute.*)
199
+ - **2b — `@kind`** is reserved for variants *inside* a subtype that earned its place
200
+ by 2a. `@kind` on a plain `field.string` is wrong: a plain string isn't a
201
+ behavioral subtype, so there's nothing for the kinds to be *kinds of*. Never let
202
+ `@kind` become a catch-all discriminator.
203
+ - **2c — attribute** shape follows what it is: a **boolean exception-flag** whose
204
+ common case is *absent* (`@localTime` — never a default-true opt-out); a **closed
205
+ set** → enum attr with `allowedValues`; a **validation constraint** that narrows a
206
+ value without changing its type (the thing stays a plain `<base>`, there's no
207
+ behavior to own — else it would be 2a); a **config value** (sizing, precision,
208
+ locale) → a typed attr (`@precision`/`@scale`).
209
+
210
+ **Two corollaries that break ties:**
211
+ - **Self-documentation over economy.** Prefer a specific named attribute
212
+ (`@localTime`, `@unique`) over folding several concerns into one generic attr. A
213
+ name should tell you what it does without a per-type lookup. The *primary*
214
+ universal discriminator is already `type.subType` — don't invent a second one.
215
+ - **Same concept → same attr name; never same-name / different meaning.** If an attr
216
+ name already means something else on another type, give the new one a distinct
217
+ name rather than overload it.
218
+
219
+ This procedure is authority-backed: **ADR-0037** is the source of truth, sequencing
220
+ ADR-0013 (physical vs logical), ADR-0023 (derive, don't invent), and ADR-0001
221
+ (build-time native binding).
222
+
223
+ Canonical form for common field needs — reach for these before inventing anything:
224
+
225
+ | Need | Author it as | Note |
226
+ |---|---|---|
227
+ | IDs / unique keys | `field.uuid` | native UUID; use `@dbColumnType: uuid` only to force a string-typed value over a uuid column on purpose |
228
+ | Money | `field.currency` | integer minor units; never a float |
229
+ | Closed set of symbols | `field.enum` | `@values` required |
230
+ | Instant / event time (created/updated) | `field.timestamp` | instant / tz-aware by default (Postgres `timestamptz`; native `Instant`/`DateTimeOffset`/aware `datetime`) |
231
+ | 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 |
232
+ | A list of anything | `isArray: true` | on the base subtype (e.g. `field.string` + `isArray`) — there is **no** array `@dbColumnType` (retired) |
233
+ | Long / unbounded text | bare `field.string` | add `@maxLength` only when you want `varchar(N)` |
234
+ | Nested structured value | `field.object` | `@objectRef` + `@storage` |
235
+ | Open JSON bag (no fixed shape) | `field.string` + `@dbColumnType: jsonb` | logical type stays string; column is jsonb |
236
+ | URL / URI | `field.uri` | native `URI`/`Uri`; `text` column; URL validation — a real native type + behavior, so a subtype (not a validated string) |
237
+ | IP address | `field.inet` | native IP type; Postgres `inet` column |
238
+ | Validated plain string (email / hostname) | `field.string` + `@stringFormat` | `@stringFormat: email` or `@stringFormat: hostname` — idiomatic per-port validation; don't hand-write the `validator.regex` |
239
+
240
+ **Timestamps — instant by default, `@localTime` for naive wall-clock (ADR-0036 Wave 2).**
241
+ `field.timestamp` is **instant / timezone-aware by default** (Postgres `timestamptz`;
242
+ native `Instant` / `DateTimeOffset` / aware `datetime`) — use it for created/updated/event
243
+ times. Add **`@localTime: true`** only for a genuine naive wall-clock value (a store-open
244
+ time, a birthday-with-time, a recurring local schedule) → `timestamp without time zone`.
245
+ Never use `@dbColumnType: timestamp_with_tz` — it is **retired**; timezone-awareness now
246
+ lives in `field.timestamp` (instant by default) + the `@localTime` naive opt-out.
247
+
248
+ ```json
249
+ { "field.timestamp": { "name": "createdAt", "@required": true } }
250
+ { "field.timestamp": { "name": "opensAt", "@localTime": true } }
251
+ ```
252
+
253
+ **String-shaped natives & validated strings (ADR-0036 Wave 3).** A URL/URI is its own
254
+ native type with URL behavior → **`field.uri`** (subtype, step 2a), not a validated
255
+ string. An IP address likewise → **`field.inet`**. An email or hostname is a *plain
256
+ string that just needs validating* (native type stays `string`) → **`field.string` +
257
+ `@stringFormat: email`/`hostname`** (validation attribute, step 2c) — let the per-port
258
+ codegen emit the idiomatic check; don't hand-write a `validator.regex` for it.
259
+
260
+ ```json
261
+ { "field.uri": { "name": "homepage" } }
262
+ { "field.inet": { "name": "lastLoginIp" } }
263
+ { "field.string": { "name": "email", "@stringFormat": "email", "@required": true } }
264
+ ```
265
+
266
+ **Reverse navigation is generated for you (ADR-0038) — don't hand-write reverse queries.**
267
+ The natural question *"find all the rows that reference this one"* (every `Scene` a
268
+ `GameSession` points at, every `Message` naming a `User`) is **codegen, not authoring**.
269
+ For each FK, the *referenced* entity's query surface gains explicit finders derived from
270
+ the relationship + `identity.reference` metadata — idiomatic per port (a Spring repository
271
+ finder, an EF query method, a Python query function, a TS query function):
272
+
273
+ - `find<Source>By<FkField>(id)` — one indexed `WHERE <fk> = ?` lookup.
274
+ - `find<Source>By<FkField>In(ids)` — the batched variant, one `WHERE <fk> IN (…)` for the
275
+ many-parent case (no N+1).
276
+
277
+ They are **performant by construction** (a single indexed query, no lazy collections /
278
+ proxies / N+1 surprises) and **framework-free** (a plain function over the query layer —
279
+ runs without MetaObjects). When an entity has **two FKs to the same target**, you get **two
280
+ distinct finders** automatically — named by the FK field, unique by construction. There is
281
+ **no attribute to author** for this — reverse navigation is a *codegen feature, not a
282
+ metamodel attribute*: you declare the FK once via `identity.reference`, and the reverse
283
+ finders fall out of codegen. So never hand-roll a `findByParentId` / `WHERE fk = ?` helper —
284
+ consume the generated finder.
285
+
286
+ **Extending the metamodel (custom providers):** the same ordered procedure above
287
+ governs new vocabulary you register — apply it mechanically before registering
288
+ anything. A would-be subtype that differs from an existing one only by a *property*
289
+ is an **attribute**, not a subtype (a "short string" isn't a new field subtype —
290
+ that is `@maxLength`); a plain string that merely needs validating is a **validation
291
+ attribute**, not a subtype (its native type is still `string`, and there's no
292
+ behavior to own); a concept with its own native type or behavior is a **subtype**,
293
+ and structural variants *within* such a subtype are `@kind`. Every new first-class
294
+ element also requires a registered provider + a `registry-conformance` fixture
295
+ (ADR-0023 strict provenance), and closed enums (including any `@kind` value-set)
296
+ carry `allowedValues` in the gate (ADR-0036). ADR-0037 is the authority.
297
+
164
298
  ### Currency
165
299
 
166
300
  `field.currency` stores money as **integer minor units** (cents for USD, yen for
@@ -406,6 +540,16 @@ Resolution facts:
406
540
 
407
541
  `abstract` and `extends` are **structural keys** (bare, no `@`).
408
542
 
543
+ **Extends-inherited properties are real — consume metadata through the resolving
544
+ accessors (ADR-0039).** If you write a custom generator or a metamodel provider that
545
+ reads this metadata, a concrete field/entity's inherited attributes and members live
546
+ on the parent it `extends`, not on the node itself (extends is a super-*reference*, not
547
+ a flatten). Always read a property or iterate a member set via the **resolving/effective**
548
+ accessor (TS `attr()`/`children()`/`fields()`, Python `attrs().get()`), **never an
549
+ `own*()` accessor** — an own-only read silently drops everything inherited via `extends`
550
+ and corrupts the generated code. See the `metaobjects-codegen` skill for the full
551
+ per-port mapping.
552
+
409
553
  **`overlay` is a different concept.** `extends:` is an IS-A relationship between
410
554
  two distinct nodes. `overlay: true` re-opens the *same* named node to amend it
411
555
  across files (same `package` + same `name` → merged; last-writer-wins on attr
@@ -132,6 +132,44 @@ the `generators` array in `metaobjects.config.ts` next to the built-ins — it r
132
132
  the same pass, writes under the same target rules, and carries the `@generated`
133
133
  header so it round-trips like any other.
134
134
 
135
+ ### Never read metadata through an `own*()` accessor (ADR-0039) — top bug source
136
+
137
+ When writing OR reviewing a generator, **read every field/node property and iterate
138
+ every member set through the resolving/effective accessor — never the `own*()` form.**
139
+ `extends` is a **super-reference, not a flatten**: a concrete field/entity that
140
+ `extends` an abstract parent keeps its inherited attributes and members physically on
141
+ the parent, reachable only through the *resolving* accessor. An `own*()` read of an
142
+ effective property (`isArray`, `subType`, `maxLength`, `precision`/`scale`, `default`,
143
+ the physical column name, `objectRef`, `storage`, `required`, …) or an own-only member
144
+ iteration **silently drops everything inherited via `extends`** — the classic symptom
145
+ was a concrete field that inherited `isArray: true` from an abstract parent generating
146
+ a *scalar* column. These reads compile and pass every fixture that never exercises
147
+ `extends`, so they are a latent, cross-port top bug source.
148
+
149
+ **The one legitimate `own*()` use:** a generator emitting a generated **subclass** that
150
+ `extends` a generated base iterates **own members** (`ownFields()`) so the inherited
151
+ members are **not re-emitted** — the generated base class already declares them (the
152
+ `class Sub extends Base` / TPH pattern). Everywhere else, resolve. (The own-mode
153
+ canonical serializer and overlay-merge are the only other sanctioned own reads, and
154
+ they are library-internal, not app-generator concerns.) The one deliberately-own
155
+ attribute is `@dbColumnType` — a physical column-type override that is never inherited.
156
+
157
+ **Per-port own↔resolving mapping** (reach for the resolving column; comment any
158
+ `own*()` call with the sanctioned case it is):
159
+
160
+ | Port | Resolving (default — use this) | Own-only (avoid unless emitting a subclass's own members) |
161
+ |---|---|---|
162
+ | TypeScript | `attr(name)`, `children()`, `fields()` | `ownAttr(name)`, `ownChildren()`, `ownFields()`, the raw `isArray` field flag |
163
+ | Python | `attrs().get(name)`, `children()`, `fields()` | `attr(name)` **(own!)**, `own_children()`, `own_fields()` |
164
+ | Java / Kotlin | `getMetaAttr(name)`, resolving `getChildren()` | `getMetaAttr(name, false)`, own-only child walks |
165
+ | C# | resolving attr/`Children`/`Fields` accessors | `IsArray` native flag, `OwnChildren()`, own attr reads |
166
+
167
+ **Naming inversion — the trap:** the *default-named* accessor is NOT consistently the
168
+ safe one. **TS `attr()` RESOLVES; Python `attr()` is OWN** (own-only). In Python you
169
+ must call `attrs().get(name)` to get the inherited value — a bare `attr(name)` is the
170
+ own read that drops inheritance. When you review or port a generator, check the port's
171
+ convention, not the method name.
172
+
135
173
  **Close but not exact?** You don't always need a new generator — a generated file is
136
174
  a normal source file. Copy it and customize the copy (three-way merge preserves your
137
175
  edits on regen), or customize the template a built-in renders from. Reach for a
@@ -157,6 +195,15 @@ dialect's column types deterministically (`field.string` + `@maxLength` →
157
195
  `varchar(N)`, `field.currency` → integer, `field.uuid` → native `uuid` on
158
196
  Postgres, `field.enum` → `varchar` + `CHECK`, etc.).
159
197
 
198
+ Codegen only ever maps the **shapes you authored** — so author them right. If you
199
+ find the generator emitting the wrong column type, the fix is the field shape, not a
200
+ template hack. See "Choosing the right shape — the general decision procedure" in the
201
+ **`metaobjects-authoring`** skill for the ordered derive→`@dbColumnType`→subtype/
202
+ `@kind`/attribute routing (ADR-0037) — e.g. arrays are `isArray: true` (never an
203
+ array column type) and a native UUID is `field.uuid` (not a string + `@dbColumnType`).
204
+ When you register custom vocabulary for a custom generator, the same ADR-0037
205
+ procedure decides whether it's a subtype, a `@kind` variant, or an attribute.
206
+
160
207
  ## Per-target output
161
208
 
162
209
  Generated code can be routed to **multiple output directories/packages** so each
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metaobjectsdev/sdk",
3
- "version": "0.14.1",
3
+ "version": "0.15.0-rc.1",
4
4
  "description": "Workspace helpers and agent-docs utilities for MetaObjects projects.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -56,7 +56,7 @@
56
56
  "access": "public"
57
57
  },
58
58
  "dependencies": {
59
- "@metaobjectsdev/metadata": "0.14.1",
59
+ "@metaobjectsdev/metadata": "0.15.0-rc.1",
60
60
  "zod": "^3.23.0"
61
61
  },
62
62
  "devDependencies": {