@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.
Files changed (40) hide show
  1. package/README.md +2 -2
  2. package/dist/build.d.ts +1 -1
  3. package/dist/build.d.ts.map +1 -1
  4. package/dist/build.mjs +252 -22
  5. package/dist/build.mjs.map +1 -1
  6. package/dist/cli-asset.d.ts +63 -0
  7. package/dist/cli-asset.d.ts.map +1 -1
  8. package/dist/cli-asset.mjs +142 -4
  9. package/dist/cli-asset.mjs.map +1 -1
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/native-cooker-registry.d.ts.map +1 -1
  12. package/dist/native-cooker.mjs +13 -1
  13. package/dist/native-cooker.mjs.map +1 -1
  14. package/dist/pack-authoring-node.d.ts +6 -0
  15. package/dist/pack-authoring-node.d.ts.map +1 -1
  16. package/dist/pack-authoring-node.mjs +230 -16
  17. package/dist/pack-authoring-node.mjs.map +1 -1
  18. package/dist/pack-authoring.d.ts +26 -0
  19. package/dist/pack-authoring.d.ts.map +1 -1
  20. package/dist/pack-authoring.mjs.map +1 -1
  21. package/dist/runtime-publication.d.ts +8 -0
  22. package/dist/runtime-publication.d.ts.map +1 -1
  23. package/dist/scanner.d.ts +1 -1
  24. package/dist/scanner.mjs +5 -5
  25. package/dist/scanner.mjs.map +1 -1
  26. package/dist/scriptable-pack-node.mjs.map +1 -1
  27. package/dist/scriptable-pack.mjs.map +1 -1
  28. package/package.json +2 -2
  29. package/src/__tests__/cli.unit.test.ts +87 -0
  30. package/src/__tests__/native-cooker-registry.test.ts +16 -0
  31. package/src/__tests__/pack-authoring-gateway.unit.test.ts +120 -1
  32. package/src/__tests__/pack.unit.test.ts +6 -6
  33. package/src/__tests__/runtime-publication.unit.test.ts +27 -1
  34. package/src/build.ts +2 -0
  35. package/src/cli-asset.ts +227 -5
  36. package/src/native-cooker-registry.ts +14 -1
  37. package/src/pack-authoring-node.ts +278 -13
  38. package/src/pack-authoring.ts +26 -0
  39. package/src/runtime-publication.ts +44 -0
  40. 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
- ...sourceResult(subject, operation),
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
- ...sourceResult(subject, operation),
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
- ...sourceResult(subject, operation),
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(sourceResult(subject, operation));
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
- return ok({
1224
- operation: operation.operation,
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') return verify(snapshot.value, operation);
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
  }
@@ -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. A cold worker must
67
- // compile the authored source closure before it can return metadata;
68
- // heavy procedural packs otherwise make the shared catalog fall back to
69
- // an empty degraded projection at the old 15-second budget.
70
- timeoutMs: 60_000,
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 {