Working reference · Logical Postgres structures

Postgres-shaped contracts for Supernomic Core

A concrete review layer under the product schemas. These are logical Postgres structures and lifecycle diagrams, not final migrations. The goal is to expose where the model is too complex, where state can be simplified, and where product contracts imply durable records.

Logical DDL Lifecycle diagrams Backend contract review Not final migrations
No matching sections. Clear the search or use Show all.
01

Stance

Model as durable contracts, not final storage

This page translates the product schema into Postgres-shaped structures so the team can review identity, lifecycle, causality, and explainability. It intentionally avoids storage-engine commitments like partitioning strategy, index tuning, and service boundaries.

Boundary: these structures are reference contracts. They can become migrations later, but right now they are here to expose weak concepts, over-broad status fields, and missing audit or evidence links.

Schema namespaces

core

Cases, relationships, decision packets, governed actions, projections.

gov

Governance candidates, review, activation, policy evaluations.

runtime

Runtime artifacts, artifact versions, artifact reviews.

knowledge

The epistemic spine: signals, sources, claims, justifications, validation, authority, conflicts, corrections, and world-model truth.

ledger

Event and projection envelopes.

identity

People, users, teams, roles. Sketched only where needed.

Reading the field tables

In table view every field carries a purpose line and an origin marker — who or what sets its value. Fields I'd question for MVP also get a simplification flag.

Field origin

generated LLM-produced human a person sets it system platform / runtime plumbing human-overridable generated, a person can change it

MVP simplification flag

defer fine concept, not needed to ship fold overlaps something — collapse it cut? I'd remove it
02

Research

Prior art & design constraints

We pressure-tested this model against published research and production systems before building deeper. The shape is validated — the closest production analog (Zep / Graphiti) is nearly 1:1, and 2026 agent-architecture papers independently converged on the same spine-plus-projections design. But several failure modes are well documented; these are the constraints we retain to design against them.

Validated by the prior art — keep: spine-as-record with the world model as a projection; evidence = frozen justification, truth = live re-projection (this is bitemporality); foundationalist bounded justification bundles; per-field effective values; human review as a designed outcome. Add an explicit abstain / not_enough_info verdict (from FEVER fact-checking).

Constraints we design against

critical

Evidence must be causal, verified at freeze

Up to 57% of LLM citations are "post-rationalized" — the claim is written first, then a source found to fit. A citation can be correct yet not actually support the claim, and freezing makes that error permanent. Provenance proves where, not whether the source was true.

Our rule: generation is constrained by evidence; run an entailment/faithfulness check before a bundle is frozen; record a trust signal per source, not just a pointer. arXiv 2412.18004

core

Bitemporal — invalidate, don't delete

Flagged independently by three of four research threads. The "evidence frozen / truth live" split is the database world's transaction-time vs valid-time distinction — a solved, performant pattern. Contradicted facts are invalidated, not deleted.

Our rule: every assertion carries valid_from/valid_to (world time) separate from observed_at/superseded_at (system time); current truth re-projects from the latest non-invalidated facts. Zep / Graphiti, XTDB

core

Authority is a prior, not hard precedence

Rule-based "system X always wins" is the MDM golden-record trap — it doesn't scale and is context-blind (who owns the VPN differs for billing vs ops vs security). Knowledge Vault learned per-source reliability and fused it with a plausibility prior instead.

Our rule: authority_rules are a declared prior; evidence + freshness can override; human corrections feed back as training labels (the review loop is free supervised data). Knowledge Vault, KDD 2014; MDM survivorship

core

Identity merges must be reversible

The CDP industry's most expensive failure: once profiles merge, they can't be unmerged (Segment). Transitive-closure clustering over-merges — one bad alias link collapses unrelated people into one identity.

Our rule: merged_into_person_id is a retractable, evidence-backed bitemporal edge, not a destructive fold; cluster don't transitively close; require deterministic identifiers (or human confirmation) before a stitch can authorize an action. Segment / CDP identity graphs

guardrail

No live justification network (the TMS grave)

Truth Maintenance Systems died at scale: keeping a global dependency graph consistent on every update is combinatorially expensive. Our snapshot design already dodges this — keep it that way.

Our rule: bundles are foundationalist — a bounded, acyclic, de-duped set of cited sources. Re-validating a retracted source re-derives; it never naively restores old conclusions (AGM "Recovery" is contested). Doyle / de Kleer TMS; AGM belief revision

guardrail

Corroboration ≠ independence

"Many sources agree" inflates confidence when sources copy each other (a CMDB auto-filling a wiki). On real data, sophisticated trust-weighting did worse than plain voting when a copying cluster propagated a false value.

Our rule: track source independence; discount correlated sources; calibrate confidence rather than multiplying it. Dong, Berti-Équille, Srivastava, VLDB 2009

guardrail

Staleness is the #1 failure; ingest is an attack surface

"Entity decay" — a once-true value served confidently after it changed — is the dominant accuracy failure. Slack/email ingest is a documented memory-poisoning surface; one bad reflection scales across a long-lived agent.

Our rule: confidence decays with freshness; contradiction-check new claims at write time; bound how far one observation propagates before review. Mem0; memory-poisoning research

guardrail

Replay-safe side effects

If events trigger outbound calls, projection rebuild/replay re-fires them — re-sending Slack messages, re-executing governed actions. This is the classic agent-product footgun.

Our rule: every outbound effect (Slack post, ticket write, governed action) sits behind a port that is inert during replay. Fowler, Event Sourcing

guardrail

Erasure vs the immutable PII log

The spine holds people's identities and message content; an append-only log structurally conflicts with right-to-erasure.

Our rule: decide crypto-shredding vs PII-by-reference before finalizing event payloads; rebuild logic must tolerate "forgotten" events. GDPR + event-sourcing practice

