Repository navigation
feat(ingest): name each embedded image ingest_document leaves out (#732, slice 1) - #893
Conversation
…, slice 1) ingest_document reduced every embedded image to an anonymous count (image_count / images_skipped), so a caller could not tell which screenshot of which section was lost. The docx and Confluence parsers now inventory each image in document order (DocumentAttachment: ordinal, the reference the markup names — docx relationship id, Confluence attachment filename/URL or img src — the heading of the section it sits in, and its state). Nothing is extracted or preserved yet, so every image is reported "unsupported"; the handler returns the inventory as `attachments` beside `images_skipped`, which is now its length. Refs #732 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018kyZYraH8hqphMRC2MG7Jz
|
Ajouter Vérifié localement avec mutmut 3.8.1 (la version de Generated by Claude Code |
…ventory # Conflicts: # mcp_server/core/document_normalizer.py
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
Security findingsAdvisory findings (2)
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Claude finished @cdeust's task in 3m 38s —— View job Review of PR #893 finished
Summary: one Blocker. Each image record copies the full section heading, and neither the number of images nor the reference length is limited. A document just under the 1 MiB input cap can therefore produce a result of about 20 GB. I traced this by reading the code. I did not reproduce it, because this job wasn't allowed to run Python. There are also three Notes:
Not done here: I didn't run lint, the craftsmanship gate or pytest (permission was denied in this job). The Verdict: block |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0763c5c958
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| for node in image.iter(): | ||
| for attr in (f"{{{_R_NS}}}embed", f"{{{_R_NS}}}id"): | ||
| value = node.get(attr) | ||
| if value: | ||
| return value |
There was a problem hiding this comment.
Select the image relationship instead of any descendant r:id
For a DOCX picture with a clickable hyperlink, wp:docPr/a:hlinkClick can occur before the picture's a:blip within w:drawing. This traversal returns the hyperlink's r:id rather than the image's r:embed, so ingest_document.attachments identifies the hyperlink relationship as the skipped image even though the correct image reference is available. Restrict relationship lookup to a:blip and legacy v:imagedata instead of accepting relationship attributes from arbitrary descendants.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmé : dans wp:inline/wp:anchor, wp:docPr/a:hlinkClick@r:id précède a:blip@r:embed, donc le parcours renvoyait l'id du lien hypertexte pour une image cliquable. Corrigé dans f280f3d : seuls a:blip@r:embed et v:imagedata@r:id sont lus ; test test_a_picture_hyperlink_is_not_taken_for_the_image (plus les cas blip sans r:embed et imagedata sans r:id).
Generated by Claude Code
Review 1 of 3: PR #893 at
|
|
Claude finished @cdeust's task in 2m 4s —— View job PR #893 review (1 of 3) done
The review found no Blockers and the verdict is approve. The full review, including three Notes, is in this comment. The main Note: one image that Word saves twice (a |
Review 1 of 3 — PR #893 (head
|
There was a problem hiding this comment.
🛡️ Codex Security Review · Automatically triggered
Here are some automated security review suggestions for this pull request.
Reviewed commit: f280f3ddd8
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
| "retired_receipt_ids": written["retired_receipt_ids"], | ||
| "images_skipped": normalized.image_count, | ||
| "images_skipped": len(normalized.attachments), | ||
| "attachments": [asdict(a) for a in normalized.attachments], |
There was a problem hiding this comment.
🛡️ Codex Security Review · Automatically triggered
Security: Bound attachment metadata before returning it
When a user ingests an attacker-supplied Confluence export, this emits the full section_heading once per image without a response budget. A 16 KB file with a 10 KB heading and 1,000 <img/> nodes serializes to 10.1 MB; a file below the existing 1 MiB input cap can require over 45 GB before the stdio write, exhausting the Cortex or MCP-client process. Cap the attachment count and serialized size, and truncate or deduplicate headings before returning them. Cortex runs locally at the user's permission level, so the residual impact is local availability rather than a remote service outage.
SECURITY.md reference: SECURITY.md:L15-L20
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Même cause que le Blocker de la revue Claude 1, corrigée dans f14f905 : une pièce jointe ne recopie plus le titre de sa section, elle porte section_index (indice dans ParsedDocument.sections) ; test test_a_long_heading_is_not_copied_once_per_image.
Ce qui reste : la sortie est désormais linéaire en l'entrée. reference est un texte distinct de l'entrée (jamais recopié), et chaque image coûte un objet de taille fixe. Mesuré sur f14f905 (Python 3.11, parse_confluence_storage + asdict + json.dumps) : 1 048 572 octets de <img/> répétés donnent 174 762 entrées et 14 219 379 octets de JSON, soit 13,6× l'entrée — amplification bornée, plus de croissance quadratique. Plafonner le nombre d'entrées avec un compteur truncated explicite est possible ; je laisse ce fil ouvert pour la décision du propriétaire.
Generated by Claude Code
|
Claude finished @cdeust's task in 3m 23s —— View job Review 3 of 3: PR #893 finished
Summary: Accepted: Open Blocker: Not done: I didn't run lint, pytest or mutmut, and I didn't check CI on Verdict: block |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f14f905ec0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "retired_receipt_ids": written["retired_receipt_ids"], | ||
| "images_skipped": normalized.image_count, | ||
| "images_skipped": len(normalized.attachments), | ||
| "attachments": [asdict(a) for a in normalized.attachments], |
There was a problem hiding this comment.
Expose the section mapping used by attachment indices
When a document contains image-only or headings-only sections, section_index cannot be resolved against the returned section_memory_ids: it indexes ParsedDocument.sections, whereas the normalizer excludes empty sections from section memories, and the parsed sections are never returned. For example, an image-only preamble followed by a populated heading produces attachment indices 0 and 1 but only one section memory; the preamble is also absent from the rendered wiki. Consequently, callers cannot reliably identify which section contains a skipped image. Return an index-to-section mapping alongside the inventory (once per section to avoid repeating long headings), or expose another resolvable section identifier.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmé. Corrigé dans f0d8fd8 : le résultat porte section_headings, le titre de chaque section analysée dans l'ordre où section_index l'indexe, émis une seule fois (pas une fois par image) ; le test handler vérifie ["Design", "Rollout"].
Generated by Claude Code
| if _local(node.tag) in ("drawing", "pict"): | ||
| out.append( | ||
| DocumentAttachment( | ||
| ordinal=len(out) + 1, | ||
| reference=_image_reference(node), |
There was a problem hiding this comment.
Inventory every image reference inside grouped pictures
For a DOCX containing grouped pictures, a single w:pict can contain a v:group with multiple v:shape/v:imagedata elements. The collector creates only one attachment for that container, and _image_reference returns immediately on its first image, silently omitting the remaining image references from the new inventory. Parsing a group with rId1 and rId2 reproduces this: only rId1 is returned. Collect the individual image-bearing elements within each container so every skipped picture is represented, while retaining an empty-reference entry for containers whose reference is unavailable.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmé (reproduit : un v:group avec rId1 et rId2 ne donnait que rId1). Corrigé dans f0d8fd8 : l'inventaire compte chaque élément qui nomme une image (a:blip, v:imagedata), plus une entrée vide par conteneur extérieur qui n'en nomme aucun (core/document_images.py) ; test test_every_picture_of_a_group_is_inventoried.
Generated by Claude Code
Review 3 of 3: PR #893 at
|
There was a problem hiding this comment.
🛡️ Codex Security Review · Automatically triggered
Here are some automated security review suggestions for this pull request.
Reviewed commit: f14f905ec0
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
| out.append( | ||
| DocumentAttachment( | ||
| ordinal=len(out) + 1, | ||
| reference=_image_reference(node), |
There was a problem hiding this comment.
🛡️ Codex Security Review · Automatically triggered
Security: Parse image references in a single pass
When a user ingests an attacker-supplied Confluence export, nested <ac:image> elements make _collect_images call _image_reference() once per ancestor, and each call walks that ancestor's full subtree. This is quadratic: the reproduction parsed 8,000 nodes/168 KB in 2.28 s, while 16,000 nodes/336 KB exceeded 8 s, both below the 1 MiB cap. safe_handler only moves the work to an admitted thread and supplies no deadline, so repeated ingests can consume a core and degrade the local MCP service. Traverse once (the same pattern exists in the DOCX parser), or inspect only allowed direct descendants and cap image count/depth. This is fresh evidence distinct from the prior response-size report: the slowdown occurs before serialization.
SECURITY.md reference: SECURITY.md:L15-L16
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmé sur f14f905, corrigé dans f0d8fd8 : l'inventaire (core/document_images.py) fait un seul parcours itératif en préordre de chaque bloc ; chaque élément est visité une fois et un conteneur imbriqué dans un autre n'est plus relu. Mesuré sur f0d8fd8 (CPython 3.11, parse_confluence_storage) avec N <ac:image> imbriqués autour d'un ri:attachment : 8 000 → 0,019 s, 16 000 → 0,036 s, 60 000 (1,26 Mo) → 0,142 s, 1 entrée à chaque fois. Tests test_nested_confluence_images_around_one_attachment_yield_one_entry et test_nested_containers_around_one_reference_yield_one_entry (docx, 2 000 niveaux).
Generated by Claude Code
…out (#732, slice 1) Review 3 Blocker: the collectors recorded every drawing/pict or image/img in the subtree, nested ones included, and each took the first reference below it, so N nested containers around one L-byte reference gave N copies (quadratic output). Codex: a v:group of pictures gave one entry; section_index could not be resolved by the caller. The inventory now lives in core/document_images.py: one entry per element that names an image (a:blip, v:imagedata, ri:attachment, ri:url, img), plus one per outermost container that names none, read from that element's own attribute in an iterative preorder walk. Output is linear in the input (40,000 nested w:pict around a 500 KB id: 1 entry, 0.06 s). The result carries section_headings once, the table section_index points into. Four equivalent mutants are registered with their reason. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018kyZYraH8hqphMRC2MG7Jz
…out (#732, slice 1) unit, run 42233499091: "functions changed, no test file imports mcp_server.core.document_images" (ADR-1122 untested module). The tests reached the module only through the parsers; test_document_images.py now drives collect_images directly and pins a leaf without its attribute (mutants 22/24/25 of _image_references). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018kyZYraH8hqphMRC2MG7Jz
|
Le gate de mutation (ADR-1122) choisit les tests d'un module par import direct ; Vérifié localement avec mutmut 3.8.1 sur la sélection exacte de CI ( Generated by Claude Code |
|
Sur Generated by Claude Code |
|
Claude finished @cdeust's task in 2m 21s —— View job Review 4 of 3: PR #893 at
|
#732, slice 2) (#978) * feat(ingest): each embedded docx image names its part, digest and size (#732, slice 2) The attachment inventory from slice 1 (#893) named a docx image only by its relationship id. The id is now resolved through word/_rels/document.xml.rels (ECMA-376 Part 2 §9.3, Annex A.3) to the package part holding the bytes, and that part is streamed through SHA-256. Linked images, absent parts and Confluence images keep part/digest/size empty; state stays unsupported until a later slice stores the bytes. Malformed relationships fail before any write, like the main part. Refs #732 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018kyZYr4VjkzmxX2THRjfx7 * fixup! feat(ingest): each embedded docx image names its part, digest and size (#732, slice 2) * fixup! feat(ingest): each embedded docx image names its part, digest and size (#732, slice 2) * fixup! feat(ingest): each embedded docx image names its part, digest and size (#732, slice 2) fixup! feat(ingest): each embedded docx image names its part, digest and size (#732, slice 2) unit (run 42750861159): test_every_storage_primitive_is_an_audited_site read archive.open(name) as open-write, the first positional argument of an open on an unresolved receiver being taken as its mode. The member is opened for reading: say so with mode="r". * fixup! feat(ingest): each embedded docx image names its part, digest and size (#732, slice 2) unit (run 42755857850): the mutation gate (ADR-1122) left 12 survivors and found no test importing ingest_document_media. - docx_relationships: the all-'..' guard was redundant (popping word/ already yields ''), so it is now 'if not named'. Id and Target are read without a default and a relationship missing either is skipped; a missing-Target test pins it. - docx_media_reader: the tests assert that the docx path appears in read errors, and that every read asks for at most 64 KiB. mode="r" (the ZipFile.open default) and 'UTF-8' vs 'utf-8' are registered equivalents. - ingest_document_media: the lookup has no default; tests_py/handlers/test_ingest_document_media.py covers it directly. --------- Signed-off-by: Claude <[email protected]> Co-authored-by: Claude <[email protected]>
Summary
ingest_documentréduisait chaque image embarquée à un compteur anonyme (image_countdans le modèle,images_skippeddans le résultat) : un appelant ne pouvait pas savoir quelle capture de quelle section avait été perdue. Cause racine : les parseurs docx et Confluence comptaient les éléments image sur tout l'arbre (_count_images) sans en garder la référence ni la position.Cette première tranche de #732 remplace ce compteur par un inventaire : chaque image devient un
DocumentAttachment(ordinal, reference, section_index, state)dans l'ordre du document ;section_indexest la position de la section dansParsedDocument.sections(un indice plutôt que le texte du titre, pour ne pas recopier un long titre une fois par image).referenceest ce que le balisage nomme (id de relation docxa:blip@r:embed/v:imagedata@r:id,ri:attachment@ri:filename/ri:url@ri:valueouimg@srcpour Confluence,""sinon). Rien n'est encore extrait ni conservé, donc chaque image est rapportéeunsupported. Le handler renvoie l'inventaire sousattachmentsà côté deimages_skipped(qui en est la longueur) etsection_headings, le titre de chaque section dans l'ordre quesection_indexindexe ; la notice et le résumé sont inchangés.L'inventaire (
core/document_images.py, partagé par les deux parseurs) compte une entrée par élément qui nomme une image (a:blip,v:imagedata,ri:attachment,ri:url,img) plus une par conteneur extérieur (w:drawing/w:pict,ac:image) qui n'en nomme aucun, lue sur l'attribut de cet élément, par un parcours itératif : la sortie est linéaire en l'entrée (mesuré : 40 000w:pictimbriqués autour d'un id de 500 Ko → 1 entrée, 0,06 s ; 1 MiB de<img/>→ 174 762 entrées, 14,2 Mo de JSON).Refs #732
Tranches restantes (non couvertes ici) : conserver les octets, le digest et la version du document avec une référence de source accessible (état
preserved-only) ; étatsextracted/failedexplicites ; OCR/vision optionnels avec fournisseurs déclarés ; propriété des références partagées à la correction/suppression ; rejeu d'une fixture illustrée publique dans les deux hôtes.Type of change
attachmentsdans le résultat ;images_skippedgarde sa valeur.Test plan
pytest tests_py/core tests_py/handlers tests_py/test_main.py: 5525 passed, 242 skipped, 1 failed —tests_py/core/test_wiki_coverage_dashboard.py::test_unreadable_page_is_skipped_not_fatal, qui échoue identiquement surmain(8ef4813) dans cet environnement (exécuté en root, un fichierchmod 000reste lisible). Sans rapport avec ce diff.r:embed,v:imagedata@r:id), image dans un tableau, ordre et section des images, Confluence par nom de fichier / URL /src/ aucune référence, inventaire exact renvoyé par le handler (tests_py/handlers/test_ingest_document_attachments.py).ruff check/ruff format --check(ruff 0.17.3, version deuv.lock) sur les fichiers modifiés : OK.python scripts/check_craftsmanship.py: OK.Mutation (mutmut 3.8.1, version de
uv.lock) lancée localement sur les fonctions modifiées : aucun survivant hors des quatre équivalents enregistrés dansmemory/mutation-equivalents.json(_local×3, valeur initiale depending), avec leur justification. Le script du gate (uv run) n'a pas pu s'exécuter ici (torch hors réseau autorisé). Pyright non lancé.Audit notes
core/(pur, stdlibxml.etreeseulement) ; le handler ne fait que sérialiser (dataclasses.asdict).w:bodyau lieu de toute la racinew:document; une image hors du corps (inexistante en pratique dansword/document.xml) ne serait plus comptée.Breaking changes
ParsedDocument.image_countetNormalizedDocument.image_count(modèle internecore/) sont remplacés parattachments; aucun appelant hors des tests. Le contrat MCP gardeimages_skipped.Reviewer checklist
ingest_documentmise à jour.Generated by Claude Code