@playdrop/playdrop-cli 0.13.6 → 0.13.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.13.5",
2
+ "version": "0.13.7",
3
3
  "build": 1,
4
- "runtimeSdkVersion": "0.13.5",
4
+ "runtimeSdkVersion": "0.13.7",
5
5
  "runtimeSdkBuild": 1,
6
6
  "clients": {
7
7
  "all": {
@@ -26,7 +26,7 @@ export type AppPipelineOptions = {
26
26
  agentTaskId?: number;
27
27
  playtestProof?: AgentTaskPlaytestProof;
28
28
  mediaCaptureRequired?: boolean;
29
- machineCausalGateRequired?: boolean;
29
+ playtestSmokeCheckRequired?: boolean;
30
30
  captureSession?: TaskCaptureSession | null;
31
31
  artifacts?: AppBuildArtifacts;
32
32
  };
@@ -67,7 +67,7 @@ async function runAppPipeline(client, task, options) {
67
67
  agentTaskId: options?.agentTaskId,
68
68
  playtestProof: options?.playtestProof,
69
69
  mediaCaptureRequired: options?.mediaCaptureRequired,
70
- machineCausalGateRequired: options?.machineCausalGateRequired,
70
+ playtestSmokeCheckRequired: options?.playtestSmokeCheckRequired,
71
71
  captureSession: options?.captureSession,
72
72
  };
73
73
  const upload = await (0, upload_1.uploadApp)(client, task, artifacts, uploadOptions);
@@ -1,5 +1,5 @@
1
1
  import type { ApiClient } from '@playdrop/api-client';
2
- import { type AppPlaytestTape, type AppSurface, type AppUploadCausalComparison, type AppUploadMachineCausalEvidence, type UserResponse } from '@playdrop/types';
2
+ import { type AppPlaytestTape, type AppPlaytestTapeEvent, type AppSurface, type AppUploadCausalComparison, type AppUploadMachineCausalEvidence, type UserResponse } from '@playdrop/types';
3
3
  import type { BrowserContextOptions, Frame, Page } from 'playwright-core';
4
4
  import type { AppTask } from '../catalogue';
5
5
  import { type TaskCaptureSession } from '../appUrls';
@@ -52,7 +52,9 @@ export declare function focusProjectCheckGameFrame(page: Pick<Page, 'mouse'>, fr
52
52
  width: number;
53
53
  height: number;
54
54
  }>;
55
- export declare function dispatchPlaytestTapeToFrame(page: Page, frame: Frame, surface: AppSurface, tape: AppPlaytestTape): Promise<void>;
55
+ export declare function dispatchPlaytestTapeToFrame(page: Page, frame: Frame, surface: AppSurface, tape: AppPlaytestTape, hooks?: {
56
+ onBeforeEvent?: (eventIndex: number, event: AppPlaytestTapeEvent) => void;
57
+ }): Promise<void>;
56
58
  export declare function dispatchProjectCheckActionsToFrame(page: Page, frame: Frame, actions: readonly ProjectCheckAction[]): Promise<void>;
57
59
  export declare function formatHostedLoadCheckFailure(taskName: string, result: HostedLoadCheckResult, scope?: 'local' | 'staged' | 'final'): string;
58
60
  export declare function redactHostedLoadCheckSecrets(text: string, sourceUrl: string): string;
@@ -89,17 +91,15 @@ export declare function runUploadedHostedLoadCheck(input: {
89
91
  playtestTape?: AppPlaytestTape;
90
92
  postReadyWaitMs?: number;
91
93
  }): Promise<HostedLoadCheckResult>;
92
- export declare function sliceFirstPrimaryVerbAction(tape: AppPlaytestTape): AppPlaytestTape;
93
- export declare function compareCausalFrames(firstPath: string, secondPath: string, thresholds?: {
94
+ export declare function comparePlaytestFrames(firstPath: string, secondPath: string, thresholds?: {
94
95
  meanDeltaMin: number;
95
96
  changedPixelRatioMin: number;
96
97
  }): Promise<AppUploadCausalComparison>;
97
- export declare function runUploadedHostedCausalCheck(input: {
98
+ export declare function runUploadedHostedPlaytestSmokeCheck(input: {
98
99
  client: ApiClient;
99
100
  creatorUsername: string;
100
101
  task: AppTask;
101
102
  sessionId: string;
102
- teachingText: string;
103
103
  timeoutMs?: number;
104
104
  token?: string | null;
105
105
  currentUser?: UserResponse | null;
@@ -15,9 +15,8 @@ exports.formatHostedLoadCheckFailure = formatHostedLoadCheckFailure;
15
15
  exports.redactHostedLoadCheckSecrets = redactHostedLoadCheckSecrets;
16
16
  exports.runLocalHostedLoadCheck = runLocalHostedLoadCheck;
17
17
  exports.runUploadedHostedLoadCheck = runUploadedHostedLoadCheck;
18
- exports.sliceFirstPrimaryVerbAction = sliceFirstPrimaryVerbAction;
19
- exports.compareCausalFrames = compareCausalFrames;
20
- exports.runUploadedHostedCausalCheck = runUploadedHostedCausalCheck;
18
+ exports.comparePlaytestFrames = comparePlaytestFrames;
19
+ exports.runUploadedHostedPlaytestSmokeCheck = runUploadedHostedPlaytestSmokeCheck;
21
20
  const types_1 = require("@playdrop/types");
22
21
  const node_crypto_1 = require("node:crypto");
23
22
  const promises_1 = require("node:fs/promises");
@@ -39,12 +38,8 @@ Object.defineProperty(exports, "PLAYDROP_SURFACE_CONTEXT_OPTIONS", { enumerable:
39
38
  const FRAME_SELECTOR = 'iframe[title="Game"]';
40
39
  const DEFAULT_HOSTED_LOAD_TIMEOUT_MS = 15000;
41
40
  const POST_READY_SETTLE_MS = 750;
42
- const CAUSAL_FRAME_MEAN_DELTA_MIN = 0.025;
43
- const CAUSAL_FRAME_CHANGED_PIXEL_RATIO_MIN = 0.12;
44
- const CAUSAL_START_FRAME_MEAN_DELTA_MIN = 0.02;
45
- const CAUSAL_START_FRAME_CHANGED_PIXEL_RATIO_MIN = 0.08;
46
- const CAUSAL_PROGRESS_FRAME_MEAN_DELTA_MIN = 0.012;
47
- const CAUSAL_PROGRESS_FRAME_CHANGED_PIXEL_RATIO_MIN = 0.05;
41
+ const PLAYTEST_INTERACTIVE_MEAN_DELTA_MIN = 0.002;
42
+ const PLAYTEST_INTERACTIVE_CHANGED_PIXEL_RATIO_MIN = 0.01;
48
43
  const LOAD_CHECK_SURFACE_ORDER = ['DESKTOP', 'MOBILE_LANDSCAPE', 'MOBILE_PORTRAIT'];
49
44
  const GOOGLE_TELEMETRY_HOSTS = [
50
45
  'analytics.google.com',
@@ -370,78 +365,86 @@ async function dispatchTouchPoints(session, type, points) {
370
365
  })),
371
366
  });
372
367
  }
373
- async function dispatchPlaytestTapeToFrame(page, frame, surface, tape) {
368
+ async function dispatchPlaytestTapeToFrame(page, frame, surface, tape, hooks = {}) {
374
369
  const box = await prepareProjectCheckGameFrame(frame);
375
370
  const mobile = surface !== 'DESKTOP';
376
371
  const session = mobile ? await page.context().newCDPSession(page) : null;
377
372
  const touchPoints = new Map();
378
373
  const startedAt = Date.now();
379
374
  try {
380
- for (const event of tape.events) {
381
- const waitMs = event.atMs - (Date.now() - startedAt);
382
- if (waitMs > 0) {
383
- await page.waitForTimeout(waitMs);
384
- }
385
- if (event.type === 'keyDown') {
386
- await page.keyboard.down(event.key);
387
- continue;
388
- }
389
- if (event.type === 'keyUp') {
390
- await page.keyboard.up(event.key);
391
- continue;
392
- }
393
- if (!('x' in event) || !('y' in event)) {
394
- throw new Error(`project_check_playtest_event_unsupported:${event.type}`);
395
- }
396
- const point = resolveTapePoint(box, event);
397
- const pointerId = 'pointerId' in event ? event.pointerId ?? 0 : 0;
398
- if (event.type === 'tap') {
399
- if (mobile && session) {
400
- touchPoints.set(pointerId, point);
401
- await dispatchTouchPoints(session, 'touchStart', touchPoints);
402
- await page.waitForTimeout(event.durationMs ?? types_1.APP_PLAYTEST_DEFAULT_TAP_DURATION_MS);
403
- touchPoints.delete(pointerId);
404
- await dispatchTouchPoints(session, 'touchEnd', touchPoints);
375
+ for (const [eventIndex, event] of tape.events.entries()) {
376
+ hooks.onBeforeEvent?.(eventIndex, event);
377
+ try {
378
+ const waitMs = event.atMs - (Date.now() - startedAt);
379
+ if (waitMs > 0) {
380
+ await page.waitForTimeout(waitMs);
405
381
  }
406
- else {
407
- await page.mouse.move(point.x, point.y);
408
- await page.mouse.down({ button: 'left' });
409
- await page.waitForTimeout(event.durationMs ?? types_1.APP_PLAYTEST_DEFAULT_TAP_DURATION_MS);
410
- await page.mouse.up({ button: 'left' });
382
+ if (event.type === 'keyDown') {
383
+ await page.keyboard.down(event.key);
384
+ continue;
411
385
  }
412
- continue;
413
- }
414
- if (event.type === 'pointerDown') {
415
- if (mobile && session) {
416
- touchPoints.set(pointerId, point);
417
- await dispatchTouchPoints(session, 'touchStart', touchPoints);
386
+ if (event.type === 'keyUp') {
387
+ await page.keyboard.up(event.key);
388
+ continue;
418
389
  }
419
- else {
420
- await page.mouse.move(point.x, point.y);
421
- await page.mouse.down({ button: 'left' });
390
+ if (!('x' in event) || !('y' in event)) {
391
+ throw new Error(`project_check_playtest_event_unsupported:${event.type}`);
422
392
  }
423
- continue;
424
- }
425
- if (event.type === 'pointerMove') {
426
- if (mobile && session) {
427
- touchPoints.set(pointerId, point);
428
- await dispatchTouchPoints(session, 'touchMove', touchPoints);
393
+ const point = resolveTapePoint(box, event);
394
+ const pointerId = 'pointerId' in event ? event.pointerId ?? 0 : 0;
395
+ if (event.type === 'tap') {
396
+ if (mobile && session) {
397
+ touchPoints.set(pointerId, point);
398
+ await dispatchTouchPoints(session, 'touchStart', touchPoints);
399
+ await page.waitForTimeout(event.durationMs ?? types_1.APP_PLAYTEST_DEFAULT_TAP_DURATION_MS);
400
+ touchPoints.delete(pointerId);
401
+ await dispatchTouchPoints(session, 'touchEnd', touchPoints);
402
+ }
403
+ else {
404
+ await page.mouse.move(point.x, point.y);
405
+ await page.mouse.down({ button: 'left' });
406
+ await page.waitForTimeout(event.durationMs ?? types_1.APP_PLAYTEST_DEFAULT_TAP_DURATION_MS);
407
+ await page.mouse.up({ button: 'left' });
408
+ }
409
+ continue;
429
410
  }
430
- else {
431
- await page.mouse.move(point.x, point.y);
411
+ if (event.type === 'pointerDown') {
412
+ if (mobile && session) {
413
+ touchPoints.set(pointerId, point);
414
+ await dispatchTouchPoints(session, 'touchStart', touchPoints);
415
+ }
416
+ else {
417
+ await page.mouse.move(point.x, point.y);
418
+ await page.mouse.down({ button: 'left' });
419
+ }
420
+ continue;
432
421
  }
433
- continue;
434
- }
435
- if (event.type === 'pointerUp') {
436
- if (mobile && session) {
437
- touchPoints.delete(pointerId);
438
- await dispatchTouchPoints(session, 'touchEnd', touchPoints);
422
+ if (event.type === 'pointerMove') {
423
+ if (mobile && session) {
424
+ touchPoints.set(pointerId, point);
425
+ await dispatchTouchPoints(session, 'touchMove', touchPoints);
426
+ }
427
+ else {
428
+ await page.mouse.move(point.x, point.y);
429
+ }
430
+ continue;
439
431
  }
440
- else {
441
- await page.mouse.move(point.x, point.y);
442
- await page.mouse.up({ button: 'left' });
432
+ if (event.type === 'pointerUp') {
433
+ if (mobile && session) {
434
+ touchPoints.delete(pointerId);
435
+ await dispatchTouchPoints(session, 'touchEnd', touchPoints);
436
+ }
437
+ else {
438
+ await page.mouse.move(point.x, point.y);
439
+ await page.mouse.up({ button: 'left' });
440
+ }
443
441
  }
444
442
  }
443
+ catch (error) {
444
+ const detail = error instanceof Error ? error.message : String(error);
445
+ throw new Error(`project_check_playtest_action_failed: action ${eventIndex + 1} of ${tape.events.length} `
446
+ + `(${event.type} at ${event.atMs} ms) failed: ${detail}`);
447
+ }
445
448
  }
446
449
  const remainingMs = tape.durationMs - (Date.now() - startedAt);
447
450
  if (remainingMs > 0) {
@@ -581,6 +584,8 @@ async function runHostedLoadCheck(options) {
581
584
  const shouldBootstrapSavedSession = options.savedSessionBootstrap !== false && hasExplicitToken;
582
585
  const bootstrapToken = shouldBootstrapSavedSession ? options.token?.trim() || null : null;
583
586
  const bootstrapUser = shouldBootstrapSavedSession ? options.user ?? null : null;
587
+ let currentTapeAction = '';
588
+ const withTapeAction = (line) => (currentTapeAction ? `${line} after ${currentTapeAction}` : line);
584
589
  let hostedLaunchWaiterSettled = false;
585
590
  let resolveHostedLaunchWaiter = null;
586
591
  const hostedLaunchWaiter = new Promise((resolve) => {
@@ -672,7 +677,7 @@ async function runHostedLoadCheck(options) {
672
677
  const type = message.type();
673
678
  const args = await Promise.all(message.args().map((arg) => arg.jsonValue().catch(() => arg.toString())));
674
679
  const rendered = args.length > 0 ? args.map(formatConsoleValue).join(' ') : message.text();
675
- const line = redactReportText(`[check][console:${type}] ${rendered}`);
680
+ const line = redactReportText(withTapeAction(`[check][console:${type}] ${rendered}`));
676
681
  if (type === 'error' || type === 'assert' || type === 'trace') {
677
682
  errors.push(line);
678
683
  console.error(line);
@@ -688,8 +693,9 @@ async function runHostedLoadCheck(options) {
688
693
  });
689
694
  page.on('pageerror', (error) => {
690
695
  const text = redactReportText(error?.message ?? String(error));
691
- errors.push(`[check][pageerror] ${text}`);
692
- console.error(`[check][pageerror] ${text}`);
696
+ const line = withTapeAction(`[check][pageerror] ${text}`);
697
+ errors.push(line);
698
+ console.error(line);
693
699
  });
694
700
  page.on('requestfailed', (request) => {
695
701
  const failure = request.failure();
@@ -702,8 +708,9 @@ async function runHostedLoadCheck(options) {
702
708
  if (/ERR_ABORTED/i.test(errorText) || /ERR_HTTP2_PROTOCOL_ERROR/i.test(errorText)) {
703
709
  return;
704
710
  }
705
- errors.push(`[check][requestfailed] ${text}`);
706
- console.error(`[check][requestfailed] ${text}`);
711
+ const line = withTapeAction(`[check][requestfailed] ${text}`);
712
+ errors.push(line);
713
+ console.error(line);
707
714
  });
708
715
  page.on('response', (response) => {
709
716
  const status = response.status();
@@ -715,8 +722,9 @@ async function runHostedLoadCheck(options) {
715
722
  warnings.push(`[check][response] ${text}`);
716
723
  return;
717
724
  }
718
- errors.push(`[check][response] ${text}`);
719
- console.error(`[check][response] ${text}`);
725
+ const line = withTapeAction(`[check][response] ${text}`);
726
+ errors.push(line);
727
+ console.error(line);
720
728
  });
721
729
  console.log(`[check] Opening ${reportTargetUrl}`);
722
730
  const response = await page.goto(options.targetUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
@@ -768,7 +776,11 @@ async function runHostedLoadCheck(options) {
768
776
  if (!options.playtestSurface) {
769
777
  throw new Error('project_check_playtest_surface_required');
770
778
  }
771
- await dispatchPlaytestTapeToFrame(page, frame, options.playtestSurface, options.playtestTape);
779
+ await dispatchPlaytestTapeToFrame(page, frame, options.playtestSurface, options.playtestTape, {
780
+ onBeforeEvent: (eventIndex, event) => {
781
+ currentTapeAction = `action ${eventIndex + 1} of ${options.playtestTape.events.length} (${event.type} at ${event.atMs} ms)`;
782
+ },
783
+ });
772
784
  }
773
785
  else if (options.actions?.length) {
774
786
  await dispatchProjectCheckActionsToFrame(page, frame, options.actions);
@@ -1082,55 +1094,12 @@ async function runUploadedHostedLoadCheck(input) {
1082
1094
  artifactFingerprint: recordedLaunchCheck.artifactFingerprint,
1083
1095
  };
1084
1096
  }
1085
- function sliceFirstPrimaryVerbAction(tape) {
1086
- const prefix = tape.events.slice(0, tape.startOnlyEventCount);
1087
- const postStartEvents = tape.events.slice(tape.startOnlyEventCount);
1088
- let actionEventCount = 0;
1089
- if (tape.primaryVerb === 'tap') {
1090
- actionEventCount = 1;
1091
- }
1092
- else if (tape.primaryVerb === 'key') {
1093
- const activeKeys = new Set();
1094
- for (let index = 0; index < postStartEvents.length; index += 1) {
1095
- const event = postStartEvents[index];
1096
- if (event.type === 'keyDown')
1097
- activeKeys.add(event.key);
1098
- if (event.type === 'keyUp')
1099
- activeKeys.delete(event.key);
1100
- if (activeKeys.size === 0 && index > 0) {
1101
- actionEventCount = index + 1;
1102
- break;
1103
- }
1104
- }
1105
- }
1106
- else {
1107
- const activePointers = new Set();
1108
- for (let index = 0; index < postStartEvents.length; index += 1) {
1109
- const event = postStartEvents[index];
1110
- if (event.type === 'pointerDown')
1111
- activePointers.add(event.pointerId ?? 0);
1112
- if (event.type === 'pointerUp')
1113
- activePointers.delete(event.pointerId ?? 0);
1114
- if (activePointers.size === 0 && index > 0) {
1115
- actionEventCount = index + 1;
1116
- break;
1117
- }
1118
- }
1119
- }
1120
- if (actionEventCount <= 0) {
1121
- throw new Error(`agent_task_machine_causal_gate_invalid_tape:first_${tape.primaryVerb}_action_incomplete`);
1122
- }
1123
- return {
1124
- ...tape,
1125
- events: [...prefix, ...postStartEvents.slice(0, actionEventCount)],
1126
- };
1127
- }
1128
1097
  async function hashFile(path) {
1129
1098
  return (0, node_crypto_1.createHash)('sha256').update(await (0, promises_1.readFile)(path)).digest('hex');
1130
1099
  }
1131
- async function compareCausalFrames(firstPath, secondPath, thresholds = {
1132
- meanDeltaMin: CAUSAL_FRAME_MEAN_DELTA_MIN,
1133
- changedPixelRatioMin: CAUSAL_FRAME_CHANGED_PIXEL_RATIO_MIN,
1100
+ async function comparePlaytestFrames(firstPath, secondPath, thresholds = {
1101
+ meanDeltaMin: PLAYTEST_INTERACTIVE_MEAN_DELTA_MIN,
1102
+ changedPixelRatioMin: PLAYTEST_INTERACTIVE_CHANGED_PIXEL_RATIO_MIN,
1134
1103
  }) {
1135
1104
  const readPixels = async (path) => {
1136
1105
  return await (0, sharp_1.default)(path)
@@ -1141,7 +1110,7 @@ async function compareCausalFrames(firstPath, secondPath, thresholds = {
1141
1110
  };
1142
1111
  const [first, second] = await Promise.all([readPixels(firstPath), readPixels(secondPath)]);
1143
1112
  if (first.length !== second.length || first.length === 0) {
1144
- throw new Error('agent_task_machine_causal_gate_frame_shape_mismatch');
1113
+ throw new Error('playtest_smoke_check_frame_shape_mismatch: idle and tape screenshots have different dimensions');
1145
1114
  }
1146
1115
  let deltaTotal = 0;
1147
1116
  let changedPixels = 0;
@@ -1159,30 +1128,19 @@ async function compareCausalFrames(firstPath, secondPath, thresholds = {
1159
1128
  passed: meanDelta >= thresholds.meanDeltaMin || changedPixelRatio >= thresholds.changedPixelRatioMin,
1160
1129
  };
1161
1130
  }
1162
- async function runUploadedHostedCausalCheck(input) {
1131
+ async function runUploadedHostedPlaytestSmokeCheck(input) {
1163
1132
  const primarySurface = input.task.primarySurface;
1164
1133
  const fullTape = primarySurface ? input.task.playtestTapes?.[primarySurface] : null;
1165
1134
  if (!primarySurface || !fullTape) {
1166
- throw new Error('agent_task_machine_causal_gate_primary_tape_required');
1135
+ throw new Error('agent_task_playtest_smoke_check_primary_tape_required');
1167
1136
  }
1168
- if (fullTape.startOnlyEventCount <= 0) {
1169
- throw new Error('agent_task_machine_causal_gate_start_only_required');
1170
- }
1171
- const startOnlyTape = {
1172
- ...fullTape,
1173
- events: fullTape.events.slice(0, fullTape.startOnlyEventCount),
1174
- };
1175
- const firstActionTape = sliceFirstPrimaryVerbAction(fullTape);
1176
- const tempDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'playdrop-causal-gate-'));
1137
+ const tempDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'playdrop-playtest-smoke-'));
1177
1138
  const paths = {
1178
1139
  zeroInput: (0, node_path_1.join)(tempDir, 'zero-input.png'),
1179
- startOnlyReady: (0, node_path_1.join)(tempDir, 'start-only-ready.png'),
1180
- startOnly: (0, node_path_1.join)(tempDir, 'start-only.png'),
1181
- firstAction: (0, node_path_1.join)(tempDir, 'first-action.png'),
1182
1140
  fullTape: (0, node_path_1.join)(tempDir, 'full-tape.png'),
1183
1141
  };
1184
1142
  const run = async (label, options) => {
1185
- console.log(`[upload] Machine causal gate: ${label} from sdk.host.ready().`);
1143
+ console.log(`[upload] Playtest smoke check: ${label} from sdk.host.ready().`);
1186
1144
  const result = await runUploadedHostedLoadCheck({
1187
1145
  client: input.client,
1188
1146
  creatorUsername: input.creatorUsername,
@@ -1195,10 +1153,11 @@ async function runUploadedHostedCausalCheck(input) {
1195
1153
  ...options,
1196
1154
  });
1197
1155
  if (result.status !== 'PASSED') {
1198
- throw new Error(`Machine causal ${label} run failed. ${formatHostedLoadCheckFailure(input.task.name, result, 'staged')}`);
1156
+ throw new Error(`agent_task_playtest_smoke_check_runtime_failed: ${label} failed. `
1157
+ + formatHostedLoadCheckFailure(input.task.name, result, 'staged'));
1199
1158
  }
1200
1159
  if (!result.artifactFingerprint) {
1201
- throw new Error('agent_task_machine_causal_gate_artifact_fingerprint_missing');
1160
+ throw new Error('agent_task_playtest_smoke_check_artifact_fingerprint_missing');
1202
1161
  }
1203
1162
  return result;
1204
1163
  };
@@ -1207,87 +1166,38 @@ async function runUploadedHostedCausalCheck(input) {
1207
1166
  postReadyWaitMs: fullTape.durationMs,
1208
1167
  gameFrameScreenshotPath: paths.zeroInput,
1209
1168
  });
1210
- const startOnly = await run('start-only', {
1211
- playtestTape: startOnlyTape,
1212
- readyGameFrameScreenshotPath: paths.startOnlyReady,
1213
- gameFrameScreenshotPath: paths.startOnly,
1214
- });
1215
- const firstAction = await run('first-action', {
1216
- playtestTape: firstActionTape,
1217
- gameFrameScreenshotPath: paths.firstAction,
1218
- });
1219
1169
  const full = await run('full-tape', {
1220
1170
  playtestTape: fullTape,
1221
1171
  gameFrameScreenshotPath: paths.fullTape,
1222
1172
  });
1223
1173
  const artifactFingerprint = full.artifactFingerprint;
1224
- if ([zeroInput, startOnly, firstAction].some((result) => result.artifactFingerprint !== artifactFingerprint)) {
1225
- throw new Error('agent_task_machine_causal_gate_artifact_changed_during_check');
1226
- }
1227
- const comparisons = {
1228
- startOnlyVsReady: await compareCausalFrames(paths.startOnlyReady, paths.startOnly, {
1229
- meanDeltaMin: CAUSAL_START_FRAME_MEAN_DELTA_MIN,
1230
- changedPixelRatioMin: CAUSAL_START_FRAME_CHANGED_PIXEL_RATIO_MIN,
1231
- }),
1232
- startOnlyVsZeroInput: await compareCausalFrames(paths.zeroInput, paths.startOnly, {
1233
- meanDeltaMin: CAUSAL_START_FRAME_MEAN_DELTA_MIN,
1234
- changedPixelRatioMin: CAUSAL_START_FRAME_CHANGED_PIXEL_RATIO_MIN,
1235
- }),
1236
- fullTapeVsZeroInput: await compareCausalFrames(paths.zeroInput, paths.fullTape, {
1237
- meanDeltaMin: CAUSAL_PROGRESS_FRAME_MEAN_DELTA_MIN,
1238
- changedPixelRatioMin: CAUSAL_PROGRESS_FRAME_CHANGED_PIXEL_RATIO_MIN,
1239
- }),
1240
- fullTapeVsStartOnly: await compareCausalFrames(paths.startOnly, paths.fullTape, {
1241
- meanDeltaMin: CAUSAL_PROGRESS_FRAME_MEAN_DELTA_MIN,
1242
- changedPixelRatioMin: CAUSAL_PROGRESS_FRAME_CHANGED_PIXEL_RATIO_MIN,
1243
- }),
1244
- fullTapeVsFirstAction: await compareCausalFrames(paths.firstAction, paths.fullTape, {
1245
- meanDeltaMin: CAUSAL_PROGRESS_FRAME_MEAN_DELTA_MIN,
1246
- changedPixelRatioMin: CAUSAL_PROGRESS_FRAME_CHANGED_PIXEL_RATIO_MIN,
1247
- }),
1248
- };
1249
- if (!comparisons.startOnlyVsReady.passed || !comparisons.startOnlyVsZeroInput.passed) {
1250
- throw new Error(`agent_task_machine_causal_gate_failed:start-only did not provably leave the title state. `
1251
- + `ready(mean=${comparisons.startOnlyVsReady.meanDelta.toFixed(4)},changed=${comparisons.startOnlyVsReady.changedPixelRatio.toFixed(4)}); `
1252
- + `zero(mean=${comparisons.startOnlyVsZeroInput.meanDelta.toFixed(4)},changed=${comparisons.startOnlyVsZeroInput.changedPixelRatio.toFixed(4)}). `
1253
- + 'Fix the start action, visible start-only outcome, or ready-relative timing.');
1254
- }
1255
- if (!comparisons.fullTapeVsZeroInput.passed || !comparisons.fullTapeVsStartOnly.passed) {
1256
- throw new Error(`agent_task_machine_causal_gate_failed:full tape did not meaningfully beat zero-input and start-only controls. `
1257
- + `zero(mean=${comparisons.fullTapeVsZeroInput.meanDelta.toFixed(4)},changed=${comparisons.fullTapeVsZeroInput.changedPixelRatio.toFixed(4)}); `
1258
- + `start(mean=${comparisons.fullTapeVsStartOnly.meanDelta.toFixed(4)},changed=${comparisons.fullTapeVsStartOnly.changedPixelRatio.toFixed(4)}). `
1259
- + 'Fix the game or tape before upload.');
1174
+ if (zeroInput.artifactFingerprint !== artifactFingerprint) {
1175
+ throw new Error('agent_task_playtest_smoke_check_artifact_changed_during_check');
1260
1176
  }
1261
- if (!comparisons.fullTapeVsFirstAction.passed) {
1262
- throw new Error(`agent_task_machine_causal_gate_failed:first primary action trivially reached the full-tape outcome. `
1263
- + `action(mean=${comparisons.fullTapeVsFirstAction.meanDelta.toFixed(4)},changed=${comparisons.fullTapeVsFirstAction.changedPixelRatio.toFixed(4)}). `
1264
- + 'Add a sustained causal loop before upload.');
1177
+ const comparison = await comparePlaytestFrames(paths.zeroInput, paths.fullTape);
1178
+ if (!comparison.passed) {
1179
+ throw new Error(`agent_task_playtest_smoke_check_not_interactive: The validator completed all ${fullTape.events.length} tape actions `
1180
+ + `without a crash, but the result looked the same as running the game for ${fullTape.durationMs} ms without input `
1181
+ + `(mean pixel change ${comparison.meanDelta.toFixed(4)}, changed pixels ${(comparison.changedPixelRatio * 100).toFixed(2)}%). `
1182
+ + 'Make sure the tape performs the core gameplay action and produces a visible response, then rerun the tape check.');
1265
1183
  }
1266
- const teachingText = input.teachingText.trim();
1267
1184
  const evidence = {
1268
- version: 1,
1185
+ version: 2,
1269
1186
  source: 'CLI_MACHINE',
1270
1187
  artifactFingerprint,
1271
1188
  primarySurface,
1272
1189
  primaryVerb: fullTape.primaryVerb,
1273
1190
  runs: {
1274
1191
  zeroInput: { finalFrameSha256: await hashFile(paths.zeroInput) },
1275
- startOnly: {
1276
- readyFrameSha256: await hashFile(paths.startOnlyReady),
1277
- finalFrameSha256: await hashFile(paths.startOnly),
1278
- },
1279
- firstAction: { finalFrameSha256: await hashFile(paths.firstAction) },
1280
1192
  fullTape: { finalFrameSha256: await hashFile(paths.fullTape) },
1281
1193
  },
1282
- comparisons,
1283
- teachingText,
1284
- teachingTextSha256: (0, node_crypto_1.createHash)('sha256').update(teachingText).digest('hex'),
1194
+ comparison,
1285
1195
  };
1286
1196
  const recorded = await input.client.recordAppUploadCausalCheck(input.creatorUsername, input.task.name, input.sessionId, { evidence });
1287
1197
  if (recorded.session.causalCheck.status !== 'PASSED') {
1288
- throw new Error('agent_task_machine_causal_gate_not_recorded');
1198
+ throw new Error('agent_task_playtest_smoke_check_not_recorded');
1289
1199
  }
1290
- console.log(`[upload] Machine causal gate passed for ${input.creatorUsername}/${input.task.name}.`);
1200
+ console.log(`[upload] Playtest smoke check passed for ${input.creatorUsername}/${input.task.name}.`);
1291
1201
  return evidence;
1292
1202
  }
1293
1203
  finally {
@@ -23,7 +23,7 @@ export type AppUploadOptions = {
23
23
  agentTaskId?: number;
24
24
  playtestProof?: AgentTaskPlaytestProof;
25
25
  mediaCaptureRequired?: boolean;
26
- machineCausalGateRequired?: boolean;
26
+ playtestSmokeCheckRequired?: boolean;
27
27
  captureSession?: TaskCaptureSession | null;
28
28
  };
29
29
  export declare function uploadApp(client: ApiClient, task: AppTask, artifacts: AppBuildArtifacts | null, options?: AppUploadOptions): Promise<AppUploadResult>;
@@ -9,7 +9,6 @@ const node_path_1 = require("node:path");
9
9
  const http_1 = require("../http");
10
10
  const loadCheck_1 = require("./loadCheck");
11
11
  const listingPreflight_1 = require("../listingPreflight");
12
- const playtestContract_1 = require("./playtestContract");
13
12
  const EXTENSION_TO_MIME = {
14
13
  '.png': 'image/png',
15
14
  '.jpg': 'image/jpeg',
@@ -355,14 +354,12 @@ async function uploadAppVersion(client, task, artifacts, options) {
355
354
  }
356
355
  if (task.hostingMode !== 'EXTERNAL'
357
356
  && !task.externalUrl
358
- && options?.machineCausalGateRequired) {
359
- const teachingText = (0, playtestContract_1.assertPrimaryVerbTeachingParity)(task);
360
- await (0, loadCheck_1.runUploadedHostedCausalCheck)({
357
+ && options?.playtestSmokeCheckRequired) {
358
+ await (0, loadCheck_1.runUploadedHostedPlaytestSmokeCheck)({
361
359
  client,
362
360
  creatorUsername,
363
361
  task,
364
362
  sessionId,
365
- teachingText,
366
363
  timeoutMs: options.loadCheckTimeoutMs,
367
364
  token: options?.token ?? null,
368
365
  currentUser: options?.user ?? null,
@@ -222,7 +222,6 @@ async function check(targetArg, options = {}) {
222
222
  });
223
223
  if (tapeSurface && playtestTape) {
224
224
  const idleScreenshotPath = buildComparisonScreenshotPath(screenshotPath, 'idle');
225
- const startOnlyScreenshotPath = buildComparisonScreenshotPath(screenshotPath, 'start-only');
226
225
  const tapeScreenshotPath = buildComparisonScreenshotPath(screenshotPath, 'tape');
227
226
  console.log(`[check] Running ${playtestTape.durationMs} ms zero-input baseline on ${tapeSurface}.`);
228
227
  const idleResult = await runCheck({
@@ -235,22 +234,6 @@ async function check(targetArg, options = {}) {
235
234
  process.exitCode = 1;
236
235
  return;
237
236
  }
238
- console.log(`[check] Reopening a clean run and executing only the first ${playtestTape.startOnlyEventCount} startup event(s).`);
239
- const startOnlyTape = playtestTape.startOnlyEventCount > 0
240
- ? { ...playtestTape, events: playtestTape.events.slice(0, playtestTape.startOnlyEventCount) }
241
- : null;
242
- const startOnlyResult = await runCheck({
243
- screenshotPath: startOnlyScreenshotPath,
244
- surface: tapeSurface,
245
- ...(startOnlyTape
246
- ? { tape: startOnlyTape }
247
- : { postReadyWaitMs: playtestTape.durationMs }),
248
- });
249
- if (startOnlyResult.status !== 'PASSED') {
250
- (0, messages_1.printErrorWithHelp)((0, loadCheck_1.formatHostedLoadCheckFailure)(appName, startOnlyResult, 'local'), startOnlyResult.screenshotPath ? [`Start-only screenshot: ${startOnlyResult.screenshotPath}`] : [], { command: 'project check' });
251
- process.exitCode = 1;
252
- return;
253
- }
254
237
  console.log(`[check] Reopening a clean run and executing the ${tapeSurface} tape.`);
255
238
  const tapeResult = await runCheck({
256
239
  screenshotPath: tapeScreenshotPath,
@@ -262,14 +245,23 @@ async function check(targetArg, options = {}) {
262
245
  process.exitCode = 1;
263
246
  return;
264
247
  }
265
- console.log(`[check] Captured matched idle, start-only, and tape runs for ${currentUsername}/${appName}.`);
266
- console.log(`[check] Idle: ${idleResult.screenshotPath ?? idleScreenshotPath}`);
267
- console.log(`[check] Start-only: ${startOnlyResult.screenshotPath ?? startOnlyScreenshotPath}`);
268
- console.log(`[check] Tape: ${tapeResult.screenshotPath ?? tapeScreenshotPath}`);
269
- console.log('[check] The tape passes only when visible evidence shows it meaningfully beats both controls.');
270
- for (const signal of playtestTape.successSignals) {
271
- console.log(`[check] Success signal ${signal.kind}: ${signal.description}`);
248
+ const idleCapture = idleResult.screenshotPath ?? idleScreenshotPath;
249
+ const tapeCapture = tapeResult.screenshotPath ?? tapeScreenshotPath;
250
+ const comparison = await (0, loadCheck_1.comparePlaytestFrames)(idleCapture, tapeCapture);
251
+ if (!comparison.passed) {
252
+ (0, messages_1.printErrorWithHelp)(`playtest_smoke_check_not_interactive: The validator completed all ${playtestTape.events.length} tape actions `
253
+ + `without a crash, but the result looked the same as running the game for ${playtestTape.durationMs} ms without input.`, [
254
+ `Idle screenshot: ${idleCapture}`,
255
+ `Tape screenshot: ${tapeCapture}`,
256
+ 'Make sure the tape performs the core gameplay action and produces a visible response.',
257
+ ], { command: 'project check' });
258
+ process.exitCode = 1;
259
+ return;
272
260
  }
261
+ console.log(`[check] Playtest smoke check passed for ${currentUsername}/${appName}.`);
262
+ console.log(`[check] ${playtestTape.events.length} tape actions completed without a crash and produced a visible response.`);
263
+ console.log(`[check] Idle: ${idleCapture}`);
264
+ console.log(`[check] Tape: ${tapeCapture}`);
273
265
  return;
274
266
  }
275
267
  const result = await runCheck({ screenshotPath });