規範指定位置的時候說的是那裡要交付什麼:每條跨篇路由要交代目的地會給什麼、每張卡要有角色與何時讀的起手段、每份文件開頭要交代適用範圍。規範不說那裡要長什麼樣,因為形態本來就該隨內容變。

而生成端會替它補上一個形態,並且整批補同一個。補完之後這個位置在每一篇裡都合規、都交付了規範要的東西,而讀者連讀十幾篇時拿到的是同一段話重複十幾次。

同骨化與合規在這個位置上無法區分,因為它們是同一件事的兩面——規範說了要交付什麼,所以形態重複沒有違反任何一條規則。

失效的形態

全批同段名、同句型

一個 21 篇的教學分類,21 篇全部以同一個段名收尾,句型逐篇相同:粗體的「把 A 換成 B」起手、破折號後複述一次讀者剛讀完的東西、接一個連結、再用一句話寫目的地給什麼。佔全文行數的百分之七到十六,而它是每一篇最後留下的印象。

規範那一條要求的是「每條路由都要交代目的地會給什麼」。21 篇全部做到了。

上一個實例落在另一個位置,機制相同

同一個分類的 22 個 description 曾被量到全數寫成觸發條件式,記在把 description 寫成觸發條件。那次的歸因是「因為它照規範寫」——規範當時只規定了那個欄位要回答什麼,沒有規定教材與查閱型內容的體例不同。

兩次的位置不同、規範條文不同,而路徑一樣:規定交付 → 生成端補形態 → 整批同形 → 逐篇檢查全綠。

規則沒寫作用單位時,會在它沒打算管的那個單位上也生效

同族的第二種形態換一個位置:規則自己的作用單位沒有寫明。

一條「每段首句先說這個概念是什麼」的規則,作用單位是段落。而文章的第一段也是一個段落,於是它在那裡同樣生效,產出的是每一篇的第一句都是結論。同一份規範另有一條寫著未經推導的開頭結論要抽掉——兩條規則在同一個位置給出相反的指示,而各自單獨讀都通過。

實測:那 21 篇的首句全部是結論句。

為什麼既有的審查掃不到

多輪審查有一個專掃同骨化的 frame,它的對象正是一批三篇以上的稿件。它跑過了。

掃到重複結構之後,reviewer 的下一個動作是回去對照規範,而規範明文要求這個位置要有東西。「這個重複是規範要求的」於是成為豁免理由,而規範要求的是內容不是形態。 判定停在這裡,重複不進 finding。

三件事讓它更難現形:

  • 逐篇讀不會顯形。 那個段落在每一篇裡只出現一次,單篇冷讀、單篇合規檢查、單篇的理解探針全部通過。
  • 內容確實不同。 21 篇各自連到不同的目的地、各自複述自己那一篇的內容,逐字比對不會相同。相同的是骨架。
  • 零 finding 與沒有問題同訊號,而這正是檢查規則的作用域要顯式列舉那一類——沒有任何一條規則說「形態不可以整批相同」。

判讀方式

一句話:規範在這個位置要求的是「要交付什麼」還是「要長什麼樣」。

要求的是交付,而產出全批同形時,那個同形沒有任何規則背書——它是生成端的省力路徑,不是合規的結果。

偵測要把同一批的那個段落抽出來並排,逐篇讀不會顯形:

1# 先普查段名:<目錄> 換成要看的那一批,<段名> 之後由這一步的輸出決定
2# -I 是不印檔名(rg 的 -h 是 help);LC_ALL=C 不可省,
3# 少了它 sort 與 uniq 會把不同的中文段名視為等值而全部併成一組
4LC_ALL=C rg -I --max-depth 1 -g '*.md' -g '!_index.md' '^## ' <目錄> | LC_ALL=C sort | LC_ALL=C uniq -c | LC_ALL=C sort -rn | head
5
6# 再抽出可疑的那一個段落,連段名與後續句型一起看
7rg -A 6 '^## <上一步命中的段名>' <目錄>/

第一條的判讀是計數接近檔案數的那幾個段名。量測當下該分類的輸出是一個段名出現 21 次、其餘每個段名各出現一次,而該目錄當時正好有 21 篇——落差這麼大的時候不必訂門檻,看第一列就夠。落差小的時候(7 比 12 這一類)這個啟發式不給答案,要改用第二條指令逐段讀句型。正文的篇數在五篇以下時它實質上一律不給答案——一次四個小目錄的實測,比值全部落在 1.5 到 2.5 之間(3 比 2、4 比 2、5 比 2),其中一個目錄的第一名還是平手。成因是天花板與雜訊底同量級:三篇的目錄裡,每篇都寫的段名上限是 3,而兩篇碰巧同名的雜訊是 2。

三個前置步驟決定這個普查的分母,少一個就會漏。

