@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
@@ -1,8 +1,8 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { existsSync } from 'node:fs';
3
- import { mkdir, mkdtemp, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { mkdtemp, readFile, realpath, rm, stat } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
- import { dirname, extname, isAbsolute, relative, resolve, sep } from 'node:path';
5
+ import { dirname, extname, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { Worker } from 'node:worker_threads';
8
8
  import type { AssetGuid, Result } from '@forgeax/engine-types';
@@ -12,19 +12,14 @@ import {
12
12
  type ProducerSemanticIdentityInput,
13
13
  producerRelativeDdcKey,
14
14
  } from './evidence/source-inventory.js';
15
- import { AssetGuid as AssetGuidCodec } from './guid.js';
16
15
  import {
17
- type AssetReader,
18
- projectScriptablePackMeta,
19
- type ScriptablePackAssetKind,
20
- type ScriptablePackAuthoringMutation,
21
- type ScriptablePackAuthoringPort,
22
- type ScriptablePackDefinition,
23
- type ScriptablePackError,
24
- type ScriptablePackReadError,
25
- type ScriptablePackSourceClosureEntry,
26
- validateScriptablePackDefinition,
27
- } from './scriptable-pack.js';
16
+ type AnyParameterizedPackDefinition,
17
+ type PackAuthoringError,
18
+ type PackBuildContextWithoutParameters,
19
+ type PackBuildResult,
20
+ validatePackDefinition,
21
+ } from './parameterized-pack.js';
22
+ import type { AssetReader, ScriptablePackSourceClosureEntry } from './scriptable-pack.js';
28
23
 
29
24
  const IMPORT_META_RESOLVE_FLAG = '--experimental-import-meta-resolve';
30
25
 
@@ -44,6 +39,15 @@ export interface ScriptablePackModuleExecutor {
44
39
  dispose?(reason: 'complete' | 'timeout' | 'failure'): void | Promise<void>;
45
40
  }
46
41
 
42
+ interface SerializedParameterizedBuildContext {
43
+ readonly packageId: readonly number[];
44
+ readonly values?: Readonly<Record<string, unknown>>;
45
+ }
46
+
47
+ interface WorkerBackedScriptablePackExecutor {
48
+ readonly supportsParameterizedContext: true;
49
+ }
50
+
47
51
  export interface ScriptablePackModuleExecutorPool {
48
52
  acquire(): Promise<ScriptablePackModuleExecutor>;
49
53
  dispose(): Promise<void>;
@@ -77,16 +81,6 @@ export async function readOptionalBuildCache<T>(cache: OptionalBuildCache<T>): P
77
81
  return { value: await cache.coldCook(), fromCache: false };
78
82
  }
79
83
 
80
- export interface LoadScriptablePackOptions {
81
- /** Maximum time allowed for module initialization in the isolated worker. */
82
- readonly timeoutMs?: number;
83
- /** Maximum time allowed for one definition.build(reader) in the isolated worker. */
84
- readonly buildTimeoutMs?: number;
85
- readonly executor?: ScriptablePackModuleExecutor;
86
- /** Release the isolated loader after identity projection when build will not be called. */
87
- readonly metadataOnly?: boolean;
88
- }
89
-
90
84
  async function resolveRelativeImport(
91
85
  sourcePath: string,
92
86
  specifier: string,
@@ -215,7 +209,10 @@ function hydrateFailure(value: unknown): unknown {
215
209
  return value;
216
210
  }
217
211
 
218
- class WorkerScriptablePackExecutor implements ScriptablePackModuleExecutor {
212
+ class WorkerScriptablePackExecutor
213
+ implements ScriptablePackModuleExecutor, WorkerBackedScriptablePackExecutor
214
+ {
215
+ readonly supportsParameterizedContext = true as const;
219
216
  private worker: Worker | undefined;
220
217
  private compileRoot: string | undefined;
221
218
  private nextBuildId = 0;
@@ -257,7 +254,10 @@ class WorkerScriptablePackExecutor implements ScriptablePackModuleExecutor {
257
254
  ? undefined
258
255
  : {
259
256
  ...(value.definition as Record<string, unknown>),
260
- build: (reader: AssetReader) => this.runBuild(reader),
257
+ build: (
258
+ readByGuid: AssetReader['readByGuid'],
259
+ context?: SerializedParameterizedBuildContext,
260
+ ) => this.runBuild({ readByGuid }, context),
261
261
  },
262
262
  });
263
263
  } else if (value.kind === 'load-threw') {
@@ -291,7 +291,10 @@ class WorkerScriptablePackExecutor implements ScriptablePackModuleExecutor {
291
291
  return this.disposal;
292
292
  }
293
293
 
294
- private runBuild(reader: AssetReader): Promise<unknown> {
294
+ private runBuild(
295
+ reader: AssetReader,
296
+ context?: SerializedParameterizedBuildContext,
297
+ ): Promise<unknown> {
295
298
  const worker = this.worker;
296
299
  if (worker === undefined) return Promise.reject(new Error('ScriptablePack worker is closed'));
297
300
  if (this.build !== undefined)
@@ -299,7 +302,7 @@ class WorkerScriptablePackExecutor implements ScriptablePackModuleExecutor {
299
302
  const id = this.nextBuildId++;
300
303
  return new Promise((resolveBuild, rejectBuild) => {
301
304
  this.build = { id, reader, resolve: resolveBuild, reject: rejectBuild };
302
- worker.postMessage({ kind: 'build', buildId: id });
305
+ worker.postMessage({ kind: 'build', buildId: id, context });
303
306
  });
304
307
  }
305
308
 
@@ -341,7 +344,10 @@ function scriptablePackWorkerUrl(): URL {
341
344
  : new URL('../dist/scriptable-pack-worker.mjs', import.meta.url);
342
345
  }
343
346
 
344
- class ReusableWorkerScriptablePackExecutor implements ScriptablePackModuleExecutor {
347
+ class ReusableWorkerScriptablePackExecutor
348
+ implements ScriptablePackModuleExecutor, WorkerBackedScriptablePackExecutor
349
+ {
350
+ readonly supportsParameterizedContext = true as const;
345
351
  private readonly worker: Worker;
346
352
  private readonly compileRootReady = mkdtemp(resolve(tmpdir(), 'forgeax-scriptable-pack-pool-'));
347
353
  private compileRoot: string | undefined;
@@ -476,7 +482,10 @@ class ReusableWorkerScriptablePackExecutor implements ScriptablePackModuleExecut
476
482
  building?.reject(error);
477
483
  }
478
484
 
479
- private runBuild(reader: AssetReader): Promise<unknown> {
485
+ private runBuild(
486
+ reader: AssetReader,
487
+ context?: SerializedParameterizedBuildContext,
488
+ ): Promise<unknown> {
480
489
  if (this.released || this.disposal !== undefined) {
481
490
  return Promise.reject(new Error('ScriptablePack executor is not leased'));
482
491
  }
@@ -487,7 +496,7 @@ class ReusableWorkerScriptablePackExecutor implements ScriptablePackModuleExecut
487
496
  return new Promise((resolveBuild, rejectBuild) => {
488
497
  this.build = { id, reader, resolve: resolveBuild, reject: rejectBuild };
489
498
  try {
490
- this.worker.postMessage({ kind: 'build', buildId: id });
499
+ this.worker.postMessage({ kind: 'build', buildId: id, context });
491
500
  } catch (error) {
492
501
  this.build = undefined;
493
502
  rejectBuild(error);
@@ -508,7 +517,10 @@ class ReusableWorkerScriptablePackExecutor implements ScriptablePackModuleExecut
508
517
  ? undefined
509
518
  : {
510
519
  ...(value.definition as Record<string, unknown>),
511
- build: (reader: AssetReader) => this.runBuild(reader),
520
+ build: (
521
+ readByGuid: AssetReader['readByGuid'],
522
+ context?: SerializedParameterizedBuildContext,
523
+ ) => this.runBuild({ readByGuid }, context),
512
524
  },
513
525
  });
514
526
  return;
@@ -630,17 +642,24 @@ export function createScriptablePackModuleExecutorPool(
630
642
  return new DefaultScriptablePackModuleExecutorPool(options);
631
643
  }
632
644
 
633
- function loadFailure(
645
+ export interface LoadParameterizedScriptablePackOptions {
646
+ readonly timeoutMs?: number;
647
+ readonly buildTimeoutMs?: number;
648
+ readonly executor?: ScriptablePackModuleExecutor;
649
+ readonly metadataOnly?: boolean;
650
+ }
651
+
652
+ function parameterizedLoadFailure(
634
653
  sourcePath: string,
635
654
  reason: 'module-load' | 'timeout',
636
655
  phase: 'module-load' | 'build',
637
656
  diagnostic: string,
638
657
  timeoutMs?: number,
639
- ): Result<never, ScriptablePackError> {
640
- return err({
641
- code: 'pack-source-load-failed',
642
- expected: 'a trusted synchronous module with a valid default ScriptablePack export',
643
- hint: 'repair the module load or initialization failure, then inspect Meta again',
658
+ ): PackAuthoringError {
659
+ return {
660
+ code: 'pack-parameter-invalid',
661
+ expected: 'a trusted Pack v2 authoring module with a valid default export',
662
+ hint: 'repair the source module or its build timeout, then inspect the Pack again',
644
663
  detail: {
645
664
  sourcePath,
646
665
  reason,
@@ -648,38 +667,21 @@ function loadFailure(
648
667
  diagnostic,
649
668
  ...(timeoutMs === undefined ? {} : { timeoutMs }),
650
669
  },
651
- });
652
- }
653
-
654
- function buildTimeoutFailure(sourcePath: string, timeoutMs: number): ScriptablePackReadError {
655
- return {
656
- code: 'pack-source-load-failed',
657
- expected: 'a ScriptablePack build to settle within the configured build timeout',
658
- hint: 'repair the authored build, then retry through a fresh ScriptablePack generation',
659
- detail: {
660
- sourcePath,
661
- reason: 'timeout',
662
- phase: 'build',
663
- timeoutMs,
664
- diagnostic: `ScriptablePack build exceeded ${timeoutMs}ms`,
665
- },
666
670
  };
667
671
  }
668
672
 
669
- export async function loadScriptablePack(
673
+ /** Load the parameterized Pack source contract in an isolated module executor. */
674
+ export async function loadParameterizedScriptablePack(
670
675
  sourcePath: string,
671
- options: LoadScriptablePackOptions = {},
672
- ): Promise<Result<Readonly<ScriptablePackDefinition>, ScriptablePackError>> {
673
- // Module initialization includes transpiling the trusted TypeScript source
674
- // closure in a fresh Worker. A cold SDK workspace can legitimately exceed
675
- // the build execution budget before any authored build() code has run.
676
+ options: LoadParameterizedScriptablePackOptions = {},
677
+ ): Promise<Result<Readonly<AnyParameterizedPackDefinition>, PackAuthoringError>> {
676
678
  const timeoutMs = options.timeoutMs ?? 15_000;
677
679
  const buildTimeoutMs = options.buildTimeoutMs ?? 5_000;
678
680
  const executor = options.executor ?? new WorkerScriptablePackExecutor();
679
681
  let disposeReason: 'complete' | 'timeout' | 'failure' | undefined;
680
682
  let timeout: ReturnType<typeof setTimeout> | undefined;
681
683
  try {
682
- const timeoutSignal = Symbol('scriptable-pack-module-timeout');
684
+ const timeoutSignal = Symbol('parameterized-pack-module-timeout');
683
685
  const loaded = await Promise.race([
684
686
  executor.load(sourcePath),
685
687
  new Promise<typeof timeoutSignal>((resolve) => {
@@ -688,42 +690,68 @@ export async function loadScriptablePack(
688
690
  ]);
689
691
  if (loaded === timeoutSignal) {
690
692
  disposeReason = 'timeout';
691
- return loadFailure(
692
- sourcePath,
693
- 'timeout',
694
- 'module-load',
695
- `ScriptablePack module initialization exceeded ${timeoutMs}ms`,
696
- timeoutMs,
693
+ return err(
694
+ parameterizedLoadFailure(
695
+ sourcePath,
696
+ 'timeout',
697
+ 'module-load',
698
+ `Pack module initialization exceeded ${timeoutMs}ms`,
699
+ timeoutMs,
700
+ ),
697
701
  );
698
702
  }
699
703
  const moduleValue =
700
704
  loaded !== null && typeof loaded === 'object' && 'default' in loaded
701
705
  ? (loaded as { readonly default: unknown }).default
702
706
  : undefined;
703
- const validated = validateScriptablePackDefinition(moduleValue, sourcePath);
707
+ const validated = validatePackDefinition(moduleValue, sourcePath);
704
708
  if (!validated.ok) {
705
709
  disposeReason = 'failure';
706
710
  return validated;
707
711
  }
708
712
  if (options.metadataOnly === true) {
709
713
  disposeReason = 'complete';
710
- return validated;
714
+ return ok(validated.value);
711
715
  }
712
716
  const definition = validated.value;
713
- type BuildOutcome = Awaited<ReturnType<ScriptablePackDefinition['build']>>;
717
+ const build = definition.build as unknown as (
718
+ first: unknown,
719
+ context?: SerializedParameterizedBuildContext,
720
+ ) => PackBuildResult;
714
721
  return ok({
715
722
  ...definition,
716
- build: async (reader: AssetReader): Promise<BuildOutcome> => {
723
+ build: async (
724
+ context: PackBuildContextWithoutParameters,
725
+ ): Promise<Awaited<PackBuildResult>> => {
717
726
  let buildTimedOut = false;
718
727
  let buildTimeout: ReturnType<typeof setTimeout> | undefined;
719
- const timeoutResult = err(buildTimeoutFailure(sourcePath, buildTimeoutMs));
728
+ const timeoutResult = err(
729
+ parameterizedLoadFailure(
730
+ sourcePath,
731
+ 'timeout',
732
+ 'build',
733
+ `ScriptablePack build exceeded ${buildTimeoutMs}ms`,
734
+ buildTimeoutMs,
735
+ ),
736
+ );
720
737
  try {
738
+ const invocation =
739
+ 'supportsParameterizedContext' in executor &&
740
+ (executor as Partial<WorkerBackedScriptablePackExecutor>)
741
+ .supportsParameterizedContext === true
742
+ ? build(context.readByGuid, {
743
+ packageId: [...context.packageId],
744
+ ...('values' in context && context.values !== undefined
745
+ ? { values: context.values as Readonly<Record<string, unknown>> }
746
+ : {}),
747
+ })
748
+ : definition.build(context as never);
721
749
  const built = await Promise.race([
722
- Promise.resolve(definition.build(reader)),
723
- new Promise<BuildOutcome>((resolve) => {
750
+ Promise.resolve(invocation),
751
+ new Promise<Awaited<PackBuildResult>>((resolve) => {
724
752
  buildTimeout = setTimeout(() => {
725
753
  buildTimedOut = true;
726
- resolve(timeoutResult as BuildOutcome);
754
+ resolve(timeoutResult as Awaited<PackBuildResult>);
727
755
  }, buildTimeoutMs);
728
756
  }),
729
757
  ]);
@@ -740,658 +768,19 @@ export async function loadScriptablePack(
740
768
  if (buildTimeout !== undefined) clearTimeout(buildTimeout);
741
769
  }
742
770
  },
743
- });
771
+ } as AnyParameterizedPackDefinition);
744
772
  } catch (error) {
745
- const diagnostic = error instanceof Error ? error.message : String(error);
746
773
  disposeReason = 'failure';
747
- return loadFailure(sourcePath, 'module-load', 'module-load', diagnostic);
748
- } finally {
749
- if (timeout !== undefined) clearTimeout(timeout);
750
- if (disposeReason !== undefined) await executor.dispose?.(disposeReason);
751
- }
752
- }
753
-
754
- const CANONICAL_SCAFFOLD_MARKER = '// @forgeax-scriptable-pack canonical-v1';
755
- const CANONICAL_MANIFEST_NAME = 'canonicalManifest';
756
- const CANONICAL_MUTABLE_KINDS = new Set<ScriptablePackAssetKind>(['scene', 'mesh', 'material']);
757
-
758
- interface CanonicalOutput {
759
- readonly guid: string;
760
- readonly kind: ScriptablePackAssetKind;
761
- readonly name?: string;
762
- }
763
-
764
- interface CanonicalManifest {
765
- readonly schemaVersion: '1.0.0';
766
- readonly packageId: string;
767
- readonly name?: string;
768
- readonly assets: Readonly<Record<string, CanonicalOutput>>;
769
- readonly externalAssets: Readonly<Record<string, string>>;
770
- }
771
-
772
- export interface FileSystemScriptablePackAuthoringOptions {
773
- /** All source and target paths are confined to this game root. */
774
- readonly gameRoot: string;
775
- readonly incomingRefs?: (sourcePath: string, sourceKey?: string) => Promise<readonly string[]>;
776
- readonly rebuild?: (
777
- sourcePath: string,
778
- mode: 'rebuild' | 'cold-cook',
779
- ) => Promise<Result<void, ScriptablePackError>>;
780
- }
781
-
782
- function authoringError(
783
- code:
784
- | 'pack-source-path-invalid'
785
- | 'pack-source-revision-conflict'
786
- | 'pack-source-mutation-unsupported'
787
- | 'pack-source-reference-conflict'
788
- | 'pack-source-write-failed',
789
- input: {
790
- readonly expected: string;
791
- readonly actual?: string;
792
- readonly hint: string;
793
- readonly retryable: boolean;
794
- readonly recoveryActions: readonly string[];
795
- readonly requestId?: string;
796
- readonly sourcePath?: string;
797
- readonly sourceKey?: string;
798
- readonly incomingRefs?: readonly string[];
799
- },
800
- ): ScriptablePackError {
801
- return {
802
- code,
803
- expected: input.expected,
804
- ...(input.actual === undefined ? {} : { actual: input.actual }),
805
- hint: input.hint,
806
- retryable: input.retryable,
807
- recoveryActions: input.recoveryActions,
808
- detail: {
809
- ...(input.requestId === undefined ? {} : { requestId: input.requestId }),
810
- ...(input.sourcePath === undefined ? {} : { sourcePath: input.sourcePath }),
811
- ...(input.sourceKey === undefined ? {} : { sourceKey: input.sourceKey }),
812
- ...(input.incomingRefs === undefined ? {} : { incomingRefs: input.incomingRefs }),
813
- },
814
- };
815
- }
816
-
817
- function confinedSourcePath(
818
- gameRoot: string,
819
- sourcePath: string,
820
- ): Result<{ readonly absolute: string; readonly relative: string }, ScriptablePackError> {
821
- const root = resolve(gameRoot);
822
- const absolute = resolve(root, sourcePath);
823
- const rel = relative(root, absolute);
824
- if (
825
- rel.length === 0 ||
826
- rel === '..' ||
827
- rel.startsWith(`..${sep}`) ||
828
- isAbsolute(sourcePath) ||
829
- !rel.endsWith('.pack.ts')
830
- ) {
831
774
  return err(
832
- authoringError('pack-source-path-invalid', {
833
- expected: 'a game-root-relative *.pack.ts path confined to the selected game',
834
- actual: sourcePath,
835
- hint: 'choose a relative ScriptablePack source path inside the selected game root',
836
- retryable: false,
837
- recoveryActions: ['choose-game-relative-source-path'],
775
+ parameterizedLoadFailure(
838
776
  sourcePath,
839
- }),
777
+ 'module-load',
778
+ 'module-load',
779
+ error instanceof Error ? error.message : String(error),
780
+ ),
840
781
  );
782
+ } finally {
783
+ if (timeout !== undefined) clearTimeout(timeout);
784
+ if (disposeReason !== undefined) await executor.dispose?.(disposeReason);
841
785
  }
842
- return ok({ absolute, relative: rel.split(sep).join('/') });
843
- }
844
-
845
- function revisionOf(source: string): string {
846
- return createHash('sha256').update(source).digest('hex');
847
- }
848
-
849
- function unwrapExpression(expression: ts.Expression): ts.Expression {
850
- if (
851
- ts.isAsExpression(expression) ||
852
- ts.isSatisfiesExpression(expression) ||
853
- ts.isParenthesizedExpression(expression)
854
- ) {
855
- return unwrapExpression(expression.expression);
856
- }
857
- return expression;
858
- }
859
-
860
- function literalValue(expression: ts.Expression): unknown {
861
- const value = unwrapExpression(expression);
862
- if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)) return value.text;
863
- if (value.kind === ts.SyntaxKind.TrueKeyword) return true;
864
- if (value.kind === ts.SyntaxKind.FalseKeyword) return false;
865
- if (value.kind === ts.SyntaxKind.NullKeyword) return null;
866
- if (ts.isNumericLiteral(value)) return Number(value.text);
867
- if (ts.isArrayLiteralExpression(value)) return value.elements.map(literalValue);
868
- if (ts.isObjectLiteralExpression(value)) {
869
- const result: Record<string, unknown> = {};
870
- for (const property of value.properties) {
871
- if (!ts.isPropertyAssignment(property))
872
- throw new TypeError('manifest properties must be data');
873
- const name = property.name;
874
- const key =
875
- ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)
876
- ? name.text
877
- : undefined;
878
- if (key === undefined) throw new TypeError('manifest property names must be static');
879
- result[key] = literalValue(property.initializer);
880
- }
881
- return result;
882
- }
883
- throw new TypeError('manifest values must be literal data');
884
- }
885
-
886
- function parseCanonicalManifest(source: string, sourcePath: string): CanonicalManifest | undefined {
887
- if (!source.startsWith(`${CANONICAL_SCAFFOLD_MARKER}\n`)) return undefined;
888
- const file = ts.createSourceFile(
889
- sourcePath,
890
- source,
891
- ts.ScriptTarget.Latest,
892
- true,
893
- ts.ScriptKind.TS,
894
- );
895
- for (const statement of file.statements) {
896
- if (!ts.isVariableStatement(statement)) continue;
897
- for (const declaration of statement.declarationList.declarations) {
898
- if (
899
- ts.isIdentifier(declaration.name) &&
900
- declaration.name.text === CANONICAL_MANIFEST_NAME &&
901
- declaration.initializer !== undefined
902
- ) {
903
- const value = literalValue(declaration.initializer);
904
- if (value === null || typeof value !== 'object') return undefined;
905
- return value as CanonicalManifest;
906
- }
907
- }
908
- }
909
- return undefined;
910
- }
911
-
912
- function guidExpression(value: string): string {
913
- return `parseGuid(${JSON.stringify(value)})`;
914
- }
915
-
916
- /** Render the one source shape that the file authoring port may mutate structurally. */
917
- export function renderCanonicalScriptablePack(manifest: CanonicalManifest): string {
918
- const data = JSON.stringify(manifest, null, 2);
919
- return `${CANONICAL_SCAFFOLD_MARKER}
920
- import { AssetGuid } from '@forgeax/engine-pack/guid';
921
- import { buildMeshAttributeMapForUvSets } from '@forgeax/engine-geometry';
922
- import type { ScriptablePackDefinition } from '@forgeax/engine-pack/source';
923
- import type { Asset, AssetGuid as AssetGuidType } from '@forgeax/engine-types';
924
- import { ok } from '@forgeax/engine-types';
925
-
926
- const ${CANONICAL_MANIFEST_NAME} = ${data} as const;
927
-
928
- function parseGuid(value: string): AssetGuidType {
929
- const parsed = AssetGuid.parse(value);
930
- if (!parsed.ok) throw parsed.error;
931
- return parsed.value;
932
- }
933
-
934
- function createOutput(kind: string): Asset {
935
- switch (kind) {
936
- case 'scene':
937
- return { kind: 'scene', entities: [], mounts: [] };
938
- case 'material':
939
- return { kind: 'material', values: {} };
940
- case 'mesh': {
941
- const vertices = new Float32Array([
942
- 0, 0.7, 0, 0, 0, 1, 0.5, 1, 0, 0, 0, 1,
943
- -0.7, -0.6, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1,
944
- 0.7, -0.6, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1,
945
- ]);
946
- return {
947
- kind: 'mesh',
948
- vertices,
949
- indices: new Uint16Array([0, 1, 2]),
950
- attributes: { ...buildMeshAttributeMapForUvSets(1), position: vertices },
951
- aabb: new Float32Array([-0.7, -0.6, 0, 0.7, 0.7, 0]),
952
- submeshes: [{ indexOffset: 0, indexCount: 3, vertexCount: 3, topology: 'triangle-list', materialSlot: 0 }],
953
- materialSlots: [{ slotName: 'Default' }],
954
- };
955
- }
956
- default:
957
- throw new Error(\`canonical ScriptablePack output kind is not mutable: \${kind}\`);
958
- }
959
- }
960
-
961
- const assets = Object.fromEntries(
962
- Object.entries(${CANONICAL_MANIFEST_NAME}.assets).map(([sourceKey, asset]) => [
963
- sourceKey,
964
- { ...asset, guid: parseGuid(asset.guid) },
965
- ]),
966
- );
967
- const externalAssets = Object.fromEntries(
968
- Object.entries(${CANONICAL_MANIFEST_NAME}.externalAssets).map(([alias, guid]) => [alias, parseGuid(guid)]),
969
- );
970
-
971
- export default {
972
- schemaVersion: '1.0.0',
973
- packageId: ${guidExpression(manifest.packageId)},
974
- ${manifest.name === undefined ? '' : `name: ${JSON.stringify(manifest.name)},\n `}assets,
975
- externalAssets,
976
- build: () => ok(Object.fromEntries(Object.entries(assets).map(([sourceKey, asset]) => [sourceKey, createOutput(asset.kind)])) as Record<string, Asset>),
977
- } satisfies ScriptablePackDefinition;
978
- `;
979
- }
980
-
981
- function definitionFromCanonical(
982
- manifest: CanonicalManifest,
983
- ): Result<ScriptablePackDefinition, ScriptablePackError> {
984
- const packageId = AssetGuidCodec.parse(manifest.packageId);
985
- if (!packageId.ok) return invalidCanonicalGuid('$.packageId', manifest.packageId);
986
- const assets: Record<string, { guid: AssetGuid; kind: ScriptablePackAssetKind; name?: string }> =
987
- {};
988
- for (const [sourceKey, output] of Object.entries(manifest.assets)) {
989
- const guid = AssetGuidCodec.parse(output.guid);
990
- if (!guid.ok)
991
- return invalidCanonicalGuid(`$.assets[${JSON.stringify(sourceKey)}].guid`, output.guid);
992
- assets[sourceKey] = {
993
- guid: guid.value,
994
- kind: output.kind,
995
- ...(output.name === undefined ? {} : { name: output.name }),
996
- };
997
- }
998
- const externalAssets: Record<string, AssetGuid> = {};
999
- for (const [alias, value] of Object.entries(manifest.externalAssets)) {
1000
- const guid = AssetGuidCodec.parse(value);
1001
- if (!guid.ok) return invalidCanonicalGuid(`$.externalAssets[${JSON.stringify(alias)}]`, value);
1002
- externalAssets[alias] = guid.value;
1003
- }
1004
- return validateScriptablePackDefinition({
1005
- schemaVersion: '1.0.0',
1006
- packageId: packageId.value,
1007
- ...(manifest.name === undefined ? {} : { name: manifest.name }),
1008
- assets,
1009
- externalAssets,
1010
- build: () => ok({} as Record<string, never>),
1011
- });
1012
- }
1013
-
1014
- function invalidCanonicalGuid(
1015
- propertyPath: string,
1016
- actual: string,
1017
- ): Result<never, ScriptablePackError> {
1018
- return err({
1019
- code: 'pack-source-definition-invalid',
1020
- expected: 'a canonical UUID string',
1021
- hint: 'repair the canonical scaffold manifest GUID and inspect Meta again',
1022
- detail: { propertyPath, actual },
1023
- });
1024
- }
1025
-
1026
- function invalidCanonicalSource(
1027
- sourcePath: string,
1028
- error: unknown,
1029
- ): Result<never, ScriptablePackError> {
1030
- return err({
1031
- code: 'pack-source-definition-invalid',
1032
- expected: 'a literal canonical-v1 manifest with the required package/assets/external fields',
1033
- hint: 'repair the canonical scaffold or open it as custom source without structured mutation',
1034
- detail: {
1035
- sourcePath,
1036
- propertyPath: '$.canonicalManifest',
1037
- actual: error instanceof Error ? error.message : String(error),
1038
- },
1039
- });
1040
- }
1041
-
1042
- async function atomicWrite(path: string, source: string): Promise<void> {
1043
- await mkdir(dirname(path), { recursive: true });
1044
- const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
1045
- try {
1046
- await writeFile(temporary, source, { encoding: 'utf8', flag: 'wx' });
1047
- await rename(temporary, path);
1048
- } catch (error) {
1049
- await rm(temporary, { force: true });
1050
- throw error;
1051
- }
1052
- }
1053
-
1054
- function sameStrings(left: readonly string[], right: readonly string[]): boolean {
1055
- return [...left].sort().join('\0') === [...right].sort().join('\0');
1056
- }
1057
-
1058
- /** Node filesystem adapter for the public ScriptablePack authoring gateway. */
1059
- export function createFileSystemScriptablePackAuthoringPort(
1060
- options: FileSystemScriptablePackAuthoringOptions,
1061
- ): ScriptablePackAuthoringPort {
1062
- const resolvePath = (sourcePath: string) => confinedSourcePath(options.gameRoot, sourcePath);
1063
-
1064
- async function readSource(sourcePath: string) {
1065
- const path = resolvePath(sourcePath);
1066
- if (!path.ok) return path;
1067
- try {
1068
- const source = await readFile(path.value.absolute, 'utf8');
1069
- return ok({ ...path.value, source, revision: revisionOf(source) });
1070
- } catch (error) {
1071
- return err(
1072
- authoringError('pack-source-write-failed', {
1073
- expected: 'a readable ScriptablePack source file',
1074
- actual: error instanceof Error ? error.message : String(error),
1075
- hint: 'restore the source file or create it through the ScriptablePack gateway',
1076
- retryable: true,
1077
- recoveryActions: ['inspect-source-path', 'retry'],
1078
- sourcePath,
1079
- }),
1080
- );
1081
- }
1082
- }
1083
-
1084
- async function inspect(sourcePath: string) {
1085
- const read = await readSource(sourcePath);
1086
- if (!read.ok) return read;
1087
- let canonical: CanonicalManifest | undefined;
1088
- try {
1089
- canonical = parseCanonicalManifest(read.value.source, read.value.relative);
1090
- } catch (error) {
1091
- return invalidCanonicalSource(read.value.relative, error);
1092
- }
1093
- const loaded =
1094
- canonical === undefined
1095
- ? await loadScriptablePack(read.value.absolute, { metadataOnly: true })
1096
- : definitionFromCanonical(canonical);
1097
- if (!loaded.ok) return loaded;
1098
- return ok({
1099
- revision: read.value.revision,
1100
- meta: projectScriptablePackMeta(loaded.value, read.value.relative),
1101
- });
1102
- }
1103
-
1104
- async function preflight(sourcePath: string) {
1105
- const read = await readSource(sourcePath);
1106
- if (!read.ok) return read;
1107
- const inspected = await inspect(sourcePath);
1108
- if (!inspected.ok) return inspected;
1109
- let canonical: CanonicalManifest | undefined;
1110
- try {
1111
- canonical = parseCanonicalManifest(read.value.source, read.value.relative);
1112
- } catch (error) {
1113
- return invalidCanonicalSource(read.value.relative, error);
1114
- }
1115
- const incomingRefs = (await options.incomingRefs?.(read.value.relative)) ?? [];
1116
- return ok({
1117
- revision: inspected.value.revision,
1118
- meta: inspected.value.meta,
1119
- capabilities:
1120
- canonical === undefined
1121
- ? ({
1122
- inspect: true,
1123
- rebuild: true,
1124
- coldCook: true,
1125
- reason: 'structured mutation is available only for canonical-v1 scaffolds',
1126
- } as const)
1127
- : ({
1128
- inspect: true,
1129
- rebuild: true,
1130
- coldCook: true,
1131
- addOutput: true,
1132
- addExternalAsset: true,
1133
- renameDisplay: true,
1134
- removeOutput: true,
1135
- clone: true,
1136
- } as const),
1137
- incomingRefs,
1138
- });
1139
- }
1140
-
1141
- async function mutate(operation: ScriptablePackAuthoringMutation) {
1142
- const targetPath =
1143
- operation.kind === 'clone-scriptable-pack' ? operation.targetPath : operation.sourcePath;
1144
- const target = resolvePath(targetPath);
1145
- if (!target.ok) return target;
1146
-
1147
- let manifest: CanonicalManifest;
1148
- let currentRevision: string | undefined;
1149
- if (operation.kind === 'create-scriptable-pack') {
1150
- if (!CANONICAL_MUTABLE_KINDS.has(operation.initialOutput.kind)) {
1151
- return err(
1152
- authoringError('pack-source-mutation-unsupported', {
1153
- expected: 'a canonical scene, mesh, or material initial output',
1154
- actual: operation.initialOutput.kind,
1155
- hint: 'create one of the canonical output kinds, then hand-author advanced outputs',
1156
- retryable: false,
1157
- recoveryActions: ['choose-supported-output-kind'],
1158
- requestId: operation.requestId,
1159
- sourcePath: operation.sourcePath,
1160
- }),
1161
- );
1162
- }
1163
- try {
1164
- await stat(target.value.absolute);
1165
- return err(
1166
- authoringError('pack-source-revision-conflict', {
1167
- expected: 'an unused target source path',
1168
- actual: 'source already exists',
1169
- hint: 'inspect the existing source or choose a new path',
1170
- retryable: false,
1171
- recoveryActions: ['asset.preflight', 'choose-new-source-path'],
1172
- requestId: operation.requestId,
1173
- sourcePath: operation.sourcePath,
1174
- }),
1175
- );
1176
- } catch {
1177
- // Missing is the required create precondition.
1178
- }
1179
- manifest = {
1180
- schemaVersion: '1.0.0',
1181
- packageId: AssetGuidCodec.format(operation.packageId),
1182
- ...(operation.name === undefined ? {} : { name: operation.name }),
1183
- assets: {
1184
- [operation.initialOutput.sourceKey]: {
1185
- guid: AssetGuidCodec.format(operation.initialOutput.guid),
1186
- kind: operation.initialOutput.kind,
1187
- ...(operation.initialOutput.name === undefined
1188
- ? {}
1189
- : { name: operation.initialOutput.name }),
1190
- },
1191
- },
1192
- externalAssets: {},
1193
- };
1194
- } else {
1195
- const read = await readSource(operation.sourcePath);
1196
- if (!read.ok) return read;
1197
- currentRevision = read.value.revision;
1198
- if (
1199
- operation.expectedRevision !== undefined &&
1200
- operation.expectedRevision !== currentRevision
1201
- ) {
1202
- return err(
1203
- authoringError('pack-source-revision-conflict', {
1204
- expected: operation.expectedRevision,
1205
- actual: currentRevision,
1206
- hint: 'inspect the current source revision, reconcile the edit, then retry with a new requestId',
1207
- retryable: true,
1208
- recoveryActions: ['asset.preflight', 'mint-request-id'],
1209
- requestId: operation.requestId,
1210
- sourcePath: operation.sourcePath,
1211
- }),
1212
- );
1213
- }
1214
- let parsed: CanonicalManifest | undefined;
1215
- try {
1216
- parsed = parseCanonicalManifest(read.value.source, read.value.relative);
1217
- } catch (error) {
1218
- return invalidCanonicalSource(read.value.relative, error);
1219
- }
1220
- if (parsed === undefined) {
1221
- return err(
1222
- authoringError('pack-source-mutation-unsupported', {
1223
- expected: 'a canonical-v1 scaffold for structured source mutation',
1224
- actual: 'custom ScriptablePack source',
1225
- hint: 'clone into a canonical scaffold or edit custom source with a code editor',
1226
- retryable: false,
1227
- recoveryActions: ['clone-scriptable-pack', 'open-code-editor'],
1228
- requestId: operation.requestId,
1229
- sourcePath: operation.sourcePath,
1230
- }),
1231
- );
1232
- }
1233
- manifest = parsed;
1234
- }
1235
-
1236
- if (operation.kind === 'clone-scriptable-pack') {
1237
- try {
1238
- await stat(target.value.absolute);
1239
- return err(
1240
- authoringError('pack-source-revision-conflict', {
1241
- expected: 'an unused clone target source path',
1242
- actual: 'source already exists',
1243
- hint: 'inspect the existing target or choose a new path',
1244
- retryable: false,
1245
- recoveryActions: ['asset.preflight', 'choose-new-source-path'],
1246
- requestId: operation.requestId,
1247
- sourcePath: target.value.relative,
1248
- }),
1249
- );
1250
- } catch {
1251
- // Missing is the required clone precondition.
1252
- }
1253
- }
1254
-
1255
- if (operation.kind === 'add-output') {
1256
- if (!CANONICAL_MUTABLE_KINDS.has(operation.assetKind)) {
1257
- return err(
1258
- authoringError('pack-source-mutation-unsupported', {
1259
- expected: 'a canonical scene, mesh, or material output',
1260
- actual: operation.assetKind,
1261
- hint: 'hand-author advanced output kinds in the source module',
1262
- retryable: false,
1263
- recoveryActions: ['open-code-editor'],
1264
- requestId: operation.requestId,
1265
- sourcePath: operation.sourcePath,
1266
- sourceKey: operation.sourceKey,
1267
- }),
1268
- );
1269
- }
1270
- manifest = {
1271
- ...manifest,
1272
- assets: {
1273
- ...manifest.assets,
1274
- [operation.sourceKey]: {
1275
- guid: AssetGuidCodec.format(operation.guid),
1276
- kind: operation.assetKind,
1277
- ...(operation.name === undefined ? {} : { name: operation.name }),
1278
- },
1279
- },
1280
- };
1281
- } else if (operation.kind === 'add-external-asset') {
1282
- manifest = {
1283
- ...manifest,
1284
- externalAssets: {
1285
- ...manifest.externalAssets,
1286
- [operation.alias]: AssetGuidCodec.format(operation.guid),
1287
- },
1288
- };
1289
- } else if (operation.kind === 'rename-display') {
1290
- if (operation.target.kind === 'package') manifest = { ...manifest, name: operation.name };
1291
- else {
1292
- const output = manifest.assets[operation.target.sourceKey];
1293
- if (output === undefined)
1294
- return err(
1295
- authoringError('pack-source-mutation-unsupported', {
1296
- expected: 'an existing sourceKey',
1297
- actual: operation.target.sourceKey,
1298
- hint: 'inspect Meta and choose a current output',
1299
- retryable: true,
1300
- recoveryActions: ['asset.preflight'],
1301
- requestId: operation.requestId,
1302
- sourcePath: operation.sourcePath,
1303
- sourceKey: operation.target.sourceKey,
1304
- }),
1305
- );
1306
- manifest = {
1307
- ...manifest,
1308
- assets: {
1309
- ...manifest.assets,
1310
- [operation.target.sourceKey]: { ...output, name: operation.name },
1311
- },
1312
- };
1313
- }
1314
- } else if (operation.kind === 'remove-output') {
1315
- const incomingRefs =
1316
- (await options.incomingRefs?.(operation.sourcePath, operation.sourceKey)) ?? [];
1317
- if (
1318
- incomingRefs.length > 0 &&
1319
- !sameStrings(incomingRefs, operation.confirmIncomingRefs ?? [])
1320
- ) {
1321
- return err(
1322
- authoringError('pack-source-reference-conflict', {
1323
- expected: 'explicit acknowledgement of every current incoming reference',
1324
- actual: `${incomingRefs.length} incoming references`,
1325
- hint: 'inspect dependents, then confirm the exact incoming reference identities or cancel',
1326
- retryable: true,
1327
- recoveryActions: ['inspect-incoming-refs', 'confirm-remove-output'],
1328
- requestId: operation.requestId,
1329
- sourcePath: operation.sourcePath,
1330
- sourceKey: operation.sourceKey,
1331
- incomingRefs,
1332
- }),
1333
- );
1334
- }
1335
- const { [operation.sourceKey]: removed, ...assets } = manifest.assets;
1336
- if (removed === undefined || Object.keys(assets).length === 0) {
1337
- return err(
1338
- authoringError('pack-source-mutation-unsupported', {
1339
- expected: 'an existing output while retaining at least one package output',
1340
- actual: operation.sourceKey,
1341
- hint: 'keep one output or remove the entire source file through an explicit file operation',
1342
- retryable: false,
1343
- recoveryActions: ['asset.preflight'],
1344
- requestId: operation.requestId,
1345
- sourcePath: operation.sourcePath,
1346
- sourceKey: operation.sourceKey,
1347
- }),
1348
- );
1349
- }
1350
- manifest = { ...manifest, assets };
1351
- } else if (operation.kind === 'clone-scriptable-pack') {
1352
- manifest = {
1353
- ...manifest,
1354
- packageId: AssetGuidCodec.format(operation.packageId),
1355
- assets: Object.fromEntries(
1356
- Object.entries(manifest.assets).map(([sourceKey, output]) => [
1357
- sourceKey,
1358
- {
1359
- ...output,
1360
- guid: AssetGuidCodec.format(operation.outputGuids[sourceKey] as AssetGuid),
1361
- },
1362
- ]),
1363
- ),
1364
- };
1365
- }
1366
-
1367
- const source = renderCanonicalScriptablePack(manifest);
1368
- try {
1369
- await atomicWrite(target.value.absolute, source);
1370
- return ok({ sourcePath: target.value.relative, revision: revisionOf(source) });
1371
- } catch (error) {
1372
- return err(
1373
- authoringError('pack-source-write-failed', {
1374
- expected: 'an atomic source-file replacement',
1375
- actual: error instanceof Error ? error.message : String(error),
1376
- hint: 'repair filesystem permissions or disk capacity, then retry with a new requestId',
1377
- retryable: true,
1378
- recoveryActions: ['inspect-filesystem', 'mint-request-id'],
1379
- requestId: operation.requestId,
1380
- sourcePath: target.value.relative,
1381
- }),
1382
- );
1383
- }
1384
- }
1385
-
1386
- return {
1387
- preflight,
1388
- mutate,
1389
- inspect,
1390
- async rebuild(sourcePath, mode) {
1391
- const path = resolvePath(sourcePath);
1392
- if (!path.ok) return path;
1393
- const rebuilt = (await options.rebuild?.(path.value.relative, mode)) ?? ok(undefined);
1394
- return rebuilt.ok ? inspect(path.value.relative) : rebuilt;
1395
- },
1396
- };
1397
786
  }