Files
graphify-analysis/public/index.html
kang 0e9ef529cc feat(ui): 左侧固定边栏 TOC + 滚动联动高亮
- 两栏布局: 260px sticky sidebar + max 820px 主栏
- TOC 跟随滚动自动高亮当前章节(scrollspy 监听 scroll 事件)
- 平滑跳转 + URL hash 同步, 不依赖 :target 防止 scroll-margin 兼容问题
- 移动端 (< 920px) sidebar 自动折叠成顶部横向标签栏
- 边栏底部加项目元数据小卡片(日期/版本/对比/结论)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:46:23 +08:00

730 lines
36 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Graphify 源码解析 & GitNexus 对比 · 2026-04-13</title>
<style>
:root {
--bg: #0b0d12;
--bg-2: #11141b;
--bg-3: #171b24;
--border: #232836;
--fg: #e6e8ee;
--muted: #8b93a7;
--accent: #7aa2f7;
--accent-2: #9ece6a;
--warn: #e0af68;
--bad: #f7768e;
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Noto Sans CJK SC", sans-serif;
}
* { box-sizing: border-box; }
html, body { margin: 0; background: var(--bg); color: var(--fg); font-family: var(--sans); -webkit-font-smoothing: antialiased; }
body { line-height: 1.65; font-size: 15px; scroll-behavior: smooth; }
/* ── two-column layout ───────────────────────────────────────── */
.layout {
max-width: 1280px;
margin: 0 auto;
padding: 0 28px;
display: grid;
grid-template-columns: 260px minmax(0, 1fr);
gap: 56px;
align-items: start;
}
.main { min-width: 0; padding: 48px 0 96px; max-width: 820px; }
/* ── sticky sidebar + scrollspy TOC ──────────────────────────── */
.sidebar {
position: sticky;
top: 0;
height: 100vh;
padding: 36px 0 28px;
overflow-y: auto;
border-right: 1px solid var(--border);
margin-right: -28px;
padding-right: 28px;
}
.sidebar::-webkit-scrollbar { width: 6px; }
.sidebar::-webkit-scrollbar-track { background: transparent; }
.sidebar::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.sidebar::-webkit-scrollbar-thumb:hover { background: #2f3548; }
.brand {
font-size: 12px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
padding-bottom: 10px;
margin-bottom: 14px;
border-bottom: 1px solid var(--border);
}
.brand b { color: var(--accent); font-weight: 600; }
.toc-label {
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
margin: 18px 0 10px;
}
nav.toc { display: flex; flex-direction: column; gap: 2px; }
nav.toc a {
display: block;
padding: 8px 12px 8px 16px;
font-size: 13px;
color: var(--muted);
border-left: 2px solid transparent;
border-radius: 0 6px 6px 0;
border-bottom: none;
transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease, padding-left 0.15s ease;
line-height: 1.4;
}
nav.toc a:hover {
color: var(--fg);
background: var(--bg-2);
border-left-color: var(--border);
}
nav.toc a.active {
color: var(--accent);
background: linear-gradient(90deg, rgba(122,162,247,0.12), transparent);
border-left-color: var(--accent);
padding-left: 20px;
font-weight: 600;
}
.toc-num { display: inline-block; width: 22px; color: var(--muted); font-family: var(--mono); font-size: 11px; }
nav.toc a.active .toc-num { color: var(--accent); }
.sidebar-foot {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid var(--border);
font-size: 11px;
color: var(--muted);
line-height: 1.6;
}
.sidebar-foot a { color: var(--accent); border: none; }
.sidebar-foot .row { display: flex; justify-content: space-between; padding: 3px 0; }
.sidebar-foot .k { color: var(--muted); }
/* anchor offset so headings don't get hidden under sticky bars */
h2[id] { scroll-margin-top: 24px; }
/* mobile: sidebar collapses to top bar */
@media (max-width: 920px) {
.layout { grid-template-columns: 1fr; gap: 0; padding: 0 20px; }
.main { padding-top: 24px; max-width: 100%; }
.sidebar {
position: static;
height: auto;
border-right: none;
border-bottom: 1px solid var(--border);
margin-right: 0;
padding: 18px 0;
}
nav.toc {
flex-direction: row;
flex-wrap: wrap;
gap: 4px;
}
nav.toc a {
border-left: none;
border-bottom: 2px solid transparent;
border-radius: 4px;
padding: 6px 10px;
font-size: 12px;
}
nav.toc a.active {
padding-left: 10px;
border-left: none;
border-bottom-color: var(--accent);
background: var(--bg-2);
}
.sidebar-foot { display: none; }
}
header { border-bottom: 1px solid var(--border); padding-bottom: 20px; margin-bottom: 40px; }
header .eyebrow { color: var(--muted); font-size: 12px; letter-spacing: 0.12em; text-transform: uppercase; }
header h1 { font-size: 28px; font-weight: 600; margin: 8px 0 6px; }
header .sub { color: var(--muted); font-size: 14px; }
header .meta { margin-top: 14px; display: flex; gap: 12px; flex-wrap: wrap; font-size: 12px; color: var(--muted); }
header .meta span { background: var(--bg-3); padding: 4px 10px; border-radius: 12px; border: 1px solid var(--border); }
h2 { font-size: 20px; font-weight: 600; margin: 52px 0 16px; padding-top: 18px; border-top: 1px solid var(--border); }
h2:first-of-type { border-top: none; padding-top: 0; }
h3 { font-size: 16px; font-weight: 600; margin: 28px 0 10px; color: var(--fg); }
h4 { font-size: 14px; font-weight: 600; margin: 18px 0 8px; color: var(--accent); }
p { margin: 0 0 14px; }
code { font-family: var(--mono); font-size: 0.88em; background: var(--bg-3); padding: 1px 6px; border-radius: 4px; color: var(--accent-2); }
a { color: var(--accent); text-decoration: none; border-bottom: 1px dashed var(--border); }
a:hover { border-bottom-color: var(--accent); }
ul, ol { margin: 0 0 14px; padding-left: 22px; }
li { margin-bottom: 6px; }
.card { background: var(--bg-2); border: 1px solid var(--border); border-radius: 10px; padding: 18px 22px; margin: 14px 0; }
.card-accent { border-left: 3px solid var(--accent); }
.card-good { border-left: 3px solid var(--accent-2); }
.card-warn { border-left: 3px solid var(--warn); }
.card-bad { border-left: 3px solid var(--bad); }
.tldr { background: linear-gradient(180deg, rgba(122,162,247,0.06), transparent); border: 1px solid var(--border); border-radius: 12px; padding: 20px 24px; margin-bottom: 28px; }
.tldr .label { font-size: 12px; letter-spacing: 0.1em; color: var(--accent); text-transform: uppercase; margin-bottom: 8px; }
.tldr .verdict { font-size: 18px; font-weight: 600; line-height: 1.5; }
table { width: 100%; border-collapse: collapse; margin: 14px 0 24px; font-size: 13.5px; }
th, td { border: 1px solid var(--border); padding: 10px 12px; text-align: left; vertical-align: top; }
th { background: var(--bg-3); font-weight: 600; color: var(--fg); }
td.dim { color: var(--muted); }
td.yes { color: var(--accent-2); font-weight: 600; }
td.no { color: var(--bad); }
td.partial { color: var(--warn); }
.num { text-align: right; font-family: var(--mono); }
.ref { color: var(--muted); font-family: var(--mono); font-size: 12px; }
.pill { display: inline-block; font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--border); background: var(--bg-3); color: var(--muted); margin-right: 4px; font-family: var(--mono); }
.pill-good { color: var(--accent-2); border-color: rgba(158,206,106,0.3); }
.pill-warn { color: var(--warn); border-color: rgba(224,175,104,0.3); }
.pill-bad { color: var(--bad); border-color: rgba(247,118,142,0.3); }
pre { background: var(--bg-3); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; overflow-x: auto; font-family: var(--mono); font-size: 12.5px; line-height: 1.55; color: #cdd6f4; }
pre .c { color: var(--muted); }
pre .k { color: var(--accent); }
pre .s { color: var(--accent-2); }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 720px) { .grid-2 { grid-template-columns: 1fr; } }
.toc { background: var(--bg-2); border: 1px solid var(--border); border-radius: 10px; padding: 16px 22px; margin-bottom: 32px; }
.toc .label { font-size: 11px; letter-spacing: 0.12em; color: var(--muted); text-transform: uppercase; margin-bottom: 10px; }
.toc ol { margin: 0; padding-left: 20px; font-size: 13.5px; }
.toc li { margin-bottom: 3px; }
footer { margin-top: 72px; padding-top: 20px; border-top: 1px solid var(--border); color: var(--muted); font-size: 12px; }
</style>
</head>
<body>
<div class="layout">
<aside class="sidebar">
<div class="brand">graphify <b>analysis</b></div>
<div class="toc-label">章节</div>
<nav class="toc" id="toc">
<a href="#overview"><span class="toc-num">01</span>架构速览</a>
<a href="#pipeline"><span class="toc-num">02</span>执行流程</a>
<a href="#modules"><span class="toc-num">03</span>逐模块解剖</a>
<a href="#graphrag"><span class="toc-num">04</span>GraphRAG 验证</a>
<a href="#bench"><span class="toc-num">05</span>71.5× 真相</a>
<a href="#highlights"><span class="toc-num">06</span>设计亮点</a>
<a href="#weaknesses"><span class="toc-num">07</span>技术短板</a>
<a href="#vs"><span class="toc-num">08</span>vs GitNexus</a>
<a href="#recommendation"><span class="toc-num">09</span>最终建议</a>
</nav>
<div class="sidebar-foot">
<div class="row"><span class="k">日期</span><span>2026-04-13</span></div>
<div class="row"><span class="k">Graphify</span><span>v0.4.8</span></div>
<div class="row"><span class="k">行数对比</span><span>8.2k vs 31k</span></div>
<div class="row"><span class="k">结论</span><span>不替换</span></div>
</div>
</aside>
<main class="main">
<header>
<div class="eyebrow">源码评估报告</div>
<h1>Graphify 全面解析 &amp; GitNexus 替换评估</h1>
<div class="sub">"看下这个项目是不是噱头 · 需不需要把工作看板的代码图谱换成这个"</div>
<div class="meta">
<span>日期 2026-04-13</span>
<span>Graphify v0.4.8 · 8,237 LOC · Python</span>
<span>GitNexus · 31,367 LOC · TypeScript</span>
<span>仓库快照 <code>~/Projects/research/20260413-graphify-analysis/</code></span>
</div>
</header>
<div class="tldr">
<div class="label">结论</div>
<div class="verdict">
Graphify 不是噱头,是一个<b>被过度营销的实用单项目 AST 索引工具</b>,"GraphRAG / 多模态 AI 架构助理" 是包装话术。
<b>不要替换工作看板的 GitNexus</b> —— GitNexus 在代码静态分析这一维度比 Graphify 领先一个量级。
</div>
</div>
<h2 id="overview">一、Graphify 架构速览</h2>
<p>一句话:<b>tree-sitter AST 提取器 + networkx 图 + Leiden 社区检测 + 10 种导出格式 + 9 种 Agent 平台集成脚本</b>
零 LLM 依赖,"智能"部分全部外包给宿主 Agent(Claude Code / Codex / OpenCode / ...)。</p>
<table>
<thead><tr><th>文件</th><th class="num">行数</th><th>作用</th></tr></thead>
<tbody>
<tr><td><code>extract.py</code></td><td class="num">2804</td><td>tree-sitter AST 提取(22 语言)—— <b>真正的发动机</b></td></tr>
<tr><td><code>export.py</code></td><td class="num">1001</td><td>HTML / SVG / GraphML / Cypher / Obsidian 导出</td></tr>
<tr><td><code>__main__.py</code></td><td class="num">972</td><td>CLI + 9 种 AI 平台 install/uninstall 脚本</td></tr>
<tr><td><code>analyze.py</code></td><td class="num">537</td><td>god nodes(度数排序)+ surprising connections(跨社区边)</td></tr>
<tr><td><code>detect.py</code></td><td class="num">502</td><td>文件扫描 + 类型识别(含论文启发式)+ .graphifyignore</td></tr>
<tr><td><code>serve.py</code></td><td class="num">364</td><td>MCP stdio server(7 个工具)</td></tr>
<tr><td><code>ingest.py</code></td><td class="num">297</td><td>URL 抓取:twitter/arxiv/github/youtube/pdf/image/web</td></tr>
<tr><td><code>hooks.py</code></td><td class="num">198</td><td>git post-commit/checkout 自动重建(纯 AST)</td></tr>
<tr><td><code>security.py</code></td><td class="num">197</td><td>SSRF 防护 + URL 校验 + redirect 再验证</td></tr>
<tr><td><code>watch.py</code></td><td class="num">183</td><td>文件监听,代码变更增量重建</td></tr>
<tr><td><code>transcribe.py</code></td><td class="num">182</td><td>whisper 视频/音频转写(可选)</td></tr>
<tr><td><code>report.py</code></td><td class="num">175</td><td>GRAPH_REPORT.md 生成</td></tr>
<tr><td><code>cache.py</code></td><td class="num">154</td><td>SHA256 文件级缓存(markdown 剥 YAML frontmatter)</td></tr>
<tr><td><code>cluster.py</code></td><td class="num">137</td><td>Leiden / Louvain 社区检测 + 大社区拆分</td></tr>
<tr><td><code>benchmark.py</code></td><td class="num">129</td><td>token reduction 计算 —— 71.5× 来源</td></tr>
<tr><td><code>build.py</code></td><td class="num">87</td><td>dict → networkx.Graph 组装</td></tr>
<tr><td colspan="3" class="dim">(其余小文件:validate / wiki / __init__ / manifest · 共 ~130 行)</td></tr>
</tbody>
<tfoot><tr><th>合计</th><th class="num">8,237</th><th class="dim">20 个 .py 文件 · MIT License · PyPI: <code>graphifyy</code></th></tr></tfoot>
</table>
<h3>依赖(全部本地库,无 LLM SDK)</h3>
<pre><span class="c"># 核心</span>
networkx
tree-sitter &gt;= 0.23.0
tree-sitter-{python,javascript,typescript,go,rust,java,c,cpp,ruby,
c-sharp,kotlin,scala,php,swift,lua,zig,powershell,
elixir,objc,julia} <span class="c"># 22 种语言</span>
<span class="c"># 可选 extras</span>
mcp <span class="c"># MCP server</span>
neo4j <span class="c"># 直推图数据库</span>
pypdf <span class="c"># PDF 文本提取</span>
graspologic <span class="c"># Leiden(否则回落 Louvain)</span>
python-docx <span class="c"># Office</span>
faster-whisper <span class="c"># 视频转写</span>
yt-dlp <span class="c"># YouTube 下载</span></pre>
<h2 id="pipeline">二、执行流程 &amp; 关键发现</h2>
<div class="card card-accent">
<h4>关键发现:完整 pipeline 不在 CLI 里</h4>
<p><code>__main__.py</code><code>main()</code> 只有 <code>install / query / benchmark / save-result / hook</code> 等子命令
(<span class="ref">__main__.py:780-968</span>)—— <b>根本没有 "build graph" 子命令</b></p>
<p>完整 pipeline 是被 <code>skill.md</code> 驱动的:宿主 Agent 按 skill.md 的 step 1..N,用内联
<code>python3 -c "from graphify.X import Y"</code> 挨个调各模块。这种"skill 脚本驱动"设计
把 LLM 部分合法地外包给宿主 Agent,Python 包就完全 offline。</p>
</div>
<pre><span class="c"># 实际执行顺序(由 skill.md 协调)</span>
detect.detect(root) <span class="c"># detect.py:329 扫描+分类</span>
<span class="c">并行</span>
├── extract.extract(code_paths) <span class="c"># extract.py:2639 tree-sitter AST</span>
└── [<span class="k">宿主 Agent subagent 读 PDF / 图 / markdown</span>] → JSON
cache.save_semantic_cache(...) <span class="c"># cache.py:119</span>
build.build([ast_result, semantic_result]) <span class="c"># build.py:84</span>
cluster.cluster(G) <span class="c"># cluster.py:59 Leiden/Louvain</span>
analyze.god_nodes + surprising_connections <span class="c"># analyze.py:42/61</span>
report.generate(...) <span class="c"># report.py:14</span>
export.to_html / to_json / to_obsidian <span class="c"># export.py:329/285/447</span></pre>
<h2 id="modules">三、核心模块逐个解剖</h2>
<h3>3.1 <code>extract.py</code>(2804 行) —— 真正的发动机</h3>
<p><b>架构模式</b>:<code>LanguageConfig</code> dataclass(<span class="ref">extract.py:24-60</span>)+ 通用
<code>_extract_generic()</code>(<span class="ref">extract.py:645-1010</span>)+ 每种语言一个配置实例 + 少数语言特有的 walker。</p>
<pre><span class="k">class</span> LanguageConfig:
class_types <span class="c"># 哪些 tree-sitter 节点类型算"类"</span>
function_types <span class="c"># 哪些算"函数"</span>
import_types
call_types
name_field <span class="c"># 默认从哪个 field 拿名字</span>
body_field
function_boundary_types <span class="c"># 递归到这里停</span>
import_handler <span class="c"># 每种语言 import 语法不一样,单独回调</span>
resolve_function_name_fn <span class="c"># C/C++ 用 declarator 解包</span>
extra_walk_fn <span class="c"># JS 箭头、C# namespace 等特殊分支</span></pre>
<p>13 种语言走通用路径(<span class="ref">extract.py:419-627</span>),<b>7 种独立手写</b>(因为 tree-sitter 节点命名差太大):
<code>extract_go</code>(190 行)<code>extract_rust</code>(168 行)<code>extract_zig</code>
<code>extract_powershell</code><code>extract_objc</code><code>extract_elixir</code><code>extract_julia</code></p>
<p><b>两遍 walk</b>:</p>
<ol>
<li><b>结构 walk</b>(<span class="ref">extract.py:703-866</span>):进 class 建节点 + <code>contains/method/inherits</code> 边;进 function 记下 body 挂到 <code>function_bodies</code> 列表</li>
<li><b>调用图 walk</b>(<span class="ref">extract.py:877+</span>):对每个 function body 再 walk,找 callee 名字,查 <code>label_to_nid</code><code>calls</code></li>
</ol>
<div class="card card-good">
<h4>亮点: Python 独有的 <code>_resolve_cross_file_imports</code></h4>
<p><span class="ref">extract.py:2110-2240</span>,130 行 —— 整个项目 <b>唯一真正跨文件推理</b>的地方。</p>
<p>Pass 1:扫所有 Python 文件的 AST 节点,建 <code>stem_to_entities[stem][ClassName] = nid</code> 全局映射表。<br>
Pass 2:对每个文件找 <code>from .models import Response</code>,在映射表里查到 <code>Response</code> 的 nid,然后<b>对当前文件里的每个 class</b>,添加 <code>DigestAuth --uses--&gt; Response</code> 的 INFERRED 边。</p>
<p>这是"god nodes / surprising connections"最后能出东西的关键 —— 没这步,Python 项目全是 <code>contains</code> 边的垃圾。<b>其他 21 种语言都没做这个</b>,只有结构 + 同文件 calls。</p>
</div>
<h3>3.2 <code>detect.py</code>(502 行) —— 文件扫描</h3>
<ul>
<li><code>_looks_like_paper()</code>(<span class="ref">detect.py:68</span>)用 13 条正则(arxiv / doi / <code>\cite{}</code> / <code>\d{4}\.\d{4,5}</code>)判断一个 <code>.md</code> 是不是学术论文转出来的 —— 命中 ≥3 条升级为 PAPER</li>
<li><code>_SENSITIVE_PATTERNS</code>(<span class="ref">detect.py:33-40</span>)过滤 <code>.env / .pem / id_rsa / aws_credentials</code> —— <b>默认不读密钥文件</b></li>
<li><code>_SKIP_DIRS</code>(<span class="ref">detect.py:238</span>)自动跳 <code>venv / node_modules / .git / dist / build</code></li>
<li><code>.graphifyignore</code> 支持向上查找到 <code>.git</code> 目录为止(<span class="ref">detect.py:266</span>)</li>
<li><code>convert_office_file</code>(<span class="ref">detect.py:194</span>)把 .docx / .xlsx 转 markdown sidecar,哈希命名避冲突</li>
</ul>
<h3>3.3 <code>cache.py</code>(154 行) —— SHA256 内容缓存</h3>
<ul>
<li><code>file_hash = SHA256(content) ⊕ SHA256(resolved_path)</code>(<span class="ref">cache.py:20</span>)—— 路径加进哈希防同内容碰撞</li>
<li><b>贴心细节</b>:Markdown 的 YAML frontmatter 会先剥离再哈希(<span class="ref">cache.py:10-17</span>),metadata 变更(reviewed/status/tags)不触发重抽取</li>
<li>存成 <code>graphify-out/cache/{hash}.json</code>,<code>os.replace</code> 原子写</li>
</ul>
<h3>3.4 <code>cluster.py</code>(137 行) —— Leiden / Louvain</h3>
<ul>
<li>先试 <code>graspologic.partition.leiden</code>,失败 fallback 到 <code>networkx.community.louvain_communities</code>(<span class="ref">cluster.py:22-52</span>)</li>
<li>孤立点(degree 0)Leiden 会 warning,单独抠出来每个自成一个社区(<span class="ref">cluster.py:77-91</span>)</li>
<li>大社区限制 <code>_MAX_COMMUNITY_FRACTION = 0.25</code>:超过总节点 25% 的社区二次 Leiden 拆分(<span class="ref">cluster.py:93</span>)</li>
<li>最后按 size 降序重编号 —— 社区 0 永远是最大的(可复现)</li>
<li>PowerShell 5.1 缓冲污染 workaround:graspologic 会吐 ANSI 转义,专门包了 <code>redirect_stdout</code>(<span class="ref">cluster.py:11-20</span>,issue #19)</li>
</ul>
<h3>3.5 <code>analyze.py</code>(537 行) —— "智能"层</h3>
<p><b>其实都是 networkx 一行题</b></p>
<p><code>god_nodes</code>(<span class="ref">analyze.py:42</span>)= <code>dict(G.degree())</code> 排序,过滤掉文件节点 + concept 节点,取 top_n。
所谓"god nodes" = 度数最高的真实实体。<code>_is_file_node</code>(<span class="ref">analyze.py:11-38</span>)过滤三类伪节点:文件级 hub、<code>.method()</code> 方法桩、孤立 <code>func()</code></p>
<p><code>_surprise_score()</code>(<span class="ref">analyze.py:131-184</span>)是一个朴素加权:</p>
<ul>
<li>置信度加成:AMBIGUOUS +3 / INFERRED +2 / EXTRACTED +1</li>
<li>跨文件类型(code↔paper/image)+2</li>
<li>跨 repo(顶级目录不同)+2</li>
<li>跨社区(Leiden 说结构上远)+1</li>
<li><code>semantically_similar_to</code> 关系 ×1.5</li>
<li>低度节点(≤2)连到高度节点(≥5)+1,生成 "peripheral → hub" 叙述</li>
</ul>
<h3>3.6 <code>serve.py</code>(364 行) —— MCP 服务器</h3>
<p>暴露 7 个 MCP 工具:<code>query_graph</code><code>get_node</code><code>get_neighbors</code><code>get_community</code><code>god_nodes</code><code>graph_stats</code><code>shortest_path</code>(<span class="ref">serve.py:156-226</span>)。</p>
<div class="card card-warn">
<h4>注意:"查询"不是语义搜索</h4>
<p><code>_score_nodes</code>(<span class="ref">serve.py:42</span>)就是把问题 split 成 &gt;2 字符的词,
在 label 里子串匹配 +1 / source_file +0.5,排序取 top3 当 BFS 起点。
<b>没有 embedding、没有 BM25、甚至没有 stemming</b>。对中英混合语料很不友好。</p>
</div>
<h3>3.7 <code>export.py</code>(1001 行) —— 十种输出</h3>
<ul>
<li><code>to_json</code> / <code>to_cypher</code> / <code>to_html</code>(交互 vis.js 单文件)/ <code>to_obsidian</code>(每节点一 md,每社区一 hub,带 wikilinks)</li>
<li><code>to_canvas</code>(Obsidian Canvas)/ <code>push_to_neo4j</code>(bolt 协议直推)/ <code>to_graphml</code>(Gephi/yEd)/ <code>to_svg</code></li>
<li><code>MAX_NODES_FOR_VIZ = 5_000</code>(<span class="ref">export.py:19</span>)—— 超过就不出 HTML</li>
</ul>
<h3>3.8 <code>security.py</code>(197 行) —— SSRF 防护</h3>
<p><code>validate_url</code> 解析 hostname → <code>getaddrinfo</code> → 拒绝
<code>is_private / is_reserved / is_loopback / is_link_local</code>,加黑名单 <code>metadata.google.internal</code>
(<span class="ref">security.py:14-64</span>)。<code>_NoFileRedirectHandler</code> 重写 <code>HTTPRedirectHandler</code>,
每次 redirect 都重新校验一遍 —— 防 open-redirect SSRF。<b>写一个抓取模块就自带这种层次的防护,不常见。</b></p>
<h2 id="graphrag">四、"GraphRAG" 噱头验证</h2>
<p>"GraphRAG" 按微软原论文定义:<b>LLM 抽实体 + LLM 抽关系 + LLM 写社区摘要 + 分层答案合成</b>。Graphify 对齐情况:</p>
<table>
<thead><tr><th>GraphRAG 环节</th><th>Graphify 做法</th><th>状态</th></tr></thead>
<tbody>
<tr><td>LLM 抽实体</td><td>tree-sitter AST(代码)/ 宿主 Agent subagent(非代码)</td><td class="no"></td></tr>
<tr><td>LLM 抽关系</td><td>AST + 字符串 heuristics</td><td class="no"></td></tr>
<tr><td>社区检测</td><td>Leiden / Louvain</td><td class="yes"></td></tr>
<tr><td>LLM 写社区摘要</td><td>只有 label 模板,宿主 Agent 可能帮忙写</td><td class="no"></td></tr>
<tr><td>分层答案合成</td><td>BFS + term-frequency 子串匹配</td><td class="no"></td></tr>
</tbody>
</table>
<p><b>结论</b>:这不是 GraphRAG,是"一个有置信度标签的 tree-sitter 代码图谱工具 + Obsidian 导出器 + 9 个 Agent 平台的 SKILL.md 胶水"。
技术本身合格(8k LOC 大部分在 extract.py 真的干活),但把它叫 "GraphRAG AI 架构助理" 是营销话术。</p>
<h2 id="bench">五、"71.5× token 节省" 真相</h2>
<p>拆开 <code>benchmark.py</code> 看(<span class="ref">benchmark.py:1-130</span>):</p>
<pre>_CHARS_PER_TOKEN = 4
corpus_tokens = corpus_words * 100 // 75 <span class="c"># 1 词 ≈ 1.33 token</span>
query_tokens = BFS 子图渲染文本长度 // 4
reduction_ratio = corpus_tokens / avg(query_tokens of 5 个固定英文问题)
<span class="c"># 5 个固定问题(benchmark.py:55-61)</span>
"how does authentication work"
"what is the main entry point"
"how are errors handled"
"what connects the data layer to the api"
"what are the core abstractions"</pre>
<div class="card card-warn">
<h4>71.5× 是怎么来的</h4>
<p>= "整个语料的 token 数" 除以 "单次 BFS 子图的 token 数"。<b>不包含建图时烧掉的 subagent token</b></p>
<p><b>代码库</b>很好看(AST 零 token + 后续查询便宜),对<b>论文/截图</b>混合语料有水分 —— 首次建图那一次 PDF subagent 读取的 token 会把 ratio 稀释。</p>
<p>而且固定 5 个英文问题 + 子串匹配选起点 —— 换一个中文项目可能选不到起点直接返回 0。</p>
</div>
<h2 id="highlights">六、设计亮点(这些可以抄)</h2>
<div class="grid-2">
<div class="card card-good">
<h4>1. LanguageConfig 模式</h4>
<p><span class="ref">extract.py:24</span>。通用 walker + 每语言差异填配置。13 种语言共用 200 行通用代码。</p>
</div>
<div class="card card-good">
<h4>2. SHA256 内容缓存 + Markdown 剥 frontmatter</h4>
<p><span class="ref">cache.py:20</span>。"改 metadata 不触发重抽取"这种细节。</p>
</div>
<div class="card card-good">
<h4>3. 三档置信度 + score</h4>
<p>每条边打 EXTRACTED/INFERRED/AMBIGUOUS,INFERRED 再带 0.6-0.95 的 <code>confidence_score</code>,报告里直接出现 "avg confidence: 0.78",<b>诚实度拉满</b></p>
</div>
<div class="card card-good">
<h4>4. Python 跨文件 class 级 uses 边</h4>
<p><span class="ref">extract.py:2110</span>。让 god nodes 变得有意义的关键。</p>
</div>
<div class="card card-good">
<h4>5. 单 HTML 交互图</h4>
<p><span class="ref">export.py:329</span>。内嵌 vis.js,无需服务器,发一个文件给客户直接看。</p>
</div>
<div class="card card-good">
<h4>6. 9 平台集成脚本</h4>
<p><span class="ref">__main__.py:49-105</span>。Claude/Codex/Cursor/OpenCode/Aider/Copilot/OpenClaw/Factory/Trae/Antigravity/Gemini。</p>
</div>
<div class="card card-good">
<h4>7. SSRF 防护默认打开</h4>
<p><span class="ref">security.py:14-64</span>。默认写一个 URL 抓取模块就有这种层次的防护,业界少见。</p>
</div>
<div class="card card-good">
<h4>8. 大社区自动拆分</h4>
<p><span class="ref">cluster.py:93</span>&gt;25% 超大社区二次 Leiden 拆,防退化。</p>
</div>
</div>
<h2 id="weaknesses">七、技术短板</h2>
<ol>
<li><b>22 语言"支持"很不均匀</b>:只有 Python 做了跨文件 uses 推断,其他 21 种只有"同文件级 calls + imports"。多语言 polyglot 项目图质量会肉眼可见不如 Python。</li>
<li><b><code>_extract_python_rationale</code></b>(<span class="ref">extract.py:1011</span>)基于 docstring 的 "X because Y" heuristics 脆得很。</li>
<li><b>查询不是语义搜索</b>,是子串 +1/+0.5 打分,没有 embedding/BM25/stemming。中英混合语料不友好。</li>
<li><b><code>_make_id</code><code>re.sub(r"[^a-zA-Z0-9]+", "_", combined)</code></b>(<span class="ref">extract.py:14</span>)—— 对中文/非 ASCII 标识符直接 collapse 成空串,<b>中文代码库识别不了</b></li>
<li><b>C++ 模板/宏、Python metaclass/装饰器动态类、JS Proxy/Reflect</b> 这类 AST 看不穿的东西通通识别不出来 —— 本质上是静态结构提取。</li>
<li><b>benchmark 的 71.5×</b>:固定 5 个英文问题 + 子串匹配选起点。</li>
<li><b>无类型系统</b>:没有 symbol table、没有 type env、没有方法解析顺序(MRO)。</li>
<li><b><code>_is_file_node</code> 启发式</b>(<span class="ref">analyze.py:11-38</span>)在非 Python 语言下容易误伤 —— Go/Rust 函数都是 <code>()</code> 结尾,又按 degree 判断……补丁摞补丁。</li>
<li><b>PyPI 包名 <code>graphifyy</code></b>(两个 y,因为 <code>graphify</code> 被占),看得出作者赶时间。</li>
</ol>
<h2 id="vs">八、GitNexus vs Graphify 对比</h2>
<p>你的工作看板现在用的是 <code>~/Projects/code/20260319-gitnexus/</code>(来源 <code>abhigyanpatwari/GitNexus</code>,17.8k stars,浏览器端 WASM 方案)。</p>
<table>
<thead>
<tr><th>维度</th><th>GitNexus(你已有)</th><th>Graphify</th></tr>
</thead>
<tbody>
<tr>
<td>代码规模</td>
<td class="yes">31,367 行 TypeScript</td>
<td class="partial">8,237 行 Python</td>
</tr>
<tr>
<td>架构</td>
<td class="yes">浏览器 WASM 零服务器<br><span class="pill">Tree-sitter</span><span class="pill">LadybugDB</span><span class="pill">Sigma.js</span></td>
<td class="partial">Python CLI + networkx 内存图</td>
</tr>
<tr>
<td>图数据库</td>
<td class="yes">LadybugDB(Cypher 查询)</td>
<td class="no">JSON 文件</td>
</tr>
<tr>
<td>类型系统</td>
<td class="yes"><code>symbol-table.ts</code> + <code>type-env.ts</code> + <code>type-extractors/</code> —— 真的做类型推导</td>
<td class="no"></td>
</tr>
<tr>
<td>Python MRO</td>
<td class="yes"><code>mro-processor.ts</code> —— 方法解析顺序</td>
<td class="no"></td>
</tr>
<tr>
<td>框架识别</td>
<td class="yes"><code>framework-detection.ts</code></td>
<td class="no"></td>
</tr>
<tr>
<td>入口打分</td>
<td class="yes"><code>entry-point-scoring.ts</code></td>
<td class="no"></td>
</tr>
<tr>
<td>跨文件 resolver</td>
<td class="yes"><code>resolvers/</code> + <code>resolution-context.ts</code> + <code>named-binding-extraction.ts</code></td>
<td class="partial">只有 Python 做了 130 行</td>
</tr>
<tr>
<td>社区检测</td>
<td class="yes">Leiden + <code>cluster-enricher.ts</code></td>
<td class="yes">Leiden/Louvain</td>
</tr>
<tr>
<td>可视化</td>
<td class="yes">Sigma.js Web UI @ 4090</td>
<td class="yes">单 HTML + vis.js</td>
</tr>
<tr>
<td>多项目面板</td>
<td class="yes">Web UI 常驻</td>
<td class="no">每项目一目录,一次性</td>
</tr>
<tr>
<td>MCP Server</td>
<td class="yes"><code>gitnexus/src/mcp/</code></td>
<td class="yes"><code>serve.py</code>(7 工具)</td>
</tr>
<tr>
<td>非代码输入(PDF/图/视频)</td>
<td class="no"></td>
<td class="yes">支持(外包给 subagent)</td>
</tr>
<tr>
<td>Obsidian 导出</td>
<td class="no"></td>
<td class="yes">有(每节点一 md + 社区 hub)</td>
</tr>
<tr>
<td>Agent 平台集成</td>
<td class="partial">Claude plugin + Cursor integration</td>
<td class="yes">9 种平台</td>
</tr>
<tr>
<td>诚信度标签</td>
<td class="partial">未知</td>
<td class="yes">EXTRACTED/INFERRED/AMBIGUOUS 三档</td>
</tr>
<tr>
<td>LLM 依赖</td>
<td class="partial">LangChain + 多家 LLM(可选增强)</td>
<td class="yes">无硬依赖(委托宿主 Agent)</td>
</tr>
</tbody>
</table>
<div class="card card-good">
<h4>GitNexus 的 <code>gitnexus/src/core/ingestion/</code> 目录(一瞥)</h4>
<pre>ast-cache.ts call-processor.ts call-routing.ts
cluster-enricher.ts community-processor.ts entry-point-scoring.ts
export-detection.ts filesystem-walker.ts framework-detection.ts
heritage-processor.ts import-processor.ts language-config.ts
mro-processor.ts named-binding-extraction.ts
parsing-processor.ts pipeline.ts process-processor.ts
resolution-context.ts structure-processor.ts symbol-table.ts
tree-sitter-queries.ts type-env.ts type-extractors/
resolvers/ workers/</pre>
<p class="dim">光是 ingestion 里就有 20+ 个处理器。在代码静态分析这件事上 <b>GitNexus 已经把 Graphify 吊打了</b></p>
</div>
<h2 id="recommendation">九、最终建议</h2>
<div class="card card-bad">
<h4>不要替换</h4>
<p>GitNexus 在代码静态分析维度领先 Graphify 一个量级:真类型系统、MRO、框架识别、入口打分、完整 resolver —— 这些 Graphify 全都没有。
替换过去你会失去跨项目 Web UI、看板集成、浏览器 WASM 零服务器架构,换来的只是一套更弱的单项目 AST 抽取和几个你不需要的 Obsidian 模板。</p>
</div>
<div class="card card-good">
<h4>保持现状,按需并用</h4>
<p>GitNexus 继续作为<b>工作看板代码图谱主干</b>,Graphify 作为<b>偶尔跑一次的单项目深度报告工具</b>(需要 Karpathy 式 <code>/raw</code> 混合语料场景时)。两者并不冲突。</p>
</div>
<div class="card card-accent">
<h4>可以从 Graphify 借鉴的东西(选抄)</h4>
<ol>
<li><b>三档置信度 EXTRACTED/INFERRED/AMBIGUOUS + <code>confidence_score</code></b> —— 给 GitNexus 的边也加上,报告诚实度立即拉满</li>
<li><b>Markdown 剥 YAML frontmatter 再哈希</b>(<code>cache.py:10-17</code>)—— 细节体验</li>
<li><b>论文启发式识别 <code>_looks_like_paper</code></b>(<code>detect.py:68</code>)—— 13 条正则,简单有效</li>
<li><b>敏感文件过滤 <code>_SENSITIVE_PATTERNS</code></b>(<code>detect.py:33-40</code>)—— 安全默认值</li>
<li><b>SSRF 防护整套</b>(<code>security.py:14-90</code>)—— 如果 GitNexus 要加 URL 抓取功能直接移植</li>
<li><b>大社区 25% 拆分规则</b>(<code>cluster.py:93</code>)—— 防 Leiden 出一个超大社区</li>
<li><b><code>ingest.py</code> 的 URL 分类抓取</b>(297 行:twitter/arxiv/github/youtube/pdf/image/web)—— 给 GitNexus 补非代码输入时有现成逻辑</li>
</ol>
</div>
<div class="card">
<h4>下一步(如果你感兴趣)</h4>
<p>我可以继续深入看 GitNexus 的 <code>gitnexus/src/core/ingestion/pipeline.ts</code>
<code>type-extractors/</code>,给你出一份 "GitNexus 现状 + 可增强点" 的专门评估 —— 这份评估完成后,
决定要不要把上述 Graphify 的借鉴项真正落到 GitNexus 里。</p>
</div>
<footer>
<p>
解析日期 2026-04-13 ·
源码快照 <code>~/Projects/research/20260413-graphify-analysis/graphify/</code>(Graphify v0.4.8, commit <code>04e2960</code>) ·
对照项目 <code>~/Projects/code/20260319-gitnexus/</code> ·
本页由 Claude Code(Opus 4.6 1M)生成
</p>
</footer>
</main>
</div>
<script>
(function () {
const links = Array.from(document.querySelectorAll('nav.toc a[href^="#"]'));
const map = new Map();
links.forEach(a => {
const id = a.getAttribute('href').slice(1);
const target = document.getElementById(id);
if (target) map.set(target, a);
});
let activeLink = null;
const setActive = (link) => {
if (activeLink === link) return;
if (activeLink) activeLink.classList.remove('active');
if (link) {
link.classList.add('active');
// keep active item in view inside the sidebar scroll area
link.scrollIntoView({ block: 'nearest' });
}
activeLink = link;
};
// Scroll spy: pick the section whose top is closest above viewport's 30% line.
// IntersectionObserver alone is jumpy with long sections, so we listen to scroll
// and recompute manually.
const sections = Array.from(map.keys());
const onScroll = () => {
const probe = window.innerHeight * 0.3;
let current = sections[0];
for (const s of sections) {
const top = s.getBoundingClientRect().top;
if (top - probe <= 0) current = s;
else break;
}
setActive(map.get(current));
};
// Smooth scroll override (avoids :target jump that ignores scroll-margin on some browsers)
links.forEach(a => {
a.addEventListener('click', (e) => {
const id = a.getAttribute('href').slice(1);
const target = document.getElementById(id);
if (!target) return;
e.preventDefault();
window.scrollTo({ top: target.getBoundingClientRect().top + window.scrollY - 18, behavior: 'smooth' });
history.replaceState(null, '', '#' + id);
});
});
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
onScroll();
})();
</script>
</body>
</html>