Implements CLAUDE.md Build Order Phase 2 — same scraping/extraction
logic as the legacy app.py, reorganized into the three-layer structure
and upgraded per §10's explicit instructions:
- core/scraper.py — playwright-stealth replaces the manual
add_init_script stealth block; smart proxy (Webshare→Bright Data)
wired via core/proxy_service.py with block-detection + auto-escalation;
check_robots_txt() gate added per §30
- core/extractor.py — OpenRouter model cycling, merging the legacy
JSON-repair fallback chain with the spec's env-driven free/paid
model list and Sentry breadcrumb on paid-fallback use
- core/ai_fetch.py — AI web fetch fallback, ported unchanged
- industries/adventure_travel/{fields,prompt}.py ported unchanged;
schema.py added as the future V2 custom-fields attachment point
- config/client.py — gspread Sheet write-back, ported unchanged
- services/analysis_service.py now sources CACHE_STALENESS_HOURS from
core/scraper.py, its canonical location per §7
Dropped the legacy's commented-out Groq/Gemini alternates — OpenRouter
is the sole active provider and the dead code added nothing.
requirements.txt: relaxed playwright to >=1.55.0 and pinned
playwright-stealth==1.0.6 + setuptools<81 — Python 3.14 has no
prebuilt wheels for the originally-listed pins, and playwright-stealth
2.x replaced the stealth_async(page) API §10 documents verbatim with a
class-based one.
192 tests, 94% coverage. Also fixes a real bug surfaced by this phase:
several test helpers mutated core.extractor / industries.adventure_travel.prompt
via raw setattr instead of monkeypatch.setattr, silently corrupting
shared module state across tests once those modules became real.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>