Files
zernie__vigiles/docs/rules/orphan-docs.md
zernie d6b1ad06aa fix!: exclude reaches every pass, and the guard stops missing re-spelled commands (#194)
`.vigilesrc.json#exclude` was honoured by 3 of 11 discovery walks, and the walks
that did honour it disagreed on the dialect: glob's string `ignore` treated bare
`bench` and `bench/` as matching nothing, while the minimatch helper accepted
them. One `ExcludeSet` is now parsed where `loadConfig()` runs and taken as a
REQUIRED parameter by every in-scope walk in cli.ts — the shape that let
`findSpecs` go a year without it was optional-with-default, which compiles
silently. Closes #192.

The disaster battery now generates its own inputs: `experimental_alternateSpellings`
(renamed from `equivalentDisasters`) re-spells hook test cases so the shell
executes them identically — nine families, 7 seeds to 136 spellings. Five of
those came from a normalizer fix: `g\it push --force` runs git but was not
matched by `runs("git push")`.

That fix then had to be scoped, because CI caught the regression it caused:
escapes are resolved for the HEAD and FLAG-shaped operands (what the command IS)
and never for a path operand (where it WRITES) — the same bytes are a resolved
escape to `sh` and a real Windows UNC path on Windows, and fail-closed keeps the
path. Both directions pinned by mutation.

Also: `spec-refs` and `duplicate-rules` had a meta and a doc but no row in the
canonical rules matrix, and nothing compared either against it. A test now
asserts every rule has a matrix ROW (matched on the table row, not a prose
mention).

BREAKING CHANGE: `findSpecs` changed signature; `orphans` gained a `repoExclude`
option; `equivalentDisasters` was renamed to `experimental_alternateSpellings`
and now takes a required argument. Any repo with an `exclude` key sees it applied
to every pass, so an excluded spec is no longer compiled and an excluded
harness/eval script is no longer discovered.
2026-09-03 15:56:41 +05:00

4.3 KiB

orphan-docs

Flag a markdown doc that no other markdown file references — a doc that quietly rots in a cross-linked corpus because nothing routes a reader (human or agent) to it. The inverse of stale-reference detection: stale-ref catches a spec pointing at a missing file; orphan-docs catches an existing file that no .md points at. Implemented in src/core/orphans.ts, surfaced by vigiles lint.

Opt-in — and why

This rule is off unless you opt in by declaring an orphans block in .vigilesrc.json. That's deliberate. "Unreferenced" only means "rot" for a hand-cross-linked corpus — docs that link each other with markdown links. It does not mean rot for a nav-managed doc site (Docusaurus, MkDocs, VitePress), where the page graph lives in sidebars.js / mkdocs.yml / config, not in inline links. Pointed at a doc site, the rule flags nearly every page — a sweep across popular OSS repos found ~100% false positives on doc sites.

So declaring orphans.include is your assertion that those dirs are a cross-linked corpus. If they are, the rule is useful; if they're a generated site, don't opt in (or narrow the scope).

It stays a warning, never a hard error, even when opted in: a backtick prose mention counts as a reference and a page reached only from code does not, so the signal is a proxy, not a decidable fact.

Configuration

{
  "orphans": {
    "include": ["docs/**/*.md", "research/**/*.md"],
    "exclude": ["docs/CHANGELOG.md"]
  }
}
Key Default Meaning
(block) absent → off The block's presence opts the repo in — no block, no scan.
include ["docs/**/*.md"] Dirs to hold to the rule. docs/ is the convention; add others (e.g. a research/ notes tree) explicitly. [] = opted in, scans nothing.
exclude [] Globs to drop from the candidate set. The repo-wide top-level exclude is the floor under it: an excluded corpus is neither a candidate nor a referencer that keeps a doc alive.

docs/ is the near-universal convention, so it's the default when you opt in without naming dirs. A vigiles-specific dir like research/ is not in the default — declare it. This is how "provide dirs" and "default dir" reconcile: the block's presence is the switch, include is the optional override.

What it checks

A doc under include counts as referenced when some other .md in the repo links to it ([text](path.md)) or names it in a backtick span (`docs/foo.md`). A self-reference doesn't count. Anything else is an orphan.

What it never flags — harness-loaded instruction files

Files the harness loads directly are load-bearing by their name/location, not because another .md links to them, so they're categorically not docs and are never reported as orphans — even if you broaden include to the whole repo:

  • CLAUDE.md / AGENTS.md — the instruction file
  • SKILL.md — a skill
  • agents/*.md — a subagent
  • commands/*.md — a slash command

(They're still scanned as referencers, so a real doc that only your CLAUDE.md links to is still credited — the exemption only removes them from the orphan candidate set.)

Opt out a single doc

For an intentionally-unreferenced doc (a top-level index, a changelog) that isn't rot, add the inline marker:

<!-- vigiles-disable orphan-docs -->

…or exclude it via orphans.exclude (or narrow orphans.include).

Why

A hand-maintained docs/ tree accumulates files nothing links to; they go stale because no reader is routed to them. On a corpus that really is cross-linked, flagging orphans keeps the doc set honest — every doc is reachable from the README / a spec / another doc, or explicitly marked standalone.