@playdrop/playdrop-cli 0.14.0 → 0.14.2

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.
@@ -35,7 +35,6 @@ exports.classifyWorkerEventDrainError = classifyWorkerEventDrainError;
35
35
  exports.isWorkerTransientNetworkError = isWorkerTransientNetworkError;
36
36
  exports.classifyWorkerHeartbeatError = classifyWorkerHeartbeatError;
37
37
  exports.assertWorkerClaimErrorRetryable = assertWorkerClaimErrorRetryable;
38
- exports.resolvePlaydropAssetRequirementFromText = resolvePlaydropAssetRequirementFromText;
39
38
  exports.resolvePlaywrightBrowsersPathFromExecutable = resolvePlaywrightBrowsersPathFromExecutable;
40
39
  exports.loadWorkerEnvFile = loadWorkerEnvFile;
41
40
  exports.buildWorkerCapabilities = buildWorkerCapabilities;
@@ -686,6 +685,18 @@ function normalizeWorkerInstrumentSurface(value) {
686
685
  ? value
687
686
  : null;
688
687
  }
688
+ const LEGACY_REVIEW_SURFACE_PRIORITY = [
689
+ 'MOBILE_PORTRAIT',
690
+ 'MOBILE_LANDSCAPE',
691
+ 'DESKTOP',
692
+ ];
693
+ function normalizeWorkerInstrumentSurfaceTargets(value) {
694
+ if (!Array.isArray(value))
695
+ return [];
696
+ return Array.from(new Set(value
697
+ .map(normalizeWorkerInstrumentSurface)
698
+ .filter((surface) => Boolean(surface))));
699
+ }
689
700
  function resolveWorkerInstrumentSurface(taskContext) {
690
701
  const playdrop = taskContext.metadata.playdrop;
691
702
  if (!playdrop || typeof playdrop !== 'object' || Array.isArray(playdrop)) {
@@ -714,7 +725,12 @@ function resolveWorkerInstrumentSurface(taskContext) {
714
725
  if (!reviewTarget || typeof reviewTarget !== 'object' || Array.isArray(reviewTarget)) {
715
726
  return null;
716
727
  }
717
- return normalizeWorkerInstrumentSurface(reviewTarget.primarySurface);
728
+ const target = reviewTarget;
729
+ const primarySurface = normalizeWorkerInstrumentSurface(target.primarySurface);
730
+ if (primarySurface)
731
+ return primarySurface;
732
+ const surfaceTargets = normalizeWorkerInstrumentSurfaceTargets(target.surfaceTargets);
733
+ return LEGACY_REVIEW_SURFACE_PRIORITY.find((surface) => surfaceTargets.includes(surface)) ?? null;
718
734
  }
719
735
  if (taskContext.kind === 'GAME_EVAL') {
720
736
  const evalTarget = metadata.evalTarget;
@@ -958,12 +974,15 @@ function normalizeTaskClaimSlugPrimarySurface(value) {
958
974
  }
959
975
  return primarySurface;
960
976
  }
961
- function readTaskNextStepsFile(filePath) {
977
+ function resolveTaskWorkspaceInputPath(filePath, workspaceDir) {
978
+ return node_path_1.default.isAbsolute(filePath) ? node_path_1.default.normalize(filePath) : node_path_1.default.resolve(workspaceDir, filePath);
979
+ }
980
+ function readTaskNextStepsFile(filePath, workspaceDir = node_process_1.default.cwd()) {
962
981
  const normalizedPath = typeof filePath === 'string' ? filePath.trim() : '';
963
982
  if (!normalizedPath) {
964
983
  return undefined;
965
984
  }
966
- const resolved = node_path_1.default.resolve(normalizedPath);
985
+ const resolved = resolveTaskWorkspaceInputPath(normalizedPath, workspaceDir);
967
986
  if (!(0, node_fs_1.existsSync)(resolved)) {
968
987
  throw new Error('invalid_agent_task_next_steps_file');
969
988
  }
@@ -1038,7 +1057,7 @@ async function recordRejectedNextStepsFile(input) {
1038
1057
  }
1039
1058
  async function readTaskNextStepsFileForCommand(input) {
1040
1059
  try {
1041
- return readTaskNextStepsFile(input.filePath);
1060
+ return readTaskNextStepsFile(input.filePath, input.workspaceDir);
1042
1061
  }
1043
1062
  catch (error) {
1044
1063
  const code = error instanceof Error ? error.message : String(error);
@@ -1609,9 +1628,6 @@ function assertWorkerClaimErrorRetryable(error) {
1609
1628
  throw new Error(`worker_claim_forbidden: the server rejected this worker session (${error.message}).`);
1610
1629
  }
1611
1630
  }
1612
- function resolvePlaydropAssetRequirementFromText(value) {
1613
- return (0, upload_1.resolveWorkerPlaydropAssetRequirement)(value);
1614
- }
1615
1631
  function readCodexAuthenticated(homeDir = node_os_1.default.homedir()) {
1616
1632
  try {
1617
1633
  const parsed = JSON.parse((0, node_fs_1.readFileSync)(node_path_1.default.join(homeDir, '.codex', 'auth.json'), 'utf8'));
@@ -2592,6 +2608,8 @@ function buildAgentFailureCode(agent, result, request) {
2592
2608
  || providerStatus === 502
2593
2609
  || providerStatus === 503
2594
2610
  || providerStatus === 504
2611
+ || (agent === 'CLAUDE_CODE'
2612
+ && /\bResponse stalled mid-stream\.?(?:\s+The response above may be incomplete\.?)?/i.test(combinedOutput))
2595
2613
  || /\b(?:service|provider|api)\s+(?:is\s+)?(?:temporarily\s+)?unavailable\b/i.test(combinedOutput)
2596
2614
  || /\btoo many requests\b/i.test(combinedOutput)) {
2597
2615
  return `agent_provider_temporarily_unavailable:${provider}${providerStatus ? `:${providerStatus}` : ''}`;
@@ -5518,8 +5536,6 @@ async function uploadTask(options = {}) {
5518
5536
  expectedSubtitle: taskContext.outputSubtitle,
5519
5537
  expectedPrimarySurface: taskContext.outputPrimarySurface,
5520
5538
  remixSourceRef: taskContext.remixSourceRef ?? null,
5521
- playdropAssetRequirement: (0, upload_1.resolveWorkerPlaydropAssetRequirement)(taskContext.creatorRequest),
5522
- creatorRequest: taskContext.creatorRequest,
5523
5539
  projectDir,
5524
5540
  creatorUsername: taskContext.creatorUsername,
5525
5541
  apiBase: ctx.envConfig.apiBase,
@@ -5559,8 +5575,6 @@ async function uploadTask(options = {}) {
5559
5575
  expectedSubtitle: taskContext.outputSubtitle,
5560
5576
  expectedPrimarySurface: taskContext.outputPrimarySurface,
5561
5577
  remixSourceRef: taskContext.remixSourceRef ?? null,
5562
- playdropAssetRequirement: (0, upload_1.resolveWorkerPlaydropAssetRequirement)(taskContext.creatorRequest),
5563
- creatorRequest: taskContext.creatorRequest,
5564
5578
  projectDir,
5565
5579
  creatorUsername: taskContext.creatorUsername,
5566
5580
  apiBase: ctx.envConfig.apiBase,
@@ -5643,6 +5657,7 @@ async function claimSlugTask(options) {
5643
5657
  }
5644
5658
  async function completeTask(options) {
5645
5659
  const taskContext = readTaskContextFile();
5660
+ const workspaceDir = resolveTaskWorkspaceDir();
5646
5661
  if (taskContext.kind === 'GAME_REVIEW' || taskContext.kind === 'GAME_EVAL') {
5647
5662
  throw new Error('task_done_not_allowed_for_game_review');
5648
5663
  }
@@ -5668,6 +5683,7 @@ async function completeTask(options) {
5668
5683
  }
5669
5684
  const nextSteps = await readTaskNextStepsFileForCommand({
5670
5685
  filePath: options.nextSteps,
5686
+ workspaceDir,
5671
5687
  command: 'task done',
5672
5688
  ctx,
5673
5689
  taskContext,
@@ -5675,7 +5691,7 @@ async function completeTask(options) {
5675
5691
  if (nextSteps === NEXT_STEPS_READ_FAILED) {
5676
5692
  return;
5677
5693
  }
5678
- const platformFeedback = readPlatformFeedbackFile(options.platformFeedbackFile);
5694
+ const platformFeedback = readPlatformFeedbackFile(options.platformFeedbackFile, workspaceDir);
5679
5695
  try {
5680
5696
  await ctx.client.workerCompleteAgentTask(taskContext.taskId, {
5681
5697
  taskToken: taskContext.taskToken,
@@ -5714,18 +5730,6 @@ async function completeTask(options) {
5714
5730
  }
5715
5731
  (0, output_1.printSuccess)('Task marked done.');
5716
5732
  }
5717
- function normalizeReviewSubmitState(value) {
5718
- const normalized = typeof value === 'string' ? value.trim().toUpperCase() : '';
5719
- if (normalized !== 'PASSED'
5720
- && normalized !== 'GOOD'
5721
- && normalized !== 'EXCELLENT'
5722
- && normalized !== 'LOW_QUALITY'
5723
- && normalized !== 'FAILED'
5724
- && normalized !== 'ERROR') {
5725
- throw new Error('invalid_review_state');
5726
- }
5727
- return normalized;
5728
- }
5729
5733
  function readRequiredTextFile(filePath, code) {
5730
5734
  const normalizedPath = typeof filePath === 'string' ? filePath.trim() : '';
5731
5735
  if (!normalizedPath) {
@@ -5753,13 +5757,13 @@ function readOptionalTextFile(filePath) {
5753
5757
  const content = (0, node_fs_1.readFileSync)(resolved, 'utf8').trim();
5754
5758
  return content || null;
5755
5759
  }
5756
- function readPlatformFeedbackFile(filePath) {
5760
+ function readPlatformFeedbackFile(filePath, workspaceDir) {
5757
5761
  const normalizedPath = typeof filePath === 'string' ? filePath.trim() : '';
5758
5762
  if (!normalizedPath) {
5759
5763
  return null;
5760
5764
  }
5761
5765
  try {
5762
- const resolved = node_path_1.default.resolve(normalizedPath);
5766
+ const resolved = resolveTaskWorkspaceInputPath(normalizedPath, workspaceDir);
5763
5767
  if (!(0, node_fs_1.existsSync)(resolved)) {
5764
5768
  throw new Error('file_missing');
5765
5769
  }
@@ -5839,27 +5843,23 @@ function resolveReviewEvidenceDir(evidenceDir) {
5839
5843
  }
5840
5844
  async function submitReviewTask(options) {
5841
5845
  const taskContext = readTaskContextFile();
5846
+ const workspaceDir = resolveTaskWorkspaceDir();
5842
5847
  if (taskContext.kind !== 'GAME_REVIEW') {
5843
5848
  throw new Error('task_submit_review_requires_game_review');
5844
5849
  }
5845
- const state = normalizeReviewSubmitState(options.state);
5850
+ const state = (0, review_1.normalizeTerminalReviewState)(options.state);
5846
5851
  const reviewMessageFile = resolveReviewFilePath(options.messageFile, 'invalid_review_message_file');
5847
- const creatorFeedbackFile = state === 'ERROR'
5848
- ? null
5849
- : resolveReviewFilePath(options.creatorFeedbackFile, 'invalid_creator_feedback_file');
5852
+ const creatorFeedbackFile = resolveReviewFilePath(options.creatorFeedbackFile, 'invalid_creator_feedback_file');
5850
5853
  const evidenceDir = resolveReviewEvidenceDir(options.evidenceDir);
5851
5854
  const reviewMessage = readRequiredTextFile(reviewMessageFile, 'invalid_review_message_file');
5852
- const creatorFeedback = state === 'ERROR'
5853
- ? null
5854
- : readOptionalTextFile(creatorFeedbackFile ?? undefined);
5855
- const platformFeedback = readPlatformFeedbackFile(options.platformFeedbackFile);
5855
+ const creatorFeedback = readOptionalTextFile(creatorFeedbackFile);
5856
+ const platformFeedback = readPlatformFeedbackFile(options.platformFeedbackFile, workspaceDir);
5856
5857
  await (0, review_1.validateGameReviewResult)({
5857
5858
  reviewState: state,
5858
5859
  reviewMessage,
5859
5860
  creatorFeedback: creatorFeedback ?? '',
5860
5861
  evidenceDir,
5861
5862
  });
5862
- const workspaceDir = resolveTaskWorkspaceDir();
5863
5863
  const instrumentEvidence = (0, instrument_evidence_1.readAndValidateInstrumentEvidenceManifest)({ workspaceDir, evidenceDir });
5864
5864
  const evidenceFiles = readReviewEvidenceFiles(evidenceDir, workspaceDir);
5865
5865
  const ctx = await resolveTaskCommandContext('task submit-review', options.env, taskContext);
@@ -5868,6 +5868,7 @@ async function submitReviewTask(options) {
5868
5868
  }
5869
5869
  const nextSteps = await readTaskNextStepsFileForCommand({
5870
5870
  filePath: options.nextSteps,
5871
+ workspaceDir,
5871
5872
  command: 'task submit-review',
5872
5873
  ctx,
5873
5874
  taskContext,
@@ -6005,11 +6006,12 @@ async function failTask(options) {
6005
6006
  return;
6006
6007
  }
6007
6008
  const taskContext = readTaskContextFile();
6009
+ const workspaceDir = resolveTaskWorkspaceDir();
6008
6010
  const ctx = await resolveTaskCommandContext('task fail', options.env, taskContext);
6009
6011
  if (!ctx) {
6010
6012
  return;
6011
6013
  }
6012
- const platformFeedback = readPlatformFeedbackFile(options.platformFeedbackFile);
6014
+ const platformFeedback = readPlatformFeedbackFile(options.platformFeedbackFile, workspaceDir);
6013
6015
  await ctx.client.workerFailAgentTask(taskContext.taskId, {
6014
6016
  taskToken: taskContext.taskToken,
6015
6017
  error: message,
package/dist/index.js CHANGED
@@ -289,7 +289,7 @@ task
289
289
  task
290
290
  .command('submit-review')
291
291
  .description('Submit the active game review task result')
292
- .requiredOption('--state <state>', 'Terminal review state')
292
+ .requiredOption('--state <state>', 'Review outcome: Blocked, Limited, Passed, Good, or Excellent')
293
293
  .requiredOption('--message-file <path>', 'Internal assessment file')
294
294
  .option('--creator-feedback-file <path>', 'Creator feedback file')
295
295
  .option('--platform-feedback-file <path>', 'Markdown feedback about the PlayDrop platform')
@@ -339,7 +339,7 @@ const review = program.command('review').description('Review helper commands for
339
339
  review
340
340
  .command('validate-result')
341
341
  .description('Validate a canonical game review result')
342
- .requiredOption('--state <state>', 'Terminal review state')
342
+ .requiredOption('--state <state>', 'Review outcome: Blocked, Limited, Passed, Good, or Excellent')
343
343
  .requiredOption('--message-file <path>', 'Internal assessment file')
344
344
  .option('--creator-feedback-file <path>', 'Creator feedback file')
345
345
  .requiredOption('--evidence-dir <path>', 'Review evidence directory')
@@ -1038,14 +1038,12 @@ project
1038
1038
  .option('--app <name>', 'App name when the target is a workspace')
1039
1039
  .option('--timeout <seconds>', 'Load-check timeout in seconds')
1040
1040
  .option('--screenshot <path>', 'Screenshot output path')
1041
- .option('--actions <path>', 'JSON array of click {x,y}, press {key}, and wait {ms <= 60000} actions')
1042
1041
  .option('--tape <surface>', 'Run matched idle and catalogue playtest-tape captures on a declared surface')
1043
1042
  .action(async (target, opts = {}) => {
1044
1043
  await (0, check_1.check)(target, {
1045
1044
  app: opts.app,
1046
1045
  timeout: opts.timeout,
1047
1046
  screenshot: opts.screenshot,
1048
- actions: opts.actions,
1049
1047
  tape: opts.tape,
1050
1048
  });
1051
1049
  });
@@ -17,6 +17,9 @@ export declare function readMediaCaptureProof(input: {
17
17
  }): RecordAppUploadMediaCaptureRequest;
18
18
  export declare function readWorkerUploadPreflightContext(projectDir: string): WorkerUploadPreflightContext | null;
19
19
  export declare function assertAgentGameHeroListingPreflight(task: AppTask): void;
20
+ export declare function assertAgentGameIconListingPreflight(task: AppTask): void;
21
+ export declare function assertAgentGameVideoListingPreflight(task: AppTask): void;
22
+ export declare function assertAgentGamePreviewListingPreflight(task: AppTask): void;
20
23
  export declare function assertAgentGameScreenshotListingPreflight(task: AppTask): void;
21
24
  export declare function assertAgentGameCaptureReportPreflight(task: AppTask, options: {
22
25
  requireCaptureReport: boolean;
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.readMediaCaptureProof = readMediaCaptureProof;
4
4
  exports.readWorkerUploadPreflightContext = readWorkerUploadPreflightContext;
5
5
  exports.assertAgentGameHeroListingPreflight = assertAgentGameHeroListingPreflight;
6
+ exports.assertAgentGameIconListingPreflight = assertAgentGameIconListingPreflight;
7
+ exports.assertAgentGameVideoListingPreflight = assertAgentGameVideoListingPreflight;
8
+ exports.assertAgentGamePreviewListingPreflight = assertAgentGamePreviewListingPreflight;
6
9
  exports.assertAgentGameScreenshotListingPreflight = assertAgentGameScreenshotListingPreflight;
7
10
  exports.assertAgentGameCaptureReportPreflight = assertAgentGameCaptureReportPreflight;
8
11
  exports.assertAgentGameListingPreflight = assertAgentGameListingPreflight;
@@ -37,9 +40,12 @@ function readPngDimensions(filePath, errorCode = 'agent_task_listing_hero_art_in
37
40
  }
38
41
  return { width, height };
39
42
  }
40
- function assertListingIcon(task) {
43
+ function assertListingIcon(task, required = false) {
41
44
  const iconPath = task.listing?.iconPath;
42
45
  if (!iconPath) {
46
+ if (required) {
47
+ throw new Error('agent_task_listing_icon_missing: agent game listings must include a dedicated square PNG through listing.icon.');
48
+ }
43
49
  return;
44
50
  }
45
51
  const icon = readPngDimensions(iconPath, 'listing_icon_invalid');
@@ -116,15 +122,6 @@ function assertScreenshotPath(task, filePath, field) {
116
122
  function assertListingScreenshots(task) {
117
123
  const portraitPaths = task.listing?.screenshotPortraitPaths ?? [];
118
124
  const landscapePaths = task.listing?.screenshotLandscapePaths ?? [];
119
- if (portraitPaths.length === 0 && landscapePaths.length === 0) {
120
- throw new Error('agent_task_listing_screenshots_missing: new game tasks must include listing.screenshotsPortrait and/or listing.screenshotsLandscape PNG marketing images.');
121
- }
122
- if (task.surfaceTargets.includes('MOBILE_PORTRAIT') && portraitPaths.length === 0) {
123
- throw new Error('agent_task_listing_screenshots_missing: mobile portrait games must include at least one listing.screenshotsPortrait PNG marketing image.');
124
- }
125
- if ((task.surfaceTargets.includes('DESKTOP') || task.surfaceTargets.includes('MOBILE_LANDSCAPE')) && landscapePaths.length === 0) {
126
- throw new Error('agent_task_listing_screenshots_missing: desktop or landscape games must include at least one listing.screenshotsLandscape PNG marketing image.');
127
- }
128
125
  for (const filePath of portraitPaths) {
129
126
  assertScreenshotPath(task, filePath, 'listing.screenshotsPortrait');
130
127
  readPngDimensions(filePath);
@@ -134,6 +131,18 @@ function assertListingScreenshots(task) {
134
131
  readPngDimensions(filePath);
135
132
  }
136
133
  }
134
+ function assertListingGameplayVideo(task) {
135
+ const videoCount = (task.listing?.videoPortraitPaths?.length ?? 0)
136
+ + (task.listing?.videoLandscapePaths?.length ?? 0);
137
+ if (videoCount === 0) {
138
+ throw new Error('agent_task_listing_video_missing: agent game listings must include at least one real gameplay video through listing.videosPortrait or listing.videosLandscape.');
139
+ }
140
+ }
141
+ function assertListingPreviewMode(task) {
142
+ if (task.previewable !== true) {
143
+ throw new Error('agent_task_listing_preview_missing: agent game listings must set previewable: true and implement the real gameplay preview contract before capture.');
144
+ }
145
+ }
137
146
  function isSha256Hex(value) {
138
147
  return typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value.trim());
139
148
  }
@@ -158,6 +167,12 @@ function readMediaCaptureProof(input) {
158
167
  if (!binding || typeof binding !== 'object' || Array.isArray(binding)) {
159
168
  throw new Error(`agent_task_media_capture_report_invalid: ${reportPath} is missing binding.`);
160
169
  }
170
+ if (report.appName !== input.task.name) {
171
+ throw new Error(`agent_task_media_capture_wrong_app: listing.captureReport is for ${String(report.appName || 'unknown')}, not ${input.task.name}; rerun "playdrop project capture".`);
172
+ }
173
+ if (binding.pixelValidation !== 'passed') {
174
+ throw new Error(`agent_task_media_capture_pixels_unvalidated: ${reportPath} must come from a capture that passed wrong-game, black-frame, and motion checks.`);
175
+ }
161
176
  if (input.expectedRuntimeBundleHash) {
162
177
  const capturedRuntimeBundleHash = typeof binding.runtimeBundleHash === 'string'
163
178
  ? binding.runtimeBundleHash.trim().toLowerCase()
@@ -309,6 +324,15 @@ function readWorkerUploadPreflightContext(projectDir) {
309
324
  function assertAgentGameHeroListingPreflight(task) {
310
325
  assertHeroListingArt(task);
311
326
  }
327
+ function assertAgentGameIconListingPreflight(task) {
328
+ assertListingIcon(task, true);
329
+ }
330
+ function assertAgentGameVideoListingPreflight(task) {
331
+ assertListingGameplayVideo(task);
332
+ }
333
+ function assertAgentGamePreviewListingPreflight(task) {
334
+ assertListingPreviewMode(task);
335
+ }
312
336
  function assertAgentGameScreenshotListingPreflight(task) {
313
337
  assertListingScreenshots(task);
314
338
  }
@@ -322,7 +346,10 @@ function assertAgentGameCaptureReportPreflight(task, options) {
322
346
  }
323
347
  }
324
348
  function assertAgentGameListingPreflight(task, options) {
349
+ assertAgentGameIconListingPreflight(task);
325
350
  assertAgentGameHeroListingPreflight(task);
351
+ assertAgentGamePreviewListingPreflight(task);
352
+ assertAgentGameVideoListingPreflight(task);
326
353
  assertAgentGameScreenshotListingPreflight(task);
327
354
  assertAgentGameCaptureReportPreflight(task, options);
328
355
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.14.0",
2
+ "version": "0.14.2",
3
3
  "build": 1,
4
- "runtimeSdkVersion": "0.14.0",
4
+ "runtimeSdkVersion": "0.14.2",
5
5
  "runtimeSdkBuild": 1,
6
6
  "clients": {
7
7
  "all": {
@@ -1 +1 @@
1
- {"version":3,"file":"app-playtest-tape.d.ts","sourceRoot":"","sources":["../src/app-playtest-tape.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,UAAU,EAEhB,MAAM,UAAU,CAAC;AAElB,eAAO,MAAM,yBAAyB,EAAG,CAAU,CAAC;AACpD,eAAO,MAAM,oCAAoC,KAAK,CAAC;AAEvD,eAAO,MAAM,gCAAgC,0CAA2C,CAAC;AACzF,MAAM,MAAM,sBAAsB,GAAG,OAAO,gCAAgC,CAAC,MAAM,CAAC,CAAC;AASrF,wBAAgB,kCAAkC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAEtG;AAED,eAAO,MAAM,uCAAuC,sFAK1C,CAAC;AAEX,MAAM,MAAM,4BAA4B,GAAG,OAAO,uCAAuC,CAAC,MAAM,CAAC,CAAC;AAElG,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,4BAA4B,CAAC;IACnC,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,KAAK,qBAAqB,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,qBAAqB,GAAG;IAC5D,IAAI,EAAE,aAAa,GAAG,aAAa,GAAG,WAAW,CAAC;IAClD,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,qBAAqB,GAAG;IACxD,IAAI,EAAE,KAAK,CAAC;IACZ,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,qBAAqB,GAAG;IAC7D,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAC5B,uBAAuB,GACvB,mBAAmB,GACnB,wBAAwB,CAAC;AAE7B,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,OAAO,yBAAyB,CAAC;IAC1C,WAAW,EAAE,sBAAsB,CAAC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,cAAc,EAAE,wBAAwB,EAAE,CAAC;IAC3C,MAAM,EAAE,oBAAoB,EAAE,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC,CAAC;AAE5E,MAAM,MAAM,+BAA+B,GACvC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,gBAAgB,CAAA;CAAE,GACrC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AA0PjC,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,OAAO,EACd,OAAO,GAAE;IACP,cAAc,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IACvC,eAAe,CAAC,EAAE,OAAO,CAAC;CACtB,GACL,+BAA+B,CAoCjC"}
1
+ {"version":3,"file":"app-playtest-tape.d.ts","sourceRoot":"","sources":["../src/app-playtest-tape.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,UAAU,EAEhB,MAAM,UAAU,CAAC;AAElB,eAAO,MAAM,yBAAyB,EAAG,CAAU,CAAC;AACpD,eAAO,MAAM,oCAAoC,KAAK,CAAC;AAEvD,eAAO,MAAM,gCAAgC,0CAA2C,CAAC;AACzF,MAAM,MAAM,sBAAsB,GAAG,OAAO,gCAAgC,CAAC,MAAM,CAAC,CAAC;AASrF,wBAAgB,kCAAkC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAEtG;AAED,eAAO,MAAM,uCAAuC,sFAK1C,CAAC;AAEX,MAAM,MAAM,4BAA4B,GAAG,OAAO,uCAAuC,CAAC,MAAM,CAAC,CAAC;AAElG,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,4BAA4B,CAAC;IACnC,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,KAAK,qBAAqB,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,qBAAqB,GAAG;IAC5D,IAAI,EAAE,aAAa,GAAG,aAAa,GAAG,WAAW,CAAC;IAClD,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,qBAAqB,GAAG;IACxD,IAAI,EAAE,KAAK,CAAC;IACZ,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,qBAAqB,GAAG;IAC7D,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAC5B,uBAAuB,GACvB,mBAAmB,GACnB,wBAAwB,CAAC;AAE7B,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,OAAO,yBAAyB,CAAC;IAC1C,WAAW,EAAE,sBAAsB,CAAC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,cAAc,EAAE,wBAAwB,EAAE,CAAC;IAC3C,MAAM,EAAE,oBAAoB,EAAE,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC,CAAC;AAE5E,MAAM,MAAM,+BAA+B,GACvC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,gBAAgB,CAAA;CAAE,GACrC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAgQjC,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,OAAO,EACd,OAAO,GAAE;IACP,cAAc,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IACvC,eAAe,CAAC,EAAE,OAAO,CAAC;CACtB,GACL,+BAA+B,CAoCjC"}
@@ -106,7 +106,8 @@ function normalizeEvent(value, surface, durationMs, index) {
106
106
  if (surface !== "DESKTOP") {
107
107
  throw new Error(`keyboard_input_not_allowed_on_mobile_tape:${surface}:${index}`);
108
108
  }
109
- const key = typeof value["key"] === "string" ? value["key"].trim() : "";
109
+ const rawKey = typeof value["key"] === "string" ? value["key"] : "";
110
+ const key = rawKey === " " ? rawKey : rawKey.trim();
110
111
  if (!key || key.length > 64) {
111
112
  throw new Error(`invalid_playtest_tape_key:${surface}:${index}`);
112
113
  }
@@ -242,30 +243,37 @@ function normalizeTape(value, surface) {
242
243
  if (primaryVerb === "tap")
243
244
  return event.type === "tap";
244
245
  if (primaryVerb === "key")
245
- return event.type === "keyDown" || event.type === "keyUp";
246
+ return event.type === "keyDown";
246
247
  return event.type === "pointerDown" || event.type === "pointerMove" || event.type === "pointerUp";
247
248
  };
248
- const mismatchedEventIndex = postStartEvents.findIndex((event) => !postStartEventMatchesVerb(event));
249
- if (mismatchedEventIndex >= 0) {
250
- throw new Error(`playtest_tape_primary_verb_event_mismatch:${surface}:${primaryVerb}:${startOnlyEventCount + mismatchedEventIndex}`);
249
+ if (!postStartEvents.some(postStartEventMatchesVerb)) {
250
+ throw new Error(`playtest_tape_primary_verb_events_required:${surface}:${primaryVerb}`);
251
251
  }
252
252
  if (primaryVerb === "swipe" || primaryVerb === "drag") {
253
- let actionHasMove = false;
254
- let actionOpen = false;
255
- for (let index = 0; index < postStartEvents.length; index += 1) {
256
- const event = postStartEvents[index];
253
+ const openGestures = /* @__PURE__ */ new Set();
254
+ const movedGestures = /* @__PURE__ */ new Set();
255
+ let completePrimaryGesture = false;
256
+ for (const event of postStartEvents) {
257
+ if (event.type !== "pointerDown" && event.type !== "pointerMove" && event.type !== "pointerUp")
258
+ continue;
259
+ const pointerId = event.pointerId ?? 0;
257
260
  if (event.type === "pointerDown") {
258
- actionOpen = true;
259
- actionHasMove = false;
261
+ openGestures.add(pointerId);
262
+ movedGestures.delete(pointerId);
260
263
  } else if (event.type === "pointerMove") {
261
- actionHasMove = true;
264
+ if (openGestures.has(pointerId))
265
+ movedGestures.add(pointerId);
262
266
  } else if (event.type === "pointerUp") {
263
- if (!actionOpen || !actionHasMove) {
264
- throw new Error(`playtest_tape_primary_verb_gesture_incomplete:${surface}:${primaryVerb}:${startOnlyEventCount + index}`);
267
+ if (openGestures.has(pointerId) && movedGestures.has(pointerId)) {
268
+ completePrimaryGesture = true;
265
269
  }
266
- actionOpen = false;
270
+ openGestures.delete(pointerId);
271
+ movedGestures.delete(pointerId);
267
272
  }
268
273
  }
274
+ if (!completePrimaryGesture) {
275
+ throw new Error(`playtest_tape_primary_verb_gesture_required:${surface}:${primaryVerb}`);
276
+ }
269
277
  }
270
278
  return {
271
279
  version: APP_PLAYTEST_TAPE_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playdrop/playdrop-cli",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "description": "Official Playdrop CLI for publishing browser games, creator apps, and AI-generated game assets on playdrop.ai",
5
5
  "homepage": "https://www.playdrop.ai",
6
6
  "repository": {