AI 協助導入
在既有專案導入 blueprint 是判斷性工作,不是初始化工作:
分層結構早就存在,必須先有人「讀懂」它,規則才有辦法把它編碼下來。
blueprint 把這件事切成三個階段,只有中間那段需要智慧判斷:
蒐證 —— 資料夾結構、匯入關係矩陣、模組形狀、套件集中度
- 執行者 — 決定性程序
- 工具 —
blueprint survey
判斷 —— 哪些資料夾是分層、依賴方向往哪、哪些是債務、哪些是設計意圖
- 執行者 — AI Agent(或開發者本人)
- 工具 — 導入作業手冊
驗證 —— 每項違規都要能解釋為真實債務,而不是 config 譯錯
- 執行者 — 決定性程序
- 工具 —
blueprint inspect與 baseline 棘輪
蒐證步驟的必要性
讓 AI Agent 自己從零翻整個專案,又慢又不可靠。survey 直接把決定性的事實餵給 Agent:
npx @kekkai/blueprint survey # 供人閱讀的報告
npx @kekkai/blueprint survey --json # 供工具或 Agent 讀取若 workspace 包含多個 application,survey 會要求指定範圍,不會把 workspace 根目錄 誤判成 starter。請針對要導入的 application 重新執行:
npx @kekkai/blueprint survey --source-root apps/web/src- 頂層資料夾清單與模組形狀證據(
index檔覆蓋率、巢狀深度 —— 作為判斷資料夾式或扁平式模組的依據) - 同層資料夾實例中重複出現的直接子資料夾,列出量測的父路徑、參與的同層實例與明確比例
—— 這只是證據,不會把專案分類為 feature/module 架構,也不提出架構建議 - 資料夾之間的匯入關係矩陣,依匯入次數由高至低排列 ——
呈現設計意圖中的依賴方向,以及屬於債務的逆向依賴(矩陣含測試檔;inspect會把 glob 掃得到的那些排除掉,所以 inspect 的數字會少掉這一段) - 套件集中度 —— 作為
owns(套件歸屬)宣告的候選依據 - 只出現在一個資料夾的具名匯入,而它所屬的套件散在好幾個資料夾
—— 這是「單一具名匯入歸屬」(owns: [{ package, imports: […] }])唯一能對照的證據。
只讀大括號裡的名字,所以透過import * as取用的成員不會被算進去 - 測試檔慣例統計 —— 應歸入
testFiles而非layers的項目
導入流程
於「已有程式碼、尚無 blueprint.config.mjs」的專案執行 init:
npx @kekkai/blueprint initinit 不會硬套 preset,而是掃描程式碼後產出:
blueprint-authoring.md—— 可執行的導入作業手冊:
蒐證數據、推導方法、規則語意(扁平/資料夾式模組的判定、接線後會咬到什麼)、config 結構速覽與驗收條件.claude/commands/blueprint-author.md—— Claude Code 使用者可直接輸入/blueprint-author啟動
接著交給 AI Agent 執行:
claude "Read blueprint-authoring.md at the repository root and execute it end to end."
# codex 亦使用相同的提示語
# 或以單一指令完成:
npx @kekkai/blueprint init --agent claudeAgent 依蒐證數據推導 config,反覆對照 blueprint inspect 直到每項違規都能解釋為真實債務,
再重新執行 init 產出各項成品並鎖定 baseline。
開發者只需要審閱最終結果。
--agent 是刻意設計得最薄的一層:
它在前景以互動模式執行畫面上已印出的那行指令,且跑在你自己 Agent CLI 的權限之下。
確切的安全邊界見安全與信任。
若欲完全跳過編寫流程、即使在既有專案上也直接以框架 preset 建置,可改用 init --preset ——
這是已確認 preset 適用時的快捷途徑。
建議的提示詞
方法不用寫進 prompt —— 蒐證、推導、驗收都在 blueprint-authoring.md 裡。
prompt 只要釘住「怎樣算完成」:
請協助導入 @kekkai/blueprint,並自主完成:
執行 `npx @kekkai/blueprint init --authoring`,
將其產出的 blueprint-authoring.md 全數完整執行完畢
(playbook 自己給的結論就是完整執行 —— 它叫你早退,早退就是做完)。
驗收 —— `blueprint doctor` 要過,另外:
- lint、`inspect --baseline`、原有測試都要過(沒有測試=空泛通過,不用補)
- emitLint 真的接進 ESLint(不留 reference 檔)
- 不改任何 source code —— 既有債(如果有)各自鎖進原生帳本:架構用
`inspect --update-baseline`、lint 用 `eslint --suppress-all`(都只在有債時跑 —— 空帳本是儀式);
零違規就代表完成、帳本留空 —— 那就是成功,不要為了有東西可鎖去製造債--authoring 保證即使在小 repo 上也會產出 playbook(純 init 在檔數低於門檻時會改建 preset、不產 playbook)。
多 application workspace 的零檔案結果不是 starter 判定;套用門檻前,必須先以 survey --source-root 選定 application。
三條驗收各自對應實測中出現過的未完成狀態:整合只做一半、檢核沒跑完、把還債混進導入。
有兩條在特定 repo 上會「空泛地成立」而且這樣就對了:
沒有測試的 repo,「原有測試都過」直接成立、不用去補 test runner;
零債的 repo,鎖帳指令直接跳過、不用為了儀式跑一遍。
全新專案不需要這段 —— init 一個指令就完成;init 跑過之後也可以在 Claude Code 直接輸入 /blueprint-author。
用數字決定規則衝突 —— blueprint impact
實測中最花時間的編寫步驟,是接線前的 rule 衝突判斷:「每條 emitted rule 在這個 repo 會中幾發?」
以前只能把 emitted config dump 出來、自己對著程式碼讀。impact 直接回答這題:
npx @kekkai/blueprint impact # 接線下去會有多紅?
npx @kekkai/blueprint impact --json # 把數字餵給工具或 Agent它用 emitLint 編譯已寫好的 config,再用專案自己的 ESLint、只掛這份 config 去 lint layer 檔案,回報每條 rule 的命中數與最重的檔案。
純資訊、不是關卡 —— 不管中幾發都 exit 0,而且 total 只計接線會真正引入的違規。
隔離環境的 artifact 全部另列、不灌水:parse-error(檔案解析不了、數字不可信)跟 unused-disable-directive(這個 disable 在隔離環境壓不到東西 —— 指向你自己 config 規則的那種 merge 後就消失,真正過期的才會留下)歸在「Isolation caveats」;
blueprint 沒 emit 的 rule id 另列成「這些名字是你自己 config 的」——
那是你的 code 寫在 eslint-disable 註解裡、而這次隔離執行解析不了的名字,回報在那句註解的位置上。
所以那區的數字數的是「被提到幾次」,不是違規幾次,對註解底下那段 code 什麼都沒說;
其中跟上面 blueprint 命中同檔同行的 row,是同一個點透過你家 rule 的名字再看一次,不是第二筆違規。
報告收尾也講明白:命中數是拿來決定 tier 的,不是只拿來壓 suppressions ——
一條你想整片壓掉的 rule,通常該在 blueprint 的 rules 直接宣告 warn/off,suppressions 只鎖剩下的。
驗證有沒有做完 —— blueprint doctor
「導入到底做完了沒?」這問題以前只能靠人記 prompt 的驗收條款。blueprint doctor 把它變成一份唯讀 checklist ——
可以直接塞進 Agent 的驗證迴圈或 CI:
npx @kekkai/blueprint doctor- blueprint.config.mjs 存在
- 沒有殘留的 reference 檔、authoring 產出物或過期的 contract 檔 ——
*.blueprint.*reference 還在就代表 merge 沒做完(最常漏的一步);blueprint-authoring.md與它的/blueprint-author指令檔是作業手冊自己最後一步要刪掉的東西,所以在編寫途中跑 doctor,這條紅是預期內的;
帶著 BLUEPRINT 標記、卻不在emit.agents宣告範圍內的 contract 檔,是沒人維護的孤兒,不准躲在綠燈後面 - eslint 真的接上 emitLint ——
legacy.eslintrc會被標記為「先遷移」,不會無聲留半套 - 宣告的 alias 接得上 toolchain ——
alias 宣告了卻沒有任何工具(tsconfigpaths,或 vite / webpack / vue-cli / next / rsbuild 的 bundler config)解析得到,agent contract 就會把 agent 指向解析不了的匯入;
失敗訊息直接附上 wiring 片段 - emitted rules 在合併後的 config 裡活著 ——
flat config 對同一條 rule 從不合併:後面的 entry 會「靜靜地」整包取代 blueprint 的結構禁令,lint 還是綠的。
doctor 用一個真實 layer 檔解析最終 config,點名弄丟了什麼。
它的 ✓ 也會說出自己的作用範圍:比對的是 config 文字,從不執行 ESLint —— 涵蓋結構禁令,加上每個有開的關卡的承載規則,每層一個探點;
門檻值、套件歸屬的條目,以及只蓋到某一層一部分的合併條目,都不在比對範圍內。
config 解析不開的時候,這條檢查是跳過而不是失敗(見下),並把 loader 的原話引出來 —— 讓缺的套件直接出現在螢幕上,而不是隔著一次npm run lint - 架構乾淨 ——
沒有 baseline 以外的違規;detail 會標明 coverage:幾個 source 檔在 layer 網內,而網外的那些會被點名列出來(有上限),因為「40 個裡面 12 個」是一個讀的人查不了的數字;
再加上幾條 optional gate 有開,以及結構規則本來就永遠開著。
這樣「空網子的綠燈」看得見,不會安靜地騙過你;空網的 callout 還會點名下一步(把 code 搬進宣告的 layer,網子就開始咬) - lint suppressions 帳本沒過期 ——
eslint-suppressions.json裡指向已不存在檔案的條目會讓檢查失敗
三種結果,不是兩種
跑不起來的檢查,不等於通過的檢查。
合併存活那條檢查在 config 解析不開時是跳過、不是失敗 —— 一個你怎麼弄都消不掉的紅,比沒有這條檢查更糟。
但那個「跳過」以前還是算在通過數裡面,於是輸出會變成 ✓ …(skipped) 疊在 ✓ Adoption complete — all 7 checks passed 上面。
現在你看到的是:
⊘ emitted rules survive the merged eslint config (skipped — could not resolve …)
⊘ Adoption unverified — 6 of 7 checks passed, 1 could not run (⊘ above). Nothing failed, and nothing here proves what those checks cover.(banner 是單一行字串,上面看到的換行是終端機折的。)
exit code 沒有變 —— 跳過不是失敗,所以這次執行照樣 exit 0。
這正是「該用 --json 當 gate、不要用 exit code」的理由:
{ "ok": true, "verdict": "unverified",
"summary": "⊘ Adoption unverified — 6 of 7 checks passed, 1 could not run …",
"counts": { "total": 7, "passed": 6, "failed": 0, "skipped": 1 },
"checks": [ { "label": "…", "ok": true, "skipped": "why it could not run" } ] }ok 維持 exit code 需要的那個意思 —— 沒有任何一項失敗。verdict 是 complete / unverified / incomplete 三選一,也掛在 runDoctor 的回傳值上,
所以 CI 的 gate 該看的是 verdict 或 counts.skipped。
還有一件事,綠燈會直接講出來 —— 放在 banner 底下而不是當成第八條檢查(它不可能失敗,列進去只會灌大分母):
在沒有版本控制的 repo 上,每一條檢查都可以過,而導入寫下的東西一個都沒被 commit ——
一副只活在未 commit 工作目錄裡的棘輪等於沒裝,因為下一次 clone 從零開始。
要不要起版控是擁有者的決定,永遠不是導入中的 Agent 的。
既有債務 —— 弄紅,然後上棘輪
導入的意義是把債弄成看得見、然後鎖住不准變多 —— 不是把螢幕弄安靜。
碰上已經有違規的 repo,severity 保持 error,把兩側的債各自鎖進原生的帳本:
- 架構債 →
npx blueprint inspect --update-baseline(.blueprint-baseline.json—— 就是你認識的那個棘輪) - lint 債(maxLines、unusedVars⋯)→
npx eslint . --suppress-all(ESLint ≥ 9.24 原生 bulk suppressions —— 按「檔 × 規則 × 數量」記帳,新增違規照樣紅)
你的 gate 兩個都跑 —— eslint 跟 blueprint inspect --baseline —— 各自只擋「新」債。
兩份檔案、一套紀律:blueprint doctor 會驗兩本帳都沒過期。
還在 ESLint 8 或 legacy .eslintrc?
suppressions 需要 ESLint ≥ 9.24 + flat config,那次遷移由你拍板、playbook 不會擅自動手。
過渡期的替代方案:emit: { lint: { severity: 'warn' } } ——
但代價要講明:severity 只蓋結構規則(maxLines 這類度量規則吃自己的 tier),而且 warn 期間新的度量債不會被擋。
失敗情境的處理原則
所有產出結果都在任何 AI Agent 啟動之前就寫入磁碟。
啟動失敗、或 Agent 中途放棄,流程就回到手動路徑 —— 同一份作業手冊,改由開發者親自執行。inspect 唯讀、init 冪等、baseline 只在最後一步寫入,所以不存在「導入到一半」的中間狀態要清理。
同樣的順序在 init 內部又出現一次:所有檔案寫入都排在依賴安裝之前,
所以一次被中斷的執行留下的是「一棵完整的樹,只少了 node_modules」。
這件事有意義,是因為安裝是唯一一個可能卡上好幾分鐘的步驟 —— 套件管理工具連不到 registry 時會安靜地重試 ——
所以它上面那行會把接下來要跑的指令印出來,說明安靜是正常的、安靜好幾分鐘就代表該把它停掉、自己跑那行(或加 --no-install 重跑),
並且點名停掉會少什麼:package.json 裡的這幾個套件。
在那行跑完之前,任何指名其中一個套件的失敗都是這個缺口,不是導入壞掉。
範圍的誠實界定
作業手冊只承諾「產出 config 並鎖定 baseline」,不承諾幫你清償既有債務。
既有違規會記錄進 baseline,後續透過 baseline 棘輪逐步清償 ——
導入跟債務清償是兩件獨立的事。
反方向的期望也要講明:在乾淨或剛起步的 repo 上,零違規才是正常結果 ——
那是 codebase 乾淨,不是 config 太鬆(coverage 那行會告訴你網子有沒有真的罩到檔案)。
此時 blueprint 的即時價值是「往前看」的:把「以後的 code 怎麼算對」先釘死 —— handbook、agent contract、gates —— 而不是馬上抓出一堆既有問題。
「keep a codebase honest」的起點是在違規發生之前把誠實的標準寫下來;
牙齒會隨著 code 落進 layer 開始咬。
