Skip to content

refactor: type-safe recording backends + exhaustive capability gating#894

Merged
thymikee merged 6 commits into
mainfrom
refactor/type-safety-foundation
Jun 27, 2026
Merged

refactor: type-safe recording backends + exhaustive capability gating#894
thymikee merged 6 commits into
mainfrom
refactor/type-safety-foundation

Conversation

@thymikee

@thymikee thymikee commented Jun 26, 2026

Copy link
Copy Markdown
Member

Summary

Two behaviorless type-safety improvements — the first Phase-0 step of the architecture roadmap added in plans/perfect-shape.md. Both make an invalid state a compile error instead of a latent runtime hazard, with zero runtime behavior change.

1. Parametrize RecordingBackend by recording tag (drops 5 unsound casts)

RecordingBackend<P> is now generic over the recording's platform tag, so each backend's stop() receives an already-narrowed recording. This deletes all five recording as Extract<ActiveRecording, { platform: ... }> casts — the discriminated-union-narrowing-by-cast anti-pattern — and makes a backend/tag mismatch unrepresentable.

  • start() stays wide (DaemonResponse | ActiveRecording) because a device platform doesn't map 1:1 to a recording tag (an iOS device resolves to either the ios or ios-device-runner recording).
  • Device resolution now returns a stop-less view (RecordingStartBackend); stop is dispatched per active recording via a new exhaustive stopActiveRecording(), replacing resolveRecordingBackendForRecording().

2. Make capability platform selection exhaustive

isCommandSupportedOnDevice resolved the per-platform capability bucket with an if/else ladder whose final branch funneled every unmatched platform into capability.web. That silently absorbs a future Platform with no compile error. Replaced with selectCapabilityForPlatform() — an exhaustive switch over the Platform union with a never guard — so adding a platform is a compile error here instead of a silent web mis-gate. Behavior is identical for all five current platforms (ios/macos → apple, android, linux, web).

Why now

plans/perfect-shape.md (included here) is a target-architecture roadmap. Its typed-result spine is a prerequisite for the later registry work, and these two changes are the cheapest, lowest-risk first dominoes: pure type-level wins that delete real unsoundness today.

Scope note for reviewers: this PR intentionally implements only the two behaviorless items above. The larger pieces from the roadmap (parse-at-boundary on the MCP/HTTP edge, the CommandDescriptor/PlatformPlugin registries, the import-graph inversion) are deliberately deferred to separate, independently shippable PRs — no need to relitigate the whole roadmap here.

Validation

  • pnpm typecheck — pass
  • pnpm lint (oxlint --deny-warnings) — pass
  • pnpm format:check — pass
  • Full unit project (vitest run --project unit) — 278 files / 2729 tests pass
  • Targeted record-trace + capabilities suites — 58/58 pass

Net: −10 lines, 5 unsound casts removed, 2 new compile-time exhaustiveness guards.

Roadmap / governance docs (also in this PR)

Phase-0 sits under the roadmap in plans/perfect-shape.md (already in this PR). This update aligns the docs with maintainer review:

  • docs/adr/0003-daemon-command-registry.md — amended (not reversed) with a "single-declaration / derivation model" clause. Ratifies the review caveat: the future CommandDescriptor work must compose with the daemon-registry boundary (daemon facet owned under src/daemon/, predicate interface unchanged, no leakage, one declaration per concern), never collapse daemon policy into a public registry.
  • plans/perfect-shape.md — command axis refined to facet composition honoring ADR 0003; the two shipped Phase-0 items marked; before/after diagrams added; Apple linked as the first PlatformPlugin instance.
  • plans/apple-platform-consolidation.md — new: consolidate Apple under platforms/apple with an AppleOS leaf axis (one apple Platform, not six literals). Per-OS readiness + sequencing.

These are docs-only (no source touched); the code scope of this PR is unchanged. Happy to split the docs into their own PR if preferred.

thymikee added 3 commits June 26, 2026 21:02
Captures the target architecture (two-registry thesis: CommandDescriptor +
PlatformPlugin over a clean folder DAG with a typed-result spine) and a sequenced,
strangler-fig migration path, grounded in a survey of the current codebase.

