@forgeax/engine-pack 0.1.21 → 0.1.24

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 (139) hide show
  1. package/README.md +167 -48
  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__/material-cook-receipt-layer-plan.unit.test.d.ts +2 -0
  5. package/dist/__tests__/material-cook-receipt-layer-plan.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/pack-authoring-gateway.unit.test.d.ts +2 -0
  7. package/dist/__tests__/pack-authoring-gateway.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/pack-authoring.unit.test.d.ts +2 -0
  9. package/dist/__tests__/pack-authoring.unit.test.d.ts.map +1 -0
  10. package/dist/__tests__/pack-scanner.unit.test.d.ts +2 -0
  11. package/dist/__tests__/pack-scanner.unit.test.d.ts.map +1 -0
  12. package/dist/__tests__/scanner-blacklist.test.d.ts +2 -0
  13. package/dist/__tests__/scanner-blacklist.test.d.ts.map +1 -0
  14. package/dist/build.d.ts +9 -4
  15. package/dist/build.d.ts.map +1 -1
  16. package/dist/build.mjs +3449 -897
  17. package/dist/build.mjs.map +1 -1
  18. package/dist/builtin.mjs +37 -4
  19. package/dist/builtin.mjs.map +1 -1
  20. package/dist/catalog-builder.d.ts.map +1 -1
  21. package/dist/cli-asset.d.ts.map +1 -1
  22. package/dist/cli-asset.mjs +1361 -845
  23. package/dist/cli-asset.mjs.map +1 -1
  24. package/dist/deriveAssetName.d.ts +11 -7
  25. package/dist/deriveAssetName.d.ts.map +1 -1
  26. package/dist/evidence/material-cook.d.ts +11 -0
  27. package/dist/evidence/material-cook.d.ts.map +1 -1
  28. package/dist/evidence/source-inventory.d.ts.map +1 -1
  29. package/dist/guid.d.ts +15 -0
  30. package/dist/guid.d.ts.map +1 -1
  31. package/dist/guid.mjs +53 -5
  32. package/dist/guid.mjs.map +1 -1
  33. package/dist/index.d.ts +4 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.mjs +1297 -562
  36. package/dist/index.mjs.map +1 -1
  37. package/dist/inventory/binding.d.ts +6 -0
  38. package/dist/inventory/binding.d.ts.map +1 -0
  39. package/dist/inventory/declaration.d.ts +24 -0
  40. package/dist/inventory/declaration.d.ts.map +1 -0
  41. package/dist/inventory/index.d.ts +3 -0
  42. package/dist/inventory/index.d.ts.map +1 -0
  43. package/dist/inventory/sync.d.ts +5 -0
  44. package/dist/inventory/sync.d.ts.map +1 -0
  45. package/dist/material-cook.d.ts +1 -1
  46. package/dist/material-cook.d.ts.map +1 -1
  47. package/dist/material-cook.mjs +50 -3
  48. package/dist/material-cook.mjs.map +1 -1
  49. package/dist/name.mjs +4 -7
  50. package/dist/name.mjs.map +1 -1
  51. package/dist/pack-authoring-node.d.ts +26 -0
  52. package/dist/pack-authoring-node.d.ts.map +1 -0
  53. package/dist/pack-authoring-node.mjs +11089 -0
  54. package/dist/pack-authoring-node.mjs.map +1 -0
  55. package/dist/pack-authoring.d.ts +230 -0
  56. package/dist/pack-authoring.d.ts.map +1 -0
  57. package/dist/pack-authoring.mjs +1137 -0
  58. package/dist/pack-authoring.mjs.map +1 -0
  59. package/dist/package-finalizer.d.ts +2 -20
  60. package/dist/package-finalizer.d.ts.map +1 -1
  61. package/dist/resolve-asset-source.d.ts +1 -3
  62. package/dist/resolve-asset-source.d.ts.map +1 -1
  63. package/dist/resolve-asset-source.mjs +6 -81
  64. package/dist/resolve-asset-source.mjs.map +1 -1
  65. package/dist/runtime.mjs +77 -7
  66. package/dist/runtime.mjs.map +1 -1
  67. package/dist/scanner.d.ts +18 -7
  68. package/dist/scanner.d.ts.map +1 -1
  69. package/dist/scanner.mjs +1164 -687
  70. package/dist/scanner.mjs.map +1 -1
  71. package/dist/schema-compiled.d.ts.map +1 -1
  72. package/dist/schema.mjs +79 -7
  73. package/dist/schema.mjs.map +1 -1
  74. package/dist/scriptable-pack-node.d.ts +7 -31
  75. package/dist/scriptable-pack-node.d.ts.map +1 -1
  76. package/dist/scriptable-pack-node.mjs +572 -1019
  77. package/dist/scriptable-pack-node.mjs.map +1 -1
  78. package/dist/scriptable-pack-worker.mjs +29 -11
  79. package/dist/scriptable-pack-worker.mjs.map +1 -1
  80. package/dist/scriptable-pack.d.ts +20 -532
  81. package/dist/scriptable-pack.d.ts.map +1 -1
  82. package/dist/scriptable-pack.mjs +1038 -527
  83. package/dist/scriptable-pack.mjs.map +1 -1
  84. package/package.json +2 -7
  85. package/schema/meta.schema.json +1 -1
  86. package/schema/pack.schema.json +76 -6
  87. package/src/__tests__/guid-collision.unit.test.ts +18 -9
  88. package/src/__tests__/inventory-schema.test.ts +109 -0
  89. package/src/__tests__/material-cook-receipt-layer-plan.unit.test.ts +16 -0
  90. package/src/__tests__/material-cook-schema.unit.test.ts +62 -0
  91. package/src/__tests__/pack-authoring-gateway.unit.test.ts +249 -0
  92. package/src/__tests__/pack-authoring.unit.test.ts +266 -0
  93. package/src/__tests__/pack-scanner.unit.test.ts +123 -0
  94. package/src/__tests__/pack.unit.test.ts +1 -238
  95. package/src/__tests__/package-finalizer.contract.test.ts +55 -0
  96. package/src/__tests__/resolve-asset-source.test.ts +15 -98
  97. package/src/__tests__/scanner-blacklist.test.ts +55 -0
  98. package/src/__tests__/scanner-inventory.contract.test.ts +11 -14
  99. package/src/__tests__/scanner-inventory.test.ts +23 -10
  100. package/src/__tests__/scriptable-pack-cli.integration.test.ts +4 -9
  101. package/src/__tests__/topology.unit.test.ts +23 -0
  102. package/src/build.ts +28 -18
  103. package/src/catalog-builder.ts +60 -33
  104. package/src/cli-asset.ts +68 -83
  105. package/src/deriveAssetName.ts +14 -13
  106. package/src/evidence/material-cook.ts +79 -3
  107. package/src/evidence/source-inventory.ts +34 -21
  108. package/src/guid.ts +65 -4
  109. package/src/index.ts +15 -11
  110. package/src/inventory/binding.ts +25 -0
  111. package/src/inventory/declaration.ts +168 -0
  112. package/src/inventory/index.ts +18 -0
  113. package/src/inventory/sync.ts +22 -0
  114. package/src/material-cook.ts +1 -0
  115. package/src/pack-authoring-node.ts +1966 -0
  116. package/src/pack-authoring.ts +1478 -0
  117. package/src/package-finalizer.ts +2 -42
  118. package/src/resolve-asset-source.ts +4 -76
  119. package/src/scanner.ts +236 -56
  120. package/src/schema/material-cook.schema.json +4 -1
  121. package/src/schema-compiled.ts +2 -0
  122. package/src/scriptable-pack-node.ts +108 -719
  123. package/src/scriptable-pack-worker.ts +48 -16
  124. package/src/scriptable-pack.ts +28 -939
  125. package/dist/.tsbuildinfo +0 -1
  126. package/dist/__tests__/load-asset-config.test.d.ts +0 -2
  127. package/dist/__tests__/load-asset-config.test.d.ts.map +0 -1
  128. package/dist/__tests__/scriptable-pack.test-d.d.ts +0 -2
  129. package/dist/__tests__/scriptable-pack.test-d.d.ts.map +0 -1
  130. package/dist/__tests__/scriptable-pack.unit.test.d.ts +0 -2
  131. package/dist/__tests__/scriptable-pack.unit.test.d.ts.map +0 -1
  132. package/dist/config.d.ts +0 -6
  133. package/dist/config.d.ts.map +0 -1
  134. package/dist/config.mjs +0 -29
  135. package/dist/config.mjs.map +0 -1
  136. package/src/__tests__/load-asset-config.test.ts +0 -121
  137. package/src/__tests__/scriptable-pack.test-d.ts +0 -131
  138. package/src/__tests__/scriptable-pack.unit.test.ts +0 -789
  139. package/src/config.ts +0 -36
