Skip to content

Latest commit

 

History

History
173 lines (151 loc) · 47.7 KB

CONTEXT.md

File metadata and controls

173 lines (151 loc) · 47.7 KB

CONTEXT — domain language

The glossary for this project. Use these names in code, tests, docs, and conversation; when a term here is sharpened, sharpen it here first. Architecture vocabulary (module, interface, depth, seam, adapter, leverage, locality) is not domain language and does not belong in this file.

Recorded decisions live in docs/adr/. Do not re-litigate them without the human; if a decision is genuinely wrong, write a new ADR that supersedes it.

The protocol

Term Meaning
phase 1 — research Closing every blocking unknown with fetched primary sources, then passing the gate. Its deliverable is a briefing, never product code.
phase 2 — build Implementation from the brief, by a different agent or a person. Starts only after the gate passes.
the builder Whoever owns phase 2. Should not need to re-research anything; if they do, phase 1 was incomplete.
collector machine The machine that holds the Firecrawl key and produces the corpus: decompose and research run here, doctor FAILs when the key is missing. The default role — an install that never declares one is a collector. See ADR-0010.
builder machine The machine that builds from the corpus the collector pushed, with no Firecrawl key and no Firecrawl egress. It must not collect: the collection CLIs refuse (exit 2) rather than fall back on another transport, and doctor reports a missing credential as informational instead of a blocker. See ADR-0010, ADR-0011.
handoff The corpus crossing from the collector machine to the builder machine through git — the seam the two-machine model rests on. Verified on arrival by bin/handoff.mjs and by doctor on a builder. The remedy depends on the cause and handoffRemedy(report) picks it: something that did not travel lives on the collector (push research/raw/ including its dotfiles), a corpus that travelled whole and was rewritten on checkout here lives on this machine (line-ending rewrite) — and one blanket text for both is what used to send operators to re-collect, spending credits to reproduce what was already on disk. See ADR-0011, ADR-0020.
research-first The rule that findable facts are collection tasks, not questions. The name of the skill that carries it.
skill set The skills the kit ships beside research-first, one directory each under fetchproof/skills/, deployed to every skill root (ADR-0146). Guidance, never enforcement.
skill router The skill-router skill: picks which skills may run from the machine role and the gate state, then by task. Never routes a builder to a skill that collects, and never picks a model.
desktop panel bin/panel.mjs: a page on 127.0.0.1 over doctor, handoff, preflight and install, plus the search key and the builder hand-off text. A shell over existing commands; nothing on the page collects, only auto-collect does (ADR-0147, ADR-0148).
builder request research/requests/<id>.json: one external fact a builder needs, the owner page or queries, and why it blocks. It is the builder-written part of the collector's Git corpus, answered by <id>.result.json. collected delivers captures; the builder reads/judges them and reports review-only gaps in prose, while the collector records Findings, closures, map and the current authored brief. Local review/re-packaging of a received package remains available without collection (ADR-0052, ADR-0148, ADR-0150).
auto-collect The desktop panel's mode in which the collector pulls builder requests, validates them, collects each with the same research.mjs within a per-request and a daily page cap, and pushes the corpus with its ledger. It runs only on a collector, and it pauses, never falls back, when the credits run out (ADR-0148). A private received child report supplies page accounting and exhaustion; human diagnostics do not. An unmeasured attempt reserves its remaining admitted allowance against the daily cap and pauses with actual spend unknown. Earlier measured continuation attempts stay counted once. The existing numeric meter is therefore labeled pages used or reserved (ADR-0149). Its Git inspection must be complete before it authorizes collection or delivery; a truncated answer is refused, and a partial result retains collector trust across that refusal so a restart does not collect it again (ADR-0151). Diagnostic text is scrubbed with the existing redactor before result/report persistence and when inherited status is presented; reading old trusted results preserves their bytes and hashes. Before the child starts, its newly admitted allowance and an in-flight pause are saved atomically in existing fields. Collector termination leaves that charge/pause for restart inspection; a valid report refunds unused allowance once. A previous-day allowance is never refunded against today's other spend; cross-day completion charging is conservative, not a dated vendor bill (ADR-0152).
fact request What a builder writes when the brief lacks a fact: the unknown, why it blocks, the page that owns it - for the collector to add to plan.json. A builder never fetches it itself (ADR-0010).

The standing protocol

