name: ui-path-radar description: 'UI path tracer for SwiftUI/UIKit apps. 5-layer audit with 34 issue categories and 19 automated checks: discover entry points, trace flows, detect dead ends and broken promises, evaluate UX impact, verify data wiring. Supports targeted trace, diff against previous audits, and handoff to planning skills. Triggers: "trace UI paths", "find dead ends", "/ui-path-radar".' version: 2.2.0 # orphan feature detection (was 2.1.0) author: Terry Nyberg license: MIT allowed-tools: [Read, Grep, Glob, Bash, Edit, Write, AskUserQuestion] inherits: radar-suite-core.md metadata: tier: execution category: analysis
UI Path Radar
Quick Ref: 5-layer UI path audit: discover entry points → trace flows → detect issues → evaluate UX → verify data wiring. Output:
.ui-path-radar/in project root.
You are performing a systematic UI path audit on this SwiftUI application.
Required output: Every finding MUST include Urgency, Risk, ROI, and Blast Radius ratings using the Issue Rating Table format. Do not omit these ratings.
Quick Commands
| Command | Description |
|---|---|
/ui-path-radar |
Full 5-layer audit |
/ui-path-radar layer1 |
Discovery only — find all entry points |
/ui-path-radar layer2 |
Trace — trace critical paths |
/ui-path-radar layer3 |
Issues — detect problems across codebase |
/ui-path-radar layer4 |
Evaluate — assess user impact |
/ui-path-radar layer5 |
Data wiring — verify real data usage |
/ui-path-radar trace "A → B → C" |
Trace a specific user flow path |
/ui-path-radar diff |
Compare current findings against previous audit |
/ui-path-radar fix |
Generate fixes for found issues |
/ui-path-radar status |
Show audit progress and remaining issues |
--show-suppressed |
Show findings suppressed by known-intentional entries |
--accept-intentional |
Mark current finding as known-intentional (not a bug) |
Overview
UI Path Radar uses a 5-layer approach:
| Layer | Purpose | Est. Time (small / large codebase) | Output |
|---|---|---|---|
| Layer 1 | Pattern Discovery — Find all UI entry points | ~1-2 min / ~3-5 min | Entry point inventory |
| Layer 2 | Flow Tracing — Trace critical paths in depth | ~2-3 min / ~5-8 min | Detailed flow traces |
| Layer 3 | Issue Detection — Categorize issues across codebase | ~2-4 min / ~5-10 min | Issue catalog |
| Layer 4 | Semantic Evaluation — Evaluate from user perspective | ~1-2 min / ~3-5 min | UX impact analysis |
| Layer 5 | Data Wiring — Verify features use real data | ~2-4 min / ~5-10 min | Data integrity report |
Codebase size guide: Small = <200 files, Large = 500+ files. Estimate from
find Sources -name "*.swift" | wc -l.
Issue Categories
Each category maps to a default axis (see skills/radar-suite-axis-classification/SKILL.md for the framework). The default axis may be overridden by the verification checklist (see "Axis Classification Protocol" section below) — e.g., a "Dead End" finding whose branch is unreachable from any production call site gets reclassified from axis_1_bug to axis_3_dead_code by the reachability trace.
| Category | Severity | Default Axis | Description |
|---|---|---|---|
| Dead End | 🔴 CRITICAL | axis_1_bug (→ axis_3_dead_code if unreachable) | Entry point leads nowhere |
| Wrong Destination | 🔴 CRITICAL | axis_1_bug | Entry point leads to wrong place |
| Mock Data | 🔴 CRITICAL | axis_1_bug | Feature shows fabricated data when real data exists |
| Destructive Without Confirmation | 🔴 CRITICAL | axis_1_bug | Delete/clear happens immediately without confirmation dialog |
| Silent State Reset | 🔴 CRITICAL | axis_1_bug | In-progress work lost when navigating away and back (form clears, selections lost) |
| Incomplete Navigation | 🟡 HIGH | axis_1_bug | User must scroll/search after landing |
| Missing Auto-Activation | 🟡 HIGH | axis_1_bug | Expected mode/state not set |
| Unwired Data | 🟡 HIGH | axis_1_bug (→ axis_3_smelly if model field has no read/write sites) | Model data exists but feature ignores it |
| Platform Parity Gap | 🟡 HIGH | axis_1_bug | Feature works on one platform, broken on another |
| Promise-Scope Mismatch | 🟡 HIGH | axis_1_bug | Specific CTA opens generic/broad destination |
| Buried Primary Action | 🟡 HIGH | axis_1_bug | Primary button hidden below scroll fold |
| Dismiss Trap | 🟡 HIGH | axis_1_bug | Only visible action is Cancel/back, no forward path |
| Context Dropping | 🟡 HIGH | axis_1_bug | Navigation path loses item context between platforms or via notifications |
| Notification Nav Fragility | 🟡 HIGH | axis_1_bug | Untyped NotificationCenter dict used for navigation context |
| Sheet Presentation Asymmetry | 🟡 HIGH | axis_2_scatter (→ axis_1_bug if only one platform works) | Different presentation mechanisms per platform for same feature |
| Empty State Missing | 🟡 HIGH | axis_1_bug (→ axis_3_dead_code if empty case unreachable) | No guidance when list/view is empty — users think app is broken |
| Error Recovery Missing | 🟡 HIGH | axis_1_bug | Error displayed but no retry button or recovery path |
| Keyboard Obscures Input | 🟡 HIGH | axis_1_bug | Text field covered by keyboard with no scroll adjustment (iOS) |
| Permission Denied Dead End | 🟡 HIGH | axis_1_bug | Permission denied but no explanation or path to Settings |
| Modal Stacking | 🟡 HIGH | axis_1_bug | Multiple sheets/alerts open on top of each other |
| Navigation Container Mismatch | 🟡 HIGH | axis_1_bug | selectedSection value not a valid tag in current TabView/sidebar |
| Two-Step Flow | 🟢 MEDIUM | axis_1_bug | Intermediate selection required |
| Missing Feedback | 🟢 MEDIUM | axis_1_bug | No confirmation of success |
| Gesture-Only Action | 🟢 MEDIUM | axis_1_bug | Feature only accessible via swipe/long-press |
| Loading State Trap | 🟢 MEDIUM | axis_1_bug | Spinner with no cancel/timeout/escape |
| Stale Navigation Context | 🟢 MEDIUM | axis_2_scatter (→ axis_1_bug if user observes stale data) | Cached context with no clearing/validation mechanism |
| Phantom Touch Target | 🟢 MEDIUM | axis_1_bug | Visual element looks tappable but isn't (icon without action, card without nav) |
| Race Condition UX | 🟢 MEDIUM | axis_1_bug | User can trigger conflicting operations simultaneously (double-tap, edit while sync) |
| Invisible Selection | 🟢 MEDIUM | axis_1_bug | Item is selected/active but visual indicator missing or too subtle |
| Inconsistent Pattern | ⚪ LOW | axis_2_scatter | Same feature accessed differently |
| Orphaned Code | ⚪ LOW | axis_3_dead_code (if unreachable) or axis_3_smelly (if reachable but unjustified) | Feature exists but no entry point |
| Command-Palette-Only Feature | 🟡 HIGH | axis_1_bug | Feature reachable only via command palette (QuickFind/Go To), no visible UI entry point |
| Deeply Buried Feature | 🟡 HIGH | axis_1_bug | User-facing feature requires 4+ taps from nearest tab bar item |
| Double-Nested Navigation | ⚪ LOW | axis_2_scatter | NavigationStack inside NavigationStack causing doubled nav bars |
Axis Classification Protocol (MANDATORY — before emitting any finding)
Every finding must be classified on the 3-axis framework and pass the schema gate in radar-suite-core.md before emission. The protocol:
Assign default axis from the table above based on the issue category.
Run required verification checks:
- Reachability trace (MANDATORY for Dead End, Empty State Missing, Orphaned Code) — walk upstream from the flagged branch at least 2 call-site levels. If no production call site reaches it, RECLASSIFY to
axis_3_dead_code. - Whole-file scan (MANDATORY for "missing handler" categories: Empty State Missing, Error Recovery Missing, Missing Feedback) — read the ENTIRE file (not just the flagged region) for handlers elsewhere. If found, RECLASSIFY to
axis_2_scatter. - Branch enumeration (MANDATORY for Platform Parity Gap, Sheet Presentation Asymmetry) — read BOTH sides of every
#if os(iOS)/#elseblock before claiming platform-broken. Stuffolio has 266 such blocks; dropping the#elsebranch is the #1 false-positive source. - Pattern citation lookup (MANDATORY for every finding, regardless of category) — grep the audited codebase for a similar pattern shape. Cite by file:line in the
better_approachfield. A finding without this citation is REJECTED.
- Reachability trace (MANDATORY for Dead End, Empty State Missing, Orphaned Code) — walk upstream from the flagged branch at least 2 call-site levels. If no production call site reaches it, RECLASSIFY to
Write coaching fields. Populate
current_approach,suggested_fix,better_approach(with citation),better_approach_tradeoffs— all mandatory. Load coaching examples via.radar-suite/project.yamlcoaching_examplesarray.Validate against schema gate. Run the gate checks from
radar-suite-core.md. If any mandatory field is missing, either fix the finding or downgrade confidence topossibleand incrementrejected_no_citationin the handoff.Write the finding to the handoff YAML with full axis + coaching fields.
Reclassification logging: When the verification checklist reclassifies a finding's axis (e.g., "Dead End" → axis_3_dead_code via reachability trace), log the reclassification in the finding's verification_log so capstone and the user can see the framework caught a would-be false positive:
verification_log:
- check: reachability_trace
result: "no production call site found; reclassified from axis_1_bug (Dead End) to axis_3_dead_code"
Axis summary block. At the end of the handoff, include:
axis_summary:
axis_1_bug: [count]
axis_2_scatter: [count]
axis_3_dead_code: [count]
axis_3_smelly: [count]
rejected_no_citation: [count]
Design Principles
1. Honor the Promise
When a button/card says "Do X", tapping it should DO X. Not "go somewhere you might find X."
2. Context-Aware Shortcuts
If user's context implies a specific item, skip pickers.
3. State Preservation
When navigating to a feature, set up the expected state.
4. Consistent Access Patterns
Same feature should be accessed the same way everywhere.
5. Data Integrity
If the app tracks data relevant to a feature, the feature must use it. Never show mock/hardcoded data when real user data exists. Never ignore model relationships that would improve decisions.
6. Primary Action Visibility
The primary action must be visible without scrolling after the user completes the key interaction. Pin Save/Continue/Done buttons outside ScrollView or in toolbar. Never bury them below tall content.
7. Escape Hatch
Every view must have a visible way to go forward OR back. Cancel alone is not enough after user completes a step.
8. Gesture Discoverability
Every action available via gesture (swipe, long-press) should also be accessible via a visible button or menu.
Freshness
Base all findings on current source code only. Do not read or reference
files in .agents/, scratch/, or prior audit reports. Ignore cached
findings from auto-memory or previous sessions. Every finding must come
from scanning the actual codebase as it exists now.
Before Starting
All setup questions were captured during the Skill Introduction call below (USER_EXPERIENCE, FIX_MODE, DELIVERY, PRESENCE_MODE). Do NOT re-ask them here. Show the one-line settings reminder from § Skill Introduction and proceed to the audit.
If any of the four variables is missing for some reason (e.g., session-prefs file deleted mid-session), re-run the full Skill Introduction call before continuing — never partially re-ask, since the questions are interdependent (Question 4 overrides Question 1 — see Hands-Free Mode below).
Permission Modes
Normal Mode
- Read any file without asking.
- Edit files only if user chose auto-fix and the fix is isolated to the audited flow.
- Build and run tests without asking.
- If a fix breaks the build, restore the original code and document as "Documented."
Hands-Free Mode
Guarantees no blocking prompts. Only uses: Read, Grep, Glob.
Does NOT use: Bash, Edit, Write, AskUserQuestion.
Precedence rules (load-bearing — Hands-Free wins all ties):
- Hands-Free overrides
FIX_MODE. Even if Question 2 was answeredAuto-fix safe itemsorBatch mode, no fixes are applied in Hands-Free mode. All findings are emitted withStatus: Deferred (hands-free)in the Issue Rating table. The Fix Plan is still produced (so the user can act on it on return), but no Wave 1-4 fix application runs. - Hands-Free suppresses the next-wave
AskUserQuestion. The "CRITICAL — BLOCKING requirement" rule under § Progress Banner applies in Normal and Pre-Approved modes only. In Hands-Free mode, the progress banner still prints, but theAskUserQuestioncall is omitted and replaced by the completion message below. - Hands-Free defers all write-tool layer steps. The following steps require Edit/Write and are deferred until the user returns: writing
.ui-path-radar/layer3-results.yaml,.ui-path-radar/layer5-data-wiring.yaml,.ui-path-radar/canaries.yaml,.ui-path-radar/handoff.yaml,.agents/ui-audit/ui-path-radar-handoff.yaml, and.radar-suite/ledger.yaml. To resolve: in Hands-Free mode, the skill emits these YAML files inline in the conversation as fenced YAML blocks so the user can persist them on return. Inline emission does not count as a write. - Hands-Free covers all 5 layers, not just 1-4. Layer 5 step 7 (write
layer5-data-wiring.yaml) and Layer 3's cross-layer verification table edits both fall under rule 3 above — emitted inline rather than written to disk.
When complete:
⏱ Hands-free audit complete through Layer [N] of 5: [plain description].
Layers requiring write access: [list]
Findings deferred: [count] (no fixes applied — Hands-Free mode)
Handoff YAML + ledger entries: emitted inline above; copy to .ui-path-radar/, .agents/ui-audit/, and .radar-suite/ when you return
Reply to continue with supervised steps.
Pre-Approved Mode
Full speed, no restrictions. Assumes you've set up permissions.
Permission Setup (for unattended runs)
# Already safe by default (no setup needed):
Read, Grep, Glob — always auto-approved
# Add these for unattended Bash scans:
Bash(find:*)
Bash(wc:*)
Bash(stat:*)
Do NOT auto-approve (keep prompted — they modify state):
Edit, Write — file modifications
Bash(rm:*), Bash(git:*) — destructive operations
Tip: If you frequently run audit-only layers (1-4), the Hands-free mode eliminates permission prompts entirely without changing any settings.
Context Budget
If context is running low, prioritize in this order:
- Finish the current phase
- Emit findings for what you've audited so far
- Skip remaining unaudited flows
Never start auditing a new flow you can't finish.
Experience-Level Adaptation
Adjust ALL output based on the user's experience level:
Beginner
- Use plain language with real-world analogies.
- Define technical terms on first use in parentheses.
- Explain flags/categories and why they matter.
- Include file context: "DashboardView.swift (the main home screen) line 118"
- Explain the "why" behind each suggestion.
- Use compact 4-column table format.
Intermediate
- Use standard SwiftUI terminology without defining basics.
- Explain non-obvious patterns.
- Standard file:line format.
- Full 9-column format with brief finding descriptions.
Experienced (default)
- Concise findings. No definitions or explanations of standard patterns.
- Recommendations: what to fix, where.
- Full 9-column format, terse findings.
Senior/Expert
- Minimal findings text. No prose between tables.
- File:line + one-line fix description only.
- Skip: progress explanations, design principle citations, category definitions.
- Full 9-column format, maximally compressed.
Enforcement Rule
At the start of each layer, silently check: "Am I writing at the selected experience level?" Do NOT drift toward "Experienced" as a default.
Execution Instructions
Skill Introduction (MANDATORY — run before anything else)
This section replaces radar-suite-core.md § Session Setup for the ui-path-radar entry point. Do NOT also run core's 4-question Session Setup — its questions are consolidated below. On first invocation, ask all four setup questions in a single AskUserQuestion call. The "Before Starting" section above reuses these answers and never re-asks.
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: "How should fixes be handled?"
- Auto-fix safe items (Recommended) — Apply isolated, low-blast-radius fixes automatically. Present cross-cutting fixes and design decisions for approval first.
- Review first — Present all findings with ratings, then ask before making any changes. Fixes still happen — you just approve each wave first.
- Batch mode — Approve all fixes in each wave at once.
IMPORTANT: All three modes lead to fixes. "Review first" means the user sees the plan before code changes — it does NOT mean "skip fixes and jump to handoff." After presenting findings, ALWAYS offer to fix them regardless of which mode was selected. (Exception: Hands-Free mode overrides this — see Question 4.)
Question 3: "How should results be delivered?"
- Display only (Recommended) — Show findings in the conversation. No file written.
- Report only — Write findings to
.ui-path-radar/[DATE]-audit.md. Minimal conversation output. Before writing, per Artifact Lifecycle (Class 3) inradar-suite-core.md, archive any existing.ui-path-radar/*-audit.mdto.ui-path-radar/archive/superseded/. - Display and report — Show findings in the conversation AND write to file.
Question 4: "Will you be stepping away during the audit?"
- I'll be here (Recommended) — Normal mode. Permission prompts may appear for writes/edits.
- Hands-Free (walk away safe) — Read-only tools (Read, Grep, Glob) only. No Bash, no Edit, no Write, no AskUserQuestion. Hands-Free overrides Question 2: all fixes are deferred regardless of
FIX_MODE. The progress banner still prints, but theAskUserQuestionnext-wave prompt is suppressed; the skill emits the "audit complete through Layer N" completion message instead (see Hands-Free Mode below). - Pre-Approved — You have already configured Claude Code permissions. Run at full speed.
Store as: USER_EXPERIENCE, FIX_MODE, DELIVERY, PRESENCE_MODE. Apply to ALL output for the session, per radar-suite-core.md § Experience-Level Output Rules. Also persist to .radar-suite/session-prefs.yaml per radar-suite-core.md § Session Persistence.
Question 5 (optional follow-up): "Would you like a brief explanation of what this skill does?"
- No, let's go (Recommended) — Skip explanation, proceed to audit.
- Yes, explain it — Show one of the explanations below adapted to experience level, then proceed.
Experience-adapted explanations:
Beginner: "UI Path Radar checks every button, link, and menu item in your app to make sure they actually work. Think of it like walking through every door in a building to verify none are locked, lead nowhere, or open to the wrong room. It finds 'dead ends' (buttons that do nothing), 'broken promises' (a button says 'Export PDF' but opens the wrong screen), and missing features. It runs in 5 layers, each going deeper — from finding all the buttons, to tracing what happens when you tap them, to checking if the data behind them is real."
Intermediate: "UI Path Radar systematically audits all UI entry points (sheets, navigation links, toolbar buttons, deep links, notifications) across your SwiftUI app. It traces user flows end-to-end, flags dead ends, promise-scope mismatches, platform gaps, and orphaned state. Five layers: discovery → flow tracing → issue detection → UX evaluation → data wiring verification."
Experienced: "5-layer UI path audit across 34 issue categories and 19 automated checks: entry point discovery, flow tracing, issue detection, semantic UX evaluation, and data wiring verification. Outputs issue rating tables with fix plans."
Senior/Expert: "Entry point → flow trace → issue scan → UX eval → data wiring. 34 categories, 19 checks. Rating tables + fix plans."
User impact explanations: Can be toggled at any time with --explain / --no-explain. When enabled, each finding gets a 3-line companion explanation (what's wrong, fix, user experience before/after). See radar-suite-core.md for format and rules. Store as EXPLAIN_FINDINGS (default: false).
Experience-level auto-apply (ui-path-radar local): If USER_EXPERIENCE = Beginner, auto-set EXPLAIN_FINDINGS = true and default sort to impact. If Senior/Expert, default sort to effort. Apply all output rules from radar-suite-core.md § Experience-Level Output Rules.
Shared Patterns
See radar-suite-core.md for: Tier System, Pipeline UX Enhancements, Table Format, Plain Language Communication, Work Receipts, Contradiction Detection, Finding Classification, Audit Methodology, Context Exhaustion, Progress Banner, Issue Rating Tables, Handoff YAML schema, Known-Intentional Suppression, Pattern Reintroduction Detection, Experience-Level Output Rules, Implementation Sort Algorithm, short_title requirement.
Pre-Scan Startup (MANDATORY — before any layer scan)
Known-intentional suppression: Run the protocol in
radar-suite-core.md § Known-Intentional Suppression. Core owns this — do not restate the steps here.Pattern reintroduction detection: Run the protocol in
radar-suite-core.md § Pattern Reintroduction Detection. Core owns this.
When invoked, perform the audit:
If no arguments or "full":
Before starting, print:
Full Audit: 5 layers — estimated total: ~10-30 min depending on codebase size
Layer 1: Find all entry points → Layer 2: Trace how users navigate → Layer 3: Detect issues → Layer 4: Evaluate user impact → Layer 5: Verify data wiring
Run all 5 layers sequentially, outputting findings to .ui-path-radar/ in the project root.
Between layers, print: ✓ Layer [N] of 5 complete: [plain description] — starting Layer [N+1]: [plain description]
If "layer1" or "discovery": enumerate-required
Before starting, count Swift files and print an estimate:
Layer 1: Discovery — scanning [N] Swift files
Estimated time: ~[1-2 min for <200 files / 3-5 min for 500+ files]
Progress will be shown after each tier completes.
Scan in 3 tiers, from top-level down. After completing each tier, print a progress line:
Tier 1: Top-Level Structure
- Find the app's navigation skeleton:
TabView,NavigationSplitView, sidebar sections - For each top-level destination, identify the view file
- Scan for sheet routing enums:
grep -r "enum.*Sheet\|enum.*SheetType" Sources/ - Scan for navigation state:
grep -r "selectedSection\|selectedTab\|activeSheet" Sources/
After Tier 1, print: Layer 1: ✓ Tier 1 Structure (1/3) — found [N] tabs, [N] sidebar items, [N] sheet enums
Tier 2: Entry Point Patterns
Scan for these patterns across ALL source files:
| Pattern | Search | Priority |
|---|---|---|
| Sheets | .sheet(, .fullScreenCover( |
HIGH — primary feature access |
| Navigation | NavigationLink(, .navigationDestination( |
HIGH — screen transitions |
| Tab views | TabView, .tabItem( |
HIGH — top-level entry points |
| Buttons with state | Button(.*{ near showing or activeSheet or selected |
HIGH — action triggers |
| Deep links | onOpenURL, DeepLinkRouter, URL scheme handlers |
HIGH — external entry points |
| Notification nav | .onReceive(NotificationCenter, NotificationCenter.default near navigation/sheet state |
HIGH — invisible triggers |
| Spotlight/Handoff | onContinueUserActivity, CSSearchableItemActionType |
HIGH — system search entry |
| MenuBarExtra / CommandGroup | MenuBarExtra, CommandGroup, .commands { |
HIGH — macOS app menu commands |
| File operations | .fileImporter, .fileExporter, PhotosPicker |
MEDIUM — data entry paths |
| Context menus | .contextMenu { |
MEDIUM — long-press actions |
| Swipe actions | .swipeActions |
MEDIUM — list row shortcuts |
| Toolbars | .toolbar { with Button |
MEDIUM — persistent actions |
| Keyboard shortcuts | .keyboardShortcut |
MEDIUM — power user entry |
| Promotion cards | PromotionCard, CompactPromotionCard, dismissable feature cards |
MEDIUM — conditional entry |
| Confirmation dialogs | .confirmationDialog(, .alert( |
LOW — exit gates, not entry points |
Zero-match handling: When a scanned pattern returns 0 results, do NOT silently skip it. Report: "<PatternName>: 0 matches — pattern not present in codebase". This prevents confusion where a category appears "clean" when it was never scanned.
After Tier 2, print: Layer 1: ✓ Tier 2 Patterns (2/3) — found [N] entry points across [N] patterns
Tier 3: Container View Enumeration
For views that are feature hubs (tools, settings, reports, dashboards), don't just log the hub as one entry point — enumerate each actionable card/row inside it as a sub-entry-point. These hubs often contain 10-20+ features that the Tier 2 scan misses because they're wired through enum-based routing, not direct .sheet() modifiers.
Also identify the primary detail view (the view users spend the most time in) and audit its sheet/action surface separately — it often has the largest entry point count.
After Tier 3, print: Layer 1: ✓ Tier 3 Containers (3/3) — enumerated [N] hub views, [N] sub-entry-points added
Catalog Rules
For each entry point found:
| Field | Description |
|---|---|
| Label | What the user sees (button text, card title, menu item) |
| Location | File and line where the trigger lives |
| Action type | Sheet, navigation, state change, deep link, notification, keyboard shortcut |
| Destination | What view/screen opens |
| Depth | Hierarchy level: L0 (tab/sidebar) → L1 (section view) → L2 (detail/sheet) → L3 (sub-sheet) |
| Condition | When is this entry point visible? (e.g., "only if items exist", "after dismissal: never") |
| Flags | Suspicious patterns (see below) |
Flags to apply during discovery:
| Flag | Description |
|---|---|
dead_end |
Trigger exists but destination is missing or broken |
promise_mismatch |
Specific label opens generic/broad destination |
incomplete_nav |
Lands on section top, not the specific feature |
missing_state |
Navigation without setting up expected mode/state |
two_step |
Requires intermediate picker before reaching feature |
no_feedback |
Action completes without confirmation |
orphaned |
View exists but has no entry point |
platform_gap |
Works on one platform, broken on another |
alias |
Entry point mirrors another entry point's destination. Group aliases separately — they confirm redundancy, not new paths |
conditional |
Entry point disappears after user action. Scan for: @AppStorage keys containing hasDismissed, hasUsed, hasScanned, hasSeen; if !settings.someFlag guards around UI elements; .onboardingComplete checks. |
invisible |
Entry point triggered by notification, deep link, or Spotlight — not visible in the UI hierarchy |
callback_wiring |
View takes an onComplete, onItemSelected, or similar closure — trace what the closure does |
container_mismatch |
Navigation sets selectedSection to a value that may not be a valid tag in the current container |
De-duplication Rules
- Context menus repeated on identical UI elements (e.g., same menu on product/receipt/nameplate photos): catalog once, note multiplier (e.g., "×3 photo types")
- QuickFind / Spotlight / Siri Shortcuts that mirror other entry points: tag as
alias, group in a separate "Meta-Entry-Points" section at the end - Confirmation dialogs: list separately as "Exit Gates" — they guard destructive actions, not open features
Discovery Output
Group the table by hierarchy level, not flat:
## L0: Top-Level Navigation (tabs, sidebar)
| # | Label | Location | Action | Destination | Flags |
## L1: Section Views (dashboard, tools, settings, detail)
| # | Label | Location | Action | Destination | Flags |
## L2: Feature Sheets & Sub-Navigation
| # | Label | Location | Action | Destination | Flags |
## Meta-Entry-Points (QuickFind, Spotlight, Deep Links, Keyboard Shortcuts)
| # | Label | Location | Action | Mirrors # | Flags |
## Exit Gates (Confirmation Dialogs)
| # | Label | Location | Guards |
After the tables, list:
- Total entry points found (primary + aliases)
- Count by flag type
- Count by depth level
- Recommended flows to audit in Layer 2 (flagged entries first, deepest paths second)
If "layer2" or "trace" (no path argument): enumerate-required
Before starting, print:
Layer 2: Flow Tracing — [N] flagged entry points to trace
Estimated time: ~[2-3 min for <5 flows / 5-8 min for 10+ flows]
- Read flagged entry points from Layer 1
- For each flagged entry point, trace the complete user journey. After each flow, print:
Layer 2: ✓ Flow [N]/[total] — "[flow name]" - Document in
layer2-traces/flow-XXX.yaml - Identify gaps between expected and actual journeys
IMPORTANT — Trace into callbacks and closures:
When tracing a flow, do NOT stop at the view presentation. If a sheet presents a view that takes an onComplete, onItemSelected, or similar closure, trace what that closure does. An empty closure { } or unimplemented handler is a dead end.
IMPORTANT — Verify navigation container constraints:
At each navigation step, if selectedSection = .X is set:
- Check whether
.Xis a valid tag in the current navigation container (TabView tags on iPhone, sidebar items on iPad/macOS) - If it's not a valid tag, the assignment is a no-op — flag as dead end
- Different platforms may have different valid tags — check both
Check both pre-action AND post-action feedback:
- Pre-action: Is there a confirmation dialog before destructive operations?
- Post-action: Is there a toast, haptic, or visual confirmation after non-destructive actions (save, duplicate, archive)?
Flow Trace Template
flow_trace:
id: "flow-001"
name: "Feature Name from Entry Point"
entry_point: "entry-point-id"
steps:
- step: 1
action: "User taps [button/card/menu item]"
file: "SourceFile.swift:78"
code: "selectedSection = .tools"
issues: []
- step: 2
action: "App navigates to [destination]"
file: "DestinationView.swift"
result: "[what user sees]"
issues:
- category: "incomplete_navigation"
detail: "User must scroll to find feature"
expected_journey:
- "[step 1]"
- "[step 2]"
actual_journey:
- "[step 1]"
- "[extra step]"
- "[step 2]"
gap_analysis:
type: "[issue category]"
extra_steps: 2
user_confusion_risk: "medium"
If "trace" with path argument (e.g., trace "Dashboard → Add Item → Photo → Save"):
Targeted flow trace — trace a specific user journey described in natural language:
- Parse the path description into discrete steps (split on
→,->, or,) - For each step, identify the SwiftUI view, button, or action that triggers it
- Trace the complete code path step by step (file, line, state changes, view transitions)
- At each step, check for issues (Buried Primary Action, Dismiss Trap, Promise-Scope Mismatch, Missing Feedback, Callback Wiring)
- Document the trace and any issues found
- Output: Issue Rating Table for any findings, plus the step-by-step trace
If "layer3" or "issues": mixed
Before starting, print:
Layer 3: Issue Detection — scanning [N] entry points for issues
Estimated time: ~[2-4 min for <100 entries / 5-10 min for 200+]
CRITICAL: Do NOT delegate Layer 3 checks to Explore subagents. Run each check directly using Grep/Glob/Read tools against the full codebase. Subagent sampling causes false negatives — a previous audit checked 1 of 7 list views for gesture-only actions and concluded "all clear," missing 6 findings.
Step 0 — Cross-layer verification (MANDATORY): Before scanning for new issues, re-verify ALL flagged findings from Layer 1 and Layer 2. For each:
- Check whether the flag still holds when you look at extension files (
+Sections.swift,+Actions.swift, etc.) and ViewModel bindings (@Bindable,Binding<Bool>parameters) - If a finding was based on "this @State var is never set to true" — also check if it's passed as a
$bindingto a ViewModel method or child view that sets.wrappedValue = true - Retract false positives explicitly:
~~#N — [original finding]~~ RETRACTED: [reason] - Print:
Layer 3: ✓ Cross-layer verification — [N] confirmed, [N] retracted
Then proceed with automated checks 1-18 below.
After each check, print: Layer 3: ✓ Check [N] [check name] ([N]/19) — [N] findings so far
Numbering note: Checks 1-10, 10b, 11-18 = 19 total. Check 10b ("Notification Type-Safety") was added in v2.0 as a strict sub-case of Check 10. Rather than renumber 11-18 (which would invalidate every external reference to "Check 11" etc.), 10b is kept as a suffix and counted in the
/19denominator. When printing progress for Check 10b, use the literal token10b:Layer 3: ✓ Check 10b Notification Type-Safety (11/19) — N findings so far.
Automated Check 1: Sheet Case Coverage
What it detects: Enum cases defined in a sheet routing enum (e.g., DashboardSheetType) that have no corresponding handler in the sheetContent(for:) switch — the case exists but presenting it shows nothing or crashes.
How to detect:
# Step 1: Find all sheet enum cases
grep -n "case " <SheetEnumFile>.swift | grep -v "//"
# Step 2: Find all handled cases in sheet content switch
grep -n "case \." <SheetContentFile>.swift
# Step 3: Compare — any cases in Step 1 without a handler in Step 2?
Also check the reverse: handlers that reference cases no longer in the enum (dead handlers).
Safe patterns (do NOT flag):
- Cases with a
default:handler that provides meaningful content - Deprecated cases with explicit comments
Severity: 🔴 CRITICAL (sheet opens blank or crashes)
Automated Check 2: Orphaned Views
What it detects: View structs defined (struct XxxView: View) but never instantiated anywhere outside of #Preview blocks.
How to detect:
# Step 1: Find all View struct definitions
grep -rn "struct.*: View" Sources/ --include="*.swift" | grep -v "Preview\|test\|Test"
# Step 2: For each view name, search for instantiation
# A view is "used" if its name appears as `ViewName(` somewhere outside its own file and outside #Preview
grep -rn "ViewName(" Sources/ --include="*.swift" | grep -v "#Preview\|Preview {"
Also check for orphaned modifiers: Structs conforming to ViewModifier that are defined but .modifier() is never called with them, and no .xxxModifier() extension applies them.
Safe patterns (do NOT flag):
- Views used only in previews if explicitly marked as preview-only
- Views instantiated dynamically via reflection or factory patterns
- Views referenced in
@ViewBuilderresults
Severity: ⚪ LOW (dead code, no user impact — but may indicate ~hundreds of lines of wasted code)
Automated Check 3: Promise-Scope Mismatch
What it detects: A specific-sounding CTA ("Track AppleCare+", "Set Warranty") that opens a generic/overly-broad destination. The user expects a focused action but gets a container with the action buried among unrelated content.
How to detect:
# Step 1: Find sheet presentations that use generic wrappers
grep -rn "EditItemSheetWrapper\|FullEditView\|SettingsView" Sources/ --include="*.swift" \
| grep -i "sheet\|present"
# Step 2: Find onItemSelected callbacks — trace what they open
grep -A5 "onItemSelected" Sources/ --include="*.swift" \
| grep "showing\|activeSheet\|EditItem"
# Step 3: Cross-reference specific CTA labels vs destination scope
grep -B5 "EditItemSheetWrapper" Sources/ --include="*.swift" \
| grep "Track\|Set\|Add\|Manage\|Configure"
Programmatic detection logic:
- Find CTA labels with specific verbs ("Track X", "Set Y", "Add Z", "Manage W")
- Trace to the destination view
- Count distinct sections/concerns in that destination
- If CTA specificity = 1 concern but destination has 3+ unrelated sections → flag
Safe patterns (do NOT flag):
- General CTAs ("Edit Product", "Settings") opening broad views — this is expected
- CTAs that open a pre-scrolled or pre-filtered version of a broad view
Severity: 🟡 HIGH (user confusion, broken trust in CTAs)
Automated Check 4: Entry Point Coverage (Orphan Feature Detection)
What it detects: Features defined in routing enums (sheet types, navigation sections, command palette items) that lack a visible UI entry point outside the command palette. Users who don't use the command palette will never discover these features.
Three-tier detection:
Tier 1: Enumerate all feature destinations
# Find all sheet/routing enum cases (adapt patterns to the project)
grep -n "case " Sources/ --include="*.swift" -r | grep -i "Sheet\|SheetType\|Section\|Destination"
# Find all command palette / QuickFind items
grep -rn "QuickFindItem\|QuickFindSheet" Sources/ --include="*.swift" | grep "case "
For each case found, record it in a feature inventory table: | Feature | Enum | Has Primary UI Entry? | Has Command Palette Entry? | Tap Depth |
Tier 2: Cross-reference against visible UI triggers
# Find all places each enum case is triggered from visible UI
# (buttons, navigation links, list rows — NOT command palette)
grep -rn "activeSheet = \.featureName" Sources/ --include="*.swift" | grep -v "QuickFind\|CommandPalette"
grep -rn "selectedSection = \.featureName" Sources/ --include="*.swift"
# Count tap depth: how many taps from a tab bar item to reach each trigger?
# Tab bar = 0, view on tab = 1, button on that view = 2, sheet from button = 3, etc.
Tier 3: Classify each feature
| Classification | Criteria | Severity |
|---|---|---|
| Orphan | No UI trigger at all (only in code, never presented) | 🔴 CRITICAL |
| Command-palette-only | Trigger exists only in QuickFind/Go To/Spotlight | 🟡 HIGH |
| Deeply buried | Primary UI trigger exists but requires 4+ taps | 🟡 HIGH |
| Adequately surfaced | Reachable in 1-3 taps from a tab | No finding |
What to flag:
- Features with zero visible UI entry points (command-palette-only or orphan)
- Features requiring 4+ taps when they serve a primary user need (not a power-user utility)
- Features whose only entry point is a gesture (swipe, long-press) with no visual hint
Safe patterns (do NOT flag):
- Power-user/maintenance utilities (photo optimizer, data cleanup) at 3+ taps — these are correctly placed as secondary tools
- Features intentionally gated behind settings (e.g., Legacy Wishes behind
legacyWishesEnabled) - Debug/developer-only features
- Command palette entries that mirror an existing primary UI entry point (aliases)
Severity: 🔴 CRITICAL if feature is user-facing with no entry point, 🟡 HIGH if command-palette-only or 4+ taps for a primary feature, ⚪ LOW if internal/utility
Automated Check 5: Buried Primary Action
What it detects: Primary action button (Save, Continue, Done, Submit) placed inside a ScrollView after tall content, making it invisible without scrolling. Users see only "Cancel" and feel trapped.
How to detect:
# Step 1: Find files with primary action buttons
grep -rn "\.borderedProminent\|\.controlSize(.large)" Sources/ --include="*.swift" \
| cut -d: -f1 | sort -u > /tmp/primary_buttons.txt
# Step 2: Find files with ScrollView
grep -rn "ScrollView" Sources/ --include="*.swift" \
| cut -d: -f1 | sort -u > /tmp/scrollviews.txt
# Step 3: Cross-reference — files with BOTH are candidates
comm -12 /tmp/primary_buttons.txt /tmp/scrollviews.txt
# Step 4: For each candidate, manually check:
# - Is the button INSIDE the ScrollView? (not pinned outside)
# - Is it the last child of a VStack inside ScrollView?
# - Are there 4+ tall elements above it?
# - Exclude: .toolbar buttons, buttons outside ScrollView, Form-based layouts
Safe patterns (do NOT flag):
// ✅ Button pinned OUTSIDE ScrollView
VStack(spacing: 0) {
ScrollView { content }
Divider()
actionButtons.padding() // pinned below scroll
}
// ✅ Button in .toolbar
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Done") { ... }
}
}
// ✅ Standard Form sections
Form {
Section { ... }
Section { Button("Save") { ... } }
}
// ✅ Bottom action bar outside ScrollView
VStack(spacing: 0) {
ScrollView { photoGrid }
bottomActionBar
}
Severity: 🟡 HIGH (user feels trapped, only sees Cancel)
Automated Check 6: Dismiss Traps
What it detects: A view where the only visible action is Cancel/Dismiss/back with no forward path shown.
How to detect:
# Step 1: Find views with only cancellationAction in toolbar
grep -rn "cancellationAction" Sources/ --include="*.swift" \
| cut -d: -f1 | sort -u > /tmp/cancel_views.txt
# Step 2: Find views with confirmationAction or primaryAction
grep -rn "confirmationAction\|primaryAction" Sources/ --include="*.swift" \
| cut -d: -f1 | sort -u > /tmp/forward_views.txt
# Step 3: Files with cancel but no forward action in toolbar
comm -23 /tmp/cancel_views.txt /tmp/forward_views.txt
# Step 4: For each candidate, check if body has visible .borderedProminent button
# Exclude: HelpView, WhatsNewSheet, info/about views (dismiss is expected)
Safe patterns (do NOT flag):
// ✅ Cancel + Done/Save in toolbar
.toolbar {
ToolbarItem(placement: .cancellationAction) { Button("Cancel") { ... } }
ToolbarItem(placement: .confirmationAction) { Button("Done") { ... } }
}
// ✅ Cancel in toolbar + primary action in body (visible without scroll)
// ✅ Read-only/info sheets (dismiss is the only expected action)
Severity: 🟡 HIGH (user feels stuck after completing a step)
Automated Check 7: Gesture-Only Actions
What it detects: Feature or action accessible only via gesture (swipe, long-press, context menu) with no visible button or menu alternative.
How to detect:
# Step 1: Find swipeActions and contextMenu usage
grep -rn "\.swipeActions\|\.contextMenu" Sources/ --include="*.swift"
# Step 2: For each file with gesture actions, extract action labels
grep -A10 "\.swipeActions\|\.contextMenu" Sources/ --include="*.swift" \
| grep 'Button("'
# Step 3: Check if those same action labels appear as visible buttons
# in the same view (outside gesture blocks)
IMPORTANT — Check ALL list views, not just a sample. A previous audit checked 1 of 7 list views and concluded "all clear," missing 6 findings.
Safe patterns (do NOT flag):
// ✅ Swipe action + toolbar/menu equivalent
.swipeActions { Button("Delete") { ... } }
.toolbar {
ToolbarItem { Menu { Button("Delete") { ... } } }
}
// ✅ Standard list delete (.onDelete) — well-known iOS convention
// ✅ Context menu duplicating a visible button (convenience)
Severity: 🟢 MEDIUM (feature undiscoverable for some users; 🟡 HIGH on macOS where swipe is rare)
Automated Check 8: Loading State Traps
What it detects: A view that shows a loading indicator with no way for the user to cancel, go back, or timeout.
How to detect:
# Step 1: Find ProgressView paired with interactiveDismissDisabled
grep -rn "interactiveDismissDisabled" Sources/ --include="*.swift"
# Step 2: Find full-screen loading overlays
grep -B5 -A5 "ProgressView" Sources/ --include="*.swift" \
| grep -l "ignoresSafeArea\|ZStack"
# Step 3: For each, check if cancel button exists during loading state
# Check if async operation has timeout/cancellation
Safe patterns (do NOT flag):
// ✅ Loading with cancel button
// ✅ Loading with timeout
// ✅ Inline progress indicator (doesn't block interaction)
// ✅ Brief loading (< 2 sec typical) for local operations
Severity: 🟢 MEDIUM (user trapped if operation hangs)
Automated Check 9: Context Dropping
What it detects: Navigation path has item/context available at the source but drops it before reaching the destination. The right destination is opened but without the context the user expects.
How to detect:
# Step 1: Find platform-split presentations
grep -rn "#if os(iOS)" Sources/ --include="*.swift" \
| xargs -I{} grep -l "\.sheet\|\.fullScreenCover" {} 2>/dev/null
# Step 2: Compare parameters in iOS sheet vs macOS notification
grep -B2 -A15 "NotificationCenter.default.post" Sources/ --include="*.swift" \
| grep -E "userInfo|let context"
# Step 3: Find NavigationContext structs and their properties
grep -rn "NavigationContext\|NavigationInfo" Sources/ --include="*.swift"
# Step 4: Compare context struct properties with destination view init params
# Step 5: Find show* flags set from item-context closures where destination
# uses parameterless init
grep -B10 "show.*= true" Sources/ --include="*.swift" \
| grep -E "item\.|onItemSelected"
Safe patterns (do NOT flag):
// ✅ All destination parameters passed on all platforms
// ✅ Parameterless init is intentional (fresh/empty state)
// ✅ Context struct matches notification sender 1:1
Severity: 🟡 HIGH (user loses context; may cause data loss)
Automated Check 10: Notification Navigation Fragility
What it detects: Navigation between views using NotificationCenter with untyped [String: Any] dictionaries instead of typed function calls or bindings. Key typos, type mismatches, or omitted fields are silent at compile time.
How to detect:
# Step 1: Find NotificationCenter posts with userInfo (navigation-related)
grep -rn "NotificationCenter.default.post" Sources/ --include="*.swift" \
| grep -v "object: nil)" \
| grep -i "userInfo"
# Step 2: Find the notification names used for navigation
grep -rn "\.requestNavigate\|\.navigateTo\|\.showFeature\|\.openSection" Sources/ --include="*.swift"
# Step 3: Find corresponding receivers
grep -rn "\.onReceive\|publisher(for:" Sources/ --include="*.swift" \
| grep -i "navigate\|show\|open"
# Step 4: For each sender/receiver pair, compare key counts and key names
Safe patterns (do NOT flag):
// ✅ Typed callback/closure
// ✅ Environment-based navigation
// ✅ Binding-based state change
// ✅ Notification used for non-navigation purposes (sync events, refresh triggers)
Severity: 🟡 HIGH (silent bugs, no compiler safety)
Automated Check 10b: Notification Type-Safety
What it detects: Same userInfo key sent with one type (e.g., PersistentIdentifier) but cast to a different type on receive (e.g., as? String). Compiles silently, fails at runtime with silent nil.
How to detect:
# Step 1: Extract all userInfo key-value pairs from senders
grep -B2 -A20 "NotificationCenter.default.post" Sources/ --include="*.swift" -rn \
| grep -E '"[a-zA-Z]+":' > /tmp/sender_keys.txt
# Step 2: Extract all userInfo key casts from receivers
grep -B2 -A10 "userInfo\?" Sources/ --include="*.swift" -rn \
| grep -E '"[a-zA-Z]+".*as\?' > /tmp/receiver_casts.txt
# Step 3: For each key in BOTH sender and receiver:
# Compare the type being sent vs the type being cast
# Mismatch example:
# Sender: "itemID": item.persistentModelID (PersistentIdentifier)
# Receiver: info["itemID"] as? String (String)
# → CRITICAL: silent nil, navigation silently broken
# Step 4: Flag mismatches:
# - Sender PersistentIdentifier, receiver String (or vice versa)
# - Sender uses `as Any` type erasure
# - Orphaned keys sent but never read
# - Missing keys expected by receiver but never sent
Severity: 🔴 CRITICAL (type mismatches cause silent navigation failures)
Automated Check 11: Sheet Presentation Asymmetry
What it detects: Same feature uses fundamentally different presentation mechanisms on different platforms — e.g., iOS uses .sheet with direct view init, macOS uses NotificationCenter → navigation. Both work, but different mechanisms drift apart.
How to detect:
# Step 1: Find files with platform-conditional sheet presentations
grep -rn "#if os(iOS)" Sources/ --include="*.swift" \
| cut -d: -f1 | sort -u > /tmp/platform_split.txt
# Step 2: For each, check if iOS uses .sheet and macOS uses a different mechanism
for f in $(cat /tmp/platform_split.txt); do
ios_sheet=$(grep -c "\.sheet\|\.fullScreenCover" "$f" 2>/dev/null || echo 0)
notification=$(grep -c "NotificationCenter.default.post" "$f" 2>/dev/null || echo 0)
if [ "$ios_sheet" -gt 0 ] && [ "$notification" -gt 0 ]; then
echo "ASYMMETRY: $f"
fi
done
# Step 3: For asymmetric files, count parameters in each path
Safe patterns (do NOT flag):
// ✅ Same mechanism, minor platform differences (styling, frame size)
// ✅ Platform-specific STYLING, not mechanism
Severity: 🟡 HIGH (maintenance burden, drift risk, enables context dropping)
Automated Check 12: Stale Navigation Context
What it detects: A view stores navigation context in @State for later use, but the context is never cleared — can become stale if source item is deleted or user navigates away.
How to detect:
# Step 1: Find @State properties with Context/Info types
grep -rn "@State.*private.*var.*[Cc]ontext\|@State.*private.*var.*[Ii]nfo\|@State.*private.*var.*[Nn]avigation" \
Sources/ --include="*.swift"
# Step 2: For each, check if it's ever set to nil
# Extract variable name, search for "varName = nil"
# Step 3: If no clearing mechanism exists → flag as stale context risk
# Step 4: Check if context references PersistentIdentifier without validation
Safe patterns (do NOT flag):
// ✅ Context cleared after use (.onDisappear, .onChange)
// ✅ Context is a computed property (always fresh)
// ✅ Context validated before use (modelContext.model(for:) != nil check)
Severity: 🟢 MEDIUM (edge case but can cause crashes or stale data display)
Automated Check 13: Simulated Delay
What it detects: Task.sleep or asyncAfter followed by hardcoded data assignment — simulating a network fetch or AI computation with fake data.
How to detect:
# Step 1: Find Task.sleep or asyncAfter in feature views (not tests/previews)
grep -rn "Task\.sleep\|asyncAfter" Sources/ --include="*.swift" \
| grep -v "Test\|Preview\|#Preview" \
| grep -v "// animation\|// dismiss\|// sheet" > /tmp/delay_sites.txt
# Step 2: For each delay site, check if the next 10 lines set a hardcoded value
while IFS=: read -r file line _; do
sed -n "$((line+1)),$((line+10))p" "$file" \
| grep -q "= \[.*\]\|= .*(\|= \".*\"\|= true\|= false\|\.result =\|\.suggestion" \
&& echo "SIMULATED: $file:$line"
done < /tmp/delay_sites.txt
# Step 3: Exclude legitimate uses:
# - Task.sleep for animation timing (< 0.5s, near dismiss/animation code)
# - asyncAfter for sheet presentation sequencing
# - Delays before real async/API calls
# Flag: delay + hardcoded data WITHOUT any network/API call between them
Severity: 🟢 MEDIUM to 🔴 CRITICAL (depends on whether users see fake data as real)
Automated Check 14: Navigation Container Constraints
What it detects: selectedSection = .X assignments where .X is not a valid tag in the current navigation container. On iPhone (TabView), only tab tags are valid. On iPad/macOS (sidebar), sidebar items are valid. Setting a non-existent tag is a silent no-op — the user taps and nothing happens.
How to detect:
# Step 1: Find all selectedSection assignments
grep -rn "selectedSection = \." Sources/ --include="*.swift"
# Step 2: Find valid TabView tags (iPhone)
grep -rn "\.tag(\." Sources/ --include="*.swift" | grep -i "tab"
# Step 3: Find valid sidebar items
grep -rn "\.tag(\." Sources/ --include="*.swift" | grep -i "sidebar\|NavigationSplitView"
# Step 4: For each assignment in Step 1, check if the value is valid for both platforms
# If assigned value is not a valid tag on either platform → flag
Safe patterns (do NOT flag):
// ✅ Assigned value matches a .tag() in both TabView and sidebar
// ✅ Assignment is inside a platform check (#if os(iOS)) using platform-specific tags
Severity: 🟡 HIGH (user taps, nothing happens — silent dead end)
Automated Check 15: Notification Lifecycle
What it detects: Notification names that are posted but never received (dead posts), or declared but never posted or received (dead declarations). Also catches receivers without corresponding posters.
How to detect:
# Step 1: Find all notification name declarations
grep -rn "static let\|static var" Sources/ --include="*.swift" \
| grep "Notification.Name\|NSNotification.Name"
# Step 2: For each notification name, count posts and receivers
# Posts: grep for ".post(name: .notificationName"
# Receivers: grep for ".onReceive" or "publisher(for: .notificationName"
# Step 3: Flag:
# - Declared + posted but never received → dead post
# - Declared but never posted or received → dead declaration
# - Received but never posted → will never fire
Safe patterns (do NOT flag):
// ✅ System notifications received but not posted (e.g., .willResignActive)
// ✅ Notifications used in extension targets (may be posted/received in a different target)
Severity: ⚪ LOW for dead declarations, 🟢 MEDIUM for dead posts (code that runs but has no effect), 🟡 HIGH for receivers without posters (code waiting for something that never happens)
Automated Check 16: Success Feedback
What it detects: Non-destructive actions (save, duplicate, archive, bookmark, export) that complete without any user-visible confirmation — no toast, haptic, banner, or navigation change.
How to detect:
# Step 1: Find action completion points
grep -rn "modelContext\.save\|dismiss()\|\.delete\|duplicate\|archive\|export\|\.insert" \
Sources/ --include="*.swift"
# Step 2: For each, check surrounding 10 lines for feedback:
# - ToastManager, toast, showToast
# - UINotificationFeedbackGenerator, haptic
# - banner, alert, confirmationDialog
# - Navigation change (dismiss + new sheet)
# Step 3: Flag actions with no feedback within 10 lines of the completion point
# Exclude: destructive actions (delete) — those should have pre-confirmation, not post-feedback
Safe patterns (do NOT flag):
// ✅ Toast shown after save
// ✅ Haptic played on completion
// ✅ View dismisses with completion callback that shows feedback
// ✅ Destructive actions with pre-confirmation dialog (feedback is the confirmation)
Severity: 🟢 MEDIUM (user unsure if action succeeded)
Automated Check 17: Empty State Coverage
What it detects: List or collection views that show nothing when empty — no ContentUnavailableView, no placeholder text, no onboarding prompt. Users see a blank screen and think the app is broken.
How to detect:
# Step 1: Find list/collection views
grep -rn "List {\|ForEach\|LazyVGrid\|LazyHGrid\|LazyVStack" Sources/ --include="*.swift" \
| cut -d: -f1 | sort -u
# Step 2: For each, check if there's an empty state handler:
# - ContentUnavailableView
# - .overlay { if items.isEmpty { ... } }
# - if items.isEmpty { emptyView } else { List { ... } }
# Step 3: Flag lists with no empty state handling
# Exclude: lists that are always populated (e.g., settings sections, static menus)
Severity: 🟡 HIGH (users think app is broken when they first open it)
Automated Check 18: Error Recovery
What it detects: Error states displayed to the user with no retry button, no "Open Settings" link, or no other recovery path. The user sees an error and has no way forward.
How to detect:
# Step 1: Find error display patterns
grep -rn "\.alert\|errorMessage\|showError\|isError" Sources/ --include="*.swift"
# Step 2: For each error display, check if there's a retry or recovery action:
# - Button("Retry"), Button("Try Again")
# - Link to Settings (openURL with app-settings:)
# - Alternative action offered
# Step 3: Flag errors with no recovery path
# Exclude: fatal errors that genuinely can't be retried, informational alerts with "OK"
Severity: 🟡 HIGH (user stuck at error with no path forward)
Verification Template (MANDATORY for Layer 3)
Before grading issues, produce this table for each flagged entry point from Layer 1:
| # | Entry Point | Flag | Verified? | Receipt | Status |
|---|-------------|------|-----------|---------|--------|
| 1 | [label] | [flag] | ? | (file:line checked) | confirmed / retracted / needs-runtime |
Rules:
- Every flagged entry point from Layer 1 must appear in this table
?in the Verified column means the finding hasn't been checked in Layer 3 yet- Layer 3 cannot produce a grade while any flagged entry has
?in Verified - Retracted findings stay in the table with strikethrough — they prove you checked, not just confirmed
If "layer4" or "evaluate": enumerate-required
Before starting, print:
Layer 4: Semantic Evaluation — [N] issues to evaluate
Estimated time: ~[1-2 min for <10 issues / 3-5 min for 20+]
- For each issue, assess user impact using these 4 criteria (score each 1-5):
| Criterion | 1 (no impact) | 3 (moderate) | 5 (severe) |
|---|---|---|---|
| Discoverability | Feature is obvious, multiple entry points | Requires learning but findable | Hidden, gesture-only, or buried 4+ taps deep |
| Efficiency | One tap from context | 2-3 taps, minor detour | 4+ taps, navigation required, context lost |
| Feedback | Clear confirmation + undo | Confirmation but no undo | No feedback, user unsure if action completed |
| Recovery | Easy undo, no data loss | Partial undo, minor data loss possible | No undo, data loss, or must recreate from scratch |
For each finding, assign a confidence level:
verified— confirmed by grep, file read, or code trace. The issue definitely exists.probable— code suggests the issue but not fully traced through all code paths.needs-runtime— static analysis can't determine; requires running the app to confirm.
Map violations to design principles (#1-#8)
After each evaluation, print:
Layer 4: ✓ Issue [N]/[total] — [confidence] — [D/E/F/R scores]Output to
layer4-semantic-evaluation.md
If "layer5" or "data-wiring" or "wiring": enumerate-required
Before starting, print:
Layer 5: Data Wiring — inventorying models and cross-referencing features
Estimated time: ~[2-4 min for <20 features / 5-10 min for 40+]
- Model inventory. Catalog what data the app tracks:
# Find all @Model classes and their properties
grep -rn "@Model" Sources/Models/ --include="*.swift" -l
# Find relationships
grep -rn "var.*:.*\[.*\]?" Sources/Models/ --include="*.swift" | grep -v "//"
# Find computed properties that aggregate data
grep -rn "var.*:.*{" Sources/Models/ --include="*.swift" | grep -i "total\|average\|count\|cost\|price"
Print: Layer 5: ✓ Model inventory (1/7) — [N] models, [N] properties
Select features to cross-reference. Don't check every feature — select the top 5-8 using these criteria:
- Features that make decisions based on model data (advisors, calculators, suggestion engines)
- Features with the most model properties available but unclear how many they use
- Features flagged in earlier layers
- The primary detail view (reads the most model data)
For each, check what model data it reads vs what's available. Print:
Layer 5: ✓ Feature scan (2/7) — [N] features checkedMock data detection:
# Fake fetch pattern
grep -rn "asyncAfter" Sources/Features/ --include="*.swift" -A 10 | grep -B 5 "=.*[0-9]\|\".*\$"
# Static arrays pretending to be fetched data
grep -rn "let.*=.*\[" Sources/Features/ --include="*.swift" | grep -i "alternative\|suggestion\|recommendation"
# Hardcoded scores/ratings in non-test code
grep -rn "Score.*=.*[0-9]\|rating.*=.*[0-9]" Sources/Features/ --include="*.swift" | grep -v "test\|Test\|preview\|Preview"
# Functions that simulate work
grep -rn "func fetch\|func load\|func compute" Sources/Features/ --include="*.swift" -A 15 | grep "asyncAfter\|sleep\|\.random"
Print: Layer 5: ✓ Mock data scan (3/7)
- Cross-reference matrix:
| Feature | Data Available | Data Used | Data Ignored |
|---|---|---|---|
| [name] | [model properties] | [what it reads] | [gap] |
Print: Layer 5: ✓ Cross-reference matrix (4/7) — [N] features cross-referenced, [N] gaps found
Form-to-detail parity check (MANDATORY):
For every model that has BOTH a form/edit view AND a detail/read-only view, verify that every user-editable field in the form has a corresponding display in the detail view.
Method: a. Identify form/detail view pairs (e.g.,
ExtendedWarrantyFormView+EnhancedItemDetailView+Sectionsextended warranty section;RMAFormView+RMAListView). b. In each form, enumerate fields bound to TextFields, Pickers, Toggles, DatePickers, Steppers. c. For each editable field, grep the detail view for a display consumer. Classify:displayed: Field appears in a DetailKeyValueRow, Text(), or equivalent read-only rendering.form-only: Field is only read by the form to populate edit state. User enters data that vanishes after save.computed: Field feeds a computed property that IS displayed (indirect display -- acceptable).serialization-only: Field is backed up/exported but never shown to the user in any view.
d. Report
form-onlyfields as findings withpattern_fingerprint: form_editable_but_no_detail_display.Why this matters: Users who enter data and can't see it after saving perceive it as data loss, even when the data is correctly persisted. This is the most common "vanishing field" UX bug pattern and is invisible to data-integrity audits (the data round-trips perfectly).
Handoff input: If data-model-radar ran first, check
.agents/ui-audit/data-model-radar-handoff.yamlforform_only_fields[]. These are pre-identified suspects -- verify each against the actual detail view rather than re-scanning from scratch.Print:
Layer 5: ✓ Form-to-detail parity (5/7) — [N] form-only fields foundIntegration gap detection:
# Find Manager/Service classes
grep -rn "class.*Manager\|class.*Service" Sources/ --include="*.swift" | grep -v "test\|Test"
# Check if feature views reference them
Print: Layer 5: ✓ Integration gap detection (6/7) — [N] manager/service references checked
- Platform parity check:
# Find iOS-only dismiss buttons
grep -rn "#if os(iOS)" Sources/ --include="*.swift" -A 3 | grep -i "dismiss\|toolbar\|done"
# Find extension files with platform-specific computed properties
grep -rl "extension.*View" Sources/ --include="*.swift" | grep "+"
Print: Layer 5: ✓ Platform parity (7/7)
- Output to
layer5-data-wiring.yaml
If "diff":
Compare findings against the previous ui-path-radar audit — surface what regressed, what got fixed, and what's new since the last run.
Usage
/ui-path-radar diff
/ui-path-radar diff --since 2026-04-01
/ui-path-radar diff --layer 3 # restrict to one layer's findings
Source of Truth
The diff reads from .radar-suite/ledger.yaml — the only authoritative cross-session store of ui-path-radar findings. Per-layer YAML files (.ui-path-radar/layer3-results.yaml, .ui-path-radar/layer5-data-wiring.yaml) and per-run handoff files are overwritten each run, so they cannot serve as a diff baseline.
The "previous audit" is defined as the most recent ledger session entry with skill: ui-path-radar that is strictly older than the current session. If no prior session exists, the diff command MUST refuse with:
"No prior ui-path-radar audit found in
.radar-suite/ledger.yaml. Run a full audit (/ui-path-radar) first to establish a baseline."
Do not invent a baseline. Do not fall back to memory or stale .ui-path-radar/*-audit.md reports.
How It Works
- Identify the baseline session — read
.radar-suite/ledger.yaml, find the most recent prior session entry withskill: ui-path-radar. With--since YYYY-MM-DD, use the latest entry on or after that date instead. - Identify the current session — either the in-progress session (if the user just ran an audit) or the most recent completed session.
- Run a quick re-verification scan on the current codebase for each previously-reported issue's file:line — has the problem been fixed in place?
- Bucket every finding from the union of baseline + current into one of four categories by RS-NNN ID:
- Fixed — present in baseline with
status: open, present in current withstatus: fixed, OR the file:line no longer matches the pattern fingerprint - Regressed — present in baseline with
status: fixed, butfile_hashchanged AND the pattern fingerprint matches at the same file:line again - New — RS-NNN ID is in current but not baseline
- Persistent — present in both with the same status
- Fixed — present in baseline with
- Apply optional filters:
--layer [1-5]restricts to findings whoselayerfield matches.
Output Format
Audit Diff: <previous date> → <current date> (<days> days)
✅ Fixed (<count>)
| RS-NNN | short_title | Category | Fixed in |
🔴 Regressed (<count>)
| RS-NNN | short_title | Category | Was fixed | file_hash changed? |
🆕 New (<count>)
| RS-NNN | short_title | Category | Urgency |
📌 Persistent (<count>)
[collapsed by default; pass --verbose to expand]
📁 Changed files since baseline: <count> (may need re-verification)
Refusal cases
- No prior ledger session: refuse with the message above
--sincedate is in the future: refuse with "Date is in the future; no audits to compare"--layer [N]outside 1-5: refuse with "Layer N is not a valid ui-path-radar layer (must be 1-5)"--layer [N]matches no findings in either baseline or current: print "No findings in Layer N across either session" (not a refusal — a legitimate empty result)
If "fix" or "fixes":
- Read
layer3-results.yamlandlayer5-data-wiring.yamlfor unfixed issues - Generate specific code fixes
- Prioritize by severity (critical first)
- Group into:
- Safe fixes — isolated, low blast radius
- Cross-cutting fixes — touch shared code
- Requires design decision — multiple valid approaches
- Deferred — no action needed now
- Out of scope — belongs to a different audit type
If "status":
- Read existing audit files
- Report: issues found, fixed, remaining
- Show priority queue for unfixed issues
Output Format
CRITICAL FORMATTING RULE: The Issue Rating Table below IS the output. Do NOT create separate sections for "Critical Issues", "Data Wiring Issues", "Recommendations", or any other vertical breakdown of findings. Every finding — navigation issues, data wiring issues, orphaned code, missing feedback, design violations — goes into ONE table as ONE row. Context goes in the Finding column. No exceptions.
Layer Transition Summary (between each layer)
When completing a layer and moving to the next, print:
✓ Layer [N] of 5 complete: [plain description] — [M] findings ([X] verified, [Y] probable, [Z] needs-runtime)
Retracted from prior layers: [count or "none"]
Cumulative: [total] findings ([C] critical, [H] high, [M] medium, [L] low)
Next: Layer [N+1] — [plain description of what it does and why it matters given current findings].
→ "proceed" | "explain #[N]" | "stop here"
Final Output (after all layers or after a single layer run)
After completing the audit, provide these 6 items in order:
- One-line summary — entry point count, issue count by severity (one sentence, not a section)
- Issue Rating Table — every finding in a single table. Each finding MUST include a confidence tag (
verified/probable/needs-runtime) in the Finding column. - Proactive risk callout — Auto-identify the top 3 riskiest findings:
Before proceeding, these findings have elevated risk profiles:
#[N] — [short description] (Risk:Fix [indicator], Blast [count] files)
#[N] — [short description] (Risk:No Fix [indicator], [consequence])
→ Say "explain #[N]" for a detailed risk breakdown before deciding.
- Cross-skill handoff notes (if applicable):
Related areas to check next:
#[N] → Check whether saved data survives editing round-trips (data safety audit)
#[N] → Check visual layout and spacing for this screen (visual quality audit)
- Limitations disclaimer:
Limitations: Static analysis only. Not checked: animation smoothness,
real-device timing, race conditions under memory pressure, subjective
UX feel, accessibility with VoiceOver. Consider runtime testing for
findings marked "needs-runtime."
- One-line next step — suggest next action
Items 3-5 may be omitted if not applicable.
Issue Rating Table
Hard formatting rule — Table, not list: ALL findings MUST be in a single markdown table. Each finding is ONE ROW. Never expand into individual sections or bullet-pointed ratings. ALL categories go in the same table.
Full table:
| # | Finding | Conf | Urgency | Risk:Fix | Risk:NoFix | ROI | Blast | Effort | Status |
|-----|---------------------------|----------|--------------|----------|------------|----------|----------|---------|--------|
| 1 | Dead end: ".warranty" | verified | 🔴 Critical | ⚪ Low | 🔴 Crit | 🟠 Exc | 🟢 2f | Trivial | Open |
| | unhandled | | | | | | | |
Compact table (narrow terminal):
| # | Finding | Conf | Urgency | Effort |
|-----|---------------------------|----------|--------------|---------|
| 1 | Dead end: ".warranty" | verified | 🔴 Critical | Trivial |
Indicator Scale
| Indicator | General meaning | ROI meaning |
|---|---|---|
| 🔴 | Critical / high concern | Poor return — reconsider |
| 🟡 | High / notable | Marginal return |
| 🟢 | Medium / moderate | Good return |
| ⚪ | Low / negligible | — |
| 🟠 | Pass / positive | Excellent return |
- Urgency: 🔴 CRITICAL (dead end, wrong destination, mock data) · 🟡 HIGH (broken promise, missing activation, unwired data) · 🟢 MEDIUM (two-step flow, missing feedback) · ⚪ LOW (inconsistency, orphaned code)
- Risk: Fix: Risk of the fix introducing regressions
- Risk: No Fix: User-facing consequence of leaving the issue
- ROI: 🟠 Excellent · 🟢 Good · 🟡 Marginal · 🔴 Poor
- Blast Radius: Number of files the fix touches. Do not use
<br>tags. Count by grepping for callers/references before rating. - Fix Effort: Trivial / Small / Medium / Large
Finding Dependencies and Fingerprints
When creating findings, populate these optional fields where relationships are obvious:
depends_on/enables: UI path findings often chain -- a dead-end fix enables a flow that was previously untestable. If one fix must come before another, populate with finding IDs.pattern_fingerprint/grep_pattern/exclusion_pattern: Assign fingerprints for generalizable UI anti-patterns (e.g.,dead_end_sheet,unhandled_navigation_case,mock_data_in_production,missing_platform_parity).
Fix Application Workflow
After presenting findings, apply fixes in waves. After each wave (including commits), always print the progress banner and auto-prompt for the next wave. Never leave the user with a blank prompt.
Waves
| Wave | Section | Est. Time | Description |
|---|---|---|---|
| 1 | Safe fixes + tests | ~10-15 min | Isolated, low blast radius. Auto-apply. Write tests. |
| 2 | Cross-cutting fixes + tests | ~15-25 min | Touch shared code. Present for review. Write tests. |
| 3 | Design decisions | ~5-15 min | Multiple options. Requires user input per item. |
| 4 | Build + Test + Commit | ~5 min | Build both platforms, run tests, stage, commit. |
Every fix must have a test. Do not move to the next wave until tests for the current wave's fixes are written and compiling.
Skip empty waves.
Progress Banner (MANDATORY after every wave)
CRITICAL — BLOCKING requirement in Normal and Pre-Approved modes. After EVERY wave and EVERY commit, your NEXT output MUST be the progress banner followed by the next-wave AskUserQuestion.
Hands-Free mode exception: In Hands-Free mode, the progress banner still prints, but the next-wave AskUserQuestion call is suppressed (per the Hands-Free Mode precedence rules above). Replace the AskUserQuestion with the Hands-Free completion message instead.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ Wave [N] of [total] complete: [wave name]
[X] findings fixed, [Y] remaining, [Z] deferred
⏱ Next: Wave [N+1] — [wave name] (~[time estimate])
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Then immediately ask: "Ready for Wave [N+1]?" with options:
- Proceed (Recommended) — Start the next wave
- Commit first — Commit current changes before continuing
- Stop here — End for now, resume later
Pipeline Mode Behavior (Tier 2/3)
When running inside a Tier 2 or Tier 3 pipeline (detected via tier field in .radar-suite/session-prefs.yaml):
- On skill start: Emit the pipeline-level progress banner (see
radar-suite-core.mdPipeline UX Enhancements #1). If this is the first skill in the pipeline ORexperience_levelis Beginner/Intermediate, also emit the audit-only statement. - On skill completion: Emit a per-skill mini rating table marked "PRELIMINARY" (see Pipeline UX Enhancements #2). Then emit the pipeline-level progress banner showing this skill as complete.
- Within-skill wave banners (above) are still emitted normally in addition to the pipeline-level banners.
short_title Requirement (v2.1)
Every finding MUST include a short_title field (max 8 words). This is the human-scannable label used in pipeline banners, pre-capstone summaries, and ledger output.
Example: short_title: "Delete button missing on edit sheet"
All finding ID references in output (tables, banners, summaries) use the format: RS-NNN (short_title).
Regression Canaries
After fixing a workflow issue, generate a "canary" — a specific check that detects if the issue recurs. Canaries are stored in .ui-path-radar/canaries.yaml and can be run as a quick regression check before release.
Canary Format
canaries:
- id: "canary-001"
issue_ref: "issue-017"
description: "NavigationContext must include existingItemID"
check_type: "grep_match"
file: "Sources/Views/Navigation/AppNavigationView.swift"
pattern: "existingItemID.*PersistentIdentifier"
expect: "match" # fail if pattern NOT found
added: "2026-03-08"
- id: "canary-002"
issue_ref: "issue-013"
description: "Continue button must appear before tall content in flow"
check_type: "line_order"
file: "Sources/Features/ItemManagement/Views/UnifiedPhotoFlow.swift"
first_pattern: "Continue with AI Analysis"
second_pattern: "sourcePickerContent"
expect: "first_before_second"
added: "2026-03-08"
Canary Check Types
| Type | Description | Pass condition |
|---|---|---|
grep_match |
Pattern must exist in file | Pattern found |
grep_absent |
Pattern must NOT exist in file | Pattern not found |
line_order |
Two patterns must appear in specific order | First before second |
param_count |
Count params in a function/dict | Count matches expected |
platform_parity |
Same pattern must exist in both iOS and macOS blocks | Found in both |
Running Canaries
for canary in $(yq '.canaries[].id' .ui-path-radar/canaries.yaml); do
file=$(yq ".canaries[] | select(.id == \"$canary\") | .file" .ui-path-radar/canaries.yaml)
pattern=$(yq ".canaries[] | select(.id == \"$canary\") | .pattern" .ui-path-radar/canaries.yaml)
expect=$(yq ".canaries[] | select(.id == \"$canary\") | .expect" .ui-path-radar/canaries.yaml)
if [ "$expect" = "match" ]; then
grep -q "$pattern" "$file" && echo "✅ $canary" || echo "❌ $canary REGRESSION"
elif [ "$expect" = "absent" ]; then
grep -q "$pattern" "$file" && echo "❌ $canary REGRESSION" || echo "✅ $canary"
fi
done
When to Generate Canaries
After each fix in fix mode, generate a canary:
- After fixing Context Dropping → canary verifying the field exists in both paths
- After fixing Buried Primary Action → canary verifying button order
- After fixing Platform Parity Gap → canary verifying pattern on both platforms
- After fixing Dismiss Trap → canary verifying forward action exists
Canaries are additive — they accumulate over time as a regression safety net.
Handoff Brief Generation
After completing all layers (full audit) or fix mode, generate .ui-path-radar/handoff.yaml.
When to Generate
- After a full 5-layer audit completes
- After
fixmode completes (refreshes with current state) - NOT after individual layer runs
Format
# Handoff Brief — generated by ui-path-radar
project: <project name from directory>
audit_date: <ISO 8601 date>
source_files_scanned: <count>
summary:
total_issues: <count>
critical: <count>
high: <count>
medium: <count>
low: <count>
file_timestamps:
<file path>: "<ISO 8601 mod date>"
issues:
- id: <sequential number>
finding: "<description>"
category: <dead_end|wrong_destination|mock_data|incomplete_navigation|missing_activation|unwired_data|platform_gap|promise_scope_mismatch|buried_primary_action|dismiss_trap|two_step_flow|missing_feedback|gesture_only_action|loading_state_trap|context_dropping|notification_nav_fragility|sheet_presentation_asymmetry|stale_navigation_context|navigation_container_mismatch|empty_state_missing|error_recovery_missing|inconsistent_pattern|orphaned_code|command_palette_only|deeply_buried_feature|double_nested_navigation>
urgency: <critical|high|medium|low>
risk_fix: <critical|high|medium|low>
risk_no_fix: <critical|high|medium|low>
roi: <excellent|good|marginal|poor>
blast_radius: "<description, e.g. '1 file' or '4 files'>"
fix_effort: <trivial|small|medium|large>
files:
- <file path>
suggested_fix: "<what to do, not how>"
group_hint: "<optional grouping suggestion>"
File Timestamps
stat -f "%Sm" -t "%Y-%m-%dT%H:%M:%SZ" "<file path>"
Group Hints
Common hints: missing_confirmations, missing_feedback, orphaned_features, dead_code, platform_parity, dead_notifications, navigation_container
Cross-Skill Handoff
UI Path Radar complements data-model-radar (model layer), roundtrip-radar (data safety), ui-enhancer-radar (visual quality), and capstone-radar (ship readiness).
On Completion — Write Handoff
After completing an audit, write .agents/ui-audit/ui-path-radar-handoff.yaml:
source: ui-path-radar
date: <ISO 8601>
project: <project name>
file_timestamps:
<file path>: "<ISO 8601 mod date>"
for_roundtrip_radar:
suspects:
- workflow: "<affected workflow>"
finding: "<what was found>"
file: "<file:line>"
question: "<specific question for roundtrip-radar to verify>"
group_hint: "<optional>"
for_ui_enhancer_radar:
suspects:
- view: "<view file>"
finding: "<what was found>"
action: "remove or wire up before visual audit"
group_hint: "<optional>"
for_capstone_radar:
blockers:
- finding: "<description>"
urgency: "<CRITICAL|HIGH>"
group_hint: "<optional>"
checks_performed:
# automated_checks: 19 = Checks 1-10, 10b, 11-18 (Check 10b kept as suffix to preserve external references — see § Layer 3 Numbering Note)
automated_checks: 19
# categories_scanned MUST match the Issue Categories table at SKILL.md:57-92 (34 categories) — keep in sync when adding categories
categories_scanned:
- dead_end
- wrong_destination
- mock_data
- destructive_no_confirm
- silent_state_reset
- incomplete_navigation
- missing_activation
- unwired_data
- platform_gap
- promise_scope_mismatch
- buried_primary_action
- dismiss_trap
- context_dropping
- notif_nav_fragility
- sheet_asymmetry
- empty_state_missing
- error_recovery_missing
- keyboard_obscures_input
- permission_denied_dead_end
- modal_stacking
- nav_container_mismatch
- two_step_flow
- missing_feedback
- gesture_only_action
- loading_state_trap
- stale_nav_context
- phantom_touch_target
- race_condition_ux
- invisible_selection
- inconsistent_pattern
- orphaned_code
- command_palette_only
- deeply_buried_feature
- double_nested_nav
persona_evaluation: false
confidence_scoring: true
End-of-Run Directory Cleanup (MANDATORY)
Per the Artifact Lifecycle rules in radar-suite-core.md, before returning from this skill:
- List files in
.radar-suite/(and.ui-path-radar/if used). - Move any stale single-use handoffs (
RESUME_PHASE_*.md,RESUME_*.mdexceptNEXT_STEPS.md,*-v[0-9]*.md) to.radar-suite/archive/superseded/. - Confirm Class 1 persistent-state files (
ledger.yaml,session-prefs.yaml) are in-place rewrites — not dated or versioned. - Confirm Class 2 handoff files are overwrites, not appends.
This prevents .radar-suite/ from accumulating stale prose artifacts across runs.
Write to Unified Ledger (MANDATORY)
After writing the handoff YAML, also write findings to .radar-suite/ledger.yaml following the Ledger Write Rules in radar-suite-core.md:
- Read existing ledger (or initialize if missing)
- Record this session (timestamp, skill name, build)
- For each finding: check for duplicates, assign RS-NNN ID if new, set
impact_category, computefile_hash - Write updated ledger
Impact category mapping for ui-path-radar findings:
- Dead-end screen (no way forward or back) →
ux-broken - Broken navigation (button does nothing, link goes nowhere) →
ux-broken - Missing empty state or loading state →
ux-degraded - Buried CTA or hard-to-find feature →
ux-degraded - Accessibility dead end →
ux-broken - Visual-only issues →
polish
On Startup — Read Ledger & Handoffs (MANDATORY)
Before starting the audit, read the unified ledger and ALL companion handoff YAMLs:
Read .radar-suite/ledger.yaml (if exists) — check for existing findings to avoid duplicates
Read .agents/ui-audit/data-model-radar-handoff.yaml (if exists)
Read .agents/ui-audit/roundtrip-radar-handoff.yaml (if exists)
Read .agents/ui-audit/ui-enhancer-radar-handoff.yaml (if exists)
Read .agents/ui-audit/capstone-radar-handoff.yaml (if exists)
Read .workflow-audit/persona-handoff.yaml (if exists)
Read .workflow-audit/handoff.yaml (if exists)
Workflow-Audit Persona Integration
When .workflow-audit/persona-handoff.yaml exists:
- Display: "Persona evaluation from workflow-audit available -- incorporating."
- Layer 4 enrichment: Use persona D/E/F/R ratings to weight your own Layer 4 scoring. If workflow-audit rated a workflow's Feedback as 2/5, weight Feedback-related issues higher for that workflow.
- Skip persona derivation: Use workflow-audit's personas instead of re-deriving them. They were built from semantic analysis you don't replicate.
- Category overlap: Read
checks_performed.categories_scanned. For categories both skills check, still run your own automated checks but note "Also flagged by workflow-audit" for duplicate findings in the same file.
When .workflow-audit/handoff.yaml exists (without persona handoff):
- Import CRITICAL/HIGH findings as companion findings tagged
[via workflow-audit] - Note which categories workflow-audit checked
When neither exists: proceed normally. No change to audit behavior.
Ledger check: If the ledger contains findings for views you're about to audit, note their RS-NNN IDs. When you find the same issue, update the existing finding instead of creating a new one.
Regression check: For any fixed findings in the ledger whose file_hash no longer matches the current file, flag for re-verification per the Regression Detection protocol in radar-suite-core.md.
Parse for_ui_path_radar sections. Incorporate as priority targets — verify each independently.
If not found, proceed normally.
Cautionary Note
This skill is a tool, not an oracle.
It systematically scans using pattern matching and heuristics. It surfaces real issues you'd miss manually — but it has inherent limitations:
Good at: Structural inconsistencies, patterns that compile but fail silently, cross-platform parity, repeatable checklists.
Can miss: Business logic correctness, UX nuance, false positives (intentionally retained code), novel bug patterns.
Use responsibly: Treat findings as leads to investigate, not verdicts. Verify critical findings manually. Don't assume clean = zero issues — it means zero known-pattern issues.
End Reminder
After every layer/wave/commit: print progress banner → AskUserQuestion → never blank prompt.