-- ============================================================================= -- B2: URT Database Schema -- Universal Review Taxonomy v5.1 -- ============================================================================= -- -- PostgreSQL schema for URT v5.1 classification storage and analytics. -- Supports all implementation profiles (Lite, Core, Standard, Full). -- -- Design Principles: -- 1. Normalized reference data (domains, categories, subcodes, causal codes) -- 2. Flexible span classification with metadata dimensions -- 3. Full issue lifecycle tracking per C1 framework -- 4. Comprehensive audit trail for compliance -- 5. Optimized for analytics with materialized views -- 6. Partitioning-ready for high-volume scenarios -- -- Status: Production Ready -- Version: 1.0 -- Date: 2026-01-23 -- Depends On: B1-urt-codes.yaml, C1-Issue-Lifecycle-Framework.md -- ============================================================================= -- Enable required extensions CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "btree_gist"; -- For exclusion constraints -- ============================================================================= -- SECTION 1: REFERENCE TABLES -- Core URT taxonomy structure from B1-urt-codes.yaml -- ============================================================================= -- ----------------------------------------------------------------------------- -- 1.1 Experience Domains (7 domains) -- Top-level organizational structure -- ----------------------------------------------------------------------------- CREATE TABLE urt_domains ( domain_code CHAR(1) PRIMARY KEY, name VARCHAR(50) NOT NULL, description TEXT NOT NULL, core_question TEXT NOT NULL, default_owner VARCHAR(100) NOT NULL, display_order SMALLINT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT urt_domains_code_check CHECK (domain_code IN ('O', 'P', 'J', 'E', 'A', 'V', 'R')) ); COMMENT ON TABLE urt_domains IS 'URT v5.1 Experience Domains - 7 top-level classification areas'; -- ----------------------------------------------------------------------------- -- 1.2 Categories (28 categories) -- Second-tier classification within domains -- ----------------------------------------------------------------------------- CREATE TABLE urt_categories ( category_code VARCHAR(2) PRIMARY KEY, domain_code CHAR(1) NOT NULL REFERENCES urt_domains(domain_code), name VARCHAR(50) NOT NULL, definition TEXT NOT NULL, display_order SMALLINT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT urt_categories_code_format CHECK (category_code ~ '^[OPJEAVR][1-4]$') ); CREATE INDEX idx_categories_domain ON urt_categories(domain_code); COMMENT ON TABLE urt_categories IS 'URT v5.1 Categories - 28 second-tier classifications within domains'; -- ----------------------------------------------------------------------------- -- 1.3 Subcodes (138 subcodes per B1 actual count) -- Third-tier diagnostic codes for detailed classification -- ----------------------------------------------------------------------------- CREATE TABLE urt_subcodes ( subcode VARCHAR(6) PRIMARY KEY, category_code VARCHAR(2) NOT NULL REFERENCES urt_categories(category_code), domain_code CHAR(1) NOT NULL REFERENCES urt_domains(domain_code), name VARCHAR(100) NOT NULL, definition TEXT NOT NULL, positive_example TEXT, negative_example TEXT, dont_confuse_with VARCHAR(6), dont_confuse_reason TEXT, display_order SMALLINT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT urt_subcodes_code_format CHECK (subcode ~ '^[OPJEAVR][1-4]\.[0-9]{2}$') ); CREATE INDEX idx_subcodes_category ON urt_subcodes(category_code); CREATE INDEX idx_subcodes_domain ON urt_subcodes(domain_code); COMMENT ON TABLE urt_subcodes IS 'URT v5.1 Subcodes - 138 diagnostic-level classification codes'; -- ----------------------------------------------------------------------------- -- 1.4 Causal Codes (16 codes across 3 layers) -- For root cause analysis in Full profile -- ----------------------------------------------------------------------------- CREATE TABLE urt_causal_codes ( causal_code VARCHAR(4) PRIMARY KEY, layer VARCHAR(20) NOT NULL, layer_prefix VARCHAR(3) NOT NULL, name VARCHAR(50) NOT NULL, definition TEXT NOT NULL, display_order SMALLINT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT urt_causal_layer_check CHECK (layer IN ('conditions', 'management', 'systemic')), CONSTRAINT urt_causal_prefix_check CHECK (layer_prefix IN ('CD-', 'MG-', 'SY-')), CONSTRAINT urt_causal_code_format CHECK (causal_code ~ '^(CD|MG|SY)-[A-Z]$') ); CREATE INDEX idx_causal_layer ON urt_causal_codes(layer); COMMENT ON TABLE urt_causal_codes IS 'URT v5.1 Causal Codes - 16 root cause analysis codes (CD/MG/SY layers)'; -- ----------------------------------------------------------------------------- -- 1.5 Metadata Dimension Values (24 values across 7 dimensions) -- All possible metadata values for span classification -- ----------------------------------------------------------------------------- CREATE TABLE urt_metadata_values ( dimension_code VARCHAR(2) NOT NULL, value_code VARCHAR(4) NOT NULL, dimension_name VARCHAR(30) NOT NULL, label VARCHAR(30) NOT NULL, definition TEXT NOT NULL, is_default BOOLEAN NOT NULL DEFAULT FALSE, display_order SMALLINT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (dimension_code, value_code), CONSTRAINT urt_metadata_dimension_check CHECK (dimension_code IN ('V', 'I', 'S', 'A', 'T', 'E', 'CR')) ); CREATE INDEX idx_metadata_dimension ON urt_metadata_values(dimension_code); COMMENT ON TABLE urt_metadata_values IS 'URT v5.1 Metadata Values - 24 values across 7 dimensions (V,I,S,A,T,E,CR)'; -- ============================================================================= -- SECTION 2: CORE CLASSIFICATION TABLES -- Reviews, spans, and classifications -- ============================================================================= -- ----------------------------------------------------------------------------- -- 2.1 Reviews -- Original review text and source metadata -- ----------------------------------------------------------------------------- CREATE TABLE reviews ( review_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), external_id VARCHAR(255), -- ID from source system (e.g., Google place_id) source VARCHAR(50) NOT NULL, -- 'google', 'yelp', 'tripadvisor', etc. business_id VARCHAR(255), -- Business identifier author_name VARCHAR(255), author_id VARCHAR(255), review_text TEXT NOT NULL, star_rating NUMERIC(2,1), -- 1.0-5.0 scale review_date DATE, language_code VARCHAR(5), -- ISO 639-1 raw_metadata JSONB, -- Source-specific metadata created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ, -- Soft delete support CONSTRAINT reviews_rating_check CHECK (star_rating IS NULL OR (star_rating >= 1.0 AND star_rating <= 5.0)) ); CREATE INDEX idx_reviews_source ON reviews(source); CREATE INDEX idx_reviews_business ON reviews(business_id); CREATE INDEX idx_reviews_date ON reviews(review_date); CREATE INDEX idx_reviews_external ON reviews(external_id) WHERE external_id IS NOT NULL; CREATE INDEX idx_reviews_created ON reviews(created_at); CREATE INDEX idx_reviews_deleted ON reviews(deleted_at) WHERE deleted_at IS NULL; COMMENT ON TABLE reviews IS 'Original customer reviews from various sources'; -- ----------------------------------------------------------------------------- -- 2.2 Spans -- Individual classified text segments within reviews -- A single review may contain multiple spans (composite review decomposition) -- ----------------------------------------------------------------------------- CREATE TABLE spans ( span_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), review_id UUID NOT NULL REFERENCES reviews(review_id) ON DELETE CASCADE, span_text TEXT NOT NULL, char_start INTEGER, -- Start position in review_text char_end INTEGER, -- End position in review_text span_order SMALLINT NOT NULL DEFAULT 1, -- Order within review -- Implementation profile used profile VARCHAR(10) NOT NULL DEFAULT 'standard', -- Primary classification (required for all profiles) primary_code VARCHAR(6) NOT NULL, primary_tier SMALLINT NOT NULL, -- 1=domain, 2=category, 3=subcode -- Metadata dimensions (availability depends on profile) valence VARCHAR(2) NOT NULL, intensity VARCHAR(2) NOT NULL, specificity VARCHAR(2), -- Standard+ only actionability VARCHAR(2), -- Standard+ only temporal VARCHAR(2) DEFAULT 'TC', evidence VARCHAR(2) DEFAULT 'ES', comparative VARCHAR(4) DEFAULT 'CR-N', -- Extended attributes for issue aggregation entity_reference VARCHAR(255), -- Product, person, feature location_reference VARCHAR(255), -- Physical or logical location -- Confidence and annotation metadata confidence_score NUMERIC(3,2), -- 0.00-1.00 annotator_notes TEXT, annotation_source VARCHAR(20), -- 'human', 'llm', 'hybrid' -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ, CONSTRAINT spans_profile_check CHECK (profile IN ('lite', 'core', 'standard', 'full')), CONSTRAINT spans_tier_check CHECK (primary_tier IN (1, 2, 3)), CONSTRAINT spans_valence_check CHECK (valence IN ('V+', 'V-', 'V0', 'V±')), CONSTRAINT spans_intensity_check CHECK (intensity IN ('I1', 'I2', 'I3')), CONSTRAINT spans_specificity_check CHECK (specificity IS NULL OR specificity IN ('S1', 'S2', 'S3')), CONSTRAINT spans_actionability_check CHECK (actionability IS NULL OR actionability IN ('A1', 'A2', 'A3')), CONSTRAINT spans_temporal_check CHECK (temporal IS NULL OR temporal IN ('TC', 'TR', 'TH', 'TF')), CONSTRAINT spans_evidence_check CHECK (evidence IS NULL OR evidence IN ('ES', 'EI', 'EC')), CONSTRAINT spans_comparative_check CHECK (comparative IS NULL OR comparative IN ('CR-N', 'CR-B', 'CR-W', 'CR-S')), CONSTRAINT spans_confidence_check CHECK (confidence_score IS NULL OR (confidence_score >= 0 AND confidence_score <= 1)), CONSTRAINT spans_char_range_check CHECK ((char_start IS NULL AND char_end IS NULL) OR (char_start < char_end)) ); CREATE INDEX idx_spans_review ON spans(review_id); CREATE INDEX idx_spans_primary_code ON spans(primary_code); CREATE INDEX idx_spans_valence ON spans(valence); CREATE INDEX idx_spans_intensity ON spans(intensity); CREATE INDEX idx_spans_comparative ON spans(comparative) WHERE comparative != 'CR-N'; CREATE INDEX idx_spans_created ON spans(created_at); CREATE INDEX idx_spans_deleted ON spans(deleted_at) WHERE deleted_at IS NULL; CREATE INDEX idx_spans_entity ON spans(entity_reference) WHERE entity_reference IS NOT NULL; CREATE INDEX idx_spans_location ON spans(location_reference) WHERE location_reference IS NOT NULL; -- Composite indexes for common query patterns CREATE INDEX idx_spans_classification ON spans(primary_code, valence, intensity); CREATE INDEX idx_spans_negative ON spans(primary_code, created_at) WHERE valence = 'V-'; COMMENT ON TABLE spans IS 'Individual classified text segments from reviews with URT metadata'; -- ----------------------------------------------------------------------------- -- 2.3 Span Classifications (Secondary Codes) -- Supports up to 2 secondary codes per span (Standard+ profiles) -- ----------------------------------------------------------------------------- CREATE TABLE span_classifications ( classification_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), span_id UUID NOT NULL REFERENCES spans(span_id) ON DELETE CASCADE, code VARCHAR(6) NOT NULL, code_tier SMALLINT NOT NULL, is_primary BOOLEAN NOT NULL DEFAULT FALSE, classification_order SMALLINT NOT NULL DEFAULT 1, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT span_class_tier_check CHECK (code_tier IN (1, 2, 3)), CONSTRAINT span_class_order_check CHECK (classification_order BETWEEN 1 AND 3), -- Ensure max 3 classifications per span (1 primary + 2 secondary) UNIQUE (span_id, classification_order) ); CREATE INDEX idx_span_class_span ON span_classifications(span_id); CREATE INDEX idx_span_class_code ON span_classifications(code); CREATE INDEX idx_span_class_primary ON span_classifications(span_id) WHERE is_primary = TRUE; COMMENT ON TABLE span_classifications IS 'Primary and secondary code assignments for spans (max 3 per span)'; -- ----------------------------------------------------------------------------- -- 2.4 Causal Chains -- Links spans to causal codes (CD -> MG -> SY) for Full profile -- ----------------------------------------------------------------------------- CREATE TABLE causal_chains ( chain_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), span_id UUID NOT NULL REFERENCES spans(span_id) ON DELETE CASCADE, -- Causal chain: Conditions -> Management -> Systemic condition_code VARCHAR(4) REFERENCES urt_causal_codes(causal_code), management_code VARCHAR(4) REFERENCES urt_causal_codes(causal_code), systemic_code VARCHAR(4) REFERENCES urt_causal_codes(causal_code), -- Confidence in causal attribution chain_confidence NUMERIC(3,2), analyst_notes TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT causal_chain_layer_check CHECK ( (condition_code IS NULL OR condition_code ~ '^CD-') AND (management_code IS NULL OR management_code ~ '^MG-') AND (systemic_code IS NULL OR systemic_code ~ '^SY-') ), CONSTRAINT causal_chain_minimum CHECK ( condition_code IS NOT NULL OR management_code IS NOT NULL OR systemic_code IS NOT NULL ) ); CREATE INDEX idx_causal_span ON causal_chains(span_id); CREATE INDEX idx_causal_condition ON causal_chains(condition_code) WHERE condition_code IS NOT NULL; CREATE INDEX idx_causal_management ON causal_chains(management_code) WHERE management_code IS NOT NULL; CREATE INDEX idx_causal_systemic ON causal_chains(systemic_code) WHERE systemic_code IS NOT NULL; COMMENT ON TABLE causal_chains IS 'Root cause analysis chains linking spans to CD/MG/SY causal codes'; -- ============================================================================= -- SECTION 3: ISSUE TRACKING TABLES -- Based on C1 Issue Lifecycle Framework -- ============================================================================= -- ----------------------------------------------------------------------------- -- 3.1 Issues -- Aggregated issue records from negative spans -- ----------------------------------------------------------------------------- CREATE TABLE issues ( issue_id VARCHAR(20) PRIMARY KEY, -- Format: ISSUE-YYYY-NNNN -- Classification context primary_subcode VARCHAR(6) NOT NULL, domain_code CHAR(1) NOT NULL REFERENCES urt_domains(domain_code), -- Aggregation context entity_reference VARCHAR(255), location_reference VARCHAR(255), -- Current state (per C1 state machine) state VARCHAR(15) NOT NULL DEFAULT 'DETECTED', state_changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Metrics from aggregated spans span_count INTEGER NOT NULL DEFAULT 1, max_intensity VARCHAR(2) NOT NULL, priority_score NUMERIC(6,2) NOT NULL, confidence_score NUMERIC(3,2) NOT NULL, -- Ownership owner_team VARCHAR(100), owner_individual VARCHAR(100), -- Resolution details resolution_code VARCHAR(20), resolution_notes TEXT, decline_reason VARCHAR(10), -- Causal analysis (for Full profile) causal_codes VARCHAR(4)[], -- Recurrence tracking reopen_count INTEGER NOT NULL DEFAULT 0, verification_window_days INTEGER DEFAULT 60, -- Key timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), acknowledged_at TIMESTAMPTZ, work_started_at TIMESTAMPTZ, resolved_at TIMESTAMPTZ, verified_at TIMESTAMPTZ, declined_at TIMESTAMPTZ, -- Soft delete deleted_at TIMESTAMPTZ, CONSTRAINT issues_state_check CHECK ( state IN ('DETECTED', 'ACKNOWLEDGED', 'IN_PROGRESS', 'RESOLVED', 'VERIFIED', 'DECLINED', 'REOPENED', 'STALE') ), CONSTRAINT issues_intensity_check CHECK (max_intensity IN ('I1', 'I2', 'I3')), CONSTRAINT issues_decline_reason_check CHECK ( decline_reason IS NULL OR decline_reason IN ('DEC-DUP', 'DEC-OOS', 'DEC-INS', 'DEC-NAR', 'DEC-EXT', 'DEC-POL', 'DEC-OLD') ), CONSTRAINT issues_verification_window_check CHECK (verification_window_days IN (30, 60, 90)) ); CREATE INDEX idx_issues_state ON issues(state); CREATE INDEX idx_issues_domain ON issues(domain_code); CREATE INDEX idx_issues_subcode ON issues(primary_subcode); CREATE INDEX idx_issues_priority ON issues(priority_score DESC); CREATE INDEX idx_issues_created ON issues(created_at); CREATE INDEX idx_issues_owner_team ON issues(owner_team); CREATE INDEX idx_issues_deleted ON issues(deleted_at) WHERE deleted_at IS NULL; -- Partial indexes for active issues CREATE INDEX idx_issues_open ON issues(priority_score DESC) WHERE state IN ('DETECTED', 'ACKNOWLEDGED', 'IN_PROGRESS', 'REOPENED'); CREATE INDEX idx_issues_pending_verification ON issues(resolved_at, verification_window_days) WHERE state = 'RESOLVED'; COMMENT ON TABLE issues IS 'Aggregated issues from negative feedback spans per C1 lifecycle framework'; -- ----------------------------------------------------------------------------- -- 3.2 Issue-Span Links -- Many-to-many relationship between issues and contributing spans -- ----------------------------------------------------------------------------- CREATE TABLE issue_spans ( issue_id VARCHAR(20) NOT NULL REFERENCES issues(issue_id) ON DELETE CASCADE, span_id UUID NOT NULL REFERENCES spans(span_id) ON DELETE CASCADE, linked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), link_type VARCHAR(20) NOT NULL DEFAULT 'contributor', correlation_score NUMERIC(3,2), PRIMARY KEY (issue_id, span_id), CONSTRAINT issue_spans_link_type_check CHECK (link_type IN ('contributor', 'verification', 'reopen_trigger')) ); CREATE INDEX idx_issue_spans_span ON issue_spans(span_id); CREATE INDEX idx_issue_spans_linked ON issue_spans(linked_at); COMMENT ON TABLE issue_spans IS 'Links between issues and their contributing spans'; -- ----------------------------------------------------------------------------- -- 3.3 Issue State History -- Complete audit trail of state transitions -- ----------------------------------------------------------------------------- CREATE TABLE issue_state_history ( history_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), issue_id VARCHAR(20) NOT NULL REFERENCES issues(issue_id) ON DELETE CASCADE, from_state VARCHAR(15), to_state VARCHAR(15) NOT NULL, trigger_type VARCHAR(20) NOT NULL, -- 'manual', 'auto', 'span', 'sla' trigger_span_id UUID REFERENCES spans(span_id), actor_id VARCHAR(100), actor_type VARCHAR(20), -- 'user', 'system', 'rule' notes TEXT, transitioned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT history_state_check CHECK ( to_state IN ('DETECTED', 'ACKNOWLEDGED', 'IN_PROGRESS', 'RESOLVED', 'VERIFIED', 'DECLINED', 'REOPENED', 'STALE') ) ); CREATE INDEX idx_history_issue ON issue_state_history(issue_id); CREATE INDEX idx_history_transition ON issue_state_history(transitioned_at); CREATE INDEX idx_history_state ON issue_state_history(to_state); COMMENT ON TABLE issue_state_history IS 'Audit trail of all issue state transitions'; -- ----------------------------------------------------------------------------- -- 3.4 Issue Resolutions -- Detailed resolution records for resolved/verified issues -- ----------------------------------------------------------------------------- CREATE TABLE issue_resolutions ( resolution_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), issue_id VARCHAR(20) NOT NULL REFERENCES issues(issue_id) ON DELETE CASCADE, resolution_code VARCHAR(20) NOT NULL, resolution_type VARCHAR(30) NOT NULL, -- 'fix', 'workaround', 'policy_change', etc. resolution_summary TEXT NOT NULL, resolution_details TEXT, -- Preventive measures prevention_notes TEXT, process_changes TEXT, -- Verification verified_by_span_id UUID REFERENCES spans(span_id), verification_method VARCHAR(20), -- 'cr_b', 'positive_span', 'time_based' resolved_by VARCHAR(100), resolved_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), verified_at TIMESTAMPTZ, CONSTRAINT resolution_type_check CHECK ( resolution_type IN ('fix', 'workaround', 'policy_change', 'training', 'process_improvement', 'equipment', 'other') ) ); CREATE INDEX idx_resolutions_issue ON issue_resolutions(issue_id); CREATE INDEX idx_resolutions_code ON issue_resolutions(resolution_code); CREATE INDEX idx_resolutions_resolved ON issue_resolutions(resolved_at); COMMENT ON TABLE issue_resolutions IS 'Detailed resolution records with verification tracking'; -- ============================================================================= -- SECTION 4: AUDIT AND HISTORY TABLES -- Comprehensive tracking for compliance and analytics -- ============================================================================= -- ----------------------------------------------------------------------------- -- 4.1 Annotation Audit -- Who annotated what, when, and how -- ----------------------------------------------------------------------------- CREATE TABLE annotation_audit ( audit_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), span_id UUID NOT NULL REFERENCES spans(span_id) ON DELETE CASCADE, annotator_id VARCHAR(100) NOT NULL, annotator_type VARCHAR(20) NOT NULL, -- 'human', 'llm', 'rule' model_version VARCHAR(50), -- For LLM annotations -- What was set field_name VARCHAR(30) NOT NULL, old_value TEXT, new_value TEXT NOT NULL, -- Confidence and reasoning confidence NUMERIC(3,2), reasoning TEXT, annotated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT audit_annotator_type_check CHECK (annotator_type IN ('human', 'llm', 'rule', 'hybrid')) ); CREATE INDEX idx_audit_span ON annotation_audit(span_id); CREATE INDEX idx_audit_annotator ON annotation_audit(annotator_id); CREATE INDEX idx_audit_field ON annotation_audit(field_name); CREATE INDEX idx_audit_time ON annotation_audit(annotated_at); COMMENT ON TABLE annotation_audit IS 'Complete audit trail of all span annotation changes'; -- ----------------------------------------------------------------------------- -- 4.2 Classification History -- Track changes to span classifications over time -- ----------------------------------------------------------------------------- CREATE TABLE classification_history ( history_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), span_id UUID NOT NULL REFERENCES spans(span_id) ON DELETE CASCADE, change_type VARCHAR(20) NOT NULL, -- 'create', 'update', 'delete' -- Previous state (null for create) prev_primary_code VARCHAR(6), prev_valence VARCHAR(2), prev_intensity VARCHAR(2), prev_secondary_codes VARCHAR(6)[], -- New state (null for delete) new_primary_code VARCHAR(6), new_valence VARCHAR(2), new_intensity VARCHAR(2), new_secondary_codes VARCHAR(6)[], change_reason TEXT, changed_by VARCHAR(100) NOT NULL, changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT history_change_type_check CHECK (change_type IN ('create', 'update', 'delete')) ); CREATE INDEX idx_class_history_span ON classification_history(span_id); CREATE INDEX idx_class_history_time ON classification_history(changed_at); CREATE INDEX idx_class_history_type ON classification_history(change_type); COMMENT ON TABLE classification_history IS 'Historical record of classification changes for audit and rollback'; -- ============================================================================= -- SECTION 5: ANALYTICS VIEWS AND MATERIALIZED VIEWS -- Pre-computed aggregations for dashboard and reporting -- ============================================================================= -- ----------------------------------------------------------------------------- -- 5.1 Domain Summary View -- Current counts and averages by domain -- ----------------------------------------------------------------------------- CREATE MATERIALIZED VIEW mv_domain_summary AS SELECT d.domain_code, d.name AS domain_name, d.default_owner, COUNT(DISTINCT s.span_id) AS total_spans, COUNT(DISTINCT s.span_id) FILTER (WHERE s.valence = 'V+') AS positive_spans, COUNT(DISTINCT s.span_id) FILTER (WHERE s.valence = 'V-') AS negative_spans, COUNT(DISTINCT s.span_id) FILTER (WHERE s.valence = 'V0') AS neutral_spans, COUNT(DISTINCT s.span_id) FILTER (WHERE s.valence = 'V±') AS mixed_spans, COUNT(DISTINCT s.span_id) FILTER (WHERE s.intensity = 'I3') AS critical_spans, ROUND(AVG(s.confidence_score), 3) AS avg_confidence, COUNT(DISTINCT i.issue_id) FILTER (WHERE i.state NOT IN ('VERIFIED', 'DECLINED')) AS open_issues, MAX(s.created_at) AS last_span_at FROM urt_domains d LEFT JOIN spans s ON LEFT(s.primary_code, 1) = d.domain_code AND s.deleted_at IS NULL LEFT JOIN issues i ON i.domain_code = d.domain_code AND i.deleted_at IS NULL GROUP BY d.domain_code, d.name, d.default_owner ORDER BY d.display_order; CREATE UNIQUE INDEX idx_mv_domain_summary ON mv_domain_summary(domain_code); COMMENT ON MATERIALIZED VIEW mv_domain_summary IS 'Aggregated metrics by URT domain for executive dashboards'; -- ----------------------------------------------------------------------------- -- 5.2 Category Breakdown View -- Detailed metrics at category level -- ----------------------------------------------------------------------------- CREATE MATERIALIZED VIEW mv_category_breakdown AS SELECT c.category_code, c.name AS category_name, c.domain_code, d.name AS domain_name, COUNT(DISTINCT s.span_id) AS total_spans, COUNT(DISTINCT s.span_id) FILTER (WHERE s.valence = 'V-') AS negative_count, COUNT(DISTINCT s.span_id) FILTER (WHERE s.valence = 'V+') AS positive_count, ROUND( COUNT(DISTINCT s.span_id) FILTER (WHERE s.valence = 'V-')::NUMERIC / NULLIF(COUNT(DISTINCT s.span_id), 0) * 100, 1 ) AS negative_pct, COUNT(DISTINCT i.issue_id) AS total_issues, COUNT(DISTINCT i.issue_id) FILTER (WHERE i.state = 'VERIFIED') AS resolved_verified, ROUND(AVG(CASE WHEN s.intensity = 'I1' THEN 1 WHEN s.intensity = 'I2' THEN 2 WHEN s.intensity = 'I3' THEN 3 END), 2) AS avg_intensity FROM urt_categories c JOIN urt_domains d ON c.domain_code = d.domain_code LEFT JOIN spans s ON s.primary_code LIKE c.category_code || '%' AND s.deleted_at IS NULL LEFT JOIN issues i ON i.primary_subcode LIKE c.category_code || '%' AND i.deleted_at IS NULL GROUP BY c.category_code, c.name, c.domain_code, d.name ORDER BY c.domain_code, c.display_order; CREATE UNIQUE INDEX idx_mv_category_breakdown ON mv_category_breakdown(category_code); COMMENT ON MATERIALIZED VIEW mv_category_breakdown IS 'Detailed category-level metrics for operational analysis'; -- ----------------------------------------------------------------------------- -- 5.3 Issue Pipeline View -- Current state of all open issues -- ----------------------------------------------------------------------------- CREATE MATERIALIZED VIEW mv_issue_pipeline AS SELECT i.state, i.domain_code, d.name AS domain_name, COUNT(*) AS issue_count, AVG(i.priority_score) AS avg_priority, AVG(i.span_count) AS avg_span_count, AVG(EXTRACT(EPOCH FROM (NOW() - i.created_at)) / 3600) AS avg_age_hours, COUNT(*) FILTER (WHERE i.max_intensity = 'I3') AS critical_count, COUNT(*) FILTER (WHERE i.reopen_count > 0) AS reopened_count FROM issues i JOIN urt_domains d ON i.domain_code = d.domain_code WHERE i.deleted_at IS NULL AND i.state NOT IN ('VERIFIED', 'DECLINED') GROUP BY i.state, i.domain_code, d.name ORDER BY CASE i.state WHEN 'DETECTED' THEN 1 WHEN 'ACKNOWLEDGED' THEN 2 WHEN 'IN_PROGRESS' THEN 3 WHEN 'REOPENED' THEN 4 WHEN 'RESOLVED' THEN 5 ELSE 6 END, d.display_order; CREATE INDEX idx_mv_issue_pipeline ON mv_issue_pipeline(state, domain_code); COMMENT ON MATERIALIZED VIEW mv_issue_pipeline IS 'Current issue pipeline by state and domain for SLA tracking'; -- ----------------------------------------------------------------------------- -- 5.4 Comparative Reference Trends View -- Track improvement/decline signals over time -- ----------------------------------------------------------------------------- CREATE MATERIALIZED VIEW mv_cr_trends AS SELECT DATE_TRUNC('week', s.created_at) AS week_start, s.comparative, LEFT(s.primary_code, 1) AS domain_code, COUNT(*) AS span_count, COUNT(*) FILTER (WHERE s.valence = 'V+') AS positive_count, COUNT(*) FILTER (WHERE s.valence = 'V-') AS negative_count FROM spans s WHERE s.deleted_at IS NULL AND s.comparative IS NOT NULL AND s.created_at >= NOW() - INTERVAL '90 days' GROUP BY DATE_TRUNC('week', s.created_at), s.comparative, LEFT(s.primary_code, 1) ORDER BY week_start DESC, domain_code; CREATE INDEX idx_mv_cr_trends ON mv_cr_trends(week_start, comparative); COMMENT ON MATERIALIZED VIEW mv_cr_trends IS 'Weekly trends of comparative reference signals (CR-B/W/S) for improvement tracking'; -- ----------------------------------------------------------------------------- -- 5.5 Daily Span Volume (for time-series analysis) -- Partitioning-ready daily aggregation -- ----------------------------------------------------------------------------- CREATE MATERIALIZED VIEW mv_daily_volume AS SELECT DATE(s.created_at) AS span_date, LEFT(s.primary_code, 1) AS domain_code, s.valence, s.intensity, COUNT(*) AS span_count, COUNT(DISTINCT s.review_id) AS review_count, AVG(s.confidence_score) AS avg_confidence FROM spans s WHERE s.deleted_at IS NULL GROUP BY DATE(s.created_at), LEFT(s.primary_code, 1), s.valence, s.intensity ORDER BY span_date DESC; CREATE INDEX idx_mv_daily_volume_date ON mv_daily_volume(span_date); CREATE INDEX idx_mv_daily_volume_domain ON mv_daily_volume(domain_code); COMMENT ON MATERIALIZED VIEW mv_daily_volume IS 'Daily span volume by domain/valence/intensity for trend analysis'; -- ============================================================================= -- SECTION 6: HELPER FUNCTIONS -- Utility functions for common operations -- ============================================================================= -- ----------------------------------------------------------------------------- -- 6.1 Generate Issue ID -- Format: ISSUE-YYYY-NNNN (sequential within year) -- ----------------------------------------------------------------------------- CREATE SEQUENCE issue_id_seq START 1; CREATE OR REPLACE FUNCTION generate_issue_id() RETURNS VARCHAR(20) AS $$ DECLARE year_part VARCHAR(4); seq_part INTEGER; BEGIN year_part := TO_CHAR(NOW(), 'YYYY'); seq_part := NEXTVAL('issue_id_seq'); RETURN 'ISSUE-' || year_part || '-' || LPAD(seq_part::TEXT, 4, '0'); END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION generate_issue_id IS 'Generate sequential issue IDs in format ISSUE-YYYY-NNNN'; -- ----------------------------------------------------------------------------- -- 6.2 Calculate Priority Score -- Based on C1 framework: I_weight * (1 + log(span_count)) * decay * recurrence_boost -- ----------------------------------------------------------------------------- CREATE OR REPLACE FUNCTION calculate_priority_score( p_max_intensity VARCHAR(2), p_span_count INTEGER, p_days_old NUMERIC, p_recurrence_count INTEGER DEFAULT 0, p_has_cr_w BOOLEAN DEFAULT FALSE ) RETURNS NUMERIC AS $$ DECLARE intensity_weight NUMERIC; decay_factor NUMERIC; recurrence_boost NUMERIC; trend_modifier NUMERIC; BEGIN -- Intensity weights intensity_weight := CASE p_max_intensity WHEN 'I1' THEN 1.0 WHEN 'I2' THEN 2.0 WHEN 'I3' THEN 4.0 ELSE 1.0 END; -- Time decay: exp(-lambda * days), lambda = 0.023 (half-life ~30 days) decay_factor := EXP(-0.023 * p_days_old); -- Recurrence boost: 1.0 + 0.5 * log2(recurrence_count + 1) IF p_recurrence_count > 0 THEN recurrence_boost := 1.0 + 0.5 * LOG(2, p_recurrence_count + 1); ELSE recurrence_boost := 1.0; END IF; -- Trend modifier (CR-W = worsening = 1.3) trend_modifier := CASE WHEN p_has_cr_w THEN 1.3 ELSE 1.0 END; RETURN ROUND( intensity_weight * (1 + LOG(GREATEST(p_span_count, 1))) * decay_factor * recurrence_boost * trend_modifier, 2 ); END; $$ LANGUAGE plpgsql IMMUTABLE; COMMENT ON FUNCTION calculate_priority_score IS 'Calculate issue priority score per C1 time decay model'; -- ----------------------------------------------------------------------------- -- 6.3 Calculate Confidence Score -- Based on C1: base_confidence + specificity_bonus, weighted by evidence type -- ----------------------------------------------------------------------------- CREATE OR REPLACE FUNCTION calculate_confidence_score( p_span_count INTEGER, p_max_specificity VARCHAR(2), p_avg_evidence_weight NUMERIC ) RETURNS NUMERIC AS $$ DECLARE base_confidence NUMERIC; specificity_bonus NUMERIC; BEGIN -- Base confidence by span count base_confidence := CASE WHEN p_span_count = 1 THEN 0.50 WHEN p_span_count = 2 THEN 0.70 WHEN p_span_count = 3 THEN 0.80 WHEN p_span_count BETWEEN 4 AND 5 THEN 0.85 WHEN p_span_count BETWEEN 6 AND 10 THEN 0.90 ELSE 0.95 END; -- Specificity bonus specificity_bonus := CASE p_max_specificity WHEN 'S1' THEN 0.00 WHEN 'S2' THEN 0.05 WHEN 'S3' THEN 0.10 ELSE 0.00 END; RETURN ROUND( LEAST(0.95, base_confidence + specificity_bonus) * COALESCE(p_avg_evidence_weight, 1.0), 3 ); END; $$ LANGUAGE plpgsql IMMUTABLE; COMMENT ON FUNCTION calculate_confidence_score IS 'Calculate issue confidence score per C1 framework'; -- ============================================================================= -- SECTION 7: TRIGGERS -- Automated data maintenance -- ============================================================================= -- ----------------------------------------------------------------------------- -- 7.1 Auto-update timestamps -- ----------------------------------------------------------------------------- CREATE OR REPLACE FUNCTION update_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER tr_reviews_updated BEFORE UPDATE ON reviews FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER tr_spans_updated BEFORE UPDATE ON spans FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER tr_causal_chains_updated BEFORE UPDATE ON causal_chains FOR EACH ROW EXECUTE FUNCTION update_updated_at(); -- ----------------------------------------------------------------------------- -- 7.2 Refresh materialized views (scheduled separately) -- ----------------------------------------------------------------------------- CREATE OR REPLACE FUNCTION refresh_urt_materialized_views() RETURNS void AS $$ BEGIN REFRESH MATERIALIZED VIEW CONCURRENTLY mv_domain_summary; REFRESH MATERIALIZED VIEW CONCURRENTLY mv_category_breakdown; REFRESH MATERIALIZED VIEW mv_issue_pipeline; REFRESH MATERIALIZED VIEW mv_cr_trends; REFRESH MATERIALIZED VIEW mv_daily_volume; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION refresh_urt_materialized_views IS 'Refresh all URT materialized views - call periodically via pg_cron or similar'; -- ============================================================================= -- SECTION 8: REFERENCE DATA INSERTS -- Populate reference tables from B1-urt-codes.yaml -- ============================================================================= -- ----------------------------------------------------------------------------- -- 8.1 Insert Domains -- ----------------------------------------------------------------------------- INSERT INTO urt_domains (domain_code, name, description, core_question, default_owner, display_order) VALUES ('O', 'Offering', 'The core product, service, or outcome delivered', 'Does what we provide actually work and meet expectations?', 'Product / Operations', 1), ('P', 'People', 'Human interactions and personnel behavior', 'How do the people we interact with treat us and perform their roles?', 'HR / Training', 2), ('J', 'Journey', 'The process, timing, and operational flow', 'Is the experience smooth, timely, and friction-free?', 'Operations / Process', 3), ('E', 'Environment', 'Physical, digital, and ambient context', 'Is the space where the experience occurs functional, safe, and pleasant?', 'Facilities / IT', 4), ('A', 'Access', 'Availability, accessibility, and inclusivity', 'Can everyone who wants to participate do so fully and fairly?', 'Compliance / Design', 5), ('V', 'Value', 'Cost, pricing, and worth of the exchange', 'Is what I am giving up fair for what I am getting?', 'Finance / Pricing', 6), ('R', 'Relationship', 'Trust, reliability, and ongoing connection', 'Can I trust this business and do they value our relationship?', 'Leadership / CX', 7); -- ----------------------------------------------------------------------------- -- 8.2 Insert Categories (28 total) -- ----------------------------------------------------------------------------- INSERT INTO urt_categories (category_code, domain_code, name, definition, display_order) VALUES -- Offering ('O1', 'O', 'Function', 'Does it do what it is supposed to do?', 1), ('O2', 'O', 'Quality', 'How well is it made or executed?', 2), ('O3', 'O', 'Completeness', 'Is everything included that should be?', 3), ('O4', 'O', 'Fit', 'Does it match the customer specific needs?', 4), -- People ('P1', 'P', 'Attitude', 'Disposition, manner, and emotional tone', 1), ('P2', 'P', 'Competence', 'Knowledge, skill, and professional capability', 2), ('P3', 'P', 'Responsiveness', 'Attentiveness, initiative, and follow-through', 3), ('P4', 'P', 'Communication', 'Quality of information exchange', 4), -- Journey ('J1', 'J', 'Timing', 'Speed, punctuality, and time management', 1), ('J2', 'J', 'Ease', 'Effort required and friction encountered', 2), ('J3', 'J', 'Reliability', 'Consistency and predictability of process', 3), ('J4', 'J', 'Resolution', 'How problems are handled when they arise', 4), -- Environment ('E1', 'E', 'Physical Space', 'Tangible environment attributes', 1), ('E2', 'E', 'Digital Space', 'Online and application interface', 2), ('E3', 'E', 'Ambiance', 'Intangible environmental qualities', 3), ('E4', 'E', 'Safety', 'Security and wellbeing factors', 4), -- Access ('A1', 'A', 'Availability', 'Can you get it when you need it?', 1), ('A2', 'A', 'Accessibility', 'Can everyone use it regardless of ability?', 2), ('A3', 'A', 'Inclusivity', 'Does it work for diverse backgrounds?', 3), ('A4', 'A', 'Convenience', 'Is it easy to reach and engage with?', 4), -- Value ('V1', 'V', 'Price', 'The monetary cost', 1), ('V2', 'V', 'Transparency', 'Clarity and honesty about costs', 2), ('V3', 'V', 'Effort', 'Non-monetary costs (time, hassle)', 3), ('V4', 'V', 'Worth', 'Overall value assessment', 4), -- Relationship ('R1', 'R', 'Integrity', 'Honesty and ethical behavior', 1), ('R2', 'R', 'Dependability', 'Consistency over time', 2), ('R3', 'R', 'Recovery', 'Response to failures', 3), ('R4', 'R', 'Loyalty', 'Investment in ongoing relationship', 4); -- ----------------------------------------------------------------------------- -- 8.3 Insert Causal Codes (16 total) -- ----------------------------------------------------------------------------- INSERT INTO urt_causal_codes (causal_code, layer, layer_prefix, name, definition, display_order) VALUES -- Conditions layer (5 codes) ('CD-S', 'conditions', 'CD-', 'Staff State', 'Fatigue, training, motivation, experience', 1), ('CD-T', 'conditions', 'CD-', 'Team Dynamics', 'Handoffs, coordination, communication', 2), ('CD-E', 'conditions', 'CD-', 'Equipment', 'Malfunction, unavailable, outdated', 3), ('CD-F', 'conditions', 'CD-', 'Facility', 'Maintenance, capacity, hazards', 4), ('CD-O', 'conditions', 'CD-', 'Operational', 'Understaffing, demand surge, time pressure', 5), -- Management layer (5 codes) ('MG-P', 'management', 'MG-', 'Planning', 'Staffing plans, scheduling, forecasting', 1), ('MG-T', 'management', 'MG-', 'Training', 'Preparation, development, competency', 2), ('MG-O', 'management', 'MG-', 'Oversight', 'Supervision, monitoring, correction', 3), ('MG-R', 'management', 'MG-', 'Resources', 'Maintenance, supplies, equipment', 4), ('MG-C', 'management', 'MG-', 'Communication', 'Policy relay, expectations, culture', 5), -- Systemic layer (6 codes) ('SY-R', 'systemic', 'SY-', 'Resource Decisions', 'Budget, investment, staffing levels', 1), ('SY-P', 'systemic', 'SY-', 'Policy/Procedure', 'Rules, requirements, bureaucracy', 2), ('SY-C', 'systemic', 'SY-', 'Culture', 'Values, priorities, norms', 3), ('SY-S', 'systemic', 'SY-', 'Standards', 'Quality thresholds, metrics, expectations', 4), ('SY-H', 'systemic', 'SY-', 'Human Capital', 'Compensation, hiring, retention', 5), ('SY-X', 'systemic', 'SY-', 'External', 'Market, regulatory, competitive pressure', 6); -- ----------------------------------------------------------------------------- -- 8.4 Insert Metadata Values (24 total across 7 dimensions) -- ----------------------------------------------------------------------------- INSERT INTO urt_metadata_values (dimension_code, value_code, dimension_name, label, definition, is_default, display_order) VALUES -- Valence (4 values) ('V', 'V+', 'Valence', 'Positive', 'Praise, satisfaction', FALSE, 1), ('V', 'V-', 'Valence', 'Negative', 'Complaint, dissatisfaction', FALSE, 2), ('V', 'V0', 'Valence', 'Neutral', 'Observation without judgment', FALSE, 3), ('V', 'V±', 'Valence', 'Mixed', 'Both positive and negative in same span', FALSE, 4), -- Intensity (3 values) ('I', 'I1', 'Intensity', 'Mild', 'Slight preference/concern', FALSE, 1), ('I', 'I2', 'Intensity', 'Moderate', 'Clear but not extreme', FALSE, 2), ('I', 'I3', 'Intensity', 'Strong', 'Emphatic or intense', FALSE, 3), -- Specificity (3 values) ('S', 'S1', 'Specificity', 'Vague', 'General impression only', FALSE, 1), ('S', 'S2', 'Specificity', 'Moderate', 'Some details or context', FALSE, 2), ('S', 'S3', 'Specificity', 'Specific', 'Concrete details, names, times, amounts', FALSE, 3), -- Actionability (3 values) ('A', 'A1', 'Actionability', 'Low', 'Feeling with no clear action path', FALSE, 1), ('A', 'A2', 'Actionability', 'Medium', 'Suggests improvement area', FALSE, 2), ('A', 'A3', 'Actionability', 'High', 'Specific implementable feedback', FALSE, 3), -- Temporal (4 values) ('T', 'TC', 'Temporal Reference', 'Current', 'This specific visit/experience', TRUE, 1), ('T', 'TR', 'Temporal Reference', 'Recent', 'Recent pattern of experiences', FALSE, 2), ('T', 'TH', 'Temporal Reference', 'Historical', 'Long-standing pattern', FALSE, 3), ('T', 'TF', 'Temporal Reference', 'Future', 'Expectations or predictions', FALSE, 4), -- Evidence (3 values) ('E', 'ES', 'Evidence Type', 'Stated', 'Explicitly said by customer', TRUE, 1), ('E', 'EI', 'Evidence Type', 'Inferred', 'Logically entailed by text', FALSE, 2), ('E', 'EC', 'Evidence Type', 'Contextual', 'Requires surrounding text', FALSE, 3), -- Comparative Reference (4 values) ('CR', 'CR-N', 'Comparative Reference', 'None', 'No comparison to previous state', TRUE, 1), ('CR', 'CR-B', 'Comparative Reference', 'Better', 'Explicit improvement vs. before', FALSE, 2), ('CR', 'CR-W', 'Comparative Reference', 'Worse', 'Explicit decline vs. before', FALSE, 3), ('CR', 'CR-S', 'Comparative Reference', 'Same', 'Explicitly unchanged', FALSE, 4); -- ----------------------------------------------------------------------------- -- 8.5 Insert Subcodes (sample - O domain) -- Full 138 subcodes would be inserted similarly -- ----------------------------------------------------------------------------- INSERT INTO urt_subcodes (subcode, category_code, domain_code, name, definition, positive_example, negative_example, dont_confuse_with, dont_confuse_reason, display_order) VALUES -- O1: Function ('O1.01', 'O1', 'O', 'Works/Doesn''t Work', 'Basic functionality success or failure', 'Software runs perfectly', 'Car won''t start', 'J3.03', 'J3.03 is system uptime, O1.01 is product function', 1), ('O1.02', 'O1', 'O', 'Performance Level', 'How well it operates', 'Incredibly fast processor', 'Sluggish and laggy', 'E2.03', 'E2.03 is interface speed, O1.02 is product performance', 2), ('O1.03', 'O1', 'O', 'Durability', 'Longevity and resistance to wear', 'Still perfect after 5 years', 'Fell apart in a month', 'O2.01', 'O2.01 is material quality, O1.03 is longevity', 3), ('O1.04', 'O1', 'O', 'Reliability', 'Consistency of function over time', 'Never fails me', 'Works sometimes, not others', 'J3.01', 'J3.01 is process consistency, O1.04 is product reliability', 4), ('O1.05', 'O1', 'O', 'Outcome Achievement', 'Did customer accomplish their goal?', 'Passed my exam!', 'Treatment didn''t work', 'V4.03', 'V4.03 is satisfaction with exchange, O1.05 is goal achievement', 5), -- O2: Quality ('O2.01', 'O2', 'O', 'Materials/Inputs', 'Quality of components or ingredients', 'Real leather, premium feel', 'Cheap plastic parts', 'O1.03', 'O1.03 is durability, O2.01 is material quality', 1), ('O2.02', 'O2', 'O', 'Craftsmanship', 'Skill of construction or execution', 'Beautifully sewn seams', 'Sloppy assembly', 'P2.02', 'P2.02 is staff skill, O2.02 is product craftsmanship', 2), ('O2.03', 'O2', 'O', 'Presentation', 'Visual and aesthetic quality', 'Gorgeous plating', 'Looked thrown together', 'E3.05', 'E3.05 is space aesthetics, O2.03 is product presentation', 3), ('O2.04', 'O2', 'O', 'Attention to Detail', 'Finishing touches and refinement', 'Every corner perfect', 'Full of typos', 'O3.01', 'O3.01 is completeness, O2.04 is refinement', 4), ('O2.05', 'O2', 'O', 'Condition at Delivery', 'State when received', 'Still warm from oven', 'Arrived damaged', 'J3.02', 'J3.02 is process accuracy, O2.05 is delivery condition', 5), -- O3: Completeness ('O3.01', 'O3', 'O', 'All Components Present', 'Nothing missing from what was promised', 'Everything in the box', 'Missing the charger', 'O4.01', 'O4.01 is spec match, O3.01 is completeness', 1), ('O3.02', 'O3', 'O', 'Feature Availability', 'Promised features actually work', 'All menu items available', 'Half the features disabled', 'A1.03', 'A1.03 is inventory, O3.02 is feature availability', 2), ('O3.03', 'O3', 'O', 'Scope Delivery', 'Full scope of work completed', 'Cleaned entire house', 'Left the bathrooms', 'J4.04', 'J4.04 is resolution quality, O3.03 is scope delivery', 3), ('O3.04', 'O3', 'O', 'Documentation', 'Supporting materials provided', 'Great user manual', 'No instructions at all', 'J2.01', 'J2.01 is process simplicity, O3.04 is documentation as product artifact', 4), -- O4: Fit ('O4.01', 'O4', 'O', 'Specification Match', 'Matches what was ordered', 'Exactly what I ordered', 'Wrong size delivered', 'J3.02', 'J3.02 is execution accuracy, O4.01 is spec match', 1), ('O4.02', 'O4', 'O', 'Personalization', 'Adapted to individual preferences', 'Remembered my usual', 'No way to save prefs', 'P3.01', 'P3.01 is attentiveness, O4.02 is personalization', 2), ('O4.03', 'O4', 'O', 'Flexibility', 'Can be modified or adjusted', 'Happy to substitute', 'No modifications allowed', 'V2.04', 'V2.04 is policy fairness, O4.03 is flexibility', 3), ('O4.04', 'O4', 'O', 'Appropriateness', 'Right solution for the need', 'Perfect recommendation', 'Sold me wrong thing', 'P2.01', 'P2.01 is knowledge, O4.04 is appropriateness', 4); -- Note: Additional subcodes for P, J, E, A, V, R domains would be inserted similarly. -- The full B1-urt-codes.yaml contains 138 subcodes total. -- For production, generate INSERT statements from the YAML source. -- ============================================================================= -- SECTION 9: PARTITIONING STRATEGY (for high-volume scenarios) -- ============================================================================= -- -- For deployments expecting >1M spans/month, consider partitioning: -- -- 1. Spans table: Partition by created_at (monthly) -- CREATE TABLE spans (...) PARTITION BY RANGE (created_at); -- CREATE TABLE spans_2026_01 PARTITION OF spans -- FOR VALUES FROM ('2026-01-01') TO ('2026-02-01'); -- -- 2. Reviews table: Same monthly partitioning -- -- 3. Annotation audit: Partition by annotated_at -- -- 4. Issue state history: Partition by transitioned_at -- -- Benefits: -- - Faster queries on recent data (partition pruning) -- - Easier data retention management (drop old partitions) -- - Parallel query execution across partitions -- -- ============================================================================= -- ============================================================================= -- SECTION 10: GRANTS AND SECURITY -- ============================================================================= -- Create application roles -- CREATE ROLE urt_reader; -- CREATE ROLE urt_writer; -- CREATE ROLE urt_admin; -- Reader permissions (dashboards, analytics) -- GRANT SELECT ON ALL TABLES IN SCHEMA public TO urt_reader; -- GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO urt_reader; -- Writer permissions (classification pipeline) -- GRANT SELECT, INSERT, UPDATE ON reviews, spans, span_classifications, -- causal_chains, issues, issue_spans, issue_state_history, -- issue_resolutions, annotation_audit, classification_history TO urt_writer; -- GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO urt_writer; -- Admin permissions (schema management) -- GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO urt_admin; -- GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO urt_admin; -- ============================================================================= -- END OF SCHEMA -- ============================================================================= -- -- Post-deployment checklist: -- 1. Run: SELECT refresh_urt_materialized_views(); -- 2. Set up pg_cron job for periodic MV refresh -- 3. Configure connection pooling (PgBouncer recommended) -- 4. Set up monitoring for table sizes and index usage -- 5. Import full subcode data from B1-urt-codes.yaml -- -- =============================================================================