Radar Suite — Axis Classification Framework
Every radar in the suite invokes this skill before emitting findings. This is the verification gate and the coaching engine. Findings that do not pass the gate are rejected.
Inheritance Note (this is a framework skill, not an audit skill)
This skill inherits radar-suite-core.md for shared schema definitions (Issue Rating Table format, Handoff YAML schema) but does NOT run core's interactive protocols (Session Setup, Pre-Scan Startup, Known-Intentional Suppression, Pattern Reintroduction Detection). Those protocols apply within the audit skills that invoke this framework, not at this skill's level.
This skill is invoked programmatically by audit skills during finding emission; it never runs standalone. There is no /radar-suite-axis-classification slash command, no setup interview, no phases, no progress banner. A radar reads this skill's spec, follows the Invocation Protocol below to classify and coach its candidate findings, then writes them to its own handoff YAML with the required axis + coaching fields.
If you're reading this skill expecting a runnable command, you want a sibling radar instead (/data-model-radar, /ui-path-radar, /roundtrip-radar, /time-bomb-radar, /ui-enhancer-radar, /capstone-radar).
What This Skill Does
Three things, in order:
- Classify every candidate finding on three axes (with axis_3 splitting into two sub-labels, giving four total schema values — see § The Three Axes below): does it break user-visible behavior, is it correct code that is hard to read, or is it dead or unjustified code?
- Verify the classification against a checklist of concrete evidence checks before the finding can be emitted.
- Coach with a mandatory
better_approachsection that cites a real file:line pattern from the audited codebase (not generic advice).
Any radar can invoke this skill. The skill itself does not scan code directly — it provides the framework, the checklist, and the schema gate that each radar uses before writing its handoff YAML.
The Three Axes
axis_1 — Real Bug
Code does the wrong thing from the user's perspective. The behavior needs to change.
Examples:
- CSV import freezes the main thread on large files
- Sheet with unsaved changes discards on dismiss with no confirmation
- Platform-branch parity gap: iOS has a dismiss button, macOS does not
- Silent error swallowing with no user feedback
Default audience: end_user
Severity: 4-tier scale (critical, high, medium, low)
Grade impact: Yes — counts toward fix-before-shipping grade
axis_2 — Scatter (Correct but Hard to Read)
Code runs correctly but is structured in a way that makes the next developer's job harder. The fix is reorganization, not behavior change. No user-visible change after the fix.
Examples:
- Empty-state handlers for the same view scattered 500 lines apart in one file
- Duplicated
#if os(iOS)/#elseforks for the same UI concern across multiple files - State machine implicit in boolean flags rather than an enum
- Managers referenced as singletons without a protocol abstraction when other managers in the codebase already have one
Default audience: code_reader
Severity: Hygiene scale (urgent, rolling, backlog)
Grade impact: No — lives in the hygiene backlog, does not affect ship grade
axis_3 — Dead Code or Smelly
Either unreachable (dead code) or reachable but not clearly justified (smelly). The fix is delete, document, or interrogate.
Two sub-labels:
- axis_3_dead_code — Unreachable branch. Verified by reachability trace. Cannot be hit from any production call site.
- axis_3_smelly — Reachable but poorly justified. Defensive guard with no documented failure mode. Error path that logs but cannot actually fire. Field defined in model but not written or read anywhere.
Examples:
- Empty-state branch in a view that is unreachable because an upstream filter removes the empty case
guard leton a value that is constructed two lines above and cannot be nil- Model field that has neither write sites nor read sites in the wired-up app (as opposed to a wiring bug, which is axis_1)
Default audience: future_maintainer
Severity: Hygiene scale
Grade impact: No — lives in the hygiene backlog
Verification Checklist (MANDATORY — before every finding emission)
A radar MUST run these checks before assigning an axis and emitting the finding. Each check that was run is logged in the finding's verification_log field. A finding without a verification_log is rejected by the schema gate.
Check 1: Reachability Trace (required for axis_1 dead-end claims)
Rule: Before emitting "this branch is a user-facing dead end," trace the branch back to a production call site.
How:
- Identify the file and line of the flagged branch
- Grep for the enclosing function / view struct name across the codebase
- Walk up call sites at least 2 levels (who calls this view, who navigates here)
- If no production call site reaches this branch, reclassify as axis_3_dead_code and emit with the reclassification logged
Log entry:
- check: reachability_trace
result: "reached from MyProductsView.swift:915 via galleryContent(items) — reachable"
# or:
result: "no production call site found; reclassified to axis_3_dead_code"
Check 2: Whole-File Scan (required for "missing handler" claims)
Rule: Before claiming a state / case / branch is unhandled, scan the ENTIRE file (not just the flagged region) for a handler.
How:
- Read the full file where the finding was detected (or at minimum, grep the whole file for the case name, enum value, or state flag)
- If a handler exists elsewhere in the same file, reclassify as axis_2_scatter (the handler exists but is scattered)
- The original finding is still valid, but its fix is reorganization, not adding missing logic
Log entry:
- check: whole_file_scan
result: "scanned 1169 lines; no other handlers for .empty case found — finding stands as axis_1"
# or:
result: "scanned 1169 lines; handler at LegacyWishesView.swift:847; reclassified to axis_2_scatter"
Check 3: Branch Enumeration (required for any #if claim)
Rule: Before classifying a #if os(iOS) (or any conditional compilation block) as iOS-only or platform-broken, READ both the #if and the #else branches. Do not drop the #else.
How:
- When a flagged pattern is inside a
#ifblock, read the full block including all#elseifand#elseclauses - Verify the claim against every branch, not just the one you noticed first
- If the
#elsebranch handles the case and the radar missed it, the finding is a false positive
Log entry:
- check: branch_enumeration
result: "#if os(iOS) block at lines 102-118 has #else at 112-116 that handles the macOS case; finding retracted"
# or:
result: "#if os(iOS) block at lines 102-118 has no #else; iOS-only code confirmed"
Check 4: Pattern Citation Lookup (required for every better_approach)
Rule: The better_approach coaching field MUST cite an existing pattern in the SAME codebase being audited. Grep for the pattern shape before writing the recommendation. Generic advice ("consider using a protocol abstraction") without a citation is rejected.
How:
- When writing a
better_approach, identify the pattern shape (protocol abstraction, async bridge, typed navigation enum, etc.) - Grep the audited codebase for that shape
- If a match exists, cite it by file:line in the
better_approachbody - If no match exists, fall back to an anonymized shape reference from
coaching-examples-generic.mdAND note explicitly "no existing pattern found in this codebase"
Log entry:
- check: pattern_citation_lookup
result: "found similar pattern at Sources/Protocols/CloudSyncManaging.swift:14 (protocol + @MainActor class)"
#