@metaobjectsdev/sdk 0.15.14 → 0.15.15-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.
- package/agent-context/skills/metaobjects-audit/SKILL.md +30 -3
- package/agent-context/skills/metaobjects-authoring/SKILL.md +73 -1
- package/agent-context/skills/metaobjects-codegen/SKILL.md +16 -0
- package/agent-context/skills/metaobjects-verify/SKILL.md +26 -0
- package/agent-context/templates/always-on.md.mustache +1 -0
- package/package.json +1 -1
|
@@ -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:
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
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).
|