Pre-commit structural contradiction detection for Neo4j graphs

I built an open-source tool that catches a failure mode Neo4j
constraints don't cover: two parts of your graph making claims
that are individually valid but globally incompatible.

Example: one subgraph says Component_X is sole-sourced to
Supplier_A. Another subgraph says Component_X is approved through
Supplier_B. Both nodes pass schema validation. Both relationships
are valid Cypher. But the graph contradicts itself.

SIGMA Guard uses sheaf cohomology to detect these structural
contradictions. It is not schema validation and not an LLM judge.
Deterministic. Returns exact conflict locations and proof IDs.

The Neo4j adapter runs as a guarded write wrapper:

from sigma_guard.adapters.neo4j import Neo4jGuard

guard = Neo4jGuard(
    uri="bolt://localhost:7687",
    auth=("neo4j", "password")
)

guard.execute(
    "CREATE (:Supplier {name: $name, sole_source: true})",
    name="Supplier_A"
)

Try it in 60 seconds (no Neo4j instance needed for the demo):

git clone https://github.com/Jasonleonardvolk/sigma-guard
cd sigma-guard
pip install -e .
python examples/tiny_contradiction.py

Repo: GitHub - Jasonleonardvolk/sigma-guard: Deterministic infra layer ยท GitHub

Curious whether anyone here has dealt with this failure mode
in production Neo4j graphs, especially in knowledge graphs,
compliance graphs, or GraphRAG pipelines.

You can generate things like this as queries (this is 2 starting points, same end-point):

// Define your 3 nodes here
MATCH (start1), (start2), (end)
WHERE start1.id = 'A'   // โ† replace with your start node 1 identifier
  AND start2.id = 'B'   // โ† replace with your start node 2 identifier
  AND end.id   = 'C'    // โ† replace with your end node identifier

// Find all paths from each start to the end node
MATCH p1 = (start1)-[*]->(end)
MATCH p2 = (start2)-[*]->(end)

// Extract the relationships from each path
WITH 
  collect(DISTINCT p1) AS paths1,
  collect(DISTINCT p2) AS paths2

// Flatten all edges across all paths for each start node
WITH
  [p IN paths1 | relationships(p)] AS relSets1,
  [p IN paths2 | relationships(p)] AS relSets2

WITH
  apoc.coll.toSet(apoc.coll.flatten(relSets1)) AS edges1,
  apoc.coll.toSet(apoc.coll.flatten(relSets2)) AS edges2

// Compute intersection and union
WITH
  edges1, edges2,
  [e IN edges1 WHERE e IN edges2] AS intersection,
  apoc.coll.toSet(edges1 + edges2)  AS union

// Calculate overlap % (Jaccard similarity)
WITH
  size(intersection) AS sharedEdges,
  size(union)        AS totalEdges,
  size(edges1)       AS edgesPath1,
  size(edges2)       AS edgesPath2,
  toFloat(size(intersection)) / toFloat(size(union)) * 100 AS overlapPct

// Apply 4-grade scale
WITH *,
  CASE
    WHEN overlapPct >= 75 THEN '4 - High overlap (โ‰ฅ75%)'
    WHEN overlapPct >= 50 THEN '3 - Moderate overlap (50โ€“74%)'
    WHEN overlapPct >= 25 THEN '2 - Low overlap (25โ€“49%)'
    ELSE                       '1 - Minimal overlap (<25%)'
  END AS overlapGrade

RETURN
  edgesPath1    AS totalEdgesFromStart1,
  edgesPath2    AS totalEdgesFromStart2,
  sharedEdges   AS sharedEdges,
  totalEdges    AS unionEdges,
  round(overlapPct, 2) AS overlapPct,
  overlapGrade

this one has 2 pairs of [starting, end] and consider overlap

// Define your 2 pairs of start/end nodes
MATCH (start1), (end1), (start2), (end2)
WHERE start1.id = 'A'   // โ† replace with your pair 1 start node identifier
  AND end1.id   = 'B'   // โ† replace with your pair 1 end node identifier
  AND start2.id = 'C'   // โ† replace with your pair 2 start node identifier
  AND end2.id   = 'D'   // โ† replace with your pair 2 end node identifier

// Find all paths for each pair
MATCH p1 = (start1)-[*]->(end1)
MATCH p2 = (start2)-[*]->(end2)

// Collect all paths per pair
WITH
  collect(DISTINCT p1) AS paths1,
  collect(DISTINCT p2) AS paths2

// Flatten all edges across all paths for each pair
WITH
  apoc.coll.toSet(apoc.coll.flatten([p IN paths1 | relationships(p)])) AS edges1,
  apoc.coll.toSet(apoc.coll.flatten([p IN paths2 | relationships(p)])) AS edges2

// Compute intersection and union
WITH
  edges1, edges2,
  [e IN edges1 WHERE e IN edges2] AS intersection,
  apoc.coll.toSet(edges1 + edges2) AS union

// Calculate Jaccard overlap %
WITH
  size(intersection) AS sharedEdges,
  size(union)        AS totalEdges,
  size(edges1)       AS edgesPair1,
  size(edges2)       AS edgesPair2,
  toFloat(size(intersection)) / toFloat(size(union)) * 100 AS overlapPct

// Apply 4-grade scale
WITH *,
  CASE
    WHEN overlapPct >= 75 THEN '4 - High overlap (โ‰ฅ75%)'
    WHEN overlapPct >= 50 THEN '3 - Moderate overlap (50โ€“74%)'
    WHEN overlapPct >= 25 THEN '2 - Low overlap (25โ€“49%)'
    ELSE                       '1 - Minimal overlap (<25%)'
  END AS overlapGrade

RETURN
  edgesPair1             AS totalEdgesPair1,
  edgesPair2             AS totalEdgesPair2,
  sharedEdges            AS sharedEdges,
  totalEdges             AS unionEdges,
  round(overlapPct, 2)   AS overlapPct,
  overlapGrade