-- IT Glue asset audit + write-back tables. -- See docs/wulf-pulse-ticket-analyzer-build-notes.md → Phase 4. -- -- Two tables: -- 1. itglue_asset_audits — one row per audit run (LLM gap analysis). -- 2. itglue_writes — one row per write attempt (one field, one moment), -- with provenance back to the audit that prompted it. -- -- A separate generic audit_log row is also written by the API layer -- (lib/services/audit.ts) so /admin/audit-log surfaces every IT Glue change -- alongside other admin actions. The two domain-specific tables here exist -- because: -- - audits accumulate full LLM context snapshots (heavy, infrequent), -- - writes are atomic per-field decisions with before/after diffs that the -- generic audit_log's free-form JSONB doesn't capture cleanly. CREATE TABLE IF NOT EXISTS itglue_asset_audits ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), asset_type TEXT NOT NULL CHECK (asset_type IN ('flexible_asset')), asset_id BIGINT NOT NULL, asset_type_id BIGINT, organization_id BIGINT, generated_by_user_id TEXT REFERENCES "user"(id) ON DELETE SET NULL, generated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), provider TEXT NOT NULL DEFAULT 'anthropic' CHECK (provider IN ('anthropic','openrouter')), model_used TEXT, asset_snapshot JSONB NOT NULL, ticket_count INT NOT NULL, field_gaps JSONB NOT NULL, notes_promotions JSONB NOT NULL, contradictions JSONB NOT NULL, overall_score NUMERIC(3,2), estimated_cost_usd NUMERIC(10,4), total_input_tokens INT, total_output_tokens INT, status TEXT NOT NULL DEFAULT 'complete' CHECK (status IN ('pending','running','complete','failed')), error_message TEXT ); CREATE INDEX IF NOT EXISTS ix_itglue_asset_audits_asset ON itglue_asset_audits (asset_type, asset_id, generated_at DESC); CREATE INDEX IF NOT EXISTS ix_itglue_asset_audits_org ON itglue_asset_audits (organization_id, generated_at DESC); COMMENT ON TABLE itglue_asset_audits IS 'One row per LLM-driven audit of an IT Glue asset against ticket history. ' 'Captures the full input snapshot (asset traits at the time, redacted), the ' 'extracted gaps/promotions/contradictions, and cost. Forever-retained.'; COMMENT ON COLUMN itglue_asset_audits.field_gaps IS 'JSON array: [{ field_name, why_missing_matters, suggested_value, ' 'evidence_ticket_numbers, confidence }]'; COMMENT ON COLUMN itglue_asset_audits.notes_promotions IS 'JSON array: [{ quoted_note_text, target_field, suggested_value, confidence }]'; COMMENT ON COLUMN itglue_asset_audits.contradictions IS 'JSON array: [{ description, evidence }]'; CREATE TABLE IF NOT EXISTS itglue_writes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), audit_id UUID REFERENCES itglue_asset_audits(id) ON DELETE SET NULL, asset_type TEXT NOT NULL CHECK (asset_type IN ('flexible_asset')), asset_id BIGINT NOT NULL, field_name TEXT NOT NULL, before_value JSONB, after_value JSONB NOT NULL, performed_by_user_id TEXT REFERENCES "user"(id) ON DELETE SET NULL, performed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), status TEXT NOT NULL CHECK (status IN ('pending','committed','failed','reverted')), itglue_response JSONB, error_message TEXT, source_evidence JSONB ); CREATE INDEX IF NOT EXISTS ix_itglue_writes_asset ON itglue_writes (asset_type, asset_id, performed_at DESC); CREATE INDEX IF NOT EXISTS ix_itglue_writes_user ON itglue_writes (performed_by_user_id, performed_at DESC); CREATE INDEX IF NOT EXISTS ix_itglue_writes_audit ON itglue_writes (audit_id); COMMENT ON TABLE itglue_writes IS 'One row per write attempt to IT Glue. Pending → committed | failed | reverted. ' 'Reverts produce a new row whose before_value/after_value swap the original; ' 'the original row gets status=reverted. Forever-retained.';