All 176 files in the four accumulating directories now use a zero-padded 4-digit counter prefix that reflects creation order (`NNNN-slug.md`). The counter is assigned per directory in strict git-log creation order; ties broken alphabetically by original name. The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is dropped — the date is recoverable from git log and the counter carries the ordering. A file's counter is stable for the life of the file: never reassigned, never reused, never compacted. Deleted files retire their counter; subsequent files do not fill the gap. This is the property that lets cross-references stay literal — refs use the full filename including the counter (`design/contracts/0007-honesty-rule.md`) so they grep cleanly and resolve directly without a glob step. 313 cross-references updated across .md/.rs/.toml/.c/.json files (test pins, include_str! paths, design-INDEX entries, baseline notes, runtime C comments, inter-contract markdown links incl. bare basename and `../models/foo.md` forms). CLAUDE.md gets a new "File-naming convention" section spelling out the rule and rationale. skills/brainstorm/SKILL.md and skills/planner/SKILL.md updated so new spec/plan creation produces counter-prefixed names from the start. The full test suite (cargo test --workspace) passes.
32 KiB
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/implementto 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
grep -n '^```rust' docs/DESIGN.md
Expected: 2 matches.
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 proseThe 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<Type>,
param_modes: Vec<ParamMode>, // same length as params
ret: Box<Type>,
ret_mode: ParamMode,
effects: Vec<String>,
}
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.
FnDef.suppress: Vec<Suppress> ; 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.
grep -nE '^\s*(struct |enum |pub (struct|enum|fn))' docs/DESIGN.md
Expected: empty.
- Step 5: Commit
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.rsis the source of truth. Every additive field is declared withskip_serializing_ifso pre-existing fixtures keep bit-identical canonical-JSON hashes — that gating contract is what makes growing the schema cheap.
Two issues:
- "schema mirror of an enum or struct in ast.rs" frames ast.rs as primary.
- "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
grep -n 'whenever the two disagree' docs/DESIGN.md
Expected: 1 match.
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_ifgating-contract sentence unchanged. -
Step 3: Post-test
grep -n 'whenever the two disagree' docs/DESIGN.md
Expected: empty.
grep -n 'This section is the canonical schema' docs/DESIGN.md
Expected: 1 match.
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
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
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:
//! 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:
//! 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
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
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:
//! 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 (
calleevsfn,type_namevstype,in_termvsin,ret_tyvsret,param_tysvsparamTypes) 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.methodsandInstanceDef.methodsuse the namesmethods(verify in ast.rs lines 258-340 region). -
Step 2: Run the test (expect GREEN — DESIGN.md already aligned)
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
cargo test --workspace 2>&1 | grep -E "test result:|FAILED" | grep -v "0 failed" | head -10
Expected: empty (no FAILED lines).
- Step 5: Commit
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
grep -nE '^\s*(struct |enum |pub (struct|enum|fn))' docs/DESIGN.md
Expected: empty (no Rust type definitions remain).
grep -n 'whenever the two disagree\|ast.rs is the source of truth' docs/DESIGN.md
Expected: empty (the SoT-inverted disclaimer is gone).
grep -n 'This section is the canonical schema' docs/DESIGN.md
Expected: 1 match (the prominent SoT sentence).
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
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
cargo test --workspace 2>&1 | grep -E "test result:|FAILED" | grep -v "0 failed" | head -10
Expected: empty.
- Step 4: Bench gates stay green
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
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).
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):
## 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
git add docs/JOURNAL.md docs/roadmap.md
git commit -m "design-md-consolidation 3: journal entry + roadmap sweep-3 closed"