convention: counter-prefix file naming across docs/specs/, docs/plans/, design/contracts/, design/models/

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.
This commit is contained in:
2026-05-28 13:31:31 +02:00
parent 7b8596cef0
commit 832375f2ac
227 changed files with 785 additions and 751 deletions
+2 -2
View File
@@ -1098,7 +1098,7 @@ fn workspace_error_to_diagnostic(
// typeclass-coherence diagnostics emitted from
// `workspace::build_registry`. The codes follow the
// diagnostic-categories section of
// `design/contracts/typeclasses.md`.
// `design/contracts/0013-typeclasses.md`.
W::OrphanInstance {
class,
type_repr,
@@ -1568,7 +1568,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/contracts/data-model.md.
/// `Var`/`Wild` — see design/contracts/0002-data-model.md.
fn bind_pattern(
p: &ailang_core::ast::Pattern,
scope: &mut std::collections::HashSet<String>,
@@ -1,5 +1,5 @@
//! Pin for the iter-24.3 codegen `import_map`-fallback path
//! (design/contracts/typeclasses.md, "Cross-module references in
//! (design/contracts/0013-typeclasses.md, "Cross-module references in
//! synthesised bodies" invariant 2).
//!
//! Property protected: post-mono synthesised body cross-module
+1 -1
View File
@@ -2742,7 +2742,7 @@ fn str_clone_drop_balances_rc_stats() {
/// 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/contracts/str-abi.md — str_clone only reads len + bytes +
/// design/contracts/0011-str-abi.md — str_clone only reads len + bytes +
/// NUL, never the rc_header.
#[test]
fn str_clone_cross_realisation_uniform_abi() {
+1 -1
View File
@@ -1,6 +1,6 @@
/* Embedding-ABI M3 record round-trip C host (own + borrow via a
* compile-time -DBORROW switch). Frozen value layout
* (design/contracts/frozen-value-layout.md):
* (design/contracts/0006-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
+1 -1
View File
@@ -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/contracts/frozen-value-layout.md):
* (design/contracts/0006-frozen-value-layout.md):
*
* State (24-byte payload):
* p - 8 .. p uint64_t refcount header (set to 1 by ailang_rc_alloc)
+2 -2
View File
@@ -2,7 +2,7 @@
//!
//! Property protected: a polymorphic Eq/Ord helper invoked at Float
//! fires `no-instance` (since Float has neither Eq nor Ord instance
//! per design/contracts/float-semantics.md) AND the diagnostic message
//! per design/contracts/0005-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.
@@ -38,7 +38,7 @@ fn eq_at_float_fires_float_aware_noinstance() {
// 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("design/contracts/float-semantics.md"),
no_inst.message.contains("design/contracts/0005-float-semantics.md"),
"expected NoInstance message to cross-reference the float-semantics contract, got: {:?}",
no_inst.message
);
+2 -2
View File
@@ -7,8 +7,8 @@
//! invoked as `(app greet)`.
//!
//! Two independent fieldtest specs flagged the same gap:
//! `docs/specs/2026-05-15-fieldtest-mut-local.md` F3 and
//! `docs/specs/2026-05-18-fieldtest-loop-recur.md` spec_gap. The
//! `docs/specs/0030-fieldtest-mut-local.md` F3 and
//! `docs/specs/0035-fieldtest-loop-recur.md` spec_gap. The
//! Form-A surface refusing nullary calls had no schema backing —
//! `Term::App.args` is `[Term...]` (zero-or-more), so the parser
//! guard was an unbacked rule.
@@ -1,5 +1,5 @@
//! Pin for the iter-24.3 FreeFnCall constraint-residual-push
//! invariant (design/contracts/typeclasses.md, "Cross-module
//! invariant (design/contracts/0013-typeclasses.md, "Cross-module
//! references in synthesised bodies" invariant 3).
//!
//! Property protected: bare-name references to polymorphic free fns
+4 -4
View File
@@ -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/contracts/typeclasses.md, so the
//! cross-references design/contracts/0013-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,11 +40,11 @@ fn print_on_fn_type_fires_show_aware_no_instance() {
no_inst.message
);
// Must cross-reference design/contracts/typeclasses.md — that's
// Must cross-reference design/contracts/0013-typeclasses.md — that's
// the canonical contract naming which types ship with Show.
assert!(
no_inst.message.contains("design/contracts/typeclasses.md"),
"expected design/contracts/typeclasses.md cross-reference, got message: {:?}",
no_inst.message.contains("design/contracts/0013-typeclasses.md"),
"expected design/contracts/0013-typeclasses.md cross-reference, got message: {:?}",
no_inst.message
);
}
+1 -1
View File
@@ -39,7 +39,7 @@
//! - `module-cycle` — workspace loader (Iter 5b, in the CLI path)
//! - `module-name-mismatch` — workspace loader (Iter 5b, in the CLI path)
//! - `module-hash-mismatch` — workspace loader (Iter 5b, in the CLI path)
//! - `tail-call-not-in-tail-position` (see `design/contracts/tail-calls.md`)
//! - `tail-call-not-in-tail-position` (see `design/contracts/0012-tail-calls.md`)
//! - `use-after-consume` — `ctx`: `{"binder": "<n>"}` (Iter 18c.2);
//! carries non-empty [`Diagnostic::suggested_rewrites`] showing how to
//! spell the fix in form-A AILang.
+9 -9
View File
@@ -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/contracts/embedding-abi.md):
/// of M3 (design/contracts/0003-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
@@ -556,7 +556,7 @@ pub enum CheckError {
/// a `Term::App { tail: true, .. }` or
/// `Term::Do { tail: true, .. }` was found in a non-tail position.
/// Code: `tail-call-not-in-tail-position`. See `design/contracts/tail-calls.md`.
/// Code: `tail-call-not-in-tail-position`. See `design/contracts/0012-tail-calls.md`.
#[error("call marked `tail` is not in tail position")]
TailCallNotInTailPosition,
@@ -858,7 +858,7 @@ impl CheckError {
}
// Float-aware addendum on `NoInstance`. When the
// unresolved class is `Eq` or `Ord` AND the type is `Float`,
// append a cross-reference to design/contracts/float-semantics.md so
// append a cross-reference to design/contracts/0005-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
@@ -874,7 +874,7 @@ impl CheckError {
orderability per IEEE-754); use float_eq / float_lt \
(and siblings float_ne / float_le / float_gt / float_ge) \
for explicit IEEE-aware comparison. \
See design/contracts/float-semantics.md.",
See design/contracts/0005-float-semantics.md.",
d.message
);
} else if class == "prelude.Show" {
@@ -883,12 +883,12 @@ impl CheckError {
// 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/contracts/typeclasses.md so the author learns
// design/contracts/0013-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/contracts/typeclasses.md. \
the prelude; see design/contracts/0013-typeclasses.md. \
User types declare their own \
`instance prelude.Show <T>` in the type's defining \
module per the typeclass design coherence.",
@@ -2643,10 +2643,10 @@ fn constraint_type_matches(declared: &Type, residual: &Type) -> bool {
/// verifies that every `Term::App { tail: true, .. }` and
/// `Term::Do { tail: true, .. }` actually sits in tail position, per
/// the explicit-tail-call contract
/// (`design/contracts/tail-calls.md`).
/// (`design/contracts/0012-tail-calls.md`).
///
/// `is_tail` is the tail-context flag for the term currently being
/// visited. The propagation rules (from design/contracts/tail-calls.md):
/// visited. The propagation rules (from design/contracts/0012-tail-calls.md):
///
/// - The body of a fn / Lam is visited with `is_tail = true`.
/// - `Match { scrutinee, arms }`: the scrutinee is **not** in tail
@@ -6031,7 +6031,7 @@ 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/contracts/float-semantics.md — even when the match arm reaches `check` through
/// design/contracts/0005-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
+2 -2
View File
@@ -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/contracts/embedding-abi.md.
// FROZEN ABI (embedding boundary) — see design/contracts/0003-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/contracts/embedding-abi.md.
// FROZEN ABI (embedding boundary) — see design/contracts/0003-embedding-abi.md.
let off = 8 + (j as i64) * 8;
let addr_id = local;
local += 1;
+3 -3
View File
@@ -174,7 +174,7 @@ pub enum Target {
/// 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/contracts/frozen-value-layout.md).
/// (design/contracts/0006-frozen-value-layout.md).
StaticLib,
}
@@ -687,7 +687,7 @@ fn llvm_scalar(t: &Type) -> &'static str {
// 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/contracts/frozen-value-layout.md — payload pointer).
// (design/contracts/0006-frozen-value-layout.md — payload pointer).
Type::Con { .. } => "ptr",
_ => "i64",
}
@@ -2813,7 +2813,7 @@ impl<'a> Emitter<'a> {
/// allocates an `Ordering` ctor per call via the
/// `compare__Int` → `match` path, which surfaces as a
/// `bench_closure_chain` regression. See
/// `design/contracts/prelude-classes.md` for the full surface
/// `design/contracts/0017-prelude-classes.md` for the full surface
/// and the Int-only asymmetry rationale.
fn try_emit_primitive_instance_body(
&mut self,
+1 -1
View File
@@ -105,7 +105,7 @@ impl<'a> Emitter<'a> {
compiled.push((v, vty));
}
// FROZEN ABI (embedding boundary) —
// design/contracts/frozen-value-layout.md. size = 8 + n*8, tag@0, fields@8+i*8.
// design/contracts/0006-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();
// pick allocator based on escape analysis. `alloca`
@@ -1,4 +1,4 @@
//! FROZEN ABI byte-pin — see design/contracts/frozen-value-layout.md.
//! FROZEN ABI byte-pin — see design/contracts/0006-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
@@ -45,7 +45,7 @@ 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/contracts/frozen-value-layout.md). The M2 forwarder body
/// design/contracts/0006-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
+2 -2
View File
@@ -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/contracts/embedding-abi.md.
design/contracts/0003-embedding-abi.md.
`suppress` clauses silence advisory diagnostics for this def. The
`code` MUST be one of the registered codes (today: `over-strict-mode`).
@@ -324,7 +324,7 @@ Notes:
both enforced at typecheck (`recur-arity-mismatch`,
`recur-not-in-tail-position`). `loop`/`recur` make no termination
claim: a `loop` with no non-`recur` exit runs forever. See
`docs/specs/2026-05-17-loop-recur.md`.
`docs/specs/0034-loop-recur.md`.
## Patterns
+9 -9
View File
@@ -1,6 +1,6 @@
//! AST nodes for the AILang language.
//!
//! **The canonical schema lives in `design/contracts/data-model.md`**;
//! **The canonical schema lives in `design/contracts/0002-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,7 +208,7 @@ pub struct FnDef {
/// embedding boundary. Codegen's `Target::StaticLib` mode
/// additionally emits an externally-visible C entrypoint
/// `@<sym>` (signature frozen as of M3 —
/// design/contracts/embedding-abi.md) forwarding
/// design/contracts/0003-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.
@@ -256,7 +256,7 @@ pub struct Suppress {
}
/// A typeclass declaration (narrative in
/// `design/contracts/typeclasses.md`).
/// `design/contracts/0013-typeclasses.md`).
///
/// Single-parameter, multi-method, optional-default, optional-superclass
/// typeclass. The `param` is a single string — multi-param classes are
@@ -326,7 +326,7 @@ pub struct ClassMethod {
}
/// An instance declaration (narrative in
/// `design/contracts/typeclasses.md`).
/// `design/contracts/0013-typeclasses.md`).
///
/// `class` is the name of the class being instantiated. `type_` is the
/// concrete type expression the class is applied to — never the class
@@ -360,7 +360,7 @@ pub struct InstanceMethod {
}
/// A class constraint on a polymorphic function (narrative in
/// `design/contracts/typeclasses.md`). `(class, type)` pair where
/// `design/contracts/0013-typeclasses.md`). `(class, type)` pair where
/// `class` is a class name and `type` is a `Type` expression —
/// typically a single `Type::Var` (e.g. `(Eq, a)` for
/// `Eq a => ...`). Concrete-type constraints are legal schema-wise
@@ -412,7 +412,7 @@ pub enum Term {
/// `args` are evaluated left-to-right.
///
/// `tail` marks this call as occurring in tail position (see
/// `design/contracts/tail-calls.md`). When set, codegen lowers
/// `design/contracts/0012-tail-calls.md`). When set, codegen lowers
/// the call as `musttail call`. The flag defaults to `false` and
/// is omitted during canonical-JSON serialisation when unset, so
/// pre-tail-flag fixtures keep bit-identical hashes.
@@ -548,7 +548,7 @@ pub enum Term {
/// loop-carried SSA / phi form is produced by `clang -O2`
/// mem2reg (NOT hand-emitted phi). No totality claim — an
/// infinite loop is legal. See
/// `docs/specs/2026-05-17-loop-recur.md`.
/// `docs/specs/0034-loop-recur.md`.
Loop {
binders: Vec<LoopBinder>,
body: Box<Term>,
@@ -707,7 +707,7 @@ pub enum Type {
/// every entry is `Implicit`; `ret_mode` is omitted when it is
/// `Implicit`, so pre-mode-annotation fixtures hash
/// bit-identically. Full contract in
/// `design/contracts/memory-model.md`.
/// `design/contracts/0008-memory-model.md`.
Fn {
params: Vec<Type>,
#[serde(default, skip_serializing_if = "all_implicit")]
@@ -780,7 +780,7 @@ impl Type {
}
/// Per-parameter / return mode marker on a [`Type::Fn`]. Full
/// contract lives in `design/contracts/memory-model.md`.
/// contract lives in `design/contracts/0008-memory-model.md`.
///
/// `Implicit` is the legacy state for fn-types that were constructed
/// before the borrow/own surface annotations existed. Semantically,
+2 -2
View File
@@ -1090,12 +1090,12 @@ fn is_flat(p: &Pattern) -> bool {
/// Builds the equality-test AST node for a literal pattern. Lowers
/// `(pat-lit <lit>)` to `(if (eq sv <lit>) <body> <fall_k>)`. The
/// emitted `eq` resolves via prelude.Eq's class-method dispatch
/// (per design/contracts/method-dispatch.md) — Int/Bool/Str/Unit
/// (per design/contracts/0016-method-dispatch.md) — Int/Bool/Str/Unit
/// patterns all route through the corresponding primitive instance.
///
/// Float-Literal patterns are hard-rejected at typecheck
/// (`CheckError::FloatPatternNotAllowed`, per
/// design/contracts/float-semantics.md) before this fn is reached,
/// design/contracts/0005-float-semantics.md) before this fn is reached,
/// so the `eq`-dispatch never encounters Float at runtime; the
/// Float arm of the match below remains for AST-level totality.
///
+1 -1
View File
@@ -108,7 +108,7 @@ pub type Result<T> = std::result::Result<T, Error>;
pub const SCHEMA: &str = "ailang/v0";
/// Complete LLM-targeted specification of Form-A — the canonical
/// authoring surface (see `design/contracts/authoring-surface.md`).
/// authoring surface (see `design/contracts/0001-authoring-surface.md`).
/// Embedded verbatim into any prompt that asks an LLM to produce or
/// edit AILang code; in particular, `ail merge-prose` includes it in
/// the round-trip prompt template. The string is the raw bytes of
+2 -2
View File
@@ -4,8 +4,8 @@
//! dangles, a contract loses its ratifying test, docs/DESIGN.md is
//! resurrected, or a design/ body cross-link fails to resolve into
//! the durable tier (clause-5; spec
//! docs/specs/2026-05-19-design-ledger-formal-links.md). Spec for
//! the split: docs/specs/2026-05-19-design-md-rolesplit.md.
//! docs/specs/0046-design-ledger-formal-links.md). Spec for
//! the split: docs/specs/0045-design-md-rolesplit.md.
use std::fs;
use std::path::PathBuf;
+20 -20
View File
@@ -1,6 +1,6 @@
//! Drift detection between ast.rs and `design/contracts/data-model.md`.
//! Drift detection between ast.rs and `design/contracts/0002-data-model.md`.
//!
//! `design/contracts/data-model.md` is the canonical schema
//! `design/contracts/0002-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
@@ -9,7 +9,7 @@
//! 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
//! `design/contracts/data-model.md`. The whole file is the data-model
//! `design/contracts/0002-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).
@@ -19,7 +19,7 @@ use ailang_core::ast::{
InstanceMethod, Literal, Pattern, ParamMode, Suppress, Term, Type, TypeDef,
};
const DATA_MODEL: &str = include_str!("../../../design/contracts/data-model.md");
const DATA_MODEL: &str = include_str!("../../../design/contracts/0002-data-model.md");
/// Scoped substring match: returns `true` iff `anchor` appears inside a
/// fenced code block (``` or ~~~) of `md`. Info-string `jsonc` / `json` /
@@ -47,7 +47,7 @@ fn anchor_in_jsonc_block(md: &str, anchor: &str) -> bool {
}
/// Every `Term` variant must have its JSON-schema anchor present in
/// design/contracts/data-model.md. An LLM author cannot produce a term variant
/// design/contracts/0002-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() {
@@ -182,8 +182,8 @@ fn design_md_anchors_every_term_variant() {
};
assert!(
anchor_in_jsonc_block(DATA_MODEL, anchor),
"design/contracts/data-model.md is missing anchor `{anchor}` for a Term variant — \
add it to design/contracts/data-model.md"
"design/contracts/0002-data-model.md is missing anchor `{anchor}` for a Term variant — \
add it to design/contracts/0002-data-model.md"
);
}
@@ -194,14 +194,14 @@ fn design_md_anchors_every_term_variant() {
for anchor in [r#""param-types""#, r#""ret-type""#] {
assert!(
anchor_in_jsonc_block(DATA_MODEL, anchor),
"design/contracts/data-model.md is missing Term::Lam kebab-key anchor `{anchor}` — \
"design/contracts/0002-data-model.md is missing Term::Lam kebab-key anchor `{anchor}` — \
check the `lam` fenced JSON block"
);
}
}
/// Every `Pattern` variant must have its JSON-schema anchor present in
/// design/contracts/data-model.md. Missing anchors mean an LLM cannot produce
/// design/contracts/0002-data-model.md. Missing anchors mean an LLM cannot produce
/// the corresponding pattern form.
#[test]
fn design_md_anchors_every_pattern_variant() {
@@ -224,13 +224,13 @@ fn design_md_anchors_every_pattern_variant() {
};
assert!(
anchor_in_jsonc_block(DATA_MODEL, anchor),
"design/contracts/data-model.md is missing anchor `{anchor}` for a Pattern variant"
"design/contracts/0002-data-model.md is missing anchor `{anchor}` for a Pattern variant"
);
}
}
/// Every `Type` variant must have its JSON-schema anchor present in
/// design/contracts/data-model.md. The `"k"` discriminator is load-bearing for
/// design/contracts/0002-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() {
@@ -257,13 +257,13 @@ fn design_md_anchors_every_type_variant() {
};
assert!(
anchor_in_jsonc_block(DATA_MODEL, anchor),
"design/contracts/data-model.md is missing anchor `{anchor}` for a Type variant"
"design/contracts/0002-data-model.md is missing anchor `{anchor}` for a Type variant"
);
}
}
/// Every `Literal` variant must have its JSON-schema anchor present in
/// design/contracts/data-model.md. The `"kind"` discriminator identifies the
/// design/contracts/0002-data-model.md. The `"kind"` discriminator identifies the
/// literal type; an LLM cannot produce a literal it hasn't seen in the
/// schema.
#[test]
@@ -286,13 +286,13 @@ fn design_md_anchors_every_literal_variant() {
};
assert!(
anchor_in_jsonc_block(DATA_MODEL, anchor),
"design/contracts/data-model.md is missing anchor `{anchor}` for a Literal variant"
"design/contracts/0002-data-model.md is missing anchor `{anchor}` for a Literal variant"
);
}
}
/// Every `Def` kind must have its JSON-schema anchor present in
/// design/contracts/data-model.md. All five kinds (`fn`, `const`, `type`,
/// design/contracts/0002-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]
@@ -358,13 +358,13 @@ fn design_md_anchors_every_def_kind() {
};
assert!(
anchor_in_jsonc_block(DATA_MODEL, anchor),
"design/contracts/data-model.md is missing anchor `{anchor}` for a Def kind"
"design/contracts/0002-data-model.md is missing anchor `{anchor}` for a Def kind"
);
}
}
/// Every `ParamMode` variant must have its serialized string form present
/// in design/contracts/data-model.md. The mode annotations are load-bearing for
/// in design/contracts/0002-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() {
@@ -382,7 +382,7 @@ fn design_md_anchors_every_parammode_variant() {
};
assert!(
anchor_in_jsonc_block(DATA_MODEL, anchor),
"design/contracts/data-model.md is missing anchor `{anchor}` for a ParamMode variant"
"design/contracts/0002-data-model.md is missing anchor `{anchor}` for a ParamMode variant"
);
}
}
@@ -502,7 +502,7 @@ fn lam_serialises_with_kebab_keys() {
);
}
/// Nested struct key anchors must be present in design/contracts/data-model.md.
/// Nested struct key anchors must be present in design/contracts/0002-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.
@@ -534,7 +534,7 @@ fn design_md_anchors_nested_struct_keys() {
for anchor in anchors {
assert!(
anchor_in_jsonc_block(DATA_MODEL, anchor),
"design/contracts/data-model.md is missing nested-struct-key anchor `{anchor}`"
"design/contracts/0002-data-model.md is missing nested-struct-key anchor `{anchor}`"
);
}
}
+23 -23
View File
@@ -28,25 +28,25 @@ fn norm(s: &str) -> String {
/// The `design/` prose set the absent-pins span after the role-split
/// (these pins formerly scanned the single canonical design doc): the
/// RC + bump memory-model narrative
/// (`models/rc-uniqueness.md`), float semantics
/// (`contracts/float-semantics.md`), the typeclass contract incl.
/// (`models/0004-rc-uniqueness.md`), float semantics
/// (`contracts/0005-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 /
/// (`contracts/0013-typeclasses.md`), the effects model
/// (`models/0002-effects.md`), and the authoring-surface notation model
/// (`models/0001-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/contracts/method-dispatch.md"),
read("design/contracts/prelude-classes.md"),
read("design/contracts/language-constraints.md"),
read("design/models/effects.md"),
read("design/models/authoring-surface.md"),
read("design/models/prose-projection.md"),
read("design/models/0004-rc-uniqueness.md"),
read("design/contracts/0005-float-semantics.md"),
read("design/contracts/0013-typeclasses.md"),
read("design/contracts/0016-method-dispatch.md"),
read("design/contracts/0017-prelude-classes.md"),
read("design/contracts/0015-language-constraints.md"),
read("design/models/0002-effects.md"),
read("design/models/0001-authoring-surface.md"),
read("design/models/0006-prose-projection.md"),
]
.join("\n")
}
@@ -103,13 +103,13 @@ fn design_md_present_tense_anchors_present() {
// ever lived in the (now-removed) decision-records annex are
// dropped — anything the design/ ledger truly needs lives in
// design/contracts/ or design/models/.
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 str_abi = norm(&read("design/contracts/str-abi.md"));
let prelude_classes = norm(&read("design/contracts/prelude-classes.md"));
let honesty = norm(&read("design/contracts/0007-honesty-rule.md"));
let scope = norm(&read("design/contracts/0010-scope-boundaries.md"));
let memory = norm(&read("design/contracts/0008-memory-model.md"));
let str_abi = norm(&read("design/contracts/0011-str-abi.md"));
let prelude_classes = norm(&read("design/contracts/0017-prelude-classes.md"));
// the discriminator meta-subsection -> contracts/honesty-rule.md
// the discriminator meta-subsection -> contracts/0007-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"),
@@ -144,7 +144,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("design/contracts/embedding-abi.md"));
let d = norm(&read("design/contracts/0003-embedding-abi.md"));
// --- ABSENT: the four old unconditional phrasings must be gone ---
assert!(!f.contains("Mode annotations on every `(fn ...)` parameter."),
@@ -166,7 +166,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/contracts/embedding-abi.md mirror ---
// --- PRESENT: the design/contracts/0003-embedding-abi.md mirror ---
assert!(d.contains("Export parameters are written **bare**: a scalar type carries no `own`/`borrow` mode"),
"design/contracts/embedding-abi.md must mirror the canonical bare-scalar export-param rule");
"design/contracts/0003-embedding-abi.md must mirror the canonical bare-scalar export-param rule");
}
@@ -25,23 +25,23 @@ fn norm(s: &str) -> String {
#[test]
fn design_md_effect_prose_is_true() {
// Effect prose -> design/models/effects.md; the
// Effect prose -> design/models/0002-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"));
// `## What is not (yet) supported` -> design/contracts/0010-scope-boundaries.md.
let d = norm(&[read("design/models/0002-effects.md"),
read("design/contracts/0010-scope-boundaries.md")].join("\n"));
// fiction absent
assert!(!d.contains("row-polymorphic"),
"design/: the effect row is NOT row-polymorphic (no EffectRow / row var exists)");
assert!(!d.contains("`IO` and `Diverge` (for infinite"),
"design/: `Diverge` is not a wired-up MVP effect");
assert!(!d.contains("the built-in IO and Diverge ops"),
"design/contracts/scope-boundaries.md: there is no built-in Diverge op");
"design/contracts/0010-scope-boundaries.md: there is no built-in Diverge op");
// corrected anchors present
assert!(d.contains("flat, unordered, closed set of effect names"),
"design/models/effects.md Decision 3 must describe the real (flat set-equality) effect model");
"design/models/0002-effects.md Decision 3 must describe the real (flat set-equality) effect model");
assert!(d.contains("`Diverge` (for non-termination) is reserved"),
"design/models/effects.md Decision 3 must state Diverge is reserved/unimplemented");
"design/models/0002-effects.md Decision 3 must state Diverge is reserved/unimplemented");
}
#[test]
+1 -1
View File
@@ -15,7 +15,7 @@
//! effects, `clone`, `reuse-as`, doc strings, type annotations on
//! signatures and lambdas, the `tail` flag) stays visible.
//!
//! See `design/contracts/authoring-surface.md` for the prose-surface
//! See `design/contracts/0001-authoring-surface.md` for the prose-surface
//! invariants this crate must respect.
//!
//! # Public API
+1 -1
View File
@@ -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 `design/contracts/authoring-surface.md`).
//! production by LLMs (see `design/contracts/0001-authoring-surface.md`).
//!
//! ## Modules
//!
+1 -1
View File
@@ -76,7 +76,7 @@
//! ```
//!
//! Notes on the form (the binding constraints are in
//! design/contracts/authoring-surface.md):
//! design/contracts/0001-authoring-surface.md):
//!
//! - The `lam` form carries `param-types`, a `ret` type, and an
//! optional `effects` clause. The AST stores them per-lambda and
+1 -1
View File
@@ -125,7 +125,7 @@ fn write_type_def(out: &mut String, td: &TypeDef, level: usize) {
}
// `(drop-iterative)` annotation. Printed last (after
// every ctor) so the canonical form lands consistent with the
// design/contracts/data-model.md example. Omitted when `drop_iterative == false`.
// design/contracts/0002-data-model.md example. Omitted when `drop_iterative == false`.
if td.drop_iterative {
out.push('\n');
indent(out, level + 1);
@@ -23,7 +23,7 @@ fn prelude_ail_json_does_not_exist_on_disk() {
re-introduced this file, you must also: (a) remove this pin, \
(b) restore the §C4 (b) entry in carve_out_inventory.rs, \
(c) revert the §C4 (b) status marker in \
docs/specs/2026-05-13-form-a-default-authoring.md, and \
docs/specs/0025-form-a-default-authoring.md, and \
(d) record the rationale in the commit body."
);
}
+2 -2
View File
@@ -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/contracts/roundtrip-invariant.md). For every
//! Invariant, design/contracts/0009-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/contracts/roundtrip-invariant.md): for every
/// Roundtrip Invariant, design/contracts/0009-roundtrip-invariant.md): for every
/// well-formed `.ail` text `t`, the composition `parse → print → parse`
/// is idempotent on the AST.
///