Skip to content

feat: tick-character guidance + three-valued glyph-coverage probe - #193

Open
kiki830621 wants to merge 4 commits into
mainfrom
idd/189-feat-checkbox-cjk-emoji-fallback
Open

feat: tick-character guidance + three-valued glyph-coverage probe#193
kiki830621 wants to merge 4 commits into
mainfrom
idd/189-feat-checkbox-cjk-emoji-fallback

Conversation

@kiki830621

Copy link
Copy Markdown
Member

Refs #189

Summary

勾選表單時把 換成 會產生一份文字層完全正確、外觀完全錯誤的文件:逐格比對全過,
但沒有任何實測字型帶有 U+2611 的字形,渲染器改用彩色 emoji 字體。工具沒動過 run 的字型宣告
——被換掉的是實際繪製的字型,而那一層文字比對看不到。

兩件事:

  1. replace_text / replace_text_batch 的說明改推 (U+25A0)。刻意不寫成 CJK 專屬
    實測 U+2611 在 Times New Roman 與 Arial 同樣缺字形,寫成 CJK 注意事項等於告訴呼叫者拉丁
    字型的表單是安全的。
  2. 內部 GlyphCoverageProbe,回答三值hasGlyph / noGlyph / unknown)。unknown
    是重點:CTFontCreateWithName 對未安裝字型回傳 Helvetica 而非失敗,而 Helvetica 連
    都沒有字形——兩值探測會指控本文件建議使用的那個字元,同時丟掉真正的發現。

Deliberately out of scope

沒有任何呼叫端使用這個探測。 三種接法都會新增或改變對外 MCP tool surface,那是需要人決定
的事(本次 /idd-all 在 unattended 下執行,Plan tier 的 approval gate 沒有跑);其中「附加在
回傳字串」另外卡在 #192本 PR 不新增任何 MCP tool、不更動任何既有 tool 的回傳格式
tools/list 仍回 245 個)。

Verification

  • swift test → 358 tests, 11 skipped, 0 failures
  • Mutation:把 unknown 塌縮成 noGlyph → 恰好 3 條 unknown 測試失敗;把指引改寫成 CJK-only
    → 恰好那條 scope 測試失敗。測試證明抓得到它們存在的理由,不只是通過。
  • Runtime tools/list:兩個 replace 工具的 description 確實帶著指引。
  • 未驗swift build -c release 逾時未跑完,release 組態本次無證據。

