# design-md-consolidation iter 3 — sweep 3: schema SoT inversion + data-model hardening > **Parent spec:** `docs/specs/0004-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/0004-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" ```