-- 022_canonical_facts.sql — 0pt canonical structured observations (X-800 / Scrape). -- -- 0pt owns normalized OBSERVED facts (what a source said, when, from which -- artifact). L0RE owns epistemic propositions/assertions and projections; it -- must consume these rows through this API/contract, never by writing here. -- -- Conflict preservation is structural: the unique key includes source_ref and -- observed_at, so two sources disagreeing about the same predicate are two -- rows, never an overwrite. `status` records observed/supported/disputed/ -- retracted; reconciliation is a separate, explicit decision. -- -- Tenant isolation: RLS is FORCEd and keyed on a transaction-local GUC that -- only trusted server code sets (same pattern as 019_document_credit_rls.sql). CREATE TABLE IF NOT EXISTS opt_canonical_entities ( entity_id UUID PRIMARY KEY, tenant_id TEXT NOT NULL, entity_type TEXT NOT NULL, -- company|person|web_page|product|... identity_namespace TEXT NOT NULL, -- url|orgnr|lei|gtin|... identity_value TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (tenant_id, entity_type, identity_namespace, identity_value) ); CREATE INDEX IF NOT EXISTS idx_canonical_entities_tenant_type ON opt_canonical_entities (tenant_id, entity_type); CREATE TABLE IF NOT EXISTS opt_canonical_facts ( fact_id UUID PRIMARY KEY, tenant_id TEXT NOT NULL, entity_id UUID NOT NULL REFERENCES opt_canonical_entities (entity_id), predicate TEXT NOT NULL, -- e.g. web.title, product.price value_json JSONB NOT NULL, value_type TEXT NOT NULL, -- string|number|boolean|date|json value_unit TEXT, source_ref TEXT NOT NULL, source_record_id TEXT, st0re_artifact_id TEXT, aud0_evidence_ref TEXT, run_ref TEXT, extractor_version TEXT NOT NULL, observed_at TIMESTAMPTZ NOT NULL, valid_from TIMESTAMPTZ, valid_to TIMESTAMPTZ, confidence NUMERIC CHECK (confidence BETWEEN 0 AND 1), quality_status TEXT NOT NULL DEFAULT 'unvalidated', -- passed|warned|failed|unvalidated schema_version TEXT NOT NULL DEFAULT '0pt.canonical_fact/v1', status TEXT NOT NULL DEFAULT 'observed' CHECK (status IN ('observed','supported','disputed','retracted')), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (tenant_id, entity_id, predicate, source_ref, observed_at) ); CREATE INDEX IF NOT EXISTS idx_canonical_facts_entity ON opt_canonical_facts (tenant_id, entity_id, predicate); CREATE INDEX IF NOT EXISTS idx_canonical_facts_predicate ON opt_canonical_facts (tenant_id, predicate, observed_at DESC); CREATE TABLE IF NOT EXISTS opt_canonical_relations ( relation_id UUID PRIMARY KEY, tenant_id TEXT NOT NULL, subject_id UUID NOT NULL REFERENCES opt_canonical_entities (entity_id), predicate TEXT NOT NULL, -- LINKS_TO|SOLD_BY|USES_SOFTWARE|... object_id UUID NOT NULL REFERENCES opt_canonical_entities (entity_id), attributes_json JSONB NOT NULL DEFAULT '{}'::jsonb, source_ref TEXT NOT NULL, st0re_artifact_id TEXT, aud0_evidence_ref TEXT, run_ref TEXT, extractor_version TEXT NOT NULL, observed_at TIMESTAMPTZ NOT NULL, valid_from TIMESTAMPTZ, valid_to TIMESTAMPTZ, confidence NUMERIC CHECK (confidence BETWEEN 0 AND 1), schema_version TEXT NOT NULL DEFAULT '0pt.canonical_relation/v1', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (tenant_id, subject_id, predicate, object_id, source_ref, observed_at) ); CREATE INDEX IF NOT EXISTS idx_canonical_relations_subject ON opt_canonical_relations (tenant_id, subject_id, predicate); CREATE INDEX IF NOT EXISTS idx_canonical_relations_object ON opt_canonical_relations (tenant_id, object_id, predicate); ALTER TABLE opt_canonical_entities ENABLE ROW LEVEL SECURITY; ALTER TABLE opt_canonical_entities FORCE ROW LEVEL SECURITY; DROP POLICY IF EXISTS opt_canonical_entities_tenant ON opt_canonical_entities; CREATE POLICY opt_canonical_entities_tenant ON opt_canonical_entities USING (tenant_id = current_setting('app.opt_canonical_tenant_id', true)) WITH CHECK (tenant_id = current_setting('app.opt_canonical_tenant_id', true)); ALTER TABLE opt_canonical_facts ENABLE ROW LEVEL SECURITY; ALTER TABLE opt_canonical_facts FORCE ROW LEVEL SECURITY; DROP POLICY IF EXISTS opt_canonical_facts_tenant ON opt_canonical_facts; CREATE POLICY opt_canonical_facts_tenant ON opt_canonical_facts USING (tenant_id = current_setting('app.opt_canonical_tenant_id', true)) WITH CHECK (tenant_id = current_setting('app.opt_canonical_tenant_id', true)); ALTER TABLE opt_canonical_relations ENABLE ROW LEVEL SECURITY; ALTER TABLE opt_canonical_relations FORCE ROW LEVEL SECURITY; DROP POLICY IF EXISTS opt_canonical_relations_tenant ON opt_canonical_relations; CREATE POLICY opt_canonical_relations_tenant ON opt_canonical_relations USING (tenant_id = current_setting('app.opt_canonical_tenant_id', true)) WITH CHECK (tenant_id = current_setting('app.opt_canonical_tenant_id', true));