@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.
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ export { IMPORT_ERROR_HINTS, ImportError } from '@forgeax/engine-types';
3
3
  import { resolve, dirname, isAbsolute, relative } from 'path';
4
4
  import { resolveAssetSource, finalizePackageTransportSource, packageTransportRevision, catalogSourcePathFor, buildCatalogProjection, projectPackageCatalog, createRuntimePackPublication, metaPathForGuid } from '@forgeax/engine-pack/build';
5
5
  import { AssetGuid as AssetGuid$1, PackageId as PackageId$1 } from '@forgeax/engine-pack/guid';
6
- import { resolvePackParameterValues, PackageId, isValidPackSourceKey, AssetGuid, parsePackSourceJson, projectDirectPackJson, isScriptablePackAssetKind, projectScriptablePackSceneComponents, resolvePackParameterInheritance } from '@forgeax/engine-pack/source';
6
+ import { resolvePackParameterValues, PackageId, isValidPackSourceKey, isScriptablePackAssetKind, AssetGuid, parsePackSourceJson, projectDirectPackJson, projectScriptablePackSceneComponents, resolvePackParameterInheritance } from '@forgeax/engine-pack/source';
7
7
  import { loadScriptablePack } from '@forgeax/engine-pack/source-node';
8
8
  import { stat, readFile, mkdir, writeFile, rename, rm, readdir } from 'fs/promises';
9
9
  import { createAcceptedPublication, ddcOutputDigest, DdcLifecycle, DdcGenerationSession } from '@forgeax/engine-ddc';
@@ -11,12 +11,12 @@ import { deriveVertexLayoutProjection, normalizeMeshPayload } from '@forgeax/eng
11
11
  import { BUILTIN_MESH_ASSETS } from '@forgeax/engine-pack/builtin';
12
12
  import { NativeCookerRegistry } from '@forgeax/engine-pack/native-cooker';
13
13
  import { isEngineMaterial } from '@forgeax/engine-shader';
14
- import { externalizeSceneAsset } from '@forgeax/engine-scene';
15
14
  import { sha256 } from '@noble/hashes/sha2.js';
16
15
  import { bytesToHex } from '@noble/hashes/utils.js';
16
+ import { externalizeSceneAsset } from '@forgeax/engine-scene';
17
17
  import { MESH_BIN_HEADER_V4_BYTES, writeMeshBinHeader } from '@forgeax/engine-pack/mesh-bin-contract';
18
- import { createHash, randomUUID } from 'crypto';
19
18
  import { validatePack, isScriptablePackAssetKind as isScriptablePackAssetKind$1 } from '@forgeax/engine-pack';
19
+ import { createHash, randomUUID } from 'crypto';
20
20
  import { canonicalDdcJson } from '@forgeax/engine-ddc/key';
21
21
 
22
22
  // src/index.ts
@@ -45,8 +45,8 @@ async function sha256Hex(bytes2) {
45
45
  if (subtle === void 0) throw new Error("Web Crypto API is required for importer digests");
46
46
  const owned = new Uint8Array(bytes2.byteLength);
47
47
  owned.set(bytes2);
48
- const digest3 = await subtle.digest("SHA-256", owned.buffer);
49
- return Array.from(new Uint8Array(digest3), (byte) => byte.toString(16).padStart(2, "0")).join("");
48
+ const digest = await subtle.digest("SHA-256", owned.buffer);
49
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
50
50
  }
51
51
  async function artifactDigest(bytes2) {
52
52
  return `sha256:${await sha256Hex(bytes2)}`;
@@ -109,19 +109,19 @@ function finalizeImportProducts(product, inputFingerprint) {
109
109
  const products = [];
110
110
  for (const asset of product.assets) {
111
111
  const artifacts = await artifactDescriptors(asset.artifacts);
112
- const digest3 = await productDigest(asset, artifacts);
112
+ const digest = await productDigest(asset, artifacts);
113
113
  products.push({
114
114
  guid: asset.guid,
115
115
  payload: asset.payload,
116
116
  refs: asset.refs.map((ref2) => ref2.guid),
117
117
  artifacts,
118
- digest: digest3,
118
+ digest,
119
119
  receipt: {
120
120
  guid: asset.guid,
121
121
  origin: "sourceMeta",
122
122
  status: "succeeded",
123
123
  inputFingerprint,
124
- outputDigest: digest3
124
+ outputDigest: digest
125
125
  }
126
126
  });
127
127
  }
@@ -779,1217 +779,1364 @@ function projectImportProductForBuild(product) {
779
779
  }))
780
780
  };
781
781
  }