@@ -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 { parsePackSourceJson, projectDirectPackJson } from './pack-authoring.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,7 +195,6 @@ 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,
@@ -214,19 +202,10 @@ function projectSource(
214
202
  sourceIdentityFor: CatalogBuildProjectionOptions['sourceIdentityFor'],
215
203
  ): { readonly entries: PackIndexEntry[]; readonly error?: CatalogBuildError } {
216
204
  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
- };
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 = parsePackSourceJson(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,8 +26,8 @@ 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';
29
+ import { projectScriptablePackMeta } from './pack-authoring.js';
30
+ import { type ScanSourceDeclaration, scanInventory } from './scanner.js';
31
31
  import { loadScriptablePack } from './scriptable-pack-node.js';
32
32
 
33
33
  export interface PackEntry {
@@ -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. ScriptablePack
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,7 +233,9 @@ 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 loadScriptablePack(absolutePath(source, cwd), {
237
+ metadataOnly: true,
238
+ });
254
239
  if (!loaded.ok) return emitError(ctx, loaded.error);
255
240
  ctx.stdoutWrite(JSON.stringify(projectScriptablePackMeta(loaded.value, source)));
256
241
  return 0;
@@ -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
  }
@@ -7,7 +7,7 @@ import type {
7
7
  MaterialValue,
8
8
  Result,
9
9
  } from '@forgeax/engine-types';
10
- import { err, MATERIAL_TEXTURE_SLOTS, ok } from '@forgeax/engine-types';
10
+ import { deriveStandardLayerPlan, err, MATERIAL_TEXTURE_SLOTS, ok } from '@forgeax/engine-types';
11
11
  import { sha256 } from '@noble/hashes/sha2.js';
12
12
  import { bytesToHex } from '@noble/hashes/utils.js';
13
13
 
@@ -56,7 +56,11 @@ export interface MaterialCookReceipt {
56
56
  readonly profile: string;
57
57
  readonly compilerVersion: string;
58
58
  readonly identity: MaterialCookIdentity;
59
- readonly derivedInterface: { readonly layoutIdentity: string };
59
+ readonly derivedInterface: {
60
+ readonly layoutIdentity: string;
61
+ /** Build-time Standard physical lowering identity, when the root is Standard. */
62
+ readonly layerPlanIdentity?: string;
63
+ };
60
64
  }
61
65
 
62
66
  export interface MaterialParameterContract {
@@ -101,6 +105,36 @@ export interface MaterialCookIdentityExpectation {
101
105
  readonly inputDigest?: string;
102
106
  }
103
107
 
108
+ const STANDARD_ROOT_MODULES = new Set([
109
+ 'forgeax::default-standard-pbr',
110
+ 'forgeax::pbr-skin',
111
+ 'forgeax_material::standard',
112
+ 'forgeax_material::pbr-skin',
113
+ ]);
114
+
115
+ /** Shared ownership predicate for the built-in Standard material root. */
116
+ export function isStandardRootModule(module: string): boolean {
117
+ return STANDARD_ROOT_MODULES.has(module);
118
+ }
119
+
120
+ export function isStandardMaterialRecord(record: Pick<CookedMaterialRecord, 'resolved'>): boolean {
121
+ return record.resolved.passes.some((pass) => isStandardRootModule(pass.program.module));
122
+ }
123
+
124
+ /**
125
+ * Recompute the Standard layer identity from the admitted resolved root.
126
+ * Material admission and runtime loading share this owner so a stale receipt
127
+ * cannot select a different physical layer plan after cooking.
128
+ */
129
+ export function materialLayerPlanIdentity(
130
+ record: Pick<CookedMaterialRecord, 'resolved'>,
131
+ ): string | undefined {
132
+ if (!isStandardMaterialRecord(record)) {
133
+ return undefined;
134
+ }
135
+ return deriveStandardLayerPlan(record.resolved.parameters, record.resolved.passes).identity;
136
+ }
137
+
104
138
  function unique(values: readonly string[]): readonly string[] {
105
139
  return [...new Set(values)].sort();
106
140
  }
@@ -286,6 +320,16 @@ export function validateMaterialCookReceipt(
286
320
  if (derivedInterface.layoutIdentity !== identity.layoutIdentity) {
287
321
  return invalid('receipt.derivedInterface.layoutIdentity', derivedInterface.layoutIdentity);
288
322
  }
323
+ if (
324
+ derivedInterface.layerPlanIdentity !== undefined &&
325
+ (typeof derivedInterface.layerPlanIdentity !== 'string' ||
326
+ derivedInterface.layerPlanIdentity.length === 0)
327
+ ) {
328
+ return invalid(
329
+ 'receipt.derivedInterface.layerPlanIdentity',
330
+ derivedInterface.layerPlanIdentity,
331
+ );
332
+ }
289
333
  for (const field of ['sourceClosure', 'profile', 'compilerVersion']) {
290
334
  const fieldValue = candidate[field];
291
335
  if (
@@ -320,7 +364,12 @@ export function validateMaterialCookReceipt(
320
364
  profile: candidate.profile as string,
321
365
  compilerVersion: candidate.compilerVersion as string,
322
366
  identity,
323
- derivedInterface: { layoutIdentity: derivedInterface.layoutIdentity as string },
367
+ derivedInterface: {
368
+ layoutIdentity: derivedInterface.layoutIdentity as string,
369
+ ...(derivedInterface.layerPlanIdentity === undefined
370
+ ? {}
371
+ : { layerPlanIdentity: derivedInterface.layerPlanIdentity as string }),
372
+ },
324
373
  });
325
374
  }
326
375
 
@@ -380,6 +429,33 @@ export function validateCookedMaterialRecord(
380
429
  artifactDigest: artifact.digest as string,
381
430
  });
382
431
  if (!receiptResult.ok) return receiptResult;
432
+ const resolved = candidate.resolved as Record<string, unknown>;
433
+ if (!Array.isArray(resolved.passes)) return invalid('resolved.passes', resolved.passes);
434
+ if (!Array.isArray(resolved.parameters))
435
+ return invalid('resolved.parameters', resolved.parameters);
436
+ if (
437
+ resolved.values === null ||
438
+ typeof resolved.values !== 'object' ||
439
+ Array.isArray(resolved.values)
440
+ )
441
+ return invalid('resolved.values', resolved.values);
442
+ let expectedLayerPlanIdentity: string | undefined;
443
+ try {
444
+ expectedLayerPlanIdentity = materialLayerPlanIdentity({
445
+ resolved: resolved as unknown as CookedMaterialRecord['resolved'],
446
+ });
447
+ } catch (error) {
448
+ return invalid('resolved.layerPlanIdentity', error instanceof Error ? error.message : error);
449
+ }
450
+ if (
451
+ expectedLayerPlanIdentity !== undefined &&
452
+ receiptResult.value.derivedInterface.layerPlanIdentity !== expectedLayerPlanIdentity
453
+ ) {
454
+ return invalid(
455
+ 'receipt.derivedInterface.layerPlanIdentity',
456
+ receiptResult.value.derivedInterface.layerPlanIdentity,
457
+ );
458
+ }
383
459
  if (candidate.artifactDigest !== undefined && candidate.artifactDigest !== artifact.digest)
384
460
  return invalid('artifactDigest', candidate.artifactDigest);
385
461
  if (
@@ -2,9 +2,8 @@ import { createHash } from 'node:crypto';
2
2
  import { readdir, readFile } from 'node:fs/promises';
3
3
  import { basename, dirname, join, relative, resolve, sep } from 'node:path';
4
4
  import type { SourceDeclarationEvidence } from '@forgeax/engine-types';
5
+ import { parsePackSourceJson, projectDirectPackJson } from '../pack-authoring.js';
5
6
  import { SCANNER_BLACKLIST, scan } from '../scanner.js';
6
- import { projectScriptablePackMeta } from '../scriptable-pack.js';
7
- import { loadScriptablePack } from '../scriptable-pack-node.js';
8
7
 
9
8
  /** Project root and GUID used to locate an authoritative source declaration. */
10
9
  export interface SourceInventoryRequest {
@@ -150,34 +149,43 @@ export async function readSourceInventory(
150
149
  let authored: SourceDeclarationEvidence | undefined;
151
150
  for (const path of paths) {
152
151
  if (path.endsWith('.pack.ts')) {
153
- const loaded = await loadScriptablePack(path, { metadataOnly: true });
154
- if (!loaded.ok) continue;
155
- const meta = projectScriptablePackMeta(loaded.value, path);
156
- if (meta.subAssets.some((asset) => asset.guid.toLowerCase() === guid)) {
157
- return { origin: 'sourceMeta', sourcePath: path };
158
- }
152
+ // ScriptablePack source identity is a build result, not a declaration
153
+ // table. The source index therefore records the source only after the
154
+ // producer publishes its materialized Pack output.
159
155
  continue;
160
156
  }
161
157
  const raw = await readFile(path, 'utf8').catch(() => undefined);
162
158
  if (raw === undefined) continue;
163
- let parsed: {
164
- readonly assets?: readonly { readonly guid?: unknown }[];
165
- readonly source?: unknown;
166
- readonly inputFingerprint?: unknown;
167
- readonly importSettings?: unknown;
168
- readonly subAssets?: readonly { readonly guid?: unknown }[];
169
- };
159
+ let parsed: Record<string, unknown>;
170
160
  try {
171
- parsed = JSON.parse(raw) as typeof parsed;
161
+ const value: unknown = JSON.parse(raw);
162
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) continue;
163
+ parsed = value as Record<string, unknown>;
172
164
  } catch {
173
165
  continue;
174
166
  }
175
167
  if (path.endsWith('.pack.json')) {
176
- if (
177
- parsed.assets?.some(
168
+ if (parsed.schemaVersion === '3.0.0') {
169
+ const direct = parsePackSourceJson(parsed);
170
+ if (direct.ok && direct.value.format === 'direct') {
171
+ const projected = projectDirectPackJson(direct.value);
172
+ if (
173
+ projected.ok &&
174
+ projected.value.assets.some((asset) => asset.guid.toLowerCase() === guid)
175
+ ) {
176
+ authored = { origin: 'authoredPack', sourcePath: path };
177
+ }
178
+ }
179
+ } else if (
180
+ Array.isArray(parsed.assets) &&
181
+ parsed.assets.some(
178
182
  (asset) =>
179
- typeof asset.guid === 'string' &&
180
- (asset.guid === request.guid || asset.guid.toLowerCase() === guid),
183
+ asset !== null &&
184
+ typeof asset === 'object' &&
185
+ !Array.isArray(asset) &&
186
+ typeof (asset as { readonly guid?: unknown }).guid === 'string' &&
187
+ ((asset as { readonly guid: string }).guid === request.guid ||
188
+ (asset as { readonly guid: string }).guid.toLowerCase() === guid),
181
189
  )
182
190
  ) {
183
191
  authored = { origin: 'authoredPack', sourcePath: path };
@@ -185,7 +193,12 @@ export async function readSourceInventory(
185
193
  continue;
186
194
  }
187
195
  if (path.endsWith('.meta.json')) {
188
- const source = sourceFromMeta(path, parsed, request.guid, raw);
196
+ const source = sourceFromMeta(
197
+ path,
198
+ parsed as Parameters<typeof sourceFromMeta>[1],
199
+ request.guid,
200
+ raw,
201
+ );
189
202
  if (source !== undefined) return source;
190
203
  }
191
204
  }