@playdrop/playdrop-cli 0.12.41 → 0.13.0

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.
Files changed (116) hide show
  1. package/config/client-meta.json +2 -2
  2. package/dist/apps/index.d.ts +3 -0
  3. package/dist/apps/index.js +3 -1
  4. package/dist/apps/loadCheck.d.ts +21 -1
  5. package/dist/apps/loadCheck.js +253 -1
  6. package/dist/apps/playtestContract.d.ts +2 -0
  7. package/dist/apps/playtestContract.js +77 -0
  8. package/dist/apps/staticHtml.d.ts +28 -0
  9. package/dist/apps/staticHtml.js +343 -0
  10. package/dist/apps/upload.d.ts +1 -0
  11. package/dist/apps/upload.js +17 -0
  12. package/dist/apps/validate.js +2 -1
  13. package/dist/commands/devServer.js +19 -0
  14. package/dist/commands/feedback.d.ts +2 -0
  15. package/dist/commands/feedback.js +44 -2
  16. package/dist/commands/upload.d.ts +24 -0
  17. package/dist/commands/upload.js +131 -60
  18. package/dist/commands/worker/instrument-evidence.js +1 -1
  19. package/dist/commands/worker/runtime.d.ts +14 -0
  20. package/dist/commands/worker/runtime.js +97 -8
  21. package/dist/commands/worker.d.ts +27 -2
  22. package/dist/commands/worker.js +688 -146
  23. package/dist/index.js +7 -0
  24. package/dist/listingPreflight.js +15 -4
  25. package/node_modules/@playdrop/api-client/dist/client.d.ts +6 -2
  26. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  27. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts +3 -2
  28. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.d.ts.map +1 -1
  29. package/node_modules/@playdrop/api-client/dist/domains/agent-tasks.js +13 -2
  30. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +3 -1
  31. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  32. package/node_modules/@playdrop/api-client/dist/domains/apps.js +11 -0
  33. package/node_modules/@playdrop/api-client/dist/domains/me.d.ts +2 -1
  34. package/node_modules/@playdrop/api-client/dist/domains/me.d.ts.map +1 -1
  35. package/node_modules/@playdrop/api-client/dist/domains/me.js +11 -0
  36. package/node_modules/@playdrop/api-client/dist/index.d.ts +6 -3
  37. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  38. package/node_modules/@playdrop/api-client/dist/index.js +14 -2
  39. package/node_modules/@playdrop/config/client-meta.json +2 -2
  40. package/node_modules/@playdrop/config/dist/src/constants.d.ts +1 -0
  41. package/node_modules/@playdrop/config/dist/src/constants.d.ts.map +1 -1
  42. package/node_modules/@playdrop/config/dist/src/constants.js +1 -0
  43. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  44. package/node_modules/@playdrop/types/dist/api.d.ts +195 -4
  45. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  46. package/node_modules/@playdrop/types/dist/api.js +25 -2
  47. package/node_modules/@playdrop/types/dist/app-playtest-tape.d.ts +4 -0
  48. package/node_modules/@playdrop/types/dist/app-playtest-tape.d.ts.map +1 -1
  49. package/node_modules/@playdrop/types/dist/app-playtest-tape.js +56 -0
  50. package/node_modules/@playdrop/types/dist/version.d.ts +49 -1
  51. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  52. package/node_modules/entities/decode-codepoint.d.ts +19 -0
  53. package/node_modules/entities/decode-codepoint.d.ts.map +1 -0
  54. package/node_modules/entities/decode-codepoint.js +77 -0
  55. package/node_modules/entities/decode-codepoint.js.map +1 -0
  56. package/node_modules/entities/decode.d.ts +209 -0
  57. package/node_modules/entities/decode.d.ts.map +1 -0
  58. package/node_modules/entities/decode.js +511 -0
  59. package/node_modules/entities/decode.js.map +1 -0
  60. package/node_modules/entities/encode.d.ts +22 -0
  61. package/node_modules/entities/encode.d.ts.map +1 -0
  62. package/node_modules/entities/encode.js +73 -0
  63. package/node_modules/entities/encode.js.map +1 -0
  64. package/node_modules/entities/escape.d.ts +43 -0
  65. package/node_modules/entities/escape.d.ts.map +1 -0
  66. package/node_modules/entities/escape.js +121 -0
  67. package/node_modules/entities/escape.js.map +1 -0
  68. package/node_modules/entities/generated/decode-data-html.d.ts +2 -0
  69. package/node_modules/entities/generated/decode-data-html.d.ts.map +1 -0
  70. package/node_modules/entities/generated/decode-data-html.js +10 -0
  71. package/node_modules/entities/generated/decode-data-html.js.map +1 -0
  72. package/node_modules/entities/generated/decode-data-xml.d.ts +2 -0
  73. package/node_modules/entities/generated/decode-data-xml.d.ts.map +1 -0
  74. package/node_modules/entities/generated/decode-data-xml.js +10 -0
  75. package/node_modules/entities/generated/decode-data-xml.js.map +1 -0
  76. package/node_modules/entities/generated/encode-html.d.ts +8 -0
  77. package/node_modules/entities/generated/encode-html.d.ts.map +1 -0
  78. package/node_modules/entities/generated/encode-html.js +13 -0
  79. package/node_modules/entities/generated/encode-html.js.map +1 -0
  80. package/node_modules/entities/index.d.ts +96 -0
  81. package/node_modules/entities/index.d.ts.map +1 -0
  82. package/node_modules/entities/index.js +131 -0
  83. package/node_modules/entities/index.js.map +1 -0
  84. package/node_modules/entities/package.json +3 -0
  85. package/node_modules/parse5/common/doctype.d.ts +4 -0
  86. package/node_modules/parse5/common/doctype.js +118 -0
  87. package/node_modules/parse5/common/error-codes.d.ts +67 -0
  88. package/node_modules/parse5/common/error-codes.js +66 -0
  89. package/node_modules/parse5/common/foreign-content.d.ts +9 -0
  90. package/node_modules/parse5/common/foreign-content.js +237 -0
  91. package/node_modules/parse5/common/html.d.ts +289 -0
  92. package/node_modules/parse5/common/html.js +528 -0
  93. package/node_modules/parse5/common/token.d.ts +84 -0
  94. package/node_modules/parse5/common/token.js +24 -0
  95. package/node_modules/parse5/common/unicode.d.ts +42 -0
  96. package/node_modules/parse5/common/unicode.js +70 -0
  97. package/node_modules/parse5/index.d.ts +71 -0
  98. package/node_modules/parse5/index.js +54 -0
  99. package/node_modules/parse5/package.json +1 -0
  100. package/node_modules/parse5/parser/formatting-element-list.d.ts +36 -0
  101. package/node_modules/parse5/parser/formatting-element-list.js +114 -0
  102. package/node_modules/parse5/parser/index.d.ts +221 -0
  103. package/node_modules/parse5/parser/index.js +3240 -0
  104. package/node_modules/parse5/parser/open-element-stack.d.ts +53 -0
  105. package/node_modules/parse5/parser/open-element-stack.js +328 -0
  106. package/node_modules/parse5/serializer/index.d.ts +60 -0
  107. package/node_modules/parse5/serializer/index.js +172 -0
  108. package/node_modules/parse5/tokenizer/index.d.ts +247 -0
  109. package/node_modules/parse5/tokenizer/index.js +2714 -0
  110. package/node_modules/parse5/tokenizer/preprocessor.d.ts +36 -0
  111. package/node_modules/parse5/tokenizer/preprocessor.js +200 -0
  112. package/node_modules/parse5/tree-adapters/default.d.ts +84 -0
  113. package/node_modules/parse5/tree-adapters/default.js +176 -0
  114. package/node_modules/parse5/tree-adapters/interface.d.ts +255 -0
  115. package/node_modules/parse5/tree-adapters/interface.js +2 -0
  116. package/package.json +7 -1
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.12.41",
2
+ "version": "0.13.0",
3
3
  "build": 1,
