iter design-ledger-formal-links.1 (DONE 5/5): clause-5 hard gate + 7 prose-ref conversions + 2 disposition-(b) homeless removals + honesty-rule positive-half (whole milestone in one iter)
Positive-half completion of the DESIGN.md -> design/ split: design/
body cross-references are now formal, file-relative Markdown links
into the durable tier (design/ or source), and a new in-tree hard
gate (design_index_pin.rs clause-5,
design_body_links_are_durable_and_resolve) walks every
design/contracts/*.md + design/models/*.md, strips fenced code
(strip_fences toggles on ```/~~~ lines so a ]( inside a fence is not
treated as a link), extracts every ](path), and asserts the target
resolves file-relative to a real file under design/-or-crates/-or-
runtime/; never docs/, never an in-file #anchor.
RED-first via identity-stubbed strip_fences (four embedded synthetic
vectors -- first one FAILS); replacing the stub with the real
toggle-on-fence impl turns the test GREEN. clause-5 composes with
clause-3 into the complete invariant the milestone establishes:
every contract cross-reference is EITHER a resolving durable
file-link OR clause-3-forbidden decision-record prose.
Conversions (recon-and-corpus-verified closed set):
Task 2 (7 prose refs, 8 link tokens):
float-semantics.md:69 Prelude classes -> [..](typeclasses.md)
float-semantics.md:100 bare-path -> [Str ABI](str-abi.md)
embedding-abi.md:45 "Frozen value layout" -> [..](frozen-value-layout.md) (drop stale "below")
memory-model.md:44 Data model -> [..](data-model.md)
memory-model.md:105-106 Method dispatch -> [..](typeclasses.md) (drop stale "below"; the target heading lives in typeclasses.md:227, not in this file)
scope-boundaries.md:48 Str ABI -> [..](str-abi.md)
scope-boundaries.md:88 mixed split: ailang-core::desugar -> source link + Pipeline -> ../models/pipeline.md (drop stale "above")
Task 3 (2 disposition-(b) homeless removals):
pipeline.md:60-61 (see docs/PROSE_ROUNDTRIP.md) pointer removed, CLI prose preserved
authoring-surface.md:178-181 cross-tier pointer clause removed, ail merge-prose sentence preserved
Task 4: honesty-rule.md positive-half paragraph inserted between L14 and the existing L15-blank-L16; both docs_honesty_pin.rs-pinned phrases byte-identical at L14/L19 (now shifted to L19 -> L25 by the +6 lines).
Out of scope, preserved (asserted independently): every intra-file
"above/below"; embedding-abi.md:51 "frozen value layout below
specifies" (no quoted title, no (see) form); data-model.md
38/66/79/206/226 (in-fence ```jsonc schema annotations -- the inline
analog of the nominal-mention carve-out). INDEX.md and the
decision-records journal byte-unchanged; clauses 1-4 of
design_index_pin.rs source byte-unchanged (the only `-` lines in
the diff are the two-line //! header rewrite Task 1 Step 5 itself
delivers).
Boss-verified independently (not on agent report alone):
cargo test --workspace 647 passed / 0 failed
(+1 vs pre-milestone 646:
the new clause-5)
cargo test --test design_index_pin 5 / 5 passed
cargo test --test docs_honesty_pin 5 / 5 passed (additive
paragraph is pin-safe)
grep ](.../docs/.../) under design/ zero
grep ](#) under design/ zero
](-link count under design/ 8 (closed convert-set)
git diff --quiet design/INDEX.md ok
git diff --quiet decision-records ok
embedding-abi.md:48 pinned phrase byte-identical
One Concerns item: Task-5 Step-7's plan-predicted "`-` line count = 1"
was actually 2 because Task 1 Step 5 rewrote the //! header 5 -> 8
lines (removing the original L4 + L5, not just L5). Planner self-
review-item-8 miss on my part -- a verification-arithmetic error in
the plan, NOT an implementation defect. The substantive assertion
(clauses 1-4 source byte-unchanged) is fully satisfied; the
implementer correctly flagged it and proceeded. The plan stands as
written; the assertion's `1` should have been `2`. Lesson noted for
future header-rewrite tasks.
Spec: docs/specs/2026-05-19-design-ledger-formal-links.md
(grounding-check PASS x3 across two corpus-grounded amendments --
clause-6 + cross-ref definition; clause-5 fence-skip + closed
convert-set enumeration).
Next: mandatory milestone-close audit (no fieldtest -- zero
authoring-surface change, reasoned exclusion).
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
//! 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.
|
||||
//! 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.
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
@@ -250,3 +253,118 @@ fn contracts_carry_no_decision_record_prose() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[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 {
|
||||
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
|
||||
}
|
||||
|
||||
// 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user