Skip to content

Commit a00560a

Browse files
Merge pull request #21 from StepenkoAnatoli/docs/phase1-architecture-research
Delivery-architecture research, gated and committed (ADR-0030, ADR-0031)
2 parents c44810e + c708a61 commit a00560a

33 files changed

Lines changed: 7985 additions & 6 deletions

‎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)