Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
- Fix: the `--update` runbook no longer marks a semantic file as done when its extraction produced no output (#2015). Step 9 stamped the entire detected corpus into the manifest, so a doc, paper, or image whose chunk failed or was omitted was recorded as complete and never re-queued on the next update, losing its content permanently. The runbook now builds the manifest with the same stamping the library uses (only files that actually produced nodes, edges, or hyperedges are stamped; dispatched-but-empty files have their stale hash cleared so they are retried), across the Claude, Aider, and Devin skill bodies and the shared update reference.
- Fix: `build_merge` (the `--update` runbook path) now prunes a deleted file's nodes, edges, and hyperedges regardless of whether their stored `source_file` is absolute or relative (#2012). When the caller passed no scan root, a node that had kept an absolute path slipped past the relative prune set and the deleted file's graph survived silently. Matching is now form-insensitive (raw, normalized-relative, then an absolute-identity fallback), a re-extracted file is still never pruned, and `graphify extract` records the scan root marker after every write so a later update relativizes paths correctly even under a custom `--out`.

- Fix: `graph.html` is now part of the dated pre-overwrite snapshot (`graphify-out/YYYY-MM-DD/`). `backup_if_protected` preserved every other artifact of a protected graph before a rebuild overwrote it, but not the interactive viewer — so each run silently destroyed the only browsable copy of the previous graph, and the dated folders could not be opened in a browser at all.
## 0.9.20 (2026-07-18)

- Fix: the graphify-first search nudge now fires on Claude Code's dedicated **Grep tool**, not just Bash (#1986, thanks @mdshzb04). The installed PreToolUse hook only matched `Bash`, so a `Grep` tool call (whose `tool_input` is `{pattern, path, glob, ...}`, not `{command}`) slipped through and never got nudged toward `graphify query`. The matcher is now `Bash|Grep` and the search guard recognizes the Grep shape; it stays nudge-only (never the strict deny), and the uninstall filters + #1840 gating are unchanged.
Expand Down
3 changes: 3 additions & 0 deletions graphify/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
"manifest.json",
".graphify_semantic_marker",
"cost.json",
# The rendered viewer is regenerable, but only by re-running the build it
# belongs to — without it the dated folder can't be opened in a browser.
"graph.html",
]


Expand Down
11 changes: 10 additions & 1 deletion graphify/skill-amp.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ 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 graphify.export import to_json, backup_if_protected
from pathlib import Path

extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
Expand All @@ -423,6 +423,15 @@ labels = {cid: 'Community ' + str(cid) for cid in communities}
# Placeholder questions - regenerated with real labels in Step 5
questions = suggest_questions(G, communities, labels)

# Snapshot the previous run into graphify-out/<date>/ before anything overwrites
# it. This is the same call the CLI makes in watch._rebuild_code; without it the
# skill path silently destroys the old graph.json / GRAPH_REPORT.md / graph.html.
# Runs after the empty-graph guard (nothing to protect if we are about to abort)
# and before to_json, so it captures graph.html too — Step 6 rewrites that later.
# No-ops unless the graph is protected (semantic marker or curated labels), never
# raises, and GRAPHIFY_NO_BACKUP=1 disables it.
backup_if_protected(Path('graphify-out'))

# 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
Expand Down
11 changes: 10 additions & 1 deletion graphify/skill-claw.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ 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 graphify.export import to_json, backup_if_protected
from pathlib import Path

extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
Expand All @@ -426,6 +426,15 @@ labels = {cid: 'Community ' + str(cid) for cid in communities}
# Placeholder questions - regenerated with real labels in Step 5
questions = suggest_questions(G, communities, labels)

# Snapshot the previous run into graphify-out/<date>/ before anything overwrites
# it. This is the same call the CLI makes in watch._rebuild_code; without it the
# skill path silently destroys the old graph.json / GRAPH_REPORT.md / graph.html.
# Runs after the empty-graph guard (nothing to protect if we are about to abort)
# and before to_json, so it captures graph.html too — Step 6 rewrites that later.
# No-ops unless the graph is protected (semantic marker or curated labels), never
# raises, and GRAPHIFY_NO_BACKUP=1 disables it.
backup_if_protected(Path('graphify-out'))

# 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
Expand Down
11 changes: 10 additions & 1 deletion graphify/skill-codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ 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 graphify.export import to_json, backup_if_protected
from pathlib import Path

extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
Expand All @@ -423,6 +423,15 @@ labels = {cid: 'Community ' + str(cid) for cid in communities}
# Placeholder questions - regenerated with real labels in Step 5
questions = suggest_questions(G, communities, labels)

# Snapshot the previous run into graphify-out/<date>/ before anything overwrites
# it. This is the same call the CLI makes in watch._rebuild_code; without it the
# skill path silently destroys the old graph.json / GRAPH_REPORT.md / graph.html.
# Runs after the empty-graph guard (nothing to protect if we are about to abort)
# and before to_json, so it captures graph.html too — Step 6 rewrites that later.
# No-ops unless the graph is protected (semantic marker or curated labels), never
# raises, and GRAPHIFY_NO_BACKUP=1 disables it.
backup_if_protected(Path('graphify-out'))

# 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
Expand Down
11 changes: 10 additions & 1 deletion graphify/skill-copilot.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ 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 graphify.export import to_json, backup_if_protected
from pathlib import Path

extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
Expand All @@ -426,6 +426,15 @@ labels = {cid: 'Community ' + str(cid) for cid in communities}
# Placeholder questions - regenerated with real labels in Step 5
questions = suggest_questions(G, communities, labels)

# Snapshot the previous run into graphify-out/<date>/ before anything overwrites
# it. This is the same call the CLI makes in watch._rebuild_code; without it the
# skill path silently destroys the old graph.json / GRAPH_REPORT.md / graph.html.
# Runs after the empty-graph guard (nothing to protect if we are about to abort)
# and before to_json, so it captures graph.html too — Step 6 rewrites that later.
# No-ops unless the graph is protected (semantic marker or curated labels), never
# raises, and GRAPHIFY_NO_BACKUP=1 disables it.
backup_if_protected(Path('graphify-out'))

# 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
Expand Down
11 changes: 10 additions & 1 deletion graphify/skill-droid.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ 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 graphify.export import to_json, backup_if_protected
from pathlib import Path

extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
Expand All @@ -423,6 +423,15 @@ labels = {cid: 'Community ' + str(cid) for cid in communities}
# Placeholder questions - regenerated with real labels in Step 5
questions = suggest_questions(G, communities, labels)

# Snapshot the previous run into graphify-out/<date>/ before anything overwrites
# it. This is the same call the CLI makes in watch._rebuild_code; without it the
# skill path silently destroys the old graph.json / GRAPH_REPORT.md / graph.html.
# Runs after the empty-graph guard (nothing to protect if we are about to abort)
# and before to_json, so it captures graph.html too — Step 6 rewrites that later.
# No-ops unless the graph is protected (semantic marker or curated labels), never
# raises, and GRAPHIFY_NO_BACKUP=1 disables it.
backup_if_protected(Path('graphify-out'))

# 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
Expand Down
11 changes: 10 additions & 1 deletion graphify/skill-kilo.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ 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 graphify.export import to_json, backup_if_protected
from pathlib import Path

extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
Expand All @@ -426,6 +426,15 @@ labels = {cid: 'Community ' + str(cid) for cid in communities}
# Placeholder questions - regenerated with real labels in Step 5
questions = suggest_questions(G, communities, labels)

# Snapshot the previous run into graphify-out/<date>/ before anything overwrites
# it. This is the same call the CLI makes in watch._rebuild_code; without it the
# skill path silently destroys the old graph.json / GRAPH_REPORT.md / graph.html.
# Runs after the empty-graph guard (nothing to protect if we are about to abort)
# and before to_json, so it captures graph.html too — Step 6 rewrites that later.
# No-ops unless the graph is protected (semantic marker or curated labels), never
# raises, and GRAPHIFY_NO_BACKUP=1 disables it.
backup_if_protected(Path('graphify-out'))

# 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
Expand Down
11 changes: 10 additions & 1 deletion graphify/skill-kiro.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ 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 graphify.export import to_json, backup_if_protected
from pathlib import Path

extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
Expand All @@ -426,6 +426,15 @@ labels = {cid: 'Community ' + str(cid) for cid in communities}
# Placeholder questions - regenerated with real labels in Step 5
questions = suggest_questions(G, communities, labels)

