Skip to content

Commit c708a61

Browse files
Make the nested decision project a convention, not a one-off (ADR-0030, ADR-0031)
The scratch directory that destroyed the last set of captures was not carelessness about an available alternative - it was the only shape the kit offered. readCorpus reads one research/ per root, a project has exactly one discovery contract, and this repository's contract enumerates U-1..U-8 about transports. Architecture unknowns had nowhere to live, and the cleanup that deleted them was the ordinary treatment of a temporary directory. Asking the next person to be more careful with a temp dir is not a fix. ADR-0030 gives them a home: research supporting a repository-level architectural decision lives in a self-contained project at docs/decisions/<date>-<decision-name>/, committed with its ledger, verified with that directory as the working directory. Costs no code - the project has always been the cwd (ADR-0017). The two alternatives are recorded with their reasons: a repository per decision (scatters the reasoning away from what it reasons about) and one contract with several topics (changes the protocol, and is the thing to build if these become frequent). ADR-0031 records the decision itself, naming the nested research/BRIEF.md as its evidence basis: Actions-first, any client pins X-GitHub-Api-Version, the ZIP is labelled COLLECTED CORPUS - HUMAN REVIEW REQUIRED inside the package, the artifact is delivery and never storage, and the paid run stays behind a required reviewer. The local .exe is deferred on useVfs being Stability 1.0 - not rejected; Windows is supported and the SEA exclusion is macOS x64, which is out of scope. The rule is written where agents read their rules (AGENTS.md) and where the project records what it has settled (research/DISCOVERY.md, Already decided), not only in an ADR nobody is routed to. The preliminary note keeps its text, with a superseded banner. It was right about what it observed and wrong about the cause, and rewriting it would erase the most useful thing in this decision: a claim that survived reading and failed running. bundle.mjs gains AGENTS.md in EXPECTED_TO_DRIFT, and the reason is the check working. AGENTS.md is the file the protocol is written in, so every protocol decision moves it; freezing it would mean recording decisions somewhere nobody is bound by. Same argument that added CONTEXT.md and README.md on 2026-09-20. docs/ARCHITECTURE.md moves in this commit per ADR-0007. 645 passed, 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent dce565f commit c708a61

8 files changed

Lines changed: 304 additions & 1 deletion

‎AGENTS.md‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,37 @@ whose topic is plainly about something else. Any one of them is a reason to ask,
9494
of them is a reason to search. An existing corpus about another topic is the signal doing
9595
its job - the answer is not to write over it, and not to search past it, but to ask.
9696

97+
## Researching a decision ABOUT this repository: a nested project, never a scratch dir
98+
99+
This repository's contract (`research/DISCOVERY.md`) enumerates U-1..U-8, all about
100+
transports and vendors. A decision about the repository itself - how it should be
101+
delivered, packaged, hosted - is a **different question**, and putting it in that contract
102+
would leave one corpus asserting two unrelated sets of claims with `unknown-closure` and
103+
`subtopic-coverage` judging them together.
104+
105+
**Scaffold a nested project instead** (ADR-0030):
106+
107+
```
108+
node research-kit/bin/new-project.mjs docs/decisions/<date>-<decision-name> --topic "<question>"
109+
cd docs/decisions/<date>-<decision-name>
110+
```
111+
112+
Then work there exactly as in any project, and **commit the corpus including
113+
`research/raw/.fetches.jsonl`**. The worked example is
114+
`docs/decisions/2026-09-21-delivery-architecture/`.
115+
116+
Two things this rule exists to prevent, both of which have already happened once:
117+
118+
- **Do not collect into a temporary directory.** The first attempt at that decision
119+
collected nine pages into a scratch project outside the repository, read the findings,
120+
and deleted the directory as routine cleanup - destroying the captures, the ledger and
121+
the provenance for every claim. Scratch projects stay legitimate for *experiments*: what
122+
they cannot do is support a committed decision. The moment a scratch finding is going to
123+
be cited, it belongs in a nested project.
124+
- **Do not cite this repository's preflight as validation for a nested one.** The root
125+
verdict judges the root corpus. A nested project is verified with its own directory as
126+
the working directory, and nothing else counts.
127+
97128
## Resuming after an interruption: resume, do not restart
98129

99130
A session can die mid-task - the sandbox resets, the turn is cut, the working tree is

