@forgeax/engine-pack 0.1.31 → 0.1.33

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 (40) hide show
  1. package/README.md +2 -2
  2. package/dist/build.d.ts +1 -1
  3. package/dist/build.d.ts.map +1 -1
  4. package/dist/build.mjs +252 -22
  5. package/dist/build.mjs.map +1 -1
  6. package/dist/cli-asset.d.ts +63 -0
  7. package/dist/cli-asset.d.ts.map +1 -1
  8. package/dist/cli-asset.mjs +142 -4
  9. package/dist/cli-asset.mjs.map +1 -1
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/native-cooker-registry.d.ts.map +1 -1
  12. package/dist/native-cooker.mjs +13 -1
  13. package/dist/native-cooker.mjs.map +1 -1
  14. package/dist/pack-authoring-node.d.ts +6 -0
  15. package/dist/pack-authoring-node.d.ts.map +1 -1
  16. package/dist/pack-authoring-node.mjs +230 -16
  17. package/dist/pack-authoring-node.mjs.map +1 -1
  18. package/dist/pack-authoring.d.ts +26 -0
  19. package/dist/pack-authoring.d.ts.map +1 -1
  20. package/dist/pack-authoring.mjs.map +1 -1
  21. package/dist/runtime-publication.d.ts +8 -0
  22. package/dist/runtime-publication.d.ts.map +1 -1
  23. package/dist/scanner.d.ts +1 -1
  24. package/dist/scanner.mjs +5 -5
  25. package/dist/scanner.mjs.map +1 -1
  26. package/dist/scriptable-pack-node.mjs.map +1 -1
  27. package/dist/scriptable-pack.mjs.map +1 -1
  28. package/package.json +2 -2
  29. package/src/__tests__/cli.unit.test.ts +87 -0
  30. package/src/__tests__/native-cooker-registry.test.ts +16 -0
  31. package/src/__tests__/pack-authoring-gateway.unit.test.ts +120 -1
  32. package/src/__tests__/pack.unit.test.ts +6 -6
  33. package/src/__tests__/runtime-publication.unit.test.ts +27 -1
  34. package/src/build.ts +2 -0
  35. package/src/cli-asset.ts +227 -5
  36. package/src/native-cooker-registry.ts +14 -1
  37. package/src/pack-authoring-node.ts +278 -13
  38. package/src/pack-authoring.ts +26 -0
  39. package/src/runtime-publication.ts +44 -0
  40. package/src/scanner.ts +5 -5
@@ -1,4 +1,4 @@
1
- import { mkdtemp, readFile, rm } from 'node:fs/promises';
1
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { afterEach, describe, expect, it } from 'vitest';
@@ -146,6 +146,30 @@ describe('Pack authoring gateway', () => {
146
146
  ok: true,
147
147
  value: { sources: [{ packageId: SOURCE_PACKAGE, sourcePath: 'assets/source.pack.ts' }] },
148
148
  });
149
+ const verified = await gateway.execute({
150
+ operation: 'asset.verify',
151
+ requestId: 'verify-source',
152
+ });
153
+ expect(verified).toMatchObject({
154
+ ok: true,
155
+ value: {
156
+ schemaVersion: 'asset-verification-v1',
157
+ scope: {
158
+ sourceCount: 1,
159
+ assetCount: 0,
160
+ scriptablePackSourceCount: 1,
161
+ scriptablePackSources: [
162
+ {
163
+ sourcePath: expect.stringContaining('assets/source.pack.ts'),
164
+ packageId: SOURCE_PACKAGE,
165
+ output: 'unproduced',
166
+ },
167
+ ],
168
+ },
169
+ assets: [],
170
+ summary: { unmaterializedScriptablePackCount: 1 },
171
+ },
172
+ });
149
173
 
