mcpField
G
graphify

graphify

github.com/graphify-labs数据与数据库137.8k11.6k 次安装1 天前更新
代码仓库

关于 graphify

graphify 是一项技能,可将代码、文档、论文、图像或视频文件夹转换为可浏览的知识图谱。它生成交互式 HTML、适用于 GraphRAG 的 JSON 和通俗报告,并记录提取来源及社区结构。

主要功能

  1. 01构建可浏览的知识图谱
  2. 02生成交互式 HTML
  3. 03导出适用于 GraphRAG 的 JSON
  4. 04编写通俗易懂的报告
  5. 05标记提取与推断关系
  6. 06支持增量更新

适用场景

  1. 01探索项目文件间的联系
  2. 02准备 GraphRAG 数据
  3. 03文件变更后更新图谱
  4. 04检查提取与推断的关系

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/graphify-labs/graphify --skill graphify

安装后在 Claude Code 中输入/graphify确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

__init__.py
__main__.py
_minhash.py
affected.py
analyze.py
benchmark.py
build.py
cache.py
callflow_html.py
cargo_introspect.py
cli.py
cluster.py
command-kilo.md
cross_repo_calls.py
cross_repo_types.py
csharp_dispatch.py
dedup.py
detect.py
diagnostics.py
export.py
always_on/
agents-md.md
antigravity-rules.md
claude-md.md
gemini-md.md
kiro-steering.md
vscode-instructions.md
exporters/
__init__.py
base.py
graphdb.py
html.py

详细介绍

/graphify

将任意文件夹转换为可导航的知识图谱,具备社区检测、可靠的审计追踪,并生成三种输出:交互式 HTML、可用于 GraphRAG 的 JSON,以及通俗易懂的 GRAPH_REPORT.md。

用法

/graphify                                             # full pipeline on current directory (HTML viz; add --obsidian for a vault)
/graphify <path>                                      # full pipeline on specific path
/graphify https://github.com/<owner>/<repo>           # clone repo then run full pipeline on it
/graphify https://github.com/<owner>/<repo> --branch <branch>  # clone a specific branch
/graphify <url1> <url2> ...                           # clone multiple repos, build each, merge into one cross-repo graph
/graphify <path> --mode deep                          # thorough extraction, richer INFERRED edges
/graphify <path> --update                             # incremental - re-extract only new/changed files
/graphify <path> --directed                            # build directed graph (preserves edge direction: source→target)
/graphify <path> --whisper-model medium                # use a larger Whisper model for better transcription accuracy
/graphify <path> --cluster-only                       # rerun clustering on existing graph
/graphify <path> --no-viz                             # skip visualization, just report + JSON
/graphify <path> --html                               # (HTML is generated by default - this flag is a no-op)
/graphify <path> --svg                                # also export graph.svg (embeds in Notion, GitHub)
/graphify <path> --graphml                            # export graph.graphml (Gephi, yEd)
/graphify <path> --neo4j                              # generate graphify-out/cypher.txt for Neo4j
/graphify <path> --neo4j-push bolt://localhost:7687   # push directly to Neo4j
/graphify <path> --falkordb                           # generate graphify-out/cypher.txt for FalkorDB
/graphify <path> --falkordb-push falkordb://localhost:6379   # push directly to FalkorDB
/graphify <path> --mcp                                # start MCP stdio server for agent access
/graphify <path> --watch                              # watch folder, auto-rebuild on code changes (no LLM needed)
/graphify <path> --wiki                               # build agent-crawlable wiki (index.md + one article per community)
/graphify <path> --obsidian --obsidian-dir ~/vaults/my-project  # write vault to custom path (e.g. existing vault)
/graphify add <url>                                   # fetch URL, save to ./raw, update graph
/graphify add <url> --author "Name"                   # tag who wrote it
/graphify add <url> --contributor "Name"              # tag who added it to the corpus
/graphify query "<question>"                          # BFS traversal - broad context
/graphify query "<question>" --dfs                    # DFS - trace a specific path
/graphify query "<question>" --budget 1500            # cap answer at N tokens
/graphify path "AuthModule" "Database"                # shortest path between two concepts
/graphify explain "SwinTransformer"                   # plain-language explanation of a node

graphify 的用途

将任意代码、文档、论文、图像或视频文件夹交给 graphify,即可获得可查询的知识图谱。图谱会在不同会话之间持久保留,并提供可靠的审计追踪(EXTRACTED/INFERRED/AMBIGUOUS);社区检测会揭示跨文档关联,而这些关联可能是你想不到要查询的。

调用时必须执行的操作

如果用户调用 /graphify --help 或 /graphify -h(且没有其他参数),请原样打印上方 ## 用法 部分的内容,然后停止。不要运行任何命令,不要检测文件,也不要将路径默认为 .。只需打印用法块并返回。

快速路径 — 已有图谱: 在执行任何其他操作之前,检查 graphify-out/graph.json 是否存在。预期位置是相对于当前工作目录(即你运行命令的项目根目录)的 graphify-out/graph.json。如果该文件存在,且用户的请求是关于代码库的自然语言问题(例如“X 是如何工作的?”、“什么调用了 Y?”、“追踪 Z 中的数据流”),而不是明确的重建命令(--update、--cluster-only,或暗示需要重新提取的单独路径/URL),则:完全跳过步骤 1–5,直接跳转到 ## 用于 /graphify query。 立即运行 graphify query "<question>"。不要运行 detect。不要检查语料库大小。不要要求用户缩小范围。图谱已经构建完成,直接使用即可。

