feat: graphify curate — durable human corrections that survive a rebuild#1871
Open
cgmoore120 wants to merge 2 commits into
Open
feat: graphify curate — durable human corrections that survive a rebuild#1871cgmoore120 wants to merge 2 commits into
cgmoore120 wants to merge 2 commits into
Conversation
added 2 commits
July 13, 2026 18:14
Every edge in graph.json is extractor-derived, so a hand correction is transient by construction — in both directions, and silently. A DELETED edge comes back. The semantic cache is keyed by file content, not by graph state, so an unchanged doc keeps its cached edges and the next `extract` re-injects the edge — written with force=True, past the shrink guard. An ADDED edge is destroyed. build_merge replaces per source_file: everything belonging to a re-extracted file is dropped from the base before merging. A human-authored edge on that file is dropped, and no extractor re-emits it. The node count GROWS while this happens, so the loss reads as a successful update. Measured on a 3,384-node Angular graph: one `graphify update` took it to 3,425 nodes and silently dropped 3 verified edges. `save-result --outcome dead_end` records that an edge is false, but it is advisory — it informs a future reader through LESSONS.md and never gates extraction. So the graph and the lessons drift apart: the graph keeps asserting an edge the operator has already disproved. Adds graphify-out/curation.json, applied at the end of build_from_json — the funnel build, build_merge, watch and the skill's agent path all construct the graph through. Applying it at BUILD time rather than write time means clustering, god nodes and GRAPH_REPORT.md all see the corrected graph, so a disproved edge stops being re-advertised in Surprising Connections rather than merely vanishing from graph.json. The two --no-cluster paths never build a NetworkX graph, so they apply the overlay to the payload directly. graphify curate deny SRC TGT [--relation R] [--reason ...] [--evidence ...] graphify curate pin SRC TGT --relation R [--confidence C] [--score F] graphify curate list graphify curate apply Denies match the unordered pair (undirected storage canonicalizes endpoint order); omitting --relation denies every edge between the pair. A pinned edge whose endpoints are not both present is skipped, never invented — the overlay corrects the graph, it does not fabricate nodes. Re-application is idempotent. curation.json joins the dated-folder backup set (Graphify-Labs#834). Inert when absent, so no behavior change for existing users; GRAPHIFY_NO_CURATION=1 disables it. 23 new tests, including regressions for both failure modes above.
# Conflicts: # CHANGELOG.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Every edge in
graph.jsonis extractor-derived, so a hand correction is transient by construction — in both directions, and silently.A deleted edge comes back. The semantic cache is keyed by file content, not by graph state (
cache.py:531). An unchanged doc keeps its cached edges forever, so the nextgraphify extractre-injects the edge you deleted and writes it withforce=True(cli.py:2574) — past the shrink guard.An added edge is destroyed.
build_mergereplaces persource_file(build.py:925-950): everysource_filepresent in the new chunks is dropped from the base graph — nodes and edges — before merging. A human-authored edge whosesource_filenames a re-extracted file is dropped, and no extractor re-emits it. Nothing warns, and the node count grows while it happens, so the loss reads as a successful update.I hit the second one on a 3,384-node Angular graph. I had verified four extracted edges against the source, deleted the three that were false, and added three that extraction had missed. Then I ran
graphify update .to time it:The
backup_if_protecteddated folder from #834 saved me — but that is recovery, not persistence: it hands you a snapshot and lets the overwrite proceed. (The asymmetry is nasty: my deletions survived, because those edges came from.htmlfiles thatupdatedoesn't re-extract, while my additions died, because they were attributed to.tsfiles that it did. A partial wipe looks like success.)save-result --outcome dead_endlooks like the answer, but it is advisory — it informs a future reader throughLESSONS.mdand never gates extraction. So the graph and the lessons actively drift apart: the graph keeps asserting an edge the operator has already disproved, and keeps re-advertising it in Surprising Connections.There is currently no user-owned override mechanism anywhere in the package (grep for
curat|override|pin|deny: the word "curated" appears only inexport.py, meaning "the labels file has a non-default community name").Solution
A small, declarative
graphify-out/curation.json, applied at the end ofbuild_from_json— the single funnel through whichbuild,build_merge,watchand the skill's agent path all construct the graph.Applying it at build time rather than write time is the load-bearing choice: clustering, god nodes,
surprising_connectionsandGRAPH_REPORT.mdall see the corrected graph. A disproved edge stops being re-advertised, rather than merely being absent fromgraph.json.Semantics:
(a,b)must match an edge stored(b,a)), and honour_src/_tgtso a direction-restored edge still matches. Omitting--relationdenies every edge between the pair.--no-clusterpaths (cli.py,watch.py) never build a NetworkX graph, so they apply the overlay to the payload directly — otherwise a denied edge survives on exactly those paths.curation.jsonjoins_BACKUP_ARTIFACTS(Feature: protect semantic and curated graphs from silent overwrite via dated-folder backup #834).GRAPHIFY_NO_CURATION=1disables it (mirrorsGRAPHIFY_NO_BACKUP).This complements
save-result --outcome dead_endrather than replacing it: that records that a path was false, for a human readingLESSONS.md. Curation makes the graph itself stop asserting it.Testing
23 new tests in
tests/test_curation.py, including a regression for each failure mode:test_pinned_edge_survives_build_merge_replace_per_source— pins an edge whosesource_fileis the file that then gets re-extracted, and asserts it survivesbuild_merge.test_denied_edge_stays_denied_when_extraction_reasserts_it— runs three builds where the extraction keeps asserting the edge (as the content-keyed cache does), and asserts it never lands.Plus order-insensitivity, relation scoping, idempotency, deny-before-pin, missing-endpoint skip, malformed/future-schema/env-var handling, and
--no-clusterpayload coverage.End-to-end on a real corpus: pinned an edge on
src/alpha.py, modifiedsrc/alpha.py(forcing re-extraction of exactly that file), rangraphify update .— the pin survived and the deny held. A subsequent fullgraphify extract .(the cache-resurrection path) also held.5 pre-existing failures (
test_ollama_retry_cap.py,test_labeling.py::test_label_communities_batches_when_over_batch_size) fail identically onv8without this change — verified by stashing.CI gates:
ruffclean;skillgen --check,--audit-coverage,--schema-singleton,--monolith-roundtrip,--always-on-roundtripall OK;banditreports nothing in the new code.Notes for review
merge-driver(cli.py:1420), which unions two graphs and bypassesto_json, backup and the shrink guard. Applying the overlay there is defensible but it is a separate concern — happy to add it if you'd like it in scope.build_mergesevers edges from unchanged files whose endpoints were replaced — a different mechanism from this one, and not fixed here).version: 1) and a future/unknown version is ignored with a warning rather than half-applied.