@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
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.26",
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.26",
135
135
  "@noble/hashes": "^2.2.0",
136
136
  "fast-glob": "^3.3.3",
137
137
  "typescript": "^5.8.3",
@@ -10,7 +10,7 @@ const artifactDigest = 'sha256:output';
10
10
 
11
11
  function receipt(overrides: Record<string, unknown> = {}): MaterialCookReceipt {
12
12
  return {
13
- schemaVersion: 'material-cook/3',
13
+ schemaVersion: 'material-cook/4',
14
14
  sourceClosure: ['b.wgsl', 'a.material.json'],
15
15
  profile: 'webgpu/v1',
16
16
  compilerVersion: 'compiler/1',
@@ -40,9 +40,9 @@ function receipt(overrides: Record<string, unknown> = {}): MaterialCookReceipt {
40
40
  }
41
41
 
42
42
  describe('material cook receipt', () => {
43
- it('requires the v3 identity and provenance tuple', () => {
44
- const v3 = {
45
- schemaVersion: 'material-cook/3',
43
+ it('requires the v4 identity and provenance tuple', () => {
44
+ const v4 = {
45
+ schemaVersion: 'material-cook/4',
46
46
  sourceClosure: ['materials/mat.material.json', 'shaders/pbr.wgsl'],
47
47
  profile: 'webgpu/v1',
48
48
  compilerVersion: 'compiler/1',
@@ -68,12 +68,12 @@ describe('material cook receipt', () => {
68
68
  },
69
69
  derivedInterface: { layoutIdentity },
70
70
  };
71
- expect(validateMaterialCookReceipt(v3)).toMatchObject({ ok: true, value: v3 });
71
+ expect(validateMaterialCookReceipt(v4)).toMatchObject({ ok: true, value: v4 });
72
72
  expect(validateMaterialCookReceipt(receipt())).toMatchObject({ ok: true });
73
73
  expect(
74
74
  validateMaterialCookReceipt({
75
- ...v3,
76
- identity: { ...v3.identity, compilerFingerprint: undefined },
75
+ ...v4,
76
+ identity: { ...v4.identity, compilerFingerprint: undefined },
77
77
  }),
78
78
  ).toMatchObject({
79
79
  ok: false,
@@ -81,14 +81,14 @@ describe('material cook receipt', () => {
81
81
  });
82
82
  });
83
83
 
84
- it('serializes a material-cook/3 receipt stably for equivalent cooks', () => {
84
+ it('serializes a material-cook/4 receipt stably for equivalent cooks', () => {
85
85
  const first = serializeMaterialCookReceipt(receipt());
86
86
  const second = serializeMaterialCookReceipt(
87
87
  receipt({ sourceClosure: ['a.material.json', 'b.wgsl'] }),
88
88
  );
89
89
  expect(first).toBe(second);
90
90
  expect(JSON.parse(first)).toMatchObject({
91
- schemaVersion: 'material-cook/3',
91
+ schemaVersion: 'material-cook/4',
92
92
  identity: { layoutIdentity },
93
93
  derivedInterface: { layoutIdentity },
94
94
  });
@@ -5,13 +5,15 @@ import { describe, expect, it } from 'vitest';
5
5
  import {
6
6
  type CookedMaterialRecord,
7
7
  createMaterialArtifactDigest,
8
+ createMaterialProgramSetDigest,
9
+ type MaterialCookProgram,
8
10
  projectCookedMaterialRecord,
9
11
  serializeCookedMaterialRecord,
10
12
  validateCookedMaterialRecord,
11
13
  } from '../evidence/material-cook.js';
12
14
 
13
15
  const record: CookedMaterialRecord = {
14
- schemaVersion: 'material-cook/3',
16
+ schemaVersion: 'material-cook/4',
15
17
  guid: 'mat-root',
16
18
  authored: {
17
19
  kind: 'material',
@@ -25,14 +27,34 @@ const record: CookedMaterialRecord = {
25
27
  values: { roughness: 0.5 },
26
28
  },
27
29
  refs: { parent: [], textures: [], samplers: [], modules: ['core/pbr'] },
28
- artifact: {
29
- mediaType: 'text/wgsl',
30
- path: 'materials/mat-root/forward.wgsl',
31
- digest: 'sha256:artifact',
32
- bytes: new TextEncoder().encode('shader'),
33
- },
30
+ programs: [
31
+ {
32
+ specializationKey: 'sha256:program',
33
+ selections: [
34
+ {
35
+ pass: 'forward',
36
+ context: {
37
+ backend: 'webgpu',
38
+ capability: 'storage-buffer',
39
+ pipeline: 'forward',
40
+ geometry: 'mesh',
41
+ pass: 'forward',
42
+ profile: 'forgeax-material-wgsl-v1',
43
+ toolchain: 'naga-oil',
44
+ instrumentation: 'none',
45
+ },
46
+ },
47
+ ],
48
+ artifact: {
49
+ mediaType: 'text/wgsl',
50
+ path: 'materials/mat-root/forward.wgsl',
51
+ digest: 'sha256:artifact',
52
+ bytes: new TextEncoder().encode('shader'),
53
+ },
54
+ },
55
+ ],
34
56
  receipt: {
35
- schemaVersion: 'material-cook/3',
57
+ schemaVersion: 'material-cook/4',
36
58
  sourceClosure: ['materials/mat-root.material.json'],
37
59
  profile: 'webgpu/v1',
38
60
  compilerVersion: 'compiler/1',
@@ -60,8 +82,21 @@ const record: CookedMaterialRecord = {
60
82
  },
61
83
  };
62
84
 
85
+ function seal(value: CookedMaterialRecord): CookedMaterialRecord {
86
+ return {
87
+ ...value,
88
+ receipt: {
89
+ ...value.receipt,
90
+ identity: {
91
+ ...value.receipt.identity,
92
+ artifactDigest: createMaterialProgramSetDigest(value.programs, value.resolved.passes),
93
+ },
94
+ },
95
+ };
96
+ }
97
+
63
98
  describe('cooked material schema', () => {
64
- it('requires material-cook/3 identity and derived interface association', () => {
99
+ it('requires material-cook/4 identity and derived interface association', () => {
65
100
  const schema = JSON.parse(
66
101
  readFileSync(
67
102
  fileURLToPath(new URL('../schema/material-cook.schema.json', import.meta.url)),
@@ -74,14 +109,14 @@ describe('cooked material schema', () => {
74
109
  >;
75
110
  required: string[];
76
111
  };
77
- expect(schema.properties.schemaVersion?.const).toBe('material-cook/3');
112
+ expect(schema.properties.schemaVersion?.const).toBe('material-cook/4');
78
113
  expect(schema.properties.receipt?.required).toEqual(
79
114
  expect.arrayContaining(['identity', 'derivedInterface']),
80
115
  );
81
116
  expect(schema.properties.receipt?.properties?.derivedInterface).toBeDefined();
82
117
  });
83
118
 
84
- it('defines the v3 identity, provenance, and generation contract', () => {
119
+ it('defines the v4 identity, provenance, and generation contract', () => {
85
120
  const schema = JSON.parse(
86
121
  readFileSync(
87
122
  fileURLToPath(new URL('../schema/material-cook.schema.json', import.meta.url)),
@@ -93,7 +128,7 @@ describe('cooked material schema', () => {
93
128
  { const?: string; required?: string[]; properties?: Record<string, unknown> }
94
129
  >;
95
130
  };
96
- expect(schema.properties.schemaVersion?.const).toBe('material-cook/3');
131
+ expect(schema.properties.schemaVersion?.const).toBe('material-cook/4');
97
132
  expect(schema.properties.receipt?.properties?.identity).toMatchObject({
98
133
  type: 'object',
99
134
  required: expect.arrayContaining([
@@ -119,25 +154,141 @@ describe('cooked material schema', () => {
119
154
  });
120
155
 
121
156
  it('serializes a stable cooked DTO and derives an artifact digest', () => {
122
- const first = serializeCookedMaterialRecord(record);
123
- const second = serializeCookedMaterialRecord({ ...record });
157
+ const first = serializeCookedMaterialRecord(seal(record));
158
+ const second = serializeCookedMaterialRecord(seal({ ...record }));
124
159
 
125
160
  expect(first).toBe(second);
126
- expect(createMaterialArtifactDigest(record.artifact.bytes)).toBe(
161
+ expect(createMaterialArtifactDigest(new TextEncoder().encode('shader'))).toBe(
127
162
  'sha256:e137e75a5e0e7a623ca39de480667a55b43e0eedec58767634ebaef07c33383a',
128
163
  );
129
- expect(validateCookedMaterialRecord(JSON.parse(first))).toEqual({ ok: true, value: record });
164
+ expect(validateCookedMaterialRecord(JSON.parse(first))).toEqual({
165
+ ok: true,
166
+ value: seal(record),
167
+ });
130
168
  });
131
169
 
132
170
  it('projects cooked data without exposing an authored GUID entry', () => {
133
171
  const projection = projectCookedMaterialRecord(record);
134
172
 
135
173
  expect(projection.resolved.values).toEqual({ roughness: 0.5 });
136
- expect(projection.artifact.digest).toBe('sha256:artifact');
174
+ expect(projection.programs[0]?.artifact.digest).toBe('sha256:artifact');
137
175
  expect(projection).not.toHaveProperty('guid');
138
176
  expect(projection).not.toHaveProperty('authored');
139
177
  });
140
178
 
179
+ it('admits different programs with equal entry names and shared programs with multiple selections', () => {
180
+ const primary = record.programs[0] as MaterialCookProgram;
181
+ const passes = [
182
+ ...record.resolved.passes,
183
+ {
184
+ name: 'second',
185
+ program: { module: 'other', vertexEntry: 'vs_main', fragmentEntry: 'fs_main' },
186
+ },
187
+ ];
188
+ const selection = {
189
+ ...primary.selections[0],
190
+ pass: 'second',
191
+ } as MaterialCookProgram['selections'][number];
192
+ const distinct = seal({
193
+ ...record,
194
+ resolved: { ...record.resolved, passes },
195
+ programs: [
196
+ primary,
197
+ {
198
+ ...primary,
199
+ specializationKey: 'second',
200
+ artifact: {
201
+ ...primary.artifact,
202
+ path: 'second.wgsl',
203
+ digest: 'sha256:second',
204
+ bytes: new TextEncoder().encode('other'),
205
+ },
206
+ selections: [selection],
207
+ },
208
+ ],
209
+ });
210
+ expect(validateCookedMaterialRecord(distinct)).toMatchObject({ ok: true });
211
+ const shared = seal({
212
+ ...distinct,
213
+ programs: [{ ...primary, selections: [...primary.selections, selection] }],
214
+ });
215
+ expect(validateCookedMaterialRecord(shared)).toMatchObject({ ok: true });
216
+ });
217
+
218
+ it('rejects ambiguous selections, unpublished passes and unknown context axes', () => {
219
+ const primary = record.programs[0] as MaterialCookProgram;
220
+ expect(
221
+ validateCookedMaterialRecord({
222
+ ...record,
223
+ programs: [primary, { ...primary, specializationKey: 'duplicate' }],
224
+ }),
225
+ ).toMatchObject({
226
+ ok: false,
227
+ error: { detail: { actual: 'ambiguous Pass/context selection' } },
228
+ });
229
+ expect(
230
+ validateCookedMaterialRecord({
231
+ ...record,
232
+ resolved: {
233
+ ...record.resolved,
234
+ passes: [...record.resolved.passes, { name: 'missing', program: { module: 'other' } }],
235
+ },
236
+ }),
237
+ ).toMatchObject({ ok: false, error: { detail: { actual: 'unpublished Pass' } } });
238
+ expect(
239
+ validateCookedMaterialRecord({
240
+ ...record,
241
+ programs: [
242
+ {
243
+ ...primary,
244
+ selections: [
245
+ { pass: 'forward', context: { ...primary.selections[0]?.context, arbitrary: true } },
246
+ ],
247
+ },
248
+ ],
249
+ }),
250
+ ).toMatchObject({
251
+ ok: false,
252
+ error: { detail: { field: 'programs[0].selections[0].context.arbitrary' } },
253
+ });
254
+ });
255
+
256
+ it('rejects stale manifests after entry or context changes and rejects legacy publication fields', () => {
257
+ const sealed = seal(record);
258
+ expect(
259
+ validateCookedMaterialRecord({
260
+ ...sealed,
261
+ resolved: {
262
+ ...sealed.resolved,
263
+ passes: [{ name: 'forward', program: { module: 'core/pbr', fragmentEntry: 'fs_other' } }],
264
+ },
265
+ }),
266
+ ).toMatchObject({ ok: false, error: { detail: { field: 'receipt.identity.artifactDigest' } } });
267
+ const primary = record.programs[0] as MaterialCookProgram;
268
+ expect(
269
+ validateCookedMaterialRecord({
270
+ ...sealed,
271
+ programs: [
272
+ {
273
+ ...primary,
274
+ selections: [
275
+ {
276
+ pass: 'forward',
277
+ context: { ...primary.selections[0]?.context, geometry: 'skinned' },
278
+ },
279
+ ],
280
+ },
281
+ ],
282
+ }),
283
+ ).toMatchObject({ ok: false, error: { detail: { field: 'receipt.identity.artifactDigest' } } });
284
+ expect(
285
+ validateCookedMaterialRecord({ ...sealed, schemaVersion: 'material-cook/3' }),
286
+ ).toMatchObject({ ok: false });
287
+ expect(validateCookedMaterialRecord({ ...sealed, artifact: primary.artifact })).toMatchObject({
288
+ ok: false,
289
+ });
290
+ });
291
+
141
292
  it('rejects missing fields and unsupported schema versions', () => {
142
293
  expect(validateCookedMaterialRecord({ ...record, refs: undefined })).toMatchObject({
143
294
  ok: false,
@@ -154,7 +305,7 @@ describe('cooked material schema', () => {
154
305
  it('rejects a Standard record whose receipt layer plan is stale', () => {
155
306
  const standardPass = { name: 'forward', program: { module: 'forgeax::default-standard-pbr' } };
156
307
  const standardPlan = deriveStandardLayerPlan(record.resolved.parameters, [standardPass]);
157
- const standardRecord = {
308
+ const standardRecord = seal({
158
309
  ...record,
159
310
  resolved: { ...record.resolved, passes: [standardPass] },
160
311
  receipt: {
@@ -164,7 +315,7 @@ describe('cooked material schema', () => {
164
315
  layerPlanIdentity: standardPlan.identity,
165
316
  },
166
317
  },
167
- };
318
+ });
168
319
  expect(validateCookedMaterialRecord(standardRecord)).toMatchObject({ ok: true });
169
320
  expect(
170
321
  validateCookedMaterialRecord({
@@ -185,7 +336,7 @@ describe('cooked material schema', () => {
185
336
  it('rejects a Standard record whose receipt omits the layer plan identity', () => {
186
337
  const standardPass = { name: 'forward', program: { module: 'forgeax::default-standard-pbr' } };
187
338
  const standardPlan = deriveStandardLayerPlan(record.resolved.parameters, [standardPass]);
188
- const standardRecord = {
339
+ const standardRecord = seal({
189
340
  ...record,
190
341
  resolved: { ...record.resolved, passes: [standardPass] },
191
342
  receipt: {
@@ -195,7 +346,7 @@ describe('cooked material schema', () => {
195
346
  layerPlanIdentity: standardPlan.identity,
196
347
  },
197
348
  },
198
- };
349
+ });
199
350
  expect(
200
351
  validateCookedMaterialRecord({
201
352
  ...standardRecord,
@@ -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,162 @@
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 { validateCookedMaterialRecord } from '../material-cook.js';
8
+ import { NativeCookerRegistry } from '../native-cooker-registry.js';
9
+
10
+ const compilerModule = pathToFileURL(
11
+ resolve(
12
+ dirname(fileURLToPath(import.meta.url)),
13
+ '../../../../packages/shader-compiler/dist/index.mjs',
14
+ ),
15
+ ).href;
16
+
17
+ const GUID = '11111111-1111-4111-8111-111111111111';
18
+ const ROOT_GUID = '22222222-2222-4222-8222-222222222222';
19
+ const CHILD_GUID = '33333333-3333-4333-8333-333333333333';
20
+ const SURFACE = `#define_import_path game_3d::rusted_iron_surface
21
+ #import forgeax_material::surface_v1::{SurfaceData, SurfaceInput}
22
+ fn evaluate_surface(input: SurfaceInput) -> SurfaceData {
23
+ 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);
24
+ }`;
25
+
26
+ describe('Surface Pack publication', () => {
27
+ it('publishes one complete cooked record with source closure and artifact', async () => {
28
+ const root = await mkdtemp(join(tmpdir(), 'forgeax-surface-publication-'));
29
+ try {
30
+ const sourcePath = join(root, 'rusted-iron-surface.wgsl');
31
+ await writeFile(sourcePath, SURFACE);
32
+ const material = Materials.standard({
33
+ surfaceModule: 'game_3d::rusted_iron_surface',
34
+ parameters: [],
35
+ values: {},
36
+ });
37
+ const { createMaterialPackCooker } = await import(compilerModule);
38
+ const draft = await createMaterialPackCooker([root]).cook({
39
+ guid: GUID,
40
+ source: material,
41
+ sourcePath,
42
+ });
43
+ const cooked = validateCookedMaterialRecord(draft.payload.cooked).unwrap();
44
+ expect(cooked.receipt.sourceClosure).toContain(sourcePath);
45
+ expect(cooked.programs.length).toBeGreaterThan(0);
46
+ for (const program of cooked.programs) {
47
+ const artifact = draft.artifacts[program.artifact.path];
48
+ expect(artifact?.bytes.byteLength).toBeGreaterThan(0);
49
+ expect(artifact?.bytes).toEqual(new Uint8Array(program.artifact.bytes));
50
+ }
51
+ expect(
52
+ cooked.resolved.passes?.find((pass) => pass.name === 'shadow-caster')?.program,
53
+ ).toMatchObject({ module: 'forgeax::default-shadow-caster', fragmentEntry: 'fs_shadow' });
54
+ } finally {
55
+ await rm(root, { recursive: true, force: true });
56
+ }
57
+ });
58
+
59
+ it('keeps the accepted generation as last known good after cook failure', async () => {
60
+ const registry = new NativeCookerRegistry();
61
+ registry.register({
62
+ key: 'material',
63
+ cook: async (input: { readonly fail?: boolean }) => {
64
+ if (input.fail) throw new Error('surface compose failed');
65
+ return {
66
+ guid: GUID,
67
+ payload: { generation: 1 },
68
+ refs: [],
69
+ artifacts: {},
70
+ inputFingerprint: 'sha256:surface-input',
71
+ };
72
+ },
73
+ });
74
+ const first = await registry.runTransaction({ key: 'material', input: {} });
75
+ expect(first).toMatchObject({ ok: true, value: { generation: 1, status: 'committed' } });
76
+ if (!first.ok) return;
77
+ const failed = await registry.runTransaction({
78
+ key: 'material',
79
+ input: { fail: true },
80
+ previous: { draft: first.value.draft, generation: first.value.generation },
81
+ });
82
+ expect(failed).toMatchObject({
83
+ ok: true,
84
+ value: {
85
+ status: 'recovered',
86
+ lastKnownGood: { payload: { generation: 1 } },
87
+ lastKnownGoodGeneration: 1,
88
+ },
89
+ });
90
+ });
91
+
92
+ it('cold-cooks a child through the Pack-owned root table and publishes its parent ref', async () => {
93
+ const root = await mkdtemp(join(tmpdir(), 'forgeax-surface-root-child-'));
94
+ try {
95
+ const rootMaterial = Materials.standard({
96
+ baseColor: [1, 1, 1, 1],
97
+ metallic: 0,
98
+ roughness: 0.5,
99
+ });
100
+ const child = {
101
+ kind: 'material' as const,
102
+ parent: ROOT_GUID as never,
103
+ values: { baseColor: [0.4, 0.45, 0.47, 1] },
104
+ };
105
+ const { createMaterialPackCooker } = await import(compilerModule);
106
+ const draft = await createMaterialPackCooker([root]).cook({
107
+ guid: CHILD_GUID,
108
+ source: child,
109
+ table: { [ROOT_GUID]: rootMaterial, [CHILD_GUID]: child },
110
+ });
111
+ const cooked = draft.payload.cooked as {
112
+ readonly authored: { readonly parent?: string };
113
+ readonly resolved: {
114
+ readonly parameters: readonly { readonly name: string }[];
115
+ readonly values: Readonly<Record<string, unknown>>;
116
+ };
117
+ readonly refs: { readonly parent: readonly string[] };
118
+ };
119
+ expect(cooked.authored.parent).toBe(ROOT_GUID);
120
+ expect(cooked.refs.parent).toEqual([ROOT_GUID]);
121
+ expect(cooked.resolved.parameters.map((parameter) => parameter.name)).toContain('baseColor');
122
+ expect(cooked.resolved.values.baseColor).toEqual([0.4, 0.45, 0.47, 1]);
123
+ expect(draft.refs).toContain(ROOT_GUID);
124
+ } finally {
125
+ await rm(root, { recursive: true, force: true });
126
+ }
127
+ });
128
+
129
+ it('does not publish an artifact when a physical material requests Deferred', async () => {
130
+ const { createMaterialPackCooker } = await import(compilerModule);
131
+ const material = {
132
+ kind: 'material' as const,
133
+ passes: [
134
+ {
135
+ name: 'forward',
136
+ program: {
137
+ module: 'forgeax_material::standard',
138
+ moduleSlots: { surface: 'forgeax_material::default_standard_surface' },
139
+ },
140
+ },
141
+ {
142
+ name: 'deferred',
143
+ program: {
144
+ module: 'forgeax_material::standard',
145
+ moduleSlots: { surface: 'forgeax_material::default_standard_surface' },
146
+ },
147
+ },
148
+ ],
149
+ parameters: [
150
+ { name: 'clearcoat', type: 'f32' as const },
151
+ { name: 'clearcoatRoughness', type: 'f32' as const },
152
+ ],
153
+ values: { clearcoat: 0, clearcoatRoughness: 0.5 },
154
+ };
155
+ await expect(
156
+ createMaterialPackCooker().cook({
157
+ guid: GUID,
158
+ source: material,
159
+ }),
160
+ ).rejects.toMatchObject({ code: 'material-physical-contract-invalid' });
161
+ });
162
+ });
@@ -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 modules plus the default shadow caster 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::default-shadow-caster',
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
@@ -57,6 +57,8 @@ export {
57
57
  createMaterialArtifactDigest,
58
58
  type MaterialCookArtifact,
59
59
  type MaterialCookIdentityExpectation,
60
+ type MaterialCookProgram,
61
+ type MaterialCookProgramContext,
60
62
  type MaterialCookReceipt,
61
63
  type MaterialCookRecordError,
62
64
  type MaterialCookRefs,