150
174
  const instance = await gateway.execute({
151
175
  operation: 'asset-source.create-instance',
@@ -246,4 +270,99 @@ describe('Pack authoring gateway', () => {
246
270
  const result = await gateway.execute({ operation: 'asset.list', requestId: 'bad-callback' });
247
271
  expect(result).toMatchObject({ ok: false, error: { code: 'pack-parameter-invalid' } });
248
272
  });
273
+
274
+ it('accepts references to published assets owned by another producer', async () => {
275
+ const root = await mkdtemp(join(tmpdir(), 'forgeax-pack-gateway-external-ref-'));
276
+ roots.push(root);
277
+ const externalGuid = '01900000-0000-7000-8000-000000000099';
278
+ const gateway = createFileSystemPackAuthoringGateway({
279
+ gameRoot: root,
280
+ additionalKnownGuids: () => [externalGuid],
281
+ });
282
+ await gateway.execute({
283
+ operation: 'asset-source.create',
284
+ requestId: 'create-external-ref',
285
+ targetPath: 'assets/direct.pack.json',
286
+ format: 'pack.json',
287
+ packageId: DIRECT_PACKAGE,
288
+ initialAssets: {
289
+ 'material/main': { kind: 'material', payload: {}, refs: [externalGuid] },
290
+ },
291
+ });
292
+ const verified = await gateway.execute({
293
+ operation: 'asset.verify',
294
+ requestId: 'verify-external-ref',
295
+ });
296
+ expect(verified).toMatchObject({ ok: true, value: { summary: { assetCount: 1 } } });
297
+ });
298
+ });
299
+
300
+ it('inspects the exact source revision used by rebuild conflict checks', async () => {
301
+ const root = await mkdtemp(join(tmpdir(), 'forgeax-inspect-revision-'));
302
+ roots.push(root);
303
+ const outputs = [
304
+ {
305
+ packageId: SOURCE_PACKAGE,
306
+ sourceKey: 'mesh/main',
307
+ guid: AssetGuid.format(AssetGuid.derive(packageId(SOURCE_PACKAGE), 'mesh/main')),
308
+ kind: 'mesh',
309
+ sourcePath: 'assets/source.pack.ts',
310
+ ready: true,
311
+ },
312
+ ];
313
+ let published = false;
314
+ const gateway = createFileSystemPackAuthoringGateway({
315
+ gameRoot: root,
316
+ materialized: () => (published ? outputs : []),
317
+ rebuild: async () => ({ ok: true, value: undefined }),
318
+ });
319
+ const created = await gateway.execute({
320
+ operation: 'asset-source.create',
321
+ requestId: 'create',
322
+ targetPath: 'assets/source.pack.ts',
323
+ format: 'pack.ts',
324
+ packageId: SOURCE_PACKAGE,
325
+ });
326
+ expect(created.ok).toBe(true);
327
+ if (!created.ok) return;
328
+ published = true;
329
+ const inspected = await gateway.execute({
330
+ operation: 'asset.inspect',
331
+ subject: 'assets/source.pack.ts',
332
+ requestId: 'inspect',
333
+ });
334
+ expect(inspected).toMatchObject({
335
+ ok: true,
336
+ value: {
337
+ revision: created.value.revision,
338
+ sourcePath: 'assets/source.pack.ts',
339
+ assets: outputs,
340
+ },
341
+ });
342
+ const rebuilt = await gateway.execute({
343
+ operation: 'asset-source.rebuild',
344
+ sourcePath: 'assets/source.pack.ts',
345
+ expectedRevision: created.value.revision,
346
+ requestId: 'rebuild',
347
+ });
348
+ expect(rebuilt).toMatchObject({
349
+ ok: true,
350
+ value: { assets: outputs, revision: created.value.revision },
351
+ });
352
+ const path = join(root, 'assets/source.pack.ts');
353
+ await writeFile(path, `${await readFile(path, 'utf8')}\n// source edit\n`);
354
+ const changed = await gateway.execute({
355
+ operation: 'asset.inspect',
356
+ subject: 'assets/source.pack.ts',
357
+ requestId: 'inspect-edited',
358
+ });
359
+ expect(changed.ok).toBe(true);
360
+ if (changed.ok) expect(changed.value.revision).not.toBe(created.value.revision);
361
+ const stale = await gateway.execute({
362
+ operation: 'asset-source.rebuild',
363
+ sourcePath: 'assets/source.pack.ts',
364
+ expectedRevision: created.value.revision,
365
+ requestId: 'stale',
366
+ });
367
+ expect(stale).toMatchObject({ ok: false, error: { code: 'pack-source-revision-conflict' } });
249
368
  });
