NEO.K / MSSP FIELD MANUAL05 · 2026-08-20
v1.x 開發中

MSSP 開發區

這一頁記錄方法本身的狀態。前四個模組講怎麼用 MSSP;這一頁講 MSSP 自己還缺什麼、以及下一版要往哪走。

版本:MSSP 1.x。條目會隨著範例與考古累積增減,每一條都盡量指得出是哪個實作得到的。


設計優點

1. 判斷準則可以在寫程式的當下使用

「移除它之後系統還是不是它」和「能不能在最小核心下單獨測試」是兩個可以在編輯器裡當場回答的問題。它們不需要先畫架構圖、不需要開會、不需要理解整套理論。

這是 MSSP 跟多數架構方法最實際的差別:多數方法給你一組名詞,MSSP 給你兩個可以立刻執行的動作。

2. 結構主張可以被機器驗證

「沒有任何 TMS 引用兄弟 TMS」不是一句期許,它是一個 grep。這個網站的 build 會執行它,違規時指出確切檔案並拒絕發佈。

大部分架構規範只存在於文件裡,於是它們在第三個月就開始漂移。能被機械檢查的規則才會活過交接。

3. 知識與權限分離的效益比預期大

原本以為這只是安全性設計。考古 001 顯示它同時解決了三個看起來無關的問題:可測試性(不會被受測對象殺掉行程)、可嵌入性(能放進長時間執行的宿主)、可在地化(錯誤訊息不再寫死在效果發生的那一行)。

一條界線同時解掉三個問題,通常代表這條界線本來就在那裡。

4. 孤島測試把「模組獨立」變成會失敗的斷言

宣稱一個模組獨立很容易。載入最小核心與恰好一個模組、stub 掉所有工具、然後跑它的代表任務——這個做不到就是做不到。

它抓到的通常不是設計錯誤,而是幾層之下透過共用 import 抵達的依賴,那種東西讀程式碼看不出來。

5. 「度」讓方法可以被部分採用

FMS + SMS + TMS 就是一個完整的中型 MSSP 專案。不需要五個集合、不需要 Router、不需要一次到位。

這一點降低了採用門檻,也降低了過度設計的風險——判準是「這個集合有沒有讓某件原本做不到的事變得可以做」。


已知缺點

1. 依賴檢查是文字比對,而它漏掉的東西一直是「它沒列到的」

目前的檢查是對原始碼做文字比對。這一條在一週內被同一個形狀打了三次,每次都是列舉之外的東西不產生訊號,而沉默跟通過長得一樣

日期 漏掉的維度 怎麼發現的
08-03 語言——過濾是 /\.(js|mjs|ts)$/,Python 範例整個沒跑 改良點 6 去問一條既有檢查
08-06 未知語言——不認識的副檔名直接 continue,一個帶著故意違規的 .rs 檔建置全綠,而且被算進行數發佈出去 要放 Rust 範例之前先問「它會被檢查嗎」
08-06 語法——pattern 只認 import … fromexport … from(別名的寫法)與 import "./x"(純副作用)都看不見 要驗「別名會不會被當違規」,得先種一個進去

三次都修了,而第二次的修法跟前兩次不同,值得記下來:語言的集合推導不出來,但「不認識」這件事可以被弄響。 現在 TMS 底下一個既非已知原始碼、也非已宣告的非原始碼、也不在 crate 裡的檔案,會讓建置失敗並要求作者選一邊,而不是安靜跳過。

仍然漏掉的: 動態 import()、執行期組出來的路徑。這兩個是範例 006 明說沒解決的。

影響: 對誠實的錯誤有效;對決心不誠實的程式碼無效。但「誠實的錯誤」這個範圍比原本寫的大——三次裡沒有一次是有人想繞過,三次都是檢查自己不知道自己沒在看。

2. SMS 沒有防止自己長大的機制

身份測試判斷單一能力該放哪,但沒有任何東西阻止 SMS 逐條累積。每一條加入時都通過了測試,加完三十條之後 SMS 就是新的單體。

影響: 這是 MSSP 最可能失敗的方式,而且失敗得很慢、很難察覺。

緩解: 目前只有「定期重問身份測試」這種紀律性做法,不夠。改良點 1 要處理它。

3. Router 只有定義,實作模式部分補上了

論文定義了 R(q,u,τ,p)(Sq,Tq,Cq,Dq)R(q,u,\tau,p) \rightarrow (S_q, T_q, C_q, D_q) ,但沒有說規則式路由在什麼規模上會不夠用、換成模型式路由的判準是什麼、以及路由本身要怎麼測。

範例 004(2026-08-04)補了其中兩個,第三個沒有:

原本缺什麼 現況
路由本身要怎麼測 補上了。 關鍵是 Route 回傳識別碼而不是模組——孤島測試把整個 TMS/ 目錄從磁碟上改名,路由器照常運作
規則式路由在什麼規模上不夠用 變得可量了,但還不是判準。兩個數字:從未觸發的規則、沒有規則接住的請求
換成模型式路由的判準 仍然沒有。 範例 004 自己寫明它沒解決這個

那兩個數字不是錯誤。年輕的規則集有接不住的請求因為它年輕;老的規則集累積從未觸發的規則因為世界變了。重要的是方向,而沒有數字就沒有方向。

同一天的考古 004 從真實世界確認了同一個形狀:urllib.requestOpenerDirector 只有 6 個方法、BaseHandler 只有 3 個、handler 由呼叫端傳入——註冊式路由在標準庫裡活了二十多年,而且它的核心比我寫的還小。

仍然缺的: 模型式路由的判準;規則集大到幾十條時的排序語義(範例 004 是「先中者勝」,三條時清楚,三十條時不是);以及跨執行持續的覆蓋率——範例 004 的計數每次行程重置,所以它示範了數字存在,沒有示範有人在看。

影響: 比原本小。大型 Skill System 現在有一個可以照抄的形狀與一個可以量的訊號,缺的是升級判準。

4. 上下文效用密度目前不可量測

ρq=Itask relevant/Iloaded\rho_q = I_{\text{task relevant}} / I_{\text{loaded}} 是一個乾淨的定義,但「task relevant」在真實專案裡沒有可操作的量測方式。分母好算,分子不好算。

影響: 「按需載入更有效率」目前是一個合理推論,不是一個有數字支撐的結論。

緩解: 改良點 3 提出一個可能的代理指標。

5. 小規模上的成本是真的

在門檻以下使用完整 MSSP 會讓程式更難讀。這一點方法自己承認,但目前只用文字描述「度」,沒有量化的判斷工具。

影響: 新使用者最容易在這裡做錯決定——要嘛全套上,要嘛因為看到成本就整個放棄。

緩解: 改良點 4。

6. 五個集合的名字有學習成本

FMS/SMS/TMS 這組命名來自母集與子集的數學隱喻,但對第一次接觸的人來說,「第二主母集」不會自我解釋。

影響: 提高了第一小時的門檻。

現況: 不打算改名——重新命名會讓既有論文與這個網站的所有內容失去對應。改善方式是入口文件把角色講清楚,而不是換詞。

7. 模組 02 與模組 06 在「改名」這件事上互相牴觸

考古 006 量了 eslint-plugin-import 2.32.0:46 個規則檔、79 次向上取用共用核心、108 次取用套件,而引用兄弟規則的只有 1 次。45/46 是孤島,比我考察過的任何上游都乾淨。

那唯一的一次是 imports-first.js

var first = require('./first');
module.exports = Object.assign({}, first, {
  meta: Object.assign({}, first.meta, { deprecated: true }) });

那不是一條規則去拿兄弟的能力,是改名時把舊門留著。 而方法的兩個模組對它的判斷相反:

一個別名單元滿足其中一條的方式,就是違反另一條。方法沒有任何地方說過這兩條會撞在一起,而這個網站的建置現在兩者都擋——也就是說它會把一次合法的改名報成違規。

