Repository navigation
0.4.0 — 重建追逐根治、卸載正確性與對外治理
對應 commit:041ace3(main)
安裝:
dsh plugin --profile web add github:WwW7olFWwW/dsh-codebase-watcher或直接用下面附的預建包(免建置、免 allowBuilds 授權):
curl -L -o dsh-codebase-watcher.tgz https://github.com/WwW7olFWwW/dsh-codebase-watcher/releases/latest/download/dsh-codebase-watcher.tgz這一版的完整說明見 CHANGELOG.md;要點:
這一版由一次四維度審查(程式碼品質/效能/UI-UX/產品市場)驅動,共 8 條工作流。
最重要的一件事:0.3.0 的「45 秒冷卻」只把重建風暴壓低頻率,沒有治好它——實測顯示
一個正在被編輯的專案仍然每 52 秒燒掉一次完整重建,而且每一次都在重建途中被中止,
圖譜從頭到尾沒有追上過。這一版給出可重現的量測工具與根治手段。
量測:npm run bench
新增 tools/bench-dirty-chase.mjs——用真的 CbmKeeper、真的 node:fs.watch、真的計時器,
把 git 探針/CBM CLI/重建本身換成可計數的注入替身,模擬「連續編輯 20 秒、50 次存檔,然後停手」。
時間參數等比壓縮約 1/30(比例不變,可外推),重建替身忠實重現 CBM 的
aborted_previous_preserved 行為。零外部相依、可重跑、可進 CI。
同一支工具在 0.3.0 與 0.4.0 上的對照(node tools/bench-dirty-chase.mjs):
| 0.3.0 | 0.4.0(dirtySettleSeconds: 90) |
|
|---|---|---|
| 編輯期間的重建 | 10 次 | 0 次 |
| 重建成功 | 0 次 | 1 次 |
被 aborted_previous_preserved 中止 |
10 次 | 0 次 |
| 停手後追上圖譜 | 沒有追上 | 追上 |
| 注入探針呼叫 | 214 | 66(−69%) |
| 其中 CBM CLI | 112 | 9(−92%) |
Added
- 設定欄位
dirtySettleSeconds(預設0=維持原行為):未提交變更的專案要靜默幾秒才重建。
設成90可讓「編輯期間的重建」從 10 次降到 0 次、被中止從 10 次降到 0 次,圖譜仍在停手後追上。
預設刻意留 0:這是行為變更,決定權在部署者。 - 具名警告
dirty-chase-detected:當dirtySettleSeconds還是0、且插件實際觀察到重建在途中被中止
(24 小時內 ≥3 次、且佔已完成嘗試 ≥50%)時,用你自己的統計數字提醒你這個欄位存在。
沒有觀察到就不會出現——新安裝不會被嘮叨,視窗滑出後警告自己消失。 status().stats:20 個扁平數字 +statsSince。涵蓋排入/成功/失敗/被中止/冷卻跳過/
閘門跳過/settle 延後與落地/被短路省下的檢查/累計重建毫秒,每個概念都有
sinceStart*與last24h*兩鍵。刻意命名為「本次啟動以來」而非歷史總計——
日誌檔 5 MB 輪替只留.1,回填出來的「歷史」本身殘缺,比誠實標示更容易誤導。
同一組數字也呈現在卡片上的「成效」區塊(統計缺席或全 0 時整塊不渲染)。- 常駐狀態指示(側邊欄
sidebar.footer.action):有落後專案時顯示 warn 圓點、有重建失敗或
監看失敗時顯示 error 圓點與數量,全部新鮮時不渲染任何東西。輪詢?log=0、30 秒一次、
頁面隱藏時暫停、失敗退避(約 0.6 MB/h)。導航 API 經查證不存在,因此它是純指示、不可點擊。 SECURITY.md、docs/PUBLISHING.md(npm 發布的可執行清單)、
.github/ISSUE_TEMPLATE/bug_report.yml。- README 截圖(
docs/assets/,中英各一張):由docs/assets/capture-card.mjs以零相依 CDP
驅動真實 DSH GUI 拍攝,資料是當下的真/state。可重跑。 - 辨識碼:
status().running.startedAt、status().logFileError、status().stateLoadError。
Changed
- 監看路徑的落後複驗不再強制失效圖譜 HEAD 快取:每次存檔少一次
query_graphCLI 呼叫
(實測 2.18–2.81 s)。配合下一條,50 次存檔的 CBM CLI 呼叫從 112 次降到 9 次。 - settle 視窗內只做便宜的 HEAD 探測:視窗內的每次觸發只跑
git rev-parse HEAD(2–3 ms),
HEAD 沒變就只重排計時器、完全不碰 CBM CLI;HEAD 變了(commit/換分支/rebase)則立刻完整複驗
並重建。真正的 HEAD 落後永遠不受 settle 約束,維持立即重建。 includeDirty: false時不再執行git status:這個設定下record.dirty恆為false,
語意是「未檢查」而非「乾淨」;卡片在該情況下不顯示髒污標記(沉默,不是斷言乾淨)。POST /rebuild的force現在真的傳到佇列:先前人工與強制重建仍會被冷卻擋下,
與本檔 0.3.0 的敘述「人工與強制重建不受此限」矛盾。GET /state?log=0現在真的回 0 筆(先前recent()以Math.max(1, …)夾住下限)。- 被
includeProjects/excludeProjects排除的專案不再標成孤兒:孤兒的定義回到
「上游已經沒有這棵樹」,與docs/LIMITATIONS.md一致。 POST /config加上欄位白名單:未知欄位名(例如把scanMinutes打成scanMs)
不再靜默寫進 Loader config,回應會帶unknown陣列;若全部欄位都無法辨識則回 400。engines.node由>=20改為>=20.13:Linux 的遞迴fs.watch自 Node 20.13.0 才有
(nodejs/node#45098),先前 20.0–20.12 會直接落到
backend: 'failed'。CI 的 node 20 格永遠是最新 20.x,測不到這一段。- 設定頁卡片整塊重寫(743 → 1707 行):頂端摘要列、離線與錯誤橫幅置頂、統一的
role="status"操作回饋、健康區塊(CLI 原因與候選路徑、日誌/狀態檔錯誤)、專案卡兩層與
chips 過濾、破壞性操作兩段式確認、relativeTime等硬編碼中文收進字典。
輪詢成本:日誌面板收起時由每次 26,428 bytes 降到 5,253 bytes(−80%,約 34.9 → 6.3 MB/h),
頁面隱藏時為 0。 - 英文介面的警告與「不可作為證據的宣告」改走
code → 字典,未知 code 一律回退顯示 host 原文。
Fixed
drain()的 promise 拒絕無人接手:void this.drain()沒有.catch(),
一次重建拋錯就會變成 unhandledRejection,而 Node 的預設行為是終止行程——
等於殺掉整個 DSH 宿主。已改為記錄drain.failed,並加上會讓修法還原就變紅的測試。- 卸載競態:
stop()不等在飛的掃描:掃描會在stop()回傳之後才建立監看器,且此後不再被停止
(實測stop()回傳後watchers.size === 1)。已讓stop()等待refreshPromise,
並在reconcileWatchers/startWatcher入口檢查stopped。 - 卸載時重建子行程可能在背後繼續跑:
AbortController原本在取得重建鎖之後才建立,
stop()撞上取鎖期間就來不及 abort。已改為先建 controller 再取鎖。 - 監看器的執行期錯誤永遠到不了卡片:
record.watcher是建立當下的快照,全庫沒有任何地方
重讀watcher.status()/lastError——0.3.0 的「inotify 用盡要顯示 failed」修正因此從未生效。
已改為在list()/status()向監看器取即時狀態。 fs.watch沒有檔名時的退路被副檔名白名單丟棄:該分支用'__unknown__'當路徑,
它沒有副檔名,所以一律被白名單擋掉,與註解「保守地當成一次觸發」正好相反。- PATH 掃描硬編
::Windows 的分隔符是;,而同一段邏輯在lib/exec.js與lib/cli.js
各有一份。已抽出共用並改用path.delimiter。 - 生成檔不再驅動重建:
auto-imports.d.ts一個檔案就佔了全部監看觸發的 529/920(約 57%)。
新增DEFAULT_WATCH_GENERATED_PATTERNS(明確清單,刻意不用廣義*.d.ts,
手寫的.d.ts仍有測試保證會觸發)。 - 清掉一批死碼(
PLUGIN_NAME、SETTINGS_SECTION_ID、isGitWorktree、__gitInternals、
removeProject、writeTempFile、keeper 未使用的兩個匯入),並以test/api-surface.test.js
守門,避免它們悄悄回來。 - 發佈包從 672 kB 瘦回 124 kB:
files原本收了整個docs/,於是 README 用的兩張截圖
(566 kB)與開發用的截圖腳本(26 kB)全都進了發佈包——而市場安裝走的就是這個預覽包
(GitHub Release 資產=npm pack的產物),等於每個安裝者都要下載永遠用不到的圖。
改成docs/*.md(保留全部文件、排除docs/assets/)。README 的圖仍留在 repo,
GitHub 與 npm 的 README 呈現都不受影響。 - CI 的語法檢查不再抄一份檔案清單:
ci.yml原本自己列lib/*.js test/*.js test/helpers/*.js tools/*.mjs,與package.json的test:syntax是兩份拷貝——而且已經
漂移過一次(加了docs/assets/*.mjs卻只改了package.json,截圖腳本因此在 CI 上沒被
檢查到)。改為直接呼叫npm run test:syntax,單一來源。
驗證
- 單元測試 139 → 204,
node --test test/*.test.js全綠。 tools/verify-keeper.mjs23/23;tools/verify-client.mjs30 → 212。- 新增
test/http-surface.test.js:控制面改在真的node:http伺服器上用真的fetch
打一遍。先前的路由測試全用手寫的假req/res——那個替身只要與真的 Node HTTP 物件有
一處語意不同(req.url的形式、for await疊代、writeHead之後才能end…),
幾百條斷言可以全綠而真的端點是壞的。這一支把「假替身與真物件一致」本身也變成被測項:
真的查詢字串解析、真的 POST 內文讀取、真的 HTTP 狀態碼與content-type、七條路由在真
伺服器上都接得上、未命中的路徑由伺服器回 404。這也是換代前對新 host 碼的 REST 檢查
代償——dsh web要重啟才會上線,但這條路徑現在就能用真 HTTP 走一遍新碼。 - 三條關鍵修法做過突變驗證(把修法還原→測試必須變紅→還原):
force傳進佇列(2 條測試
變紅)、includeDirty: false不付git status(2 條)、監看路徑的 idle-settle 閘門
(7 條)。固定的不只是「程式碼有那段」,而是「那段被拿掉時測試會叫」。 - 發佈包實測可安裝可載入:
npm pack→ 在乾淨目錄npm install→import得到
Config, apply, dshHomeDir, inject, name五個匯出,name === 'codebase-watcher'(與
cordis.patch.yml的 row id 一致),cordis.patch.yml在包內,./client入口可解析且
帶__ModuleLoader__註冊形狀。CI 新增一關守著「docs/assets不得進發佈包、且發佈包
≤ 200 kB」,並已實測該關卡會擋住這個回歸。 - 新增
test/index.test.js:插件入口與控制面先前完全沒有測試,而它正是「UI 依賴的
對外介面」真正被組出來的地方。這一支用真的apply(),只換掉三個邊界(DSH_HOME
指向暫存目錄、cliPath指向假 CLI、graphUrl指向必然拒絕連線的埠),其餘路徑解析、
狀態機、佇列、日誌、生命週期順序都是真的在跑。它固定了:七條路由的註冊、生命週期順序
(plugin.start必須排在plugin.stop之前)、未宣告的設定鍵會具名警告、GET /state
上卡片依賴的每一個欄位、?log=0真的是 0 筆、POST /config的白名單與命名空間、
沒有 settings 服務時拒絕寫入、不存在的 id 回 404,以及卸載後路由被收回且不再產生掃描。
另有一條把狀態目錄建成普通檔案,逼出logFileError/stateLoadError的真實錯誤
路徑——否則那兩個欄位只存在於原始碼,線上永遠不會有人看到。 node tools/bench-dirty-chase.mjs(npm run bench)可重跑,上表即為它的輸出;
--assert模式已加進 CI,會擋住「編輯期間重建 > 1 次、或仍有重建被中止、或停手後沒追上」的回歸。- CI 新增獨立的
benchjob。verify-keeper/verify-client沒有進 CI——前者需要真的
codebase-memory-mcp,後者需要一個跑著的 GUI,硬塞只會得到永遠紅或永遠 skip 的假訊號。