背景
「為什麼我改了 auth.py 一行,test 卻在 billing/test_invoice.py 炸掉?」這種問題在 mid-size codebase 每天上演。除錯過程通常長這樣:
- 看 test 失敗的 stack trace
- 從失敗點反向找呼叫鏈,每跳一層 grep 一次
- 在腦海拼出「test → service A → service B → auth」的路徑
- 中間有 dynamic dispatch(回呼、依賴注入、事件訂閱)就斷線
graphify 的 path 跟 /graphify path-explain 把這條鏈做成可展示的成品。path 給你最短路徑,path-explain 給你每個節點的進出邊跟 community 歸屬,讓你不用再靠推測跟 grep 拼圖。
💡 範例 001 用過
graphify path找兩個概念的連結。本範例把path接到「除錯與影響分析」這個更實際的場景,並補上/graphify path-explain的進階用法。
適用情境
- debug 跨層失敗:上層 test 炸、根因在下層,要展示呼叫鏈
- code review 時 reviewer 問「這個改動為何會影響 X?」需要給出明確路徑
- 重購前的影響半徑(blast radius)簡報
- 解釋給非作者的人聽:兩個 service 為何暗中耦合
完整流程
步驟 1:建圖並定位兩個端點
情境:你改了 src/auth/session.py 的 Session.expiry(),發現 tests/billing/test_invoice.py 失敗。要在圖譜上展示這條因果鏈。
確認圖譜在:
cd ~/my-project
uv tool install graphifyy && graphify install
graphify extract . --code-only
兩個端點是 Session.expiry()(變更起點)跟 test_invoice 或它底層的 InvoiceGenerator(失敗點)。
步驟 2:用 path 找最短路徑
graphify path "Session.expiry" "InvoiceGenerator"
graphify 在圖譜上跑 BFS(廣度優先搜尋),回傳最短路徑:
Shortest path (4 hops):
Session.expiry() --calls--> Session.refresh()
Session.refresh() --calls--> AuthMiddleware.verify()
AuthMiddleware.verify() --calls--> RequestContext.current_user()
RequestContext.current_user() --calls--> InvoiceGenerator.for_user()
4 跳的路徑立刻告訴你:Session.expiry 的回傳值會經過 AuthMiddleware 跟 RequestContext 兩個中介層傳到 InvoiceGenerator。你改回傳型別,下游全部受影響。
步驟 3:用 path-explain 看每個節點的脈絡
path 給你骨架,path-explain 給你血肉。path-explain 是 slash command 形式(助理專用),它會把指定節點的所有 in/out 邊、所屬 community、是不是 god node 一次攤開:
/graphify path-explain "AuthMiddleware.verify"
助理拿到該節點的完整鄰居後,可以解釋「為什麼 verify 是個關鍵中繼點」:
AuthMiddleware.verify() (Community: auth, god node)
In-edges (7 callers):
- Session.refresh()
- LoginView.post()
- TokenRefreshView.post()
- APIView.dispatch() ← 所有 API 都過這
- AdminGuard.check()
- GraphQLView.dispatch()
- WebSocketHandler.connect()
Out-edges (5 callees):
- RequestContext.current_user()
- audit_log.write()
- rate_limiter.hit()
- Session.rotate()
- PermissionDenied → exception handler
看到 APIView.dispatch() 也在 callers 裡,就懂為什麼 billing test 會炸:所有 API 都過這個 middleware, InvoiceGenerator 的 API 當然也過。
步驟 4:用 path 解釋「為何沒影響」
path 也用來驗證反向假設:你以為改動會炸某處,結果 graphify 回「unreachable」。
graphify path "Session.expiry" "CsvExporter"
No path found between Session.expiry and CsvExporter (within 6 hops).
unreachable 是有價值的結論:CSV 匯出跟 session 無關,這次改動可以放心跳過 CSV 相關 test。path 給你「不影響」的證據,而不只是「影響」的清單。
步驟 5:把路徑寫進根因報告
debug 完成後,把找到的路徑跟 path-explain 的關鍵節點寫進根因報告(root cause report):
## Root cause: Session.expiry signature change breaks InvoiceGenerator
### Failure chain (graphify path)
Session.expiry → Session.refresh → AuthMiddleware.verify
→ RequestContext.current_user → InvoiceGenerator.for_user
### Key node (graphify path-explain)
AuthMiddleware.verify is a god node (in-degree 7).
All APIViews dispatch through it, including /api/invoice/generate.
### Fix
Revert return type to int. Move TTL logic into separate Session.ttl().
下次有人問「為什麼這個改動會炸 billing」,不用重新調查,看報告就懂。
步驟 6:在 PR 討論裡用 path-explain 回答 reviewer
reviewer 在 PR 留言:「為什麼改這個會碰到 AdminController?」
不必口頭解釋,直接在助理內跑:
/graphify path-explain "Session.expiry"
助理基於該節點的 out-edges 跟最短路徑,回具體的鏈:
改動會沿這條鏈傳到 AdminController: Session.expiry → Session.refresh → AuthMiddleware.verify → AdminGuard.check → AdminController.index
AuthMiddleware.verify 是 god node(7 callers),所有需要認證的 API 都過這。
reviewer 拿到機器可驗證的路徑,比人腦推論可信。
真實輸出範例
$ graphify path "Session.expiry" "InvoiceGenerator"
Shortest path (4 hops):
Session.expiry() --calls--> Session.refresh()
Session.refresh() --calls--> AuthMiddleware.verify()
AuthMiddleware.verify() --calls--> RequestContext.current_user()
RequestContext.current_user() --calls--> InvoiceGenerator.for_user()
$ graphify path "Session.expiry" "CsvExporter"
No path found between Session.expiry and CsvExporter (within 6 hops).
$ /graphify path-explain "AuthMiddleware.verify"
Node: AuthMiddleware.verify() [Community: auth, god node]
In-edges (7 callers):
Session.refresh(), LoginView.post(), TokenRefreshView.post(),
APIView.dispatch(), AdminGuard.check(), GraphQLView.dispatch(),
WebSocketHandler.connect()
Out-edges (5 callees):
RequestContext.current_user(), audit_log.write(),
rate_limiter.hit(), Session.rotate(), PermissionDenied
對比傳統做法
| 任務 | grep + 讀檔 | graphify path + path-explain |
|---|---|---|
| 找呼叫鏈 | 每跳 grep 一次,dynamic dispatch 處斷線 | path 一次給最短路徑 |
| 解釋給 reviewer 聽 | 口頭推論,憑記憶 | 貼 path 輸出,可驗證 |
| 驗證「不影響」 | 無法證明否定 | path 回 unreachable 就是證據 |
| 看 god node 影響 | 要從 import 拓樸推論 | path-explain 直接標 god node + 列邊 |
| 中介層的角色 | 容易漏掉 middleware / context 層 | path 一定走完整條鏈 |
path 的核心價值:把「推測的呼叫鏈」變成「圖譜驗證的最短路徑」。path-explain 的核心價值:把節點的脈絡一次攤開,不需要逐個 grep 鄰居。
失敗處理
| 情境 | 排查 |
|---|---|
path 回 No path found 但你確定兩者有關 |
跳數可能超過預設上限。先確認圖譜新鮮(graphify update),或檢查中介層是不是 dynamic dispatch(回呼、事件訂閱),這類邊可能信心標籤是 [INFERRED] 或 [AMBIGUOUS] |
path 回的路徑跟實際呼叫鏈對不上 |
圖譜過時。graphify hook status 確認 hook 跑過,或手動 graphify update |
/graphify path-explain 助理說「不認識這個指令」 |
還沒 graphify install。先跑對應平台的 install(例如 graphify claude install) |
| path-explain 回的鄰居很多,看不懂重點 | 先看 god node 標籤,god node 才是核心。再看 community 歸屬,同 community 的鄰居關聯最強 |
| 同名 function(overload)path 結果混淆 | graphify 會列所有匹配。在端點名稱加上檔名或路徑片段(例如 verify vs auth.verify)收斂 |
內部連結
下一步
- 用 query 做跨檔案依賴追蹤(path 的前置:先用 query 找入口)
- 大型 monorepo 重購(path 之外,社群分析與 god node 識別)
關鍵學習點
path給最短路徑,path-explain給節點脈絡。前者是骨架,後者是血肉,除錯跟 review 兩個都要用。path回unreachable跟回「找到路徑」一樣有價值:它是「不影響」的證據,可以放心跳過無關 test。- 中介層(middleware、context、依賴注入)是 grep 容易漏、
path不會漏的所在。凡是涉及跨層除錯,先跑一次path把骨架拉出來。 - god node 是路徑上的高風險節點。
path-explain標出來後,重購要特別處理,因為它的下游影響面特別大。