@forgeax/engine-pack 0.1.28 → 0.1.30

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 (63) hide show
  1. package/README.md +33 -5
  2. package/dist/build.mjs +99 -60
  3. package/dist/build.mjs.map +1 -1
  4. package/dist/catalog-projection.d.ts +3 -3
  5. package/dist/cli-asset.mjs +22 -13
  6. package/dist/cli-asset.mjs.map +1 -1
  7. package/dist/evidence/material-cook.d.ts +7 -1
  8. package/dist/evidence/material-cook.d.ts.map +1 -1
  9. package/dist/index.mjs +61 -5
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/inventory/declaration.d.ts +1 -1
  12. package/dist/inventory/declaration.d.ts.map +1 -1
  13. package/dist/inventory/sync.d.ts.map +1 -1
  14. package/dist/material-cook.mjs +61 -5
  15. package/dist/material-cook.mjs.map +1 -1
  16. package/dist/native-cooker-registry.d.ts +1 -0
  17. package/dist/native-cooker-registry.d.ts.map +1 -1
  18. package/dist/native-cooker.mjs.map +1 -1
  19. package/dist/pack-authoring-node.mjs +22 -13
  20. package/dist/pack-authoring-node.mjs.map +1 -1
  21. package/dist/pack-authoring.d.ts +8 -2
  22. package/dist/pack-authoring.d.ts.map +1 -1
  23. package/dist/pack-authoring.mjs.map +1 -1
  24. package/dist/runtime-publication.d.ts +2 -0
  25. package/dist/runtime-publication.d.ts.map +1 -1
  26. package/dist/runtime.mjs.map +1 -1
  27. package/dist/scanner.mjs +22 -13
  28. package/dist/scanner.mjs.map +1 -1
  29. package/dist/schema-compiled.d.ts.map +1 -1
  30. package/dist/schema.mjs +40 -62
  31. package/dist/schema.mjs.map +1 -1
  32. package/dist/scriptable-pack-node.d.ts.map +1 -1
  33. package/dist/scriptable-pack-node.mjs +12 -4
  34. package/dist/scriptable-pack-node.mjs.map +1 -1
  35. package/dist/scriptable-pack-worker.mjs +12 -5
  36. package/dist/scriptable-pack-worker.mjs.map +1 -1
  37. package/dist/scriptable-pack.d.ts +2 -1
  38. package/dist/scriptable-pack.d.ts.map +1 -1
  39. package/dist/scriptable-pack.mjs.map +1 -1
  40. package/package.json +2 -2
  41. package/src/__tests__/inventory-schema.test.ts +10 -10
  42. package/src/__tests__/material-cook-schema.unit.test.ts +92 -1
  43. package/src/__tests__/pack-authoring.unit.test.ts +2 -4
  44. package/src/__tests__/pack.unit.test.ts +86 -124
  45. package/src/__tests__/runtime-publication.unit.test.ts +36 -0
  46. package/src/__tests__/scanner-instance-cycle.test.ts +92 -0
  47. package/src/__tests__/scanner-inventory.contract.test.ts +2 -2
  48. package/src/__tests__/scriptable-pack-cli.integration.test.ts +35 -1
  49. package/src/__tests__/scriptable-pack-diagnostic.integration.test.ts +45 -0
  50. package/src/evidence/material-cook.ts +79 -5
  51. package/src/inventory/binding.ts +3 -3
  52. package/src/inventory/declaration.ts +10 -48
  53. package/src/inventory/sync.ts +3 -2
  54. package/src/native-cooker-registry.ts +1 -0
  55. package/src/pack-authoring.ts +11 -2
  56. package/src/runtime-publication.ts +7 -3
  57. package/src/scanner.ts +28 -28
  58. package/src/schema/material-cook.schema.json +107 -0
  59. package/src/schema-compiled.ts +40 -79
  60. package/src/scriptable-pack-node.ts +16 -4
  61. package/src/scriptable-pack-worker.ts +23 -5
  62. package/src/scriptable-pack.ts +2 -1
  63. package/src/__tests__/scanner-mount-cycle.test.ts +0 -232
package/README.md CHANGED
@@ -37,11 +37,22 @@ export default definePack({
37
37
  build: ({ packageId: subjectId }) =>
38
38
  ok({
39
39
  'mesh/main': { kind: 'mesh', materialSlots: [] },
40
+ 'scene/hero': {
41
+ kind: 'scene',
42
+ entities: { body: { components: {} } },
43
+ },
40
44
  'scene/main': {
41
45
  kind: 'scene',
42
- entities: [],
43
- mounts: [],
44
- mesh: AssetGuid.format(AssetGuid.derive(subjectId, 'mesh/main')),
46
+ entities: {
47
+ root: { components: {} },
48
+ hero: {
49
+ components: {},
50
+ instance: {
51
+ source: AssetGuid.format(AssetGuid.derive(subjectId, 'scene/hero')),
52
+ overrides: [{ target: ['body'], components: {} }],
53
+ },
54
+ },
55
+ },
45
56
  },
46
57
  }),
47
58
  });
@@ -201,9 +212,10 @@ Pack owns the offline half of the GUID evidence chain: `source inventory -> cata
201
212
 
202
213
  The catalog is a locator, not proof. `lookup/verify --guid --project --catalog --json` joins the source meta or authored pack, the catalog row, the receipt, and the package descriptors. Both commands emit one JSON record on stdout or one structured `{code, expected, hint, detail}` record on stderr; there is no runtime or WebSocket dependency.
203
214
 
204
- ## VFX Pack v2 migration
215
+ ## VFX Program v3 in Pack v2
205
216
 
206
- VFX source v2 is cooked in one atomic producer step. The executable contract is
217
+ VFX source v3 is cooked in one atomic producer step and stored in the ordinary
218
+ Pack v2 envelope. The executable contract is
207
219
  `scripts/asset-cook-contract.mjs`; a package-local scripts path is not valid.
208
220
  The cook publishes the Pack payload and
209
221
  `particle-effect/program.json` with the same source fingerprint. Runtime loads
@@ -266,6 +278,22 @@ vectors, colors, and asset-GUID constraints. The isolated Pack worker passes
266
278
  only serializable authoring data to the build and never exposes filesystem or
267
279
  runtime state.
268
280
 
281
+ Custom build-only content uses `PackCookSource` in the same output map:
282
+
283
+ ```ts
284
+ { 'volume/main': { kind: 'game-volume', execution: 'cooked', source: { size: values.size } } }
285
+ ```
286
+
287
+ Register the matching `NativeCooker` in the build host. The host supplies the
288
+ derived GUID, source key/path and source data; the cooker returns matching-kind
289
+ metadata, references and artifact bytes. Missing cookers, rewritten GUIDs and
290
+ invalid products fail before publication. Custom sources can coexist with
291
+ ordinary Assets and inherit normal Pack parameters and instance identities.
292
+ They cannot override the closed ordinary Asset kinds. Authoring `readByGuid`
293
+ can explicitly request a `PackCookSource` from the current build generation;
294
+ that is source data, not a decoded runtime asset. Runtime custom kinds still
295
+ need their normal Registry loader/decoder. No cooker enters the player.
296
+
269
297
  `forgeax asset inspect --subject <source.pack.ts> --root <project> --json` executes module initialization, validates the default export, and projects canonical Meta without calling `build`. `@forgeax/engine-pack/source-node` accepts a host executor with `load` and optional `dispose`; `timeoutMs` bounds module initialization and `buildTimeoutMs` bounds one `build(context)` call. A build timeout returns one structured `pack-source-load-failed` Result with `detail.phase: 'build'`, the configured `timeoutMs`, and deterministic cleanup of the isolated worker and compile root.
270
298
 
271
299
  The default worker executes the complete relative TypeScript module closure on the supported Node floor, including Node 22 hosts that do not load `.ts` files directly. It transpiles that closure into a disposable ESM directory, resolves bare imports through the source project's nearest `node_modules`, and removes the directory when the worker is disposed. Bulk producers use the internal `createScriptablePackModuleExecutorPool()` with two recyclable workers; a pooled lease is released after metadata projection or one build, so a generation never retains one live Worker-backed definition per source.
package/dist/build.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { ok, err, catalogOperationsFor, authoringCapabilityForAssetKind, MATERIAL_TEXTURE_SLOTS, projectCookProductEvidence, projectAssetEvidence, validateSourceOverrideMap, PACK_ERROR_HINTS, catalogEntryDigest, deriveStandardLayerPlan, AssetError, ImportError } from '@forgeax/engine-types';
1
+ import { ok, err, catalogOperationsFor, authoringCapabilityForAssetKind, MATERIAL_TEXTURE_SLOTS, isMaterialProgramAbi, projectCookProductEvidence, projectAssetEvidence, validateSourceOverrideMap, PACK_ERROR_HINTS, catalogEntryDigest, deriveStandardLayerPlan, AssetError, ImportError } from '@forgeax/engine-types';
2
2
  import { dirname, resolve, isAbsolute, relative, join, basename, sep, extname } from 'path';
3
3
  import { sha1 } from '@noble/hashes/legacy.js';
4
4
  import { uuidv7obj } from 'uuidv7';
@@ -9061,9 +9061,17 @@ var WorkerScriptablePackExecutor = class {
9061
9061
  }
9062
9062
  };
9063
9063
  worker.on("message", onMessage);
9064
- worker.once("error", rejectLoad);
9064
+ const fail = (error) => {
9065
+ rejectLoad(error);
9066
+ const active = this.build;
9067
+ this.build = void 0;
9068
+ active?.reject(error);
9069
+ void this.dispose("failure");
9070
+ };
9071
+ worker.once("error", fail);
9065
9072
  worker.once("exit", (code) => {
9066
- if (code !== 0) rejectLoad(new Error(`ScriptablePack worker exited with code ${code}`));
9073
+ if (this.disposal !== void 0) return;
9074
+ fail(new Error(`ScriptablePack worker exited with code ${code}`));
9067
9075
  });
9068
9076
  });
9069
9077
  }
@@ -9141,7 +9149,7 @@ function scriptablePackLoadFailure(sourcePath, reason, phase, diagnostic, timeou
9141
9149
  }
9142
9150
  async function loadScriptablePack(sourcePath, options = {}) {
9143
9151
  const timeoutMs = options.timeoutMs ?? 15e3;
9144
- const buildTimeoutMs = options.buildTimeoutMs ?? 5e3;
9152
+ const buildTimeoutMs = options.buildTimeoutMs ?? 15e3;
9145
9153
  const executor = options.executor ?? new WorkerScriptablePackExecutor();
9146
9154
  let disposeReason;
9147
9155
  let timeout;
@@ -9298,16 +9306,17 @@ function makePackError(code, detail) {
9298
9306
  detail
9299
9307
  });
9300
9308
  }
9301
- function* extractMountSourceGuids(asset) {
9309
+ function* extractInstanceSourceGuids(asset) {
9302
9310
  if (asset.kind !== "scene") return;
9303
9311
  const payload = asset.payload;
9304
- if (!payload || !Array.isArray(payload.mounts)) return;
9305
- for (const rawMount of payload.mounts) {
9306
- const mount = rawMount;
9307
- const idx = mount.source;
9308
- if (typeof idx !== "number" || !Number.isInteger(idx)) continue;
9309
- if (idx < 0 || idx >= asset.refs.length) continue;
9310
- const resolved = asset.refs[idx];
9312
+ if (!payload || payload.entities === null || typeof payload.entities !== "object" || Array.isArray(payload.entities))
9313
+ return;
9314
+ for (const rawEntity of Object.values(payload.entities)) {
9315
+ if (rawEntity === null || typeof rawEntity !== "object" || Array.isArray(rawEntity)) continue;
9316
+ const instance = rawEntity.instance;
9317
+ if (instance === null || typeof instance !== "object" || Array.isArray(instance)) continue;
9318
+ const source = instance.source;
9319
+ const resolved = typeof source === "number" && Number.isInteger(source) ? asset.refs[source] : typeof source === "string" ? source : void 0;
9311
9320
  if (typeof resolved !== "string") continue;
9312
9321
  yield resolved.toLowerCase();
9313
9322
  }
@@ -9425,7 +9434,7 @@ async function scanValidated(roots, opts = {}, capture) {
9425
9434
  guidToPath.set(normalizedGuid, packPath);
9426
9435
  packRefs.set(normalizedGuid, [
9427
9436
  ...asset.refs.map((ref) => ref.toLowerCase()),
9428
- ...extractMountSourceGuids(asset)
9437
+ ...extractInstanceSourceGuids(asset)
9429
9438
  ]);
9430
9439
  }
9431
9440
  }
@@ -9529,7 +9538,7 @@ async function scanValidated(roots, opts = {}, capture) {
9529
9538
  guidToPath.set(normalizedGuid, packPath);
9530
9539
  packRefs.set(normalizedGuid, [
9531
9540
  ...asset.refs.map((ref) => ref.toLowerCase()),
9532
- ...extractMountSourceGuids(asset)
9541
+ ...extractInstanceSourceGuids(asset)
9533
9542
  ]);
9534
9543
  }
9535
9544
  capture?.declarations.set(packPath, {
@@ -10538,6 +10547,9 @@ function validateCookedMaterialRecord(value) {
10538
10547
  const programs = [];
10539
10548
  const programKeys = /* @__PURE__ */ new Set();
10540
10549
  const selections = /* @__PURE__ */ new Set();
10550
+ const submissionSelections = /* @__PURE__ */ new Map();
10551
+ let modernPublication = false;
10552
+ let legacySelectionField;
10541
10553
  const selectedPasses = /* @__PURE__ */ new Set();
10542
10554
  for (const [index, entry] of candidate.programs.entries()) {
10543
10555
  const field = `programs[${index}]`;
@@ -10555,9 +10567,11 @@ function validateCookedMaterialRecord(value) {
10555
10567
  if (!Array.isArray(program.selections) || program.selections.length === 0)
10556
10568
  return invalid(`${field}.selections`);
10557
10569
  const programSelections = [];
10558
- for (const [selectionIndex, selection] of program.selections.entries()) {
10570
+ for (const [selectionIndex, rawSelection] of program.selections.entries()) {
10559
10571
  const selectionField = `${field}.selections[${selectionIndex}]`;
10560
- if (selection === null || typeof selection !== "object" || !passNames.has(selection.pass))
10572
+ if (rawSelection === null || typeof rawSelection !== "object") return invalid(selectionField);
10573
+ const selection = rawSelection;
10574
+ if (typeof selection.pass !== "string" || !passNames.has(selection.pass))
10561
10575
  return invalid(selectionField);
10562
10576
  const context = validateMaterialCookProgramContext(selection.context);
10563
10577
  if (!context.ok)
@@ -10565,11 +10579,51 @@ function validateCookedMaterialRecord(value) {
10565
10579
  `${selectionField}.${context.error.detail.field}`,
10566
10580
  context.error.detail.actual
10567
10581
  );
10568
- const key = JSON.stringify([selection.pass, materialProgramContextKey(context.value)]);
10582
+ const address = selection.address === void 0 ? "direct" : selection.address;
10583
+ if (address !== "direct" && address !== "scene-index")
10584
+ return invalid(`${selectionField}.address`, selection.address);
10585
+ const hasAddressFacts = selection.address !== void 0 || selection.entry !== void 0 || selection.abi !== void 0;
10586
+ modernPublication ||= hasAddressFacts;
10587
+ if (!hasAddressFacts) legacySelectionField ??= selectionField;
10588
+ const rawEntry = selection.entry;
10589
+ const entry2 = typeof rawEntry === "string" ? rawEntry : void 0;
10590
+ if (hasAddressFacts && selection.address === void 0)
10591
+ return invalid(
10592
+ `${selectionField}.address`,
10593
+ "modern ABI selections require an explicit address"
10594
+ );
10595
+ if (hasAddressFacts && (entry2 === void 0 || entry2.length === 0))
10596
+ return invalid(`${selectionField}.entry`, rawEntry);
10597
+ if (hasAddressFacts && !isMaterialProgramAbi(selection.abi))
10598
+ return invalid(`${selectionField}.abi`, selection.abi);
10599
+ if (hasAddressFacts) {
10600
+ const abi = selection.abi;
10601
+ const expectedEntry = address === "direct" ? abi.directEntry : abi.sceneIndexEntry;
10602
+ if (entry2 !== expectedEntry)
10603
+ return invalid(`${selectionField}.entry`, "entry does not match published ABI");
10604
+ const submissionKey = JSON.stringify([
10605
+ selection.pass,
10606
+ materialProgramContextKey(context.value)
10607
+ ]);
10608
+ const addresses = submissionSelections.get(submissionKey) ?? /* @__PURE__ */ new Set();
10609
+ addresses.add(address);
10610
+ submissionSelections.set(submissionKey, addresses);
10611
+ }
10612
+ const key = JSON.stringify([
10613
+ selection.pass,
10614
+ materialProgramContextKey(context.value),
10615
+ address
10616
+ ]);
10569
10617
  if (selections.has(key)) return invalid(selectionField, "ambiguous Pass/context selection");
10570
10618
  selections.add(key);
10571
10619
  selectedPasses.add(selection.pass);
10572
- programSelections.push({ pass: selection.pass, context: context.value });
10620
+ programSelections.push({
10621
+ pass: selection.pass,
10622
+ context: context.value,
10623
+ ...selection.address === void 0 ? {} : { address },
10624
+ ...entry2 === void 0 ? {} : { entry: entry2 },
10625
+ ...selection.abi === void 0 ? {} : { abi: selection.abi }
10626
+ });
10573
10627
  }
10574
10628
  programs.push({
10575
10629
  specializationKey: program.specializationKey,
@@ -10577,6 +10631,17 @@ function validateCookedMaterialRecord(value) {
10577
10631
  selections: programSelections
10578
10632
  });
10579
10633
  }
10634
+ if (modernPublication && legacySelectionField !== void 0) {
10635
+ return invalid(
10636
+ legacySelectionField,
10637
+ "modern material publications require address, entry, and ABI facts on every selection"
10638
+ );
10639
+ }
10640
+ for (const [selectionKey, addresses] of submissionSelections) {
10641
+ if (addresses.size !== 2) {
10642
+ return invalid("programs.selections", `incomplete submission address pair: ${selectionKey}`);
10643
+ }
10644
+ }
10580
10645
  if ([...passNames].some((pass) => !selectedPasses.has(pass)))
10581
10646
  return invalid("programs.selections", "unpublished Pass");
10582
10647
  const manifestDigest = createMaterialProgramSetDigest(programs, passes);
@@ -10714,9 +10779,9 @@ function projectAssetRefs(inventory) {
10714
10779
  }
10715
10780
  function projectSceneEntityRefs(inventory) {
10716
10781
  return inventory.declarations.flatMap(
10717
- (row) => row.sceneBindings === void 0 ? [] : row.sceneBindings.map((bindingKey) => ({
10782
+ (row) => row.sceneEntityKeys === void 0 ? [] : row.sceneEntityKeys.map((address) => ({
10718
10783
  sceneSourceKey: row.sourceKey,
10719
- bindingKey
10784
+ address
10720
10785
  }))
10721
10786
  );
10722
10787
  }
@@ -10776,49 +10841,23 @@ function validateAuthorInventory(value) {
10776
10841
  { guid, sourceKey: sourceKey2 }
10777
10842
  );
10778
10843
  }
10779
- if (row.kind === "scene") {
10780
- const payload = row.payload;
10781
- const payloadRecord = typeof payload === "object" && payload !== null ? payload : void 0;
10782
- const entities = payloadRecord !== void 0 && Array.isArray(payloadRecord.entities) ? payloadRecord.entities : [];
10783
- const bindings = /* @__PURE__ */ new Set();
10784
- for (const entity of entities) {
10785
- const bindingKey = typeof entity === "object" && entity !== null && typeof entity.bindingKey === "string" ? entity.bindingKey : void 0;
10786
- if (bindingKey !== void 0 && bindingKey.length === 0) {
10787
- return failure2(
10788
- "inventory-scene-binding-missing",
10789
- "each declared scene bindingKey is non-empty",
10790
- "remove the empty bindingKey or declare a stable key in the scene producer",
10791
- { sourceKey: sourceKey2 }
10792
- );
10793
- }
10794
- if (bindingKey === void 0) continue;
10795
- if (bindings.has(bindingKey)) {
10796
- return failure2(
10797
- "inventory-scene-binding-duplicate",
10798
- "bindingKey values are unique within one scene",
10799
- "rename the duplicate bindingKey in the scene producer",
10800
- { sourceKey: sourceKey2 }
10801
- );
10802
- }
10803
- bindings.add(bindingKey);
10804
- }
10805
- }
10806
10844
  guids.add(normalizedGuid);
10807
10845
  sourceKeys.add(sourceKey2);
10808
- const sceneBindings = row.kind === "scene" && typeof row.payload === "object" && row.payload !== null ? row.payload.entities?.flatMap(
10809
- (entity) => {
10810
- if (typeof entity !== "object" || entity === null) return [];
10811
- const key = entity.bindingKey;
10812
- return typeof key === "string" && key.length > 0 ? [key] : [];
10813
- }
10814
- ) : void 0;
10846
+ const sceneEntityKeys = (() => {
10847
+ if (row.kind !== "scene" || typeof row.payload !== "object" || row.payload === null)
10848
+ return void 0;
10849
+ const entities = row.payload.entities;
10850
+ if (entities === null || typeof entities !== "object" || Array.isArray(entities))
10851
+ return void 0;
10852
+ return Object.keys(entities);
10853
+ })();
10815
10854
  declarations.push({
10816
10855
  guid,
10817
10856
  sourceKey: sourceKey2,
10818
10857
  kind: typeof row.kind === "string" ? row.kind : "unknown",
10819
10858
  payload: typeof row.payload === "object" && row.payload !== null ? row.payload : {},
10820
10859
  refs: Array.isArray(row.refs) ? row.refs.filter((ref) => typeof ref === "string") : [],
10821
- ...sceneBindings === void 0 ? {} : { sceneBindings }
10860
+ ...sceneEntityKeys === void 0 ? {} : { sceneEntityKeys }
10822
10861
  });
10823
10862
  }
10824
10863
  return ok({ declarations });
@@ -10827,7 +10866,7 @@ function validateAuthorInventory(value) {
10827
10866
  // src/inventory/sync.ts
10828
10867
  function inventoryDigest(inventory) {
10829
10868
  return inventory.declarations.map(
10830
- (row) => `${row.guid}\0${row.sourceKey}\0${row.kind}\0${row.sceneBindings?.join("\0") ?? ""}`
10869
+ (row) => `${row.guid}\0${row.sourceKey}\0${row.kind}\0${row.sceneEntityKeys?.join("\0") ?? ""}`
10831
10870
  ).sort().join("\n");
10832
10871
  }
10833
10872
  function syncAuthorInventory(inventory) {
@@ -10835,7 +10874,7 @@ function syncAuthorInventory(inventory) {
10835
10874
  declarations: inventory.declarations.map((row) => ({
10836
10875
  ...row,
10837
10876
  refs: [...row.refs],
10838
- ...row.sceneBindings === void 0 ? {} : { sceneBindings: [...row.sceneBindings] }
10877
+ ...row.sceneEntityKeys === void 0 ? {} : { sceneEntityKeys: [...row.sceneEntityKeys] }
10839
10878
  }))
10840
10879
  };
10841
10880
  }
@@ -12784,10 +12823,10 @@ function normalizedAssets(pack) {
12784
12823
  };
12785
12824
  });
12786
12825
  }
12787
- function outputFor(asset) {
12826
+ function outputFor(asset, sourceKey2) {
12788
12827
  return {
12789
12828
  guid: asset.guid.toLowerCase(),
12790
- sourceKey: asset.guid.toLowerCase(),
12829
+ sourceKey: sourceKey2 ?? asset.guid.toLowerCase(),
12791
12830
  kind: asset.kind,
12792
12831
  digest: digest2({
12793
12832
  guid: asset.guid.toLowerCase(),
@@ -12826,7 +12865,7 @@ function createRuntimePackPublication(input) {
12826
12865
  )
12827
12866
  };
12828
12867
  const valueDigest = input.digest ?? digest2(semantic);
12829
- const outputs = input.outputs ?? assets.map(outputFor);
12868
+ const outputs = input.outputs ?? assets.map((asset) => outputFor(asset, input.sourceKeys?.get(asset.guid.toLowerCase())));
12830
12869
  const outputDigest = outputSetDigest(outputs);
12831
12870
  const generation = input.generation ?? publicationGeneration(input.sourceRevision, valueDigest, outputDigest);
12832
12871
  const externalEvidence = input.externalEvidence ?? [];