@metaobjectsdev/sdk 0.14.2 → 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.
|
|
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.
|
|
59
|
+
"@metaobjectsdev/metadata": "0.15.0-rc.1",
|
|
60
60
|
"zod": "^3.23.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|