@forgeax/engine-pack 0.1.27 → 0.1.29
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 +30 -17
- package/dist/build.mjs +35 -52
- package/dist/build.mjs.map +1 -1
- package/dist/cli-asset.d.ts +1 -1
- package/dist/cli-asset.d.ts.map +1 -1
- package/dist/cli-asset.mjs +40 -33
- package/dist/cli-asset.mjs.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/inventory/declaration.d.ts +1 -1
- package/dist/inventory/declaration.d.ts.map +1 -1
- package/dist/inventory/sync.d.ts.map +1 -1
- package/dist/native-cooker-registry.d.ts +1 -0
- package/dist/native-cooker-registry.d.ts.map +1 -1
- package/dist/native-cooker.mjs.map +1 -1
- package/dist/pack-authoring-node.mjs +22 -13
- package/dist/pack-authoring-node.mjs.map +1 -1
- package/dist/runtime.mjs +1 -1
- package/dist/runtime.mjs.map +1 -1
- package/dist/scanner.mjs +22 -13
- package/dist/scanner.mjs.map +1 -1
- package/dist/schema-compiled.d.ts.map +1 -1
- package/dist/schema.mjs +41 -63
- package/dist/schema.mjs.map +1 -1
- package/dist/scriptable-pack-node.d.ts.map +1 -1
- package/dist/scriptable-pack-node.mjs +11 -3
- package/dist/scriptable-pack-node.mjs.map +1 -1
- package/dist/scriptable-pack-worker.mjs +5 -1
- package/dist/scriptable-pack-worker.mjs.map +1 -1
- package/package.json +3 -6
- package/schema/meta.schema.json +1 -1
- package/src/__tests__/atlas-cli-region-mismatch.test.ts +1 -1
- package/src/__tests__/cli.unit.test.ts +1 -1
- package/src/__tests__/inventory-schema.test.ts +10 -10
- package/src/__tests__/pack-authoring.unit.test.ts +2 -4
- package/src/__tests__/pack.unit.test.ts +89 -127
- package/src/__tests__/scanner-instance-cycle.test.ts +92 -0
- package/src/__tests__/scanner-inventory.contract.test.ts +2 -2
- package/src/__tests__/scriptable-pack-diagnostic.integration.test.ts +45 -0
- package/src/atlas/run-atlas.ts +3 -3
- package/src/atlas/shelf-pack.ts +1 -1
- package/src/atlas/upng-ambient.d.ts +1 -1
- package/src/cli-asset.ts +22 -23
- package/src/inventory/binding.ts +3 -3
- package/src/inventory/declaration.ts +10 -48
- package/src/inventory/sync.ts +3 -2
- package/src/native-cooker-registry.ts +1 -0
- package/src/scanner.ts +29 -29
- package/src/schema-compiled.ts +40 -79
- package/src/scriptable-pack-node.ts +11 -3
- package/src/scriptable-pack-worker.ts +8 -1
- package/src/__tests__/scanner-mount-cycle.test.ts +0 -232
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Scanner dependency-cycle coverage for keyed SceneAsset instances.
|
|
2
|
+
|
|
3
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
7
|
+
import { scan } from '../scanner.js';
|
|
8
|
+
|
|
9
|
+
const GUID_A = 'aa000000-0000-4000-8000-000000000aaa';
|
|
10
|
+
const GUID_B = 'bb000000-0000-4000-8000-000000000bbb';
|
|
11
|
+
const GUID_C = 'cc000000-0000-4000-8000-000000000ccc';
|
|
12
|
+
|
|
13
|
+
function sceneAssetPack(guid: string, source?: string): unknown {
|
|
14
|
+
return {
|
|
15
|
+
schemaVersion: '1.0.0',
|
|
16
|
+
kind: 'internal-text-package',
|
|
17
|
+
assets: [
|
|
18
|
+
{
|
|
19
|
+
guid,
|
|
20
|
+
kind: 'scene',
|
|
21
|
+
refs: source === undefined ? [] : [source],
|
|
22
|
+
payload: {
|
|
23
|
+
kind: 'scene',
|
|
24
|
+
entities:
|
|
25
|
+
source === undefined
|
|
26
|
+
? { root: { components: {} } }
|
|
27
|
+
: { child: { components: {}, instance: { source: 0 } } },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('scanner keyed SceneAsset instance cycle detection', () => {
|
|
35
|
+
let dir: string;
|
|
36
|
+
|
|
37
|
+
beforeEach(async () => {
|
|
38
|
+
dir = await mkdtemp(join(tmpdir(), 'pack-instance-cycle-'));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(async () => {
|
|
42
|
+
await rm(dir, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('accepts an acyclic instance chain A -> B -> C', async () => {
|
|
46
|
+
await writeFile(
|
|
47
|
+
join(dir, 'a.pack.json'),
|
|
48
|
+
JSON.stringify(sceneAssetPack(GUID_A, GUID_B)),
|
|
49
|
+
'utf8',
|
|
50
|
+
);
|
|
51
|
+
await writeFile(
|
|
52
|
+
join(dir, 'b.pack.json'),
|
|
53
|
+
JSON.stringify(sceneAssetPack(GUID_B, GUID_C)),
|
|
54
|
+
'utf8',
|
|
55
|
+
);
|
|
56
|
+
await writeFile(join(dir, 'c.pack.json'), JSON.stringify(sceneAssetPack(GUID_C)), 'utf8');
|
|
57
|
+
expect((await scan([dir])).ok).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it.each([
|
|
61
|
+
['two', [GUID_A, GUID_B]],
|
|
62
|
+
['three', [GUID_A, GUID_B, GUID_C]],
|
|
63
|
+
])('rejects a %s-asset instance cycle', async (_name, cycle) => {
|
|
64
|
+
const [a, b, c] = cycle;
|
|
65
|
+
await writeFile(join(dir, 'a.pack.json'), JSON.stringify(sceneAssetPack(a, b)), 'utf8');
|
|
66
|
+
await writeFile(join(dir, 'b.pack.json'), JSON.stringify(sceneAssetPack(b, c ?? a)), 'utf8');
|
|
67
|
+
if (c !== undefined)
|
|
68
|
+
await writeFile(join(dir, 'c.pack.json'), JSON.stringify(sceneAssetPack(c, a)), 'utf8');
|
|
69
|
+
const result = await scan([dir]);
|
|
70
|
+
expect(result.ok).toBe(false);
|
|
71
|
+
if (result.ok) return;
|
|
72
|
+
expect(result.error.code).toBe('pack-cyclic-reference');
|
|
73
|
+
const detail = result.error.detail as { kind?: string; cycle?: readonly string[] };
|
|
74
|
+
expect(detail.kind).toBe('mount-asset');
|
|
75
|
+
const found = detail.cycle ?? [];
|
|
76
|
+
expect(found[0]).toBe(found[found.length - 1]);
|
|
77
|
+
for (const guid of cycle) expect(found).toContain(guid);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('rejects a self-instance', async () => {
|
|
81
|
+
await writeFile(
|
|
82
|
+
join(dir, 'a.pack.json'),
|
|
83
|
+
JSON.stringify(sceneAssetPack(GUID_A, GUID_A)),
|
|
84
|
+
'utf8',
|
|
85
|
+
);
|
|
86
|
+
const result = await scan([dir]);
|
|
87
|
+
expect(result.ok).toBe(false);
|
|
88
|
+
if (result.ok) return;
|
|
89
|
+
expect(result.error.code).toBe('pack-cyclic-reference');
|
|
90
|
+
expect((result.error.detail as { kind?: string }).kind).toBe('mount-asset');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -95,7 +95,7 @@ describe('engine-pack scanner inventory contract', () => {
|
|
|
95
95
|
default: {
|
|
96
96
|
schemaVersion: '2.0.0',
|
|
97
97
|
packageId: scriptablePackageId,
|
|
98
|
-
build: () => ({ ok: true, value: { effect: { kind: 'scene', entities:
|
|
98
|
+
build: () => ({ ok: true, value: { effect: { kind: 'scene', entities: {} } } }),
|
|
99
99
|
},
|
|
100
100
|
}),
|
|
101
101
|
},
|
|
@@ -143,7 +143,7 @@ describe('engine-pack scanner inventory contract', () => {
|
|
|
143
143
|
default: {
|
|
144
144
|
schemaVersion: '2.0.0',
|
|
145
145
|
packageId,
|
|
146
|
-
build: () => ({ ok: true, value: { effect: { kind: 'scene', entities:
|
|
146
|
+
build: () => ({ ok: true, value: { effect: { kind: 'scene', entities: {} } } }),
|
|
147
147
|
},
|
|
148
148
|
}),
|
|
149
149
|
dispose: async () => {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { expect, it } from 'vitest';
|
|
5
|
+
import { loadScriptablePack } from '../scriptable-pack-node.js';
|
|
6
|
+
|
|
7
|
+
it('keeps the source line and column for a malformed author module', async () => {
|
|
8
|
+
const root = await mkdtemp(join(tmpdir(), 'pack-source-diagnostic-'));
|
|
9
|
+
const source = join(root, 'broken.pack.ts');
|
|
10
|
+
try {
|
|
11
|
+
await writeFile(source, 'export default {\n build: () => { const = 1; }\n};\n');
|
|
12
|
+
const loaded = await loadScriptablePack(source);
|
|
13
|
+
expect(loaded.ok).toBe(false);
|
|
14
|
+
if (loaded.ok) return;
|
|
15
|
+
expect(JSON.stringify(loaded.error)).toContain(`${source}:2:`);
|
|
16
|
+
} finally {
|
|
17
|
+
await rm(root, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it.each([
|
|
22
|
+
0, 17,
|
|
23
|
+
])('propagates worker exit %i during a build instead of waiting for timeout', async (code) => {
|
|
24
|
+
const root = await mkdtemp(join(tmpdir(), 'pack-worker-exit-'));
|
|
25
|
+
const source = join(root, 'exit.pack.ts');
|
|
26
|
+
try {
|
|
27
|
+
await writeFile(
|
|
28
|
+
source,
|
|
29
|
+
`export default { schemaVersion: '2.0.0', packageId: new Uint8Array([121,171,30,178,252,222,65,195,145,119,233,61,213,193,142,245]), build() { process.exit(${code}); } };`,
|
|
30
|
+
);
|
|
31
|
+
const loaded = await loadScriptablePack(source, { buildTimeoutMs: 1000 });
|
|
32
|
+
expect(loaded.ok).toBe(true);
|
|
33
|
+
if (!loaded.ok) return;
|
|
34
|
+
await expect(
|
|
35
|
+
loaded.value.build({
|
|
36
|
+
packageId: loaded.value.packageId,
|
|
37
|
+
readByGuid: async () => {
|
|
38
|
+
throw new Error('unexpected read');
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
).rejects.toThrow(`worker exited with code ${code}`);
|
|
42
|
+
} finally {
|
|
43
|
+
await rm(root, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
});
|
package/src/atlas/run-atlas.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `forgeax
|
|
1
|
+
// `forgeax asset atlas` subcommand backend
|
|
2
2
|
// (feat-20260521-sprite-atlas-animation M5' T-32). Replaces the v1 vite-
|
|
3
3
|
// plugin idiom (PR #190 deleted `@forgeax/engine-vite-plugin-image`) with
|
|
4
4
|
// a one-shot Node CLI that reads a glob of PNGs, runs the pure shelf
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
//
|
|
8
8
|
// Why CLI not Vite plugin (PR #190 architectural pivot):
|
|
9
9
|
// - asset importing is producer-side disk work; v2 routes the producer
|
|
10
|
-
// through the
|
|
10
|
+
// through the unified `forgeax asset atlas` producer so the
|
|
11
11
|
// same surface composes with `scan` / `lookup` / `verify` (charter P5
|
|
12
12
|
// producer/consumer split).
|
|
13
13
|
// - vite plugins reach for runtime-coupled hooks (buildStart / emitFile);
|
|
@@ -327,7 +327,7 @@ export async function runAtlas(rest: string[], ctx: AssetCtx): Promise<number> {
|
|
|
327
327
|
return emitError(ctx, {
|
|
328
328
|
code: 'atlas-empty-input',
|
|
329
329
|
expected:
|
|
330
|
-
'forgeax
|
|
330
|
+
'forgeax asset atlas --input <glob> --name <prefix> [--output <dir>] [--max-atlas-size <n>]',
|
|
331
331
|
hint: argsOutcome.message,
|
|
332
332
|
detail: { receivedCount: 0 },
|
|
333
333
|
});
|
package/src/atlas/shelf-pack.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Pure-function shelf packer for the `forgeax
|
|
1
|
+
// Pure-function shelf packer for the `forgeax asset atlas`
|
|
2
2
|
// subcommand (feat-20260521-sprite-atlas-animation M5' T-31). Maps a list
|
|
3
3
|
// of decoded RGBA sprites to a non-overlapping atlas region map +
|
|
4
4
|
// atlasWidth / atlasHeight envelope.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Ambient declaration for `upng-js` (PNG decoder/encoder) used by the
|
|
2
|
-
// `forgeax
|
|
2
|
+
// `forgeax asset atlas` subcommand (run-atlas.ts) and by
|
|
3
3
|
// its hermetic CLI integration tests under `__tests__/`. The package
|
|
4
4
|
// ships without bundled `.d.ts`; consumer sites cast through local
|
|
5
5
|
// interfaces to match the subset they use.
|
package/src/cli-asset.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// @forgeax/engine-pack/src/cli-asset —
|
|
4
|
-
//
|
|
5
|
-
// section 2.9). Discovered by the base bin via the kubectl 4th-path
|
|
6
|
-
// `forgeax-engine-remote-` prefix scanner; subcommands scan / lookup /
|
|
7
|
-
// verify operate offline against the pack scanner.
|
|
3
|
+
// @forgeax/engine-pack/src/cli-asset — Pack producer implementation used by
|
|
4
|
+
// DevKit's unified `forgeax asset` command. It is not a package bin.
|
|
8
5
|
//
|
|
9
6
|
// stderr contract (plan-strategy section 2.3 weak-contract): every error
|
|
10
7
|
// path emits a single JSON Lines record carrying `code` / `expected` /
|
|
@@ -30,6 +27,10 @@ import { projectScriptablePackMeta } from './pack-authoring.js';
|
|
|
30
27
|
import { type ScanSourceDeclaration, scanInventory } from './scanner.js';
|
|
31
28
|
import { loadScriptablePack } from './scriptable-pack-node.js';
|
|
32
29
|
|
|
30
|
+
// The unified DevKit command tree reuses the producer implementation without
|
|
31
|
+
// reviving the historical standalone executable as a second public surface.
|
|
32
|
+
export { runAtlas } from './atlas/run-atlas.js';
|
|
33
|
+
|
|
33
34
|
export interface PackEntry {
|
|
34
35
|
readonly guid: string;
|
|
35
36
|
readonly kind: string;
|
|
@@ -153,16 +154,14 @@ export async function scanEntries(
|
|
|
153
154
|
|
|
154
155
|
function helpBody(): string {
|
|
155
156
|
return [
|
|
156
|
-
'forgeax
|
|
157
|
+
'forgeax asset — offline pack scanner / lookup / verifier / atlas builder',
|
|
157
158
|
'',
|
|
158
159
|
'Usage:',
|
|
159
|
-
' forgeax
|
|
160
|
-
' forgeax
|
|
161
|
-
' forgeax
|
|
162
|
-
' forgeax
|
|
163
|
-
' forgeax
|
|
164
|
-
' forgeax-engine-remote-asset verify --guid <guid> --project <dir> --catalog <path> --json',
|
|
165
|
-
' forgeax-engine-remote-asset atlas --input <glob> --name <prefix> [--output <dir>] [--max-atlas-size <n>]',
|
|
160
|
+
' forgeax asset list --root <project> --json',
|
|
161
|
+
' forgeax asset inspect --subject <guid> --root <project> --json',
|
|
162
|
+
' forgeax asset verify --root <project> --json',
|
|
163
|
+
' forgeax asset inspect --subject <source.pack.ts> --root <project> --json',
|
|
164
|
+
' forgeax asset atlas --input <glob> --name <prefix> --root <project> [--output <dir>]',
|
|
166
165
|
'',
|
|
167
166
|
'AI recovery commands (host execution remains explicit):',
|
|
168
167
|
' inspect [--guid <guid>] read subject, execution, lifecycle, authority, and sourceKey evidence',
|
|
@@ -205,7 +204,7 @@ export async function runCliAsset(rest: string[], ctx: AssetCtx): Promise<number
|
|
|
205
204
|
return emitError(ctx, {
|
|
206
205
|
code: 'unknown-subcommand',
|
|
207
206
|
expected: 'subcommand in {scan, lookup, verify, atlas, meta}',
|
|
208
|
-
hint: "run 'forgeax
|
|
207
|
+
hint: "run 'forgeax help asset' for usage",
|
|
209
208
|
detail: { subcommand: sub },
|
|
210
209
|
});
|
|
211
210
|
}
|
|
@@ -227,7 +226,7 @@ async function runMeta(rest: string[], ctx: AssetCtx): Promise<number> {
|
|
|
227
226
|
} catch (error) {
|
|
228
227
|
return emitError(ctx, {
|
|
229
228
|
code: 'cli-parse-error',
|
|
230
|
-
expected: 'forgeax
|
|
229
|
+
expected: 'forgeax asset inspect --subject <source.pack.ts> --root <project> --json',
|
|
231
230
|
hint: 'pass one trusted ScriptablePack module path',
|
|
232
231
|
detail: { message: error instanceof Error ? error.message : String(error) },
|
|
233
232
|
});
|
|
@@ -277,7 +276,7 @@ async function runRecoveryCommand(
|
|
|
277
276
|
return emitError(ctx, {
|
|
278
277
|
code: 'cli-parse-error',
|
|
279
278
|
expected: `${operation} [--guid <guid>]`,
|
|
280
|
-
hint: "run 'forgeax
|
|
279
|
+
hint: "run 'forgeax help asset' for usage",
|
|
281
280
|
detail: { message: error instanceof Error ? error.message : String(error) },
|
|
282
281
|
});
|
|
283
282
|
}
|
|
@@ -297,8 +296,8 @@ async function runScan(rest: string[], ctx: AssetCtx): Promise<number> {
|
|
|
297
296
|
const message = e instanceof Error ? e.message : String(e);
|
|
298
297
|
return emitError(ctx, {
|
|
299
298
|
code: 'cli-parse-error',
|
|
300
|
-
expected: 'forgeax
|
|
301
|
-
hint: "run 'forgeax
|
|
299
|
+
expected: 'forgeax asset list --root <project> --json',
|
|
300
|
+
hint: "run 'forgeax help asset' for usage",
|
|
302
301
|
detail: { message },
|
|
303
302
|
});
|
|
304
303
|
}
|
|
@@ -314,7 +313,7 @@ async function runLookup(rest: string[], ctx: AssetCtx): Promise<number> {
|
|
|
314
313
|
if (typeof guid !== 'string') {
|
|
315
314
|
return emitError(ctx, {
|
|
316
315
|
code: 'cli-parse-error',
|
|
317
|
-
expected: 'forgeax
|
|
316
|
+
expected: 'forgeax asset inspect --subject <guid> --root <project> --json',
|
|
318
317
|
hint: 'pass a 36-char dash-form UUID positional argument',
|
|
319
318
|
});
|
|
320
319
|
}
|
|
@@ -356,8 +355,8 @@ async function runVerify(rest: string[], ctx: AssetCtx): Promise<number> {
|
|
|
356
355
|
const message = e instanceof Error ? e.message : String(e);
|
|
357
356
|
return emitError(ctx, {
|
|
358
357
|
code: 'cli-parse-error',
|
|
359
|
-
expected: 'forgeax
|
|
360
|
-
hint: "run 'forgeax
|
|
358
|
+
expected: 'forgeax asset verify --root <project> --json',
|
|
359
|
+
hint: "run 'forgeax help asset' for usage",
|
|
361
360
|
detail: { message },
|
|
362
361
|
});
|
|
363
362
|
}
|
|
@@ -397,7 +396,7 @@ function parseEvidenceOptions(rest: string[], ctx: AssetCtx): EvidenceCliOptions
|
|
|
397
396
|
emitError(ctx, {
|
|
398
397
|
code: 'cli-parse-error',
|
|
399
398
|
expected: '--guid <guid> --project <dir> --catalog <path> --json',
|
|
400
|
-
hint: "run 'forgeax
|
|
399
|
+
hint: "run 'forgeax help asset' for usage",
|
|
401
400
|
});
|
|
402
401
|
return undefined;
|
|
403
402
|
}
|
|
@@ -407,7 +406,7 @@ function parseEvidenceOptions(rest: string[], ctx: AssetCtx): EvidenceCliOptions
|
|
|
407
406
|
emitError(ctx, {
|
|
408
407
|
code: 'cli-parse-error',
|
|
409
408
|
expected: '--guid <guid> --project <dir> --catalog <path> --json',
|
|
410
|
-
hint: "run 'forgeax
|
|
409
|
+
hint: "run 'forgeax help asset' for usage",
|
|
411
410
|
detail: { message },
|
|
412
411
|
});
|
|
413
412
|
return undefined;
|
package/src/inventory/binding.ts
CHANGED
|
@@ -15,11 +15,11 @@ export function projectAssetRefs(inventory: AuthorInventory): readonly AssetRef[
|
|
|
15
15
|
/** Derive instance-relative scene refs from the same validated author rows. */
|
|
16
16
|
export function projectSceneEntityRefs(inventory: AuthorInventory): readonly SceneEntityRef[] {
|
|
17
17
|
return inventory.declarations.flatMap((row) =>
|
|
18
|
-
row.
|
|
18
|
+
row.sceneEntityKeys === undefined
|
|
19
19
|
? []
|
|
20
|
-
: row.
|
|
20
|
+
: row.sceneEntityKeys.map((address) => ({
|
|
21
21
|
sceneSourceKey: row.sourceKey,
|
|
22
|
-
|
|
22
|
+
address,
|
|
23
23
|
})),
|
|
24
24
|
);
|
|
25
25
|
}
|
|
@@ -6,7 +6,7 @@ export interface AuthorInventoryRow {
|
|
|
6
6
|
readonly kind: string;
|
|
7
7
|
readonly payload: Readonly<Record<string, unknown>>;
|
|
8
8
|
readonly refs: readonly string[];
|
|
9
|
-
readonly
|
|
9
|
+
readonly sceneEntityKeys?: readonly string[];
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export interface AuthorInventory {
|
|
@@ -102,54 +102,16 @@ export function validateAuthorInventory(value: unknown): Result<AuthorInventory,
|
|
|
102
102
|
{ guid, sourceKey },
|
|
103
103
|
);
|
|
104
104
|
}
|
|
105
|
-
if (row.kind === 'scene') {
|
|
106
|
-
const payload = row.payload;
|
|
107
|
-
const payloadRecord =
|
|
108
|
-
typeof payload === 'object' && payload !== null
|
|
109
|
-
? (payload as Record<string, unknown>)
|
|
110
|
-
: undefined;
|
|
111
|
-
const entities =
|
|
112
|
-
payloadRecord !== undefined && Array.isArray(payloadRecord.entities)
|
|
113
|
-
? payloadRecord.entities
|
|
114
|
-
: [];
|
|
115
|
-
const bindings = new Set<string>();
|
|
116
|
-
for (const entity of entities) {
|
|
117
|
-
const bindingKey =
|
|
118
|
-
typeof entity === 'object' && entity !== null && typeof entity.bindingKey === 'string'
|
|
119
|
-
? entity.bindingKey
|
|
120
|
-
: undefined;
|
|
121
|
-
if (bindingKey !== undefined && bindingKey.length === 0) {
|
|
122
|
-
return failure(
|
|
123
|
-
'inventory-scene-binding-missing',
|
|
124
|
-
'each declared scene bindingKey is non-empty',
|
|
125
|
-
'remove the empty bindingKey or declare a stable key in the scene producer',
|
|
126
|
-
{ sourceKey },
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
if (bindingKey === undefined) continue;
|
|
130
|
-
if (bindings.has(bindingKey)) {
|
|
131
|
-
return failure(
|
|
132
|
-
'inventory-scene-binding-duplicate',
|
|
133
|
-
'bindingKey values are unique within one scene',
|
|
134
|
-
'rename the duplicate bindingKey in the scene producer',
|
|
135
|
-
{ sourceKey },
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
bindings.add(bindingKey);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
105
|
guids.add(normalizedGuid);
|
|
142
106
|
sourceKeys.add(sourceKey);
|
|
143
|
-
const
|
|
144
|
-
row.kind
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
)
|
|
152
|
-
: undefined;
|
|
107
|
+
const sceneEntityKeys = (() => {
|
|
108
|
+
if (row.kind !== 'scene' || typeof row.payload !== 'object' || row.payload === null)
|
|
109
|
+
return undefined;
|
|
110
|
+
const entities = (row.payload as { readonly entities?: unknown }).entities;
|
|
111
|
+
if (entities === null || typeof entities !== 'object' || Array.isArray(entities))
|
|
112
|
+
return undefined;
|
|
113
|
+
return Object.keys(entities);
|
|
114
|
+
})();
|
|
153
115
|
declarations.push({
|
|
154
116
|
guid,
|
|
155
117
|
sourceKey,
|
|
@@ -161,7 +123,7 @@ export function validateAuthorInventory(value: unknown): Result<AuthorInventory,
|
|
|
161
123
|
refs: Array.isArray(row.refs)
|
|
162
124
|
? row.refs.filter((ref): ref is string => typeof ref === 'string')
|
|
163
125
|
: [],
|
|
164
|
-
...(
|
|
126
|
+
...(sceneEntityKeys === undefined ? {} : { sceneEntityKeys }),
|
|
165
127
|
});
|
|
166
128
|
}
|
|
167
129
|
return ok({ declarations });
|
package/src/inventory/sync.ts
CHANGED
|
@@ -5,7 +5,8 @@ export type InventoryDigest = string;
|
|
|
5
5
|
export function inventoryDigest(inventory: AuthorInventory): InventoryDigest {
|
|
6
6
|
return inventory.declarations
|
|
7
7
|
.map(
|
|
8
|
-
(row) =>
|
|
8
|
+
(row) =>
|
|
9
|
+
`${row.guid}\0${row.sourceKey}\0${row.kind}\0${row.sceneEntityKeys?.join('\0') ?? ''}`,
|
|
9
10
|
)
|
|
10
11
|
.sort()
|
|
11
12
|
.join('\n');
|
|
@@ -16,7 +17,7 @@ export function syncAuthorInventory(inventory: AuthorInventory): AuthorInventory
|
|
|
16
17
|
declarations: inventory.declarations.map((row) => ({
|
|
17
18
|
...row,
|
|
18
19
|
refs: [...row.refs],
|
|
19
|
-
...(row.
|
|
20
|
+
...(row.sceneEntityKeys === undefined ? {} : { sceneEntityKeys: [...row.sceneEntityKeys] }),
|
|
20
21
|
})),
|
|
21
22
|
};
|
|
22
23
|
}
|
|
@@ -20,6 +20,7 @@ export interface NativeCookDraft<P = unknown> {
|
|
|
20
20
|
readonly refs: readonly string[];
|
|
21
21
|
readonly artifacts: Readonly<Record<string, NativeCookArtifact>>;
|
|
22
22
|
readonly inputFingerprint: string;
|
|
23
|
+
readonly sourceDependencies?: readonly string[];
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export interface NativeCooker<P = unknown, I = unknown> {
|
package/src/scanner.ts
CHANGED
|
@@ -184,7 +184,7 @@ interface ScanCapture {
|
|
|
184
184
|
* Requirements §3.4 + §5 blacklist.
|
|
185
185
|
*
|
|
186
186
|
* Re-exported as `SCANNER_BLACKLIST` for cross-package reuse: the
|
|
187
|
-
* `forgeax
|
|
187
|
+
* `forgeax asset import --check` traversal (M4 / w21 +
|
|
188
188
|
* plan-strategy section 2.8 path b) walks the same set of source-orphan
|
|
189
189
|
* candidates as the scanner, so we share the single SSOT here.
|
|
190
190
|
*/
|
|
@@ -254,27 +254,35 @@ function makePackError(
|
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
/**
|
|
257
|
-
* For scene assets
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
* mounts with malformed `source` (out-of-range integer / non-integer) —
|
|
261
|
-
* those are caught by ajv schema validation upstream. The yielded GUIDs
|
|
262
|
-
* feed scanner step-6's mount-asset cycle DFS (D-1, R10).
|
|
257
|
+
* For keyed scene assets, return the lowercased GUID each nested instance
|
|
258
|
+
* source resolves to via `asset.refs[]` (or directly carries). The yielded GUIDs feed
|
|
259
|
+
* scanner step-6's scene dependency cycle DFS.
|
|
263
260
|
*/
|
|
264
|
-
function*
|
|
261
|
+
function* extractInstanceSourceGuids(asset: {
|
|
265
262
|
kind?: unknown;
|
|
266
263
|
payload?: unknown;
|
|
267
264
|
refs: readonly string[];
|
|
268
265
|
}): Generator<string> {
|
|
269
266
|
if (asset.kind !== 'scene') return;
|
|
270
|
-
const payload = asset.payload as {
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
267
|
+
const payload = asset.payload as { entities?: unknown } | undefined;
|
|
268
|
+
if (
|
|
269
|
+
!payload ||
|
|
270
|
+
payload.entities === null ||
|
|
271
|
+
typeof payload.entities !== 'object' ||
|
|
272
|
+
Array.isArray(payload.entities)
|
|
273
|
+
)
|
|
274
|
+
return;
|
|
275
|
+
for (const rawEntity of Object.values(payload.entities as Record<string, unknown>)) {
|
|
276
|
+
if (rawEntity === null || typeof rawEntity !== 'object' || Array.isArray(rawEntity)) continue;
|
|
277
|
+
const instance = (rawEntity as { instance?: unknown }).instance;
|
|
278
|
+
if (instance === null || typeof instance !== 'object' || Array.isArray(instance)) continue;
|
|
279
|
+
const source = (instance as { source?: unknown }).source;
|
|
280
|
+
const resolved =
|
|
281
|
+
typeof source === 'number' && Number.isInteger(source)
|
|
282
|
+
? asset.refs[source]
|
|
283
|
+
: typeof source === 'string'
|
|
284
|
+
? source
|
|
285
|
+
: undefined;
|
|
278
286
|
if (typeof resolved !== 'string') continue;
|
|
279
287
|
yield resolved.toLowerCase();
|
|
280
288
|
}
|
|
@@ -450,7 +458,7 @@ async function scanValidated(
|
|
|
450
458
|
guidToPath.set(normalizedGuid, packPath);
|
|
451
459
|
packRefs.set(normalizedGuid, [
|
|
452
460
|
...asset.refs.map((ref) => ref.toLowerCase()),
|
|
453
|
-
...
|
|
461
|
+
...extractInstanceSourceGuids(asset),
|
|
454
462
|
]);
|
|
455
463
|
}
|
|
456
464
|
}
|
|
@@ -563,20 +571,12 @@ async function scanValidated(
|
|
|
563
571
|
}
|
|
564
572
|
guidToPath.set(normalizedGuid, packPath);
|
|
565
573
|
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
// asset.refs[]. By the .pack.json convention mount.source is an
|
|
570
|
-
// integer index into the same asset.refs[], so the resolved GUID is
|
|
571
|
-
// already present in `existingRefs`; this defensive pass guarantees
|
|
572
|
-
// that any author-supplied mounts[] references participate in the
|
|
573
|
-
// cycle DFS even if the schema-emitter forgot to mirror them into
|
|
574
|
-
// refs[]. The `kind: 'mount-asset'` tag on the resulting
|
|
575
|
-
// pack-cyclic-reference detail is set by the cycle producer below
|
|
576
|
-
// (R10).
|
|
574
|
+
// Keyed scene instances are explicit dependency edges. Keep the
|
|
575
|
+
// producer source in the cycle graph alongside ordinary asset refs so
|
|
576
|
+
// recursive SceneAsset declarations fail before publication.
|
|
577
577
|
packRefs.set(normalizedGuid, [
|
|
578
578
|
...asset.refs.map((ref) => ref.toLowerCase()),
|
|
579
|
-
...
|
|
579
|
+
...extractInstanceSourceGuids(asset),
|
|
580
580
|
]);
|
|
581
581
|
}
|
|
582
582
|
capture?.declarations.set(packPath, {
|