@forgeax/engine-pack 0.1.20 → 0.1.23

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 (128) hide show
  1. package/README.md +159 -46
  2. package/dist/__tests__/inventory-schema.test.d.ts +2 -0
  3. package/dist/__tests__/inventory-schema.test.d.ts.map +1 -0
  4. package/dist/__tests__/parameterized-pack-authoring-gateway.unit.test.d.ts +2 -0
  5. package/dist/__tests__/parameterized-pack-authoring-gateway.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/parameterized-pack-scanner.unit.test.d.ts +2 -0
  7. package/dist/__tests__/parameterized-pack-scanner.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/parameterized-pack.unit.test.d.ts +2 -0
  9. package/dist/__tests__/parameterized-pack.unit.test.d.ts.map +1 -0
  10. package/dist/__tests__/scanner-blacklist.test.d.ts +2 -0
  11. package/dist/__tests__/scanner-blacklist.test.d.ts.map +1 -0
  12. package/dist/build.d.ts +9 -4
  13. package/dist/build.d.ts.map +1 -1
  14. package/dist/build.mjs +3317 -804
  15. package/dist/build.mjs.map +1 -1
  16. package/dist/builtin.mjs +37 -4
  17. package/dist/builtin.mjs.map +1 -1
  18. package/dist/catalog-builder.d.ts.map +1 -1
  19. package/dist/cli-asset.d.ts.map +1 -1
  20. package/dist/cli-asset.mjs +1368 -850
  21. package/dist/cli-asset.mjs.map +1 -1
  22. package/dist/deriveAssetName.d.ts +11 -7
  23. package/dist/deriveAssetName.d.ts.map +1 -1
  24. package/dist/evidence/material-cook.d.ts.map +1 -1
  25. package/dist/evidence/source-inventory.d.ts.map +1 -1
  26. package/dist/guid.d.ts +15 -0
  27. package/dist/guid.d.ts.map +1 -1
  28. package/dist/guid.mjs +53 -5
  29. package/dist/guid.mjs.map +1 -1
  30. package/dist/index.d.ts +3 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.mjs +1255 -561
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/inventory/binding.d.ts +6 -0
  35. package/dist/inventory/binding.d.ts.map +1 -0
  36. package/dist/inventory/declaration.d.ts +24 -0
  37. package/dist/inventory/declaration.d.ts.map +1 -0
  38. package/dist/inventory/index.d.ts +3 -0
  39. package/dist/inventory/index.d.ts.map +1 -0
  40. package/dist/inventory/sync.d.ts +5 -0
  41. package/dist/inventory/sync.d.ts.map +1 -0
  42. package/dist/material-cook.mjs +3 -1
  43. package/dist/material-cook.mjs.map +1 -1
  44. package/dist/name.mjs +4 -7
  45. package/dist/name.mjs.map +1 -1
  46. package/dist/package-finalizer.d.ts +2 -20
  47. package/dist/package-finalizer.d.ts.map +1 -1
  48. package/dist/parameterized-pack-node.d.ts +26 -0
  49. package/dist/parameterized-pack-node.d.ts.map +1 -0
  50. package/dist/parameterized-pack-node.mjs +11093 -0
  51. package/dist/parameterized-pack-node.mjs.map +1 -0
  52. package/dist/parameterized-pack.d.ts +233 -0
  53. package/dist/parameterized-pack.d.ts.map +1 -0
  54. package/dist/resolve-asset-source.d.ts +1 -3
  55. package/dist/resolve-asset-source.d.ts.map +1 -1
  56. package/dist/resolve-asset-source.mjs +6 -81
  57. package/dist/resolve-asset-source.mjs.map +1 -1
  58. package/dist/runtime.mjs +77 -7
  59. package/dist/runtime.mjs.map +1 -1
  60. package/dist/scanner.d.ts +19 -8
  61. package/dist/scanner.d.ts.map +1 -1
  62. package/dist/scanner.mjs +1169 -690
  63. package/dist/scanner.mjs.map +1 -1
  64. package/dist/schema-compiled.d.ts.map +1 -1
  65. package/dist/schema.mjs +79 -7
  66. package/dist/schema.mjs.map +1 -1
  67. package/dist/scriptable-pack-node.d.ts +8 -32
  68. package/dist/scriptable-pack-node.d.ts.map +1 -1
  69. package/dist/scriptable-pack-node.mjs +576 -1021
  70. package/dist/scriptable-pack-node.mjs.map +1 -1
  71. package/dist/scriptable-pack-worker.mjs +29 -11
  72. package/dist/scriptable-pack-worker.mjs.map +1 -1
  73. package/dist/scriptable-pack.d.ts +19 -532
  74. package/dist/scriptable-pack.d.ts.map +1 -1
  75. package/dist/scriptable-pack.mjs +1042 -527
  76. package/dist/scriptable-pack.mjs.map +1 -1
  77. package/package.json +7 -7
  78. package/schema/meta.schema.json +1 -1
  79. package/schema/pack.schema.json +76 -6
  80. package/src/__tests__/guid-collision.unit.test.ts +18 -9
  81. package/src/__tests__/inventory-schema.test.ts +109 -0
  82. package/src/__tests__/pack.unit.test.ts +1 -238
  83. package/src/__tests__/package-finalizer.contract.test.ts +55 -0
  84. package/src/__tests__/parameterized-pack-authoring-gateway.unit.test.ts +249 -0
  85. package/src/__tests__/parameterized-pack-scanner.unit.test.ts +123 -0
  86. package/src/__tests__/parameterized-pack.unit.test.ts +266 -0
  87. package/src/__tests__/resolve-asset-source.test.ts +15 -98
  88. package/src/__tests__/scanner-blacklist.test.ts +55 -0
  89. package/src/__tests__/scanner-inventory.contract.test.ts +14 -15
  90. package/src/__tests__/scanner-inventory.test.ts +24 -12
  91. package/src/__tests__/scriptable-pack-cli.integration.test.ts +4 -9
  92. package/src/__tests__/topology.unit.test.ts +23 -0
  93. package/src/build.ts +28 -18
  94. package/src/catalog-builder.ts +61 -34
  95. package/src/cli-asset.ts +70 -85
  96. package/src/deriveAssetName.ts +14 -13
  97. package/src/evidence/material-cook.ts +3 -1
  98. package/src/evidence/source-inventory.ts +34 -21
  99. package/src/guid.ts +65 -4
  100. package/src/index.ts +12 -11
  101. package/src/inventory/binding.ts +25 -0
  102. package/src/inventory/declaration.ts +168 -0
  103. package/src/inventory/index.ts +18 -0
  104. package/src/inventory/sync.ts +22 -0
  105. package/src/package-finalizer.ts +2 -42
  106. package/src/parameterized-pack-node.ts +1968 -0
  107. package/src/parameterized-pack.ts +1493 -0
  108. package/src/resolve-asset-source.ts +4 -76
  109. package/src/scanner.ts +241 -59
  110. package/src/schema-compiled.ts +2 -0
  111. package/src/scriptable-pack-node.ts +111 -722
  112. package/src/scriptable-pack-worker.ts +48 -16
  113. package/src/scriptable-pack.ts +27 -939
  114. package/dist/.tsbuildinfo +0 -1
  115. package/dist/__tests__/load-asset-config.test.d.ts +0 -2
  116. package/dist/__tests__/load-asset-config.test.d.ts.map +0 -1
  117. package/dist/__tests__/scriptable-pack.test-d.d.ts +0 -2
  118. package/dist/__tests__/scriptable-pack.test-d.d.ts.map +0 -1
  119. package/dist/__tests__/scriptable-pack.unit.test.d.ts +0 -2
  120. package/dist/__tests__/scriptable-pack.unit.test.d.ts.map +0 -1
  121. package/dist/config.d.ts +0 -6
  122. package/dist/config.d.ts.map +0 -1
  123. package/dist/config.mjs +0 -29
  124. package/dist/config.mjs.map +0 -1
  125. package/src/__tests__/load-asset-config.test.ts +0 -121
  126. package/src/__tests__/scriptable-pack.test-d.ts +0 -131
  127. package/src/__tests__/scriptable-pack.unit.test.ts +0 -789
  128. package/src/config.ts +0 -36
