Skip to content

Commit 0732d36

Browse files
Merge pull request #43 from StepenkoAnatoli/docs/corroborate-everything
Corroborate sea-assets and delivery-architecture, and find a race the design never needed
2 parents 3744aed + 25f962c commit 0732d36

29 files changed

Lines changed: 7990 additions & 57 deletions

‎docs/ARCHITECTURE.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ else, and it never touches the corpus:
121121
| `artifact.mjs` | **Packaging a project without changing it, and deriving every authorization field.** `createArtifact` → `{ bytes, name, manifest, entries, derived }`; `writeArtifact` writes then **re-validates with the consumer's own validator** before reporting success. There is no `--build-authorized` and no option that reaches an authorization field: `deriveState(root)` runs the real gate with `env` defaulting to `{}` so a packaging shell's override cannot become a property of a package that outlives it. `mapClassified` and `briefReviewed` have honest machine answers; `findingsReviewState` answers the third **negatively** — a `Finding` byte-identical to what `firstFinding` re-extracts from the capture was demonstrably never rewritten, which detects the unreviewed case without grading the reviewed one (ADR-0013's line, uncrossed). `packageName` never carries the topic (an artifact listing is world-readable on a public repository) and falls back to `run-<workflowRunId>`; `checkClientRef` refuses rather than sanitises. Inventory sorted by ZIP path, manifest and digest appended **last**, digest over the exact manifest bytes. | ADR-0032, ADR-0031 |
122122
| `mcp.mjs` | **The collector as a Model Context Protocol server**, and the first module in this repository built from an APPROVED BRIEF rather than from a plan - every design choice cites an E-row in `docs/decisions/2026-09-21-agent-interface/`. `handle(message, deps)` is a pure function of a JSON-RPC message plus injected transport, so the whole surface is tested offline. **stdio, not Streamable HTTP**: semantics are identical on both, so the difference is operational, and remote costs the entire OAuth 2.1 chain. **Two tools, not one** - `collect` returns a run id immediately, `fetch_corpus` returns a link - because a server cannot initiate a request, so the alternative is one call held open for minutes and an agent that loses a paid run when its process dies. **A `resource_link`, never embedded bytes**: the spec defines embedding but states no maximum size, so a link is the defensible option. **No tool takes a token** and a test asserts no parameter is credential-shaped. `PROTOCOL_VERSION` is pinned and a mismatch answers `-32022` with `data.supported`, the same discipline as pinning `X-GitHub-Api-Version`. `createStdioLoop` serialises chunk processing through one promise chain - overlapping `data` events were mutating the buffer concurrently, which a test caught. **DUAL-ERA**: `SUPPORTED_VERSIONS` is `['2026-07-28', '2025-11-25']` and `initialize` is implemented beside `server/discover`, because the specification is ahead of every shipped client - the official SDK at v1.30.0 declares `2025-11-25` as its latest and answered the modern-only first version with `-32601: unknown method initialize`. `initialize` **echoes** a serveable requested version and falls back to **legacy** rather than to this server's preference, because a client throws on a version it does not support. | ADR-0034 |
123123
| `disclosure.mjs` | **What a public run actually exposes, measured rather than asserted.** Written because the opposite claim was documented first: the collector's header said dispatch inputs are visible to anyone with read access, and checking it found the topic in none of the readable responses. `probeRun` sends **unauthenticated** requests - there is no token parameter, and a probe that quietly authenticated would answer a different question while looking like this one. `summarise` separates `exposedNames` (true on any public repo, and the reason the topic is kept out of run and artifact names) from `exposedContent` and `exposedSubject`. `needleChecked` exists so a report can never let "false" read as "no": without a topic to search for, the rendering says **not checked**. An unreachable endpoint is a refusal to conclude, never a clean result. | ADR-0035 |
124-
| `dispatch.mjs` | **Asking the collector to run, from outside GitHub** - the seam an agent or a desktop app uses. `dispatchCollection` (pins `X-GitHub-Api-Version: 2026-03-10` on the REQUEST, not merely in a comment), `waitForRun`, `listArtifacts`, `downloadArtifact`, `unwrapArtifact`, plus `tokenFromEnv` and `redact`. **No token may be passed as an argument** - `tokenFromEnv` is the only way in, because a credential on a command line reaches the shell history, `ps`, and any log that echoes its own command; a test asserts the source contains no token flag. `redact` is PATTERN-based rather than "remove the token we hold", so it also catches a credential the process was never given - one echoed back by a server, a proxy, a redirect. A **204** is a named `NO_RUN_ID` failure explaining that the server served `2022-11-28`, never an empty success: "the run started and I cannot tell which one" is the failure a caller is least able to diagnose from a status code. `waiting` is reported as a status, not a stall - it means a protection rule is holding the job. `unwrapArtifact` opens GitHub's outer ZIP through the same distrustful reader, and passes a hostile or ambiguous container through untouched for the validator to refuse rather than guessing at packaging depth. Every request goes through an injectable `fetch`, so the whole module is tested offline. | ADR-0033, ADR-0032 |
124+
| `dispatch.mjs` | **Asking the collector to run, from outside GitHub** - the seam an agent or a desktop app uses. `dispatchCollection` (pins `X-GitHub-Api-Version: 2026-03-10` on the REQUEST, not merely in a comment), `waitForRun`, `listArtifacts`, `downloadArtifact`, `unwrapArtifact`, plus `tokenFromEnv` and `redact`. **No token may be passed as an argument** - `tokenFromEnv` is the only way in, because a credential on a command line reaches the shell history, `ps`, and any log that echoes its own command; a test asserts the source contains no token flag. `redact` is PATTERN-based rather than "remove the token we hold", so it also catches a credential the process was never given - one echoed back by a server, a proxy, a redirect. A **204** is a named `NO_RUN_ID` failure, never an empty success: "the run started and I cannot tell which one" is the failure a caller is least able to diagnose from a status code. **Both routes to a run id are requested**, not just the pinned version: the `X-GitHub-Api-Version: 2026-03-10` header AND `return_run_details: true`, the optional parameter GitHub added 2026-02-19 that returns the run id on `2022-11-28` as well. Measured before it was trusted - four calls on 2026-09-21 established that the old version honours the parameter and the new version accepts rather than rejects it, so sending both is safe and a server that strips or downgrades the header still answers with a run id. That retired ADR-0031's original fallback, which was to poll `/actions/runs` and correlate by `created_at` - a race the design never actually needed. `NO_RUN_ID` now means BOTH routes were ignored, which is a far narrower diagnosis. `waiting` is reported as a status, not a stall - it means a protection rule is holding the job. `unwrapArtifact` opens GitHub's outer ZIP through the same distrustful reader, and passes a hostile or ambiguous container through untouched for the validator to refuse rather than guessing at packaging depth. Every request goes through an injectable `fetch`, so the whole module is tested offline. | ADR-0033, ADR-0032 |
125125
| `brief.mjs` | The phase-1 → phase-2 handoff: renders `research/BRIEF.md` from the corpus, and **owns the brief's shape** — `BRIEF_SECTIONS` (six sections, the two that are judged marked), the markers that distinguish *still the scaffold* from *the drafter wrote this* (`briefState` → `template \| legacy \| draft \| authored`), and the section reader (`briefSection` / `judgedSection`) that `lib/audit.mjs` consumes instead of regexes of its own. The renderer writes headings **from** that definition, so the writer and its readers cannot drift; the scaffold ships `BRIEF_FILE_MARKER`, which the substitution pass leaves alone — the alternative, matching the template's prose, made a sentence of English load-bearing. | — |
126126
| `corpus.mjs` | The research corpus: **one reader** (`readCorpus`, `readCaptures`, `parseTable`, `parseCapture`, `readOverrides`) and **one writer family** (`appendRow`, `upsertRow`, `appendJsonLine`, `writeRaw` lives in collect), plus the **cache decision** over the capture index (`cacheDecision`) — the one freshness predicate the collector, the run, and phase 0 consume. Table machinery: `splitRow`/`escapeCell`/`tableRow`/`repairRowArity`. Malformation is reported, never absorbed. The capture index has one constructor (`captureEntry`, used by `readCaptures` **and** by the collector when it has just written a page) and one writer (`rememberCapture`), so the index a run keeps in memory holds the same shape as the index read from disk — callers store the entry they are handed and shape nothing. The format's **joins** are here too: `traceOf` (evidence row → the fetch that produced it and the capture behind it), `captureOf` (row → capture, via the by-file index the reader builds), and `claimOf` (unknown → the claim it rests on, first cited row, falling back to the unknown's own text). Five consumers re-derived those joins, including the subtle "an empty Raw cell matches on the URL alone" clause; now a format change lands in one place. | ADR-0003 |
127127
| `transport.mjs` | **Adapter selection**, and the one reader of the operator's transport choice — now for **two sides** (ADR-0027). The *fetch* ladder is unchanged: `--transport`, `RESEARCH_KIT_TRANSPORT`, the config's `transport`, then auto-detection (`probeFirecrawl` — authenticated or anonymous CLI wins, otherwise keyless). The *search* ladder mirrors it: `--search-transport`, `RESEARCH_KIT_SEARCH_TRANSPORT`, the config's `searchTransport`, then one auto-detect rule — a SerpAPI key wins, otherwise **the search side IS the fetch provider**, so a machine with no key behaves exactly as before. `selectTransport` returns `{ name, adapter, why }` still meaning the FETCH side, with `search` added alongside; that is what let six call sites stay unedited. Exposes `TRANSPORTS`/`TRANSPORT_NAMES` (whole-shape adapters), `SEARCH_PROVIDERS`/`SEARCH_PROVIDER_NAMES` (a **separate** map — a search-only module in the first would make `ADAPTER_SHAPE` a claim it no longer keeps), `FETCH_SHAPE`, `SEARCH_SHAPE`, `satisfies`, `isSearchOnly`, `selectSearch`. `isSearchOnly` reads a module's declared `CONTRACTS` rather than duck-typing, because a search-only module exports `scrape` on purpose — to refuse by name. | ADR-0005, ADR-0027 |
@@ -416,7 +416,7 @@ each line — is in the ADR named beside it and in
416416
| A ZIP's structural faults are refused from the **central directory, before inflation**; the inflation ceiling is `maxOutputLength`, not a field the archive declared about itself | ADR-0032 |
417417
| **No dispatch input reaches a shell**: `${{ inputs.x }}` inside a `run:` block is substitution *before* the shell parses, so inputs arrive through `env:` and then an argv array. Enforced across **every** workflow, not only the newest | [ADR-0033](adr/0033-the-collector-refuses-rather-than-degrades.md), ADR-0020 |
418418
| The collector's spend gate is a **protected environment whose existence is proven**, because GitHub silently creates an *unprotected* one when a workflow names a missing environment; a missing credential **refuses** rather than falling back to a keyless route | ADR-0033 |
419-
| A claim's **support is weighed, not just checked**: `corroboration` reports whether a closed unknown rests on one reading, several readings of one source, or several hosts — it never fails a corpus on its own, because single-sourcing is often the right answer | [ADR-0036](adr/0036-the-gate-can-see-single-sourcing.md) |
419+
| A claim's **support is weighed, not just checked**: `corroboration` reports whether a closed unknown rests on one reading, several readings of one source, a **republished copy of one document across hosts** (`mirror`, via `lib/similarity.mjs`), or genuinely distinct documents — it never fails a corpus on its own, because single-sourcing is often the right answer. Rows are **deduped by id first**: `citedIds` returns every `E-##` mention, so prose naming one row twice used to produce it twice, and once grouping folded the duplicate it reported a "republished copy" about a row that was merely mentioned twice | [ADR-0036](adr/0036-the-gate-can-see-single-sourcing.md) |
420420
| A privacy claim is **measured, not asserted**: `bin/disclosure.mjs` re-runs the probe, and a report never lets an unchecked thing read as a clean one | [ADR-0035](adr/0035-a-privacy-claim-is-measured-not-asserted.md) |
421421
| A credential that resolves in a job declaring **no** environment is **not behind the approval gate**, and fails the run: the scope question GitHub will not answer directly is answered by asking it from a context that can see only one scope | ADR-0033 |
422422
| The vendor **package name** lives in `lib/firecrawl.mjs` (`CLI_PACKAGE`, `cliInstallSpec`), never in a workflow: the CLI is **`firecrawl-cli`**, and the npm package called `firecrawl` is the SDK and ships no binary. Both workflows installed the wrong one for weeks, and only a real dispatch could find it | ADR-0005, ADR-0033 |

