Time Bomb Radar
Finds code that works today but crashes after the data gets old enough.
Time bombs are deferred operations that pass every test, every code review, every pattern matcher, then crash your app weeks or months after release. The trigger is data age + environment state, not code paths. They produce 1-star reviews from your most loyal users -- the ones who kept the app long enough for the timer to fire.
Origin: A production-class crash where SafeDeletionManager archived items for 30 days, then cascade-deleted them, triggering a SwiftData _FullFutureBackingData fatal error on unresolved iCloud .externalStorage faults. The bug was invisible during development because no test data was 30 days old. If shipped, every user would have crashed on day 31.
Quick commands
| Command | What it does |
|---|---|
/time-bomb-radar | Full audit across all 7 patterns |
/time-bomb-radar deferred-deletes | Pattern 1 only -- cascade deletes on aged data |
/time-bomb-radar cache-expiry | Pattern 2 only -- cache purge with model relationships |
/time-bomb-radar trial-expiry | Pattern 3 only -- subscription/trial expiry paths |
/time-bomb-radar background-tasks | Pattern 4 only -- accumulated background work |
/time-bomb-radar date-transitions | Pattern 5 only -- date-threshold state changes |
/time-bomb-radar scheduled-side-effects | Pattern 6 only -- notifications/reminders scheduled from aged data |
/time-bomb-radar cascade-live-refs | Pattern 7 only -- cascade delete with live child references |
--show-suppressed | Show findings suppressed by known-intentional entries (see § Intentional Suppression Flags) |
--accept-intentional | Mark current finding as known-intentional (see § Intentional Suppression Flags) |
Intentional Suppression Flags
Both flags wrap the protocol in radar-suite-core.md § Known-Intentional Suppression, which owns the canonical spec for .radar-suite/known-intentional.yaml (file format, fields, matching rules).
| Flag | Behavior |
|---|---|
--show-suppressed | After the scan completes, list every finding that was suppressed by a matching entry in .radar-suite/known-intentional.yaml this session. Output includes the finding (file:line + pattern), the suppression entry that matched, and the date the entry was added. Read-only — does not modify the suppression file. |
--accept-intentional | Interactive flow that appends a new entry to .radar-suite/known-intentional.yaml for the most recently presented finding in the current conversation. Asks via AskUserQuestion to confirm the file:line + pattern_fingerprint + reason text before writing. Requires the conversation to contain at least one finding emitted by this skill (refuses with "No recent finding to accept" otherwise). |
Future scans (this session or later) will silently skip findings matching accepted entries and increment the intentional_suppressed counter per § Pre-Scan Startup.
Shared Patterns
See radar-suite-core.md for: Tier System, Pipeline UX Enhancements, Table Format, Progress Banner, Issue Rating Tables, Handoff YAML schema, Known-Intentional Suppression, Pattern Reintroduction Detection, Experience-Level Output Rules, Session Persistence, short_title requirement.
Key concepts
These concepts appear throughout the 7 patterns. Understanding them makes the patterns easier to apply regardless of framework.
Lazy loading and faults
Most ORMs don't load related objects until you access them. A User object with 50 photos doesn't load those photos into memory just because you fetched the user. Instead, the photos are represented as faults -- lightweight placeholders that get filled in when you access them.
This is efficient for normal use. It becomes dangerous when:
- The real data is stored remotely (cloud sync, external storage) and hasn't been downloaded
- The object is being deleted and the ORM tries to resolve all its faults to track the cascade
- The app has been idle for weeks and the local cache has been evicted
In SwiftData: Faults are _FullFutureBackingData<T> objects. Accessing them triggers resolution. If resolution fails (data not available), it's a fatalError -- not a throwing error. You cannot catch it.
In Core Data: Faults are NSManagedObject subclasses with isFault == true. Accessing a property triggers resolution. If the store is unavailable, you get NSObjectInaccessibleException.
In Django/SQLAlchemy/ActiveRecord: Lazy-loaded relationships raise database errors if the connection is lost or the row was deleted. The ORM equivalent of "this object doesn't exist anymore."
In any ORM with cloud sync: The object exists in the schema but the data hasn't been synced to this device. The fault resolution goes to the network, which may be unavailable.
Cascade deletes
When you delete a parent object, the ORM can automatically delete its children. This is configured via delete rules (.cascade in SwiftData/Core Data, on_delete=CASCADE in Django, dependent: :destroy in Rails).
The problem: cascade deletion forces the ORM to find and visit every child before deleting them. If any child is a fault whose data isn't locally available, the visit fails.
Object-level cascade delete: ORM loads each child into memory, snapshots it for change tracking, then deletes it. Triggers fault resolution. Dangerous on aged data.
Batch/SQL-level delete: ORM issues DELETE FROM children WHERE parent_id = ? directly. Never loads objects. Never triggers faults. Safe on aged data.
External storage
Some ORMs store large binary data (photos, PDFs, audio) outside the main database file. SwiftData uses .externalStorage to put Data properties on disk instead of inline in SQLite. Core Data has "Allows External Storage" in the model editor. Other frameworks use file references.
External storage is the highest-risk target for time bombs because:
- The file may not be downloaded from the cloud yet
- The file may have been evicted from the local cache
- The ORM may not distinguish between "file not downloaded yet" and "file doesn't exist"
Why testing misses these
- No test data is 30 days old
- Simulators/emulators have perfect local data (no cloud sync delays)
- Unit tests use in-memory stores (no external storage faults)
- CI runs on fresh environments every time
- The developer's device has good Wi-Fi and fully synced data
To catch a time bomb manually, you'd need to: create data, archive it, set your device clock forward 30-90 days, disconnect from the network, and relaunch. Nobody does this.
Skill Introduction (MANDATORY — run before scanning)
This section replaces radar-suite-core.md § Session Setup for the time-bomb-radar entry point. Do NOT also run core's 4-question Session Setup — its questions are consolidated below. On first invocation, ask all setup questions in a single AskUserQuestion call:
Question 1: "What's your experience level with Swift/SwiftUI?"
- Beginner — New to Swift. Plain language, analogies, define terms on first use.
- Intermediate — Comfortable with SwiftUI basics. Standard terms, explain non-obvious patterns.
- Experienced (Recommended) — Fluent with SwiftUI. Concise findings, no definitions.
- Senior/Expert — Deep expertise. Terse, file:line only, skip explanations.
Question 2: "Table format?"
- Full tables (Recommended) — full Issue Rating Tables
- Compact tables — 3-column with details below
Question 3: "Would you like a brief explanation of what this skill does?"
- No, let's go (Recommended) — Skip explanation, proceed to scan.
- Yes, explain it — Show one of the explanations below adapted to experience level, then proceed.
Store as: USER_EXPERIENCE, TABLE_FORMAT. Apply to ALL output for the session, per radar-suite-core.md § Experience-Level Output Rules. Also persist to `.radar-suite/session-prefs.ya