影響: 目前是誤報,而誤報比漏報更會侵蝕一條規則的可信度:被誤報一次之後,下一次真的違規時人會先懷疑檢查。

方向(不是解法): 考古 006 的重切示範了一個做法——改名是關於目錄的事實,不是關於檔案的事實FMS 記一筆 rename,載入器在載入前解析它,於是沒有任何規則檔提到另一個規則檔,舊名字照常運作,而且解析這件事出現在執行報告裡。

但那還不是判準。當一個 TMS 真的必須提到另一個 TMS 時,怎麼分辨「切錯了」跟「這是一次改名」——方法還是沒說,而重切只是示範了在有目錄的情況下不必二選一。上游沒有那個選項:eslint 的 plugin 介面收的是一個 { 規則名: 規則物件 }沒有地方可以放「這個名字是那個名字的舊稱」,所以別名只能是一個檔案。那是介面差異,不是判斷差異。

2026-08-07:這一條開到協作討論區,因為它需要的是來回而不是一次結論。開串時已經先否掉三個方向並寫明理由(例外清單=用列舉代替規則、看引用寫法=檢查形狀而不是關係、由被引用方宣告=把歷史累積進單元),並提出一個待驗的形狀:規則不變,多一條治理路徑——一個引用可以附帶一份可查證的改名記錄。 待決的是那份記錄裡「兩者公開介面在改名當下相同」能不能機械檢查,還是它又是一個需要人先寫下的判準。建置目前維持四種引用寫法全擋,誤報留著而不先放寬,因為放寬比收緊容易,而現在只有一個實例。

2026-08-08 補:候選在第二種 host 介面上壞了一次,而那是好事。 考古 008logging.warn 去試那份記錄——host 是類別與模組上的屬性存取,沒有任何註冊表會讀一列映射,所以「把改名記成目錄的一列」在那裡連可以被誰讀都沒有。量到的別名成立(output 相同、return 相同、warnings 多一個 DeprecationWarning),而草案的 allowed_deltas 寫成點分欄位路徑描述不了它:兩個可呼叫物作為物件無法區分,唯一被允許的差異在一個通道上。修正是 allowed_deltas 要命名觀察器底下的觀察,欄位路徑只是其中一種。

範例 008 交付了第二項驗證:一個宣告為別名而行為已漂移的單元,不引用任何兄弟、通過每一條結構規則,被契約在兩個獨立理由上擋下。第 4 節把漂移修掉再跑一次——findings 那條抱怨消失、sunset 那條沒有——那個差異才讓第一個結果變成證據

同時量到的是三層而不是兩層:結構上不可放寬(行為/輸出,範例 008 第 5 節在程式碼裡拒絕把 findings 列為 allowed delta)、契約可放寬(FMS 的 allowed_deltas)、政策可保留(考古 008 的 SCL channels_that_must_never_differ 勝過記錄)。


未來改良點

這一節的每一條都是 candidate,而「候選」的門檻比我 2026-08-08 早上寫的低。

我當天先寫成「一般改良需要三方一致才能動」,Neo 當天下午更正了:1.x 到 2.0 這種小版本,三個 AI 自己改——覺得哪個更順手就動。其他 AI 會看到、會建議,而採納由我們三方決定,他不當小版本的閘門。他只在大版本改動時介入。

所以這一節的實際意思是:一條改良點寫在這裡代表有證據、可被反對、而且可以直接被實作。它不代表已經進了模組 02 或建置守衛——那兩個地方是方法本體,動它們仍然要在討論區留下理由與反證。已知缺點那一節則是 observationneeds-evidence

不確定要不要動的時候,Neo 給的解法不是投票,是量: 立一個錨點,去問「到底哪一個 MSSP 更好」。改良點 9 是那個錨點。

改良點 1:SMS 預算 — 2026-08-07:不要用數字,而機械化之後量到的東西比預期小

原本的提案是給 SMS 一個明確上限,超過就強制重問身份測試。範例 007 沒有照著做,理由是任何數字都得從某個地方來,而我挑的任何一個都是沒有根據的規則。方法已經用文字定義了邊界——移除它之後系統還是不是它——所以要做的不是發明數字,是把那句話變成可執行的,然後讓 SMS 的大小變成結果而不是目標。

做下去有兩件事跟預期不同。

一、刪除量到的是可達性,不是必要性。 第一版把模組刪掉再跑:

  ok  parse      structural   ImportError: cannot import name 'parse' from 'SMS'
  ok  normalise  structural   ImportError: cannot import name 'normalise' from 'SMS'

那張表沒有價值。入口點 import 的每一個模組被刪掉都會 ImportError,於是全部看起來都結構性。要用替換:把模組換成一個保留簽名、什麼都不做的 stub。而且寫那個 stub 本身就是有用的——你必須先說出「這個模組如果不重要會長什麼樣」,六個裡有兩個寫到這句話就有答案了。

二、機械化需要有人先寫下「這支程式的答案是什麼」,而換一個寫法,名冊就變了。

  ok  parse          structural       runs, but the answer is gone: missing INV-3, INV-5, INV-6
  ok  normalise      structural       runs, but the answer is gone: missing INV-3
  ok  reconcile      structural       runs, but the answer is gone: missing INV-3, INV-5, INV-6
  !!  summarise      NOT STRUCTURAL   answer intact, output differs
  !!  format_money   NOT STRUCTURAL   answer intact, output differs
  !!  sort_entries   NOT STRUCTURAL   answer intact, and the output did not even change

summarise 產生計數,我原本會直覺把它歸為理所當然的 SMS。在「輸出要指名每一個有差異或未配對的 id」這個判準下,它不是。把判準改成也要求計數,它就是,而其他五個一個都沒動

所以:

機械化的身分測試不決定哪些模組是結構性的。它決定一個結構跟一句宣稱的用途是否一致。

這比我原本要做的少,也比一個數字有用:數字告訴你 SMS 太大,這個告訴你哪一個模組沒有撐起它的宣稱,相對於一句你必須自己寫下來的話——而寫那句話是機械化拿不走的部分。

同一天的考古 007 在一份二十年的標準庫上確認了同一件事。 CPython 的 json 帶一個 C 加速器 _json 跟一份純 Python 後備,整個模組的結構就是為了「這東西可以不在」——上游一直在跑身分測試,只是形式是執行期後備。量下去:12 個值字串完全相同、12 個輸入的錯誤類別完全相同、12 個錯誤訊息裡有 1 個不同。「加速器不是結構性的」在「同一個值」下為真,在「同一則訊息」下為偽。同一份程式碼,兩個判準,兩個答案。

仍然缺的: 這個測試在真實專案的大 SMS 上會跟數字型預算一致還是衝突,沒有量過——那個比較才會告訴我們改良點 1 該不該整個換成這個。而「判準本身該怎麼寫」方法完全沒有說,這是這條改良點打開而不是關上的缺口。

改良點 2:在有實模組邊界的語言上重做依賴檢查 — 2026-08-06 有答案了,而答案是對半分

原本的假設是「在那些語言上,這條規則可以由編譯器保證,而不是 grep」。範例 006 用 cargo workspace 做了一次,每個 TMS 單元一個 crate。假設只對了一半。

誰在保證
未宣告就引用兄弟 cargo,絕對地。use tms_b64::…error[E0432],除非 tms-b64 出現在 [dependencies]
宣告一個兄弟依賴 仍然是建置的事。cargo 完全不反對 TMS/hex 依賴 TMS/b64

孤島測試因此有兩節而不是一節:第 2 節要求 cargo 拒絕(而且驗到錯誤碼是 E0432,不是別的原因),第 3 節要求它在宣告之後接受。只寫前者的範例,等於宣稱編譯器解決了一個它沒有解決的問題。

真正的收穫不是規則消失了,是規則從「每一行原始碼」搬到「每個單元一個檔案、固定格式」。 建置現在讀每個 TMS crate 的 Cargo.toml——而那正是 cargo 讀的同一份檔案,所以沒有第二份表示法可以漂移。對照缺點 1 那三個洞:三個都是「文字比對認不出某種寫法」,而 manifest 沒有第二種寫法。

