-- AI Ticket Analyzer — Phase 2.6 aggregate reports. -- See docs/ticket-analyzer-phase2-spec.md → Section D.4. -- -- Adds: -- 1. analyzer_aggregate_reports — one row per cross-ticket report -- 2. analyzer_stage_executions: -- a. drop NOT NULL on analysis_id (a stage row may belong to a report instead) -- b. add aggregate_report_id FK -- c. add CHECK constraint enforcing exactly one of analysis_id or aggregate_report_id CREATE TABLE IF NOT EXISTS analyzer_aggregate_reports ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), generated_by_user_id TEXT REFERENCES "user"(id) ON DELETE SET NULL, generated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Inputs filter_criteria JSONB NOT NULL, analysis_ids UUID[] NOT NULL, ticket_count INT NOT NULL, include_itglue_context BOOLEAN NOT NULL DEFAULT true, report_title TEXT, -- SQL-derived (computed before LLM call) category_distribution JSONB, client_distribution JSONB, resolution_path_distribution JSONB, root_cause_distribution JSONB, date_range_actual JSONB, -- LLM-derived documentation_gaps JSONB, process_gaps JSONB, client_patterns JSONB, recurrence_clusters JSONB, systemic_observations JSONB, recommended_actions JSONB, narrative_summary TEXT, executive_summary TEXT, -- Metadata total_input_tokens INT, total_output_tokens INT, estimated_cost_usd NUMERIC(10,4), model_used TEXT, itglue_context_included BOOLEAN, status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','running','complete','failed')), error_message TEXT ); CREATE INDEX IF NOT EXISTS idx_analyzer_aggregate_reports_user_date ON analyzer_aggregate_reports (generated_by_user_id, generated_at DESC); CREATE INDEX IF NOT EXISTS idx_analyzer_aggregate_reports_analysis_ids ON analyzer_aggregate_reports USING gin (analysis_ids); CREATE INDEX IF NOT EXISTS idx_analyzer_aggregate_reports_pending ON analyzer_aggregate_reports (status, generated_at) WHERE status IN ('pending','running'); COMMENT ON TABLE analyzer_aggregate_reports IS 'Cross-ticket aggregate reports. Inputs (filter_criteria, analysis_ids) are immutable; outputs (LLM-derived fields) are written once when the runner completes.'; -- analyzer_stage_executions: relax analysis_id, add aggregate_report_id, enforce mutual exclusion. ALTER TABLE analyzer_stage_executions ALTER COLUMN analysis_id DROP NOT NULL; ALTER TABLE analyzer_stage_executions ADD COLUMN IF NOT EXISTS aggregate_report_id UUID REFERENCES analyzer_aggregate_reports(id) ON DELETE CASCADE; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'analyzer_stage_executions_parent_check' ) THEN ALTER TABLE analyzer_stage_executions ADD CONSTRAINT analyzer_stage_executions_parent_check CHECK ((analysis_id IS NOT NULL) <> (aggregate_report_id IS NOT NULL)); END IF; END $$; CREATE INDEX IF NOT EXISTS idx_analyzer_stage_executions_aggregate_report ON analyzer_stage_executions (aggregate_report_id, stage_order) WHERE aggregate_report_id IS NOT NULL;