Skip to content

0.4.0 — 重建追逐根治、卸載正確性與對外治理

Choose a tag to compare

@WwW7olFWwW WwW7olFWwW released this 06 Oct 18:43
· 4 commits to main since this release

對應 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_graph CLI 呼叫
    (實測 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.mjs 23/23;tools/verify-client.mjs 30 → 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 新增獨立的 bench job。verify-keeper/verify-client 沒有進 CI——前者需要真的
    codebase-memory-mcp,後者需要一個跑著的 GUI,硬塞只會得到永遠紅或永遠 skip 的假訊號。