- 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>
44 lines
1.9 KiB
Bash
Executable File
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
|