Skip to content

Commit 9fe361e

Browse files
dmealingclaude
andcommitted
docs(adr): ADR-0006 -> Option 1 — bare metadata keys, no @ sigil (closed reserved set + open attrs)
The @ sigil is redundant once bare keys are accepted (reserved set is closed/known), and it's YAML-hostile. Purge it metamodel-wide: bare keys everywhere; reserved-set membership classifies; @-prefixed keys rejected (ERR_AT_PREFIX_KEY); JSON==YAML. Synced wire-format body-key-order, CLAUDE pointer, and added bare-notation notes to the source-v2 + persistence specs (full table normalization happens in implementation). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3b524aa commit 9fe361e

5 files changed

Lines changed: 67 additions & 108 deletions

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ Preserve the following contracts exactly across all language ports:
404404
- Dotted-path syntax for `@via`: `"Program.weeks"` or `"Program.weeks.workouts"`.
405405
- Dotted-path syntax for `@of`: `"Week.id"`.
406406
- Package segments: `::` separator — `acme::common::id`.
407-
- Reserved structural keywords (`name`/`package`/`extends`/`abstract`/`overlay`/`isArray`/`children`/`value`) are bare; `@` is exclusively the inline-attribute namespace. `@`-prefixing a reserved word (e.g. `@isArray`) is a hard error (`ERR_RESERVED_ATTR`). Array-ness is the structural `isArray`, never an attr. See [ADR-0006](spec/decisions/ADR-0006-reserved-keywords-vs-inline-attributes.md).
407+
- Metadata keys are **bare — no `@` sigil** (ADR-0006). Reserved structural keywords are a closed set (`name`/`package`/`extends`/`abstract`/`overlay`/`isArray`/`children`/`value`); any other bare key is an inline attribute (classified by reserved-set membership). An `@`-prefixed key is rejected (`ERR_AT_PREFIX_KEY`) — write `schema`, not `@schema`. Array-ness is the bare reserved `isArray` (YAML: the `[]` key-suffix sugar). See [ADR-0006](spec/decisions/ADR-0006-reserved-keywords-vs-inline-attributes.md).
408408

409409
**Loader pipeline:**
410410
- `extends:` resolution happens after all files are loaded (deferred, not eager).

docs/superpowers/specs/2026-05-23-persistence-attributes-cross-language-design.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
- **Companion to:** [source v2 spec](2026-05-23-source-v2-paradigm-subtypes-multisource-design.md) + [ADR-0007](../../../spec/decisions/ADR-0007-source-v2-paradigm-subtypes-multisource.md). Obeys [ADR-0006](../../../spec/decisions/ADR-0006-reserved-keywords-vs-inline-attributes.md) (reserved words bare; everything here is an `@`-attr).
66
- **Goal:** pull together every persistence concept already in TS (Drizzle/migrate/runtime), Java (ObjectManagerDB), and C# (EF Core) into **one normalized vocabulary**, fill the gaps (explicit referential actions, soft-delete, concurrency), and pin which attrs apply per source subtype.
77

8+
> **Notation (per revised [ADR-0006](../../../spec/decisions/ADR-0006-reserved-keywords-vs-inline-attributes.md)):** metadata keys are **bare — no `@` sigil**. Every `@`-prefixed name in this doc (`@column`, `@onDelete`, `@storage`, …) is the bare key (`column`, `onDelete`, `storage`); the `@` is dropped metamodel-wide and shown here only until normalization.
9+
810
## 1. Archaeology — what already exists (and diverges)
911

1012
| Concept | TS | Java (OMDB) | C# | normalized |

