graphify 增量更新与簇重建实战:--update / --cluster-only 参考手册深度解析

发布时间:2026/9/7 5:41:36
graphify 增量更新与簇重建实战:--update / --cluster-only 参考手册深度解析 graphify 增量更新与簇重建实战--update / --cluster-only 参考手册深度解析【免费下载链接】graphifyTurn any codebase, with its docs, SQL schemas, configs, and PDFs, into a queryable knowledge graph. A /graphify skill for Claude Code, Cursor, Codex, and Gemini CLI: local deterministic AST parsing, every edge explained, no vector store.项目地址: https://gitcode.com/GitHub_Trending/graph/graphify本文围绕 graphify 的 增量更新参考手册 展开完整覆盖--update增量重抽取与--cluster-only仅重新聚类两条运行路径从变更检测detect_incremental、变更子集检测文件填充、code-only 快速路径、删除修剪与build_merge合并到 manifest 持久化与graph_diff图差异展示并深入 graphify/detect.py、graphify/build.py 等源码印证每个参数背后的实现动机。读完本文你可以理解 graphify 如何只重抽变过的文件、绝不把旧节点留在图里并掌握在 Agent 技能运行手册中正确执行增量更新全流程的方法。参考手册的定位只在增量路径加载这份 reference 文件是 graphify 面向 Claude Code 等 Agent 平台提供的技能参考文档graphify/skills/claude/references/update.md原文明确规定Load this only when the user passed--updateor--cluster-only. A first-time full build never reads this file.即只有当用户传入--update或--cluster-only时才加载本文件首次全量构建永远不会读取它。这与同目录下的 transcribe.md视频转写子步骤、query.md查询等参考文档共同构成技能的按需加载体系。下文按原文两条主线分别展开。一、--update增量重抽取的完整流程增量更新适用于上一次运行之后新增或修改了文件的场景只重抽变更文件从而节省 token 和时间。整个流程分为四段变更检测 → 填充检测文件 → 分支判定code-only / 全量语义→ 合并、manifest 与图 diff。1. 变更检测detect_incremental第一步调用graphify.detect.detect_incremental对比上次运行以来的文件变化并把结果落盘到.graphify_incremental.json$(cat graphify-out/.graphify_python) -c import sys, json from graphify.detect import detect_incremental, save_manifest from pathlib import Path result detect_incremental(Path(INPUT_PATH)) new_total result.get(new_total, 0) print(json.dumps(result, indent2, ensure_asciiFalse)) Path(graphify-out/.graphify_incremental.json).write_text(json.dumps(result, ensure_asciiFalse), encoding\utf-8\) deleted list(result.get(deleted_files, [])) if new_total 0 and not deleted: print(No files changed since last run. Nothing to update.) raise SystemExit(0) if deleted: print(f{len(deleted)} deleted file(s) to prune.) if new_total 0: print(f{new_total} new/changed file(s) to re-extract.) INPUT_PATH是技能执行时由 Agent 填入的实际项目路径占位符。结果中new_total为新增/变更文件总数deleted_files是真正被删除的文件。若两者皆空直接退出——无事可做。源码印证mtime 快速路径 内容哈希慢路径从 graphify/detect.py 的detect_incremental实现看变更判定采用两级策略快速路径文件的 mtime 未变且与 manifest 记录的 mtime 一致则判定为未变更只做 stat不做磁盘 IO慢速路径mtime 被刷新后用 MD5 内容与 manifest 中对应哈希字段比对内容一致才算未变更。其中有一个值得注意的细节——mtime 精度窗口防护。源码定义了_MTIME_COARSE_S 2.0与_MTIME_SUBSECOND_S 0.05两个常量graphify/detect.py当 manifest 记录时间戳与文件 mtime 落在同一文件系统刻度内时粗粒度文件系统会把 mtime 舍入到整秒一次同长度编辑可能不会移动 mtime导致文件被静默跳过。此时_mtime_may_hide_a_rewrite会强制走一次内容哈希校验避免图持续提供旧内容。这个窗口机制与 graphify/cache.py 哈希缓存层的假设保持一致并有专门的回归测试 tests/test_incremental_mtime_collision.py。另一个实现细节是双哈希 schemamanifest 每个文件行同时保存ast_hash与semantic_hash并通过kind参数区分语义——kindast服务于graphify updateAST-only 重建kindsemantic服务于graphify extract。缺少semantic_hash的文件比如只跑过 AST 更新的文档在语义检测中始终视为需重抽。此外删除文件与被排除文件被严格区分磁盘上文件已不存在才计入deleted_files文件仍在磁盘但被.graphifyignore/.gitignore/--exclude排除的计入excluded_files不会误报为删除见 tests/test_detect.py 中对test_detect_incremental_*系列用例的覆盖包括 mtime 回退、schema 漂移等边界场景。2. 填充 .graphify_detect.json让后续步骤看到正确的状态检测到变更子集后需要重写.graphify_detect.json使无条件读取该文件的 Step 3A–6 看到增量运行应有的状态。原文的关键说明是files携带变更子集——驱动 Step 3A 的 AST 抽取以及只对变更文件做 Step 3B0 缓存检查all_files携带全量语料——供任何需要全库上下文的步骤使用。$(cat graphify-out/.graphify_python) -c import json from pathlib import Path r json.loads(Path(graphify-out/.graphify_incremental.json).read_text(encoding\utf-8\)) Path(graphify-out/.graphify_detect.json).write_text(json.dumps({ files: r.get(new_files, {}), all_files: r.get(files, {}), total_files: r.get(new_total, 0), total_words: r.get(total_words, 0), skipped_sensitive: r.get(skipped_sensitive, []), needs_graph: True, }, ensure_asciiFalse), encoding\utf-8\) 字段对应关系值得注意new_files变更子集映射为files而detect_incremental返回的完整扫描结果files全量语料映射为all_files。3. 分支判定code-only 快速路径 vs 完整语义管线若存在新文件先判断所有变更文件是否都是代码文件$(cat graphify-out/.graphify_python) -c import json from pathlib import Path result json.loads(open(graphify-out/.graphify_incremental.json, encodingutf-8).read()) if Path(graphify-out/.graphify_incremental.json).exists() else {} code_exts {.py,.ts,.js,.go,.rs,.java,.cpp,.c,.rb,.swift,.kt,.cs,.scala,.php,.cc,.cxx,.hpp,.h,.kts,.lua,.toc,.f,.F,.f90,.F90,.f95,.F95,.f03,.F03,.f08,.F08} new_files result.get(new_files, {}) all_changed [f for files in new_files.values() for f in files] code_only all(Path(f).suffix.lower() in code_exts for f in all_changed) print(code_only:, code_only) 原文据此给出两条分支code_only True打印[graphify update] Code-only changes detected - skipping semantic extraction (no LLM needed)只对变更文件执行 Step 3AAST 确定性解析完全跳过 Step 3BLLM 语义子代理直接进入合并与 Steps 4–8。这与项目本地确定性 AST 解析、无向量库的核心设计一脉相承——代码结构图不消耗 LLM token。code_only False任一变更文件是文档/论文/图片/视频若变更文件中有new_files[video]先按 transcribe.mdStep 2.5对视频转写然后重写.graphify_detect.json把转写产出路径移入files[document]、删除files[video]——否则裸的.mp4/.mp3路径会被当作文本喂给语义子代理导致不可读媒体进入抽取原文标注对应 issue #1392。之后按正常流程执行完整 Steps 3A–3C。与 detect.py 扩展名分类的对照这份code_exts集合是运行手册内的自包含判定。对照 graphify/detect.py 的官方分类CODE_EXTENSIONS覆盖面更广还包括.vue、.svelte、.zig、.sql、.terraform系、PowerShell、C# 工程文件等并额外区分DOC_EXTENSIONS.md/.mdx/.rst等、PAPER_EXTENSIONS.pdf、IMAGE_EXTENSIONS与VIDEO_EXTENSIONS含.mp3/.wav等音频。可以推断参考手册中的精简集合是刻意为 Agent 内联脚本保持轻量——FileType枚举code/document/paper/image/video才是分类权威内联集合只服务于是否需要 LLM的二元判定。4. 仅有删除构造空抽取以供合并修剪若没有新/变更文件、只有删除需要创建空抽取文件让合并步骤据此做节点修剪if [ ! -f graphify-out/.graphify_extract.json ]; then echo [graphify update] Only deletions -- creating empty extraction for merge. $(cat graphify-out/.graphify_python) -c import json from pathlib import Path Path(graphify-out/.graphify_extract.json).write_text(json.dumps({nodes:[],edges:[],hyperedges:[],input_tokens:0,output_tokens:0}), encodingutf-8) fi5. 合并build_merge 的四个关键参数合并阶段是整个增量流程的核心原文以详尽注释给出了调用与理由$(cat graphify-out/.graphify_python) -c import json from pathlib import Path from graphify.build import build_merge from graphify.detect import save_manifest new_extraction json.loads(Path(graphify-out/.graphify_extract.json).read_text(encoding\utf-8\)) incremental json.loads(Path(graphify-out/.graphify_incremental.json).read_text(encoding\utf-8\)) deleted list(incremental.get(deleted_files, [])) prune list(deleted) or None G build_merge( [new_extraction], graph_pathgraphify-out/graph.json, prune_sourcesprune, rootINPUT_PATH, directedIS_DIRECTED, ) print(f[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges) merged_out { nodes: [{id: n, **d} for n, d in G.nodes(dataTrue)], edges: [ {**{k: val for k, val in d.items() if k not in (_src, _tgt, source, target)}, source: d.get(_src, u), target: d.get(_tgt, v)} for u, v, d in G.edges(dataTrue) ], hyperedges: list(G.graph.get(hyperedges, [])), input_tokens: new_extraction.get(input_tokens, 0), output_tokens: new_extraction.get(output_tokens, 0), } Path(graphify-out/.graphify_extract.json).write_text(json.dumps(merged_out, ensure_asciiFalse), encoding\utf-8\) print(f[graphify update] Merged extraction written ({len(merged_out[\nodes\])} nodes, {len(merged_out[\edges\])} edges)) from graphify.cli import _stamped_manifest_files _manifest_files _stamped_manifest_files(incremental[files], new_extraction, Path(INPUT_PATH)) _sem_types (document, paper, image) _dispatched {f for t, fl in incremental.get(new_files, {}).items() if t in _sem_types for f in fl} _stamped {f for fl in _manifest_files.values() for f in fl} _cleared _dispatched - _stamped _scan {f for fl in incremental[files].values() for f in fl} save_manifest(_manifest_files, rootINPUT_PATH, scan_corpus_scan, clear_semantic_cleared or None) print([graphify update] Manifest saved.) 原文为build_merge给出的每条参数理由IS_DIRECTED同样为 Agent 按--directed开关替换的占位符参数原文理由含 issue 溯源源码印证prune_sourcesprune只用于真正被删除的文件。变更/重抽文件由build_merge的 replace-on-re-extract 机制处理#1344new_chunks中每个source_file在合并前从基图中剔除旧节点不会残留。切勿把changed加入 prune当传了root时prune 集合会与刚合并节点相对化到同一基准反而删掉刚重抽的内容graphify/build.py 注释明确Re-extracted files REPLACE their prior contribution且替换是**按层tier-scoped**的AST 层与语义层各自独立替换重抽一层不会抹掉另一层的节点rootINPUT_PATH让detect_incremental返回的绝对路径 prune 集合相对化与图中相对source_file匹配缺省则什么都剪不掉每次更新都累积幽灵节点#1361/#1571build_merge内部_eff_root逻辑未传root时会回退推断扫描根避免绝对 win32 路径与相对 posix 键不匹配#1007directedIS_DIRECTED必须显式传入否则--directed --update会静默重建为无向图把互反的 A↔B 边坍缩#1392directedNone时build_merge继承磁盘上图自身的directed标记#2342显式值总是覆盖graph_path只读直接读graph.json无 NetworkX 往返边方向calls/implements/imports恒保真#801docstring 明确 Does NOT write to disk — the caller persists the result合并后G.graph[hyperedges]同时包含旧图与新抽取的超边原文特别警告只回退到new_extraction会静默丢弃前次运行的超边#801——上表代码中hyperedges: list(G.graph.get(hyperedges, []))正是对应处理。边序列化时显式把source/target放在最后使其优先于d中任何陈旧属性。6. manifest 持久化让下一次 --update 对比今天的状态合并后必须保存 manifest否则下次--update仍会对比上次的基线产生幽灵节点报告。原文还解释了三个精细参数对应 graphify/detect.py 中save_manifest的 docstringrootINPUT_PATHmanifest 键相对化到扫描根跨 clone/机器可移植。不传的话目录一移动--update就会对每个缓存文件全部 miss#1417/#777。只盖章真正产出输出的语义文件_stamped_manifest_filesgraphify/cli.py只对本次实际产出了节点/超边的文档盖章某个 chunk 抽取失败的文件保持未盖章下次--update自动重新排队——否则它会被标记完成、内容永久丢失#2015/#933。clear_semantic本次被派发但未盖章的文件chunk 失败或被 LLM 遗漏清除其陈旧semantic_hash强制重排#1948。scan_corpus传入原始全量语料使自上次运行以来新被排除的根内文件被正常丢弃而不是伪装成删除未触碰的行保留#1908。7. 图差异展示graph_diffSteps 4–8 在合并后的完整图上照常执行。Step 4 之后用更新前的备份图做差异对比$(cat graphify-out/.graphify_python) -c import json from graphify.analyze import graph_diff from graphify.build import build_from_json from networkx.readwrite import json_graph import networkx as nx from pathlib import Path old_data json.loads(Path(graphify-out/.graphify_old.json).read_text(encoding\utf-8\)) if Path(graphify-out/.graphify_old.json).exists() else None new_extract json.loads(Path(graphify-out/.graphify_extract.json).read_text(encoding\utf-8\)) G_new build_from_json(new_extract, directedIS_DIRECTED) if old_data: G_old json_graph.node_link_graph(old_data, edgeslinks) diff graph_diff(G_old, G_new) print(diff[summary]) if diff[new_nodes]: print(New nodes:, , .join(n[label] for n in diff[new_nodes][:5])) if diff[new_edges]: print(New edges:, len(diff[new_edges])) 配套操作原文原样保留合并步骤之前cp graphify-out/graph.json graphify-out/.graphify_old.json结束后清理rm -f graphify-out/.graphify_old.jsongraph_diffgraphify/analyze.py返回new_nodes、removed_nodes、new_edges、removed_edges与一行summary形如 3 new nodes, 5 new edges, 1 node removed。从源码看无向图的边按(min(u,v), max(u,v), relation)归一化有向图按(u, v, relation)区分保证互反边不会被误判为增删——这与--directed必须显式传参的要求形成闭环。二、--cluster-only对现有图单独重跑聚类原文对该模式的说明极其明确Skip Steps 1–3. Re-run clustering on the existing graph:graphify cluster-only .graphify cluster-only .是自包含的重新聚类、命名社区并从现有图重新生成GRAPH_REPORT.md、graph.json与graph.html。切勿重跑 Steps 5–9——这些步骤读取的中间文件.graphify_extract.json、.graphify_detect.json、.graphify_analysis.json已在此前构建的清理步骤Step 9中被删除重跑会抛FileNotFoundError#1392。完成后照常呈现刷新后的GRAPH_REPORT.md摘要。从 graphify/cli.py 的cluster-only分支可印证该命令的实际能力面与label子命令共用代码路径label是总是重新生成社区名的 cluster-only支持--no-viz、--no-label、--missing-only、--timing、--backend、--model、--batch-size、--min-community-size默认 3等标志重聚类后会用remap_communities_to_previous将新社区 ID 映射回上一次标注#1027避免标签因原始 cid 索引变化而错位token 用量来自真实的标注 LLM 调用而非硬编码零值#1694--no-label的纯占位标签不会被持久化复用#2073。适用场景调整了社区命名后端/模型、想刷新报告与可视化但不想重新支付抽取成本时cluster-only是最小代价路径。三、设计要点小结综合参考手册与源码graphify 增量更新的设计可以归纳为五条不变量每条都有对应的源码与测试证据只重抽变更文件mtime 快速路径 内容哈希慢路径 双哈希ast_hash/semantic_hash区分 AST 与语义两个抽取层graphify/detect.py测试见 tests/test_detect.py、tests/test_incremental_mtime_collision.py变更文件替换而非追加build_merge按层替换重抽文件的既有贡献删除文件走prune_sources二者机制严格分离graphify/build.py测试见 tests/test_build_merge_shrink_guard.py、tests/test_prune_sweeps_orphans.py失败不盖章未产出输出的文件永远不写入 manifest 哈希下次自动重排队graphify/cli.py路径可移植manifest 与图键均相对化到扫描根跨机器/跨 clone 依然命中缓存#777/#1417方向性不静默丢失directed必须显式传入合并调用图自身的 directed 标记只作兜底#1392/#2342。此外仓库根目录下的 docs/superpowers/specs/2026-05-04-incremental-updates-dedup-design.md 与 docs/superpowers/plans/2026-05-04-incremental-updates-dedup.md 记录了增量更新与去重合并的设计演进可作为进一步深入阅读的材料。需要说明的是文中所有$(cat graphify-out/.graphify_python)调用都假定graphify-out/中已由首次构建写入解释器路径标记且INPUT_PATH、IS_DIRECTED等占位符由 Agent 在执行时按实际上下文替换——脱离首次构建直接运行这些片段不会成立这是使用本参考手册的前提限制。【免费下载链接】graphifyTurn any codebase, with its docs, SQL schemas, configs, and PDFs, into a queryable knowledge graph. A /graphify skill for Claude Code, Cursor, Codex, and Gemini CLI: local deterministic AST parsing, every edge explained, no vector store.项目地址: https://gitcode.com/GitHub_Trending/graph/graphify创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考