@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.
- package/README.md +2 -2
- package/dist/build.d.ts +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.mjs +252 -22
- package/dist/build.mjs.map +1 -1
- package/dist/cli-asset.d.ts +63 -0
- package/dist/cli-asset.d.ts.map +1 -1
- package/dist/cli-asset.mjs +142 -4
- package/dist/cli-asset.mjs.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/native-cooker-registry.d.ts.map +1 -1
- package/dist/native-cooker.mjs +13 -1
- package/dist/native-cooker.mjs.map +1 -1
- package/dist/pack-authoring-node.d.ts +6 -0
- package/dist/pack-authoring-node.d.ts.map +1 -1
- package/dist/pack-authoring-node.mjs +230 -16
- package/dist/pack-authoring-node.mjs.map +1 -1
- package/dist/pack-authoring.d.ts +26 -0
- package/dist/pack-authoring.d.ts.map +1 -1
- package/dist/pack-authoring.mjs.map +1 -1
- package/dist/runtime-publication.d.ts +8 -0
- package/dist/runtime-publication.d.ts.map +1 -1
- package/dist/scanner.d.ts +1 -1
- package/dist/scanner.mjs +5 -5
- package/dist/scanner.mjs.map +1 -1
- package/dist/scriptable-pack-node.mjs.map +1 -1
- package/dist/scriptable-pack.mjs.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/cli.unit.test.ts +87 -0
- package/src/__tests__/native-cooker-registry.test.ts +16 -0
- package/src/__tests__/pack-authoring-gateway.unit.test.ts +120 -1
- package/src/__tests__/pack.unit.test.ts +6 -6
- package/src/__tests__/runtime-publication.unit.test.ts +27 -1
- package/src/build.ts +2 -0
- package/src/cli-asset.ts +227 -5
- package/src/native-cooker-registry.ts +14 -1
- package/src/pack-authoring-node.ts +278 -13
- package/src/pack-authoring.ts +26 -0
- package/src/runtime-publication.ts +44 -0
- 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).
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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 {
|
|
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<
|
|
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
|
-
|
|
369
|
-
|
|
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:
|
|
158
|
+
return failure(key, { guid: 'unknown', producer: thrownProducer(error) });
|
|
146
159
|
}
|
|
147
160
|
if (
|
|
148
161
|
draft.guid.length === 0 ||
|