832375f2ac
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.
948 lines
32 KiB
Markdown
948 lines
32 KiB
Markdown
# 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<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`.**
|
|
|
|
```rust
|
|
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.
|
|
|
|
```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"
|
|
```
|