feat: list_images reports body references — orphans are named, not listed as present (#199) - #218
feat: list_images reports body references — orphans are named, not listed as present (#199)#218kiki830621 wants to merge 6 commits into
Conversation
…sted as present (#199) The listing ran on the relationship-driven getImages() and used the body only for dimensions; an orphan (relationship declared, no <w:drawing> in word/document.xml) showed as size 0x0 inside "Found N image(s)". Run the same PackageInspector the save gate uses, on the same bytes (Session: writeData; Direct: the file on disk): every row gets referenced: yes | NO (orphan) | unknown, orphans get a named warning that predicts E_IMAGE_CONSISTENCY, other parts' orphans are named separately, and an inspection that cannot run marks rows unknown instead of reverting to listed-means-present. Not a refusal. Refs #199
Verify (R1) — #199 via PR #218Enginemanual fan-out (4 lens Agents + sequenced DA, model: opus, file-based output) + Codex (gpt-5.6-sol, xhigh, via pai 2.22.1 codex-call). Frozen diff: PR head Lens verdicts
要求覆蓋率Strategy 五項與 Expected 兩形狀:全部 FULLY(requirements lens 逐項核對; Findings(合併後,去重;B = Blocking / I = In-scope fix / F = Follow-up)Blocking(六路交集;修法均 in-scope)
In-scope(非阻擋,本輪一併修)
Follow-up(另立 issue,不在本 PR)
DA mutation probes(APFS clone):M1 孤兒集合恆空 → GUARDED;M5 Scope Checkdiff 恰為 Strategy 命名的 4 檔(regression 逐一比對, ResultFAIL — 核心判定(document part 孤兒不再以 |
…toms, entity-decoded ids, linear comment guard, reconciled counts, single read (#199) Verify R1 (PR #218) failed on five blocking findings in the new listing, none in the core orphan detection: - B1 the warning predicted E_IMAGE_CONSISTENCY unconditionally; the gate only refuses orphans that appeared after open. Label each orphan new-this-session / pre-existing-at-open from documentImageOrphanBaseline and predict only for the former; Direct Mode says it has no gate. - B2 ids / file names / part paths were interpolated raw; a crafted Id with forged a whole referenced: yes row. Quote every package-derived atom, escape controls/bidi/quotes, neutralise the structural tokens. - B3 getImages() ids are NSXML-decoded, the inspector's are raw; decode before comparing (ooxml-swift#137 for the root). - B4 Direct Mode fed raw disk bytes to a quadratic comment stripper; a linear pre-scan refuses parts with unterminated <!-- (ooxml-swift#138 for the root). - B5 header counts mixed two populations and the imageless early return ran before inspection; reconcile rows with the package, name unlistable relationships separately, inspect before deciding a document is imageless. Also: Direct Mode reads the file once (rows and inspection from one buffer), readable failure reasons without scratch paths, a document snapshot seam for tests, and fifteen cases that each pin one of the above. Refs #199
Fix round 1 (after verify R1 FAIL) —
|
| R1 finding | 修法 | 測試 |
|---|---|---|
| B1 save 預告對 baseline 孤兒錯誤 | ImageListingInspection.sessionNewOrphans(qualified refs − 開檔 baseline,兩邊同樣 entity-decode)→ 每個孤兒標 (new this session) / (pre-existing at open);只對 new 說「WILL refuse」,全為 baseline 說「will NOT refuse」;Direct 說沒有 gate;other-part 孤兒同樣標籤(它們也進 gate) |
(b) 三步復現 list 預測 vs save 結果雙向一致;(g) 純函式 |
| B2 未轉義回射可偽造列 | listingAtom:所有 package 來的值加引號,跳脫控制字元/U+2028/9/bidi/引號/反斜線,中和 referenced: Package: - id: ⚠;列恆單行、referenced: 恆尾欄 |
(j) Id="rId8 - id: …, referenced: yes NOTE: …" + 檔名 evil, referenced: yes.png → 3 列、referenced: yes$ 恰 1 |
| B3 entity 解碼不對稱 | xmlEntityDecoded 套在 inspector 的 id 與 baseline 上再比對(根因 → ooxml-swift#137) |
(i) Id="rId66" → 列 "rId66" NO (orphan) |
| B4 原始位元組暴露於二次退化 regex | guardCommentBalance:inspector 前線性掃每個 word/** .rels/.xml,<!-- > --> 即拒檢 → 全列 unknown 並具名 part(根因 → ooxml-swift#138) |
(k) 20000 個未閉合 <!-- → < 5 s、unknown、具名 part |
| B5 兩母體計數/早退順序 | 標題列只數列得出來的(K+M=N);宣告了但列不出來的 rel 另一行具名;inspection 先於「無圖」判定,三計數皆 0 才回逐位元組 No images in document(Session 以 documentMayCarryImages 短路避免無圖文件付序列化) |
(h) 缺 media rel → 1 referenced, 0 orphan + unlisted 行;(h2) dangling-only → 不回 No images in document |
| I1 Direct 讀兩次 | 讀一次 bytes → scratch 檔給 DocxReader、同一份 bytes 給 inspector |
— |
| I2 測試 (b) 無鑑別力 | 改為 chart-part 孤兒 fixture,先斷言 disk report ≠ roundtrip report | (g) |
| I3 Session scratch vs Direct disk | 措辭改「與 save gate 檢查的同一份位元組」;gate 本身 → #220 | — |
| I4 dirty 斷言恆真 | documentSnapshotForTesting seam,WordDocument 值相等 |
(a) |
I5 <w:drawing> 措辭 |
改 no reference from word/document.xml |
— |
| I6 reason 可讀性/路徑洩漏 | describeInspectionFailure:LocalizedError 優先、否則 Type: case,遮蔽絕對路徑 |
pure |
I7 docs 鏡像/part:rId |
CHANGELOG 條目重寫(含效能數字)、升級注意改列首、README_zh-TW + SKILL.md 同步;⚠ 具名用引號 atom |
— |
| I8 混合形狀端到端 | (d) 兩張插一張刪 → 1 referenced, 1 orphan |
(d) |
| I9 helper 連鎖失敗 | helper 改 throw(Precondition) |
— |
swift test:400 tests, 0 failures(+8)。Follow-ups filed:ooxml-swift#137 / #138、#220;#219 補範圍註記。接著 R2 ensemble(4 lens + DA + Codex)針對 delta 重驗。
Verify (R2) — #199 via PR #218 (after fix round 1)Enginemanual fan-out (4 lens Agents + sequenced DA, model: opus, file-based output) + Codex (gpt-5.6-sol, xhigh, via pai 2.22.1 codex-call). Frozen diff: PR head Lens verdicts
要求覆蓋率Strategy 五項與 Expected 兩形狀仍 FULLY(requirements);400 tests / 0 failures 由四路各自實跑;R1 對抗性 fixture 在新 binary 上全部正確(regression)。fix round 1 收掉了 B2、B5 的算術半邊與 I1/I2/I4/I5/I8/I9;沒收掉的是 B1/B3/B4 各自的第二種形狀,加一個由修法引入的新崩潰面。 Findings(合併後,去重;B = Blocking / I = In-scope / F = Follow-up)Blocking
In-scope(本輪一併修)
Follow-up(另立 issue)
DA mutation probes(R2):12 個破壞形狀突變全部 GUARDED(含 R1 的 M2/M3/M6a/M7);N9(entity 上限放寬)、N10(guard 改順序配對)兩個「修法形狀」突變 15/15 仍綠 → 那兩條界線零覆蓋。四個對抗性 fixture 與新繞法(BY1–BY7)見 Scope Checkdelta 只含 R1 列出的修法(regression); ResultFAIL — B2(注入)與 B5 的算術半邊、I1/I2/I4/I5/I8/I9 已收;B1/B3/B4 各剩第二種形狀(兩套正規形/長參照與空白/順序繞過與三個呼叫點),B2′/B5′ 各有一處殘留,外加修法引入的 DA-1 崩潰面。全部有 in-scope 修法;這是 unattended fix loop 的第二輪 FAIL——idd-all 預設 MAX_ROUND=2 到此應交回人工;依使用者「七張全部走完 IDD 到 close」的既定指示,再做一輪 fix round 2 → R3,並在 final report 明標此決定;使用者可隨時叫停。不打 |
…te, ordered comment guard on every inspector site, category-based escaping, full rels reconciliation, duplicate-id precheck (#199) Verify R2 (PR #218) left B1/B3/B4 each with a second shape, two residues and a crash the fix round introduced: - B1'/B3' the listing and the gate canonicalized ids differently (listing decoded, gate raw; a 10-char cap on character references; no attribute whitespace normalization) so predictions inverted. Canonicalize once, in the gate (recordImageBaseline + imageConsistencySaveRefusal), NSXML-equivalent (whitespace TAB/CR/LF to space, any-length XML-Char-valid references), build qualified refs from the (part, id) tuple, and let the listing consume the gate's own new-orphan computation. - B4' the guard compared totals; (-->)xN (<!--)xN passed and open_document had no guard at all. Ordered linear pairing, applied at all three inspector call sites. - B2' the guard's error carried a zip entry name unescaped; C1 controls were not escaped. Escape by Unicode general category and run every failure reason through it. - B5' relationships the body references but whose media is missing were invisible, and a chart-only document promised warnings that never came. Reconcile rows against the ids word/_rels/document.xml.rels declares, name unlistable ones as referenced or orphan, decide imageless on relationship and media counts only, name other-part relationships. - DA-1 Session listing now serialized, and a duplicate relationship id traps the writer. Pre-check duplicates (fixed rId1-rId4 + header/footer/image/ hyperlink ids) and refuse to serialize instead. Also: labels new since baseline / in baseline (an allowed save refreshes the baseline), mode-aware failure wording, part:rId everywhere, Package line qualifier, Direct Mode close() and scratch lifetime, ZIPFoundation declared, test tearDown cleanup; 22 cases. Refs #199
Fix round 2 (after verify R2 FAIL) —
|
| R2 finding | 修法 | 測試 |
|---|---|---|
B1′ listing 與 gate 兩套正規形(含 DA-3 rId:6 拆字串) |
canonicalization 只做在 gate 一處:canonicalAttributeValue + canonicalRef((part, id) tuple)套在 recordImageBaseline 與 imageConsistencySaveRefusal;listing 的 sessionNewOrphans = gate 的 newOrphanQualifiedIds |
(b2) entity 既有孤兒:list「in baseline / will NOT refuse」→ save 不帶 flag 成功;(b) 三段序列含 allow-save 後標籤 |
| B3′ 10 字元上限、屬性空白 | 任意長度字元參照(僅 XML Char 有效)、TAB/CR/LF → 空格先於實體替換 | (i2) 6�…65; → rId6A、字面 TAB → rId 7,皆 NO (orphan);純函式 |
| B4′ guard 順序繞過、三呼叫點 | hasUnpairedCommentOpener 線性前向配對;guardInspectableComments 套到 open/revert/reload baseline、save gate、listing |
(k2) (-->)×20000 (<!--)×20000:list 與 open_document 皆 < 5 s;純函式 |
| B2′ guard 錯誤回射 part 名、C1 | listingEscape 依 general category;describeInspectionFailure 整段跳脫+遮蔽絕對路徑 |
(j2) zip entry 名含 \n- id: "rId999"…⚠ → 仍 1 列、0 個 referenced: yes$;純函式 C1/零寬/U+2028 |
| B5′ body 有引用但 media 缺的 rel、chart-only 自相矛盾、header-only | declaredDocumentImageIds 從 rels 位元組讀全部 image rel → rows + unlistable(referenced|orphan) = declared;imageless 只看 rel 與 media 數;「see the warnings below」只在有 ⚠ 時;其他 parts 具名數量 |
(h2) referenced-but-missing → 具名「(referenced in body)」;純函式 chart-only → No images in document、header-only → 具名 #219 |
| DA-1 重複 rel id 讓 writer fatalError | duplicateTypedRelationshipIds 鏡射 buildTypedRelationships(固定 rId1–rId4 + header/footer/image/hyperlink id),命中即不序列化 |
(r) 複製第一個 rId 為 image rel(撞 rId1)→ open 正常、list 回 unknown 具名 id、不 crash |
| I-a 標籤語意 | (new since baseline) / (in baseline) + baseline 定義行 |
(b) allow-save 後 |
| I-b 失敗分支 mode | isSession 參數,Direct 明說沒有 gate |
純函式 |
| I-c README.md / CHANGELOG 數字 | 同步;效能改寫為 0.5–0.6 s(R1 465、R2 ~576) | — |
| I-d ZIPFoundation | Package.swift 明宣告 | — |
| I-e close / scratch / autosave | Direct directDoc?.close() 與 scratch 刪除在函式 defer;tests tearDown 清 i199-* |
— |
I-f part:rId / 去重 / Package 限定 |
全部 ⚠ 用 "part:rId";去重;Package (as this session serializes it) / (on disk) |
(a)(c)(d)(g) |
swift test:407 tests, 0 failures(+7,Issue199ListImagesBodyReferenceTests 22 案例)。Follow-ups R2:ooxml-swift#139、#221。接著 R3 ensemble。
| guard let entry = archive["word/_rels/document.xml.rels"] else { return [] } | ||
| var data = Data(); _ = try archive.extract(entry) { data.append($0) } | ||
| var xml = String(decoding: data, as: UTF8.self) | ||
| xml = xml.replacingOccurrences(of: #"<!--.*?-->"#, with: "", options: .regularExpression) |
Verify (R3) — #199 via PR #218 (after fix round 2)Enginemanual fan-out (4 lens Agents + sequenced DA, model: opus, file-based output) + Codex (gpt-5.6-sol, xhigh, via pai 2.22.1 codex-call). Frozen diff: PR head Lens verdicts
要求覆蓋率Strategy 五項與 Expected 兩形狀仍 FULLY;407 tests / 0 failures 由五路各自實跑。fix round 2 確實收掉:guard 順序繞過與三個 inspector 呼叫點(240 KB 平衡 bomb 三處皆 ≤ 32 ms)、guard 錯誤路徑與 C1 的回射、chart-only/header-only/referenced-but-missing 的呈現、Direct Findings(合併後,去重;B = Blocking / I = In-scope / F = Follow-up)Blocking(六路交集)
In-scope(非阻擋)
Follow-up
DA 對其他 lens 的事實更正:requirements N1/logic/security 說「帶 Scope Checkdelta 只含 R2 列出的修法(regression); ResultFAIL(第三輪) — 修法有實質進展(R2 的 B2′/B4′/DA-1(list)/I-a…I-f 全數確認 FIXED),但 blocking 集合沒有收斂到零:每一輪都在「che-word-mcp 端模擬 libxml2 屬性正規化 + inspector regex 語意」這條線上再長出新形狀(R1 entity → R2 長參照/空白 → R3 CRLF 折疊;R1 regex DoS → R2 順序繞過 → R3 nested+換行與第二個 regex)。根因全在 ooxml-swift: 這一輪之後停止無人值守的修補迴圈(已超過 idd-all 預設 MAX_ROUND=2 一輪;再模擬一輪的預期是第四種形狀,不是收斂)。三條路徑交由使用者決定(見 issue dashboard): |
Refs #199
Summary
list_imagesiterated the relationship-drivengetImages()and used the body only to look up dimensions, so an orphan image (relationship declared, no<w:drawing>reference inword/document.xml) appeared assize: 0x0pxinside "Found N image(s)" — the macdoc#175 delivery read 4 missing images as "all 7 present". The listing now runs the samePackageInspector.imageConsistencyReportthe save gate uses, on the same bytes (Session Mode:DocxWriter.writeData; Direct Mode: the file on disk):referenced: yes | NO (orphan) | unknown; header lineFound N image(s) — K referenced in body, M orphan (…)⚠block naming the rIds, naming the env-gated cross-check tests need version-alignment guards after dependency bumps #175 signature, and predictingsave_document'sE_IMAGE_CONSISTENCY/allow_orphan_imagesPackage: bodyDrawings= imageRelationships= mediaEntries=closes the replyunknownand says why — never reverting to "listed ⇒ present"isErrorstays unset (拒絕/錯誤以return "Error: …"字串回傳時 MCPisError未設——協定層仍是成功(repo-wide 慣例) #202 — only refusals are errors);No images in documentis byte-identicalDiagnosis: #199 (comment) (Complexity Plan; approach A chosen — bytes-level inspector over typed-model walk — so list and save read one truth).
Tests
Issue199ListImagesBodyReferenceTests(7): session orphan + session state untouched; Direct Mode reads disk bytes; consistent doc → allyes, no⚠; no-images reply unchanged; protocol-levelisError != true; inspection failure →unknown; formatter names other-part orphans. Full suite: 392 tests, 0 failures.Checklist
b82b505feat,c22ef74docs)Related
Sister concerns filed from diagnosis: #217 (
get_document_infoimagesCountis a relationship count), PsychQuant/ooxml-swift#136 (getImages()dimension lookup skips table-cell paragraphs).🤖 Generated by /idd-all (unattended; Plan tier deliberation skipped under unattended mode). Do NOT add a GitHub close trailer (Closes/Fixes/Resolves) — IDD discipline requires the manual idd-close skill after merge.