From 66fc40de65c9f9434ec7f636660087b95a837ce0 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 21 Oct 2026 00:59:51 -0700 Subject: [PATCH] fix(kb-lint): a wikilink must not resolve into the quarantine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The resolver copied Obsidian's filename-anywhere semantics with a raw rglob, which walked .AGENTTRASH/ as well. [[log]] therefore stayed "valid" by landing on the quarantined core/log.md — a page that is out of circulation until a pruning session and, being gitignored, does not exist on any other machine. The gate reported a green bundle while pointing readers at a file git does not carry: exactly the #57 failure, hidden by the check that exists to catch it. Resolution now uses the same filtered set linting uses (pages()), so the two can never disagree about what exists. A companion test pins that filename-anywhere resolution for live pages still works, so the narrowing costs nothing; mutating the resolver back to rglob turns the new test red. Refs #60 --- scripts/kb-lint.sh | 11 +++++++++-- tests/kb-lint.bats | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/scripts/kb-lint.sh b/scripts/kb-lint.sh index ef25c22..4b8b933 100755 --- a/scripts/kb-lint.sh +++ b/scripts/kb-lint.sh @@ -20,6 +20,10 @@ def pages(): if ".AGENTTRASH" not in p.parts: yield p +# Names that a wikilink may point at: exactly the pages pages() reports. Deriving this +# once keeps linting and link resolution from disagreeing about what exists. +live = {p.stem for p in pages()} + for p in pages(): rel = p.relative_to(kb) text = p.read_text(errors="replace") @@ -70,9 +74,12 @@ for p in pages(): if not Path(target).is_file(): violations.append(f"{rel}: broken relative link -> {m.group(1)}") for m in re.finditer(r"\[\[([^\]|#]+)", text): - # wikilinks resolve by filename anywhere in the KB (Obsidian semantics) + # wikilinks resolve by filename anywhere in the KB (Obsidian semantics) — but only + # among pages that BELONG to the bundle. A raw rglob also walked .AGENTTRASH/, so a + # link to a quarantined page stayed "valid" here while pointing at nothing on every + # other machine (the trash is gitignored). Same exclusion as pages(), one source. name = m.group(1).strip().rsplit("/", 1)[-1] - if not list(kb.rglob(name + ".md")): + if name not in live: violations.append(f"{rel}: broken wikilink -> [[{m.group(1).strip()}]]") for v in violations: diff --git a/tests/kb-lint.bats b/tests/kb-lint.bats index e145b54..cc8dfc8 100755 --- a/tests/kb-lint.bats +++ b/tests/kb-lint.bats @@ -120,6 +120,28 @@ EOF [[ "$output" == *"0 violation(s)"* ]] } +@test "a wikilink must not resolve into the quarantine" { + # Obsidian resolves [[name]] by filename anywhere under the vault, and the resolver copied + # that with a raw rglob — so [[log]] stayed "valid" by landing on the copy waiting in + # .AGENTTRASH/. That is #57's failure wearing a green check: the page is out of circulation + # until a pruning session with Frank, and it is not even tracked, so on another machine the + # link points at nothing while the gate here says it is fine. + mkdir -p "$KB/.AGENTTRASH" + printf 'no frontmatter, quarantined\n' > "$KB/.AGENTTRASH/log.md" + printf -- '---\ntype: reference\n---\nSee [[log]] for the history.\n' > "$KB/good.md" + run bash "$LINT" "$KB" + [ "$status" -ne 0 ] + [[ "$output" == *"broken wikilink -> [[log]]"* ]] +} + +@test "a wikilink to a live page still resolves, in any directory" { + # The narrowing above must cost nothing: filename-anywhere semantics is the point. + printf -- '---\ntype: reference\n---\nSee [[good]].\n' > "$KB/sub/other.md" + run bash "$LINT" "$KB" + [ "$status" -eq 0 ] + [[ "$output" == *"0 violation(s)"* ]] +} + @test "the shipped knowledge base itself is conformant" { # The tests above exercise the linter against fixtures; nothing else in the # suite ever points it at the real bundle, so a page added without frontmatter