Skip to content

feat(wiki): kb.wiki_lint - orphan/dead-link/stale/uncited page checks - #729

Open
joaovictor91123 wants to merge 3 commits into
vouchdev:testfrom
joaovictor91123:feat/wiki-lint
Open

feat(wiki): kb.wiki_lint - orphan/dead-link/stale/uncited page checks#729
joaovictor91123 wants to merge 3 commits into
vouchdev:testfrom
joaovictor91123:feat/wiki-lint

Conversation

@joaovictor91123

Copy link
Copy Markdown
Contributor

What changed

New kb.wiki_lint method — a page-level sibling to kb.lint's claim-health
sweep, registered on all four surfaces (MCP kb_wiki_lint, JSONL
kb.wiki_lint, capabilities.py, CLI vouch wiki-lint). Four checks over
the live, already-approved page set:

  • orphan_page — zero inbound [[wikilinks]] (via wiki_render.backlinks).
  • dead_wikilink — a [[link]] that doesn't resolve against the live,
    non-archived page set (via wiki_render.resolve_link; deduplicated per
    page+target so a repeated dead link isn't reported twice).
  • stale_page — not updated in --stale-days (default 180, mirroring
    vouch lint --stale-days).
  • uncited_section — citation coverage below --min-coverage (default
    0.5, reusing compile.py's own _citation_coverage sentence walk and
    MIN_CITATION_COVERAGE threshold).

Why

ROADMAP.md's 1.4 milestone: "wiki lint: orphan pages, stale pages, dead
links, uncited sections." None of the four existed. Wikilink resolution and
citation coverage were previously checked exactly once — at compile.py's
draft-approval gate, before a page is ever proposed — and never again once
it's live. A link whose target is later archived, a hand-edited page whose
coverage erodes after approval, or a page nothing else links to all
surfaced nowhere.

New module rather than folding into health.lint(): lint()'s docstring
scopes it explicitly to claim/proposal health, and its own
stale_after_days already means something different (claim confirmation
vs. page edit recency) — a shared flag with two meanings under one method
would be confusing. kb.lint / kb.doctor already establish the pattern
of separate entry points for separate sweep scopes.

Closes #726

What might break

Nothing for users with an existing .vouch/ directory — purely additive:
a new method, a new module, no object model or on-disk shape change. Fully
read-only, like wiki_render's existing index/MOC/backlinks — never
proposes, writes, or mutates, so it doesn't touch proposals.py,
lifecycle.py, storage.py, or the review gate. None of the four findings
are error severity (none represent a data-integrity violation the way
health.lint's broken_citation/dangling_relation do), so ok stays
true even with findings — consistent with health.lint's own framing of
"user-actionable, not necessarily broken."

Archived pages are excluded entirely from what gets checked, and treated
as unresolvable link targets — this matches render-wiki's existing (#695)
exclusion policy exactly (a link to an archived page is exactly as dead as
a link to nothing), so it's not a new precedent.

VEP

Not applicable — no object model, on-disk layout, bundle format, or
audit-log shape change. A new read-only aggregation method, same shape as
kb.explain_ranking / kb.effectiveness / themes.detect_themes.

Tests

  • Local make check-equivalent: ruff clean (src + tests); mypy
    clean on wiki_lint.py; tests/test_capabilities.py (four-surface
    parity), tests/test_health.py, tests/test_wiki_render.py,
    tests/test_wiki_lint.py, tests/test_cli.py all pass
  • New behaviour has a dedicated test file —
    tests/test_wiki_lint.py (14 cases: each check's positive/negative
    case, archived-page exclusion from both the checked set and as a
    link target, dedup of repeated dead links, ok semantics, JSONL
    envelope success and param handling)
  • CHANGELOG.md updated under ## [Unreleased]

health.lint() is thorough for claim/proposal health (broken citations,
stale claims, dangling refs) but pages have no sibling sweep - a
[[wikilink]] whose target is later archived, a page nothing links to,
a page untouched in a year, or a hand-edited page whose citation
coverage eroded after approval all surface nowhere. wikilink
validation and citation coverage were previously checked exactly once,
at compile.py's draft-approval gate, never again after a page is live.

add a new wiki_lint module, registered the same way kb.lint is on all
four surfaces (mcp kb_wiki_lint, jsonl kb.wiki_lint, capabilities.py,
cli vouch wiki-lint), reusing wiki_render's link index/backlinks and
compile's citation-coverage sentence walk rather than reimplementing
either:

- orphan_page: zero inbound backlinks
- dead_wikilink: a [[link]] that doesn't resolve against the live,
  non-archived page set (deduplicated per page+target)
- stale_page: not updated in --stale-days (default 180)
- uncited_section: citation coverage below --min-coverage (default
  0.5, same threshold compile.py enforces on drafts)

archived pages are excluded entirely from what's checked, and treated
as unresolvable link targets - matching render-wiki's own vouchdev#695
exclusion policy (a link to an archived page is exactly as dead as a
link to nothing).

purely read-only, like wiki_render's existing derived views - never
proposes, writes, or mutates, so it doesn't touch proposals.py,
lifecycle.py, or the review gate. none of the four checks are
data-integrity errors the way health.lint's broken_citation is, so
`ok` stays true even with findings.

Closes vouchdev#726
@github-actions github-actions Bot added docs documentation, specs, examples, and repo guidance cli command line interface mcp mcp, jsonl, and http surfaces tests tests and fixtures size: M 200-499 changed non-doc lines labels Jul 31, 2026
test_hot_memory_universal_coverage asserts every kb.* method is either
in HOT_MEMORY_COVERED or explicitly excluded in HOT_MEMORY_EXCLUDED -
a new method with neither fails the suite. kb.wiki_lint landed with
neither, breaking CI. excluded with the same reason kb.lint/kb.doctor
already carry: diagnostic findings, no claim payload to decorate.
@github-actions github-actions Bot added the retrieval context, search, synthesis, and evaluation label Jul 31, 2026
…path

diff-coverage flagged three gaps: server.py's kb_wiki_lint body (only
exercised indirectly via jsonl, never called directly), cli.py's
wiki-lint command (had zero test coverage at all), and wiki_lint.py's
naive-datetime normalization branch in the stale_page check (every
existing test used tz-aware datetime.now(UTC) timestamps).

add a direct server.kb_wiki_lint() test diffed against a direct
wiki_lint() call, CliRunner tests for both a findings case and a
clean-kb case, and a stale_page case with a timezone-naive updated_at
(hand-written yaml or an older on-disk page predating a tz-aware write
path both produce this shape).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli command line interface docs documentation, specs, examples, and repo guidance mcp mcp, jsonl, and http surfaces retrieval context, search, synthesis, and evaluation size: M 200-499 changed non-doc lines tests tests and fixtures

Projects

None yet

Development

Successfully merging this pull request may close these issues.

wiki lint: orphan pages, stale pages, dead links, uncited sections (roadmap 1.4)

1 participant