@plasius/asset-contracts 0.6.0 → 0.7.0

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/CHANGELOG.md CHANGED
@@ -14,6 +14,23 @@
14
14
  - **Security**
15
15
  - (placeholder)
16
16
 
17
+ ## [0.7.0] - 2026-10-24
18
+
19
+ - **Added**
20
+ - Opt-in reloadable canonical texture/material resource qualification, exact
21
+ storage role/MIME mapping and separate GPU upload/atlas source bindings.
22
+ Preserves authored metadata and original resource identity through JSON reload.
23
+ - (placeholder)
24
+
25
+ - **Changed**
26
+ - (placeholder)
27
+
28
+ - **Fixed**
29
+ - (placeholder)
30
+
31
+ - **Security**
32
+ - (placeholder)
33
+
17
34
  ## [0.6.0] - 2026-10-24
18
35
 
19
36
  - **Added**
@@ -349,3 +366,4 @@
349
366
  [0.4.6]: https://github.com/Plasius-LTD/asset-contracts/releases/tag/v0.4.6
350
367
  [0.5.0]: https://github.com/Plasius-LTD/asset-contracts/releases/tag/v0.5.0
351
368
  [0.6.0]: https://github.com/Plasius-LTD/asset-contracts/releases/tag/v0.6.0
369
+ [0.7.0]: https://github.com/Plasius-LTD/asset-contracts/releases/tag/v0.7.0
package/README.md CHANGED
@@ -28,6 +28,7 @@ The exported surface covers:
28
28
  - asset job metadata contracts
29
29
  - manifest and file descriptor contracts
30
30
  - opt-in canonical model package inventory and exact-byte qualification
31
+ - reloadable canonical texture/material metadata and explicit upload/atlas roles
31
32
  - opt-in public-safe canonical conversion diagnostics and resource/source binding
32
33
  - screenshot plan definitions
33
34
  - review findings and review report contracts
@@ -91,7 +92,7 @@ Embedded resources remain inside their owning entrypoint; list only actual
91
92
  exported files. This helper checks bytes and declarations, not internal format
92
93
  references, MIME sniffing, malware, rendering, storage or promotion authority.
93
94
  Keep source provenance in the canonical document and do not substitute derived
94
- hashes for original source hashes. Detailed role mapping, animation/source
95
+ hashes for original source hashes. Role qualification is available through the additive helper below; animation/source
95
96
  relationships and promotion closure remain separately tracked work. The result
96
97
  is qualification at call time; callers must preserve the exact bytes in their
97
98
  own immutable storage boundary.
@@ -105,6 +106,71 @@ runtime parser or renderer loading. Downstream adoption must use a released
105
106
  package. See [ADR 0009](./docs/adrs/adr-0009-model-resource-manifest-qualification.md)
106
107
  and [TDR 0004](./docs/tdrs/tdr-0004-model-resource-manifest-qualification.md).
107
108
 