Checklist

  • Diagnose
  • Implement (3 commits)
  • Verify (/idd-verify #189)
  • Verify-gated:verify PASS 後才可合併 → 合併後跑 /idd-close 收尾(人工 gate + closing summary,無 auto-close trailer)

Do NOT add a GitHub close trailer — IDD 紀律要求合併後手動 /idd-close,以強制 checklist gate 與 closing summary。

A checkbox ticked as U+2611 stops matching the form because no measured
font carries that glyph — including Times New Roman and Arial — so the
renderer substitutes an emoji face. The run's declared font never changed;
the font that actually drew the character did.

The probe answers with three values rather than a Bool. CTFontCreateWithName
resolves an uninstalled family to Helvetica instead of failing, and Helvetica
has no U+25A0 either, so a two-valued probe asked about the recommended tick
in an absent CJK family reports "no glyph" for the one character that is
actually safe. `unknown` keeps "nothing was measured" from being readable as
"the glyph is missing".

Font resolution goes through a descriptor match rather than comparing against
CTFontCopyFamilyName, which answers in English: a literal comparison calls the
installed DFKai-SB missing when it is declared as its localized name.

No call site consumes this yet — see the Implementation Complete comment.

Refs #189
…ng (#189)

Ticking a form by replacing U+25A1 with U+2611 leaves the text layer correct
and the appearance wrong: no measured font carries U+2611, so the renderer
falls back to a colour emoji face and the box stops matching the form. Cell-by-
cell comparison passes, which is why this reached a real document — only a
human opening the file could see it.

The guidance names U+25A0 instead, and the tests pin the scope rather than the
phrasing: it must cite a non-CJK font, because U+2611 is missing from Times New
Roman and Arial too. Wording it as a CJK-font caveat would tell callers that
Latin-font documents are safe.

allTools drops to internal so the tests can assert on the surface that is
actually registered rather than on a copy of the string.

Refs #189
Cross-model verification found the resolver treating CoreText's best-match as
a lookup. CTFontDescriptorCreateMatchingFontDescriptor with no mandatory
attributes substitutes: asking for "System Font" hands back the .SF NS family.
The probe measured that font and reported hasGlyph/noGlyph as though it had
measured the declared one — a confident answer about something the document
never named, which is the exact silent-wrong-answer the third value exists to
prevent, arriving through the one door it did not cover.

Resolution now accepts only a font that answers to the declared name, compared
against the resolved font's own name set (family, full, PostScript, localized
family) so 標楷體 and differently-cased spellings still resolve while .SF NS
does not. Both decided cases carry the family actually measured; without it a
caller cannot audit which font a verdict describes. `unknown` carries a reason,
because "declared nothing", "not installed" and "landed elsewhere" need
different things said about them.

Also from the same review:
- The surrogate-pair comment described the CoreText contract wrongly. Behaviour
  was right; the stated reason was not. Measured and corrected, with tests for
  a non-BMP scalar in both directions.
- The localized-name test gated its skip on the function under test, so a
  regression breaking localized resolution would have skipped rather than
  failed. It now checks font presence through CoreText directly.
- The safe-character test asserted only `!= .noGlyph`, which a substituted font
  containing U+25A0 would have satisfied. It asserts the full verdict now.
- U+2612 was named unsafe in the tool description with no test behind it.
- The description predicted emoji fallback as a certainty; it is one of several
  platform-dependent outcomes, and the font claims hold for measured versions.
- The allTools note claimed `internal` widens visibility no further than the
  test bundle. It is module-wide.

Refs #189
@kiki830621

Copy link
Copy Markdown
Member Author

Verify Report — PR #193

Engine

降級 tier,請照此讀這份報告。 本 session 的操作指示禁止在未經使用者要求下使用 Agent tool
與 Workflow,而 idd-verify 的 canonical tier(pai-ensemble)走 Workflow、manual fan-out tier 走
Agent,兩條都不可用。實際跑的是:

來源 狀態
4 個 lens(requirements / logic / security / regression) coordinator 自審 — process gap,非獨立 reviewer
Devil's Advocate coordinator 自審 — process gap
Codex(gpt-5.6-sol, effort xhigh,經 pai codex-call 真正獨立的跨模型 leg,兩輪各跑一次

所以這不是 6-AI ensemble,是「1 個獨立模型 + 1 個自審協調者」。下方「這次驗證證明了什麼」段
有這件事的實質後果——不是形式免責。

Aggregate

FAIL — 3 blocking(round 2 後仍未解),已達 unattended 模式的 2 輪自動修復上限。

Frozen snapshot:073af55(round 2)。Round 1 審的是 7b68a35


這次驗證證明了什麼(而不是形式上的 tier 免責)

跨模型 leg 兩輪都推翻了 coordinator 的實證結論,而且是同一種錯誤的兩次發作。

  • Round 1:coordinator 測了 Songti / Aria / Times New Roman Bold 等片段名稱全部 no match,
    結論「descriptor 比對是精確的」。Codex 從 API 契約反駁(mandatoryAttributes: nil 是 best-match
    而非查表)。照它的指控重測會真的觸發替換的情境,找到反例:System Font.SF NS 家族。
  • Round 2:coordinator 修好後在自己機器上驗證 標楷體 正確接受,結論「修好了」。Codex 指出
    CTFontCopyLocalizedName 只回目前語言偏好的一個名稱。實測(per-process -AppleLanguages '(en)'):
    英文語系下 標楷體 的 localized family 回 DFKai-SB,請求字串不在名稱集合中 →
    正確安裝的字型被判成 .resolvedToDifferentFamily

兩次都是同一個病:拿單一機器的樣本當通則。而兩次都是 coordinator 自己看不出來、由另一個模型
指出的。這正是 4 個 lens 與 DA 缺席的代價——這份報告本身就是那個代價的證據,不是理論風險。


#189 — feat: checkbox 勾選的字形覆蓋指引/偵測

要求覆蓋率

Issue 要求 判定
replace_text 文件提供勾選字元選擇指引 FULLY
同指引涵蓋 replace_text_batch FULLY
正確表達根因不是 run formatting 遺失 FULLY
偵測插入字元在 run 宣告字型是否缺字形 PARTIALLY — primitive 存在但解析模型仍不正確(見 B1/B2)
工具層對呼叫端發出警告 NOT addressed — 刻意不做(scope 決定,非缺陷),無任何 call site

Findings

# Severity Finding Source Action
B1 HIGH identities(of:)CTFontCopyLocalizedName 取名稱,那只回目前語言偏好選出的一個本機化名稱,非字型完整名稱表。英文語系下 標楷體 不在集合中 → 已安裝字型被誤判 .resolvedToDifferentFamilyGlyphCoverage.swift:149-160 宣稱「接受 localized spelling」,實際只在偏好語系相符時成立。實測確認(per-process -AppleLanguages '(en)')。 codex(round 2)+ coordinator 實測 Blocking
B2 HIGH 查詢用 kCTFontFamilyNameAttribute,驗收卻允許 family / full / PostScript 任一相等。三者不同命名空間、無全域唯一性:宣告 Foo Bold、CoreText 匹配到 family Foo 的 Bold face、該 face full name 恰為 Foo Bold → 家族不同卻被接受。仍是「對未宣告家族給出確定 verdict」的路徑。GlyphCoverage.swift:129-145 codex(round 2) Blocking
B3 MEDIUM→Blocking(契約不實) resolvedFont: 這個 label 承諾識別實際量測的字型,實際只帶 family。同 family 的 Regular/Bold/Italic face 不保證 cmap 相同,而查詢未帶 run 的 bold/italic traits,量的是 CoreText 選的預設 face。標籤比實際能力強。GlyphCoverage.swift:82-103 codex(round 2) Blocking
M1 MEDIUM unknown 的 reason 列舉不完整:缺 ambiguousMatch(跨命名空間碰撞)與「descriptor 回 nil ≠ 已證明未安裝」。目前 matched == nil 直接斷言 .notInstalled,過度確定。GlyphCoverage.swift:133-137 codex(round 2) In-scope(未修)
M2 MEDIUM 「缺字形時渲染器改用別的字型」仍是必然性主張,且與同段列出的 .notdef 方框自相矛盾(.notdef 正是 fallback 沒有發生)。GlyphCoverage.swift:45-48Server.swift:905,939 codex(round 2) In-scope(未修)
M3 MEDIUM CHANGELOG.mdunknown 有「兩種來源」,型別已有三種(含 noDeclaredFont)。文件與型別不一致。 codex(round 2) In-scope(未修)
M4 MEDIUM substitution 測試只斷言 landedOn != 請求名。production 若永遠硬回 "Helvetica" 仍會通過——沒有保護「攜帶實際 matched family 以供稽核」這個宣稱。GlyphCoverageTests.swift:91-108 codex(round 2) In-scope(未修)
M5 MEDIUM XCTSkipUnless 放在 for family in [...] 迴圈內會 throw 並結束整個 test:Times New Roman 缺席時 Arial 完全不會被測,且整個 test 標成 skipped。GlyphCoverageTests.swift:134-147 codex(round 2) In-scope(未修)
M6 MEDIUM guidance 測試只要求「最早出現的否定詞」在 U+2611 之前——「不要用不支援的字型;請使用 U+2611」仍會通過,註解宣稱的「否定詞 govern U+2611」並未成立。batch 測試更只驗 token 存在。Issue189ReplaceTextGuidanceTests.swift:38-45, 54-60 codex(round 2) In-scope(未修)
L1 LOW 測試依賴宿主字型版本(Helvetica / DFKai-SB / Times New Roman / Arial),非 hermetic。字型更新改 cmap 即環境相依失敗。建議附測試字型 fixture 或注入 resolver。 codex(兩輪) Follow-up
L2 LOW 已完整斷言 .unknown(...) 後再斷言 != .noGlyph 為冗餘。GlyphCoverageTests.swift:59-70 codex(round 2) Follow-up
I1 INFO swift build -c release 兩次都未跑完(一次 10 分鐘前景逾時、一次背景被中止)。release 組態本次無證據。debug 建置與整套測試通過,故編譯正確性有覆蓋,但 release-only 最佳化問題不在證據範圍。 coordinator 發版前必驗
I2 INFO 4 個 lens 與 Devil's Advocate 由 coordinator 自審(見 Engine 段)。 process gap

Round 1 修正逐項判定(跨模型複核)

項目 判定
a) family substitution 不完整System Font → .SF NS 具體回歸已擋住,但 B1/B2 仍在
b) surrogate-pair 契約與註解 成立 — 兩方向皆有完整 verdict 測試
c) skip gate 不再依賴待測函式 成立 — 改用 CTFontManagerCopyAvailableFontFamilyNames
d) 完整 equality 斷言 成立
e) fallback 描述限定 部分 — emoji 已加「通常」與平台限定,但「一定會換字型」仍過度(M2)
f) Swift internal 說明 成立

