Skip to content

Commit 999269c

Browse files
dmealingclaude
andauthored
docs(agent-context): harden skills for ADR-0039 own-accessor discipline (#137)
Teach the agent-context skills that own*() accessors break extends inheritance and must not be the default — resolving/effective accessors are the default everywhere. - codegen skill: prominent rule + per-port own↔resolving mapping table + the Python(attr=own)/TS(attr=resolves) naming inversion; own is only for emitting a generated subclass's own members (ownFields). - authoring skill: brief reinforcement for adopters writing custom generators/providers — extends-inherited properties are real; consume via resolving accessors, never own*(). - audit skill: new axis G2 active check (correctness defect, not advisory) flagging own-only reads of effective properties / own-only member iteration in generators + runtime paths, with the sanctioned subclass-emit / own-serializer / @dbColumnType exceptions; plus a drift signature entry. Regenerated the agent-context conformance goldens; conformance (4/4), drift (4/4), and capability-grounding (5/5) gates all green. Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n Co-authored-by: Doug Mealing <noreply@anthropic.com>
1 parent f956dcb commit 999269c

15 files changed

Lines changed: 390 additions & 5 deletions

File tree

  • agent-context/skills
  • fixtures/agent-context-conformance

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,35 @@ code behind a grep hit; a "duplicate" validator's *divergence* is the finding.
6969
(ADR-0019); runtime reflection to resolve a type from FQN vs generated static imports /
7070
FQN registry (ADR-0001 / 0017); process-global registry vs per-loader (ADR-0014); code
7171
that **mutates the loaded metadata tree** (read-only after load); JVM/Kotlin missing
72-
startup validator; writes not routed to the `@role: primary` source.
72+
startup validator; writes not routed to the `@role: primary` source; **`own*()`
73+
accessor reads of effective properties / own-only member iteration (ADR-0039 — see
74+
the active check below).**
75+
76+
- [ ] **G2. `own*()` accessor discipline (ADR-0039 — CORRECTNESS DEFECT, not advisory).**
77+
In any custom generator, metamodel provider, or runtime path (NOT the sanctioned cases
78+
below), **flag every read of a field/node's effective property, or own-only member
79+
iteration, done through an own-only accessor** — it silently drops everything inherited
80+
via `extends` (a super-reference, not a flatten), corrupting codegen and runtime. This
81+
is exactly the class of bug that broke Kotlin's array-type derivation (a concrete field
82+
inheriting an array flag from an abstract parent generated a scalar) and, per the audit,
83+
is latent cross-port. Grep for the own-only accessors and verify each hit:
84+
- **TS:** `ownAttr(`, `ownChildren(`, `ownFields(`, a raw `isArray` field flag read →
85+
should be `attr(` / `children()` / `fields()` unless emitting a subclass's own members.
86+
- **Python:** `own_children(`, `own_fields(`, and the **inverted** bare `attr(` (Python
87+
`attr()` is OWN; the resolving form is `attrs().get(`) → flag `attr(` used to read an
88+
effective value.
89+
- **Java / Kotlin:** `getMetaAttr(name, false)` (the `,false` own overload), own-only
90+
child walks (e.g. an own-only `filterIsInstance<…>()` source lookup that emits nothing
91+
for an entity inheriting its source).
92+
- **C#:** a native `IsArray` flag read, `OwnChildren()`, own attr reads.
93+
94+
**Sanctioned (do NOT flag):** (a) a generator emitting a generated **subclass** that
95+
iterates `ownFields()` so inherited members aren't re-emitted (the generated base
96+
declares them — the `class Sub extends Base` / TPH pattern); (b) the own-mode canonical
97+
serializer + overlay-merge + super-resolution walks (library-internal); (c) the single
98+
deliberately-own attribute `@dbColumnType` (a physical column-type override, never
99+
inherited). Any own read that carries a comment naming one of these cases is fine; an
100+
uncommented own read of an effective property is the defect.
73101
- [ ] **H. Authoring-correctness / ADR-conformance (deep).** Invented/unregistered
74102
`@`-attrs or post-bootstrap registration (ADR-0023 — custom attrs belong in a registered
75103
provider or `attr.properties`); retired source-v2 forms (`source.dbTable` / `@name` /
@@ -172,6 +200,7 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend
172200
4. **Drift-admitting comments** — grep: `"keep in sync with"` / `"mirrors the"` / `"matching the"`.
173201
5. **Runtime schema patching** (`ALTER TABLE … ADD COLUMN IF NOT EXISTS`, `_ensure_schema()`) — N schema owners.
174202
6. **N declarations of one shape** — same entity as Drizzle table + Zod schema + Pydantic model + hand dataclass; target is 1 + N generated.
203+
7. **`own*()` accessor read of an effective property** (ADR-0039) — `ownAttr` / `ownFields` / `own_children` / bare Python `attr(` / `getMetaAttr(name, false)` / native `IsArray` used to read a value or iterate members outside the sanctioned subclass-emit / own-serializer / `@dbColumnType` cases → silently drops `extends`-inherited values. A **correctness defect** (axis G2), not advisory.
175204

176205
---
177206

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,16 @@ Resolution facts:
540540

541541
`abstract` and `extends` are **structural keys** (bare, no `@`).
542542

543+
**Extends-inherited properties are real — consume metadata through the resolving
544+
accessors (ADR-0039).** If you write a custom generator or a metamodel provider that
545+
reads this metadata, a concrete field/entity's inherited attributes and members live
546+
on the parent it `extends`, not on the node itself (extends is a super-*reference*, not
547+
a flatten). Always read a property or iterate a member set via the **resolving/effective**
548+
accessor (TS `attr()`/`children()`/`fields()`, Python `attrs().get()`), **never an
549+
`own*()` accessor** — an own-only read silently drops everything inherited via `extends`
550+
and corrupts the generated code. See the `metaobjects-codegen` skill for the full
551+
per-port mapping.
552+
543553
**`overlay` is a different concept.** `extends:` is an IS-A relationship between
544554
two distinct nodes. `overlay: true` re-opens the *same* named node to amend it
545555
across files (same `package` + same `name` → merged; last-writer-wins on attr

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,44 @@ the `generators` array in `metaobjects.config.ts` next to the built-ins — it r
132132
the same pass, writes under the same target rules, and carries the `@generated`
133133
header so it round-trips like any other.
134134

135+
### Never read metadata through an `own*()` accessor (ADR-0039) — top bug source
136+
137+
When writing OR reviewing a generator, **read every field/node property and iterate
138+
every member set through the resolving/effective accessor — never the `own*()` form.**
139+
`extends` is a **super-reference, not a flatten**: a concrete field/entity that
140+
`extends` an abstract parent keeps its inherited attributes and members physically on
141+
the parent, reachable only through the *resolving* accessor. An `own*()` read of an
142+
effective property (`isArray`, `subType`, `maxLength`, `precision`/`scale`, `default`,
143+
the physical column name, `objectRef`, `storage`, `required`, …) or an own-only member
144+
iteration **silently drops everything inherited via `extends`** — the classic symptom
145+
was a concrete field that inherited `isArray: true` from an abstract parent generating
146+
a *scalar* column. These reads compile and pass every fixture that never exercises
147+
`extends`, so they are a latent, cross-port top bug source.
148+
149+
**The one legitimate `own*()` use:** a generator emitting a generated **subclass** that
150+
`extends` a generated base iterates **own members** (`ownFields()`) so the inherited
151+
members are **not re-emitted** — the generated base class already declares them (the
152+
`class Sub extends Base` / TPH pattern). Everywhere else, resolve. (The own-mode
153+
canonical serializer and overlay-merge are the only other sanctioned own reads, and
154+
they are library-internal, not app-generator concerns.) The one deliberately-own
155+
attribute is `@dbColumnType` — a physical column-type override that is never inherited.
156+
157+
**Per-port own↔resolving mapping** (reach for the resolving column; comment any
158+
`own*()` call with the sanctioned case it is):
159+
160+
| Port | Resolving (default — use this) | Own-only (avoid unless emitting a subclass's own members) |
161+
|---|---|---|
162+
| TypeScript | `attr(name)`, `children()`, `fields()` | `ownAttr(name)`, `ownChildren()`, `ownFields()`, the raw `isArray` field flag |
163+
| Python | `attrs().get(name)`, `children()`, `fields()` | `attr(name)` **(own!)**, `own_children()`, `own_fields()` |
164+
| Java / Kotlin | `getMetaAttr(name)`, resolving `getChildren()` | `getMetaAttr(name, false)`, own-only child walks |
165+
| C# | resolving attr/`Children`/`Fields` accessors | `IsArray` native flag, `OwnChildren()`, own attr reads |
166+
167+
**Naming inversion — the trap:** the *default-named* accessor is NOT consistently the
168+
safe one. **TS `attr()` RESOLVES; Python `attr()` is OWN** (own-only). In Python you
169+
must call `attrs().get(name)` to get the inherited value — a bare `attr(name)` is the
170+
own read that drops inheritance. When you review or port a generator, check the port's
171+
convention, not the method name.
172+
135173
**Close but not exact?** You don't always need a new generator — a generated file is
136174
a normal source file. Copy it and customize the copy (three-way merge preserves your
137175
edits on regen), or customize the template a built-in renders from. Reach for a

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,35 @@ code behind a grep hit; a "duplicate" validator's *divergence* is the finding.
6969
(ADR-0019); runtime reflection to resolve a type from FQN vs generated static imports /
7070
FQN registry (ADR-0001 / 0017); process-global registry vs per-loader (ADR-0014); code
7171
that **mutates the loaded metadata tree** (read-only after load); JVM/Kotlin missing
72-
startup validator; writes not routed to the `@role: primary` source.
72+
startup validator; writes not routed to the `@role: primary` source; **`own*()`
73+
accessor reads of effective properties / own-only member iteration (ADR-0039 — see
74+
the active check below).**
75+
76+
- [ ] **G2. `own*()` accessor discipline (ADR-0039 — CORRECTNESS DEFECT, not advisory).**
77+
In any custom generator, metamodel provider, or runtime path (NOT the sanctioned cases
78+
below), **flag every read of a field/node's effective property, or own-only member
79+
iteration, done through an own-only accessor** — it silently drops everything inherited
80+
via `extends` (a super-reference, not a flatten), corrupting codegen and runtime. This
81+
is exactly the class of bug that broke Kotlin's array-type derivation (a concrete field
82+
inheriting an array flag from an abstract parent generated a scalar) and, per the audit,
83+
is latent cross-port. Grep for the own-only accessors and verify each hit:
84+
- **TS:** `ownAttr(`, `ownChildren(`, `ownFields(`, a raw `isArray` field flag read →
85+
should be `attr(` / `children()` / `fields()` unless emitting a subclass's own members.
86+
- **Python:** `own_children(`, `own_fields(`, and the **inverted** bare `attr(` (Python
87+
`attr()` is OWN; the resolving form is `attrs().get(`) → flag `attr(` used to read an
88+
effective value.
89+
- **Java / Kotlin:** `getMetaAttr(name, false)` (the `,false` own overload), own-only
90+
child walks (e.g. an own-only `filterIsInstance<…>()` source lookup that emits nothing
91+
for an entity inheriting its source).
92+
- **C#:** a native `IsArray` flag read, `OwnChildren()`, own attr reads.
93+
94+
**Sanctioned (do NOT flag):** (a) a generator emitting a generated **subclass** that
95+
iterates `ownFields()` so inherited members aren't re-emitted (the generated base
96+
declares them — the `class Sub extends Base` / TPH pattern); (b) the own-mode canonical
97+
serializer + overlay-merge + super-resolution walks (library-internal); (c) the single
98+
deliberately-own attribute `@dbColumnType` (a physical column-type override, never
99+
inherited). Any own read that carries a comment naming one of these cases is fine; an
100+
uncommented own read of an effective property is the defect.
73101
- [ ] **H. Authoring-correctness / ADR-conformance (deep).** Invented/unregistered
74102
`@`-attrs or post-bootstrap registration (ADR-0023 — custom attrs belong in a registered
75103
provider or `attr.properties`); retired source-v2 forms (`source.dbTable` / `@name` /
@@ -172,6 +200,7 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend
172200
4. **Drift-admitting comments** — grep: `"keep in sync with"` / `"mirrors the"` / `"matching the"`.
173201
5. **Runtime schema patching** (`ALTER TABLE … ADD COLUMN IF NOT EXISTS`, `_ensure_schema()`) — N schema owners.
174202
6. **N declarations of one shape** — same entity as Drizzle table + Zod schema + Pydantic model + hand dataclass; target is 1 + N generated.
203+
7. **`own*()` accessor read of an effective property** (ADR-0039) — `ownAttr` / `ownFields` / `own_children` / bare Python `attr(` / `getMetaAttr(name, false)` / native `IsArray` used to read a value or iterate members outside the sanctioned subclass-emit / own-serializer / `@dbColumnType` cases → silently drops `extends`-inherited values. A **correctness defect** (axis G2), not advisory.
175204

176205
---
177206

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,16 @@ Resolution facts:
540540

541541
`abstract` and `extends` are **structural keys** (bare, no `@`).
542542

543+
**Extends-inherited properties are real — consume metadata through the resolving
544+
accessors (ADR-0039).** If you write a custom generator or a metamodel provider that
545+
reads this metadata, a concrete field/entity's inherited attributes and members live
546+
on the parent it `extends`, not on the node itself (extends is a super-*reference*, not
547+
a flatten). Always read a property or iterate a member set via the **resolving/effective**
548+
accessor (TS `attr()`/`children()`/`fields()`, Python `attrs().get()`), **never an
549+
`own*()` accessor** — an own-only read silently drops everything inherited via `extends`
550+
and corrupts the generated code. See the `metaobjects-codegen` skill for the full
551+
per-port mapping.
552+
543553
**`overlay` is a different concept.** `extends:` is an IS-A relationship between
544554
two distinct nodes. `overlay: true` re-opens the *same* named node to amend it
545555
across files (same `package` + same `name` → merged; last-writer-wins on attr

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,44 @@ the `generators` array in `metaobjects.config.ts` next to the built-ins — it r
132132
the same pass, writes under the same target rules, and carries the `@generated`
133133
header so it round-trips like any other.
134134

135+
### Never read metadata through an `own*()` accessor (ADR-0039) — top bug source
136+
137+
When writing OR reviewing a generator, **read every field/node property and iterate
138+
every member set through the resolving/effective accessor — never the `own*()` form.**
139+
`extends` is a **super-reference, not a flatten**: a concrete field/entity that
140+
`extends` an abstract parent keeps its inherited attributes and members physically on
141+
the parent, reachable only through the *resolving* accessor. An `own*()` read of an
142+
effective property (`isArray`, `subType`, `maxLength`, `precision`/`scale`, `default`,
143+
the physical column name, `objectRef`, `storage`, `required`, …) or an own-only member
144+
iteration **silently drops everything inherited via `extends`** — the classic symptom
145+
was a concrete field that inherited `isArray: true` from an abstract parent generating
146+
a *scalar* column. These reads compile and pass every fixture that never exercises
147+
`extends`, so they are a latent, cross-port top bug source.
148+
149+
**The one legitimate `own*()` use:** a generator emitting a generated **subclass** that
150+
`extends` a generated base iterates **own members** (`ownFields()`) so the inherited
151+
members are **not re-emitted** — the generated base class already declares them (the
152+
`class Sub extends Base` / TPH pattern). Everywhere else, resolve. (The own-mode
153+
canonical serializer and overlay-merge are the only other sanctioned own reads, and
154+
they are library-internal, not app-generator concerns.) The one deliberately-own
155+
attribute is `@dbColumnType` — a physical column-type override that is never inherited.
156+
157+
**Per-port own↔resolving mapping** (reach for the resolving column; comment any
158+
`own*()` call with the sanctioned case it is):
159+
160+
| Port | Resolving (default — use this) | Own-only (avoid unless emitting a subclass's own members) |
161+
|---|---|---|
162+
| TypeScript | `attr(name)`, `children()`, `fields()` | `ownAttr(name)`, `ownChildren()`, `ownFields()`, the raw `isArray` field flag |
163+
| Python | `attrs().get(name)`, `children()`, `fields()` | `attr(name)` **(own!)**, `own_children()`, `own_fields()` |
164+
| Java / Kotlin | `getMetaAttr(name)`, resolving `getChildren()` | `getMetaAttr(name, false)`, own-only child walks |
165+
| C# | resolving attr/`Children`/`Fields` accessors | `IsArray` native flag, `OwnChildren()`, own attr reads |
166+
167+
**Naming inversion — the trap:** the *default-named* accessor is NOT consistently the
168+
safe one. **TS `attr()` RESOLVES; Python `attr()` is OWN** (own-only). In Python you
169+
must call `attrs().get(name)` to get the inherited value — a bare `attr(name)` is the
170+
own read that drops inheritance. When you review or port a generator, check the port's
171+
convention, not the method name.
172+
135173
**Close but not exact?** You don't always need a new generator — a generated file is
136174
a normal source file. Copy it and customize the copy (three-way merge preserves your
137175
edits on regen), or customize the template a built-in renders from. Reach for a

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,35 @@ code behind a grep hit; a "duplicate" validator's *divergence* is the finding.
6969
(ADR-0019); runtime reflection to resolve a type from FQN vs generated static imports /
7070
FQN registry (ADR-0001 / 0017); process-global registry vs per-loader (ADR-0014); code
7171
that **mutates the loaded metadata tree** (read-only after load); JVM/Kotlin missing
72-
startup validator; writes not routed to the `@role: primary` source.
72+
startup validator; writes not routed to the `@role: primary` source; **`own*()`
73+
accessor reads of effective properties / own-only member iteration (ADR-0039 — see
74+
the active check below).**
75+
76+
- [ ] **G2. `own*()` accessor discipline (ADR-0039 — CORRECTNESS DEFECT, not advisory).**
77+
In any custom generator, metamodel provider, or runtime path (NOT the sanctioned cases
78+
below), **flag every read of a field/node's effective property, or own-only member
79+
iteration, done through an own-only accessor** — it silently drops everything inherited
80+
via `extends` (a super-reference, not a flatten), corrupting codegen and runtime. This
81+
is exactly the class of bug that broke Kotlin's array-type derivation (a concrete field
82+
inheriting an array flag from an abstract parent generated a scalar) and, per the audit,
83+
is latent cross-port. Grep for the own-only accessors and verify each hit:
84+
- **TS:** `ownAttr(`, `ownChildren(`, `ownFields(`, a raw `isArray` field flag read →
85+
should be `attr(` / `children()` / `fields()` unless emitting a subclass's own members.
86+
- **Python:** `own_children(`, `own_fields(`, and the **inverted** bare `attr(` (Python
87+
`attr()` is OWN; the resolving form is `attrs().get(`) → flag `attr(` used to read an
88+
effective value.
89+
- **Java / Kotlin:** `getMetaAttr(name, false)` (the `,false` own overload), own-only
90+
child walks (e.g. an own-only `filterIsInstance<…>()` source lookup that emits nothing
91+
for an entity inheriting its source).
92+
- **C#:** a native `IsArray` flag read, `OwnChildren()`, own attr reads.
93+
94+
**Sanctioned (do NOT flag):** (a) a generator emitting a generated **subclass** that
95+
iterates `ownFields()` so inherited members aren't re-emitted (the generated base
96+
declares them — the `class Sub extends Base` / TPH pattern); (b) the own-mode canonical
97+
serializer + overlay-merge + super-resolution walks (library-internal); (c) the single
98+
deliberately-own attribute `@dbColumnType` (a physical column-type override, never
99+
inherited). Any own read that carries a comment naming one of these cases is fine; an
100+
uncommented own read of an effective property is the defect.
73101
- [ ] **H. Authoring-correctness / ADR-conformance (deep).** Invented/unregistered
74102
`@`-attrs or post-bootstrap registration (ADR-0023 — custom attrs belong in a registered
75103
provider or `attr.properties`); retired source-v2 forms (`source.dbTable` / `@name` /
@@ -172,6 +200,7 @@ Per finding: `file:line` → what → generated-equivalent exists? → recommend
172200
4. **Drift-admitting comments** — grep: `"keep in sync with"` / `"mirrors the"` / `"matching the"`.
173201
5. **Runtime schema patching** (`ALTER TABLE … ADD COLUMN IF NOT EXISTS`, `_ensure_schema()`) — N schema owners.
174202
6. **N declarations of one shape** — same entity as Drizzle table + Zod schema + Pydantic model + hand dataclass; target is 1 + N generated.
203+
7. **`own*()` accessor read of an effective property** (ADR-0039) — `ownAttr` / `ownFields` / `own_children` / bare Python `attr(` / `getMetaAttr(name, false)` / native `IsArray` used to read a value or iterate members outside the sanctioned subclass-emit / own-serializer / `@dbColumnType` cases → silently drops `extends`-inherited values. A **correctness defect** (axis G2), not advisory.
175204

176205
---
177206

0 commit comments

Comments
 (0)