diff --git a/.gitattributes b/.gitattributes
index 82edc76..6ed67a5 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -23,3 +23,5 @@ docs/**/*.mdx linguist-detectable=true linguist-language=MDX
.agents/** linguist-detectable=false linguist-generated
.claude/** linguist-detectable=false linguist-generated
+
+*.js
diff --git a/apps/native-macos/.swiftformat b/apps/native-macos/.swiftformat
index b407da3..6e311fb 100644
--- a/apps/native-macos/.swiftformat
+++ b/apps/native-macos/.swiftformat
@@ -5,3 +5,4 @@
--importgrouping testable-last
--exclude .build
--disable swiftTestingTestCaseNames
+--disable trailingCommas
diff --git a/apps/native-macos/Scripts/entitlements/spike-allow-jit.plist b/apps/native-macos/Scripts/entitlements/spike-allow-jit.plist
new file mode 100644
index 0000000..4efe1ce
--- /dev/null
+++ b/apps/native-macos/Scripts/entitlements/spike-allow-jit.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ com.apple.security.cs.allow-jit
+
+
+
diff --git a/apps/native-macos/Scripts/entitlements/spike-allow-unsigned-executable-memory.plist b/apps/native-macos/Scripts/entitlements/spike-allow-unsigned-executable-memory.plist
new file mode 100644
index 0000000..46f6756
--- /dev/null
+++ b/apps/native-macos/Scripts/entitlements/spike-allow-unsigned-executable-memory.plist
@@ -0,0 +1,12 @@
+
+
+
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+ com.apple.security.cs.disable-library-validation
+
+
+
diff --git a/apps/native-macos/Scripts/entitlements/spike-baseline.plist b/apps/native-macos/Scripts/entitlements/spike-baseline.plist
new file mode 100644
index 0000000..6631ffa
--- /dev/null
+++ b/apps/native-macos/Scripts/entitlements/spike-baseline.plist
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/apps/native-macos/Scripts/hardened-runtime-spike.sh b/apps/native-macos/Scripts/hardened-runtime-spike.sh
new file mode 100755
index 0000000..a52bbfa
--- /dev/null
+++ b/apps/native-macos/Scripts/hardened-runtime-spike.sh
@@ -0,0 +1,56 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PACKAGE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+BUILD_DIR="$PACKAGE_DIR/.build/release"
+APP_BUNDLE="$PACKAGE_DIR/.build/spike/HardenedRuntimeSpike.app"
+ENTITLEMENTS_DIR="$SCRIPT_DIR/entitlements"
+
+echo "==> Building release binary"
+swift build -c release --package-path "$PACKAGE_DIR"
+
+echo "==> Assembling minimal .app bundle"
+rm -rf "$APP_BUNDLE"
+mkdir -p "$APP_BUNDLE/Contents/MacOS"
+cp "$BUILD_DIR/App" "$APP_BUNDLE/Contents/MacOS/App"
+
+cat > "$APP_BUNDLE/Contents/Info.plist" <<'PLIST'
+
+
+
+
+ CFBundleExecutable
+ App
+ CFBundleIdentifier
+ dev.mc-vector.native.spike.hardened-runtime
+ CFBundlePackageType
+ APPL
+
+
+PLIST
+
+declare -a RESULTS=()
+
+for plist in "$ENTITLEMENTS_DIR"/spike-*.plist; do
+ name="$(basename "$plist" .plist)"
+ echo "==> Signing with entitlements: $name"
+ codesign --force --options runtime -s - --entitlements "$plist" "$APP_BUNDLE"
+
+ echo "==> Running (MCV_SPIKE=hardened-runtime-java) with $name"
+ set +e
+ OUTPUT="$(MCV_SPIKE=hardened-runtime-java "$APP_BUNDLE/Contents/MacOS/App" 2>&1)"
+ STATUS=$?
+ set -e
+
+ echo "--- $name (exit=$STATUS) ---"
+ echo "$OUTPUT"
+ echo "---------------------------"
+ RESULTS+=("$name: exit=$STATUS")
+done
+
+echo ""
+echo "==> Summary"
+for line in "${RESULTS[@]}"; do
+ echo "$line"
+done
diff --git a/apps/native-macos/Sources/App/Main.swift b/apps/native-macos/Sources/App/Main.swift
index 6f311bf..dabb16f 100644
--- a/apps/native-macos/Sources/App/Main.swift
+++ b/apps/native-macos/Sources/App/Main.swift
@@ -1,8 +1,49 @@
import Core
+import Foundation
@main
struct Main {
- static func main() {
- print("MC-Vector Native starting…")
+ static func main() async {
+ guard let spike = ProcessInfo.processInfo.environment["MCV_SPIKE"] else {
+ print("MC-Vector Native starting…")
+ return
+ }
+
+ switch spike {
+ case "hardened-runtime-java":
+ await self.runHardenedRuntimeJavaSpike()
+ case "panel-nspanel":
+ PanelSpikeRunner.runNSPanelBridge()
+ case "panel-window":
+ PanelSpikeRunner.runSwiftUIWindowLevel()
+ case "log-stream":
+ PanelSpikeRunner.runLogStreamSpike()
+ default:
+ print("Unknown MCV_SPIKE value: \(spike)")
+ }
+ }
+
+ static func runHardenedRuntimeJavaSpike() async {
+ let javaCandidates = [
+ "/opt/homebrew/opt/openjdk/bin/java",
+ "/usr/bin/java"
+ ]
+ guard let javaPath = javaCandidates.first(where: { FileManager.default.isExecutableFile(atPath: $0) }) else {
+ print("java executable not found in known locations")
+ return
+ }
+
+ let harness = JavaLaunchHarness()
+ do {
+ let result = try await harness.launch(
+ executableURL: URL(fileURLWithPath: javaPath),
+ arguments: ["-version"],
+ )
+ print("exitCode=\(result.terminationStatus)")
+ print("stdout=\(result.standardOutput)")
+ print("stderr=\(result.standardError)")
+ } catch {
+ print("launch failed: \(error)")
+ }
}
}
diff --git a/apps/native-macos/Sources/App/Spikes/PanelSpikeRunner.swift b/apps/native-macos/Sources/App/Spikes/PanelSpikeRunner.swift
new file mode 100644
index 0000000..05abae8
--- /dev/null
+++ b/apps/native-macos/Sources/App/Spikes/PanelSpikeRunner.swift
@@ -0,0 +1,22 @@
+import AppKit
+import Core
+
+@MainActor
+enum PanelSpikeRunner {
+ static func runNSPanelBridge() {
+ let app = NSApplication.shared
+ app.setActivationPolicy(.accessory)
+ let panel = NonactivatingGlassPanel()
+ panel.center()
+ panel.makeKeyAndOrderFront(nil)
+ app.run()
+ }
+
+ static func runSwiftUIWindowLevel() {
+ SwiftUIWindowLevelSpike.main()
+ }
+
+ static func runLogStreamSpike() {
+ LogStreamSpikeApp.main()
+ }
+}
diff --git a/apps/native-macos/Sources/Core/Spikes/JavaLaunchHarness.swift b/apps/native-macos/Sources/Core/Spikes/JavaLaunchHarness.swift
new file mode 100644
index 0000000..9abbb9c
--- /dev/null
+++ b/apps/native-macos/Sources/Core/Spikes/JavaLaunchHarness.swift
@@ -0,0 +1,62 @@
+import Foundation
+
+public struct JavaLaunchResult: Sendable, Equatable {
+ public let terminationStatus: Int32
+ public let standardOutput: String
+ public let standardError: String
+
+ public init(terminationStatus: Int32, standardOutput: String, standardError: String) {
+ self.terminationStatus = terminationStatus
+ self.standardOutput = standardOutput
+ self.standardError = standardError
+ }
+}
+
+public enum JavaLaunchError: Error, Sendable {
+ case executableNotFound(URL)
+}
+
+public actor JavaLaunchHarness {
+ public init() {}
+
+ public func launch(executableURL: URL, arguments: [String]) async throws -> JavaLaunchResult {
+ guard FileManager.default.isExecutableFile(atPath: executableURL.path) else {
+ throw JavaLaunchError.executableNotFound(executableURL)
+ }
+
+ let process = Process()
+ process.executableURL = executableURL
+ process.arguments = arguments
+
+ let stdoutPipe = Pipe()
+ let stderrPipe = Pipe()
+ process.standardOutput = stdoutPipe
+ process.standardError = stderrPipe
+
+ try process.run()
+
+ // Read both pipes concurrently: draining stdout fully before starting
+ // on stderr (or vice versa) deadlocks once a child fills the OS pipe
+ // buffer on the pipe being read second while the other is still full.
+ async let stdoutData = stdoutPipe.fileHandleForReading.readToEndCompat()
+ async let stderrData = stderrPipe.fileHandleForReading.readToEndCompat()
+ let (stdoutBytes, stderrBytes) = try await (stdoutData, stderrData)
+
+ process.waitUntilExit()
+
+ return JavaLaunchResult(
+ terminationStatus: process.terminationStatus,
+ standardOutput: String(bytes: stdoutBytes, encoding: .utf8) ?? "",
+ standardError: String(bytes: stderrBytes, encoding: .utf8) ?? "",
+ )
+ }
+}
+
+private extension FileHandle {
+ func readToEndCompat() throws -> Data {
+ if let data = try self.readToEnd() {
+ return data
+ }
+ return Data()
+ }
+}
diff --git a/apps/native-macos/Sources/Core/Spikes/LogSpike/DummyLogGenerator.swift b/apps/native-macos/Sources/Core/Spikes/LogSpike/DummyLogGenerator.swift
new file mode 100644
index 0000000..763ea40
--- /dev/null
+++ b/apps/native-macos/Sources/Core/Spikes/LogSpike/DummyLogGenerator.swift
@@ -0,0 +1,40 @@
+import Foundation
+
+public struct LogLine: Sendable, Identifiable, Equatable {
+ public let id: Int
+ public let timestamp: ContinuousClock.Instant
+ public let text: String
+
+ public init(id: Int, timestamp: ContinuousClock.Instant, text: String) {
+ self.id = id
+ self.timestamp = timestamp
+ self.text = text
+ }
+}
+
+public actor DummyLogGenerator {
+ public init() {}
+
+ public func stream(linesPerSecond: Int) -> AsyncStream {
+ let interval = Duration.seconds(1) / Double(max(linesPerSecond, 1))
+
+ return AsyncStream { continuation in
+ let task = Task {
+ var index = 0
+ let clock = ContinuousClock()
+ while !Task.isCancelled {
+ index += 1
+ continuation.yield(
+ LogLine(id: index, timestamp: clock.now, text: "[spike] log line #\(index)"),
+ )
+ try? await Task.sleep(for: interval)
+ }
+ continuation.finish()
+ }
+
+ continuation.onTermination = { _ in
+ task.cancel()
+ }
+ }
+ }
+}
diff --git a/apps/native-macos/Sources/Core/Spikes/LogSpike/LogBatcher.swift b/apps/native-macos/Sources/Core/Spikes/LogSpike/LogBatcher.swift
new file mode 100644
index 0000000..18a71e0
--- /dev/null
+++ b/apps/native-macos/Sources/Core/Spikes/LogSpike/LogBatcher.swift
@@ -0,0 +1,35 @@
+import Foundation
+
+public struct LogBatcher: Sendable {
+ private let interval: Duration
+
+ public init(interval: Duration) {
+ self.interval = interval
+ }
+
+ /// Groups `lines` into batches such that every line whose timestamp falls
+ /// within the same `interval`-sized window (measured from the first
+ /// line's timestamp) lands in the same batch, in arrival order.
+ public func batch(_ lines: [LogLine]) -> [[LogLine]] {
+ guard let first = lines.first else { return [] }
+
+ var batches: [[LogLine]] = []
+ var currentBatch: [LogLine] = []
+ var windowStart = first.timestamp
+
+ for line in lines {
+ if line.timestamp - windowStart >= self.interval, !currentBatch.isEmpty {
+ batches.append(currentBatch)
+ currentBatch = []
+ windowStart = line.timestamp
+ }
+ currentBatch.append(line)
+ }
+
+ if !currentBatch.isEmpty {
+ batches.append(currentBatch)
+ }
+
+ return batches
+ }
+}
diff --git a/apps/native-macos/Sources/Core/Spikes/LogSpike/LogLineBuffer.swift b/apps/native-macos/Sources/Core/Spikes/LogSpike/LogLineBuffer.swift
new file mode 100644
index 0000000..de3d7d8
--- /dev/null
+++ b/apps/native-macos/Sources/Core/Spikes/LogSpike/LogLineBuffer.swift
@@ -0,0 +1,22 @@
+/// A fixed-capacity buffer that trims with hysteresis: it only shifts the
+/// backing array once the overshoot allowance is exceeded, rather than on
+/// every single append. Trimming to exactly `retainedLineCount` on every
+/// append would make `Array.removeFirst(_:)` the dominant cost at
+/// high ingest rates, masking the rendering cost this spike exists to measure.
+public struct LogLineBuffer {
+ public private(set) var lines: [LogLine] = []
+ private let retainedLineCount: Int
+ private let trimOvershoot: Int
+
+ public init(retainedLineCount: Int, trimOvershoot: Int) {
+ self.retainedLineCount = retainedLineCount
+ self.trimOvershoot = trimOvershoot
+ }
+
+ public mutating func append(_ line: LogLine) {
+ self.lines.append(line)
+ if self.lines.count > self.retainedLineCount + self.trimOvershoot {
+ self.lines.removeFirst(self.lines.count - self.retainedLineCount)
+ }
+ }
+}
diff --git a/apps/native-macos/Sources/Core/Spikes/LogSpike/LogStreamSpikeView.swift b/apps/native-macos/Sources/Core/Spikes/LogSpike/LogStreamSpikeView.swift
new file mode 100644
index 0000000..c77e2cb
--- /dev/null
+++ b/apps/native-macos/Sources/Core/Spikes/LogSpike/LogStreamSpikeView.swift
@@ -0,0 +1,81 @@
+import Foundation
+import SwiftUI
+
+public struct LogStreamListView: View {
+ @State private var buffer: LogLineBuffer
+ private let generator = DummyLogGenerator()
+ private let linesPerSecond: Int
+
+ public init(linesPerSecond: Int = 1000, retainedLineCount: Int = 5000, trimOvershoot: Int = 500) {
+ self.linesPerSecond = linesPerSecond
+ self._buffer = State(
+ initialValue: LogLineBuffer(retainedLineCount: retainedLineCount, trimOvershoot: trimOvershoot),
+ )
+ }
+
+ public var body: some View {
+ List(self.buffer.lines) { line in
+ Text(line.text)
+ .font(.system(.caption, design: .monospaced))
+ }
+ .task {
+ for await line in await self.generator.stream(linesPerSecond: self.linesPerSecond) {
+ self.buffer.append(line)
+ }
+ }
+ }
+}
+
+public struct LogStreamScrollView: View {
+ @State private var buffer: LogLineBuffer
+ private let generator = DummyLogGenerator()
+ private let linesPerSecond: Int
+
+ public init(linesPerSecond: Int = 1000, retainedLineCount: Int = 5000, trimOvershoot: Int = 500) {
+ self.linesPerSecond = linesPerSecond
+ self._buffer = State(
+ initialValue: LogLineBuffer(retainedLineCount: retainedLineCount, trimOvershoot: trimOvershoot),
+ )
+ }
+
+ public var body: some View {
+ ScrollView {
+ LazyVStack(alignment: .leading) {
+ ForEach(self.buffer.lines) { line in
+ Text(line.text)
+ .font(.system(.caption, design: .monospaced))
+ }
+ }
+ }
+ .task {
+ for await line in await self.generator.stream(linesPerSecond: self.linesPerSecond) {
+ self.buffer.append(line)
+ }
+ }
+ }
+}
+
+public struct LogStreamSpikeApp: App {
+ public enum Variant: String, Sendable {
+ case list
+ case scroll
+ }
+
+ private let variant: Variant
+
+ public init() {
+ let raw = ProcessInfo.processInfo.environment["MCV_LOG_SPIKE_VARIANT"] ?? Variant.list.rawValue
+ self.variant = Variant(rawValue: raw) ?? .list
+ }
+
+ public var body: some Scene {
+ WindowGroup("Log Stream Spike") {
+ switch self.variant {
+ case .list:
+ LogStreamListView()
+ case .scroll:
+ LogStreamScrollView()
+ }
+ }
+ }
+}
diff --git a/apps/native-macos/Sources/Core/Spikes/PanelSpike/GlassSpikeContent.swift b/apps/native-macos/Sources/Core/Spikes/PanelSpike/GlassSpikeContent.swift
new file mode 100644
index 0000000..742583b
--- /dev/null
+++ b/apps/native-macos/Sources/Core/Spikes/PanelSpike/GlassSpikeContent.swift
@@ -0,0 +1,26 @@
+import SwiftUI
+
+public struct GlassSpikeContent: View {
+ private let title: String
+
+ public init(title: String) {
+ self.title = title
+ }
+
+ public var body: some View {
+ VStack(spacing: 12) {
+ Text(self.title)
+ .font(.headline)
+ Text("Move focus to another app and observe this panel.")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ .padding(24)
+ .frame(width: 320, height: 160)
+ .glassEffect(.regular, in: .rect(cornerRadius: 16))
+ }
+}
+
+#Preview {
+ GlassSpikeContent(title: "NSPanel bridge")
+}
diff --git a/apps/native-macos/Sources/Core/Spikes/PanelSpike/NonactivatingGlassPanel.swift b/apps/native-macos/Sources/Core/Spikes/PanelSpike/NonactivatingGlassPanel.swift
new file mode 100644
index 0000000..a7e9e02
--- /dev/null
+++ b/apps/native-macos/Sources/Core/Spikes/PanelSpike/NonactivatingGlassPanel.swift
@@ -0,0 +1,21 @@
+import AppKit
+import SwiftUI
+
+@MainActor
+public final class NonactivatingGlassPanel: NSPanel {
+ public init() {
+ super.init(
+ contentRect: NSRect(x: 0, y: 0, width: 320, height: 160),
+ styleMask: [.nonactivatingPanel, .titled, .resizable, .closable],
+ backing: .buffered,
+ defer: false,
+ )
+
+ self.isFloatingPanel = true
+ self.level = .floating
+ self.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
+ self.titleVisibility = .hidden
+ self.titlebarAppearsTransparent = true
+ self.contentView = NSHostingView(rootView: GlassSpikeContent(title: "NSPanel bridge"))
+ }
+}
diff --git a/apps/native-macos/Sources/Core/Spikes/PanelSpike/SwiftUIWindowLevelSpike.swift b/apps/native-macos/Sources/Core/Spikes/PanelSpike/SwiftUIWindowLevelSpike.swift
new file mode 100644
index 0000000..e3b8d9b
--- /dev/null
+++ b/apps/native-macos/Sources/Core/Spikes/PanelSpike/SwiftUIWindowLevelSpike.swift
@@ -0,0 +1,12 @@
+import SwiftUI
+
+public struct SwiftUIWindowLevelSpike: App {
+ public init() {}
+
+ public var body: some Scene {
+ Window("Glass Spike", id: "glass-spike-window") {
+ GlassSpikeContent(title: "SwiftUI Window level")
+ }
+ .windowLevel(.floating)
+ }
+}
diff --git a/apps/native-macos/Tests/CoreTests/JavaLaunchHarnessTests.swift b/apps/native-macos/Tests/CoreTests/JavaLaunchHarnessTests.swift
new file mode 100644
index 0000000..f445ae1
--- /dev/null
+++ b/apps/native-macos/Tests/CoreTests/JavaLaunchHarnessTests.swift
@@ -0,0 +1,37 @@
+import Foundation
+import Testing
+@testable import Core
+
+@Test("launch succeeds and captures stdout for a trivial executable")
+func javaLaunchHarnessCapturesStdout() async throws {
+ let harness = JavaLaunchHarness()
+ let result = try await harness.launch(
+ executableURL: URL(fileURLWithPath: "/bin/echo"),
+ arguments: ["hardened-runtime-spike"],
+ )
+
+ #expect(result.terminationStatus == 0)
+ #expect(result.standardOutput.trimmingCharacters(in: .whitespacesAndNewlines) == "hardened-runtime-spike")
+ #expect(result.standardError.isEmpty)
+}
+
+@Test("launch surfaces non-zero exit codes")
+func javaLaunchHarnessSurfacesNonZeroExit() async throws {
+ let harness = JavaLaunchHarness()
+ let result = try await harness.launch(
+ executableURL: URL(fileURLWithPath: "/usr/bin/false"),
+ arguments: [],
+ )
+
+ #expect(result.terminationStatus != 0)
+}
+
+@Test("launch throws for a non-existent executable")
+func javaLaunchHarnessThrowsForMissingExecutable() async throws {
+ let harness = JavaLaunchHarness()
+ let missingURL = URL(fileURLWithPath: "/nonexistent/path/to/java")
+
+ await #expect(throws: JavaLaunchError.self) {
+ _ = try await harness.launch(executableURL: missingURL, arguments: [])
+ }
+}
diff --git a/apps/native-macos/Tests/CoreTests/LogBatcherTests.swift b/apps/native-macos/Tests/CoreTests/LogBatcherTests.swift
new file mode 100644
index 0000000..38fad45
--- /dev/null
+++ b/apps/native-macos/Tests/CoreTests/LogBatcherTests.swift
@@ -0,0 +1,71 @@
+import Foundation
+import Testing
+@testable import Core
+
+@Test("batch groups lines within the same interval window together")
+func logBatcherGroupsWithinWindow() {
+ let clock = ContinuousClock()
+ let start = clock.now
+ let lines = [
+ LogLine(id: 1, timestamp: start, text: "a"),
+ LogLine(id: 2, timestamp: start + .milliseconds(10), text: "b"),
+ LogLine(id: 3, timestamp: start + .milliseconds(20), text: "c")
+ ]
+
+ let batcher = LogBatcher(interval: .milliseconds(100))
+ let batches = batcher.batch(lines)
+
+ #expect(batches.count == 1)
+ #expect(batches.first?.count == 3)
+}
+
+@Test("batch splits lines that cross the interval boundary")
+func logBatcherSplitsAcrossWindows() {
+ let clock = ContinuousClock()
+ let start = clock.now
+ let lines = [
+ LogLine(id: 1, timestamp: start, text: "a"),
+ LogLine(id: 2, timestamp: start + .milliseconds(50), text: "b"),
+ LogLine(id: 3, timestamp: start + .milliseconds(150), text: "c"),
+ LogLine(id: 4, timestamp: start + .milliseconds(160), text: "d")
+ ]
+
+ let batcher = LogBatcher(interval: .milliseconds(100))
+ let batches = batcher.batch(lines)
+
+ #expect(batches.count == 2)
+ #expect(batches[0].map(\.id) == [1, 2])
+ #expect(batches[1].map(\.id) == [3, 4])
+}
+
+@Test("batch returns an empty array for empty input")
+func logBatcherHandlesEmptyInput() {
+ let batcher = LogBatcher(interval: .milliseconds(100))
+ #expect(batcher.batch([]).isEmpty)
+}
+
+@Test("batch returns a single batch for a single line")
+func logBatcherHandlesSingleLine() {
+ let line = LogLine(id: 1, timestamp: ContinuousClock().now, text: "a")
+ let batcher = LogBatcher(interval: .milliseconds(100))
+ let batches = batcher.batch([line])
+
+ #expect(batches.count == 1)
+ #expect(batches.first?.map(\.id) == [1])
+}
+
+@Test("batch groups lines sharing an identical timestamp")
+func logBatcherGroupsIdenticalTimestamps() {
+ let timestamp = ContinuousClock().now
+ let lines = [
+ LogLine(id: 1, timestamp: timestamp, text: "a"),
+ LogLine(id: 2, timestamp: timestamp, text: "b"),
+ LogLine(id: 3, timestamp: timestamp, text: "c")
+ ]
+
+ let batcher = LogBatcher(interval: .milliseconds(100))
+ let batches = batcher.batch(lines)
+
+ #expect(batches.count == 1)
+ #expect(batches.first?.map(\.id) == [1, 2, 3])
+}
diff --git a/apps/native-macos/Tests/CoreTests/LogLineBufferTests.swift b/apps/native-macos/Tests/CoreTests/LogLineBufferTests.swift
new file mode 100644
index 0000000..698fff6
--- /dev/null
+++ b/apps/native-macos/Tests/CoreTests/LogLineBufferTests.swift
@@ -0,0 +1,28 @@
+import Foundation
+import Testing
+@testable import Core
+
+private func makeLine(_ id: Int) -> LogLine {
+ LogLine(id: id, timestamp: ContinuousClock().now, text: "line #\(id)")
+}
+
+@Test("append retains all lines while under the overshoot threshold")
+func logLineBufferRetainsUnderThreshold() {
+ var buffer = LogLineBuffer(retainedLineCount: 5, trimOvershoot: 2)
+ for id in 1 ... 7 {
+ buffer.append(makeLine(id))
+ }
+
+ #expect(buffer.lines.count == 7)
+}
+
+@Test("append trims back down to retainedLineCount once overshoot is exceeded")
+func logLineBufferTrimsAfterOvershoot() {
+ var buffer = LogLineBuffer(retainedLineCount: 5, trimOvershoot: 2)
+ for id in 1 ... 8 {
+ buffer.append(makeLine(id))
+ }
+
+ #expect(buffer.lines.count == 5)
+ #expect(buffer.lines.map(\.id) == [4, 5, 6, 7, 8])
+}
diff --git a/apps/native-macos/Tests/CoreTests/PanelSpikeTests.swift b/apps/native-macos/Tests/CoreTests/PanelSpikeTests.swift
new file mode 100644
index 0000000..7349b05
--- /dev/null
+++ b/apps/native-macos/Tests/CoreTests/PanelSpikeTests.swift
@@ -0,0 +1,12 @@
+import Testing
+@testable import Core
+
+@MainActor
+@Test("NonactivatingGlassPanel is configured for floating, non-activating presentation")
+func nonactivatingGlassPanelConfiguration() {
+ let panel = NonactivatingGlassPanel()
+
+ #expect(panel.styleMask.contains(.nonactivatingPanel))
+ #expect(panel.isFloatingPanel)
+ #expect(panel.level == .floating)
+}
diff --git a/spec/native-macos-requirements.md b/spec/native-macos-requirements.md
index 16c1f35..f8c0d65 100644
--- a/spec/native-macos-requirements.md
+++ b/spec/native-macos-requirements.md
@@ -267,9 +267,10 @@ lint-format-swift:
| Swift側とTauri側でロジックがドリフトする | 中 | データ契約(JSONスキーマ)のみ厳密に一致させる運用ルール。機能追加時は両実装のチェックリスト化を検討 |
| `security.rs`相当の正しさが重要な処理の移植漏れ | 中 | Rust側の実装とテストケースを忠実にSwift側へ移植する運用ルール(§4.4) |
| プラグイン解決ロジックの二重実装コスト | 低(現Spike範囲外) | Native版でプラグイン管理を実装する段階で再評価 |
-| glass非アクティブ劣化 | 低 | 実機検証→Materialフォールバック(確定済み) |
-| `.xcodeproj`なしによるXcode GUI機能の一部制約(Instruments連携等) | 低 | Xcodeは`Package.swift`を直接開けるため多くの機能は利用可能。制約が顕在化したら`.xcodeproj`併用を再検討 |
+| glass非アクティブ劣化 | 低 | 実機検証→Materialフォールバック(確定済み)。Phase 3-Aでスキャフォールド実装済み、実機での目視判定は`spec/phase3a-spike-results.md`参照(検証待ち) |
+| `.xcodeproj`なしによるXcode GUI機能の一部制約(Instruments連携等) | 低 | Xcodeは`Package.swift`を直接開けるため多くの機能は利用可能。制約が顕在化したら`.xcodeproj`併用を再検討。Phase 3-Aで`record_trace.py`/`analyze_trace.py`によるCLI経由のトレース取得・解析が可能なことを確認(`spec/phase3a-spike-results.md`) |
| macOSランナーCIコスト | 低 | Lintはubuntu-latestに寄せ、ビルドのみmacos-latest |
+| Hardened Runtime下でのJavaプロセス起動可否 | 低(検証済み) | Phase 3-Aで検証: 追加entitlementsなしでも`java -version`はexit 0で起動可能。実ワークロードでの再検証は3-7(サーバー起動/停止実装)で実施(`spec/phase3a-spike-results.md`) |
## 7. 主要出典
diff --git a/spec/phase3a-spike-results.md b/spec/phase3a-spike-results.md
new file mode 100644
index 0000000..06f63c8
--- /dev/null
+++ b/spec/phase3a-spike-results.md
@@ -0,0 +1,64 @@
+# Phase 3-A: 実機検証3項目 — 結果記録
+
+> 関連: `spec/phase-tasks.md`(Phase 3-Aタスク定義), `spec/native-macos-requirements.md` §5.4/§5.5/§6
+
+このセッションはバックグラウンドジョブとして起動されておりWindowServerに接続されていないため(`screencapture`が`could not create image from display`で失敗することを確認済み)、3-1と3-3はスキャフォールド・自動テスト・解析スクリプトまでをこのセッションで用意し、実機でのスクリーンショット撮影/トレース記録をユーザーに依頼する形で進めている。3-2はGUI操作が不要なため、このセッションのみで検証まで完結した。
+
+## 3-1: NSPanel×glassEffect 非アクティブ時劣化
+
+| 項目 | 内容 |
+|---|---|
+| 検証日 | 未実施(ユーザー実機待ち) |
+| 実施環境 | ユーザー実機(macOS Tahoe 26+) |
+| 結果 | — |
+| 判定 | 未確定 |
+| 証跡パス | `apps/native-macos/spec-assets/3-1/{nspanel,window}-{active,inactive}.png`(未作成) |
+
+**実装済みスキャフォールド**: `apps/native-macos/Sources/Core/Spikes/PanelSpike/`(`GlassSpikeContent.swift`, `NonactivatingGlassPanel.swift`, `SwiftUIWindowLevelSpike.swift`)、`apps/native-macos/Sources/App/Spikes/PanelSpikeRunner.swift`。
+
+**ユーザーへの依頼**: `swift run App` を `MCV_SPIKE=panel-nspanel` と `MCV_SPIKE=panel-window` のそれぞれで起動し、他アプリへフォーカスを移して非アクティブ化した状態を含むスクリーンショットを4枚(`nspanel-active.png` / `nspanel-inactive.png` / `window-active.png` / `window-inactive.png`)撮影の上、`apps/native-macos/spec-assets/3-1/` 配下に保存してほしい。保存後、Claudeが画像を読み込み劣化度合いを比較し、本セクションと `native-macos-requirements.md` §5.4 に確定した実装方式を追記する。
+
+## 3-2: Hardened Runtime下のJavaプロセス起動
+
+| 項目 | 内容 |
+|---|---|
+| 検証日 | 2026-07-08 |
+| 実施環境 | このセッション(macOS 26 / Xcode 26.5.1 / Swift 6.3、Homebrew OpenJDK 25.0.2) |
+| 結果 | baseline(entitlements無し)/ `allow-jit` / `allow-unsigned-executable-memory`+`disable-library-validation` の3パターンいずれも `java -version` がexit code 0で成功。stderrにJVMバージョン情報が正常出力され、`Killed`やcodesign関連エラーは発生しなかった |
+| 判定 | Hardened Runtime + ad-hoc署名下でも、追加entitlementsなしでJavaサブプロセスの起動自体は可能。ただし検証は`-version`起動のみであり、JITを本格的に使う実ワークロード(サーバー起動後の稼働)でのentitlements要否は未検証 |
+| 証跡パス | `apps/native-macos/Scripts/hardened-runtime-spike.sh` 実行ログ(本ファイル上部に転記) |
+
+**実装済み**: `apps/native-macos/Sources/Core/Spikes/JavaLaunchHarness.swift`(actor化したProcessラッパー)、`apps/native-macos/Scripts/hardened-runtime-spike.sh`、`apps/native-macos/Scripts/entitlements/spike-{baseline,allow-jit,allow-unsigned-executable-memory}.plist`。
+
+**実行結果ログ(要約)**:
+
+```
+spike-allow-jit: exit=0
+spike-allow-unsigned-executable-memory: exit=0
+spike-baseline: exit=0
+```
+
+**今後の課題**: 実際のMinecraftサーバーjarを長時間稼働させた際のJIT挙動・GC・ネイティブライブラリロード(一部modが使用)まで含めた検証は3-Bのサーバー起動/停止実装(3-7)時に改めて行う。
+
+**コードレビュー指摘への対応**: 初回実装の`JavaLaunchHarness.launch`はstdout→stderrの順に`readToEnd()`を逐次実行しており、子プロセスが標準出力を閉じないまま標準エラーへ大量出力するとOSパイプバッファが埋まりデッドロックする既知の`Process`/`Pipe`アンチパターンだった(`java -version`はstderrのみの小出力のため顕在化しなかった)。3-7でこのハーネスを実サーバー起動に転用する前提のため、`async let`で両パイプを並行読み取りする実装に修正済み(`JavaLaunchHarness.swift`)。
+
+## 3-3: 高頻度ログ描画パフォーマンス
+
+| 項目 | 内容 |
+|---|---|
+| 検証日 | 未実施(ユーザー実機待ち) |
+| 実施環境 | ユーザー実機(macOS Tahoe 26+、Instruments) |
+| 結果 | — |
+| 判定 | 未確定 |
+| 証跡パス | `.trace`ファイル(未取得、パスは取得後にユーザーから共有) |
+
+**実装済みスキャフォールド**: `apps/native-macos/Sources/Core/Spikes/LogSpike/`(`DummyLogGenerator.swift`, `LogBatcher.swift`, `LogLineBuffer.swift`, `LogStreamSpikeView.swift`)。`LogBatcher`のユニットテストは`Tests/CoreTests/LogBatcherTests.swift`で完了済み(同一ウィンドウ内のグルーピング、ウィンドウ境界での分割、単一行、同一タイムスタンプ、空入力)。
+
+**コードレビュー指摘への対応**: 初回実装は`List`/`ScrollView`両方とも「1行追加するたびに`retainedLineCount`ちょうどまで`removeFirst`する」実装で、1000行/秒の負荷下では毎行O(n)の配列シフトが発生し、Instrumentsトレースが本来測定したい「List vs ScrollViewのレンダリングコスト差」ではなく「配列シフトのコスト」を支配的に示してしまう懸念があった。`LogLineBuffer`(ヒステリシス付きトリム: `retainedLineCount + trimOvershoot`を超えた時だけまとめてトリム)を切り出し両Viewで共有する実装に修正し、`Tests/CoreTests/LogLineBufferTests.swift`でトリム挙動を検証済み。
+
+**ユーザーへの依頼**: `swift run App` を `MCV_SPIKE=log-stream`(`MCV_LOG_SPIKE_VARIANT=list` または `scroll`)で起動し、`.claude/skills/swiftui-expert-skill/scripts/record_trace.py --launch --template "SwiftUI" --time-limit 30s` でトレースを取得してほしい。取得済み`.trace`はClaudeが`analyze_trace.py --trace `で解析し、ヒッチ/CPUホットスポットからバッチ化/仮想化戦略を決定する。
+
+## まとめ
+
+- 3-2は検証完了。追加entitlementsなしでもJavaサブプロセス起動自体は妨げられないことが判明し、`native-macos-requirements.md` §5.3の「entitlements要否の洗い出し」という設計課題に対する一次情報が得られた。
+- 3-1・3-3はスキャフォールド・自動テストまで完了し、実機でのみ可能な操作(スクリーンショット撮影・Instrumentsトレース記録)をユーザーに依頼した状態で本タスクを一旦クローズする。成果物が揃い次第、このファイルと`native-macos-requirements.md` §5.4/§6を追加コミットで更新する。