Term Meaning
standing protocol The five contribution rules every project built on this kit carries, in AGENTS.md and in the scaffold's template/AGENTS.md so every scaffolded project inherits them: the architecture map current in the same commit; the five-part commit report with the got-wrong line (one line per part for a commit outside the declared code paths); an ADR for any design choice with a rejected alternative; one commit per revertable task; a red suite stops work, reported immediately and alone with the cwd. Documentation, not enforcement, with two exceptions that judge state rather than prose: rule 1 (the map was staged) everywhere, and rule 5 in the kit's own checkout (the suite rule, ADR-0120).
commit report The five named parts a commit carries: what changed / why / what it touched / what you verified / what you got wrong and fixed. The parts are named so nobody has to infer them — someone who has never seen the brief that produced the work must still be able to read the report. The got-wrong line is not optional: if nothing was gotten wrong it is written as "nothing to report", never omitted, because a report that never admits a mistake cannot be distinguished from one that never checked. Each verification claim carries one of three status words - verified (run, result seen), untested (written, not run), expected (reasoned, not run) - and a mistake is reported in six fields - mistake, where, impact, cause, fix, verified - both taken from Anthropic's careful-coding skill on 2026-10-03, the two devices of that discipline the protocol had not carried.
revertable task The unit a commit is measured by: a change that can be reverted alone — git revert <sha> undoes it without breaking what was built before or after it. A task that cannot be reverted alone was not a task; it is two tasks wearing one commit, and it gets split. The same-commit map update and the CONTEXT.md term a commit owes belong with it, which is why they do not break the test.
resume Arriving mid-task and orienting from repository state instead of starting over: doctor and preflight answer where this project is from disk alone, the last commit report and docs/ARCHITECTURE.md say what was happening and why, and the working tree's uncommitted changes are the interrupted task — possibly partial, never trusted as state. Resume rather than restart: the corpus is already collected and spent credits are not spent again. Documented in AGENTS.md and the scaffold template, deliberately unenforced — whether an agent oriented properly is not something a gate can judge. See ADR-0013.
wrong project Arriving in a cwd that is not the project the operator means — the kit's own repo, a parent folder, an unrelated checkout. The project is always the cwd, and the kit takes no project argument and never will, so the answer is to ask which project: never a filesystem search, never an inference from a name, never a remote lookup. Signals: no AGENTS.md, no research/, or a corpus plainly about something else. An existing corpus about another topic is the signal working, not an obstacle to write over. Documented in AGENTS.md and the scaffold template, deliberately unenforced — a gate cannot judge whether an agent guessed or was told. See ADR-0017.
stop-the-line The red-suite rule: a failing test stops work and is reported immediately and alone, with the cwd recorded (some tests are cwd-sensitive, and a red without a cwd is a failure that cannot be localised), before any further building. Work resumes when the red is explained — fixed, or pinned as a recorded defect with the reason — never when it is averaged into a summary. The rule is only as strong as the runner that reports the red, which is why the harness awaits every test: an async test that failed used to print ok and let the suite exit 0, so there was no red to stop for (ADR-0021).

The contract

Term Meaning
Discovery Contract research/DISCOVERY.md: the stated definition of enough information to build. Holds the build intent and the blocking unknowns.
build intent The ## Build intent section: what is being built, for whom, and what "done" means.
blocking unknown A fact where a wrong guess changes the design — limits, pricing, auth model, schemas, licensing, platform behaviour, current versions, data availability. Row U-## in the contract.
CLOSED A blocking unknown proven by an E-## evidence row.
KNOWN-UNKNOWN A blocking unreachable now (login-walled, paywalled, private). Allowed — but its Evidence cell must name the day-one verification step. An unlabelled gap is the failure mode this project exists to prevent.
question budget At most three questions, once, up front, and only about intent. A question whose answer is in public documentation is a research task.

The evidence