docs/superpowers/specs/2026-05-23-source-v2-paradigm-subtypes-multisource-design.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
- **Related:** [ADR-0006](../../../spec/decisions/ADR-0006-reserved-keywords-vs-inline-attributes.md) (reserved keywords vs `@`-attrs — resolved here for `source`), ADR-0002 (subtype behavior), ADR-0004 (per-subtype attr schemas).
88
- **Companion:** [persistence attributes spec](2026-05-23-persistence-attributes-cross-language-design.md) — the full per-level, per-subtype attribute vocabulary (field/identity/relationship/source), including the new explicit `@onDelete`/`@onUpdate` referential actions, `@storage`, `@softDelete`, `@version`.
99

10+
> **Notation (per revised [ADR-0006](../../../spec/decisions/ADR-0006-reserved-keywords-vs-inline-attributes.md)):** metadata keys are **bare — no `@` sigil**. Where this doc writes `@table`, `@kind`, `@role`, `@onDelete`, etc., read them as the bare keys `table`, `kind`, `role`, `onDelete`; the `@` is dropped metamodel-wide. The tables retain the `@`-form only until this doc is normalized in implementation.
11+
1012
## 1. Model (the rules)
1113

1214
A `source` declares **where an object's data physically lives**. Rules:
Lines changed: 53 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,73 @@
1-
# ADR-0006 — Metadata document structure: reserved structural keywords vs `@`-prefixed inline attributes
1+
# ADR-0006 — Metadata keys are bare: a closed reserved-keyword set + open inline attributes (no sigil)
22

33
**Status:** Proposed (2026-05-23)
44

55
## Context
66

7-
Every metadata node serializes as a single fused-key object — `{"object.entity": { ...body... }}` — whose **body** mixes two kinds of keys:
7+
A metadata node body mixes two kinds of keys: a small **closed** set of reserved structural
8+
keywords (`name`, `package`, `extends`, `abstract`, `overlay`, `isArray`, `children`, `value`)
9+
and an **open** set of inline attributes (`schema`, `objectRef`, `maxLength`, `payloadRef`, …).
10+
The original design distinguished them with an `@` sigil on attributes (`@schema`), reserved
11+
words bare.
812

9-
- **Reserved structural keywords** — a small, *closed* vocabulary that describes the node's
10-
shape and identity: `name`, `package`, `extends`, `abstract`, `overlay`, `isArray`,
11-
`children`, `value`. Written **bare** (no sigil).
12-
- **Inline attributes** — an *open*, extensible namespace of typed `@`-prefixed key/values
13-
(`@payloadRef`, `@dbColumn`, `@maxLength`, …) that decorate a node.
13+
Two problems killed the sigil:
1414

15-
`spec/wire-format.md` already states this ("the reserved structural keys are exactly those
16-
listed … everything else is either an `@`-prefixed attribute or invalid", and "`isArray`
17-
structural, NOT an `@`-attr"). The two namespaces are meant to be **disjoint**: the `@` sigil
18-
is *exclusively* the inline-attribute namespace; reserved words are *exclusively* bare.
15+
1. **`@` is hostile to YAML.** `@` is a YAML reserved indicator, so `@schema:` can't be a plain
16+
key — every attribute would need quoting (`"@schema":`). Across the attribute-heavy source-v2
17+
and persistence designs that's relentless. (The incident that surfaced this: `@isArray`, a
18+
reserved word wrongly given the attr sigil.)
19+
2. **The sigil is redundant.** The reserved keyword set is **closed and known to the loader**, so
20+
a bare key can *always* be classified — reserved vs attribute — without any marker. The `@`
21+
conveys no information the reserved-set membership doesn't already give the parser, any
22+
cross-language tool, or a human who knows the set. Once bare keys are accepted, `@` is pure
23+
ceremony.
1924