109
+ ## Reloadable Texture and Resource Roles
110
+
111
+ `qualifyModelTextureResources` composes exact byte/inventory qualification with
112
+ published model-core's verified material/texture graph. It keeps authored
113
+ metadata and source image identity separate from derived GPU uploads/atlases.
114
+
115
+ ```ts
116
+ import { qualifyModelTextureResources } from "@plasius/asset-contracts";
117
+
118
+ const reload = await qualifyModelTextureResources({
119
+ manifest, packageManifest, files: exportedFiles,
120
+ graph: verifiedCanonicalGraph,
121
+ resources: [
122
+ { resourceId: "source-image", filePath: "textures/source.png" },
123
+ { resourceId: "embedded-buffer", filePath: "model.gltf" },
124
+ ], // Exactly one binding per graph resource.
125
+ derived: [
126
+ { role: "gpu-upload", filePath: "derived/upload.bin", sourceResourceIds: ["source-image"] },
127
+ { role: "texture-atlas", filePath: "derived/atlas.png", sourceResourceIds: ["source-image"] },
128
+ ], // Or [] when no derivative is exported.
129
+ }, { signal: abortController.signal, timeoutMs: 10_000 });
130
+ // Persist JSON.stringify(reload) alongside the qualified immutable bytes.
131
+ ```
132
+
133
+ Use existing storage roles: canonical texture → texture with PNG/JPEG/WebP/KTX2
134
+ MIME; material/animation → metadata with application/json; mesh → model or
135
+ binary; auxiliary → binary or JSON metadata. Uploads require auxiliary/binary
136
+ buffer files, atlases texture/image files. See TDR 0006 for the exact MIME table.
137
+ Missing/duplicate resources, incompatible roles/MIME, unknown roles, graph
138
+ clones and source/output collisions reject. External files match graph path,
139
+ hash, byte length and MIME; embedded resources name the owning model entrypoint
140
+ and retain their independent resource hash. List only actual exported files.
141
+
142
+ The frozen version-1 result contains manifest, original source provenance,
143
+ resources, canonical materials/textures and derived records. Authored usage,
144
+ color space, sampler, binding transform, embedding policy and sourceMetadata
145
+ survive JSON reload. Original image IDs and bytes are never replaced by atlas
146
+ hashes or upload paths. No Blob payload or renderer allocation is serialized.
147
+ Adapters own model-internal references, derivation algorithms and image content
148
+ inspection; qualification does not attest those operations or grant promotion.
149
+ Task 12 animation schemas, Task 23 shader ABI and Task 51 promotion closure
150
+ remain separate.
151
+
152
+ This is **trusted package metadata**, including names, extensions, original
153
+ paths and authored sourceMetadata. Minimize/sanitize that data before public
154
+ distribution; use the separate public diagnostics API for technical responses.
155
+ Serialized graphs lose their private brand: verify reloaded canonical resources
156
+ and reconstruct a graph through core before qualifying again. Produce graphs
157
+ with the same installed model-core instance and matching module format (ESM
158
+ import with ESM, require with CJS). The normal named guard import preserves that
159
+ identity; unused exports remain tree-shakeable through sideEffects=false.
160
+
161
+ Inputs/bytes are snapshotted before awaiting. Fixed ceilings are 16,384 resource
162
+ bindings, 4,096 derivatives, 65,536 graph nodes, shared 262,144 metadata values,
163
+ 2,097,152 UTF-16 characters, depth 16 and 2,048 characters/string. Existing file
164
+ limits also apply. Default deadline 10 seconds, allowed range 1–60,000 ms;
165
+ cancellation/deadline reject without a partial result. In-flight Web Crypto
166
+ cannot be cancelled. ModelTextureResourceError.code is value-free.
167
+
168
+ The backend evaluator gpu.model.conversion.enabled controls caller adoption;
169
+ disabled/rollback paths retain legacy factories. No lookup/mutation or UI
170
+ capability is introduced. Dependencies and legacy factories are unchanged.
171
+ See [ADR 0011](./docs/adrs/adr-0011-reloadable-texture-resource-roles.md) and
172
+ [TDR 0006](./docs/tdrs/tdr-0006-reloadable-texture-resource-roles.md).
173
+
108
174
  ## Public Conversion Diagnostics
109
175
 
110
176
  `qualifyModelConversionDiagnostics` adds an opt-in, version-1 JSON boundary over
package/dist/index.cjs CHANGED
@@ -65,9 +65,11 @@ __export(index_exports, {
65
65
  MODEL_RESOLUTION_V2_STATES: () => MODEL_RESOLUTION_V2_STATES,
66
66
  MODEL_RESOURCE_MANIFEST_LIMITS: () => MODEL_RESOURCE_MANIFEST_LIMITS,
67
67
  MODEL_RIGHTS_STATUSES: () => MODEL_RIGHTS_STATUSES,
68
+ MODEL_TEXTURE_RESOURCE_LIMITS: () => MODEL_TEXTURE_RESOURCE_LIMITS,
68
69
  MODEL_TEXT_ONLY_ASSURANCE_CEILING_REASON_CODE: () => MODEL_TEXT_ONLY_ASSURANCE_CEILING_REASON_CODE,
69
70
  ModelConversionDiagnosticsError: () => ModelConversionDiagnosticsError,
70
71
  ModelResourceManifestError: () => ModelResourceManifestError,
72
+ ModelTextureResourceError: () => ModelTextureResourceError,
71
73
  PVOX_ASSEMBLY_CLOSURE_HASH_DOMAIN: () => PVOX_ASSEMBLY_CLOSURE_HASH_DOMAIN,
72
74
  PVOX_ASSET_MANIFEST_VERSION: () => PVOX_ASSET_MANIFEST_VERSION,
73
75
  PVOX_BINARY_CLOSURE_HASH_DOMAIN: () => PVOX_BINARY_CLOSURE_HASH_DOMAIN,
@@ -260,6 +262,7 @@ __export(index_exports, {
260
262
  projectPvoxNamedJsonClosureV1: () => projectPvoxNamedJsonClosureV1,
261
263
  qualifyModelConversionDiagnostics: () => qualifyModelConversionDiagnostics,
262
264
  qualifyModelResourceManifest: () => qualifyModelResourceManifest,
265
+ qualifyModelTextureResources: () => qualifyModelTextureResources,
263
266
  toLegacyModelResolutionState: () => toLegacyModelResolutionState,
264
267
  validateGpuAssetFiles: () => validateGpuAssetFiles
265
268
  });
@@ -448,11 +451,11 @@ function assertNoDirectUrl(value, fieldName) {
448
451
  }
449
452
  }
450
453
  function requiredToken(value, fieldName) {
451
- const token2 = requiredString(value, fieldName, 128);
452
- if (!TOKEN_PATTERN.test(token2)) {
454
+ const token3 = requiredString(value, fieldName, 128);
455
+ if (!TOKEN_PATTERN.test(token3)) {
453
456
  throw new Error(`${fieldName} must be a token up to 128 characters.`);
454
457
  }
455
- return token2;
458
+ return token3;
456
459
  }
457
460
  function requiredModelPathSegment(value, fieldName) {
458
461
  const segment = requiredString(value, fieldName, 128);
@@ -6953,9 +6956,9 @@ function snapshot(value, budget, depth = 0) {
6953
6956
  return value;
6954
6957
  }
6955
6958
  if (Array.isArray(value)) return Object.freeze(list(value).map((item) => snapshot(item, budget, depth + 1)));
6956
- const fields2 = record(value);
6957
- for (const key of Object.keys(fields2)) fields2[key] = snapshot(fields2[key], budget, depth + 1);
6958
- return Object.freeze(fields2);
6959
+ const fields3 = record(value);
6960
+ for (const key of Object.keys(fields3)) fields3[key] = snapshot(fields3[key], budget, depth + 1);
6961
+ return Object.freeze(fields3);
6959
6962
  }
6960
6963
  function path(value) {
6961
6964
  if (typeof value !== "string" || value.length > 1024 || !value.length) fail();
@@ -7064,6 +7067,176 @@ async function qualify(input) {
7064
7067
  return Object.freeze({ ...manifest, files: Object.freeze([...manifest.files].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) });
7065
7068
  }
7066
7069
 
7070
+ // src/model-texture-resources.ts
7071
+ var import_gpu_model_core = require("@plasius/gpu-model-core");
7072
+ var MODEL_TEXTURE_RESOURCE_LIMITS = Object.freeze({ resources: 16384, derived: 4096, nodes: 65536, values: 262144, characters: 2097152, depth: 16, timeoutMs: 6e4 });
7073
+ var ModelTextureResourceError = class extends TypeError {
7074
+ constructor(code) {
7075
+ super(`Invalid model texture resources: ${code}`);
7076
+ this.code = code;
7077
+ this.name = "ModelTextureResourceError";
7078
+ }
7079
+ code;
7080
+ };
7081
+ function fail2(code = "invalid-input") {
7082
+ throw new ModelTextureResourceError(code);
7083
+ }
7084
+ function fields(value, allowed) {
7085
+ if (!value || typeof value !== "object" || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) fail2();
7086
+ const keys = Reflect.ownKeys(value);
7087
+ if (keys.length > 1024) fail2("limit-exceeded");
7088
+ const result = /* @__PURE__ */ Object.create(null);
7089
+ for (const key of keys) {
7090
+ if (typeof key !== "string" || key.length > 128 || ["__proto__", "prototype", "constructor"].includes(key) || allowed && !allowed.includes(key)) fail2();
7091
+ const property = Object.getOwnPropertyDescriptor(value, key);
7092
+ if (!("value" in property) || !property.enumerable) fail2();
7093
+ result[key] = property.value;
7094
+ }
7095
+ return result;
7096
+ }
7097
+ function array(value, maximum) {
7098
+ if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) fail2();
7099
+ if (value.length > maximum) fail2("limit-exceeded");
7100
+ if (Reflect.ownKeys(value).length !== value.length + 1) fail2();
7101
+ const result = [];
7102
+ for (let i = 0; i < value.length; i++) {
7103
+ const property = Object.getOwnPropertyDescriptor(value, String(i));
7104
+ if (!property || !("value" in property) || !property.enumerable) fail2();
7105
+ result.push(property.value);
7106
+ }
7107
+ return result;
7108
+ }
7109
+ function snapshot2(value, budget, depth = 0) {
7110
+ if (--budget.values < 0 || depth > MODEL_TEXTURE_RESOURCE_LIMITS.depth) fail2("limit-exceeded");
7111
+ if (value === null || value === void 0 || typeof value === "boolean") return value;
7112
+ if (typeof value === "number" && Number.isFinite(value)) return value;
7113
+ if (typeof value === "string") {
7114
+ budget.characters -= value.length;
7115
+ if (value.length > 2048 || budget.characters < 0) fail2("limit-exceeded");
7116
+ return value;
7117
+ }
7118
+ if (Array.isArray(value)) return Object.freeze(array(value, MODEL_TEXTURE_RESOURCE_LIMITS.resources).map((v) => snapshot2(v, budget, depth + 1)));
7119
+ const result = fields(value);
7120
+ for (const key of Object.keys(result)) {
7121
+ budget.characters -= key.length;
7122
+ if (budget.characters < 0) fail2("limit-exceeded");
7123
+ result[key] = snapshot2(result[key], budget, depth + 1);
7124
+ }
7125
+ return Object.freeze(result);
7126
+ }
7127
+ function token2(value) {
7128
+ if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value)) fail2();
7129
+ return value;
7130
+ }
7131
+ var compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
7132
+ var imageMime = (value) => ["image/png", "image/jpeg", "image/webp", "image/ktx2"].includes(value);
7133
+ var bufferMime = (value) => ["application/octet-stream", "application/vnd.plasius.gpu-buffer"].includes(value);
7134
+ function storageRole(role, file) {
7135
+ const valid = role === "texture" ? file.role === "texture" && imageMime(file.contentType) : role === "material" || role === "animation" ? file.role === "metadata" && file.contentType === "application/json" : role === "mesh" ? file.role === "binary" && bufferMime(file.contentType) || file.role === "model" && (file.contentType.startsWith("model/") || ["application/json", "application/octet-stream", "text/plain"].includes(file.contentType)) : role === "auxiliary" && (file.role === "binary" && bufferMime(file.contentType) || file.role === "metadata" && file.contentType === "application/json");
7136
+ if (!valid) fail2("role-mismatch");
7137
+ }
7138
+ async function qualifyModelTextureResources(input, options = {}) {
7139
+ let timer;
7140
+ let signal;
7141
+ let onAbort;
7142
+ try {
7143
+ const started = performance.now();
7144
+ const controls = fields(options, ["signal", "timeoutMs"]);
7145
+ const timeoutMs = controls.timeoutMs === void 0 ? 1e4 : controls.timeoutMs;
7146
+ if (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MODEL_TEXTURE_RESOURCE_LIMITS.timeoutMs) fail2();
7147
+ signal = controls.signal;
7148
+ const aborted = Object.getOwnPropertyDescriptor(AbortSignal.prototype, "aborted").get;
7149
+ const check = () => {
7150
+ if (signal !== void 0 && aborted.call(signal)) fail2("aborted");
7151
+ if (performance.now() - started >= timeoutMs) fail2("deadline-exceeded");
7152
+ };
7153
+ check();
7154
+ const root = fields(input, ["manifest", "packageManifest", "files", "graph", "resources", "derived"]);
7155
+ const graph = root.graph;
7156
+ if (!(0, import_gpu_model_core.isGpuModelResourceGraph)(graph)) fail2("unverified-graph");
7157
+ if (graph.nodes.length > MODEL_TEXTURE_RESOURCE_LIMITS.nodes) fail2("limit-exceeded");
7158
+ const budget = { values: MODEL_TEXTURE_RESOURCE_LIMITS.values, characters: MODEL_TEXTURE_RESOURCE_LIMITS.characters };
7159
+ const inventory = snapshot2(root.packageManifest, budget);
7160
+ const bindings = array(snapshot2(root.resources, budget), MODEL_TEXTURE_RESOURCE_LIMITS.resources).map((value) => {
7161
+ const binding = fields(value, ["resourceId", "filePath"]);
7162
+ return Object.freeze({ resourceId: token2(binding.resourceId), filePath: tokenPath(binding.filePath) });
7163
+ });
7164
+ const derived = array(snapshot2(root.derived, budget), MODEL_TEXTURE_RESOURCE_LIMITS.derived).map((value) => {
7165
+ const record2 = fields(value, ["role", "filePath", "sourceResourceIds"]);
7166
+ if (record2.role !== "gpu-upload" && record2.role !== "texture-atlas") fail2("role-mismatch");
7167
+ const ids = array(record2.sourceResourceIds, MODEL_TEXTURE_RESOURCE_LIMITS.resources).map(token2);
7168
+ if (!ids.length || new Set(ids).size !== ids.length) fail2("resource-mismatch");
7169
+ return Object.freeze({ role: record2.role, filePath: tokenPath(record2.filePath), sourceResourceIds: Object.freeze(ids.sort(compare)) });
7170
+ });
7171
+ check();
7172
+ const interrupted = new Promise((_resolve, reject) => {
7173
+ timer = setTimeout(() => reject(new ModelTextureResourceError("deadline-exceeded")), Math.max(1, timeoutMs - (performance.now() - started)));
7174
+ onAbort = () => reject(new ModelTextureResourceError("aborted"));
7175
+ if (signal) AbortSignal.prototype.addEventListener.call(signal, "abort", onAbort, { once: true });
7176
+ });
7177
+ const manifest = await Promise.race([
7178
+ qualifyModelResourceManifest({ manifest: root.manifest, files: root.files, packageManifest: inventory, signal, timeoutMs }),
7179
+ interrupted
7180
+ ]);
7181
+ check();
7182
+ const files = new Map(manifest.files.map((file) => [file.path, file]));
7183
+ const roles = new Map(inventory.entries.map((entry) => [entry.path, entry.role]));
7184
+ for (const entry of inventory.entries) storageRole(entry.role, files.get(entry.path));
7185
+ const resources = new Map(graph.nodes.filter((node) => node.kind === "resource").map((node) => [node.id, node.value]));
7186
+ if (bindings.length !== resources.size || new Set(bindings.map((b) => b.resourceId)).size !== bindings.length) fail2("resource-mismatch");
7187
+ const resourceFiles = /* @__PURE__ */ new Set();
7188
+ const qualified = bindings.map((binding) => {
7189
+ const value = resources.get(binding.resourceId);
7190
+ const file = files.get(binding.filePath);
7191
+ if (!value || !file) fail2("resource-mismatch");
7192
+ const { resource, location, exportEmbeddingPolicy } = value;
7193
+ if (location.kind === "external") {
7194
+ if (binding.filePath !== location.relativePath || file.sha256 !== resource.contentHash || file.byteLength !== resource.byteLength || file.contentType !== resource.mimeType) fail2("resource-mismatch");
7195
+ if (resource.kind === "image" ? roles.get(file.path) !== "texture" : file.role !== "binary") fail2("role-mismatch");
7196
+ } else if (binding.filePath !== manifest.entrypoint || file.role !== "model") fail2("resource-mismatch");
7197
+ resourceFiles.add(binding.filePath);
7198
+ return Object.freeze({ ...binding, kind: resource.kind, contentHash: resource.contentHash, byteLength: resource.byteLength, mimeType: resource.mimeType, location, exportEmbeddingPolicy });
7199
+ });
7200
+ const outputFiles = /* @__PURE__ */ new Set();
7201
+ for (const output of derived) {
7202
+ const file = files.get(output.filePath);
7203
+ if (!file || outputFiles.has(output.filePath) || resourceFiles.has(output.filePath)) fail2("resource-mismatch");
7204
+ outputFiles.add(output.filePath);
7205
+ for (const id of output.sourceResourceIds) {
7206
+ const source2 = resources.get(id);
7207
+ if (!source2 || output.role === "texture-atlas" && source2.resource.kind !== "image") fail2("resource-mismatch");
7208
+ }
7209
+ if (output.role === "gpu-upload" ? file.role !== "binary" || !bufferMime(file.contentType) || roles.get(file.path) !== "auxiliary" : file.role !== "texture" || !imageMime(file.contentType) || roles.get(file.path) !== "texture") fail2("role-mismatch");
7210
+ }
7211
+ const materials = graph.nodes.filter((node) => node.kind === "material").map((node) => node.value);
7212
+ const textures = graph.nodes.filter((node) => node.kind === "texture").map((node) => node.value);
7213
+ const source = graph.nodes.find((node) => node.kind === "provenance");
7214
+ if (source.kind !== "provenance") fail2();
7215
+ snapshot2({ materials, textures, source: source.value }, budget);
7216
+ check();
7217
+ return Object.freeze({
7218
+ contractVersion: 1,
7219
+ manifest,
7220
+ source: source.value,
7221
+ resources: Object.freeze(qualified.sort((a, b) => compare(a.resourceId, b.resourceId))),
7222
+ materials: Object.freeze(materials),
7223
+ textures: Object.freeze(textures),
7224
+ derived: Object.freeze(derived.sort((a, b) => compare(a.filePath, b.filePath)))
7225
+ });
7226
+ } catch (error) {
7227
+ if (error instanceof ModelTextureResourceError) throw error;
7228
+ if (error instanceof ModelResourceManifestError) return fail2(error.code === "aborted" || error.code === "deadline-exceeded" || error.code === "limit-exceeded" ? error.code : "package-mismatch");
7229
+ return fail2();
7230
+ } finally {
7231
+ if (timer !== void 0) clearTimeout(timer);
7232
+ if (signal && onAbort) AbortSignal.prototype.removeEventListener.call(signal, "abort", onAbort);
7233
+ }
7234
+ }
7235
+ function tokenPath(value) {
7236
+ if (typeof value !== "string" || !value.length || value.length > 1024) fail2();
7237
+ return value;
7238
+ }
7239
+
7067
7240
  // src/model-conversion-diagnostics.ts
