假設為什麼是錯的
前一天留下來的記錄寫:「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。