diff --git a/graphify/build.py b/graphify/build.py index f180419d..a4d33c5e 100644 --- a/graphify/build.py +++ b/graphify/build.py @@ -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: diff --git a/graphify/hooks.py b/graphify/hooks.py index 4792319d..9746737f 100644 --- a/graphify/hooks.py +++ b/graphify/hooks.py @@ -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