Skip to content

fix(build): inherit persisted directed flag on incremental rebuild (#2342) - #2350

Closed
MANOJ21K wants to merge 1 commit into
Graphify-Labs:v8from
MANOJ21K:fix/2342-inherit-directed-on-incremental-merge
Closed

fix(build): inherit persisted directed flag on incremental rebuild (#2342)#2350
MANOJ21K wants to merge 1 commit into
Graphify-Labs:v8from
MANOJ21K:fix/2342-inherit-directed-on-incremental-merge

Conversation

@MANOJ21K

Copy link
Copy Markdown
Contributor

Fixes #2342.

Problem

An incremental refresh silently rebuilds a --directed graph as undirected, and
writes it back with force=True. Nothing in stdout or the report says the graph
changed type.

Betweenness centrality is only meaningful on a directed graph. Undirected, a leaf
utility that everything imports becomes a fake bridge: every pair of its importers
sits two hops apart "through" it, though no control flow passes through it at all.
So ## God Nodes and the betweenness-derived ## Suggested Questions quietly start
reporting ubiquity instead of architecture.

Root cause

Two paths drop the persisted graph type. Both re-serialize with force=True, so the
downgrade is permanent:

  • build.py::build_merge() took directed: bool = False, and cli.py's incremental
    extract calls it without directed= (_build_merge([merged], graph_path=..., ...)).
  • watch.py::_rebuild_code() — the function behind graphify update and the
    post-commit hook — called build_from_json(result) with no directed= at all.

The flag already round-trips correctly everywhere else. cluster-only inherits it
(cli.py, build_from_json(_raw, directed=_directed)), and #801 went to the trouble
of preserving per-edge source/target through the merge. It is specifically the
graph type that was dropped.

Fix

Inherit the persisted flag instead of defaulting it.

  • _load_existing_graph() already parses the raw JSON dict for the merge, so it now
    returns directed alongside (nodes, edges, hyperedges) — no second read of a
    graph.json that may be up to the 512 MiB cap.
  • build_merge(directed=...) becomes bool | None, defaulting to None = "inherit
    from the graph on disk". An explicit True/False still wins, so existing callers
    are unaffected. With no graph on disk there is nothing to inherit and the historical
    False default stands.
  • _rebuild_code() passes directed= from existing_graph_data, which it has already
    loaded and uses two lines later for the topology compare.

Reproduction

The script from the issue, before and after:

$ python repro.py          # before
saved directed: True
after update  : False

$ python repro.py          # after
saved directed: True
after update  : True

Tests

Two regression tests, both failing on clean v8 (assert G2.is_directed()assert False,
assert rebuilt["directed"] is Trueassert False is True), plus two guards that pin the
behaviour this must not change:

  • tests/test_build.py::test_build_merge_inherits_persisted_directed_flag — merge an
    empty chunk set into a directed graph; asserts the graph stays a DiGraph, that the
    re-written JSON keeps "directed": true, and that the sink's betweenness is 0.0
    (the symptom the issue is actually about).
  • tests/test_build.py::test_build_merge_directed_flag_still_forceable — explicit
    directed=True/False still overrides the inherited value.
  • tests/test_build.py::test_build_merge_defaults_undirected_without_existing_graph
    pins the no-existing-graph default.
  • tests/test_watch.py::test_rebuild_code_preserves_directed_graph — full
    _rebuild_code round trip: rebuild a directed graph after a corpus change and assert
    the persisted flag survives.

uv run pytest tests/ -q → 3903 passed, 3 skipped (baseline on v8 is 3899 passed,
3 skipped — the delta is exactly these four tests, no regressions).

Out of scope

extract --no-cluster / update --no-cluster raw-dumps a graph payload that has no
directed key at all, so that path loses the flag by a different mechanism (the raw
dict is assembled from the fresh extraction, not from the loaded graph). It needs its
own decision about carrying the key forward through merge_raw_extraction, and
touching it here would drag the shrink-guard's same_graph comparison into this diff.
Happy to open it as a separate issue if useful.

…raphify-Labs#2342)

A graph saved with --directed was silently rebuilt as undirected by any
routine refresh, then written back with force=True, so the downgrade was
permanent and nothing in stdout or the report said the graph changed type.

Betweenness is only meaningful on a directed graph. Undirected, a leaf
utility that everything imports becomes a fake bridge between every pair
of its importers, so God Nodes and the betweenness-derived Suggested
Questions start reporting ubiquity instead of architecture.

Two paths dropped the flag: build_merge() defaulted to directed=False and
the incremental extract call never passed it, and _rebuild_code() — the
function behind `graphify update` and the post-commit hook — called
build_from_json(result) with no directed= at all.

Both now inherit the persisted value. _load_existing_graph() already
parses the graph dict for the merge, so it returns `directed` alongside
the node/edge lists rather than re-reading a file that can reach the
512 MiB cap; _rebuild_code() reads it from existing_graph_data, which it
already loads for the topology compare. build_merge's directed parameter
becomes bool | None, where None means inherit — an explicit True/False
still wins, so no existing caller changes behavior, and with no graph on
disk there is nothing to inherit and the historical False default stands.

This mirrors the round-trip cluster-only already does via
build_from_json(_raw, directed=_directed).
@safishamsi

Copy link
Copy Markdown
Collaborator

Closing as superseded. The directed-flag inheritance for #2342 shipped in v0.9.32 via #2354. Thanks for the fix, @MANOJ21K.

@safishamsi safishamsi closed this Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants