@forgeax/engine-pack 0.1.6 → 0.1.19

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 (37) hide show
  1. package/README.md +12 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/build.d.ts +1 -1
  4. package/dist/build.d.ts.map +1 -1
  5. package/dist/build.mjs +100 -51
  6. package/dist/build.mjs.map +1 -1
  7. package/dist/catalog-builder.d.ts +12 -0
  8. package/dist/catalog-builder.d.ts.map +1 -1
  9. package/dist/cli-asset.mjs +1 -1
  10. package/dist/cli-asset.mjs.map +1 -1
  11. package/dist/evidence/material-cook.d.ts +28 -6
  12. package/dist/evidence/material-cook.d.ts.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.mjs +97 -42
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/material-cook.d.ts +1 -1
  18. package/dist/material-cook.d.ts.map +1 -1
  19. package/dist/material-cook.mjs +96 -41
  20. package/dist/material-cook.mjs.map +1 -1
  21. package/dist/scanner.mjs +1 -1
  22. package/dist/scanner.mjs.map +1 -1
  23. package/dist/scriptable-pack-node.d.ts.map +1 -1
  24. package/dist/scriptable-pack-node.mjs +1 -1
  25. package/dist/scriptable-pack-node.mjs.map +1 -1
  26. package/package.json +3 -3
  27. package/src/__tests__/material-cook-dependencies.unit.test.ts +111 -0
  28. package/src/__tests__/material-cook-receipt.unit.test.ts +77 -20
  29. package/src/__tests__/material-cook-schema.unit.test.ts +64 -10
  30. package/src/__tests__/pack-v2-fixture-migration.test.ts +24 -1
  31. package/src/build.ts +1 -0
  32. package/src/catalog-builder.ts +42 -5
  33. package/src/evidence/material-cook.ts +133 -54
  34. package/src/index.ts +4 -0
  35. package/src/material-cook.ts +2 -0
  36. package/src/schema/material-cook.schema.json +48 -8
  37. package/src/scriptable-pack-node.ts +4 -1
@@ -25,16 +25,38 @@ export interface MaterialCookArtifact {
25
25
  readonly bytes: Uint8Array;
26
26
  }
27
27
 
28
+ export interface MaterialCookWasmProvenance {
29
+ readonly sourceContentKey: string;
30
+ readonly artifactSha256: string;
31
+ readonly glueSha256: string;
32
+ }
33
+
34
+ export interface MaterialCookIdentity {
35
+ readonly materialContractDigest: string;
36
+ readonly sourceRevision: string;
37
+ readonly sourceClosureDigest: string;
38
+ readonly layoutIdentity: string;
39
+ readonly programIdentity: string;
40
+ readonly pipelineIdentity: string;
41
+ readonly materialPublicationIdentity: string;
42
+ readonly cookIdentity: string;
43
+ readonly compilerFingerprint: string;
44
+ readonly wasm: MaterialCookWasmProvenance;
45
+ readonly artifactDigest: string;
46
+ readonly valueGeneration: number;
47
+ readonly dependencyGeneration: number;
48
+ readonly cookGeneration: number;
49
+ }
50
+
51
+ export type MaterialCookIdentityInput = Omit<MaterialCookIdentity, 'cookIdentity'>;
52
+
28
53
  export interface MaterialCookReceipt {
29
- readonly schemaVersion: 'material-cook/2';
54
+ readonly schemaVersion: 'material-cook/3';
30
55
  readonly sourceClosure: readonly string[];
31
56
  readonly profile: string;
32
57
  readonly compilerVersion: string;
33
- readonly inputDigest: string;
34
- readonly outputDigest: string;
35
- readonly layoutIdentity: string;
58
+ readonly identity: MaterialCookIdentity;
36
59
  readonly derivedInterface: { readonly layoutIdentity: string };
37
- readonly publicationGeneration?: number;
38
60
  }
39
61
 
40
62
  export interface MaterialParameterContract {
@@ -43,7 +65,7 @@ export interface MaterialParameterContract {
43
65
  }
44
66
 
45
67
  export interface CookedMaterialRecord {
46
- readonly schemaVersion: 'material-cook/2';
68
+ readonly schemaVersion: 'material-cook/3';
47
69
  readonly guid: string;
48
70
  readonly authored?: MaterialAsset;
49
71
  readonly materialGuid?: string;
@@ -66,7 +88,11 @@ export interface MaterialCookRecordError {
66
88
  readonly code: 'material-cook-record-invalid';
67
89
  readonly expected: string;
68
90
  readonly hint: string;
69
- readonly detail: { readonly field: string };
91
+ readonly detail: {
92
+ readonly field: string;
93
+ readonly actual?: unknown;
94
+ readonly action: string;
95
+ };
70
96
  }
71
97
 
72
98
  export interface MaterialCookIdentityExpectation {
@@ -154,6 +180,27 @@ function jsonValue(value: unknown): unknown {
154
180
  return value;
155
181
  }
156
182
 
183
+ export function createMaterialCookIdentity(input: MaterialCookIdentityInput): MaterialCookIdentity {
184
+ const cookIdentity = createMaterialArtifactDigest(
185
+ new TextEncoder().encode(
186
+ JSON.stringify(
187
+ jsonValue({
188
+ materialContractDigest: input.materialContractDigest,
189
+ sourceRevision: input.sourceRevision,
190
+ sourceClosureDigest: input.sourceClosureDigest,
191
+ layoutIdentity: input.layoutIdentity,
192
+ programIdentity: input.programIdentity,
193
+ pipelineIdentity: input.pipelineIdentity,
194
+ compilerFingerprint: input.compilerFingerprint,
195
+ wasm: input.wasm,
196
+ artifactDigest: input.artifactDigest,
197
+ }),
198
+ ),
199
+ ),
200
+ );
201
+ return { ...input, cookIdentity };
202
+ }
203
+
157
204
  export function serializeCookedMaterialRecord(record: CookedMaterialRecord): string {
158
205
  return JSON.stringify(jsonValue(record));
159
206
  }
@@ -162,83 +209,116 @@ export function serializeMaterialCookReceipt(receipt: MaterialCookReceipt): stri
162
209
  return JSON.stringify(jsonValue({ ...receipt, sourceClosure: unique(receipt.sourceClosure) }));
163
210
  }
164
211
 
165
- function invalid(field: string): Result<never, MaterialCookRecordError> {
212
+ function invalid(field: string, actual?: unknown): Result<never, MaterialCookRecordError> {
166
213
  return err({
167
214
  code: 'material-cook-record-invalid',
168
- expected: 'a complete material-cook/2 record with derived layout identity',
215
+ expected: 'a complete material-cook/3 record with layered identity and provenance',
169
216
  hint: 're-cook the material and publish its record, artifact, references, and receipt together',
170
- detail: { field },
217
+ detail: {
218
+ field,
219
+ ...(actual === undefined ? {} : { actual }),
220
+ action: 'inspect the named field and recook the material generation',
221
+ },
171
222
  });
172
223
  }
173
224
 
225
+ const IDENTITY_FIELDS = [
226
+ 'materialContractDigest',
227
+ 'sourceRevision',
228
+ 'sourceClosureDigest',
229
+ 'layoutIdentity',
230
+ 'programIdentity',
231
+ 'pipelineIdentity',
232
+ 'materialPublicationIdentity',
233
+ 'cookIdentity',
234
+ 'compilerFingerprint',
235
+ 'artifactDigest',
236
+ ] as const;
237
+
238
+ function isGeneration(value: unknown): value is number {
239
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 1;
240
+ }
241
+
242
+ function validateIdentity(value: unknown): Result<MaterialCookIdentity, MaterialCookRecordError> {
243
+ if (value === null || typeof value !== 'object') return invalid('receipt.identity', value);
244
+ const candidate = value as Record<string, unknown>;
245
+ for (const field of IDENTITY_FIELDS) {
246
+ if (typeof candidate[field] !== 'string' || candidate[field].length === 0) {
247
+ return invalid(`receipt.identity.${field}`, candidate[field]);
248
+ }
249
+ }
250
+ if (candidate.wasm === null || typeof candidate.wasm !== 'object') {
251
+ return invalid('receipt.identity.wasm', candidate.wasm);
252
+ }
253
+ const wasm = candidate.wasm as Record<string, unknown>;
254
+ for (const field of ['sourceContentKey', 'artifactSha256', 'glueSha256']) {
255
+ if (typeof wasm[field] !== 'string' || wasm[field].length === 0) {
256
+ return invalid(`receipt.identity.wasm.${field}`, wasm[field]);
257
+ }
258
+ }
259
+ for (const field of ['valueGeneration', 'dependencyGeneration', 'cookGeneration']) {
260
+ if (!isGeneration(candidate[field]))
261
+ return invalid(`receipt.identity.${field}`, candidate[field]);
262
+ }
263
+ return ok({
264
+ ...candidate,
265
+ wasm: wasm as unknown as MaterialCookWasmProvenance,
266
+ } as MaterialCookIdentity);
267
+ }
268
+
174
269
  export function validateMaterialCookReceipt(
175
270
  value: unknown,
176
271
  expected: MaterialCookIdentityExpectation = {},
177
272
  ): Result<MaterialCookReceipt, MaterialCookRecordError> {
178
273
  if (value === null || typeof value !== 'object') return invalid('receipt');
179
274
  const candidate = value as Record<string, unknown>;
180
- if (candidate.schemaVersion !== 'material-cook/2') return invalid('receipt.schemaVersion');
181
- if (typeof candidate.layoutIdentity !== 'string' || candidate.layoutIdentity.length === 0) {
182
- return invalid('receipt.layoutIdentity');
183
- }
275
+ if (candidate.schemaVersion !== 'material-cook/3')
276
+ return invalid('receipt.schemaVersion', candidate.schemaVersion);
277
+ const identityResult = validateIdentity(candidate.identity);
278
+ if (!identityResult.ok) return identityResult;
279
+ const identity = identityResult.value;
184
280
  if (candidate.derivedInterface === null || typeof candidate.derivedInterface !== 'object') {
185
- return invalid('receipt.derivedInterface');
281
+ return invalid('receipt.derivedInterface', candidate.derivedInterface);
186
282
  }
187
283
  const derivedInterface = candidate.derivedInterface as Record<string, unknown>;
188
- if (derivedInterface.layoutIdentity !== candidate.layoutIdentity) {
189
- return invalid('receipt.derivedInterface.layoutIdentity');
284
+ if (derivedInterface.layoutIdentity !== identity.layoutIdentity) {
285
+ return invalid('receipt.derivedInterface.layoutIdentity', derivedInterface.layoutIdentity);
190
286
  }
191
- for (const field of [
192
- 'sourceClosure',
193
- 'profile',
194
- 'compilerVersion',
195
- 'inputDigest',
196
- 'outputDigest',
197
- ]) {
287
+ for (const field of ['sourceClosure', 'profile', 'compilerVersion']) {
198
288
  const fieldValue = candidate[field];
199
289
  if (
200
290
  (field === 'sourceClosure' && !Array.isArray(fieldValue)) ||
201
291
  (field !== 'sourceClosure' && typeof fieldValue !== 'string')
202
292
  ) {
203
- return invalid(`receipt.${field}`);
293
+ return invalid(`receipt.${field}`, fieldValue);
204
294
  }
205
295
  }
206
296
  const sourceClosure = candidate.sourceClosure;
207
297
  if (Array.isArray(sourceClosure) && sourceClosure.some((path) => typeof path !== 'string')) {
208
- return invalid('receipt.sourceClosure');
298
+ return invalid('receipt.sourceClosure', sourceClosure);
209
299
  }
210
300
  if (
211
301
  expected.layoutIdentity !== undefined &&
212
- candidate.layoutIdentity !== expected.layoutIdentity
302
+ identity.layoutIdentity !== expected.layoutIdentity
213
303
  ) {
214
- return invalid('receipt.layoutIdentity');
215
- }
216
- if (expected.artifactDigest !== undefined && candidate.outputDigest !== expected.artifactDigest) {
217
- return invalid('receipt.outputDigest');
218
- }
219
- if (expected.inputDigest !== undefined && candidate.inputDigest !== expected.inputDigest) {
220
- return invalid('receipt.inputDigest');
304
+ return invalid('receipt.identity.layoutIdentity', identity.layoutIdentity);
221
305
  }
222
306
  if (
223
- candidate.publicationGeneration !== undefined &&
224
- (typeof candidate.publicationGeneration !== 'number' ||
225
- !Number.isSafeInteger(candidate.publicationGeneration) ||
226
- candidate.publicationGeneration < 1)
307
+ expected.artifactDigest !== undefined &&
308
+ identity.artifactDigest !== expected.artifactDigest
227
309
  ) {
228
- return invalid('receipt.publicationGeneration');
310
+ return invalid('receipt.identity.artifactDigest', identity.artifactDigest);
311
+ }
312
+ if (expected.inputDigest !== undefined && identity.cookIdentity !== expected.inputDigest) {
313
+ return invalid('receipt.identity.cookIdentity', identity.cookIdentity);
229
314
  }
230
315
  return ok({
231
- schemaVersion: 'material-cook/2',
316
+ schemaVersion: 'material-cook/3',
232
317
  sourceClosure: candidate.sourceClosure as string[],
233
318
  profile: candidate.profile as string,
234
319
  compilerVersion: candidate.compilerVersion as string,
235
- inputDigest: candidate.inputDigest as string,
236
- outputDigest: candidate.outputDigest as string,
237
- layoutIdentity: candidate.layoutIdentity,
320
+ identity,
238
321
  derivedInterface: { layoutIdentity: derivedInterface.layoutIdentity as string },
239
- ...(candidate.publicationGeneration === undefined
240
- ? {}
241
- : { publicationGeneration: candidate.publicationGeneration as number }),
242
322
  });
243
323
  }
244
324
 
@@ -247,17 +327,16 @@ export function validateCookedMaterialRecord(
247
327
  ): Result<CookedMaterialRecord, MaterialCookRecordError> {
248
328
  if (value === null || typeof value !== 'object') return invalid('record');
249
329
  const candidate = value as Record<string, unknown>;
250
- if (candidate.schemaVersion !== 'material-cook/2') return invalid('schemaVersion');
330
+ if (candidate.schemaVersion !== 'material-cook/3')
331
+ return invalid('schemaVersion', candidate.schemaVersion);
251
332
  if (typeof candidate.guid !== 'string' || !candidate.guid) return invalid('guid');
252
333
  if (candidate.materialGuid !== undefined && typeof candidate.materialGuid !== 'string')
253
334
  return invalid('materialGuid');
254
335
  if (
255
336
  candidate.publicationGeneration !== undefined &&
256
- (typeof candidate.publicationGeneration !== 'number' ||
257
- !Number.isSafeInteger(candidate.publicationGeneration) ||
258
- candidate.publicationGeneration < 1)
337
+ !isGeneration(candidate.publicationGeneration)
259
338
  )
260
- return invalid('publicationGeneration');
339
+ return invalid('publicationGeneration', candidate.publicationGeneration);
261
340
  if (candidate.specializationKey !== undefined && typeof candidate.specializationKey !== 'string')
262
341
  return invalid('specializationKey');
263
342
  if (candidate.artifactDigest !== undefined && typeof candidate.artifactDigest !== 'string')
@@ -300,12 +379,12 @@ export function validateCookedMaterialRecord(
300
379
  });
301
380
  if (!receiptResult.ok) return receiptResult;
302
381
  if (candidate.artifactDigest !== undefined && candidate.artifactDigest !== artifact.digest)
303
- return invalid('artifactDigest');
382
+ return invalid('artifactDigest', candidate.artifactDigest);
304
383
  if (
305
384
  candidate.publicationGeneration !== undefined &&
306
- receiptResult.value.publicationGeneration !== candidate.publicationGeneration
385
+ receiptResult.value.identity.cookGeneration !== candidate.publicationGeneration
307
386
  )
308
- return invalid('receipt.publicationGeneration');
387
+ return invalid('receipt.identity.cookGeneration', receiptResult.value.identity.cookGeneration);
309
388
  const normalized = {
310
389
  ...candidate,
311
390
  artifact: {
package/src/index.ts CHANGED
@@ -6,11 +6,15 @@ export {
6
6
  type CookedMaterialRecord,
7
7
  collectMaterialCookRefs,
8
8
  createMaterialArtifactDigest,
9
+ createMaterialCookIdentity,
9
10
  type MaterialCookArtifact,
11
+ type MaterialCookIdentity,
10
12
  type MaterialCookIdentityExpectation,
13
+ type MaterialCookIdentityInput,
11
14
  type MaterialCookReceipt,
12
15
  type MaterialCookRecordError,
13
16
  type MaterialCookRefs,
17
+ type MaterialCookWasmProvenance,
14
18
  projectCookedMaterialRecord,
15
19
  serializeCookedMaterialRecord,
16
20
  serializeMaterialCookReceipt,
@@ -2,10 +2,12 @@ export {
2
2
  type CookedMaterialRecord,
3
3
  collectMaterialCookRefs,
4
4
  createMaterialArtifactDigest,
5
+ createMaterialCookIdentity,
5
6
  type MaterialCookArtifact,
6
7
  type MaterialCookReceipt,
7
8
  type MaterialCookRecordError,
8
9
  type MaterialCookRefs,
10
+ type MaterialCookWasmProvenance,
9
11
  projectCookedMaterialRecord,
10
12
  serializeCookedMaterialRecord,
11
13
  serializeMaterialCookReceipt,
@@ -4,7 +4,7 @@
4
4
  "type": "object",
5
5
  "required": ["schemaVersion", "guid", "resolved", "refs", "artifact", "receipt"],
6
6
  "properties": {
7
- "schemaVersion": { "const": "material-cook/2" },
7
+ "schemaVersion": { "const": "material-cook/3" },
8
8
  "guid": { "type": "string", "minLength": 1 },
9
9
  "materialGuid": { "type": "string", "minLength": 1 },
10
10
  "publicationGeneration": { "type": "integer", "minimum": 1 },
@@ -51,19 +51,59 @@
51
51
  "sourceClosure",
52
52
  "profile",
53
53
  "compilerVersion",
54
- "inputDigest",
55
- "outputDigest",
56
- "layoutIdentity",
54
+ "identity",
57
55
  "derivedInterface"
58
56
  ],
59
57
  "properties": {
60
- "schemaVersion": { "const": "material-cook/2" },
58
+ "schemaVersion": { "const": "material-cook/3" },
61
59
  "sourceClosure": { "type": "array", "items": { "type": "string" } },
62
60
  "profile": { "type": "string" },
63
61
  "compilerVersion": { "type": "string" },
64
- "inputDigest": { "type": "string" },
65
- "outputDigest": { "type": "string" },
66
- "layoutIdentity": { "type": "string", "minLength": 1 },
62
+ "identity": {
63
+ "type": "object",
64
+ "required": [
65
+ "materialContractDigest",
66
+ "sourceRevision",
67
+ "sourceClosureDigest",
68
+ "layoutIdentity",
69
+ "programIdentity",
70
+ "pipelineIdentity",
71
+ "materialPublicationIdentity",
72
+ "cookIdentity",
73
+ "compilerFingerprint",
74
+ "wasm",
75
+ "artifactDigest",
76
+ "valueGeneration",
77
+ "dependencyGeneration",
78
+ "cookGeneration"
79
+ ],
80
+ "properties": {
81
+ "materialContractDigest": { "type": "string", "minLength": 1 },
82
+ "sourceRevision": { "type": "string", "minLength": 1 },
83
+ "sourceClosureDigest": { "type": "string", "minLength": 1 },
84
+ "layoutIdentity": { "type": "string", "minLength": 1 },
85
+ "programIdentity": { "type": "string", "minLength": 1 },
86
+ "pipelineIdentity": { "type": "string", "minLength": 1 },
87
+ "materialPublicationIdentity": { "type": "string", "minLength": 1 },
88
+ "cookIdentity": { "type": "string", "minLength": 1 },
89
+ "compilerFingerprint": { "type": "string", "minLength": 1 },
90
+ "wasm": {
91
+ "type": "object",
92
+ "required": ["sourceContentKey", "artifactSha256", "glueSha256"],
93
+ "properties": {
94
+ "sourceContentKey": { "type": "string", "minLength": 1 },
95
+ "artifactSha256": { "type": "string", "minLength": 1 },
96
+ "glueSha256": { "type": "string", "minLength": 1 }
97
+ },
98
+ "additionalProperties": false
99
+ },
100
+ "artifactDigest": { "type": "string", "minLength": 1 },
101
+ "valueGeneration": { "type": "integer", "minimum": 1 },
102
+ "dependencyGeneration": { "type": "integer", "minimum": 1 },
103
+ "cookGeneration": { "type": "integer", "minimum": 1 }
104
+ },
105
+ "additionalProperties": false
106
+ },
67
107
  "derivedInterface": {
68
108
  "type": "object",
69
109
  "required": ["layoutIdentity"],
@@ -670,7 +670,10 @@ export async function loadScriptablePack(
670
670
  sourcePath: string,
671
671
  options: LoadScriptablePackOptions = {},
672
672
  ): Promise<Result<Readonly<ScriptablePackDefinition>, ScriptablePackError>> {
673
- const timeoutMs = options.timeoutMs ?? 5_000;
673
+ // Module initialization includes transpiling the trusted TypeScript source
674
+ // closure in a fresh Worker. A cold SDK workspace can legitimately exceed
675
+ // the build execution budget before any authored build() code has run.
676
+ const timeoutMs = options.timeoutMs ?? 15_000;
674
677
  const buildTimeoutMs = options.buildTimeoutMs ?? 5_000;
675
678
  const executor = options.executor ?? new WorkerScriptablePackExecutor();
676
679
  let disposeReason: 'complete' | 'timeout' | 'failure' | undefined;