@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.
- package/README.md +3 -38
- 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 -34
- 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/skills/playcraft-image-generation/SKILL.md +81 -23
- package/project-templates/phaser/.claude/skills/playcraft-image-generation/SKILL.md +81 -23
- package/project-templates/threejs/.claude/skills/playcraft-image-generation/SKILL.md +81 -23
- 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/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-runtime/codex/app-server.js +0 -636
- package/dist/workspace-runtime/codex/config-toml.js +0 -107
- package/dist/workspace-runtime/codex/jsonrpc-stdio.js +0 -123
- package/dist/workspace-runtime/codex/launcher-client.js +0 -163
- package/dist/workspace-runtime/codex/loopback.js +0 -143
- package/dist/workspace-runtime/codex/model-broker.js +0 -258
- package/dist/workspace-runtime/codex/native-adapter.js +0 -9
- 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 -1057
- package/dist/workspace-runtime/previews/publish.js +0 -259
- 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 -90
- package/dist/workspace-runtime/production/processes.js +0 -86
- package/dist/workspace-runtime/production/save-command.js +0 -24
- package/dist/workspace-runtime/production/source-copy.js +0 -91
- package/dist/workspace-runtime/production/stages.js +0 -202
- package/dist/workspace-runtime/production/storage.js +0 -32
- package/dist/workspace-runtime/serve.js +0 -202
- 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 -903
- 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 -984
- 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 -294
- package/dist/workspace-runtime/server/types.js +0 -1
- package/dist/workspace-runtime/workspaces/artifact-events.js +0 -55
- package/dist/workspace-runtime/workspaces/artifact-files.js +0 -195
- package/dist/workspace-runtime/workspaces/artifact-receipts.js +0 -89
- package/dist/workspace-runtime/workspaces/artifacts.js +0 -260
- package/dist/workspace-runtime/workspaces/attachment-content.js +0 -111
- package/dist/workspace-runtime/workspaces/attachments.js +0 -175
- 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/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 -113
- package/dist/workspace-runtime/workspaces/directory-access.js +0 -29
- 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.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 -517
- package/dist/workspace-runtime/workspaces/json5-edit.js +0 -170
- 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 -226
- 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/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 -25
- 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 => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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
|
-
}
|