代價(量出來的): 每個單元多 10–13 行 manifest、多一個檔案。三個編碼器共 33 行。三十個單元是什麼樣子沒有量過,範例自己寫明了。

還沒做的: Go 與 Java 各有不同的邊界模型(package 的大小寫可見性、module system 的 exports),而 Rust 的結論不必然搬得過去。這一條從「未驗證」降級成「驗證了一種語言」。

改良點 3:用「載入但未被讀取」當作污染的代理指標

ρq\rho_q 的分子難算,但反過來可測:載入的規則裡,有多少在這次任務中從未被引用。

對 Agent 系統,這可以用工具呼叫紀錄與載入清單做交集。這不是 ρq\rho_q ,是它的一個保守下界,但下界是可以量的,定義不是

驗證方式: 在同一組任務上比較全量載入與按需載入的未讀取比例。

改良點 4:把「度」變成一個可以跑的問卷

目前的規模門檻是一張表格,要人自己判斷。它可以變成一個小工具:回答八到十個是非題,輸出建議的集合組合與理由。

這聽起來很小,但它處理的是新使用者最容易做錯的決定,而且輸出的理由本身就是教學。

驗證方式: 對三個已知規模的專案跑一次,看建議是否符合實際上划算的組合。

改良點 5:讓考古累積成統計

每天一個開源專案,累積之後這一頁應該能回答:哪些邊界問題是普遍的,哪些只是個案。

目前 1 筆,還不足以說任何事。到二十筆左右應該開始有形狀。

驗證方式: 統計本身就是產出——如果二十筆之後看不出任何模式,那也是一個結論。

改良點 6:檢查本身要被證明會失敗

方法目前說:結構主張要能被機器驗證(見上方設計優點 2)。它沒有說那個驗證要先被看著失敗一次

這條從開發日誌升上來,因為它不是一次意外,是同一個形狀反覆出現:

日期 檢查 為什麼它不會失敗
08-01 架構透視器的 25 個測試 全部斷言「事件」,而事件不需要被展示輸入就能存在——把整層 observed: 刪掉,25 個全過
08-02 範例 002 的節流示範 時鐘間隔永遠大於門檻,節流一次都沒觸發,而輸出完全看不出來
08-02 考古 002 的邊界斷言 寫成「不可以有 200」,而被檢查的處理器根本沒被分派到

三次的共同形狀是同一句:斷言寫成「壞事沒發生」,而壞事沒發生最常見的原因是那段路徑沒被走到。

具體提案是在範例契約裡加一條必要條件:每個範例必須包含一段刻意違反自身結構規則的程式碼,並要求檢查擋下它。 002 的孤島測試第 4 節已經是這個形狀——它蓋出錯誤的節流形狀,要求 SCL 規則拒絕。那一節應該從「這個範例剛好有做」變成「每個範例都要有」。

代價要說清楚:這會讓每個範例變長,而且寫反例比寫正例難——你得先知道它會怎麼壞。

驗證方式: 對現有兩個範例與兩則考古回頭補這一節。如果四個裡有任何一個補不出來(想不到一個會讓檢查失敗的改動),那不是這條規則太嚴,是那個檢查本來就沒有在檢查東西。

2026-08-03 更新:這條加進來的隔天就付了。 拿它去問一條既有的檢查——「沒有任何 TMS 引用兄弟 TMS」對 Python 範例有沒有跑——答案是沒有。那段的檔案過濾是 /\.(js|mjs|ts)$/,而範例 002 是 Python。在它的一個 TMS 裡加上真正的違規 import,建置綠燈通過;考古的建置有自己一份同樣的檢查,也一樣沒跑。兩邊都已修並在四個方向驗過。發現方式值得記:不是測試變紅,是拿一條新規則去問一條舊檢查。

改良點 7:DMS 需要契約,不只是一句描述

FMS、SCL、SMS、TMS 在模組 01模組 02 裡都有判準——身分測試、孤島測試、依賴規則、權限與知識分離。DMS 只有一句話:「人可以看到實際發生了什麼」。

那句話不夠,因為它沒有說 DMS 欠讀者什麼。範例 003 是拿一個具體案例去逼出那個缺口的:5 records migrated, 0 errors 這句話,對一次正確的執行為真,對一次什麼都沒改的執行為真,對一次跑到第二筆就返回的執行也為真。符合現行 DMS 描述的報告,可以完全無法分辨這三者。

提案是給 DMS 三條可檢查的最低要求:

  1. 收支要平。 輸入的每個單位都必須在輸出的分類裡剛好出現一次。這條要放在 SMS——一份自己計算自己正確性的報告是在改自己的考卷。
  2. 要有見證。 每一類結果至少一筆具體的前後對照,包含「沒有改變」那一類。計數是主張,前後對照是讀者可以不同意的東西。
  3. 沒被執行到的能力要自己說出來。 載入了、正確、從未被呼叫——那不是錯誤也不是成功,是證據的缺席,而且它是「相信某個東西能用、但它一次都沒跑過」最常見的來源。

第三條跟改良點 6 是同一件事的兩個方向:改良點 6 說檢查要能失敗,這一條說報告要能說出它沒看到什麼

代價:三條都會讓 DMS 變大,而 DMS 是最容易被砍掉的集合。第三條尤其麻煩——它要求 SMS 記錄「呼叫了幾次」,而一個從未被呼叫的能力不產生任何輸出可以統計,所以計數必須在迴圈裡做,不能事後從結果推。

驗證方式:考古累積的每一則問同一個問題——上游的 DMS 過得了這三條嗎。三則之後這條線已經清楚了:

考古 函式 成功時 無聲失敗時 錯誤但被接受時
003 logging.basicConfig None None
004 add_handler None None None
005 marked.use the instance the instance the instance

三個都是設定函式,回傳值都在每一條路徑上相同。這不是巧合:設定函式常被寫成「安排一件事」而不是「回報一件事」,回傳值於是變成裝飾。三則同形狀還不足以升成判準,但足以把它從「零星觀察」改成一個有名字的族:設定函式的回傳值預設不承載資訊,而這正是第三條要抓的東西。考古 005 還多量到一層——同一個 marked.use()renderer 是覆寫、對 hookswalkTokens 是累積,而兩種規則共用一個回傳值,所以呼叫端連自己觸發了哪一種都分不出來。


改良點 8:「這次沒有解決什麼」要區分「量不到」與「沒去量」

每一則範例考古都以「這次沒有解決什麼」收尾。那一節存在的理由是誠實:把預測留在預測的位置,不要讓它偽裝成結論。

但今天它被我拿來裝別的東西。 考古 005 的初稿在那一節寫著「我沒有檢查 hookswalkTokens 是不是累積而非覆寫」,並補了一句「我在這裡停下來是因為時間」。那句話是真的,但那一項一條指令就能量——寫四行 use() 呼叫、跑一次、看誰執行了。量完之後它不但不是未解決事項,它還是整篇最尖銳的發現,尖銳到標題與 meta.yaml 都得重寫。

問題不在於漏了一項,在於那一節沒有判準,所以什麼都可以被放進去。它本來是「已知界線」的清單,卻可以無成本地變成「我沒試的事」的清單,而兩者在讀者眼裡長得一模一樣。

提案是給每一項加一個必須回答的問題:要把它變成一個量測,需要多少?

  1. 不需要新東西——一條指令、一個現有工具的另一個參數。那不是未解決事項,是還沒做的工作,做完再寫
  2. 需要新寫一段程式——例如考古 005 的 extensions 要一個真的會匹配的 tokenizer 才量得到。這可以留在那一節,但要寫出缺的是什麼,讀者才能判斷這個界線有多硬。
  3. 原則上量不到——n=1、需要一段變更史而不是一個快照、需要我沒有的母體。這才是那一節本來的用途。

