Milestone 7 — Graph Relationships and Contradiction Detection
Overview
Milestone 7 adds a Neo4j property graph projection of the KGS registry, enabling:
- Full contradiction detection — assertion grammar (subject + predicate + scope) comparison across all approved answers
- Supersession chain traversal — retrieve the complete version history for any answer
- Related-answers traversal — retrieve all assertions about a concept (e.g. "platform")
- Periodic sync — Celery tasks keep the graph current with PostgreSQL
Architecture
API (FastAPI)
└─ GraphService (application)
├─ GraphRepository (domain protocol → Neo4jAdapter)
├─ RegistryRepository (domain protocol → PostgresRegistryRepository)
└─ ContradictionDetector (pure domain service)
Key Design Decisions
Contradiction Detection via Assertion Grammar
Contradictions are detected at the grammar level: two approved answers conflict when they share the same subject and predicate but have different object_value fields. Scope acts as a context boundary — answers in different scopes (e.g. enterprise vs community) are not considered contradictory.
This logic lives entirely in the ContradictionDetector domain service (zero I/O), making it testable without any running infrastructure.
Neo4j for Relationship Storage
The graph stores five relationship types:
| Relationship | Direction | Meaning |
|---|---|---|
| ANSWERED_BY | Question → Answer | A transcript question has a validated answer |
| SUPPORTED_BY | Answer → Evidence | An answer is backed by a source document |
| ABOUT | Answer → Concept | An answer asserts something about a concept |
| SUPERSEDES | New Answer → Old Answer | Version chain link |
| CONFLICTS_WITH | Answer ↔ Answer | Contradiction recorded at publish time |
Relational Store Remains Source of Truth
PostgreSQL remains the authoritative store. Neo4j is a derived projection. If Neo4j is unavailable or out of sync, the API returns an error rather than serving stale data. The Celery sync worker reconciles the two stores.
Protocol-Backed Testability
GraphRepository is a Protocol in the domain layer. Tests use FakeGraphRepo (pure Python dict) and FakeRegistryRepo — no Neo4j, no Postgres required. All 35 unit tests run in < 1 second.
Files Added
Domain
| File | Description |
|---|---|
src/raw_to_knowledge/domain/models/graph.py |
AnswerNode, QuestionNode, ConceptNode, EvidenceNode, ContradictionResult, SupersessionChain, RelatedAnswersResult |
src/raw_to_knowledge/domain/protocols/graph.py |
GraphRepository Protocol (10 async methods) |
src/raw_to_knowledge/domain/services/contradiction_detector.py |
ContradictionDetector — pure domain, zero I/O |
Infrastructure
| File | Description |
|---|---|
src/raw_to_knowledge/infrastructure/graph/cypher_queries.py |
All Cypher query constants (UPSERT, LINK, TRAVERSE, CONSTRAINT) |
src/raw_to_knowledge/infrastructure/graph/neo4j_adapter.py |
Neo4jAdapter — async neo4j driver, ensure_constraints(), close() |
Application
| File | Description |
|---|---|
src/raw_to_knowledge/application/graph/graph_service.py |
GraphService — sync, detect, traverse |
src/raw_to_knowledge/application/graph/sync_tasks.py |
Celery tasks: sync_graph_task, detect_contradictions_task |
API
| File | Description |
|---|---|
src/raw_to_knowledge/api/v1/graph.py |
5 FastAPI routes |
API Endpoints
| Method | Path | Description |
|---|---|---|
POST |
/v1/graph/answers/{id}/sync |
Sync one answer from Postgres → Neo4j |
GET |
/v1/graph/answers/{id}/contradictions |
List known contradictions (graph-stored) |
POST |
/v1/graph/answers/{id}/detect-contradictions |
Run fresh detection against all fact answers |
GET |
/v1/graph/answers/{id}/supersessions |
Get the full version chain |
GET |
/v1/graph/concepts/{label}/answers |
Get all answers about a concept |
Graph Schema
(Question)-[:ANSWERED_BY]->(Answer)
(Answer)-[:SUPPORTED_BY]->(Evidence)
(Answer)-[:ABOUT]->(Concept)
(Answer)-[:SUPERSEDES]->(Answer)
(Answer)-[:CONFLICTS_WITH]->(Answer)
Configuration
New settings added to src/raw_to_knowledge/config.py:
| Setting | Default | Description |
|---|---|---|
NEO4J_URI |
bolt://localhost:7687 |
Neo4j Bolt URI |
NEO4J_USERNAME |
neo4j |
Neo4j username |
NEO4J_PASSWORD |
"" |
Neo4j password |
Celery Workers
# Start the graph sync worker
celery -A raw_to_knowledge.infrastructure.celery_app worker --loglevel=info -Q raw_to_knowledge_graph
# Trigger a full sync manually
celery -A raw_to_knowledge.infrastructure.celery_app call raw_to_knowledge.application.graph.sync_tasks.sync_graph
# Trigger contradiction detection for one answer
celery -A raw_to_knowledge.infrastructure.celery_app call \
raw_to_knowledge.application.graph.sync_tasks.detect_contradictions \
--args '["<answer_id>"]'
Quality Gates
| Gate | Threshold | Measurement |
|---|---|---|
| Contradiction detection rate | ≥ 90% of seeded fixtures detected | Unit test: test_contradiction_detector.py |
| Supersession chain integrity | Full chain returned for versioned answers | Integration test |
| Graph sync consistency | All approved answers projected within 60 s | Celery periodic task |
| Graph query performance | p95 traversal ≤ 500 ms for two-hop queries | Integration benchmark |
Testing
# Unit tests (no external deps)
pytest tests/unit/test_contradiction_detector.py tests/unit/test_graph_service.py
# Integration tests (requires NEO4J_URI)
NEO4J_URI=bolt://localhost:7687 NEO4J_PASSWORD=password \
pytest tests/integration/test_graph_roundtrip.py -m integration