Releases: Lockyc/docgraph
Release list
v3.2.5
Patch release: the three hook messages are reworded. No check changes what it detects, blocks or exits with.
Changed
doc-driftasked you to "reconcile" a stale doc. It now asks for the smallest edit that makes the doc true again — swap the value, rename the symbol, or delete the sentence that now describes nothing — and says that what changed and why belongs in the commit message, not the doc.covers-driftended with "Editing the doc silences it", which reads as an instruction. It now says a still-accurate doc wants no edit, that an edit made only to quiet the advisory is doc bloat, and that a falsified doc is corrected or cut, nothing more.footgun-driftnow names the commonest false footgun: a bug you just fixed. Its story belongs in the commit message; the doc keeps at most the surviving rule.
Agents answer a nag that sounds like "touch the doc" by appending a paragraph about the change they just made, so a consuming repo's docs grew on every push. The README sections for all three checks match the new wording.
Also
- CI: GitHub Actions dependency bumps.
v3.2.4
Patch release: turn CI green again. No behaviour change.
Fixed
TestRunBrokenWorktreePointerNamesGitCauseasserted that git names the gitdir path in its
error, which macOS git does and Ubuntu's does not (not a git repository: (null)) — so the test
passed locally and failed in CI. It now asserts what v3.2.3 actually guarantees: that git's own
parenthesised cause is carried through at all. The v3.2.3 fix itself is unchanged.
v3.2.3
Patch release: a clearer failure when the repo root can't be resolved.
Fixed
docgraphnow reports git's own reason when it can't resolve the repo root, instead of a flat
not a git repository. Linked worktrees always worked — git resolves the gitdir pointer natively
— but a dangling pointer (the main repo moved, pruned, or unreachable from a sandbox) produced
an error that read as "docgraph can't run in a worktree".GitRootand the other git wrappers now
carry git's stderr off*ExitError, and every call site surfaces it.
v3.2.2
Performance and two silent-failure fixes. No CLI surface change — go install github.com/lockyc/docgraph/v3@latest and existing hooks pick it up as-is.
leaks no longer dominates the run
The leaks check was ~98% of docgraph's runtime on a large repo, which made the
whole pre-push gate feel slow and hid how cheap the doc-graph work actually is.
Measured on a 9.7k-file repo (~1GB tracked, ~14M lines, 14 deny rules): ~25s →
~6s, with the six doc-graph checks at ~0.3s combined either way. Output is
byte-identical.
scanLine was running one FindAllStringIndex per rule per line — ~250M regexp
calls on that repo — and looksBinary only ever inspects a file's first 8000
bytes but was handed the whole file, so every vendored archive was read in full
on every run. Now:
- a whole-file prefilter answers "could any rule match here" in one pass, so the
per-line loop runs only on files with a candidate; - binary-ness is decided from an 8000-byte prefix before the rest is read;
- allow spans are computed on the first deny hit rather than eagerly per line.
The prefilter is deliberately superset-only — narrowing it would mean a missed
leak with a green gate — and three new tests pin that (casing, non-ASCII
literals, matches past the probe window).
Flags after the path are no longer silently dropped
docgraph . --skip leaks parsed . as the path and then ignored --skip and
leaks entirely: the check ran anyway, with no error. Go's flag stops at the
first non-flag argument, and every subcommand had the shape — --range after the
path on footgun-drift / covers-drift / doc-drift, --json / --ref on
graph, --ignore on the views. Generated hooks were unaffected (they put flags
first), but anything hand-typed or scripted was. Flags now parse in either
position.
footgun-drift prints the payload, not the paragraph
The nag echoed each added declaration line verbatim, and a footgun declaration is
often a whole CLAUDE.md paragraph. On one real push, nine findings printed 6.1k
characters of prose around ~270 characters of file:line. The echo only ever
said which declaration — you open the file to judge one — so it is now capped
at 100 runes and the repeated two-question preamble is folded into the header.
Same findings, 7314 → 1767 characters, and they now fit on screen at once.
v3.2.1
Fixes
doc-drift no longer costs ~1.3s at the end of every turn. It runs as a Stop hook, so
its cost is paid continuously — this release cuts it to ~0.04s. Two independent causes,
neither of them the actual drift scan (~20ms):
- Diff-base resolution was ~85% of a warm run.
docDriftDiffBasere-ran
ClosestBaseon every invocation — amerge-baseplus arev-listper
integration-branch candidate, a dozen git subprocesses — to recompute a value that
only changes when HEAD moves. It is now memoized per (repo, HEAD) alongside the
existing nag marker under$XDG_STATE_HOME/docgraph/doc-drift. A stale memo fails
safe: it can only go stale when an integration branch advances while HEAD stays put,
and the remembered base is then an ancestor of the true one — a superset diff, so
doc-drift over-reports rather than missing drift. - The loop guard now short-circuits before the diff rather than after it. Once a HEAD
has been nagged, every remaining path returns 0, so the base resolution, diff and doc
greps were pure waste. Exit codes are unchanged by construction.
Installs now absorb the first-exec code-signature assessment. On macOS a freshly
written binary has a new cdhash, so its first exec blocks in dyld for a live Gatekeeper
assessment — ~1.0s for this binary, versus ~0.03s warm. Every rebuild therefore re-armed
that ~1s toll on the next turn's Stop hook. All install paths now exec the new binary
once to absorb it while you are already waiting on the build: install.sh and
/docgraph:install already did so incidentally for the version string, and just install
now does too. All three are annotated as load-bearing, since the tempting cleanup is to
drop an exec that reads as a cosmetic smoke test.
Measured in this repo: warm 0.13s → 0.03s; first run after a rebuild 1.10s → 0.04s.
Behaviour, exit codes and CLI surface are unchanged — this is purely a cost fix.
Docs
CLAUDE.mdcarries the cold-exec mechanism as a footgun, and itsrunDocDrift
description tracks the new state-file helpers.
v3.2.0
Named deny groups for leaks
The leaks config could classify exceptions by directory but never by term
class, so the only cheap way to quiet a private repo saturated with your own
footprint vocabulary was ignore = ["**"] — which also blinded the scan to terms
that must not appear in any repo.
[[group]]blocks are named deny lists (name+terms+regex).
Top-levelterms/regexare the implicit, reserveddefaultgroup.[[dir]].ignore_groupsdecides which groups anignoreglob silences. It
replaces the default list rather than extending it; absent, it means
["default"], so every existing config behaves exactly as before.defaultis
itself a legal entry — naming it alongside another group restores whole-file
skipping.allow/allow_regexare unchanged and are not group-scoped: naming a string
still suppresses it whatever group it is in.docgraph leaks-rulesexports every group's vocabulary, so a history scrub
covers grouped terms too.
regex = ['(?-i)AKIA[0-9A-Z]{16}'] # default group: secret shapes
[[group]]
name = "footprint"
terms = ["acme-host", "/Users/you"]
[[dir]]
path = "/abs/path/to/private-repo"
ignore = ["**"]
ignore_groups = ["footprint"] # footprint is fine here; secrets stay liveUnknown keys in leaks.toml are now fatal
This can block a push on an existing config, so it is the change to read.
leaks.toml previously decoded unknown keys silently. That was tolerable when a
typo merely killed a rule; with ignore_groups it inverts which class is
silenced — ignore_group (singular) decodes clean, leaves the field empty, and
the ["default"] default then suppresses your secret shapes while leaving the
group you meant to silence live. A one-character typo, no warning.
Unknown keys now exit 2 with the offending keys named
(unknown key(s): dir.ignore_group), consistent with the existing
malformed-config-is-fatal contract. An absent config is still non-fatal, so
CI and fresh clones are unaffected.
If your leaks.toml carries a stray or legacy key, the first push after
upgrading will fail with that message — remove the key. A [log] table belongs
in config.toml, not leaks.toml, and is the most likely offender.
Also fatal
A [[group]] with no usable terms or regex. It would otherwise register as a
defined group, so an ignore_groups naming it would filter nothing and leave the
blanket ignore it was written for completely inert.
Upgrading
Nothing to do beyond the unknown-key check above. Grouping is opt-in; a config
with no [[group]] blocks behaves identically to 3.1.1.
v3.1.1
docgraph v3.1.1
Quieter pre-push output — a clean run no longer dumps a screenful into every push.
- Clean runs are one line.
docgraph .with no findings now prints a single
docgraph: clean ✓ (N tracked .md, M reachable, 0 findings)line instead of the
multi-line banner plus a(0)-count section for every check. - Findings keep the full detail — the self-describing banner, per-check
sections, and remediate footer still print when a check fails, so a failed push
is still explained end-to-end. On a finding, only the sections that actually have
findings are shown; the empty(0)sections are dropped there too.
No flag, exit-code, or check-behaviour changes — output formatting only.
v3.1.0
graph --ref <ref> — read the doc-graph at a git ref (bare-repo capable)
docgraph graph reads the working tree by default. The new --ref <ref> flag
(e.g. --ref HEAD, --ref dev) builds the graph from the committed state at
that ref, read entirely from the git object store — so graph now runs inside
a bare repo, with no checkout. This is the mode a scanner (e.g. Mycelium
reading a bare repo store) uses to capture a repo's doc-graph without
materializing a work-tree.
- Same output, one computation. A
fileSourceseam backs the graph builders
with two sources — the working tree (git ls-files+os.ReadFile) and a git
ref (git ls-tree+git show). Both feed the same graph computation, so the
served graph can never diverge by mode.GraphSchemaVersionis unchanged (still
1); on a clean tree,--ref HEADproduces output identical to the default run. - Correct-by-definition divergence.
--refreflects committed state only, so
it ignores uncommitted or untracked edits to tracked docs — which is what
reading "at a ref" means, and what a bare-store scanner wants. - Working-tree behavior unchanged. With no
--ref, every path behaves exactly
as before. Onlygraphis ref-aware; the pre-push audit checks stay
working-tree only.
No breaking changes — additive flag, unchanged payload shape. Consumers on
@latest pick it up transparently.
v3.0.0
docgraph v3.0.0
A major release: docgraph now models documentation as two independently-enforced
graphs and, for the first time, serves that graph to agents and tools.
Breaking
- Module path moved to
github.com/lockyc/docgraph/v3. Reinstall with
go install github.com/lockyc/docgraph/v3@latestand regenerate any pre-push
hook (docgraph install-hook --force). A hook still pointing at/v2@latest
keeps installing the v2 line. orphansis now the content-graph island rule. An orphan is a non-root
doc with zero inbound content edges (markdown.mdlinks ∪ path-mentions,
scanned over the doc body). Root-reachability is no longer enforced — findability
is served by the newgraphview instead of a hand-curated index chain.- A frontmatter block is now required on every doc, except any file named
README.md(basename-wide — GitHub renders leading YAML as a metadata table).
Repos that don't want this opt out with--skip frontmatter.
New
disconnectedcheck — the metadata-graph island rule. A doc carrying
frontmatter but declaring no doc→doc edge (part-of/supersedes/see-also/
depends-on;covers→code andsource→external don't count) is flagged. It
enforces that every doc declares its place in the structure. Opt out with
--skip disconnected.docgraph graph— a read-only view that serves both graphs. Human render
(thepart-ofhierarchy, cross-references, and both island lists) by default;
--jsonemits a stable,schemaVersion: 1, uniformly-camelCase payload
(nodes,contentEdges,metadataEdges,islands) — the machine seam a
catalog or ecosystem graph ingests. Likecovers/index/stale, it never
gates.
Fixes since v2.1.0
- The guided installer's detect step and install hints completed the
/v2module
sweep. - The
.gitignorescratch/worktree baseline was completed.
Migrating a consumer repo
- Reinstall:
go install github.com/lockyc/docgraph/v3@latest. - Regenerate the hook:
docgraph install-hook --force. - Run
docgraph .and conform, or opt out per the repo's doc model:- a nav-driven MkDocs repo:
--skip orphans(every page is a prose-orphan by design); - a repo not adopting frontmatter yet:
--skip frontmatter(and--skip disconnected); - a derived/generated corpus:
.docgraphignore.
A prose-linked repo conforms by giving each doc atype:frontmatter block and
a doc→doc edge, and linking any genuine orphans.
- a nav-driven MkDocs repo:
v2.1.0
⚠️ The install command has changed
go install github.com/lockyc/docgraph/v2@latestThe /v2 suffix is required. go install github.com/lockyc/docgraph@latest — the
command in every previous doc — has been hard-failing for all consumers since the
docaudit → docgraph rename, producing no binary at all:
module declares its path as: github.com/lockyc/docaudit
but was required as: github.com/lockyc/docgraph
The module path lacked the /vN suffix Go's semantic import versioning requires at major
≥2, so the proxy rejected every v2 tag and @latest fell back to the newest v1 — which
still carried the pre-rename module path. v2.0.0 was never installable. v2.1.0 is the
first working v2 release. If you have a pre-push hook generated by an older docgraph,
regenerate it with docgraph install-hook --force to pick up the corrected install hint.
Added since v2.0.0
A frontmatter model for the doc graph. Docs can carry a leading YAML block with a
type, freshness metadata (verified / review), and typed edges (links:) — covers,
part-of, supersedes, depends-on and friends. Two new whole-state checks enforce it:
frontmatter (well-formedness + a required type) and edges (internal edge targets must
exist; part-of/supersedes must not cycle). Typed doc edges also feed orphan
reachability, so a doc reached only via an edge is no longer a false orphan. docgraph schema emits the vocabulary as JSON Schema so other tools conform instead of re-encoding it.
covers-drift — a second advisory pre-push rider. Flags a doc whose covers edge
points at code your push changed while the doc itself went untouched. It's the graph join
doc-drift can't do: a rewritten function whose doc describes the old behaviour in prose
leaves no removed symbol and no changed literal to grep. It judges nothing, so it never
blocks — editing the doc is the escape hatch, and a repo with no covers edges never sees
it. Opt out with --no-covers-drift or DOCGRAPH_COVERS_OFF=1.
Read-only doc-graph views — covers <path> (which doc governs this file), index (a
generated markdown index, redirect it into a tracked file), and stale (docs past their
freshness threshold). None gate; all exit 0.
A docgraph skill + /docgraph:install. The gates push themselves at an agent; the
views are pull-only, so the skill advertises them for the cost of one description line.
/docgraph:install wires the doc-drift Stop hook, offers the per-repo gate, and seeds the
leaks config.
Fixed
- A
.mdedge with a colon in its anchor classified as cross-repo instead of a doc. indexlabels fall back to the body H1 rather than the path.- Five load-bearing behaviours had no test and were mutation-verified into one:
CoversOf's
directory-prefix guard, the generated hook's advisory|| trueon thecovers-driftline,
ClosestBase's fewest-commits arbitration,nonCodePathspec's full extension set, and
StaleDocs' fallback on an unparseablereview.
Docs
Reconciled five claims the code had already falsified — the dependency list (yaml.v3 is
direct), verified/review being read by the stale view, the usage-log example record's
check count, the broken-link scope, and /docgraph:install never naming the covers-drift
rider it installed.
Full Changelog: v2.0.0...v2.1.0