@forgeax/engine-pack 0.1.28 → 0.1.30
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 +33 -5
- package/dist/build.mjs +99 -60
- package/dist/build.mjs.map +1 -1
- package/dist/catalog-projection.d.ts +3 -3
- package/dist/cli-asset.mjs +22 -13
- package/dist/cli-asset.mjs.map +1 -1
- package/dist/evidence/material-cook.d.ts +7 -1
- package/dist/evidence/material-cook.d.ts.map +1 -1
- package/dist/index.mjs +61 -5
- 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/material-cook.mjs +61 -5
- package/dist/material-cook.mjs.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/pack-authoring.d.ts +8 -2
- package/dist/pack-authoring.d.ts.map +1 -1
- package/dist/pack-authoring.mjs.map +1 -1
- package/dist/runtime-publication.d.ts +2 -0
- package/dist/runtime-publication.d.ts.map +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 +40 -62
- package/dist/schema.mjs.map +1 -1
- package/dist/scriptable-pack-node.d.ts.map +1 -1
- package/dist/scriptable-pack-node.mjs +12 -4
- package/dist/scriptable-pack-node.mjs.map +1 -1
- package/dist/scriptable-pack-worker.mjs +12 -5
- package/dist/scriptable-pack-worker.mjs.map +1 -1
- package/dist/scriptable-pack.d.ts +2 -1
- package/dist/scriptable-pack.d.ts.map +1 -1
- package/dist/scriptable-pack.mjs.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/inventory-schema.test.ts +10 -10
- package/src/__tests__/material-cook-schema.unit.test.ts +92 -1
- package/src/__tests__/pack-authoring.unit.test.ts +2 -4
- package/src/__tests__/pack.unit.test.ts +86 -124
- package/src/__tests__/runtime-publication.unit.test.ts +36 -0
- 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-cli.integration.test.ts +35 -1
- package/src/__tests__/scriptable-pack-diagnostic.integration.test.ts +45 -0
- package/src/evidence/material-cook.ts +79 -5
- 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/pack-authoring.ts +11 -2
- package/src/runtime-publication.ts +7 -3
- package/src/scanner.ts +28 -28
- package/src/schema/material-cook.schema.json +107 -0
- package/src/schema-compiled.ts +40 -79
- package/src/scriptable-pack-node.ts +16 -4
- package/src/scriptable-pack-worker.ts +23 -5
- package/src/scriptable-pack.ts +2 -1
- package/src/__tests__/scanner-mount-cycle.test.ts +0 -232
|
@@ -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
|
+
});
|
|
@@ -2,12 +2,20 @@ import type {
|
|
|
2
2
|
MaterialAsset,
|
|
3
3
|
MaterialParameter,
|
|
4
4
|
MaterialPass,
|
|
5
|
+
MaterialProgramAbi,
|
|
6
|
+
MaterialProgramAddress,
|
|
5
7
|
MaterialTextureReference,
|
|
6
8
|
MaterialTextureValue,
|
|
7
9
|
MaterialValue,
|
|
8
10
|
Result,
|
|
9
11
|
} from '@forgeax/engine-types';
|
|
10
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
deriveStandardLayerPlan,
|
|
14
|
+
err,
|
|
15
|
+
isMaterialProgramAbi,
|
|
16
|
+
MATERIAL_TEXTURE_SLOTS,
|
|
17
|
+
ok,
|
|
18
|
+
} from '@forgeax/engine-types';
|
|
11
19
|
import { sha256 } from '@noble/hashes/sha2.js';
|
|
12
20
|
import { bytesToHex } from '@noble/hashes/utils.js';
|
|
13
21
|
|
|
@@ -44,6 +52,12 @@ export interface MaterialCookProgram {
|
|
|
44
52
|
readonly selections: readonly {
|
|
45
53
|
readonly pass: string;
|
|
46
54
|
readonly context: MaterialCookProgramContext;
|
|
55
|
+
/** Renderer submission address selected by this publication. */
|
|
56
|
+
readonly address?: MaterialProgramAddress;
|
|
57
|
+
/** Actual entry point paired with the selected address. */
|
|
58
|
+
readonly entry?: string;
|
|
59
|
+
/** Producer-reflected ABI facts for the selected artifact/entry. */
|
|
60
|
+
readonly abi?: MaterialProgramAbi;
|
|
47
61
|
}[];
|
|
48
62
|
}
|
|
49
63
|
|
|
@@ -483,6 +497,9 @@ export function validateCookedMaterialRecord(
|
|
|
483
497
|
const programs: MaterialCookProgram[] = [];
|
|
484
498
|
const programKeys = new Set<string>();
|
|
485
499
|
const selections = new Set<string>();
|
|
500
|
+
const submissionSelections = new Map<string, Set<MaterialProgramAddress>>();
|
|
501
|
+
let modernPublication = false;
|
|
502
|
+
let legacySelectionField: string | undefined;
|
|
486
503
|
const selectedPasses = new Set<string>();
|
|
487
504
|
for (const [index, entry] of candidate.programs.entries()) {
|
|
488
505
|
const field = `programs[${index}]`;
|
|
@@ -511,9 +528,11 @@ export function validateCookedMaterialRecord(
|
|
|
511
528
|
if (!Array.isArray(program.selections) || program.selections.length === 0)
|
|
512
529
|
return invalid(`${field}.selections`);
|
|
513
530
|
const programSelections: MaterialCookProgram['selections'][number][] = [];
|
|
514
|
-
for (const [selectionIndex,
|
|
531
|
+
for (const [selectionIndex, rawSelection] of program.selections.entries()) {
|
|
515
532
|
const selectionField = `${field}.selections[${selectionIndex}]`;
|
|
516
|
-
if (
|
|
533
|
+
if (rawSelection === null || typeof rawSelection !== 'object') return invalid(selectionField);
|
|
534
|
+
const selection = rawSelection as Record<string, unknown>;
|
|
535
|
+
if (typeof selection.pass !== 'string' || !passNames.has(selection.pass))
|
|
517
536
|
return invalid(selectionField);
|
|
518
537
|
const context = validateMaterialCookProgramContext(selection.context);
|
|
519
538
|
if (!context.ok)
|
|
@@ -521,11 +540,55 @@ export function validateCookedMaterialRecord(
|
|
|
521
540
|
`${selectionField}.${context.error.detail.field}`,
|
|
522
541
|
context.error.detail.actual,
|
|
523
542
|
);
|
|
524
|
-
const
|
|
543
|
+
const address = selection.address === undefined ? 'direct' : selection.address;
|
|
544
|
+
if (address !== 'direct' && address !== 'scene-index')
|
|
545
|
+
return invalid(`${selectionField}.address`, selection.address);
|
|
546
|
+
const hasAddressFacts =
|
|
547
|
+
selection.address !== undefined ||
|
|
548
|
+
selection.entry !== undefined ||
|
|
549
|
+
selection.abi !== undefined;
|
|
550
|
+
modernPublication ||= hasAddressFacts;
|
|
551
|
+
if (!hasAddressFacts) legacySelectionField ??= selectionField;
|
|
552
|
+
const rawEntry = selection.entry;
|
|
553
|
+
const entry = typeof rawEntry === 'string' ? rawEntry : undefined;
|
|
554
|
+
if (hasAddressFacts && selection.address === undefined)
|
|
555
|
+
return invalid(
|
|
556
|
+
`${selectionField}.address`,
|
|
557
|
+
'modern ABI selections require an explicit address',
|
|
558
|
+
);
|
|
559
|
+
if (hasAddressFacts && (entry === undefined || entry.length === 0))
|
|
560
|
+
return invalid(`${selectionField}.entry`, rawEntry);
|
|
561
|
+
if (hasAddressFacts && !isMaterialProgramAbi(selection.abi))
|
|
562
|
+
return invalid(`${selectionField}.abi`, selection.abi);
|
|
563
|
+
if (hasAddressFacts) {
|
|
564
|
+
const abi = selection.abi as MaterialProgramAbi;
|
|
565
|
+
const expectedEntry = address === 'direct' ? abi.directEntry : abi.sceneIndexEntry;
|
|
566
|
+
if (entry !== expectedEntry)
|
|
567
|
+
return invalid(`${selectionField}.entry`, 'entry does not match published ABI');
|
|
568
|
+
const submissionKey = JSON.stringify([
|
|
569
|
+
selection.pass,
|
|
570
|
+
materialProgramContextKey(context.value),
|
|
571
|
+
]);
|
|
572
|
+
const addresses =
|
|
573
|
+
submissionSelections.get(submissionKey) ?? new Set<MaterialProgramAddress>();
|
|
574
|
+
addresses.add(address);
|
|
575
|
+
submissionSelections.set(submissionKey, addresses);
|
|
576
|
+
}
|
|
577
|
+
const key = JSON.stringify([
|
|
578
|
+
selection.pass,
|
|
579
|
+
materialProgramContextKey(context.value),
|
|
580
|
+
address,
|
|
581
|
+
]);
|
|
525
582
|
if (selections.has(key)) return invalid(selectionField, 'ambiguous Pass/context selection');
|
|
526
583
|
selections.add(key);
|
|
527
584
|
selectedPasses.add(selection.pass);
|
|
528
|
-
programSelections.push({
|
|
585
|
+
programSelections.push({
|
|
586
|
+
pass: selection.pass,
|
|
587
|
+
context: context.value,
|
|
588
|
+
...(selection.address === undefined ? {} : { address }),
|
|
589
|
+
...(entry === undefined ? {} : { entry }),
|
|
590
|
+
...(selection.abi === undefined ? {} : { abi: selection.abi as MaterialProgramAbi }),
|
|
591
|
+
});
|
|
529
592
|
}
|
|
530
593
|
programs.push({
|
|
531
594
|
specializationKey: program.specializationKey,
|
|
@@ -533,6 +596,17 @@ export function validateCookedMaterialRecord(
|
|
|
533
596
|
selections: programSelections,
|
|
534
597
|
});
|
|
535
598
|
}
|
|
599
|
+
if (modernPublication && legacySelectionField !== undefined) {
|
|
600
|
+
return invalid(
|
|
601
|
+
legacySelectionField,
|
|
602
|
+
'modern material publications require address, entry, and ABI facts on every selection',
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
for (const [selectionKey, addresses] of submissionSelections) {
|
|
606
|
+
if (addresses.size !== 2) {
|
|
607
|
+
return invalid('programs.selections', `incomplete submission address pair: ${selectionKey}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
536
610
|
if ([...passNames].some((pass) => !selectedPasses.has(pass)))
|
|
537
611
|
return invalid('programs.selections', 'unpublished Pass');
|
|
538
612
|
const manifestDigest = createMaterialProgramSetDigest(programs, passes);
|
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/pack-authoring.ts
CHANGED
|
@@ -569,7 +569,9 @@ function validateParameterDefinitions(
|
|
|
569
569
|
}
|
|
570
570
|
|
|
571
571
|
export interface PackBuildReadContext {
|
|
572
|
-
readByGuid<TAsset extends Asset = Asset>(
|
|
572
|
+
readByGuid<TAsset extends Asset | PackCookSource = Asset>(
|
|
573
|
+
guid: AssetGuid,
|
|
574
|
+
): Promise<Result<TAsset, unknown>>;
|
|
573
575
|
}
|
|
574
576
|
|
|
575
577
|
export interface PackBuildContextWithoutParameters extends PackBuildReadContext {
|
|
@@ -588,7 +590,14 @@ export type PackBuildContext<TParameters extends readonly PackParameterDefinitio
|
|
|
588
590
|
? PackBuildContextWithParameters<TParameters>
|
|
589
591
|
: PackBuildContextWithoutParameters;
|
|
590
592
|
|
|
591
|
-
|
|
593
|
+
/** Build-only custom source. The host derives identity and invokes the kind's NativeCooker. */
|
|
594
|
+
export interface PackCookSource {
|
|
595
|
+
readonly kind: string;
|
|
596
|
+
readonly execution: 'cooked';
|
|
597
|
+
readonly source: unknown;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
export type PackOutputMap = Readonly<Record<string, Asset | PackCookSource>>;
|
|
592
601
|
export type PackBuildResult<TError = unknown> =
|
|
593
602
|
| Result<PackOutputMap, TError>
|
|
594
603
|
| Promise<Result<PackOutputMap, TError>>;
|
|
@@ -48,6 +48,8 @@ export interface RuntimePackPublicationInput {
|
|
|
48
48
|
readonly digest?: string;
|
|
49
49
|
readonly generation?: number;
|
|
50
50
|
readonly outputs?: readonly AssetPublicationOutput[];
|
|
51
|
+
/** Author/producer keys by normalized GUID when deriving output rows. */
|
|
52
|
+
readonly sourceKeys?: ReadonlyMap<string, string>;
|
|
51
53
|
readonly externalEvidence?: readonly AssetPublicationExternalEvidence[];
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -88,10 +90,10 @@ function normalizedAssets(pack: RuntimePackInput): readonly RuntimePackAsset[] {
|
|
|
88
90
|
});
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
function outputFor(asset: RuntimePackAsset): AssetPublicationOutput {
|
|
93
|
+
function outputFor(asset: RuntimePackAsset, sourceKey?: string): AssetPublicationOutput {
|
|
92
94
|
return {
|
|
93
95
|
guid: asset.guid.toLowerCase(),
|
|
94
|
-
sourceKey: asset.guid.toLowerCase(),
|
|
96
|
+
sourceKey: sourceKey ?? asset.guid.toLowerCase(),
|
|
95
97
|
kind: asset.kind,
|
|
96
98
|
digest: digest({
|
|
97
99
|
guid: asset.guid.toLowerCase(),
|
|
@@ -145,7 +147,9 @@ export function createRuntimePackPublication(
|
|
|
145
147
|
),
|
|
146
148
|
};
|
|
147
149
|
const valueDigest = input.digest ?? digest(semantic);
|
|
148
|
-
const outputs =
|
|
150
|
+
const outputs =
|
|
151
|
+
input.outputs ??
|
|
152
|
+
assets.map((asset) => outputFor(asset, input.sourceKeys?.get(asset.guid.toLowerCase())));
|
|
149
153
|
const outputDigest = outputSetDigest(outputs);
|
|
150
154
|
const generation =
|
|
151
155
|
input.generation ?? publicationGeneration(input.sourceRevision, valueDigest, outputDigest);
|
package/src/scanner.ts
CHANGED
|
@@ -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, {
|
|
@@ -273,6 +273,113 @@
|
|
|
273
273
|
"type": "string",
|
|
274
274
|
"minLength": 1
|
|
275
275
|
},
|
|
276
|
+
"address": {
|
|
277
|
+
"enum": ["direct", "scene-index"]
|
|
278
|
+
},
|
|
279
|
+
"entry": {
|
|
280
|
+
"type": "string",
|
|
281
|
+
"minLength": 1
|
|
282
|
+
},
|
|
283
|
+
"abi": {
|
|
284
|
+
"type": "object",
|
|
285
|
+
"required": [
|
|
286
|
+
"directEntry",
|
|
287
|
+
"sceneIndexEntry",
|
|
288
|
+
"materialRow",
|
|
289
|
+
"resourceSlots",
|
|
290
|
+
"uvSets",
|
|
291
|
+
"vertexInputs",
|
|
292
|
+
"alphaMask",
|
|
293
|
+
"reflection",
|
|
294
|
+
"receiptIdentity",
|
|
295
|
+
"generation"
|
|
296
|
+
],
|
|
297
|
+
"properties": {
|
|
298
|
+
"directEntry": { "type": "string", "minLength": 1 },
|
|
299
|
+
"sceneIndexEntry": { "type": "string", "minLength": 1 },
|
|
300
|
+
"materialRow": {
|
|
301
|
+
"type": "object",
|
|
302
|
+
"required": ["byteLength", "fields"],
|
|
303
|
+
"properties": {
|
|
304
|
+
"byteLength": { "type": "integer", "minimum": 16, "multipleOf": 16 },
|
|
305
|
+
"fields": { "type": "array", "items": { "type": "string" } }
|
|
306
|
+
},
|
|
307
|
+
"additionalProperties": false
|
|
308
|
+
},
|
|
309
|
+
"resourceSlots": {
|
|
310
|
+
"type": "array",
|
|
311
|
+
"items": {
|
|
312
|
+
"type": "object",
|
|
313
|
+
"required": ["name", "parameter", "kind", "group", "binding"],
|
|
314
|
+
"properties": {
|
|
315
|
+
"name": { "type": "string", "minLength": 1 },
|
|
316
|
+
"parameter": { "type": "string", "minLength": 1 },
|
|
317
|
+
"kind": { "enum": ["sampler", "texture", "storage-buffer"] },
|
|
318
|
+
"group": { "type": "integer", "minimum": 0 },
|
|
319
|
+
"binding": { "type": "integer", "minimum": 0 }
|
|
320
|
+
},
|
|
321
|
+
"additionalProperties": false
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
"uvSets": {
|
|
325
|
+
"type": "array",
|
|
326
|
+
"items": {
|
|
327
|
+
"type": "object",
|
|
328
|
+
"required": ["parameter", "set"],
|
|
329
|
+
"properties": {
|
|
330
|
+
"parameter": { "type": "string", "minLength": 1 },
|
|
331
|
+
"set": { "type": "integer", "minimum": 0 }
|
|
332
|
+
},
|
|
333
|
+
"additionalProperties": false
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
"vertexInputs": {
|
|
337
|
+
"type": "array",
|
|
338
|
+
"items": {
|
|
339
|
+
"type": "object",
|
|
340
|
+
"required": ["semantic", "location", "format"],
|
|
341
|
+
"properties": {
|
|
342
|
+
"semantic": { "type": "string", "minLength": 1 },
|
|
343
|
+
"location": { "type": "integer", "minimum": 0 },
|
|
344
|
+
"format": { "type": "string", "minLength": 1 }
|
|
345
|
+
},
|
|
346
|
+
"additionalProperties": false
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
"alphaMask": {
|
|
350
|
+
"type": "object",
|
|
351
|
+
"required": ["cutoff", "source"],
|
|
352
|
+
"properties": {
|
|
353
|
+
"cutoff": { "type": "string" },
|
|
354
|
+
"source": { "type": "string" }
|
|
355
|
+
},
|
|
356
|
+
"additionalProperties": false
|
|
357
|
+
},
|
|
358
|
+
"skinPaletteAddress": {
|
|
359
|
+
"type": "object",
|
|
360
|
+
"required": ["group", "binding", "stride"],
|
|
361
|
+
"properties": {
|
|
362
|
+
"group": { "type": "integer", "minimum": 0 },
|
|
363
|
+
"binding": { "type": "integer", "minimum": 0 },
|
|
364
|
+
"stride": { "type": "integer", "minimum": 1 }
|
|
365
|
+
},
|
|
366
|
+
"additionalProperties": false
|
|
367
|
+
},
|
|
368
|
+
"reflection": {
|
|
369
|
+
"type": "object",
|
|
370
|
+
"required": ["layoutIdentity", "resourceSlots", "vertexInputs"],
|
|
371
|
+
"properties": {
|
|
372
|
+
"layoutIdentity": { "type": "string", "minLength": 1 },
|
|
373
|
+
"resourceSlots": { "type": "array" },
|
|
374
|
+
"vertexInputs": { "type": "array" }
|
|
375
|
+
},
|
|
376
|
+
"additionalProperties": false
|
|
377
|
+
},
|
|
378
|
+
"receiptIdentity": { "type": "string", "minLength": 1 },
|
|
379
|
+
"generation": { "type": "integer", "minimum": 1 }
|
|
380
|
+
},
|
|
381
|
+
"additionalProperties": false
|
|
382
|
+
},
|
|
276
383
|
"context": {
|
|
277
384
|
"type": "object",
|
|
278
385
|
"required": [
|