782
- function record(value) {
783
- return value !== null && typeof value === "object" && !Array.isArray(value);
784
- }
785
- function isAsset(value) {
786
- return record(value) && typeof value.kind === "string" && isScriptablePackAssetKind(value.kind);
787
- }
788
- function needsMaterialCook(asset) {
789
- return asset.kind === "material" && Array.isArray(asset.passes) && !isEngineMaterial(asset);
790
- }
791
- function clone(value) {
792
- return structuredClone(value);
793
- }
794
- function stable(value) {
795
- if (value instanceof Uint8Array) return JSON.stringify(Array.from(value));
796
- if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
797
- if (value !== null && typeof value === "object") {
798
- const object = value;
799
- return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stable(object[key])}`).join(",")}}`;
800
- }
801
- return JSON.stringify(value) ?? "null";
802
- }
803
- async function digest(value) {
804
- const crypto = globalThis.crypto?.subtle;
805
- if (crypto === void 0) throw new Error("Web Crypto API is required for Pack fingerprints");
806
- const bytes2 = await crypto.digest("SHA-256", new TextEncoder().encode(stable(value)));
807
- return `sha256:${Array.from(new Uint8Array(bytes2), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
808
- }
809
- function observedReader(source) {
810
- const reads = /* @__PURE__ */ new Map();
811
- const reader = {
812
- async readByGuid(guid) {
813
- const key = AssetGuid.format(guid).toLowerCase();
814
- const cached = reads.get(key);
815
- if (cached !== void 0) return ok(clone(cached.asset));
816
- if (source === void 0) {
817
- return err(
818
- new AssetError({
819
- code: "asset-not-found",
820
- expected: `a published Asset snapshot for content dependency ${key}`,
821
- hint: "publish the dependency or remove the content read from the Pack build",
822
- detail: { sourcePath: key }
823
- })
824
- );
782
+ function scriptablePackFingerprint(value) {
783
+ const hash = sha256.create();
784
+ const encoder = new TextEncoder();
785
+ const text = (part) => {
786
+ hash.update(encoder.encode(part));
787
+ };
788
+ const binary = (type, bytes2) => {
789
+ text(`\0${type}:${bytes2.byteLength}:`);
790
+ hash.update(bytes2);
791
+ text("\0");
792
+ };
793
+ const visit = (item) => {
794
+ if (ArrayBuffer.isView(item)) {
795
+ binary(item.constructor.name, new Uint8Array(item.buffer, item.byteOffset, item.byteLength));
796
+ } else if (item instanceof ArrayBuffer) {
797
+ binary("ArrayBuffer", new Uint8Array(item));
798
+ } else if (Array.isArray(item)) {
799
+ text("[");
800
+ for (let i = 0; i < item.length; i++) {
801
+ if (i) text(",");
802
+ visit(item[i]);
825
803
  }
826
- const result = await source.readByGuid(guid);
827
- if (!result.ok) return result;
828
- const asset = clone(result.value.asset);
829
- reads.set(key, {
830
- guid: key,
831
- asset,
832
- generation: result.value.generation,
833
- digest: result.value.digest
834
- });
835
- return ok(clone(asset));
804
+ text("]");
805
+ } else if (item !== null && typeof item === "object") {
806
+ const object = item;
807
+ text("{");
808
+ let first = true;
809
+ for (const key of Object.keys(object).sort()) {
810
+ if (!first) text(",");
811
+ first = false;
812
+ text(`${JSON.stringify(key)}:`);
813
+ visit(object[key]);
814
+ }
815
+ text("}");
816
+ } else {
817
+ text(JSON.stringify(item) ?? "null");
836
818
  }
837
819
  };
838
- return { reader, reads };
839
- }
840
- function buildContext(packageId, values, reader) {
841
- if (values === void 0) return { packageId, readByGuid: reader.readByGuid };
842
- return { packageId, values, readByGuid: reader.readByGuid };
843
- }
844
- function sourceKeyFailure(sourcePath, sourceKey) {
845
- return {
846
- code: "pack-source-key-invalid",
847
- expected: "a sourceKey matching the Pack source-key grammar",
848
- hint: "return stable lower-case semantic keys instead of paths or output indexes",
849
- detail: { sourcePath, sourceKey }
850
- };
820
+ text("scriptable-pack-fingerprint/2:");
821
+ visit(value);
822
+ return `sha256:${bytesToHex(hash.digest())}`;
851
823
  }
852
- function outputValueError(sourcePath, sourceKey, expected, actual) {
824
+ function failure(sourceKey, expected, actual) {
853
825
  return {
854
- code: "pack-parameter-invalid",
826
+ code: "mesh-bin-payload-invalid",
827
+ subject: "mesh-bin",
828
+ sourceKey,
855
829
  expected,
856
- hint: "repair the build output and rebuild the Pack from a fresh generation",
857
- detail: { sourcePath, sourceKey, actual: typeof actual === "string" ? actual : typeof actual }
830
+ actual,
831
+ recovery: "re-cook the source with its Meta sidecar through the build-time importer"
858
832
  };
859
833
  }
860
- function referenceError(code, sourcePath, guids) {
861
- return {
862
- code,
863
- expected: code === "pack-output-reference-missing" ? "every output reference to resolve to a local or published AssetGuid" : "removed output GUIDs to have no incoming references",
864
- hint: code === "pack-output-reference-missing" ? "build or publish the referenced Pack before verifying this output" : "migrate incoming references before publishing the topology change",
865
- detail: { sourcePath, guids: [...guids].sort() }
866
- };
834
+ function asAttributeMap(value) {
835
+ return value ?? {};
867
836
  }
868
- function productError(sourcePath, sourceKey, producer, product) {
869
- if (!record(product)) {
870
- return outputValueError(
871
- sourcePath,
872
- sourceKey,
873
- `producer ${producer.kind} to return an asset product object`,
874
- product
875
- );
876
- }
877
- if (!Array.isArray(product.refs)) {
878
- return outputValueError(
879
- sourcePath,
880
- sourceKey,
881
- `producer ${producer.kind} to return a refs array`,
882
- product.refs
883
- );
884
- }
885
- if (!record(product.artifacts)) {
886
- return outputValueError(
887
- sourcePath,
888
- sourceKey,
889
- `producer ${producer.kind} to return an artifacts object`,
890
- product.artifacts
891
- );
892
- }
893
- const payload = product.payload;
894
- if (!record(payload) || payload.kind !== producer.kind) {
895
- return outputValueError(
896
- sourcePath,
897
- sourceKey,
898
- `producer ${producer.kind} to return a matching payload kind`,
899
- payload
837
+ function jsonValue(value) {
838
+ if (value instanceof Float32Array || value instanceof Uint16Array) return Array.from(value);
839
+ if (Array.isArray(value)) return value.map(jsonValue);
840
+ if (value !== null && typeof value === "object") {
841
+ return Object.fromEntries(
842
+ Object.entries(value).map(([key, nested]) => [key, jsonValue(nested)])
900
843
  );
901
844
  }
902
- for (const ref2 of product.refs) {
903
- if (!record(ref2) || typeof ref2.guid !== "string" || !AssetGuid.parse(ref2.guid).ok) {
904
- return outputValueError(
905
- sourcePath,
906
- sourceKey,
907
- "producer refs to contain valid AssetGuid values",
908
- ref2
909
- );
845
+ return value;
846
+ }
847
+ function refsMeta(payload, refs) {
848
+ const materialSlots = (payload.materialSlots ?? [{ slotName: "Default" }]).map(
849
+ (slot, slotIndex) => {
850
+ const defaultMaterial = slot.defaultMaterial;
851
+ let defaultMaterialRef;
852
+ if (defaultMaterial !== void 0) {
853
+ const guid = AssetGuid$1.format(defaultMaterial);
854
+ defaultMaterialRef = refs.findIndex((candidate) => candidate.toLowerCase() === guid);
855
+ if (defaultMaterialRef < 0) {
856
+ throw new Error(
857
+ `material slot ${slotIndex} default material ${guid} is absent from refs`
858
+ );
859
+ }
860
+ }
861
+ return {
862
+ slotName: slot.slotName,
863
+ ...slot.sourceKey === void 0 ? {} : { sourceKey: slot.sourceKey },
864
+ ...defaultMaterialRef === void 0 ? {} : { defaultMaterialRef }
865
+ };
910
866
  }
867
+ );
868
+ if (payload.lods !== void 0 && payload.lods.length > 7) {
869
+ throw new Error("MeshAsset LOD chain supports at most seven lower-detail levels");
911
870
  }
912
- for (const [key, artifact] of Object.entries(product.artifacts)) {
913
- if (key.length === 0 || key.startsWith("/") || key.includes("..") || key.includes("\\") || !record(artifact) || typeof artifact.mediaType !== "string" || !(artifact.bytes instanceof Uint8Array)) {
914
- return outputValueError(
915
- sourcePath,
916
- sourceKey,
917
- "asset-local artifacts with safe keys and bytes",
918
- key
871
+ let previousCoverage = 1;
872
+ const seenLodGuids = /* @__PURE__ */ new Set();
873
+ const lods = payload.lods?.map((lod, lodIndex) => {
874
+ const guid = AssetGuid$1.format(lod.mesh).toLowerCase();
875
+ const meshRef = refs.findIndex((candidate) => candidate.toLowerCase() === guid);
876
+ if (meshRef < 0) {
877
+ throw new Error(`LOD ${lodIndex} mesh ${guid} is absent from refs`);
878
+ }
879
+ if (seenLodGuids.has(guid)) {
880
+ throw new Error(`LOD ${lodIndex} mesh ${guid} is duplicated`);
881
+ }
882
+ if (!Number.isFinite(lod.screenCoverage) || lod.screenCoverage <= 0 || lod.screenCoverage > 1 || lod.screenCoverage >= previousCoverage) {
883
+ throw new Error(
884
+ `LOD ${lodIndex} screenCoverage must be finite, in (0, 1], and strictly decreasing`
919
885
  );
920
886
  }
887
+ seenLodGuids.add(guid);
888
+ previousCoverage = lod.screenCoverage;
889
+ return { meshRef, screenCoverage: lod.screenCoverage };
890
+ });
891
+ if (payload.lodHysteresis !== void 0 && (!Number.isFinite(payload.lodHysteresis) || payload.lodHysteresis < 0 || payload.lodHysteresis >= 1)) {
892
+ throw new Error("lodHysteresis must be finite and in [0, 1)");
921
893
  }
922
- return void 0;
923
- }
924
- function errorCode(value) {
925
- return record(value) && typeof value.code === "string" ? value.code : void 0;
926
- }
927
- function normalizedGuidSet(value) {
928
- return value === void 0 ? void 0 : new Set([...value].map((guid) => guid.toLowerCase()));
894
+ return {
895
+ submeshes: payload.submeshes === void 0 || payload.submeshes.length === 0 ? [{ indexOffset: 0, indexCount: payload.indices?.length ?? 0, materialSlot: 0 }] : payload.submeshes,
896
+ materialSlots,
897
+ ...payload.aabb === void 0 ? {} : { aabb: jsonValue(payload.aabb) },
898
+ ...payload.morphTargets === void 0 ? {} : { morphTargets: jsonValue(payload.morphTargets) },
899
+ ...payload.morphWeights === void 0 ? {} : { morphWeights: jsonValue(payload.morphWeights) },
900
+ ...lods === void 0 ? {} : { lods },
901
+ ...payload.lodHysteresis === void 0 ? {} : { lodHysteresis: payload.lodHysteresis }
902
+ };
929
903
  }
930
- async function buildScriptablePack(options) {
931
- const subjectPackageId = options.subjectPackageId ?? options.definition.packageId;
932
- const availableGuids = normalizedGuidSet(options.availableGuids);
933
- const observed = observedReader(options.assetSource);
934
- let effectiveValues;
935
- if ("parameters" in options.definition) {
936
- const resolved = resolvePackParameterValues(
937
- options.definition,
938
- options.values ?? {},
939
- options.inheritedValues
940
- );
941
- if (!resolved.ok) return resolved;
942
- effectiveValues = resolved.value;
943
- } else if (options.values !== void 0 && Object.keys(options.values).length > 0) {
944
- return err(
945
- outputValueError(
946
- options.sourcePath,
947
- "$.values",
948
- "zero-parameter Packs to omit values and instance capabilities",
949
- options.values
950
- )
951
- );
952
- } else if (options.subjectPackageId !== void 0 && PackageId.format(options.subjectPackageId).toLowerCase() !== PackageId.format(options.definition.packageId).toLowerCase()) {
953
- return err({
954
- code: "pack-parent-has-no-parameters",
955
- expected: "a ScriptablePack source with parameters for an independent instance packageId",
956
- hint: "use clone for a zero-parameter Pack instead of building it as an instance",
957
- detail: {
958
- sourcePath: options.sourcePath,
959
- rootPackageId: PackageId.format(options.definition.packageId),
960
- subjectPackageId: PackageId.format(options.subjectPackageId)
961
- }
962
- });
963
- }
964
- let built;
904
+ function packMeshBinV4(payload, sourceKey, refs = []) {
965
905
  try {
966
- const context = buildContext(subjectPackageId, effectiveValues, observed.reader);
967
- built = options.definition.build(context);
968
- built = await built;
969
- } catch (cause) {
970
- return err(
971
- new ImportError({
972
- code: "import-internal-error",
973
- expected: "Pack build to return a structured Result without throwing",
974
- hint: "repair the authoring function and return err(...) for expected failures",
975
- detail: {
976
- reason: `${options.sourcePath}: ${cause instanceof Error ? cause.message : String(cause)}`
977
- }
978
- })
979
- );
980
- }
981
- if (!record(built) || typeof built.ok !== "boolean") {
982
- return err(
983
- new ImportError({
984
- code: "import-internal-error",
985
- expected: "Pack build to return a Result object",
986
- hint: "return ok(sourceKeyToAsset) or err(structuredError) from the authoring function",
987
- detail: { reason: `${options.sourcePath}: malformed build result` }
988
- })
989
- );
990
- }
991
- if (!built.ok) {
992
- if (errorCode(built.error) !== void 0) return err(built.error);
993
- return err(
994
- new ImportError({
995
- code: "import-internal-error",
996
- expected: "a structured Pack build error",
997
- hint: "return an error carrying code, expected, hint and detail",
998
- detail: { reason: `${options.sourcePath}: ${String(built.error)}` }
999
- })
1000
- );
1001
- }
1002
- if (!record(built.value)) {
1003
- return err(
1004
- outputValueError(
1005
- options.sourcePath,
1006
- "$",
1007
- "build to return a sourceKey-to-Asset object",
1008
- built.value
1009
- )
1010
- );
1011
- }
1012
- const imported = [];
1013
- const stagedOutputs = [];
1014
- const localGuids = /* @__PURE__ */ new Set();
1015
- const materialCookerRegistry = new NativeCookerRegistry();
1016
- for (const cooker of options.cookers ?? []) materialCookerRegistry.register(cooker);
1017
- const nativeFingerprints = /* @__PURE__ */ new Map();
1018
- for (const sourceKey of Object.keys(built.value).sort()) {
1019
- if (!isValidPackSourceKey(sourceKey))
1020
- return err(sourceKeyFailure(options.sourcePath, sourceKey));
1021
- const asset = built.value[sourceKey];
1022
- if (!isAsset(asset)) {
906
+ const vertices = payload.vertices;
907
+ const indices = payload.indices;
908
+ if (!(vertices instanceof Float32Array)) {
1023
909
  return err(
1024
- outputValueError(
1025
- options.sourcePath,
1026
- sourceKey,
1027
- "a concrete Asset with a supported kind",
1028
- asset
1029
- )
910
+ failure(sourceKey, "Float32Array interleaved vertices", "vertices is not Float32Array")
1030
911
  );
1031
912
  }
1032
- const guid = AssetGuid.format(AssetGuid.derive(subjectPackageId, sourceKey));
1033
- const normalizedGuid = guid.toLowerCase();
1034
- if (localGuids.has(normalizedGuid) || availableGuids?.has(normalizedGuid)) {
1035
- return err({
1036
- code: "pack-guid-collision",
1037
- expected: "derived output GUIDs to be unique in the global source index",
1038
- hint: "change the colliding packageId or repair the source index before publishing",
1039
- detail: { sourcePath: options.sourcePath, guid }
1040
- });
913
+ if (indices !== void 0 && !(indices instanceof Uint16Array || indices instanceof Uint32Array)) {
914
+ return err(
915
+ failure(sourceKey, "Uint16Array or Uint32Array indices", "indices has an unsupported type")
916
+ );
1041
917
  }
1042
- localGuids.add(normalizedGuid);
1043
- const producer = options.outputs.get(asset.kind);
1044
- if (producer === void 0) {
918
+ const attributes = asAttributeMap(payload.attributes);
919
+ const projection = deriveVertexLayoutProjection(attributes);
920
+ if (projection.attributes.length === 0 || projection.arrayStride === 0) {
1045
921
  return err(
1046
- outputValueError(
1047
- options.sourcePath,
922
+ failure(
1048
923
  sourceKey,
1049
- `a registered output producer for ${asset.kind}`,
1050
- asset.kind
924
+ "a non-empty canonical geometry projection",
925
+ "projection has no attributes"
1051
926
  )
1052
927
  );
1053
928
  }
1054
- let produced;
1055
- try {
1056
- produced = await producer.produce({ guid, sourceKey, asset });
1057
- } catch (cause) {
929
+ const vertexCount = payload.vertexCount ?? vertices.byteLength / projection.arrayStride;
930
+ if (!Number.isSafeInteger(vertexCount) || vertexCount < 0) {
1058
931
  return err(
1059
- new ImportError({
1060
- code: "import-internal-error",
1061
- expected: `producer ${producer.kind} to return a structured Result without throwing`,
1062
- hint: "repair the output producer and return err(...) for expected failures",
1063
- detail: {
1064
- reason: `${options.sourcePath}:${sourceKey}: ${cause instanceof Error ? cause.message : String(cause)}`
1065
- }
1066
- })
932
+ failure(sourceKey, "a non-negative safe vertex cardinality", `vertexCount=${vertexCount}`)
1067
933
  );
1068
934
  }
1069
- if (!record(produced) || typeof produced.ok !== "boolean") {
935
+ if (vertices.byteLength !== vertexCount * projection.arrayStride) {
1070
936
  return err(
1071
- outputValueError(
1072
- options.sourcePath,
937
+ failure(
1073
938
  sourceKey,
1074
- `producer ${producer.kind} to return a Result`,
1075
- produced
939
+ `vertices.byteLength=${vertexCount * projection.arrayStride}`,
940
+ `vertices.byteLength=${vertices.byteLength}; stride=${projection.arrayStride}`
1076
941
  )
1077
942
  );
1078
943
  }
1079
- if (!produced.ok) {
1080
- if (errorCode(produced.error) !== void 0) return err(produced.error);
1081
- return err(
1082
- new ImportError({
1083
- code: "import-internal-error",
1084
- expected: `producer ${producer.kind} to return a structured error`,
1085
- hint: "return an error carrying code, expected, hint and detail",
1086
- detail: { reason: `${options.sourcePath}:${sourceKey}: ${String(produced.error)}` }
1087
- })
1088
- );
1089
- }
1090
- const invalidProduct2 = productError(options.sourcePath, sourceKey, producer, produced.value);
1091
- if (invalidProduct2 !== void 0) return err(invalidProduct2);
1092
- const product2 = produced.value;
1093
- let payload = product2.payload;
1094
- let artifacts2 = product2.artifacts;
1095
- if (needsMaterialCook(asset) && materialCookerRegistry.get("material") !== void 0) {
1096
- const cooked = await materialCookerRegistry.runDraft("material", {
1097
- guid,
1098
- source: asset,
1099
- sourceKey,
1100
- sourcePath: options.sourcePath,
1101
- refs: product2.refs.map((reference) => reference.guid)
1102
- });
1103
- if (!cooked.ok) return err(cooked.error);
1104
- if (cooked.value.guid.toLowerCase() !== normalizedGuid) {
1105
- return err({
1106
- code: "pack-source-output-invalid",
1107
- expected: "the authored material cooker to preserve the derived AssetGuid",
1108
- hint: "repair the native material cooker output GUID and rebuild the Pack",
1109
- detail: {
1110
- sourcePath: options.sourcePath,
944
+ for (const attribute of projection.attributes) {
945
+ const value = attributes[attribute.key];
946
+ const components = attribute.byteLength / (attribute.format === "uint16x4" ? 2 : 4);
947
+ if (value === void 0 || !(value instanceof Float32Array) && !(value instanceof Uint16Array) || value.length !== vertexCount * components) {
948
+ return err(
949
+ failure(
1111
950
  sourceKey,
1112
- expectedGuid: guid,
1113
- actualGuid: cooked.value.guid
951
+ `${attribute.key} cardinality=${vertexCount * components}`,
952
+ `${attribute.key} cardinality=${value?.byteLength ?? "missing"}`
953
+ )
954
+ );
955
+ }
956
+ }
957
+ const interleaved = new Uint8Array(vertexCount * projection.arrayStride);
958
+ const interleavedView = new DataView(interleaved.buffer);
959
+ for (const attribute of projection.attributes) {
960
+ const value = attributes[attribute.key];
961
+ if (value === void 0) continue;
962
+ const components = attribute.byteLength / (attribute.format === "uint16x4" ? 2 : 4);
963
+ for (let vertex = 0; vertex < vertexCount; vertex++) {
964
+ for (let component = 0; component < components; component++) {
965
+ const sourceIndex = vertex * components + component;
966
+ const targetOffset = vertex * projection.arrayStride + attribute.offset + component * (attribute.format === "uint16x4" ? 2 : 4);
967
+ if (attribute.format === "uint16x4") {
968
+ interleavedView.setUint16(targetOffset, value[sourceIndex] ?? 0, true);
969
+ } else {
970
+ interleavedView.setFloat32(
971
+ targetOffset,
972
+ value[sourceIndex] ?? 0,
973
+ true
974
+ );
1114
975
  }
1115
- });
976
+ }
1116
977
  }
1117
- payload = cooked.value.payload;
1118
- artifacts2 = cooked.value.artifacts;
1119
- nativeFingerprints.set(sourceKey, cooked.value.inputFingerprint);
1120
978
  }
1121
- imported.push({
1122
- guid,
1123
- kind: asset.kind,
1124
- payload,
1125
- refs: product2.refs,
1126
- artifacts: artifacts2
1127
- });
1128
- stagedOutputs.push({
1129
- guid: AssetGuid.derive(subjectPackageId, sourceKey),
1130
- sourceKey,
1131
- asset: clone(asset),
1132
- digest: await digest(asset)
1133
- });
979
+ const indexCount = indices?.length ?? 0;
980
+ const indexWidth = indices === void 0 || indexCount === 0 ? 0 : indices.BYTES_PER_ELEMENT;
981
+ const indexBytes = indexCount * indexWidth;
982
+ if (!Number.isSafeInteger(indexBytes) || indexBytes > 4294967295) {
983
+ return err(failure(sourceKey, "safe index payload byte length", `indexBytes=${indexBytes}`));
984
+ }
985
+ const meta = new TextEncoder().encode(JSON.stringify(refsMeta(payload, refs)));
986
+ const header = {
987
+ version: 4,
988
+ projectionVersion: projection.schemaVersion,
989
+ mask: projection.mask,
990
+ digest: projection.digest,
991
+ stride: projection.arrayStride,
992
+ vertexCount,
993
+ vertexBytes: interleaved.byteLength,
994
+ indexCount,
995
+ indexWidth,
996
+ indexBytes,
997
+ jsonBytes: meta.byteLength
998
+ };
999
+ const total = MESH_BIN_HEADER_V4_BYTES + interleaved.byteLength + indexBytes + meta.byteLength;
1000
+ if (!Number.isSafeInteger(total) || total > 4294967295) {
1001
+ return err(failure(sourceKey, "safe mesh binary byte length", `total=${total}`));
1002
+ }
1003
+ const out = new Uint8Array(total);
1004
+ writeMeshBinHeader(header, out);
1005
+ let offset = MESH_BIN_HEADER_V4_BYTES;
1006
+ out.set(interleaved, offset);
1007
+ offset += interleaved.byteLength;
1008
+ if (indices !== void 0 && indexBytes > 0) {
1009
+ out.set(new Uint8Array(indices.buffer, indices.byteOffset, indices.byteLength), offset);
1010
+ offset += indexBytes;
1011
+ }
1012
+ out.set(meta, offset);
1013
+ return ok(out);
1014
+ } catch (error) {
1015
+ return err(
1016
+ failure(
1017
+ sourceKey,
1018
+ "valid canonical mesh payload",
1019
+ error instanceof Error ? error.message : String(error)
1020
+ )
1021
+ );
1134
1022
  }
1135
- const referenced = /* @__PURE__ */ new Set();
1136
- for (const asset of imported) {
1137
- for (const ref2 of asset.refs) {
1138
- const guid = ref2.guid.toLowerCase();
1139
- if (!localGuids.has(guid)) referenced.add(guid);
1023
+ }
1024
+
1025
+ // src/scriptable-pack.ts
1026
+ var AssetOutputProducerRegistry = class {
1027
+ producers = /* @__PURE__ */ new Map();
1028
+ register(producer) {
1029
+ if (producer.kind.trim().length === 0 || producer.version.trim().length === 0) {
1030
+ throw new TypeError("Pack output producer kind and version must be non-empty");
1031
+ }
1032
+ if (typeof producer.produce !== "function") {
1033
+ throw new TypeError(`Pack output producer ${producer.kind} must expose produce`);
1140
1034
  }
1035
+ this.producers.set(producer.kind, producer);
1141
1036
  }
1142
- if (options.deferReferenceValidation !== true) {
1143
- const missing = [...referenced].filter(
1144
- (guid) => availableGuids !== void 0 && !availableGuids.has(guid)
1037
+ get(kind) {
1038
+ return this.producers.get(kind);
1039
+ }
1040
+ versions() {
1041
+ return Object.fromEntries(
1042
+ [...this.producers.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([kind, producer]) => [kind, producer.version])
1145
1043
  );
1146
- if (missing.length > 0)
1147
- return err(referenceError("pack-output-reference-missing", options.sourcePath, missing));
1148
1044
  }
1149
- const externalEvidence = [...observed.reads.values()].sort((left, right) => left.guid.localeCompare(right.guid)).map(
1150
- (read) => ({
1151
- guid: read.guid,
1152
- usage: referenced.has(read.guid) ? "both" : "content",
1153
- generation: read.generation,
1154
- digest: read.digest
1155
- })
1156
- );
1157
- const authoredInputFingerprint = await digest({
1158
- packageId: PackageId.format(subjectPackageId),
1159
- sourcePath: options.sourcePath,
1160
- sourceClosure: options.sourceClosure ?? [],
1161
- values: effectiveValues,
1162
- externalEvidence: externalEvidence.map(({ guid, usage, digest: evidenceDigest }) => ({
1163
- guid,
1164
- usage,
1165
- digest: evidenceDigest
1166
- })),
1167
- authoringContractVersion: options.authoringContractVersion ?? "scriptable-pack/1",
1168
- producerVersions: options.outputs.versions()
1169
- });
1170
- const inputFingerprint = nativeFingerprints.size === 0 ? authoredInputFingerprint : await digest({
1171
- sourceRevision: authoredInputFingerprint,
1172
- nativeCookers: [...nativeFingerprints.entries()].sort(
1173
- ([left], [right]) => left.localeCompare(right)
1174
- )
1175
- });
1176
- const refs = imported.flatMap((asset) => asset.refs);
1177
- const artifacts = Object.fromEntries(
1178
- imported.flatMap(
1179
- (asset) => Object.entries(asset.artifacts).map(([key, artifact]) => [`${asset.guid}/${key}`, artifact])
1180
- )
1181
- );
1182
- const product = createImportProduct({
1183
- assets: imported,
1184
- sourceDependencies: (options.sourceClosure ?? []).map((entry) => entry.path),
1185
- refs,
1186
- artifacts,
1187
- receipts: imported.map((asset) => ({
1188
- guid: asset.guid,
1189
- origin: "authoredPack",
1190
- status: "succeeded",
1191
- inputFingerprint
1192
- })),
1193
- diagnostics: [],
1194
- sourceRevision: inputFingerprint,
1195
- sourceKey: options.sourcePath
1196
- });
1197
- if (!product.ok) return err(product.error);
1198
- return ok({
1199
- product: product.value,
1200
- stagedOutputs,
1201
- externalEvidence,
1202
- inputFingerprint,
1203
- ...options.publication === void 0 ? {} : { publication: options.publication }
1045
+ };
1046
+
1047
+ // src/scriptable-pack-output-producers.ts
1048
+ function producerError(input, reason) {
1049
+ return new ImportError({
1050
+ code: "import-internal-error",
1051
+ expected: `ScriptablePack ${input.asset.kind} output ${input.sourceKey} to satisfy its domain producer contract`,
1052
+ hint: "fix the generated Asset payload and rebuild the ScriptablePack",
1053
+ detail: { reason: reason instanceof Error ? reason.message : String(reason) }
1204
1054
  });
1205
1055
  }
1206
- function stagedSource(staged, fallback) {
1056
+ function formatGuid(value) {
1057
+ if (typeof value === "string") {
1058
+ const parsed = AssetGuid$1.parse(value);
1059
+ if (!parsed.ok) throw parsed.error;
1060
+ return AssetGuid$1.format(parsed.value);
1061
+ }
1062
+ return AssetGuid$1.format(value);
1063
+ }
1064
+ function canonical(value) {
1065
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
1066
+ if (value !== null && typeof value === "object") {
1067
+ return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => `${JSON.stringify(key)}:${canonical(child)}`).join(",")}}`;
1068
+ }
1069
+ return JSON.stringify(value) ?? "null";
1070
+ }
1071
+ function particleProgramArtifact(effect) {
1072
+ const artifactProgram = { format: effect.program.format, emitters: effect.program.emitters };
1073
+ const bytes2 = new TextEncoder().encode(canonical(artifactProgram));
1074
+ const fingerprint = `sha256:${bytesToHex(sha256(bytes2))}`;
1207
1075
  return {
1208
- async readByGuid(guid) {
1209
- const key = AssetGuid.format(guid).toLowerCase();
1210
- const local = staged.get(key);
1211
- if (local !== void 0) {
1212
- return ok({
1213
- asset: clone(local.asset),
1214
- generation: 1,
1215
- digest: local.digest ?? "sha256:staged"
1216
- });
1217
- }
1218
- if (fallback === void 0) {
1219
- return err({
1220
- code: "asset-not-found",
1221
- expected: "a staged or published content dependency",
1222
- hint: "wait for the dependency subject to materialize",
1223
- detail: { guid: key }
1224
- });
1225
- }
1226
- return fallback.readByGuid(guid);
1227
- }
1076
+ program: { ...effect.program, fingerprint },
1077
+ bytes: bytes2,
1078
+ fingerprint
1228
1079
  };
1229
1080
  }
