@playcraft/cli 0.1.33 → 0.1.35-prod
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.
- package/README.md +3 -56
- package/dist/agent/local-backend.js +1 -1
- package/dist/cli-root-help.js +0 -2
- package/dist/commands/build.js +4 -9
- package/dist/commands/create.js +33 -26
- package/dist/commands/image.js +6 -2
- package/dist/commands/login.js +42 -84
- package/dist/commands/skills.js +0 -2
- package/dist/commands/tools-3d.js +0 -11
- package/dist/commands/tools-generation.js +109 -68
- package/dist/commands/tools-image-context.js +23 -0
- package/dist/commands/tools.js +2 -2
- package/dist/config.js +4 -59
- package/dist/index.js +3 -36
- package/dist/remix/clone.js +0 -2
- package/dist/remix/init-template.js +0 -2
- package/dist/remix/pull.js +0 -2
- package/dist/remix/push.js +1 -21
- package/dist/utils/agent-api-client.js +30 -185
- package/dist/utils/image-asset-context.js +105 -0
- package/dist/utils/playcraft-http-client.js +4 -6
- package/package.json +5 -14
- package/project-template/.claude/agents/refs/ta-image-generation-detail.md +67 -123
- package/project-template/.claude/skills/playcraft-image-generation/SKILL.md +105 -44
- package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +42 -33
- package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +18 -20
- package/project-template/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +6 -7
- package/project-template/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +36 -39
- package/project-templates/phaser/.claude/agents/refs/ta-image-generation-detail.md +59 -59
- package/project-templates/phaser/.claude/skills/playcraft-image-generation/SKILL.md +105 -44
- package/project-templates/phaser/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +42 -33
- package/project-templates/phaser/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +18 -20
- package/project-templates/phaser/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +6 -7
- package/project-templates/phaser/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +36 -39
- package/project-templates/threejs/.claude/agents/refs/ta-image-generation-detail.md +59 -59
- package/project-templates/threejs/.claude/skills/playcraft-image-generation/SKILL.md +105 -44
- package/project-templates/threejs/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +42 -33
- package/project-templates/threejs/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +18 -20
- package/project-templates/threejs/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +6 -7
- package/project-templates/threejs/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +36 -39
- package/dist/auth/account-activation.js +0 -157
- package/dist/auth/messages.js +0 -30
- package/dist/commands/platform-skills.generated.js +0 -19
- package/dist/commands/playable.js +0 -53
- package/dist/commands/tools-batch.js +0 -87
- package/dist/commands/video.js +0 -111
- package/dist/commands/workspace-runtime.js +0 -54
- package/dist/commands/workspace.js +0 -35
- package/dist/project-skills/builtin-import.js +0 -225
- package/dist/project-skills/commands.js +0 -77
- package/dist/project-skills/lifecycle.js +0 -28
- package/dist/project-skills/local-run.js +0 -112
- package/dist/project-skills/local-store.js +0 -221
- package/dist/project-skills/messages.js +0 -80
- package/dist/project-skills/reconcile.js +0 -62
- package/dist/project-skills/remote-cache.js +0 -51
- package/dist/project-skills/validation.js +0 -19
- package/dist/recording/messages.js +0 -40
- package/dist/recording/project-digest.js +0 -85
- package/dist/recording/recorder.js +0 -293
- package/dist/recording/scenario.js +0 -50
- package/dist/utils/environment-endpoints.js +0 -17
- package/dist/utils/tool-operation-journal.js +0 -124
- package/dist/utils/video-evidence-git.js +0 -81
- package/dist/video-replication/client.js +0 -222
- package/dist/video-replication/comparison-evidence.js +0 -149
- package/dist/video-replication/comparison-files.js +0 -57
- package/dist/video-replication/comparison-log.js +0 -72
- package/dist/video-replication/comparison-messages.js +0 -104
- package/dist/video-replication/comparison-output.js +0 -63
- package/dist/video-replication/comparison-preflight.js +0 -66
- package/dist/video-replication/comparison.js +0 -216
- package/dist/video-replication/evidence.js +0 -165
- package/dist/video-replication/media.js +0 -111
- package/dist/video-replication/messages.js +0 -89
- package/dist/workspace/messages.js +0 -35
- package/dist/workspace/transfer-client.js +0 -174
- package/dist/workspace-runtime/codex/app-server.js +0 -965
- package/dist/workspace-runtime/codex/compact.js +0 -46
- package/dist/workspace-runtime/codex/config-toml.js +0 -107
- package/dist/workspace-runtime/codex/jsonrpc-stdio.js +0 -145
- package/dist/workspace-runtime/codex/launcher-client.js +0 -177
- package/dist/workspace-runtime/codex/loopback.js +0 -146
- package/dist/workspace-runtime/codex/mcp-config.js +0 -39
- package/dist/workspace-runtime/codex/model-broker.js +0 -263
- package/dist/workspace-runtime/codex/native-adapter.js +0 -9
- package/dist/workspace-runtime/codex/permissions.js +0 -70
- package/dist/workspace-runtime/codex/work-mode.js +0 -15
- package/dist/workspace-runtime/main.js +0 -10
- package/dist/workspace-runtime/persistence/journal.js +0 -355
- package/dist/workspace-runtime/persistence/native-subagents.js +0 -162
- package/dist/workspace-runtime/persistence/sqlite-client.js +0 -77
- package/dist/workspace-runtime/persistence/sqlite-worker.js +0 -1096
- package/dist/workspace-runtime/previews/output.js +0 -36
- package/dist/workspace-runtime/previews/publish.js +0 -262
- package/dist/workspace-runtime/previews/single-file.js +0 -43
- package/dist/workspace-runtime/processes/managed-writes.js +0 -69
- package/dist/workspace-runtime/production/contracts.js +0 -17
- package/dist/workspace-runtime/production/controller.js +0 -207
- package/dist/workspace-runtime/production/dependencies.js +0 -60
- package/dist/workspace-runtime/production/native.js +0 -203
- package/dist/workspace-runtime/production/processes.js +0 -86
- package/dist/workspace-runtime/production/save-command.js +0 -35
- package/dist/workspace-runtime/production/source-copy.js +0 -91
- package/dist/workspace-runtime/production/stages.js +0 -205
- package/dist/workspace-runtime/production/storage.js +0 -60
- package/dist/workspace-runtime/serve.js +0 -207
- package/dist/workspace-runtime/server/auth.js +0 -21
- package/dist/workspace-runtime/server/connectivity.js +0 -201
- package/dist/workspace-runtime/server/desktop-host.js +0 -429
- package/dist/workspace-runtime/server/desktop-preview-config.js +0 -81
- package/dist/workspace-runtime/server/desktop-preview-service.js +0 -157
- package/dist/workspace-runtime/server/desktop-preview-static.js +0 -53
- package/dist/workspace-runtime/server/desktop-preview.js +0 -189
- package/dist/workspace-runtime/server/dispatch.js +0 -1174
- package/dist/workspace-runtime/server/endpoints.js +0 -19
- package/dist/workspace-runtime/server/execution-group.js +0 -85
- package/dist/workspace-runtime/server/http.js +0 -1127
- package/dist/workspace-runtime/server/lock.js +0 -53
- package/dist/workspace-runtime/server/rfb-watch-gate.js +0 -168
- package/dist/workspace-runtime/server/terminal-host.js +0 -366
- package/dist/workspace-runtime/server/types.js +0 -1
- package/dist/workspace-runtime/server/workspace-transfer.js +0 -99
- package/dist/workspace-runtime/workspaces/archive.js +0 -207
- package/dist/workspace-runtime/workspaces/artifact-events.js +0 -55
- package/dist/workspace-runtime/workspaces/artifact-files.js +0 -197
- package/dist/workspace-runtime/workspaces/artifact-receipts.js +0 -89
- package/dist/workspace-runtime/workspaces/artifacts.js +0 -282
- package/dist/workspace-runtime/workspaces/attachment-content.js +0 -123
- package/dist/workspace-runtime/workspaces/attachments.js +0 -181
- package/dist/workspace-runtime/workspaces/batch-commit.js +0 -142
- package/dist/workspace-runtime/workspaces/cli-context.js +0 -23
- package/dist/workspace-runtime/workspaces/cli-credentials.js +0 -111
- package/dist/workspace-runtime/workspaces/composer-context.js +0 -125
- package/dist/workspace-runtime/workspaces/context-error.js +0 -2
- package/dist/workspace-runtime/workspaces/dependencies.js +0 -264
- package/dist/workspace-runtime/workspaces/developer-command.js +0 -64
- package/dist/workspace-runtime/workspaces/directories.js +0 -123
- package/dist/workspace-runtime/workspaces/directory-access.js +0 -37
- package/dist/workspace-runtime/workspaces/directory-attachments.js +0 -245
- package/dist/workspace-runtime/workspaces/directory-maintenance.js +0 -111
- package/dist/workspace-runtime/workspaces/directory-stats.js +0 -49
- package/dist/workspace-runtime/workspaces/disk-snapshot.js +0 -31
- package/dist/workspace-runtime/workspaces/empty-template.js +0 -40
- package/dist/workspace-runtime/workspaces/files-search.js +0 -116
- package/dist/workspace-runtime/workspaces/files.js +0 -120
- package/dist/workspace-runtime/workspaces/git-credential-broker.js +0 -312
- package/dist/workspace-runtime/workspaces/git-credential-helper.js +0 -68
- package/dist/workspace-runtime/workspaces/git-direct-store.js +0 -155
- package/dist/workspace-runtime/workspaces/git.js +0 -658
- package/dist/workspace-runtime/workspaces/instructions.js +0 -56
- package/dist/workspace-runtime/workspaces/json5-edit.js +0 -170
- package/dist/workspace-runtime/workspaces/library-attachments.js +0 -268
- package/dist/workspace-runtime/workspaces/parameters.js +0 -272
- package/dist/workspace-runtime/workspaces/prepare.js +0 -264
- package/dist/workspace-runtime/workspaces/registry.js +0 -236
- package/dist/workspace-runtime/workspaces/revisions.js +0 -31
- package/dist/workspace-runtime/workspaces/skill-artifact-loader.js +0 -91
- package/dist/workspace-runtime/workspaces/skills.js +0 -189
- package/dist/workspace-runtime/workspaces/transfer.js +0 -288
- package/dist/workspace-runtime/workspaces/turn-changes.js +0 -72
- package/dist/workspace-runtime/workspaces/video-attachments.js +0 -49
- package/project-template/.claude/skills/playcraft-replica-validation/SKILL.md +0 -25
- package/project-template/.claude/skills/playcraft-replica-validation/references/contracts.md +0 -79
- package/project-template/.claude/skills/playcraft-video-understanding/SKILL.md +0 -29
- package/project-template/.claude/skills/playcraft-video-understanding/references/contracts.md +0 -37
- package/project-template-v2/.claude/agents/artist.md +0 -82
- package/project-template-v2/.claude/agents/developer.md +0 -153
- package/project-template-v2/.claude/agents/game-designer.md +0 -264
- package/project-template-v2/.claude/agents/refs/artist-art-style-catalog.md +0 -533
- package/project-template-v2/.claude/agents/refs/artist-color-audio-recipes.md +0 -153
- package/project-template-v2/.claude/agents/refs/artist-dimension-axis.md +0 -27
- package/project-template-v2/.claude/agents/refs/artist-master-composite-recipes.md +0 -208
- package/project-template-v2/.claude/agents/refs/atom-skill-library.md +0 -81
- package/project-template-v2/.claude/agents/refs/developer-impl-cookbook.md +0 -432
- package/project-template-v2/.claude/agents/refs/framework-5-component-filter.md +0 -252
- package/project-template-v2/.claude/agents/refs/framework-game-feel-juice.md +0 -266
- package/project-template-v2/.claude/agents/refs/framework-mda.md +0 -147
- package/project-template-v2/.claude/agents/refs/game-designer-gameplay-sufficiency.md +0 -123
- package/project-template-v2/.claude/agents/refs/ta-3d-flip-recipe.md +0 -88
- package/project-template-v2/.claude/agents/refs/ta-atlas-deliverable-standard.md +0 -67
- package/project-template-v2/.claude/agents/refs/ta-batch-pipeline-recipes.md +0 -120
- package/project-template-v2/.claude/agents/refs/ta-image-generation-detail.md +0 -300
- package/project-template-v2/.claude/agents/refs/ta-image-ops-reference.md +0 -495
- package/project-template-v2/.claude/agents/refs/ta-pipeline-cookbook.md +0 -1141
- package/project-template-v2/.claude/agents/refs/ta-tools-reference.md +0 -111
- package/project-template-v2/.claude/agents/refs/ta-vfx-preset-catalog.md +0 -365
- package/project-template-v2/.claude/agents/refs/threejs-cannon-pitfalls.md +0 -412
- package/project-template-v2/.claude/agents/reviewer.md +0 -75
- package/project-template-v2/.claude/agents/technical-artist.md +0 -86
- package/project-template-v2/.claude/hooks/snapshot-milestone.mjs +0 -243
- package/project-template-v2/.claude/hooks/user-prompt.mjs +0 -133
- package/project-template-v2/.claude/settings.json +0 -33
- package/project-template-v2/.claude/skills/brainstorming/SKILL.md +0 -161
- package/project-template-v2/.claude/skills/brainstorming/scripts/frame-template.html +0 -270
- package/project-template-v2/.claude/skills/brainstorming/scripts/helper.js +0 -177
- package/project-template-v2/.claude/skills/brainstorming/scripts/server.cjs +0 -354
- package/project-template-v2/.claude/skills/brainstorming/scripts/start-server.sh +0 -148
- package/project-template-v2/.claude/skills/brainstorming/scripts/stop-server.sh +0 -56
- package/project-template-v2/.claude/skills/brainstorming/scripts/wait-for-selection.sh +0 -62
- package/project-template-v2/.claude/skills/brainstorming/spec-document-reviewer-prompt.md +0 -49
- package/project-template-v2/.claude/skills/brainstorming/visual-companion.md +0 -309
- package/project-template-v2/.claude/skills/playcraft-ad-psychology/SKILL.md +0 -182
- package/project-template-v2/.claude/skills/playcraft-art-style-guide/SKILL.md +0 -123
- package/project-template-v2/.claude/skills/playcraft-asset-state-sheet/SKILL.md +0 -205
- package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +0 -280
- package/project-template-v2/.claude/skills/playcraft-batch-pipeline/SKILL.md +0 -184
- package/project-template-v2/.claude/skills/playcraft-build-optimizer/SKILL.md +0 -306
- package/project-template-v2/.claude/skills/playcraft-image-generation/SKILL.md +0 -298
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/build-sprite-sheet.template.mjs +0 -123
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/compare-style.template.mjs +0 -254
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +0 -324
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +0 -97
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/gen-edit-variants.template.mjs +0 -118
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +0 -137
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +0 -397
- package/project-template-v2/.claude/skills/playcraft-image-generation/reference/validate-sprite-sheet.template.mjs +0 -296
- package/project-template-v2/.claude/skills/playcraft-image-ops/SKILL.md +0 -122
- package/project-template-v2/.claude/skills/playcraft-image-processing/SKILL.md +0 -219
- package/project-template-v2/.claude/skills/playcraft-masking/SKILL.md +0 -373
- package/project-template-v2/.claude/skills/playcraft-playable-optimization/SKILL.md +0 -161
- package/project-template-v2/.claude/skills/playcraft-research/SKILL.md +0 -215
- package/project-template-v2/.claude/skills/playcraft-skill-recommender/SKILL.md +0 -382
- package/project-template-v2/.claude/skills/playcraft-sprite-generation/SKILL.md +0 -423
- package/project-template-v2/.claude/skills/playcraft-sprite-remix/SKILL.md +0 -158
- package/project-template-v2/.claude/skills/playcraft-sprite-sheet/SKILL.md +0 -100
- package/project-template-v2/.claude/skills/playcraft-storyboard/SKILL.md +0 -167
- package/project-template-v2/.claude/skills/playcraft-style-qa/SKILL.md +0 -270
- package/project-template-v2/.claude/skills/playcraft-text-rendering/SKILL.md +0 -236
- package/project-template-v2/.claude/skills/playcraft-vfx-animation/SKILL.md +0 -130
- package/project-template-v2/.claude/skills/playwright-cli/SKILL.md +0 -390
- package/project-template-v2/.claude/skills/playwright-cli/references/element-attributes.md +0 -23
- package/project-template-v2/.claude/skills/playwright-cli/references/playwright-tests.md +0 -39
- package/project-template-v2/.claude/skills/playwright-cli/references/request-mocking.md +0 -87
- package/project-template-v2/.claude/skills/playwright-cli/references/running-code.md +0 -240
- package/project-template-v2/.claude/skills/playwright-cli/references/session-management.md +0 -226
- package/project-template-v2/.claude/skills/playwright-cli/references/spec-driven-testing.md +0 -308
- package/project-template-v2/.claude/skills/playwright-cli/references/storage-state.md +0 -275
- package/project-template-v2/.claude/skills/playwright-cli/references/test-generation.md +0 -134
- package/project-template-v2/.claude/skills/playwright-cli/references/tracing.md +0 -142
- package/project-template-v2/.claude/skills/playwright-cli/references/video-recording.md +0 -153
- package/project-template-v2/.claude/skills/session-analyzer/SKILL.md +0 -386
- package/project-template-v2/.claude/skills/session-analyzer/scripts/execution-breakdown.mjs +0 -182
- package/project-template-v2/.claude/skills/session-analyzer/scripts/find-turns.mjs +0 -72
- package/project-template-v2/.claude/skills/session-analyzer/scripts/heavy-output.mjs +0 -121
- package/project-template-v2/.claude/skills/session-analyzer/scripts/resolve-session.mjs +0 -102
- package/project-template-v2/.claude/skills/session-analyzer/scripts/subagent-stats.mjs +0 -127
- package/project-template-v2/.claude/skills/session-analyzer/scripts/subagent-tool-timeline.mjs +0 -106
- package/project-template-v2/.claude/skills/session-analyzer/scripts/time-gaps.mjs +0 -128
- package/project-template-v2/.claude/skills/session-analyzer/scripts/turn-timeline.mjs +0 -67
- package/project-template-v2/.claude/snapshot.mjs +0 -263
- package/project-template-v2/.playcraft/skills.lock.json +0 -152
- package/project-template-v2/CLAUDE.md +0 -146
- package/project-template-v2/assets/audio/bgm/.gitkeep +0 -0
- package/project-template-v2/assets/audio/sfx/.gitkeep +0 -0
- package/project-template-v2/assets/bundles/.gitkeep +0 -0
- package/project-template-v2/assets/images/bg/.gitkeep +0 -0
- package/project-template-v2/assets/images/reference/.gitkeep +0 -0
- package/project-template-v2/assets/images/storyboard/.gitkeep +0 -0
- package/project-template-v2/assets/images/tiles/.gitkeep +0 -0
- package/project-template-v2/assets/images/ui/.gitkeep +0 -0
- package/project-template-v2/assets/images/vfx/.gitkeep +0 -0
- package/project-template-v2/assets/models/.gitkeep +0 -0
- package/project-template-v2/docs/harness/iteration-1/context-flow.md +0 -254
- package/project-template-v2/docs/harness/iteration-1/generate-flow.md +0 -91
- package/project-template-v2/docs/harness/iteration-1/ideate-flow.md +0 -214
- package/project-template-v2/docs/harness/iteration-1/optimize-flow.md +0 -75
- package/project-template-v2/docs/harness/iteration-1/wrapup-flow.md +0 -63
- package/project-template-v2/docs/harness/iteration-2/context-flow.md +0 -223
- package/project-template-v2/docs/harness/iteration-2/generate-flow.md +0 -129
- package/project-template-v2/docs/harness/iteration-2/ideate-flow.md +0 -267
- package/project-template-v2/docs/harness/iteration-2/optimize-flow.md +0 -164
- package/project-template-v2/docs/harness/iteration-2/wrapup-flow.md +0 -115
- package/project-template-v2/docs/harness/orchestrator-flow.md +0 -364
- package/project-template-v2/docs/project-state.json +0 -60
- package/project-template-v2/docs/project-state.md +0 -72
- package/project-template-v2/docs/standards/README.md +0 -225
- package/project-template-v2/docs/standards/agent-behavior-standards.md +0 -174
- package/project-template-v2/docs/standards/artifacts/design-brief.md +0 -19
- package/project-template-v2/docs/standards/artifacts/design.md +0 -22
- package/project-template-v2/docs/standards/artifacts/game-code.md +0 -41
- package/project-template-v2/docs/standards/artifacts/todo-list.md +0 -41
- package/project-template-v2/docs/standards/iter1-agent-behavior-standards.md +0 -343
- package/project-template-v2/game/index.ts +0 -18
- package/project-template-v2/globals.d.ts +0 -51
- package/project-template-v2/index.css +0 -34
- package/project-template-v2/index.html +0 -18
- package/project-template-v2/main.ts +0 -9
- package/project-template-v2/package.json +0 -46
- package/project-template-v2/skills/_shared/scripts/dispatch-clear.mjs +0 -31
- package/project-template-v2/skills/_shared/scripts/dispatch-set.mjs +0 -86
- package/project-template-v2/skills/_shared/scripts/dod-check.mjs +0 -153
- package/project-template-v2/skills/_shared/scripts/handoff-append.mjs +0 -70
- package/project-template-v2/skills/_shared/scripts/lib/validator-artifacts.mjs +0 -91
- package/project-template-v2/skills/_shared/scripts/pipeline/dod-config.mjs +0 -131
- package/project-template-v2/skills/_shared/scripts/pipeline/index.mjs +0 -80
- package/project-template-v2/skills/_shared/scripts/pipeline/iteration-1-core.mjs +0 -90
- package/project-template-v2/skills/_shared/scripts/pipeline/iteration-2-wrap.mjs +0 -93
- package/project-template-v2/skills/_shared/scripts/pipeline/iteration-3-visual.mjs +0 -23
- package/project-template-v2/skills/_shared/scripts/render-project-state.mjs +0 -230
- package/project-template-v2/skills/_shared/scripts/state-advance-stage.mjs +0 -58
- package/project-template-v2/skills/_shared/scripts/state-get.mjs +0 -339
- package/project-template-v2/skills/_shared/scripts/state-handoff.mjs +0 -39
- package/project-template-v2/skills/_shared/scripts/state-set.mjs +0 -94
- package/project-template-v2/skills/_shared/scripts/state-store.mjs +0 -783
- package/project-template-v2/skills/_shared/scripts/todo-add.mjs +0 -88
- package/project-template-v2/skills/_shared/scripts/todo-get.mjs +0 -130
- package/project-template-v2/skills/_shared/scripts/todo-remove.mjs +0 -33
- package/project-template-v2/skills/_shared/scripts/todo-set.mjs +0 -47
- package/project-template-v2/skills/_shared/scripts/verify-asset-code-sync.mjs +0 -268
- package/project-template-v2/skills/_shared/scripts/verify-env.mjs +0 -285
- package/project-template-v2/skills/_shared/scripts/verify-placeholders.mjs +0 -161
- package/project-template-v2/skills/playable-autoplay/SKILL.md +0 -176
- package/project-template-v2/skills/playable-autoplay/agents/openai.yaml +0 -4
- package/project-template-v2/skills/playable-debug/SKILL.md +0 -116
- package/project-template-v2/skills/playable-debug/agents/openai.yaml +0 -4
- package/project-template-v2/skills/playable-debug/references/debug-config.md +0 -40
- package/project-template-v2/skills/playable-record/SKILL.md +0 -140
- package/project-template-v2/skills/playable-record/scripts/lib/dev-server.mjs +0 -104
- package/project-template-v2/skills/playable-record/scripts/lib/record-audio-bridge.js +0 -141
- package/project-template-v2/skills/playable-record/scripts/record-playable.mjs +0 -425
- package/project-template-v2/skills/playable-record/scripts/verify-contract.mjs +0 -261
- package/project-template-v2/skills/playable-record/scripts/verify-firstwin.mjs +0 -398
- package/project-template-v2/skills/playable-record/scripts/verify-fusion.mjs +0 -322
- package/project-template-v2/skills/playable-record/scripts/verify-lifecycle.mjs +0 -105
- package/project-template-v2/skills/playable-record/scripts/verify-vlm-video.mjs +0 -230
- package/project-template-v2/skills/playable-validate/SKILL.md +0 -61
- package/project-template-v2/skills/playable-validate/validation-rules.md +0 -14
- package/project-template-v2/skills/playable-verify-ui/SKILL.md +0 -63
- package/project-template-v2/skills/playable-verify-ui/scripts/lib/__init__.py +0 -1
- package/project-template-v2/skills/playable-verify-ui/scripts/lib/config_expr.js +0 -218
- package/project-template-v2/skills/playable-verify-ui/scripts/lib/node_utils.js +0 -51
- package/project-template-v2/skills/playable-verify-ui/scripts/lib/profile_loader.py +0 -59
- package/project-template-v2/skills/playable-verify-ui/scripts/lib/report.py +0 -106
- package/project-template-v2/skills/playable-verify-ui/scripts/lib/runtime_contract.js +0 -116
- package/project-template-v2/skills/playable-verify-ui/scripts/verify_fetch_antipatterns.py +0 -59
- package/project-template-v2/skills/playable-verify-ui/scripts/verify_hardcoded_layout.js +0 -64
- package/project-template-v2/skills/playable-verify-ui/scripts/verify_runtime_contract.js +0 -58
- package/project-template-v2/skills/playable-verify-ui/scripts/verify_text_overlap.py +0 -333
- package/project-template-v2/ta-workspace/scripts/.gitkeep +0 -0
- package/project-template-v2/tsconfig.json +0 -20
- package/project-template-v2/vite.config.ts +0 -27
- package/project-templates/batch-locks/phaser.yaml +0 -922
- package/project-templates/batch-locks/threejs.yaml +0 -978
- package/project-templates/empty/AGENTS.md +0 -39
- package/project-templates/empty/gitignore +0 -33
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { constants } from 'node:fs';
|
|
2
|
-
import { lstat, mkdir, open, realpath } from 'node:fs/promises';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
|
-
const ENTRY = '/.playcraft/video-replication/';
|
|
5
|
-
/** Keep local analysis evidence out of a repository without changing its tracked ignore rules. */
|
|
6
|
-
export async function ensureVideoEvidenceGitExcluded(directory) {
|
|
7
|
-
let root = await realpath(directory);
|
|
8
|
-
while (true) {
|
|
9
|
-
const marker = await lstat(join(root, '.git')).catch((error) => {
|
|
10
|
-
if (error.code === 'ENOENT')
|
|
11
|
-
return undefined;
|
|
12
|
-
throw error;
|
|
13
|
-
});
|
|
14
|
-
if (marker) {
|
|
15
|
-
// Cloud workspaces own a real .git directory; never follow a redirected Git metadata tree.
|
|
16
|
-
if (!marker.isDirectory() || marker.isSymbolicLink())
|
|
17
|
-
return false;
|
|
18
|
-
try {
|
|
19
|
-
await appendExclude(root);
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
|
-
catch (error) {
|
|
23
|
-
if (['EACCES', 'EPERM', 'EROFS'].includes(error.code ?? ''))
|
|
24
|
-
return false;
|
|
25
|
-
throw error;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
const parent = dirname(root);
|
|
29
|
-
if (parent === root)
|
|
30
|
-
return true;
|
|
31
|
-
root = parent;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
async function appendExclude(root) {
|
|
35
|
-
const flags = constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW;
|
|
36
|
-
const handles = [];
|
|
37
|
-
let file;
|
|
38
|
-
try {
|
|
39
|
-
const directory = await open(root, flags);
|
|
40
|
-
handles.push(directory);
|
|
41
|
-
const pinned = (handle, fallback) => process.platform === 'linux' ? `/proc/self/fd/${handle.fd}` : fallback;
|
|
42
|
-
const git = await open(join(pinned(directory, root), '.git'), flags);
|
|
43
|
-
handles.push(git);
|
|
44
|
-
const gitPath = pinned(git, join(root, '.git'));
|
|
45
|
-
await mkdir(join(gitPath, 'info')).catch((error) => {
|
|
46
|
-
if (error.code !== 'EEXIST')
|
|
47
|
-
throw error;
|
|
48
|
-
});
|
|
49
|
-
const info = await open(join(gitPath, 'info'), flags);
|
|
50
|
-
handles.push(info);
|
|
51
|
-
const exclude = join(pinned(info, join(root, '.git/info')), 'exclude');
|
|
52
|
-
const readable = await open(exclude, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK)
|
|
53
|
-
.catch((error) => { if (error.code === 'ENOENT')
|
|
54
|
-
return undefined; throw error; });
|
|
55
|
-
if (readable) {
|
|
56
|
-
try {
|
|
57
|
-
const stat = await readable.stat();
|
|
58
|
-
if (!stat.isFile() || stat.nlink !== 1 || stat.size > 1024 * 1024)
|
|
59
|
-
throw new Error('invalid_git_exclude');
|
|
60
|
-
if ((await readable.readFile('utf8')).split('\n').some(line => line.trim() === ENTRY))
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
finally {
|
|
64
|
-
await readable.close();
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
file = await open(exclude, constants.O_RDWR | constants.O_CREAT | constants.O_APPEND | constants.O_NOFOLLOW | constants.O_NONBLOCK, 0o666);
|
|
68
|
-
const stat = await file.stat();
|
|
69
|
-
if (!stat.isFile() || stat.nlink !== 1 || stat.size > 1024 * 1024)
|
|
70
|
-
throw new Error('invalid_git_exclude');
|
|
71
|
-
const current = await file.readFile('utf8');
|
|
72
|
-
if (!current.split('\n').some(line => line.trim() === ENTRY)) {
|
|
73
|
-
await file.writeFile(`${current && !current.endsWith('\n') ? '\n' : ''}${ENTRY}\n`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
finally {
|
|
77
|
-
await file?.close();
|
|
78
|
-
for (const handle of handles.reverse())
|
|
79
|
-
await handle.close();
|
|
80
|
-
}
|
|
81
|
-
}
|
|
@@ -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 => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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
|
-
}
|