# Engineering Directive v2 — Active Pointer + Smart Deletion Pattern (hardened) **Author**: Matthew Raymer **Date**: 2025-01-27 **Status**: 🎯 **ACTIVE** - Production-grade engineering directive for implementing smart deletion patterns ## Overview This supersedes the previous draft and is **copy-pasteable** for any ``. It keeps UX smooth, guarantees data integrity, and adds production-grade safeguards (bootstrapping, races, soft deletes, bulk ops, and testability). Built on your prior pattern. ## 0) Objectives (non-negotiable) 1. Exactly **one active ``** pointer (or `NULL` during first-run). 2. **Block deletion** when it would leave **zero** ``. 3. If deleting the **active** item, **atomically re-point** to a deterministic **next** item **before** delete. 4. Enforce with **app logic** + **FK `RESTRICT`** (and `ON UPDATE CASCADE` if `ref` can change). --- ## 1) Schema / Migration (SQLite) ```sql -- __active_.sql PRAGMA foreign_keys = ON; -- Stable external key on (e.g., did/slug/uuid) -- ALTER TABLE ADD COLUMN ref TEXT UNIQUE NOT NULL; -- if missing CREATE TABLE IF NOT EXISTS active_ ( id INTEGER PRIMARY KEY CHECK (id = 1), activeRef TEXT UNIQUE, -- allow NULL on first run lastUpdated TEXT NOT NULL DEFAULT (datetime('now')), FOREIGN KEY (activeRef) REFERENCES (ref) ON UPDATE CASCADE ON DELETE RESTRICT ); -- Seed singleton row (idempotent) INSERT INTO active_ (id, activeRef) SELECT 1, NULL WHERE NOT EXISTS (SELECT 1 FROM active_ WHERE id = 1); ``` **Rules** * **Never** default `activeRef` to `''`—use `NULL` for "no selection yet". * Ensure `PRAGMA foreign_keys = ON` for **every connection**. --- ## 2) Data Access API (TypeScript) ```ts // Required DAL async function getAllRefs(): Promise { /* SELECT ref FROM ORDER BY created_at, ref */ } async function getRefById(id: number): Promise { /* SELECT ref FROM WHERE id=? */ } async function getActiveRef(): Promise { /* SELECT activeRef FROM active_ WHERE id=1 */ } async function setActiveRef(ref: string|null): Promise { /* UPDATE active_ SET activeRef=?, lastUpdated=datetime('now') WHERE id=1 */ } async function deleteById(id: number): Promise { /* DELETE FROM WHERE id=? */ } async function countModels(): Promise { /* SELECT COUNT(*) FROM */ } // Deterministic "next" function pickNextRef(all: string[], current?: string): string { const sorted = [...all].sort(); if (!current) return sorted[0]; const i = sorted.indexOf(current); return sorted[(i + 1) % sorted.length]; } ``` --- ## 3) Smart Delete (Atomic, Race-safe) ```ts async function smartDeleteModelById(id: number, notify: (m: string) => void) { await db.transaction(async trx => { const total = await countModels(); if (total <= 1) { notify("Cannot delete the last item. Keep at least one."); throw new Error("blocked:last-item"); } const refToDelete = await getRefById(id); const activeRef = await getActiveRef(); if (activeRef === refToDelete) { const all = (await getAllRefs()).filter(r => r !== refToDelete); const next = pickNextRef(all, refToDelete); await setActiveRef(next); notify(`Switched active to ${next} before deletion.`); } await deleteById(id); // RESTRICT prevents orphaning if we forgot to switch }); // Post-tx: emit events / refresh UI } ``` --- ## 4) Bootstrapping & Repair ```ts async function ensureActiveSelected() { const active = await getActiveRef(); const all = await getAllRefs(); if (active === null && all.length > 0) { await setActiveRef(pickNextRef(all)); // first stable choice } } ``` Invoke after migrations and after bulk imports. --- ## 5) Concurrency & Crash Safety * **Always** wrap "switch → delete" inside a **single transaction**. * Treat any FK violation as a **logic regression**; surface telemetry (`fk:restrict`). --- ## 6) Soft Deletes (if applicable) If `` uses `deleted_at`: * Replace `DELETE` with `UPDATE SET deleted_at = datetime('now') WHERE id=?`. * Add a **partial uniqueness** strategy for `ref`: * SQLite workaround: make `ref` unique globally and never reuse; or maintain a shadow `refs` ledger to prevent reuse. * Adjust `getAllRefs()` to filter `WHERE deleted_at IS NULL`. --- ## 7) Bulk Ops & Imports * For batch deletes: 1. Compute survivors. 2. If a batch would remove **all** survivors → **refuse**. 3. If the **active** is included, precompute a deterministic **new active** and set it **once** before deleting. * After imports, run `ensureActiveSelected()`. --- ## 8) Multi-Scope Actives (optional) To support **one active per workspace/tenant**: * Replace singleton with scoped pointer: ```sql CREATE TABLE active_ ( scope TEXT NOT NULL, -- e.g., workspace_id activeRef TEXT, lastUpdated TEXT NOT NULL DEFAULT (datetime('now')), PRIMARY KEY (scope), FOREIGN KEY (activeRef) REFERENCES (ref) ON UPDATE CASCADE ON DELETE RESTRICT ); ``` * All APIs gain `scope` parameter; transactions remain unchanged in spirit. --- ## 9) UX Contract * Delete confirmation must state: * Deleting the **active** item will **auto-switch**. * Deleting the **last** item is **not allowed**. * Keep list ordering aligned with `pickNextRef` strategy for predictability. --- ## 10) Observability * Log categories: * `blocked:last-item` * `fk:restrict` * `repair:auto-selected-active` * `active:switch:pre-delete` * Emit metrics counters; attach `` and (if used) `scope`. --- ## 11) Test Matrix (must pass) 1. **Non-active delete** (≥2): deleted; active unchanged. 2. **Active delete** (≥2): active switches deterministically, then delete succeeds. 3. **Last item delete** (==1): blocked with message. 4. **First-run**: 0 items → `activeRef` stays `NULL`; add first → `ensureActiveSelected()` selects it. 5. **Ref update** (if allowed): `activeRef` follows via `ON UPDATE CASCADE`. 6. **Soft delete** mode: filters respected; invariants preserved. 7. **Bulk delete** that includes active but not all: pre-switch then delete set. 8. **Foreign keys disabled** (fault injection): tests must fail to surface missing PRAGMA. --- ## 12) Rollout & Rollback * **Feature-flag** the new deletion path. * Migrations are **idempotent**; ship `ensureActiveSelected()` with them. * Keep a pre-migration backup for `` on first rollout. * Rollback leaves `active_` table harmlessly present. --- ## 13) Replace-Me Cheatsheet * `` → singular (e.g., `project`) * `` → plural table (e.g., `projects`) * `ref` → stable external key (`did` | `slug` | `uuid`) --- **Outcome:** You get **predictable UX**, **atomic state changes**, and **hard integrity guarantees** across single- or multi-scope actives, with clear tests and telemetry to keep it honest. --- ## TimeSafari Implementation Guide ### Current State Analysis (2025-01-27) **Status**: ✅ **FULLY COMPLIANT** - Active Pointer + Smart Deletion Pattern implementation complete. **Compliance Score**: 100% (6/6 components compliant) #### ✅ **What's Working** - **Smart Deletion Logic**: `IdentitySwitcherView.vue` implements atomic transaction-safe deletion - **Data Access API**: All required DAL methods exist in `PlatformServiceMixin.ts` - **Schema Structure**: `active_identity` table follows singleton pattern correctly - **Bootstrapping**: `$ensureActiveSelected()` method implemented - **Foreign Key Constraint**: ✅ **FIXED** - Now uses `ON DELETE RESTRICT` (Migration 005) - **Settings Cleanup**: ✅ **COMPLETED** - Orphaned records removed (Migration 006) #### ✅ **All Issues Resolved** - ✅ Foreign key constraint fixed to `ON DELETE RESTRICT` - ✅ Settings table cleaned up (orphaned records removed) ### Updated Implementation Plan **Note**: Smart deletion logic is already implemented correctly. Focus on fixing security issues and cleanup. #### 1) Critical Security Fix (Migration 005) **Fix Foreign Key Constraint:** ```sql -- Migration 005: Fix foreign key constraint to ON DELETE RESTRICT { name: "005_active_identity_constraint_fix", sql: ` PRAGMA foreign_keys = ON; -- Recreate table with ON DELETE RESTRICT constraint (SECURITY FIX) CREATE TABLE active_identity_new ( id INTEGER PRIMARY KEY CHECK (id = 1), activeDid TEXT REFERENCES accounts(did) ON DELETE RESTRICT, lastUpdated TEXT NOT NULL DEFAULT (datetime('now')) ); -- Copy existing data INSERT INTO active_identity_new (id, activeDid, lastUpdated) SELECT id, activeDid, lastUpdated FROM active_identity; -- Replace old table DROP TABLE active_identity; ALTER TABLE active_identity_new RENAME TO active_identity; -- Recreate indexes CREATE UNIQUE INDEX IF NOT EXISTS idx_active_identity_single_record ON active_identity(id); ` } ``` ### Updated Implementation Plan **Note**: Smart deletion logic is already implemented correctly. Migration 005 (security fix) completed successfully. #### ✅ **Phase 1: Critical Security Fix (COMPLETED)** - **Migration 005**: ✅ **COMPLETED** - Fixed foreign key constraint to `ON DELETE RESTRICT` - **Impact**: Prevents accidental account deletion - **Status**: ✅ **Successfully applied and tested** #### **Phase 2: Settings Cleanup (CURRENT)** - **Migration 006**: Remove orphaned settings records - **Impact**: Cleaner architecture, reduced confusion - **Risk**: LOW - Only removes obsolete data #### 3) Optional Future Enhancement (Migration 007) **Remove Legacy activeDid Column:** ```sql -- Migration 007: Remove activeDid column entirely (future task) { name: "007_remove_activeDid_column", sql: ` -- Remove the legacy activeDid column from settings table ALTER TABLE settings DROP COLUMN activeDid; ` } ``` ### Current Implementation Status #### ✅ **Already Implemented Correctly** - **Smart Deletion Logic**: `IdentitySwitcherView.vue` lines 285-315 - **Data Access API**: All methods exist in `PlatformServiceMixin.ts` - **Transaction Safety**: Uses `$withTransaction()` for atomicity - **Last Account Protection**: Blocks deletion when `total <= 1` - **Deterministic Selection**: `$pickNextAccountDid()` method - **Bootstrapping**: `$ensureActiveSelected()` method #### ❌ **Requires Immediate Fix** 1. **Foreign Key Constraint**: Change from `ON DELETE SET NULL` to `ON DELETE RESTRICT` 2. **Settings Cleanup**: Remove orphaned records with `accountDid=null` ### Implementation Priority #### **Phase 1: Critical Security Fix (IMMEDIATE)** - **Migration 005**: Fix foreign key constraint to `ON DELETE RESTRICT` - **Impact**: Prevents accidental account deletion - **Risk**: HIGH - Current implementation allows data loss #### **Phase 2: Settings Cleanup (HIGH PRIORITY)** - **Migration 006**: Remove orphaned settings records - **Impact**: Cleaner architecture, reduced confusion - **Risk**: LOW - Only removes obsolete data #### **Phase 3: Future Enhancement (OPTIONAL)** - **Migration 007**: Remove `activeDid` column from settings - **Impact**: Complete separation of concerns - **Risk**: LOW - Architectural cleanup #### **Phase 2: Settings Cleanup Implementation (Migration 006)** **Remove Orphaned Records:** ```sql -- Migration 006: Settings cleanup { name: "006_settings_cleanup", sql: ` -- Remove orphaned settings records (accountDid is null) DELETE FROM settings WHERE accountDid IS NULL; -- Clear any remaining activeDid values in settings UPDATE settings SET activeDid = NULL; ` } ``` ### Updated Compliance Assessment #### **Current Status**: ✅ **FULLY COMPLIANT** (100%) | Component | Status | Compliance | |-----------|--------|------------| | Smart Deletion Logic | ✅ Complete | 100% | | Data Access API | ✅ Complete | 100% | | Schema Structure | ✅ Complete | 100% | | Foreign Key Constraint | ✅ Fixed (`RESTRICT`) | 100% | | Settings Cleanup | ✅ Completed | 100% | | **Overall** | ✅ **Complete** | **100%** | ### Implementation Benefits **Current implementation already provides:** - ✅ **Atomic Operations**: Transaction-safe account deletion - ✅ **Last Account Protection**: Prevents deletion of final account - ✅ **Smart Switching**: Auto-switches active account before deletion - ✅ **Deterministic Behavior**: Predictable "next account" selection - ✅ **NULL Handling**: Proper empty state management **After fixes will add:** - ✅ **Data Integrity**: Foreign key constraints prevent orphaned references - ✅ **Clean Architecture**: Complete separation of identity vs. settings - ✅ **Production Safety**: No accidental account deletion possible ### Implementation Complete ✅ **All Required Steps Completed:** 1. ✅ **Migration 005**: Foreign key constraint fixed to `ON DELETE RESTRICT` 2. ✅ **Migration 006**: Settings cleanup completed (orphaned records removed) 3. ✅ **Testing**: All migrations executed successfully with no performance delays **Optional Future Enhancement:** - **Migration 007**: Remove `activeDid` column from settings table (architectural cleanup) The Active Pointer + Smart Deletion Pattern is now **fully implemented** with **100% compliance**.