Skip to content

Commit 2a1190e

Browse files
dmealingclaude
andauthored
docs(agent-context): propagate ADR-0037 vocabulary-expansion framework into adopter surfaces (#125)
Translate the metamodel vocabulary-expansion decision framework (ADR-0037) into the three adopter-facing agent-context surfaces so every app using MetaObjects makes consistent subtype-vs-attribute-vs-@dbColumnType authoring calls. - metaobjects-authoring SKILL: new "Choosing the right field shape" section — the ordered test (derivable -> derive; physical-only -> @dbColumnType; logical: different native type -> subtype, same kind + modifier -> attribute) + a canonical-form table for common needs (uuid/currency/enum/timestamp/isArray/string/object/jsonb) + a custom-provider "apply ADR-0037" pointer. - metaobjects-codegen SKILL: brief reference to the authoring guidance + ADR-0037 pointer in the dialect/column-mapping section ("author the shape right, don't hack the template"). - metaobjects-audit SKILL: new advisory axis I "Vocabulary hygiene / modernization" — flags retired forms (uuid_array/text_array -> isArray, @kind:text hack -> bare field.string, @dbColumnType:uuid -> field.uuid) and checks custom-provider vocab against ADR-0037; non-failing, scored as modernization opportunities; carries a clearly-marked Wave-2/3 TODO list. Grounding-safe: examples use only currently-registered vocab. Wave-2/3 spots are marked with TODO(ADR-0036 Wave 2/3) comments. The planned @localTime/@relationName attrs the audit skill names in its forward TODO are added to the grounding test's deliberately- named exemption set. Regenerated the 12 agent-context conformance fixtures (4 stacks x 3 skills). All agent-context conformance + drift + capability-grounding tests green (13/13). Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n Co-authored-by: Doug Mealing <noreply@anthropic.com>
1 parent 9213b6c commit 2a1190e

16 files changed

Lines changed: 465 additions & 0 deletions

File tree

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,53 @@ code behind a grep hit; a "duplicate" validator's *divergence* is the finding.
8080
in committed canonical JSON (ADR-0032); DB-type-as-logical-subtype (ADR-0013); per-port
8181
migration engine where schema is Node-`meta`-owned (ADR-0015).
8282

83+
- [ ] **I. Vocabulary hygiene / modernization (ADVISORY).** Flag already-retired or
84+
deprecated authoring patterns and recommend the canonical form (see § Vocabulary
85+
hygiene). Advisory severity — scored as modernization opportunities, **never a
86+
failing finding**.
87+
8388
**Phase 4 — Synthesize** into the tiered roadmap and populate both artifacts.
8489

8590
---
8691

92+
## Vocabulary hygiene / modernization (axis I — ADVISORY)
93+
94+
Per ADR-0037, vocabulary expansion follows ONE ordered test (derivable → derive;
95+
physical-only → `@dbColumnType`; logical: different native type → subtype, same kind +
96+
modifier → attribute). This axis surfaces authoring that predates or contradicts that
97+
framework. **All findings here are advisory** — modernization opportunities scored as
98+
such, surfaced in the roadmap, but **non-failing** (the code works; the form is dated).
99+
100+
**Already-retired / deprecated forms → recommend the canonical form:**
101+
102+
- `@dbColumnType: uuid_array` / `@dbColumnType: text_array` (a physical array column
103+
type) → **`isArray: true`** on the base subtype. Array-ness is logical and derivable;
104+
the array column type is retired.
105+
- The `@kind: text` hack (forcing text via a kind override) → **bare `field.string`**
106+
(text is the default; no override needed).
107+
- `@dbColumnType: uuid` where a native UUID type is actually wanted → **`field.uuid`**
108+
(a distinct native type is a subtype, not a physical override). Keep `@dbColumnType:
109+
uuid` ONLY for the deliberate string-over-uuid-column case.
110+
111+
**Custom-provider vocabulary (adopters who register their own types/attrs):** check
112+
new/custom vocab against the ADR-0037 procedure (advisory) — e.g. *a custom subtype
113+
that differs from an existing one only by a property should be an attribute, not a
114+
subtype*; a string-shape validation (email/url) is an attribute (native type unchanged),
115+
not a subtype. Recommend re-shaping against the ordered test.
116+
117+
**TODO — Wave-2/3 checks to add when the conventions merge (NOT yet active — these
118+
tokens are pending ADR-0036 Wave 2/3 and not yet registered; do NOT flag adopters for
119+
them until merged):**
120+
121+
- `@dbColumnType: timestamp_with_tz`**drop it** (timezone-aware is the
122+
`field.timestamp` default) — *pending Wave 2/3*.
123+
- A bare wall-clock `field.timestamp` meaning naive/local time → flag for the
124+
forthcoming `@localTime` zone-flag attribute — *pending Wave 2/3, not yet active*.
125+
- An author-supplied regex validating email / url shape → flag for the forthcoming
126+
`@format` string-format attribute — *pending Wave 2/3, not yet active*.
127+
128+
---
129+
87130
## Classification scheme (every surface; classify on codegen AND runtime)
88131

89132
| Class | Meaning | Action |
@@ -183,6 +226,10 @@ one-consumer need, read it codegen-locally.
183226
Coarse bands only (none / some / most / all). Worst-of within a pillar. On re-run, grade
184227
the delta. Lead with gaps, not the grade.
185228

229+
**Vocabulary hygiene (axis I) is advisory** — it surfaces as modernization
230+
opportunities in the roadmap, scored as such, and **never gates a tier or fails the
231+
audit.** Dated-but-working vocabulary is a quality nudge, not a defect.
232+
186233
---
187234

188235
## Report

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,42 @@ name package extends abstract overlay isArray children value
161161
Common field attributes: `@required`, `@maxLength`, `@column` (physical column
162162
name), `@default`, `@filterable`, `@sortable`.
163163

164+
### Choosing the right field shape
165+
166+
When a need doesn't obviously map to a subtype, run this ordered test (ADR-0037) —
167+
**the first that matches decides:**
168+
169+
1. **Derivable from what you already have?** → derive it, declare NOTHING new. An
170+
array is `isArray: true` (never an array column type); a bounded string is
171+
`@maxLength`; FK columns come from `identity.reference`. Don't add vocabulary for
172+
something the metadata already implies.
173+
2. **Pure physical-DB detail, native type unchanged?** → the narrow `@dbColumnType`
174+
escape hatch (e.g. an open JSON bag). Used sparingly; not a logical type.
175+
3. **Logical concept** — a *different kind* of value (its own native type, e.g.
176+
UUID, money) → a **subtype**; the *same kind* with a modifier (still a string/
177+
decimal, just constrained) → an **attribute** (`@maxLength`, `@precision`).
178+
179+
Canonical form for common needs — reach for these before inventing anything:
180+
181+
| Need | Author it as | Note |
182+
|---|---|---|
183+
| IDs / unique keys | `field.uuid` | native UUID; use `@dbColumnType: uuid` only to force a string-typed value over a uuid column on purpose |
184+
| Money | `field.currency` | integer minor units; never a float |
185+
| Closed set of symbols | `field.enum` | `@values` required |
186+
| Instant / timestamp | `field.timestamp` | ISO 8601 with timezone on the wire |
187+
| A list of anything | `isArray: true` | on the base subtype (e.g. `field.string` + `isArray`) — there is **no** array `@dbColumnType` (retired) |
188+
| Long / unbounded text | bare `field.string` | add `@maxLength` only when you want `varchar(N)` |
189+
| Nested structured value | `field.object` | `@objectRef` + `@storage` |
190+
| Open JSON bag (no fixed shape) | `field.string` + `@dbColumnType: jsonb` | logical type stays string; column is jsonb |
191+
192+
<!-- TODO(ADR-0036 Wave 2/3): add @localTime / @format / @relationName guidance when merged -->
193+
194+
**Extending the metamodel (custom providers):** the same ordered test governs new
195+
vocabulary you register. A would-be subtype that differs from an existing one only
196+
by a property is an **attribute**, not a subtype; a string-shape validation
197+
(email/url) is an attribute, not a subtype (its native type is still `string`).
198+
Apply ADR-0037 before registering anything.
199+
164200
### Currency
165201

166202
`field.currency` stores money as **integer minor units** (cents for USD, yen for

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,15 @@ dialect's column types deterministically (`field.string` + `@maxLength` →
157157
`varchar(N)`, `field.currency` → integer, `field.uuid` → native `uuid` on
158158
Postgres, `field.enum``varchar` + `CHECK`, etc.).
159159

160+
Codegen only ever maps the **shapes you authored** — so author them right. If you
161+
find the generator emitting the wrong column type, the fix is the field shape, not a
162+
template hack. See "Choosing the right field shape" in the **`metaobjects-authoring`**
163+
skill for the ordered subtype-vs-attribute-vs-`@dbColumnType` test (ADR-0037) — e.g.
164+
arrays are `isArray: true` (never an array column type) and a native UUID is
165+
`field.uuid` (not a string + `@dbColumnType`). When you register custom vocabulary
166+
for a custom generator, the same ADR-0037 procedure decides whether it's a subtype or
167+
an attribute.
168+
160169
## Per-target output
161170

162171
Generated code can be routed to **multiple output directories/packages** so each

fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,53 @@ code behind a grep hit; a "duplicate" validator's *divergence* is the finding.
8080
in committed canonical JSON (ADR-0032); DB-type-as-logical-subtype (ADR-0013); per-port
8181
migration engine where schema is Node-`meta`-owned (ADR-0015).
8282

83+
- [ ] **I. Vocabulary hygiene / modernization (ADVISORY).** Flag already-retired or
84+
deprecated authoring patterns and recommend the canonical form (see § Vocabulary
85+
hygiene). Advisory severity — scored as modernization opportunities, **never a
86+
failing finding**.
87+
8388
**Phase 4 — Synthesize** into the tiered roadmap and populate both artifacts.
8489

8590
---
8691

92+
## Vocabulary hygiene / modernization (axis I — ADVISORY)
93+
94+
Per ADR-0037, vocabulary expansion follows ONE ordered test (derivable → derive;
95+
physical-only → `@dbColumnType`; logical: different native type → subtype, same kind +
96+
modifier → attribute). This axis surfaces authoring that predates or contradicts that
97+
framework. **All findings here are advisory** — modernization opportunities scored as
98+
such, surfaced in the roadmap, but **non-failing** (the code works; the form is dated).
99+
100+
**Already-retired / deprecated forms → recommend the canonical form:**
101+
102+
- `@dbColumnType: uuid_array` / `@dbColumnType: text_array` (a physical array column
103+
type) → **`isArray: true`** on the base subtype. Array-ness is logical and derivable;
104+
the array column type is retired.
105+
- The `@kind: text` hack (forcing text via a kind override) → **bare `field.string`**
106+
(text is the default; no override needed).
107+
- `@dbColumnType: uuid` where a native UUID type is actually wanted → **`field.uuid`**
108+
(a distinct native type is a subtype, not a physical override). Keep `@dbColumnType:
109+
uuid` ONLY for the deliberate string-over-uuid-column case.
110+
111+
**Custom-provider vocabulary (adopters who register their own types/attrs):** check
112+
new/custom vocab against the ADR-0037 procedure (advisory) — e.g. *a custom subtype
113+
that differs from an existing one only by a property should be an attribute, not a
114+
subtype*; a string-shape validation (email/url) is an attribute (native type unchanged),
115+
not a subtype. Recommend re-shaping against the ordered test.
116+
117+
**TODO — Wave-2/3 checks to add when the conventions merge (NOT yet active — these
118+
tokens are pending ADR-0036 Wave 2/3 and not yet registered; do NOT flag adopters for
119+
them until merged):**
120+
121+
- `@dbColumnType: timestamp_with_tz`**drop it** (timezone-aware is the
122+
`field.timestamp` default) — *pending Wave 2/3*.
123+
- A bare wall-clock `field.timestamp` meaning naive/local time → flag for the
124+
forthcoming `@localTime` zone-flag attribute — *pending Wave 2/3, not yet active*.
125+
- An author-supplied regex validating email / url shape → flag for the forthcoming
126+
`@format` string-format attribute — *pending Wave 2/3, not yet active*.
127+
128+
---
129+
87130
## Classification scheme (every surface; classify on codegen AND runtime)
88131

89132
| Class | Meaning | Action |
@@ -183,6 +226,10 @@ one-consumer need, read it codegen-locally.
183226
Coarse bands only (none / some / most / all). Worst-of within a pillar. On re-run, grade
184227
the delta. Lead with gaps, not the grade.
185228

229+
**Vocabulary hygiene (axis I) is advisory** — it surfaces as modernization
230+
opportunities in the roadmap, scored as such, and **never gates a tier or fails the
231+
audit.** Dated-but-working vocabulary is a quality nudge, not a defect.
232+
186233
---
187234

188235
## Report

fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,42 @@ name package extends abstract overlay isArray children value
161161
Common field attributes: `@required`, `@maxLength`, `@column` (physical column
162162
name), `@default`, `@filterable`, `@sortable`.
163163

164+
### Choosing the right field shape
165+
166+
When a need doesn't obviously map to a subtype, run this ordered test (ADR-0037) —
167+
**the first that matches decides:**
168+
169+
1. **Derivable from what you already have?** → derive it, declare NOTHING new. An
170+
array is `isArray: true` (never an array column type); a bounded string is
171+
`@maxLength`; FK columns come from `identity.reference`. Don't add vocabulary for
172+
something the metadata already implies.
173+
2. **Pure physical-DB detail, native type unchanged?** → the narrow `@dbColumnType`
174+
escape hatch (e.g. an open JSON bag). Used sparingly; not a logical type.
175+
3. **Logical concept** — a *different kind* of value (its own native type, e.g.
176+
UUID, money) → a **subtype**; the *same kind* with a modifier (still a string/
177+
decimal, just constrained) → an **attribute** (`@maxLength`, `@precision`).
178+
179+
Canonical form for common needs — reach for these before inventing anything:
180+
181+
| Need | Author it as | Note |
182+
|---|---|---|
183+
| IDs / unique keys | `field.uuid` | native UUID; use `@dbColumnType: uuid` only to force a string-typed value over a uuid column on purpose |
184+
| Money | `field.currency` | integer minor units; never a float |
185+
| Closed set of symbols | `field.enum` | `@values` required |
186+
| Instant / timestamp | `field.timestamp` | ISO 8601 with timezone on the wire |
187+
| A list of anything | `isArray: true` | on the base subtype (e.g. `field.string` + `isArray`) — there is **no** array `@dbColumnType` (retired) |
188+
| Long / unbounded text | bare `field.string` | add `@maxLength` only when you want `varchar(N)` |
189+
| Nested structured value | `field.object` | `@objectRef` + `@storage` |
190+
| Open JSON bag (no fixed shape) | `field.string` + `@dbColumnType: jsonb` | logical type stays string; column is jsonb |
191+
192+
<!-- TODO(ADR-0036 Wave 2/3): add @localTime / @format / @relationName guidance when merged -->
193+
194+
**Extending the metamodel (custom providers):** the same ordered test governs new
195+
vocabulary you register. A would-be subtype that differs from an existing one only
196+
by a property is an **attribute**, not a subtype; a string-shape validation
197+
(email/url) is an attribute, not a subtype (its native type is still `string`).
198+
Apply ADR-0037 before registering anything.
199+
164200
### Currency
165201

166202
`field.currency` stores money as **integer minor units** (cents for USD, yen for

fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,15 @@ dialect's column types deterministically (`field.string` + `@maxLength` →
157157
`varchar(N)`, `field.currency` → integer, `field.uuid` → native `uuid` on
158158
Postgres, `field.enum``varchar` + `CHECK`, etc.).
159159

160+
Codegen only ever maps the **shapes you authored** — so author them right. If you
161+
find the generator emitting the wrong column type, the fix is the field shape, not a
162+
template hack. See "Choosing the right field shape" in the **`metaobjects-authoring`**
163+
skill for the ordered subtype-vs-attribute-vs-`@dbColumnType` test (ADR-0037) — e.g.
164+
arrays are `isArray: true` (never an array column type) and a native UUID is
165+
`field.uuid` (not a string + `@dbColumnType`). When you register custom vocabulary
166+
for a custom generator, the same ADR-0037 procedure decides whether it's a subtype or
167+
an attribute.
168+
160169
## Per-target output
161170

162171
Generated code can be routed to **multiple output directories/packages** so each

fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/SKILL.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,53 @@ code behind a grep hit; a "duplicate" validator's *divergence* is the finding.
8080
in committed canonical JSON (ADR-0032); DB-type-as-logical-subtype (ADR-0013); per-port
8181
migration engine where schema is Node-`meta`-owned (ADR-0015).
8282

83+
- [ ] **I. Vocabulary hygiene / modernization (ADVISORY).** Flag already-retired or
84+
deprecated authoring patterns and recommend the canonical form (see § Vocabulary
85+
hygiene). Advisory severity — scored as modernization opportunities, **never a
86+
failing finding**.
87+
8388
**Phase 4 — Synthesize** into the tiered roadmap and populate both artifacts.
8489

8590
---
8691

92+
## Vocabulary hygiene / modernization (axis I — ADVISORY)
93+
94+
Per ADR-0037, vocabulary expansion follows ONE ordered test (derivable → derive;
95+
physical-only → `@dbColumnType`; logical: different native type → subtype, same kind +
96+
modifier → attribute). This axis surfaces authoring that predates or contradicts that
97+
framework. **All findings here are advisory** — modernization opportunities scored as
98+
such, surfaced in the roadmap, but **non-failing** (the code works; the form is dated).
99+
100+
**Already-retired / deprecated forms → recommend the canonical form:**
101+
102+
- `@dbColumnType: uuid_array` / `@dbColumnType: text_array` (a physical array column
103+
type) → **`isArray: true`** on the base subtype. Array-ness is logical and derivable;
104+
the array column type is retired.
105+
- The `@kind: text` hack (forcing text via a kind override) → **bare `field.string`**
106+
(text is the default; no override needed).
107+
- `@dbColumnType: uuid` where a native UUID type is actually wanted → **`field.uuid`**
108+
(a distinct native type is a subtype, not a physical override). Keep `@dbColumnType:
109+
uuid` ONLY for the deliberate string-over-uuid-column case.
110+
111+
**Custom-provider vocabulary (adopters who register their own types/attrs):** check
112+
new/custom vocab against the ADR-0037 procedure (advisory) — e.g. *a custom subtype
113+
that differs from an existing one only by a property should be an attribute, not a
114+
subtype*; a string-shape validation (email/url) is an attribute (native type unchanged),
115+
not a subtype. Recommend re-shaping against the ordered test.
116+
117+
**TODO — Wave-2/3 checks to add when the conventions merge (NOT yet active — these
118+
tokens are pending ADR-0036 Wave 2/3 and not yet registered; do NOT flag adopters for
119+
them until merged):**
120+
121+
- `@dbColumnType: timestamp_with_tz`**drop it** (timezone-aware is the
122+
`field.timestamp` default) — *pending Wave 2/3*.
123+
- A bare wall-clock `field.timestamp` meaning naive/local time → flag for the
124+
forthcoming `@localTime` zone-flag attribute — *pending Wave 2/3, not yet active*.
125+
- An author-supplied regex validating email / url shape → flag for the forthcoming
126+
`@format` string-format attribute — *pending Wave 2/3, not yet active*.
127+
128+
---
129+
87130
## Classification scheme (every surface; classify on codegen AND runtime)
88131

89132
| Class | Meaning | Action |
@@ -183,6 +226,10 @@ one-consumer need, read it codegen-locally.
183226
Coarse bands only (none / some / most / all). Worst-of within a pillar. On re-run, grade
184227
the delta. Lead with gaps, not the grade.
185228

229+
**Vocabulary hygiene (axis I) is advisory** — it surfaces as modernization
230+
opportunities in the roadmap, scored as such, and **never gates a tier or fails the
231+
audit.** Dated-but-working vocabulary is a quality nudge, not a defect.
232+
186233
---
187234

188235
## Report

fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/SKILL.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,42 @@ name package extends abstract overlay isArray children value
161161
Common field attributes: `@required`, `@maxLength`, `@column` (physical column
162162
name), `@default`, `@filterable`, `@sortable`.
163163

164+
### Choosing the right field shape
165+
166+
When a need doesn't obviously map to a subtype, run this ordered test (ADR-0037) —
167+
**the first that matches decides:**
168+
169+
1. **Derivable from what you already have?** → derive it, declare NOTHING new. An
170+
array is `isArray: true` (never an array column type); a bounded string is
171+
`@maxLength`; FK columns come from `identity.reference`. Don't add vocabulary for
172+
something the metadata already implies.
173+
2. **Pure physical-DB detail, native type unchanged?** → the narrow `@dbColumnType`
174+
escape hatch (e.g. an open JSON bag). Used sparingly; not a logical type.
175+
3. **Logical concept** — a *different kind* of value (its own native type, e.g.
176+
UUID, money) → a **subtype**; the *same kind* with a modifier (still a string/
177+
decimal, just constrained) → an **attribute** (`@maxLength`, `@precision`).
178+
179+
Canonical form for common needs — reach for these before inventing anything:
180+
181+
| Need | Author it as | Note |
182+
|---|---|---|
183+
| IDs / unique keys | `field.uuid` | native UUID; use `@dbColumnType: uuid` only to force a string-typed value over a uuid column on purpose |
184+
| Money | `field.currency` | integer minor units; never a float |
185+
| Closed set of symbols | `field.enum` | `@values` required |
186+
| Instant / timestamp | `field.timestamp` | ISO 8601 with timezone on the wire |
187+
| A list of anything | `isArray: true` | on the base subtype (e.g. `field.string` + `isArray`) — there is **no** array `@dbColumnType` (retired) |
188+
| Long / unbounded text | bare `field.string` | add `@maxLength` only when you want `varchar(N)` |
189+
| Nested structured value | `field.object` | `@objectRef` + `@storage` |
190+
| Open JSON bag (no fixed shape) | `field.string` + `@dbColumnType: jsonb` | logical type stays string; column is jsonb |
191+
192+
<!-- TODO(ADR-0036 Wave 2/3): add @localTime / @format / @relationName guidance when merged -->
193+
194+
**Extending the metamodel (custom providers):** the same ordered test governs new
195+
vocabulary you register. A would-be subtype that differs from an existing one only
196+
by a property is an **attribute**, not a subtype; a string-shape validation
197+
(email/url) is an attribute, not a subtype (its native type is still `string`).
198+
Apply ADR-0037 before registering anything.
199+
164200
### Currency
165201

166202
`field.currency` stores money as **integer minor units** (cents for USD, yen for

0 commit comments

Comments
 (0)