第一個是作用域:要涵蓋整批,不能只掃新增的部分。 同骨化是跨篇現象,而新增的那幾篇之間可以完全不同形——把分母縮成新檔就把要量的東西整個排除掉了。

第二個是作用域不能跨定位。 普查的分母要是同一種定位底下的正文——文章與知識卡是兩個族群,各自有不同的規範背書,混在一起數會讓判定翻面。一次實測:同一個目錄非遞迴掃 24 檔得到「21 對其餘全 1」,判同骨化;遞迴掃 45 檔(收進知識卡子目錄)得到「21、20、11、9」,21 比 45 不接近檔案數,判沒有同骨化——而那個 20 是知識卡被規範指定的段名,它一致是合法的。兩條指令的範圍要寫成同一個,並排除目錄頁(成員表不是正文,N 小的時候它是可觀的污染:四篇的目錄裡它佔四分之一;各家靜態網站產生器給目錄頁的檔名不同,換專案要換掉下面那個排除樣式):

1# --max-depth 1 讓它只掃這一層,不遞迴進子目錄;每一個定位跑一次,不要合併
2LC_ALL=C rg -I --max-depth 1 -g '*.md' -g '!_index.md' '^## ' <目錄> | ...

同一條指令指向那個實測目錄的文章層得到「21 比 1」,指向它的知識卡子目錄得到「20 比 11 比 9」——兩個族群各自都有自己的判定,合併之後兩個都讀不出來。

第三個是正規化:近似段名要先歸成一組再計數。 普查鍵在逐字相同的段名上,所以改了段名而保留形態的新實例會逃過它:一次實測裡新寫的那一篇把段名從「換掉其中一項就走到另一篇」改成只差三個字的另一個名字,而句型逐字同構,普查因此報成 21 比 1,看起來那個新段名是獨一無二的。正規化之後才是 22 比 0。做法是把只差幾個字的段名歸成一組再數:

1# 先看全部段名,肉眼找出只差幾個字的那幾組
2LC_ALL=C rg -I --max-depth 1 -g '*.md' -g '!_index.md' '^## ' <目錄> | LC_ALL=C sort -u
3
4# 再把那幾組用 sed 歸一之後重數。<變體一> / <變體二> 換成上一步看到的那幾個段名,
5# 每多一個變體就多一條 s/// 規則;<鍵> 是隨便取的一個歸一後的名字
6LC_ALL=C rg -I --max-depth 1 -g '*.md' -g '!_index.md' '^## ' <目錄> \
7  | LC_ALL=C sed 's/<變體一>/<鍵>/;s/<變體二>/<鍵>/' \
8  | LC_ALL=C sort | LC_ALL=C uniq -c | LC_ALL=C sort -rn | head

對規則本身另問一句:這條規則的作用單位是什麼,它有沒有寫在規則裡。 沒寫的時候它會在每一個符合字面的位置生效,包含規則沒打算管的那一個。

修法

規範側:指定一個位置要交付什麼的時候,同一段寫明「這裡不規定形態」,並給至少兩種明顯不同的形態當範例。只給一個範例等於指定了形態,而給零個範例會讓生成端自己挑一個並整批沿用。

規則側:每一條會被反覆套用的規則寫出它的作用單位(一句、一段、一節、一篇、一批),以及它不管哪一個單位。並查同一個位置有沒有第二條規則在管——失效的形態是兩條規則在同一位置給出相反指示,所以只宣告單位修不好它。找到第二條之後在兩條裡各寫一句指向對方,而引述對方的內容時逐字取原文:一次實測裡,執行者照這一條補上了單位宣告與對另一條的指向,而引述時寫進了那一條沒有的一句話,並且用了第三條規則明文禁止的那個詞。

審查側:把「這個重複是規範要求的」從豁免理由改成加強掃描的訊號。規範要求的位置是同骨化最容易發生的位置,理由正是它有一條規則保證每一篇都會有它。

動手之前先查規範與規範裡的正面範例,有沒有連位置一起指定。 範例做的事與規定相同:一次全批處置找到的兩個源頭,一個明文規定「收在末節」,另一個沒有規定、而把模具寫成正面範例。位置指定本身就讓連結離開變因所在的那一句、集中到文末,形態的同一是它的後果之一。所以逐篇處置時,先對每一條連結問「它的變因在正文哪一句」——那一次處置裡,末節的條目絕大多數在正文找得到本來該在的那一句,另有相當一部分是正文既有連結的重複(多半是正文裡一個不交代目的地的裸連結,末節那條才交代了目的地給什麼;處置是把交代補回正文、刪掉末節那條)。搬回那一句旁邊時,接在那一段論證的結論之後,不插進推導中間:同一批的下一輪審查量到,插在推導中途的連結把前後句的指代與「因此」接到了它身上。

