Repository navigation
Delivery-architecture research, gated and committed (ADR-0030, ADR-0031) - #21
Merged
Merged
Conversation
Found by the clock, not by a change. Four tests in checks.test.mjs went red at
00:00 UTC while nothing in the repository had moved - they had been green all
day and failed on the calendar turning over.
ENOENT: copyfile '.../2026-09-20-limits-example-a4e22bcd.md'
-> '.../2026-09-21-limits-example-refreshed.md'
`makePassingProject` names its capture with `today()`. `withRefresh` looked for
`2026-09-20-limits-example-a4e22bcd.md` by literal, and the same test wrote
evidence rows dated `2026-09-21` by literal. So the fixture and the test agreed
on exactly ONE calendar day, and every other day the copy could not find its
source.
A test that only works on the date it was written is a test with an expiry
nobody wrote down, and it expires into a red suite that looks like a real
regression.
WHAT CHANGED
The capture is DISCOVERED from the fixture's raw directory rather than named,
and both dates are DERIVED from it: the refresh is that capture's date plus
one day, used for the new filename, the ledger `at`, and the evidence row.
The same-day duplicate test reads the fixture's own date instead of carrying
`2026-09-20` twice.
Both now hold whatever the clock says, and the relationship the tests are
actually about - one capture, then a LATER one - is expressed rather than
coincidental. `duplicate-url` exists to catch two rows for one URL on the same
day, which is why deriving the date matters and not only the filename: an
intermediate fix that shared the original's date made the refresh a legitimate
duplicate and the test correctly stayed red.
LEFT ALONE
audit.test.mjs passes an explicit `{ date: '2026-09-20' }` and asserts a path
length against a literal. Those are self-consistent - the date is an input,
not an assumption about when the suite runs.
VERIFIED
node research-kit/bin/selftest.mjs -> 645 passed, 0 failed, exit 0
node research-kit/bin/preflight.mjs -> PASS, 0 blocking, 0 warnings
The architecture comparison agreed before any of it is built: Actions-only
collector, local Windows .exe, hybrid, or REST/API seam. Collected through the
metered route - 9 primary vendor pages, 9 credits - and written up as a decision
INPUT, not a decision.
THE FINDING THAT CHANGES THE DESIGN
POST /actions/workflows/{id}/dispatches returns 204 No Content. No body, no
run id, no run url. Verified by calling it, not by reading about it:
HTTP/2.0 204 No Content
The proposal that prompted this research stated the response "includes the
workflow-run ID and run URL, allowing another program to monitor the run".
It does not. A caller cannot learn which run it just started.
The consequence was demonstrated rather than assumed - two dispatches fifteen
seconds apart:
35546293660 queued workflow_dispatch 00:00:48Z actor=StepenkoAnatoli
35546279728 in_progress workflow_dispatch 00:00:33Z actor=StepenkoAnatoli
Same event, same actor, nothing but a timestamp between them. Any async
integration needs a correlation token the CALLER chooses - a required
client_ref input echoed into the run and artifact names - and adding it later
means changing the contract after something depends on it.
THE OTHER FOUR
- Actions is free for public repositories on standard runners. Not a
discriminator while this repo is public; it becomes one the moment privacy
requires otherwise.
- Artifact retention is 90 days maximum. A ZIP is not archival.
- Node's single-executable support is Stability 1.1, "Active development",
with macOS x64 explicitly unsupported. Windows IS supported, so this cuts
for the .exe option rather than against - but the packaging story may move.
- Environments support required reviewers and wait timers, which is the
mechanism for gating a paid run behind approval.
HOW THE THREE HUMAN STEPS FARE
The axis the decision actually turns on, because it is the one the protocol
constrains. Actions-only does none of them and must say so on the artifact;
local .exe fits them best, since they are editing tasks; hybrid is the
strongest long-term shape and the most work; the API seam is not a delivery
option at all but the interface the others would use.
RECOMMENDED, NOT DECIDED
Actions-only collector first, with the COLLECTED CORPUS - HUMAN REVIEW
REQUIRED label inside the package rather than only in the workflow summary,
and a required client_ref input. Both follow from evidence above rather than
from taste.
The document says in its own first lines that the decision is not taken and
that its findings want reading against their sources. A research brief that
asserted its own conclusion would be the thing this kit exists to prevent.
NOT ESTABLISHED
The runner-side npm install path; whether artifact download is ergonomic for
the intended beginner, which no vendor documentation answers; anything about
the desktop app beyond the constraint the 204 imposes.
A reviewer noticed PR #21 called itself "research complete" while changing no corpus files - no discovery contract, no evidence rows, no captures, no ledger entries. That is correct, and the cause is worse than an omission. I DELETED THE EVIDENCE Nine pages were collected on 2026-09-21 through the metered route into a scratch project outside the repository - deliberately outside, to avoid mixing a second research question into the corpus that answers this kit's own unknowns. The findings were read from those captures. Then the scratch project was removed as routine cleanup, taking the captures, the hash-chained ledger, and the provenance for every claim with it. That is the exact failure this kit exists to prevent, committed while using the kit. And the repository's clean preflight - which I cited - validates the EXISTING corpus. It says nothing about this note, and quoting it beside the note implied otherwise. WHAT CHANGED requirements-2026-09-21-* -> preliminary-2026-09-21-*, and the document opens by saying it is not a completed Phase 1 handoff and that formal collection remains outstanding. It also separates findings by how checkable they are, because they are not equally weak: 204 No Content re-verifiable in one command, depends on no capture of mine the correlation race re-verifiable; run ids 35546279728 and 35546293660 are in GitHub's API today retention, billing, SEA, CITATIONS, not evidence. Live URLs anyone can environment rules re-read, with no capture, hash or chain behind them. Leads to confirm, not facts proven. The recommendation rests mostly on the first two, which is why it survives the downgrade rather than being withdrawn. THE STRUCTURAL GAP, WHICH WILL RECUR The scratch project was the only option the kit offers. `readCorpus(root)` reads one `research/` per project root, and a project has one discovery contract; this repository's enumerates U-1..U-8, all about transports. Architecture-decision unknowns are a different question, and one contract asserting two unrelated sets of claims would have `unknown-closure` and `subtopic-coverage` judging them together. Three options, none currently supported well: a nested project under docs/decisions/<name>/, a second repository per decision, or one contract with multiple topics. The first is cheapest and needs no code - it needs a decision that decision-research lives there, so the next person does not reach for a scratch directory and delete the evidence as I did. VERIFIED node research-kit/bin/selftest.mjs -> 645 passed, 0 failed, exit 0 node research-kit/bin/preflight.mjs -> PASS, 0 blocking, 0 warnings (both about the repository's own corpus, which is the point above)
…oject
The recommendation in docs/preliminary-2026-09-21-delivery-architecture.md had no
provenance under it: nine pages were collected into a scratch project outside the
repository, read, and then deleted as routine cleanup, taking the captures, the
hash-chained ledger and every claim's traceability with them.
This is that research done again, through the gate, in a place it cannot be lost
from. Nine pages, metered route, nine credits, all graded full, none failed.
preflight PASS 0 blocking, 0 warning(s), 12 passing
handoff OK - 9 ledger entries, every cited capture on disk, chain verifies
doctor READY
Run with docs/decisions/2026-09-21-delivery-architecture/ as the working directory.
The repository's own preflight judges the repository's corpus and says nothing about
this one; citing it here would repeat the error that produced the preliminary note.
THE EVIDENCE OVERTURNED THE HEADLINE FINDING IT WAS COLLECTED TO SUPPORT.
The preliminary note recorded that POST /actions/workflows/{id}/dispatches returns
204 No Content, that a caller therefore cannot learn which run it started, and that
a client_ref correlation token had to be designed into the first release. The
collected documentation says 200, with workflow_run_id, run_url and html_url.
Both are true, and neither page explains the other - so it was settled by running
both against this repository:
2022-11-28 (the default) 204 No Content, empty body
2026-03-10 200 OK, {"workflow_run_id":35548135379,...}
X-Github-Api-Version-Selected names the version in each response, and run
35548135379 resolves to a real queued workflow_dispatch run. The Deprecation header
on the 204 is the same signal from the other side: the version returning nothing is
the deprecated one, and it is the default.
So the correlation race is a property of an unpinned client, not of the endpoint.
The requirement that replaces client_ref is smaller and firmer: pin
X-GitHub-Api-Version.
A second finding worth the collection on its own. Environments give required
reviewers and wait timers - but on a GitHub Free plan those rules exist only for
public repositories, and a Free repository converted to private has its protection
rules and environment secrets IGNORED, not refused. The privacy argument for going
private would silently remove both the approval gate on the paid run and the secret
holding the credential. Sensitive research needs a paid plan, not a visibility
toggle.
The three human steps were done here rather than claimed: every MAP row judged
COVERED or DISMISSED with a reason, every auto-extracted Finding rewritten into a
claim, and the brief's Contradictions and Decision sections authored. The extractor's
draft for E-01 was a single table row; the claim it became is the finding above.
Committed deliberately: research/raw/.fetches.jsonl. The nested .gitignore excludes
the byproducts (.usage.jsonl and friends) with patterns relative to this project, so
the repository's own rules never had to learn the path exists.
Two gaps are recorded rather than papered over: the runner-side
`npm install -g firecrawl@<version>` path has still never executed, and whether
artifact download suits the intended operator is a usability question no vendor page
answers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…0, 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
The delivery-architecture research, collected through the gate into a committed nested
project at
docs/decisions/2026-09-21-delivery-architecture/, plus the convention thatgives such research a home (ADR-0030) and the decision it produced (ADR-0031).
Verified with the nested directory as the working directory — the repository's own
preflight judges the repository's corpus and says nothing about this one:
Also verified from a clean
git archiveextract of the committed bytes, so the dotfileledger demonstrably travels: same PASS, same chain.
The evidence overturned the finding it was collected to support
The preliminary note recorded that
POST /actions/workflows/{id}/dispatchesreturns204 No Content, that a caller "cannot learn which run it started", and that aclient_refcorrelation token was therefore required in the first release.The collected documentation says
200, withworkflow_run_id,run_url,html_url.Neither page explains the other, so both were run against this repository:
X-Github-Api-Version-Selectedgh api -X POST .../dispatches -f ref=main -i2022-11-28(default)204 No Content, empty body-H "X-GitHub-Api-Version: 2026-03-10"2026-03-10200 OK,{"workflow_run_id":35548135379,...}Run
35548135379resolves to a real queuedworkflow_dispatchrun. TheDeprecationheader on the 204 is the same signal from the other side: the version that returns nothing
is the deprecated one, and it is the default.
The correlation race is a property of an unpinned client, not of the endpoint. The
requirement that replaces
client_refis smaller and firmer: pinX-GitHub-Api-Version.The second finding worth the nine credits
Environments give required reviewers and wait timers — but on a GitHub Free plan those
rules exist only for public repositories, and a Free repository converted to private
has its protection rules and environment secrets ignored, not refused. The privacy
argument for going private would silently remove both the approval gate on the paid run and
the secret holding the credential. Sensitive research needs a paid plan, not a visibility
toggle.
Why a nested project (ADR-0030)
The scratch directory that destroyed the previous captures was the only shape the kit
offered:
readCorpusreads oneresearch/per root, and this repository's contractenumerates U-1..U-8 about transports. Architecture unknowns had nowhere to live, and
deleting a temp directory is the ordinary treatment of a temp directory.
So: research supporting a repository-level architectural decision lives at
docs/decisions/<date>-<decision-name>/, committed with its ledger, verified from thatdirectory. Costs no code — the project has always been the cwd (ADR-0017). Scratch projects
stay legitimate for experiments; what they cannot do is support a committed decision.
Recorded where agents read their rules (
AGENTS.md) and where the project records what ithas settled (
research/DISCOVERY.md, Already decided), not only in an ADR nobody isrouted to.
The three human steps were done, not claimed
Every MAP row judged
COVEREDorDISMISSEDwith a reason; every auto-extracted Findingrewritten into a claim; the brief's Contradictions and Decision sections authored. The
extractor's draft for E-01 was one table row — the claim it became is the finding above.
Kept honest
docs/preliminary-2026-09-21-delivery-architecture.mdkeeps its text under a supersededbanner. It was right about what it observed and wrong about the cause, and rewriting it
would erase the most useful thing here: a claim that survived reading and failed running.
npm install -g firecrawl@<version>path has still never executed, and whether artifactdownload suits the intended operator is a usability question no vendor page answers.
bundle.mjsgainsAGENTS.mdinEXPECTED_TO_DRIFT; that was the drift check working,and the reason is in the code.
Also carried from earlier on this branch: the fix for four tests that hardcoded
2026-09-20and would have failed at midnight.645 passed, 0 failed. Cost: 9 Firecrawl credits, no searches, nothing cached, nothing
failed.
🤖 Generated with Claude Code