@forgeax/engine-import 0.1.28 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -109,16 +109,11 @@ function runtimePublicationFor(
109
109
  ...(input.digest === undefined ? {} : { digest: input.digest }),
110
110
  ...(input.inputFingerprint === undefined ? {} : { inputFingerprint: input.inputFingerprint }),
111
111
  ...(input.outputs === undefined ? {} : { outputs: input.outputs }),
112
+ ...(input.sourceKeys === undefined ? {} : { sourceKeys: input.sourceKeys }),
112
113
  ...(input.externalEvidence === undefined ? {} : { externalEvidence: input.externalEvidence }),
113
114
  generation: context.runtimeBinding?.generation ?? context.generation,
114
115
  };
115
- if (input.sourceKeys === undefined) return createRuntimePackPublication(publicationInput);
116
- const derived = createRuntimePackPublication(publicationInput);
117
- const outputs = derived.publication.outputs.map((output) => ({
118
- ...output,
119
- sourceKey: input.sourceKeys?.get(output.guid.toLowerCase()) ?? output.sourceKey,
120
- }));
121
- return createRuntimePackPublication({ ...publicationInput, outputs });
116
+ return createRuntimePackPublication(publicationInput);
122
117
  }
123
118
 
124
119
  function sourceKeysFor(
@@ -622,6 +617,7 @@ async function emitAuthoredPack(
622
617
  };
623
618
  const runtimePublication = runtimePublicationFor(work.context, {
624
619
  pack: authoredPack,
620
+ sourceKeys: sourceKeysFor(legacy.assets),
625
621
  sourcePath: entry.sourcePath,
626
622
  sourceRevision: declaration.sourceRevision,
627
623
  packageUrl:
@@ -9,6 +9,8 @@ import {
9
9
  PackageId,
10
10
  type PackBuildContextWithoutParameters,
11
11
  type PackBuildContextWithParameters,
12
+ type PackCookSource,
13
+ type PackOutputMap,
12
14
  type PackParameterDefinition,
13
15
  type PackParameterValue,
14
16
  resolvePackParameterValues,
@@ -18,6 +20,7 @@ import type {
18
20
  Asset,
19
21
  AssetGuid as AssetGuidType,
20
22
  AssetPublicationEnvelope,
23
+ AssetRef,
21
24
  ImportedAsset,
22
25
  Result,
23
26
  } from '@forgeax/engine-types';
@@ -37,6 +40,8 @@ import type {
37
40
  ScriptablePackSourceClosureEntry,
38
41
  ScriptablePackStagedOutput,
39
42
  } from './scriptable-pack.js';
43
+ import { scriptablePackFingerprint as digest } from './scriptable-pack-fingerprint.js';
44
+ import { materialAssetOutputProducer } from './scriptable-pack-output-producers.js';
40
45
 
41
46
  export interface ScriptablePackBuildOptions {
42
47
  readonly definition: AnyScriptablePackDefinition;
@@ -80,7 +85,7 @@ export type ScriptablePackBuildResult = Result<
80
85
 
81
86
  interface ObservedRead {
82
87
  readonly guid: string;
83
- readonly asset: Asset;
88
+ readonly asset: Asset | PackCookSource;
84
89
  readonly generation: number;
85
90
  readonly digest: string;
86
91
  }
@@ -101,35 +106,17 @@ function clone<T>(value: T): T {
101
106
  return structuredClone(value);
102
107
  }
103
108
 
104
- function stable(value: unknown): string {
105
- if (value instanceof Uint8Array) return JSON.stringify(Array.from(value));
106
- if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
107
- if (value !== null && typeof value === 'object') {
108
- const object = value as Record<string, unknown>;
109
- return `{${Object.keys(object)
110
- .sort()
111
- .map((key) => `${JSON.stringify(key)}:${stable(object[key])}`)
112
- .join(',')}}`;
113
- }
114
- return JSON.stringify(value) ?? 'null';
115
- }
116
-
117
- async function digest(value: unknown): Promise<string> {
118
- const crypto = globalThis.crypto?.subtle;
119
- if (crypto === undefined) throw new Error('Web Crypto API is required for Pack fingerprints');
120
- const bytes = await crypto.digest('SHA-256', new TextEncoder().encode(stable(value)));
121
- return `sha256:${Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, '0')).join('')}`;
122
- }
123
-
124
109
  function observedReader(source: ScriptablePackAssetSnapshotSource | undefined): {
125
110
  readonly reader: {
126
- readByGuid<TAsset extends Asset = Asset>(guid: AssetGuidType): Promise<Result<TAsset, unknown>>;
111
+ readByGuid<TAsset extends Asset | PackCookSource = Asset>(
112
+ guid: AssetGuidType,
113
+ ): Promise<Result<TAsset, unknown>>;
127
114
  };
128
115
  readonly reads: ReadonlyMap<string, ObservedRead>;
129
116
  } {
130
117
  const reads = new Map<string, ObservedRead>();
131
118
  const reader = {
132
- async readByGuid<TAsset extends Asset = Asset>(
119
+ async readByGuid<TAsset extends Asset | PackCookSource = Asset>(
133
120
  guid: AssetGuidType,
134
121
  ): Promise<Result<TAsset, unknown>> {
135
122
  const key = AssetGuid.format(guid).toLowerCase();
@@ -218,7 +205,7 @@ function referenceError(
218
205
  function productError(
219
206
  sourcePath: string,
220
207
  sourceKey: string,
221
- producer: AssetOutputProducer,
208
+ producer: Pick<AssetOutputProducer, 'kind'>,
222
209
  product: unknown,
223
210
  ): PackAuthoringError | undefined {
224
211
  if (!record(product)) {
@@ -338,9 +325,7 @@ export async function buildScriptablePack(
338
325
  });
339
326
  }
340
327
 
341
- let built:
342
- | Result<Record<string, Asset>, unknown>
343
- | Promise<Result<Record<string, Asset>, unknown>>;
328
+ let built: Result<PackOutputMap, unknown> | Promise<Result<PackOutputMap, unknown>>;
344
329
  try {
345
330
  const context = buildContext(subjectPackageId, effectiveValues, observed.reader);
346
331
  built = options.definition.build(context as never);
@@ -392,14 +377,32 @@ export async function buildScriptablePack(
392
377
  const imported: ImportedAsset<unknown>[] = [];
393
378
  const stagedOutputs: ScriptablePackStagedOutput[] = [];
394
379
  const localGuids = new Set<string>();
395
- const materialCookerRegistry = new NativeCookerRegistry();
396
- for (const cooker of options.cookers ?? []) materialCookerRegistry.register(cooker);
380
+ const cookerRegistry = new NativeCookerRegistry();
381
+ for (const cooker of options.cookers ?? []) cookerRegistry.register(cooker);
397
382
  const nativeFingerprints = new Map<string, string>();
398
383
  for (const sourceKey of Object.keys(built.value).sort()) {
399
384
  if (!isValidPackSourceKey(sourceKey))
400
385
  return err(sourceKeyFailure(options.sourcePath, sourceKey));
401
386
  const asset = built.value[sourceKey];
402
- if (!isAsset(asset)) {
387
+ const customSource = record(asset) && asset.execution === 'cooked';
388
+ if (
389
+ customSource &&
390
+ (typeof asset.kind !== 'string' ||
391
+ asset.kind.trim().length === 0 ||
392
+ isScriptablePackAssetKind(asset.kind) ||
393
+ !('source' in asset) ||
394
+ Object.keys(asset).some((key) => !['kind', 'execution', 'source'].includes(key)))
395
+ ) {
396
+ return err(
397
+ outputValueError(
398
+ options.sourcePath,
399
+ sourceKey,
400
+ 'a custom kind with only execution: cooked and source; ordinary Assets use their standard producers',
401
+ asset,
402
+ ),
403
+ );
404
+ }
405
+ if (!customSource && !isAsset(asset)) {
403
406
  return err(
404
407
  outputValueError(
405
408
  options.sourcePath,
@@ -420,6 +423,47 @@ export async function buildScriptablePack(
420
423
  });
421
424
  }
422
425
  localGuids.add(normalizedGuid);
426
+ if (customSource) {
427
+ const cooked = await cookerRegistry.runDraft(asset.kind, {
428
+ guid,
429
+ sourceKey,
430
+ sourcePath: options.sourcePath,
431
+ source: asset.source,
432
+ refs: [],
433
+ });
434
+ if (!cooked.ok) return err(cooked.error);
435
+ if (cooked.value.guid.toLowerCase() !== normalizedGuid)
436
+ return err({
437
+ code: 'pack-source-output-invalid',
438
+ expected: 'the custom cooker to preserve the derived AssetGuid',
439
+ hint: 'remove cooker-owned identity generation and retain the host GUID',
440
+ detail: {
441
+ sourcePath: options.sourcePath,
442
+ sourceKey,
443
+ expectedGuid: guid,
444
+ actualGuid: cooked.value.guid,
445
+ },
446
+ });
447
+ const product = {
448
+ payload: cooked.value.payload,
449
+ refs: cooked.value.refs.map((guid) => ({ guid })),
450
+ artifacts: cooked.value.artifacts,
451
+ };
452
+ const invalid = productError(options.sourcePath, sourceKey, { kind: asset.kind }, product);
453
+ if (invalid !== undefined) return err(invalid);
454
+ imported.push({ guid, kind: asset.kind, ...product });
455
+ nativeFingerprints.set(sourceKey, cooked.value.inputFingerprint);
456
+ stagedOutputs.push({
457
+ guid: AssetGuid.derive(subjectPackageId, sourceKey),
458
+ sourceKey,
459
+ asset: clone(asset),
460
+ digest: await digest(asset),
461
+ });
462
+ continue;
463
+ }
464
+ // A custom source has been handled above; ordinary producers retain the closed Asset union.
465
+ if (!isAsset(asset))
466
+ return err(outputValueError(options.sourcePath, sourceKey, 'an ordinary Asset', asset));
423
467
  const producer = options.outputs.get(asset.kind);
424
468
  if (producer === undefined) {
425
469
  return err(
@@ -472,8 +516,9 @@ export async function buildScriptablePack(
472
516
  const product = produced.value as ImportAssetProduct<unknown>;
473
517
  let payload = product.payload;
474
518
  let artifacts = product.artifacts;
475
- if (needsMaterialCook(asset) && materialCookerRegistry.get('material') !== undefined) {
476
- const cooked = await materialCookerRegistry.runDraft('material', {
519
+ let refs = product.refs;
520
+ if (needsMaterialCook(asset) && cookerRegistry.get('material') !== undefined) {
521
+ const cooked = await cookerRegistry.runDraft('material', {
477
522
  guid,
478
523
  source: asset,
479
524
  sourceKey,
@@ -494,15 +539,64 @@ export async function buildScriptablePack(
494
539
  },
495
540
  });
496
541
  }
497
- payload = cooked.value.payload;
498
- artifacts = cooked.value.artifacts;
542
+ // Native cooking owns shader/program composition, but the Pack output
543
+ // producer owns the serialized wire shape. Re-project the cooked
544
+ // material once so texture/sampler/parent values become refsIndex
545
+ // entries again instead of leaking authored GUID strings into JSON.
546
+ const cookedAsset = cooked.value.payload;
547
+ if (!isAsset(cookedAsset) || cookedAsset.kind !== 'material') {
548
+ return err({
549
+ code: 'pack-source-output-invalid',
550
+ expected: 'the authored material cooker to return a material payload',
551
+ hint: 'repair the native material cooker payload and rebuild the Pack',
552
+ detail: { sourcePath: options.sourcePath, sourceKey },
553
+ });
554
+ }
555
+ const projected = await materialAssetOutputProducer.produce({
556
+ guid,
557
+ sourceKey,
558
+ asset: cookedAsset,
559
+ });
560
+ if (!projected.ok) return err(projected.error);
561
+ const projectedInvalid = productError(
562
+ options.sourcePath,
563
+ sourceKey,
564
+ materialAssetOutputProducer,
565
+ projected.value,
566
+ );
567
+ if (projectedInvalid !== undefined) return err(projectedInvalid);
568
+ payload = projected.value.payload;
569
+ artifacts = {
570
+ ...product.artifacts,
571
+ ...cooked.value.artifacts,
572
+ ...projected.value.artifacts,
573
+ };
574
+ const mergedRefs: AssetRef[] = [...projected.value.refs];
575
+ const seenRefs = new Set(mergedRefs.map((reference) => reference.guid.toLowerCase()));
576
+ for (const cookerRef of cooked.value.refs) {
577
+ const parsed = AssetGuid.parse(cookerRef);
578
+ if (!parsed.ok) {
579
+ return err({
580
+ code: 'pack-source-output-invalid',
581
+ expected: 'the authored material cooker refs to contain valid AssetGuid values',
582
+ hint: 'repair the native material cooker refs and rebuild the Pack',
583
+ detail: { sourcePath: options.sourcePath, sourceKey, ref: cookerRef },
584
+ });
585
+ }
586
+ const formatted = AssetGuid.format(parsed.value);
587
+ const normalizedRef = formatted.toLowerCase();
588
+ if (seenRefs.has(normalizedRef)) continue;
589
+ seenRefs.add(normalizedRef);
590
+ mergedRefs.push({ guid: formatted });
591
+ }
592
+ refs = mergedRefs;
499
593
  nativeFingerprints.set(sourceKey, cooked.value.inputFingerprint);
500
594
  }
501
595
  imported.push({
502
596
  guid,
503
597
  kind: asset.kind,
504
598
  payload,
505
- refs: product.refs,
599
+ refs,
506
600
  artifacts,
507
601
  });
508
602
  stagedOutputs.push({
@@ -1,4 +1,3 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { readdir, readFile, stat } from 'node:fs/promises';
3
2
  import { resolve } from 'node:path';
4
3
  import { isScriptablePackAssetKind, validatePack } from '@forgeax/engine-pack';
@@ -9,6 +8,7 @@ import type {
9
8
  ScriptablePackAssetSnapshot,
10
9
  ScriptablePackAssetSnapshotSource,
11
10
  } from './scriptable-pack.js';
11
+ import { scriptablePackFingerprint as digest } from './scriptable-pack-fingerprint.js';
12
12
 
13
13
  export interface ScriptablePackFileAssetSnapshotSourceOptions {
14
14
  readonly assetRoots: readonly string[];
@@ -90,30 +90,6 @@ function isRecord(value: unknown): value is Record<string, unknown> {
90
90
  return value !== null && typeof value === 'object' && !Array.isArray(value);
91
91
  }
92
92
 
93
- function stable(value: unknown): string {
94
- if (value instanceof ArrayBuffer) {
95
- return `ArrayBuffer:${JSON.stringify(Array.from(new Uint8Array(value)))}`;
96
- }
97
- if (ArrayBuffer.isView(value)) {
98
- return `${value.constructor.name}:${JSON.stringify(
99
- Array.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)),
100
- )}`;
101
- }
102
- if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
103
- if (value !== null && typeof value === 'object') {
104
- const record = value as Record<string, unknown>;
105
- return `{${Object.keys(record)
106
- .sort()
107
- .map((key) => `${JSON.stringify(key)}:${stable(record[key])}`)
108
- .join(',')}}`;
109
- }
110
- return JSON.stringify(value) ?? 'null';
111
- }
112
-
113
- function digest(value: unknown): string {
114
- return `sha256:${createHash('sha256').update(stable(value)).digest('hex')}`;
115
- }
116
-
117
93
  function generationFromDigest(value: string): number {
118
94
  const parsed = Number.parseInt(value.slice('sha256:'.length, 'sha256:'.length + 8), 16);
119
95
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 1;
@@ -302,9 +278,7 @@ async function indexPackFiles(
302
278
  }
303
279
  }
304
280
 
305
- const sourceDigest = digest(
306
- [...documents].sort((left, right) => stable(left).localeCompare(stable(right))),
307
- );
281
+ const sourceDigest = digest(documents.map(digest).sort());
308
282
  return ok({
309
283
  generation: generationFromDigest(sourceDigest),
310
284
  assets: byGuid,
@@ -0,0 +1,48 @@
1
+ import { sha256 } from '@noble/hashes/sha2.js';
2
+ import { bytesToHex } from '@noble/hashes/utils.js';
3
+
4
+ /** One bounded fingerprint encoding for authored, staged and file-backed assets. */
5
+ export function scriptablePackFingerprint(value: unknown): string {
6
+ const hash = sha256.create();
7
+ const encoder = new TextEncoder();
8
+ const text = (part: string) => {
9
+ hash.update(encoder.encode(part));
10
+ };
11
+ const binary = (type: string, bytes: Uint8Array) => {
12
+ // Raw NUL cannot occur in JSON tokens. The type and byte length delimit the
13
+ // binary lane without expanding every float into a sorted JSON property.
14
+ text(`\0${type}:${bytes.byteLength}:`);
15
+ hash.update(bytes);
16
+ text('\0');
17
+ };
18
+ const visit = (item: unknown): void => {
19
+ if (ArrayBuffer.isView(item)) {
20
+ binary(item.constructor.name, new Uint8Array(item.buffer, item.byteOffset, item.byteLength));
21
+ } else if (item instanceof ArrayBuffer) {
22
+ binary('ArrayBuffer', new Uint8Array(item));
23
+ } else if (Array.isArray(item)) {
24
+ text('[');
25
+ for (let i = 0; i < item.length; i++) {
26
+ if (i) text(',');
27
+ visit(item[i]);
28
+ }
29
+ text(']');
30
+ } else if (item !== null && typeof item === 'object') {
31
+ const object = item as Record<string, unknown>;
32
+ text('{');
33
+ let first = true;
34
+ for (const key of Object.keys(object).sort()) {
35
+ if (!first) text(',');
36
+ first = false;
37
+ text(`${JSON.stringify(key)}:`);
38
+ visit(object[key]);
39
+ }
40
+ text('}');
41
+ } else {
42
+ text(JSON.stringify(item) ?? 'null');
43
+ }
44
+ };
45
+ text('scriptable-pack-fingerprint/2:');
46
+ visit(value);
47
+ return `sha256:${bytesToHex(hash.digest())}`;
48
+ }
@@ -466,9 +466,24 @@ function ordinaryPodProduct(input: AssetOutputInput): AssetOutputProduct {
466
466
  case 'particle-effect': {
467
467
  const effect = asset as ParticleEffectAsset;
468
468
  const cooked = particleProgramArtifact(effect);
469
+ const refs = new Set<string>();
470
+ for (const emitter of cooked.program.emitters) {
471
+ for (const renderer of emitter.renderers) {
472
+ if (!('material' in renderer) || typeof renderer.material !== 'string') {
473
+ throw new Error(`Particle emitter '${emitter.id}' requires a renderer Material GUID`);
474
+ }
475
+ refs.add(formatGuid(renderer.material));
476
+ if ('kind' in renderer && renderer.kind === 'mesh') {
477
+ if (!('mesh' in renderer) || typeof renderer.mesh !== 'string') {
478
+ throw new Error(`Particle emitter '${emitter.id}' requires a renderer Mesh GUID`);
479
+ }
480
+ refs.add(formatGuid(renderer.mesh));
481
+ }
482
+ }
483
+ }
469
484
  return {
470
485
  payload: { ...effect, programFingerprint: cooked.fingerprint, program: cooked.program },
471
- refs: [],
486
+ refs: [...refs].sort().map((guid) => ({ guid })),
472
487
  artifacts: {
473
488
  'particle-effect/program.json': {
474
489
  mediaType: 'application/json',
@@ -504,6 +519,7 @@ export const materialAssetOutputProducer = createSafeProducer(
504
519
  'material-pack/2',
505
520
  materialProduct,
506
521
  );
522
+
507
523
  export const meshAssetOutputProducer = createSafeProducer('mesh', 'mesh-binary/4', meshProduct);
508
524
  export const textureAssetOutputProducer = createSafeProducer(
509
525
  'texture',
@@ -547,7 +563,13 @@ export function createStandardAssetOutputProducerRegistry(
547
563
  'particle-effect',
548
564
  'ies-profile',
549
565
  ] as const) {
550
- registry.register(createSafeProducer(kind, 'ordinary-pod/1', ordinaryPodProduct));
566
+ registry.register(
567
+ createSafeProducer(
568
+ kind,
569
+ kind === 'particle-effect' ? 'particle-effect/2' : 'ordinary-pod/1',
570
+ ordinaryPodProduct,
571
+ ),
572
+ );
551
573
  }
552
574
  return registry;
553
575
  }
@@ -1,6 +1,6 @@
1
1
  import { AssetGuid } from '@forgeax/engine-pack/guid';
2
2
  import type { PackAuthoringError } from '@forgeax/engine-pack/source';
3
- import type { Asset, AssetGuid as AssetGuidType, ImportError, Result } from '@forgeax/engine-types';
3
+ import type { AssetGuid as AssetGuidType, ImportError, Result } from '@forgeax/engine-types';
4
4
  import { AssetError, err, ok } from '@forgeax/engine-types';
5
5
  import type {
6
6
  ScriptablePackAssetSnapshot,
@@ -8,6 +8,7 @@ import type {
8
8
  ScriptablePackDomainError,
9
9
  ScriptablePackStagedOutput,
10
10
  } from './scriptable-pack.js';
11
+ import { scriptablePackFingerprint as assetDigest } from './scriptable-pack-fingerprint.js';
11
12
 
12
13
  export type ScriptablePackSnapshotError =
13
14
  | AssetError
@@ -29,27 +30,6 @@ export interface ScriptablePackStagedSnapshotOptions {
29
30
  readonly declaredExternalOutputs?: readonly ScriptablePackStagedOutput[];
30
31
  }
31
32
 
32
- function stable(value: unknown): string {
33
- if (ArrayBuffer.isView(value)) {
34
- return `${value.constructor.name}:${JSON.stringify(Array.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)))}`;
35
- }
36
- if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
37
- if (value !== null && typeof value === 'object') {
38
- const record = value as Record<string, unknown>;
39
- return `{${Object.keys(record)
40
- .sort()
41
- .map((key) => `${JSON.stringify(key)}:${stable(record[key])}`)
42
- .join(',')}}`;
43
- }
44
- return JSON.stringify(value) ?? 'null';
45
- }
46
-
47
- async function assetDigest(asset: Asset): Promise<string> {
48
- const bytes = new TextEncoder().encode(stable(asset));
49
- const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes);
50
- return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('')}`;
51
- }
52
-
53
33
  function cycleError(
54
34
  stack: readonly string[],
55
35
  owner: string,
@@ -2,6 +2,7 @@ import type { ScriptablePackAssetKind } from '@forgeax/engine-pack';
2
2
  import type {
3
3
  AssetReader,
4
4
  PackAuthoringError,
5
+ PackCookSource,
5
6
  ScriptablePackReadError,
6
7
  } from '@forgeax/engine-pack/source';
7
8
  import type {
@@ -22,7 +23,7 @@ import type { ImportAssetProduct, TerminalImportProduct } from './import-product
22
23
  export type { ScriptablePackSourceClosureEntry } from '@forgeax/engine-pack/source';
23
24
 
24
25
  export interface ScriptablePackAssetSnapshot {
25
- readonly asset: Asset;
26
+ readonly asset: Asset | PackCookSource;
26
27
  readonly generation: number;
27
28
  readonly digest: string;
28
29
  }
@@ -30,7 +31,7 @@ export interface ScriptablePackAssetSnapshot {
30
31
  export interface ScriptablePackStagedOutput {
31
32
  readonly guid: AssetGuidType;
32
33
  readonly sourceKey: string;
33
- readonly asset: Asset;
34
+ readonly asset: Asset | PackCookSource;
34
35
  readonly digest?: string;
35
36
  }
36
37
 
@@ -44,6 +45,8 @@ export interface AssetOutputInput {
44
45
  readonly guid: string;
45
46
  readonly sourceKey: string;
46
47
  readonly asset: Asset;
48
+ /** Filesystem-backed authoring host; absent for pure payload projections. */
49
+ readonly sourcePath?: string;
47
50
  }
48
51
 
49
52
  type MaterialPackPayload = Omit<MaterialAsset, 'parent' | 'values'> & {
@@ -91,7 +94,10 @@ export type AssetOutputPayloadByKind = {
91
94
  };
92
95
 
93
96
  export type AssetOutputPayload = AssetOutputPayloadByKind[ScriptablePackAssetKind];
94
- export type AssetOutputProduct = ImportAssetProduct<AssetOutputPayload>;
97
+ export interface AssetOutputProduct extends ImportAssetProduct<AssetOutputPayload> {
98
+ readonly inputFingerprint?: string;
99
+ readonly sourceDependencies?: readonly string[];
100
+ }
95
101
 
96
102
  export interface AssetOutputProducer {
97
103
  readonly kind: string;