Summary
A graph maintained with --update ends up missing edges that a clean build produces — predominantly cross-file imports / imports_from, plus some calls. --force does not repair it. Only deleting the graphify-out directory and rebuilding yields a graph equal to a clean build.
This is silent: nothing in the output indicates the graph is incomplete, and graph.json gives a consumer no way to tell.
Measurement
Project: ~130 Python files, 3,277 nodes. Comparison is against a full build of the same source copied to a clean directory, diffing on (source_file, label) pairs rather than node ids (ids are root-dependent, so an id diff is meaningless across roots).
| Build method |
Node difference |
Edge difference |
maintained with --update |
0 |
65 missing |
--force over that same directory |
0 |
64 missing |
graphify-out deleted, then rebuilt |
0 |
0 |
Relation breakdown of the 65 missing edges:
imports 41
imports_from 19
calls 5
Every missing edge originates in a file that had been re-extracted by an
earlier --update. Examples:
reachability.py --imports--> _parser_for() (source_spans.py)
reachability.py --imports--> function_spans() (source_spans.py)
validation.py --imports--> _parser_for() (source_spans.py)
attacker_value_ranking.py --imports--> is_file_node() (graph_data.py)
Expected
--update should produce a graph equivalent to a full build of the same source, or --force should repair one that has diverged.
Actual
Re-extracting a changed file appears not to re-resolve that file's edges into other files. The stale state survives --force and is only cleared by removing the output directory.
Why it matters
Any consumer that walks imports/imports_from (dependency analysis, supply-chain reach) undercounts, and anything walking calls can produce false negatives — a reachability query returning "no path" when a path exists in the source. Since the loss happens during the build, it leaves no trace in graph.json, so it cannot be detected by inspecting the graph alone.
Reproduction
- Build a graph for a multi-file project.
- Edit a file that imports from several others; run
graphify <path> --update --code-only. Repeat a few times across different files.
- Copy the source to a clean directory and build it fresh.
- Diff the two graphs on
(source_file, label) node keys and (source_key, target_key, relation) edge keys.
Environment
- graphify
0.9.65 (PyPI graphifyy)
- Python 3.14, Windows 10
Possibly related
Summary
A graph maintained with
--updateends up missing edges that a clean build produces — predominantly cross-fileimports/imports_from, plus somecalls.--forcedoes not repair it. Only deleting thegraphify-outdirectory and rebuilding yields a graph equal to a clean build.This is silent: nothing in the output indicates the graph is incomplete, and
graph.jsongives a consumer no way to tell.Measurement
Project: ~130 Python files, 3,277 nodes. Comparison is against a full build of the same source copied to a clean directory, diffing on
(source_file, label)pairs rather than node ids (ids are root-dependent, so an id diff is meaningless across roots).--update--forceover that same directorygraphify-outdeleted, then rebuiltRelation breakdown of the 65 missing edges:
Every missing edge originates in a file that had been re-extracted by an
earlier
--update. Examples:Expected
--updateshould produce a graph equivalent to a full build of the same source, or--forceshould repair one that has diverged.Actual
Re-extracting a changed file appears not to re-resolve that file's edges into other files. The stale state survives
--forceand is only cleared by removing the output directory.Why it matters
Any consumer that walks
imports/imports_from(dependency analysis, supply-chain reach) undercounts, and anything walkingcallscan produce false negatives — a reachability query returning "no path" when a path exists in the source. Since the loss happens during the build, it leaves no trace ingraph.json, so it cannot be detected by inspecting the graph alone.Reproduction
graphify <path> --update --code-only. Repeat a few times across different files.(source_file, label)node keys and(source_key, target_key, relation)edge keys.Environment
0.9.65(PyPIgraphifyy)Possibly related
callsedges are deterministically deleted byreferenceson the same node pair (alphabetical last-write-wins in build_from_json) #2391 (callsdeleted byreferenceson the same pair) — a different mechanism; the drift above is measured after accounting for that.graph_statscannot tell an agent how stale its graph is:built_at_commitis dropped by_load_graph, and no build timestamp is recorded at all #3354 (no build timestamp / commit recorded) — with no build provenance ingraph.json, a drifted graph is indistinguishable from a current one.