@forgeax/engine-pack 0.1.24 → 0.1.25

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 (119) hide show
  1. package/dist/build.d.ts +1 -1
  2. package/dist/build.d.ts.map +1 -1
  3. package/dist/build.mjs +60 -5
  4. package/dist/build.mjs.map +1 -1
  5. package/dist/cli-asset.mjs.map +1 -1
  6. package/dist/evidence/material-cook.d.ts +15 -0
  7. package/dist/evidence/material-cook.d.ts.map +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.mjs +53 -2
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/material-cook.d.ts +1 -1
  13. package/dist/material-cook.d.ts.map +1 -1
  14. package/dist/material-cook.mjs +53 -2
  15. package/dist/material-cook.mjs.map +1 -1
  16. package/dist/pack-authoring-node.mjs.map +1 -1
  17. package/dist/scanner.d.ts +3 -1
  18. package/dist/scanner.d.ts.map +1 -1
  19. package/dist/scanner.mjs +7 -3
  20. package/dist/scanner.mjs.map +1 -1
  21. package/package.json +2 -2
  22. package/src/__tests__/material-declaration-boundary.unit.test.ts +35 -0
  23. package/src/__tests__/material-surface-consumer-census.test.ts +24 -0
  24. package/src/__tests__/material-surface-publication.unit.test.ts +159 -0
  25. package/src/__tests__/material-surface-wire.unit.test.ts +30 -0
  26. package/src/build.ts +2 -0
  27. package/src/evidence/material-cook.ts +91 -4
  28. package/src/index.ts +2 -0
  29. package/src/material-cook.ts +2 -0
  30. package/src/scanner.ts +7 -3
  31. package/src/schema/material-cook.schema.json +43 -0
  32. package/dist/__tests__/ai-recovery.unit.test.d.ts +0 -2
  33. package/dist/__tests__/ai-recovery.unit.test.d.ts.map +0 -1
  34. package/dist/__tests__/artifact-path.test.d.ts +0 -2
  35. package/dist/__tests__/artifact-path.test.d.ts.map +0 -1
  36. package/dist/__tests__/asset-evidence-error-hints.test.d.ts +0 -2
  37. package/dist/__tests__/asset-evidence-error-hints.test.d.ts.map +0 -1
  38. package/dist/__tests__/asset-evidence-lifecycle.test.d.ts +0 -2
  39. package/dist/__tests__/asset-evidence-lifecycle.test.d.ts.map +0 -1
  40. package/dist/__tests__/asset-evidence-schema.test.d.ts +0 -2
  41. package/dist/__tests__/asset-evidence-schema.test.d.ts.map +0 -1
  42. package/dist/__tests__/atlas-cli-region-mismatch.test.d.ts +0 -2
  43. package/dist/__tests__/atlas-cli-region-mismatch.test.d.ts.map +0 -1
  44. package/dist/__tests__/atlas-error-code-owner.test-d.d.ts +0 -2
  45. package/dist/__tests__/atlas-error-code-owner.test-d.d.ts.map +0 -1
  46. package/dist/__tests__/cli-asset-evidence.integration.test.d.ts +0 -2
  47. package/dist/__tests__/cli-asset-evidence.integration.test.d.ts.map +0 -1
  48. package/dist/__tests__/cli.unit.test.d.ts +0 -2
  49. package/dist/__tests__/cli.unit.test.d.ts.map +0 -1
  50. package/dist/__tests__/evidence-source-inventory.unit.test.d.ts +0 -2
  51. package/dist/__tests__/evidence-source-inventory.unit.test.d.ts.map +0 -1
  52. package/dist/__tests__/guid-collision.unit.test.d.ts +0 -2
  53. package/dist/__tests__/guid-collision.unit.test.d.ts.map +0 -1
  54. package/dist/__tests__/guid-validator-surface.unit.test.d.ts +0 -2
  55. package/dist/__tests__/guid-validator-surface.unit.test.d.ts.map +0 -1
  56. package/dist/__tests__/inventory-schema.test.d.ts +0 -2
  57. package/dist/__tests__/inventory-schema.test.d.ts.map +0 -1
  58. package/dist/__tests__/material-cook-dependencies.unit.test.d.ts +0 -2
  59. package/dist/__tests__/material-cook-dependencies.unit.test.d.ts.map +0 -1
  60. package/dist/__tests__/material-cook-receipt-layer-plan.unit.test.d.ts +0 -2
  61. package/dist/__tests__/material-cook-receipt-layer-plan.unit.test.d.ts.map +0 -1
  62. package/dist/__tests__/material-cook-receipt.unit.test.d.ts +0 -2
  63. package/dist/__tests__/material-cook-receipt.unit.test.d.ts.map +0 -1
  64. package/dist/__tests__/material-cook-schema.unit.test.d.ts +0 -2
  65. package/dist/__tests__/material-cook-schema.unit.test.d.ts.map +0 -1
  66. package/dist/__tests__/mesh-bin-consumer-surface.unit.test.d.ts +0 -2
  67. package/dist/__tests__/mesh-bin-consumer-surface.unit.test.d.ts.map +0 -1
  68. package/dist/__tests__/mesh-bin-contract.unit.test.d.ts +0 -2
  69. package/dist/__tests__/mesh-bin-contract.unit.test.d.ts.map +0 -1
  70. package/dist/__tests__/meta-schema-optional.test.d.ts +0 -2
  71. package/dist/__tests__/meta-schema-optional.test.d.ts.map +0 -1
  72. package/dist/__tests__/meta-source-overrides.unit.test.d.ts +0 -2
  73. package/dist/__tests__/meta-source-overrides.unit.test.d.ts.map +0 -1
  74. package/dist/__tests__/native-cooker-registry.test.d.ts +0 -2
  75. package/dist/__tests__/native-cooker-registry.test.d.ts.map +0 -1
  76. package/dist/__tests__/offline-evidence-artifact-verification-owner.test-d.d.ts +0 -2
  77. package/dist/__tests__/offline-evidence-artifact-verification-owner.test-d.d.ts.map +0 -1
  78. package/dist/__tests__/offline-evidence.integration.test.d.ts +0 -2
  79. package/dist/__tests__/offline-evidence.integration.test.d.ts.map +0 -1
  80. package/dist/__tests__/owner-chain.integration.test.d.ts +0 -2
  81. package/dist/__tests__/owner-chain.integration.test.d.ts.map +0 -1
  82. package/dist/__tests__/pack-authoring-gateway.unit.test.d.ts +0 -2
  83. package/dist/__tests__/pack-authoring-gateway.unit.test.d.ts.map +0 -1
  84. package/dist/__tests__/pack-authoring.unit.test.d.ts +0 -2
  85. package/dist/__tests__/pack-authoring.unit.test.d.ts.map +0 -1
  86. package/dist/__tests__/pack-scanner.unit.test.d.ts +0 -2
  87. package/dist/__tests__/pack-scanner.unit.test.d.ts.map +0 -1
  88. package/dist/__tests__/pack-v2-fixture-migration.test.d.ts +0 -2
  89. package/dist/__tests__/pack-v2-fixture-migration.test.d.ts.map +0 -1
  90. package/dist/__tests__/pack-v2-schema.test.d.ts +0 -2
  91. package/dist/__tests__/pack-v2-schema.test.d.ts.map +0 -1
  92. package/dist/__tests__/pack.unit.test.d.ts +0 -2
  93. package/dist/__tests__/pack.unit.test.d.ts.map +0 -1
  94. package/dist/__tests__/package-finalizer.contract.test.d.ts +0 -2
  95. package/dist/__tests__/package-finalizer.contract.test.d.ts.map +0 -1
  96. package/dist/__tests__/producer-contract-schema.unit.test.d.ts +0 -2
  97. package/dist/__tests__/producer-contract-schema.unit.test.d.ts.map +0 -1
  98. package/dist/__tests__/resolve-asset-source.test.d.ts +0 -2
  99. package/dist/__tests__/resolve-asset-source.test.d.ts.map +0 -1
  100. package/dist/__tests__/runtime-projection.unit.test.d.ts +0 -2
  101. package/dist/__tests__/runtime-projection.unit.test.d.ts.map +0 -1
  102. package/dist/__tests__/runtime.browser.test.d.ts +0 -2
  103. package/dist/__tests__/runtime.browser.test.d.ts.map +0 -1
  104. package/dist/__tests__/scanner-blacklist.test.d.ts +0 -2
  105. package/dist/__tests__/scanner-blacklist.test.d.ts.map +0 -1
  106. package/dist/__tests__/scanner-inventory.contract.test.d.ts +0 -2
  107. package/dist/__tests__/scanner-inventory.contract.test.d.ts.map +0 -1
  108. package/dist/__tests__/scanner-inventory.test.d.ts +0 -2
  109. package/dist/__tests__/scanner-inventory.test.d.ts.map +0 -1
  110. package/dist/__tests__/scanner-mount-cycle.test.d.ts +0 -2
  111. package/dist/__tests__/scanner-mount-cycle.test.d.ts.map +0 -1
  112. package/dist/__tests__/scriptable-pack-cli.integration.test.d.ts +0 -2
  113. package/dist/__tests__/scriptable-pack-cli.integration.test.d.ts.map +0 -1
  114. package/dist/__tests__/scriptable-pack-ddc-key.unit.test.d.ts +0 -2
  115. package/dist/__tests__/scriptable-pack-ddc-key.unit.test.d.ts.map +0 -1
  116. package/dist/__tests__/topology-actions.unit.test.d.ts +0 -2
  117. package/dist/__tests__/topology-actions.unit.test.d.ts.map +0 -1
  118. package/dist/__tests__/topology.unit.test.d.ts +0 -2
  119. package/dist/__tests__/topology.unit.test.d.ts.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/engine-pack",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -131,7 +131,7 @@