如果没有提供路径,则使用 .(当前目录)。不要向用户询问路径。

如果路径参数以 https://github.com/ 或 http://github.com/ 开头,则将其视为 GitHub URL——先运行步骤 0,然后继续使用解析后的本地路径。

请按顺序执行以下步骤。不要跳过步骤。

步骤 0 - GitHub 仓库与多路径合并(仅适用于 URL 或多个路径)

仅当路径是一个或多个 https://github.com/... URL,或需要合并多个本地子文件夹时才执行。请参阅 references/github-and-merge.md,了解克隆、跨仓库合并和单体仓库流程,然后继续使用解析后的本地路径。普通本地路径跳过此步骤。

步骤 1 - 确保已安装 graphify
# Detect the correct Python interpreter (handles uv tool, pipx, venv, system installs)
PYTHON=""
GRAPHIFY_BIN=$(which graphify 2>/dev/null)
# 1. uv tool installs — most reliable on modern Mac/Linux
if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then
    _UV_PY=$(uv tool run --from graphifyy python -c "import sys; print(sys.executable)" 2>/dev/null)
    if [ -n "$_UV_PY" ]; then PYTHON="$_UV_PY"; fi
fi
# 2. Read shebang from graphify binary (pipx and direct pip installs)
if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then
    _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!')
    case "$_SHEBANG" in
        *[!a-zA-Z0-9/_.@-]*) ;;
        *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;;
    esac
fi
# 3. Fall back to python3
if [ -z "$PYTHON" ]; then PYTHON="python3"; fi
if ! "$PYTHON" -c "import graphify" 2>/dev/null; then
    if command -v uv >/dev/null 2>&1; then
        uv tool install --upgrade graphifyy -q 2>&1 | tail -3
        _UV_PY=$(uv tool run --from graphifyy python -c "import sys; print(sys.executable)" 2>/dev/null)
        if [ -n "$_UV_PY" ]; then PYTHON="$_UV_PY"; fi
    else
        "$PYTHON" -m pip install graphifyy -q 2>/dev/null \
          || "$PYTHON" -m pip install graphifyy -q --break-system-packages 2>&1 | tail -3
    fi
fi
# Write interpreter path for all subsequent steps (persists across invocations)
mkdir -p graphify-out
"$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w', encoding='utf-8').write(sys.executable)"
# Save scan root so `graphify update` (no args) knows where to look next time.
# The scan path is passed through a quoted heredoc, never substituted into the
# command line itself: a bare `cd <path>` (or an unquoted heredoc, which
# still expands $()/backticks in its body) would let a malicious path execute
# as shell code the moment this line runs.
"$PYTHON" -c "import os, sys; out_path = os.path.abspath('graphify-out/.graphify_root'); os.chdir(sys.stdin.readline().rstrip('\n')); open(out_path, 'w', encoding='utf-8').write(os.getcwd())" <<'GRAPHIFY_ROOT_EOF'
INPUT_PATH
GRAPHIFY_ROOT_EOF

如果导入成功,则不输出任何内容,直接进入步骤 2。

在之后的每个 bash 代码块中,将 python3 替换为 $(cat graphify-out/.graphify_python),以使用正确的解释器。

