Skip to content

把「新增文本錄入必須掛異體字落地替換」寫成常規並機械化把關(S8) - #1278

Merged
sudoghut merged 1 commit into
developfrom
feat/variant-replace-guardrails
Aug 22, 2026
Merged

把「新增文本錄入必須掛異體字落地替換」寫成常規並機械化把關(S8)#1278
sudoghut merged 1 commit into
developfrom
feat/variant-replace-guardrails

Conversation

@sudoghut

Copy link
Copy Markdown
Member

這是什麼

docs/CHAR_VARIANT_MAP_TEXT_COLUMN_ROLLOUT_PLAN.mdS8:把「新增文本錄入必須掛落地替換」寫成常規,並補上會在漏掉時發出聲音的機械化把關。

沒有行為改動:只有規則文件、skill、一支新測試,加一處 docblock 修正。

為什麼需要

第二階段之所以漏掉 19 個 handler,正是因為當時沒有任何機制會在漏掉時發出聲音。本階段價值的一半在「以後不會再漏」。

內容

規則

  • AGENTS.md 新增 §1.3,與 §1.1(外鍵 RESTRICT)/§1.2(稽核欄)並列為資料完整性規則。含兩個常被忽略的邊界:繼承基底但自己另寫副表/鏡像的 handler、沒有 tableName() 的 handler 必須顯式傳表名(省略是 runtime fatal 而非靜態錯誤)。
  • 「提交前最低檢查」補一行。
  • .claude/skills/mutation-api-record-editing.md:掛哪個 hook、順序為什麼是硬性要求、通知怎麼接、測試該斷言哪 8 件事(含鑑別力)。
  • .claude/skills/database-schema.md:新增文本欄自動進入範圍;新增對照/映射表必須加進排除清單。

機械化把關 tests/Unit/VariantReplaceHookCoverageTest.php(10 支測試)

  • 每一個 handler 都必須掛(或繼承)替換 trait,或列在清冊。刻意不先判斷「有沒有寫入」——現存 8 個非基底 handler 裡有 4 個把寫入委派給 repository/service,自己檔案裡沒有任何寫入慣用法;以「有寫入才要求登記」為前提會 fail-open。
  • 三本清冊都有機械檢查:EXEMPT(要寫理由)、EXEMPT_DELEGATES(指名下層檔案與掛鉤數)、SUBCLASS_EXTRA_WRITES。已掛 trait 的類別不可列進前兩本(殭屍豁免會讓它從此不受檢查)。
  • 基底不只要 use,還要真的呼叫三個 trait 方法。
  • 逐檔記數鎖住體系外的 12 個掛鉤點,掛鉤變少也會紅。
  • 比對走 PHP tokenizer:註解與字串字面值都不算,且只認 ->method(,同名靜態呼叫無法冒充。
  • 目錄內非 handler 檔案若自己落庫也要交代——這條當場抓出 Concerns/RecordsAiFillSubmissionai_fill_logs(紀錄類表,確認不替換後登記)。
  • 偵測邊界誠實寫在 docblock 與 AGENTS.md:目錄外新開的檔案、以及委派給下層方法的鏡像同步偵測不到。

驗證

  • 鑑別力逐項實測會紅:中和基底呼叫、拿掉下層掛鉤、掛鉤數減少、字串字面值冒充、同名靜態呼叫冒充、新增未登記 handler、把寫入搬進目錄內 helper、殭屍豁免、未登記的子類額外寫入。
  • 全量測試:2955 tests / 15539 assertions 綠。
  • php-cs-fixer clean(0 of 675)。
  • review agent + codex 三輪,HIGH/MEDIUM 全數處理完畢。

🤖 Generated with Claude Code

第二階段之所以漏掉 19 個 handler,是因為當時沒有任何機制會在漏掉時發出聲音。本次補上規則
與機械化把關,讓繞過掛鉤的新寫入路徑在測試階段就會紅。

規則文件:
- `AGENTS.md` 新增 §1.3(與 §1.1/§1.2 並列的資料完整性規則):範圍由欄位型別決定、未知表
  fail-closed、預設 lenient、掛鉤必須早於 PK 計算/查重/拼音派生/`resource_id` 組裝、
  精確比對必須兩形都探、通知(含 409/422)要掛、不該替換的地方一律不掛。
  另補兩個常被忽略的邊界:繼承基底但自己另寫副表/鏡像的 handler、以及沒有 `tableName()`
  的 handler 必須顯式傳表名(否則是 runtime fatal)。
- 「提交前最低檢查」補一行。
- `.claude/skills/mutation-api-record-editing.md` 補實作步驟與踩過的坑(掛哪個 hook、順序
  為什麼是硬性要求、通知怎麼接、測試該斷言哪 8 件事含鑑別力)。
- `.claude/skills/database-schema.md` 補交叉引用:新增文本欄自動進入範圍;新增對照/映射表
  必須加進排除清單。

機械化把關 `tests/Unit/VariantReplaceHookCoverageTest.php`(10 支測試):
- `app/Services/Mutations` 下**每一個** handler 都必須掛(或繼承)替換 trait,或列在例外
  清冊。刻意不先判斷「有沒有寫入」——現存 8 個非基底 handler 裡有 4 個把寫入委派給
  repository/service,自己檔案裡沒有任何寫入慣用法,以「有寫入才要求登記」為前提會 fail-open。
- 三本清冊各有機械檢查:`EXEMPT`(要寫理由)、`EXEMPT_DELEGATES`(指名下層檔案**與掛鉤數**)、
  `SUBCLASS_EXTRA_WRITES`(繼承基底但自己另有寫入)。已掛 trait 的類別不可列進前兩本
  (殭屍豁免會讓它從此不受檢查)。
- 基底不只要 `use`,還要**真的呼叫** `applyVariantReplacement()`/`resetVariantReplaced()`/
  `withVariantNotices()`——刪掉呼叫比刪 `use` 容易得多,後果一樣(21 個子類靜默失去替換)。
- 逐檔記數鎖住 handler 體系之外的 12 個掛鉤點(controller/repository/import service),
  掛鉤變少也會紅。
- 比對走 PHP tokenizer 而非正則:註解與字串字面值都不算,且只認 `->method(` 形式,
  同名靜態呼叫無法冒充。
- 目錄內非 handler 檔案若自己落庫也要交代(這條當場抓出 `Concerns/RecordsAiFillSubmission`
  寫 `ai_fill_logs`,屬紀錄類表、確認不替換後登記)。
- 偵測邊界誠實寫在 docblock 與 AGENTS.md:目錄外新開檔案、以及委派給下層方法的鏡像同步
  偵測不到,靠 review 與人工判斷。

鑑別力已逐項實測會紅:中和基底呼叫、拿掉下層掛鉤、掛鉤數減少、用字串字面值冒充掛鉤、
用同名靜態呼叫冒充、新增未登記的 handler、把寫入搬進目錄內的 helper、殭屍豁免、
未登記的子類額外寫入。

順帶修正 `AppliesVariantReplacement` 的 docblock:顯式傳表名的是四個 handler,不是三個。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sudoghut
sudoghut merged commit b0dde59 into develop Aug 22, 2026
6 checks passed
@sudoghut
sudoghut deleted the feat/variant-replace-guardrails branch August 22, 2026 00:32
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