@metaobjectsdev/sdk 0.15.14 → 0.15.15

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.
@@ -109,6 +109,21 @@ code behind a grep hit; a "duplicate" validator's *divergence* is the finding.
109
109
  in committed canonical JSON (ADR-0032); DB-type-as-logical-subtype (ADR-0013); per-port
110
110
  migration engine where schema is Node-`meta`-owned (ADR-0015).
111
111
 
112
+ - [ ] **H2. Wrong native type — `field.<x>` + `@dbColumnType` that hides the real type
113
+ (CORRECTNESS-ADJACENT finding, NOT advisory axis-I).** The headline instance is a **UUID
114
+ column modeled `field.string` + `@dbColumnType: uuid`**: the DB column is uuid but the
115
+ generated property is a **`String`**, so the code coerces `String↔UUID` at every boundary
116
+ and the native type is wrong everywhere the field is used. `verify --db` **cannot** catch it
117
+ (the column type matches), so it hides in plain sight. When it sits in a shared
118
+ `BaseEntity`/`BaseAuditedEntity`, **every inheriting `id`/`tenantId`/FK is wrong** — count the
119
+ blast radius (grep every `field.string` paired with `@dbColumnType: uuid`; it is often
120
+ hundreds of fields). This is a **real finding**, not a modernization nudge: recommend
121
+ `field.uuid` and flag it as a **staged migration** (re-typing `id`/FK ripples through
122
+ repositories, finders, and call sites) — tier by blast radius, not buried as advisory. The
123
+ ONLY non-finding is a field the code genuinely handles as a *string* over a uuid column
124
+ (explicitly justified). Report the total pair count so the migration has a completion
125
+ criterion (see the CI ratchet gate in `metaobjects-verify`).
126
+
112
127
  - [ ] **I. Vocabulary hygiene / modernization (ADVISORY).** Flag already-retired or
113
128
  deprecated authoring patterns and recommend the canonical form (see § Vocabulary
114
129
  hygiene). Advisory severity — scored as modernization opportunities, **never a
