Community寫作與編輯github.com

FalkWoldmann/skills

用於對無 I/O(sans-IO)Rust 協定核心進行行為保留重構的技能。

skills 是什麼?

此技能專門用於重構無 I/O(sans-IO)Rust 協定核心,這類庫本質上是狀態機。其目標是讓狀態機的狀態、轉換、輸出和時序在程式碼中清晰可見,同時保持行為完全一致。技能會映射狀態、輸入、輸出和計時器,以小型步驟進行重構並逐一驗證,保持公共 API 和輸出順序不變,最後提供總結。它適用於 `quinn-proto`、`str0m` 或解析器、交握、框架或客戶端核心等 crate,並支援小型、中型或大型重構,從簡單的處理器清理到標誌重構為狀態枚舉。此技能不應用於修復錯誤、新增功能、效能調整、審查 PR、撰寫使用者文件或將非同步/基於 socket 的程式碼轉換為無 I/O。

相容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/FalkWoldmann/skills/tree/HEAD/sansio-readability

Installed? Explore more 寫作與編輯 skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

可讀的無 I/O 狀態機

無 I/O 庫是一個將 I/O 抽離的狀態機。呼叫者餵入輸入(handle_*,每個都帶有 now),擷取輸出(poll_transmit、poll_event、poll_timeout),並擁有 socket 和計時器。唯有當人們能夠理解這類程式碼時,它才有存在的價值:無論是下一位維護者、驅動程式的作者、還是根據 RFC 審查程式碼的審查者。每個人都應該能快速回答四個問題:

  1. 狀態:每個長期存在的實體(連線、查詢、串流、工作階段)可能處於哪些狀態,以及每個狀態中存在哪些資料?
  2. 轉換:哪些輸入會將其從一個狀態轉移到另一個狀態,並且每個轉移發生在程式碼中的哪個位置?
  3. 輸出:每個轉移會排入哪些項目(傳輸、事件、寫入),以及以何種順序?
  4. 時間:它何時需要被喚醒,以及哪個規則設定了每個截止時間?

你的工作就是讓這些答案在型別和程式碼中變得可見,同時讓機器繼續完全按照它之前的方式運作。你編輯工作目錄,並以總結作為結尾。

<skill> 下方指的是此技能的基礎目錄,即包含此 SKILL.md 的資料夾。

基本規則

  • 行為保持不變。 在無 I/O crate 中,行為涵蓋看似外觀上的事項:
    • 輸出排入佇列的順序
    • 映射的疊代順序(BTreeMap 或插入順序的 Vec 決定誰優先)
    • poll_timeout 回傳的實例
    • 是否完全發出空寫入或無操作事件
    • 實體失敗或關閉時產生的錯誤
    • 如果你變更了欄位,#[derive(Debug)] 型別的輸出,前提是快照、日誌斷言或錯誤訊息會印出它

透過公共 API 的測試和每個快照都必須不變地通過。如果需要編輯,表示該步驟變更了行為:將其復原。crate 內部引用私有項目的單元測試可能需要與程式碼相同的機械式編輯(變為變體的標誌、重新命名的輔助函式)。僅進行該編輯,保持每個斷言檢查的內容,並在總結中列出測試。

  • 公共 API 保持不變。 保持 pub 項目的名稱、簽章、變體和欄位不變,因為驅動程式和下遊 crate 依賴它們(以及語意化版本)。pub(crate) 和私有項目則不受限制。將 API 改進作為建議放入總結。如何檢查在第 5 步中。
  • 不要為了可讀性犧牲效能。 在每個輸入(每個封包、訊息或查詢)執行的路徑上,不要新增新的 clone、分配或對映射的額外遍歷。mem::replace 或 mem::take 是免費的;為避開借用檢查器而 clone 一個 Vec 則不是。
  • 以小型步驟進行。 每個步驟都必須能建置並通過檢查,這樣失敗的檢查就能指向一個變更,並且 diff 可以逐塊審查。
  • 遵循專案規範。 在編輯之前,閱讀其 agent 和貢獻者文件(AGENTS.md、CLAUDE.md、CONTRIBUTING)、lint 配置和 CI 工作流程。比照周圍的註解密度和慣用語法。如果專案將文件與程式碼保持同步(生命週期表、將行映射到函式的註解、規格涵蓋表),請在同一步驟中更新它們。

根據變更規模調整工作量

安全網的目的是捕捉現有測試可能遺漏的行為變更,因此請根據你計劃的變更規模來調整:

  • 小型。 一個處理器或少數輔助函式,不變更儲存狀態的欄位。在變更前後各執行一次專案的檢查,並跳過下方標記為大型的所有事項。在總結中,僅映射你觸及的實體。
  • 中型。 一個模組中的多個轉換或輸出輔助函式。在每個步驟之前和結束時執行檢查。僅在現有測試未鎖定你將變更之路徑的確切輸出時,才建置一個差異化測試工具,且只針對那些路徑。不需要模糊測試、額外的屬性測試案例或基準測試:專案在正常設定下的檢查就是這裡的適當權重。
  • 大型。 將標誌替換為階段枚舉、將狀態欄位的多個寫入者合併到轉換中、重塑大多數處理器。使用完整的工作流程。

