> ## Documentation Index
> Fetch the complete documentation index at: https://docs.whyfile.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# 0029 whyfile artifact

# ADR-0029: The Whyfile artifact — ship the repo's own contract

**Status:** Accepted (naming amended by ADR-0031)
**Date:** 2026-07-12

## Context

ADR-0026 (C1) named the convergence thresholds a value, not a constant, and routed the
user-facing control to "the Whyfile artifact and the C3 CLI, which build a policy from config and
pass it in" — but shipped no artifact, only the pure function. C3 (ADR-0028) wired convergence
into `audit`'s gate and report, but `cli.py:923` still called `panel.converge_report` with
`policy=panel.DEFAULT_CONVERGENCE` hard-coded; there was no way for a repo to tune it without
editing source. Tasks 1-5 of #8 built the artifact ADR-0026 promised: a loader (`config.load`), a
precedence seam (`config.resolve_sources`), and four call sites that read it. `panel.py`'s docstring
briefly cited "#15" for this ticket during those tasks (commit `9839935`); the correct ticket is
\#8, and the reference was fixed in the same commit that wired the policy through.

This task is the last piece: ship this repo's own `Whyfile` at the root, set to today's defaults,
so the contract is not just built but adopted — the tool practices what it audits. It also writes
this record, since the artifact and its config schema are exactly the kind of decision ADR-0021
says should be captured, not left implicit in code.

## Decision

**The artifact is a bare, capitalized `Whyfile` at repo root.** The name is the brand (ADR-0024:
"Whyfile names the artifact the paradigm produces"), not a generic `.whyfile.json` or `config.json`
— a repo adopting the tool gets a single, recognizable file to look for. Its contents are strictly
config, policy, and pointers: `_about` (free text), `intent_sources` (`docs`, `records` — where
intent lives), and `convergence` (`structural`, `sharpen`, `note` — the audit panel's dispute
threshold). It is never a copy of graph or sidecar content (design law 3) — the graph is
regenerated by `graphify update`, not duplicated into a hand-edited file that could drift from it.

**JSON, not TOML.** Every existing config file in this codebase (`intent-rules.json`, the graph
sidecar, the trust log, the affirmation ledger) is JSON; a second serialization format would be a
second parser and a second set of failure modes for no reader benefit. The project targets Python
`>=3.10`, so `tomllib` (3.11+) is not available without a third-party dependency, and Whyfile ships
zero new dependencies. The brand lives in the filename, not the format — nothing about `Whyfile`
implies TOML the way, say, `pyproject.toml` does.

**Loader and precedence.** `config.load(root)` reads `<root>/Whyfile`; a missing file returns
`Whyfile()` — every field unset, `present=False` — so a repo with no Whyfile is byte-for-byte
identical to before this ticket on every code path. `resolve_sources(docs_arg, records_arg, cfg)`
applies explicit-flag > Whyfile > hardcoded-default precedence. `records` is the one field that
stays `None` rather than being defaulted to a concrete list when unset by both the flag and the
Whyfile: `_resolve_record_dirs` derives it from `docs` (skip-if-absent) today, and injecting a
concrete default here would short-circuit that derivation and silently change behavior for every
repo that has never heard of a Whyfile. The unset sentinel is what makes "no Whyfile present"
provably a no-op.

**Fail loud on a malformed contract.** `config.load` raises `QueryInputError` on invalid JSON, a
non-object top level, an unknown top-level key, an unknown `intent_sources` key, wrong-typed
fields, or convergence thresholds that violate `structural >= sharpen >= note >= 1` — the same
posture `_load_rules` takes on `intent-rules.json`. A committed contract that is silently ignored
because of a typo is the exact dishonesty this tool exists to catch elsewhere; the loader does not
get a pass on its own honesty.

