fix(kb-lint): a markdown link may not resolve into the quarantine

The link-integrity pass ended in `is_file()`, and `.AGENTTRASH/` is a real directory on
this machine: a page pointing at `[x](.AGENTTRASH/gone.md)` passed the gate here and was a
dead link everywhere else, because the quarantine is gitignored. This is incident #57 — the
wikilink resolver resolving `[[log]]` onto a quarantined copy — reproduced in markdown
clothing, which is to say: fixing one resolver while leaving the identical assumption in the
other one is not a fix.

A link that lands inside the bundle must land on a page that belongs to the bundle. A link
that leaves the bundle is still honoured (`tools/live-sync.md` -> `../../AGENTS.md` points at
the operating contract, which is not bundle content and is real on disk). Red test added
first, both link spellings covered; nothing previously green changed.

Refs #64
This commit is contained in:
Frank
2026-10-21 02:10:37 -07:00
parent 22d2f3bcd8
commit 4735702965
2 changed files with 26 additions and 2 deletions
+11 -2
View File
@@ -23,6 +23,7 @@ def pages():
# 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()}
live_paths = {p.resolve() for p in pages()}
for p in pages():
rel = p.relative_to(kb)
@@ -70,8 +71,16 @@ for p in pages():
text = re.sub(r"^```[^\n]*$^```[^\n]*$", "", p.read_text(errors="replace"), flags=re.M)
rel = p.relative_to(kb)
for m in re.finditer(r"\]\((?!https?:|#)([^)#\s]+\.md)", text):
target = (p.parent / m.group(1)).resolve() if not m.group(1).startswith("/") else (kb / m.group(1).lstrip("/"))
if not Path(target).is_file():
target = (p.parent / m.group(1)).resolve() if not m.group(1).startswith("/") else (kb / m.group(1).lstrip("/")).resolve()
# A link INSIDE the bundle must land on a page that belongs to the bundle: `is_file()`
# alone also accepted a target parked in .AGENTTRASH/, which is gitignored — green here,
# a dead link on every machine that pulls (#57 again, in markdown clothing). A link that
# leaves the bundle is allowed: ../../AGENTS.md is the operating contract, not bundle
# content, and it is real on disk.
if kb in target.parents:
if target not in live_paths:
violations.append(f"{rel}: broken relative link -> {m.group(1)}")
elif not 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) — but only
+15
View File
@@ -134,6 +134,21 @@ EOF
[[ "$output" == *"broken wikilink -> [[log]]"* ]]
}
@test "a markdown link into the quarantine is broken" {
# #57's failure mode wearing markdown instead of brackets: the relative-link check was a
# bare is_file(), so a link that lands on a page waiting in .AGENTTRASH/ passes this gate
# and points at nothing on every machine that pulls — the quarantine is gitignored. Both
# spellings count: relative, and bundle-absolute with a leading slash.
mkdir -p "$KB/.AGENTTRASH"
printf 'no frontmatter, quarantined\n' > "$KB/.AGENTTRASH/gone.md"
printf -- '---\ntype: reference\n---\nRelative: [gone](.AGENTTRASH/gone.md)\n' > "$KB/rel.md"
printf -- '---\ntype: reference\n---\nAbsolute: [gone](/.AGENTTRASH/gone.md)\n' > "$KB/abs.md"
run bash "$LINT" "$KB"
[ "$status" -ne 0 ]
[[ "$output" == *"rel.md: broken relative link -> .AGENTTRASH/gone.md"* ]]
[[ "$output" == *"abs.md: broken relative link -> /.AGENTTRASH/gone.md"* ]]
}
@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"