You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit c27b101
Browse filesBrowse the repository at this point in the historyBrowse files
The operator believed this was already happening. It was not: the ladder
picked ONE provider, so the second key bought a separate meter rather than a
second look.
With a SerpAPI key present the search side now asks both and merges. The
meters are genuinely separate (ADR-0027), so this costs one search on each
rather than more of either.
Round-robin rather than concatenation, and the reason is the EUDR run. One
provider returned eight US financial-regulation pages for a question about
EU deforestation - SEC, CFPB, federalregister, California water boards - and
nothing on topic. The other returned seventeen, all on topic. Concatenating
would have let the failing provider spend the entire page budget before the
working one was reached. Interleaving bounds that: each contributes its best
result, then its second, so a provider that is wholly wrong costs at most
its share.
Every merged row carries the provider that surfaced it, through the existing
rankedBy -> discoveredBy path into the ledger. That is what keeps a claim
about search quality checkable afterwards instead of being an impression. A
URL both providers return appears once and keeps its first finder, so
agreement does not inflate either one's count.
An explicitly pinned provider stays single. Pinning exists to make two runs
comparable, and a pin that quietly merged would destroy the thing it is for.
Nothing about spend changes: maxScrapes still bounds the fetches, and this
only widens the list they are chosen from.
867 -> 872 tests, including the interleave case that motivated it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/ARCHITECTURE.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -426,6 +426,7 @@ each line — is in the ADR named beside it and in
426
426
| A reviewer note is **not a citation**: `stripReviewNotes` in lib/corpus.mjs removes `[single-witness: ...]` and `[render-reviewed: ...]` before `citedIds` runs. Those notes are prose written INSIDE the same cell the citations are read from, so explaining why no second witness exists - by pointing at a row that demonstrates it - turned that row into a citation, and the unknown read as corroborated while its own note denied it. It caught its author three times in two days across three projects; the guard was right every time and the mechanism was the problem. The raw cell is untouched, because the check that reads the reason needs it | ADR-0038 |
427
427
| A finding a reviewer has **considered and overruled** is recorded, not repeated forever: `[single-witness: …]` in an unknown's Evidence cell and `[render-reviewed: …]` in an evidence row's Finding cell turn a true-but-permanent warning into a pass **that prints the reason**. A reason under 30 characters is named (`single-witness-unreasoned`) rather than obeyed, and a note left on an unknown that has since been corroborated is reported (`single-witness-stale`) so the acknowledgement cannot rot. The render note lives on the ROW because `verifyLedger` hashes the whole capture file — a capture is evidence and stays byte-immutable, a judgement about it is corpus prose |[ADR-0038](adr/0038-a-judgement-the-gate-can-read.md)|
428
428
| The collector lock is exclusive **on a local filesystem, and not promised on a network one**: Node states "the exclusive flag might not work with network file systems", a condition ADR-0020 never carried. Measured 2026-09-22 - 24 racers, 1 winner, 23 EEXIST on win32 - so the guarantee holds where it was tested and nothing detects where it is standing. Evidence: `docs/decisions/2026-09-22-wx-lock/`| ADR-0020 |
429
+
| With a SerpAPI key present the search side asks **BOTH providers and interleaves by rank** (`mergeByRank`, lib/research-run.mjs). The meters are separate, so each is asked once rather than either more. Round-robin is the point: on 2026-09-22 one provider returned eight US financial-regulation pages for a question about EU deforestation and the other returned seventeen on topic, and concatenation would have spent the whole page budget before reaching the working one. Every merged row carries the provider that surfaced it, so the ledger keeps a claim about search quality checkable. An explicitly pinned provider stays single, because pinning exists to make two runs comparable | ADR-0027 |
429
430
| The SEARCH side is **selectable at dispatch**: collect.yml takes a search_transport input (auto | serpapi | firecrawl-cli) and collect-remote.mjs a matching --search-transport, passed as RESEARCH_KIT_SEARCH_TRANSPORT. ADR-0027 built the ladder and the collector never exposed it, so a dispatched run always took whatever auto-detect picked and two providers could not be compared on one topic through the kit at all. auto becomes the EMPTY STRING, falsy in the ladder, so the default is the previous behaviour exactly | ADR-0027 |
430
431
| A **rate limit is an instruction to wait, not a failure**: `rateLimitWaitMs` (lib/firecrawl.mjs) reads the delay the vendor states in its own error text, and `collectOne` sleeps and retries, bounded at two. Firecrawl free tier allows 10 requests a minute and the collector did not throttle, so a run started inside a busy minute shed six of eight fetches and recorded them as ordinary failures. The vendor number is used rather than an invented backoff, for the reason the kit researched for Retry-After: when a server says when to come back, that is the number. Non-rate-limit errors are NOT retried - a 404 does not become a 200 if you wait | ADR-0005 |
431
432
| A collection reports **how much of the topic own vocabulary reached the captures** - `topicMatch` in lib/research-run.mjs, printed by bin/research.mjs right after collecting and deciding nothing. It exists because a run returned eight captures about US financial regulation for a question about EU deforestation and passed every structural check: no check asks whether the evidence is about the topic. Two thresholds were measured and REJECTED - per-capture 0.5 flags RFC 9728 in the agent-interface corpus, which never says MCP and is an independent witness precisely because it does not; per-corpus at-least-one-strong flags delivery-architecture, which scores identically to the genuine failure because its topic is internal vocabulary and its evidence is GitHub docs. Telling those apart needs meaning (ADR-0013), so the number is surfaced at the moment a reviewer can act on it and the grade does not move | ADR-0013 |
0 commit comments