analyze + report:把圖變成洞見

god nodes、surprising connections、建議問題,與 GRAPH_REPORT.md 的生成
檔案:analyze.py(749 行)· report.py(300 行)

大方向

圖建好、分好群之後,analyze 負責「找出這張圖值得說的事」,report 負責把它排版成 GRAPH_REPORT.md——也就是你的 AI 助手會先讀的那份摘要。


1. analyze.py — 圖分析

god_nodes(G, top_n) nx.Graph → list[dict] analyze.py:109找出最「核心」的抽象——排除檔案級 hub 與內建型別雜訊。

「god node = 最高度數節點」這個直覺很簡單,但實作關鍵在「排除誰」:

  • 檔案級 hub 節點(_is_file_node)排除——它們機械式地累積 import/contains 邊,不代表真正的架構抽象。
  • 概念節點(_is_concept_node)與 JSON key 節點排除。
  • _BUILTIN_NOISE_LABELS 排除——這是 0.8.33 修的 bug:strintMagicMock 等內建型別節點曾灌爆度數、把真正的抽象擠出 god-node 排名。
教學重點:一個「找最高度數」的函數,真正的功力在「懂得排除雜訊」。程式 20 行,問題定義(什麼算真抽象)是它價值的 80%。
115 degree = dict(G.degree())
116 sorted_nodes = sorted(degree.items(), key=lambda x: x[1], reverse=True)
119 if _is_file_node(G, nid) or _is_concept_node(G, nid) or _is_json_key_node(G, nid):
120 continue # 排除機械 hub
121 if G.nodes[nid].get("label", "") in _BUILTIN_NOISE_LABELS: continue
128 if len(result) >= top_n: break
surprising_connections(G, communities, top_n) → list[dict] analyze.py:133找「真的出乎意料」的連線——依語料規模切換兩種策略。

「驚喜連線」是報告最有趣的一段。策略依語料規模分流:

  • 多檔語料is_multi_source):找「跨檔的實體之間」的邊,排序 AMBIGUOUS → INFERRED → EXTRACTED(越不確定越值得看)。
  • 單檔/單源語料:找「跨社群」的橋樑邊(用邊的介數中心性)——它揭露非顯而易見的結構耦合。

概念節點被排除——它們是刻意加入的,不是被發現的。

152 source_files = { data.get("source_file","") for _, data in G.nodes(data=True) … }
157 is_multi_source = len(source_files) > 1
159 if is_multi_source: return _cross_file_surprises(G, …, top_n)
162 else: return _cross_community_surprises(G, …, top_n)
suggest_questions(G, communities, community_labels, top_n) → list[dict] analyze.py:428生成「這張圖特別適合回答的問題」。

問題來源有四種,每個都帶 type / question / why 三個欄位:

  1. AMBIGUOUS 邊 → 未解決關係問題:「XY 的確實關係是什麼?」(因為那條邊被標不確定)。
  2. 橋樑節點(高介數中心性)→ 跨領域關注問題——那些連接多個社群的節點,可能是 cross-cutting concern。
  3. 未開發的 god nodes → 值得深入的核心抽象。
  4. 孤立節點 → 可能缺邊或未文件化。

這種「由圖結構自動生成問題」的能力,正是報告「4-5 個建議問題」的來源——也是 Graphify 最有產品感的輸出之一。

find_import_cycles(G) → list[dict] analyze.py:640在檔案級相依圖上找循環 import。

回傳「N 檔循環」,供報告的 Import Cycles 章節使用。只對含程式碼的語料有意義——文件語料沒有 import,那一段會是純噪音,所以 report.py 會先檢查圖裡有沒有 code 才輸出。

graph_diff(G_old, G_new) → dict analyze.py:556比較兩張圖的差異(增量更新的「什麼變了」)。

段落式說明:以 (u, v, relation) 為邊 key 做集合差異,算出新增/移除的節點與邊。這是 graphify update / PR 影響分析背後的差集工具。


2. report.py — 生成 GRAPH_REPORT.md

generate(G, communities, cohesion_scores, …, obsidian) → str report.py:71把圖 + 分析結果排版成完整 markdown 報告。

300 行的純函數:generate() 接收所有分析結果,回傳一份 markdown 字串。章節順序即報告的閱讀順序:

  1. Corpus Check — 檔案數、字數、語料是否大到需要圖。
  2. Summary — 節點/邊/社群數、EXTRACTED/INFERRED/AMBIGUOUS 比例、token 成本。
  3. Graph Freshnessbuilt_at_commit(0.7.0 加入,讓團隊能查圖是否過期)。
  4. Community Hubs — 社群導覽;只有 --obsidian 時輸出 wikilink,否則純清單(避免預設輸出一堆斷鏈)。
  5. God NodesSurprising ConnectionsImport Cycles(僅含 code)、Hyperedges
  6. Communities — 每個社群的 label、凝聚力、前 8 個成員(過薄社群省略)。
  7. Ambiguous Edges — 需要人工檢視的邊。
  8. Knowledge Gaps — 孤立節點、過薄社群、高 AMBIGUOUS 比例。
  9. Work-memory lessons — 從 .graphify_learning.json 來的學習覆蓋層(0.9.x 新功能)。
  10. Suggested Questions — 最後放建議問題。
