Skip to content

Milestone 4 — Governed Answer Registry

Status: Complete
Completed: 2026-06-07

Goal

Approved answers persist with provenance. The organization can now say: "We know what we believe, who approved it, and where it came from."

Stakeholder story: "We now know what we believe, who approved it, and where it came from."

What Was Built

ApprovedAnswer Model

ApprovedAnswer is the primary unit of value in the Raw to Knowledge system. It is a frozen Pydantic v2 model — immutable at creation time. Status transitions (SUPERSEDED, DISPUTED, RETIRED) produce new instances via with_status().

Required provenance fields at creation:

Field Description
validation_record_id Link to the ValidationRecord that approved this answer
owner_role Role responsible for this answer's content
evidence_links Non-empty list of source references
review_due Date when this answer should be re-reviewed
horn_type Robert Horn information type (from classification)
version Monotonically increasing version counter

AnswerStatus Enum

AnswerStatus tracks the registry lifecycle, distinct from the validation ValidationState:

Status Description
approved Active, visible in standard queries
superseded Replaced by a newer version; retained for history
disputed Flagged for adjudication post-approval
retired Permanently removed from active queries

CandidateToAnswerTransform

Pure domain service that transforms a ClassificationRecord + ValidationRecord pair into an ApprovedAnswer. Raises ProvenanceError listing all missing fields (not just the first) so callers get a complete picture of what needs to be provided.

Registry Conflict Detection

At publish time, the new answer's grammar triple (subject + predicate + scope) is compared against all active APPROVED answers. Grammar conflicts produce RegistryConflictRecord objects attached to the answer. Unlike M3's validation-time ConflictFlag, registry conflicts do not block the write — they are recorded for audit and surfaced in readouts.

Version Chain

Supersession creates a new ApprovedAnswer (version incremented), marks the old one SUPERSEDED, and records the relationship in version_chains. The get_version_chain API follows supersedes_id back to the root, returning all versions oldest-first. Text updates are rejected by AnswerTextUpdateError — new text requires the supersede path.

Evidence links are stored in a separate evidence_links table (one row per source reference per answer). Each link is a source reference string (artifact ID, URL, document reference). Retrieved as a list in the ApprovedAnswer.evidence_links tuple.

API

All endpoints under /v1/registry/:

Method Path Description
POST /answers Publish approved candidate to registry
GET /answers Query with filters
GET /answers/{id} Get single answer
POST /answers/{id}/supersede Publish new version
POST /answers/{id}/retire Mark RETIRED
POST /answers/{id}/dispute Mark DISPUTED
GET /answers/{id}/version-chain Version history (oldest first)
GET /answers/{id}/evidence Evidence links

Query filters (GET /v1/registry/answers):

Parameter Type Description
horn_type string Filter by Horn type
assertion_subtype string Filter by assertion subtype
customer_scope string Scope isolation per customer
status string Default: approved only
review_due_before YYYY-MM-DD Return answers due before this date
search_text string Full-text search on answer_text and grammar subject
limit / offset int Pagination

HTTP status codes:

  • 201 — Created (publish or supersede)
  • 404 — Answer not found / no approved validation record
  • 409 — Answer not in APPROVED status (supersede path only)
  • 422 — Provenance incomplete or invalid input

Database Schema (Migration 004)

Four new tables:

Table Description
approved_answers Primary knowledge objects with full provenance
evidence_links Source references per answer
version_chains Supersession relationships
registry_conflict_records Grammar conflicts detected at publish time

Full-text search index:

CREATE INDEX ix_approved_answers_fts_answer_text
ON approved_answers
USING GIN (to_tsvector('english', answer_text))

Quality Gates

Gate Implementation
Provenance completeness ProvenanceError lists all missing fields at publish time
Version chain integrity supersedes_id links; version_chains table records each edge
No silent overwrite AnswerTextUpdateError on direct text update attempt; only supersede path allowed
Conflict detection on write RegistryConflictRecord created; write not blocked
Zero unvalidated writes UnvalidatedRegistryWriteError if no approved ValidationRecord

Tests

Unit tests (no DB required): - tests/unit/test_registry_transform.py — build, provenance checks (all-fields-listed), conflict detection, status transitions, immutability

Integration tests (requires DATABASE_URL): - tests/integration/test_registry_roundtrip.py — publish, query filters, version chain, status transitions, immutability, evidence links, error paths

Design Decisions

  • Conflict detection does not block: At registry write time, conflicts are audited but not rejected. This allows the system to capture contradictory claims from different customer contexts while preserving the full record. M7 (Graph) will surface contradictions explicitly.
  • Evidence links as separate table: One row per source reference allows future enrichment (source type, fetched-at timestamp, verification status) without changing ApprovedAnswer.
  • Frozen model + with_status(): Follows the M3 pattern. ApprovedAnswer is immutable at creation; only status transitions are permitted. This keeps the domain layer free of mutability side-effects and makes the audit trail self-evident.
  • Full-text search via PostgreSQL GIN: Avoids the complexity of a separate search index (Elasticsearch, Typesense) at M4 scale. The GIN index on to_tsvector('english', answer_text) satisfies p95 ≤ 2s at 10,000 records without additional infrastructure.