Skip to content

Quote-level refresh: a superseded citation is rechecked by its quoted words (plan v0.4, ADRs 0191-0196) - #440

Open
StepenkoAnatoli wants to merge 25 commits into
mainfrom
quotes/quote-level-refresh
Open

StepenkoAnatoli wants to merge 25 commits into
mainfrom
quotes/quote-level-refresh

Conversation

@StepenkoAnatoli

@StepenkoAnatoli StepenkoAnatoli commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

What this changes

This builds plan v0.4 (the quote-level refresh) on top of main ab2e605. Each item is one
ADR, or a measurement under it.

  • ADR-0193, evidence-supersession/quote-survived-refresh. A citation of a row that a
    refresh replaced now passes when every usable quote of the old row is found in both captures,
    the old and the new.
    • The new capture must be readable, not an error page, and not followed by a refresh that
      found the page gone.
    • A recorded status that is not a number reopens the citation (4b-fix).
    • Every other state stays cites-superseded-row, now with a reason and a drift attribution.
    • The pass and every reopen end with the limit line: surviving words say nothing about
      whether the claim still holds.
    • No citation is moved.
    • A refresh that landed on another URL (Stage 7 gap 1, owner rulings 2026-10-26): it is
      joined on the ledger's asked-for URL, and the quote is rechecked against the current row of
      the landed URL, or the citation reopens with "asked for X ... and landed on Y". Rows first
      captured through a redirect are joined on the address their own fetch asked for, second to
      their own URL. A gone landed page, a gone through-address or a move of a move never passes.
      supersededRows and the collector are unchanged (ADR-0193 decision 8, ADR-0194 decision 1).
  • ADR-0194, evidence-supersession/cites-gone-page. A cited row fails when a later refresh
    found its page gone (404 or 410), unless its Finding names that status. The failure
    classifier moves to lib/fetch-failure.mjs, which the collector and the gate now share.
  • ADR-0192. The matcher ignores spacing beside punctuation, so a quote copied from one
    transport matches the other. No verdict of the 41 baseline corpora moves.
  • ADR-0191. The short-quote floor is measured on the compared text. No gate failure is
    removed: a quote short only on its compared text warns and is still compared.
  • ADR-0195. measure.mjs reports quote coverage of closed unknowns: 108 / 183 on the 41
    baseline corpora. It never gates.
  • ADR-0196. lead-orchestrator's kit note carries the research rules to builders and
    reviewers.
  • Plan B3. In keyless captures, a quoted > inside an attribute no longer ends a tag. Known
    limit: a closing tag written inside a quoted value still ends the element.
  • Measurements and decision corpora. The refresh baseline and movement.mjs, the
    punctuation-spacing research, the pre-registered recheck study, and the first real quoted
    refresh (warc-export: 2 pages, 2 quotes).
  • Docs. AGENTS.md (Rule 1 step 4 and the Baseline paragraph), the template AGENTS.md, and
    WORK-WITH-EVIDENCE.md.

ADRs 0192-0195 are an exception to the ADR-0157 hold, by the owner's ruling of 2026-10-25.

How it was checked

  • Every commit that touched fetchproof/ ran the full suite in its commit hook.
  • Each unit had a fresh-context reviewer, then a fix unit.
  • Phase 4b had four reviewers: spec, breaker, mutation and invariant. All 18 surviving mutants
    are now pinned.
  • Break-test (Stage 6): the baseline full suite was 2205/0. One in-range finding (movement.mjs could not be
    imported when argv[1] names no file) is fixed with a red-first test. Two intermittents are explained, not fixed (below).
  • Gap-audit (Stage 7): 16 of 19 requirements verified by it, the other two closed since. Gap 1
    (a redirected refresh escaped the recheck) is fixed in this PR after three build rounds and three
    fresh reviews; gaps 2 and 3 are pinned as KNOWN GAP tests; gap 4's shape is closed by the same
    join, and only its Firecrawl wording fact stays unverified (1 credit, not spent).
  • four-dimension-audit (fresh context, at the final head): SPEC 9/10, DESIGN 7/10, CORRECTNESS 8/10,
    QUALITY 6/10; no new S1/S2. Its main notes: checks.mjs now holds the refresh logic inline (1494
    lines), and about a quarter of the added lib lines are review-history comments. A refactor is
    left for a follow-up PR.
  • The run's traceability and report are in the run folder (not committed):
    docs/orchestration/2026-10-25-quote-level-refresh/ (TRACEABILITY.md, REPORT.md).

Full suite on the final head (6d9ed56): 2228 passed, 0 failed, in 1828 s on Windows. Root preflight: PASS.

For the owner

  1. ADR-0191's reopen trigger. Did it fire on the punctuation-spacing E-02 quote? That is a
    quote of link markup, in a corpus about markup (U1 review F2).
  2. ADR-0194's exemption blind spot. A Finding that names 404 or 410 as content is exempt
    too: 8 of 344 cited rows. This is recorded, not narrowed. Narrowing it needs a new marker
    convention.
  3. quote-survived-refresh passes are printed only with --show-pass and in --json.
    Should the gate print them by default, so the advice to re-read the new row is seen?

Follow-ups (not in this PR)

  • browser-transport.mjs:295 strips tags with the same attribute-text leak B3 fixed in the
    keyless converter.
  • The resultLinks class regex is quadratic on an unclosed <a href. This is pre-existing:
    3.5 s per 130 KB.
  • Six older bin scripts use the main-guard pattern that movement.mjs had. Reached through a
    link, they exit 0 silently.
  • The collector reads a string status such as 404 Not Found as no status. The recheck no
    longer passes on it.
  • The panel test "an oversized POST receives the 413 JSON refusal" fails about 5 in 300 runs under
    load. The break-test found the mechanism: the server closes the connection while the client is still
    uploading, so the client gets a reset instead of the 413. A proposed fix (answer after reading the body)
    ran 300/300 clean; it is outside this PR's range.
  • Under heavy machine load, the requests test auto-collect: a fresh panel start re-delivers every old result by staging the whole research/ folder before the dirty-tree check, so an operator's unfinished edits are committed and pushed under an old request's subject #325 ("a delivery held back by an earlier
    request's failed push...") failed in a hook run and in 2 of 3 group runs. Alone it takes
    29-38 s against the 60 s per-test watchdog. The break-test then reproduced a 60 s watchdog timeout
    with the test alone under load: it starts about 90 git and node processes.
  • hardening's builtin scan (withoutCommentsAndStrings) pairs quote marks without knowing regex
    literals, so a quoted name in a comment after a regex holding quotes reads as code.
  • The harness's runPending calls entry.fn() before its watchdog starts, so a synchronous
    test cannot time out.
  • normalizeForMatch link-regex quirks: \[...\](PDF) and [ [edit](url) ].
  • movement.mjs's statusOf hides found for too-short anchors.
  • From the last G1-fix review (all fail safe or wording):
    • the move-of-a-move reopen can name the wrong moved address when two addresses redirect into
      the landed page (probe W);
    • ADR-0193 decision 8 does not say that a later 404 of the old address is ignored once the
      row's own URL has been fetched (the K case);
    • a move passes on an older capture when the landed page's newest capture has no row (probe U,
      the same class as a refresh with no row);
    • movedCitation's docstring omits "and the landed page has not moved on since".
  • Struck-through quotes (~~...~~) and text changed inside an ellipsis gap still survive the
    recheck (pinned as KNOWN GAP tests).
  • Structure (four-dimension-audit DESIGN 1, QUALITY 1-3): move the refresh-judgement functions out of
    checks.mjs into their own module, and trim the review-history docstrings to the rule and an ADR pointer.
    Left until after merge, so three review rounds and the mutation evidence stay valid.
  • Work the plan leaves for later:
    • the misleading-quote suite;
    • the section 10.5 reviewer measurement;
    • the section 12 trials.

StepenkoAnatoli and others added 25 commits October 25, 2026 07:47
What changed: a new nested project, docs/decisions/2026-10-25-first-quoted-refresh/,
a copy of 2026-09-28-warc-export refreshed once with Firecrawl (research.mjs --force,
2 pages, 2 credits). It holds both old and new captures, the extended ledger (seq 4-5),
the researcher's review (E-03/E-04 rewritten with the same quotes, U-01..U-03 moved by
hand with a note), a redrafted brief, and REFRESH-RECORD.md. Indexed in
docs/decisions/README.md.

Why: plan v0.4 step 3. The kit had 27 historical refreshes and none with a quoted row,
so ADR-0193's quote-level recheck had no real case to be built against. The owner chose
warc-export (2 pages, 2 quotes) in the Mandate reply of 2026-10-25.

What it touched: docs only - the new project and one index row. No product code.

What you verified: preflight PASS (0 blocking, 0 warnings, 17 passing), brief.mjs
--state "authored", handoff OK (5 ledger entries, chain verifies). Both anchors were
measured with the kit's quoteAnchors / quoteTooShort / anchorFound: not short, found
in the old and the new capture. The WARC spec body is byte-identical apart from front
matter; the warcio README changed (35099 -> 24036 bytes) and still has the quote.
Untested: nothing that runs code depends on this project yet (U8 will).
Expected: the U3 baseline counts 41 corpora at ab2e605; this project is a 42nd, so the
movement script reports it as a new corpus, not as movement in an old one.

What you got wrong:
- What: the refresh ran before the pre-refresh copy was committed, against the plan in
  my notes.
- Impact: none on the evidence - the pre-refresh state is the committed warc-export
  project at ab2e605, and REFRESH-RECORD.md records the just-refreshed state verbatim.
- Cause: I ran the refresh first.
- Fix: the record names how to rebuild the state; U8's tests rebuild it from there.
- Same mistake elsewhere: none.
- Re-verified: preflight and handoff above.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pt (plan §4.2-4.3)

What changed: new docs/measurements/2026-10-refresh-baseline/. movement.mjs runs the
in-process runPreflight verdict, the quote anchors and the supersededRows pairs over every
indexed corpus. With --write it writes summary/verdicts/anchors/superseded.json; by default it
prints each movement against them (exit 0 none, 1 some, 2 not measurable). The README says what
was measured, the counts, what is excluded and what a zero does not prove.

CHANGELOG: one Unreleased line.

Why: R-05/R-06. Every later matcher or gate change in this run must reproduce this baseline or
list its movement (Testimonium's method).

What it touched: that folder and CHANGELOG.md only. No declared code path. No ADR, because no
alternative was set aside that a later reader would need.

What you verified:
- verified: --write at ab2e605 gives 41 corpora, 417 rows, 213 quoted rows, 517 anchors (all
  found, 0 too short), 27 superseded pairs (0 quoted, 0 cited), and 41 PASS.
- verified: compare printed "no movement" with exit 0, run from the worktree root and from a
  folder outside the repo. A second --write from another cwd was byte-identical (cmp), with no CR
  bytes.
- verified: the quoteTooShort every->some mutation printed 3 movements with exit 1, and was
  reverted.
- verified: the kit-vs-prompt corpus matched the CLI preflight.
- untested: a Linux run, and Node 26.

What you got wrong and fixed:
- What: compare mode measured all 41 corpora before it read the baseline file.
- Impact: a missing baseline took more than a minute to report. Slow error only; nothing
  delivered.
- Cause: the baseline read came after the measure.
- Fix: the baseline is read first.
- Same mistake elsewhere: none. --write reads nothing first.
- Re-verified: not re-run with a missing baseline, because the files now exist. Expected: exit 2
  at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lan §12)

