@playcraft/cli 0.1.27 → 0.1.29

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 (314) hide show
  1. package/README.md +3 -38
  2. package/dist/agent/local-backend.js +1 -1
  3. package/dist/cli-root-help.js +0 -2
  4. package/dist/commands/build.js +4 -9
  5. package/dist/commands/create.js +33 -26
  6. package/dist/commands/image.js +6 -2
  7. package/dist/commands/login.js +42 -84
  8. package/dist/commands/skills.js +0 -2
  9. package/dist/commands/tools-3d.js +0 -11
  10. package/dist/commands/tools-generation.js +109 -68
  11. package/dist/commands/tools-image-context.js +23 -0
  12. package/dist/commands/tools.js +2 -2
  13. package/dist/config.js +4 -59
  14. package/dist/index.js +3 -34
  15. package/dist/remix/clone.js +0 -2
  16. package/dist/remix/init-template.js +0 -2
  17. package/dist/remix/pull.js +0 -2
  18. package/dist/remix/push.js +1 -21
  19. package/dist/utils/agent-api-client.js +30 -185
  20. package/dist/utils/image-asset-context.js +105 -0
  21. package/dist/utils/playcraft-http-client.js +4 -6
  22. package/package.json +5 -14
  23. package/project-template/.claude/skills/playcraft-image-generation/SKILL.md +81 -23
  24. package/project-templates/phaser/.claude/skills/playcraft-image-generation/SKILL.md +81 -23
  25. package/project-templates/threejs/.claude/skills/playcraft-image-generation/SKILL.md +81 -23
  26. package/dist/auth/account-activation.js +0 -157
  27. package/dist/auth/messages.js +0 -30
  28. package/dist/commands/platform-skills.generated.js +0 -19
  29. package/dist/commands/playable.js +0 -53
  30. package/dist/commands/tools-batch.js +0 -87
  31. package/dist/commands/video.js +0 -111
  32. package/dist/commands/workspace-runtime.js +0 -54
  33. package/dist/project-skills/builtin-import.js +0 -225
  34. package/dist/project-skills/commands.js +0 -77
  35. package/dist/project-skills/lifecycle.js +0 -28
  36. package/dist/project-skills/local-run.js +0 -112
  37. package/dist/project-skills/local-store.js +0 -221
  38. package/dist/project-skills/messages.js +0 -80
  39. package/dist/project-skills/reconcile.js +0 -62
  40. package/dist/project-skills/remote-cache.js +0 -51
  41. package/dist/project-skills/validation.js +0 -19
  42. package/dist/recording/messages.js +0 -40
  43. package/dist/recording/project-digest.js +0 -85
  44. package/dist/recording/recorder.js +0 -293
  45. package/dist/recording/scenario.js +0 -50
  46. package/dist/utils/environment-endpoints.js +0 -17
  47. package/dist/utils/tool-operation-journal.js +0 -124
  48. package/dist/utils/video-evidence-git.js +0 -81
  49. package/dist/video-replication/client.js +0 -222
  50. package/dist/video-replication/comparison-evidence.js +0 -149
  51. package/dist/video-replication/comparison-files.js +0 -57
  52. package/dist/video-replication/comparison-log.js +0 -72
  53. package/dist/video-replication/comparison-messages.js +0 -104
  54. package/dist/video-replication/comparison-output.js +0 -63
  55. package/dist/video-replication/comparison-preflight.js +0 -66
  56. package/dist/video-replication/comparison.js +0 -216
  57. package/dist/video-replication/evidence.js +0 -165
  58. package/dist/video-replication/media.js +0 -111
  59. package/dist/video-replication/messages.js +0 -89
  60. package/dist/workspace-runtime/codex/app-server.js +0 -636
  61. package/dist/workspace-runtime/codex/config-toml.js +0 -107
  62. package/dist/workspace-runtime/codex/jsonrpc-stdio.js +0 -123
  63. package/dist/workspace-runtime/codex/launcher-client.js +0 -163
  64. package/dist/workspace-runtime/codex/loopback.js +0 -143
  65. package/dist/workspace-runtime/codex/model-broker.js +0 -258
  66. package/dist/workspace-runtime/codex/native-adapter.js +0 -9
  67. package/dist/workspace-runtime/main.js +0 -10
  68. package/dist/workspace-runtime/persistence/journal.js +0 -355
  69. package/dist/workspace-runtime/persistence/native-subagents.js +0 -162
  70. package/dist/workspace-runtime/persistence/sqlite-client.js +0 -77
  71. package/dist/workspace-runtime/persistence/sqlite-worker.js +0 -1057
  72. package/dist/workspace-runtime/previews/publish.js +0 -259
  73. package/dist/workspace-runtime/previews/single-file.js +0 -43
  74. package/dist/workspace-runtime/processes/managed-writes.js +0 -69
  75. package/dist/workspace-runtime/production/contracts.js +0 -17
  76. package/dist/workspace-runtime/production/controller.js +0 -207
  77. package/dist/workspace-runtime/production/dependencies.js +0 -60
  78. package/dist/workspace-runtime/production/native.js +0 -90
  79. package/dist/workspace-runtime/production/processes.js +0 -86
  80. package/dist/workspace-runtime/production/save-command.js +0 -24
  81. package/dist/workspace-runtime/production/source-copy.js +0 -91
  82. package/dist/workspace-runtime/production/stages.js +0 -202
  83. package/dist/workspace-runtime/production/storage.js +0 -32
  84. package/dist/workspace-runtime/serve.js +0 -202
  85. package/dist/workspace-runtime/server/auth.js +0 -21
  86. package/dist/workspace-runtime/server/connectivity.js +0 -201
  87. package/dist/workspace-runtime/server/desktop-host.js +0 -429
  88. package/dist/workspace-runtime/server/desktop-preview-config.js +0 -81
  89. package/dist/workspace-runtime/server/desktop-preview-service.js +0 -157
  90. package/dist/workspace-runtime/server/desktop-preview-static.js +0 -53
  91. package/dist/workspace-runtime/server/desktop-preview.js +0 -189
  92. package/dist/workspace-runtime/server/dispatch.js +0 -903
  93. package/dist/workspace-runtime/server/endpoints.js +0 -19
  94. package/dist/workspace-runtime/server/execution-group.js +0 -85
  95. package/dist/workspace-runtime/server/http.js +0 -984
  96. package/dist/workspace-runtime/server/lock.js +0 -53
  97. package/dist/workspace-runtime/server/rfb-watch-gate.js +0 -168
  98. package/dist/workspace-runtime/server/terminal-host.js +0 -294
  99. package/dist/workspace-runtime/server/types.js +0 -1
  100. package/dist/workspace-runtime/workspaces/artifact-events.js +0 -55
  101. package/dist/workspace-runtime/workspaces/artifact-files.js +0 -195
  102. package/dist/workspace-runtime/workspaces/artifact-receipts.js +0 -89
  103. package/dist/workspace-runtime/workspaces/artifacts.js +0 -260
  104. package/dist/workspace-runtime/workspaces/attachment-content.js +0 -111
  105. package/dist/workspace-runtime/workspaces/attachments.js +0 -175
  106. package/dist/workspace-runtime/workspaces/batch-commit.js +0 -142
  107. package/dist/workspace-runtime/workspaces/cli-context.js +0 -23
  108. package/dist/workspace-runtime/workspaces/cli-credentials.js +0 -111
  109. package/dist/workspace-runtime/workspaces/context-error.js +0 -2
  110. package/dist/workspace-runtime/workspaces/dependencies.js +0 -264
  111. package/dist/workspace-runtime/workspaces/developer-command.js +0 -64
  112. package/dist/workspace-runtime/workspaces/directories.js +0 -113
  113. package/dist/workspace-runtime/workspaces/directory-access.js +0 -29
  114. package/dist/workspace-runtime/workspaces/directory-maintenance.js +0 -111
  115. package/dist/workspace-runtime/workspaces/directory-stats.js +0 -49
  116. package/dist/workspace-runtime/workspaces/disk-snapshot.js +0 -31
  117. package/dist/workspace-runtime/workspaces/empty-template.js +0 -40
  118. package/dist/workspace-runtime/workspaces/files.js +0 -120
  119. package/dist/workspace-runtime/workspaces/git-credential-broker.js +0 -312
  120. package/dist/workspace-runtime/workspaces/git-credential-helper.js +0 -68
  121. package/dist/workspace-runtime/workspaces/git-direct-store.js +0 -155
  122. package/dist/workspace-runtime/workspaces/git.js +0 -517
  123. package/dist/workspace-runtime/workspaces/json5-edit.js +0 -170
  124. package/dist/workspace-runtime/workspaces/parameters.js +0 -272
  125. package/dist/workspace-runtime/workspaces/prepare.js +0 -264
  126. package/dist/workspace-runtime/workspaces/registry.js +0 -226
  127. package/dist/workspace-runtime/workspaces/revisions.js +0 -31
  128. package/dist/workspace-runtime/workspaces/skill-artifact-loader.js +0 -91
  129. package/dist/workspace-runtime/workspaces/skills.js +0 -189
  130. package/dist/workspace-runtime/workspaces/video-attachments.js +0 -49
  131. package/project-template/.claude/skills/playcraft-replica-validation/SKILL.md +0 -25
  132. package/project-template/.claude/skills/playcraft-replica-validation/references/contracts.md +0 -79
  133. package/project-template/.claude/skills/playcraft-video-understanding/SKILL.md +0 -29
  134. package/project-template/.claude/skills/playcraft-video-understanding/references/contracts.md +0 -37
  135. package/project-template-v2/.claude/agents/artist.md +0 -82
  136. package/project-template-v2/.claude/agents/developer.md +0 -153
  137. package/project-template-v2/.claude/agents/game-designer.md +0 -264
  138. package/project-template-v2/.claude/agents/refs/artist-art-style-catalog.md +0 -533
  139. package/project-template-v2/.claude/agents/refs/artist-color-audio-recipes.md +0 -153
  140. package/project-template-v2/.claude/agents/refs/artist-dimension-axis.md +0 -27
  141. package/project-template-v2/.claude/agents/refs/artist-master-composite-recipes.md +0 -208
  142. package/project-template-v2/.claude/agents/refs/atom-skill-library.md +0 -81
  143. package/project-template-v2/.claude/agents/refs/developer-impl-cookbook.md +0 -432
  144. package/project-template-v2/.claude/agents/refs/framework-5-component-filter.md +0 -252
  145. package/project-template-v2/.claude/agents/refs/framework-game-feel-juice.md +0 -266
  146. package/project-template-v2/.claude/agents/refs/framework-mda.md +0 -147
  147. package/project-template-v2/.claude/agents/refs/game-designer-gameplay-sufficiency.md +0 -123
  148. package/project-template-v2/.claude/agents/refs/ta-3d-flip-recipe.md +0 -88
  149. package/project-template-v2/.claude/agents/refs/ta-atlas-deliverable-standard.md +0 -67
  150. package/project-template-v2/.claude/agents/refs/ta-batch-pipeline-recipes.md +0 -120
  151. package/project-template-v2/.claude/agents/refs/ta-image-generation-detail.md +0 -300
  152. package/project-template-v2/.claude/agents/refs/ta-image-ops-reference.md +0 -495
  153. package/project-template-v2/.claude/agents/refs/ta-pipeline-cookbook.md +0 -1141
  154. package/project-template-v2/.claude/agents/refs/ta-tools-reference.md +0 -111
  155. package/project-template-v2/.claude/agents/refs/ta-vfx-preset-catalog.md +0 -365
  156. package/project-template-v2/.claude/agents/refs/threejs-cannon-pitfalls.md +0 -412
  157. package/project-template-v2/.claude/agents/reviewer.md +0 -75
  158. package/project-template-v2/.claude/agents/technical-artist.md +0 -86
  159. package/project-template-v2/.claude/hooks/snapshot-milestone.mjs +0 -243
  160. package/project-template-v2/.claude/hooks/user-prompt.mjs +0 -133
  161. package/project-template-v2/.claude/settings.json +0 -33
  162. package/project-template-v2/.claude/skills/brainstorming/SKILL.md +0 -161
  163. package/project-template-v2/.claude/skills/brainstorming/scripts/frame-template.html +0 -270
  164. package/project-template-v2/.claude/skills/brainstorming/scripts/helper.js +0 -177
  165. package/project-template-v2/.claude/skills/brainstorming/scripts/server.cjs +0 -354
  166. package/project-template-v2/.claude/skills/brainstorming/scripts/start-server.sh +0 -148
  167. package/project-template-v2/.claude/skills/brainstorming/scripts/stop-server.sh +0 -56
  168. package/project-template-v2/.claude/skills/brainstorming/scripts/wait-for-selection.sh +0 -62
  169. package/project-template-v2/.claude/skills/brainstorming/spec-document-reviewer-prompt.md +0 -49
  170. package/project-template-v2/.claude/skills/brainstorming/visual-companion.md +0 -309
  171. package/project-template-v2/.claude/skills/playcraft-ad-psychology/SKILL.md +0 -182
  172. package/project-template-v2/.claude/skills/playcraft-art-style-guide/SKILL.md +0 -123
  173. package/project-template-v2/.claude/skills/playcraft-asset-state-sheet/SKILL.md +0 -205
  174. package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +0 -280
  175. package/project-template-v2/.claude/skills/playcraft-batch-pipeline/SKILL.md +0 -184
  176. package/project-template-v2/.claude/skills/playcraft-build-optimizer/SKILL.md +0 -306
  177. package/project-template-v2/.claude/skills/playcraft-image-generation/SKILL.md +0 -298
  178. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/build-sprite-sheet.template.mjs +0 -123
  179. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/compare-style.template.mjs +0 -254
  180. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +0 -324
  181. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +0 -97
  182. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-edit-variants.template.mjs +0 -118
  183. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +0 -137
  184. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +0 -397
  185. package/project-template-v2/.claude/skills/playcraft-image-generation/reference/validate-sprite-sheet.template.mjs +0 -296
  186. package/project-template-v2/.claude/skills/playcraft-image-ops/SKILL.md +0 -122
  187. package/project-template-v2/.claude/skills/playcraft-image-processing/SKILL.md +0 -219
  188. package/project-template-v2/.claude/skills/playcraft-masking/SKILL.md +0 -373
  189. package/project-template-v2/.claude/skills/playcraft-playable-optimization/SKILL.md +0 -161
  190. package/project-template-v2/.claude/skills/playcraft-research/SKILL.md +0 -215
  191. package/project-template-v2/.claude/skills/playcraft-skill-recommender/SKILL.md +0 -382
  192. package/project-template-v2/.claude/skills/playcraft-sprite-generation/SKILL.md +0 -423
  193. package/project-template-v2/.claude/skills/playcraft-sprite-remix/SKILL.md +0 -158
  194. package/project-template-v2/.claude/skills/playcraft-sprite-sheet/SKILL.md +0 -100
  195. package/project-template-v2/.claude/skills/playcraft-storyboard/SKILL.md +0 -167
  196. package/project-template-v2/.claude/skills/playcraft-style-qa/SKILL.md +0 -270
  197. package/project-template-v2/.claude/skills/playcraft-text-rendering/SKILL.md +0 -236
  198. package/project-template-v2/.claude/skills/playcraft-vfx-animation/SKILL.md +0 -130
  199. package/project-template-v2/.claude/skills/playwright-cli/SKILL.md +0 -390
  200. package/project-template-v2/.claude/skills/playwright-cli/references/element-attributes.md +0 -23
  201. package/project-template-v2/.claude/skills/playwright-cli/references/playwright-tests.md +0 -39
  202. package/project-template-v2/.claude/skills/playwright-cli/references/request-mocking.md +0 -87
  203. package/project-template-v2/.claude/skills/playwright-cli/references/running-code.md +0 -240
  204. package/project-template-v2/.claude/skills/playwright-cli/references/session-management.md +0 -226
  205. package/project-template-v2/.claude/skills/playwright-cli/references/spec-driven-testing.md +0 -308
  206. package/project-template-v2/.claude/skills/playwright-cli/references/storage-state.md +0 -275
  207. package/project-template-v2/.claude/skills/playwright-cli/references/test-generation.md +0 -134
  208. package/project-template-v2/.claude/skills/playwright-cli/references/tracing.md +0 -142
  209. package/project-template-v2/.claude/skills/playwright-cli/references/video-recording.md +0 -153
  210. package/project-template-v2/.claude/skills/session-analyzer/SKILL.md +0 -386
  211. package/project-template-v2/.claude/skills/session-analyzer/scripts/execution-breakdown.mjs +0 -182
  212. package/project-template-v2/.claude/skills/session-analyzer/scripts/find-turns.mjs +0 -72
  213. package/project-template-v2/.claude/skills/session-analyzer/scripts/heavy-output.mjs +0 -121
  214. package/project-template-v2/.claude/skills/session-analyzer/scripts/resolve-session.mjs +0 -102
  215. package/project-template-v2/.claude/skills/session-analyzer/scripts/subagent-stats.mjs +0 -127
  216. package/project-template-v2/.claude/skills/session-analyzer/scripts/subagent-tool-timeline.mjs +0 -106
  217. package/project-template-v2/.claude/skills/session-analyzer/scripts/time-gaps.mjs +0 -128
  218. package/project-template-v2/.claude/skills/session-analyzer/scripts/turn-timeline.mjs +0 -67
  219. package/project-template-v2/.claude/snapshot.mjs +0 -263
  220. package/project-template-v2/.playcraft/skills.lock.json +0 -152
  221. package/project-template-v2/CLAUDE.md +0 -146
  222. package/project-template-v2/assets/audio/bgm/.gitkeep +0 -0
  223. package/project-template-v2/assets/audio/sfx/.gitkeep +0 -0
  224. package/project-template-v2/assets/bundles/.gitkeep +0 -0
  225. package/project-template-v2/assets/images/bg/.gitkeep +0 -0
  226. package/project-template-v2/assets/images/reference/.gitkeep +0 -0
  227. package/project-template-v2/assets/images/storyboard/.gitkeep +0 -0
  228. package/project-template-v2/assets/images/tiles/.gitkeep +0 -0
  229. package/project-template-v2/assets/images/ui/.gitkeep +0 -0
  230. package/project-template-v2/assets/images/vfx/.gitkeep +0 -0
  231. package/project-template-v2/assets/models/.gitkeep +0 -0
  232. package/project-template-v2/docs/harness/iteration-1/context-flow.md +0 -254
  233. package/project-template-v2/docs/harness/iteration-1/generate-flow.md +0 -91
  234. package/project-template-v2/docs/harness/iteration-1/ideate-flow.md +0 -214
  235. package/project-template-v2/docs/harness/iteration-1/optimize-flow.md +0 -75
  236. package/project-template-v2/docs/harness/iteration-1/wrapup-flow.md +0 -63
  237. package/project-template-v2/docs/harness/iteration-2/context-flow.md +0 -223
  238. package/project-template-v2/docs/harness/iteration-2/generate-flow.md +0 -129
  239. package/project-template-v2/docs/harness/iteration-2/ideate-flow.md +0 -267
  240. package/project-template-v2/docs/harness/iteration-2/optimize-flow.md +0 -164
  241. package/project-template-v2/docs/harness/iteration-2/wrapup-flow.md +0 -115
  242. package/project-template-v2/docs/harness/orchestrator-flow.md +0 -364
  243. package/project-template-v2/docs/project-state.json +0 -60
  244. package/project-template-v2/docs/project-state.md +0 -72
  245. package/project-template-v2/docs/standards/README.md +0 -225
  246. package/project-template-v2/docs/standards/agent-behavior-standards.md +0 -174
  247. package/project-template-v2/docs/standards/artifacts/design-brief.md +0 -19
  248. package/project-template-v2/docs/standards/artifacts/design.md +0 -22
  249. package/project-template-v2/docs/standards/artifacts/game-code.md +0 -41
  250. package/project-template-v2/docs/standards/artifacts/todo-list.md +0 -41
  251. package/project-template-v2/docs/standards/iter1-agent-behavior-standards.md +0 -343
  252. package/project-template-v2/game/index.ts +0 -18
  253. package/project-template-v2/globals.d.ts +0 -51
  254. package/project-template-v2/index.css +0 -34
  255. package/project-template-v2/index.html +0 -18
  256. package/project-template-v2/main.ts +0 -9
  257. package/project-template-v2/package.json +0 -46
  258. package/project-template-v2/skills/_shared/scripts/dispatch-clear.mjs +0 -31
  259. package/project-template-v2/skills/_shared/scripts/dispatch-set.mjs +0 -86
  260. package/project-template-v2/skills/_shared/scripts/dod-check.mjs +0 -153
  261. package/project-template-v2/skills/_shared/scripts/handoff-append.mjs +0 -70
  262. package/project-template-v2/skills/_shared/scripts/lib/validator-artifacts.mjs +0 -91
  263. package/project-template-v2/skills/_shared/scripts/pipeline/dod-config.mjs +0 -131
  264. package/project-template-v2/skills/_shared/scripts/pipeline/index.mjs +0 -80
  265. package/project-template-v2/skills/_shared/scripts/pipeline/iteration-1-core.mjs +0 -90
  266. package/project-template-v2/skills/_shared/scripts/pipeline/iteration-2-wrap.mjs +0 -93
  267. package/project-template-v2/skills/_shared/scripts/pipeline/iteration-3-visual.mjs +0 -23
  268. package/project-template-v2/skills/_shared/scripts/render-project-state.mjs +0 -230
  269. package/project-template-v2/skills/_shared/scripts/state-advance-stage.mjs +0 -58
  270. package/project-template-v2/skills/_shared/scripts/state-get.mjs +0 -339
  271. package/project-template-v2/skills/_shared/scripts/state-handoff.mjs +0 -39
  272. package/project-template-v2/skills/_shared/scripts/state-set.mjs +0 -94
  273. package/project-template-v2/skills/_shared/scripts/state-store.mjs +0 -783
  274. package/project-template-v2/skills/_shared/scripts/todo-add.mjs +0 -88
  275. package/project-template-v2/skills/_shared/scripts/todo-get.mjs +0 -130
  276. package/project-template-v2/skills/_shared/scripts/todo-remove.mjs +0 -33
  277. package/project-template-v2/skills/_shared/scripts/todo-set.mjs +0 -47
  278. package/project-template-v2/skills/_shared/scripts/verify-asset-code-sync.mjs +0 -268
  279. package/project-template-v2/skills/_shared/scripts/verify-env.mjs +0 -285
  280. package/project-template-v2/skills/_shared/scripts/verify-placeholders.mjs +0 -161
  281. package/project-template-v2/skills/playable-autoplay/SKILL.md +0 -176
  282. package/project-template-v2/skills/playable-autoplay/agents/openai.yaml +0 -4
  283. package/project-template-v2/skills/playable-debug/SKILL.md +0 -116
  284. package/project-template-v2/skills/playable-debug/agents/openai.yaml +0 -4
  285. package/project-template-v2/skills/playable-debug/references/debug-config.md +0 -40
  286. package/project-template-v2/skills/playable-record/SKILL.md +0 -140
  287. package/project-template-v2/skills/playable-record/scripts/lib/dev-server.mjs +0 -104
  288. package/project-template-v2/skills/playable-record/scripts/lib/record-audio-bridge.js +0 -141
  289. package/project-template-v2/skills/playable-record/scripts/record-playable.mjs +0 -425
  290. package/project-template-v2/skills/playable-record/scripts/verify-contract.mjs +0 -261
  291. package/project-template-v2/skills/playable-record/scripts/verify-firstwin.mjs +0 -398
  292. package/project-template-v2/skills/playable-record/scripts/verify-fusion.mjs +0 -322
  293. package/project-template-v2/skills/playable-record/scripts/verify-lifecycle.mjs +0 -105
  294. package/project-template-v2/skills/playable-record/scripts/verify-vlm-video.mjs +0 -230
  295. package/project-template-v2/skills/playable-validate/SKILL.md +0 -61
  296. package/project-template-v2/skills/playable-validate/validation-rules.md +0 -14
  297. package/project-template-v2/skills/playable-verify-ui/SKILL.md +0 -63
  298. package/project-template-v2/skills/playable-verify-ui/scripts/lib/__init__.py +0 -1
  299. package/project-template-v2/skills/playable-verify-ui/scripts/lib/config_expr.js +0 -218
  300. package/project-template-v2/skills/playable-verify-ui/scripts/lib/node_utils.js +0 -51
  301. package/project-template-v2/skills/playable-verify-ui/scripts/lib/profile_loader.py +0 -59
  302. package/project-template-v2/skills/playable-verify-ui/scripts/lib/report.py +0 -106
  303. package/project-template-v2/skills/playable-verify-ui/scripts/lib/runtime_contract.js +0 -116
  304. package/project-template-v2/skills/playable-verify-ui/scripts/verify_fetch_antipatterns.py +0 -59
  305. package/project-template-v2/skills/playable-verify-ui/scripts/verify_hardcoded_layout.js +0 -64
  306. package/project-template-v2/skills/playable-verify-ui/scripts/verify_runtime_contract.js +0 -58
  307. package/project-template-v2/skills/playable-verify-ui/scripts/verify_text_overlap.py +0 -333
  308. package/project-template-v2/ta-workspace/scripts/.gitkeep +0 -0
  309. package/project-template-v2/tsconfig.json +0 -20
  310. package/project-template-v2/vite.config.ts +0 -27
  311. package/project-templates/batch-locks/phaser.yaml +0 -922
  312. package/project-templates/batch-locks/threejs.yaml +0 -978
  313. package/project-templates/empty/AGENTS.md +0 -25
  314. package/project-templates/empty/gitignore +0 -33
