You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit fe13e35
Browse filesBrowse the repository at this point in the historyBrowse files
feat(agent-context): teach the metadata-extension decision + audit for new-vocabulary opportunities
Two skill gaps this leaves closed:
- metaobjects-authoring: the description now advertises the custom-vocabulary
decision (intent trigger), and the "Extending the metamodel" paragraph is
promoted to a subsection covering the judgment layer the ADR-0037 table didn't:
converge-before-inventing (don't claim the chartered api/operation/surface/binding
names), the design rules that make downstream vocabulary age well (protocol/
address-free nodes with transport behind a @kind variant, names-only fail-closed
config, reference-typed payloads), and the register→extend→promote lifecycle. Links
to the new docs/features/downstream-metadata-decisions.md guide.
- metaobjects-audit: a new inverse hunt — the audit checked *existing* custom vocab
against ADR-0037; it now also flags where an app SHOULD register vocabulary but
hand-coded the pattern (N parallel integration modules, an ad-hoc string
discriminator, provider-shaped repetition, an integration modeled entirely in
code). New VOCAB CANDIDATE (advisory) class + classification row + drift signature
9 + a capability-checklist cross-cutting line. Advisory-only, bias-to-under-flag,
grounded phrasing (the hypothetical subtype stays in prose; only registered tokens
are named).
Regenerated the four agent-context-conformance stacks; grounding + drift + conformance
+ full sdk suite green. Skill-only change (no code, no metamodel vocab added).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaRaYFjvWVV8D6h33ejj1m
a small generator would own the payload wiring, the names-only fail-closed config check,
212
+
and drift detection.
213
+
214
+
Run the **ADR-0037 ordered test** before proposing: derivable → derive; differs from an
215
+
existing subtype only by a property → an attribute; a plain validated value → an
216
+
attribute; one-off author-supplied properties → the `attr.properties` bag. Only a concept
217
+
with its own behavior/attributes earns a subtype, and transport/protocol is never the
218
+
subtype axis (keep the node protocol- and address-free; variants behind the discriminator).
219
+
**Converge before inventing** — check shipped vocabulary AND the planned/chartered names
220
+
(see CALIBRATION "planned, not shipped"); never claim a chartered name for a project-local
221
+
type. **Recommendation:** propose a project-registered provider (explicit loader wiring,
222
+
ADR-0023 — never a loosened `strict` free-ride), a `metadata_sketch` of the node, and the
223
+
owned generator that retires the duplication; a second independent consumer needing the
224
+
same concept is a consumer→core promotion candidate (ADR-0011 — file an upstream issue).
225
+
**Verdict: VOCAB CANDIDATE (advisory)** — a modeling opportunity, never a failing finding,
226
+
never a tier gate; bias to under-flagging (the >15% false-positive kill criterion applies
227
+
with full force). Full guidance: `docs/features/downstream-metadata-decisions.md`.
228
+
192
229
---
193
230
194
231
## Classification scheme (every surface; classify on codegen AND runtime)
@@ -201,6 +238,7 @@ to author). Advisory severity — a modernization opportunity, not a failing fin
201
238
|**CODEGEN CANDIDATE (partial)**| Generatable data layer, bespoke presentation. | Generate data layer; keep viz hand-written. |
202
239
|**DYNAMIC-RUNTIME CANDIDATE**| Behavior that could be metadata-driven at runtime. | Assess runtime-metadata feasibility. |
203
240
|**BESPOKE (keep)**| Genuine custom: irreducible SQL (recursive CTEs, window functions, set ops — NOT plain count/sum/avg rollups, which are `origin.aggregate` on a projection), graph, SSE, auth, search, viz. | Leave hand-written — still import generated types. |
241
+
|**VOCAB CANDIDATE (advisory)**| A recurring, closed hand-coded pattern (parallel integration modules / an ad-hoc string discriminator) that project-registered vocabulary — a custom subtype/attr via a provider — plus a small owned generator would own. | Propose the provider + `metadata_sketch`; apply the ADR-0037 ordered test; advisory only. |
204
242
205
243
**Gold-standard exception.** A hand-written component that *derives* from generated metadata
206
244
cannot drift — flag as good. A "bespoke" component hardcoding a shape metadata knows is a
@@ -223,6 +261,7 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend
223
261
8.**Hand-written `CREATE VIEW` / read-only SQL standing in for a projection (view-necessity test).** Grep migrations, checked-in `.sql`, and repository/query code for `CREATE [OR REPLACE] [MATERIALIZED] VIEW`, and for hand-rolled read-only queries that mirror a read model — a pure-`SELECT` repository/service method with joins or `GROUP BY` feeding a DTO, or a raw-SQL escape (`db.execute(sql…)`, `FromSqlRaw`, a JPA @Query with hand-written SQL). For each, run the **necessity test** — can `object.projection` + origins express this shape? It can when every output column is (a) a base-entity or relationship-joined column → `origin.passthrough` (`@from` / `@via`), (b) a count/sum/avg/min/max over related rows → `origin.aggregate` (`@agg` / `@of` / `@via`, optionally row-scoped with `@filter`), (c) a child collection → `origin.collection` (`@via`), or (d) a column borrowed via `extends` — and the joins follow declared relationships / `identity.reference` FKs.
224
262
-**Expressible → CODEGEN CANDIDATE (high):** convert to an `object.projection` with a read-only `source.rdb``@kind: view` child, let `meta migrate` emit the `CREATE VIEW`, and consume the generated read-only query — the hand-written view is a second source of truth for a derivable shape. Flag in the finding that **`meta verify --db` cannot catch this**: an unmodeled DB view is *unmanaged*, so this audit is the only gate that sees it. Parity-gate: the generated view returns row-identical results before the hand-written SQL is deleted.
225
263
-**Not expressible → BESPOKE (keep), with a NAMED justification:** record the construct that makes it irreducible (recursive CTE, window function / `OVER`, `UNION` / `INTERSECT` / `EXCEPT`, `DISTINCT ON`, lateral join, non-aggregate expression column). "It's an aggregation" is NOT a justification — plain count/sum/avg/min/max rollups are `origin.aggregate`.
264
+
9.**A closed variant-set hand-modeled per instance** — N sibling modules / classes / config blocks, one per channel / provider / target, sharing a payload + config shape and diverging only by transport. Grep for sibling-file families and switch-on-a-string dispatch; verify the set is closed and recurring (never a one-off). → axis I "New-vocabulary OPPORTUNITY" (VOCAB CANDIDATE, advisory).
Copy file name to clipboardExpand all lines: agent-context/skills/metaobjects-authoring/SKILL.md
+34-12Lines changed: 34 additions & 12 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,6 @@
1
1
---
2
2
name: metaobjects-authoring
3
-
description: Use when authoring or modifying MetaObjects metadata — fields, entities, relationships, sources, enums, abstracts/inheritance — in YAML or canonical JSON.
3
+
description: Use when authoring or modifying MetaObjects metadata — fields, entities, relationships, sources, enums, abstracts/inheritance — in YAML or canonical JSON; includes deciding whether and how to register custom vocabulary (new subtypes or attributes via a provider).
4
4
---
5
5
6
6
# Authoring MetaObjects metadata
@@ -355,17 +355,39 @@ metamodel attribute*: you declare the FK once via `identity.reference`, and the
355
355
finders fall out of codegen. So never hand-roll a `findByParentId` / `WHERE fk = ?` helper —
356
356
consume the generated finder.
357
357
358
-
**Extending the metamodel (custom providers):** the same ordered procedure above
359
-
governs new vocabulary you register — apply it mechanically before registering
360
-
anything. A would-be subtype that differs from an existing one only by a *property*
361
-
is an **attribute**, not a subtype (a "short string" isn't a new field subtype —
362
-
that is `@maxLength`); a plain string that merely needs validating is a **validation
363
-
attribute**, not a subtype (its native type is still `string`, and there's no
364
-
behavior to own); a concept with its own native type or behavior is a **subtype**,
365
-
and structural variants *within* such a subtype are `@kind`. Every new first-class
366
-
element also requires a registered provider + a `registry-conformance` fixture
367
-
(ADR-0023 strict provenance), and closed enums (including any `@kind` value-set)
368
-
carry `allowedValues` in the gate (ADR-0036). ADR-0037 is the authority.
358
+
### Extending the metamodel — custom providers, and the downstream lifecycle
359
+
360
+
The same ordered procedure above governs new vocabulary you register — apply it
361
+
mechanically before registering anything. A would-be subtype that differs from an
362
+
existing one only by a *property* is an **attribute**, not a subtype (a "short
363
+
string" isn't a new field subtype — that is `@maxLength`); a plain string that merely
364
+
needs validating is a **validation attribute**, not a subtype (its native type is
365
+
still `string`, and there's no behavior to own); a concept with its own native type
366
+
or behavior is a **subtype**, and structural variants *within* such a subtype are
367
+
`@kind`. Every new first-class element also requires a registered provider + a
368
+
`registry-conformance` fixture (ADR-0023 strict provenance), and closed enums
369
+
(including any `@kind` value-set) carry `allowedValues` in the gate (ADR-0036).
370
+
ADR-0037 is the authority.
371
+
372
+
**Converge before inventing.** Search the shipped vocabulary first (`meta types
373
+
<term>`) and check the roadmap — if core already models (or plans) the concept, model
374
+
yours in a fold-in-friendly shape. The type names `api`, `operation`, `surface`, and
375
+
`binding` are chartered for planned core vocabulary — never claim one for a
376
+
project-local type (a later core release would force a breaking rename).
377
+
378
+
**Design rules for downstream vocabulary that ages well.** Keep the node protocol-
379
+
and address-free — the subtype names the *concept*; transport/protocol is a closed
380
+
`@kind` variant *within* the subtype (as `source.rdb` puts table/view behind
381
+
`@kind`), never the subtype axis, and endpoints/addresses never enter metadata.
382
+
Declare config as the *names* of required keys (values stay in env/config) and
383
+
generate a fail-closed check. Reference typed payloads instead of inlining shapes.
384
+
385
+
**Lifecycle.** Register with explicit provider wiring — never loosen `strict` to
386
+
free-ride ad-hoc attrs. When core later ships the concept your provider shrinks from
387
+
`register` to `extend` (the `template.toolcall` precedent). A second independent
388
+
consumer wanting the same concept is a consumer→core promotion candidate (ADR-0011) —
389
+
open an upstream issue rather than adding core vocabulary yourself. Full guidance:
Copy file name to clipboardExpand all lines: fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/SKILL.md
+39Lines changed: 39 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -189,6 +189,43 @@ emits two distinct finders (named by the FK field) automatically — no annotati
189
189
reverse finder is a **codegen feature, not an attribute** (there is no reverse-nav `@`-attr
190
190
to author). Advisory severity — a modernization opportunity, not a failing finding.
191
191
192
+
**New-vocabulary OPPORTUNITY (the inverse hunt — advisory).** The checks above audit
193
+
custom vocabulary the adopter *already* registered; this one hunts where the app
194
+
**should** register vocabulary but hand-coded the pattern instead. Smells
195
+
(grep-then-verify; require a *recurring, closed* set — never flag a one-off):
196
+
-**N parallel hand-written integration modules** sharing a payload shape and a config
197
+
pattern (a closed set of channels / providers / export targets, each a near-copy with
198
+
a different transport) → candidate for ONE project-registered subtype whose variants
199
+
sit behind a closed structural-variant discriminator (the `source.rdb``@kind`
200
+
pattern), plus a small owned generator emitting the per-variant wiring.
a small generator would own the payload wiring, the names-only fail-closed config check,
212
+
and drift detection.
213
+
214
+
Run the **ADR-0037 ordered test** before proposing: derivable → derive; differs from an
215
+
existing subtype only by a property → an attribute; a plain validated value → an
216
+
attribute; one-off author-supplied properties → the `attr.properties` bag. Only a concept
217
+
with its own behavior/attributes earns a subtype, and transport/protocol is never the
218
+
subtype axis (keep the node protocol- and address-free; variants behind the discriminator).
219
+
**Converge before inventing** — check shipped vocabulary AND the planned/chartered names
220
+
(see CALIBRATION "planned, not shipped"); never claim a chartered name for a project-local
221
+
type. **Recommendation:** propose a project-registered provider (explicit loader wiring,
222
+
ADR-0023 — never a loosened `strict` free-ride), a `metadata_sketch` of the node, and the
223
+
owned generator that retires the duplication; a second independent consumer needing the
224
+
same concept is a consumer→core promotion candidate (ADR-0011 — file an upstream issue).
225
+
**Verdict: VOCAB CANDIDATE (advisory)** — a modeling opportunity, never a failing finding,
226
+
never a tier gate; bias to under-flagging (the >15% false-positive kill criterion applies
227
+
with full force). Full guidance: `docs/features/downstream-metadata-decisions.md`.
228
+
192
229
---
193
230
194
231
## Classification scheme (every surface; classify on codegen AND runtime)
@@ -201,6 +238,7 @@ to author). Advisory severity — a modernization opportunity, not a failing fin
201
238
|**CODEGEN CANDIDATE (partial)**| Generatable data layer, bespoke presentation. | Generate data layer; keep viz hand-written. |
202
239
|**DYNAMIC-RUNTIME CANDIDATE**| Behavior that could be metadata-driven at runtime. | Assess runtime-metadata feasibility. |
203
240
|**BESPOKE (keep)**| Genuine custom: irreducible SQL (recursive CTEs, window functions, set ops — NOT plain count/sum/avg rollups, which are `origin.aggregate` on a projection), graph, SSE, auth, search, viz. | Leave hand-written — still import generated types. |
241
+
|**VOCAB CANDIDATE (advisory)**| A recurring, closed hand-coded pattern (parallel integration modules / an ad-hoc string discriminator) that project-registered vocabulary — a custom subtype/attr via a provider — plus a small owned generator would own. | Propose the provider + `metadata_sketch`; apply the ADR-0037 ordered test; advisory only. |
204
242
205
243
**Gold-standard exception.** A hand-written component that *derives* from generated metadata
206
244
cannot drift — flag as good. A "bespoke" component hardcoding a shape metadata knows is a
@@ -223,6 +261,7 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend
223
261
8.**Hand-written `CREATE VIEW` / read-only SQL standing in for a projection (view-necessity test).** Grep migrations, checked-in `.sql`, and repository/query code for `CREATE [OR REPLACE] [MATERIALIZED] VIEW`, and for hand-rolled read-only queries that mirror a read model — a pure-`SELECT` repository/service method with joins or `GROUP BY` feeding a DTO, or a raw-SQL escape (`db.execute(sql…)`, `FromSqlRaw`, a JPA @Query with hand-written SQL). For each, run the **necessity test** — can `object.projection` + origins express this shape? It can when every output column is (a) a base-entity or relationship-joined column → `origin.passthrough` (`@from` / `@via`), (b) a count/sum/avg/min/max over related rows → `origin.aggregate` (`@agg` / `@of` / `@via`, optionally row-scoped with `@filter`), (c) a child collection → `origin.collection` (`@via`), or (d) a column borrowed via `extends` — and the joins follow declared relationships / `identity.reference` FKs.
224
262
-**Expressible → CODEGEN CANDIDATE (high):** convert to an `object.projection` with a read-only `source.rdb``@kind: view` child, let `meta migrate` emit the `CREATE VIEW`, and consume the generated read-only query — the hand-written view is a second source of truth for a derivable shape. Flag in the finding that **`meta verify --db` cannot catch this**: an unmodeled DB view is *unmanaged*, so this audit is the only gate that sees it. Parity-gate: the generated view returns row-identical results before the hand-written SQL is deleted.
225
263
-**Not expressible → BESPOKE (keep), with a NAMED justification:** record the construct that makes it irreducible (recursive CTE, window function / `OVER`, `UNION` / `INTERSECT` / `EXCEPT`, `DISTINCT ON`, lateral join, non-aggregate expression column). "It's an aggregation" is NOT a justification — plain count/sum/avg/min/max rollups are `origin.aggregate`.
264
+
9.**A closed variant-set hand-modeled per instance** — N sibling modules / classes / config blocks, one per channel / provider / target, sharing a payload + config shape and diverging only by transport. Grep for sibling-file families and switch-on-a-string dispatch; verify the set is closed and recurring (never a one-off). → axis I "New-vocabulary OPPORTUNITY" (VOCAB CANDIDATE, advisory).
Copy file name to clipboardExpand all lines: fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md
+6Lines changed: 6 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -163,6 +163,12 @@ classify it (using the classification scheme in `SKILL.md`) and route the cutove
0 commit comments