@forgeax/engine-pack 0.1.21 → 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 (124) 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 +3314 -803
  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/source-inventory.d.ts.map +1 -1
  25. package/dist/guid.d.ts +15 -0
  26. package/dist/guid.d.ts.map +1 -1
  27. package/dist/guid.mjs +53 -5
  28. package/dist/guid.mjs.map +1 -1
  29. package/dist/index.d.ts +3 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.mjs +1252 -560
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/inventory/binding.d.ts +6 -0
  34. package/dist/inventory/binding.d.ts.map +1 -0
  35. package/dist/inventory/declaration.d.ts +24 -0
  36. package/dist/inventory/declaration.d.ts.map +1 -0
  37. package/dist/inventory/index.d.ts +3 -0
  38. package/dist/inventory/index.d.ts.map +1 -0
  39. package/dist/inventory/sync.d.ts +5 -0
  40. package/dist/inventory/sync.d.ts.map +1 -0
  41. package/dist/name.mjs +4 -7
  42. package/dist/name.mjs.map +1 -1
  43. package/dist/package-finalizer.d.ts +2 -20
  44. package/dist/package-finalizer.d.ts.map +1 -1
  45. package/dist/parameterized-pack-node.d.ts +26 -0
  46. package/dist/parameterized-pack-node.d.ts.map +1 -0
  47. package/dist/parameterized-pack-node.mjs +11093 -0
  48. package/dist/parameterized-pack-node.mjs.map +1 -0
  49. package/dist/parameterized-pack.d.ts +233 -0
  50. package/dist/parameterized-pack.d.ts.map +1 -0
  51. package/dist/resolve-asset-source.d.ts +1 -3
  52. package/dist/resolve-asset-source.d.ts.map +1 -1
  53. package/dist/resolve-asset-source.mjs +6 -81
  54. package/dist/resolve-asset-source.mjs.map +1 -1
  55. package/dist/runtime.mjs +77 -7
  56. package/dist/runtime.mjs.map +1 -1
  57. package/dist/scanner.d.ts +19 -8
  58. package/dist/scanner.d.ts.map +1 -1
  59. package/dist/scanner.mjs +1169 -690
  60. package/dist/scanner.mjs.map +1 -1
  61. package/dist/schema-compiled.d.ts.map +1 -1
  62. package/dist/schema.mjs +79 -7
  63. package/dist/schema.mjs.map +1 -1
  64. package/dist/scriptable-pack-node.d.ts +8 -32
  65. package/dist/scriptable-pack-node.d.ts.map +1 -1
  66. package/dist/scriptable-pack-node.mjs +576 -1021
  67. package/dist/scriptable-pack-node.mjs.map +1 -1
  68. package/dist/scriptable-pack-worker.mjs +29 -11
  69. package/dist/scriptable-pack-worker.mjs.map +1 -1
  70. package/dist/scriptable-pack.d.ts +19 -532
  71. package/dist/scriptable-pack.d.ts.map +1 -1
  72. package/dist/scriptable-pack.mjs +1042 -527
  73. package/dist/scriptable-pack.mjs.map +1 -1
  74. package/package.json +7 -7
  75. package/schema/meta.schema.json +1 -1
  76. package/schema/pack.schema.json +76 -6
  77. package/src/__tests__/guid-collision.unit.test.ts +18 -9
  78. package/src/__tests__/inventory-schema.test.ts +109 -0
  79. package/src/__tests__/pack.unit.test.ts +1 -238
  80. package/src/__tests__/package-finalizer.contract.test.ts +55 -0
  81. package/src/__tests__/parameterized-pack-authoring-gateway.unit.test.ts +249 -0
  82. package/src/__tests__/parameterized-pack-scanner.unit.test.ts +123 -0
  83. package/src/__tests__/parameterized-pack.unit.test.ts +266 -0
  84. package/src/__tests__/resolve-asset-source.test.ts +15 -98
  85. package/src/__tests__/scanner-blacklist.test.ts +55 -0
  86. package/src/__tests__/scanner-inventory.contract.test.ts +14 -15
  87. package/src/__tests__/scanner-inventory.test.ts +24 -12
  88. package/src/__tests__/scriptable-pack-cli.integration.test.ts +4 -9
  89. package/src/__tests__/topology.unit.test.ts +23 -0
  90. package/src/build.ts +28 -18
  91. package/src/catalog-builder.ts +61 -34
  92. package/src/cli-asset.ts +70 -85
  93. package/src/deriveAssetName.ts +14 -13
  94. package/src/evidence/source-inventory.ts +34 -21
  95. package/src/guid.ts +65 -4
  96. package/src/index.ts +12 -11
  97. package/src/inventory/binding.ts +25 -0
  98. package/src/inventory/declaration.ts +168 -0
  99. package/src/inventory/index.ts +18 -0
  100. package/src/inventory/sync.ts +22 -0
  101. package/src/package-finalizer.ts +2 -42
  102. package/src/parameterized-pack-node.ts +1968 -0
  103. package/src/parameterized-pack.ts +1493 -0
  104. package/src/resolve-asset-source.ts +4 -76
  105. package/src/scanner.ts +241 -59
  106. package/src/schema-compiled.ts +2 -0
  107. package/src/scriptable-pack-node.ts +111 -722
  108. package/src/scriptable-pack-worker.ts +48 -16
  109. package/src/scriptable-pack.ts +27 -939
  110. package/dist/.tsbuildinfo +0 -1
  111. package/dist/__tests__/load-asset-config.test.d.ts +0 -2
  112. package/dist/__tests__/load-asset-config.test.d.ts.map +0 -1
  113. package/dist/__tests__/scriptable-pack.test-d.d.ts +0 -2
  114. package/dist/__tests__/scriptable-pack.test-d.d.ts.map +0 -1
  115. package/dist/__tests__/scriptable-pack.unit.test.d.ts +0 -2
  116. package/dist/__tests__/scriptable-pack.unit.test.d.ts.map +0 -1
  117. package/dist/config.d.ts +0 -6
  118. package/dist/config.d.ts.map +0 -1
  119. package/dist/config.mjs +0 -29
  120. package/dist/config.mjs.map +0 -1
  121. package/src/__tests__/load-asset-config.test.ts +0 -121
  122. package/src/__tests__/scriptable-pack.test-d.ts +0 -131
  123. package/src/__tests__/scriptable-pack.unit.test.ts +0 -789
  124. package/src/config.ts +0 -36