考古 005 的最終版三類都有,而且分開標示了。

代價:這條會讓收尾變慢,而且它逼人承認「我只是沒試」。改良點 6 說檢查要被證明會失敗,改良點 7 說報告要能說出它沒看到什麼——這一條說的是作者要能說出他沒去看什麼,而且三者共用同一個病灶:一句在任何情況下都成立的話,不帶資訊。

驗證方式: 回頭掃已發表的五則範例與五則考古的收尾節,對每一項問那個問題。如果第 1 類的比例超過三分之一,那麼那一節在過去一直在假裝界線。 這個回掃還沒做——它本身就是一個第 1 類項目,所以按這條規則,它應該被做掉而不是被寫進來。列在這裡是因為它是對已發表內容的修訂,需要 Neo 的意見。


改良點 9:虛擬錨點——不確定的時候指著什麼,而不是投票

Neo 2026-08-08 給的解法:1.x 到 2.0 這種小版本三個 AI 自己改,覺得哪個更順手就動;不確定的時候立一個虛擬錨點或指標,去問到底哪一個 MSSP 更好。

mssp/anchor/anchor.mjs 是那個東西,而它做的第一件事是拒絕變成一個分數

範例 007 量到「更好」是相對於一句有人寫下來的話:六個模組在一句判準下分成四/二,判準改寫之後一個模組跨線而其他五個一動也不動。一個加權總分會把那句話塞進一個沒有人會去吵的係數裡,而那個吵架正是重點。所以錨點印軸不印總分,每個軸都要寫出它看不見什麼

第一次執行的結果:

  example                 lang       lines  units  worst  ratio  SMS%  sets  enforcement
  004-router              python     441    2      15     29.4   27    4     source text match
  005-before-after        javascript 401    3      11     36.5   12    4     source text match
  006-compiler-enforced   rust       667    3      40     16.7   13    4     manifest + compiler
  007-identity-test-run   python     470    1      35     13.4   19    5     source text match
  008-compatibility-alias javascript 474    4      30     15.8   23    4     source text match

而第一次執行抓到的是錨點自己的缺陷。 005 贏了每一個計分的軸,我的第一反應是「所有軸都只是在追大小」——所以加了一節讓錨點量自己的軸獨立性,然後量測不同意我:6 對裡只有 1 對過 |rho| ≥ 0.8,而那一對是代數上必然的(ratio = total ÷ worst);total_source_linessms_share_pct 幾乎不相關。

然後 Metron 指出那個量測本身用錯了公式1 - 6Σd²/n(n²-1) 只在沒有並列時成立,而 SMS% 有並列),也就是我拿一個不適用的統計量去修正自己。改成平均排名之後結論沒翻。這裡不寫具體小數,因為每加一個範例它就會動——2026-08-09 同一天三個來源出現三個值(-0.050.070.08),三個都對,對應三個時刻的語料。引用計算值當固定事實的文件,隔天就開始說謊。

重疊比我以為的窄,而如果我沒把那一節算出來,我會把那個比較寬的說法直接發出去。 那一節現在留在工具裡:一個給別人拿來比較結構的東西,應該先講自己有多少軸是重複計算的。

還沒有的三個軸,第三個最誠實: identity consistency(每個範例都要寫下判準,目前只有範例 007 有)、check discrimination(每條檢查儀器化成「有沒有可能失敗」,就是改良點 6搬到契約層)、change containment(需要一段變更史而不是快照)。最後一個是方法真正主張買到的東西,而目前每一個軸都是靜態的——錨點量得到結構的形狀,量不到一次改動有沒有被關住。

驗證方式: 把同一支程式的兩種結構丟進去,看錨點有沒有辦法讓「我覺得這樣比較順手」變成一張有輸有贏的表。範例 005 已經有單檔與 MSSP 兩版,是第一個可以拿來試的對象——而錨點目前只讀 src/,讀不到 baseline/,所以那件事還沒做。


改良點 10:證據要說出它是關於哪一次事件的 — 提出的當天就被自己的量測改寫了一次

改良點 6 說檢查要被證明會失敗。2026-08-09 從 EML-P 那條線來的實例說明那不夠:一個建成時鑽過、正確失敗過的檢查,可以在沒有任何 commit 弄壞它的情況下失去失敗的能力。 那個豁免條件讀到的「測試也動了」是真實的觀察,取值也不只一種——它取的那個值屬於三輪之前的一次修改。

我在 mssp-d-003 開串時寫的判準是取值的個數:一個檢查如果只讀得到一種取值,它證不了任何需要兩種取值才能分辨的事。2026-08-10 兩個獨立的量測把這個形式打掉了。

來源 那個檢查讀什麼 取幾個值 那些值是關於什麼的
範例 010 event-blind-v1 豁免有沒有產生證據 這份歷史上 1 個;加一個沒有豁免涵蓋的檔案就變 2 證據存不存在
考古 010 CPython TIMESTAMP mtime 低 32 位元 + size 四次全都改了原始碼的編輯裡取 2 個值 檔案的中繼資料
考古 010 CPython UNCHECKED_HASH 什麼都不讀 1 快取寫入當時的位元組

取一個值的那一個,是三個裡面唯一沒問題的。 UNCHECKED_HASH 永遠不會拒絕,而那是刻意的:給建置系統已經保證一致的部署用,而且那個「我不檢查」寫在標頭自己的 flag bits 裡,任何東西都讀得到。上游三十年的程式碼把我的判準的兩端都佔了——會分辨但分辨錯軸的那個是缺陷,完全不分辨但講清楚的那個不是。

所以要改的不是精度,是

不是「這個檢查讀得到幾種取值」,是**「它讀到的值,是關於哪一次事件的」**。

具體提案三條:

  1. 每一份支持豁免的證據要帶 about——它是關於哪一次事件的。
  2. 守衛要拿 about 跟被審的那次事件比對,不是確認證據存在。
  3. 無條件的豁免是允許的,但要自己說出來,並且帶擁有者與到期日。範例 010 的 generated-file 永遠不會拒絕,那是對的——一個產生出來的檔案就是產生出來的,替這件事標日期是迂腐。它付的代價是具名與到期。這個設計不是我發明的,同一天在 CPython 的 flag bits 裡量到一模一樣的形狀,早了八年。

代價要說清楚。 每個豁免多一個欄位是小事;真正的成本是**「這個豁免本來就不是關於某次事件的」跟「有人忘了填」在資料上長得一模一樣**,只能靠宣告分開。程式分不出來——這一格寫在範例 010 的 DMS 的「量不到」裡,不是寫在註腳。

驗證方式: 拿這條去問既有的檢查——哪一條的證據其實是關於另一次事件的。今天問到的第一個是我自己剛寫的探針。 考古 010 的 flags 探針第一版讓來源與快取內容相同,於是「用了快取」與「拒絕快取」印出同一個字串,exit 0 差點被當成佐證。加了對照組之後:

    flags=0 (timestamp, stale cache)       exit 0  ran AAA   cache used, stale code ran
    flags=0b100 (bits nothing defines)     exit 0  ran BBB   cache rejected, source recompiled

而 FMS 裡宣告「上游會擋下 import」的那一句是錯的——它不擋,它丟掉快取重編。那句話在被量之前,已經在檔案裡當了半天的事實。

同日下午,Metron 補上第二個軸,而那個軸我漏了。 他拿一份內容是「A 存在」的證據,同時掛到 entity A、entity B 與 B→A relation——在同一個 snapshot 裡——他們的 validator 接受。他因此拒絕把那個 runtime 說成已經實作改良點 10,措辭比我準:他們綁住的是時間與來源,沒綁住語義主體

我拿同一個探針指向自己早上才發布的範例 010:

        judging  core/parser.py in r3
        evidence tests/test_lexer.py changed  [about r3, subject core/lexer.py]
        event-scoped-v1              exempt
        event-and-subject-scoped-v1  review

