首页
/ graphify 增量更新(--update)与仅聚类重跑(--cluster-only)完整实操指南

graphify 增量更新(--update)与仅聚类重跑(--cluster-only)完整实操指南

2026-09-07 20:05:46作者:温艾琴Wonderful

导读:本文围绕 graphify 为 Kilo 等 Agent 提供的技能参考文档 update.md 展开,讲解在已建立过知识图谱后,如何通过 --update 仅对新增/变更文件做增量重抽取(省 token、省时间),以及如何用 --cluster-only 在既有图谱上重新聚类并刷新产出物。读完本文,你将掌握完整的增量合并链路(detect → 代码态快路径 → 语义重抽取 → build_merge 合并 → manifest 盖章 → 图谱 diff 汇报),并理解 prune_sourcesreplace-on-re-extractroot 相对化、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.mdgraph.jsongraph.html

update.md 在仓库中面向多种 Agent 平台各自维护了一份副本,本文引用的是 graphify/skills/kilo/references/update.md,其姊妹文档(extraction-spec.mdtranscribe.mdquery.mdadd-watch.md 等)位于同一目录,可互相参照。


二、--update 的整体执行流程

增量更新的核心思路是:只处理「自上次运行以来真的变了」的文件,其余文件复用上次抽取结果,从而节省 token 与时间。整体分四段:

  1. 探测变更:调用 detect_incremental,得到新增/变更文件、被删除文件清单,并把结果落盘到 .graphify_incremental.json
  2. 回填状态文件:把增量结果改写成 .graphify_detect.json,让后续固定读取它的步骤(3A–6)看到「增量运行」的正确状态;
  3. 按变更类别分流:纯代码变更走 AST-only 快路径(不调 LLM);含文档/论文/图片/视频的变更走完整 3A–3C 语义管线;仅删除则生成空 extraction 供合并剪枝;
  4. 合并与收尾:用 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:按类型分组(如 codedocumentpaperimagevideo)的新增/变更文件;
  • 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\")
"

这里两个字段有明确分工:

  • filesnew_files变更子集):驱动步骤 3A 的 AST 抽取和步骤 3B0 的缓存检查,只处理变化的部分;
  • all_files 取完整的 files全量语料):供任何需要语料级上下文的步骤使用。

其余字段沿用增量结果的 total_wordsskipped_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 Noneprune_sources 只能放「磁盘上确实被删除」的文件。变更/被重抽取的文件由 build_mergereplace-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,从而保证边方向(callsimplementsimports 等 source→target)永远被保留(#801)。从 build.py 的 build_merge 签名看,它接收 new_chunksgraph_pathprune_sources,以及关键字参数 directeddedupdedup_llm_backendroot

  • 只读语义:它本身不写盘,返回合并后的 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);即便调用方不传 rootbuild_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.jsonnew_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_oldjson_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.mdgraph.jsongraph.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.pytests/test_incremental_mtime_collision.py
  • 被排除文件不误报为删除:tests/test_stale_prune.pytests/test_ignore_file_encoding.py
  • 删除文件的剪枝与孤儿清扫:tests/test_prune_sweeps_orphans.py
  • 重抽取替换(超边/变更文件去重):tests/test_dedup_remaps_hyperedges.pytests/test_carried_hyperedge_remap.pytests/test_build_merge_shrink_guard.pytests/test_build_merge_hyperedges_and_prune.py
  • 有向图方向保留与构建:tests/test_build.pytests/test_hypergraph.py
  • AST-only 与语义盖章的差异、失败重排队:tests/test_incomplete_build_guard.pytests/test_partial_extraction_warning.pytests/test_zero_node_no_cache.py
  • manifest 可移植与清除语义:tests/test_semantic_cleanup.pytests/test_semantic_id_remap_root.pytests/test_semantic_cache_out_root.py

若需对照完整调用方式,可阅读同一参考目录下的 extraction-spec.md(抽取产物 schema 与 source_file 规则)与 update.md(本文骨架),并在 detect.pybuild.pycli.pyanalyze.py 中追踪底层实现。


总结:增量更新的正确姿势

一次成功的 --update 只需守住五条纪律:

  1. 探测先行detect_incremental 判定无变化即退出,有变化则把 new_files/deleted_files 落盘;
  2. 状态回填:用增量结果重写 .graphify_detect.jsonfiles 变更子集 + all_files 全量语料);
  3. 按类型分流:纯代码走 AST-only 快路径(省 LLM),文档/图片/视频走完整语义管线且视频先转录,纯删除则用空 extraction 触发剪枝;
  4. 正确合并prune_sources 只放真删除文件,变更文件靠 replace-on-re-extract 按层替换,同时传 root= 相对化、显式传 directed= 防止方向丢失(--directed --update 时务必为 True);
  5. 盖章收尾:只给真正产出语义输出的文件盖章,失败项清空 hash 留待重跑,manifest 相对化存储以保证下次 diff 正确。

--cluster-only 则是一条完全独立的轻量路径:信任它是自包含的、只消费 graph.json,绝不回头去读已被清理的中间文件。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.79 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
390