‎docs/adr/0031-actions-first-delivery-and-pin-the-api-version.md‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,31 @@ reads `workflow_run_id` from the `200` body. Polling `/runs` and correlating by
5353
is the durable rule: the endpoint's contract demonstrably differs between versions, and the
5454
unpinned default is the deprecated one.
5555

56+
> **Amendment, 2026-09-21 — the fallback was wrong, and a corroborating source found it.**
57+
> Evidence: E-13/E-14 of the delivery corpus, plus four measured calls in its `BRIEF.md`.
58+
>
59+
> The rule above stands. The **fallback** does not: a caller stuck on `2022-11-28` never
60+
> needed to poll and race. GitHub added an optional `return_run_details` parameter on
61+
> 2026-02-19, and it returns `200` with `workflow_run_id` **on the old version**. Measured,
62+
> not read — `2022-11-28` + `return_run_details=true` → `200`.
63+
>
64+
> The two facts looked contradictory at first. The changelog says that *without* the
65+
> parameter the endpoint "will continue to return the current `204`", which flatly denies
66+
> what this repository had measured. Both are true: the parameter is the opt-in route on the
67+
> old version, and `2026-03-10` makes run details the **default**. Optional parameters are
68+
> non-breaking and reach every version, so making one the default is precisely the kind of
69+
> response-shape change a new calendar version exists to carry.
70+
>
71+
> `lib/dispatch.mjs` now sends **both** the pinned header and the parameter — safe rather
72+
> than merely plausible, because the fourth measured call confirms `2026-03-10` accepts the
73+
> parameter instead of rejecting it as unknown. `NO_RUN_ID` now means both routes were
74+
> ignored.
75+
>
76+
> **Worth being precise about what corroboration bought here: not a corrected error.** E-01
77+
> was right and this decision was right. A second host supplied the *mechanism and its date*,
78+
> and with them a fallback that removes a race from the design — which the corpus could not
79+
> have known while everything it had read came from `docs.github.com`.
80+
5681
**3. The artifact is labelled honestly, inside the package.** The corpus a collector
5782
produces fails its own preflight by design — a freshly scaffolded project has no unknowns
5883
in its contract, so `discovery-contract/no-unknowns` blocks it, verified by running it