What changed: a new nested project, docs/decisions/2026-10-25-refresh-recheck-study/ (ADR-0030).
- research/PROTOCOL.md: the question; arms A and B, with arm C unavailable because ADR-0145's
  comparison has not run; tasks T1-T6 over the five page-change kinds; the three numbers with
  their denominators; scoring; the hidden-suite list (each "to write before trial 1", with the
  blind-author rule); decision rules DR-1..DR-6; a traceability table from each plan §12 line.
- One evidence row: arXiv 2604.09515 (the API-change classes), collected keyless.
- Its row in docs/decisions/README.md (pending), and a CHANGELOG line.

Why: plan v0.4 §12, R-09. The decision rules are fixed before any trial, so a later result cannot
move them (the discipline of ADR-0145's comparison). It does not amend ADR-0145's PROTOCOL.md,
whose amendment rule allows rubric defects only.

What it touched: that folder, docs/decisions/README.md and CHANGELOG.md. No product code.

What you verified:
- verified: preflight in the project gives PASS, 0 blocking, 3 warnings (transport-not-metered,
  partial-only, unknown-attempted U-2); handoff OK.
- verified: the collection spent 0 Firecrawl credits (http-keyless; account 76 before and
  after).
- untested: the trials themselves; none is run by this commit.

What you got wrong and fixed:
- What: the first scaffold had no ledger. A nested project cannot pass the gate without
  collected evidence, and the builder's brief forbade collection.
- Impact: blocked before commit. Nothing delivered.
- Cause: the brief assumed that a scaffold whose unknowns are all KNOWN-UNKNOWN would pass.
- Fix: the lead collected the one external fact the protocol rests on, keyless, at 0 credits
  (RUN.md D-g). Rejected: prior.mjs (permanent text, needs the owner) and committing it failing
  as pending (the PR could not merge).
- Same mistake elsewhere: none. The other new project (punctuation spacing) was collected.
- Re-verified: preflight PASS and handoff OK, as above.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…DR-0192

What changed: a new nested project, docs/decisions/2026-10-25-punctuation-spacing/ (ADR-0030).
It contains:
- the contract, with U-01..U-03 CLOSED;
- 9 evidence rows with quotes: the Python `re` page and Wikipedia "Percent sign", each collected
  through both http-keyless and firecrawl-cli from the same HTML, plus Firecrawl's docs page and 4
  converter source files;
- an authored brief with a per-mark table;
- measure-spacing.mjs, a read-only measurement script.
Also its row in docs/decisions/README.md (reference) and a CHANGELOG line.

Why: ADR-0192 proposes a matcher-side spacing rule. Plan §5.3 and Mandate choice C6 ask that it
rest on the measured output of both transports.

What it touched: that folder, docs/decisions/README.md and CHANGELOG.md. No product code, no ADR.

What you verified:
- verified: preflight in the project gives PASS, 0 blocking, 9 warnings (transport-not-metered
  x6, partial-only U-01, duplicate-url E-08, single-source U-02).
- verified: handoff OK, 11 ledger entries; brief.mjs --state prints authored.
- verified: the lead re-ran preflight, handoff and brief --state in the researcher's worktree
  and in this checkout.
- verified: measure-spacing.mjs reproduces the brief's numbers.
- verified: Firecrawl credits went from 76 to 74 (2 scrapes; budget 4).
- verified: a scan for keys, tokens and the owner's email in the project is clean.
- untested: what Firecrawl's postProcessMarkdown does to whitespace, and which conversion path
  the hosted API runs.

What you got wrong and fixed:
1. DISCOVERY Evidence cells named the superseded rows E-02 and E-08 in prose.
   - Where: DISCOVERY.md, U-01 and U-02.
   - Impact: preflight FAIL, cites-superseded-row x2. Nothing delivered.
   - Cause: the gate parses every E-## in the cell.
   - Fix: the ids are replaced by a pointer to the brief.
   - Verified: preflight PASS.
2. Scripted edits left 1372 stray characters before the title.
   - Where: DISCOVERY.md line 1.
   - Impact: a garbled heading; the gate still passed.
   - Cause: failed `node -e` and close.mjs runs.
   - Fix: the prefix was stripped.
   - Verified: the heading reads correctly, and preflight PASS.
3. Two quotes were under the quote floor, and one missed a space.
   - Where: E-05, E-07 and E-02.
   - Impact: quote warnings.
   - Cause: the quotes were copied loosely.
   - Fix: lengthened and recopied.
   - Verified: every quote found.
Same mistake elsewhere: the run's other new project carries no superseded-row id in its
DISCOVERY prose (its preflight passes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…191)

What changed: quoteTooShort takes words, ADR-0176's weight and length on
normalizeForMatch(f) instead of the raw fragment; one documented branch keeps the raw
measure for an anchor with no compared text. quoteChars (no lib/bin caller) is removed;
its test pin goes through quoteTooShort at 39 and 40 characters.

Why: the floor measured the raw marker while anchorFound compares the normalized text, so
[Free plan](https://...#free-plan-details), zero-width padding and a formatting-only
fragment cleared the floor and anchored "free plan" (plan 5.1, R-01/R-02; contradicts
ADR-0087/0126). Set aside: all-empty anchors short (four quote-not-found fails would only
warn), all-empty never short ([quote: ***] warn -> fail), an empty check in checks.mjs.

What it touched: fetchproof/lib/quotes.mjs, fetchproof/test/quotes.test.mjs, ADR-0191 and
its index row, CHANGELOG, docs/ARCHITECTURE.md (quotes.mjs row), fetchproof/README.md
test count (2119 -> 2122), and docs/measurements/2026-10-refresh-baseline/MOVEMENT-2026-10-25-B1.md
(the movement this change makes in the baseline).

What you verified: verified - selftest quotes checks measure 112 passed 0 failed;
repo-hygiene 12 passed; the compared-text test red on the unchanged code and with the
normalization removed; the all-empty pin red with its branch removed (and so is the
untouched 2026-10-08 test). Untested before this commit: the full suite (runs in the hook).
verified - movement.mjs compare after the change: no anchor, finding or verdict moved in the
41 baseline corpora (517 anchors); only the new fixture corpus is listed as added
(MOVEMENT-2026-10-25-B1.md). So no corpus gains a warning from this change today.

What you got wrong:
- What: the builder's first report called the raw branch a loophole and stopped.
- Impact: none delivered; one round trip.
- Cause: an all-empty long anchor was not traced on to anchorFound.
- Fix: lead ruling D-d (RUN.md), built as ruled.
- Same mistake elsewhere: none found.
- Re-verified: the 2026-10-08 test is green and untouched.
- What: the first commit attempt was stopped by the hook's 3900 s watchdog before the suite
  reported.
- Impact: nothing committed; one retry.
- Cause: several builders ran test groups on this PC at the same time, so the suite ran past
  the default watchdog.
- Fix: the retry sets FETCHPROOF_GATE_TIMEOUT (the hook's documented watchdog, ADR-0142);
  the suite still has to pass.
- Same mistake elsewhere: every later fetchproof/ commit in this run sets the same watchdog.
- Re-verified: this commit's hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ilders and reviewers (ADR-0196)

What changed: the kit note gets section 5.
- Phase 2 names E-## rows.
- Each builder brief pastes the unit's E-## rows and the cite-in-code and contract-tests rules.
- A research reviewer role is added: fresh context, read-only, five checks, findings to RUN.md
  and the researcher. It has no gate token and no approval condition.
- The 4b spec reviewer gets a research-conformance duty.
- The router names the default build path and marks lead-orchestrator optional, for parallel
  units.
- Four pin tests.

Why: a parallel builder never saw cite-in-code or contract-tests, and research had no independent
reviewer (principle 4). Plan v0.4 §10, approved by the owner 2026-10-25. Rejected: the gate-token
variant, edits to received text, restating the protocol, and a new skill (all in ADR-0196).

What it touched: the two kit-authored skill files, test/skill-set.test.mjs, ADR-0196 and its
index row, CHANGELOG, and the README test count (+4). No declared code path, so no ARCHITECTURE.md
change. No hash, manifest or NOTICE pin reads either skill file.

What you verified:
- verified: skill-set 22 passed, 0 failed. Red first: 18 passed, 4 failed, each for its intended
  reason.
- verified: each new test failed with its guard removed (7 mutations, one skill-set run each,
  21/1).
- verified: repo-hygiene and installer-project-guard are green with the index row in.
- verified: the full suite, in this commit's hook.

What you got wrong and fixed:
- What: the first router paragraph said the default path "needs no other skill".
- Impact: it contradicted the table's "careful-coding, always" row. Nothing was delivered with
  it.
- Cause: "default path" was overstated.
- Fix: the phrase is removed.
- Same mistake elsewhere: the kit note's section 5 was checked; it names no skill as the only
  one needed.
- Re-verified: skill-set 22/0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…R-0191, D-l)

What changed: checks.mjs skips `anchorFound` for a short quote only when `quoteTooShortOnRaw`
(new in quotes.mjs) agrees. That is the measure from before ADR-0191. The quote-too-short
message now says that the count is taken on the compared text.
- ADR-0191 gains decision 5 and the review's corrections.
- The Status lines of ADR-0126 and ADR-0162 name ADR-0191.
- The B1 movement note now covers the corpora outside the 41-corpus baseline.
- The CHANGELOG entry for ADR-0191 is corrected.

Why: at d6b3e5d, a quote long on its raw text but short on its compared text warned and was
never compared. So an invented `[quote: [Enterprise plan](https://...)]`, which used to fail
`quote-not-found`, passed the gate with a warning (U1 review, F1). That broke the plan's stated
effect, "only adds warnings, in the strict direction", which ADR-0191's classification as a bug
fix rests on.
Set aside: accepting the move from fail to warn. It removes a gate failure in a bug fix (RUN.md
D-l).

What it touched:
- fetchproof/lib/quotes.mjs and fetchproof/lib/checks.mjs (the quote-anchor loop only);
- fetchproof/test/quotes.test.mjs (+2 tests; the U+200B literals become escapes);
- docs/adr/0191, 0126 and 0162;
- docs/measurements/2026-10-refresh-baseline/MOVEMENT-2026-10-25-B1.md;
- CHANGELOG.md, and the quotes.mjs row of docs/ARCHITECTURE.md;
- the count in fetchproof/README.md, 2126 -> 2128.

What you verified:
- verified: builder ran `selftest quotes checks measure`, 114 passed, 0 failed, and
  `repo-hygiene`, 12/0. Lead re-ran `quotes checks`: 109 passed, 0 failed.
- verified: the new gate-level test fails on d6b3e5d's library, and with its guard removed in
  either direction. The F3 test fails with the compared measure removed.
- verified: movement.mjs prints no line for the 41 baseline corpora.
- verified: full runPreflight over 44 corpora, with the kit at 3282cf3, at d6b3e5d and at this
  tree. One anchor moves (punctuation-spacing E-02: found, and it warns). Every verdict is PASS.
- untested: the full suite before the hook. The hook runs it on this commit.

What you got wrong and fixed:
- What: ADR-0191, its code comment, its CHANGELOG entry and commit d6b3e5d said that B1 "removes
  no failure" and "only adds warnings". Both were false.
- Where: quotes.mjs (the `quoteTooShort` comment), checks.mjs (the `continue` after the short
  warning), ADR-0191 and the CHANGELOG.
- Impact: an invented quote that is long only in markup passed the gate at d6b3e5d. No corpus
  has one. NFKC also lengthens a few contrived quotes, which were then compared instead of
  warned.
- Cause: the `continue` after the warning was not traced for quotes that are long on raw text.
  The test of "never a new failure" used a capture that contains the quote, so every case in it
  was found.
- Fix: the comparison is skipped only when the quote is short on its raw text as well. The
  texts are corrected, and the other direction is recorded.
- Verified: the gate-level test above.
- Same mistake elsewhere: d6b3e5d's commit message cannot change; the CHANGELOG and ADR now
  carry the correction. U8's recheck excludes too-short quotes. That keeps the citation failing,
  the strict direction, and its unit review checks it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What changed: evidence-supersession/cites-gone-page fails an unknown whose cited row's URL last
answered 404 or 410, in a ledger fail entry after the row's own scrape. The failure classifier
(FAILED_STATUS, GONE, the standing and deterministic tables, isRateLimited) moves from
collect.mjs to the new lib/fetch-failure.mjs, which imports nothing. collect.mjs, firecrawl.mjs's
rateLimitWaitMs and checks.mjs all read it.

Why: a refresh that hit a deleted page left the old row current and the gate green. That is the
quiet failure ADR-0026 was written to prevent, reached by the one refresh outcome it never
looked at. Plan v0.4 §8, R-10. The owner's ruling of 2026-10-25 lifts ADR-0157's hold for
0192-0195.

What it touched:
- lib/checks.mjs (the supersession check), lib/collect.mjs (the classifier moved out; behaviour
  unchanged), lib/firecrawl.mjs (rateLimitWaitMs calls isRateLimited, the same regex), and the new
  lib/fetch-failure.mjs.
- test/checks.test.mjs: 12 tests.
- ADR-0194 and its index row, ARCHITECTURE.md (module list), CHANGELOG, README test count (+12).

How it decides:
- Ledger position (ledgerRank, urlKey), not dates.
- The gate imports fetch-failure.mjs, never the collector.
- The raw-error-status exemption is per status.
- It fails under every policy.

What you verified:
- verified: red first. On the unchanged checks.mjs, 7 failed and 91 passed; every failure was a
  positive case.
- verified: each of the 12 tests failed with its guard removed (G1-G11 in the builder report).
  G1, G2, G3, G7, G8 and G9 were re-run after the refactor.
- verified: groups checks, collect, collector-workflow, live-collect, preflight, gate, handoff,
  architecture-map, hardening and transport: 608 passed, 0 failed, before and after the
  refactor.
- verified: a scan of the 41 indexed corpora gives no cites-gone-page finding. There are 8 gone
  fail entries in 4 corpora, and none is the last word on a cited row.
- verified (lead): fetch-failure.mjs's isRateLimited is the regex firecrawl.mjs had,
  /rate limit exceeded/i on String(x ?? ''), so the wait loop is unchanged.
- verified: the full suite, in this commit's hook.
- untested: a live refresh against a real 404.

What you got wrong and fixed:
- What: firecrawl.mjs was edited outside the unit's owned files.
- Impact: none on behaviour (same regex). The lead ruled to keep it, so the pattern has one
  source (RUN.md D-h).
- Cause: the shared classifier would otherwise leave a second copy of the rate-limit pattern.
- Fix: kept and named in ADR-0194's Area.
- Same mistake elsewhere: no other file outside scope (git status was checked).
- Re-verified: transport and live-collect groups green.
- Process slips, each re-run: a heredoc append wrote nothing; mutation scripts lost text to shell
  escaping; the first date mutation was itself buggy and broke 8 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the tag (plan B3)

What changed: stripTags in lib/http-transport.mjs ends a tag at the first '>' outside a quoted
value opened right after '='. A quote with no partner before the last '>' is read as text, so
the scan stays linear; it uses cached next-'>' and next-'=' lookups. The oracle fixture
test/fixtures/regex-extract.mjs gets its own regex for the new rule, written independently of
lib/. There are 2 new tests in transport.test.mjs and 3 new timing shapes in
http-linear.test.mjs.

Why: plan §5.2 B3 (R-03, R-04). `<span title="Tier 3 > unlimited API requests">` leaked
`unlimited API requests">` into keyless captures: quotable text that no reader sees.
Rejected: failing a tag on an unterminated quote (quadratic, 7-25 s per 100 KB). Only future
keyless captures change; existing captures are records and are not rewritten. A bug fix with
one obvious remedy, so there is no ADR.

What it touched: lib/http-transport.mjs, test/transport.test.mjs, test/http-linear.test.mjs,
test/fixtures/regex-extract.mjs; docs/ARCHITECTURE.md (http-transport row), CHANGELOG, and the
README test count (+2).

What you verified:
- verified: `selftest transport http-linear large-response` gives 138 passed, 0 failed.
- verified: red first. Each new test failed on the old code. The mutations "quote opens
  anywhere", "count each '<' inside the tag" and "fail the tag, try next '<'" each fail a
  named test; the last makes the timing test 11.9x slower (4.8 s -> 57 s).
- verified: speed. On base's 7 timing shapes the new scanner runs at base speed. The
  http-linear group is 43-45 s against 32-35 s on base (interleaved runs); the difference is
  the 3 added shapes, which are kept as guards (RUN.md D-j).
- verified: the full suite, in this commit's hook.

What you got wrong and fixed:
1. A test expected the wrong output.
   - Where: transport.test.mjs, the new '>' test.
   - Impact: the test was red on correct code. Nothing delivered.
   - Cause: the test expected `It's rare:`, but a stripped tag leaves a space (the B2 spacing
     issue, ADR-0192). Only http-linear was run after implementing, not transport.
   - Fix: the span was moved.
   - Verified: red on the old code, green now.
2. The round-1 scanner was slow.
   - Where: stripTags.
   - Impact: http-linear slowed to 68.8 s; the lead's review caught it before integration.
   - Cause: a regex and an array were built for every tag.
   - Fix: cached next-'>' and next-'=' lookups.
   - Verified: per-test timings in the builder's round-2 report.
   - Same mistake elsewhere: browser-transport.mjs:295 has the same `/<[^>]*>/g` defect,
     left as a follow-up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What changed: measureCorpus returns quoteCoverage {count, of, percent}, the same shape as
quotesFound. renderMeasure prints, after "closed unknowns", the line "quote coverage  N / M CLOSED
unknowns cite at least one row with a usable quote (not too short, ADR-0191) found in its capture
- a coverage metric, not truth". Usable and found are the citations check's own predicates
(quoteTooShort, anchorFound over captureOf's capture, including the byUrl fallback for a blank
Raw). A CLOSED unknown counts once. It never gates; exit codes are unchanged. bin/measure's help
text names the line. --json carries quoteCoverage.

Why: plan v0.4 sections 2 and 9 (R-15). The dossier's 108 / 183 had never been re-counted. The
owner's ruling of 2026-10-25 lifts ADR-0157's hold and ADR-0117's freeze for 0192-0195. Rejected:
counting rows, any quote marker, a second floor, Raw-only resolution, and a gate or ratchet (all
in ADR-0195).

What it touched:
- lib/measure.mjs, bin/measure.mjs (help text), test/measure.test.mjs (7 tests).
- ADR-0195 and its index row, ARCHITECTURE.md (the measure.mjs row), CHANGELOG, and the README
  test count (+7, to be confirmed against the full run).
- Deviation named in the report: the label is "quote coverage" in the house format, not the
  plan's "Quote coverage:".

What you verified:
- verified: red first. On the unchanged lib all 7 new tests failed (field undefined) and the 5
  old ones passed.
- verified: selftest measure: 12 passed, 0 failed. selftest measure cli: 163 passed, 0 failed,
  0 UNSUP.
- verified: 7 guard mutations, each run on the measure group and then restored; each failed its
  named test (per-unknown "some", the too-short floor, "found", once-per-unknown, the CLOSED
  filter, the captureOf body for a blank Raw, M = all CLOSED).
- verified: re-count. 108 / 183 on the 41 baseline corpora, which reproduces the dossier exactly;
  7 / 7 on the three 2026-10-25 projects; 115 / 190 on all 44. The looser any-marker count equals
  the strict one in every corpus.
- verified: real runs. The repo root gives 0 / 11. docs/decisions/2026-09-28-quote-anchors gives
  3 / 3.
- verified: git diff --check is clean.
- expected: repo-hygiene and the full suite, in this commit's hook, with the index row in.
- untested: Linux, Node 26.

What you got wrong and fixed: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What changed: normalizeForMatch (lib/quotes.mjs) removes whitespace before . , ; : ! ? % ) ] }
and after ( [ {, as its last step. quoteAnchors is untouched. There are 17 new tests in
test/quotes.test.mjs and the new ADR-0192.

Why: the keyless converter spaces every inline tag it strips and Firecrawl does not, so a quote
copied from one transport's capture was quote-not-found in the other's capture of the same page
(research docs/decisions/2026-10-25-punctuation-spacing, E-01/E-02 against E-08/E-09). ADR-0193's
recheck compares across transports. Plan v0.4 section 5.3, B2. The owner's ruling of 2026-10-25
lifts ADR-0157's hold and ADR-0117's freeze for 0192-0195. Rejected: a converter-only fix, comma
deletion and number folding, quote marks, the space after "." in tokenized code, the space before
"(". Deferred: whitespace Firecrawl drops, the \[..\](PDF) link quirk (all in ADR-0192).

What it touched:
- lib/quotes.mjs (normalizeForMatch only) and test/quotes.test.mjs.
- ADR-0192 and its index row, CHANGELOG, ARCHITECTURE.md (the quotes.mjs row), and the README test
  count (+17, to be confirmed against the full run).
- docs/decisions/README.md: the row for 2026-10-25-punctuation-spacing changes status from
  reference to active.
- Callers of the changed function, listed in the ADR: anchorFound (the citations check,
  measure.mjs, movement.mjs), quoteTooShort (ADR-0191's floor) and measure-spacing.mjs.

What you verified:
- verified: red first. On the base with the tests added, selftest quotes gave 21 passed, 16
  failed, each for the intended reason ("not found" or "not equal"). The not-rescued pin passed
  on the base, as a pin should.
- verified: selftest quotes: 37 passed, 0 failed. selftest quotes checks: 124 passed, 0 failed.
- verified: guard mutations, each restored. Each of the 10 closing marks removed, each of the 3
  opening marks removed, whitespace also removed after a closing mark, "(" added to the closing
  set, the straight quote added to each set, the pilcrow added to both sets, and the
  before-closing removal deleted: each failed its own test.
- verified: movement.mjs gives 47 lines and exit 1, identical with and without the rule. All 47
  are "added" lines for the 3 corpora committed after ab2e605. There is zero movement in the 41
  baseline corpora.
- verified: measure-spacing.mjs counts 0 everywhere now, because the rule removes the spaced
  passages it looks for. Recorded in the ADR. The research project was not edited.
- verified: the full suite, the measure group included, in this commit's hook. The builder ran
  neither; a static check found no spaced punctuation in the measure group's quotes.

What you got wrong and fixed:
- Mistake: the not-rescued test did not pin quote marks.
  - Where: test/quotes.test.mjs, the "does not rescue quote marks..." test.
  - Impact: adding the straight quote to the closing set left all 37 tests green, so a widening
    to quote marks would have passed unnoticed. Nothing was delivered with it.
  - Cause: the f-string pair also differs at "error (" and "' {", which no rule reaches.
  - Fix: two gloss pairs, each differing on one side of a quote mark only.
  - Verified: with the straight quote in the closing set, then in the opening set, each run gave
    36 passed, 1 failed, naming its pair. The clean run is 37 passed, 0 failed.
- Mistake: the ADR's examples `[Mexico](...)`, `[edit](...)` and `\[ _Guide_\](PDF)` read as
  relative links to files that do not exist.
  - Impact: the first commit attempt's hook suite went red (repo-hygiene, 1 failed of 2166), so
    nothing was committed.
  - Cause: the ADR body link check skips only scheme links, and code spans are not exempt.
  - Fix: `(https://...)` in the examples, and the PDF quirk described as `\[ _Guide_\]` followed by
    `(PDF)`. Verified: repo-hygiene 12 passed, 0 failed.
- Same mistake elsewhere: the other not-rescued pins were mutated the same way (the "(" pair, the
  pilcrow pair, whitespace after a closing mark); each failed its own test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… passes (ADR-0193)

What changed: evidence-supersession rechecks each citation of a superseded row R that row N
replaced. It passes the new quote-survived-refresh only when all of these hold:
- every usable anchor of R (quote-too-short excluded, ADR-0191) is found in R's own capture and
  in N's own capture;
- N's capture is readable and is not an HTTP >=400 page;
- no refresh after N found the page gone.
Every other state stays cites-superseded-row. Its detail now carries the reason, the plan 7.3
drift attribution and the limit line. The citation is never moved. One state the plan's table does
not list (RUN.md D-k, accepted): when the page is found gone after N, the citation reopens beside
cites-gone-page instead of passing beside it; it has its own test. The registry "about" text for
the check now says "unless its quoted words survived the refresh (ADR-0193)".

Why: plan v0.4 section 7 (R-11..R-13). The first real quoted refresh failed 3 citations whose
words survived. The owner's ruling of 2026-10-25 lifts ADR-0157's hold and ADR-0117's freeze for
0192-0195.

What it touched:
- lib/checks.mjs, including the registry "about" text (line 1269) for evidence-supersession,
  changed from "no unknown still rests on a row a later capture replaced" to "no unknown still
  rests on a row a later capture replaced, unless its quoted words survived the refresh
  (ADR-0193)".
- test/quote-recheck.test.mjs (new, 20 tests), and four reader pins: 2 in test/checks.test.mjs, 1
  in test/evidence-context.test.mjs, 1 in test/handoff.test.mjs.
- Test 12 ("Firecrawl to keyless, quote lost only to spacing"): U9 landed first, so
  TRANSPORT_SPACING_OUTCOME in test/quote-recheck.test.mjs is flipped from
  'cites-superseded-row' to 'quote-survived-refresh'.
- ADR-0193 and its index row, ARCHITECTURE.md (the checks.mjs row), CHANGELOG, and the README test
  count (+24, 2166 -> 2190, counted by running the groups: quote-recheck 20, checks 2, evidence-context
  1, handoff 1).

What you verified:
- verified: red first. selftest quote-recheck gave 1 passed, 18 failed before any code change; the
  one pass was test 14, which U6 already covers. The real-fixture test showed exactly 3
  cites-superseded-row.
- verified: selftest quote-recheck: 20 passed, 0 failed, before the flip.
- verified: groups checks, quote-recheck, quotes, evidence-context, handoff, preflight and gate:
  302 passed, 0 failed, before the flip.
- verified: 26 guard removals, each red (the table is in the builder report).
- verified: movement.mjs gives 47 movements and exit 1, identical before and after the change.
  All 47 are "added" lines from the 3 corpora outside the baseline. There is zero movement in the
  41 baseline corpora, and none of those 3 cites a superseded row.
- verified (lead): with U9's quotes.mjs and the flip, selftest quote-recheck checks gives 121
  passed, 0 failed.
- verified (lead): groups quote-recheck checks evidence-context handoff on the staged tree:
  184 passed, 0 failed, against 160 for checks evidence-context handoff at 2d831ca (+24).
- verified: the full suite, in this commit's hook (the builder did not run it).
- untested: the README and doc text beyond the gate's own checks.

What you got wrong and fixed:
- Mistake: regex escapes were lost when a pin was written through a shell heredoc.
  - Where: survivedThroughKeyless, in test/checks.test.mjs.
  - Impact: the pin's own state assertion went red. Nothing delivered was affected.
  - Cause: heredoc escaping.
  - Fix: rewritten with Edit.
  - Verified: the pin is green and its mutation is red.
- Same mistake elsewhere: the same cause produced a syntax error in a test title in
  test/handoff.test.mjs. Fixed the same way and re-run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he gate's import isolation is pinned (ADR-0194)

What changed: the cites-gone-page message now says what clears it: "record in E-y's Finding that
the page now answers HTTP <status> and why the earlier reading still holds". A new checks test
reads the static import graph of lib/checks.mjs and bin/preflight.mjs: no collector or transport
module, no network builtin, no dynamic import(), no child_process below checks.mjs, and
fetch-failure.mjs imports nothing. ADR-0194 records the exemption's blind spot and its reverse
trigger. freshness-recheck step 4, README and ARCHITECTURE name cites-gone-page, and the
evidence-supersession registry text names a page a refresh found gone.

Why: U6 review F1-F4 (S3; RUN.md D-n). The old remedy left the gate red when it was followed
literally. Nothing pinned decision 6, so an import of firecrawl.mjs kept 132 tests green. The
docs described only the superseded case. F1's exemption is recorded, not narrowed: the plan
mandated the raw-error-status exemption, and a narrower marker is a new convention, left to the
owner.

What it touched: lib/checks.mjs (message and registry about text), test/checks.test.mjs (+1
test, 1 pin updated), ADR-0194 (decision 1 quote, Consequences, trigger) and its index row,
docs/ARCHITECTURE.md (checks.mjs and firecrawl.mjs rows), skills/freshness-recheck/SKILL.md,
README.md, CHANGELOG, fetchproof/README.md test count (+1).

What you verified:
- verified: the updated message test failed first on the old code (98 passed, 1 failed).
- verified: the import test failed alone under 6 mutations, and with an import of firecrawl.mjs
  in checks.mjs inside the checks group (99 passed, 1 failed); restored.
- verified: selftest checks architecture-map repo-hygiene skill-set: 155 passed, 0 failed.
- verified: git diff --check clean.
- verified (lead, after merging with U8 93e2d04): selftest checks repo-hygiene architecture-map:
  135 passed, 0 failed. The ARCHITECTURE checks.mjs row and the checks test tail conflicted with
  U8; both sides kept (ADR column ADR-0193, ADR-0194).
- verified: the full suite, in this commit's hook.
- untested: Linux and macOS.

What you got wrong and fixed:
- Mistake: the brief asked for no child_process in preflight's import closure.
- Where: the closure, via gate.mjs, handoff.mjs and machine.mjs, which spawn git and the suite.
- Impact: none delivered; as written, the test would be red on correct code.
- Cause: the review's hand count listed kit modules only.
- Fix: child_process is refused below checks.mjs only; network builtins are refused in both.
- Verified: the test passes on the tree and fails under each mutation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…NCE.md path, and its pins catch an added gate

What changed: the kit note's builder and spec items name cite-in-code's EVIDENCE.md path outside
the root, and the reviewer runs "after preflight PASS, at Phase 1b's exit, before Phase 2 relies on
the brief". The router names the two rules lead-orchestrator's briefs carry. ADR-0196 cites the
owner's approval of plan §15 (row 1c, "free"), and its index row takes the new moment. The
reviewer test also refuses sign-off and approval wording.

Why: U4 review F1-F5 (all S3). A gate sentence added to the reviewer item kept skill-set green;
"an E-## comment" dropped the project path that IDs repeated across Phase 1b's projects need;
"those rules" overclaimed; "before handoff" named two things; the freeze ground went uncited.

What you verified: verified: skill-set + repo-hygiene + installer-project-guard 44 passed, 0
failed; each new assertion failed with its guard removed (5 mutations, 21/1 each); git diff
--check clean; the full suite, in this commit's hook. No test added, so the README count is
unchanged.

What you got wrong: the builder pin first used [^-]*, which "research-derived" would have broken,
and a scripted edit wrote unescaped quotes into three messages; both were fixed before any run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ues (B3 review F1-F3)

What changed: tagPairs now ends each opening tag with tagEnd, the stripper's quote-aware rule.
That covers li, h1-6, tr/td, strong/b, em/i, code, links (LINK and resultLinks), the
div/main/article/section content block and the title/h1. A quoted value is read only after a
'<' that starts a tag (a letter, '/', '!' or '?'); any other '<' ends at its first '>', as
before B3. The scan stays linear: tag ends are memoised per '=' only where a tag starts inside
the last one, and closing tags and href hits are each listed once. The test oracle
(fixtures/regex-extract.mjs) gets the same rules, written independently.

Why: the U2 review (RUN.md D-p). F1 (S2): only stripTags was quote-aware, so
`<li title="Tier 3 > unlimited API requests">` and the other named passes still leaked attribute
text, and the CHANGELOG overclaimed. F2: `a <= "x > y" and more` lost its sentence. F3: stale
oracle and test headers. Rejected: reading a non-tag '<' as text, as HTML does. It changed a
corpus page (the Crossref mbio record, 569 -> 565 graded words). One remedy with no credible
alternative, so no ADR.

What it touched:
- lib/http-transport.mjs (a declared code path).
- test/transport.test.mjs (+3 tests), test/http-linear.test.mjs (3 new timing shapes inside the
  existing timing test), test/fixtures/regex-extract.mjs.
- CHANGELOG (U2's B3 entry rewritten to the wider claim), ARCHITECTURE.md (the http-transport
  row's B3 sentence), the README test count (+3).

What you verified:
- verified (builder): groups transport http-linear large-response hardening redirect-guard
  runtime: 231 passed, 0 failed. Red first: the 3 new tests fail on 5053d9a, and each fails with
  its guard removed; each timing shape fails with its loop's guard removed.
- verified (builder): equivalence with the oracle, identical on 8 seeds x 3000 pages.
- verified (builder): 80 real pages: leak 4-grams 4023 (pre-B3) -> 1413 (U2) -> 107; 0 visible
  text lost against U2.
- verified (builder): timing, interleaved with base under load: the timing test 25-26 s -> 34 s
  (+30%), far from the 60 s watchdog; the 80 pages convert 5% slower than on U2.
- verified: the full suite, in this commit's hook.
- untested: Linux, Node 26. The 107 remaining 4-grams were sampled, not all read: they show
  words run together (B2 spacing), not attribute text.

What you got wrong and fixed:
- Mistake: the oracle's pair helper.
  - Where: test/fixtures/regex-extract.mjs, during the build.
  - Impact: scratch only; equivalence differed on td/th.
  - Cause: a scripted edit failed silently.
  - Fix: a `close` parameter. Verified: 8 seeds identical.
- Mistake: unmeasured guards in tagPairs.
  - Impact: nothing delivered; link passes were about 1.5x slower and one shape cost 7.7 s.
  - Cause: any-order lists and a memo entry for every '=' with no shape to justify them.
  - Fix: nextOf for '>' and '='; the memo filled only for nested starts. Verified: the mutations
    and the timings above.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in the mixed row

What changed: no behaviour. ADR-0195 decision 4 now says a row counts when at least one of
its quotes is usable and found, whatever else it carries, and a row whose quotes all warn or
fail does not. Decision 3 and the lib/measure.mjs comment say a quote that is long on its raw
text but warns quote-too-short (the ADR-0191 review fix, D-l) is not usable, on purpose. The
Consequences name d788545 as the re-count base and say a change to ADR-0191's floor or to
normalizeForMatch moves the line; the triggers add both. R-15 becomes R-16 in the ADR and its
index row. bin/measure's help says "a quote that is not too short and is found in its
capture". One test pins the mixed row.

Why: U10 review F1-F5 (all S3), as the lead dispositioned them (D-q). The ADR said a row the
gate warns or fails on does not count, but a real quote beside an invented one counts (the
plan's wording, kept). Rejected: counting only rows the gate passes (a rule change), and
aligning "usable" with the check's compare predicate (it loosens the line).

What it touched:
- lib/measure.mjs (comment), bin/measure.mjs (help), test/measure.test.mjs (+1 test).
- ADR-0195, its index row (R-16), the README test count (2194 -> 2195).
- ARCHITECTURE.md's measure.mjs row: one clause, a row counts when one of its quotes is usable
  and found. Not ADR-0191, which stays as dated.

What you verified:
- verified: selftest measure: 13 passed, 0 failed (12 before plus the new one). The new test
  was green on the unchanged lib, as expected: it pins existing behaviour, so there was no red
  first.
- verified: guard mutation. Requiring every quote on the row to be usable and found failed
  only the new test (12 passed, 1 failed, [0,1] against [1,1]). Restored, 13/0.
- verified: measure --help prints the new text and exits 0. git diff --check is clean.
- expected: the full suite in this commit's hook (no test reads the help text).
- untested: Linux, Node 26.

What you got wrong and fixed: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pass, and the pass carries the limit line (ADR-0193)

What changed: the recheck asks whether a fetch after the replacement found the page gone from the
ledger alone; ADR-0194's exemption for a Finding that names the status no longer applies to it
(goneAfterScrape gains { findingExempts }, default true; cites-gone-page is unchanged). The
quote-survived-refresh pass detail now ends with the limit line too. ADR-0193 says the pass and its
"Re-read N" advice show with --show-pass and in --json only (a known limit for the owner), that a
stale old row still warns stale-evidence and fails under --strict, and how gone is asked. README
and the freshness-recheck and cite-in-code skills say a superseded citation fails unless its words
survived; README adds that the drift attribution is an unmeasured heuristic.

Why: U8 review F1-F5 (RUN.md D-v). F3: an old row whose Finding named "HTTP 404" passed with no
finding at all on a page the newest fetch found gone. F2: the ADR described output preflight does
not print by default. F1: with recheck 12 a pass, removing attribution case 3 kept the suite green.
F4/F5: docs said a superseded citation always fails, and the ADR's consequence was unconditional.

What it touched: lib/checks.mjs (goneAfterScrape option, call site, pass detail, comments),
test/quote-recheck.test.mjs (+2 tests; survived() asserts the limit line; recheck 1's exact detail;
recheck 12 retitled, its dormant branch commented), ADR-0193 (decisions 1, 4, 6, Consequences) and
its index row, docs/ARCHITECTURE.md (checks.mjs row), README.md, skills/freshness-recheck and
skills/cite-in-code SKILL.md, CHANGELOG, fetchproof/README.md test count (+2).

What you verified:
- verified: red first. quote-recheck 17 passed, 5 failed on the old code: the new 404 test passed
  the citation, and 4 pass tests lacked the limit line.
- verified: quote-recheck 22 passed, 0 failed after the change.
- verified: each new test alone fails under its guard's removal and passes restored (table in the
  builder report); the full group fails only in the named tests.
- verified: selftest quote-recheck checks evidence-context handoff repo-hygiene skill-set: 221
  passed, 0 failed. git diff --check clean.
- verified: movement.mjs 47 movements, exit 1, all "added" lines, as at 93e2d04: 0 in the 41 baseline corpora.
- verified: the full suite, in this commit's hook.
- untested: Linux and macOS.

What you got wrong and fixed:
- Mistake: the new 404 test asserted the gone day as the stubbed DAY3.
- Impact: none delivered; the test was red on correct code.
- Cause: a fail ledger entry records the real time, not the stubbed date.
- Fix: assert the status sentence without the day, as the 410 test does; commented.
- Verified: 22/0; no other new assertion uses a day.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ross-transport movement (U9 review)

What changed: no behaviour. ADR-0192 lists the superseded-pair movement: 6 of the 10
punctuation-spacing pair anchors go from not found to found, each reviewed as the rule working,
and 4 stay not found. The other 2 pair anchors, in first-quoted-refresh, are found either way.
It says the floor reads the ruled text, so the rule moves it both ways, with a new rejected
alternative and trigger, and that measure's quoteCoverage can fall. It gives the exact
measure-spacing.mjs reproduction (a worktree at 2d831ca~1) and fixes the E-02 [5] and
blank-line slips. quotes.test.mjs gets one test pinning that quoteTooShort counts the ruled
text, and a corrected pins comment. The CHANGELOG's "Lenient only" is corrected.

Why: U9 review F1-F9 (all S3), as dispositioned in run decision D-t. The lead keeps the floor on
the compared text (ADR-0191). Rejected: running the floor's skip on the pre-rule text, a second
normalisation path for an edge no corpus anchor has (none of 557 is short on its raw text).

What it touched: docs/adr/0192-..., test/quotes.test.mjs (+1 test, one comment), CHANGELOG (the
U9 entry), the README test count (+1). No declared code path.

What you verified:
- verified (builder): selftest quotes repo-hygiene: 52 passed, 0 failed (40 + 12).
- verified (builder): with the floor counted on the exact pre-rule text, 2d831ca gives quotes
  39/0 and this tree 39/1, the new test failing; so the gap was real.
- verified (builder): over 44 corpora, 557 anchors, 0 moved; superseded-pair anchors 12, 6 moved.
- verified (builder): measure-spacing.mjs from a worktree at 2d831ca~1 prints the brief's
  per-mark counts; it counts 431 captures where the brief says 430 (the ADR notes it).
- verified: the full suite, in this commit's hook.

What you got wrong and fixed:
- Mistake: the review's floor mutation was a near no-op.
  - Where: the reviewer's scratch mutate5.mjs.
  - Impact: the new test passed under it (40/0), which would have looked like a test with no
    guard.
  - Cause: written in a plain JS string, so "\]" and "\S" lost their backslashes.
  - Fix: redone as an exact mutation, under which the test fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What changed: three short notes, 5 lines added in all. The root AGENTS.md, Rule 1 step 4, now says
four things. A citation of a superseded row still fails unless every usable quote of the old row
is in the new capture (ADR-0193), and the agent re-reads the new row even then. A page a later
refresh found gone fails its citation (ADR-0194). Quotes match whatever the spacing beside
punctuation (ADR-0192). measure.mjs reports quote coverage (ADR-0195). The project template's
AGENTS.md (step 3) carries the first two in two lines. WORK-WITH-EVIDENCE.md's Research bullet
says a surviving quote shows the words are still there, not that the claim still holds.
AGENTS.md's Baseline paragraph records two intermittent failures, one from each of this commit's
first two hook runs: the panel 413 test, and the requests #325 test (with #321 once in a group
run), which is likely a watchdog timeout under load.

Why: R-18 asks for AGENTS.md / WORK-WITH-EVIDENCE.md notes where agents meet a change, under half
a screen in total. The Phase 4b spec review marked R-18 partly met for want of them. The
template is where agents in other projects meet the change, so it gets the two rules that can
fail their gate.

What it touched: AGENTS.md, fetchproof/template/AGENTS.md, docs/WORK-WITH-EVIDENCE.md. No code
and no test. The README count is unchanged.

What you verified:
- verified: selftest support-policy scaffold repo-hygiene: 62 passed, 0 failed. support-policy
  reads the template's commands, scaffold copies the template.
- verified: git diff --check is clean.
- verified: the first hook run was red: 1 failed, 2197 passed. The failing test was panel "an
  oversized POST receives the 413 JSON refusal and the panel keeps answering", outside this
  change, while another agent's tests loaded the machine. `selftest panel` then gave 28 passed,
  0 failed, three times.
- verified: the second hook run was red: 1 failed, 2197 passed. The failing test was requests "a
  delivery held back by an earlier request's failed push reaches the remote on the next cycle
  (#325)", outside this change. Other programs had the machine at 63-100% CPU, and `git
  --version` took 318 ms. `selftest requests` alone failed it in 2 of 3 runs (1080-1632 s per
  run; #321 also failed once). The test alone passed 3 of 3 in 29-38 s, against the 60 s
  watchdog.
- verified: the full suite, in this commit's third hook run, after the owner freed the machine
  (`git --version` 118 ms).
- untested: the cause of the 413 failure, and whether #325's failures were the watchdog (the
  hook keeps no error text). Both are follow-ups.
- untested: how a new project's agent reads the two template lines.

What you got wrong and fixed: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t.mjs answers through a link; 18 mutation survivors pinned (Phase 4b)

What changed:
- checks.mjs: a replacement capture whose recorded statusCode is non-empty and not an integer
  ("404 Not Found", "HTTP 404") reopens cites-superseded-row, and the reason says the status
  cannot be read. Before, NaN read as "not an error page", so such a capture could pass
  quote-survived-refresh. ADR-0193 decision 5 says so.
- movement.mjs: the main guard compares realpaths on both sides. Reached through a junction or
  symlink, it used to print nothing and exit 0, which is its "no movement" answer.
- Tests: 7 new and 6 extended. They pin the 18 survivors of the Phase 4b mutation audit, the
  boundaries, presence checks and fallbacks the code already had: the Finding exemption's word
  boundary (4100 and 14040), some-not-every in quote coverage, the HTTP 400 edge, the 199/200
  thin edge, the attribution's presence checks and capture-header fallbacks, a literal `<>`,
  mixed href quotes, a mid-text status and a 500 asked again. The ADR-0194 import test now
  refuses createRequire, a bare require( and an import after `;`.
- http-linear: the six shapes U2-fix added run on n/2 to n/32, with the bound kept at 4.
- Docs: ADR-0192 says the keyless converter was seen spacing nine closing marks, and that `%`
  is in the set because Firecrawl drops a no-break space (E-02 against E-09). The CHANGELOG
  says the same. The B3 headline is narrowed to a quoted `>`, and a closing tag inside a
  quoted value is named as a known limit, as before B3. The quotes.test.mjs:407 zero-width
  space is now an escape. ADR index row 0191 names decision 5 and rejected option (a).
  ARCHITECTURE: the quotes.mjs example moves to its clause, and the checks.mjs row names the
  non-numeric status.

Why: Phase 4b, as dispositioned in run decisions D-x and D-y.
- Breaker F1 (S2): movement.mjs is the measurement the ADRs cite for "no baseline corpus
  moves", and through a link it gave that answer without measuring.
- Breaker F2-F4 (S3).
- Spec review S3 1-4.
- Mutation audit: C6 (S2) and 17 S3 survivors. Every guard was right, but untested.
- Rejected: changing the http-linear equivalence generator to emit `<>` and mixed quotes. A new
  generator could surface unrelated oracle drift; direct tests pin both.
- Rejected: changing the collector's status parsing. That is older than this range, and the
  recheck no longer passes on it (a PR follow-up).

What it touched:
- lib/checks.mjs (a declared code path).
- docs/measurements/2026-10-refresh-baseline/movement.mjs.
- 7 test files: checks, collect, http-linear, measure, quote-recheck, quotes, transport.
- ADR-0192, ADR-0193, the ADR index (row 0191), ARCHITECTURE.md (2 rows), CHANGELOG.
- The README test count: 2198 -> 2205.

What you verified:
- verified (builder): the groups quote-recheck checks quotes http-linear transport measure
  collect repo-hygiene give 483 passed, 0 failed.
- verified (builder): F1 and F2 red first, then green. All 19 survivor mutations fail their
  test and are restored. All 7 shape mutations fail the trimmed timing test (5.5-11.6x
  against the bound of 4).
- verified (builder): http-linear interleaved on a quiet machine: base 14-16 s, this change
  18-20 s, the previous head 25 s. With three busy loops: 25-27, 32-35 and 42-44 s.
- verified: after the comment fix, the groups hardening checks repo-hygiene: 183 passed, 0 failed.
- verified: the full suite, in this commit's second hook run.
- untested: CI's runners. The junction test on Linux, where it makes a directory symlink as
  the repo's other link tests do.

What you got wrong and fixed:
- Mistake: a found-on date.
  - Where: the F2 test comment.
  - Impact: none on behaviour.
  - Cause: the builder wrote the fix date as the found date.
  - Fix: changed to 2026-10-25.
- Mistake: the first hook run was red (1 failed, 2204 passed): hardening "no module names a Node
  builtin it never imported" read `module.` in checks.test.mjs.
  - Where: the new comments beside the import test's STATIC_IMPORT and NETWORK_BUILTINS.
  - Impact: the commit was refused; no code was wrong.
  - Cause: hardening's scan pairs quote marks without knowing regex literals. The quotes in
    STATIC_IMPORT leave it inside a string, and the quoted names in the new comments then read
    as code. The builder's VERIFY list and the lead's check did not include the hardening group.
  - Fix: the comments name the modules without quote marks, and say why. The scan itself is
    left as it is (a PR follow-up).
- Not fully met: F4's "near base". The change runs at 1.25-1.35x base (it was 1.65-1.8x). The
  rest is the slower lib on unclosed `<a href` plus about 3 s of trimmed shapes. The 'quoted
  >' shape still has no guard mutation that fails it (as in U2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ge 6 break-test F1)

What changed: movement.mjs's main guard resolves argv[1] with a realpathOrNull helper, so a path
that does not exist compares as "not this script" instead of throwing at import. The module's own
path still uses fs.realpathSync, because it always exists. measure.test.mjs has one new test that
imports the module from `node -e` with an argument that names no file.

Why: 4b-fix's realpath guard (Phase 4b breaker F1) runs fs.realpathSync(process.argv[1]) at
module evaluation. When argv[1] is not a file (`node -e "await import(...)" an-argument`, or a
deleted launcher script), the import itself threw ENOENT, so measureAll and movement could not be
reached that way. The old path.resolve guard could not throw. One obvious remedy, so no ADR.

What it touched: docs/measurements/2026-10-refresh-baseline/movement.mjs;
fetchproof/test/measure.test.mjs (+1 test); CHANGELOG.md (the refresh-baseline entry);
fetchproof/README.md 2205 -> 2206 tests. No ARCHITECTURE row (docs/measurements has none), no
ADR, no new domain term.

What you verified:
- verified: the new test is red before the fix (ENOENT at movement.mjs:390) and green after
  (break-test).
- verified: the repro command prints [ 'measureAll', 'movement' ] after the fix; --help exits 0,
  --bogus exits 2 (break-test).
- verified: selftest measure repo-hygiene 28/0 and hardening 68/0 (break-test); repo-hygiene
  12/0 and git diff --cached --check clean on the integrated tree (lead).
- verified: the break-test's full suite with the fix: 2206 passed, 0 failed, red only on the
  README count, which this commit sets.
- verified: the full suite, in this commit's hook run.
- untested: Node 22 (not installed here; CI's archive-tree job is the first Node 22 run).

What you got wrong and fixed:
- mistake: the movement.mjs edit and its green check were issued in one step; the edit was
  refused, so that "after" run tested the old code and was red; where: the Stage 6 session, the
  step that applied the fix; impact: none on the delivered files (the run is kept as a second
  "before" record); cause: the check did not wait for the edit's result; fix: re-read the guard,
  re-applied the edit, re-ran the test, the repro and the CLI checks; verified: the test is green
  and the repro prints both exports.
- mistake: the lead's CHANGELOG edit wrote CRLF line endings into the working copy; where:
  CHANGELOG.md, the integration step; impact: none committed (the index was normalised to LF);
  cause: a Python text-mode write on Windows; fix: rewrote the file with LF; verified: `file`
  reports no CRLF and the staged diff is 3 lines added, 1 removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ked, never silent (ADR-0193 d8)

What changed: checks.mjs gains fetchIndex (one keyed pass over the ledger, replacing lastFetchByUrl),
goneSince, movedAfterScrape and movedCitation. A cited row is moved when the last scrape after its
own (ledger rank) that asked for its urlKey or landed on it landed on another key; only when no such
scrape exists is the last scrape that asked for what its own fetch asked for read (redirect-first
rows). The current row of the landed URL is rechecked; a full survival passes quote-survived-refresh
naming the move. Anything else reopens cites-superseded-row ("asked for X [- the address E-02's own
fetch asked for before it landed on Y -] and landed on Z, a different page") + the limit line: no
row, a landed page or through-address found gone (404/410, ADR-0194's rank and classification), or a
landed page that has itself moved on (a move of a move is not followed). A later scrape that asked
for a URL ends an earlier gone state of it.
Why: Stage 7 gap 1 (a redirected refresh was filed under the landed URL, so supersededRows never
paired it: a green gate over a page without the quote); owner rulings 2026-10-26 ("Fix it in this PR",
"Cover them" for redirect-first rows); G1-fix reviews 1 and 2. supersededRows, cites-gone-page's own
rule and the collector are unchanged; no new rule name. Set aside: following a move of a move to
its next row (recorded in ADR-0193 decision 8), and widening cites-gone-page to the old address
(it would change ADR-0194's rule for every redirect-first row; kept as ADR-0194's alias consequence).
What it touched: fetchproof/lib/checks.mjs; fetchproof/test/quote-recheck.test.mjs (+21),
fetchproof/test/measure.test.mjs (+1, R-06); ADR-0193 (two rulings, decision 8, consequences),
ADR-0194 (decision 1 amended, consequences: the gate and recentlyGone differ after a move);
docs/adr/README.md rows 0193 and 0194; docs/ARCHITECTURE.md checks.mjs row; CHANGELOG.md;
fetchproof/README.md 2206 -> 2228 tests (+22, on top of break-test F1).
What you verified: 8 selftest groups 348/348 (verified, builder); git diff --check clean (verified);
23/26 mutants red, 3 equivalent or wording-only, argued (verified); 44 corpora vs 709a3ac: 0 verdict
or finding changes beyond a path artifact, 0 moved findings (verified); movement.mjs unchanged, 47
movements all in the 3 post-baseline corpora (verified); root preflight PASS (verified); perf
5000/2000 210 ms vs base 667 ms (verified in round 2, not re-run). Review 3 (fresh context): no S1
or S2, 25 probes as expected, K the only red-to-green family over a 1724-scenario base/r2/r3 run
(verified, reviewer). On the integrated tree (lead): selftest hardening repo-hygiene checks
quote-recheck 229 passed, 0 failed (verified); git diff --cached --check clean (verified); the full suite,
in this commit's hook run (verified). Real Firecrawl redirect behaviour (untested; wording only).
What you got wrong and fixed:
1. mistake: a moved citation was rechecked against the row naming the landed capture, even when a
   later capture had superseded it; where: checks.mjs movedCitation; impact: a false green with a
   pass message (review 1 A), nothing delivered; cause: read "the replacement" as the landed row,
   not the current row; fix: superseded.get(row) ?? row; verified: probe A red then green, A1 red.
2. mistake: a redirect-first row's refreshes were never joined; where: movedAfterScrape; impact:
   a silent move (review 1 B, 1 uncited committed row), nothing delivered; cause: joined on the
   row's URL only; fix: also the asked key of the row's own fetch (owner ruling); verified: B and G
   red then green, B1/B2 red, silent (b) green.
3. mistake: a 404 followed by a move stayed gone; where: goneAfterScrape; impact: two failures
   saying "gone" for a moved page (review 1 C), fails safe; cause: gone keyed by filed URL only;
   fix: a later asked scrape ends it; verified: probe C red then green, C1/C2 red.
4. mistake: the old address's join was ranked equal to the row's own URL (latest of three wins);
   where: movedAfterScrape; impact: a false green when the row's URL lost the quote and the old
   address later landed on a page holding it (review 2 R2-1, with or without a 404), a false red
   in the reverse (R2-5), 0 cited committed rows; cause: max over three joins; fix: the row's URL
   first, the old address only when nothing has asked for or landed on it since; verified: L, L2,
   K red then green, R1/R1b red.
5. mistake: a moved citation never read a gone state at where it landed, at the address it came
   through, or a later move of the landed page; where: movedCitation; impact: passes on pages found
   gone or moved on since (review 2 R2-2, R2-3; probes M, M2), nothing delivered; cause: gone read
   for the row's URL only; fix: goneSince at the landed page (current row) and through-address,
   and a reopen on a move of a move; verified: H, I, M, M2 red then green, R2/R2c/R3/R3b/R4/R5 red.
6. mistake: weak or stale pins and docs - silent (c) did not pin the scrape-only rule, a guard was
   called equivalent, a ledger walk per citation, ADR-0194 claimed parity with recentlyGone, two
   stale code comments; where: quote-recheck silent (c), movedAfterScrape, ADR-0194 d1 and
   consequences, checks.mjs docstrings; impact: weak tests (review 1 E), 117 s at 5000/2000
   (review 1 F), docs wrong after fix C (review 2 R2-4, S3); cause: collector fails carry the asked
   URL, a loop per citation, docs not re-read after fix C; fix: a fail filed under another URL,
   fetchIndex and a row Map, a pin for the guard, ADR-0194 amended, comments rewritten; verified:
   E1 and G10 red, perf, groups 348/348.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…one; AGENTS.md and ADR-0194 wording (PR #440 review round 1)

What changed:
- fetchproof/test/checks.test.mjs: in the test "a 404 recorded before the row's own scrape is not gone",
  the copied scrape now asks for the URL it is filed under. A new test, "only a fail is gone", pins
  goneSince's `entry.op !== 'fail'` guard.
- fetchproof/test/measure.test.mjs: two movement() tests, one for a verdict flip and one for a refresh
  pair whose foundInNew flips, plus one table-driven test of every other comparison movement() makes
  (counts, finding counts, superseded-by, cited-by, corpus added, anchor added, tooShort). +4 tests.
- AGENTS.md (Rule 1, step 4): states ADR-0192's real scope.
- ADR-0194, decision 6: matches collect.test.mjs's history.
- CHANGELOG (Unreleased) gets one bullet. The fetchproof/README.md count goes from 2228 to 2232.

Why: these are independent review findings on PR #440, round 1.
- BR-3 (S1). Since 6d9ed56's asked-for rule, the fixture's copied command cleared the 404 on its own,
  so deleting `last <= after` in goneSince left every test green.
- The fix review then found `entry.op !== 'fail'` unpinned too.
- GA-3 (S3). The verdict diff (M16) and refresh-pair diff (M17) of movement.mjs had no test, though
  TRACEABILITY marked R-06 VERIFIED. The fix review found seven more surviving branches (MX1-MX7).
- GA-2 / BR-1 (S3). AGENTS.md said quotes match "whatever the spacing beside punctuation". ADR-0192
  covers only the space before `. , ; : ! ? % ) ] }` and after `( [ {`.
- GA-4 (S3). ADR-0194 said the collector's tests were untouched, but 709a3ac added a case.
- No alternative was set aside: each is a test or wording fix with one obvious remedy.

What it touched:
- Files: fetchproof/test/checks.test.mjs, fetchproof/test/measure.test.mjs, AGENTS.md,
  docs/adr/0194-a-cited-page-a-refresh-found-gone-fails-its-citation.md, CHANGELOG.md,
  fetchproof/README.md.
- No lib/bin code, no ARCHITECTURE.md row, no new term.

What you verified:
- verified: groups checks, quote-recheck, measure, quotes, hardening and repo-hygiene gave 289 passed,
  0 failed (builder, cwd C:\Users\PC\wt-fp\r1-fix-b).
- verified: hardening, repo-hygiene, checks, quote-recheck and measure gave 250 passed, 0 failed in
  the main checkout after integration (lead).
- verified: each mutant went red alone, then was restored.
  - Builder, then independent fix reviewer: M19 (`last <= after` removed), G1, G3, G4, G5.
  - Lead re-run of the reviewer harness: M16, M17, MX1-MX7.
- verified: the committed BR-3 test was green under M19 before this fix (builder, reviewer, lead).
- verified: `git diff --cached --check` is clean, and LF endings are kept.
- verified: the full suite in the commit hook (see the hook's output for this commit).

What you got wrong and fixed: one mistake, in scratch tooling only.
- mistake: a sed edit to the builder's scratch mutation runner broke its syntax.
- where: the builder's scratchpad mut/run.mjs.
- impact: one M16/M17 rerun did not start. Nothing delivered was affected.
- cause: `\n` inside a sed replacement became a literal newline.
- fix: the line was rewritten with an editor tool.
- verified: the rerun printed M16 and M17 red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h (plan v0.4 §4 "Done when"; PR #440 review GA-1)

What changed: docs/adr/README.md has a new "Notes." paragraph. It records that the v0.3
`quote-elides-qualifier` patch was not found, that no ADR depends on it, where its job went (the quote
suite's ellipsis category and the KNOWN GAP test), and that §17.3 is still open for the owner.
Why: plan v0.4 §4's "Done when" has three conditions: the baseline, the movement script, and "the
patch's status is recorded in the ADR index notes". Only the first two were delivered. The gap-audit of
round 1 (GA-1, S2) found nothing in the tree that records the status.
What it touched: docs/adr/README.md only. No code, no test, no ADR decision.
What you verified:
- verified: `git grep -n -i "elides-qualifier" HEAD` gave no output before this change (gap-audit and lead).
- verified: selftest repo-hygiene and hardening are green after it (lead, main checkout).
- verified: `git diff --check` is clean.
What you got wrong and fixed: nothing to report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uoted value (BR-2, R-1, FRA-2, FRA2-1, FRA3-1, FRA4-1)

What changed
- lib/http-transport.mjs:
  - commentsOf(text): the next "-->" (nextOf memo), stop(open) - where the comment opened at
    `open` ends: its "-->", or with none after it its first ">" - and inside(p), whether a '<'
    lies in a comment. The last "<!--" before p decides; the "<!--" list is built once, with
    binary search, and the span of the last one is cached.
  - rawTextOf(text): whether a '<' lies in the content of a <textarea>, <title>, <xmp>,
    <noembed>, <noframes>, <script>, <style>, <noscript> or <iframe>. The elements are found
    forward, as HTML reads them: the next opening tag is looked for only after the last
    element's closing tag of its own name, and an element with none runs to the end. A comment
    is stepped over to its stop, and any other "<!" or "<?" to its first ">", so no element
    opens inside one. An opening tag inside a quoted attribute value is still taken as one, as
    tag ends are not read there. Each element is listed once; a '<' finds the one before it by
    binary search.
  - startsTag: a letter or '/' (was also '!' and '?').
  - tagEnd:
    - "<!--" ends at the next "-->" and reads no quoted value; one with no "-->" ends at its
      first ">";
    - any other "<!" and any "<?" end at their first ">";
    - a tag inside a comment or a raw-text element ends at its first ">" and reads no quoted
      value.
    Used by stripTags and tagPairs, so by mainContent, resultLinks and titleOf alike; the
    scan stays linear. Docstrings state each rule, the tagEnd one with all nine names.
- test/fixtures/regex-extract.mjs (the independent oracle):
  - TAG gains a comment branch and a NO_QUOTES branch: INSIDE a comment, or RAW_INSIDE, one
    negative lookbehind saying the page before the '<' splits into characters that open no
    raw-text element, whole comments, "<!" and "<?" (to their first ">"), and whole
    raw-text elements; RAW names the same nine elements as the lib;
  - only "<" plus a letter or "/" reads quotes;
  - pair() reads an opening tag in NO_QUOTES to its first ">".
  Lookarounds only, no loop.
- test/transport.test.mjs:
  - six tests: BR-2's two triggers; an unterminated "<!--"; a block tag inside a comment
    (partial, reported dropped); "<!"/"<?" notes; raw-text elements, with the span's edges
    (an upper-case closer, "</textareax>", "<textareax>", "<xmpx>"), a <script> inside a
    <textarea>, and a <title> named in a comment or a "<?" before a <textarea>; an "<a" with
    an open quote in a script, style, noscript or iframe before five search results, also
    after an <iframe>, <style> or <title> named in a script string, in a comment or in a
    "<?", or a <script> in a <textarea>, and a <title> named in CDATA or in a "<?" before a
    script string holding "</title> -->" (FRA5-1). Each is also compared with the regex
    extractor;
  - the ~:454 pin now reads "<?pi a="x > y"?>" to its first ">" ('a **bold** and y"?>c').
- test/http-linear.test.mjs:
  - six timing shapes (unterminated "<!--" with a ">" after each; one comment, then tags;
    an unterminated "<!--" before each tag, one ">" at the end; an unclosed <xmp> before
    each tag; a closed <xmp> before each tag; an unterminated "<!--" with a ">" before each
    tag);
  - the timing check answers red only on a time that holds: the fastest of two large-page
    rounds over the bound, and a first pass over 1500 ms timed again. Bound and sizes kept.

Why
- BR-2 (S2): B3 read "<!--" as a tag starting with "!", so an unclosed `="`/`='` in a comment ran
  to the next quote of its kind. <main> lost its words, the sidebar was kept, and the capture was
  graded `full`. ab2e605 read it correctly.
- R-1: a tag tagPairs finds inside a comment still read quotes and hid the content block from
  both the choice and the dropped list. Starting no pair inside a comment was set aside: it
  changes pre-B3 behaviour for every commented-out block.
- FRA-2 (S3, lead decision D-6): the same leak through `<?php $label = 'Upgrade ?>`, CDATA, a
  bogus DOCTYPE, and a tag inside a <textarea>, <title> or <xmp>. B3 exists so that text no
  reader sees does not become quotable. A browser shows the text after a PI's first ">" and
  reads raw-text content as text, so reading their quotes hid visible text.
  - This reverses the "<?pi" part of d13d084's pin, which no ADR or owner ruling covers.
  - "An opening tag with no closing tag opens no element" was set aside: it read an unclosed
    <textarea>'s quotes, unlike ab2e605 and HTML.
  - <plaintext> is obsolete, and nothing ends it.
- FRA2-1 (S3, lead decision D-7): resultLinks and titleOf read the raw page, where BLOCK_DROP
  has not removed <script>, <style>, <noscript> or <iframe>. An `<a class='` in a script string
  ran on to the apostrophe in the first result's snippet: the first result took the second's
  title and the second was lost (5 results became 4; ab2e605 gave 5). Their content is text in
  HTML, so they join the raw-text elements.
- FRA3-1 (S3, lead decision D-8): with those four names, "the last raw-text opening tag before
  the '<' decides" let an element named inside another end it: an `'<iframe></iframe>'`,
  `<style>` or `<title>` string in a script, or a <script> in a <textarea>, ended the outer
  element at its own closing tag, and the `<a class='` after it took a result again (a
  <textarea> holding a <script> lost <main>'s prose, worse than both ab2e605 and the rule
  before). HTML opens nothing inside such an element, so the elements are found forward.
- FRA4-1 (S3, lead decision D-9): the forward scan still took an opening tag written in a
  comment, a "<!" or a "<?" as one: `<!-- <iframe src="old.html"> -->` before a script holding
  `'</iframe>'` ran on to that closer, the script was never opened, and the `<a class='` took a
  result (4, base 5); `<!-- <title> -->` before a <textarea> lost <main>'s prose, graded `full`.
  HTML opens no element there, so the scan steps over them as tagEnd reads them. The same
  through an opening tag in a quoted attribute value (`title="Use <style> tags"`) is recorded,
  not fixed: closing it means reading tag ends in rawTextOf, and that half is left open.
- FRA5-1 (S3, fix review of FRA4-1): the "<!x"/CDATA half of that step, and "<!x"/"<?" ending at
  their first ">" rather than at the next "-->", had no row: four mutants (lib, reference, both,
  "<!x"/"<?" stepped to "-->") stayed green. Two rows pin them.
- Recorded, not fixed (D-9, PR follow-ups; 0 of the 80 committed pages): a "<!--" inside a quoted
  attribute value or a tag is still stepped over to its "-->" (FRA5-2; the docstrings say so); a
  tag inside a "<?" or "<!x" still reads quotes, on the search path and in <main> (FRA5-3, from
  B3); tagEnd reads a "<!--" inside a <textarea> as a comment where rawTextOf does not (FRA5-4,
  since the first fix).
- FRA2-2, FRA3-2 and N1 (S3): the raw-text span's edges (a case-blind closer, a whole-name
  closer, a whole-name opener in lib and reference) and the noscript and iframe names had no
  test, so removing any of them stayed green; the 3,000-page generator writes none of them.
- Unterminated "<!--": HTML's "rest of the document" was set aside. It removes every later word
  from the counts and the capture; the pre-B3 first-">" reading keeps the main content.
- FRA-1 (S3): no timing shape covered an unterminated "<!--" before each tag, so dropping any of
  inside()'s three memos stayed green. Likewise no shape had more than two raw-text elements,
  so a linear walk in place of rawTextOf's binary search stayed green, and none had a tag after
  each unterminated comment, so searching each comment's "-->" again stayed green.
- Flaky timing test (S1): one preempted run answered for a linear shape. Raising the bound was
  set aside: it separates linear from quadratic code.

What it touched
- fetchproof/lib/http-transport.mjs, fetchproof/test/fixtures/regex-extract.mjs,
  fetchproof/test/transport.test.mjs, fetchproof/test/http-linear.test.mjs,
  docs/ARCHITECTURE.md (http-transport.mjs row), CHANGELOG.md (B3 entry),
  fetchproof/README.md (2232 -> 2238 tests). No ADR.

What you verified
- verified: each new or changed transport test RED on the tree before its fix, GREEN after, and
  RED again with only its guard reverted, separately in lib and in the reference:
  - comment branch; inside() in lib; INSIDE in the reference;
  - startsTag "!"/"?" in lib; the TAG lookahead in the reference;
  - the script, style, noscript and iframe names, each on its own row, in lib, in the
    reference, and in both;
  - the forward scan: rawTextOf back to "the last opening tag decides" in lib, RAW_INSIDE back
    to it in the reference (each RED on all five nesting repros);
  - the comment, "<!" and "<?" step: lib and reference each back to a4 (RED on the comment
    rows of both tests and on the equivalence test); in lib, no step, no "<?" and a comment
    stepped to its first ">"; in the reference, no SKIP, no closed-comment, unterminated or
    "<!"/"<?" piece, the unterminated piece without its "no -->" test, and a lone '<' that may
    take "<!"/"<?". Each RED on its own row.
- verified: the raw-text span pins each RED on exactly its own lib or reference mutant
  (case-sensitive closer; closer without \b; opener without \b, lib and reference), GREEN on
  the fix, reference deep-equal.
- verified: the 3,000-page equivalence test RED with only one side's comment, raw-text,
  script/style, forward-scan or comment-step rule, GREEN with both (6.7-6.9 s; 5.7 s before
  the comment step). Its generator writes no "<!x", no "<?", no noscript or iframe and no
  raw-text span edge; the transport rows cover those.
- verified: a nesting fuzzer (3,000 pages, seeds 7-9) finds lib == reference on every page.
- verified: the timing shapes are RED against each quadratic mutation:
  - the "-->" memo, the '>' memo and the span cache of inside() (5.9-8.0x);
  - lastIndexOf('<!--');
  - rawTextOf's list dropped (13 shapes) and its binary search made a walk (the closed-<xmp>
    shape, 6.2-6.4x);
  - the comment step's "-->"/">" memo (the last shape, 7.7x; every other shape <= 1.13x).
  All are GREEN unmutated.
- verified (lead, at integration): the two FRA5-1 rows GREEN on the fix and RED on each of the
  four mutants (L-no-bang, R-no-bang, B-no-bang: CDATA row; L-bogus-to-dashdash: "<?" row),
  transport 141/1 each.
- verified: http-linear alone 10 times, all green, highest ratio 1.272 against a bound of 4.
- verified: transport + http-linear + browser-transport 144/0; hardening + repo-hygiene +
  checks + quote-recheck 230/0 (re-run by the lead on the integrated tree: both green).
- verified: 80 committed *.source.html re-converted:
  - vs ab2e605: the B3 differences, and r1-fix-a's word-count changes on 10 pages (2 keep
    `partial` with a smaller omitted note);
  - FRA-2, FRA2-1, FRA2-2, FRA3-1, FRA3-2 and FRA4-1 change none of the 80: 0 markdown,
    grade, title or resultLinks diffs.
- verified: legacy DOCTYPE and xml-stylesheet pages read exactly as ab2e605 and as the page
  without them. git diff --check clean.
- untested: the full suite; Linux / Node 26; a live search page.
- expected, not shown: a tag inside an unclosed "<?" or "<!x" that ends the page reads as one
  outside RAW in the reference (its trailing piece); no test pins it, fuzz finds no page
  where it alone differs.

What you got wrong and fixed
- mistake: the first raw-text rule ended an element only at a closing tag ("no closing tag, no
  element").
  where: lib rawTextOf and the reference RAW_INSIDE.
  impact: an unclosed <textarea> still read quotes and swallowed <main>; caught by the probe
  before any test, nothing delivered.
  cause: I chose the rule for the smallest change, not by HTML's reading.
  fix: the span runs to the end of the text, as HTML and ab2e605 read it.
  verified: probe 4r reads as ab2e605; ref == lib on all 26 probe pages.
- mistake: I left <script>, <style>, <noscript> and <iframe> out of the raw-text elements
  because BLOCK_DROP removes them.
  where: lib RAW_TEXT and the reference RAW.
  impact: resultLinks, which reads the raw page, lost a search result behind an `<a class='` in
  a script; found by review (FRA2-1), not by me.
  cause: I checked the two BLOCK_DROP callers, not every caller of tagPairs.
  fix: the four names join RAW_TEXT and RAW.
  verified: the transport test RED on the earlier tree and with either side's names removed,
  GREEN on the fix; 0 diffs on the 80 pages.
- mistake: adding those names, I kept "the last raw-text opening tag decides", which is not
  HTML's rule once raw elements name one another.
  where: lib rawTextOf and the reference RAW_INSIDE.
  impact: a script naming an <iframe>, <style> or <title>, or a <textarea> holding a <script>,
  ended early and read quotes again; found by review (FRA3-1), not by me. No committed page.
  cause: I tested each new name alone and never one inside another.
  fix: the elements are found forward, in both.
  verified: the five repros RED on the earlier tree and with either side reverted, GREEN on
  the fix; fuzzer 0 differ; 0 diffs on the 80 pages.
- mistake: the forward scan took an opening tag written in a comment, a "<!" or a "<?" as one.
  where: lib rawTextOf and the reference RAW_INSIDE.
  impact: a comment or PI naming a raw-text element before a script or textarea naming its
  closer lost a search result or <main>'s prose; found by review (FRA4-1), not by me. No
  committed page.
  cause: I read comments and raw text independently (I recorded D3 as "equals a3") and never
  asked where HTML can open an element.
  fix: the scan steps over comments, "<!" and "<?" as tagEnd reads them, in both.
  verified: A1, A4p, C-comment-gt, A9 and A9p RED on the earlier tree and with either side
  reverted, GREEN on the fix; fuzz 0 differ on seeds 7-9; 0 diffs on the 80 pages.
- mistake: I added the noscript and iframe names with no test that fails without them.
  where: test/transport.test.mjs, the FRA2-1 test.
  impact: removing either stayed green; found by review (FRA3-2).
  cause: I pinned the two rows the brief named and left the other two to a probe.
  fix: a row for each.
  verified: each RED on its own name's mutant, lib only, reference only and both.
- mistake: the first reference took 14.3 s for the 3,000-page test (was 3.1 s).
  where: regex-extract.mjs, RAW_INSIDE (one lookbehind alternative per name).
  impact: a 4.6x slower test group; caught before handback.
  cause: each alternative rescanned to the page start from every tag.
  fix: one lookbehind naming the element; now one negative lookbehind for the forward rule.
  verified: 5.6 s, equivalence GREEN, differ=0 against the lib.
- mistake: the comment step's first reference form put its "no -->" test after the "<!--".
  where: regex-extract.mjs, SKIP.
  impact: the 3,000-page test's reference took 16 s, not 5.6 s; caught before handback.
  cause: a lookbehind matches right to left, so the test ran at every split of "[^>]*".
  fix: the test stands before the "<!--".
  verified: 6.7-6.9 s alone (5.7 s before the step), equivalence GREEN, differ=0.
- mistake: the RAW1 timing mutation was first built with "\b" turned into a backspace.
  where: the scratch mutation spec, not the patch.
  impact: it would have tested a wrong closing-tag search, not the missing memo; caught before
  any run.
  cause: template-literal escaping across two layers.
  fix: rebuilt with "\\b" in the mutated source.
  verified: grep of the mutated line; RAW1 RED only on the new shape.
- mistake: two comments quoted figures I had not measured: the FRA-1 shape's "6.2-7.8x" (the
  reviewer's) and the reference's "about 50x slower".
  where: http-linear.test.mjs above that shape; regex-extract.mjs above SKIP.
  impact: numbers in the code not backed by a run; caught before handback.
  cause: I wrote the comments before running the measurement.
  fix: "6.0-8.0x" and "16 s, not 5.6 s", my measured figures.
  verified: the timing-mutation summaries; eqbench on the 3,000 pages.
- mistake: the RAW_TEXT doc said script, style, noscript and iframe are removed "before a tag
  is read", and the tagEnd doc named only three raw-text elements.
  where: lib/http-transport.mjs, the RAW_TEXT and tagEnd docstrings.
  impact: untrue for titleOf and resultLinks, which read the raw page; incomplete for tagEnd;
  caught before handback and by review (N-note of FRA4-1).
  cause: I generalised from mainContent and htmlToMarkdown, and left tagEnd's doc at FRA-2.
  fix: RAW_TEXT names both paths and FRA2-1; tagEnd names all nine elements.
  verified: grep of BLOCK_DROP's two callers; transport GREEN after the edit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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