131
131
  }
132
132
  },
133
133
  "dependencies": {
134
- "@forgeax/engine-types": "0.1.24",
134
+ "@forgeax/engine-types": "0.1.25",
135
135
  "@noble/hashes": "^2.2.0",
136
136
  "fast-glob": "^3.3.3",
137
137
  "typescript": "^5.8.3",
@@ -0,0 +1,35 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { dirname, relative, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import ts from 'typescript';
5
+ import { describe, expect, it } from 'vitest';
6
+
7
+ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../../../');
8
+ const CONFIG_PATH = resolve(REPO_ROOT, 'packages/pack/tsconfig.json');
9
+
10
+ async function declarationProjectInputs(): Promise<readonly string[]> {
11
+ const source = await readFile(CONFIG_PATH, 'utf8');
12
+ const parsed = ts.parseJsonConfigFileContent(
13
+ JSON.parse(source),
14
+ ts.sys,
15
+ dirname(CONFIG_PATH),
16
+ undefined,
17
+ CONFIG_PATH,
18
+ );
19
+ return parsed.fileNames.map((fileName) => relative(REPO_ROOT, fileName));
20
+ }
21
+
22
+ describe('Pack declaration project boundary', () => {
23
+ it('keeps tests and emitted declarations out of production inputs', async () => {
24
+ const inputs = await declarationProjectInputs();
25
+ const config = JSON.parse(await readFile(CONFIG_PATH, 'utf8')) as {
26
+ readonly exclude?: readonly string[];
27
+ };
28
+
29
+ expect(config.exclude).toEqual(expect.arrayContaining(['src/**/__tests__/**', 'dist/**']));
30
+ expect(inputs.some((fileName) => fileName.startsWith('packages/pack/src/__tests__/'))).toBe(
31
+ false,
32
+ );
33
+ expect(inputs.some((fileName) => fileName.startsWith('packages/pack/dist/'))).toBe(false);
34
+ });
35
+ });
@@ -0,0 +1,24 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ const packageRoots = ['packages/pack/src', 'packages/vite-plugin-pack/src', 'packages/render/src'];
6
+
7
+ describe('Surface consumer census', () => {
8
+ it('keeps the runtime and Pack consumers on the existing MaterialAsset wire', async () => {
9
+ const sources = await Promise.all(
10
+ packageRoots.map(async (root) => {
11
+ const path = resolve(process.cwd(), root, 'index.ts');
12
+ try {
13
+ return await readFile(path, 'utf8');
14
+ } catch {
15
+ return '';
16
+ }
17
+ }),
18
+ );
19
+ const source = sources.join('\n');
20
+ expect(source).not.toContain('SurfaceAsset');
21
+ expect(source).not.toContain('surfaceModule');
22
+ expect(source).not.toContain('shadingModel');
23
+ });
24
+ });
@@ -0,0 +1,159 @@
1
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+ import { Materials } from '@forgeax/engine-render';
6
+ import { describe, expect, it } from 'vitest';
7
+ import { NativeCookerRegistry } from '../native-cooker-registry.js';
8
+
9
+ const compilerModule = pathToFileURL(
10
+ resolve(
11
+ dirname(fileURLToPath(import.meta.url)),
12
+ '../../../../packages/shader-compiler/dist/index.mjs',
13
+ ),
14
+ ).href;
15
+
16
+ const GUID = '11111111-1111-4111-8111-111111111111';
17
+ const ROOT_GUID = '22222222-2222-4222-8222-222222222222';
18
+ const CHILD_GUID = '33333333-3333-4333-8333-333333333333';
19
+ const SURFACE = `#define_import_path game_3d::rusted_iron_surface
20
+ #import forgeax_material::surface_v1::{SurfaceData, SurfaceInput}
21
+ fn evaluate_surface(input: SurfaceInput) -> SurfaceData {
22
+ return SurfaceData(vec3<f32>(0.4, 0.45, 0.47), input.geometricNormalWS, 0.0, 0.5, vec3<f32>(0.0), 1.0, 1.0, 0.0);
23
+ }`;
24
+
25
+ describe('Surface Pack publication', () => {
26
+ it('publishes one complete cooked record with source closure and artifact', async () => {
27
+ const root = await mkdtemp(join(tmpdir(), 'forgeax-surface-publication-'));
28
+ try {
29
+ const sourcePath = join(root, 'rusted-iron-surface.wgsl');
30
+ await writeFile(sourcePath, SURFACE);
31
+ const material = Materials.standard({
32
+ surfaceModule: 'game_3d::rusted_iron_surface',
33
+ parameters: [],
34
+ values: {},
35
+ });
36
+ const { createMaterialPackCooker } = await import(compilerModule);
37
+ const draft = await createMaterialPackCooker([root]).cook({
38
+ guid: GUID,
39
+ source: material,
40
+ sourcePath,
41
+ });
42
+ const cooked = draft.payload.cooked as {
43
+ readonly receipt: { readonly sourceClosure: readonly string[] };
44
+ readonly artifact: { readonly path: string; readonly bytes: Uint8Array };
45
+ };
46
+ expect(cooked.receipt.sourceClosure).toContain(sourcePath);
47
+ expect(cooked.artifact.path).toContain(GUID);
48
+ const artifact = draft.artifacts[cooked.artifact.path];
49
+ expect(artifact?.bytes.byteLength).toBeGreaterThan(0);
50
+ expect(Object.keys(draft.artifacts)).toContain(cooked.artifact.path);
51
+ } finally {
52
+ await rm(root, { recursive: true, force: true });
53
+ }
54
+ });
55
+
56
+ it('keeps the accepted generation as last known good after cook failure', async () => {
57
+ const registry = new NativeCookerRegistry();
58
+ registry.register({
59
+ key: 'material',
60
+ cook: async (input: { readonly fail?: boolean }) => {
61
+ if (input.fail) throw new Error('surface compose failed');
62
+ return {
63
+ guid: GUID,
64
+ payload: { generation: 1 },
65
+ refs: [],
66
+ artifacts: {},
67
+ inputFingerprint: 'sha256:surface-input',
68
+ };
69
+ },
70
+ });
71
+ const first = await registry.runTransaction({ key: 'material', input: {} });
72
+ expect(first).toMatchObject({ ok: true, value: { generation: 1, status: 'committed' } });
73
+ if (!first.ok) return;
74
+ const failed = await registry.runTransaction({
75
+ key: 'material',
76
+ input: { fail: true },
77
+ previous: { draft: first.value.draft, generation: first.value.generation },
78
+ });
79
+ expect(failed).toMatchObject({
80
+ ok: true,
81
+ value: {
82
+ status: 'recovered',
83
+ lastKnownGood: { payload: { generation: 1 } },
84
+ lastKnownGoodGeneration: 1,
85
+ },
86
+ });
87
+ });
88
+
89
+ it('cold-cooks a child through the Pack-owned root table and publishes its parent ref', async () => {
90
+ const root = await mkdtemp(join(tmpdir(), 'forgeax-surface-root-child-'));
91
+ try {
92
+ const rootMaterial = Materials.standard({
93
+ baseColor: [1, 1, 1, 1],
94
+ metallic: 0,
95
+ roughness: 0.5,
96
+ });
97
+ const child = {
98
+ kind: 'material' as const,
99
+ parent: ROOT_GUID as never,
100
+ values: { baseColor: [0.4, 0.45, 0.47, 1] },
101
+ };
102
+ const { createMaterialPackCooker } = await import(compilerModule);
103
+ const draft = await createMaterialPackCooker([root]).cook({
104
+ guid: CHILD_GUID,
105
+ source: child,
106
+ table: { [ROOT_GUID]: rootMaterial, [CHILD_GUID]: child },
107
+ });
108
+ const cooked = draft.payload.cooked as {
109
+ readonly authored: { readonly parent?: string };
110
+ readonly resolved: {
111
+ readonly parameters: readonly { readonly name: string }[];
112
+ readonly values: Readonly<Record<string, unknown>>;
113
+ };
114
+ readonly refs: { readonly parent: readonly string[] };
115
+ };
116
+ expect(cooked.authored.parent).toBe(ROOT_GUID);
117
+ expect(cooked.refs.parent).toEqual([ROOT_GUID]);
118
+ expect(cooked.resolved.parameters.map((parameter) => parameter.name)).toContain('baseColor');
119
+ expect(cooked.resolved.values.baseColor).toEqual([0.4, 0.45, 0.47, 1]);
120
+ expect(draft.refs).toContain(ROOT_GUID);
121
+ } finally {
122
+ await rm(root, { recursive: true, force: true });
123
+ }
124
+ });
125
+
126
+ it('does not publish an artifact when a physical material requests Deferred', async () => {
127
+ const { createMaterialPackCooker } = await import(compilerModule);
128
+ const material = {
129
+ kind: 'material' as const,
130
+ passes: [
131
+ {
132
+ name: 'forward',
133
+ program: {
134
+ module: 'forgeax_material::standard',
135
+ moduleSlots: { surface: 'forgeax_material::default_standard_surface' },
136
+ },
137
+ },
138
+ {
139
+ name: 'deferred',
140
+ program: {
141
+ module: 'forgeax_material::standard',
142
+ moduleSlots: { surface: 'forgeax_material::default_standard_surface' },
143
+ },
144
+ },
145
+ ],
146
+ parameters: [
147
+ { name: 'clearcoat', type: 'f32' as const },
148
+ { name: 'clearcoatRoughness', type: 'f32' as const },
149
+ ],
150
+ values: { clearcoat: 0, clearcoatRoughness: 0.5 },
151
+ };
152
+ await expect(
153
+ createMaterialPackCooker().cook({
154
+ guid: GUID,
155
+ source: material,
156
+ }),
157
+ ).rejects.toThrow('material-physical-contract-invalid');
158
+ });
159
+ });
@@ -0,0 +1,30 @@
1
+ import { Materials } from '@forgeax/engine-render';
2
+ import { assertMaterialAsset } from '@forgeax/engine-types';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ const customOptions = {
6
+ surfaceModule: 'game_3d::rusted_iron_surface',
7
+ parameters: [{ name: 'ironColor', type: 'color' as const }],
8
+ values: { ironColor: [0.4, 0.45, 0.47, 1] },
9
+ };
10
+
11
+ describe('custom Surface MaterialAsset wire projection', () => {
12
+ it('uses Standard module and the existing Surface slot wire', () => {
13
+ const material = Materials.standard(customOptions as never);
14
+ expect(material.passes?.map((pass) => pass.program.module)).toEqual([
15
+ 'forgeax_material::standard',
16
+ 'forgeax_material::standard',
17
+ 'forgeax_material::standard',
18
+ ]);
19
+ expect(
20
+ material.passes?.every(
21
+ (pass) => pass.program.moduleSlots?.surface === customOptions.surfaceModule,
22
+ ),
23
+ ).toBe(true);
24
+ const wire = JSON.parse(JSON.stringify(material)) as Record<string, unknown>;
25
+ assertMaterialAsset(wire);
26
+ expect(wire).not.toHaveProperty('surfaceModule');
27
+ expect(wire).not.toHaveProperty('shadingModel');
28
+ expect(wire).not.toHaveProperty('surface');
29
+ });
30
+ });
package/src/build.ts CHANGED
@@ -60,6 +60,8 @@ export {
60
60
  type MaterialCookReceipt,
61
61
  type MaterialCookRecordError,
62
62
  type MaterialCookRefs,
63
+ type MaterialCookVariant,
64
+ type MaterialCookVariantContext,
63
65
  projectCookedMaterialRecord,
64
66
  serializeCookedMaterialRecord,
65
67
  serializeMaterialCookReceipt,
@@ -25,6 +25,19 @@ export interface MaterialCookArtifact {
25
25
  readonly bytes: Uint8Array;
26
26
  }
27
27
 
28
+ /** Backend capability projection for one immutable cooked material artifact. */
29
+ export interface MaterialCookVariantContext {
30
+ readonly backend: 'webgpu' | 'webgl2' | 'wgpu-native';
31
+ readonly capability: 'storage-buffer' | 'uniform-fallback';
32
+ }
33
+
34
+ /** Additional backend artifact published with one material generation. */
35
+ export interface MaterialCookVariant {
36
+ readonly context: MaterialCookVariantContext;
37
+ readonly specializationKey: string;
38
+ readonly artifact: MaterialCookArtifact;
39
+ }
40
+
28
41
  export interface MaterialCookWasmProvenance {
29
42
  readonly sourceContentKey: string;
30
43
  readonly artifactSha256: string;
@@ -75,6 +88,10 @@ export interface CookedMaterialRecord {
75
88
  readonly materialGuid?: string;
76
89
  readonly publicationGeneration?: number;
77
90
  readonly specializationKey?: string;
91
+ /** Compiler context used by the primary `artifact` entry. */
92
+ readonly variantContext?: MaterialCookVariantContext;
93
+ /** Additional immutable artifacts for other backend capability lanes. */
94
+ readonly variants?: readonly MaterialCookVariant[];
78
95
  readonly artifactDigest?: string;
79
96
  readonly sourceClosure?: readonly string[];
80
97
  readonly parameterContract?: MaterialParameterContract;
@@ -110,6 +127,7 @@ const STANDARD_ROOT_MODULES = new Set([
110
127
  'forgeax::pbr-skin',
111
128
  'forgeax_material::standard',
112
129
  'forgeax_material::pbr-skin',
130
+ 'forgeax::default-shadow-caster',
113
131
  ]);
114
132
 
115
133
  /** Shared ownership predicate for the built-in Standard material root. */
@@ -258,6 +276,17 @@ function invalid(field: string, actual?: unknown): Result<never, MaterialCookRec
258
276
  });
259
277
  }
260
278
 
279
+ function normalizeArtifactBytes(value: unknown): Uint8Array | undefined {
280
+ if (Array.isArray(value)) {
281
+ return value.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)
282
+ ? Uint8Array.from(value)
283
+ : undefined;
284
+ }
285
+ if (!ArrayBuffer.isView(value)) return undefined;
286
+ const view = value as ArrayBufferView;
287
+ return new Uint8Array(view.buffer, view.byteOffset, view.byteLength).slice();
288
+ }
289
+
261
290
  const IDENTITY_FIELDS = [
262
291
  'materialContractDigest',
263
292
  'sourceRevision',
@@ -390,6 +419,49 @@ export function validateCookedMaterialRecord(
390
419
  return invalid('publicationGeneration', candidate.publicationGeneration);
391
420
  if (candidate.specializationKey !== undefined && typeof candidate.specializationKey !== 'string')
392
421
  return invalid('specializationKey');
422
+ if (candidate.variantContext !== undefined) {
423
+ if (candidate.variantContext === null || typeof candidate.variantContext !== 'object')
424
+ return invalid('variantContext');
425
+ const context = candidate.variantContext as Record<string, unknown>;
426
+ if (
427
+ (context.backend !== 'webgpu' &&
428
+ context.backend !== 'webgl2' &&
429
+ context.backend !== 'wgpu-native') ||
430
+ (context.capability !== 'storage-buffer' && context.capability !== 'uniform-fallback')
431
+ )
432
+ return invalid('variantContext', candidate.variantContext);
433
+ }
434
+ if (candidate.variants !== undefined) {
435
+ if (!Array.isArray(candidate.variants)) return invalid('variants');
436
+ for (const [index, value] of candidate.variants.entries()) {
437
+ if (value === null || typeof value !== 'object') return invalid(`variants[${index}]`);
438
+ const variant = value as Record<string, unknown>;
439
+ const context = variant.context;
440
+ if (context === null || typeof context !== 'object')
441
+ return invalid(`variants[${index}].context`);
442
+ const contextRecord = context as Record<string, unknown>;
443
+ if (
444
+ (contextRecord.backend !== 'webgpu' &&
445
+ contextRecord.backend !== 'webgl2' &&
446
+ contextRecord.backend !== 'wgpu-native') ||
447
+ (contextRecord.capability !== 'storage-buffer' &&
448
+ contextRecord.capability !== 'uniform-fallback')
449
+ )
450
+ return invalid(`variants[${index}].context`, context);
451
+ if (typeof variant.specializationKey !== 'string' || variant.specializationKey.length === 0)
452
+ return invalid(`variants[${index}].specializationKey`);
453
+ if (variant.artifact === null || typeof variant.artifact !== 'object')
454
+ return invalid(`variants[${index}].artifact`);
455
+ const variantArtifact = variant.artifact as Record<string, unknown>;
456
+ if (
457
+ typeof variantArtifact.mediaType !== 'string' ||
458
+ typeof variantArtifact.path !== 'string' ||
459
+ typeof variantArtifact.digest !== 'string' ||
460
+ normalizeArtifactBytes(variantArtifact.bytes) === undefined
461
+ )
462
+ return invalid(`variants[${index}].artifact`);
463
+ }
464
+ }
393
465
  if (candidate.artifactDigest !== undefined && typeof candidate.artifactDigest !== 'string')
394
466
  return invalid('artifactDigest');
395
467
  if (
@@ -467,12 +539,25 @@ export function validateCookedMaterialRecord(
467
539
  ...candidate,
468
540
  artifact: {
469
541
  ...artifact,
470
- bytes: ArrayBuffer.isView(artifact.bytes)
471
- ? Uint8Array.from(artifact.bytes as Uint8Array)
472
- : Uint8Array.from(artifact.bytes as number[]),
542
+ bytes: normalizeArtifactBytes(artifact.bytes) as Uint8Array,
473
543
  },
544
+ ...(Array.isArray(candidate.variants)
545
+ ? {
546
+ variants: candidate.variants.map((value) => {
547
+ const variant = value as Record<string, unknown>;
548
+ const variantArtifact = variant.artifact as Record<string, unknown>;
549
+ return {
550
+ ...variant,
551
+ artifact: {
552
+ ...variantArtifact,
553
+ bytes: normalizeArtifactBytes(variantArtifact.bytes) as Uint8Array,
554
+ },
555
+ };
556
+ }),
557
+ }
558
+ : {}),
474
559
  receipt: receiptResult.value,
475
- } as CookedMaterialRecord;
560
+ } as unknown as CookedMaterialRecord;
476
561
  return ok(normalized);
477
562
  }
478
563
 
@@ -483,6 +568,8 @@ export function projectCookedMaterialRecord(
483
568
  resolved: record.resolved,
484
569
  refs: record.refs,
485
570
  artifact: record.artifact,
571
+ ...(record.variantContext === undefined ? {} : { variantContext: record.variantContext }),
572
+ ...(record.variants === undefined ? {} : { variants: record.variants }),
486
573
  receipt: record.receipt,
487
574
  schemaVersion: record.schemaVersion,
488
575
  };
package/src/index.ts CHANGED
@@ -16,6 +16,8 @@ export {
16
16
  type MaterialCookReceipt,
17
17
  type MaterialCookRecordError,
18
18
  type MaterialCookRefs,
19
+ type MaterialCookVariant,
20
+ type MaterialCookVariantContext,
19
21
  type MaterialCookWasmProvenance,
20
22
  materialLayerPlanIdentity,
21
23
  projectCookedMaterialRecord,
@@ -7,6 +7,8 @@ export {
7
7
  type MaterialCookReceipt,
8
8
  type MaterialCookRecordError,
9
9
  type MaterialCookRefs,
10
+ type MaterialCookVariant,
11
+ type MaterialCookVariantContext,
10
12
  type MaterialCookWasmProvenance,
11
13
  materialLayerPlanIdentity,
12
14
  projectCookedMaterialRecord,
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;
@@ -9,6 +9,49 @@
9
9
  "materialGuid": { "type": "string", "minLength": 1 },
10
10
  "publicationGeneration": { "type": "integer", "minimum": 1 },
11
11
  "specializationKey": { "type": "string", "minLength": 1 },
12
+ "variantContext": {
13
+ "type": "object",
14
+ "required": ["backend", "capability"],
15
+ "properties": {
16
+ "backend": { "enum": ["webgpu", "webgl2", "wgpu-native"] },
17
+ "capability": { "enum": ["storage-buffer", "uniform-fallback"] }
18
+ },
19
+ "additionalProperties": false
20
+ },
21
+ "variants": {
22
+ "type": "array",
23
+ "items": {
24
+ "type": "object",
25
+ "required": ["context", "specializationKey", "artifact"],
26
+ "properties": {
27
+ "context": {
28
+ "type": "object",
29
+ "required": ["backend", "capability"],
30
+ "properties": {
31
+ "backend": { "enum": ["webgpu", "webgl2", "wgpu-native"] },
32
+ "capability": { "enum": ["storage-buffer", "uniform-fallback"] }
33
+ },
34
+ "additionalProperties": false
35
+ },
36
+ "specializationKey": { "type": "string", "minLength": 1 },
37
+ "artifact": {
38
+ "type": "object",
39
+ "required": ["mediaType", "path", "digest", "bytes"],
40
+ "properties": {
41
+ "mediaType": { "type": "string" },
42
+ "path": { "type": "string" },
43
+ "digest": { "type": "string", "pattern": "^sha256:" },
44
+ "bytes": {
45
+ "type": "array",
46
+ "items": { "type": "integer", "minimum": 0, "maximum": 255 }
47
+ }
48
+ },
49
+ "additionalProperties": false
50
+ }
51
+ },
52
+ "additionalProperties": false
53
+ }
54
+ },
12
55
  "artifactDigest": { "type": "string", "pattern": "^sha256:" },
13
56
  "sourceClosure": { "type": "array", "items": { "type": "string" } },
14
57
  "parameterContract": {
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=ai-recovery.unit.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ai-recovery.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/ai-recovery.unit.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=artifact-path.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"artifact-path.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/artifact-path.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=asset-evidence-error-hints.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"asset-evidence-error-hints.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/asset-evidence-error-hints.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=asset-evidence-lifecycle.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"asset-evidence-lifecycle.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/asset-evidence-lifecycle.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=asset-evidence-schema.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"asset-evidence-schema.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/asset-evidence-schema.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=atlas-cli-region-mismatch.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"atlas-cli-region-mismatch.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/atlas-cli-region-mismatch.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=atlas-error-code-owner.test-d.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"atlas-error-code-owner.test-d.d.ts","sourceRoot":"","sources":["../../src/__tests__/atlas-error-code-owner.test-d.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=cli-asset-evidence.integration.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cli-asset-evidence.integration.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/cli-asset-evidence.integration.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=cli.unit.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cli.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/cli.unit.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=evidence-source-inventory.unit.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"evidence-source-inventory.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/evidence-source-inventory.unit.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=guid-collision.unit.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"guid-collision.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/guid-collision.unit.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=guid-validator-surface.unit.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"guid-validator-surface.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/guid-validator-surface.unit.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=inventory-schema.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"inventory-schema.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/inventory-schema.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=material-cook-dependencies.unit.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"material-cook-dependencies.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/material-cook-dependencies.unit.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=material-cook-receipt-layer-plan.unit.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"material-cook-receipt-layer-plan.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/material-cook-receipt-layer-plan.unit.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=material-cook-receipt.unit.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"material-cook-receipt.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/material-cook-receipt.unit.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=material-cook-schema.unit.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"material-cook-schema.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/material-cook-schema.unit.test.ts"],"names":[],"mappings":""}