異體字落地替換 S2:Codes UI 全表串接 - #1272
Merged
Merged
Conversation
執行 docs/CHAR_VARIANT_MAP_TEXT_COLUMN_ROLLOUT_PLAN.md 的 S2。Codes UI 的 5 條寫入路徑 (performStore/performUpdate/performProposalStore/performProposalUpdate/ proposalUpdateExisting)全部掛上落地替換;Blade 與 React 共用 perform*,一次涵蓋兩個入口。 ## 掛鉤與通知 - 替換一律只碰「要寫入的值」,不碰「用來定位既有列的條件」(D7 第四類): performUpdate 的 $conditions 來自 URL 主鍵、在 extractFormData 之前就組好; 提案路徑的主鍵查重則在替換之後,所以查重看到的是替換後的值。 - 成功與**失敗**分支都 flash 通知。失敗分支特別重要:唯一鍵衝突/「未偵測到任何修改內容」 可能是替換自己造成的,而 withInput() 回填的是**替換前**的原始輸入——不附通知的話 使用者會看到「你什麼都沒改」而完全無從理解。 ## D7「兩形並存」的去重(實作時發現,原計畫漏了) `ALTNAME_DATA.c_alt_name_chn` 同時是**文本主鍵成員**又在替換範圍內(strict),而 ALTNAME_DATA 是 Codes UI 可寫的表——這是 Codes UI 裡唯一會踩到 D7 的形狀。 D6 不回溯,所以既有列的主鍵可能還是變體形,只用替換後的值查重會錯過它、鑄出語義重複的 第二列,而資料庫唯一鍵擋不住(不同字形是不同鍵值)——**比不替換更糟**。 正確做法(前兩版都是錯的,記錄下來避免重演): - ❌ 只探「輸入值 + 替換後值」兩形:對照是多對一,既有列可能是**另一個**變體 (既有「菁客」、新輸入「靑客」,兩者都歸一成「青客」)。 - ❌ 列舉所有等價字形再逐一查:查詢次數等於等價字形數,而為避免組合爆炸設的上限 **本身是正確性缺口**(超過就退回只比對正規形=完全失去去重),且可由合法對照表資料 觸發(一個參考字 6 個變體、主鍵含 2 個這樣的字就是 7×7=49)。 - ✅ 把**不在替換範圍內**的主鍵欄固定在 SQL 條件裡取回那一小群候選列,再在 PHP 端把 它們的文本主鍵歸一後比對:精確(不管對照表什麼形狀)、只要一次查詢、無上限可調。 待審提案那一側同理——`hasActiveCreateProposalConflict()` 是拿 resource_id 做完全相等比對, S2 之前留下的 pending 提案帶變體形 resource_id、新提案帶歸一後的 ⇒ 不會衝突 ⇒ 兩筆並存、 依序核准就落成兩種字形的兩筆列。改以 resource_id 的**位置式 LIKE 樣式**收斂 + lazyById() 分批。 兩個陷阱:前導前綴在 production 的 ALTNAME_DATA 會失效(主鍵第一欄就是可替換的文字欄); cursor() 在 PDO MySQL 預設 buffered query 下記憶體並非有界。 ## 其他實質修正 - **缺表降級**:落地替換現在掛在 Codes UI 全部寫入路徑上,若因缺表拋錯會讓**整個代碼表的 寫入功能 500**——為加值功能讓核心錄入停擺是錯的取捨。只在「表不存在」時降級(確定性、 可快取、reset() 會清),其餘錯誤一律往上拋。早期寫法對所有 Throwable 降級並快取空 map, 後果是一次瞬時錯誤就讓整個 process 不再替換,只留一行 warning。 - **新增 VariantMappingException**:QueryException 繼承 PDOException 繼承 RuntimeException, 而 assertWritable() 內部會查表——catch(\RuntimeException) 會把資料庫錯誤當成驗證失敗、 把原始 SQL 顯示給使用者,而且該次寫入被靜默跳過而不是誠實 500。 - **proposalUpdateExisting 的 excludeId 改用權威來源** operation.resource_id,而非使用者 送出的 body id(body 的 id 可能被改、可能是空字串 ⇒ (int) 變 0 ⇒ 不排除舊邊 ⇒ 合法修改 被誤報成環)。 - **getKeyColumns() 的方法內 static cache 改成可重置**:那是既有的測試隔離地雷(測試會為 同一個表名建不同的合成 schema,一旦被快取成錯的主鍵欄,後面的測試就會拿到污染值, 症狀是與該測試無關的「請確認主鍵欄位已填寫完整」)。生產語義不變。 ## 既有測試的調整 CodesCharVariantMapAuditTest 原本把 c_reference_char 設成「新參考字」(4 個字),新 guard 會 擋下多字元對照。改成單一字元——該值本來就不是合法的「參考字」,而該測試的主體是稽核紀錄。 ## 驗證 三輪 review agent + 六輪 codex。實測鑑別力(neuter 對應機制後會紅的測試數): 替換 hook 6/22、通知 5、去重 4、提案側去重 1、定位器 1。 全量 phpunit 2845 tests/15190 assertions 全過;php-cs-fixer(已刪 cache、用 dist config)0 fixes。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
執行
docs/CHAR_VARIANT_MAP_TEXT_COLUMN_ROLLOUT_PLAN.md的 S2。Codes UI 的 5 條寫入路徑全部掛上落地替換;Blade 與 React 共用perform*,一次涵蓋兩個入口。這是第一個真正改變使用者可見行為的步驟 —— 從此透過 Codes UI 寫入約 80 張代碼表時,含這 7 個異體字的文本欄會被歸一化,並 flash 一則非阻塞通知。
掛鉤與通知
替換一律只碰「要寫入的值」,不碰「用來定位既有列的條件」(D7 第四類)。
performUpdate的$conditions來自 URL 主鍵、在extractFormData之前就組好;提案路徑的主鍵查重則在替換之後 —— 所以不存在「查重用替換前值、落庫用替換後值」的錯位。成功與失敗分支都通知。 失敗分支特別重要:唯一鍵衝突、以及「未偵測到任何修改內容」都可能是替換自己造成的,而
withInput()回填的是替換前的原始輸入。不附通知的話,使用者會看到「你什麼都沒改」而完全無從理解 —— 這是本 PR 修掉的最誤導的一條訊息。D7「兩形並存」的去重(原計畫漏了,codex 抓到)
ALTNAME_DATA.c_alt_name_chn同時是文本主鍵成員又在替換範圍內(strict),而ALTNAME_DATA是 Codes UI 可寫的表 —— 這是 Codes UI 裡唯一會踩到 D7 的形狀。D6 不回溯,所以既有列的主鍵可能還是變體形,只用替換後的值查重會錯過它、鑄出語義重複的第二列,而資料庫唯一鍵擋不住(不同字形是不同鍵值)—— 比不替換更糟。前兩版做法都是錯的,記錄下來避免重演:
待審提案那一側同理 ——
hasActiveCreateProposalConflict()是拿resource_id做完全相等比對,S2 之前留下的 pending 提案帶變體形resource_id、新提案帶歸一後的 ⇒ 不會衝突 ⇒ 兩筆並存、依序核准就落成兩種字形的兩筆列。改以resource_id的位置式 LIKE 樣式收斂 +lazyById()分批。兩個陷阱:前導前綴在 production 的ALTNAME_DATA會失效(主鍵第一欄就是可替換的文字欄,而我的測試 fixture 原本用了不同順序所以測不到);cursor()在 PDO MySQL 預設 buffered query 下記憶體並非有界。其他實質修正
reset()會清),其餘錯誤一律往上拋。早期寫法對所有Throwable降級並快取空 map,後果是一次瞬時錯誤就讓整個 process 不再替換,只留一行 warning。VariantMappingException:QueryException → PDOException → RuntimeException,而assertWritable()內部會查表 ——catch (\RuntimeException)會把資料庫錯誤當成驗證失敗、把原始 SQL 顯示給使用者,而且該次寫入被靜默跳過而不是誠實 500。proposalUpdateExisting的excludeId改用權威來源operation.resource_id,而非使用者送出的 body id(body 的 id 可能被改、可能是空字串 ⇒(int)變 0 ⇒ 不排除舊邊 ⇒ 合法修改被誤報成環)。getKeyColumns()的方法內staticcache 改成可重置:那是既有的測試隔離地雷(測試會為同一個表名建不同的合成 schema,一旦被快取成錯的主鍵欄,後面的測試就會拿到污染值,症狀是與該測試無關的「請確認主鍵欄位已填寫完整」)。生產語義不變 —— schema 在同一 process 內不會變。既有測試的調整
CodesCharVariantMapAuditTest原本把c_reference_char設成「新參考字」(4 個字),新 guard 會擋下多字元對照。改成單一字元 —— 該值本來就不是合法的「參考字」,而該測試的主體是稽核紀錄、不是多字元是否放行。驗證
三輪 review agent + 六輪 codex。實測鑑別力(neuter 對應機制後會紅的測試數):替換 hook 6/22、通知 5、去重 4、提案側去重 1、定位器 1。
phpunit:2845 tests / 15190 assertions 全過php-cs-fixer(已刪 cache、用 dist config):0 fixesCodesVariantReplacementTest(22 tests);CharVariantMapServiceRowTest新增缺表降級 2 支無 API 改動(web 表單,非 v2 API);
notices適用範圍擴大是 S9 的項目。🤖 Generated with Claude Code