detect.py(1996 行)· extract.py(5993 行)· build.py(1943 行)· cluster.py(320 行)這四支模組就是「把檔案變成圖」的資料流:detect 找出有哪些檔案 → extract 把每個檔案抽成 {nodes, edges} → build 併成一張 NetworkX 圖 → cluster 用 Leiden 把節點分成社群。每一步的介面都很乾淨:dict 進、dict 出,只在 graphify-out/ 寫檔。
這是整個管線的「分類開關」。它依序檢查、先比對特殊情況、再比對副檔名:
pyproject.toml、go.mod、pom.xml 等被導向 CODE(用確定性的 manifest 解析,而不是丟給 LLM)——否則一個 apm.yml 會被當成「文件」送 LLM,把套件拆成重複節點。.blade.php 要在查 .php 之前判斷。CODE_EXTENSIONS、PAPER_EXTENSIONS(PDF 在 Xcode asset catalog 裡是向量圖示,要排除)、IMAGE_EXTENSIONS、DOC_EXTENSIONS(再檢查是否長得像論文)、office、Google Workspace、video。_shebang_file_type()(#!/usr/bin/env bash 的 CLI 也是程式碼)。這就是「程式碼零 LLM、文件要 LLM」的源頭——類型決定後,走哪條抽取路徑就定了。
detect() 是管線的第一站,也承擔最多「別掃到不該掃的」責任:
.graphifyinclude(印警告)、讀取 .graphifyignore 合併 .gitignore 規則。os.walk(followlinks=…, onerror=…) 遞迴掃描。onerror 確保中途的 PermissionError 不會變成「靜默地少掃一個資料夾」。node_modules、.next 等)、被 ignore 規則排除的目錄,直接 dirnames[:] = kept_dirs 就地修剪,os.walk 就不會往下走。_SKIP_FILES、自己輸出的 converted/ sidecar、symlink 逃出掃描根目錄的、敏感檔。{files, total_files, total_words, needs_graph, warning, skipped_sensitive, unclassified, ignored, …}——所有「被排除的」都有記錄,不會默默消失。注意回傳結構:needs_graph 與 warning 會告訴上層「語料太小,可能不需要圖」或「語料太大,語意抽取會很貴」。
三個段落式講解的過濾器:
_is_noise_dir(part, parent) — 判斷目錄名是不是框架快取/產物(.next、.nuxt、node_modules…)。注意:dot 目錄(.github、.claude)是允許的,使用者常常想把它們畫進圖。_is_sensitive(path) — 環境變數檔(.env 等)與金鑰相關檔案不進圖,避免把密鑰抽成節點。_is_ignored(path, root, patterns, _cache=…) — 核心 ignore 引擎,支援 gitignore 語法(! 否定、anchored patterns、.gitignore 合併、last-match-wins)。_cache 在整個掃描過程共享,避免每個檔重算。這是程式碼抽取的入口。docstring 講得很清楚,兩遍:
DigestAuth --uses--> Response。實作上最關鍵的是「快取與平行化」:
_get_extractor(path) 找出每個檔的抽取器;沒有抽取器的檔直接給空結果。load_cached),命中就直接用。uncached_work 若超過平行門檻(_PARALLEL_THRESHOLD)就用 ProcessPoolExecutor 平行抽取(_extract_parallel,繞過 GIL);池中途壞掉時,_extract_sequential 只重抽沒完成的(issue #2444)。pip install "graphifyy[sql]")。extract() 主流程、後段的 ID 正規化/重映射(為了跨機器與 AST↔semantic 一致),以及語言的 _import_* 家族。這裡只帶你看主流程;語言的通用抽取邏輯在 extractors 頁。extract() 對「失敗」極度敏感——零節點、無抽取器、缺相依,每一種都明確印出警告。理由是:一個檔默默消失是最難除錯的資料問題。dispatch 邏輯的「動腦」所在:
.blade.php、MCP config、套件 manifest 都先於泛型副檔名比對。.h 可能是 C/C++/ObjC——用內容 sniff(_is_objc_header、_is_cpp_header)決定;.m 可能是 Objective-C 或 MATLAB——只有真的像 ObjC 才走 extract_objc,否則寧可沒抽取器(會被警告)也不硬解析。#!/usr/bin/env bash → extract_bash)。_DISPATCH 表。語言的抽取函數其實都很薄——真正的邏輯在 _extract_generic(path, config)(來自 extractors/engine.py,見 extractors 頁)與一顆 LanguageConfig:
def extract_python(path: Path) -> dict:
result = _extract_generic(path, _PYTHON_CONFIG) # 通用 tree-sitter 走訪
if "error" not in result:
_extract_python_rationale(path, result) # 補 docstring / # NOTE 節點
return result
這說明了 0.3.0 那次「2527 → 1588 行」重構的成果:12 份複製貼上的語言抽取器收斂成「設定 + 通用引擎」。
節點 ID 是 graphify 最容易踩雷的地方。規範:{parent_dir}_{stem}——只取一層父目錄、去掉副檔名。頂層檔收斂成裸 stem(setup.py → setup)。
docstring 強調 這必須跟語意子代理產生的 ID 一致,否則 AST 與 semantic 抽取會把同一檔案拆成兩個不相連的「ghost 節點」。
段落式說明:
_import_python / _import_js / _import_java … — 每種語言的 import 語句怎麼變成邊。簽名幾乎一致:(node, source, file_nid, stem, edges, str_path, scope_stack)。_extract_python_rationale — 把 docstring 與 # NOTE: / # WHY: 註解抽成「rationale 節點」並連回它們說明的程式碼——這是 GRAPH_REPORT 裡「the why」的來源。_shorten_rationale_label — 過長註解截斷成 80 字元。_is_autogenerated_python — 偵測自動產生的 Python(避免把產物當原始碼抽)。這是「進料 → 一張圖」的合併點:
nodes、edges、hyperedges、token 統計全部併進一個 combined dict。dedup=True:先做型別強制(_coerce_non_string_ids)與欄位別名整併(_fold_node_aliases),再呼叫 deduplicate_entities()(見 ops · dedup),處理同實體不同 label 的問題。build_from_json() 真正組圖。build() 只管「合併 + 可選去重」,真正組圖的 dirty work 在 build_from_json——這讓測試可以分層:測合併邏輯 vs 測組圖邏輯。一個很有趣的「活化石」:docstring 很誠實地承認——這個函數沒有接進 build()。真正在用的是 deduplicate_entities()(dedup.py)。它只依 label 合併、沒有 file_type 保護,所以不能給 code 節點用(兩個不同檔案的 Account 型別會被錯誤併成一個)。
它是「label-only 去重」的對照組:合併時偏好無 chunk 後綴(_c\d+)的短 ID,並丟掉合併造成的自環。
教學價值:一份開源專案保留「被取代的舊實作」並用 docstring 說明為什麼不用,勝過直接刪掉——這份歷史讓新讀者理解去重演進。
這是最髒也最重要的組圖函數(741–1264 約 500 行),它處理一大堆邊角:
_semantic_id_remap)、doc/ AST twin 重映射。_disambiguate_file_node_labels)。G.graph["hyperedges"] 的寫入與正規化。實際看 graph 屬性時,節點會帶 _community、_source_file、_degree 等前綴屬性——那是在 export 前由 cluster/analyze 加上去的。
這是 增量更新設計 的實作點。與 build()(從零合併)不同,build_merge() 讀取既有的 graph.json,把這次的 new_chunks 併進去,並用 prune_sources 把已刪除檔案產生的節點剪掉。它處理跨檔邊的存活(changed file 指到 unchanged target 的邊要保留)、shrink guard、與 manifest 的一致性。
「最佳品質優先、退化優雅」的代表作:
stable 圖——讓分群輸入確定性(同樣的圖一定同樣的輸入順序)。from graspologic.partition import leiden,透過 inspect.signature 動態傳 random_seed=42、trials=1、resolution(相容不同版本的 graspologic)。stdout/stderr 導到 devnull——graspologic 會印 ANSI 進度條,在 Windows PowerShell 5.1 會毀掉 scroll buffer(issue #19,一個很實用的雷)。ImportError 就退回 networkx 的 Louvain,同樣用 inspect.signature 相容版本差異(max_level 只傳給支援的版本)。這是 cluster.py 的主角,把「分群」這件事補齊到實用:
exclude_hubs_percentile):度數超過百分位的「超集線器」不參與分群(避免把無關子系統拉進同群),之後依鄰居社群多數決重新掛回。_partition 拆開。tuple(sorted(nodes)) 當總序 tiebreak——否則每次跑,那些等大小的社群 ID 會亂跳,造成偽「社群 churn」(issue #1090)。社群名稱的「零成本」方案:直接用最高度數成員的 label(去掉尾綴 ())。所以報告讀起來是 auth / log_action 而不是 Community 70。平手時用節點 ID 保證跨跑穩定。當有設定 LLM 時,這個預設命名會被更豐富的 LLM 命名覆蓋。
段落式說明:
cohesion_score(G, nodes) — 實際社內邊數 / 最大可能邊數(n(n-1)/2)。這就是社群「緊密度」的量化。score_all(G, communities) — 一行迴圈,把每個社群算一遍凝聚力,回傳 {cid: score}。community_member_sigs(communities) — 每個社群算「成員 ID 排序後的 SHA256 指紋」,存到 .graphify_labels.json 旁。之後 cluster-only 可以判斷哪個社群真的變了——成員不再雜湊一致的社群是「不同社群」,沿用舊 label 就是「stale label」bug。detect 怎麼決定一個檔案走 AST 還是 LLM;extract 的快取與平行策略;build 的 dedup 在哪一步;cluster 為了確定性做了哪三件事。接著讀 analyze + report 看「圖建好之後怎麼分析」。相關:extractors 語言抽取器 · 增量更新設計 · 概念地圖
主線 pipeline 是 Graphify 的脊椎:detect → extract → build → cluster。每個階段都是自己模組裡的一個函數,用純 Python dict 與 NetworkX 圖溝通。
detect.py 負責收集檔案並過濾(1996 行)、extract.py 負責 tree-sitter AST 抽取(5993 行,最大模組)、build.py 負責併入 NetworkX 圖(1943 行)、cluster.py 負責 Leiden 社群偵測(320 行,最簡單)。
關鍵設計:沒有共享狀態,除了 graphify-out/ 之外沒有副作用。這讓每個模組都可以獨立測試。
graphify detect my-project/,查看 .graphify_detect.json 的內容。graphify extract my-project/ --verbose,觀察每個階段的輸出。graphify-out/ 下的暫存檔(如果有的話)。| 錯誤訊息 | 原因 | 解決方式 |
|---|---|---|
detect: permission denied | 無法讀取某些目錄 | 檢查檔案權限或用 sudo 執行 |
extract: tree-sitter parse error | 語法錯誤或不支援的語法 | 檢查檔案是否完整,或語法是否在支援列表中 |
build: duplicate node ID | 多個檔案產生相同的節點 ID | 這是 0.9.0 修掉的問題,升級 graphify |
cluster: too few nodes | 圖中節點太少無法分群 | 這是正常的,小圖可能只有 1 個社群 |
前面的 Worked Example 看 pipeline 中間產物;這次是真實 debug 劇本:你的專案有 Python + TypeScript + Rust,跑完 graphify extract . 後 GRAPH_REPORT 顯示 Rust 檔案「零節點、零邊」。
graphify detect . 看 .graphify_detect.json——Rust 的 .rs 是否被歸類為 CODE?(可能是副檔名沒註冊進 CODE_EXTENSIONS)。_get_extractor(path) 對 .rs 是否回傳非 None;確認 _DISPATCH 表有 rust 且 tree-sitter-rust 有安裝。graphify extract . --verbose——extract 對「零節點檔 / 無抽取器 / 缺相依套件」會各印一種醒目標記(issue #1666/#1689/#1745),錯誤訊息通常直接告訴你答案。這頁最容易被忽略的是 cluster.py 那些「圍繞演算法的工程」——它們才是社群結果可用的原因:
random_seed=42 + trials=1;社群依大小降序重編號,等大小用 tuple(sorted(nodes)) 當 tiebreak(issue #1090)——否則等大小的社群 ID 每次跑都會亂跳,產生偽「社群 churn」。_partition;doc-hub 造成的低凝聚力社群(cohesion < 0.05 且 ≥50)再拆一次。inspect.signature 動態傳參(不同套件版本參數名不同);graspologic 的 ANSI 進度條被導到 devnull(Windows PowerShell 會毀 scroll buffer,issue #19)。| 症狀 | 可能原因 | 解決方案 |
|---|---|---|
| 某語言整批零節點 | 副檔名未註冊 / dispatch 缺項 / tree-sitter grammar 沒裝 | 逐站查 detect→dispatch→抽取;用 --verbose 看警告類型 |
| 同一次跑內社群數不穩定(重跑就變) | 輸入順序被 dict 順序影響,或隨機種子沒固定 | 確認走排序重組的 stable 圖;確認 seed=42;不要手動改節點順序 |
| 整個 repo 幾乎一個大社群 | 檔案級 hub / doc-hub 把所有節點拉在一起 | 檢查 exclude_hubs_percentile;看凝聚力分數,低凝聚力大社群會被再拆 |
| 分群結果每次跑編號不同 | 社群 ID 穩定化沒生效(舊版 bug)或等大小社群 tiebreak 衝突 | 升級到含 issue #1090 修復的版本;比成員集合而非編號 |
| build 階段 ghost 節點(同節點兩份) | AST 與 semantic 抽取的 ID 慣例不一致(_file_node_id 規則沒對齊) | 確認語意子代理遵循 {parent_dir}_{stem} 慣例;檢查 _semantic_id_remap 是否被繞過 |