@@ -0,0 +1,1968 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
4
+ import type { AssetGuid as EngineAssetGuid, Result } from '@forgeax/engine-types';
5
+ import { err, ok } from '@forgeax/engine-types';
6
+ import { AssetGuid, isValidAssetGuidString, isValidPackSourceKey, PackageId } from './guid.js';
7
+ import {
8
+ type AnyParameterizedPackDefinition,
9
+ createParameterizedPackAuthoringGateway,
10
+ type DirectPackJsonAsset,
11
+ type PackAuthoringError,
12
+ type PackAuthoringErrorCode,
13
+ type PackParameterDefinition,
14
+ type PackParameterInheritanceSubject,
15
+ type ParameterizedPackGatewayPort,
16
+ type ParameterizedPackOperation,
17
+ type ParameterizedPackOperationResult,
18
+ type ParameterizedPackResolutionStatus,
19
+ parseParameterizedPackJson,
20
+ projectDirectPackJson,
21
+ type ResolvedPackParameterInheritance,
22
+ resolvePackParameterInheritance,
23
+ validatePackDefinition,
24
+ } from './parameterized-pack.js';
25
+ import { type ScanInventory, scanInventory } from './scanner.js';
26
+
27
+ export interface ParameterizedPackMaterializedAsset {
28
+ readonly packageId: string;
29
+ readonly sourceKey: string;
30
+ readonly guid: string;
31
+ readonly kind: string;
32
+ readonly sourcePath?: string;
33
+ readonly refs?: readonly string[];
34
+ readonly ready?: boolean;
35
+ readonly artifactsReady?: boolean;
36
+ }
37
+
38
+ export interface FileSystemParameterizedPackAuthoringOptions {
39
+ readonly gameRoot: string;
40
+ readonly scanRoots?: readonly string[];
41
+ /** Current-generation rows only; this callback is never called by mutations. */
42
+ readonly materialized?: () =>
43
+ | readonly ParameterizedPackMaterializedAsset[]
44
+ | Promise<readonly ParameterizedPackMaterializedAsset[]>;
45
+ /** Build owner callback; the gateway itself does not own DDC or Catalog state. */
46
+ readonly rebuild?: (
47
+ sourcePath: string,
48
+ mode: 'rebuild' | 'cold-cook',
49
+ ) => Promise<Result<unknown, PackAuthoringError>>;
50
+ /** Injection point for hosts that already own a validated Source Index. */
51
+ readonly inventory?: () => Promise<Result<ScanInventory, unknown>>;
52
+ }
53
+
54
+ interface ConfinedPath {
55
+ readonly absolute: string;
56
+ readonly relative: string;
57
+ }
58
+
59
+ interface ProjectedDirectAsset extends DirectPackJsonAsset {
60
+ readonly guid: string;
61
+ readonly sourceKey: string;
62
+ }
63
+
64
+ interface SubjectBase {
65
+ readonly packageId: PackageId;
66
+ readonly sourcePath: string;
67
+ readonly relativePath: string;
68
+ }
69
+
70
+ interface SourceSubject extends SubjectBase {
71
+ readonly format: 'source';
72
+ readonly definition: AnyParameterizedPackDefinition;
73
+ }
74
+
75
+ interface DirectSubject extends SubjectBase {
76
+ readonly format: 'direct';
77
+ readonly assets: readonly ProjectedDirectAsset[];
78
+ }
79
+
80
+ interface InstanceSubject extends SubjectBase {
81
+ readonly format: 'instance';
82
+ readonly parent: PackageId;
83
+ readonly values: Readonly<Record<string, unknown>>;
84
+ }
85
+
86
+ type Subject = SourceSubject | DirectSubject | InstanceSubject;
87
+
88
+ interface MaterializedAsset {
89
+ readonly packageId: string;
90
+ readonly sourceKey: string;
91
+ readonly guid: string;
92
+ readonly kind: string;
93
+ readonly sourcePath?: string;
94
+ readonly name?: string;
95
+ readonly refs?: readonly string[];
96
+ readonly ready: boolean;
97
+ }
98
+
99
+ type DirectAuthorAsset = Omit<MaterializedAsset, 'ready'>;
100
+
101
+ interface GatewaySnapshot {
102
+ readonly inventory: ScanInventory;
103
+ readonly subjects: ReadonlyMap<string, Subject>;
104
+ readonly directAssets: readonly DirectAuthorAsset[];
105
+ readonly materialized: readonly MaterializedAsset[];
106
+ readonly knownGuids: ReadonlySet<string>;
107
+ }
108
+
109
+ function isRecord(value: unknown): value is Record<string, unknown> {
110
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
111
+ }
112
+
113
+ function jsonValue(value: unknown): unknown {
114
+ if (value instanceof Uint8Array) return AssetGuid.format(value as EngineAssetGuid);
115
+ if (Array.isArray(value)) return value.map((item) => jsonValue(item));
116
+ if (isRecord(value)) {
117
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, jsonValue(item)]));
118
+ }
119
+ return value;
120
+ }
121
+
122
+ function revisionOf(source: string): string {
123
+ return `sha256:${createHash('sha256').update(source).digest('hex')}`;
124
+ }
125
+
126
+ function makeError(
127
+ code: PackAuthoringErrorCode,
128
+ expected: string,
129
+ hint: string,
130
+ detail: Readonly<Record<string, unknown>> = {},
131
+ actual?: string,
132
+ ): PackAuthoringError {
133
+ return {
134
+ code,
135
+ expected,
136
+ hint,
137
+ ...(actual === undefined ? {} : { actual }),
138
+ detail,
139
+ };
140
+ }
141
+
142
+ function pathError(
143
+ code:
144
+ | 'pack-source-path-invalid'
145
+ | 'pack-source-not-found'
146
+ | 'pack-source-write-failed'
147
+ | 'pack-source-revision-conflict',
148
+ operation: ParameterizedPackOperation,
149
+ expected: string,
150
+ hint: string,
151
+ detail: Readonly<Record<string, unknown>> = {},
152
+ actual?: string,
153
+ ): PackAuthoringError {
154
+ return makeError(code, expected, hint, { requestId: operation.requestId, ...detail }, actual);
155
+ }
156
+
157
+ function confinedPath(
158
+ gameRoot: string,
159
+ candidate: unknown,
160
+ operation: ParameterizedPackOperation,
161
+ ): Result<ConfinedPath, PackAuthoringError> {
162
+ if (typeof candidate !== 'string' || candidate.length === 0 || isAbsolute(candidate)) {
163
+ return err(
164
+ pathError(
165
+ 'pack-source-path-invalid',
166
+ operation,
167
+ 'a non-empty game-root-relative .pack.ts or .pack.json path',
168
+ 'pass a relative source locator inside the selected game root',
169
+ { sourcePath: candidate },
170
+ typeof candidate === 'string' ? candidate : undefined,
171
+ ),
172
+ );
173
+ }
174
+ const root = resolve(gameRoot);
175
+ const absolute = resolve(root, candidate);
176
+ const relativePath = relative(root, absolute).split(sep).join('/');
177
+ if (
178
+ relativePath.length === 0 ||
179
+ relativePath === '..' ||
180
+ relativePath.startsWith('../') ||
181
+ (!relativePath.endsWith('.pack.ts') && !relativePath.endsWith('.pack.json'))
182
+ ) {
183
+ return err(
184
+ pathError(
185
+ 'pack-source-path-invalid',
186
+ operation,
187
+ 'a game-root-relative .pack.ts or .pack.json path',
188
+ 'choose a Pack source path inside the selected game root',
189
+ { sourcePath: candidate },
190
+ candidate,
191
+ ),
192
+ );
193
+ }
194
+ return ok({ absolute, relative: relativePath });
195
+ }
196
+
197
+ async function atomicWrite(path: string, source: string): Promise<void> {
198
+ await mkdir(dirname(path), { recursive: true });
199
+ const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
200
+ try {
201
+ await writeFile(temporary, source, { encoding: 'utf8', flag: 'wx' });
202
+ await rename(temporary, path);
203
+ } catch (cause) {
204
+ await rm(temporary, { force: true });
205
+ throw cause;
206
+ }
207
+ }
208
+
209
+ function packageKey(packageId: PackageId): string {
210
+ return PackageId.format(packageId).toLowerCase();
211
+ }
212
+
213
+ function parsePackageId(value: unknown): PackageId | undefined {
214
+ if (typeof value !== 'string') return undefined;
215
+ const parsed = PackageId.parse(value);
216
+ return parsed.ok ? parsed.value : undefined;
217
+ }
218
+
219
+ function serializedParameters(
220
+ definition: AnyParameterizedPackDefinition,
221
+ ): readonly Readonly<Record<string, unknown>>[] | undefined {
222
+ if (!('parameters' in definition)) return undefined;
223
+ return definition.parameters.map((parameter) => ({
224
+ name: parameter.name,
225
+ type: parameter.type,
226
+ default: jsonValue(parameter.default),
227
+ ...(parameter.minimum === undefined ? {} : { minimum: parameter.minimum }),
228
+ ...(parameter.maximum === undefined ? {} : { maximum: parameter.maximum }),
229
+ ...(parameter.values === undefined ? {} : { values: parameter.values }),
230
+ ...(parameter.kind === undefined ? {} : { kind: parameter.kind }),
231
+ }));
232
+ }
233
+
234
+ function sourceResult(
235
+ subject: Subject,
236
+ operation: ParameterizedPackOperation,
237
+ ): ParameterizedPackOperationResult {
238
+ const base = {
239
+ operation: operation.operation,
240
+ requestId: operation.requestId,
241
+ sourcePath: subject.relativePath,
242
+ packageId: PackageId.format(subject.packageId),
243
+ format: subject.format === 'source' ? 'pack.ts' : subject.format,
244
+ } as const;
245
+ if (subject.format === 'source') {
246
+ const parameters = serializedParameters(subject.definition);
247
+ return {
248
+ ...base,
249
+ ...(parameters === undefined ? {} : { parameters }),
250
+ ...(parameters === undefined
251
+ ? {}
252
+ : {
253
+ capabilities: [
254
+ 'asset-source.create-instance',
255
+ 'asset-source.rebuild',
256
+ 'asset-source.cold-cook',
257
+ ],
258
+ }),
259
+ };
260
+ }
261
+ if (subject.format === 'instance') {
262
+ return {
263
+ ...base,
264
+ parentPackageId: PackageId.format(subject.parent),
265
+ values: jsonValue(subject.values) as Readonly<Record<string, unknown>>,
266
+ };
267
+ }
268
+ return {
269
+ ...base,
270
+ assets: subject.assets.map((asset) => ({
271
+ sourceKey: asset.sourceKey,
272
+ guid: asset.guid,
273
+ kind: asset.kind,
274
+ ...(asset.name === undefined ? {} : { name: asset.name }),
275
+ refs: asset.refs,
276
+ })),
277
+ };
278
+ }
279
+
280
+ function scanFailure(operation: ParameterizedPackOperation, cause: unknown): PackAuthoringError {
281
+ const value =
282
+ cause !== null && typeof cause === 'object' ? (cause as Record<string, unknown>) : {};
283
+ const detail = value.detail !== null && typeof value.detail === 'object' ? value.detail : {};
284
+ const reason = isRecord(detail) && typeof detail.reason === 'string' ? detail.reason : undefined;
285
+ const code: PackAuthoringErrorCode =
286
+ reason === 'pack-parent-not-found' ||
287
+ reason === 'pack-parent-cycle' ||
288
+ reason === 'pack-parent-not-parameterized'
289
+ ? reason
290
+ : value.code === 'pack-guid-collision'
291
+ ? 'pack-guid-collision'
292
+ : 'pack-parameter-invalid';
293
+ return makeError(
294
+ code,
295
+ typeof value.expected === 'string' ? value.expected : 'a valid Pack Source Index',
296
+ typeof value.hint === 'string'
297
+ ? value.hint
298
+ : 'repair the first source or package diagnostic, then retry',
299
+ { requestId: operation.requestId, cause: value.detail ?? String(cause) },
300
+ );
301
+ }
302
+
303
+ function scanRoots(options: FileSystemParameterizedPackAuthoringOptions): readonly string[] {
304
+ return options.scanRoots === undefined || options.scanRoots.length === 0
305
+ ? [resolve(options.gameRoot)]
306
+ : options.scanRoots.map((root) => resolve(options.gameRoot, root));
307
+ }
308
+
309
+ async function readMaterialized(
310
+ options: FileSystemParameterizedPackAuthoringOptions,
311
+ operation: ParameterizedPackOperation,
312
+ ): Promise<Result<readonly MaterializedAsset[], PackAuthoringError>> {
313
+ let rawRows: unknown;
314
+ try {
315
+ rawRows = (await options.materialized?.()) ?? [];
316
+ } catch (cause) {
317
+ return err(
318
+ makeError(
319
+ 'pack-parameter-invalid',
320
+ 'the materialized projection callback to return current-generation rows',
321
+ 'repair the build-owner projection callback, then retry the Pack operation',
322
+ {
323
+ requestId: operation.requestId,
324
+ cause: cause instanceof Error ? cause.message : String(cause),
325
+ },
326
+ ),
327
+ );
328
+ }
329
+ if (!Array.isArray(rawRows)) {
330
+ return err(
331
+ makeError(
332
+ 'pack-parameter-invalid',
333
+ 'the materialized projection callback to return an array',
334
+ 'repair the build-owner projection callback, then retry the Pack operation',
335
+ { requestId: operation.requestId, actual: typeof rawRows },
336
+ ),
337
+ );
338
+ }
339
+ const materialized: MaterializedAsset[] = [];
340
+ const pairOwners = new Map<string, string>();
341
+ const guidOwners = new Map<string, string>();
342
+ for (const rawRow of rawRows) {
343
+ if (!isRecord(rawRow)) {
344
+ return err(
345
+ makeError(
346
+ 'pack-parameter-invalid',
347
+ 'materialized rows to be objects',
348
+ 'repair the current generation projection before querying Pack authoring',
349
+ { requestId: operation.requestId, actual: typeof rawRow },
350
+ ),
351
+ );
352
+ }
353
+ const row = rawRow as unknown as ParameterizedPackMaterializedAsset;
354
+ const parsedPackageId = parsePackageId(row.packageId);
355
+ if (parsedPackageId === undefined) {
356
+ return err(
357
+ makeError(
358
+ 'pack-package-id-invalid',
359
+ 'materialized rows to contain a valid UUID packageId',
360
+ 'repair the current generation projection before querying Pack authoring',
361
+ { requestId: operation.requestId, packageId: row.packageId, sourceKey: row.sourceKey },
362
+ ),
363
+ );
364
+ }
365
+ if (!isValidPackSourceKey(row.sourceKey)) {
366
+ return err(
367
+ makeError(
368
+ 'pack-source-key-invalid',
369
+ 'materialized rows to contain a valid sourceKey',
370
+ 'repair the current generation projection before querying Pack authoring',
371
+ { requestId: operation.requestId, packageId: row.packageId, sourceKey: row.sourceKey },
372
+ ),
373
+ );
374
+ }
375
+ if (
376
+ typeof row.guid !== 'string' ||
377
+ !isValidAssetGuidString(row.guid) ||
378
+ typeof row.kind !== 'string' ||
379
+ row.kind.length === 0
380
+ ) {
381
+ return err(
382
+ makeError(
383
+ 'pack-parameter-invalid',
384
+ 'materialized rows to contain a valid GUID and non-empty kind',
385
+ 'repair the current generation projection before querying Pack authoring',
386
+ { requestId: operation.requestId, packageId: row.packageId, sourceKey: row.sourceKey },
387
+ ),
388
+ );
389
+ }
390
+ if (
391
+ row.refs !== undefined &&
392
+ (!Array.isArray(row.refs) ||
393
+ row.refs.some((ref) => typeof ref !== 'string' || !isValidAssetGuidString(ref)))
394
+ ) {
395
+ return err(
396
+ makeError(
397
+ 'pack-parameter-invalid',
398
+ 'materialized refs to contain UUID AssetGuid strings when present',
399
+ 'repair the current generation projection before querying Pack authoring',
400
+ { requestId: operation.requestId, packageId: row.packageId, sourceKey: row.sourceKey },
401
+ ),
402
+ );
403
+ }
404
+ if (row.sourcePath !== undefined && typeof row.sourcePath !== 'string') {
405
+ return err(
406
+ makeError(
407
+ 'pack-parameter-invalid',
408
+ 'materialized sourcePath to be a string when present',
409
+ 'repair the current generation projection before querying Pack authoring',
410
+ { requestId: operation.requestId, sourcePath: row.sourcePath },
411
+ ),
412
+ );
413
+ }
414
+ const packageId = PackageId.format(parsedPackageId).toLowerCase();
415
+ const expectedGuid = AssetGuid.format(
416
+ AssetGuid.derive(parsedPackageId, row.sourceKey),
417
+ ).toLowerCase();
418
+ const guid = row.guid.toLowerCase();
419
+ if (guid !== expectedGuid) {
420
+ return err(
421
+ makeError(
422
+ 'pack-guid-collision',
423
+ 'materialized GUID to equal UUIDv5(packageId, sourceKey)',
424
+ 'rebuild the current generation from the stable packageId and sourceKey pair',
425
+ {
426
+ requestId: operation.requestId,
427
+ packageId,
428
+ sourceKey: row.sourceKey,
429
+ expectedGuid,
430
+ observedGuid: row.guid,
431
+ },
432
+ ),
433
+ );
434
+ }
435
+ const pair = `${packageId}/${row.sourceKey}`;
436
+ const pairOwner = pairOwners.get(pair);
437
+ if (pairOwner !== undefined) {
438
+ return err(
439
+ makeError(
440
+ 'pack-guid-collision',
441
+ 'one materialized row per packageId/sourceKey pair',
442
+ 'remove the stale duplicate row from the current generation projection',
443
+ { requestId: operation.requestId, pair, paths: [pairOwner, row.sourcePath] },
444
+ ),
445
+ );
446
+ }
447
+ pairOwners.set(pair, row.sourcePath ?? pair);
448
+ const guidOwner = guidOwners.get(guid);
449
+ if (guidOwner !== undefined) {
450
+ return err(
451
+ makeError(
452
+ 'pack-guid-collision',
453
+ 'one materialized owner per derived AssetGuid',
454
+ 'repair the current generation projection so each derived GUID has one owner',
455
+ { requestId: operation.requestId, guid, paths: [guidOwner, row.sourcePath] },
456
+ ),
457
+ );
458
+ }
459
+ guidOwners.set(guid, row.sourcePath ?? pair);
460
+ materialized.push({
461
+ packageId,
462
+ sourceKey: row.sourceKey,
463
+ guid,
464
+ kind: row.kind,
465
+ ...(row.sourcePath === undefined ? {} : { sourcePath: row.sourcePath }),
466
+ ...(row.refs === undefined ? {} : { refs: Object.freeze([...row.refs]) }),
467
+ ready: row.ready === true && row.artifactsReady !== false,
468
+ });
469
+ }
470
+ materialized.sort((left, right) =>
471
+ `${left.packageId}/${left.sourceKey}`.localeCompare(`${right.packageId}/${right.sourceKey}`),
472
+ );
473
+ return ok(materialized);
474
+ }
475
+
476
+ async function createSnapshot(
477
+ options: FileSystemParameterizedPackAuthoringOptions,
478
+ operation: ParameterizedPackOperation,
479
+ ): Promise<Result<GatewaySnapshot, PackAuthoringError>> {
480
+ const scanned =
481
+ options.inventory === undefined
482
+ ? await scanInventory(scanRoots(options))
483
+ : await options.inventory();
484
+ if (!scanned.ok) return err(scanFailure(operation, scanned.error));
485
+ const subjects = new Map<string, Subject>();
486
+ const directAssets: DirectAuthorAsset[] = [];
487
+ for (const [sourcePath, declaration] of scanned.value.declarations) {
488
+ const relativePath = relative(resolve(options.gameRoot), sourcePath).split(sep).join('/');
489
+ if (declaration.format === 'pack.ts-parameterized') {
490
+ subjects.set(packageKey(declaration.definition.packageId), {
491
+ format: 'source',
492
+ packageId: declaration.definition.packageId,
493
+ sourcePath,
494
+ relativePath,
495
+ definition: declaration.definition,
496
+ });
497
+ continue;
498
+ }
499
+ if (declaration.format !== 'pack.json' || declaration.value.schemaVersion !== '3.0.0') continue;
500
+ const parsed = parseParameterizedPackJson(declaration.value);
501
+ if (!parsed.ok) return err(parsed.error);
502
+ if (parsed.value.format === 'direct') {
503
+ const projected = projectDirectPackJson(parsed.value);
504
+ if (!projected.ok) return err(projected.error);
505
+ const subject: DirectSubject = {
506
+ format: 'direct',
507
+ packageId: parsed.value.packageId,
508
+ sourcePath,
509
+ relativePath,
510
+ assets: projected.value.assets,
511
+ };
512
+ subjects.set(packageKey(subject.packageId), subject);
513
+ directAssets.push(
514
+ ...projected.value.assets.map((asset) => ({
515
+ packageId: projected.value.packageId.toLowerCase(),
516
+ sourceKey: asset.sourceKey,
517
+ guid: asset.guid.toLowerCase(),
518
+ kind: asset.kind,
519
+ sourcePath: relativePath,
520
+ ...(asset.name === undefined ? {} : { name: asset.name }),
521
+ refs: asset.refs,
522
+ })),
523
+ );
524
+ } else {
525
+ subjects.set(packageKey(parsed.value.packageId), {
526
+ format: 'instance',
527
+ packageId: parsed.value.packageId,
528
+ parent: parsed.value.parent,
529
+ values: parsed.value.values,
530
+ sourcePath,
531
+ relativePath,
532
+ });
533
+ }
534
+ }
535
+ const materializedResult = await readMaterialized(options, operation);
536
+ if (!materializedResult.ok) return materializedResult;
537
+ const materialized = materializedResult.value;
538
+ const guidOwners = new Map<string, string>();
539
+ const directPairs = new Set<string>();
540
+ for (const entry of scanned.value.inventory) {
541
+ const declaration = scanned.value.declarations.get(entry.sourcePath);
542
+ // scanInventory already projects v3 direct entries below; keep that
543
+ // source-local projection from colliding with its gateway subject copy.
544
+ if (declaration?.format === 'pack.json' && declaration.value.schemaVersion === '3.0.0') {
545
+ continue;
546
+ }
547
+ guidOwners.set(entry.guid.toLowerCase(), entry.sourcePath);
548
+ }
549
+ for (const asset of directAssets) {
550
+ directPairs.add(`${asset.packageId}/${asset.sourceKey}`);
551
+ const existing = guidOwners.get(asset.guid);
552
+ if (existing !== undefined) {
553
+ return err(
554
+ makeError(
555
+ 'pack-guid-collision',
556
+ 'one current-generation owner per AssetGuid',
557
+ 'repair the direct Pack or external source projection before querying Pack authoring',
558
+ {
559
+ requestId: operation.requestId,
560
+ guid: asset.guid,
561
+ paths: [existing, asset.sourcePath],
562
+ },
563
+ ),
564
+ );
565
+ }
566
+ guidOwners.set(asset.guid, asset.sourcePath ?? asset.packageId);
567
+ }
568
+ for (const asset of materialized) {
569
+ const existing = guidOwners.get(asset.guid);
570
+ const pair = `${asset.packageId}/${asset.sourceKey}`;
571
+ // A published row for a direct author asset is the expected current
572
+ // generation. It shares the source identity; only a different owner is a
573
+ // collision.
574
+ if (existing !== undefined && !directPairs.has(pair)) {
575
+ return err(
576
+ makeError(
577
+ 'pack-guid-collision',
578
+ 'one current-generation owner per AssetGuid',
579
+ 'remove the stale materialized row or repair the direct/source owner before querying Pack authoring',
580
+ {
581
+ requestId: operation.requestId,
582
+ guid: asset.guid,
583
+ paths: [existing, asset.sourcePath],
584
+ },
585
+ ),
586
+ );
587
+ }
588
+ guidOwners.set(asset.guid, asset.sourcePath ?? asset.packageId);
589
+ }
590
+ const knownGuids = new Set<string>([
591
+ ...scanned.value.inventory.map((entry) => entry.guid.toLowerCase()),
592
+ ...directAssets.map((asset) => asset.guid),
593
+ ...materialized.map((asset) => asset.guid),
594
+ ]);
595
+ return ok({ inventory: scanned.value, subjects, directAssets, materialized, knownGuids });
596
+ }
597
+
598
+ function parentSubject(
599
+ snapshot: GatewaySnapshot,
600
+ packageId: PackageId,
601
+ ): PackParameterInheritanceSubject | undefined {
602
+ const subject = snapshot.subjects.get(packageKey(packageId));
603
+ if (subject === undefined) return undefined;
604
+ if (subject.format === 'source') {
605
+ return {
606
+ format: 'source',
607
+ packageId: subject.packageId,
608
+ parameters: 'parameters' in subject.definition ? subject.definition.parameters : [],
609
+ };
610
+ }
611
+ if (subject.format === 'instance') {
612
+ return {
613
+ format: 'instance',
614
+ packageId: subject.packageId,
615
+ parent: subject.parent,
616
+ values: subject.values,
617
+ };
618
+ }
619
+ return { format: 'direct', packageId: subject.packageId };
620
+ }
621
+
622
+ async function resolveInstance(
623
+ snapshot: GatewaySnapshot,
624
+ subject: InstanceSubject,
625
+ ): Promise<Result<ResolvedPackParameterInheritance, PackAuthoringError>> {
626
+ return resolvePackParameterInheritance(
627
+ {
628
+ format: 'instance',
629
+ packageId: subject.packageId,
630
+ parent: subject.parent,
631
+ values: subject.values,
632
+ },
633
+ async (packageId) => parentSubject(snapshot, packageId),
634
+ );
635
+ }
636
+
637
+ function subjectFor(
638
+ gameRoot: string,
639
+ snapshot: GatewaySnapshot,
640
+ operation: ParameterizedPackOperation,
641
+ ): Result<Subject, PackAuthoringError> {
642
+ if (operation.sourcePath !== undefined) {
643
+ const confined = confinedPath(gameRoot, operation.sourcePath, operation);
644
+ if (!confined.ok) return confined;
645
+ const expected = confined.value.relative;
646
+ const source = [...snapshot.subjects.values()].find(
647
+ (candidate) => candidate.relativePath === expected,
648
+ );
649
+ if (source !== undefined) {
650
+ const requestedPackageId =
651
+ operation.packageId === undefined ? undefined : PackageId.parse(operation.packageId);
652
+ if (requestedPackageId?.ok === false) {
653
+ return err(
654
+ makeError(
655
+ 'pack-package-id-invalid',
656
+ 'a UUID packageId',
657
+ 'repair packageId before querying the Pack subject',
658
+ { requestId: operation.requestId, packageId: operation.packageId },
659
+ operation.packageId,
660
+ ),
661
+ );
662
+ }
663
+ if (
664
+ requestedPackageId?.ok === true &&
665
+ packageKey(requestedPackageId.value) !== packageKey(source.packageId)
666
+ ) {
667
+ return err(
668
+ makeError(
669
+ 'pack-parameter-invalid',
670
+ 'sourcePath and packageId to identify the same Pack subject',
671
+ 'remove the conflicting locator or use the packageId returned by asset.list',
672
+ {
673
+ requestId: operation.requestId,
674
+ sourcePath: expected,
675
+ packageId: operation.packageId,
676
+ observedPackageId: PackageId.format(source.packageId),
677
+ },
678
+ ),
679
+ );
680
+ }
681
+ return ok(source);
682
+ }
683
+ return err(
684
+ pathError(
685
+ 'pack-source-not-found',
686
+ operation,
687
+ 'sourcePath to identify a current Pack subject',
688
+ 'inspect the current Source Index and choose an existing source path',
689
+ { sourcePath: expected },
690
+ expected,
691
+ ),
692
+ );
693
+ }
694
+ if (operation.packageId !== undefined) {
695
+ const parsed = PackageId.parse(operation.packageId);
696
+ if (!parsed.ok) {
697
+ return err(
698
+ makeError(
699
+ 'pack-package-id-invalid',
700
+ 'a UUID packageId',
701
+ 'repair packageId before querying the Pack',
702
+ { requestId: operation.requestId, packageId: operation.packageId },
703
+ operation.packageId,
704
+ ),
705
+ );
706
+ }
707
+ const subject = snapshot.subjects.get(packageKey(parsed.value));
708
+ if (subject !== undefined) return ok(subject);
709
+ return err(
710
+ makeError(
711
+ 'pack-source-not-found',
712
+ 'packageId to exist in the current Source Index',
713
+ 'inspect the current source index and choose an existing packageId',
714
+ { requestId: operation.requestId, packageId: operation.packageId },
715
+ ),
716
+ );
717
+ }
718
+ if (operation.subject !== undefined && !isValidAssetGuidString(operation.subject)) {
719
+ const confined = confinedPath(gameRoot, operation.subject, operation);
720
+ if (!confined.ok) return confined;
721
+ const source = [...snapshot.subjects.values()].find(
722
+ (candidate) => candidate.relativePath === confined.value.relative,
723
+ );
724
+ if (source !== undefined) return ok(source);
725
+ }
726
+ if (operation.subject !== undefined && isValidAssetGuidString(operation.subject)) {
727
+ const guid = operation.subject.toLowerCase();
728
+ const packageSubject = snapshot.subjects.get(guid);
729
+ if (packageSubject !== undefined) return ok(packageSubject);
730
+ const asset = [...snapshot.directAssets, ...snapshot.materialized].find(
731
+ (candidate) => candidate.guid === guid,
732
+ );
733
+ if (asset !== undefined) {
734
+ const subject = snapshot.subjects.get(asset.packageId);
735
+ if (subject !== undefined) return ok(subject);
736
+ }
737
+ }
738
+ return err(
739
+ makeError(
740
+ 'pack-source-not-found',
741
+ 'a packageId, source path, or known AssetGuid subject',
742
+ 'run asset.list or asset.inspect to obtain a current subject locator',
743
+ { requestId: operation.requestId, subject: operation.subject },
744
+ ),
745
+ );
746
+ }
747
+
748
+ async function inspectAsset(
749
+ gameRoot: string,
750
+ snapshot: GatewaySnapshot,
751
+ operation: ParameterizedPackOperation,
752
+ ): Promise<Result<ParameterizedPackOperationResult, PackAuthoringError>> {
753
+ const selected = subjectFor(gameRoot, snapshot, operation);
754
+ if (!selected.ok) return selected;
755
+ const subject = selected.value;
756
+ if (operation.sourceKey !== undefined) {
757
+ if (!isValidPackSourceKey(operation.sourceKey)) {
758
+ return err(
759
+ makeError(
760
+ 'pack-source-key-invalid',
761
+ 'a stable lower-case sourceKey',
762
+ 'repair sourceKey before inspecting the output identity',
763
+ { requestId: operation.requestId, sourceKey: operation.sourceKey },
764
+ operation.sourceKey,
765
+ ),
766
+ );
767
+ }
768
+ if (subject.format === 'direct') {
769
+ const asset = subject.assets.find((candidate) => candidate.sourceKey === operation.sourceKey);
770
+ if (asset === undefined) {
771
+ return err(
772
+ makeError(
773
+ 'pack-output-not-materialized',
774
+ 'sourceKey to exist in the direct Pack',
775
+ 'inspect the current source and choose an existing sourceKey',
776
+ {
777
+ requestId: operation.requestId,
778
+ sourceKey: operation.sourceKey,
779
+ packageId: PackageId.format(subject.packageId),
780
+ },
781
+ ),
782
+ );
783
+ }
784
+ const materialized = snapshot.materialized.find(
785
+ (candidate) =>
786
+ candidate.packageId === packageKey(subject.packageId) &&
787
+ candidate.sourceKey === operation.sourceKey,
788
+ );
789
+ return ok({
790
+ ...sourceResult(subject, operation),
791
+ sourceKey: asset.sourceKey,
792
+ guid: asset.guid,
793
+ kind: asset.kind,
794
+ status:
795
+ materialized === undefined
796
+ ? ('identity' as const)
797
+ : materialized.ready
798
+ ? 'ready'
799
+ : 'present',
800
+ });
801
+ }
802
+ try {
803
+ const guid = AssetGuid.format(AssetGuid.derive(subject.packageId, operation.sourceKey));
804
+ const materialized = [...snapshot.materialized].find(
805
+ (candidate) =>
806
+ candidate.packageId === packageKey(subject.packageId) &&
807
+ candidate.sourceKey === operation.sourceKey,
808
+ );
809
+ return ok({
810
+ ...sourceResult(subject, operation),
811
+ sourceKey: operation.sourceKey,
812
+ guid,
813
+ ...(materialized === undefined
814
+ ? { status: 'identity' as const }
815
+ : {
816
+ status: materialized.ready ? ('ready' as const) : ('present' as const),
817
+ kind: materialized.kind,
818
+ }),
819
+ });
820
+ } catch (cause) {
821
+ return err(
822
+ makeError(
823
+ 'pack-source-key-invalid',
824
+ 'a valid lower-case sourceKey',
825
+ 'repair sourceKey before inspecting the output identity',
826
+ { requestId: operation.requestId, sourceKey: operation.sourceKey, cause: String(cause) },
827
+ operation.sourceKey,
828
+ ),
829
+ );
830
+ }
831
+ }
832
+ if (subject.format === 'instance') {
833
+ const resolved = await resolveInstance(snapshot, subject);
834
+ if (!resolved.ok) return resolved;
835
+ return ok({
836
+ ...sourceResult(subject, operation),
837
+ parameters: resolved.value.parameters.map((parameter) => ({
838
+ name: parameter.name,
839
+ type: parameter.type,
840
+ default: jsonValue(parameter.default),
841
+ ...(parameter.minimum === undefined ? {} : { minimum: parameter.minimum }),
842
+ ...(parameter.maximum === undefined ? {} : { maximum: parameter.maximum }),
843
+ ...(parameter.values === undefined ? {} : { values: parameter.values }),
844
+ ...(parameter.kind === undefined ? {} : { kind: parameter.kind }),
845
+ })),
846
+ effectiveValues: jsonValue(resolved.value.values) as Readonly<Record<string, unknown>>,
847
+ parentChain: resolved.value.parentChain,
848
+ });
849
+ }
850
+ return ok(sourceResult(subject, operation));
851
+ }
852
+
853
+ function listAssets(
854
+ snapshot: GatewaySnapshot,
855
+ operation: ParameterizedPackOperation,
856
+ ): ParameterizedPackOperationResult {
857
+ const assets = snapshotAssets(snapshot)
858
+ .map((asset) => ({
859
+ packageId: asset.packageId,
860
+ sourceKey: asset.sourceKey,
861
+ guid: asset.guid,
862
+ kind: asset.kind,
863
+ ...(asset.name === undefined ? {} : { name: asset.name }),
864
+ ...(asset.sourcePath === undefined ? {} : { sourcePath: asset.sourcePath }),
865
+ status: !('ready' in asset)
866
+ ? ('identity' as const)
867
+ : asset.ready
868
+ ? ('ready' as const)
869
+ : ('present' as const),
870
+ }))
871
+ .sort((left, right) =>
872
+ `${left.packageId}/${left.sourceKey}`.localeCompare(`${right.packageId}/${right.sourceKey}`),
873
+ );
874
+ const sources = [...snapshot.subjects.values()]
875
+ .map((subject) => {
876
+ if (subject.format === 'source') {
877
+ const parameters = serializedParameters(subject.definition);
878
+ return {
879
+ packageId: PackageId.format(subject.packageId),
880
+ sourcePath: subject.relativePath,
881
+ format: 'pack.ts' as const,
882
+ ...(parameters === undefined ? {} : { parameters }),
883
+ };
884
+ }
885
+ if (subject.format === 'instance') {
886
+ return {
887
+ packageId: PackageId.format(subject.packageId),
888
+ sourcePath: subject.relativePath,
889
+ format: 'instance' as const,
890
+ parentPackageId: PackageId.format(subject.parent),
891
+ values: jsonValue(subject.values) as Readonly<Record<string, unknown>>,
892
+ };
893
+ }
894
+ return {
895
+ packageId: PackageId.format(subject.packageId),
896
+ sourcePath: subject.relativePath,
897
+ format: 'direct' as const,
898
+ };
899
+ })
900
+ .sort((left, right) =>
901
+ `${left.packageId}/${left.sourcePath}`.localeCompare(
902
+ `${right.packageId}/${right.sourcePath}`,
903
+ ),
904
+ );
905
+ return {
906
+ operation: operation.operation,
907
+ requestId: operation.requestId,
908
+ assets,
909
+ sources,
910
+ snapshot: { sourceCount: snapshot.subjects.size, assetCount: assets.length },
911
+ };
912
+ }
913
+
914
+ function snapshotAssets(
915
+ snapshot: GatewaySnapshot,
916
+ ): readonly (DirectAuthorAsset | MaterializedAsset)[] {
917
+ const assets = new Map<string, DirectAuthorAsset | MaterializedAsset>();
918
+ for (const asset of snapshot.directAssets) {
919
+ assets.set(`${asset.packageId}/${asset.sourceKey}`, asset);
920
+ }
921
+ for (const asset of snapshot.materialized) {
922
+ assets.set(`${asset.packageId}/${asset.sourceKey}`, asset);
923
+ }
924
+ return [...assets.values()];
925
+ }
926
+
927
+ function assetForPair(
928
+ snapshot: GatewaySnapshot,
929
+ packageId: PackageId,
930
+ sourceKey: string,
931
+ ): DirectAuthorAsset | MaterializedAsset | undefined {
932
+ const key = `${packageKey(packageId)}/${sourceKey}`;
933
+ return snapshotAssets(snapshot).find((asset) => `${asset.packageId}/${asset.sourceKey}` === key);
934
+ }
935
+
936
+ async function resolveAsset(
937
+ gameRoot: string,
938
+ snapshot: GatewaySnapshot,
939
+ operation: ParameterizedPackOperation,
940
+ ): Promise<Result<ParameterizedPackOperationResult, PackAuthoringError>> {
941
+ if (
942
+ operation.require !== undefined &&
943
+ operation.require !== 'identity' &&
944
+ operation.require !== 'present' &&
945
+ operation.require !== 'ready'
946
+ ) {
947
+ return err(
948
+ makeError(
949
+ 'pack-parameter-invalid',
950
+ "require to be 'identity', 'present', or 'ready'",
951
+ 'choose one of the three Pack identity proof levels',
952
+ { requestId: operation.requestId, require: operation.require },
953
+ ),
954
+ );
955
+ }
956
+ let packageId: PackageId | undefined;
957
+ let sourceKey = operation.sourceKey;
958
+ let resolvedOutputGuid: string | undefined;
959
+ if (operation.packageId !== undefined) {
960
+ const parsed = PackageId.parse(operation.packageId);
961
+ if (!parsed.ok) {
962
+ return err(
963
+ makeError(
964
+ 'pack-package-id-invalid',
965
+ 'a UUID packageId',
966
+ 'repair packageId before resolving a sourceKey',
967
+ { requestId: operation.requestId },
968
+ operation.packageId,
969
+ ),
970
+ );
971
+ }
972
+ packageId = parsed.value;
973
+ }
974
+ if (operation.subject !== undefined && isValidAssetGuidString(operation.subject)) {
975
+ const found = [...snapshot.directAssets, ...snapshot.materialized].find(
976
+ (asset) => asset.guid === operation.subject?.toLowerCase(),
977
+ );
978
+ if (found !== undefined) {
979
+ const parsed = PackageId.parse(found.packageId);
980
+ if (parsed.ok) {
981
+ if (packageId !== undefined && packageKey(packageId) !== packageKey(parsed.value)) {
982
+ return err(
983
+ makeError(
984
+ 'pack-parameter-invalid',
985
+ 'packageId and AssetGuid to identify the same Pack subject',
986
+ 'remove the conflicting locator or resolve the pair from one source',
987
+ {
988
+ requestId: operation.requestId,
989
+ packageId: PackageId.format(packageId),
990
+ observedPackageId: found.packageId,
991
+ guid: operation.subject,
992
+ },
993
+ ),
994
+ );
995
+ }
996
+ if (sourceKey !== undefined && sourceKey !== found.sourceKey) {
997
+ return err(
998
+ makeError(
999
+ 'pack-parameter-invalid',
1000
+ 'sourceKey and AssetGuid to identify the same Pack output',
1001
+ 'remove the conflicting locator or use the sourceKey returned by asset.list',
1002
+ {
1003
+ requestId: operation.requestId,
1004
+ sourceKey,
1005
+ observedSourceKey: found.sourceKey,
1006
+ guid: operation.subject,
1007
+ },
1008
+ ),
1009
+ );
1010
+ }
1011
+ packageId = parsed.value;
1012
+ sourceKey ??= found.sourceKey;
1013
+ resolvedOutputGuid = found.guid;
1014
+ }
1015
+ }
1016
+ }
1017
+ if (
1018
+ packageId === undefined &&
1019
+ operation.subject !== undefined &&
1020
+ isValidAssetGuidString(operation.subject)
1021
+ ) {
1022
+ const parsed = PackageId.parse(operation.subject);
1023
+ if (parsed.ok) packageId = parsed.value;
1024
+ }
1025
+ const sourceLocatorCandidate =
1026
+ operation.sourcePath ??
1027
+ (operation.subject !== undefined && !isValidAssetGuidString(operation.subject)
1028
+ ? operation.subject
1029
+ : undefined);
1030
+ let sourceLocator: string | undefined;
1031
+ if (sourceLocatorCandidate !== undefined) {
1032
+ const confined = confinedPath(gameRoot, sourceLocatorCandidate, operation);
1033
+ if (!confined.ok) return confined;
1034
+ sourceLocator = confined.value.relative;
1035
+ }
1036
+ if (sourceLocator !== undefined) {
1037
+ const source = [...snapshot.subjects.values()].find(
1038
+ (candidate) => candidate.relativePath === sourceLocator,
1039
+ );
1040
+ if (source === undefined) {
1041
+ return err(
1042
+ pathError(
1043
+ 'pack-source-not-found',
1044
+ operation,
1045
+ 'sourcePath to identify a current Pack subject',
1046
+ 'inspect the current Source Index and choose an existing source path',
1047
+ { sourcePath: sourceLocator },
1048
+ sourceLocator,
1049
+ ),
1050
+ );
1051
+ }
1052
+ if (packageId !== undefined && packageKey(packageId) !== packageKey(source.packageId)) {
1053
+ return err(
1054
+ makeError(
1055
+ 'pack-parameter-invalid',
1056
+ 'sourcePath and packageId to identify the same Pack subject',
1057
+ 'remove the conflicting locator or use the packageId returned by asset.list',
1058
+ {
1059
+ requestId: operation.requestId,
1060
+ sourcePath: sourceLocator,
1061
+ packageId: PackageId.format(packageId),
1062
+ observedPackageId: PackageId.format(source.packageId),
1063
+ },
1064
+ ),
1065
+ );
1066
+ }
1067
+ packageId ??= source.packageId;
1068
+ }
1069
+ if (packageId === undefined || sourceKey === undefined) {
1070
+ return err(
1071
+ makeError(
1072
+ 'pack-parameter-invalid',
1073
+ 'packageId and sourceKey (or a known output GUID)',
1074
+ 'pass packageId + sourceKey, a known AssetGuid, or a source path plus sourceKey',
1075
+ { requestId: operation.requestId, packageId: operation.packageId, sourceKey },
1076
+ ),
1077
+ );
1078
+ }
1079
+ if (!isValidPackSourceKey(sourceKey)) {
1080
+ return err(
1081
+ makeError(
1082
+ 'pack-source-key-invalid',
1083
+ 'a stable lower-case sourceKey',
1084
+ 'repair sourceKey before deriving its AssetGuid',
1085
+ { requestId: operation.requestId, sourceKey },
1086
+ sourceKey,
1087
+ ),
1088
+ );
1089
+ }
1090
+ let guid: EngineAssetGuid;
1091
+ try {
1092
+ guid = AssetGuid.derive(packageId, sourceKey);
1093
+ } catch (cause) {
1094
+ return err(
1095
+ makeError(
1096
+ 'pack-source-key-invalid',
1097
+ 'a valid packageId and sourceKey pair',
1098
+ 'repair the identity pair before resolving',
1099
+ {
1100
+ requestId: operation.requestId,
1101
+ cause: cause instanceof Error ? cause.message : String(cause),
1102
+ },
1103
+ ),
1104
+ );
1105
+ }
1106
+ const guidString = AssetGuid.format(guid);
1107
+ if (resolvedOutputGuid !== undefined && guidString.toLowerCase() !== resolvedOutputGuid) {
1108
+ return err(
1109
+ makeError(
1110
+ 'pack-guid-collision',
1111
+ 'the known output GUID to equal UUIDv5(packageId, sourceKey)',
1112
+ 'rebuild the current generation from the stable identity pair',
1113
+ {
1114
+ requestId: operation.requestId,
1115
+ packageId: PackageId.format(packageId),
1116
+ sourceKey,
1117
+ expectedGuid: guidString,
1118
+ observedGuid: resolvedOutputGuid,
1119
+ },
1120
+ ),
1121
+ );
1122
+ }
1123
+ const asset = assetForPair(snapshot, packageId, sourceKey);
1124
+ const materialized = snapshot.materialized.find(
1125
+ (candidate) =>
1126
+ candidate.packageId === packageKey(packageId) && candidate.sourceKey === sourceKey,
1127
+ );
1128
+ const requirement: ParameterizedPackResolutionStatus = operation.require ?? 'identity';
1129
+ if (requirement === 'present' && materialized === undefined) {
1130
+ return err(
1131
+ makeError(
1132
+ 'pack-output-not-materialized',
1133
+ 'the current build topology to contain this sourceKey',
1134
+ 'run rebuild/cold-cook and retry after the current generation publishes the output',
1135
+ {
1136
+ requestId: operation.requestId,
1137
+ packageId: PackageId.format(packageId),
1138
+ sourceKey,
1139
+ guid: guidString,
1140
+ },
1141
+ ),
1142
+ );
1143
+ }
1144
+ if (requirement === 'ready' && (materialized === undefined || !materialized.ready)) {
1145
+ return err(
1146
+ makeError(
1147
+ 'asset-not-ready',
1148
+ 'current Pack evidence and artifacts for this derived GUID',
1149
+ 'inspect producer evidence, then rebuild or cold-cook the same subject',
1150
+ {
1151
+ requestId: operation.requestId,
1152
+ packageId: PackageId.format(packageId),
1153
+ sourceKey,
1154
+ guid: guidString,
1155
+ },
1156
+ ),
1157
+ );
1158
+ }
1159
+ return ok({
1160
+ operation: operation.operation,
1161
+ requestId: operation.requestId,
1162
+ packageId: PackageId.format(packageId),
1163
+ sourceKey,
1164
+ guid: guidString,
1165
+ status: materialized === undefined ? 'identity' : materialized.ready ? 'ready' : 'present',
1166
+ ...(asset === undefined
1167
+ ? {}
1168
+ : {
1169
+ ...(asset.sourcePath === undefined ? {} : { sourcePath: asset.sourcePath }),
1170
+ kind: asset.kind,
1171
+ }),
1172
+ });
1173
+ }
1174
+
1175
+ async function verify(
1176
+ snapshot: GatewaySnapshot,
1177
+ operation: ParameterizedPackOperation,
1178
+ ): Promise<Result<ParameterizedPackOperationResult, PackAuthoringError>> {
1179
+ for (const subject of snapshot.subjects.values()) {
1180
+ if (subject.format === 'instance') {
1181
+ const resolved = await resolveInstance(snapshot, subject);
1182
+ if (!resolved.ok) return err(resolved.error);
1183
+ }
1184
+ if (subject.format === 'direct') {
1185
+ for (const asset of subject.assets) {
1186
+ const missing = asset.refs.find((ref) => !snapshot.knownGuids.has(ref.toLowerCase()));
1187
+ if (missing !== undefined) {
1188
+ return err(
1189
+ makeError(
1190
+ 'pack-output-reference-missing',
1191
+ 'every direct Pack ref to resolve to a known AssetGuid',
1192
+ 'repair the payload ref or add the owning external asset before verifying',
1193
+ {
1194
+ requestId: operation.requestId,
1195
+ packageId: PackageId.format(subject.packageId),
1196
+ sourceKey: asset.sourceKey,
1197
+ guid: missing,
1198
+ },
1199
+ ),
1200
+ );
1201
+ }
1202
+ }
1203
+ }
1204
+ }
1205
+ for (const asset of snapshotAssets(snapshot)) {
1206
+ const missing = asset.refs?.find((ref) => !snapshot.knownGuids.has(ref.toLowerCase()));
1207
+ if (missing !== undefined) {
1208
+ return err(
1209
+ makeError(
1210
+ 'pack-output-reference-missing',
1211
+ 'every materialized Pack ref to resolve to a known AssetGuid',
1212
+ 'repair the published output reference or publish its owning asset before verifying',
1213
+ {
1214
+ requestId: operation.requestId,
1215
+ packageId: asset.packageId,
1216
+ sourceKey: asset.sourceKey,
1217
+ guid: missing,
1218
+ },
1219
+ ),
1220
+ );
1221
+ }
1222
+ }
1223
+ return ok({
1224
+ operation: operation.operation,
1225
+ requestId: operation.requestId,
1226
+ snapshot: {
1227
+ sourceCount: snapshot.subjects.size,
1228
+ assetCount: snapshotAssets(snapshot).length,
1229
+ },
1230
+ });
1231
+ }
1232
+
1233
+ function packageIdFromOperation(
1234
+ snapshot: GatewaySnapshot,
1235
+ operation: ParameterizedPackOperation,
1236
+ ): Result<PackageId, PackAuthoringError> {
1237
+ const value =
1238
+ operation.packageId === undefined
1239
+ ? ok(PackageId.random())
1240
+ : PackageId.parse(operation.packageId);
1241
+ if (!value.ok) {
1242
+ return err(
1243
+ makeError(
1244
+ 'pack-package-id-invalid',
1245
+ 'a UUID packageId',
1246
+ 'repair packageId or omit it so the gateway can mint one',
1247
+ { requestId: operation.requestId },
1248
+ operation.packageId,
1249
+ ),
1250
+ );
1251
+ }
1252
+ const normalized = packageKey(value.value);
1253
+ const alreadyMaterialized = snapshot.materialized.some((asset) => asset.packageId === normalized);
1254
+ return snapshot.subjects.has(normalized) || alreadyMaterialized
1255
+ ? err(
1256
+ makeError(
1257
+ 'pack-package-id-collision',
1258
+ 'a packageId unused by the current Source Index',
1259
+ 'choose a new packageId or use clone without an explicit identity',
1260
+ { requestId: operation.requestId, packageId: PackageId.format(value.value) },
1261
+ ),
1262
+ )
1263
+ : ok(value.value);
1264
+ }
1265
+
1266
+ async function readConfined(
1267
+ options: FileSystemParameterizedPackAuthoringOptions,
1268
+ operation: ParameterizedPackOperation,
1269
+ sourcePath: unknown,
1270
+ ): Promise<
1271
+ Result<ConfinedPath & { readonly source: string; readonly revision: string }, PackAuthoringError>
1272
+ > {
1273
+ const path = confinedPath(options.gameRoot, sourcePath, operation);
1274
+ if (!path.ok) return path;
1275
+ try {
1276
+ const source = await readFile(path.value.absolute, 'utf8');
1277
+ return ok({ ...path.value, source, revision: revisionOf(source) });
1278
+ } catch (cause) {
1279
+ const code: 'pack-source-not-found' | 'pack-source-write-failed' =
1280
+ (cause as NodeJS.ErrnoException).code === 'ENOENT'
1281
+ ? 'pack-source-not-found'
1282
+ : 'pack-source-write-failed';
1283
+ return err(
1284
+ pathError(
1285
+ code,
1286
+ operation,
1287
+ 'the requested Pack source to be readable',
1288
+ 'restore the source or choose a current locator',
1289
+ {
1290
+ sourcePath: path.value.relative,
1291
+ cause: cause instanceof Error ? cause.message : String(cause),
1292
+ },
1293
+ path.value.relative,
1294
+ ),
1295
+ );
1296
+ }
1297
+ }
1298
+
1299
+ function revisionConflict(
1300
+ operation: ParameterizedPackOperation,
1301
+ path: string,
1302
+ actual: string,
1303
+ ): PackAuthoringError {
1304
+ return makeError(
1305
+ 'pack-source-revision-conflict',
1306
+ 'expectedRevision to match the current source bytes',
1307
+ 'inspect the current source, reconcile the edit, and retry with a new requestId',
1308
+ {
1309
+ requestId: operation.requestId,
1310
+ sourcePath: path,
1311
+ expectedRevision: operation.expectedRevision,
1312
+ actualRevision: actual,
1313
+ },
1314
+ actual,
1315
+ );
1316
+ }
1317
+
1318
+ function directJson(
1319
+ packageId: PackageId,
1320
+ assets: Readonly<Record<string, DirectPackJsonAsset>>,
1321
+ ): Record<string, unknown> {
1322
+ return { schemaVersion: '3.0.0', packageId: PackageId.format(packageId), assets };
1323
+ }
1324
+
1325
+ function parameterizedScaffold(
1326
+ packageId: PackageId,
1327
+ parameters: readonly PackParameterDefinition[] | undefined,
1328
+ ): string {
1329
+ const parameterSource =
1330
+ parameters === undefined
1331
+ ? ''
1332
+ : '\n parameters: ' +
1333
+ JSON.stringify(
1334
+ parameters.map((parameter) => jsonValue(parameter)),
1335
+ null,
1336
+ 2,
1337
+ ).replaceAll('\n', '\n ') +
1338
+ ',';
1339
+ return (
1340
+ "import { definePack, definePackageId } from '@forgeax/engine-pack/source';\n" +
1341
+ "import { ok } from '@forgeax/engine-types';\n\n" +
1342
+ 'const packageId = definePackageId(' +
1343
+ JSON.stringify(PackageId.format(packageId)) +
1344
+ ');\n\n' +
1345
+ 'export default definePack({\n' +
1346
+ " schemaVersion: '2.0.0',\n" +
1347
+ ' packageId,' +
1348
+ parameterSource +
1349
+ '\n build: () => ok({}),\n' +
1350
+ '});\n'
1351
+ );
1352
+ }
1353
+
1354
+ function replacePackageIdLiteral(source: string, packageId: string): string | undefined {
1355
+ const pattern =
1356
+ /(definePackageId\s*\(\s*)(['"])([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})(\2\s*\))/;
1357
+ if (!pattern.test(source)) return undefined;
1358
+ return source.replace(pattern, `$1${JSON.stringify(packageId)}$4`);
1359
+ }
1360
+
1361
+ async function inspectAfterWrite(
1362
+ operation: ParameterizedPackOperation,
1363
+ path: ConfinedPath,
1364
+ source: string,
1365
+ packageId: PackageId,
1366
+ extra: Partial<ParameterizedPackOperationResult> = {},
1367
+ ): Promise<Result<ParameterizedPackOperationResult, PackAuthoringError>> {
1368
+ return ok({
1369
+ operation: operation.operation,
1370
+ requestId: operation.requestId,
1371
+ sourcePath: path.relative,
1372
+ targetPath: path.relative,
1373
+ packageId: PackageId.format(packageId),
1374
+ revision: revisionOf(source),
1375
+ ...extra,
1376
+ });
1377
+ }
1378
+
1379
+ async function executeRaw(
1380
+ options: FileSystemParameterizedPackAuthoringOptions,
1381
+ operation: ParameterizedPackOperation,
1382
+ ): Promise<Result<ParameterizedPackOperationResult, PackAuthoringError>> {
1383
+ if (operation.operation === 'asset.list' || operation.operation === 'asset.inspect') {
1384
+ const snapshot = await createSnapshot(options, operation);
1385
+ if (!snapshot.ok) return snapshot;
1386
+ return operation.operation === 'asset.list'
1387
+ ? ok(listAssets(snapshot.value, operation))
1388
+ : await inspectAsset(options.gameRoot, snapshot.value, operation);
1389
+ }
1390
+ const snapshot = await createSnapshot(options, operation);
1391
+ if (!snapshot.ok) return snapshot;
1392
+ if (operation.operation === 'asset.resolve')
1393
+ return resolveAsset(options.gameRoot, snapshot.value, operation);
1394
+ if (operation.operation === 'asset.verify') return verify(snapshot.value, operation);
1395
+
1396
+ if (operation.operation === 'asset-source.create') {
1397
+ const path = confinedPath(
1398
+ options.gameRoot,
1399
+ operation.targetPath ?? operation.sourcePath,
1400
+ operation,
1401
+ );
1402
+ if (!path.ok) return path;
1403
+ try {
1404
+ await stat(path.value.absolute);
1405
+ return err(
1406
+ pathError(
1407
+ 'pack-source-revision-conflict',
1408
+ operation,
1409
+ 'an unused target source path',
1410
+ 'inspect the existing source or choose another target path',
1411
+ { targetPath: path.value.relative },
1412
+ path.value.relative,
1413
+ ),
1414
+ );
1415
+ } catch (cause) {
1416
+ if ((cause as NodeJS.ErrnoException).code !== 'ENOENT') {
1417
+ return err(
1418
+ pathError(
1419
+ 'pack-source-write-failed',
1420
+ operation,
1421
+ 'the target path to be available for atomic creation',
1422
+ 'repair the filesystem and retry',
1423
+ { targetPath: path.value.relative, cause: String(cause) },
1424
+ ),
1425
+ );
1426
+ }
1427
+ }
1428
+ const identity = packageIdFromOperation(snapshot.value, operation);
1429
+ if (!identity.ok) return identity;
1430
+ const format =
1431
+ operation.format ?? (path.value.relative.endsWith('.pack.json') ? 'pack.json' : 'pack.ts');
1432
+ if (
1433
+ (format === 'pack.json' && !path.value.relative.endsWith('.pack.json')) ||
1434
+ (format === 'pack.ts' && !path.value.relative.endsWith('.pack.ts'))
1435
+ ) {
1436
+ return err(
1437
+ makeError(
1438
+ 'pack-source-path-invalid',
1439
+ 'format and target extension to agree',
1440
+ 'use a .pack.ts target for source creation or a .pack.json target for direct data',
1441
+ { requestId: operation.requestId, format, targetPath: path.value.relative },
1442
+ ),
1443
+ );
1444
+ }
1445
+ let source: string;
1446
+ let extra: Partial<ParameterizedPackOperationResult> = { format };
1447
+ if (format === 'pack.json') {
1448
+ if (
1449
+ operation.parameters !== undefined ||
1450
+ operation.parent !== undefined ||
1451
+ operation.parentPackageId !== undefined ||
1452
+ operation.values !== undefined
1453
+ ) {
1454
+ return err(
1455
+ makeError(
1456
+ 'pack-parameter-invalid',
1457
+ 'direct create to contain assets only',
1458
+ 'use pack.ts for a parameterized source or create-instance for parent+values',
1459
+ { requestId: operation.requestId },
1460
+ ),
1461
+ );
1462
+ }
1463
+ const assets = operation.initialAssets ?? {};
1464
+ const parsed = parseParameterizedPackJson(directJson(identity.value, assets));
1465
+ if (!parsed.ok) return err(parsed.error);
1466
+ if (parsed.value.format !== 'direct') {
1467
+ return err(
1468
+ makeError(
1469
+ 'pack-parameter-invalid',
1470
+ 'direct create to produce a direct v3 Pack document',
1471
+ 'use create-instance for parent + values instead of direct assets',
1472
+ { requestId: operation.requestId },
1473
+ ),
1474
+ );
1475
+ }
1476
+ source = `${JSON.stringify(directJson(identity.value, parsed.value.assets), null, 2)}\n`;
1477
+ } else {
1478
+ if (
1479
+ operation.initialAssets !== undefined &&
1480
+ Object.keys(operation.initialAssets).length > 0
1481
+ ) {
1482
+ return err(
1483
+ makeError(
1484
+ 'pack-parameter-invalid',
1485
+ 'a pack.ts source to define its outputs in build()',
1486
+ 'use pack.json for direct assets or omit initialAssets when creating a pack.ts source',
1487
+ { requestId: operation.requestId },
1488
+ ),
1489
+ );
1490
+ }
1491
+ if (
1492
+ operation.parent !== undefined ||
1493
+ operation.parentPackageId !== undefined ||
1494
+ operation.values !== undefined
1495
+ ) {
1496
+ return err(
1497
+ makeError(
1498
+ 'pack-parameter-invalid',
1499
+ 'a new pack.ts source without instance-only parent/value fields',
1500
+ 'use asset-source.create-instance for parent + values',
1501
+ { requestId: operation.requestId },
1502
+ ),
1503
+ );
1504
+ }
1505
+ let parameters: readonly PackParameterDefinition[] | undefined;
1506
+ if (operation.parameters !== undefined) {
1507
+ const validated = validatePackDefinition({
1508
+ schemaVersion: '2.0.0',
1509
+ packageId: identity.value,
1510
+ parameters: operation.parameters,
1511
+ build: () => ok({}),
1512
+ });
1513
+ if (!validated.ok) return err(validated.error);
1514
+ if (!('parameters' in validated.value)) {
1515
+ return err(
1516
+ makeError(
1517
+ 'pack-parameter-invalid',
1518
+ 'parameters to be non-empty when supplied',
1519
+ 'omit parameters for a zero-parameter Pack',
1520
+ { requestId: operation.requestId },
1521
+ ),
1522
+ );
1523
+ }
1524
+ parameters = validated.value.parameters;
1525
+ }
1526
+ source = parameterizedScaffold(identity.value, parameters);
1527
+ const serialized =
1528
+ parameters === undefined
1529
+ ? undefined
1530
+ : serializedParameters({
1531
+ schemaVersion: '2.0.0',
1532
+ packageId: identity.value,
1533
+ parameters,
1534
+ build: () => ok({}),
1535
+ } as AnyParameterizedPackDefinition);
1536
+ extra = {
1537
+ format,
1538
+ ...(serialized === undefined ? {} : { parameters: serialized }),
1539
+ };
1540
+ }
1541
+ try {
1542
+ await atomicWrite(path.value.absolute, source);
1543
+ } catch (cause) {
1544
+ return err(
1545
+ pathError(
1546
+ 'pack-source-write-failed',
1547
+ operation,
1548
+ 'the new Pack source to be written atomically',
1549
+ 'repair filesystem permissions or disk capacity, then retry with a new requestId',
1550
+ {
1551
+ targetPath: path.value.relative,
1552
+ cause: cause instanceof Error ? cause.message : String(cause),
1553
+ },
1554
+ ),
1555
+ );
1556
+ }
1557
+ return inspectAfterWrite(operation, path.value, source, identity.value, extra);
1558
+ }
1559
+
1560
+ if (operation.operation === 'asset-source.clone') {
1561
+ const read = await readConfined(options, operation, operation.sourcePath);
1562
+ if (!read.ok) return read;
1563
+ if (
1564
+ operation.expectedRevision !== undefined &&
1565
+ operation.expectedRevision !== read.value.revision
1566
+ ) {
1567
+ return err(revisionConflict(operation, read.value.relative, read.value.revision));
1568
+ }
1569
+ const target = confinedPath(options.gameRoot, operation.targetPath, operation);
1570
+ if (!target.ok) return target;
1571
+ const sourceIsTypeScript = read.value.relative.endsWith('.pack.ts');
1572
+ const targetIsTypeScript = target.value.relative.endsWith('.pack.ts');
1573
+ if (sourceIsTypeScript !== targetIsTypeScript) {
1574
+ return err(
1575
+ makeError(
1576
+ 'pack-source-path-invalid',
1577
+ 'clone source and target to use the same .pack.ts or .pack.json format',
1578
+ 'preserve the source extension when cloning a Pack subject',
1579
+ {
1580
+ requestId: operation.requestId,
1581
+ sourcePath: read.value.relative,
1582
+ targetPath: target.value.relative,
1583
+ },
1584
+ ),
1585
+ );
1586
+ }
1587
+ try {
1588
+ await stat(target.value.absolute);
1589
+ return err(
1590
+ pathError(
1591
+ 'pack-source-revision-conflict',
1592
+ operation,
1593
+ 'an unused clone target path',
1594
+ 'inspect the target or choose another path',
1595
+ { targetPath: target.value.relative },
1596
+ target.value.relative,
1597
+ ),
1598
+ );
1599
+ } catch (cause) {
1600
+ if ((cause as NodeJS.ErrnoException).code !== 'ENOENT') {
1601
+ return err(
1602
+ pathError(
1603
+ 'pack-source-write-failed',
1604
+ operation,
1605
+ 'the clone target to be writable',
1606
+ 'repair filesystem permissions and retry',
1607
+ { targetPath: target.value.relative, cause: String(cause) },
1608
+ ),
1609
+ );
1610
+ }
1611
+ }
1612
+ const identity = packageIdFromOperation(snapshot.value, operation);
1613
+ if (!identity.ok) return identity;
1614
+ let source = read.value.source;
1615
+ if (read.value.relative.endsWith('.pack.json')) {
1616
+ let parsedValue: unknown;
1617
+ try {
1618
+ parsedValue = JSON.parse(source);
1619
+ } catch {
1620
+ return err(
1621
+ makeError(
1622
+ 'pack-parameter-invalid',
1623
+ 'a valid v3 pack.json document',
1624
+ 'repair the source before cloning it',
1625
+ { requestId: operation.requestId, sourcePath: read.value.relative },
1626
+ ),
1627
+ );
1628
+ }
1629
+ const parsed = parseParameterizedPackJson(parsedValue);
1630
+ if (!parsed.ok) return err(parsed.error);
1631
+ source = `${JSON.stringify(
1632
+ parsed.value.format === 'direct'
1633
+ ? directJson(identity.value, parsed.value.assets)
1634
+ : {
1635
+ schemaVersion: '3.0.0',
1636
+ packageId: PackageId.format(identity.value),
1637
+ parent: PackageId.format(parsed.value.parent),
1638
+ values: parsed.value.values,
1639
+ },
1640
+ null,
1641
+ 2,
1642
+ )}\n`;
1643
+ } else {
1644
+ const replaced = replacePackageIdLiteral(source, PackageId.format(identity.value));
1645
+ if (replaced === undefined) {
1646
+ return err(
1647
+ makeError(
1648
+ 'pack-source-mutation-unsupported',
1649
+ 'a constrained definePackageId(UUID) literal in the source',
1650
+ 'clone a generated Pack source or create a new Pack; arbitrary TypeScript AST rewriting is unsupported',
1651
+ { requestId: operation.requestId, sourcePath: read.value.relative },
1652
+ ),
1653
+ );
1654
+ }
1655
+ source = replaced;
1656
+ }
1657
+ try {
1658
+ await atomicWrite(target.value.absolute, source);
1659
+ } catch (cause) {
1660
+ return err(
1661
+ pathError(
1662
+ 'pack-source-write-failed',
1663
+ operation,
1664
+ 'the cloned Pack source to be written atomically',
1665
+ 'repair filesystem permissions or disk capacity, then retry',
1666
+ {
1667
+ targetPath: target.value.relative,
1668
+ cause: cause instanceof Error ? cause.message : String(cause),
1669
+ },
1670
+ ),
1671
+ );
1672
+ }
1673
+ return inspectAfterWrite(operation, target.value, source, identity.value, {
1674
+ format: target.value.relative.endsWith('.pack.json') ? 'pack.json' : 'pack.ts',
1675
+ });
1676
+ }
1677
+
1678
+ if (operation.operation === 'asset-source.create-instance') {
1679
+ const target = confinedPath(
1680
+ options.gameRoot,
1681
+ operation.targetPath ?? operation.sourcePath,
1682
+ operation,
1683
+ );
1684
+ if (!target.ok) return target;
1685
+ const parentText = operation.parentPackageId ?? operation.parent;
1686
+ if (parentText === undefined) {
1687
+ return err(
1688
+ makeError(
1689
+ 'pack-parent-not-found',
1690
+ 'a parent packageId for the new instance',
1691
+ 'pass parentPackageId or parent and point it at a parameterized source',
1692
+ { requestId: operation.requestId },
1693
+ ),
1694
+ );
1695
+ }
1696
+ const parentId = PackageId.parse(parentText);
1697
+ if (!parentId.ok) {
1698
+ return err(
1699
+ makeError(
1700
+ 'pack-package-id-invalid',
1701
+ 'a UUID parent packageId',
1702
+ 'repair the parent package identity',
1703
+ { requestId: operation.requestId },
1704
+ parentText,
1705
+ ),
1706
+ );
1707
+ }
1708
+ const parent = snapshot.value.subjects.get(packageKey(parentId.value));
1709
+ if (parent === undefined) {
1710
+ return err(
1711
+ makeError(
1712
+ 'pack-parent-not-found',
1713
+ 'the parent packageId to exist in the current Source Index',
1714
+ 'inspect the Source Index and choose a parameterized parent',
1715
+ { requestId: operation.requestId, parent: parentText },
1716
+ ),
1717
+ );
1718
+ }
1719
+ if (parent.format === 'source' && !('parameters' in parent.definition)) {
1720
+ return err(
1721
+ makeError(
1722
+ 'pack-parent-not-parameterized',
1723
+ 'a non-empty parameterized Pack source as the parent',
1724
+ 'use asset-source.clone for a zero-parameter Pack',
1725
+ { requestId: operation.requestId, parent: parentText },
1726
+ ),
1727
+ );
1728
+ }
1729
+ if (parent.format === 'direct') {
1730
+ return err(
1731
+ makeError(
1732
+ 'pack-parent-not-parameterized',
1733
+ 'a parameterized Pack source or instance as the parent',
1734
+ 'direct Packs cannot be instance parents',
1735
+ { requestId: operation.requestId, parent: parentText },
1736
+ ),
1737
+ );
1738
+ }
1739
+ try {
1740
+ await stat(target.value.absolute);
1741
+ return err(
1742
+ pathError(
1743
+ 'pack-source-revision-conflict',
1744
+ operation,
1745
+ 'an unused instance target path',
1746
+ 'inspect the existing instance or choose another path',
1747
+ { targetPath: target.value.relative },
1748
+ target.value.relative,
1749
+ ),
1750
+ );
1751
+ } catch (cause) {
1752
+ if ((cause as NodeJS.ErrnoException).code !== 'ENOENT') {
1753
+ return err(
1754
+ pathError(
1755
+ 'pack-source-write-failed',
1756
+ operation,
1757
+ 'the instance target to be writable',
1758
+ 'repair filesystem permissions and retry',
1759
+ { targetPath: target.value.relative, cause: String(cause) },
1760
+ ),
1761
+ );
1762
+ }
1763
+ }
1764
+ const identity = packageIdFromOperation(snapshot.value, operation);
1765
+ if (!identity.ok) return identity;
1766
+ const values = operation.values ?? {};
1767
+ const serializedValues = jsonValue(values) as Readonly<Record<string, unknown>>;
1768
+ const candidate: InstanceSubject = {
1769
+ format: 'instance',
1770
+ packageId: identity.value,
1771
+ parent: parentId.value,
1772
+ values,
1773
+ sourcePath: target.value.absolute,
1774
+ relativePath: target.value.relative,
1775
+ };
1776
+ const withCandidate: GatewaySnapshot = {
1777
+ ...snapshot.value,
1778
+ subjects: new Map([...snapshot.value.subjects, [packageKey(identity.value), candidate]]),
1779
+ };
1780
+ const resolved = await resolveInstance(withCandidate, candidate);
1781
+ if (!resolved.ok) return err(resolved.error);
1782
+ const source = `${JSON.stringify(
1783
+ {
1784
+ schemaVersion: '3.0.0',
1785
+ packageId: PackageId.format(identity.value),
1786
+ parent: PackageId.format(parentId.value),
1787
+ values: serializedValues,
1788
+ },
1789
+ null,
1790
+ 2,
1791
+ )}\n`;
1792
+ try {
1793
+ await atomicWrite(target.value.absolute, source);
1794
+ } catch (cause) {
1795
+ return err(
1796
+ pathError(
1797
+ 'pack-source-write-failed',
1798
+ operation,
1799
+ 'the new instance to be written atomically',
1800
+ 'repair filesystem permissions or disk capacity, then retry',
1801
+ {
1802
+ targetPath: target.value.relative,
1803
+ cause: cause instanceof Error ? cause.message : String(cause),
1804
+ },
1805
+ ),
1806
+ );
1807
+ }
1808
+ return inspectAfterWrite(operation, target.value, source, identity.value, {
1809
+ format: 'instance',
1810
+ parentPackageId: PackageId.format(parentId.value),
1811
+ values: serializedValues,
1812
+ effectiveValues: jsonValue(resolved.value.values) as Readonly<Record<string, unknown>>,
1813
+ parentChain: resolved.value.parentChain,
1814
+ });
1815
+ }
1816
+
1817
+ if (operation.operation === 'asset-source.apply-values') {
1818
+ const read = await readConfined(options, operation, operation.sourcePath);
1819
+ if (!read.ok) return read;
1820
+ if (
1821
+ operation.expectedRevision === undefined ||
1822
+ operation.expectedRevision !== read.value.revision
1823
+ ) {
1824
+ return err(revisionConflict(operation, read.value.relative, read.value.revision));
1825
+ }
1826
+ let json: unknown;
1827
+ try {
1828
+ json = JSON.parse(read.value.source);
1829
+ } catch {
1830
+ return err(
1831
+ makeError(
1832
+ 'pack-parameter-invalid',
1833
+ 'a valid v3 instance JSON document',
1834
+ 'repair the source before applying values',
1835
+ { requestId: operation.requestId, sourcePath: read.value.relative },
1836
+ ),
1837
+ );
1838
+ }
1839
+ const parsed = parseParameterizedPackJson(json);
1840
+ if (!parsed.ok) return err(parsed.error);
1841
+ if (parsed.value.format !== 'instance') {
1842
+ return err(
1843
+ makeError(
1844
+ 'pack-parameter-invalid',
1845
+ 'an instance pack.json subject',
1846
+ 'apply-values only edits parent+values instances',
1847
+ { requestId: operation.requestId, sourcePath: read.value.relative },
1848
+ ),
1849
+ );
1850
+ }
1851
+ const values = operation.values ?? {};
1852
+ const serializedValues = jsonValue(values) as Readonly<Record<string, unknown>>;
1853
+ const candidate: InstanceSubject = {
1854
+ format: 'instance',
1855
+ packageId: parsed.value.packageId,
1856
+ parent: parsed.value.parent,
1857
+ values,
1858
+ sourcePath: read.value.absolute,
1859
+ relativePath: read.value.relative,
1860
+ };
1861
+ const withCandidate: GatewaySnapshot = {
1862
+ ...snapshot.value,
1863
+ subjects: new Map([...snapshot.value.subjects, [packageKey(candidate.packageId), candidate]]),
1864
+ };
1865
+ const resolved = await resolveInstance(withCandidate, candidate);
1866
+ if (!resolved.ok) return err(resolved.error);
1867
+ const source = `${JSON.stringify(
1868
+ {
1869
+ schemaVersion: '3.0.0',
1870
+ packageId: PackageId.format(parsed.value.packageId),
1871
+ parent: PackageId.format(parsed.value.parent),
1872
+ values: serializedValues,
1873
+ },
1874
+ null,
1875
+ 2,
1876
+ )}\n`;
1877
+ try {
1878
+ await atomicWrite(read.value.absolute, source);
1879
+ } catch (cause) {
1880
+ return err(
1881
+ pathError(
1882
+ 'pack-source-write-failed',
1883
+ operation,
1884
+ 'the updated instance to be written atomically',
1885
+ 'repair filesystem permissions or disk capacity, then retry with a new requestId',
1886
+ {
1887
+ sourcePath: read.value.relative,
1888
+ cause: cause instanceof Error ? cause.message : String(cause),
1889
+ },
1890
+ ),
1891
+ );
1892
+ }
1893
+ return inspectAfterWrite(operation, read.value, source, parsed.value.packageId, {
1894
+ format: 'instance',
1895
+ parentPackageId: PackageId.format(parsed.value.parent),
1896
+ values: serializedValues,
1897
+ effectiveValues: jsonValue(resolved.value.values) as Readonly<Record<string, unknown>>,
1898
+ parentChain: resolved.value.parentChain,
1899
+ });
1900
+ }
1901
+
1902
+ if (
1903
+ operation.operation === 'asset-source.rebuild' ||
1904
+ operation.operation === 'asset-source.cold-cook'
1905
+ ) {
1906
+ const read = await readConfined(options, operation, operation.sourcePath);
1907
+ if (!read.ok) return read;
1908
+ if (
1909
+ operation.expectedRevision === undefined ||
1910
+ operation.expectedRevision !== read.value.revision
1911
+ ) {
1912
+ return err(revisionConflict(operation, read.value.relative, read.value.revision));
1913
+ }
1914
+ const rebuilt = await options.rebuild?.(
1915
+ read.value.relative,
1916
+ operation.operation === 'asset-source.rebuild' ? 'rebuild' : 'cold-cook',
1917
+ );
1918
+ if (rebuilt === undefined) {
1919
+ return err(
1920
+ makeError(
1921
+ 'pack-source-mutation-unsupported',
1922
+ 'a build-owner callback for rebuild or cold-cook',
1923
+ 'connect the gateway to the project build owner before requesting a mutation',
1924
+ { requestId: operation.requestId, sourcePath: read.value.relative },
1925
+ ),
1926
+ );
1927
+ }
1928
+ if (!rebuilt.ok) return err(rebuilt.error);
1929
+ const refreshed = await readConfined(options, operation, operation.sourcePath);
1930
+ if (!refreshed.ok) return refreshed;
1931
+ const refreshedSnapshot = await createSnapshot(options, operation);
1932
+ if (!refreshedSnapshot.ok) return refreshedSnapshot;
1933
+ const selected = subjectFor(options.gameRoot, refreshedSnapshot.value, operation);
1934
+ if (!selected.ok) return selected;
1935
+ return inspectAfterWrite(
1936
+ operation,
1937
+ refreshed.value,
1938
+ refreshed.value.source,
1939
+ selected.value.packageId,
1940
+ {
1941
+ format: refreshed.value.relative.endsWith('.pack.json') ? 'pack.json' : 'pack.ts',
1942
+ },
1943
+ );
1944
+ }
1945
+
1946
+ return err(
1947
+ makeError(
1948
+ 'pack-parameter-invalid',
1949
+ 'a supported parameterized Pack operation',
1950
+ 'use one of PARAMETERIZED_PACK_OPERATION_IDS',
1951
+ { requestId: operation.requestId, operation: operation.operation },
1952
+ ),
1953
+ );
1954
+ }
1955
+
1956
+ export function createFileSystemParameterizedPackAuthoringPort(
1957
+ options: FileSystemParameterizedPackAuthoringOptions,
1958
+ ): ParameterizedPackGatewayPort<ParameterizedPackOperationResult> {
1959
+ return { execute: (operation) => executeRaw(options, operation) };
1960
+ }
1961
+
1962
+ export function createFileSystemParameterizedPackAuthoringGateway(
1963
+ options: FileSystemParameterizedPackAuthoringOptions,
1964
+ ): ParameterizedPackGatewayPort<ParameterizedPackOperationResult> {
1965
+ return createParameterizedPackAuthoringGateway(
1966
+ createFileSystemParameterizedPackAuthoringPort(options),
1967
+ );
1968
+ }