Repository navigation
Report parser failures and blocks honestly, cap paid solves per page, write output atomically - #7
Merged
Merged
Conversation
… and write output atomically - A rendered listing grid that parses to nothing is a parser failure (exit 5, or 6 with other rows), not an empty search (exit 4); the Scraper API path uses the same page verdicts. - One paid captcha solve per page across attempts, rotations and markers, checked before purchase in all three engines. - JSON/CSV/sidecar written via temp file + os.replace, modes preserved; formula-shaped CSV text cells escaped and counted. - Negative --retries/--delay and zero --pages/--concurrency rejected; --proxy with --cdp-endpoint refused; inert --captcha-api removed. - Credential scan reads .json/.csv/.html/.cfg fixtures and detects venvs by pyvenv.cfg; refuses to report an unscanned tree clean. - Canary fails, naming ZIMMO_PROXY, instead of skipping green. - Public files no longer cite private maintainer notes; a suite check keeps it that way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e log masks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jehrr
marked this pull request as ready for review
October 15, 2026 22:19
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.
Problem
An audit of
mainatb21c30a(2026-10-14) found these contract gaps, each reproduced offline:retries + 1solves (Playwright--retries 5→ 6). A page with both a Turnstile and a reCAPTCHA marker bought 2.json.dumptruncated a 497-byterun.json(and the.meta.jsonsidecar) to 13 bytes of invalid JSON.=HYPERLINK(...)were written to the CSV unchanged.--retries -1ran zero navigations and reported exit 5.--pages 0and--concurrency 0were accepted.--proxywith--cdp-endpointsilently dropped the proxy, and--captcha-apiwas parsed but never read..jsonfixtures, and it could report a tree clean after reading no files.ZIMMO_PROXYsecret exists but reaches the workflow empty, so the live job was skipped every day while the workflow showed green.Resulting behaviour
Pages are classified by the shared
classify_page(), which now has aparser_failedoutcome. With no rows recovered the run exits 5; with rows from other pages it exits 6 (partial), and the sidecar getsparser_failed_pagesand astop_reasonthat names the parser. If no rows were recovered and any page failed, the exit is 5. The Scraper API path uses the same page classification (http_code, challenge detection, rendered grid) and recordsselection.url.captcha_solver.SolveBudget: each engine creates one budget per page, before its attempt loop.solve_turnstile()andsolve_recaptcha_v3()require the budget and spend it immediately before submitting a solve. A missing key or a Managed Challenge without a sitekey does not spend it.All three output files are written through a temp file in the same directory, then flush, fsync and
os.replace; the temp file is removed on error. A new file gets mode0o666 & ~umask, and an existing file keeps its mode.In the CSV, text cells starting with
= + - @ \t \r \nget a leading'. Numbers and the JSON output are unchanged. The sidecar getscsv_cells_escaped.Argparse now rejects bad values with exit 2 in all three engines:
--retries,--delayand--retry-delaymust be ≥ 0;--pages,--concurrencyand--proxy-block-retriesmust be ≥ 1;--proxyor--proxy-filetogether with--cdp-endpointis refused, whether set by flags or by.env.--captcha-apiis removed.Product.currencydefaults to null. The JSON-LD path no longer assumes EUR.The credential scan:
.json,.csv,.htmland.cfgfiles;pyvenv.cfg;sample_outputallowlist on the file name rather than the full path;The canary now fails with
::error::namingZIMMO_PROXYwhen the secret is missing or empty. It no longer has a gate or skip, and any nonzero exit is red. The row floor and the non-null field checks are unchanged.Citations of private maintainer notes are removed from every tracked file, including two lines in released CHANGELOG sections (citation only). A suite check (
check_no_private_citations) fails if one comes back. Release notes for v0.4.0 through v1.2.1 contained none, so none were edited.README: the credential-lifetime wording is corrected, the flag table and exit codes are updated, and the Docker image is documented as needing
--cdp-endpointor a display. The Dockerfile comment is now true:smoke_test.pychecks the COPY list against Playwright's import closure.Evidence
python smoke_test.py, Python 3.13, venv with onlyrequirements.txt: exit 0, 85 PASS, 3 SKIP (the engines' drivers are not installed). With each engine's venv (requirements.txt+requirements-<engine>.txt, no browser download): exit 0, 86 PASS.pytest tests/: core 9 passed; playwright 37 passed; puppeteer 33 passed; selenium 26 passed. The newtests/test_paid_solves_and_verdicts.pyruns each engine's real fetch loop against a fake browser, with both 2captcha submit functions stubbed. Results:--retries 5on a 503 page with a Turnstile widget makes 6 navigations and buys 1 solve.parser_failed_pages == [2].--helpworks for all three engines andscraper_api_client.py.compileallis clean.check_no_credentials.pyreports "No committed credentials found (36 files scanned)".Planted-fault controls, each run with
PYTHONDONTWRITEBYTECODE=1and__pycache__cleared: each fault turned the named check red, and the suite was green again after restoring.open(path, "w")instead of the atomic writerchmodof the temp file removedbudget.spendremovedSolveBudget()moved inside the attempt loop (Playwright)SolveBudget()moved inside the attempt loop (Selenium)test_one_paid_solve_per_page_across_retriesclassify_pageparser branch disabled--retries type=int(Selenium)--retries type=int(Playwright)test_bad_flag_values_are_usage_errorsblocked=dropped.jsondropped from the credential scanpages_completed == 0condition restoredNo live request, paid call, proxy or credential was used. The 2captcha submit functions and
requests.postwere stubbed in every probe.Canary
b21c30ashowgatesuccess andcanaryskipped (HAS_ZIMMO_PROXY: false). Earlier runs onfa41651ran without a proxy, got HTTP 403 and exit 3, and passed with a notice.ZIMMO_PROXYholds a residential proxy URL. Belgian and German exits are known to be served; a US exit was refused on 2026-10-13. The secret currently exists but is empty.Not changed
turnstile/cf-turnstile-response), the sitekey fallback, and therender=explicitmapping. Changing them needs real served and refused captures over--cdp-endpoint.--fingerprint,--fp-*,--localeand--mode,fingerprint_client.py, and thefingerprintstopic. These are owner decisions.[Unreleased]..envkeys, no CSP check on the string-based readiness wait.🤖 Generated with Claude Code