Role-split the 3020-line docs/DESIGN.md into design/contracts/ (the hot, test-linked invariant set), design/models/ (one whitepaper per model), and design/INDEX.md as the sole addressable spine; decision- records rehomed to docs/journals/. Clean cut (DESIGN.md deleted, no stub), ###-subsection relocation granularity, build-atomic landing (design_schema_drift.rs include_str! forces one commit), polymorphic INDEX links (code-SoT contracts point at source //!, no prose dup), typed-INDEX anti-regrowth pin (design_index_pin.rs). grounding-check PASS 13/13 after one re-dispatch (commitment-4 pin-status claim corrected: Decision-6 :242 audit-trail sentence is pinned by no test, retired to the decision-record journal, no successor pin by design). roadmap: DESIGN.md->design/ marked [~] in flight (spec landed).
25 KiB
DESIGN.md → design/ role-split — Design Spec
Date: 2026-05-19 Status: Draft — awaiting user spec review Authors: Brummel (orchestrator) + Claude
Goal
docs/DESIGN.md is one 3020-line file doing three different jobs
for three different consumers with three different lifetimes. The
line count is the symptom of role-conflation, not of mess: the
just-closed docs-honesty-lint milestone ran a full honesty pass and
DESIGN.md stayed 3020 lines, because honesty-lint operates within a
job and its own audit-trail invariant mandates retaining the bulky
decision-record prose (DESIGN.md Decision 6, lines 241–243: "records
the why … for the audit trail"). You cannot clean your way out of
conflation; the conflation is the structural defect.
Replace docs/DESIGN.md with a design/ directory split on the
consumer/lifetime axis — design/contracts/ (the hot,
test-linked invariant set), design/models/ (one whitepaper per
model) — with design/INDEX.md as the sole addressable spine.
Decision-record prose moves to docs/journals/. The split is a
clean cut: docs/DESIGN.md is deleted; every named reference is
repointed in one build-atomic milestone.
The three conflated jobs and their distinct consumers:
- Contract — prescriptive, test-linked invariants (Decision 6's
binding Form-A constraints, the Roundtrip Invariant, the
borrow/own binding rules, the frozen value layout). Consumer:
architect(mandatory full read at every milestone close),grounding-check(every spec), the honesty sweep. Wants: structured, addressable, test-linked, small, hot. The only job on the per-milestone hot path. - Model exposition — whitepaper-shaped narratives (RC +
Uniqueness, typeclasses, effects, the authoring-surface rationale).
Consumer: onboarding (fresh-context LLM,
fieldtester). Irreducible — the code cannot speak the gestalt. - Decision-record / relitigating-guard — why X was chosen, why
Y/Z were rejected (region inference, tracing GC, …). Consumer: a
future
brainstorm, so it does not re-propose a settled-and- rejected idea. This already has a home —docs/journals/.
Underlying principle: the code is authoritative for what it does; the spec's irreducible job is what is promised plus a pointer to the green test that ratifies it.
Architecture
design/
INDEX.md sole addressable spine — a typed two-table ledger
contracts/ prose-authoritative invariants (the architect hot path)
feature-acceptance.md authoring-surface.md roundtrip-invariant.md
memory-model.md typeclasses.md float-semantics.md
frozen-value-layout.md honesty-rule.md tail-calls.md
data-model.md embedding-abi.md verification.md
models/ one whitepaper per model (onboarding path)
rc-uniqueness.md typeclasses.md effects.md authoring-surface.md pipeline.md
docs/journals/2026-05-19-design-decision-records.md relitigation-guard archive
docs/DESIGN.md DELETED (clean cut)
Five structural commitments:
-
Polymorphic INDEX link. An INDEX row's
linkis either adesign/file or an authoritative source path. Where code is the single source of truth (mangling, env construction, qualified-cross-module-ref resolution), there is no prose contract file — the row points at the source//!header and names the ratifying test, killing the prose-vs-code second truth.frozen-value-layoutis the deliberate dual-link exception (a one-way human commitment and a codegen contract → both a prose file and a source pointer). The polymorphic disposition is fixed per-contract in the Components disposition table below. -
###-subsection is the unit of relocation. No sentence- surgery. Each###subsection of DESIGN.md moves whole tocontracts/,models/, or the decision-record journal. The MIXED##Decisions (6/9/10/11) decompose at their existing###seam, which recon confirmed resolves cleanly. -
Build-atomic, not just semantically atomic.
crates/ailang-core/tests/design_schema_drift.rs:22usesinclude_str!("../../../docs/DESIGN.md")— a compile-time path. The instantdocs/DESIGN.mdis deleted the workspace does not compile until thatinclude_str!is retargeted. The content move + the 4-executable retarget + the 2-diagnostic/2-E2E retarget + the new pin test land in one commit; the tree is never half-moved. This is a fail-closed property, not a hazard. -
The honesty-rule self-repointing resolves the internal contradiction. Two distinct self-referential passages, with different pin status (grounding-check verified):
### What this document is — and the honesty rule it holds itself to(DESIGN.md:56–82) is pinned:docs_honesty_pin.rs:70pins"the honesty rule it holds itself to"and:72pins"whether the document asserts something exists, works, or changed that does not"(both insidedesign_md_present_tense_anchors_present). This becomescontracts/honesty-rule.md, rewritten so the rule states the new home (rationale →docs/journals/); those two pins are retargeted to the new file and the new wording in the same atomic change.- Decision 6's :242 sentence — "The rest of this section records
the why of Decision 6 for the audit trail; the constraints
listed below describe the surface as shipped." — is pinned by
no test anywhere (grounding-check grepped
crates/*/tests/, in-sourcecrates/*/src/,effect_doc_honesty_pin.rs: zero matches). It is itself decision-record self-assertion describing the old conflated structure. The milestone retires it: its "why of Decision 6" content moves to the decision-record journal; the meta-sentence that asserts DESIGN.md retains audit-trail prose is not carried forward and needs no successor pin — code never protected it, and should not, because that sentence is exactly the audit-trail self-claim the split removes from the contract surface.
The internal contradiction (a doc asserting of itself that it "records the why for the audit trail" while the milestone moves the why out) is closed by the milestone: the asserting sentence is retired, and the only honesty self-claim that survives — "rationale lives in
docs/journals/" — is the rewrittenhonesty-rule.md, which the retargeted:70,72pins protect. -
Clean cut, no stub, no redirect.
docs/DESIGN.mdis deleted. No transitional pointer file (a "see elsewhere" file is itself history-pointing — a doc-honesty violation in the milestone that exists to enforce doc honesty). Matches the no-deprecation-window precedent (mut/var/assign removal) and the current-state-mirror discipline.
Concrete code shapes
1. The delivered artifacts (this is an infra milestone — these are the headline)
design/INDEX.md — the typed two-table ledger. link is
polymorphic; ratifying-test is mandatory for kind=contract:
# AILang Design — Index
The sole addressable entry point. Every contract and model is
reached from here. A contract is a prescriptive, test-linked
invariant; a model is a whitepaper narrative. `ratifying-test` names
the green test that proves a contract still holds. `link` is
polymorphic: a `design/` file, or the authoritative source `//!`
header when the code is the single source of truth.
## Contracts
| id | consumer / lifetime | ratifying-test | link |
|----|---------------------|----------------|------|
| feature-acceptance | brainstorm-gate / stable | skills/brainstorm/SKILL.md Step 4 | contracts/feature-acceptance.md |
| authoring-surface | LLM author / stable | crates/ailang-surface round_trip.rs | contracts/authoring-surface.md |
| roundtrip-invariant | every release / stable | crates/ailang-surface round_trip.rs | contracts/roundtrip-invariant.md |
| memory-model | LLM author / stable | crates/ailang-check uniqueness/mode tests | contracts/memory-model.md |
| data-model | LLM author / stable | crates/ailang-core design_schema_drift.rs | contracts/data-model.md |
| mangling | codegen / stable | crates/ail eq_ord_e2e.rs | crates/ailang-codegen/src/lib.rs //! |
| env-construction | codegen / stable | crates/ailang-codegen duplicate_ctor_pin.rs | crates/ailang-codegen/src/lib.rs //! |
| frozen-value-layout | embedding ABI / one-way-frozen| crates/ailang-codegen embed_record_layout_pin.rs | contracts/frozen-value-layout.md + runtime/rc.c §layout |
| float-semantics | LLM author / stable | crates/ail eq_float_noinstance.rs | contracts/float-semantics.md |
| typeclasses | LLM author / stable | crates/ail show_no_instance_e2e.rs | contracts/typeclasses.md |
| tail-calls | codegen / stable | crates/ailang-check tail-call tests | contracts/tail-calls.md |
| honesty-rule | architect+grounding / stable | crates/ailang-core docs_honesty_pin.rs | contracts/honesty-rule.md |
| embedding-abi | embedding host / stable | crates/ailang-codegen embed_record_layout_pin.rs | contracts/embedding-abi.md |
| verification | architect / stable | bench/architect_sweeps.sh | contracts/verification.md |
## Models
| id | consumer / lifetime | link |
|----|---------------------|------|
| rc-uniqueness | onboarding / evolves | models/rc-uniqueness.md |
| typeclasses | onboarding / evolves | models/typeclasses.md |
| effects | onboarding / evolves | models/effects.md |
| authoring-surface | onboarding / evolves | models/authoring-surface.md |
| pipeline | onboarding / evolves | models/pipeline.md |
The ratifying-test column above shows illustrative tokens. The
planner resolves each to an exact, recon-verified file path (the
design_index_pin.rs clause-2 check requires every token to resolve
to a real file); the spec owns the shape and the disposition, the
planner owns the exact bytes per the planner Iron Law.
A sample contract file — design/contracts/float-semantics.md
(content moved verbatim from DESIGN.md:2700–2838, header re-levelled
to file root, all decision-record/why-prose stripped to the journal):
# Float semantics
Float is the IEEE-754 double-precision primitive. Float has **no
`Eq` and no `Ord` instance by design**: IEEE-754 ordering is partial
(NaN is unordered), so a total `Eq`/`Ord` would be a lie about the
type. … [present-tense contract prose only] …
`float_to_str` is type-installed; codegen is reserved and not yet
shipped.
Ratified by: `crates/ail/tests/eq_float_noinstance.rs`.
The anti-regrowth guard — crates/ailang-core/tests/design_index_pin.rs
(the concrete must-fail artifact for an infra milestone — its correct
behaviour is to go RED the instant the structure re-conflates):
//! Structural pin for the design/ ledger. Sibling of
//! docs_honesty_pin.rs. Fails RED the instant the design/ split
//! re-conflates contract + rationale + narrative, an INDEX row
//! dangles, a contract loses its ratifying test, or docs/DESIGN.md
//! is resurrected.
// helper: workspace root = CARGO_MANIFEST_DIR/../.. ; norm() =
// split_whitespace().join(" ") — line-wrap-independent, identical
// shape to docs_honesty_pin.rs.
#[test]
fn design_md_is_gone() {
// clause 4 — clean-cut pin
assert!(!root().join("docs/DESIGN.md").exists(),
"docs/DESIGN.md was resurrected; the split is clean-cut");
}
#[test]
fn every_index_link_resolves() {
// clause 1 — parse design/INDEX.md tables; for each row, the
// `link` target (design/ file OR source path; the dual-link
// row's first segment before " + ") exists on disk.
}
#[test]
fn every_contract_names_a_resolvable_ratifying_test() {
// clause 2 — every kind=contract row's ratifying-test token
// resolves to a real file under crates/**/tests, bench/, or
// skills/**/SKILL.md.
}
#[test]
fn contracts_carry_no_decision_record_prose() {
// clause 3 — the conflation tripwire. For each
// design/contracts/*.md, norm() the body and assert it contains
// none of: "we rejected", "an earlier draft", "Why not other",
// "was retired in iter", "rollback plan", "previously all".
}
2. The one real code change — compiler diagnostic, before → after
This is the clause-1 / clause-3 evidence. The pre-existing fixture the diagnostic E2E runs (unchanged), the program an LLM author writes that must fail to typecheck:
; examples/eq_float_noinstance.ail
(module eq_float_noinstance
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (app print (if (app eq 1.0 2.0) 1 0)))))
crates/ailang-check/src/lib.rs:873 (the Float branch):
- orderability per IEEE-754); see DESIGN.md §\"Float semantics\".",
+ orderability per IEEE-754); see design/contracts/float-semantics.md.",
crates/ailang-check/src/lib.rs:888 (the Show branch):
- the prelude; see DESIGN.md §\"Prelude (built-in) \
- classes\". User types declare their own \
+ the prelude; see design/contracts/typeclasses.md. \
+ User types declare their own \
crates/ail/tests/eq_float_noinstance.rs:40 — the assertion
currently passes on "Float semantics" || "DESIGN"; both substrings
vanish, so it updates in lockstep:
- no_inst.message.contains("Float semantics") || no_inst.message.contains("DESIGN"),
- "expected NoInstance message to cross-reference DESIGN.md §Float semantics, got: {:?}",
+ no_inst.message.contains("design/contracts/float-semantics.md"),
+ "expected NoInstance message to cross-reference the float-semantics contract, got: {:?}",
crates/ail/tests/show_no_instance_e2e.rs:48 — analogous: the
asserted substring becomes "design/contracts/typeclasses.md".
3. The four executable retargets (before → after)
bench/architect_sweeps.sh:22-28:
- DESIGN="docs/DESIGN.md"
- [[ ! -f "$DESIGN" ]] && { echo "could not find $DESIGN"; exit 2; }
+ INDEX="design/INDEX.md"
+ [[ ! -f "$INDEX" ]] && { echo "could not find $INDEX"; exit 2; }
Each run_sweep greps the design/contracts/*.md + design/models/*.md
set instead of the single file; exit-code contract preserved
(0 clean / 1 anchor-found / 2 spine-not-found). Sweep 5 (honesty)
scans all design/ prose; the verbatim regexes are unchanged.
crates/ailang-core/tests/design_schema_drift.rs:22:
- const DESIGN_MD: &str = include_str!("../../../docs/DESIGN.md");
+ const DATA_MODEL: &str = include_str!("../../../design/contracts/data-model.md");
The data_model_section() slicer (## Data model … \n## Pipeline)
and data_model_section_is_bounded are removed: the whole file
is now the data-model contract, so the section-extraction premise
vanishes — a simplification the split enables. Every
section.contains(anchor) assertion runs against the whole
DATA_MODEL string.
crates/ailang-core/tests/docs_honesty_pin.rs and
effect_doc_honesty_pin.rs: the read("docs/DESIGN.md") calls split
into reads of the specific design/ files where each pinned sentence
now lives. The present/absent pin strings are unchanged except
the two honesty-rule anchors at docs_honesty_pin.rs:70,72 ("the honesty rule it holds itself to" / "whether the document asserts something exists, works, or changed that does not"), which are
updated to the rewritten contracts/honesty-rule.md wording in
lockstep with that file. The Decision-6 :242 audit-trail sentence
is pinned by no test (verified) and is retired to the decision-record
journal, not retargeted — there is nothing to repoint, by design.
4. Agent reading-list rewrite (representative before → after)
skills/audit/agents/ailang-architect.md:30:
- 2. `docs/DESIGN.md` — the canonical specification. Drift is measured against
+ 2. `design/INDEX.md` — the typed contract ledger and sole spine.
+ Walk the Contracts table; drift is measured against each row's
+ `link` target (a `design/contracts/` file or the source `//!`
+ it names). `design/models/` is context, not a drift surface.
The other ~11 reading-list bullets
(grounding-check, fieldtester, debugger, bencher, tester,
implementer, implement-orchestrator, spec-reviewer,
quality-reviewer, plan-recon, docwriter) are rewritten to their
role: grounding-check and architect read contracts/; fieldtester
reads INDEX.md + models/ (no-edit limit preserved, repointed);
bencher reads models/rc-uniqueness.md (Decision-9/Boehm narrative);
the rest point at design/INDEX.md as the spine.
Components
The per-contract disposition (fixes the polymorphic link, planner does not re-decide it):
| Contract | Disposition | Rationale |
|---|---|---|
| feature-acceptance | prose file | gate text, prose-authoritative |
| authoring-surface | prose file | binding Form-A constraints, prose-stated |
| roundtrip-invariant | prose file | the invariant statement is prose |
| memory-model | prose file | Decision 10 ### Language-design constraints (binding) + ### Schema additions + ### Codegen contract; binding, prose-stated |
| data-model | prose file | the wire schema an author must produce is a promise (prose-authoritative); design_schema_drift.rs include_str!s it |
| float-semantics | prose file | author-facing; the diagnostic points here |
| typeclasses | prose file | coherence/dispatch rules + prelude classes; the Show diagnostic points here |
| tail-calls | prose file | the propagation rule is prose-stated, check-ratified |
| frozen-value-layout | prose file + source link | one-way human commitment AND codegen contract — deliberate dual-link |
| honesty-rule | prose file | rewritten; resolves the internal contradiction |
| embedding-abi | prose file | the M3-frozen ABI contract, author + host facing |
| verification | prose file | the cross-cycle verification contract |
| mangling | source link only | codegen behaviour; prose would duplicate code; eq_ord_e2e.rs ratifies |
| env-construction | source link only | codegen behaviour; duplicate_ctor_pin.rs ratifies |
Models (always prose whitepapers): rc-uniqueness (Decision 10
narrative + Decision 9 Boehm rationale), typeclasses (resolution /
monomorphisation narrative), effects (Decision 3 narrative),
authoring-surface (notation rationale, candidate notations,
empirical addendum), pipeline (Pipeline + CLI).
Decision-record archive — docs/journals/2026-05-19-design-decision-records.md:
the ### Why not other memory models, ### What this Decision deliberately does not do (×N), ### Empirical addendum,
### First choice and rollback plan, and every "considered and
rejected" passage, moved whole at ### granularity. The journals
INDEX.md gets the one-line pointer per project convention.
Data flow
- Architect, milestone close: reads
design/INDEX.md→ walks the Contracts table → for each row reads thelinktarget → measures drift against it.architect_sweeps.shscans thedesign/prose set; exit codes unchanged. - grounding-check, per spec: reads
design/INDEX.md→ the contract rows the spec's assumptions touch → composes. - LLM author, NoInstance: the diagnostic names
design/contracts/float-semantics.md(or…/typeclasses.md) → opens exactly that contract → self-corrects. - Future brainstorm tempted to re-propose region inference: the
"why rejected" prose is in
docs/journals/2026-05-19-design-decision-records.md, reachable from the journals INDEX — not in any contract file. design_index_pin.rs, everycargo test: parses INDEX.md → asserts all links resolve, all contracts name a resolvable ratifying-test, no contract carries decision-record prose,docs/DESIGN.mdis absent.
Error handling
All failure modes fail closed:
- INDEX row → missing target ⇒
design_index_pin.rsevery_index_link_resolvesRED. - A
contracts/file regrows "we rejected"/"Why not other"/… ⇒contracts_carry_no_decision_record_proseRED (the conflation tripwire — the milestone's reason to exist, enforced in code). docs/DESIGN.mdresurrected ⇒design_md_is_goneRED.- A contract names a ratifying-test that does not resolve ⇒
every_contract_names_a_resolvable_ratifying_testRED. architect_sweeps.shcannot finddesign/INDEX.md⇒ exit 2 (preserved "infra missing" semantics, repointed).- Content move lands without the
include_str!retarget ⇒cargo buildfails. This is the build-atomic fail-closed property; the planner sequences the single landing build-green. - Recon-undercount countermeasure (acceptance gate). The
milestone's acceptance includes a tree-wide
grep -rIn 'DESIGN\.md'over all file types (bench/ skills/ crates/ runtime/ ail-embed/ examples/ docs/ CLAUDE.md) returning only the append-only history docs (docs/journals/,docs/specs/,docs/plans/,docs/roadmap.md,docs/WhatsNew.md,docs/journal-archive.md) — zero live references in code, scripts, agent files, or comments. Recon proved the carrier's file-type list (*.md *.rs *.py *.sh *.toml) silently misses.c/.h/.ail; the gate is unfiltered (grep -rIn, no--include) and any claimed-absent path isls-verified. This is the fourth-recurrence recon-undercount class made an explicit gate.
Testing strategy
- RED-first spine:
design_index_pin.rs(the 4 clauses above) is written first; RED untildesign/exists, is consistent, anddocs/DESIGN.mdis gone; GREEN when the split is complete and faithful. - Retarget, not rewrite:
design_schema_drift.rs,docs_honesty_pin.rs,effect_doc_honesty_pin.rskeep their property (the schema/honesty invariant stays documented); only the path/anchor moves.design_schema_drift.rsadditionally simplifies (the## Data model/## Pipelineslicer is removed — the file boundary now bounds the section). - Lockstep E2Es:
eq_float_noinstance.rs,show_no_instance_e2e.rsupdated with the diagnostic strings; they continue to assert the author-affordance (a correct, resolvable pointer). - Whole-suite GREEN:
cargo testworkspace-wide must be green post-split (theinclude_str!retarget + all path moves consistent). Build-atomicity makes a half-move a compile failure, not a silent-wrong state. - Bench unaffected: recon confirmed the bench harness
(
bench/check.py,compile_check.py,cross_lang.py,run.sh) has zeroDESIGN.mdreferences; onlyarchitect_sweeps.sh(audit-time, not bench-time) opens it. Generated IR is byte-identical (no codegen change — the diagnostic string is not in the IR). Any bench firing is the tracked P2 noise and is causally exonerable by byte-identical-IR. - Audit, mandatory at milestone close: architect drift review
against the new
design/(the architect's own reading list is one of the rewritten artifacts — the audit validates the structure it now reads); bencher causal exoneration as usual. - No fieldtest: zero authoring-surface change. The diagnostic-string change is a doc-pointer, not a language or authoring-surface change. (Recorded explicitly so the post-audit step is not skipped by omission but by reasoned exclusion.)
Acceptance criteria
docs/DESIGN.mddoes not exist;design/INDEX.md,design/contracts/*.md,design/models/*.md, anddocs/journals/2026-05-19-design-decision-records.mdexist and are internally consistent.- Every
###subsection of the former DESIGN.md is accounted for in exactly one destination (contract / model / decision-record); no content is silently dropped. design/INDEX.mdContracts table: everylinkresolves, every row names a resolvableratifying-test. Models table: everylinkresolves.crates/ailang-core/tests/design_index_pin.rsis GREEN with all four clauses; it was demonstrably RED before the split landed.design_schema_drift.rs,docs_honesty_pin.rs,effect_doc_honesty_pin.rsare GREEN against the new files; their protected properties are unchanged.- The compiler emits
design/contracts/float-semantics.md/design/contracts/typeclasses.mdin the two NoInstance diagnostics;eq_float_noinstance.rsandshow_no_instance_e2e.rsassert the new pointer and are GREEN. cargo testworkspace-wide is GREEN;cargo buildis clean.- Tree-wide
grep -rIn 'DESIGN\.md'(no--include) returns matches only in the append-only history docs; zero live references in code, scripts, agent files, SKILL bodies,CLAUDE.md,skills/README.md, or comments. bench/architect_sweeps.shruns againstdesign/, exits 0 on the clean tree, and its exit-2 path triggers on a missingdesign/INDEX.md.- The ~12 agent reading lists, the 5 SKILL.md bodies,
CLAUDE.md, andskills/README.mdname the new spine; no agent contract still instructs reading a file that does not exist. - The milestone landed in a build-atomic shape (no commit leaves the workspace non-compiling).