Skip to content

fix: write graph.json as one line of compact JSON (#3413) - #4319

Closed
andrebrait wants to merge 1 commit into
Graphify-Labs:v8from
pfBlockerNG:fix/compact-graph-json
Closed

andrebrait wants to merge 1 commit into
Graphify-Labs:v8from
pfBlockerNG:fix/compact-graph-json

Conversation

@andrebrait

@andrebrait andrebrait commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Closes #3413.

graphify-out/graph.json was written pretty-printed (indent=2). For a mid-size repo that is roughly a million lines, so every PR that commits a rebuilt graph reports 50k–160k changed lines.

GitHub does honor linguist-generated=true: the file is collapsed in the Files changed view and left out of the language statistics. It still counts every changed line toward the PR's additions/deletions totals, though, and no other attribute changes that either. Measured on pfBlockerNG/pfBlockerNG with linguist-generated=true in place, adding -diff, removing it, and marking the file binary all produced the same counts (for example +158987 −157008 for graph.json alone on one PR). This is why #3575 (attribute-only, closed by its author) could not fix #3413. Only the content of the file changes the count: one line of compact JSON shows as +1 −1.

This PR writes graph.json as one line of compact JSON (separators=(",", ":"), no indent) by default, at every writer of a project graph. There is no flag, env var, or config option. Readers are unaffected (json.load). Each site keeps its existing ensure_ascii and trailing-newline behaviour, so only whitespace changes.

Writers changed

  • export.to_json (clustered build, update, cluster-only, etc.)
  • watch._json_text — candidate text for the update --no-cluster path
  • cli._prune_graph_json_sources — --no-cluster exclusion prune
  • merge-driver output (the committed graph.json)
  • merge-graphs --out
  • extract --no-cluster
  • global_graph._save_global_graph — same node-link shape as a project graph; changed for consistency
  • paths.write_json_atomic gains a compact= keyword used by the above

Left alone on purpose: manifest, .graphify_labels.json, .graphify_analysis.json, global manifest, Obsidian/canvas exports, installer configs, reflect sidecars and the skill files. They are not a project graph.json and are not what bloats PR diffs. The skill snippets that write .graphify_*.json intermediates are untouched.

Existing indented graphs

update decides "nothing changed" by comparing parsed data (_canonical_graph_for_compare, _canonical_topology_for_compare), not bytes. An already-committed indented graph.json would therefore compare equal to the freshly built candidate and be left indented forever. _rebuild_code now checks whether the existing file is a single line; if not, the unchanged-graph / unchanged-topology skips are bypassed and the graph is rewritten compact once. A compact, unchanged graph is still left untouched.

Type of change

  • Bug fix

Verification & Invariants

Invariant protected: a project graph.json is one line, round-trips to the same data, and an unchanged compact graph is not rewritten.

tests/test_compact_graph_json.py (new, 12 cases) drives real builds and CLI subprocesses:

  • to_json, clustered update, update --no-cluster, extract --no-cluster, merge-graphs, prune, global graph: output is exactly one line with compact separators and loads to the expected nodes/edges.
  • merge-driver on two compact inputs: one line, union of nodes and links.
  • An existing indented graph is rewritten compact by update (clustered and no-cluster), with identical nodes/links.
  • Guard: an already-compact unchanged graph is not rewritten (inode and mtime unchanged).

Red/green, same unchanged test file (git hash-object 6673ef56cb441291b6355343e446ac9d3168ed61 before and after):

  • Unmodified upstream/v8 (b4e6307): 10 failed, 2 passed (the 2 passes are the "compact graph left untouched" guards). Failures are e.g. graph.json spans 82 lines, cur.json spans 41 lines.
  • Writers changed but update legacy handling not yet added: the two test_update_rewrites_existing_indented_graph_compact cases fail with graph.json spans 82 lines / 67 lines, proving that the data-equality skip alone leaves an indented graph in place.
  • With this change: 12 passed.

How was this tested?

uv run pytest tests/test_compact_graph_json.py -q          # red on v8: 10 failed, 2 passed; green: 12 passed
uv run pytest tests -q -k "merge_driver or merge_graphs or global"   # 83 passed, 7 skipped
uv run pytest tests -q                                      # 6965 passed, 168 skipped, 1 failed
uvx ruff check <touched files>                              # All checks passed

The one failure, tests/test_extract.py::test_collect_files_skips_hidden, fails identically on unmodified upstream/v8 in the same checkout (the worktree lives under a hidden directory), so it is unrelated.

Graphify-specific checklist

  • No skill fragments touched, so no skillgen run needed.
  • Extraction remains deterministic; only the serialized whitespace changes.
  • No unsafe interpolation into shell/Python.
  • No API keys or local-only graph data included.
  • AI authorship disclosed in the commit (Co-Authored-By: Claude).

Limitations: the first update after upgrading rewrites an existing indented graph.json once (one large diff in that commit), after which diffs are one line. The file is no longer human-readable without a formatter (python -m json.tool, jq .).

graph.json was pretty-printed (indent=2), so every PR that commits a
rebuilt graph reports tens of thousands of changed lines. GitHub's PR
line counts ignore .gitattributes (-diff, binary), so only the file's
content can fix it: one line of compact JSON is +1 -1.

Every writer of a project graph.json now emits separators=(",", ":")
with no indent: to_json, the `update` raw/no-cluster candidate text,
prune, merge-driver, merge-graphs, `extract --no-cluster` and the global
graph. Readers are unaffected.

`update` compares parsed data, not bytes, so an existing indented graph
would have been "unchanged" forever. It is now rewritten compact once,
then left alone.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Thanks for the pull request, @andrebrait. A maintainer will review it soon.

Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions.

A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic.

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

No issues found by static checks — no coupling regressions or blocking issues in the code graph. That is not the same as safe to merge: see what was not checked.

Not checked: tests were not run; formal verification proved 0 of 7 changed function(s) (2 sampled, not proven, 4 not verified).

Formal verification. 1 change(s) alter behavior, breaking input(s) attached. PR-changed functions: 3/7 verified (0 proven, 2 may-equivalent, 1 distinguished) · 4 not verified (1 vacuous, 3 unsupported).

Behavior changes: \_json\_text changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_json\_text behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"data":"\{'a': \{'b': 1\}\}"\}, the old code produced '\{\\n "a": \{\\n "b": 1\\n \}\\n\}\\n' but the new code produces '\{"a":\{"b":1\}\}\\n' (outputs first differ at character 3). Paste that input straight into a regression test.

Not verified on this run: \_prune\_graph\_json\_sources (unsupported), dispatch\_command (vacuous: never exercised), write\_json\_atomic (unsupported), \_rebuild\_code (unsupported).


Graphify review — findings

Writes every project graph.json as one compact line, with no padding after ,/:, so a committed rebuild no longer shows up as tens of thousands of changed lines. write_json_atomic gains a compact flag, which to_json, the global graph, the merge driver, merge-graphs and source pruning now use. During update, _rebuild_code treats an existing multi-line graph.json as changed even when its parsed data matches, so legacy indented graphs get rewritten once while already-compact, unchanged graphs are left alone.

No blocking issues surfaced. 2 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 3261 functions depend on the 262 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: _rebuild_code() — 166 callers, 57 callees
  • worse: to_json() — 66 callers, 9 callees

Verification — 3261 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 2476 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

151 of 368 test file(s) selected (41%) via static blast radius.

  • tests/test_affected_cli.py — impact
  • tests/test_agents_platform.py — impact
  • tests/test_analyze.py — impact
  • tests/test_atomic_canvas_export.py — impact
  • tests/test_atomic_version_stamp.py — impact
  • tests/test_atomic_writes.py — impact
  • tests/test_benchmark.py — impact
  • tests/test_benchmark_raw_graph.py — impact
  • tests/test_build.py — impact
  • tests/test_build_located_semantic_identity.py — impact
  • tests/test_build_merge_dedup_scope.py — impact
  • tests/test_build_merge_hyperedges_and_prune.py — impact
  • tests/test_build_merge_shrink_guard.py — impact
  • tests/test_cache.py — impact
  • tests/test_cache_stale_import_target.py — impact
  • tests/test_callflow_html.py — impact
  • tests/test_carried_hyperedge_remap.py — impact
  • tests/test_charmap_encoding.py — impact
  • tests/test_chunking.py — impact
  • tests/test_cli_export.py — impact
  • tests/test_cloud_cta.py — impact
  • tests/test_cluster.py — impact
  • tests/test_codebuddy.py — impact
  • tests/test_community_labels_skill.py — impact
  • tests/test_compact_graph_json.py — impact, changed-test
  • tests/test_confidence.py — impact
  • tests/test_corrupt_graph_json.py — impact
  • tests/test_cpp_objc_cross_file_calls.py — impact
  • tests/test_cross_extension_reexport_self_cycle.py — impact
  • tests/test_csharp_object_creation.py — impact
  • tests/test_dedup.py — impact
  • tests/test_dedup_remaps_hyperedges.py — impact
  • tests/test_dedup_shrink_refuses_force_write.py — impact
  • tests/test_definition_file_portability.py — impact
  • tests/test_detect.py — impact
  • tests/test_devin.py — impact
  • tests/test_duplicate_annotation_edges.py — impact
  • tests/test_elixir_import_resolution.py — impact
  • tests/test_elixir_qualified_calls.py — impact
  • tests/test_elixir_unqualified_call_scope.py — impact
  • tests/test_evidence_binding.py — impact
  • tests/test_explain_cli.py — impact
  • tests/test_export.py — impact
  • tests/test_export_control_characters.py — impact
  • tests/test_export_direction.py — impact
  • tests/test_export_idempotent_writes.py — impact
  • tests/test_export_path_length.py — impact
  • tests/test_external_stub_endpoints.py — impact
  • tests/test_extract.py — impact
  • tests/test_extract_cache_location.py — impact
  • … and 101 more

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Docs that may be stale (advisory)

