@playcraft/cli 0.1.66 → 0.1.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/assets/client.js +4 -2
  2. package/dist/assets/messages.js +2 -0
  3. package/dist/audio-source/messages.js +35 -0
  4. package/dist/commands/assets.js +1 -1
  5. package/dist/commands/audio-program.js +82 -0
  6. package/dist/commands/audio-source.js +109 -0
  7. package/dist/commands/audio.js +13 -4
  8. package/dist/commands/build.js +4 -2
  9. package/dist/commands/tools-context.js +46 -0
  10. package/dist/commands/tools.js +2 -0
  11. package/dist/project-skills/builtin-import.js +69 -22
  12. package/dist/project-skills/commands.js +11 -2
  13. package/dist/project-skills/lifecycle.js +1 -1
  14. package/dist/project-skills/local-store.js +90 -31
  15. package/dist/project-skills/messages.js +16 -0
  16. package/dist/project-skills/migrate-lock.js +166 -0
  17. package/dist/project-skills/remote-cache.js +36 -5
  18. package/dist/utils/agent-api-client.js +18 -1
  19. package/dist/utils/agent-generation-provenance.js +8 -2
  20. package/dist/utils/audio-export.js +249 -0
  21. package/dist/utils/audio-processor.js +22 -99
  22. package/dist/utils/execution-context-bridge.js +28 -0
  23. package/dist/workspace-runtime/batch/control.js +13 -2
  24. package/dist/workspace-runtime/batch/generate.js +43 -2
  25. package/dist/workspace-runtime/batch/prepare.js +4 -0
  26. package/dist/workspace-runtime/batch/skill-artifacts.js +103 -0
  27. package/dist/workspace-runtime/batch/source-capture.js +6 -8
  28. package/dist/workspace-runtime/codex/app-server.js +26 -2
  29. package/dist/workspace-runtime/codex/execution-context-broker.js +61 -0
  30. package/dist/workspace-runtime/codex/execution-context-tools.js +37 -0
  31. package/dist/workspace-runtime/codex/generation-provenance-broker.js +9 -6
  32. package/dist/workspace-runtime/codex/level-editor-tools.js +18 -0
  33. package/dist/workspace-runtime/codex/model-broker.js +15 -2
  34. package/dist/workspace-runtime/codex/scheduled-task-tools.js +1 -1
  35. package/dist/workspace-runtime/previews/finalize.js +1 -1
  36. package/dist/workspace-runtime/previews/output.js +4 -1
  37. package/dist/workspace-runtime/previews/publish.js +5 -2
  38. package/dist/workspace-runtime/previews/runtime-preview.js +38 -3
  39. package/dist/workspace-runtime/serve.js +6 -0
  40. package/dist/workspace-runtime/server/accept-operation.js +85 -13
  41. package/dist/workspace-runtime/server/audio-assets.js +257 -0
  42. package/dist/workspace-runtime/server/audio-export.js +136 -0
  43. package/dist/workspace-runtime/server/directory-access.js +222 -67
  44. package/dist/workspace-runtime/server/dispatch.js +252 -29
  45. package/dist/workspace-runtime/server/http.js +97 -3
  46. package/dist/workspace-runtime/server/operation-record.js +1 -1
  47. package/dist/workspace-runtime/workspaces/artifact-files.js +10 -3
  48. package/dist/workspace-runtime/workspaces/artifacts.js +47 -6
  49. package/dist/workspace-runtime/workspaces/asset-library-import.js +27 -7
  50. package/dist/workspace-runtime/workspaces/audio-assets/artifact-package.js +104 -0
  51. package/dist/workspace-runtime/workspaces/audio-assets/binding-usage.js +71 -0
  52. package/dist/workspace-runtime/workspaces/audio-assets/build-inputs.js +80 -0
  53. package/dist/workspace-runtime/workspaces/audio-assets/library-import.js +149 -0
  54. package/dist/workspace-runtime/workspaces/audio-assets/library-package.js +26 -0
  55. package/dist/workspace-runtime/workspaces/audio-assets/program-artifact-audit.js +90 -0
  56. package/dist/workspace-runtime/workspaces/audio-assets/program-browser.js +194 -0
  57. package/dist/workspace-runtime/workspaces/audio-assets/program-build-inputs.js +122 -0
  58. package/dist/workspace-runtime/workspaces/audio-assets/program-engine.js +39 -0
  59. package/dist/workspace-runtime/workspaces/audio-assets/program-package.js +289 -0
  60. package/dist/workspace-runtime/workspaces/audio-assets/program-variant-build.js +217 -0
  61. package/dist/workspace-runtime/workspaces/audio-assets/references.js +107 -0
  62. package/dist/workspace-runtime/workspaces/audio-assets/render-worker.js +117 -0
  63. package/dist/workspace-runtime/workspaces/audio-assets/renderer.js +128 -0
  64. package/dist/workspace-runtime/workspaces/audio-assets/representations.js +73 -0
  65. package/dist/workspace-runtime/workspaces/audio-assets/service.js +459 -0
  66. package/dist/workspace-runtime/workspaces/audio-assets/storage.js +125 -0
  67. package/dist/workspace-runtime/workspaces/build-isolation.js +2 -0
  68. package/dist/workspace-runtime/workspaces/composer-context.js +40 -1
  69. package/dist/workspace-runtime/workspaces/empty-template.js +1 -1
  70. package/dist/workspace-runtime/workspaces/execution-context.js +24 -0
  71. package/dist/workspace-runtime/workspaces/files.js +3 -5
  72. package/dist/workspace-runtime/workspaces/git.js +7 -0
  73. package/dist/workspace-runtime/workspaces/level-editor-agent-store.js +143 -0
  74. package/dist/workspace-runtime/workspaces/level-editor-agent.js +410 -0
  75. package/dist/workspace-runtime/workspaces/level-editor-atomic.js +424 -0
  76. package/dist/workspace-runtime/workspaces/level-editor-extensions.js +140 -0
  77. package/dist/workspace-runtime/workspaces/level-editor-format.js +75 -0
  78. package/dist/workspace-runtime/workspaces/level-editor-history-store.js +250 -0
  79. package/dist/workspace-runtime/workspaces/level-editor-json5.js +132 -0
  80. package/dist/workspace-runtime/workspaces/level-editor-requests.js +724 -0
  81. package/dist/workspace-runtime/workspaces/level-editor.js +454 -0
  82. package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +62 -22
  83. package/dist/workspace-runtime/workspaces/parameter-production.js +1 -1
  84. package/dist/workspace-runtime/workspaces/remix-reference-skill.js +4 -3
  85. package/dist/workspace-runtime/workspaces/skill-artifact-loader.js +19 -5
  86. package/dist/workspace-runtime/workspaces/skills.js +48 -13
  87. package/package.json +9 -4
  88. package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
  89. package/project-template/.claude/skills/playcraft-level-editor/SKILL.md +34 -0
  90. package/project-template/.claude/skills/playcraft-level-editor/references/authoring-contract.json +3475 -0
  91. package/project-template/.claude/skills/playcraft-level-editor/references/extensions.md +37 -0
  92. package/project-template/.claude/skills/playcraft-level-editor/references/preview.md +15 -0
  93. package/project-template/.claude/skills/playcraft-level-editor/references/protocol-v1.md +35 -0
  94. package/project-template/vite.config.ts +1 -1
  95. package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
  96. package/project-template-v2/.playcraft/skills.lock.json +9 -2
  97. package/project-template-v2/vite.config.ts +1 -1
  98. package/project-templates/empty/AGENTS.md +2 -0
  99. package/project-templates/phaser/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
  100. package/project-templates/phaser/vite.config.ts +1 -1
  101. package/project-templates/playable-template-phaser/vite.config.ts +1 -1
  102. package/project-templates/threejs/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
  103. package/project-templates/threejs/vite.config.ts +1 -1
  104. package/templates/preview-runtime/v1/README.md +12 -0
  105. package/templates/preview-runtime/v1/build-preview.mjs +17 -8
  106. package/templates/preview-runtime/v1/manifest.json +9 -6
  107. package/templates/preview-runtime/v1/release-guard.mjs +1 -1
  108. package/templates/preview-runtime/v1/runtime.d.mts +1 -3
  109. package/templates/preview-runtime/v1/runtime.d.ts +1 -3
  110. package/templates/preview-runtime/v1/runtime.mjs +1142 -257
  111. package/templates/preview-runtime/v1/types/host.d.ts +29 -0
  112. package/templates/preview-runtime/v1/types/index.d.ts +6 -0
  113. package/templates/preview-runtime/v1/types/level-editor.d.ts +123 -0