在繪製地圖(第 2 步)後決定規模。如果變更在進行到一半時增大,則提高一個規模級別,並在總結中說明。

工作流程

1. 設定安全網

在第一次編輯之前執行此操作,這樣之後的每個失敗都是你的問題,而不是預先存在的。

  • 記錄基準。 執行 git status 並記錄基準提交:之後的每次比較都以此為基準。如果你計劃觸及的檔案已經有未提交的變更,請在將你的編輯與它們混合之前告知使用者。如果你決定在其之上繼續,請使用 git stash create 作為基準:它會將它們記錄為一個提交,而不會觸及工作目錄。
  • 確定一個檢查指令。 優先使用專案自己的檢查指令(CI 工作流程、justfile、xtask、其 agent 文件中的指令)。否則使用隨附的閘道:<skill>/scripts/check.sh <repo> default all。列出 CI 使用的功能集,例如 default all none tls。閘道會在不上色格式的情況下檢查格式,然後對每個功能集執行 clippy 和測試,並在有任何失敗時以非零狀態碼退出。使用 CHECK_PACKAGES="-p <crate> -p <dependent>" 將範圍限制在你變更的套件及其依賴者,並在最後對整個工作空間執行一次。如果 CI 還執行了其他項目(MSRV 檢查、額外的 lint 標誌),請一併執行。
  • 如果基準不為綠色,請不要修復它。 記錄哪些檢查失敗以及如何失敗,告知使用者,然後從那時起預期相同的失敗,且不預期新的失敗。僅對你編輯的檔案進行格式化。由環境而非程式碼導致的失敗(例如全域 cargo 配置包含僅限 nightly 的標誌、缺少工具鏈或工具)應列在「驗證」下,說明「無法執行」並附上原因。如果 repo 沒有 CI,則從 rust-version、edition 和使用的語言功能中取得最低工具鏈版本,並說明你能執行和不能執行的項目。
  • 大型變更:建置差異化測試工具,當現有測試未鎖定你將變更之路徑的確切輸出時。鎖定意味著追蹤或快照測試會斷言輸出序列和截止時間;僅對最終狀態的斷言不算數。該測試工具會在同一組隨機呼叫上並行執行基準版本和工作目錄版本,並比較每個輸出。<skill>/scripts/difftest_scaffold.py 會在 repo 外部建立 crate 對和模板。填寫模板是真正的工作,因此請為此預留時間:先閱讀 references/differential.md。在依賴它之前,檢查它是否能捕捉到一個植入的一行突變;一個就夠了。
  • 保持測試工具比重構更廉價。 驅動你重構的路徑,而不是整個 crate。如果涵蓋一條路徑的成本超過對該路徑的變更,則改為在「驗證」下報告此差距,並依賴該路徑的現有測試。範例:位於 TLS 或加密後面的路徑、你必須正規化的非確定性、你必須模擬的對等端。
  • 僅大型變更:使用更多案例執行一次慢速檢查。 執行屬性測試,將其規模設定為在幾分鐘內完成(預設的十倍,或當案例低廉時為 PROPTEST_CASES=10000),並在結束時再次執行。僅當模糊目標能觸及你將變更的程式碼時才執行它,時間限制為 30-60 秒。僅當變更會對每輸入路徑增加工作量時才執行基準測試;重構控制流程不需要它們。
  • 填補應比變更更長久存在的涵蓋缺口。 如果你將重構的路徑沒有測試涵蓋,且你未建置測試工具,請先新增鎖定其目前輸出的測試。如果你正在建置測試工具,它涵蓋了重構的缺口;僅為其揭露的、值得保留的行為(你有意保留的怪癖)新增測試。將新測試放在新檔案或模組中,作為獨立步驟,以便現有測試保持不變。例外情況是單一 crate 測試佈局(例如 tests/main.rs)所需的 mod 行。

2. 繪製機器地圖

繪製請求涉及的實體,以及計劃變更將觸及的任何實體。在請求未說明哪個部分重要的大型 crate 中,選擇使用者命名的實體,並說明你遺漏了哪些。在變更任何內容之前,先寫下地圖(草稿檔案即可)。它將成為總結的狀態地圖,並且通常是模組文件。

  • 實體。 對於範圍內的每個實體,列出其欄位,並按它們在有意義的階段進行分組。組合的 bool 和 Option 如果編碼了一個階段,則是主要目標。
  • 輸入。 每個接受外部資料或 now 的 pub fn,以及每個能觸發的內容。
  • 寫入者。 對於每個狀態欄位,每個對其賦值的位置(搜尋 .field =、mem::replace、.take()、+= 1)。具有多個寫入者的欄位是分佈在檔案中的轉換。
  • 輸出。 每個推入輸出佇列的操作,以及 poll_timeout 讀取的每個截止時間。
  • 不變量。 必須始終成立的條件:一個 ID 恰好存在於一個映射中、每當有東西等待時設定的截止時間、存在被引用的連線。
  • 怪癖。 看似意外的行為:寬鬆的解析、兩個錯誤之間的奇怪優先級、對垃圾數據的回覆、截止時間後仍接受的輸入。你保留它(規則 1),但要為使用者列出。

然後將地圖寫成表格:狀態、輸入、下一個狀態、輸出。如果你無法在不追蹤幾個函式的情況下從程式碼中填寫它,則表示程式碼還不夠清晰,而那就是工作所在。

3. 選擇變更

references/patterns.md 是目錄,包含之前/之後的範例以及每個模式不值得使用的情況。閱讀其索引,然後根據你的地圖找到的內容閱讀相關章節,包括其「不值得」段落。對於大型變更,請閱讀整個檔案。根據每個變更在多大程度上縮短對四個問題之一的回答,以及 diff 的大小,對候選變更進行排名。通常最有回報的動作:

  • 一個枚舉,其變體攜帶每個階段的資料,取代編碼階段的標誌和選項(模式 1)
  • 每個轉移一個命名的函式,以便每個狀態欄位有少量寫入者(模式 2)
  • 處理器匹配輸入、決定、應用轉移並排入輸出佇列,沿著一個一致的調度軸(模式 3)
  • 每種輸出一個輔助函式,以其意圖命名(模式 4)
  • 每個截止時間規則只寫一次,poll_timeout 從狀態推導(模式 5)
  • 在讀者能看到的生命週期,以及對欄位的不變量(模式 9)

如果使用者命名了一個函式或一個部分,則僅變更該部分及其所需的內容。你提取的輔助函式可能會被其自然的同級重複使用,但不要僅因為輔助函式也適用那裡就重構請求之外的程式碼。而是將這些改進列在「建議」下:要求審查一個函式的審查者不應該需要檢查第二個。

忽略那些只移動程式碼、對區域變數重新命名以符合個人喜好、以及任何讓程式碼變長但沒有讓問題更容易回答的變更。當使用者要求移動本身(將長檔案拆分為模組)時,請參閱模式 11。已經可讀性良好的程式碼可以保持原樣:在總結中說明,而不是發明工作。

4. 逐步應用

排列步驟的順序,使每個步驟都可審查,並且不會新增後續步驟會刪除的程式碼:

  1. 純粹的添加和提取:輸出輔助函式、命名謂詞、截止時間輔助函式、功能門控步驟移入輔助函式、線路編碼和解析移入函式(模式 4、5、6、10)。
  2. 核心變更:階段枚舉,以及隨之強制執行的轉移和處理器形狀(模式 1–3、7、12)。這通常必須作為一個步驟完成。如果是,請說明原因。
  3. 名稱(模式 8)。
  4. 文件(模式 9)。
  5. 重新排序方法,如果有的話,最後且單獨進行(模式 11)。

在編輯之間,使用快速循環:clippy 和你正在變更的套件的測試,使用預設功能集,加上你建置的測試工具(如果有的話)。在每個步驟結束時,執行第 1 步中的檢查指令。然後,如果使用者想要提交(他們要求,或專案的工作流程期望提交),則將該步驟作為一個提交;否則將工作保持未提交狀態,並在總結中列出步驟。不要使用下一個步驟會移除的 scaffolding(僅為讓一個步驟變綠而添加的 cfg 門控或 #[allow])來靜默 lint。改為重新排列步驟。

在任何借用檢查器強制將模式扭曲的地方放棄該模式,因為扭曲的程式碼比原先的更難讀。通常的跡象:

  • 一個方法將 self 的同級欄位(輸出佇列,例如 &mut VecDeque<Transmit>)或其自身的映射鍵作為參數,只是為了通過借用
  • 一個 &self 方法只為了能讀取配置而接受一個子結構
  • 克隆的鍵或額外的查找

通常的修復方法:

  • 回傳輸出,讓呼叫者將其排入佇列
  • 將輔助函式放在實體上,並傳入其需要的 Copy 配置值
  • 保持程式碼內聯

5. 驗證並總結

最後一次執行檢查指令,針對整個工作空間,以及你在開始時執行的任何其他指令:慢速檢查和測試工具。

然後,如果 diff 觸及了一個 pub 項目、一個 derive、一個 trait 實現、一個欄位型別(自動 trait 如 Send 會跟隨私有欄位)或一個項目上的 cfg,則檢查公共 API:

  • 在每個功能集上使用 cargo semver-checks --baseline-rev <base> 一次,使用明確標誌:--default-features、--all-features、--only-explicit-features(無),以及 --only-explicit-features --features x。
  • 它只會捕捉破壞性變更,因此也要確認沒有新的東西變成公開:如果已安裝,使用 cargo public-api diff,或閱讀 git diff <base> | grep -E '^\+.*\bpub\b'。
  • 如果沒有安裝工具,請在安裝前詢問,否則報告 API 檢查未執行。
  • 如果 diff 僅觸及函式主體和私有項目,則說明該情況。

相關技能