Repository navigation
Independent audit P0 fixes: A-1, F01, F03, F02 and FP-011 (ADRs 0197-0199) - #441
Merged
Merged
Conversation
…mit (independent audit 2026-11-04, A-1)
What changed: fetchproof/lib/gate.mjs - materializeIndex catches a failed mkdtempSync and returns
{ ok: false, reason: 'scratch-failed', detail } instead of throwing; evaluate's unreadable-index block gives that
reason a fix naming the temp folder (TMPDIR, TEMP or TMP) instead of "check that git works". One new test in
fetchproof/test/gate-index-failure.test.mjs.
Why: the independent audit of 532c696 (Claude, finding A-1, Medium after its re-rating) reproduced a commit allowed
when TEMP does not exist: mkdtempSync threw out of evaluate, the pre-commit hook reported "internal error" and,
fail-open by default, allowed the commit, while a git failure a few lines further down the same function blocked
it. Set aside: moving the scratch folder under .git/ (the audit's second suggestion) - it changes where every
commit's scratch lives and is not needed to make the two failures agree.
What it touched: fetchproof/lib/gate.mjs (materializeIndex, evaluate's unreadable-index branch);
fetchproof/test/gate-index-failure.test.mjs (+1 test); docs/ARCHITECTURE.md (gate.mjs row, one sentence);
CHANGELOG.md (Unreleased); fetchproof/README.md (2255 -> 2256 tests). No ADR: one obvious remedy, the block
ADR-0024 already set for a git failure.
What you verified: verified - node fetchproof/bin/selftest.mjs gate-index-failure fails the new test against HEAD's
gate.mjs ("materializeIndex threw ... ENOENT ... mkdtemp") and passes 5/5 with the fix; verified - the audit's
external tests (regression-tests/a1-unwritable-temp-blocks-commit.test.mjs) pass 2/2 against this checkout: the
gate exits 1 with verdict block and the pre-commit hook refuses the commit; expected - the full suite, run by this
commit's hook (ADR-0120): the commit exists only if it is green.
What you got wrong: mistake - wrote the fix before the failing test; where - step order on this task; impact - none
on the change: the test was then run against HEAD's gate.mjs and failed for the reported reason before the fix
went back; cause - order slip; fix - ran the red step against the original file; verified - the red output above.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s the contract (independent audit 2026-11-04, F01) What changed: fetchproof/lib/corpus.mjs - a new blockContext() says, line by line, which lines GitHub shows as code or hides (a fenced code block with its fences, an HTML comment). parseTable's header search now skips those lines, so the table it reads is one GitHub renders, and it returns the header's 1-based `line`. The stray-row scan below the table asks the same blockContext instead of its own copy, so a fence now closes only on a run of its own character at least as long as its opener with nothing after it, a backtick opener whose info string holds a backtick (```code```) is inline code, and a fence inside a comment opens nothing. appendRow puts a row for an empty table below the separator of the header parseTable found, instead of searching for the header itself. Two new tests in fetchproof/test/corpus.test.mjs. Why: the independent audit of 532c696 (Codex, finding F01, High) showed an archived CLOSED copy of the unknowns table, commented out or kept as a fenced example above the live table, read as the contract: the live table's OPEN unknown was never read, preflight printed PASS and derivePortableState gave APPROVED_BRIEF with buildAuthorized: true. Only the stray-row scan knew fences and comments; the header search took the first table with the header wherever it stood. The writers ask the same reader, so appendRow wrote into the commented copy too: below its last row, or, for an empty live table, below the first header led by the same column - the commented one. Set aside: refusing any document in which more than one table carries the header (the audit's "consider an ambiguity failure") - it would block a fenced example of the format, which is ordinary, and a new failure is a check under the feature freeze (ADR-0117). Not covered, and named here so it is not taken as fixed: a table in an indented code block or in an HTML block other than a comment is still read as a table, and a second VISIBLE table with the same header is still not read (the reader takes the first; the stray scan stops at the second's header, as documented for SOURCES.md's two tables). What it touched: fetchproof/lib/corpus.mjs (blockContext, parseTable's header search and stray-row scan, appendRow's empty-table branch); fetchproof/test/corpus.test.mjs (+2 tests); docs/ARCHITECTURE.md (corpus.mjs row, one paragraph); CHANGELOG.md (Unreleased); fetchproof/README.md (2256 -> 2258 tests). No ADR: one obvious remedy, the rule the stray-row scan already documented ("skips fenced code and HTML comments, where GitHub shows a row as code or hides it") applied to the header search. No Python conformance runner reads markdown tables. What you verified: verified - node fetchproof/bin/selftest.mjs corpus fails both new tests against HEAD's corpus.mjs (the commented CLOSED U-1 read as the contract; appendRow wrote E-01 inside the comment) and passes 53/53 with the fix; verified - the audit's F01 assertion (audit-regressions.mjs with FETCHPROOF_AUDIT_SOURCE set to this checkout) passes and its three controls still pass, and derivePortableState on its four fixtures gives baseline APPROVED_BRIEF/true and visible_open_control, commented_old_table and fenced_example_table PREFLIGHT_BLOCKED/false; verified - readCorpus of this repository's research/ and of the audit's baseline and control fixtures is identical under the old and the new parser (unknowns, evidence, problems); verified - groups collect, requests, decompose, prior, preflight, checks and handoff pass, and artifact-validator's 54 tests at this commit pass; expected - the full suite, run by this commit's hook (ADR-0120): the commit exists only if it is green. What you got wrong: mistake - the first CHANGELOG entry lost two code spans (`buildAuthorized: true`, `appendRow`); where - CHANGELOG.md, this entry; impact - the entry would have shipped with two blanks, nothing delivered affected; cause - written through a double-quoted shell string, which ran the backticks as commands; fix - the two spans restored by a direct edit; verified - the staged diff of CHANGELOG.md, re-read before this commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… name is refused (independent audit 2026-11-04, F03) What changed: fetchproof/lib/artifact-zip.mjs - checkEntryName also refuses, as ZIP-PATH, an empty segment, a character Windows does not allow in a name (< > : " | ? * and the control characters U+0001 to U+001F), a segment ending in a dot or a space, a Windows device name (CON, PRN, AUX, NUL, COM0-9, LPT0-9, COM/LPT with a superscript 1-3, any extension, any case) and a segment shaped like an 8.3 short name (EVIDEN~1.MD). A new pathKey (NFC, then upper- and lower-cased) is the one identity inspectEntries compares duplicates, case collisions (now "differ only in case or Unicode form") and file-folder collisions by, so a file docs/a beside docs/A/child.md is a ZIP-PATH-COLLISION. openZip also returns `directories`, the directory entries' names. fetchproof/lib/artifact-validator.mjs - the never-packaged paths (FORBIDDEN_PAYLOAD) are compared by pathKey, as a file, as a directory entry and as a folder prefix of any entry. fetchproof/lib/artifact.mjs - the producer's collision refusal folds by pathKey and also refuses a file that is, spelled another way, the folder another file sits in. ADR-0197. Thirteen new tests. Why: the independent audit of 532c696 (Codex, finding F03, High) showed the reader judging a name by its spelling while an extractor writes it by its meaning. project/research//BRIEF.md passed beside the reviewed brief and the package validated PASS with buildAuthorized: true, while path.join (materializeProject) and Python's zipfile write it onto research/BRIEF.md; research/gate_off and research//GATE_OFF carried the gate override past the never-packaged check, which compared the exact spelling; readme.md., report.md:hidden, CON and a case-only file-folder pair each meant another file, or none, on Windows. Python's zipfile on Windows writes "_" for each character Windows does not allow, so research/GATE?OFF extracts as GATE_OFF, and a folder named GATE_OFF answers the gate's exists() as a file does. Set aside (ADR-0197): translating names (ADR-0032 refuses them); folding exactly as each filesystem does; judging by the validating host's filesystem; accepting 8.3 shapes; a second copy of the rules in the manifest schema's path pattern, which stays looser than the reader because a ZIP name problem stops validation before the manifest is read. Not covered, and named here so it is not taken as fixed: a gate-input path spelled only in another case (project/Research/brief.md with no research/BRIEF.md) is judged by the validating host's filesystem - approved on Windows and macOS, not on Linux; and the materialized copy is not written with exclusive create, which would make an alias these rules miss fail instead of overwrite (a follow-up). What it touched: fetchproof/lib/artifact-zip.mjs (checkEntryName, DEVICE_NAME, SHORT_NAME, pathKey, inspectEntries, openZip); fetchproof/lib/artifact-validator.mjs (the FORBIDDEN_PAYLOAD comparison); fetchproof/lib/artifact.mjs (collectProjectFiles's collision refusal); fetchproof/test/artifact-validator.test.mjs (+11 tests: nine hostile fixtures, the name rules on their own, an approved package carrying another spelling of a packaged or forbidden path); fetchproof/test/artifact-producer.test.mjs (+2); docs/adr/0197 and its index row; docs/ARCHITECTURE.md (rows artifact-zip.mjs, artifact-validator.mjs, artifact.mjs); CHANGELOG.md (Unreleased); fetchproof/README.md (2258 -> 2271 tests). No new error code, flag, check name or format. The other openZip callers, dispatch's unwrapArtifact and fi-validator's XLSX reader, read names - a package file name, the parts of an XLSX - that no new rule refuses. What you verified: verified - node fetchproof/bin/selftest.mjs artifact-validator fails the 11 new tests against the parent commit's library and passes 65/65 with the fix; artifact-producer fails both new tests against the parent's library (a scratch copy of fetchproof/ with the parent's three artifact modules) and passes 39/39 with the fix; verified - the audit's probe packages, validated read-only: under the parent's library brief-alias-order, gate-off-slash, gate-off-case, windows-alias, name-CON, name-b_md, name-report_md_ and name-report_md_hidden PASS with buildAuthorized true and case-directory-collision is BLOCKED; with the fix all nine FAIL (ZIP-PATH, ZIP-PATH-COLLISION or FILE-UNDECLARED) with buildAuthorized false, and the baseline package still PASSes - the probes crc-disagreement and local-header-disagreement still PASS, and belong to other findings; verified - the audit's F03 assertion (audit-regressions.mjs with FETCHPROOF_AUDIT_SOURCE set to this checkout) passes and its three controls still pass; verified - groups fi-validator-adapter, dispatch, artifact-cli, audit, evidence-context, hardening, collector-workflow, cli, repo-hygiene and bundle pass; verified - none of the 1845 files tracked in this repository has a name the new rules refuse, and no two share a pathKey; expected - the full suite, run by this commit's hook (ADR-0120): the commit exists only if it is green; expected - an 8.3-shaped name opens another file on a volume that makes short names: not reproduced, both volumes of this machine have short-name creation off. What you got wrong: (1) mistake - the producer's file-folder test was first written with a case-only pair (docs/Notes beside a folder docs/notes); where - fetchproof/test/artifact-producer.test.mjs, the second new test; impact - NTFS cannot hold that pair, so on Windows the test returned before asserting and counted as a pass without testing anything; nothing delivered affected; cause - the pair was chosen from the Linux case without checking what this filesystem holds; fix - a composed-name file beside a decomposed-name folder, which NTFS and ext4 hold as two; verified - it fails against the parent's library and passes with the fix. (2) mistake - the composed and decomposed names in both test files and the superscripts in DEVICE_NAME were first written as literal characters instead of escapes; where - artifact-validator.test.mjs (fixture 40, the accepted-names list), artifact-producer.test.mjs (both new tests), artifact-zip.mjs (DEVICE_NAME); impact - a composed and a decomposed name look identical, so a reviewer could not see the difference those tests rest on; nothing delivered affected; cause - the editing tool decoded the escapes as they were typed; fix - each character rewritten as an escape; verified - a byte dump of each line, and a search for non-ASCII bytes in the three files that finds only a line that was already there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…is not plain HTTP (independent audit 2026-11-04, F02) What changed: fetchproof/lib/browser-guard.mjs - parseUpstream throws for a proxy URL whose scheme is anything but http: (https:, socks5:, any other), with a message that names the scheme, host and port, says the browser route speaks only plain HTTP to its proxy and asks for the proxy's http:// address; it never contains the credential. A value with no scheme is still read as http://, whatever its port, and the default port is now just 80. startGuard reads NO_PROXY first and, under NO_PROXY=*, does not read the upstream at all; otherwise the throw leaves startGuard before it listens, so the guard child answers with its existing GUARD error before any browser is launched or any render folder made, and nothing is sent to the proxy. The refusal never falls back to a direct connection. ADR-0198. Two new tests. Why: the independent audit of 532c696 (Codex, finding F02, High) showed the guard speaking plain HTTP to an https:// proxy. The guard writes its requests (http.request) and CONNECT lines (net.connect, then a written line) to the upstream in clear, and parseUpstream kept the host, port and credential and dropped the scheme, so behind HTTPS_PROXY=https://user:pass@proxy:8443 the CONNECT line and its Proxy-Authorization header went in clear to a port that expects TLS: a TLS-only proxy refused every render, and one that also took plain HTTP there carried the render with the credential exposed. A socks5:// proxy was misread the same way. Set aside (ADR-0198): speaking TLS to an https: proxy, the complete fix and a new capability under the freeze (ADR-0117) - the audit names a refusal before any connection as the smaller repair; going direct, which routes around the operator's egress control; warning and continuing in plain HTTP; refusing under NO_PROXY=*, where the proxy is never contacted. Not covered, and named here so it is not taken as fixed: the audit's own F02 assertion expects a TLS handshake from the guard and stays red by design; doctor does not report the refused proxy (the first render does; a doctor line would be a new check); a proxy value from which no host can be read is still taken as no proxy, as before; the keyless route (Node's own proxy support, ADR-0047) is not this code and was not tested against an https: proxy. What it touched: fetchproof/lib/browser-guard.mjs (parseUpstream and its doc comment, startGuard's first lines); fetchproof/test/browser-guard.test.mjs (+2 tests: parseUpstream refuses https:, HTTPS:, socks5: and socks: values with and without a credential, keeps http:// and scheme-less values, startGuard rejects before the proxy accepts a connection and starts under NO_PROXY=*; through the child, an https:// HTTPS_PROXY with a credential is the render's GUARD error, the fake browser never runs and the proxy accepts nothing); docs/adr/0198 and its index row; docs/ARCHITECTURE.md (row browser-guard.mjs, and ADR-0198 in its ADR column); CHANGELOG.md (Unreleased); fetchproof/README.md (2271 -> 2273 tests). No new error code, flag, check name or format. parseUpstream has no other caller; renderThroughGuard, the only caller of startGuard outside the tests, is called only by the guard child, whose catch already reports a thrown error as GUARD. What you verified: verified - node fetchproof/bin/selftest.mjs browser-guard fails exactly the two new tests before the library change (27 passed, 2 failed: "Missing expected exception: https://proxy.corp:8443", and the child reporting errorCode null after the fake browser rendered behind the https proxy) and passes 29/29 with it; verified - the audit's F02 assertion (audit-regressions.mjs, --test-name-pattern F02, FETCHPROOF_AUDIT_SOURCE set to this checkout) now fails at startGuard with the refusal message, before its fixture proxy receives a byte, where it failed on a plaintext CONNECT before; verified - groups browser-transport, transport, doctor, proxy, handoff, searxng and checks pass, 400/400 (their tests set proxy variables or spawn the guard child; proxy.test.mjs's https:// proxy is the keyless route's, which this change does not reach); expected - the full suite, run by this commit's hook (ADR-0120): the commit exists only if it is green. What you got wrong: nothing to report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ions, and neither passes over it (independent audit 2026-11-04, FP-011) What changed: fetchproof/lib/checks.mjs - a new lostEvidenceTable(corpus) reads the snapshot's evidenceFile and answers, for a gated project's research/EVIDENCE.md, evidence-missing when the file does not exist or is not a readable file (the detail says which), evidence-unparsed when it holds no evidence table (no header row of its six columns outside a code block or an HTML comment), else nothing. citations returns that one fail and nothing else, in place of its pass over zero rows; corpus-shape names it beside raw-missing, so it no longer says the corpus parses cleanly. Both details name the file and say to restore it rather than edit the citations that now point at nothing. The dangling-citation findings are unchanged. fetchproof/lib/corpus.mjs - readCorpus's evidenceFile also carries unreadable, true when something stands at the path that could not be read as a file; readCorpus records no problem for it. ADR-0199. Four new tests. Why: the independent audit of 532c696 (Claude, finding FP-011, alias B-2, Medium) showed readCorpus reading a missing research/EVIDENCE.md, or a directory in its place, as an empty table with no problem recorded: readText answers null for both, parseTable(null) is an empty table, and the snapshot's evidenceFile was read only by the handoff. corpus-shape said "the corpus parses cleanly", citations "0 evidence rows, each with a cached page", and the verdict failed only on the citations that now dangled - which send the reader to edit citations rather than restore the table - while a corpus whose unknowns were all KNOWN-UNKNOWN cited nothing and PASSED with its evidence table gone. A file holding no evidence table (emptied, or keeping the table only in a code block) was read the same way. The shape (LAYOUT: required, gating) and the handoff (handoff-evidence-missing, handoff-evidence-unparsed) already called the file required. Set aside (ADR-0199): judging it only once something was collected (the audit's guard for a fresh project, which is written with the file and fails on its OPEN unknowns anyway); one check naming it while the other keeps its pass (corpus-shape alone leaves `preflight --check citations` passing over the loss); a new readCorpus problem kind, which would travel into the portable state's corpusProblems; a new check; dropping the dangling citations. Not covered, and named here so it is not taken as fixed: MAP.md and SOURCES.md, lost the same way, are unchanged (neither is required or gating; the audit leaves the map's case open); a commit confined to research/ that deletes the file is still committable as phase-1 work (ADR-0048), as before. What it touched: fetchproof/lib/checks.mjs (lostEvidenceTable, citations, corpusShape, the HEADERS import); fetchproof/lib/corpus.mjs (readCorpus's evidenceFile); fetchproof/test/checks.test.mjs (+4 tests: the file deleted, a directory in its place, a file with no evidence table - prose only, and the table kept only in a fence - and a KNOWN-UNKNOWN-only corpus that passes with its table and fails without it, on exactly the two new findings); docs/adr/0199 and its index row; docs/ARCHITECTURE.md (rows checks.mjs, with ADR-0199 in its ADR column, and corpus.mjs); CHANGELOG.md (Unreleased); fetchproof/README.md (2273 -> 2277 tests). No new check, flag, error code or format; the two finding names are new rules inside existing checks. What you verified: verified - red first: with the four tests and without the library change, `selftest checks` failed exactly those four (104 passed, 4 failed): corpus-shape said "the corpus parses cleanly" over each lost table, and the KNOWN-UNKNOWN-only corpus passed without its table (its control, the same corpus passing WITH the table, held); verified - green: `selftest checks preflight` 120 passed, 0 failed with it; verified - the groups that read the evidence table, the corpus or the verdict (artifact, audit, brief, bundle, corpus, evidence-context, gate, hardening, handoff, measure, provenance, quote, requests, scaffold, skill-set, stdout-flush, warc, collector-workflow, doctor) 889 passed, 0 failed; verified - the other groups that judge a gated project (cli, hook, prior, mcp, panel, decompose, git-output-trust, contract-read-diagnostic, installer-project-guard, conformance-hostile) 378 passed, 0 failed; verified - by hand, on a project scaffolded by new-project.mjs: `preflight --check citations` and `--check corpus-shape` exit 0 with the file, exit 1 naming citations/evidence-missing and corpus-shape/evidence-missing once it is deleted, and exit 1 naming it "not a readable file" with a directory in its place; expected - the live workflow's integrity checks (live-collection.yml, the same two among them) are unaffected, since its scratch project is scaffolded the same way (not run: it needs the network); expected - the full suite, run by this commit's hook (ADR-0120): the commit exists only if it is green. What you got wrong: mistake - ADR-0199 (decision 2 and a rejected alternative) and this message's draft named the run of one check `--only citations`; preflight's flag is `--check`, `--only` belongs to research.mjs, and preflight refuses it with exit 2; where - docs/adr/0199, lines 41 and 63, and this message's Why part; impact - a reader following the ADR would have run a command preflight refuses, nothing delivered affected; cause - took research.mjs's flag for preflight's without reading preflight's usage; fix - both now say `preflight --check citations`; verified - preflight.mjs's usage reads "--check <name> run one check (repeatable)", grep finds no `--only` left in ADR-0199 or in the Why part, and the probe above ran the corrected command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…umns, inside list items (review of #441) What changed: fetchproof/lib/corpus.mjs - `blockContext`, which was fed each line trimmed, is replaced by one pass over the raw lines, `hiddenLines(lines)` -> `{ at, open }`: `at[i]` is null for a line that can be a table's, else `{ kind: 'fence' | 'comment' | 'indented', line }` naming the line that opened it; `open` is the fence or comment a document ends inside (a fence only outside list items). CommonMark's block rules in columns (a tab to the next multiple of 4), each line measured from the content column of the list items it stays inside: list items open at `-`/`+`/`*`/`1.`/`1)` indented 0-3 and end at a less-indented non-blank line, a fence in them with them; a fence opens and closes indented 0-3 from its container; a line indented 4 or more opens nothing and is indented code when no paragraph is open; a comment opens at `<!--` where a line's content starts (after a list marker too) and runs to the line holding its `-->`, and a `<!--` after that `-->` reopens it, while `<!-->` and `<!--->` close at once. `parseTable`'s header search, its stray-row scan and its search for the heading above the table all read it: a table in indented code is now hidden, and a heading in a hidden block heads no section. Helpers `skipSpace` and `commentOpenAfter`. Three tests. ADR-0203. Why: the fresh-eyes review of #441 (2026-11-06), findings 3 and 4. Finding 3, new in #441: trimmed, four spaces and ``` or <!-- - indented code on GitHub - opened a fence or a comment that ran to the end of the file, so an indented example above the table hid every table below it: on a copy of a real project of this repository preflight failed twelve times ("the contract enumerates no blocking unknowns"), an EVIDENCE.md was "holds no evidence table" with the remedy "restore the file", and with the example's --> indented below the live table the visible OPEN table was passed over for an archived CLOSED copy further down, which passed. Finding 4, older (main passes them too) but claimed closed by #441's words: an archived CLOSED copy after a list marker (`- <!--`, `1. ````), below a fence line indented four (its content, taken as its closer), or in a comment reopened on the line that closed one, was read as the contract. Fixed with the class: a tab is four columns; a heading in a fence above a table set the section the stray-row scan runs to (found while fixing; older). Set aside (ADR-0203): the review's smallest fix, the 4-column rule outside list items - without the items' columns a fence in a list item is either missed, reading the archived copy the review's case 01 keeps there, or every 4-column line in an item is code; stripping list markers from trimmed lines (mends 4a only); a Markdown parser (the kit has no dependencies); hiding every 4-column line (prose in a paragraph or an item is shown); narrowing the words instead (leaves the false PASS). Named as not read: block quotes, a list item's lazy continuation, other HTML blocks, a mid-line `<!--`, an escaped `-->` in a reopened comment. What it touched: fetchproof/lib/corpus.mjs (`skipSpace`, `commentOpenAfter`, `hiddenLines`, `parseTable`'s three searches and its doc and comments); fetchproof/test/corpus.test.mjs (the F01 test's `hideAbove` hoisted to the module and shared; +3 tests: an indented example of a fence or a comment, with spaces or a tab, above the contract and above the evidence table - the table is read and a correct project passes; the indented comment example below the live OPEN table - OPEN read, preflight fails; eight shapes of an archived copy - in a list item's fence, after `- `, after `1. `, after `-` and a tab, below a fence line indented four, reopened after a comment, reopened on a closing line, indented as code - each hidden, OPEN read, preflight fails, plus `<!-->`/`<!--->` hiding nothing and CRLF; a heading in a fence, a comment or indented code does not set the section); docs/adr/0203 and its index row; docs/ARCHITECTURE.md (row corpus.mjs: the F01 sentence rewritten for `hiddenLines`, ADR-0203); CHANGELOG.md (the F01 entry under Unreleased, which this completes: "a code fence" -> "a code block", Two tests -> Five); fetchproof/README.md (2277 -> 2280 tests). No new check, flag, error code or format. What you verified (cwd E:/Claude/fp-p0-followup throughout): verified - red first: with 1078ead's corpus.mjs in place, `node fetchproof/bin/selftest.mjs corpus` gave 53 passed, 3 failed, exactly the three new tests (the F01 test stays green). Verified - the review's cases (`E:\Temp\review-pr441\repro\cases`, read by 1078ead's reader and this one): 02-06, 07 and 07 with two visible tables now read the visible OPEN table where #441 read the archived CLOSED copy; 12 and 13 read U-1 CLOSED and U-2 KNOWN-UNKNOWN where #441 found no table; 14 reads E-01 where #441 found none; 00, 01 and 08-11 read as before. Verified - green: the 25 groups that read or write a corpus table (corpus checks preflight handoff brief measure requests collect decompose audit prior warc scaffold provenance hardening evidence-context finding gate artifact-producer artifact-validator artifact-cli contract-read-diagnostic architecture-map), 1017 passed, 0 failed in 906 s. Verified - differential, 1078ead's reader against this one: the repository's 44 corpora (its root and every project under docs/decisions) read the same rows and problems, with the same preflight verdict and failing rules; over 7246 markdown files under E:\Claude and the review's cases, each searched for the four tables' headers, 30 readings differ, all of them the ten case files above (three headers each). Verified by this commit's hook: the full suite (ADR-0120). What you got wrong and fixed: 1. mistake: F01's reader judged trimmed lines ("Indentation is not weighed"), so an indented line opened a fence or a comment; where: corpus.mjs `blockContext` and its call in the header search (b2ae053); impact: a correct project with an indented example above its contract or evidence table failed preflight with the wrong remedy, and the indented-comment shape passed an OPEN unknown - new in #441, not merged; cause: I gave the header search the stray scan's opener test, which had trimmed its lines, without testing an indented opener; fix: `hiddenLines` reads the raw lines in columns; verified: the first new test, red at b2ae053's reader, green here (below). 2. mistake: #441's words said the class was closed - "of those GitHub renders as a table" (parseTable's doc) and "one `blockContext` says which lines GitHub shows as code or hides" (ARCHITECTURE, row corpus.mjs) - while five shapes of a hidden copy were read; where: corpus.mjs parseTable's doc, docs/ARCHITECTURE.md:202 (b2ae053); impact: the record overstated the fix; not merged; cause: I tested the shapes the audit named, not my own pre-mortem's list (list items, a comment closed mid-line, fence indentation); fix: the shapes are read, the doc names what is not; verified: the second new test, red then green (below). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lection asks before it spends (review of #441) What changed: fetchproof/lib/corpus.mjs - `appendRow`, finding no table the reader reads, refuses with code `TABLE_HIDDEN` and writes nothing when the file ends inside a fence or a comment never closed, or when a table with its header stands on hidden lines with rows below it; it used to start a table at the end of the file. A hidden header with no rows - an example, commented out - is left alone, and a table is still started below it. `tableHidden(text, header)` says which (the record of the hiding block, `open` true for a block the file ends inside); `assertTablesWritable(root)` asks it of the evidence and sources tables; the message names the file, what hides the table, the line that opened it, and the remedy. `headerAt(lines, i, header)` is factored out of `parseTable`, so the reader and the refusal recognise a header by one rule. fetchproof/lib/collect.mjs - `collectOne` asks under the lock, before its fetch, as `appendFetch` asks again of the ledger. fetchproof/lib/research-run.mjs and fetchproof/lib/decompose.mjs - `runResearch` and `decompose` ask beside `assertAppendable`, before anything is spent, dry runs too. fetchproof/lib/core.mjs - `TABLE_HIDDEN` is a named run refusal. fetchproof/bin/decompose.mjs - the named run refusals are answered with their message and exit 2, as bin/research.mjs answers them. Five tests. ADR-0204. Why: the fresh-eyes review of #441 (2026-11-06), finding 1, new in #441: F01 taught the header search to skip what GitHub hides, and with a fence above the evidence table never closed (a pasted command) the reader found no table, so `appendRow` wrote a header, a separator and the row at the end of the file - inside the open fence, read by nothing - and `nextId`, counting the rows it could see, issued E-01 again on every run. The collector reports that id to the builder, who cites it; once the fence is closed, the first table is read and every citation of a new E-01 resolves to the old E-01, another page. The review's repro wrote E-01, E-02, E-01, E-01 into three tables (main: E-01..E-04 into one). The same writer appends a builder request's OPEN unknown to DISCOVERY.md and a source to SOURCES.md. The check runs before the money because a refusal by the writer alone comes after the fetch: the capture and its ledger entry stay with no row, and the next run takes the capture as a cache hit and never writes the row. bin/decompose.mjs re-threw every named refusal - a broken or torn ledger, a held lock - as a stack trace and exit 1, which the kit reserves for FAIL (found while fixing this; older than #441). Set aside (ADR-0204): closing the open block, then appending (edits the operator's structure, and splits a hidden table from its new one silently); appending with a warning (the row is still read by nothing, its id still a held one); `nextId` over every id in the file (stops the duplicate ids, not the hidden rows); refusing any hidden header (a commented-out example loses nothing); refusing only in the writer (after the fetch) or only under the lock (after the run's searches). What it touched: fetchproof/lib/corpus.mjs (`headerAt`, `tableHidden`, `tableHiddenError`, `assertTablesWritable`, `appendRow`, `parseTable`'s header search); fetchproof/lib/collect.mjs (`collectOne`); fetchproof/lib/research-run.mjs and fetchproof/lib/decompose.mjs (the checks before the money); fetchproof/lib/core.mjs (`NAMED_RUN_REFUSALS`); fetchproof/bin/decompose.mjs (its catch); fetchproof/test/corpus.test.mjs (+2: an open fence above the table, an open comment above it, an open fence with no table, the table with rows in a comment and indented as code - each refused, the file byte-identical, the message naming the block and its line - and `upsertRow` on a hidden SOURCES table refused; a hidden header with no rows and a visible table above an open block still take the row); fetchproof/test/hardening.test.mjs (+1: `runResearch`, its dry run and `decompose` refuse a hidden evidence table with zero searches and fetches, and `collectOne` refuses before its fetch); fetchproof/test/requests.test.mjs (+1: a request into a hidden contract table is refused, the contract and the plan unchanged, no request plan written); fetchproof/test/cli.test.mjs (+1: research.mjs and decompose.mjs answer a hidden evidence table with the message and exit 2, no stack); docs/adr/0204 and its index row; docs/ARCHITECTURE.md (rows core.mjs, corpus.mjs, collect.mjs, research-run.mjs, decompose.mjs); CHANGELOG.md (the F01 entry under Unreleased, which this completes; Five tests -> Ten); fetchproof/README.md (2280 -> 2285 tests). One new error code, `TABLE_HIDDEN`, for the refusal; no new check, flag or format. What you verified (cwd E:/Claude/fp-p0-followup, but the red run): verified - red first: the four changed test files copied into a worktree of 86df9a0 (E:/Temp/fp-red-a, removed after), `node fetchproof/bin/selftest.mjs corpus hardening requests cli` gave 341 passed, 4 failed in 941 s, exactly the four refusal tests (the corpus refusal, hardening's collection, the request, the CLI's exit 2); the fifth new test, "still writes", passed there too, as it should: it pins what the writer keeps doing. Verified - green: the 12 groups that write a table or answer for a named refusal (corpus collect hardening requests cli decompose collection-prerequisites collector-workflow concurrency live-collect auto-accounting auto-reservation), 575 passed, 0 failed in 1012 s. Verified - `tableHidden` over nine shapes (probe th.mjs): a visible table, null; a fence or comment never closed above the table, or with no table, open at line 3; a closed comment holding the table with a row, and the table indented four, held at line 3; a hidden header with no row, a visible table above a fence never closed, and an empty file, null. Verified - the review's repro pointed at this commit (append-hidden-a.mjs): both rows refused with TABLE_HIDDEN naming the fenced code block at line 4, the file unchanged, and `assertTablesWritable` refusing the same file; the review's own run found main writing E-01..E-04 into one table and #441's head writing E-01, E-02, E-01, E-01. Verified by this commit's hook: the full suite (ADR-0120). What you got wrong and fixed: 1. mistake: F01 taught the reader to skip hidden lines without asking what the writer does when the reader then finds no table; where: corpus.mjs `appendRow`'s branch for an absent table (b2ae053); impact: a cited E-## row written where no reader reads it, its id issued again on each run - new in #441, not merged; cause: I tested the writer against an archived copy in a closed comment above a visible table, not a block left open; fix: the refusal and the checks before the money; verified: the first new corpus test, red at the reader commit, green here (below). 2. mistake: ADR-0204's first draft said decompose.mjs answers the named refusals with exit 2 "as it answers a broken ledger", and the next draft listed an incompatible CLI among the refusals decompose re-threw; where: the draft, before staging; impact: none delivered - the record would have claimed an answer the command did not give, and a refusal it never meets (only runResearch throws CLI_INCOMPATIBLE); cause: I generalised from bin/research.mjs and runResearch without reading bin/decompose.mjs's catch or grepping the code's throw sites; fix: bin/decompose.mjs answers them, and the ADR names only the refusals decompose can meet; verified: the cli test (exit 2, no stack), red then green (below), and `grep CLI_INCOMPATIBLE` finds one throw, in research-run.mjs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…me, and auto-collect asks before it reserves (re-review of #441) What changed: fetchproof/lib/corpus.mjs - `hiddenLines` reads as GitHub does: (1) a byte-order mark before the first line is skipped, so a fence or a comment opened there opens; (2) an item opened on an empty line ends at a blank line whose spaces stop short of its content, unless a line has gone into it (`held`), so a fence or indented code below it is code; (3) a run of `=` or `-` under a paragraph (`SETEXT_UNDERLINE`) makes it a heading and ends it, so a line indented four below it is code; (4) a line indented less than an item's content that continues the item's paragraph and starts no block is a lazy continuation line and keeps the item (`startsBlock`: a thematic break, a list marker, `>`, an ATX heading, three backticks or tildes, `<`, or a footnote definition `[^label]:`, judged at its column in the container it would fall back to, found by a binary search, `baseAt`); a paragraph line above a table's delimiter row (`delimiterRow`: cells of `-` with an optional `:` at either end, between pipes or with none) is that table's header, and no line continues a table lazily; (5) the thematic-break test is asked once per line (`breakSpan`, the indexes from which the rest of the line is a break) and a list marker is tried at an index with a sticky regex, so a line of n markers is read in linear time - each marker had sliced the rest of the line and tested it. Its comment names what stays unread (footnote definitions, which GitHub drops with all they hold when nothing refers to them) and what is taken the safe way (a line led by `<` or by a fence's characters, and a line after a `<`-led paragraph line or a delimiter row of another width: the item ends where GitHub may keep it). fetchproof/lib/auto-collect.mjs - `collect` asks `assertTablesWritable(target)` first in its `try`, so a request into a project whose evidence or sources table is hidden gets a refused result, committed and pushed, before its pages are reserved, the request is applied or research runs. Five tests. ADR-0203 and ADR-0204 amended. Why: the re-review of #441's two fix commits (2026-11-06) found six defects, all in code this PR adds, none merged. Four are regressions 86df9a0's column reading brought in over b2ae053's trimmed lines: (1) `trim()` drops a byte-order mark and the raw lines kept it, so with one (Windows PowerShell 5's `-Encoding utf8` writes it) a fence or a comment opened on the first line was not seen, the archived copy in it was read as the contract, and `appendRow` wrote a row into a comment never closed; (2) an item opened on an empty line was held open across a blank line, so a fence at its column below was the item's, ended at the table's own line, and the fenced copy was read; (4) a lazy continuation line ended its item, so a table indented in the item, which GitHub shows, was read as indented code and a correct contract as having no unknowns; (5) a line of n list markers took time in n squared - 22.6 s for 80 KB in the review, 18.6 s in the red run here - and a package's files reach the reader through the validator. (3) was a hole in 86df9a0's indented-code rule: a heading's `===` or `--` was read as its paragraph's next line, so a copy indented four below a setext heading, code on GitHub, was read. (6) `research.mjs` refused a hidden evidence table (ADR-0204), but only after auto-collect had reserved the request's pages, and with no accounting to settle them by: the pages were charged, the queue paused, and the request went back as failed. Each rule was checked on GitHub's own renderer before it was kept. Set aside (ADR-0203): ending an item at any line indented less than its content (finding 4 itself); taking a line that holds a `|` as a table's, never lazy (it hides `a | b` text GitHub shows, and reads lazily past a pipe-less table at which GitHub ends the item). What it touched: fetchproof/lib/corpus.mjs (`hiddenLines`; new `breakSpan`, `startsBlock`, `delimiterRow`, `SETEXT_UNDERLINE`; `LIST_MARKER` made sticky; `THEMATIC_BREAK` removed, no other user); fetchproof/lib/auto-collect.mjs (`collect`, the import); fetchproof/test/corpus.test.mjs (+4: an archived copy fenced or indented below an empty item, an empty ordered item or a setext heading is hidden, and a blank line whose spaces reach the item keeps it; a comment or a fence opened on a first line led by a BOM hides its copy, and `appendRow` refuses a file ending in such a comment, unchanged; a table indented in an item below a lazy line is the table, for the contract and the evidence, with six lazy shapes as GitHub rendered them; 40,000 markers on a line, and 40,000 lazy lines below, read within 2 s); fetchproof/test/requests.test.mjs (+1: auto-collect refuses a request into a project whose evidence table is in a comment never closed - nothing run, no page charged, no pause, the contract unchanged, no request plan); docs/adr/0203 (Prompted by, Context, decisions 2, 3 and 5, two rejected alternatives, Consequences) and docs/adr/0204 (Area, Prompted by, decision 3, Consequences), both of this unmerged PR, amended in place, titles unchanged; docs/ARCHITECTURE.md (rows corpus.mjs, auto-collect.mjs); CHANGELOG.md (the F01 entry under Unreleased; Ten tests -> Fifteen); fetchproof/README.md (2285 -> 2290 tests). No new check, flag, error code or format. What you verified (cwd E:/Claude/fp-p0-followup, but the red run): verified - red first: the two changed test files copied into a worktree of 4433d10 (E:/Temp/fp-red-p0c, removed after), whose harness copy alone was given a name filter for the five new tests, `node fetchproof/bin/selftest.mjs corpus requests`: 0 passed, 5 failed in 26.0 s, each for what it pins - U-1 read CLOSED from the copy fenced below an empty item; U-1 read CLOSED from the copy commented on a BOM-led first line; no unknowns read from the item's table; "40,000 list markers on one line: 18553 ms"; the request "collected" where it is refused, the stand-in research having run. Not reached there, as each follows an assertion that failed first: the kept item, the hidden lazy shapes and the 40,000-lazy-line case, which pin this commit's own rules; the footnote and pipe-less lazy shapes were red against its first lazy rule (probe, below). Verified - green: `node fetchproof/bin/selftest.mjs corpus requests auto-accounting auto-reservation collect collector-workflow checks preflight artifact decompose hardening cli` (17 test files), 782 passed, 0 failed in 739.1 s; one sentence of `hiddenLines`' comment was reflowed after it, comment only, and `node --check` re-run. Verified - GitHub's renderer: 22 synthetic cases rendered with `gh api markdown` on 2026-11-06 (scratch, not committed): the empty-item, setext, blank-line-with-spaces and BOM cases are the tests' shapes; of the eight lazy-line cases read through this reader (probe.mjs), six agree with GitHub, and two - a `<`-led paragraph line, a delimiter row of another width - end the item GitHub keeps, decision 5's safe-way shapes; the footnote cases fixed the residual decision 5 names. Verified - a grep for the bytes EF BB BF finds no byte-order mark in any changed file, and `node --check` passes on both modules. Verified by this commit's hook: the full suite (ADR-0120). What you got wrong and fixed: 1. mistake: 86df9a0 read four shapes otherwise than GitHub and worse than b2ae053 had (findings 1, 2, 4 and 5); where: corpus.mjs `hiddenLines` (86df9a0); impact: on this branch, not merged - an archived copy read as the contract (1, 2), a correct contract read as having no unknowns (4), 18-23 s on an 80 KB line (5); cause: I wrote the column rules from CommonMark and tested them on shapes of my own, none with a BOM, an empty item or a lazy line, and none rendered by GitHub; fix: the rules above, each checked on GitHub's renderer; verified: the four corpus tests, red at 4433d10, green here. 2. mistake: a heading's underline was read as its paragraph's next line (finding 3); where: `hiddenLines`' paragraph rule (86df9a0); impact: a copy indented four below a setext heading was read, as before #441; cause: the rule listed the blocks that end a paragraph and missed the setext underline; fix: `SETEXT_UNDERLINE` ends it; verified: the two setext cases of the first corpus test, red then green. 3. mistake: this commit's first lazy rule took a line holding a `|` as a table's, never lazy; where: the last line of `hiddenLines`, before staging; impact: none delivered - it read lazily past a pipe-less table (`:--`) and a footnote definition, which GitHub ends the item at, so a table GitHub hides was read, and hid `a | b` text GitHub shows; cause: I keyed it on a table row's shape before rendering the cases on GitHub; fix: `delimiterRow` decides, and a footnote definition starts a block; verified: the probe, before and after, and the lazy test's six shapes, green. 4. mistake: ADR-0204 put the table check before the money in the entry points that call `runResearch`, and missed the one that reserves pages and then spawns `research.mjs` (finding 6); where: auto-collect.mjs `collect` (4433d10); impact: on this branch, not merged - a refused run charged its pages and paused the queue; cause: I listed the callers of `runResearch`, not the paths that spend; fix: `collect` asks first; verified: the requests test, red then green. 5. mistake: the Write tool decodes a backslash-u escape in its input into the character, as the Edit tool does, and I wrote the BOM test's text with it as if it did not; where: the BOM test's text, spliced into corpus.test.mjs; impact: two literal byte-order marks in a test file, caught before staging; cause: I had seen the decoding only in the Edit tool; fix: a script restored the escapes; verified: the grep above. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…der GitHub makes no table of, and a row stays in its list item (re-review of 9b9afaa) What changed: fetchproof/lib/corpus.mjs - `hiddenLines` is a port of cmark-gfm's block parser (blocks.c, table.c), line by line: a line ends at a carriage return alone too (`segs`); block quotes, list items and footnote definitions are containers continued by cmark's rules; the openers run in cmark's order - the seven HTML block types (`htmlStart`, `htmlTag`, `htmlEnds`), fences (`fenceOpens`, `fenceCloses`), ATX and setext headings, thematic breaks (`breakTest`), footnote definitions, list items (`listMarker`), indented code, a table's header and rows (`delimiterCells`, and `rowCells`, cmark's row_from_string); lazy lines; a table's cell cap and its one attempt per paragraph; a comment reopened after its `-->` (`commentOpenAfter`) or cut off where its container ends (`runaway`, hidden to the end of the file); a footnote definition nothing refers to, hidden. It returns `{ at, open, heads, rowOf, segs, endWhy }`; `startsBlock`, `delimiterRow`, `breakSpan`, `baseAt` and the rules of its own are gone. `parseTable` takes the first header GitHub makes a table of, outside hidden lines; with none, it names the first header GitHub makes none of, with the reason and the remedy (`notATable`, `NOT_A_TABLE`), or the first one a cut-off comment hides below the cut; it reads the rows GitHub reads (`rowOf`); it names the line that ended them when GitHub shows it apart from the table (outside its item, indented 4, a carriage return alone, no cell); the end of the rows run on below a named line is found once (`runEnd`), and `isSeparator` reads a line in one pass. `tableHidden` passes only a header GitHub makes a table of, and otherwise returns the record - the block hiding it, or `{ kind: 'not-a-table', why }` - when rows follow it, `open` first; `tableHiddenError` words a runaway, a header made no table of (the reader's reason and remedy), an HTML block and an unreferenced footnote, and says "which GitHub does not show as a table". `appendRow` and `upsertRow` keep the leading whitespace of the row they follow or replace. fetchproof/test/corpus.test.mjs - eight tests. ADR-0203 and ADR-0204 amended; the ADR index rows, the corpus.mjs map row, the CHANGELOG F01 entry and the README count follow. Why: the re-review of 9b9afaa (E:/Temp/review-followups/c-findings.md, 2026-11-07) made six findings, and the owner's word was to fix every one before the merge. F-A (medium): the "safe way" of ADR-0203's decision 5 failed open - where it ended an item GitHub keeps (led by an autolink, inline code or a tag, or a delimiter row of another width), a comment or fence right below at the item's column was read as text, so an archived CLOSED copy passed preflight over the live OPEN table and `appendRow` wrote into it. F-B (medium): a header and delimiter row as lazy lines below an item were read as a table GitHub does not make, and column-0 rows below an item's table as its rows. F-C (low): footnote definitions held nothing, and one nothing refers to was read. F-D (low): a carriage return alone hid a fence's opener. F-E (low): `isSeparator` and the stray scan's `runsOn` took quadratic time, against ADR-0203's "linear". N1 (low): a comment cut off by its item was taken as closed. Set aside: widening the safe way (the reviewer's second option), which still reads tables GitHub does not show and blocks ones it shows; stripping one trailing carriage return; hiding a cut-off comment only to its next raw `-->` - each in ADR-0203. What it touched: fetchproof/lib/corpus.mjs (`hiddenLines` and its scanners, `parseTable`, `notATable`, `tableHidden`, `tableHiddenError`, `appendRow`, `upsertRow`, `isSeparator`); fetchproof/test/corpus.test.mjs; docs/adr/0203 and 0204, docs/adr/README.md (rows 0203, 0204), docs/ARCHITECTURE.md (the corpus.mjs row), CHANGELOG.md (the F01 entry), fetchproof/README.md (the count). `hiddenLines`, `tableHidden`, `isSeparator` and `splitRow` have no caller outside corpus.mjs and its tests; `parseTable` and `assertTablesWritable` keep their shapes for collect, requests, research-run, decompose, prior and the artifact validator. No new check, flag, error code or format (ADR-0117): a header made no table of is a `table-split` problem, a refusal `TABLE_HIDDEN`. What you verified (cwd E:/Claude/fp-p0-followup): - verified: the eight tests red before the port (corpus 62 passed, 8 failed) and green after (70 passed, 0 failed). - verified: the groups requests, auto-accounting, auto-reservation, collect, collector-workflow, checks, preflight, artifact, decompose, hardening, cli, architecture-map and repo-hygiene - 753 passed, 0 failed in 748 s, before the docs were edited; the hook runs the whole suite on the staged tree. - verified: the reviewer's probes, copied and pointed at this tree: C1-C9 read the live OPEN table (9b9afaa the CLOSED copy), K1-K3 unchanged; preflight fails on C1-C3, on the lazy table and on the stray-CR projects, which 9b9afaa passed; `appendRow` writes below the live table, not into the copy; L1 and L2 read the live table, L3 the table in the item; L4 names the column-0 row and reads none; F1 and F2 hidden, F3 unchanged; N1 hidden from the cut, and the writer refuses; S1 and S2 read the live table; 40 KB of a separator's characters read in 8 ms, 8,000 rows run on below a named line in 40 ms, every shape of the linear probes up to 781 KB under 1 s. - verified: across the repository's 45 corpora (180 tables, 6,872 lines) the reader reads every table as 9b9afaa and 1078ead read it, refuses none, and none ends inside a block. - verified: the rules on GitHub's renderer, 55 synthetic cases (`gh api markdown`, 2026-11-07), and cmark-gfm's blocks.c and table.c read. expected: that a file with CR CR LF line ends shows no table on GitHub, as the reader now says (a stray CR on one line was rendered; a whole file of CR CR LF was not). - verified: the cut-off comment's named header, red on the first condition (69 passed, 1 failed), green on the new. - expected: 2298 tests in the full run (2290 at 9b9afaa, and eight); only the full run counts them. What you got wrong and fixed: 1. mistake: decision 5's "safe way" failed open (F-A); where: corpus.mjs `startsBlock` and the lazy rule (86df9a0, 9b9afaa); impact: on fix/audit-p0, not merged - an archived CLOSED copy under such an item passed preflight over a live OPEN table, and `appendRow` wrote into it; cause: I took ending an item early as erring toward a block, without asking what a line at the item's column becomes once the item is gone; fix: the port, which has no shape to take a way about; verified: test 1, probes C1-C9 and K1-K3, the e2e. 2. mistake: a header and delimiter row read as a table wherever they stood (F-B, older than this branch); where: `parseTable`'s header test; impact: a lazy table GitHub does not show was the contract, and column-0 rows were an item's table's rows; cause: I asked whether a delimiter row follows, not whether GitHub opens a table there; fix: `heads` and `rowOf`, and the reasons named; verified: tests 2 and 7, probes L1-L4. 3. mistake: footnote definitions named as not read, as if that erred toward showing (F-C); where: ADR-0203 decision 5 (86df9a0); impact: a comment or fence at column 4 below `[^x]:` was text, and the copy in it passed; cause: I named the residual without tracing a block inside it; fix: footnote containers, and an unreferenced definition hidden; verified: test 3, probes F1-F3. 4. mistake: `(.*)$` did not match past a lone CR (F-D); where: the fence opener test (86df9a0); impact: a fence opened on a CR CR LF line was not seen, and its copy passed; cause: I split lines as the kit does, not as cmark does; fix: each line read as its CR-separated parts; verified: test 4, probes S1, S2, B1-B6. 5. mistake: ADR-0203 said "linear" with `isSeparator` and `runsOn` quadratic (F-E); where: corpus.mjs, the ADR; impact: a long separator-shaped line or many run-on rows took seconds; cause: I timed `hiddenLines`, not the reader; fix: a one-pass `isSeparator`, `runEnd` found once; verified: test 8, probes separator and runson. 6. mistake: a comment cut off by its item taken as closed (N1, older); where: `hiddenLines`; impact: the table below it was read, where the browser hides it; cause: I ended the comment with its block as cmark does, without asking what the page then holds; fix: `runaway`, hidden to the end, and the writer refuses; verified: test 5, probe N1. 7. mistake: the first header a runaway hides was named even when it was the copy inside the comment; where: `parseTable` (this commit's draft, and test 5 pinned it); impact: none delivered - the problem pointed at line 7, not the live table at line 12; cause: I named the first hidden candidate without asking which one the operator means; fix: the first below the cut, else the one inside; verified: red on the old condition, green here. 8. mistake: the arity message drafted with four backslashes before its pipe, and a column-0 header of another table after an item's table would have been named as standing outside the item; where: the scratch draft of `parseTable`, before it was spliced; impact: none delivered; cause: a placeholder doubled once too often, and a case I had not walked; fix: two backslashes, as before (the diff shows the line unchanged), and 'outside' only when no delimiter row follows; verified: corpus 70 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…footnotes, a reopened comment and lazy headers are read as GitHub shows them (re-review of 88fe19a) ## What changed - `fetchproof/lib/corpus.mjs` - `escapeCell` writes LF, CR LF and a carriage return alone as a space. - `writeCheck` (internal) parses once for `tableHidden`, `appendRow` and `upsertRow`. Where the reader finds the table and names a `table-split` in it, the write is refused (`kind: 'split'`) with the table's line, the line named and the reader's words; `tableHiddenError` words it. `assertTablesWritable(root, tables)` takes the `[path, header]` pairs to ask (default: evidence and sources). The second `parseTable` in `appendRow` is gone. - `hiddenLines` returns `note`: the footnote definition GitHub shows that holds a line, with `order`, its first reference's place. The header search takes a header in one only when the body holds none, the lowest `order` first. - Of a label defined twice, the definition that ends first is kept, as cmark-gfm's `process_footnotes` keeps it (a nested one ends before the one holding it); the other is hidden, and the writer names it ("repeats the label of the one at line N"). - A comment reopened by a `<!--` after its block's `-->` is a record of its own (`reopened`), closed only by a `-->` in an HTML block. A header hidden by a block the file never closes is named in the reader (`table-split`), with the block. - Footnote definitions, shown or dropped, mark each line once, with the outermost one holding it. - A lazy header with leading whitespace is `lazy-header`, with the block holding its paragraph; lazy lines below a footnote definition get a footnote's words (4 spaces). - `blockNamed` names a record for reader and writer alike; `headerOf`, unused, is removed. - `fetchproof/lib/requests.mjs`: `applyRequest` asks `assertTablesWritable` of the unknowns table in `DISCOVERY.md` before it reads ids or writes a row or a plan. - `fetchproof/lib/decompose.mjs`: `mapHoldsContent` counts a map with a `table-split` as drafted. - Tests: eight new - corpus: a cell on one line; the writers refuse where the reader stops; a footnote's table read only when the body holds none (and a label defined twice, side by side and nested); a reopened comment closed only in an HTML block; linear time on nested footnotes; a lazy header named with its holder. decompose: a dry run keeps a map the reader stops in. requests: a request refused on a split contract, and a fact written on one line. One case added to "appendRow refuses ... whatever the reason" (a second definition of a label). ## Why The re-review of 88fe19a (2026-11-07, E:/Temp/review-followups/d-findings.md) found: - F1 (high): where the reader found a table but stopped above rows GitHub shows - a row holding a carriage return alone, a row at column 0 below a table in a list item, a row indented 4 - the writers numbered past the rows read and wrote above the stop: a second U-2 or E-02 beside one no check read. `escapeCell` could write the carriage return itself (a fetched title, a request's fact), and a dry run of `decompose` wrote over such a map. - F2 (medium): a table in a footnote definition something refers to was read before the body's, though GitHub shows footnotes below the body: preflight passed on a contract GitHub shows OPEN. - F3 (low): a reopened comment was closed by a `-->` in a paragraph, which GitHub writes `-->`, so the comment stays open in the browser. - F4 (low): nested footnote definitions were marked line by line once per definition holding them (3.9 s on 200,000 lines). - F5 (low): a lazy header with a leading space got the cells reason; its remedy made a table GitHub shows one column off. - F6 (low): lazy lines below a footnote definition were named in a list item's words. - Found while fixing them: a label defined twice was read in both definitions; GitHub keeps one. Set aside, with ADR-0204's reasons: refusing a request whose fact holds a line end (`requestProblems`) - `escapeCell` now writes any line end as a space, so the kit can record it; `nextId` over every id in the file - it would stop the duplicate ids, not the unread rows. The reopened comment got a record and a per-segment state of its own rather than a flag on the HTML block's record and a per-line state: the flag would have named the block's start for lines the block no longer holds, and a per-line state would close the comment at a `-->` in a segment after a lone carriage return, which GitHub reads as a line of its own. ## What it touched - Code: `fetchproof/lib/corpus.mjs`, `fetchproof/lib/requests.mjs`, `fetchproof/lib/decompose.mjs`. - Tests: `fetchproof/test/corpus.test.mjs`, `decompose.test.mjs`, `requests.test.mjs`. - Map: `docs/ARCHITECTURE.md` rows `corpus.mjs`, `decompose.mjs`, `requests.mjs`. - ADRs (PR #441 unmerged, amended in place): ADR-0203 (Prompted-by, Context F2-F6 and the label defined twice, decisions 1, 2, 5, 6, a rejected alternative, consequences); ADR-0204 (Area, Prompted-by, Context F1, decision 1's hiding blocks, decisions 6 and 7, two rejected alternatives, a consequence). - `CHANGELOG.md` (the F01 entry, 23 -> 31 tests), `fetchproof/README.md` (2298 -> 2306 tests). ## What you verified - verified: `node fetchproof/bin/selftest.mjs corpus decompose` - 119 passed, 0 failed, after the last change to `corpus.mjs`. - verified: `node fetchproof/bin/selftest.mjs requests` - 68 passed, 0 failed (385 s), before the duplicate-label change; that change touches footnote definitions only. - verified: groups auto-accounting, auto-reservation, collect, checks, preflight, artifact, hardening, cli, architecture-map, repo-hygiene with corpus and decompose - 763 passed, 0 failed (390 s). - verified: each new test red before its fix (corpus and decompose 7, requests 1, the two duplicate-label cases 2); the nested case also against a scratch copy of the library keeping the first definition in document order: it read the outer table, the fix reads none. - verified: GitHub's renderer (`gh api markdown`, synthetic text): of a label defined twice the first is shown and the second dropped with its table; of two nested, the inner is shown and the outer dropped with its table. - verified: the reviewer's probes on the fixed tree (copies in E:/Temp/review-followups/e/probes): p03 refused as a split and the fact written on one line, ids U-2, U-2, U-3 no longer issued; p05 the title written "Pricing Plans"; p08 a footnote writer writes into the live table; p09 every named end refused; p11 the live OPEN table read; p07 the footnote and lazy-header words; p12 (80 200000 90) 505 ms, was 3857 ms; p06 no table where GitHub shows none. - verified: the full suite, run by the commit hook (ADR-0120) - this commit exists only if it passed. ## What you got wrong and fixed Commit D's mistakes, found by the re-review: 1. - mistake: the writers numbered and placed a row as if the reader read the whole table. - where: `fetchproof/lib/corpus.mjs` `tableHidden`, `appendRow`, `upsertRow`; `escapeCell`. - impact: a second U-2 or E-02 beside a row no check read; on PR #441 only, not released. - cause: the refusal asked only whether a table was found, not whether it was read whole. - fix: a write into a table with a `table-split` is refused, also before a request is applied and in a dry run's map; `escapeCell` writes a lone carriage return as a space. - verified: the two writer tests, the requests and decompose tests, probes p03, p05, p09. 2. - mistake: a referenced footnote's table was read before the body's. - where: `hiddenLines` and the header search in `parseTable`. - impact: preflight PASS on a contract GitHub shows OPEN; PR #441 only. - cause: the header search went in source order; GitHub moves footnotes to the end. - fix: `note` and `order`; a footnote header only when the body holds none. - verified: the footnote test, probe p11. 3. - mistake: a reopened comment closed at a `-->` GitHub escapes. - where: the line loop of `hiddenLines`. - impact: lines the browser hides read as shown; also before D. - cause: the raw line was searched for `-->`, whatever block held it. - fix: closed only by an HTML block segment's `-->`. - verified: the reopened-comment test. 4. - mistake: nested footnote definitions marked once per definition holding a line. - where: the definitions loop of `hiddenLines`. - impact: 3.9 s on 200,000 lines (9.9 s per parse of 1.55 MB in the review). - cause: each record marked its whole range. - fix: each line marked once (`reach`, `noteReach`). - verified: the linear-time test, probe p12 505 ms. 5. - mistake: a lazy header with a leading space named with the cells reason. - where: the table-header attempt in `hiddenLines`; `notATable`. - impact: following the remedy gave a table GitHub shows one column off. - cause: the leading space GitHub reads as an empty first cell was not considered. - fix: `lazy-header`, "indent the header as the delimiter row is". - verified: the lazy-header test, probe p07. 6. - mistake: lazy lines below a footnote named in a list item's words. - where: `notATable`. - impact: a wrong remedy (a list item's indentation, not 4 spaces). - cause: the lazy outcome did not carry its holder. - fix: `under`; a footnote's own words. - verified: the lazy-header test, probe p07. This commit's: 7. - mistake: the first fix for a label defined twice kept the first definition in document order. - where: the definitions loop of `hiddenLines`. - impact: none delivered; of two nested, the outer's table would have been read where GitHub drops it. - cause: assumed GitHub keeps the first written; cmark-gfm keeps the first to end. - fix: `ended`, counted as containers close; the first to end is kept. - verified: GitHub's renderer on the nested shape, the nested case red on the old rule. 8. - mistake: a dropped duplicate was first named "a footnote definition ... that nothing refers to". - where: `blockNamed`. - impact: none delivered; a refusal would have named the wrong reason. - cause: one wording for every dropped definition. - fix: `repeats`, "repeats the label of the one at line N". - verified: the added case in "appendRow refuses ... whatever the reason". 9. - mistake: the first remedy for a lazy header (F5) also offered "or put a blank line above it", and a test was planned for a lazy header in a block quote. - where: the design of F5. - impact: none delivered. Under a footnote definition the blank line ends the footnote, and its delimiter row, indented 4, then opens no table; a quote takes no lazy header that way. - cause: the remedy was checked against a list item, whose content column is under 4. - fix: the remedy is "indent the header as the delimiter row is"; the quote test dropped as unreachable. - verified: probe p07 (G5 the footnote's words, G6 lazy-header). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ences only where GitHub reads them, and no row is written beside a table GitHub shows that no check reads (re-review of 116a2a3) ## What changed - `fetchproof/lib/corpus.mjs` - Footnotes and comments in GitHub's order. GitHub writes the footnote definitions below the body, so a comment the body leaves open - never closed, reopened, or cut off where its block ends - hides the footnotes too: every line of a definition GitHub shows is hidden with it. `hiddenLines` keeps the definitions open (`footnotes`) and the comments they leave open (`events`: cut off, never closed, or reopened and left open when the definition ends). The first, in GitHub's order, hides the rest of its definition and the definitions shown after it, never the body, and carries `footnote`, its definition's line. A comment the body reopened is closed only by a `-->` in a body HTML block, one a definition reopened only by one in that definition (`rawOwner`). - References only where GitHub reads one. `scanRefs` replaces `noteRefs`: a paragraph is read whole when it ends, as a code span or a comment can run across its lines, and `[^label]` counts only in its text - not in a code span, an autolink or raw HTML (a tag, comment, processing instruction, declaration or CDATA), not behind a backslash, not followed by `(` (a link's text); `[\^label]` counts, as on GitHub. A heading's text is read, and a row's cells one by one: a cell divider ends a code span. One pass: each backtick run's closers are indexed once, and a quote's next place or a marker found missing is not looked for again. `htmlTagEnd`, factored out of `htmlTag`, gives a tag's end to both. - Nested footnotes: a line is the innermost definition's holding it, shown in that one's order, in one sweep with a stack; it was the outermost one's. - `open` is also an HTML block of type 1 or 3 to 5 the body ends inside (`<pre>`, `<script>`, `<style>`, `<textarea>`, `<?`, `<!X`, CDATA): what is written at the end of the file is in it. The reader names a header in one as "not shown as a table". - `hiddenLines` returns `tables`, every table GitHub makes: its header's line, GitHub's line number for it (a carriage return alone ends a line), its header text past the markers of the blocks holding it, and whether a carriage return alone ends a line in its header or delimiter row. The writers refuse (`kind: 'unread'`, with the `line` and the `shape`) where such a table names the header (`namesHeader`, factored out of `headerAt`) and the header search passes it over: no outer pipes, a block quote, a header on the line of a list item's marker or a footnote definition's label, or line ends of a carriage return alone; rows or none. `UNREAD` holds each reason and remedy; `tableHiddenError` words it. - The reader names a hidden header in a footnote definition with where GitHub shows it; of the headers a block never closed hides, it names one in the body before one in a definition, and below a cut-off comment the first below the cut (`rank`). The writer names a comment a definition leaves open with that definition; "would be hidden too" is "would be in it too". - `readTable` (internal) returns the reader's lines and what `hiddenLines` said of them with the table; `writeCheck` judges the writes from them, where it read the file a second time when no table was found. - Tests: seven new - corpus: a comment the body leaves open hides the footnotes; one a footnote leaves open hides only what GitHub shows after it; references only where GitHub reads one (11 shapes not counted, 8 counted, 4 read in linear time); nested footnotes in the inner one's order; the writers refuse beside a table GitHub shows and no check reads (5 shapes, evidence, sources, both writers); a file ending inside an HTML block never closed. requests: a request refused, writing nothing, in five of those shapes. ## Why The re-review of 116a2a3 (2026-11-07, E:/Temp/review-followups/e-findings.md) found: - 1 (medium): comment state was read in file order, though GitHub shows the footnotes after the body. Below a comment the body never closed, a footnote's archived CLOSED contract was read and preflight passed, where GitHub shows no table or the live OPEN one; a comment reopened in a footnote hid the body below it, which GitHub shows. - 2 (medium): a table GitHub shows with no outer pipes, in a block quote, or with line ends of a carriage return alone was passed over by the header search, and the writers started a second table beside it, numbered from U-1 or E-01 again. - 3 (low): a `[^label]` in a code span or an inline comment was counted as a reference, which kept a definition GitHub drops, and its table, read. - 4 (low): of two nested definitions both referred to, the inner one took the outer's place in the order, and the table GitHub shows second was read. - 5 (low): a file ending inside a `<pre>` or a `<script>` never closed took the first table inside the block. - Found while fixing them, each checked on GitHub's renderer: a body comment reopened stays open past a `-->` only a footnote's HTML block holds (C1); a comment cut off in a footnote hides the rest of it (C2) and the footnotes shown after it (C3), not those shown before (C4); a header on a list item's marker line or a footnote's label line is passed over too. And 116a2a3's report said `writeCheck` parses once; with no table found, it parsed twice. Set aside, with the ADRs' reasons: hiding every footnote definition once a comment is left open anywhere (ADR-0203) - it would block a contract GitHub shows whole; reading the tables the header search passes over (ADR-0204) - writing into a quoted table or lone carriage returns is a new way of writing the corpus, which the freeze reads as a feature; refusing beside such a table only when it holds rows (ADR-0204) - a table GitHub shows is on the page, and a new one would be the second. `[^label](` is no reference here, where GitHub reads one unless a link follows: stricter, and named in ADR-0203 decision 6 with the one lenient shape left (a reference inside a URL). ## What it touched - Code: `fetchproof/lib/corpus.mjs`. - Tests: `fetchproof/test/corpus.test.mjs`, `fetchproof/test/requests.test.mjs`. - Map: `docs/ARCHITECTURE.md` row `corpus.mjs`. - ADRs (PR #441 unmerged, amended in place): ADR-0203 (Prompted-by, Context, decisions 1, 2, 5 and 6, a rejected alternative, consequences); ADR-0204 (Prompted-by, Context, decisions 1 and 2, two rejected alternatives, a consequence). - `CHANGELOG.md` (the F01 entry, 31 -> 38 tests), `fetchproof/README.md` (2306 -> 2313 tests). ## What you verified - verified: `node fetchproof/bin/selftest.mjs corpus` - 82 passed, 0 failed, twice, after the last change to `corpus.mjs`. - verified: groups requests, auto-accounting, auto-reservation, collect, checks, preflight, artifact, decompose, hardening, cli, architecture-map, repo-hygiene - 755 passed, 1 failed (824 s): the ADR link check, on a link shape in ADR-0203's new text (item 11). After the fix and the map, CHANGELOG and README edits: architecture-map, repo-hygiene, corpus - 115 passed, 0 failed. - verified: each new test red before its fix - the six corpus tests failed on 116a2a3's library (76 passed, 6 failed); the request shapes, run in-process on snapshots, were applied by 116a2a3 and by main (532c696) - U-2 written into the footnote's archived table, U-1 beside the table GitHub shows, U-1 inside the `<pre>` - and are refused, writing nothing, by this commit. - verified: GitHub's renderer (`gh api markdown`, synthetic text, 64 cases under E:/Temp/review-followups/f/gh): C1-C4; the shapes counted and not counted as references in the test, the four linear-time shapes at a small size; tables shown with no outer pipes, in a quote, after lone carriage returns, on a list marker's or footnote label's line; the departures ADR-0203 decision 6 names. - verified: the reviewer's probes, on copies (E:/Temp/review-followups/f/rprobes): p02, p02b, p03, p06, p07, p10, p11, p13 read as GitHub shows, or more strictly where decision 6 says so; p04 refused as unread in each shape (NP, BQ, CR), GitHub's table unchanged; p12 refused; p01 176 real Markdown files read and judged as before, none refused. - verified: timing - parseTable on the reviewer's 3.12 MB table 377 ms and 8.48 MB of nested footnotes 436 ms; scanRefs on 15 adversarial paragraphs of 0.2-1.6 MB (k2), each under 100 ms (the comment case 45 ms). - verified: `hiddenLines` is called once, in `readTable` (grep). - verified: the full suite, run by the commit hook (ADR-0120) - this commit exists only if it passed. ## What you got wrong and fixed 116a2a3's mistakes, found by the re-review: 1. - mistake: comments were read in file order, though GitHub shows the footnotes below the body. - where: `fetchproof/lib/corpus.mjs` `hiddenLines` (the comment state and the footnote marking) and the header search in `parseTable`. - impact: preflight PASS on an archived CLOSED contract in a footnote where GitHub shows no table or the live OPEN one, and writes into it; a body table hidden below a comment a footnote reopened. The reader on PR #441 only; main reads the first table with the header, whatever hides it, so it reads the same archived table (F01's own defect). - cause: a footnote definition was read where it is written. - fix: a comment the body leaves open hides every shown footnote line; a footnote's comment hides the rest of it and those shown after it, in GitHub's order (`events`, `rawOwner`). - verified: the two comment tests, red before; probes p02, p02b, p03, p13. 2. - mistake: the writers started a table beside one GitHub shows that the header search passes over. - where: `writeCheck` (`tableHidden`, `appendRow`, `upsertRow`). - impact: a second U-1 or E-01 on the page, the rows GitHub shows first read by no check. Also on main (532c696), where the writers append wherever the header search finds no table. - cause: "no table found" was read as "no table on the page". - fix: `tables`, `namesHeader`, the `unread` refusal with the shape to fix. - verified: the writer and request tests, red before; probes p04, p05. 3. - mistake: a `[^label]` was counted as a reference in a code span, a comment or other raw HTML. - where: `noteRefs` in `hiddenLines`. - impact: a definition GitHub drops, and its table, was read; PR #441 only. - cause: the raw text was searched for `[^`, not read as GitHub reads inline text. - fix: `scanRefs`. - verified: the reference test, red before; GitHub's renderer on each shape. 4. - mistake: nested definitions were marked with the outermost one. - where: the definitions sweep in `hiddenLines`. - impact: the table GitHub shows second was read; PR #441 only. - cause: each line was marked once, by the first definition to reach it (E's fix for F4). - fix: the innermost, in one sweep with a stack. - verified: the nested test, red before; the linear-time tests unchanged. 5. - mistake: `open` named a fence and a comment only. - where: the end of `hiddenLines`. - impact: the first table written into a `<pre>` or a `<script>`, its unknown and plan written for a row no check reads. Also on main, which has no such refusal. - cause: HTML blocks of types 1 and 3 to 5, which end only at their marker, were left out. - fix: `open` for them at the top level. - verified: the HTML-block test and the `<pre>` request shape, red before; probe p12. 6. - mistake: the shapes found while fixing 1 and 2 - a footnote's `-->` closed a comment the body reopened (C1); a comment cut off in a footnote hid nothing (C2, C3); a header on a list marker's or footnote label's line was passed over. - where: `hiddenLines`; `writeCheck`. - impact: lines GitHub hides read, and a second table on the page; PR #441 only, and the last also on main. - cause: the same as 1 and 2. - fix: as in 1 and 2 (`rawOwner`, `events`; the `marker` shape). - verified: the cases in the comment and writer tests; k1 (C1-C4) against GitHub. 7. - mistake: 116a2a3's report said `writeCheck` parses once; with no table found, it parsed the file twice. - where: `writeCheck`; the report's "What changed". - impact: twice the time on that path; a false claim in a commit report on PR #441. - cause: the claim was checked on the found path only. - fix: `readTable`. - verified: `hiddenLines` is called once (grep); the corpus group. This commit's: 8. - mistake: two GitHub probes lost their backslashes: one written through a heredoc, one whose "escaped" case was not escaped in its JavaScript source. - where: the probes g2 and g3 (E:/Temp/review-followups/f/probes). - impact: none delivered; the escape rules would have been recorded from unescaped input. - cause: shell and JavaScript quoting each take one backslash. - fix: g4, written with the editor, checked the escaped shapes again. - verified: g4's results, which the reference test asserts. 9. - mistake: the timing case for open comments put `<!-- ` at the start of the line. - where: the k2 probe; the planned case in the reference test. - impact: none delivered; it timed an HTML block, not a paragraph, and the test would have asserted a reference GitHub does not read there. - cause: a line starting `<!--` opens an HTML block. - fix: `A ` before it; GitHub reads the reference, 45 ms. - verified: g7 on GitHub's renderer; the reference test. 10. - mistake: `scanRefs`' first draft copied the run lists (quadratic), shadowed `at`, and left `AUTOLINK_EMAIL.lastIndex` stale; a probe imported the library by an `E:/` path. - where: `scanRefs`; a probe. - impact: none delivered; each was caught before the code ran, the import by its error. - cause: written fast, read once. - fix: indexed closers (`passed`), renamed, the sticky reset by `sticky`; `file:///` URLs. - verified: k2 timing, the corpus group. 11. - mistake: ADR-0203's decision 6 wrote a `[^label]` and a `(` in code, followed by text and a `)`, which the ADR link check reads as a link to a file that does not exist. - where: `docs/adr/0203-...md`, decision 6. - impact: none delivered; the repo-hygiene group went red. - cause: a link's shape written in prose that the check reads raw. - fix: reworded ("a `(` with no link after it"). - verified: repo-hygiene, 0 failed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
StepenkoAnatoli
added a commit
that referenced
this pull request
Nov 7, 2026
What changed - Brings PR #441's commits since the merge base 1078ead into the #442 branch: the corpus reader and writer fixes for F01 and its reviews (`hiddenLines`, `readTable`, `TABLE_HIDDEN`, decompose's named refusals). The merge itself changes no code: the two branches touch no module in common. Why - #442 is stacked on #441. #441 merges first; #442 is then retargeted to main and must hold #441's final head, so it merges with nothing left to reconcile and its CI runs the combined code. What it touched - Resolved by hand: docs/adr/README.md, where both branches appended rows after 0199 (kept #442's 0200-0202 and #441's 0203-0204, in number order), and fetchproof/README.md, the test count (2277 at the merge base; #442 adds 28 for 2305, #441 adds 36 for 2313; together 2341). - Merged by git: CHANGELOG.md (Unreleased holds #442's entries, then #441's), docs/ARCHITECTURE.md (each branch edited other rows), fetchproof/test/cli.test.mjs (tests added at different places). - No ADR owed: no decision was made. The map rows and ADRs come with each branch. What you verified - verified: `git merge-tree --write-tree HEAD dde656a` before merging named only those two conflicts. - verified: no conflict marker is left in any file the merge changed (grep). - verified: the groups repo-hygiene, architecture-map, cli and decompose on the merged tree: 230 passed, 0 failed (274 s). - verified: the commit gate ran the full suite on this commit (ADR-0120); it lands only when green, and it refuses a stale "N tests, offline" line, so 2341 is checked too. What you got wrong and fixed - Nothing to report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ach owner's HTML by its own tokenizer, and a table no check can judge is refused (re-review of dde656a) ## What changed - `fetchproof/lib/corpus.mjs` - References by cmark-gfm's inline parse. `footnoteRefs` replaces `scanRefs`: a paragraph, a heading, and a row's cells up to the header's count (`rowCellTexts`) are read as cmark-gfm's `inlines.c` reads them, once the link reference definitions of the whole file are known (`parseRefDefs`, `refDefAt`: taken out of the paragraphs they lead, into the `refmap`). A `[^label]` counts outside code spans, autolinks, inline HTML and escapes, and outside links: followed by an inline link (`linkDestination`, `linkTitle`, `inlineLinkEnd`) or by a `[ref]` the refmap defines, it is link text, and a link inside brackets deactivates them. A declaration is `<!`, uppercase letters and a space; `![^` opens no image. The autolink extension (`urlMatch`, `wwwMatch`, `checkDomain`, `autolinkDelim`) takes in a `[^label]` written straight after a bare URL or a `www.` link, as GitHub does. - A label is the text GitHub merges into one: escapes and entities read (`entityAt`: eleven names and numeric references of up to eight digits), up to a code span, inline HTML or a line break, of 1 to 1000 bytes; it matches a definition after both are case-folded with their spaces collapsed (`normalizeLabel`). A definition's label was lowercased. - Each owner's HTML by its own tokenizer. `feedHtml` replaces `rawComment`, `rawOwner` and `commentOpenAfter`: the raw lines of every HTML block, of any type, go to the tokenizer of their owner - the body, or the footnote definition holding them - which reads comments, bogus comments, tags with their attributes and quoted values, and as text the nine tags the tagfilter escapes. A comment or a tag a block leaves open is a record (`kind` 'comment' or 'tag', `HIDDEN_BY.tag`) hiding what its owner shows after it, in GitHub's order: the body's every definition too; a definition's the rest of it and the definitions shown after it. - `uncertain`: a reference whose label holds an entity name the kit does not know, where the table is looked for among the footnotes, is a `table-split` problem with the remedy, and the writers refuse it before any other refusal. - F-1: where a table is read, a table GitHub shows before it with the header that the header search passes over is a `table-split` problem (`done.unread`, `before`) and the writers refuse; one whose lines a carriage return alone ends is named, wherever it stands, as a table no check can place (F-5, `unreadShape`), not as one GitHub shows. - The doubt rule (ADR-0203 decision 8). A tag record carries `first`, the line GitHub writes first after its block (`wrote`): it hides that one surely, as any later block may end it. A comment record carries `closer` and `lt`: a `-->` that may close it on GitHub - in a paragraph past a `<`, or in a footnote shown before (`sooner`). Over an HTML block that starts while its owner's record is uncertain, `alt` reads the block from text as well; where the two readings end apart, `unsure` marks what the owner reads after it and the footnotes shown after that owner. `doubtOf` weighs every table GitHub makes with the header, taken by the header search or passed over; the first in doubt - before the table read, that table, or anywhere when none is read - is a `table-split` problem naming the tag or the comment (`doubtWords`), and the writers refuse (`doubt`, with `before` or `same`). - `writeCheck` refuses in this order: uncertain, unread, doubt, a split table, a block left open, an unread table where none is read, a hidden header with rows; `tableHiddenError` words each. - F-6: `upsertRow` of a new key appends with the text and the table it read (`appendChecked`), where it read the file again. - Tests: twenty new. corpus (19): twelve on references - `![^`; a definition shown, and its table read, only for a reference GitHub counts; counted where GitHub reads one; the label as merged text, escapes and entities read, up to a code span or inline HTML; numeric references and 1000 bytes; followed by a defined `[ref]` or an inline link; in a declaration; straight after a URL; where the autolink extension starts; a row's cells up to the header's count - and one each on an unknown entity name, a comment left open in an HTML block of any type, carriage returns alone, the doubt rule, the unsure reading, the writers beside an unread table before the one read, and `upsertRow` reading once. requests (1): a request refused, writing nothing, while GitHub shows an unread table before the one the kit reads. Two tests changed for the F-5 wording. ## Why The re-review of dde656a (2026-11-07, E:/Temp/review-followups/f-findings.md) found: - F-1 (medium): with the contract in a shape the header search passes over - no outer pipes, a block quote, a header on a list marker's or a footnote label's line, carriage returns alone - and a copy of its header further down, the copy was read as the contract: preflight passed on an archived CLOSED table while the one GitHub shows first held an OPEN unknown, and the writers numbered an id a second time. The `unread` refusal ran only where no table was read. - F-2 (low, a regression): three of dde656a's reference rules dropped references GitHub reads - `<!` and any letter taken as a declaration, a label given up at a backtick or a `<`, and a reference dropped at any `(` after it - so a definition GitHub shows was left out and a later one's archived copy read in its place (RC4: preflight passed). - F-3 (low): two kinds of reference GitHub does not read were counted: one in a row's cell past the header's count, and `[^x][y]` where the file defines `[y]`. - F-4 (low): a footnote definition's reopened comment was lost while the body's was open: one comment state served the body and every definition. - F-5 (low): a table whose lines a carriage return alone ends, which GitHub hides, was refused as one "GitHub shows". - F-6 (low): `upsertRow` of a new key still read the file twice. Found while fixing them, each checked on GitHub's renderer: the autolink extension takes in a `[^label]` written straight after a URL; `![^` opens no image; links and the link reference definitions decide which `[^label]` is link text, so the inline parse needs the refmap; a label is case-folded with its escapes and entities read, and one holding an entity name the kit does not know cannot be matched without HTML5's table; a comment left open in an HTML block of any type, not only one that starts with `<!--`, hides what follows (the T shapes), each owner's its own (H7: a body comment closed by a later body `-->` leaves the footnotes shown, and left open hides them; a definition's hides no body table after it). The fix's own first reading, rendered before this commit (v10-v12), found that GitHub writes HTML between the blocks the kit reads: a tag or a comment the kit keeps open may close earlier on the page, so the table read may be the one GitHub shows second, or one it hides (L1, L3-L6) - hence the doubt rule and the second reading. Set aside, with the ADRs' reasons: refusing beside an unread table, or naming one in doubt, only where no table is read (ADR-0204, ADR-0203) - the CLOSED copy below is the false PASS itself; documenting the doubtful shapes as residuals - rendered, each was a false PASS; modelling GitHub's markup exactly - so modelled, it is the renderer; taking any `-->` in a footnote, in the file's order, as a closer (h6c) - a table GitHub surely hides was refused as one in doubt; taking any `-->` in a hidden paragraph as one (r1) - GitHub writes one with no `<` before it as text, and a correct project would be blocked; counting every HTML block after an uncertain record as doubt, without a second reading - it refuses a closed `<div>`; shipping HTML5's 2,125 entity names, or reading an unknown one as text or as a wildcard - a hand-kept table for labels no contract needs, or a definition left out or read in error. Accepted, and named in decision 8: the reading errs only to a refusal, so a table GitHub shows may be refused - among them probe p06's rows below a `<?php`, a `<!DOCTYPE` or CDATA left open at the end of a list item or a quote. ## What it touched - Code: `fetchproof/lib/corpus.mjs`. - Tests: `fetchproof/test/corpus.test.mjs`, `fetchproof/test/requests.test.mjs`. - Map: `docs/ARCHITECTURE.md` row `corpus.mjs`. - ADRs (PR #441 unmerged, amended in place, 2026-11-08): ADR-0203 (Context; decisions 1, 2, 5 and 6; new decisions 7, unknown entity names, and 8, the doubt rule with the ways it errs; eight rejected alternatives; consequences); ADR-0204 (date, area, prompted-by, context; new decisions 8, the refusal order, and 9, one read; a rejected alternative; a consequence). - `CHANGELOG.md` (the F01 entry, 38 -> 58 tests), `fetchproof/README.md` (2313 -> 2333 tests). - No new domain term; `CONTEXT.md` unchanged. ## What you verified - verified: `node fetchproof/bin/selftest.mjs corpus requests` on the final code - 171 passed, 0 failed (397.9 s; corpus 101, requests 70), cwd E:/Claude/fp-p0-followup (E:/Temp/review-followups/g/unsure-green.log). - verified: groups auto-accounting, auto-reservation, collect, collector-workflow, checks, preflight, artifact, decompose, hardening, cli, architecture-map, repo-hygiene, with the files their names match - 687 passed, 0 failed (373.0 s, g/step3-groups.log); after the last ADR edit, architecture-map and repo-hygiene - 33 passed, 0 failed (g/docs-groups.log). - verified: each new test red before its fix - on dde656a's library 149 passed, 20 failed (g/red-1.log): seventeen new corpus tests, the changed corpus test and the two request tests; the doubt test before decision 8 (99 passed, 1 failed, g/doubt-red.log); the unsure test before the second reading (100 passed, 1 failed, g/unsure-red.log). - verified: GitHub's renderer (`gh api markdown`, synthetic text, renders cached under E:/Temp/review-followups/g/gh): probes v1-v12 under g/probes on each reference, label, link, autolink, entity and comment shape; v8, every case the new tests assert - 221 cases, 0 mismatches; v10-v11, eleven doubt shapes; v12, L1 and L3-L6 refused, the control L2 clean. - verified: the reviewer's probes, on copies (g/fcopy), each but p10 and p11 run: p01, 43 reference shapes, 0 mismatches with GitHub; p02, p04, p07-p09, p12, p14-p16 and p18-p20 read and refuse as GitHub shows - F-1 to F-6 fixed; p17, the file main and 116a2a3 leave behind, reads U-1 OPEN and refuses the next request as unread; p03, 176 real Markdown files read and judged as before, none refused; p06 refused, as decision 8 names. p11 not run: it runs the suite, which the hook runs. - verified: timing - `parseTable` on the p10 shapes, at worst 295 ms at 100k; p10b, the three slowest at 100k, 200k and 400k, grows linearly (references then `(`: 288, 436, 938 ms). - verified: the full suite, run by the commit hook (ADR-0120) - this commit exists only if it passed. ## What you got wrong and fixed dde656a's mistakes, found by the re-review: 1. - mistake: the `unread` refusal ran only where no table was read. - where: `fetchproof/lib/corpus.mjs` `readTable` (the found branch) and `writeCheck`. - impact: preflight PASS on an archived CLOSED copy below a contract the header search passes over, while GitHub shows its OPEN unknown first, and an id written a second time. Also on main (532c696), whose header search passes over the same shapes and whose writers start the second table (probe p19). - cause: "a table is read" was taken for "the table GitHub shows first is read". - fix: the unread loop in the found branch (`done.unread`, `before`); the writers refuse. - verified: the F-1 corpus and request tests, red before; probes p02, p17, p19, p20. 2. - mistake: three reference rules dropped references GitHub reads: `<!` and any letter taken as a declaration, a label given up at a backtick or a `<`, and a reference dropped at any `(`. - where: `scanRefs` (dde656a's corpus.mjs lines 507, 518, 522). - impact: a definition GitHub shows left out and a later one's archived copy read in its place; preflight passed (RC4). PR #441 only, a regression from 116a2a3. - cause: rules written from the shapes seen, not from cmark-gfm's `inlines.c`. - fix: `footnoteRefs`: the declaration rule, the label as merged text, `inlineLinkEnd`. - verified: the reference tests, red before; probes p01, p08, p09 and p15; v8 against GitHub. 3. - mistake: a reference in a row's cell past the header's count, and `[^x][y]` with `[y]` defined, were counted. - where: `scanRefs` and its callers in `hiddenLines`. - impact: a definition GitHub drops, and its table, read. Both trees of PR #441. - cause: a row's cells were read whole; the link reference definitions were not known. - fix: `rowCellTexts` up to the header's count; the refmap, known before any reference is read. - verified: the cells and `[ref]` tests, red before; probes p01 and p08. 4. - mistake: one comment state served the body and every definition, so a definition's reopened comment was lost while the body's was open. - where: `rawComment`, `rawOwner` in `hiddenLines`. - impact: lines GitHub hides in a definition read. Both trees of PR #441. - cause: GitHub writes each definition apart from the body; the state was kept once. - fix: `feedHtml`, a tokenizer per owner, read in GitHub's order. - verified: the comment test (F-4 and H7: a body comment closed by a later body `-->` leaves the footnotes shown, left open hides them, and a definition's hides no body table after it), red before; probes p04 and p14; v2 against GitHub. 5. - mistake: a table whose lines a carriage return alone ends, which GitHub hides, was refused as one "GitHub shows", and its remedy led to a different refusal. - where: `tableHiddenError`, the `unread` message. - impact: a wrong reason and remedy; nothing written. PR #441 only. - cause: every table the header search passes over was worded as one GitHub shows. - fix: one ended by carriage returns alone is named as a table no check can place. - verified: the carriage-return test and the changed request test, red before; probe p18. 6. - mistake: `upsertRow` of a new key read and judged the file twice. - where: `upsertRow` and `appendRow`. - impact: twice the time on that path, and the map's "a write reads the file once" untrue. Both trees of PR #441. - cause: the append was delegated to `appendRow`, which reads the file again. - fix: `appendChecked`, with the text and the table already read. - verified: the F-6 test, red before; probe p16. dde656a's, found while fixing them: 7. - mistake: a comment was followed only in an HTML block that starts with `<!--`, and a tag left open (`<div title="x`) was not followed at all. - where: `hiddenLines`, the HTML block branches. - impact: a table GitHub hides below a `<div>` holding `<!--`, or below a tag left open, read. PR #441; main reads the first table with the header, whatever hides it. - cause: the HTML block type was taken for what the browser reads. - fix: `feedHtml` reads the raw lines of every HTML block, comments and tags. - verified: the comment test (the T shapes), red before; v3 against GitHub. 8. - mistake: ADR-0203 decision 6 named as read more strictly than GitHub a reopened comment closed only by an HTML block's `-->`, a body comment hiding every footnote, and a footnote's comment hiding past a `-->`; this commit's first plan kept them so. - where: ADR-0203 decision 6; the plan in E:/Claude/notes. - impact: where a later table is read they are no stricter: GitHub showed the OPEN table the kit hid, above the CLOSED copy the checks read, and preflight passed. PR #441 only. - cause: "stricter" was judged by the table hidden, not by the one read in its place. - fix: the doubt rule (decision 8): such a table cannot be judged and is refused. - verified: the doubt test, red before (99 passed, 1 failed); v10 and v11 against GitHub. 9. - mistake: links were not modelled: a `[^label]` in a link's destination or title, in a link reference definition, or written straight after a bare URL was counted. - where: `scanRefs`; ADR-0203 decision 6 named these as departures. - impact: a definition GitHub drops, and its table, read. PR #441 only. - cause: the inline parse was approximated without the refmap and the autolink extension. - fix: the refmap, `inlineLinkEnd`, `urlMatch` and `wwwMatch`. - verified: the link, URL and autolink tests, red before; v1, v5-v7 and v9 against GitHub. 10. - mistake: labels were lowercased, their escapes and entities left unread, their spaces kept. - where: dde656a's corpus.mjs lines 522 and 709. - impact: a reference GitHub matches to a definition missed, or the reverse, so a definition left out or one GitHub drops read. PR #441 only. - cause: a label was taken for its raw text. - fix: the label as merged text, `normalizeLabel` (case fold, spaces collapsed), `entityAt`. - verified: the label tests, red before; v2 and v4 against GitHub. This commit's: 11. - mistake: three design notes misread GitHub: inline comments were noted as cmark 0.29's rule; a setext underline below a paragraph of link reference definitions alone as a thematic break; `]` holds no reference); the `![^` test. 12. - mistake: a reference the browser hides - in a paragraph below a comment left open - was believed not to count. - where: the analysis, not the code. - impact: none delivered; it would have left out definitions GitHub writes below the body. - cause: what the browser hides was taken for what the parser reads. - fix: every paragraph, heading and row cell cmark-gfm parses is read for references, hidden or not (`pending`). - verified: read; the corpus group, 101 passed, 0 failed. 13. - mistake: the first doubt designs took any `-->` in a footnote, in the file's order, as one that may close a comment (h6c), or any `-->` in a hidden paragraph (r1). - where: the doubt rule in `fetchproof/lib/corpus.mjs`, before this commit. - impact: none delivered; h6c refused a table GitHub surely hides as one in doubt, and r1 would block a correct project, as GitHub writes a `-->` with no `<` before it as text. - cause: closers counted by where they stand in the file, not by what GitHub writes raw. - fix: `sooner` (a footnote GitHub shows before) and `lt` (a `<` before it). - verified: the h6a and h6c cases of the comment test, and the doubt test; v10 and v11. 14. - mistake: the doubt search looked only at headers the header search takes. - where: `readTable`, the doubt search. - impact: none delivered; a table with no outer pipes that a tag hides, which GitHub may show first, was passed over (L4). - cause: the search reused `headerAt`. - fix: it runs over every table GitHub makes with the header (`read.tables`). - verified: the unsure test, red before (100 passed, 1 failed); v12. 15. - mistake: an HTML block starting after a tag or a comment GitHub may already have closed was read in the kit's state only. - where: `feedHtml`, `endHtml`. - impact: none delivered; a table the browser may show second, or hide, was read (L1, L3, L5, L6). - cause: one reading where the browser may stand in either of two states. - fix: `alt` reads the block from text as well; where the readings end apart, `unsure`. - verified: the unsure test, red before; v12, L1 and L3-L6 refused, the control L2 clean. 16. - mistake: text holding backslashes went through a Bash heredoc or sed, which mangle them, and the tool harness decodes a backslash-u escape in an edit's text into the character itself. - where: probes and edits during this work (process). - impact: none delivered: `corpus.mjs` holds no byte outside ASCII. - cause: each layer takes a backslash of its own. - fix: code and probes written with the editor tools, hex escapes or `fromCharCode` in place of a backslash-u escape, and a backslash constant in probes. - verified: grep for bytes outside ASCII in `corpus.mjs`: none; the corpus group. 17. - mistake: probe v8 compared footnote ids raw, where GitHub writes `&` in an attribute as `&`. - where: `notes()` in E:/Temp/review-followups/g/probes/gh.mjs. - impact: none delivered; three false mismatches (L11x, W9, E16). - cause: the id was read as text, not as an attribute value. - fix: the attribute's entities decoded before `decodeURIComponent`. - verified: v8, 221 cases, 0 mismatches. 18. - mistake: the copy of probe p17 wrote its first row with dde656a's writer, which refuses it. - where: E:/Temp/review-followups/g/fcopy/p17-left-by-main.mjs. - impact: none delivered; the probe exited 1 before reading anything. - cause: the copies map their old tree to dde656a, where p17 needs 116a2a3's writer. - fix: p17's old writer is 116a2a3 (snap-e), as in the reviewer's probe. - verified: p17 reads U-1 OPEN and refuses the next request as unread. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… tables after them in doubt, and three inline rules follow cmark-gfm (re-review of 50b18d1) ## What changed - `fetchproof/lib/corpus.mjs` - G-1, inline raw HTML. `footnoteRefs(source, refmap, spans)` hands back in `spans` the raw HTML GitHub writes as it stands: in a link's text too, not in an image's alt text, which it writes escaped, nor in a footnote reference's brackets, which it drops (`autolinkAt`, `rawHtmlAt`; a bracket notes how many spans came before it). After the pass, `hiddenLines` reads the spans of each paragraph, heading and cell not hidden by a block, in order, with a scratch tokenizer from text (`feedHtml`): a span that leaves the browser in a quoted value is doubt at once, and a comment open at the text's end is doubt. Either marks its owner `unsure` from that line on - the body's reaching every footnote, a footnote's those shown after it - and, in a row's cell, the header line of its table (`wasRowOf`). A span over two lines is read line by line. - G-5, `<select>`. `feedHtml` keeps `owner.select`, the line of a `<select>` the browser is in (`tagName`); another `<select>`, `</select>`, `<input>` or `<keygen>` closes it. One open where an HTML block ends (`endHtml`, its second reading too) or where a paragraph's or a heading's text ends - not a cell's (`cell` on the cell texts), whose end closes it - marks the owner `unsure`. - The doubt carries `why` ('inline', 'select'): `doubtOf` passes it on, and `doubtWords` words each, with its remedy - write that HTML in a code span or escape its `<`; close the select inside its block. - The end of the file. `hiddenLines` returns `endUnsure`, the first doubt that reaches the body's end; with no table read and no other refusal, `writeCheck` refuses there (`kind` 'doubt', `end`), and `tableHiddenError` names the end of the file. - G-2: one `noLinkOpeners` flag, as cmark-gfm's `no_link_openers` - set by a link made, cleared by a bracket pushed, neither an image's; at `]` a bracket not an image's is text while it is set. The `pop` helper is gone. - G-3: `normalizeLabel` folds by GitHub's tables (`FOLDED`, `foldChar`): ASCII lowered; a character in the blocks of cased letters they cover folded as lowering, raising and lowering it again does, the dotless i kept; any other character kept as written. - G-4: `linkDestination` keeps a `(` the destination leaves open, as cmark-gfm's `manual_scan_link_url_2` does (the limit of 32 stays); a run that reaches the end of the text is still no destination. - Tests: seven new. corpus (6): a destination that leaves a `(` open (P1-P4, P7-P9, M1, M2); brackets around a link and a later bracket (B1-B5, B8, B10-B12, and an archived copy); 27 label pairs folded by GitHub's tables (F1-F27); 18 shapes of raw HTML that leave the browser in a comment or a quoted value, and the end of a file below one; 12 the browser reads to their end, or GitHub writes escaped or drops; 12 shapes of a `<select>` left open, 12 closed or harmless, and the end of a file. requests (1): a request refused, writing nothing, below raw HTML that leaves the browser in a comment. ## Why The re-review of 50b18d1 (2026-11-08, E:/Temp/review-followups/g-findings.md; each finding rendered again on GitHub, E:/Temp/review-followups/h/verify.md) found: - G-1 (medium): GitHub writes a paragraph's, a heading's or a cell's raw HTML as it stands, and the browser ends what cmark-gfm takes whole earlier - a processing instruction or CDATA at a `>` inside it, a comment at `--!>` - so a `<!--` or an open quoted value after it hid what followed. Below one, an archived CLOSED copy of the unknowns table passed preflight while GitHub showed none, or the OPEN one, and a request was written into the copy as U-2 (g02). - G-2 (low): a link closed inside brackets deactivated the brackets around it for good, where cmark-gfm opens them again once a bracket not an image's is pushed: five reference shapes read otherwise than GitHub (B1-B3, B8, B10), and with them the footnotes it shows (N1). - G-3 (low): labels were case-folded by Node's Unicode, newer than GitHub's fold tables (Unicode 9 or 10), so labels GitHub holds apart matched and a definition it drops was shown (F3-F7, F21, F25). - G-4 (low, a regression from F): a destination that leaves a `(` open was taken for none, so a `[^label]` GitHub reads as link text kept a definition it drops (P1, P4, P9; M1, M2). - G-5 (low): a `<select>` left open drops the table tags GitHub writes after it, until something closes it, and the kit read that table (S1-S4, S13-S17, S21, S23, S24). - G-6 (low): the map's `corpus.mjs` row still named `scanRefs` and F's `(` rule, and sentences of ADR-0203 and the CHANGELOG said what G-1 to G-5 make untrue. Found while fixing them: with no table read, the writer started one at the end of a file whose body was already in doubt - past an HTML block read two ways (50b18d1's own rule), and now past raw HTML or a select - which GitHub may hide; the next read then refused the file. Set aside, with the reasons in verify.md: a record kind for inline HTML that hides surely - what GitHub writes in the paragraph (links, `</p>`, quotes) may end it earlier, so only doubt is honest; feeding the scratch tokenizer the text between spans - GitHub writes it escaped, and its own markup there opens no comment and closes none, while a quoted value it may close is doubt at once; a full fold table pinned in the kit - large, where the blocks GitHub covers fit in nineteen ranges; `uncertain` for labels outside them - GitHub folds those deterministically, by keeping them; a select in a cell judged like one in a paragraph - the cell's end closes it (S10). Accepted, and named in ADR-0203 decision 8: a table below inline HTML that leaves a comment open which a later HTML block's `-->` closes (H1f, AF1), below a select closed later (S1, S4), and below a select in a cell of a raw `<table>` is refused, though GitHub shows it. ## What it touched - Code: `fetchproof/lib/corpus.mjs`. - Tests: `fetchproof/test/corpus.test.mjs` (six new), `fetchproof/test/requests.test.mjs` (one new). - Map: `docs/ARCHITECTURE.md` row `corpus.mjs` (G-6). - ADRs (PR #441 unmerged, amended in place, 2026-11-08): ADR-0203 (prompted-by; decision 1, the inline and select `unsure` and `endUnsure`; decision 2, a `-->` in text, inline HTML, a destination's open `(`, `no_link_openers` and the fold; decision 8, inline HTML and `<select>`, the remedy, the end of the file and the over-refusals it accepts; consequences; a trigger); ADR-0204 (prompted-by; decision 8's `doubt` bullet, the end of the file). - `CHANGELOG.md` (the F01 entry, 58 -> 65 tests), `fetchproof/README.md` (2333 -> 2340 tests). - No new domain term; `CONTEXT.md` unchanged. ## What you verified - verified: `node fetchproof/bin/selftest.mjs corpus requests` on the final code - 178 passed, 0 failed (397.1 s; corpus 107, requests 71), cwd E:/Claude/fp-p0-followup (E:/Temp/review-followups/h/green.log). - verified: each new test red before its fix - the seven new tests on 50b18d1's library: 172 passed, 6 failed (h/red.log), the six that assert the fixes; the seventh, the judged shapes, passed there as a control should. - verified: groups architecture-map, artifact, auto-accounting, auto-reservation, checks, cli, collect, collection-prerequisites, collector-workflow, decompose, hardening, preflight, repo-hygiene and selftest-cli, with the files their names match - 687 passed, 0 failed (358.1 s, h/groups.log). - verified: GitHub's renderer (`gh api markdown`, synthetic text; renders cached under E:/Temp/review-followups/h/gh): probe v1 (h/probes/v1-verify.mjs, output h/out/v1-verify-h.txt), 57 table cases and 43 footnote cases on G-1 to G-5. On this tree 0 false passes and 0 footnotes read otherwise than GitHub; on 50b18d1, 33 and 15. Where GitHub shows a table the tree refuses, it is one ADR-0203 decision 8 names (H1f, AF1, S1, S4), or TC2, whose row GitHub cuts at the comment; the end-of-file case (EOF) refuses, writable at 50b18d1. - verified: the reviewer's probes, on copies (h/gcopy, whose `f` is this tree): g01, 0 false passes (50b18d1: 7), the two refusals as at 50b18d1; g03, 64 reference shapes, 0 mismatches with GitHub (50b18d1: 10); g04, 64 README-style shapes read and judged as at 50b18d1 (63 writable, R23 refused as there); g05, 150 fuzzed pages, the 14 false passes of 50b18d1 refused, the other 136 as at 50b18d1; g08, 176 real Markdown files read and judged as at 50b18d1, none refused. - verified: timing - p10's shapes at 100k, at worst 247 ms (295 when measured for 50b18d1); the three slowest at 100k, 200k and 400k, on both trees in one process, as fast or faster here and linear (references then `(`: 263, 456, 801 ms; 50b18d1 294, 555, 813; h/out/p10b-both.txt); the inline scan on six span-heavy shapes at 100k, 200k and 400k, at worst 492 ms at 400k (h/out/t1-inline.txt). - verified: the full suite, run by the commit hook (ADR-0120) - this commit exists only if it passed. ## What you got wrong and fixed 50b18d1's mistakes, found by the re-review: 1. - mistake: raw HTML GitHub writes as it stands in a paragraph, a heading or a cell was read for references only, never by the tokenizer, so a `<!--` or a quoted value the browser opens after its early end of a span - a processing instruction or CDATA at a `>` inside it, a comment at `--!>` - was never seen. - where: `hiddenLines` in `fetchproof/lib/corpus.mjs` (50b18d1's lines 1394-1406, `feedHtml` for HTML blocks only, and 1434-1447); ADR-0203 decisions 2 and 8; the CHANGELOG's F01 entry. - impact: preflight PASS on an archived CLOSED copy GitHub hides while it shows an OPEN unknown, and a request written into the copy as U-2 (g02); rows written to a table no one sees (H1a to H1e). dde656a the same. - cause: cmark-gfm's end of a span was taken for the browser's, and an HTML block for the only place that opens what hides a table. - fix: `spans` from `footnoteRefs`; the inline scan after the pass; `unsure` with `why` 'inline'. - verified: the raw-HTML corpus test and the request test, red before (h/red.log); probe v1 on H1a-H1f, Q1, TC1, TC2, FD1-FD3, LI1, LT1, ML1, ML2, AF1, AF2, CX and IA2 against GitHub. 2. - mistake: a link made inside brackets deactivated the brackets around it for good. - where: `footnoteRefs`, `inactiveBelow` and `pop` (50b18d1's lines 655-668 and 707). - impact: a footnote reference GitHub reads taken for text, so the footnotes it shows misread: the gate passed on a superseded copy, and a U-2 was written that GitHub shows twice (g02b; N1). dde656a the same. - cause: cmark-gfm's `no_link_openers` was modelled as a depth, not as a flag the next bracket pushed clears. - fix: the `noLinkOpeners` flag. - verified: the bracket test, red before; B1-B5, B8, B10-B12 and N1 against GitHub. 3. - mistake: labels were case-folded with Node's Unicode. - where: `normalizeLabel` and its comment (50b18d1's lines 287-297). - impact: labels GitHub holds apart matched, so a definition GitHub drops, and its table, read (F3-F7, F21, F25). dde656a the same. - cause: a case fold was taken to be the same in every Unicode version; GitHub's tables are older than Node's. - fix: `FOLDED` and `foldChar`: the blocks GitHub's tables cover, folded; the rest kept. - verified: the fold test, red before; F1-F27 against GitHub. 4. - mistake: a link destination that leaves a `(` open was taken for none. - where: `linkDestination` (50b18d1's line 363), used by `inlineLinkEnd` and `refDefAt`. - impact: a `[^label]` GitHub reads as link text counted, so a definition GitHub drops, and its table, read (M1, M2). In the inline-link case a regression from dde656a. - cause: a balance test was written for the shapes seen, not read from cmark-gfm's `manual_scan_link_url_2`, which has none. - fix: no destination only where the run reaches the end of the text. - verified: the destination test, red before; P1-P4, P7-P9, M1 and M2 against GitHub. 5. - mistake: a `<select>` was not modelled. - where: `feedHtml` and `endHtml` (50b18d1's line 837 on), and the inline texts. - impact: a table GitHub shows as the text of its cells, or drops, read as the contract and written to (S1-S4, S13-S17, S21, S23, S24). dde656a the same. - cause: the browser was modelled as its tokenizer only; its tree builder's "in select" mode drops the table's tags. - fix: `owner.select` and `tagName`; the select checks in `endHtml` and the inline scan, `why` 'select'. - verified: the select test, red before; S1-S24 against GitHub. 6. - mistake: the map's `corpus.mjs` row named `scanRefs` and the "before a `(`" rule, and five sentences of ADR-0203 and the CHANGELOG said what G-1 to G-5 make untrue - among them "The reading errs one way only, to a refusal". - where: `docs/ARCHITECTURE.md`, ADR-0203 (decisions 2 and 8), `CHANGELOG.md`. - impact: words only; a reader trusting them expected a refusal the kit did not give. - cause: the words were written before the renderer was asked about inline HTML. - fix: the sentences rewritten (What it touched). - verified: read against the code; groups architecture-map and repo-hygiene pass. Found while fixing them: 7. - mistake: with no table read, the writer started one at the end of a file whose body was in doubt - past an HTML block read two ways (50b18d1's own rule), and now past raw HTML or a select. - where: `writeCheck` in `fetchproof/lib/corpus.mjs`, its path with no table read. - impact: a table started where GitHub may hide it, which the next read refuses. PR #441 only. - cause: the doubt was weighed for the tables read, not for the place the writer starts one. - fix: `endUnsure`; the writer's end-of-file refusal (`kind` 'doubt', `end`) and its words. - verified: the end-of-file cases of the raw-HTML and select tests; probe v1's EOF case, writable at 50b18d1 and refused on this tree. This commit's: 8. - mistake: I predicted that `[^z <?x > <!-- ?>]`, with `[^z]:` defined, holds no reference; GitHub makes it a reference to z, and drops the raw HTML inside it. - where: the analysis (h/verify.md, the step 7 notes), not the code. - impact: none delivered; coded so, the scan would have read spans GitHub drops and refused a file GitHub reads. - cause: the label was taken to be the whole of the brackets; cmark-gfm takes the text up to the first node that is not text. - fix: the spans are cut at a reference made (`opener.spans`), defined or not. - verified: FR1-FR3 against GitHub (probes show1 and show2); FR1-FR3 in the judged test. 9. - mistake: the first design of the inline scan counted a span's line from the text's start for each span, quadratic in the spans of a text. - where: the inline scan in `hiddenLines`, before this commit. - impact: none delivered; fixed before the tests ran. - cause: the line was found by search, not carried along. - fix: the count carried along the spans, in order (`row`, `nl`). - verified: probe p10's timing (What you verified). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oubt are marked in linear time (re-review of 50c0afd) ## What changed - `fetchproof/lib/corpus.mjs`, the inline scan in `hiddenLines`: - A, line ends. Each line of a raw-HTML span but its last is fed to the scratch tokenizer (`feedHtml`) with its line end, which the browser reads as white space: it ends a tag's name and an unquoted value, and a filtered tag's name before one (`FILTERED`, which already takes a line end) leaves the `<` as text. Fed without it, `<b` and `title="` on the next line were read as one tag name, `<title` and `x=` as a tag `titlex=` the tagfilter does not filter, and `<b` and `title="x>` as a tag ended at the `>` inside the value. - B, the marking. The scan keeps each owner's first doubt - the body's or a footnote definition's, the one at the lowest line - and each cell's mark for its table's header line (`earliest`, `rowMarks`). After it, one pass over the lines marks each owner's lines from that doubt on (`unsure[k] ??=`), then the header lines, then each owner's `unsure`. Each text in doubt had marked its owner's lines from its own line to the file's end. ## Why The fresh-eyes re-review of 50c0afd (E:/Temp/review-followups/h-findings.md) found two defects. I checked each on my own renders and timings (h/probes/i1-verify.mjs, output h/out/i1-verify.txt): - A (medium; a gap G-1's fix left, 50b18d1 the same): raw HTML over a line end. GitHub shows no table below `Text <?x > <b` / `title=" ?> end.` (ML1), the same with `a=1` (ML4), `Text <title` / `x="<!--"> end.` (T1) or a list item's `<style` / `media="<!--">` (T3); the kit read the table below each, and the writer would have written there. With an archived copy above a `<div class="x">` block (ARC), GitHub shows the live table and the kit read the copy. `Text <b` / `title="x><!--"> end.` (O1) was refused, though GitHub shows its table. End to end (the reviewer's h01), preflight passed and a request wrote U-2 into the hidden copy. - B (medium; a regression in 50c0afd): the marking was quadratic. 20,000 paragraphs of `x <?><!--?>` took 2.7 s here, and 40,000 took 10.4 s in the red run below; the reviewer measured 5.1 s for 20,000, 9.4 s for 40,000, 37 s for 1 MB and 5.2 s for 20,000 footnote definitions in doubt. The reader runs on every check and every write, and on a package's EVIDENCE.md in the validator. Set aside: - feeding a span whole, line ends and all, in one call - each line keeps its own number, which `since` records; - marking each owner from its first text in the scan's order rather than its lowest line - the same while the texts come in document order, as they do, and the lowest line marks the lines 50c0afd marked even if they did not. ## What it touched - Code: `fetchproof/lib/corpus.mjs`. - Tests: `fetchproof/test/corpus.test.mjs` (two new: the line-end shapes; many texts in doubt read in linear time), `fetchproof/test/requests.test.mjs` (one new: a request refused, writing nothing, below a quoted value opened over a line end). - Map: `docs/ARCHITECTURE.md` row `corpus.mjs` (the inline scan's line ends and its marking). - ADR-0203 (PR #441 unmerged, amended in place, 2026-11-08): decision 8's inline bullet, each line with its line end; the consequences - the shapes rendered for this re-review, and the linear claim, each owner's lines marked once, with the two new shapes in the 2 s bound. ADR-0204 is unchanged: the writer's `doubt` refusal is the same, only more shapes reach it. - `CHANGELOG.md` (the F01 entry: the re-review's two findings; 65 -> 68 tests), `fetchproof/README.md` (2340 -> 2343 tests). - No new domain term; `CONTEXT.md` unchanged. ## What you verified - verified: red first - `node fetchproof/bin/selftest.mjs corpus requests` with the new tests on 50c0afd's library: 178 passed, 3 failed in 399.1 s (h/i-red.log), cwd E:/Claude/fp-p0-followup. The three are the new tests: MLa read as judged (`[true, 8, []]` where `[8]` was expected), 40,000 paragraphs in 10,358 ms, and the request applied where `TABLE_HIDDEN` was expected. - verified: `node fetchproof/bin/selftest.mjs corpus requests` on the final code - 181 passed, 0 failed in 393.4 s (h/i-green.log), cwd E:/Claude/fp-p0-followup. - verified: the groups architecture-map, artifact, auto-accounting, auto-reservation, checks, cli, collect, collection-prerequisites, collector-workflow, decompose, hardening, preflight, repo-hygiene and selftest-cli - 687 passed, 0 failed in 355.8 s (h/i-groups.log). - verified: GitHub's renderer (`gh api markdown`, synthetic text only): the new tests' bodies (h/probes/i2-bodies.mjs, h/out/i2-bodies.txt) - GitHub shows no table for MLa to MLd and MLf to MLj, nor one appended at the end of the file; for MLe and the requests test's contract only the live table; for MLo to MLr the table. On this tree (h/out/i2-bodies-w.txt, no GitHub call): MLa to MLj and the requests test's contract are refused at their table (`table-split`, the writer's `doubt`), MLo to MLr judged and writable, and the end of the file refused by the writer (`doubt`, line 5). - verified: the reviewer's probes, on copies pointed at this tree (h/icopy, output h/icopy/out), no GitHub call: - h08-fix, the finding shapes: ML1 to ML8, T1 to T3, E1 and E2 refused; ARC and T4 refused where 50c0afd read the archived copy; O1 judged; O2 to O4 and the controls as on 50c0afd. - h08-fix, 1744 pages (697 synthetic, 1047 real): read as on 50c0afd but for the same 19 finding pages, none of them a real file. - h08b, 1499 cached synthetic pages: 31 read differently from 50c0afd - 23 where GitHub shows no table, now refused (one of them already refused at the end of the file, now naming line 6, where the quoted value opens, for 5); 6 where GitHub shows only the live table below an archived copy, now refused where 50c0afd read the copy; and 2 where GitHub shows the table, now judged (O1, MLo). - i08c: 0 of those 1499 pages read differently from the reviewer's patched copy (Hfix). - h01, end to end on this tree (i01e-e2e-w.mjs): preflight fails on `table-split` and the request is refused with `TABLE_HIDDEN`, writing nothing; 50c0afd passed and wrote U-2. - verified: the full suite, run by the commit hook (ADR-0120) - this commit exists only if it passed. ## What you got wrong and fixed 50c0afd's mistakes, found by the re-review: 1. - mistake: the inline scan fed each line of a span to the tokenizer without its line end. - where: `fetchproof/lib/corpus.mjs`, the inline scan in `hiddenLines` (50c0afd's line 1584). - impact: G-1's fix was incomplete. Raw HTML that opens a quoted value or a comment after a line end still hid the table below it while preflight passed and the writers wrote there; an archived copy above a `<div class="x">` block was read in place of the live table; and one shape GitHub shows was refused. PR #441 is unmerged: nothing delivered is affected. - cause: the span was split on its line ends and the pieces fed as they were; the browser reads the line end too. - fix: each piece but the last is fed with its line end. - verified: the new test refuses MLa to MLj and judges MLo to MLr, each body rendered on GitHub (h/probes/i2-bodies.mjs, output h/out/i2-bodies.txt); i1-verify reads every shape as GitHub shows it (h/out/i1-verify-w.txt: ML1, ML4, T1, T3 and ARC refused, O1 judged, the controls C1 and C3 refused as before, the end of the file refused by the writer). 2. - mistake: each text in doubt marked its owner's lines from its own line to the file's end. - where: the same scan (50c0afd's line 1591). - impact: time in the square of the file on many texts in doubt - 20,000 paragraphs 2.7 s here and 40,000 10.4 s; 1 MB 37 s by the reviewer's measure - on every check and write. Nothing delivered is affected. - cause: the mark was written per text, without asking how many texts a file can hold in doubt. - fix: each owner's first doubt is kept, and the lines are marked in one pass after the scan. - verified: the new linear-time test (passed; on 50c0afd 40,000 paragraphs took 10,358 ms); the reviewer's timings on copies, parseTable: h08 40,000 paragraphs with a comment, a quoted value or a `<select>` left open 418 to 490 ms (50c0afd 9,020 to 11,258 ms), 20,000 footnote definitions 467 ms (4,661 ms); h02 40,000 paragraphs 296 ms (21,059 ms). 3. - mistake: 50c0afd's message and ADR-0203 claimed what A and B make untrue: "0 false passes" on probe v1, decision 8's "errs to a refusal, in each shape rendered on GitHub", the consequences' "linear in the file ... and so is the inline HTML of a text", and "at worst 492 ms at 400k", which timed shapes with few texts in doubt. - where: commit 50c0afd's message; ADR-0203, decision 8 and the consequences. - impact: a reader of the PR would have trusted a guarantee that did not hold; no code was affected. - cause: probe v1 held no span that crosses a line end inside a tag, and the timings no file with many texts in doubt. - fix: ADR-0203 amended - the inline bullet, the shapes rendered, the linear claim and its shapes; this message corrects the pushed message's claims. - verified: i2-bodies covers a line end after a tag's name, an attribute's name, an unquoted value and a filtered tag's name, and a `>` in a value; the new timing test, many texts in doubt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
StepenkoAnatoli
added a commit
that referenced
this pull request
Nov 8, 2026
What changed - Brings PR #441's three commits since the merge base dde656a into the #442 branch: 50b18d1, 50c0afd and 5f44730, the corpus reader's fixes from the re-reviews of dde656a, 50b18d1 and 50c0afd (footnote references read by cmark-gfm's inline parse, each owner's HTML read by its own tokenizer, a table no check can judge refused; inline raw HTML and a `<select>` left open leave the tables after them in doubt; raw HTML over a line end read with its line end, and the texts in doubt marked in linear time). The merge itself changes no code: since dde656a the two branches touch no module and no test file in common. Why - #442 is stacked on #441. #441 merges first; #442 is then retargeted to main and must hold #441's final head, so it merges with nothing left to reconcile and its CI runs the combined code. What it touched - Resolved by hand: fetchproof/README.md, the test count (2313 at the merge base; #442 adds 28 for 2341, #441 adds 30 for 2343; together 2371). - Merged by git: CHANGELOG.md (#441 edited its F01 entry, #442 added its own), docs/ARCHITECTURE.md (each branch edited other rows). - No ADR owed: no decision was made. The map rows and ADRs come with each branch. What you verified - verified: `git merge-tree --write-tree HEAD 5f44730` before merging named only the README count line as a conflict. - verified: no conflict marker is left in any file the merge changed (grep). - verified: the count, by the test declarations in fetchproof/test: dde656a 2248, #442's head 2276 (+28), #441's head 2278 (+30), the merged tree 2306 (+58); 2313 + 58 = 2371. - verified: the groups repo-hygiene, architecture-map, cli and decompose on the merged tree: 230 passed, 0 failed in 240.5 s (h/merge-p1-groups.log), cwd E:/Claude/fp-p0-fixes. - verified: the commit gate ran the full suite on this commit (ADR-0120); it lands only when green, and it refuses a stale "N tests, offline" line, so 2371 is checked too. What you got wrong and fixed - Nothing to report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
StepenkoAnatoli
added a commit
that referenced
this pull request
Nov 8, 2026
- What changed: nothing in the tree; this records main's merge commit of PR #441 (e7e10a4) in the #442 branch, which already held #441's head 5f44730. - Why: #442, retargeted to main after #441 merged, was behind main by that merge commit alone. - What you verified: verified - `git merge-tree --write-tree HEAD origin/main` gave HEAD's own tree (7ff7d4d), and e7e10a4's tree is 5f44730's (81b18d8), so no file changes; the full suite ran on this tree in the hook for 705e3a6. - What you got wrong: nothing to report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Five P0 defects found by two of the three independent audits of
532c696(kit 0.9.5): F01, F03 and F02from the Codex audit, A-1 and FP-011 (alias B-2) from the Claude audit. Each fix has a test seen to fail
on the old code, and its own commit with the five-part report; the three with an ADR record it as a bug fix
under the freeze (ADR-0117).
109d800). With TEMP missing,mkdtempSyncthrew out ofevaluate, and the pre-commithook, fail-open by default, allowed the commit.
materializeIndexnow returns the failure, and theunreadable-index block stops the commit with a fix that names the temp folder (TMPDIR, TEMP or TMP).
b2ae053). An archived CLOSED copy of the unknowns table, in an HTML comment or afenced block above the live one, was read as the contract: the live table's OPEN unknown was never read
and the brief was approved.
parseTablenow reads the first table GitHub renders, andappendRowwritesinto that table.
52f7b86, ADR-0197). A ZIP entry name that another platform reads as a different nameis refused: an empty segment, a character or device name Windows does not allow, a segment ending in a
dot or a space, an 8.3 short-name shape. One
pathKeyidentity (NFC, case-folded) now judges duplicates,case collisions and file-folder collisions, in the reader, the validator's never-packaged paths and the
producer.
47de5a1, ADR-0198). The browser route refuses an upstream proxy that is notplain HTTP (
https:,socks5:, any other scheme) before any browser starts, instead of speakingplaintext to it. It never falls back to a direct connection, and the message never contains the
credential.
1078ead, ADR-0199). A missing, unreadable or table-lessresearch/EVIDENCE.mdina gated project is named by
corpus-shapeandcitations(evidence-missing,evidence-unparsed).citationsfails with it instead of passing over zero rows, andcorpus-shapeno longer says the corpusparses cleanly.
How it was checked
against the old code and pass with the fix; the commit messages give the counts. A-1's fix was written
before its test, as that commit reports.
audit-regressions.mjs) on this head: 5 pass, 5 fail. Controls 1-3, F01 andF03 pass. F02 fails at the new refusal (below). F04, F05a, F05b and F06 are P1, fixed in the PR stacked
on this one. The unfixed baseline was 3 pass, 7 fail.
For the owner
https://proxy, which would be anew capability under the freeze. This PR refuses such a proxy instead (ADR-0198).
Follow-ups (not in this PR)
check. Tables in indented code blocks, and in HTML blocks other than comments, are still read.
aliasing was not reproducible here (short names are off on this machine). Exclusive-create (
wx)materialisation is a follow-up. The producer's EXCLUDED list is compared by exact spelling.
doctordoes not report it.MAP.mdandSOURCES.mdlost the same way are unchanged. A commit confined toresearch/that deletes
EVIDENCE.mdis still committable as phase-1 work (ADR-0048).project/.git/, and the validator does not refuse it.🤖 Generated with Claude Code