guardrail

Don't event-source everything

A single universal log is the riskiest framing, and "property-sourcing" (events that mirror columns) carries no value.

Our rule: spine for business-meaningful facts (observations, claims, corrections, actions, approvals); CRUD for config/reference; model business operations, not field writes; treat full projection-rebuild time as the scaling canary. Young; Dudycz

Key sources

  • Zep / Graphiti — temporal KG for agent memory (closest production analog) · arxiv.org/abs/2501.13956
  • Knowledge Vault — web-scale probabilistic knowledge fusion · Dong et al., KDD 2014
  • Truth discovery survey — Li et al., SIGKDD Explorations 2015 · kdd.org
  • Citation faithfulness — 57% post-rationalized citations · arXiv 2412.18004
  • FEVER — claim + evidence → verdict pipeline · Thorne et al., NAACL 2018 · arXiv 1803.05355
  • TMS / ATMS — Doyle 1979, de Kleer 1986 · AGM belief revision — Gärdenfors
  • BitemporalityXTDB · W3C PROVw3.org/TR/prov-dm
  • Identity resolution / no-unmergeSegment / Twilio
  • Event sourcingFowler · Greg Young (versioning) · Dudycz
03

Research

Signal selection & the knowledge layer

Everything is a signal — external (Slack / email / tickets) and internal (governed actions, case-state changes, decisions). The hard question is which signals become durable knowledge and which we drop — the field's currently-recognized bottleneck (the "write step"). We researched how mature systems condense high-volume streams without keeping everything; the proven recipe is strikingly consistent across agent memory, stream processing, observability sampling, and salience research.

Validated — keep: the signal processor → Source design sits at the convergence of session windows, online changepoint detection, and event-segmentation theory (a Source ≈ a topic-keyed session closed by a hybrid boundary). Two-gate selection (worth → redundancy) + an offline reflection pass; belief-change as the worth metric; supersede-not-delete; store-by-reference + digest for external content.

Constraints we design against

core

Separate retention, fidelity, and attention

Sampling conflates three different decisions. SIEM risk-based alerting separates them: whether to keep a signal at all (retention), at what resolution (fidelity), and whether it surfaces to a person (attention).

Our rule: decide each independently, by signal class — not one blanket sample rate. Splunk risk-based alerting

core

Don't sample by default — agents have no intuition for missing data

For agent products, naive sampling and rollups are actively harmful: "missing data = broken reasoning," and rollups that drop entity dimensions (case-id, user) leave the agent at a dead end.

Our rule: internal signals (actions, case changes, decisions) kept 100% structured, never sampled; external signals kept as reference + digest + redacted summary long-term, raw body only 7–30d for replay then hashed. Expiry driven by privacy/compliance, not cost. ClickHouse "three villains"; OpenTelemetry; GDPR minimization

core

Two-gate selection + an offline reflection pass

The proven stream-to-record pattern: a worth gate, then a redundancy gate, with periodic reflection. Score worth by belief-change (Bayesian surprise — "how much does this move our posterior"), not raw novelty and not a single LLM importance score (LLMs are systematically overconfident).

Our rule: worth (composite, calibrated) → redundancy (ADD / UPDATE / NOOP / SUPERSEDE) → offline reflection that synthesizes low-salience signals. Reflection is the only defense against dropping the slow-burn that mattered. Generative Agents; Mem0; Itti & Baldi (Bayesian surprise)

guardrail

No single global similarity threshold — merge on a band

A single cosine cutoff for "is this the same thing" is the most-cited fragility in incremental knowledge-base construction.

Our rule: use a band — high-confidence auto-merge, low-confidence auto-add, a middle "adjudicate" zone routed to review; key merges on event identity (timestamp, case-id, actor), not phrasing, so distinct-but-similar events never collapse. iText2KG; DIAL-KG

guardrail

Supersede, never hard-delete

Given LLM miscalibration, deletion is the irreversible mistake — the dropped signal that later mattered can't be recovered.

Our rule: every drop/merge is a reversible supersede with bitemporal validity, not a destructive delete (reinforces the bitemporal constraint above). Zep / Graphiti; Mem0

guardrail

"Closed" is not immutable

Late or out-of-order signals reopen a "closed" Source; over/under-segmentation is the central quality failure of every grouping system.

