graphify-out regenerates differently on every `graphify update` even
when no source changed, so the committed graph is perpetually dirty and
the post-commit/post-checkout hooks fight every commit. Two independent
nondeterminism sources, each fixed here:
1. Edge direction flips. build.py builds an undirected graph and stores
direction in _src/_tgt; collapsing two edges onto the same node pair
is last-write-wins, and unstable edge iteration order flips them
run-to-run. Fixed by sorting edges by (source, target, relation)
before the add loop.
2. Clustering churn. The networkx Louvain fallback iterates string-keyed
sets whose order is randomized per-process by PYTHONHASHSEED, so
community assignments differ run-to-run even with seed=42. Fixed by
exporting PYTHONHASHSEED=0 in the generated post-commit and
post-checkout hook scripts.
With both fixes, `graphify update` is idempotent: rebuilding an
already-converged graphify-out reproduces graph.json and GRAPH_REPORT.md
byte-for-byte.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
- hooks.py: add _user_hooks_dir() to target .husky/ instead of .husky/_ on Husky 9 repos (#987)
- skill.md: replace unsubstituted 'INPUT_PATH' literal with '.' in generate() calls (#986)
- extract.py: wrap future.result() per-future so a single worker failure prints a warning instead of falling back to full sequential re-extraction (#943)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- hooks.py: drop --path-format=absolute (added git 2.31), validate no newlines in path, anchor relative paths on repo root (#907)
- detect.py: seed save_manifest from existing manifest before loop so incremental callers don't erase untouched file entries (#917)
- cluster.py: drop round(..., 2) from cohesion_score so split threshold 0.05 fires correctly; add resolution param to _partition and cluster; add exclude_hubs_percentile to cluster with majority-vote reattachment (#919)
- report.py: format cohesion with :.2f for display
- __main__.py: wire --resolution and --exclude-hubs into extract and cluster-only commands (#919)
- C++ inheritance already written to disk by analysis agent (#915)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In a linked worktree .git is a file not a directory, so root/.git/hooks
fails with NotADirectoryError. Fall back to git rev-parse --path-format=absolute
--git-path hooks which resolves the correct hooks dir from any worktree.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
#791: per-repo fcntl flock in _rebuild_code prevents concurrent hook rebuilds
from exhausting memory; changed_paths wired through so only modified files are
re-extracted; stale nodes evicted on deletion; SIGALRM watchdog with
GRAPHIFY_REBUILD_TIMEOUT; Darwin-aware RLIMIT_DATA memory cap
#792: remove hard 8-worker cap (GRAPHIFY_MAX_WORKERS env var); add
--max-workers, --token-budget, --max-concurrency, --api-timeout CLI flags
to graphify extract; fix ollama API key gate for loopback URLs; explicit
timeout on OpenAI client (GRAPHIFY_API_TIMEOUT, default 600s); per-chunk
progress prints during extraction
#453 + #785: rename .agent -> .agents throughout antigravity install/uninstall;
add trigger:always_on YAML frontmatter to _ANTIGRAVITY_RULES so Antigravity
recognises the rules file
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bypasses the node-count safety check in to_json for refactors that
legitimately shrink the graph (renames, package deletions). Also
honored by post-commit and post-checkout hooks via GRAPHIFY_FORCE=1.
Implements the approach from #639 (targeted at v5) adapted for v6.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
watch.py: filter preserved nodes by ID membership in new AST output instead
of file_type — INFERRED/AMBIGUOUS nodes on code files also carry file_type=code
and were being wrongly dropped, triggering the to_json safety check refusal.
hooks.py: detach post-commit and post-checkout rebuilds with nohup + disown
so git commit returns immediately instead of blocking for the full rebuild
duration. Rebuild log written to ~/.cache/graphify-rebuild.log.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- build/validate: accept NetworkX <=3.1 "links" key alongside "edges" (#212)
- __main__: skip version check during install/uninstall, deduplicate paths (#220)
- all file IO: explicit encoding="utf-8" to prevent crashes on Windows CJK locales (#204)
- hooks: add newline="\n" on write to prevent CRLF shebang breakage on Windows (#204)
- export: strip trailing .md from safe_name so "CLAUDE.md" doesn't become "CLAUDE.md.md" (#221)
- report: add Community Hubs navigation block so Obsidian vault stays connected (#221)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- extract.py: use str(path) for node IDs to prevent same-basename collision (#211)
- build.py: normalize from/to edge keys before KeyError (#216)
- export.py: guard ZeroDivisionError when graph has no edges (#217)
- hooks.py: remove stale CODE_EXTS filter, rebuild on any changed file (#222)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: git hooks fail when graphify is installed via pipx
When installed via pipx, the graphify module is only available in
pipx's isolated venv, not the system python3. The git hooks
(post-commit, post-checkout) hardcoded `python3` which cannot import
graphify in this case.
Detect the correct Python interpreter from the graphify binary's
shebang line, matching the approach already used in skill.md Step 1.
Falls back to python3 for system installs.
* fix: handle env-style shebangs and improve interpreter detection
- Use POSIX `command -v` instead of non-standard `which`
- Parse `#!/usr/bin/env python3` shebangs correctly (previous
`tr -d ' '` would produce `/usr/bin/envpython3`)
- Add import validation fallback to python3 if resolved interpreter
cannot import graphify