**Four consumers, plus the doctor line and the checked-in file itself.** Per design law 1 (build
only for a real consumer), the artifact ships wired to four call sites, all landing in `cli.py`:
the audit convergence policy (`panel.converge_report(report["nodes"], policy=cfg.convergence)`,
replacing the hard-coded `DEFAULT_CONVERGENCE`); the reconstruct/`--doctor` docs and records
defaults (`config.resolve_sources` ahead of every reader); the conformance-gate's citation of the
contract's address in both text and markdown output ("Contract: this repo's Whyfile"); and the
audit-gate's citation of the convergence policy in its unresolved-structural output ("Policy:
Whyfile convergence"). `_whyfile_doctor_line` adds a fifth, advisory-only surface: `whyfile --doctor` reports whether a Whyfile is present and, if so, its resolved convergence and docs path —
the diagnostic a human runs to confirm the contract is actually being read. Finally, this repo's own
checked-in `Whyfile` (this ticket) is itself part of the wiring: it is the human adoption signal,
proof the loader works against a real repo, and the reason every existing audit test run from repo
cwd now exercises the present-and-3/2/1 path instead of the absent path.

## Assumptions

* We assume today's defaults (convergence 3/2/1, docs `docs/`, records `["docs/adr"]`) remain this
  repo's actual policy; the checked-in Whyfile is the file to edit, not this ADR, when that changes
  (review by 2026-10-01, alongside ADR-0028's roster-stability review).
* We assume no repo has an existing file literally named `Whyfile` for an unrelated purpose that
  this loader would now shadow (expires: if a collision is reported, the loader needs a more
  specific filename or a `--no-whyfile` escape hatch).

## Alternatives considered

1. **TOML with a `tomli`/`tomllib`-gated dependency.** Rejected: every other config file here is
   JSON, the project floor is 3.10 (below stdlib `tomllib`), and a human-friendliness argument for
   TOML does not outweigh adding a dependency and a second parse path for one file.
2. **A single god-config file merging Whyfile's contract with `intent-rules.json`'s compiled
   constraints.** Rejected: the two serve different audiences and lifecycles — `intent-rules.json`
   is compiled/generated policy for `whyfile check`, `Whyfile` is a small hand-edited contract a
   human reads and tunes. Merging them would make the common case (tune one convergence threshold)
   require touching a file also used for something unrelated.
3. **Have the Whyfile carry a copy of graph or sidecar summary data (e.g. cached golden-fraction,
   node counts) for faster doctor output.** Rejected by design law 3: any such copy would drift from
   the regenerated graph the moment either changed, and "which one is stale" is a question this
   tool should never let a user ask about its own contract file.
4. **Ship all deferred fields (below) now, to avoid a second schema-touching ticket later.**
   Rejected: each deferred field has no consumer yet, and adding a key with nothing reading it
   violates design law 1 and gives `config.load`'s fail-loud unknown-key check a growing set of
   fields to special-case with no behavioral payoff.

## Consequences

* A repo can now tune the panel's dispute strictness (and, incidentally, where its docs/records
  live) by editing one committed JSON file instead of a code change — the control ADR-0026 promised
  two ADRs ago now exists.
* `panel.converge_report`'s policy argument is no longer a hard-coded constant at `cli.py:923`; it
  is threaded from `config.load()` on every `audit` invocation, so the ticket most literally closes
  the seam ADR-0026 opened.
* The `records` unset-sentinel design means this ticket is safe to land with zero behavior change
  for every repo (including this one, since 3/2/1 is the existing default) — the full test suite
  passes unchanged with the dogfood `Whyfile` now present at cwd for every audit test that resolves
  it.
* Three fields are deliberately out of scope and deferred until a consumer exists:
  `attested_trailers` (no writer yet); the **conformance-gate trust tier** — a policy knob for how
  much authority a superseding decision record needs to clear `whyfile check`'s gate, which is
  distinct from the audit convergence `structural` knob this ticket does wire (the two gates answer
  different questions: "has a human looked at this dispute" versus "does this PR conform to a
  recorded constraint"); and golden-fraction / drift configuration. Golden fraction has a genuine
  hard-coded policy today (`provenance.GOLDEN_TIERS`); drift has no time-based reaffirmation-window
  concept to extract at all — `needs_reaffirmation` is a pure content-fingerprint comparison
  (`drift.compute_fingerprint`), so that deferred field awaits both a consumer and the underlying
  concept, not just a reader for an existing constant. Each awaits the ticket that gives it a
  reader, per the same design-law-1 discipline that shaped this one.
