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