From f99827717f9641874a924124775ae1c2bd3a1308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Sat, 24 Jan 2026 12:55:31 +0000 Subject: [PATCH] Final polish: v3.1.2 operational safety constraints - Add chk_dedup_scoped constraint enforcing tenant-scoped dedup format - Filter location_type='owned' in populate_facts() for 'ALL' rollup - Document competitor exclusion from 'ALL' sentinel rollups - Add explicit comments in aggregation code for maintainability Co-Authored-By: Claude Opus 4.5 --- .artifacts/ReviewIQ-Architecture-v3.1.md | 41 ++++++++++++++++++------ 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/.artifacts/ReviewIQ-Architecture-v3.1.md b/.artifacts/ReviewIQ-Architecture-v3.1.md index 14e6231..a4f2ef7 100644 --- a/.artifacts/ReviewIQ-Architecture-v3.1.md +++ b/.artifacts/ReviewIQ-Architecture-v3.1.md @@ -271,6 +271,11 @@ CREATE INDEX idx_enriched_embedding ON reviews_enriched ALTER TABLE reviews_enriched ADD CONSTRAINT fk_enriched_location FOREIGN KEY (business_id, place_id) REFERENCES locations(business_id, place_id); + +-- Enforce tenant-scoped dedup format +ALTER TABLE reviews_enriched + ADD CONSTRAINT chk_dedup_scoped + CHECK (dedup_group_id IS NULL OR dedup_group_id LIKE business_id || ':%'); ``` ### 2.3 Issue Tables (Relational, No Arrays) @@ -488,6 +493,11 @@ CREATE INDEX idx_facts_all_locations ON fact_timeseries(business_id, period_date | `domain` | โšก Derived | Rollup from urt_code at query time | | `issue` | ๐Ÿ”œ Optional | Recommended for issue timelines (v3.2) | +**v3.1 Rollup Rules**: +- `place_id='ALL'` includes **owned locations only** (not competitors) +- Competitor facts live at their `competitor_place_id`, never in `'ALL'` rollup +- Competitor comparison queries explicitly join on `competitor_place_id` + **v3.1 Trust Score Usage**: - `trust_score` is applied to **issue priority scoring** and **filtering** (see ยง4.2) - `trust_weighted_strength` / `trust_weighted_negative` columns are **reserved but not populated** in v3.1 @@ -985,23 +995,34 @@ async def populate_facts(business_id: str, date: date, bucket_type: str = 'day') next_month = period_start.replace(day=28) + timedelta(days=4) period_end = next_month.replace(day=1) - locations = await db.query( - "SELECT place_id FROM locations WHERE business_id = %s AND is_active = TRUE", + # Get owned locations (competitors excluded from 'ALL' rollup) + owned_locations = await db.query( + "SELECT place_id FROM locations WHERE business_id = %s AND is_active = TRUE AND location_type = 'owned'", + [business_id] + ) + owned_place_ids = [loc['place_id'] for loc in owned_locations] + + # Get competitor locations (facts per place only, no 'ALL' rollup) + competitor_locations = await db.query( + "SELECT place_id FROM locations WHERE business_id = %s AND is_active = TRUE AND location_type = 'competitor'", [business_id] ) - all_place_ids = [loc['place_id'] for loc in locations] - - # Per-location facts - for loc in locations: - place_id = loc['place_id'] + # Per-location facts (owned) + for loc in owned_locations: await populate_location_facts( - business_id, place_id, period_start, period_end, bucket_type + business_id, loc['place_id'], period_start, period_end, bucket_type ) - # All-locations rollup (place_id='ALL') + # Per-location facts (competitors โ€” no 'ALL' rollup) + for loc in competitor_locations: + await populate_location_facts( + business_id, loc['place_id'], period_start, period_end, bucket_type + ) + + # All-locations rollup (owned only โ€” place_id='ALL') await populate_all_locations_facts( - business_id, all_place_ids, period_start, period_end, bucket_type + business_id, owned_place_ids, period_start, period_end, bucket_type )