| name | true-up |
| description | Keep a repo's docs/data/code in sync after a change. Use when a change may have left dependent docs or generated views stale, to find exactly what's affected (deterministically) and regenerate/worklist it. Triggers — "true up the repo", "what's stale after this change", "did editing X break the docs", drift between a source-of-truth file and the prose that cites it. |
true-up
A deterministic Git/jj-native dependency-graph CLI: it knows what each artifact depends on (at
fact-level granularity) and computes — without guessing — what a change makes stale.
When to use
- After editing a source-of-truth file (a data/config JSON, a spec), to find which docs now drift.
- Before a commit/PR, to regenerate generated views and worklist the prose that needs a human/LLM edit.
- As a gate: fail a commit/push if the graph is stale or a zone/leak rule is violated. The gate
commands exit non-zero on a problem (
--check/--check --committed exit 1 if stale; --policy
and --externalities exit 1 on violations/leaks), so they wire straight into pre-commit/CI.
Adopt it in a repo (one-time)
- Scaffold a starter config:
true-up init writes a .true-up.json at the repo root (idempotent —
re-running on an existing config is a no-op and still exits 0; it never overwrites). The scaffold
ships a commented _seed_example showing the edge shape. Then edit it to declare your
stewards (source-of-truth data files to decompose into facts), zones (per-path
visibility/intent + rules), and declared seed edges. Use seed when the dependency should
live in config instead of an inline marker: { "from": "doc.md", "to": "src/app.py#parse_config", "kind": "derives-facts-from" }. For the full wire-up recipe (file- vs fact-granular, span anchors,
generated-from), run true-up robot-docs — the in-tool handbook.
- Add fact-anchors where a doc cites a specific fact:
<!-- fact: <path>#<key> -->. The <key> is a
JSON <arrayProp>.<key>, a span-anchor id, or a tree-sitter symbol name (see "Code as a
source-of-truth").
- Make code a source-of-truth (any language): bracket a region with
true-up:anchor id=NAME /
true-up:end, or set "symbols": true to auto-extract code symbols via tree-sitter.
- Mark generated blocks with a self-describing marker:
<!-- generated by <gen> from <source> -->.
Invoking the tool
If you installed from npm (npm i -g true-up, or run it via npx true-up), the true-up command is
on your PATH — run it directly inside your repo:
true-up [command]
true-up --repo <target-repo> [command]
From a bare clone (not npm-installed), there is no true-up on PATH — invoke the entry directly:
node bin/true-up --repo <target-repo> [command]
--repo resolves the target; absent it, the tool uses $TRUE_UP_REPO, then the Git/jj toplevel of
the CWD, then the CWD. All paths/targets below are relative to that target repo. In multi-worktree or
jj sessions, start with true-up status --json and inspect .workspace.root, .workspace.repoSource,
.workspace.git.linkedWorktree, .workspace.vcs, and .workspace.warnings before acting. The
true-up shorthand in the examples is that same command.
Run it
true-up status
true-up graph --json
true-up robot-docs
true-up build
true-up --impact --since HEAD~1
true-up --impact --since HEAD~1 --proof --json
true-up run --since HEAD~1
true-up gate
true-up hooks --install
true-up --verify-scope --since HEAD~1
true-up --check
true-up --check --committed
true-up --policy
true-up --externalities
true-up init
true-up capabilities
true-up <read-cmd> --json
true-up --no-write
true-up --version
true-up --help
true-up --repo <path>
Structured output. Every read-side command takes --json → a single JSON object on
stdout (data only; diagnostics go to stderr), so an agent/workflow parses the result instead of
scraping human text. true-up capabilities returns the whole contract (commands, flags, the exit-code
dictionary) so you never have to remember it. A mistyped command gets a did you mean: … suggestion.
For status --json, ok means the probe ran; gateGreen means cache/policy/leak gates are clean;
green means no truing-up work remains. Automation should not treat ok: true as "done." Agents in
parallel worktrees should also check .workspace.root and .workspace.warnings before running any
suggested command. When status was run through --repo or $TRUE_UP_REPO from another repo,
nextCommands[] are repo-qualified with --repo <root> for safe copy-paste across panes.
run is the deterministic gate: it regenerates mechanical dependents, prints the advisory worklist,
and verifies (policy clean + on-disk graph in sync). The CLI never edits prose. The agentic layer
that makes the minimal advisory prose edits lives in workflows/: the default
maintenance workflow (light, blast-radius-scoped, anti-code-golf — gated by --verify-scope) and a
separate report-only audit workflow (whole-repo lint + drift + candidate missing edges; proposes,
never decides). Use maintenance after a change; run audit deliberately (pre-release / periodic).
true-up does not replace the target repo's formatter, linter, or test suite. Discover those from the
repo's package scripts, CI, or contributor docs and run them separately, then run true-up gate as the
cross-file dependency/leak gate. If a formatter or lint fixer rewrites files, rerun
true-up status --since <ref> because those edits may move tracked facts, spans, or generated outputs.
How to read the output
- mechanical dependents = regenerated automatically (generated views) — no prose edit.
- advisory dependents = a human or LLM must rewrite the prose to match the moved fact, minimally.
true-up does not edit prose itself; it tells you precisely what and why (which fact moved).
true-up --impact <source> is the full blast-radius list: it prints every dependent artifact. If
many generated files share one generator via, --impact still lists all files; run executes that
distinct via once.
true-up --impact --since <ref> is the remaining-stale view. After an agent has already edited a
dependent, that dependent may be absent from the default list because it changed in the same range.
Add --proof --json to audit changed facts and mark dependents as changed-in-range or
not-changed-in-range; live symlink aliases are marked satisfied-by-live-alias. This proves edit
coverage, not semantic correctness.
Exit codes (so you can gate on them)
--check — exit 0 if the on-disk graph matches a fresh build; exit 1 if it drifted OR is absent
(--check rebuilds in-process, so an unbuilt graph reads as stale → exit 1, never exit 2). The
"graph not built yet → exit 2" case applies to --impact/run, which read the prebuilt graph.
--check --committed — exit 0 if the committed-or-staged graph blob equals a fresh rebuild;
exit 1 if it's stale, and exit 1 if the graph is untracked (no false assurance). This is the real
drift gate for repos that commit the graph; prefers the staged blob (pre-commit), else HEAD (CI).
--policy / --externalities — exit 1 on any violation/leak; exit 0 when clean. Add --report
to force exit 0 (print the findings without failing the build — report-only).
--impact --since <ref> — exit 0; but a <ref> that does not resolve to a commit exits 2 (a bad
ref is an error, never a silent "0 dependents"). Add --proof for the completed-pass audit map.
run — exit 0 if GREEN; exit 1 if not GREEN (a generator failed, policy had violations, or the
graph is stale). With --strict, exit 2 when advisory prose still needs review.
--verify-scope --since <ref> — the anti-code-golf gate. exit 0 if every changed file is explained
by the graph (the changed source-of-truth, its regenerated/advisory dependents, or the cache); exit 1
(naming the offender) if a changed file has no declared dependency linking it to what changed — i.e. a
maintenance pass strayed outside the deterministic blast radius. Undeclared staleness is the audit's
job; this keeps the change set deterministic. Bad ref → exit 2.
--help / -h / help — prints the command table and writes nothing, exit 0.
- An unknown command/flag exits 2 and writes nothing — it does not silently build and write
the graph into the target repo.
The graph file is commit-optional
.true-up/depgraph.json is regenerable from sources + markers + .true-up.json, so committing it is
optional. If you don't commit it, --check (working-tree freshness) is your gate. If you do
commit or track it (so the VCS is the database and reviewers can diff the graph), --check --committed
is the drift gate that catches "source changed without the regenerated graph" — and it fails closed
when the graph is absent from the VCS view. In Git it reads the staged blob first, then HEAD; in
jj-only repos it reads @. Pick one model per repo and gate accordingly.
Suppressing legitimate path examples
The leak scans (--externalities, and the no-machine-local-paths / no-private-operational-leak
zone rules in --policy) ignore anything inside inline or fenced code spans, so a doc that shows
a forbidden path shape as an example is not flagged. For a legitimate path example in plain prose
(e.g. a privacy policy quoting a path), opt that line out with a directive:
<!-- true-up:ignore-line [rule] --> suppresses findings on THIS line
<!-- true-up:ignore-next [rule] --> suppresses findings on the NEXT line
The optional [rule] (e.g. no-machine-local-paths) scopes the suppression to one rule; omit it to
suppress all rules on that line. Directives are matched against the original line text, so they still
apply even where the surrounding code span would otherwise be blanked.
Code as a source-of-truth (any language)
A doc can derive from code at fact granularity, three ways:
- JSON stewards — JSON with a top-level array-of-objects (
facts in .true-up.json); facts are
path#arrayProp.key.
- Span anchors (zero-dep, any language) — bracket a region with a paired comment token that rides
whatever comment syntax the language already has, so no parser is needed:
# true-up:anchor id=NAME … # true-up:end (Python/shell/TOML), // true-up:anchor id=NAME …
// true-up:end (JS/Rust/Go/C/C++). The region's bytes become the fact path#NAME; a doc anchors
with <!-- fact: path#NAME -->. A bare true-up:end closes the most-recent open. Examples shown in
markdown code fences are inert (not parsed as live).
- Symbols (opt-in) — set
"symbols": true to auto-extract top-level definitions via tree-sitter
(Python/Rust/Go/JS/TS/C/C++) as path#SymbolName, no manual markers. Needs the optional
web-tree-sitter + tree-sitter-wasms deps (npm install in the tool dir); enabled-but-missing
fails loud (exit 2), never a silent symbol-less graph.
In every case edges stay explicit — extraction only produces better nodes; a doc must cite the
fact for an edge to exist (correlation never assigns the arrow). Cite it marker-free via a
fact-granular seed in .true-up.json — { "from": "doc.md", "to": "src/app.py#parse_config" } (the
to is a JSON-key / span-id / symbol) — so no inline marker touches your files; or use an inline
<!-- fact: path#id --> anchor if you prefer a co-located citation. Both resolve to the identical edge,
and a seed to a nonexistent file/fact is a hard error (fail-loud). A code file is also a valid
file-granularity seed endpoint without any of these. If you declare no
facts / anchors / symbols / seed, the build prints a NOTICE: the drift layer is inert (it passes
--check trivially) until you give it something to track.
Case study: this repo
true-up uses true-up on itself. In this repo, .true-up.json declares document audiences through
zones and document/code/release dependencies through marker-free seed edges: README.md is the
external overview, SKILL.md is for external agents, AGENTS.md is for maintainer agents,
PUBLISHING.md is for credentialed release agents, and docs/CONFIG.md is the config reference.
Query it directly with true-up graph --json, true-up --impact docs/CONFIG.md, or
true-up --impact meta/contract.json#agent_guidance.declared-seed-edge.
Invariant to respect
Never edit a generated artifact by hand (regenerate it); never change a fact in prose to mask drift
(fix the source-of-truth — prose only re-derives). The graph treats those as errors.