mirror of
https://github.com/safishamsi/graphify.git
synced 2026-08-28 09:17:02 +00:00
fix: make graph output deterministic (stop graphify-out churn) (#1010)
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>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
d1d5751fa4
commit
a54a542b6c
+12
-1
@@ -160,7 +160,18 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat
|
||||
# slightly different casing or punctuation than the AST extractor.
|
||||
# e.g. "Session_ValidateToken" maps to "session_validatetoken".
|
||||
norm_to_id: dict[str, str] = {_normalize_id(nid): nid for nid in node_set}
|
||||
for edge in extraction.get("edges", []):
|
||||
# Iterate edges in a deterministic order. The graph is undirected and stores
|
||||
# direction in _src/_tgt; when two edges collapse onto the same node pair the
|
||||
# last write wins, so an unstable iteration order flips _src/_tgt run-to-run
|
||||
# and makes the serialized graph churn. Sorting fixes the last-write outcome.
|
||||
for edge in sorted(
|
||||
extraction.get("edges", []),
|
||||
key=lambda e: (
|
||||
str(e.get("source", e.get("from", ""))),
|
||||
str(e.get("target", e.get("to", ""))),
|
||||
str(e.get("relation", "")),
|
||||
),
|
||||
):
|
||||
if "source" not in edge and "from" in edge:
|
||||
edge["source"] = edge["from"]
|
||||
if "target" not in edge and "to" in edge:
|
||||
|
||||
@@ -48,6 +48,11 @@ _HOOK_SCRIPT = """\
|
||||
# Auto-rebuilds the knowledge graph after each commit (code files only, no LLM needed).
|
||||
# Installed by: graphify hook install
|
||||
|
||||
# Deterministic clustering: networkx louvain iterates string-keyed sets whose
|
||||
# order is randomized per-process by PYTHONHASHSEED, so community assignments
|
||||
# churn run-to-run. Pinning it makes graphify-out reproducible.
|
||||
export PYTHONHASHSEED=0
|
||||
|
||||
# Skip during rebase/merge/cherry-pick to avoid blocking --continue with unstaged changes
|
||||
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
|
||||
[ -d "$GIT_DIR/rebase-merge" ] && exit 0
|
||||
@@ -106,6 +111,11 @@ _CHECKOUT_SCRIPT = """\
|
||||
# Auto-rebuilds the knowledge graph (code only) when switching branches.
|
||||
# Installed by: graphify hook install
|
||||
|
||||
# Deterministic clustering: networkx louvain iterates string-keyed sets whose
|
||||
# order is randomized per-process by PYTHONHASHSEED, so community assignments
|
||||
# churn run-to-run. Pinning it makes graphify-out reproducible.
|
||||
export PYTHONHASHSEED=0
|
||||
|
||||
PREV_HEAD=$1
|
||||
NEW_HEAD=$2
|
||||
BRANCH_SWITCH=$3
|
||||
|
||||
Reference in New Issue
Block a user