處置不是刪掉那個段落,那會讓規範要的交付整批消失。動的是形態:有的篇適合寫成一段散文、有的適合兩三句、有的本來就只有一個去處而不需要清單。而不能整批套同一種新形態——判定看骨架,不看位置類別:段名正規化後沒有一個的計數接近篇數、連結前後的接續語沒有一種佔全批連結的一半以上、連到同一個目的地的各篇交代句不共用同一個命題,三項都成立才算。只在位置類別上分散(這篇回正文、那篇留短節)不算;把幾種形態輪流分派給各篇,同樣是用新模具換舊模具(同一批實測過一次:為修「首句是結論」而套的同一組改法,讓三篇的開場變成同構)。

處置的完成條件是同形的那幾篇全部換過形態,而處置中的報告要列出仍未處置的篇數與篇名——改掉一篇不改變任何讀者的體驗(模具的傷害是連讀十幾篇拿到同一段話重複十幾次),而少了這條完成條件,文字允許在「改了 1/22」的狀態下結案。一次實測就是這樣:新寫的那一篇換了形態、被記成「處置生效」,而存量 21 篇逐篇數過,全部仍是三到六條的粗體條目模具。結案的痕跡是四項輸出,貼進 commit message 或 work-log:段名全清單、正規化後的重數、句型比對,以及子句層的量測。只附段名普查不夠,改了段名而保留形態的實例會通過它。

子句層的量測要換一個粒度:段落拿掉之後,同一個模具在一次全批處置裡連續兩輪在子句層重現——交代目的地的子句(「那一篇寫……」)在同一批出現十一次,下一輪又量到相鄰兩篇收在逐字相同的一句上、多篇連到同一個目的地時各寫一句同形的交代。量法三步,範圍是處置涉及的全部篇的全文,不是新增行(新增行在版本歷史裡重算不出來):把每一條站內連結緊接前後的幾個字抽出來計數,分母是全部連結;把連到同一個目的地的各篇交代句並排,比它們交付的命題;把相鄰篇的末句逐字比對。後兩步量不到接續語,所以要分開做。

論述基礎與限制

證據是三個分類、單一生成端、單次判定。 同一個段尾模具實測出現在 32 篇、三個分類(一個語言層教材分類 21 篇、一個料理短文分類 10 篇、一份方法論記錄 1 篇),三邊的句型逐字同構——所以「批」的定義是那個形態出現的全部位置,不是一個目錄。 21 篇的段名與首句是實測(指令可重跑),而「規範要求的位置是同骨化最容易發生的位置」這個比較級沒有對照組——本卡沒有量過「非規範要求的位置」的同形率,所以那一句是推導不是觀測。

「至少兩種範例」這個數字沒有支撐。 修法裡「只給一個範例等於指定了形態」有本批的證據(生成端確實照單一範例整批沿用),而「兩種就夠」是推論。

量化訊號(計數接近檔案數)只在落差大的時候給得出答案。 上面那個 21 比 1 的實例落差是一個量級,判讀不需要門檻;落差小的情形本卡沒有量過。

本卡寫下的適用邊界對引用處沒有強制力——「沒有效力」不等於「不該寫」,上面那幾條就是本卡在寫自己的射程。所以引用本卡的位置要各自再寫一次它在那個場景下的邊界(這一條的推導見下面與 description 那一張卡的關係)。

跟其他抽象層原則的關係

  • #308 把 description 寫成觸發條件,文章就變成那個問題的答案——同一個機制在 description 這個位置的實例,那一張記了「因為它照規範寫」而沒有把機制抽出來;本卡是它的一般形態,而它另外貢獻了一條可以合用的推論:一張卡的射程限制放在它自己身上沒有效力,要由引用它的規範寫出適用邊界。
  • #302 定位決定體例,先於任何規則生效——那裡是規則的觸發條件先於它自己的判斷標準生效(為 A 設計的規則套到 B 上而形狀對得上),這裡是規則規定了交付而沒有規定形態。兩者都是規則在它沒打算管的地方生效,分辨方式是問規則漏掉的是適用對象還是作用單位。
  • #254 寫給帶問題來的讀者、不是要被吸引的聽眾——那一張明文擋掉未經推導的開頭結論,而首句是結論的形態仍然在 21 篇裡全數出現;成因是另一條規則的作用單位涵蓋了同一個位置,所以一條規則寫對了不保證那個位置被守住。
  • #221 檢查規則的作用域要顯式列舉——零 finding 與沒有問題同訊號,這裡的特殊之處是 frame 有跑、規則也有,而判定被一個看起來正當的豁免理由收掉了。
  • #126 寫作 review 是多軸完整性——同骨化屬於 cadence 這一軸,而這一軸跑過了仍然漏掉,所以修法不在加輪數也不在加軸,在改那一軸的豁免規則。

Case

