-- 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;