@@ -540,7 +540,7 @@ const V1_WHITELIST = new Set([
540
540
  cwd: tempDir,
541
541
  });
542
542
  expect(code).toBe(0);
543
- expect(io.stdout).toContain('material-validated: 1');
543
+ expect(JSON.parse(io.stdout[0] as string).summary.materialCount).toBe(1);
544
544
  });
545
545
 
546
546
  it('CLI verify prints material-validated: 0 when no material assets', async () => {
@@ -559,7 +559,7 @@ const V1_WHITELIST = new Set([
559
559
  cwd: tempDir,
560
560
  });
561
561
  expect(code).toBe(0);
562
- expect(io.stdout).toContain('material-validated: 0');
562
+ expect(JSON.parse(io.stdout[0] as string).summary.materialCount).toBe(0);
563
563
  });
564
564
 
565
565
  it('CLI verify reports material payloads for later cook validation', async () => {
@@ -579,7 +579,7 @@ const V1_WHITELIST = new Set([
579
579
  });
580
580
  expect(code).toBe(0);
581
581
  expect(io.stderr).toEqual([]);
582
- expect(io.stdout).toContain('material-validated: 1');
582
+ expect(JSON.parse(io.stdout[0] as string).summary.materialCount).toBe(1);
583
583
  });
584
584
 
585
585
  it('CLI verify counts only material assets among multiple kinds', async () => {
@@ -603,7 +603,7 @@ const V1_WHITELIST = new Set([
603
603
  cwd: tempDir,
604
604
  });
605
605
  expect(code).toBe(0);
606
- expect(io.stdout).toContain('material-validated: 1');
606
+ expect(JSON.parse(io.stdout[0] as string).summary.materialCount).toBe(1);
607
607
  });
608
608
  });
609
609
  });
@@ -1572,7 +1572,7 @@ const V1_WHITELIST = new Set([
1572
1572
  });
1573
1573
  expect(exitCode).toBe(0);
1574
1574
  expect(stderrParts).toEqual([]);
1575
- expect(stdoutParts).toContain('material-validated: 0');
1575
+ expect(JSON.parse(stdoutParts[0] as string).summary.materialCount).toBe(0);
1576
1576
  });
1577
1577
 
1578
1578
  it('accepts shader sidecar with missing paramSchema', async () => {
@@ -1606,7 +1606,7 @@ const V1_WHITELIST = new Set([
1606
1606
  });
1607
1607
  expect(exitCode).toBe(0);
1608
1608
  expect(stderrParts).toEqual([]);
1609
- expect(stdoutParts).toContain('material-validated: 0');
1609
+ expect(JSON.parse(stdoutParts[0] as string).summary.materialCount).toBe(0);
1610
1610
  });
1611
1611
 
