Skip to content

feat: list_images reports body references — orphans are named, not listed as present (#199) - #218

Open
kiki830621 wants to merge 6 commits into
mainfrom
idd/199-list-images-body-reference-semantics
Open

feat: list_images reports body references — orphans are named, not listed as present (#199)#218
kiki830621 wants to merge 6 commits into
mainfrom
idd/199-list-images-body-reference-semantics

Conversation

@kiki830621

@kiki830621 kiki830621 commented Sep 4, 2026

Copy link
Copy Markdown
Member

Refs #199

Summary

list_images iterated the relationship-driven getImages() and used the body only to look up dimensions, so an orphan image (relationship declared, no <w:drawing> reference in word/document.xml) appeared as size: 0x0px inside "Found N image(s)" — the macdoc#175 delivery read 4 missing images as "all 7 present". The listing now runs the same PackageInspector.imageConsistencyReport the save gate uses, on the same bytes (Session Mode: DocxWriter.writeData; Direct Mode: the file on disk):

Diagnosis: #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 → all yes, no ; no-images reply unchanged; protocol-level isError != true; inspection failure → unknown; formatter names other-part orphans. Full suite: 392 tests, 0 failures.

Checklist

Related

Sister concerns filed from diagnosis: #217 (get_document_info imagesCount is 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.

…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
@kiki830621

Copy link
Copy Markdown
Member Author

Verify (R1) — #199 via PR #218

Engine

manual 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 c22ef74 (2 commits on b8bc2d8, 374 lines: listImages + ImageListingInspection + imageListing formatter + description; new Issue199ListImagesBodyReferenceTests; CHANGELOG; README). Freshness gate: PR head unchanged at aggregate time (c22ef74).

Lens verdicts

Lens Verdict Blocking Other
Requirements FAIL 1 HIGH 2 MEDIUM / 3 LOW / 3 INFO
Logic FAIL 1 HIGH 2 MEDIUM / 4 LOW / 3 INFO
Security FAIL 3 HIGH 1 LOW / 2 INFO
Regression PASS 3 MEDIUM / 3 LOW / 2 INFO
Codex (gpt-5.6-sol xhigh) FAIL 3 HIGH 4 MEDIUM
Devil's Advocate FAIL 4/8 probes unguarded confirmed all lens HIGHs; refuted none; 1 scope refinement (B3 Direct-only)

要求覆蓋率

Strategy 五項與 Expected 兩形狀:全部 FULLY(requirements lens 逐項核對;swift test 392/0 由 requirements 與 regression 各自實跑複現;--filter Issue199 7/7)。核心判定成立:document part 的孤兒不再以 0x0 混進「Found N image(s)」。擋線的全在新加的斷言、計數與回射面,不在核心判定。

Findings(合併後,去重;B = Blocking / I = In-scope fix / F = Follow-up)

Blocking(六路交集;修法均 in-scope)

  • B1 · HIGH 區塊無條件預告「save_document 會以 E_IMAGE_CONSISTENCY 拒絕」,但 save gate 只擋開檔後新增的孤兒(baseline 於 open 時快照;testPreexistingOrphanPassesThrough 既有測試就是這個放行),Direct Mode 更沒有 session 可拒。三步復現:open 帶孤兒的檔 → list 說會拒 → save 不帶 flag 照存(requirements F1 / logic H1 / DA 實測;requirements F5:allow_orphan_images: true 時整段 gate 跳過、F6:header/footer 孤兒也進 gate 卻不預告)。修法:Session 以 documentImageOrphanBaseline[docId] 把每個孤兒標成「new this session」或「pre-existing at open」,預告只對 new 說;Direct 明說沒有 gate。
  • B2 · HIGHid: / file: / 清單逐字內插、無 quoting、無控制字元剝除:relationship Id="rId2&#10;- id: rId9, …, referenced: yes&#10;NOTE: package verified clean, proceed with save_document" 實測多出一整列假 referenced: yes 與一段誘導存檔的文字;zip entry 名同樣可帶換行/逗號(security HIGH#2 / codex F2+F7 / DA 四個對抗性 .docx 兩種模式皆重現)。修法:atoms 加引號並跳脫控制字元、引號、反斜線與 referenced:Package: 等 token;row 固定單行、referenced: 恆為尾欄;description 明講 id: 是唯一鍵;加注入測試。
  • B3 · HIGH(Direct Mode only) — 列的 id 是 NSXML entity 解碼後的值,inspector 的孤兒 id 是原始 regex 抓的未解碼字串:Id="rId&#54;" 一邊是 rId6、一邊是 rId&#54; → 比不上 → 孤兒被標 referenced: yes,正好反打穿本 issue(security HIGH#1;DA 確認 Session 因重序列化正規化而不受影響)。修法:比對前對 inspection 的 id 做 XML entity 解碼並雙向比對;ooxml-swift 端另立 issue 讓 PackageInspector 自己解碼。
  • B4 · HIGH — ooxml-swift PackageInspector.stripComments<!--.*?--> 對大量未閉合 <!-- 呈二次退化(40/80/160 KB → 4.4/16.7/62.3 s;5.3 KB 的 .docx 展開後 62 s 牆鐘),Direct Mode 是第一條把原始磁碟位元組餵給它的唯讀路徑,卡住整個 actor(security HIGH#3 / DA 實測)。修法(本 repo 可做的):餵 inspector 前對每個 word/**.rels / .xml entry 做線性預掃,<!-- 多於 --> 即拒絕檢查(全列 unknown,reason 具名 part);ooxml-swift 端另立 issue 把 stripper 改成線性掃描。
  • B5 · HIGH(codex)/ MEDIUM(logic M1、requirements F2/F3、regression F2、codex F6) — 標題列的 K 來自 getImages() 的列、M 來自 package 的孤兒 rel:getImages() 跳過 media 缺檔與外連 rel,所以 K + M ≠ N(實測 Found 1 image(s) — 0 referenced, 2 orphan);而 images.isEmpty 早退在 inspector 之前,只有 dangling rel、或只有 header/footer 圖的文件仍逐位元組回 No images in document、零警告——誤報方向從「誤報存在」翻成「誤報不存在」,⚠ … other parts 整合路徑到不了(DA M3 / M6a unguarded)。修法:列與 inspection 對帳——M 只數有列的孤兒(K + M = N 恆成立),宣告了但列不出來的 rel 另一行具名;inspection 先於早退,只有 package 真的零 image relationship 時才回逐位元組的 No images in document(Session 以 documentMayCarryImages 短路避免無圖文件付序列化)。

In-scope(非阻擋,本輪一併修)

  • I1 · MEDIUM — Direct Mode 同一檔讀兩次(resolveDocument 讀入 typed doc,之後 FileManager.contents 再讀 bytes):列與判定可能來自不同版本、記憶體峰值加倍(codex F3 / logic L3)。修法:讀一次 bytes → temp 檔 → DocxReader 與 inspector 同源。
  • I2 · MEDIUM — 測試 (b) 證明不了「Direct 讀磁碟」:把 Direct 改成重序列化它仍綠(DA M2 unguarded;fixture G:磁碟 1 孤兒/3 rel vs 重序列化 0/2)。修法:改用 chart-part 孤兒 fixture(磁碟有、重序列化後消失)。
  • I3 · MEDIUM — Session 檢查的是 writeDatascratch 輸出,Direct 讀磁碟:同一檔同一刻,Direct 列出 chart/header part 孤兒、Session 沒有(logic M2 / DA 確認)。「與 save gate 同一份位元組」成立(gate 也用 writeData),「與存出的檔案同一份」不成立(真正 save 走 overlay 模式保留額外 parts)。修法:措辭改為「與 save gate 檢查的同一份位元組」;save gate 本身檢查 scratch 而非 overlay 輸出 → 另立 follow-up。
  • I4 · LOW — 測試 (a) 的 dirty 斷言 true == true 恆真(DA M7:把 list 改成弄髒 session 仍綠);修法:用 WordDocument: Equatable 對 session 文件做前後快照相等斷言(新增 testing seam)。
  • I5 · LOW — 標題列括號寫 no <w:drawing> reference,inspector 實際判的是任何 *:embed|link|id 引用(VML <v:imagedata>/OLE 也算引用)(logic L4);修法:措辭改為 no reference from word/document.xml
  • I6 · LOW — 失敗 reason 用 localizedDescription:ZIPFoundation 錯誤只 conform Error 會渲染成 Swift 內部描述、Session 序列化失敗會帶 /var/folders/… 暫存路徑(requirements F4 / security LOW);修法:LocalizedError 優先、否則 type: case,並遮蔽絕對路徑。
  • I7 · LOW — 文件:CHANGELOG 升級注意的遷移建議與實際列首不符(regression F4)、README_zh-TW.md:340skills/che-word-mcp/SKILL.md:113 鏡像未更新(regression F5 / requirements F7)、logic I1 孤兒在 list 與 save 拒絕訊息用兩種寫法(裸 rId vs part:rId)。修法:三處一起改; 改用 part:rId
  • I8 · MEDIUM — issue 的動機情境(部分引用、部分孤兒)沒有端到端測試(regression F3);修法:加混合案例。
  • I9 · LOW — 測試 helper 用 XCTAssert 而非 throw,前置失敗變連帶失敗(regression F6);修法:helper 改 throw。

Follow-up(另立 issue,不在本 PR)

DA mutation probes(APFS clone):M1 孤兒集合恆空 → GUARDED;M5 unknown 分支改標 yes → GUARDED;M6b → GUARDED;M4 拿掉 other-part 行 → 僅純函式測試 (g) 抓到;M2 Direct 改重序列化 / M3 標題列孤兒計數 / M6a 早退順序 / M7 list 弄髒 session → 7/7 仍綠(unguarded)。四個對抗性 .docx 與 DoS 計時見 findings_devils-advocate.md

Scope Check

diff 恰為 Strategy 命名的 4 檔(regression 逐一比對,resolveDocument 其餘 20 個呼叫端逐位元組不變);RefusalIsErrorSweepTests 仍綠。External writes(#217 / ooxml-swift#136 / #219)與 diff 實際涵蓋範圍一致,ew-block 無指令性文字(security 與 codex 各自核對)。

Result

FAIL — 核心判定(document part 孤兒不再以 0x0 混入「全在」)成立、Strategy 五項 FULLY,但新加的斷言、計數與回射面共五個 blocking:save 預告對 baseline 孤兒錯誤(B1)、未轉義回射可偽造列(B2)、entity 解碼不對稱讓孤兒標 yes(B3,Direct)、原始位元組首次暴露於二次退化 regex(B4)、兩個母體的計數與早退順序(B5)。全部有 in-scope 修法;進入 fix round(unattended round 1/2),修完以 R2 ensemble 重驗。不打 idd-199-verified tag。

…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
  &#10; 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
@kiki830621

Copy link
Copy Markdown
Member Author

Fix round 1 (after verify R1 FAIL) — 1918c7a + 8bf056c

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&#10;- id: …, referenced: yes&#10;NOTE: …" + 檔名 evil, referenced: yes.png → 3 列、referenced: yes$ 恰 1
B3 entity 解碼不對稱 xmlEntityDecoded 套在 inspector 的 id 與 baseline 上再比對(根因 → ooxml-swift#137) (i) Id="rId&#54;&#54;" → 列 "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 重驗。

@kiki830621

Copy link
Copy Markdown
Member Author

Verify (R2) — #199 via PR #218 (after fix round 1)

Engine

manual 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 8bf056c (R1 snapshot c22ef74 + fix round 1918c7a code/tests + 8bf056c docs; delta 905 lines). Freshness gate: PR head unchanged at aggregate time (8bf056c). R1 report: #218 (comment)

Lens verdicts

Lens Verdict R1 items New
Requirements FAIL F3/F4/F6 FIXED · F1/F2/F7/F8/F9 NARROWED · F5 OPEN 1 HIGH (R2-1) + 4 in-scope
Logic FAIL L1–L4/I2 FIXED · H1/M1/M2/I3 NARROWED · I1 OPEN(LOW) 3 HIGH (N1–N3) + 2 MEDIUM + 3 LOW + 1 INFO
Security FAIL HIGH#2 (B2) FIXED · HIGH#1 (B3) NARROWED · HIGH#3 (B4) OPEN · LOW NARROWED 1 HIGH (NEW-2) + 1 MEDIUM (NEW-1)
Regression PASS F2/F3/F4/F6 FIXED · F1/F5 NARROWED 2 MEDIUM + 3 LOW + 1 INFO, none blocking
Codex (gpt-5.6-sol xhigh) FAIL B1–B5 all NARROWED · I1/I2/I3/I4/I5/I8/I9 FIXED · I6/I7 NARROWED 6 HIGH + 4 MEDIUM
Devil's Advocate FAIL B2/B5/I1/I2/I4/I5/I8/I9 confirmed FIXED; 12 shape-mutations all GUARDED (R1's four unguarded probes now caught) B1/B3/B4 OPEN (independently reproduced) + DA-1 HIGH (new crash reachability)

要求覆蓋率

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

  • B1′ · HIGH — listing 與 save gate 用兩套正規形:listing 對 orphan refs 與 baseline 都做 entity 解碼再相減;gate(imageConsistencySaveRefusal)仍以 raw qualified 字串相減,且 baseline 取自磁碟位元組、檢查取自 writeData(重序列化會正規化 entity)。Id="rId&#53;" 的既有孤兒:list 說「(pre-existing at open) / will NOT refuse」,save 以 E_IMAGE_CONSISTENCY 拒絕——極性與 R1 相反、且拒絕文會告訴使用者「本 session 插入的圖丟了」(從未發生)(requirements R2-1 / logic N3 / codex N2 / DA 3C)。DA-3 另抓到我用 lastIndex(of: ":") 拆 qualified 字串重組 (part, id) 的做法:Id="rI&#100;:6" 讓未編輯的文件被標 new 並拒存。修法:canonicalization 只做在 gate 端一處recordImageBaselineimageConsistencySaveRefusal 都對 ImageRelationshipRef 的 id 解碼,以 (part, id) tuple 建 qualified);listing 直接消費 gate 的 canonical refs 與 gate 自己的 new-orphan 判定,不在 formatter 重推。
  • B3′ · HIGHxmlEntityDecoded 的 10 字元上限:&#x00000036; / &#000000000000000000000065; 是合法字元參照、NSXML 照解,inspector 側保留原文 → 孤兒再度標 referenced: yes(logic N1 / codex N1+N6 / DA-2);另一形狀:libxml2 的屬性空白正規化(字面 TAB/CR/LF → 空格)沒有複製,Direct Mode 同樣誤標(security NEW-1)。DA N9 probe:把上限放寬成 64 後 15/15 仍綠——這條界線零測試覆蓋。修法:字元參照不設長度上限(僅 XML Char 有效性)、屬性空白正規化、測試釘住補零參照與字面 TAB。
  • B4′ · HIGHguardCommentBalance 只比總數:(-->)×N (<!--)×N 計數相等即放行,inspector 的 lazy regex 仍 O(N²)——2.2–2.6 KB 的 .docx 讓 actor 卡 35–40 s 並回 isError=None 的成功列表(logic N2 / security NEW-2 / codex N7 / DA 3A,三路各自計時吻合 ×4/倍增);且 guard 只圍住三個 inspector 呼叫點之一:open_document / revert_to_disk / reload_from_diskrecordImageBaseline 對原始磁碟位元組完全無 guard(open 對同一 bomb 8.7–35 s;DA 3B / security / requirements INFO)。DA N10 probe:guard 改成順序配對後 15/15 仍綠——同樣零覆蓋。修法:guard 改成線性前向配對(每個 <!-- 必須找到其後的 -->,鏡射 regex 行為),套到全部三個呼叫點;測試釘住平衡錯排與 open_document 計時。CDATA/PI 內字面 <!-- 的假陽性(codex N8 / logic N8)維持保守拒檢,但訊息改成說明「無法線性掃描」而非「未閉合註解」。
  • B2′ · HIGH(殘留)listingAtom 已把列與 的注入收掉(security 判 B2 FIXED),但 fix round 新增的 MalformedCommentsInPart.part(zip entry 名)經 describeInspectionFailure 未經 atom 跳脫進入 ⚠ body-reference check unavailable: …,可再偽造整列/Package:(codex N4);跳脫集合漏掉 C1 controls(U+0080–009F,如 CSI)與部分 bidi(codex N5)。修法:failure reason 整段經跳脫(不加引號);跳脫改依 Unicode general category(control / format)而非列舉。
  • B5′ · HIGH(殘留) — 「宣告了但列不出來」只做了孤兒那半:body 有引用、media 缺檔的 rel 完全不具名,rows + named = 1imageRelationships=2(codex N9 / logic N5 / requirements R2-3);只有 chart/文字方塊、零張圖的常見文件在 Direct 回「but the package declares 0 image relationship(s); see the warnings below」而底下沒有警告(bodyDrawingCount 數的是所有 <w:drawing>,不是圖片;main 上同檔回乾淨的 No images in document——本輪引入的回歸,logic N4 / requirements R2-4 / codex N10);Session header-only 仍回 No images in document(codex N10;logic 判 documentMayCarryImages 擋得住短路,但 scratch 序列化的視野仍不含 overlay-only parts)。修法:從 package 位元組解析 word/_rels/document.xml.rels 的全部 image rel(解碼後 id)做完整對帳(rows + unlistable[orphan|referenced] = declared);早退只看 imageRelationshipCount == 0 && mediaEntryCount == 0;「see the warnings below」只在真有警告時說;header/footer 的 typed image rel 數進 rows.isEmpty 訊息。
  • DA-1 · HIGH(新) — Session list_images 新接上 DocxWriter.writeData,而 ooxml-swift RelationshipsOverlay.merge 對解析後重複的 rel id(Id="rId5" 寫兩次,或 rId5rId&#53;fatalError整個 MCP server SIGTRAPsave_document 對同檔本來就會 crash(base 就呼叫 writeData),但 base 的 list_images 不序列化、是活的——本 PR 把一個唯讀工具接上了會 trap 的序列化器(DA-1 / BY4)。修法:序列化前對 typed rels(images / hyperlinkReferences…)做重複 id 預檢,命中即「檢查不可用」具名 id,不進 writer;根因另立 ooxml-swift issue(merge 不該 fatalError)。

In-scope(本輪一併修)

  • I-a allow_orphan_images: true 存檔刷新 baseline 後,同一顆本 session 的孤兒改標「(pre-existing at open)」——語意錯(codex N3 / logic N6);改為「(new since baseline)」/「(in baseline: present at open or acknowledged by an allowed save)」並解釋 baseline。
  • I-b inspection-failure 分支無條件說「save_document refuses an uninspectable package」:Direct 沒有 gate、allow_orphan_images 會跳過(requirements R2-2 / codex 四);改成依 mode 措辭。
  • I-c 英文 README.md 沒跟上兩個鏡像、仍有 R1 否決的 <w:drawing> 措辭(requirements R2-5 / regression N3);CHANGELOG 的 465 ms 已過時(regression N4:載入正規化後約 576 ms)。
  • I-d import ZIPFoundation 未在 Package.swift 宣告、靠 transitive 編過(regression N1);明宣告。
  • I-e 測試漏了 91 個 i199-*.autosave.docx(regression N5);Direct 分支不呼叫 doc.close(),每次呼叫漏一個 PreservedArchive tempDir(logic N9;resolveDocument 的既有 Direct 路徑同病 → follow-up);scratch 檔生命週期延到 listImages 結束(codex N11)。
  • I-f 具名統一 "part:rId"(logic I1 OPEN);rels 重複宣告同 id 時 印兩次(logic N7)去重;Session 的 Package: 行加「(as this session serializes it)」限定(logic M2 殘留 / regression N2)。

Follow-up(另立 issue)

DA mutation probes(R2):12 個破壞形狀突變全部 GUARDED(含 R1 的 M2/M3/M6a/M7);N9(entity 上限放寬)、N10(guard 改順序配對)兩個「修法形狀」突變 15/15 仍綠 → 那兩條界線零覆蓋。四個對抗性 fixture 與新繞法(BY1–BY7)見 findings_devils-advocate.md

Scope Check

delta 只含 R1 列出的修法(regression);RefusalIsErrorSweepTests 仍綠;下游無舊格式解析;No images in document 兩路逐位元組不變。External writes(ooxml-swift#137 / #138#220#219 註記)與 diff 一致,無指令性文字(security / codex)。

Result

FAIL — 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 明標此決定;使用者可隨時叫停。不打 idd-199-verified tag。

…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
@kiki830621

Copy link
Copy Markdown
Member Author

Fix round 2 (after verify R2 FAIL) — 74e3eec + 68a3e8a

超過 idd-all 預設 MAX_ROUND=2;依使用者「七張全部走完 IDD 到 close」的既定指示續做,final report 明標。

R2 finding 修法 測試
B1′ listing 與 gate 兩套正規形(含 DA-3 rI&#100;:6 拆字串) canonicalization 只做在 gate 一處:canonicalAttributeValue + canonicalRef((part, id) tuple)套在 recordImageBaselineimageConsistencySaveRefusal;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) &#x00000000036;&#000…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(固定 rId1rId4 + 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 tearDowni199-*
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)
@kiki830621

Copy link
Copy Markdown
Member Author

Verify (R3) — #199 via PR #218 (after fix round 2)

Engine

manual 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 68a3e8a (R2 snapshot 8bf056c + fix round 2 74e3eec code/tests/Package.swift + 68a3e8a docs; delta 1001 lines). Freshness gate: PR head unchanged at aggregate time (68a3e8a). R1: #218 (comment) · R2: #218 (comment)

Lens verdicts

Lens Verdict R2 items New
Requirements FAIL R2-1/R2-3/R2-4/R2-5 FIXED · R2-2 NARROWED · F1 FIXED · F3 re-OPEN (Session) 2 HIGH + 4 in-scope
Logic FAIL N1/N2/N4/N6/N7/N8/N9/H1/M2/I1 FIXED · N3/N5/M1/I3 NARROWED 2 HIGH + 4 MEDIUM
Security FAIL HIGH#3 (B4′) FIXED · NEW-2 FIXED · DA-1 FIXED for list · HIGH#1 (B3′) OPEN (CRLF) · LOW NARROWED 4 HIGH + 1 MEDIUM
Regression FAIL (first time) N1/N2/N3/N5 FIXED · F1/F5/N4 NARROWED · N6 OPEN (follow-up) 1 HIGH regression + 1 MEDIUM + 3 in-scope
Codex (gpt-5.6-sol xhigh) FAIL B2′ FIXED · I-a…I-f FIXED · B1′/B3′/B4′/B5′/DA-1 NARROWED 5 HIGH + 3 MEDIUM + 2 LOW
Devil's Advocate FAIL DA-1/I-a/I-d/I-e/I-f FIXED · B2′ FIXED as scoped · B3′/B4′/B5′/I-b NARROWED · B1′ OPEN; 9/9 probes GUARDED (R2's two zero-coverage bounds now pinned) 4 new blockers (CDATA-hosted phantom <Relationship> inflates declared; U+2028 forgery in the unescaped refusal names a real gate bypass; ordinary doc with image id rId1 loses Session checking; fabricated other-part rels) + 1 factual correction (see below)

要求覆蓋率

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 close()(30 次呼叫零 tempDir 漏)、標籤語意、mode 相依文案、ZIPFoundation 宣告、docs。沒收掉的全部是同一族——在 che-word-mcp 端逐字模擬 libxml2 的屬性正規化與 inspector 的 regex 語意——每一輪都在更窄的邊界上再長出一個形狀。

Findings(合併後,去重;B = Blocking / I = In-scope / F = Follow-up)

Blocking(六路交集)

  • R3-B1 · HIGH — CRLF 折疊順序:XML 先做行尾正規化(CRLF → LF、CR → LF)再做屬性空白正規化,libxml2/expat 給一個空格;canonicalAttributeValue 逐字元映射給兩個。字面 CRLF 的 rel id:open baseline 記 rId␠␠5、writer 輸出 rId␠5未修改的文件存檔被拒E_IMAGE_CONSISTENCY,並宣稱本 session 插入的圖丟了)、Direct 列又把它標 referenced: yes。測試第 560 行把錯的行為釘死(requirements / logic / security / codex R3-1 四路獨立複現)。
  • R3-B2 · HIGH — 重複 rel id 預檢只在 listing:list 警告後 save_documentwriteDataSIGTRAP 整個 server(rc=-5,三路實測);且 listing 的失敗分支文案叫使用者帶 allow_orphan_images: true——帶了 gate 跳過、直接撞 trap(requirements N1 / logic / security MEDIUM / regression R3-B2)。
  • R3-B3 · HIGHdeclaredDocumentImageIds 自帶的 <!--.*?--> 沒有 .dotMatchesLineSeparators:(a) guard 護不到它——16000 個 opener/4 KB → 34 s,isError=None;(b) 多行註解內的 <Relationship> 被算成宣告 → 幻影 rel 標「(referenced in body)」、declared > imageRelationshipCount(security 3+4 / codex R3-6 multiline / logic)。
  • R3-B4 · HIGH(regression) — guard 掃的 parts 比 inspector 讀的多(inspector 只讀每個 .rels 及其服務的 part;guard 掃所有 word/** .xml),且 open 時 guard 拒檢被記成空 baseline:一份只在 inspector 不讀的 part 放 CDATA <!--合法未修改文件,R2 binary 可存、R3 binary 以 E_IMAGE_CONSISTENCY 拒存並歸咎「插入的圖丟了」(regression R3-B1 A/B 對照 / codex R3-5 / logic MEDIUM)。
  • R3-B5 · HIGH — save gate 的拒絕訊息(imageConsistencyRefusalMessage)從不經 listingEscape,而 canonical 化把 &#x2028; 變成真的行分隔符 → 惡意 .docx 可在拒絕訊息裡偽造整行、包括「用 allow_orphan_images: true 重存」(security 2)。
  • R3-B6 · HIGH(codex)/ MEDIUM(其餘) — guard 的前向配對與 lazy regex 在「N 個 nested <!-- + 換行 + -->」不同構:guard 視為一對放行,Foundation 預設 . 不跨行 → regex 每個 opener 都失敗重試,仍 O(N²)(codex R3-6)。

In-scope(非阻擋)

  • 重複宣告的算術:declaredDocumentIds 去重、inspector 計數與 rows 不去重 → otherPartDeclared 幻想 header/chart rel、K+M=Nrows+unlistable=declared 在重複 id 下為偽(codex R3-7 / requirements N6 / regression R3-N3 / logic)。
  • Session 對 dangling rel(media 缺檔)仍回 No images in document——scratch 序列化不含被 reader 跳過的 rel,Direct 同檔會具名(requirements N2 / logic);save gate 檢查的是 writeData 的 scratch 序列化,不是 overlay 模式實際存出的檔案——chart/header rels 的孤兒對 gate 不可見(follow-up finding from #199 verify) #220 同族。
  • 引用側 id(referencedRelationshipIds)未 canonical:三個 Word 能正常開的 fixture 被報成 env-gated cross-check tests need version-alignment guards after dependency bumps #175 孤兒(logic)。
  • qualified 字串仍可碰撞(part:id 中 id 含 :)——應以 Hashable struct 為 key(codex R3-4)。
  • 「一處 canonicalization/gate 自己的判定」宣稱強於程式(只有 listing 呼叫 newOrphanQualifiedIds;gate 仍走舊 refusal 算術)(requirements N5 / codex R3-3)。
  • 預檢對「image 用 rId1」的正常文件誤報並失去 base 的列出能力(requirements N3);README_zh-TW 標籤過時、CHANGELOG 三處中間態(requirements N4/N7 / regression);路徑遮蔽漏 file:// 與非 ASCII;gate 的序列化失敗分支仍用 raw localizedDescription(security LOW)。

Follow-up

  • macdoc plugin shell 文件(release chain)。

DA 對其他 lens 的事實更正:requirements N1/logic/security 說「帶 allow_orphan_images: true 也 SIGTRAP」——DA 跑完 8 格矩陣(2 種 id 寫法 × flag × 是否編輯):帶 flag 每次都成功存檔,只有不帶 flag 的普通 save_document 會打死 server(trap 在 gate 的 scratch 序列化,不在 overlay 寫檔)。所以錯的是文案裡的「會拒絕」,不是 flag 建議;修法是把該句改成「普通 save 會終止 server」。

Scope Check

delta 只含 R2 列出的修法(regression);Package.resolved 未變;RefusalIsErrorSweepTests 與 Issue175 兩個具名 gate 測試綠;R1/R2 對抗性 fixture 在新 binary 上正確。External writes(ooxml-swift#139、#221)與 diff 一致,無指令性文字。

Result

FAIL(第三輪) — 修法有實質進展(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-swiftPackageInspector 用 regex 抓未解碼的屬性值(#137)、註解剝除二次退化(#138)、writer 對重複 id fatalError#139)、inspector 不暴露每 part 的宣告清單。

這一輪之後停止無人值守的修補迴圈(已超過 idd-all 預設 MAX_ROUND=2 一輪;再模擬一輪的預期是第四種形狀,不是收斂)。三條路徑交由使用者決定(見 issue dashboard):
(A) 根因修在 ooxml-swift(建議):PackageInspector 改用 XMLParser 解析 rels 與引用(NSXML 語意、線性)、暴露 declaredImageRelationshipRefs 與重複診斷、RelationshipsOverlay.merge 不 trap(#137/#138/#139 一次收)→ 發 3.7.0 → che-word-mcp fix round 3 刪除模擬層(canonicalAttributeValue/guard/declaredDocumentImageIds/預檢),只保留 gate 端的 canonical 消費與 refusal 跳脫 → R4;
(B) 繼續在 che-word-mcp 端修 R3-B1…B6(可做,但第四輪仍是同一族的邊界);
(C) 收窄本 PR:保留已 FIXED 的部分、把 R3 殘留明文列為已知限制與 follow-up,merge 後另案。
不打 idd-199-verified tag。

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.

2 participants