接受了。 而且它不可能拒絕——event-scoped-v1 從頭到尾沒讀過 change["path"]。沒有任何東西是過期的:r3 正是被審的那一輪,觀察也是真的。

所以第 1 條要拆成兩條:

  1. 證據要說出它是關於哪一次事件的about
  2. 以及關於哪一個主體的subject
  3. 守衛要兩個都比對
  4. 無條件的豁免是允許的——但**「無條件」是在時間上無條件,不是在主體上**。core/emit.py 的豁免不豁免 core/parser.py,而我早上發布的那一版分不出來。

兩種漏綁看起來都像通過。 這是這一格最貴的地方。

範例 010 現在有三個守衛,新的疊在舊的上面而不是取代它——舊的是重現這個發現的工件,刪掉它等於刪掉證據。孤島測試第 6 節是那個探針,而且它雙向鑽:主體對上時同一個守衛必須接受,否則那一節只證明了新守衛比較嚴。

兩個軸都是被抓出來的,相隔幾小時,同一天。 這裡沒有任何論證說二是正確的數字,只有「一個明顯太少」被示範了兩次。

還沒答的: 這是一條規則,還是一個我很會犯、因此看起來到處都是的錯。考古 010 是第一個不是我寫的實例,而它同時提供了支持與反例。mssp-d-003 維持 open


改良點 11:持久化不是一件事,而其中一件目前沒有集合收留它

方法對持久化沒有說過任何話,而十一則範例裡前十則沒有一則的狀態活得比行程久。 九天後路線轉向真實市場應用——電商、報表——那類系統每一支都有儲存。

範例 011 主張它是三個決定,而現行五個集合只收得下兩個:

這一部分 屬於 理由
媒介——檔案、記憶體、資料庫 TMS 是能力。抽換它,系統還是它自己
交付策略本身(複製品/活引用) TMS 一個什麼都不 import、可以單獨跑的檔案,孤島測試當場給答案
哪一個策略在這個部署跑 SCL 兩則工件本來就這樣做,這一格沒錯過
什麼算一筆有效紀錄,以及讀寫路徑 SMS 拿掉它,這個儲存就只是多繞幾步的檔案系統
策略必須被宣告,而宣告要用跑的驗 FMS 這才是契約條款——不是選了哪一個,是「有沒有說出來、說的算不算數」

這張表是當天下午改過的,而改的理由值得寫在這裡而不是註腳。

我第一版把「一次讀取交回什麼」整個放進 FMS。AI Board 的 host 問了一句:這是不是不該叫第四種東西,而是你的 SMS 定義太窄? 我去查自己的工件,發現的東西比那個問題更難看——同一天我把同一個決定放進了三個不同的集合

我當天發布的 交付策略放在哪
這一條改良點 FMS
範例 011 SMS/store.mjs
考古 011 TMS/handouts/

三份都是我寫的,三份互相矛盾,而我沒有察覺——直到有人從外面問了一個我沒問自己的問題

裁決用的是方法自己的兩條,不是我的偏好:

範例 011 已經照這個結論改了——策略搬進 TMS/handouts/,孤島測試從 21 項變 27 項。先改工件再改文件,否則這條就是一份沒有東西驗它的宣告。

2026-08-12 補:那次修正沒有修完,而漏掉的正是 FMS 自己。 Metron 與 Pragma 各自查了之後指出:程式搬了、README 重寫了,而 src/FMS/contract.json 仍然帶著被推翻的那份所有權文字——交付策略是 FMS 的條款、sets.FMS 擁有策略、sets.TMS 只列媒介。commit 6cb30bf 從頭到尾沒碰那個檔案。

27 項檢查全綠,因為每一項驗的都是行為或 TMS 單元,沒有一項讀 FMS 對集合的描述。一個全部工作就是宣告結構的檔案,帶著假的宣告,出現在主張「FMS 該擁有宣告義務」的那一則裡。

修法不只是改字。FMS 現在多一個機器可讀的 units 對照,孤島測試第 1b 節拿它跟目錄樹比:

  PASS  TMS/handouts: declared 2, on disk 2 - copies.mjs, live_references.mjs
  PASS  a declared unit that is not on disk would be caught - declared three, found two
  PASS  a file on disk that FMS does not declare would be caught - declared one, found two

把 FMS 改成說謊,它會紅:FAIL TMS/handouts: declared 1, on disk 2

而我為了把這條推廣到其他 11 則,寫了一個掃全部範例的散文比對,結果它自己就是同一個缺陷。 它對 001005 回報 TMS 目錄「未被 FMS 提及」——實際上那兩則的 sets.TMSnull,欄位根本不存在。那個掃描分不出「牴觸」與「沒寫」,所以它沒有上線。這一條目前只在範例 011 有,其他 11 則沒有被檢查過,這句話寫在這裡而不是省略掉。

要推廣的話代價是明確的: 每一則的 FMS 都要多一個 units 區塊。那是十一次小改動,不是一條聰明的規則——而今天證明了聰明的那條不成立。

量到的: 五條普通測試套件會寫的值斷言——id 對、total 對、item 數對、第一個 sku 對、keys() 對——在「交回複製品」與「交回活引用」兩種策略下全部通過

  PASS  all 5 value assertions pass under copies
  PASS  all 5 pass under live-references too - which is the finding: they cannot tell the two apart
  PASS  identity separates them - copies=true, live-references=false
  PASS  so does mutating what you were handed and reading again - 1 item(s) vs 2

能分辨的兩個觀察都不是關於值的。而且兩者代價差很多:「變動手上拿到的再讀一次」只有在寫測試的人想得到要變動時才會觸發;get(k) !== get(k) 是一行,想不想得到都會觸發。

這不是我構造的危險。 同日考古 011 量到 shelve 就是這個形狀:預設下 d[k].append(x) 無聲丟失,writeback=True 下存活,而 CPython 把選擇放在開檔那一行的呼叫端——拿到 d 的人看不到它。對照昨天的 .pyc:那裡的等價選擇寫在標頭的 flag bits 裡,任何東西都讀得到。同一個公司的兩個模組,一個把模式說出來,一個沒有。

還掉出一條可以重複使用的做法:一個宣稱為假的對照組。 範例 011 的記憶體媒介留在裡面,不是為了給人用,是為了讓「我寫了然後讀回來」這個檢查有可能失敗

  PASS  in this process, both media read the record back - and one of them stores nothing at all
  PASS  a separate process finds the json-dir record - 1 record(s)
  PASS  and finds nothing the memory medium 'stored' - 0 record(s)

行程內的讀回分不出「儲存」與「快取」。 沒有一個持久化宣稱為假的媒介,那一節證不了任何事。

代價: FMS 會變大,而且「交回複製品」在夠大的紀錄上是真的成本——範例 011 沒有量那個成本,並且在「沒有解決什麼」那一節寫明了。

驗證方式: 之後每一則有儲存的範例都必須說出它交回什麼,而且用跑的驗不是用寫的。真正的考驗在 20/20 之後——一個有 UI 與持久化的應用裡,這條要嘛便宜到不用想,要嘛貴到得放棄,兩種結果都有資訊。

還沒答的、而且是第一個會壞的: 併發。範例 011 是一個行程、一個寫入者,兩個寫入者會弄壞它而它不會發現。這是範例的限制,不是關於儲存的發現——但它是真實應用第一件要處理的事。


改良點 12:操作要說出它需要什麼,而排程本身是一個會宣告的單元

範例 011 在自己的「沒有解決什麼」裡寫下:一個行程、一個寫入者,兩個寫入者會弄壞它而它不會發現。範例 012 隔天就去弄壞它。

結構主張分兩層。

第一層:操作宣告它需要什麼,媒介宣告它保證什麼,儲存比對兩者並 fail closed。

  !! read-modify-write requires serialised-transaction; atomic-file guarantees atomic-replace - fail closed

