-- AI Ticket Analyzer feature -- Stores versioned analyses of Autotask tickets, share log, and an on-demand job queue. -- See docs/wulf-pulse-ticket-analyzer-prompt.md for the feature spec. CREATE EXTENSION IF NOT EXISTS pgcrypto; -- ============================================================================= -- analyzer_analyses -- One row per completed (or failed) analysis; versioned per ticket_number. -- ============================================================================= CREATE TABLE IF NOT EXISTS analyzer_analyses ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), ticket_number TEXT NOT NULL, autotask_ticket_id BIGINT NOT NULL, analysis_version INT NOT NULL, content_hash_at_analysis TEXT NOT NULL, triggered_by_user_id TEXT REFERENCES "user"(id) ON DELETE SET NULL, triggered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','running','complete','failed')), completed_at TIMESTAMPTZ, -- Model usage haiku_used BOOLEAN NOT NULL DEFAULT false, sonnet_used BOOLEAN NOT NULL DEFAULT false, opus_used BOOLEAN NOT NULL DEFAULT false, total_input_tokens INT NOT NULL DEFAULT 0, total_output_tokens INT NOT NULL DEFAULT 0, estimated_cost_usd NUMERIC(10,4) NOT NULL DEFAULT 0, -- Structured output (each LLM stage's parsed JSON) summary TEXT, timeline JSONB, what_was_done JSONB, what_should_have_been_done JSONB, gaps JSONB, next_step TEXT, next_step_rationale TEXT, post_resolution_analysis TEXT, confidence_score NUMERIC(3,2), needs_human_review BOOLEAN NOT NULL DEFAULT false, human_review_reasons JSONB, itglue_docs_referenced JSONB NOT NULL DEFAULT '[]'::jsonb, -- Debugging / observability model_traces JSONB, filtered_noise_count INT NOT NULL DEFAULT 0, error_message TEXT, CONSTRAINT analyzer_analyses_version_unique UNIQUE (ticket_number, analysis_version) ); CREATE INDEX IF NOT EXISTS idx_analyzer_analyses_ticket_version ON analyzer_analyses (ticket_number, analysis_version DESC); CREATE INDEX IF NOT EXISTS idx_analyzer_analyses_triggered_at ON analyzer_analyses (triggered_at DESC); CREATE INDEX IF NOT EXISTS idx_analyzer_analyses_needs_review ON analyzer_analyses (needs_human_review) WHERE needs_human_review = true; CREATE INDEX IF NOT EXISTS idx_analyzer_analyses_autotask_ticket_id ON analyzer_analyses (autotask_ticket_id); COMMENT ON TABLE analyzer_analyses IS 'Versioned AI analyses of Autotask tickets; one row per completed analysis run.'; COMMENT ON COLUMN analyzer_analyses.content_hash_at_analysis IS 'sha256 of canonical-JSON of (tagged_events, ticket_status, ticket_priority, queue) at analysis time. Used for idempotency.'; COMMENT ON COLUMN analyzer_analyses.filtered_noise_count IS 'Count of workflow-rule and notification-email notes stripped during pre-processing.'; COMMENT ON COLUMN analyzer_analyses.itglue_docs_referenced IS 'IDs/names of IT Glue docs cited; doc bodies are NEVER stored here (security).'; -- ============================================================================= -- analyzer_shares -- Audit log of share-by-email events for any analysis. -- ============================================================================= CREATE TABLE IF NOT EXISTS analyzer_shares ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), analysis_id UUID NOT NULL REFERENCES analyzer_analyses(id) ON DELETE CASCADE, shared_by_user_id TEXT NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, shared_with_email TEXT NOT NULL, note TEXT, shared_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), viewed_at TIMESTAMPTZ ); CREATE INDEX IF NOT EXISTS idx_analyzer_shares_analysis_id ON analyzer_shares (analysis_id); CREATE INDEX IF NOT EXISTS idx_analyzer_shares_shared_by ON analyzer_shares (shared_by_user_id); CREATE INDEX IF NOT EXISTS idx_analyzer_shares_shared_at ON analyzer_shares (shared_at DESC); COMMENT ON TABLE analyzer_shares IS 'Audit log of analyses shared by email. Recipient domains validated against ALLOWED_SHARE_DOMAINS at write time.'; -- ============================================================================= -- analyzer_jobs -- On-demand pipeline queue. A worker initialised at server start polls this table. -- ============================================================================= CREATE TABLE IF NOT EXISTS analyzer_jobs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), ticket_number TEXT NOT NULL, queued_by_user_id TEXT REFERENCES "user"(id) ON DELETE SET NULL, status TEXT NOT NULL DEFAULT 'queued' CHECK (status IN ('queued','fetching','triaging','itglue','analyzing','deep_review','complete','failed')), result_analysis_id UUID REFERENCES analyzer_analyses(id) ON DELETE SET NULL, queued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), started_at TIMESTAMPTZ, finished_at TIMESTAMPTZ, error_message TEXT ); CREATE INDEX IF NOT EXISTS idx_analyzer_jobs_status_queued ON analyzer_jobs (status, queued_at) WHERE status IN ('queued','fetching','triaging','itglue','analyzing','deep_review'); CREATE INDEX IF NOT EXISTS idx_analyzer_jobs_ticket_number ON analyzer_jobs (ticket_number); CREATE INDEX IF NOT EXISTS idx_analyzer_jobs_queued_at ON analyzer_jobs (queued_at DESC); COMMENT ON TABLE analyzer_jobs IS 'On-demand analyzer pipeline jobs. Polled by a single worker that initialises at server start.';