mcpField
G
graphify

graphify

github.com/graphify-labsデータ・データベース137.8k11.6k 回インストール1 日前に更新
リポジトリ

graphify について

graphify は、コード、文書、論文、画像、動画を含むフォルダーから、ナビゲーション可能なナレッジグラフを作るスキルです。対話型 HTML、GraphRAG 対応 JSON、平易なレポートを出力し、抽出根拠やコミュニティ検出も扱います。

主な機能

  1. 01ナビゲーション可能なグラフの構築
  2. 02対話型 HTML の生成
  3. 03GraphRAG 対応 JSON の出力
  4. 04平易なレポートの作成
  5. 05抽出・推定リンクの記録
  6. 06増分更新への対応

活用シーン

  1. 01プロジェクトファイル間の関連探索
  2. 02GraphRAG 用データの準備
  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

任意のフォルダー内のファイルを、コミュニティ検出機能、正確な監査証跡、3 種類の出力(インタラクティブ 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 または複数パスの場合のみ)

パスが 1 つ以上の 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 をユーザーが指定した実際のパスに置き換えてください。JSON を cat したり表示したりしないでください。内容をひそかに読み取り、代わりにわかりやすい要約を提示してください。

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 の場合は、「No supported files found in [path].」で終了します。
  • skipped_sensitive が空でない場合は、件数とスキップされたファイル名の一覧を報告します。誤って判定されたソースやドキュメントが見えるため、名前を変更したり移動したりできます(#2106)。
  • total_words が 2,000,000 を超える場合、または total_files が 500 を超える場合は、警告を表示します。その後、ファイル数が多い上位 5 つの第 1 階層サブディレクトリを計算します。
    • 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 が video ファイルを 0 件返した場合は、このステップ全体をスキップします。コーパスに動画または音声が含まれている場合は、まず references/transcribe.md を参照してテキストに書き起こし、その後、ステップ 3 では書き起こしをドキュメントファイルとして扱います。

Step 3 - エンティティと関係性の抽出

開始前: --mode deep が指定されていたか確認します。指定されていた場合、Step B2 の各サブエージェントに DEEP_MODE=true を渡す必要があります。最初の呼び出しでの指定を追跡し、見失わないでください。

このステップは、構造抽出(決定的で無料)と意味抽出(LLM を使用し、トークンを消費)の2つのパートで構成されます。

graphify に API キーは不要です。ユーザーに API キーを求めたり、API キーがないことで処理を中断したりしないでください。 コードは LLM も API キーも使わず、構造的に(AST)抽出されます。コードのみのコーパス(リポジトリで一般的な /graphify .)では意味抽出を完全にスキップするため、ここでは何も必要ありません。Part A に進み、Part B はスキップしてください。意味抽出(ドキュメント、論文、画像のみ)では、GEMINI_API_KEY/GOOGLE_API_KEY がすでに設定されている場合に限り Gemini を使用し、そうでなければホストエージェント自体が LLM になります。graphify は ANTHROPIC_API_KEY、OPENAI_API_KEY、その他のプロバイダーの API キーを読み取りません。API キーがないことを理由に、入力を求めたり、待機したり、処理を止めたりしそうになった場合は、このスキルを読み違えています。API キーなしで続行してください。

意味抽出の前に: GEMINI_API_KEY または GOOGLE_API_KEY が設定されているか確認します。どちらも設定されていない場合は、次の一文をユーザーに表示します。

ヒント:意味抽出で Gemini を使用するには、GEMINI_API_KEY または GOOGLE_API_KEY を設定してください(pip install 'graphifyy[gemini]')。

一度だけ表示し、そのまま続行してください。ユーザーが API キーを入力するのを待たないでください。GEMINI_API_KEY または GOOGLE_API_KEY が設定されている場合は、サブエージェントを呼び出す代わりに、意味抽出で graphify.llm.extract_corpus_parallel(files, backend="gemini") を使用してください。デフォルトの Gemini モデルは gemini-3-flash-preview です。ヘッドレス CLI のフローでモデルを変更するには、GRAPHIFY_GEMINI_MODEL を設定するか、--model を渡してください。

その他の API キーは読み取られません。 GEMINI_API_KEY/GOOGLE_API_KEY が設定されていない場合、意味抽出はホストエージェント自体が行います。実行中のセッションが LLM になります。サブエージェントを呼び出せるホスト(例:Claude Code)では、Part B に記載のとおり呼び出してください。ターミナルで CLI を直接実行し、サブエージェントを呼び出せないホストでは、処理を停滞させないでください。コードのみのコーパスには意味抽出の作業がないため、空の意味抽出ファイルを作成し(Part B の「高速パス」を参照)、Part C に進みます。ドキュメント/論文/画像を含むコーパスの場合は、Gemini の API キーを設定するか、それらをインラインで抽出してください。ただし、いかなる場合も ANTHROPIC_API_KEY を求めないでください。求めるのは、このスキルの読み違えです。

Part A(AST)と Part B(意味抽出)を並行して実行してください。すべての意味抽出サブエージェントを呼び出すと同時に、AST 抽出も開始します。異なる種類のファイルを処理するため、両方を同時に実行できます。これまでどおり、Part C で結果を統合してください。

注:AST と意味抽出を並行化すると、大規模なコーパスで 5~15 秒短縮できます。AST は決定的かつ高速です。サブエージェントがドキュメント/論文を処理している間に開始してください。

Part A - コードファイルの構造抽出

検出されたコードファイルがある場合は、Part B のサブエージェントと並行して AST 抽出を実行します。

$(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')
"
Part B - 意味抽出(サブエージェントの並列実行)

高速パス: 検出されたドキュメント、論文、画像がゼロ(コードのみのコーパス)の場合は、Part B 全体をスキップし、Part C に進みます。AST がコードを処理するため、意味抽出サブエージェントの作業はありません。**まず空の意味抽出ファイルを作成してください。**Part 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 ツールを使用してください。ファイルを1つずつ自分で読むのは禁止です。5~10倍遅くなります。Agent ツールを使用しない場合、手順を誤っています。

サブエージェントを呼び出す前に、所要時間の見積もりを表示します。

  • graphify-out/.graphify_detect.json から total_words とファイル数を読み込みます
  • 必要なエージェント数を見積もります:ceil(未キャッシュの非コードファイル数 / 22)(チャンクサイズは20~25)
  • 所要時間を見積もります:エージェントのバッチごとに約45秒(並列実行されるため、合計は約45秒 × ceil(エージェント数 / 並列数上限))
  • 「Semantic extraction: ~N files → X agents, estimated ~Ys」と表示します

Step B0 - まず抽出キャッシュを確認 サブエージェントをディスパッチする前に、抽出結果がすでにキャッシュされているファイルを確認してください。

以下の SPEC_PATH は、この SKILL.md と同じ場所にある references/extraction-spec.md の絶対パスです。これは Step B2 が読み込み、各サブエージェントに渡すファイルと同じものです。これは抽出プロンプトであるため、キャッシュエントリはこのファイルに基づいて管理されます。graphify のアップグレードでプロンプトが変更されると、古いプロンプトで生成されたエントリは再抽出され、変更されていないプロンプトのエントリはそのまま保持されます(#1939)。Step B0 と Step 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 に記載されたファイルだけをサブエージェントにディスパッチしてください。すべてのファイルがキャッシュ済みの場合は、Step B1 と B2 をスキップしますが、Step B3 のコマンドは実行してください。Part B で graphify-out/.graphify_semantic.json に書き込むのはマージ処理だけであり、Part C はこのファイルを無条件に読み込むためです。代わりに空の .graphify_semantic.json を書き込まないでください。キャッシュ済みのノードがすべて失われます。

Step B1 - チャンクに分割する

graphify-out/.graphify_uncached.txt からファイルを読み込みます。20~25 個ずつのチャンクに分割します。各画像は個別のチャンクにしてください(画像の解析には独立したコンテキストが必要です)。分割する際は、同じディレクトリにあるファイルをまとめてください。関連する成果物を同じチャンクに含めることで、ファイル間の関係が抽出されやすくなります。

Step B2 - すべてのサブエージェントを1つのメッセージでディスパッチする

Agent ツールを複数回、同じ応答内で呼び出してください。チャンクごとに1回ずつ呼び出します。並列実行するには、これが唯一の方法です。Agent を1回呼び出して完了を待ってから、もう1回呼び出すと、順次実行となり、並列実行の利点が失われます。

重要 - サブエージェントのタイプ: 常に subagent_type="general-purpose" を使用してください。Explore は使用しないでください。読み取り専用であり、チャンクファイルをディスクに書き込めないため、抽出結果が気付かれないまま失われます。general-purpose は、サブエージェントに必要な 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"]

3つすべてを1つのメッセージ内で呼び出してください。別々のメッセージに分けないでください。

各サブエージェントには、次のプロンプトをそのまま渡します(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"

サブエージェントのプロンプトテンプレート:

正確なサブエージェントプロンプト(JSON スキーマ、ノード ID の規則、信頼度の評価基準、フロントマター、ハイパーエッジ、画像解析の規則)については、references/extraction-spec.md を参照してください。これを読み込むのはここだけで、ドキュメント、論文、または画像を含むチャンクが少なくとも1つある場合に限ります。コードのみのコーパスでは Part B がスキップされ、このファイルも読み込まれません。FILE_LIST、CHUNK_NUM、TOTAL_CHUNKS、DEEP_MODE、CHUNK_PATH を置き換えたうえで、このプロンプトをそのまま各サブエージェントに渡し、結果を CHUNK_PATH に書き込ませてください。

Step B3 - 収集、キャッシュ、マージする

すべてのサブエージェントが完了するまで待ちます。各結果について、次の処理を行います。

  • graphify-out/.graphify_chunk_NN.json がディスク上に存在することを確認します。これが成功の判定基準です。
  • ファイルが存在し、nodes と edges を含む有効な JSON である場合は、そのファイルをマージ対象に含め、キャッシュに保存します。
  • ファイルが見つからない場合は、サブエージェントが読み取り専用(Explore タイプ)としてディスパッチされた可能性があります。警告 "chunk N missing from disk — subagent may have been read-only. Re-run with general-purpose agent." を表示してください。黙ってスキップしないでください。
  • サブエージェントが失敗した場合、または無効な JSON を返した場合は、警告を表示してそのチャンクをスキップします。処理全体は中止しないでください。

失敗したチャンク、またはファイルが見つからないチャンクが半数を超える場合は、処理を中止し、subagent_type="general-purpose" が使用されるようにして再実行することをユーザーに伝えてください。

すべてのチャンクファイルを .graphify_semantic_new.json にマージします。各 Agent 呼び出しの完了後、Agent ツールの結果にある usage フィールドから実際のトークン数を読み取り、マージする前にチャンク 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')
"

新しい結果をキャッシュに保存します。Step 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 - グラフの構築、クラスタリング、分析、出力の生成

開始する前に: 以下のコードブロックは build_from_json() に directed=IS_DIRECTED を渡します。--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 が出力された場合は、最終サマリーでそれを提示してください (中断しないでください - グラフはまだ使用可能ですが、Honesty Rules に従い、整合性の問題を可視化する必要があります)。

Step 5 - コミュニティのラベリング

graphify-out/.graphify_analysis.json を読み取ります。各コミュニティキーについて、そのノードラベルを確認し、2〜5 語の平易な名前を書きます (例: "Attention Mechanism"、"Training Pipeline"、"Data Loading")。

次に、レポートを再生成し、ビジュアライザー用にラベルを保存します:

$(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 を構築した実際の dict に置き換えてください (例: {0: "Attention Mechanism", 1: "Training Pipeline"})。 INPUT_PATH を実際のパスに置き換えてください。

Step 6 - Obsidian vault (オプトイン) + HTML の生成

HTML は常に生成します (--no-viz の場合を除く)。Obsidian vault は --obsidian が明示的に指定された場合のみ生成します - それ以外の場合はスキップしてください。ノードごとに 1 ファイルを生成します。

--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 を参照してください。.graphify_labels.json がまだ利用可能であるように、--wiki エクスポートは Step 9 のクリーンアップの前に実行してください。


ステップ 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 に表示されます。ユーザーは .gitignore に graphify-out/ を追加するか、チームで共有すべき出力だけをコミットできます(https://docs.graphify.com/guides/team-workflows を参照)。

GRAPH_REPORT.md の次のセクションをチャットにそのまま貼り付けます。

  • 重要なノード
  • 意外なつながり
  • 提案する質問

レポート全体は貼り付けないでください。この 3 つのセクションだけにします。簡潔にまとめてください。

その後すぐに、探索を続けることを提案します。レポートから最も興味深い質問を 1 つ選びます。コミュニティの境界を最も多くまたぐもの、または最も意外な橋渡しノードを持つものを選び、次のように尋ねます。

「このグラフで答えられる最も興味深い質問は、[question] です。たどってみますか?」

ユーザーが了承したら、グラフに対して /graphify query "[question]" を実行し、グラフの構造を使って回答を説明します。どのノードがつながっているか、どのコミュニティの境界を越えているか、その経路から何が分かるかを伝えます。ユーザーが探索を続けたい限り、続けます。各回答の最後には「これは X につながっています。さらに掘り下げますか?」のように自然なフォローアップを加え、1 回限りのレポートではなく、ナビゲーションのように感じられるセッションにします。

グラフは地図です。パイプライン実行後のあなたの役割は、案内役です。


サブコマンドのインタープリター確認

以下のサブコマンド(--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 可視化を実行してはいけません。

コメント