7068
7241
  var MODEL_CONVERSION_DIAGNOSTIC_LIMITS = Object.freeze({ issues: 100, resources: 100, values: 16384, depth: 12, string: 2048, characters: 262144, timeoutMs: 6e4 });
7069
7242
  var ModelConversionDiagnosticsError = class extends TypeError {
@@ -7074,50 +7247,50 @@ var ModelConversionDiagnosticsError = class extends TypeError {
7074
7247
  }
7075
7248
  code;
7076
7249
  };
7077
- function fail2(code = "invalid-input") {
7250
+ function fail3(code = "invalid-input") {
7078
7251
  throw new ModelConversionDiagnosticsError(code);
7079
7252
  }
7080
- function fields(value) {
7081
- if (!value || typeof value !== "object" || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) fail2();
7253
+ function fields2(value) {
7254
+ if (!value || typeof value !== "object" || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) fail3();
7082
7255
  const result = /* @__PURE__ */ Object.create(null);
7083
7256
  const keys = Reflect.ownKeys(value);
7084
- if (keys.length > 1024) fail2("limit-exceeded");
7257
+ if (keys.length > 1024) fail3("limit-exceeded");
7085
7258
  for (const key of keys) {
7086
- if (typeof key !== "string" || key.length > 128 || ["__proto__", "prototype", "constructor"].includes(key)) fail2();
7259
+ if (typeof key !== "string" || key.length > 128 || ["__proto__", "prototype", "constructor"].includes(key)) fail3();
7087
7260
  const descriptor = Object.getOwnPropertyDescriptor(value, key);
7088
- if (!("value" in descriptor) || !descriptor.enumerable) fail2();
7261
+ if (!("value" in descriptor) || !descriptor.enumerable) fail3();
7089
7262
  result[key] = descriptor.value;
7090
7263
  }
7091
7264
  return result;
7092
7265
  }
7093
7266
  function only(record2, keys) {
7094
- if (Object.keys(record2).some((key) => !keys.includes(key))) fail2();
7267
+ if (Object.keys(record2).some((key) => !keys.includes(key))) fail3();
7095
7268
  }
7096
- function snapshot2(value, budget, depth = 0) {
7097
- if (--budget.values < 0 || depth > MODEL_CONVERSION_DIAGNOSTIC_LIMITS.depth) fail2("limit-exceeded");
7269
+ function snapshot3(value, budget, depth = 0) {
7270
+ if (--budget.values < 0 || depth > MODEL_CONVERSION_DIAGNOSTIC_LIMITS.depth) fail3("limit-exceeded");
7098
7271
  if (value === null || typeof value === "boolean") return value;
7099
7272
  if (typeof value === "string") {
7100
7273
  budget.characters -= value.length;
7101
- if (value.length > MODEL_CONVERSION_DIAGNOSTIC_LIMITS.string || budget.characters < 0) fail2("limit-exceeded");
7274
+ if (value.length > MODEL_CONVERSION_DIAGNOSTIC_LIMITS.string || budget.characters < 0) fail3("limit-exceeded");
7102
7275
  return value;
7103
7276
  }
7104
7277
  if (typeof value === "number" && Number.isFinite(value)) return value;
7105
7278
  if (Array.isArray(value)) {
7106
- if (Object.getPrototypeOf(value) !== Array.prototype || value.length > 1024) fail2("limit-exceeded");
7107
- if (Reflect.ownKeys(value).length !== value.length + 1) fail2();
7279
+ if (Object.getPrototypeOf(value) !== Array.prototype || value.length > 1024) fail3("limit-exceeded");
7280
+ if (Reflect.ownKeys(value).length !== value.length + 1) fail3();
7108
7281
  const result = [];
7109
7282
  for (let i = 0; i < value.length; i++) {
7110
7283
  const descriptor = Object.getOwnPropertyDescriptor(value, String(i));
7111
- if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) fail2();
7112
- result.push(snapshot2(descriptor.value, budget, depth + 1));
7284
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) fail3();
7285
+ result.push(snapshot3(descriptor.value, budget, depth + 1));
7113
7286
  }
7114
7287
  return Object.freeze(result);
7115
7288
  }
7116
- const record2 = fields(value);
7289
+ const record2 = fields2(value);
7117
7290
  for (const key of Object.keys(record2)) {
7118
7291
  budget.characters -= key.length;
7119
- if (budget.characters < 0) fail2("limit-exceeded");
7120
- record2[key] = snapshot2(record2[key], budget, depth + 1);
7292
+ if (budget.characters < 0) fail3("limit-exceeded");
7293
+ record2[key] = snapshot3(record2[key], budget, depth + 1);
7121
7294
  }
7122
7295
  return Object.freeze(record2);
7123
7296
  }
@@ -7130,34 +7303,34 @@ async function qualifyModelConversionDiagnostics(input, options = {}) {
7130
7303
  let onAbort;
7131
7304
  try {
7132
7305
  const start = performance.now();
7133
- const controls = fields(options);
7306
+ const controls = fields2(options);
7134
7307
  only(controls, ["signal", "timeoutMs"]);
7135
7308
  const timeoutMs = controls.timeoutMs === void 0 ? 1e4 : controls.timeoutMs;
7136
- if (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MODEL_CONVERSION_DIAGNOSTIC_LIMITS.timeoutMs) fail2();
7309
+ if (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MODEL_CONVERSION_DIAGNOSTIC_LIMITS.timeoutMs) fail3();
7137
7310
  signal = controls.signal;
7138
7311
  const aborted = Object.getOwnPropertyDescriptor(AbortSignal.prototype, "aborted").get;
7139
7312
  const check = () => {
7140
- if (signal !== void 0 && aborted.call(signal)) fail2("aborted");
7141
- if (performance.now() - start >= timeoutMs) fail2("deadline-exceeded");
7313
+ if (signal !== void 0 && aborted.call(signal)) fail3("aborted");
7314
+ if (performance.now() - start >= timeoutMs) fail3("deadline-exceeded");
7142
7315
  };
7143
7316
  check();
7144
- const args = fields(snapshot2(input, { values: MODEL_CONVERSION_DIAGNOSTIC_LIMITS.values, characters: MODEL_CONVERSION_DIAGNOSTIC_LIMITS.characters }));
7317
+ const args = fields2(snapshot3(input, { values: MODEL_CONVERSION_DIAGNOSTIC_LIMITS.values, characters: MODEL_CONVERSION_DIAGNOSTIC_LIMITS.characters }));
7145
7318
  only(args, ["outcome", "source", "report", "resources"]);
7146
- if (!["completed", "unsupported", "failed"].includes(args.outcome)) fail2();
7319
+ if (!["completed", "unsupported", "failed"].includes(args.outcome)) fail3();
7147
7320
  const source = createModelProvenance(args.source);
7148
- const reportInput = fields(args.report);
7321
+ const reportInput = fields2(args.report);
7149
7322
  const maximumIssues = MODEL_CONVERSION_DIAGNOSTIC_LIMITS.issues - (args.outcome === "completed" ? 0 : 1);
7150
- if (!Array.isArray(reportInput.issues) || reportInput.issues.length > maximumIssues) fail2("limit-exceeded");
7151
- if (!Array.isArray(args.resources) || args.resources.length > MODEL_CONVERSION_DIAGNOSTIC_LIMITS.resources) fail2("limit-exceeded");
7323
+ if (!Array.isArray(reportInput.issues) || reportInput.issues.length > maximumIssues) fail3("limit-exceeded");
7324
+ if (!Array.isArray(args.resources) || args.resources.length > MODEL_CONVERSION_DIAGNOSTIC_LIMITS.resources) fail3("limit-exceeded");
7152
7325
  const resources = /* @__PURE__ */ new Map();
7153
7326
  const identities = /* @__PURE__ */ new Map();
7154
7327
  for (const value of args.resources) {
7155
- const binding = fields(value);
7328
+ const binding = fields2(value);
7156
7329
  only(binding, ["path", "resource"]);
7157
- if (typeof binding.path !== "string" || !binding.path.startsWith("/") || binding.path === "/" || resources.has(binding.path)) fail2();
7330
+ if (typeof binding.path !== "string" || !binding.path.startsWith("/") || binding.path === "/" || resources.has(binding.path)) fail3();
7158
7331
  const resource = createModelResourceRef(binding.resource);
7159
7332
  const identity = JSON.stringify(resource);
7160
- if (identities.has(resource.uri) && identities.get(resource.uri) !== identity) fail2();
7333
+ if (identities.has(resource.uri) && identities.get(resource.uri) !== identity) fail3();
7161
7334
  identities.set(resource.uri, identity);
7162
7335
  resources.set(binding.path, resource);
7163
7336
  }
@@ -7172,7 +7345,7 @@ async function qualifyModelConversionDiagnostics(input, options = {}) {
7172
7345
  const report = core.evaluateGpuModelDiagnostics(reportInput);
7173
7346
  const diagnostics = report.diagnostics.map((diagnostic) => {
7174
7347
  const resource = resources.get(diagnostic.path);
7175
- if (diagnostic.path !== "/" && !resource) fail2("unbound-resource");
7348
+ if (diagnostic.path !== "/" && !resource) fail3("unbound-resource");
7176
7349
  const base = resource ? { resource } : {};
7177
7350
  switch (diagnostic.kind) {
7178
7351
  case "warning":
@@ -7204,7 +7377,7 @@ async function qualifyModelConversionDiagnostics(input, options = {}) {
7204
7377
  });
7205
7378
  } catch (error) {
7206
7379
  if (error instanceof ModelConversionDiagnosticsError) throw error;
7207
- return fail2();
7380
+ return fail3();
7208
7381
  } finally {
7209
7382
  if (timer !== void 0) clearTimeout(timer);
7210
7383
  if (signal && onAbort) AbortSignal.prototype.removeEventListener.call(signal, "abort", onAbort);
@@ -8015,9 +8188,11 @@ function assertRequiredString(value, fieldName) {
8015
8188
  MODEL_RESOLUTION_V2_STATES,
8016
8189
  MODEL_RESOURCE_MANIFEST_LIMITS,
8017
8190
  MODEL_RIGHTS_STATUSES,
8191
+ MODEL_TEXTURE_RESOURCE_LIMITS,
8018
8192
  MODEL_TEXT_ONLY_ASSURANCE_CEILING_REASON_CODE,
8019
8193
  ModelConversionDiagnosticsError,
8020
8194
  ModelResourceManifestError,
8195
+ ModelTextureResourceError,
8021
8196
  PVOX_ASSEMBLY_CLOSURE_HASH_DOMAIN,
8022
8197
  PVOX_ASSET_MANIFEST_VERSION,
8023
8198
  PVOX_BINARY_CLOSURE_HASH_DOMAIN,
@@ -8210,6 +8385,7 @@ function assertRequiredString(value, fieldName) {
8210
8385
  projectPvoxNamedJsonClosureV1,
8211
8386
  qualifyModelConversionDiagnostics,
8212
8387
  qualifyModelResourceManifest,
8388
+ qualifyModelTextureResources,
8213
8389
  toLegacyModelResolutionState,
8214
8390
  validateGpuAssetFiles
8215
8391
  });