diff --git a/agent-context/skills/metaobjects-audit/SKILL.md b/agent-context/skills/metaobjects-audit/SKILL.md index 4361fc8d8..b4e08a4b3 100644 --- a/agent-context/skills/metaobjects-audit/SKILL.md +++ b/agent-context/skills/metaobjects-audit/SKILL.md @@ -109,6 +109,21 @@ code behind a grep hit; a "duplicate" validator's *divergence* is the finding. in committed canonical JSON (ADR-0032); DB-type-as-logical-subtype (ADR-0013); per-port migration engine where schema is Node-`meta`-owned (ADR-0015). +- [ ] **H2. Wrong native type — `field.` + `@dbColumnType` that hides the real type + (CORRECTNESS-ADJACENT finding, NOT advisory axis-I).** The headline instance is a **UUID + column modeled `field.string` + `@dbColumnType: uuid`**: the DB column is uuid but the + generated property is a **`String`**, so the code coerces `String↔UUID` at every boundary + and the native type is wrong everywhere the field is used. `verify --db` **cannot** catch it + (the column type matches), so it hides in plain sight. When it sits in a shared + `BaseEntity`/`BaseAuditedEntity`, **every inheriting `id`/`tenantId`/FK is wrong** — count the + blast radius (grep every `field.string` paired with `@dbColumnType: uuid`; it is often + hundreds of fields). This is a **real finding**, not a modernization nudge: recommend + `field.uuid` and flag it as a **staged migration** (re-typing `id`/FK ripples through + repositories, finders, and call sites) — tier by blast radius, not buried as advisory. The + ONLY non-finding is a field the code genuinely handles as a *string* over a uuid column + (explicitly justified). Report the total pair count so the migration has a completion + criterion (see the CI ratchet gate in `metaobjects-verify`). + - [ ] **I. Vocabulary hygiene / modernization (ADVISORY).** Flag already-retired or deprecated authoring patterns and recommend the canonical form (see § Vocabulary 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 the array column type is retired. - The `@kind: text` hack (forcing text via a kind override) → **bare `field.string`** (text is the default; no override needed). -- `@dbColumnType: uuid` where a native UUID type is actually wanted → **`field.uuid`** - (a distinct native type is a subtype, not a physical override). Keep `@dbColumnType: - uuid` ONLY for the deliberate string-over-uuid-column case. +- `@dbColumnType: uuid_array` was covered above. **`field.string` + `@dbColumnType: uuid` + is NOT advisory — it is a real mismodeling finding (see axis H).** It generates a `String` + where the code uses/wants a native `UUID`, forcing `String↔UUID` coercions at every + boundary; `verify --db` passes (the column really is uuid), so the schema gate can't see it. + The genuine string-over-uuid-column case (code truly handles the value as text) is the ONE + legitimate use and must be explicitly justified — otherwise recommend **`field.uuid`**. - `@dbColumnType: timestamp_with_tz` (ADR-0036 Wave 2) → **drop it.** `field.timestamp` is instant / timezone-aware **by default** now; the `timestamp_with_tz` column-type override 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**. ## Guardrails +- **Adoption direction — metadata follows the code.** This is a brownfield project: existing + code and the live schema are the spec. Every `metadata_sketch` must **reproduce the code's + existing native types, names, and nullability** (model `field.uuid` where the code uses + `UUID`, carry over `@column`/`@table`/`@required`) and every cutover must **minimize churn to + code the generator is not replacing** — customize the codegen to match the existing shape + before proposing edits to working call sites. A sketch that would re-type or rename working + code the generator isn't replacing is modeling the wrong thing; when a choice is ambiguous, + flag it for the human rather than proposing the churnier option. (Full doctrine: + `metaobjects-authoring` → "Adopting onto an existing codebase".) - **Parity-gate every cutover** — prove behavior-equivalent before deleting hand-written code; generated schemas are often looser. - **Verify, don't assume** — read the code behind a grep hit. - **Verify the DB artifact, not just the types** — the contract may claim a column the view DDL dropped. diff --git a/agent-context/skills/metaobjects-authoring/SKILL.md b/agent-context/skills/metaobjects-authoring/SKILL.md index 6138460d6..826e4ce2b 100644 --- a/agent-context/skills/metaobjects-authoring/SKILL.md +++ b/agent-context/skills/metaobjects-authoring/SKILL.md @@ -60,6 +60,56 @@ aggregate — that is almost always **metadata you have not declared yet.** In o Rule of thumb: **if the metadata could describe it, declaring it is never the wrong call** — even when a one-off hand-write would be faster today. +## Adopting onto an existing codebase — metadata FOLLOWS the code + +The principle above is the **greenfield** default: declare the model, generate the +code. **Adoption reverses the direction.** When you are introducing MetaObjects into +a project that already has **working code and/or a live database** — a migration, not +a fresh start — the existing code and schema are the specification, and the metadata's +first job is to **reproduce them**. You are documenting a reality that already runs, not +redefining it. (The metadata is still the durable spine *going forward*; only the +*direction of fit on the way in* changes. Once adopted, the greenfield rules resume.) + +**The observable predicate:** does working code or a populated schema already exist for +what you're modeling? If yes, you are in adoption mode and these rules apply. + +**Author metadata to match what the code ALREADY IS — not what you'd design fresh.** +Read the existing code and schema *first*, then model to reproduce them: +- The **native types the code uses** are the spec — model `field.uuid` when the code + uses `UUID`, `field.decimal` when it uses `BigDecimal`, etc. Do **not** pick a + metadata shape whose generated type differs from the type already in use (that is the + exact mistake that turned a `UUID` column into a `String` and forced coercions across + hundreds of fields — see the UUID rule below). +- The existing **column names, table names, nullability, and field shapes** are the + spec — carry them over (`@column`, `@table`, `@required`, `@maxLength`) so the + generated schema matches the live one and `verify --db` is clean. + +**Customize the CODEGEN to match the existing code before you change the existing code.** +If generated output doesn't match the code's shape (naming, file layout, imports, +signatures), **tune the generator/template/config to reproduce it** — that is the +intended adoption path (owned generators, `outputPattern`, naming strategy — see the +`metaobjects-codegen` skill), **not a hack**. Reshaping working call sites to satisfy +the generator's defaults is the *last* resort, not the first. + +**Minimize churn to code the generator is not replacing.** The ONLY existing code that +should change is the hand-written layer codegen now **owns** (the hand-rolled +CRUD/DTO/validator/mapper you're deleting) — parity-gate it, then delete it; that is the +point of adopting. Everything else — call sites, business logic, adjacent modules — +stays untouched. **If a metadata choice would force a wide edit across code the +generator isn't replacing, treat that as a signal the metadata is modeling the wrong +thing** and re-check it against the code, rather than editing the code to fit the +metadata. + +**When a modeling choice is genuinely ambiguous, ask — don't pick the churnier option.** +If two metadata shapes both fit the existing code and they imply different amounts of +existing-code change, surface the tradeoff to the user rather than choosing silently. +**Default to the choice that changes the least existing code.** + +Do NOT: change metadata, regenerate, and then work through the resulting compile/type +errors in the existing code as if they were bugs. On an adoption those "errors" are the +metadata failing to match the code — fix the *metadata* (or the codegen customization), +not the code. + ## The fused-key encoding (non-negotiable) @@ -224,7 +274,7 @@ Canonical form for common field needs — reach for these before inventing anyth | Need | Author it as | Note | |---|---|---| -| IDs / unique keys | `field.uuid` | native UUID; use `@dbColumnType: uuid` only to force a string-typed value over a uuid column on purpose | +| IDs / unique keys / **any UUID column** | `field.uuid` | native UUID type. **NEVER `field.string` + `@dbColumnType: uuid`** — see the smell callout below | | Money | `field.currency` | integer minor units; never a float | | Closed set of symbols | `field.enum` | `@values` required | | 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 | IP address | `field.inet` | native IP type; Postgres `inet` column | | Validated plain string (email / hostname) | `field.string` + `@stringFormat` | `@stringFormat: email` or `@stringFormat: hostname` — idiomatic per-port validation; don't hand-write the `validator.regex` | +**UUID columns are `field.uuid` — `field.string` + `@dbColumnType: uuid` is a forbidden smell.** +A UUID column is modeled with the **`field.uuid`** subtype (native `UUID` / `Guid` / +`uuid.UUID`, canonical lowercase-hex on the wire). Do **not** reach for `field.string` + +`@dbColumnType: uuid`: that pairing makes the *DB column* a uuid but generates a **`String` +property in code**, so every consumer must coerce `String ↔ UUID` at every boundary. It reads +"correct" because `verify --db` passes (the column really is uuid) — the defect is invisible to +the schema gate and only shows up as wrong native types rippling through the code. Left in a +`BaseEntity`, it is inherited by every `id`/`tenantId`/FK — hundreds of fields across a repo, a +staged multi-PR migration to undo. So: + +```json +{ "field.uuid": { "name": "id" } } // ✅ native UUID +{ "field.string": { "name": "id", "@dbColumnType": "uuid" } } // ❌ generates String over a uuid column +``` + +The `field.string` + `@dbColumnType: uuid` form is legitimate **only** in the genuinely rare +case where your code truly wants a *string-typed* value stored in a uuid column (you handle the +uuid as text everywhere and never as a native UUID). That is an explicit, justified exception — +not a default, and never the way to model an identifier. When adopting an existing schema whose +code already uses `UUID`, `field.uuid` is the match-the-code choice (see "Adopting onto an +existing codebase" above). + **Timestamps — instant by default, `@localTime` for naive wall-clock (ADR-0036 Wave 2).** `field.timestamp` is **instant / timezone-aware by default** (Postgres `timestamptz`; native `Instant` / `DateTimeOffset` / aware `datetime`) — use it for created/updated/event diff --git a/agent-context/skills/metaobjects-codegen/SKILL.md b/agent-context/skills/metaobjects-codegen/SKILL.md index 79a0e9d5c..6a8aee636 100644 --- a/agent-context/skills/metaobjects-codegen/SKILL.md +++ b/agent-context/skills/metaobjects-codegen/SKILL.md @@ -90,6 +90,22 @@ the data access too. `meta gen --list` prints every generator by stable name; the `generators` array in `metaobjects.config.ts` is where you opt each one in or out. +### Adopting onto existing code — make codegen match the code, not the code match codegen + +On a **brownfield adoption** (existing working code / live schema — see +`metaobjects-authoring` → "Adopting onto an existing codebase"), the goal of codegen is to +**reproduce the shape the code already has** so the generated output drops in with minimal +churn. When generated output doesn't match — different names, file layout, imports, or +signatures than the existing code — **customize the codegen to match the existing code first**, +using the à-la-carte layers, `outputPattern`/target layout, naming strategy, template +customization, and owned/custom generators described here. That is the intended adoption path, +**not a hack** — the whole point of owned generators + three-way merge is to shape output to +your codebase. Reshaping working call sites to fit the generator's defaults is the **last** +resort, and only for the layer codegen is actually replacing (the hand-rolled CRUD/DTO/mapper +you're deleting behind a parity gate). If matching the existing shape would require a genuinely +hacky generator contortion, that is the moment to **ask the human** which side should give — +don't silently churn the existing code. + ## Write your own generators — the built-ins rarely fit an app exactly The built-in generators (entity, queries, routes, form, grid, barrel) cover the diff --git a/agent-context/skills/metaobjects-verify/SKILL.md b/agent-context/skills/metaobjects-verify/SKILL.md index 472673850..eda0ff48d 100644 --- a/agent-context/skills/metaobjects-verify/SKILL.md +++ b/agent-context/skills/metaobjects-verify/SKILL.md @@ -70,6 +70,32 @@ and (for templates) the missing reference. **Bias toward trusting the tool** — verify failure almost always means the metadata changed and a derived artifact didn't follow. +## What `verify` can't catch — semantic mismodeling (add a CI ratchet lint) + +The three subverbs check that derived artifacts *match the metadata*. They do **not** +check that the metadata *models the right thing* — so a semantically wrong metadata +choice that is internally consistent passes clean. The canonical case: a UUID column +modeled **`field.string` + `@dbColumnType: uuid`**. The generated property is a `String`, +the DB column is genuinely `uuid`, so **`verify --db` passes** while every consumer coerces +`String↔UUID` and the native type is wrong throughout the code (see `metaobjects-authoring` +→ the UUID smell). No drift subverb can see it, because nothing has drifted — the model +itself is wrong. + +For semantic invariants like this, add a **project-local CI ratchet lint** over the +metadata sources — a grep-level gate is enough: + +``` +# fail the build if any field.string carries @dbColumnType: uuid (a UUID-column-as-string smell). +# Illustrative — tune the matcher to your source format (canonical JSON vs sigil-free YAML) and +# tighten to per-node scope if a coarse co-occurrence match is too broad for your files. +! grep -rEzl '"field\.string"[^}]*"@dbColumnType"[^}]*"uuid"' metaobjects/ +``` + +Make it a **ratchet**: it can't go green until the last offending field is migrated to +`field.uuid`, so it doubles as the migration's completion criterion **and** a permanent +backstop against reintroducing the smell. The same pattern generalizes to any semantic +metadata rule your project wants enforced that `verify` structurally can't express. + ## Schema migrations are the shared TypeScript engine — for every port This is the load-bearing architectural fact (ADR-0015): **schema migrations are diff --git a/agent-context/templates/always-on.md.mustache b/agent-context/templates/always-on.md.mustache index a74ea8f57..8f3500724 100644 --- a/agent-context/templates/always-on.md.mustache +++ b/agent-context/templates/always-on.md.mustache @@ -6,6 +6,7 @@ MetaObjects is a metadata standard: typed metadata in `metaobjects/` is the dura spine; generated code is the disposable artifact. Regenerate with `{{codegenCommand}}`. ## Principles +- **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.) - 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. - 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. - Never hand-edit generated files — change the metadata and regenerate (three-way merge preserves hand-written regions). diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md index 4361fc8d8..b4e08a4b3 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -109,6 +109,21 @@ code behind a grep hit; a "duplicate" validator's *divergence* is the finding. in committed canonical JSON (ADR-0032); DB-type-as-logical-subtype (ADR-0013); per-port migration engine where schema is Node-`meta`-owned (ADR-0015). +- [ ] **H2. Wrong native type — `field.` + `@dbColumnType` that hides the real type + (CORRECTNESS-ADJACENT finding, NOT advisory axis-I).** The headline instance is a **UUID + column modeled `field.string` + `@dbColumnType: uuid`**: the DB column is uuid but the + generated property is a **`String`**, so the code coerces `String↔UUID` at every boundary + and the native type is wrong everywhere the field is used. `verify --db` **cannot** catch it + (the column type matches), so it hides in plain sight. When it sits in a shared + `BaseEntity`/`BaseAuditedEntity`, **every inheriting `id`/`tenantId`/FK is wrong** — count the + blast radius (grep every `field.string` paired with `@dbColumnType: uuid`; it is often + hundreds of fields). This is a **real finding**, not a modernization nudge: recommend + `field.uuid` and flag it as a **staged migration** (re-typing `id`/FK ripples through + repositories, finders, and call sites) — tier by blast radius, not buried as advisory. The + ONLY non-finding is a field the code genuinely handles as a *string* over a uuid column + (explicitly justified). Report the total pair count so the migration has a completion + criterion (see the CI ratchet gate in `metaobjects-verify`). + - [ ] **I. Vocabulary hygiene / modernization (ADVISORY).** Flag already-retired or deprecated authoring patterns and recommend the canonical form (see § Vocabulary 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 the array column type is retired. - The `@kind: text` hack (forcing text via a kind override) → **bare `field.string`** (text is the default; no override needed). -- `@dbColumnType: uuid` where a native UUID type is actually wanted → **`field.uuid`** - (a distinct native type is a subtype, not a physical override). Keep `@dbColumnType: - uuid` ONLY for the deliberate string-over-uuid-column case. +- `@dbColumnType: uuid_array` was covered above. **`field.string` + `@dbColumnType: uuid` + is NOT advisory — it is a real mismodeling finding (see axis H).** It generates a `String` + where the code uses/wants a native `UUID`, forcing `String↔UUID` coercions at every + boundary; `verify --db` passes (the column really is uuid), so the schema gate can't see it. + The genuine string-over-uuid-column case (code truly handles the value as text) is the ONE + legitimate use and must be explicitly justified — otherwise recommend **`field.uuid`**. - `@dbColumnType: timestamp_with_tz` (ADR-0036 Wave 2) → **drop it.** `field.timestamp` is instant / timezone-aware **by default** now; the `timestamp_with_tz` column-type override 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**. ## Guardrails +- **Adoption direction — metadata follows the code.** This is a brownfield project: existing + code and the live schema are the spec. Every `metadata_sketch` must **reproduce the code's + existing native types, names, and nullability** (model `field.uuid` where the code uses + `UUID`, carry over `@column`/`@table`/`@required`) and every cutover must **minimize churn to + code the generator is not replacing** — customize the codegen to match the existing shape + before proposing edits to working call sites. A sketch that would re-type or rename working + code the generator isn't replacing is modeling the wrong thing; when a choice is ambiguous, + flag it for the human rather than proposing the churnier option. (Full doctrine: + `metaobjects-authoring` → "Adopting onto an existing codebase".) - **Parity-gate every cutover** — prove behavior-equivalent before deleting hand-written code; generated schemas are often looser. - **Verify, don't assume** — read the code behind a grep hit. - **Verify the DB artifact, not just the types** — the contract may claim a column the view DDL dropped. diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md index 6138460d6..826e4ce2b 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md @@ -60,6 +60,56 @@ aggregate — that is almost always **metadata you have not declared yet.** In o Rule of thumb: **if the metadata could describe it, declaring it is never the wrong call** — even when a one-off hand-write would be faster today. +## Adopting onto an existing codebase — metadata FOLLOWS the code + +The principle above is the **greenfield** default: declare the model, generate the +code. **Adoption reverses the direction.** When you are introducing MetaObjects into +a project that already has **working code and/or a live database** — a migration, not +a fresh start — the existing code and schema are the specification, and the metadata's +first job is to **reproduce them**. You are documenting a reality that already runs, not +redefining it. (The metadata is still the durable spine *going forward*; only the +*direction of fit on the way in* changes. Once adopted, the greenfield rules resume.) + +**The observable predicate:** does working code or a populated schema already exist for +what you're modeling? If yes, you are in adoption mode and these rules apply. + +**Author metadata to match what the code ALREADY IS — not what you'd design fresh.** +Read the existing code and schema *first*, then model to reproduce them: +- The **native types the code uses** are the spec — model `field.uuid` when the code + uses `UUID`, `field.decimal` when it uses `BigDecimal`, etc. Do **not** pick a + metadata shape whose generated type differs from the type already in use (that is the + exact mistake that turned a `UUID` column into a `String` and forced coercions across + hundreds of fields — see the UUID rule below). +- The existing **column names, table names, nullability, and field shapes** are the + spec — carry them over (`@column`, `@table`, `@required`, `@maxLength`) so the + generated schema matches the live one and `verify --db` is clean. + +**Customize the CODEGEN to match the existing code before you change the existing code.** +If generated output doesn't match the code's shape (naming, file layout, imports, +signatures), **tune the generator/template/config to reproduce it** — that is the +intended adoption path (owned generators, `outputPattern`, naming strategy — see the +`metaobjects-codegen` skill), **not a hack**. Reshaping working call sites to satisfy +the generator's defaults is the *last* resort, not the first. + +**Minimize churn to code the generator is not replacing.** The ONLY existing code that +should change is the hand-written layer codegen now **owns** (the hand-rolled +CRUD/DTO/validator/mapper you're deleting) — parity-gate it, then delete it; that is the +point of adopting. Everything else — call sites, business logic, adjacent modules — +stays untouched. **If a metadata choice would force a wide edit across code the +generator isn't replacing, treat that as a signal the metadata is modeling the wrong +thing** and re-check it against the code, rather than editing the code to fit the +metadata. + +**When a modeling choice is genuinely ambiguous, ask — don't pick the churnier option.** +If two metadata shapes both fit the existing code and they imply different amounts of +existing-code change, surface the tradeoff to the user rather than choosing silently. +**Default to the choice that changes the least existing code.** + +Do NOT: change metadata, regenerate, and then work through the resulting compile/type +errors in the existing code as if they were bugs. On an adoption those "errors" are the +metadata failing to match the code — fix the *metadata* (or the codegen customization), +not the code. + ## The fused-key encoding (non-negotiable) @@ -224,7 +274,7 @@ Canonical form for common field needs — reach for these before inventing anyth | Need | Author it as | Note | |---|---|---| -| IDs / unique keys | `field.uuid` | native UUID; use `@dbColumnType: uuid` only to force a string-typed value over a uuid column on purpose | +| IDs / unique keys / **any UUID column** | `field.uuid` | native UUID type. **NEVER `field.string` + `@dbColumnType: uuid`** — see the smell callout below | | Money | `field.currency` | integer minor units; never a float | | Closed set of symbols | `field.enum` | `@values` required | | 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 | IP address | `field.inet` | native IP type; Postgres `inet` column | | Validated plain string (email / hostname) | `field.string` + `@stringFormat` | `@stringFormat: email` or `@stringFormat: hostname` — idiomatic per-port validation; don't hand-write the `validator.regex` | +**UUID columns are `field.uuid` — `field.string` + `@dbColumnType: uuid` is a forbidden smell.** +A UUID column is modeled with the **`field.uuid`** subtype (native `UUID` / `Guid` / +`uuid.UUID`, canonical lowercase-hex on the wire). Do **not** reach for `field.string` + +`@dbColumnType: uuid`: that pairing makes the *DB column* a uuid but generates a **`String` +property in code**, so every consumer must coerce `String ↔ UUID` at every boundary. It reads +"correct" because `verify --db` passes (the column really is uuid) — the defect is invisible to +the schema gate and only shows up as wrong native types rippling through the code. Left in a +`BaseEntity`, it is inherited by every `id`/`tenantId`/FK — hundreds of fields across a repo, a +staged multi-PR migration to undo. So: + +```json +{ "field.uuid": { "name": "id" } } // ✅ native UUID +{ "field.string": { "name": "id", "@dbColumnType": "uuid" } } // ❌ generates String over a uuid column +``` + +The `field.string` + `@dbColumnType: uuid` form is legitimate **only** in the genuinely rare +case where your code truly wants a *string-typed* value stored in a uuid column (you handle the +uuid as text everywhere and never as a native UUID). That is an explicit, justified exception — +not a default, and never the way to model an identifier. When adopting an existing schema whose +code already uses `UUID`, `field.uuid` is the match-the-code choice (see "Adopting onto an +existing codebase" above). + **Timestamps — instant by default, `@localTime` for naive wall-clock (ADR-0036 Wave 2).** `field.timestamp` is **instant / timezone-aware by default** (Postgres `timestamptz`; native `Instant` / `DateTimeOffset` / aware `datetime`) — use it for created/updated/event diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md index 79a0e9d5c..6a8aee636 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -90,6 +90,22 @@ the data access too. `meta gen --list` prints every generator by stable name; the `generators` array in `metaobjects.config.ts` is where you opt each one in or out. +### Adopting onto existing code — make codegen match the code, not the code match codegen + +On a **brownfield adoption** (existing working code / live schema — see +`metaobjects-authoring` → "Adopting onto an existing codebase"), the goal of codegen is to +**reproduce the shape the code already has** so the generated output drops in with minimal +churn. When generated output doesn't match — different names, file layout, imports, or +signatures than the existing code — **customize the codegen to match the existing code first**, +using the à-la-carte layers, `outputPattern`/target layout, naming strategy, template +customization, and owned/custom generators described here. That is the intended adoption path, +**not a hack** — the whole point of owned generators + three-way merge is to shape output to +your codebase. Reshaping working call sites to fit the generator's defaults is the **last** +resort, and only for the layer codegen is actually replacing (the hand-rolled CRUD/DTO/mapper +you're deleting behind a parity gate). If matching the existing shape would require a genuinely +hacky generator contortion, that is the moment to **ask the human** which side should give — +don't silently churn the existing code. + ## Write your own generators — the built-ins rarely fit an app exactly The built-in generators (entity, queries, routes, form, grid, barrel) cover the diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/SKILL.md index 472673850..eda0ff48d 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/SKILL.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/SKILL.md @@ -70,6 +70,32 @@ and (for templates) the missing reference. **Bias toward trusting the tool** — verify failure almost always means the metadata changed and a derived artifact didn't follow. +## What `verify` can't catch — semantic mismodeling (add a CI ratchet lint) + +The three subverbs check that derived artifacts *match the metadata*. They do **not** +check that the metadata *models the right thing* — so a semantically wrong metadata +choice that is internally consistent passes clean. The canonical case: a UUID column +modeled **`field.string` + `@dbColumnType: uuid`**. The generated property is a `String`, +the DB column is genuinely `uuid`, so **`verify --db` passes** while every consumer coerces +`String↔UUID` and the native type is wrong throughout the code (see `metaobjects-authoring` +→ the UUID smell). No drift subverb can see it, because nothing has drifted — the model +itself is wrong. + +For semantic invariants like this, add a **project-local CI ratchet lint** over the +metadata sources — a grep-level gate is enough: + +``` +# fail the build if any field.string carries @dbColumnType: uuid (a UUID-column-as-string smell). +# Illustrative — tune the matcher to your source format (canonical JSON vs sigil-free YAML) and +# tighten to per-node scope if a coarse co-occurrence match is too broad for your files. +! grep -rEzl '"field\.string"[^}]*"@dbColumnType"[^}]*"uuid"' metaobjects/ +``` + +Make it a **ratchet**: it can't go green until the last offending field is migrated to +`field.uuid`, so it doubles as the migration's completion criterion **and** a permanent +backstop against reintroducing the smell. The same pattern generalizes to any semantic +metadata rule your project wants enforced that `verify` structurally can't express. + ## Schema migrations are the shared TypeScript engine — for every port This is the load-bearing architectural fact (ADR-0015): **schema migrations are diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md index 3c6c730bb..97d416ccc 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md @@ -6,6 +6,7 @@ MetaObjects is a metadata standard: typed metadata in `metaobjects/` is the dura spine; generated code is the disposable artifact. Regenerate with `mvn metaobjects:generate`. ## Principles +- **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.) - 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. - 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. - Never hand-edit generated files — change the metadata and regenerate (three-way merge preserves hand-written regions). diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md index 3c6c730bb..97d416ccc 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md @@ -6,6 +6,7 @@ MetaObjects is a metadata standard: typed metadata in `metaobjects/` is the dura spine; generated code is the disposable artifact. Regenerate with `mvn metaobjects:generate`. ## Principles +- **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.) - 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. - 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. - Never hand-edit generated files — change the metadata and regenerate (three-way merge preserves hand-written regions). diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md index 4361fc8d8..b4e08a4b3 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -109,6 +109,21 @@ code behind a grep hit; a "duplicate" validator's *divergence* is the finding. in committed canonical JSON (ADR-0032); DB-type-as-logical-subtype (ADR-0013); per-port migration engine where schema is Node-`meta`-owned (ADR-0015). +- [ ] **H2. Wrong native type — `field.` + `@dbColumnType` that hides the real type + (CORRECTNESS-ADJACENT finding, NOT advisory axis-I).** The headline instance is a **UUID + column modeled `field.string` + `@dbColumnType: uuid`**: the DB column is uuid but the + generated property is a **`String`**, so the code coerces `String↔UUID` at every boundary + and the native type is wrong everywhere the field is used. `verify --db` **cannot** catch it + (the column type matches), so it hides in plain sight. When it sits in a shared + `BaseEntity`/`BaseAuditedEntity`, **every inheriting `id`/`tenantId`/FK is wrong** — count the + blast radius (grep every `field.string` paired with `@dbColumnType: uuid`; it is often + hundreds of fields). This is a **real finding**, not a modernization nudge: recommend + `field.uuid` and flag it as a **staged migration** (re-typing `id`/FK ripples through + repositories, finders, and call sites) — tier by blast radius, not buried as advisory. The + ONLY non-finding is a field the code genuinely handles as a *string* over a uuid column + (explicitly justified). Report the total pair count so the migration has a completion + criterion (see the CI ratchet gate in `metaobjects-verify`). + - [ ] **I. Vocabulary hygiene / modernization (ADVISORY).** Flag already-retired or deprecated authoring patterns and recommend the canonical form (see § Vocabulary 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 the array column type is retired. - The `@kind: text` hack (forcing text via a kind override) → **bare `field.string`** (text is the default; no override needed). -- `@dbColumnType: uuid` where a native UUID type is actually wanted → **`field.uuid`** - (a distinct native type is a subtype, not a physical override). Keep `@dbColumnType: - uuid` ONLY for the deliberate string-over-uuid-column case. +- `@dbColumnType: uuid_array` was covered above. **`field.string` + `@dbColumnType: uuid` + is NOT advisory — it is a real mismodeling finding (see axis H).** It generates a `String` + where the code uses/wants a native `UUID`, forcing `String↔UUID` coercions at every + boundary; `verify --db` passes (the column really is uuid), so the schema gate can't see it. + The genuine string-over-uuid-column case (code truly handles the value as text) is the ONE + legitimate use and must be explicitly justified — otherwise recommend **`field.uuid`**. - `@dbColumnType: timestamp_with_tz` (ADR-0036 Wave 2) → **drop it.** `field.timestamp` is instant / timezone-aware **by default** now; the `timestamp_with_tz` column-type override 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**. ## Guardrails +- **Adoption direction — metadata follows the code.** This is a brownfield project: existing + code and the live schema are the spec. Every `metadata_sketch` must **reproduce the code's + existing native types, names, and nullability** (model `field.uuid` where the code uses + `UUID`, carry over `@column`/`@table`/`@required`) and every cutover must **minimize churn to + code the generator is not replacing** — customize the codegen to match the existing shape + before proposing edits to working call sites. A sketch that would re-type or rename working + code the generator isn't replacing is modeling the wrong thing; when a choice is ambiguous, + flag it for the human rather than proposing the churnier option. (Full doctrine: + `metaobjects-authoring` → "Adopting onto an existing codebase".) - **Parity-gate every cutover** — prove behavior-equivalent before deleting hand-written code; generated schemas are often looser. - **Verify, don't assume** — read the code behind a grep hit. - **Verify the DB artifact, not just the types** — the contract may claim a column the view DDL dropped. diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/SKILL.md index 6138460d6..826e4ce2b 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/SKILL.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/SKILL.md @@ -60,6 +60,56 @@ aggregate — that is almost always **metadata you have not declared yet.** In o Rule of thumb: **if the metadata could describe it, declaring it is never the wrong call** — even when a one-off hand-write would be faster today. +## Adopting onto an existing codebase — metadata FOLLOWS the code + +The principle above is the **greenfield** default: declare the model, generate the +code. **Adoption reverses the direction.** When you are introducing MetaObjects into +a project that already has **working code and/or a live database** — a migration, not +a fresh start — the existing code and schema are the specification, and the metadata's +first job is to **reproduce them**. You are documenting a reality that already runs, not +redefining it. (The metadata is still the durable spine *going forward*; only the +*direction of fit on the way in* changes. Once adopted, the greenfield rules resume.) + +**The observable predicate:** does working code or a populated schema already exist for +what you're modeling? If yes, you are in adoption mode and these rules apply. + +**Author metadata to match what the code ALREADY IS — not what you'd design fresh.** +Read the existing code and schema *first*, then model to reproduce them: +- The **native types the code uses** are the spec — model `field.uuid` when the code + uses `UUID`, `field.decimal` when it uses `BigDecimal`, etc. Do **not** pick a + metadata shape whose generated type differs from the type already in use (that is the + exact mistake that turned a `UUID` column into a `String` and forced coercions across + hundreds of fields — see the UUID rule below). +- The existing **column names, table names, nullability, and field shapes** are the + spec — carry them over (`@column`, `@table`, `@required`, `@maxLength`) so the + generated schema matches the live one and `verify --db` is clean. + +**Customize the CODEGEN to match the existing code before you change the existing code.** +If generated output doesn't match the code's shape (naming, file layout, imports, +signatures), **tune the generator/template/config to reproduce it** — that is the +intended adoption path (owned generators, `outputPattern`, naming strategy — see the +`metaobjects-codegen` skill), **not a hack**. Reshaping working call sites to satisfy +the generator's defaults is the *last* resort, not the first. + +**Minimize churn to code the generator is not replacing.** The ONLY existing code that +should change is the hand-written layer codegen now **owns** (the hand-rolled +CRUD/DTO/validator/mapper you're deleting) — parity-gate it, then delete it; that is the +point of adopting. Everything else — call sites, business logic, adjacent modules — +stays untouched. **If a metadata choice would force a wide edit across code the +generator isn't replacing, treat that as a signal the metadata is modeling the wrong +thing** and re-check it against the code, rather than editing the code to fit the +metadata. + +**When a modeling choice is genuinely ambiguous, ask — don't pick the churnier option.** +If two metadata shapes both fit the existing code and they imply different amounts of +existing-code change, surface the tradeoff to the user rather than choosing silently. +**Default to the choice that changes the least existing code.** + +Do NOT: change metadata, regenerate, and then work through the resulting compile/type +errors in the existing code as if they were bugs. On an adoption those "errors" are the +metadata failing to match the code — fix the *metadata* (or the codegen customization), +not the code. + ## The fused-key encoding (non-negotiable) @@ -224,7 +274,7 @@ Canonical form for common field needs — reach for these before inventing anyth | Need | Author it as | Note | |---|---|---| -| IDs / unique keys | `field.uuid` | native UUID; use `@dbColumnType: uuid` only to force a string-typed value over a uuid column on purpose | +| IDs / unique keys / **any UUID column** | `field.uuid` | native UUID type. **NEVER `field.string` + `@dbColumnType: uuid`** — see the smell callout below | | Money | `field.currency` | integer minor units; never a float | | Closed set of symbols | `field.enum` | `@values` required | | 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 | IP address | `field.inet` | native IP type; Postgres `inet` column | | Validated plain string (email / hostname) | `field.string` + `@stringFormat` | `@stringFormat: email` or `@stringFormat: hostname` — idiomatic per-port validation; don't hand-write the `validator.regex` | +**UUID columns are `field.uuid` — `field.string` + `@dbColumnType: uuid` is a forbidden smell.** +A UUID column is modeled with the **`field.uuid`** subtype (native `UUID` / `Guid` / +`uuid.UUID`, canonical lowercase-hex on the wire). Do **not** reach for `field.string` + +`@dbColumnType: uuid`: that pairing makes the *DB column* a uuid but generates a **`String` +property in code**, so every consumer must coerce `String ↔ UUID` at every boundary. It reads +"correct" because `verify --db` passes (the column really is uuid) — the defect is invisible to +the schema gate and only shows up as wrong native types rippling through the code. Left in a +`BaseEntity`, it is inherited by every `id`/`tenantId`/FK — hundreds of fields across a repo, a +staged multi-PR migration to undo. So: + +```json +{ "field.uuid": { "name": "id" } } // ✅ native UUID +{ "field.string": { "name": "id", "@dbColumnType": "uuid" } } // ❌ generates String over a uuid column +``` + +The `field.string` + `@dbColumnType: uuid` form is legitimate **only** in the genuinely rare +case where your code truly wants a *string-typed* value stored in a uuid column (you handle the +uuid as text everywhere and never as a native UUID). That is an explicit, justified exception — +not a default, and never the way to model an identifier. When adopting an existing schema whose +code already uses `UUID`, `field.uuid` is the match-the-code choice (see "Adopting onto an +existing codebase" above). + **Timestamps — instant by default, `@localTime` for naive wall-clock (ADR-0036 Wave 2).** `field.timestamp` is **instant / timezone-aware by default** (Postgres `timestamptz`; native `Instant` / `DateTimeOffset` / aware `datetime`) — use it for created/updated/event diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md index 79a0e9d5c..6a8aee636 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -90,6 +90,22 @@ the data access too. `meta gen --list` prints every generator by stable name; the `generators` array in `metaobjects.config.ts` is where you opt each one in or out. +### Adopting onto existing code — make codegen match the code, not the code match codegen + +On a **brownfield adoption** (existing working code / live schema — see +`metaobjects-authoring` → "Adopting onto an existing codebase"), the goal of codegen is to +**reproduce the shape the code already has** so the generated output drops in with minimal +churn. When generated output doesn't match — different names, file layout, imports, or +signatures than the existing code — **customize the codegen to match the existing code first**, +using the à-la-carte layers, `outputPattern`/target layout, naming strategy, template +customization, and owned/custom generators described here. That is the intended adoption path, +**not a hack** — the whole point of owned generators + three-way merge is to shape output to +your codebase. Reshaping working call sites to fit the generator's defaults is the **last** +resort, and only for the layer codegen is actually replacing (the hand-rolled CRUD/DTO/mapper +you're deleting behind a parity gate). If matching the existing shape would require a genuinely +hacky generator contortion, that is the moment to **ask the human** which side should give — +don't silently churn the existing code. + ## Write your own generators — the built-ins rarely fit an app exactly The built-in generators (entity, queries, routes, form, grid, barrel) cover the diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/SKILL.md index 472673850..eda0ff48d 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/SKILL.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/SKILL.md @@ -70,6 +70,32 @@ and (for templates) the missing reference. **Bias toward trusting the tool** — verify failure almost always means the metadata changed and a derived artifact didn't follow. +## What `verify` can't catch — semantic mismodeling (add a CI ratchet lint) + +The three subverbs check that derived artifacts *match the metadata*. They do **not** +check that the metadata *models the right thing* — so a semantically wrong metadata +choice that is internally consistent passes clean. The canonical case: a UUID column +modeled **`field.string` + `@dbColumnType: uuid`**. The generated property is a `String`, +the DB column is genuinely `uuid`, so **`verify --db` passes** while every consumer coerces +`String↔UUID` and the native type is wrong throughout the code (see `metaobjects-authoring` +→ the UUID smell). No drift subverb can see it, because nothing has drifted — the model +itself is wrong. + +For semantic invariants like this, add a **project-local CI ratchet lint** over the +metadata sources — a grep-level gate is enough: + +``` +# fail the build if any field.string carries @dbColumnType: uuid (a UUID-column-as-string smell). +# Illustrative — tune the matcher to your source format (canonical JSON vs sigil-free YAML) and +# tighten to per-node scope if a coarse co-occurrence match is too broad for your files. +! grep -rEzl '"field\.string"[^}]*"@dbColumnType"[^}]*"uuid"' metaobjects/ +``` + +Make it a **ratchet**: it can't go green until the last offending field is migrated to +`field.uuid`, so it doubles as the migration's completion criterion **and** a permanent +backstop against reintroducing the smell. The same pattern generalizes to any semantic +metadata rule your project wants enforced that `verify` structurally can't express. + ## Schema migrations are the shared TypeScript engine — for every port This is the load-bearing architectural fact (ADR-0015): **schema migrations are diff --git a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md index 7ed1de5f6..513d86af6 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md @@ -6,6 +6,7 @@ MetaObjects is a metadata standard: typed metadata in `metaobjects/` is the dura spine; generated code is the disposable artifact. Regenerate with `mvn metaobjects:generate`. ## Principles +- **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.) - 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. - 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. - Never hand-edit generated files — change the metadata and regenerate (three-way merge preserves hand-written regions). diff --git a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md index 7ed1de5f6..513d86af6 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md @@ -6,6 +6,7 @@ MetaObjects is a metadata standard: typed metadata in `metaobjects/` is the dura spine; generated code is the disposable artifact. Regenerate with `mvn metaobjects:generate`. ## Principles +- **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.) - 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. - 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. - Never hand-edit generated files — change the metadata and regenerate (three-way merge preserves hand-written regions). diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md index 4361fc8d8..b4e08a4b3 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -109,6 +109,21 @@ code behind a grep hit; a "duplicate" validator's *divergence* is the finding. in committed canonical JSON (ADR-0032); DB-type-as-logical-subtype (ADR-0013); per-port migration engine where schema is Node-`meta`-owned (ADR-0015). +- [ ] **H2. Wrong native type — `field.` + `@dbColumnType` that hides the real type + (CORRECTNESS-ADJACENT finding, NOT advisory axis-I).** The headline instance is a **UUID + column modeled `field.string` + `@dbColumnType: uuid`**: the DB column is uuid but the + generated property is a **`String`**, so the code coerces `String↔UUID` at every boundary + and the native type is wrong everywhere the field is used. `verify --db` **cannot** catch it + (the column type matches), so it hides in plain sight. When it sits in a shared + `BaseEntity`/`BaseAuditedEntity`, **every inheriting `id`/`tenantId`/FK is wrong** — count the + blast radius (grep every `field.string` paired with `@dbColumnType: uuid`; it is often + hundreds of fields). This is a **real finding**, not a modernization nudge: recommend + `field.uuid` and flag it as a **staged migration** (re-typing `id`/FK ripples through + repositories, finders, and call sites) — tier by blast radius, not buried as advisory. The + ONLY non-finding is a field the code genuinely handles as a *string* over a uuid column + (explicitly justified). Report the total pair count so the migration has a completion + criterion (see the CI ratchet gate in `metaobjects-verify`). + - [ ] **I. Vocabulary hygiene / modernization (ADVISORY).** Flag already-retired or deprecated authoring patterns and recommend the canonical form (see § Vocabulary 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 the array column type is retired. - The `@kind: text` hack (forcing text via a kind override) → **bare `field.string`** (text is the default; no override needed). -- `@dbColumnType: uuid` where a native UUID type is actually wanted → **`field.uuid`** - (a distinct native type is a subtype, not a physical override). Keep `@dbColumnType: - uuid` ONLY for the deliberate string-over-uuid-column case. +- `@dbColumnType: uuid_array` was covered above. **`field.string` + `@dbColumnType: uuid` + is NOT advisory — it is a real mismodeling finding (see axis H).** It generates a `String` + where the code uses/wants a native `UUID`, forcing `String↔UUID` coercions at every + boundary; `verify --db` passes (the column really is uuid), so the schema gate can't see it. + The genuine string-over-uuid-column case (code truly handles the value as text) is the ONE + legitimate use and must be explicitly justified — otherwise recommend **`field.uuid`**. - `@dbColumnType: timestamp_with_tz` (ADR-0036 Wave 2) → **drop it.** `field.timestamp` is instant / timezone-aware **by default** now; the `timestamp_with_tz` column-type override 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**. ## Guardrails +- **Adoption direction — metadata follows the code.** This is a brownfield project: existing + code and the live schema are the spec. Every `metadata_sketch` must **reproduce the code's + existing native types, names, and nullability** (model `field.uuid` where the code uses + `UUID`, carry over `@column`/`@table`/`@required`) and every cutover must **minimize churn to + code the generator is not replacing** — customize the codegen to match the existing shape + before proposing edits to working call sites. A sketch that would re-type or rename working + code the generator isn't replacing is modeling the wrong thing; when a choice is ambiguous, + flag it for the human rather than proposing the churnier option. (Full doctrine: + `metaobjects-authoring` → "Adopting onto an existing codebase".) - **Parity-gate every cutover** — prove behavior-equivalent before deleting hand-written code; generated schemas are often looser. - **Verify, don't assume** — read the code behind a grep hit. - **Verify the DB artifact, not just the types** — the contract may claim a column the view DDL dropped. diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/SKILL.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/SKILL.md index 6138460d6..826e4ce2b 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/SKILL.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/SKILL.md @@ -60,6 +60,56 @@ aggregate — that is almost always **metadata you have not declared yet.** In o Rule of thumb: **if the metadata could describe it, declaring it is never the wrong call** — even when a one-off hand-write would be faster today. +## Adopting onto an existing codebase — metadata FOLLOWS the code + +The principle above is the **greenfield** default: declare the model, generate the +code. **Adoption reverses the direction.** When you are introducing MetaObjects into +a project that already has **working code and/or a live database** — a migration, not +a fresh start — the existing code and schema are the specification, and the metadata's +first job is to **reproduce them**. You are documenting a reality that already runs, not +redefining it. (The metadata is still the durable spine *going forward*; only the +*direction of fit on the way in* changes. Once adopted, the greenfield rules resume.) + +**The observable predicate:** does working code or a populated schema already exist for +what you're modeling? If yes, you are in adoption mode and these rules apply. + +**Author metadata to match what the code ALREADY IS — not what you'd design fresh.** +Read the existing code and schema *first*, then model to reproduce them: +- The **native types the code uses** are the spec — model `field.uuid` when the code + uses `UUID`, `field.decimal` when it uses `BigDecimal`, etc. Do **not** pick a + metadata shape whose generated type differs from the type already in use (that is the + exact mistake that turned a `UUID` column into a `String` and forced coercions across + hundreds of fields — see the UUID rule below). +- The existing **column names, table names, nullability, and field shapes** are the + spec — carry them over (`@column`, `@table`, `@required`, `@maxLength`) so the + generated schema matches the live one and `verify --db` is clean. + +**Customize the CODEGEN to match the existing code before you change the existing code.** +If generated output doesn't match the code's shape (naming, file layout, imports, +signatures), **tune the generator/template/config to reproduce it** — that is the +intended adoption path (owned generators, `outputPattern`, naming strategy — see the +`metaobjects-codegen` skill), **not a hack**. Reshaping working call sites to satisfy +the generator's defaults is the *last* resort, not the first. + +**Minimize churn to code the generator is not replacing.** The ONLY existing code that +should change is the hand-written layer codegen now **owns** (the hand-rolled +CRUD/DTO/validator/mapper you're deleting) — parity-gate it, then delete it; that is the +point of adopting. Everything else — call sites, business logic, adjacent modules — +stays untouched. **If a metadata choice would force a wide edit across code the +generator isn't replacing, treat that as a signal the metadata is modeling the wrong +thing** and re-check it against the code, rather than editing the code to fit the +metadata. + +**When a modeling choice is genuinely ambiguous, ask — don't pick the churnier option.** +If two metadata shapes both fit the existing code and they imply different amounts of +existing-code change, surface the tradeoff to the user rather than choosing silently. +**Default to the choice that changes the least existing code.** + +Do NOT: change metadata, regenerate, and then work through the resulting compile/type +errors in the existing code as if they were bugs. On an adoption those "errors" are the +metadata failing to match the code — fix the *metadata* (or the codegen customization), +not the code. + ## The fused-key encoding (non-negotiable) @@ -224,7 +274,7 @@ Canonical form for common field needs — reach for these before inventing anyth | Need | Author it as | Note | |---|---|---| -| IDs / unique keys | `field.uuid` | native UUID; use `@dbColumnType: uuid` only to force a string-typed value over a uuid column on purpose | +| IDs / unique keys / **any UUID column** | `field.uuid` | native UUID type. **NEVER `field.string` + `@dbColumnType: uuid`** — see the smell callout below | | Money | `field.currency` | integer minor units; never a float | | Closed set of symbols | `field.enum` | `@values` required | | 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 | IP address | `field.inet` | native IP type; Postgres `inet` column | | Validated plain string (email / hostname) | `field.string` + `@stringFormat` | `@stringFormat: email` or `@stringFormat: hostname` — idiomatic per-port validation; don't hand-write the `validator.regex` | +**UUID columns are `field.uuid` — `field.string` + `@dbColumnType: uuid` is a forbidden smell.** +A UUID column is modeled with the **`field.uuid`** subtype (native `UUID` / `Guid` / +`uuid.UUID`, canonical lowercase-hex on the wire). Do **not** reach for `field.string` + +`@dbColumnType: uuid`: that pairing makes the *DB column* a uuid but generates a **`String` +property in code**, so every consumer must coerce `String ↔ UUID` at every boundary. It reads +"correct" because `verify --db` passes (the column really is uuid) — the defect is invisible to +the schema gate and only shows up as wrong native types rippling through the code. Left in a +`BaseEntity`, it is inherited by every `id`/`tenantId`/FK — hundreds of fields across a repo, a +staged multi-PR migration to undo. So: + +```json +{ "field.uuid": { "name": "id" } } // ✅ native UUID +{ "field.string": { "name": "id", "@dbColumnType": "uuid" } } // ❌ generates String over a uuid column +``` + +The `field.string` + `@dbColumnType: uuid` form is legitimate **only** in the genuinely rare +case where your code truly wants a *string-typed* value stored in a uuid column (you handle the +uuid as text everywhere and never as a native UUID). That is an explicit, justified exception — +not a default, and never the way to model an identifier. When adopting an existing schema whose +code already uses `UUID`, `field.uuid` is the match-the-code choice (see "Adopting onto an +existing codebase" above). + **Timestamps — instant by default, `@localTime` for naive wall-clock (ADR-0036 Wave 2).** `field.timestamp` is **instant / timezone-aware by default** (Postgres `timestamptz`; native `Instant` / `DateTimeOffset` / aware `datetime`) — use it for created/updated/event diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md index 79a0e9d5c..6a8aee636 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -90,6 +90,22 @@ the data access too. `meta gen --list` prints every generator by stable name; the `generators` array in `metaobjects.config.ts` is where you opt each one in or out. +### Adopting onto existing code — make codegen match the code, not the code match codegen + +On a **brownfield adoption** (existing working code / live schema — see +`metaobjects-authoring` → "Adopting onto an existing codebase"), the goal of codegen is to +**reproduce the shape the code already has** so the generated output drops in with minimal +churn. When generated output doesn't match — different names, file layout, imports, or +signatures than the existing code — **customize the codegen to match the existing code first**, +using the à-la-carte layers, `outputPattern`/target layout, naming strategy, template +customization, and owned/custom generators described here. That is the intended adoption path, +**not a hack** — the whole point of owned generators + three-way merge is to shape output to +your codebase. Reshaping working call sites to fit the generator's defaults is the **last** +resort, and only for the layer codegen is actually replacing (the hand-rolled CRUD/DTO/mapper +you're deleting behind a parity gate). If matching the existing shape would require a genuinely +hacky generator contortion, that is the moment to **ask the human** which side should give — +don't silently churn the existing code. + ## Write your own generators — the built-ins rarely fit an app exactly The built-in generators (entity, queries, routes, form, grid, barrel) cover the diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-verify/SKILL.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-verify/SKILL.md index 472673850..eda0ff48d 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-verify/SKILL.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-verify/SKILL.md @@ -70,6 +70,32 @@ and (for templates) the missing reference. **Bias toward trusting the tool** — verify failure almost always means the metadata changed and a derived artifact didn't follow. +## What `verify` can't catch — semantic mismodeling (add a CI ratchet lint) + +The three subverbs check that derived artifacts *match the metadata*. They do **not** +check that the metadata *models the right thing* — so a semantically wrong metadata +choice that is internally consistent passes clean. The canonical case: a UUID column +modeled **`field.string` + `@dbColumnType: uuid`**. The generated property is a `String`, +the DB column is genuinely `uuid`, so **`verify --db` passes** while every consumer coerces +`String↔UUID` and the native type is wrong throughout the code (see `metaobjects-authoring` +→ the UUID smell). No drift subverb can see it, because nothing has drifted — the model +itself is wrong. + +For semantic invariants like this, add a **project-local CI ratchet lint** over the +metadata sources — a grep-level gate is enough: + +``` +# fail the build if any field.string carries @dbColumnType: uuid (a UUID-column-as-string smell). +# Illustrative — tune the matcher to your source format (canonical JSON vs sigil-free YAML) and +# tighten to per-node scope if a coarse co-occurrence match is too broad for your files. +! grep -rEzl '"field\.string"[^}]*"@dbColumnType"[^}]*"uuid"' metaobjects/ +``` + +Make it a **ratchet**: it can't go green until the last offending field is migrated to +`field.uuid`, so it doubles as the migration's completion criterion **and** a permanent +backstop against reintroducing the smell. The same pattern generalizes to any semantic +metadata rule your project wants enforced that `verify` structurally can't express. + ## Schema migrations are the shared TypeScript engine — for every port This is the load-bearing architectural fact (ADR-0015): **schema migrations are diff --git a/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md index 4dd7aacf0..97f4b380f 100644 --- a/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md @@ -6,6 +6,7 @@ MetaObjects is a metadata standard: typed metadata in `metaobjects/` is the dura spine; generated code is the disposable artifact. Regenerate with `metaobjects gen`. ## Principles +- **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.) - 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. - 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. - Never hand-edit generated files — change the metadata and regenerate (three-way merge preserves hand-written regions). diff --git a/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md index 4dd7aacf0..97f4b380f 100644 --- a/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md @@ -6,6 +6,7 @@ MetaObjects is a metadata standard: typed metadata in `metaobjects/` is the dura spine; generated code is the disposable artifact. Regenerate with `metaobjects gen`. ## Principles +- **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.) - 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. - 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. - Never hand-edit generated files — change the metadata and regenerate (three-way merge preserves hand-written regions). diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md index 4361fc8d8..b4e08a4b3 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md @@ -109,6 +109,21 @@ code behind a grep hit; a "duplicate" validator's *divergence* is the finding. in committed canonical JSON (ADR-0032); DB-type-as-logical-subtype (ADR-0013); per-port migration engine where schema is Node-`meta`-owned (ADR-0015). +- [ ] **H2. Wrong native type — `field.` + `@dbColumnType` that hides the real type + (CORRECTNESS-ADJACENT finding, NOT advisory axis-I).** The headline instance is a **UUID + column modeled `field.string` + `@dbColumnType: uuid`**: the DB column is uuid but the + generated property is a **`String`**, so the code coerces `String↔UUID` at every boundary + and the native type is wrong everywhere the field is used. `verify --db` **cannot** catch it + (the column type matches), so it hides in plain sight. When it sits in a shared + `BaseEntity`/`BaseAuditedEntity`, **every inheriting `id`/`tenantId`/FK is wrong** — count the + blast radius (grep every `field.string` paired with `@dbColumnType: uuid`; it is often + hundreds of fields). This is a **real finding**, not a modernization nudge: recommend + `field.uuid` and flag it as a **staged migration** (re-typing `id`/FK ripples through + repositories, finders, and call sites) — tier by blast radius, not buried as advisory. The + ONLY non-finding is a field the code genuinely handles as a *string* over a uuid column + (explicitly justified). Report the total pair count so the migration has a completion + criterion (see the CI ratchet gate in `metaobjects-verify`). + - [ ] **I. Vocabulary hygiene / modernization (ADVISORY).** Flag already-retired or deprecated authoring patterns and recommend the canonical form (see § Vocabulary 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 the array column type is retired. - The `@kind: text` hack (forcing text via a kind override) → **bare `field.string`** (text is the default; no override needed). -- `@dbColumnType: uuid` where a native UUID type is actually wanted → **`field.uuid`** - (a distinct native type is a subtype, not a physical override). Keep `@dbColumnType: - uuid` ONLY for the deliberate string-over-uuid-column case. +- `@dbColumnType: uuid_array` was covered above. **`field.string` + `@dbColumnType: uuid` + is NOT advisory — it is a real mismodeling finding (see axis H).** It generates a `String` + where the code uses/wants a native `UUID`, forcing `String↔UUID` coercions at every + boundary; `verify --db` passes (the column really is uuid), so the schema gate can't see it. + The genuine string-over-uuid-column case (code truly handles the value as text) is the ONE + legitimate use and must be explicitly justified — otherwise recommend **`field.uuid`**. - `@dbColumnType: timestamp_with_tz` (ADR-0036 Wave 2) → **drop it.** `field.timestamp` is instant / timezone-aware **by default** now; the `timestamp_with_tz` column-type override 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**. ## Guardrails +- **Adoption direction — metadata follows the code.** This is a brownfield project: existing + code and the live schema are the spec. Every `metadata_sketch` must **reproduce the code's + existing native types, names, and nullability** (model `field.uuid` where the code uses + `UUID`, carry over `@column`/`@table`/`@required`) and every cutover must **minimize churn to + code the generator is not replacing** — customize the codegen to match the existing shape + before proposing edits to working call sites. A sketch that would re-type or rename working + code the generator isn't replacing is modeling the wrong thing; when a choice is ambiguous, + flag it for the human rather than proposing the churnier option. (Full doctrine: + `metaobjects-authoring` → "Adopting onto an existing codebase".) - **Parity-gate every cutover** — prove behavior-equivalent before deleting hand-written code; generated schemas are often looser. - **Verify, don't assume** — read the code behind a grep hit. - **Verify the DB artifact, not just the types** — the contract may claim a column the view DDL dropped. diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md index 6138460d6..826e4ce2b 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md @@ -60,6 +60,56 @@ aggregate — that is almost always **metadata you have not declared yet.** In o Rule of thumb: **if the metadata could describe it, declaring it is never the wrong call** — even when a one-off hand-write would be faster today. +## Adopting onto an existing codebase — metadata FOLLOWS the code + +The principle above is the **greenfield** default: declare the model, generate the +code. **Adoption reverses the direction.** When you are introducing MetaObjects into +a project that already has **working code and/or a live database** — a migration, not +a fresh start — the existing code and schema are the specification, and the metadata's +first job is to **reproduce them**. You are documenting a reality that already runs, not +redefining it. (The metadata is still the durable spine *going forward*; only the +*direction of fit on the way in* changes. Once adopted, the greenfield rules resume.) + +**The observable predicate:** does working code or a populated schema already exist for +what you're modeling? If yes, you are in adoption mode and these rules apply. + +**Author metadata to match what the code ALREADY IS — not what you'd design fresh.** +Read the existing code and schema *first*, then model to reproduce them: +- The **native types the code uses** are the spec — model `field.uuid` when the code + uses `UUID`, `field.decimal` when it uses `BigDecimal`, etc. Do **not** pick a + metadata shape whose generated type differs from the type already in use (that is the + exact mistake that turned a `UUID` column into a `String` and forced coercions across + hundreds of fields — see the UUID rule below). +- The existing **column names, table names, nullability, and field shapes** are the + spec — carry them over (`@column`, `@table`, `@required`, `@maxLength`) so the + generated schema matches the live one and `verify --db` is clean. + +**Customize the CODEGEN to match the existing code before you change the existing code.** +If generated output doesn't match the code's shape (naming, file layout, imports, +signatures), **tune the generator/template/config to reproduce it** — that is the +intended adoption path (owned generators, `outputPattern`, naming strategy — see the +`metaobjects-codegen` skill), **not a hack**. Reshaping working call sites to satisfy +the generator's defaults is the *last* resort, not the first. + +**Minimize churn to code the generator is not replacing.** The ONLY existing code that +should change is the hand-written layer codegen now **owns** (the hand-rolled +CRUD/DTO/validator/mapper you're deleting) — parity-gate it, then delete it; that is the +point of adopting. Everything else — call sites, business logic, adjacent modules — +stays untouched. **If a metadata choice would force a wide edit across code the +generator isn't replacing, treat that as a signal the metadata is modeling the wrong +thing** and re-check it against the code, rather than editing the code to fit the +metadata. + +**When a modeling choice is genuinely ambiguous, ask — don't pick the churnier option.** +If two metadata shapes both fit the existing code and they imply different amounts of +existing-code change, surface the tradeoff to the user rather than choosing silently. +**Default to the choice that changes the least existing code.** + +Do NOT: change metadata, regenerate, and then work through the resulting compile/type +errors in the existing code as if they were bugs. On an adoption those "errors" are the +metadata failing to match the code — fix the *metadata* (or the codegen customization), +not the code. + ## The fused-key encoding (non-negotiable) @@ -224,7 +274,7 @@ Canonical form for common field needs — reach for these before inventing anyth | Need | Author it as | Note | |---|---|---| -| IDs / unique keys | `field.uuid` | native UUID; use `@dbColumnType: uuid` only to force a string-typed value over a uuid column on purpose | +| IDs / unique keys / **any UUID column** | `field.uuid` | native UUID type. **NEVER `field.string` + `@dbColumnType: uuid`** — see the smell callout below | | Money | `field.currency` | integer minor units; never a float | | Closed set of symbols | `field.enum` | `@values` required | | 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 | IP address | `field.inet` | native IP type; Postgres `inet` column | | Validated plain string (email / hostname) | `field.string` + `@stringFormat` | `@stringFormat: email` or `@stringFormat: hostname` — idiomatic per-port validation; don't hand-write the `validator.regex` | +**UUID columns are `field.uuid` — `field.string` + `@dbColumnType: uuid` is a forbidden smell.** +A UUID column is modeled with the **`field.uuid`** subtype (native `UUID` / `Guid` / +`uuid.UUID`, canonical lowercase-hex on the wire). Do **not** reach for `field.string` + +`@dbColumnType: uuid`: that pairing makes the *DB column* a uuid but generates a **`String` +property in code**, so every consumer must coerce `String ↔ UUID` at every boundary. It reads +"correct" because `verify --db` passes (the column really is uuid) — the defect is invisible to +the schema gate and only shows up as wrong native types rippling through the code. Left in a +`BaseEntity`, it is inherited by every `id`/`tenantId`/FK — hundreds of fields across a repo, a +staged multi-PR migration to undo. So: + +```json +{ "field.uuid": { "name": "id" } } // ✅ native UUID +{ "field.string": { "name": "id", "@dbColumnType": "uuid" } } // ❌ generates String over a uuid column +``` + +The `field.string` + `@dbColumnType: uuid` form is legitimate **only** in the genuinely rare +case where your code truly wants a *string-typed* value stored in a uuid column (you handle the +uuid as text everywhere and never as a native UUID). That is an explicit, justified exception — +not a default, and never the way to model an identifier. When adopting an existing schema whose +code already uses `UUID`, `field.uuid` is the match-the-code choice (see "Adopting onto an +existing codebase" above). + **Timestamps — instant by default, `@localTime` for naive wall-clock (ADR-0036 Wave 2).** `field.timestamp` is **instant / timezone-aware by default** (Postgres `timestamptz`; native `Instant` / `DateTimeOffset` / aware `datetime`) — use it for created/updated/event diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md index 79a0e9d5c..6a8aee636 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -90,6 +90,22 @@ the data access too. `meta gen --list` prints every generator by stable name; the `generators` array in `metaobjects.config.ts` is where you opt each one in or out. +### Adopting onto existing code — make codegen match the code, not the code match codegen + +On a **brownfield adoption** (existing working code / live schema — see +`metaobjects-authoring` → "Adopting onto an existing codebase"), the goal of codegen is to +**reproduce the shape the code already has** so the generated output drops in with minimal +churn. When generated output doesn't match — different names, file layout, imports, or +signatures than the existing code — **customize the codegen to match the existing code first**, +using the à-la-carte layers, `outputPattern`/target layout, naming strategy, template +customization, and owned/custom generators described here. That is the intended adoption path, +**not a hack** — the whole point of owned generators + three-way merge is to shape output to +your codebase. Reshaping working call sites to fit the generator's defaults is the **last** +resort, and only for the layer codegen is actually replacing (the hand-rolled CRUD/DTO/mapper +you're deleting behind a parity gate). If matching the existing shape would require a genuinely +hacky generator contortion, that is the moment to **ask the human** which side should give — +don't silently churn the existing code. + ## Write your own generators — the built-ins rarely fit an app exactly The built-in generators (entity, queries, routes, form, grid, barrel) cover the diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-verify/SKILL.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-verify/SKILL.md index 472673850..eda0ff48d 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-verify/SKILL.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-verify/SKILL.md @@ -70,6 +70,32 @@ and (for templates) the missing reference. **Bias toward trusting the tool** — verify failure almost always means the metadata changed and a derived artifact didn't follow. +## What `verify` can't catch — semantic mismodeling (add a CI ratchet lint) + +The three subverbs check that derived artifacts *match the metadata*. They do **not** +check that the metadata *models the right thing* — so a semantically wrong metadata +choice that is internally consistent passes clean. The canonical case: a UUID column +modeled **`field.string` + `@dbColumnType: uuid`**. The generated property is a `String`, +the DB column is genuinely `uuid`, so **`verify --db` passes** while every consumer coerces +`String↔UUID` and the native type is wrong throughout the code (see `metaobjects-authoring` +→ the UUID smell). No drift subverb can see it, because nothing has drifted — the model +itself is wrong. + +For semantic invariants like this, add a **project-local CI ratchet lint** over the +metadata sources — a grep-level gate is enough: + +``` +# fail the build if any field.string carries @dbColumnType: uuid (a UUID-column-as-string smell). +# Illustrative — tune the matcher to your source format (canonical JSON vs sigil-free YAML) and +# tighten to per-node scope if a coarse co-occurrence match is too broad for your files. +! grep -rEzl '"field\.string"[^}]*"@dbColumnType"[^}]*"uuid"' metaobjects/ +``` + +Make it a **ratchet**: it can't go green until the last offending field is migrated to +`field.uuid`, so it doubles as the migration's completion criterion **and** a permanent +backstop against reintroducing the smell. The same pattern generalizes to any semantic +metadata rule your project wants enforced that `verify` structurally can't express. + ## Schema migrations are the shared TypeScript engine — for every port This is the load-bearing architectural fact (ADR-0015): **schema migrations are diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md index 25fe07b49..0428279cc 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md @@ -6,6 +6,7 @@ MetaObjects is a metadata standard: typed metadata in `metaobjects/` is the dura spine; generated code is the disposable artifact. Regenerate with `npx meta gen`. ## Principles +- **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.) - 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. - 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. - Never hand-edit generated files — change the metadata and regenerate (three-way merge preserves hand-written regions). diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md index 25fe07b49..0428279cc 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md @@ -6,6 +6,7 @@ MetaObjects is a metadata standard: typed metadata in `metaobjects/` is the dura spine; generated code is the disposable artifact. Regenerate with `npx meta gen`. ## Principles +- **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.) - 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. - 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. - Never hand-edit generated files — change the metadata and regenerate (three-way merge preserves hand-written regions).