Skip to content

graphify update desyncs GRAPH_REPORT.md and graph.html when refusing to overwrite graph.json #562

Description

@ndany

Summary

After a full /graphify <path> build, subsequent graphify update <path> calls
produce inconsistent output: graph.json is correctly preserved (the "Refusing
to overwrite" guard fires), but GRAPH_REPORT.md and graph.html are
regenerated from the smaller AST-only working set anyway. Result: the derived
artifacts describe fewer nodes/communities than graph.json actually contains.

This is a silent desync — the warning is logged but the artifacts are written
without further indication.

Reproduction

On a corpus with both code and docs (so the semantic cache is smaller than
the merged graph):

  1. /graphify <path> to build a full graph (AST + semantic). Observe e.g.
    graph.json = 1955 nodes / 4171 edges / 139 communities, with
    GRAPH_REPORT.md reporting the same.

  2. Make any small change (e.g. edit a .md file at the root).

  3. Run graphify update <path> directly, OR install the post-commit hook
    (graphify hook install) and commit.

  4. Observe output:

    [graphify hook] 1 file(s) changed - rebuilding graph...
      AST extraction: 100/183 files (54%)
      AST extraction: 183/183 files (100%)
    [graphify] WARNING: new graph has 1772 nodes but existing graph.json
      has 1955. Refusing to overwrite — you may be missing chunk files
      from a previous session. Pass force=True to override.
    [graphify watch] Rebuilt: 1772 nodes, 3484 edges, 118 communities
    [graphify watch] graph.json, graph.html and GRAPH_REPORT.md
      updated in graphify-out
    
  5. Inspect afterwards:

    • graph.json → still 1955 nodes (correctly preserved)
    • GRAPH_REPORT.md → reports 1772 nodes · 3484 edges · 43 communities
      (regenerated from the smaller view)
    • graph.html → same — visualizes the smaller graph

Expected behavior

When graphify update decides to refuse overwriting graph.json (because the
new view has fewer nodes), it should apply the same protection to derived
artifacts. Two reasonable fixes:

  • (a) Skip regeneration of GRAPH_REPORT.md and graph.html entirely
    when refusing the merge.
  • (b) Regenerate GRAPH_REPORT.md and graph.html from the preserved
    graph.json (re-cluster + render), so the artifacts stay consistent with
    the data file.

Either resolution avoids the silent desync.

Actual behavior

graph.json is preserved. GRAPH_REPORT.md and graph.html are clobbered
with the smaller view's data. The desync is not flagged in the success
message, which says "graph.json, graph.html and GRAPH_REPORT.md updated"
even though graph.json was not in fact updated.

Why this matters

Downstream consumers (Claude Code agents reading GRAPH_REPORT.md for god
nodes / community structure, viewers opening graph.html) trust the report
to describe the graph. Silent disagreement between the report and the
underlying data leads to incomplete answers and potentially incorrect
navigation.

The post-commit hook (graphify hook install) makes this fire on every
commit, so the desync is perpetual once a corpus has more semantic nodes
than the AST cache covers (which is the normal case for any project with
documentation).

Workaround (for users)

Until fixed, regenerate report/HTML directly from graph.json after any
graphify update:

import json
from pathlib import Path
from networkx.readwrite import json_graph
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_html

data = json.loads(Path('graphify-out/graph.json').read_text())
G = json_graph.node_link_graph(data, edges='links')
communities = cluster(G)
cohesion = score_all(G, communities)
gods = god_nodes(G)
surprises = surprising_connections(G, communities)
labels = {cid: f'Community {cid}' for cid in communities}  # or curated labels
questions = suggest_questions(G, communities, labels)

report = generate(
    G, communities, cohesion, labels, gods, surprises,
    detection={'total_files': 0, 'total_words': 0, 'needs_graph': True,
               'warning': None, 'files': {'code': [], 'document': [],
               'paper': [], 'image': []}},
    tokens={'input': 0, 'output': 0},
    project_path='.',
    suggested_questions=questions,
)
Path('graphify-out/GRAPH_REPORT.md').write_text(report)
to_html(G, communities, 'graphify-out/graph.html', community_labels=labels)

Environment

  • graphifyy 0.5.0 (pip)
  • Python 3.13
  • macOS Darwin 25.4.0
  • Corpus: 251 files (183 code, 63 docs, 5 images), ~268k words

Notes

  • Initial full /graphify . run produced the 1955-node graph cleanly.
  • Bug surfaces on the first incremental update afterward.
  • graph.json mtime stays at the original-build time, while
    GRAPH_REPORT.md and graph.html mtimes update — easy way to confirm
    the desync at the filesystem level.

Happy to test a patch if helpful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions