Skip to content

feat(extract): Phase 18 C5 — LLM extraction assist under the substrate rule#189

Merged
bryanmatthewsimonson merged 2 commits into
mainfrom
claude/phase-18-c5-llm-extract
Jul 18, 2026
Merged

feat(extract): Phase 18 C5 — LLM extraction assist under the substrate rule#189
bryanmatthewsimonson merged 2 commits into
mainfrom
claude/phase-18-c5-llm-extract

Conversation

@bryanmatthewsimonson

Copy link
Copy Markdown
Owner

The last built-able Phase 18 slice (COMPLEX_CONTENT_DESIGN.md §6). Built rebuild-free like #188dist/ 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-grounding and 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-hash tags 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 by source_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:

  1. Token-bounded short keys — a match under 15 chars counts only when its substrate neighbors are non-alphanumeric. Kills the sub-token shards.
  2. Substantive-cell requirement — a table survives only if the substrate vouches for at least one grounded worded cell. A table of number-shaped strings that would match anywhere is the model's table, not the document's.
  3. Disclosure over fake verification. Row/column association is never machine-checkable against a scrambled substrate — that's the feature's own premise (a correct reconstruction legitimately has non-local cells; a locality check would break it). So the toast and a persistent structure-mode banner (+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

  • Base64 inflation: the "30MB" raw cap produced ~40MB requests against the 32MB API limit — guaranteed post-consent rejection. Now 22MB raw, with the math pinned in a test.
  • Stale pageMap: a reconstruction without model page hints kept the OLD map against the NEW markdown — wrong page numbers stamped into published claim anchors. The map is deleted when unmapped; absent beats wrong.
  • Silent edit clobbering: pending edits refuse the flow up front, and the multi-minute flight re-checks on landing — your state wins, the API cost is sunk.
  • Method chaining: re-runs composed …+llm:a+llm:b (a malformed wire value); extractionMethod is now idempotent over the deterministic base, and the post-success banner has no second button anyway (the body is no longer the substrate).
  • Stale audit keys (auditableHash), unbannered transcribed copies after Load archive, thin wire read-back replacing rich local records, ignored stored:false from 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

🤖 Generated with Claude Code

bryanmatthewsimonson and others added 2 commits July 17, 2026 17:54
…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
@bryanmatthewsimonson
bryanmatthewsimonson merged commit 7ea1600 into main Jul 18, 2026
@bryanmatthewsimonson
bryanmatthewsimonson deleted the claude/phase-18-c5-llm-extract branch July 18, 2026 05:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant