執行 check 腳本
此技能是在 lt-dev 工作流程中執行 package.json 的 check 腳本的單一事實來源。所有需要確保專案可執行性的審查者、重製者和協調者都必須嚴格遵循此程序——在各代理間重複規則會導致偏差。
目標: 一個完全通過的
check執行(exit code 0)是任何審查或重製被視為完成的必要前提。唯一可接受的殘留問題是已經用盡完整修復升級階梯的上游相依性漏洞。
何時使用此技能
| 呼叫者 | 階段 | 觸發時機 | 模式 |
|---|---|---|---|
/lt-dev:review | 第 1.5 階段 | 在產生任何專門審查者之前 | 建議性 |
lt-dev:code-reviewer | 第 1.5 階段 | 在單次審查之前(若協調者已執行則跳過) | 建議性 |
lt-dev:test-reviewer | (輸入簡報) | 遵循此處定義的跳過語意 | 建議性 |
/lt-dev:take-ticket | 第 8 步 | 在所有測試支柱都通過後 | 阻塞性 |
/lt-dev:git:ship | 第 1 步,第 4b 步 | 預檢,以及在重製後重新驗證 | 阻塞性 |
/lt-dev:ticket-cycle | 經由 take-ticket + git:ship | 完整循環 | 阻塞性 |
lt-dev:branch-rebaser | 第 6.5 階段 | 在 lint/format 之後,測試之前 | 阻塞性 |
呼叫者模式
兩種模式的程序完全相同。差別僅在於未解決的錯誤如何處理,而這正是區分的關鍵:
- 建議性 — 呼叫者的工作是回報。紅色的
check會被標記為嚴重發現,工作流程繼續進行,因為使用者仍希望獲得其餘審查。 - 阻塞性 — 呼叫者的工作是發送。紅色的
check會停止工作流程。不會推送任何內容,不會建立 MR/PR,也不會合併。
所有導向 MR/PR 或合併的路徑都以阻塞模式執行。 紅色的 check 到達基礎分支會讓整個團隊付出代價:所有人的 CI 都會變紅,下一個 take-ticket 會從損壞的程式碼開始分支,而最終修復問題的人完全不知道是哪個變更造成的。這就是為什麼第 3 步的階梯沒有迭代次數上限,以及為什麼第 5 步的殘留清單如此狹窄。
程序
第 1 步 — 探索 check 腳本
使用專用的輔助腳本(位於 lt-dev 插件中):
bash "${CLAUDE_PLUGIN_ROOT}/scripts/discover-check-scripts.sh"
輸出為 TSV 格式,每行一個專案:
<package.json 路徑>\t<check 腳本>\t<包含測試:no|unit+api|yes>\t<套件管理器>
includes_tests 的值 — 呼叫者決定每個值的處理方式,探索本身不規定任何行為:
| 值 | 意義 | Playwright 涵蓋範圍 |
|---|---|---|
no | 在 check 鏈中未檢測到測試執行器。 | 未涵蓋。 |
unit+api | check 鏈委派給 lt check.mjs 包裝器,它會執行單元測試 (應用) + API 測試 (api) 步驟,並刻意跳過 Playwright(Playwright 透過 lt dev test / CI 執行 — 請參閱 scripts/check.mjs)。 | 未涵蓋 — 如果需要,Playwright 仍需單獨執行。 |
yes | 在 check 鏈中直接匹配到 test、vitest、jest 或 playwright 模式(遞迴解析一層複合腳本)。範圍由腳本本身定義。 | 未知 — 如果需要 Playwright 涵蓋範圍,呼叫者應在本地驗證。 |
輔助腳本:
- 使用
git ls-files "package.json" "**/package.json",因此會遵守.gitignore並跳過node_modules - 透過
jq→node -e→grep+sed回退鏈解析 JSON(即使沒有安裝jq也能運作) - 解析單層複合腳本(
"check": "pnpm run ci"→ 查詢ci) - 透過 lockfile 檢測每個專案的套件管理器(
pnpm-lock.yaml→pnpm等;預設pnpm)
如果輔助腳本回傳空值 → 沒有專案定義 check → 跳過此技能的其餘部分。
第 1a 步 — 無 oxlint 建議修正
oxlint --fix-suggestions 會套用 oxlint 自身標記為可能改變行為的修正:其 no-console 建議會刪除該呼叫。--fix-dangerously 則更進一步。目前的範本兩者都不傳遞,但來自較舊範本的專案仍可能在 check 包裝器、lint:fix、lint-staged 或專案自己的 scripts/check.mjs 中傳遞它們。lt fullstack update 會在工作區根目錄和應用程式中移除它們(僅限受追蹤的乾淨檔案);API 專案保留自己的檔案,因此若該處有命中,則需手動移除或取得目前的 nest-server-starter 檔案。在 nest-server 中觀察到(2026-09,claude-code PR #10):一個提交鉤子將 console.log 變成了一個空的函數體,但仍然回報「通過」。在第 2 步之前,請尋找這兩個旗標的使用;註解中提到不計:
git grep -n -E "(^|[[:space:]\"'])--fix-(suggestions|dangerously)" -- '*package.json' '*.lintstagedrc*' '*lint-staged.config.*' '*scripts/check.mjs' \
| grep -v -E '^[^:]+:[0-9]+:[[:space:]]*(//|/?\*|#)'
第二個過濾器會移除註解行:目前的 nest-server-starter 包裝器在註解中說明了它為什麼只傳遞普通的 --fix。
若有命中,則檢查在沒有自動修正的情況下執行:
- 以唯讀閘門方式執行 lt 包裝器:
<pm> run check --no-fix(npm:npm run check -- --no-fix)。每個仍傳遞其中一個旗標的包裝器都已經知道--no-fix。如果專案自己的鏈呼叫了lint:fix,或其自己的scripts/check.mjs是命中的內容之一,則以lint代替lint:fix來執行該專案的步驟。 - 手動或使用普通的
oxlint --fix修正 lint 發現;像往常一樣使用專案的格式化工具進行格式化。 - 在第 8 步報告中命名每個命中(
file:line),並附上其補救措施:根目錄和應用程式使用lt fullstack update,API 專案則手動移除旗標(或使用目前的 nest-server-starter 檔案)。
第 2 步 — 對每個專案執行 check
對於每個發現的專案,cd 到其目錄並使用檢測到的套件管理器執行 check 腳本:
cd "$(dirname <package.json 路徑>)"
<套件管理器> run check
捕捉 stdout、stderr 和 exit code。追蹤一個從 1 開始的迭代計數器。
第 3 步 — 自動修正迴圈(反覆迭代直到完全通過)
如果專案的 check 以非零 exit code 退出:
- 解析目前執行的所有錯誤(型別檢查、lint、建置、缺少匯入、型別不符、未使用的符號、稽核發現等)
- 從根本原因修正每個錯誤,使用
Read+Edit。預先存在的錯誤也會被修正 — 無論錯誤來自此差異還是早已存在,對專案是否可執行都沒有影響,而紅色的check對下一個人造成同樣嚴重的阻礙。範圍邊界不適用於可執行性。這裡決定一切的只有一個問題:「這個可以修嗎?」,只要答案是肯定的,就會被修正。 - 從頭開始重新執行
check。 絕不相信部分狀態。 - 持續迭代,直到達到以下終止條件之一:
- (a) 通過 —
checkexit code 為 0 → 此專案完成 - (b) 停滯 — 一個完整的迭代沒有產生錯誤數量的淨減少
- (a) 通過 —
- 沒有硬性的迭代上限。 只要每次迭代都嚴格減少錯誤數量,就繼續進行。目標是真正的完全通過。
一個狹窄的例外,這是有關衝突而非範圍。 第 2 點原則不變:預先存在的錯誤會被修正,因為無論是誰寫的,紅色的 check 都會阻礙下一個人。但第 2 點不涵蓋存在於另一個活躍工作階段正在進行中的未提交工作內部的錯誤 — 重構那不屬於範圍問題,而是兩個工作階段同時編輯一個檔案,同行回到一個被重寫的重構時,會損失比修正所節省的時間更多的時間。當樹狀結構是髒的,且 bash "${CLAUDE_PLUGIN_ROOT}/scripts/change-provenance.sh" 將失敗路徑歸因於一個正在運行的同行時:如果錯誤是微不足道且自包含的(缺少匯入、格式違規、遺留的 console.log),則修正它;否則,將其回報為一個阻塞項並指明作者,並附帶一個 SOLVED,包含診斷結果,以便他們能一步修正自己的工作。沒有人正在運行,或無法歸因:第 2 點不變地適用。
停滯是改變方法的信號,而非停止的許可。 當重試相同的修正時,計數會停止下降,因此下一次迭代必須以不同的方式攻擊錯誤:完整讀取失敗的檔案,而不是修補報告的那一行;檢查兩個錯誤是否共享一個原因;查看一個正常運作的同級模組如何解決相同問題;單獨執行失敗的步驟,以查看聚合執行所吞沒的輸出;檢查它是否屬於第 6.5 步到第 6.8 步中已知的危險之一。只有在真正不同的方法被嘗試過而計數仍然保持不變時,第 5 步的殘留分類才適用 — 而在阻塞性模式下,除了已耗盡階梯的相依性漏洞之外,所有內容仍會停止工作流程。
第 4 步 — 稽核發現:強制修復升級階梯
當 pnpm audit / npm audit / yarn audit(由 check 調用)回報漏洞時,在將發現分類為「已接受」之前,請耗盡此階梯。每一步之後都重新執行 check。
| # | 步驟 | 指令 (pnpm / npm / yarn) |
|---|---|---|
| 1 | 更新至最新的相容版本 | pnpm update <pkg> / npm update <pkg> / yarn upgrade <pkg> |
| 2 | 自動修復 | pnpm audit --fix / npm audit fix / yarn audit --fix |
| 3 | 強制 semver-major 升級 | pnpm audit --fix --force / npm audit fix --force |
| 4 | 如果修補程式指出修正存在於下一個 major 版本,則將直接相依性提升至該版本 | pnpm add <pkg>@<major> / npm install <pkg>@<major> / yarn add <pkg>@<major> |
| 5 | 強制指定傳遞相依性版本 | 在 package.json 中使用 pnpm.overrides / resolutions (yarn) / overrides (npm) 區塊,然後重新安裝 |
| 6 | 提升消費者,而非有漏洞的套件 | 拉取它的直接相依性,以及如果 lt 框架固定了它,則提升框架本身(@lenne.tech/nest-server / @lenne.tech/nuxt-extensions)— 完整的 /lt-dev:maintenance:maintain 是徹底的形式 |
| 7 | 修補消費者 — 僅在以下條件下 | pnpm patch <pkg> → patchedDependencies |
| 8 | 更換套件 | 如果套件已被棄用,則切換到有維護的替代品 |
第 6 步是最常被跳過的一級,但它是最有效的一級。 第 1 到第 5 步都在推動有漏洞的套件向前。當這失敗時,原因通常根本不是有漏洞的套件——而是持有它的消費者,透過固定一個有漏洞的範圍或使用已修補 major 版本移除的 API。所以問題是「誰將此版本保留在這裡?」,答案是直接相依性,或是其固定無法被覆蓋的框架。覆蓋是一種固定,而固定嚴格來說比真正的版本提升更差:它會使樹狀結構偏離上游測試的內容,並且會悄悄地腐爛。在有版本提升的地方進行提升。
第 7 步(修補)刻意狹窄,而狹窄正是重點。 當修補的 major 版本僅在載入時失敗 — 一個被移除的預設匯出,一個被重新命名的入口點 — 一行修補看起來像是正確關閉了修補程式,而不是壓制它。有時確實如此。但必須先滿足三個條件,而第三個通常會失敗:
- 修補目標是有漏洞套件中的一個穩定路徑,並且被固定到一個精確的版本。破壞修補的不是
dist/本身——發布的套件只包含dist/和別的什麼——而是會在版本之間移動的路徑。dist/chunks/<hash>.mjs會在下一次建置時被重新命名,然後修補會對團隊中的每個人失敗;dist/index.mjs是套件自己的入口點,並且保持不變。一個命名了最小化符號的修補,搭配版本固定仍然是可接受的,因為固定會將風險變成響亮的pnpm install失敗,而不是靜默的錯誤修補。修補有漏洞的相依性,而不是碰巧匯入它的消費者 — 請參閱下面測量的案例,其中第一次評估查看了消費者的 chunk 並在此基礎上拒絕了該級別。 - 權衡生產閉包與維護成本。修補是對未來每一次升級的持續義務,因此對於一個無法到達生產環境的開發工具路徑,它可能花費的比它買到的更多——在一個單一專案中。一個範本會翻轉那個算術:每個產生的專案繼承一個修補,而殘留則必須在每個專案中重新論證。這是一個判斷,不是否決;條件 1 和 3 是硬條件。
- 載入不等於運作。一個移除了匯出的 major 版本通常也改變了行為,因此修補後的匯入會讓模組被匯入,同時讓消費者在一個它從未測試過的 API 上運行。如果你無法說出該 major 版本中還有什麼改變,以及為什麼在這裡不重要,那麼修補就是一個穿著修正外衣的猜測。
修補有漏洞的套件,而非消費者。 這正是決定該級別是否有效的區別,而且很容易弄錯,因為錯誤指向了消費者。測量案例,lt-crm 2026-10-06:@nuxt/[email protected] 執行了 import Git from 'simple-git',而將 simple-git 提升到唯一已修復的行(4.x,它移除了那個預設匯出)使得 nuxt prepare 因 does not provide an export named 'default' 而中止。第一個評估查看了 devtools 自己的 dist/chunks/module-main.mjs,在那裡找到了匯入,並基於條件 1 拒絕了修補——一個已編譯的 chunk——這是對錯誤檔案的正確判決。修補屬於下一層,在 simple-git 自己的 dist/index.mjs 中,在那裡一個單獨添加的行將模組已經作為 simpleGit 匯出的相同工廠重新匯出為 default。硬化程式碼沒有任何改變;只有模組的表面恢復了。
至此,所有三個條件都成立:它是有漏洞的套件而不是消費者產物,它被固定到一個精確的版本(patchedDependencies: { '[email protected]': … } 加上覆蓋目標),並且 devtools 實際使用的 API——branch、revparse 等方法——都不是已移除的匯出的一部分。它們在 simple-git 內部定義,根本沒有被 patch 改變。載入修補是為了讓匯入語法再次匹配;硬化邏輯保持不變。
第 8 步:如果第 1 到第 7 步對該套件都失敗了,記錄該發現並給出理由。如果它在建議性模式中,該發現會被回報為高優先級,工作流程繼續。如果它在阻塞性模式中:該發現會停止工作流程(一個耗盡了階梯的漏洞是此規則的唯一例外,見第 5 步)。
第 5 步 — 殘留分類
當「停滯」發生時:
- 區分新錯誤和觀察到的錯誤。一個新錯誤是之前迭代中未出現的錯誤——停滯不是分水嶺,它是一個計數淨零的迭代。重新分類是在每次停滯時進行的。
- 對於建議性模式: 將每個殘留錯誤按以下方式分類:
- 臨時 — 一個已知的下一次建置會修復的錯誤(例如,從一個仍在處理中的相依性 PR 中提取了尚未發布的 type,並且
check中的型別檢查階段抱怨缺少匯出)。用一個WORKAROUND標記,指明它如何以及何時會自行清除。 - 上游 — 需要一個尚未發布的相依性修復的錯誤。用一個
DEPENDS標記,引用 upstream 問題或 PR。 - 可接受的漏洞 — 已耗盡升級階梯的相依性漏洞。用一個
ACCEPTED_VULN標記,指明最後嘗試的級別以及為什麼失敗。 - 阻止 — 其他所有。用一個
BLOCKER標記,帶有根本原因和一個到具體錯誤行或設定的鏈接。
- 臨時 — 一個已知的下一次建置會修復的錯誤(例如,從一個仍在處理中的相依性 PR 中提取了尚未發布的 type,並且
- 對於阻塞性模式: 相同的分類,但只有可接受的漏洞允許工作流程繼續。臨時、上游和阻止錯誤都會停止工作流程。
- 永遠不會有「超出範圍」的分類。 可執行性沒有範圍邊界。一個不在你差異中的錯誤仍然是下一個人的阻塞項。
第 6 步 — 最終報告
將結果回報給呼叫者:
- 對於每個專案:通過/失敗,迭代次數,殘留錯誤(如有)
- 對於每個發現:類型(型別、lint、建置、稽核)、檔案 + 行、根本原因、修正嘗試、分類
- 對於每個已修正的錯誤:做了什麼、用了什麼指令、是否被驗證
- 對於每個殘留:由哪個代理分類、標記、下游影響
- 一個總體結論:所有專案是否可執行?
報告格式是結構化的(JSON 或類似),但最終回覆給使用者的是人類可讀的摘要。
常見陷阱
- 型別檢查錯誤不會互相抵消。 如果你修正了 3 個但引入了 2 個新的,淨收益是 1,迭代繼續。修正引入新錯誤是修正策略的錯誤,而不是終止的條件。
- Audit findings 的修正可能引入回歸。 始終在修復相依性後重新執行完整的
check。 - 不要假設兩次相同的輸出意味著沒有變化。 使用第 3 步的全新重新執行。部分狀態是 bug。
- 不要在「從根本原因修正」上偷懶。 看到一個缺失的 type?找到定義它的檔案並匯入它。不要在你的檔案中添加一個臨時類型來讓型別檢查器安靜下來。短期內看起來更快;長遠來看,它保證了技術債務。如果該定義不存在,你的步驟是創建它(如果它應該存在)或對你的特定使用者案例使用
declare module,並附上一個解釋性的註解。 - 如果你修改了一個類型定義,驗證類型消費者是否仍然可以使用它。 有時修正一個類型會在不同的消費者中破壞另一個類型。
- 樣板設定是程式碼。 如果
tsconfig.json、eslint.config.js或.lintstagedrc妨礙了可執行性,它們可以被修正。你不會為了讓測試通過而重寫整個專案架構,但一個將無效路徑列入白名單的tsconfig路徑別名?修正它。一個在不存在的文件上運行的 lint-staged 設定?修正它。這些並不是不可觸及的。 - 當你發現錯誤時追蹤錯誤。 一個煮熟的
tsconfig路徑別名可能會發展出它自己的錯誤類型,然後從整體錯誤計數中消失——但型別檢查器仍然輸出它。不要僅匹配已知模式;如果 exit code 為非零且你沒有捕捉到任何東西,則捕獲原始輸出。
與 lt-dev:take-ticket 的整合
此程序在 /lt-dev:take-ticket 工作流程中扮演關鍵角色。當與 take-ticket 整合時,它作為第 8 步執行,並且始終以阻塞模式執行。如果 check 在 take-ticket 工作流程中失敗:
- 工作流程停止。 沒有票證被領取,沒有分支被創建。
- 使用者會收到一個詳細報告,說明哪些專案失敗了以及為什麼。
- 使用者必須先手動解決問題或使用輔助技能(如
/lt-dev:maintenance)才能繼續。 - 一旦解決,使用者可以重新發出指令;技能不會記住先前的狀態。
與 lt-dev:git:ship 的整合
當與 /lt-dev:git:ship 整合時,此程序會執行兩次:
- 第 1 步(預檢): 在推送變更之前作為閘門。如果失敗,推送到遠端儲存庫會被阻止。
- 第 4b 步(重製後重新驗證): 在與目標分支重製之後,作為一個最終健全性檢查,確認重製沒有引入回歸。
兩個實例都以阻塞模式執行。
常見的障礙與解決方案
第 6.5 步 — 因 node_modules 損壞而卡住
當 check 因不穩定的 node_modules 而以晦澀的錯誤失敗時——主要是 Node.js 版本切換後或從另一個分支拉取後的 C++ 附加元件錯誤。症狀:ERESOLVE、ERR_DLOPEN_FAILURE、gyp 錯誤、遺失的 .node 文件、或 ERR_REQUIRE_ESM。
# 先嘗試這個——通常就夠了
rm -rf node_modules .pnpm-store .yarn/cache
<pm> install
如果專案特定清除不夠,則使用工作區範圍的:
# 對於 pnpm 工作區
pnpm store prune
pnpm install --frozen-lockfile
# 如果 --frozen-lockfile 失敗(鎖定檔案變更),則
pnpm install
提示: 如果你的鎖定檔案是 git clean — git checkout -- pnpm-lock.yaml — 則從基礎分支恢復它。如果鎖定檔案本身是髒的且在 diff 中,請勿還原,因為變更可能是合法的。
在極少數情況下,pnpm store 本身已損壞:
pnpm store prune
# 然後在每個專案中
<pm> install
第 6.6 步 — 因 git 鉤子而卡住
當 check 在 git 鉤子(pre-commit、pre-push)內執行時,可能導致遞歸調用或乾擾。如果 check 調用了 git 命令,而這些命令又觸發了包含 check 的鉤子:
# 暫時禁用鉤子
HUSKY=0 npx --no-install lint-staged
# 或直接
SKIP_HOOKS=true <pm> run check
並注意在自動修正迴圈期間,不要讓你的編輯器自動執行 lint-staged。
第 6.7 步 — 因格式差異而卡住
如果 check 鏈包含一個格式化步驟(例如,prettier --check),而自動修正僅修正 lint 問題,則迭代可能會因格式錯誤而停滯:
# 在項目中應用格式
<pm> run format
# 或者如果沒有 format 腳本
npx prettier --write .
完成後,重新執行 check。
第 6.8 步 — 因快取而卡住
如果 check 使用快取(例如 TypeScript 的 --incremental、ESLint 的快取、Webpack/Vite 的建置快取),舊的快取可能導致誤報或錯過修正:
# TypeScript 增量建置
rm -rf .tsbuildinfo tsconfig.tsbuildinfo
# ESLint 快取
rm -rf .eslintcache
# Vite/Webpack 快取
rm -rf node_modules/.vite node_modules/.cache
# 然後重新執行 check
<pm> run check
第 6.9 步 — 因重複類型而卡住
當 TypeScript 因兩個提供相同全域類型的套件而失敗時,從最不必要的套件中移除:
git diff --name-only --diff-filter=d | xargs head -100 | grep -oE "@types/[a-z0-9_-]+" | sort -u
# 按匯入次數對每個候選者排名
# 移除匯入次數最少的那個
<pm> remove @types/least-used-package
這是特定於定義被合併到主套件中的 @types/ 套件(常見於 React、Node、Express 類型)。在移除任何內容之前,請檢查 node_modules/@types/<pkg>/package.json 中的 types 欄位是否為空或指向一個缺失的文件。如果目標套件在其自己的 package.json 中使用 types 或 typings 欄位提供其類型,則 @types/ 套件是多餘的。
支援與回饋
如有問題或改善建議,請在 lt-dev 儲存庫中開啟一個議題。