Imported from timcsy/semorphe (
knowledge/skills/retire-imperative-block/SKILL.md). Install upstream withnpx skills add timcsy/semorphe --skill retire-imperative-block. Copyright stays with the author.
讓一顆命令式的積木定義退場
這把刀先問一句
「這一顆的形狀,宣告表達得完嗎?」
答不出來 → 先看比對報表(npx vitest run tests/integration/audit-block-def-parity.test.ts)。
報表把每一顆的落差逐項印出來:插槽、欄位、output、statement、顏色。
⚠️ 為什麼不能直接刪
命令式那份沒有守衛(Blockly.Blocks['x'] = {...} 直接賦值、後跑)
→ 使用者看到的是它;宣告式那份有守衛且先跑 → 它躺著。
所以「刪掉命令式」=「換成宣告式」——那是一次行為改動,不是清理。
而躺著的那份可能是錯的:spec 164 發現 cpp_func_call 與
cpp_func_call_expression 的宣告完全對調,而使用者看不到。
步驟
1. 看報表,判哪一邊對——用這一顆的語義,不是「哪一份比較新」
⚠️ 兩個方向都有實例:
cpp_break 宣告多了 nextStatement → 命令式對(break 之後是不可達的程式碼)
cpp_var_ref 宣告只有死文字框 → 命令式對(它的下拉跟著工作區長)
cpp_func_call 宣告與另一顆對調 → 兩邊都錯
🔴 我判反過一次(把 cpp_break 寫成「命令式才是錯的」)。
先問「這顆積木在語義上該長怎樣」,再看兩份定義。
2. 補齊對的那一份
宣告錯了 → 修宣告(多數)
宣告表達不完 → 先補【機制】,例如 `field_dynamic_dropdown`、`builder: "variadic"`
⚠️ 補機制是另一件事,可能該是自己的一刀
🔴 「表達不完」要分兩層問(2026-08-26)
缺的是一個【概念】 一種新的組合方式 → 新建構子,自己的一刀
缺的是一個【參數】 既有概念的一個維度沒開 → 三行,同一刀做掉
兩者的成本差一個數量級,而它們在「表達不出來」這句話裡長得一樣。
實例:最後一顆積木被判成「需要一個還不存在的建構子」,而實際差的是
paramList 的三格參數(值的種類/擺法/預設)+ 一格讓存檔沿用原本形狀的。
從那個判斷到查清楚,花了三個 grep。
→ 宣稱「需要新機制」之前,逐條寫出既有機制差在哪一格, 而每一條要指得出檔名與行號。指不出來的那一條,就是還沒查。
不是「刪掉就降」——是「把對的那一份補齊,另一份才變成多餘的」。
2.5 🔴 先確認比對器沒有在指控宣告——也沒有在【不看】它
spec 163→165 之間它四次說「宣告寫錯了」,而四次都是比對器自己缺東西:
膠囊標籤沒載 · src/i18n 沒載 · field_multilinetext 沒註冊 · field_dynamic_dropdown 沒註冊。
第五、六次是同一個形狀(建構子沒接上、altLayout 沒接上)。
🪦 第九次之後這件事機制化了(2026-08-26)
前八次的處置都是「在 beforeAll 再手寫一個樁」,而那份手寫的清單本身就是這個病:
每宣告一個新的下拉來源,那裡就要有人跟著加一行。
一份必須跟著另一份手動更新的清單,就是那個缺陷的形狀,不是它的解藥。
處置:兩條護欄改成 import 真的語言套件(它們在模組層級登記來源), 並新增入口斷言「宣告引用到的每一個下拉來源都登記過——逐個指名」。
⚠️ 同一個病在兩個現場的症狀完全不同,而第二個更難認:
比對護欄 宣告擲例外 → 那顆【從母體消失】 → 報表少一行
audit-dropdown-domain 選項產生器擲例外 → 選項變空 →
「設清單外的值再讀回來」量到【不保值】 → 記成一筆缺陷
後者把量測環境的問題記進了缺陷欄。
比對之前,要先把【產品那側需要的每一樣東西】都備齊;少一樣,比對就會指控宣告。
⚠️ 而它的代價是方向性的:它會讓人以為「宣告式那條路走不通」。
🔴 第七次的失敗模式是新的,而它不會變紅(2026-08-26)
前六次 比對器缺一樣東西 → 【指控宣告】 → 報表上多一行,會紅
第七次 比對器【直接不看】那一類 → 報表上【少】一行,什麼都不會發生
audit-block-def-parity 的建置函式第一行是 if (!def.message0) return null。
而用宣告式建構子的積木本來就沒有 message0(形狀由建構子長出來),
於是它們被歸進「沒有 message0 或建不起來」那一桶——而那一桶不進比對。
一條護欄的能力邊界,如果剛好切掉它要驗的那一類, 它的綠燈就與「沒有人在看」等價。
⚠️ 而這一類正是這把刀的產物:每退一顆命令式定義,就多一顆「只有建構子」的宣告。 也就是說:這把刀用得越成功,那條護欄看得到的就越少。
→ 機制(散文擋不住第七次,build-guardrail 的教訓):那條護欄要有一支
入口斷言——「用建構子的宣告有 N 顆,而它們必須全部進得了比對」。
N 錨在合成量(宣告了 builder/branchList/paramList 的顆數),
而它不會因為缺陷被修好而變小。
3. 比對護欄說「一模一樣」了,才刪
🔴 先問:這個差異,是不是這一刀的目的?
這道閘假設「宣告是對的,只是躺著」。而第 1 步允許另一種判:命令式那份錯了。 那時宣告與它本來就不該一樣,閘永遠過不了——
差異不是目的 → 閘照舊:比對說一模一樣了才刪
差異【就是】目的 → 那不是「比對沒過」,是**一次行為改動**
(這把刀開頭那句話已經說了:「刪掉命令式=換成宣告式」)
差異是目的時,驗收換成三樣(缺一不可):
- 把差異寫下來——哪一格、為什麼那一邊才對。寫不出來就是還沒判清楚。
- 對照組:來回轉換逐字相同(
declaration-change-parity)+ 全套綠。 🔴 2026-08-26 抓到cin >> a;來回變成cin >> x;的就是它,不是型別檢查。 - 開瀏覽器,並且釘死環境(第 5 步)。
⚠️ 而「差異是目的」不是萬用鑰匙:能修到一樣的就要修到一樣。
2026-08-26 有一顆的差異只是啞輸入的名字(HEAD vs FORMAT_ROW)
——那種要讓建構子能指定,不是拿這條出口繞過去。
⚠️ 而「一模一樣」只在這條護欄看得到的範圍內成立
cpp_raw_code 比對通過而它不能刪:loadExtraState 會依 degradationCause
換顏色與 tooltip,而比對只比「剛建好的樣子」。
刪之前多問一句:「它有沒有一段只在別的時機才跑的邏輯?」
—— loadExtraState、onchange、mutator 都是那種時機。
🪦 而它 2026-08-26 刪掉了——問法要再往前一步
它卡了三輪,而解法不是「讓宣告表達得出依 extraState 換視覺」。 拆開來看那是兩件事,而兩件都不該由那顆積木做:
套用視覺 面板本來就對【每一顆】積木在做(降級/信心兩段都在那裡)
——只有「認不得的那一段」留在積木自己身上
保存狀態 Blockly 對【沒有 save/load 鉤子的積木不留 extraState】(實測 `{}`)
——那是一個藏著的缺陷,不是這顆積木的需求
一個機制搬家的時候,留在原地的那一半會看起來像「它需要特別處理」。
→ 所以上面那句「它有沒有一段只在別的時機才跑的邏輯」問完之後, 再問一句「那段邏輯該由它做嗎」。多半不該——而那時刪它是便宜的。
🟢 loadExtraState 那一半,護欄已經會問了(spec 166 的第五維)
——它比命令式 saveExtraState 真的會吐出的鍵與宣告表達得出的鍵。
⚠️ 而 onchange/mutator 還是要人問。
刪除時留一個墓碑註解(🪦 …已於 spec N 刪除,理由是…)——
下一個人會問「這裡本來有東西嗎」。
4. 順手檢查:注入契約裡有沒有變成孤兒的欄位
setLanguageInputNames 的 arrayAccess(163)與 returnBlock(164)
都是唯一消費者退場後留下的孤兒。
一個沒有消費者的注入欄位,會讓組裝點以為它還要提供那份資料 ——而那份資料從此沒有人驗。
4.5 🔴 瀏覽器裡「少了東西」時,先問語義樹裡有沒有
2026-08-26:實測看到 vector<int> v = {3,1,4} 只剩一顆「宣告 int 變數 v」,
我用 git stash 回 HEAD 確認「不是這一刀造成的」(那一步是對的),
然後把它記成資料遺失。
它不是。sync-controller 在渲染前呼叫 downgradeComponentsForLevel
——app 手上的語義樹是完整的(P4:「這是過濾,不是簡化」)。
git stash答的是「是不是我造成的」,答不了「這是不是一個缺陷」。
→ 一行就答得完,而且比 stash 快:
const ids = []; const walk = n => { if(!n) return; ids.push(n.componentId)
for (const ks of Object.values(n.children ?? {})) for (const k of ks) walk(k) }
walk(window.__app.currentTree); console.log([...new Set(ids)])
樹裡有 → 投影在過濾,不是遺失。
⚠️ 而降級規則有例外,那個例外會讓人否掉正確的假設:不是「不可見就降」,
是「不可見 + 自己宣告了 abstractComponent」才降。
於是同一次實測裡 int a[3](abstractComponent: null)沒有被降,
而它與被降的那顆同樣不在啟用層級裡。
5. 🔴 開瀏覽器看——沒有任何測試在看標籤
工具箱快照只比 id 與順序。所以:
① 明確切到那顆積木所屬的目標 ← ⚠️ 不釘死環境的話會誤判(見下)
② 貼一段真的會用到它的程式碼
③ 看【標籤文字】與【欄位型別】 ← 下拉要真的還是下拉,不是文字框
④ 切一次 Block Style ← CLAUDE.md:JSON 名稱錯誤只在這裡暴露
⑤ 截圖
⚠️ 量測本身最容易出錯的兩處(各踩過一次):
- 環境沒釘死:目標停在別的語言時,整段會降級成
raw_code——症狀看起來像「你把它弄壞了」 - 拿著舊參照:切 Block Style 會重建工作區,
切換前存的
ws之後量到的是一個已經不存在的東西 → 寫成snap()每次現查
6. 基線是三份,一起更新並註明原因
tests/baselines/block-def-parity.json 落差數 + hollowDeclaration
tests/baselines/neutrality.json 第二維(積木型別)
tests/baselines/dual-truth.json 兩處都有定義的數量
7. 🔴 落差歸零那天,先確認母體沒有縮水
2026-08-26 報表說「一模一樣 0 顆/有差異 0 顆」,而檔案裡還有 10 顆命令式定義
——其中 7 顆兩份定義都有,而它們的宣告是空殼(連 message0 都沒有),
於是比對器把它們略過。
「有差異 0 顆」不等於「清乾淨了」——那幾顆是從母體裡消失的, 而消失與沒問題長得一模一樣。
block-def-parity.json 的 hollowDeclaration 逐顆指名它們。
⚠️ 兩個數字必須一起看:一顆的宣告從「有內容但不一樣」退成「空殼」,
會讓 differ 下降而看起來像進步。
7.5 ⚠️ 那句「因為機制表達不出來」的註解,不會自己過期
cpp_array_declare/cpp_array_2d_declare/cpp_vector_declare 三顆的
_why 都寫著
「改成命令式註冊,因為初始值需要動態插槽, 而宣告式的
args0只描述固定欄位。」
而那個限制 2026-08-22 就沒有了——attachVariadic 就是為
「固定前段(args0)+ 可變尾段」而在。三顆各躺了四天。
一句「因為機制表達不出來」的註解,不會在機制補上的那天自己更新。
→ 動手前把那句話當成過期的假設去查,不是當成理由讀。 判準一句話:那個機制今天還缺嗎?
8. ⚠️ 這顆積木的形狀,是不是別的地方在靠它分辨事情
cpp_var_assign 的 NAME 下拉退場(左值接點化)之後,
「讀的下拉列變數 ∪ 板子常數、寫的只列變數」這個區別失去了它的載體
——賦值的左邊現在裝的就是一顆讀取用的積木,於是學生選得到 HIGH = 5。
5749 支單元測試全綠,抓到它的是三天前寫的 e2e。
一個靠「你是哪一種積木」成立的區別, 在那兩種積木合而為一的那天會安靜地消失。
→ 併掉一顆積木的形狀之前,grep 一次它的型別名與欄位名:
有人拿它當判準嗎?有 → 那個判準要先搬到別的載體上(多半是位置,
而位置的意思通常已經在 traits 裡宣告過了)。
🪦 這把刀 2026-08-26 用完了
block-registrar.ts 的 Blockly.Blocks[...] = { 歸零(history/163)。
它仍然留著——下一個語言、下一個宿主都會再長出命令式定義,而那時
這裡記的每一條都還成立。
⚠️ 而它的兩條護欄(第七條雙重真相、比對護欄)母體現在是空的 ——而那不是退休的理由:
母體空掉的護欄,正是唯一擋得住它再長回來的東西。
build-guardrail 那句「母體永遠是空的護欄 ≒ 沒有人跑的護欄」講的是
量既有的債;而這兩條現在不是溫度計,是閘門。
🟢 兩條的棘輪 2026-08-26 改成硬性零(共四支,逐顆指名)。
明確否決
| 做法 | 為什麼不行 |
|---|---|
| 把宣告改成跟命令式一樣,讓比對變綠 | 那是假設命令式是對的——而它有時候是錯的 |
| 上調棘輪去容納自己的實作 | 同一件事的鏡像:前者改宣告迎合量測,後者改量測迎合宣告。⚠️ 分界在於那個上調記錄的是世界變了,還是我的實作需要它變 |
| 一次刪好幾顆 | 紅了無法歸因;而每一顆的判都不一樣 |
| 只跑測試不開瀏覽器 | 沒有任何測試在看標籤 |
| 直接刪「形狀差很多」的那幾顆 | 它們的宣告真的表達不完,刪了會壞 |
相關
tests/integration/audit-block-def-parity.test.ts— 比對報表knowledge/history/114(落差第一次被量出來)、history/113(第二維第一次下降)knowledge/concepts/宣告登記處.md— 補機制時多半是加一個登記處