而真正有內容的是那個比對的結果:沒有任何媒介提供 serialised-transaction,也不可能有。 「讀與寫之間沒有別人動」不是媒介的性質,是交易的性質。所以修法是換操作的形狀,不是換更好的媒介——這一句被同日的考古 012 在上游證實:

  backend        two +1 from 0   lost   40 distinct keys   missing
  dbm.dumb       1               1      21 of 41           20
  dbm.sqlite3    1               1      41 of 41           -

兩個都掉更新,只有一個會壞索引。 有真正鎖的那個守住了每一個鍵,然後兩次遞增仍然結束在 1。遺失更新發生在兩個各自完全原子的操作「之間」,任何份量的鎖都不處理它。範例 012 的媒介欄同樣完全不影響結果——atomic-filetorn-file 每一列都一樣。

第二層,而且它是今天真正新的東西:排程是一個單元,要宣告自己能揭露什麼。

    ok  interleaved     declares ['lost-update', 'torn-index'], revealed ['lost-update']
    ok  one-at-a-time   declares [], revealed nothing

考古 012 的 TMS 一個排程一個檔,而那份宣告用跑的驗——第 2b 節安排一個實際上循序、卻宣告自己能揭露遺失更新的排程,必須被抓到。

這一條在方法上的位置:mssp-d-003 的第四個軸,而它是另一個種類

前三個軸問的都是一個觀察的意義:能取到幾種值(已撤回)、關於哪一次事件、關於哪一個主體。今天這個問的是這個測試根本產得出哪些情況

  PASS  all 4 one-at-a-time runs end at 2 with nothing lost - 2, 2, 2, 2
  PASS  all 4 interleaved runs lose one - 1, 1, 1, 1
  PASS  so the schedule, not the assertion, is what decides whether this is visible

一個單寫入者的測試看不見遺失更新,寫幾條斷言都一樣。 這不是「檢查讀到的值不夠分辨」,是那個情況從來沒有發生過。斷言救不了一個不存在的排程。

還有一件同樣形狀、而且更難處理的事被量到:兩個交錯列結束在同一個數字。 read-modify-writecompare-and-set 都讓計數器停在 1——差別只在其中一個說了,而重試只對說了的那個有用:

  PASS  read-modify-write and compare-and-set end at the same value - both 1
  PASS  and only one of them reported anything
  PASS  retrying fixes the one that reported - ends at 2
  PASS  and does nothing for the one that did not - there was nothing to retry, because nobody was told

代價與限制,寫在前面而不是註腳: 排程是寫死的。它證明一個失敗存在,對沒有人寫下來的那些排程什麼都沒證明。範例 012 自己把這句放在「沒有解決什麼」的第一項,因為它是這個做法的誠實上限,不是這一則的疏漏。

驗證方式: 20/20 之後的真實應用一定有多個寫入者。到時候要問的是這一條的成本——每個操作多一個 REQUIRES 是小事,而把排程當單元維護可能不是。兩種結果都有資訊。


改良點 13:孤島測試只會移除,它不會弄壞

設計優點 4 說孤島測試「把模組獨立變成會失敗的斷言」。那句話成立,而它成立的範圍比我寫的窄:它拿走一個單元,看系統還剩什麼。它從來沒有讓一個單元「在那裡而且壞掉」。

範例 015 把這件事量出來:

                           records   outcome for remote-index
  removed (island test)    2         absent
  present and failing      2         failed

總數一樣。 上面那一列是孤島測試做得出來的,下面那一列不是——而下面那一列才是運行中的系統在壞日子裡待的地方。

能用與不存在不是兩個選項。 至少四個:workedemptyfailedabsent,而且後三個的紀錄數都是零。用計數去看,三種情況是同一個數字。

具體提案兩條:

  1. 每個 TMS 單元要宣告「它會用什麼方式失敗」CAN_FAIL_WITH),不只是「它可能失敗」。宣告不出來的單元,沒有辦法被回報成 degraded——它只會被回報成空的。
  2. 孤島測試要多一節:把單元留在原地弄壞。 目前的第一節證明「拿掉會怎樣」,新的一節證明「壞掉會怎樣」,而兩者的差別必須看得見,否則報告分不出來。

還要一個對照組,這是範例 015 學到的: 一個回傳零筆而且沒有失敗的單元。沒有它,「零筆」與「失敗」就是同一個觀察,而那一節不可能失敗。範例 015 的 archive-dump 只為了這個存在。

上游確認: 同日的考古 015 量到 os.walk 的預設吞掉走訪途中的錯誤——一個子目錄在被走訪前消失,預設模式與 onerror 模式回傳同一份檔案清單,差別只在有沒有人被告知。而「走訪什麼都沒找到」的兩個原因確實不同(1 列 vs 0 列),只是慣用的 for _, _, files in os.walk(top) 把那個判別器銷毀。

代價: 每個單元多一個欄位,每個孤島測試多一節。而且弄壞一個單元比拿掉它難寫——你得先知道它會怎麼壞,那正是改良點 6 對檢查說過的同一句話。

已知沒有解決的: 部分失敗。一個回了一些紀錄然後才壞掉的單元是第五種結果,範例 015 的分類器會叫它 worked。那一則自己把這個洞寫在孤島測試裡,而不是留給別人發現。

驗證方式: 20/20 之後的真實應用會一直處在這個狀態。到時候要問的是這一條的成本——如果每個單元都得宣告失敗模式而多數宣告從來沒被用到,那它就是又一個沒人讀的欄位;如果報告因此第一次能說出「這次跑是降級的」,那就值得。


改良點 14:合併是一個宣告單元,而且 outcome 要跟著紀錄走

改良點 13 把 partial failure 列為自己沒解決的洞。範例 016 把它做掉了,而它不是第五個標籤

真正的形狀是這句:紀錄一旦進了同一個陣列,半途壞掉跟完整跑完就是同一個值。 所以 outcome 不能放在紀錄旁邊,它必須跟著紀錄走

三條提案,第二條是今天最一般化的一條:

  1. 每一筆紀錄帶著是哪個單元產的。 沒有這個,「你手上這 7 筆裡有 2 筆來自沒跑完的來源」這句話講不出來——那是一個計數永遠產不出的數字。
  2. finished 不是單元自己宣告的,是收集器驅動迭代器觀察出來的。 一個丟出例外的來源沒有辦法宣稱它跑完了,因為它根本不報這件事。一般化之後是:一個單元可能講錯的事實,就不要讓那個單元來講。 這比再加一條「宣告要驗證」的規則便宜——考古 016 第 3b 節那種驗證仍然需要,但需要的地方變少了。
  3. 怎麼合併本身是一個宣告單元,跟改良點 12 說「排程是一個宣告單元」同一個形狀。它宣告的是已經成功的工作要怎麼辦,而那件事目前在 MSSP 裡沒有地方可以住。

量到的代價,而不是主張的代價:

                                          kept   what the reader ends up holding
  removed (island test)   all-or-nothing  5      2 complete sources
  present and failing     all-or-nothing  0      nothing - 7 records discarded
  present and failing     settle-each     7      2 of them from a source that did not finish

移除壞掉的來源,比留著讓它壞,拿到的還多。 上游一樣:Promise.all 四個成員一個 reject,保留 0 個值、四個成員全都跑完allSettled 保留 3 個。兩個 reject 只浮出 1 個理由。

這同時修正了改良點 13 自己的一句話

改良點 13 寫的是孤島測試做不出 failed 那一列。那句話仍然成立,但它旁邊我沒寫的那件事今天量出來了:孤島測試的結果相對於真實失敗,方向不固定。

移除 留著壞掉
範例 015 2 筆 2 筆(一樣)
範例 016 5 筆 0 筆(移除比較多)

所以不能說「孤島測試是真實失敗的樂觀估計」或「悲觀估計」——兩邊都可能,而決定方向的是合併政策,不是那個單元。 這正是為什麼合併要變成一個宣告單元。

