iter design-md-rolesplit.1 (DONE 9/9): DESIGN.md -> design/ ledger role-split
The 3020-line docs/DESIGN.md is replaced by the design/ ledger:
design/INDEX.md (sole addressable spine, typed Contracts+Models tables,
polymorphic links — prose file OR authoritative source //!), 14
design/contracts/*.md test-linked invariants + 3 source-link-only
contracts (mangling/env-construction/qualified-xref, no prose file —
code is SoT), 5 design/models/*.md whitepapers, and
docs/journals/2026-05-19-design-decision-records.md (the
relitigation-guard archive — every why/rejected/does-not-do/rollback/
empirical ### moved out at ###-granularity). Clean cut: git rm
docs/DESIGN.md, no stub.
RED-first crates/ailang-core/tests/design_index_pin.rs — the 4-clause
anti-regrowth spine (DESIGN.md-gone / every-INDEX-link-resolves /
every-contract-names-a-resolvable-ratifier /
contracts-carry-no-decision-record-prose) — demonstrably RED before,
GREEN after. Build-atomic by task ordering: design_schema_drift.rs's
include_str! (the only compile-time consumer) retargeted to
design/contracts/data-model.md BEFORE the deletion; its
## Data model/## Pipeline slicer dropped (a simplification the split
enables). 2 NoInstance diagnostics + 2 lockstep E2Es retargeted to
design/contracts/{float-semantics,typeclasses}.md. ~12 agent reading
lists + 5 SKILL bodies + CLAUDE.md + skills/README.md + ~25
code/C/.ail/spec comment xrefs retargeted; OQ7 dangling 'Iter 13b'
cite deleted (no forward target — a pointer would be fiction).
honesty-rule.md rewritten so the rule names the new home
(rationale->journals), resolving the recon-found internal
contradiction; the two docs_honesty_pin.rs:70,72 pinned phrases kept
verbatim+contiguous.
Boss-verified independently: cargo test --workspace 646 passed /
0 failed; design_index_pin 4/4; acceptance grep CLEAN of live
DESIGN.md refs (residuals = only the spec-mandated clause-4
deletion-enforcer). 2 DONE_WITH_CONCERNS routed to the mandatory
milestone-close audit: (a) str-abi.md:23 '(iter str-concat,
2026-05-13)' provenance stamp trips advisory architect_sweeps Sweep-1
— Boss-confirmed byte-identical to DESIGN.md@deeffb1:2062-2065, a
faithfully-migrated PRE-EXISTING anchor (regexes verbatim, only path
retargeted), NOT split-introduced — RATIFY-or-tidy at audit; (b) a
now stale-direction intra-prose 'see Str ABI below' cross-ref in
float-semantics.md — audit-adjudication candidate. Plan defect noted:
Task 9 Step 4's verbatim acceptance grep used a ^./ anchor not
matching the system's grep -rIn output; substance re-verified CLEAN.
Spec grounding-check PASS x2. Journals INDEX + decision-records
pointer appended (Boss-only).
This commit is contained in:
@@ -1390,7 +1390,8 @@ fn load_workspace_human(
|
||||
///
|
||||
/// Cross-module references (`<prefix>.<def>`) are always kept — a local
|
||||
/// binding can never shadow a dotted name (the typechecker rejects defs
|
||||
/// with a dot, see DESIGN.md "qualified cross-module references").
|
||||
/// with a dot; qualified cross-module references are code-SoT — see
|
||||
/// the `qualified-xref` row in design/INDEX.md).
|
||||
fn collect_refs(def: &ailang_core::Def) -> std::collections::BTreeSet<String> {
|
||||
let mut out = std::collections::BTreeSet::new();
|
||||
let builtins: std::collections::HashSet<&'static str> =
|
||||
@@ -1569,7 +1570,7 @@ fn walk_term(
|
||||
/// Adds the variable bindings introduced by a pattern to `scope` and
|
||||
/// returns the names that were freshly inserted (so the caller can roll
|
||||
/// them back). MVP-restricted: nested ctor patterns only contain
|
||||
/// `Var`/`Wild` — see DESIGN.md.
|
||||
/// `Var`/`Wild` — see design/contracts/data-model.md.
|
||||
fn bind_pattern(
|
||||
p: &ailang_core::ast::Pattern,
|
||||
scope: &mut std::collections::HashSet<String>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Pin for the iter-24.3 codegen `import_map`-fallback path
|
||||
//! (DESIGN.md §"Cross-module references in synthesised bodies"
|
||||
//! invariant 2).
|
||||
//! (design/contracts/typeclasses.md, "Cross-module references in
|
||||
//! synthesised bodies" invariant 2).
|
||||
//!
|
||||
//! Property protected: post-mono synthesised body cross-module
|
||||
//! references resolve at codegen via the fallback to
|
||||
|
||||
@@ -2840,9 +2840,9 @@ fn str_clone_drop_balances_rc_stats() {
|
||||
/// static-Str input (literal `"abc"`). Both clones print their
|
||||
/// input bytes; RC stats account for three heap-Str slabs (one
|
||||
/// from int_to_str, two from str_clone) and three matching frees.
|
||||
/// Pins the "uniform consumer ABI" claim from DESIGN.md §"Str
|
||||
/// ABI" — str_clone only reads len + bytes + NUL, never the
|
||||
/// rc_header.
|
||||
/// Pins the "uniform consumer ABI" claim from
|
||||
/// design/contracts/str-abi.md — str_clone only reads len + bytes +
|
||||
/// NUL, never the rc_header.
|
||||
#[test]
|
||||
fn str_clone_cross_realisation_uniform_abi() {
|
||||
let (stdout, allocs, frees, live) =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* Embedding-ABI M3 record round-trip C host (own + borrow via a
|
||||
* compile-time -DBORROW switch). Frozen value layout (DESIGN.md
|
||||
* §"Embedding ABI" > "Frozen value layout"):
|
||||
* compile-time -DBORROW switch). Frozen value layout
|
||||
* (design/contracts/frozen-value-layout.md):
|
||||
* p - 8 .. p uint64_t refcount header (set to 1 by ailang_rc_alloc)
|
||||
* p + 0 int64_t constructor tag (single ctor → 0)
|
||||
* p + 8 IEEE-754 double field 0 = Float acc
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* only ever push a *scalar* per-tick sample; this host pushes a
|
||||
* single-ctor all-scalar *record* `Tick` as the per-call payload —
|
||||
* the actual minimal data-server binding shape. Frozen value layout
|
||||
* (DESIGN.md §"Embedding ABI" > "Frozen value layout"):
|
||||
* (design/contracts/frozen-value-layout.md):
|
||||
*
|
||||
* State (24-byte payload):
|
||||
* p - 8 .. p uint64_t refcount header (set to 1 by ailang_rc_alloc)
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
//!
|
||||
//! Property protected: a polymorphic Eq/Ord helper invoked at Float
|
||||
//! fires `no-instance` (since Float has neither Eq nor Ord instance
|
||||
//! per DESIGN.md §"Float semantics") AND the diagnostic message
|
||||
//! cross-references the canonical Float-semantics section so the
|
||||
//! per design/contracts/float-semantics.md) AND the diagnostic message
|
||||
//! cross-references the canonical float-semantics contract so the
|
||||
//! LLM author immediately learns the partial-Float story rather
|
||||
//! than seeing a bare "no instance" message.
|
||||
|
||||
@@ -35,11 +35,11 @@ fn eq_at_float_fires_float_aware_noinstance() {
|
||||
no_inst.message
|
||||
);
|
||||
|
||||
// Cross-ref to DESIGN.md §Float semantics — the LLM author should be
|
||||
// pointed at the canonical explanation of partial Float orderability.
|
||||
// Cross-ref to the float-semantics contract — the LLM author should
|
||||
// be pointed at the canonical explanation of partial Float orderability.
|
||||
assert!(
|
||||
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: {:?}",
|
||||
no_inst.message
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,8 +86,10 @@ fn eq_ord_user_adt_eq_intbox_hash_stable() {
|
||||
let mono = monomorphise_workspace(&ws).expect("mono");
|
||||
|
||||
// The mono symbol for `eq` at a user-defined ADT uses the
|
||||
// `<method>__<8-hex-prefix>` mangling per DESIGN.md
|
||||
// §"Mangling scheme" (primitives use the bare type name; compound
|
||||
// `<method>__<8-hex-prefix>` mangling (mangling is code-SoT —
|
||||
// see the `mangling` row in design/INDEX.md / the
|
||||
// crates/ailang-codegen/src/lib.rs //! header) (primitives use
|
||||
// the bare type name; compound
|
||||
// / user-defined types use the 8-hex BLAKE3 prefix of the
|
||||
// canonical type bytes). We look up the single `eq__` symbol
|
||||
// synthesised at `IntBox` — there is only one user instance,
|
||||
@@ -107,8 +109,9 @@ fn eq_ord_user_adt_eq_intbox_hash_stable() {
|
||||
.expect("eq__<IntBox-hash> not synthesised");
|
||||
|
||||
// The 8-hex-prefix in the mono-symbol name is the BLAKE3 prefix
|
||||
// of the canonical IntBox type bytes (DESIGN.md §"Mangling
|
||||
// scheme"). Drift here means either the canonical-form encoding
|
||||
// of the canonical IntBox type bytes (mangling is code-SoT — see
|
||||
// the `mangling` row in design/INDEX.md). Drift here means either
|
||||
// the canonical-form encoding
|
||||
// changed or the type's canonical bytes changed — both warrant
|
||||
// investigation before re-pinning.
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Pin for the iter-24.3 FreeFnCall constraint-residual-push
|
||||
//! invariant (DESIGN.md §"Cross-module references in synthesised
|
||||
//! bodies" invariant 3).
|
||||
//! invariant (design/contracts/typeclasses.md, "Cross-module
|
||||
//! references in synthesised bodies" invariant 3).
|
||||
//!
|
||||
//! Property protected: bare-name references to polymorphic free fns
|
||||
//! that resolve through the auto-injected-prelude path AT THE
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! Property protected: calling `print` on a function type fires the
|
||||
//! `no-instance` diagnostic with a Show-aware message that
|
||||
//! cross-references DESIGN.md §"Prelude (built-in) classes", so the
|
||||
//! cross-references design/contracts/typeclasses.md, so the
|
||||
//! LLM author immediately learns which types ship with built-in Show
|
||||
//! and how to declare their own instance for a user type.
|
||||
|
||||
@@ -40,12 +40,11 @@ fn print_on_fn_type_fires_show_aware_no_instance() {
|
||||
no_inst.message
|
||||
);
|
||||
|
||||
// Must cross-reference DESIGN.md §"Prelude (built-in) classes"
|
||||
// verbatim — that's the canonical anchor naming which types
|
||||
// ship with Show.
|
||||
// Must cross-reference design/contracts/typeclasses.md — that's
|
||||
// the canonical contract naming which types ship with Show.
|
||||
assert!(
|
||||
no_inst.message.contains("Prelude (built-in) classes"),
|
||||
"expected DESIGN.md §Prelude (built-in) classes cross-reference, got message: {:?}",
|
||||
no_inst.message.contains("design/contracts/typeclasses.md"),
|
||||
"expected design/contracts/typeclasses.md cross-reference, got message: {:?}",
|
||||
no_inst.message
|
||||
);
|
||||
}
|
||||
|
||||
@@ -446,7 +446,7 @@ pub enum CheckError {
|
||||
|
||||
/// An `(export …)` fn has a parameter or return type that is not
|
||||
/// a permitted embedding-ABI type. The embedding ABI is frozen as
|
||||
/// of M3 (DESIGN.md §"Embedding ABI" > "Frozen value layout"):
|
||||
/// of M3 (design/contracts/embedding-abi.md):
|
||||
/// `Int`/`Float` or a single-constructor record of those; every
|
||||
/// other shape (multi-ctor sums, `Str`/`List`/nested-record
|
||||
/// fields) is rejected at typecheck (the feature-acceptance
|
||||
@@ -858,7 +858,7 @@ impl CheckError {
|
||||
}
|
||||
// Iter 23.5: Float-aware addendum on `NoInstance`. When the
|
||||
// unresolved class is `Eq` or `Ord` AND the type is `Float`,
|
||||
// append a cross-reference to DESIGN.md §"Float semantics" so
|
||||
// append a cross-reference to design/contracts/float-semantics.md so
|
||||
// the LLM author learns the partial-Float story immediately
|
||||
// rather than seeing a bare "no instance" message. Float has
|
||||
// neither Eq nor Ord by design (partial orderability per
|
||||
@@ -871,7 +871,7 @@ impl CheckError {
|
||||
if (class == "prelude.Eq" || class == "prelude.Ord") && at_type == "Float" {
|
||||
d.message = format!(
|
||||
"{} — Float has no Eq/Ord instance by design (partial \
|
||||
orderability per IEEE-754); see DESIGN.md §\"Float semantics\".",
|
||||
orderability per IEEE-754); see design/contracts/float-semantics.md.",
|
||||
d.message
|
||||
);
|
||||
} else if class == "prelude.Show" {
|
||||
@@ -879,14 +879,14 @@ impl CheckError {
|
||||
// for any type lacking a Show instance — most commonly
|
||||
// a function type (`print f` where f is bare-fn-typed)
|
||||
// or a user type the LLM-author forgot to give an
|
||||
// instance for. Cross-references DESIGN.md §"Prelude
|
||||
// (built-in) classes" so the author learns which types
|
||||
// ship with built-in Show immediately.
|
||||
// instance for. Cross-references
|
||||
// design/contracts/typeclasses.md so the author learns
|
||||
// which types ship with built-in Show immediately.
|
||||
d.message = format!(
|
||||
"{} — `print` and `show` require a Show instance. \
|
||||
Built-in Show ships for Int, Bool, Str, Float in \
|
||||
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 \
|
||||
`instance prelude.Show <T>` in the type's defining \
|
||||
module per Decision 11 coherence.",
|
||||
d.message
|
||||
@@ -2644,7 +2644,7 @@ fn constraint_type_matches(declared: &Type, residual: &Type) -> bool {
|
||||
/// Decision 8.
|
||||
///
|
||||
/// `is_tail` is the tail-context flag for the term currently being
|
||||
/// visited. The propagation rules (from DESIGN.md Decision 8):
|
||||
/// visited. The propagation rules (from design/contracts/tail-calls.md):
|
||||
///
|
||||
/// - The body of a fn / Lam is visited with `is_tail = true`.
|
||||
/// - `Match { scrutinee, arms }`: the scrutinee is **not** in tail
|
||||
@@ -6333,8 +6333,8 @@ mod tests {
|
||||
|
||||
/// Floats milestone post-fieldtest B1 (RED): pattern-matching on a
|
||||
/// `Literal::Float` literal must be hard-rejected at typecheck via
|
||||
/// `CheckError::FloatPatternNotAllowed` per DESIGN.md §"Float
|
||||
/// semantics" — even when the match arm reaches `check` through
|
||||
/// `CheckError::FloatPatternNotAllowed` per
|
||||
/// design/contracts/float-semantics.md — even when the match arm reaches `check` through
|
||||
/// the *full* check pipeline (which runs `desugar_module` first).
|
||||
///
|
||||
/// The existing `reject_float_pattern_in_match` test in
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
//! Pin for `CheckError::DuplicateCtor` — same-named ctors in two
|
||||
//! different ADTs within one module. Ratifies the load-bearing
|
||||
//! consumer of the per-module `env.ctor_index` rebuild in
|
||||
//! `check_in_workspace` that DESIGN.md §"Env construction" rests
|
||||
//! on as the asymmetry-with-mono-side argument. If the
|
||||
//! `check_in_workspace`. Env construction is code-SoT (the
|
||||
//! `env-construction` ledger row in design/INDEX.md is source-link
|
||||
//! only); the why-two-overlays rationale this test rests on as the
|
||||
//! asymmetry-with-mono-side argument lives in
|
||||
//! docs/journals/2026-05-19-design-decision-records.md. If the
|
||||
//! per-module rebuild is ever removed without an equivalent
|
||||
//! replacement path, this test goes red.
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ impl<'a> Emitter<'a> {
|
||||
out.push_str(" %is_null = icmp eq ptr %p, null\n");
|
||||
out.push_str(" br i1 %is_null, label %ret, label %live\n");
|
||||
out.push_str("live:\n");
|
||||
// FROZEN ABI (embedding boundary) — see DESIGN.md §"Embedding ABI".
|
||||
// FROZEN ABI (embedding boundary) — see design/contracts/embedding-abi.md.
|
||||
out.push_str(" %tag = load i64, ptr %p, align 8\n");
|
||||
|
||||
let n_ctors = td.ctors.len();
|
||||
@@ -111,7 +111,7 @@ impl<'a> Emitter<'a> {
|
||||
if lty != "ptr" {
|
||||
continue;
|
||||
}
|
||||
// FROZEN ABI (embedding boundary) — see DESIGN.md §"Embedding ABI".
|
||||
// FROZEN ABI (embedding boundary) — see design/contracts/embedding-abi.md.
|
||||
let off = 8 + (j as i64) * 8;
|
||||
let addr_id = local;
|
||||
local += 1;
|
||||
|
||||
@@ -99,9 +99,10 @@ pub enum CodegenError {
|
||||
|
||||
/// `llvm_type` was asked to lower an AILang [`Type`] it does not
|
||||
/// know how to represent. In the MVP this fires for an unresolved
|
||||
/// rigid `Type::Var` reaching codegen (a substitution bug; see
|
||||
/// Iter 13b notes in `DESIGN.md`) or for any non-`Con`/`Fn`/`Var`
|
||||
/// shape that has not yet been wired through.
|
||||
/// rigid `Type::Var` reaching codegen (a substitution bug: a
|
||||
/// parameterised-ADT type argument that monomorphisation should
|
||||
/// have substituted to a concrete type before codegen) or for any
|
||||
/// non-`Con`/`Fn`/`Var` shape that has not yet been wired through.
|
||||
#[error("unsupported type: {0}")]
|
||||
UnsupportedType(String),
|
||||
|
||||
@@ -175,8 +176,8 @@ pub enum Target {
|
||||
/// a mandatory leading `ptr %ctx` (per-thread embedding context)
|
||||
/// published via `@__ail_tls_ctx` around the unchanged internal
|
||||
/// call. The ctx-threaded C signature is the M2 shape; the
|
||||
/// value/record layout is frozen as of M3 (DESIGN.md §"Embedding
|
||||
/// ABI" > "Frozen value layout").
|
||||
/// value/record layout is frozen as of M3
|
||||
/// (design/contracts/frozen-value-layout.md).
|
||||
StaticLib,
|
||||
}
|
||||
|
||||
@@ -682,8 +683,8 @@ fn llvm_scalar(t: &Type) -> &'static str {
|
||||
Type::Con { name, .. } if name == "Int" => "i64",
|
||||
// M3: any other Type::Con reaching an (export) signature is,
|
||||
// by the Task-2 export gate, a single-ctor scalar record;
|
||||
// it crosses the C ABI as a bare `ptr` (DESIGN.md §"Embedding
|
||||
// ABI" frozen layout — payload pointer).
|
||||
// it crosses the C ABI as a bare `ptr`
|
||||
// (design/contracts/frozen-value-layout.md — payload pointer).
|
||||
Type::Con { .. } => "ptr",
|
||||
_ => "i64",
|
||||
}
|
||||
|
||||
@@ -104,8 +104,8 @@ impl<'a> Emitter<'a> {
|
||||
}
|
||||
compiled.push((v, vty));
|
||||
}
|
||||
// FROZEN ABI (embedding boundary) — DESIGN.md §"Embedding ABI"
|
||||
// > "Frozen value layout". size = 8 + n*8, tag@0, fields@8+i*8.
|
||||
// FROZEN ABI (embedding boundary) —
|
||||
// design/contracts/frozen-value-layout.md. size = 8 + n*8, tag@0, fields@8+i*8.
|
||||
let size_bytes = 8 + (compiled.len() * 8) as i64;
|
||||
let p = self.fresh_ssa();
|
||||
// Iter 17a: pick allocator based on escape analysis. `alloca`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! FROZEN ABI byte-pin — see DESIGN.md §"Embedding ABI" frozen layout.
|
||||
//! FROZEN ABI byte-pin — see design/contracts/frozen-value-layout.md.
|
||||
//! Pins the heap (@ailang_rc_alloc) box layout a boundary-crossing
|
||||
//! single-ctor scalar record takes: size = 8 + n*8, tag i64 @ offset
|
||||
//! 0, fields i64-strided from offset 8. Goes RED if codegen moves an
|
||||
|
||||
@@ -44,8 +44,8 @@ fn staticlib_emits_forwarder_no_main() {
|
||||
}
|
||||
|
||||
/// M3: a single-ctor scalar record param/ret crosses the embedding
|
||||
/// C boundary as a bare `ptr` (the frozen payload pointer — DESIGN.md
|
||||
/// §"Embedding ABI" > "Frozen value layout"). The M2 forwarder body
|
||||
/// C boundary as a bare `ptr` (the frozen payload pointer —
|
||||
/// design/contracts/frozen-value-layout.md). The M2 forwarder body
|
||||
/// is byte-unchanged: leading `ptr %ctx`, TLS save/store/restore, the
|
||||
/// internal `@ail_<module>_<fn>` call carrying no ctx arg and no
|
||||
/// aggregate calling convention (no `sret`/`byval` — a record is a
|
||||
|
||||
@@ -86,7 +86,7 @@ externally-callable entrypoint (Embedding ABI M1). The symbol is
|
||||
author-chosen and decoupled from the internal `ail_<module>_<def>`
|
||||
mangling. M1 requires the fn's parameter and return types to be
|
||||
scalar (`Int`/`Float`) and its effect set to be empty — see
|
||||
DESIGN.md §"Embedding ABI".
|
||||
design/contracts/embedding-abi.md.
|
||||
|
||||
`suppress` clauses silence advisory diagnostics for this def. The
|
||||
`code` MUST be one of the registered codes (today: `over-strict-mode`).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! AST nodes for the AILang language.
|
||||
//!
|
||||
//! **The canonical schema lives in `docs/DESIGN.md` §"Data model"**;
|
||||
//! **The canonical schema lives in `design/contracts/data-model.md`**;
|
||||
//! this module is the Rust-side projection of it. When the two drift,
|
||||
//! `crates/ailang-core/tests/design_schema_drift.rs` fires.
|
||||
//!
|
||||
@@ -208,8 +208,8 @@ pub struct FnDef {
|
||||
/// Iter embedding-abi-m1: when `Some(sym)`, this fn is the
|
||||
/// embedding boundary. Codegen's `Target::StaticLib` mode
|
||||
/// additionally emits an externally-visible C entrypoint
|
||||
/// `@<sym>` (signature frozen as of M3 — DESIGN.md §"Embedding
|
||||
/// ABI" > "Frozen value layout") forwarding
|
||||
/// `@<sym>` (signature frozen as of M3 —
|
||||
/// design/contracts/embedding-abi.md) forwarding
|
||||
/// to `@ail_<module>_<fn>`. The symbol is author-chosen and
|
||||
/// decoupled from the `ail_<module>_<def>` mangling so the
|
||||
/// M3-frozen ABI survives module/fn refactors.
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
//! 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. Spec: docs/specs/2026-05-19-design-md-rolesplit.md.
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn root() -> PathBuf {
|
||||
PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/../../"))
|
||||
}
|
||||
|
||||
fn read(rel: &str) -> String {
|
||||
fs::read_to_string(root().join(rel))
|
||||
.unwrap_or_else(|e| panic!("read {rel}: {e}"))
|
||||
}
|
||||
|
||||
fn norm(s: &str) -> String {
|
||||
s.split_whitespace().collect::<Vec<_>>().join(" ")
|
||||
}
|
||||
|
||||
/// Parse the two pipe-tables out of design/INDEX.md.
|
||||
/// Returns (contracts, models) as rows of trimmed cells.
|
||||
fn index_tables() -> (Vec<Vec<String>>, Vec<Vec<String>>) {
|
||||
let idx = read("design/INDEX.md");
|
||||
let mut contracts = Vec::new();
|
||||
let mut models = Vec::new();
|
||||
let mut section = "";
|
||||
for line in idx.lines() {
|
||||
let t = line.trim();
|
||||
if t.starts_with("## Contracts") {
|
||||
section = "c";
|
||||
continue;
|
||||
}
|
||||
if t.starts_with("## Models") {
|
||||
section = "m";
|
||||
continue;
|
||||
}
|
||||
if !t.starts_with('|') {
|
||||
continue;
|
||||
}
|
||||
let cells: Vec<String> = t
|
||||
.trim_matches('|')
|
||||
.split('|')
|
||||
.map(|c| c.trim().to_string())
|
||||
.collect();
|
||||
// skip header + separator rows
|
||||
if cells.iter().any(|c| c.starts_with("---")) {
|
||||
continue;
|
||||
}
|
||||
if cells.first().map(|c| c.as_str()) == Some("id") {
|
||||
continue;
|
||||
}
|
||||
match section {
|
||||
"c" => contracts.push(cells),
|
||||
"m" => models.push(cells),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
(contracts, models)
|
||||
}
|
||||
|
||||
/// A link cell may be a design/ path, a source path, a dual-link
|
||||
/// "A + B", or carry a trailing "(in-source ...)" / "§..." note.
|
||||
/// Resolve to the first concrete path token and check it exists.
|
||||
fn link_target_exists(cell: &str) -> bool {
|
||||
let first = cell.split(" + ").next().unwrap_or(cell).trim();
|
||||
// strip a trailing parenthetical or §-note
|
||||
let path = first
|
||||
.split(" (")
|
||||
.next()
|
||||
.unwrap_or(first)
|
||||
.split(" §")
|
||||
.next()
|
||||
.unwrap_or(first)
|
||||
.trim()
|
||||
.trim_end_matches("//!")
|
||||
.trim();
|
||||
!path.is_empty() && root().join(path).exists()
|
||||
}
|
||||
|
||||
#[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
|
||||
let (contracts, models) = index_tables();
|
||||
assert!(contracts.len() >= 15, "expected >=15 contract rows, got {}", contracts.len());
|
||||
assert!(models.len() >= 5, "expected >=5 model rows, got {}", models.len());
|
||||
for row in contracts.iter().chain(models.iter()) {
|
||||
let link = row.last().expect("row has a link cell");
|
||||
assert!(
|
||||
link_target_exists(link),
|
||||
"INDEX link does not resolve: {:?} (row {:?})",
|
||||
link,
|
||||
row
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_contract_names_a_resolvable_ratifying_test() {
|
||||
// clause 2 — ratifying-test token resolves to a real file under
|
||||
// crates/**/tests, crates/**/src (in-source #[cfg(test)] mod
|
||||
// tests are first-class ratifiers — spec OQ1/OQ2), bench/, or
|
||||
// skills/**/SKILL.md.
|
||||
let (contracts, _) = index_tables();
|
||||
for row in &contracts {
|
||||
// columns: id | consumer/lifetime | ratifying-test | link
|
||||
let rt = &row[2];
|
||||
// take the path token (before any " (" note)
|
||||
let path = rt.split(" (").next().unwrap_or(rt).trim();
|
||||
assert!(
|
||||
root().join(path).exists(),
|
||||
"ratifying-test does not resolve to a real file: {:?} (contract {:?})",
|
||||
path,
|
||||
row[0]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contracts_carry_no_decision_record_prose() {
|
||||
// clause 3 — the conflation tripwire.
|
||||
let dir = root().join("design/contracts");
|
||||
let markers = [
|
||||
"we rejected",
|
||||
"an earlier draft",
|
||||
"Why not other",
|
||||
"was retired in iter",
|
||||
"rollback plan",
|
||||
"previously all",
|
||||
];
|
||||
for entry in fs::read_dir(&dir).expect("design/contracts/ exists") {
|
||||
let p = entry.unwrap().path();
|
||||
if p.extension().and_then(|e| e.to_str()) != Some("md") {
|
||||
continue;
|
||||
}
|
||||
let body = norm(&fs::read_to_string(&p).unwrap());
|
||||
for m in &markers {
|
||||
assert!(
|
||||
!body.contains(m),
|
||||
"decision-record marker {:?} found in contract {:?} — re-conflation",
|
||||
m,
|
||||
p.file_name().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,28 @@
|
||||
//! Drift detection between ast.rs and `docs/DESIGN.md` §"Data model".
|
||||
//! Drift detection between ast.rs and `design/contracts/data-model.md`.
|
||||
//!
|
||||
//! DESIGN.md is the canonical schema source-of-truth. Every AST enum
|
||||
//! (`Term`, `Pattern`, `Type`, `Def`, `Literal`, `ParamMode`) must have a
|
||||
//! JSON-schema anchor (e.g. `"t": "lit"`, `"k": "fn"`) present in that
|
||||
//! document. These tests enforce the property.
|
||||
//! `design/contracts/data-model.md` is the canonical schema
|
||||
//! source-of-truth. Every AST enum (`Term`, `Pattern`, `Type`, `Def`,
|
||||
//! `Literal`, `ParamMode`) must have a JSON-schema anchor (e.g.
|
||||
//! `"t": "lit"`, `"k": "fn"`) present in that document. These tests
|
||||
//! enforce the property.
|
||||
//!
|
||||
//! The exhaustive `match` per enum is the load-bearing mechanism: adding a
|
||||
//! new variant without a matching arm fails compilation before the test runs.
|
||||
//! Once the variant is matched, the test asserts the anchor is present in
|
||||
//! §"Data model" specifically — not anywhere in DESIGN.md. The narrow scope
|
||||
//! is load-bearing: before this tightening, the test scanned the whole
|
||||
//! document, so an anchor that appeared only in §"Decision 11" (or any
|
||||
//! other discussion section) was treated as "present" even though
|
||||
//! §"Data model" — the canonical schema reference — was missing it.
|
||||
//! `design/contracts/data-model.md`. The whole file is the data-model
|
||||
//! contract, so the file boundary now bounds the section — the former
|
||||
//! `## Data model` … `## Pipeline` slicer is no longer needed (the split
|
||||
//! gave the section its own file).
|
||||
|
||||
use ailang_core::ast::{
|
||||
ClassDef, ClassMethod, Constraint, ConstDef, Ctor, Def, FnDef, InstanceDef,
|
||||
InstanceMethod, Literal, Pattern, ParamMode, Suppress, Term, Type, TypeDef,
|
||||
};
|
||||
|
||||
const DESIGN_MD: &str = include_str!("../../../docs/DESIGN.md");
|
||||
|
||||
/// Slice of `DESIGN_MD` covering only §"Data model": from the `## Data model`
|
||||
/// header to the next top-level `## ` header. Returned as a `&'static str`
|
||||
/// because `DESIGN_MD` is itself static. Panics if §"Data model" is missing
|
||||
/// — that itself would be drift.
|
||||
fn data_model_section() -> &'static str {
|
||||
let start = DESIGN_MD
|
||||
.find("## Data model")
|
||||
.expect("DESIGN.md must contain `## Data model` header");
|
||||
let from_start = &DESIGN_MD[start..];
|
||||
match from_start.find("\n## ") {
|
||||
Some(end) => &from_start[..end],
|
||||
None => from_start,
|
||||
}
|
||||
}
|
||||
const DATA_MODEL: &str = include_str!("../../../design/contracts/data-model.md");
|
||||
|
||||
/// Every `Term` variant must have its JSON-schema anchor present in
|
||||
/// DESIGN.md §"Data model". An LLM author cannot produce a term variant
|
||||
/// design/contracts/data-model.md. An LLM author cannot produce a term variant
|
||||
/// whose `"t"` tag is absent from the canonical schema document.
|
||||
#[test]
|
||||
fn design_md_anchors_every_term_variant() {
|
||||
@@ -171,15 +156,15 @@ fn design_md_anchors_every_term_variant() {
|
||||
Term::Recur { .. } => "recur",
|
||||
};
|
||||
assert!(
|
||||
data_model_section().contains(anchor),
|
||||
"DESIGN.md §Data-model is missing anchor `{anchor}` for a Term variant — \
|
||||
add it to docs/DESIGN.md"
|
||||
DATA_MODEL.contains(anchor),
|
||||
"design/contracts/data-model.md is missing anchor `{anchor}` for a Term variant — \
|
||||
add it to design/contracts/data-model.md"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Every `Pattern` variant must have its JSON-schema anchor present in
|
||||
/// DESIGN.md §"Data model". Missing anchors mean an LLM cannot produce
|
||||
/// design/contracts/data-model.md. Missing anchors mean an LLM cannot produce
|
||||
/// the corresponding pattern form.
|
||||
#[test]
|
||||
fn design_md_anchors_every_pattern_variant() {
|
||||
@@ -201,14 +186,14 @@ fn design_md_anchors_every_pattern_variant() {
|
||||
Pattern::Ctor { .. } => "ctor",
|
||||
};
|
||||
assert!(
|
||||
data_model_section().contains(anchor),
|
||||
"DESIGN.md §Data-model is missing anchor `{anchor}` for a Pattern variant"
|
||||
DATA_MODEL.contains(anchor),
|
||||
"design/contracts/data-model.md is missing anchor `{anchor}` for a Pattern variant"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Every `Type` variant must have its JSON-schema anchor present in
|
||||
/// DESIGN.md §"Data model". The `"k"` discriminator is load-bearing for
|
||||
/// design/contracts/data-model.md. The `"k"` discriminator is load-bearing for
|
||||
/// the codegen and typechecker; an LLM must know all four forms.
|
||||
#[test]
|
||||
fn design_md_anchors_every_type_variant() {
|
||||
@@ -234,14 +219,14 @@ fn design_md_anchors_every_type_variant() {
|
||||
Type::Forall { .. } => "forall",
|
||||
};
|
||||
assert!(
|
||||
data_model_section().contains(anchor),
|
||||
"DESIGN.md §Data-model is missing anchor `{anchor}` for a Type variant"
|
||||
DATA_MODEL.contains(anchor),
|
||||
"design/contracts/data-model.md is missing anchor `{anchor}` for a Type variant"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Every `Literal` variant must have its JSON-schema anchor present in
|
||||
/// DESIGN.md §"Data model". The `"kind"` discriminator identifies the
|
||||
/// design/contracts/data-model.md. The `"kind"` discriminator identifies the
|
||||
/// literal type; an LLM cannot produce a literal it hasn't seen in the
|
||||
/// schema.
|
||||
#[test]
|
||||
@@ -263,14 +248,14 @@ fn design_md_anchors_every_literal_variant() {
|
||||
Literal::Float { .. } => "float",
|
||||
};
|
||||
assert!(
|
||||
data_model_section().contains(anchor),
|
||||
"DESIGN.md §Data-model is missing anchor `{anchor}` for a Literal variant"
|
||||
DATA_MODEL.contains(anchor),
|
||||
"design/contracts/data-model.md is missing anchor `{anchor}` for a Literal variant"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Every `Def` kind must have its JSON-schema anchor present in
|
||||
/// DESIGN.md §"Data model". All five kinds (`fn`, `const`, `type`,
|
||||
/// design/contracts/data-model.md. All five kinds (`fn`, `const`, `type`,
|
||||
/// `class`, `instance`) must be documented so an LLM can write
|
||||
/// any kind of top-level definition.
|
||||
#[test]
|
||||
@@ -335,14 +320,14 @@ fn design_md_anchors_every_def_kind() {
|
||||
Def::Instance(_) => "instance",
|
||||
};
|
||||
assert!(
|
||||
data_model_section().contains(anchor),
|
||||
"DESIGN.md §Data-model is missing anchor `{anchor}` for a Def kind"
|
||||
DATA_MODEL.contains(anchor),
|
||||
"design/contracts/data-model.md is missing anchor `{anchor}` for a Def kind"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Every `ParamMode` variant must have its serialized string form present
|
||||
/// in DESIGN.md §"Data model". The mode annotations are load-bearing for
|
||||
/// in design/contracts/data-model.md. The mode annotations are load-bearing for
|
||||
/// ownership checking; an LLM author must know all three forms.
|
||||
#[test]
|
||||
fn design_md_anchors_every_parammode_variant() {
|
||||
@@ -359,13 +344,13 @@ fn design_md_anchors_every_parammode_variant() {
|
||||
ParamMode::Borrow => "borrow",
|
||||
};
|
||||
assert!(
|
||||
data_model_section().contains(anchor),
|
||||
"DESIGN.md §Data-model is missing anchor `{anchor}` for a ParamMode variant"
|
||||
DATA_MODEL.contains(anchor),
|
||||
"design/contracts/data-model.md is missing anchor `{anchor}` for a ParamMode variant"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Nested struct key anchors must be present in DESIGN.md §"Data model".
|
||||
/// Nested struct key anchors must be present in design/contracts/data-model.md.
|
||||
/// These keys appear inside `Suppress`, `ClassMethod`, `InstanceMethod`,
|
||||
/// and `Type::Forall` — they are not discriminators but they ARE part
|
||||
/// of the canonical JSON schema and must remain documented.
|
||||
@@ -396,36 +381,10 @@ fn design_md_anchors_nested_struct_keys() {
|
||||
|
||||
for anchor in anchors {
|
||||
assert!(
|
||||
data_model_section().contains(anchor),
|
||||
"DESIGN.md §Data-model is missing nested-struct-key anchor `{anchor}`"
|
||||
DATA_MODEL.contains(anchor),
|
||||
"design/contracts/data-model.md is missing nested-struct-key anchor `{anchor}`"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Pin the §"Data model" section extractor itself: it must return a
|
||||
/// non-empty slice starting with `## Data model`, and it must NOT bleed
|
||||
/// into the next top-level section (`## Pipeline` today). A bug in the
|
||||
/// extractor would otherwise silently widen every drift test's scope
|
||||
/// back toward full-document scanning — the failure mode this file's
|
||||
/// tightening exists to prevent.
|
||||
#[test]
|
||||
fn data_model_section_is_bounded() {
|
||||
let section = data_model_section();
|
||||
assert!(
|
||||
section.starts_with("## Data model"),
|
||||
"section must start at the §Data model header; got first 40 bytes: {:?}",
|
||||
§ion[..40.min(section.len())]
|
||||
);
|
||||
assert!(
|
||||
!section.contains("\n## Pipeline"),
|
||||
"section must stop before the next top-level header (\\n## Pipeline); \
|
||||
extractor regressed to whole-document scanning"
|
||||
);
|
||||
assert!(
|
||||
section.len() > 1000,
|
||||
"§Data model is the canonical schema reference; a slice shorter than \
|
||||
1 KB suggests the extractor truncated; got {} bytes",
|
||||
section.len()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Regression pin: the canonical docs must stay present-tense-honest.
|
||||
//!
|
||||
//! `docs/DESIGN.md` (and `docs/PROSE_ROUNDTRIP.md`) must not assert
|
||||
//! The `design/` ledger (and `docs/PROSE_ROUNDTRIP.md`) must not assert
|
||||
//! something exists/works/changed that does not. This pin fails if any
|
||||
//! swept-and-corrected Wunschdenken / post-mortem string reappears, or
|
||||
//! if any present-tense corrected anchor (incl. the protected
|
||||
@@ -25,70 +25,107 @@ fn norm(s: &str) -> String {
|
||||
s.split_whitespace().collect::<Vec<_>>().join(" ")
|
||||
}
|
||||
|
||||
/// The `design/` prose set the absent-pins span after the role-split
|
||||
/// (these pins formerly scanned the single canonical design doc): the
|
||||
/// Boehm/Decision-9 narrative
|
||||
/// (`models/rc-uniqueness.md`), float semantics
|
||||
/// (`contracts/float-semantics.md`), the typeclass contract incl.
|
||||
/// prelude classes & "does NOT commit to"
|
||||
/// (`contracts/typeclasses.md`), the effects model
|
||||
/// (`models/effects.md`), and the authoring-surface notation model
|
||||
/// (`models/authoring-surface.md`). A swept Wunschdenken /
|
||||
/// doc-archaeology string must be absent from **all** of them, so the
|
||||
/// reads are joined and a single absence assertion covers the set.
|
||||
fn design_corpus() -> String {
|
||||
[
|
||||
read("design/models/rc-uniqueness.md"),
|
||||
read("design/contracts/float-semantics.md"),
|
||||
read("design/contracts/typeclasses.md"),
|
||||
read("design/models/effects.md"),
|
||||
read("design/models/authoring-surface.md"),
|
||||
]
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn design_md_has_no_wunschdenken() {
|
||||
let d = norm(&read("docs/DESIGN.md"));
|
||||
let d = norm(&design_corpus());
|
||||
assert!(!d.contains("is on the path to retirement"),
|
||||
"DESIGN.md: Boehm retirement is forward intent — it lives in docs/roadmap.md, not DESIGN.md");
|
||||
"design/: Boehm retirement is forward intent — it lives in docs/roadmap.md, not the ledger");
|
||||
assert!(!d.contains("Will back `Show Float`"),
|
||||
"DESIGN.md: 'Will back Show Float' asserts a future capability as fact");
|
||||
"design/: 'Will back Show Float' asserts a future capability as fact");
|
||||
assert!(!d.contains("codegen ships in a future iter"),
|
||||
"DESIGN.md: float_to_str codegen is reserved/not-yet-shipped, not a promised future ship");
|
||||
"design/: float_to_str codegen is reserved/not-yet-shipped, not a promised future ship");
|
||||
assert!(!d.contains("may land in a future iteration"),
|
||||
"DESIGN.md: auto-derivation 'may land in a future iteration' is Wunschdenken");
|
||||
"design/: auto-derivation 'may land in a future iteration' is Wunschdenken");
|
||||
assert!(!d.contains("If/when concurrency arrives"),
|
||||
"DESIGN.md: 'if/when concurrency arrives … will be' is forward conditional");
|
||||
"design/: 'if/when concurrency arrives … will be' is forward conditional");
|
||||
assert!(!d.contains("was deferred — all three layer additively"),
|
||||
"DESIGN.md: deferred future integration paths are roadmap intent, not DESIGN.md");
|
||||
"design/: deferred future integration paths are roadmap intent, not the ledger");
|
||||
assert!(!d.contains("was deferred from milestone 22 entirely"),
|
||||
"DESIGN.md: 'was deferred from milestone 22 entirely … A future iter ships' is history+Wunschdenken (Task 4 Step 14 catch-all)");
|
||||
"design/: 'was deferred from milestone 22 entirely … A future iter ships' is history+Wunschdenken");
|
||||
assert!(!d.contains("A future iter ships the full prose projection"),
|
||||
"DESIGN.md: Form-B class/instance prose 'a future iter ships' is forward intent → roadmap");
|
||||
"design/: Form-B class/instance prose 'a future iter ships' is forward intent → roadmap");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn design_md_has_no_doc_archaeology() {
|
||||
let d = norm(&read("docs/DESIGN.md"));
|
||||
let d = norm(&design_corpus());
|
||||
assert!(!d.contains("An earlier draft of this Decision said"),
|
||||
"DESIGN.md: doc-archaeology ('an earlier draft said … a follow-up replaced it') lives in docs/journals/");
|
||||
"design/: doc-archaeology ('an earlier draft said … a follow-up replaced it') lives in docs/journals/");
|
||||
assert!(!d.contains("previously all diagnostics were `Error`"),
|
||||
"DESIGN.md: 'previously all diagnostics were Error' is project history → docs/journals/");
|
||||
"design/: 'previously all diagnostics were Error' is project history → docs/journals/");
|
||||
assert!(!d.contains("What did change in 2026-05-13 (this milestone) is"),
|
||||
"DESIGN.md: 'what did change in <date> (this milestone)' is post-mortem narrative");
|
||||
"design/: 'what did change in <date> (this milestone)' is post-mortem narrative");
|
||||
assert!(!d.contains("primitives were retired 2026-05-14 in iter rpe.1"),
|
||||
"DESIGN.md: 'X were retired <date> in iter Y' is history → docs/journals/");
|
||||
"design/: 'X were retired <date> in iter Y' is history → docs/journals/");
|
||||
assert!(!d.contains("were retired in iter rpe.1 (2026-05-14)"),
|
||||
"DESIGN.md: the second per-type-print-op retirement-narrative sentence is history");
|
||||
"design/: the second per-type-print-op retirement-narrative sentence is history");
|
||||
assert!(!d.contains("originally listed here as the eighth fixture, was retired"),
|
||||
"DESIGN.md: 'originally listed … was retired by milestone X' is doc-archaeology");
|
||||
"design/: 'originally listed … was retired by milestone X' is doc-archaeology");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn design_md_present_tense_anchors_present() {
|
||||
let d = norm(&read("docs/DESIGN.md"));
|
||||
// the discriminator meta-subsection (Task 3)
|
||||
assert!(d.contains("the honesty rule it holds itself to"),
|
||||
"DESIGN.md must carry the discriminator meta-subsection heading");
|
||||
assert!(d.contains("whether the document asserts something exists, works, or changed that does not"),
|
||||
"DESIGN.md discriminator must state the operational test");
|
||||
// Each anchor is read from its post-split home (spec Appendix
|
||||
// relocation map). Three honest-reserved rationale anchors
|
||||
// ("Regions were considered and rejected", the "does not infer
|
||||
// everything" bullet, the Form-B prose-placeholder state) sat in
|
||||
// `### What this Decision deliberately does not do` / `### What
|
||||
// this decision does NOT commit to`, which the Appendix routes to
|
||||
// the decision-record journal — so the journal is part of the
|
||||
// canonical anchor corpus, not a contract surface.
|
||||
let honesty = norm(&read("design/contracts/honesty-rule.md"));
|
||||
let scope = norm(&read("design/contracts/scope-boundaries.md"));
|
||||
let memory = norm(&read("design/contracts/memory-model.md"));
|
||||
let pipeline = norm(&read("design/models/pipeline.md"));
|
||||
let str_abi = norm(&read("design/contracts/str-abi.md"));
|
||||
let typeclasses = norm(&read("design/contracts/typeclasses.md"));
|
||||
let records = norm(&read("docs/journals/2026-05-19-design-decision-records.md"));
|
||||
|
||||
// the discriminator meta-subsection -> contracts/honesty-rule.md
|
||||
assert!(honesty.contains("the honesty rule it holds itself to"),
|
||||
"honesty-rule.md must carry the discriminator meta-subsection heading");
|
||||
assert!(honesty.contains("whether the document asserts something exists, works, or changed that does not"),
|
||||
"honesty-rule.md discriminator must state the operational test");
|
||||
// protected honest-reserved exceptions — over-correction guard
|
||||
assert!(d.contains("`Diverge` is a reserved effect name with no op and no codegen"),
|
||||
"the gold-form honest-reserved anchor must remain (do not over-strip)");
|
||||
assert!(d.contains("Regions were considered and rejected"),
|
||||
"the present-tense design-rationale exclusion must remain (do not over-strip)");
|
||||
assert!(d.contains("a tiebreaker, not a rationale"),
|
||||
"the self-labelled tiebreaker is honest and stays (do not over-strip)");
|
||||
// corrected present-tense anchors (Task 4)
|
||||
assert!(d.contains("`--alloc=gc` selects the transitional Boehm backend"),
|
||||
"DESIGN.md must describe Boehm present-tense, not as 'on the path to retirement'");
|
||||
assert!(d.contains("type-installed; codegen is reserved and not yet shipped"),
|
||||
"float_to_str must be present-tense honest-reserved");
|
||||
assert!(d.contains("AILang demands annotations *because* the LLM author can produce them effortlessly"),
|
||||
"the 'does not infer everything' bullet must be present-tense (no doc-archaeology)");
|
||||
assert!(d.contains("`io/print_str` is the only built-in direct-output effect-op"),
|
||||
"the print-op set must be stated present-tense, not as a retirement narrative");
|
||||
assert!(d.contains("the render in `crates/ailang-prose/src/lib.rs` is a placeholder and informational only"),
|
||||
"the Form-B class/instance prose-projection state must be present-tense (Task 4 Step 14 catch-all)");
|
||||
assert!(scope.contains("`Diverge` is a reserved effect name with no op and no codegen"),
|
||||
"the gold-form honest-reserved anchor must remain in scope-boundaries.md (do not over-strip)");
|
||||
assert!(records.contains("Regions were considered and rejected"),
|
||||
"the present-tense design-rationale exclusion must remain in the decision-records (do not over-strip)");
|
||||
assert!(memory.contains("a tiebreaker, not a rationale"),
|
||||
"the self-labelled tiebreaker is honest and stays in memory-model.md (do not over-strip)");
|
||||
// corrected present-tense anchors
|
||||
assert!(pipeline.contains("`--alloc=gc` selects the transitional Boehm backend"),
|
||||
"models/pipeline.md must describe Boehm present-tense, not as 'on the path to retirement'");
|
||||
assert!(str_abi.contains("type-installed; codegen is reserved and not yet shipped"),
|
||||
"float_to_str must be present-tense honest-reserved in str-abi.md");
|
||||
assert!(records.contains("AILang demands annotations *because* the LLM author can produce them effortlessly"),
|
||||
"the 'does not infer everything' bullet must be present-tense in the decision-records (no doc-archaeology)");
|
||||
assert!(typeclasses.contains("`io/print_str` is the only built-in direct-output effect-op"),
|
||||
"the print-op set must be stated present-tense in typeclasses.md, not as a retirement narrative");
|
||||
assert!(records.contains("the render in `crates/ailang-prose/src/lib.rs` is a placeholder and informational only"),
|
||||
"the Form-B class/instance prose-projection state must be present-tense in the decision-records");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -109,7 +146,7 @@ fn form_a_scalar_param_carveout_present_and_old_rule_absent() {
|
||||
// (planner Step-5 item 6: line-wrap is structurally discharged,
|
||||
// the substrings need not be contiguous in the .md source).
|
||||
let f = norm(FORM_A_SPEC);
|
||||
let d = norm(&read("docs/DESIGN.md"));
|
||||
let d = norm(&read("design/contracts/embedding-abi.md"));
|
||||
|
||||
// --- ABSENT: the four old unconditional phrasings must be gone ---
|
||||
assert!(!f.contains("Mode annotations on every `(fn ...)` parameter."),
|
||||
@@ -131,7 +168,7 @@ fn form_a_scalar_param_carveout_present_and_old_rule_absent() {
|
||||
assert!(f.contains("Putting a mode on a scalar parameter."),
|
||||
"form_a.md Pitfalls must carry the inverse pitfall — over-wrapping a scalar (site 4)");
|
||||
|
||||
// --- PRESENT: the DESIGN.md §\"Embedding ABI\" mirror ---
|
||||
// --- PRESENT: the design/contracts/embedding-abi.md mirror ---
|
||||
assert!(d.contains("Export parameters are written **bare**: a scalar type carries no `own`/`borrow` mode"),
|
||||
"DESIGN.md §Embedding ABI must mirror the canonical bare-scalar export-param rule");
|
||||
"design/contracts/embedding-abi.md must mirror the canonical bare-scalar export-param rule");
|
||||
}
|
||||
|
||||
@@ -16,21 +16,32 @@ fn read(rel: &str) -> String {
|
||||
.unwrap_or_else(|e| panic!("read {rel}: {e}"))
|
||||
}
|
||||
|
||||
/// Collapse every whitespace run to a single space so a pinned phrase
|
||||
/// split across the design/ files' hard line-wrap (or across the
|
||||
/// join seam) still matches a single-line assertion substring.
|
||||
fn norm(s: &str) -> String {
|
||||
s.split_whitespace().collect::<Vec<_>>().join(" ")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn design_md_effect_prose_is_true() {
|
||||
let d = read("docs/DESIGN.md");
|
||||
// Decision 3 effect prose -> design/models/effects.md; the
|
||||
// "the built-in IO and Diverge ops" absent-pin's home
|
||||
// `## What is not (yet) supported` -> design/contracts/scope-boundaries.md.
|
||||
let d = norm(&[read("design/models/effects.md"),
|
||||
read("design/contracts/scope-boundaries.md")].join("\n"));
|
||||
// fiction absent
|
||||
assert!(!d.contains("row-polymorphic"),
|
||||
"DESIGN.md: the effect row is NOT row-polymorphic (no EffectRow / row var exists)");
|
||||
"design/: the effect row is NOT row-polymorphic (no EffectRow / row var exists)");
|
||||
assert!(!d.contains("`IO` and `Diverge` (for infinite"),
|
||||
"DESIGN.md: `Diverge` is not a wired-up MVP effect");
|
||||
"design/: `Diverge` is not a wired-up MVP effect");
|
||||
assert!(!d.contains("the built-in IO and Diverge ops"),
|
||||
"DESIGN.md §What-is-not-supported: there is no built-in Diverge op");
|
||||
"design/contracts/scope-boundaries.md: there is no built-in Diverge op");
|
||||
// corrected anchors present
|
||||
assert!(d.contains("flat, unordered, closed set of effect names"),
|
||||
"DESIGN.md Decision 3 must describe the real (flat set-equality) effect model");
|
||||
"design/models/effects.md Decision 3 must describe the real (flat set-equality) effect model");
|
||||
assert!(d.contains("`Diverge` (for non-termination) is reserved"),
|
||||
"DESIGN.md Decision 3 must state Diverge is reserved/unimplemented");
|
||||
"design/models/effects.md Decision 3 must state Diverge is reserved/unimplemented");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! [`ailang_core::ast::Module`] values. The JSON-AST in `ailang-core`
|
||||
//! remains the canonical, hashable, content-addressed source of truth;
|
||||
//! form (A) is the AI-authoring projection optimised for token-efficient
|
||||
//! production by LLMs (see `docs/DESIGN.md` Decision 6).
|
||||
//! production by LLMs (see `design/contracts/authoring-surface.md`).
|
||||
//!
|
||||
//! ## Modules
|
||||
//!
|
||||
|
||||
@@ -75,10 +75,12 @@
|
||||
//! lit-form ::= integer | "true" | "false" | string
|
||||
//! ```
|
||||
//!
|
||||
//! Notes on the form (deviations from the spec in DESIGN.md Decision 6):
|
||||
//! Notes on the form (the binding constraints are in
|
||||
//! design/contracts/authoring-surface.md; the form-refinement
|
||||
//! rationale is in docs/journals/2026-05-19-design-decision-records.md):
|
||||
//!
|
||||
//! - The `lam` form carries `paramTypes`, a `ret` type, and an
|
||||
//! optional `effects` clause. The original DESIGN.md sketch left
|
||||
//! optional `effects` clause. The early form sketch left
|
||||
//! these out; the AST stores them per-lambda and so the form must
|
||||
//! round-trip them.
|
||||
//! - The `import` form admits an optional `as` alias to round-trip
|
||||
|
||||
@@ -125,7 +125,7 @@ fn write_type_def(out: &mut String, td: &TypeDef, level: usize) {
|
||||
}
|
||||
// Iter 18e: `(drop-iterative)` annotation. Printed last (after
|
||||
// every ctor) so the canonical form lands consistent with the
|
||||
// DESIGN.md example. Omitted when `drop_iterative == false`.
|
||||
// design/contracts/data-model.md example. Omitted when `drop_iterative == false`.
|
||||
if td.drop_iterative {
|
||||
out.push('\n');
|
||||
indent(out, level + 1);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//!
|
||||
//! 1. `parse_is_deterministic_over_every_ail_fixture` — pins
|
||||
//! **parse-determinism** (property 1 of the post-form-a Roundtrip
|
||||
//! Invariant, DESIGN.md §"Roundtrip Invariant"). For every
|
||||
//! Invariant, design/contracts/roundtrip-invariant.md). For every
|
||||
//! well-formed `.ail` text `t`, `parse(t)` produces the same
|
||||
//! canonical bytes every invocation. The parser is a pure
|
||||
//! function of input — no randomness, no time dependence, no
|
||||
@@ -69,7 +69,7 @@ fn list_ail_fixtures() -> Vec<PathBuf> {
|
||||
}
|
||||
|
||||
/// Pins **idempotency-under-print** (property 2 of the post-form-a
|
||||
/// Roundtrip Invariant, DESIGN.md §"Roundtrip Invariant"): for every
|
||||
/// Roundtrip Invariant, design/contracts/roundtrip-invariant.md): for every
|
||||
/// well-formed `.ail` text `t`, the composition `parse → print → parse`
|
||||
/// is idempotent on the AST.
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user