@@ -1,222 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { readFile, stat } from 'node:fs/promises';
3
- import { basename, dirname, join, resolve } from 'node:path';
4
- import { setTimeout as delay } from 'node:timers/promises';
5
- import { videoAnalysisRequestSchema, videoAnalysisRunSchema, videoMediaInitResultSchema, videoMediaResultSchema, videoModelCatalogSchema, videoUnderstandingAnalysisSchema, } from '@playcraft/common/agent-tools';
6
- import { VideoCliError, videoMessage } from './messages.js';
7
- import { openVideo, verifyVideo } from './media.js';
8
- import { loadSavedRequest, prepareArtifactDirectory, writeJsonAtomic, } from './evidence.js';
9
- const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
10
- export function requireVideoId(value) {
11
- if (!uuid.test(value))
12
- throw new VideoCliError('invalidArguments', 64);
13
- return value.toLowerCase();
14
- }
15
- export async function resolveVideoWorkspace(explicit, cwd = process.cwd()) {
16
- let context;
17
- for (let path = resolve(cwd);; path = dirname(path)) {
18
- try {
19
- const file = join(path, '.playcraft/workspace-context.json');
20
- if ((await stat(file)).size > 8192)
21
- throw new VideoCliError('workspaceMissing', 64);
22
- const value = JSON.parse(await readFile(file, 'utf8'));
23
- if (value.schemaVersion !== 'playcraft.workspace-context.v1' || typeof value.workspaceId !== 'string')
24
- throw new VideoCliError('workspaceMissing', 64);
25
- context = requireVideoId(value.workspaceId);
26
- break;
27
- }
28
- catch (error) {
29
- if (error.code !== 'ENOENT')
30
- throw error;
31
- }
32
- if (dirname(path) === path)
33
- break;
34
- }
35
- const workspace = explicit === undefined ? context : requireVideoId(explicit);
36
- if (!workspace)
37
- throw new VideoCliError('workspaceMissing', 64);
38
- if (context && explicit && context !== workspace)
39
- throw new VideoCliError('workspaceMismatch', 64);
40
- return workspace;
41
- }
42
- export function parseVideoPolicy(options) {
43
- const processing = options.processing ?? 'auto';
44
- if (!['auto', 'agentic', 'static'].includes(processing))
45
- throw new VideoCliError('invalidArguments', 64);
46
- const fps = options.fps === undefined ? undefined : Number(options.fps);
47
- if (fps !== undefined && ![1, 2, 4, 8].includes(fps))
48
- throw new VideoCliError('invalidArguments', 64);
49
- let clip;
50
- if (options.clip !== undefined) {
51
- if (!/^\d+(?:\.\d+)?:\d+(?:\.\d+)?$/.test(options.clip))
52
- throw new VideoCliError('invalidArguments', 64);
53
- const [startMs, endMs] = options.clip.split(':').map(value => Number(value) * 1000);
54
- if (!Number.isFinite(endMs) || endMs <= startMs)
55
- throw new VideoCliError('invalidArguments', 64);
56
- clip = { startMs, endMs };
57
- }
58
- if (processing === 'agentic' && (fps !== undefined || clip))
59
- throw new VideoCliError('invalidArguments', 64);
60
- return { processing: processing, ...(fps !== undefined && { fps: fps }), ...(clip && { clip }) };
61
- }
62
- export function videoTimeout(options) {
63
- const seconds = options.timeout === undefined ? 600 : Number(options.timeout);
64
- if (!Number.isFinite(seconds) || seconds <= 0 || seconds > 86_400)
65
- throw new VideoCliError('invalidArguments', 64);
66
- return seconds * 1000;
67
- }
68
- async function readContext(path, video) {
69
- if (!path)
70
- return undefined;
71
- try {
72
- if ((await stat(path)).size > 2 * 1024 * 1024)
73
- throw new Error('large');
74
- const analysis = videoUnderstandingAnalysisSchema.parse(JSON.parse(await readFile(path, 'utf8')));
75
- if (analysis.source.sha256 !== video.source.sha256)
76
- throw new Error('different video');
77
- return analysis;
78
- }
79
- catch {
80
- throw new VideoCliError('invalidContext', 64);
81
- }
82
- }
83
- export async function uploadVideo(video, workspaceId, deps, signal) {
84
- const mediaId = randomUUID();
85
- const init = videoMediaInitResultSchema.parse(await deps.client.post('/video-media/init', {
86
- workspaceId, mediaId, filename: basename(video.path), sha256: video.source.sha256,
87
- sizeBytes: video.source.sizeBytes, mimeType: video.source.mimeType, contentMd5: video.contentMd5,
88
- }, { signal }));
89
- if (init.mediaId !== mediaId || Date.parse(init.upload.expiresAt) <= Date.now())
90
- throw new VideoCliError('uploadFailed');
91
- const file = await openVideo(video.path);
92
- try {
93
- await verifyVideo(file, video.source);
94
- const stream = file.createReadStream({ start: 0, autoClose: false });
95
- if (init.upload.authenticated) {
96
- await deps.client.putVideoMedia(init.upload.url, stream, init.upload.headers, signal);
97
- }
98
- else {
99
- const url = new URL(init.upload.url);
100
- const allowed = new Set(['content-type', 'content-md5', 'content-length', 'x-cos-acl']);
101
- if (url.protocol !== 'https:' || url.username || url.password || url.hash || Object.entries(init.upload.headers).some(([key, value]) => !allowed.has(key.toLowerCase()) || (key.toLowerCase() === 'x-cos-acl' && value !== 'private')))
102
- throw new VideoCliError('uploadFailed');
103
- const response = await fetch(url, { method: 'PUT', redirect: 'error', signal, headers: init.upload.headers, body: stream, duplex: 'half' });
104
- if (!response.ok) {
105
- await response.body?.cancel();
106
- throw new VideoCliError('uploadFailed');
107
- }
108
- await response.body?.cancel();
109
- }
110
- await verifyVideo(file, video.source);
111
- }
112
- finally {
113
- await file.close();
114
- }
115
- const finalized = videoMediaResultSchema.parse(await deps.client.post('/video-media/finalize', { workspaceId, mediaId }, { signal }));
116
- if (finalized.mediaId !== mediaId || finalized.source.sha256 !== video.source.sha256 || finalized.source.sizeBytes !== video.source.sizeBytes)
117
- throw new VideoCliError('sourceChanged');
118
- return mediaId;
119
- }
120
- export async function readVideoRun(client, runId, workspaceId, signal) {
121
- const run = videoAnalysisRunSchema.parse(await client.get(`/video-analysis/${runId}`, { workspaceId }, { signal }));
122
- if (run.runId !== runId || run.workspaceId !== workspaceId || (run.analysis && run.analysis.runId !== runId))
123
- throw new VideoCliError('invalidResult');
124
- return run;
125
- }
126
- async function waitForVideoRun(run, deps, signal) {
127
- while (['pending', 'uploading', 'submitting', 'running'].includes(run.state)) {
128
- deps.progress('waiting');
129
- await delay(Math.max(250, Math.min(run.pollAfterMs, 10_000)), undefined, { signal });
130
- run = await readVideoRun(deps.client, run.runId, run.workspaceId, signal);
131
- }
132
- return run;
133
- }
134
- export async function executeVideoUnderstanding(options, signal, deps) {
135
- let runId;
136
- let out;
137
- let saved;
138
- let submitted = false;
139
- try {
140
- const workspaceId = await resolveVideoWorkspace(options.workspace);
141
- const policy = parseVideoPolicy(options);
142
- if (options.question !== undefined && (!options.question.trim() || options.question.length > 8000))
143
- throw new VideoCliError('invalidArguments', 64);
144
- if (options.resume && [options.video, options.question, options.context, options.processing, options.fps, options.clip, options.modelConfig].some(value => value !== undefined))
145
- throw new VideoCliError('invalidArguments', 64);
146
- if (!options.resume && !options.video)
147
- throw new VideoCliError('invalidArguments', 64);
148
- runId = options.resume ? requireVideoId(options.resume) : randomUUID();
149
- out = resolve(options.out ?? join('.playcraft/video-replication', runId));
150
- await prepareArtifactDirectory(out, runId, !!options.resume);
151
- let run;
152
- if (options.resume) {
153
- saved = await loadSavedRequest(out, runId, workspaceId);
154
- submitted = true;
155
- // Recovery only reads the existing task. No upload or POST can be triggered here.
156
- run = await readVideoRun(deps.client, runId, workspaceId, signal);
157
- if (saved.request && run.mediaId !== saved.request.mediaId)
158
- throw new VideoCliError('invalidResult');
159
- }
160
- else {
161
- deps.progress('probing');
162
- const video = await deps.probe(resolve(options.video), signal);
163
- if (policy.clip && policy.clip.endMs > video.source.durationMs)
164
- throw new VideoCliError('invalidArguments', 64);
165
- const context = await readContext(options.context, video);
166
- const catalog = videoModelCatalogSchema.parse(await deps.client.get('/video-models', { workspaceId }, { signal }));
167
- const configId = options.modelConfig ? requireVideoId(options.modelConfig) : catalog.defaultConfigId;
168
- const model = catalog.models.find(item => item.configId === configId);
169
- const mode = policy.processing === 'auto'
170
- ? policy.fps !== undefined || policy.clip || !model?.processing.includes('agentic') ? 'static' : 'agentic'
171
- : policy.processing;
172
- if (!model || !model.processing.includes(mode))
173
- throw new VideoCliError('modelUnavailable', 64);
174
- saved = { schemaVersion: 'playcraft.video-request.v1', runId, workspaceId, videoPath: video.path, out, state: 'uploading', createdAt: new Date().toISOString() };
175
- await writeJsonAtomic(join(out, 'request.json'), saved);
176
- deps.progress('uploading');
177
- const mediaId = await uploadVideo(video, workspaceId, deps, signal);
178
- const request = videoAnalysisRequestSchema.parse({
179
- workspaceId, runId, mediaId, modelConfigId: configId, ...policy,
180
- ...(options.question && { question: options.question }), ...(context && { context }),
181
- });
182
- saved = { ...saved, request, state: 'submitting' };
183
- await writeJsonAtomic(join(out, 'request.json'), saved);
184
- deps.progress('submitting');
185
- submitted = true;
186
- run = videoAnalysisRunSchema.parse(await deps.client.post('/video-analysis', request, {
187
- signal,
188
- onPrepared: async (operation) => {
189
- saved = { ...saved, requestId: operation.requestId };
190
- await writeJsonAtomic(join(out, 'request.json'), saved);
191
- },
192
- }));
193
- if (run.runId !== runId || run.workspaceId !== workspaceId || run.mediaId !== mediaId)
194
- throw new VideoCliError('invalidResult');
195
- }
196
- run = await waitForVideoRun(run, deps, signal);
197
- await writeJsonAtomic(join(out, 'task.json'), run);
198
- if (run.state === 'uncertain')
199
- throw new VideoCliError('uncertain', 75);
200
- if (run.state !== 'completed')
201
- return {
202
- exitCode: 4, envelope: { schemaVersion: 'playcraft.video-cli.v1', command: 'video understand', runId, state: run.state, ok: false, artifacts: { request: join(out, 'request.json') }, error: { code: run.error?.code ?? 'remoteFailed', message: videoMessage('remoteFailed') } },
203
- };
204
- if (!run.analysis || !saved || run.analysis.runId !== runId ||
205
- (saved.request?.modelConfigId && run.analysis.model.configId !== saved.request.modelConfigId))
206
- throw new VideoCliError('invalidResult');
207
- deps.progress('extracting');
208
- const artifacts = await deps.materialize({ analysis: run.analysis, sourcePath: saved.videoPath, out, request: saved.request, signal });
209
- await writeJsonAtomic(join(out, 'request.json'), { ...saved, state: 'completed' });
210
- return { exitCode: 0, envelope: { schemaVersion: 'playcraft.video-cli.v1', command: 'video understand', runId, state: 'completed', ok: true, artifacts } };
211
- }
212
- catch (error) {
213
- const timeout = signal.aborted && signal.reason?.name === 'TimeoutError';
214
- const interrupted = signal.aborted && !timeout;
215
- const known = error instanceof VideoCliError;
216
- const code = timeout ? 'timeoutReached' : interrupted ? 'interrupted' : known ? error.code : submitted ? 'uncertain' : 'failed';
217
- const exitCode = timeout ? 75 : interrupted ? 130 : known ? error.exitCode : submitted ? 75 : 4;
218
- if (saved && out && submitted && (exitCode === 75 || exitCode === 130))
219
- await writeJsonAtomic(join(out, 'request.json'), { ...saved, state: 'uncertain' }).catch(() => { });
220
- return { exitCode, envelope: { schemaVersion: 'playcraft.video-cli.v1', command: 'video understand', runId, state: exitCode === 75 || exitCode === 130 ? 'uncertain' : 'failed', ok: false, ...(saved && out && { artifacts: { request: join(out, 'request.json') } }), error: { code, message: videoMessage(code), ...(interrupted && { remoteCancellationConfirmed: false }) } } };
221
- }
222
- }
@@ -1,149 +0,0 @@
1
- import { readFile, writeFile } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import sharp from 'sharp';
4
- import { replicaValidationResultSchema, validateReplicaComparisonContent, } from '@playcraft/common/agent-tools';
5
- import { openVideo, runMediaTool, verifyVideo } from './media.js';
6
- import { comparisonOutputPath, writeComparisonArtifact, writeComparisonJson } from './comparison-output.js';
7
- import { comparisonMessage as t, ComparisonCliError } from './comparison-messages.js';
8
- import { comparisonSha256 } from './comparison-files.js';
9
- const xml = (text) => text.replace(/[&<>"']/g, value => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;' })[value]);
10
- const md = (text) => text.replace(/([\\`*_[\]<>])/g, '\\$1');
11
- const reasonMessages = {
12
- 'video.eventPairMissing': 'reasonEventPairMissing', 'video.dimensionUncertain': 'reasonDimensionUncertain',
13
- 'video.dimensionEventMissing': 'reasonDimensionEventMissing', 'video.progressionEvidenceMissing': 'reasonProgressionEvidenceMissing',
14
- 'video.timingPrecisionInsufficient': 'reasonTimingPrecisionInsufficient', 'video.timingFailureUnverified': 'reasonTimingFailureUnverified',
15
- 'video.requiredEffectsMissing': 'reasonRequiredEffectsMissing', 'video.controlRuntimeUncertain': 'reasonControlRuntimeUncertain',
16
- 'video.controlEvidenceMissing': 'reasonControlEvidenceMissing', 'video.requestsFailed': 'reasonRequestsFailed',
17
- };
18
- const renderReason = (reason) => {
19
- const key = reason.startsWith('video.referenceKindMissing:') ? 'reasonReferenceKindMissing'
20
- : Object.prototype.hasOwnProperty.call(reasonMessages, reason) ? reasonMessages[reason] : undefined;
21
- return md(key ? `${t(key)} (${reason})` : reason);
22
- };
23
- async function tile(frame, out) {
24
- const image = await sharp(join(out, frame.path)).resize(260, 190, { fit: 'contain', background: '#f4f4f4' }).png().toBuffer();
25
- const label = Buffer.from(`<svg width="260" height="34"><rect width="100%" height="100%" fill="white"/><text x="8" y="14" font-size="11">${xml(frame.inputId)} · ${xml(frame.evidenceId)}</text><text x="8" y="29" font-size="11">${(frame.timeMs / 1000).toFixed(3)}s</text></svg>`);
26
- return sharp({ create: { width: 260, height: 224, channels: 3, background: '#fff' } }).composite([{ input: image, top: 0, left: 0 }, { input: label, top: 190, left: 0 }]).png().toBuffer();
27
- }
28
- export function renderComparisonReport(comparison, frames) {
29
- const frameMap = new Map(frames.map(frame => [frame.evidenceId, frame]));
30
- const refs = (ids) => ids.map(id => `[${md(id)}](${frameMap.get(id)?.path})`).join(', ');
31
- const list = (values) => values.length ? values.map(md).join(', ') : t('reportNone');
32
- const timingStatus = comparison.timingChecks.some(check => check.status === 'fail') ? 'fail'
33
- : !comparison.timingChecks.length || comparison.timingChecks.some(check => check.status === 'inconclusive') ? 'inconclusive' : 'pass';
34
- const lines = [`# ${t('summary')}: ${comparison.status}`, '', t('coverageNotice'), '',
35
- `## ${t('decision')}`, '', `- ${t('reportStatus')}: **${comparison.status}**`, `- ${t('reportScope')}: ${comparison.scope}`,
36
- `- ${t('reportPolicy')}: ${comparison.policySha256}`, `- ${t('reportRecording')}: ${comparison.recordingHash}`, ''];
37
- lines.push(t(comparison.status === 'pass' ? 'policyPassedNotice' : 'policyNotPassedNotice'), '', `## ${t('coverage')}`, '', t('deterministicNotice'), '', `- ${t('reportRequired')}: ${list(comparison.coverage.requiredEventIds)}`, `- ${t('reportEvaluated')}: ${list(comparison.coverage.evaluatedEventIds)}`, `- ${t('reportMissing')}: ${list(comparison.coverage.missingEventIds)}`, `- ${t('reportRequiredKinds')}: ${list(comparison.coverage.requiredKinds)}`, `- ${t('reportMissingKinds')}: ${list(comparison.coverage.missingKinds)}`, '', `### ${t('reportMissingEvidence')}`, '');
38
- if (comparison.missingEvidence.some(item => item.dimension === 'interaction'))
39
- lines.push(t('interactionUnverified'), '');
40
- for (const item of comparison.missingEvidence)
41
- lines.push(`- ${item.eventId ? `${md(item.eventId)}: ` : ''}${item.dimension ? `${item.dimension}: ` : ''}${renderReason(item.reason)}`);
42
- if (!comparison.missingEvidence.length)
43
- lines.push(`- ${t('reportNone')}`);
44
- lines.push('', `### ${t('reportTimingChecks')}: ${timingStatus}`, '');
45
- if (timingStatus === 'inconclusive')
46
- lines.push(t('timingUnverified'), '');
47
- for (const check of comparison.timingChecks)
48
- lines.push(`- ${t('reportTiming')} ${md(check.eventId)} (${check.measurement}): ${check.status}; ${t('reportReference')} ${check.referenceRangeMs.join('–')} ms; ${t('reportCandidate')} ${check.candidateRangeMs.join('–')} ms; ${t('reportTolerance')} ${check.toleranceMs} ms`);
49
- lines.push('', `## ${t('modelObservations')}`, '', t('modelObservationNotice'), '', `- ${t('reportModel')}: ${comparison.model.provider}/${comparison.model.model} (${comparison.model.processing})`, '', `### ${t('modelSummary')}`, '', md(comparison.summary), '', `### ${t('modelDimensions')}`, '');
50
- for (const [dimension, judgment] of Object.entries(comparison.dimensions))
51
- lines.push(`- **${dimension}: ${judgment.status}** (${judgment.confidence}) — ${md(judgment.reason)} ${refs(judgment.evidenceIds)}`);
52
- lines.push('', `## ${t('differences')}`, '');
53
- for (const finding of comparison.findings)
54
- lines.push(`### ${md(finding.id)} · ${finding.severity} · ${finding.dimension}`, '', `- ${t('reportExpected')}: ${md(finding.expected)}`, `- ${t('reportObserved')}: ${md(finding.observed)}`, `- ${t('reportSuggestedFix')}: ${md(finding.suggestedFix)}`, `- ${t('reportConfidence')}: ${finding.confidence}; ${t('reportEvidence')}: ${refs(finding.evidenceIds)}`, '');
55
- lines.push(`## ${t('alignment')}`, '');
56
- for (const alignment of comparison.alignments) {
57
- const reference = frameMap.get(alignment.referenceEvidenceId);
58
- const candidate = frameMap.get(alignment.candidateEvidenceId);
59
- lines.push(`- **${md(alignment.eventId)}** (${alignment.confidence}): ${t('reportReference')} ${reference?.timeMs} ms ↔ ${t('reportCandidate')} ${candidate?.timeMs} ms; ${refs([alignment.referenceEvidenceId, alignment.candidateEvidenceId])}`);
60
- }
61
- lines.push('', `## ${t('controls')}`, '');
62
- for (const check of comparison.controlChecks)
63
- lines.push(`- ${md(check.stepId)} → ${md(check.eventId)}; ${md(check.controlInputId)}: ${check.effect} (${check.confidence}); ${refs([check.candidateEvidenceId, check.controlEvidenceId])}`);
64
- lines.push('', `## ${t('evidence')}`, '');
65
- for (const frame of frames)
66
- lines.push(`- ${md(frame.evidenceId)} · ${md(frame.inputId)}: ${t('reportRequested')} ${frame.requestedTimeMs} ms, ${t('reportDecoded')} ${frame.timeMs} ms; [${t('reportFrame')}](${frame.path})`);
67
- return `${lines.join('\n')}\n`;
68
- }
69
- /** Extract real source frames for each validated input; model output never supplies local paths. */
70
- export async function materializeComparisonEvidence(input) {
71
- let comparison;
72
- try {
73
- comparison = replicaValidationResultSchema.parse(input.comparison);
74
- validateReplicaComparisonContent({ summary: comparison.summary, evidence: comparison.evidence, alignments: comparison.alignments,
75
- dimensions: comparison.dimensions, findings: comparison.findings, controlChecks: comparison.controlChecks, timings: comparison.timings, missingEvidence: comparison.missingEvidence }, { inputs: comparison.inputs, analysis: input.request.analysis, recording: input.request.recording, policy: input.request.policy, processing: input.request.processing, fps: input.request.fps });
76
- }
77
- catch {
78
- throw new ComparisonCliError('comparisonResult');
79
- }
80
- // Validate every destination before replacing any prior artifact.
81
- for (const path of [...comparison.evidence.map(item => `keyframes/${item.inputId}/${item.id}.png`), 'comparison.json', 'comparison.md', 'manifest.json', 'contact-sheet.jpg', 'paired-contact-sheet.jpg'])
82
- await comparisonOutputPath(input.out, path);
83
- const frames = [];
84
- for (const source of comparison.inputs) {
85
- const local = input.inputs.find(item => item.id === source.id && item.role === source.role);
86
- if (!local)
87
- throw new ComparisonCliError('comparisonResult');
88
- const evidence = comparison.evidence.filter(item => item.inputId === source.id);
89
- if (!evidence.length)
90
- continue;
91
- const file = await openVideo(local.video.path);
92
- try {
93
- await verifyVideo(file, source.source);
94
- const output = await runMediaTool('ffprobe', ['-v', 'error', '-protocol_whitelist', 'file,pipe', '-format_whitelist', 'mov,matroska,webm',
95
- '-select_streams', 'v:0', '-show_entries', 'frame=best_effort_timestamp_time', '-of', 'csv=p=0', '/dev/fd/3'], file.fd, input.signal);
96
- const raw = output.split('\n').map(line => Number.parseFloat(line) * 1000).filter(Number.isFinite);
97
- const origin = raw[0] ?? 0;
98
- const times = raw.map(time => time - origin).filter(time => time >= 0 && time < source.source.durationMs);
99
- if (!times.length)
100
- throw new ComparisonCliError('comparisonResult');
101
- for (const item of evidence) {
102
- input.signal?.throwIfAborted();
103
- const requestedTimeMs = (item.startMs + item.endMs) / 2;
104
- const timeMs = times.reduce((best, time) => Math.abs(time - requestedTimeMs) < Math.abs(best - requestedTimeMs) ? time : best);
105
- const path = `keyframes/${source.id}/${item.id}.png`;
106
- await writeComparisonArtifact(input.out, path, temporary => runMediaTool('ffmpeg', ['-v', 'error', '-nostdin', '-y', '-protocol_whitelist', 'file,pipe', '-format_whitelist', 'mov,matroska,webm',
107
- '-ss', (Math.max(0, timeMs - 0.01) / 1000).toFixed(6), '-i', '/dev/fd/3', '-map', '0:v:0', '-frames:v', '1',
108
- '-vf', 'scale=min(1280\\,iw):-2', '-threads', '1', temporary], file.fd, input.signal));
109
- const bytes = await readFile(join(input.out, path));
110
- const metadata = await sharp(bytes).metadata();
111
- if (!metadata.width || !metadata.height)
112
- throw new ComparisonCliError('comparisonResult');
113
- frames.push({ evidenceId: item.id, inputId: source.id, requestedTimeMs, timeMs, path, sha256: comparisonSha256(bytes) });
114
- }
115
- await verifyVideo(file, source.source);
116
- }
117
- finally {
118
- await file.close();
119
- }
120
- }
121
- if (frames.length !== comparison.evidence.length)
122
- throw new ComparisonCliError('comparisonResult');
123
- const tiles = await Promise.all(frames.map(frame => tile(frame, input.out)));
124
- const columns = Math.min(4, tiles.length);
125
- const contactSheet = join(input.out, 'contact-sheet.jpg');
126
- await writeComparisonArtifact(input.out, 'contact-sheet.jpg', temporary => sharp({ create: { width: columns * 260, height: Math.ceil(tiles.length / columns) * 224, channels: 3, background: '#ddd' } })
127
- .composite(tiles.map((value, index) => ({ input: value, left: index % columns * 260, top: Math.floor(index / columns) * 224 }))).jpeg({ quality: 85 }).toFile(temporary));
128
- const pairs = comparison.alignments.flatMap(alignment => [alignment.referenceEvidenceId, alignment.candidateEvidenceId]);
129
- const selected = pairs.length ? pairs.map(id => tiles[frames.findIndex(frame => frame.evidenceId === id)])
130
- : ['reference', 'candidate'].flatMap(inputId => { const index = frames.findIndex(frame => frame.inputId === inputId); return index >= 0 ? [tiles[index]] : []; });
131
- if (!selected.length)
132
- throw new ComparisonCliError('comparisonResult');
133
- const pairedContactSheet = join(input.out, 'paired-contact-sheet.jpg');
134
- await writeComparisonArtifact(input.out, 'paired-contact-sheet.jpg', temporary => sharp({ create: { width: 520, height: Math.ceil(selected.length / 2) * 224, channels: 3, background: '#ddd' } })
135
- .composite(selected.map((value, index) => ({ input: value, left: index % 2 * 260, top: Math.floor(index / 2) * 224 }))).jpeg({ quality: 85 }).toFile(temporary));
136
- const resultPath = join(input.out, 'comparison.json');
137
- const markdown = join(input.out, 'comparison.md');
138
- await writeComparisonJson(input.out, 'comparison.json', comparison);
139
- await writeComparisonArtifact(input.out, 'comparison.md', temporary => writeFile(temporary, renderComparisonReport(comparison, frames), { mode: 0o600, flag: 'wx' }));
140
- const files = [];
141
- for (const path of ['comparison.json', 'comparison.md', 'contact-sheet.jpg', 'paired-contact-sheet.jpg'])
142
- files.push({ path, sha256: comparisonSha256(await readFile(join(input.out, path))) });
143
- const manifest = join(input.out, 'manifest.json');
144
- await writeComparisonJson(input.out, 'manifest.json', { schemaVersion: 'playcraft.replica-evidence.v1', runId: comparison.runId, state: 'completed', status: comparison.status,
145
- recordingHash: comparison.recordingHash, policySha256: comparison.policySha256, model: comparison.model, inputs: comparison.inputs,
146
- comparison: 'comparison.json', markdown: 'comparison.md', contactSheet: 'contact-sheet.jpg', pairedContactSheet: 'paired-contact-sheet.jpg',
147
- files, frames, frameTimeSemantics: 'nearest-decoded-original-video-frame', audio: 'not_evaluated' });
148
- return { manifest, comparison: resultPath, markdown, contactSheet, pairedContactSheet, keyframes: frames.map(frame => join(input.out, frame.path)) };
149
- }
@@ -1,57 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { lstat, open, realpath } from 'node:fs/promises';
3
- import { isAbsolute, relative, resolve, sep } from 'node:path';
4
- import { ComparisonCliError } from './comparison-messages.js';
5
- export function comparisonSha256(value) {
6
- return createHash('sha256').update(value).digest('hex');
7
- }
8
- export async function readComparisonJson(path, maxBytes = 2 * 1024 * 1024) {
9
- const file = await open(path, 'r');
10
- try {
11
- const stat = await file.stat();
12
- if (!stat.isFile() || stat.size <= 0 || stat.size > maxBytes)
13
- throw new ComparisonCliError('invalidRecording', 64);
14
- return JSON.parse(await file.readFile('utf8'));
15
- }
16
- finally {
17
- await file.close();
18
- }
19
- }
20
- /** Only recorder-owned, regular files under the exact recording directory may become evidence. */
21
- export async function recordingArtifactPath(root, path) {
22
- if (!path || isAbsolute(path) || path.includes('\\') || path.split('/').some(part => !part || part === '.' || part === '..'))
23
- throw new ComparisonCliError('invalidRecording', 64);
24
- const base = await realpath(root);
25
- const target = resolve(base, path);
26
- const local = relative(base, target);
27
- if (!local || local === '..' || local.startsWith(`..${sep}`) || isAbsolute(local))
28
- throw new ComparisonCliError('invalidRecording', 64);
29
- let current = base;
30
- for (const part of local.split(sep)) {
31
- current = resolve(current, part);
32
- const stat = await lstat(current);
33
- if (stat.isSymbolicLink())
34
- throw new ComparisonCliError('invalidRecording', 64);
35
- }
36
- if (await realpath(target) !== target || !(await lstat(target)).isFile())
37
- throw new ComparisonCliError('invalidRecording', 64);
38
- return target;
39
- }
40
- export async function verifyRecordingArtifact(root, artifact, maxBytes = 8 * 1024 * 1024) {
41
- const path = await recordingArtifactPath(root, artifact.path);
42
- const file = await open(path, 'r');
43
- try {
44
- const stat = await file.stat();
45
- if (!stat.isFile() || stat.size > maxBytes)
46
- throw new ComparisonCliError('invalidRecording', 64);
47
- const hash = createHash('sha256');
48
- for await (const chunk of file.createReadStream({ autoClose: false, start: 0 }))
49
- hash.update(chunk);
50
- if (hash.digest('hex') !== artifact.sha256)
51
- throw new ComparisonCliError('invalidRecording', 64);
52
- }
53
- finally {
54
- await file.close();
55
- }
56
- return path;
57
- }
@@ -1,72 +0,0 @@
1
- import { canonicalReplicaJson } from '@playcraft/common/agent-tools';
2
- import { ComparisonCliError } from './comparison-messages.js';
3
- /** Completed capture and every real/omitted scenario step must be represented in the hashed log. */
4
- export function validateComparisonInputLog(records, scenario, artifact) {
5
- const invalid = () => { throw new ComparisonCliError('invalidRecording', 64); };
6
- const finiteTime = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0;
7
- if (records.some(value => value === null || typeof value !== 'object' || Array.isArray(value)))
8
- invalid();
9
- const log = records;
10
- const starts = log.filter(record => record.kind === 'recording-start'), ends = log.filter(record => record.kind === 'recording-end');
11
- if (starts.length !== 1 || ends.length !== 1 || log[0] !== starts[0] || log.at(-1) !== ends[0])
12
- invalid();
13
- const start = starts[0], end = ends[0];
14
- if (start.plannedAtMs !== 0 || start.actualAtMs !== 0 || start.device !== scenario.pointer || canonicalReplicaJson(start.reset) !== canonicalReplicaJson(scenario.reset)
15
- || end.plannedAtMs !== scenario.durationMs || !finiteTime(end.actualAtMs) || end.actualAtMs < scenario.durationMs || end.status !== 'completed')
16
- invalid();
17
- if (log.some(record => record.status === 'failed' || !finiteTime(record.actualAtMs) || !finiteTime(record.plannedAtMs)))
18
- invalid();
19
- const finals = log.filter(record => ['tap', 'drag'].includes(String(record.kind)) && ['dispatched', 'omitted'].includes(String(record.status)));
20
- if (finals.length !== scenario.steps.length)
21
- invalid();
22
- let previousEnd = -1;
23
- for (const step of scenario.steps) {
24
- const matching = finals.filter(record => record.stepId === step.id);
25
- if (matching.length !== 1)
26
- invalid();
27
- const record = matching[0], omitted = artifact.omittedStepIds.includes(step.id);
28
- if (record.kind !== step.type || record.plannedAtMs !== step.atMs || record.device !== scenario.pointer || record.status !== (omitted ? 'omitted' : 'dispatched')
29
- || !finiteTime(record.actualAtMs) || record.actualAtMs + 2 < step.atMs || record.actualAtMs < previousEnd)
30
- invalid();
31
- const expected = (step.type === 'tap' ? [{ offsetMs: 0, x: step.x, y: step.y }] : step.path).map(point => ({ ...point, actual: {
32
- x: (scenario.contentRect.x + point.x * scenario.contentRect.width) * scenario.viewport.width,
33
- y: (scenario.contentRect.y + point.y * scenario.contentRect.height) * scenario.viewport.height,
34
- } }));
35
- if (canonicalReplicaJson(record.points) !== canonicalReplicaJson(expected))
36
- invalid();
37
- if (!omitted) {
38
- if (!finiteTime(record.completedAtMs) || record.completedAtMs < record.actualAtMs
39
- || record.completedAtMs > artifact.video.source.durationMs + artifact.timing.uncertaintyMs)
40
- invalid();
41
- const pointers = log.filter(pointer => pointer.stepId === step.id && pointer.kind === 'pointer' && pointer.status === 'dispatched');
42
- if (step.type === 'tap' ? !pointers.some(pointer => pointer.phase === 'tap') : !['start', 'move', 'end'].every(phase => pointers.some(pointer => pointer.phase === phase)))
43
- invalid();
44
- previousEnd = record.completedAtMs;
45
- }
46
- else
47
- previousEnd = record.actualAtMs;
48
- }
49
- }
50
- /** Runtime counters must agree with the hashed console and browser event evidence. */
51
- export function validateComparisonConsoleLog(records, artifact) {
52
- const counts = { pageErrors: 0, consoleErrors: 0, failedRequests: 0 };
53
- for (const value of records) {
54
- if (value === null || typeof value !== 'object' || Array.isArray(value))
55
- throw new ComparisonCliError('invalidRecording', 64);
56
- const record = value;
57
- if (typeof record.atMs !== 'number' || !Number.isFinite(record.atMs) || record.atMs < 0)
58
- throw new ComparisonCliError('invalidRecording', 64);
59
- if (record.kind === 'console') {
60
- if (record.level === 'error')
61
- counts.consoleErrors++;
62
- }
63
- else if (['pageerror', 'popup', 'dialog'].includes(String(record.kind)))
64
- counts.pageErrors++;
65
- else if (record.kind === 'requestfailed')
66
- counts.failedRequests++;
67
- else
68
- throw new ComparisonCliError('invalidRecording', 64);
69
- }
70
- if (canonicalReplicaJson(counts) !== canonicalReplicaJson(artifact.runtime))
71
- throw new ComparisonCliError('invalidRecording', 64);
72
- }
@@ -1,104 +0,0 @@
1
- const messages = {
2
- en: {
3
- compare: 'Compare a recorded playable with its reference and extract paired evidence',
4
- processing: 'static | agentic (defaults to static)',
5
- reference: 'Original local reference video', analysis: 'Completed reference analysis.json',
6
- candidate: 'Locally recorded candidate video', recording: 'Recording metadata.json with input and control evidence',
7
- policy: 'Optional versioned comparison policy JSON', kind: 'understanding | comparison (defaults to understanding)',
8
- invalidComparison: 'Comparison arguments or policy are invalid.',
9
- unsafeOutput: 'The comparison output contains a linked or invalid directory or artifact. Choose a safe output directory.',
10
- invalidRecording: 'Recording metadata, source hashes, input logs, or controls are invalid.',
11
- staleRecording: 'The project changed after recording. Make a fresh recording before comparison.',
12
- comparisonContext: 'The reference analysis does not match the reference video.',
13
- comparisonResume: 'The saved comparison request is missing or does not match this run and workspace.',
14
- comparisonUnavailable: 'No authorized model supports the requested comparison.',
15
- comparisonResult: 'The comparison service returned invalid or incomplete evidence.',
16
- comparisonFailed: 'Comparison failed. Inspect its saved request and task before retrying.',
17
- summary: 'Comparison', decision: 'Policy decision', alignment: 'Model event alignment',
18
- differences: 'Model-observed differences and modification suggestions', coverage: 'Coverage and uncertainty',
19
- evidence: 'Paired evidence', controls: 'Model interpretation of interaction controls',
20
- reportStatus: 'Result', reportRequiredKinds: 'Required event kinds', reportMissingKinds: 'Missing event kinds',
21
- reportMissingEvidence: 'Missing or uncertain evidence', reportTimingChecks: 'Deterministic timing checks', reportNone: 'none',
22
- modelObservations: 'Model observations', modelSummary: 'Model summary', modelDimensions: 'Model dimension judgments',
23
- modelObservationNotice: 'These are model observations and interpretations. A model pass does not override the policy decision or missing evidence above.',
24
- policyPassedNotice: 'Policy acceptance passed for this recorded scenario.',
25
- policyNotPassedNotice: 'Policy acceptance has not passed. Gameplay equivalence has not been established.',
26
- deterministicNotice: 'Coverage and timing checks below are deterministic. Missing evidence remains unverified even when a model observation says pass.',
27
- interactionUnverified: 'Interaction is not fully verified: required causal or control evidence is missing or uncertain.',
28
- timingUnverified: 'Timing is not fully verified: evidence precision or coverage is insufficient for the policy tolerance.',
29
- reasonReferenceKindMissing: 'Reference analysis lacks a required event kind.',
30
- reasonEventPairMissing: 'Reference and candidate lack a sufficiently confident event pair.',
31
- reasonDimensionUncertain: 'The model judgment for this dimension is uncertain or below the required confidence.',
32
- reasonDimensionEventMissing: 'This dimension does not cover a required event.',
33
- reasonProgressionEvidenceMissing: 'Evidence for event progression timing is missing or insufficient.',
34
- reasonTimingPrecisionInsufficient: 'Timing precision or coverage cannot resolve the policy tolerance.',
35
- reasonTimingFailureUnverified: 'The model timing failure is not confirmed by deterministic timing checks.',
36
- reasonRequiredEffectsMissing: 'Required gameplay results are not linked to recorded input steps.',
37
- reasonControlRuntimeUncertain: 'A control recording contains runtime errors or failed requests.',
38
- reasonControlEvidenceMissing: 'Required control evidence is missing, uncertain, or does not cover the candidate result window.',
39
- reasonRequestsFailed: 'The candidate recording contains failed network requests.',
40
- reportScope: 'Scope', reportModel: 'Model', reportPolicy: 'Policy', reportRecording: 'Recording',
41
- reportExpected: 'Expected', reportObserved: 'Observed', reportSuggestedFix: 'Suggested fix',
42
- reportConfidence: 'Confidence', reportEvidence: 'evidence', reportReference: 'reference', reportCandidate: 'candidate',
43
- reportRequired: 'Required', reportEvaluated: 'Evaluated', reportMissing: 'Missing', reportTiming: 'Timing',
44
- reportTolerance: 'tolerance', reportRequested: 'requested', reportDecoded: 'decoded', reportFrame: 'frame',
45
- coverageNotice: 'This result covers only the recorded scenario. Audio and unshown gameplay branches were not evaluated.',
46
- },
47
- zh: {
48
- compare: '比较复刻玩法录像与参考视频,并抽取成对证据',
49
- processing: 'static | agentic,默认 static',
50
- reference: '本地原始参考视频', analysis: '已完成的参考分析 analysis.json',
51
- candidate: '本地录制的候选视频', recording: '包含输入和对照证据的录制 metadata.json',
52
- policy: '可选的版本化比较策略 JSON', kind: 'understanding | comparison,默认 understanding',
53
- invalidComparison: '比较参数或策略无效。',
54
- unsafeOutput: '比较输出目录或制品包含符号链接或无效文件,请使用安全的输出目录。',
55
- invalidRecording: '录制元数据、视频摘要、输入日志或对照证据无效。',
56
- staleRecording: '工程在录制后发生了变化,请重新录制再比较。',
57
- comparisonContext: '参考分析与参考视频不一致。',
58
- comparisonResume: '比较请求不存在,或与任务、工作区不一致。',
59
- comparisonUnavailable: '没有有权使用且支持所需比较模式的模型。',
60
- comparisonResult: '比较服务返回的证据无效或不完整。',
61
- comparisonFailed: '比较失败,请先核对已保存的请求和任务再重试。',
62
- summary: '比较报告', decision: '策略结论', alignment: '模型事件配对',
63
- differences: '模型观察到的差异和修改建议', coverage: '覆盖范围与未知项', evidence: '成对证据',
64
- controls: '模型对交互对照的解释', coverageNotice: '结论仅覆盖本次录制场景;音频及未展示的玩法分支尚未评估。',
65
- reportStatus: '结论', reportRequiredKinds: '必需事件类型', reportMissingKinds: '缺失事件类型',
66
- reportMissingEvidence: '缺失或不确定的证据', reportTimingChecks: '确定性时序检查', reportNone: '无',
67
- modelObservations: '模型观察', modelSummary: '模型摘要', modelDimensions: '模型维度判断',
68
- modelObservationNotice: '以下内容是模型的观察和解释。模型判断为 pass,不会覆盖前面的策略结论或证据缺口。',
69
- policyPassedNotice: '本次录制场景已通过策略验收。',
70
- policyNotPassedNotice: '策略验收尚未通过,玩法等价性尚未得到验证。',
71
- deterministicNotice: '以下覆盖范围和时序检查由确定性规则计算。即使模型判断为 pass,缺失证据的部分仍未得到验证。',
72
- interactionUnverified: '交互尚未完整验证:必需的因果或对照证据缺失或不确定。',
73
- timingUnverified: '时序尚未完整验证:证据精度或覆盖不足,无法满足策略容差。',
74
- reasonReferenceKindMissing: '参考分析缺少必需的事件类型。',
75
- reasonEventPairMissing: '参考视频与候选视频缺少达到置信度要求的事件配对。',
76
- reasonDimensionUncertain: '模型对该维度的判断不确定,或未达到置信度要求。',
77
- reasonDimensionEventMissing: '该维度尚未覆盖必需事件。',
78
- reasonProgressionEvidenceMissing: '事件进展的时序证据缺失或不足。',
79
- reasonTimingPrecisionInsufficient: '时序证据的精度或覆盖不足,无法按策略容差得出结论。',
80
- reasonTimingFailureUnverified: '模型报告的时序失败尚未得到确定性时序检查的确认。',
81
- reasonRequiredEffectsMissing: '必需的玩法结果尚未关联到录制输入步骤。',
82
- reasonControlRuntimeUncertain: '对照录制存在运行错误或网络请求失败。',
83
- reasonControlEvidenceMissing: '必需的对照证据缺失、不确定,或未覆盖候选结果的时间窗口。',
84
- reasonRequestsFailed: '候选录制存在网络请求失败。',
85
- reportScope: '范围', reportModel: '模型', reportPolicy: '策略', reportRecording: '录制',
86
- reportExpected: '预期', reportObserved: '实际', reportSuggestedFix: '修改建议',
87
- reportConfidence: '置信度', reportEvidence: '证据', reportReference: '参考', reportCandidate: '候选',
88
- reportRequired: '必需', reportEvaluated: '已评估', reportMissing: '缺失', reportTiming: '时序',
89
- reportTolerance: '容差', reportRequested: '请求时刻', reportDecoded: '解码时刻', reportFrame: '帧',
90
- },
91
- };
92
- export function comparisonMessage(key) {
93
- const locale = process.env.PLAYCRAFT_LANGUAGE || process.env.LANG || 'en';
94
- return messages[locale.startsWith('zh') ? 'zh' : 'en'][key];
95
- }
96
- export class ComparisonCliError extends Error {
97
- code;
98
- exitCode;
99
- constructor(code, exitCode = 4) {
100
- super(comparisonMessage(code));
101
- this.code = code;
102
- this.exitCode = exitCode;
103
- }
104
- }