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