fix(guards): close the silent-omission gaps — crawl, extract, money, charset - #7
Merged
Conversation
…charset
Every defect here fails SILENTLY, which is the class that actually costs research:
the tool returns something that looks clean while the guard that should have
warned could not arm.
crawl ran extraction alone — no looks_blocked, no assess_extraction. A consent
banner or bot wall was written to disk as a numbered page file, cached, and listed
in INDEX.md with an ordinary token count. Walls are now refused: never written,
never cached, links never followed, reported as `refused: <path> (<reason>)` with a
count in the summary, and page warnings ride in the index too. This matters more on
crawl than anywhere else because its output is read back off disk later instead of
being re-fetched — a wall written once is a wall trusted for weeks.
extract cached whatever it fetched into the SAME pages.html row scrape reads, so one
call could poison both verbs, and it returned the wall's own structured data —
usually "no structured data found", which reads as "this page has none" rather than
"we never saw the page".
Money matching had four blind spots, each disarming the dropped-price guard on the
exact page class it exists for. Figures are now canonicalised symbol-first and
space-free through money_figures(), because `$ 49` and `$49` are one price:
- `<span>$</span><span>49</span>`, ordinary pricing-grid markup — tag stripping
inserts a space and the pattern required a digit right after the symbol, so it
matched NOTHING on such a page
- `$49,` in prose kept the comma, so the same figure compared unequal against its
table rendering and the guard reported a FALSE drop
- `19,99 €`, the German/French suffix form on exactly the tarif/preise pages the
title path targets, never matched at all
- `$1.1 billion` matched as `$1`, silently changing the figure
`tarif` had no word boundary, so English "tariffs" matched and handed news articles
the low arming floor the design reserves for pricing pages — and tariff coverage is
the genre that scatters real dollar figures through prose.
Guard thresholds are now link-invariant: they are absolute character counts, but
scrape passes the caller's include_links through, and link syntax inflates markdown
~70% — enough for a failing page to go silent because the caller asked for links.
html_to_text is charset-aware instead of utf-8-only. This was pinned as a cosmetic
`raw` limitation, which understated it: this function produces the page_text that
ARMS the price/stub/gated guards, so a latin-1 pricing page had its £ figures
replaced before counting and no guard could arm.
fresh=True now evicts a poisoned row — replace_poisoned was only reachable through
the cache-read branch fresh skips, so the caller got good content while the cache
kept the wall for the rest of the TTL and the next scrape replayed it.
CALIBRATION: replaying all 194 cached pages gives ZERO guard-verdict changes, so
nothing above widened or narrowed what fires on the real corpus. Distinct figures
went 363 -> 377, every delta verified legitimate.
NOT FIXED, deliberately: the collapsed-column guard still has no real-page coverage.
A replacement signal was built and measured and REJECTED — it separates the fixtures
(quo 19, heyrosie 1) then fires just as hard on federal prose (FAR 52.227-20 19,
artificialintelligenceact.eu 16, NASA SBIR 13). No threshold separates quo from a
FAR page, so it would trade one missed collapse for routine false warnings. The
numbers are recorded next to _COLLAPSE_RUN_LEN so it is not re-proposed.
Gate: ruff, mypy, 275 tests, consistency all green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wynelson94
force-pushed
the
fix/guard-coverage
branch
from
August 11, 2026 19:50
acabcff to
520849a
Compare
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.
Every defect here fails silently, which is the class that actually costs research: the tool returns something that looks clean while the guard that should have warned could not arm.
crawlhad no guards at allIt imported
extract_contentalone — nolooks_blocked, noassess_extraction. A consent banner or bot wall was written to disk as a numbered page file, cached, and listed inINDEX.mdwith an ordinary token count. The failing test output said it plainly:004-just-a-moment.md ~18tok Just a moment.Walls are now refused — never written, never cached, links never followed — and reported as
refused: <path> (<reason>)with a count in the summary. Page warnings ride in the index too. This matters more here than anywhere else: crawl output is read back off disk later instead of re-fetched, so a wall written once is a wall trusted for weeks.extracthad the same hole and wrote to the samepages.htmlrowscrapereads, so one call could poison both verbs — while returning the wall's own structured data, usually"no structured data found", which reads as this page has none rather than we never saw the page.Money matching had four blind spots
Each disarmed the dropped-price guard on the exact page class it exists for. Figures are now canonicalised symbol-first and space-free via
money_figures(), because$ 49and$49are one price:<span>$</span><span>49</span>$49$49,in prose$49,≠$49→ false drop warning$4919,99 €(German/French suffix)€19,99$1.1 billion$1— silently wrong figure$1.1The first is ordinary pricing-grid markup; the third appears on exactly the
tarif/preisepages the title-arming path deliberately targets.Three more silent disarms
tarifmatched English "tariff"/"tariffs" — no word boundary — handing news articles the low arming floor the design reserves for pricing pages. Tariff coverage is precisely the genre that scatters real dollar figures through prose.scrapepasses the caller'sinclude_linksthrough and link syntax inflates markdown ~70% — enough for a failing page to go silent because the caller asked for links.html_to_textis charset-aware. This was pinned as a cosmeticrawlimitation, which understated it: this function produces thepage_textthat arms the price/stub/gated guards, so a latin-1 pricing page had its£figures replaced before counting and no guard could arm.fresh=Truenow evicts a poisoned row —replace_poisonedwas only reachable through the cache-read branchfreshskips, so the caller got good content while the cache kept the wall for the rest of the TTL.Calibration
Replaying all 194 cached pages: zero guard-verdict changes. Nothing here widened or narrowed what fires on the real corpus. Corpus distinct figures went 363 → 377, every delta verified legitimate (split-markup figures recovered,
$1.1no longer truncated, trailing-comma duplicates correctly merging).Deliberately NOT fixed
The collapsed-column guard still has no real-page coverage. I built a replacement signal (detect the deduplication itself) and measured and rejected it — it separates the fixtures cleanly (quo 19, heyrosie 1, article_peripheral 0) then fires just as hard on ordinary federal prose:
No threshold separates quo from a FAR page; at ≥10 it fires on 20 of 194, mostly the federal corpus this tool is used for. That trades one missed collapse for routine false warnings — the trade that destroys the value of every other warning. Same reasoning that rejected the bare yield ratio in v0.1.2. The numbers live next to
_COLLAPSE_RUN_LENso it isn't re-proposed, and a test documents the gap.Verification
ruff·mypy· 275 tests (+23) ·check_consistency.py— all green. Every fix was written test-first; the 194-page replay is the calibration guard.🤖 Generated with Claude Code