Term Meaning
research corpus Everything under research/ read as one body of state: contract, evidence rows, sources, raw captures, fetch ledger, overrides, diagnostics. Owned by fetchproof/lib/corpus.mjs — readCorpus(root) parses; a caller that needs the chain verdict adds verifyLedger(root, { corpus }) from provenance.mjs (the loadCorpus wrapper nobody called was removed, #297). See ADR-0003.
cache decision The freshness judgement over the capture index: is this URL's raw capture fresh enough to reuse? Owned by cacheDecision() in fetchproof/lib/corpus.mjs — one predicate, one owner; the collector, the run coordinator, and phase 0 all consume it, and a cache hit never spends scrape budget. Search-found candidates use that same decision before selection; same-run deduplication does not exclude a stale or forced capture before freshness is judged.
capture index The byUrl map readCaptures(root) builds: one entry per URL, the newest retrieval winning. Owned by fetchproof/lib/corpus.mjs, like the format it comes from: captureEntry() is the one constructor — the reader and the collector both build through it, so the entry a run holds in memory is the entry a later read reproduces, field for field — and rememberCapture() is the one writer, for a URL collected mid-run. cacheDecision consumes it; callers that have just collected store the entry collectOne returned rather than shaping one.
corpus problem Something the reader could not parse cleanly: a row whose cell count does not match its header (table-arity), a cell pointing at a capture that is not on disk (raw-dangling), a capture with no URL, a plan that will not parse. The corpus reports problems; it never decides whether one blocks.
trace The join between an evidence row and its fetch: the scrape entry in the ledger that traces it and the raw capture behind it. Owned by fetchproof/lib/corpus.mjs — traceOf(corpus, row), with captureOf(corpus, row) for the capture half — because which pointers a row has is format knowledge, including the clause that an empty Raw cell matches on the URL alone. Consumers read the trace instead of searching the ledger and the capture list themselves. See ADR-0015.
evidence row One E-## line in research/EVIDENCE.md: retrieval date, source type, URL, finding, and the raw capture behind it.
finding The claim cell of an evidence row. Arrives as an auto-extracted summary; the agent must rewrite it into a real claim before the gate. The words it arrives with are chosen by fetchproof/lib/finding.mjs — firstFinding(markdown, fallback), page markdown in and one line of prose out — which the collector consumes to fill the cell (ADR-0016; the extractor used to live inside lib/collect.mjs). Which finding an unknown rests on is the corpus's convention, not each renderer's: claimOf(corpus, unknown) returns the first cited row's finding, falling back to the unknown's own text, so the brief and the audit name the same claim.
raw capture A fetched page cached under research/raw/ with front-matter (url, retrieved, command, statusCode). What makes a claim checkable.
fetch ledger research/raw/.fetches.jsonl: the hash-chained record written only by the collector. Proves a page was fetched rather than typed.
chain The ledger's linked hashes. Tamper-evidence, not truth: it proves collection happened, not that a page says what a claim asserts.
line-ending rewrite A capture whose recorded body hash no longer recomputes only because git rewrote CRLF into it on checkout — core.autocrlf=true, a default Windows install, smudges every text file leaving the object store, and the hashes were taken over LF bytes. The corpus arrived whole; this machine changed it on the way out. Proved, not guessed: isLineEndingRewrite() in lib/provenance.mjs folds CRLF back to LF and re-hashes, which either reproduces the recorded hash or does not — so a genuinely tampered capture still reports kind: 'modified' and gets its own remedy (alteredRemedy, 2026-10-04): restore the capture from the committed copy when the change is local to this checkout, else the collector restores or re-collects it - never the push remedy, which would send the same bytes again. Prevented at both ends by .gitattributes (research/raw/* text eol=lf, *.jsonl text eol=lf), which ships in this repository and in fetchproof/template/, so a scaffolded project cannot be born without it; that file outranks a reader's core.autocrlf. It is a LAYOUT entry but not a gate marker: a project without it still gates, it just breaks on a Windows builder. See ADR-0020.
exclusive section The lock every writer of the fetch chain holds while it writes — withLock(root, fn) in fetchproof/lib/provenance.mjs. Acquired with O_EXCL (flag: 'wx'), so the kernel refuses a second creator instead of letting it overwrite: exclusivity is a syscall's answer, not a check the module performs and can lose between performing it and acting on it. Reentrant in-process, so appendFetch serializes its own read-modify-write (seq, prev derived from the ledger as read under the lock) inside a run that already holds it; release removes only the acquisition it made. Two collectors arriving at once is routine — an agent run and an operator run, cron plus interactive — and the duplicate seq and broken prev they used to produce were unrecoverable: repairLedgerTail no-ops on a chain broken in the middle and rebuildLedger refuses it, so the corpus was dead and the credits were spent twice. See ADR-0020.
source type P primary/official — the page that owns the fact, and the only kind that carries a design. S secondary — context. L lead-only — a hint, never proof.
contradiction Two sources disagreeing. Recorded, never averaged: say which you trust and why.
audit bundle research/audits/<slug>-v<version>-<date>.zip: one topic's latest main audit plus every subtopic audit of that version, in one attachment. Packaging, never a new artifact: the file list comes from the manifest's latest pointers rather than a directory listing, nothing is rendered, no version is bumped. Refuses rather than guesses — no audits, several unnamed topics, or a manifest naming a file that is not on disk — and an archive that is quietly incomplete is treated as worse than none. The container is lib/archive.mjs's; what goes in it is lib/audit.mjs's. See ADR-0019.
brief research/BRIEF.md: the phase-1 → phase-2 handoff, and the only file phase 2 is required to read.
brief shape What a brief is: the six sections, the two the corpus cannot fill (Contradictions, Decision), and how they are marked. Owned by fetchproof/lib/brief.mjs — the renderer writes the headings from it, lib/audit.mjs reads the judged sections through it, and the scaffold ships the marker that says this is still the scaffold. briefState(text) answers template | legacy | draft | authored; only the first two are safe to draft over, and legacy (a scaffold written before the marker shipped, which the text cannot tell from a brief someone wrote in) refuses loudly rather than guessing. See ADR-0014.
audit The immutable, copy-pasteable snapshot of the corpus under research/audits/ — one full-corpus file plus one per COVERED/GAP subtopic — and the manifest (index.json) that records which versions exist. A derived artifact: it reads the corpus and never writes back to it, and it renders only when the gate passes. fetchproof/lib/audit.mjs owns the family, including the manifest's reader (listVersions, resolveVersion); bin/audit.mjs parses flags and prints.
timeline research/TIMELINE.md: the derived chronological review aid — evidence, provenance and diagnostic state transitions. Deliberately outside the chain and never part of a gate decision; owned by fetchproof/lib/timeline.mjs, which also writes the diagnostics log the gate records through.
collect / collector Turning a URL into a raw capture plus its ledger entry and rows. fetchproof/bin/research.mjs is its CLI, and it runs on a collector machine: a builder refuses it by role rather than collecting through some other transport.
search session How a run puts a query to its search providers, owned once for both coordinators (fetchproof/lib/search-session.mjs, ADR-0135): every provider under a merge, interleaved by rank and attributed; otherwise the one provider, with one reported degrade to the fetch provider's own search, unless the coordinator passes searchExplicit (a provider named by flag, environment or machine config, #317). It keeps the search meters (searchesUsed, searchesOn, searchCreditsEstimate, searchFailures, searchFailuresOn, degraded) under one rule - every ask counts what the provider reports, every failed ask is a failure - hands one failure row per failed ask to the coordinator's sink, and refuses a provider that reports notReady before anything is spent. What a coordinator makes of an answer is not the session's.

The gate

Term Meaning
gated project At the repository's top level, a project holding any one gate marker. Below the top level, a folder holding research/DISCOVERY.md and either research/raw/ or a contract new-project rendered (ADR-0182). A commit that removes the evidence HEAD holds for a project is judged as one even with no marker left on disk, unless the project is below the top level and keeps no tracked path - deleted or moved whole (ADR-0184).
gate marker One of exactly four artifacts: research/DISCOVERY.md, research/plan.json, research/EVIDENCE.md, research/raw/. Declared once, in fetchproof/lib/scaffold.mjs; the predicate derives from it.
gate verdict The single answer to is this project gated, and does it pass? — evaluate() in fetchproof/lib/gate.mjs, consumed by all three callers so they cannot disagree.
verdict context The unit the verdict reads: the research corpus, the machine-side gate state, and the operator's evidence policy. Owned by verdictContext() in fetchproof/lib/preflight.mjs — preflight, doctor, and the gate path all consume it, so the same snapshot cannot yield a different verdict.
gate warning A nonblocking finding from a particular preflight evaluation. PASS can carry warnings and disclosed known unknowns; it does not establish factual correctness. Human-facing briefs, audits and package reading guides preserve the finding's check/rule, reason and remedy, labeled as whole-corpus observations at evaluation time. No supplied warning observation is different from an observed zero. lib/render.mjs presents the list without deciding policy; see ADR-0143.
latest audit observation The corpus and gate evaluation represented by the manifest's latest version. A consecutive identical fingerprint reuses that version; a return to an earlier state earns a new chronological snapshot. Default show and bundle therefore retain the most recent observation without rewriting history (ADR-0144).
preflight The verdict: which findings block the build, which warn, and whether the answer is PASS. One function, three callers. fetchproof/lib/preflight.mjs.
contract check One named rule in the registry (discovery-contract, citations, provenance, unknown-closure, collection-attempts, hygiene, corpus-shape), running over a corpus snapshot. The registry is fetchproof/lib/checks.mjs; a check emits findings, the verdict decides what they mean. See ADR-0004.
finding (of a check) { severity: pass | warn | fail, check, detail }. Not to be confused with the finding cell of an evidence row, which is a claim.
override Exactly three: git commit --no-verify, a deliberate research/GATE_OFF, and a repository-local core.hooksPath — the third is silent by nature (it disables the hook that would report it), so doctor and preflight detect it and log it, and install-hooks.mjs reports it at install time when run inside such a repository (ADR-0131). The last two are recorded in research/overrides.log and counted by doctor; --no-verify records nothing, because git does not run the hook it skips - the docs claimed all three until 2026-10-03 (outside audit), while checks.mjs said the opposite.
fail open The default posture: a broken gate must not brick the machine. It is always loud, and --fail-closed inverts it deliberately. The default applies to a machine that never configured anything - not to one whose config cannot be read (unreadable config). See ADR-0002, ADR-0020.
unreadable config A machine config that exists and is not a JSON object the kit can read: it does not parse - a truncated write, a full disk, an editor crash, garbage from a sync tool - or it parses to an array, a string or another non-object (#310), which doctor words as what it is rather than as a parse failure. The third of three states lib/machine.mjs reads the config in (absent / readable / unreadable), and the one the old try { parse } catch {} collapsed into absent, so a hardened machine silently reverted to the defaults: {"failOpen": false} corrupted to {"failOpen": fal came back fail-open, and the hard-block, strict and builder settings riding along with it reverted too - a builder becoming a collector is the role that may collect and spend credits. An unreadable config holds the last one that parsed (snapshotted at <config>.last-good by saveConfig, and by a successful read when the snapshot differs and the config is still the version seen before the read, #370) and, with nothing to hold, fails closed; it never adopts the fail-open default on its own. posture() carries configState / configSource / configError so no consumer has to guess where its answer came from, doctor makes it a blocking CRITICAL finding, and gate.mjs --posture and the hook's posture_without_kit share one exit vocabulary - 0 allow, 1 a config that parses says fail-closed, 2 unreadable and resolved to blocking - where the code carries the resolved posture, not the state. Refines ADR-0002, which stays in its own words. See ADR-0020.
commit gate The machine-wide core.hooksPath pre-commit hook. Its staged-path list reaches the verdict as data on a pipe (git diff --cached --name-only … | node bin/gate.mjs --gate commit --staged-stdin), never as a growing argv — argv was O(n²) to build and bounded by the OS besides. An empty staged set is allowed; an UNREADABLE stdin when paths were declared is exit 2, an internal error, because "no paths" and "could not read the paths" must not both mean allow. See ADR-0020.
gate watchdog The bound the commit hook puts around the gate (timeout, or gtimeout on a macOS with coreutils; FETCHPROOF_GATE_TIMEOUT, default 120s). A watchdog kill is an internal error, not a verdict: it is named on stderr and then decided by the machine's posture — fail-closed blocks, fail-open allows. What it prevents is a hook that runs for minutes printing nothing, which an operator cannot tell from git being broken. Where coreutils is absent there is no watchdog and the gate runs unwatched — a degradation, not a failure. In the kit's own checkout the hook widens it to the suite budget plus 120 s, and an operator's FETCHPROOF_GATE_TIMEOUT governs both: the suite gets that watchdog less 120 s (never more than half of one under 240 s), so the gate reports before the watchdog can fire (ADR-0142). Both read it in one grammar - seconds with an optional fraction and s, m, h or d; a value outside it is named and ignored, and 0, which disables the watchdog, leaves the suite budget at its default. See ADR-0020.
edit-time gate The runtime's tool-call hook (hooks/edit-gate.mjs), registered in the runtime's settings file. Asks by default (permissionDecision: "ask"); hard-block stays behind editGate.mode. A registration left at a retired hook name is repointed by the installer, never doubled. See ADR-0012.

Project shape

Term Meaning
canonical project shape The structure every research project carries — the seven artifacts plus the raw-capture directory, and the .gitattributes that keeps the corpus's line endings identical on every machine (line-ending rewrite). Owned by LAYOUT in fetchproof/lib/scaffold.mjs, 12 entries. See ADR-0001, ADR-0020.
scaffold Creating that shape: bin/new-project.mjs for real projects, the test fixture for temporary ones. Both are the same call, so they cannot drift.
deployed hook A hook file the kit installs. Its executable bit is part of its contract: git skips a hook it cannot execute, silently, and reports the commit as clean.

The machine

Term Meaning
machine policy The machine-scoped state the kit reads and writes: ~/.agents/fetchproof.config.json, the install-state file, git core.hooksPath, and the runtime's settings file. Owned by fetchproof/lib/machine.mjs.
runtime anchors The one place the kit names a runtime's paths — settings file, personal skill roots, project-relative skill directory — in lib/machine.mjs (RUNTIME_ANCHORS). Every value is a DEFAULT: editGate.settingsPath, skillRoots, and projectSkillDir in the machine config override it, and FETCHPROOF_EDIT_GATE_SETTINGS overrides the settings path for one invocation. Not configurable for its own sake: a hardcoded skill root on a machine whose agent reads another root installs a skill nothing discovers. A retired config key (claudeGate) is still read into editGate.mode and reported by doctor until a write drops it; a retired ENVIRONMENT variable (CLAUDE_SETTINGS_PATH) is never honoured and doctor reports it while it is set. See ADR-0012.
machine role Which half of the two-machine kit a box is: role: collector | builder in the machine config, read by machineRole(), defaulting to collector (the stricter role — a collector without a key is blocked). Machine state, not project state: the same clone keeps one shape on both boxes. Set it with install-hooks.mjs --role. See ADR-0010.
collection policy The one answer to may this machine collect? — collectionPolicy() in fetchproof/lib/machine.mjs. A collector may; a builder must not, and its policy carries the reason and the remedy. The collection CLIs (research.mjs, decompose.mjs) ask it before touching an adapter and refuse (exit 2) when the answer is no; --dry-run and --status are still allowed because they spend nothing.
handoff check The arrival question, owned by fetchproof/lib/handoff.mjs (verifyHandoff): ledger present and non-empty, every capture an evidence row names on disk, chain verifies. Findings are named handoff-* (handoff-ledger-missing, handoff-ledger-empty, handoff-capture-missing, handoff-chain-broken) plus one handoff-remedy. One composition (verifyHandoff), two consumers: bin/handoff.mjs reports it and doctor consumes it directly — passing the corpus the verdict already read, so neither consumer can hold its own copy of the question or pay the hashing twice. A blocker on a builder machine, silent on a collector (whose existing reporters own the same states). It answers integrity, not currency: whether the corpus arrived as it was fetched, never whether the pages still say the same thing - a builder cannot fetch, and re-checking is the collector's (freshness-recheck). Its report's fetched and the OK line give the days the current rows' captures were fetched and the oldest one's age - superseded rows left out, each row dated as freshness dates it, by the capture its Raw cell names (ADR-0187). See ADR-0011.
posture The machine's fail-open / fail-closed setting. One implementation, one reader. See ADR-0002. A posture key that is present and not of its type ("failOpen": "false") resolves to its restrictive state and is named by doctor (ADR-0121); an absent key keeps its default.
deploy Copying the kit to ~/.agents/fetchproof and the skill to the personal skill root(s) — see runtime anchors. Overwrites by default, because a stale deployed copy silently defeats an update, and prunes kit files a past version shipped. --into <project> additionally binds the skill into that project (ADR-0006), and every later install refreshes each project bound that way, leaving a copy the project edited (#328).
transport The adapter that actually fetched a capture, recorded as a first-class transport field in the ledger entry: firecrawl-cli when the CLI ran, anything else names itself. transport-provenance in preflight names every evidence row whose capture did not come from the Firecrawl CLI. A cmd string is an annotation and proves nothing.
the browser's own traffic What a full Chrome or Chromium in headless mode asks the guard for on its own, not the page's doing: rendering a plain loopback page, Chromium 141 made nine connections - the component updater's clock, the default-search preconnect, accounts.google.com, the Cloud Messaging check-in and push channel - and CI's Chrome stable one more, a preconnect to www.gstatic.com. Flags, profile preferences and endpoint overrides were measured and left most of it. The guard knows it two ways: by name (BROWSER_SERVICE_HOSTS, ADR-0124) and by order (page first, ADR-0125). It is dropped, never judged, carried or counted as a refusal; a page's own request, exempt by host and port, never is.
dropped The guard's fourth outcome, beside a status, refused and tunnel: a request answered 403 with DROP_MARKER and logged dropped because it was the browser's own traffic, not the page's. A refusal is the page's doing and names the render's verdict; a drop is not, so it never reaches the verdict and never appears in refused. The CONNECT socket gets its error listener before any such answer: a browser resets a dropped preconnect with the body unread, and that reset once killed the guard child mid-render.
page first The render's rule (pageFirst, ADR-0125): until the host and port the operator asked for has been requested, every request is dropped, whatever its host - the page cannot have asked for anything before it was requested. From the page's own request on, only the named service hosts are dropped and the rest is judged. A guard started without it, as the unit tests do, judges everything; a drop by order needs no list and survives the next Chrome release, which a list does not.
asked-for URL The URL a plan or a search named when a page was fetched, as opposed to the URL the bytes came from after any redirect, which is what the capture and the ledger record. The capture index answers to both (requestedUrl, read out of the capture's command line), so a redirected page is a cache hit on the next run instead of a second paid fetch.
subtopic One dimension of the topic, enumerated in research/MAP.md before the Discovery Contract is written (phase 0). Status is COVERED, DISMISSED, or GAP - the same grammar as the contract's statuses. bin/decompose.mjs drafts the map with the table empty; the agent fills it.
universal dimensions The domain-agnostic checklist in lib/dimensions.mjs (access model, auth and credentials, rate limits and quotas, ToS/licensing/legality, schema stability, freshness, cost at expected volume, runtime and platform limits, output obtainability). decompose.mjs seeds every draft map with it - statuses blank; --recipe adds domain dimensions on top, never instead. subtopic-coverage fails a map that omits a dimension entirely: dismissing is fine, omitting is not.
quote anchor [quote: ...] in an EVIDENCE Finding: a verbatim passage of the row's capture, which the gate checks is really there (ADR-0087). A match needs at least one nonempty normalized fragment; formatting alone proves no occurrence. It proves the passage exists, never that it supports the claim.
kit path (recorded) The project's research/kit.json → kitPath: how its files spell the kit, written by new-project --kit (default $HOME/.agents/fetchproof). Files the kit writes later - the brief, the timeline - reuse it, so a project that travels never names the drafting machine's path (ADR-0084).
code paths (declared) The project's research/kit.json → architecture.codePaths: the directories the architecture-map rule guards (defaults src/lib/bin/scripts/app when undeclared). A commit touching a declared path carries docs/ARCHITECTURE.md in the same commit — the gate checks the map was staged, not that it is current.
coverage claim What a MAP.md with rows asserts: every subtopic was either researched or deliberately dismissed. A contract without a map is allowed, but makes no coverage claim - preflight warns once ("no coverage claim") because it cannot tell a thorough contract from a shallow one.
GAP A subtopic nobody researched. subtopic-coverage fails every GAP row by name: the gate can close listed unknowns, but a GAP is the shape of a question nobody asked.
DISMISSED A subtopic declared out of scope, with the reason required in "Covered by". A dismissal without a reason fails - it is a silent gap wearing a label.
capture completeness A raw capture's front-matter grade: full, partial (with an omitted note saying what is not in the file), or unspecified. capture-completeness in preflight flags a CLOSED unknown resting solely on captures that are not the whole page.
evidence policy The operator's machine-config switch (evidencePolicy: pluralist | strict in ~/.agents/fetchproof.config.json). Pluralist (default) turns transport and completeness findings into warnings; strict fails them. The call is the operator's, not the agent's.
credit One Firecrawl unit of spend. Every collection decision is a budget decision; the free tier is ~1,000.
attacker-controlled data Input this kit must treat as hostile even though a person typed it: a URL or search query in research/plan.json, which travels between machines through git, and any query or discovered URL pasted back from a web result. The consequence is local and concrete — such a value reaches lib/firecrawl.mjs, the one module that leaves the process, on the one machine holding the Firecrawl API key — so it is passed as an argv element and never as part of a shell string. See ADR-0020.
Firecrawl Adapter The vendor seam: fetchproof/lib/firecrawl.mjs. Every piece of vendor knowledge — the .cmd-shim resolution, argument rendering, ANSI noise, payload shapes, failure classification — lives there and nowhere else. Its exec is injectable and carries an argv array, spawned with no shell, so the adapter replays offline and no attacker-controlled data reaches an interpreter; command(args) renders a human-readable line for --dry-run and the ledger's cmd annotation, and nothing executes it. See ADR-0005, ADR-0020.

The validator layer

Ported 2026-09-20 under ADR-0029. These terms are its vocabulary; the protocol above does not depend on any of them.

Term Means
release evidence The R28–R32 record set a release is qualified against: envelopes, an artifact registry, a role roster, promotion pointers. release-validator.mjs reads it and reduces to one status; it never writes back to it.
release qualification The judgment that record set earns: PASS, INCOMPLETE, FAIL, or REOPEN. The four are ordered, and the worst one wins — a reduction, not a vote. REOPEN is not a synonym for FAIL: it means something that was once accepted has been withdrawn, which is a different fact about the world and deserves a different word.
conformance vector One fixed input with its expected output, in a signed, versioned packet. A vector is the opposite of a generated test case: it cannot drift with the generator, so two independent implementations can be asked the same question and their answers compared.
vector packet The file a set of vectors travels in. Strictly validated before any vector runs — packetVersion, profile, signature algorithm and encoding, non-empty vectors, unique ids — so a drifted packet is refused rather than partly executed. Its own SHA-256 is reported but deliberately excluded from the canonical JSON that gets hashed.
cross-language equivalence The property that the Node and Python runners return the same per-vector result for the same packet. Asserted per vector and never by comparing report hashes, because reportSha256 covers implementation.runtime and the two runtimes are not the same string. Two implementations agreeing is a stronger claim than one implementation tested twice.
replay envelope The self-describing record written when a property test fails: profile, schema and generator versions, family, seed, iteration, an input hash, and the failure. Enough to reproduce the failure without the run that found it.
write-once capture How a replay envelope is persisted: flag: 'wx' (O_EXCL), so the kernel refuses a second write rather than a check racing one. A later failure on the same seed returns the existing envelope instead of replacing it — the first failure is the evidence, the second is a repeat.
self-excluding hash A digest computed over a record with its own hash field removed. Without the exclusion the field would have to contain a hash of itself; with it, tampering with any other field is detectable. Used by the ledger records, the path-authority envelopes and the replay envelopes alike.
path authority The claim that a snapshot's paths are inside the root they say they are, proven against a Git origin rather than asserted. safePath resolves, checks containment, then realpath-checks the nearest existing ancestor — because a path that does not exist yet cannot be realpath'd, and skipping that step is the usual way symlink traversal gets through.
FI bundle A field-integrity submission: an evidence manifest, sign-off sidecars, and a workbook. fi-validator.mjs reads a projection out of the workbook in memory (zlib.inflateRawSync) rather than unpacking it to disk.
R29 linkage register The reviewer-workbook cross-reference: 24 task ids, six reference kinds, and the requirement that every explicit reference resolves to the task that claims it. A validator of 58 lines with zero imports — every input is an argument, so it touches no filesystem, network or clock.
hostile argument An argv element that cannot cross a Windows .cmd shim safely. This kit names them with a denylist of cmd.exe metacharacters (CMD_UNSAFE_CHARS) and refuses rather than re-quotes. Deliberately not an allowlist: the ADR-0029 source tree uses one, and an allowlist of ASCII punctuation refuses every accented letter — cafés and naïve façades is a permitted argument here and a hostile one there. unsafeCmdArgs(argv) is the single owner of the question, so the refusal and the message naming the offenders cannot drift apart.

The portable artifact

Added 2026-09-21 under ADR-0032. The vocabulary of the ZIP that leaves this kit.

Term Means
artifact The versioned ZIP a collection produces: README-FIRST.md, manifest.json, manifest.sha256, the project under project/, reports/, and the manifest schema. One contract for four consumers — an AI agent, an Actions workflow, a Windows application, and a person with an unzip tool — because four formats would drift and the field that drifts first is the authorization field.
manifest The machine-readable entry point, and the contract. README-FIRST.md is the human twin, rendered from the same derived state so the two cannot disagree; where they ever do, the manifest wins and both files say so.
derived authorization The rule that state, kind, buildAuthorized, review.* and gate.* come from running the real gate over the real project, never from a caller. There is no --build-authorized. A caller supplies identity; permission is computed. The reason: a ZIP outlives the shell that made it, and an asserted permission is the easiest field in the system to set and the most expensive one to get wrong.
the build rule state == "APPROVED_BRIEF" && gate.verdict == "PASS" && buildAuthorized == true. One implementation (authorizationProblems), encoded a second time by the schema. When the schema catches a violation first, the rule is still reported in its own words — $.buildAuthorized: must equal false is true, generic, and no use to somebody deciding whether they have been handed a forged permission.
valid ≠ authorized status: 'PASS' means the package is internally consistent: safe container, matching digest, every declared byte present, chain verifies. It says nothing about permission, which is the sibling field buildAuthorized. A valid collected corpus is PASS with buildAuthorized: false, and the CLI says so in words because exit 0 there is the most misreadable result the tool produces.
unrewritten finding An EVIDENCE.md Finding cell byte-identical to what the extractor re-produces from the cached capture — so it was demonstrably never rewritten by the reviewing agent. The negative half of a question with no positive answer: ADR-0013 refuses to grade whether an agent reasoned properly, and this does not try. It detects the unreviewed case only.
structural refusal A ZIP fault decided from the central directory, before one byte is inflated: traversal, absolute path, drive letter, backslash, NUL, duplicate, case collision, symlink, entry count, declared size, compression ratio. The directory is attacker-controlled too, so inflateRawSync also runs with maxOutputLength — the ceiling belongs to the runtime rather than to a field the attacker wrote.
transport, not storage What an artifact is. Workflow artifacts last at most 90 days on a public repository and vanish with the run that produced them, so anything that must persist is committed instead.
source (of a capture) The text a capture's Markdown was converted from - Firecrawl's rawHtml, the keyless transport's decoded HTML, the browser's rendered DOM - kept LF-folded beside the capture as <capture>.source.html and named in the fetch's ledger entry (source, sourceSha256), where it is verified like the capture (ADR-0140). A verbatim text or JSON capture has none, being its own source. Not a capture: nothing cites, grades or exports it; it exists so that a conversion defect can be repaired after the fact.
net log Chromium's own record of every request it made during a render (--log-net-log), which the browser transport reads for the origin's HTTP status and the page's final URL, because --dump-dom reports neither (ADR-0137). The guard child has it written to the render's folder - which the transport makes, which holds the browser's profile too, and which is removed after the read and again by the transport once the child is gone (ADR-0185); it never reaches the corpus. A render whose log names no response for the page is status not observed: the capture is graded partial saying so, and the status is never guessed from the page's text.
corroboration How many INDEPENDENT readings a closed unknown rests on, and the thirteenth check. Three shapes: one row (single-source), several rows from one host (one-voice), several hosts (independent). The middle one is the distinction a count cannot make — two pages from one vendor catch a misreading and cannot catch the vendor being wrong about itself. It reports and never fails on its own: a vendor's own reference is the authority on that vendor, and demanding a second opinion about it would be ceremony. evidencePolicy=strict is where an operator says otherwise.
suite rule The commit gate's check in the kit's own checkout only (ADR-0120): a commit staging a path under fetchproof/ is allowed when bin/selftest.mjs, run by the gate and read through its result file, reports no failures. It runs on the commit, not the working tree: in a scratch repository built from the index being committed, so the runner, the tests and the library are the commit's own; a scratch that cannot be built, or an index that changes while the suite runs, is a block (ADR-0183). The checkout is recognised by four markers (fetchproof/lib/core.mjs, bin/gate.mjs, bin/selftest.mjs, test/), at the top level or below it (#309); a scaffolded project has none and never pays. An unsupported test is not red; a runner that produced no result is a block; a run past the suite budget is a block; a docs-only commit owes nothing. Overridden by git commit --no-verify, which nothing records: git skips the hook that would. The suite budget is 65 minutes - twice the slowest honest run measured on a supported host - or, when the operator sets FETCHPROOF_GATE_TIMEOUT, that gate watchdog less 120 s, never more than half of one under 240 s (ADR-0142).
the suite describes the kit, not the machine What bin/selftest.mjs guarantees before the first test runs (ADR-0123): the run has a scratch home of its own (HOME, USERPROFILE; APPDATA, LOCALAPPDATA on Windows), no FETCHPROOF_* variable but the ones that describe the host or the run (the result file, the unsupported opt-in, the browser, the gate timeout, the live opt-ins, the harness's FETCHPROOF_TEST_* seams), and no vendor key. A test that spawns a kit command had read the operator's machine config, deployed kit, CLI login and keys, so a strict posture config turned 18 tests red and a Firecrawl CLI installed and not logged in two more - a verdict about the machine, not the kit. A test that wants a config, a deployed kit or a key makes its own.