20-
**The problem this ADR fixes:** the language ports never *enforced* disjointness. A reserved
21-
word written with a sigil — `@isArray` — was silently accepted as an inline attribute named
22-
`isArray`. That produced **two incoherent ways to express the same concept**: the structural
23-
`isArray` property (which marks a field as a collection) versus a meaningless attribute that
24-
does *not*. One shared conformance fixture (`origin-collection-simple`) was authored with
25-
`@isArray`, and the TS Python entity-codegen briefly papered over the split by reading array-ness
26-
from *either* form — masking the incoherence rather than resolving it. They cannot both be valid.
27-
28-
**Prior art** (validates the design, not just the bug fix):
29-
- The closed-keyword-vs-open-extension split with a distinguishing sigil is the **OpenAPI**
30-
model — bare names are spec keywords; `x-`-prefixed names are vendor extensions, which
31-
"must not conflict with existing keywords." MetaObjects mirrors this with the sigil on the
32-
attribute side (`@`) and the structural vocabulary bare. JSON Schema is similar (`$`-core
33-
keywords + ignored/custom keywords).
34-
- `isArray` as a **boolean modifier on a typed field** (rather than array-as-its-own-type) is
35-
the **Protocol Buffers `repeated`** lineage, shared by GraphQL (`[T]`) and TypeScript
36-
(`T[]`). JSON Schema/OpenAPI/Avro instead model array as a distinct type (`type: array` +
37-
`items`). MetaObjects deliberately follows the modifier family: a field has an element
38-
subtype (`field.string`, `field.object` + `@objectRef`) plus a structural `isArray` flag.
25+
Prior art: Kubernetes, Docker Compose, and GitHub Actions YAML all use **bare keys + a known
26+
schema, no sigil** — the dominant pattern for schema-backed config.
3927

4028
## Decision
4129

42-
1. **Two disjoint namespaces.** A node body contains exactly: reserved structural keywords
43-
(bare) and inline attributes (`@`-prefixed). Nothing else is valid.
44-
45-
2. **The reserved structural keyword set is closed and fixed:** `name`, `package`, `extends`,
46-
`abstract`, `overlay`, `isArray`, `children`, `value`. (Canonical key order is defined in
47-
`spec/wire-format.md`.) Growing this set is itself an ADR-level change.
48-
49-
3. **Array-ness is structural.** A field is a collection iff its bare `isArray` reserved
50-
keyword is `true`. This is the sole representation; array-ness is never an attribute.
51-
52-
4. **`@<reserved-word>` is a hard load error.** Applying the `@` sigil to any reserved
53-
keyword name (e.g. `@isArray`, `@abstract`, `@name`) is a category error — using the
54-
attribute namespace for a structural word — and fails the load with **`ERR_RESERVED_ATTR`**.
55-
This is enforced identically in **every** language port; an error conformance fixture pins it.
56-
57-
5. **Consumers read the canonical form only.** Codegen, runtime, and migration read array-ness
58-
(and every other structural concept) from the structural keyword/property — never from an
59-
attribute. No "accept either form" compatibility shims.
30+
1. **Metadata keys are bare. There is no `@` sigil.** Identical in JSON and YAML.
31+
2. **Reserved structural keywords are a closed, fixed set:** `name`, `package`, `extends`,
32+
`abstract`, `overlay`, `isArray`, `children`, `value`. (Canonical key order in
33+
`spec/wire-format.md`. Growing the set is an ADR-level change.)
34+
3. **Any other bare key is an inline attribute.** Classification is reserved-set membership; the
35+
per-(type,subType) `AttrSchema` (ADR-0004) still types/validates declared attributes.
36+
4. **An `@`-prefixed key is rejected** (`ERR_AT_PREFIX_KEY`): "metadata keys are bare; `@x` is not
37+
valid — write `x`." This catches legacy `@attr` authoring and the `@isArray`-style mistake.
38+
5. **Arrays** are the bare reserved `isArray` (JSON) or the YAML `[]` key-suffix sugar
39+
(`field.object[]:``isArray: true`) — never a sigiled key.
6040

6141
## Consequences
6242

