Skip to content

fix: fail closed on empty top exports - #19

Merged
axisrow merged 3 commits into
mainfrom
ao/wordstat-9/root
Aug 21, 2026
Merged

axisrow merged 3 commits into
mainfrom
ao/wordstat-9/root

Conversation

@axisrow

@axisrow axisrow commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #11 by refusing to treat a header-only top_popular/top_related CSV as a successful export when the active page has rendered rows.

Live CDP investigation found that Wordstat itself currently creates a header-only wordstat_similar_queries.csv blob for both top views, despite the DOM showing data. The same was reproduced by clicking the export link directly in the attached browser, so this is not a collector readiness race. The collector now raises a visible failure, preserves the raw CSV for inspection, and leaves manifest.json incomplete instead of silently producing status: complete.

Verification

  • Live CDP (http://127.0.0.1:9223), купить телефон: failed closed with Wordstat returned an empty top_popular CSV while the page rendered 200 table rows; export is not trustworthy; raw CSV 211 bytes; manifest status incomplete.
  • Live CDP (http://127.0.0.1:9223), курсы английского языка: same failure; raw CSV 228 bytes; manifest status incomplete.
  • Direct browser export inspection: top_popular and top_related both produced header-only wordstat_similar_queries.csv; dynamics and regions produced populated files (1094 and 41687 bytes respectively).
  • uv run pytest -q — 112 passed
  • uv run ruff check . — passed

Risk / follow-up

This is intentionally fail-closed rather than a fabricated fallback: the upstream Wordstat export is empty while the rendered table is populated. A later fix can add a supported export/data source once the upstream behavior is understood.

@axisrow

axisrow commented Aug 21, 2026

Copy link
Copy Markdown
Owner Author

🔍 Local review (cycle 1) — round d98c10e5-b896-4eca-aa0d-9bc80d2c9ed3

Reviewed locally (/review + Codex companion), no bots pinged.

Verdict Reviewer Finding Location
claude (/review) Diff reviewed as correct and well-scoped; no findings raised. src/wordstat/collector.py
FIX codex Post-download empty-table check can accept a bad export instead of always rejecting it for two views. src/wordstat/collector.py:340

Proceeding to fix the codex finding before continuing.

…ownload DOM re-check

_select_view already hard-gates TABLE_ROW_SELECTOR.length > 0 on the DOM
before "Скачать" is clicked for TOP_POPULAR/TOP_RELATED — a phrase with
genuinely zero rows never reaches the download step, it dies inside
_select_view's retry loop. So an empty CSV reaching _collect_one's parse
step already contradicts a state the code itself proved moments earlier.

The previous gate re-queried the DOM after download (an unbounded polling
window) and only raised when that later read still showed rows. That
re-read can observe a table that has since emptied (rerender, auth
transition, page degradation) and silently wave the corrupted export
through as an apparently valid row_count: 0 — the exact silent-corruption
path the fail-closed fix for issue #11 was meant to close.

Extract the predicate into _is_untrustworthy_empty_export(view, dataset), a
pure function taking no page/session argument, so it is unit-testable
without a CDP/browser harness — closing the "collector.py is untested" gap
for this specific gate. Adds regression tests covering top_popular/
top_related (always rejected when empty) and dynamics/regions (untouched
by this gate, per issue #11's live-CDP data showing dynamics consistently
populated).

Found by Codex review during the cycle-review pass on PR #19.
@axisrow

axisrow commented Aug 21, 2026

Copy link
Copy Markdown
Owner Author

🔍 Local review (cycle 2) — round 0b252bc1-be5b-47f3-b057-2cf2e80faa12

Reviewed locally (/review + Codex companion), no bots pinged.

Verdict Reviewer Finding Location
claude (/review) Cycle-1 fix reviewed as a sound, well-justified simplification; no findings raised. src/wordstat/collector.py
FIX codex Fixed: empty-export guard now also rejects DYNAMICS, matching the same pre-download rows>0 gate as top_popular/top_related. src/wordstat/collector.py:59

Proceeding to fix the codex finding before continuing.

_select_view's pre-download hard gate (TABLE_ROW_SELECTOR.length > 0) is
keyed off `view != WordstatView.REGIONS` — it applies identically to
TOP_POPULAR, TOP_RELATED, and DYNAMICS, exempting only REGIONS (which has
no table in its DOM). _is_untrustworthy_empty_export's tuple, however, only
checked (TOP_POPULAR, TOP_RELATED), leaving DYNAMICS unguarded: an empty
dynamics CSV could still pass through, get written as a valid row_count: 0
export, and contribute to a manifest status of "complete" — the exact
silent-corruption path the cycle-1 fix (for issue #11) was meant to close.

The previous exclusion of DYNAMICS rested on an empirical/frequency
argument (issue #11's live-CDP data showed it consistently populated, 24
rows across three runs) rather than the gate's actual structural premise
(the DOM already proved rows > 0 immediately before the download click, so
a later empty read is always a lie) — which holds for DYNAMICS exactly as
it does for the other two table-based views. Selecting views by observed
frequency of emptiness instead of by the structural gate they share was the
bug.

Widen the tuple to include DYNAMICS, and rewrite the predicate's docstring
to state the rule structurally (every view _select_view gates on
TABLE_ROW_SELECTOR.length > 0, i.e. all but REGIONS) so it can't drift from
_select_view again. Update the regression tests in test_collector_view.py
accordingly.

Found by Codex review during round 2 of the cycle-review pass on PR #19.
@axisrow

axisrow commented Aug 21, 2026

Copy link
Copy Markdown
Owner Author

📋 Review summary — all cycles

Cycle Reviewer Finding Verdict Resolution
1 codex Post-download DOM re-check could silently accept an already-corrupted empty top_popular/top_related export. FIX Fixed in 5bfb556
1 claude (/review) Diff reviewed as correct and well-scoped. No findings
2 codex Empty-export guard omitted dynamics, even though it shares the same pre-download rows>0 gate. FIX Fixed in 5361365
2 claude (/review) Cycle-1 fix reviewed as a sound simplification. No findings
3 codex Clean (verdict: approve)
3 claude (/review) A reviewer note about phrases-file cp1251 decoding falls outside this PRs diff (collector.py only) and was left unaddressed here. IRRELEVANT Out of scope for this PR's diff (src/wordstat/cli.py, not touched here); not addressed

Totals: 2 FIX (both resolved), 1 IRRELEVANT, 0 SKIP, 0 UNVERIFIED. 3 review cycles completed.

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.

top_popular/top_related возвращают 0 строк данных при --keep-raw, хотя интерфейс показывает данные

1 participant