# Pre-commit structural contradiction detection for Neo4j graphs

**URL:** <https://community.neo4j.com/t/pre-commit-structural-contradiction-detection-for-neo4j-graphs/77789>\
**Category:** Newbie Questions\
**Created:** [May 9, 2026, 10:52am UTC](https://community.neo4j.com/t/pre-commit-structural-contradiction-detection-for-neo4j-graphs/77789 "2026-05-09T10:52:03Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![jasonlvolk](https://sea1.discourse-cdn.com/flex021/user_avatar/community.neo4j.com/jasonlvolk/32/34385_2.png) [@jasonlvolk](https://community.neo4j.com/u/jasonlvolk)\
**Post date:** [May 9, 2026, 10:52am UTC](https://community.neo4j.com/t/pre-commit-structural-contradiction-detection-for-neo4j-graphs/77789/1 "2026-05-09T10:52:03Z")

</div>

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:

```auto
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):

```auto
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](https://github.com/Jasonleonardvolk/sigma-guard)

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

---

<div class="post-metadata">

**Author:** ![joshcornejo](https://sea1.discourse-cdn.com/flex021/user_avatar/community.neo4j.com/joshcornejo/32/34171_2.png) [@joshcornejo](https://community.neo4j.com/u/joshcornejo)\
**Post date:** [May 22, 2026, 10:32am UTC](https://community.neo4j.com/t/pre-commit-structural-contradiction-detection-for-neo4j-graphs/77789/2 "2026-05-22T10:32:21Z")

</div>

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

```auto
// 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

```auto
// 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

```