‎docs/ARCHITECTURE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ else, and it never touches the corpus:
144144
dropping a setting the operator had tightened, and reports a retired ENVIRONMENT
145145
variable (`CLAUDE_SETTINGS_PATH`) without ever honouring it. Owns the machine role and the collection policy that follows from it (`machineRole`, `collectionPolicy`, `collectionRefusal`) — the single answer to "may this machine collect?". The only module that reaches past the project root. | ADR-0002, ADR-0010, ADR-0012 |
146146
| `handoff.mjs` | The **arrival question**: `verifyHandoff(root)` reports a ledger that is absent or empty, every capture an evidence row names that is not on disk, and a chain that does not verify — each named. Read-only by design: the machine that asks cannot collect the missing bytes. The report also carries `lineEndings`, the captures whose body hash fails *only* because git rewrote CRLF into them on checkout, and **`handoffRemedy(report)` picks the remedy from the cause** rather than printing one text for every way a corpus can look broken (ADR-0020): `HANDOFF_REMEDY` when something did not travel (the collector must push `research/raw/`, dotfiles included), `lineEndingRemedy` when everything travelled and this machine rewrote it (pin with `.gitattributes`, re-check out, touch no collector and spend no credits), both when a report holds both. Consumed by `bin/handoff.mjs` and doctor (blocker on `role=builder`, silent on a collector; doctor passes the snapshot it already read, so the question is composed once for both). | ADR-0011, ADR-0020 |
147-
| `bundle.mjs` | The **archive this project arrived as**, and how far the tree has moved from it. `BUNDLE_INDEX.md` lists 80 SHA-256 digests taken at the 2026-09-17 handover; until ADR-0028 **nothing had ever read them**, which is how ADR-0022 could treat them as a constraint for three days without noticing the constraint was already violated. `verifyBundle(root)` sorts every entry into unchanged / drifted / unexpected / missing, where "drifted" means a file in `EXPECTED_TO_DRIFT` — the corpus, the map, the ADR index, the manifest itself, and (from 2026-09-20) `CONTEXT.md` and the root `README.md`, which are documentation ABOUT the code and go stale the moment it moves, each of which moves because one of this project's own rules *requires* it to. The list is deliberately narrow: exempting a planning document would leave the report unable to say anything, and a test asserts none is exempted. Hashes **bytes**, not decoded text. Reports; never fails a build — a changed file is a fact about history, and the only thing that makes it a problem is nobody knowing. | ADR-0028 |
147+
| `bundle.mjs` | The **archive this project arrived as**, and how far the tree has moved from it. `BUNDLE_INDEX.md` lists 80 SHA-256 digests taken at the 2026-09-17 handover; until ADR-0028 **nothing had ever read them**, which is how ADR-0022 could treat them as a constraint for three days without noticing the constraint was already violated. `verifyBundle(root)` sorts every entry into unchanged / drifted / unexpected / missing, where "drifted" means a file in `EXPECTED_TO_DRIFT` — the corpus, the map, the ADR index, the manifest itself, (from 2026-09-20) `CONTEXT.md` and the root `README.md`, which are documentation ABOUT the code and go stale the moment it moves, and (from 2026-09-21) `AGENTS.md`, which is the file the agent protocol is *written in* — caught drifting by this check when ADR-0030 gave decision research a committed home and the rule that sends an agent there had to be where agents read their rules — each of which moves because one of this project's own rules *requires* it to. The list is deliberately narrow: exempting a planning document would leave the report unable to say anything, and a test asserts none is exempted. Hashes **bytes**, not decoded text. Reports; never fails a build — a changed file is a fact about history, and the only thing that makes it a problem is nobody knowing. | ADR-0028 |
148148
| `git-config.mjs`, `config.mjs` | Compatibility shims → `machine.mjs`. | ADR-0002 |
149149
| `runtime.mjs` | The host prerequisites a command actually needs: `checkNode` (22+), `checkGit`, `checkPython` (3.12+, reading either stream because Python 2 printed its version to stderr, trying `python3` **first** and continuing past an old interpreter rather than stopping at it — `python` is an alias for 2.7 on the most common Linux and older-macOS layout, and stopping there reported "no usable Python" with a good 3.12 one name away), and `requireRuntime({ node, git, python })` which exits **3** — distinct from a product failure — naming the missing piece and how to install it. Deliberately per-command rather than one gate that demands everything: Python is used by the cross-language conformance runners and nothing else, and a command that only reads the corpus has no business refusing to run because a language it never calls is absent. | — |
150150
| `scaffold.mjs` | Project shape: `LAYOUT` (12 entries), the **four** `GATE_MARKERS` (pinned by ADR-0001 and its test), template rendering, placeholder validation, hook executability. `.gitattributes` is a LAYOUT entry but **not** a gate marker: a project without it still gates, it just breaks on a Windows builder (ADR-0020). | ADR-0001, ADR-0020 |
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# ADR-0030 — Research for a repository-level decision lives in a nested project
2+
3+
- **Date:** 2026-09-21
4+
- **Status:** accepted
5+
- **Area:** research protocol, project layout, provenance
6+
- **Depends on:** ADR-0003 (the corpus has one reader and one writer), ADR-0017 (the project is the cwd)
7+
- **Worked example:** `docs/decisions/2026-09-21-delivery-architecture/`
8+
9+
## Context
10+
11+
On 2026-09-21 this repository needed to decide how the kit should be delivered to a
12+
non-technical operator. That question has its own unknowns — what the dispatch API returns,
13+
how long an artifact survives, what a public run discloses — and none of them belong to the
14+
contract at `research/DISCOVERY.md`, which enumerates U-1..U-8 about transports and vendors.
15+
16+
The kit offers one place to put a corpus. `readCorpus(root)` reads `research/` relative to
17+
one project root (ADR-0003), a project has exactly one discovery contract, and the project
18+
is the current working directory with no override (ADR-0017). Putting architecture
19+
unknowns into the repository's own contract would mean one corpus asserting two unrelated
20+
sets of claims, with `unknown-closure` and `subtopic-coverage` judging them together.
21+
22+
So the research was collected into a temporary directory outside the repository. The
23+
findings were read, the note was written — and the temporary directory was then removed as
24+
routine cleanup, taking nine captures, the hash-chained ledger, and the provenance for
25+
every claim with it. What remained was a recommendation with nothing underneath it, in a
26+
repository whose entire premise is that a claim without provenance is not evidence.
27+
28+
That was not carelessness about an available alternative. **It was the only shape the kit
29+
offered**, and the cleanup that destroyed the evidence was the ordinary treatment of a
30+
temporary directory. A protocol that makes the careful path unavailable will keep producing
31+
this outcome.
32+
33+
## Decision
34+
35+
**Research supporting a repository-level architectural decision lives in a self-contained
36+
project at `docs/decisions/<date>-<decision-name>/`.**
37+
38+
It is an ordinary project in every respect the kit can see: scaffolded by
39+
`bin/new-project.mjs`, holding its own `AGENTS.md`, `START_HERE.md`, `research/DISCOVERY.md`,
40+
`research/MAP.md`, `research/plan.json`, `research/EVIDENCE.md`, `research/BRIEF.md` and
41+
`research/raw/`, and verified by `cd`-ing into it. The kit needs no change to support this,
42+
because the project is the working directory and always was.
43+
44+
Two rules bind it:
45+
46+
1. **The corpus is committed, ledger included.** `research/raw/.fetches.jsonl` travels with
47+
the decision. The nested `.gitignore` the scaffold writes already negates it explicitly,
48+
and its patterns are relative to the nested root, so the byproducts (`.usage.jsonl`,
49+
`.diagnostics.jsonl`, `.failures.jsonl`, `.fetches.lock`) are excluded there without the
50+
repository's own rules needing to know the path exists.
51+
2. **Temporary scratch projects may be used for experiments, but their findings cannot
52+
support a committed decision unless the complete evidence corpus and ledger are
53+
preserved.** A scratch project remains the right tool for testing whether a predicate is
54+
satisfiable, or what a transport does. It stops being the right tool the moment its
55+
output is cited.
56+
57+
And one consequence that has already been violated once, so it is stated rather than
58+
implied: **verification runs with the nested directory as the current working directory.**
59+
The repository's own preflight judges the repository's corpus. Quoting it beside a nested
60+
project's claims asserts something it does not say.
61+
62+
## Alternatives considered
63+
64+
**A second repository per decision.** Clean separation, and each decision gets a real
65+
project. It scatters the reasoning away from the thing it reasons about, multiplies
66+
repositories without bound, and makes the evidence for a decision something you have to
67+
know to go looking for. Rejected on cost, not on correctness.
68+
69+
**One contract, several topics.** The most general fix and the most invasive: it changes
70+
the protocol rather than a directory layout. `unknown-closure`, `subtopic-coverage` and the
71+
brief's shape all assume one topic per corpus, and each would need a topic dimension.
72+
Deferred rather than rejected — if decision projects become frequent enough that the
73+
duplication hurts, this is the thing to build. Nothing here forecloses it.
74+
75+
**Leave it as it was, and be more careful.** The failure this ADR responds to was a
76+
`rm -rf` of a directory whose entire purpose was to be temporary. Asking for more care from
77+
the next person, who will also be looking at a temporary directory, is not a fix.
78+
79+
## Consequences
80+
81+
- `docs/decisions/<date>-<name>/` is a recognised location, and a reader who finds a
82+
`research/` tree there is looking at a decision project rather than a stray copy.
83+
- A decision can be re-verified years later: the captures are in the repository, the chain
84+
verifies, and every claim in the brief resolves to a page that was actually fetched.
85+
- The repository now contains more than one `research/` tree. Nothing in the kit is
86+
confused by this — every command resolves relative to its cwd — but a reader might be,
87+
which is what the nested `AGENTS.md` and `START_HERE.md` are for.
88+
- The nested project's `docs/ARCHITECTURE.md` is scaffolded and has no code to map. It is
89+
kept and says so, rather than being deleted and reported missing by `doctor` forever.
90+
- The convention is recorded in the repository's own `AGENTS.md` and in
91+
`research/DISCOVERY.md` under **Already decided**, so an agent meets it before it needs
92+
it rather than after.
93+
94+
## What this ADR does not claim
95+
96+
That nested projects are the right home for *all* research. They are for research whose
97+
subject is a decision about this repository. Research about what this kit should be able to
98+
do belongs in the repository's own contract, where it has always been.

0 commit comments

Comments
 (0)