Files
inter-hub/Application/Schema.sql
Bernd Worsch 55af11342d feat(P6/T01): Phase 6 schema — WidgetAdapterSpec, contracts, widgets.adapter_spec_id
Adds Phase 6 tables: envelope_emission_contracts, interaction_reporting_contracts,
widget_adapter_specs. Adds adapter_spec_id FK to widgets and api_key to hubs.
Seeds v1.0 contracts in migration. Registers Phase 6 controller types and routes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 21:03:00 +00:00

382 lines
16 KiB
PL/PgSQL

-- IHF Phase 1 + Phase 2 Schema
-- Hub, Widget, WidgetVersion, InteractionEvent, Annotation
-- Phase 2: AnnotationThread, RequirementCandidate, TriageState, ReviewerAssignment
-- See workplans/IHUB-WP-0001-ihf-phase1-minimal-interaction-core.md
-- See workplans/IHUB-WP-0002-ihf-phase2-structured-feedback-and-triage.md
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Users (T10 — authentication)
CREATE TABLE users (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
name TEXT NOT NULL,
locked_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
failed_login_attempts INT NOT NULL DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
-- Hubs — bounded domains of responsibility
CREATE TABLE hubs (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
slug TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
domain TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
-- Widgets — smallest semantically governable interaction units
CREATE TABLE widgets (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
hub_id UUID NOT NULL REFERENCES hubs(id) ON DELETE RESTRICT,
name TEXT NOT NULL,
widget_type TEXT NOT NULL,
capability_ref TEXT,
view_context TEXT,
policy_scope TEXT NOT NULL DEFAULT 'internal',
status TEXT NOT NULL DEFAULT 'active',
version INT NOT NULL DEFAULT 1,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
-- Widget version history
CREATE TABLE widget_versions (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
widget_id UUID NOT NULL REFERENCES widgets(id) ON DELETE CASCADE,
version INT NOT NULL,
schema_snapshot JSONB NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,
UNIQUE (widget_id, version)
);
-- Interaction events — append-only capture
CREATE TABLE interaction_events (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
widget_id UUID NOT NULL REFERENCES widgets(id) ON DELETE CASCADE,
event_type TEXT NOT NULL,
actor_id UUID,
actor_type TEXT NOT NULL DEFAULT 'user',
view_context_ref TEXT,
metadata JSONB DEFAULT '{}' NOT NULL,
occurred_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX interaction_events_widget_id_idx ON interaction_events (widget_id);
CREATE INDEX interaction_events_occurred_at_idx ON interaction_events (occurred_at DESC);
-- Enforce append-only on interaction_events
CREATE OR REPLACE FUNCTION prevent_interaction_event_mutation()
RETURNS TRIGGER AS $$
BEGIN
RAISE EXCEPTION 'interaction_events is append-only: UPDATE and DELETE are not permitted';
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER interaction_events_no_update
BEFORE UPDATE ON interaction_events
FOR EACH ROW EXECUTE FUNCTION prevent_interaction_event_mutation();
CREATE TRIGGER interaction_events_no_delete
BEFORE DELETE ON interaction_events
FOR EACH ROW EXECUTE FUNCTION prevent_interaction_event_mutation();
-- Annotation threads — groups related annotations for triage (Phase 2)
CREATE TABLE annotation_threads (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
widget_id UUID NOT NULL REFERENCES widgets(id) ON DELETE CASCADE,
title TEXT NOT NULL,
description TEXT,
created_by UUID REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
-- Annotations — structured commentary, also append-only by convention
-- Phase 2 additions: severity, thread_id
CREATE TABLE annotations (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
widget_id UUID NOT NULL REFERENCES widgets(id) ON DELETE CASCADE,
parent_id UUID REFERENCES annotations(id) ON DELETE CASCADE,
body TEXT NOT NULL,
category TEXT NOT NULL DEFAULT 'friction',
severity TEXT NOT NULL DEFAULT 'medium',
thread_id UUID REFERENCES annotation_threads(id) ON DELETE SET NULL,
actor_id UUID,
actor_type TEXT NOT NULL DEFAULT 'user',
widget_state_ref TEXT,
retracted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX annotations_widget_id_idx ON annotations (widget_id);
-- Requirement candidates — escalated from annotations/threads (Phase 2)
CREATE TABLE requirement_candidates (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
title TEXT NOT NULL,
description TEXT NOT NULL,
source_widget_id UUID NOT NULL REFERENCES widgets(id) ON DELETE RESTRICT,
source_thread_id UUID REFERENCES annotation_threads(id) ON DELETE SET NULL,
source_annotation_id UUID REFERENCES annotations(id) ON DELETE SET NULL,
category TEXT NOT NULL DEFAULT 'friction',
status TEXT NOT NULL DEFAULT 'open',
created_by UUID REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX requirement_candidates_widget_id_idx ON requirement_candidates (source_widget_id);
CREATE INDEX requirement_candidates_status_idx ON requirement_candidates (status);
-- Triage state history — append-only audit trail of status transitions (Phase 2)
CREATE TABLE triage_states (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
candidate_id UUID NOT NULL REFERENCES requirement_candidates(id) ON DELETE CASCADE,
status TEXT NOT NULL,
notes TEXT,
changed_by UUID REFERENCES users(id),
changed_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX triage_states_candidate_id_idx ON triage_states (candidate_id);
-- Reviewer assignments — one reviewer per candidate (Phase 2)
CREATE TABLE reviewer_assignments (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
candidate_id UUID NOT NULL REFERENCES requirement_candidates(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
assigned_by UUID REFERENCES users(id),
assigned_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,
UNIQUE (candidate_id)
);
-- Requirements — promoted from accepted RequirementCandidates (Phase 3)
CREATE TABLE requirements (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
title TEXT NOT NULL,
description TEXT NOT NULL,
source_candidate_id UUID NOT NULL REFERENCES requirement_candidates(id) ON DELETE RESTRICT,
status TEXT NOT NULL DEFAULT 'active',
created_by UUID REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX requirements_source_candidate_id_idx ON requirements (source_candidate_id);
-- Decision records — governance decisions acting on requirements/candidates (Phase 3)
CREATE TABLE decision_records (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
title TEXT NOT NULL,
rationale TEXT NOT NULL,
outcome TEXT NOT NULL,
requirement_id UUID REFERENCES requirements(id) ON DELETE SET NULL,
candidate_id UUID REFERENCES requirement_candidates(id) ON DELETE SET NULL,
decided_by UUID REFERENCES users(id),
decided_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,
notes TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX decision_records_outcome_idx ON decision_records (outcome);
CREATE INDEX decision_records_requirement_id_idx ON decision_records (requirement_id);
-- Policy references — editorial links from decisions to policy scope (Phase 3)
CREATE TABLE policy_references (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
decision_id UUID NOT NULL REFERENCES decision_records(id) ON DELETE CASCADE,
policy_scope TEXT NOT NULL,
constraint_note TEXT,
created_by UUID REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX policy_references_decision_id_idx ON policy_references (decision_id);
-- Implementation change references — editorial links to work items (Phase 3)
CREATE TABLE implementation_change_references (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
decision_id UUID NOT NULL REFERENCES decision_records(id) ON DELETE CASCADE,
work_item_ref TEXT NOT NULL,
system TEXT NOT NULL DEFAULT 'github',
linked_by UUID REFERENCES users(id),
linked_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX impl_change_refs_decision_id_idx ON implementation_change_references (decision_id);
-- Back-reference: which candidate was promoted to a requirement (Phase 3)
ALTER TABLE requirement_candidates ADD COLUMN requirement_id UUID REFERENCES requirements(id) ON DELETE SET NULL;
-- Deployment records — connect decisions to deployed versions (Phase 4)
CREATE TABLE deployment_records (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
impl_ref_id UUID REFERENCES implementation_change_references(id) ON DELETE SET NULL,
decision_id UUID NOT NULL REFERENCES decision_records(id) ON DELETE RESTRICT,
version_ref TEXT NOT NULL,
deployed_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,
deployed_by UUID REFERENCES users(id),
notes TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX deployment_records_decision_id_idx ON deployment_records (decision_id);
CREATE INDEX deployment_records_deployed_at_idx ON deployment_records (deployed_at DESC);
-- Outcome signals — append-only observation of widget behaviour post-deployment (Phase 4)
CREATE TABLE outcome_signals (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
widget_id UUID NOT NULL REFERENCES widgets(id) ON DELETE CASCADE,
deployment_id UUID NOT NULL REFERENCES deployment_records(id) ON DELETE CASCADE,
signal_type TEXT NOT NULL,
value NUMERIC,
observed_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX outcome_signals_widget_id_idx ON outcome_signals (widget_id);
CREATE INDEX outcome_signals_deployment_id_idx ON outcome_signals (deployment_id);
CREATE INDEX outcome_signals_observed_at_idx ON outcome_signals (observed_at DESC);
CREATE OR REPLACE FUNCTION prevent_outcome_signal_mutation()
RETURNS TRIGGER AS $$
BEGIN
RAISE EXCEPTION 'outcome_signals is append-only: UPDATE and DELETE are not permitted';
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER outcome_signals_no_update
BEFORE UPDATE ON outcome_signals
FOR EACH ROW EXECUTE FUNCTION prevent_outcome_signal_mutation();
CREATE TRIGGER outcome_signals_no_delete
BEFORE DELETE ON outcome_signals
FOR EACH ROW EXECUTE FUNCTION prevent_outcome_signal_mutation();
-- Change evaluations — one score per deployment (Phase 4)
CREATE TABLE change_evaluations (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
deployment_id UUID NOT NULL REFERENCES deployment_records(id) ON DELETE CASCADE,
decision_id UUID REFERENCES decision_records(id) ON DELETE SET NULL,
score SMALLINT NOT NULL CHECK (score BETWEEN 1 AND 5),
rationale TEXT NOT NULL,
evaluated_by UUID REFERENCES users(id),
evaluated_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,
UNIQUE (deployment_id)
);
CREATE INDEX change_evaluations_deployment_id_idx ON change_evaluations (deployment_id);
-- Agent proposals — AI-generated outputs awaiting human review (Phase 5)
CREATE TABLE agent_proposals (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
proposal_type TEXT NOT NULL,
-- proposal_type values: summary | requirement_draft | duplicate_flag |
-- policy_flag | impl_proposal
source_widget_id UUID REFERENCES widgets(id) ON DELETE SET NULL,
source_candidate_id UUID REFERENCES requirement_candidates(id) ON DELETE SET NULL,
source_thread_id UUID REFERENCES annotation_threads(id) ON DELETE SET NULL,
source_decision_id UUID REFERENCES decision_records(id) ON DELETE SET NULL,
content TEXT NOT NULL,
model_ref TEXT NOT NULL,
confidence NUMERIC CHECK (confidence BETWEEN 0 AND 1),
status TEXT NOT NULL DEFAULT 'pending',
-- status values: pending | accepted | rejected | superseded
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX agent_proposals_proposal_type_idx ON agent_proposals (proposal_type);
CREATE INDEX agent_proposals_status_idx ON agent_proposals (status);
CREATE INDEX agent_proposals_source_widget_id_idx ON agent_proposals (source_widget_id);
CREATE INDEX agent_proposals_created_at_idx ON agent_proposals (created_at DESC);
-- One review record per proposal (human decision on AI output) (Phase 5)
CREATE TABLE agent_review_records (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
proposal_id UUID NOT NULL REFERENCES agent_proposals(id) ON DELETE CASCADE,
reviewer_id UUID REFERENCES users(id),
decision TEXT NOT NULL, -- accepted | rejected | modified
notes TEXT,
reviewed_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,
UNIQUE (proposal_id)
);
CREATE INDEX agent_review_records_proposal_id_idx ON agent_review_records (proposal_id);
-- Confidence annotations — per-dimension breakdown of AI confidence (Phase 5)
CREATE TABLE confidence_annotations (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
proposal_id UUID NOT NULL REFERENCES agent_proposals(id) ON DELETE CASCADE,
dimension TEXT NOT NULL,
-- dimension values: accuracy | relevance | completeness | policy_alignment
score NUMERIC NOT NULL CHECK (score BETWEEN 0 AND 1),
explanation TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX confidence_annotations_proposal_id_idx ON confidence_annotations (proposal_id);
-- ============================================================
-- Phase 6 — Cross-Framework UI Adaptation Layer
-- ============================================================
-- Formalises the rules for widget envelope emission: which data-* attributes
-- are required, their format, and the contract version.
CREATE TABLE envelope_emission_contracts (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
contract_version TEXT NOT NULL UNIQUE, -- e.g. "1.0", "1.1"
required_attributes JSONB NOT NULL,
-- e.g. ["data-widget-id", "data-view-context", "data-hub-id"]
optional_attributes JSONB NOT NULL DEFAULT '[]',
validation_rules JSONB NOT NULL DEFAULT '{}',
-- machine-readable rules: format checks, presence guards
description TEXT,
status TEXT NOT NULL DEFAULT 'active',
-- status values: draft | active | superseded
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX envelope_emission_contracts_status_idx ON envelope_emission_contracts (status);
-- Standardised REST interface contract for external event and annotation
-- submission — used by non-IHP adapters.
CREATE TABLE interaction_reporting_contracts (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
contract_version TEXT NOT NULL UNIQUE, -- e.g. "1.0"
endpoint_path TEXT NOT NULL, -- e.g. "/api/v1/interaction-events"
accepted_event_types JSONB NOT NULL, -- e.g. ["clicked","viewed","submitted"]
required_fields JSONB NOT NULL,
-- minimum payload: widget_id, hub_id, event_type, occurred_at
auth_scheme TEXT NOT NULL DEFAULT 'bearer',
description TEXT,
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX interaction_reporting_contracts_status_idx ON interaction_reporting_contracts (status);
-- Describes how a specific UI technology maps to IHF widget protocol obligations.
CREATE TABLE widget_adapter_specs (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
name TEXT NOT NULL UNIQUE, -- e.g. "react-18", "vue-3", "web-component"
framework TEXT NOT NULL, -- e.g. "react", "vue", "vanilla"
version TEXT NOT NULL, -- adapter spec version, e.g. "1.0"
envelope_contract_id UUID REFERENCES envelope_emission_contracts(id),
reporting_contract_id UUID REFERENCES interaction_reporting_contracts(id),
status TEXT NOT NULL DEFAULT 'draft',
-- status values: draft | active | deprecated
notes TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX widget_adapter_specs_framework_idx ON widget_adapter_specs (framework);
CREATE INDEX widget_adapter_specs_status_idx ON widget_adapter_specs (status);
-- Link widgets to their adapter spec (null = native IHP widget).
ALTER TABLE widgets
ADD COLUMN adapter_spec_id UUID REFERENCES widget_adapter_specs(id);
CREATE INDEX widgets_adapter_spec_id_idx ON widgets (adapter_spec_id);
-- Per-hub API key for bearer-token auth on the interaction reporting endpoint.
ALTER TABLE hubs
ADD COLUMN api_key TEXT;