Detect (and optionally repair) extraction schemas that no longer fit the page - #2106
Open
AliaksandrNazaruk wants to merge 1 commit into
Open
Detect (and optionally repair) extraction schemas that no longer fit the page#2106AliaksandrNazaruk wants to merge 1 commit into
AliaksandrNazaruk wants to merge 1 commit into
Conversation
A generated schema is validated once, against the snapshot it was generated from, and then applied for months while the site keeps changing. When a selector stops matching, extraction does not fail: it returns fewer records, or records with a silently empty column, and the caller is told nothing. The second shape is the dangerous one - the record count is unchanged, so nothing downstream can see it. The diagnostic for this already exists in the library (_validate_schema); it just never runs after generation. This adds serve-time health scoring: health_threshold score every extraction, report anything below it on_degraded callback for the report; defaults to a logger warning heal_llm_config optional: regenerate the schema from the page that degraded it max_regenerations bound on that (default 1) The score is computed from the records already extracted - no second pass over the DOM and no model call, so the check costs nothing. Off by default: without health_threshold the behaviour is byte-identical to before, which is pinned by a test. Regeneration is deliberately conservative. The replacement is scored by the same measure that raised the alarm and adopted only if it beats it, because 'the selectors match something' is not the same as 'this extractor is healthy again' - and the current schema's field names are carried into the request, since everything downstream is keyed on them. A failed repair keeps the old schema and never breaks the crawl. 12 tests: both rot shapes, threshold behaviour, the off-by-default guarantee, and the regeneration policy with the generator stubbed (adopt / refuse / bound / provider failure). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
The problem
generate_schemavalidates a candidate against the HTML it was generated from, refines itif needed, and returns it. From then on the schema is applied to thousands of pages over
months while the site keeps changing — and nothing checks it again.
When a selector stops matching, extraction doesn't fail. It returns:
record count is unchanged, so nothing downstream can see it. This is the dangerous one.
Reproduced on a real page (
books.toscrape.com, 20 products), with two ordinary CSS classrenames:
extract()reports successWhat this adds
Serve-time health scoring, off by default:
Same two broken pages, with
health_threshold=0.9:Cost: none. The score is computed from the records
extract()already produced — nosecond pass over the DOM, no model call. I deliberately did not reuse
_validate_schemahere, since it re-runs extraction internally and would double the work on every page.
Compatibility: without
health_thresholdnothing changes — same return values, samesilence, zero overhead. There's an explicit test for that.
Regeneration
Optional and conservative:
max_regenerationstimes per strategy(default 1) — a permanently unreadable site must not bill a model call per page;
downstream is keyed on them: a repair that renames a column is not a repair;
it beats it. "The selectors match something" is not "this extractor is healthy again",
and adopting a replacement on the generator's own opinion is how a stale extractor
becomes a differently-stale one;
breaks the crawl.
End-to-end on the real page, generator stubbed:
coverage 67% -> 100%, 20 records withprices restored, and the next page is served by the healed schema at zero cost.
Tests
tests/test_schema_health.py, 12 tests, no network and no API key:replacement that is no better, carries field names, stays bounded across 5 pages,
survives a provider failure.
12 passed. Existing tests touching this class (test_generate_schema_usage.py,docker/test_serialization.py) still pass — 31 total. One unrelated file(
docker/test_config_object.py) fails to collect onmainas well (FastFilterChainimport), verified with
git stash.Notes
Happy to split this: the detection half stands on its own and is the part with no policy
decisions in it. If you'd rather own the regeneration behaviour yourselves, I can drop it
from this PR and leave the health report plus the callback.
🤖 Generated with Claude Code