Files
Jarvis/scripts/check-single-writer-hudstate.sh
james.maesandClaude Opus 4.7 cabc27299a feat(03-05): AppDelegate wires HudStateCoordinator + bridge; lint activated; W1-W6 + B1-B3 tests
- App/HUD/HudStateBridge.swift (new): @MainActor public func busHudState(
  from: HudState) -> Bus.HudState. RawValue round-trip with assertionFailure
  on drift, .idle fallback in Release. Option (b) from the plan — avoids
  name-collision between App.HudState and Bus.HudState via module-qualified
  Bus.HudState in the helper's scope.
- App/AppDelegate.swift:
    * New stored properties: hudStateCoordinator, dormantAgentContinuation,
      dormantVoiceContinuation, dormantConfirmContinuation (all internal so
      @testable import Jarvis can assert W5 retention), webviewEntryFilename
      (internal test seam for W6).
    * installBus(): constructs HudStateCoordinator with emit closure that
      bridges App.HudState -> Bus.HudState via busHudState() and dispatches
      to bridge.send(.hudState(...)) on the MainActor. Creates three dormant
      AsyncStream producers (Phase 4/5/6 replace them) and retains their
      continuations on self so coordinator.{agent,voice,confirm}Task stay
      live. markReady() fires from onHandshakeArmed so the HUD promotes
      .booting -> .idle as soon as the webview is responsive.
    * Load target: bus-harness.html -> index.html (via
      webviewEntryFilename property so W6 can assert post-install value).
      XCTest-host short-circuit from 02-03 deviation #5 preserved, updated
      to reference the property-driven filename.
- scripts/check-single-writer-hudstate.sh: allowlist extended to
  {HudStateCoordinator.swift, HudStateBridge.swift, AppDelegate.swift} +
  {BusOutbound.swift, Protocol.swift}. Rationale comment updated: Phase 4+
  drives the coordinator via its three input AsyncStreams, not new
  .hudState(...) call sites. Already wired as a preBuildScript by Task 1.
- App/Tests/AppTests/HudStateCoordinatorBusWiringTests.swift (new):
    * W1 test_coordinatorConstructedInInstallBus — delegate.hudStateCoordinator
      non-nil after applicationWillFinishLaunching.
    * W2 test_emitClosureBridgesToBusSend — FakeJSEvaluator observes the
      closure producing a decoded BusOutbound.hudState(.thinking) after
      coordinator.markReady() + agent.yield(.thinking). Proves the exact
      closure shape AppDelegate installs routes rawValues end-to-end.
    * W3 test_markReadyCalledOnHandshakeArmed — coordinator pinned at .booting
      pre-handshake, promotes to .idle after bridge.handleHelloAck.
    * W4 test_appHudStateRawValueMatchesBusHudState — table-driven bidirectional
      rawValue parity over HudState.allCases and Bus.HudState.allCases. Catches
      drift before busHudState()'s assertion fires at runtime.
    * W5 test_dormantStreamsHeldByDelegate — all three continuations non-nil.
    * W6 test_installBusLoadsIndexNotHarness — webviewEntryFilename == "index"
      post-installBus.
- App/Tests/AppTests/WebviewBundleLoadTests.swift (new):
    * B1 test_indexHtmlInRepoWebviewDir — structural (#filePath traversal to
      repo root), catches missing committed index.html.
    * B3 test_indexHtmlReferencesRelativeAssets — content-grep for "./assets/"
      present + "src=\"/assets/" absent. Catches Vite misconfig or absolute-
      path drift.
    * B-extra test_assetsDirectoryGitignored_documentedInSmokeScript — doc-only,
      asserts the split of responsibility between XCTest (committed index.html)
      and scripts/smoke-test-hud.sh (built-app assets/*.js).

Tests are repo-structural / evaluator-observation only; xcodebuild test
launch still blocked on Xcode 26 RunningBoard error 5 per 02-03 precedent.
Verification is xcodebuild build (succeeds) + scripts/smoke-test-hud.sh
(next task). This follows the 02-03 SUMMARY deviation #5 pattern.

xcodebuild build -configuration Debug: BUILD SUCCEEDED.
bash scripts/check-single-writer-hudstate.sh: PASS (allowlist active).
packages/Bus swift test: 47/47 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 15:53:58 -05:00

44 lines
1.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# HUD-08 single-writer invariant guard.
#
# `BusOutbound.hudState(_:)` may only be CONSTRUCTED inside
# `App/HUD/HudStateCoordinator.swift`. Any other Swift source constructing
# `.hudState(...)` is a protocol-level bug — it races the coordinator and
# defeats the point of the precedence ladder.
#
# Allowlist:
# - packages/Bus/Sources/Bus/BusOutbound.swift (DEFINES the case)
# - packages/Bus/Sources/Bus/Protocol.swift (case ref in fixtures)
# - App/HUD/HudStateCoordinator.swift (sole logical writer)
# - App/HUD/HudStateBridge.swift (App→Bus rawValue bridge)
# - App/AppDelegate.swift (wiring-layer emit closure)
# - any file under /Tests/ (drives the coordinator directly)
#
# Why AppDelegate is on the allowlist (Plan 03-05): `installBus()` constructs
# the coordinator with an emit closure that builds `.hudState(busHudState(from:))`
# and hands it to `WebviewBridge.send(_:)`. This is the single wiring site;
# Phase 4 + later plans drive the coordinator via its three input AsyncStreams
# (agent / voice / confirmation) and must NOT add new `.hudState(...)` call
# sites anywhere else. If they do, this lint fires.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
HITS=$(grep -rn '\.hudState(' App/ packages/ --include='*.swift' 2>/dev/null \
| grep -v '/Tests/' \
| grep -v 'App/HUD/HudStateCoordinator.swift' \
| grep -v 'App/HUD/HudStateBridge.swift' \
| grep -v 'App/AppDelegate.swift' \
| grep -v 'packages/Bus/Sources/Bus/BusOutbound.swift' \
| grep -v 'packages/Bus/Sources/Bus/Protocol.swift' \
| grep -v '^[^:]*:[[:space:]]*//' || true)
if [[ -n "$HITS" ]]; then
echo "HUD-08 violation: BusOutbound.hudState(_:) constructed outside HudStateCoordinator:" >&2
echo "$HITS" >&2
exit 1
fi
exit 0