Skip to content

fix(export): give a committed output directory its diff attributes - #3575

Open
L4XB wants to merge 1 commit into
Graphify-Labs:v8from
L4XB:fix/3413-out-dir-gitattributes
Open

L4XB wants to merge 1 commit into
Graphify-Labs:v8from
L4XB:fix/3413-out-dir-gitattributes

Conversation

@L4XB

@L4XB L4XB commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Closes #3413.

A committed graphify-out/ can cost a pull request its whole diff. graph.json is pretty-printed and node ordering shifts between rebuilds, so a rebuild presents a very large changed-line count. GitHub gives up computing a diff that size and answers 422, and the pull request then reports zero changed files for every file in it, not only for the artifacts. The Files changed tab goes empty and the API agrees with it, which is what the reporter is looking at in the screenshot.

The reporter had already worked out the right incantation by hand. This ships it, so the next person does not have to.

The change

to_json writes a .gitattributes into the output directory when it has none:

* linguist-generated
graph.json -diff
manifest.json -diff
GRAPH_REPORT.md -diff
.graphify_labels.json -diff

-diff is the part that matters. It makes git treat these as opaque, so the diff is never computed. linguist-generated only collapses the rendered result, which is too late: the computation has already happened and already timed out. The repo already carries graphify-out/**/*.html linguist-vendored=true in its own root .gitattributes, so marking these artifacts is an established position here, not a new one.

Three boundaries, each with a test:

  • Only the output directory. --graph can name any path, and dropping a file into a directory the user chose for one artifact would be writing somewhere we were not invited. The write is gated on the parent directory being GRAPHIFY_OUT_NAME.
  • Never overwrite. An existing .gitattributes is left exactly as it is, so a user who has tuned theirs keeps it.
  • Never fail the build. A read-only output directory costs the attributes file, not the graph, which is the same stance backup_if_protected takes.

GRAPHIFY_NO_GITATTRIBUTES=1 turns it off, mirroring GRAPHIFY_NO_BACKUP.

Test

Five cells in tests/test_export.py. Four fail on v8:

FAILED test_to_json_gives_the_output_directory_diff_attributes
  FileNotFoundError: .../graphify-out/.gitattributes
FAILED test_existing_gitattributes_is_never_overwritten
  ImportError: cannot import name 'ensure_output_gitattributes'
FAILED test_gitattributes_env_disable
FAILED test_gitattributes_failure_never_breaks_the_build

The fifth, test_to_json_leaves_a_directory_it_was_only_pointed_at_alone, passes before and after on purpose: it pins that this does not start writing into arbitrary --graph targets.

tests/test_export.py is green at 65; the export, cli-export and atomic slices at 326 passed, 25 skipped. The full tests/ run is 5255 passed with 14 failures, all of which also fail on unmodified v8 here (terraform, ollama retry, markdown frontmatter, the wheel payload and a TS timing test) and none of which touch this path.

Mutation-checked: six seeded defects, including removing the call, dropping the output-directory guard, overwriting an existing file, ignoring the opt-out, letting a read-only directory raise, and dropping the -diff line. Each is caught by a named cell; none survive.

No formatter was run over the file, so the diff is only these lines.

graph.json is pretty-printed and its node ordering shifts between rebuilds, so
a rebuild of a committed graphify-out/ can present a million changed lines.
GitHub gives up computing a diff that size, answers 422, and the pull request
then reports zero changed files for every file in it, not only this one: the
Files changed tab goes empty and the API agrees with it.

Write a .gitattributes into the output directory when it has none, marking the
regenerated artifacts `-diff` so git never computes that diff, and
`linguist-generated` so the repo's language stats ignore them.
`linguist-generated` alone does not help here, since it only collapses the
rendered diff after it has been computed.

Only the output directory gets one: `--graph` can name any path, and dropping
a file into a directory the user chose for one artifact would be writing
somewhere we were not invited. An existing .gitattributes is never touched,
GRAPHIFY_NO_GITATTRIBUTES=1 turns it off the way GRAPHIFY_NO_BACKUP does, and
a directory that refuses the write costs the attributes file rather than the
build.

@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.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.

Formal verification. 1 change(s) tested, no difference found (not proven).


Graphify review — findings

Writes a .gitattributes into the graphify output directory on to_json, marking graph.json, manifest.json, GRAPH_REPORT.md, and .graphify_labels.json as -diff (plus * linguist-generated) so a rebuild's reordered pretty-printed JSON can't trigger GitHub's 422-on-huge-diff and blank out the whole PR's file changes. Only touches directories named GRAPHIFY_OUT_NAME, so a bare --graph path is left alone, an existing .gitattributes is never overwritten, and GRAPHIFY_NO_GITATTRIBUTES=1 opts out. Failure to write (e.g. read-only dir) is swallowed via ensure_output_gitattributes returning None rather than failing the build.

Worth a look

  • Dangling .gitattributes symlink is followed and writes outside output dirgraphify/export.py:66 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 604 functions depend on the 177 functions this change touches.

Health — this change adds coupling hotspots:

  • new: _rebuild_code() — 116 callers, 51 callees
  • new: to_obsidian() — 38 callers, 14 callees
  • new: to_json() — 58 callers, 8 callees
  • new: dispatch_command() — 2 callers, 124 callees
  • new: _make_graph() — 33 callers, 6 callees
  • new: run_pipeline() — 8 callers, 13 callees
  • new: to_canvas() — 20 callers, 5 callees
  • new: to_graphml() — 15 callers, 5 callees
  • …and 3 more — each is listed as a finding

Verification — 604 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: 429 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

21 of 275 test file(s) selected (8%) via static blast radius.

  • tests/test_atomic_canvas_export.py — impact
  • tests/test_atomic_writes.py — impact
  • tests/test_build.py — impact
  • tests/test_carried_hyperedge_remap.py — impact
  • tests/test_cli_export.py — impact
  • tests/test_community_labels_skill.py — impact
  • tests/test_confidence.py — impact
  • tests/test_cross_extension_reexport_self_cycle.py — impact
  • tests/test_export.py — impact, changed-test
  • tests/test_export_control_characters.py — impact
  • tests/test_export_path_length.py — impact
  • tests/test_go_qualified_resolution.py — impact
  • tests/test_god_nodes_exclude_hubs.py — impact
  • tests/test_hyperedge_roundtrip.py — impact
  • tests/test_hypergraph.py — impact
  • tests/test_obsidian_dangling_member.py — impact
  • tests/test_obsidian_filename_cap.py — impact
  • tests/test_obsidian_unicode_tags.py — impact
  • tests/test_obsidian_vault_migration.py — impact
  • tests/test_pipeline.py — impact
  • tests/test_reflect.py — impact

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.

Formal verification

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.

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

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

1 participant