4
- "runtimeSdkVersion": "0.12.41",
4
+ "runtimeSdkVersion": "0.13.0",
5
5
  "runtimeSdkBuild": 1,
6
6
  "clients": {
7
7
  "all": {
@@ -19,13 +19,16 @@ export type AppPipelineOptions = {
19
19
  token?: string | null;
20
20
  user?: UserResponse | null;
21
21
  runLocalLoadCheck?: boolean;
22
+ devRouterPort?: number;
22
23
  runStagedUploadLoadCheck?: boolean;
23
24
  loadCheckTimeoutMs?: number;
24
25
  ensureRegisteredAppShell?: boolean;
25
26
  agentTaskId?: number;
26
27
  playtestProof?: AgentTaskPlaytestProof;
27
28
  mediaCaptureRequired?: boolean;
29
+ machineCausalGateRequired?: boolean;
28
30
  captureSession?: TaskCaptureSession | null;
31
+ artifacts?: AppBuildArtifacts;
29
32
  };
30
33
  export declare function runAppPipeline(client: ApiClient, task: AppTask, options?: AppPipelineOptions): Promise<AppPipelineResult>;
31
34
  export { collectAppValidationWarnings, validateAppTask, buildApp, uploadApp, runFormatScript };
@@ -29,7 +29,7 @@ async function runAppPipeline(client, task, options) {
29
29
  agentTaskId: options.agentTaskId,
30
30
  });
31
31
  }
