Repository navigation
Walk every event page, and stop reading every page as its own 404 - #15
Merged
Merged
Conversation
Works the third-party audit of 8 October against ed65d78, which is current main. Its P1 is real; reproducing it found a second defect underneath that the audit did not see, and the two compound into a silently truncated run reported as complete. P1 — the --mode events walk ended at the first event page with no markets. Reproduced end to end: one empty page at position 4 of 20 left 17 event pages never fetched, with status: complete, exit 0 and pages_completed: 4 of 21 in the same sidecar. The rule came from a sibling whose pages are consecutive days; here pages 2..N are the event URLs page 1 named, capped at --pages - 1, so there is no catalogue to exhaust and the early stop saved nothing. The walk now visits every planned URL and records the empty ones as pages_without_rows. no_new_products is set nowhere and leaves COMPLETE_STOP_REASONS rather than staying as policy with no reader. Underneath it: Next.js inlines its 404 TEMPLATE into the payload of every page, and looks_not_found scanned 200,000 bytes for that template's wording. Measured 2026-10-09 over the twenty event pages /predictions named: the wording is on 20 of 20, inside the window on 17 of 20, rendered into body markup on 0 of 5 — and 12 of 20 held 5 to 53 markets and had every row discarded by empty's parse: False. The markers are gone rather than narrowed, and the content check moved to the top. A slug that does not exist answers HTTP 200 (4 of 4), so the docstring promising a real 404 was wrong too; "digest" looked like a replacement and is on 2 of 10 good pages, so it was not adopted. The browser engines escaped by byte position, not design: the template sits at 511,547 in a browser's DOM and 134,033 in the served bytes of the same URL. scraper_api_client.py reads the served bytes. Reclassifying a bogus slug as shell cost 67s instead of 8s, so the readiness wait is now gated on the payload ARRIVING rather than on it naming markets (page_flow.payload_arrived). Back to 7.8s, same exit 4. captures/event_served_bytes.html is the first capture of what the HTTP client path sees — all eleven others are a browser's page.content(), which is why this was invisible to a green suite. make_fixtures.py carries the "notFound" branch through the trim on purpose: without it the new checks cannot fail, which the first version of the fixture demonstrated. Also fixed, neither in the audit: the four tree-wide scanners in smoke_test.py skipped virtualenvs by NAME, so one called .venv-pw was walked into and pip's vendored charset_normalizer was reported as "describing this site"; and .gitignore had the same hole, leaving that venv untracked and not ignored. Both now key on pyvenv.cfg. Eight faults planted, suite red on a named check for each. Three controls were wrong first time and are described in the PR. Live after the fix: --mode events --pages 10 complete, 213 markets, pages_without_rows []; 0 of 20 event pages misclassified, against 12 before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merged
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.
Works the third-party audit of 8 October against
ed65d78, which is currentmain. Its P1 is real, and reproducing it turned up a second defect underneath that the audit did not see — the two compound, and the second is what makes the first common rather than theoretical.P1 — the
--mode eventswalk stopped on the first empty page and called itcompleteConfirmed, and it is this repo's own code rather than anything inherited. Reproduced end to end against
main, driving the real dispatcher into the realfinish_run, with page 4 of 20 holding no markets and pages 5..21 holding three each:The sidecar carries
pages_completed: 4 of 21and calls itselfcompletein the same file. That is the self-contradiction commit60e65bafixed for pages that FAILED; it never looked at pages that were never attempted.Why the rule was wrong here, rather than just unlucky. It came from a sibling whose pages are consecutive days, where "asking for 40 days of a tag that published on three would fetch 37 empty ones" is a real saving. In this mode the specs are the event URLs page 1 named, capped at
--pages - 1— the engine already logs how long the walk really is before starting it. So the queue cannot contain a page worth skipping: the saving did not exist and the cost was the rest of the listing.Fixed in all three engines, sequential and concurrent paths. An event page with no readable markets is recorded in the sidecar as
pages_without_rowsand the walk carries on.no_new_productsis set nowhere now, so it is out ofCOMPLETE_STOP_REASONSrather than left as policy nothing reads.Same scenario after the fix: 20 of 20 fetched,
pages_completed: 21 of 21, and the file holds 77 markets instead of 26.The defect underneath it: every served page classified as its own 404
Found while measuring whether an empty event page is an edge case. It is not, because the classifier was manufacturing them.
Next.js inlines its 404 template — a
"notFound":[…]branch the router would render if the route had 404'd — into the payload of every page the site serves.looks_not_foundscanned the first 200,000 bytes for the wording in that template. Counted on 2026-10-09 across the twenty event pages/predictionsnamed, as the site serves them:emptywhile holding marketsemptycarriesparse: False, so those rows were discarded and the page produced zero rows — which is exactly what tripped P1 above. The two defects compose into a silently truncated run reported ascomplete.The browser engines escaped it by byte position, not by design: a browser serialises rendered markup first and pushes the template past the window (511,547 against 134,033 on the same URL). The margin on the smallest page measured was 67 KB.
scraper_api_client.pyreads the served bytes and had no such margin.Three further measurements, each of which changed what the fix had to be:
detect_page_state's docstring promising "a real HTTP 404" was wrong, and the status could not be the backstop the text markers were assumed to have;"digest"looked like a clean replacement — 4 of 4 bogus pages, 0 of the first two good ones — and is not: counted properly it is on 2 of 10 good pages. Not adopted.So the markers are gone rather than narrowed, and the content check moved to the top: a page whose payload names markets is content, whatever templates it also ships. A nonexistent event now falls through to
shell, parses to no rows and exits 4, which is what a bogus listing tag already did.Cost of that reordering, and the second fix it needed.
shellspends the readiness wait, so a bogus slug went from 8 s to 67 s. The wait exists for one case — the payload never arrived — so it is now gated on the payload ARRIVING rather than on it naming markets (page_flow.payload_arrived, used byis_unpaintedand by all three engines' fast path). Back to 7.8 s, same exit 4.Why no fixture could show it
All eleven existing captures are a browser's
page.content(). The repo had no capture of what the HTTP client path sees, which is a different document shape from the same URL — the same gap as the CDP fixture added in #7, and the same lesson: a guard is only as good as the fixture it runs against.captures/event_served_bytes.htmlis that capture, andmake_fixtures.pynow carries the"notFound"branch through the trim on purpose. Without that the new checks could not fail — the first version of the fixture had the template trimmed out and passed against the unfixed parser.Verification
Eight faults planted, suite required to go RED on a named check:
no text marker decides not-found — only an explicit 404a page naming markets is content even under a 404 statusan empty event page does not stop dispatch(+2)no_new_productscomplete again`no_new_products` is NOT a complete stop reason(+1)describes this siteon pip's vendored code (+3 wiring)test_wording() walks the tree and skips virtualenvsthe virtualenv .venv-pw/ is gitignoredthe served-bytes fixture really does carry the 404 templateThree of those controls were wrong on the first attempt, and each is the reason the pass was worth running:
Tracebackcalled every control a crash, because a GREEN run of this suite prints one deliberately;defline, producing a SyntaxError — a non-zero exit that proves the edit was reachable and nothing about the guard;Live, after the fix:
--mode events --pages 8complete with 192 markets over 8 pages; the event that curl classifiedemptyparses 5 rows; a bogus slug exits 4 in 7.8 s; 0 of 20 event pages misclassified, against 12 before.Also fixed: virtualenvs, by name instead of by kind — twice
Neither is in the audit; both were found by the suite failing on my own working tree.
The tree scanners walked in. The scanner widened to the whole tree in #8 reported that pip's vendored
charset_normalizer"describes this site", because it skipped virtualenvs by NAME (.venv,venv) and mine is.venv-pw..github/ci_checks.pyhad already been fixed to recognise a venv bypyvenv.cfg; the four scanners insmoke_test.pyhad not. It is the first thing a new user sees frompython3 smoke_test.py, since the README tells them to make a virtualenv per engine and does not say where.The four fail in three different ways, which is why the wiring is pinned rather than the content: the describes-this-site scan gives a false failure, the dead-name corpus gives a false pass (vendored code keeps a dead name alive by coincidence), and the banned-wording scan is unaffected today.
And
.gitignorehad the same shape of hole. It listed.venv,venvandenv, so.venv-pwwas untracked and not ignored — oneadd -Afrom being committed. The patterns are broader now, and since a glob can always be out-named, the suite asserts that every directory holding apyvenv.cfgreally is ignored. On a clean checkout that check reports it had nothing to find, rather than passing silently.What is NOT changed here, and why
saverefuses to overwrite good output with an empty result. An id would add a mechanism to enforce something enforced by not writing.finish_run. What is genuinely missing is a LIVE cross-engine check, which is the audit's other option and is cheap.🤖 Generated with Claude Code