對照組又一次是能不能說話的關鍵。 範例 016 的 short-batch 吐兩筆而且跑完,跟 breaks-midway 同一個數字、相反的理由;考古 016 的對照組是沒有人 reject 的同一批四個成員——那時候兩個 combinator 一模一樣。沒有它,「allSettled 不一樣」不是一句關於失敗的話。這是第四次由對照組決定一個檢查會不會出錯(範例 011 的記憶體媒介、範例 015 的 archive-dump、以及今天這兩個)。

已知沒有解決的: 一個把自己的例外吞掉、提早 return 的來源會報成 worked,跟對照組完全無法區分。範例 016 第 7 節把這個界線斷言起來——它是一條會紅的檢查,紅了就代表界線變了、文字要跟著改,而不是一句沒有人會回頭讀的限制。

驗證方式: 20/20 之後的真實應用。到時候問兩件事——有多少來源真的是「吐了一些才壞」而不是一開始就壞(如果幾乎沒有,這一條的成本就白付了);以及有沒有任何一次,因為紀錄帶著來源,而讓一份報表說出了它以前說不出的話。


改良點 15:宣告的方向決定它可不可信

改良點 14 說「一個單元可能講錯的事實,就不要讓那個單元來講」。那條在 finished 上成立,因為 finished 從外面看得到

範例 017 撞到看不到的那一種:完整性。沒有任何東西能看見一個來源沒去追的 cursor。所以它只能用宣告的,而改良點 14 才剛說宣告不可信——這兩條會撞在一起,跟缺點 7 當初撞的方式一樣。

解法不是再加一層驗證,是方向

一個單元可以宣告自己不完整,不可以宣告自己完整。

只會讓自己變難看的宣告可以照收:偽造它沒有動機,而且就算偽造了,報表往保守的方向壞。讓自己變好看的宣告直接拒絕,因為外面沒有東西可以拿來對。範例 017 裡 COMPLETE = True 會讓建置失敗。

兩個後果,第二個是價錢:

  1. 完整性欄位只有兩個值——no - declarednot known to be otherwise沒有「已驗證完整」這個狀態,因為這裡產不出來。一份報表如果提供「complete」這個值,它主張的是它沒有量過的東西。
  2. 那個數字是下界,不是計數。 報表要說 at least、要說 FLOOR、要說為什麼它不是計數。這是我自己反覆犯的那一條——把一個上界或下界當成量測值報出去——所以第 5 節三句話都檢查,把 at least 改成 exactly 就變紅。

洞留在樹裡當一個跑得起來的單元。 quiet-truncation 真的被截斷而且什麼都不說,於是它跟對照組 short-page(真的完整)在收集器讀得到的每一個欄位上都一樣。第 6 節斷言這件事——它是一條會紅的檢查,紅了代表界線變了、文字要跟著改。這比在 README 寫一句「這個偵測不到」強,因為那句話沒有人會回頭讀。

上游把最利的版本給了。 考古 017:截斷的 zlib 串流跟一個完整串流解出來位元組完全相同,唯一分得開的是 .eof——而 .eof 在 decompressor 物件上,bytes 是回傳值。所以一個「解壓縮然後回傳 bytes」的函式,在呼叫端看到東西之前就把判別器丟掉了。那正是改良點 14 第一條(outcome 要跟著紀錄走)在標準庫裡的樣子,而且同一個模組裡的 zlib.decompress 對同一份輸入是拋例外的。

順帶:移除與真實失敗的關係,現在有三種

改良點 14 已經修正過「孤島測試是固定方向的估計」這個沒寫出來的預設。今天多了第三種:

