假設為什麼是錯的

前一天留下來的記錄寫:「Understand Anything 無 CLI,uv run understand 失敗」。 這個結論其實跳過了一步:沒有 CLI 不代表套件不能用,只代表它沒有命令列入口點。

今天把 pnpm workspace 目錄掃一遍,發現 @understand-anything/core 是個獨立 TS 套件, dist/index.js 存在且已 build。核心能力被分成兩層: 結構層(TreeSitterPlugin、GraphBuilder、SearchEngine、fingerprint)完全離線、deterministic; 語意層只負責 build prompt 和 parse response,LLM 呼叫是外部的事,core 本身不碰。 再寫一個唯讀 spike 驗證:Node 可以 import、init 只需 20–34ms、symbols 提取正確、 build() 輸出標準 KnowledgeGraph。

關鍵發現:require.resolve 是 module-relative,從 core dist 位置解析它自己的 node_modules,因此 nomad-hub 從任意目錄呼叫都能跑。這個 cwd 獨立性是整個途徑成立的前提, 所以 spike 要優先驗這一點,再提議任何實作計畫。

四個方向為什麼選途徑 B

開工指引列了四個方向:A. 接 claude -p headless 做語意分析、B. Node 呼叫 core 結構層、 C. 嵌入 Understand Anything dashboard、D. 其他。

A 最先排掉:headless 認證在前幾天已踩過多層環境污染問題,而且每次分析都要呼叫 LLM、非 deterministic、速度慢。 C 也排掉:dashboard 是 React 19 + Vite SPA,目前未 build,系統中連一份 knowledge-graph.json 都沒有。 B 通過 spike 驗證,離線、deterministic、已知 cwd 獨立性成立,選這個。

TDD 五步

第一步:scripts/analyze.mjs,遞迴掃原始碼(從 builtinLanguageConfigs 動態取有 tree-sitter 的語言, skip node_modules/.venv 等),用 TreeSitterPlugin 逐檔、GraphBuilder.addFileWithAnalysis 建 file→function/class contains 邊 + addImportEdge 建依賴邊,輸出 KnowledgeGraph JSON。 唯一硬編碼是 UA_CORE_PATH(抽常數允許 env override)和 UA_MAX_FILES(2000)。

第二步:core/analysis_runner.py,對齊 gemma.py 風格,模組級 run_analysis、 自訂 AnalysisError、可注入 script/node/timeout 參數。TDD:先寫 7 個測試,再實作。 第三步:改寫 routes_analysis.py,用 runner 替換舊的 uv run understand subprocess, 回 {status, project_id, graph},補 8 個 route 測試。 第四步:前端 analysis.js(從 index.html 內聯 JS 抽出,約 200 行)+ self-host vendor/d3.v7.min.js(v7.9.0,280KB):統計卡 + D3 force-graph 雙視圖。 第五步:回歸 + preview 端到端目視。

三個坑與根因

坑一:~ 路徑未展開。registry 存的是 ~/程式倉庫/nomad-hub, Python Path.is_dir() 與 Node statSync 都不認 shell 的 ~, 分析直接報「路徑不存在」。問題在 preview 首次真實分析才暴露,因為 mock 測試傳的是 tmp_path 絕對路徑, 根本測不出來。修法:runner 統一 Path(project_path).expanduser() 後才檢查並傳給 Node, 一個責任點,不在 mjs 各自展開。

坑二:import 邊解析全錯,兩個語言各有原因。Python 的 import 是點分模組名 nomad_hub.api.routes_x 而非相對路徑,我的解析器當成路徑比對,全部 miss,0 邊。 修:從 root 做映射,對應到 <root>/nomad_hub/api/routes_x.py/__init__.py。 TypeScript ESM 的 import "./types.js",實體檔是 types.ts(ESM 慣例帶 .js 副檔名), 直接比對永遠 miss。修:先 stem 化(剝 .js/.jsx 再補 .ts/.tsx)再比對。 修前:Python 0 邊 / TS 2 邊;修後:Python 59 邊 / TS 263 邊。

坑三:route 測試污染真實 data/。routes_analysis 用固定路徑 data/analysis/ 寫快取, 測試跑起來會寫進真實 repo 目錄。修:monkeypatch get_analysis_dir 指向 tmp_path; 並把 data/analysis/ 加進 .gitignore(per-project 產物、含絕對路徑,比照 data/handoff/)。

結果

preview 端到端,對 nomad-hub 自身跑:54 個檔 / 222 個函式 / 69 個類別 / 62 條依賴邊。 圖有兩個視圖:「檔案依賴圖」54 節點/62 邊(app.py 是樞紐,模組依賴清晰), 「完整結構」345 節點/353 邊(三色節點,function/class 以 contains 星狀 cluster 展開)。 視圖切換、拖曳縮放、≤80 節點顯示 label 全通,無 console 錯誤。 測試 198→213,analysis_runner 和 routes_analysis 各達 100% 覆蓋。

關於 D3 選擇 self-host(280KB)而非 CDN:離線結構分析是這個 tab 的賣點, 靠 CDN 斷網就畫不出圖,反差太諷刺。 預設「檔案依賴圖」而非「完整結構」也是設計決策:335 節點畫全會糊, file+imports 視圖優先讓人看清模組依賴,再由使用者選擇切換。

關鍵教訓

「沒有 CLI」不等於「不能用」:先查套件是否已 build、有沒有可 import 的 dist,再下結論。誤判封閉了一個其實可行的方向。

spike 先驗、再提案:途徑 B 成立的前提是「cwd 獨立性」——Node 從任意目錄呼叫 core 都能跑。這種前提不驗就提案,等實作到一半發現不成立比發現得早要貴。

mock 測試測不出真實資料的假設:~ 路徑問題在 7 個 unit test 全過後才被 preview 端到端目視抓出。系統邊界(真實 registry 路徑格式)必須用真實資料跑一次才算驗過。

Python import 解析與 TypeScript ESM 副檔名是兩個不同的坑:前者是點分模組名 vs 路徑的語意差異,後者是 .js 慣例與 .ts 實體的不一致。各自有明確的修法,不要把它們混在一起。

測試污染來自寫死路徑:凡是需要「寫進某目錄」的 route,route test 一定要 monkeypatch 那個目錄,並把 per-project 產物加進 .gitignore。不然本地跑出來的快取會混進 git status。

來源:個人開發日誌 2026-06-02 · nomad-hub Analysis tab · commit 0876f25 · 測試 198→213 · 54 檔 / 222 函式 / 69 類別 / 322 邊