Skip to content

Commit d138a8e

Browse files
dmealingclaude
andcommitted
docs(agent-context): brownfield-adoption direction doctrine + hardened UUID rule
The metaobjects-* skills were framed greenfield-only ("metadata is the spine, generated code is disposable"), which licensed a backward migration loop: change metadata → regenerate → treat the resulting compile/type errors in existing code as bugs to fix. On an adoption onto existing working code / a live DB the direction is the reverse — metadata FOLLOWS the code. - authoring: new "Adopting onto an existing codebase — metadata FOLLOWS the code" section (conditional on the brownfield predicate, reconciled with model-first): author metadata + tune codegen to reproduce the existing native types, names, and nullability; minimize churn to code the generator isn't replacing; ask on ambiguity, default to least existing-code change. Hardened the UUID row + a smell callout: NEVER field.string + @dbColumnType:uuid (generates a String over a uuid column, forcing coercions; verify --db can't see it). field.uuid is the match-the-code choice. - audit: promoted UUID-as-string from non-failing advisory (axis I) to a real correctness-adjacent finding (axis H2) with blast-radius counting; added an adoption-direction guardrail on every proposed cutover. - codegen: "make codegen match the code, not the code match codegen" — customize owned generators / outputPattern / naming to reproduce the existing shape before editing working call sites. - always-on: one concise direction bullet (within the 120-line size gate). - verify: "what verify can't catch — semantic mismodeling" + a project-local CI ratchet lint (grep-fail on field.string paired with @dbColumnType:uuid) as the migration's completion criterion and a permanent backstop. Goldens regenerated across all four stacks; agent-context conformance, size-gate, and capability-grounding suites green (42/42). The SDK agent-context bundle is a build-time artifact (gitignored) and is not committed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013VXidfNN755CyTZTg1w8UV
1 parent c9fc031 commit d138a8e

29 files changed

Lines changed: 734 additions & 20 deletions

File tree

agent-context/skills/metaobjects-audit/SKILL.md

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,21 @@ code behind a grep hit; a "duplicate" validator's *divergence* is the finding.
109109
in committed canonical JSON (ADR-0032); DB-type-as-logical-subtype (ADR-0013); per-port
110110
migration engine where schema is Node-`meta`-owned (ADR-0015).
111111

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+
112127
- [ ] **I. Vocabulary hygiene / modernization (ADVISORY).** Flag already-retired or
113128
deprecated authoring patterns and recommend the canonical form (see § Vocabulary
114129
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
133148
the array column type is retired.
134149
- The `@kind: text` hack (forcing text via a kind override) → **bare `field.string`**
135150
(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`**.
139157
- `@dbColumnType: timestamp_with_tz` (ADR-0036 Wave 2) → **drop it.** `field.timestamp` is
140158
instant / timezone-aware **by default** now; the `timestamp_with_tz` column-type override
141159
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**.
329347

330348
## Guardrails
331349

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".)
332359
- **Parity-gate every cutover** — prove behavior-equivalent before deleting hand-written code; generated schemas are often looser.
333360
- **Verify, don't assume** — read the code behind a grep hit.
334361
- **Verify the DB artifact, not just the types** — the contract may claim a column the view DDL dropped.

agent-context/skills/metaobjects-authoring/SKILL.md

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,56 @@ aggregate — that is almost always **metadata you have not declared yet.** In o
6060
Rule of thumb: **if the metadata could describe it, declaring it is never the wrong
6161
call** — even when a one-off hand-write would be faster today.
6262

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+
63113

64114
## The fused-key encoding (non-negotiable)
65115

@@ -224,7 +274,7 @@ Canonical form for common field needs — reach for these before inventing anyth
224274

225275
| Need | Author it as | Note |
226276
|---|---|---|
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 |
228278
| Money | `field.currency` | integer minor units; never a float |
229279
| Closed set of symbols | `field.enum` | `@values` required |
230280
| 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
237287
| IP address | `field.inet` | native IP type; Postgres `inet` column |
238288
| Validated plain string (email / hostname) | `field.string` + `@stringFormat` | `@stringFormat: email` or `@stringFormat: hostname` — idiomatic per-port validation; don't hand-write the `validator.regex` |
239289

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+
240312
**Timestamps — instant by default, `@localTime` for naive wall-clock (ADR-0036 Wave 2).**
241313
`field.timestamp` is **instant / timezone-aware by default** (Postgres `timestamptz`;
242314
native `Instant` / `DateTimeOffset` / aware `datetime`) — use it for created/updated/event

agent-context/skills/metaobjects-codegen/SKILL.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,22 @@ the data access too.
9090
`meta gen --list` prints every generator by stable name; the `generators` array in
9191
`metaobjects.config.ts` is where you opt each one in or out.
9292

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+
93109
## Write your own generators — the built-ins rarely fit an app exactly
94110

95111
The built-in generators (entity, queries, routes, form, grid, barrel) cover the

agent-context/skills/metaobjects-verify/SKILL.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,32 @@ and (for templates) the missing reference. **Bias toward trusting the tool** —
7070
verify failure almost always means the metadata changed and a derived artifact
7171
didn't follow.
7272

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+
7399
## Schema migrations are the shared TypeScript engine — for every port
74100

75101
This is the load-bearing architectural fact (ADR-0015): **schema migrations are

agent-context/templates/always-on.md.mustache

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ MetaObjects is a metadata standard: typed metadata in `metaobjects/` is the dura
66
spine; generated code is the disposable artifact. Regenerate with `{{codegenCommand}}`.
77

88
## 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.)
910
- 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.
1011
- 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.
1112
- Never hand-edit generated files — change the metadata and regenerate (three-way merge preserves hand-written regions).

0 commit comments

Comments
 (0)