@forgeax/engine-pack 0.1.31 → 0.1.33
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 +2 -2
- package/dist/build.d.ts +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.mjs +252 -22
- package/dist/build.mjs.map +1 -1
- package/dist/cli-asset.d.ts +63 -0
- package/dist/cli-asset.d.ts.map +1 -1
- package/dist/cli-asset.mjs +142 -4
- package/dist/cli-asset.mjs.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/native-cooker-registry.d.ts.map +1 -1
- package/dist/native-cooker.mjs +13 -1
- package/dist/native-cooker.mjs.map +1 -1
- package/dist/pack-authoring-node.d.ts +6 -0
- package/dist/pack-authoring-node.d.ts.map +1 -1
- package/dist/pack-authoring-node.mjs +230 -16
- package/dist/pack-authoring-node.mjs.map +1 -1
- package/dist/pack-authoring.d.ts +26 -0
- package/dist/pack-authoring.d.ts.map +1 -1
- package/dist/pack-authoring.mjs.map +1 -1
- package/dist/runtime-publication.d.ts +8 -0
- package/dist/runtime-publication.d.ts.map +1 -1
- package/dist/scanner.d.ts +1 -1
- package/dist/scanner.mjs +5 -5
- package/dist/scanner.mjs.map +1 -1
- package/dist/scriptable-pack-node.mjs.map +1 -1
- package/dist/scriptable-pack.mjs.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/cli.unit.test.ts +87 -0
- package/src/__tests__/native-cooker-registry.test.ts +16 -0
- package/src/__tests__/pack-authoring-gateway.unit.test.ts +120 -1
- package/src/__tests__/pack.unit.test.ts +6 -6
- package/src/__tests__/runtime-publication.unit.test.ts +27 -1
- package/src/build.ts +2 -0
- package/src/cli-asset.ts +227 -5
- package/src/native-cooker-registry.ts +14 -1
- package/src/pack-authoring-node.ts +278 -13
- package/src/pack-authoring.ts +26 -0
- package/src/runtime-publication.ts +44 -0
- package/src/scanner.ts +5 -5
|
@@ -3,6 +3,7 @@ import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import type { AssetGuid as EngineAssetGuid, Result } from '@forgeax/engine-types';
|
|
5
5
|
import { err, ok } from '@forgeax/engine-types';
|
|
6
|
+
import type { AssetVerificationReport } from './cli-asset.js';
|
|
6
7
|
import { AssetGuid, isValidAssetGuidString, isValidPackSourceKey, PackageId } from './guid.js';
|
|
7
8
|
import {
|
|
8
9
|
type AnyScriptablePackDefinition,
|
|
@@ -42,6 +43,12 @@ export interface FileSystemPackAuthoringOptions {
|
|
|
42
43
|
readonly materialized?: () =>
|
|
43
44
|
| readonly PackAuthoringMaterializedAsset[]
|
|
44
45
|
| Promise<readonly PackAuthoringMaterializedAsset[]>;
|
|
46
|
+
/**
|
|
47
|
+
* GUIDs published by non-Pack producers in the same Catalog projection.
|
|
48
|
+
* They are accepted as dependency identities but never exposed as Pack
|
|
49
|
+
* authoring assets.
|
|
50
|
+
*/
|
|
51
|
+
readonly additionalKnownGuids?: () => readonly string[] | Promise<readonly string[]>;
|
|
45
52
|
/** Build owner callback; the gateway itself does not own DDC or Catalog state. */
|
|
46
53
|
readonly rebuild?: (
|
|
47
54
|
sourcePath: string,
|
|
@@ -106,6 +113,8 @@ interface GatewaySnapshot {
|
|
|
106
113
|
readonly knownGuids: ReadonlySet<string>;
|
|
107
114
|
}
|
|
108
115
|
|
|
116
|
+
const VERIFY_PROVENANCE_LIMIT = 256;
|
|
117
|
+
|
|
109
118
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
110
119
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
111
120
|
}
|
|
@@ -473,6 +482,58 @@ async function readMaterialized(
|
|
|
473
482
|
return ok(materialized);
|
|
474
483
|
}
|
|
475
484
|
|
|
485
|
+
async function readAdditionalKnownGuids(
|
|
486
|
+
options: FileSystemPackAuthoringOptions,
|
|
487
|
+
operation: PackAuthoringOperation,
|
|
488
|
+
): Promise<Result<readonly string[], PackAuthoringError>> {
|
|
489
|
+
let rawGuids: unknown;
|
|
490
|
+
try {
|
|
491
|
+
rawGuids = (await options.additionalKnownGuids?.()) ?? [];
|
|
492
|
+
} catch (cause) {
|
|
493
|
+
return err(
|
|
494
|
+
makeError(
|
|
495
|
+
'pack-parameter-invalid',
|
|
496
|
+
'the additionalKnownGuids callback to return current Catalog GUIDs',
|
|
497
|
+
'repair the build-owner Catalog projection callback, then retry the Pack operation',
|
|
498
|
+
{
|
|
499
|
+
requestId: operation.requestId,
|
|
500
|
+
cause: cause instanceof Error ? cause.message : String(cause),
|
|
501
|
+
},
|
|
502
|
+
),
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
if (!Array.isArray(rawGuids)) {
|
|
506
|
+
return err(
|
|
507
|
+
makeError(
|
|
508
|
+
'pack-parameter-invalid',
|
|
509
|
+
'additionalKnownGuids to be an array of AssetGuid strings',
|
|
510
|
+
'repair the build-owner Catalog projection before querying Pack authoring',
|
|
511
|
+
{ requestId: operation.requestId, actual: typeof rawGuids },
|
|
512
|
+
),
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
const guids: string[] = [];
|
|
516
|
+
const seen = new Set<string>();
|
|
517
|
+
for (const candidate of rawGuids) {
|
|
518
|
+
if (typeof candidate !== 'string' || !isValidAssetGuidString(candidate)) {
|
|
519
|
+
return err(
|
|
520
|
+
makeError(
|
|
521
|
+
'pack-parameter-invalid',
|
|
522
|
+
'additionalKnownGuids to contain valid AssetGuid strings',
|
|
523
|
+
'repair the build-owner Catalog projection before querying Pack authoring',
|
|
524
|
+
{ requestId: operation.requestId, guid: candidate },
|
|
525
|
+
),
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
const guid = candidate.toLowerCase();
|
|
529
|
+
if (!seen.has(guid)) {
|
|
530
|
+
seen.add(guid);
|
|
531
|
+
guids.push(guid);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return ok(guids);
|
|
535
|
+
}
|
|
536
|
+
|
|
476
537
|
async function createSnapshot(
|
|
477
538
|
options: FileSystemPackAuthoringOptions,
|
|
478
539
|
operation: PackAuthoringOperation,
|
|
@@ -534,6 +595,8 @@ async function createSnapshot(
|
|
|
534
595
|
}
|
|
535
596
|
const materializedResult = await readMaterialized(options, operation);
|
|
536
597
|
if (!materializedResult.ok) return materializedResult;
|
|
598
|
+
const additionalKnownGuidsResult = await readAdditionalKnownGuids(options, operation);
|
|
599
|
+
if (!additionalKnownGuidsResult.ok) return additionalKnownGuidsResult;
|
|
537
600
|
const materialized = materializedResult.value;
|
|
538
601
|
const guidOwners = new Map<string, string>();
|
|
539
602
|
const directPairs = new Set<string>();
|
|
@@ -591,6 +654,7 @@ async function createSnapshot(
|
|
|
591
654
|
...scanned.value.inventory.map((entry) => entry.guid.toLowerCase()),
|
|
592
655
|
...directAssets.map((asset) => asset.guid),
|
|
593
656
|
...materialized.map((asset) => asset.guid),
|
|
657
|
+
...additionalKnownGuidsResult.value,
|
|
594
658
|
]);
|
|
595
659
|
return ok({ inventory: scanned.value, subjects, directAssets, materialized, knownGuids });
|
|
596
660
|
}
|
|
@@ -753,6 +817,19 @@ async function inspectAsset(
|
|
|
753
817
|
const selected = subjectFor(gameRoot, snapshot, operation);
|
|
754
818
|
if (!selected.ok) return selected;
|
|
755
819
|
const subject = selected.value;
|
|
820
|
+
const source = await readConfined({ gameRoot }, operation, subject.relativePath);
|
|
821
|
+
if (!source.ok) return source;
|
|
822
|
+
const inspectedSource = {
|
|
823
|
+
...sourceResult(subject, operation),
|
|
824
|
+
revision: source.value.revision,
|
|
825
|
+
...(subject.format === 'source'
|
|
826
|
+
? {
|
|
827
|
+
assets: snapshot.materialized
|
|
828
|
+
.filter((asset) => asset.packageId === packageKey(subject.packageId))
|
|
829
|
+
.map((asset) => ({ ...asset })),
|
|
830
|
+
}
|
|
831
|
+
: {}),
|
|
832
|
+
};
|
|
756
833
|
if (operation.sourceKey !== undefined) {
|
|
757
834
|
if (!isValidPackSourceKey(operation.sourceKey)) {
|
|
758
835
|
return err(
|
|
@@ -787,7 +864,7 @@ async function inspectAsset(
|
|
|
787
864
|
candidate.sourceKey === operation.sourceKey,
|
|
788
865
|
);
|
|
789
866
|
return ok({
|
|
790
|
-
...
|
|
867
|
+
...inspectedSource,
|
|
791
868
|
sourceKey: asset.sourceKey,
|
|
792
869
|
guid: asset.guid,
|
|
793
870
|
kind: asset.kind,
|
|
@@ -807,7 +884,7 @@ async function inspectAsset(
|
|
|
807
884
|
candidate.sourceKey === operation.sourceKey,
|
|
808
885
|
);
|
|
809
886
|
return ok({
|
|
810
|
-
...
|
|
887
|
+
...inspectedSource,
|
|
811
888
|
sourceKey: operation.sourceKey,
|
|
812
889
|
guid,
|
|
813
890
|
...(materialized === undefined
|
|
@@ -833,7 +910,7 @@ async function inspectAsset(
|
|
|
833
910
|
const resolved = await resolveInstance(snapshot, subject);
|
|
834
911
|
if (!resolved.ok) return resolved;
|
|
835
912
|
return ok({
|
|
836
|
-
...
|
|
913
|
+
...inspectedSource,
|
|
837
914
|
parameters: resolved.value.parameters.map((parameter) => ({
|
|
838
915
|
name: parameter.name,
|
|
839
916
|
type: parameter.type,
|
|
@@ -847,7 +924,7 @@ async function inspectAsset(
|
|
|
847
924
|
parentChain: resolved.value.parentChain,
|
|
848
925
|
});
|
|
849
926
|
}
|
|
850
|
-
return ok(
|
|
927
|
+
return ok(inspectedSource);
|
|
851
928
|
}
|
|
852
929
|
|
|
853
930
|
function listAssets(
|
|
@@ -1172,7 +1249,197 @@ async function resolveAsset(
|
|
|
1172
1249
|
});
|
|
1173
1250
|
}
|
|
1174
1251
|
|
|
1252
|
+
type VerificationAsset = AssetVerificationReport['assets'][number];
|
|
1253
|
+
|
|
1254
|
+
function verificationSourceFormat(
|
|
1255
|
+
gameRoot: string,
|
|
1256
|
+
sourcePath: string | undefined,
|
|
1257
|
+
fallback: VerificationAsset['source']['format'] = 'pack.ts',
|
|
1258
|
+
): VerificationAsset['source']['format'] {
|
|
1259
|
+
if (sourcePath === undefined) return fallback;
|
|
1260
|
+
const absolute = resolve(gameRoot, sourcePath);
|
|
1261
|
+
if (absolute.endsWith('.meta.json')) return 'meta.json';
|
|
1262
|
+
if (absolute.endsWith('.pack.json')) return 'pack.json';
|
|
1263
|
+
if (absolute.endsWith('.pack.ts')) return 'pack.ts';
|
|
1264
|
+
return fallback;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function verificationAuthorFacts(
|
|
1268
|
+
snapshot: GatewaySnapshot,
|
|
1269
|
+
entry: GatewaySnapshot['inventory']['inventory'][number],
|
|
1270
|
+
): { readonly name?: string; readonly dependencies: readonly string[] } {
|
|
1271
|
+
const declaration = snapshot.inventory.declarations.get(entry.sourcePath);
|
|
1272
|
+
if (declaration?.format === 'meta.json') {
|
|
1273
|
+
const asset = declaration.value.subAssets.find(
|
|
1274
|
+
(candidate) => candidate.guid.toLowerCase() === entry.guid.toLowerCase(),
|
|
1275
|
+
);
|
|
1276
|
+
return {
|
|
1277
|
+
...(asset?.name === undefined && asset?.sourceKey === undefined
|
|
1278
|
+
? {}
|
|
1279
|
+
: { name: asset.name ?? asset.sourceKey }),
|
|
1280
|
+
dependencies: [],
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
if (declaration?.format === 'pack.json') {
|
|
1284
|
+
if (declaration.value.schemaVersion === '3.0.0') {
|
|
1285
|
+
const parsed = parsePackSourceJson(declaration.value);
|
|
1286
|
+
if (parsed.ok && parsed.value.format === 'direct' && entry.sourceKey !== undefined) {
|
|
1287
|
+
const asset = parsed.value.assets[entry.sourceKey];
|
|
1288
|
+
return {
|
|
1289
|
+
...(asset?.name === undefined ? {} : { name: asset.name }),
|
|
1290
|
+
dependencies: asset?.refs ?? [],
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
} else {
|
|
1294
|
+
const asset = declaration.value.assets.find(
|
|
1295
|
+
(candidate) => candidate.guid.toLowerCase() === entry.guid.toLowerCase(),
|
|
1296
|
+
);
|
|
1297
|
+
return {
|
|
1298
|
+
...(asset?.name === undefined ? {} : { name: asset.name }),
|
|
1299
|
+
dependencies: asset?.refs ?? [],
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return { dependencies: [] };
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function verificationOutput(
|
|
1307
|
+
materialized: MaterializedAsset | undefined,
|
|
1308
|
+
legacyPublished = false,
|
|
1309
|
+
): VerificationAsset['output'] {
|
|
1310
|
+
if (materialized !== undefined) {
|
|
1311
|
+
return {
|
|
1312
|
+
status: materialized.ready ? 'produced' : 'unknown',
|
|
1313
|
+
availability: materialized.ready ? 'available' : 'unknown',
|
|
1314
|
+
freshness: 'unknown',
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
if (legacyPublished) {
|
|
1318
|
+
return {
|
|
1319
|
+
status: 'produced',
|
|
1320
|
+
availability: 'unknown',
|
|
1321
|
+
freshness: 'unknown',
|
|
1322
|
+
// A legacy Pack row is a validated published declaration. The bytes
|
|
1323
|
+
// remain outside this read-only gateway, so availability is unknown.
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
return { status: 'unproduced', availability: 'unknown', freshness: 'unknown' };
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function createGatewayVerificationReport(
|
|
1330
|
+
gameRoot: string,
|
|
1331
|
+
snapshot: GatewaySnapshot,
|
|
1332
|
+
): AssetVerificationReport & Pick<PackAuthoringOperationResult, 'operation' | 'requestId'> {
|
|
1333
|
+
const materializedByGuid = new Map(
|
|
1334
|
+
snapshot.materialized.map((asset) => [asset.guid.toLowerCase(), asset] as const),
|
|
1335
|
+
);
|
|
1336
|
+
const rows = new Map<string, VerificationAsset>();
|
|
1337
|
+
for (const entry of snapshot.inventory.inventory) {
|
|
1338
|
+
const declaration = snapshot.inventory.declarations.get(entry.sourcePath);
|
|
1339
|
+
const format = declaration?.format ?? verificationSourceFormat(gameRoot, entry.sourcePath);
|
|
1340
|
+
const facts = verificationAuthorFacts(snapshot, entry);
|
|
1341
|
+
const materialized = materializedByGuid.get(entry.guid.toLowerCase());
|
|
1342
|
+
const legacyPublished =
|
|
1343
|
+
declaration?.format === 'pack.json' && declaration.value.schemaVersion !== '3.0.0';
|
|
1344
|
+
rows.set(entry.guid.toLowerCase(), {
|
|
1345
|
+
guid: entry.guid,
|
|
1346
|
+
type: entry.kind,
|
|
1347
|
+
source: {
|
|
1348
|
+
path: entry.sourcePath,
|
|
1349
|
+
format,
|
|
1350
|
+
role: 'author',
|
|
1351
|
+
...(entry.sourceRevision === undefined ? {} : { revision: entry.sourceRevision }),
|
|
1352
|
+
},
|
|
1353
|
+
output: verificationOutput(materialized, legacyPublished),
|
|
1354
|
+
dependencies: facts.dependencies,
|
|
1355
|
+
producer: {
|
|
1356
|
+
state: materialized !== undefined ? 'published' : legacyPublished ? 'published' : 'not-run',
|
|
1357
|
+
},
|
|
1358
|
+
...(facts.name === undefined ? {} : { name: facts.name }),
|
|
1359
|
+
...(entry.sourceKey === undefined ? {} : { sourceKey: entry.sourceKey }),
|
|
1360
|
+
...(entry.sourceIndex === undefined ? {} : { sourceIndex: entry.sourceIndex }),
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
for (const asset of snapshot.materialized) {
|
|
1364
|
+
const key = asset.guid.toLowerCase();
|
|
1365
|
+
if (rows.has(key)) continue;
|
|
1366
|
+
const sourcePath =
|
|
1367
|
+
asset.sourcePath === undefined ? undefined : resolve(gameRoot, asset.sourcePath);
|
|
1368
|
+
const declaration =
|
|
1369
|
+
sourcePath === undefined ? undefined : snapshot.inventory.declarations.get(sourcePath);
|
|
1370
|
+
const format = declaration?.format ?? verificationSourceFormat(gameRoot, asset.sourcePath);
|
|
1371
|
+
rows.set(key, {
|
|
1372
|
+
guid: asset.guid,
|
|
1373
|
+
type: asset.kind,
|
|
1374
|
+
source: {
|
|
1375
|
+
path: sourcePath ?? `${asset.packageId}/${asset.sourceKey}`,
|
|
1376
|
+
format,
|
|
1377
|
+
role: 'author',
|
|
1378
|
+
...(declaration === undefined ? {} : { revision: declaration.sourceRevision }),
|
|
1379
|
+
},
|
|
1380
|
+
output: verificationOutput(asset),
|
|
1381
|
+
dependencies: asset.refs ?? [],
|
|
1382
|
+
producer: { state: 'published' },
|
|
1383
|
+
sourceKey: asset.sourceKey,
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
const assets = [...rows.values()].sort((left, right) =>
|
|
1387
|
+
`${left.source.path}:${left.guid}`.localeCompare(`${right.source.path}:${right.guid}`),
|
|
1388
|
+
);
|
|
1389
|
+
const emitted = assets.slice(0, VERIFY_PROVENANCE_LIMIT);
|
|
1390
|
+
const sourcePaths = [...snapshot.inventory.declarations.keys()].sort();
|
|
1391
|
+
const scriptablePackSources = [...snapshot.inventory.declarations.values()]
|
|
1392
|
+
.filter((declaration) => declaration.format === 'pack.ts')
|
|
1393
|
+
.sort((left, right) => left.sourcePath.localeCompare(right.sourcePath));
|
|
1394
|
+
const unmaterializedScriptable = scriptablePackSources.filter(
|
|
1395
|
+
(source) =>
|
|
1396
|
+
!snapshot.materialized.some(
|
|
1397
|
+
(asset) =>
|
|
1398
|
+
asset.packageId === source.value.packageId.toLowerCase() ||
|
|
1399
|
+
(asset.sourcePath !== undefined &&
|
|
1400
|
+
resolve(gameRoot, asset.sourcePath) === source.sourcePath),
|
|
1401
|
+
),
|
|
1402
|
+
);
|
|
1403
|
+
return {
|
|
1404
|
+
schemaVersion: 'asset-verification-v1',
|
|
1405
|
+
root: resolve(gameRoot),
|
|
1406
|
+
operation: 'asset.verify',
|
|
1407
|
+
requestId: 'gateway',
|
|
1408
|
+
scope: {
|
|
1409
|
+
sourceCount: sourcePaths.length,
|
|
1410
|
+
assetCount: assets.length,
|
|
1411
|
+
sourcePaths: sourcePaths.slice(0, VERIFY_PROVENANCE_LIMIT),
|
|
1412
|
+
omittedSourceCount: Math.max(0, sourcePaths.length - VERIFY_PROVENANCE_LIMIT),
|
|
1413
|
+
assetLimit: VERIFY_PROVENANCE_LIMIT,
|
|
1414
|
+
truncated: assets.length > VERIFY_PROVENANCE_LIMIT,
|
|
1415
|
+
scriptablePackSourceCount: scriptablePackSources.length,
|
|
1416
|
+
scriptablePackSources: unmaterializedScriptable
|
|
1417
|
+
.slice(0, VERIFY_PROVENANCE_LIMIT)
|
|
1418
|
+
.map((source) => ({
|
|
1419
|
+
sourcePath: source.sourcePath,
|
|
1420
|
+
packageId: source.value.packageId,
|
|
1421
|
+
output: 'unproduced' as const,
|
|
1422
|
+
reason: 'producer-not-run' as const,
|
|
1423
|
+
})),
|
|
1424
|
+
omittedScriptablePackSourceCount: Math.max(
|
|
1425
|
+
0,
|
|
1426
|
+
unmaterializedScriptable.length - VERIFY_PROVENANCE_LIMIT,
|
|
1427
|
+
),
|
|
1428
|
+
},
|
|
1429
|
+
assets: emitted,
|
|
1430
|
+
summary: {
|
|
1431
|
+
assetCount: assets.length,
|
|
1432
|
+
emittedAssetCount: emitted.length,
|
|
1433
|
+
materialCount: assets.filter((asset) => asset.type === 'material').length,
|
|
1434
|
+
unproducedAssetCount: assets.filter((asset) => asset.output.status === 'unproduced').length,
|
|
1435
|
+
unknownAssetCount: assets.filter((asset) => asset.output.status === 'unknown').length,
|
|
1436
|
+
unmaterializedScriptablePackCount: unmaterializedScriptable.length,
|
|
1437
|
+
},
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1175
1441
|
async function verify(
|
|
1442
|
+
gameRoot: string,
|
|
1176
1443
|
snapshot: GatewaySnapshot,
|
|
1177
1444
|
operation: PackAuthoringOperation,
|
|
1178
1445
|
): Promise<Result<PackAuthoringOperationResult, PackAuthoringError>> {
|
|
@@ -1220,14 +1487,8 @@ async function verify(
|
|
|
1220
1487
|
);
|
|
1221
1488
|
}
|
|
1222
1489
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
requestId: operation.requestId,
|
|
1226
|
-
snapshot: {
|
|
1227
|
-
sourceCount: snapshot.subjects.size,
|
|
1228
|
-
assetCount: snapshotAssets(snapshot).length,
|
|
1229
|
-
},
|
|
1230
|
-
});
|
|
1490
|
+
const report = createGatewayVerificationReport(gameRoot, snapshot);
|
|
1491
|
+
return ok({ ...report, operation: operation.operation, requestId: operation.requestId });
|
|
1231
1492
|
}
|
|
1232
1493
|
|
|
1233
1494
|
function packageIdFromOperation(
|
|
@@ -1391,7 +1652,8 @@ async function executeRaw(
|
|
|
1391
1652
|
if (!snapshot.ok) return snapshot;
|
|
1392
1653
|
if (operation.operation === 'asset.resolve')
|
|
1393
1654
|
return resolveAsset(options.gameRoot, snapshot.value, operation);
|
|
1394
|
-
if (operation.operation === 'asset.verify')
|
|
1655
|
+
if (operation.operation === 'asset.verify')
|
|
1656
|
+
return verify(options.gameRoot, snapshot.value, operation);
|
|
1395
1657
|
|
|
1396
1658
|
if (operation.operation === 'asset-source.create') {
|
|
1397
1659
|
const path = confinedPath(
|
|
@@ -1939,6 +2201,9 @@ async function executeRaw(
|
|
|
1939
2201
|
selected.value.packageId,
|
|
1940
2202
|
{
|
|
1941
2203
|
format: refreshed.value.relative.endsWith('.pack.json') ? 'pack.json' : 'pack.ts',
|
|
2204
|
+
assets: refreshedSnapshot.value.materialized
|
|
2205
|
+
.filter((asset) => asset.packageId === packageKey(selected.value.packageId))
|
|
2206
|
+
.map((asset) => ({ ...asset })),
|
|
1942
2207
|
},
|
|
1943
2208
|
);
|
|
1944
2209
|
}
|
package/src/pack-authoring.ts
CHANGED
|
@@ -1371,6 +1371,7 @@ export type PackAuthoringResolutionStatus = 'identity' | 'present' | 'ready';
|
|
|
1371
1371
|
export interface PackAuthoringOperationResult {
|
|
1372
1372
|
readonly operation: PackAuthoringOperationId;
|
|
1373
1373
|
readonly requestId: string;
|
|
1374
|
+
readonly schemaVersion?: 'asset-verification-v1';
|
|
1374
1375
|
readonly sourcePath?: string;
|
|
1375
1376
|
readonly targetPath?: string;
|
|
1376
1377
|
readonly revision?: string;
|
|
@@ -1392,6 +1393,31 @@ export interface PackAuthoringOperationResult {
|
|
|
1392
1393
|
readonly sourceCount: number;
|
|
1393
1394
|
readonly assetCount: number;
|
|
1394
1395
|
};
|
|
1396
|
+
/** Bounded source/output facts for read-only verification consumers. */
|
|
1397
|
+
readonly scope?: {
|
|
1398
|
+
readonly sourceCount: number;
|
|
1399
|
+
readonly assetCount: number;
|
|
1400
|
+
readonly sourcePaths: readonly string[];
|
|
1401
|
+
readonly omittedSourceCount: number;
|
|
1402
|
+
readonly assetLimit: number;
|
|
1403
|
+
readonly truncated: boolean;
|
|
1404
|
+
readonly scriptablePackSourceCount: number;
|
|
1405
|
+
readonly scriptablePackSources: readonly {
|
|
1406
|
+
readonly sourcePath: string;
|
|
1407
|
+
readonly packageId: string;
|
|
1408
|
+
readonly output: 'unproduced';
|
|
1409
|
+
readonly reason: 'producer-not-run';
|
|
1410
|
+
}[];
|
|
1411
|
+
readonly omittedScriptablePackSourceCount: number;
|
|
1412
|
+
};
|
|
1413
|
+
readonly summary?: {
|
|
1414
|
+
readonly assetCount: number;
|
|
1415
|
+
readonly emittedAssetCount: number;
|
|
1416
|
+
readonly materialCount: number;
|
|
1417
|
+
readonly unproducedAssetCount: number;
|
|
1418
|
+
readonly unknownAssetCount: number;
|
|
1419
|
+
readonly unmaterializedScriptablePackCount: number;
|
|
1420
|
+
};
|
|
1395
1421
|
}
|
|
1396
1422
|
|
|
1397
1423
|
export interface PackAuthoringGatewayPort<TResult = unknown> {
|
|
@@ -53,6 +53,50 @@ export interface RuntimePackPublicationInput {
|
|
|
53
53
|
readonly externalEvidence?: readonly AssetPublicationExternalEvidence[];
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function isRuntimePackEnvelope(value: unknown): value is RuntimePackEnvelope {
|
|
57
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
58
|
+
const record = value as Record<string, unknown>;
|
|
59
|
+
return (
|
|
60
|
+
record.schemaVersion === '2.0.0' &&
|
|
61
|
+
record.kind === 'internal-text-package' &&
|
|
62
|
+
typeof record.scopeId === 'string' &&
|
|
63
|
+
record.scopeId.length > 0 &&
|
|
64
|
+
typeof record.generation === 'number' &&
|
|
65
|
+
Number.isSafeInteger(record.generation) &&
|
|
66
|
+
record.generation > 0 &&
|
|
67
|
+
Array.isArray(record.assets)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Return the immutable Pack content used by DDC identity. Runtime scope and
|
|
73
|
+
* publication generation fence a live consumer, but do not change the
|
|
74
|
+
* cooked asset bytes represented by an immutable DDC key.
|
|
75
|
+
*/
|
|
76
|
+
export function stripRuntimePackLifecycle(value: unknown): unknown {
|
|
77
|
+
if (!isRuntimePackEnvelope(value)) return value;
|
|
78
|
+
const { scopeId: _scopeId, generation: _generation, ...semantic } = value;
|
|
79
|
+
return semantic;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Rehydrate a DDC Pack payload into the active runtime scope for transport. */
|
|
83
|
+
export function bindRuntimePackScope(value: unknown, scopeId: string, generation: number): unknown {
|
|
84
|
+
if (
|
|
85
|
+
value === null ||
|
|
86
|
+
typeof value !== 'object' ||
|
|
87
|
+
Array.isArray(value) ||
|
|
88
|
+
(value as { readonly schemaVersion?: unknown }).schemaVersion !== '2.0.0' ||
|
|
89
|
+
(value as { readonly kind?: unknown }).kind !== 'internal-text-package' ||
|
|
90
|
+
!Array.isArray((value as { readonly assets?: unknown }).assets)
|
|
91
|
+
) {
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
// The accepted Catalog tuple is authoritative for a live transport. This
|
|
95
|
+
// also repairs a legacy DDC body whose publication generation predates the
|
|
96
|
+
// current accepted candidate.
|
|
97
|
+
return { ...(value as Record<string, unknown>), scopeId, generation };
|
|
98
|
+
}
|
|
99
|
+
|
|
56
100
|
function stable(value: unknown): string {
|
|
57
101
|
if (value instanceof Uint8Array) return `bytes:${Buffer.from(value).toString('base64')}`;
|
|
58
102
|
if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
|
package/src/scanner.ts
CHANGED
|
@@ -63,11 +63,11 @@ export interface ScriptablePackScanOptions {
|
|
|
63
63
|
|
|
64
64
|
/** Stable default policy shared by inventory and production owners. */
|
|
65
65
|
export const STANDARD_SCRIPTABLE_PACK_SCAN_OPTIONS = Object.freeze({
|
|
66
|
-
// Scriptable Packs execute in an isolated worker.
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
timeoutMs:
|
|
66
|
+
// Scriptable Packs execute in an isolated worker. A cold worker must compile
|
|
67
|
+
// the authored source closure before it can return metadata. Keep the bound
|
|
68
|
+
// finite while allowing a heavy character/scene closure to complete on a
|
|
69
|
+
// busy host.
|
|
70
|
+
timeoutMs: 120_000,
|
|
71
71
|
}) satisfies ScriptablePackScanOptions;
|
|
72
72
|
|
|
73
73
|
export interface InventoryDeclaration {
|