Conversation
…nt_pages table + matter pages CLI (RFC 0004); honest per-page failure for scanned/LZW/CID PDFs; 5 new tests, 93/93 green
… document_blocks table (RFC 0005); zero fabricated pagination; 57 new tests, 150/150 green DOCX carries no rendered page numbers — pagination is produced by the renderer at layout time, not stored in the file — so locators here are structural, and a page number would be a citation this tool invented. Locator contract (RFC 0005 §2, also in the extract.py module docstring): a citation is (document_sha256, part, locator), where locator is a path of integer coordinates through the OOXML block tree, prefixes cycling b/r/c: word/document.xml#b3 4th block child of the body word/document.xml#b3/r1/c0/b2 3rd paragraph of row 1, cell 0 of that table Paths are tree coordinates, so two blocks cannot collide; identical text in two places is still two distinct citations. Every path has length 3n+1 and the addressed unit is always a paragraph. Character offsets (not run indexes, which Word re-splits on any edit) give doc.text[char_start:char_end] == block.text. Zero fabricated pagination, enforced in four places rather than by convention: extract_docx returns pages == [] always; document_blocks has no page_number column and CHECKs locator_scheme to 'docx-structural'; record_blocks raises on any other scheme; matter pages writes no document_pages row for a .docx. Storage is a sibling table, not a locator_scheme column on document_pages, so the invariant is structural rather than a rule someone must remember. Purely additive: CREATE TABLE IF NOT EXISTS on next connect, no backfill, nothing in document_pages read or rewritten. Honest failure, never a silent empty: not-a-zip (magic checked before zipfile, so a renamed .txt/.pdf and an appended-zip polyglot are refused, not re-dispatched), truncated archive, CRC mismatch, encrypted member, zip bomb (declared size and bounded read both capped), missing/duplicate/blank word/document.xml, DOCTYPE (ElementTree expands internal entities and the C parser exposes no expat handle; also guarded behind a UTF-8 BOM), non-UTF-8 part, malformed XML with no partial salvage, strict-OOXML namespace, wrong root, missing w:body. A well-formed empty document is the distinct case 'extracted, zero blocks' and records an explicit row, so the store can tell "never extracted" from "extracted, nothing in it" from "could not be read". w:delText (tracked deletions) and w:instrText (field codes) are never extracted; w:pPr is skipped whole so tab-stop definitions don't inject tabs. PartInventory records seen / extracted / skipped (headers, footers, footnotes, endnotes, comments) / unhandled (e.g. w:sdt), so a reviewer can tell "skipped by design" from "missed". Stdlib only — zipfile + xml.etree, no python-docx, no lxml; asserted by a subprocess import with site-packages stripped from sys.path, not by a comment. Sources stay read-only; a test re-hashes after extraction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kz5aom58RiiCEzewHdvUec
…ction A library function committing mid-transaction steals rollback control from its caller. The CLI (matter.py) already commits after the extraction loop. Adds a regression test proving a caller-side rollback discards record_blocks' writes.
…open aggregate ACCEPTANCE LIMIT (RFC 0005 §4a) MAX_ARCHIVE_MEMBERS = 10,000 is enforced PRE-OPEN, straight off the EOCD / ZIP64 tail records, before zipfile.ZipFile is constructed — ZipFile.__init__ materialises one ZipInfo per entry before any in-process check could run, so a namelist() check would be no guard at all. A size_limit:members refusal constructs no ZipFile; the tests prove it with a spy on zipfile.ZipFile as seen by matterkit.extract (zero constructions on every members path, >=1 on every aggregate path — the labels cannot be swapped silently). MAX_ARCHIVE_UNCOMPRESSED_BYTES = 256 MiB is the ACCEPTANCE LIMIT, not a guard: per-member declared sizes live in central-directory entries and no tail record sums them, so the checked sum runs post-open in _extract_docx_open before any part read. The distinction is documented at both the constants and the call sites. Pre-open reader: <=65,577 tail bytes, right-to-left EOCD scan, accepted only if the comment length puts the record end exactly at EOF; entries at EOCD+10 (2B), cd_size at EOCD+12 (4B). ZIP64 sentinels fire INDEPENDENTLY PER FIELD; a sentinel consults the locator at eocd_start-20, follows its offset to the ZIP64 EOCD record, validates record-size+12 ends at the locator, and reads count at +32 / cd_size at +40. Structural lies refused pre-open as corrupt/truncated: sentinel without locator, locator pointing at non-ZIP64 bytes, record-size arithmetic failure, cd_offset+cd_size past EOF, count*46 > cd_size. Post-open, len(zf.namelist()) is reconciled against the pre-open count — an honesty check, not a resource guard. RFC 0005 gains §4a 'Resource bounds' (approved design note, verbatim). The aggregate acceptance limit is fixtured on both halves of the pair: a non-ZIP64 archive whose 32-bit declared sizes are forged, and a ZIP64 one declaring 5 GiB per member through the 0x0001 extra field, asserted on the exact 64-bit total so a reader summing the saturated sentinels would fail. 14 new tests; 165/165 green under pytest and unittest discover. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVd1Q11u4AhhYbWwpXBDqG
…tests (RFC 0005 §4a) Bound declared ZIP central-directory size at 32 MiB before ZipFile exists, alongside the member-count guard. Variant-4 honest oversized directories are refused as size_limit:central_directory with zero constructor attempts. Downward cd_size lies still reach CPython and fail as corrupt, not the cap. Parser-agreement tests pin honest EOCD and all-sentinel ZIP64 against CPython zipfile; magic-in-comment is fail-closed non-agreement (P3-1).
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.
Extends the RFC 0004 extraction slice from PDF-only to DOCX. Stdlib only (
zipfile+xml.etree); nopython-docx, nolxml.Do not merge — review gate first: @adversarial-review-deepseek-agent on (1) locator collision/uniqueness, (2) every ZIP/XML failure path actually failing, (3) zero fabricated pagination anywhere.
The hard scope point
DOCX carries no rendered page numbers. Pagination is produced by the renderer at layout time from font metrics, page setup, field results and hyphenation — it is not in the file, and
<w:br w:type="page"/>is a hint the content still reflows around. Any page number attached to DOCX content would be one this tool invented.So none is. Enforced in four places rather than by convention:
extract_docxreturnspages == []in every case, success and failurematterkit/extract.pydocument_blockshas nopage_numbercolumn, and CHECKslocator_schemetodocx-structuralmatterkit/store.pyrecord_blocksraises on any other scheme — the block store cannot launder a page locatormatterkit/extract.pymatter pageswrites nodocument_pagesrow for a.docxmatter.pyStorage is a sibling table, not a
locator_schemecolumn ondocument_pages: with a shared table "never put a paragraph index inpage_number" is a rule someone has to keep remembering and one careless INSERT breaks silently; with a separate table there is no column to misuse. Purely additive migration —CREATE TABLE IF NOT EXISTSon nextconnect(), no backfill,document_pagesuntouched.The locator contract
Full text in
rfcs/0005-docx-structural-extraction.md§2 and in theextract.pymodule docstring. A citation is(document_sha256, part, locator):Prefixes cycle
b(block child) /r(row) /c(cell), so every path has length3n+1and the addressed unit is always a paragraph.partseparates them, across documentsdocument_sha256does. PK(document_sha256, part, locator)enforces it and makes re-extraction idempotent.text_sha256and have two different locators. The locator names a position; the hash describes content.doc.text[b.char_start:b.char_end] == b.text. Aw:rboundary is a formatting artefact Word re-splits on any edit, so "run 3 of paragraph 7" names a different substring tomorrow.run_countis kept as a diagnostic and is never part of a citation. (§2d explains the deviation from the brief's sketch.)\nbut is not a delimiter — aw:brrenders as\ntoo, sodoc.textmust not be re-split to recover blocks; the stored offsets are authoritative.Honest failure — never a silent empty
Every path below returns
extraction-failedwith a distinct reason and zero blocks (§3 has the full table with the reason strings):not-a-zip (magic checked before
zipfile, so a renamed.txt/.pdfand an appended-zip polyglot are refused, not re-dispatched) · truncated archive · CRC mismatch · encrypted member · zip bomb (declared size and bounded read both capped) · missing / duplicate / blankword/document.xml· DOCTYPE · non-UTF-8 part · malformed XML with no partial salvage · strict-OOXML namespace · wrong root · missingw:body.Two that came out of adversarial reading rather than the brief, both verified to work against the un-guarded code first:
xml.etree.ElementTreeexpands internal general entities (billion laughs) and the CPython C parser exposes no expat handle to disable it. A conforming OOXML part has no DTD, so it is refused — scanned per the XML prolog grammar so document text containing<!DOCTYPEcan't trip it, and with a UTF-8 BOM stripped first.word/document.xmlentries —zipfileresolves to the last, another reader may take the first; which bytes the citation pins to would be reader-dependent, so there is no honest answer.Three states the store must distinguish, and does: never extracted (no row) · extracted, could not be read (one row, failed,
text IS NULL, reason kept) · extracted, genuinely empty (one row,extracted,container_type='part',text='').What counts as text
w:delText(tracked deletions) andw:instrText(field codes) are never extracted — quoting struck-out or machine-internal text as content misstates the evidence.w:pPris skipped whole, becausew:pPr/w:tabs/w:tabis a tab-stop definition a naive walk would render as a tab that isn't in the document. Element tails are ignored so pretty-printing whitespace never enters the text.PartInventoryrecordsseen/extracted/skipped(headers, footers, footnotes, endnotes, comments) /unhandled(e.g.w:sdt), so "skipped by design" is distinguishable from "missed" —matter pagesprints both.Tests
57 new in
tests/test_extract_docx.py, 150 total green underpython -m unittest discover -s tests(the CI runner) on 3.11 and 3.14. Every fixture is synthesised in-process from XML strings +zipfile— no binary fixtures, no client documents. The stdlib floor is asserted by a subprocess import withsite-packagesstripped fromsys.path, not by a comment. Source immutability is a re-hash after extraction.Brief's list (a)–(g) all covered; §8 maps each to its test class.
Named gaps, documented not hidden (§10)
officeDocumentpackage relationship — a producer naming it otherwise fails loudly instead of being read.w:sdtcontent controls aren't descended into (they appear inunhandled); doing so changes the locator grammar and wants its own review.partper document — the schema already allows it, the extractor doesn't produce it..txt/.mdsit indocument_pagesas "page 1", which is a whole-file locator wearing a page number. Same category of claim; left alone here rather than widened into this RFC.matter-mcp.pyuntouched.🤖 Generated with Claude Code
https://claude.ai/code/session_01Kz5aom58RiiCEzewHdvUec