Scope Check

無超出 issue 範圍的改動。唯一非直接要求的變更是 allToolsprivate 改 internal(為讓測試斷言
實際註冊的 tool 描述而非字串副本),已在原地註明且註解已於 round 1 修正為正確描述 Swift 語意。


為什麼停在這裡(而不是繼續修)

  1. 契約上限:unattended 模式 2 輪自動修復已用盡(round 1 修、round 2 仍 FAIL)。

  2. 實質上更重要:B1/B2/B3 共同指向同一個未定的設計問題——declaredFont 的契約到底是什麼?
    只接受 OOXML family?也接受 full / PostScript name?本機化名稱在任意語系下如何判定等價?
    量測的 face 要不要跟著 run 的 bold/italic traits 走?

    這正是被跳過的 Plan tier approval gate 該讓人拍板的決定。unattended 下由我單方面裁定,
    等於把同一個 gate 跳過第二次——而第一次跳過的代價已經寫在上面那兩輪裡。

建議的最小修復方向(供決策,非已執行)

  • B1:不要依賴偏好語系的單一 localized name。可解析字型 name 表(CTFontCopyTable(font, kCTFontTableName)
    全部 localized family records,或改為「以 family attribute 反向查詢:desc(requested)
    desc(canonicalFamily) 是否指向同一 PostScript name」——但後者需先確認它不會把 System Font 也放行。
  • B2:只比對 family 命名空間(含全部 family aliases);若要支援 full/PostScript,需分類名稱種類並要求
    唯一 exact candidate,跨命名空間碰撞回 unknown(.ambiguousMatch)
  • B3:改名 resolvedFamily,或同時攜帶 PostScript name / face identity;並決定是否依 run traits 選 face。

Next

不要合併 PR #193 Branch idd/189-feat-checkbox-cjk-emoji-fallback 保留在 073af55
決定上述契約後再跑 /idd-verify #189未 auto-close、未打 verified tag(FAIL 不打)。

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