Skip to content

One paid solve per page, parser failures exit 5, CSV formula guard, canary fails without its secret - #2

Merged
jehrr merged 1 commit into
mainfrom
fix/audit-2026-10-14
Oct 15, 2026
Merged

jehrr merged 1 commit into
mainfrom
fix/audit-2026-10-14

Conversation

@jehrr

@jehrr jehrr commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

Fixes from the 2026-10-14 audit of kohls-scraper (base 61db4e0 = audited SHA, no other open PRs).

Behaviour changes

  • Exit code: a page that was served with its listing and parsed to zero rows (served_but_unparsed) now exits 5, not 4. Exit 4 says the catalogue is empty. A genuinely empty listing is classified empty before parsing and still exits 4. The smoke check that pinned 4 was rewritten, with the reason in a comment.
  • Captcha budget: a page buys at most one paid solve across all its block retries and exit rotations (page_flow.SolveBudget, SOLVES_PER_PAGE = 1). The allowance is created once per page and checked before the solver is called. Before this, a probe with a stub solver counted 2 solves with no pool and 6 with --proxy-block-retries 5. Both cases now buy 1.
  • File modes: the atomic writer keeps an existing output's mode (0640 stays 0640). New files still get 0o666 & ~umask.

Other fixes

  • CSV formula guard: a string cell starting with = + - @ TAB CR LF gets a leading ' in the CSV only. The check runs after lists are joined. Numbers and JSON are untouched. csv_cells_escaped goes into the sidecar, under the run's own keys.
  • pyppeteer proxy auth: page.authenticate falls back to CDP Fetch.authRequired (source == "Proxy" only) when Network.setRequestInterception is missing. Ported from a sibling repo where it was measured. Not run through an authenticated proxy here. The README engine table now says so.
  • .gitignore: added /*.html, so a bare --dump-html page.html is ignored.
  • Private citations (section numbers, private file names) removed from 8 tracked files. A suite guard now fails if any tracked file contains them, and it builds its banned tokens from pieces so it does not match itself. The release notes for v1.0.0 and v1.0.1 contained none, so they were not edited.
  • Removed the "credentials live about a day" claim from .env.example and canary.yml. It was never measured for this repo.
  • captcha_solver.py docstring: describes the removed flag without quoting its name.

Canary (owner request)

  • Before: dispatch-only. It skipped green with a ::notice:: when the secret was missing, and its floors were a fixed 3 pages and 300 rows.
  • After: if KOHLS_CDP_ENDPOINT is missing, the job fails with ::error::Missing secret KOHLS_CDP_ENDPOINT. The assertions moved to .github/canary_assert.py, which the suite tests. Exit 3, 4 or 5, zero rows, or any missing core field (sku, url, title, price, currency) is red. The page and row floors come from the listing's own total_results, pages_available and per_page, at 95%. A smaller brand assortment is therefore not a failure, while a parser that loses rows is.
  • Still dispatch-only. Scheduling it spends Scraping Browser usage, and the audit marks that as an owner decision.

Evidence

  • Core-only venv (Python 3.13): smoke_test.py passes, all 3 engine groups skipped. ci_checks.py --all ok.
  • pyppeteer venv: suite passes, and the 8 new proxy-auth checks ran.
  • Controls run with PYTHONDONTWRITEBYTECODE=1 and __pycache__ removed. I planted each fault, confirmed the suite went red on the named check, restored it, and confirmed green:
    • CSV escape off
    • mode preservation off
    • budget check removed
    • budget not shared across attempts
    • a planted private citation
    • parser exit back to 4
    • canary back to ::notice::
    • canary rc check off
    • /*.html removed
    • pyppeteer fallback broken

All 10 were red on the named check and green after restore.

Not changed

  • F10: refusing, instead of warning, for --concurrency/--proxy/--fingerprint with --cdp-endpoint. Owner decision.
  • F13: diffing skus_in_stock/skus_total/rating. Owner decision.
  • Canary schedule. Owner decision.
  • Versions, tags and releases.
  • Repo description (27 words; informational only in the org audit). Topics are fine.
  • .env.example KOHLS_URL: this repo has no builder flags it could hijack, and the audit's loader round-trip passed.

🤖 Generated with Claude Code

…lls escaped

- CSV: string cells starting with = + - @ TAB CR LF get a leading apostrophe
  in the CSV only; csv_cells_escaped recorded in the sidecar.
- Captcha: one paid solve per page (page_flow.SolveBudget), checked before
  purchase and not reset by block retries or exit rotation.
- served_but_unparsed with zero rows exits 5 (parser failure), not 4.
- Atomic writer keeps an existing output's mode; new files follow the umask.
- pyppeteer: page.authenticate falls back to CDP Fetch.authRequired when
  Network.setRequestInterception is missing.
- Canary fails with ::error:: without KOHLS_CDP_ENDPOINT; assertions moved to
  .github/canary_assert.py and tied to the listing's stated size.
- .gitignore: repo-root *.html (bare --dump-html captures).
- Comments and docs no longer cite private notes; suite guard added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jehrr
jehrr marked this pull request as ready for review October 15, 2026 21:16
@jehrr
jehrr merged commit 80943cb into main Oct 15, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant