Skip to content
  •  
  •  
  •  
12 changes: 12 additions & 0 deletions .fallowrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@
"file": "src/platforms/android/perf.ts",
"exports": ["sampleAndroidMemoryPerf"]
},
{
"comment": "Apple perf sampling metadata is selected through the same lazy platform host; Fallow cannot follow the dynamic property read in src/platform-runtime-perf-host.ts.",
"file": "src/platforms/apple/core/perf.ts",
"exports": ["buildAppleFrameSamplingMetadata", "buildAppleMemorySamplingMetadata"]
},
{
"comment": "Daemon route handlers are reached only through the dynamic `import()` table in request-handler-chain.ts, which --production analysis cannot follow to a consumer.",
"file": "src/daemon/handlers/{lease,session,snapshot,react-native,record-trace,find,interaction}.ts",
Expand Down Expand Up @@ -212,6 +217,13 @@
"rules": {
"unused-types": "off"
}
},
{
"comment": "Host-kit seam façades: same false-positive shape as the contracts façades above — each seam names every type its re-exported functions put in a public signature, and callers use those types structurally without importing the name. unused-exports (the VALUE check) stays on.",
"files": ["packages/host-kit/src/*.ts"],
"rules": {
"unused-types": "off"
}
}
],
"production": {
Expand Down
11 changes: 8 additions & 3 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,23 +37,28 @@
},
"overrides": [
{
"files": ["src/**/*.ts"],
"files": ["src/**/*.ts", "packages/capture-kit/src/**/*.ts"],
"rules": {
"eslint/no-restricted-imports": [
"error",
{
"paths": [
{
"name": "node:child_process",
"message": "Use process helpers from src/utils/exec.ts instead of importing node:child_process directly."
"message": "Use process helpers from @agent-device/capture-kit/exec instead of importing node:child_process directly."
}
]
}
]
}
},
{
"files": ["src/utils/exec.ts", "src/**/*.test.ts", "src/**/__tests__/**/*.ts"],
"files": [
"packages/capture-kit/src/exec.ts",
"packages/capture-kit/src/*.test.ts",
"src/**/*.test.ts",
"src/**/__tests__/**/*.ts"
],
"rules": {
"eslint/no-restricted-imports": ["error", { "paths": [] }]
}
Expand Down
9 changes: 5 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,10 @@ Read the declaration rather than maintaining a prose copy:
- common command input fields, and which surface may write an input key (model, operator, retired):
`src/commands/common-input-fields.ts` and `src/commands/input-audience.ts`

Shared selector parsing and matching belongs in `@agent-device/selectors`; request cancellation and
progress in `src/request`; cross-layer contracts in `src/contracts`; CLI flags in
`src/commands/cli-grammar`; cross-surface schema composition in `src/cli-schema`.
Shared selector parsing and matching belongs in `@agent-device/selectors`; request cancellation
and progress in `@agent-device/capture-kit` (`request-cancel`, `request-progress`); cross-layer
contracts in `src/contracts`; CLI flags in `src/commands/cli-grammar`; cross-surface schema
composition in `src/cli-schema`.

The enforced registries are self-declaring. A failing completeness, parity, coverage, timeout,
layering, or construction gate means the new cell or path is unclassified; do not suppress or
Expand Down Expand Up @@ -123,7 +124,7 @@ The OS-neutral Apple runner lives under `packages/platform-apple/src/runner/`. F
retry policy, or command typing, start at `runner-contract.ts`; transport stays below session/client
behavior, and xctestrun build/cache logic stays outside request execution.

Diagnostics use `src/utils/diagnostics.ts`. Request diagnostics belong in the session request log;
Diagnostics use `@agent-device/capture-kit/diagnostics`. Request diagnostics belong in the session request log;
session artifact paths come from `src/daemon/session-store.ts`. App/device logs remain in `app.log`;
Apple runner and xcodebuild output remains in `runner.log`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import XCTest
// This is pure geometry on purpose — no XCUIElement — so the exact decision
// can be proven against the golden fixture table shared with the TS twin:
// table: contracts/fixtures/tap-point-policy.json
// TS twin: src/snapshot/mobile-snapshot-semantics.ts#isTapPointInsideViewport
// TS test: src/snapshot/__tests__/tap-point-policy-parity.test.ts
// TS twin: packages/contracts/src/snapshot-visibility.ts#isTapPointInsideViewport
// TS test: packages/contracts/src/snapshot-tap-point-policy.test.ts
// Drift on either side turns CI red without needing a simulator.
enum TapPointPolicy {
static func isAllowed(elementFrame: CGRect, windowFrame: CGRect) -> Bool {
Expand Down
35 changes: 35 additions & 0 deletions docs/adr/0019-request-bound-platform-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,41 @@ platform-common package, and it preserves the package façades' implementation-l
Its introduction carries the normal workspace-package compliance surface: `check:affected`
selection, R11/R13 package enumeration, and the composite typecheck project list.

> **Amendment (#2082): the substrate below the platform families.** Retiring the shared
> `src/utils` and `src/platforms` root surfaces (so the family trees can move behind their
> exports maps) forces every shared file onto a declared domain owner, and the paragraph above
> is amended to name that layout rather than let capture-kit absorb it:
>
> - `@agent-device/host-kit` owns mechanics that act on the host machine, and nothing else. Each
> export is one narrow capability port, not a category barrel: `command` (running host
> commands), `process` (observing and owning host processes), `diagnostics`, `retry`
> (deadline/backoff/sleep), `archive` (bounded extraction and byte limits), `file` (atomic
> publishes, locks, path resolution), `request` (request-scoped cancellation and progress),
> and `version` (the installed version off disk). Modules under `src/internal/` are reachable
> only through a port, and a port may only hold mechanics a consumer of that capability
> needs — the eager-closure row per port is what keeps that honest.
> - A helper that touches no process, file, or environment is not host mechanics and does not
> belong here: pure record readers, config-source values, result text, memoization, async
> scoping, coordinate validation, and device-scope parsing live in `@agent-device/kernel`
> beside its other primitives.
> - `@agent-device/capture-kit` owns capture, snapshot, and recording behavior — PNG tooling,
> screenshot density and pixel diffing, snapshot occlusion, mobile snapshot semantics,
> quality verdicts and backend capability tables. Snapshot *behavior* is capture domain, not
> contracts vocabulary, and host mechanics are host-kit's, not capture-kit's.
> - `@agent-device/provision-kit` owns provisioning mechanics — install-artifact acquisition
> (local paths, archives, guarded network downloads) and host toolchain probing.
> - The enforced direction is `kernel < contracts < host-kit < capture-kit < provision-kit <
> platform/provider/daemon`.
> - Contracts stays vocabulary, plan models, and pure classification with no process,
> filesystem, or timer mechanics (the existing planted-red gate); platform-specific parsing
> stays with its family package and reaches legacy callers through composition, never by a
> family importing another owner's internals.
>
> Enforcement: each substrate package's exported subpaths are pinned in
> `package-boundaries.test.ts` (widening fails the gate), the contracts mechanics gate stays
> planted red, and the `platforms-root-shape` rule rejects any new shared file or directory
> appearing directly under `src/platforms`.

The Apple XCUITest runner client is a durable platform-owned implementation facet colocated
inside `packages/platform-apple` as the `src/runner/` subtree (#2040) — Apple mechanics belong to
the Apple package. R13 models the facet by enumeration rather than by exception sprawl: the family
Expand Down
2 changes: 1 addition & 1 deletion docs/agents/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ whether tests distinguish changed decision logic. Do not infer redundancy from l
Run `pnpm depgraph affected` before touching a high-fan-in module:

```sh
pnpm depgraph affected src/utils/exec.ts
pnpm depgraph affected packages/capture-kit/src/exec.ts
pnpm depgraph affected src/daemon/ref-frame.ts --json --limit 25
```

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@
"check:unit": "pnpm test:unit && pnpm check:tmpdir-leaks && pnpm test:smoke",
"check": "pnpm check:tooling && pnpm check:fallow && pnpm check:unit",
"prepack": "pnpm check:mcp-metadata && pnpm package:npm",
"typecheck": "tsc -b packages/xml packages/kernel packages/contracts packages/capture-kit packages/platform-apple packages/platform-android packages/platform-harmonyos packages/platform-vega packages/platform-linux packages/platform-web packages/ad-script packages/selectors packages/ad-replay packages/maestro packages/replay-test packages/provider-webdriver packages/provider-limrun && tsc -p tsconfig.json && tsc -p examples/sdk/tsconfig.json",
"typecheck": "tsc -b packages/xml packages/kernel packages/contracts packages/host-kit packages/capture-kit packages/platform-apple packages/platform-android packages/platform-harmonyos packages/platform-vega packages/platform-linux packages/platform-web packages/ad-script packages/selectors packages/ad-replay packages/maestro packages/replay-test packages/provider-webdriver packages/provider-limrun && tsc -p tsconfig.json && tsc -p examples/sdk/tsconfig.json",
"test-app:install": "pnpm install --dir examples/test-app",
"test-app:start": "pnpm --dir examples/test-app start",
"test-app:ios": "pnpm --dir examples/test-app ios",
Expand Down Expand Up @@ -278,6 +278,7 @@
"@agent-device/ad-script": "workspace:*",
"@agent-device/capture-kit": "workspace:*",
"@agent-device/contracts": "workspace:*",
"@agent-device/host-kit": "workspace:*",
"@agent-device/kernel": "workspace:*",
"@agent-device/maestro": "workspace:*",
"@agent-device/platform-android": "workspace:*",
Expand Down
14 changes: 0 additions & 14 deletions packages/ad-replay/src/internal/canonical-json.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/ad-replay/src/internal/plan-digest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createHash } from 'node:crypto';
import type { SessionAction } from '@agent-device/contracts/session';
import { canonicalJson } from './canonical-json.ts';
import { canonicalJson } from '@agent-device/kernel/collections';

/**
* ADR 0012 decision 4 / migration step 5: `planDigest` is SHA-256 over the
Expand Down
51 changes: 50 additions & 1 deletion packages/capture-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,64 @@
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Private durable-capture mechanics shared by platform runtimes, providers, and daemon orchestration.",
"description": "Private host and durable-capture mechanics shared by platform runtimes, providers, and daemon orchestration: process execution, diagnostics, archives, PNG tooling, and request-scoped plumbing.",
"dependencies": {
"@agent-device/contracts": "workspace:*",
"@agent-device/host-kit": "workspace:*",
"@agent-device/kernel": "workspace:*"
},
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
},
"./mobile-snapshot-semantics": {
"types": "./src/mobile-snapshot-semantics.ts",
"default": "./src/mobile-snapshot-semantics.ts"
},
"./png": {
"types": "./src/png.ts",
"default": "./src/png.ts"
},
"./png-resize": {
"types": "./src/png-resize.ts",
"default": "./src/png-resize.ts"
},
"./png-rgb-difference": {
"types": "./src/png-rgb-difference.ts",
"default": "./src/png-rgb-difference.ts"
},
"./png-size": {
"types": "./src/png-size.ts",
"default": "./src/png-size.ts"
},
"./png-worker-client": {
"types": "./src/png-worker-client.ts",
"default": "./src/png-worker-client.ts"
},
"./screenshot-density": {
"types": "./src/screenshot-density.ts",
"default": "./src/screenshot-density.ts"
},
"./screenshot-diff-pixels": {
"types": "./src/screenshot-diff-pixels.ts",
"default": "./src/screenshot-diff-pixels.ts"
},
"./snapshot-occlusion": {
"types": "./src/snapshot-occlusion.ts",
"default": "./src/snapshot-occlusion.ts"
},
"./snapshot-quality-backend-capabilities": {
"types": "./src/snapshot-quality-backend-capabilities.ts",
"default": "./src/snapshot-quality-backend-capabilities.ts"
},
"./snapshot-quality-verdict": {
"types": "./src/snapshot-quality-verdict.ts",
"default": "./src/snapshot-quality-verdict.ts"
}
},
"devDependencies": {
"@types/pngjs": "^6.0.5",
"pngjs": "^7.0.0"
}
}
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
buildMobileSnapshotPresentation,
classifyOffscreenScrollDirection,
isConfirmedOnScreenProbe,
} from '../../snapshot/mobile-snapshot-semantics.ts';
} from './mobile-snapshot-semantics.ts';
import { isNodeVisibleInEffectiveViewport } from '@agent-device/contracts/snapshot';
import type { Rect, SnapshotNode } from '@agent-device/kernel/snapshot';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
resolveEffectiveViewportRect,
resolveViewportRect,
} from '@agent-device/contracts/snapshot';
import { inferVerticalScrollIndicatorDirections } from '../utils/scroll-indicator.ts';
import { inferVerticalScrollIndicatorDirections } from '@agent-device/kernel/scroll-indicator';
import type { HiddenContentHint, Rect, SnapshotNode } from '@agent-device/kernel/snapshot';

type Direction = 'above' | 'below';
Expand Down Expand Up @@ -320,6 +320,7 @@ function classifyVerticalDirection(targetRect: Rect, viewportRect: Rect): Direct
return null;
}

// fallow-ignore-next-line complexity
function isDiscoverableOffscreenNode(node: SnapshotNode): boolean {
if (node.hittable === true) {
return true;
Expand Down Expand Up @@ -378,6 +379,7 @@ function findNearestVisibleScrollableAncestor(
);
}

// fallow-ignore-next-line complexity
function mergeScrollIndicatorDirections(
nodes: SnapshotNode[],
visibleNodeIndexes: Set<number>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { afterAll, test } from 'vitest';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { PNG } from '../png.ts';
import { resizePngFileToScale } from '../png-resize.ts';
import { terminatePngWorker } from '../png-worker-client.ts';
import { mkdtempForTestSync } from '../../__tests__/test-utils/tmp-dir.ts';
import { PNG } from './png.ts';
import { resizePngFileToScale } from './png-resize.ts';
import { terminatePngWorker } from './png-worker-client.ts';
import { mkdtempForTestSync } from './tmp-dir.fixtures.ts';

afterAll(async () => {
await terminatePngWorker();
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { AppError } from '@agent-device/kernel/errors';
import { PNG } from '../png.ts';
import { readPngSize } from '../png-size.ts';
import { mkdtempForTestSync } from '../../__tests__/test-utils/tmp-dir.ts';
import { PNG } from './png.ts';
import { readPngSize } from './png-size.ts';
import { mkdtempForTestSync } from './tmp-dir.fixtures.ts';

test('readPngSize reads PNG dimensions from the file header', async () => {
const filePath = tmpPngPath('size');
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { afterAll, test } from 'vitest';
import assert from 'node:assert/strict';
import { AppError } from '@agent-device/kernel/errors';
import { PNG } from '../png.ts';
import { PNG } from './png.ts';
import {
computePngRgbDifferenceAsync,
computeScreenshotDiffPixelsAsync,
decodePngAsync,
encodePngAsync,
terminatePngWorker,
} from '../png-worker-client.ts';
import { computeScreenshotDiffPixels } from '../screenshot-diff-pixels.ts';
} from './png-worker-client.ts';
import { computeScreenshotDiffPixels } from './screenshot-diff-pixels.ts';

afterAll(async () => {
await terminatePngWorker();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Worker } from 'node:worker_threads';
import { emitDiagnostic } from './diagnostics.ts';
import { emitDiagnostic } from '@agent-device/host-kit/diagnostics';
import { AppError, toAppErrorCode } from '@agent-device/kernel/errors';
import { resolveInternalEntryModulePath } from './internal-entry.ts';
import { decodePng, PNG } from './png.ts';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test } from 'vitest';
import assert from 'node:assert/strict';
import { resultTransferList } from '../png-worker.ts';
import { resultTransferList } from './png-worker.ts';

test('resultTransferList transfers a buffer that fully owns its ArrayBuffer', () => {
const owned = new Uint8Array(8).fill(1);
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { test } from 'vitest';
import assert from 'node:assert/strict';
import type { RawSnapshotNode } from '@agent-device/kernel/snapshot';
import { annotateCoveredSnapshotNodes } from '../snapshot-occlusion.ts';
import { annotateCoveredSnapshotNodes } from './snapshot-occlusion.ts';

// #1478 P5 codec-extraction regression: `annotateCoveredSnapshotNodes` marks
// a touch candidate `interactionBlocked: 'covered'` when a later, floating
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { RawSnapshotNode, Rect } from '@agent-device/kernel/snapshot';
import { centerOfRect } from '@agent-device/kernel/snapshot';
import { areRectsApproximatelyEqual, normalizeRect } from '../utils/rect-center.ts';
import { areRectsApproximatelyEqual, normalizeRect } from '@agent-device/kernel/rect-center';
import { containsPoint } from '@agent-device/kernel/rect';
import { normalizeType, isViewportRootNode } from '@agent-device/contracts/snapshot';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { expect, test } from 'vitest';
import {
ANDROID_SNAPSHOT_BACKEND_CAPABILITIES,
SNAPSHOT_BACKEND_CAPABILITIES,
} from './backend-capabilities.ts';
} from './snapshot-quality-backend-capabilities.ts';

type SnapshotBackendParityFixture = {
backends: Array<{
Expand All @@ -33,6 +33,7 @@ const SNAPSHOT_BACKEND_PARITY_FIXTURE_PATH = path.resolve(
import.meta.dirname,
'..',
'..',
'..',
'contracts',
'fixtures',
'ios-snapshot-backends.json',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
isSparseSnapshotQualityVerdict,
preferredSnapshotBackendForVerdict,
readSnapshotQualityVerdict,
} from '../verdict.ts';
} from './snapshot-quality-verdict.ts';

test('readSnapshotQualityVerdict accepts a well-formed verdict', () => {
const verdict = readSnapshotQualityVerdict({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { SnapshotQualityVerdict } from '@agent-device/kernel/snapshot';
import { SNAPSHOT_QUALITY_BACKEND_CAPABILITIES } from './backend-capabilities.ts';
import { SNAPSHOT_QUALITY_BACKEND_CAPABILITIES } from './snapshot-quality-backend-capabilities.ts';

const SNAPSHOT_QUALITY_STATES = new Set<SnapshotQualityVerdict['state']>([
'healthy',
Expand Down
Loading
Loading