/// // // Adds `scraped_url` to `scrape_runs` โ€” fixes a real bug where content-hash // change detection was scoped only by competitor_id, not by which specific // page was actually scraped. Once trip-intent matching (CLAUDE.md ยง8) let a // PM scrape different trip pages for the same competitor across different // analysis runs, get_last_hash_for_url(competitor_id) compared each new // scrape's hash against whatever page happened to be scraped MOST // RECENTLY for that competitor โ€” almost always a different page โ€” so // change_detected kept getting set back to True on nearly every run, and // needs_refresh() (which gates the fast path on change_detected being // False) never let the true cache-only fast path trigger in practice. // Confirmed directly against real scrape_runs history: the same // competitor's recorded content_hash changed on 5 of 6 consecutive scrapes // over one test session, even though re-scraping the exact same URL twice // in immediate succession produced an identical hash both times. // // See services/analysis_service.py's detect_change()/needs_refresh()/ // run_competitor_analysis() and repositories/scrape_repository.py's // get_last_hash_for_url() for the corresponding code fix. migrate((app) => { const collection = app.findCollectionByNameOrId('scrape_runs'); collection.fields.add(new Field({ type: 'text', name: 'scraped_url' })); collection.indexes = [ ...collection.indexes, 'CREATE INDEX idx_scrape_runs_url_hash ON scrape_runs (competitor_id, scraped_url, content_hash, started_at)', ]; return app.save(collection); }, (app) => { const collection = app.findCollectionByNameOrId('scrape_runs'); collection.fields.removeByName('scraped_url'); collection.indexes = collection.indexes.filter((idx) => !idx.includes('idx_scrape_runs_url_hash')); return app.save(collection); });