步骤 2 - 检测文件
$(cat graphify-out/.graphify_python) -c "
import json
from graphify.detect import detect
from pathlib import Path
from graphify.paths import out_path
from graphify.watch import _read_build_excludes, _read_build_gitignore
result = detect(Path('INPUT_PATH'), extra_excludes=_read_build_excludes(out_path()), gitignore=_read_build_gitignore(out_path()))
# Write the sidecar from Python, not a shell redirect, so the same block renders
# on PowerShell hosts without console-encoding drift (#2528).
Path('graphify-out/.graphify_detect.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\")
print(f'Detected {result[\"total_files\"]} files')
"

将 INPUT_PATH 替换为用户提供的实际路径。不要 cat 或打印 JSON——静默读取,并呈现简明的摘要:

Corpus: X files · ~Y words
  code:     N files (.py .ts .go ...)
  docs:     N files (.md .txt ...)
  papers:   N files (.pdf ...)
  images:   N files
  video:    N files (.mp4 .mp3 ...)

摘要中省略文件数为 0 的类别。

然后据此处理:

  • 如果 total_files 为 0:停止,并显示“在 [path] 中找不到受支持的文件。”
  • 如果 skipped_sensitive 非空:报告数量并列出被跳过的文件名,以便发现被错误标记的源文件或文档,并重命名或移动它们(#2106)。
  • 如果 total_words > 2,000,000 或 total_files > 500:显示警告。然后计算文件数最多的前 5 个一级子目录:
    • 从 detect JSON 中读取 scan_root(始终是已解析 INPUT_PATH 的绝对路径)。
    • 合并所有类型(code、document、paper、image、video)的文件列表。
    • 筛除路径以 scan_root + "/graphify-out/" 开头的文件,以排除已转换的旁文件。
    • 对每个文件,去掉 scan_root 前缀,并取第一个路径组件。直接位于 scan_root 中且没有子目录的文件计为 (root)。
    • 如果所有文件都在 (root) 中且没有子目录,则不要要求缩小范围——不存在子文件夹。改为建议使用 --no-cluster 跳过耗时的聚类步骤并继续。
    • 否则按数量排序,显示前 5 个子目录及其文件数,然后询问要在哪个子文件夹中运行。等待用户回答后再继续。
  • 否则:如果检测到视频文件,则直接进入步骤 2.5;如果没有,则进入步骤 3。
步骤 2.5 - 视频和音频(仅在检测到视频文件时)

如果 detect 返回的视频文件数为 0,则完全跳过此步骤。当语料中包含视频或音频时,请参阅 references/transcribe.md,先将其转录为文本,然后在步骤 3 中将转录文本视为文档文件。

步骤 3 - 提取实体和关系

**开始前:**注意是否指定了 --mode deep。如果指定了,必须在步骤 B2 中向每个子 Agent 传递 DEEP_MODE=true。从原始调用中记录此信息,不要遗漏。

此步骤分为两部分:结构化提取(确定性、免费)和语义提取(大模型,会消耗令牌)。

**graphify 不需要 API 密钥。绝不要向用户索要密钥,也绝不要因此停滞。**代码会通过 AST 进行结构化提取,无需大模型,也完全不需要密钥——仅包含代码的语料(常见情况:对代码库运行 /graphify .)会完全跳过语义提取,因此无需在此进行任何操作:直接进入 A 部分并跳过 B 部分。语义提取(仅针对文档、论文和图像)只有在 GEMINI_API_KEY/GOOGLE_API_KEY 已设置时才会使用 Gemini;否则由宿主 Agent 自身充当大模型。graphify 不会读取 ANTHROPIC_API_KEY、OPENAI_API_KEY 或任何其他提供商的密钥。如果你发现自己正准备提示用户提供 API 密钥、等待密钥,或因缺少密钥而停止,那就是误解了此技能——无需密钥,继续操作。

**语义提取前:**检查是否设置了 GEMINI_API_KEY 或 GOOGLE_API_KEY。如果两者都未设置,请向用户打印以下单行提示:

提示:设置 GEMINI_API_KEY 或 GOOGLE_API_KEY,即可使用 Gemini 进行语义提取(pip install 'graphifyy[gemini]')。

打印一次,然后继续——不要等待用户提供密钥。如果已设置 GEMINI_API_KEY 或 GOOGLE_API_KEY,请使用 graphify.llm.extract_corpus_parallel(files, backend="gemini") 进行语义提取,而不是分派子 Agent。默认的 Gemini 模型为 gemini-3-flash-preview;在无头 CLI 流程中,设置 GRAPHIFY_GEMINI_MODEL 或传递 --model 即可覆盖默认模型。

**不会读取其他 API 密钥。**未设置 GEMINI_API_KEY/GOOGLE_API_KEY 时,语义提取由宿主 Agent 自身完成——当前运行的会话就是大模型。在会分派子 Agent 的宿主环境中(例如 Claude Code),按 B 部分中的说明分派子 Agent。在直接于终端中运行 CLI 且无法分派子 Agent 的宿主环境中,不要停滞:仅包含代码的语料没有语义处理工作,因此请写入空语义文件(B 部分“快速路径”),然后继续到 C 部分;对于包含文档、论文或图像的语料,可以设置 Gemini 密钥,也可以自行内联提取,但无论如何都不要提示用户提供 ANTHROPIC_API_KEY——这样的提示是对本技能的误解。

并行运行 A 部分(AST)和 B 部分(语义)。在同一条消息中分派所有语义子 Agent 并启动 AST 提取。两者处理的文件类型不同,因此可以同时运行。像之前一样,在 C 部分合并结果。

注意:并行运行 AST 和语义提取,可在大型语料上节省 5–15 秒。AST 是确定且快速的;在子 Agent 处理文档和论文时启动 AST。

A 部分 - 代码文件的结构化提取

检测到任何代码文件时,请并行运行 AST 提取和 B 部分的子 Agent:

$(cat graphify-out/.graphify_python) -c "
import sys, json
from graphify.extract import collect_files, extract
from pathlib import Path
import json

code_files = []
detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\"))
for f in detect.get('files', {}).get('code', []):
    code_files.extend(collect_files(Path(f)) if Path(f).is_dir() else [Path(f)])

if code_files:
    # root= anchors node ids and source_file on the scan root; cache_root only says
    # where graphify-out/cache lives. Pass both: without root= the anchor falls back
    # to cache_root, so pointing the cache elsewhere would rename every node (#3727).
    result = extract(code_files, cache_root=Path('INPUT_PATH'), root=Path('INPUT_PATH'))
    Path('graphify-out/.graphify_ast.json').write_text(json.dumps(result, indent=2, ensure_ascii=False), encoding=\"utf-8\")
    print(f'AST: {len(result[\"nodes\"])} nodes, {len(result[\"edges\"])} edges')
else:
    Path('graphify-out/.graphify_ast.json').write_text(json.dumps({'nodes':[],'edges':[],'input_tokens':0,'output_tokens':0}, ensure_ascii=False), encoding=\"utf-8\")
    print('No code files - skipping AST extraction')
"
B 部分 - 语义提取(并行子 Agent)

**快速路径:**如果检测结果中没有文档、论文和图像(仅包含代码的语料),请完全跳过 B 部分,直接进入 C 部分。AST 会处理代码——语义子 Agent 无需执行任何操作。首先写入一个空语义文件,确保 C 部分合并时有输入(它会无条件读取 .graphify_semantic.json;如果没有该文件,仅包含代码的运行会触发 FileNotFoundError):

$(cat graphify-out/.graphify_python) -c "
import json
from pathlib import Path
Path('graphify-out/.graphify_semantic.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8')
"

强制要求:此处必须使用 Agent 工具。禁止逐个自行读取文件——速度会慢 5–10 倍。不使用 Agent 工具就是错误操作。

分派子 Agent 前,请打印时间估算:

  • 从 graphify-out/.graphify_detect.json 加载 total_words 和文件数量
  • 估算所需 Agent 数量:ceil(uncached_non_code_files / 22)(每批大小为 20–25 个文件)
  • 估算耗时:每批约 45 秒(各批并行运行,因此总耗时 ≈ 45s × ceil(agents/parallel_limit))
  • 打印:“语义提取:约 N 个文件 → X 个 Agent,预计约 Ys”

步骤 B0 - 首先检查提取缓存 在派发任何子 Agent 之前,先检查哪些文件已有缓存的提取结果:

下方的 SPEC_PATH 是与此 SKILL.md 一同提供的 references/extraction-spec.md 的绝对路径——也就是步骤 B2 加载并传给每个子 Agent 的同一个文件。它是提取提示词,因此缓存条目会记录该提示词:当 graphify 升级更改提示词时,旧提示词生成的条目会重新提取,而不是直接重放;提示词未变时则保留已有条目(#1939)。在步骤 B0 和 B3 中都替换为实际路径——两处传入同一个路径,不要省略该参数。

$(cat graphify-out/.graphify_python) -c "
import json
from graphify.cache import check_semantic_cache
from pathlib import Path

detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\"))
# Only content files go to semantic extraction. Code is already covered structurally
# by the AST pass (Part A); flattening every category here makes subagents re-read
# every source file (#1392). Video is transcribed to a document in Step 2.5 first.
all_files = [f for cat in ('document', 'paper', 'image') for f in detect['files'].get(cat, [])]

cached_nodes, cached_edges, cached_hyperedges, uncached = check_semantic_cache(all_files, root='INPUT_PATH', prompt_file='SPEC_PATH')

# Always (re)write the cache file: write hits, else DELETE any leftover from a prior
# run so Part C never merges a stale .graphify_cached.json (#1392).
if cached_nodes or cached_edges or cached_hyperedges:
    Path('graphify-out/.graphify_cached.json').write_text(json.dumps({'nodes': cached_nodes, 'edges': cached_edges, 'hyperedges': cached_hyperedges}, ensure_ascii=False), encoding=\"utf-8\")
else:
    Path('graphify-out/.graphify_cached.json').unlink(missing_ok=True)
Path('graphify-out/.graphify_uncached.txt').write_text('\n'.join(uncached), encoding=\"utf-8\")
# Nothing has been dispatched yet, so any chunk file on disk is a leftover from an
# interrupted run, and Step B3 merges every .graphify_chunk_*.json it finds.
for stale in Path('graphify-out').glob('.graphify_chunk_*.json'):
    stale.unlink()
print(f'Cache: {len(all_files)-len(uncached)} files hit, {len(uncached)} files need extraction')
"

仅为 graphify-out/.graphify_uncached.txt 中列出的文件派发子 Agent。如果所有文件都已缓存,则跳过步骤 B1 和 B2,但仍要运行步骤 B3 的命令:合并是 Part B 中唯一会写入 graphify-out/.graphify_semantic.json 的步骤,而 Part C 会无条件读取该文件。不要改为写入空的 .graphify_semantic.json,否则会丢弃所有缓存节点。

步骤 B1 - 拆分为块

从 graphify-out/.graphify_uncached.txt 加载文件。将文件拆分为每块 20-25 个。每张图片单独成块(视觉处理需要独立上下文)。拆分时,将同一目录下的文件归为一组,以便相关产物落在同一块中,更有可能提取出跨文件关系。

步骤 B2 - 在同一条消息中派发所有子 Agent

在同一条回复中多次调用 Agent 工具——每块调用一次。这是让它们并行运行的唯一方式。如果先调用一次 Agent 并等待,然后再调用另一次,就是顺序执行,违背了并行处理的目的。

**重要 - 子 Agent 类型:**始终使用 subagent_type="general-purpose"。不要使用 Explore——它是只读的,无法将块文件写入磁盘,提取结果会因此悄然丢失。General-purpose 具有子 Agent 所需的 Write 和 Bash 访问权限。

3 个块的具体示例:

[Agent tool call 1: files 1-15, subagent_type="general-purpose"]
[Agent tool call 2: files 16-30, subagent_type="general-purpose"]
[Agent tool call 3: files 31-45, subagent_type="general-purpose"]

全部放在同一条消息中,不要拆成三条消息。

每个子 Agent 都会收到以下精确提示词(替换 FILE_LIST、CHUNK_NUM、TOTAL_CHUNKS、DEEP_MODE 和 CHUNK_PATH)。

CHUNK_PATH 必须是绝对路径——请在派发之前计算得出:

PROJECT_ROOT=$(pwd)  # cwd — where Part C globs graphify-out/ (NOT .graphify_root/scan dir, #1392)
# Then for chunk N: CHUNK_PATH="${PROJECT_ROOT}/graphify-out/.graphify_chunk_0N.json"

子 Agent 提示词模板:

请参阅 references/extraction-spec.md,了解子 Agent 提示词的具体内容(JSON 架构、节点 ID 规则、置信度标准、frontmatter、超边和视觉规则)。仅在此处、且至少有一个块包含文档、论文或图片时加载它;纯代码语料会跳过 Part B,不会读取它。将该提示词原文传给每个子 Agent,并替换 FILE_LIST、CHUNK_NUM、TOTAL_CHUNKS、DEEP_MODE 和 CHUNK_PATH;要求其将结果写入 CHUNK_PATH。

步骤 B3 - 收集、缓存并合并

等待所有子 Agent 完成。对每个结果:

  • 检查 graphify-out/.graphify_chunk_NN.json 是否存在于磁盘上——这是成功的标志
  • 如果文件存在,且包含有效的 nodes 和 edges JSON,则将其纳入并保存到缓存
  • 如果文件缺失,子 Agent 可能是以只读模式派发的(Explore 类型)——打印警告:"chunk N missing from disk — subagent may have been read-only. Re-run with general-purpose agent." 不要静默跳过。
  • 如果子 Agent 失败或返回无效 JSON,则打印警告并跳过该块——不要中止

如果失败或缺失的块超过一半,请停止并告知用户重新运行,并确保使用 subagent_type="general-purpose"。

将所有块文件合并到 .graphify_semantic_new.json。每次 Agent 调用完成后,从 Agent 工具结果的 usage 字段读取实际 token 数量,并将其写回块 JSON 后再进行合并——块 JSON 本身始终包含占位用的零值。然后运行:

$(cat graphify-out/.graphify_python) -c "
import json, glob
from pathlib import Path

chunks = sorted(glob.glob('graphify-out/.graphify_chunk_*.json'))
all_nodes, all_edges, all_hyperedges = [], [], []
total_in, total_out = 0, 0
for c in chunks:
    d = json.loads(Path(c).read_text(encoding=\"utf-8\"))
    all_nodes += d.get('nodes', [])
    all_edges += d.get('edges', [])
    all_hyperedges += d.get('hyperedges', [])
    total_in += d.get('input_tokens', 0)
    total_out += d.get('output_tokens', 0)
Path('graphify-out/.graphify_semantic_new.json').write_text(json.dumps({
    'nodes': all_nodes, 'edges': all_edges, 'hyperedges': all_hyperedges,
    'input_tokens': total_in, 'output_tokens': total_out,
}, indent=2, ensure_ascii=False), encoding=\"utf-8\")
print(f'Merged {len(chunks)} chunks: {total_in:,} in / {total_out:,} out tokens')
"

将新结果保存到缓存。传入与步骤 B0 相同的 SPEC_PATH——它会为每个条目标记生成该条目的提示词;如果写入时使用的提示词与读取时不同,条目就会被存到下次运行不会查找的位置(#1939):

$(cat graphify-out/.graphify_python) -c "
import json
from graphify.cache import save_semantic_cache
from pathlib import Path

new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached, prompt_file='SPEC_PATH')
print(f'Cached {saved} files')
"

将缓存结果和新结果合并到 graphify-out/.graphify_semantic.json:

$(cat graphify-out/.graphify_python) -c "
import json
from pathlib import Path

cached = json.loads(Path('graphify-out/.graphify_cached.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_cached.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}

all_nodes = cached['nodes'] + new.get('nodes', [])
all_edges = cached['edges'] + new.get('edges', [])
all_hyperedges = cached.get('hyperedges', []) + new.get('hyperedges', [])
seen = set()
deduped = []
for n in all_nodes:
    if n['id'] not in seen:
        seen.add(n['id'])
        deduped.append(n)

merged = {
    'nodes': deduped,
    'edges': all_edges,
    'hyperedges': all_hyperedges,
    'input_tokens': new.get('input_tokens', 0),
    'output_tokens': new.get('output_tokens', 0),
}
Path('graphify-out/.graphify_semantic.json').write_text(json.dumps(merged, indent=2, ensure_ascii=False), encoding=\"utf-8\")
print(f'Extraction complete - {len(deduped)} nodes, {len(all_edges)} edges ({len(cached[\"nodes\"])} from cache, {len(new.get(\"nodes\",[]))} new)')
"

清理临时文件:rm -f graphify-out/.graphify_cached.json graphify-out/.graphify_uncached.txt graphify-out/.graphify_semantic_new.json

Part C - 将 AST 与语义结果合并为最终提取结果
$(cat graphify-out/.graphify_python) -c "
import sys, json
from pathlib import Path

ast = json.loads(Path('graphify-out/.graphify_ast.json').read_text(encoding=\"utf-8\"))
sem = json.loads(Path('graphify-out/.graphify_semantic.json').read_text(encoding=\"utf-8\"))

# Stamp tier provenance on the semantic side. build._is_ast_tier reads
# _origin when present and otherwise guesses from the SHAPE of
# source_location ('L<line>' means AST). A subagent that writes 'L12' for a
# doc section therefore made that node read as AST, and the next
# graphify update deleted it along with the re-extracted code file it
# seemed to belong to (#2843). extract() stamps its own items 'ast'; the
# semantic side must say so explicitly.
for n in sem['nodes']:
    n.setdefault('_origin', 'semantic')
for e in sem['edges']:
    e.setdefault('_origin', 'semantic')

# Merge: AST nodes first, semantic nodes deduplicated by id
seen = {n['id'] for n in ast['nodes']}
merged_nodes = list(ast['nodes'])
for n in sem['nodes']:
    if n['id'] not in seen:
        merged_nodes.append(n)
        seen.add(n['id'])

merged_edges = ast['edges'] + sem['edges']
merged_hyperedges = sem.get('hyperedges', [])
merged = {
    'nodes': merged_nodes,
    'edges': merged_edges,
    'hyperedges': merged_hyperedges,
    'input_tokens': sem.get('input_tokens', 0),
    'output_tokens': sem.get('output_tokens', 0),
}
Path('graphify-out/.graphify_extract.json').write_text(json.dumps(merged, indent=2, ensure_ascii=False), encoding=\"utf-8\")
total = len(merged_nodes)
edges = len(merged_edges)
print(f'Merged: {total} nodes, {edges} edges ({len(ast[\"nodes\"])} AST + {len(sem[\"nodes\"])} semantic)')
"
Step 4 - 构建图、聚类、分析并生成输出

开始前: 以下代码块会将 directed=IS_DIRECTED 传给 build_from_json()。如果指定了 --directed,将 IS_DIRECTED 替换为 True(构建保留边方向 source→target 的 DiGraph);否则替换为 False(默认的无向 Graph)。替换方式与替换 INPUT_PATH 相同——不要在代码中保留字面值 IS_DIRECTED。

mkdir -p graphify-out
$(cat graphify-out/.graphify_python) -c "
import sys, json
from graphify.build import build_from_json
from graphify.cluster import cluster, score_all
from graphify.analyze import god_nodes, surprising_connections, suggest_questions
from graphify.report import generate
from graphify.export import to_json
from pathlib import Path

extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
detection  = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\"))

# root= mirrors the --update runbook (#1361): relativize source_file to the same
# base so the full build and incremental --update never drift apart on re-extract.
G = build_from_json(extraction, root='INPUT_PATH', directed=IS_DIRECTED)
# Guard BEFORE any write: an empty extraction must not clobber a good graph.json /
# GRAPH_REPORT.md / analysis sidecar. Check immediately after build (#1392).
if G.number_of_nodes() == 0:
    print('ERROR: Graph is empty - extraction produced no nodes.')
    print('Possible causes: all files were skipped, binary-only corpus, or extraction failed.')
    raise SystemExit(1)
communities = cluster(G)
cohesion = score_all(G, communities)
tokens = {'input': extraction.get('input_tokens', 0), 'output': extraction.get('output_tokens', 0)}
gods = god_nodes(G)
surprises = surprising_connections(G, communities)
labels = {cid: 'Community ' + str(cid) for cid in communities}
# Placeholder questions - regenerated with real labels in Step 5
questions = suggest_questions(G, communities, labels)

# Export FIRST and honor the #479 shrink-guard: to_json returns False (writing
# nothing) when the new graph is smaller than the existing graph.json. Only write
# GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so
# they never describe a graph that graph.json doesn't contain (#1392).
wrote = to_json(G, communities, 'graphify-out/graph.json')
if not wrote:
    print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).')
    print('If this shrink is intentional (you deleted files), re-run a full build with --force.')
    raise SystemExit(1)
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions)
Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\")
analysis = {
    'communities': {str(k): v for k, v in communities.items()},
    'cohesion': {str(k): v for k, v in cohesion.items()},
    'gods': gods,
    'surprises': surprises,
    'questions': questions,
}
Path('graphify-out/.graphify_analysis.json').write_text(json.dumps(analysis, indent=2, ensure_ascii=False), encoding=\"utf-8\")
print(f'Graph: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges, {len(communities)} communities')
"

如果此步骤输出 ERROR: Graph is empty,请停止并告知用户发生了什么——不要继续进行标注或可视化。

将 INPUT_PATH 替换为实际路径。

Step 4.5 - 图健康检查(只读完整性关卡)

这是在标注前对提取结果进行的非破坏性诊断。它会揭示边坍缩、悬空/缺失端点和自环——这些都是增量更新和 AST/LLM ID 不匹配时可能出现的静默损坏模式。仅供读取;永远不会中止流程。

$(cat graphify-out/.graphify_python) -c "
import json
from pathlib import Path
from graphify.diagnostics import diagnose_extraction, format_diagnostic_report

extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
summary = diagnose_extraction(extraction, directed=IS_DIRECTED, root='INPUT_PATH')
print(format_diagnostic_report(summary))
flags = [f'{summary[k]} {label}' for k, label in (
    ('dangling_endpoint_edges', 'dangling-endpoint edges'),
    ('missing_endpoint_edges', 'missing-endpoint edges'),
    ('self_loop_edges', 'self-loop edges'),
    ('directed_same_endpoint_collapsed_edges', 'collapsed (directed) edges'),
    ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'),
) if summary.get(k, 0)]
print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).')
"

按照 Step 4 的说明替换 IS_DIRECTED 和 INPUT_PATH。如果输出 GRAPH HEALTH WARNING,请在最终摘要中说明(不要中止——图仍可使用,但必须明确指出完整性问题,以遵守诚信规则)。

Step 5 - 标注社区

读取 graphify-out/.graphify_analysis.json。对于每个社区键,查看其节点标签,并为其编写一个 2-5 个词的通俗名称(例如“注意力机制”“训练流水线”“数据加载”)。

然后重新生成报告并保存可视化工具所需的标签:

$(cat graphify-out/.graphify_python) -c "
import sys, json
from graphify.build import build_from_json
from graphify.cluster import score_all
from graphify.analyze import god_nodes, surprising_connections, suggest_questions
from graphify.report import generate
from graphify.export import to_json
from pathlib import Path

extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
detection  = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\"))
analysis   = json.loads(Path('graphify-out/.graphify_analysis.json').read_text(encoding=\"utf-8\"))

# root= as in Step 4 / the --update runbook (#1361) — same base for node-key parity.
G = build_from_json(extraction, root='INPUT_PATH', directed=IS_DIRECTED)
communities = {int(k): v for k, v in analysis['communities'].items()}
cohesion = {int(k): v for k, v in analysis['cohesion'].items()}
tokens = {'input': extraction.get('input_tokens', 0), 'output': extraction.get('output_tokens', 0)}

# LABELS - replace these with the names you chose above
labels = LABELS_DICT

# Regenerate questions with real community labels (labels affect question phrasing)
questions = suggest_questions(G, communities, labels)

report = generate(G, communities, cohesion, labels, analysis['gods'], analysis['surprises'], detection, tokens, 'INPUT_PATH', suggested_questions=questions)
Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\")
Path('graphify-out/.graphify_labels.json').write_text(json.dumps({str(k): v for k, v in labels.items()}, ensure_ascii=False), encoding=\"utf-8\")
# Re-export so graph.json nodes carry the curated community_name (#2490).
# Same extraction as Step 4, so the #479 shrink-guard passes on node count;
# if it still refuses, surface the guard message - do not force past it.
wrote = to_json(G, communities, 'graphify-out/graph.json', community_labels=labels)
if not wrote:
    print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).')
    print('If this shrink is intentional (you deleted files), re-run a full build with --force.')
print('Report updated with community labels')
"

将 LABELS_DICT 替换为你构造的实际字典(例如 {0: "注意力机制", 1: "训练流水线"})。 将 INPUT_PATH 替换为实际路径。

Step 6 - 生成 Obsidian 知识库(可选启用)+ HTML

始终生成 HTML(除非指定了 --no-viz)。仅当明确指定了 --obsidian 时才生成 Obsidian 知识库——否则跳过,因为它会为每个节点生成一个文件。

如果指定了 --obsidian:

  • 如果还指定了 --obsidian-dir <path>,则通过 --dir 传入。否则默认为 graphify-out/obsidian。
graphify export obsidian
# or with custom dir: graphify export obsidian --dir ~/vaults/my-project

生成 HTML 图(始终执行,除非指定了 --no-viz):

graphify export html  # auto-aggregates to community view if graph > 5000 nodes
# or: graphify export html --no-viz
Steps 6b-8 - Wiki、Neo4j、FalkorDB、SVG、GraphML、MCP、基准测试(仅在指定相应标志时执行)

仅当存在相应标志(--wiki、--neo4j/--neo4j-push、--falkordb/--falkordb-push、--svg、--graphml、--mcp),或对于词数缩减基准测试而言,total_words 超过 5,000 时,才会运行这些步骤。默认运行时如果没有导出标志,则会跳过所有这些步骤。各项说明请参阅 references/exports.md。请在 Step 9 清理前执行任何 --wiki 导出,以确保 .graphify_labels.json 仍可用。


步骤 9 - 保存清单、更新成本跟踪器、清理并生成报告
$(cat graphify-out/.graphify_python) -c "
import json
from pathlib import Path
from datetime import datetime, timezone
from graphify.detect import save_manifest

# Save manifest for --update
detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\"))
extract = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
# In --update mode, 'all_files' carries the full corpus; 'files' is the changed
# subset. Full-rebuild mode populates only 'files', so the fallback handles that.
# root= relativizes the manifest keys to the scan root (same base as the build),
# so the on-disk manifest is portable across clones/machines and a later --update
# matches cached files instead of missing every one (#1417).
#
# Only stamp semantic files (docs/papers/images) that ACTUALLY produced output:
# a detected file whose chunk failed or was omitted must stay unstamped so the
# next --update re-queues it, otherwise it is marked done and its content is lost
# forever (#2015). This mirrors the library extract path exactly
# (cli._stamped_manifest_files + clear_semantic + scan_corpus); do not stamp the
# raw corpus. Code files are always stamped (AST is deterministic); only semantic
# types are gated on output.
from graphify.cli import _stamped_manifest_files
_corpus = detect.get('all_files') or detect['files']
_manifest_files = _stamped_manifest_files(_corpus, extract, Path('INPUT_PATH'))
# Files dispatched this run (the changed subset) but NOT stamped above still carry
# a stale semantic_hash from a prior run; clear it so detect_incremental re-queues
# them instead of reading them as unchanged (#1948).
_sem_types = ('document', 'paper', 'image')
_dispatched = {f for t, fl in detect['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 (not the stamp-filtered subset) so in-root
# files newly excluded since last run are dropped rather than masquerading as
# deletions; untouched files' prior rows are still preserved (#1908).
_scan = {f for fl in _corpus.values() for f in fl}
save_manifest(_manifest_files, root='INPUT_PATH', scan_corpus=_scan, clear_semantic=_cleared or None)

# Update cumulative cost tracker
input_tok = extract.get('input_tokens', 0)
output_tok = extract.get('output_tokens', 0)

cost_path = Path('graphify-out/cost.json')
if cost_path.exists():
    cost = json.loads(cost_path.read_text(encoding=\"utf-8\"))
else:
    cost = {'runs': [], 'total_input_tokens': 0, 'total_output_tokens': 0}

cost['runs'].append({
    'date': datetime.now(timezone.utc).isoformat(),
    'input_tokens': input_tok,
    'output_tokens': output_tok,
    'files': detect.get('total_files', 0),
})
cost['total_input_tokens'] += input_tok
cost['total_output_tokens'] += output_tok
cost_path.write_text(json.dumps(cost, indent=2, ensure_ascii=False), encoding=\"utf-8\")

print(f'This run: {input_tok:,} input tokens, {output_tok:,} output tokens')
print(f'All time: {cost[\"total_input_tokens\"]:,} input, {cost[\"total_output_tokens\"]:,} output ({len(cost[\"runs\"])} runs)')
"
rm -f graphify-out/.graphify_detect.json graphify-out/.graphify_extract.json graphify-out/.graphify_ast.json graphify-out/.graphify_semantic.json graphify-out/.graphify_analysis.json
find graphify-out -maxdepth 1 -name '.graphify_chunk_*.json' -delete 2>/dev/null
rm -f graphify-out/.needs_update 2>/dev/null || true

将 INPUT_PATH 替换为实际路径(与步骤 4-5 中使用的值相同),这样清单就会相对于扫描根目录进行计算。

告知用户(除非提供了 --obsidian,否则省略 obsidian 那一行):

Graph complete. Outputs in PATH_TO_DIR/graphify-out/

  graph.html            - interactive graph, open in browser
  GRAPH_REPORT.md       - audit report
  graph.json            - raw graph data
  obsidian/             - Obsidian vault (only if --obsidian was given)

如果 graphify 为你节省了时间,可以考虑支持它:https://github.com/sponsors/safishamsi

将 PATH_TO_DIR 替换为已处理目录的实际绝对路径。

如果 PATH_TO_DIR 位于 git 仓库中,请在报告中添加一行:graphify-out/ 已写入工作树,因此除非仓库已忽略该目录,否则它会显示在 git status 中;用户可以将 graphify-out/ 添加到 .gitignore,或者只提交团队应共享的输出(参见 https://docs.graphify.com/guides/team-workflows)。

然后直接将 GRAPH_REPORT.md 中的以下部分粘贴到聊天中:

  • 重要节点
  • 意外关联
  • 建议的问题

不要粘贴完整报告,只粘贴这三个部分。内容要简洁。

然后立即主动提出继续探索。从报告中选出最有趣的一个建议问题——也就是跨越最多社区边界,或包含最令人意外的桥接节点的问题——并询问:

“这张图最有趣的问题是:[question]。要我追踪一下吗?”

如果用户回答是,就在图上运行 /graphify query "[question]",并利用图结构带用户了解答案——哪些节点相连、跨越了哪些社区边界,以及这条路径揭示了什么。只要用户想探索,就继续下去。每个回答都应以自然的后续问题结尾(“这与 X 有关联——要深入了解吗?”),让整个会话像导航,而不是一次性报告。

图就是地图。流水线运行完毕后,你的工作就是带路。


子命令的解释器检查

运行以下任何子命令(--update、--cluster-only、query、path、explain、add)之前,请检查 .graphify_python 是否存在。如果文件缺失(例如用户删除了 graphify-out/),请先重新解析解释器:

if [ ! -f graphify-out/.graphify_python ]; then
    GRAPHIFY_BIN=$(which graphify 2>/dev/null)
    if [ -n "$GRAPHIFY_BIN" ]; then
        PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!')
        case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac
    else
        PYTHON="python3"
    fi
    mkdir -p graphify-out
    "$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w', encoding='utf-8').write(sys.executable)"
fi

关于 --update 和 --cluster-only

两者都是非默认子命令。--update 只重新提取新增或已更改的文件;--cluster-only 则在现有图上重新运行聚类。两种流程请参见 references/update.md。


关于 /graphify query

当 graphify-out/graph.json 已存在且用户询问有关语料库的问题时,应基于图回答,而不是重新构建:

graphify query "<question>"

遍历前,请根据图自身的词汇扩展问题,避免因措辞不匹配而使答案沦为噪声。如果 graphify query CLI 不可用,则回退到对 graphify-out/graph.json 执行内联 NetworkX 遍历。回答只能依据图输出中包含的内容;引用具体事实时,请引用 source_location。有关词汇扩展步骤、BFS/DFS 遍历模式、--budget 上限、NetworkX 回退、save-result 反馈,以及 /graphify path 和 /graphify explain 流程,请参见 references/query.md。


关于 /graphify add 和 --watch

两者都不是默认构建流程的一部分。当用户运行 /graphify add <url> 将 URL 获取到语料库中,或传入 --watch 以便在文件更改时自动重新构建时,请参见 references/add-watch.md。


关于提交钩子和原生 CLAUDE.md 集成

当用户要求安装提交后自动重新构建钩子,或将 graphify 接入项目的 CLAUDE.md 时,请参见 references/hooks.md。


诚实守则

  • 绝不捏造边。如果不确定,请使用 AMBIGUOUS。
  • 绝不省略语料库检查警告。
  • 始终在报告中显示令牌成本。
  • 绝不使用符号隐藏凝聚度分数——请显示原始数值。
  • 如果图超过 5,000 个节点,未经警告用户,绝不运行 HTML 可视化。

评论

支持 Markdown