You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Hey Nelson, took the four PRs through local verification end to end on
my side, including a live update run against data/minimax-audio.json
so the audit + update story actually got exercised (about $0.06 of
OpenRouter spend total). Putting everything in one issue because the
cleanest path forward is one consolidated PR rather than four parallel
ones with overlapping cli.py edits and a partial dependency chain
between them. Details below.
fix(db): make insert_documentation an upsert (closes #48) #52 upsert flow with re seed twice: same doc_id, created_at preserved, updated_at advances, FK CASCADE actually fires (the PRAGMA foreign_keys = ON per connection is what makes it real), FTS5 'delete' protocol runs before the sections delete so no orphaned index entries, embeddings flush after commit so a rollback cannot desync the numpy file from the DB. Solid.
fix(scraper): one chunk failure no longer aborts enrich batch (#47) #53 enrich resilience with a real non transient ProviderError on one chunk in a five chunk batch: 4 of 5 enriched, 1 dropped, batch did not abort. Schema fallback success does NOT write to the primary cache (verified by patching enrich_cache.put and watching call count stay at 0 when only the fallback succeeded). PROMPT_VERSION derives from sha256(ENRICHMENT_PROMPT)[:16], so any prompt edit invalidates cache automatically. Best PR in the group, no concerns.
feat(scraper): SCRAPE_CACHE_MODE and --no-fetch-cache for crawl4ai (#50) #54 cache bypass: env precedence works (existing SCRAPE_CACHE_MODE wins over flag via setdefault), the try/finally block restores or pops correctly, _resolve_cache_mode maps bypass / disabled / read_only / write_only (case insensitive) and warns once on garbage. Honoured by crawl4ai, firecrawl correctly ignores it.
Add king-scrape update for incremental corpus refresh (ADR-0014) #51 update: reuse by content_hash works (296 of 297 chunks reused on a re run with no upstream changes, zero LLM call on those), staged drift on one corrupted _meta.content_hash correctly triggered exactly one re enrich (reused 296, enriched 1, lost 0), atomic write leaves no .tmp files, _meta is back filled on legacy corpora, CLI error paths are clear.
Worth knowing: _generate_and_save_embedding appends to embeddings.npy and never prunes entries from _section_id_to_idx when a section id is replaced by a re seed. Not a regression you introduced, the pre upsert path had the same characteristic. But this PR unlocks the "re seed often" workflow, so the bloat becomes visible. Worth a follow up issue, not a blocker.
One thing worth flagging in the PR description: this PR is a hard dependency of #51's UpdateReport.lost contract. Without #53, a single non transient ProviderError on any chunk during king-scrape update would have crashed the process mid pipeline instead of being counted as lost. The "lost 0" assertion my e2e validates on #51 only holds in a world where #53 is merged. Worth a sentence in the PR description so the reviewer of #51 knows.
--no-fetch-cache only exists on the main parser. king-scrape update foo --no-fetch-cache and king-scrape audit foo --no-fetch-cache both die with unrecognized argument because update_main (Add king-scrape update for incremental corpus refresh (ADR-0014) #51) and audit_main (Add king-scrape audit subcommand for corpus drift #49) have their own argparsers. The workaround is to set SCRAPE_CACHE_MODE=bypass directly, which works, but is not discoverable. Update is the command that most needs cache bypass (force refresh is the point), so the flag should plumb into all three entry points.
Worth knowing: guaranteed merge conflict in cli.py main() with #49 and #51 since all three touch the dispatcher. Mechanical resolve, but worth knowing before you rebase.
base_url fallback on legacy corpora wipes the curated set and spends LLM credits silently. Repro: data/minimax-audio.json (legacy, no _meta, base_url: https://platform.minimax.io/docs). The original had 12 curated section URLs. After king-scrape update minimax-audio --yes: discover found 116 URLs, filter accepted 113, chunk produced 360 chunks, zero reused, all original enrichment lost, $0.06 of OpenRouter spend with no warning between the cost preview and the writeback. _resolve_source_url falls back to base_url silently in update.py:164, and on legacy corpora base_url is the host root, not the original entry point. Suggested fix: detect if not corpus.get("_meta") and require --allow-legacy-fallback explicitly, or refuse to proceed if the URL count diff against the existing corpus exceeds some threshold (e.g. >2x growth aborts).
Fetch failures silently truncate the corpus. On my e2e Run 2 and Run 3, fetch reported fetched 100, failed 14 on stdout. The UpdateReport has no fetch_failed field, the writeback proceeds anyway, and the corpus shrank from 360 to 297 sections between runs without anything obvious in the report. The existing "refuse to wipe a non empty corpus to empty" guard at update.py:317 only fires on a totally empty fetch; partial fetch failures pass through. Suggested fix: add fetch_failed: int to UpdateReport, surface it in the summary line, and abort the writeback if the failure ratio crosses a threshold (e.g. >10%).
removed_urls and added_urls counts are unreliable after the first update. They compare corpus_urls - fresh_urls, but once update has written through the existing pipeline the corpus carries slug form URLs (platform-minimax-io-docs-...) while fresh_urls is real https://.... Run 2 reported removed 106, added 114 when the real disk diff was 9 URLs lost and 2 new. Root cause is the pre existing chunk_pages bug below, but this PR inherits the noise. Suggested fix: either canonicalise both sides before diffing, or suppress the counters until the root cause is addressed.
Cost confirmation prompt fires even when there are zero new chunks (i.e. cost is $0.0000). I verified by calling _confirm_cost(plan, cfg, yes=False) with new_chunks=[] and input() is still invoked. The if plan.new_chunks check at update.py:333 is after the prompt at line 328. Suggested fix: short circuit the prompt when not plan.new_chunks, or move the prompt to inside the if plan.new_chunks: branch.
Discover is non deterministic. Three consecutive runs returned 116, 117, 117 URLs. Partly upstream's fault but the update flow has no stability gate against transient discovery drift. Suggested fix: if the fresh URL count diverges from the existing corpus by more than X%, abort with a clear error pointing at investigation.
ADR-0014 has two factual drifts with the code as merged. Line 65 says "Atomic write via tempfile + replace is a future hardening" and references save_and_index, but update.py:135 already implements _atomic_write_json and update.py:360 calls it (the actual export path is export_to_json + _atomic_write_json, not save_and_index). One paragraph rewrite to keep the ADR honest.
Worth knowing (not from your PR, but surfaced in my e2e):
The audit subcommand (Add king-scrape audit subcommand for corpus drift #49) is broken on main: audit.py:242 calls resolve_provider_name(stage, explicit=...) but the live scraper_providers API is resolve_provider_name(stage) with no explicit kwarg. Blocks the audit + update demo as a pair. Worth a small fixup commit, either on top of Add king-scrape audit subcommand for corpus drift #49 or as part of the unified PR below.
chunk_pages at chunk.py:202 uses md_file.stem as the chunk source_url instead of reading the real URL from the .meta.json sidecar that fetch_pages writes alongside each page. This is the root cause of the slug form URLs in the corpus and the reason concern (3) on Add king-scrape update for incremental corpus refresh (ADR-0014) #51 surfaces. Pre existing since Feb (3793aba), affects every corpus produced by the current pipeline (hono-test.json, httpx.json both show this). Worth fixing in the unified PR if you scope it that way.
Suggested unification
Rather than juggle four parallel PRs with cli.py conflicts and the #53 to #51 dependency, my suggestion is one consolidated PR that lands in this order:
The two small pre existing fixes: audit.py:242resolve_provider_name signature, and the chunk_pages slug bug if you want to clear concern (3) on update.
CHANGELOG entries fold into one section. ADR-0014 gets the paragraph fix. The cli.py main() ends up with one clean dispatcher block covering audit, update, and the cache env handling.
If you prefer to keep them separate, the dependency order to avoid mid pipeline crashes is: #52, then #53, then #49 fix + #51 + #54 together (or strictly ordered). Either way #51 should not merge without the six items above.
Happy to run the e2e again on whatever you ship, and to push the small fixups myself if it saves you time. Let me know which shape you want.
Hey Nelson, took the four PRs through local verification end to end on
my side, including a live update run against
data/minimax-audio.jsonso the audit + update story actually got exercised (about $0.06 of
OpenRouter spend total). Putting everything in one issue because the
cleanest path forward is one consolidated PR rather than four parallel
ones with overlapping
cli.pyedits and a partial dependency chainbetween them. Details below.
What I verified
created_atpreserved,updated_atadvances, FK CASCADE actually fires (thePRAGMA foreign_keys = ONper connection is what makes it real), FTS5 'delete' protocol runs before the sections delete so no orphaned index entries, embeddings flush after commit so a rollback cannot desync the numpy file from the DB. Solid.enrich_cache.putand watching call count stay at 0 when only the fallback succeeded).PROMPT_VERSIONderives fromsha256(ENRICHMENT_PROMPT)[:16], so any prompt edit invalidates cache automatically. Best PR in the group, no concerns.SCRAPE_CACHE_MODEwins over flag viasetdefault), thetry/finallyblock restores or pops correctly,_resolve_cache_modemapsbypass / disabled / read_only / write_only(case insensitive) and warns once on garbage. Honoured by crawl4ai, firecrawl correctly ignores it.content_hashworks (296 of 297 chunks reused on a re run with no upstream changes, zero LLM call on those), staged drift on one corrupted_meta.content_hashcorrectly triggered exactly one re enrich (reused 296, enriched 1, lost 0), atomic write leaves no.tmpfiles,_metais back filled on legacy corpora, CLI error paths are clear.Per PR punch list
#52 (db upsert)
Blocking: none.
Worth knowing:
_generate_and_save_embeddingappends toembeddings.npyand never prunes entries from_section_id_to_idxwhen a section id is replaced by a re seed. Not a regression you introduced, the pre upsert path had the same characteristic. But this PR unlocks the "re seed often" workflow, so the bloat becomes visible. Worth a follow up issue, not a blocker.#53 (enrich batch resilience)
Blocking: none. Nothing to change.
One thing worth flagging in the PR description: this PR is a hard dependency of #51's
UpdateReport.lostcontract. Without #53, a single non transient ProviderError on any chunk duringking-scrape updatewould have crashed the process mid pipeline instead of being counted aslost. The "lost 0" assertion my e2e validates on #51 only holds in a world where #53 is merged. Worth a sentence in the PR description so the reviewer of #51 knows.#54 (cache bypass)
Blocking:
--no-fetch-cacheonly exists on the main parser.king-scrape update foo --no-fetch-cacheandking-scrape audit foo --no-fetch-cacheboth die withunrecognized argumentbecauseupdate_main(Add king-scrape update for incremental corpus refresh (ADR-0014) #51) andaudit_main(Add king-scrape audit subcommand for corpus drift #49) have their own argparsers. The workaround is to setSCRAPE_CACHE_MODE=bypassdirectly, which works, but is not discoverable. Update is the command that most needs cache bypass (force refresh is the point), so the flag should plumb into all three entry points.Worth knowing: guaranteed merge conflict in
cli.py main()with #49 and #51 since all three touch the dispatcher. Mechanical resolve, but worth knowing before you rebase.#51 (king scrape update)
Blocking:
base_url fallback on legacy corpora wipes the curated set and spends LLM credits silently. Repro:
data/minimax-audio.json(legacy, no_meta,base_url: https://platform.minimax.io/docs). The original had 12 curated section URLs. Afterking-scrape update minimax-audio --yes: discover found 116 URLs, filter accepted 113, chunk produced 360 chunks, zero reused, all original enrichment lost, $0.06 of OpenRouter spend with no warning between the cost preview and the writeback._resolve_source_urlfalls back tobase_urlsilently inupdate.py:164, and on legacy corporabase_urlis the host root, not the original entry point. Suggested fix: detectif not corpus.get("_meta")and require--allow-legacy-fallbackexplicitly, or refuse to proceed if the URL count diff against the existing corpus exceeds some threshold (e.g. >2x growth aborts).Fetch failures silently truncate the corpus. On my e2e Run 2 and Run 3, fetch reported
fetched 100, failed 14on stdout. TheUpdateReporthas nofetch_failedfield, the writeback proceeds anyway, and the corpus shrank from 360 to 297 sections between runs without anything obvious in the report. The existing "refuse to wipe a non empty corpus to empty" guard atupdate.py:317only fires on a totally empty fetch; partial fetch failures pass through. Suggested fix: addfetch_failed: inttoUpdateReport, surface it in the summary line, and abort the writeback if the failure ratio crosses a threshold (e.g. >10%).removed_urlsandadded_urlscounts are unreliable after the first update. They comparecorpus_urls - fresh_urls, but onceupdatehas written through the existing pipeline the corpus carries slug form URLs (platform-minimax-io-docs-...) whilefresh_urlsis realhttps://.... Run 2 reportedremoved 106, added 114when the real disk diff was 9 URLs lost and 2 new. Root cause is the pre existingchunk_pagesbug below, but this PR inherits the noise. Suggested fix: either canonicalise both sides before diffing, or suppress the counters until the root cause is addressed.Cost confirmation prompt fires even when there are zero new chunks (i.e. cost is $0.0000). I verified by calling
_confirm_cost(plan, cfg, yes=False)withnew_chunks=[]andinput()is still invoked. Theif plan.new_chunkscheck atupdate.py:333is after the prompt at line 328. Suggested fix: short circuit the prompt whennot plan.new_chunks, or move the prompt to inside theif plan.new_chunks:branch.Discover is non deterministic. Three consecutive runs returned 116, 117, 117 URLs. Partly upstream's fault but the update flow has no stability gate against transient discovery drift. Suggested fix: if the fresh URL count diverges from the existing corpus by more than X%, abort with a clear error pointing at investigation.
ADR-0014 has two factual drifts with the code as merged. Line 65 says "Atomic write via tempfile + replace is a future hardening" and references
save_and_index, butupdate.py:135already implements_atomic_write_jsonandupdate.py:360calls it (the actual export path isexport_to_json + _atomic_write_json, notsave_and_index). One paragraph rewrite to keep the ADR honest.Worth knowing (not from your PR, but surfaced in my e2e):
auditsubcommand (Add king-scrape audit subcommand for corpus drift #49) is broken onmain:audit.py:242callsresolve_provider_name(stage, explicit=...)but the livescraper_providersAPI isresolve_provider_name(stage)with noexplicitkwarg. Blocks the audit + update demo as a pair. Worth a small fixup commit, either on top of Add king-scrape audit subcommand for corpus drift #49 or as part of the unified PR below.chunk_pagesatchunk.py:202usesmd_file.stemas the chunksource_urlinstead of reading the real URL from the.meta.jsonsidecar thatfetch_pageswrites alongside each page. This is the root cause of the slug form URLs in the corpus and the reason concern (3) on Add king-scrape update for incremental corpus refresh (ADR-0014) #51 surfaces. Pre existing since Feb (3793aba), affects every corpus produced by the current pipeline (hono-test.json,httpx.jsonboth show this). Worth fixing in the unified PR if you scope it that way.Suggested unification
Rather than juggle four parallel PRs with cli.py conflicts and the #53 to #51 dependency, my suggestion is one consolidated PR that lands in this order:
audit.py:242resolve_provider_namesignature, and thechunk_pagesslug bug if you want to clear concern (3) on update.update_mainandaudit_mainso it actually works on the subcommands.CHANGELOG entries fold into one section. ADR-0014 gets the paragraph fix. The cli.py main() ends up with one clean dispatcher block covering audit, update, and the cache env handling.
If you prefer to keep them separate, the dependency order to avoid mid pipeline crashes is: #52, then #53, then #49 fix + #51 + #54 together (or strictly ordered). Either way #51 should not merge without the six items above.
Happy to run the e2e again on whatever you ship, and to push the small fixups myself if it saves you time. Let me know which shape you want.