From 636c21ab63227fee84da29576ec2f45ff8140881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 27 Aug 2026 17:13:32 +0200 Subject: [PATCH 1/4] refactor: sink package-closed src modules into existing packages Move closed modules into contracts, kernel, capture-kit, and ad-script, and declare DaemonCommandDescriptor in core so R6/R9 can pin the remaining provider-webdriver type cycle. Co-authored-by: Cursor --- AGENTS.md | 2 +- docs/dependency-graph-findings.md | 32 +++++---- packages/ad-replay/src/internal/inspect.ts | 2 +- packages/ad-script/src/index.ts | 28 ++++++-- .../src/internal}/__tests__/format.test.ts | 0 .../internal}/__tests__/replay-input.test.ts | 0 .../ad-script/src/internal}/format.ts | 0 .../ad-script/src/internal}/recorded-input.ts | 2 +- .../ad-script/src/internal}/replay-input.ts | 2 +- packages/ad-script/src/internal/script.ts | 3 +- .../src/internal}/target-evidence-tree.ts | 13 +--- packages/capture-kit/package.json | 4 ++ .../src}/device-inventory-context.ts | 0 packages/contracts/package.json | 8 +++ .../contracts/src}/interaction-outcome.ts | 0 packages/contracts/src/ref-frame-effect.ts | 14 +--- .../src/snapshot-quality-warnings.ts | 0 packages/kernel/package.json | 4 ++ .../kernel/src}/screenshot-geometry.ts | 2 +- .../help-conformance-sample-producers.ts | 2 +- scripts/help-conformance-sample-outputs.mjs | 2 +- scripts/layering/check.ts | 23 ++----- scripts/layering/daemon-modularity.test.ts | 31 ++++----- scripts/layering/daemon-modularity.ts | 19 ++---- scripts/layering/model.ts | 1 - scripts/layering/package-boundaries.test.ts | 3 + src/__tests__/eager-closure-budgets.ts | 19 ++++-- .../test-utils/device-inventory-gateways.ts | 2 +- src/commands/capture/runtime/snapshot.ts | 2 +- .../interaction/runtime/interactions.ts | 2 +- .../interaction/runtime/selector-wait.ts | 13 ++-- .../daemon-command-descriptor.ts | 64 ++++++++++++++++++ src/core/command-descriptor/derive.ts | 2 +- src/core/command-descriptor/types.ts | 2 +- src/core/dispatch-resolve.ts | 2 +- src/core/interaction-targeting.ts | 2 +- src/core/press-retarget.ts | 7 +- .../__tests__/replay-device-selection.test.ts | 2 +- .../__tests__/snapshot-quality-latch.test.ts | 2 +- src/daemon/daemon-command-registry.ts | 65 +++---------------- .../session-inventory-appleos.test.ts | 6 +- .../__tests__/session-replay-runtime.test.ts | 5 +- src/daemon/handlers/interaction-common.ts | 2 +- .../handlers/interaction-recorded-input.ts | 2 +- .../handlers/interaction-touch-runtime.ts | 2 +- src/daemon/handlers/session-doctor-device.ts | 2 +- src/daemon/handlers/session-doctor.ts | 2 +- src/daemon/handlers/session-inventory.ts | 2 +- .../handlers/session-replay-action-runtime.ts | 2 +- .../handlers/session-replay-repair-hint.ts | 3 +- .../handlers/session-replay-runtime-plan.ts | 4 +- src/daemon/handlers/session-replay-runtime.ts | 2 +- .../session-replay-target-classification.ts | 12 ++-- .../handlers/session-test-shard-devices.ts | 2 +- .../handlers/session-test-source-discovery.ts | 3 +- src/daemon/ios-app-session-hint.test.ts | 4 +- src/daemon/parameterized-recorded-fill.ts | 2 +- src/daemon/replay-device-selection.ts | 8 ++- src/daemon/request-handler-chain.ts | 11 +++- src/daemon/request-router.ts | 2 +- src/daemon/session-action-recorder.ts | 2 +- src/daemon/session-target-evidence.ts | 8 +-- src/daemon/snapshot-quality-latch.ts | 2 +- src/platform-runtime-android-emulator-host.ts | 2 +- src/platform-runtime-open-target.ts | 2 +- src/platforms/vega/__tests__/doctor.test.ts | 2 +- src/replay/script-source-bundle.ts | 2 +- src/replay/source-discovery.ts | 2 +- .../screenshot-diff-overlay-matches.ts | 2 +- .../screenshot-diff-regions.ts | 2 +- src/screenshot-diff/screenshot-diff.ts | 2 +- .../__tests__/warnings.test.ts | 2 +- src/utils/__tests__/output.test.ts | 2 +- test/replay-compat/corpus.test.ts | 2 +- 74 files changed, 258 insertions(+), 237 deletions(-) rename {src/replay => packages/ad-script/src/internal}/__tests__/format.test.ts (100%) rename {src/compat => packages/ad-script/src/internal}/__tests__/replay-input.test.ts (100%) rename {src/replay => packages/ad-script/src/internal}/format.ts (100%) rename {src/replay => packages/ad-script/src/internal}/recorded-input.ts (94%) rename {src/compat => packages/ad-script/src/internal}/replay-input.ts (95%) rename {src/replay => packages/ad-script/src/internal}/target-evidence-tree.ts (81%) rename {src/request => packages/capture-kit/src}/device-inventory-context.ts (100%) rename {src/contracts => packages/contracts/src}/interaction-outcome.ts (100%) rename src/snapshot-quality/warnings.ts => packages/contracts/src/snapshot-quality-warnings.ts (100%) rename {src/utils => packages/kernel/src}/screenshot-geometry.ts (93%) create mode 100644 src/core/command-descriptor/daemon-command-descriptor.ts diff --git a/AGENTS.md b/AGENTS.md index 45daae9698..715f591ca6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,7 +63,7 @@ Read the declaration rather than maintaining a prose copy: 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 +contracts in `@agent-device/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, diff --git a/docs/dependency-graph-findings.md b/docs/dependency-graph-findings.md index 290f8aeedd..51484ebbba 100644 --- a/docs/dependency-graph-findings.md +++ b/docs/dependency-graph-findings.md @@ -76,9 +76,9 @@ type-only inversions, R7 pins SessionState field ownership, and the shared selec - Still outside every rule: **dynamic** import direction (0 inversions today, nothing watching), and anything inside a zone. -## 0. Where the inversions ended up (and why 7 is the floor for now) +## 0. Where the inversions ended up (and why 5 is the floor for now) -61 → 7. The last pass moved four keystones, each of which was pinning a much larger set: +61 → 7, then 5. The last pass moved four keystones, each of which was pinning a much larger set: | Keystone moved to `contracts/` | Unblocked | |---|---| @@ -110,7 +110,7 @@ with `command`/`positionals` `Pick`ed from the wire so they cannot drift from it resolver already read only those three, in two spellings (the full type and a `Pick` of it); one narrow name replaced both. -**The remaining 7 are positions, not debt** — each for a mechanical reason, not an appeal to an ADR: +**The remaining 5 are positions, not debt** — each for a mechanical reason, not an appeal to an ADR: - **4 × `AgentDeviceClient`** (`commands/command-contract.ts`, `commands/command-surface.ts`, `commands/family/types.ts`, `mcp/command-tools.ts`). The facade cannot move below `commands/` @@ -119,20 +119,18 @@ narrow name replaced both. zone-level type cycle, and breaking it means deciding where the projection registry belongs — a design call, not a file move. A narrower port does not exist either: 4 files *name* the facade, but 26 call sites use methods across 13 of its namespaces, so any port would re-declare it. -- **2 × `DaemonCommandDescriptor`** (`core/command-descriptor/derive.ts`, `.../types.ts`). It is - *stated in terms of* the server-private `daemon/types.ts` `DaemonRequest` — - `refFrameEffect?: (req: DaemonRequest) => RefFrameEffect`, - `allowSessionlessDefaultDevice?: (req: DaemonRequest) => boolean` — so it cannot be declared below - the daemon. Having `core/` re-declare a parallel 13-field shape instead would trade one erased - edge for a second source of truth. -- **1 × `DaemonCommandRoute`** (`commands/command-explain.ts`). It is - `keyof typeof DAEMON_ROUTE_HANDLERS` — *computed from* the daemon's handler table, so it cannot - exist below that table. `command-explain.ts` uses it to key an exhaustive - `Record` of owner files; a hand-written union in `contracts/` would - drop exactly that exhaustiveness. - -All three are argued at `TYPE_INVERSION_BASELINE` in `scripts/layering/check.ts`, next to the -numbers they explain. +- **1 × `DaemonCommandRoute`** (`commands/command-explain.ts`). The union lives in core so + descriptors can name a route without importing the daemon, and the handler table covers it with + `satisfies Record`. `command-explain.ts` still type-imports the re-export + from `daemon-command-registry.ts` to key an exhaustive owner-file map; that remaining inversion + is the commands-zone consumer, not a second source of truth for the union. + +The two `DaemonCommandDescriptor` inversions (`core/command-descriptor/derive.ts`, `.../types.ts`) +are gone: the shape is declared in core, generic over `DispatchedCommand`, so those files no longer +import the daemon. `daemon/types.ts` `DaemonRequest` stays server-private. + +All remaining inversions are argued at `TYPE_INVERSION_BASELINE` in `scripts/layering/check.ts`, next +to the numbers they explain. ## 0b. The biggest structural finding is not an inversion diff --git a/packages/ad-replay/src/internal/inspect.ts b/packages/ad-replay/src/internal/inspect.ts index cd695938d3..6cb63be596 100644 --- a/packages/ad-replay/src/internal/inspect.ts +++ b/packages/ad-replay/src/internal/inspect.ts @@ -13,7 +13,7 @@ import { resolveReplayEntryIndex, type PendingRecordAndHeal } from './resume.ts' * #1478 P5 stage C2b: the read-only `.ad` inspection façade. Moved out of * `session-replay-runtime.ts`'s old `parseReplayScript` (the * legacy-JSON-payload rejection it guarded) plus the `parseReplayInput` - * composition (`src/compat/replay-input.ts`) it fed into — this is the same + * composition (`parseReplayInput`) it fed into — this is the same * `parseReplayScriptDetailed` + `readReplayScriptMetadata` pair * `src/cli/commands/replay.ts` and `session-test-source-discovery.ts` already * call directly off `@agent-device/ad-script`; nothing beyond the actions, diff --git a/packages/ad-script/src/index.ts b/packages/ad-script/src/index.ts index c2f772047c..b559ca74b5 100644 --- a/packages/ad-script/src/index.ts +++ b/packages/ad-script/src/index.ts @@ -41,12 +41,8 @@ * record/replay AND the daemon" shape this package exists to own. */ -export { - parseReplayScriptDetailed, - readReplayScriptMetadata, - REPLAY_VAR_KEY_RE, -} from './internal/script.ts'; -export type { ParsedReplayScript, ReplayScriptMetadata } from './internal/script.ts'; +export { parseReplayScriptDetailed, readReplayScriptMetadata } from './internal/script.ts'; +export type { ReplayScriptMetadata } from './internal/script.ts'; export { resolveDeclaredScriptPlatform } from './internal/open-script.ts'; @@ -101,3 +97,23 @@ export { readReplayShellEnvSource, resolveReplayAction, } from './internal/vars.ts'; + +export { + isMaestroYamlPath, + maestroBackendRequiredMessage, + resolveReplayFormat, +} from './internal/format.ts'; + +export { + readRecordedInputVariableName, + recordedInputPlaceholder, + validateRecordedInputVariableName, +} from './internal/recorded-input.ts'; + +export { + buildAncestryChain, + buildIndexMap, + filterIdentitySet, +} from './internal/target-evidence-tree.ts'; + +export { parseReplayInput } from './internal/replay-input.ts'; diff --git a/src/replay/__tests__/format.test.ts b/packages/ad-script/src/internal/__tests__/format.test.ts similarity index 100% rename from src/replay/__tests__/format.test.ts rename to packages/ad-script/src/internal/__tests__/format.test.ts diff --git a/src/compat/__tests__/replay-input.test.ts b/packages/ad-script/src/internal/__tests__/replay-input.test.ts similarity index 100% rename from src/compat/__tests__/replay-input.test.ts rename to packages/ad-script/src/internal/__tests__/replay-input.test.ts diff --git a/src/replay/format.ts b/packages/ad-script/src/internal/format.ts similarity index 100% rename from src/replay/format.ts rename to packages/ad-script/src/internal/format.ts diff --git a/src/replay/recorded-input.ts b/packages/ad-script/src/internal/recorded-input.ts similarity index 94% rename from src/replay/recorded-input.ts rename to packages/ad-script/src/internal/recorded-input.ts index e6684dbf16..af909de7d7 100644 --- a/src/replay/recorded-input.ts +++ b/packages/ad-script/src/internal/recorded-input.ts @@ -1,5 +1,5 @@ import { AppError } from '@agent-device/kernel/errors'; -import { REPLAY_VAR_KEY_RE } from '@agent-device/ad-script'; +import { REPLAY_VAR_KEY_RE } from './script.ts'; const RECORDED_INPUT_PLACEHOLDER_RE = /^\$\{([A-Z_][A-Z0-9_]*)\}$/; diff --git a/src/compat/replay-input.ts b/packages/ad-script/src/internal/replay-input.ts similarity index 95% rename from src/compat/replay-input.ts rename to packages/ad-script/src/internal/replay-input.ts index b53916a4dd..ad15763ffe 100644 --- a/src/compat/replay-input.ts +++ b/packages/ad-script/src/internal/replay-input.ts @@ -5,7 +5,7 @@ import { readReplayScriptMetadata, type ParsedReplayScript, type ReplayScriptMetadata, -} from '@agent-device/ad-script'; +} from './script.ts'; export type ParsedReplayInput = ParsedReplayScript & { metadata: ReplayScriptMetadata; diff --git a/packages/ad-script/src/internal/script.ts b/packages/ad-script/src/internal/script.ts index 14824602e8..d5e972d5a2 100644 --- a/packages/ad-script/src/internal/script.ts +++ b/packages/ad-script/src/internal/script.ts @@ -25,8 +25,7 @@ import { * underscores, leading with a letter or underscore. Canonical here because * `env KEY=VALUE` directive parsing is script grammar; the sibling * `vars.ts` (runtime `${VAR}` resolution) imports it directly, and - * `src/replay/recorded-input.ts` imports it from this package's façade - * rather than duplicating the rule. + * `recorded-input.ts` imports it directly rather than duplicating the rule. */ export const REPLAY_VAR_KEY_RE = /^[A-Z_][A-Z0-9_]*$/; diff --git a/src/replay/target-evidence-tree.ts b/packages/ad-script/src/internal/target-evidence-tree.ts similarity index 81% rename from src/replay/target-evidence-tree.ts rename to packages/ad-script/src/internal/target-evidence-tree.ts index 7caa7422e6..e6065399e5 100644 --- a/src/replay/target-evidence-tree.ts +++ b/packages/ad-script/src/internal/target-evidence-tree.ts @@ -1,21 +1,10 @@ -/** - * ADR 0012 decision 3: the tree-structural evidence primitives shared by the - * record-time writer (`src/daemon/session-target-evidence.ts`), replay-time - * verification (`src/daemon/handlers/session-replay-target-classification.ts`), - * and the command-resolution landmark check `wait` runs inside its polling - * loop (#1349, `src/commands/interaction/runtime/selector-read.ts`). They are - * pure functions over `SnapshotNode` trees; keeping them in the shared - * `replay/` zone lets the commands runtime consume them without importing the - * daemon. - */ - import type { SnapshotNode } from '@agent-device/kernel/snapshot'; import { matchesAncestryPrefix, matchesLocalIdentity, readNodeLocalIdentity, type LocalIdentity, -} from '@agent-device/ad-script'; +} from './target-annotation-identity.ts'; import type { TargetAncestryEntry } from '@agent-device/contracts/replay'; export function buildIndexMap(nodes: readonly SnapshotNode[]): Map { diff --git a/packages/capture-kit/package.json b/packages/capture-kit/package.json index 063685d2c1..a42b716ebe 100644 --- a/packages/capture-kit/package.json +++ b/packages/capture-kit/package.json @@ -14,6 +14,10 @@ "types": "./src/index.ts", "default": "./src/index.ts" }, + "./device-inventory-context": { + "types": "./src/device-inventory-context.ts", + "default": "./src/device-inventory-context.ts" + }, "./mobile-snapshot-semantics": { "types": "./src/mobile-snapshot-semantics.ts", "default": "./src/mobile-snapshot-semantics.ts" diff --git a/src/request/device-inventory-context.ts b/packages/capture-kit/src/device-inventory-context.ts similarity index 100% rename from src/request/device-inventory-context.ts rename to packages/capture-kit/src/device-inventory-context.ts diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 8513fa9e65..5101a12e7b 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -243,6 +243,10 @@ "types": "./src/interaction-guarantees.ts", "default": "./src/interaction-guarantees.ts" }, + "./interaction-outcome": { + "types": "./src/interaction-outcome.ts", + "default": "./src/interaction-outcome.ts" + }, "./interactor-operation-catalog": { "types": "./src/interactor-operation-catalog.ts", "default": "./src/interactor-operation-catalog.ts" @@ -423,6 +427,10 @@ "types": "./src/snapshot-presentation.ts", "default": "./src/snapshot-presentation.ts" }, + "./snapshot-quality-warnings": { + "types": "./src/snapshot-quality-warnings.ts", + "default": "./src/snapshot-quality-warnings.ts" + }, "./snapshot-runtime": { "types": "./src/snapshot-runtime.ts", "default": "./src/snapshot-runtime.ts" diff --git a/src/contracts/interaction-outcome.ts b/packages/contracts/src/interaction-outcome.ts similarity index 100% rename from src/contracts/interaction-outcome.ts rename to packages/contracts/src/interaction-outcome.ts diff --git a/packages/contracts/src/ref-frame-effect.ts b/packages/contracts/src/ref-frame-effect.ts index dcde9e1675..7ea780dced 100644 --- a/packages/contracts/src/ref-frame-effect.ts +++ b/packages/contracts/src/ref-frame-effect.ts @@ -1,15 +1,5 @@ -// ADR 0014 ref-frame effect classification. -// -// A plain string union, and the only part of the daemon command descriptor that zones below the -// daemon actually need: `core/command-descriptor/registry.ts` classifies each command with it while -// composing the descriptor registry. Declaring it beside `DAEMON_ROUTE_HANDLERS` made core reach up -// four ranks for three string literals. -// -// The rest of the descriptor shape deliberately stays in the daemon: `DaemonCommandRoute` is -// `keyof typeof DAEMON_ROUTE_HANDLERS` and `DaemonRefFrameEffect` resolves against the daemon's own -// request type. Moving those down would mean re-declaring the route names in contracts and adding a -// gate to prove the handler map still covers them — more coupling to remove a dependency, which is -// the wrong trade. ADR 0003/0008 own that boundary. +// ADR 0014 ref-frame effect classification. A plain string union so +// `core/command-descriptor/` can classify commands without importing the daemon. /** * ADR 0014 session ref-frame lifetime. Declares how a daemon command relates to diff --git a/src/snapshot-quality/warnings.ts b/packages/contracts/src/snapshot-quality-warnings.ts similarity index 100% rename from src/snapshot-quality/warnings.ts rename to packages/contracts/src/snapshot-quality-warnings.ts diff --git a/packages/kernel/package.json b/packages/kernel/package.json index e1ae4b3d4d..78f6a0b753 100644 --- a/packages/kernel/package.json +++ b/packages/kernel/package.json @@ -53,6 +53,10 @@ "types": "./src/rect-center.ts", "default": "./src/rect-center.ts" }, + "./screenshot-geometry": { + "types": "./src/screenshot-geometry.ts", + "default": "./src/screenshot-geometry.ts" + }, "./redaction": { "types": "./src/redaction.ts", "default": "./src/redaction.ts" diff --git a/src/utils/screenshot-geometry.ts b/packages/kernel/src/screenshot-geometry.ts similarity index 93% rename from src/utils/screenshot-geometry.ts rename to packages/kernel/src/screenshot-geometry.ts index 789b2383fc..95a3cc75ae 100644 --- a/src/utils/screenshot-geometry.ts +++ b/packages/kernel/src/screenshot-geometry.ts @@ -1,4 +1,4 @@ -import type { Rect } from '@agent-device/kernel/snapshot'; +import type { Rect } from './snapshot.ts'; export type ImageDimensions = { width: number; height: number }; diff --git a/scripts/__tests__/help-conformance-sample-producers.ts b/scripts/__tests__/help-conformance-sample-producers.ts index 5e01bd0ec0..02de2d8fff 100644 --- a/scripts/__tests__/help-conformance-sample-producers.ts +++ b/scripts/__tests__/help-conformance-sample-producers.ts @@ -34,7 +34,7 @@ import type { ConnectVerification } from '../../src/cli/connection/connect-provi import type { RemoteConnectionState } from '../../src/remote/remote-connection-state.ts'; import { AppError, normalizeError } from '@agent-device/kernel/errors'; import type { SnapshotQualityVerdict } from '@agent-device/kernel/snapshot'; -import { renderSnapshotQualityWarnings } from '../../src/snapshot-quality/warnings.ts'; +import { renderSnapshotQualityWarnings } from '@agent-device/contracts/snapshot-quality-warnings'; import { formatSnapshotText, printHumanError } from '../../src/utils/output.ts'; // The production renderer behind each captured sample in diff --git a/scripts/help-conformance-sample-outputs.mjs b/scripts/help-conformance-sample-outputs.mjs index 703c0f6bc1..bdcf583220 100644 --- a/scripts/help-conformance-sample-outputs.mjs +++ b/scripts/help-conformance-sample-outputs.mjs @@ -72,7 +72,7 @@ hint: The UI kept changing for the whole settle budget (animation, carousel, or // Recovered snapshot: the private-ax fallback fired but still exposed // actionable refs. Warning wording is renderSnapshotQualityWarnings -// (src/snapshot-quality/warnings.ts); lines are the structured snapshot +// (packages/contracts/src/snapshot-quality-warnings.ts); lines are the structured snapshot // renderer (src/utils/output.ts formatSnapshotText). export const PRIVATE_AX_RECOVERY_SAMPLE = { command: 'agent-device snapshot -i', diff --git a/scripts/layering/check.ts b/scripts/layering/check.ts index f9169c1707..5cacc3c703 100644 --- a/scripts/layering/check.ts +++ b/scripts/layering/check.ts @@ -194,7 +194,7 @@ function checkBackEdges(edges: readonly ResolvedImportEdge[]): LayeringViolation // R6 ratchet: type-only spine inversions, per zone pair. R5 cannot see these (a type-only import // is free at runtime), but "zone A is declared in terms of zone B" is still a boundary claim, and -// ranking type edges surfaced 61 of them. Down to 7, and every one of the 7 is now a deliberate +// ranking type edges surfaced 61 of them. Down to 5, and every one of the 5 is now a deliberate // architectural position rather than a misplaced declaration: // // commands/mcp -> client (4) `AgentDeviceClient`, used as an opaque handle ("the client this @@ -207,20 +207,12 @@ function checkBackEdges(edges: readonly ResolvedImportEdge[]): LayeringViolation // a design call, not a file move. R5 is zero here: nothing imports // the client at runtime, only its type. // -// core -> daemon-server (2) `DaemonCommandDescriptor`, which is STATED IN TERMS OF the daemon's -// own server-private `DaemonRequest` (`refFrameEffect`, -// `allowSessionlessDefaultDevice`, `skipSessionlessProviderDevice` -// are all `(req: DaemonRequest) => …`). It therefore cannot be -// declared below the daemon, and having core/ re-declare a parallel -// 13-field shape would trade one erased edge for a second source of -// truth. Zones that only need to CLASSIFY a command take -// `contracts/dispatched-command.ts` instead. ADR 0003/0008. -// -// commands -> daemon-server (1) `DaemonCommandRoute` = `keyof typeof DAEMON_ROUTE_HANDLERS`, so -// it is COMPUTED FROM the daemon's handler table and cannot exist -// below it. `commands/command-explain.ts` uses it to key an -// exhaustive `Record` of owner files; a -// hand-written union in contracts/ would drop that exhaustiveness. +// commands -> daemon-server (1) `DaemonCommandRoute` is declared in core so descriptors can +// name a route without importing the daemon. `command-explain.ts` +// still type-imports the re-export from `daemon-command-registry.ts` +// to key an exhaustive `Record` of +// owner files; that remaining inversion is the commands-zone +// consumer, not a second source of truth for the union. // // See docs/dependency-graph-findings.md §0 for the long form. The counts may only go DOWN. Fixing edges without lowering the number fails too, so the baseline // cannot quietly stop describing the tree. @@ -230,7 +222,6 @@ function checkBackEdges(edges: readonly ResolvedImportEdge[]): LayeringViolation export const TYPE_INVERSION_BASELINE: Readonly> = { 'commands -> client': 3, 'commands -> daemon-server': 1, - 'core -> daemon-server': 2, 'mcp -> client': 1, }; diff --git a/scripts/layering/daemon-modularity.test.ts b/scripts/layering/daemon-modularity.test.ts index c611c359d2..074b95e00d 100644 --- a/scripts/layering/daemon-modularity.test.ts +++ b/scripts/layering/daemon-modularity.test.ts @@ -37,6 +37,7 @@ const ZONE_DIRECTORY: Readonly> = { '(root)': 'src/', 'daemon-server': 'src/daemon/', 'ad-replay': 'packages/ad-replay/src/', + 'provider-webdriver': 'packages/provider-webdriver/src/', }; /** @@ -63,9 +64,9 @@ test('daemon modularity baseline records the measured R7 ownership pressure', () Object.values(SESSION_STATE_FIELD_OWNERS).reduce((sum, owners) => sum + owners.length, 0), DAEMON_MODULARITY_BASELINE.sessionState.ownerFileClaims, ); - assert.equal(TYPE_CYCLE_BASELINE, 15); - assert.equal(DAEMON_MODULARITY_BASELINE.largestTypeCycle.zoneMembers['daemon-server'], 10); - assert.equal('daemon' in DAEMON_MODULARITY_BASELINE.largestTypeCycle.zoneMembers, false); + assert.equal(TYPE_CYCLE_BASELINE, 6); + assert.equal(DAEMON_MODULARITY_BASELINE.largestTypeCycle.zoneMembers['provider-webdriver'], 6); + assert.equal('daemon-server' in DAEMON_MODULARITY_BASELINE.largestTypeCycle.zoneMembers, false); }); test('external daemon/types.ts importer membership changes require the baseline to change', () => { @@ -175,16 +176,15 @@ test('internal trees reject deep imports globally, including from daemon', () => }); test('R9 records zone ceilings and keeps engine files outside the largest component', () => { - // One commands file and one engine file traded for two daemon-server ones, so the total - // stays at the baseline and only the per-zone claims are on trial. `commands` left the cycle - // entirely when screenshot stopped threading through generic dispatch, so its ceiling is 0. + // One commands file and one engine file traded for two provider-webdriver ones, so the + // total stays at the baseline and only the per-zone claims are on trial. const zones = DAEMON_MODULARITY_BASELINE.largestTypeCycle.zoneMembers; const violations = checkDaemonModularityRatchets( baselineEdges(), baselineTypeCycleMembers({ commands: 1, 'ad-replay': 1, - 'daemon-server': zones['daemon-server']! - 2, + 'provider-webdriver': zones['provider-webdriver']! - 2, }), ); @@ -199,13 +199,14 @@ test('R9 records zone ceilings and keeps engine files outside the largest compon // The ceiling records a count, not a membership, so the message lists every zone member instead. test('R10 zone overflow lists the whole zone so the joining member is visible', () => { const zones = DAEMON_MODULARITY_BASELINE.largestTypeCycle.zoneMembers; - // Sorts after the daemon-server probes: the old first-member pick could not name it by luck. + // Sorts after the provider-webdriver probes: the old first-member pick could not name it by luck. const joined = 'src/daemon/snapshot-interactor-capture.ts'; - // `commands` left the cycle with the screenshot cutover, so the offsetting removal comes from - // `core` instead: the total stays at the baseline and only the daemon-server claim is on trial. - const members = [...baselineTypeCycleMembers({ core: zones.core! - 1 }), joined].sort(); + const members = [ + ...baselineTypeCycleMembers({ 'provider-webdriver': zones['provider-webdriver']! - 1 }), + joined, + ].sort(); const daemonMembers = members.filter((member) => member.startsWith('src/daemon/')); - assert.notEqual(daemonMembers[0], joined); + assert.deepEqual(daemonMembers, [joined]); const violations = checkDaemonModularityRatchets(baselineEdges(), members); @@ -213,7 +214,7 @@ test('R10 zone overflow lists the whole zone so the joining member is visible', const [violation] = violations; assert.equal(violation!.rule, 'R10 daemon-modularity'); assert.equal(violation!.file, 'scripts/layering/daemon-modularity.ts'); - assert.match(violation!.message, /contains 11 daemon-server file\(s\) \(baseline 10\)/); + assert.match(violation!.message, /contains 1 daemon-server file\(s\) \(baseline 0\)/); for (const member of daemonMembers) { assert.ok(violation!.message.includes(member), `${member} missing from: ${violation!.message}`); } @@ -226,11 +227,11 @@ test('R9 rejects a baseline left above the measured cycle', () => { const zones = DAEMON_MODULARITY_BASELINE.largestTypeCycle.zoneMembers; const violations = checkDaemonModularityRatchets( baselineEdges(), - baselineTypeCycleMembers({ 'daemon-server': zones['daemon-server']! - 1 }), + baselineTypeCycleMembers({ 'provider-webdriver': zones['provider-webdriver']! - 1 }), ); assert.equal(violations.length, 1); assert.match(violations[0]!.rule, /^R9 /); - assert.match(violations[0]!.message, /dropped to 14 files \(baseline 15\)/); + assert.match(violations[0]!.message, /dropped to 5 files \(baseline 6\)/); assert.match(violations[0]!.message, /Lower LARGEST_TYPE_CYCLE_ZONE_CEILINGS by the same 1/); }); diff --git a/scripts/layering/daemon-modularity.ts b/scripts/layering/daemon-modularity.ts index c94482499d..c5aa52bde1 100644 --- a/scripts/layering/daemon-modularity.ts +++ b/scripts/layering/daemon-modularity.ts @@ -3,20 +3,11 @@ import { targetDagZone, type LayeringViolation, type ResolvedImportEdge } from ' import { SESSION_STATE_FIELD_OWNERS } from './session-state.ts'; const LARGEST_TYPE_CYCLE_ZONE_CEILINGS: Readonly> = { - // Request provider composition now consumes the neutral contracts context instead of importing - // daemon request/session types, removing the root provider seam from this component. - '(root)': 1, - core: 4, - // Same move, daemon side: with no dispatcher to re-fire a tap through, the pending-outcome - // retry declares its own callback seam instead of importing runtime admission, so - // `interaction-outcome-policy.ts` and `deferred-interaction-outcome.ts` both left the cycle. - // R63 then deleted `session-install-capability-projection.ts` outright — the general - // fact-owned projection subsumes it — taking a third member with it. - // The daemon side of that seam no longer imports the concrete provider resolver table. - 'daemon-server': 10, - // R64 deletes the last perf support closure from `apple/plugin.ts`, taking the final - // platform-owned member out of the type cycle. - platforms: 0, + // After cutting the two core→daemon DaemonCommandDescriptor type edges, the previous + // 15-file mixed-zone component collapsed; the largest remaining cycle is this + // co-defined-contract pair (`capabilities.ts` ↔ `runtime.ts` and the four files they + // pull in). The standard shrink is a third module holding the shared type. + 'provider-webdriver': 6, }; export const DAEMON_MODULARITY_BASELINE = { diff --git a/scripts/layering/model.ts b/scripts/layering/model.ts index d45a4443fe..9af061d5bc 100644 --- a/scripts/layering/model.ts +++ b/scripts/layering/model.ts @@ -51,7 +51,6 @@ const TARGET_DAG_RANK = new Map([ ['mcp', 3], ['ai-sdk', 4], ['client', 4], - ['compat', 4], ['daemon-server', 4], ['metro', 4], ['remote', 4], diff --git a/scripts/layering/package-boundaries.test.ts b/scripts/layering/package-boundaries.test.ts index 9ff43b863f..2342ac9499 100644 --- a/scripts/layering/package-boundaries.test.ts +++ b/scripts/layering/package-boundaries.test.ts @@ -105,6 +105,7 @@ const CONTRACT_EXPORTS = [ '@agent-device/contracts/interaction', '@agent-device/contracts/interaction-error', '@agent-device/contracts/interaction-guarantees', + '@agent-device/contracts/interaction-outcome', '@agent-device/contracts/interactor-operation-catalog', '@agent-device/contracts/interactor-types', '@agent-device/contracts/keyboard', @@ -150,6 +151,7 @@ const CONTRACT_EXPORTS = [ '@agent-device/contracts/settings-runtime', '@agent-device/contracts/snapshot', '@agent-device/contracts/snapshot-presentation', + '@agent-device/contracts/snapshot-quality-warnings', '@agent-device/contracts/snapshot-runtime', '@agent-device/contracts/snapshot-scope', '@agent-device/contracts/snapshot-timeout-evidence', @@ -457,6 +459,7 @@ test('the real tree parses, declares, and passes R11', () => { ); assert.deepEqual([...captureKitPackage.exportTargets.keys()].sort(), [ '@agent-device/capture-kit', + '@agent-device/capture-kit/device-inventory-context', '@agent-device/capture-kit/mobile-snapshot-semantics', '@agent-device/capture-kit/png', '@agent-device/capture-kit/png-resize', diff --git a/src/__tests__/eager-closure-budgets.ts b/src/__tests__/eager-closure-budgets.ts index 444761d4d5..d81e65d62d 100644 --- a/src/__tests__/eager-closure-budgets.ts +++ b/src/__tests__/eager-closure-budgets.ts @@ -106,10 +106,13 @@ export function discoverFacadeEntryFiles(repoRoot: string): string[] { */ export const FACADE_BUDGETS: Readonly> = Object.freeze({ // --- @agent-device/ad-replay --- - 'packages/ad-replay/src/index.ts': 57, + 'packages/ad-replay/src/index.ts': 61, // --- @agent-device/ad-script --- - 'packages/ad-script/src/index.ts': 37, + // +4: format, recorded-input, target-evidence-tree, and parseReplayInput join the codec façade + // after leaving root src. Consumers of the façade (selectors, maestro, ad-replay) move by the + // same 4; the CLI already had three of those modules via other edges and moves by 1. + 'packages/ad-script/src/index.ts': 41, // --- @agent-device/platform-apple/runner --- // #2040 extraction: the façade stays types/pure-helpers/bundle-ids; the whole @@ -122,6 +125,7 @@ export const FACADE_BUDGETS: Readonly> = Object.freeze({ // --- @agent-device/capture-kit --- // R60 review: audio-probe split into descriptor/status/recovery/live-process modules (+3 files). 'packages/capture-kit/src/index.ts': 32, + 'packages/capture-kit/src/device-inventory-context.ts': 3, 'packages/capture-kit/src/png-resize.ts': 18, 'packages/capture-kit/src/png-rgb-difference.ts': 1, 'packages/capture-kit/src/png-size.ts': 3, @@ -216,6 +220,7 @@ export const FACADE_BUDGETS: Readonly> = Object.freeze({ 'packages/contracts/src/interaction.ts': 1, 'packages/contracts/src/interaction-error.ts': 1, 'packages/contracts/src/interaction-guarantees.ts': 1, + 'packages/contracts/src/interaction-outcome.ts': 1, 'packages/contracts/src/interactor-types.ts': 1, 'packages/contracts/src/keyboard.ts': 1, 'packages/contracts/src/logs-runtime-plan.ts': 5, @@ -249,6 +254,7 @@ export const FACADE_BUDGETS: Readonly> = Object.freeze({ 'packages/contracts/src/selector-observation-runtime.ts': 1, 'packages/contracts/src/settings.ts': 3, 'packages/contracts/src/snapshot-presentation.ts': 2, + 'packages/contracts/src/snapshot-quality-warnings.ts': 1, 'packages/contracts/src/snapshot-runtime.ts': 3, 'packages/contracts/src/snapshot-scope.ts': 1, 'packages/contracts/src/snapshot-timeout-evidence.ts': 1, @@ -275,6 +281,7 @@ export const FACADE_BUDGETS: Readonly> = Object.freeze({ 'packages/kernel/src/location-coordinates.ts': 3, 'packages/kernel/src/record.ts': 3, 'packages/kernel/src/scoped-provider.ts': 1, + 'packages/kernel/src/screenshot-geometry.ts': 1, 'packages/kernel/src/source-value.ts': 3, 'packages/kernel/src/success-text.ts': 1, 'packages/kernel/src/ttl-memo.ts': 1, @@ -283,7 +290,7 @@ export const FACADE_BUDGETS: Readonly> = Object.freeze({ 'packages/kernel/src/snapshot.ts': 1, // --- @agent-device/maestro --- - 'packages/maestro/src/index.ts': 106, + 'packages/maestro/src/index.ts': 110, // --- @agent-device/platform-*: ADR-0019's metadata-eager/implementation-lazy façades. Each // evaluates only itself; every implementation sits behind a function-scoped `await import`. @@ -324,7 +331,7 @@ export const FACADE_BUDGETS: Readonly> = Object.freeze({ // --- @agent-device/selectors --- 'packages/selectors/src/ast.ts': 16, 'packages/selectors/src/engine.ts': 19, - 'packages/selectors/src/index.ts': 50, + 'packages/selectors/src/index.ts': 54, // --- @agent-device/xml --- 'packages/xml/src/index.ts': 3, @@ -380,9 +387,9 @@ export const HUB_BUDGETS: Readonly> = Object.freeze({ // readers), `input-audience.ts` (who may write a key), and `common-input-fields.ts` (the table // itself). Every command schema already evaluated all three concerns; the growth is three more // module records for the same code, with no new subtree behind any of them. - 'src/cli.ts': 380, + 'src/cli.ts': 381, 'src/platform-runtime.ts': 47, - 'src/core/command-descriptor/registry.ts': 67, + 'src/core/command-descriptor/registry.ts': 71, 'src/core/command-descriptor/platform-execution-entry.ts': 3, 'src/core/interactors/register-builtins.ts': 6, // R64 removes the perf plugin facet and keeps collector binding behind the selected runtime diff --git a/src/__tests__/test-utils/device-inventory-gateways.ts b/src/__tests__/test-utils/device-inventory-gateways.ts index 36982656d1..c71f8a8873 100644 --- a/src/__tests__/test-utils/device-inventory-gateways.ts +++ b/src/__tests__/test-utils/device-inventory-gateways.ts @@ -14,7 +14,7 @@ import type { PlatformRequestScope, } from '@agent-device/contracts/platform-runtime-host'; import type { DeviceInfo, Platform } from '@agent-device/kernel/device'; -import { withDeviceInventoryContext } from '../../request/device-inventory-context.ts'; +import { withDeviceInventoryContext } from '@agent-device/capture-kit/device-inventory-context'; import { createComposedDeviceInventoryGateways } from '../../platform-runtime-device-inventory.ts'; type TestDeviceInventoryOptions = Readonly<{ diff --git a/src/commands/capture/runtime/snapshot.ts b/src/commands/capture/runtime/snapshot.ts index d050cc9516..e6fc34df23 100644 --- a/src/commands/capture/runtime/snapshot.ts +++ b/src/commands/capture/runtime/snapshot.ts @@ -20,7 +20,7 @@ import { buildSnapshotDiff, countSnapshotComparableLines, } from '../../../snapshot/snapshot-diff.ts'; -import { renderSnapshotQualityWarnings } from '../../../snapshot-quality/warnings.ts'; +import { renderSnapshotQualityWarnings } from '@agent-device/contracts/snapshot-quality-warnings'; import { buildSnapshotVisibility } from '../../../snapshot/snapshot-visibility.ts'; import { ANDROID_SYSTEM_SURFACE_DISCLOSURE } from '../../../core/android-system-surface-disclosure.ts'; import { formatReactNativeOverlayWarning } from '../../react-native/overlay.ts'; diff --git a/src/commands/interaction/runtime/interactions.ts b/src/commands/interaction/runtime/interactions.ts index 5b1f4657b8..330244690a 100644 --- a/src/commands/interaction/runtime/interactions.ts +++ b/src/commands/interaction/runtime/interactions.ts @@ -11,7 +11,7 @@ import { SELECTOR_PIPELINE_POLICIES } from '../../../core/selector-pipeline-poli import type { Point } from '@agent-device/kernel/snapshot'; import type { AgentDeviceRuntime, CommandContext } from '../../../runtime-contract.ts'; import { isFillableType } from '@agent-device/contracts/snapshot'; -import { attachResolvedInteractionTarget } from '../../../contracts/interaction-outcome.ts'; +import { attachResolvedInteractionTarget } from '@agent-device/contracts/interaction-outcome'; import { toBackendContext } from '../../runtime-common.ts'; import { toBackendResult, type RuntimeCommand } from '../../runtime-types.ts'; import { diff --git a/src/commands/interaction/runtime/selector-wait.ts b/src/commands/interaction/runtime/selector-wait.ts index 38ef28f0bf..2666e9feba 100644 --- a/src/commands/interaction/runtime/selector-wait.ts +++ b/src/commands/interaction/runtime/selector-wait.ts @@ -1,17 +1,18 @@ import { AppError } from '@agent-device/kernel/errors'; import { WAIT_REASONS } from '@agent-device/contracts/wait'; import { findNodeByRef, normalizeRef, type SnapshotNode } from '@agent-device/kernel/snapshot'; -import { annotationLocalIdentity, readNodeLocalIdentity } from '@agent-device/ad-script'; +import { + annotationLocalIdentity, + buildAncestryChain, + buildIndexMap, + filterIdentitySet, + readNodeLocalIdentity, +} from '@agent-device/ad-script'; import { WAIT_LANDMARK_MISMATCH_REASON, type TargetAnnotationV1, type WaitLandmarkMismatchEvidence, } from '@agent-device/contracts/replay'; -import { - buildAncestryChain, - buildIndexMap, - filterIdentitySet, -} from '../../../replay/target-evidence-tree.ts'; import type { PublicPlatform } from '@agent-device/kernel/device'; import { checkWaitText, type SelectorChainMatchList } from '@agent-device/selectors'; import { diff --git a/src/core/command-descriptor/daemon-command-descriptor.ts b/src/core/command-descriptor/daemon-command-descriptor.ts new file mode 100644 index 0000000000..713fb39ff7 --- /dev/null +++ b/src/core/command-descriptor/daemon-command-descriptor.ts @@ -0,0 +1,64 @@ +import type { DispatchedCommand } from '@agent-device/contracts/command'; +import type { RefFrameEffect } from '@agent-device/contracts/replay'; + +export type SessionCommandKind = 'inventory' | 'state' | 'observability' | 'publication' | 'replay'; + +/** + * Routes a daemon command to its handler family. The handler table in + * `request-handler-chain.ts` must cover every member (`satisfies Record<…>`). + */ +export type DaemonCommandRoute = + | 'lease' + | 'session' + | 'snapshot' + | 'reactNative' + | 'recordTrace' + | 'find' + | 'interaction' + | 'generic'; + +export type DaemonRefFrameEffect = + | RefFrameEffect + | ((req: TRequest) => RefFrameEffect); + +/** + * Daemon route + request-policy traits for one command. Generic over the request + * the closure traits read so core can declare the shape in terms of + * `DispatchedCommand` without importing the server-private `DaemonRequest`. + */ +export type DaemonCommandDescriptor = { + command: string; + route: DaemonCommandRoute; + sessionKind?: SessionCommandKind; + refFrameEffect?: DaemonRefFrameEffect; + leaseAdmissionExempt?: boolean; + sessionExecutionLockExempt?: boolean; + selectorValidationExempt?: boolean; + replayScopedAction?: boolean; + allowInvalidRecording?: boolean; + /** + * #1478: this command's REQUEST may carry `flags.saveScript` to arm session + * script publication. Only the released flag owners (`open`, `close`, + * `replay` — the commands whose CLI grammar declares `--save-script`) set + * this; every other command's raw request is rejected at the daemon request + * seam by `unsupportedSaveScriptFlagResponse`, so a recordable command such + * as `record` or `trace` cannot arm publication over the wire. + */ + saveScriptFlagOwner?: boolean; + lockPolicySelectorOverride?: boolean; + androidBlockingDialogGuard?: boolean; + preferExplicitDeviceOverExistingSession?: boolean; + allowSessionlessDefaultDevice?: (req: TRequest) => boolean; + skipSessionlessProviderDevice?: (req: TRequest) => boolean; + /** + * #2016: this request shape is eligible for the sessionless, + * no-lease-anywhere lease-admission bypass — a session that was never + * created (deferred `connect`, `open` never ran) has no lease to admit or + * release. Only `close` declares it, and only for the plain-close shape + * (no app-target positional): `close ` resolves its device straight + * from flags when there's no session, so it must stay behind full + * lease/tenant admission. Declared here so `request-admission.ts` asks the + * registry instead of reclassifying `req.command`/`req.positionals` itself. + */ + sessionlessPlainCloseAdmissionExempt?: (req: TRequest) => boolean; +}; diff --git a/src/core/command-descriptor/derive.ts b/src/core/command-descriptor/derive.ts index 32a3c9618a..695f6fe93c 100644 --- a/src/core/command-descriptor/derive.ts +++ b/src/core/command-descriptor/derive.ts @@ -1,4 +1,4 @@ -import type { DaemonCommandDescriptor } from '../../daemon/daemon-command-registry.ts'; +import type { DaemonCommandDescriptor } from './daemon-command-descriptor.ts'; import type { CommandDescriptor } from './types.ts'; /** diff --git a/src/core/command-descriptor/types.ts b/src/core/command-descriptor/types.ts index 124dfdf251..9424d9e9c9 100644 --- a/src/core/command-descriptor/types.ts +++ b/src/core/command-descriptor/types.ts @@ -1,4 +1,4 @@ -import type { DaemonCommandDescriptor } from '../../daemon/daemon-command-registry.ts'; +import type { DaemonCommandDescriptor } from './daemon-command-descriptor.ts'; // The typed-flags request from contracts/, not the daemon's server-side refinement: these // descriptors read `command`, `positionals` and `flags` and never touch `internal`. import type { DispatchedCommand } from '@agent-device/contracts/command'; diff --git a/src/core/dispatch-resolve.ts b/src/core/dispatch-resolve.ts index f0d8553a4d..56778ad60c 100644 --- a/src/core/dispatch-resolve.ts +++ b/src/core/dispatch-resolve.ts @@ -18,7 +18,7 @@ import { listLocalDeviceInventory, readDeviceInventory, shouldPropagateDeviceInventoryProbeError, -} from '../request/device-inventory-context.ts'; +} from '@agent-device/capture-kit/device-inventory-context'; import type { DeviceSelectionResult, InventoryDeviceSelectionParams, diff --git a/src/core/interaction-targeting.ts b/src/core/interaction-targeting.ts index 2ebcb4853a..fe0d80cce7 100644 --- a/src/core/interaction-targeting.ts +++ b/src/core/interaction-targeting.ts @@ -13,7 +13,7 @@ import { normalizeRect, resolveRectCenter, } from '@agent-device/kernel/rect-center'; -import { intersectArea } from '../utils/screenshot-geometry.ts'; +import { intersectArea } from '@agent-device/kernel/screenshot-geometry'; import { isSemanticTouchTarget } from './touch-semantics.ts'; type ActionableTouchResolutionReason = diff --git a/src/core/press-retarget.ts b/src/core/press-retarget.ts index 29c145e435..54947867f3 100644 --- a/src/core/press-retarget.ts +++ b/src/core/press-retarget.ts @@ -15,8 +15,11 @@ import type { SnapshotNode } from '@agent-device/kernel/snapshot'; import { containsPoint } from '@agent-device/kernel/rect'; import { resolveRectCenter } from '@agent-device/kernel/rect-center'; -import { demoteNonUniqueLocalIdentity, readNodeLocalIdentity } from '@agent-device/ad-script'; -import { buildIndexMap } from '../replay/target-evidence-tree.ts'; +import { + buildIndexMap, + demoteNonUniqueLocalIdentity, + readNodeLocalIdentity, +} from '@agent-device/ad-script'; import { normalizeSelectorText } from '@agent-device/selectors'; import { isSemanticTouchTarget } from './touch-semantics.ts'; diff --git a/src/daemon/__tests__/replay-device-selection.test.ts b/src/daemon/__tests__/replay-device-selection.test.ts index f06b2ce433..0a2914a729 100644 --- a/src/daemon/__tests__/replay-device-selection.test.ts +++ b/src/daemon/__tests__/replay-device-selection.test.ts @@ -1,7 +1,7 @@ import { test, expect } from 'vitest'; import fs from 'node:fs'; import path from 'node:path'; -import { parseReplayInput } from '../../compat/replay-input.ts'; +import { parseReplayInput } from '@agent-device/ad-script'; import { buildReplayScriptPlatformFlags, buildReplayTargetDeviceResolution, diff --git a/src/daemon/__tests__/snapshot-quality-latch.test.ts b/src/daemon/__tests__/snapshot-quality-latch.test.ts index bdb7a81049..5244f2e8d8 100644 --- a/src/daemon/__tests__/snapshot-quality-latch.test.ts +++ b/src/daemon/__tests__/snapshot-quality-latch.test.ts @@ -6,7 +6,7 @@ import { makeIosSession } from '../../__tests__/test-utils/session-factories.ts' import { recoveredSnapshotQualityWarning, renderSnapshotQualityWarnings, -} from '../../snapshot-quality/warnings.ts'; +} from '@agent-device/contracts/snapshot-quality-warnings'; import { applyRecoveredWarningLatch, resolveRecoveredWarningLatch, diff --git a/src/daemon/daemon-command-registry.ts b/src/daemon/daemon-command-registry.ts index 0229520bbb..5447c52d2f 100644 --- a/src/daemon/daemon-command-registry.ts +++ b/src/daemon/daemon-command-registry.ts @@ -1,61 +1,14 @@ +import { + type DaemonCommandDescriptor, + type DaemonCommandRoute, + type SessionCommandKind, +} from '../core/command-descriptor/daemon-command-descriptor.ts'; import { deriveDaemonCommandDescriptors } from '../core/command-descriptor/derive.ts'; import { commandDescriptors } from '../core/command-descriptor/registry.ts'; -import type { DaemonCommandRoute } from './request-handler-chain.ts'; -import type { DaemonRequest } from './types.ts'; - -export type { DaemonCommandRoute } from './request-handler-chain.ts'; - -export type SessionCommandKind = 'inventory' | 'state' | 'observability' | 'publication' | 'replay'; - -// Declared in contracts/ so core/ can classify commands without importing the daemon; -// re-exported here because the descriptor shape below is stated in terms of it. import type { RefFrameEffect } from '@agent-device/contracts/replay'; +import type { DaemonRequest } from './types.ts'; -/** - * Request-sensitive form of {@link RefFrameEffect}. Commands whose subactions - * differ (keyboard `status` vs `dismiss`, alert `get`/`wait` vs - * `accept`/`dismiss`) use the resolver form instead of pretending all - * subcommands behave alike. Mirrors the existing `(req) => boolean` closure - * traits below. - */ -export type DaemonRefFrameEffect = RefFrameEffect | ((req: DaemonRequest) => RefFrameEffect); - -export type DaemonCommandDescriptor = { - command: string; - route: DaemonCommandRoute; - sessionKind?: SessionCommandKind; - refFrameEffect?: DaemonRefFrameEffect; - leaseAdmissionExempt?: boolean; - sessionExecutionLockExempt?: boolean; - selectorValidationExempt?: boolean; - replayScopedAction?: boolean; - allowInvalidRecording?: boolean; - /** - * #1478: this command's REQUEST may carry `flags.saveScript` to arm session - * script publication. Only the released flag owners (`open`, `close`, - * `replay` — the commands whose CLI grammar declares `--save-script`) set - * this; every other command's raw request is rejected at the daemon request - * seam by `unsupportedSaveScriptFlagResponse`, so a recordable command such - * as `record` or `trace` cannot arm publication over the wire. - */ - saveScriptFlagOwner?: boolean; - lockPolicySelectorOverride?: boolean; - androidBlockingDialogGuard?: boolean; - preferExplicitDeviceOverExistingSession?: boolean; - allowSessionlessDefaultDevice?: (req: DaemonRequest) => boolean; - skipSessionlessProviderDevice?: (req: DaemonRequest) => boolean; - /** - * #2016: this request shape is eligible for the sessionless, - * no-lease-anywhere lease-admission bypass — a session that was never - * created (deferred `connect`, `open` never ran) has no lease to admit or - * release. Only `close` declares it, and only for the plain-close shape - * (no app-target positional): `close ` resolves its device straight - * from flags when there's no session, so it must stay behind full - * lease/tenant admission. Declared here so `request-admission.ts` asks the - * registry instead of reclassifying `req.command`/`req.positionals` itself. - */ - sessionlessPlainCloseAdmissionExempt?: (req: DaemonRequest) => boolean; -}; +export type { DaemonCommandDescriptor, DaemonCommandRoute, SessionCommandKind }; export type DaemonProviderDeviceResolutionIntent = | 'existing-session' @@ -67,9 +20,7 @@ export type DaemonProviderDeviceResolutionIntent = // The hand-authored literal that previously lived here was proven byte-equal to // this derived value by `src/core/command-descriptor/__tests__/parity.test.ts` (#906) // and has been deleted; the daemon now derives its routes/traits from the single -// source. The back-edge from derive.ts/registry.ts to this module's -// `DaemonCommandDescriptor` is type-only (erased at runtime), so there is no -// runtime import cycle. +// source. The descriptor shape lives in core so that zone does not import daemon. export const DAEMON_COMMAND_DESCRIPTORS: readonly DaemonCommandDescriptor[] = deriveDaemonCommandDescriptors(commandDescriptors); diff --git a/src/daemon/handlers/__tests__/session-inventory-appleos.test.ts b/src/daemon/handlers/__tests__/session-inventory-appleos.test.ts index 42b4d5425c..c6c18e6742 100644 --- a/src/daemon/handlers/__tests__/session-inventory-appleos.test.ts +++ b/src/daemon/handlers/__tests__/session-inventory-appleos.test.ts @@ -3,14 +3,14 @@ import { test, expect, vi, beforeEach } from 'vitest'; // The `devices` handler resolves its inventory through listDeviceInventory; mocking it // lets us drive the additive `appleOs` projection off the shared device fixtures without // touching real local discovery. -vi.mock('../../../request/device-inventory-context.ts', async (importOriginal) => { +vi.mock('@agent-device/capture-kit/device-inventory-context', async (importOriginal) => { const actual = - await importOriginal(); + await importOriginal(); return { ...actual, listDeviceInventory: vi.fn(async () => []) }; }); import { handleSessionInventoryCommands } from '../session-inventory.ts'; -import { listDeviceInventory } from '../../../request/device-inventory-context.ts'; +import { listDeviceInventory } from '@agent-device/capture-kit/device-inventory-context'; import { makeSessionStore } from '../../../__tests__/test-utils/store-factory.ts'; import type { DaemonRequest, DaemonResponse } from '../../types.ts'; import type { AppleOS, DeviceInfo } from '@agent-device/kernel/device'; diff --git a/src/daemon/handlers/__tests__/session-replay-runtime.test.ts b/src/daemon/handlers/__tests__/session-replay-runtime.test.ts index 7cdb90872d..3e97e0a20d 100644 --- a/src/daemon/handlers/__tests__/session-replay-runtime.test.ts +++ b/src/daemon/handlers/__tests__/session-replay-runtime.test.ts @@ -447,7 +447,7 @@ test('replay rejects legacy JSON payload files', async () => { // #1555 P1: the P5 extraction moved `.ad` inspection to `inspectAdReplay` // (`packages/ad-replay/src/internal/inspect.ts`), which never receives // `req.flags` — so the `parseReplayInput` check that used to reject an -// unrecognized `--replay-backend` value (`src/compat/replay-input.ts`) no +// unrecognized `--replay-backend` value (`parseReplayInput`) no // longer ran on this path. `buildReplayTargetDeviceResolution` // (`src/daemon/replay-device-selection.ts`) still calls `parseReplayInput` // for advisory device-lock binding, but its `catch` deliberately swallows @@ -474,8 +474,7 @@ test('replay rejects an unknown --replay-backend value before any step dispatch expect(response.ok).toBe(false); if (response.ok) return; expect(response.error.code).toBe('INVALID_ARGS'); - // Byte-identical to `parseReplayInput`'s message on main - // (`src/compat/replay-input.ts`), so the CLI/client-facing text is unchanged. + // Byte-identical to `parseReplayInput`'s message, so the CLI/client-facing text is unchanged. expect(response.error.message).toBe('Unsupported replay backend "unknown".'); expect(invoke).not.toHaveBeenCalled(); }); diff --git a/src/daemon/handlers/interaction-common.ts b/src/daemon/handlers/interaction-common.ts index e4442648a5..28a1849249 100644 --- a/src/daemon/handlers/interaction-common.ts +++ b/src/daemon/handlers/interaction-common.ts @@ -9,7 +9,7 @@ import { stripInternalInteractionFlags } from '../interaction-outcome-policy.ts' import { computeTargetEvidence, type RecordedTargetCapture } from '../session-target-evidence.ts'; import type { MultiTargetAnnotationV1 } from '@agent-device/contracts/replay'; import { inferFillText } from '../action-utils.ts'; -import { recordedInputPlaceholder } from '../../replay/recorded-input.ts'; +import { recordedInputPlaceholder } from '@agent-device/ad-script'; import { parameterizeRecordedFillPayload } from '../parameterized-recorded-fill.ts'; import { isSessionRecording } from '../session-script-publication-capability.ts'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; diff --git a/src/daemon/handlers/interaction-recorded-input.ts b/src/daemon/handlers/interaction-recorded-input.ts index 37ecbcd84b..40aecf6671 100644 --- a/src/daemon/handlers/interaction-recorded-input.ts +++ b/src/daemon/handlers/interaction-recorded-input.ts @@ -1,6 +1,6 @@ import type { CommandFlags } from '@agent-device/contracts/command'; import { AppError } from '@agent-device/kernel/errors'; -import { validateRecordedInputVariableName } from '../../replay/recorded-input.ts'; +import { validateRecordedInputVariableName } from '@agent-device/ad-script'; import type { SessionState } from '../types.ts'; import { isSessionRecording } from '../session-script-publication-capability.ts'; diff --git a/src/daemon/handlers/interaction-touch-runtime.ts b/src/daemon/handlers/interaction-touch-runtime.ts index 95ccfc19b6..24ebfd7408 100644 --- a/src/daemon/handlers/interaction-touch-runtime.ts +++ b/src/daemon/handlers/interaction-touch-runtime.ts @@ -7,7 +7,7 @@ import type { } from '@agent-device/contracts/interaction'; import type { GestureReferenceFrame } from '@agent-device/contracts/scroll-gesture'; import { asAppError, normalizeError } from '@agent-device/kernel/errors'; -import { readResolvedInteractionTarget } from '../../contracts/interaction-outcome.ts'; +import { readResolvedInteractionTarget } from '@agent-device/contracts/interaction-outcome'; import { isSessionRecording } from '../session-script-publication-capability.ts'; import type { DaemonResponse, SessionState } from '../types.ts'; import { publishInteractionAmbiguityCandidates } from './interaction-ambiguity-publication.ts'; diff --git a/src/daemon/handlers/session-doctor-device.ts b/src/daemon/handlers/session-doctor-device.ts index 9961051b97..0ae6924c93 100644 --- a/src/daemon/handlers/session-doctor-device.ts +++ b/src/daemon/handlers/session-doctor-device.ts @@ -1,5 +1,5 @@ import { buildDeviceInventoryRequestFromFlags } from '../../core/dispatch-resolve.ts'; -import { listDeviceInventory } from '../../request/device-inventory-context.ts'; +import { listDeviceInventory } from '@agent-device/capture-kit/device-inventory-context'; import { countDeviceInventoryByGroup, LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS, diff --git a/src/daemon/handlers/session-doctor.ts b/src/daemon/handlers/session-doctor.ts index f14019e5af..ec560bf63f 100644 --- a/src/daemon/handlers/session-doctor.ts +++ b/src/daemon/handlers/session-doctor.ts @@ -7,7 +7,7 @@ import { isActiveProviderDevice } from '../../provider-device-runtime.ts'; import { listLocalDeviceInventory, shouldPropagateDeviceInventoryProbeError, -} from '../../request/device-inventory-context.ts'; +} from '@agent-device/capture-kit/device-inventory-context'; import { readVersion } from '@agent-device/host-kit/version'; import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts'; import { SessionStore } from '../session-store.ts'; diff --git a/src/daemon/handlers/session-inventory.ts b/src/daemon/handlers/session-inventory.ts index ab14ffea8e..7e42115fe8 100644 --- a/src/daemon/handlers/session-inventory.ts +++ b/src/daemon/handlers/session-inventory.ts @@ -2,7 +2,7 @@ import { commandRuntimeUseRequirements, listRuntimeFactCommands, } from '../../core/command-descriptor/registry.ts'; -import { listDeviceInventory } from '../../request/device-inventory-context.ts'; +import { listDeviceInventory } from '@agent-device/capture-kit/device-inventory-context'; import { assertResolvedAppsFilter } from '@agent-device/contracts/device'; import { AppError, asAppError } from '@agent-device/kernel/errors'; import { diff --git a/src/daemon/handlers/session-replay-action-runtime.ts b/src/daemon/handlers/session-replay-action-runtime.ts index 2a08f2700a..c8357f845e 100644 --- a/src/daemon/handlers/session-replay-action-runtime.ts +++ b/src/daemon/handlers/session-replay-action-runtime.ts @@ -10,7 +10,7 @@ import { import { buildDisplayPositionals } from '../session-event-action.ts'; import { appendReplayTraceEvent } from './session-replay-trace.ts'; import { inferFillText } from '../action-utils.ts'; -import { readRecordedInputVariableName } from '../../replay/recorded-input.ts'; +import { readRecordedInputVariableName } from '@agent-device/ad-script'; import { resolveImplicitSessionScope } from '../session-routing.ts'; type ReplayBaseRequest = Omit; diff --git a/src/daemon/handlers/session-replay-repair-hint.ts b/src/daemon/handlers/session-replay-repair-hint.ts index b0a2da94bd..bf818efa0e 100644 --- a/src/daemon/handlers/session-replay-repair-hint.ts +++ b/src/daemon/handlers/session-replay-repair-hint.ts @@ -23,9 +23,8 @@ import type { SnapshotNode } from '@agent-device/kernel/snapshot'; import type { ReplayDivergenceKind, ReplayRepairHint } from '@agent-device/contracts/divergence'; -import { matchesAncestryPrefix } from '@agent-device/ad-script'; +import { matchesAncestryPrefix, buildAncestryChain, buildIndexMap } from '@agent-device/ad-script'; import type { TargetAnnotationV1, TargetScrollRegion } from '@agent-device/contracts/replay'; -import { buildAncestryChain, buildIndexMap } from '../../replay/target-evidence-tree.ts'; import { computeScrollRegionKey, scrollRegionKeysEqual } from '../session-target-evidence.ts'; export type ReplayRepairHintCapture = diff --git a/src/daemon/handlers/session-replay-runtime-plan.ts b/src/daemon/handlers/session-replay-runtime-plan.ts index da8fe8314e..7365071dd8 100644 --- a/src/daemon/handlers/session-replay-runtime-plan.ts +++ b/src/daemon/handlers/session-replay-runtime-plan.ts @@ -15,9 +15,9 @@ import { parseReplayCliEnvEntries, readReplayCliEnvEntries, readReplayShellEnvSource, + resolveReplayFormat, type ReplayScriptMetadata, } from '@agent-device/ad-script'; -import { resolveReplayFormat } from '../../replay/format.ts'; import { buildReplayBuiltinVars } from './session-replay-vars.ts'; import { runTypedMaestroReplay } from './session-replay-maestro-runtime.ts'; import type { ReplayTestAttemptStepSink } from '@agent-device/replay-test'; @@ -154,7 +154,7 @@ export function prepareReplayPlan(params: { * value. Extraction moved `.ad` inspection to `inspectAdReplay`, which never * receives flags — restoring the check here (the one caller of * `inspectAdReplay` that reaches this point with a non-Maestro request) - * matches `src/compat/replay-input.ts`'s `parseReplayInput` exactly, byte for + * matches `parseReplayInput` exactly, byte for * byte, before any plan/session work begins. `replayBackend: 'maestro'` still * passes here because `runReplayScriptSource` has already routed a real * Maestro-format request to `runTypedMaestroReplay` above; only a diff --git a/src/daemon/handlers/session-replay-runtime.ts b/src/daemon/handlers/session-replay-runtime.ts index 2aec107346..dd7855e515 100644 --- a/src/daemon/handlers/session-replay-runtime.ts +++ b/src/daemon/handlers/session-replay-runtime.ts @@ -6,7 +6,7 @@ import { runAdReplay } from '@agent-device/ad-replay'; import type { SnapshotTimingSample } from '@agent-device/contracts/capture'; import { summarizeSnapshotTimingSamples } from '@agent-device/contracts/capture'; import type { ReplayCommandResult } from '@agent-device/contracts/replay'; -import { isMaestroYamlPath, maestroBackendRequiredMessage } from '../../replay/format.ts'; +import { isMaestroYamlPath, maestroBackendRequiredMessage } from '@agent-device/ad-script'; import { getRequestSignal } from '@agent-device/host-kit/request'; import { createReplayCoordinator, type ReplayCoordinator } from '../session-replay-coordinator.ts'; import { diff --git a/src/daemon/handlers/session-replay-target-classification.ts b/src/daemon/handlers/session-replay-target-classification.ts index 868bb66488..120c5c5062 100644 --- a/src/daemon/handlers/session-replay-target-classification.ts +++ b/src/daemon/handlers/session-replay-target-classification.ts @@ -34,10 +34,14 @@ import type { Platform, PublicPlatform } from '@agent-device/kernel/device'; import { findNodeByRef, normalizeRef, type SnapshotNode } from '@agent-device/kernel/snapshot'; import { findNodeByLabel } from '../../core/snapshot-node-lookup.ts'; import { + annotationLocalIdentity, buildAncestryChain, buildIndexMap, + classifyTargetBindingMatch, filterIdentitySet, -} from '../../replay/target-evidence-tree.ts'; + firstAncestryMismatch, + identityFieldMismatches, +} from '@agent-device/ad-script'; import { boundedLocalIdentity, computeSiblingOrdinal, @@ -46,12 +50,6 @@ import { orderByViewportPosition, } from '../session-target-evidence.ts'; import { resolveRecordedTarget } from '@agent-device/selectors'; -import { - annotationLocalIdentity, - classifyTargetBindingMatch, - firstAncestryMismatch, - identityFieldMismatches, -} from '@agent-device/ad-script'; import type { TargetAnnotationV1 } from '@agent-device/contracts/replay'; import type { ReplayDivergenceTargetBindingKind } from '@agent-device/contracts/divergence'; diff --git a/src/daemon/handlers/session-test-shard-devices.ts b/src/daemon/handlers/session-test-shard-devices.ts index 03ef5adc3d..de65dbd9fb 100644 --- a/src/daemon/handlers/session-test-shard-devices.ts +++ b/src/daemon/handlers/session-test-shard-devices.ts @@ -1,6 +1,6 @@ import type { CommandFlags } from '@agent-device/contracts/command'; import type { DeviceInventoryRequest } from '@agent-device/contracts/device'; -import { listDeviceInventory } from '../../request/device-inventory-context.ts'; +import { listDeviceInventory } from '@agent-device/capture-kit/device-inventory-context'; import { resolveAndroidSerialAllowlist, resolveIosSimulatorDeviceSetPath, diff --git a/src/daemon/handlers/session-test-source-discovery.ts b/src/daemon/handlers/session-test-source-discovery.ts index f6c243ad7e..f9bda7aa13 100644 --- a/src/daemon/handlers/session-test-source-discovery.ts +++ b/src/daemon/handlers/session-test-source-discovery.ts @@ -1,7 +1,6 @@ import { inspectMaestroFlow } from '@agent-device/maestro'; import type { ReplayScriptSourceBundle } from '@agent-device/contracts/replay'; -import { resolveReplayFormat } from '../../replay/format.ts'; -import { readReplayScriptMetadata } from '@agent-device/ad-script'; +import { readReplayScriptMetadata, resolveReplayFormat } from '@agent-device/ad-script'; import { readReplayScriptSourceFile } from '../../replay/script-source-bundle.ts'; import type { ReplayTestDiscoverSources, diff --git a/src/daemon/ios-app-session-hint.test.ts b/src/daemon/ios-app-session-hint.test.ts index 9bf11e352a..bb5b18bd1c 100644 --- a/src/daemon/ios-app-session-hint.test.ts +++ b/src/daemon/ios-app-session-hint.test.ts @@ -4,8 +4,8 @@ import { beforeEach, expect, test, vi } from 'vitest'; const listBootedIosSimulators = vi.hoisted(() => vi.fn()); const detectSoleRunningIosSimulatorApp = vi.hoisted(() => vi.fn()); -vi.mock('../request/device-inventory-context.ts', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('@agent-device/capture-kit/device-inventory-context', async (importOriginal) => ({ + ...(await importOriginal()), listLocalDeviceInventory: listBootedIosSimulators, })); vi.mock('../platforms/apple/core/app-resolution.ts', () => ({ detectSoleRunningIosSimulatorApp })); diff --git a/src/daemon/parameterized-recorded-fill.ts b/src/daemon/parameterized-recorded-fill.ts index 150a18c7a5..9033c9b54c 100644 --- a/src/daemon/parameterized-recorded-fill.ts +++ b/src/daemon/parameterized-recorded-fill.ts @@ -382,7 +382,7 @@ function parameterizeSensitiveString(value: string, literal: string, placeholder /** * Mirrors the `${VAR}` grammar `@agent-device/ad-script`'s `REPLAY_VAR_KEY_RE` - * and `src/replay/recorded-input.ts`'s `RECORDED_INPUT_PLACEHOLDER_RE` define. + * and recorded-input placeholder define. * The sticky (`y`) flag makes `exec` match only starting exactly at * `lastIndex` (never scanning ahead to find a later match), so the scan below * can test one position at a time without slicing a fresh substring per diff --git a/src/daemon/replay-device-selection.ts b/src/daemon/replay-device-selection.ts index 185f46936b..8f34d39000 100644 --- a/src/daemon/replay-device-selection.ts +++ b/src/daemon/replay-device-selection.ts @@ -1,10 +1,12 @@ import type { SessionAction } from '@agent-device/contracts/session'; import { inspectMaestroFlow } from '@agent-device/maestro'; -import { resolveDeclaredScriptPlatform } from '@agent-device/ad-script'; -import { parseReplayInput } from '../compat/replay-input.ts'; +import { + parseReplayInput, + resolveDeclaredScriptPlatform, + resolveReplayFormat, +} from '@agent-device/ad-script'; import type { ResolveTargetDeviceOptions } from '../core/dispatch-resolve.ts'; import { isDeepLinkTarget, type CommandFlags } from '@agent-device/contracts/command'; -import { resolveReplayFormat } from '../replay/format.ts'; import { readReplayScriptSourceFile } from '../replay/script-source-bundle.ts'; import { appleSimulatorAppTargetForOpenTarget } from './open-device-selection.ts'; import type { DaemonRequest } from './types.ts'; diff --git a/src/daemon/request-handler-chain.ts b/src/daemon/request-handler-chain.ts index fff4e4211a..3cb39940a6 100644 --- a/src/daemon/request-handler-chain.ts +++ b/src/daemon/request-handler-chain.ts @@ -1,6 +1,7 @@ import type { CommandFlags } from '@agent-device/contracts/command'; import type { CloudArtifactProvider } from '@agent-device/contracts/observability'; import { AppError } from '@agent-device/kernel/errors'; +import type { DaemonCommandRoute } from '../core/command-descriptor/daemon-command-descriptor.ts'; import { getDaemonCommandRoute } from './daemon-command-registry.ts'; import * as genericRequestHandlerModule from './request-generic-dispatch.ts'; import type { DaemonCommandContext } from './context.ts'; @@ -97,9 +98,15 @@ const DAEMON_ROUTE_HANDLERS = { load: async () => genericRequestHandlerModule, run: async () => null, }), -} as const; +} as const satisfies Record< + DaemonCommandRoute, + { + loadModule: () => Promise; + run: (params: RequestHandlerChainParams) => Promise; + } +>; -export type DaemonCommandRoute = keyof typeof DAEMON_ROUTE_HANDLERS; +export type { DaemonCommandRoute }; export async function runRequestHandlerChain( params: RequestHandlerChainParams, diff --git a/src/daemon/request-router.ts b/src/daemon/request-router.ts index 4228a4b80e..423a835ece 100644 --- a/src/daemon/request-router.ts +++ b/src/daemon/request-router.ts @@ -1,5 +1,5 @@ import { withResolveTargetDeviceCacheScope } from '../core/dispatch-resolve.ts'; -import { withDeviceInventoryContext } from '../request/device-inventory-context.ts'; +import { withDeviceInventoryContext } from '@agent-device/capture-kit/device-inventory-context'; import type { LeaseLifecycleProvider } from '@agent-device/contracts/device'; import type { ComposedDeviceInventoryGateways } from '@agent-device/contracts/platform-module'; import type { DeviceRuntimeGateway } from '@agent-device/contracts/platform-runtime'; diff --git a/src/daemon/session-action-recorder.ts b/src/daemon/session-action-recorder.ts index 1627815a33..e437bbc552 100644 --- a/src/daemon/session-action-recorder.ts +++ b/src/daemon/session-action-recorder.ts @@ -10,7 +10,7 @@ import { inferFillText } from './action-utils.ts'; import { recordedInputPlaceholder, validateRecordedInputVariableName, -} from '../replay/recorded-input.ts'; +} from '@agent-device/ad-script'; import { parameterizeRecordedFillPayload, parameterizeRecordedFillTargetEvidence, diff --git a/src/daemon/session-target-evidence.ts b/src/daemon/session-target-evidence.ts index d7f1b5b638..7a3173de4c 100644 --- a/src/daemon/session-target-evidence.ts +++ b/src/daemon/session-target-evidence.ts @@ -24,8 +24,11 @@ import type { SnapshotNode } from '@agent-device/kernel/snapshot'; import { resolveRectCenter } from '@agent-device/kernel/rect-center'; import { findNearestScrollableContainer } from '../snapshot/snapshot-presentation/tree.ts'; import { + buildAncestryChain, + buildIndexMap, classifyTargetBindingMatch, demoteNonUniqueLocalIdentity, + filterIdentitySet, matchesLocalIdentity, readNodeLocalIdentity, serializeTargetAnnotationV1, @@ -35,11 +38,6 @@ import { TARGET_ANNOTATION_MAX_ANCESTRY, TARGET_ANNOTATION_MAX_PAYLOAD_BYTES, } from '@agent-device/ad-script'; -import { - buildAncestryChain, - buildIndexMap, - filterIdentitySet, -} from '../replay/target-evidence-tree.ts'; import type { TargetAncestryEntry, TargetAnnotationV1, diff --git a/src/daemon/snapshot-quality-latch.ts b/src/daemon/snapshot-quality-latch.ts index 5ea8150eef..7aee38c304 100644 --- a/src/daemon/snapshot-quality-latch.ts +++ b/src/daemon/snapshot-quality-latch.ts @@ -1,5 +1,5 @@ import type { SnapshotQualityVerdict } from '@agent-device/kernel/snapshot'; -import { recoveredSnapshotQualityWarning } from '../snapshot-quality/warnings.ts'; +import { recoveredSnapshotQualityWarning } from '@agent-device/contracts/snapshot-quality-warnings'; import type { DaemonResponseData, SessionState } from './types.ts'; type RecoveredWarningLatch = NonNullable; diff --git a/src/platform-runtime-android-emulator-host.ts b/src/platform-runtime-android-emulator-host.ts index 5670e44190..da016f1a2f 100644 --- a/src/platform-runtime-android-emulator-host.ts +++ b/src/platform-runtime-android-emulator-host.ts @@ -1,5 +1,5 @@ import type { DeviceReadinessRuntimeHost } from '@agent-device/contracts/device-readiness-runtime'; -import { listLocalDeviceInventory } from './request/device-inventory-context.ts'; +import { listLocalDeviceInventory } from '@agent-device/capture-kit/device-inventory-context'; import { runCmdDetached } from '@agent-device/host-kit/command'; import { stopPidsWithEscalation } from '@agent-device/host-kit/process'; diff --git a/src/platform-runtime-open-target.ts b/src/platform-runtime-open-target.ts index 9293937406..38e8aa2505 100644 --- a/src/platform-runtime-open-target.ts +++ b/src/platform-runtime-open-target.ts @@ -22,7 +22,7 @@ export async function resolveSoleForegroundIosApp( options: Readonly<{ simulatorSetPath?: string }> = {}, ): Promise { const { listLocalDeviceInventory, shouldPropagateDeviceInventoryProbeError } = - await import('./request/device-inventory-context.ts'); + await import('@agent-device/capture-kit/device-inventory-context'); try { const booted = await listLocalDeviceInventory({ platform: 'ios', diff --git a/src/platforms/vega/__tests__/doctor.test.ts b/src/platforms/vega/__tests__/doctor.test.ts index 68d2905ce8..559df83ddc 100644 --- a/src/platforms/vega/__tests__/doctor.test.ts +++ b/src/platforms/vega/__tests__/doctor.test.ts @@ -4,7 +4,7 @@ import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; import type { DoctorCheck } from '@agent-device/contracts/observability'; import type { HostDiagnosticsContext } from '@agent-device/contracts/host-diagnostics'; -import { shouldPropagateDeviceInventoryProbeError } from '../../../request/device-inventory-context.ts'; +import { shouldPropagateDeviceInventoryProbeError } from '@agent-device/capture-kit/device-inventory-context'; import type { VegaToolProvider } from '../tool-provider.ts'; import { withVegaToolProvider } from '../tool-provider.ts'; import { vegaToolchainCheck } from '../doctor.ts'; diff --git a/src/replay/script-source-bundle.ts b/src/replay/script-source-bundle.ts index ee42516ba1..10faf6c113 100644 --- a/src/replay/script-source-bundle.ts +++ b/src/replay/script-source-bundle.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import { AppError } from '@agent-device/kernel/errors'; import type { ReplayScriptSourceBundle } from '@agent-device/contracts/replay'; import { resolveUserPath } from '@agent-device/host-kit/file'; -import { resolveReplayFormat } from './format.ts'; +import { resolveReplayFormat } from '@agent-device/ad-script'; /** * The caller-side half of the replay script source bundle (#1802): the CLI, diff --git a/src/replay/source-discovery.ts b/src/replay/source-discovery.ts index 2fdbd56d0b..43e0768c2f 100644 --- a/src/replay/source-discovery.ts +++ b/src/replay/source-discovery.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { AppError } from '@agent-device/kernel/errors'; import { resolveUserPath } from '@agent-device/host-kit/file'; -import { isMaestroYamlPath, maestroBackendRequiredMessage } from './format.ts'; +import { isMaestroYamlPath, maestroBackendRequiredMessage } from '@agent-device/ad-script'; const GLOB_PATTERN_CHARS = /[*?[\]{}]/; type ReplayInputSource = 'directory' | 'file' | 'glob'; diff --git a/src/screenshot-diff/screenshot-diff-overlay-matches.ts b/src/screenshot-diff/screenshot-diff-overlay-matches.ts index 735204d131..afbc8ee5b9 100644 --- a/src/screenshot-diff/screenshot-diff-overlay-matches.ts +++ b/src/screenshot-diff/screenshot-diff-overlay-matches.ts @@ -4,7 +4,7 @@ import type { } from './screenshot-diff-regions.ts'; import type { ScreenshotOverlayRef } from '@agent-device/kernel/snapshot'; import { rectArea } from '@agent-device/kernel/rect'; -import { intersectArea } from '../utils/screenshot-geometry.ts'; +import { intersectArea } from '@agent-device/kernel/screenshot-geometry'; const MAX_MATCHES_PER_REGION = 3; diff --git a/src/screenshot-diff/screenshot-diff-regions.ts b/src/screenshot-diff/screenshot-diff-regions.ts index 39d5f87be8..952b2066e5 100644 --- a/src/screenshot-diff/screenshot-diff-regions.ts +++ b/src/screenshot-diff/screenshot-diff-regions.ts @@ -1,6 +1,6 @@ import type { Rect } from '@agent-device/kernel/snapshot'; import type { PNG } from '@agent-device/capture-kit/png'; -import { normalizedRect, type NormalizedRect } from '../utils/screenshot-geometry.ts'; +import { normalizedRect, type NormalizedRect } from '@agent-device/kernel/screenshot-geometry'; import { findConnectedMaskComponents } from './screenshot-diff-components.ts'; import { splitLargeDiffRegions } from './screenshot-diff-region-split.ts'; import type { MutableDiffRegion } from './screenshot-diff-region-types.ts'; diff --git a/src/screenshot-diff/screenshot-diff.ts b/src/screenshot-diff/screenshot-diff.ts index 2aaf0b02cc..a449935e98 100644 --- a/src/screenshot-diff/screenshot-diff.ts +++ b/src/screenshot-diff/screenshot-diff.ts @@ -10,7 +10,7 @@ import { } from '@agent-device/capture-kit/png-worker-client'; import { annotateDiffRegions } from './screenshot-diff-region-overlay.ts'; import { summarizeDiffRegions, type ScreenshotDiffRegion } from './screenshot-diff-regions.ts'; -import type { ImageDimensions } from '../utils/screenshot-geometry.ts'; +import type { ImageDimensions } from '@agent-device/kernel/screenshot-geometry'; export type ScreenshotDimensionMismatch = { expected: ImageDimensions; diff --git a/src/snapshot-quality/__tests__/warnings.test.ts b/src/snapshot-quality/__tests__/warnings.test.ts index 024586435a..925afee49f 100644 --- a/src/snapshot-quality/__tests__/warnings.test.ts +++ b/src/snapshot-quality/__tests__/warnings.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { test } from 'vitest'; import { readSnapshotQualityVerdict } from '@agent-device/capture-kit/snapshot-quality-verdict'; -import { renderSnapshotQualityWarnings } from '../warnings.ts'; +import { renderSnapshotQualityWarnings } from '@agent-device/contracts/snapshot-quality-warnings'; const sharedRecoveryReason = 'iOS XCTest snapshot failed while serializing the accessibility tree. Error kAXErrorIllegalArgument getting snapshot for element '; diff --git a/src/utils/__tests__/output.test.ts b/src/utils/__tests__/output.test.ts index 7815ac4b0d..a578d057e0 100644 --- a/src/utils/__tests__/output.test.ts +++ b/src/utils/__tests__/output.test.ts @@ -9,7 +9,7 @@ import { printHumanError, } from '../output.ts'; import { formatRole, formatSnapshotLine } from '../../snapshot/snapshot-lines.ts'; -import { normalizedRect } from '../screenshot-geometry.ts'; +import { normalizedRect } from '@agent-device/kernel/screenshot-geometry'; import { AppError } from '@agent-device/kernel/errors'; function captureStderr(run: () => void): string { diff --git a/test/replay-compat/corpus.test.ts b/test/replay-compat/corpus.test.ts index 5f38ca3ffe..d453abe596 100644 --- a/test/replay-compat/corpus.test.ts +++ b/test/replay-compat/corpus.test.ts @@ -3,7 +3,7 @@ import { createHash } from 'node:crypto'; import { readdirSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { parseReplayInput } from '../../src/compat/replay-input.ts'; +import { parseReplayInput } from '@agent-device/ad-script'; import { AppError } from '@agent-device/kernel/errors'; import { REPLAY_COMPAT_CORPUS, From 03e3538fe437e225a79f81ace35ebcb0219ce811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 27 Aug 2026 20:46:00 +0200 Subject: [PATCH 2/4] refactor: keep contracts and capture-kit off generic sinks Move interaction-outcome, snapshot warning rendering, and inventory ALS behind focused owners, and plant R18/R70 domain-shape gates so they cannot return as package export-map growth. Co-authored-by: Cursor --- docs/dependency-graph-findings.md | 6 +- packages/capture-kit/package.json | 4 - packages/contracts/package.json | 8 -- .../help-conformance-sample-producers.ts | 2 +- scripts/help-conformance-sample-outputs.mjs | 2 +- scripts/layering/check.ts | 6 + .../contracts-implementation-policy.test.ts | 17 +++ .../contracts-implementation-policy.ts | 18 +++ scripts/layering/daemon-modularity.ts | 2 - scripts/layering/package-boundaries.test.ts | 3 - .../layering/substrate-domain-shape.test.ts | 60 ++++++++++ scripts/layering/substrate-domain-shape.ts | 110 ++++++++++++++++++ src/__tests__/eager-closure-budgets.ts | 6 - .../test-utils/device-inventory-gateways.ts | 2 +- src/commands/capture/runtime/snapshot.ts | 2 +- .../interaction/runtime/interactions.ts | 2 +- src/core/dispatch-resolve.ts | 2 +- .../src => src/core}/interaction-outcome.ts | 0 .../__tests__/snapshot-quality-latch.test.ts | 2 +- src/daemon/daemon-command-registry.ts | 2 +- .../session-inventory-appleos.test.ts | 6 +- .../handlers/interaction-touch-runtime.ts | 2 +- src/daemon/handlers/session-doctor-device.ts | 2 +- src/daemon/handlers/session-doctor.ts | 2 +- src/daemon/handlers/session-inventory.ts | 2 +- .../handlers/session-test-shard-devices.ts | 2 +- src/daemon/ios-app-session-hint.test.ts | 4 +- src/daemon/request-router.ts | 2 +- src/daemon/snapshot-quality-latch.ts | 2 +- src/platform-runtime-android-emulator-host.ts | 2 +- src/platform-runtime-open-target.ts | 2 +- src/platforms/vega/__tests__/doctor.test.ts | 2 +- .../request}/device-inventory-context.ts | 0 .../__tests__/warnings.test.ts | 2 +- .../snapshot-presentation/quality-warnings.ts | 0 35 files changed, 236 insertions(+), 52 deletions(-) create mode 100644 scripts/layering/substrate-domain-shape.test.ts create mode 100644 scripts/layering/substrate-domain-shape.ts rename {packages/contracts/src => src/core}/interaction-outcome.ts (100%) rename {packages/capture-kit/src => src/request}/device-inventory-context.ts (100%) rename packages/contracts/src/snapshot-quality-warnings.ts => src/snapshot/snapshot-presentation/quality-warnings.ts (100%) diff --git a/docs/dependency-graph-findings.md b/docs/dependency-graph-findings.md index 51484ebbba..3ac54e2575 100644 --- a/docs/dependency-graph-findings.md +++ b/docs/dependency-graph-findings.md @@ -78,7 +78,7 @@ type-only inversions, R7 pins SessionState field ownership, and the shared selec ## 0. Where the inversions ended up (and why 5 is the floor for now) -61 → 7, then 5. The last pass moved four keystones, each of which was pinning a much larger set: +The last pass moved four keystones, each of which was pinning a much larger set: | Keystone moved to `contracts/` | Unblocked | |---|---| @@ -125,10 +125,6 @@ narrow name replaced both. from `daemon-command-registry.ts` to key an exhaustive owner-file map; that remaining inversion is the commands-zone consumer, not a second source of truth for the union. -The two `DaemonCommandDescriptor` inversions (`core/command-descriptor/derive.ts`, `.../types.ts`) -are gone: the shape is declared in core, generic over `DispatchedCommand`, so those files no longer -import the daemon. `daemon/types.ts` `DaemonRequest` stays server-private. - All remaining inversions are argued at `TYPE_INVERSION_BASELINE` in `scripts/layering/check.ts`, next to the numbers they explain. diff --git a/packages/capture-kit/package.json b/packages/capture-kit/package.json index a42b716ebe..063685d2c1 100644 --- a/packages/capture-kit/package.json +++ b/packages/capture-kit/package.json @@ -14,10 +14,6 @@ "types": "./src/index.ts", "default": "./src/index.ts" }, - "./device-inventory-context": { - "types": "./src/device-inventory-context.ts", - "default": "./src/device-inventory-context.ts" - }, "./mobile-snapshot-semantics": { "types": "./src/mobile-snapshot-semantics.ts", "default": "./src/mobile-snapshot-semantics.ts" diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 5101a12e7b..8513fa9e65 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -243,10 +243,6 @@ "types": "./src/interaction-guarantees.ts", "default": "./src/interaction-guarantees.ts" }, - "./interaction-outcome": { - "types": "./src/interaction-outcome.ts", - "default": "./src/interaction-outcome.ts" - }, "./interactor-operation-catalog": { "types": "./src/interactor-operation-catalog.ts", "default": "./src/interactor-operation-catalog.ts" @@ -427,10 +423,6 @@ "types": "./src/snapshot-presentation.ts", "default": "./src/snapshot-presentation.ts" }, - "./snapshot-quality-warnings": { - "types": "./src/snapshot-quality-warnings.ts", - "default": "./src/snapshot-quality-warnings.ts" - }, "./snapshot-runtime": { "types": "./src/snapshot-runtime.ts", "default": "./src/snapshot-runtime.ts" diff --git a/scripts/__tests__/help-conformance-sample-producers.ts b/scripts/__tests__/help-conformance-sample-producers.ts index 02de2d8fff..80c1c08b28 100644 --- a/scripts/__tests__/help-conformance-sample-producers.ts +++ b/scripts/__tests__/help-conformance-sample-producers.ts @@ -34,7 +34,7 @@ import type { ConnectVerification } from '../../src/cli/connection/connect-provi import type { RemoteConnectionState } from '../../src/remote/remote-connection-state.ts'; import { AppError, normalizeError } from '@agent-device/kernel/errors'; import type { SnapshotQualityVerdict } from '@agent-device/kernel/snapshot'; -import { renderSnapshotQualityWarnings } from '@agent-device/contracts/snapshot-quality-warnings'; +import { renderSnapshotQualityWarnings } from '../../src/snapshot/snapshot-presentation/quality-warnings.ts'; import { formatSnapshotText, printHumanError } from '../../src/utils/output.ts'; // The production renderer behind each captured sample in diff --git a/scripts/help-conformance-sample-outputs.mjs b/scripts/help-conformance-sample-outputs.mjs index bdcf583220..c098bdfd7b 100644 --- a/scripts/help-conformance-sample-outputs.mjs +++ b/scripts/help-conformance-sample-outputs.mjs @@ -72,7 +72,7 @@ hint: The UI kept changing for the whole settle budget (animation, carousel, or // Recovered snapshot: the private-ax fallback fired but still exposed // actionable refs. Warning wording is renderSnapshotQualityWarnings -// (packages/contracts/src/snapshot-quality-warnings.ts); lines are the structured snapshot +// (src/snapshot/snapshot-presentation/quality-warnings.ts); lines are the structured snapshot // renderer (src/utils/output.ts formatSnapshotText). export const PRIVATE_AX_RECOVERY_SAMPLE = { command: 'agent-device snapshot -i', diff --git a/scripts/layering/check.ts b/scripts/layering/check.ts index 5cacc3c703..920918ccc6 100644 --- a/scripts/layering/check.ts +++ b/scripts/layering/check.ts @@ -91,6 +91,7 @@ import { } from './platform-package-repository.ts'; import { policyLead, policyViolation, ZONE_POLICIES } from './zone-policy.ts'; import { contractsImplementationAuthorityViolations } from './contracts-implementation-policy.ts'; +import { substrateDomainShapeViolations } from './substrate-domain-shape.ts'; import { selectorPipelineOwnershipViolations } from './selector-pipeline-ownership.ts'; import { recordRuntimeRegistryJoinViolations } from './record-runtime-registry-policy.ts'; import { recordRuntimeDaemonMechanicsViolations } from './record-runtime-mechanics-policy.ts'; @@ -535,6 +536,7 @@ export const LAYERING_RULE_IDS = [ 'session-resource-ownership', 'application-lifecycle-ownership', 'contracts-implementation-authority', + 'substrate-domain-shape', 'selector-pipeline-ownership', 'back-edges', 'type-spine-inversions', @@ -561,6 +563,10 @@ export const LAYERING_RULES: Readonly> = { applicationLifecycleOwnershipViolations(context.sources), 'contracts-implementation-authority': (context) => checkContractsImplementationAuthority(context.sources), + 'substrate-domain-shape': (context) => + substrateDomainShapeViolations( + [...context.allTypeScriptSources].map(([path, source]) => ({ path, source })), + ), 'selector-pipeline-ownership': (context) => selectorPipelineOwnershipViolations(context.edges, workspaceSpecifierTargets(repoRoot)), 'back-edges': (context) => checkBackEdges(context.edges), diff --git a/scripts/layering/contracts-implementation-policy.test.ts b/scripts/layering/contracts-implementation-policy.test.ts index 652fcb0025..7b9c5a101a 100644 --- a/scripts/layering/contracts-implementation-policy.test.ts +++ b/scripts/layering/contracts-implementation-policy.test.ts @@ -74,3 +74,20 @@ test('network traffic vocabulary cannot grow parser implementation inside contra [], ); }); + +test('contracts rejects mutable interaction-outcome lifecycle', () => { + assert.match( + messages('const targets = new WeakMap();', 'packages/contracts/src/interaction-outcome.ts')[0]!, + /src\/core/, + ); +}); + +test('contracts rejects snapshot quality warning rendering', () => { + assert.match( + messages( + 'export function renderSnapshotQualityWarnings() { return []; }', + 'packages/contracts/src/snapshot-quality-warnings.ts', + )[0]!, + /src\/snapshot\/snapshot-presentation/, + ); +}); diff --git a/scripts/layering/contracts-implementation-policy.ts b/scripts/layering/contracts-implementation-policy.ts index 9db0cfb65a..993e047f97 100644 --- a/scripts/layering/contracts-implementation-policy.ts +++ b/scripts/layering/contracts-implementation-policy.ts @@ -27,6 +27,24 @@ export function contractsImplementationAuthorityViolations( file.source, parsed.program.body, ); + if (file.path === 'packages/contracts/src/interaction-outcome.ts') { + violations.push( + violation( + file.path, + 1, + 'contracts may not own mutable interaction-outcome lifecycle; that WeakMap identity map belongs in src/core', + ), + ); + } + if (file.path === 'packages/contracts/src/snapshot-quality-warnings.ts') { + violations.push( + violation( + file.path, + 1, + 'contracts may not own snapshot quality warning rendering; that presentation policy belongs in src/snapshot/snapshot-presentation', + ), + ); + } if (networkTrafficViolation) violations.push(networkTrafficViolation); for (const site of moduleSpecifiers(parsed.module, file.source)) { if (!FORBIDDEN_HOST_MODULES.test(site.spec)) continue; diff --git a/scripts/layering/daemon-modularity.ts b/scripts/layering/daemon-modularity.ts index c5aa52bde1..d3444615cf 100644 --- a/scripts/layering/daemon-modularity.ts +++ b/scripts/layering/daemon-modularity.ts @@ -3,8 +3,6 @@ import { targetDagZone, type LayeringViolation, type ResolvedImportEdge } from ' import { SESSION_STATE_FIELD_OWNERS } from './session-state.ts'; const LARGEST_TYPE_CYCLE_ZONE_CEILINGS: Readonly> = { - // After cutting the two core→daemon DaemonCommandDescriptor type edges, the previous - // 15-file mixed-zone component collapsed; the largest remaining cycle is this // co-defined-contract pair (`capabilities.ts` ↔ `runtime.ts` and the four files they // pull in). The standard shrink is a third module holding the shared type. 'provider-webdriver': 6, diff --git a/scripts/layering/package-boundaries.test.ts b/scripts/layering/package-boundaries.test.ts index 2342ac9499..9ff43b863f 100644 --- a/scripts/layering/package-boundaries.test.ts +++ b/scripts/layering/package-boundaries.test.ts @@ -105,7 +105,6 @@ const CONTRACT_EXPORTS = [ '@agent-device/contracts/interaction', '@agent-device/contracts/interaction-error', '@agent-device/contracts/interaction-guarantees', - '@agent-device/contracts/interaction-outcome', '@agent-device/contracts/interactor-operation-catalog', '@agent-device/contracts/interactor-types', '@agent-device/contracts/keyboard', @@ -151,7 +150,6 @@ const CONTRACT_EXPORTS = [ '@agent-device/contracts/settings-runtime', '@agent-device/contracts/snapshot', '@agent-device/contracts/snapshot-presentation', - '@agent-device/contracts/snapshot-quality-warnings', '@agent-device/contracts/snapshot-runtime', '@agent-device/contracts/snapshot-scope', '@agent-device/contracts/snapshot-timeout-evidence', @@ -459,7 +457,6 @@ test('the real tree parses, declares, and passes R11', () => { ); assert.deepEqual([...captureKitPackage.exportTargets.keys()].sort(), [ '@agent-device/capture-kit', - '@agent-device/capture-kit/device-inventory-context', '@agent-device/capture-kit/mobile-snapshot-semantics', '@agent-device/capture-kit/png', '@agent-device/capture-kit/png-resize', diff --git a/scripts/layering/substrate-domain-shape.test.ts b/scripts/layering/substrate-domain-shape.test.ts new file mode 100644 index 0000000000..b2c49594ad --- /dev/null +++ b/scripts/layering/substrate-domain-shape.test.ts @@ -0,0 +1,60 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { substrateDomainShapeViolations } from './substrate-domain-shape.ts'; + +function messages(path: string, source: string): string[] { + return substrateDomainShapeViolations([{ path, source }]).map((violation) => violation.message); +} + +test('capture-kit rejects request-scoped async_hooks dispatch', () => { + assert.match( + messages( + 'packages/capture-kit/src/device-inventory-context.ts', + "import { AsyncLocalStorage } from 'node:async_hooks';\n", + )[0]!, + /request-scoped AsyncLocalStorage dispatch belongs in src\/request/, + ); + assert.match( + messages( + 'packages/capture-kit/src/device-inventory-context.ts', + 'const store = new AsyncLocalStorage();\n', + )[0]!, + /request-scoped dispatch belongs in src\/request/, + ); +}); + +test('capture-kit policy ignores types, prose, tests, and root-runtime ALS', () => { + assert.deepEqual( + messages( + 'packages/capture-kit/src/app-log-live-handle.ts', + [ + 'export type Store = { AsyncLocalStorage: number };', + 'const prose = "import { AsyncLocalStorage } from \'node:async_hooks\'";', + '// new AsyncLocalStorage();', + ].join('\n'), + ), + [], + ); + assert.deepEqual( + messages( + 'packages/capture-kit/src/device-inventory-context.test.ts', + "import { AsyncLocalStorage } from 'node:async_hooks';", + ), + [], + ); + assert.deepEqual( + messages( + 'src/request/device-inventory-context.ts', + "import { AsyncLocalStorage } from 'node:async_hooks';\nconst store = new AsyncLocalStorage();\n", + ), + [], + ); +}); + +test('retired src/contracts/ production files fail closed', () => { + assert.match( + messages('src/contracts/interaction-outcome.ts', 'export const x = 1;\n')[0]!, + /src\/contracts\/ is retired/, + ); + assert.deepEqual(messages('packages/contracts/src/interaction.ts', 'export type X = 1;\n'), []); +}); diff --git a/scripts/layering/substrate-domain-shape.ts b/scripts/layering/substrate-domain-shape.ts new file mode 100644 index 0000000000..9533e58feb --- /dev/null +++ b/scripts/layering/substrate-domain-shape.ts @@ -0,0 +1,110 @@ +import { parseSync } from 'oxc-parser'; +import type { LayeringViolation } from './model.ts'; + +export const SUBSTRATE_DOMAIN_SHAPE_RULE = 'R70 substrate-domain-shape'; + +export type SubstrateDomainSource = Readonly<{ path: string; source: string }>; + +const ASYNC_HOOKS = /^(?:node:)?async_hooks(?:\/|$)/; + +/** Capture-kit is durable capture; request ALS and retired src/contracts/ stay out of substrate packages. */ +export function substrateDomainShapeViolations( + sources: readonly SubstrateDomainSource[], +): LayeringViolation[] { + const violations: LayeringViolation[] = []; + for (const file of sources) { + if (isRetiredContractsRoot(file.path)) { + violations.push( + violation( + file.path, + 1, + 'src/contracts/ is retired; vocabulary lives in packages/contracts, and executable policy belongs to its owning domain', + ), + ); + continue; + } + if (!isCaptureKitProduction(file.path)) continue; + const parsed = parseSync(file.path, file.source); + for (const site of moduleSpecifiers(parsed.module, file.source)) { + if (!ASYNC_HOOKS.test(site.spec)) continue; + violations.push( + violation( + file.path, + site.line, + `capture-kit imports '${site.spec}'; request-scoped AsyncLocalStorage dispatch belongs in src/request`, + ), + ); + } + visit(parsed.program, (node) => { + if (!isAsyncLocalStorageConstruction(node)) return; + violations.push( + violation( + file.path, + lineAt(file.source, Number(node.start ?? 0)), + 'capture-kit constructs AsyncLocalStorage; request-scoped dispatch belongs in src/request', + ), + ); + }); + } + return violations; +} + +function isRetiredContractsRoot(file: string): boolean { + return file.startsWith('src/contracts/') && file.endsWith('.ts'); +} + +function isCaptureKitProduction(file: string): boolean { + return ( + file.startsWith('packages/capture-kit/src/') && + !file.endsWith('.test.ts') && + !file.includes('/__tests__/') + ); +} + +function isAsyncLocalStorageConstruction(node: Record): boolean { + if (node.type !== 'NewExpression') return false; + const callee = node.callee; + if (callee === null || typeof callee !== 'object') return false; + const expression = callee as Record; + return expression.type === 'Identifier' && expression.name === 'AsyncLocalStorage'; +} + +function moduleSpecifiers( + module: ReturnType['module'], + source: string, +): ReadonlyArray<{ spec: string; line: number }> { + const sites: Array<{ spec: string; line: number }> = []; + const add = (request: { value?: string; start?: number } | undefined): void => { + if (request?.value) + sites.push({ spec: request.value, line: lineAt(source, request.start ?? 0) }); + }; + for (const entry of module.staticImports) add(entry.moduleRequest); + for (const entry of module.staticExports) { + for (const exported of entry.entries) add(exported.moduleRequest); + } + for (const entry of module.dynamicImports) { + const raw = source.slice(entry.moduleRequest.start, entry.moduleRequest.end); + const literal = /^(['"])([^'"]*)\1$/.exec(raw); + if (literal) sites.push({ spec: literal[2]!, line: lineAt(source, entry.moduleRequest.start) }); + } + return sites; +} + +function violation(file: string, line: number, message: string): LayeringViolation { + return { rule: SUBSTRATE_DOMAIN_SHAPE_RULE, file, line, message }; +} + +function lineAt(source: string, offset: number): number { + return source.slice(0, offset).split('\n').length; +} + +function visit(node: unknown, callback: (node: Record) => void): void { + if (node === null || typeof node !== 'object') return; + if (Array.isArray(node)) { + for (const child of node) visit(child, callback); + return; + } + const record = node as Record; + callback(record); + for (const value of Object.values(record)) visit(value, callback); +} diff --git a/src/__tests__/eager-closure-budgets.ts b/src/__tests__/eager-closure-budgets.ts index d81e65d62d..5566af6d2b 100644 --- a/src/__tests__/eager-closure-budgets.ts +++ b/src/__tests__/eager-closure-budgets.ts @@ -109,9 +109,6 @@ export const FACADE_BUDGETS: Readonly> = Object.freeze({ 'packages/ad-replay/src/index.ts': 61, // --- @agent-device/ad-script --- - // +4: format, recorded-input, target-evidence-tree, and parseReplayInput join the codec façade - // after leaving root src. Consumers of the façade (selectors, maestro, ad-replay) move by the - // same 4; the CLI already had three of those modules via other edges and moves by 1. 'packages/ad-script/src/index.ts': 41, // --- @agent-device/platform-apple/runner --- @@ -125,7 +122,6 @@ export const FACADE_BUDGETS: Readonly> = Object.freeze({ // --- @agent-device/capture-kit --- // R60 review: audio-probe split into descriptor/status/recovery/live-process modules (+3 files). 'packages/capture-kit/src/index.ts': 32, - 'packages/capture-kit/src/device-inventory-context.ts': 3, 'packages/capture-kit/src/png-resize.ts': 18, 'packages/capture-kit/src/png-rgb-difference.ts': 1, 'packages/capture-kit/src/png-size.ts': 3, @@ -220,7 +216,6 @@ export const FACADE_BUDGETS: Readonly> = Object.freeze({ 'packages/contracts/src/interaction.ts': 1, 'packages/contracts/src/interaction-error.ts': 1, 'packages/contracts/src/interaction-guarantees.ts': 1, - 'packages/contracts/src/interaction-outcome.ts': 1, 'packages/contracts/src/interactor-types.ts': 1, 'packages/contracts/src/keyboard.ts': 1, 'packages/contracts/src/logs-runtime-plan.ts': 5, @@ -254,7 +249,6 @@ export const FACADE_BUDGETS: Readonly> = Object.freeze({ 'packages/contracts/src/selector-observation-runtime.ts': 1, 'packages/contracts/src/settings.ts': 3, 'packages/contracts/src/snapshot-presentation.ts': 2, - 'packages/contracts/src/snapshot-quality-warnings.ts': 1, 'packages/contracts/src/snapshot-runtime.ts': 3, 'packages/contracts/src/snapshot-scope.ts': 1, 'packages/contracts/src/snapshot-timeout-evidence.ts': 1, diff --git a/src/__tests__/test-utils/device-inventory-gateways.ts b/src/__tests__/test-utils/device-inventory-gateways.ts index c71f8a8873..36982656d1 100644 --- a/src/__tests__/test-utils/device-inventory-gateways.ts +++ b/src/__tests__/test-utils/device-inventory-gateways.ts @@ -14,7 +14,7 @@ import type { PlatformRequestScope, } from '@agent-device/contracts/platform-runtime-host'; import type { DeviceInfo, Platform } from '@agent-device/kernel/device'; -import { withDeviceInventoryContext } from '@agent-device/capture-kit/device-inventory-context'; +import { withDeviceInventoryContext } from '../../request/device-inventory-context.ts'; import { createComposedDeviceInventoryGateways } from '../../platform-runtime-device-inventory.ts'; type TestDeviceInventoryOptions = Readonly<{ diff --git a/src/commands/capture/runtime/snapshot.ts b/src/commands/capture/runtime/snapshot.ts index e6fc34df23..b661c4552a 100644 --- a/src/commands/capture/runtime/snapshot.ts +++ b/src/commands/capture/runtime/snapshot.ts @@ -20,7 +20,7 @@ import { buildSnapshotDiff, countSnapshotComparableLines, } from '../../../snapshot/snapshot-diff.ts'; -import { renderSnapshotQualityWarnings } from '@agent-device/contracts/snapshot-quality-warnings'; +import { renderSnapshotQualityWarnings } from '../../../snapshot/snapshot-presentation/quality-warnings.ts'; import { buildSnapshotVisibility } from '../../../snapshot/snapshot-visibility.ts'; import { ANDROID_SYSTEM_SURFACE_DISCLOSURE } from '../../../core/android-system-surface-disclosure.ts'; import { formatReactNativeOverlayWarning } from '../../react-native/overlay.ts'; diff --git a/src/commands/interaction/runtime/interactions.ts b/src/commands/interaction/runtime/interactions.ts index 330244690a..3df732e103 100644 --- a/src/commands/interaction/runtime/interactions.ts +++ b/src/commands/interaction/runtime/interactions.ts @@ -11,7 +11,7 @@ import { SELECTOR_PIPELINE_POLICIES } from '../../../core/selector-pipeline-poli import type { Point } from '@agent-device/kernel/snapshot'; import type { AgentDeviceRuntime, CommandContext } from '../../../runtime-contract.ts'; import { isFillableType } from '@agent-device/contracts/snapshot'; -import { attachResolvedInteractionTarget } from '@agent-device/contracts/interaction-outcome'; +import { attachResolvedInteractionTarget } from '../../../core/interaction-outcome.ts'; import { toBackendContext } from '../../runtime-common.ts'; import { toBackendResult, type RuntimeCommand } from '../../runtime-types.ts'; import { diff --git a/src/core/dispatch-resolve.ts b/src/core/dispatch-resolve.ts index 56778ad60c..f0d8553a4d 100644 --- a/src/core/dispatch-resolve.ts +++ b/src/core/dispatch-resolve.ts @@ -18,7 +18,7 @@ import { listLocalDeviceInventory, readDeviceInventory, shouldPropagateDeviceInventoryProbeError, -} from '@agent-device/capture-kit/device-inventory-context'; +} from '../request/device-inventory-context.ts'; import type { DeviceSelectionResult, InventoryDeviceSelectionParams, diff --git a/packages/contracts/src/interaction-outcome.ts b/src/core/interaction-outcome.ts similarity index 100% rename from packages/contracts/src/interaction-outcome.ts rename to src/core/interaction-outcome.ts diff --git a/src/daemon/__tests__/snapshot-quality-latch.test.ts b/src/daemon/__tests__/snapshot-quality-latch.test.ts index 5244f2e8d8..bfbcdb806a 100644 --- a/src/daemon/__tests__/snapshot-quality-latch.test.ts +++ b/src/daemon/__tests__/snapshot-quality-latch.test.ts @@ -6,7 +6,7 @@ import { makeIosSession } from '../../__tests__/test-utils/session-factories.ts' import { recoveredSnapshotQualityWarning, renderSnapshotQualityWarnings, -} from '@agent-device/contracts/snapshot-quality-warnings'; +} from '../../snapshot/snapshot-presentation/quality-warnings.ts'; import { applyRecoveredWarningLatch, resolveRecoveredWarningLatch, diff --git a/src/daemon/daemon-command-registry.ts b/src/daemon/daemon-command-registry.ts index 5447c52d2f..7c97d2221c 100644 --- a/src/daemon/daemon-command-registry.ts +++ b/src/daemon/daemon-command-registry.ts @@ -20,7 +20,7 @@ export type DaemonProviderDeviceResolutionIntent = // The hand-authored literal that previously lived here was proven byte-equal to // this derived value by `src/core/command-descriptor/__tests__/parity.test.ts` (#906) // and has been deleted; the daemon now derives its routes/traits from the single -// source. The descriptor shape lives in core so that zone does not import daemon. +// source. export const DAEMON_COMMAND_DESCRIPTORS: readonly DaemonCommandDescriptor[] = deriveDaemonCommandDescriptors(commandDescriptors); diff --git a/src/daemon/handlers/__tests__/session-inventory-appleos.test.ts b/src/daemon/handlers/__tests__/session-inventory-appleos.test.ts index c6c18e6742..42b4d5425c 100644 --- a/src/daemon/handlers/__tests__/session-inventory-appleos.test.ts +++ b/src/daemon/handlers/__tests__/session-inventory-appleos.test.ts @@ -3,14 +3,14 @@ import { test, expect, vi, beforeEach } from 'vitest'; // The `devices` handler resolves its inventory through listDeviceInventory; mocking it // lets us drive the additive `appleOs` projection off the shared device fixtures without // touching real local discovery. -vi.mock('@agent-device/capture-kit/device-inventory-context', async (importOriginal) => { +vi.mock('../../../request/device-inventory-context.ts', async (importOriginal) => { const actual = - await importOriginal(); + await importOriginal(); return { ...actual, listDeviceInventory: vi.fn(async () => []) }; }); import { handleSessionInventoryCommands } from '../session-inventory.ts'; -import { listDeviceInventory } from '@agent-device/capture-kit/device-inventory-context'; +import { listDeviceInventory } from '../../../request/device-inventory-context.ts'; import { makeSessionStore } from '../../../__tests__/test-utils/store-factory.ts'; import type { DaemonRequest, DaemonResponse } from '../../types.ts'; import type { AppleOS, DeviceInfo } from '@agent-device/kernel/device'; diff --git a/src/daemon/handlers/interaction-touch-runtime.ts b/src/daemon/handlers/interaction-touch-runtime.ts index 24ebfd7408..50fb272130 100644 --- a/src/daemon/handlers/interaction-touch-runtime.ts +++ b/src/daemon/handlers/interaction-touch-runtime.ts @@ -7,7 +7,7 @@ import type { } from '@agent-device/contracts/interaction'; import type { GestureReferenceFrame } from '@agent-device/contracts/scroll-gesture'; import { asAppError, normalizeError } from '@agent-device/kernel/errors'; -import { readResolvedInteractionTarget } from '@agent-device/contracts/interaction-outcome'; +import { readResolvedInteractionTarget } from '../../core/interaction-outcome.ts'; import { isSessionRecording } from '../session-script-publication-capability.ts'; import type { DaemonResponse, SessionState } from '../types.ts'; import { publishInteractionAmbiguityCandidates } from './interaction-ambiguity-publication.ts'; diff --git a/src/daemon/handlers/session-doctor-device.ts b/src/daemon/handlers/session-doctor-device.ts index 0ae6924c93..9961051b97 100644 --- a/src/daemon/handlers/session-doctor-device.ts +++ b/src/daemon/handlers/session-doctor-device.ts @@ -1,5 +1,5 @@ import { buildDeviceInventoryRequestFromFlags } from '../../core/dispatch-resolve.ts'; -import { listDeviceInventory } from '@agent-device/capture-kit/device-inventory-context'; +import { listDeviceInventory } from '../../request/device-inventory-context.ts'; import { countDeviceInventoryByGroup, LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS, diff --git a/src/daemon/handlers/session-doctor.ts b/src/daemon/handlers/session-doctor.ts index ec560bf63f..f14019e5af 100644 --- a/src/daemon/handlers/session-doctor.ts +++ b/src/daemon/handlers/session-doctor.ts @@ -7,7 +7,7 @@ import { isActiveProviderDevice } from '../../provider-device-runtime.ts'; import { listLocalDeviceInventory, shouldPropagateDeviceInventoryProbeError, -} from '@agent-device/capture-kit/device-inventory-context'; +} from '../../request/device-inventory-context.ts'; import { readVersion } from '@agent-device/host-kit/version'; import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts'; import { SessionStore } from '../session-store.ts'; diff --git a/src/daemon/handlers/session-inventory.ts b/src/daemon/handlers/session-inventory.ts index 7e42115fe8..ab14ffea8e 100644 --- a/src/daemon/handlers/session-inventory.ts +++ b/src/daemon/handlers/session-inventory.ts @@ -2,7 +2,7 @@ import { commandRuntimeUseRequirements, listRuntimeFactCommands, } from '../../core/command-descriptor/registry.ts'; -import { listDeviceInventory } from '@agent-device/capture-kit/device-inventory-context'; +import { listDeviceInventory } from '../../request/device-inventory-context.ts'; import { assertResolvedAppsFilter } from '@agent-device/contracts/device'; import { AppError, asAppError } from '@agent-device/kernel/errors'; import { diff --git a/src/daemon/handlers/session-test-shard-devices.ts b/src/daemon/handlers/session-test-shard-devices.ts index de65dbd9fb..03ef5adc3d 100644 --- a/src/daemon/handlers/session-test-shard-devices.ts +++ b/src/daemon/handlers/session-test-shard-devices.ts @@ -1,6 +1,6 @@ import type { CommandFlags } from '@agent-device/contracts/command'; import type { DeviceInventoryRequest } from '@agent-device/contracts/device'; -import { listDeviceInventory } from '@agent-device/capture-kit/device-inventory-context'; +import { listDeviceInventory } from '../../request/device-inventory-context.ts'; import { resolveAndroidSerialAllowlist, resolveIosSimulatorDeviceSetPath, diff --git a/src/daemon/ios-app-session-hint.test.ts b/src/daemon/ios-app-session-hint.test.ts index bb5b18bd1c..9bf11e352a 100644 --- a/src/daemon/ios-app-session-hint.test.ts +++ b/src/daemon/ios-app-session-hint.test.ts @@ -4,8 +4,8 @@ import { beforeEach, expect, test, vi } from 'vitest'; const listBootedIosSimulators = vi.hoisted(() => vi.fn()); const detectSoleRunningIosSimulatorApp = vi.hoisted(() => vi.fn()); -vi.mock('@agent-device/capture-kit/device-inventory-context', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('../request/device-inventory-context.ts', async (importOriginal) => ({ + ...(await importOriginal()), listLocalDeviceInventory: listBootedIosSimulators, })); vi.mock('../platforms/apple/core/app-resolution.ts', () => ({ detectSoleRunningIosSimulatorApp })); diff --git a/src/daemon/request-router.ts b/src/daemon/request-router.ts index 423a835ece..4228a4b80e 100644 --- a/src/daemon/request-router.ts +++ b/src/daemon/request-router.ts @@ -1,5 +1,5 @@ import { withResolveTargetDeviceCacheScope } from '../core/dispatch-resolve.ts'; -import { withDeviceInventoryContext } from '@agent-device/capture-kit/device-inventory-context'; +import { withDeviceInventoryContext } from '../request/device-inventory-context.ts'; import type { LeaseLifecycleProvider } from '@agent-device/contracts/device'; import type { ComposedDeviceInventoryGateways } from '@agent-device/contracts/platform-module'; import type { DeviceRuntimeGateway } from '@agent-device/contracts/platform-runtime'; diff --git a/src/daemon/snapshot-quality-latch.ts b/src/daemon/snapshot-quality-latch.ts index 7aee38c304..f8853fd262 100644 --- a/src/daemon/snapshot-quality-latch.ts +++ b/src/daemon/snapshot-quality-latch.ts @@ -1,5 +1,5 @@ import type { SnapshotQualityVerdict } from '@agent-device/kernel/snapshot'; -import { recoveredSnapshotQualityWarning } from '@agent-device/contracts/snapshot-quality-warnings'; +import { recoveredSnapshotQualityWarning } from '../snapshot/snapshot-presentation/quality-warnings.ts'; import type { DaemonResponseData, SessionState } from './types.ts'; type RecoveredWarningLatch = NonNullable; diff --git a/src/platform-runtime-android-emulator-host.ts b/src/platform-runtime-android-emulator-host.ts index da016f1a2f..5670e44190 100644 --- a/src/platform-runtime-android-emulator-host.ts +++ b/src/platform-runtime-android-emulator-host.ts @@ -1,5 +1,5 @@ import type { DeviceReadinessRuntimeHost } from '@agent-device/contracts/device-readiness-runtime'; -import { listLocalDeviceInventory } from '@agent-device/capture-kit/device-inventory-context'; +import { listLocalDeviceInventory } from './request/device-inventory-context.ts'; import { runCmdDetached } from '@agent-device/host-kit/command'; import { stopPidsWithEscalation } from '@agent-device/host-kit/process'; diff --git a/src/platform-runtime-open-target.ts b/src/platform-runtime-open-target.ts index 38e8aa2505..9293937406 100644 --- a/src/platform-runtime-open-target.ts +++ b/src/platform-runtime-open-target.ts @@ -22,7 +22,7 @@ export async function resolveSoleForegroundIosApp( options: Readonly<{ simulatorSetPath?: string }> = {}, ): Promise { const { listLocalDeviceInventory, shouldPropagateDeviceInventoryProbeError } = - await import('@agent-device/capture-kit/device-inventory-context'); + await import('./request/device-inventory-context.ts'); try { const booted = await listLocalDeviceInventory({ platform: 'ios', diff --git a/src/platforms/vega/__tests__/doctor.test.ts b/src/platforms/vega/__tests__/doctor.test.ts index 559df83ddc..68d2905ce8 100644 --- a/src/platforms/vega/__tests__/doctor.test.ts +++ b/src/platforms/vega/__tests__/doctor.test.ts @@ -4,7 +4,7 @@ import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; import type { DoctorCheck } from '@agent-device/contracts/observability'; import type { HostDiagnosticsContext } from '@agent-device/contracts/host-diagnostics'; -import { shouldPropagateDeviceInventoryProbeError } from '@agent-device/capture-kit/device-inventory-context'; +import { shouldPropagateDeviceInventoryProbeError } from '../../../request/device-inventory-context.ts'; import type { VegaToolProvider } from '../tool-provider.ts'; import { withVegaToolProvider } from '../tool-provider.ts'; import { vegaToolchainCheck } from '../doctor.ts'; diff --git a/packages/capture-kit/src/device-inventory-context.ts b/src/request/device-inventory-context.ts similarity index 100% rename from packages/capture-kit/src/device-inventory-context.ts rename to src/request/device-inventory-context.ts diff --git a/src/snapshot-quality/__tests__/warnings.test.ts b/src/snapshot-quality/__tests__/warnings.test.ts index 925afee49f..0c7f96ec9f 100644 --- a/src/snapshot-quality/__tests__/warnings.test.ts +++ b/src/snapshot-quality/__tests__/warnings.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { test } from 'vitest'; import { readSnapshotQualityVerdict } from '@agent-device/capture-kit/snapshot-quality-verdict'; -import { renderSnapshotQualityWarnings } from '@agent-device/contracts/snapshot-quality-warnings'; +import { renderSnapshotQualityWarnings } from '../../snapshot/snapshot-presentation/quality-warnings.ts'; const sharedRecoveryReason = 'iOS XCTest snapshot failed while serializing the accessibility tree. Error kAXErrorIllegalArgument getting snapshot for element '; diff --git a/packages/contracts/src/snapshot-quality-warnings.ts b/src/snapshot/snapshot-presentation/quality-warnings.ts similarity index 100% rename from packages/contracts/src/snapshot-quality-warnings.ts rename to src/snapshot/snapshot-presentation/quality-warnings.ts From c826a453e26e4a0951c99aa2db297f06e5eb409f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 27 Aug 2026 21:50:16 +0200 Subject: [PATCH 3/4] refactor: drop moved implementation comments from owner modules Names, types, and tests already carry those invariants; the relocated files should not keep review-history or control-flow narration. Co-authored-by: Cursor --- packages/ad-script/src/index.ts | 43 ------------------- src/core/interaction-outcome.ts | 5 --- src/request/device-inventory-context.ts | 1 - .../snapshot-presentation/quality-warnings.ts | 30 ------------- 4 files changed, 79 deletions(-) diff --git a/packages/ad-script/src/index.ts b/packages/ad-script/src/index.ts index b559ca74b5..6c8e11d748 100644 --- a/packages/ad-script/src/index.ts +++ b/packages/ad-script/src/index.ts @@ -1,46 +1,3 @@ -/** - * The `.ad` script codec façade (#1478 P5 scoping dossier, "the codec seam"; - * widened by the P5 review pass, "keep genuinely shared recording vocabulary - * in its proper shared owner"). - * - * The canonical `.ad` replay script format — read half (parsing a script into - * actions) and write half (formatting actions back into script lines) of one - * artifact, mutually coupled (`script-formatting.ts` calls into the `open` - * action's writer; `script.ts` calls into its parser). Shared by the daemon's - * session-script publication writer, the replay engine's script reader, the - * CLI's `replay export`, and Maestro's failure-label formatting. - * - * Also owns the `# agent-device:target-v1` annotation SERDE (wire type, - * canonical field order, normalization, size caps, payload parsing) and, - * alongside it, the local-identity + ancestry-prefix matching primitives and - * their diagnostic diffs (`target-annotation-identity.ts`) — both record/ - * replay-shared `.ad` vocabulary, not engine policy. The companion - * CLASSIFICATION core (`classifyTargetBindingMatch`, - * `target-annotation-classification.ts`, decision 3's replay-time - * verification paths 2-6) moved here too (#1555 review, "complete the - * binding façade instead of documenting deviations"): its only real - * consumers are the daemon's record-time self-check - * (`src/daemon/session-target-evidence.ts`) and replay-time classification - * wrapper (`src/daemon/handlers/session-replay-target-classification.ts`), - * neither reachable through `@agent-device/ad-replay`'s - * `inspectAdReplay`/`runAdReplay`. The annotation SHAPE is not exported here - * either: it lives in `@agent-device/contracts/replay`, which every consumer - * (this package included) imports directly. - * - * Also owns `${VAR}` scope/env/resolution (`vars.ts`): the same script- - * language semantics as `env KEY=VALUE` directive parsing, shared by the - * daemon's replay runtime and the Maestro replay path. - * - * Also owns `resolveDeclaredScriptPlatform` (`open-script.ts`, #1555 - * structural-quality review): the platform a script declares before its - * first real `open` (`runtime` actions, then the `open` action's own - * attached hint) — `.ad` script semantics, not engine or daemon policy, and - * needed independently by both `@agent-device/ad-replay`'s plan-digest - * precedence and the daemon's device-selection platform resolution - * (`src/daemon/replay-device-selection.ts`), which is exactly the "shared by - * record/replay AND the daemon" shape this package exists to own. - */ - export { parseReplayScriptDetailed, readReplayScriptMetadata } from './internal/script.ts'; export type { ReplayScriptMetadata } from './internal/script.ts'; diff --git a/src/core/interaction-outcome.ts b/src/core/interaction-outcome.ts index 1d9845523e..8c1c197972 100644 --- a/src/core/interaction-outcome.ts +++ b/src/core/interaction-outcome.ts @@ -1,11 +1,7 @@ import type { ResolvedInteractionTarget } from '@agent-device/contracts/interaction'; -// The runtime can resolve an interaction before the backend reports a failure. -// Keep that resolution out of serialized error details while making it -// available to the daemon's failure-corroboration boundary. const resolvedInteractionTargets = new WeakMap(); -/** Preserve pre-dispatch target identity across a backend rejection. */ export function attachResolvedInteractionTarget( error: unknown, target: ResolvedInteractionTarget, @@ -13,7 +9,6 @@ export function attachResolvedInteractionTarget( if (isObjectLike(error)) resolvedInteractionTargets.set(error, target); } -/** Read the target captured before a backend interaction rejection. */ export function readResolvedInteractionTarget( error: unknown, ): ResolvedInteractionTarget | undefined { diff --git a/src/request/device-inventory-context.ts b/src/request/device-inventory-context.ts index 75c0dba19b..5770d52711 100644 --- a/src/request/device-inventory-context.ts +++ b/src/request/device-inventory-context.ts @@ -68,7 +68,6 @@ function requiredContext(): DeviceInventoryContext { return context; } -/** Control-flow and composition failures that a best-effort inventory probe must never hide. */ export function shouldPropagateDeviceInventoryProbeError(error: unknown): boolean { if (isRequestCanceledError(error)) return true; if ( diff --git a/src/snapshot/snapshot-presentation/quality-warnings.ts b/src/snapshot/snapshot-presentation/quality-warnings.ts index 5c7ec84894..5853f6aacc 100644 --- a/src/snapshot/snapshot-presentation/quality-warnings.ts +++ b/src/snapshot/snapshot-presentation/quality-warnings.ts @@ -1,6 +1,5 @@ import type { SnapshotNode, SnapshotQualityVerdict } from '@agent-device/kernel/snapshot'; -/** Canonical warning lines for a verdict; the single place degradation is worded. */ export function renderSnapshotQualityWarnings( verdict: SnapshotQualityVerdict, nodes: Pick[], @@ -13,24 +12,11 @@ export function renderSnapshotQualityWarnings( ]; } -/** - * Disclosed at response level, once, and only when the pass was incomplete: a - * merged element the bounded pass never reached renders exactly like one with - * no custom actions, so staying silent would teach the reader that the rest of - * the list has no affordances — the mis-inference `--actions` exists to stop. - * - * The remedy is scrolling, not `--scope`: the runner reads on-screen elements - * first, and scope is applied after the read pass, so it cannot redirect the - * budget. - */ function customActionCoverageWarning(verdict: SnapshotQualityVerdict): string[] { const coverage = verdict.customActions; if (!coverage) return []; const lines: string[] = []; if (coverage.blocked) { - // Not a budget stop, so it must not borrow the budget stop's remedy: - // scrolling cannot clear a hung read, and telling the reader to try would - // send them in circles. lines.push( 'Custom actions were not read: an earlier accessibility read is still hung, so this capture skipped the read pass instead of queueing behind it. No element’s actions list is authoritative here. Reads resume once that call returns.', ); @@ -40,8 +26,6 @@ function customActionCoverageWarning(verdict: SnapshotQualityVerdict): string[] ); } if (coverage.truncated > 0) { - // A clipped list looks complete, which is the same failure mode as an - // unread element, so it gets its own line rather than a silent cap. lines.push( `${coverage.truncated} element(s) published more custom actions than are shown; those lists are clipped to the first 8 names, and long names are shortened.`, ); @@ -49,11 +33,6 @@ function customActionCoverageWarning(verdict: SnapshotQualityVerdict): string[] return lines; } -/** - * The full recovered-state warning line. Shared with the daemon's one-shot deferred - * latch (`src/daemon/snapshot-quality-latch.ts`), which re-renders it exactly once when - * the penalty was armed by an internal capture that never reached the user. - */ export function recoveredSnapshotQualityWarning( backend: SnapshotQualityVerdict['backend'], ): string { @@ -62,10 +41,6 @@ export function recoveredSnapshotQualityWarning( function stateWarning(verdict: SnapshotQualityVerdict): string[] { if (verdict.state === 'recovered') { - // Penalty-deferred captures repeat on every capture of a hostile screen, so the full - // warning is suppressed here. When the capture that ARMED the penalty was internal - // (selector resolution, settle observation loops, system-modal probes) and never rendered it, - // the daemon's session latch re-renders the warning once at the response seam. if (verdict.reasonCode === 'deferred' || verdict.reasonCode === 'requested-backend') return []; if (verdict.reasonCode === 'presentation-failed') { return [ @@ -85,11 +60,6 @@ function stateWarning(verdict: SnapshotQualityVerdict): string[] { return []; } -/** - * Only `sparse-tree` is evidence about the app: every backend reached the screen and it - * published no semantic content. Other sparse reasons describe capture limits and must - * not be presented as an application accessibility defect. - */ function appAccessibilityDefectWarning(verdict: SnapshotQualityVerdict): string[] { if (verdict.reasonCode !== 'sparse-tree') return []; return [ From f48d2d983b68b4f9a7d2b2508b4c2914a112566e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 28 Aug 2026 07:53:08 +0200 Subject: [PATCH 4/4] refactor: drop the empty snapshot-quality layering zone W1 moved the verdict into capture-kit and this PR moved warning rendering into snapshot-presentation, so the ranked zone no longer has production files. Co-authored-by: Cursor --- scripts/layering/model.test.ts | 1 - scripts/layering/model.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/scripts/layering/model.test.ts b/scripts/layering/model.test.ts index ba55390a89..091c30b132 100644 --- a/scripts/layering/model.test.ts +++ b/scripts/layering/model.test.ts @@ -187,7 +187,6 @@ test('classifyZone separates the ranked spine from intentionally-unranked zones' // forbids daemon/ from importing commands/ so the files that wire them cannot be ranked. assert.equal(classifyZone('mcp'), 'ranked'); assert.equal(classifyZone('snapshot'), 'ranked'); - assert.equal(classifyZone('snapshot-quality'), 'ranked'); // A zone that is neither ranked nor listed peripheral must be flagged, never // silently treated as back-edge-free. assert.equal(classifyZone('not-a-real-zone'), 'unclassified'); diff --git a/scripts/layering/model.ts b/scripts/layering/model.ts index 9af061d5bc..7cbca94d79 100644 --- a/scripts/layering/model.ts +++ b/scripts/layering/model.ts @@ -43,7 +43,6 @@ const TARGET_DAG_RANK = new Map([ ['screenshot-diff', 1], ['selectors', 1], ['snapshot', 1], - ['snapshot-quality', 1], ['utils', 1], ['core', 2], ['cli-schema', 3],