一個 SQL 教學分類,21 篇正文。全數以同一個段名收尾、句型逐篇相同,佔全文百分之七到十六;全數以結論句開頭。

這個分類跑過多輪審查(分類目錄頁記著其中一輪的 outbound frame 產出了五項 backlog),而這兩件事都沒有進 finding。首句那一件由 docs/rules/writing.md〈段落的第一句寫這一段的主題〉(舊版的原則一)要求每段首句給核心原則,段尾那一件由另一條規範要求那個位置要有內容。

判定由使用者在連讀整個分類之後給出,他的用詞是每篇文章不夠有序、不夠有脈絡——那是全批同形在讀者端的表現,而它在任何單篇上都不存在。

這張卡立起來之後,同一批新寫的文章當場再犯一次,而且是本卡最需要記下來的那一種。 新寫的一篇把段名改成「⋯⋯走到別處」(與原本的「⋯⋯走到另一篇」差三個字)而句型逐字同構——改了段名、保留了形態,正好與本卡的處置相反。它因此同時證明了兩件事:處置寫成「動的是形態」是必要的(否則最省力的做法就是改段名),以及上面那個普查少了正規化這一步。抓到它的是跨篇一致性的審查,不是段名普查。

後續:那一篇後來照本卡的處置改了形態(四條粗體模具換成三段散文),於是「走到別處」這個段名在該庫裡不再存在。這留下一個本卡自己要記的副作用——處置生效之後,記錄這個缺陷的實例本身消失了,而上面那段 sed 的範例如果寫死那個字面值就會變成一條零命中的指令。所以現行指令裡的變體一律寫成佔位符,歷史事實留在這一段。

處置完成:32 篇在 2026-09-23 全數換過形態,兩個分類目錄的段名普查回到「各 1」,方法論記錄那一篇的模具段名已移除(它只有一篇帶模具,普查本來分不出來)。處置時查到規範側的源頭有兩個,而且形態不同:料理分類的目錄頁明文寫著「跟別篇的關係全部收在末節,寫成『換掉其中一項就走到另一篇』」,段名與位置一起規定;方法論記錄沒有規定形態,而它把「在末節開頭交代可動項、再逐項寫換掉會走到哪一篇」寫成正面範例——範例在這裡做的事與規定相同。兩處都改成只規定交付,並附由可動項位置決定的幾種形態(目錄頁三種、方法論記錄四種)。

逐篇拆開之後浮現一件本卡原本沒記的事:末節裡有相當一部分條目是正文既有連結的重複——32 篇裡有 21 篇的末節有一條以上指向的目的地,正文裡早已連過,分布是 SQL 18/21、方法論記錄 1/1、料理 2/10(計法:以段名為界把每篇切成正文與末節,各取連結目的地求交集;相對路徑依頁面網址正規化之後同數)。正文那一條多半是裸的「見 1.6」,末節那條才交代了目的地給什麼。其餘的條目,絕大多數在正文裡找得到它本來該在的那一句:SQL.5 講一位顧客變成兩列的那一句、料理的派皮布丁塔講澱粉的那一段、SQL.12 開場的範圍宣告。所以位置的規定(「收在末節」)本身就讓連結離開它的變因、集中到文末,形態的同一只是它的一個後果——兩個分類離開的方式不同:料理有明文規定,連結整條離開正文;SQL 的正文多半留著裸連結,離開的是「目的地給什麼」那一句交代。末節當初也有它的理由,那是 #303 的處置(講鄰居的篇幅佔掉本篇的位置),拆回正文就重新承擔那個風險:一次量測裡料理各篇講鄰居的段落佔全文的比例前後沒有下降,只換了位置,所以驗收要重跑 #303 那一題「這一篇讓你具備什麼能力」。處置後的形態分布因此偏向「回到正文的變因旁邊」,另有幾篇保留一個承載結論的短節(一段論證、一個有界例外、一支的收束與交接),各篇依內容決定。

處置本身又製造了一次同形,換到了子句層:段落拿掉之後,交代目的地的那個子句「那一篇寫……」在料理十篇的新增行裡出現了十一次(提交前在工作區量的,版本歷史裡重算不出來)——同一個模具在子句層重現。處置之後的第一輪審查另外量到兩種:相鄰兩篇收在逐字相同的一句上(SQL.12 與 SQL.13 都以同一句連到 SQL.17),以及多篇連到同一個目的地時各寫一句形狀相同的交代(料理九篇連到蛋液凝凍那一頁的句子)。它被抓到是因為處置時另外量了新增行裡的重複子句,不是因為段名普查(段名普查對子句層沒有射程)。所以本卡的偵測在處置之後要換一個粒度再跑一次,量法寫在修法段的子句層量測。這個觀察的射程是一批文章、同一個生成端、連續兩輪,沒有跨批的對照。