WORK-594
ID:WORK-594Status:done

The edge index, the git scan, and refrakt stale

SPEC-134 is precise and costs a stamp per invocation, so it only ever covers the references someone already suspected. This is the cheap, wide signal underneath it.

Documentation already declares edges into the repository. Indexing them costs nothing per reference and needs no new syntax, no marker, and no change to a single existing page.

Priority:highComplexity:complexMilestone:v0.37.0Source:SPEC-136
claude/v0-37-0-review-vqpl41 View source

Criteria completion

Criteria completion: 18 of 18 (100%) checked; history from Sep 22 to Sep 240%25%50%75%100%Sep 22Sep 24
Branches 4
History 6
  1. 7d1cb64
    Content editedby bjornolofandersson
  2. 97231be
    • ☑ A `stale` command is registered in `packages/cli/src/commands/` — no such command exists today
    • ☑ `refrakt stale` reports a ranked list of edges, ordered by commits to the target since the referrer last changed
    • ☑ The git scan is a single `git log --name-only` pass over the repository, not one `git log` invocation per edge
    • ☑ The scan runs with `--full-history` and no pathspec, so history simplification cannot drop commits from a file's history
    • ☑ A test pins a file whose simplified and full histories differ, asserting the scan reports the full one
    • ☑ Embedded-source edges are extracted from `snippet`, `file-ref` and `expand` `path=` attributes
    • ☑ An edge whose referrer is newer than every change to its target scores zero and is omitted
    • ☑ Output is bounded by `--top`, defaulting to 10, and each entry lists commit subjects for the target's changes
    • ☑ `--class`, `--min` and `--format json` behave as specified
    • ☑ `refrakt stale` exits zero whatever it finds, including when every edge in the corpus is stale
    • ☑ A refusal (shallow clone, non-git tree) exits non-zero, distinguishing "could not measure" from "measured, nothing wrong"
    • ☑ A shallow clone is detected and refused with a message naming `fetch-depth: 0`
    • ☑ A non-git working tree is refused with its own message, not treated as a clean corpus
    • ☑ The report footer states each class's base rate — how many edges of that class were non-zero out of how many scanned
    • ☑ The edge index is a shared module, with ranking as one reader of it and no query owning it
    • ☑ A test fixture reproduces the measured case: a page referencing a file that has since taken N commits ranks above one referencing an unchanged file
    • ☑ Docs state that a zero score is the absence of evidence of staleness, not evidence of freshness
    • ☑ `site/content/docs/cli/cli-overview.md` gains a row for `stale` — the page {% ref "WORK-595" /%} cites as its motivating instance of a stale command table
    by bjornolofandersson
  3. aebbae3
    Created (ready)by bjornolofandersson
  4. 81a138f
    Content editedby Claude
    docs(plan): renumber the masker item to WORK-597, resolving the collisio
  5. 14707a7
    Content editedby Claude
    docs(plan): add the described-link edge class (SPEC-136 D16) and fix the
  6. d40fe65
    Content editedby Claude
    docs(plan): break the documentation-drift specs into v0.37.0

This track is independent of SPEC-131

Worth stating because the milestone otherwise reads as one long chain. Phase 1 extracts edges from path= attributes, which exist today. symbol= → resolved line range is only needed for --precise, which is not in this milestone. So this item can start on day one, in parallel with WORK-597.

Build the index so the MCP tool does not have to rewrite it

The temptation is a function that walks content, scores as it goes, and prints. That shape cannot answer touching, because WORK-596's query runs in the opposite direction and without the git scan at all.

Extract edges into an addressable index first and let the ranking be one reader of it. The cost is an afternoon here and a rewrite avoided later.

Almost nothing new is being built

PieceState
Single-pass git log --format=%at --name-only scanpackages/content/src/timestamps.ts:55 — the exact scan shape
Shallow-clone detectiontimestamps.ts:37 — isShallowClone, already written
Git-root-relative path prefixingtimestamps.ts:123 — getGitRelativePrefix
Embedded-source edge extractionthe shared reader, read-file.ts
A ranked report with --format jsonplan status, the pattern to follow

The refusals are load-bearing from the first commit (D5)

A shallow clone reports an empty result that is indistinguishable from a clean corpus. That is the failure this command cannot afford, because its output is already "nothing is very wrong" most of the time.

This phase will find almost nothing here, and that is the right order

The spec is explicit: embedded-source edges are the class whose extraction is already trustworthy, so they are the right one to establish the index, the scan and the output shape against. WORK-595 is where the corpus is actually reached.

