-- 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; -- Phase 7: Advanced Observability and Operational Integration -- Aggregated pain score per widget, recomputed on demand or scheduled. CREATE TABLE friction_scores ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, widget_id UUID NOT NULL REFERENCES widgets(id), score INTEGER NOT NULL DEFAULT 0, annotation_count INTEGER NOT NULL DEFAULT 0, error_event_count INTEGER NOT NULL DEFAULT 0, regression_flag BOOLEAN NOT NULL DEFAULT FALSE, stale_candidate_count INTEGER NOT NULL DEFAULT 0, last_computed_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, UNIQUE (widget_id) ); CREATE INDEX friction_scores_widget_id_idx ON friction_scores (widget_id); CREATE INDEX friction_scores_score_idx ON friction_scores (score DESC); -- Detected stalls at specific pipeline stages. CREATE TABLE bottleneck_records ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, hub_id UUID NOT NULL REFERENCES hubs(id), stage TEXT NOT NULL, subject_type TEXT NOT NULL, subject_id UUID NOT NULL, stalled_since TIMESTAMP WITH TIME ZONE NOT NULL, severity TEXT NOT NULL DEFAULT 'medium', resolved_at TIMESTAMP WITH TIME ZONE, notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); CREATE INDEX bottleneck_records_hub_id_idx ON bottleneck_records (hub_id); CREATE INDEX bottleneck_records_stage_idx ON bottleneck_records (stage); CREATE INDEX bottleneck_records_resolved_idx ON bottleneck_records (resolved_at) WHERE resolved_at IS NULL; -- Periodic health snapshots for trend tracking. CREATE TABLE hub_health_snapshots ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, hub_id UUID NOT NULL REFERENCES hubs(id), health_score INTEGER NOT NULL, open_candidates INTEGER NOT NULL DEFAULT 0, regressed_widgets INTEGER NOT NULL DEFAULT 0, stale_decisions INTEGER NOT NULL DEFAULT 0, active_bottlenecks INTEGER NOT NULL DEFAULT 0, computed_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); CREATE INDEX hub_health_snapshots_hub_id_idx ON hub_health_snapshots (hub_id); CREATE INDEX hub_health_snapshots_computed_at_idx ON hub_health_snapshots (hub_id, computed_at DESC); -- Patterns detected across multiple hubs. CREATE TABLE cross_hub_propagations ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, pattern_type TEXT NOT NULL, source_hub_id UUID REFERENCES hubs(id), affected_hub_ids JSONB NOT NULL DEFAULT '[]', summary TEXT NOT NULL, detected_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, status TEXT NOT NULL DEFAULT 'open', notes TEXT ); CREATE INDEX cross_hub_propagations_status_idx ON cross_hub_propagations (status); CREATE INDEX cross_hub_propagations_pattern_idx ON cross_hub_propagations (pattern_type); -- Phase 8: Federated Hub Maturity -- Explicit ownership record for a widget. CREATE TABLE widget_ownerships ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, widget_id UUID NOT NULL REFERENCES widgets(id), owner_hub_id UUID NOT NULL REFERENCES hubs(id), steward_hub_id UUID REFERENCES hubs(id), ownership_type TEXT NOT NULL DEFAULT 'local', -- 'local' | 'delegated' | 'global' effective_from TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), effective_until TIMESTAMP WITH TIME ZONE, notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); CREATE INDEX widget_ownerships_widget_id_idx ON widget_ownerships (widget_id); CREATE INDEX widget_ownerships_owner_hub_idx ON widget_ownerships (owner_hub_id); CREATE INDEX widget_ownerships_steward_hub_idx ON widget_ownerships (steward_hub_id); -- Routing rule: automatically routes a RequirementCandidate to another hub. CREATE TABLE hub_routing_rules ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, source_hub_id UUID NOT NULL REFERENCES hubs(id), target_hub_id UUID NOT NULL REFERENCES hubs(id), match_category TEXT, match_widget_type TEXT, priority INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'inactive', -- 'active' | 'inactive' notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); CREATE INDEX hub_routing_rules_source_idx ON hub_routing_rules (source_hub_id); CREATE INDEX hub_routing_rules_status_idx ON hub_routing_rules (status); -- Routing destination on requirement candidates. ALTER TABLE requirement_candidates ADD COLUMN routed_to_hub_id UUID REFERENCES hubs(id); CREATE INDEX requirement_candidates_routed_hub_idx ON requirement_candidates (routed_to_hub_id) WHERE routed_to_hub_id IS NOT NULL; -- Org-wide policy overlay applied across selected hubs. CREATE TABLE federated_policy_overlays ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, title TEXT NOT NULL, policy_text TEXT NOT NULL, applies_to_hubs JSONB NOT NULL DEFAULT '[]', enforced_from TIMESTAMP WITH TIME ZONE, status TEXT NOT NULL DEFAULT 'draft', -- 'draft' | 'active' | 'retired' notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); CREATE INDEX federated_policy_overlays_status_idx ON federated_policy_overlays (status); -- Named governance role assigned to a hub. CREATE TABLE stewardship_roles ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, hub_id UUID NOT NULL REFERENCES hubs(id), role_name TEXT NOT NULL, assigned_to TEXT NOT NULL, granted_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, revoked_at TIMESTAMP WITH TIME ZONE, notes TEXT ); CREATE INDEX stewardship_roles_hub_id_idx ON stewardship_roles (hub_id); CREATE INDEX stewardship_roles_active_idx ON stewardship_roles (revoked_at) WHERE revoked_at IS NULL; -- Long-term archival entry for any IHF artifact. CREATE TABLE archive_records ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, subject_type TEXT NOT NULL, subject_id UUID NOT NULL, archived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, reason TEXT NOT NULL, archived_by TEXT NOT NULL, lineage_ref TEXT ); CREATE INDEX archive_records_subject_type_idx ON archive_records (subject_type); CREATE INDEX archive_records_subject_id_idx ON archive_records (subject_id); -- Soft-archive flag on widgets. ALTER TABLE widgets ADD COLUMN is_archived BOOLEAN NOT NULL DEFAULT FALSE; CREATE INDEX widgets_is_archived_idx ON widgets (is_archived) WHERE is_archived = TRUE; -- ============================================================ -- GAAF Compliance Foundation (IHUB-WP-0009) -- T02: hub_kind | T03: type registries + seed | T04: maturity columns | T05: manifests -- ============================================================ -- T02 — Hub kind classification ALTER TABLE hubs ADD COLUMN hub_kind TEXT NOT NULL DEFAULT 'domain'; CREATE INDEX hubs_hub_kind_idx ON hubs (hub_kind); CREATE UNIQUE INDEX hubs_one_framework_idx ON hubs (hub_kind) WHERE hub_kind = 'framework'; -- T03 — Type registries CREATE TABLE widget_type_registry ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, name TEXT NOT NULL UNIQUE, label TEXT NOT NULL, description TEXT, owner_hub_id UUID REFERENCES hubs(id), status TEXT NOT NULL DEFAULT 'active', deprecated_in_favour_of TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); CREATE INDEX widget_type_registry_status_idx ON widget_type_registry (status); CREATE INDEX widget_type_registry_owner_hub_idx ON widget_type_registry (owner_hub_id); CREATE TABLE event_type_registry ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, name TEXT NOT NULL UNIQUE, label TEXT NOT NULL, description TEXT, owner_hub_id UUID REFERENCES hubs(id), status TEXT NOT NULL DEFAULT 'active', deprecated_in_favour_of TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); CREATE INDEX event_type_registry_status_idx ON event_type_registry (status); CREATE INDEX event_type_registry_owner_hub_idx ON event_type_registry (owner_hub_id); CREATE TABLE annotation_category_registry ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, name TEXT NOT NULL UNIQUE, label TEXT NOT NULL, description TEXT, owner_hub_id UUID REFERENCES hubs(id), status TEXT NOT NULL DEFAULT 'active', deprecated_in_favour_of TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); CREATE INDEX annotation_category_registry_status_idx ON annotation_category_registry (status); CREATE INDEX annotation_category_registry_owner_hub_idx ON annotation_category_registry (owner_hub_id); CREATE TABLE policy_scope_registry ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, name TEXT NOT NULL UNIQUE, label TEXT NOT NULL, description TEXT, owner_hub_id UUID REFERENCES hubs(id), status TEXT NOT NULL DEFAULT 'active', deprecated_in_favour_of TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); CREATE INDEX policy_scope_registry_status_idx ON policy_scope_registry (status); CREATE INDEX policy_scope_registry_owner_hub_idx ON policy_scope_registry (owner_hub_id); -- T03 — Seed framework-level vocabulary (owner_hub_id = NULL) INSERT INTO widget_type_registry (name, label, description) VALUES ('chart', 'Chart', 'Data visualisation chart widget'), ('form', 'Form', 'Data entry form widget'), ('table', 'Table', 'Tabular data display widget'), ('action', 'Action Control', 'Button, link, or trigger widget'), ('panel', 'Status Panel', 'Summary or status information panel'), ('workflow-step', 'Workflow Step', 'Single step in a multi-step workflow'), ('recommendation','Recommendation', 'AI or system recommendation block'), ('chat', 'Chat Region', 'Conversational interaction region'), ('diff', 'Diff / Review', 'Code diff or change review element'); INSERT INTO event_type_registry (name, label, description) VALUES ('viewed', 'Viewed', 'Widget was rendered and visible to the user'), ('focused', 'Focused', 'Widget received input focus'), ('clicked', 'Clicked', 'Widget was clicked or tapped'), ('submitted', 'Submitted', 'Form or action was submitted'), ('abandoned', 'Abandoned', 'User navigated away without completing'), ('retried', 'Retried', 'Action was retried after failure'), ('failed', 'Failed', 'Action or submission resulted in an error'), ('commented', 'Commented', 'User added a comment or annotation'), ('flagged_confusing', 'Flagged Confusing', 'User flagged the widget as confusing'), ('flagged_helpful', 'Flagged Helpful', 'User flagged the widget as helpful'), ('blocked_by_policy', 'Blocked by Policy', 'Action was blocked by a policy rule'), ('escalated', 'Escalated', 'Issue was escalated for review'), ('accepted_recommendation', 'Accepted Recommendation', 'User accepted an AI recommendation'), ('rejected_recommendation', 'Rejected Recommendation', 'User rejected an AI recommendation'), ('retracted', 'Retracted', 'Correction marker referencing original event in metadata'); INSERT INTO annotation_category_registry (name, label, description) VALUES ('friction', 'Friction', 'Interaction caused user effort or difficulty'), ('missing_capability', 'Missing Capability', 'Required feature or function is absent'), ('policy_conflict', 'Policy Conflict', 'Widget behaviour conflicts with a policy'), ('trust_deficit', 'Trust Deficit', 'User lacks confidence in the widget output'), ('accessibility', 'Accessibility', 'Accessibility or inclusive design concern'), ('workflow_bottleneck', 'Workflow Bottleneck', 'Widget creates a slowdown in the workflow'), ('documentation_gap', 'Documentation Gap', 'Missing or insufficient documentation'), ('product_opportunity', 'Product Opportunity', 'Observation suggesting a product improvement'), ('governance_concern', 'Governance Concern', 'Concern about governance, audit, or compliance'); INSERT INTO policy_scope_registry (name, label, description) VALUES ('internal', 'Internal', 'Applies to internal operators only'), ('org-wide', 'Organisation-Wide', 'Applies across the entire organisation'), ('external', 'External-Facing', 'Applies to externally visible surfaces'), ('regulatory', 'Regulatory', 'Driven by regulatory or compliance requirements'), ('security', 'Security', 'Security policy scope'); -- T04 — Maturity columns on existing contract tables ALTER TABLE envelope_emission_contracts ADD COLUMN maturity TEXT NOT NULL DEFAULT 'stable'; ALTER TABLE interaction_reporting_contracts ADD COLUMN maturity TEXT NOT NULL DEFAULT 'stable'; ALTER TABLE widget_adapter_specs ADD COLUMN maturity TEXT NOT NULL DEFAULT 'beta'; -- T05 — Hub Capability Manifest CREATE TABLE hub_capability_manifests ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, hub_id UUID NOT NULL UNIQUE REFERENCES hubs(id), manifest_version TEXT NOT NULL DEFAULT '1.0', declared_widget_types JSONB NOT NULL DEFAULT '[]', declared_event_types JSONB NOT NULL DEFAULT '[]', declared_annotation_categories JSONB NOT NULL DEFAULT '[]', declared_policy_scopes JSONB NOT NULL DEFAULT '[]', capability_description TEXT, contact TEXT, status TEXT NOT NULL DEFAULT 'draft', activated_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); CREATE INDEX hub_capability_manifests_hub_id_idx ON hub_capability_manifests (hub_id); CREATE INDEX hub_capability_manifests_status_idx ON hub_capability_manifests (status); -- GAAF: type registries enforced from here (IHUB-WP-0009) -- All new type discriminator columns (widget_type, event_type, category, -- policy_scope) must reference a registry table or carry a CHECK constraint. -- IHF Phase 9 — External API Surface and Consumer SDKs (IHUB-WP-0010) CREATE TABLE api_consumers ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, name TEXT NOT NULL, description TEXT, hub_capability_manifest_id UUID REFERENCES hub_capability_manifests(id), rate_limit_per_minute INTEGER NOT NULL DEFAULT 60, quota_per_day INTEGER NOT NULL DEFAULT 10000, quota_resets_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (date_trunc('day', NOW() AT TIME ZONE 'UTC') + interval '1 day'), is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL ); CREATE INDEX api_consumers_manifest_idx ON api_consumers (hub_capability_manifest_id); CREATE TABLE api_keys ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, api_consumer_id UUID NOT NULL REFERENCES api_consumers(id) ON DELETE CASCADE, key_prefix TEXT NOT NULL, key_hash TEXT NOT NULL, scopes TEXT NOT NULL DEFAULT '', token_type TEXT NOT NULL DEFAULT 'static' CHECK (token_type IN ('static', 'oauth')), expires_at TIMESTAMP WITH TIME ZONE, revoked_at TIMESTAMP WITH TIME ZONE, last_used_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL ); CREATE UNIQUE INDEX api_keys_prefix_idx ON api_keys (key_prefix); CREATE INDEX api_keys_consumer_idx ON api_keys (api_consumer_id); CREATE INDEX api_keys_hash_idx ON api_keys (key_hash); CREATE TABLE webhook_subscriptions ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, api_consumer_id UUID NOT NULL REFERENCES api_consumers(id) ON DELETE CASCADE, event_type TEXT NOT NULL CHECK (event_type IN ( 'interaction_event.created', 'annotation.created', 'requirement_candidate.created', 'decision_record.created', 'deployment_record.created', 'outcome_signal.created' )), target_url TEXT NOT NULL, secret TEXT NOT NULL, is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL ); CREATE INDEX webhook_subs_consumer_idx ON webhook_subscriptions (api_consumer_id); CREATE INDEX webhook_subs_event_type_idx ON webhook_subscriptions (event_type); CREATE TABLE webhook_deliveries ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, webhook_subscription_id UUID NOT NULL REFERENCES webhook_subscriptions(id), payload JSONB NOT NULL, attempted_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, status TEXT NOT NULL CHECK (status IN ('pending', 'delivered', 'failed')), response_code INTEGER, latency_ms INTEGER, error_message TEXT ); CREATE INDEX webhook_deliveries_sub_idx ON webhook_deliveries (webhook_subscription_id, attempted_at DESC); CREATE TABLE api_request_log ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, api_consumer_id UUID REFERENCES api_consumers(id), endpoint TEXT NOT NULL, method TEXT NOT NULL, status_code INTEGER NOT NULL, latency_ms INTEGER, requested_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL ); CREATE INDEX api_request_log_consumer_time_idx ON api_request_log (api_consumer_id, requested_at DESC);