@@ -2,6 +2,7 @@ import { mkdtemp, realpath, rm, writeFile } from 'node:fs/promises';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { afterAll, beforeAll, describe, expect, it } from 'vitest';
5
+ import { inventoryDigest } from '../inventory/sync.js';
5
6
  import { scanInventory } from '../scanner.js';
6
7
 
7
8
  describe('AssetInventory', () => {
@@ -18,11 +19,8 @@ describe('AssetInventory', () => {
18
19
  [
19
20
  'const packageId = new Uint8Array(16);',
20
21
  'packageId[15] = 1;',
21
- 'const meshGuid = new Uint8Array(16);',
22
- 'meshGuid[15] = 2;',
23
22
  'export default {',
24
- "schemaVersion: '1.0.0', packageId,",
25
- "assets: { mesh: { guid: meshGuid, kind: 'mesh' } }, externalAssets: {},",
23
+ "schemaVersion: '2.0.0', packageId,",
26
24
  "build: () => ({ ok: true, value: { mesh: { kind: 'mesh' } } }),",
27
25
  '};',
28
26
  ].join('\n'),
@@ -46,11 +44,13 @@ describe('AssetInventory', () => {
46
44
  const { inventory, sourcePath } = getFixture();
47
45
  const declaration = inventory.declarations.get(sourcePath);
48
46
  expect(declaration).toMatchObject({
49
- format: 'pack.ts',
47
+ format: 'pack.ts-parameterized',
50
48
  sourcePath,
51
- meta: {
52
- importer: 'pack-ts',
53
- subAssets: [{ guid, sourceKey: 'mesh', kind: 'mesh' }],
49
+ value: {
50
+ schemaVersion: '2.0.0',
51
+ kind: 'parameterized-pack-source',
52
+ packageId: '00000000-0000-0000-0000-000000000001',
53
+ source: sourcePath,
54
54
  },
55
55
  });
56
56
  });
@@ -59,7 +59,7 @@ describe('AssetInventory', () => {
59
59
  const { inventory, sourcePath } = getFixture();
60
60
  const declaration = inventory.declarations.get(sourcePath);
61
61
  expect(
62
- declaration?.format === 'pack.ts'
62
+ declaration?.format === 'pack.ts-parameterized'
63
63
  ? declaration.sourceClosure.map((entry) => entry.path)
64
64
  : undefined,
65
65
  ).toEqual([await realpath(sourcePath)]);
@@ -69,8 +69,20 @@ describe('AssetInventory', () => {
69
69
  const { inventory, sourcePath } = getFixture();
70
70
  const declaration = inventory.declarations.get(sourcePath);
71
71
  expect(inventory.inventory).toEqual([]);
72
- expect(declaration?.format === 'pack.ts' ? declaration.meta.subAssets : undefined).toEqual([
73
- { guid, sourceIndex: 0, sourceKey: 'mesh', kind: 'mesh' },
74
- ]);
72
+ expect(
73
+ declaration?.format === 'pack.ts-parameterized' ? declaration.definition : undefined,
74
+ ).toMatchObject({ packageId: expect.any(Uint8Array), schemaVersion: '2.0.0' });
75
+ });
76
+
77
+ it('keeps semantic identity stable when the source path is renamed', () => {
78
+ const before = inventoryDigest({
79
+ declarations: [{ guid, sourceKey: 'hero/body', kind: 'mesh', payload: {}, refs: [] }],
80
+ });
81
+ const after = inventoryDigest({
82
+ declarations: [{ guid, sourceKey: 'hero/body', kind: 'mesh', payload: {}, refs: [] }],
83
+ });
84
+
85
+ expect(after).toBe(before);
86
+ expect(after).not.toContain('generated.pack.ts');
75
87
  });
76
88
  });
@@ -14,14 +14,9 @@ describe('ScriptablePack CLI Meta inspection', () => {
14
14
  await writeFile(
15
15
  source,
16
16
  `
17
- const guid = (hex) => new Uint8Array(hex.match(/../g).map((byte) => Number.parseInt(byte, 16)));
18
17
  export default {
19
- schemaVersion: '1.0.0',
20
- packageId: guid('019ffa978b397ad284cc273268591ab4'),
21
- assets: {
22
- wall: { guid: guid('019ffa97a5ee7645b613043323952808'), kind: 'mesh' }
23
- },
24
- externalAssets: {},
18
+ schemaVersion: '2.0.0',
19
+ packageId: new Uint8Array([1, 159, 250, 151, 139, 57, 122, 210, 132, 204, 39, 50, 104, 89, 26, 180]),
25
20
  build() { throw new Error('META_MUST_NOT_BUILD'); }
26
21
  };
27
22
  `,
@@ -36,9 +31,9 @@ describe('ScriptablePack CLI Meta inspection', () => {
36
31
  expect(code).toBe(0);
37
32
  expect(stderr).toEqual([]);
38
33
  expect(JSON.parse(stdout[0] ?? '{}')).toMatchObject({
39
- importer: 'pack-ts',
34
+ kind: 'parameterized-pack-source',
40
35
  source,
41
- subAssets: [{ sourceKey: 'wall', kind: 'mesh' }],
36
+ packageId: '019ffa97-8b39-7ad2-84cc-273268591ab4',
42
37
  });
43
38
  } finally {
44
39
  await rm(root, { recursive: true, force: true });
@@ -82,4 +82,27 @@ describe('diffTopology', () => {
82
82
  expect(result.preserved).toEqual([]);
83
83
  expect(result.ambiguous).toMatchObject([{ reason: 'duplicate-source-key' }]);
84
84
  });
85
+
86
+ it('keeps LOD source keys prefix-stable while allowing a suffix level', () => {
87
+ const result = diffTopology(
88
+ [
89
+ { guid: 'root', sourceKey: 'mesh/root', sourceIndex: 0, kind: 'mesh' },
90
+ { guid: 'lod1', sourceKey: 'mesh/lod1', sourceIndex: 1, kind: 'mesh' },
91
+ ],
92
+ [
93
+ { guid: 'root-next', sourceKey: 'mesh/root', sourceIndex: 0, kind: 'mesh' },
94
+ { guid: 'lod1-next', sourceKey: 'mesh/lod1', sourceIndex: 1, kind: 'mesh' },
95
+ { guid: 'lod2-next', sourceKey: 'mesh/lod2', sourceIndex: 2, kind: 'mesh' },
96
+ ],
97
+ );
98
+ expect(result.preserved).toEqual([
99
+ { guid: 'root', oldKey: 'mesh/root', newKey: 'mesh/root' },
100
+ { guid: 'lod1', oldKey: 'mesh/lod1', newKey: 'mesh/lod1' },
101
+ ]);
102
+ expect(result.added).toEqual([
103
+ { guid: 'lod2-next', sourceKey: 'mesh/lod2', sourceIndex: 2, kind: 'mesh' },
104
+ ]);
105
+ expect(result.removed).toEqual([]);
106
+ expect(result.ambiguous).toEqual([]);
107
+ });
85
108
  });
package/src/build.ts CHANGED
@@ -51,7 +51,6 @@ export {
51
51
  projectRuntimeCatalogRow,
52
52
  type RuntimeCatalogRowInput,
53
53
  } from './catalog-projection.js';
54
- export { type LoadAssetConfigResult, loadAssetConfig } from './config.js';
55
54
  export {
56
55
  type CookedMaterialRecord,
57
56
  collectMaterialCookRefs,
@@ -68,6 +67,22 @@ export {
68
67
  validateMaterialCookReceipt,
69
68
  } from './evidence/material-cook.js';
70
69
  export { buildOfflineAssetEvidence, packageVerification } from './evidence/offline-evidence.js';
70
+ export {
71
+ AssetGuid,
72
+ isValidAssetGuidString,
73
+ isValidPackSourceKey,
74
+ PACK_SOURCE_KEY_RE,
75
+ PackageId,
76
+ } from './guid.js';
77
+ export { projectAssetRefs, projectSceneEntityRefs } from './inventory/binding.js';
78
+ export {
79
+ type AuthorInventory,
80
+ type AuthorInventoryRow,
81
+ type InventoryError,
82
+ type InventoryErrorCode,
83
+ validateAuthorInventory,
84
+ } from './inventory/declaration.js';
85
+ export { inventoryDigest, syncAuthorInventory } from './inventory/sync.js';
71
86
  export {
72
87
  decodeMeshBinHeader,
73
88
  MESH_BIN_DIGEST_BYTES,
@@ -80,8 +95,6 @@ export {
80
95
  writeMeshBinHeader,
81
96
  } from './mesh-bin-contract.js';
82
97
  export {
83
- type AuthoredPackAssetInput,
84
- type AuthoredPackInput,
85
98
  type FinalizedPackageProduct,
86
99
  finalizePackageProduct,
87
100
  finalizePackageTransportSource,
@@ -92,8 +105,14 @@ export {
92
105
  type PackageProduct,
93
106
  type PackageProductAsset,
94
107
  packageTransportRevision,
95
- upgradeLegacyAuthoredPack,
96
108
  } from './package-finalizer.js';
109
+ export * from './parameterized-pack.js';
110
+ export {
111
+ createFileSystemParameterizedPackAuthoringGateway,
112
+ createFileSystemParameterizedPackAuthoringPort,
113
+ type FileSystemParameterizedPackAuthoringOptions,
114
+ type ParameterizedPackMaterializedAsset,
115
+ } from './parameterized-pack-node.js';
97
116
  export { validateProducerContract, validateProducerOutputs } from './producer-contract.js';
98
117
  export { resolveAssetSource } from './resolve-asset-source.js';
99
118
  export { parsePackV2, validateMeta, validatePack, validatePackV2 } from './runtime.js';
@@ -107,8 +126,13 @@ export {
107
126
  } from './runtime-publication.js';
108
127
  export {
109
128
  type InventoryDeclaration,
129
+ type LegacyPackInventoryDocument,
130
+ type PackInventoryAsset,
131
+ type PackInventoryDocument,
132
+ type ParameterizedPackInventoryDocument,
110
133
  type ScanInventory,
111
134
  type ScanOptions,
135
+ type ScanSourceDeclaration,
112
136
  type ScriptablePackInventoryDeclaration,
113
137
  type ScriptablePackScanOptions,
114
138
  STANDARD_SCRIPTABLE_PACK_SCAN_OPTIONS,
@@ -117,24 +141,10 @@ export {
117
141
  export {
118
142
  type AssetReader,
119
143
  isScriptablePackAssetKind,
120
- projectScriptablePackMeta,
121
144
  projectScriptablePackSceneComponents,
122
145
  SCRIPTABLE_PACK_ASSET_KINDS,
123
- SCRIPTABLE_PACK_CAPABILITY_MANIFEST,
124
- type ScriptablePackAssetDeclaration,
125
- type ScriptablePackAssetDeclarations,
126
- type ScriptablePackAssetFor,
127
146
  type ScriptablePackAssetKind,
128
- type ScriptablePackDefinition,
129
- type ScriptablePackError,
130
- type ScriptablePackExternalAssets,
131
- type ScriptablePackMetaJson,
132
- type ScriptablePackOutputs,
133
- type ScriptablePackPublicationEnvelope,
134
147
  type ScriptablePackReadError,
135
- type ScriptablePackSceneComponent,
136
- type ScriptablePackSceneComponentInput,
137
148
  type ScriptablePackSourceClosureEntry,
138
- validateScriptablePackDefinition,
139
149
  } from './scriptable-pack.js';
140
150
  export { calculateTopologyDiff, diffTopology } from './topology.js';
@@ -5,8 +5,8 @@ import {
5
5
  projectExternalCatalogEntries,
6
6
  projectPackageCatalog,
7
7
  } from './catalog-projection.js';
8
- import { loadAssetConfig } from './config.js';
9
8
  import { deriveAssetName } from './deriveAssetName.js';
9
+ import { parseParameterizedPackJson, projectDirectPackJson } from './parameterized-pack.js';
10
10
  import { resolveAssetSource } from './resolve-asset-source.js';
11
11
  import {
12
12
  type MetaInventoryDocument,
@@ -138,7 +138,6 @@ function projectMeta(
138
138
  rawPath: string,
139
139
  cwd: string,
140
140
  base: string,
141
- assetPaths: Record<string, string>,
142
141
  importerPolicy: (importer: string) => CatalogImporterPolicy,
143
142
  sourceDeclaration: Extract<ScanSourceDeclaration, { readonly format: 'meta.json' }>,
144
143
  sourceIdentityFor: CatalogBuildProjectionOptions['sourceIdentityFor'],
@@ -152,22 +151,12 @@ function projectMeta(
152
151
  if (policy.disposition !== 'publish') {
153
152
  return { entries: [], error: unsupportedPolicyError(rawPath, meta.importer, policy) };
154
153
  }
155
- const resolved = resolveAssetSource(rawPath, meta.source, assetPaths);
156
- if (!resolved.ok) {
157
- return {
158
- entries: [],
159
- error: {
160
- code: 'catalog-meta-schema-invalid',
161
- path: rawPath,
162
- message: `source resolution failed: ${resolved.error.code} - ${resolved.error.hint}`,
163
- },
164
- };
165
- }
166
- const sourcePath = catalogSourcePathFor(cwd, resolved.value, sourceIdentityFor);
154
+ const resolved = resolveAssetSource(rawPath, meta.source);
155
+ const sourcePath = catalogSourcePathFor(cwd, resolved, sourceIdentityFor);
167
156
  const packageUrl = `${base}/__forgeax-ddc/${meta.subAssets[0]?.guid?.toLowerCase() ?? 'pack'}.pack.json`;
168
157
  const declaration: CatalogImportMeta = {
169
158
  importer: meta.importer,
170
- source: resolved.value,
159
+ source: resolved,
171
160
  sourceRevision: sourceDeclaration.sourceRevision,
172
161
  ...(meta.packageId === undefined ? {} : { packageId: meta.packageId }),
173
162
  ...(meta.provenance === undefined ? {} : { provenance: meta.provenance }),
@@ -197,7 +186,7 @@ function projectMeta(
197
186
  return {
198
187
  declaration,
199
188
  entries: projectExternalCatalogEntries(meta, sourcePath, packageUrl, meta.subAssets, (output) =>
200
- deriveAssetName(resolved.value, meta.subAssets.length, output.name),
189
+ deriveAssetName(resolved, meta.subAssets.length, output.name),
201
190
  ),
202
191
  };
203
192
  }
@@ -206,27 +195,17 @@ function projectSource(
206
195
  path: string,
207
196
  cwd: string,
208
197
  base: string,
209
- assetPaths: Record<string, string>,
210
198
  importerPolicy: (importer: string) => CatalogImporterPolicy,
211
199
  visibility: CatalogBuildProjectionOptions['visibility'],
212
200
  sourceDeclaration: ScanSourceDeclaration,
213
201
  declarations: Map<string, CatalogImportMeta>,
214
202
  sourceIdentityFor: CatalogBuildProjectionOptions['sourceIdentityFor'],
215
203
  ): { readonly entries: PackIndexEntry[]; readonly error?: CatalogBuildError } {
216
- if (sourceDeclaration.format === 'pack.ts') {
217
- const meta = { ...sourceDeclaration.meta, source: sourcePathFor(cwd, path) };
218
- const anchorGuid = meta.subAssets.map((asset) => asset.guid.toLowerCase()).sort()[0];
219
- return anchorGuid === undefined
220
- ? { entries: [] }
221
- : {
222
- entries: projectExternalCatalogEntries(
223
- meta,
224
- catalogSourcePathFor(cwd, path, sourceIdentityFor),
225
- `${base}/__forgeax-ddc/${anchorGuid}.pack.json`,
226
- meta.subAssets,
227
- (output) => deriveAssetName(path, meta.subAssets.length, output.name),
228
- ),
229
- };
204
+ if (sourceDeclaration.format === 'pack.ts-parameterized') {
205
+ // A source declaration is an authoring identity and parameter contract,
206
+ // not a materialized output set. Dynamic build owns publication after its
207
+ // fixed-point worklist converges; Catalog scanning must not invent rows.
208
+ return { entries: [] };
230
209
  }
231
210
  if (sourceDeclaration.format === 'meta.json') {
232
211
  const policy = importerPolicy(sourceDeclaration.value.importer);
@@ -244,7 +223,6 @@ function projectSource(
244
223
  path,
245
224
  cwd,
246
225
  base,
247
- assetPaths,
248
226
  importerPolicy,
249
227
  sourceDeclaration,
250
228
  sourceIdentityFor,
@@ -254,6 +232,57 @@ function projectSource(
254
232
  }
255
233
  const parsed = sourceDeclaration.value;
256
234
  const sourcePath = catalogSourcePathFor(cwd, path, sourceIdentityFor);
235
+ if (parsed.schemaVersion === '3.0.0') {
236
+ const authoring = parseParameterizedPackJson(parsed);
237
+ if (!authoring.ok) {
238
+ return {
239
+ entries: [],
240
+ error: {
241
+ code: 'catalog-meta-schema-invalid',
242
+ path,
243
+ message: `v3 Pack parse failed: ${authoring.error.code}`,
244
+ expected: 'a valid direct or instance v3 Pack document',
245
+ actual: authoring.error.code,
246
+ hint: authoring.error.hint,
247
+ subjects: [path],
248
+ },
249
+ };
250
+ }
251
+ if (authoring.value.format !== 'direct') return { entries: [] };
252
+ const projected = projectDirectPackJson(authoring.value);
253
+ if (!projected.ok) {
254
+ return {
255
+ entries: [],
256
+ error: {
257
+ code: 'catalog-meta-schema-invalid',
258
+ path,
259
+ message: `v3 Pack projection failed: ${projected.error.code}`,
260
+ expected: 'direct v3 assets to derive stable AssetGuids',
261
+ actual: projected.error.code,
262
+ hint: projected.error.hint,
263
+ subjects: [path],
264
+ },
265
+ };
266
+ }
267
+ const provenance = { provider: 'pack', version: '3.0.0' } satisfies ProviderProvenance;
268
+ return {
269
+ entries: projectPackageCatalog(
270
+ projected.value.assets.map((asset, sourceIndex) => ({
271
+ guid: asset.guid,
272
+ kind: asset.kind,
273
+ sourcePath,
274
+ sourceIndex,
275
+ sourceKey: asset.sourceKey,
276
+ refs: asset.refs,
277
+ execution: 'direct' as const,
278
+ packageId: projected.value.packageId,
279
+ provenance,
280
+ name: deriveAssetName(path, projected.value.assets.length, asset.name),
281
+ })),
282
+ `${base}/${sourcePath}`,
283
+ ),
284
+ };
285
+ }
257
286
  const provenance =
258
287
  parsed.provenance ??
259
288
  ({ provider: 'pack', version: parsed.schemaVersion } satisfies ProviderProvenance);
@@ -317,7 +346,6 @@ export async function buildCatalogProjection(
317
346
  return empty('degraded', [error]);
318
347
  }
319
348
  const { paths, declarations: sourceDeclarations } = scanned.value;
320
- const { paths: assetPaths } = loadAssetConfig(cwd);
321
349
  const entries: PackIndexEntry[] = [];
322
350
  const errors: CatalogBuildError[] = [];
323
351
  const declarations = new Map<string, CatalogImportMeta>();
@@ -328,7 +356,6 @@ export async function buildCatalogProjection(
328
356
  path,
329
357
  cwd,
330
358
  base,
331
- assetPaths,
332
359
  options.importerPolicy,
333
360
  options.visibility,
334
361
  sourceDeclaration,
package/src/cli-asset.ts CHANGED
@@ -26,9 +26,9 @@ import {
26
26
  import { readSourceInventory } from './evidence/source-inventory.js';
27
27
  import { isValidAssetGuidString } from './guid.js';
28
28
  import { parsePackV2 } from './index.js';
29
- import { scan } from './scanner.js';
30
- import { isRecord, projectScriptablePackMeta } from './scriptable-pack.js';
31
- import { loadScriptablePack } from './scriptable-pack-node.js';
29
+ import { projectParameterizedPackMeta } from './parameterized-pack.js';
30
+ import { type ScanSourceDeclaration, scanInventory } from './scanner.js';
31
+ import { loadParameterizedScriptablePack } from './scriptable-pack-node.js';
32
32
 
33
33
  export interface PackEntry {
34
34
  readonly guid: string;
@@ -51,6 +51,10 @@ interface ScanErrShape {
51
51
  detail?: unknown;
52
52
  }
53
53
 
54
+ function isRecord(value: unknown): value is Record<string, unknown> {
55
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
56
+ }
57
+
54
58
  function emitError(ctx: AssetCtx, err: ScanErrShape): number {
55
59
  const payload: Record<string, unknown> = {
56
60
  code: err.code,
@@ -66,7 +70,7 @@ export async function scanEntries(
66
70
  roots: readonly string[],
67
71
  ctx: AssetCtx,
68
72
  ): Promise<{ ok: true; value: PackEntry[] } | { ok: false }> {
69
- const result = await scan(roots);
73
+ const result = await scanInventory(roots);
70
74
  if (!result.ok) {
71
75
  emitError(ctx, {
72
76
  code: result.error.code,
@@ -77,94 +81,73 @@ export async function scanEntries(
77
81
  return { ok: false };
78
82
  }
79
83
  const entries: PackEntry[] = [];
80
- // Two file kinds carry GUID-addressed entries (disk schema SSOT, AGENTS.md
81
- // §Disk schema): `.pack.json` (`internal-text-package`) holds top-level
82
- // `assets[]`, while every `*.meta.json` sidecar (including
83
- // any `*.meta.json` regardless of source extension (top-level `importer`), all of
84
- // kind `external-asset-package`) holds `subAssets[]`. The CLI surface
85
- // must enumerate both so that `scan` / `lookup` mirror what the
86
- // build-time catalog builder folds into `pack-index.json` (otherwise
87
- // the same disk schema yields different entity counts on the two
88
- // surfaces — observed regression in feat-20260517 sandbox T-2.B1).
89
- const packPaths = result.value.filter((p) => p.endsWith('.pack.json'));
90
- const metaPaths = result.value.filter((p) => p.endsWith('.meta.json'));
91
- for (const packPath of packPaths) {
92
- let parsed: unknown;
93
- try {
94
- const raw = await readFile(packPath, 'utf-8');
95
- parsed = JSON.parse(raw);
96
- } catch {
97
- continue;
98
- }
99
- const packObj = parsed as { assets?: { guid?: unknown; kind?: unknown; name?: unknown }[] };
100
- if (!Array.isArray(packObj.assets)) continue;
101
- for (const asset of packObj.assets) {
102
- if (typeof asset.guid === 'string' && typeof asset.kind === 'string') {
103
- entries.push({
104
- guid: asset.guid,
105
- kind: asset.kind,
106
- sourcePath: packPath,
107
- ...(typeof asset.name === 'string' ? { name: asset.name } : {}),
108
- });
84
+ // The scanner is the single identity projection for Pack JSON. Parameterized
85
+ // source modules deliberately contribute no rows until a build generation.
86
+ for (const declaration of result.value.declarations.values()) {
87
+ if (declaration.format !== 'meta.json') continue;
88
+ const sourcePath = declaration.sourcePath;
89
+ const assets = declaration.value.subAssets;
90
+ for (const asset of assets) {
91
+ if (!isRecord(asset) || typeof asset.guid !== 'string' || typeof asset.kind !== 'string') {
92
+ continue;
109
93
  }
94
+ const name =
95
+ typeof asset.name === 'string'
96
+ ? asset.name
97
+ : typeof asset.sourceKey === 'string'
98
+ ? asset.sourceKey
99
+ : undefined;
100
+ entries.push({
101
+ guid: asset.guid,
102
+ kind: asset.kind,
103
+ sourcePath,
104
+ ...(name === undefined ? {} : { name }),
105
+ });
110
106
  }
111
107
  }
112
- for (const metaPath of metaPaths) {
113
- let parsed: unknown;
114
- try {
115
- const raw = await readFile(metaPath, 'utf-8');
116
- parsed = JSON.parse(raw);
117
- } catch {
108
+ const names = new Map<string, string>();
109
+ for (const declaration of result.value.declarations.values()) {
110
+ if (declaration.format !== 'pack.json') continue;
111
+ if (declaration.value.schemaVersion === '3.0.0') {
112
+ for (const [sourceKey, asset] of Object.entries(declaration.value.assets ?? {})) {
113
+ if (isRecord(asset) && typeof asset.name === 'string') {
114
+ names.set(`${declaration.sourcePath}:${sourceKey}`, asset.name);
115
+ }
116
+ }
118
117
  continue;
119
118
  }
120
- const metaObj = parsed as {
121
- subAssets?: { guid?: unknown; kind?: unknown; name?: unknown; sourceKey?: unknown }[];
122
- };
123
- if (!Array.isArray(metaObj.subAssets)) continue;
124
- for (const sub of metaObj.subAssets) {
125
- if (typeof sub.guid === 'string' && typeof sub.kind === 'string') {
126
- const name =
127
- typeof sub.name === 'string'
128
- ? sub.name
129
- : typeof sub.sourceKey === 'string'
130
- ? sub.sourceKey
131
- : undefined;
132
- entries.push({
133
- guid: sub.guid,
134
- kind: sub.kind,
135
- sourcePath: metaPath,
136
- ...(name === undefined ? {} : { name }),
137
- });
119
+ for (const asset of declaration.value.assets) {
120
+ if (typeof asset.name === 'string') {
121
+ names.set(`${declaration.sourcePath}:${asset.guid.toLowerCase()}`, asset.name);
138
122
  }
139
123
  }
140
124
  }
141
- for (const sourcePath of result.value.filter((path) => path.endsWith('.pack.ts'))) {
142
- const loaded = await loadScriptablePack(sourcePath, { metadataOnly: true });
143
- if (!loaded.ok) {
144
- emitError(ctx, loaded.error);
145
- return { ok: false };
146
- }
147
- const meta = projectScriptablePackMeta(loaded.value, sourcePath);
148
- for (const sub of meta.subAssets) {
149
- entries.push({ guid: sub.guid, kind: sub.kind, sourcePath });
150
- }
151
- }
152
- const firstByGuid = new Map<string, PackEntry>();
153
- for (const entry of entries) {
154
- const guid = entry.guid.toLowerCase();
155
- const first = firstByGuid.get(guid);
156
- if (first !== undefined) {
157
- emitError(ctx, {
158
- code: 'pack-guid-collision',
159
- expected:
160
- 'one author declaration per GUID across Meta, Pack v2, and ScriptablePack sources',
161
- hint: 'allocate a new durable GUID through the asset authoring gateway, then rerun verify',
162
- detail: { guid, paths: [first.sourcePath, entry.sourcePath] },
163
- });
164
- return { ok: false };
165
- }
166
- firstByGuid.set(guid, entry);
125
+ for (const inventoryEntry of result.value.inventory) {
126
+ const declaration = result.value.declarations.get(inventoryEntry.sourcePath) as
127
+ | ScanSourceDeclaration
128
+ | undefined;
129
+ const namedByKey =
130
+ inventoryEntry.sourceKey === undefined
131
+ ? names.get(`${inventoryEntry.sourcePath}:${inventoryEntry.guid.toLowerCase()}`)
132
+ : names.get(`${inventoryEntry.sourcePath}:${inventoryEntry.sourceKey}`);
133
+ const name =
134
+ namedByKey ?? names.get(`${inventoryEntry.sourcePath}:${inventoryEntry.guid.toLowerCase()}`);
135
+ entries.push({
136
+ guid: inventoryEntry.guid,
137
+ kind: inventoryEntry.kind,
138
+ sourcePath: inventoryEntry.sourcePath,
139
+ ...(name === undefined ? {} : { name }),
140
+ ...(declaration?.format === 'pack.json' &&
141
+ declaration.value.schemaVersion === '3.0.0' &&
142
+ inventoryEntry.sourceKey !== undefined &&
143
+ name === undefined
144
+ ? { name: inventoryEntry.sourceKey }
145
+ : {}),
146
+ });
167
147
  }
148
+ entries.sort((left, right) =>
149
+ `${left.sourcePath}:${left.guid}`.localeCompare(`${right.sourcePath}:${right.guid}`),
150
+ );
168
151
  return { ok: true, value: entries };
169
152
  }
170
153
 
@@ -250,9 +233,11 @@ async function runMeta(rest: string[], ctx: AssetCtx): Promise<number> {
250
233
  });
251
234
  }
252
235
  const cwd = ctx.cwd ?? process.cwd();
253
- const loaded = await loadScriptablePack(absolutePath(source, cwd), { metadataOnly: true });
236
+ const loaded = await loadParameterizedScriptablePack(absolutePath(source, cwd), {
237
+ metadataOnly: true,
238
+ });
254
239
  if (!loaded.ok) return emitError(ctx, loaded.error);
255
- ctx.stdoutWrite(JSON.stringify(projectScriptablePackMeta(loaded.value, source)));
240
+ ctx.stdoutWrite(JSON.stringify(projectParameterizedPackMeta(loaded.value, source)));
256
241
  return 0;
257
242
  }
258
243
 
@@ -16,27 +16,28 @@ function baseName(p: string): string {
16
16
  *
17
17
  * Pure function -- zero IO, zero side effects, browser-safe. Build-time
18
18
  * (build-catalog) and runtime (resolveName) share this single source of truth
19
- * for the XOR name-resolution rules (plan-strategy D-6).
19
+ * for deterministic asset display-name resolution.
20
20
  *
21
- * Branches:
22
- * 1. single-asset package (assetCount === 1) -> baseName(packagePath)
23
- * 2. multi-asset package + storedName -> storedName
24
- * 3. multi-asset package + no storedName (AC-15.1) -> baseName(packagePath)
25
- * 4. null packagePath + no storedName (AC-15.2) -> ''
21
+ * Resolution contract:
22
+ * - an explicit storedName is authoritative;
23
+ * - a package path is the fallback when no storedName is present;
24
+ * - an asset without a package and without a storedName resolves to ''.
25
+ *
26
+ * `assetCount` remains in the signature because callers derive it from their
27
+ * package model, but it must not erase an explicitly authored name. In
28
+ * particular, a single authored entry in `Materials.pack.json` resolves to its
29
+ * entry name rather than temporarily appearing as the package basename.
26
30
  */
27
31
  export function deriveAssetName(
28
32
  packagePath: string | null,
29
- assetCount: number,
33
+ _assetCount: number,
30
34
  storedName?: string,
31
35
  ): string {
32
- if (packagePath === null) {
33
- return storedName ?? '';
34
- }
35
- if (assetCount === 1) {
36
- return baseName(packagePath);
37
- }
38
36
  if (storedName !== undefined) {
39
37
  return storedName;
40
38
  }
39
+ if (packagePath === null) {
40
+ return '';
41
+ }
41
42
  return baseName(packagePath);
42
43
  }
@@ -139,7 +139,9 @@ export function collectMaterialCookRefs(material: Partial<MaterialAsset>): Mater
139
139
  ? new Set<string>(MATERIAL_TEXTURE_SLOTS)
140
140
  : new Set(
141
141
  material.parameters
142
- .filter((parameter) => parameter.type === 'texture')
142
+ .filter(
143
+ (parameter) => parameter.type === 'texture' || parameter.type === 'texture_cube',
144
+ )
143
145
  .map((parameter) => parameter.name),
144
146
  );
145
147
  const textures = textureValues(material.values, textureFields);