graphify 增量更新(--update)与仅聚类重跑(--cluster-only)完整实操指南
导读:本文围绕 graphify 为 Kilo 等 Agent 提供的技能参考文档 update.md 展开,讲解在已建立过知识图谱后,如何通过
--update仅对新增/变更文件做增量重抽取(省 token、省时间),以及如何用--cluster-only在既有图谱上重新聚类并刷新产出物。读完本文,你将掌握完整的增量合并链路(detect → 代码态快路径 → 语义重抽取 →build_merge合并 → manifest 盖章 → 图谱 diff 汇报),并理解prune_sources、replace-on-re-extract、root相对化、directed方向保留等关键参数背后的源码实现,可直接驱动 Agent 按步骤完成一次零缺陷的增量更新。
一、这份参考文档的定位与加载时机
在 graphify 的技能体系中,update.md 是一份按需加载的 runbook 参考:
- 只有当用户显式传入了
--update或--cluster-only时才需要读取它; - 首次全量构建(full build)永远不需要读这份文件——首次构建走的是完整管线(见 skill-kilo.md 中
/graphify <path>的使用方式)。
两条入口命令对应的能力截然不同:
| 命令形态 | 语义 | 行为 |
|---|---|---|
/graphify <path> --update |
增量重抽取 | 对比上次运行的 manifest,只对新加/修改的文件做 re-extraction,再合并回现有 graph.json |
/graphify <path> --cluster-only |
仅重跑聚类 | 跳过抽取步骤 1–3,在既有图谱上重新做社区发现与命名,刷新 GRAPH_REPORT.md、graph.json、graph.html |
update.md 在仓库中面向多种 Agent 平台各自维护了一份副本,本文引用的是 graphify/skills/kilo/references/update.md,其姊妹文档(extraction-spec.md、transcribe.md、query.md、add-watch.md 等)位于同一目录,可互相参照。
二、--update 的整体执行流程
增量更新的核心思路是:只处理「自上次运行以来真的变了」的文件,其余文件复用上次抽取结果,从而节省 token 与时间。整体分四段:
- 探测变更:调用
detect_incremental,得到新增/变更文件、被删除文件清单,并把结果落盘到.graphify_incremental.json; - 回填状态文件:把增量结果改写成
.graphify_detect.json,让后续固定读取它的步骤(3A–6)看到「增量运行」的正确状态; - 按变更类别分流:纯代码变更走 AST-only 快路径(不调 LLM);含文档/论文/图片/视频的变更走完整 3A–3C 语义管线;仅删除则生成空 extraction 供合并剪枝;
- 合并与收尾:用
build_merge将新抽取结果合入graph.json,写回.graphify_extract.json,再保存 manifest,最后执行步骤 4–8 并在步骤 4 之后向用户展示图谱 diff。
2.1 第一步:detect_incremental 探测并落盘
$(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, indent=2, ensure_ascii=False))
Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), 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.')
"
其中的 graphify-out/.graphify_python 保存着首次全量构建时解析出的可用 Python 解释器路径($(cat ...) 将其取出后执行内联 Python),INPUT_PATH 则是本次构建的扫描根目录。
结果字段语义(对应 detect.py 的 detect_incremental 返回值):
new_files:按类型分组(如code、document、paper、image、video)的新增/变更文件;files:本次扫描的完整语料(全量);new_total为新增/变更文件总数;unchanged_files:未变化文件;deleted_files:已从磁盘消失、上次入库但本次扫描已不在语料中的文件;excluded_files:仍在磁盘上、但因 ignore 规则或--exclude退出扫描的文件(不会被误报为删除,#1908);incremental:标记本次是否为增量运行。
从源码看,detect_incremental(root, ...) 内部先调用全量 detect(),再加载上一次的 manifest(manifest.json,由 save_manifest 写入 mtime + 内容 hash)。判定的核心逻辑是:
- 快路径:mtime 未变且 hash 匹配 → 未变化,零磁盘 IO;
- 慢路径:mtime 变化 → 用 MD5 内容哈希确认后再决定是否重抽取;
- manifest 缺失:视为首次运行,全部文件进入
new_files; - 对
{mtime, hash}旧格式与纯 mtime 格式做了向后兼容;mtime 倒拨(git checkout 旧 commit、rsync--times等)也会触发重抽取,避免图谱与磁盘漂移(#1859); - 还处理了文件系统时间戳粗粒度场景(整秒 mtime)下的同秒覆盖写竞态(详见 test_incremental_mtime_collision.py 的覆盖场景)。
若既无新增又无删除,脚本直接 SystemExit(0) 结束,后续什么都不用做。
2.2 第二步:回填 .graphify_detect.json
管线中步骤 3A–6 会无条件读取 .graphify_detect.json,因此在增量运行前必须把它改写成正确的状态:
$(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_ascii=False), encoding=\"utf-8\")
"
这里两个字段有明确分工:
files取new_files(变更子集):驱动步骤 3A 的 AST 抽取和步骤 3B0 的缓存检查,只处理变化的部分;all_files取完整的files(全量语料):供任何需要语料级上下文的步骤使用。
其余字段沿用增量结果的 total_words、skipped_sensitive,并显式把 needs_graph 置为 True。
三、变更类别分流:快路径 vs 完整语义管线
有了增量清单后,先判断「变更的文件是否全部是代码文件」:
$(cat graphify-out/.graphify_python) -c "
import json
from pathlib import Path
result = json.loads(open('graphify-out/.graphify_incremental.json', encoding='utf-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_exts 覆盖了 graphify AST 抽取器支持的主流语言扩展名(.py/.ts/.go/.rs/.java/.cpp/.swift/.kt/.cs/.scala/.php 等,含 Fortran 的大小写变体 .f90/.F90 等)。
3.1 分支一:code_only 为 True —— AST-only 快路径
如果所有变更文件都是代码文件,则打印:
[graphify update] Code-only changes detected - skipping semantic extraction (no LLM needed)
随后只对变更文件执行步骤 3A(确定性 AST 抽取),完全跳过步骤 3B(语义子代理/LLM),直接进入合并与步骤 4–8。因为代码实体的抽取是确定性的、不需要 LLM,这一分支在 --watch(见 add-watch.md)与 git hook(见 hooks.md)等自动化场景中同样被复用:代码变化即时重建,无需人工干预。
3.2 分支二:code_only 为 False —— 走完整 3A–3C 管线
只要变更文件里含文档/论文/图片/视频,就必须走完整语义管线。此时有一个关键前置处理:若变更文件里存在 new_files['video'](视频/音频类),需先按 transcribe.md(即步骤 2.5)对这些文件做转录,然后重写 .graphify_detect.json,把转录生成的文本路径并入 files['document'],并移除 files['video']——否则裸的 .mp4/.mp3 路径会被直接交给语义子代理当作不可读媒体,导致抽取失败或产出脏数据(#1392)。
这一步对应 detect.py 中定义的扩展名集合:VIDEO_EXTENSIONS = {'.mp4', '.mov', '.webm', '.mkv', '.avi', '.m4v', '.mp3', '.wav', '.m4a', '.ogg'},而 document/paper/image/video 则对应文件类型枚举(见 detect.py 类型常量)。完整转录规则可参考 transcribe.md。
3.3 分支三:无新增(仅删除)—— 生成空 extraction
如果只有删除、没有新增文件,则需创建一个空的 extraction,让合并步骤能够据此完成剪枝:
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}), encoding='utf-8')
"
fi
空 extraction 的结构为 {nodes:[], edges:[], hyperedges:[], input_tokens:0, output_tokens:0},合并时它不贡献任何节点,仅作为「本次没有任何新增内容」的信号,让 build_merge 只执行 deleted_files 的剪枝。
四、核心合并步骤:build_merge 与 manifest 盖章
这是整个增量更新最关键的一步,文档给出的脚本较长,下面逐段解析:
$(cat graphify-out/.graphify_python) -c "
import json
from pathlib import Path
from graphify.build import build_merge
from graphify.detect import save_manifest
# Load new extraction and incremental state
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_sources is ONLY for genuinely DELETED files. Changed/re-extracted files are
# handled by build_merge's replace-on-re-extract (#1344): every source_file in
# new_chunks is dropped from the base before merge, so old/stale nodes don't survive.
# Do NOT add `changed` here: with root= passed, prune_set relativizes to the same base
# as the freshly merged nodes and would DELETE the re-extracted content (#1178 is moot
# now that replace — not the dedup pass — reconciles changed files).
prune = list(deleted) or None
# Use build_merge() — reads graph.json directly without NetworkX round-trip
# so edge direction (calls, implements, imports) is always preserved (#801).
# Pass root= so prune_sources (absolute paths from detect_incremental) are
# relativized to match the graph's relative source_file values; without it
# nothing is pruned and stale nodes accumulate on every update (#1361).
# directed=IS_DIRECTED: replace IS_DIRECTED with True if --directed was given, else
# False. Without it a --directed --update silently rebuilds undirected and collapses
# reciprocal A<->B edges (#1392).
G = build_merge(
[new_extraction],
graph_path='graphify-out/graph.json',
prune_sources=prune,
root='INPUT_PATH',
directed=IS_DIRECTED,
)
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
# Write merged result back to .graphify_extract.json so Step 4 sees the full graph
merged_out = {
'nodes': [{'id': n, **d} for n, d in G.nodes(data=True)],
'edges': [
# Explicit source/target last so they win over any stale attrs in d.
{**{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(data=True)
],
# G.graph["hyperedges"] holds hyperedges from both existing graph.json
# and new_extraction (build_merge combines them). Falling back to
# new_extraction only would silently drop prior-run hyperedges (#801).
'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_ascii=False), encoding=\"utf-8\")
print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"])} nodes, {len(merged_out[\"edges\"])} edges)')
# Save manifest so next --update diffs against today's state, not the
# prior run's baseline (prevents ghost-node reports on subsequent updates).
# root= matches the build_merge call above so the manifest keys stay relative to
# the scan root — portable across clones/machines, so --update keeps matching
# cached files instead of missing every one after a move (#1417).
#
# Only stamp semantic files (docs/papers/images) that ACTUALLY produced output
# THIS run (new_extraction is this run's fresh extraction, read above before the
# merge overwrote the file): a changed doc whose chunk failed must stay unstamped
# so the next --update re-queues it, otherwise it is marked done and its content
# is lost forever (#2015). Mirrors the library extract path
# (cli._stamped_manifest_files + clear_semantic + scan_corpus).
from graphify.cli import _stamped_manifest_files
_manifest_files = _stamped_manifest_files(incremental['files'], new_extraction, Path('INPUT_PATH'))
# Changed semantic files dispatched this run but NOT stamped had their chunk fail
# or be omitted; clear any stale semantic_hash so they are re-queued (#1948).
_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_corpus = the RAW full corpus so in-root files newly excluded since last run
# are dropped rather than masquerading as deletions; untouched rows preserved (#1908).
_scan = {f for fl in incremental['files'].values() for f in fl}
save_manifest(_manifest_files, root='INPUT_PATH', scan_corpus=_scan, clear_semantic=_cleared or None)
print('[graphify update] Manifest saved.')
"
4.1 prune_sources 只服务于真正删除的文件
prune = list(deleted) or None:prune_sources 只能放「磁盘上确实被删除」的文件。变更/被重抽取的文件由 build_merge 的 replace-on-re-extract 机制兜底——每个出现在 new_chunks 中的 source_file 都会先从存量图中剔除再合并,因此旧/过期节点不会残留(#1344)。
特别注意注释中的警告:不要把 changed 加进 prune 集合。因为在传入 root= 时,prune 集合会以与「新合并节点」相同的根做相对化;若把变更文件也放进去,会把刚重抽取的内容一并删掉(这解释了为何 #1178 的旧逻辑如今已失效——现在由 replace 机制、而非 dedup 阶段来调和变更文件)。
4.2 为什么用 build_merge 而不是先过 NetworkX
build_merge 直接读取 graph.json,不做 NetworkX round-trip,从而保证边方向(calls、implements、imports 等 source→target)永远被保留(#801)。从 build.py 的 build_merge 签名看,它接收 new_chunks、graph_path、prune_sources,以及关键字参数 directed、dedup、dedup_llm_backend、root:
- 只读语义:它本身不写盘,返回合并后的 NetworkX 图,由调用方持久化;
- replace-on-re-extract:针对每个 tier(AST 层 vs 语义层,按节点是否属于 AST tier 区分,见
_is_ast_tier)分别剔除重抽取文件在存量图中的旧节点/旧边——按层替换,一个文件被单层重抽取时不会误删另一层的贡献(#2333/#2336 的 COEXIST 语义);未在new_chunks出现的文件原样保留;真正删除的文件经prune_sources(不分层)移除; root参数:把new_chunks中的绝对source_file相对化(#932);即便调用方不传root,build_merge也会回退到从既有图推断的扫描根_infer_merge_root,确保绝对路径的 prune 与相对化的节点 key 能对上(#1571);directed语义:directed=None(默认)时继承磁盘上既有图自身的directed标记,防止一次增量合并悄悄把有向图翻转成无向(#2342);显式传True/False才覆盖。本文 runbook 要求显式替换:IS_DIRECTED在用户给了--directed时替换为True,否则False——否则--directed --update会默默重建无向图并把互指的 A↔B 边坍缩掉(#1392)。
4.3 写回 .graphify_extract.json:完整图供步骤 4 使用
合并结果写回 .graphify_extract.json,使步骤 4 看到的是一张完整图而不是仅本次增量片段。写回时有三个细节值得注意:
- 节点序列化保留全部属性(
{'id': n, **d}); - 边的
source/target被显式放到属性字典最后,用d.get('_src', u)等取值,确保其优先于陈旧属性,同时剔除内部使用的_src/_tgt/source/target键避免污染; hyperedges取自G.graph.get('hyperedges', [])而非new_extraction——因为build_merge会把既有graph.json与new_extraction的超边合并进图的graph属性,若只回退到new_extraction会静默丢弃历史运行产生的超边(#801)。
4.4 manifest 盖章:防止幽灵节点与重复排队
合并后必须保存 manifest,使下一次 --update 对比的是「今天的基线」而非「上次运行前的基线」,避免后续更新反复误报已删除的幽灵节点。关键在于:
root='INPUT_PATH'与上面的build_merge保持一致,使 manifest 的 key 相对扫描根存储,从而跨克隆/跨机器可移植(#1417),--update不会在仓库被移动后因路径不匹配而全部 miss;_stamped_manifest_files(incremental['files'], new_extraction, Path('INPUT_PATH')):只对本次确实产出了输出的语义文件(docs/papers/images)盖章。从 cli.py 的实现看,节点与超边都算有效输出、edge-only 结果不算(#2927),且文件必须既被抽取又非 partial(被截断)才会盖章;某个变更文档的 chunk 失败时保持未盖章,下一次--update才会重新排队它,否则会被标记为已完成、其内容永久丢失(#933/#2015);clear_semantic(#1948):_dispatched - _stamped得到「本次已分发但未盖章」的语义文件,把它们的旧semantic_hash清空,使它们在下次运行时被重新排队,避免陈旧 hash 掩盖本次失败;scan_corpus(#1908):传入的是incremental['files']展开的原始全量语料(绝对路径,非盖章后的子集)——这样自上次运行起因 ignore 规则/--exclude新被排除的文件会被正确丢弃,而不是伪装成「删除」;同时未触碰的行会被保留(保存子集时该参数必须为None以保留未触碰行)。
结合 save_manifest 源码可看到,kind 参数区分 ast/semantic/both 三种盖章范围,并会把 key 相对化后以 POSIX 风格写入磁盘。
五、步骤 4 之后:旧图备份与图谱 diff 汇报
合并完成后,按正常流程继续执行步骤 4–8。在步骤 4 之后应向用户展示图谱变化摘要。脚本思路是:先在合并前备份旧图,再用 graph_diff 对比新旧两图:
$(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
# Load old graph (before update) from backup written before merge
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, directed=IS_DIRECTED)
if old_data:
G_old = json_graph.node_link_graph(old_data, edges='links')
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']))
"
配套操作两条:
# 在 merge 步骤之前保存旧图
cp graphify-out/graph.json graphify-out/.graphify_old.json
# 使用后清理
rm -f graphify-out/.graphify_old.json
new_extract 读的是上一节写回的全量 .graphify_extract.json,通过 build_from_json 重建出 G_new(传 directed=IS_DIRECTED 以保持方向一致);G_old 用 json_graph.node_link_graph 从备份还原。graph_diff 的定义见 analyze.py,其返回值结构为:
new_nodes:[{"id": ..., "label": ...}]形式的新增节点列表(脚本取前 5 个打印 label);removed_nodes:被移除的节点;new_edges:新增边(含source/target/relation/confidence);removed_edges:被移除的边;summary:一行人话摘要,例如"3 new nodes, 5 new edges, 1 node removed"。
注意有向图与无向图的边对比 key 不同:有向按 (u, v, relation),无向按 (min, max, relation) 归一(见 analyze.py edge_key),因此对比时两图的 directed 必须一致,这也是脚本里 IS_DIRECTED 需要前后保持相同的原因。
六、--cluster-only:只重跑聚类,别碰中间文件
当用户只想让既有图谱重新聚类、重新命名社区、刷新产出物(例如改了聚类参数或加入新模块后想重新划分社区),使用:
graphify cluster-only .
graphify cluster-only . 是自包含的:它基于现有 graph.json 直接重新聚类(community detection)、为社区命名,并重新生成 GRAPH_REPORT.md、graph.json、graph.html 三份产出物,不需要重新抽取任何文件。
文档给出的关键红线是:不要重跑步骤 5–9。原因很实际——步骤 5–9 会读取 .graphify_extract.json、.graphify_detect.json、.graphify_analysis.json 等中间产物文件,而一次完整构建的清理阶段(步骤 9)已经把它们删除;强行重跑会直接抛 FileNotFoundError(#1392)。也就是说,cluster-only 与「完整构建管线」是不可混用的两条路径:前者只消费最终的 graph.json。
命令结束后的汇报动作与全量构建一致:把刷新后的 GRAPH_REPORT.md 摘要呈现给用户即可(社区划分与命名结果的汇总都在其中)。
七、三者的边界与衔接
把 update.md 放在 graphify 的整体工作流中看,增量维护有几种互补的自动化入口:
| 入口 | 触发条件 | 覆盖范围 | 是否需要 LLM |
|---|---|---|---|
/graphify --update(本文核心) |
用户显式发起,或 add/watch 报告文档类变更 |
代码 AST 重抽取 + 语义重抽取 + 合并 + 重新聚类 | 语义文件需要,纯代码走 AST-only 快路径 |
/graphify --watch |
后台监视文件变化,防抖后自动触发 | 仅代码文件自动 AST 重建;文档类只写 needs_update 标记(见 add-watch.md) |
代码不需要;文档类需再跑 --update |
/graphify --cluster-only |
用户显式发起 | 仅重聚类 + 刷新产出物 | 不需要 |
| git post-commit hook | 每次 commit | 代码文件 AST 重建(见 hooks.md) | 代码不需要 |
几个值得注意的衔接点:
add-watch.md中保存新 URL 成功后会自动对./raw跑--update管线,把新文件并入既有图;--watch下 Agent 写入的代码变更会在 wave 之间被自动拾取,而文档/笔记变更仍需要一次手动的/graphify --update;- 转录步骤(transcribe.md / Step 2.5)与增量语义管线是串联关系:视频类变更必须先转录、再走语义抽取,见本文 3.2 节;
- 语义抽取产出也会被回写进图(query 的答案保存后,下一次
--update会把 Q&A 作为节点抽取进来,形成自改进闭环,见 query.md)。
八、仓库内的验证与测试佐证
增量更新与合并的每一处关键语义在仓库中都有对应测试覆盖,可作为深入源码时的路线图:
- 变更探测与 mtime 竞态:
tests/test_incremental.py、tests/test_incremental_mtime_collision.py; - 被排除文件不误报为删除:
tests/test_stale_prune.py、tests/test_ignore_file_encoding.py; - 删除文件的剪枝与孤儿清扫:
tests/test_prune_sweeps_orphans.py; - 重抽取替换(超边/变更文件去重):
tests/test_dedup_remaps_hyperedges.py、tests/test_carried_hyperedge_remap.py、tests/test_build_merge_shrink_guard.py、tests/test_build_merge_hyperedges_and_prune.py; - 有向图方向保留与构建:
tests/test_build.py、tests/test_hypergraph.py; - AST-only 与语义盖章的差异、失败重排队:
tests/test_incomplete_build_guard.py、tests/test_partial_extraction_warning.py、tests/test_zero_node_no_cache.py; - manifest 可移植与清除语义:
tests/test_semantic_cleanup.py、tests/test_semantic_id_remap_root.py、tests/test_semantic_cache_out_root.py。
若需对照完整调用方式,可阅读同一参考目录下的 extraction-spec.md(抽取产物 schema 与 source_file 规则)与 update.md(本文骨架),并在 detect.py、build.py、cli.py、analyze.py 中追踪底层实现。
总结:增量更新的正确姿势
一次成功的 --update 只需守住五条纪律:
- 探测先行:
detect_incremental判定无变化即退出,有变化则把new_files/deleted_files落盘; - 状态回填:用增量结果重写
.graphify_detect.json(files变更子集 +all_files全量语料); - 按类型分流:纯代码走 AST-only 快路径(省 LLM),文档/图片/视频走完整语义管线且视频先转录,纯删除则用空 extraction 触发剪枝;
- 正确合并:
prune_sources只放真删除文件,变更文件靠replace-on-re-extract按层替换,同时传root=相对化、显式传directed=防止方向丢失(--directed --update时务必为True); - 盖章收尾:只给真正产出语义输出的文件盖章,失败项清空 hash 留待重跑,manifest 相对化存储以保证下次 diff 正确。
而 --cluster-only 则是一条完全独立的轻量路径:信任它是自包含的、只消费 graph.json,绝不回头去读已被清理的中间文件。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00