diff --git a/App/AppDelegate.swift b/App/AppDelegate.swift index dcaa730..439ea5a 100644 --- a/App/AppDelegate.swift +++ b/App/AppDelegate.swift @@ -740,10 +740,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate { // 4.5 Bus wiring — construct the `WebviewBridge` around the HUD's // `WKWebView`, install the `WKUserScript` at document-start in - // the isolated `JarvisBusWorld`, start the single-writer - // `HudStateCoordinator`, and load the R3F bundle's - // `index.html` so the handshake kicks off. See `installBus` - // for the seven-step sequence. + // the page world (the shared `WKContentWorld.page` used by + // Injection.js and the HUD bundle alike since `fb41c5f`), + // start the single-writer `HudStateCoordinator`, and load the + // R3F bundle's `index.html` so the handshake kicks off. See + // `installBus` for the seven-step sequence. installBus() // 5. Keychain probe. Banner suppression: when the API key is diff --git a/CLAUDE.md b/CLAUDE.md index 489b8fe..f6f3a97 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -209,6 +209,7 @@ bash scripts/check-corpus-secrets.sh bash scripts/check-embedding-dim-literal.sh bash scripts/check-install-order.sh bash scripts/check-no-evaluate-javascript.sh +bash scripts/check-no-jarvis-bus-world-references.sh bash scripts/check-no-leftover-stubs.sh bash scripts/check-no-modal-presentation.sh bash scripts/check-no-null-voice-adapters.sh diff --git a/packages/Bus/Sources/Bus/Resources/Injection.js b/packages/Bus/Sources/Bus/Resources/Injection.js index ee0cd3a..90d85b3 100644 --- a/packages/Bus/Sources/Bus/Resources/Injection.js +++ b/packages/Bus/Sources/Bus/Resources/Injection.js @@ -1,6 +1,7 @@ -// Injected at document-start in WKContentWorld("JarvisBusWorld"). -// Installs window.jarvisBus with the minimum surface needed to complete -// the handshake. +// Injected at document-start in the page world (the default +// `WKContentWorld.page` since `fb41c5f` retired the prior isolated named +// world; see WebviewBridge.swift). Installs window.jarvisBus with the +// minimum surface needed to complete the handshake. // // IMPORTANT — protocolVersion drift hazard: the full TS bundle's installer // (Vy() in the minified output) shape-checks window.jarvisBus and SKIPS diff --git a/packages/Bus/Sources/Bus/WebviewBridge.swift b/packages/Bus/Sources/Bus/WebviewBridge.swift index f695613..50d25b3 100644 --- a/packages/Bus/Sources/Bus/WebviewBridge.swift +++ b/packages/Bus/Sources/Bus/WebviewBridge.swift @@ -6,8 +6,9 @@ import JarvisLogging /// Main-actor-bound bridge between Swift and the webview's JS layer. /// /// Responsibilities: -/// - Install a `WKScriptMessageHandlerWithReply` in an isolated content world -/// (`JarvisBusWorld`) so page scripts cannot observe or post to it. +/// - Install a `WKScriptMessageHandlerWithReply` on the page world +/// (`WKContentWorld.page`, the default since `fb41c5f`) so page scripts +/// in the loaded HUD bundle can address it via `window.webkit.messageHandlers.jarvisBus`. /// - Inject a `WKUserScript` at `.atDocumentStart` in the same world so /// `window.jarvisBus` exists before any page script runs (pitfall 3). /// - Decode inbound JSON into `BusInbound` and either (a) handle the handshake diff --git a/packages/Bus/Tests/BusTests/RealWKWebViewIntegrationTests.swift b/packages/Bus/Tests/BusTests/RealWKWebViewIntegrationTests.swift index 97ad7c0..fb34f66 100644 --- a/packages/Bus/Tests/BusTests/RealWKWebViewIntegrationTests.swift +++ b/packages/Bus/Tests/BusTests/RealWKWebViewIntegrationTests.swift @@ -15,7 +15,7 @@ import WebKit /// **Why this exists** — Phase D-Phase-2 / Phase F1 of /// `.planning/AUDIT-AND-FIX-PLAN.md`. The 2026-05-02 audit-and-fix /// session uncovered a `WKContentWorld` isolation bug -/// (`Injection.js` ran in `WKContentWorld(name: "JarvisBusWorld")` +/// (`Injection.js` ran in a prior isolated named content world /// while the bundle ran in the page world; `window.jarvisBus` /// properties were per-world isolated; bundle's `attachBus()` saw /// `undefined` and created a SECOND fresh bus object; two stubs, two @@ -127,13 +127,13 @@ final class RealWKWebViewIntegrationTests: XCTestCase { /// **The test that would have caught the WKContentWorld bug.** /// - /// On the bug branch, `Injection.js` ran in - /// `WKContentWorld(name: "JarvisBusWorld")` while - /// `WebviewBridge.sendRaw` invoked `callAsyncJavaScript` against - /// the same named world — but the message *handler* was registered - /// against `WKContentWorld.page`, so JS-side `postMessage` calls - /// from the named-world stub never reached Swift. Handshake hung - /// at `.sentHello`, eventually timed out with an `NSAlert`. + /// On the bug branch, `Injection.js` ran in the prior named + /// content world while `WebviewBridge.sendRaw` invoked + /// `callAsyncJavaScript` against the same named world — but the + /// message *handler* was registered against `WKContentWorld.page`, + /// so JS-side `postMessage` calls from the named-world stub never + /// reached Swift. Handshake hung at `.sentHello`, eventually timed + /// out with an `NSAlert`. /// /// On the fixed branch (fb41c5f), Injection.js auto-acks `hello` /// at the stub layer in the unified `.page` world, the bridge's diff --git a/packages/Bus/Tests/BusTests/WebviewBridgeOutboundTests.swift b/packages/Bus/Tests/BusTests/WebviewBridgeOutboundTests.swift index 8730914..f5b9474 100644 --- a/packages/Bus/Tests/BusTests/WebviewBridgeOutboundTests.swift +++ b/packages/Bus/Tests/BusTests/WebviewBridgeOutboundTests.swift @@ -78,8 +78,8 @@ final class WebviewBridgeOutboundTests: XCTestCase { .decode(BusOutbound.self, from: Data((payload ?? "").utf8)) XCTAssertEqual(decoded, .hudState(.idle)) // F-A2-01: production WebviewBridge uses `WKContentWorld.page` (the - // default since `fb41c5f`); the prior `JarvisBusWorld` named-world - // assertion was retired in that commit but this test wasn't updated. + // default since `fb41c5f`); the prior named-world assertion was + // retired in that commit but this test wasn't updated until #45. XCTAssertEqual(call.contentWorld, .page) } diff --git a/packages/Bus/Tests/BusTests/WebviewBridgeTests.swift b/packages/Bus/Tests/BusTests/WebviewBridgeTests.swift index a63b494..2e7839c 100644 --- a/packages/Bus/Tests/BusTests/WebviewBridgeTests.swift +++ b/packages/Bus/Tests/BusTests/WebviewBridgeTests.swift @@ -160,11 +160,14 @@ final class WebviewBridgeTests: XCTestCase { // Re-adding a handler with the same (contentWorld, name) pair would // raise `NSInvalidArgumentException`. The constructor succeeding is // itself evidence of registration. As a double-check, `remove` on a - // registered handler must succeed without throwing. + // registered handler must succeed without throwing. We remove from + // `WKContentWorld.page` because that is the default world + // `WebviewBridge.init` registers the handler against since `fb41c5f` + // unified bus and bundle into the same page world. let controller = webView.configuration.userContentController controller.removeScriptMessageHandler( forName: "jarvisBus", - contentWorld: WKContentWorld.world(name: "JarvisBusWorld") + contentWorld: .page ) // No XCTAssert needed — the remove call succeeding on a registered // handler is the assertion. diff --git a/packages/Memory/Sources/Memory/MemoryStore.swift b/packages/Memory/Sources/Memory/MemoryStore.swift index 9c849e4..f2b9c86 100644 --- a/packages/Memory/Sources/Memory/MemoryStore.swift +++ b/packages/Memory/Sources/Memory/MemoryStore.swift @@ -12,7 +12,7 @@ import Replay /// Test seam over `Replay.ReplayLog`. The Memory module deliberately does NOT /// depend on a concrete `ReplayLog` instance — `AppDelegate.installMemory` -/// (Plan 07-06) hands a wrapper through `setReplayLog`. Tests inject a +/// hands a wrapper through `setReplayLog` at startup. Tests inject a /// mock conforming to this protocol. public protocol MemoryReplaySink: Sendable { func record(_ event: ReplayEvent, forTriggerTurnId triggerTurnId: Int64) @@ -34,8 +34,10 @@ public protocol MemoryReplaySink: Sendable { /// memory store the app refuses to launch. This eliminates the silent /// dormancy that hid B-02 / B-04-class substrate failures. /// -/// The actor is the only writer. Plan 07-02 wires the bounded extraction -/// channel; Plan 07-03 wires the search path + ReplayEvent emission. +/// The actor is the only writer. The bounded extraction channel writes +/// here via `applyOp`; the hybrid (FTS5 + vec0) search path reads through +/// `MemoryReadStore` conformance and emits `ReplayEvent.memoryRetrieval` +/// via `recordRetrieval`. public actor MemoryStore { public static let logChannel = "memory" @@ -331,11 +333,11 @@ public actor MemoryStore { sink.record(.memoryMutation(bytes), forTriggerTurnId: triggerTurnId) } - // MARK: - Read path (Plan 07-03 — MEM-07 + TEXT-03 + D-02 + D-05) + // MARK: - Read path (MEM-07 + TEXT-03 + D-02 + D-05) /// SOLE production emission site for ReplayEvent.memoryRetrieval (D-05). - /// Mirrors recordMemoryMutation from 07-02. Best-effort; if no replay - /// sink has been injected, the call is a silent no-op. + /// Mirrors `recordMemoryMutation` on the write path. Best-effort; if no + /// replay sink has been injected, the call is a silent no-op. public func recordRetrieval(_ ref: FactRef, triggerTurnId: Int64) { guard let sink = replayLog else { return } let stamp: Int64 = ref.timestamp == 0 @@ -538,13 +540,13 @@ public actor MemoryStore { } } -// MARK: - HybridSearch + SessionHistory conformance (Plan 07-03 Task 2) +// MARK: - HybridSearch + SessionHistory conformance extension MemoryStore: MemoryReadStore {} extension MemoryStore: SessionHistoryReading {} extension OllamaEmbeddingClient: EmbeddingProviding {} -// MARK: - Plan 07-06 / Regression corpus test seams +// MARK: - Regression corpus test seams // // These small read-only seams are exercised exclusively by the env-gated // MemoryRegressionCorpusTests suite (JARVIS_REAL_MODELS=1). They wrap raw diff --git a/scripts/check-no-jarvis-bus-world-references.sh b/scripts/check-no-jarvis-bus-world-references.sh new file mode 100755 index 0000000..466f734 --- /dev/null +++ b/scripts/check-no-jarvis-bus-world-references.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# +# check-no-jarvis-bus-world-references.sh +# +# Issue #45 boundary gate. +# +# The named `WKContentWorld("JarvisBusWorld")` was retired in `fb41c5f` — +# the bus message handler and `Injection.js` user script now both register +# against `WKContentWorld.page` (the default page world). Old doc-comments +# and test fixtures hung onto the name for months afterward; #45 closed +# the cleanup. +# +# This gate prevents new code or comments from re-introducing the literal +# string `JarvisBusWorld` outside the historical reference corpora: +# +# - `.planning/` archived plans / audit reports / handoffs +# - `docs/` spec documents that explain the migration +# - `node_modules/` vendored deps (out of repo control anyway) +# - `.build/`, build/ SwiftPM + Xcode derived data +# - `webview/packages/*/dist/` TypeScript build output (regenerated from src) +# - this script itself we have to reference the literal to grep for it +# - `.git/` git internals +# +# Fails CI if any other tracked file contains the literal string. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(dirname "$SCRIPT_DIR")" + +cd "$REPO_ROOT" + +# Use `git grep` so we automatically respect .gitignore (so dist/ etc. that +# aren't tracked don't trip the gate even if they exist on disk locally). +# `--cached` would miss new working-tree files; default mode greps the +# working tree filtered by tracked-or-untracked-not-ignored, which is what +# we want before a commit. +matches=$(git grep --no-color -n "JarvisBusWorld" -- \ + ':!.planning/' \ + ':!docs/' \ + ':!scripts/check-no-jarvis-bus-world-references.sh' \ + ':!webview/packages/*/dist/' \ + || true) + +if [ -n "$matches" ]; then + echo "[check-no-jarvis-bus-world-references] FAIL: tracked source still references the retired 'JarvisBusWorld' named content world." >&2 + echo "" >&2 + echo "$matches" >&2 + echo "" >&2 + echo "[check-no-jarvis-bus-world-references] Rewrite these to 'page world' / 'shared world' / 'defensive double-install guard' — see #45 and the JSDoc in webview/packages/bus/src/bridge.ts for the canonical phrasing." >&2 + exit 1 +fi + +echo "[check-no-jarvis-bus-world-references] PASS (no tracked source references the retired named world)" diff --git a/webview/packages/bus/package.json b/webview/packages/bus/package.json index 31377a8..6769ddf 100644 --- a/webview/packages/bus/package.json +++ b/webview/packages/bus/package.json @@ -1,7 +1,7 @@ { "name": "@jarvis/bus", "private": true, - "version": "0.0.0", + "version": "0.0.1", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/webview/packages/bus/src/bridge.ts b/webview/packages/bus/src/bridge.ts index b633baf..9dbdcb5 100644 --- a/webview/packages/bus/src/bridge.ts +++ b/webview/packages/bus/src/bridge.ts @@ -57,27 +57,28 @@ export interface InstallOptions { * - `send(msg)` JSON-encodes and posts to `webkit.messageHandlers.jarvisBus`. * Callers await the returned Promise to observe Swift's reply. * - * ## WKContentWorld attach-to-existing path (H-02) + * ## Defensive double-install guard (H-02) * - * When the HUD bundle runs in the page's default JS world (via - * `