spec: design-ledger-formal-links — formal cross-links in the design/ ledger

Positive completion of the DESIGN.md -> design/ split: informal prose
cross-references become formal file-relative Markdown links, gated by a
new RED-first design_index_pin.rs clause-5 (every design/ body link
resolves into the durable tier or fails the build). INDEX spine stays
repo-root-relative (registry tier, clause-1 unchanged). No authoring
surface. Eight converged commitments + the user-decided INDEX sub-fork;
grounding-check PASS 7/7. roadmap [~] in-flight.
This commit is contained in:
2026-05-19 22:51:21 +02:00
parent 2ee408770e
commit 63b669fa8f
2 changed files with 390 additions and 0 deletions
+28
View File
@@ -506,6 +506,34 @@ work progresses.
convergence). CLOSED — audited + drift-resolved; hard gate
enforces the honesty spirit.
- [~] **\[milestone\]** Formal cross-links in the `design/` ledger
(browsable wiki) — IN FLIGHT 2026-05-19. The positive completion
of DESIGN.md → `design/`: informal prose cross-references between
contracts/models (`(see "Str ABI" below)`, bare-path-in-prose,
`§"Section"`) become **formal file-relative Markdown links**
`[label](./sibling.md)` / `[label](../../crates/.../x.rs)`
resolved relative to the containing file, plus a new RED-first
`design_index_pin.rs` **clause-5** that fails the build on any
body link that does not resolve into the durable tier (`design/`
+ `crates/**` + `runtime/**` only — never `docs/`, never a
`#fragment`). clause-5 ∘ clause-3 = complete invariant: every
contract cross-reference is a resolving durable file-link or
clause-3-forbidden history prose. The INDEX **spine stays
repo-root-relative** (registry tier, clause-1 byte-unchanged) —
the move-fragility rationale that drives body prose to
file-relative is absent for the file that never moves.
No authoring surface (no `.ail`; precedent: the split spec,
audited clean). Out of scope (recon-forced): intra-file
"above/below" within one document (no file-link form; the
file-split is a separate concern).
- context: spec `docs/specs/2026-05-19-design-ledger-formal-links.md`
(grounding-check PASS, 7/7 ratified); origin this session's
six-turn design conversation following the split close;
precedent slice already shipped in the split's tidy
(`float-semantics.md` `(see "Str ABI" below)`
`(see design/contracts/str-abi.md)`, now to be brought to the
final `[Str ABI](str-abi.md)` form). Awaiting plan.
- [ ] **\[milestone\]** Flat array/slice primitive — performance
follow-up to the Embedding ABI arc, *not* a capability gap.
AILang's only sequence is the recursive `List` ADT (one RC cell
@@ -0,0 +1,362 @@
# Formal Cross-Links in the design/ Ledger — Design Spec
**Date:** 2026-05-19
**Status:** Draft — awaiting user spec review
**Authors:** Brummel (orchestrator) + Claude
## Goal
Make the `design/` ledger a browsable wiki: replace informal prose
cross-references between contracts/models (`(see "Str ABI" below)`,
`(see design/contracts/str-abi.md …)`, `see §"Data model"`) with
**formal, machine-checkable, file-level Markdown links**, and add an
in-tree hard gate that turns a broken cross-link into a same-commit
test failure instead of silent rot.
This is the positive completion of the DESIGN.md → design/ split.
That milestone established the negative half — *history prose must
not live in a contract* (`contracts_carry_no_decision_record_prose`,
clause-3). This milestone establishes the positive half — *what
cross-reference prose remains must resolve to a durable file*.
Together they form a complete invariant: every cross-reference in a
contract is **either** a resolving file-relative link into
`design/`-or-source, **or** decision-record prose that clause-3
already forbids.
The milestone has **no authoring surface**: it changes no `.ail`
program, no language construct, no compiler/checker/codegen path. It
is a documentation-form + Rust-test-infrastructure milestone, the
same category as the immediately-preceding DESIGN.md → design/ split
(`docs/specs/2026-05-19-design-md-rolesplit.md`), whose concrete code
was likewise the structural Rust pin plus the ledger shape, audited
clean. The no-`.ail` disposition is therefore precedent-governed and
reasoned, not an omission (see Concrete code shapes §"No authoring
surface").
## Architecture
Four locked design commitments (converged interactively this
session; the INDEX sub-choice was resolved by user MC during
brainstorm Step 2). They are the spec's input contract, not open
forks:
1. **File-level granularity, no in-file anchors.** A link addresses a
whole file. Finer reference granularity is achieved by *splitting
a file*, never by a `#fragment`. A link whose target is a pure
`#anchor`, or carries a `#fragment` suffix, is a contract
violation the hard gate rejects.
2. **Durable-tier targets only.** A formal link may target a file
under `design/` (contracts + models) **or** a source file
(`crates/**`, `runtime/**`). It may **never** target `docs/`
(specs, plans, journals, or standing docs like
`docs/PROSE_ROUNDTRIP.md`) — that is the *potentially ephemeral
tracking* tier; linking the durable ledger into it re-couples
exactly what the split decoupled.
3. **Standard relative Markdown link syntax** `[label](path)`. Not
Obsidian `[[wikilinks]]`: wikilink name-fuzzy resolution is the
prose-reference problem in another costume (it contradicts
content-addressing), is not native on Gitea (the platform we
actually use, self-hosted) nor in GitHub repo view, and does not
uniformly address source files.
4. **File-relative path form for body prose; the INDEX spine stays
repo-root-relative.** Body cross-links are
`[label](./sibling.md)` / `[label](../models/x.md)` /
`[label](../../crates/ailang-core/src/desugar.rs)`, resolved
relative to the *containing file*. This is the universal standard
(resolves in Gitea blob, GitHub, VS Code preview, Obsidian,
static generators), serving the named primary goal (browsability)
on every platform; its move-fragility is real but mechanically
bounded by the resolve-check. The earlier "root-relative = the
content-addressing thesis" argument was retracted as
principle-overreach (the thesis kills name-fuzzy wikilinks;
between two deterministic path forms it is an engineering trade).
The **INDEX registry table** (`design/INDEX.md`) is a
*deliberately-distinct structured tier*: it is machine-parsed as
a table, and it is the one `design/` file whose path is
structurally invariant (it is THE spine — it does not move). The
move-fragility rationale that drove body prose to file-relative is
*absent* for the spine, so the INDEX keeps its repo-root-relative
cells and `design_index_pin.rs` clause-1 is **unchanged**.
Forcing the body rule onto the spine would itself be the
principle-overreach pattern this session already retracted once.
Derived scope boundaries (forced by the commitments + Step-1 recon,
not forks):
- **Intra-file directional prose is out of scope.** "the constraints
below", "Direction 1 above", "see §X below" *within the same
document* is not a cross-*file* reference; it has no file-link
form (same file) and anchors are forbidden by commitment 1.
These stay as prose. The remedy for "this file is too big for
'above' to be clear" is commitment 1's file-split — a separate
future concern, explicitly NOT this milestone.
- **Homeless references resolve or die.** A prose reference whose
referent has no durable home (e.g.
`design/models/pipeline.md``(see docs/PROSE_ROUNDTRIP.md)`)
is retargeted to the durable file that carries the content today
(here: `../contracts/roundtrip-invariant.md`) if one exists, else
deleted as decision-record-class prose (clause-3 already forbids
the relitigation form). Enumerating them is a recon/plan-time
scan, not a spec byte-decision.
- **A cross-*file* `§"Section"` reference becomes a file-link.**
`(see §"Data model" …)` pointing at content in *another* file
becomes `[Data model](data-model.md)` — the section title may
survive only as the human link *label*, never as a `#fragment`
(commitment 1). The reader lands on the file and finds the section
by reading; that is file-level granularity working as designed,
not a loss. (A `§"Section"` that names a section of the *same*
file is intra-file directional prose — the bullet above — and is
untouched.)
## Concrete code shapes
### No authoring surface — the delivered concrete code
There is no `.ail` because the milestone touches nothing in the
language: no construct, no type rule, no codegen, no fixture. Per
the brainstorm infra clause, the rationalisation to refuse is "no
surface so no code to show"; the honest response is to show the
concrete code the milestone *does* deliver. For a docs-form +
test-infra milestone that is (a) the new Rust hard-gate clause,
(b) the real prose-reference before→after, (c) the explicit
no-change statement for clause-1. All three are shown below. The
governing precedent is the immediately-preceding same-category
milestone (DESIGN.md → design/ split), whose spec led its concrete
code with the Rust pin and the ledger shape and was audited clean —
this milestone follows that precedent exactly.
### (a) The new hard gate — `design_index_pin.rs` clause-5
A new test in the existing `crates/ailang-core/tests/design_index_pin.rs`
(sibling of clauses 14; reuses the file's `root()` helper and the
clause-3 `fs::read_dir` walk idiom verbatim). It is RED-first via an
embedded synthetic-vector self-check before it asserts the live tree
(a tree-scan test cannot be RED-first by tree state alone — the tree
is GREEN-vacuous until the first ref is converted — so the RED proof
is an in-test malformed-link vector, the honest mechanism for
"scan-the-tree" gates).
```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. Composes with clause-3:
// a surviving cross-reference is a resolving durable file-link
// or it is clause-3-forbidden decision-record prose.
// link target := first capture of \]\(([^)]+)\)
// (matches [x](y) and the ](y) of image ![x](y) alike — every
// parenthesised target must resolve; conservative on purpose)
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
}
// classify a repo-relative resolved path
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
}
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(&raw) {
if tgt.starts_with("http://")
|| tgt.starts_with("https://")
|| tgt.starts_with("mailto:")
{
continue; // external — out of the ledger's scope
}
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"
);
// resolve RELATIVE TO THE CONTAINING 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"
);
}
}
}
}
```
`#[test]` count in the file goes 4 → 5; clauses 14 are byte-
unchanged. The exact byte form of the extractor/regex is the
planner's to finalise against this shape; the *contract* is the
three predicates (resolves-from-containing-file; durable-tier;
no-fragment) and the RED-first synthetic vector.
### (b) Real prose-reference before → after
Three real, in-tree references (recon-confirmed line numbers; exact
bytes are the planner's):
```
design/contracts/float-semantics.md:100 (the tidy precedent, still
half-formal: bare root path in prose, not a Markdown link)
- before: … a fresh heap-Str slab at the call site (see
design/contracts/str-abi.md for the dual realisation).
- after: … a fresh heap-Str slab at the call site (see
[Str ABI](str-abi.md) for the dual realisation).
# float-semantics.md and str-abi.md are siblings in
# design/contracts/ ⇒ file-relative target is `str-abi.md`
design/contracts/embedding-abi.md:45 (informal AND stale-direction
— the exact wart class motivating the milestone: "below" but the
target is a different file)
- before: … layout is **frozen as of M3** (see "Frozen value
layout" below); …
- after: … layout is **frozen as of M3** (see
[Frozen value layout](frozen-value-layout.md)); …
design/models/pipeline.md:61 (the canonical HOMELESS reference —
points into docs/, the non-durable tier)
- before: … (see docs/PROSE_ROUNDTRIP.md)
- after: … (see [round-trip invariant](../contracts/roundtrip-invariant.md))
# retargeted to the durable home that carries the
# content; if recon finds no durable home, the clause-6
# rule deletes it as decision-record-class prose instead
```
### (c) clause-1 is unchanged (commitment 4, INDEX = spine)
```
crates/ailang-core/tests/design_index_pin.rs
fn link_target_exists(cell) { … root().join(path).exists() } // UNCHANGED
fn every_index_link_resolves() { … } // UNCHANGED
```
The INDEX table cells stay repo-root-relative
(`design/contracts/str-abi.md`); clause-1 keeps resolving them via
`root().join`. No INDEX content edit, no clause-1 resolver edit.
One resolution rule per *tier* (registry-spine: root-relative;
movable body prose: file-relative), each locally justified — not
one rule everywhere, by deliberate design.
## Components
| Component | Disposition |
|---|---|
| `crates/ailang-core/tests/design_index_pin.rs` | **Modify** — add clause-5 (`design_body_links_are_durable_and_resolve`); clauses 14 byte-unchanged; module `//!` extended one sentence to name clause-5. |
| `design/contracts/*.md`, `design/models/*.md` | **Modify** — every genuine cross-*file* prose reference rewritten to a file-relative Markdown link; homeless refs retargeted-or-deleted; intra-file "above/below" untouched (out of scope). |
| `design/INDEX.md` | **Untouched** — spine tier, commitment 4. |
| `docs/journals/2026-05-19-design-decision-records.md` | **Untouched** — monolithic append-only relitigation archive; never a link target (commitment 2/7). |
| `design/contracts/honesty-rule.md` | **Modify (prose only)** — one sentence stating cross-references are formal file-relative links into the durable tier (the positive-half companion to the clause-3 sentence it already carries). Pin-safe: the two `docs_honesty_pin.rs` phrases stay verbatim+contiguous. |
| `bench/architect_sweeps.sh` | **Untouched** — clause-5 ("does every link resolve into the durable tier?") and Sweep-1 ("does any line carry a history anchor: `Iter <n>`, a date, `**Status:`?") are orthogonal predicates over the same files. A Markdown link `[label](file.md)` contains no Sweep-1 anchor token, so converting prose to links cannot trip Sweep-1; clause-5 shares no regex or scope with it. No sweep change. |
## Data flow
`cargo test` → clause-5 reads each `design/contracts/*.md` and
`design/models/*.md` → extracts every `](…)` target → for each:
external? skip. `#`-fragment or empty file-part? fail (commitment
1). Else resolve against the *containing file's directory*,
canonicalize, strip the repo root → repo-relative path → assert
under `design/`|`crates/`|`runtime/` (commitment 2). The synthetic
vector runs first and unconditionally (RED-first proof). clause-1
independently resolves INDEX cells root-relative (unchanged).
## Error handling
Every failure is a `panic!`/`assert!` naming the file, the offending
target, and which commitment it violates ("non-durable tier",
"in-file #anchor — split the file", "does not resolve"). No silent
skips except the three external schemes. A homeless reference that
recon cannot map to a durable home is not worked around in the test
— it is deleted from the prose as clause-3-class content (the test
then passes because the reference is gone, which is the correct
outcome, not a dodge).
## Testing strategy
- **RED-first:** clause-5's embedded synthetic-vector block asserts
the extractor + durable classifier reject a `docs/specs/…` target
and recognise a `#anchor` *before* the tree walk. Planner scripts
the first task as: add clause-5 (RED on the synthetic vector if
the classifier is stubbed wrong) → make the vector GREEN → then
convert prose refs until the tree walk is GREEN.
- **Composition with clause-3:** a per-milestone assertion (journal,
not code) that on the post-milestone tree, for every
`design/contracts/*.md`, clause-3 GREEN ∧ clause-5 GREEN — i.e.
every cross-reference is a resolving durable link or it is gone.
- **Whole-suite gate:** `cargo test --workspace` GREEN, count
unchanged except +1 test. `design_index_pin` 5/5.
- **No bench surface:** zero codegen/runtime change ⇒ the regression
trio is expected pristine; any firing is the tracked-P2 noise,
causally exonerable as in the M2/M3/M5 and the just-closed split.
## Acceptance criteria
1. `crates/ailang-core/tests/design_index_pin.rs` has a 5th test
`design_body_links_are_durable_and_resolve`; clauses 14 are
byte-identical to HEAD `2ee4087`.
2. clause-5 is demonstrably RED-able (the synthetic vector fails if
the durable classifier or the anchor check is wrong) and GREEN on
the final tree.
3. Every genuine cross-*file* prose reference in `design/contracts/`
and `design/models/` is a file-relative `[label](path)` Markdown
link resolving (from its containing file) to an existing
`design/`-or-source file. The float-semantics, embedding-abi, and
pipeline references above are converted exactly as shown (modulo
recon's homeless-home confirmation).
4. No reference in `design/` targets `docs/` (any subtree) or
carries a `#fragment`.
5. `design/INDEX.md` and clause-1 are byte-unchanged; the
decision-records journal is byte-unchanged and unlinked.
6. Intra-file "above/below" directional prose is untouched
(recon-enumerated as out of scope, not missed).
7. `cargo test --workspace` GREEN; bench trio exit 0 or
causally-exonerated per the standing P2 disposition.
8. `honesty-rule.md` carries the positive-half sentence; its two
`docs_honesty_pin.rs` pinned phrases remain verbatim+contiguous.