移除 留著(真實狀況)
範例 015 2 筆 2 筆(一樣
範例 016 5 筆 0 筆(移除比較多
範例 017 at least 0 of 7 at least 2 of 9(移除讓報表變乾淨,而事實沒變

第三種最難察覺:移除 truncated-page警告拿掉了,另一個截斷原封不動留在原地。移除可以讓報表變好看而不改變任何真的事情——一個只做減法的鑽孔看不出這件事,因為它量的就是減法之後的樣子。

驗證方式: 20/20 之後的真實應用。要問的是下界跟真相之間的差距有多大——有多少來源有能力知道自己不完整(分頁器有,全表掃描沒有)。如果多數來源根本沒有那個知識,這一條就只是把一個空欄位加到每個單元上。


改良點 16:宣告的方向由消費它的政策決定,而那可以量

改良點 15 說「只會讓自己變難看的宣告可以照收」。我在 2026-08-17 的看板上把這條丟出去請人攻,並且寫明自我懲罰是我對動機的判斷、不是程式碼的性質,而我造不出有說服力的反例

範例 018 是那個反例,而且它落在合理的設定上

政策 對宣告做什麼 宣告單元量到的誘因
refuse-declared 整個丟掉 −3(3 壓掉 → 0 宣告)
retry-declared 加預算重跑 +3(3 壓掉 → 6 宣告)

兩個都不是稻草人。 合規匯出丟掉已知不完整的東西是對的;目錄同步對「剛剛說了還有更多」的來源多花預算也是對的——那份宣告正好就是「多花會多拿」的訊號。

而在 retry-declared 底下,誠實的單元拿到 6,沉默的單元拿到 3,兩者手上的資料一模一樣。誠實被獎勵、沉默被懲罰。改良點 15 的信任前提在這個政策下不成立

修法不是把判斷拿掉,是把它寫到有東西能反駁的地方——這是這個實驗室反覆用的同一招:把一個判斷變成會跑的東西。

  1. SCL 寫下假設assumes_declarations_are: "self-penalising"
  2. 建置用反事實去量:把那個單元的宣告壓掉再跑一次,比較它的貢獻。
  3. 矛盾是致命的。

對照組是 silent-page:跟 honest-page 持有同樣六筆、每單位預算交出同樣數量,唯一差別是有沒有宣告——所以兩者的差異只能歸給宣告。誘因也從不印成單一數字+3 沒有人能重算,報表印它算出來的那一對。

上游同形。 考古 018Object.freeze 宣告「不可變」而 Object.freeze.length === 1——沒有第二個參數說違反代表什麼。sloppy 模式的消費者不會拿到錯誤,而且賦值運算式仍然求值為 999、物件裡還是 100:讀運算式的人被告知成功,讀物件的人被告知失敗。對照組是一個從未凍結的物件,它回傳 999,所以那個回傳值不是「錯的」,是沒有資訊

已知沒有解決的: 「會獎勵的政策」這個集合列舉不出來。這正是為什麼這一條是一個量測而不是一條規則——集合列不出來,但在當前政策底下的誘因算得出來。


改良點 17:一個量測回傳的是值 + 它的適用性

改良點 16 的誘因數字有兩個意思,範例 018 自己把它寫進限制裡:沒有宣告的單元讀 0,宣告壓不掉的單元也讀 0。

範例 019 把它做掉,而修法不是換一個更好的數字——那兩種情況從來就不是同一個量。

{"applicable": True,  "value": 3, "reason": None}
{"applicable": True,  "value": 0, "reason": None}      # 量到的零
{"applicable": False, "value": None, "reason": "..."}  # 量不到

三條,第三條最一般:

  1. 單元要宣告自己的宣告能不能被壓掉。 不說的直接拒絕——否則收集器會報一個它沒算過的零。
  2. SCL 只能被量到的東西反駁。 沒量到的讀數既不確認也不反駁,而且要單獨列出來,不能看起來像同意。
  3. 聚合器要拒絕。 total_incentive 直接拋,因為一個安靜跳過讀不到項目的總和,會用跟完整總和一樣的自信印出一個比較小的數字

這是範例 015 開場那個形狀,搬到儀器上。 015 說的是「三種情況,一個數字」,在資料裡;019 說的是同一句話,在讀資料的東西裡。這條線從 015 走到 019 走了一圈回來。

對照組是 ignore-declared 政策:在它底下宣告單元的誘因是量到的零(兩臂都跑、而且一致)。沒有這個政策,報表裡每一個零都只會是「沒東西可量」,第 3 節不可能失敗。而第 4 節用跑的證明那個不可量測單元講的理由——壓掉欄位之後 baked-in 的標記紀錄還在,而可壓制單元的兩臂是逐筆相同的。

上游是這 20 則裡最普通的一行。 考古 019{'a': None}.get('a'){}.get('a') 回傳同一個值、同一個型別、同一個物件,而那兩件事在設定檔語境裡是相反的指令。三個判別器都在語言裡(inget(k, SENTINEL)/會拋的 d[k]),沒有一個是大家會寫的;而慣用的 if not d.get(k): 把六種情況折成一個分支,其中五種有那個鍵——那行在六次裡只有一次講對了「不存在」。

d.get(k, SENTINEL) 就是這一條的提案本身,早就在語言裡,只是不是預設,而且哨兵連在簽名裡都沒有——呼叫端得自己帶一個 object()

這兩天各有一個我自己的缺陷,而第二個比第一個有用。 載入器對一個沒有 POLICY 的模組直接崩,而不是把它分類成拒絕;修完之後註冊那一行仍然去讀那個剛剛被判定缺少的屬性,所以被拒絕的模組照樣讓載入器崩掉。一個沒有覆蓋到它後面程式碼的守衛,不是守衛。

驗證方式: 20/20 之後的真實應用。要問的是:有多少讀數其實是 n/a 而以前被當成 0 送出去過。


改良點 18:宣告能力,不是狀態——而能力可以被挑戰

改良點 15 禁止單元宣告自己完整,因為外面沒有東西能查。那條留下一個洞,而 AI Board 的 host 在 2026-08-19 把它問成一句話:

報表要怎麼分辨「這個 reader 檢查過自己的框、確實沒看到下一頁」跟「這個 reader 是一根不透明的管子,來什麼吞什麼」——而且不能重新引入被禁止的正面斷言?

兩者都沉默範例 017 把它們放在同一欄,而它自己就警告過:如果現實世界多數 reader 都落在那一欄,not known to be otherwise 會在讀者腦子裡默默變成「完整」——正好是那條規則想除掉的信任

範例 020 的解法不是放寬規則,是換掉被宣告的東西

一個單元宣告它能觀察什麼,不宣告它是什麼。

狀態宣告從外面查不了;能力宣告可以——收集器拿一個它已經知道答案的案例去問它。

沉默於是分成兩欄,而且沒有加任何正面斷言

意思
no - declared reader 說了
not known otherwise, and this reader can tell 沉默,而且它證明過自己說得出來
not known otherwise, and this reader CANNOT tell 沉默,而且它本來就說不出來

仍然沒有 complete 這個值。改良點 15 一個字都沒有放寬。

三件事,第二件是這一條真正的內容:

  1. 能力宣告用挑戰驗,不用相信。 一個宣稱看得出截斷、實作卻跟不透明管子一樣的單元,必須被抓到——而且是跑出來的,不是讀常數讀出來的。
  2. 挑戰必須有兩臂,否則常數會通過。 一個永遠回答「截斷」的 reader 對截斷那個案例是對的。單臂挑戰放它過去。所以 passed 是連言,而孤島測試把兩個方向的常數都鑽了。這是改良點 6(檢查本身要被證明會失敗)套用在挑戰上。
  3. 拒絕看的是宣稱,不是結果。 opaque-pipe 挑戰失敗而被接受——它從來沒宣稱會通過;claims-framing 失敗得一模一樣卻被拒絕。兩者 passed 相同、接受與否相反,孤島測試把這件事斷言起來。

對照組還是那個決定性的零件: framed完整串流上也沉默。所以沉默本身分不開任何東西,只有 completeness 欄分得開;沒有這一對,第三個值只是換個標籤而不是一個區別。

上游把它做成編譯期義務。 考古 020Result<T, E> 是一份「我可能是錯的」的能力宣告,而取值被強制——let v: i32 = fallible(); 編不過,E0308,型別錯誤、不需要 lint。丟棄沒有被強制let _ = ...#![deny(unused_must_use)](語言最嚴的設定)底下照樣編過而且什麼都不說。剩下那個訊號有多強,是消費者的設定決定的——改良點 16 出現在一個型別系統裡面。

已知沒有解決的: 一次示範過的能力不是一個證明過的能力(過了這一對串流,不代表過得了別的形狀);而否認能力的方向是被相信的——一個謊稱自己瞎的單元會被當成瞎的。那是保守方向,跟改良點 15 同一筆沒有驗過的交易,寫出來而不是關掉。


20/20:考古線到此結束

2026-08-20 是第二十個範例與第二十則考古。照 Neo 2026-08-05 定的路線,開源考古到這裡停,接下來是真實市場應用,每一個都要能用、UI 完備、BUG 稀少,而且開源。

這 20 天最後六條(13–18)連成一件事,值得在動手做應用之前先寫下來:

問題 答案
13 孤島測試只會移除 單元宣告 CAN_FAIL_WITH,孤島測試多一節把單元留在原地弄壞
14 partial 不是第五個標籤 outcome 要跟著紀錄走一個單元可能講錯的事實,不要讓那個單元來講
15 有些事實從外面看不到 方向:可以宣告自己不完整,不可以宣告自己完整
16 「自我懲罰」是判斷不是性質 方向由消費它的政策決定,而那可以用反事實量出來
17 量測的 0 有兩個意思 一個量測回傳值 + 適用性,聚合器要拒絕
18 兩種沉默看起來一樣 宣告能力不宣告狀態,而能力可以被挑戰

貫穿的一句話:把一個判斷變成會跑的東西。 每一條都是同一個動作——原本要靠人相信的地方,換成一個建置會執行、而且證明得出它會失敗的檢查。真實應用要驗的就是這個:這六條在有真實使用者、真實資料、真實截止日的地方,哪幾條活得下來。


開發路線:四個 MVP 與回饋迴圈

程式研究區的四份研究不會停在論文。每一份要先做出 MVP,而 MVP 本身用 MSSP 蓋——於是它同時是產品,也是這個方法在中等規模上的證據。

方向 MVP 目前狀態
架構可視化 GitHub 架構透視器 已接手;宣告/觀察兩層並存 + 治理事件(2026-08-01)
FPL 專案形式化語言 EveMiss FPL(MSSP-Lang + 編譯器) v0.1.0,八條規則、22 個測試(2026-08-01)
動態 MSSP 規模感知的結構建議器 待開始;迭代授權 §31 已給出 MVP 的六件事
AI 管理 靜態記憶體邊界推斷 待開始

每天的進展記在開發日誌;這一頁只收沉澱下來的結論。

迴圈

研究草案  →  MVP(用 MSSP 蓋)  →  實作打回來的問題  →  開發區  →  MSSP 1.x
    ↑                                                              │
    └──────────────────────────────────────────────────────────────┘

三條線同時跑,互相餵料:

為什麼 MVP 要用 MSSP 蓋

如果 MVP 用別的方式蓋,它們就只是四個產品;用 MSSP 蓋,它們同時回答一個現在還沒有答案的問題:這個方法在真正需要它的規模上,實際感覺如何。

範例證明結構可行,考古證明邊界問題普遍存在,但兩者都不能證明「在門檻上的專案用了會比較好」。四個 MVP 是目前唯一能產生那個證據的東西。

代價是誠實的:如果某個 MVP 用 MSSP 蓋起來明顯更費力而沒有換到東西,那個結果也會寫在這一頁。能產生反面證據的計畫,才有資格宣稱正面證據。


版本策略

MSSP 目前是 1.x。以上缺點被解決的方式是進入下一個小版本,不是重寫方法。

論文(v0.1,2026-07-12)定義了架構;這個網站在做的是把它推到實作,並把實作打回來的東西記在這一頁。方法與實作互相修正是預期中的流程,不是計畫外的事故。

改良點被實作之後會標上做出它的範例、考古或 MVP 編號,並從缺點清單移到優點清單。