@@ -133,9 +148,12 @@ such, surfaced in the roadmap, but **non-failing** (the code works; the form is
133
148
  the array column type is retired.
134
149
  - The `@kind: text` hack (forcing text via a kind override) → **bare `field.string`**
135
150
  (text is the default; no override needed).
136
- - `@dbColumnType: uuid` where a native UUID type is actually wanted → **`field.uuid`**
137
- (a distinct native type is a subtype, not a physical override). Keep `@dbColumnType:
138
- uuid` ONLY for the deliberate string-over-uuid-column case.
151
+ - `@dbColumnType: uuid_array` was covered above. **`field.string` + `@dbColumnType: uuid`
152
+ is NOT advisory it is a real mismodeling finding (see axis H).** It generates a `String`
153
+ where the code uses/wants a native `UUID`, forcing `String↔UUID` coercions at every
154
+ boundary; `verify --db` passes (the column really is uuid), so the schema gate can't see it.
155
+ The genuine string-over-uuid-column case (code truly handles the value as text) is the ONE
156
+ legitimate use and must be explicitly justified — otherwise recommend **`field.uuid`**.
139
157
  - `@dbColumnType: timestamp_with_tz` (ADR-0036 Wave 2) → **drop it.** `field.timestamp` is
140
158
  instant / timezone-aware **by default** now; the `timestamp_with_tz` column-type override
141
159
  is **retired**. Timezone-awareness lives in `field.timestamp` + the `@localTime` opt-out.
@@ -329,6 +347,15 @@ The audit never edits code. Pattern: **dry-run → review the diff → apply**.
329
347
 
330
348
  ## Guardrails
331
349
 
350
+ - **Adoption direction — metadata follows the code.** This is a brownfield project: existing
351
+ code and the live schema are the spec. Every `metadata_sketch` must **reproduce the code's
352
+ existing native types, names, and nullability** (model `field.uuid` where the code uses
353
+ `UUID`, carry over `@column`/`@table`/`@required`) and every cutover must **minimize churn to
354
+ code the generator is not replacing** — customize the codegen to match the existing shape
355
+ before proposing edits to working call sites. A sketch that would re-type or rename working
356
+ code the generator isn't replacing is modeling the wrong thing; when a choice is ambiguous,
357
+ flag it for the human rather than proposing the churnier option. (Full doctrine:
358
+ `metaobjects-authoring` → "Adopting onto an existing codebase".)
332
359
  - **Parity-gate every cutover** — prove behavior-equivalent before deleting hand-written code; generated schemas are often looser.
333
360
  - **Verify, don't assume** — read the code behind a grep hit.
334
361
  - **Verify the DB artifact, not just the types** — the contract may claim a column the view DDL dropped.
@@ -60,6 +60,56 @@ aggregate — that is almost always **metadata you have not declared yet.** In o
60
60
  Rule of thumb: **if the metadata could describe it, declaring it is never the wrong
61
61
  call** — even when a one-off hand-write would be faster today.
62
62
 
63
+ ## Adopting onto an existing codebase — metadata FOLLOWS the code
64
+
65
+ The principle above is the **greenfield** default: declare the model, generate the
66
+ code. **Adoption reverses the direction.** When you are introducing MetaObjects into
67
+ a project that already has **working code and/or a live database** — a migration, not
68
+ a fresh start — the existing code and schema are the specification, and the metadata's
69
+ first job is to **reproduce them**. You are documenting a reality that already runs, not
70
+ redefining it. (The metadata is still the durable spine *going forward*; only the
71
+ *direction of fit on the way in* changes. Once adopted, the greenfield rules resume.)
72
+
73
+ **The observable predicate:** does working code or a populated schema already exist for
74
+ what you're modeling? If yes, you are in adoption mode and these rules apply.
75
+
76
+ **Author metadata to match what the code ALREADY IS — not what you'd design fresh.**
77
+ Read the existing code and schema *first*, then model to reproduce them:
78
+ - The **native types the code uses** are the spec — model `field.uuid` when the code
79
+ uses `UUID`, `field.decimal` when it uses `BigDecimal`, etc. Do **not** pick a
80
+ metadata shape whose generated type differs from the type already in use (that is the
81
+ exact mistake that turned a `UUID` column into a `String` and forced coercions across
82
+ hundreds of fields — see the UUID rule below).
83
+ - The existing **column names, table names, nullability, and field shapes** are the
84
+ spec — carry them over (`@column`, `@table`, `@required`, `@maxLength`) so the
85
+ generated schema matches the live one and `verify --db` is clean.
86
+
87
+ **Customize the CODEGEN to match the existing code before you change the existing code.**
88
+ If generated output doesn't match the code's shape (naming, file layout, imports,
89
+ signatures), **tune the generator/template/config to reproduce it** — that is the
90
+ intended adoption path (owned generators, `outputPattern`, naming strategy — see the
91
+ `metaobjects-codegen` skill), **not a hack**. Reshaping working call sites to satisfy
92
+ the generator's defaults is the *last* resort, not the first.
93
+
94
+ **Minimize churn to code the generator is not replacing.** The ONLY existing code that
95
+ should change is the hand-written layer codegen now **owns** (the hand-rolled
96
+ CRUD/DTO/validator/mapper you're deleting) — parity-gate it, then delete it; that is the
97
+ point of adopting. Everything else — call sites, business logic, adjacent modules —
98
+ stays untouched. **If a metadata choice would force a wide edit across code the
99
+ generator isn't replacing, treat that as a signal the metadata is modeling the wrong
100
+ thing** and re-check it against the code, rather than editing the code to fit the
101
+ metadata.
102
+
103
+ **When a modeling choice is genuinely ambiguous, ask — don't pick the churnier option.**
104
+ If two metadata shapes both fit the existing code and they imply different amounts of
105
+ existing-code change, surface the tradeoff to the user rather than choosing silently.
106
+ **Default to the choice that changes the least existing code.**
107
+
108
+ Do NOT: change metadata, regenerate, and then work through the resulting compile/type
109
+ errors in the existing code as if they were bugs. On an adoption those "errors" are the
110
+ metadata failing to match the code — fix the *metadata* (or the codegen customization),
111
+ not the code.
112
+
63
113
 
64
114
  ## The fused-key encoding (non-negotiable)
65
115
 
@@ -224,7 +274,7 @@ Canonical form for common field needs — reach for these before inventing anyth
224
274
 
225
275
  | Need | Author it as | Note |
226
276
  |---|---|---|
227
- | IDs / unique keys | `field.uuid` | native UUID; use `@dbColumnType: uuid` only to force a string-typed value over a uuid column on purpose |
277
+ | IDs / unique keys / **any UUID column** | `field.uuid` | native UUID type. **NEVER `field.string` + `@dbColumnType: uuid`** see the smell callout below |
228
278
  | Money | `field.currency` | integer minor units; never a float |
229
279
  | Closed set of symbols | `field.enum` | `@values` required |
230
280
  | Instant / event time (created/updated) | `field.timestamp` | instant / tz-aware by default (Postgres `timestamptz`; native `Instant`/`DateTimeOffset`/aware `datetime`) |
@@ -237,6 +287,28 @@ Canonical form for common field needs — reach for these before inventing anyth
237
287
  | IP address | `field.inet` | native IP type; Postgres `inet` column |
238
288
  | 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
289
 
290
+ **UUID columns are `field.uuid` — `field.string` + `@dbColumnType: uuid` is a forbidden smell.**
291
+ A UUID column is modeled with the **`field.uuid`** subtype (native `UUID` / `Guid` /
292
+ `uuid.UUID`, canonical lowercase-hex on the wire). Do **not** reach for `field.string` +
293
+ `@dbColumnType: uuid`: that pairing makes the *DB column* a uuid but generates a **`String`
294
+ property in code**, so every consumer must coerce `String ↔ UUID` at every boundary. It reads
295
+ "correct" because `verify --db` passes (the column really is uuid) — the defect is invisible to
296
+ the schema gate and only shows up as wrong native types rippling through the code. Left in a
297
+ `BaseEntity`, it is inherited by every `id`/`tenantId`/FK — hundreds of fields across a repo, a
298
+ staged multi-PR migration to undo. So:
299
+
300
+ ```json
301
+ { "field.uuid": { "name": "id" } } // ✅ native UUID
302
+ { "field.string": { "name": "id", "@dbColumnType": "uuid" } } // ❌ generates String over a uuid column
303
+ ```
304
+
305
+ The `field.string` + `@dbColumnType: uuid` form is legitimate **only** in the genuinely rare
306
+ case where your code truly wants a *string-typed* value stored in a uuid column (you handle the
307
+ uuid as text everywhere and never as a native UUID). That is an explicit, justified exception —
308
+ not a default, and never the way to model an identifier. When adopting an existing schema whose
309
+ code already uses `UUID`, `field.uuid` is the match-the-code choice (see "Adopting onto an
310
+ existing codebase" above).
311
+
240
312
  **Timestamps — instant by default, `@localTime` for naive wall-clock (ADR-0036 Wave 2).**
241
313
  `field.timestamp` is **instant / timezone-aware by default** (Postgres `timestamptz`;
242
314
  native `Instant` / `DateTimeOffset` / aware `datetime`) — use it for created/updated/event
@@ -90,6 +90,22 @@ the data access too.
90
90
  `meta gen --list` prints every generator by stable name; the `generators` array in
91
91
  `metaobjects.config.ts` is where you opt each one in or out.
92
92
 
93
+ ### Adopting onto existing code — make codegen match the code, not the code match codegen
94
+
95
+ On a **brownfield adoption** (existing working code / live schema — see
96
+ `metaobjects-authoring` → "Adopting onto an existing codebase"), the goal of codegen is to
97
+ **reproduce the shape the code already has** so the generated output drops in with minimal
98
+ churn. When generated output doesn't match — different names, file layout, imports, or
99
+ signatures than the existing code — **customize the codegen to match the existing code first**,
100
+ using the à-la-carte layers, `outputPattern`/target layout, naming strategy, template
101
+ customization, and owned/custom generators described here. That is the intended adoption path,
102
+ **not a hack** — the whole point of owned generators + three-way merge is to shape output to
103
+ your codebase. Reshaping working call sites to fit the generator's defaults is the **last**
104
+ resort, and only for the layer codegen is actually replacing (the hand-rolled CRUD/DTO/mapper
105
+ you're deleting behind a parity gate). If matching the existing shape would require a genuinely
106
+ hacky generator contortion, that is the moment to **ask the human** which side should give —
107
+ don't silently churn the existing code.
108
+
93
109
  ## Write your own generators — the built-ins rarely fit an app exactly
94
110
 
95
111
  The built-in generators (entity, queries, routes, form, grid, barrel) cover the
@@ -70,6 +70,32 @@ and (for templates) the missing reference. **Bias toward trusting the tool** —
70
70
  verify failure almost always means the metadata changed and a derived artifact
71
71
  didn't follow.
72
72
 
73
+ ## What `verify` can't catch — semantic mismodeling (add a CI ratchet lint)
74
+
75
+ The three subverbs check that derived artifacts *match the metadata*. They do **not**
76
+ check that the metadata *models the right thing* — so a semantically wrong metadata
77
+ choice that is internally consistent passes clean. The canonical case: a UUID column
78
+ modeled **`field.string` + `@dbColumnType: uuid`**. The generated property is a `String`,
79
+ the DB column is genuinely `uuid`, so **`verify --db` passes** while every consumer coerces
80
+ `String↔UUID` and the native type is wrong throughout the code (see `metaobjects-authoring`
81
+ → the UUID smell). No drift subverb can see it, because nothing has drifted — the model
82
+ itself is wrong.
83
+
84
+ For semantic invariants like this, add a **project-local CI ratchet lint** over the
85
+ metadata sources — a grep-level gate is enough:
86
+
87
+ ```
88
+ # fail the build if any field.string carries @dbColumnType: uuid (a UUID-column-as-string smell).
89
+ # Illustrative — tune the matcher to your source format (canonical JSON vs sigil-free YAML) and
90
+ # tighten to per-node scope if a coarse co-occurrence match is too broad for your files.
91
+ ! grep -rEzl '"field\.string"[^}]*"@dbColumnType"[^}]*"uuid"' metaobjects/
92
+ ```
93
+
94
+ Make it a **ratchet**: it can't go green until the last offending field is migrated to
95
+ `field.uuid`, so it doubles as the migration's completion criterion **and** a permanent
96
+ backstop against reintroducing the smell. The same pattern generalizes to any semantic
97
+ metadata rule your project wants enforced that `verify` structurally can't express.
98
+
73
99
  ## Schema migrations are the shared TypeScript engine — for every port
74
100
 
75
101
  This is the load-bearing architectural fact (ADR-0015): **schema migrations are
@@ -6,6 +6,7 @@ MetaObjects is a metadata standard: typed metadata in `metaobjects/` is the dura
6
6
  spine; generated code is the disposable artifact. Regenerate with `{{codegenCommand}}`.
7
7
 
8
8
  ## Principles
9
+ - **Adopting onto existing code? Metadata FOLLOWS the code.** On a migration (existing working code / live DB), author metadata + tune codegen to *reproduce* what the code already is — native types (`field.uuid` when the code uses `UUID`, not `field.string`), names, nullability — so regen changes as little existing code as possible. The only existing code that should change is the hand-written layer codegen replaces; ask when a modeling choice is ambiguous. (Greenfield: model-first, below.)
9
10
  - Pattern-derivable from metadata = codegen, never hand-write — FKs, CRUD, validators, finders, and the database schema and migrations. The schema is a disposable, generated artifact: change the metadata and regenerate, never hand-write SQL.
10
11
  - The **live database** is a derived artifact too — never hand-apply a schema change to a running DB (ad-hoc `psql`/console `ALTER`/`CREATE`/`DROP`), not even to preview a column or unblock a boot. Apply schema only through `meta migrate` (metadata → DDL). A hand-applied change drifts the live DB from the metadata + migration history and collides at the next migrate/boot ("column already exists") — a state no migration can reproduce. Run `meta verify --db` after any DB-touching work to catch that drift early.
11
12
  - Never hand-edit generated files — change the metadata and regenerate (three-way merge preserves hand-written regions).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metaobjectsdev/sdk",
3
- "version": "0.15.14",
3
+ "version": "0.15.15",
4
4
  "description": "Workspace helpers and agent-docs utilities for MetaObjects projects.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",