…and 10 more.

Formal verification

Behavior changes: \_json\_text changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_json\_text behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"data":"\{'a': \{'b': 1\}\}"\}, the old code produced '\{\\n "a": \{\\n "b": 1\\n \}\\n\}\\n' but the new code produces '\{"a":\{"b":1\}\}\\n' (outputs first differ at character 3). Paste that input straight into a regression test.

Could not verify: Could not verify \_prune\_graph\_json\_sources.

The verifier did not have enough to check \_prune\_graph\_json\_sources, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: the input domain has 83 values but only 11 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)

Could not verify: Could not verify dispatch\_command.

The verifier did not have enough to check dispatch\_command, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 40 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

No difference found (not proven): No behavior difference found in to\_json (not a proof).

The verifier ran both versions of to\_json on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

No difference found (not proven): No behavior difference found in \_save\_global\_graph (not a proof).

The verifier ran both versions of \_save\_global\_graph on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify write\_json\_atomic.

The verifier did not have enough to check write\_json\_atomic, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: parameter `path` is annotated `'str | Path'` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_rebuild\_code.

The verifier did not have enough to check \_rebuild\_code, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: nondeterministic output (both versions disagreed with themselves on the witness) — not a behaviour change; a function whose output is not a function of its inputs (random / time / uuid / os.urandom, or state carried across calls) cannot be soundly distinguished by execution, so this tier abstains for the whole function

· 2 more finding(s) on lines outside this diff (see the check run).

andrebrait added a commit to pfBlockerNG/graphify that referenced this pull request Oct 11, 2026
…SON (Graphify-Labs#3413)

Squashed from origin/fix/compact-graph-json (4c31235) onto upstream/v8.
@andrebrait

Copy link
Copy Markdown
Contributor Author

Review companion on our fork (same head, same base commit): pfBlockerNG#13.

andrebrait added a commit to pfBlockerNG/graphify that referenced this pull request Oct 11, 2026
…SON (Graphify-Labs#3413)

Squashed from origin/fix/compact-graph-json (4c31235) onto upstream/v8.
@safishamsi

Copy link
Copy Markdown
Member

Landed in 0.9.86 (now on PyPI) via an authorship-preserving cherry-pick, so your commit authorship is retained. Thank you, @andrebrait!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Github diff too large for commited graphify-out content

2 participants