Whyfile: intent-driven development for your codebase. Extract the decisions, constraints, and trade-offs behind your code as grounded, anchored graph nodes, and query them back.
Project description
Whyfile
Your codebase remembers what it does. It forgot why.
whyfile reconstructs that why, keeps it honest — every unit tagged authored or reconstructed, so a model's guess never poses as a decision you made — and puts it on a gate that fails the PR quietly breaking it. Every repo has a Makefile; this is the missing Whyfile. (Formerly graphify-intent; see ADR-0024.)
Why "Whyfile"?
Every repo has a Makefile that says how to build, a Dockerfile that says how to run, and a lockfile that says exactly what ships. None of them say why. Whyfile is the missing file: the decisions, constraints, and trade-offs behind your code, extracted into a queryable graph, anchored to the code they govern, and kept honest by provenance tiers that separate what a human recorded from what a model reconstructed.
The name is also the roadmap. Intent-driven development has one invariant: no unexplained change. Every change either conforms to recorded intent, supersedes it explicitly, or records a new decision. The conformance gate (whyfile intent-diff --gate) enforces it: a pull request that silently violates a recorded constraint does not merge. The why goes on file, and the file bears load.
What it does
whyfile reconstructs the why your code lost: the decisions, the dead ends, and the constraints the awkward bits are quietly paying for — the reasoning that never became a comment because nobody writes # WHY: above the thing they decided not to do. Surfacing rationale that's already written down is table stakes; two things make reconstructed intent trustworthy instead of plausible-sounding fiction:
- It's kept honest by provenance. Every unit is tagged
authored(a human recorded it) orreconstructed(a model inferred it), grounded to the exact source span it came from, so a model's guess never outranks the record or poses as a decision you made. - It's load-bearing, not decorative.
whyfile intent-diff --gatefails a pull request that changes constraint-governed code without conforming to the constraint or explicitly superseding the decision behind it. The why doesn't just sit in a graph you query — it can block your merge.
Under the hood it reads a graphify knowledge graph alongside your prose docs, runs a short pipeline of LLM passes (extract → anchor → cross-doc relate, plus an opt-in concept-resolution pass), and writes a sidecar JSON, an enriched graph.json, and a report. Three LLM-free query commands read it back. Think of it as the month of code archaeology every new hire does on their way in — done once, checked in, and enforced.
flowchart LR
D["docs/*.md"] --> A
G["graph.json"] --> B
A["Pass A<br/>extract intent"] --> B["Pass B<br/>anchor to concepts"]
B --> C["Pass C<br/>cross-doc intent"]
B --> R["Pass D<br/>concept resolution<br/>(opt-in)"]
A --> OUT
C --> OUT
R --> OUT
OUT["outputs:<br/>.whyfile.json<br/>graph.enriched.json<br/>enrichment_report.md"]
Requirements
whyfile post-processes graphify graphs and depends on graphifyy (the graphify runtime that every backend calls), which is installed automatically (ADR-0010). By default it uses your Claude Pro/Max subscription via the Claude Code CLI, so no API key is required. To use a billed API key instead, see LLM backend.
Installation
pip install whyfile # brings graphifyy (the graphify runtime) with it
# or as a standalone tool:
uv tool install whyfile
Not a Python project? whyfile is a standalone dev tool — install it the way you install your other CLIs:
| You have… | Install with |
|---|---|
uv |
uvx whyfile — or uv tool install whyfile to put it on PATH |
npm / Node |
npx whyfile — delegates to uv; if uv is missing it prints the one-line install |
| neither / no Python | install uv (curl -LsSf https://astral.sh/uv/install.sh | sh, brew install uv, or winget install astral-sh.uv), then uvx whyfile — uv brings its own Python |
The LLM backend still needs the claude CLI (subscription default) or an API key — see LLM backend.
For an API backend, add the provider extra so graphify has its SDK (the subscription backend needs neither):
pip install 'whyfile[anthropic]' # Anthropic API
pip install 'whyfile[gemini]' # Gemini API
To enable Pass D's embedding-based concept resolution, add the optional extra:
pip install whyfile[embeddings] # local model2vec embedder for Pass D
Without it, Pass D still runs, degrading gracefully to a lexical-similarity fallback.
Quick start
# Uses your Claude Pro/Max subscription by default (needs the `claude` CLI).
whyfile \
--graph graphify-out/graph.json \
--docs docs/ \
--passes A,B,C
See it on this repo
We point the tool at itself, because it's the most honest demo we can give you. A full run over
this project's own docs/adr/ trail grows 172 code concepts into 234 nodes and pulls
62 intent units (18 decisions, 18 mechanisms, 16 constraints, 10 trade-offs) out of the prose,
each grounded to the exact span it came from (100% grounding), 53 anchored back to the code
they explain.
Then the why bears load. Open a PR that edits cache.py — code governed by four constraints
recorded in ADR-0015 — without accounting for them, and the gate
stops the merge:
$ whyfile intent-diff --files src/whyfile/cache.py --gate
Intent diff: introduces 0, supersedes 0, governed 1, conformance-review 1
! src/whyfile/cache.py governed by constraint: Restores extraction, not re-sync, …
Conformance gate: action required — conform to each constraint, or supersede its decision record:
✗ src/whyfile/cache.py — Restores extraction, not re-sync (ADR-0015) → conform, or supersede ADR-0015
✗ src/whyfile/cache.py — Consistent --docs spelling required (ADR-0015) → conform, or supersede ADR-0015
✗ src/whyfile/cache.py — End-of-run sidecar write loses work (ADR-0015) → conform, or supersede ADR-0015
✗ src/whyfile/cache.py — Corrupt entry treated as miss (ADR-0015) → conform, or supersede ADR-0015
$ echo $?
5
That exit 5 is the product. The change conforms to each recorded constraint, or the same PR supersedes ADR-0015 out loud — silently breaking a decision the code is built on is not a path the merge leaves open.
And it stays honest about what it knows. Ask why the code looks the way it does, and every
answer wears its provenance — reconstructed (a model inferred it) never dressed up as authored
(a human recorded it):
$ whyfile explain cache.py --format text
cache.py (code)
• Orphan cache entries accumulate [reconstructed tradeoff] — Orphan cache entries accumulate
over a corpus's lifetime with no automatic pruning.
why: Deferring cleanup accepts unbounded cache growth to avoid deleting live entries, because
auto-prune cannot safely distinguish orphaned entries from ones excluded by the current run.
alternatives: Auto-prune or an immediate --prune-cache flag, deferred because --docs subsetting
makes automatic pruning unsafe.
That rationale isn't a comment in cache.py — it lives in
ADR-0015, which the tool read, distilled, tagged reconstructed,
and wired back to the file the decision governs. Surfacing that round trip is table stakes.
Gating on it — and never letting a model's guess pose as ground truth — is the product. (The
query commands are covered in Querying the intent layer below.)
LLM backend
By default, whyfile uses your Claude Pro/Max subscription through the
Claude Code CLI (claude): no API key,
no per-token billing (the run still counts against your plan's usage limits; see
What a run costs). It falls back to a billed API key if the CLI isn't
available.
--backend |
Behaviour |
|---|---|
| (unset) | Subscription if the claude CLI is on your PATH, else a detected API key |
subscription |
Force the Claude Code CLI (subscription auth) |
api |
Force a billed API key (ANTHROPIC_API_KEY / GEMINI_API_KEY) |
claude / gemini / … |
Force a specific graphify backend |
You can also set GRAPHIFY_INTENT_BACKEND. For subscription mode, install the claude
CLI and run it once to sign in. For API mode, install the matching extra
(graphifyy[anthropic] or graphifyy[gemini]) and provide the key.
Keep API keys out of .env and your shell history
If you do use an API key, prefer a password-manager CLI over inlining the secret. With
1Password's op, inject it at runtime so it
never lands on disk in plaintext:
export ANTHROPIC_API_KEY="$(op read 'op://<vault>/<item>/credential')"
whyfile --backend api --graph graphify-out/graph.json --docs docs/
…or wrap the command with op run so the secret lives only for that process:
op run --env-file=.env.op -- whyfile --backend api --graph graphify-out/graph.json --docs docs/
A real key committed in .env risks leaking into git history, CI logs, and backups; a
manager keeps it encrypted, access-audited, and revocable. Best of all, the default
subscription backend needs no key at all.
CLI flags
| Flag | Default | Description |
|---|---|---|
--graph |
graphify-out/graph.json |
Path to the graphify output graph.json |
--docs |
docs/ |
Directory containing markdown source files |
--passes |
A,B,C |
Comma-separated passes: A extract, B anchor, C cross-doc relate, D cross-doc concept resolution (opt-in) |
--backend |
(auto) | Backend selection (see LLM backend) |
--progress |
auto |
Progress reporting mode: auto (live line on TTY, heartbeat when piped), plain (heartbeat lines only), json (NDJSON events on stderr), none (silent). stdout stays results only; warnings and progress go to stderr. |
--min-confidence |
0.75 |
Minimum confidence_score for Pass C and Pass D edges |
--max-concurrency |
4 |
Maximum number of concurrent LLM requests |
--max-tokens |
500000 |
Estimated-token ceiling for the whole run (all passes, summed). Enforced three ways (a pre-flight prompt, a mid-Pass-A breaker, and a post-Pass-A checkpoint before Pass B), so a run stays within the estimate it showed you. Nothing is written on an abort; --yes skips only the pre-flight prompt, never the checkpoint. 0 = unlimited. Full semantics in AGENTS.md |
--max-cost-usd |
2.00 |
Cost guard for API backends: prompts if the pre-flight estimate exceeds this (the subscription backend has no per-token bill to guard, so it skips this check; --max-tokens is the guard that applies there). Pre-flight only; the post-Pass-A checkpoint gates tokens, not cost |
--yes |
false |
Skip the pre-flight ceiling prompt (non-interactive / CI); does not skip the post-Pass-A whole-run checkpoint |
--dry-run |
false |
Project calls/tokens/cost/time as JSON and exit: no extraction (one tiny backend liveness ping only). JSON shape documented in AGENTS.md |
--no-cache |
false |
Bypass the Pass A cache entirely: no reads, no writes; every section re-extracts. See Caching & resume |
--doctor [--json] |
false |
Check the environment (runtime, backend auth, graph/docs shape, version drift) and exit; --json for machine output. Version drift is advisory only; a stale editable install no longer fails the run |
--version |
n/a | Print the installed version and exit |
Set --backend to subscription, api, or an explicit provider name (see LLM backend).
stdout is strictly results-only. The pre-flight projection line (Pass A: ~… tokens (whole run) …), the Pass A: gated N section(s) prefilter notice, and every
ceiling/prompt/abort message print to stderr; stdout carries only the per-pass result lines,
the sidecar path, and Done..
Exit codes
Stable across releases (src/whyfile/errors.py), safe for scripts/agents to branch on:
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Unexpected error (unhandled exception; never raised deliberately) |
2 |
Bad input: invalid --passes, graph.json missing, no .md docs found |
3 |
Token/cost ceiling: pre-flight decline, non-interactive over-ceiling, the Pass A budget breaker, or the post-Pass-A whole-run checkpoint before Pass B |
4 |
Backend unusable: none configured, the graphify runtime missing, or all Pass A sections failed |
5 |
Policy gate: changed --fail-on-constraint (a changed file is governed by a constraint), intent-diff --gate (constraint-governed code changed with no superseding record in the PR), or check (a compiled constraint in intent-rules.json is violated) |
See AGENTS.md for the full contract (per-code agent actions, --dry-run/--doctor
JSON shapes, progress events, and manifest fields) for driving this tool non-interactively.
Per-pass models (subscription backend)
On the subscription (claude-cli) backend, each pass defaults to a different model rather
than one blanket choice, which kills the old Opus-for-everything default:
| Pass | Default model |
|---|---|
| A (extract) | sonnet |
| B (anchor) | haiku |
| C (cross-doc relate) | haiku |
| D (concept resolution) | haiku |
Pass A keeps the stronger model because it does the actual reasoning (deriving a claim,
rationale, and alternatives from prose); B/C/D are comparatively mechanical, so haiku
carries them at a fraction of the cost. Set GRAPHIFY_CLAUDE_CLI_MODEL to override the
model for every pass, e.g. GRAPHIFY_CLAUDE_CLI_MODEL=haiku if your corpus is
code-heavy enough that even Pass A doesn't need sonnet-level reasoning.
--max-concurrency changes wall-clock time only, not cost, and only on the API backend.
The actual cost levers are the model (per-pass tiering above), the call count (Pass A's
structural gate on trivial sections), and per-call context; raising concurrency just runs the
same calls faster in parallel, there. On the subscription backend, claude-cli calls serialize
upstream in graphify (a lock around the CLI subprocess), so --max-concurrency has little
effect: raising it does not make subscription runs faster.
Caching & resume
Pass A caches each section's extraction on disk at graphify-out/.intent_cache/ (next to
graph.json), keyed by a content hash of the exact prompt (source file + section path + body),
the resolved Pass A model, and a fingerprint of the system prompt + schema. Each entry is
written the moment its section completes.
- Automatic reuse. Re-running the same corpus serves already-extracted sections from
cache; only new or edited sections (their body hash changed) re-extract. The pre-flight
estimate, ETA, cost gate,
--dry-runprojection, and the post-Pass-A checkpoint all size Pass A off the cache-miss sections only, so a fully cache-warm re-run projects ~0 calls/tokens for Pass A. - Resume after an abort. If a run aborts (either the mid-Pass-A token breaker or the
post-Pass-A
--max-tokenswhole-run checkpoint before Pass B), every section that finished before the abort is already cached. Just re-run (after raising--max-tokens, or narrowing--docs) and Pass A resumes from where it left off; the abort message says so. --no-cachebypasses the cache completely for the run: no reads, no writes.- Force a refresh by deleting
graphify-out/.intent_cache/; there's no separate "clear cache" flag. - Use a consistent
--docsspelling. The cache key embeds the source-file path string as--docsproduces it, so re-running against the same docs with a differently-spelled--docs(e.g. relative vs. absolute) can miss the cache even though the files are the same.
What a run costs
On an API backend you pay for real tokens, and it stays cheap. whyfile scales with your
docs, not your lines of code (the base graphify graph for source is AST-based and free), so the
bill tracks how much prose you point it at. Pass A does the reasoning on sonnet at roughly half
a cent per doc section and dominates the total; Pass B/C/D run on haiku for pennies.
On the default subscription backend there is no per-token bill: the run goes through your Claude Pro/Max plan, so there is no separate dollar cost. But the run is not free: it still consumes the estimated tokens in the table below against your plan's usage limits, the same tokens an API run would be billed for. "No bill" is not "no cost".
| Corpus | Doc sections | Intent units | Est. tokens | API cost |
|---|---|---|---|---|
This repo's docs/adr (measured) |
55 | 62 | 65.6k | ~$0.33 |
| Medium doc set | ~200 | ~225 | 230k | ~$1.18 |
| Large doc set | ~800 | ~900 | 914k | ~$4.71 |
Passes A,B,C (the default); adding Pass D moves the total by under a cent. The first row is measured
on this repository; the other two are the tool's own projections at representative sizes, priced at
the published API rates (sonnet $3/$15, haiku $0.80/$4.00 per Mtok). These are cold-cache,
from-scratch figures: because Pass A caches every section, a re-run costs about $0 on API
(only new or edited sections re-extract); on subscription a re-run still costs about zero
tokens, for the same cache-hit reason.
--dry-run and the end-of-run manifest report this honestly: est_cost_usd is a real number on
the API backend, but null on subscription (never 0.0, which would read as free), paired with a
billing note stating the estimated token count and that it counts against your plan.
To see the exact projection for your own corpus before spending anything, --dry-run prints it as
JSON and makes no extraction calls:
whyfile --dry-run --graph graphify-out/graph.json --docs docs/
Outputs
All outputs are written to the same directory as graph.json:
| File | Contents |
|---|---|
.whyfile.json |
Sidecar: all intent nodes and edges added across runs |
graph.enriched.json |
Original graph merged with the intent layer (graphify node-link format) |
enrichment_report.md |
Before/after metrics: node/link/isolated counts, intent breakdown by kind, anchored vs orphaned, grounding coverage |
.whyfile_progress.jsonl |
Progress stream (NDJSON, one event per line): run_start, pass_start, tick, unit_fail, pass_end events for agents to tail or poll. Written for any --progress mode except none. |
intent_run.json |
Per-run manifest: status/error, metrics, artifacts, and per-pass records (passes[], including s_per_call, cache_hits, and cache_misses; the cache fields are nonzero only for Pass A). Written on every run (independent of --progress mode). At run end, an estimated-vs-actual runtime/duration reconciliation line is printed to stderr. |
intent_runs.jsonl |
History: the same manifest object appended as one line per run, oldest first: a run-over-run log without snapshotting intent_run.json yourself. |
.whyfile.prev.json |
Snapshot of the sidecar as it was before this run's merge. Written every run (skipped on the first run). Powers digest --since last-run. Git-ignored under graphify-out/. |
Querying the intent layer
Once a run has produced its outputs, three subcommands read them back: read-only, LLM-free,
and free of charge (no backend call, no cost). Each prints exactly one JSON object on
stdout by default; add --format text for a human-readable render instead. All three accept
--graph PATH (default graphify-out/graph.json, same as the pipeline; may be given before or
after the subcommand name) and exit 0 for a status of ok/no_strong_match, 2 for
not_found/ambiguous/no_intent_layer (see Exit codes; full JSON schemas are
in AGENTS.md).
explain <node> resolves <node> (an exact node id, else a rapidfuzz match over node
labels) and explains it: for a code/document node, the intent node(s) that motivate it (the
intent-origin rationale_for edges pointing at it); for an intent node, its own
claim/rationale, the node(s) it explains, and its Pass-C relations (a supersession is surfaced
as superseded_by on the superseded node). Requires graph.enriched.json; returns
status: "no_intent_layer" (exit 2) if it doesn't exist yet.
whyfile explain AuthModule
whyfile explain i_d1 --format text
list-intent [--kind decision|mechanism|constraint|tradeoff] [--min-confidence F] returns the
intent inventory, optionally filtered by kind and/or a minimum confidence_score.
whyfile list-intent --kind decision --min-confidence 0.8
why "<question>" ranks intent nodes against free text with rapidfuzz token matching and
returns the top --top (default 5) by score (0-100); a top score below the cutoff (60) still
returns its results, just with status: "no_strong_match" so a caller can tell the match was
weak.
$ whyfile why "why does Pass A use a stronger model than the others" --top 2 --format text
Top 2 match(es) (no_strong_match):
56 [tradeoff] Per-pass tiering protects Pass A quality — Running Opus on every pass was the
cost bug; tiering lets cheaper models handle most passes, but Pass A is deliberately held
on sonnet because its output quality matters enough to justify the higher tier.
56 [decision] Per-pass model tiering — Passes B/C/D need less reasoning than A, so downgrading
them to cheaper haiku captures most of the cost saving without hurting quality — but a
user's explicit model choice still wins.
list-intent and why don't require graph.enriched.json; they fall back to the
.whyfile.json sidecar when it's the only artifact present. The pipeline itself is
unchanged: running whyfile with no subcommand still runs the extract/anchor/relate
run described above.
Team collaboration
whyfile turns the tribal "why is the code like this?" into a layer that is queryable, PR-reviewable, and survives turnover. Three jobs a team gets for free (all LLM-free, $0 at query time):
- Onboard. A new hire runs
whyfile explain <file>instead of interrupting a senior. - Review.
whyfile changed --base mainsurfaces the intent a PR touches, so a reviewer sees the constraints a change is bound by. - Retain.
whyfile coverageshows which code has recorded intent and which is dark, andwhyfile digestreports what was decided since a checkpoint.
The PR check
git diff --name-only origin/main...HEAD | whyfile changed --format markdown
Maps each changed file to the intent that governs it (constraints and trade-offs first). It is
informational by default (exit 0). Add --fail-on-constraint to turn it into a soft gate that
exits 5 when a PR touches a file bound by a constraint, so the constraint gets a reviewer's
acknowledgement.
Coverage and digest
whyfile coverage --format text # golden fraction, intent debt, covered vs dark code
whyfile digest --since last-run # what intent changed since the last run
coverage reports the golden fraction beside the coverage numbers: the share of intent that is
trusted evidence (human-written: authored decision records, captured live decisions, and attested
commit trailers / # why: comments; the numerator grows further as the confirmed tier lands) over total
intent. It is partitioned by evidence class and never blended, so reconstructed intent is shown
separately and never inflates the trusted count. Alongside it, intent_debt reports dark files and
orphaned intent (intent that explains no code); stale decisions come from drift and disputes from
the audit phase, reported as null here rather than a misleading zero.
The attested tier is the zero-workflow capture channel: on every pipeline run (unless
--no-attest), commit trailers (Why:/Decision:) and # why: code comments are ingested
deterministically as provenance: attested intent nodes, anchored to the code they touch or
annotate. This gives any repo a non-empty trusted layer on day one, with no new workflow to adopt.
List them with whyfile list-intent --provenance attested.
The capture subcommand records a decision at the moment of choosing, the cheapest time to capture ground truth:
whyfile capture --chosen "Use SQLite" --question "Which datastore?" \
--option "Use SQLite" --option "Use Postgres" --rationale "single-writer, zero-ops" \
--recommendation "Use SQLite"
It writes a # Decision: record to docs/decisions/ (idempotent by slug) and merges a
provenance: captured node into the graph, immediately queryable via why/explain/list-intent --provenance captured. The node is built through the same ingest path a committed record takes, so
re-ingesting the record reproduces the identical node. If --recommendation differs from
--chosen, the node records a resolution_delta capturing where the human overrode the agent.
--since last-run needs at least two pipeline runs: the first run has no prior snapshot to diff
against, so it writes none and digest --since last-run exits 2 with a clear message until a
second run has happened.
Drift and affirmation
Recorded decisions rot silently when the code they explain changes underneath them. affirm stamps
a decision as reviewed against the current code; drift flags any affirmed decision whose anchored
code has changed since, so staleness is visible instead of silent.
whyfile affirm <decision> # "I reviewed this against the code; it still holds"
whyfile drift --format text # decisions whose anchored code changed since affirming
affirm records a content fingerprint of the anchored code (symbol-level where the anchor names a
function or class, file-level otherwise) into intent-affirmations.json, a small human-owned ledger
you commit. drift recomputes those fingerprints and reports four buckets: needs-reaffirmation
(affirmed, but the code moved), expired-assumptions (see below), never-affirmed (anchored, staleness
unknown), and orphaned (a ledger entry for a decision no longer in the graph). It is deterministic,
LLM-free, and informational (exit 0); re-affirming a flagged decision clears it.
Assumptions are the other decay mechanism: most stale intent is stale because an assumption quietly
expired. A record's ## Assumptions section (see docs/adr/TEMPLATE.md) is ingested into authored
assumption intent nodes, each with an optional review-by date:
whyfile list-intent --kind assumption # the facts your decisions bet on
whyfile drift --as-of 2026-12-31 # what is overdue for review by that date
An assumption past its review-by date surfaces in drift as an expired assumption, prompting you to
revisit the decision that rests on it. --as-of defaults to today; pass a future date to see what
will be overdue by then. Because assumptions are first-class intent nodes, explain <file> and
why "..." also answer "which assumption governs this path?" during incident triage.
Trust lifecycle
Trust is a lifecycle, not a label. trust resolves each intent node's current state: it rises
through provisional (reconstructed, unverified), corroborated, confirmed (survived an audit),
and affirmed (a human signed off), and falls to stale (affirmed but the code drifted since),
disputed, superseded (a newer record replaced it), or orphaned (its anchored code was
deleted).
whyfile trust --format text # every node's state, counted by state
whyfile trust <node> --set confirmed --by panel # record a court verdict (who/what/when)
States are derived deterministically from the graph plus the affirmation ledger plus a recorded
verdict log (intent-trust.json), so they are stable across re-runs. Structural facts (superseded,
orphaned) win over promotions, and a human affirm outranks a panel verdict. Recording a verdict
appends to the log with the actor and timestamp; it never edits the graph. Superseded and orphaned
states are detected automatically from the graph; confirmed and disputed come from recorded verdicts
today, and from the audit panel once it lands.
Ongoing cost per PR
Updating the intent layer on a PR is cheap: cents on an API backend (a ~$0.03 floor), or a small token draw against your plan's usage limits on the subscription backend, no separate bill either way. A per-PR update re-anchors existing intent plus any changed doc sections; the Pass A cache means unchanged sections cost nothing on either backend. See What a run costs for the full breakdown.
Two CI postures
| Posture | CI cost | Needs a secret? | Freshness |
|---|---|---|---|
| Regenerate in CI | ~cents/PR | Yes (API key) | Always current |
| Committed sidecar | $0 | No | Refreshed by a separate job |
Example workflows for both live in examples/workflows/.
Roadmap
Cached Pass B anchoring (to make per-PR updates truly diff-proportional) and --since pipeline
scoping are planned.
MCP server
The same read-only query layer is available to IDEs and coding agents over the Model Context Protocol, so the why is one tool call away without shelling out. Install the extra and start a stdio server:
pip install 'whyfile[mcp]'
whyfile --mcp --graph graphify-out/graph.json
Register it with a client (Claude Code, Cursor, IDE agents). For Claude Code, add to .mcp.json
at the project root (launch the server from the project root so a --base-style git diff resolves
against your repo):
{
"mcpServers": {
"whyfile": {
"command": "whyfile",
"args": ["--mcp", "--graph", "graphify-out/graph.json"]
}
}
}
Seven intent_* tools are exposed: six read-only query tools plus one explicit write tool, each a
thin wrapper over the CLI command of the same name, so a tool result is identical to its CLI
counterpart's JSON:
| Tool | Inputs | What it does |
|---|---|---|
intent_explain |
node |
Why is this node like this: the intent that motivates a code/doc node, or an intent node's claim, rationale, alternatives, and relations. |
intent_list |
kind?, min_confidence?, provenance? |
The intent inventory, optionally filtered. |
intent_why |
question, top? |
Ranked intent matching a natural-language question. |
intent_changed |
files[] or base |
The intent governing changed files, constraints first. |
intent_coverage |
(none) | Which code has recorded intent and which is dark. |
intent_digest |
since |
What intent was added, removed, or superseded since a checkpoint. |
intent_capture (write) |
chosen, rationale, question?, options?, recommendation? |
Persist a resolved decision as a record + a captured node, without shelling out. |
The six query tools are read-only, LLM-free, and free at query time (they read the local graph,
make no backend call). intent_capture is the one write tool (B5): it is explicit (a distinctly
named tool, never a mode on a read tool), schema-validated, and scoped to capturing a decision, so
the read-only guarantee of the others is unchanged. Results carry both a JSON body and MCP
structuredContent; a real failure sets
isError, while a missing intent layer returns a graceful no_intent_layer result rather than an
error. The server only reads a local graph: no mutations, no pipeline runs, no secrets, safe to
hand to any agent. See ADR-0018 for the design.
How it works
Pass A, intent extraction. Each markdown document is split into heading-bounded sections. Per section, the LLM extracts 0-N intent units (a decision, mechanism, constraint, or tradeoff), each with a one-sentence claim, the rationale (the why), any alternatives considered, and a deterministic ID grounded to the section's character span.
Pass B, anchoring. Each intent node is linked to the graph concept(s) it explains via a rationale_for edge. Candidate graph nodes are found without embeddings (same source_file plus lexical overlap), then the LLM adjudicates which the intent actually explains. Nodes are adjudicated in small batches (several per call) for throughput, each keeping its own candidate list so batching does not blur anchoring (ADR-0019).
Pass C, cross-doc intent. The intent nodes are related in bounded chunks (one LLM call per chunk) to surface intent-level relationships between them: supersedes, trade_off_against, constrains, motivated_by, contradicts. Edges below --min-confidence are discarded. Chunking keeps each call bounded; relationships between nodes in different chunks are not surfaced (ADR-0019).
Pass D, cross-doc concept resolution (opt-in). Enable with --passes A,B,C,D.
Pass D unifies the graphify concepts the intent layer touches non-destructively,
emitting same_concept edges between existing concept nodes (nothing is merged or
rewritten). Cross-document intent linkage then emerges transitively:
intent -[rationale_for]-> JWT -[same_concept]- token <-[rationale_for]- intent. Candidate
pairs are cross-file only, blocked by embedding cosine when a local model is
available (optional [embeddings] extra) or lexical Jaccard otherwise, then the LLM
adjudicates each pair; edges below --min-confidence are dropped. Every edge records its
method (embedding/lexical) and similarity for provenance.
Idempotency
Re-running whyfile is safe. Intent nodes already present in the sidecar (.whyfile.json) are skipped by ID, and edges are skipped by (source, target, relation) key, so a re-run adds only genuinely new intent. You can also run passes separately (e.g. --passes A then --passes B,C): when A is not in the run, later passes seed from the existing sidecar.
Development
git clone <repo>
cd whyfile
pip install -e ".[dev]"
python -m pytest tests/ -v
Tests cover every module: IDs, section splitting with spans, relation/confidence validation, all four passes (including Pass D's candidate resolution and embedding fallback), merge/enriched-graph assembly, the report, backend resolution, and an end-to-end smoke test. The LLM boundary and the graphify-runtime probe are injected/mocked, so the suite makes no network calls and needs no API key or live backend.
graphify runtime dependency
Every backend routes through graphify's graphify.llm, so whyfile declares graphifyy as a runtime dependency; pip install whyfile installs it (ADR-0010). For an API backend, add the provider extra so graphify's SDK is present:
pip install 'whyfile[anthropic]' # --backend api with Anthropic
pip install 'whyfile[gemini]' # --backend api with Gemini
The default subscription backend needs no provider SDK, only the Claude Code CLI (claude), installed and signed in. If the graphify runtime is somehow missing, whyfile exits immediately with a clear install message rather than failing deep inside a run.
Building the plane while we fly it
Structure tools map imports and call graphs: the what and the how-it-connects. What they
can't hand you is the reasoning: which decision a file is the consequence of, what got tried and
thrown away, why the ugly workaround earns its keep. whyfile is a thin reasoning layer
over the structural graph graphify already builds: a decision/strategy view, not another entity
extractor, and deliberately not a vector DB or a RAG stack (that would be a much heavier
machine than the job needs).
The See it on this repo demo up top wasn't a one-off. This whole
docs/adr/ trail is fair game (decisions, rationale, and the alternatives
considered), except we wrote them by hand as we built it, so you can watch the reasoning
actually move:
- ADR-0002 kills the predecessor's generic "concept extraction" and bets the whole tool on intent: the one niche the graph constructors leave empty.
- ADR-0004 ships v1 deliberately embedding-free and defers cross-document resolution… which ADR-0009 then delivers, its design shaped by reading graphify's own code and discovering its dedup engine couldn't do the job.
- ADR-0010 is the fun one: it openly supersedes an earlier README claim after a clean install exposed a silently broken package. The old rationale, in its own words, "does not survive contact with the code." ADR-0011 then catches extraction quietly running on Opus ("overkill," per the code's own comment) and puts it on a budget.
That last beat, a decision that doesn't survive contact with the code, recurs across three ADRs, and it's the whole reason the tool exists: it's the reasoning that lives in a maintainer's head and evaporates the day they leave. We're building the plane while flying it; the flight recorder is checked in right below.
Architecture decisions
Design rationale is recorded as ADRs in docs/adr/:
| ADR | Decision |
|---|---|
| 0001 | Direct, schema-enforced LLM calls |
| 0002 | Intent/rationale as the product wedge + node data model |
| 0003 | Span-level grounding & provenance |
| 0004 | Embedding-free, same-file anchoring (v1) |
| 0005 | Extend graphify's relation vocabulary deliberately |
| 0006 | Enriched-graph assembly on the node-link format |
| 0007 | Determinism, idempotency, and a single confidence policy |
| 0008 | Subscription-first LLM backend via the Claude CLI |
| 0009 | Cross-document concept resolution (Pass D) |
| 0010 | graphify (graphifyy) is a hard runtime dependency |
| 0011 | Cheaper, safer intent passes (model tiering, gate, estimate, breaker) |
| 0012 | Intent-layer progress observability |
| 0013 | Fail-closed automation surface and first-hour DX |
| 0014 | Whole-run token ceiling + calibrated ETA |
| 0015 | Per-section Pass A cache + resume |
| 0016 | Intent query layer (explain/list-intent/why) |
| 0017 | Team collaboration bundle (changed/coverage/digest + PR check) |
| 0018 | MCP server (--mcp) exposing the query layer to IDEs and agents |
| 0019 | Pipeline throughput (batched Pass B, chunked Pass C, serial-aware estimate) + honest token-based cost reporting |
| 0020 | Adopt the decision-layer arc (capture, ground, audit) |
License and attribution
whyfile is licensed under the MIT License, Copyright (c) 2026 Will Neill.
This project is an independent post-processor built to interoperate with
graphify by Safi Shamsi. It reuses graphify's
graph schema and relation vocabulary and calls graphify as a separately-installed runtime
dependency; no graphify source code is bundled with or distributed as part of this project.
graphify is licensed under the MIT License (Copyright (c) 2026 Safi Shamsi); see the
ACKNOWLEDGEMENT AND ATTRIBUTION section of this repository's LICENSE file and
the upstream license for the
full text. With thanks to the graphify project.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file whyfile-0.3.1.tar.gz.
File metadata
- Download URL: whyfile-0.3.1.tar.gz
- Upload date:
- Size: 248.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8d2093829107dd79060aa384d6f5abe55728fa94d5ea72b997accb12b27784ef
|
|
| MD5 |
22af4fc4558f45d1490168642b4c2161
|
|
| BLAKE2b-256 |
9128399f5531eb84524644fb7c4e676f9c87f3eb78ea03b9eb0b1613fd26da6b
|
Provenance
The following attestation bundles were made for whyfile-0.3.1.tar.gz:
Publisher:
publish.yml on whyfile/whyfile
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whyfile-0.3.1.tar.gz -
Subject digest:
8d2093829107dd79060aa384d6f5abe55728fa94d5ea72b997accb12b27784ef - Sigstore transparency entry: 2133553899
- Sigstore integration time:
-
Permalink:
whyfile/whyfile@7f2ad664d044fe144a9ed4bc995fd84b3940674d -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/whyfile
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7f2ad664d044fe144a9ed4bc995fd84b3940674d -
Trigger Event:
release
-
Statement type:
File details
Details for the file whyfile-0.3.1-py3-none-any.whl.
File metadata
- Download URL: whyfile-0.3.1-py3-none-any.whl
- Upload date:
- Size: 107.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ebcb3e02313a5a4967057d9bd28ad5d85b17e401ea2ceedbadc4807f456b39c5
|
|
| MD5 |
3509347dfc0d0682f77ebc9aad8194b5
|
|
| BLAKE2b-256 |
a0935e12a29df6a2f7f0188bd6d4c1d1bb424c2f57a75c425a9bf72a3f689510
|
Provenance
The following attestation bundles were made for whyfile-0.3.1-py3-none-any.whl:
Publisher:
publish.yml on whyfile/whyfile
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
whyfile-0.3.1-py3-none-any.whl -
Subject digest:
ebcb3e02313a5a4967057d9bd28ad5d85b17e401ea2ceedbadc4807f456b39c5 - Sigstore transparency entry: 2133553919
- Sigstore integration time:
-
Permalink:
whyfile/whyfile@7f2ad664d044fe144a9ed4bc995fd84b3940674d -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/whyfile
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7f2ad664d044fe144a9ed4bc995fd84b3940674d -
Trigger Event:
release
-
Statement type: