diff --git a/.github/workflows/shared-openclawkit-periphery.yml b/.github/workflows/shared-openclawkit-periphery.yml new file mode 100644 index 000000000000..57a72413d915 --- /dev/null +++ b/.github/workflows/shared-openclawkit-periphery.yml @@ -0,0 +1,263 @@ +name: Shared OpenClawKit Periphery + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] + workflow_dispatch: + +concurrency: + group: shared-openclawkit-periphery-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + +permissions: + contents: read + pull-requests: read + +jobs: + scope: + name: Detect shared OpenClawKit scan scope + runs-on: ubuntu-24.04 + outputs: + should-scan: ${{ steps.scope.outputs.should-scan }} + steps: + - name: Detect changed paths + id: scope + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + if (context.eventName === "workflow_dispatch") { + core.setOutput("should-scan", "true"); + return; + } + if (context.payload.pull_request?.draft) { + core.setOutput("should-scan", "false"); + return; + } + + const files = await github.paginate(github.rest.pulls.listFiles, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + per_page: 100, + }); + const isScanPath = (filename) => + typeof filename === "string" && ( + filename.startsWith("apps/ios/") || + filename.startsWith("apps/macos/") || + filename.startsWith("apps/shared/OpenClawKit/") || + filename === ".github/workflows/shared-openclawkit-periphery.yml" || + filename === "scripts/periphery-intersection.mjs" || + filename === "scripts/ios-configure-signing.sh" || + filename === "scripts/ios-write-swift-filelist.mjs" || + filename === "scripts/ios-write-version-xcconfig.sh" || + filename === "test/scripts/periphery-intersection.test.ts" + ); + const shouldScan = files.some( + ({ filename, previous_filename: previousFilename }) => + isScanPath(filename) || isScanPath(previousFilename) + ); + core.setOutput("should-scan", String(shouldScan)); + + scan-ios: + name: Scan shared kit from iOS + needs: scope + if: ${{ needs.scope.outputs.should-scan == 'true' }} + runs-on: ${{ github.event_name == 'workflow_dispatch' && 'macos-26' || (github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-26' || 'macos-26') }} + timeout-minutes: 45 + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + fetch-depth: 1 + fetch-tags: false + persist-credentials: false + submodules: false + + - name: Verify Xcode + run: | + set -euo pipefail + for xcode_app in /Applications/Xcode_26.5.app /Applications/Xcode-26.5.0.app; do + if [ -d "$xcode_app/Contents/Developer" ]; then + sudo xcode-select -s "$xcode_app/Contents/Developer" + break + fi + done + xcodebuild -version + xcode_version="$(xcodebuild -version | awk 'NR == 1 { print $2 }')" + if [[ "$xcode_version" != 26.* ]]; then + echo "error: expected Xcode 26.x, got $xcode_version" >&2 + exit 1 + fi + swift --version + + - name: Setup Node environment + uses: ./.github/actions/setup-node-env + with: + install-bun: "false" + + - name: Install iOS scan tooling + run: | + brew update + brew install xcodegen periphery + swift_tools_dir="$RUNNER_TEMP/openclaw-swift-tools" + ./scripts/install-swift-tools.sh "$swift_tools_dir" + echo "$swift_tools_dir" >> "$GITHUB_PATH" + "$swift_tools_dir/swiftformat" --version + "$swift_tools_dir/swiftlint" version + + - name: Generate iOS project + run: | + set -euo pipefail + ./scripts/ios-configure-signing.sh + ./scripts/ios-write-version-xcconfig.sh + node scripts/ios-write-swift-filelist.mjs + cd apps/ios + xcodegen generate + + - name: Scan shared kit + run: | + set -euo pipefail + output_dir="$RUNNER_TEMP/shared-periphery-ios" + mkdir -p "$output_dir" + cd apps/ios + set +e + periphery scan \ + --config .periphery.yml \ + --clean-build \ + --format json \ + --report-include '../shared/OpenClawKit/Sources/**' \ + --retain-files '../shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift' \ + --write-results "$output_dir/periphery.json" \ + >"$output_dir/periphery.stdout.json" \ + 2>"$output_dir/periphery.stderr.log" + periphery_status="$?" + set -e + printf '%s\n' "$periphery_status" >"$output_dir/periphery.status" + if [ ! -s "$output_dir/periphery.json" ]; then + cp "$output_dir/periphery.stdout.json" "$output_dir/periphery.json" + fi + + - name: Upload iOS consumer report + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: shared-periphery-ios-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/shared-periphery-ios + if-no-files-found: error + retention-days: 14 + + scan-macos: + name: Scan shared kit from macOS + needs: scope + if: ${{ needs.scope.outputs.should-scan == 'true' }} + runs-on: ${{ github.event_name == 'workflow_dispatch' && 'macos-26' || (github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-26' || 'macos-26') }} + timeout-minutes: 45 + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + fetch-depth: 1 + fetch-tags: false + persist-credentials: false + submodules: false + + - name: Verify Xcode + run: | + set -euo pipefail + for xcode_app in /Applications/Xcode_26.5.app /Applications/Xcode-26.5.0.app; do + if [ -d "$xcode_app/Contents/Developer" ]; then + sudo xcode-select -s "$xcode_app/Contents/Developer" + break + fi + done + xcodebuild -version + xcode_version="$(xcodebuild -version | awk 'NR == 1 { print $2 }')" + if [[ "$xcode_version" != 26.* ]]; then + echo "error: expected Xcode 26.x, got $xcode_version" >&2 + exit 1 + fi + swift --version + + - name: Install Periphery + run: | + brew update + brew install periphery + + - name: Scan shared kit + run: | + set -euo pipefail + output_dir="$RUNNER_TEMP/shared-periphery-macos" + mkdir -p "$output_dir" + cd apps/macos + set +e + periphery scan \ + --config .periphery.yml \ + --clean-build \ + --format json \ + --report-include '../shared/OpenClawKit/Sources/**' \ + --retain-files '../shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift' \ + --write-results "$output_dir/periphery.json" \ + >"$output_dir/periphery.stdout.json" \ + 2>"$output_dir/periphery.stderr.log" + periphery_status="$?" + set -e + printf '%s\n' "$periphery_status" >"$output_dir/periphery.status" + if [ ! -s "$output_dir/periphery.json" ]; then + cp "$output_dir/periphery.stdout.json" "$output_dir/periphery.json" + fi + + - name: Upload macOS consumer report + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: shared-periphery-macos-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/shared-periphery-macos + if-no-files-found: error + retention-days: 14 + + intersect: + name: Intersect shared OpenClawKit dead code + needs: [scope, scan-ios, scan-macos] + if: ${{ always() && needs.scope.outputs.should-scan == 'true' && needs.scan-ios.result != 'cancelled' && needs.scan-macos.result != 'cancelled' }} + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + fetch-depth: 1 + fetch-tags: false + persist-credentials: false + submodules: false + + - name: Download iOS consumer report + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: shared-periphery-ios-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/shared-periphery-ios + + - name: Download macOS consumer report + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: shared-periphery-macos-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/shared-periphery-macos + + - name: Intersect exact Swift identities + run: | + node scripts/periphery-intersection.mjs \ + --ios-results "$RUNNER_TEMP/shared-periphery-ios/periphery.json" \ + --ios-status "$RUNNER_TEMP/shared-periphery-ios/periphery.status" \ + --macos-results "$RUNNER_TEMP/shared-periphery-macos/periphery.json" \ + --macos-status "$RUNNER_TEMP/shared-periphery-macos/periphery.status" \ + --output "$RUNNER_TEMP/shared-periphery-intersection/periphery.json" + + - name: Upload shared intersection + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: shared-periphery-intersection-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/shared-periphery-intersection + if-no-files-found: warn + retention-days: 14 diff --git a/apps/.i18n/native-source.json b/apps/.i18n/native-source.json index 0724f1268ab9..e483ccdd475c 100644 --- a/apps/.i18n/native-source.json +++ b/apps/.i18n/native-source.json @@ -32707,7 +32707,7 @@ }, { "kind": "conditional-branch", - "line": 547, + "line": 548, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift", "source": "Image", "surface": "apple", @@ -33155,7 +33155,7 @@ }, { "kind": "ui-localized-call", - "line": 1178, + "line": 1179, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift", "source": "Remove attachments or wait for delivery to resolve before switching chats.", "surface": "apple", @@ -33163,7 +33163,7 @@ }, { "kind": "ui-localized-call", - "line": 1234, + "line": 1235, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift", "source": "Remove attachments or wait for delivery to resolve before starting a new chat.", "surface": "apple", @@ -33595,7 +33595,7 @@ }, { "kind": "conditional-branch", - "line": 198, + "line": 191, "path": "apps/shared/OpenClawKit/Sources/OpenClawKit/ToolDisplay.swift", "source": "\\(preview)…", "surface": "apple", diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatHaptics.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatHaptics.swift index 7a4ff80fab90..d9821eb2d572 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatHaptics.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatHaptics.swift @@ -17,6 +17,7 @@ public struct OpenClawChatHaptics: Sendable { self.performer = Self.defaultPerformer } + // periphery:ignore - package tests inject a recorder; app consumers use the live performer. public init(performer: @escaping @Sendable (Event) -> Void) { self.performer = performer } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift index c05aa91d04d4..fe160e27ce1b 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift @@ -236,6 +236,7 @@ struct ChatMarkdownProse { } } + // periphery:ignore - package tests inspect parsed math spans without exposing renderer internals. var inlineMathLatex: [String] { self.inlineContent?.compactMap { content in if case let .math(span) = content { diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatModels.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatModels.swift index e975b9cc47ab..67116ae52084 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatModels.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatModels.swift @@ -376,6 +376,7 @@ public struct OpenClawChatInFlightRun: Codable, Sendable { public let runId: String public let text: String + // periphery:ignore - package tests construct history fixtures; app consumers decode this payload. public init(runId: String, text: String) { self.runId = runId self.text = text @@ -385,6 +386,7 @@ public struct OpenClawChatInFlightRun: Codable, Sendable { public struct OpenClawChatSessionInfo: Codable, Sendable { public let hasActiveRun: Bool? + // periphery:ignore - package tests construct history fixtures; app consumers decode this payload. public init(hasActiveRun: Bool?) { self.hasActiveRun = hasActiveRun } @@ -455,6 +457,7 @@ public struct OpenClawChatEventPayload: Codable, Sendable { public let message: AnyCodable? public let errorMessage: String? + // periphery:ignore - package tests construct transport events; app consumers decode them. public init( runId: String?, sessionKey: String?, @@ -479,6 +482,7 @@ public struct OpenClawSessionMessageEventPayload: Codable, Sendable { public let messageId: String? public let messageSeq: Int? + // periphery:ignore - package tests construct transport events; app consumers decode them. public init( sessionKey: String?, agentId: String? = nil, diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessions.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessions.swift index 8cfdc2eb99e7..461a58b8fd13 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessions.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessions.swift @@ -59,6 +59,7 @@ public struct OpenClawChatModelPatchResult: Decodable, Sendable, Equatable { public let thinkingLevel: String? public let thinkingLevels: [OpenClawChatThinkingLevelOption]? + // periphery:ignore - package tests construct patch responses; app consumers decode them. public init( key: String? = nil, modelProvider: String?, diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatTranscriptCacheContracts.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatTranscriptCacheContracts.swift index de403e596b92..bea4a7973e09 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatTranscriptCacheContracts.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatTranscriptCacheContracts.swift @@ -71,7 +71,7 @@ extension OpenClawChatTranscriptCache { /// Optional atomic merge seam for cache owners that also provide a durable /// outbox. Keeping this separate preserves source compatibility for read-only /// transcript-cache conformers. -public protocol OpenClawChatCanonicalTranscriptMerging: OpenClawChatTranscriptCache { +protocol OpenClawChatCanonicalTranscriptMerging: OpenClawChatTranscriptCache { func mergeCanonicalTranscriptMessage( sessionKey: String, agentID: String?, diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift index 72f4dc288f5c..22e072de3292 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift @@ -455,6 +455,7 @@ public final class OpenClawChatViewModel { self.applySessionSwitch(to: sessionKey, intent: .externalSync) } + // periphery:ignore - package tests vary one identity field while preserving the current routing contract. public func syncActiveAgentId(_ agentId: String?) { self.syncDeliveryIdentity( activeAgentId: agentId, diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/TalkWaveformView.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/TalkWaveformView.swift index 225767066ca2..9f787536685a 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/TalkWaveformView.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/TalkWaveformView.swift @@ -98,9 +98,9 @@ public struct TalkWaveformView: View { /// Pure waveform math, split from the view for unit testing and so the Android /// port has one canonical reference for every constant. -public enum TalkWaveformMath { +enum TalkWaveformMath { /// Per-phase drive for the wave amplitude in 0...1. - public static func power(for phase: TalkWaveformPhase, time: Double) -> Double { + static func power(for phase: TalkWaveformPhase, time: Double) -> Double { switch phase { case .idle: return 0.05 @@ -121,7 +121,7 @@ public enum TalkWaveformMath { } /// One wave = max envelope of three drifting lobes, mirrored around the midline. - public static func wavePath(in size: CGSize, time: Double, seed: Double, power: Double) -> Path { + static func wavePath(in size: CGSize, time: Double, seed: Double, power: Double) -> Path { let midX = Double(size.width) / 2 let midY = Double(size.height) / 2 diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/BridgeFrames.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/BridgeFrames.swift index bd5139615d8c..a551899608c9 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/BridgeFrames.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/BridgeFrames.swift @@ -1,13 +1,5 @@ import Foundation -public struct BridgeBaseFrame: Codable, Sendable { - public let type: String - - public init(type: String) { - self.type = type - } -} - public struct BridgeInvokeRequest: Codable, Sendable { public let type: String public let id: String @@ -54,216 +46,3 @@ public struct BridgeInvokeResponse: Codable, Sendable { self.error = error } } - -public struct BridgeEventFrame: Codable, Sendable { - public let type: String - public let event: String - public let payloadJSON: String? - - public init(type: String = "event", event: String, payloadJSON: String? = nil) { - self.type = type - self.event = event - self.payloadJSON = payloadJSON - } -} - -public struct BridgeHello: Codable, Sendable { - public let type: String - public let nodeId: String - public let displayName: String? - public let token: String? - public let platform: String? - public let version: String? - public let coreVersion: String? - public let uiVersion: String? - public let deviceFamily: String? - public let modelIdentifier: String? - public let caps: [String]? - public let commands: [String]? - public let permissions: [String: Bool]? - - public init( - type: String = "hello", - nodeId: String, - displayName: String?, - token: String?, - platform: String?, - version: String?, - coreVersion: String? = nil, - uiVersion: String? = nil, - deviceFamily: String? = nil, - modelIdentifier: String? = nil, - caps: [String]? = nil, - commands: [String]? = nil, - permissions: [String: Bool]? = nil) - { - self.type = type - self.nodeId = nodeId - self.displayName = displayName - self.token = token - self.platform = platform - self.version = version - self.coreVersion = coreVersion - self.uiVersion = uiVersion - self.deviceFamily = deviceFamily - self.modelIdentifier = modelIdentifier - self.caps = caps - self.commands = commands - self.permissions = permissions - } -} - -public struct BridgeHelloOk: Codable, Sendable { - public let type: String - public let serverName: String - public let mainSessionKey: String? - - public init( - type: String = "hello-ok", - serverName: String, - mainSessionKey: String? = nil) - { - self.type = type - self.serverName = serverName - self.mainSessionKey = mainSessionKey - } -} - -public struct BridgePairRequest: Codable, Sendable { - public let type: String - public let nodeId: String - public let displayName: String? - public let platform: String? - public let version: String? - public let coreVersion: String? - public let uiVersion: String? - public let deviceFamily: String? - public let modelIdentifier: String? - public let caps: [String]? - public let commands: [String]? - public let permissions: [String: Bool]? - public let remoteAddress: String? - public let silent: Bool? - - public init( - type: String = "pair-request", - nodeId: String, - displayName: String?, - platform: String?, - version: String?, - coreVersion: String? = nil, - uiVersion: String? = nil, - deviceFamily: String? = nil, - modelIdentifier: String? = nil, - caps: [String]? = nil, - commands: [String]? = nil, - permissions: [String: Bool]? = nil, - remoteAddress: String? = nil, - silent: Bool? = nil) - { - self.type = type - self.nodeId = nodeId - self.displayName = displayName - self.platform = platform - self.version = version - self.coreVersion = coreVersion - self.uiVersion = uiVersion - self.deviceFamily = deviceFamily - self.modelIdentifier = modelIdentifier - self.caps = caps - self.commands = commands - self.permissions = permissions - self.remoteAddress = remoteAddress - self.silent = silent - } -} - -public struct BridgePairOk: Codable, Sendable { - public let type: String - public let token: String - - public init(type: String = "pair-ok", token: String) { - self.type = type - self.token = token - } -} - -public struct BridgePing: Codable, Sendable { - public let type: String - public let id: String - - public init(type: String = "ping", id: String) { - self.type = type - self.id = id - } -} - -public struct BridgePong: Codable, Sendable { - public let type: String - public let id: String - - public init(type: String = "pong", id: String) { - self.type = type - self.id = id - } -} - -public struct BridgeErrorFrame: Codable, Sendable { - public let type: String - public let code: String - public let message: String - - public init(type: String = "error", code: String, message: String) { - self.type = type - self.code = code - self.message = message - } -} - -// MARK: - Optional RPC (node -> bridge) - -public struct BridgeRPCRequest: Codable, Sendable { - public let type: String - public let id: String - public let method: String - public let paramsJSON: String? - - public init(type: String = "req", id: String, method: String, paramsJSON: String? = nil) { - self.type = type - self.id = id - self.method = method - self.paramsJSON = paramsJSON - } -} - -public struct BridgeRPCError: Codable, Sendable, Equatable { - public let code: String - public let message: String - - public init(code: String, message: String) { - self.code = code - self.message = message - } -} - -public struct BridgeRPCResponse: Codable, Sendable { - public let type: String - public let id: String - public let ok: Bool - public let payloadJSON: String? - public let error: BridgeRPCError? - - public init( - type: String = "res", - id: String, - ok: Bool, - payloadJSON: String? = nil, - error: BridgeRPCError? = nil) - { - self.type = type - self.id = id - self.ok = ok - self.payloadJSON = payloadJSON - self.error = error - } -} diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/CameraCapturePipelineSupport.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/CameraCapturePipelineSupport.swift index d5dad3a98672..bf5693a8bdc7 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/CameraCapturePipelineSupport.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/CameraCapturePipelineSupport.swift @@ -72,28 +72,6 @@ public enum CameraCapturePipelineSupport { } } - /// Keeps the flat overload source-compatible while the options form owns the implementation. - public static func prepareMovieSession( - preferFrontCamera: Bool, - deviceId: String?, - includeAudio: Bool, - durationMs: Int, - pickCamera: (_ preferFrontCamera: Bool, _ deviceId: String?) -> AVCaptureDevice?, - cameraUnavailableError: @autoclosure () -> Error, - mapSetupError: (CameraSessionConfigurationError) -> Error) throws - -> (session: AVCaptureSession, output: AVCaptureMovieFileOutput) - { - try self.prepareMovieSession( - options: CameraMovieSessionOptions( - preferFrontCamera: preferFrontCamera, - deviceId: deviceId, - includeAudio: includeAudio, - durationMs: durationMs), - pickCamera: pickCamera, - cameraUnavailableError: cameraUnavailableError(), - mapSetupError: mapSetupError) - } - public static func prepareWarmMovieSession( options: CameraMovieSessionOptions, pickCamera: (_ preferFrontCamera: Bool, _ deviceId: String?) -> AVCaptureDevice?, @@ -119,28 +97,6 @@ public enum CameraCapturePipelineSupport { return prepared } - /// Keeps the flat overload source-compatible while the options form owns the implementation. - public static func prepareWarmMovieSession( - preferFrontCamera: Bool, - deviceId: String?, - includeAudio: Bool, - durationMs: Int, - pickCamera: (_ preferFrontCamera: Bool, _ deviceId: String?) -> AVCaptureDevice?, - cameraUnavailableError: @autoclosure () -> Error, - mapSetupError: (CameraSessionConfigurationError) -> Error) async throws - -> (session: AVCaptureSession, output: AVCaptureMovieFileOutput) - { - try await self.prepareWarmMovieSession( - options: CameraMovieSessionOptions( - preferFrontCamera: preferFrontCamera, - deviceId: deviceId, - includeAudio: includeAudio, - durationMs: durationMs), - pickCamera: pickCamera, - cameraUnavailableError: cameraUnavailableError(), - mapSetupError: mapSetupError) - } - public static func withWarmMovieSession( options: CameraMovieSessionOptions, pickCamera: (_ preferFrontCamera: Bool, _ deviceId: String?) -> AVCaptureDevice?, @@ -161,29 +117,6 @@ public enum CameraCapturePipelineSupport { operation: { try await operation(prepared.output) }) } - /// Keeps the flat overload source-compatible while the options form owns the implementation. - public static func withWarmMovieSession( - preferFrontCamera: Bool, - deviceId: String? = nil, - includeAudio: Bool, - durationMs: Int, - pickCamera: (_ preferFrontCamera: Bool, _ deviceId: String?) -> AVCaptureDevice?, - cameraUnavailableError: @autoclosure () -> Error, - mapSetupError: (CameraSessionConfigurationError) -> Error, - operation: (AVCaptureMovieFileOutput) async throws -> T) async throws -> T - { - try await self.withWarmMovieSession( - options: CameraMovieSessionOptions( - preferFrontCamera: preferFrontCamera, - deviceId: deviceId, - includeAudio: includeAudio, - durationMs: durationMs), - pickCamera: pickCamera, - cameraUnavailableError: cameraUnavailableError(), - mapSetupError: mapSetupError, - operation: operation) - } - static func withCaptureSessionLifecycle( start: () -> Void, stop: () -> Void, diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/CameraSessionConfiguration.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/CameraSessionConfiguration.swift index eb579683238f..23a0639e76e2 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/CameraSessionConfiguration.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/CameraSessionConfiguration.swift @@ -25,8 +25,8 @@ public enum CameraSessionConfigurationError: LocalizedError { } } -public enum CameraSessionConfiguration { - public static func addCameraInput(session: AVCaptureSession, camera: AVCaptureDevice) throws { +enum CameraSessionConfiguration { + static func addCameraInput(session: AVCaptureSession, camera: AVCaptureDevice) throws { let input = try AVCaptureDeviceInput(device: camera) guard session.canAddInput(input) else { throw CameraSessionConfigurationError.addCameraInputFailed @@ -34,7 +34,7 @@ public enum CameraSessionConfiguration { session.addInput(input) } - public static func addPhotoOutput(session: AVCaptureSession) throws -> AVCapturePhotoOutput { + static func addPhotoOutput(session: AVCaptureSession) throws -> AVCapturePhotoOutput { let output = AVCapturePhotoOutput() guard session.canAddOutput(output) else { throw CameraSessionConfigurationError.addPhotoOutputFailed @@ -44,7 +44,7 @@ public enum CameraSessionConfiguration { return output } - public static func addMovieOutput( + static func addMovieOutput( session: AVCaptureSession, includeAudio: Bool, durationMs: Int) throws -> AVCaptureMovieFileOutput diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/CanvasA2UICommands.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/CanvasA2UICommands.swift index ab3af0c367a5..7b1d285fdf5e 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/CanvasA2UICommands.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/CanvasA2UICommands.swift @@ -11,10 +11,6 @@ public enum OpenClawCanvasA2UICommand: String, Codable, Sendable { public struct OpenClawCanvasA2UIPushParams: Codable, Sendable, Equatable { public var messages: [AnyCodable] - - public init(messages: [AnyCodable]) { - self.messages = messages - } } public struct OpenClawCanvasA2UIPushJSONLParams: Codable, Sendable, Equatable { diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/CanvasCommandParams.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/CanvasCommandParams.swift index 2c109cf2fdac..53b98c2aa964 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/CanvasCommandParams.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/CanvasCommandParams.swift @@ -13,13 +13,6 @@ public struct OpenClawCanvasPlacement: Codable, Sendable, Equatable { public var y: Double? public var width: Double? public var height: Double? - - public init(x: Double? = nil, y: Double? = nil, width: Double? = nil, height: Double? = nil) { - self.x = x - self.y = y - self.width = width - self.height = height - } } public struct OpenClawCanvasPresentParams: Codable, Sendable, Equatable { @@ -67,10 +60,4 @@ public struct OpenClawCanvasSnapshotParams: Codable, Sendable, Equatable { public var maxWidth: Int? public var quality: Double? public var format: OpenClawCanvasSnapshotFormat? - - public init(maxWidth: Int? = nil, quality: Double? = nil, format: OpenClawCanvasSnapshotFormat? = nil) { - self.maxWidth = maxWidth - self.quality = quality - self.format = format - } } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceAuthPayload.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceAuthPayload.swift index c0984dc3a20c..04dc1d8c7b14 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceAuthPayload.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceAuthPayload.swift @@ -61,27 +61,6 @@ public enum GatewayDeviceAuthPayload { ].joined(separator: "|") } - /// Keeps the flat overload source-compatible while `Fields` owns canonical serialization. - public static func buildConnectCompatibilityPayload( - deviceId: String, - clientId: String, - clientMode: String, - role: String, - scopes: [String], - signedAtMs: Int64, - token: String? = nil, - nonce: String) -> String - { - self.buildConnectCompatibilityPayload(fields: Fields( - deviceId: deviceId, - client: Client(id: clientId, mode: clientMode), - role: role, - scopes: scopes, - signedAtMs: signedAtMs, - token: token, - nonce: nonce)) - } - public static func buildV3( fields: Fields, platform: String?, @@ -106,32 +85,6 @@ public enum GatewayDeviceAuthPayload { ].joined(separator: "|") } - /// Keeps the flat overload source-compatible while `Fields` owns canonical serialization. - public static func buildV3( - deviceId: String, - clientId: String, - clientMode: String, - role: String, - scopes: [String], - signedAtMs: Int64, - token: String? = nil, - nonce: String, - platform: String? = nil, - deviceFamily: String? = nil) -> String - { - self.buildV3( - fields: Fields( - deviceId: deviceId, - client: Client(id: clientId, mode: clientMode), - role: role, - scopes: scopes, - signedAtMs: signedAtMs, - token: token, - nonce: nonce), - platform: platform, - deviceFamily: deviceFamily) - } - static func normalizeMetadataField(_ value: String?) -> String { guard let value else { return "" } let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/ElevenLabsKitShim.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/ElevenLabsKitShim.swift index 1bfaa02abf0d..2d02a7c6367f 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/ElevenLabsKitShim.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/ElevenLabsKitShim.swift @@ -1,7 +1,6 @@ #if Talk && canImport(ElevenLabsKit) @_exported import ElevenLabsKit -public typealias ElevenLabsVoice = ElevenLabsKit.ElevenLabsVoice public typealias ElevenLabsTTSRequest = ElevenLabsKit.ElevenLabsTTSRequest public typealias ElevenLabsTTSClient = ElevenLabsKit.ElevenLabsTTSClient public typealias TalkTTSValidation = ElevenLabsKit.TalkTTSValidation diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift index f6ef76809830..46e638305cd5 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift @@ -681,17 +681,6 @@ public actor GatewayNodeSession { } } - public func send(method: String, paramsJSON: String?) async throws { - guard let channel else { - throw NSError(domain: "Gateway", code: 11, userInfo: [ - NSLocalizedDescriptionKey: "not connected", - ]) - } - - let params = try decodeParamsJSON(paramsJSON) - try await channel.send(method: method, params: params) - } - public func request( method: String, paramsJSON: String?, @@ -1086,14 +1075,17 @@ extension GatewayNodeSession { } #if DEBUG + // periphery:ignore - package tests observe admission rollover without exposing mutable state. func _test_admissionGeneration() -> UInt64 { self.admissionGeneration } + // periphery:ignore - package tests drive the private connection callback deterministically. func _test_notifyConnectedIfNeeded(admissionGeneration: UInt64) async { await self.notifyConnectedIfNeeded(admissionGeneration: admissionGeneration) } + // periphery:ignore - package tests inject gateway pushes without a live socket. func _test_handlePush(_ push: GatewayPush, socketGeneration: UInt64) async { await self.handlePush( push, @@ -1101,6 +1093,7 @@ extension GatewayNodeSession { socketGeneration: socketGeneration) } + // periphery:ignore - package tests inject socket retirement without a live channel. func _test_handleChannelDisconnected(_ reason: String, socketGeneration: UInt64) async { await self.handleChannelDisconnected( reason, @@ -1108,6 +1101,7 @@ extension GatewayNodeSession { socketGeneration: socketGeneration) } + // periphery:ignore - package tests verify event stream filtering without a live gateway. func _test_broadcastServerEvent(_ event: EventFrame) { self.broadcastServerEvent(event) } @@ -1247,6 +1241,7 @@ extension GatewayNodeSession { } #if DEBUG + // periphery:ignore - package tests exercise receipt dedupe around the private invoke path. func invokeComputerWithReceiptForTesting( requestId: String, paramsJSON: String, @@ -1275,6 +1270,7 @@ extension GatewayNodeSession { onInvoke: onInvoke) } + // periphery:ignore - package tests assert receipt joining without exposing the receipt store. func computerReceiptJoinCountForTesting( idempotencyKey: String, receiptScope: String) -> Int diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayPayloadDecoding.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayPayloadDecoding.swift index c4eb59cb1383..ab876fece5e8 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayPayloadDecoding.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayPayloadDecoding.swift @@ -9,12 +9,4 @@ public enum GatewayPayloadDecoding { let data = try JSONEncoder().encode(payload) return try JSONDecoder().decode(T.self, from: data) } - - public static func decodeIfPresent( - _ payload: AnyCodable?, - as _: T.Type = T.self) throws -> T? - { - guard let payload else { return nil } - return try self.decode(payload, as: T.self) - } } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayTLSPinning.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayTLSPinning.swift index 3be9538ed5c1..7fb6c5517c12 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayTLSPinning.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayTLSPinning.swift @@ -72,11 +72,11 @@ public struct GatewayTLSValidationError: LocalizedError, Sendable { } } -public protocol GatewayTLSFailureProviding: AnyObject { +protocol GatewayTLSFailureProviding: AnyObject { func consumeLastTLSFailure() -> GatewayTLSValidationFailure? } -public protocol GatewayDeviceTokenRetryTrustProviding: AnyObject { +protocol GatewayDeviceTokenRetryTrustProviding: AnyObject { var allowsDeviceTokenRetryAuth: Bool { get } } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/JPEGTranscoder.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/JPEGTranscoder.swift index 1eafcbcc6e8c..3256fa7edf56 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/JPEGTranscoder.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/JPEGTranscoder.swift @@ -3,13 +3,13 @@ import Foundation import ImageIO import UniformTypeIdentifiers -public enum JPEGTranscodeError: LocalizedError, Sendable { +enum JPEGTranscodeError: LocalizedError, Sendable { case decodeFailed case propertiesMissing case encodeFailed case sizeLimitExceeded(maxBytes: Int, actualBytes: Int) - public var errorDescription: String? { + var errorDescription: String? { switch self { case .decodeFailed: "Failed to decode image data" @@ -23,8 +23,8 @@ public enum JPEGTranscodeError: LocalizedError, Sendable { } } -public struct JPEGTranscoder: Sendable { - public static func clampQuality(_ quality: Double) -> Double { +struct JPEGTranscoder: Sendable { + static func clampQuality(_ quality: Double) -> Double { min(1.0, max(0.05, quality)) } @@ -32,7 +32,7 @@ public struct JPEGTranscoder: Sendable { /// /// - Important: This normalizes EXIF orientation (the output pixels are rotated if needed; orientation tag is not /// relied on). - public static func transcodeToJPEG( + static func transcodeToJPEG( imageData: Data, maxWidthPx: Int?, quality: Double, @@ -51,7 +51,7 @@ public struct JPEGTranscoder: Sendable { /// When `maxLongEdgePx` is provided it takes precedence over `maxWidthPx`. /// - Important: This normalizes EXIF orientation (the output pixels are rotated if needed; orientation tag is not /// relied on). - public static func transcodeToJPEG( + static func transcodeToJPEG( imageData: Data, maxWidthPx: Int? = nil, maxLongEdgePx: Int?, diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/LocationServiceSupport.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/LocationServiceSupport.swift index 0d8ce80050b0..16f2a76e1fa8 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/LocationServiceSupport.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/LocationServiceSupport.swift @@ -25,12 +25,6 @@ extension LocationServiceCommon { public func accuracyAuthorization() -> CLAccuracyAuthorization { LocationServiceSupport.accuracyAuthorization(manager: self.locationManager) } - - public func requestLocationOnce() async throws -> CLLocation { - try await LocationServiceSupport.requestLocation(manager: self.locationManager) { continuation in - self.locationRequestContinuation = continuation - } - } } extension ConcurrentLocationServiceCommon { @@ -61,8 +55,8 @@ extension ConcurrentLocationServiceCommon { } } -public enum LocationServiceSupport { - public static func accuracyAuthorization(manager: CLLocationManager) -> CLAccuracyAuthorization { +enum LocationServiceSupport { + static func accuracyAuthorization(manager: CLLocationManager) -> CLAccuracyAuthorization { if #available(iOS 14.0, macOS 11.0, *) { return manager.accuracyAuthorization } @@ -70,7 +64,7 @@ public enum LocationServiceSupport { } @MainActor - public static func requestLocation( + static func requestLocation( manager: CLLocationManager, setContinuation: @escaping (CheckedContinuation) -> Void) async throws -> CLLocation { diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/ScreenCommands.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/ScreenCommands.swift index bfa3e79d5c73..793fc0060605 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/ScreenCommands.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/ScreenCommands.swift @@ -10,25 +10,6 @@ public enum OpenClawScreenSnapshotFormat: String, Codable, Sendable { case png } -public struct OpenClawScreenSnapshotParams: Codable, Sendable, Equatable { - public var screenIndex: Int? - public var maxWidth: Int? - public var quality: Double? - public var format: OpenClawScreenSnapshotFormat? - - public init( - screenIndex: Int? = nil, - maxWidth: Int? = nil, - quality: Double? = nil, - format: OpenClawScreenSnapshotFormat? = nil) - { - self.screenIndex = screenIndex - self.maxWidth = maxWidth - self.quality = quality - self.format = format - } -} - public struct OpenClawScreenRecordParams: Codable, Sendable, Equatable { public var screenIndex: Int? public var durationMs: Int? diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/SessionMutationResponses.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/SessionMutationResponses.swift index 808130bd4223..c3f07ff9b3e8 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/SessionMutationResponses.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/SessionMutationResponses.swift @@ -12,15 +12,11 @@ public struct OpenClawSessionsCompactResponse: Decodable, Sendable { } } -public struct OpenClawSessionsCompactError: Error, LocalizedError, Sendable { - public let reason: String? +struct OpenClawSessionsCompactError: Error, LocalizedError, Sendable { + let reason: String? - public var errorDescription: String? { + var errorDescription: String? { let detail = self.reason?.trimmingCharacters(in: .whitespacesAndNewlines) return detail?.isEmpty == false ? detail : "Session compaction failed" } - - public init(reason: String?) { - self.reason = reason - } } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/SystemCommands.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/SystemCommands.swift index bf8f04aef649..43c9059e033a 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/SystemCommands.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/SystemCommands.swift @@ -24,103 +24,6 @@ public enum OpenClawNotificationDelivery: String, Codable, Sendable { case auto } -public struct OpenClawSystemRunApprovalFileOperand: Codable, Sendable, Equatable { - public var argvIndex: Int - public var path: String - public var sha256: String - - public init(argvIndex: Int, path: String, sha256: String) { - self.argvIndex = argvIndex - self.path = path - self.sha256 = sha256 - } -} - -public struct OpenClawSystemRunApprovalPlan: Codable, Sendable, Equatable { - public var argv: [String] - public var cwd: String? - public var commandText: String - public var commandPreview: String? - public var agentId: String? - public var sessionKey: String? - public var policySnapshot: OpenClawSystemRunApprovalPolicySnapshot? - public var mutableFileOperand: OpenClawSystemRunApprovalFileOperand? - - public init( - argv: [String], - cwd: String?, - commandText: String, - commandPreview: String? = nil, - agentId: String?, - sessionKey: String?, - policySnapshot: OpenClawSystemRunApprovalPolicySnapshot? = nil, - mutableFileOperand: OpenClawSystemRunApprovalFileOperand? = nil) - { - self.argv = argv - self.cwd = cwd - self.commandText = commandText - self.commandPreview = commandPreview - self.agentId = agentId - self.sessionKey = sessionKey - self.policySnapshot = policySnapshot - self.mutableFileOperand = mutableFileOperand - } -} - -public struct OpenClawSystemRunParams: Codable, Sendable, Equatable { - public var command: [String] - public var rawCommand: String? - public var cwd: String? - public var env: [String: String]? - public var timeoutMs: Int? - public var needsScreenRecording: Bool? - public var agentId: String? - public var sessionKey: String? - public var runId: String? - public var systemRunPlan: OpenClawSystemRunApprovalPlan? - public var approved: Bool? - public var approvalDecision: String? - public var approvalSource: String? - - public init( - command: [String], - rawCommand: String? = nil, - cwd: String? = nil, - env: [String: String]? = nil, - timeoutMs: Int? = nil, - needsScreenRecording: Bool? = nil, - agentId: String? = nil, - sessionKey: String? = nil, - runId: String? = nil, - systemRunPlan: OpenClawSystemRunApprovalPlan? = nil, - approved: Bool? = nil, - approvalDecision: String? = nil, - approvalSource: String? = nil) - { - self.command = command - self.rawCommand = rawCommand - self.cwd = cwd - self.env = env - self.timeoutMs = timeoutMs - self.needsScreenRecording = needsScreenRecording - self.agentId = agentId - self.sessionKey = sessionKey - self.runId = runId - self.systemRunPlan = systemRunPlan - self.approved = approved - self.approvalDecision = approvalDecision - self.approvalSource = approvalSource - } -} - -public struct OpenClawSystemWhichParams: Codable, Sendable, Equatable { - public var bins: [String] - - public init(bins: [String]) { - self.bins = bins - } -} - public struct OpenClawSystemNotifyParams: Codable, Sendable, Equatable { public var title: String public var body: String diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkPlaybackLevelMeters.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkPlaybackLevelMeters.swift index 7e7a67a7eb55..4fd7ba6c83c2 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkPlaybackLevelMeters.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkPlaybackLevelMeters.swift @@ -181,16 +181,16 @@ public final class PCMPlaybackEnvelope { /// via its built-in metering at ~30 Hz. Detach clears the level to nil so the /// consumer can distinguish "silent" from "not playing". @MainActor -public final class AudioPlayerLevelMeter { +final class AudioPlayerLevelMeter { private let onLevel: @MainActor (Double?) -> Void private var pollTask: Task? private weak var player: AVAudioPlayer? - public init(onLevel: @escaping @MainActor (Double?) -> Void) { + init(onLevel: @escaping @MainActor (Double?) -> Void) { self.onLevel = onLevel } - public func attach(_ player: AVAudioPlayer) { + func attach(_ player: AVAudioPlayer) { self.detach() player.isMeteringEnabled = true self.player = player @@ -204,7 +204,7 @@ public final class AudioPlayerLevelMeter { } } - public func detach() { + func detach() { self.pollTask?.cancel() self.pollTask = nil self.player = nil diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkSystemSpeechSynthesizer.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkSystemSpeechSynthesizer.swift index 585a1d8ea3d5..e06f28cc0b11 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkSystemSpeechSynthesizer.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkSystemSpeechSynthesizer.swift @@ -16,10 +16,6 @@ public final class TalkSystemSpeechSynthesizer: NSObject { private var currentToken = UUID() private var watchdog: Task? - public var isSpeaking: Bool { - self.synth.isSpeaking - } - override private init() { super.init() self.synth.delegate = self diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/ToolDisplay.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/ToolDisplay.swift index 47fa5c696184..0cc69c34c65e 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/ToolDisplay.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/ToolDisplay.swift @@ -14,13 +14,6 @@ public struct ToolDisplaySummary: Sendable, Equatable { if let detail, !detail.isEmpty { parts.append(detail) } return parts.isEmpty ? nil : parts.joined(separator: " · ") } - - public var summaryLine: String { - if let detailLine { - return "\(self.emoji) \(self.label): \(detailLine)" - } - return "\(self.emoji) \(self.label)" - } } public enum ToolDisplayRegistry { diff --git a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/WakeParamsCompatibility.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/WakeParamsCompatibility.swift index 0f0b04171dd0..daf2f1807a0b 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/WakeParamsCompatibility.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/WakeParamsCompatibility.swift @@ -1,4 +1,5 @@ extension WakeParams { + // periphery:ignore - Shipped before sessionKey; remove only at a breaking protocol API window. public init( mode: AnyCodable, text: String) diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/JPEGTranscoderTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/JPEGTranscoderTests.swift index db006ee41d8f..ddda3d1c19f1 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/JPEGTranscoderTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/JPEGTranscoderTests.swift @@ -1,10 +1,10 @@ -import OpenClawKit import CoreGraphics import ImageIO import Testing import UniformTypeIdentifiers +@testable import OpenClawKit -@Suite struct JPEGTranscoderTests { +struct JPEGTranscoderTests { private func makeSolidJPEG(width: Int, height: Int, orientation: Int? = nil) throws -> Data { let cs = CGColorSpaceCreateDeviceRGB() let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue @@ -54,7 +54,7 @@ import UniformTypeIdentifiers let cs = CGColorSpaceCreateDeviceRGB() let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue - let out = try data.withUnsafeMutableBytes { rawBuffer -> Data in + return try data.withUnsafeMutableBytes { rawBuffer -> Data in guard let base = rawBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { throw NSError(domain: "JPEGTranscoderTests", code: 6) } @@ -90,11 +90,9 @@ import UniformTypeIdentifiers } return encoded as Data } - - return out } - @Test func downscalesToMaxWidthPx() throws { + @Test func `downscales to max width px`() throws { let input = try makeSolidJPEG(width: 2000, height: 1000) let out = try JPEGTranscoder.transcodeToJPEG(imageData: input, maxWidthPx: 1600, quality: 0.9) #expect(out.widthPx == 1600) @@ -102,14 +100,14 @@ import UniformTypeIdentifiers #expect(out.data.count > 0) } - @Test func doesNotUpscaleWhenSmallerThanMaxWidthPx() throws { + @Test func `does not upscale when smaller than max width px`() throws { let input = try makeSolidJPEG(width: 800, height: 600) let out = try JPEGTranscoder.transcodeToJPEG(imageData: input, maxWidthPx: 1600, quality: 0.9) #expect(out.widthPx == 800) #expect(out.heightPx == 600) } - @Test func normalizesOrientationAndUsesOrientedWidthForMaxWidthPx() throws { + @Test func `normalizes orientation and uses oriented width for max width px`() throws { // Encode a landscape image but mark it rotated 90° (orientation 6). Oriented width becomes 1000. let input = try makeSolidJPEG(width: 2000, height: 1000, orientation: 6) let out = try JPEGTranscoder.transcodeToJPEG(imageData: input, maxWidthPx: 1600, quality: 0.9) @@ -117,7 +115,7 @@ import UniformTypeIdentifiers #expect(out.heightPx == 2000) } - @Test func respectsMaxBytes() throws { + @Test func `respects max bytes`() throws { let input = try makeNoiseJPEG(width: 1600, height: 1200) let out = try JPEGTranscoder.transcodeToJPEG( imageData: input, @@ -127,7 +125,7 @@ import UniformTypeIdentifiers #expect(out.data.count <= 180_000) } - @Test func explicitlyFailsWhenSizeLimitCannotBeMet() throws { + @Test func `explicitly fails when size limit cannot be met`() throws { let input = try makeSolidJPEG(width: 800, height: 600) do { @@ -137,7 +135,7 @@ import UniformTypeIdentifiers quality: 0.9, maxBytes: 1) Issue.record("Expected a size-limit error") - } catch JPEGTranscodeError.sizeLimitExceeded(let maxBytes, let actualBytes) { + } catch let JPEGTranscodeError.sizeLimitExceeded(maxBytes, actualBytes) { #expect(maxBytes == 1) #expect(actualBytes > maxBytes) } catch { diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/SessionMutationResponsesTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/SessionMutationResponsesTests.swift index da4f5fbc2b76..9ab13b3a7f6a 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/SessionMutationResponsesTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/SessionMutationResponsesTests.swift @@ -1,16 +1,16 @@ import Foundation -import OpenClawKit import Testing +@testable import OpenClawKit struct SessionMutationResponsesTests { @Test - func compactResponseAcceptsSuccess() throws { + func `compact response accepts success`() throws { try OpenClawSessionsCompactResponse.requireSuccess( from: Data(#"{"ok":true,"key":"agent:main:main","compacted":true}"#.utf8)) } @Test - func compactResponseSurfacesGatewayFailureReason() { + func `compact response surfaces gateway failure reason`() { let data = Data( #"{"ok":false,"key":"agent:main:main","compacted":false,"reason":"turn failed"}"#.utf8) do { diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkWaveformTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkWaveformTests.swift index 4f674a11e6bf..51bf8683ce05 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkWaveformTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkWaveformTests.swift @@ -1,8 +1,8 @@ import AVFoundation import Foundation -import OpenClawChatUI -import OpenClawKit import Testing +@testable import OpenClawChatUI +@testable import OpenClawKit struct TalkWaveformMathTests { @Test diff --git a/docs/ci.md b/docs/ci.md index 097713333d50..62711e16b773 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -52,6 +52,8 @@ dispatch. | `test-performance-agent` | Separate workflow: daily Codex slow-test optimization after trusted activity | Main CI success or manual dispatch | | `openclaw-performance` | Separate workflow: daily/on-demand Kova runtime performance reports with mock-provider, deep-profile, and GPT 5.6 live lanes | Scheduled and manual dispatch | +Standalone Periphery workflows enforce zero dead-code findings for the iOS and macOS apps. The shared OpenClawKit workflow scans both consumers in parallel and reports a declaration only when Periphery emits the same Swift USR from both builds. Its generated `OpenClawProtocol/GatewayModels.swift` schema contract is retained as generator-owned code rather than treated as app-local dead code. + ## Fail-fast order 1. `runner-admission` waits only for canonical `main` pushes; a newer push cancels the run before Blacksmith registration. diff --git a/scripts/periphery-intersection.d.mts b/scripts/periphery-intersection.d.mts new file mode 100644 index 000000000000..0a08225940ce --- /dev/null +++ b/scripts/periphery-intersection.d.mts @@ -0,0 +1,30 @@ +export type PeripheryFinding = { + ids: string[]; + kind: string; + location: string; + name: string; + hints?: string[]; + [key: string]: unknown; +}; + +export type PeripheryIntersectionOptions = { + iosResults: string; + iosStatus: string; + macosResults: string; + macosStatus: string; + output: string; +}; + +export function parseArgs(args: string[]): PeripheryIntersectionOptions; +export function validateFindings(value: unknown, label: string): PeripheryFinding[]; +export function intersectFindings(iosFindings: unknown, macosFindings: unknown): PeripheryFinding[]; +export function parseRepoLocation(location: string): { + column: string; + file: string; + line: string; +}; +export function escapeCommandData(value: unknown): string; +export function escapeCommandProperty(value: unknown): string; +export function formatAnnotation(finding: PeripheryFinding): string; +export function buildSummary(findings: PeripheryFinding[]): string; +export function run(args: string[], env?: NodeJS.ProcessEnv): number; diff --git a/scripts/periphery-intersection.mjs b/scripts/periphery-intersection.mjs new file mode 100644 index 000000000000..92c8e16ae53f --- /dev/null +++ b/scripts/periphery-intersection.mjs @@ -0,0 +1,199 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import path from "node:path"; +import { isDirectRunUrl } from "./lib/direct-run.mjs"; + +const SHARED_LOCATION_PREFIX = "../shared/OpenClawKit/Sources/"; + +function requireValue(args, index, option) { + const value = args[index + 1]; + if (!value || value.startsWith("--")) { + throw new Error(`${option} requires a value`); + } + return value; +} + +export function parseArgs(args) { + const options = {}; + for (let index = 0; index < args.length; index += 1) { + const option = args[index]; + switch (option) { + case "--ios-results": + options.iosResults = requireValue(args, index, option); + index += 1; + break; + case "--ios-status": + options.iosStatus = requireValue(args, index, option); + index += 1; + break; + case "--macos-results": + options.macosResults = requireValue(args, index, option); + index += 1; + break; + case "--macos-status": + options.macosStatus = requireValue(args, index, option); + index += 1; + break; + case "--output": + options.output = requireValue(args, index, option); + index += 1; + break; + default: + throw new Error(`unknown option: ${option}`); + } + } + + for (const key of ["iosResults", "iosStatus", "macosResults", "macosStatus", "output"]) { + if (!options[key]) { + throw new Error(`missing required option: ${key}`); + } + } + return options; +} + +export function validateFindings(value, label) { + if (!Array.isArray(value)) { + throw new Error(`${label} results must be a JSON array`); + } + + return value.map((finding, index) => { + if (!finding || typeof finding !== "object" || Array.isArray(finding)) { + throw new Error(`${label} finding ${index} must be an object`); + } + if ( + !Array.isArray(finding.ids) || + finding.ids.length === 0 || + finding.ids.some((id) => typeof id !== "string" || id.length === 0) + ) { + throw new Error(`${label} finding ${index} has no usable Swift USR`); + } + if ( + typeof finding.location !== "string" || + !finding.location.startsWith(SHARED_LOCATION_PREFIX) + ) { + throw new Error(`${label} finding ${index} is outside shared OpenClawKit sources`); + } + if (typeof finding.kind !== "string" || typeof finding.name !== "string") { + throw new Error(`${label} finding ${index} is missing its kind or name`); + } + return finding; + }); +} + +export function intersectFindings(iosFindings, macosFindings) { + const ios = validateFindings(iosFindings, "iOS"); + const macos = validateFindings(macosFindings, "macOS"); + const macosIds = new Set(macos.flatMap((finding) => finding.ids)); + + return ios + .filter((finding) => finding.ids.some((id) => macosIds.has(id))) + .toSorted((left, right) => + [left.location, left.kind, left.name] + .join("\0") + .localeCompare([right.location, right.kind, right.name].join("\0")), + ); +} + +export function parseRepoLocation(location) { + const match = /^(.*):(\d+):(\d+)$/.exec(location); + if (!match || !match[1].startsWith("../shared/")) { + throw new Error(`invalid shared Periphery location: ${location}`); + } + return { + column: match[3], + file: `apps/shared/${match[1].slice("../shared/".length)}`, + line: match[2], + }; +} + +export function escapeCommandData(value) { + return String(value ?? "") + .replaceAll("%", "%25") + .replaceAll("\r", "%0D") + .replaceAll("\n", "%0A"); +} + +export function escapeCommandProperty(value) { + return escapeCommandData(value).replaceAll(":", "%3A").replaceAll(",", "%2C"); +} + +export function formatAnnotation(finding) { + const location = parseRepoLocation(finding.location); + const title = `${finding.kind || "Unused code"} ${finding.name}`.trim(); + return `::error file=${escapeCommandProperty(location.file)},line=${location.line},col=${location.column},title=Dead shared Swift code::${escapeCommandData(title)}`; +} + +export function buildSummary(findings) { + if (findings.length === 0) { + return [ + "### Shared OpenClawKit Periphery", + "", + "No declarations were reported dead by both the iOS and macOS consumer scans.", + ].join("\n"); + } + return [ + "### Shared OpenClawKit Periphery", + "", + `Found ${findings.length} shared Swift ${findings.length === 1 ? "declaration" : "declarations"} reported dead by both consumer scans.`, + "", + "The gate matches Periphery's Swift USRs, not declaration names.", + ].join("\n"); +} + +function readStatus(file, label) { + const raw = fs.readFileSync(file, "utf8").trim(); + if (!/^\d+$/.test(raw)) { + throw new Error(`${label} Periphery status is invalid`); + } + const status = Number(raw); + if (status !== 0) { + throw new Error(`${label} Periphery scan exited with status ${status}`); + } +} + +function readFindings(file, label) { + let parsed; + try { + parsed = JSON.parse(fs.readFileSync(file, "utf8")); + } catch (error) { + throw new Error(`${label} Periphery results are not valid JSON`, { cause: error }); + } + return validateFindings(parsed, label); +} + +export function run(args, env = process.env) { + const options = parseArgs(args); + readStatus(options.iosStatus, "iOS"); + readStatus(options.macosStatus, "macOS"); + const findings = intersectFindings( + readFindings(options.iosResults, "iOS"), + readFindings(options.macosResults, "macOS"), + ); + + fs.mkdirSync(path.dirname(options.output), { recursive: true }); + fs.writeFileSync(options.output, `${JSON.stringify(findings, null, 2)}\n`); + for (const finding of findings) { + console.log(formatAnnotation(finding)); + } + + const summary = buildSummary(findings); + if (env.GITHUB_STEP_SUMMARY) { + fs.appendFileSync(env.GITHUB_STEP_SUMMARY, `${summary}\n`); + } else { + console.log(summary); + } + return findings.length === 0 ? 0 : 1; +} + +if (isDirectRunUrl(process.argv[1], import.meta.url)) { + try { + process.exitCode = run(process.argv.slice(2)); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error( + `::error title=Shared Periphery intersection failed::${escapeCommandData(message)}`, + ); + process.exitCode = 2; + } +} diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 74f723339c34..e45ce5ef9aee 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -503,6 +503,10 @@ const GITHUB_WORKFLOW_OWNER_TEST_TARGETS = new Map([ ["test/scripts/ios-periphery-comment-workflow.test.ts"], ], [".github/workflows/ios-periphery.yml", ["test/scripts/ios-periphery-comment-workflow.test.ts"]], + [ + ".github/workflows/shared-openclawkit-periphery.yml", + ["test/scripts/periphery-intersection.test.ts"], + ], [ ".github/workflows/live-media-runner-image.yml", ["test/scripts/package-acceptance-workflow.test.ts"], @@ -789,6 +793,7 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([ ], ], ["scripts/ci-changed-scope.mjs", ["src/scripts/ci-changed-scope.test.ts"]], + ["scripts/periphery-intersection.mjs", ["test/scripts/periphery-intersection.test.ts"]], ["scripts/ci-docker-pull-retry.sh", ["test/scripts/ci-docker-pull-retry.test.ts"]], ["scripts/control-ui-i18n.ts", ["test/scripts/control-ui-i18n.test.ts"]], ["scripts/apple-app-i18n.ts", ["test/scripts/apple-app-i18n.test.ts"]], @@ -2060,6 +2065,7 @@ const TOOLING_DECLARATION_SOURCE_MIRRORS = [ ["scripts/ci-changed-scope.d.mts", "scripts/ci-changed-scope.mjs"], ["scripts/copy-bundled-plugin-metadata.d.mts", "scripts/copy-bundled-plugin-metadata.mjs"], ["scripts/docs-link-audit.d.mts", "scripts/docs-link-audit.mjs"], + ["scripts/periphery-intersection.d.mts", "scripts/periphery-intersection.mjs"], [ "scripts/lib/bundled-plugin-build-entries.d.mts", "scripts/lib/bundled-plugin-build-entries.mjs", diff --git a/test/scripts/periphery-intersection.test.ts b/test/scripts/periphery-intersection.test.ts new file mode 100644 index 000000000000..3bdd8d66e5a2 --- /dev/null +++ b/test/scripts/periphery-intersection.test.ts @@ -0,0 +1,157 @@ +import { readFileSync } from "node:fs"; +import { compileFunction } from "node:vm"; +import { describe, expect, it } from "vitest"; +import { parse } from "yaml"; +import { + buildSummary, + formatAnnotation, + intersectFindings, + parseRepoLocation, + validateFindings, +} from "../../scripts/periphery-intersection.mjs"; + +const WORKFLOW_PATH = ".github/workflows/shared-openclawkit-periphery.yml"; + +type WorkflowStep = { + id?: string; + name?: string; + run?: string; + with?: { name?: string; path?: string; script?: string }; +}; + +type Workflow = { + jobs?: Record< + string, + { + name?: string; + needs?: string[] | string; + steps?: WorkflowStep[]; + } + >; +}; + +function finding(overrides: Record = {}) { + return { + ids: ["s:11OpenClawKit7ExampleV"], + kind: "struct", + location: "../shared/OpenClawKit/Sources/OpenClawKit/Example.swift:12:8", + name: "Example", + ...overrides, + }; +} + +describe("Periphery intersection", () => { + it("matches exact Swift USRs instead of declaration names", () => { + const sameNameDifferentUsr = finding({ ids: ["s:11OpenClawKit7ExampleV_other"] }); + expect(intersectFindings([finding()], [sameNameDifferentUsr])).toEqual([]); + expect(intersectFindings([finding()], [finding()])).toEqual([finding()]); + }); + + it("matches any USR emitted for a declaration compiled into multiple iOS modules", () => { + const ios = finding({ ids: ["s:16OpenClawWatchApp7ExampleV", "s:11OpenClawKit7ExampleV"] }); + expect(intersectFindings([ios], [finding()])).toEqual([ios]); + }); + + it("sorts findings deterministically", () => { + const later = finding({ + ids: ["s:11OpenClawKit5LaterV"], + location: "../shared/OpenClawKit/Sources/OpenClawKit/Later.swift:2:1", + name: "Later", + }); + expect(intersectFindings([later, finding()], [finding(), later])).toEqual([finding(), later]); + }); + + it("fails closed when a finding has no USR", () => { + expect(() => validateFindings([finding({ ids: [] })], "iOS")).toThrow( + "iOS finding 0 has no usable Swift USR", + ); + }); + + it("rejects findings outside shared OpenClawKit", () => { + expect(() => + validateFindings([finding({ location: "Sources/App.swift:1:1" })], "macOS"), + ).toThrow("macOS finding 0 is outside shared OpenClawKit sources"); + }); + + it("maps relative scan locations to repository annotations", () => { + expect(parseRepoLocation(finding().location)).toEqual({ + column: "8", + file: "apps/shared/OpenClawKit/Sources/OpenClawKit/Example.swift", + line: "12", + }); + expect(formatAnnotation(finding())).toBe( + "::error file=apps/shared/OpenClawKit/Sources/OpenClawKit/Example.swift,line=12,col=8,title=Dead shared Swift code::struct Example", + ); + }); + + it("reports the zero-findings policy in the summary", () => { + expect(buildSummary([])).toContain("No declarations were reported dead by both"); + expect(buildSummary([finding()])).toContain("Found 1 shared Swift declaration"); + }); +}); + +describe("shared OpenClawKit Periphery workflow", () => { + const workflow = parse(readFileSync(WORKFLOW_PATH, "utf8")) as Workflow; + + it("runs two consumer scans and a same-run intersection", () => { + expect(workflow.jobs?.["scan-ios"]?.name).toBe("Scan shared kit from iOS"); + expect(workflow.jobs?.["scan-macos"]?.name).toBe("Scan shared kit from macOS"); + expect(workflow.jobs?.intersect?.needs).toEqual(["scope", "scan-ios", "scan-macos"]); + + const iosUpload = workflow.jobs?.["scan-ios"]?.steps?.find( + (step) => step.name === "Upload iOS consumer report", + ); + const macosUpload = workflow.jobs?.["scan-macos"]?.steps?.find( + (step) => step.name === "Upload macOS consumer report", + ); + expect(iosUpload?.with?.name).toContain("shared-periphery-ios-"); + expect(macosUpload?.with?.name).toContain("shared-periphery-macos-"); + }); + + it("retains the generated protocol contract and leaves findings for the intersection", () => { + for (const jobName of ["scan-ios", "scan-macos"]) { + const scan = workflow.jobs?.[jobName]?.steps?.find((step) => step.name === "Scan shared kit"); + expect(scan?.run).toContain("--report-include '../shared/OpenClawKit/Sources/**'"); + expect(scan?.run).toContain( + "--retain-files '../shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift'", + ); + expect(scan?.run).not.toContain("--strict"); + } + const macosScan = workflow.jobs?.["scan-macos"]?.steps?.find( + (step) => step.name === "Scan shared kit", + ); + expect(macosScan?.run).not.toContain("--exclude-tests"); + }); + + it("scopes native consumer, shared package, and workflow changes", async () => { + const script = workflow.jobs?.scope?.steps?.find((step) => step.id === "scope")?.with?.script; + expect(script).toBeTruthy(); + const execute = compileFunction(`return (async () => {\n${script}\n})();`, [ + "context", + "core", + "github", + ]) as (context: unknown, core: unknown, github: unknown) => Promise; + + for (const filename of [ + "apps/ios/Sources/App.swift", + "apps/macos/Sources/OpenClaw/App.swift", + "apps/shared/OpenClawKit/Sources/OpenClawKit/Example.swift", + WORKFLOW_PATH, + ]) { + const outputs = new Map(); + await execute( + { + eventName: "pull_request", + payload: { pull_request: { draft: false, number: 1 } }, + repo: {}, + }, + { setOutput: (name: string, value: string) => outputs.set(name, value) }, + { + paginate: async () => [{ filename }], + rest: { pulls: { listFiles() {} } }, + }, + ); + expect(outputs.get("should-scan"), filename).toBe("true"); + } + }); +});