1612
1612
  it('ignores legacy shader paramSchema types', async () => {
@@ -1,5 +1,9 @@
1
1
  import { expect, it } from 'vitest';
2
- import { createRuntimePackPublication } from '../runtime-publication.js';
2
+ import {
3
+ bindRuntimePackScope,
4
+ createRuntimePackPublication,
5
+ stripRuntimePackLifecycle,
6
+ } from '../runtime-publication.js';
3
7
 
4
8
  it('preserves declared source keys in derived publication rows and their digest', () => {
5
9
  const input = {
@@ -34,3 +38,25 @@ it('preserves declared source keys in derived publication rows and their digest'
34
38
  });
35
39
  expect(explicit.publication.outputs).toEqual(after.publication.outputs);
36
40
  });
41
+
42
+ it('separates immutable Pack content from the runtime publication tuple', () => {
43
+ const publication = createRuntimePackPublication({
44
+ pack: { assets: [{ guid: 'MESH-GUID', kind: 'mesh', payload: { vertexCount: 3 } }] },
45
+ scopeId: 'old-scope',
46
+ sourcePath: 'mesh.pack.ts',
47
+ sourceRevision: 'source-1',
48
+ packageUrl: '/mesh.pack.json',
49
+ });
50
+ const semantic = stripRuntimePackLifecycle(publication.pack) as Record<string, unknown>;
51
+ expect(semantic).not.toHaveProperty('scopeId');
52
+ expect(semantic).not.toHaveProperty('generation');
53
+ expect(bindRuntimePackScope(semantic, 'new-scope', 23)).toMatchObject({
54
+ scopeId: 'new-scope',
55
+ generation: 23,
56
+ digest: publication.pack.digest,
57
+ });
58
+ expect(bindRuntimePackScope(publication.pack, 'new-scope', 23)).toMatchObject({
59
+ scopeId: 'new-scope',
60
+ generation: 23,
61
+ });
62
+ });
package/src/build.ts CHANGED
@@ -119,12 +119,14 @@ export { validateProducerContract, validateProducerOutputs } from './producer-co
119
119
  export { resolveAssetSource } from './resolve-asset-source.js';
120
120
  export { parsePackV2, validateMeta, validatePack, validatePackV2 } from './runtime.js';
121
121
  export {
122
+ bindRuntimePackScope,
122
123
  createRuntimePackPublication,
123
124
  type RuntimePackAssetInput,
124
125
  type RuntimePackEnvelope,
125
126
  type RuntimePackInput,
126
127
  type RuntimePackPublication,
127
128
  type RuntimePackPublicationInput,
129
+ stripRuntimePackLifecycle,
128
130
  } from './runtime-publication.js';
129
131
  export {
130
132
  type InventoryDeclaration,
package/src/cli-asset.ts CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  import { readSourceInventory } from './evidence/source-inventory.js';
24
24
  import { isValidAssetGuidString } from './guid.js';
25
25
  import { parsePackV2 } from './index.js';
26
- import { projectScriptablePackMeta } from './pack-authoring.js';
26
+ import { parsePackSourceJson, projectScriptablePackMeta } from './pack-authoring.js';
27
27
  import { type ScanSourceDeclaration, scanInventory } from './scanner.js';
28
28
  import { loadScriptablePack } from './scriptable-pack-node.js';
29
29
 
@@ -36,8 +36,70 @@ export interface PackEntry {
36
36
  readonly kind: string;
37
37
  readonly sourcePath: string;
38
38
  readonly name?: string;
39
+ readonly sourceKey?: string;
40
+ readonly sourceIndex?: number;
41
+ readonly sourceRevision?: string;
39
42
  }
40
43
 
44
+ export interface AssetVerificationReport {
45
+ readonly schemaVersion: 'asset-verification-v1';
46
+ readonly root: string;
47
+ /** The bounded source scope used for this read-only report. */
48
+ readonly scope: {
49
+ readonly sourceCount: number;
50
+ readonly assetCount: number;
51
+ readonly sourcePaths: readonly string[];
52
+ readonly omittedSourceCount: number;
53
+ readonly assetLimit: number;
54
+ readonly truncated: boolean;
55
+ readonly scriptablePackSourceCount: number;
56
+ readonly scriptablePackSources: readonly {
57
+ readonly sourcePath: string;
58
+ readonly packageId: string;
59
+ readonly output: 'unproduced';
60
+ readonly reason: 'producer-not-run';
61
+ }[];
62
+ readonly omittedScriptablePackSourceCount: number;
63
+ };
64
+ readonly assets: readonly {
65
+ readonly guid: string;
66
+ readonly type: string;
67
+ readonly source: {
68
+ readonly path: string;
69
+ readonly format: 'meta.json' | 'pack.json' | 'pack.ts';
70
+ readonly revision?: string;
71
+ readonly role: 'author';
72
+ };
73
+ readonly output: {
74
+ readonly status: 'produced' | 'unproduced' | 'unknown';
75
+ readonly availability: 'available' | 'missing' | 'unknown';
76
+ readonly freshness: 'unknown';
77
+ readonly packagePath?: string;
78
+ readonly artifactPaths?: readonly string[];
79
+ };
80
+ readonly dependencies: readonly string[];
81
+ readonly producer: { readonly state: 'not-run' | 'published' | 'unknown' };
82
+ readonly name?: string;
83
+ readonly sourceKey?: string;
84
+ readonly sourceIndex?: number;
85
+ }[];
86
+ readonly summary: {
87
+ readonly assetCount: number;
88
+ readonly emittedAssetCount: number;
89
+ readonly materialCount: number;
90
+ readonly unproducedAssetCount: number;
91
+ readonly unknownAssetCount: number;
92
+ readonly unmaterializedScriptablePackCount: number;
93
+ };
94
+ }
95
+
96
+ const ASSET_VERIFY_LIMIT = 256;
97
+
98
+ type VerificationFacts = Pick<
99
+ AssetVerificationReport['assets'][number],
100
+ 'dependencies' | 'output' | 'producer'
101
+ >;
102
+
41
103
  interface AssetCtx {
42
104
  readonly stdoutWrite: (line: string) => void;
43
105
  readonly stderrWrite: (line: string) => void;
@@ -70,7 +132,10 @@ function emitError(ctx: AssetCtx, err: ScanErrShape): number {
70
132
  export async function scanEntries(
71
133
  roots: readonly string[],
72
134
  ctx: AssetCtx,
73
- ): Promise<{ ok: true; value: PackEntry[] } | { ok: false }> {
135
+ ): Promise<
136
+ | { ok: true; value: PackEntry[]; declarations: ReadonlyMap<string, ScanSourceDeclaration> }
137
+ | { ok: false }
138
+ > {
74
139
  const result = await scanInventory(roots);
75
140
  if (!result.ok) {
76
141
  emitError(ctx, {
@@ -102,6 +167,8 @@ export async function scanEntries(
102
167
  guid: asset.guid,
103
168
  kind: asset.kind,
104
169
  sourcePath,
170
+ sourceIndex: asset.sourceIndex,
171
+ sourceRevision: declaration.sourceRevision,
105
172
  ...(name === undefined ? {} : { name }),
106
173
  });
107
174
  }
@@ -137,6 +204,11 @@ export async function scanEntries(
137
204
  guid: inventoryEntry.guid,
138
205
  kind: inventoryEntry.kind,
139
206
  sourcePath: inventoryEntry.sourcePath,
207
+ sourceRevision: inventoryEntry.sourceRevision,
208
+ ...(inventoryEntry.sourceKey === undefined ? {} : { sourceKey: inventoryEntry.sourceKey }),
209
+ ...(inventoryEntry.sourceIndex === undefined
210
+ ? {}
211
+ : { sourceIndex: inventoryEntry.sourceIndex }),
140
212
  ...(name === undefined ? {} : { name }),
141
213
  ...(declaration?.format === 'pack.json' &&
142
214
  declaration.value.schemaVersion === '3.0.0' &&
@@ -149,7 +221,7 @@ export async function scanEntries(
149
221
  entries.sort((left, right) =>
150
222
  `${left.sourcePath}:${left.guid}`.localeCompare(`${right.sourcePath}:${right.guid}`),
151
223
  );
152
- return { ok: true, value: entries };
224
+ return { ok: true, value: entries, declarations: result.value.declarations };
153
225
  }
154
226
 
155
227
  function helpBody(): string {
@@ -160,6 +232,7 @@ function helpBody(): string {
160
232
  ' forgeax asset list --root <project> --json',
161
233
  ' forgeax asset inspect --subject <guid> --root <project> --json',
162
234
  ' forgeax asset verify --root <project> --json',
235
+ ' output is bounded to 256 assets and includes the scanned source scope',
163
236
  ' forgeax asset inspect --subject <source.pack.ts> --root <project> --json',
164
237
  ' forgeax asset atlas --input <glob> --name <prefix> --root <project> [--output <dir>]',
165
238
  '',
@@ -342,6 +415,89 @@ async function runLookup(rest: string[], ctx: AssetCtx): Promise<number> {
342
415
  return 0;
343
416
  }
344
417
 
418
+ function artifactPaths(value: unknown): readonly string[] {
419
+ if (!isRecord(value)) return [];
420
+ return Object.values(value).flatMap((descriptor) =>
421
+ isRecord(descriptor) && typeof descriptor.path === 'string' ? [descriptor.path] : [],
422
+ );
423
+ }
424
+
425
+ function unknownVerificationFacts(): VerificationFacts {
426
+ return {
427
+ dependencies: [],
428
+ output: {
429
+ status: 'unknown',
430
+ availability: 'unknown',
431
+ freshness: 'unknown',
432
+ },
433
+ producer: { state: 'unknown' },
434
+ };
435
+ }
436
+
437
+ function sourceVerificationFacts(
438
+ entry: PackEntry,
439
+ declaration: ScanSourceDeclaration | undefined,
440
+ ): VerificationFacts {
441
+ if (declaration === undefined) return unknownVerificationFacts();
442
+ if (declaration.format === 'meta.json' || declaration.format === 'pack.ts') {
443
+ return {
444
+ dependencies: [],
445
+ output: {
446
+ status: 'unproduced',
447
+ availability: 'unknown',
448
+ freshness: 'unknown',
449
+ },
450
+ producer: { state: 'not-run' },
451
+ };
452
+ }
453
+
454
+ if (declaration.value.schemaVersion === '3.0.0') {
455
+ const parsed = parsePackSourceJson(declaration.value);
456
+ if (!parsed.ok || parsed.value.format !== 'direct' || entry.sourceKey === undefined) {
457
+ return {
458
+ dependencies: [],
459
+ output: {
460
+ status: 'unproduced',
461
+ availability: 'unknown',
462
+ freshness: 'unknown',
463
+ },
464
+ producer: { state: 'not-run' },
465
+ };
466
+ }
467
+ const asset = parsed.value.assets[entry.sourceKey];
468
+ return {
469
+ dependencies: asset?.refs ?? [],
470
+ output: {
471
+ status: 'unproduced',
472
+ availability: 'unknown',
473
+ freshness: 'unknown',
474
+ },
475
+ producer: { state: 'not-run' },
476
+ };
477
+ }
478
+
479
+ const asset = declaration.value.assets.find(
480
+ (candidate) => candidate.guid.toLowerCase() === entry.guid.toLowerCase(),
481
+ );
482
+ if (asset === undefined || (asset.execution !== 'direct' && asset.execution !== 'cooked')) {
483
+ return unknownVerificationFacts();
484
+ }
485
+ return {
486
+ dependencies: asset.refs,
487
+ output: {
488
+ // A validated legacy package row is the only published evidence this
489
+ // offline command reads. Artifact bytes and cook receipts remain
490
+ // producer-owned, so availability/freshness stay explicitly unknown.
491
+ status: 'produced',
492
+ availability: 'unknown',
493
+ freshness: 'unknown',
494
+ packagePath: entry.sourcePath,
495
+ ...(asset.artifacts === undefined ? {} : { artifactPaths: artifactPaths(asset.artifacts) }),
496
+ },
497
+ producer: { state: 'published' },
498
+ };
499
+ }
500
+
345
501
  async function runVerify(rest: string[], ctx: AssetCtx): Promise<number> {
346
502
  if (rest.some((value) => value === '--guid')) return runEvidence(rest, ctx);
347
503
  try {
@@ -363,10 +519,76 @@ async function runVerify(rest: string[], ctx: AssetCtx): Promise<number> {
363
519
  const cwd = ctx.cwd ?? process.cwd();
364
520
  const result = await scanEntries([cwd], ctx);
365
521
  if (!result.ok) return 1;
522
+ ctx.stdoutWrite(JSON.stringify(createAssetVerificationReport(cwd, result)));
523
+ return 0;
524
+ }
366
525
 
526
+ /** Build the bounded provenance projection from one validated scanner pass. */
527
+ export function createAssetVerificationReport(
528
+ root: string,
529
+ result: {
530
+ readonly value: readonly PackEntry[];
531
+ readonly declarations: ReadonlyMap<string, ScanSourceDeclaration>;
532
+ },
533
+ ): AssetVerificationReport {
534
+ const declarations = result.declarations;
535
+ const sourcePaths = [...declarations.keys()].sort();
536
+ const scriptablePackSources = [...declarations.values()]
537
+ .filter(
538
+ (declaration): declaration is Extract<ScanSourceDeclaration, { format: 'pack.ts' }> =>
539
+ declaration.format === 'pack.ts',
540
+ )
541
+ .sort((left, right) => left.sourcePath.localeCompare(right.sourcePath));
542
+ const emittedEntries = result.value.slice(0, ASSET_VERIFY_LIMIT);
543
+ const facts = emittedEntries.map((entry) =>
544
+ sourceVerificationFacts(entry, declarations.get(entry.sourcePath)),
545
+ );
367
546
  const materialCount = result.value.filter((e) => e.kind === 'material').length;
368
- ctx.stdoutWrite(`material-validated: ${materialCount}`);
369
- return 0;
547
+ return {
548
+ schemaVersion: 'asset-verification-v1',
549
+ root,
550
+ scope: {
551
+ sourceCount: sourcePaths.length,
552
+ assetCount: result.value.length,
553
+ sourcePaths: sourcePaths.slice(0, ASSET_VERIFY_LIMIT),
554
+ omittedSourceCount: Math.max(0, sourcePaths.length - ASSET_VERIFY_LIMIT),
555
+ assetLimit: ASSET_VERIFY_LIMIT,
556
+ truncated: result.value.length > ASSET_VERIFY_LIMIT,
557
+ scriptablePackSourceCount: scriptablePackSources.length,
558
+ scriptablePackSources: scriptablePackSources.slice(0, ASSET_VERIFY_LIMIT).map((source) => ({
559
+ sourcePath: source.sourcePath,
560
+ packageId: source.value.packageId,
561
+ output: 'unproduced' as const,
562
+ reason: 'producer-not-run' as const,
563
+ })),
564
+ omittedScriptablePackSourceCount: Math.max(
565
+ 0,
566
+ scriptablePackSources.length - ASSET_VERIFY_LIMIT,
567
+ ),
568
+ },
569
+ assets: emittedEntries.map((entry, index) => ({
570
+ guid: entry.guid,
571
+ type: entry.kind,
572
+ source: {
573
+ path: entry.sourcePath,
574
+ format: declarations.get(entry.sourcePath)?.format ?? 'pack.json',
575
+ role: 'author' as const,
576
+ ...(entry.sourceRevision === undefined ? {} : { revision: entry.sourceRevision }),
577
+ },
578
+ ...(facts[index] as VerificationFacts),
579
+ ...(entry.name === undefined ? {} : { name: entry.name }),
580
+ ...(entry.sourceKey === undefined ? {} : { sourceKey: entry.sourceKey }),
581
+ ...(entry.sourceIndex === undefined ? {} : { sourceIndex: entry.sourceIndex }),
582
+ })),
583
+ summary: {
584
+ assetCount: result.value.length,
585
+ emittedAssetCount: emittedEntries.length,
586
+ materialCount,
587
+ unproducedAssetCount: facts.filter((value) => value.output.status === 'unproduced').length,
588
+ unknownAssetCount: facts.filter((value) => value.output.status === 'unknown').length,
589
+ unmaterializedScriptablePackCount: scriptablePackSources.length,
590
+ },
591
+ };
370
592
  }
371
593
 
372
594
  interface EvidenceCliOptions {
@@ -93,6 +93,19 @@ function transactionFailure(key: string, reason: string): Result<never, AssetSta
93
93
  return failure(key, { guid: 'unknown', producer: reason });
94
94
  }
95
95
 
96
+ function thrownProducer(error: unknown): string {
97
+ if (error instanceof Error) return error.message;
98
+ if (typeof error === 'string') return error;
99
+ if (error !== null && typeof error === 'object') {
100
+ try {
101
+ return JSON.stringify(error);
102
+ } catch {
103
+ return Object.prototype.toString.call(error);
104
+ }
105
+ }
106
+ return String(error);
107
+ }
108
+
96
109
  function recovered<P>(
97
110
  key: string,
98
111
  previous: NativeCookTransactionSnapshot<P>,
@@ -142,7 +155,7 @@ export class NativeCookerRegistry {
142
155
  const typedInput = cooker.discover === undefined ? input : await cooker.discover(input);
143
156
  draft = await cooker.cook(typedInput);
144
157
  } catch (error) {
145
- return failure(key, { guid: 'unknown', producer: String(error) });
158
+ return failure(key, { guid: 'unknown', producer: thrownProducer(error) });
146
159
  }
147
160
  if (
148
161
  draft.guid.length === 0 ||