@playdrop/playdrop-cli 0.12.26 → 0.12.28
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/config/client-meta.json +1 -1
- package/dist/apps/build.js +43 -0
- package/dist/apps/index.d.ts +2 -1
- package/dist/apps/index.js +1 -0
- package/dist/apps/upload.d.ts +2 -1
- package/dist/apps/upload.js +1 -0
- package/dist/commands/devRuntimeAssets.js +26 -124
- package/dist/commands/ownedRuntimeImageValidation.d.ts +7 -0
- package/dist/commands/ownedRuntimeImageValidation.js +76 -0
- package/dist/commands/upload.d.ts +7 -1
- package/dist/commands/upload.js +79 -402
- package/dist/commands/worker.d.ts +16 -0
- package/dist/commands/worker.js +164 -35
- package/dist/listingPreflight.d.ts +1 -0
- package/dist/listingPreflight.js +17 -0
- package/node_modules/@playdrop/api-client/dist/client.d.ts +2 -1
- package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/core/errors.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/core/errors.js +3 -1
- package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +2 -1
- package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/domains/apps.js +11 -0
- package/node_modules/@playdrop/api-client/dist/index.d.ts +2 -1
- package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/api-client/dist/index.js +5 -0
- package/node_modules/@playdrop/config/client-meta.json +1 -1
- package/node_modules/@playdrop/types/dist/agent-task-playtest.d.ts +23 -0
- package/node_modules/@playdrop/types/dist/agent-task-playtest.d.ts.map +1 -0
- package/node_modules/@playdrop/types/dist/agent-task-playtest.js +84 -0
- package/node_modules/@playdrop/types/dist/api.d.ts +13 -0
- package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/index.d.ts +1 -0
- package/node_modules/@playdrop/types/dist/index.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/index.js +3 -1
- package/node_modules/@playdrop/types/dist/version.d.ts +4 -2
- package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
- package/node_modules/@playdrop/types/dist/version.js +13 -3
- package/package.json +1 -1
package/dist/commands/upload.js
CHANGED
|
@@ -1,40 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveUploadOwner = resolveUploadOwner;
|
|
4
|
+
exports.resolveDefaultUploadCreator = resolveDefaultUploadCreator;
|
|
36
5
|
exports.upload = upload;
|
|
6
|
+
exports.assertTaskPlaytestEvidenceManifest = assertTaskPlaytestEvidenceManifest;
|
|
37
7
|
exports.publishWorkerAppProject = publishWorkerAppProject;
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
38
9
|
const node_fs_1 = require("node:fs");
|
|
39
10
|
const node_path_1 = require("node:path");
|
|
40
11
|
const semver_1 = require("semver");
|
|
@@ -51,6 +22,7 @@ const uploadLog_1 = require("../uploadLog");
|
|
|
51
22
|
const upload_content_1 = require("./upload-content");
|
|
52
23
|
const appUrls_1 = require("../appUrls");
|
|
53
24
|
const devRuntimeAssets_1 = require("./devRuntimeAssets");
|
|
25
|
+
const ownedRuntimeImageValidation_1 = require("./ownedRuntimeImageValidation");
|
|
54
26
|
const listingPreflight_1 = require("../listingPreflight");
|
|
55
27
|
const upload_graph_1 = require("./upload-graph");
|
|
56
28
|
function normalizeSavedItemKinds(kinds) {
|
|
@@ -220,6 +192,19 @@ function normalizeCreatorUsername(username) {
|
|
|
220
192
|
}
|
|
221
193
|
return trimmed;
|
|
222
194
|
}
|
|
195
|
+
function resolveUploadOwner(actorUsername, workspaceOwnerUsername) {
|
|
196
|
+
const workspaceOwner = workspaceOwnerUsername?.trim() ?? '';
|
|
197
|
+
if (workspaceOwner) {
|
|
198
|
+
return workspaceOwner;
|
|
199
|
+
}
|
|
200
|
+
const actor = actorUsername?.trim() ?? '';
|
|
201
|
+
return actor || 'unknown';
|
|
202
|
+
}
|
|
203
|
+
function resolveDefaultUploadCreator(owner, actorUsername) {
|
|
204
|
+
return normalizeCreatorUsername(owner ?? null)
|
|
205
|
+
?? normalizeCreatorUsername(actorUsername ?? null)
|
|
206
|
+
?? 'unknown';
|
|
207
|
+
}
|
|
223
208
|
function isNotFoundApiError(error) {
|
|
224
209
|
return error instanceof types_1.ApiError && error.status === 404;
|
|
225
210
|
}
|
|
@@ -1057,7 +1042,7 @@ async function flushGraphState(client, graphState, results) {
|
|
|
1057
1042
|
}
|
|
1058
1043
|
async function processUploadTasks(client, tasks, owner, ownerUsername, currentUserRole, localTagGroups, currentUser, token, warnings, apiBase, webBase, options) {
|
|
1059
1044
|
const portalBase = normalizePortalBase(webBase);
|
|
1060
|
-
const defaultCreator =
|
|
1045
|
+
const defaultCreator = resolveDefaultUploadCreator(owner, ownerUsername);
|
|
1061
1046
|
const results = [];
|
|
1062
1047
|
const preflight = await buildUploadPreflight(client, tasks, defaultCreator, currentUserRole, localTagGroups);
|
|
1063
1048
|
const sortedTasks = preflight.nodes.map((node) => node.task);
|
|
@@ -1175,9 +1160,7 @@ async function upload(pathOrName, options) {
|
|
|
1175
1160
|
}
|
|
1176
1161
|
throw error;
|
|
1177
1162
|
}
|
|
1178
|
-
const owner =
|
|
1179
|
-
? userInfo.username.trim()
|
|
1180
|
-
: 'unknown';
|
|
1163
|
+
const owner = resolveUploadOwner(userInfo.username, ctx.workspaceAuth?.config.ownerUsername);
|
|
1181
1164
|
const tagGroupLoad = (0, catalogue_1.resolveCatalogueTagGroups)(workspaceRoot);
|
|
1182
1165
|
if (tagGroupLoad.errors.length > 0) {
|
|
1183
1166
|
(0, taskSelection_1.reportTaskErrors)(tagGroupLoad.errors.map((message) => ({
|
|
@@ -1232,8 +1215,6 @@ const WORKER_SUPERVISOR_BUNDLE_EXCLUDES = [
|
|
|
1232
1215
|
'assets/marketing/',
|
|
1233
1216
|
'listing/',
|
|
1234
1217
|
];
|
|
1235
|
-
const OWNED_RUNTIME_IMAGE_CHECK_SIZE = 96;
|
|
1236
|
-
const OWNED_RUNTIME_IMAGE_GRID_CHECK_SIZE = 512;
|
|
1237
1218
|
function appTaskSatisfiesPlaydropAssetRequirement(task, requirement) {
|
|
1238
1219
|
if (!requirement) {
|
|
1239
1220
|
return true;
|
|
@@ -1251,361 +1232,6 @@ function appTaskSatisfiesPlaydropAssetRequirement(task, requirement) {
|
|
|
1251
1232
|
});
|
|
1252
1233
|
return hasFirstPartyPack || hasFirstPartyAsset || task.ownedAssets.length > 0;
|
|
1253
1234
|
}
|
|
1254
|
-
function isUnsafeDirectPlaydropAiImageAsset(detail, parsedRevision) {
|
|
1255
|
-
const asset = detail?.asset;
|
|
1256
|
-
if (!asset || asset.category !== 'IMAGE') {
|
|
1257
|
-
return false;
|
|
1258
|
-
}
|
|
1259
|
-
const currentVersion = asset.currentVersion;
|
|
1260
|
-
if (!currentVersion || Number(currentVersion.revision) !== parsedRevision) {
|
|
1261
|
-
return false;
|
|
1262
|
-
}
|
|
1263
|
-
const sourceKind = typeof currentVersion.sourceKind === 'string' ? currentVersion.sourceKind.toUpperCase() : '';
|
|
1264
|
-
if (sourceKind !== 'AI') {
|
|
1265
|
-
return false;
|
|
1266
|
-
}
|
|
1267
|
-
const format = typeof currentVersion.format === 'string' ? currentVersion.format.toUpperCase() : '';
|
|
1268
|
-
const files = Array.isArray(currentVersion.fileManifest?.files) ? currentVersion.fileManifest.files : [];
|
|
1269
|
-
const primary = files.find((file) => file?.role === 'primary');
|
|
1270
|
-
const primaryContentType = typeof primary?.contentType === 'string' ? primary.contentType.toLowerCase() : '';
|
|
1271
|
-
return format === 'JPEG' || primaryContentType === 'image/jpeg' || primaryContentType === 'image/jpg';
|
|
1272
|
-
}
|
|
1273
|
-
async function assertNewGameDirectPlaydropAssetsAreRuntimeSafe(input) {
|
|
1274
|
-
const unsafeRefs = [];
|
|
1275
|
-
for (const dependency of input.task.uses.assets) {
|
|
1276
|
-
const parsed = (0, types_1.parseContentVersionRef)(dependency.ref);
|
|
1277
|
-
if (!parsed || parsed.kind !== 'asset' || parsed.creatorUsername.toLowerCase() !== 'playdrop') {
|
|
1278
|
-
continue;
|
|
1279
|
-
}
|
|
1280
|
-
const detail = await input.client.fetchAssetBySlug(parsed.creatorUsername, parsed.name);
|
|
1281
|
-
if (isUnsafeDirectPlaydropAiImageAsset(detail, parsed.revision)) {
|
|
1282
|
-
unsafeRefs.push((0, types_1.formatContentVersionRef)(parsed));
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
if (unsafeRefs.length > 0) {
|
|
1286
|
-
throw new Error(`agent_task_playdrop_direct_ai_image_asset_unsafe: direct PlayDrop AI JPEG image assets cannot be used as new-game runtime sprites because their backgrounds are baked into the image. Use a PlayDrop asset pack or create clean owned PNG assets through the PlayDrop make-assets workflow instead. Unsafe refs: ${unsafeRefs.join(', ')}`);
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
function isOwnedRuntimeImageAsset(task, ownedAsset) {
|
|
1290
|
-
if (!ownedAsset.runtimeKey || ownedAsset.category !== 'IMAGE') {
|
|
1291
|
-
return false;
|
|
1292
|
-
}
|
|
1293
|
-
const primaryPath = ownedAsset.filePaths?.primary;
|
|
1294
|
-
if (!primaryPath) {
|
|
1295
|
-
return false;
|
|
1296
|
-
}
|
|
1297
|
-
const relativePath = primaryPath.slice((0, node_path_1.resolve)(task.projectDir).length + 1).replace(/\\/g, '/');
|
|
1298
|
-
return !relativePath.startsWith('assets/marketing/') && !relativePath.startsWith('listing/');
|
|
1299
|
-
}
|
|
1300
|
-
function isBackgroundLikeOwnedRuntimeImageAsset(ownedAsset) {
|
|
1301
|
-
const value = `${ownedAsset.name ?? ''} ${ownedAsset.runtimeKey ?? ''}`.toLowerCase();
|
|
1302
|
-
return /\b(background|backdrop|scene|sky|floor|ground|wall|terrain|landscape|level|arena)\b/i.test(value);
|
|
1303
|
-
}
|
|
1304
|
-
function looksLikeBakedCheckerboardPreview(input) {
|
|
1305
|
-
const { data, width, height, channels } = input;
|
|
1306
|
-
if (width < 24 || height < 24 || channels < 4) {
|
|
1307
|
-
return false;
|
|
1308
|
-
}
|
|
1309
|
-
const border = Math.max(8, Math.floor(Math.min(width, height) * 0.16));
|
|
1310
|
-
let sampled = 0;
|
|
1311
|
-
let transparent = 0;
|
|
1312
|
-
let opaqueGray = 0;
|
|
1313
|
-
let lowGray = 0;
|
|
1314
|
-
let highGray = 0;
|
|
1315
|
-
for (let y = 0; y < height; y += 1) {
|
|
1316
|
-
for (let x = 0; x < width; x += 1) {
|
|
1317
|
-
const inBorder = x < border || y < border || x >= width - border || y >= height - border;
|
|
1318
|
-
if (!inBorder) {
|
|
1319
|
-
continue;
|
|
1320
|
-
}
|
|
1321
|
-
sampled += 1;
|
|
1322
|
-
const offset = (y * width + x) * channels;
|
|
1323
|
-
const red = data[offset] ?? 0;
|
|
1324
|
-
const green = data[offset + 1] ?? 0;
|
|
1325
|
-
const blue = data[offset + 2] ?? 0;
|
|
1326
|
-
const alpha = data[offset + 3] ?? 255;
|
|
1327
|
-
if (alpha < 32) {
|
|
1328
|
-
transparent += 1;
|
|
1329
|
-
continue;
|
|
1330
|
-
}
|
|
1331
|
-
const brightness = (red + green + blue) / 3;
|
|
1332
|
-
const grayish = Math.max(Math.abs(red - green), Math.abs(red - blue), Math.abs(green - blue)) <= 10;
|
|
1333
|
-
if (alpha > 240 && grayish && brightness >= 70 && brightness <= 210) {
|
|
1334
|
-
opaqueGray += 1;
|
|
1335
|
-
if (brightness < 125) {
|
|
1336
|
-
lowGray += 1;
|
|
1337
|
-
}
|
|
1338
|
-
else if (brightness > 145) {
|
|
1339
|
-
highGray += 1;
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
if (sampled === 0 || transparent / sampled > 0.18) {
|
|
1345
|
-
return false;
|
|
1346
|
-
}
|
|
1347
|
-
return opaqueGray / sampled > 0.58
|
|
1348
|
-
&& lowGray / sampled > 0.08
|
|
1349
|
-
&& highGray / sampled > 0.18;
|
|
1350
|
-
}
|
|
1351
|
-
function looksLikeBakedSolidMattePreview(input) {
|
|
1352
|
-
const { data, width, height, channels } = input;
|
|
1353
|
-
if (width < 24 || height < 24 || channels < 4) {
|
|
1354
|
-
return false;
|
|
1355
|
-
}
|
|
1356
|
-
const border = Math.max(8, Math.floor(Math.min(width, height) * 0.18));
|
|
1357
|
-
let sampled = 0;
|
|
1358
|
-
let transparent = 0;
|
|
1359
|
-
let matteLike = 0;
|
|
1360
|
-
for (let y = 0; y < height; y += 1) {
|
|
1361
|
-
for (let x = 0; x < width; x += 1) {
|
|
1362
|
-
const inBorder = x < border || y < border || x >= width - border || y >= height - border;
|
|
1363
|
-
if (!inBorder) {
|
|
1364
|
-
continue;
|
|
1365
|
-
}
|
|
1366
|
-
sampled += 1;
|
|
1367
|
-
const offset = (y * width + x) * channels;
|
|
1368
|
-
const red = data[offset] ?? 0;
|
|
1369
|
-
const green = data[offset + 1] ?? 0;
|
|
1370
|
-
const blue = data[offset + 2] ?? 0;
|
|
1371
|
-
const alpha = data[offset + 3] ?? 255;
|
|
1372
|
-
if (alpha < 32) {
|
|
1373
|
-
transparent += 1;
|
|
1374
|
-
continue;
|
|
1375
|
-
}
|
|
1376
|
-
const dominant = Math.max(red, green, blue);
|
|
1377
|
-
const weakest = Math.min(red, green, blue);
|
|
1378
|
-
const chroma = dominant - weakest;
|
|
1379
|
-
const brightGreenMatte = green >= 220 && green - Math.max(red, blue) >= 65 && Math.max(red, blue) >= 70;
|
|
1380
|
-
const brightRedMatte = red >= 220 && red - Math.max(green, blue) >= 65;
|
|
1381
|
-
const brightBlueMatte = blue >= 220 && blue - Math.max(red, green) >= 65;
|
|
1382
|
-
const brightTwoChannelMatte = dominant >= 220
|
|
1383
|
-
&& chroma >= 65
|
|
1384
|
-
&& ((red >= 220 && green >= 220) || (red >= 220 && blue >= 220) || (green >= 220 && blue >= 220));
|
|
1385
|
-
if (brightGreenMatte || brightRedMatte || brightBlueMatte || brightTwoChannelMatte) {
|
|
1386
|
-
matteLike += 1;
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
if (sampled === 0 || transparent / sampled > 0.18) {
|
|
1391
|
-
return false;
|
|
1392
|
-
}
|
|
1393
|
-
return matteLike / sampled > 0.58;
|
|
1394
|
-
}
|
|
1395
|
-
function looksLikeBakedNeutralSolidMattePreview(input) {
|
|
1396
|
-
const { data, width, height, channels } = input;
|
|
1397
|
-
if (width < 24 || height < 24 || channels < 4) {
|
|
1398
|
-
return false;
|
|
1399
|
-
}
|
|
1400
|
-
const border = Math.max(8, Math.floor(Math.min(width, height) * 0.18));
|
|
1401
|
-
let sampled = 0;
|
|
1402
|
-
let transparent = 0;
|
|
1403
|
-
let neutralMatteLike = 0;
|
|
1404
|
-
for (let y = 0; y < height; y += 1) {
|
|
1405
|
-
for (let x = 0; x < width; x += 1) {
|
|
1406
|
-
const inBorder = x < border || y < border || x >= width - border || y >= height - border;
|
|
1407
|
-
if (!inBorder) {
|
|
1408
|
-
continue;
|
|
1409
|
-
}
|
|
1410
|
-
sampled += 1;
|
|
1411
|
-
const offset = (y * width + x) * channels;
|
|
1412
|
-
const red = data[offset] ?? 0;
|
|
1413
|
-
const green = data[offset + 1] ?? 0;
|
|
1414
|
-
const blue = data[offset + 2] ?? 0;
|
|
1415
|
-
const alpha = data[offset + 3] ?? 255;
|
|
1416
|
-
if (alpha < 32) {
|
|
1417
|
-
transparent += 1;
|
|
1418
|
-
continue;
|
|
1419
|
-
}
|
|
1420
|
-
const brightness = (red + green + blue) / 3;
|
|
1421
|
-
const grayish = Math.max(Math.abs(red - green), Math.abs(red - blue), Math.abs(green - blue)) <= 12;
|
|
1422
|
-
if (alpha > 240 && grayish && (brightness >= 235 || brightness <= 20)) {
|
|
1423
|
-
neutralMatteLike += 1;
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
if (sampled === 0 || transparent / sampled > 0.18) {
|
|
1428
|
-
return false;
|
|
1429
|
-
}
|
|
1430
|
-
return neutralMatteLike / sampled > 0.58;
|
|
1431
|
-
}
|
|
1432
|
-
function looksLikeResidualMatteGridArtifact(input) {
|
|
1433
|
-
const { data, width, height, channels } = input;
|
|
1434
|
-
if (width < 48 || height < 48 || channels < 4) {
|
|
1435
|
-
return false;
|
|
1436
|
-
}
|
|
1437
|
-
const rowCounts = new Uint32Array(height);
|
|
1438
|
-
const columnCounts = new Uint32Array(width);
|
|
1439
|
-
let matteLike = 0;
|
|
1440
|
-
for (let y = 0; y < height; y += 1) {
|
|
1441
|
-
for (let x = 0; x < width; x += 1) {
|
|
1442
|
-
const offset = (y * width + x) * channels;
|
|
1443
|
-
const red = data[offset] ?? 0;
|
|
1444
|
-
const green = data[offset + 1] ?? 0;
|
|
1445
|
-
const blue = data[offset + 2] ?? 0;
|
|
1446
|
-
const alpha = data[offset + 3] ?? 255;
|
|
1447
|
-
if (alpha < 24 || alpha > 190) {
|
|
1448
|
-
continue;
|
|
1449
|
-
}
|
|
1450
|
-
const greenMatte = green >= 35 && green - Math.max(red, blue) >= 25 && red <= 110 && blue <= 110;
|
|
1451
|
-
const redMatte = red >= 35 && red - Math.max(green, blue) >= 25 && green <= 120 && blue <= 120;
|
|
1452
|
-
const blueMatte = blue >= 35 && blue - Math.max(red, green) >= 25 && red <= 120 && green <= 120;
|
|
1453
|
-
const purpleMatte = red >= 35 && blue >= 35 && Math.min(red, blue) - green >= 25 && green <= 120;
|
|
1454
|
-
if (!greenMatte && !redMatte && !blueMatte && !purpleMatte) {
|
|
1455
|
-
continue;
|
|
1456
|
-
}
|
|
1457
|
-
matteLike += 1;
|
|
1458
|
-
rowCounts[y] += 1;
|
|
1459
|
-
columnCounts[x] += 1;
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
const matteRatio = matteLike / (width * height);
|
|
1463
|
-
if (matteRatio < 0.0015) {
|
|
1464
|
-
return false;
|
|
1465
|
-
}
|
|
1466
|
-
let rowsWithLines = 0;
|
|
1467
|
-
for (const count of rowCounts) {
|
|
1468
|
-
if (count / width >= 0.12) {
|
|
1469
|
-
rowsWithLines += 1;
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
let columnsWithLines = 0;
|
|
1473
|
-
for (const count of columnCounts) {
|
|
1474
|
-
if (count / height >= 0.12) {
|
|
1475
|
-
columnsWithLines += 1;
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
return rowsWithLines + columnsWithLines >= 3
|
|
1479
|
-
&& (rowsWithLines >= 2 || columnsWithLines >= 2);
|
|
1480
|
-
}
|
|
1481
|
-
function looksLikeResidualTransparentGridLineArtifact(input) {
|
|
1482
|
-
const { data, width, height, channels } = input;
|
|
1483
|
-
if (width < 48 || height < 48 || channels < 4) {
|
|
1484
|
-
return false;
|
|
1485
|
-
}
|
|
1486
|
-
const rowVisibleCounts = new Uint32Array(height);
|
|
1487
|
-
const rowSemiCounts = new Uint32Array(height);
|
|
1488
|
-
const columnVisibleCounts = new Uint32Array(width);
|
|
1489
|
-
const columnSemiCounts = new Uint32Array(width);
|
|
1490
|
-
for (let y = 0; y < height; y += 1) {
|
|
1491
|
-
for (let x = 0; x < width; x += 1) {
|
|
1492
|
-
const offset = (y * width + x) * channels;
|
|
1493
|
-
const alpha = data[offset + 3] ?? 255;
|
|
1494
|
-
if (alpha <= 0) {
|
|
1495
|
-
continue;
|
|
1496
|
-
}
|
|
1497
|
-
rowVisibleCounts[y] += 1;
|
|
1498
|
-
columnVisibleCounts[x] += 1;
|
|
1499
|
-
if (alpha < 255) {
|
|
1500
|
-
rowSemiCounts[y] += 1;
|
|
1501
|
-
columnSemiCounts[x] += 1;
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
let rowsWithLines = 0;
|
|
1506
|
-
for (let row = 0; row < height; row += 1) {
|
|
1507
|
-
const visibleRatio = rowVisibleCounts[row] / width;
|
|
1508
|
-
const semiRatio = rowSemiCounts[row] / width;
|
|
1509
|
-
if (visibleRatio >= 0.94 && semiRatio >= 0.72) {
|
|
1510
|
-
rowsWithLines += 1;
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
let columnsWithLines = 0;
|
|
1514
|
-
for (let column = 0; column < width; column += 1) {
|
|
1515
|
-
const visibleRatio = columnVisibleCounts[column] / height;
|
|
1516
|
-
const semiRatio = columnSemiCounts[column] / height;
|
|
1517
|
-
if (visibleRatio >= 0.94 && semiRatio >= 0.72) {
|
|
1518
|
-
columnsWithLines += 1;
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
return rowsWithLines + columnsWithLines >= 3
|
|
1522
|
-
&& (rowsWithLines >= 2 || columnsWithLines >= 2);
|
|
1523
|
-
}
|
|
1524
|
-
async function imageHasBakedPreviewBackground(filePath, options = {}) {
|
|
1525
|
-
let sharp;
|
|
1526
|
-
try {
|
|
1527
|
-
const sharpModule = await Promise.resolve().then(() => __importStar(require('sharp')));
|
|
1528
|
-
sharp = sharpModule.default ?? sharpModule;
|
|
1529
|
-
}
|
|
1530
|
-
catch {
|
|
1531
|
-
return false;
|
|
1532
|
-
}
|
|
1533
|
-
try {
|
|
1534
|
-
const { data, info } = await sharp(filePath, { failOn: 'error' })
|
|
1535
|
-
.rotate()
|
|
1536
|
-
.resize(OWNED_RUNTIME_IMAGE_CHECK_SIZE, OWNED_RUNTIME_IMAGE_CHECK_SIZE, { fit: 'fill' })
|
|
1537
|
-
.ensureAlpha()
|
|
1538
|
-
.raw()
|
|
1539
|
-
.toBuffer({ resolveWithObject: true });
|
|
1540
|
-
const hasPreviewBackground = looksLikeBakedCheckerboardPreview({
|
|
1541
|
-
data,
|
|
1542
|
-
width: info.width,
|
|
1543
|
-
height: info.height,
|
|
1544
|
-
channels: info.channels,
|
|
1545
|
-
}) || looksLikeBakedSolidMattePreview({
|
|
1546
|
-
data,
|
|
1547
|
-
width: info.width,
|
|
1548
|
-
height: info.height,
|
|
1549
|
-
channels: info.channels,
|
|
1550
|
-
}) || (!options.allowNeutralSolidMatte && looksLikeBakedNeutralSolidMattePreview({
|
|
1551
|
-
data,
|
|
1552
|
-
width: info.width,
|
|
1553
|
-
height: info.height,
|
|
1554
|
-
channels: info.channels,
|
|
1555
|
-
}));
|
|
1556
|
-
if (hasPreviewBackground) {
|
|
1557
|
-
return true;
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
catch {
|
|
1561
|
-
return false;
|
|
1562
|
-
}
|
|
1563
|
-
try {
|
|
1564
|
-
const { data, info } = await sharp(filePath, { failOn: 'error' })
|
|
1565
|
-
.rotate()
|
|
1566
|
-
.resize(OWNED_RUNTIME_IMAGE_GRID_CHECK_SIZE, OWNED_RUNTIME_IMAGE_GRID_CHECK_SIZE, { fit: 'inside', withoutEnlargement: true })
|
|
1567
|
-
.ensureAlpha()
|
|
1568
|
-
.raw()
|
|
1569
|
-
.toBuffer({ resolveWithObject: true });
|
|
1570
|
-
if (looksLikeResidualMatteGridArtifact({
|
|
1571
|
-
data,
|
|
1572
|
-
width: info.width,
|
|
1573
|
-
height: info.height,
|
|
1574
|
-
channels: info.channels,
|
|
1575
|
-
}) || looksLikeResidualTransparentGridLineArtifact({
|
|
1576
|
-
data,
|
|
1577
|
-
width: info.width,
|
|
1578
|
-
height: info.height,
|
|
1579
|
-
channels: info.channels,
|
|
1580
|
-
})) {
|
|
1581
|
-
return true;
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
catch {
|
|
1585
|
-
return false;
|
|
1586
|
-
}
|
|
1587
|
-
return false;
|
|
1588
|
-
}
|
|
1589
|
-
async function assertNewGameOwnedRuntimeImageAssetsAreClean(task) {
|
|
1590
|
-
const unsafeAssets = [];
|
|
1591
|
-
for (const ownedAsset of task.ownedAssets) {
|
|
1592
|
-
if (!isOwnedRuntimeImageAsset(task, ownedAsset)) {
|
|
1593
|
-
continue;
|
|
1594
|
-
}
|
|
1595
|
-
const primaryPath = ownedAsset.filePaths?.primary;
|
|
1596
|
-
if (!primaryPath) {
|
|
1597
|
-
continue;
|
|
1598
|
-
}
|
|
1599
|
-
if (await imageHasBakedPreviewBackground(primaryPath, {
|
|
1600
|
-
allowNeutralSolidMatte: isBackgroundLikeOwnedRuntimeImageAsset(ownedAsset),
|
|
1601
|
-
})) {
|
|
1602
|
-
unsafeAssets.push(`${ownedAsset.name}:${ownedAsset.runtimeKey}`);
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
if (unsafeAssets.length > 0) {
|
|
1606
|
-
throw new Error(`agent_task_owned_image_asset_checkerboard_background: owned runtime image assets must not contain baked transparency checkerboard, matte-color, or preview backgrounds. Recreate the asset through the PlayDrop make-assets workflow, use a cleaner PlayDrop asset, or use the image as non-runtime listing art only. Unsafe owned assets: ${unsafeAssets.join(', ')}`);
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
1235
|
const RUNTIME_SOURCE_EXTENSIONS = new Set([
|
|
1610
1236
|
'.html',
|
|
1611
1237
|
'.js',
|
|
@@ -2039,7 +1665,7 @@ function readPlaytestEvidenceManifest(task) {
|
|
|
2039
1665
|
throw new Error(formatPlaytestEvidenceManifestError(`playtest-evidence.json must be valid JSON. ${reason}`));
|
|
2040
1666
|
}
|
|
2041
1667
|
}
|
|
2042
|
-
const PLAYTEST_EVIDENCE_MANIFEST_HELP = 'Expected shape: {"version":1,"entries":[{"environment":"local","url":"http://localhost:8080/...","surface":"desktop","checkedAt":"2026-07-05T00:00:00.000Z","captures":["
|
|
1668
|
+
const PLAYTEST_EVIDENCE_MANIFEST_HELP = 'Expected shape: {"version":1,"entries":[{"environment":"local","url":"http://localhost:8080/...","surface":"desktop","checkedAt":"2026-07-05T00:00:00.000Z","captures":["evidence/input.png","evidence/win.png","evidence/loss.png"],"actions":["press ArrowRight","complete the final round","allow health to reach zero"],"statesObserved":["player moved right","victory overlay","defeat overlay"],"consoleErrors":[]}],"proof":{"primaryInput":{"action":"press ArrowRight","observed":"player moved right","capture":"evidence/input.png"},"win":{"action":"complete the final round","observed":"victory overlay appeared","capture":"evidence/win.png"},"loss":{"action":"allow health to reach zero","observed":"defeat overlay appeared","capture":"evidence/loss.png"}}}. NEW_GAME proof requires three distinct captures from a current non-final entry. The uploader appends hashes and the final published-route entry automatically.';
|
|
2043
1669
|
function formatMissingPlaytestEvidenceManifestError(message) {
|
|
2044
1670
|
return `agent_task_playtest_manifest_missing: ${message} ${PLAYTEST_EVIDENCE_MANIFEST_HELP}`;
|
|
2045
1671
|
}
|
|
@@ -2071,8 +1697,51 @@ function assertPlaytestCapturePaths(task, captures) {
|
|
|
2071
1697
|
}
|
|
2072
1698
|
}
|
|
2073
1699
|
}
|
|
2074
|
-
function
|
|
1700
|
+
function assertExactObjectKeys(record, allowedKeys, field) {
|
|
1701
|
+
const allowed = new Set(allowedKeys);
|
|
1702
|
+
const unexpected = Object.keys(record).find((key) => !allowed.has(key));
|
|
1703
|
+
if (unexpected) {
|
|
1704
|
+
throw new Error(formatPlaytestEvidenceManifestError(`${field} contains unsupported key "${unexpected}".`));
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
function buildRequiredTaskPlaytestProof(task, manifest, currentSelfPlaytestCaptures) {
|
|
1708
|
+
if (!manifest.proof || typeof manifest.proof !== 'object' || Array.isArray(manifest.proof)) {
|
|
1709
|
+
throw new Error(formatPlaytestEvidenceManifestError('playtest-evidence.json proof must demonstrate primaryInput, win, and loss for NEW_GAME tasks.'));
|
|
1710
|
+
}
|
|
1711
|
+
const rawProof = manifest.proof;
|
|
1712
|
+
assertExactObjectKeys(rawProof, ['primaryInput', 'win', 'loss'], 'proof');
|
|
1713
|
+
const proof = { version: 1 };
|
|
1714
|
+
for (const momentName of ['primaryInput', 'win', 'loss']) {
|
|
1715
|
+
const rawMoment = rawProof[momentName];
|
|
1716
|
+
if (!rawMoment || typeof rawMoment !== 'object' || Array.isArray(rawMoment)) {
|
|
1717
|
+
throw new Error(formatPlaytestEvidenceManifestError(`proof.${momentName} must include action, observed, and capture.`));
|
|
1718
|
+
}
|
|
1719
|
+
const moment = rawMoment;
|
|
1720
|
+
assertExactObjectKeys(moment, ['action', 'observed', 'capture'], `proof.${momentName}`);
|
|
1721
|
+
const action = typeof moment.action === 'string' ? moment.action.trim() : '';
|
|
1722
|
+
const observed = typeof moment.observed === 'string' ? moment.observed.trim() : '';
|
|
1723
|
+
const capture = typeof moment.capture === 'string' ? moment.capture.trim() : '';
|
|
1724
|
+
if (!action || !observed || !capture) {
|
|
1725
|
+
throw new Error(formatPlaytestEvidenceManifestError(`proof.${momentName} must include non-empty action, observed, and capture strings.`));
|
|
1726
|
+
}
|
|
1727
|
+
if (!currentSelfPlaytestCaptures.has(capture)) {
|
|
1728
|
+
throw new Error(formatPlaytestEvidenceManifestError(`proof.${momentName}.capture must be listed by a current non-final playtest entry.`));
|
|
1729
|
+
}
|
|
1730
|
+
const capturePath = (0, node_path_1.resolve)(task.projectDir, capture);
|
|
1731
|
+
const captureSha256 = (0, node_crypto_1.createHash)('sha256').update((0, node_fs_1.readFileSync)(capturePath)).digest('hex');
|
|
1732
|
+
proof[momentName] = { action, observed, capture, captureSha256 };
|
|
1733
|
+
}
|
|
1734
|
+
const parsed = (0, types_1.normalizeAgentTaskPlaytestProof)(proof);
|
|
1735
|
+
if (!parsed.ok) {
|
|
1736
|
+
throw new Error(formatPlaytestEvidenceManifestError(parsed.error));
|
|
1737
|
+
}
|
|
1738
|
+
return parsed.value;
|
|
1739
|
+
}
|
|
1740
|
+
function assertTaskPlaytestEvidenceManifest(task, options = {}) {
|
|
2075
1741
|
const manifest = readPlaytestEvidenceManifest(task);
|
|
1742
|
+
if (manifest.version !== 1) {
|
|
1743
|
+
throw new Error(formatPlaytestEvidenceManifestError('playtest-evidence.json version must be 1.'));
|
|
1744
|
+
}
|
|
2076
1745
|
const entries = Array.isArray(manifest.entries) ? manifest.entries : [];
|
|
2077
1746
|
if (entries.length === 0) {
|
|
2078
1747
|
throw new Error(formatPlaytestEvidenceManifestError('playtest-evidence.json entries must include a non-final self-playtest before upload.'));
|
|
@@ -2080,6 +1749,7 @@ function assertTaskPlaytestEvidenceManifest(task) {
|
|
|
2080
1749
|
const sourceLastModifiedAt = readRuntimeSourceLastModifiedAt(task.projectDir);
|
|
2081
1750
|
const latestSourceMs = sourceLastModifiedAt ? Date.parse(sourceLastModifiedAt) : null;
|
|
2082
1751
|
let hasCurrentSelfPlaytest = false;
|
|
1752
|
+
const currentSelfPlaytestCaptures = new Set();
|
|
2083
1753
|
entries.forEach((entry, index) => {
|
|
2084
1754
|
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
2085
1755
|
throw new Error(formatPlaytestEvidenceManifestError(`entries[${index}] must be an object.`));
|
|
@@ -2104,11 +1774,15 @@ function assertTaskPlaytestEvidenceManifest(task) {
|
|
|
2104
1774
|
assertPlaytestCapturePaths(task, captures);
|
|
2105
1775
|
if (!isFinalGateEntry && (latestSourceMs === null || checkedAtMs >= latestSourceMs)) {
|
|
2106
1776
|
hasCurrentSelfPlaytest = true;
|
|
1777
|
+
captures.forEach((capture) => currentSelfPlaytestCaptures.add(capture));
|
|
2107
1778
|
}
|
|
2108
1779
|
});
|
|
2109
1780
|
if (!hasCurrentSelfPlaytest) {
|
|
2110
1781
|
throw new Error(formatPlaytestEvidenceManifestError(`playtest-evidence.json must include a non-final self-playtest checkedAt after the last source change${sourceLastModifiedAt ? ` (${sourceLastModifiedAt})` : ''}.`));
|
|
2111
1782
|
}
|
|
1783
|
+
return options.requireOutcomeProof
|
|
1784
|
+
? buildRequiredTaskPlaytestProof(task, manifest, currentSelfPlaytestCaptures)
|
|
1785
|
+
: null;
|
|
2112
1786
|
}
|
|
2113
1787
|
function readRuntimeSourceLastModifiedAt(rootDir) {
|
|
2114
1788
|
const root = (0, node_path_1.resolve)(rootDir);
|
|
@@ -2185,15 +1859,14 @@ async function publishWorkerAppProject(input) {
|
|
|
2185
1859
|
if (input.kind === 'NEW_GAME' || input.kind === 'REMIX_GAME') {
|
|
2186
1860
|
(0, listingPreflight_1.assertAgentGameHeroListingPreflight)(task);
|
|
2187
1861
|
}
|
|
1862
|
+
if (input.kind === 'NEW_GAME') {
|
|
1863
|
+
(0, listingPreflight_1.assertAgentNewGameArtDirectionPreflight)(task);
|
|
1864
|
+
}
|
|
2188
1865
|
if (!appTaskSatisfiesPlaydropAssetRequirement(task, input.playdropAssetRequirement)) {
|
|
2189
1866
|
throw new Error('agent_task_playdrop_asset_dependency_missing: the creator asked for PlayDrop assets, but catalogue.json does not declare a matching uses.assets, uses.packs, or ownedAssets dependency.');
|
|
2190
1867
|
}
|
|
2191
1868
|
if (input.kind === 'NEW_GAME' || input.kind === 'REMIX_GAME') {
|
|
2192
|
-
await
|
|
2193
|
-
client: input.client,
|
|
2194
|
-
task,
|
|
2195
|
-
});
|
|
2196
|
-
await assertNewGameOwnedRuntimeImageAssetsAreClean(task);
|
|
1869
|
+
await (0, ownedRuntimeImageValidation_1.assertOwnedRuntimeImagesAreValid)(task);
|
|
2197
1870
|
}
|
|
2198
1871
|
assertRequestedPlaydropAssetsAreUsedAtRuntime({
|
|
2199
1872
|
task,
|
|
@@ -2231,9 +1904,12 @@ async function publishWorkerAppProject(input) {
|
|
|
2231
1904
|
taskId: input.taskId,
|
|
2232
1905
|
taskToken,
|
|
2233
1906
|
};
|
|
1907
|
+
let playtestProof = null;
|
|
2234
1908
|
if (input.kind === 'NEW_GAME' || input.kind === 'REMIX_GAME') {
|
|
2235
1909
|
(0, listingPreflight_1.assertAgentGameScreenshotListingPreflight)(task);
|
|
2236
|
-
assertTaskPlaytestEvidenceManifest(task
|
|
1910
|
+
playtestProof = assertTaskPlaytestEvidenceManifest(task, {
|
|
1911
|
+
requireOutcomeProof: input.kind === 'NEW_GAME',
|
|
1912
|
+
});
|
|
2237
1913
|
}
|
|
2238
1914
|
const { upload: uploadResult, warnings } = await (0, apps_1.runAppPipeline)(input.client, task, {
|
|
2239
1915
|
creatorUsername,
|
|
@@ -2245,6 +1921,7 @@ async function publishWorkerAppProject(input) {
|
|
|
2245
1921
|
runStagedUploadLoadCheck: true,
|
|
2246
1922
|
ensureRegisteredAppShell: true,
|
|
2247
1923
|
agentTaskId: input.taskId,
|
|
1924
|
+
playtestProof: playtestProof ?? undefined,
|
|
2248
1925
|
mediaCaptureRequired: input.executionTarget === 'FIRST_PARTY',
|
|
2249
1926
|
captureSession,
|
|
2250
1927
|
});
|
|
@@ -310,6 +310,22 @@ export declare function acquireWorkerSupervisorLock(input: {
|
|
|
310
310
|
file?: string;
|
|
311
311
|
}): WorkerSupervisorLock;
|
|
312
312
|
export declare function releaseWorkerSupervisorLock(lock: WorkerSupervisorLock): void;
|
|
313
|
+
export type TaskUploadLockOwner = {
|
|
314
|
+
schemaVersion: 1;
|
|
315
|
+
command: 'task upload';
|
|
316
|
+
pid: number;
|
|
317
|
+
processStartIdentity: string;
|
|
318
|
+
createdAt: string;
|
|
319
|
+
};
|
|
320
|
+
export type TaskUploadLock = {
|
|
321
|
+
file: string;
|
|
322
|
+
owner: TaskUploadLockOwner;
|
|
323
|
+
};
|
|
324
|
+
export declare function resolveTaskUploadLockFile(workspaceDir: string): string;
|
|
325
|
+
export declare function removeStaleTaskUploadLockIfUnchanged(file: string, observedHolder: TaskUploadLockOwner): boolean;
|
|
326
|
+
export declare function acquireTaskUploadLock(workspaceDir: string): TaskUploadLock;
|
|
327
|
+
export declare function releaseTaskUploadLock(lock: TaskUploadLock): void;
|
|
328
|
+
export declare function withTaskUploadLock<T>(workspaceDir: string, work: () => Promise<T>): Promise<T>;
|
|
313
329
|
export declare function readActivePersonalWorkerRuntimeState(): WorkerRuntimeState | null;
|
|
314
330
|
export declare function parseLaunchAgentProgramArguments(plist: string): string[];
|
|
315
331
|
export declare function assertLaunchAgentPlistCompatible(input: {
|