Skip to content

Delivery-architecture research, gated and committed (ADR-0030, ADR-0031) - #21

Merged
StepenkoAnatoli merged 5 commits into
mainfrom
docs/phase1-architecture-research
Sep 21, 2026
Merged

StepenkoAnatoli merged 5 commits into
mainfrom
docs/phase1-architecture-research

Conversation

@StepenkoAnatoli

@StepenkoAnatoli StepenkoAnatoli commented Sep 21, 2026 •

Copy link
Copy Markdown
Owner

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 that
gives 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:

preflight   PASS  0 blocking, 0 warning(s), 12 passing
handoff     OK - 9 ledger entries, every cited capture on disk, chain verifies
doctor      READY

Also verified from a clean git archive extract of the committed bytes, so the dotfile
ledger 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}/dispatches returns
204 No Content, that a caller "cannot learn which run it started", and that a
client_ref correlation token was therefore required in the first release.

The collected documentation says 200, with workflow_run_id, run_url, html_url.
Neither page explains the other, so both were run against this repository:

request X-Github-Api-Version-Selected result
gh api -X POST .../dispatches -f ref=main -i 2022-11-28 (default) 204 No Content, empty body
the same, -H "X-GitHub-Api-Version: 2026-03-10" 2026-03-10 200 OK, {"workflow_run_id":35548135379,...}

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 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_ref is smaller and firmer: pin X-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: readCorpus reads one research/ per root, and this repository's contract
enumerates 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 that
directory. 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 it
has settled (research/DISCOVERY.md, Already decided), not only in an ADR nobody is
routed to.

The three human steps were done, not claimed

Every MAP row judged COVERED or DISMISSED with a reason; every auto-extracted Finding
rewritten 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.md keeps its text under a superseded
    banner. 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.
  • 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.
  • bundle.mjs gains AGENTS.md in EXPECTED_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-20 and would have failed at midnight.

645 passed, 0 failed. Cost: 9 Firecrawl credits, no searches, nothing cached, nothing
failed.

🤖 Generated with Claude Code

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)
@StepenkoAnatoli StepenkoAnatoli changed the title Phase 1 research: how Research-Kit should be delivered (+ four tests that expired at midnight) Preliminary delivery-architecture note (NOT a Phase 1 handoff) + four tests that expired at midnight Sep 21, 2026
StepenkoAnatoli and others added 2 commits September 21, 2026 03:55
…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>
@StepenkoAnatoli StepenkoAnatoli changed the title Preliminary delivery-architecture note (NOT a Phase 1 handoff) + four tests that expired at midnight Delivery-architecture research, gated and committed (ADR-0030, ADR-0031) Sep 21, 2026
@StepenkoAnatoli
StepenkoAnatoli merged commit a00560a into main Sep 21, 2026
3 checks passed
@StepenkoAnatoli
StepenkoAnatoli deleted the docs/phase1-architecture-research branch September 21, 2026 08:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant