From e04d366d12ab460a2503312c2e206f712777eaff Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 13:42:22 +0000 Subject: [PATCH 1/5] refactor(platforms): break the four upward edges out of src/platforms (#2082 W2) src/platforms carried four imports that point up into root src, each of which would become an R11 violation the moment its family moves into a platform package: - android/app-helpers.ts reached the composition root for the foreground parser. The parser is pure dumpsys vocabulary, so it moves to @agent-device/contracts/android-observation; the platform-android app-state module and app-helpers both consume it from there, and the composition wrapper plus the package facade's lazy re-export retire (R13 allows only the composition root to import platform packages, so vocabulary relocation is the inversion that stays legal). - web/provider.ts and web/agent-browser-network.ts type-imported the backend diagnostics/network-dump vocabulary from src/backend.ts. Those six types move to @agent-device/contracts/backend-diagnostics; backend.ts re-exports them for its SDK consumers. - snapshot/snapshot-desktop-surface.ts split three ways: the pure projection (scope/interactive/depth) moves to @agent-device/contracts/snapshot-desktop-projection, the per-family captures move to platforms/linux/surface-snapshot.ts and platforms/apple/os/macos/surface-snapshot.ts beside the code they dispatch to, and the root file keeps only the device-dispatching runtime host behind R3-tolerated dynamic imports. apple/interactor now reaches macOS surface capture family-internally instead of through root. src/platforms -> root src is now zero edges. Contracts grows two entries (pinned, budgeted); the android foreground-parser tests move beside the parser. Left for a later pass: app-parsers.ts shares the marker-walk loop shape with the contracts parser but parameterizes it for blocking-dialog parsing - generalizing that is a design change, not a move. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH --- packages/contracts/package.json | 8 ++ .../src/android-observation.test.ts} | 2 +- packages/contracts/src/android-observation.ts | 26 ++++ packages/contracts/src/backend-diagnostics.ts | 39 ++++++ .../src/snapshot-desktop-projection.test.ts | 46 +++++++ .../src/snapshot-desktop-projection.ts | 77 +++++++++++ packages/platform-android/src/app-state.ts | 25 +--- packages/platform-android/src/index.ts | 7 - scripts/layering/package-boundaries.test.ts | 2 + src/__tests__/eager-closure-budgets.ts | 2 + src/backend.ts | 53 +++----- src/core/interactors/linux.ts | 2 +- src/core/snapshot-state.ts | 2 +- src/platform-runtime.ts | 7 - src/platforms/android/app-helpers.ts | 4 +- src/platforms/apple/interactor.ts | 2 +- .../apple/os/macos/surface-snapshot.ts | 20 +++ src/platforms/linux/surface-snapshot.ts | 19 +++ src/platforms/web/agent-browser-network.ts | 2 +- src/platforms/web/provider.ts | 5 +- src/snapshot/snapshot-desktop-surface.test.ts | 46 +------ src/snapshot/snapshot-desktop-surface.ts | 120 +----------------- 22 files changed, 272 insertions(+), 244 deletions(-) rename packages/{platform-android/src/app-state.test.ts => contracts/src/android-observation.test.ts} (94%) create mode 100644 packages/contracts/src/backend-diagnostics.ts create mode 100644 packages/contracts/src/snapshot-desktop-projection.test.ts create mode 100644 packages/contracts/src/snapshot-desktop-projection.ts create mode 100644 src/platforms/apple/os/macos/surface-snapshot.ts create mode 100644 src/platforms/linux/surface-snapshot.ts diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 5df12899ff..32dd04c540 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -127,6 +127,10 @@ "types": "./src/back-runtime.ts", "default": "./src/back-runtime.ts" }, + "./backend-diagnostics": { + "types": "./src/backend-diagnostics.ts", + "default": "./src/backend-diagnostics.ts" + }, "./boot-failure": { "types": "./src/boot-failure.ts", "default": "./src/boot-failure.ts" @@ -415,6 +419,10 @@ "types": "./src/facades/snapshot.ts", "default": "./src/facades/snapshot.ts" }, + "./snapshot-desktop-projection": { + "types": "./src/snapshot-desktop-projection.ts", + "default": "./src/snapshot-desktop-projection.ts" + }, "./snapshot-presentation": { "types": "./src/snapshot-presentation.ts", "default": "./src/snapshot-presentation.ts" diff --git a/packages/platform-android/src/app-state.test.ts b/packages/contracts/src/android-observation.test.ts similarity index 94% rename from packages/platform-android/src/app-state.test.ts rename to packages/contracts/src/android-observation.test.ts index 8601abe6f9..2d443c0ae0 100644 --- a/packages/platform-android/src/app-state.test.ts +++ b/packages/contracts/src/android-observation.test.ts @@ -1,5 +1,5 @@ import { expect, test } from 'vitest'; -import { parseAndroidForegroundApp } from './app-state.ts'; +import { parseAndroidForegroundApp } from './android-observation.ts'; test('parses Android window and activity foreground markers', () => { expect( diff --git a/packages/contracts/src/android-observation.ts b/packages/contracts/src/android-observation.ts index 82080d1ce9..6000ae7647 100644 --- a/packages/contracts/src/android-observation.ts +++ b/packages/contracts/src/android-observation.ts @@ -48,3 +48,29 @@ export type AndroidObservationAdapter = Readonly<{ readScreenSize(device: DeviceInfo): Promise>; isPermissionPackage(packageName: string): Promise; }>; + +const ANDROID_FOCUS_MARKERS = [ + 'mCurrentFocus=Window{', + 'mFocusedApp=AppWindowToken{', + 'mResumedActivity:', + 'ResumedActivity:', +] as const; + +/** Extracts the foreground package/activity from `dumpsys window`/`activity` output. */ +export function parseAndroidForegroundApp(text: string): AppStateRuntimeResult | null { + const lines = text.split('\n'); + for (const marker of ANDROID_FOCUS_MARKERS) { + for (const line of lines) { + const markerIndex = line.indexOf(marker); + if (markerIndex === -1) continue; + const parsed = parseAndroidComponentFromSegment(line.slice(markerIndex + marker.length)); + if (parsed) return parsed; + } + } + return null; +} + +function parseAndroidComponentFromSegment(segment: string): AppStateRuntimeResult | null { + const match = segment.match(/\b([A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z0-9_]+)+)\/([A-Za-z0-9_.$]+)/); + return match?.[1] && match[2] ? { package: match[1], activity: match[2] } : null; +} diff --git a/packages/contracts/src/backend-diagnostics.ts b/packages/contracts/src/backend-diagnostics.ts new file mode 100644 index 0000000000..4225c9f510 --- /dev/null +++ b/packages/contracts/src/backend-diagnostics.ts @@ -0,0 +1,39 @@ +import type { NetworkIncludeMode } from '@agent-device/kernel/contracts'; + +export type BackendDiagnosticsTimeWindow = { + since?: string; + until?: string; +}; + +export type BackendDiagnosticsPageOptions = BackendDiagnosticsTimeWindow & { + cursor?: string; + limit?: number; +}; + +export type BackendNetworkIncludeMode = NetworkIncludeMode; + +export type BackendNetworkEntry = { + timestamp?: string; + method?: string; + url?: string; + status?: number; + durationMs?: number; + requestHeaders?: Record; + responseHeaders?: Record; + requestBody?: string; + responseBody?: string; + metadata?: Record; +}; + +export type BackendDumpNetworkOptions = BackendDiagnosticsPageOptions & { + include?: BackendNetworkIncludeMode; +}; + +export type BackendDumpNetworkResult = { + entries: readonly BackendNetworkEntry[]; + nextCursor?: string; + timeWindow?: BackendDiagnosticsTimeWindow; + backend?: string; + redacted?: boolean; + notes?: readonly string[]; +}; diff --git a/packages/contracts/src/snapshot-desktop-projection.test.ts b/packages/contracts/src/snapshot-desktop-projection.test.ts new file mode 100644 index 0000000000..8989e8fd79 --- /dev/null +++ b/packages/contracts/src/snapshot-desktop-projection.test.ts @@ -0,0 +1,46 @@ +import { expect, test } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { scopeSnapshotNodes } from './snapshot-desktop-projection.ts'; + +// Golden scope-policy leg (#1832 C2): the post-wire pass must agree with +// contracts/fixtures/snapshot-scope-policy.json — the same table the Android projection and the +// shared predicate are asserted against. Fixture position rides in `rect.x` so the check is +// text-independent. +test('scopeSnapshotNodes agrees with every golden scope-policy table case', () => { + const cases = JSON.parse( + fs.readFileSync( + path.resolve(import.meta.dirname, '../../../contracts/fixtures/snapshot-scope-policy.json'), + 'utf8', + ), + ) as Array<{ + name: string; + scope: string; + nodes: Array<{ + depth: number; + label?: string; + value?: string; + identifier?: string; + presented?: boolean; + }>; + expectedSubtreeIndexes: number[]; + }>; + expect(cases.length).toBeGreaterThan(0); + for (const fixture of cases) { + const parents: number[] = []; + const nodes = fixture.nodes.map((node, index) => { + parents.length = node.depth; + const parentIndex = node.depth > 0 ? parents[node.depth - 1] : undefined; + parents[node.depth] = index; + return { ...node, index, parentIndex, rect: { x: index, y: 0, width: 1, height: 1 } }; + }); + const scoped = scopeSnapshotNodes(nodes, fixture.scope, (range) => + nodes.slice(range.start, range.end).some((node) => node.presented !== false), + ); + expect( + scoped.map((node) => node.rect?.x), + fixture.name, + ).toEqual(fixture.expectedSubtreeIndexes); + if (scoped.length > 0) expect(scoped[0]?.depth, fixture.name).toBe(0); + } +}); diff --git a/packages/contracts/src/snapshot-desktop-projection.ts b/packages/contracts/src/snapshot-desktop-projection.ts new file mode 100644 index 0000000000..83b7a62cce --- /dev/null +++ b/packages/contracts/src/snapshot-desktop-projection.ts @@ -0,0 +1,77 @@ +import type { RawSnapshotNode } from '@agent-device/kernel/snapshot'; +import type { SnapshotOptions, SnapshotResult } from './interactor-types.ts'; +import { + findSnapshotScopeRange, + normalizeSnapshotScope, + reindexSnapshotNodes, +} from './snapshot-scope.ts'; + +const INTERACTIVE_ROLE_TOKENS = [ + 'button', + 'menu', + 'textfield', + 'searchfield', + 'checkbox', + 'radio', + 'switch', +] as const; + +/** Applies the legacy desktop-surface projection once for both runtime hosts and legacy capture. */ +export function shapeDesktopSurfaceSnapshot( + data: SnapshotResult, + options: Pick, +): SnapshotResult { + let nodes = data.nodes ?? []; + if (options.scope) { + nodes = scopeSnapshotNodes(nodes, options.scope, (range) => + options.interactiveOnly + ? nodes.slice(range.start, range.end).some(isInteractiveSnapshotNode) + : true, + ); + } + if (options.interactiveOnly) nodes = filterInteractiveSnapshotNodes(nodes); + if (typeof options.depth === 'number') nodes = filterSnapshotNodesByDepth(nodes, options.depth); + return { ...data, nodes }; +} + +/** The shared scope specification applied post-wire (`./snapshot-scope.ts`). */ +export function scopeSnapshotNodes( + nodes: RawSnapshotNode[], + scope: string, + subtreeContributes?: (range: { start: number; end: number }) => boolean, +): RawSnapshotNode[] { + const normalizedScope = normalizeSnapshotScope(scope); + if (!normalizedScope) return reindexSnapshotNodes(nodes); + const range = findSnapshotScopeRange(nodes, normalizedScope, subtreeContributes); + if (!range) return []; + const slice = nodes.slice(range.start, range.end); + return reindexSnapshotNodes(slice, slice[0]?.depth ?? 0); +} + +function filterInteractiveSnapshotNodes(nodes: RawSnapshotNode[]): RawSnapshotNode[] { + if (nodes.length === 0) return nodes; + const byIndex = new Map(nodes.map((node) => [node.index, node])); + const keepIndexes = new Set(); + for (const node of nodes) { + if (!isInteractiveSnapshotNode(node)) continue; + let current: RawSnapshotNode | undefined = node; + while (current) { + if (keepIndexes.has(current.index)) break; + keepIndexes.add(current.index); + current = + typeof current.parentIndex === 'number' ? byIndex.get(current.parentIndex) : undefined; + } + } + if (keepIndexes.size === 0) return nodes; + return reindexSnapshotNodes(nodes.filter((node) => keepIndexes.has(node.index))); +} + +function filterSnapshotNodesByDepth(nodes: RawSnapshotNode[], maxDepth: number): RawSnapshotNode[] { + return reindexSnapshotNodes(nodes.filter((node) => (node.depth ?? 0) <= maxDepth)); +} + +function isInteractiveSnapshotNode(node: RawSnapshotNode): boolean { + if ([node.focused, node.hittable, node.rect].some(Boolean)) return true; + const role = `${node.type ?? ''} ${node.role ?? ''} ${node.subrole ?? ''}`.toLowerCase(); + return INTERACTIVE_ROLE_TOKENS.some((token) => role.includes(token)); +} diff --git a/packages/platform-android/src/app-state.ts b/packages/platform-android/src/app-state.ts index ae01ce33da..8d5722c5e0 100644 --- a/packages/platform-android/src/app-state.ts +++ b/packages/platform-android/src/app-state.ts @@ -4,6 +4,7 @@ import type { AppStateRuntimeResult, } from '@agent-device/contracts/app-state-runtime'; import type { DeviceInfo } from '@agent-device/kernel/device'; +import { parseAndroidForegroundApp } from '@agent-device/contracts/android-observation'; export type AndroidAppStateHost = Readonly<{ run( @@ -21,12 +22,6 @@ const ACTIVITY_COMMANDS = [ ['shell', 'dumpsys', 'activity', 'activities'], ['shell', 'dumpsys', 'activity'], ] as const; -const ANDROID_FOCUS_MARKERS = [ - 'mCurrentFocus=Window{', - 'mFocusedApp=AppWindowToken{', - 'mResumedActivity:', - 'ResumedActivity:', -] as const; export async function readAndroidAppState( host: AndroidAppStateHost, @@ -41,19 +36,6 @@ export async function readAndroidAppState( return {}; } -export function parseAndroidForegroundApp(text: string): AppStateRuntimeResult | null { - const lines = text.split('\n'); - for (const marker of ANDROID_FOCUS_MARKERS) { - for (const line of lines) { - const markerIndex = line.indexOf(marker); - if (markerIndex === -1) continue; - const parsed = parseAndroidComponentFromSegment(line.slice(markerIndex + marker.length)); - if (parsed) return parsed; - } - } - return null; -} - async function readAndroidFocus( host: AndroidAppStateHost, device: DeviceInfo, @@ -69,8 +51,3 @@ async function readAndroidFocus( } return null; } - -function parseAndroidComponentFromSegment(segment: string): AppStateRuntimeResult | null { - const match = segment.match(/\b([A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z0-9_]+)+)\/([A-Za-z0-9_.$]+)/); - return match?.[1] && match[2] ? { package: match[1], activity: match[2] } : null; -} diff --git a/packages/platform-android/src/index.ts b/packages/platform-android/src/index.ts index 8dbee8bd05..f3e6792ef8 100644 --- a/packages/platform-android/src/index.ts +++ b/packages/platform-android/src/index.ts @@ -55,13 +55,6 @@ export async function readAndroidAppState( return await read(host, device, signal); } -export async function parseAndroidForegroundApp( - text: string, -): Promise | null> { - const { parseAndroidForegroundApp: parse } = await import('./app-state.ts'); - return parse(text); -} - export const runtimeModule = Object.freeze({ ...metadata, loadRuntime: async (host) => { diff --git a/scripts/layering/package-boundaries.test.ts b/scripts/layering/package-boundaries.test.ts index 42037013eb..db1b1d8fcb 100644 --- a/scripts/layering/package-boundaries.test.ts +++ b/scripts/layering/package-boundaries.test.ts @@ -75,6 +75,7 @@ const CONTRACT_EXPORTS = [ '@agent-device/contracts/audio-probe-support', '@agent-device/contracts/audio-runtime-plan', '@agent-device/contracts/back-mode', + '@agent-device/contracts/backend-diagnostics', '@agent-device/contracts/back-runtime', '@agent-device/contracts/boot-failure', '@agent-device/contracts/capture', @@ -148,6 +149,7 @@ const CONTRACT_EXPORTS = [ '@agent-device/contracts/settings', '@agent-device/contracts/settings-runtime', '@agent-device/contracts/snapshot', + '@agent-device/contracts/snapshot-desktop-projection', '@agent-device/contracts/snapshot-presentation', '@agent-device/contracts/snapshot-runtime', '@agent-device/contracts/snapshot-timeout-evidence', diff --git a/src/__tests__/eager-closure-budgets.ts b/src/__tests__/eager-closure-budgets.ts index a8cc04988a..07379ea0ce 100644 --- a/src/__tests__/eager-closure-budgets.ts +++ b/src/__tests__/eager-closure-budgets.ts @@ -172,6 +172,7 @@ export const FACADE_BUDGETS: Readonly> = Object.freeze({ 'packages/contracts/src/audio-probe-support.ts': 5, 'packages/contracts/src/audio-runtime-plan.ts': 5, 'packages/contracts/src/back-mode.ts': 1, + 'packages/contracts/src/backend-diagnostics.ts': 1, 'packages/contracts/src/boot-failure.ts': 1, 'packages/contracts/src/click-button.ts': 3, 'packages/contracts/src/clipboard.ts': 1, @@ -237,6 +238,7 @@ export const FACADE_BUDGETS: Readonly> = Object.freeze({ 'packages/contracts/src/scroll-runtime.ts': 4, 'packages/contracts/src/selector-observation-runtime.ts': 1, 'packages/contracts/src/settings.ts': 3, + 'packages/contracts/src/snapshot-desktop-projection.ts': 2, 'packages/contracts/src/snapshot-presentation.ts': 2, 'packages/contracts/src/snapshot-runtime.ts': 3, 'packages/contracts/src/snapshot-timeout-evidence.ts': 1, diff --git a/src/backend.ts b/src/backend.ts index 488fbfcf4c..51cedac410 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -15,7 +15,6 @@ import type { ResolvedScrollExecutionOptions } from '@agent-device/contracts/scr import type { TvRemoteButton } from '@agent-device/contracts/tv-remote'; import type { RecordingExportQuality } from '@agent-device/contracts/recording'; import type { SessionSurface } from '@agent-device/contracts/session'; -import type { NetworkIncludeMode } from '@agent-device/kernel/contracts'; import type { DeviceTarget, Platform, @@ -307,14 +306,22 @@ export type BackendTraceResult = Record & { outPath?: string; }; -export type BackendDiagnosticsTimeWindow = { - since?: string; - until?: string; -}; - -export type BackendDiagnosticsPageOptions = BackendDiagnosticsTimeWindow & { - cursor?: string; - limit?: number; +import type { + BackendDiagnosticsPageOptions, + BackendDiagnosticsTimeWindow, + BackendDumpNetworkOptions, + BackendDumpNetworkResult, + BackendNetworkEntry, + BackendNetworkIncludeMode, +} from '@agent-device/contracts/backend-diagnostics'; + +export type { + BackendDiagnosticsPageOptions, + BackendDiagnosticsTimeWindow, + BackendDumpNetworkOptions, + BackendDumpNetworkResult, + BackendNetworkEntry, + BackendNetworkIncludeMode, }; export type BackendLogEntry = { @@ -340,34 +347,6 @@ export type BackendReadLogsResult = { notes?: readonly string[]; }; -export type BackendNetworkIncludeMode = NetworkIncludeMode; - -export type BackendNetworkEntry = { - timestamp?: string; - method?: string; - url?: string; - status?: number; - durationMs?: number; - requestHeaders?: Record; - responseHeaders?: Record; - requestBody?: string; - responseBody?: string; - metadata?: Record; -}; - -export type BackendDumpNetworkOptions = BackendDiagnosticsPageOptions & { - include?: BackendNetworkIncludeMode; -}; - -export type BackendDumpNetworkResult = { - entries: readonly BackendNetworkEntry[]; - nextCursor?: string; - timeWindow?: BackendDiagnosticsTimeWindow; - backend?: string; - redacted?: boolean; - notes?: readonly string[]; -}; - export type BackendPerfMetric = { name: string; value?: number; diff --git a/src/core/interactors/linux.ts b/src/core/interactors/linux.ts index cd8eac4fa7..6635740c6a 100644 --- a/src/core/interactors/linux.ts +++ b/src/core/interactors/linux.ts @@ -21,7 +21,7 @@ import { } from '../../platforms/linux/input-actions.ts'; import { singlePointerPlanEndpoints } from '@agent-device/contracts/gesture-plan'; import { screenshotLinux } from '../../platforms/linux/screenshot.ts'; -import { captureLinuxSurfaceSnapshot } from '../../snapshot/snapshot-desktop-surface.ts'; +import { captureLinuxSurfaceSnapshot } from '../../platforms/linux/surface-snapshot.ts'; import type { Interactor } from '@agent-device/contracts/interactor-types'; function unsupportedLinuxAlert(): Promise { diff --git a/src/core/snapshot-state.ts b/src/core/snapshot-state.ts index b90f78fcf1..7756311b80 100644 --- a/src/core/snapshot-state.ts +++ b/src/core/snapshot-state.ts @@ -19,7 +19,7 @@ import { annotateSnapshotNodesCoveredByPolicy, } from '@agent-device/capture-kit/snapshot-occlusion'; import { coveredAndroidReplacementNodeIndexes } from '../snapshot/android-replacement-surface-occlusion.ts'; -import { scopeSnapshotNodes } from '../snapshot/snapshot-desktop-surface.ts'; +import { scopeSnapshotNodes } from '@agent-device/contracts/snapshot-desktop-projection'; import { normalizeSnapshotTree, pruneGroupNodes } from '../core/snapshot-tree-ingestion.ts'; import { presentIosInteractiveSnapshot } from '../snapshot/snapshot-presentation/ios/index.ts'; diff --git a/src/platform-runtime.ts b/src/platform-runtime.ts index fdfe090c3d..3e75f6952b 100644 --- a/src/platform-runtime.ts +++ b/src/platform-runtime.ts @@ -31,7 +31,6 @@ import { import { createAndroidObservationAdapter as createPackageAndroidObservationAdapter, createAndroidInventoryModule, - parseAndroidForegroundApp as parseAndroidPackageForegroundApp, readAndroidAppState as readAndroidPackageAppState, loadShutdownRuntime as loadAndroidShutdownRuntime, runtimeModule as androidRuntimeModule, @@ -74,12 +73,6 @@ export async function readAndroidAppStateWithHost( return await readAndroidPackageAppState(host, device, signal); } -export async function parseAndroidForegroundApp( - text: string, -): Promise | null> { - return await parseAndroidPackageForegroundApp(text); -} - const androidInventoryModule = createAndroidInventoryModule({ sdkRoots: configuredValues(process.env.ANDROID_SDK_ROOT, process.env.ANDROID_HOME), }); diff --git a/src/platforms/android/app-helpers.ts b/src/platforms/android/app-helpers.ts index c42f9c5130..1b9f85346b 100644 --- a/src/platforms/android/app-helpers.ts +++ b/src/platforms/android/app-helpers.ts @@ -1,3 +1,4 @@ +import { parseAndroidForegroundApp } from '@agent-device/contracts/android-observation'; import { resolveAppsFilter, type AppsFilter } from '@agent-device/contracts/device'; import type { AppStateRuntimeResult } from '@agent-device/contracts/app-state-runtime'; import { androidAdbResultError, type AndroidAdbExecutor } from './adb-executor.ts'; @@ -108,10 +109,9 @@ async function readAndroidFocusWithAdb( adb: AndroidAdbExecutor, commands: string[][], ): Promise { - const { parseAndroidForegroundApp } = await import('../../platform-runtime.ts'); for (const args of commands) { const result = await adb(args, { allowFailure: true }); - const parsed = await parseAndroidForegroundApp(result.stdout ?? ''); + const parsed = parseAndroidForegroundApp(result.stdout ?? ''); if (parsed) return parsed; } return null; diff --git a/src/platforms/apple/interactor.ts b/src/platforms/apple/interactor.ts index 5e19b22ae9..5f95933438 100644 --- a/src/platforms/apple/interactor.ts +++ b/src/platforms/apple/interactor.ts @@ -36,7 +36,7 @@ import type { SnapshotOptions, } from '@agent-device/contracts/interactor-types'; import { readSnapshotQualityVerdict } from '@agent-device/capture-kit/snapshot-quality-verdict'; -import { captureMacOsSurfaceSnapshot } from '../../snapshot/snapshot-desktop-surface.ts'; +import { captureMacOsSurfaceSnapshot } from './os/macos/surface-snapshot.ts'; export function createAppleInteractor( device: DeviceInfo, diff --git a/src/platforms/apple/os/macos/surface-snapshot.ts b/src/platforms/apple/os/macos/surface-snapshot.ts new file mode 100644 index 0000000000..a65d99bd47 --- /dev/null +++ b/src/platforms/apple/os/macos/surface-snapshot.ts @@ -0,0 +1,20 @@ +import type { CaptureSnapshotInput } from '@agent-device/contracts/snapshot-runtime'; +import { shapeDesktopSurfaceSnapshot } from '@agent-device/contracts/snapshot-desktop-projection'; + +type SnapshotSurfaceOptions = NonNullable; + +export async function captureMacOsSurfaceSnapshot( + options: SnapshotSurfaceOptions, + signal?: AbortSignal, +) { + const surface = options.surface; + if (!surface || surface === 'app') { + throw new TypeError('Apple surface capture requires a non-app macOS surface'); + } + const { runMacOsSnapshotAction } = await import('./helper.ts'); + const result = await runMacOsSnapshotAction(surface, { + bundleId: surface === 'menubar' ? options.appBundleId : undefined, + signal, + }); + return shapeDesktopSurfaceSnapshot({ ...result, producer: 'macos-helper' }, options); +} diff --git a/src/platforms/linux/surface-snapshot.ts b/src/platforms/linux/surface-snapshot.ts new file mode 100644 index 0000000000..6ca0f352fb --- /dev/null +++ b/src/platforms/linux/surface-snapshot.ts @@ -0,0 +1,19 @@ +import type { CaptureSnapshotInput } from '@agent-device/contracts/snapshot-runtime'; +import { shapeDesktopSurfaceSnapshot } from '@agent-device/contracts/snapshot-desktop-projection'; + +export async function captureLinuxSurfaceSnapshot( + options: CaptureSnapshotInput['options'], + signal?: AbortSignal, +) { + const { snapshotLinux } = await import('./snapshot.ts'); + const result = await snapshotLinux(options?.surface, signal); + return shapeDesktopSurfaceSnapshot( + { + nodes: result.nodes, + truncated: result.truncated, + backend: 'linux-atspi', + producer: 'linux-atspi', + }, + options ?? {}, + ); +} diff --git a/src/platforms/web/agent-browser-network.ts b/src/platforms/web/agent-browser-network.ts index 7991c3d0a9..e0257d15f6 100644 --- a/src/platforms/web/agent-browser-network.ts +++ b/src/platforms/web/agent-browser-network.ts @@ -2,7 +2,7 @@ import type { BackendDumpNetworkOptions, BackendDumpNetworkResult, BackendNetworkEntry, -} from '../../backend.ts'; +} from '@agent-device/contracts/backend-diagnostics'; import { stripUndefined } from '@agent-device/kernel/record'; import { isJsonObject, readNumberProperty, readStringProperty } from './json-utils.ts'; diff --git a/src/platforms/web/provider.ts b/src/platforms/web/provider.ts index 43a1d19f6e..1618a5015d 100644 --- a/src/platforms/web/provider.ts +++ b/src/platforms/web/provider.ts @@ -2,7 +2,10 @@ import type { ScrollDirection } from '@agent-device/contracts/scroll-gesture'; import type { SessionSurface } from '@agent-device/contracts/session'; import { createScopedProvider } from '@agent-device/kernel/scoped-provider'; import type { RawSnapshotNode } from '@agent-device/kernel/snapshot'; -import type { BackendDumpNetworkOptions, BackendDumpNetworkResult } from '../../backend.ts'; +import type { + BackendDumpNetworkOptions, + BackendDumpNetworkResult, +} from '@agent-device/contracts/backend-diagnostics'; import type { AudioProbeResult } from '@agent-device/contracts/audio-probe-result'; import { createAgentBrowserWebProvider } from './agent-browser-provider.ts'; diff --git a/src/snapshot/snapshot-desktop-surface.test.ts b/src/snapshot/snapshot-desktop-surface.test.ts index b8418a370a..d80893042d 100644 --- a/src/snapshot/snapshot-desktop-surface.test.ts +++ b/src/snapshot/snapshot-desktop-surface.test.ts @@ -1,6 +1,4 @@ import { beforeEach, expect, test, vi } from 'vitest'; -import fs from 'node:fs'; -import path from 'node:path'; import type { DeviceInfo } from '@agent-device/kernel/device'; const { runMacOsSnapshotAction, snapshotLinux } = vi.hoisted(() => ({ @@ -11,7 +9,7 @@ const { runMacOsSnapshotAction, snapshotLinux } = vi.hoisted(() => ({ vi.mock('../platforms/apple/os/macos/helper.ts', () => ({ runMacOsSnapshotAction })); vi.mock('../platforms/linux/snapshot.ts', () => ({ snapshotLinux })); -import { createSnapshotRuntimeHost, scopeSnapshotNodes } from './snapshot-desktop-surface.ts'; +import { createSnapshotRuntimeHost } from './snapshot-desktop-surface.ts'; const macosDevice = { id: 'desktop', @@ -94,45 +92,3 @@ test('Linux snapshot host preserves interactive ancestor projection before depth ], }); }); - -// Golden scope-policy leg (#1832 C2): the post-wire pass must agree with -// contracts/fixtures/snapshot-scope-policy.json — the same table the Android projection and the -// shared predicate are asserted against. Fixture position rides in `rect.x` so the check is -// text-independent. -test('scopeSnapshotNodes agrees with every golden scope-policy table case', () => { - const cases = JSON.parse( - fs.readFileSync( - path.resolve(import.meta.dirname, '../../contracts/fixtures/snapshot-scope-policy.json'), - 'utf8', - ), - ) as Array<{ - name: string; - scope: string; - nodes: Array<{ - depth: number; - label?: string; - value?: string; - identifier?: string; - presented?: boolean; - }>; - expectedSubtreeIndexes: number[]; - }>; - expect(cases.length).toBeGreaterThan(0); - for (const fixture of cases) { - const parents: number[] = []; - const nodes = fixture.nodes.map((node, index) => { - parents.length = node.depth; - const parentIndex = node.depth > 0 ? parents[node.depth - 1] : undefined; - parents[node.depth] = index; - return { ...node, index, parentIndex, rect: { x: index, y: 0, width: 1, height: 1 } }; - }); - const scoped = scopeSnapshotNodes(nodes, fixture.scope, (range) => - nodes.slice(range.start, range.end).some((node) => node.presented !== false), - ); - expect( - scoped.map((node) => node.rect?.x), - fixture.name, - ).toEqual(fixture.expectedSubtreeIndexes); - if (scoped.length > 0) expect(scoped[0]?.depth, fixture.name).toBe(0); - } -}); diff --git a/src/snapshot/snapshot-desktop-surface.ts b/src/snapshot/snapshot-desktop-surface.ts index 57628d1d77..7d1cc6b165 100644 --- a/src/snapshot/snapshot-desktop-surface.ts +++ b/src/snapshot/snapshot-desktop-surface.ts @@ -1,60 +1,18 @@ -import type { RawSnapshotNode } from '@agent-device/kernel/snapshot'; -import type { SnapshotOptions, SnapshotResult } from '@agent-device/contracts/interactor-types'; -import type { - CaptureSnapshotInput, - SnapshotRuntimeHost, -} from '@agent-device/contracts/snapshot-runtime'; +import type { SnapshotRuntimeHost } from '@agent-device/contracts/snapshot-runtime'; import type { DeviceInfo } from '@agent-device/kernel/device'; -import { - findSnapshotScopeRange, - normalizeSnapshotScope, - reindexSnapshotNodes, -} from '@agent-device/contracts/snapshot'; - -type SnapshotSurfaceOptions = NonNullable; export function createSnapshotRuntimeHost(): SnapshotRuntimeHost { return Object.freeze({ captureSurface }); } -export async function captureLinuxSurfaceSnapshot( - options: CaptureSnapshotInput['options'], - signal?: AbortSignal, -) { - const { snapshotLinux } = await import('../platforms/linux/snapshot.ts'); - const result = await snapshotLinux(options?.surface, signal); - return shapeDesktopSurfaceSnapshot( - { - nodes: result.nodes, - truncated: result.truncated, - backend: 'linux-atspi', - producer: 'linux-atspi', - }, - options ?? {}, - ); -} - -export async function captureMacOsSurfaceSnapshot( - options: SnapshotSurfaceOptions, - signal?: AbortSignal, -) { - const surface = options.surface; - if (!surface || surface === 'app') { - throw new TypeError('Apple surface capture requires a non-app macOS surface'); - } - const { runMacOsSnapshotAction } = await import('../platforms/apple/os/macos/helper.ts'); - const result = await runMacOsSnapshotAction(surface, { - bundleId: surface === 'menubar' ? options.appBundleId : undefined, - signal, - }); - return shapeDesktopSurfaceSnapshot({ ...result, producer: 'macos-helper' }, options); -} - const captureSurface: SnapshotRuntimeHost['captureSurface'] = async (device, options, signal) => { if (device.platform === 'linux') { + const { captureLinuxSurfaceSnapshot } = await import('../platforms/linux/surface-snapshot.ts'); return await captureLinuxSurfaceSnapshot(options, signal); } requireMacOsSurfaceDevice(device); + const { captureMacOsSurfaceSnapshot } = + await import('../platforms/apple/os/macos/surface-snapshot.ts'); return await captureMacOsSurfaceSnapshot(options ?? {}, signal); }; @@ -63,73 +21,3 @@ function requireMacOsSurfaceDevice(device: DeviceInfo): void { throw new TypeError('Apple surface capture requires a non-app macOS surface'); } } - -const INTERACTIVE_ROLE_TOKENS = [ - 'button', - 'menu', - 'textfield', - 'searchfield', - 'checkbox', - 'radio', - 'switch', -] as const; - -/** Applies the legacy desktop-surface projection once for both runtime hosts and legacy capture. */ -function shapeDesktopSurfaceSnapshot( - data: SnapshotResult, - options: Pick, -): SnapshotResult { - let nodes = data.nodes ?? []; - if (options.scope) { - nodes = scopeSnapshotNodes(nodes, options.scope, (range) => - options.interactiveOnly - ? nodes.slice(range.start, range.end).some(isInteractiveSnapshotNode) - : true, - ); - } - if (options.interactiveOnly) nodes = filterInteractiveSnapshotNodes(nodes); - if (typeof options.depth === 'number') nodes = filterSnapshotNodesByDepth(nodes, options.depth); - return { ...data, nodes }; -} - -/** The shared scope specification applied post-wire (`@agent-device/contracts/snapshot`). */ -export function scopeSnapshotNodes( - nodes: RawSnapshotNode[], - scope: string, - subtreeContributes?: (range: { start: number; end: number }) => boolean, -): RawSnapshotNode[] { - const normalizedScope = normalizeSnapshotScope(scope); - if (!normalizedScope) return reindexSnapshotNodes(nodes); - const range = findSnapshotScopeRange(nodes, normalizedScope, subtreeContributes); - if (!range) return []; - const slice = nodes.slice(range.start, range.end); - return reindexSnapshotNodes(slice, slice[0]?.depth ?? 0); -} - -function filterInteractiveSnapshotNodes(nodes: RawSnapshotNode[]): RawSnapshotNode[] { - if (nodes.length === 0) return nodes; - const byIndex = new Map(nodes.map((node) => [node.index, node])); - const keepIndexes = new Set(); - for (const node of nodes) { - if (!isInteractiveSnapshotNode(node)) continue; - let current: RawSnapshotNode | undefined = node; - while (current) { - if (keepIndexes.has(current.index)) break; - keepIndexes.add(current.index); - current = - typeof current.parentIndex === 'number' ? byIndex.get(current.parentIndex) : undefined; - } - } - if (keepIndexes.size === 0) return nodes; - return reindexSnapshotNodes(nodes.filter((node) => keepIndexes.has(node.index))); -} - -function filterSnapshotNodesByDepth(nodes: RawSnapshotNode[], maxDepth: number): RawSnapshotNode[] { - return reindexSnapshotNodes(nodes.filter((node) => (node.depth ?? 0) <= maxDepth)); -} - -function isInteractiveSnapshotNode(node: RawSnapshotNode): boolean { - if ([node.focused, node.hittable, node.rect].some(Boolean)) return true; - const role = `${node.type ?? ''} ${node.role ?? ''} ${node.subrole ?? ''}`.toLowerCase(); - return INTERACTIVE_ROLE_TOKENS.some((token) => role.includes(token)); -} From 755a2d7f211a4fda0a2b47e6422a71d03603ad1d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 15:40:52 +0000 Subject: [PATCH 2/5] refactor(platforms): keep platform parsing with its family; give snapshot shaping its capture owner ADR-0019's amendment forbids satisfying R13 by moving implementation into contracts, so this wave's two relocations invert instead of sink: - The Android dumpsys foreground parser returns to @agent-device/platform-android with its owning test, and contracts/android-observation goes back to observation vocabulary only. src/platforms/android/app-helpers exposes createAndroidAppStateReader(parseForegroundApp) and never imports upward; the composition seam in src/sdk/android-adb.ts injects the root-composed parser, keeping the published getAndroidAppStateWithAdb(adb) signature intact. - The desktop snapshot projection moves to @agent-device/capture-kit/snapshot-desktop-projection beside the rest of the capture-side snapshot behavior; contracts exports the snapshot-scope vocabulary it consumes. The #1832 history narration in the projection test is gone; the test name and golden fixture carry the invariant. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH --- packages/capture-kit/package.json | 4 ++ .../src/snapshot-desktop-projection.test.ts | 4 -- .../src/snapshot-desktop-projection.ts | 6 +-- packages/contracts/package.json | 8 ++-- packages/contracts/src/android-observation.ts | 26 ----------- .../src/app-state.test.ts} | 2 +- packages/platform-android/src/app-state.ts | 25 ++++++++++- packages/platform-android/src/index.ts | 7 +++ scripts/layering/package-boundaries.test.ts | 3 +- src/__tests__/eager-closure-budgets.ts | 3 +- src/core/snapshot-state.ts | 2 +- src/platform-runtime.ts | 7 +++ .../android/__tests__/app-helpers.test.ts | 3 +- src/platforms/android/app-helpers.ts | 43 ++++++++++++------- .../apple/os/macos/surface-snapshot.ts | 2 +- src/platforms/linux/surface-snapshot.ts | 2 +- src/sdk/android-adb.ts | 21 +++++++-- src/sdk/limrun-runtime-dependencies.test.ts | 4 -- 18 files changed, 103 insertions(+), 69 deletions(-) rename packages/{contracts => capture-kit}/src/snapshot-desktop-projection.test.ts (83%) rename packages/{contracts => capture-kit}/src/snapshot-desktop-projection.ts (92%) rename packages/{contracts/src/android-observation.test.ts => platform-android/src/app-state.test.ts} (94%) diff --git a/packages/capture-kit/package.json b/packages/capture-kit/package.json index 0b2ba0e64e..063685d2c1 100644 --- a/packages/capture-kit/package.json +++ b/packages/capture-kit/package.json @@ -46,6 +46,10 @@ "types": "./src/screenshot-diff-pixels.ts", "default": "./src/screenshot-diff-pixels.ts" }, + "./snapshot-desktop-projection": { + "types": "./src/snapshot-desktop-projection.ts", + "default": "./src/snapshot-desktop-projection.ts" + }, "./snapshot-occlusion": { "types": "./src/snapshot-occlusion.ts", "default": "./src/snapshot-occlusion.ts" diff --git a/packages/contracts/src/snapshot-desktop-projection.test.ts b/packages/capture-kit/src/snapshot-desktop-projection.test.ts similarity index 83% rename from packages/contracts/src/snapshot-desktop-projection.test.ts rename to packages/capture-kit/src/snapshot-desktop-projection.test.ts index 8989e8fd79..768f316935 100644 --- a/packages/contracts/src/snapshot-desktop-projection.test.ts +++ b/packages/capture-kit/src/snapshot-desktop-projection.test.ts @@ -3,10 +3,6 @@ import fs from 'node:fs'; import path from 'node:path'; import { scopeSnapshotNodes } from './snapshot-desktop-projection.ts'; -// Golden scope-policy leg (#1832 C2): the post-wire pass must agree with -// contracts/fixtures/snapshot-scope-policy.json — the same table the Android projection and the -// shared predicate are asserted against. Fixture position rides in `rect.x` so the check is -// text-independent. test('scopeSnapshotNodes agrees with every golden scope-policy table case', () => { const cases = JSON.parse( fs.readFileSync( diff --git a/packages/contracts/src/snapshot-desktop-projection.ts b/packages/capture-kit/src/snapshot-desktop-projection.ts similarity index 92% rename from packages/contracts/src/snapshot-desktop-projection.ts rename to packages/capture-kit/src/snapshot-desktop-projection.ts index 83b7a62cce..686ab0ab17 100644 --- a/packages/contracts/src/snapshot-desktop-projection.ts +++ b/packages/capture-kit/src/snapshot-desktop-projection.ts @@ -1,10 +1,10 @@ import type { RawSnapshotNode } from '@agent-device/kernel/snapshot'; -import type { SnapshotOptions, SnapshotResult } from './interactor-types.ts'; +import type { SnapshotOptions, SnapshotResult } from '@agent-device/contracts/interactor-types'; import { findSnapshotScopeRange, normalizeSnapshotScope, reindexSnapshotNodes, -} from './snapshot-scope.ts'; +} from '@agent-device/contracts/snapshot-scope'; const INTERACTIVE_ROLE_TOKENS = [ 'button', @@ -34,7 +34,7 @@ export function shapeDesktopSurfaceSnapshot( return { ...data, nodes }; } -/** The shared scope specification applied post-wire (`./snapshot-scope.ts`). */ +/** The shared scope specification applied post-wire (contracts' `snapshot-scope`). */ export function scopeSnapshotNodes( nodes: RawSnapshotNode[], scope: string, diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 32dd04c540..8513fa9e65 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -419,10 +419,6 @@ "types": "./src/facades/snapshot.ts", "default": "./src/facades/snapshot.ts" }, - "./snapshot-desktop-projection": { - "types": "./src/snapshot-desktop-projection.ts", - "default": "./src/snapshot-desktop-projection.ts" - }, "./snapshot-presentation": { "types": "./src/snapshot-presentation.ts", "default": "./src/snapshot-presentation.ts" @@ -431,6 +427,10 @@ "types": "./src/snapshot-runtime.ts", "default": "./src/snapshot-runtime.ts" }, + "./snapshot-scope": { + "types": "./src/snapshot-scope.ts", + "default": "./src/snapshot-scope.ts" + }, "./snapshot-timeout-evidence": { "types": "./src/snapshot-timeout-evidence.ts", "default": "./src/snapshot-timeout-evidence.ts" diff --git a/packages/contracts/src/android-observation.ts b/packages/contracts/src/android-observation.ts index 6000ae7647..82080d1ce9 100644 --- a/packages/contracts/src/android-observation.ts +++ b/packages/contracts/src/android-observation.ts @@ -48,29 +48,3 @@ export type AndroidObservationAdapter = Readonly<{ readScreenSize(device: DeviceInfo): Promise>; isPermissionPackage(packageName: string): Promise; }>; - -const ANDROID_FOCUS_MARKERS = [ - 'mCurrentFocus=Window{', - 'mFocusedApp=AppWindowToken{', - 'mResumedActivity:', - 'ResumedActivity:', -] as const; - -/** Extracts the foreground package/activity from `dumpsys window`/`activity` output. */ -export function parseAndroidForegroundApp(text: string): AppStateRuntimeResult | null { - const lines = text.split('\n'); - for (const marker of ANDROID_FOCUS_MARKERS) { - for (const line of lines) { - const markerIndex = line.indexOf(marker); - if (markerIndex === -1) continue; - const parsed = parseAndroidComponentFromSegment(line.slice(markerIndex + marker.length)); - if (parsed) return parsed; - } - } - return null; -} - -function parseAndroidComponentFromSegment(segment: string): AppStateRuntimeResult | null { - const match = segment.match(/\b([A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z0-9_]+)+)\/([A-Za-z0-9_.$]+)/); - return match?.[1] && match[2] ? { package: match[1], activity: match[2] } : null; -} diff --git a/packages/contracts/src/android-observation.test.ts b/packages/platform-android/src/app-state.test.ts similarity index 94% rename from packages/contracts/src/android-observation.test.ts rename to packages/platform-android/src/app-state.test.ts index 2d443c0ae0..8601abe6f9 100644 --- a/packages/contracts/src/android-observation.test.ts +++ b/packages/platform-android/src/app-state.test.ts @@ -1,5 +1,5 @@ import { expect, test } from 'vitest'; -import { parseAndroidForegroundApp } from './android-observation.ts'; +import { parseAndroidForegroundApp } from './app-state.ts'; test('parses Android window and activity foreground markers', () => { expect( diff --git a/packages/platform-android/src/app-state.ts b/packages/platform-android/src/app-state.ts index 8d5722c5e0..ae01ce33da 100644 --- a/packages/platform-android/src/app-state.ts +++ b/packages/platform-android/src/app-state.ts @@ -4,7 +4,6 @@ import type { AppStateRuntimeResult, } from '@agent-device/contracts/app-state-runtime'; import type { DeviceInfo } from '@agent-device/kernel/device'; -import { parseAndroidForegroundApp } from '@agent-device/contracts/android-observation'; export type AndroidAppStateHost = Readonly<{ run( @@ -22,6 +21,12 @@ const ACTIVITY_COMMANDS = [ ['shell', 'dumpsys', 'activity', 'activities'], ['shell', 'dumpsys', 'activity'], ] as const; +const ANDROID_FOCUS_MARKERS = [ + 'mCurrentFocus=Window{', + 'mFocusedApp=AppWindowToken{', + 'mResumedActivity:', + 'ResumedActivity:', +] as const; export async function readAndroidAppState( host: AndroidAppStateHost, @@ -36,6 +41,19 @@ export async function readAndroidAppState( return {}; } +export function parseAndroidForegroundApp(text: string): AppStateRuntimeResult | null { + const lines = text.split('\n'); + for (const marker of ANDROID_FOCUS_MARKERS) { + for (const line of lines) { + const markerIndex = line.indexOf(marker); + if (markerIndex === -1) continue; + const parsed = parseAndroidComponentFromSegment(line.slice(markerIndex + marker.length)); + if (parsed) return parsed; + } + } + return null; +} + async function readAndroidFocus( host: AndroidAppStateHost, device: DeviceInfo, @@ -51,3 +69,8 @@ async function readAndroidFocus( } return null; } + +function parseAndroidComponentFromSegment(segment: string): AppStateRuntimeResult | null { + const match = segment.match(/\b([A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z0-9_]+)+)\/([A-Za-z0-9_.$]+)/); + return match?.[1] && match[2] ? { package: match[1], activity: match[2] } : null; +} diff --git a/packages/platform-android/src/index.ts b/packages/platform-android/src/index.ts index f3e6792ef8..8dbee8bd05 100644 --- a/packages/platform-android/src/index.ts +++ b/packages/platform-android/src/index.ts @@ -55,6 +55,13 @@ export async function readAndroidAppState( return await read(host, device, signal); } +export async function parseAndroidForegroundApp( + text: string, +): Promise | null> { + const { parseAndroidForegroundApp: parse } = await import('./app-state.ts'); + return parse(text); +} + export const runtimeModule = Object.freeze({ ...metadata, loadRuntime: async (host) => { diff --git a/scripts/layering/package-boundaries.test.ts b/scripts/layering/package-boundaries.test.ts index db1b1d8fcb..9d7a153fc1 100644 --- a/scripts/layering/package-boundaries.test.ts +++ b/scripts/layering/package-boundaries.test.ts @@ -149,9 +149,9 @@ const CONTRACT_EXPORTS = [ '@agent-device/contracts/settings', '@agent-device/contracts/settings-runtime', '@agent-device/contracts/snapshot', - '@agent-device/contracts/snapshot-desktop-projection', '@agent-device/contracts/snapshot-presentation', '@agent-device/contracts/snapshot-runtime', + '@agent-device/contracts/snapshot-scope', '@agent-device/contracts/snapshot-timeout-evidence', '@agent-device/contracts/startup-recovery-fence', '@agent-device/contracts/touch-runtime', @@ -465,6 +465,7 @@ test('the real tree parses, declares, and passes R11', () => { '@agent-device/capture-kit/png-worker-client', '@agent-device/capture-kit/screenshot-density', '@agent-device/capture-kit/screenshot-diff-pixels', + '@agent-device/capture-kit/snapshot-desktop-projection', '@agent-device/capture-kit/snapshot-occlusion', '@agent-device/capture-kit/snapshot-quality-backend-capabilities', '@agent-device/capture-kit/snapshot-quality-verdict', diff --git a/src/__tests__/eager-closure-budgets.ts b/src/__tests__/eager-closure-budgets.ts index 07379ea0ce..e4fb350361 100644 --- a/src/__tests__/eager-closure-budgets.ts +++ b/src/__tests__/eager-closure-budgets.ts @@ -130,6 +130,7 @@ export const FACADE_BUDGETS: Readonly> = Object.freeze({ 'packages/capture-kit/src/screenshot-density.ts': 6, 'packages/capture-kit/src/screenshot-diff-pixels.ts': 1, 'packages/capture-kit/src/mobile-snapshot-semantics.ts': 10, + 'packages/capture-kit/src/snapshot-desktop-projection.ts': 2, 'packages/capture-kit/src/snapshot-occlusion.ts': 10, 'packages/capture-kit/src/snapshot-quality-backend-capabilities.ts': 1, 'packages/capture-kit/src/snapshot-quality-verdict.ts': 2, @@ -238,9 +239,9 @@ export const FACADE_BUDGETS: Readonly> = Object.freeze({ 'packages/contracts/src/scroll-runtime.ts': 4, 'packages/contracts/src/selector-observation-runtime.ts': 1, 'packages/contracts/src/settings.ts': 3, - 'packages/contracts/src/snapshot-desktop-projection.ts': 2, 'packages/contracts/src/snapshot-presentation.ts': 2, 'packages/contracts/src/snapshot-runtime.ts': 3, + 'packages/contracts/src/snapshot-scope.ts': 1, 'packages/contracts/src/snapshot-timeout-evidence.ts': 1, 'packages/contracts/src/startup-recovery-fence.ts': 1, 'packages/contracts/src/tv-remote.ts': 3, diff --git a/src/core/snapshot-state.ts b/src/core/snapshot-state.ts index 7756311b80..5e9a0b83de 100644 --- a/src/core/snapshot-state.ts +++ b/src/core/snapshot-state.ts @@ -19,7 +19,7 @@ import { annotateSnapshotNodesCoveredByPolicy, } from '@agent-device/capture-kit/snapshot-occlusion'; import { coveredAndroidReplacementNodeIndexes } from '../snapshot/android-replacement-surface-occlusion.ts'; -import { scopeSnapshotNodes } from '@agent-device/contracts/snapshot-desktop-projection'; +import { scopeSnapshotNodes } from '@agent-device/capture-kit/snapshot-desktop-projection'; import { normalizeSnapshotTree, pruneGroupNodes } from '../core/snapshot-tree-ingestion.ts'; import { presentIosInteractiveSnapshot } from '../snapshot/snapshot-presentation/ios/index.ts'; diff --git a/src/platform-runtime.ts b/src/platform-runtime.ts index 3e75f6952b..fdfe090c3d 100644 --- a/src/platform-runtime.ts +++ b/src/platform-runtime.ts @@ -31,6 +31,7 @@ import { import { createAndroidObservationAdapter as createPackageAndroidObservationAdapter, createAndroidInventoryModule, + parseAndroidForegroundApp as parseAndroidPackageForegroundApp, readAndroidAppState as readAndroidPackageAppState, loadShutdownRuntime as loadAndroidShutdownRuntime, runtimeModule as androidRuntimeModule, @@ -73,6 +74,12 @@ export async function readAndroidAppStateWithHost( return await readAndroidPackageAppState(host, device, signal); } +export async function parseAndroidForegroundApp( + text: string, +): Promise | null> { + return await parseAndroidPackageForegroundApp(text); +} + const androidInventoryModule = createAndroidInventoryModule({ sdkRoots: configuredValues(process.env.ANDROID_SDK_ROOT, process.env.ANDROID_HOME), }); diff --git a/src/platforms/android/__tests__/app-helpers.test.ts b/src/platforms/android/__tests__/app-helpers.test.ts index 7aa09bda29..05c034fd9c 100644 --- a/src/platforms/android/__tests__/app-helpers.test.ts +++ b/src/platforms/android/__tests__/app-helpers.test.ts @@ -4,7 +4,8 @@ import path from 'node:path'; import { test } from 'vitest'; import type { AndroidAdbExecutor } from '../adb-executor.ts'; import { createDeviceAdbExecutor } from '../adb-executor.ts'; -import { getAndroidAppStateWithAdb, listAndroidAppsWithAdb } from '../app-helpers.ts'; +import { listAndroidAppsWithAdb } from '../app-helpers.ts'; +import { getAndroidAppStateWithAdb } from '../../../sdk/android-adb.ts'; import { mkdtempForTest } from '../../../__tests__/test-utils/tmp-dir.ts'; async function withMockedAdbScript(script: string, run: () => Promise): Promise { diff --git a/src/platforms/android/app-helpers.ts b/src/platforms/android/app-helpers.ts index 1b9f85346b..74c25abcc5 100644 --- a/src/platforms/android/app-helpers.ts +++ b/src/platforms/android/app-helpers.ts @@ -1,4 +1,3 @@ -import { parseAndroidForegroundApp } from '@agent-device/contracts/android-observation'; import { resolveAppsFilter, type AppsFilter } from '@agent-device/contracts/device'; import type { AppStateRuntimeResult } from '@agent-device/contracts/app-state-runtime'; import { androidAdbResultError, type AndroidAdbExecutor } from './adb-executor.ts'; @@ -34,21 +33,32 @@ export async function listAndroidAppsWithAdb( .sort((a, b) => a.package.localeCompare(b.package)); } -export async function getAndroidAppStateWithAdb( - adb: AndroidAdbExecutor, -): Promise { - const windowFocus = await readAndroidFocusWithAdb(adb, [ - ['shell', 'dumpsys', 'window', 'windows'], - ['shell', 'dumpsys', 'window'], - ]); - if (windowFocus) return windowFocus; +export type AndroidForegroundAppParser = ( + text: string, +) => Promise | AppStateRuntimeResult | null; + +/** + * The dumpsys foreground parser belongs to @agent-device/platform-android; + * the composition seam (src/sdk/android-adb.ts) injects it here so this + * legacy family module never imports upward or across R13. + */ +export function createAndroidAppStateReader( + parseForegroundApp: AndroidForegroundAppParser, +): (adb: AndroidAdbExecutor) => Promise { + return async (adb) => { + const windowFocus = await readAndroidFocusWithAdb(adb, parseForegroundApp, [ + ['shell', 'dumpsys', 'window', 'windows'], + ['shell', 'dumpsys', 'window'], + ]); + if (windowFocus) return windowFocus; - const activityFocus = await readAndroidFocusWithAdb(adb, [ - ['shell', 'dumpsys', 'activity', 'activities'], - ['shell', 'dumpsys', 'activity'], - ]); - if (activityFocus) return activityFocus; - return {}; + const activityFocus = await readAndroidFocusWithAdb(adb, parseForegroundApp, [ + ['shell', 'dumpsys', 'activity', 'activities'], + ['shell', 'dumpsys', 'activity'], + ]); + if (activityFocus) return activityFocus; + return {}; + }; } async function listAndroidLaunchablePackagesWithAdb( @@ -107,11 +117,12 @@ async function listAndroidUserInstalledPackagesWithAdb(adb: AndroidAdbExecutor): async function readAndroidFocusWithAdb( adb: AndroidAdbExecutor, + parseForegroundApp: AndroidForegroundAppParser, commands: string[][], ): Promise { for (const args of commands) { const result = await adb(args, { allowFailure: true }); - const parsed = parseAndroidForegroundApp(result.stdout ?? ''); + const parsed = await parseForegroundApp(result.stdout ?? ''); if (parsed) return parsed; } return null; diff --git a/src/platforms/apple/os/macos/surface-snapshot.ts b/src/platforms/apple/os/macos/surface-snapshot.ts index a65d99bd47..47d0899eb7 100644 --- a/src/platforms/apple/os/macos/surface-snapshot.ts +++ b/src/platforms/apple/os/macos/surface-snapshot.ts @@ -1,5 +1,5 @@ import type { CaptureSnapshotInput } from '@agent-device/contracts/snapshot-runtime'; -import { shapeDesktopSurfaceSnapshot } from '@agent-device/contracts/snapshot-desktop-projection'; +import { shapeDesktopSurfaceSnapshot } from '@agent-device/capture-kit/snapshot-desktop-projection'; type SnapshotSurfaceOptions = NonNullable; diff --git a/src/platforms/linux/surface-snapshot.ts b/src/platforms/linux/surface-snapshot.ts index 6ca0f352fb..41d9d1d199 100644 --- a/src/platforms/linux/surface-snapshot.ts +++ b/src/platforms/linux/surface-snapshot.ts @@ -1,5 +1,5 @@ import type { CaptureSnapshotInput } from '@agent-device/contracts/snapshot-runtime'; -import { shapeDesktopSurfaceSnapshot } from '@agent-device/contracts/snapshot-desktop-projection'; +import { shapeDesktopSurfaceSnapshot } from '@agent-device/capture-kit/snapshot-desktop-projection'; export async function captureLinuxSurfaceSnapshot( options: CaptureSnapshotInput['options'], diff --git a/src/sdk/android-adb.ts b/src/sdk/android-adb.ts index 74490b4cad..aedad622e0 100644 --- a/src/sdk/android-adb.ts +++ b/src/sdk/android-adb.ts @@ -5,10 +5,23 @@ export { type AndroidAdbProvider, type AndroidPortReverseEndpoint, } from '../platforms/android/adb-executor.ts'; -export { - getAndroidAppStateWithAdb, - listAndroidAppsWithAdb, -} from '../platforms/android/app-helpers.ts'; +export { listAndroidAppsWithAdb } from '../platforms/android/app-helpers.ts'; + +import { createAndroidAppStateReader } from '../platforms/android/app-helpers.ts'; +import type { AndroidAdbExecutor } from '../platforms/android/adb-executor.ts'; +import type { AppStateRuntimeResult } from '@agent-device/contracts/app-state-runtime'; + +/** + * Composition seam: the dumpsys foreground parser is platform-android's, so + * only the root composition may load it (R13); the published signature stays + * `(adb) => AppStateRuntimeResult`. + */ +export async function getAndroidAppStateWithAdb( + adb: AndroidAdbExecutor, +): Promise { + const { parseAndroidForegroundApp } = await import('../platform-runtime.ts'); + return await createAndroidAppStateReader(parseAndroidForegroundApp)(adb); +} export { forceStopAndroidAppWithAdb, openAndroidAppWithAdb, diff --git a/src/sdk/limrun-runtime-dependencies.test.ts b/src/sdk/limrun-runtime-dependencies.test.ts index 4ca6e58ebe..d6d8d64a9e 100644 --- a/src/sdk/limrun-runtime-dependencies.test.ts +++ b/src/sdk/limrun-runtime-dependencies.test.ts @@ -15,10 +15,6 @@ vi.mock('@limrun/api', () => ({ vi.mock('../platforms/android/app-helpers.ts', () => { moduleLoads.androidAppHelpers += 1; return { - getAndroidAppStateWithAdb: vi.fn(async () => ({ - package: 'com.example.app', - activity: '.MainActivity', - })), listAndroidAppsWithAdb: vi.fn(async () => [{ package: 'com.example.app', name: 'Example' }]), }; }); From c8918bd6a27a7c48909a9940f5cbf1531805dde3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 16:10:52 +0000 Subject: [PATCH 3/5] refactor(platforms): the adb app-state read lives whole behind the platform-android seam MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The injected-parser loop retires: platform-android's app-state module owns the complete adb-executor read/parse (readAndroidAppStateWithExecutor, beside its host-based twin), the façade exposes it lazily, the composition root wraps it, and src/sdk/android-adb.ts reaches it through that root in one hop. app-helpers keeps only the app-list helpers, and the SDK-route tests live in SDK topology as src/sdk/android-adb.test.ts. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH --- packages/platform-android/src/app-state.ts | 29 +++++++++++++ packages/platform-android/src/index.ts | 10 ++--- src/platform-runtime.ts | 10 ++--- .../android/__tests__/app-helpers.test.ts | 24 +---------- src/platforms/android/app-helpers.ts | 42 ------------------- src/sdk/android-adb.test.ts | 39 +++++++++++++++++ src/sdk/android-adb.ts | 12 ++---- 7 files changed, 83 insertions(+), 83 deletions(-) create mode 100644 src/sdk/android-adb.test.ts diff --git a/packages/platform-android/src/app-state.ts b/packages/platform-android/src/app-state.ts index ae01ce33da..4eee0ed50f 100644 --- a/packages/platform-android/src/app-state.ts +++ b/packages/platform-android/src/app-state.ts @@ -28,6 +28,35 @@ const ANDROID_FOCUS_MARKERS = [ 'ResumedActivity:', ] as const; +export type AndroidCommandExecutor = ( + args: string[], + options: { allowFailure: boolean }, +) => Promise<{ exitCode: number; stdout?: string; stderr?: string }>; + +/** The complete adb-executor read/parse loop, owned beside its host-based twin. */ +export async function readAndroidAppStateWithExecutor( + run: AndroidCommandExecutor, +): Promise { + const windowFocus = await readAndroidFocusWithExecutor(run, FOCUS_COMMANDS); + if (windowFocus) return windowFocus; + + const activityFocus = await readAndroidFocusWithExecutor(run, ACTIVITY_COMMANDS); + if (activityFocus) return activityFocus; + return {}; +} + +async function readAndroidFocusWithExecutor( + run: AndroidCommandExecutor, + commands: readonly (readonly string[])[], +): Promise { + for (const args of commands) { + const result = await run([...args], { allowFailure: true }); + const parsed = parseAndroidForegroundApp(result.stdout ?? ''); + if (parsed) return parsed; + } + return null; +} + export async function readAndroidAppState( host: AndroidAppStateHost, device: DeviceInfo, diff --git a/packages/platform-android/src/index.ts b/packages/platform-android/src/index.ts index 8dbee8bd05..1ca8a52e19 100644 --- a/packages/platform-android/src/index.ts +++ b/packages/platform-android/src/index.ts @@ -55,11 +55,11 @@ export async function readAndroidAppState( return await read(host, device, signal); } -export async function parseAndroidForegroundApp( - text: string, -): Promise | null> { - const { parseAndroidForegroundApp: parse } = await import('./app-state.ts'); - return parse(text); +export async function readAndroidAppStateWithExecutor( + run: import('./app-state.ts').AndroidCommandExecutor, +): Promise { + const { readAndroidAppStateWithExecutor: read } = await import('./app-state.ts'); + return await read(run); } export const runtimeModule = Object.freeze({ diff --git a/src/platform-runtime.ts b/src/platform-runtime.ts index fdfe090c3d..ae0ea7d736 100644 --- a/src/platform-runtime.ts +++ b/src/platform-runtime.ts @@ -31,7 +31,7 @@ import { import { createAndroidObservationAdapter as createPackageAndroidObservationAdapter, createAndroidInventoryModule, - parseAndroidForegroundApp as parseAndroidPackageForegroundApp, + readAndroidAppStateWithExecutor, readAndroidAppState as readAndroidPackageAppState, loadShutdownRuntime as loadAndroidShutdownRuntime, runtimeModule as androidRuntimeModule, @@ -74,10 +74,10 @@ export async function readAndroidAppStateWithHost( return await readAndroidPackageAppState(host, device, signal); } -export async function parseAndroidForegroundApp( - text: string, -): Promise | null> { - return await parseAndroidPackageForegroundApp(text); +export async function getAndroidAppStateWithAdb( + adb: Parameters[0], +): Promise { + return await readAndroidAppStateWithExecutor(adb); } const androidInventoryModule = createAndroidInventoryModule({ diff --git a/src/platforms/android/__tests__/app-helpers.test.ts b/src/platforms/android/__tests__/app-helpers.test.ts index 05c034fd9c..54480210ab 100644 --- a/src/platforms/android/__tests__/app-helpers.test.ts +++ b/src/platforms/android/__tests__/app-helpers.test.ts @@ -5,7 +5,6 @@ import { test } from 'vitest'; import type { AndroidAdbExecutor } from '../adb-executor.ts'; import { createDeviceAdbExecutor } from '../adb-executor.ts'; import { listAndroidAppsWithAdb } from '../app-helpers.ts'; -import { getAndroidAppStateWithAdb } from '../../../sdk/android-adb.ts'; import { mkdtempForTest } from '../../../__tests__/test-utils/tmp-dir.ts'; async function withMockedAdbScript(script: string, run: () => Promise): Promise { @@ -93,30 +92,9 @@ test('Android app helpers work with a local ADB provider', async () => { booted: true, }); - const [apps, state] = await Promise.all([ - listAndroidAppsWithAdb(adb, { target: 'mobile' }), - getAndroidAppStateWithAdb(adb), - ]); + const apps = await listAndroidAppsWithAdb(adb, { target: 'mobile' }); assert.deepEqual(apps, [{ package: 'com.example.app', name: 'Example' }]); - assert.deepEqual(state, { package: 'com.example.app', activity: '.MainActivity' }); }, ); }); - -test('getAndroidAppStateWithAdb parses focus output from failed commands', async () => { - const calls: string[][] = []; - const adb: AndroidAdbExecutor = async (args) => { - calls.push(args); - return { - exitCode: 1, - stdout: 'mCurrentFocus=Window{42 u0 com.example.app/.MainActivity}\n', - stderr: 'dumpsys warning', - }; - }; - - const state = await getAndroidAppStateWithAdb(adb); - - assert.deepEqual(state, { package: 'com.example.app', activity: '.MainActivity' }); - assert.deepEqual(calls, [['shell', 'dumpsys', 'window', 'windows']]); -}); diff --git a/src/platforms/android/app-helpers.ts b/src/platforms/android/app-helpers.ts index 74c25abcc5..845bb2056c 100644 --- a/src/platforms/android/app-helpers.ts +++ b/src/platforms/android/app-helpers.ts @@ -1,5 +1,4 @@ import { resolveAppsFilter, type AppsFilter } from '@agent-device/contracts/device'; -import type { AppStateRuntimeResult } from '@agent-device/contracts/app-state-runtime'; import { androidAdbResultError, type AndroidAdbExecutor } from './adb-executor.ts'; import { parseAndroidLaunchablePackages, @@ -33,34 +32,6 @@ export async function listAndroidAppsWithAdb( .sort((a, b) => a.package.localeCompare(b.package)); } -export type AndroidForegroundAppParser = ( - text: string, -) => Promise | AppStateRuntimeResult | null; - -/** - * The dumpsys foreground parser belongs to @agent-device/platform-android; - * the composition seam (src/sdk/android-adb.ts) injects it here so this - * legacy family module never imports upward or across R13. - */ -export function createAndroidAppStateReader( - parseForegroundApp: AndroidForegroundAppParser, -): (adb: AndroidAdbExecutor) => Promise { - return async (adb) => { - const windowFocus = await readAndroidFocusWithAdb(adb, parseForegroundApp, [ - ['shell', 'dumpsys', 'window', 'windows'], - ['shell', 'dumpsys', 'window'], - ]); - if (windowFocus) return windowFocus; - - const activityFocus = await readAndroidFocusWithAdb(adb, parseForegroundApp, [ - ['shell', 'dumpsys', 'activity', 'activities'], - ['shell', 'dumpsys', 'activity'], - ]); - if (activityFocus) return activityFocus; - return {}; - }; -} - async function listAndroidLaunchablePackagesWithAdb( adb: AndroidAdbExecutor, target: AndroidAppListTarget, @@ -114,16 +85,3 @@ async function listAndroidUserInstalledPackagesWithAdb(adb: AndroidAdbExecutor): } return parseAndroidUserInstalledPackages(result.stdout); } - -async function readAndroidFocusWithAdb( - adb: AndroidAdbExecutor, - parseForegroundApp: AndroidForegroundAppParser, - commands: string[][], -): Promise { - for (const args of commands) { - const result = await adb(args, { allowFailure: true }); - const parsed = await parseForegroundApp(result.stdout ?? ''); - if (parsed) return parsed; - } - return null; -} diff --git a/src/sdk/android-adb.test.ts b/src/sdk/android-adb.test.ts new file mode 100644 index 0000000000..503fb155e3 --- /dev/null +++ b/src/sdk/android-adb.test.ts @@ -0,0 +1,39 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; +import type { AndroidAdbExecutor } from '../platforms/android/adb-executor.ts'; +import { getAndroidAppStateWithAdb } from './android-adb.ts'; + +test('getAndroidAppStateWithAdb parses focus output from failed commands', async () => { + const calls: string[][] = []; + const adb: AndroidAdbExecutor = async (args) => { + calls.push(args); + return { + exitCode: 1, + stdout: 'mCurrentFocus=Window{42 u0 com.example.app/.MainActivity}\n', + stderr: 'dumpsys warning', + }; + }; + + const state = await getAndroidAppStateWithAdb(adb); + + assert.deepEqual(state, { package: 'com.example.app', activity: '.MainActivity' }); + assert.deepEqual(calls, [['shell', 'dumpsys', 'window', 'windows']]); +}); + +test('getAndroidAppStateWithAdb falls back to activity focus and settles empty', async () => { + const calls: string[][] = []; + const adb: AndroidAdbExecutor = async (args) => { + calls.push(args); + return { exitCode: 0, stdout: '', stderr: '' }; + }; + + const state = await getAndroidAppStateWithAdb(adb); + + assert.deepEqual(state, {}); + assert.deepEqual(calls, [ + ['shell', 'dumpsys', 'window', 'windows'], + ['shell', 'dumpsys', 'window'], + ['shell', 'dumpsys', 'activity', 'activities'], + ['shell', 'dumpsys', 'activity'], + ]); +}); diff --git a/src/sdk/android-adb.ts b/src/sdk/android-adb.ts index aedad622e0..aaf5e65124 100644 --- a/src/sdk/android-adb.ts +++ b/src/sdk/android-adb.ts @@ -7,21 +7,17 @@ export { } from '../platforms/android/adb-executor.ts'; export { listAndroidAppsWithAdb } from '../platforms/android/app-helpers.ts'; -import { createAndroidAppStateReader } from '../platforms/android/app-helpers.ts'; import type { AndroidAdbExecutor } from '../platforms/android/adb-executor.ts'; import type { AppStateRuntimeResult } from '@agent-device/contracts/app-state-runtime'; -/** - * Composition seam: the dumpsys foreground parser is platform-android's, so - * only the root composition may load it (R13); the published signature stays - * `(adb) => AppStateRuntimeResult`. - */ +// R13: the adb read/parse behavior is platform-android's, reached through the composition root. export async function getAndroidAppStateWithAdb( adb: AndroidAdbExecutor, ): Promise { - const { parseAndroidForegroundApp } = await import('../platform-runtime.ts'); - return await createAndroidAppStateReader(parseAndroidForegroundApp)(adb); + const { getAndroidAppStateWithAdb: read } = await import('../platform-runtime.ts'); + return await read(adb); } + export { forceStopAndroidAppWithAdb, openAndroidAppWithAdb, From e1c05b3c03e7562a9a9364999a22d8694bb878d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 18:03:06 +0000 Subject: [PATCH 4/5] docs: drop the seam wrapper explainer comments Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH --- packages/platform-android/src/app-state.ts | 1 - src/sdk/android-adb.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/platform-android/src/app-state.ts b/packages/platform-android/src/app-state.ts index 4eee0ed50f..8fb7de4781 100644 --- a/packages/platform-android/src/app-state.ts +++ b/packages/platform-android/src/app-state.ts @@ -33,7 +33,6 @@ export type AndroidCommandExecutor = ( options: { allowFailure: boolean }, ) => Promise<{ exitCode: number; stdout?: string; stderr?: string }>; -/** The complete adb-executor read/parse loop, owned beside its host-based twin. */ export async function readAndroidAppStateWithExecutor( run: AndroidCommandExecutor, ): Promise { diff --git a/src/sdk/android-adb.ts b/src/sdk/android-adb.ts index aaf5e65124..12a0f87c3f 100644 --- a/src/sdk/android-adb.ts +++ b/src/sdk/android-adb.ts @@ -10,7 +10,6 @@ export { listAndroidAppsWithAdb } from '../platforms/android/app-helpers.ts'; import type { AndroidAdbExecutor } from '../platforms/android/adb-executor.ts'; import type { AppStateRuntimeResult } from '@agent-device/contracts/app-state-runtime'; -// R13: the adb read/parse behavior is platform-android's, reached through the composition root. export async function getAndroidAppStateWithAdb( adb: AndroidAdbExecutor, ): Promise { From 55d3cfe1514dbd71a218bcdc6fed5f6f0e49bdbc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 19:17:29 +0000 Subject: [PATCH 5/5] docs: drop the projection docblocks the test names already carry Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH --- packages/capture-kit/src/snapshot-desktop-projection.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/capture-kit/src/snapshot-desktop-projection.ts b/packages/capture-kit/src/snapshot-desktop-projection.ts index 686ab0ab17..0d5b0139ed 100644 --- a/packages/capture-kit/src/snapshot-desktop-projection.ts +++ b/packages/capture-kit/src/snapshot-desktop-projection.ts @@ -16,7 +16,6 @@ const INTERACTIVE_ROLE_TOKENS = [ 'switch', ] as const; -/** Applies the legacy desktop-surface projection once for both runtime hosts and legacy capture. */ export function shapeDesktopSurfaceSnapshot( data: SnapshotResult, options: Pick, @@ -34,7 +33,6 @@ export function shapeDesktopSurfaceSnapshot( return { ...data, nodes }; } -/** The shared scope specification applied post-wire (contracts' `snapshot-scope`). */ export function scopeSnapshotNodes( nodes: RawSnapshotNode[], scope: string,