@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.
- package/README.md +1 -1
- package/dist/build.d.ts +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.mjs +129 -26
- package/dist/build.mjs.map +1 -1
- package/dist/cli-asset.mjs.map +1 -1
- package/dist/evidence/material-cook.d.ts +29 -3
- package/dist/evidence/material-cook.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +123 -24
- package/dist/index.mjs.map +1 -1
- package/dist/material-cook.d.ts +1 -1
- package/dist/material-cook.d.ts.map +1 -1
- package/dist/material-cook.mjs +123 -24
- package/dist/material-cook.mjs.map +1 -1
- package/dist/pack-authoring-node.mjs.map +1 -1
- package/dist/scanner.d.ts +3 -1
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.mjs +7 -3
- package/dist/scanner.mjs.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/material-cook-receipt.unit.test.ts +9 -9
- package/src/__tests__/material-cook-schema.unit.test.ts +172 -21
- package/src/__tests__/material-declaration-boundary.unit.test.ts +35 -0
- package/src/__tests__/material-surface-consumer-census.test.ts +24 -0
- package/src/__tests__/material-surface-publication.unit.test.ts +162 -0
- package/src/__tests__/material-surface-wire.unit.test.ts +30 -0
- package/src/build.ts +2 -0
- package/src/evidence/material-cook.ts +183 -31
- package/src/index.ts +5 -0
- package/src/material-cook.ts +5 -0
- package/src/scanner.ts +7 -3
- package/src/schema/material-cook.schema.json +254 -50
- package/dist/__tests__/ai-recovery.unit.test.d.ts +0 -2
- package/dist/__tests__/ai-recovery.unit.test.d.ts.map +0 -1
- package/dist/__tests__/artifact-path.test.d.ts +0 -2
- package/dist/__tests__/artifact-path.test.d.ts.map +0 -1
- package/dist/__tests__/asset-evidence-error-hints.test.d.ts +0 -2
- package/dist/__tests__/asset-evidence-error-hints.test.d.ts.map +0 -1
- package/dist/__tests__/asset-evidence-lifecycle.test.d.ts +0 -2
- package/dist/__tests__/asset-evidence-lifecycle.test.d.ts.map +0 -1
- package/dist/__tests__/asset-evidence-schema.test.d.ts +0 -2
- package/dist/__tests__/asset-evidence-schema.test.d.ts.map +0 -1
- package/dist/__tests__/atlas-cli-region-mismatch.test.d.ts +0 -2
- package/dist/__tests__/atlas-cli-region-mismatch.test.d.ts.map +0 -1
- package/dist/__tests__/atlas-error-code-owner.test-d.d.ts +0 -2
- package/dist/__tests__/atlas-error-code-owner.test-d.d.ts.map +0 -1
- package/dist/__tests__/cli-asset-evidence.integration.test.d.ts +0 -2
- package/dist/__tests__/cli-asset-evidence.integration.test.d.ts.map +0 -1
- package/dist/__tests__/cli.unit.test.d.ts +0 -2
- package/dist/__tests__/cli.unit.test.d.ts.map +0 -1
- package/dist/__tests__/evidence-source-inventory.unit.test.d.ts +0 -2
- package/dist/__tests__/evidence-source-inventory.unit.test.d.ts.map +0 -1
- package/dist/__tests__/guid-collision.unit.test.d.ts +0 -2
- package/dist/__tests__/guid-collision.unit.test.d.ts.map +0 -1
- package/dist/__tests__/guid-validator-surface.unit.test.d.ts +0 -2
- package/dist/__tests__/guid-validator-surface.unit.test.d.ts.map +0 -1
- package/dist/__tests__/inventory-schema.test.d.ts +0 -2
- package/dist/__tests__/inventory-schema.test.d.ts.map +0 -1
- package/dist/__tests__/material-cook-dependencies.unit.test.d.ts +0 -2
- package/dist/__tests__/material-cook-dependencies.unit.test.d.ts.map +0 -1
- package/dist/__tests__/material-cook-receipt-layer-plan.unit.test.d.ts +0 -2
- package/dist/__tests__/material-cook-receipt-layer-plan.unit.test.d.ts.map +0 -1
- package/dist/__tests__/material-cook-receipt.unit.test.d.ts +0 -2
- package/dist/__tests__/material-cook-receipt.unit.test.d.ts.map +0 -1
- package/dist/__tests__/material-cook-schema.unit.test.d.ts +0 -2
- package/dist/__tests__/material-cook-schema.unit.test.d.ts.map +0 -1
- package/dist/__tests__/mesh-bin-consumer-surface.unit.test.d.ts +0 -2
- package/dist/__tests__/mesh-bin-consumer-surface.unit.test.d.ts.map +0 -1
- package/dist/__tests__/mesh-bin-contract.unit.test.d.ts +0 -2
- package/dist/__tests__/mesh-bin-contract.unit.test.d.ts.map +0 -1
- package/dist/__tests__/meta-schema-optional.test.d.ts +0 -2
- package/dist/__tests__/meta-schema-optional.test.d.ts.map +0 -1
- package/dist/__tests__/meta-source-overrides.unit.test.d.ts +0 -2
- package/dist/__tests__/meta-source-overrides.unit.test.d.ts.map +0 -1
- package/dist/__tests__/native-cooker-registry.test.d.ts +0 -2
- package/dist/__tests__/native-cooker-registry.test.d.ts.map +0 -1
- package/dist/__tests__/offline-evidence-artifact-verification-owner.test-d.d.ts +0 -2
- package/dist/__tests__/offline-evidence-artifact-verification-owner.test-d.d.ts.map +0 -1
- package/dist/__tests__/offline-evidence.integration.test.d.ts +0 -2
- package/dist/__tests__/offline-evidence.integration.test.d.ts.map +0 -1
- package/dist/__tests__/owner-chain.integration.test.d.ts +0 -2
- package/dist/__tests__/owner-chain.integration.test.d.ts.map +0 -1
- package/dist/__tests__/pack-authoring-gateway.unit.test.d.ts +0 -2
- package/dist/__tests__/pack-authoring-gateway.unit.test.d.ts.map +0 -1
- package/dist/__tests__/pack-authoring.unit.test.d.ts +0 -2
- package/dist/__tests__/pack-authoring.unit.test.d.ts.map +0 -1
- package/dist/__tests__/pack-scanner.unit.test.d.ts +0 -2
- package/dist/__tests__/pack-scanner.unit.test.d.ts.map +0 -1
- package/dist/__tests__/pack-v2-fixture-migration.test.d.ts +0 -2
- package/dist/__tests__/pack-v2-fixture-migration.test.d.ts.map +0 -1
- package/dist/__tests__/pack-v2-schema.test.d.ts +0 -2
- package/dist/__tests__/pack-v2-schema.test.d.ts.map +0 -1
- package/dist/__tests__/pack.unit.test.d.ts +0 -2
- package/dist/__tests__/pack.unit.test.d.ts.map +0 -1
- package/dist/__tests__/package-finalizer.contract.test.d.ts +0 -2
- package/dist/__tests__/package-finalizer.contract.test.d.ts.map +0 -1
- package/dist/__tests__/producer-contract-schema.unit.test.d.ts +0 -2
- package/dist/__tests__/producer-contract-schema.unit.test.d.ts.map +0 -1
- package/dist/__tests__/resolve-asset-source.test.d.ts +0 -2
- package/dist/__tests__/resolve-asset-source.test.d.ts.map +0 -1
- package/dist/__tests__/runtime-projection.unit.test.d.ts +0 -2
- package/dist/__tests__/runtime-projection.unit.test.d.ts.map +0 -1
- package/dist/__tests__/runtime.browser.test.d.ts +0 -2
- package/dist/__tests__/runtime.browser.test.d.ts.map +0 -1
- package/dist/__tests__/scanner-blacklist.test.d.ts +0 -2
- package/dist/__tests__/scanner-blacklist.test.d.ts.map +0 -1
- package/dist/__tests__/scanner-inventory.contract.test.d.ts +0 -2
- package/dist/__tests__/scanner-inventory.contract.test.d.ts.map +0 -1
- package/dist/__tests__/scanner-inventory.test.d.ts +0 -2
- package/dist/__tests__/scanner-inventory.test.d.ts.map +0 -1
- package/dist/__tests__/scanner-mount-cycle.test.d.ts +0 -2
- package/dist/__tests__/scanner-mount-cycle.test.d.ts.map +0 -1
- package/dist/__tests__/scriptable-pack-cli.integration.test.d.ts +0 -2
- package/dist/__tests__/scriptable-pack-cli.integration.test.d.ts.map +0 -1
- package/dist/__tests__/scriptable-pack-ddc-key.unit.test.d.ts +0 -2
- package/dist/__tests__/scriptable-pack-ddc-key.unit.test.d.ts.map +0 -1
- package/dist/__tests__/topology-actions.unit.test.d.ts +0 -2
- package/dist/__tests__/topology-actions.unit.test.d.ts.map +0 -1
- package/dist/__tests__/topology.unit.test.d.ts +0 -2
- 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.
|
|
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.
|
|
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/
|
|
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
|
|
44
|
-
const
|
|
45
|
-
schemaVersion: 'material-cook/
|
|
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(
|
|
71
|
+
expect(validateMaterialCookReceipt(v4)).toMatchObject({ ok: true, value: v4 });
|
|
72
72
|
expect(validateMaterialCookReceipt(receipt())).toMatchObject({ ok: true });
|
|
73
73
|
expect(
|
|
74
74
|
validateMaterialCookReceipt({
|
|
75
|
-
...
|
|
76
|
-
identity: { ...
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
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/
|
|
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(
|
|
161
|
+
expect(createMaterialArtifactDigest(new TextEncoder().encode('shader'))).toBe(
|
|
127
162
|
'sha256:e137e75a5e0e7a623ca39de480667a55b43e0eedec58767634ebaef07c33383a',
|
|
128
163
|
);
|
|
129
|
-
expect(validateCookedMaterialRecord(JSON.parse(first))).toEqual({
|
|
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,
|