## Handoff: Enriched Shadow Mode — CalendarValidator Implementation **What:** Implement CalendarValidator class that queries Google Calendar (read-only) on Tier 2 tripwire fire, fuzzy-matches extracted events against calendar events, and reports MATCH | NO_MATCH | CONFLICT status. **Why:** Service account authenticated, calendar read-only verified, test event confirmed. Ready for calendar validation logic. **Files:** - `/home/hoffmann_admin/.openclaw/workspace/services/icarus/docs/ENRICHED-SHADOW-SPEC.md` — Full architecture spec - `/home/hoffmann_admin/.openclaw/workspace/services/icarus/core/observer/shadow_bot.py` — Main observer (integrate CalendarValidator) - `/home/hoffmann_admin/.openclaw/workspace/services/icarus/core/observer/shadow_database.py` — Add calendar validation columns - `/home/hoffmann_admin/.openclaw/workspace/services/icarus/core/observer/shadow_telegram.py` — Add Admin DM exhaust method **Current State:** - Service account: `family-calendar-sync@hoffmann-family-manager.iam.gserviceaccount.com` - Calendar ID: `hoffmann.family.manager@gmail.com` (primary, shared with service account) - Scope: `calendar.readonly` (verified — write returns 403) - Test query: ✅ Working, returns events - Command: `gog calendar events hoffmann.family.manager@gmail.com --from --to --account family-calendar-sync@hoffmann-family-manager.iam.gserviceaccount.com --json` **What You Need to Implement:** ### 1. CalendarValidator Class ```python # services/icarus/core/observer/calendar_validator.py class CalendarValidator: """Read-only calendar validation for extracted events.""" def __init__(self, calendar_id: str, account: str): self.calendar_id = calendar_id self.account = account def check_event(self, extracted_event: dict) -> CalendarCheckResult: """ Query calendar for events matching extracted_event. Returns: MATCH: Event exists with similar title/time NO_MATCH: No similar event found CONFLICT: Event exists but details differ """ def fuzzy_match_score(self, extracted_title: str, calendar_title: str) -> float: """Fuzzy string matching (0.0-1.0)""" def query_calendar(self, date_start: str, date_end: str) -> list: """Execute gog calendar events query""" ``` ### 2. Integration Points **In `shadow_bot.py` after Tier 2 extraction:** ```python # After LLM extraction if extraction_result and extraction_result.confidence >= 0.7: # [NEW] Calendar validation calendar_check = self.calendar_validator.check_event( extraction_result.raw_extraction ) if calendar_check.status == "MATCH": # Event exists — log silently, no DM needed self.db.update_calendar_check( shadow_msg_id=shadow_msg_id, status="match", matched_event_id=calendar_check.event_id, matched_event_title=calendar_check.event_title, ) elif calendar_check.status == "NO_MATCH": # Event not found — queue Admin DM self._send_admin_dm_no_match( shadow_msg_id=shadow_msg_id, extracted_event=extraction_result.raw_extraction, ) elif calendar_check.status == "CONFLICT": # Event exists but details differ — flag conflict self._send_admin_dm_conflict( shadow_msg_id=shadow_msg_id, extracted_event=extraction_result.raw_extraction, calendar_event=calendar_check.matched_event, ) ``` ### 3. Fuzzy Matching Logic ```python # Use difflib.SequenceMatcher or rapidfuzz for fuzzy matching # Score thresholds: # - ≥ 0.7: Strong match (same event) # - 0.4-0.7: Possible match (check time/location) # - < 0.4: No match def fuzzy_match_score(self, extracted: str, calendar_title: str) -> float: from difflib import SequenceMatcher return SequenceMatcher(None, extracted.lower(), calendar_title.lower()).ratio() ``` ### 4. Admin DM Exhaust Methods ```python # In shadow_telegram.py or shadow_bot.py def _send_admin_dm_no_match(self, shadow_msg_id, extracted_event): """DM Matt about proposed calendar action.""" message = f""" 🔍 Shadow Mode — Proposed Calendar Action Message ID: {shadow_msg_id} Extracted: {extracted_event['what']} When: {extracted_event['when']} Who: {', '.join(extracted_event['who'])} Calendar: ❌ No matching event found I would have added: 📅 "{extracted_event['what']}" — {extracted_event['when']} Accurate? [Yes] [No] [Edit] ⚠️ This is a simulation. Calendar not modified. """ self._send_dm_to_admin(message) def _send_admin_dm_conflict(self, shadow_msg_id, extracted_event, calendar_event): """DM Matt about calendar conflict.""" message = f""" 🔍 Shadow Mode — Calendar Conflict Detected Message ID: {shadow_msg_id} Extracted: {extracted_event['what']} at {extracted_event['when']} Calendar: 📅 "{calendar_event['summary']}" at {calendar_event['start']} Conflict: {calendar_event['conflict_description']} Which is correct? [📅 Keep Calendar] [💬 Use Message] [❓ I'm Not Sure] ⚠️ This is a simulation. Calendar not modified. """ self._send_dm_to_admin(message) ``` ### 5. Database Schema Update ```sql -- Add to shadow_extractions table ALTER TABLE shadow_extractions ADD COLUMN calendar_check_status TEXT; -- match | no_match | conflict ALTER TABLE shadow_extractions ADD COLUMN calendar_event_id TEXT; ALTER TABLE shadow_extractions ADD COLUMN calendar_event_title TEXT; ALTER TABLE shadow_extractions ADD COLUMN calendar_event_time TEXT; ALTER TABLE shadow_extractions ADD COLUMN fuzzy_match_score REAL; ALTER TABLE shadow_extractions ADD COLUMN admin_dm_sent BOOLEAN DEFAULT FALSE; ALTER TABLE shadow_extractions ADD COLUMN admin_response TEXT; -- yes | no | edit | timeout ``` **Scope:** - ✅ DO: CalendarValidator class with fuzzy matching - ✅ DO: gog command execution for calendar queries - ✅ DO: Integration into shadow_bot.py flow - ✅ DO: Admin DM exhaust methods - ✅ DO: Database schema updates - ❌ DON'T: Write to calendar (read-only only) - ❌ DON'T: Modify tripwire logic (that's already tuned) - ❌ DON'T: Add frontend UI (Daedalus handles that) **Success Criteria:** - CalendarValidator queries calendar on tripwire fire - Fuzzy matching returns score 0.0-1.0 - Status correctly identified: MATCH | NO_MATCH | CONFLICT - Admin DM format matches spec - Database stores calendar validation results - No calendar write operations (verified by 403 test) **Dependencies:** - gog CLI: Already configured with service account - difflib: Python standard library (no install needed) **Calendar Query Command:** ```bash gog calendar events hoffmann.family.manager@gmail.com \ --from \ --to \ --account family-calendar-sync@hoffmann-family-manager.iam.gserviceaccount.com \ --json ``` **ETA:** TBD — provide your own after review **Coordination:** - After implementation, Wadsworth will test with real messages - Admin DM routing will be configured - Integration tests against test event "Test" on May 3