diff --git a/docs/plans/2026-05-10-design-md-consolidation-3-schema-sot-inversion.md b/docs/plans/2026-05-10-design-md-consolidation-3-schema-sot-inversion.md new file mode 100644 index 0000000..76a62a3 --- /dev/null +++ b/docs/plans/2026-05-10-design-md-consolidation-3-schema-sot-inversion.md @@ -0,0 +1,947 @@ +# design-md-consolidation iter 3 — sweep 3: schema SoT inversion + data-model hardening + +> **Parent spec:** `docs/specs/2026-05-10-design-md-consolidation.md` +> §"Sweep 3 — Schema SoT inversion + Data-model hardening". +> +> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement` +> to run this plan. Steps use `- [ ]` checkboxes for tracking. + +**Goal:** Invert the schema source-of-truth relationship between +`docs/DESIGN.md` §"Data model" and `crates/ailang-core/src/ast.rs` +so DESIGN.md is canonical and `ast.rs` follows; remove the Rust +type definitions inside DESIGN.md (which made it look secondary); +add a drift test that fails fast when an `ast.rs` enum variant +loses its DESIGN.md anchor. + +**Architecture:** Three-substance refactor + one test. (1) Two +inline Rust code blocks inside Decision 10 are replaced with +prose pointers to §Data-model; (2) the §Data-model opener loses +the "ast.rs is the source of truth" disclaimer and gains a +"DESIGN.md is canonical" sentence; (3) ast.rs's module doc-comment +is updated to name DESIGN.md §"Data model" as canonical and to +name the new drift test as the enforcement; (4) a new test file +`crates/ailang-core/tests/design_schema_drift.rs` (analogous to +the existing `spec_drift.rs`) walks exhaustive matches over +`Term` / `Pattern` / `Type` / `Def` / `Literal` / `ParamMode` and +asserts a JSON-schema anchor for each variant exists in DESIGN.md. + +**Tech Stack:** Edit (`docs/DESIGN.md`, +`crates/ailang-core/src/ast.rs`); Write +(`crates/ailang-core/tests/design_schema_drift.rs`); cargo test. + +**Files this plan creates or modifies:** +- Modify: `docs/DESIGN.md` — Decision 10 Rust code blocks removed + (Task 1); §Data-model opener inverted (Task 2). +- Modify: `crates/ailang-core/src/ast.rs` — module-level doc-comment + updated (Task 3). +- Create: `crates/ailang-core/tests/design_schema_drift.rs` — + drift test with exhaustive match per enum (Task 4). +- Append: `docs/JOURNAL.md` — iter entry (Task 5). +- Modify: `docs/roadmap.md` — Sweep 3 `[ ]` → `[x]` (Task 5). + +--- + +## Task 1: Remove Rust code blocks from Decision 10 + +**Files:** Modify `docs/DESIGN.md` (2 sites: Rust code block opening +at line ~1027 and at line ~1132 — line numbers may have shifted by +prior tasks; re-locate at Step 1). + +The two Rust code blocks inside Decision 10 illustrate the schema +in Rust — but DESIGN.md's job is to specify JSON. The Rust form is +in `ast.rs` (sweep-3 inversion makes that explicit). Each code +block is replaced with a one-line pointer to §Data-model. + +- [ ] **Step 1: Pre-test — locate the code blocks** + +```bash +grep -n '^```rust' docs/DESIGN.md +``` + +Expected: 2 matches. + +```bash +grep -nE '^\s*(struct |enum |pub (struct|enum|fn))' docs/DESIGN.md +``` + +Expected: 2 matches (the `enum ParamMode` and `struct Suppress` +inside the code blocks). + +- [ ] **Step 2: Replace the first Rust code block (Type::Fn + ParamMode)** + +The block is preceded by the prose `Internally, this is *not* a new +\`Type\` variant. Modes are metadata on \`Type::Fn\`:` and is +followed by the prose `The substantive reasons for per-position +metadata over a \`Type::Borrow\` / \`Type::Own\` variant approach:`. + +Edit: +- old: +``` +Internally, this is *not* a new `Type` variant. Modes are +metadata on `Type::Fn`: + +```rust +Type::Fn { + params: Vec, + param_modes: Vec, // same length as params + ret: Box, + ret_mode: ParamMode, + effects: Vec, +} + +enum ParamMode { Implicit, Own, Borrow } // default: Implicit +``` + +The substantive reasons for per-position metadata over a +``` +- new: +``` +Internally, this is *not* a new `Type` variant. Modes are +metadata on `Type::Fn` — `paramModes` and `retMode` fields run +parallel to `params` and `ret` (see §"Data model" for the JSON +schema). The substantive reasons for per-position metadata over a +``` + +Verification: the surrounding paragraph reads coherently after the +substitution; the `ParamMode` mention now flows into the §"Data +model" pointer, and the next sentence (`Type::Borrow` / `Type::Own` +contrast) continues seamlessly. + +- [ ] **Step 3: Replace the second Rust code block (Suppress)** + +The block is preceded by the prose `**\`FnDef.suppress\`.**` (a bold +header on its own line) and is followed by the prose `Form-A +surface: \`(suppress (code "...") (because "..."))\` clause`. + +Edit: +- old: +``` +**`FnDef.suppress`.** + +```rust +FnDef.suppress: Vec ; advisory diagnostic suppress list + +struct Suppress { + code: String, // diagnostic code being suppressed + because: String, // mandatory non-empty reason +} +``` + +Form-A surface: `(suppress (code "...") (because "..."))` clause +``` +- new: +``` +**`FnDef.suppress`.** The `suppress` field on `FnDef` carries a +list of advisory-diagnostic suppress entries; each entry has a +`code` (the diagnostic being suppressed) and a `because` (a +mandatory non-empty reason). See §"Data model" for the canonical +schema. + +Form-A surface: `(suppress (code "...") (because "..."))` clause +``` + +- [ ] **Step 4: Post-test** + +```bash +grep -n '^```rust' docs/DESIGN.md +``` + +Expected: empty. + +```bash +grep -nE '^\s*(struct |enum |pub (struct|enum|fn))' docs/DESIGN.md +``` + +Expected: empty. + +- [ ] **Step 5: Commit** + +```bash +git add docs/DESIGN.md +git commit -m "design-md-consolidation 3.1: remove 2 Rust code blocks from Decision 10 (Type::Fn + Suppress) — schema lives in §Data-model" +``` + +--- + +## Task 2: Invert §Data-model SoT relationship + +**Files:** Modify `docs/DESIGN.md` (lines ~1722-1730 at iter start +in the original file; shifted by prior sweeps and Task 1; re-locate +at Step 1). + +The §Data-model opener currently reads: + +> The on-disk JSON-AST is what the toolchain hashes, typechecks, and +> lowers. Every node in this section is the schema mirror of an enum or +> struct in `crates/ailang-core/src/ast.rs`; whenever the two disagree, +> `ast.rs` is the source of truth. Every additive field is declared with +> `skip_serializing_if` so pre-existing fixtures keep bit-identical +> canonical-JSON hashes — that gating contract is what makes growing +> the schema cheap. + +Two issues: +1. "schema mirror of an enum or struct in ast.rs" frames ast.rs as + primary. +2. "whenever the two disagree, ast.rs is the source of truth" makes + the SoT inversion explicit in the wrong direction. + +The inverted opener: + +- [ ] **Step 1: Pre-test — locate the disclaimer** + +```bash +grep -n 'whenever the two disagree' docs/DESIGN.md +``` + +Expected: 1 match. + +```bash +grep -n '^## Data model' docs/DESIGN.md +``` + +Expected: 1 match (around line 1722 in original numbering). + +- [ ] **Step 2: Replace the opener** + +Edit: +- old: +``` +## Data model + +The on-disk JSON-AST is what the toolchain hashes, typechecks, and +lowers. Every node in this section is the schema mirror of an enum or +struct in `crates/ailang-core/src/ast.rs`; whenever the two disagree, +`ast.rs` is the source of truth. Every additive field is declared with +`skip_serializing_if` so pre-existing fixtures keep bit-identical +canonical-JSON hashes — that gating contract is what makes growing +the schema cheap. +``` +- new: +``` +## Data model + +The on-disk JSON-AST is what the toolchain hashes, typechecks, and +lowers. **This section is the canonical schema.** The Rust types in +`crates/ailang-core/src/ast.rs` are the in-memory projection of it; +when the two disagree, this section wins, and the drift test +`crates/ailang-core/tests/design_schema_drift.rs` fires. Every +additive field is declared with `skip_serializing_if` so pre-existing +fixtures keep bit-identical canonical-JSON hashes — that gating +contract is what makes growing the schema cheap. +``` + +The new opener: +- States DESIGN.md is canonical (the prominent sentence the spec + asks for, in bold). +- Names ast.rs as the projection, not the source. +- Cites the drift test as the enforcement mechanism. +- Preserves the `skip_serializing_if` gating-contract sentence + unchanged. + +- [ ] **Step 3: Post-test** + +```bash +grep -n 'whenever the two disagree' docs/DESIGN.md +``` + +Expected: empty. + +```bash +grep -n 'This section is the canonical schema' docs/DESIGN.md +``` + +Expected: 1 match. + +```bash +grep -n 'design_schema_drift.rs' docs/DESIGN.md +``` + +Expected: 1 match (the new prose pointer; the test file does not +yet exist in the tree but the prose anchors it for Task 4). + +- [ ] **Step 4: Commit** + +```bash +git add docs/DESIGN.md +git commit -m "design-md-consolidation 3.2: invert §Data-model SoT — DESIGN.md canonical, ast.rs projection, drift test enforces" +``` + +--- + +## Task 3: Update ast.rs module doc-comment + +**Files:** Modify `crates/ailang-core/src/ast.rs` lines 1-17 (the +file-level `//!` doc-comment block). + +The current doc-comment names DESIGN.md but does not establish the +SoT relationship. + +- [ ] **Step 1: Pre-test — read the current doc-comment** + +```bash +sed -n '1,17p' crates/ailang-core/src/ast.rs +``` + +Expected: a 17-line `//!` block opening with `//! AST nodes for the +AILang language.` and naming DESIGN.md as the documentation source. + +- [ ] **Step 2: Replace the doc-comment** + +Edit `crates/ailang-core/src/ast.rs`: +- old: +```rust +//! AST nodes for the AILang language. +//! +//! Every type in this module is the in-memory mirror of a node in the +//! AILang JSON schema documented in `docs/DESIGN.md`. The serde +//! attributes carry the schema: field renames (`as`, `type`, `fn`, +//! `paramTypes`, `retType`), enum tags (`kind`, `t`, `k`, `p`), and +//! `skip_serializing_if` predicates that keep the canonical-JSON +//! representation backwards compatible across schema extensions. +//! +//! The entry type is [`Module`]. The two helpers [`def_name`] and +//! [`def_kind`] are intended for tools (`ail diff`, `ail manifest`) that +//! consume a [`Def`] without going through method calls. +//! +//! This module does **not** typecheck, evaluate, or hash anything — see +//! [`crate::canonical`] for canonical bytes, [`crate::hash`] for content +//! hashes, and the `ailang-check` crate for typechecking. +``` +- new: +```rust +//! AST nodes for the AILang language. +//! +//! **The canonical schema lives in `docs/DESIGN.md` §"Data model"**; +//! this module is the Rust-side projection of it. When the two drift, +//! `crates/ailang-core/tests/design_schema_drift.rs` fires. +//! +//! The serde attributes carry the schema: field renames (`as`, `type`, +//! `fn`, `paramTypes`, `retType`), enum tags (`kind`, `t`, `k`, `p`), +//! and `skip_serializing_if` predicates that keep the canonical-JSON +//! representation backwards compatible across schema extensions. +//! +//! The entry type is [`Module`]. The two helpers [`def_name`] and +//! [`def_kind`] are intended for tools (`ail diff`, `ail manifest`) that +//! consume a [`Def`] without going through method calls. +//! +//! This module does **not** typecheck, evaluate, or hash anything — see +//! [`crate::canonical`] for canonical bytes, [`crate::hash`] for content +//! hashes, and the `ailang-check` crate for typechecking. +``` + +The new doc-comment: +- Bold-emphasises that DESIGN.md §"Data model" is canonical. +- Names ast.rs as the projection, not the source. +- Cites the drift test as the enforcement mechanism. +- Preserves the rest of the doc-comment unchanged (serde-attribute + description, helper-function pointer, separation-of-concerns + callout). + +- [ ] **Step 3: Verify rustdoc still builds** + +```bash +cargo doc -p ailang-core --no-deps 2>&1 | tail -10 +``` + +Expected: clean build, no warnings introduced by the doc-comment +edit (note: any pre-existing warnings unrelated to this edit are +acceptable; the edit must not add new ones). + +- [ ] **Step 4: Commit** + +```bash +git add crates/ailang-core/src/ast.rs +git commit -m "design-md-consolidation 3.3: ast.rs doc-comment names DESIGN.md §Data-model as canonical schema, drift test as enforcement" +``` + +--- + +## Task 4: Add `design_schema_drift.rs` (RED-first) + +**Files:** Create `crates/ailang-core/tests/design_schema_drift.rs`. + +Per the implement-skill TDD discipline, the test is written +RED-first: deliberately remove one anchor temporarily so the test +fails, verify the failure message is precise, then restore. + +In practice for a guard test on already-aligned content, the test +will be GREEN immediately on first run. The "RED first" step here +is a single deliberate-corruption check: create the test, run it +(should be GREEN), then temporarily corrupt one anchor in +`docs/DESIGN.md` (e.g. change `"t": "lit"` to `"t": "litX"`), run +again (should be RED with a precise failure message), then revert +the corruption — the deliberate-corruption step is performed only +to verify the test's RED behaviour is correct, then immediately +reverted before commit. + +- [ ] **Step 1: Write the test file** + +Create `crates/ailang-core/tests/design_schema_drift.rs` with the +following content: + +```rust +//! Drift detection between the AST in this crate and the canonical +//! schema in `docs/DESIGN.md` §"Data model". +//! +//! Each `ast.rs` enum (`Term`, `Pattern`, `Type`, `Def`, `Literal`, +//! `ParamMode`) discriminates on a `#[serde(rename = "...")]` tag. +//! These tests construct a sample of every variant, then check that +//! the corresponding JSON-schema anchor (e.g. `"t": "lit"`, +//! `"kind": "fn"`, `"p": "wild"`, `"k": "con"`) appears in DESIGN.md. +//! +//! The exhaustive `match` is the load-bearing piece: adding a new +//! variant without a DESIGN.md entry fails compilation here long +//! before the test runs. Once the variant is matched, the test +//! asserts DESIGN.md mentions it. +//! +//! Pair with `tests/spec_drift.rs` (Form-A surface) — the two cover +//! orthogonal axes: spec_drift checks the Form-A authoring surface, +//! design_schema_drift checks the JSON canonical schema. + +use ailang_core::ast::{ + ClassDef, ClassMethod, ConstDef, Constraint, Ctor, Def, FnDef, InstanceDef, + InstanceMethod, Literal, ParamMode, Pattern, Suppress, Term, Type, TypeDef, +}; + +const DESIGN_MD: &str = include_str!("../../../docs/DESIGN.md"); + +/// Every `Term` variant must have its JSON-schema anchor in DESIGN.md. +#[test] +fn design_md_anchors_every_term_variant() { + let exemplars: Vec<(&str, Term)> = vec![ + ("\"t\": \"lit\"", Term::Lit { lit: Literal::Unit }), + ("\"t\": \"var\"", Term::Var { name: "x".into() }), + ( + "\"t\": \"app\"", + Term::App { + callee: Box::new(Term::Var { name: "f".into() }), + args: vec![], + tail: false, + }, + ), + ( + "\"t\": \"let\"", + Term::Let { + name: "x".into(), + value: Box::new(Term::Lit { lit: Literal::Int { value: 1 } }), + body: Box::new(Term::Var { name: "x".into() }), + }, + ), + ( + "\"t\": \"letrec\"", + Term::LetRec { + name: "f".into(), + ty: Type::fn_implicit(vec![], Type::int(), vec![]), + params: vec![], + body: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }), + in_term: Box::new(Term::Var { name: "f".into() }), + }, + ), + ( + "\"t\": \"if\"", + Term::If { + cond: Box::new(Term::Lit { lit: Literal::Bool { value: true } }), + then: Box::new(Term::Lit { lit: Literal::Int { value: 1 } }), + else_: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }), + }, + ), + ( + "\"t\": \"do\"", + Term::Do { + op: "io/print_int".into(), + args: vec![], + tail: false, + }, + ), + ( + "\"t\": \"ctor\"", + Term::Ctor { + type_name: "List".into(), + ctor: "Nil".into(), + args: vec![], + }, + ), + ( + "\"t\": \"match\"", + Term::Match { + scrutinee: Box::new(Term::Var { name: "x".into() }), + arms: vec![], + }, + ), + ( + "\"t\": \"lam\"", + Term::Lam { + params: vec![], + param_tys: vec![], + ret_ty: Box::new(Type::int()), + effects: vec![], + body: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }), + }, + ), + ( + "\"t\": \"seq\"", + Term::Seq { + lhs: Box::new(Term::Var { name: "a".into() }), + rhs: Box::new(Term::Var { name: "b".into() }), + }, + ), + ( + "\"t\": \"clone\"", + Term::Clone { + value: Box::new(Term::Var { name: "x".into() }), + }, + ), + ( + "\"t\": \"reuse-as\"", + Term::ReuseAs { + source: Box::new(Term::Var { name: "x".into() }), + body: Box::new(Term::Var { name: "y".into() }), + }, + ), + ]; + + for (anchor, term) in exemplars { + let _: &'static str = match term { + Term::Lit { .. } => "lit", + Term::Var { .. } => "var", + Term::App { .. } => "app", + Term::Let { .. } => "let", + Term::LetRec { .. } => "letrec", + Term::If { .. } => "if", + Term::Do { .. } => "do", + Term::Ctor { .. } => "ctor", + Term::Match { .. } => "match", + Term::Lam { .. } => "lam", + Term::Seq { .. } => "seq", + Term::Clone { .. } => "clone", + Term::ReuseAs { .. } => "reuse-as", + }; + assert!( + DESIGN_MD.contains(anchor), + "DESIGN.md missing anchor `{anchor}` for a Term variant — \ + update docs/DESIGN.md §\"Data model\"" + ); + } +} + +/// Every `Pattern` variant must have its JSON-schema anchor in DESIGN.md. +#[test] +fn design_md_anchors_every_pattern_variant() { + let exemplars: Vec<(&str, Pattern)> = vec![ + ("\"p\": \"wild\"", Pattern::Wild), + ("\"p\": \"var\"", Pattern::Var { name: "x".into() }), + ("\"p\": \"lit\"", Pattern::Lit { lit: Literal::Int { value: 0 } }), + ( + "\"p\": \"ctor\"", + Pattern::Ctor { ctor: "Nil".into(), fields: vec![] }, + ), + ]; + for (anchor, pat) in exemplars { + let _: &'static str = match pat { + Pattern::Wild => "wild", + Pattern::Var { .. } => "var", + Pattern::Lit { .. } => "lit", + Pattern::Ctor { .. } => "ctor", + }; + assert!( + DESIGN_MD.contains(anchor), + "DESIGN.md missing anchor `{anchor}` for a Pattern variant" + ); + } +} + +/// Every `Type` variant must have its JSON-schema anchor in DESIGN.md. +#[test] +fn design_md_anchors_every_type_variant() { + let exemplars: Vec<(&str, Type)> = vec![ + ("\"k\": \"con\"", Type::int()), + ("\"k\": \"fn\"", Type::fn_implicit(vec![], Type::unit(), vec![])), + ("\"k\": \"var\"", Type::Var { name: "a".into() }), + ( + "\"k\": \"forall\"", + Type::Forall { + vars: vec!["a".into()], + constraints: vec![], + body: Box::new(Type::Var { name: "a".into() }), + }, + ), + ]; + for (anchor, ty) in exemplars { + let _: &'static str = match ty { + Type::Con { .. } => "con", + Type::Fn { .. } => "fn", + Type::Var { .. } => "var", + Type::Forall { .. } => "forall", + }; + assert!( + DESIGN_MD.contains(anchor), + "DESIGN.md missing anchor `{anchor}` for a Type variant" + ); + } +} + +/// Every `Literal` variant must have its JSON-schema anchor in DESIGN.md. +#[test] +fn design_md_anchors_every_literal_variant() { + let exemplars: Vec<(&str, Literal)> = vec![ + ("\"kind\": \"int\"", Literal::Int { value: 0 }), + ("\"kind\": \"bool\"", Literal::Bool { value: true }), + ("\"kind\": \"str\"", Literal::Str { value: "x".into() }), + ("\"kind\": \"unit\"", Literal::Unit), + ]; + for (anchor, lit) in exemplars { + let _: &'static str = match lit { + Literal::Int { .. } => "int", + Literal::Bool { .. } => "bool", + Literal::Str { .. } => "str", + Literal::Unit => "unit", + }; + assert!( + DESIGN_MD.contains(anchor), + "DESIGN.md missing anchor `{anchor}` for a Literal variant" + ); + } +} + +/// Every `Def` variant (kind) must have its JSON-schema anchor in DESIGN.md. +#[test] +fn design_md_anchors_every_def_kind() { + let fn_def = FnDef { + name: "f".into(), + doc: None, + suppress: vec![], + ty: Type::fn_implicit(vec![], Type::int(), vec![]), + params: vec![], + body: Term::Lit { lit: Literal::Int { value: 0 } }, + }; + let const_def = ConstDef { + name: "k".into(), + doc: None, + ty: Type::int(), + value: Term::Lit { lit: Literal::Int { value: 0 } }, + }; + let type_def = TypeDef { + name: "T".into(), + doc: None, + vars: vec![], + ctors: vec![Ctor { name: "C".into(), fields: vec![] }], + drop_iterative: false, + }; + let class_def = ClassDef { + name: "C".into(), + param: "a".into(), + superclass: None, + methods: vec![], + doc: None, + }; + let instance_def = InstanceDef { + class: "C".into(), + type_: Type::int(), + methods: vec![], + doc: None, + }; + + let exemplars: Vec<(&str, Def)> = vec![ + ("\"kind\": \"fn\"", Def::Fn(fn_def)), + ("\"kind\": \"const\"", Def::Const(const_def)), + ("\"kind\": \"type\"", Def::Type(type_def)), + ("\"kind\": \"ClassDef\"", Def::Class(class_def)), + ("\"kind\": \"InstanceDef\"", Def::Instance(instance_def)), + ]; + + for (anchor, def) in exemplars { + let _: &'static str = match def { + Def::Fn(_) => "fn", + Def::Const(_) => "const", + Def::Type(_) => "type", + Def::Class(_) => "class", + Def::Instance(_) => "instance", + }; + assert!( + DESIGN_MD.contains(anchor), + "DESIGN.md missing anchor `{anchor}` for a Def variant" + ); + } +} + +/// Every `ParamMode` variant must have its JSON-schema anchor in DESIGN.md. +#[test] +fn design_md_anchors_every_parammode_variant() { + // ParamMode appears in DESIGN.md as bare quoted strings inside + // the ParamMode enumeration block (`"implicit"`, `"own"`, + // `"borrow"`). + let exemplars: Vec<(&str, ParamMode)> = vec![ + ("\"implicit\"", ParamMode::Implicit), + ("\"own\"", ParamMode::Own), + ("\"borrow\"", ParamMode::Borrow), + ]; + for (anchor, mode) in exemplars { + let _: &'static str = match mode { + ParamMode::Implicit => "implicit", + ParamMode::Own => "own", + ParamMode::Borrow => "borrow", + }; + assert!( + DESIGN_MD.contains(anchor), + "DESIGN.md missing anchor `{anchor}` for a ParamMode variant" + ); + } +} + +/// Nested struct types (Suppress, ClassMethod, InstanceMethod, +/// Constraint) are reachable through their parent enum variants. This +/// test forces the constructors so adding a new field that breaks the +/// constructor signature surfaces as a compile error in this file +/// (alongside the variant-anchor tests above). +#[test] +fn design_md_anchors_nested_struct_keys() { + // Suppress: the schema uses "code" and "because" keys. + let _ = Suppress { + code: "x".into(), + because: "y".into(), + }; + assert!(DESIGN_MD.contains("\"code\"")); + assert!(DESIGN_MD.contains("\"because\"")); + + // ClassMethod / InstanceMethod / Constraint: shape-only, no + // direct anchors here. The Decision 11 §"Form-A schema" section + // anchors them via "methods" / "constraints" keys. + let _ = ClassMethod { + name: "show".into(), + ty: Type::fn_implicit(vec![Type::Var { name: "a".into() }], Type::int(), vec![]), + default: None, + }; + let _ = InstanceMethod { + name: "show".into(), + body: Term::Lit { lit: Literal::Int { value: 0 } }, + }; + let _ = Constraint { + class: "Show".into(), + type_: "a".into(), + }; + assert!(DESIGN_MD.contains("\"methods\"")); + assert!(DESIGN_MD.contains("\"constraints\"")); +} +``` + +Notes for the implementer: +- Field names (`callee` vs `fn`, `type_name` vs `type`, `in_term` + vs `in`, `ret_ty` vs `ret`, `param_tys` vs `paramTypes`) follow + ast.rs's Rust naming, not the JSON tags. The serde attributes + on ast.rs handle the JSON ↔ Rust mapping. +- Constructor signatures must match ast.rs exactly. If the + compiler complains about a mismatched field, read ast.rs and + align. +- The `Type::int()`, `Type::unit()`, `Type::fn_implicit(...)` + helpers are public ast.rs constructors used by the existing + spec_drift.rs; reuse them. +- `ClassDef.methods` and `InstanceDef.methods` use the names + `methods` (verify in ast.rs lines 258-340 region). + +- [ ] **Step 2: Run the test (expect GREEN — DESIGN.md already aligned)** + +```bash +cargo test -p ailang-core --test design_schema_drift 2>&1 | tail -20 +``` + +Expected: 7 tests passed (one per variant family + nested-struct +test). + +If RED: the failure message must precisely name the missing +anchor. Read the message; fix DESIGN.md by adding the missing +JSON-schema anchor in §"Data model"; re-run; iterate until GREEN. + +- [ ] **Step 3: Deliberate-RED probe (optional, may skip if Step 2 GREEN on first run)** + +To verify the RED behaviour is correct (test fails clearly when an +anchor goes missing), the implementer MAY temporarily corrupt one +anchor in `docs/DESIGN.md` (e.g. change `"t": "lit"` to +`"t": "litX"`), re-run the test, observe a precise failure message +naming the missing anchor, then revert the corruption immediately. +Do not commit the corrupted state. This is purely a sanity check +on the test's RED behaviour. + +- [ ] **Step 4: Run full workspace test sweep** + +```bash +cargo test --workspace 2>&1 | grep -E "test result:|FAILED" | grep -v "0 failed" | head -10 +``` + +Expected: empty (no FAILED lines). + +- [ ] **Step 5: Commit** + +```bash +git add crates/ailang-core/tests/design_schema_drift.rs +git commit -m "design-md-consolidation 3.4: add design_schema_drift.rs — exhaustive-match drift test for ast.rs vs DESIGN.md §Data-model" +``` + +--- + +## Task 5: Final validation + JOURNAL entry + roadmap update + +**Files:** Read: `docs/DESIGN.md`, +`crates/ailang-core/src/ast.rs`, +`crates/ailang-core/tests/design_schema_drift.rs`. Append: +`docs/JOURNAL.md`. Modify: `docs/roadmap.md`. + +- [ ] **Step 1: Composite acceptance grep** + +```bash +grep -nE '^\s*(struct |enum |pub (struct|enum|fn))' docs/DESIGN.md +``` + +Expected: empty (no Rust type definitions remain). + +```bash +grep -n 'whenever the two disagree\|ast.rs is the source of truth' docs/DESIGN.md +``` + +Expected: empty (the SoT-inverted disclaimer is gone). + +```bash +grep -n 'This section is the canonical schema' docs/DESIGN.md +``` + +Expected: 1 match (the prominent SoT sentence). + +```bash +grep -n 'design_schema_drift' docs/DESIGN.md crates/ailang-core/src/ast.rs +``` + +Expected: 1 match in DESIGN.md, 1 match in ast.rs (each names the +drift test as the enforcement mechanism). + +- [ ] **Step 2: Drift test green** + +```bash +cargo test -p ailang-core --test design_schema_drift 2>&1 | tail -5 +``` + +Expected: `test result: ok` with 7 tests passed. + +- [ ] **Step 3: Workspace tests stay green** + +```bash +cargo test --workspace 2>&1 | grep -E "test result:|FAILED" | grep -v "0 failed" | head -10 +``` + +Expected: empty. + +- [ ] **Step 4: Bench gates stay green** + +```bash +python3 bench/check.py 2>&1 | tail -3 +python3 bench/compile_check.py 2>&1 | tail -3 +``` + +Expected: each summary `0 regressed`. + +- [ ] **Step 5: Sweep-1 + Sweep-2 invariants stay clean** + +```bash +grep -nE 'Iter [0-9]+[a-z]?(\.[0-9]+)?|Family [0-9]+|^[^/]*2026-[0-9]{2}-[0-9]{2}|\*\*Status: |pre-[0-9]+[a-z]?|[0-9]+[a-z]? sketch|21.g' docs/DESIGN.md +``` + +Expected: empty (Sweep-1 invariant). + +```bash +grep -nE 'REVERTED|preserved for the audit trail|Migration plan|[Oo]riginally framed|original rationale|empirically-grounded version|A future (iter|iteration|milestone|Prelude) may|A future iter that|a (later|future) iter(ation)? \(deferred\)|Concrete design deferred' docs/DESIGN.md +``` + +Expected: empty (Sweep-2 invariant). + +- [ ] **Step 6: Append JOURNAL entry** + +Template (adjust specifics to actual outcome): + +```markdown + +## 2026-05-10 — Iteration design-md-consolidation 3: schema SoT inversion + data-model hardening + +Third iteration of the milestone defined in +`docs/specs/2026-05-10-design-md-consolidation.md`. Sweep 3 +inverts the schema source-of-truth between `docs/DESIGN.md` +§"Data model" and `crates/ailang-core/src/ast.rs`: DESIGN.md is +canonical, `ast.rs` is the projection, and a new drift test +catches divergence. + +Three substantive changes plus one test: + +- **Two Rust code blocks removed from Decision 10.** The + `Type::Fn` + `ParamMode` block (around line 1027 at iter + start) and the `Suppress` struct block (around line 1132) + are replaced by prose pointers to §"Data model". Decision + 10's prose argument (per-position metadata vs `Type::Borrow` + variant) reads cleanly without the inline Rust. +- **§"Data model" SoT inversion.** The opener now reads "**This + section is the canonical schema.**" The Rust types in `ast.rs` + are framed as the in-memory projection, not the source. The + drift test is named as the enforcement mechanism in the + opener. +- **`ast.rs` module doc-comment.** The file-level `//!` block + bold-emphasises that DESIGN.md §"Data model" is canonical; + names the drift test as the enforcement; preserves the + serde-attribute description and the entry-type pointer. +- **`crates/ailang-core/tests/design_schema_drift.rs`** is the + new drift test. Pattern follows the existing `spec_drift.rs`: + exhaustive `match` per enum (`Term`, `Pattern`, `Type`, `Def`, + `Literal`, `ParamMode`) ensures adding a variant without a + DESIGN.md anchor fails compilation; the test asserts each + anchor literally appears in DESIGN.md. 7 tests total. All + GREEN on first run (DESIGN.md already lists every variant). + +Acceptance: + +- `grep -nE '^\s*(struct |enum |pub (struct|enum|fn))' docs/DESIGN.md` → empty. +- `grep -n 'whenever the two disagree' docs/DESIGN.md` → empty. +- `grep -n 'This section is the canonical schema' docs/DESIGN.md` → 1 line. +- `cargo test -p ailang-core --test design_schema_drift` → 7 tests pass. +- `cargo test --workspace` → 0 FAILED. +- `bench/check.py` and `bench/compile_check.py` → 0 regressed. +- Sweep-1 + Sweep-2 invariants stay empty (no regression). + +Tasks (commit subjects): + +- design-md-consolidation 3.1: remove 2 Rust code blocks from Decision 10 (Type::Fn + Suppress) — schema lives in §Data-model +- design-md-consolidation 3.2: invert §Data-model SoT — DESIGN.md canonical, ast.rs projection, drift test enforces +- design-md-consolidation 3.3: ast.rs doc-comment names DESIGN.md §Data-model as canonical schema, drift test as enforcement +- design-md-consolidation 3.4: add design_schema_drift.rs — exhaustive-match drift test for ast.rs vs DESIGN.md §Data-model + +Carried into sweep 4: workflow / cross-reference cleanup. +"Project ecosystem" `agents/` path correction; "Verification and +correctness" workflow detail; "What is not (yet) supported" +§"Recently lifted gates" removal; cross-reference audit. + +Process note: design_schema_drift was GREEN on first run — the +schema in §"Data model" already anchored every variant. The +RED-first discipline reduced to a deliberate-corruption probe to +verify the failure message is precise; corruption was reverted +before commit. This matches the "guard test on already-aligned +content" pattern: TDD's RED step is the probe, not a real bug. +``` + +- [ ] **Step 7: Update roadmap.md** + +Edit `docs/roadmap.md`: +- old: +``` + - [x] Sweep 1 — remove history anchors + - [x] Sweep 2 — REVERTED + migration plans out + - [ ] Sweep 3 — schema SoT inversion + data-model hardening + - [ ] Sweep 4 — workflow / cross-reference cleanup +``` +- new: +``` + - [x] Sweep 1 — remove history anchors + - [x] Sweep 2 — REVERTED + migration plans out + - [x] Sweep 3 — schema SoT inversion + data-model hardening + - [ ] Sweep 4 — workflow / cross-reference cleanup +``` + +- [ ] **Step 8: Commit** + +```bash +git add docs/JOURNAL.md docs/roadmap.md +git commit -m "design-md-consolidation 3: journal entry + roadmap sweep-3 closed" +```