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 any answer status
(ok/weak_match/topically_weak/no_strong_match — the latter three are confidence
flags, not errors), 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.
list-intent [--kind decision|mechanism|constraint|tradeoff] [--min-confidence F] returns the
intent inventory, optionally filtered by kind and/or a minimum confidence_score.
why "<question>" ranks intent nodes against free text with rapidfuzz token matching over each node’s label, claim, rationale, and alternatives (a stopword-stripped variant of the question is scored too, so filler words don’t dilute the match) 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" (or "weak_match" when it stands
clearly apart from the runner-up) so a caller can tell the match was weak. Above the cutoff,
each result carries its evidence — matched_terms and distinctive_matches (#81) — and a
top hit with at most one distinctive match covering half or less of the question’s content
terms downgrades to status: "topically_weak" (each result carries matched_coverage) —
and so does a single matched term with no distinctive evidence at any coverage, so a lone
corpus-ubiquitous term still downgrades: a topical accident, not an answer — even a
corpus-rare lone match can’t carry a question whose real subject matched nothing (#103). Calibration rule of thumb: scores 60–80 with no
corroborating distinctive terms, or with low content coverage, are weak evidence.
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. For the commands that DO need the
enriched graph (explain, changed, coverage), run whyfile sync first — it builds a
deterministic enriched graph from your records and attested intent in seconds, zero LLM calls. The pipeline itself is
unchanged: running whyfile with no subcommand still runs the extract/anchor/relate
run described above.