‎docs/decisions/2026-09-21-delivery-architecture/research/BRIEF.md‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,3 +202,63 @@ that a second reading of the same vendor would test.
202202
2. Hand this file to the builder (phase 2). Re-running `node bin/brief.mjs`
203203
after edits will refuse without `--force` so your judgements are preserved.
204204
3. The two gaps above are the first things to close if the collector is built.
205+
206+
## Corroboration pass, 2026-09-21 - and it found a contradiction worth having
207+
208+
Seven more captures into the same ledger - nineteen entries, one unbroken chain. Six of the
209+
eight unknowns now report `independent`; two are left single-sourced on purpose and the
210+
contract says which and why.
211+
212+
The pass paid for itself on U-1. **E-13, GitHub's own changelog of 2026-02-19, appears to
213+
deny E-01 outright:**
214+
215+
> "you can pass in a new optional boolean parameter, `return_run_details`, which will return
216+
> a `200 OK` response … **If you do not pass in this parameter, it will continue to return
217+
> the current `204 No Content` status code.**"
218+
219+
E-01 and this repository's own earlier measurement say the `200` arrives from pinning
220+
`X-GitHub-Api-Version: 2026-03-10`, with no such parameter. One of those had to be wrong, or
221+
incomplete.
222+
223+
### Resolved by measuring, four calls, no parameter unless stated
224+
225+
Run against `offline-suite.yml` - a workflow that spends no credits - on 2026-09-21:
226+
227+
| API version selected | `return_run_details` | result |
228+
|---|---|---|
229+
| `2022-11-28` (default) | absent | **204 No Content** |
230+
| `2026-03-10` (pinned) | absent | **200** + `workflow_run_id` |
231+
| `2022-11-28` (default) | `true` | **200** + `workflow_run_id` |
232+
| `2026-03-10` (pinned) | `true` | **200** + `workflow_run_id` |
233+
234+
**Both sources are correct and neither is complete.** The parameter is the opt-in route, and
235+
it works on the old version - which is what E-13 documents. The new calendar version makes
236+
run details the **default**, which is a response-shape change and therefore exactly what a
237+
breaking-change version is for; E-14 supplies that rule in the same sentence that resolves
238+
the conflict: optional parameters are non-breaking and reach every version, so *making one
239+
the default* is the part that needs `2026-03-10`.
240+
241+
### What changed because of it
242+
243+
ADR-0031 told a caller stuck on `2022-11-28` to poll `/actions/runs` and correlate by
244+
`created_at` - a race it acknowledged. **There was a better fallback the whole time**, and
245+
the corpus had no way to know because nothing outside `docs.github.com` had been read.
246+
247+
`lib/dispatch.mjs` now sends **both**: the pinned header and `return_run_details: true`. The
248+
fourth row of the table is why that is safe rather than merely plausible - the new version
249+
accepts the parameter instead of rejecting it as unknown. `NO_RUN_ID` now means both routes
250+
were ignored, which is a far narrower diagnosis than "confirm your header arrived".
251+
252+
### The honest shape of what corroboration bought
253+
254+
Not "the corpus was wrong". E-01 was right, and the decision built on it was right. What a
255+
second host bought was the **mechanism and its date**, and with them a fallback that removes
256+
a race from the design. That is a better argument for corroborating than "catching errors",
257+
because no error was caught.
258+
259+
Two unknowns were deliberately left alone, and the reasoning is in the contract rather than
260+
implied by silence: U-4 because a stability marker is Node's statement about Node, and U-6
261+
because GitHub is the only witness to its own permission model - and for U-6 the stronger
262+
evidence is a measurement that already exists (`lib/disclosure.mjs`, and the
263+
`public-run-visibility` project), deliberately not filed as an evidence row because a probe
264+
is not a fetched page.

0 commit comments

Comments
 (0)