Our rule: make merge / split / retract first-class operations; give each Source a grace period and a bounded max-age (unbounded accumulation state is the #1 stream-processing failure). Kafka / Flink session windows; CEP

guardrail

Protect rare-but-critical and logical anomalies

Rarity should raise worth; frequency-based dedup punishes exactly the rare signal. And "interesting" must include a bad decision that returned success, not just error-labeled events.

Our rule: never reduce errors, governed-action denials, or anomalies; novelty raises a signal's worth rather than lowering it. tail-based sampling; log-reduction research

guardrail

Boundaries are the highest-risk items

Prediction error peaks at boundaries — the handoff message ("by the way, different issue…") is the one most likely to be mis-assigned to the wrong Source.

Our rule: treat the first and last signals of a Source as ambiguous / bridging; allow them to belong to two Sources. Event Segmentation Theory (Zacks)

guardrail

Ingested content is an attack surface; the write path is silent

External Slack/email content can poison memory (the payoff is time-decoupled — it waits), and memory failures throw no exceptions — bad selection propagates as silent reasoning drift.

Our rule: never let ingested content mint durable governed-truth unvalidated; support quarantine/rollback by source cohort; don't iteratively re-summarize (drift erodes incident detail — keep evidence, point to it); instrument write-accept-rate, contradiction-at-write, and post-drop "regret." AgentPoison; agent-memory surveys

Key sources

  • Generative Agents — importance scoring + reflection · Park et al., UIST 2023
  • Mem0 — the write step (ADD/UPDATE/DELETE/NOOP) · arXiv 2504.19413 · Memory-R1 (learned ops > heuristics) · arXiv 2508.19828
  • Bayesian Surprise — belief-change beats raw novelty · Itti & Baldi, NIPS 2006
  • Event Segmentation Theory — boundaries at prediction-error spikes · Zacks; comp. model Reynolds/Zacks/Braver 2007
  • Session windows / Dataflow — Akidau et al. · Bayesian Online Changepoint Detection · Adams & MacKay 2007
  • Tail-based samplingOpenTelemetry · Refinery dynamic sampling — Honeycomb
  • "Three villains of agentic observability"ClickHouse · Risk-based alerting — Splunk ES
  • Incremental KG commitiText2KG · data-protection-by-design — EDPB Guidelines 4/2019
02

Types

State vocabulary, embedded where used

The first simplification is to stop treating every lifecycle enum as a Case status. case_status and resolution_state are the durable lifecycle axes on Case. A single derived ownership court (agent vs waiting) replaces the old automation_state and human_needed_state; the precise actor and all deadlines live on the decision packet and in core.case_timers. stale_state describes projection freshness. The remaining statuses belong to Decision Packet, Governed Action, Governance Candidate, and Runtime Artifact.

In table view, enum definitions appear as expandable chips beside the fields that use them. SQL view keeps the raw create type statements.

create schema if not exists core;
create schema if not exists gov;
create schema if not exists runtime;
create schema if not exists knowledge;
create schema if not exists ledger;
create schema if not exists identity;

create type core.case_status as enum (
  'open',
  'resolved',
  'closed',
  'archived'
);

create type core.resolution_state as enum (
  'unresolved',
  'resolved',
  'duplicate',
  'withdrawn',
  'merged'
);

create type core.ownership_state as enum (
  'agent',
  'waiting'
);

create type core.escalation_action as enum (
  'extend',
  'nudge',
  'advance',
  'reassign',
  'broaden',
  'fail'
);

create type core.stale_state as enum (
  'fresh',
  'possibly_stale',
  'stale',
  'regenerating',
  'superseded'
);

create type core.decision_packet_status as enum (
  'active',
  'answered',
  'expired',
  'stale',
  'superseded',
  'cancelled'
);

create type core.governed_action_status as enum (
  'draft',
  'pending_policy',
  'needs_approval',
  'needs_expert_review',
  'blocked',
  'approved',
  'executing',
  'succeeded',
  'failed',
  'partially_failed',
  'cancelled',
  'superseded'
);

create type gov.candidate_review_status as enum (
  'draft',
  'submitted',
  'under_review',
  'needs_changes',
  'approved',
  'denied',
  'stale',
  'superseded',
  'archived'
);

create type gov.candidate_activation_status as enum (
  'not_applicable',
  'not_activated',
  'scheduled',
  'activated',
  'rolled_back',
  'superseded'
);

create type runtime.artifact_status as enum (
  'draft',
  'under_review',
  'approved',
  'active',
  'paused',
  'rolled_back',
  'superseded',
  'archived'
);

create type knowledge.signal_origin as enum (
  'external',
  'internal'
);

create type knowledge.signal_disposition as enum (
  'dropped',
  'buffered',
  'kept'
);

create type knowledge.source_status as enum (
  'open',
  'closed',
  'merged',
  'split',
  'superseded'
);

create type knowledge.claim_status as enum (
  'candidate',
  'active',
  'superseded',
  'refuted'
);

create type knowledge.validation_verdict as enum (
  'supported',
  'refuted',
  'conflict',
  'not_enough_info',
  'needs_review'
);
03

Core

Case structures

core.cases is a projection, not a mutable row — a materialized read model built from the case event stream. Only the seed is fixed (case_id, tenant_id, case_number, created_at — set at CaseCreated, the FK anchor for everything else); every operational field (title, status, ownership, assignment, domain, entities) is projected from events and rebuildable. The write path appends business events — CaseAssigned, OwnershipChanged, StatusChanged, EntitiesExtracted — and this row is reprojected from them.

Materialized and live. A projection is still a table — cheap to read for the inbox — but maintained live by applying events, never authoritatively UPDATEd, and rebuildable from the stream. No version (optimistic concurrency doesn't apply) and no staleness metadata — the row always reflects the latest events. Freshness / stale_state lives on the async generated projections (summaries, world-model facts), which can lag — not here.
create table core.cases (
  case_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  case_number text not null,
  title text, -- LLM-generated label, human-overridable

  owning_team_id uuid not null,
  department_id uuid,
  assigned_user_id uuid,
  agent_active boolean not null default false,
  requester_ref jsonb, -- raw source actor: { source_system, external_id, display_name, email }
  requester_id uuid references identity.people(person_id), -- resolved canonical person, nullable
  watcher_ids uuid[] not null default '{}',

  case_status core.case_status not null default 'open',
  resolution_state core.resolution_state not null default 'unresolved',
  ownership core.ownership_state generated always as (
    case when agent_active then 'agent'::core.ownership_state
         else 'waiting'::core.ownership_state end
  ) stored,

  domain text,
  subdomain text,
  primary_entity_ids text[] not null default '{}',
  affected_user_ids uuid[] not null default '{}',
  affected_system_ids text[] not null default '{}',
  customer_refs jsonb not null default '[]'::jsonb,

  created_at timestamptz not null default now(), -- seed: set at CaseCreated, immutable
  updated_at timestamptz not null default now(), -- time of the latest event applied; always current

  unique (tenant_id, case_number)
);

create table core.case_external_refs (
  case_external_ref_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  case_id uuid not null references core.cases(case_id),
  ref_kind text not null, -- channel, thread, file, source_object, external_ticket
  source_system text not null,
  external_id text not null,
  external_url text,
  metadata jsonb not null default '{}'::jsonb,
  created_at timestamptz not null default now(),
  unique (tenant_id, source_system, external_id)
);

create table core.case_relationships (
  relationship_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  from_case_id uuid not null references core.cases(case_id),
  to_case_id uuid not null references core.cases(case_id),
  relationship_type text not null, -- related, duplicate_candidate, parent, child, merged_into
  status text not null default 'active',
  reason text,
  created_by uuid,
  created_at timestamptz not null default now(),
  superseded_by uuid,
  check (from_case_id <> to_case_id)
);

create table core.case_timers (
  timer_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  case_id uuid not null references core.cases(case_id),
  subject_ref jsonb, -- what it watches: the case, a decision packet, a governed action
  kind text not null, -- processing_watchdog, sla, ask_expiry, follow_up
  fire_at timestamptz not null, -- indexed; the scheduler scans this
  escalate_to jsonb, -- where the ball goes when it fires: court / actor / team
  on_fire core.escalation_action not null default 'nudge', -- default rung; policy may override
  policy_ref uuid, -- escalation-ladder policy, re-evaluated on fire
  status text not null default 'pending', -- pending, fired, cancelled, superseded
  created_at timestamptz not null default now(),
  fired_at timestamptz
);
04

Lifecycle

Case lifecycle diagram

case_status is just lifecycle — active → finished. Everything operational (processing, waiting, blocked) is the court (ownership), not a status. The governing invariant: a case is never static — it is either the agent's (processing), or it has an open ask waiting on a human, or it is finished. "Blocked" is never bare; it always means an open ask aimed at someone, and a deadline that will move it if no one answers.

Case status — lifecycle only

stateDiagram-v2
  [*] --> Open: case created
  Open --> Resolved: outcome reached
  Resolved --> Closed: retention / audit checkpoint
  Closed --> Archived: no longer active work

  Resolved --> Open: reopen on material change
  Closed --> Open: reopen by authorized user

  note right of Open
    Open = active. Processing vs waiting is the
    court (ownership), not a status. Blocked is
    never bare: it is always an open ask + a timer.
  end note
                  

Court and escalation ladder

flowchart LR
  Created([case created]) --> Agent
  Agent[Agent processing
ownership = agent] -->|resolved| Finished([resolved / closed]) Agent -->|cannot proceed: raise ask| Waiting[Waiting on a human
ownership = waiting] Waiting -->|answer received| Agent Waiting -->|human resolves| Finished Agent -. arms .-> Timer{{case_timers
watchdog / sla / ask_expiry}} Waiting -. arms .-> Timer Timer -. fires, runs policy .-> Ladder[Escalation ladder
nudge → advance → reassign → broaden → fail] Ladder -. moves the court .-> Waiting
05

Projections

Generated case projections

Generated objects need source events, generator version, stale state, and evidence refs. The frontend should query these instead of reconstructing case meaning from raw events.

create table core.case_summaries (
  case_summary_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  case_id uuid not null references core.cases(case_id),
  summary_version integer not null,
  brief text not null,
  current_status_text text,
  attention_reason text, -- why the case needs attention right now (SLA approaching, new reply, blocker cleared)
  next_action jsonb, -- suggested next action(s); an object, may carry several ranked suggestions + governed-action refs
  source_event_ids uuid[] not null default '{}',
  generator_version text not null,
  evidence_refs jsonb not null default '[]'::jsonb,
  hidden_evidence_state jsonb not null default '{}'::jsonb,
  confidence numeric(5,4),
  stale_state core.stale_state not null default 'fresh',
  stale_reason text,
  superseded_by uuid,
  generated_at timestamptz not null default now(),
  unique (case_id, summary_version)
);

create table core.case_chapters (
  chapter_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  case_id uuid not null references core.cases(case_id),
  chapter_type text not null,
  title text not null,
  summary text not null,
  expanded_summary text,
  chapter_status text not null default 'open',
  started_at timestamptz not null,
  closed_at timestamptz,
  source_event_range tstzrange,
  event_refs uuid[] not null default '{}',
  evidence_refs jsonb not null default '[]'::jsonb,
  hidden_evidence_state jsonb not null default '{}'::jsonb,
  state_changes jsonb not null default '[]'::jsonb,
  revision_reason text,
  confidence numeric(5,4),
  superseded_by uuid,
  generated_at timestamptz not null default now()
);

create table core.case_operational_projections (
  projection_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  case_id uuid not null references core.cases(case_id),
  effective_priority jsonb,
  sla_state jsonb,
  risk_state jsonb,
  placement_reason jsonb,
  required_actor jsonb,
  ownership core.ownership_state not null,
  escalation_state jsonb,
  policy_evaluation_refs uuid[] not null default '{}',
  stale_state core.stale_state not null default 'fresh',
  generated_at timestamptz not null default now(),
  source_event_ids uuid[] not null default '{}',
  generator_version text not null
);
06

Decisions

Decision packets and governed actions

Decision Packet is the portable human ask. Governed Action is the world-changing operation behind an action button, approval, tool execution, correction application, message send, case merge, or artifact activation.

create table core.decision_packets (
  decision_packet_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  case_id uuid not null references core.cases(case_id),
  question text not null,
  decision_type text not null,
  attention_reason text, -- why this decision is being asked now
  impact jsonb not null default '{}'::jsonb,
  suggested_actions jsonb not null default '[]'::jsonb, -- ranked recommended responses; each may map to / create a governed action
  required_actor jsonb,
  eligible_actors jsonb not null default '[]'::jsonb,
  actor_reason text,
  delivery_routes jsonb not null default '[]'::jsonb,
  evidence_refs jsonb not null default '[]'::jsonb,
  evidence_summary text,
  hidden_evidence_state jsonb not null default '{}'::jsonb,
  source_authority_refs uuid[] not null default '{}',
  conflicts jsonb not null default '[]'::jsonb,
  linked_governed_action_ids uuid[] not null default '{}',
  status core.decision_packet_status not null default 'active',
  -- expiry deadline lives in core.case_timers (kind = ask_expiry), not here
  stale_reason text,
  superseded_by uuid,
  answered_by uuid,
  answered_at timestamptz,
  created_at timestamptz not null default now()
);

create table core.decision_packet_deliveries (
  delivery_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  decision_packet_id uuid not null references core.decision_packets(decision_packet_id),
  channel text not null,
  external_message_ref text,
  delivered_to jsonb not null default '[]'::jsonb,
  delivery_status text not null default 'pending',
  delivered_at timestamptz,
  last_synced_at timestamptz,
  metadata jsonb not null default '{}'::jsonb
);

create table core.governed_actions (
  action_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  case_id uuid references core.cases(case_id),
  action_kind text not null,
  tool_id text,
  source_system text,
  target_refs jsonb not null default '[]'::jsonb,
  requested_by uuid,
  requested_at timestamptz not null default now(),
  proposed_result jsonb,

  status core.governed_action_status not null default 'draft',
  policy_evaluation_ref uuid,
  policy_refs uuid[] not null default '{}',
  risk_score numeric(6,3),
  permission_state jsonb not null default '{}'::jsonb,
  automation_scope text,
  approval_requirement jsonb,
  expert_review_requirement jsonb,
  dry_run_result jsonb,
  block_reason text,

  approval_state jsonb not null default '{}'::jsonb,
  required_approvers jsonb not null default '[]'::jsonb,
  eligible_approvers jsonb not null default '[]'::jsonb,
  self_approval_block boolean not null default false,
  delegation_state jsonb not null default '{}'::jsonb,
  approval_decision jsonb,

  execution_state text,
  idempotency_key text not null,
  external_operation_refs jsonb not null default '[]'::jsonb,
  started_at timestamptz,
  completed_at timestamptz,
  result jsonb,
  failure_reason text,
  partial_failure jsonb,
  compensating_action_ref uuid,

  event_refs uuid[] not null default '{}',
  evidence_refs jsonb not null default '[]'::jsonb,
  decision_packet_id uuid references core.decision_packets(decision_packet_id),
  actor_trace jsonb not null default '[]'::jsonb,
  replay_ref uuid,
  created_at timestamptz not null default now(),
  unique (tenant_id, idempotency_key)
);
07

Lifecycles

Decision and action lifecycle diagrams

Decision Packet

stateDiagram-v2
  [*] --> Active: created
  Active --> Answered: actor responds
  Active --> Expired: expires_at reached
  Active --> Stale: source/case changed
  Active --> Superseded: replaced by newer ask
  Stale --> Superseded: regenerated
  Expired --> Superseded: regenerated
  Active --> Cancelled: no longer needed
  Answered --> [*]
  Cancelled --> [*]
  Superseded --> [*]
                  

Governed Action

stateDiagram-v2
  [*] --> Draft
  Draft --> PendingPolicy: requested
  PendingPolicy --> Blocked: policy denies
  PendingPolicy --> NeedsApproval: policy requires approval
  PendingPolicy --> NeedsExpertReview: expert required
  PendingPolicy --> Approved: policy permits
  NeedsApproval --> Approved: approval granted
  NeedsApproval --> Blocked: approval denied
  NeedsExpertReview --> Approved: expert clears
  NeedsExpertReview --> Blocked: expert blocks
  Approved --> Executing
  Executing --> Succeeded
  Executing --> Failed
  Executing --> PartiallyFailed
  Failed --> Executing: retry
  PartiallyFailed --> Executing: recover
  Failed --> Cancelled
  Blocked --> Superseded: newer action replaces
  Draft --> Cancelled
  Succeeded --> [*]
  Cancelled --> [*]
  Superseded --> [*]
                  
03

Identity

Identity and channel aliasing

One canonical person carries a central UUID; each platform identity (Slack, email, ticketing) is an alias that points at it. Inbound requesters arrive as a source-shaped requester_ref and are resolved to a person through the alias table — lazily creating a provisional contact when the channel identity is new.

Mint where you can be certain. A channel identity (source_system, external_id) maps to exactly one person, idempotently. Deciding that two people across channels are the same human is a separate merged_into_person_id step — not an intake decision.
create table identity.people (
  person_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  kind text not null default 'external_contact', -- internal_user, external_contact
  status text not null default 'provisional', -- provisional, active, merged
  display_name text,
  primary_email text,
  confidence numeric(5,4),
  merged_into_person_id uuid references identity.people(person_id),
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table identity.person_aliases (
  alias_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  person_id uuid not null references identity.people(person_id),
  source_system text not null, -- slack, gmail, zendesk, teams
  external_id text not null,
  handle text,
  display_name text,
  email text,
  verified boolean not null default false,
  confidence numeric(5,4),
  resolution_method text, -- exact_email, slack_profile, manual
  evidence_refs jsonb not null default '[]'::jsonb,
  first_seen_at timestamptz not null default now(),
  last_seen_at timestamptz not null default now(),
  unique (tenant_id, source_system, external_id)
);

create table identity.users (
  user_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  person_id uuid not null references identity.people(person_id),
  email text not null,
  display_name text,
  status text not null default 'active', -- active, invited, suspended
  roles text[] not null default '{}',
  created_at timestamptz not null default now(),
  unique (tenant_id, email),
  unique (tenant_id, person_id)
);
08

Governance

Governance candidate structures

Governance Candidate is the review wrapper. Its payload may be a policy rule, authority rule, notification rule, runtime artifact, permission rule, or approval rule.

create table gov.governance_candidates (
  candidate_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  candidate_type text not null,
  review_status gov.candidate_review_status not null default 'draft',
  activation_status gov.candidate_activation_status not null default 'not_activated',
  version integer not null default 1,
  created_by uuid,
  created_at timestamptz not null default now(),

  origin_channel text,
  origin_case_id uuid references core.cases(case_id),
  origin_object_ref jsonb,
  origin_context_label text,
  source_run_id uuid,
  source_thread_id text,
  paired_one_off_ref uuid,

  condition jsonb not null default '{}'::jsonb,
  outcome jsonb not null default '{}'::jsonb,
  scope jsonb not null default '{}'::jsonb,
  rationale text,
  payload_ref jsonb not null default '{}'::jsonb,
  affected_capabilities text[] not null default '{}',

  evidence_refs jsonb not null default '[]'::jsonb,
  example_case_ids uuid[] not null default '{}',
  conflict_refs uuid[] not null default '{}',
  risk_summary jsonb not null default '{}'::jsonb,
  blast_radius jsonb not null default '{}'::jsonb,
  reversibility jsonb not null default '{}'::jsonb,

  review_requirement jsonb not null default '{}'::jsonb,
  reviewers jsonb not null default '[]'::jsonb,
  review_comments jsonb not null default '[]'::jsonb,
  decision text,
  decision_reason text,
  decided_by uuid,
  decided_at timestamptz,

  simulation_refs uuid[] not null default '{}',
  activation_scope jsonb,
  effective_at timestamptz,
  activated_ref jsonb,
  monitoring_plan jsonb,
  rollback_ref jsonb,
  superseded_by uuid
);

create table gov.candidate_status_events (
  candidate_status_event_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  candidate_id uuid not null references gov.governance_candidates(candidate_id),
  previous_review_status gov.candidate_review_status,
  next_review_status gov.candidate_review_status,
  previous_activation_status gov.candidate_activation_status,
  next_activation_status gov.candidate_activation_status,
  reason text,
  actor_ref jsonb,
  occurred_at timestamptz not null default now(),
  event_ref uuid
);
09

Lifecycle

Governance Candidate lifecycle

Review and activation are separate axes. A reviewer can approve a candidate without activating it immediately.

Review lifecycle

stateDiagram-v2
  [*] --> Draft
  Draft --> Submitted
  Submitted --> UnderReview
  UnderReview --> NeedsChanges
  NeedsChanges --> Submitted
  UnderReview --> Approved
  UnderReview --> Denied
  Submitted --> Stale: source context changed
  Approved --> Superseded: newer candidate replaces
  Denied --> Archived
  Stale --> Archived
  Superseded --> Archived
  Approved --> Archived: no activation needed
                  

Activation lifecycle

stateDiagram-v2
  [*] --> NotActivated
  NotActivated --> Scheduled: approved and scheduled
  Scheduled --> Activated: effective_at reached
  NotActivated --> Activated: immediate activation
  Activated --> RolledBack: rollback action
  Activated --> Superseded: newer version activated
  Scheduled --> NotActivated: cancelled before effect
  RolledBack --> [*]
  Superseded --> [*]
                  
10

Runtime

Runtime artifact structures

Policy should be represented as artifact_kind = 'policy', with PolicyRule as the structured payload of an artifact version.

create table runtime.runtime_artifacts (
  artifact_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  artifact_kind text not null,
  name text not null,
  owner jsonb not null default '{}'::jsonb,
  scope jsonb not null default '{}'::jsonb,
  status runtime.artifact_status not null default 'draft',
  current_version_id uuid,
  activation_scope jsonb,
  created_from_candidate_id uuid references gov.governance_candidates(candidate_id),
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table runtime.artifact_versions (
  artifact_version_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  artifact_id uuid not null references runtime.runtime_artifacts(artifact_id),
  semver text not null,
  payload_schema text not null,
  payload jsonb not null,
  change_summary text,
  source_evidence_refs jsonb not null default '[]'::jsonb,
  eval_summary jsonb not null default '{}'::jsonb,
  safety_status text,
  rollback_target_version_id uuid,
  created_by uuid,
  created_at timestamptz not null default now(),
  unique (artifact_id, semver)
);

create table runtime.artifact_reviews (
  artifact_review_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  artifact_version_id uuid not null references runtime.artifact_versions(artifact_version_id),
  reviewer uuid,
  decision text not null,
  decision_reason text,
  conditions jsonb not null default '[]'::jsonb,
  activation_scope jsonb,
  decided_at timestamptz not null default now()
);

alter table runtime.runtime_artifacts
  add constraint runtime_artifacts_current_version_fk
  foreign key (current_version_id)
  references runtime.artifact_versions(artifact_version_id);
11

Policy

Policy rule and evaluation structures

PolicyEvaluation must answer "why this?" by preserving matched rules, skipped rules, overrides, conflicts, and the winning effective value.

create table gov.policy_rules (
  rule_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  rule_type text not null,
  owner jsonb not null default '{}'::jsonb,
  scope jsonb not null default '{}'::jsonb,
  conditions jsonb not null default '{}'::jsonb,
  outcomes jsonb not null default '{}'::jsonb,
  exceptions jsonb not null default '[]'::jsonb,
  priority integer not null default 100,
  effective_at timestamptz,
  expires_at timestamptz,
  version integer not null default 1,
  runtime_artifact_version_ref uuid references runtime.artifact_versions(artifact_version_id),
  rollback_ref jsonb,
  created_at timestamptz not null default now()
);

create table gov.policy_evaluations (
  evaluation_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  case_id uuid references core.cases(case_id),
  input_projection_ref uuid,
  policy_version_refs uuid[] not null default '{}',
  rule_matches jsonb not null default '[]'::jsonb,
  skipped_rules jsonb not null default '[]'::jsonb,
  winning_rule uuid,
  overrides jsonb not null default '[]'::jsonb,
  conflicts jsonb not null default '[]'::jsonb,
  effective_value jsonb not null default '{}'::jsonb,
  reason text,
  confidence numeric(5,4),
  evaluated_context_hash text,
  evaluated_at timestamptz not null default now()
);

create table gov.policy_simulations (
  simulation_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  candidate_id uuid references gov.governance_candidates(candidate_id),
  input_rule jsonb not null,
  sample_case_ids uuid[] not null default '{}',
  before_after jsonb not null default '[]'::jsonb,
  conflicts jsonb not null default '[]'::jsonb,
  warnings jsonb not null default '[]'::jsonb,
  uncertainty jsonb not null default '{}'::jsonb,
  generated_at timestamptz not null default now()
);
12

Knowledge

The knowledge layer — how the app learns truth

This is the epistemic spine: a signal (external observation or internal system fact) is conceptualized into a source, which yields claims (hypotheses), backed by frozen justifications (evidence), tested by validation (authority + conflict + review), producing world-model facts — the validated, bitemporal truth. Internal signals are authoritative facts recorded directly; external signals are observations that must earn their way.

Validation is relative to current knowledge. A signal earns knowledge by what it does to the world model — confirms (reinforce), adds (new claim → validate), contradicts (conflict → review), or touches nothing (reference + digest only). Truth is live and re-projected; justifications are frozen at the moment of assertion, and verified before they freeze.
create table knowledge.signals (
  signal_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  origin knowledge.signal_origin not null, -- external = observation, internal = system fact
  signal_type text not null, -- message, email, ticket_update | action_executed, case_changed, decision_made
  source_system text not null,
  source_ref jsonb not null, -- reference + digest (external) or ledger event_id (internal); never the raw body
  subject_refs jsonb not null default '[]'::jsonb, -- entities/cases it touches; empty = touches nothing
  case_id uuid references core.cases(case_id),
  observed_at timestamptz not null,
  recorded_at timestamptz not null default now(),
  worth jsonb, -- calibrated composite: belief_change, decision_relevance, source_authority, rarity, extraction_confidence
  disposition knowledge.signal_disposition not null default 'buffered', -- dropped, buffered, kept
  source_id uuid references knowledge.sources(source_id), -- set once grouped into a source
  fidelity text not null default 'reference_digest', -- structured, reference_digest, raw_short
  retention_class text not null default 'standard'
);

create table knowledge.sources (
  source_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  source_system text not null,
  kind text not null, -- conversation, email_thread, ticket, document
  title text, -- generated label
  summary text, -- redacted digest, not raw content
  signal_ids uuid[] not null default '{}', -- the signals it grouped (provenance)
  observed_window tstzrange,
  status knowledge.source_status not null default 'open', -- accumulate then close; merge/split/retract are first-class
  supersedes_source_id uuid references knowledge.sources(source_id),
  valid_from timestamptz, -- world time
  valid_to timestamptz,
  observed_at timestamptz not null default now(), -- system time
  superseded_at timestamptz,
  closed_at timestamptz
);

create table knowledge.claims (
  claim_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  source_id uuid references knowledge.sources(source_id), -- which source it came from
  claim_type text not null,
  subject_ref jsonb not null, -- the entity
  predicate text not null,
  object jsonb not null, -- value or ref
  confidence numeric(5,4), -- extraction confidence; LLMs are overconfident, so calibrate
  status knowledge.claim_status not null default 'candidate', -- candidate, active, superseded, refuted
  justification_id uuid references knowledge.justifications(justification_id),
  valid_from timestamptz, -- world time
  valid_to timestamptz,
  observed_at timestamptz not null default now(), -- system time
  superseded_at timestamptz,
  extractor_ref jsonb
);

create table knowledge.justifications (
  justification_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  asserts_ref jsonb not null, -- what it justifies: a claim, decision, artifact, or fact
  supporting_signal_ids uuid[] not null default '{}', -- bounded and acyclic (foundationalist)
  supporting_claim_ids uuid[] not null default '{}',
  supporting_fact_ids uuid[] not null default '{}',
  reasoning text,
  source_trust jsonb not null default '{}'::jsonb, -- per-source trust signal; provenance is not trust
  verified boolean not null default false, -- verified at freeze (entailment check)
  verification jsonb,
  captured_at timestamptz not null default now() -- frozen snapshot; immutable
);

create table knowledge.validations (
  validation_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  claim_id uuid not null references knowledge.claims(claim_id),
  subject_ref jsonb not null, -- entity/field
  verdict knowledge.validation_verdict not null, -- supported, refuted, conflict, not_enough_info, needs_review
  authority_refs uuid[] not null default '{}', -- authority rules applied as prior
  conflict_id uuid references knowledge.conflicts(conflict_id),
  effective_fact_id uuid references knowledge.world_model_facts(fact_id),
  reviewer uuid, -- set when a human adjudicated the band
  review_reason text,
  decided_at timestamptz not null default now()
);

create table knowledge.world_model_facts (
  fact_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  entity_ref jsonb not null, -- the entity, e.g. { kind: system, id: vpn }
  field text not null, -- owner, status, ...
  value jsonb not null, -- the effective value, e.g. Clara
  confidence numeric(5,4), -- calibrated
  freshness jsonb not null default '{}'::jsonb, -- last_verified, decay
  authority_source jsonb, -- which source/claim won (prior + evidence)
  justification_id uuid references knowledge.justifications(justification_id), -- why we believe it
  valid_from timestamptz not null, -- when true in the world
  valid_to timestamptz, -- null = still believed true
  observed_at timestamptz not null default now(), -- when the system learned it
  superseded_at timestamptz, -- invalidate, never delete
  superseded_by uuid references knowledge.world_model_facts(fact_id)
);

create table knowledge.authority_rules (
  rule_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  field_or_entity_scope jsonb not null,
  source_precedence jsonb not null default '[]'::jsonb, -- declared PRIOR, not hard precedence
  role_precedence jsonb not null default '[]'::jsonb,
  learned_reliability jsonb not null default '{}'::jsonb, -- empirical per-source reliability adjusts the prior
  conflict_behavior text not null,
  exceptions jsonb not null default '[]'::jsonb,
  owner jsonb,
  runtime_artifact_version_ref uuid references runtime.artifact_versions(artifact_version_id),
  created_at timestamptz not null default now(),
  superseded_by uuid
);

create table knowledge.conflicts (
  conflict_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  target_ref jsonb not null, -- entity/field in dispute
  conflicting_claim_ids uuid[] not null,
  independence jsonb not null default '{}'::jsonb, -- copying/dependence check; corroboration is not independence
  current_effective_fact_id uuid references knowledge.world_model_facts(fact_id),
  resolution_state text not null default 'unresolved', -- unresolved, resolved, independent
  reviewer uuid,
  resolution_reason text,
  created_at timestamptz not null default now(),
  resolved_at timestamptz
);

create table knowledge.corrections (
  correction_id uuid primary key default gen_random_uuid(),
  tenant_id uuid not null,
  case_id uuid references core.cases(case_id),
  target_ref jsonb not null,
  field text not null,
  proposed_value jsonb not null,
  current_value jsonb,
  correction_scope text not null, -- case_only, durable_truth, authority_issue, governance_candidate_trigger
  reason text,
  submitted_by uuid, -- human corrections are high-trust and train authority
  outcome jsonb,
  candidate_ref uuid references gov.governance_candidates(candidate_id),
  created_at timestamptz not null default now(),
  decided_at timestamptz
);
13

Lifecycle

Validation and truth flow

A signal is validated by what it does to current knowledge. Internal signals are facts; external observations must pass a worth gate, then a truth gate.

Signal validation funnel

flowchart TD
  Signal([Signal]) --> Origin{origin?}
  Origin -->|internal · system fact| Fact[recorded as fact → truth]
  Origin -->|external · observation| Touch{touches a tracked
entity or case?} Touch -->|no| Drop[reference + digest only
not knowledge] Touch -->|yes| Source[group into Source] --> Claim[extract claim · hypothesis] Claim --> Worth{vs current world model} Worth -->|confirms| Reinforce[reinforce
raise confidence / freshness] Worth -->|adds new| Validate[validate → world-model fact] Worth -->|contradicts| Conflict[conflict → authority + review] Worth -->|redundant| Noop[NOOP]

Epistemic spine — source to truth

flowchart LR
  Signal[Signal
external obs · internal fact] --> Source[Source
conceptualized unit] Source --> Claim[Claim
hypothesis + confidence] Claim --> Validation[Validation
authority + conflict + review] Validation --> Truth[World-model fact
bitemporal truth] Claim -. justified by .-> Justification[Justification
frozen evidence bundle] Truth -. justified by .-> Justification Correction[Human correction] -. high-trust .-> Validation
14

Ledger

Event and projection envelopes

case_id is optional on events because source, governance, artifact, and identity events can happen before or outside a case.

create table ledger.event_envelopes (
  event_id uuid primary key default gen_random_uuid(),
  event_type text not null,
  tenant_id uuid not null,
  subject_ref jsonb not null,
  case_id uuid references core.cases(case_id),
  occurred_at timestamptz not null,
  recorded_at timestamptz not null default now(),
  actor_ref jsonb,
  source_ref jsonb,
  correlation_id text,
  causation_id uuid,
  sequence bigint,
  idempotency_key text,
  entity_refs jsonb not null default '[]'::jsonb,
  artifact_refs uuid[] not null default '{}',
  payload_schema_version text not null,
  payload_ref text,
  payload jsonb,
  access_class text not null default 'standard',
  retention_class text not null default 'standard',
  unique (tenant_id, idempotency_key)
);

create table ledger.projection_envelopes (
  projection_id uuid primary key default gen_random_uuid(),
  projection_type text not null,
  tenant_id uuid not null,
  subject_ref jsonb not null,
  source_event_ids uuid[] not null default '{}',
  source_watermark jsonb,
  generated_at timestamptz not null default now(),
  generator_version text not null,
  payload_schema_version text not null,
  policy_version_refs uuid[] not null default '{}',
  artifact_version_refs uuid[] not null default '{}',
  stale_state core.stale_state not null default 'fresh',
  stale_reason text,
  superseded_by uuid,
  confidence numeric(5,4),
  payload jsonb not null
);
15

Review

MVP review questions

  1. Can case_status be limited to six values: open, awaiting_human, blocked, resolved, closed, archived?
  2. Should in_progress be removed from Case status and represented only through automation/projection state?
  3. Should artifact activation always create a core.governed_actions row?
  4. Should display_policy be a candidate type, or only an artifact kind?
  5. Should PolicyEvaluation be per field or bundled per case evaluation run?
  6. Are Governance Candidate payloads better stored as typed refs, JSON payloads, or both?
  7. Which generated projections must exist for MVP: summary, operational projection, current ask, evidence summary, or all four?
Copied