diff --git a/packages/ad-script/src/internal/target-annotation-classification.ts b/packages/ad-script/src/internal/target-annotation-classification.ts index 875e9a799d..1cc26c12bd 100644 --- a/packages/ad-script/src/internal/target-annotation-classification.ts +++ b/packages/ad-script/src/internal/target-annotation-classification.ts @@ -10,7 +10,7 @@ * than script vocabulary. In practice its only real consumers were the * daemon's RECORD-time self-check (`@agent-device/selectors/target-evidence`) * and its REPLAY-time classification wrapper - * (`src/daemon/replay/internal/session-replay-target-classification.ts`) — both + * (`packages/replay-port/src/daemon-port/target-classification.ts`) — both * daemon files, neither reachable through `inspectAdReplay`/`runAdReplay`. * It interprets `TargetAnnotationV1` evidence semantics shared beyond the * engine (record-time AND replay-time both need the SAME verdict by diff --git a/packages/ad-script/src/internal/target-annotation-identity.ts b/packages/ad-script/src/internal/target-annotation-identity.ts index b54f69a90a..1a7169c8a9 100644 --- a/packages/ad-script/src/internal/target-annotation-identity.ts +++ b/packages/ad-script/src/internal/target-annotation-identity.ts @@ -3,7 +3,7 @@ * prefix matching over versioned `.ad` target-binding evidence, plus the * bounded diagnostic diffs built on top of it. Both the writer (over * `SnapshotNode`-derived values, `@agent-device/selectors/target-evidence`) and - * replay-time verification (`src/daemon/replay/internal/session-replay-target-classification.ts` and + * replay-time verification (`packages/replay-port/src/daemon-port/target-classification.ts` and * `src/commands/interaction/runtime/selector-wait.ts`) share this verbatim so both * sides compute the SAME identity/ancestry match by construction (#1478 P5 * review, "genuinely shared recording vocabulary" relocated to its owner). @@ -199,7 +199,7 @@ export function matchesAncestryPrefix( // Diagnostic diffs (decision 3): bounded, best-effort mismatch descriptions // shared by the record-time classification core and replay-time verification // (#1478 P5 stage C2a) — moved here verbatim from -// `src/daemon/replay/internal/session-replay-target-classification.ts` so both +// `packages/replay-port/src/daemon-port/target-classification.ts` so both // callers depend on one definition instead of two copies. // --------------------------------------------------------------------------- diff --git a/packages/replay-port/package.json b/packages/replay-port/package.json index dac8e0c68b..1ef44a972e 100644 --- a/packages/replay-port/package.json +++ b/packages/replay-port/package.json @@ -7,15 +7,12 @@ "description": "Daemon-side replay port: the adapter over the ad-replay, ad-script and replay-test engines.", "dependencies": { "@agent-device/ad-script": "workspace:*", + "@agent-device/capture-kit": "workspace:*", "@agent-device/contracts": "workspace:*", "@agent-device/kernel": "workspace:*", "@agent-device/selectors": "workspace:*" }, "exports": { - "./target-token": { - "types": "./src/daemon-port/target-token.ts", - "default": "./src/daemon-port/target-token.ts" - }, "./repair-hint": { "types": "./src/daemon-port/repair-hint.ts", "default": "./src/daemon-port/repair-hint.ts" @@ -27,6 +24,18 @@ "./session-replay-vars": { "types": "./src/session-replay-vars.ts", "default": "./src/session-replay-vars.ts" + }, + "./target-classification": { + "types": "./src/daemon-port/target-classification.ts", + "default": "./src/daemon-port/target-classification.ts" + }, + "./target-classification-fixtures": { + "types": "./src/target-classification-fixtures.ts", + "default": "./src/target-classification-fixtures.ts" + }, + "./target-token": { + "types": "./src/daemon-port/target-token.ts", + "default": "./src/daemon-port/target-token.ts" } } } diff --git a/src/daemon/replay/internal/__tests__/session-replay-target-classification.test.ts b/packages/replay-port/src/daemon-port/__tests__/target-classification.test.ts similarity index 98% rename from src/daemon/replay/internal/__tests__/session-replay-target-classification.test.ts rename to packages/replay-port/src/daemon-port/__tests__/target-classification.test.ts index d70cd0a310..a1e0e125c2 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-target-classification.test.ts +++ b/packages/replay-port/src/daemon-port/__tests__/target-classification.test.ts @@ -5,7 +5,7 @@ import type { TargetAnnotationV1 } from '@agent-device/contracts/replay'; import { computeTargetEvidence } from '@agent-device/selectors/target-evidence'; import { buildSelectorChainForNode, resolveRecordedTarget } from '@agent-device/selectors'; import { resolvePressRecordingTarget } from '@agent-device/selectors/press-retarget'; -import { classifyReplayTarget } from '../session-replay-target-classification.ts'; +import { classifyReplayTarget } from '../target-classification.ts'; import { resolveUnverifiedWrapperControl } from '@agent-device/selectors/interaction-targeting'; import { ELEMENT14_DISTINCT_SUBTREE_NODES, @@ -15,7 +15,7 @@ import { bottomTabsRealCaptureFixture, recordArticleEvidence, toSnapshotNodes, -} from './session-replay-target-classification-fixtures.ts'; +} from '@agent-device/replay-port/target-classification-fixtures'; /** Verified outcomes carry the verified member + matchCount (for the post-resolution guard). */ function assertVerified( @@ -30,8 +30,8 @@ function assertVerified( const PLATFORM = 'ios' as const; -test('classifyReplayTarget: real-capture fixture verifies by @ref when the tree is unchanged', () => { - const recorded = recordArticleEvidence(); +test('classifyReplayTarget: real-capture fixture verifies by @ref when the tree is unchanged', async () => { + const recorded = await recordArticleEvidence(); const replayNodes = bottomTabsRealCaptureFixture(); const winner = replayNodes.find((node) => node.label === 'Article, unselected'); assert.ok(winner); @@ -47,8 +47,8 @@ test('classifyReplayTarget: real-capture fixture verifies by @ref when the tree assertVerified(result, { winnerRef: winner.ref, matchCount: 1 }); }); -test('classifyReplayTarget: real-capture fixture — a relabeled node is identity-mismatch (path 3)', () => { - const recorded = recordArticleEvidence(); +test('classifyReplayTarget: real-capture fixture — a relabeled node is identity-mismatch (path 3)', async () => { + const recorded = await recordArticleEvidence(); const replayNodes = bottomTabsRealCaptureFixture(); const winner = replayNodes.find((node) => node.label === 'Article, unselected'); assert.ok(winner); diff --git a/src/daemon/replay/internal/session-replay-target-classification.ts b/packages/replay-port/src/daemon-port/target-classification.ts similarity index 100% rename from src/daemon/replay/internal/session-replay-target-classification.ts rename to packages/replay-port/src/daemon-port/target-classification.ts diff --git a/src/daemon/replay/internal/__tests__/session-replay-target-classification-fixtures.ts b/packages/replay-port/src/target-classification-fixtures.ts similarity index 93% rename from src/daemon/replay/internal/__tests__/session-replay-target-classification-fixtures.ts rename to packages/replay-port/src/target-classification-fixtures.ts index 60e938db8f..1eead75663 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-target-classification-fixtures.ts +++ b/packages/replay-port/src/target-classification-fixtures.ts @@ -1,6 +1,5 @@ import assert from 'node:assert/strict'; import type { RawSnapshotNode, SnapshotNode } from '@agent-device/kernel/snapshot'; -import { computeTargetEvidence } from '@agent-device/selectors/target-evidence'; import type { TargetAnnotationV1 } from '@agent-device/contracts/replay'; export function toSnapshotNodes(raw: RawSnapshotNode[]): SnapshotNode[] { @@ -81,7 +80,8 @@ export function bottomTabsRealCaptureFixture(): SnapshotNode[] { ]); } -export function recordArticleEvidence(): TargetAnnotationV1 { +export async function recordArticleEvidence(): Promise { + const { computeTargetEvidence } = await import('@agent-device/selectors/target-evidence'); const nodes = bottomTabsRealCaptureFixture(); const winner = nodes.find((node) => node.label === 'Article, unselected'); if (!winner) throw new Error('fixture missing Article tab'); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25c6aa7cad..19585d69a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -509,6 +509,9 @@ importers: '@agent-device/ad-script': specifier: workspace:* version: link:../ad-script + '@agent-device/capture-kit': + specifier: workspace:* + version: link:../capture-kit '@agent-device/contracts': specifier: workspace:* version: link:../contracts diff --git a/scripts/__tests__/committed-source-tree.ts b/scripts/__tests__/committed-source-tree.ts index c7742ac88e..0a8f39b4c6 100644 --- a/scripts/__tests__/committed-source-tree.ts +++ b/scripts/__tests__/committed-source-tree.ts @@ -54,6 +54,21 @@ export function renamedSince(repoRoot: string, base: string): ReadonlyMap, +): string | null { + const baseFile = renamedFrom.get(entryFile) ?? entryFile; + return WALKED_SOURCE.test(baseFile) && + isProductionSourceFile(baseFile) && + baseTree.isFile(path.resolve(repoRoot, baseFile)) + ? baseFile + : null; +} + /** Every path tracked at `treeish`, repo-root-relative, from ONE `git ls-tree`. */ function listCommittedTree(repoRoot: string, treeish: string): string[] { const listing = git(repoRoot, ['ls-tree', '-r', '--name-only', '-z', treeish]).toString('utf8'); diff --git a/scripts/__tests__/eager-closure-budgets.test.ts b/scripts/__tests__/eager-closure-budgets.test.ts index df93899c9b..1d7122f16e 100644 --- a/scripts/__tests__/eager-closure-budgets.test.ts +++ b/scripts/__tests__/eager-closure-budgets.test.ts @@ -5,6 +5,7 @@ import os from 'node:os'; import path from 'node:path'; import { eagerClosureGraphOf } from '../../src/__tests__/eager-import-closure.fixtures.ts'; import { + baseProductionPathOf, createCommittedSourceTree, headCommit, mergeBaseWithMain, @@ -378,6 +379,8 @@ function mkGitFixtureRepo(prefix: string): string { fs.writeFileSync(path.join(pkgDir, 'src/facades/top.ts'), 'export const b = 2;\n'); fs.writeFileSync(path.join(pkgDir, 'src/facades/nested/deep.ts'), 'export const c = 3;\n'); fs.writeFileSync(path.join(pkgDir, 'src/facades/skip.test.ts'), 'export const d = 4;\n'); + fs.mkdirSync(path.join(repo, 'scripts')); + fs.writeFileSync(path.join(repo, 'scripts/standalone.ts'), 'export const outside = 1;\n'); execFileSync('git', ['init', '-q'], { cwd: repo }); execFileSync('git', ['add', '.'], { cwd: repo }); execFileSync( @@ -409,9 +412,32 @@ test('a renamed entry is followed to its path at the base, not treated as first- execFileSync('git', ['mv', 'packages/demo/src/entry.ts', 'packages/demo/src/moved.ts'], { cwd: repo, }); - expect(renamedSince(repo, 'HEAD').get('packages/demo/src/moved.ts')).toBe( + const renamed = renamedSince(repo, 'HEAD'); + const baseTree = createCommittedSourceTree(repo, 'HEAD'); + expect(renamed.get('packages/demo/src/moved.ts')).toBe('packages/demo/src/entry.ts'); + expect(baseProductionPathOf(repo, 'packages/demo/src/moved.ts', baseTree, renamed)).toBe( 'packages/demo/src/entry.ts', ); + execFileSync( + 'git', + ['mv', 'packages/demo/src/facades/skip.test.ts', 'packages/demo/src/fixture.ts'], + { cwd: repo }, + ); + const withFixtureRename = renamedSince(repo, 'HEAD'); + expect(withFixtureRename.get('packages/demo/src/fixture.ts')).toBe( + 'packages/demo/src/facades/skip.test.ts', + ); + expect( + baseProductionPathOf(repo, 'packages/demo/src/fixture.ts', baseTree, withFixtureRename), + ).toBe(null); + execFileSync('git', ['mv', 'scripts/standalone.ts', 'packages/demo/src/standalone.ts'], { + cwd: repo, + }); + const withOutsideRename = renamedSince(repo, 'HEAD'); + expect(withOutsideRename.get('packages/demo/src/standalone.ts')).toBe('scripts/standalone.ts'); + expect( + baseProductionPathOf(repo, 'packages/demo/src/standalone.ts', baseTree, withOutsideRename), + ).toBe(null); }); test('discovery is recursive and reads TRACKED files only', () => { @@ -520,10 +546,9 @@ const baseTree = createCommittedSourceTree(repoRoot, mergeBase); const renamedFrom = renamedSince(repoRoot, mergeBase); const entries = eagerClosureEntries(repoRoot); -/** The entry's path in the merge-base tree (renames followed), or null when it was not there. */ +/** The entry's readable source path in the merge-base tree, if it had one. */ function basePathOf(entryFile: string): string | null { - const file = renamedFrom.get(entryFile) ?? entryFile; - return baseTree.isFile(absolute(file)) ? file : null; + return baseProductionPathOf(repoRoot, entryFile, baseTree, renamedFrom); } const baseGraphs = new Map>(); diff --git a/src/daemon/replay/internal/__tests__/session-replay-dispatch-selector-miss.test.ts b/src/daemon/replay/internal/__tests__/session-replay-dispatch-selector-miss.test.ts index 9ead4d6afa..020b4faec4 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-dispatch-selector-miss.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-dispatch-selector-miss.test.ts @@ -45,7 +45,7 @@ import { import { bottomTabsRealCaptureFixture, recordArticleEvidence, -} from './session-replay-target-classification-fixtures.ts'; +} from '@agent-device/replay-port/target-classification-fixtures'; import { captureSnapshotThroughLegacyDispatchFixture, legacyDispatchCapture, @@ -108,7 +108,7 @@ function assertDivergenceShape( test('(a) an ANNOTATED press whose dispatch throws a selector-miss yields REPLAY_DIVERGENCE, not COMMAND_FAILED', async () => { const root = mkdtempForTestSync('agent-device-replay-dispatch-miss-annotated-'); const { sessionStore, sessionName } = setupSession(root); - const evidence = recordArticleEvidence(); + const evidence = await recordArticleEvidence(); const filePath = writeReplayFile(root, [ `# agent-device:target-v1 ${JSON.stringify(evidence)}`, 'click id="article"', diff --git a/src/daemon/replay/internal/__tests__/session-replay-repair-empty-tail.test.ts b/src/daemon/replay/internal/__tests__/session-replay-repair-empty-tail.test.ts index f81c573824..10779ab3d0 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-repair-empty-tail.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-repair-empty-tail.test.ts @@ -55,7 +55,7 @@ import { bottomTabsRealCaptureFixture, recordArticleEvidence, toSnapshotNodes, -} from './session-replay-target-classification-fixtures.ts'; +} from '@agent-device/replay-port/target-classification-fixtures'; /** Repair-transaction status, or `undefined` outside a repair publication. */ function sessionRepairStatus(session: SessionState | undefined) { @@ -86,7 +86,7 @@ test('a record-and-heal divergence on the LAST step resumes with an empty tail a const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); - const evidence = recordArticleEvidence(); + const evidence = await recordArticleEvidence(); const filePath = writeReplayFile(root, [ 'open "Demo" --relaunch', `# agent-device:target-v1 ${JSON.stringify(evidence)}`, @@ -545,7 +545,7 @@ test('an unauthorized --from one past the plan end is rejected on an ARMED sessi const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); - const evidence = recordArticleEvidence(); + const evidence = await recordArticleEvidence(); const filePath = writeReplayFile(root, [ 'open "Demo" --relaunch', `# agent-device:target-v1 ${JSON.stringify(evidence)}`, @@ -628,7 +628,7 @@ test('a stale --plan-digest on an empty-tail resume is rejected WITHOUT consumin const sessionStore = new SessionStore(path.join(root, 'sessions')); const sessionName = 'default'; sessionStore.set(sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' })); - const evidence = recordArticleEvidence(); + const evidence = await recordArticleEvidence(); const filePath = writeReplayFile(root, [ 'open "Demo" --relaunch', `# agent-device:target-v1 ${JSON.stringify(evidence)}`, diff --git a/src/daemon/replay/internal/__tests__/session-replay-target-guard.test.ts b/src/daemon/replay/internal/__tests__/session-replay-target-guard.test.ts index dc6a0f101a..00f3586954 100644 --- a/src/daemon/replay/internal/__tests__/session-replay-target-guard.test.ts +++ b/src/daemon/replay/internal/__tests__/session-replay-target-guard.test.ts @@ -17,7 +17,7 @@ import { selector, } from '../../../../commands/interaction/runtime/selector-read-utils.ts'; import { createInteractionDevice } from '../../../../commands/interaction/runtime/__tests__/test-utils/index.ts'; -import { classifyReplayTarget } from '../session-replay-target-classification.ts'; +import { classifyReplayTarget } from '@agent-device/replay-port/target-classification'; /** The verified-member guard denotation the replay loop mints (identity + structural position). */ function guardFor(node: SnapshotNode, nodes: SnapshotNode[]): ReplayTargetGuardDenotation { return { diff --git a/src/daemon/replay/internal/session-replay-runtime-engine-adapter.ts b/src/daemon/replay/internal/session-replay-runtime-engine-adapter.ts index 4ed1e8e66f..5f52681f95 100644 --- a/src/daemon/replay/internal/session-replay-runtime-engine-adapter.ts +++ b/src/daemon/replay/internal/session-replay-runtime-engine-adapter.ts @@ -431,7 +431,7 @@ function describeReplayStepValue(action: SessionAction): string | undefined { // daemon-side: it re-inspects the already-projected `DaemonResponse` wire // shape to decide whether the wire-level diagnostics-augmentation step // applies, which is daemon/wire authority, not target-binding classification -// itself (that already happened, in `session-replay-target-classification.ts`'s +// itself (that already happened, in `@agent-device/replay-port/target-classification`'s // `classifyReplayTarget`, called from `classifyPreDispatchTarget`). function isCompleteTargetBindingDivergenceResponse(response: DaemonResponse): boolean { if (response.ok || response.error.code !== 'REPLAY_DIVERGENCE') return false; diff --git a/src/daemon/replay/internal/session-replay-target-verification.ts b/src/daemon/replay/internal/session-replay-target-verification.ts index 1f049e659f..b2689cc7bb 100644 --- a/src/daemon/replay/internal/session-replay-target-verification.ts +++ b/src/daemon/replay/internal/session-replay-target-verification.ts @@ -52,7 +52,7 @@ import { } from '@agent-device/replay-port/repair-hint'; import { buildReplayDivergenceFailureResponse } from './session-replay-runtime-failure-response.ts'; import { buildAndPersistReplayDivergenceResume } from './session-replay-resume.ts'; -import { classifyReplayTarget } from './session-replay-target-classification.ts'; +import { classifyReplayTarget } from '@agent-device/replay-port/target-classification'; import { extractReplayTargetToken, readRefLabel } from '@agent-device/replay-port/target-token'; // ---------------------------------------------------------------------------