feat(extract): Phase 18 C5 — LLM extraction assist under the substrate rule#189
Merged
Merged
Conversation
…e rule The last built-able C5 slice (COMPLEX_CONTENT_DESIGN.md §6): scans stop being a dead end, scrambled layouts get a reconstruction path, and the substrate rule is mechanized rather than promised. - shared/llm-extract.js — dual-substrate re-grounding. structure mode: every model span is a SEARCH KEY grounded via quote-grounding and either re-canonicalized to substrate bytes or dropped and counted (unverified_spans). Table structure is the model's legitimate contribution but every cell re-grounds; tables need >=1 grounded substantive (worded) cell and a non-fabricated majority to survive. transcription mode (scans): spans kept as authored — honesty comes from extraction.method 'llm:<model>' + the reader banner, per §6.2. - shared/llm-extract-prompts.js — forced-tool span schema, two system prompts, the caps. Page-range chunking is a DOCUMENTED DEFERRAL (the API has no page-range parameter; splitting PDFs needs a PDF library in the SW) — over-cap documents refuse clearly instead. - llm-client runExtractPass + the xray:llm:extract SW message — same consent gates as every pass; bytes read from source_documents by hash, never sent over runtime messaging; 300s bound; returns RAW spans so the reader applies the honesty mechanism where the substrate lives. - reader: the scan refusal is a typed error feeding a consent flow (Escape declines, focus on Cancel, dialog semantics; gates and caps checked BEFORE consent); completeScanCapture mirrors the deterministic article shape; "Reconstruct with LLM…" offered in the extraction-warnings banner only where deterministic extraction says it struggled; persistent provenance banners for BOTH llm: and +llm: methods, re-rendered on Load archive. WIRE-FORMAT ADDITION (additive, called out per CLAUDE.md): extraction-method + source-hash tags on 30023 when the capture carries an extraction record (§6.3/§7 — "so consumers can distinguish 'deterministic text layer' from 'model-transcribed' forever"), read back as a thin extraction record and MERGED with the session's rich one by source_hash on Load archive. No existing tag moves; unknown tags are skipped by existing consumers. An adversarial review of the implemented diff (4 dimensions, two skeptics per finding) confirmed 17 findings; all are fixed here, the headline being the REVERSED-TABLE ATTACK: substring grounding let a fabricated mortality table with inverted year/death associations report "all spans verified" — short keys are now token-bounded, tables need substantive grounded cells, and the un-checkable residue (row/column association against a deliberately-scrambled substrate) is DISCLOSED persistently instead of falsely claimed verified. Also fixed: base64 inflation made the old 30MB cap produce ~40MB requests against the 32MB API limit (now 22MB raw); stale pageMap stamped wrong page numbers into claim anchors after reconstruction (deleted when unmapped); pending user edits were silently clobbered (refused, and re-checked after the flight); method chaining on re-runs; stale audit keys; unbannered transcribed copies after Load archive; the thin wire read-back replacing rich local extraction records. 1764 tests green (45 new); lint 0 errors. Deliberately NOT built: dist/ is untouched so the maintainer's loaded extension is unaffected until they rebuild. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…carries BOTH _publishedDraft and the extraction read-back; both JOURNAL entries
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 last built-able Phase 18 slice (
COMPLEX_CONTENT_DESIGN.md§6). Built rebuild-free like #188 —dist/untouched, nothing live until you rebuild; CI validates the bundle.What you get
Scans stop being a dead end. A no-text-layer PDF now offers consent-gated LLM transcription instead of the old refusal. The transcription IS the capture — honestly labeled:
extraction.method = 'llm:<model>', a persistent 🤖 banner, the original bytes archived one click away. Consent states plainly the document leaves the device (size + pages shown), Escape declines, and every refusable condition — gates, size cap, page cap, archive space — is checked before the consent modal, because a consent that ends in a foreseeable refusal is a wasted consent.Scrambled layouts get "Reconstruct with LLM…" — offered only where deterministic extraction itself says it struggled (the warnings banner). The substrate rule, mechanized: every model span is a search key, re-grounded via
quote-groundingand either re-canonicalized to the document's own bytes or dropped and counted. The stored capture never contains model-authored body text.Wire addition (additive, per CLAUDE.md's call-out rule):
extraction-method+source-hashtags on 30023 (§6.3/§7) — so consumers can distinguish "deterministic text layer" from "model-transcribed" forever. Read back as a thin record, merged with the session's rich one bysource_hash. No existing tag moves.Documented deferral: page-range chunking. The API has no page-range parameter and splitting PDFs needs a PDF library in the SW — over-cap documents (100 pages / 22MB) refuse with a clear pre-consent error instead.
The reversed-table attack
The adversarial review of the implemented diff (4 dimensions → 18 findings → two skeptics each → 17 confirmed unanimous, all fixed) found something that goes to the heart of the tool:
quote-grounding's exact/normalized tiers are substring searches, so'9'grounded inside'COVID-19','201'inside'2017','12'inside'12%'— and the repro assembled a reversed mortality table (2020→7, 2021→41 against a document saying the opposite) that reported "all spans verified against the document text." A fabricated data table would have entered your corpus stamped verified.Three changes, and the third is the load-bearing one:
+llm:in the method, which round-trips the wire and therefore banners relay copies too) say plainly: text re-grounds to document bytes; table structure is the model's reading — verify against the archived original.The other 14 confirmed findings, all fixed
…+llm:a+llm:b(a malformed wire value);extractionMethodis now idempotent over the deterministic base, and the post-success banner has no second button anyway (the body is no longer the substrate).auditableHash), unbannered transcribed copies after Load archive, thin wire read-back replacing rich local records, ignoredstored:falsefrom the archive, modal a11y (Escape/focus/dialog).Accepted residue, documented in JOURNAL: Cancel on the scans path still lands on the ?pdf= error screen (scans dead-ended there pre-C5 too), and a plausible table whose labels genuinely appear in the text can still carry wrong associations — which is exactly what the persistent banner is for.
Verification
npm test— 1764 pass, 0 fail (45 new: 33 re-grounding engine incl. the reversed-table repros, 12 wire/prompts/client)node --checkon every touched file ·npm run buildNOT run (your live extension is untouched)main). Note for merge order: this touchesreader/index.jsregions distinct from the archive stack's, but merging fix(archive): "Load archive" served the wrong article — gate on identity #185–fix(archive): reload must not drift the x tag — carry the preimage, prove it #187 first is cleaner.🤖 Generated with Claude Code