@@ -0,0 +1,410 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import JSON5 from 'json5';
3
+ import { LevelEditorError, LEVEL_EDITOR_COMPONENT_CATALOG, LEVEL_EDITOR_AUTHORING_CONTRACT, levelEditorTaskRequestSchema, levelEditorTaskReportSchema, levelEditorVerifyPayloadSchema, WORKSPACE_FILE_CONTENT_MAX, } from '@playcraft/common/cloud-workspace/runtime';
4
+ import { parameterInputSnapshot } from './parameter-agent-inputs.js';
5
+ import { readLevelEditorFile, readWorkspaceLevelEditor, levelEditorDigest, } from './level-editor.js';
6
+ import { validateLevelEditorExtensions, levelEditorExtensionPaths, } from './level-editor-extensions.js';
7
+ import { revisionScope } from './registry.js';
8
+ import { runDeveloperCommand } from './developer-command.js';
9
+ import { prepareParameterProductionCheck, inspectParameterProductionCheck, } from './parameter-production.js';
10
+ import { levelEditorTaskWorkspace, readPrivateLevelEditorTask, writePrivateLevelEditorTask, listPrivateLevelEditorTaskIds, preserveLevelEditorBaseline, } from './level-editor-agent-store.js';
11
+ const current = (state, task) => task.record.generation === state.config.generation &&
12
+ task.record.runtimeEpoch === state.runtimeEpoch;
13
+ const codeOf = (error) => error instanceof Error && 'code' in error && typeof error.code === 'string'
14
+ ? error.code
15
+ : 'level_editor_verification_failed';
16
+ async function inputSnapshot(root, declaredPaths = [], readContext) {
17
+ const original = await parameterInputSnapshot(root), sources = new Map(original.sources.map((item) => [item.path, item.sha256]));
18
+ const explicit = new Set(declaredPaths);
19
+ const scope = readContext && ('operation' in readContext ? readContext.operation : readContext);
20
+ const revisions = readContext && scope
21
+ ? (readContext.state.journal.fileRevisions?.(revisionScope(readContext.state, scope.workspaceId, scope.directoryName)) ?? {})
22
+ : {};
23
+ const snapshot = await readWorkspaceLevelEditor(root, revisions, readContext);
24
+ if (snapshot.definition)
25
+ for (const path of [
26
+ snapshot.definitionPath,
27
+ ...Object.values(snapshot.definition.documents).flatMap((item) => [item.path, item.schema]),
28
+ ...Object.values(snapshot.definition.preview ?? {}),
29
+ ...levelEditorExtensionPaths(snapshot.definition),
30
+ ])
31
+ explicit.add(path);
32
+ try {
33
+ const manifest = JSON5.parse((await readLevelEditorFile(root, '.playcraft/workspace.json')).toString('utf8'));
34
+ for (const path of manifest.previewRuntime?.gameFilePaths ?? [])
35
+ explicit.add(path);
36
+ }
37
+ catch (error) {
38
+ if (error.code !== 'ENOENT')
39
+ throw error;
40
+ }
41
+ // The automatic source walk has its own 32 MiB budget. Explicit report/build inputs share
42
+ // an additional bounded read budget, including managed files excluded from that source walk.
43
+ let remaining = 32 * 1024 * 1024;
44
+ for (const path of explicit) {
45
+ if (remaining <= 0)
46
+ throw new LevelEditorError('level_editor_input_budget');
47
+ let bytes;
48
+ try {
49
+ bytes = await readLevelEditorFile(root, path, Math.min(WORKSPACE_FILE_CONTENT_MAX, remaining));
50
+ }
51
+ catch (error) {
52
+ if (error.code === 'ENOENT')
53
+ throw new LevelEditorError('level_editor_evidence_missing', [path]);
54
+ throw error;
55
+ }
56
+ remaining -= bytes.byteLength;
57
+ sources.set(path, levelEditorDigest(bytes));
58
+ }
59
+ const sorted = [...sources]
60
+ .sort(([a], [b]) => a.localeCompare(b))
61
+ .map(([path, sha256]) => ({ path, sha256 }));
62
+ return { sources: sorted, digest: levelEditorDigest(JSON.stringify(sorted)) };
63
+ }
64
+ function owner(state, operation, session = true) {
65
+ if ((session && !operation.sessionId) ||
66
+ operation.directoryName === 'root' ||
67
+ operation.environment !== state.config.environment ||
68
+ operation.userId !== state.config.userId ||
69
+ (operation.generation !== undefined && operation.generation !== state.config.generation) ||
70
+ (operation.runtimeEpoch !== undefined && operation.runtimeEpoch !== state.runtimeEpoch))
71
+ throw new LevelEditorError('level_editor_scope_invalid');
72
+ return {
73
+ workspaceId: levelEditorTaskWorkspace(state, operation.workspaceId),
74
+ directoryName: operation.directoryName ?? 'basic',
75
+ sessionId: operation.sessionId ?? '',
76
+ };
77
+ }
78
+ async function update(state, task, patch, root) {
79
+ task.record = { ...task.record, ...patch, updatedAt: new Date().toISOString() };
80
+ await writePrivateLevelEditorTask(state, task, root);
81
+ return task.record;
82
+ }
83
+ async function inspectReport(root, task, readContext) {
84
+ const bytes = await readLevelEditorFile(root, task.record.reportPath, 1024 * 1024);
85
+ const report = levelEditorTaskReportSchema.parse(JSON.parse(bytes.toString('utf8')));
86
+ if (report.taskId !== task.record.taskId)
87
+ throw new LevelEditorError('level_editor_report_identity');
88
+ const inputs = await inputSnapshot(root, report.sources.map((source) => source.path), readContext), sourceMap = new Map(inputs.sources.map((item) => [item.path, item.sha256]));
89
+ const declared = new Set(report.sources.map((item) => item.path));
90
+ for (const source of report.sources)
91
+ if (sourceMap.get(source.path) !== source.sha256)
92
+ throw new LevelEditorError('level_editor_report_stale');
93
+ for (const consumer of report.consumers) {
94
+ if (consumer.path.startsWith('.playcraft/preview-runtime/'))
95
+ throw new LevelEditorError('level_editor_consumer_evidence_invalid');
96
+ if (!declared.has(consumer.path))
97
+ throw new LevelEditorError('level_editor_consumer_evidence_missing');
98
+ const text = (await readLevelEditorFile(root, consumer.path)).toString('utf8');
99
+ if (!text.trim() || (consumer.line && consumer.line > text.split('\n').length))
100
+ throw new LevelEditorError('level_editor_consumer_evidence_invalid');
101
+ }
102
+ return { report, digest: inputs.digest, reportHash: levelEditorDigest(bytes), sources: declared };
103
+ }
104
+ export async function prepareLevelEditorTask(state, operation, value, root, skill) {
105
+ const request = levelEditorTaskRequestSchema.parse(value), scope = owner(state, operation);
106
+ if (operation.kind !== 'session.turn' || request.taskId !== operation.clientRequestId)
107
+ throw new LevelEditorError('level_editor_task_identity');
108
+ if (!skill?.entryKey || !/^[a-f0-9]{64}$/.test(skill.lockHash) || !skill.path)
109
+ throw new LevelEditorError('level_editor_skill_unavailable');
110
+ const existing = await readPrivateLevelEditorTask(state, scope.workspaceId, scope.directoryName, request.taskId, root);
111
+ if (existing &&
112
+ (!current(state, existing) ||
113
+ existing.record.sessionId !== scope.sessionId ||
114
+ JSON.stringify(existing.request) !== JSON.stringify(request) ||
115
+ existing.record.initializationSkill?.entryKey !== skill.entryKey ||
116
+ existing.record.initializationSkill?.lockHash !== skill.lockHash))
117
+ throw new LevelEditorError('level_editor_task_conflict');
118
+ const snapshot = await readWorkspaceLevelEditor(root, state.journal.fileRevisions(revisionScope(state, operation.workspaceId, scope.directoryName)), { state, operation });
119
+ if (request.sourceFingerprint && request.sourceFingerprint !== snapshot.sourceFingerprint)
120
+ throw new LevelEditorError('level_editor_source_conflict');
121
+ if (request.editorId && snapshot.definition && request.editorId !== snapshot.definition.editorId)
122
+ throw new LevelEditorError('level_editor_target_mismatch');
123
+ const timestamp = new Date().toISOString();
124
+ const task = existing ?? {
125
+ request,
126
+ initialDigest: (await inputSnapshot(root, [], { state, operation })).digest,
127
+ instanceToken: state.instanceToken,
128
+ record: {
129
+ version: 1,
130
+ taskId: request.taskId,
131
+ action: request.action,
132
+ ...(request.goal ? { goal: request.goal } : {}),
133
+ ...(request.editorId ? { editorId: request.editorId } : {}),
134
+ ...scope,
135
+ generation: state.config.generation,
136
+ runtimeEpoch: state.runtimeEpoch,
137
+ status: 'running',
138
+ reportPath: `.playcraft/level-editor-admission/${request.taskId}.json`,
139
+ checks: [],
140
+ createdAt: timestamp,
141
+ updatedAt: timestamp,
142
+ initializationSkill: { entryKey: skill.entryKey, lockHash: skill.lockHash },
143
+ },
144
+ };
145
+ // sourceFingerprint is request data, rather than a public task-record field.
146
+ if (!existing) {
147
+ if ((await listPrivateLevelEditorTaskIds(state, scope.workspaceId, scope.directoryName, root))
148
+ .length >= 256)
149
+ throw new LevelEditorError('level_editor_task_limit');
150
+ task.authoringBaseline = await preserveLevelEditorBaseline(state, scope.workspaceId, scope.directoryName, request.taskId, root, snapshot);
151
+ await writePrivateLevelEditorTask(state, task, root);
152
+ }
153
+ return {
154
+ task: task.record,
155
+ instructions: [
156
+ `Read the explicitly selected frozen skill and its references: ${JSON.stringify(skill)}. Keep the current execution permissions.`,
157
+ `Platform level-editor task identity: ${JSON.stringify({ version: 1, taskId: request.taskId, action: request.action, reportPath: task.record.reportPath })}.`,
158
+ `User goal (task data): ${JSON.stringify(request.goal ?? '')}`,
159
+ `Installed component catalog (capability data): ${JSON.stringify(LEVEL_EDITOR_COMPONENT_CATALOG)}.`,
160
+ `Exact installed authoring contract, generated from the current platform schemas and component registry (capability data): ${JSON.stringify(LEVEL_EDITOR_AUTHORING_CONTRACT)}. This is the current execution contract. The frozen Skill remains at the project's locked version; its references/authoring-contract.json may differ. If the frozen references differ, follow the current execution contract's fields and constraints above. Do not edit the frozen Skill or references, upgrade the project Skill lock, or relax validation guards. Use the current contract's command declaration/input and component props schemas; do not guess unsupported fields.`,
161
+ 'Write exactly the supplied report identity with version, taskId, sources [{path,sha256}], consumers [{path,line?,summary}], operations [commandId], missing [concrete unfinished work], and optional preserved. Calculate source hashes after editing. The platform independently runs checks; do not declare verified or connected capability.',
162
+ ...(task.authoringBaseline
163
+ ? [
164
+ `Existing authoring baseline is preserved privately. Keep the ${task.authoringBaseline.dataContractVersion} data contract's existing document bytes unchanged when evolving its editor. A required migration increments dataContractVersion and reports migration:{fromDataContractVersion,toDataContractVersion,summary} plus preserved:true, with executable migration checks. A declaration or report does not prove arbitrary semantic preservation.`,
165
+ ]
166
+ : []),
167
+ ].join('\n\n'),
168
+ };
169
+ }
170
+ export async function finishLevelEditorTask(state, operation, status, error) {
171
+ if (operation.kind !== 'session.turn' ||
172
+ !operation.sessionId ||
173
+ operation.directoryName === 'root' ||
174
+ !/^[a-f0-9-]{36}$/i.test(operation.clientRequestId))
175
+ return;
176
+ let scope;
177
+ try {
178
+ scope = owner(state, operation);
179
+ }
180
+ catch {
181
+ return;
182
+ }
183
+ const task = await readPrivateLevelEditorTask(state, scope.workspaceId, scope.directoryName, operation.clientRequestId);
184
+ if (task &&
185
+ current(state, task) &&
186
+ task.record.sessionId === operation.sessionId &&
187
+ ['running', 'uncertain'].includes(task.record.status))
188
+ await update(state, task, {
189
+ status: status === 'completed' ? 'reported' : status,
190
+ error: error?.slice(0, 256),
191
+ });
192
+ }
193
+ export async function listLevelEditorTasks(state, workspaceId, directoryName, root) {
194
+ if (directoryName === 'root')
195
+ return [];
196
+ const tasks = [];
197
+ for (const id of await listPrivateLevelEditorTaskIds(state, levelEditorTaskWorkspace(state, workspaceId), directoryName, root)) {
198
+ let task = await readPrivateLevelEditorTask(state, levelEditorTaskWorkspace(state, workspaceId), directoryName, id, root);
199
+ if (!task)
200
+ continue;
201
+ if (!current(state, task)) {
202
+ tasks.push({ ...task.record, status: 'stale' });
203
+ continue;
204
+ }
205
+ if (['running', 'uncertain'].includes(task.record.status)) {
206
+ const operation = state.journal
207
+ .listOperations?.()
208
+ .map((item) => item.operation)
209
+ .find((item) => item.kind === 'session.turn' &&
210
+ item.clientRequestId === id &&
211
+ item.sessionId === task.record.sessionId);
212
+ if (operation &&
213
+ ['completed', 'failed', 'interrupted', 'uncertain'].includes(operation.status)) {
214
+ await finishLevelEditorTask(state, operation, operation.status, operation.error);
215
+ task = (await readPrivateLevelEditorTask(state, levelEditorTaskWorkspace(state, workspaceId), directoryName, id, root));
216
+ }
217
+ }
218
+ if (task.instanceToken !== state.instanceToken && task.record.status === 'checking')
219
+ await update(state, task, {
220
+ status: 'needs-verification',
221
+ receiptId: undefined,
222
+ error: 'level_editor_check_interrupted',
223
+ }, root);
224
+ if (task.instanceToken !== state.instanceToken && task.record.status === 'running')
225
+ await update(state, task, {
226
+ status: 'uncertain',
227
+ receiptId: undefined,
228
+ error: 'level_editor_turn_unconfirmed_after_restart',
229
+ }, root);
230
+ try {
231
+ if (!['running', 'uncertain', 'interrupted'].includes(task.record.status)) {
232
+ const checked = await inspectReport(root, task, {
233
+ state,
234
+ workspaceId: task.record.workspaceId,
235
+ directoryName: task.record.directoryName,
236
+ });
237
+ if ((task.record.sourceDigest && checked.digest !== task.record.sourceDigest) ||
238
+ (task.reportHash && task.reportHash !== checked.reportHash))
239
+ task.record = {
240
+ ...task.record,
241
+ status: 'stale',
242
+ error: 'level_editor_sources_changed',
243
+ receiptId: undefined,
244
+ };
245
+ tasks.push({ ...task.record, report: checked.report });
246
+ }
247
+ else
248
+ tasks.push(task.record);
249
+ }
250
+ catch (error) {
251
+ const code = codeOf(error);
252
+ const stale = code === 'level_editor_report_stale' || code === 'level_editor_sources_changed';
253
+ tasks.push({
254
+ ...task.record,
255
+ status: stale ? 'stale' : 'invalid',
256
+ error: stale ? code : 'level_editor_report_invalid',
257
+ receiptId: undefined,
258
+ });
259
+ }
260
+ }
261
+ return tasks.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
262
+ }
263
+ export async function verifyLevelEditorTask(state, operation, value, root, options = {}) {
264
+ if (operation.kind !== 'level-editor.verify')
265
+ throw new LevelEditorError('level_editor_task_identity');
266
+ const { taskId } = levelEditorVerifyPayloadSchema.parse(value), scope = owner(state, operation, false);
267
+ const task = await readPrivateLevelEditorTask(state, scope.workspaceId, scope.directoryName, taskId, root);
268
+ if (!task ||
269
+ !current(state, task) ||
270
+ ['running', 'uncertain', 'interrupted'].includes(task.record.status))
271
+ throw new LevelEditorError('level_editor_task_unavailable');
272
+ const instance = state.instanceToken;
273
+ const active = () => {
274
+ options.signal?.throwIfAborted();
275
+ options.assertActive?.();
276
+ if (!current(state, task) || state.instanceToken !== instance || state.journal.destroyed())
277
+ throw new LevelEditorError('level_editor_task_stale');
278
+ };
279
+ try {
280
+ active();
281
+ const before = await inspectReport(root, task, { state, operation });
282
+ if (before.report.missing.length)
283
+ throw new LevelEditorError('level_editor_integration_incomplete');
284
+ const snapshot = await readWorkspaceLevelEditor(root, state.journal.fileRevisions(revisionScope(state, operation.workspaceId, scope.directoryName)), { state, operation });
285
+ if (snapshot.status !== 'ready' || !snapshot.definition?.preview)
286
+ throw new LevelEditorError('level_editor_preview_not_ready');
287
+ if (task.request.editorId && snapshot.definition.editorId !== task.request.editorId)
288
+ throw new LevelEditorError('level_editor_target_mismatch');
289
+ const baseline = task.authoringBaseline;
290
+ if (baseline) {
291
+ if (snapshot.definition.dataContractVersion === baseline.dataContractVersion) {
292
+ if (baseline.documents.some((document) => snapshot.definition.documents[document.documentId]?.path !== document.path ||
293
+ snapshot.documentHashes[document.documentId] !== document.sha256))
294
+ throw new LevelEditorError('level_editor_existing_data_changed');
295
+ }
296
+ else {
297
+ const migration = before.report.migration;
298
+ if (snapshot.definition.dataContractVersion <= baseline.dataContractVersion ||
299
+ before.report.preserved !== true ||
300
+ migration?.fromDataContractVersion !== baseline.dataContractVersion ||
301
+ migration.toDataContractVersion !== snapshot.definition.dataContractVersion)
302
+ throw new LevelEditorError('level_editor_migration_plan_missing');
303
+ }
304
+ }
305
+ if ((await validateLevelEditorExtensions(root, snapshot.definition, snapshot.documents)).some((item) => item.severity === 'error'))
306
+ throw new LevelEditorError('level_editor_validation_failed');
307
+ for (const commandId of before.report.operations)
308
+ if (!snapshot.definition.commands[commandId])
309
+ throw new LevelEditorError('level_editor_operation_evidence_invalid');
310
+ const spatial = (id) => {
311
+ const command = snapshot.definition.commands[id];
312
+ return (!!command &&
313
+ (['grid.paint', 'object.transform', 'path.edit'].includes(command.operation) ||
314
+ (command.operation === 'command.batch' &&
315
+ command.steps.some((step) => spatial(step.commandId)))));
316
+ };
317
+ const dedicated = Object.values(snapshot.definition.extensions?.views ?? {}).some((view) => view.commands.some((id) => before.report.operations.includes(id)));
318
+ if (!before.report.operations.some(spatial) && !dedicated)
319
+ throw new LevelEditorError('level_editor_space_consumer_missing');
320
+ const required = [
321
+ '.playcraft/workspace.json',
322
+ snapshot.definitionPath,
323
+ 'package.json',
324
+ ...Object.values(snapshot.definition.documents).flatMap((item) => [item.path, item.schema]),
325
+ ...Object.values(snapshot.definition.preview),
326
+ ...levelEditorExtensionPaths(snapshot.definition),
327
+ ];
328
+ if (required.some((path) => !before.sources.has(path)))
329
+ throw new LevelEditorError('level_editor_evidence_missing');
330
+ const pkg = JSON.parse((await readLevelEditorFile(root, 'package.json')).toString('utf8'));
331
+ if (['test:level-editor', 'build'].some((name) => typeof pkg.scripts?.[name] !== 'string'))
332
+ throw new LevelEditorError('level_editor_check_script_missing');
333
+ await update(state, task, {
334
+ status: 'checking',
335
+ checks: [],
336
+ report: before.report,
337
+ receiptId: undefined,
338
+ productionCheck: undefined,
339
+ }, root);
340
+ const scripts = [
341
+ 'test:level-editor',
342
+ 'build',
343
+ ...(typeof pkg.scripts?.typecheck === 'string' ? ['typecheck'] : []),
344
+ ];
345
+ let production;
346
+ for (const script of scripts) {
347
+ active();
348
+ const plan = script === 'build' ? await prepareParameterProductionCheck(root) : undefined;
349
+ const chunks = [];
350
+ let size = 0;
351
+ const startedAt = new Date().toISOString();
352
+ const result = await (options.runCommand ?? runDeveloperCommand)(state, {
353
+ cwd: root,
354
+ command: `pnpm run ${script}`,
355
+ processId: `level-editor-${taskId}-${script.replace(':', '-')}`,
356
+ timeoutMs: script === 'build' ? 300_000 : 120_000,
357
+ signal: options.signal,
358
+ onOutput(chunk) {
359
+ const data = Buffer.from(chunk);
360
+ size += data.length;
361
+ if (size <= 1024 * 1024)
362
+ chunks.push(data);
363
+ },
364
+ });
365
+ active();
366
+ task.record.checks.push({
367
+ script: script,
368
+ code: result.code ?? null,
369
+ timedOut: result.timedOut,
370
+ ...(result.canceled ? { canceled: true } : {}),
371
+ logDigest: levelEditorDigest(Buffer.concat(chunks)),
372
+ startedAt,
373
+ finishedAt: new Date().toISOString(),
374
+ });
375
+ await writePrivateLevelEditorTask(state, task, root);
376
+ if (result.code !== 0 || result.timedOut || result.canceled)
377
+ throw new LevelEditorError('level_editor_check_failed');
378
+ if (plan)
379
+ production = await inspectParameterProductionCheck(plan, active);
380
+ }
381
+ active();
382
+ const after = await inspectReport(root, task, { state, operation });
383
+ if (before.digest !== after.digest || before.reportHash !== after.reportHash)
384
+ throw new LevelEditorError('level_editor_sources_changed');
385
+ task.reportHash = after.reportHash;
386
+ task.instanceToken = state.instanceToken;
387
+ return update(state, task, {
388
+ status: 'verified',
389
+ sourceDigest: after.digest,
390
+ receiptId: randomUUID(),
391
+ error: undefined,
392
+ ...(production
393
+ ? { productionCheck: { ...production, checkedAt: new Date().toISOString() } }
394
+ : {}),
395
+ }, root);
396
+ }
397
+ catch (error) {
398
+ const code = codeOf(error);
399
+ return update(state, task, {
400
+ status: /stale|changed/.test(code)
401
+ ? 'stale'
402
+ : /incomplete|missing|not_ready/.test(code)
403
+ ? 'needs-verification'
404
+ : 'failed',
405
+ error: code,
406
+ receiptId: undefined,
407
+ productionCheck: undefined,
408
+ }, root);
409
+ }
410
+ }