# design-ledger-formal-links.1 — Implementation Plan > **Parent spec:** `docs/specs/0046-design-ledger-formal-links.md` (HEAD `42ff44a`; grounding-check PASS ×3 across two corpus-grounded amendments — clause-6 + cross-ref definition, then clause-5 fence-skip + closed convert-set enumeration) > > **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement` to run this plan. Steps use `- [ ]` checkboxes for tracking. **Goal:** Ship the formal-cross-links milestone in one iteration: add `design_index_pin.rs` clause-5 (with `strip_fences` + RED-first synthetic vectors), convert the seven Acceptance-3 prose refs to file-relative Markdown links, apply disposition-(b) to the two PROSE_ROUNDTRIP homeless pointers, append the pin-safe positive-half sentence to `honesty-rule.md`, and ratify the clause-3 ∘ clause-5 composition invariant. **Architecture:** Iteration `.1` is the whole milestone (the spec is small; same single-iter shape as the immediately-preceding rolesplit.1). Build-atomicity is not a concern — clause-5 is a new isolated test, prose edits do not affect compilation. The ordering concern is RED-first (Task 1's synthetic vector must demonstrably FAIL on an identity-stub of `strip_fences` before the implementer fixes it). **Tech Stack:** `crates/ailang-core/tests/design_index_pin.rs` (new clause-5 + `//!`-header extension); seven `design/contracts/*.md` + two `design/models/*.md` prose edits. --- **Files this plan creates or modifies:** - Modify: `crates/ailang-core/tests/design_index_pin.rs:1-5` — extend `//!` header to name clause-5. - Modify: `crates/ailang-core/tests/design_index_pin.rs:253-` — append clause-5 `design_body_links_are_durable_and_resolve` (with `strip_fences`, `targets`, `is_durable`, RED-first synthetic vectors, tree walk). - Modify: `design/contracts/0005-float-semantics.md:69` — convert §"Prelude (built-in) classes" → `[Prelude (built-in) classes](typeclasses.md)`. - Modify: `design/contracts/0005-float-semantics.md:100` — convert bare-path `design/contracts/0011-str-abi.md` → `[Str ABI](str-abi.md)`. - Modify: `design/contracts/0003-embedding-abi.md:45` — convert "Frozen value layout" below → `[Frozen value layout](frozen-value-layout.md)`; drop stale "below". - Modify: `design/contracts/0008-memory-model.md:44` — convert §"Data model" → `[Data model](data-model.md)`. - Modify: `design/contracts/0008-memory-model.md:105-106` — convert §"Method dispatch" below → `[Method dispatch](typeclasses.md)`; drop stale "below" (target is in typeclasses.md, not in this file). - Modify: `design/contracts/0010-scope-boundaries.md:48-49` — convert "Str ABI" → `[Str ABI](str-abi.md)`. - Modify: `design/contracts/0010-scope-boundaries.md:88` — convert mixed-referent: `ailang-core::desugar` → source link, "Pipeline" → `../models/0003-pipeline.md`; drop stale "above". - Modify: `design/models/0003-pipeline.md:60-61` — disposition-(b): drop the `(see docs/PROSE_ROUNDTRIP.md)` continuation line; preserve CLI prose. - Modify: `design/models/0001-authoring-surface.md:180-181` — disposition-(b): drop the `see docs/PROSE_ROUNDTRIP.md for the six-step cycle and` clause; preserve the surrounding `ail merge-prose` sentence. - Modify: `design/contracts/0007-honesty-rule.md:14` — append pin-safe positive-half paragraph after line 14, before blank line 15. - Untouched (assert): `design/INDEX.md`; clauses 1–4 of `design_index_pin.rs`; `docs/journals/2026-05-19-design-decision-records.md`; `data-model.md` lines 38/66/79/206/226 (in-fence schema annotations — out of scope); `embedding-abi.md:48` (`docs_honesty_pin.rs:172`-pinned phrase — sits between the :45 conversion and the :51 stay-prose, must be byte-identical); `embedding-abi.md:51` (no-title-no-(see)-form — out of scope); every intra-file "above/below". --- ### Task 1: clause-5 hard gate (RED-first via synthetic vectors) **Files:** - Modify: `crates/ailang-core/tests/design_index_pin.rs:1-5` — extend `//!` header. - Modify: `crates/ailang-core/tests/design_index_pin.rs:253-` — append clause-5. - [ ] **Step 1: Append clause-5 with `strip_fences` STUBBED as identity (RED setup).** Append to the END of the file (after the existing line 252 `}`): ```rust #[test] fn design_body_links_are_durable_and_resolve() { // clause 5 — every inline Markdown link in design/ body prose // (contracts/ + models/, NOT INDEX.md — the spine is the // structured registry tier resolved by clause-1, commitment 4) // resolves, relative to its CONTAINING file, to an existing // file under design/ or source (crates/** | runtime/**); never // under docs/; never an in-file #anchor. Fenced code blocks are // not scanned (a `](` inside ``` is literal text, not a link). // Composes with clause-3: a surviving cross-reference is a // resolving durable file-link or it is clause-3-forbidden // decision-record prose. // Fenced code (``` … ``` / ~~~ … ~~~) is literal text, not // Markdown — a `](` inside a fence is NOT a navigable link on // any renderer. Strip fenced regions before scanning (gate // correctness: prevents false extraction of code-example byte // sequences and of non-rendering in-fence links). fn strip_fences(md: &str) -> String { // INTENTIONALLY-STUBBED IDENTITY for RED-first (Step 2); // Step 3 replaces this body with the real toggle-on-fence impl. md.to_string() } // link target := first capture of \]\(([^)]+)\) fn targets(md: &str) -> Vec { let mut out = Vec::new(); let b = md.as_bytes(); let mut i = 0; while i + 1 < b.len() { if b[i] == b']' && b[i + 1] == b'(' { if let Some(end) = md[i + 2..].find(')') { out.push(md[i + 2..i + 2 + end].trim().to_string()); } } i += 1; } out } fn is_durable(repo_rel: &str) -> bool { repo_rel.starts_with("design/") || repo_rel.starts_with("crates/") || repo_rel.starts_with("runtime/") } // RED-first synthetic vectors (proves the gate bites before it // is pointed at the live tree). { let t = targets("see [x](../docs/specs/foo.md) and [y](#sec) and [z](./gone.md)"); assert_eq!(t, vec!["../docs/specs/foo.md", "#sec", "./gone.md"]); assert!(!is_durable("docs/specs/foo.md")); // durable-tier reject assert!("#sec".starts_with('#')); // in-file anchor reject // fenced code is not a link surface (gate correctness) assert!(targets(&strip_fences("```\nsee [E](e.md)\n```\n")).is_empty()); assert_eq!(targets(&strip_fences("[k](k.md)\n```\n[n](n.md)\n```")), vec!["k.md"]); } let bases = ["design/contracts", "design/models"]; for base in bases { let dir = root().join(base); for entry in fs::read_dir(&dir).expect("design/ subdir exists") { let p = entry.unwrap().path(); if p.extension().and_then(|e| e.to_str()) != Some("md") { continue; } let raw = fs::read_to_string(&p).unwrap(); let fname = format!("{base}/{}", p.file_name().unwrap().to_string_lossy()); for tgt in targets(&strip_fences(&raw)) { if tgt.starts_with("http://") || tgt.starts_with("https://") || tgt.starts_with("mailto:") { continue; } let file_part = tgt.split('#').next().unwrap_or(&tgt); assert!( !file_part.is_empty(), "clause-5: in-file #anchor link {tgt:?} in {fname:?} — \ commitment 1 forbids fragments; split the file" ); let resolved = p.parent().unwrap().join(file_part); let canon = resolved .canonicalize() .unwrap_or_else(|e| panic!( "clause-5: link {tgt:?} in {fname:?} does not resolve: {e}" )); let repo_rel = canon .strip_prefix(root().canonicalize().unwrap()) .unwrap_or(&canon) .to_string_lossy() .replace('\\', "/"); assert!( is_durable(&repo_rel), "clause-5: link {tgt:?} in {fname:?} targets the \ non-durable tier ({repo_rel:?}); commitment 2 \ permits design/ + crates/ + runtime/ only" ); } } } } ``` - [ ] **Step 2: Run the test to verify it FAILS (RED) on the identity-stubbed `strip_fences`.** Run: `cargo test -p ailang-core --test design_index_pin design_body_links_are_durable_and_resolve` Expected: **FAIL** at the first fence assertion: `assert!(targets(&strip_fences("```\nsee [E](e.md)\n```\n")).is_empty())` — because identity-`strip_fences` returns the input unchanged, `targets` extracts `"e.md"`, the vec is non-empty, the assert panics. Output contains `assertion failed: targets(&strip_fences(...)).is_empty()`. - [ ] **Step 3: Implement `strip_fences` correctly (toggle on fence lines) — GREEN.** Replace ONLY the `strip_fences` function body (the `INTENTIONALLY-STUBBED IDENTITY` block) with: ```rust fn strip_fences(md: &str) -> String { let mut out = String::new(); let mut in_fence = false; for line in md.lines() { let t = line.trim_start(); if t.starts_with("```") || t.starts_with("~~~") { in_fence = !in_fence; continue; // drop the fence marker line itself } if !in_fence { out.push_str(line); out.push('\n'); } } out } ``` (Drop the comment about INTENTIONALLY-STUBBED.) - [ ] **Step 4: Run the test to verify it PASSES (GREEN).** Run: `cargo test -p ailang-core --test design_index_pin design_body_links_are_durable_and_resolve` Expected: **PASS**. All four synthetic assertions hold; the tree walk runs over `design/contracts/*.md` + `design/models/*.md`, the current corpus has **zero** `](` markdown links in design/ body (bash-verified pre-plan), so `targets(&strip_fences(&raw))` returns empty for every file ⇒ no resolve/durable assertions trigger ⇒ vacuously GREEN. - [ ] **Step 5: Extend the `//!` module header to name clause-5.** Replace lines 1–5: ```rust //! 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/0045-design-md-rolesplit.md. ``` with: ```rust //! 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, docs/DESIGN.md is //! resurrected, or a design/ body cross-link fails to resolve into //! the durable tier (clause-5; spec //! docs/specs/0046-design-ledger-formal-links.md). Spec for //! the split: docs/specs/0045-design-md-rolesplit.md. ``` - [ ] **Step 6: Run the whole suite to confirm nothing else regressed.** Run: `cargo test -p ailang-core --test design_index_pin` Expected: **5 passed; 0 failed; 0 ignored**. The four pre-existing clauses (`design_md_is_gone`, `every_index_link_resolves`, `every_contract_names_a_resolvable_ratifying_test`, `contracts_carry_no_decision_record_prose`) plus the new `design_body_links_are_durable_and_resolve`. End count: **5 tests, 5 passed**. --- ### Task 2: Convert the seven Acceptance-3 prose refs to file-relative Markdown links **Files:** - Modify: `design/contracts/0005-float-semantics.md:69, 100` - Modify: `design/contracts/0003-embedding-abi.md:45` - Modify: `design/contracts/0008-memory-model.md:44, 105-106` - Modify: `design/contracts/0010-scope-boundaries.md:48, 88` Verbatim before→after for each. Iter-provenance suffixes do not appear on any of these seven (they were all on the in-fence data-model refs, which are out of scope). Each edit is a single-line or two-adjacent-line text replacement; no surrounding line is perturbed. - [ ] **Step 1: `float-semantics.md:69` — §"Prelude (built-in) classes" → `[Prelude (built-in) classes](typeclasses.md)`.** Replace verbatim: ``` internally — see §"Prelude (built-in) classes" for the Show ship). ``` with: ``` internally — see [Prelude (built-in) classes](typeclasses.md) for the Show ship). ``` (`float-semantics.md` and `typeclasses.md` are siblings in `design/contracts/` ⇒ file-relative target is `typeclasses.md`. The label retains the original section title verbatim; Markdown handles balanced parens in labels.) - [ ] **Step 2: `float-semantics.md:100` — bare path `design/contracts/0011-str-abi.md` → `[Str ABI](str-abi.md)`.** Replace verbatim (line 100; line 101 unchanged): ``` a fresh heap-Str slab at the call site (see design/contracts/0011-str-abi.md ``` with: ``` a fresh heap-Str slab at the call site (see [Str ABI](str-abi.md) ``` - [ ] **Step 3: `embedding-abi.md:45` — "Frozen value layout" below → `[Frozen value layout](frozen-value-layout.md)` (drop stale "below").** Replace verbatim line 45: ``` layout is **frozen as of M3** (see "Frozen value layout" below); the ``` with: ``` layout is **frozen as of M3** (see [Frozen value layout](frozen-value-layout.md)); the ``` (Lines 44 and 46–52 untouched. **Critical:** line 48 carries the `docs_honesty_pin.rs:172`-pinned phrase `Export parameters are written **bare**: a scalar type carries no` — must remain byte-identical. Line 51 `the frozen value layout below specifies` is also untouched: §Scope out-of-scope rule — no quoted title, no `(see …)` form.) - [ ] **Step 4: `memory-model.md:44` — §"Data model" → `[Data model](data-model.md)`.** Replace verbatim line 44: ``` parallel to `params` and `ret` (see §"Data model" for the JSON ``` with: ``` parallel to `params` and `ret` (see [Data model](data-model.md) for the JSON ``` - [ ] **Step 5: `memory-model.md:105-106` — §"Method dispatch" below → `[Method dispatch](typeclasses.md)` (drop stale "below"; target heading lives in typeclasses.md:227, not in memory-model.md — the genuine cross-file stale-direction wart).** Replace verbatim lines 105–106: ``` Method dispatch is type-driven post-mq.3 (see §"Method dispatch" below): synth resolves a `Term::Var { name: "show" }` by consulting ``` with: ``` Method dispatch is type-driven post-mq.3 (see [Method dispatch](typeclasses.md)): synth resolves a `Term::Var { name: "show" }` by consulting ``` (Line 107 onward unchanged. The pre-existing `post-mq.3` iter-token is *not* on the converted ref itself and is *not* a clause-3 trip under the current implementation — pre-existing, not introduced by this milestone, left as-is.) - [ ] **Step 6: `scope-boundaries.md:48-49` — "Str ABI" → `[Str ABI](str-abi.md)`.** Replace verbatim line 48 (line 49 unchanged): ``` call time and return it with `ret_mode: Own`; see "Str ABI" for ``` with: ``` call time and return it with `ret_mode: Own`; see [Str ABI](str-abi.md) for ``` - [ ] **Step 7: `scope-boundaries.md:88` — mixed-referent split (source link + Pipeline cross-file link, drop stale "above").** Replace verbatim line 88: ``` typecheck/codegen — see `ailang-core::desugar` and Pipeline above. ``` with: ``` typecheck/codegen — see [desugar](../../crates/ailang-core/src/desugar.rs) and [Pipeline](../models/0003-pipeline.md). ``` (`scope-boundaries.md` is in `design/contracts/`; `../../` reaches the repo root; `../models/0003-pipeline.md` reaches `design/models/0003-pipeline.md`. Both targets verified existing pre-plan. "above" dropped: stale-direction (Pipeline lives in a different file, not above in this one).) - [ ] **Step 8: Run clause-5 to confirm all seven new links resolve into the durable tier — still GREEN.** Run: `cargo test -p ailang-core --test design_index_pin design_body_links_are_durable_and_resolve` Expected: **PASS**. Each of the seven introduced `](path)` resolves from its containing file's directory to a real `design/`-or-source file (`typeclasses.md`, `str-abi.md`, `frozen-value-layout.md`, `data-model.md`, `typeclasses.md`, `str-abi.md`, `../../crates/ailang-core/src/desugar.rs` + `../models/0003-pipeline.md`). All targets are in the durable tier (`design/` or `crates/`). No `#fragment`. No in-fence link. --- ### Task 3: Disposition-(b) on the two PROSE_ROUNDTRIP homeless refs **Files:** - Modify: `design/models/0003-pipeline.md:60-61` (in-fence CLI block; pointer removed regardless — the in-fence carve-out preserves *schema documentation* annotations, not cross-tier pointers). - Modify: `design/models/0001-authoring-surface.md:178-181`. - [ ] **Step 1: `pipeline.md:60-61` — drop the `(see docs/PROSE_ROUNDTRIP.md)` continuation line.** Replace verbatim lines 60–61: ``` — compose the LLM-mediator prompt for the prose round-trip (see docs/PROSE_ROUNDTRIP.md) ``` with: ``` — compose the LLM-mediator prompt for the prose round-trip ``` (One line removed. The CLI command row's behavioural prose — "compose the LLM-mediator prompt for the prose round-trip" — is preserved verbatim; the ledger now describes the behaviour without pointing into the non-durable tier.) - [ ] **Step 2: `authoring-surface.md:178-181` — drop the cross-tier pointer clause, preserve the surrounding `ail merge-prose` sentence.** Replace verbatim lines 178–181: ``` by construction (Decision 6 constraint 2); form (B) deliberately is not. Re-integrating prose edits requires an external LLM mediator, not a compiler pass — see `docs/PROSE_ROUNDTRIP.md` for the six-step cycle and the prompt template `ail merge-prose` composes. ``` with: ``` by construction (Decision 6 constraint 2); form (B) deliberately is not. Re-integrating prose edits requires an external LLM mediator, not a compiler pass — the prompt template `ail merge-prose` composes the six-step cycle. ``` (The cross-tier pointer `see \`docs/PROSE_ROUNDTRIP.md\` for the` is removed; the substantive present-tense content — that `ail merge-prose` composes the six-step cycle — is preserved and slightly rearranged into one sentence on the ledger side, no pointer needed.) - [ ] **Step 3: Run clause-5 again — confirm no new failures (Task 3 introduces no `](` links; clause-5 stays GREEN).** Run: `cargo test -p ailang-core --test design_index_pin design_body_links_are_durable_and_resolve` Expected: **PASS** (Task 3 only *removes* prose; introduces no link). --- ### Task 4: honesty-rule.md positive-half sentence (pin-safe) **Files:** - Modify: `design/contracts/0007-honesty-rule.md:14` — append a new paragraph after line 14, before blank line 15. - [ ] **Step 1: Insert the positive-half paragraph between line 14 and line 15.** The two `docs_honesty_pin.rs`-pinned phrases sit at line 14 (`the honesty rule it holds itself to`) and line 19 (`whether the document asserts something exists, works, or changed that does not`). The pin uses whitespace-normalised `.contains()` over the whole file — any insertion that does not *split* either phrase is safe. Inserting a new paragraph after L14 (before the existing blank L15) leaves both phrases byte-identical on their physical lines. Replace verbatim: ``` the honesty rule it holds itself to. The single legitimate exception is a present-tense reserved or ``` with: ``` the honesty rule it holds itself to. A cross-reference that does belong stays: it is a formal, file-relative Markdown link into the durable tier (`design/` or source), enforced by `design_index_pin.rs` clause-5. A reference that cannot be expressed as such a link is, by that fact, the history-or-rationale prose the rule above removes. The single legitimate exception is a present-tense reserved or ``` (A new paragraph block of 5 wrapped lines is inserted between L14's end and the existing L16 `The single legitimate exception …`. The original blank L15 is preserved between L14 and the new paragraph; a second blank is added between the new paragraph and the previous L16. Net: file grows by ~6 lines; both pinned phrases remain on their own physical lines verbatim.) The new paragraph contains no clause-3-PHRASES tripwire (verified against the PHRASES list in `design_index_pin.rs:173-179`: none of `we rejected` / `an earlier draft` / `was retired` / etc. appears) and no Sweep-1 anchor (no `Iter ` / `Family ` / `pre-` / `**Status:` / `21.g` / ` sketch` / date in line-prefix-before-`/`). The phrase `the rule above` is intra-file directional prose, explicitly allowed by the milestone's §Scope out-of-scope rule (intra-file directional ≠ cross-reference). The naming of `design_index_pin.rs` is a nominal "ratified-by"-style mention, NOT a navigational pointer (per §Scope) — stays as backtick code, not a link, consistent with line 21's pre-existing `Ratified by: \`crates/ailang-core/tests/docs_honesty_pin.rs\`.`. - [ ] **Step 2: Run the docs-honesty pin to confirm both pinned phrases still match (whitespace-normalised).** Run: `cargo test -p ailang-core --test docs_honesty_pin design_md_present_tense_anchors_present` Expected: **PASS** (both `.contains()` assertions hold; the additive paragraph splits neither phrase). - [ ] **Step 3: Run clause-3 to confirm no decision-record-prose tripwire was introduced.** Run: `cargo test -p ailang-core --test design_index_pin contracts_carry_no_decision_record_prose` Expected: **PASS** (the new paragraph contains no clause-3 PHRASE substring and no Sweep-1 anchor on any physical line). - [ ] **Step 4: Run clause-5 — confirm the new paragraph introduces no `](` link (it doesn't — the only path mention is `design_index_pin.rs` inside backticks, no markdown link).** Run: `cargo test -p ailang-core --test design_index_pin design_body_links_are_durable_and_resolve` Expected: **PASS**. --- ### Task 5: Whole-suite gate + composition-invariant assertion **Files:** - None modified in this task — final verification only. The composition-invariant note is recorded in the per-iter journal (orchestrator's Boss-commit step), not as a code change. - [ ] **Step 1: Run the whole workspace test suite.** Run: `cargo test --workspace` Expected: **all GREEN**. Pre-milestone baseline (HEAD `42ff44a`) is 646 passed / 0 failed; post-milestone count is **647 passed** (the +1 from clause-5). Zero failures, zero ignored beyond the standing baseline. (If the count is anything other than 647, investigate immediately — a regression masked as a filter mismatch would be the hazard.) - [ ] **Step 2: Independently grep design/contracts + design/models for any surviving `docs/` link target (Acceptance 4 spot-check beyond clause-5).** Run: ``` grep -rnE '\]\(([^)]*docs/[^)]*)\)' design/contracts design/models ``` Expected: **zero matches** (no `](…docs/…)` link in design/ body — Acceptance 4). - [ ] **Step 3: Independently grep design/contracts + design/models for any `#fragment` link.** Run: ``` grep -rnE '\]\(#' design/contracts design/models ``` Expected: **zero matches** (no in-file anchor link — commitment 1, Acceptance 4). - [ ] **Step 4: Count `](` links introduced in design/ body (sanity — should match the convert-set: 7 from Task 2 + 1 from Task 2 Step 7's source-link sub-target = 8 navigational links).** Run: ``` grep -rhoE '\]\([^)]+\)' design/contracts design/models | grep -vE '\]\((https?://|mailto:)' | wc -l ``` Expected: **8** (the 7 Task-2 conversions including the source-link, plus the Pipeline link — i.e. seven prose refs, but scope-boundaries:88 carries TWO links in one conversion, so total `](` token count = 7 refs + 1 second link = 8). No fewer (would mean a conversion was missed); no more (would mean an unintended link was introduced). - [ ] **Step 5: Verify the in-fence data-model refs are still informal (out-of-scope assertion).** Run: ``` grep -nE 'see §"Class names"|see §"Type::Con name scoping"|see §"Embedding ABI"' design/contracts/0002-data-model.md ``` Expected: **5 lines** (the lines 38, 66, 79, 206, 226) — confirming the in-fence schema annotations were untouched (out of scope per §Scope; clause-5 skips fences regardless). - [ ] **Step 6: Confirm INDEX.md and the decision-records journal are byte-unchanged vs HEAD (commitment 4 / Acceptance 5).** Run: ``` git diff --quiet HEAD -- design/INDEX.md && echo INDEX.md unchanged git diff --quiet HEAD -- docs/journals/2026-05-19-design-decision-records.md && echo journal unchanged ``` Expected: both `unchanged` lines printed (each `git diff --quiet` exits 0 ⇒ the `&& echo` fires). - [ ] **Step 7: Confirm clauses 1–4 of `design_index_pin.rs` are byte-unchanged (only the `//!` header and the appended clause-5 changed).** Run: ``` git diff HEAD -- crates/ailang-core/tests/design_index_pin.rs | grep -cE '^-[^-]' ``` Expected: **`1`** — exactly one removed line (the original `//! is resurrected. Spec: docs/specs/0045-design-md-rolesplit.md.` being replaced by the extended four-line `//!` block). Any value > 1 means content was removed from clauses 1–4 — STOP and inspect.