refactor(snapshot): drop the bridge truncation dimension; stop promising --scope for depth caps (#2511)

The dimension and limit the bridge adapter inferred for a cut capture had
no renderer and one consumer, the comparison-identity string, where the
kind alone gives the same comparability. The runner never produced them.

The depth-cap warning suggested --scope to read deeper content; on iOS
scope narrows presentation and acquisition stays scope-blind.
This commit is contained in:
Michał Pierzchała
2026-09-12 20:52:49 +02:00
committed by GitHub
parent 1527146507
commit 8e8eeb2ced
9 changed files with 22 additions and 62 deletions
+2 -1
View File
@@ -7,7 +7,8 @@
had no disclosure at all, so an agent read a screen missing its footer, tab bar, or the items
after a long list as complete — the backends walk the tree in document order, so what falls
off is what comes last, on screen or not. One shared warning renders from the shared flag; the
limit and dimension stay backend-side.
limit and dimension stay backend-side. The depth-cap warning no longer suggests `--scope` as a
way to read deeper: on iOS, scope narrows the presented view and acquisition stays scope-blind.
- Changed: the iOS Simulator AX bridge caps a capture at 5000 nodes, up from 1500, the Android
helper's bound. Measured on a synthetic 600-row screen, acquisition time did not move with the
cap (the native read fetches the whole tree; the cap only stops conversion) while the 1500 cut
@@ -377,7 +377,7 @@ function acquisition(
truncated: false,
viewport: { kind: 'reported', rect: viewport },
lineage: { targetId: 'simulator-1', generation: 'generation-1' },
residue: [{ kind: 'truncated', dimension: 'payload', limit: 2000 }],
residue: [{ kind: 'truncated' }],
};
}
@@ -391,7 +391,7 @@ function validationFacts(request: IosSnapshotRequest): IosSnapshotValidationFact
viewport: { kind: 'reported', rect: viewport },
hittability: { kind: 'available' },
lineage: { targetId: 'simulator-1', generation: 'generation-1' },
residue: [{ kind: 'truncated', dimension: 'payload', limit: 2000 }],
residue: [{ kind: 'truncated' }],
};
}
@@ -117,7 +117,7 @@ test('comparison identity rejects every identity axis and residue mismatch', ()
{ ...base, intent: 'surface-observation' },
{ ...base, lineage: { targetId: 'simulator-1', generation: 'generation-2' } },
{ ...base, presentationKey: { ...base.presentationKey, depth: 1 } },
{ ...base, residue: [{ kind: 'truncated', dimension: 'nodes' }] },
{ ...base, residue: [{ kind: 'truncated' }] },
];
assert.equal(areIosSnapshotComparisonIdentitiesEqual(base, { ...base }), true);
assert.equal(
@@ -126,11 +126,7 @@ function residueIdentity(residue: IosAcquisitionResidue): string {
case 'missing-viewport':
return JSON.stringify({ kind: residue.kind, reason: residue.reason });
case 'truncated':
return JSON.stringify({
kind: residue.kind,
dimension: residue.dimension,
limit: residue.limit,
});
return JSON.stringify({ kind: residue.kind });
case 'stale-generation':
return JSON.stringify({
kind: residue.kind,
@@ -84,7 +84,7 @@ function appAccessibilityDefectWarning(verdict: SnapshotQualityVerdict): string[
function depthWarning(verdict: SnapshotQualityVerdict): string[] {
if (verdict.effectiveDepth === undefined) return [];
return [
`Some deeper accessibility nodes were omitted; this tree is capped at depth ${verdict.effectiveDepth}. Re-run with --depth ${verdict.effectiveDepth} --scope <container> only if you need deeper content.`,
`Some deeper accessibility nodes were omitted; the accessibility backend capped this tree at depth ${verdict.effectiveDepth}. Navigate so the content you need sits higher in the tree, and use screenshot as visual truth for the rest.`,
];
}
@@ -35,7 +35,7 @@ test('penalty-deferred recovered captures suppress the fallback warning but keep
);
assert.deepEqual(warnings, [
'Some deeper accessibility nodes were omitted; this tree is capped at depth 56. Re-run with --depth 56 --scope <container> only if you need deeper content.',
'Some deeper accessibility nodes were omitted; the accessibility backend capped this tree at depth 56. Navigate so the content you need sits higher in the tree, and use screenshot as visual truth for the rest.',
]);
});
@@ -53,7 +53,7 @@ test('non-presentation recovery keeps the generic warning for the same reason te
assert.deepEqual(warnings, [
'Detected an overly complex or slow accessibility tree. Fell back to the private-ax snapshot backend. It is OK to continue; use --json to inspect snapshotQuality.reason if you need recovery details.',
'Some deeper accessibility nodes were omitted; this tree is capped at depth 56. Re-run with --depth 56 --scope <container> only if you need deeper content.',
'Some deeper accessibility nodes were omitted; the accessibility backend capped this tree at depth 56. Navigate so the content you need sits higher in the tree, and use screenshot as visual truth for the rest.',
]);
});
@@ -71,7 +71,7 @@ test('presentation failures identify a runner bug and preserve composed warnings
assert.deepEqual(warnings, [
'Agent Device could not safely present the captured accessibility tree and fell back to the private-ax snapshot backend. This is an Agent Device runner bug, not an app accessibility-tree issue. Use screenshot as visual truth and report snapshotQuality.reason with the screenshot.',
'Some deeper accessibility nodes were omitted; this tree is capped at depth 56. Re-run with --depth 56 --scope <container> only if you need deeper content.',
'Some deeper accessibility nodes were omitted; the accessibility backend capped this tree at depth 56. Navigate so the content you need sits higher in the tree, and use screenshot as visual truth for the rest.',
]);
});
-3
View File
@@ -114,7 +114,6 @@ export type IosHittabilityEvidence =
}>;
export type IosProviderPrunedField = 'nodes' | 'depth' | 'scope' | 'interactive-only';
export type IosTruncationDimension = 'nodes' | 'depth' | 'payload';
export type IosAcquisitionResidue =
| Readonly<{
@@ -127,8 +126,6 @@ export type IosAcquisitionResidue =
}>
| Readonly<{
kind: 'truncated';
dimension: IosTruncationDimension;
limit?: number;
}>
| Readonly<{
kind: 'stale-generation';
@@ -78,11 +78,7 @@ test('the Simulator AX source returns raw acquisition facts and discloses unsupp
});
assert.equal(rawDepthOne.stage, 'acquired');
assert.equal(fixture.requestedDepths.at(-1), 1);
assert.ok(
rawDepthOne.acquisition.residue.some(
(item) => item.kind === 'truncated' && item.dimension === 'depth',
),
);
assert.ok(rawDepthOne.acquisition.residue.some((item) => item.kind === 'truncated'));
fixture.responsePid = 999;
const outcome = await source.acquire({
@@ -1,5 +1,9 @@
import { AppError } from '@agent-device/kernel/errors';
import type { CaptureHint, IosSnapshotAcquisition } from '@agent-device/contracts/ios-snapshot';
import type {
CaptureHint,
IosSnapshotAcquisition,
IosViewportEvidence,
} from '@agent-device/contracts/ios-snapshot';
import { ensureSnapshotBridgeBinary } from './cache.ts';
import { createSnapshotSourceDeadline, remainingSnapshotSourceMs } from './deadline.ts';
import { AcceptedDepthHints, type DepthHintDecision } from './depth-hints.ts';
@@ -94,13 +98,7 @@ export function createSimulatorSnapshotSource(
deadline,
});
remainingSnapshotSourceMs(deadline, 'snapshot-decode-deadline');
const acquisition = createAcquisition(
request.hint,
request.target,
envelope,
limits,
maxDepth,
);
const acquisition = createAcquisition(request.hint, request.target, envelope, limits);
recordRecovery(
host,
depthHints,
@@ -216,7 +214,6 @@ function createAcquisition(
target: SnapshotSourceRequest['target'],
envelope: SnapshotBridgeEnvelope,
limits: SnapshotSourceLimits,
maxDepth: number,
): IosSnapshotAcquisition {
if (envelope.automationEnabled !== true) {
throw snapshotSourceError('unsupported', 'automation-mode-unavailable');
@@ -243,14 +240,7 @@ function createAcquisition(
const nodes = Object.freeze(
decoded.nodes.map((node) => Object.freeze({ ...node, pid: target.pid })),
);
const residue = createAcquisitionResidue(
hint,
truncated,
decoded,
limits,
maxDepth,
nodes.length,
);
const residue = createAcquisitionResidue(hint, truncated, decoded.viewport);
const lineage = Object.freeze({
...(target.targetId ? { targetId: target.targetId } : {}),
generation,
@@ -276,36 +266,16 @@ function createAcquisition(
function createAcquisitionResidue(
hint: CaptureHint,
truncated: boolean,
decoded: ReturnType<typeof decodeSnapshotBridgeTree>,
limits: SnapshotSourceLimits,
maxDepth: number,
nodeCount: number,
viewport: IosViewportEvidence,
) {
return Object.freeze([
{ kind: 'unavailable-fact', fact: 'hittability' } as const,
...(hint.interactiveOnly
? ([{ kind: 'unavailable-fact', fact: 'interactive-query' }] as const)
: []),
...(truncated
? [truncationResidue(decoded.maxTraversalDepth, nodeCount, limits, maxDepth)]
: []),
...(decoded.viewport.kind === 'missing'
? ([{ kind: 'missing-viewport', reason: decoded.viewport.reason }] as const)
...(truncated ? ([{ kind: 'truncated' }] as const) : []),
...(viewport.kind === 'missing'
? ([{ kind: 'missing-viewport', reason: viewport.reason }] as const)
: []),
]);
}
function truncationResidue(
maxTraversalDepth: number,
nodeCount: number,
limits: SnapshotSourceLimits,
maxDepth: number,
): { kind: 'truncated'; dimension: 'nodes' | 'depth' | 'payload'; limit?: number } {
if (nodeCount >= limits.maxNodes) {
return { kind: 'truncated', dimension: 'nodes', limit: limits.maxNodes };
}
if (maxTraversalDepth >= maxDepth) {
return { kind: 'truncated', dimension: 'depth', limit: maxDepth };
}
return { kind: 'truncated', dimension: 'payload', limit: limits.maxResponseBytes };
}