# Snapshot the previous run into graphify-out/<date>/ before anything overwrites
# it. This is the same call the CLI makes in watch._rebuild_code; without it the
# skill path silently destroys the old graph.json / GRAPH_REPORT.md / graph.html.
# Runs after the empty-graph guard (nothing to protect if we are about to abort)
# and before to_json, so it captures graph.html too — Step 6 rewrites that later.
# No-ops unless the graph is protected (semantic marker or curated labels), never
# raises, and GRAPHIFY_NO_BACKUP=1 disables it.
backup_if_protected(Path('graphify-out'))

# 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
Expand Down
11 changes: 10 additions & 1 deletion graphify/skill-opencode.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ 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 graphify.export import to_json, backup_if_protected
from pathlib import Path

extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
Expand All @@ -418,6 +418,15 @@ labels = {cid: 'Community ' + str(cid) for cid in communities}
# Placeholder questions - regenerated with real labels in Step 5
questions = suggest_questions(G, communities, labels)

# Snapshot the previous run into graphify-out/<date>/ before anything overwrites
# it. This is the same call the CLI makes in watch._rebuild_code; without it the
# skill path silently destroys the old graph.json / GRAPH_REPORT.md / graph.html.
# Runs after the empty-graph guard (nothing to protect if we are about to abort)
# and before to_json, so it captures graph.html too — Step 6 rewrites that later.
# No-ops unless the graph is protected (semantic marker or curated labels), never
# raises, and GRAPHIFY_NO_BACKUP=1 disables it.
backup_if_protected(Path('graphify-out'))

# 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
Expand Down
11 changes: 10 additions & 1 deletion graphify/skill-pi.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ 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 graphify.export import to_json, backup_if_protected
from pathlib import Path

extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
Expand All @@ -426,6 +426,15 @@ labels = {cid: 'Community ' + str(cid) for cid in communities}
# Placeholder questions - regenerated with real labels in Step 5
questions = suggest_questions(G, communities, labels)

# Snapshot the previous run into graphify-out/<date>/ before anything overwrites
# it. This is the same call the CLI makes in watch._rebuild_code; without it the
# skill path silently destroys the old graph.json / GRAPH_REPORT.md / graph.html.
# Runs after the empty-graph guard (nothing to protect if we are about to abort)
# and before to_json, so it captures graph.html too — Step 6 rewrites that later.
# No-ops unless the graph is protected (semantic marker or curated labels), never
# raises, and GRAPHIFY_NO_BACKUP=1 disables it.
backup_if_protected(Path('graphify-out'))

# 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
Expand Down
11 changes: 10 additions & 1 deletion graphify/skill-trae.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ 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 graphify.export import to_json, backup_if_protected
from pathlib import Path

extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
Expand All @@ -424,6 +424,15 @@ labels = {cid: 'Community ' + str(cid) for cid in communities}
# Placeholder questions - regenerated with real labels in Step 5
questions = suggest_questions(G, communities, labels)

# Snapshot the previous run into graphify-out/<date>/ before anything overwrites
# it. This is the same call the CLI makes in watch._rebuild_code; without it the
# skill path silently destroys the old graph.json / GRAPH_REPORT.md / graph.html.
# Runs after the empty-graph guard (nothing to protect if we are about to abort)
# and before to_json, so it captures graph.html too — Step 6 rewrites that later.
# No-ops unless the graph is protected (semantic marker or curated labels), never
# raises, and GRAPHIFY_NO_BACKUP=1 disables it.
backup_if_protected(Path('graphify-out'))

# 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
Expand Down
11 changes: 10 additions & 1 deletion graphify/skill-vscode.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ 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 graphify.export import to_json, backup_if_protected
from pathlib import Path

extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
Expand All @@ -422,6 +422,15 @@ labels = {cid: 'Community ' + str(cid) for cid in communities}
# Placeholder questions - regenerated with real labels in Step 5
questions = suggest_questions(G, communities, labels)

# Snapshot the previous run into graphify-out/<date>/ before anything overwrites
# it. This is the same call the CLI makes in watch._rebuild_code; without it the
# skill path silently destroys the old graph.json / GRAPH_REPORT.md / graph.html.
# Runs after the empty-graph guard (nothing to protect if we are about to abort)
# and before to_json, so it captures graph.html too — Step 6 rewrites that later.
# No-ops unless the graph is protected (semantic marker or curated labels), never
# raises, and GRAPHIFY_NO_BACKUP=1 disables it.
backup_if_protected(Path('graphify-out'))

# 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
Expand Down
Loading