63-
- **Conformance corpus:**
64-
- New shared error code **`ERR_RESERVED_ATTR`** in `fixtures/conformance/ERROR-CODES.json`.
65-
- New error fixture (e.g. `error-reserved-word-as-attr`) asserting `@isArray``ERR_RESERVED_ATTR`.
66-
- `origin-collection-simple` corrected from `@isArray` to bare `isArray`; its `expected.json`
67-
regenerated (the field now carries the structural array flag, as intended).
68-
- **Every parser** adds one check in its `@`-key handling: if the de-sigiled name is in the
69-
reserved set, emit `ERR_RESERVED_ATTR` instead of creating an attribute. Small, local, and
70-
identical in spirit across TS / Python / C# / Java.
71-
- **Codegen** drops any "either form" array detection and reads the structural property only.
72-
- **Authoring** gets a loud, early error instead of a silent footgun (a field that *looks*
73-
marked as an array but isn't).
74-
- **Back-compat:** any existing metadata using `@<reserved>` now fails to load. This is
75-
intended — such documents were already semantically wrong. Only the one in-repo fixture is
76-
affected; downstream consumers using the documented bare form are unaffected.
77-
78-
## YAML (both renderings — "one structure, two renderings")
79-
80-
JSON and YAML authoring funnel through the **same** `buildTree`/`parser-core`, and the
81-
`ERR_RESERVED_ATTR` check lives there (not in the JSON front-end), so **this rule applies
82-
identically to YAML**. Two YAML specifics:
83-
84-
- **Arrays are the idiomatic `[]` key-suffix sugar.** The YAML desugar turns `field.object[]:`
85-
into the reserved structural `isArray: true`. So YAML authors express collections with no
86-
sigil and never write `@isArray` — naturally compliant.
87-
- **An `@`-prefixed key must be quoted in YAML** (`"@table": products`), because `@` is a YAML
88-
reserved indicator. This is functional, but across the attr-heavy source-v2 / persistence
89-
designs (`@table`, `@kind`, `@role`, `@onDelete`, …) it's verbose.
90-
91-
**Open question (deferred, not part of this ADR's core rule):** add a *fifth* YAML desugar rule
92-
so inline attributes may be written **bare** in YAML — the desugar prefixes `@` to any key not
93-
in the reserved set (the set is closed and known to the registry), keeping YAML clean
94-
(`table: products`) while the canonical form keeps the `@` distinction. This would make YAML
95-
first-class-ergonomic without weakening the namespace rule. Flagged for decision; **YAML
96-
conformance fixtures** (the corpus is JSON-only today) should land with it.
43+
- **Metamodel-wide `@`-purge migration** (mechanical but pervasive, all four ports):
44+
- Parsers classify a bare non-reserved key as an attribute (replacing the `@`-prefix loop) and
45+
reject `@`-prefixed keys.
46+
- Serializers emit attributes bare.
47+
- Every `expected.json` in the shared corpus is regenerated (all currently carry `@`).
48+
- All input fixtures, spec docs, and CLAUDE.md examples drop `@`.
49+
- **The model simplifies:** no sigil, no `@<reserved>` footgun, no special reserved-attr error
50+
beyond the blanket "no `@` prefix" rule. JSON and YAML become the same (no YAML quoting).
51+
- **An attribute cannot be named a reserved word** (it would *be* the reserved key) — the same
52+
constraint the old `@<reserved>`-ban imposed, now structural.
53+
- **The canonical/wire form loses the explicit attribute marker.** Consumers rely on the closed,
54+
known reserved set (which every port already has). Judged an acceptable, marginal loss.
55+
- **Back-compat:** documents using `@attr` stop loading (rejected). Intended — the corpus is
56+
migrated, and the metamodel is pre-1.0.
9757

9858
## Alternatives considered
9959

100-
- **Normalize silently** (`@isArray` → bare `isArray`). Rejected: it keeps two accepted
101-
spellings for one concept, which is the incoherence we are removing; `@` would no longer be
102-
*exclusively* the attribute namespace.
103-
- **Warn and normalize.** Rejected: softer, but still blesses the wrong form and complicates
104-
the canonical contract with a warning. The rule is simple enough to enforce strictly.
105-
- **Array as a distinct type** (JSON-Schema style `type: array` + items). Rejected: MetaObjects
106-
is committed to the typed-field-plus-modifier model (Protobuf `repeated` lineage); reworking
107-
it would be a far larger remodel for no expressive gain here.
60+
- **Keep `@` on attributes (original).** Clean in JSON, hostile in YAML, and — once bare keys are
61+
accepted — redundant ceremony. Rejected.
62+
- **Keep `@` in JSON, bare in YAML (per-format).** Inconsistent across the two renderings of one
63+
structure for no gain. Rejected.
64+
- **Keep `@` everywhere but also accept bare.** The half-measure that exposed the redundancy:
65+
if bare works, `@` is strippable, so just strip it. Rejected in favor of fully purging.
66+
- **`_`-prefix on reserved words (bare attrs).** YAML-safe and keeps a visual cue, but it marks
67+
the *frequent* set (`name`/`children` on every node) and uglifies the canonical form. Rejected.
10868

10969
## Realization status
11070

111-
- **Spec:** `spec/wire-format.md` already states the rule; this ADR elevates it to an enforced,
112-
cross-cutting contract and adds the `ERR_RESERVED_ATTR` enforcement requirement.
113-
- **TypeScript:** _pending this change._
114-
- **Python:** _pending this change._
115-
- **C#:** _pending this change._
116-
- **Java:** _pending this change._
71+
- **Spec:** `spec/wire-format.md` updated to bare keys.
72+
- **TypeScript / Python / C# / Java:** _pending_ — the `@`-purge rides the source-v2 rollout
73+
(shared corpus + TS reference first), since both touch the parser/serializer + the whole corpus.

spec/wire-format.md

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,17 @@ Within a node body, keys appear in this exact order in the canonical form:
3838
3. `extends` — when set
3939
4. `abstract` — when `true`
4040
5. `overlay` — when `true`
41-
6. `isArray` — when `true` (structural, NOT an `@`-attr)
42-
7. `@`-prefixed attributes — alphabetical order within this section
43-
(e.g. `@currency`, `@dbColumn`, `@default`, `@fields`, `@locale`, `@objectRef`)
41+
6. `isArray` — when `true`
42+
7. inline attributes (bare) — alphabetical order within this section
43+
(e.g. `currency`, `default`, `fields`, `locale`, `objectRef`, `schema`)
4444
8. `children` — when non-empty (declaration order, NOT alphabetized)
4545

46-
The reserved structural keys are exactly those listed above. Everything else inside a body
47-
is either an `@`-prefixed attribute or invalid.
48-
49-
The two namespaces are **disjoint** (ADR-0006): reserved structural keywords are *always* bare
50-
and `@` is *exclusively* the inline-attribute namespace. Applying the `@` sigil to a reserved
51-
keyword — e.g. `@isArray`, `@abstract` — is a hard load error (`ERR_RESERVED_ATTR`) in every
52-
language port. Array-ness in particular is structural: a field is a collection iff its bare
53-
`isArray` keyword is `true`; it is never expressed as an attribute.
46+
All keys are **bare — there is no `@` sigil** (ADR-0006). The reserved structural keywords are
47+
exactly the closed set listed above (1–6, 8); **any other bare key is an inline attribute**.
48+
Classification is reserved-set membership, so no marker is needed. An `@`-prefixed key is
49+
rejected (`ERR_AT_PREFIX_KEY`) in every language port — keys are bare; write `schema`, not
50+
`@schema`. Array-ness is the bare reserved `isArray` (or, in YAML, the `[]` key-suffix sugar);
51+
never a sigiled key.
5452

5553
## Package + file organization
5654

0 commit comments

Comments
 (0)