plan: design-ledger-formal-links.1 — clause-5 + 7 conversions + 2 disposition-(b) + honesty-rule positive-half

Single iteration covering the whole milestone (same shape as
rolesplit.1; spec is small, no build-atomicity issue — clause-5 is
isolated additive code, prose edits don't affect compilation).
Five tasks, each at task-level review granularity:

  Task 1: clause-5 added to design_index_pin.rs RED-first via
          identity-stubbed strip_fences (synthetic vector FAILS) →
          correct toggle-on-fence impl (GREEN); module //!-header
          extended to name clause-5. Test count 4 → 5.
  Task 2: 7 prose-ref conversions to file-relative Markdown links
          (float-semantics:69/100, embedding-abi:45, memory-model:44/
          105, scope-boundaries:48/88 with the source+Pipeline split).
          Exact verbatim before→after for every line. clause-5 stays
          GREEN by construction (every introduced link resolves
          durable).
  Task 3: 2 disposition-(b) homeless-ref removals (pipeline:60-61
          in-fence CLI block + authoring-surface:178-181 prose).
          Pointer dropped, behavioural prose preserved.
  Task 4: honesty-rule.md pin-safe positive-half paragraph between
          L14 and the existing L15-blank (the two docs_honesty_pin
          phrases at L14/L19 each stay verbatim on their physical
          line; the additive insertion splits neither). The new
          paragraph names design_index_pin.rs clause-5 by name
          (nominal mention, not a link — consistent with the
          existing 'Ratified by:' footer style and with §Scope).
  Task 5: Whole-suite gate (646 → 647) + 5 grep+diff verifications
          (no docs/ link, no #fragment, link count = 8, in-fence
          schema-annotations intact, INDEX.md + decision-records
          journal + clauses 1-4 byte-unchanged).

Self-review 8/8: spec coverage complete; zero placeholders; names
consistent; bite-sized; no commit steps; pin contiguity verified
(L14 + L19 stay verbatim on their physical lines, replacement body
shows L14 contiguous); no signature change ⇒ no compile-gate
ordering issue; every cargo-test filter substring is a real test
name + unfiltered runs have explicit count assertions.
This commit is contained in:
2026-05-19 23:21:29 +02:00
parent 42ff44adf6
commit 36599bedd9
+518
View File
@@ -0,0 +1,518 @@
# design-ledger-formal-links.1 — Implementation Plan
> **Parent spec:** `docs/specs/2026-05-19-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/float-semantics.md:69` — convert §"Prelude (built-in) classes" → `[Prelude (built-in) classes](typeclasses.md)`.
- Modify: `design/contracts/float-semantics.md:100` — convert bare-path `design/contracts/str-abi.md``[Str ABI](str-abi.md)`.
- Modify: `design/contracts/embedding-abi.md:45` — convert "Frozen value layout" below → `[Frozen value layout](frozen-value-layout.md)`; drop stale "below".
- Modify: `design/contracts/memory-model.md:44` — convert §"Data model" → `[Data model](data-model.md)`.
- Modify: `design/contracts/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/scope-boundaries.md:48-49` — convert "Str ABI" → `[Str ABI](str-abi.md)`.
- Modify: `design/contracts/scope-boundaries.md:88` — convert mixed-referent: `ailang-core::desugar` → source link, "Pipeline" → `../models/pipeline.md`; drop stale "above".
- Modify: `design/models/pipeline.md:60-61` — disposition-(b): drop the `(see docs/PROSE_ROUNDTRIP.md)` continuation line; preserve CLI prose.
- Modify: `design/models/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/honesty-rule.md:14` — append pin-safe positive-half paragraph after line 14, before blank line 15.
- Untouched (assert): `design/INDEX.md`; clauses 14 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<String> {
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 15:
```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/2026-05-19-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/2026-05-19-design-ledger-formal-links.md). Spec for
//! the split: docs/specs/2026-05-19-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/float-semantics.md:69, 100`
- Modify: `design/contracts/embedding-abi.md:45`
- Modify: `design/contracts/memory-model.md:44, 105-106`
- Modify: `design/contracts/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/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/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 4652 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 105106:
```
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/pipeline.md).
```
(`scope-boundaries.md` is in `design/contracts/`; `../../` reaches the repo root; `../models/pipeline.md` reaches `design/models/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/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/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/authoring-surface.md:178-181`.
- [ ] **Step 1: `pipeline.md:60-61` — drop the `(see docs/PROSE_ROUNDTRIP.md)` continuation line.**
Replace verbatim lines 6061:
```
— 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 178181:
```
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/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 <n>` / `Family <n>` / `pre-<n>` / `**Status:` / `21.g` / `<n> 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/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 14 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/2026-05-19-design-md-rolesplit.md.` being replaced by the extended four-line `//!` block). Any value > 1 means content was removed from clauses 14 — STOP and inspect.