`.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.
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 fileSKILL.md— a skillagents/*.md— a subagentcommands/*.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.