Theme / v0.9.56

Graphify

Codebase 知識圖譜

實戰範例

實戰範例 005:用 graphify path 與 path-explain 解釋『為何 A 影響 B』

把『改這一行為什麼會炸到那個 service』的問題,從推測昇級成可展示的最短路徑與逐跳解釋

背景

「為什麼我改了 auth.py 一行,test 卻在 billing/test_invoice.py 炸掉?」這種問題在 mid-size codebase 每天上演。除錯過程通常長這樣:

  1. 看 test 失敗的 stack trace
  2. 從失敗點反向找呼叫鏈,每跳一層 grep 一次
  3. 在腦海拼出「test → service A → service B → auth」的路徑
  4. 中間有 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.pySession.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 的回傳值會經過 AuthMiddlewareRequestContext 兩個中介層傳到 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 鄰居。


失敗處理

情境 排查
pathNo 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)收斂

內部連結


下一步

  1. 用 query 做跨檔案依賴追蹤(path 的前置:先用 query 找入口)
  2. 大型 monorepo 重購(path 之外,社群分析與 god node 識別)

關鍵學習點

  • path 給最短路徑,path-explain 給節點脈絡。前者是骨架,後者是血肉,除錯跟 review 兩個都要用。
  • pathunreachable 跟回「找到路徑」一樣有價值:它是「不影響」的證據,可以放心跳過無關 test。
  • 中介層(middleware、context、依賴注入)是 grep 容易漏、path 不會漏的所在。凡是涉及跨層除錯,先跑一次 path 把骨架拉出來。
  • god node 是路徑上的高風險節點。path-explain 標出來後,重購要特別處理,因為它的下游影響面特別大。