32
- const artifacts = isExternal ? null : await (0, build_1.buildApp)(task);
32
+ const artifacts = isExternal ? null : options?.artifacts ?? await (0, build_1.buildApp)(task);
33
33
  const warnings = (0, validate_1.collectAppValidationWarnings)(task, isExternal ? 'source' : 'bundle');
34
34
  if (!isExternal && options?.runLocalLoadCheck) {
35
35
  const creatorUsername = typeof options.creatorUsername === 'string' ? options.creatorUsername.trim() : '';
@@ -46,6 +46,7 @@ async function runAppPipeline(client, task, options) {
46
46
  webBase: options.webBase,
47
47
  creatorUsername,
48
48
  task,
49
+ devRouterPort: options.devRouterPort,
49
50
  timeoutMs: options.loadCheckTimeoutMs,
50
51
  token: options.token ?? null,
51
52
  currentUser: options.user ?? null,
@@ -66,6 +67,7 @@ async function runAppPipeline(client, task, options) {
66
67
  agentTaskId: options?.agentTaskId,
67
68
  playtestProof: options?.playtestProof,
68
69
  mediaCaptureRequired: options?.mediaCaptureRequired,
70
+ machineCausalGateRequired: options?.machineCausalGateRequired,
69
71
  captureSession: options?.captureSession,
70
72
  };
71
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 UserResponse } from '@playdrop/types';
2
+ import { type AppPlaytestTape, 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';
@@ -84,4 +84,24 @@ export declare function runUploadedHostedLoadCheck(input: {
84
84
  currentUser?: UserResponse | null;
85
85
  captureSession?: TaskCaptureSession | null;
86
86
  screenshotPath?: string | null;
87
+ readyGameFrameScreenshotPath?: string | null;
88
+ gameFrameScreenshotPath?: string | null;
89
+ playtestTape?: AppPlaytestTape;
90
+ postReadyWaitMs?: number;
87
91
  }): Promise<HostedLoadCheckResult>;
92
+ export declare function sliceFirstPrimaryVerbAction(tape: AppPlaytestTape): AppPlaytestTape;
93
+ export declare function compareCausalFrames(firstPath: string, secondPath: string, thresholds?: {
94
+ meanDeltaMin: number;
95
+ changedPixelRatioMin: number;
96
+ }): Promise<AppUploadCausalComparison>;
97
+ export declare function runUploadedHostedCausalCheck(input: {
98
+ client: ApiClient;
99
+ creatorUsername: string;
100
+ task: AppTask;
101
+ sessionId: string;
102
+ teachingText: string;
103
+ timeoutMs?: number;
104
+ token?: string | null;
105
+ currentUser?: UserResponse | null;
106
+ captureSession?: TaskCaptureSession | null;
107
+ }): Promise<AppUploadMachineCausalEvidence>;
@@ -1,4 +1,7 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.PLAYDROP_SURFACE_CONTEXT_OPTIONS = void 0;
4
7
  exports.resolveLoadCheckSurface = resolveLoadCheckSurface;
@@ -12,9 +15,15 @@ exports.formatHostedLoadCheckFailure = formatHostedLoadCheckFailure;
12
15
  exports.redactHostedLoadCheckSecrets = redactHostedLoadCheckSecrets;
13
16
  exports.runLocalHostedLoadCheck = runLocalHostedLoadCheck;
14
17
  exports.runUploadedHostedLoadCheck = runUploadedHostedLoadCheck;
18
+ exports.sliceFirstPrimaryVerbAction = sliceFirstPrimaryVerbAction;
19
+ exports.compareCausalFrames = compareCausalFrames;
20
+ exports.runUploadedHostedCausalCheck = runUploadedHostedCausalCheck;
15
21
  const types_1 = require("@playdrop/types");
22
+ const node_crypto_1 = require("node:crypto");
16
23
  const promises_1 = require("node:fs/promises");
24
+ const node_os_1 = require("node:os");
17
25
  const node_path_1 = require("node:path");
26
+ const sharp_1 = __importDefault(require("sharp"));
18
27
  const appUrls_1 = require("../appUrls");
19
28
  const devShared_1 = require("../commands/devShared");
20
29
  const dev_1 = require("../commands/dev");
@@ -30,6 +39,12 @@ Object.defineProperty(exports, "PLAYDROP_SURFACE_CONTEXT_OPTIONS", { enumerable:
30
39
  const FRAME_SELECTOR = 'iframe[title="Game"]';
31
40
  const DEFAULT_HOSTED_LOAD_TIMEOUT_MS = 15000;
32
41
  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;
33
48
  const LOAD_CHECK_SURFACE_ORDER = ['DESKTOP', 'MOBILE_LANDSCAPE', 'MOBILE_PORTRAIT'];
34
49
  const GOOGLE_TELEMETRY_HOSTS = [
35
50
  'analytics.google.com',
@@ -499,6 +514,14 @@ async function saveScreenshot(page, screenshotPath) {
499
514
  console.log(`[check] Saved screenshot to ${(0, node_path_1.relative)(process.cwd(), normalizedPath) || normalizedPath}`);
500
515
  return normalizedPath;
501
516
  }
517
+ async function saveGameFrameScreenshot(page, screenshotPath) {
518
+ const normalizedPath = typeof screenshotPath === 'string' ? screenshotPath.trim() : '';
519
+ if (!normalizedPath)
520
+ return null;
521
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(normalizedPath), { recursive: true });
522
+ await page.locator(FRAME_SELECTOR).screenshot({ path: normalizedPath, type: 'png' });
523
+ return normalizedPath;
524
+ }
502
525
  function normalizeLoadCheckFailure(error, artifactFingerprint) {
503
526
  const rawMessage = error instanceof Error ? error.message : String(error);
504
527
  const message = normalizeLoadCheckMessage(rawMessage);
@@ -716,7 +739,7 @@ async function runHostedLoadCheck(options) {
716
739
  hostedLaunchWaiter,
717
740
  page.waitForTimeout(timeoutMs).then(() => {
718
741
  throw new Error(expectedHostedLaunchState === 'ready'
719
- ? 'hosted_app_missing_ready: Hosted app did not report ready before the load-check timeout.'
742
+ ? 'hosted_app_missing_ready: instrument_error:game_ready_timeout: Hosted app did not report sdk.host.ready() before the instrument timeout.'
720
743
  : `hosted_app_gate_missing: Hosted app did not report ${expectedHostedLaunchState} before the load-check timeout.`);
721
744
  }),
722
745
  ]);
@@ -737,6 +760,7 @@ async function runHostedLoadCheck(options) {
737
760
  if (isSoftwareWebGlRenderer(webglRenderer)) {
738
761
  throw new Error(`project_check_webgl_renderer_software: WebGL renderer is "${webglRenderer}". Real hardware acceleration is required.`);
739
762
  }
763
+ await saveGameFrameScreenshot(page, options.readyGameFrameScreenshotPath);
740
764
  if (options.playtestTape && options.actions?.length) {
741
765
  throw new Error('project_check_input_mode_conflict: playtest tape and legacy actions cannot run together.');
742
766
  }
@@ -753,6 +777,7 @@ async function runHostedLoadCheck(options) {
753
777
  else if (options.postReadyWaitMs && options.postReadyWaitMs > 0) {
754
778
  await page.waitForTimeout(options.postReadyWaitMs);
755
779
  }
780
+ await saveGameFrameScreenshot(page, options.gameFrameScreenshotPath);
756
781
  screenshotPath = await saveScreenshot(page, options.screenshotPath);
757
782
  }, {
758
783
  ...options.contextOptions,
@@ -981,6 +1006,11 @@ async function runUploadedHostedLoadCheck(input) {
981
1006
  expectedHostedLaunchState: resolvePostAuthHostedLaunchState(preview.localAppMetadata.controllerMode),
982
1007
  contextOptions,
983
1008
  screenshotPath: input.screenshotPath,
1009
+ readyGameFrameScreenshotPath: input.readyGameFrameScreenshotPath,
1010
+ gameFrameScreenshotPath: input.gameFrameScreenshotPath,
1011
+ playtestTape: input.playtestTape,
1012
+ playtestSurface: captureSurface,
1013
+ postReadyWaitMs: input.postReadyWaitMs,
984
1014
  });
985
1015
  }
986
1016
  else if (anonymousExpectedState === 'login_required') {
@@ -1009,6 +1039,11 @@ async function runUploadedHostedLoadCheck(input) {
1009
1039
  savedSessionBootstrap: true,
1010
1040
  contextOptions,
1011
1041
  screenshotPath: input.screenshotPath,
1042
+ readyGameFrameScreenshotPath: input.readyGameFrameScreenshotPath,
1043
+ gameFrameScreenshotPath: input.gameFrameScreenshotPath,
1044
+ playtestTape: input.playtestTape,
1045
+ playtestSurface: captureSurface,
1046
+ postReadyWaitMs: input.postReadyWaitMs,
1012
1047
  });
1013
1048
  }
1014
1049
  }
@@ -1021,6 +1056,11 @@ async function runUploadedHostedLoadCheck(input) {
1021
1056
  expectedHostedLaunchState: anonymousExpectedState,
1022
1057
  contextOptions,
1023
1058
  screenshotPath: input.screenshotPath,
1059
+ readyGameFrameScreenshotPath: input.readyGameFrameScreenshotPath,
1060
+ gameFrameScreenshotPath: input.gameFrameScreenshotPath,
1061
+ playtestTape: input.playtestTape,
1062
+ playtestSurface: captureSurface,
1063
+ postReadyWaitMs: input.postReadyWaitMs,
1024
1064
  });
1025
1065
  }