1230
- async function buildScriptablePackWorklist(options) {
1231
- const orderedSubjects = [...options.subjects].sort(
1232
- (left, right) => left.sourcePath.localeCompare(right.sourcePath)
1233
- );
1234
- const pending = new Map(
1235
- orderedSubjects.map((subject, index) => [`${index}:${subject.sourcePath}`, subject])
1236
- );
1237
- const staged = /* @__PURE__ */ new Map();
1238
- const results = /* @__PURE__ */ new Map();
1239
- const availableGuids = /* @__PURE__ */ new Set([
1240
- ...normalizedGuidSet(options.availableGuids) ?? [],
1241
- ...BUILTIN_MESH_ASSETS.map((asset) => asset.guid.toLowerCase())
1242
- ]);
1243
- const maxPasses = options.maxPasses ?? Math.max(1, options.subjects.length + 1);
1244
- let iterations = 0;
1245
- for (; iterations < maxPasses && pending.size > 0; iterations += 1) {
1246
- let progress = false;
1247
- const waiting = /* @__PURE__ */ new Set();
1248
- for (const [key, subject] of pending) {
1249
- const result = await buildScriptablePack({
1250
- definition: subject.definition,
1251
- sourcePath: subject.sourcePath,
1252
- ...subject.subjectPackageId === void 0 ? {} : { subjectPackageId: subject.subjectPackageId },
1253
- ...subject.values === void 0 ? {} : { values: subject.values },
1254
- ...subject.inheritedValues === void 0 ? {} : { inheritedValues: subject.inheritedValues },
1255
- ...subject.sourceClosure === void 0 ? {} : { sourceClosure: subject.sourceClosure },
1256
- outputs: options.outputs,
1257
- ...options.cookers === void 0 ? {} : { cookers: options.cookers },
1258
- assetSource: stagedSource(staged, options.assetSource),
1259
- availableGuids: /* @__PURE__ */ new Set([...availableGuids, ...staged.keys()]),
1260
- deferReferenceValidation: true
1261
- });
1262
- if (result.ok) {
1263
- results.set(key, result.value);
1264
- for (const output of result.value.stagedOutputs)
1265
- staged.set(AssetGuid.format(output.guid).toLowerCase(), output);
1266
- pending.delete(key);
1267
- progress = true;
1268
- continue;
1269
- }
1270
- if (errorCode(result.error) === "asset-not-found") {
1271
- const detail = record(result.error) && record(result.error.detail) ? result.error.detail : void 0;
1272
- const guid = detail !== void 0 && typeof detail.guid === "string" ? detail.guid : void 0;
1273
- if (guid !== void 0) waiting.add(guid.toLowerCase());
1274
- continue;
1275
- }
1276
- return result;
1081
+ function materialProduct(input) {
1082
+ if (input.asset.kind !== "material") throw new TypeError("expected MaterialAsset");
1083
+ const material = input.asset;
1084
+ const refs = [];
1085
+ const addRef = (guid, sourceField) => {
1086
+ refs.push({ guid: formatGuid(guid), sourceField });
1087
+ return refs.length - 1;
1088
+ };
1089
+ const addMaterialRef = (value, sourceField) => {
1090
+ if (typeof value === "number") {
1091
+ throw new TypeError("material texture references must be GUIDs, not runtime handles");
1277
1092
  }
1278
- if (pending.size === 0) break;
1279
- if (!progress) {
1280
- return err({
1281
- code: "pack-content-dependency-stalled",
1282
- expected: "the content dependency worklist to make progress",
1283
- hint: "inspect waitingGuids and repair the missing output or content-read cycle, then rebuild",
1284
- detail: {
1285
- waitingGuids: [...waiting].sort(),
1286
- pendingSubjects: [...pending.values()].map((subject) => subject.sourcePath).sort(),
1287
- iterations: iterations + 1
1093
+ return addRef(value, sourceField);
1094
+ };
1095
+ const textureFields = material.parameters === void 0 ? new Set(MATERIAL_TEXTURE_SLOTS) : new Set(
1096
+ material.parameters.filter(
1097
+ (parameter) => parameter.type === "texture" || parameter.type === "texture_cube"
1098
+ ).map((parameter) => parameter.name)
1099
+ );
1100
+ const parent = material.parent === void 0 ? void 0 : addRef(material.parent, { fieldName: "parent" });
1101
+ const values = {};
1102
+ for (const fieldName of Object.keys(material.values ?? {}).sort()) {
1103
+ const value = material.values?.[fieldName];
1104
+ if (typeof value === "string" && textureFields.has(fieldName)) {
1105
+ values[fieldName] = {
1106
+ texture: addMaterialRef(value, { componentName: "<material>", fieldName })
1107
+ };
1108
+ } else if (value !== null && typeof value === "object" && !Array.isArray(value) && "texture" in value) {
1109
+ const texture = value;
1110
+ values[fieldName] = {
1111
+ ...texture,
1112
+ texture: addMaterialRef(texture.texture, { componentName: "<material>", fieldName }),
1113
+ ...texture.sampler === void 0 ? {} : {
1114
+ sampler: addMaterialRef(texture.sampler, {
1115
+ componentName: "<material>",
1116
+ fieldName: `${fieldName}.sampler`
1117
+ })
1288
1118
  }
1289
- });
1119
+ };
1120
+ } else {
1121
+ values[fieldName] = value;
1290
1122
  }
1291
1123
  }
1292
- if (pending.size > 0) {
1293
- return err({
1294
- code: "pack-content-dependency-stalled",
1295
- expected: "the content dependency worklist to finish within its bounded retry budget",
1296
- hint: "inspect pendingSubjects and waitingGuids, then repair the dependency graph",
1297
- detail: {
1298
- pendingSubjects: [...pending.values()].map((subject) => subject.sourcePath).sort(),
1299
- waitingGuids: [],
1300
- iterations
1301
- }
1124
+ const { parent: _parent, values: _values, ...materialShape } = material;
1125
+ return {
1126
+ payload: {
1127
+ ...materialShape,
1128
+ ...parent === void 0 ? {} : { parent },
1129
+ ...material.values === void 0 ? {} : { values }
1130
+ },
1131
+ refs,
1132
+ artifacts: {}
1133
+ };
1134
+ }
1135
+ function meshProduct(input) {
1136
+ if (input.asset.kind !== "mesh") throw new TypeError("expected MeshAsset");
1137
+ const mesh = input.asset;
1138
+ const refs = [];
1139
+ for (let slotIndex = 0; slotIndex < mesh.materialSlots.length; slotIndex++) {
1140
+ const defaultMaterial = mesh.materialSlots[slotIndex]?.defaultMaterial;
1141
+ if (defaultMaterial === void 0) continue;
1142
+ refs.push({
1143
+ guid: formatGuid(defaultMaterial),
1144
+ sourceField: { fieldName: "materialSlots", arrayIndex: slotIndex }
1302
1145
  });
1303
1146
  }
1304
- const knownGuids = /* @__PURE__ */ new Set([...availableGuids, ...staged.keys()]);
1305
- const missingReferences = /* @__PURE__ */ new Set();
1306
- for (const product of results.values()) {
1307
- const contentReads = new Set(product.externalEvidence.map((read) => read.guid.toLowerCase()));
1308
- for (const asset of product.product.assets) {
1309
- for (const reference of asset.refs) {
1310
- const guid = reference.guid.toLowerCase();
1311
- if (!knownGuids.has(guid) && !contentReads.has(guid)) missingReferences.add(guid);
1312
- }
1313
- }
1314
- }
1315
- if (missingReferences.size > 0) {
1316
- return err(
1317
- referenceError("pack-output-reference-missing", "worklist", [...missingReferences].sort())
1318
- );
1147
+ const seenRefs = new Set(refs.map((reference) => reference.guid.toLowerCase()));
1148
+ for (const [lodIndex, lod] of (mesh.lods ?? []).entries()) {
1149
+ const guid = formatGuid(lod.mesh);
1150
+ if (seenRefs.has(guid.toLowerCase())) continue;
1151
+ seenRefs.add(guid.toLowerCase());
1152
+ refs.push({ guid, sourceField: { fieldName: "lods", arrayIndex: lodIndex } });
1319
1153
  }
1320
- const removed = [...options.incomingRefs?.keys() ?? []].filter(
1321
- (guid) => !staged.has(guid.toLowerCase())
1322
- );
1323
- const referencedRemoved = removed.filter(
1324
- (guid) => (options.incomingRefs?.get(guid) ?? []).length > 0
1325
- );
1326
- if (referencedRemoved.length > 0)
1327
- return err(referenceError("pack-output-reference-conflict", "worklist", referencedRemoved));
1328
- return ok({
1329
- products: [...results.values()].map((result) => result.product),
1330
- buildProducts: [...results.values()],
1331
- stagedOutputs: [...staged.values()],
1332
- iterations: pending.size === 0 && options.subjects.length > 0 ? iterations + 1 : iterations
1333
- });
1334
- }
1335
- function failure(sourceKey, expected, actual) {
1336
1154
  return {
1337
- code: "mesh-bin-payload-invalid",
1338
- subject: "mesh-bin",
1339
- sourceKey,
1340
- expected,
1341
- actual,
1342
- recovery: "re-cook the source with its Meta sidecar through the build-time importer"
1155
+ payload: mesh,
1156
+ refs,
1157
+ artifacts: {
1158
+ body: {
1159
+ mediaType: "application/x-forgeax-mesh",
1160
+ assetCodec: { name: "mesh-binary", version: "4" },
1161
+ bytes: (() => {
1162
+ const packed = packMeshBinV4(
1163
+ mesh,
1164
+ input.sourceKey,
1165
+ refs.map((reference) => reference.guid)
1166
+ );
1167
+ if (!packed.ok) {
1168
+ throw packed.error;
1169
+ }
1170
+ return packed.value;
1171
+ })()
1172
+ }
1173
+ }
1343
1174
  };
1344
1175
  }
1345
- function asAttributeMap(value) {
1346
- return value ?? {};
1176
+ function sceneProduct(input, components) {
1177
+ if (input.asset.kind !== "scene") throw new TypeError("expected SceneAsset");
1178
+ const externalized = externalizeSceneAsset(input.asset, (componentName) => {
1179
+ const schema = components.get(componentName);
1180
+ if (schema === void 0) {
1181
+ throw new TypeError(
1182
+ `ScriptablePack scene component ${componentName} is missing from sceneComponents`
1183
+ );
1184
+ }
1185
+ return schema;
1186
+ });
1187
+ if (!externalized.ok) throw new TypeError(`scene field ${externalized.error.field} is invalid`);
1188
+ return {
1189
+ payload: { kind: "scene", ...externalized.value.payload },
1190
+ refs: externalized.value.refs,
1191
+ artifacts: {}
1192
+ };
1347
1193
  }
1348
- function jsonValue(value) {
1349
- if (value instanceof Float32Array || value instanceof Uint16Array) return Array.from(value);
1350
- if (Array.isArray(value)) return value.map(jsonValue);
1194
+ function containsAssetGuid(value) {
1195
+ if (typeof value === "string") return AssetGuid$1.parse(value).ok;
1196
+ if (Array.isArray(value)) return value.some(containsAssetGuid);
1351
1197
  if (value !== null && typeof value === "object") {
1352
- return Object.fromEntries(
1353
- Object.entries(value).map(([key, nested]) => [key, jsonValue(nested)])
1354
- );
1198
+ return Object.values(value).some(containsAssetGuid);
1355
1199
  }
1356
- return value;
1200
+ return false;
1357
1201
  }
1358
- function refsMeta(payload, refs) {
1359
- const materialSlots = (payload.materialSlots ?? [{ slotName: "Default" }]).map(
1360
- (slot, slotIndex) => {
1361
- const defaultMaterial = slot.defaultMaterial;
1362
- let defaultMaterialRef;
1363
- if (defaultMaterial !== void 0) {
1364
- const guid = AssetGuid$1.format(defaultMaterial);
1365
- defaultMaterialRef = refs.findIndex((candidate) => candidate.toLowerCase() === guid);
1366
- if (defaultMaterialRef < 0) {
1367
- throw new Error(
1368
- `material slot ${slotIndex} default material ${guid} is absent from refs`
1369
- );
1370
- }
1371
- }
1372
- return {
1373
- slotName: slot.slotName,
1374
- ...slot.sourceKey === void 0 ? {} : { sourceKey: slot.sourceKey },
1375
- ...defaultMaterialRef === void 0 ? {} : { defaultMaterialRef }
1376
- };
1377
- }
1378
- );
1379
- if (payload.lods !== void 0 && payload.lods.length > 7) {
1380
- throw new Error("MeshAsset LOD chain supports at most seven lower-detail levels");
1202
+ function preExternalizedSceneProduct(input) {
1203
+ if (input.asset.kind !== "scene") throw new TypeError("expected SceneAsset");
1204
+ if (containsAssetGuid(input.asset)) {
1205
+ throw new TypeError("direct scene payload must use explicit refs with runtime indices");
1381
1206
  }
1382
- let previousCoverage = 1;
1383
- const seenLodGuids = /* @__PURE__ */ new Set();
1384
- const lods = payload.lods?.map((lod, lodIndex) => {
1385
- const guid = AssetGuid$1.format(lod.mesh).toLowerCase();
1386
- const meshRef = refs.findIndex((candidate) => candidate.toLowerCase() === guid);
1387
- if (meshRef < 0) {
1388
- throw new Error(`LOD ${lodIndex} mesh ${guid} is absent from refs`);
1389
- }
1390
- if (seenLodGuids.has(guid)) {
1391
- throw new Error(`LOD ${lodIndex} mesh ${guid} is duplicated`);
1392
- }
1393
- if (!Number.isFinite(lod.screenCoverage) || lod.screenCoverage <= 0 || lod.screenCoverage > 1 || lod.screenCoverage >= previousCoverage) {
1394
- throw new Error(
1395
- `LOD ${lodIndex} screenCoverage must be finite, in (0, 1], and strictly decreasing`
1396
- );
1207
+ return {
1208
+ payload: { ...input.asset, kind: "scene" },
1209
+ refs: [],
1210
+ artifacts: {}
1211
+ };
1212
+ }
1213
+ function createSafeProducer(kind, version, product) {
1214
+ return {
1215
+ kind,
1216
+ version,
1217
+ produce(input) {
1218
+ try {
1219
+ return ok(product(input));
1220
+ } catch (error) {
1221
+ return err(producerError(input, error));
1222
+ }
1397
1223
  }
1398
- seenLodGuids.add(guid);
1399
- previousCoverage = lod.screenCoverage;
1400
- return { meshRef, screenCoverage: lod.screenCoverage };
1224
+ };
1225
+ }
1226
+ function jsonArtifact(value) {
1227
+ return {
1228
+ mediaType: "application/json",
1229
+ assetCodec: { name: "forgeax-json", version: "1" },
1230
+ bytes: new TextEncoder().encode(JSON.stringify(value))
1231
+ };
1232
+ }
1233
+ function ref(guid, fieldName, arrayIndex) {
1234
+ return {
1235
+ guid: formatGuid(guid),
1236
+ sourceField: { fieldName, ...arrayIndex === void 0 ? {} : { arrayIndex } }
1237
+ };
1238
+ }
1239
+ function bytes(value) {
1240
+ return Uint8Array.from(value);
1241
+ }
1242
+ function textureProduct(input) {
1243
+ if (input.asset.kind !== "texture") throw new TypeError("expected TextureAsset");
1244
+ const texture = input.asset;
1245
+ const layout = deriveTextureLayout({
1246
+ shape: texture.shape,
1247
+ format: texture.format,
1248
+ mips: texture.mips,
1249
+ actualByteLength: texture.data.byteLength,
1250
+ order: "mip-major,image-major,row-major"
1401
1251
  });
1402
- if (payload.lodHysteresis !== void 0 && (!Number.isFinite(payload.lodHysteresis) || payload.lodHysteresis < 0 || payload.lodHysteresis >= 1)) {
1403
- throw new Error("lodHysteresis must be finite and in [0, 1)");
1252
+ if (!layout.ok) {
1253
+ const expectedBytes = layout.error.code === "texture-packing-invalid" ? layout.error.detail.expectedBytes : texture.data.byteLength;
1254
+ const actualBytes = layout.error.code === "texture-packing-invalid" ? layout.error.detail.actualBytes : texture.data.byteLength;
1255
+ throw new TypeError(
1256
+ `texture data is not canonical: expected ${expectedBytes} bytes, got ${actualBytes}`
1257
+ );
1404
1258
  }
1405
1259
  return {
1406
- submeshes: payload.submeshes === void 0 || payload.submeshes.length === 0 ? [{ indexOffset: 0, indexCount: payload.indices?.length ?? 0, materialSlot: 0 }] : payload.submeshes,
1407
- materialSlots,
1408
- ...payload.aabb === void 0 ? {} : { aabb: jsonValue(payload.aabb) },
1409
- ...payload.morphTargets === void 0 ? {} : { morphTargets: jsonValue(payload.morphTargets) },
1410
- ...payload.morphWeights === void 0 ? {} : { morphWeights: jsonValue(payload.morphWeights) },
1411
- ...lods === void 0 ? {} : { lods },
1412
- ...payload.lodHysteresis === void 0 ? {} : { lodHysteresis: payload.lodHysteresis }
1260
+ payload: texture,
1261
+ refs: [],
1262
+ artifacts: {
1263
+ body: {
1264
+ mediaType: texture.format === "r8unorm" ? "application/x-forgeax-r8" : `application/x-forgeax-${texture.format}`,
1265
+ assetCodec: { name: texture.format, version: "1" },
1266
+ bytes: bytes(texture.data)
1267
+ }
1268
+ }
1413
1269
  };
1414
1270
  }
1415
- function packMeshBinV4(payload, sourceKey, refs = []) {
1416
- try {
1417
- const vertices = payload.vertices;
1418
- const indices = payload.indices;
1419
- if (!(vertices instanceof Float32Array)) {
1420
- return err(
1421
- failure(sourceKey, "Float32Array interleaved vertices", "vertices is not Float32Array")
1422
- );
1271
+ function ordinaryPodProduct(input) {
1272
+ const asset = input.asset;
1273
+ switch (asset.kind) {
1274
+ case "texture":
1275
+ return textureProduct(input);
1276
+ case "equirect": {
1277
+ const equirect = asset;
1278
+ return {
1279
+ payload: equirect,
1280
+ refs: [],
1281
+ artifacts: {
1282
+ body: {
1283
+ mediaType: "image/raw",
1284
+ assetCodec: { name: "raw-image", version: "1" },
1285
+ bytes: bytes(equirect.data)
1286
+ }
1287
+ }
1288
+ };
1423
1289
  }
1424
- if (indices !== void 0 && !(indices instanceof Uint16Array || indices instanceof Uint32Array)) {
1425
- return err(
1426
- failure(sourceKey, "Uint16Array or Uint32Array indices", "indices has an unsupported type")
1427
- );
1290
+ case "sampler": {
1291
+ const sampler = asset;
1292
+ return { payload: sampler, refs: [], artifacts: { body: jsonArtifact(sampler) } };
1428
1293
  }
1429
- const attributes = asAttributeMap(payload.attributes);
1430
- const projection = deriveVertexLayoutProjection(attributes);
1431
- if (projection.attributes.length === 0 || projection.arrayStride === 0) {
1432
- return err(
1433
- failure(
1434
- sourceKey,
1435
- "a non-empty canonical geometry projection",
1436
- "projection has no attributes"
1437
- )
1438
- );
1294
+ case "font": {
1295
+ const font = asset;
1296
+ const atlas = ref(font.atlas, "atlas");
1297
+ const sampler = ref(font.sampler, "sampler");
1298
+ return {
1299
+ payload: {
1300
+ kind: font.kind,
1301
+ glyphs: font.glyphs,
1302
+ common: font.common,
1303
+ atlasGuid: atlas.guid,
1304
+ samplerGuid: sampler.guid
1305
+ },
1306
+ refs: [atlas, sampler],
1307
+ artifacts: { body: jsonArtifact(font) }
1308
+ };
1439
1309
  }
1440
- const vertexCount = payload.vertexCount ?? vertices.byteLength / projection.arrayStride;
1441
- if (!Number.isSafeInteger(vertexCount) || vertexCount < 0) {
1442
- return err(
1443
- failure(sourceKey, "a non-negative safe vertex cardinality", `vertexCount=${vertexCount}`)
1444
- );
1310
+ case "render-pipeline": {
1311
+ const pipeline = asset;
1312
+ return {
1313
+ payload: pipeline,
1314
+ refs: [],
1315
+ artifacts: { body: jsonArtifact(pipeline) }
1316
+ };
1445
1317
  }
1446
- if (vertices.byteLength !== vertexCount * projection.arrayStride) {
1447
- return err(
1448
- failure(
1449
- sourceKey,
1450
- `vertices.byteLength=${vertexCount * projection.arrayStride}`,
1451
- `vertices.byteLength=${vertices.byteLength}; stride=${projection.arrayStride}`
1452
- )
1453
- );
1318
+ case "tileset": {
1319
+ const tileset = asset;
1320
+ const refs = tileset.atlases.map((atlas, index) => {
1321
+ const parsed = AssetGuid$1.parse(atlas);
1322
+ if (!parsed.ok) throw parsed.error;
1323
+ return ref(parsed.value, "atlases", index);
1324
+ });
1325
+ return {
1326
+ payload: { ...tileset, atlases: refs.map((_entry, index) => index) },
1327
+ refs,
1328
+ artifacts: { body: jsonArtifact(tileset) }
1329
+ };
1454
1330
  }
1455
- for (const attribute of projection.attributes) {
1456
- const value = attributes[attribute.key];
1457
- const components = attribute.byteLength / (attribute.format === "uint16x4" ? 2 : 4);
1458
- if (value === void 0 || !(value instanceof Float32Array) && !(value instanceof Uint16Array) || value.length !== vertexCount * components) {
1459
- return err(
1460
- failure(
1461
- sourceKey,
1462
- `${attribute.key} cardinality=${vertexCount * components}`,
1463
- `${attribute.key} cardinality=${value?.byteLength ?? "missing"}`
1464
- )
1331
+ case "video": {
1332
+ const video = asset;
1333
+ try {
1334
+ const url = new URL(video.url);
1335
+ if (url.protocol !== "http:" && url.protocol !== "https:")
1336
+ throw new Error("unsupported URL scheme");
1337
+ } catch (error) {
1338
+ throw new TypeError(
1339
+ `video URL is invalid: ${error instanceof Error ? error.message : String(error)}`
1465
1340
  );
1466
1341
  }
1342
+ return { payload: video, refs: [], artifacts: {} };
1467
1343
  }
1468
- const interleaved = new Uint8Array(vertexCount * projection.arrayStride);
1469
- const interleavedView = new DataView(interleaved.buffer);
1470
- for (const attribute of projection.attributes) {
1471
- const value = attributes[attribute.key];
1472
- if (value === void 0) continue;
1473
- const components = attribute.byteLength / (attribute.format === "uint16x4" ? 2 : 4);
1474
- for (let vertex = 0; vertex < vertexCount; vertex++) {
1475
- for (let component = 0; component < components; component++) {
1476
- const sourceIndex = vertex * components + component;
1477
- const targetOffset = vertex * projection.arrayStride + attribute.offset + component * (attribute.format === "uint16x4" ? 2 : 4);
1478
- if (attribute.format === "uint16x4") {
1479
- interleavedView.setUint16(targetOffset, value[sourceIndex] ?? 0, true);
1480
- } else {
1481
- interleavedView.setFloat32(
1482
- targetOffset,
1483
- value[sourceIndex] ?? 0,
1484
- true
1485
- );
1486
- }
1487
- }
1488
- }
1344
+ case "skeleton": {
1345
+ const skeleton = asset;
1346
+ return {
1347
+ payload: skeleton,
1348
+ refs: [],
1349
+ artifacts: { body: jsonArtifact(skeleton) }
1350
+ };
1489
1351
  }
1490
- const indexCount = indices?.length ?? 0;
1491
- const indexWidth = indices === void 0 || indexCount === 0 ? 0 : indices.BYTES_PER_ELEMENT;
1492
- const indexBytes = indexCount * indexWidth;
1493
- if (!Number.isSafeInteger(indexBytes) || indexBytes > 4294967295) {
1494
- return err(failure(sourceKey, "safe index payload byte length", `indexBytes=${indexBytes}`));
1352
+ case "skin": {
1353
+ const skin = asset;
1354
+ const skeletonGuid = AssetGuid$1.parse(skin.skeletonGuid);
1355
+ if (!skeletonGuid.ok) throw skeletonGuid.error;
1356
+ return {
1357
+ payload: skin,
1358
+ refs: [ref(skeletonGuid.value, "skeletonGuid")],
1359
+ artifacts: { body: jsonArtifact(skin) }
1360
+ };
1495
1361
  }
1496
- const meta = new TextEncoder().encode(JSON.stringify(refsMeta(payload, refs)));
1497
- const header = {
1498
- version: 4,
1499
- projectionVersion: projection.schemaVersion,
1500
- mask: projection.mask,
1501
- digest: projection.digest,
1502
- stride: projection.arrayStride,
1503
- vertexCount,
1504
- vertexBytes: interleaved.byteLength,
1505
- indexCount,
1506
- indexWidth,
1507
- indexBytes,
1508
- jsonBytes: meta.byteLength
1509
- };
1510
- const total = MESH_BIN_HEADER_V4_BYTES + interleaved.byteLength + indexBytes + meta.byteLength;
1511
- if (!Number.isSafeInteger(total) || total > 4294967295) {
1512
- return err(failure(sourceKey, "safe mesh binary byte length", `total=${total}`));
1362
+ case "animation-clip":
1363
+ return {
1364
+ payload: asset,
1365
+ refs: [],
1366
+ artifacts: { body: jsonArtifact(asset) }
1367
+ };
1368
+ case "animation-graph": {
1369
+ const graph = asset;
1370
+ const refs = [];
1371
+ const nodes = graph.nodes.map((node, index) => {
1372
+ if (node.type !== "clip") return node;
1373
+ const parsed = AssetGuid$1.parse(node.clip);
1374
+ if (!parsed.ok) throw parsed.error;
1375
+ const referenceIndex = refs.push(ref(parsed.value, "nodes", index)) - 1;
1376
+ return { ...node, clip: referenceIndex };
1377
+ });
1378
+ return {
1379
+ payload: { kind: graph.kind, root: graph.root, nodes },
1380
+ refs,
1381
+ artifacts: { body: jsonArtifact(graph) }
1382
+ };
1513
1383
  }
1514
- const out = new Uint8Array(total);
1515
- writeMeshBinHeader(header, out);
1516
- let offset = MESH_BIN_HEADER_V4_BYTES;
1517
- out.set(interleaved, offset);
1518
- offset += interleaved.byteLength;
1519
- if (indices !== void 0 && indexBytes > 0) {
1520
- out.set(new Uint8Array(indices.buffer, indices.byteOffset, indices.byteLength), offset);
1521
- offset += indexBytes;
1384
+ case "audio": {
1385
+ const audio = asset;
1386
+ return {
1387
+ payload: {
1388
+ kind: audio.kind,
1389
+ sourceKey: audio.sourceKey,
1390
+ mediaType: audio.mediaType,
1391
+ bytes: audio.bytes.slice()
1392
+ },
1393
+ refs: [],
1394
+ artifacts: {
1395
+ source: {
1396
+ mediaType: audio.mediaType,
1397
+ assetCodec: { name: "browser-audio", version: "1" },
1398
+ bytes: audio.bytes.slice()
1399
+ }
1400
+ }
1401
+ };
1522
1402
  }
1523
- out.set(meta, offset);
1524
- return ok(out);
1525
- } catch (error) {
1526
- return err(
1527
- failure(
1528
- sourceKey,
1529
- "valid canonical mesh payload",
1530
- error instanceof Error ? error.message : String(error)
1531
- )
1532
- );
1533
- }
1534
- }
1535
-
1536
- // src/scriptable-pack.ts
1537
- var AssetOutputProducerRegistry = class {
1538
- producers = /* @__PURE__ */ new Map();
1539
- register(producer) {
1540
- if (producer.kind.trim().length === 0 || producer.version.trim().length === 0) {
1541
- throw new TypeError("Pack output producer kind and version must be non-empty");
1403
+ case "particle-effect": {
1404
+ const effect = asset;
1405
+ const cooked = particleProgramArtifact(effect);
1406
+ const refs = /* @__PURE__ */ new Set();
1407
+ for (const emitter of cooked.program.emitters) {
1408
+ for (const renderer of emitter.renderers) {
1409
+ if (!("material" in renderer) || typeof renderer.material !== "string") {
1410
+ throw new Error(`Particle emitter '${emitter.id}' requires a renderer Material GUID`);
1411
+ }
1412
+ refs.add(formatGuid(renderer.material));
1413
+ if ("kind" in renderer && renderer.kind === "mesh") {
1414
+ if (!("mesh" in renderer) || typeof renderer.mesh !== "string") {
1415
+ throw new Error(`Particle emitter '${emitter.id}' requires a renderer Mesh GUID`);
1416
+ }
1417
+ refs.add(formatGuid(renderer.mesh));
1418
+ }
1419
+ }
1420
+ }
1421
+ return {
1422
+ payload: { ...effect, programFingerprint: cooked.fingerprint, program: cooked.program },
1423
+ refs: [...refs].sort().map((guid) => ({ guid })),
1424
+ artifacts: {
1425
+ "particle-effect/program.json": {
1426
+ mediaType: "application/json",
1427
+ assetCodec: { name: "forgeax-vfx-program", version: effect.program.format },
1428
+ bytes: cooked.bytes
1429
+ }
1430
+ }
1431
+ };
1542
1432
  }
1543
- if (typeof producer.produce !== "function") {
1544
- throw new TypeError(`Pack output producer ${producer.kind} must expose produce`);
1433
+ case "ies-profile": {
1434
+ const profile = asset;
1435
+ return {
1436
+ payload: profile,
1437
+ refs: [],
1438
+ artifacts: {
1439
+ body: {
1440
+ mediaType: "application/octet-stream",
1441
+ assetCodec: { name: "forgeax-ies-profile", version: "1" },
1442
+ bytes: bytes(profile.data)
1443
+ }
1444
+ }
1445
+ };
1545
1446
  }
1546
- this.producers.set(producer.kind, producer);
1547
- }
1548
- get(kind) {
1549
- return this.producers.get(kind);
1447
+ case "material":
1448
+ case "mesh":
1449
+ case "scene":
1450
+ throw new TypeError(`ordinary producer received already-owned ${asset.kind} asset`);
1550
1451
  }
1551
- versions() {
1552
- return Object.fromEntries(
1553
- [...this.producers.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([kind, producer]) => [kind, producer.version])
1452
+ }
1453
+ var materialAssetOutputProducer = createSafeProducer(
1454
+ "material",
1455
+ "material-pack/2",
1456
+ materialProduct
1457
+ );
1458
+ var meshAssetOutputProducer = createSafeProducer("mesh", "mesh-binary/4", meshProduct);
1459
+ var textureAssetOutputProducer = createSafeProducer(
1460
+ "texture",
1461
+ "texture-pack/1",
1462
+ textureProduct
1463
+ );
1464
+ function createSceneAssetOutputProducer(sceneComponents = []) {
1465
+ const schemas = new Map(
1466
+ sceneComponents.map((component) => [component.name, component.fields])
1467
+ );
1468
+ return createSafeProducer("scene", "scene-pack/3", (input) => sceneProduct(input, schemas));
1469
+ }
1470
+ function createPreExternalizedSceneAssetOutputProducer() {
1471
+ return createSafeProducer("scene", "scene-pack/3", preExternalizedSceneProduct);
1472
+ }
1473
+ function createStandardAssetOutputProducerRegistry(sceneComponents = []) {
1474
+ const registry = new AssetOutputProducerRegistry();
1475
+ registry.register(materialAssetOutputProducer);
1476
+ registry.register(meshAssetOutputProducer);
1477
+ registry.register(textureAssetOutputProducer);
1478
+ registry.register(createSceneAssetOutputProducer(sceneComponents));
1479
+ for (const kind of [
1480
+ "equirect",
1481
+ "sampler",
1482
+ "font",
1483
+ "render-pipeline",
1484
+ "tileset",
1485
+ "video",
1486
+ "skeleton",
1487
+ "skin",
1488
+ "animation-clip",
1489
+ "animation-graph",
1490
+ "audio",
1491
+ "particle-effect",
1492
+ "ies-profile"
1493
+ ]) {
1494
+ registry.register(
1495
+ createSafeProducer(
1496
+ kind,
1497
+ kind === "particle-effect" ? "particle-effect/2" : "ordinary-pod/1",
1498
+ ordinaryPodProduct
1499
+ )
1554
1500
  );
1555
1501
  }
1556
- };
1502
+ return registry;
1503
+ }
1557
1504
 
1558
- // src/scriptable-pack-output-producers.ts
1559
- function producerError(input, reason) {
1560
- return new ImportError({
1561
- code: "import-internal-error",
1562
- expected: `ScriptablePack ${input.asset.kind} output ${input.sourceKey} to satisfy its domain producer contract`,
1563
- hint: "fix the generated Asset payload and rebuild the ScriptablePack",
1564
- detail: { reason: reason instanceof Error ? reason.message : String(reason) }
1565
- });
1505
+ // src/scriptable-pack-build.ts
1506
+ function record(value) {
1507
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1566
1508
  }
1567
- function formatGuid(value) {
1568
- if (typeof value === "string") {
1569
- const parsed = AssetGuid$1.parse(value);
1570
- if (!parsed.ok) throw parsed.error;
1571
- return AssetGuid$1.format(parsed.value);
1572
- }
1573
- return AssetGuid$1.format(value);
1509
+ function isAsset(value) {
1510
+ return record(value) && typeof value.kind === "string" && isScriptablePackAssetKind(value.kind);
1574
1511
  }
1575
- function canonical(value) {
1576
- if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
1577
- if (value !== null && typeof value === "object") {
1578
- return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => `${JSON.stringify(key)}:${canonical(child)}`).join(",")}}`;
1579
- }
1580
- return JSON.stringify(value) ?? "null";
1512
+ function needsMaterialCook(asset) {
1513
+ return asset.kind === "material" && Array.isArray(asset.passes) && !isEngineMaterial(asset);
1581
1514
  }
1582
- function particleProgramArtifact(effect) {
1583
- const artifactProgram = { format: effect.program.format, emitters: effect.program.emitters };
1584
- const bytes2 = new TextEncoder().encode(canonical(artifactProgram));
1585
- const fingerprint = `sha256:${bytesToHex(sha256(bytes2))}`;
1586
- return {
1587
- program: { ...effect.program, fingerprint },
1588
- bytes: bytes2,
1589
- fingerprint
1590
- };
1515
+ function clone(value) {
1516
+ return structuredClone(value);
1591
1517
  }
1592
- function materialProduct(input) {
1593
- if (input.asset.kind !== "material") throw new TypeError("expected MaterialAsset");
1594
- const material = input.asset;
1595
- const refs = [];
1596
- const addRef = (guid, sourceField) => {
1597
- refs.push({ guid: formatGuid(guid), sourceField });
1598
- return refs.length - 1;
1599
- };
1600
- const addMaterialRef = (value, sourceField) => {
1601
- if (typeof value === "number") {
1602
- throw new TypeError("material texture references must be GUIDs, not runtime handles");
1603
- }
1604
- return addRef(value, sourceField);
1605
- };
1606
- const textureFields = material.parameters === void 0 ? new Set(MATERIAL_TEXTURE_SLOTS) : new Set(
1607
- material.parameters.filter(
1608
- (parameter) => parameter.type === "texture" || parameter.type === "texture_cube"
1609
- ).map((parameter) => parameter.name)
1610
- );
1611
- const parent = material.parent === void 0 ? void 0 : addRef(material.parent, { fieldName: "parent" });
1612
- const values = {};
1613
- for (const fieldName of Object.keys(material.values ?? {}).sort()) {
1614
- const value = material.values?.[fieldName];
1615
- if (typeof value === "string" && textureFields.has(fieldName)) {
1616
- values[fieldName] = {
1617
- texture: addMaterialRef(value, { componentName: "<material>", fieldName })
1618
- };
1619
- } else if (value !== null && typeof value === "object" && !Array.isArray(value) && "texture" in value) {
1620
- const texture = value;
1621
- values[fieldName] = {
1622
- ...texture,
1623
- texture: addMaterialRef(texture.texture, { componentName: "<material>", fieldName }),
1624
- ...texture.sampler === void 0 ? {} : {
1625
- sampler: addMaterialRef(texture.sampler, {
1626
- componentName: "<material>",
1627
- fieldName: `${fieldName}.sampler`
1518
+ function observedReader(source) {
1519
+ const reads = /* @__PURE__ */ new Map();
1520
+ const reader = {
1521
+ async readByGuid(guid) {
1522
+ const key = AssetGuid.format(guid).toLowerCase();
1523
+ const cached = reads.get(key);
1524
+ if (cached !== void 0) return ok(clone(cached.asset));
1525
+ if (source === void 0) {
1526
+ return err(
1527
+ new AssetError({
1528
+ code: "asset-not-found",
1529
+ expected: `a published Asset snapshot for content dependency ${key}`,
1530
+ hint: "publish the dependency or remove the content read from the Pack build",
1531
+ detail: { sourcePath: key }
1628
1532
  })
1629
- }
1630
- };
1631
- } else {
1632
- values[fieldName] = value;
1633
- }
1634
- }
1635
- const { parent: _parent, values: _values, ...materialShape } = material;
1636
- return {
1637
- payload: {
1638
- ...materialShape,
1639
- ...parent === void 0 ? {} : { parent },
1640
- ...material.values === void 0 ? {} : { values }
1641
- },
1642
- refs,
1643
- artifacts: {}
1644
- };
1645
- }
1646
- function meshProduct(input) {
1647
- if (input.asset.kind !== "mesh") throw new TypeError("expected MeshAsset");
1648
- const mesh = input.asset;
1649
- const refs = [];
1650
- for (let slotIndex = 0; slotIndex < mesh.materialSlots.length; slotIndex++) {
1651
- const defaultMaterial = mesh.materialSlots[slotIndex]?.defaultMaterial;
1652
- if (defaultMaterial === void 0) continue;
1653
- refs.push({
1654
- guid: formatGuid(defaultMaterial),
1655
- sourceField: { fieldName: "materialSlots", arrayIndex: slotIndex }
1656
- });
1657
- }
1658
- const seenRefs = new Set(refs.map((reference) => reference.guid.toLowerCase()));
1659
- for (const [lodIndex, lod] of (mesh.lods ?? []).entries()) {
1660
- const guid = formatGuid(lod.mesh);
1661
- if (seenRefs.has(guid.toLowerCase())) continue;
1662
- seenRefs.add(guid.toLowerCase());
1663
- refs.push({ guid, sourceField: { fieldName: "lods", arrayIndex: lodIndex } });
1664
- }
1665
- return {
1666
- payload: mesh,
1667
- refs,
1668
- artifacts: {
1669
- body: {
1670
- mediaType: "application/x-forgeax-mesh",
1671
- assetCodec: { name: "mesh-binary", version: "4" },
1672
- bytes: (() => {
1673
- const packed = packMeshBinV4(
1674
- mesh,
1675
- input.sourceKey,
1676
- refs.map((reference) => reference.guid)
1677
- );
1678
- if (!packed.ok) {
1679
- throw packed.error;
1680
- }
1681
- return packed.value;
1682
- })()
1533
+ );
1683
1534
  }
1535
+ const result = await source.readByGuid(guid);
1536
+ if (!result.ok) return result;
1537
+ const asset = clone(result.value.asset);
1538
+ reads.set(key, {
1539
+ guid: key,
1540
+ asset,
1541
+ generation: result.value.generation,
1542
+ digest: result.value.digest
1543
+ });
1544
+ return ok(clone(asset));
1684
1545
  }
1685
1546
  };
1547
+ return { reader, reads };
1686
1548
  }
1687
- function sceneProduct(input, components) {
1688
- if (input.asset.kind !== "scene") throw new TypeError("expected SceneAsset");
1689
- const externalized = externalizeSceneAsset(input.asset, (componentName) => {
1690
- const schema = components.get(componentName);
1691
- if (schema === void 0) {
1692
- throw new TypeError(
1693
- `ScriptablePack scene component ${componentName} is missing from sceneComponents`
1694
- );
1695
- }
1696
- return schema;
1697
- });
1698
- if (!externalized.ok) throw new TypeError(`scene field ${externalized.error.field} is invalid`);
1699
- return {
1700
- payload: { kind: "scene", ...externalized.value.payload },
1701
- refs: externalized.value.refs,
1702
- artifacts: {}
1703
- };
1704
- }
1705
- function containsAssetGuid(value) {
1706
- if (typeof value === "string") return AssetGuid$1.parse(value).ok;
1707
- if (Array.isArray(value)) return value.some(containsAssetGuid);
1708
- if (value !== null && typeof value === "object") {
1709
- return Object.values(value).some(containsAssetGuid);
1710
- }
1711
- return false;
1712
- }
1713
- function preExternalizedSceneProduct(input) {
1714
- if (input.asset.kind !== "scene") throw new TypeError("expected SceneAsset");
1715
- if (containsAssetGuid(input.asset)) {
1716
- throw new TypeError("direct scene payload must use explicit refs with runtime indices");
1717
- }
1718
- return {
1719
- payload: { ...input.asset, kind: "scene" },
1720
- refs: [],
1721
- artifacts: {}
1722
- };
1549
+ function buildContext(packageId, values, reader) {
1550
+ if (values === void 0) return { packageId, readByGuid: reader.readByGuid };
1551
+ return { packageId, values, readByGuid: reader.readByGuid };
1723
1552
  }
1724
- function createSafeProducer(kind, version, product) {
1553
+ function sourceKeyFailure(sourcePath, sourceKey) {
1725
1554
  return {
1726
- kind,
1727
- version,
1728
- produce(input) {
1729
- try {
1730
- return ok(product(input));
1731
- } catch (error) {
1732
- return err(producerError(input, error));
1733
- }
1734
- }
1555
+ code: "pack-source-key-invalid",
1556
+ expected: "a sourceKey matching the Pack source-key grammar",
1557
+ hint: "return stable lower-case semantic keys instead of paths or output indexes",
1558
+ detail: { sourcePath, sourceKey }
1735
1559
  };
1736
1560
  }
1737
- function jsonArtifact(value) {
1561
+ function outputValueError(sourcePath, sourceKey, expected, actual) {
1738
1562
  return {
1739
- mediaType: "application/json",
1740
- assetCodec: { name: "forgeax-json", version: "1" },
1741
- bytes: new TextEncoder().encode(JSON.stringify(value))
1563
+ code: "pack-parameter-invalid",
1564
+ expected,
1565
+ hint: "repair the build output and rebuild the Pack from a fresh generation",
1566
+ detail: { sourcePath, sourceKey, actual: typeof actual === "string" ? actual : typeof actual }
1742
1567
  };
1743
1568
  }
1744
- function ref(guid, fieldName, arrayIndex) {
1569
+ function referenceError(code, sourcePath, guids) {
1745
1570
  return {
1746
- guid: formatGuid(guid),
1747
- sourceField: { fieldName, ...arrayIndex === void 0 ? {} : { arrayIndex } }
1571
+ code,
1572
+ expected: code === "pack-output-reference-missing" ? "every output reference to resolve to a local or published AssetGuid" : "removed output GUIDs to have no incoming references",
1573
+ hint: code === "pack-output-reference-missing" ? "build or publish the referenced Pack before verifying this output" : "migrate incoming references before publishing the topology change",
1574
+ detail: { sourcePath, guids: [...guids].sort() }
1748
1575
  };
1749
1576
  }
1750
- function bytes(value) {
1751
- return Uint8Array.from(value);
1752
- }
1753
- function textureProduct(input) {
1754
- if (input.asset.kind !== "texture") throw new TypeError("expected TextureAsset");
1755
- const texture = input.asset;
1756
- const layout = deriveTextureLayout({
1757
- shape: texture.shape,
1758
- format: texture.format,
1759
- mips: texture.mips,
1760
- actualByteLength: texture.data.byteLength,
1761
- order: "mip-major,image-major,row-major"
1762
- });
1763
- if (!layout.ok) {
1764
- const expectedBytes = layout.error.code === "texture-packing-invalid" ? layout.error.detail.expectedBytes : texture.data.byteLength;
1765
- const actualBytes = layout.error.code === "texture-packing-invalid" ? layout.error.detail.actualBytes : texture.data.byteLength;
1766
- throw new TypeError(
1767
- `texture data is not canonical: expected ${expectedBytes} bytes, got ${actualBytes}`
1577
+ function productError(sourcePath, sourceKey, producer, product) {
1578
+ if (!record(product)) {
1579
+ return outputValueError(
1580
+ sourcePath,
1581
+ sourceKey,
1582
+ `producer ${producer.kind} to return an asset product object`,
1583
+ product
1768
1584
  );
1769
1585
  }
1770
- return {
1771
- payload: texture,
1772
- refs: [],
1773
- artifacts: {
1774
- body: {
1775
- mediaType: texture.format === "r8unorm" ? "application/x-forgeax-r8" : `application/x-forgeax-${texture.format}`,
1776
- assetCodec: { name: texture.format, version: "1" },
1777
- bytes: bytes(texture.data)
1778
- }
1779
- }
1780
- };
1781
- }
1782
- function ordinaryPodProduct(input) {
1783
- const asset = input.asset;
1784
- switch (asset.kind) {
1785
- case "texture":
1786
- return textureProduct(input);
1787
- case "equirect": {
1788
- const equirect = asset;
1789
- return {
1790
- payload: equirect,
1791
- refs: [],
1792
- artifacts: {
1793
- body: {
1794
- mediaType: "image/raw",
1795
- assetCodec: { name: "raw-image", version: "1" },
1796
- bytes: bytes(equirect.data)
1797
- }
1798
- }
1799
- };
1586
+ if (!Array.isArray(product.refs)) {
1587
+ return outputValueError(
1588
+ sourcePath,
1589
+ sourceKey,
1590
+ `producer ${producer.kind} to return a refs array`,
1591
+ product.refs
1592
+ );
1593
+ }
1594
+ if (!record(product.artifacts)) {
1595
+ return outputValueError(
1596
+ sourcePath,
1597
+ sourceKey,
1598
+ `producer ${producer.kind} to return an artifacts object`,
1599
+ product.artifacts
1600
+ );
1601
+ }
1602
+ const payload = product.payload;
1603
+ if (!record(payload) || payload.kind !== producer.kind) {
1604
+ return outputValueError(
1605
+ sourcePath,
1606
+ sourceKey,
1607
+ `producer ${producer.kind} to return a matching payload kind`,
1608
+ payload
1609
+ );
1610
+ }
1611
+ for (const ref2 of product.refs) {
1612
+ if (!record(ref2) || typeof ref2.guid !== "string" || !AssetGuid.parse(ref2.guid).ok) {
1613
+ return outputValueError(
1614
+ sourcePath,
1615
+ sourceKey,
1616
+ "producer refs to contain valid AssetGuid values",
1617
+ ref2
1618
+ );
1800
1619
  }
1801
- case "sampler": {
1802
- const sampler = asset;
1803
- return { payload: sampler, refs: [], artifacts: { body: jsonArtifact(sampler) } };
1620
+ }
1621
+ for (const [key, artifact] of Object.entries(product.artifacts)) {
1622
+ if (key.length === 0 || key.startsWith("/") || key.includes("..") || key.includes("\\") || !record(artifact) || typeof artifact.mediaType !== "string" || !(artifact.bytes instanceof Uint8Array)) {
1623
+ return outputValueError(
1624
+ sourcePath,
1625
+ sourceKey,
1626
+ "asset-local artifacts with safe keys and bytes",
1627
+ key
1628
+ );
1804
1629
  }
1805
- case "font": {
1806
- const font = asset;
1807
- const atlas = ref(font.atlas, "atlas");
1808
- const sampler = ref(font.sampler, "sampler");
1809
- return {
1810
- payload: {
1811
- kind: font.kind,
1812
- glyphs: font.glyphs,
1813
- common: font.common,
1814
- atlasGuid: atlas.guid,
1815
- samplerGuid: sampler.guid
1816
- },
1817
- refs: [atlas, sampler],
1818
- artifacts: { body: jsonArtifact(font) }
1819
- };
1630
+ }
1631
+ return void 0;
1632
+ }
1633
+ function errorCode(value) {
1634
+ return record(value) && typeof value.code === "string" ? value.code : void 0;
1635
+ }
1636
+ function normalizedGuidSet(value) {
1637
+ return value === void 0 ? void 0 : new Set([...value].map((guid) => guid.toLowerCase()));
1638
+ }
1639
+ async function buildScriptablePack(options) {
1640
+ const subjectPackageId = options.subjectPackageId ?? options.definition.packageId;
1641
+ const availableGuids = normalizedGuidSet(options.availableGuids);
1642
+ const observed = observedReader(options.assetSource);
1643
+ let effectiveValues;
1644
+ if ("parameters" in options.definition) {
1645
+ const resolved = resolvePackParameterValues(
1646
+ options.definition,
1647
+ options.values ?? {},
1648
+ options.inheritedValues
1649
+ );
1650
+ if (!resolved.ok) return resolved;
1651
+ effectiveValues = resolved.value;
1652
+ } else if (options.values !== void 0 && Object.keys(options.values).length > 0) {
1653
+ return err(
1654
+ outputValueError(
1655
+ options.sourcePath,
1656
+ "$.values",
1657
+ "zero-parameter Packs to omit values and instance capabilities",
1658
+ options.values
1659
+ )
1660
+ );
1661
+ } else if (options.subjectPackageId !== void 0 && PackageId.format(options.subjectPackageId).toLowerCase() !== PackageId.format(options.definition.packageId).toLowerCase()) {
1662
+ return err({
1663
+ code: "pack-parent-has-no-parameters",
1664
+ expected: "a ScriptablePack source with parameters for an independent instance packageId",
1665
+ hint: "use clone for a zero-parameter Pack instead of building it as an instance",
1666
+ detail: {
1667
+ sourcePath: options.sourcePath,
1668
+ rootPackageId: PackageId.format(options.definition.packageId),
1669
+ subjectPackageId: PackageId.format(options.subjectPackageId)
1670
+ }
1671
+ });
1672
+ }
1673
+ let built;
1674
+ try {
1675
+ const context = buildContext(subjectPackageId, effectiveValues, observed.reader);
1676
+ built = options.definition.build(context);
1677
+ built = await built;
1678
+ } catch (cause) {
1679
+ return err(
1680
+ new ImportError({
1681
+ code: "import-internal-error",
1682
+ expected: "Pack build to return a structured Result without throwing",
1683
+ hint: "repair the authoring function and return err(...) for expected failures",
1684
+ detail: {
1685
+ reason: `${options.sourcePath}: ${cause instanceof Error ? cause.message : String(cause)}`
1686
+ }
1687
+ })
1688
+ );
1689
+ }
1690
+ if (!record(built) || typeof built.ok !== "boolean") {
1691
+ return err(
1692
+ new ImportError({
1693
+ code: "import-internal-error",
1694
+ expected: "Pack build to return a Result object",
1695
+ hint: "return ok(sourceKeyToAsset) or err(structuredError) from the authoring function",
1696
+ detail: { reason: `${options.sourcePath}: malformed build result` }
1697
+ })
1698
+ );
1699
+ }
1700
+ if (!built.ok) {
1701
+ if (errorCode(built.error) !== void 0) return err(built.error);
1702
+ return err(
1703
+ new ImportError({
1704
+ code: "import-internal-error",
1705
+ expected: "a structured Pack build error",
1706
+ hint: "return an error carrying code, expected, hint and detail",
1707
+ detail: { reason: `${options.sourcePath}: ${String(built.error)}` }
1708
+ })
1709
+ );
1710
+ }
1711
+ if (!record(built.value)) {
1712
+ return err(
1713
+ outputValueError(
1714
+ options.sourcePath,
1715
+ "$",
1716
+ "build to return a sourceKey-to-Asset object",
1717
+ built.value
1718
+ )
1719
+ );
1720
+ }
1721
+ const imported = [];
1722
+ const stagedOutputs = [];
1723
+ const localGuids = /* @__PURE__ */ new Set();
1724
+ const cookerRegistry = new NativeCookerRegistry();
1725
+ for (const cooker of options.cookers ?? []) cookerRegistry.register(cooker);
1726
+ const nativeFingerprints = /* @__PURE__ */ new Map();
1727
+ for (const sourceKey of Object.keys(built.value).sort()) {
1728
+ if (!isValidPackSourceKey(sourceKey))
1729
+ return err(sourceKeyFailure(options.sourcePath, sourceKey));
1730
+ const asset = built.value[sourceKey];
1731
+ const customSource = record(asset) && asset.execution === "cooked";
1732
+ if (customSource && (typeof asset.kind !== "string" || asset.kind.trim().length === 0 || isScriptablePackAssetKind(asset.kind) || !("source" in asset) || Object.keys(asset).some((key) => !["kind", "execution", "source"].includes(key)))) {
1733
+ return err(
1734
+ outputValueError(
1735
+ options.sourcePath,
1736
+ sourceKey,
1737
+ "a custom kind with only execution: cooked and source; ordinary Assets use their standard producers",
1738
+ asset
1739
+ )
1740
+ );
1820
1741
  }
1821
- case "render-pipeline": {
1822
- const pipeline = asset;
1823
- return {
1824
- payload: pipeline,
1825
- refs: [],
1826
- artifacts: { body: jsonArtifact(pipeline) }
1742
+ if (!customSource && !isAsset(asset)) {
1743
+ return err(
1744
+ outputValueError(
1745
+ options.sourcePath,
1746
+ sourceKey,
1747
+ "a concrete Asset with a supported kind",
1748
+ asset
1749
+ )
1750
+ );
1751
+ }
1752
+ const guid = AssetGuid.format(AssetGuid.derive(subjectPackageId, sourceKey));
1753
+ const normalizedGuid = guid.toLowerCase();
1754
+ if (localGuids.has(normalizedGuid) || availableGuids?.has(normalizedGuid)) {
1755
+ return err({
1756
+ code: "pack-guid-collision",
1757
+ expected: "derived output GUIDs to be unique in the global source index",
1758
+ hint: "change the colliding packageId or repair the source index before publishing",
1759
+ detail: { sourcePath: options.sourcePath, guid }
1760
+ });
1761
+ }
1762
+ localGuids.add(normalizedGuid);
1763
+ if (customSource) {
1764
+ const cooked = await cookerRegistry.runDraft(asset.kind, {
1765
+ guid,
1766
+ sourceKey,
1767
+ sourcePath: options.sourcePath,
1768
+ source: asset.source,
1769
+ refs: []
1770
+ });
1771
+ if (!cooked.ok) return err(cooked.error);
1772
+ if (cooked.value.guid.toLowerCase() !== normalizedGuid)
1773
+ return err({
1774
+ code: "pack-source-output-invalid",
1775
+ expected: "the custom cooker to preserve the derived AssetGuid",
1776
+ hint: "remove cooker-owned identity generation and retain the host GUID",
1777
+ detail: {
1778
+ sourcePath: options.sourcePath,
1779
+ sourceKey,
1780
+ expectedGuid: guid,
1781
+ actualGuid: cooked.value.guid
1782
+ }
1783
+ });
1784
+ const product3 = {
1785
+ payload: cooked.value.payload,
1786
+ refs: cooked.value.refs.map((guid2) => ({ guid: guid2 })),
1787
+ artifacts: cooked.value.artifacts
1827
1788
  };
1789
+ const invalid = productError(options.sourcePath, sourceKey, { kind: asset.kind }, product3);
1790
+ if (invalid !== void 0) return err(invalid);
1791
+ imported.push({ guid, kind: asset.kind, ...product3 });
1792
+ nativeFingerprints.set(sourceKey, cooked.value.inputFingerprint);
1793
+ stagedOutputs.push({
1794
+ guid: AssetGuid.derive(subjectPackageId, sourceKey),
1795
+ sourceKey,
1796
+ asset: clone(asset),
1797
+ digest: await scriptablePackFingerprint(asset)
1798
+ });
1799
+ continue;
1828
1800
  }
1829
- case "tileset": {
1830
- const tileset = asset;
1831
- const refs = tileset.atlases.map((atlas, index) => {
1832
- const parsed = AssetGuid$1.parse(atlas);
1833
- if (!parsed.ok) throw parsed.error;
1834
- return ref(parsed.value, "atlases", index);
1801
+ if (!isAsset(asset))
1802
+ return err(outputValueError(options.sourcePath, sourceKey, "an ordinary Asset", asset));
1803
+ const producer = options.outputs.get(asset.kind);
1804
+ if (producer === void 0) {
1805
+ return err(
1806
+ outputValueError(
1807
+ options.sourcePath,
1808
+ sourceKey,
1809
+ `a registered output producer for ${asset.kind}`,
1810
+ asset.kind
1811
+ )
1812
+ );
1813
+ }
1814
+ let produced;
1815
+ try {
1816
+ produced = await producer.produce({ guid, sourceKey, asset });
1817
+ } catch (cause) {
1818
+ return err(
1819
+ new ImportError({
1820
+ code: "import-internal-error",
1821
+ expected: `producer ${producer.kind} to return a structured Result without throwing`,
1822
+ hint: "repair the output producer and return err(...) for expected failures",
1823
+ detail: {
1824
+ reason: `${options.sourcePath}:${sourceKey}: ${cause instanceof Error ? cause.message : String(cause)}`
1825
+ }
1826
+ })
1827
+ );
1828
+ }
1829
+ if (!record(produced) || typeof produced.ok !== "boolean") {
1830
+ return err(
1831
+ outputValueError(
1832
+ options.sourcePath,
1833
+ sourceKey,
1834
+ `producer ${producer.kind} to return a Result`,
1835
+ produced
1836
+ )
1837
+ );
1838
+ }
1839
+ if (!produced.ok) {
1840
+ if (errorCode(produced.error) !== void 0) return err(produced.error);
1841
+ return err(
1842
+ new ImportError({
1843
+ code: "import-internal-error",
1844
+ expected: `producer ${producer.kind} to return a structured error`,
1845
+ hint: "return an error carrying code, expected, hint and detail",
1846
+ detail: { reason: `${options.sourcePath}:${sourceKey}: ${String(produced.error)}` }
1847
+ })
1848
+ );
1849
+ }
1850
+ const invalidProduct2 = productError(options.sourcePath, sourceKey, producer, produced.value);
1851
+ if (invalidProduct2 !== void 0) return err(invalidProduct2);
1852
+ const product2 = produced.value;
1853
+ let payload = product2.payload;
1854
+ let artifacts2 = product2.artifacts;
1855
+ let refs2 = product2.refs;
1856
+ if (needsMaterialCook(asset) && cookerRegistry.get("material") !== void 0) {
1857
+ const cooked = await cookerRegistry.runDraft("material", {
1858
+ guid,
1859
+ source: asset,
1860
+ sourceKey,
1861
+ sourcePath: options.sourcePath,
1862
+ refs: product2.refs.map((reference) => reference.guid)
1835
1863
  });
1836
- return {
1837
- payload: { ...tileset, atlases: refs.map((_entry, index) => index) },
1838
- refs,
1839
- artifacts: { body: jsonArtifact(tileset) }
1864
+ if (!cooked.ok) return err(cooked.error);
1865
+ if (cooked.value.guid.toLowerCase() !== normalizedGuid) {
1866
+ return err({
1867
+ code: "pack-source-output-invalid",
1868
+ expected: "the authored material cooker to preserve the derived AssetGuid",
1869
+ hint: "repair the native material cooker output GUID and rebuild the Pack",
1870
+ detail: {
1871
+ sourcePath: options.sourcePath,
1872
+ sourceKey,
1873
+ expectedGuid: guid,
1874
+ actualGuid: cooked.value.guid
1875
+ }
1876
+ });
1877
+ }
1878
+ const cookedAsset = cooked.value.payload;
1879
+ if (!isAsset(cookedAsset) || cookedAsset.kind !== "material") {
1880
+ return err({
1881
+ code: "pack-source-output-invalid",
1882
+ expected: "the authored material cooker to return a material payload",
1883
+ hint: "repair the native material cooker payload and rebuild the Pack",
1884
+ detail: { sourcePath: options.sourcePath, sourceKey }
1885
+ });
1886
+ }
1887
+ const projected = await materialAssetOutputProducer.produce({
1888
+ guid,
1889
+ sourceKey,
1890
+ asset: cookedAsset
1891
+ });
1892
+ if (!projected.ok) return err(projected.error);
1893
+ const projectedInvalid = productError(
1894
+ options.sourcePath,
1895
+ sourceKey,
1896
+ materialAssetOutputProducer,
1897
+ projected.value
1898
+ );
1899
+ if (projectedInvalid !== void 0) return err(projectedInvalid);
1900
+ payload = projected.value.payload;
1901
+ artifacts2 = {
1902
+ ...product2.artifacts,
1903
+ ...cooked.value.artifacts,
1904
+ ...projected.value.artifacts
1840
1905
  };
1906
+ const mergedRefs = [...projected.value.refs];
1907
+ const seenRefs = new Set(mergedRefs.map((reference) => reference.guid.toLowerCase()));
1908
+ for (const cookerRef of cooked.value.refs) {
1909
+ const parsed = AssetGuid.parse(cookerRef);
1910
+ if (!parsed.ok) {
1911
+ return err({
1912
+ code: "pack-source-output-invalid",
1913
+ expected: "the authored material cooker refs to contain valid AssetGuid values",
1914
+ hint: "repair the native material cooker refs and rebuild the Pack",
1915
+ detail: { sourcePath: options.sourcePath, sourceKey, ref: cookerRef }
1916
+ });
1917
+ }
1918
+ const formatted = AssetGuid.format(parsed.value);
1919
+ const normalizedRef = formatted.toLowerCase();
1920
+ if (seenRefs.has(normalizedRef)) continue;
1921
+ seenRefs.add(normalizedRef);
1922
+ mergedRefs.push({ guid: formatted });
1923
+ }
1924
+ refs2 = mergedRefs;
1925
+ nativeFingerprints.set(sourceKey, cooked.value.inputFingerprint);
1841
1926
  }
1842
- case "video": {
1843
- const video = asset;
1844
- try {
1845
- const url = new URL(video.url);
1846
- if (url.protocol !== "http:" && url.protocol !== "https:")
1847
- throw new Error("unsupported URL scheme");
1848
- } catch (error) {
1849
- throw new TypeError(
1850
- `video URL is invalid: ${error instanceof Error ? error.message : String(error)}`
1851
- );
1927
+ imported.push({
1928
+ guid,
1929
+ kind: asset.kind,
1930
+ payload,
1931
+ refs: refs2,
1932
+ artifacts: artifacts2
1933
+ });
1934
+ stagedOutputs.push({
1935
+ guid: AssetGuid.derive(subjectPackageId, sourceKey),
1936
+ sourceKey,
1937
+ asset: clone(asset),
1938
+ digest: await scriptablePackFingerprint(asset)
1939
+ });
1940
+ }
1941
+ const referenced = /* @__PURE__ */ new Set();
1942
+ for (const asset of imported) {
1943
+ for (const ref2 of asset.refs) {
1944
+ const guid = ref2.guid.toLowerCase();
1945
+ if (!localGuids.has(guid)) referenced.add(guid);
1946
+ }
1947
+ }
1948
+ if (options.deferReferenceValidation !== true) {
1949
+ const missing = [...referenced].filter(
1950
+ (guid) => availableGuids !== void 0 && !availableGuids.has(guid)
1951
+ );
1952
+ if (missing.length > 0)
1953
+ return err(referenceError("pack-output-reference-missing", options.sourcePath, missing));
1954
+ }
1955
+ const externalEvidence = [...observed.reads.values()].sort((left, right) => left.guid.localeCompare(right.guid)).map(
1956
+ (read) => ({
1957
+ guid: read.guid,
1958
+ usage: referenced.has(read.guid) ? "both" : "content",
1959
+ generation: read.generation,
1960
+ digest: read.digest
1961
+ })
1962
+ );
1963
+ const authoredInputFingerprint = await scriptablePackFingerprint({
1964
+ packageId: PackageId.format(subjectPackageId),
1965
+ sourcePath: options.sourcePath,
1966
+ sourceClosure: options.sourceClosure ?? [],
1967
+ values: effectiveValues,
1968
+ externalEvidence: externalEvidence.map(({ guid, usage, digest: evidenceDigest }) => ({
1969
+ guid,
1970
+ usage,
1971
+ digest: evidenceDigest
1972
+ })),
1973
+ authoringContractVersion: options.authoringContractVersion ?? "scriptable-pack/1",
1974
+ producerVersions: options.outputs.versions()
1975
+ });
1976
+ const inputFingerprint = nativeFingerprints.size === 0 ? authoredInputFingerprint : await scriptablePackFingerprint({
1977
+ sourceRevision: authoredInputFingerprint,
1978
+ nativeCookers: [...nativeFingerprints.entries()].sort(
1979
+ ([left], [right]) => left.localeCompare(right)
1980
+ )
1981
+ });
1982
+ const refs = imported.flatMap((asset) => asset.refs);
1983
+ const artifacts = Object.fromEntries(
1984
+ imported.flatMap(
1985
+ (asset) => Object.entries(asset.artifacts).map(([key, artifact]) => [`${asset.guid}/${key}`, artifact])
1986
+ )
1987
+ );
1988
+ const product = createImportProduct({
1989
+ assets: imported,
1990
+ sourceDependencies: (options.sourceClosure ?? []).map((entry) => entry.path),
1991
+ refs,
1992
+ artifacts,
1993
+ receipts: imported.map((asset) => ({
1994
+ guid: asset.guid,
1995
+ origin: "authoredPack",
1996
+ status: "succeeded",
1997
+ inputFingerprint
1998
+ })),
1999
+ diagnostics: [],
2000
+ sourceRevision: inputFingerprint,
2001
+ sourceKey: options.sourcePath
2002
+ });
2003
+ if (!product.ok) return err(product.error);
2004
+ return ok({
2005
+ product: product.value,
2006
+ stagedOutputs,
2007
+ externalEvidence,
2008
+ inputFingerprint,
2009
+ ...options.publication === void 0 ? {} : { publication: options.publication }
2010
+ });
2011
+ }
2012
+ function stagedSource(staged, fallback) {
2013
+ return {
2014
+ async readByGuid(guid) {
2015
+ const key = AssetGuid.format(guid).toLowerCase();
2016
+ const local = staged.get(key);
2017
+ if (local !== void 0) {
2018
+ return ok({
2019
+ asset: clone(local.asset),
2020
+ generation: 1,
2021
+ digest: local.digest ?? "sha256:staged"
2022
+ });
1852
2023
  }
1853
- return { payload: video, refs: [], artifacts: {} };
1854
- }
1855
- case "skeleton": {
1856
- const skeleton = asset;
1857
- return {
1858
- payload: skeleton,
1859
- refs: [],
1860
- artifacts: { body: jsonArtifact(skeleton) }
1861
- };
1862
- }
1863
- case "skin": {
1864
- const skin = asset;
1865
- const skeletonGuid = AssetGuid$1.parse(skin.skeletonGuid);
1866
- if (!skeletonGuid.ok) throw skeletonGuid.error;
1867
- return {
1868
- payload: skin,
1869
- refs: [ref(skeletonGuid.value, "skeletonGuid")],
1870
- artifacts: { body: jsonArtifact(skin) }
1871
- };
2024
+ if (fallback === void 0) {
2025
+ return err({
2026
+ code: "asset-not-found",
2027
+ expected: "a staged or published content dependency",
2028
+ hint: "wait for the dependency subject to materialize",
2029
+ detail: { guid: key }
2030
+ });
2031
+ }
2032
+ return fallback.readByGuid(guid);
1872
2033
  }
1873
- case "animation-clip":
1874
- return {
1875
- payload: asset,
1876
- refs: [],
1877
- artifacts: { body: jsonArtifact(asset) }
1878
- };
1879
- case "animation-graph": {
1880
- const graph = asset;
1881
- const refs = [];
1882
- const nodes = graph.nodes.map((node, index) => {
1883
- if (node.type !== "clip") return node;
1884
- const parsed = AssetGuid$1.parse(node.clip);
1885
- if (!parsed.ok) throw parsed.error;
1886
- const referenceIndex = refs.push(ref(parsed.value, "nodes", index)) - 1;
1887
- return { ...node, clip: referenceIndex };
2034
+ };
2035
+ }
2036
+ async function buildScriptablePackWorklist(options) {
2037
+ const orderedSubjects = [...options.subjects].sort(
2038
+ (left, right) => left.sourcePath.localeCompare(right.sourcePath)
2039
+ );
2040
+ const pending = new Map(
2041
+ orderedSubjects.map((subject, index) => [`${index}:${subject.sourcePath}`, subject])
2042
+ );
2043
+ const staged = /* @__PURE__ */ new Map();
2044
+ const results = /* @__PURE__ */ new Map();
2045
+ const availableGuids = /* @__PURE__ */ new Set([
2046
+ ...normalizedGuidSet(options.availableGuids) ?? [],
2047
+ ...BUILTIN_MESH_ASSETS.map((asset) => asset.guid.toLowerCase())
2048
+ ]);
2049
+ const maxPasses = options.maxPasses ?? Math.max(1, options.subjects.length + 1);
2050
+ let iterations = 0;
2051
+ for (; iterations < maxPasses && pending.size > 0; iterations += 1) {
2052
+ let progress = false;
2053
+ const waiting = /* @__PURE__ */ new Set();
2054
+ for (const [key, subject] of pending) {
2055
+ const result = await buildScriptablePack({
2056
+ definition: subject.definition,
2057
+ sourcePath: subject.sourcePath,
2058
+ ...subject.subjectPackageId === void 0 ? {} : { subjectPackageId: subject.subjectPackageId },
2059
+ ...subject.values === void 0 ? {} : { values: subject.values },
2060
+ ...subject.inheritedValues === void 0 ? {} : { inheritedValues: subject.inheritedValues },
2061
+ ...subject.sourceClosure === void 0 ? {} : { sourceClosure: subject.sourceClosure },
2062
+ outputs: options.outputs,
2063
+ ...options.cookers === void 0 ? {} : { cookers: options.cookers },
2064
+ assetSource: stagedSource(staged, options.assetSource),
2065
+ availableGuids: /* @__PURE__ */ new Set([...availableGuids, ...staged.keys()]),
2066
+ deferReferenceValidation: true
1888
2067
  });
1889
- return {
1890
- payload: { kind: graph.kind, root: graph.root, nodes },
1891
- refs,
1892
- artifacts: { body: jsonArtifact(graph) }
1893
- };
1894
- }
1895
- case "audio": {
1896
- const audio = asset;
1897
- return {
1898
- payload: {
1899
- kind: audio.kind,
1900
- sourceKey: audio.sourceKey,
1901
- mediaType: audio.mediaType,
1902
- bytes: audio.bytes.slice()
1903
- },
1904
- refs: [],
1905
- artifacts: {
1906
- source: {
1907
- mediaType: audio.mediaType,
1908
- assetCodec: { name: "browser-audio", version: "1" },
1909
- bytes: audio.bytes.slice()
1910
- }
1911
- }
1912
- };
2068
+ if (result.ok) {
2069
+ results.set(key, result.value);
2070
+ for (const output of result.value.stagedOutputs)
2071
+ staged.set(AssetGuid.format(output.guid).toLowerCase(), output);
2072
+ pending.delete(key);
2073
+ progress = true;
2074
+ continue;
2075
+ }
2076
+ if (errorCode(result.error) === "asset-not-found") {
2077
+ const detail = record(result.error) && record(result.error.detail) ? result.error.detail : void 0;
2078
+ const guid = detail !== void 0 && typeof detail.guid === "string" ? detail.guid : void 0;
2079
+ if (guid !== void 0) waiting.add(guid.toLowerCase());
2080
+ continue;
2081
+ }
2082
+ return result;
1913
2083
  }
1914
- case "particle-effect": {
1915
- const effect = asset;
1916
- const cooked = particleProgramArtifact(effect);
1917
- return {
1918
- payload: { ...effect, programFingerprint: cooked.fingerprint, program: cooked.program },
1919
- refs: [],
1920
- artifacts: {
1921
- "particle-effect/program.json": {
1922
- mediaType: "application/json",
1923
- assetCodec: { name: "forgeax-vfx-program", version: effect.program.format },
1924
- bytes: cooked.bytes
1925
- }
2084
+ if (pending.size === 0) break;
2085
+ if (!progress) {
2086
+ return err({
2087
+ code: "pack-content-dependency-stalled",
2088
+ expected: "the content dependency worklist to make progress",
2089
+ hint: "inspect waitingGuids and repair the missing output or content-read cycle, then rebuild",
2090
+ detail: {
2091
+ waitingGuids: [...waiting].sort(),
2092
+ pendingSubjects: [...pending.values()].map((subject) => subject.sourcePath).sort(),
2093
+ iterations: iterations + 1
1926
2094
  }
1927
- };
2095
+ });
1928
2096
  }
1929
- case "ies-profile": {
1930
- const profile = asset;
1931
- return {
1932
- payload: profile,
1933
- refs: [],
1934
- artifacts: {
1935
- body: {
1936
- mediaType: "application/octet-stream",
1937
- assetCodec: { name: "forgeax-ies-profile", version: "1" },
1938
- bytes: bytes(profile.data)
1939
- }
1940
- }
1941
- };
2097
+ }
2098
+ if (pending.size > 0) {
2099
+ return err({
2100
+ code: "pack-content-dependency-stalled",
2101
+ expected: "the content dependency worklist to finish within its bounded retry budget",
2102
+ hint: "inspect pendingSubjects and waitingGuids, then repair the dependency graph",
2103
+ detail: {
2104
+ pendingSubjects: [...pending.values()].map((subject) => subject.sourcePath).sort(),
2105
+ waitingGuids: [],
2106
+ iterations
2107
+ }
2108
+ });
2109
+ }
2110
+ const knownGuids = /* @__PURE__ */ new Set([...availableGuids, ...staged.keys()]);
2111
+ const missingReferences = /* @__PURE__ */ new Set();
2112
+ for (const product of results.values()) {
2113
+ const contentReads = new Set(product.externalEvidence.map((read) => read.guid.toLowerCase()));
2114
+ for (const asset of product.product.assets) {
2115
+ for (const reference of asset.refs) {
2116
+ const guid = reference.guid.toLowerCase();
2117
+ if (!knownGuids.has(guid) && !contentReads.has(guid)) missingReferences.add(guid);
2118
+ }
1942
2119
  }
1943
- case "material":
1944
- case "mesh":
1945
- case "scene":
1946
- throw new TypeError(`ordinary producer received already-owned ${asset.kind} asset`);
1947
2120
  }
1948
- }
1949
- var materialAssetOutputProducer = createSafeProducer(
1950
- "material",
1951
- "material-pack/2",
1952
- materialProduct
1953
- );
1954
- var meshAssetOutputProducer = createSafeProducer("mesh", "mesh-binary/4", meshProduct);
1955
- var textureAssetOutputProducer = createSafeProducer(
1956
- "texture",
1957
- "texture-pack/1",
1958
- textureProduct
1959
- );
1960
- function createSceneAssetOutputProducer(sceneComponents = []) {
1961
- const schemas = new Map(
1962
- sceneComponents.map((component) => [component.name, component.fields])
1963
- );
1964
- return createSafeProducer("scene", "scene-pack/3", (input) => sceneProduct(input, schemas));
1965
- }
1966
- function createPreExternalizedSceneAssetOutputProducer() {
1967
- return createSafeProducer("scene", "scene-pack/3", preExternalizedSceneProduct);
1968
- }
1969
- function createStandardAssetOutputProducerRegistry(sceneComponents = []) {
1970
- const registry = new AssetOutputProducerRegistry();
1971
- registry.register(materialAssetOutputProducer);
1972
- registry.register(meshAssetOutputProducer);
1973
- registry.register(textureAssetOutputProducer);
1974
- registry.register(createSceneAssetOutputProducer(sceneComponents));
1975
- for (const kind of [
1976
- "equirect",
1977
- "sampler",
1978
- "font",
1979
- "render-pipeline",
1980
- "tileset",
1981
- "video",
1982
- "skeleton",
1983
- "skin",
1984
- "animation-clip",
1985
- "animation-graph",
1986
- "audio",
1987
- "particle-effect",
1988
- "ies-profile"
1989
- ]) {
1990
- registry.register(createSafeProducer(kind, "ordinary-pod/1", ordinaryPodProduct));
2121
+ if (missingReferences.size > 0) {
2122
+ return err(
2123
+ referenceError("pack-output-reference-missing", "worklist", [...missingReferences].sort())
2124
+ );
1991
2125
  }
1992
- return registry;
2126
+ const removed = [...options.incomingRefs?.keys() ?? []].filter(
2127
+ (guid) => !staged.has(guid.toLowerCase())
2128
+ );
2129
+ const referencedRemoved = removed.filter(
2130
+ (guid) => (options.incomingRefs?.get(guid) ?? []).length > 0
2131
+ );
2132
+ if (referencedRemoved.length > 0)
2133
+ return err(referenceError("pack-output-reference-conflict", "worklist", referencedRemoved));
2134
+ return ok({
2135
+ products: [...results.values()].map((result) => result.product),
2136
+ buildProducts: [...results.values()],
2137
+ stagedOutputs: [...staged.values()],
2138
+ iterations: pending.size === 0 && options.subjects.length > 0 ? iterations + 1 : iterations
2139
+ });
1993
2140
  }
1994
2141
  function parseProducerReadiness(value) {
1995
2142
  if (value === void 0 || value === "before-consume" || value === "on-demand") {
@@ -2779,7 +2926,7 @@ async function produceScriptablePackProducts(options) {
2779
2926
  }
2780
2927
  return ok(prepared);
2781
2928
  }
2782
- async function scriptablePackResourceRevision(displaySourcePath, digest3, sourceClosure) {
2929
+ async function scriptablePackResourceRevision(displaySourcePath, digest, sourceClosure) {
2783
2930
  const mtimes = await Promise.all(
2784
2931
  sourceClosure.map(async (entry) => {
2785
2932
  try {
@@ -2793,7 +2940,7 @@ async function scriptablePackResourceRevision(displaySourcePath, digest3, source
2793
2940
  })
2794
2941
  );
2795
2942
  const observedAt = mtimes.length === 0 ? 0 : Math.trunc(Math.max(...mtimes));
2796
- return { digest: digest3, observedAt, rootId: displaySourcePath };
2943
+ return { digest, observedAt, rootId: displaySourcePath };
2797
2944
  }
2798
2945
  function normalizeCatalogPath(path) {
2799
2946
  return path.replaceAll("\\", "/").replace(/^\.\//, "");
@@ -2826,16 +2973,11 @@ function runtimePublicationFor(context, input) {
2826
2973
  ...input.digest === void 0 ? {} : { digest: input.digest },
2827
2974
  ...input.inputFingerprint === void 0 ? {} : { inputFingerprint: input.inputFingerprint },
2828
2975
  ...input.outputs === void 0 ? {} : { outputs: input.outputs },
2976
+ ...input.sourceKeys === void 0 ? {} : { sourceKeys: input.sourceKeys },
2829
2977
  ...input.externalEvidence === void 0 ? {} : { externalEvidence: input.externalEvidence },
2830
2978
  generation: context.runtimeBinding?.generation ?? context.generation
2831
2979
  };
2832
- if (input.sourceKeys === void 0) return createRuntimePackPublication(publicationInput);
2833
- const derived = createRuntimePackPublication(publicationInput);
2834
- const outputs = derived.publication.outputs.map((output) => ({
2835
- ...output,
2836
- sourceKey: input.sourceKeys?.get(output.guid.toLowerCase()) ?? output.sourceKey
2837
- }));
2838
- return createRuntimePackPublication({ ...publicationInput, outputs });
2980
+ return createRuntimePackPublication(publicationInput);
2839
2981
  }
2840
2982
  function sourceKeysFor(declarations) {
2841
2983
  return new Map(
@@ -3234,6 +3376,7 @@ async function emitAuthoredPack(work, guidSeen, entry, availableGuids) {
3234
3376
  };
3235
3377
  const runtimePublication = runtimePublicationFor(work.context, {
3236
3378
  pack: authoredPack,
3379
+ sourceKeys: sourceKeysFor(legacy.assets),
3237
3380
  sourcePath: entry.sourcePath,
3238
3381
  sourceRevision: declaration.sourceRevision,
3239
3382
  packageUrl: prepared.finalized?.packageUrl ?? `${work.context.basePrefix === "/" ? "" : work.context.basePrefix}/assets/${outputGuid}.pack.json`,
@@ -3816,25 +3959,6 @@ var ImporterRegistry = class {
3816
3959
  function isRecord2(value) {
3817
3960
  return value !== null && typeof value === "object" && !Array.isArray(value);
3818
3961
  }
3819
- function stable2(value) {
3820
- if (value instanceof ArrayBuffer) {
3821
- return `ArrayBuffer:${JSON.stringify(Array.from(new Uint8Array(value)))}`;
3822
- }
3823
- if (ArrayBuffer.isView(value)) {
3824
- return `${value.constructor.name}:${JSON.stringify(
3825
- Array.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength))
3826
- )}`;
3827
- }
3828
- if (Array.isArray(value)) return `[${value.map(stable2).join(",")}]`;
3829
- if (value !== null && typeof value === "object") {
3830
- const record2 = value;
3831
- return `{${Object.keys(record2).sort().map((key) => `${JSON.stringify(key)}:${stable2(record2[key])}`).join(",")}}`;
3832
- }
3833
- return JSON.stringify(value) ?? "null";
3834
- }
3835
- function digest2(value) {
3836
- return `sha256:${createHash("sha256").update(stable2(value)).digest("hex")}`;
3837
- }
3838
3962
  function generationFromDigest(value) {
3839
3963
  const parsed = Number.parseInt(value.slice("sha256:".length, "sha256:".length + 8), 16);
3840
3964
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 1;
@@ -3937,7 +4061,7 @@ function assetFromRow(path, row) {
3937
4061
  });
3938
4062
  }
3939
4063
  const asset = { ...row.payload, kind: row.kind };
3940
- return ok({ asset, digest: digest2(asset) });
4064
+ return ok({ asset, digest: scriptablePackFingerprint(asset) });
3941
4065
  }
3942
4066
  async function indexPackFiles(assetRoots) {
3943
4067
  const files = await collectPackFiles(assetRoots);
@@ -3998,9 +4122,7 @@ async function indexPackFiles(assetRoots) {
3998
4122
  byGuid.set(key, asset.value);
3999
4123
  }
4000
4124
  }
4001
- const sourceDigest = digest2(
4002
- [...documents].sort((left, right) => stable2(left).localeCompare(stable2(right)))
4003
- );
4125
+ const sourceDigest = scriptablePackFingerprint(documents.map(scriptablePackFingerprint).sort());
4004
4126
  return ok({
4005
4127
  generation: generationFromDigest(sourceDigest),
4006
4128
  assets: byGuid
@@ -4030,22 +4152,6 @@ function createScriptablePackFileAssetSnapshotSource(options) {
4030
4152
  }
4031
4153
  };
4032
4154
  }
4033
- function stable3(value) {
4034
- if (ArrayBuffer.isView(value)) {
4035
- return `${value.constructor.name}:${JSON.stringify(Array.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)))}`;
4036
- }
4037
- if (Array.isArray(value)) return `[${value.map(stable3).join(",")}]`;
4038
- if (value !== null && typeof value === "object") {
4039
- const record2 = value;
4040
- return `{${Object.keys(record2).sort().map((key) => `${JSON.stringify(key)}:${stable3(record2[key])}`).join(",")}}`;
4041
- }
4042
- return JSON.stringify(value) ?? "null";
4043
- }
4044
- async function assetDigest(asset) {
4045
- const bytes2 = new TextEncoder().encode(stable3(asset));
4046
- const digest3 = await globalThis.crypto.subtle.digest("SHA-256", bytes2);
4047
- return `sha256:${Array.from(new Uint8Array(digest3), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
4048
- }
4049
4155
  function cycleError(stack, owner, guid) {
4050
4156
  const cycleStart = stack.indexOf(owner);
4051
4157
  const cycle = [...stack.slice(cycleStart), owner];
@@ -4123,7 +4229,7 @@ function createScriptablePackStagedAssetSnapshotSource(options) {
4123
4229
  next.set(outputGuid, {
4124
4230
  asset: structuredClone(output.asset),
4125
4231
  generation: options.generation,
4126
- digest: output.digest ?? await assetDigest(output.asset)
4232
+ digest: output.digest ?? await scriptablePackFingerprint(output.asset)
4127
4233
  });
4128
4234
  }
4129
4235
  for (const declared of owner.guids) {
@@ -4537,8 +4643,8 @@ function validatePublicationArtifactClosure(input) {
4537
4643
  return ok(publicationArtifacts(input.transport));
4538
4644
  }
4539
4645
  function projectImportPublication(input, head, observedAt) {
4540
- const digest3 = head.currentKey ?? input.desiredKey;
4541
- const revision = { digest: digest3, observedAt, rootId: input.root };
4646
+ const digest = head.currentKey ?? input.desiredKey;
4647
+ const revision = { digest, observedAt, rootId: input.root };
4542
4648
  const published = new Set(input.publishedGuids.map((guid) => guid.toLowerCase()));
4543
4649
  const catalog = input.nextCatalog.map((row) => {
4544
4650
  if (!published.has(row.guid.toLowerCase())) return row;