設計重點:報告是「稽核軌跡」——每一段都由圖資料計算出來,而不是 LLM 生成的故事。所以任何數字都可追溯回圖。
93 confidences = [d.get("confidence","EXTRACTED") for _,_,d in G.edges(data=True)]
95 ext_pct = round(confidences.count("EXTRACTED") / total * 100)
128 f"- {G.number_of_nodes()} nodes · {G.number_of_edges()} edges …"
150 if non_empty: lines += ["", "## Community Hubs (Navigation)"]
164 lines.append(f"{i}. `{node['label']}` - {node['degree']} edges")
222 for cid, nodes in communities.items(): # 每個社群一段
300 return "\n".join(lines)
_safe_community_name · _learning_section · load_learning_for_report report.py 各處報告的配角:檔名安全化、學習章節、工作記憶載入。

段落式說明:

  • _safe_community_name(label) — 把社群名變成安全檔名(與 export 的 safe_name 同步),確保 hub 檔名與報告 wikilink 一致。
  • _learning_section(lines, learning, top_n) — 附加「Work-memory lessons」章節:preferred 來源(過去 session 證實有用)與 dead_ends(帶到死路的問題)。都沒有時整個章節省略。
  • load_learning_for_report(graph_path) — 讀取 .graphify_learning.json 覆蓋層與 memory docs,best-effort(永遠不 raise,失敗就回 None)。
看完這頁你應該能說出:god nodes 排除哪些節點、surprising connections 怎麼依語料規模分流、GRAPH_REPORT.md 有哪些章節、哪一章節只有含 code 的圖才輸出。接著讀 export + exporters 看「圖怎麼變成 graph.html 和其他檔案」。

① 進階真實情境 Worked Example:用 graph_diff 做 PR 影響分析

前面是「理解 god nodes 怎麼算」;這次是把 analyze 變成決策工具:你的團隊要在 dedup.py 重構前,先回答「這會影響哪些社群、哪些跨檔邊」。

  1. 建「before」圖:在目前 HEAD 跑 graphify extract .,存一份 graph-before.json(含 built_at_commit)。
  2. 改分支、建「after」圖:在 feature branch 跑 graphify update . 增量建圖,存 graph-after.json
  3. 算差異:用 graph_diff(G_old, G_new) 或對應 CLI——得到新增/移除的節點與邊(以 (u,v,relation) 為 key 的集合差)。
  4. 解讀影響:新增邊指向的社群 = 受影響的模組;被移除的邊 = 已刪的依賴。把「受影響社群」對照 GRAPH_REPORT 的社群清單,寫成 PR 描述。
  5. 判斷風險:如果改的是「跨檔案 hub 節點」,連帶的社群數會很多——標記為高風險;如果只在單社群內,低風險。
為什麼選這條路徑:grep 只能看到「這個檔改了」,graph_diff 看到的是「這條依賴斷了、這個社群被波及」——結構層的影響是文字 diff 看不見的。而且它與報告的「稽核性質」一致:每個「影響」都可回溯到具體的節點/邊,不是 LLM 的推測。

② 深入原理擴充:surprising connections 的兩種策略,與 god nodes 的排除清單

analyze 頁兩個函數最值得深挖:

大家以為建圖正確、但其實有誤的案例:以為「god node = 架構上最重要的抽象」。度數只代表「被連最多」,不代表「最重要的入口」——errors.py 的 exception 類別可能度數很高(人人 import),但它是協議細節不是核心流程。更隱晦的:degree 是無向的,把 calls 的方向資訊丟了;一個「被 50 個檔呼叫」的 util 與「呼叫 50 個函數的 orchestrator」度數相同,但架構意義完全不同。讀 god nodes 時要回到有向圖查 direction。

③ 診斷式疑難排解表

症狀可能原因解決方案
god nodes 出現預期外的工具類/設定類節點排除清單沒覆蓋(新語言型別、設定 key 未被視為 JSON key 節點)檢查 _BUILTIN_NOISE_LABELS_is_concept_node/_is_json_key_node 的判定
surprising connections 全是不重要的邊多檔語料下「跨檔實體邊」太多,排序被 AMBIGUOUS 灌滿調整 top_n;對照單檔語料改用跨社群橋樑策略
graph_diff 回報大量假性新增/移除節點 ID 不穩定(hash 型 ID 或順序依賴)導致同節點被算成不同確認 ID 用穩定規範({parent_dir}_{stem});排除順序差異造成的噪音
報告出現「沒有 import 的 Import Cycles」文件語料仍輸出了 code 專屬章節確認 report.py 的「圖裡有沒有 code」檢查有生效(升級版本)
建議問題(suggested questions)問不到重點問題由結構啟發(AMBIGUOUS 邊、橋樑節點),不是語意啟發把它當「結構提議」而非「領域問題」;用 path/explain 人工補足語意深度

④ 進階挑戰題

  1. 「god node 的 degree 是無向的」——請設計一個「有向 god node」指標(考慮出度/入度分離),並預測它會讓哪類節點浮出水面、哪類沉下去。
  2. surprising connections 排序 AMBIGUOUS 優先。請提出一個反例:某條 EXTRACTED 邊其實比 AMBIGUOUS 邊更值得報告(提示:考慮邊的跨社群性質),並說明你的排序規則。
  3. report 說「每一段都由圖資料計算」。請找一段看似「計算」、其實是「啟發式」的章節(例如 Knowledge Gaps 的孤獨節點判斷),並討論這種啟發式誤判的後果與改善方向。