背景
維護過 mid-size codebase 的人一定遇過這些問題:
- 「
getUserRole()到底被哪些地方呼叫?」 - 「如果我改
auth middleware的回傳值,會不會炸到 billing?」 - 「這個看似沒人用的 util 是不是死碼(dead code)?」
傳統做法是 grep -r "getUserRole" .,然後在 50 個命中裡人工過濾:哪些是註解、哪些是同名變數、哪些才是真正的呼叫點。跨好幾個目錄之後,你還要在腦海裡把這些命中串起來。
graphify 的 query 把這件苦差事交給圖譜。你的問題打到 graphify query 或 /graphify query,graphify 在已建好的 JSON 圖譜上做 BFS(廣度優先搜尋,Breadth-First Search)展開子圖,回傳真正相關的節點跟邊。搜尋結果不再只是字串命中,而是有方向、有型別的依賴關係。
💡 範例 001 示範過
graphify query的開放式問答用法。本範例聚焦在依賴追蹤這個更結構化的場景:怎麼把開放式 query 接到「跨檔影響分析」的工作流。
適用情境
- 想重購或刪除某個 function / class,要先盤點所有呼叫點
- 接手 legacy code,需要建立「更動 A 會牽動 B、C、D」的心智模型
- code review 時,懷疑 PR 提的影響範圍不完整,想快速驗證
- 找死碼候選清單(節點 in-degree 為 0 且非 entry point)
完整流程
步驟 1:確保圖譜是新鮮的
依賴追蹤前提是圖譜要反映現況。如果還沒建過:
cd ~/my-project
uv tool install graphifyy && graphify install
graphify extract . --code-only
--code-only 只跑 tree-sitter AST 解析,不需要 API key,30 秒內建完中型 codebase。如果之前已建過,先確認是新鮮的:
graphify hook status
如果 hook 沒裝、或最近 commit 沒觸發重建,先補一次:
graphify update
update 只重抽變動檔,比全量 extract 快很多。
步驟 2:用開放式 query 找入口
假設你想追的是 getUserRole。第一個 query 先問「它在哪」:
graphify query "getUserRole 是在哪裡定義的?"
graphify 挑關鍵字、過濾 stop-words、BFS 找子圖,回傳類似:
Subgraph (4 nodes, 5 edges):
[DEF] getUserRole() src/auth/permissions.py:42
[DEF] UserRole src/auth/permissions.py:12
[REF] get_request_user() src/middleware/auth.py:88
[REF] check_permission() src/auth/policy.py:23
[DEF] 是定義點,[REF] 是參考點。這已經比 grep 淨多了:grep 會連字串字面量、註解、同名欄位全部撈進來。
步驟 3:反過來問「誰用到它」
接著問反向依賴(downstream callers):
graphify query "哪些地方依賴 getUserRole 的回傳值?"
graphify 沿 CALLS / REFERENCES 邊走,回傳子圖。看到的可能是:
Subgraph (6 nodes, 8 edges):
getUserRole() --calls--> canAccessAdmin()
getUserRole() --calls--> getDashboardView()
canAccessAdmin() --calls--> AdminController.index()
getDashboardView() --calls--> DashboardController.show()
從子圖你直接看出:改 getUserRole 的回傳型別會炸到 AdminController 跟 DashboardController 這兩個 god node。
步驟 4:在助理內用 slash command 收斂
如果你在 Claude Code / Cursor 內,把 query 包成 slash command 讓助理直接消費子圖:
/graphify query "如果我改 UserRole 的 enum 值,哪些地方會被影響?"
助理拿到子圖後,會結合程式碼上下文回答:「影響 12 個檔,最嚴重的是 AdminPolicy.enforce() 跟 DashboardController.show(),兩者都直接 switch 這個 enum。」
slash command 跟 CLI 的差別:CLI 把子圖印給你看,slash command 把子圖餵給助理做進一步推論。兩者底層都打 graphify query,只是出口不同。
步驟 5:搭配 path 確認關鍵路徑
子圖很大、看不完時,把焦點轉到 path 指令:
graphify path "getUserRole" "AdminController"
path 兩個端點之間走 BFS 找最短路徑,回傳明確的邊序列:
Shortest path (3 hops):
getUserRole() --calls--> canAccessAdmin()
canAccessAdmin() --calls--> requireAdminRole()
requireAdminRole() --calls--> AdminController.index()
這就是「為什麼改 A 會牽到 B」的具體呼叫鏈。完整用法見範例 005。
步驟 6:把發現寫進 PR 描述
依賴追蹤的結果要落地成可分享的文件。把步驟 3、5 的子圖與路徑貼進 PR description:
## Impact analysis
`getUserRole` 的 downstream callers(graphify query 結果):
- canAccessAdmin() → AdminController.index()
- getDashboardView() → DashboardController.show()
最短路徑(graphify path):
getUserRole → canAccessAdmin → requireAdminRole → AdminController.index()
本 PR 只改回傳註解,不動 enum 值,所以下游不影響。
reviewer 看到具體路徑,不用自己再 grep 一輪。
真實輸出範例
$ graphify query "getUserRole 被誰呼叫?"
Subgraph (6 nodes, 8 edges):
[DEF] getUserRole() src/auth/permissions.py:42
[REF] canAccessAdmin() src/auth/policy.py:23
[REF] getDashboardView() src/dashboard/views.py:15
[REF] exportUserCsv() src/export/csv.py:88
[REF] requireAdminRole() src/middleware/admin_guard.py:12
[REF] audit_log() src/audit/log.py:34
$ graphify query "改 UserRole enum 會炸到哪些 god node?"
Subgraph (5 nodes, 6 edges):
UserRole (enum)
→ getUserRole() [DEFINES]
→ canAccessAdmin() [CALLS UserRole.admin]
→ AdminController.index() [CALLS, god node]
→ DashboardController.show() [CALLS, god node]
$ graphify path "getUserRole" "AdminController"
Shortest path (3 hops):
getUserRole() --calls--> canAccessAdmin()
canAccessAdmin() --calls--> requireAdminRole()
requireAdminRole() --calls--> AdminController.index()
對比傳統做法
| 任務 | grep + 讀檔 | graphify query |
|---|---|---|
| 找所有呼叫點 | grep -r "getUserRole" 然後人工過濾字串命中 |
graphify query 直接回 CALLS 邊 |
| 反向依賴(誰用我) | 反向 grep,再從 import 推論 | query 反向走邊,一次到位 |
| 找 god node 關聯 | 看不出來,要靠 import 拓樸 | 子圖直接標 god node 標籤 |
| 死碼候選 | grep 不到 ≠ 沒人用 | 看 in-degree = 0 且非 entry point |
| 跨檔影響範圍 | 跨檔搜尋再人工拼接 | query + path 一氣呵成 |
graphify query 真正的優勢不在「找字串」,而在回傳結構化的子圖,有方向(CALLS / REFERENCES / DEFINES)、有標籤(god node、community)。
失敗處理
| 情境 | 排查 |
|---|---|
| query 回空子圖 | 換關鍵字:graphify 過濾 stop-words,純中文虛詞會被丟掉。用具體的 function / class 名稱 |
| query 回太多無關節點 | 收斂問句:加「定義」、「呼叫」、「實作」這類動詞,graphify 會優先走對應的邊類型 |
| 結果跟現況對不上 | 圖譜過時。跑 graphify update,或確認 graphify hook status 顯示 hook 跑過 |
| 助理拿到 slash command 卻說「沒有 graphify」 | 還沒 graphify install。先 graphify claude install 或對應平台的 install |
| query 命中字串字面量而非真正呼叫 | AST 邊帶信心標籤,看 [EXTRACTED] vs [INFERRED]。--code-only 結果只有 EXTRACTED,最可靠 |
內部連結
下一步
- 用 path / path-explain 解釋為何 A 影響 B(更深入的影響鏈分析)
- 大型 monorepo 重購(依賴追蹤之後的重購實戰)
關鍵學習點
graphify query不是「進階版 grep」,而是「在結構化圖譜上做 BFS」。回傳的是有方向、有標籤的子圖。- 依賴追蹤的標準三步:先 query 找入口,再 query 反向找下游 callers,最後
path確認關鍵路徑。 - query 結果的好壞取決於問句的關鍵字。用具體的 function / class 名稱,加「定義」「呼叫」「實作」這類動詞收斂。
- 圖譜新鮮度是前提。沒裝 hook、或最近沒
update,查到的依賴就是過時的,會誤導重購決策。