# 2026-06-23 — soul-doctor dangling-reference check Drafted a new **dangling & unlinked references** deep-check into `plugins/claude-code/commands/soul-doctor.md`. ## Why (the gotcha that motivated it) Tracing how an agent reaches a referenced doc, hit a real failure: ADR 0006 in the `demarkus-library` project soul says "supersedes ADR 0005", but **ADR 0005 isn't in the store** — `adr/` holds only 0006. Neither retrieval path reaches it: - **Graph traversal can't** — the "supersedes ADR 0005" reference is *prose*, not a `[text](url)` link. `links.Extract` (`client/links/links.go`) only walks `*ast.Link` nodes, so the ADR crawled with `LinkCount: 0`. Latent relationships written in prose / inline code are invisible to `mark_graph` / `mark_backlinks`. - **Catalog lookup proves absence** — `mark_lookup query="*"` scoped to the project returns the complete subtree census (6 docs, only ADR 0006). The catalog is **authoritative for absence**: `*` is a full census, so "no match" means "not here," not "didn't crawl it." That certainty is the catalog's real value, distinct from finding things. The existing soul-doctor **broken-links** check missed it because a broken link requires a link; this reference was never an edge. ## The check (design decisions) - **Deep tier** (per-doc fetch) — it's a body-text scan; core checks stay fetch-free. Inherits the cap/disclose discipline. - **Zero extra calls** — resolves each prose mention against the inventory (`mark_list`, existence) + edges (`mark_graph`, already-linked?) already gathered. - **Two verdicts, different fixes**: *unlinked* (target exists, no edge → convert prose to a link) vs *dangling* (resolves to nothing → restore or remove; confirm via `mark_lookup` since catalog is authoritative for absence). - **Tight pattern set** to bound false positives: `ADR[ -]?#?\d{3,4}` + bare/inline `mark://` URLs not inside a link. Exclude self-references and code-fence mentions (same fence-skip `firstH1`/the catalog use). Left out `*.go` mentions (point at repo, not soul) and title fuzzy-matching (too noisy). - Distinct from broken-links: that follows an edge to a missing target; this finds references that were never edges. ## Broader frame (not yet acted on) Two improvement axes identified. (1) prose-reference gap → this doctor check + optional publish-time link suggest-mode. (2) semantic-recall gap (catalog is tags/title only) → a **client/broker-side** embedding index, *not* a `SEARCH` verb — ADR 0006 D7 closed full-text search by design ("never will be"). Respecting that, semantic recall belongs in the gateway where `mark_index` already reads bodies, mirroring the ephemeral/bucket-store pattern of the graph store. Not yet done: dry-run the new check against the `demarkus-library` project to confirm it flags the 0005 dangling ref + 0006 unlinked refs. ## Update — dry-ran the check against demarkus-library Ran the new deep-check over all 6 docs. **Confirmed** the predicted findings: ADR 0005 dangling (cited from 5/6 docs), ADR 0003 dangling (from the ADR itself); `architecture.md` → ADR 0006 **unlinked** (cited ~4× in prose, no in-body link). index.md self-annotates 0005's absence ("no md file exists") → report as *known*, not actionable. The dry-run exposed **two bugs in the v1 draft** (now fixed in `soul-doctor.md`): 1. **already-linked? must parse the citing doc's OWN markdown links, not the crawl edge store.** The crawl seeds from the hub, so orphan docs are never visited and their outbound links never enter the edge store. `journal/2026-06-19.md` *does* link `[ADR 0006](…)` but is an orphan → the edge-store rule would falsely flag it unlinked. Fix: the deep tier already holds every body; resolve from parsed body links. 2. **bare/inline `mark://` pattern too noisy** → deferred to v2. False-positives on cross-world refs (can't existence-check in a single-world audit) and inline-code placeholders (`` `mark://host/...` ``). v1 is ADR-pattern only. Lesson: a graph-derived "is it linked?" signal silently degrades for orphans (unreachable from the crawl seed) — same failure shape as the world-map orphan bug in this project's own 2026-06-20 journal ("not proven orphan ≠ linked"). Prefer the in-hand body over a derived graph signal when you already paid for the fetch.