Acceptance Criteria

  • A stale command is registered in packages/cli/src/commands/ — no such command exists today
  • refrakt stale reports a ranked list of edges, ordered by commits to the target since the referrer last changed
  • The git scan is a single git log --name-only pass over the repository, not one git log invocation per edge
  • The scan runs with --full-history and no pathspec, so history simplification cannot drop commits from a file's history
  • A test pins a file whose simplified and full histories differ, asserting the scan reports the full one
  • Embedded-source edges are extracted from snippet, file-ref and expand path= attributes
  • An edge whose referrer is newer than every change to its target scores zero and is omitted
  • Output is bounded by --top, defaulting to 10, and each entry lists commit subjects for the target's changes
  • --class, --min and --format json behave as specified
  • refrakt stale exits zero whatever it finds, including when every edge in the corpus is stale
  • A refusal (shallow clone, non-git tree) exits non-zero, distinguishing "could not measure" from "measured, nothing wrong"
  • A shallow clone is detected and refused with a message naming fetch-depth: 0
  • A non-git working tree is refused with its own message, not treated as a clean corpus
  • The report footer states each class's base rate — how many edges of that class were non-zero out of how many scanned
  • The edge index is a shared module, with ranking as one reader of it and no query owning it
  • A test fixture reproduces the measured case: a page referencing a file that has since taken N commits ranks above one referencing an unchanged file
  • Docs state that a zero score is the absence of evidence of staleness, not evidence of freshness
  • site/content/docs/cli/cli-overview.md gains a row for stale — the page WORK-595 cites as its motivating instance of a stale command table

Approach

Adapt timestamps.ts's scan rather than rewriting it; the shallow-clone and git-root handling there are the parts most easily got wrong.

Pass --full-history, and do not take the existing call shape on trust. Found while measuring SPEC-136 D16's base rate, and it is the single easiest way to ship this item with quietly wrong numbers:

git log --name-only -- site/content/docs/cli/cli-overview.md
  → one commit, 2026-07-24

git log --full-history --name-only          (no pathspec)
  → five commits, most recent 2026-09-10

A pathspec makes git apply history simplification. Both outputs are correct git answering different questions, but this command does arithmetic on those timestamps — the referrer's last-changed time is one half of every score — so a simplified history inflates every edge out of that file, and deflates every edge into it.

The failure is invisible: the report still renders, still ranks, still looks plausible. It is the spec's own subject turned on the spec's own instrument, which is why it gets a pinned test rather than a comment.

The commit subjects are the report's whole value. "11 commits" is a number; "generate breadcrumb and timeline positions from a declared index" is a person recognising that the page they wrote does not mention declared indexes. Do not economise on them.

Notes

This ranks. It never fails (D1). No exit code on findings, no build failure, no required check, not even an opt-in flag to make it failing.

The failure mode of getting that wrong is specific and fatal: the cheapest way to turn any edge green is to edit the referring page, so a gate would train people to make trivial documentation edits to clear it — actively destroying the signal it measures. The measure only survives if nothing depends on it being zero.

Two edge classes are rejected, not deferred: plan source= edges (99% base rate) and derived rune pages (96%). Both are in the spec's table with their measurements; do not re-add them.

References

  • SPEC-136 — the measure, D1 (ranks, never fails), D5 (refusals), D9 (--top and commit subjects), D12 (the index as a shared module)
  • WORK-595 — the classes that reach the rest of the corpus
  • WORK-596 — the touching query this index must be able to answer
  • packages/content/src/timestamps.ts — the single-pass scan and isShallowClone, both adapted rather than rewritten
  • packages/runes/src/lib/read-file.ts — the shared reader carrying path=

Resolution

Completed: 2026-09-24

Branch: claude/v0-37-0-review-vqpl41 PR: refrakt-md/refrakt#648 (batched with WORK-590)

What was done

  • packages/content/src/edges/ (new) — extract.ts, git-scan.ts and index.ts. The index is addressable in both directions; ranking is one reader of it.
  • packages/cli/src/commands/stale.ts (new) — refrakt stale, registered in bin.ts with --top, --class, --min, --format.
  • packages/content/test/edges.test.ts — 21 tests.
  • site/content/docs/cli/cli-overview.md gains the row.

Notes

  • Built for touching even though WORK-596 ships it. A walk-score-print function cannot answer a query that runs target → referrer and without git. edgesTouching already exists and is tested, including for a path with no commit history at all.
  • The --full-history test needed a real divergence. My first fixture reported 2 commits both ways — it would have passed while proving nothing. A merge using -s ours (discarding the side's version) is the construction that diverges: simplified 2, scan 3. Worth knowing if this fixture is ever edited.
  • The shallow-clone refusal was exercised for real. This container is a shallow clone, so the first live run refused with fetch-depth: 0 before the test existed. git fetch --unshallow was needed to see any ranking at all.
  • Fenced invocations must be excluded. Counting {% snippet %} inside a ```markdoc fence inflated this repo's index from 6 real edges to 10. Reuses WORK-588's Markdown masker rather than re-deriving fence tracking.
  • Test fixtures need a fake clock. Commits created in the same wall-clock second make the measure — which counts commits strictly after the referrer's change — see nothing. Four tests failed on this before the clock was added.
  • This phase finds almost nothing, as predicted. One edge, base rate 1/6. Embedded source reaches a fraction of the corpus; WORK-595 is where it is actually reached.

Verification

4858 tests pass. Live run against this repository produces one genuine finding.