1026
1066
  const recorded = await input.client.recordAppUploadLaunchCheck(input.creatorUsername, input.task.name, input.sessionId, {
@@ -1042,3 +1082,215 @@ async function runUploadedHostedLoadCheck(input) {
1042
1082
  artifactFingerprint: recordedLaunchCheck.artifactFingerprint,
1043
1083
  };
1044
1084
  }
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
+ async function hashFile(path) {
1129
+ return (0, node_crypto_1.createHash)('sha256').update(await (0, promises_1.readFile)(path)).digest('hex');
1130
+ }
1131
+ async function compareCausalFrames(firstPath, secondPath, thresholds = {
1132
+ meanDeltaMin: CAUSAL_FRAME_MEAN_DELTA_MIN,
1133
+ changedPixelRatioMin: CAUSAL_FRAME_CHANGED_PIXEL_RATIO_MIN,
1134
+ }) {
1135
+ const readPixels = async (path) => {
1136
+ return await (0, sharp_1.default)(path)
1137
+ .resize(64, 64, { fit: 'fill' })
1138
+ .greyscale()
1139
+ .raw()
1140
+ .toBuffer();
1141
+ };
1142
+ const [first, second] = await Promise.all([readPixels(firstPath), readPixels(secondPath)]);
1143
+ if (first.length !== second.length || first.length === 0) {
1144
+ throw new Error('agent_task_machine_causal_gate_frame_shape_mismatch');
1145
+ }
1146
+ let deltaTotal = 0;
1147
+ let changedPixels = 0;
1148
+ for (let index = 0; index < first.length; index += 1) {
1149
+ const delta = Math.abs(first[index] - second[index]);
1150
+ deltaTotal += delta;
1151
+ if (delta >= 16)
1152
+ changedPixels += 1;
1153
+ }
1154
+ const meanDelta = deltaTotal / first.length / 255;
1155
+ const changedPixelRatio = changedPixels / first.length;
1156
+ return {
1157
+ meanDelta,
1158
+ changedPixelRatio,
1159
+ passed: meanDelta >= thresholds.meanDeltaMin || changedPixelRatio >= thresholds.changedPixelRatioMin,
1160
+ };
1161
+ }
1162
+ async function runUploadedHostedCausalCheck(input) {
1163
+ const primarySurface = input.task.primarySurface;
1164
+ const fullTape = primarySurface ? input.task.playtestTapes?.[primarySurface] : null;
1165
+ if (!primarySurface || !fullTape) {
1166
+ throw new Error('agent_task_machine_causal_gate_primary_tape_required');
1167
+ }
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-'));
1177
+ const paths = {
1178
+ 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
+ fullTape: (0, node_path_1.join)(tempDir, 'full-tape.png'),
1183
+ };
1184
+ const run = async (label, options) => {
1185
+ console.log(`[upload] Machine causal gate: ${label} from sdk.host.ready().`);
1186
+ const result = await runUploadedHostedLoadCheck({
1187
+ client: input.client,
1188
+ creatorUsername: input.creatorUsername,
1189
+ task: input.task,
1190
+ sessionId: input.sessionId,
1191
+ timeoutMs: input.timeoutMs,
1192
+ token: input.token,
1193
+ currentUser: input.currentUser,
1194
+ captureSession: input.captureSession,
1195
+ ...options,
1196
+ });
1197
+ if (result.status !== 'PASSED') {
1198
+ throw new Error(`Machine causal ${label} run failed. ${formatHostedLoadCheckFailure(input.task.name, result, 'staged')}`);
1199
+ }
1200
+ if (!result.artifactFingerprint) {
1201
+ throw new Error('agent_task_machine_causal_gate_artifact_fingerprint_missing');
1202
+ }
1203
+ return result;
1204
+ };
1205
+ try {
1206
+ const zeroInput = await run('zero-input', {
1207
+ postReadyWaitMs: fullTape.durationMs,
1208
+ gameFrameScreenshotPath: paths.zeroInput,
1209
+ });
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
+ const full = await run('full-tape', {
1220
+ playtestTape: fullTape,
1221
+ gameFrameScreenshotPath: paths.fullTape,
1222
+ });
1223
+ 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.');
1260
+ }
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.');
1265
+ }
1266
+ const teachingText = input.teachingText.trim();
1267
+ const evidence = {
1268
+ version: 1,
1269
+ source: 'CLI_MACHINE',
1270
+ artifactFingerprint,
1271
+ primarySurface,
1272
+ primaryVerb: fullTape.primaryVerb,
1273
+ runs: {
1274
+ 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
+ fullTape: { finalFrameSha256: await hashFile(paths.fullTape) },
1281
+ },
1282
+ comparisons,
1283
+ teachingText,
1284
+ teachingTextSha256: (0, node_crypto_1.createHash)('sha256').update(teachingText).digest('hex'),
1285
+ };
1286
+ const recorded = await input.client.recordAppUploadCausalCheck(input.creatorUsername, input.task.name, input.sessionId, { evidence });
1287
+ if (recorded.session.causalCheck.status !== 'PASSED') {
1288
+ throw new Error('agent_task_machine_causal_gate_not_recorded');
1289
+ }
1290
+ console.log(`[upload] Machine causal gate passed for ${input.creatorUsername}/${input.task.name}.`);
1291
+ return evidence;
1292
+ }
1293
+ finally {
1294
+ await (0, promises_1.rm)(tempDir, { recursive: true, force: true });
1295
+ }
1296
+ }
@@ -0,0 +1,2 @@
1
+ import type { AppTask } from '../catalogue';
2
+ export declare function assertPrimaryVerbTeachingParity(task: AppTask): string;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertPrimaryVerbTeachingParity = assertPrimaryVerbTeachingParity;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const types_1 = require("@playdrop/types");
7
+ function extractPotentialPlayerText(source, extension) {
8
+ const candidates = new Set();
9
+ if (extension === '.html') {
10
+ const visibleText = source
11
+ .replace(/<!--[\s\S]*?-->/g, ' ')
12
+ .replace(/<(?:script|style)\b[^>]*>[\s\S]*?<\/(?:script|style)>/gi, ' ')
13
+ .replace(/<[^>]+>/g, ' ')
14
+ .replace(/\s+/g, ' ')
15
+ .trim();
16
+ if (visibleText)
17
+ candidates.add(visibleText.slice(0, 300));
18
+ }
19
+ const stringLiteral = /"((?:\\.|[^"\\]){1,300})"|'((?:\\.|[^'\\]){1,300})'|`((?:\\.|[^`\\]){1,300})`/g;
20
+ for (const match of source.matchAll(stringLiteral)) {
21
+ const text = (match[1] ?? match[2] ?? match[3] ?? '')
22
+ .replace(/\\[nrt]/g, ' ')
23
+ .replace(/\s+/g, ' ')
24
+ .trim();
25
+ if (text)
26
+ candidates.add(text);
27
+ }
28
+ return Array.from(candidates);
29
+ }
30
+ function readPrimaryVerbTeachingText(task) {
31
+ const runtimeRoot = (0, node_path_1.dirname)(task.filePath);
32
+ const matches = new Set();
33
+ const walk = (dir) => {
34
+ for (const entry of (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })) {
35
+ const absolute = (0, node_path_1.join)(dir, entry.name);
36
+ if (entry.isDirectory()) {
37
+ if (!['node_modules', '.git', 'assets'].includes(entry.name))
38
+ walk(absolute);
39
+ continue;
40
+ }
41
+ if (!entry.isFile() || !['.html', '.js', '.mjs', '.cjs'].includes((0, node_path_1.extname)(entry.name).toLowerCase()))
42
+ continue;
43
+ const extension = (0, node_path_1.extname)(entry.name).toLowerCase();
44
+ const source = (0, node_fs_1.readFileSync)(absolute, 'utf8');
45
+ for (const text of extractPotentialPlayerText(source, extension)) {
46
+ if (types_1.APP_PLAYTEST_PRIMARY_VERB_VALUES.some((verb) => (0, types_1.appPlaytestTeachingTextMatchesVerb)(text, verb))) {
47
+ matches.add(text);
48
+ }
49
+ }
50
+ }
51
+ };
52
+ walk(runtimeRoot);
53
+ return Array.from(matches);
54
+ }
55
+ function assertPrimaryVerbTeachingParity(task) {
56
+ const primarySurface = task.primarySurface;
57
+ const tape = primarySurface ? task.playtestTapes?.[primarySurface] : null;
58
+ if (!primarySurface || !tape) {
59
+ throw new Error('agent_task_primary_verb_tape_required');
60
+ }
61
+ const teachingTexts = readPrimaryVerbTeachingText(task);
62
+ const matchingText = teachingTexts.find((text) => (0, types_1.appPlaytestTeachingTextMatchesVerb)(text, tape.primaryVerb));
63
+ if (!matchingText) {
64
+ throw new Error(`agent_task_primary_verb_not_taught:${tape.primaryVerb}: show the player a concise instruction using the declared primary verb.`);
65
+ }
66
+ const competingVerbs = types_1.APP_PLAYTEST_PRIMARY_VERB_VALUES
67
+ .filter((verb) => verb !== tape.primaryVerb)
68
+ .filter((verb) => teachingTexts.some((text) => {
69
+ if (!(0, types_1.appPlaytestTeachingTextMatchesVerb)(text, verb))
70
+ return false;
71
+ return verb !== 'tap' || !/\btap\b[^\n]{0,30}\b(?:start|play|begin|continue|retry)\b/i.test(text);
72
+ }));
73
+ if (competingVerbs.length > 0) {
74
+ throw new Error(`agent_task_primary_verb_teaching_mismatch:${tape.primaryVerb}:${competingVerbs.join(',')}`);
75
+ }
76
+ return matchingText;
77
+ }
@@ -0,0 +1,28 @@
1
+ import type { AppSurface } from '@playdrop/types';
2
+ import type { AppTask } from '../catalogue';
3
+ import { type AppBuildArtifacts } from './build';
4
+ export type StaticHtmlMetadata = {
5
+ title: string;
6
+ description: string | null;
7
+ imagePath: string | null;
8
+ warnings: string[];
9
+ };
10
+ export type PreparedStaticHtmlProject = {
11
+ sourceTask: AppTask;
12
+ runtimeTask: AppTask;
13
+ artifacts: AppBuildArtifacts;
14
+ sourceHash: string;
15
+ runtimeHash: string;
16
+ metadata: StaticHtmlMetadata;
17
+ runtimeDir: string;
18
+ cleanup: () => Promise<void>;
19
+ };
20
+ export declare function parseStaticHtmlMetadata(projectDir: string, entryPoint?: string): StaticHtmlMetadata;
21
+ export declare function buildStaticGameSlug(title: string, taskId?: number): string;
22
+ export declare function prepareStaticHtmlProject(input: {
23
+ projectDir: string;
24
+ taskId?: number;
25
+ appName?: string;
26
+ primarySurface: AppSurface;
27
+ }): Promise<PreparedStaticHtmlProject>;
28
+ export declare function sha256File(filePath: string): string;