@forgeax/engine-pack 0.1.24 → 0.1.26

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 (122) hide show
  1. package/README.md +1 -1
  2. package/dist/build.d.ts +1 -1
  3. package/dist/build.d.ts.map +1 -1
  4. package/dist/build.mjs +129 -26
  5. package/dist/build.mjs.map +1 -1
  6. package/dist/cli-asset.mjs.map +1 -1
  7. package/dist/evidence/material-cook.d.ts +29 -3
  8. package/dist/evidence/material-cook.d.ts.map +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.mjs +123 -24
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/material-cook.d.ts +1 -1
  14. package/dist/material-cook.d.ts.map +1 -1
  15. package/dist/material-cook.mjs +123 -24
  16. package/dist/material-cook.mjs.map +1 -1
  17. package/dist/pack-authoring-node.mjs.map +1 -1
  18. package/dist/scanner.d.ts +3 -1
  19. package/dist/scanner.d.ts.map +1 -1
  20. package/dist/scanner.mjs +7 -3
  21. package/dist/scanner.mjs.map +1 -1
  22. package/package.json +2 -2
  23. package/src/__tests__/material-cook-receipt.unit.test.ts +9 -9
  24. package/src/__tests__/material-cook-schema.unit.test.ts +172 -21
  25. package/src/__tests__/material-declaration-boundary.unit.test.ts +35 -0
  26. package/src/__tests__/material-surface-consumer-census.test.ts +24 -0
  27. package/src/__tests__/material-surface-publication.unit.test.ts +162 -0
  28. package/src/__tests__/material-surface-wire.unit.test.ts +30 -0
  29. package/src/build.ts +2 -0
  30. package/src/evidence/material-cook.ts +183 -31
  31. package/src/index.ts +5 -0
  32. package/src/material-cook.ts +5 -0
  33. package/src/scanner.ts +7 -3
  34. package/src/schema/material-cook.schema.json +254 -50
  35. package/dist/__tests__/ai-recovery.unit.test.d.ts +0 -2
  36. package/dist/__tests__/ai-recovery.unit.test.d.ts.map +0 -1
  37. package/dist/__tests__/artifact-path.test.d.ts +0 -2
  38. package/dist/__tests__/artifact-path.test.d.ts.map +0 -1
  39. package/dist/__tests__/asset-evidence-error-hints.test.d.ts +0 -2
  40. package/dist/__tests__/asset-evidence-error-hints.test.d.ts.map +0 -1
  41. package/dist/__tests__/asset-evidence-lifecycle.test.d.ts +0 -2
  42. package/dist/__tests__/asset-evidence-lifecycle.test.d.ts.map +0 -1
  43. package/dist/__tests__/asset-evidence-schema.test.d.ts +0 -2
  44. package/dist/__tests__/asset-evidence-schema.test.d.ts.map +0 -1
  45. package/dist/__tests__/atlas-cli-region-mismatch.test.d.ts +0 -2
  46. package/dist/__tests__/atlas-cli-region-mismatch.test.d.ts.map +0 -1
  47. package/dist/__tests__/atlas-error-code-owner.test-d.d.ts +0 -2
  48. package/dist/__tests__/atlas-error-code-owner.test-d.d.ts.map +0 -1
  49. package/dist/__tests__/cli-asset-evidence.integration.test.d.ts +0 -2
  50. package/dist/__tests__/cli-asset-evidence.integration.test.d.ts.map +0 -1
  51. package/dist/__tests__/cli.unit.test.d.ts +0 -2
  52. package/dist/__tests__/cli.unit.test.d.ts.map +0 -1
  53. package/dist/__tests__/evidence-source-inventory.unit.test.d.ts +0 -2
  54. package/dist/__tests__/evidence-source-inventory.unit.test.d.ts.map +0 -1
  55. package/dist/__tests__/guid-collision.unit.test.d.ts +0 -2
  56. package/dist/__tests__/guid-collision.unit.test.d.ts.map +0 -1
  57. package/dist/__tests__/guid-validator-surface.unit.test.d.ts +0 -2
  58. package/dist/__tests__/guid-validator-surface.unit.test.d.ts.map +0 -1
  59. package/dist/__tests__/inventory-schema.test.d.ts +0 -2
  60. package/dist/__tests__/inventory-schema.test.d.ts.map +0 -1
  61. package/dist/__tests__/material-cook-dependencies.unit.test.d.ts +0 -2
  62. package/dist/__tests__/material-cook-dependencies.unit.test.d.ts.map +0 -1
  63. package/dist/__tests__/material-cook-receipt-layer-plan.unit.test.d.ts +0 -2
  64. package/dist/__tests__/material-cook-receipt-layer-plan.unit.test.d.ts.map +0 -1
  65. package/dist/__tests__/material-cook-receipt.unit.test.d.ts +0 -2
  66. package/dist/__tests__/material-cook-receipt.unit.test.d.ts.map +0 -1
  67. package/dist/__tests__/material-cook-schema.unit.test.d.ts +0 -2
  68. package/dist/__tests__/material-cook-schema.unit.test.d.ts.map +0 -1
  69. package/dist/__tests__/mesh-bin-consumer-surface.unit.test.d.ts +0 -2
  70. package/dist/__tests__/mesh-bin-consumer-surface.unit.test.d.ts.map +0 -1
  71. package/dist/__tests__/mesh-bin-contract.unit.test.d.ts +0 -2
  72. package/dist/__tests__/mesh-bin-contract.unit.test.d.ts.map +0 -1
  73. package/dist/__tests__/meta-schema-optional.test.d.ts +0 -2
  74. package/dist/__tests__/meta-schema-optional.test.d.ts.map +0 -1
  75. package/dist/__tests__/meta-source-overrides.unit.test.d.ts +0 -2
  76. package/dist/__tests__/meta-source-overrides.unit.test.d.ts.map +0 -1
  77. package/dist/__tests__/native-cooker-registry.test.d.ts +0 -2
  78. package/dist/__tests__/native-cooker-registry.test.d.ts.map +0 -1
  79. package/dist/__tests__/offline-evidence-artifact-verification-owner.test-d.d.ts +0 -2
  80. package/dist/__tests__/offline-evidence-artifact-verification-owner.test-d.d.ts.map +0 -1
  81. package/dist/__tests__/offline-evidence.integration.test.d.ts +0 -2
  82. package/dist/__tests__/offline-evidence.integration.test.d.ts.map +0 -1
  83. package/dist/__tests__/owner-chain.integration.test.d.ts +0 -2
  84. package/dist/__tests__/owner-chain.integration.test.d.ts.map +0 -1
  85. package/dist/__tests__/pack-authoring-gateway.unit.test.d.ts +0 -2
  86. package/dist/__tests__/pack-authoring-gateway.unit.test.d.ts.map +0 -1
  87. package/dist/__tests__/pack-authoring.unit.test.d.ts +0 -2
  88. package/dist/__tests__/pack-authoring.unit.test.d.ts.map +0 -1
  89. package/dist/__tests__/pack-scanner.unit.test.d.ts +0 -2
  90. package/dist/__tests__/pack-scanner.unit.test.d.ts.map +0 -1
  91. package/dist/__tests__/pack-v2-fixture-migration.test.d.ts +0 -2
  92. package/dist/__tests__/pack-v2-fixture-migration.test.d.ts.map +0 -1
  93. package/dist/__tests__/pack-v2-schema.test.d.ts +0 -2
  94. package/dist/__tests__/pack-v2-schema.test.d.ts.map +0 -1
  95. package/dist/__tests__/pack.unit.test.d.ts +0 -2
  96. package/dist/__tests__/pack.unit.test.d.ts.map +0 -1
  97. package/dist/__tests__/package-finalizer.contract.test.d.ts +0 -2
  98. package/dist/__tests__/package-finalizer.contract.test.d.ts.map +0 -1
  99. package/dist/__tests__/producer-contract-schema.unit.test.d.ts +0 -2
  100. package/dist/__tests__/producer-contract-schema.unit.test.d.ts.map +0 -1
  101. package/dist/__tests__/resolve-asset-source.test.d.ts +0 -2
  102. package/dist/__tests__/resolve-asset-source.test.d.ts.map +0 -1
  103. package/dist/__tests__/runtime-projection.unit.test.d.ts +0 -2
  104. package/dist/__tests__/runtime-projection.unit.test.d.ts.map +0 -1
  105. package/dist/__tests__/runtime.browser.test.d.ts +0 -2
  106. package/dist/__tests__/runtime.browser.test.d.ts.map +0 -1
  107. package/dist/__tests__/scanner-blacklist.test.d.ts +0 -2
  108. package/dist/__tests__/scanner-blacklist.test.d.ts.map +0 -1
  109. package/dist/__tests__/scanner-inventory.contract.test.d.ts +0 -2
  110. package/dist/__tests__/scanner-inventory.contract.test.d.ts.map +0 -1
  111. package/dist/__tests__/scanner-inventory.test.d.ts +0 -2
  112. package/dist/__tests__/scanner-inventory.test.d.ts.map +0 -1
  113. package/dist/__tests__/scanner-mount-cycle.test.d.ts +0 -2
  114. package/dist/__tests__/scanner-mount-cycle.test.d.ts.map +0 -1
  115. package/dist/__tests__/scriptable-pack-cli.integration.test.d.ts +0 -2
  116. package/dist/__tests__/scriptable-pack-cli.integration.test.d.ts.map +0 -1
  117. package/dist/__tests__/scriptable-pack-ddc-key.unit.test.d.ts +0 -2
  118. package/dist/__tests__/scriptable-pack-ddc-key.unit.test.d.ts.map +0 -1
  119. package/dist/__tests__/topology-actions.unit.test.d.ts +0 -2
  120. package/dist/__tests__/topology-actions.unit.test.d.ts.map +0 -1
  121. package/dist/__tests__/topology.unit.test.d.ts +0 -2
  122. package/dist/__tests__/topology.unit.test.d.ts.map +0 -1
@@ -25,6 +25,28 @@ export interface MaterialCookArtifact {
25
25
  readonly bytes: Uint8Array;
26
26
  }
27
27
 
28
+ /** Domain-owned inputs used to compile and select a material program. */
29
+ export interface MaterialCookProgramContext {
30
+ readonly backend: 'webgpu' | 'webgl2' | 'wgpu-native';
31
+ readonly capability: 'storage-buffer' | 'uniform-fallback';
32
+ readonly pipeline: 'forward' | 'deferred';
33
+ readonly geometry: 'mesh' | 'skinned' | 'sprite';
34
+ readonly pass: 'forward' | 'shadow' | 'depth';
35
+ readonly profile: 'forgeax-material-wgsl-v1';
36
+ readonly toolchain: 'naga-oil';
37
+ readonly instrumentation: 'none' | 'validation';
38
+ }
39
+
40
+ /** Programs are derived artifacts, never independently authored assets. */
41
+ export interface MaterialCookProgram {
42
+ readonly specializationKey: string;
43
+ readonly artifact: MaterialCookArtifact;
44
+ readonly selections: readonly {
45
+ readonly pass: string;
46
+ readonly context: MaterialCookProgramContext;
47
+ }[];
48
+ }
49
+
28
50
  export interface MaterialCookWasmProvenance {
29
51
  readonly sourceContentKey: string;
30
52
  readonly artifactSha256: string;
@@ -51,7 +73,7 @@ export interface MaterialCookIdentity {
51
73
  export type MaterialCookIdentityInput = Omit<MaterialCookIdentity, 'cookIdentity'>;
52
74
 
53
75
  export interface MaterialCookReceipt {
54
- readonly schemaVersion: 'material-cook/3';
76
+ readonly schemaVersion: 'material-cook/4';
55
77
  readonly sourceClosure: readonly string[];
56
78
  readonly profile: string;
57
79
  readonly compilerVersion: string;
@@ -69,12 +91,14 @@ export interface MaterialParameterContract {
69
91
  }
70
92
 
71
93
  export interface CookedMaterialRecord {
72
- readonly schemaVersion: 'material-cook/3';
94
+ readonly schemaVersion: 'material-cook/4';
73
95
  readonly guid: string;
74
96
  readonly authored?: MaterialAsset;
75
97
  readonly materialGuid?: string;
76
98
  readonly publicationGeneration?: number;
77
99
  readonly specializationKey?: string;
100
+ readonly programs: readonly MaterialCookProgram[];
101
+ /** Digest of the complete program/Pass/context manifest. */
78
102
  readonly artifactDigest?: string;
79
103
  readonly sourceClosure?: readonly string[];
80
104
  readonly parameterContract?: MaterialParameterContract;
@@ -84,7 +108,6 @@ export interface CookedMaterialRecord {
84
108
  readonly values: Readonly<Record<string, MaterialValue | null>>;
85
109
  };
86
110
  readonly refs: MaterialCookRefs;
87
- readonly artifact: MaterialCookArtifact;
88
111
  readonly receipt: MaterialCookReceipt;
89
112
  }
90
113
 
@@ -248,7 +271,7 @@ export function serializeMaterialCookReceipt(receipt: MaterialCookReceipt): stri
248
271
  function invalid(field: string, actual?: unknown): Result<never, MaterialCookRecordError> {
249
272
  return err({
250
273
  code: 'material-cook-record-invalid',
251
- expected: 'a complete material-cook/3 record with layered identity and provenance',
274
+ expected: 'a complete material-cook/4 record with layered identity and provenance',
252
275
  hint: 're-cook the material and publish its record, artifact, references, and receipt together',
253
276
  detail: {
254
277
  field,
@@ -258,6 +281,17 @@ function invalid(field: string, actual?: unknown): Result<never, MaterialCookRec
258
281
  });
259
282
  }
260
283
 
284
+ function normalizeArtifactBytes(value: unknown): Uint8Array | undefined {
285
+ if (Array.isArray(value)) {
286
+ return value.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)
287
+ ? Uint8Array.from(value)
288
+ : undefined;
289
+ }
290
+ if (!ArrayBuffer.isView(value)) return undefined;
291
+ const view = value as ArrayBufferView;
292
+ return new Uint8Array(view.buffer, view.byteOffset, view.byteLength).slice();
293
+ }
294
+
261
295
  const IDENTITY_FIELDS = [
262
296
  'materialContractDigest',
263
297
  'sourceRevision',
@@ -308,7 +342,7 @@ export function validateMaterialCookReceipt(
308
342
  ): Result<MaterialCookReceipt, MaterialCookRecordError> {
309
343
  if (value === null || typeof value !== 'object') return invalid('receipt');
310
344
  const candidate = value as Record<string, unknown>;
311
- if (candidate.schemaVersion !== 'material-cook/3')
345
+ if (candidate.schemaVersion !== 'material-cook/4')
312
346
  return invalid('receipt.schemaVersion', candidate.schemaVersion);
313
347
  const identityResult = validateIdentity(candidate.identity);
314
348
  if (!identityResult.ok) return identityResult;
@@ -359,7 +393,7 @@ export function validateMaterialCookReceipt(
359
393
  return invalid('receipt.identity.cookIdentity', identity.cookIdentity);
360
394
  }
361
395
  return ok({
362
- schemaVersion: 'material-cook/3',
396
+ schemaVersion: 'material-cook/4',
363
397
  sourceClosure: candidate.sourceClosure as string[],
364
398
  profile: candidate.profile as string,
365
399
  compilerVersion: candidate.compilerVersion as string,
@@ -378,7 +412,7 @@ export function validateCookedMaterialRecord(
378
412
  ): Result<CookedMaterialRecord, MaterialCookRecordError> {
379
413
  if (value === null || typeof value !== 'object') return invalid('record');
380
414
  const candidate = value as Record<string, unknown>;
381
- if (candidate.schemaVersion !== 'material-cook/3')
415
+ if (candidate.schemaVersion !== 'material-cook/4')
382
416
  return invalid('schemaVersion', candidate.schemaVersion);
383
417
  if (typeof candidate.guid !== 'string' || !candidate.guid) return invalid('guid');
384
418
  if (candidate.materialGuid !== undefined && typeof candidate.materialGuid !== 'string')
@@ -390,6 +424,8 @@ export function validateCookedMaterialRecord(
390
424
  return invalid('publicationGeneration', candidate.publicationGeneration);
391
425
  if (candidate.specializationKey !== undefined && typeof candidate.specializationKey !== 'string')
392
426
  return invalid('specializationKey');
427
+ if ('artifact' in candidate || 'variants' in candidate || 'variantContext' in candidate)
428
+ return invalid('programs', 'legacy single-artifact publication');
393
429
  if (candidate.artifactDigest !== undefined && typeof candidate.artifactDigest !== 'string')
394
430
  return invalid('artifactDigest');
395
431
  if (
@@ -414,21 +450,8 @@ export function validateCookedMaterialRecord(
414
450
  if (candidate.resolved === null || typeof candidate.resolved !== 'object')
415
451
  return invalid('resolved');
416
452
  if (candidate.refs === null || typeof candidate.refs !== 'object') return invalid('refs');
417
- if (candidate.artifact === null || typeof candidate.artifact !== 'object')
418
- return invalid('artifact');
419
453
  if (candidate.receipt === null || typeof candidate.receipt !== 'object')
420
454
  return invalid('receipt');
421
- const artifact = candidate.artifact as Record<string, unknown>;
422
- if (
423
- typeof artifact.digest !== 'string' ||
424
- (!Array.isArray(artifact.bytes) && !ArrayBuffer.isView(artifact.bytes))
425
- ) {
426
- return invalid('artifact');
427
- }
428
- const receiptResult = validateMaterialCookReceipt(candidate.receipt, {
429
- artifactDigest: artifact.digest as string,
430
- });
431
- if (!receiptResult.ok) return receiptResult;
432
455
  const resolved = candidate.resolved as Record<string, unknown>;
433
456
  if (!Array.isArray(resolved.passes)) return invalid('resolved.passes', resolved.passes);
434
457
  if (!Array.isArray(resolved.parameters))
@@ -439,6 +462,84 @@ export function validateCookedMaterialRecord(
439
462
  Array.isArray(resolved.values)
440
463
  )
441
464
  return invalid('resolved.values', resolved.values);
465
+ const passes = resolved.passes as MaterialPass[];
466
+ const passNames = new Set<string>();
467
+ for (const [index, pass] of passes.entries()) {
468
+ if (
469
+ pass === null ||
470
+ typeof pass !== 'object' ||
471
+ typeof pass.name !== 'string' ||
472
+ !pass.name ||
473
+ passNames.has(pass.name) ||
474
+ pass.program === null ||
475
+ typeof pass.program !== 'object' ||
476
+ typeof pass.program.module !== 'string'
477
+ )
478
+ return invalid(`resolved.passes[${index}]`);
479
+ passNames.add(pass.name);
480
+ }
481
+ if (!Array.isArray(candidate.programs) || candidate.programs.length === 0)
482
+ return invalid('programs');
483
+ const programs: MaterialCookProgram[] = [];
484
+ const programKeys = new Set<string>();
485
+ const selections = new Set<string>();
486
+ const selectedPasses = new Set<string>();
487
+ for (const [index, entry] of candidate.programs.entries()) {
488
+ const field = `programs[${index}]`;
489
+ if (entry === null || typeof entry !== 'object') return invalid(field);
490
+ const program = entry as Record<string, unknown>;
491
+ if (
492
+ typeof program.specializationKey !== 'string' ||
493
+ !program.specializationKey ||
494
+ programKeys.has(program.specializationKey)
495
+ )
496
+ return invalid(`${field}.specializationKey`);
497
+ programKeys.add(program.specializationKey);
498
+ if (program.artifact === null || typeof program.artifact !== 'object')
499
+ return invalid(`${field}.artifact`);
500
+ const artifact = program.artifact as Record<string, unknown>;
501
+ const bytes = normalizeArtifactBytes(artifact.bytes);
502
+ if (
503
+ artifact.mediaType !== 'text/wgsl' ||
504
+ typeof artifact.path !== 'string' ||
505
+ !artifact.path ||
506
+ typeof artifact.digest !== 'string' ||
507
+ !artifact.digest ||
508
+ bytes === undefined
509
+ )
510
+ return invalid(`${field}.artifact`);
511
+ if (!Array.isArray(program.selections) || program.selections.length === 0)
512
+ return invalid(`${field}.selections`);
513
+ const programSelections: MaterialCookProgram['selections'][number][] = [];
514
+ for (const [selectionIndex, selection] of program.selections.entries()) {
515
+ const selectionField = `${field}.selections[${selectionIndex}]`;
516
+ if (selection === null || typeof selection !== 'object' || !passNames.has(selection.pass))
517
+ return invalid(selectionField);
518
+ const context = validateMaterialCookProgramContext(selection.context);
519
+ if (!context.ok)
520
+ return invalid(
521
+ `${selectionField}.${context.error.detail.field}`,
522
+ context.error.detail.actual,
523
+ );
524
+ const key = JSON.stringify([selection.pass, materialProgramContextKey(context.value)]);
525
+ if (selections.has(key)) return invalid(selectionField, 'ambiguous Pass/context selection');
526
+ selections.add(key);
527
+ selectedPasses.add(selection.pass);
528
+ programSelections.push({ pass: selection.pass, context: context.value });
529
+ }
530
+ programs.push({
531
+ specializationKey: program.specializationKey,
532
+ artifact: { mediaType: 'text/wgsl', path: artifact.path, digest: artifact.digest, bytes },
533
+ selections: programSelections,
534
+ });
535
+ }
536
+ if ([...passNames].some((pass) => !selectedPasses.has(pass)))
537
+ return invalid('programs.selections', 'unpublished Pass');
538
+ const manifestDigest = createMaterialProgramSetDigest(programs, passes);
539
+ const receiptResult = validateMaterialCookReceipt(candidate.receipt, {
540
+ artifactDigest: manifestDigest,
541
+ });
542
+ if (!receiptResult.ok) return receiptResult;
442
543
  let expectedLayerPlanIdentity: string | undefined;
443
544
  try {
444
545
  expectedLayerPlanIdentity = materialLayerPlanIdentity({
@@ -456,24 +557,18 @@ export function validateCookedMaterialRecord(
456
557
  receiptResult.value.derivedInterface.layerPlanIdentity,
457
558
  );
458
559
  }
459
- if (candidate.artifactDigest !== undefined && candidate.artifactDigest !== artifact.digest)
560
+ if (candidate.artifactDigest !== undefined && candidate.artifactDigest !== manifestDigest)
460
561
  return invalid('artifactDigest', candidate.artifactDigest);
461
562
  if (
462
563
  candidate.publicationGeneration !== undefined &&
463
564
  receiptResult.value.identity.cookGeneration !== candidate.publicationGeneration
464
565
  )
465
566
  return invalid('receipt.identity.cookGeneration', receiptResult.value.identity.cookGeneration);
466
- const normalized = {
567
+ return ok({
467
568
  ...candidate,
468
- artifact: {
469
- ...artifact,
470
- bytes: ArrayBuffer.isView(artifact.bytes)
471
- ? Uint8Array.from(artifact.bytes as Uint8Array)
472
- : Uint8Array.from(artifact.bytes as number[]),
473
- },
569
+ programs,
474
570
  receipt: receiptResult.value,
475
- } as CookedMaterialRecord;
476
- return ok(normalized);
571
+ } as unknown as CookedMaterialRecord);
477
572
  }
478
573
 
479
574
  export function projectCookedMaterialRecord(
@@ -482,8 +577,65 @@ export function projectCookedMaterialRecord(
482
577
  return {
483
578
  resolved: record.resolved,
484
579
  refs: record.refs,
485
- artifact: record.artifact,
580
+ programs: record.programs,
486
581
  receipt: record.receipt,
487
582
  schemaVersion: record.schemaVersion,
488
583
  };
489
584
  }
585
+
586
+ /** Canonical context key shared by publication validation and runtime selection. */
587
+ export function materialProgramContextKey(context: MaterialCookProgramContext): string {
588
+ return JSON.stringify(jsonValue(context));
589
+ }
590
+
591
+ export function validateMaterialCookProgramContext(
592
+ value: unknown,
593
+ ): Result<MaterialCookProgramContext, MaterialCookRecordError> {
594
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
595
+ return invalid('context', value);
596
+ const context = value as Record<string, unknown>;
597
+ const fields = {
598
+ backend: ['webgpu', 'webgl2', 'wgpu-native'],
599
+ capability: ['storage-buffer', 'uniform-fallback'],
600
+ pipeline: ['forward', 'deferred'],
601
+ geometry: ['mesh', 'skinned', 'sprite'],
602
+ pass: ['forward', 'shadow', 'depth'],
603
+ profile: ['forgeax-material-wgsl-v1'],
604
+ toolchain: ['naga-oil'],
605
+ instrumentation: ['none', 'validation'],
606
+ };
607
+ for (const field of Object.keys(context)) {
608
+ if (!(field in fields)) return invalid(`context.${field}`, context[field]);
609
+ }
610
+ for (const [field, allowed] of Object.entries(fields)) {
611
+ if (!allowed.includes(context[field] as string))
612
+ return invalid(`context.${field}`, context[field]);
613
+ }
614
+ return ok({ ...context } as unknown as MaterialCookProgramContext);
615
+ }
616
+
617
+ /** Covers every selection and artifact while excluding transport copies of the bytes. */
618
+ export function createMaterialProgramSetDigest(
619
+ programs: readonly MaterialCookProgram[],
620
+ passes: readonly MaterialPass[],
621
+ ): string {
622
+ const manifest = {
623
+ passes,
624
+ programs: programs
625
+ .map((program) => ({
626
+ specializationKey: program.specializationKey,
627
+ artifact: {
628
+ path: program.artifact.path,
629
+ mediaType: program.artifact.mediaType,
630
+ digest: program.artifact.digest,
631
+ },
632
+ selections: [...program.selections].sort((a, b) =>
633
+ JSON.stringify(jsonValue(a)).localeCompare(JSON.stringify(jsonValue(b))),
634
+ ),
635
+ }))
636
+ .sort((a, b) => a.specializationKey.localeCompare(b.specializationKey)),
637
+ };
638
+ return createMaterialArtifactDigest(
639
+ new TextEncoder().encode(JSON.stringify(jsonValue(manifest))),
640
+ );
641
+ }
package/src/index.ts CHANGED
@@ -7,21 +7,26 @@ export {
7
7
  collectMaterialCookRefs,
8
8
  createMaterialArtifactDigest,
9
9
  createMaterialCookIdentity,
10
+ createMaterialProgramSetDigest,
10
11
  isStandardMaterialRecord,
11
12
  isStandardRootModule,
12
13
  type MaterialCookArtifact,
13
14
  type MaterialCookIdentity,
14
15
  type MaterialCookIdentityExpectation,
15
16
  type MaterialCookIdentityInput,
17
+ type MaterialCookProgram,
18
+ type MaterialCookProgramContext,
16
19
  type MaterialCookReceipt,
17
20
  type MaterialCookRecordError,
18
21
  type MaterialCookRefs,
19
22
  type MaterialCookWasmProvenance,
20
23
  materialLayerPlanIdentity,
24
+ materialProgramContextKey,
21
25
  projectCookedMaterialRecord,
22
26
  serializeCookedMaterialRecord,
23
27
  serializeMaterialCookReceipt,
24
28
  validateCookedMaterialRecord,
29
+ validateMaterialCookProgramContext,
25
30
  validateMaterialCookReceipt,
26
31
  } from './evidence/material-cook.js';
27
32
  export { buildOfflineAssetEvidence, packageVerification } from './evidence/offline-evidence.js';
@@ -3,14 +3,19 @@ export {
3
3
  collectMaterialCookRefs,
4
4
  createMaterialArtifactDigest,
5
5
  createMaterialCookIdentity,
6
+ createMaterialProgramSetDigest,
6
7
  type MaterialCookArtifact,
8
+ type MaterialCookProgram,
9
+ type MaterialCookProgramContext,
7
10
  type MaterialCookReceipt,
8
11
  type MaterialCookRecordError,
9
12
  type MaterialCookRefs,
10
13
  type MaterialCookWasmProvenance,
11
14
  materialLayerPlanIdentity,
15
+ materialProgramContextKey,
12
16
  projectCookedMaterialRecord,
13
17
  serializeCookedMaterialRecord,
14
18
  serializeMaterialCookReceipt,
15
19
  validateCookedMaterialRecord,
20
+ validateMaterialCookProgramContext,
16
21
  } from './evidence/material-cook.js';
package/src/scanner.ts CHANGED
@@ -62,9 +62,13 @@ export interface ScriptablePackScanOptions {
62
62
  }
63
63
 
64
64
  /** Stable default policy shared by inventory and production owners. */
65
- export const STANDARD_SCRIPTABLE_PACK_SCAN_OPTIONS = Object.freeze(
66
- {},
67
- ) satisfies ScriptablePackScanOptions;
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,
71
+ }) satisfies ScriptablePackScanOptions;
68
72
 
69
73
  export interface InventoryDeclaration {
70
74
  readonly guid: string;