This PR implements the first two behaviorless Phase-0 items from that roadmap; the
larger registry work is deliberately deferred to later, independently shippable PRs.
RecordingBackend is now generic over the recording's platform tag, so each
backend's stop() receives an already-narrowed recording. This deletes all five
'recording as Extract<ActiveRecording, { platform: ... }>' casts — the textbook
discriminated-union-narrowing-by-cast anti-pattern — and makes a backend/tag
mismatch unrepresentable.

start() stays wide (DaemonResponse | ActiveRecording) because a device platform
does not map 1:1 to a recording tag (an iOS device resolves to either the 'ios' or
'ios-device-runner' recording). Device resolution returns a stop-less view
(RecordingStartBackend); stop is dispatched per active recording via the new
exhaustive stopActiveRecording(), replacing resolveRecordingBackendForRecording().

Behaviorless: pure type-level change, no runtime behavior change.
isCommandSupportedOnDevice resolved the per-platform capability bucket with an
if/else ladder whose final branch funneled every unmatched platform into
capability.web. That silently absorbs a future Platform with no compile error.

Replace it with selectCapabilityForPlatform(), an exhaustive switch over the
Platform union with a 'never' guard, so adding a new platform is a compile error
here instead of a silent web mis-gate. Identical behavior for all five current
platforms (ios/macos -> apple, android, linux, web).
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.4 MB 1.4 MB +216 B
JS gzip 445.2 kB 445.2 kB +45 B
npm tarball 583.2 kB 583.3 kB +49 B
npm unpacked 2.0 MB 2.0 MB +216 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 25.5 ms 25.4 ms -0.1 ms
CLI --help 44.9 ms 45.2 ms +0.3 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/9722.js +84 B +35 B
dist/src/record-trace-recording.js +132 B +10 B

@thymikee

Copy link
Copy Markdown
Member Author

Review pass on 96282aa: I do not see a blocker. Checked the changed production paths: recording start still resolves by device through a stop-less backend view, recording stop now dispatches exhaustively by the active recording tag, and capability bucket selection is now an exhaustive platform switch with identical current-platform behavior. CI is green, including typecheck, lint/format, unit, integration, smoke, Fallow, and iOS runner compatibility. Residual risk is low because this is type-level/routing cleanup with no device-facing behavior change.

thymikee added 3 commits June 27, 2026 12:20
Ratifies the PR review caveat into the ADR itself: the daemon command registry
boundary is about ownership + the predicate interface, not the physical file a trait
is typed in. A derived/projected daemon registry is permitted only if it preserves
four invariants (daemon-owned declaration, unchanged predicate interface, no leakage
into public projections, one declaration per concern enforced by types). The original
decision stands; collapsing daemon policy into a public command registry remains
forbidden.
- §2/§5.2: CommandDescriptor composes domain-owned facets (surface@commands,
  capability@core, daemon@src/daemon) and projects them — compose-with, not
  collapse-into. Adds the four ADR-0003 invariants.
- §6: mark the two shipped Phase-0 items (generic RecordingBackend<P>, exhaustive
  capability selection); link the Apple plan from Phase 3.
- §5.1: Apple as the first PlatformPlugin instance, owning an AppleOS leaf axis.
- §8: before/after diagrams for the command axis + the two-axis summary.
One 'apple' Platform with an AppleOS discriminant (ios/ipados/tvos/watchos/
visionos/macos) rather than six Platform literals (which would collide with the
cross-platform 'target' axis). Captures the 4-investigator survey: ~85% of
platforms/ios is already the OS-agnostic Apple engine; the XCTest runner already
builds ios|macos|tvos; macOS is included as a distinct AppKit leaf (already
entangled). visionOS is scoped net-new work; watchOS is an unsupported sentinel
(XCUITest can't drive it). Before/after diagrams, per-OS readiness, sequencing.
@thymikee thymikee merged commit 93d5275 into main Jun 27, 2026
20 checks passed
@thymikee thymikee deleted the refactor/type-safety-foundation branch June 27, 2026 10:33
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-06-27 10:33 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant