Skip to content

Commit 4ac9d07

Browse files
dmealingclaude
andcommitted
Merge design: ADR-0006 (AI-first YAML) + ADR-0007 (source v2) + source-v2/persistence specs + rollout plan
Docs-only: the reconciled metamodel design (sigil-free AI-first YAML with canonical JSON unchanged; source paradigm subtypes; consolidated persistence attributes incl. explicit referential actions) + the staged rollout plan. No code/corpus changes; ERROR-CODES.json unchanged. Implementation follows per the rollout plan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 parents e471ad8 + a581438 commit 4ac9d07

8 files changed

Lines changed: 582 additions & 2 deletions

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +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+
- **Canonical JSON:** reserved structural keywords are bare (`name`/`package`/`extends`/`abstract`/`overlay`/`isArray`/`children`/`value`); inline attributes are `@`-prefixed. `@`-prefixing a reserved word (e.g. `@isArray`) is invalid (`ERR_RESERVED_ATTR`). **YAML authoring is sigil-free** — bare attrs; the desugar re-adds `@` when lowering to canonical JSON; canonical JSON is the sole cross-language interchange (YAML is a TS-only front-end). See [ADR-0006](spec/decisions/ADR-0006-ai-first-yaml-authoring.md).
407408

408409
**Loader pipeline:**
409410
- `extends:` resolution happens after all files are loaded (deferred, not eager).
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Rollout — source v2 + persistence attrs + reserved-word enforcement + AI-first YAML
2+
3+
> **Master sequencing plan.** This is the roadmap; each stage gets its own detailed TDD plan
4+
> (via `superpowers:writing-plans`) when it starts. Execute stages in isolated worktrees off the
5+
> latest `main`; per-unit `review + simplify` then merge forward + push (the standing gate).
6+
7+
**Goal:** land the reconciled metamodel design — `source` v2, the consolidated persistence
8+
attributes (incl. explicit referential actions), the canonical reserved-word enforcement, and
9+
sigil-free AI-first YAML — across the language ports.
10+
11+
**Authoritative design docs (read first):**
12+
- `spec/decisions/ADR-0006-ai-first-yaml-authoring.md` — sigil-free YAML; canonical JSON keeps `@`.
13+
- `spec/decisions/ADR-0007-source-v2-paradigm-subtypes-multisource.md` — source paradigm subtypes.
14+
- `docs/superpowers/specs/2026-05-23-source-v2-paradigm-subtypes-multisource-design.md`
15+
- `docs/superpowers/specs/2026-05-23-persistence-attributes-cross-language-design.md`
16+
- `spec/wire-format.md` (canonical key model), `spec/conformance-tests.md` (corpus contract).
17+
18+
## Decided defaults (open forks — ratified; override before the relevant stage if desired)
19+
- **`@onDelete` defaults from relationship subtype:** composition→`cascade`, aggregation→`set-null` (relation must be optional), association→`restrict`. `@onUpdate` default `cascade`. Value set = the existing `FkAction` union `cascade|set-null|restrict|no-action` (no `setDefault`).
20+
- **`@softDelete`:** object-level (a `deletedAt` timestamp mode + read-filter); cascade-soft-delete deferred.
21+
- **`@version`:** field-level `@version: true` (optimistic lock).
22+
- **Gating:** referential actions + `@storage` round-trip in the shared conformance corpus; physical-type escape hatch (`@columnType`) is codegen-only (golden tests).
23+
24+
## Order & rationale
25+
Canonical form first (source v2 + persistence + enforcement), TS-first; sigil-free YAML **last**
26+
(it desugars to the canonical vocab, so that vocab must be final). There is **no canonical
27+
`@`-purge** — canonical JSON keeps `@`; the only `@` work is (a) `ERR_RESERVED_ATTR` enforcement,
28+
which rides source v2 because `@name``@table` is its prerequisite, and (b) the YAML sigil-drop
29+
(last). **C# is held** (gated in its conformance ledger) per decision; do TS → Java → Python.
30+
31+
---
32+
33+
## Stage 1 — Source v2 + persistence in TypeScript (the reference). The backbone.
34+
35+
**1a · Shared corpus migration** (language-agnostic; the contract).
36+
- Migrate every `source.*` fixture: `source.dbTable``source.rdb` (kind defaults to `table`);
37+
`source.dbView``source.rdb` + `@kind: view`; `@name``@table`; field `@dbColumn``@column`.
38+
- Add fixtures: a reserved-word error (`@isArray``ERR_RESERVED_ATTR`); a referential-action
39+
fixture (relationship `@onDelete`/`@onUpdate`); a multi-source `@role` fixture; the no-primary /
40+
multiple-primary error fixtures.
41+
- Regenerate every affected `expected.json` from canonical output. Gate the new/changed fixtures
42+
in the **Java/Python/C#** expected-failures ledgers until each port implements the stage.
43+
44+
**1b · TS loader.** Register `source.rdb` (+ `@kind`/`@role`/`@table`/`@schema` schemas), drop
45+
`dbTable`/`dbView`; multi-source validation (exactly one `primary`); add `@onDelete`/`@onUpdate`
46+
to the relationship schema (`allowedValues = FkAction`); land `ERR_RESERVED_ATTR` (the parser
47+
check is already drafted; safe once `@name` is gone). Conformance green.
48+
49+
**1c · TS codegen/runtime/migrate consumers.** Read-only off `@kind` (not the subtype); source
50+
name off `@table`, field off `@column`; **route by `@role`** (primary = CRUD; index/cache/publish
51+
derived — degrade gracefully when absent). Golden/contract tests green.
52+
53+
**1d · Referential actions (low-effort, high-value).** Thread the relationship `@onDelete`/
54+
`@onUpdate` into the existing `migrate-ts` `FkDescriptor` (the `FkAction` union, `emit/{postgres,
55+
sqlite}.ts`, introspect, diff already exist). Round-trip fixture green.
56+
57+
Each sub-stage: TDD, then `review + simplify`, then merge + push.
58+
59+
## Stage 2 — Java
60+
Loader: `source.rdb` + `@kind`/`@role`/`@table`/`@column`, multi-source validation,
61+
`ERR_RESERVED_ATTR`, `@onDelete`/`@onUpdate`. Normalize the legacy `db*`/`jpa*` vocabulary toward
62+
the spec where it intersects. ObjectManagerDB/migration consumers: read-only via `@kind`, name via
63+
`@table`/`@column`, referential actions into the FK DDL. Un-gate the Stage-1 fixtures. Conformance green.
64+
65+
## Stage 3 — Python
66+
Loader: same source v2 + enforcement + referential schema. Codegen (the `metaobjects.codegen`
67+
package from sub-project A): read-only via `@kind`, field via `@column`. Un-gate the fixtures.
68+
Conformance green. (Python codegen/runtime/persistence is its own larger track — see the Python
69+
foundation roadmap; source v2 lands in its loader + codegen here.)
70+
71+
## Stage 4 — AI-first YAML (ADR-0006 D1–D4), TypeScript-only, LAST.
72+
`parser-yaml` + `yaml-desugar`: sigil-free authoring (closed structural-key set; desugar re-adds
73+
`@` for the now-final source-v2/persistence vocab). D2 type-coercion guard in the schema-validation
74+
pass (reject schema-type-mismatched coerced values; located "quote this" error). D3 house style
75+
(+ optional lint warnings). D4 YAML conformance fixtures (sigil-free attrs + coercion guard);
76+
YAML stays TS-only. TDD on parser-yaml/yaml-desugar/parser-equivalence/validation.
77+
78+
## Stage 5 — Enum datatype (separate; coordinate spelling).
79+
`field.enum` per its design doc; `values:` (YAML) / `@values` (canonical JSON) per ADR-0006 D1.
80+
81+
## C# (held)
82+
Stays on `dbTable`/`dbView` + the legacy vocabulary; the Stage-1 fixtures are gated in its
83+
ledger. Migrate to source v2 as its own effort once the user resumes C#.
84+
85+
## Cross-cutting
86+
TDD throughout; named constants for all metamodel strings (no inline literals); no `any`
87+
(narrow `unknown`); public-repo hygiene (no private names / local paths in committed files);
88+
conformance-gated per port; per-unit `review + simplify` before merge; push to origin for durability.
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Persistence attributes — consolidated cross-language vocabulary (per level, per source subtype)
2+
3+
- **Date:** 2026-05-23
4+
- **Status:** Design — plan-of-record for the full persistence-attribute set.
5+
- **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).
6+
- **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.
7+
8+
> **Notation:** attribute names are shown in their **canonical JSON** spelling (`@`-prefixed: `@column`, `@onDelete`, `@storage`). In **YAML authoring** they're written sigil-free (`column:`, `onDelete:`, …); the desugar re-adds the `@` ([ADR-0006](../../../spec/decisions/ADR-0006-ai-first-yaml-authoring.md)). Reserved structural keys stay bare in both.
9+
10+
## 1. Archaeology — what already exists (and diverges)
11+
12+
| Concept | TS | Java (OMDB) | C# | normalized |
13+
|--|--|--|--|--|
14+
| field physical name | `@dbColumn` | `dbColumn` | `@dbColumn` | **`@column`** (per-subtype: `@field`/`@property`/…) |
15+
| not-null | `@required` | `required`/`dbNullable` | `@required` | **`@required`** |
16+
| max length | `@maxLength` | `dbLength` | `@maxLength` | **`@maxLength`** |
17+
| precision/scale | `@precision`/`@scale` | `dbPrecision`/`dbScale` | `@precision`/`@scale` | **`@precision`/`@scale`** |
18+
| unique | `@unique` | `dbUnique`/`isUnique` | `@unique` | **`@unique`** |
19+
| index | `@db.indexed` | `dbIndex`/`isIndex` | `@db.indexed` | **`@indexed`** |
20+
| default | `@default` | `defaultValue` | `@default` | **`@default`** |
21+
| auto timestamp | `@autoSet: onCreate\|onUpdate` | `auto: create\|update` | `@autoSet` | **`@autoSet`** |
22+
| PK generation | identity `@generation` | identity `generation` + `dbSequenceName` | `@generation` | **`@generation`** (+ `@sequence`) |
23+
| db type override | (`@db.*`) | `dbType` (e.g. jsonb) || **`@columnType`** |
24+
| object storage | `@storage`+`@objectRef` | `dbType=jsonb` | `@storage`+`@objectRef` (EF owned) | **`@storage`+`@objectRef`** |
25+
| rename hint | (migrate) | `previousName` | `@previousName` | **`@previousName`** |
26+
| relationship lifecycle | subtype assoc/aggr/comp | subtype + `jpaCascade` | subtype | subtype **+ explicit `@onDelete`/`@onUpdate`** |
27+
| FK enforcement | (implicit) | `enforce` | (implicit) | **`@enforce`** |
28+
| fetch strategy || `jpaFetch` || **`@fetch`** (eager/lazy) |
29+
| table name | `@name``@table` | `dbTable` | `@name``@table` | **`@table`** (source v2) |
30+
| composite index/unique | (per-field) | object `dbIndex`/`dbUnique` || **source `@indexes`/`@uniques`** |
31+
| concurrency || `dbAllowDirtyWrite`/`dbDirtyWriteCheckField` || **`@version` field** |
32+
| read-only | dbView subtype | `isViewOnly`/`dbView` | dbView subtype | **`@kind`-derived** (source v2) |
33+
34+
**Normalization rules:** drop the `db`/`jpa` prefixes and the `@db.*` namespace (legacy Java/TS) — they fragment one concept across spellings. The *paradigm* is already carried by the source subtype, so attrs are bare and paradigm-neutral where the concept generalizes, and per-subtype only where the concept genuinely differs (physical name).
35+
36+
## 2. Field-level attributes (`field.*`)
37+
38+
| attr | values | applies to | meaning |
39+
|--|--|--|--|
40+
| `@column` / `@field` / `@property` / … | string | all | physical address in the record (per source subtype; §source-v2) |
41+
| `@required` | bool | all | NOT NULL / required |
42+
| `@maxLength` | int | string | varchar(N) / max length |
43+
| `@precision` / `@scale` | int | decimal | numeric(p,s) |
44+
| `@default` | literal \| sqlExpr (`now`, `CURRENT_TIMESTAMP`, `uuid()`) | all | column default |
45+
| `@unique` | bool | all | single-column unique |
46+
| `@indexed` | bool \| index-name | all | create an index |
47+
| `@autoSet` | `onCreate` \| `onUpdate` | date/timestamp | auto-managed timestamp (createdAt/updatedAt) |
48+
| `@columnType` | string | all | escape hatch: explicit physical type (e.g. `jsonb`, `citext`) |
49+
| `@storage` | `flattened` \| `jsonb` \| `subdocument` | object (`@objectRef`) | nested-object storage strategy (see §6) |
50+
| `@objectRef` | object name | object | the nested/referenced object |
51+
| `@filterable` / `@sortable` | bool | all | query-layer (Project D) |
52+
| `@previousName` | string | all | migration rename hint |
53+
54+
## 3. Identity-level attributes (`identity.*`)
55+
56+
| attr | values | meaning |
57+
|--|--|--|
58+
| `@fields` | string[] | the field(s) forming the identity |
59+
| `@generation` | `increment` \| `uuid` \| `assigned` \| `sequence` \| `identity` | PK generation strategy |
60+
| `@sequence` | string | sequence name (when `@generation: sequence`) |
61+
| `@unique` | bool | secondary identity ⇒ unique constraint |
62+
63+
Subtypes: `primary` (one; the PK), `secondary` (business/alt keys), **`reference`** (FK fields → another entity; from Java) with `@references` (target `Entity`/`Entity.id`) + `@enforce` (physical FK on/off).
64+
65+
## 4. Relationship-level attributes (`relationship.*`) — incl. the new referential actions
66+
67+
Subtypes carry **default** lifecycle; explicit attrs **override**:
68+
69+
| attr | values | default (from subtype) | meaning |
70+
|--|--|--|--|
71+
| `@objectRef` | object name || target entity |
72+
| `@cardinality` | `one` \| `many` || relationship cardinality |
73+
| **`@onDelete`** | `cascade` \| `set-null` \| `restrict` \| `no-action` | composition→`cascade`, aggregation→`set-null`, association→`restrict` | **referential action on parent delete (NEW)** |
74+
| **`@onUpdate`** | (same `FkAction` set) | `cascade` | **referential action on key update (NEW)** |
75+
| `@enforce` | bool | `true` | physical FK constraint vs logical-only |
76+
| `@fetch` | `eager` \| `lazy` | `lazy` | load strategy (codegen/runtime hint) |
77+
| `@joinEntity` / `@joinFields` | string / string[] || N:M join table |
78+
79+
**The value set is the existing `FkAction` union — `cascade | set-null | restrict | no-action`** ([migrate-ts `types.ts:65`](../../../server/typescript/packages/migrate-ts/src/types.ts)), kebab-case, **no `setDefault`** (the emitters don't carry it; MySQL doesn't support it). The authoring value === the `FkDescriptor` value, so it threads straight through with no translation layer.
80+
81+
**`@onDelete` and `@autoSet` are different axes on different metatypes — do NOT unify them.** `@autoSet: onCreate|onUpdate` is a one-off *field* write-fill on a timestamp column; `@onDelete`/`@onUpdate` are *relationship* referential actions. There is no general "behavior on write" infrastructure and we are not inventing one — folding these into a single lifecycle hook would be over-engineering.
82+
83+
**Defaults-from-subtype** keep the common case zero-config (a `composition` cascades; an `association` restricts) while `@onDelete`/`@onUpdate` expose and override that intent. Note this is a *small new inference* — today the metadata→schema side leaves the action unset (FKs emit no action clause); we add the derive-from-subtype default + the explicit override. Caveat: `set-null` requires an optional (nullable) relation; `verify`/migrate flags violations.
84+
85+
## 5. Source/object-level attributes
86+
87+
| attr | level | meaning |
88+
|--|--|--|
89+
| `@table`/`@collection`/… | source | physical object name (source v2) |
90+
| `@schema` | source.rdb | DB schema/namespace |
91+
| `@kind` | source | object kind; drives read-only (source v2) |
92+
| `@role` | source | multi-source role (source v2) |
93+
| `@indexes` | source/object | composite indexes (multi-column) |
94+
| `@uniques` | source/object | composite unique constraints |
95+
| `@version` | field/object | optimistic-lock version column (replaces Java dirty-write) |
96+
| `@softDelete` | object | soft-delete mode: a `deletedAt` timestamp (or `deleted` flag) + read-filter; distinct from DB `@onDelete` |
97+
98+
## 6. Object storage (`@storage`) — answer to "the C# jsonb thing"
99+
100+
On a `field.object` (with `@objectRef`):
101+
102+
| `@storage` | relational (rdb) | document | meaning |
103+
|--|--|--|--|
104+
| `flattened` | nested columns expand into the parent table, prefixed (EF Core `OwnsOne`); `isArray` forbidden | n/a | owned/embedded, one set of columns |
105+
| `jsonb` | a single `jsonb` column holds the structured value (or array when `isArray`) | native nested | typed-by-metadata, opaque storage |
106+
| `subdocument` | (no relational column emitted) | native nested document | document-store-native |
107+
108+
Absent ⇒ default single-jsonb-column (back-compat). Validation: `flattened` + `isArray``ERR_STORAGE_FLATTENED_ARRAY`; `@storage` without `@objectRef``ERR_STORAGE_WITHOUT_OBJECT_REF` (both exist).
109+
110+
## 7. Per-source-subtype applicability
111+
112+
Most attrs are **rdb**-centric. Applicability deltas:
113+
114+
| attr group | rdb | document | event | keyValue | graph | search | vector | timeSeries |
115+
|--|--|--|--|--|--|--|--|--|
116+
| `@required`/`@default`/`@unique` ||| (schema) ||||||
117+
| `@maxLength`/`@precision`/`@scale` |||||||||
118+
| `@indexed`/`@indexes`/`@uniques` |||| `@gsi` || (mapping) |||
119+
| `@autoSet` ||| (event time) ||||| `@timeColumn` |
120+
| `@generation` || ✓ (`_id`) || `@partitionKey` |||||
121+
| `@onDelete`/`@onUpdate`/`@enforce` || (app-level) ||| ✓ (edges) ||||
122+
| `@storage` | ✓ (jsonb/flattened) | subdocument |||||||
123+
| `@version`/`@softDelete` |||||||||
124+
| paradigm-specific | `@schema` | `@database` | `@keySchema`/`@valueSchema`/`@partitions` | `@partitionKey`/`@sortKey`/`@ttl` | `@from`/`@to` | `@mappingsRef` | `@dimensions`/`@metric` | `@retention`/`@tags` |
125+
126+
## 8. What's new vs. today (the gaps we're filling)
127+
128+
1. **Explicit `@onDelete`/`@onUpdate` referential actions****low effort, high value: the plumbing already exists end-to-end and only the authoring attribute is missing.** The `FkAction` union (`cascade|set-null|restrict|no-action`), `FkDescriptor.onDelete/onUpdate`, the DDL **emit** (`migrate-ts/src/emit/{postgres,sqlite}.ts`), **introspect**, and **diff** are all built (TS confirmed; C# `PostgresEmit.cs` per the same design). What's absent is the `@onDelete`/`@onUpdate` attr on the relationship schema and threading its value into the expected-schema `FkDescriptor` (today that field is left unset → FKs emit no action clause). So the task is: add the two attrs (`allowedValues = FkAction`), thread them in, add a conformance fixture. Exposing intent, not building plumbing.
129+
2. **`@enforce`** (physical vs logical FK) and **`@fetch`** promoted from Java-only to the cross-language vocabulary.
130+
3. **`@softDelete`** and **`@version`** as first-class declarations (today: ad-hoc / Java-only dirty-write).
131+
4. **`@generation: sequence`/`identity`** + `@sequence` (Java had `dbSequenceName`; generalize).
132+
5. **Normalized names** — Java `db*`/`jpa*` and TS `@db.*` collapse into the bare cross-language set above.
133+
134+
## 9. Open questions
135+
136+
- `@onDelete` default for **aggregation**`set-null` vs `restrict`? (Prisma defaults required→restrict; our aggregation = shared ownership ⇒ `set-null` feels right but needs the relation optional.) Resolve in review.
137+
- `@softDelete` granularity — object-level mode only, or per-relationship cascade-of-soft-delete? (Start object-level; defer cascade-soft-delete.)
138+
- `@version` representation — a dedicated `@version` field marker vs an object-level `@optimisticLock: <field>`. (Lean: a field-level `@version: true`.)
139+
- Which attrs are **conformance-gated** (loader/serializer round-trip) vs **codegen-only** (golden tests). Referential actions + storage round-trip in metadata; physical-type escape hatches are codegen-only.
140+
141+
## 10. Research
142+
143+
- Prisma referential actions: `Cascade`/`Restrict`/`NoAction`/`SetNull`/`SetDefault`; defaults ON DELETE RESTRICT (required) / ON UPDATE CASCADE; `SetNull` needs optional relation; `SetDefault` unsupported on MySQL. ([Prisma referential actions](https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/referential-actions)) — **we adopt the existing 4-value `FkAction` (`cascade|set-null|restrict|no-action`), omitting `setDefault`** to match what the emitters already carry.
144+
- Soft delete is an application/middleware pattern (`deletedAt`/`deleted` + read-filter), not a DB referential action. ([Prisma soft-delete](https://www.prisma.io/docs/orm/prisma-client/client-extensions/middleware/soft-delete-middleware))
145+
- JPA cascade types (ALL/PERSIST/MERGE/REMOVE/…) + fetch (EAGER/LAZY) informed `@onDelete`/`@fetch` (Java `jpaCascade`/`jpaFetch`).

0 commit comments

Comments
 (0)