@plasius/asset-contracts 0.3.1 → 0.4.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.
@@ -0,0 +1,3244 @@
1
+ import { canonicalizeGpuContract } from "@plasius/gpu-shader";
2
+ import { assertImmutableAssetVersion } from "./asset-version.js";
3
+ import {
4
+ CANONICAL_MODEL_COORDINATE_SYSTEM,
5
+ MODEL_CONFIRMATION_VIEW_KINDS,
6
+ MODEL_CONFIRMATION_VIEW_SIZE_PX,
7
+ MODEL_REQUEST_MAX_REVISION,
8
+ MODEL_RESOLUTION_STATES,
9
+ createModelAssetRef,
10
+ createModelMatchAssessment,
11
+ createModelProvenance,
12
+ createModelRequestSpec,
13
+ createModelRightsAssessment,
14
+ type ModelAssetKind,
15
+ type ModelAssetRef,
16
+ type ModelBoundsMetres,
17
+ type ModelCandidateAssetRef,
18
+ type ModelConfirmationViews,
19
+ type ModelDimensionsMetres,
20
+ type ModelMatchAssessment,
21
+ type ModelProvenance,
22
+ type ModelRequirementPolicy,
23
+ type ModelResolutionState,
24
+ type ModelRightsAssessment,
25
+ type ModelSoftPreferences,
26
+ type ModelTransform,
27
+ } from "./model-resolution.js";
28
+
29
+ /** Additive contract revision used by PVOX-backed resolution payloads. */
30
+ export const MODEL_RESOLUTION_V2_CONTRACT_VERSION = "2026-08-20.v2" as const;
31
+
32
+ /** Version of the public PVOX asset manifest envelope. */
33
+ export const PVOX_ASSET_MANIFEST_VERSION = "plasius.pvox-asset-manifest/1" as const;
34
+
35
+ /** Version of placement-scoped PVOX edit journals. */
36
+ export const PVOX_EDIT_JOURNAL_VERSION = "plasius.pvox-edit-journal/1" as const;
37
+ export const PVOX_EDIT_JOURNAL_GENESIS_HASH = "0".repeat(64);
38
+ export const PVOX_MAX_EDIT_PATCHES = 4_096 as const;
39
+ export const PVOX_FIDELITY_PROFILE_VERSION = "1.0.0" as const;
40
+ export const PVOX_PHYSICAL_PROPERTY_POLICY_ID = "pvox-physical-properties-v1" as const;
41
+ export const PVOX_PHYSICAL_PROPERTY_POLICY_VERSION = "1.0.0" as const;
42
+ export const PVOX_NATIVE_RENDERER_ID = "gpu-renderer" as const;
43
+ export const PVOX_NATIVE_REVIEW_SETTINGS_ID = "pvox-review-v1" as const;
44
+
45
+ /** Binary PVOX format constants shared by contracts and hosted resource metadata. */
46
+ export const PVOX_FORMAT_VERSION = Object.freeze({ major: 1, minor: 0 } as const);
47
+ export const PVOX_MAGIC = "PVOX" as const;
48
+ export const PVOX_CONTENT_TYPE = "application/vnd.plasius.pvox" as const;
49
+ export const PVOX_FILE_EXTENSION = ".pvox" as const;
50
+ export const PVOX_BRICK_EDGE_VOXELS = 8 as const;
51
+ export const PVOX_PAGE_SIZE_BYTES = 65_536 as const;
52
+ export const PVOX_HEADER_BYTE_LENGTH = 256 as const;
53
+ export const PVOX_DIRECTORY_ENTRY_BYTE_LENGTH = 128 as const;
54
+ export const PVOX_SECTION_ALIGNMENT_BYTES = 256 as const;
55
+ export const PVOX_MAX_SECTIONS = 64 as const;
56
+ export const PVOX_MAX_HIERARCHY_NODES = 1_048_576 as const;
57
+ export const PVOX_MAX_RUNS_PER_BRICK = 512 as const;
58
+ export const PVOX_MAX_LOCAL_SAMPLES_PER_BRICK = 512 as const;
59
+ export const PVOX_MAX_ENCODED_BRICK_PAYLOAD_BYTES = 10_384 as const;
60
+ export const PVOX_MAX_ABSOLUTE_COORDINATE_METRES = 1_048_576 as const;
61
+ /** Conservative fixed-record lower bounds used to reject impossible manifests. */
62
+ export const PVOX_MIN_BRICK_DESCRIPTOR_BYTES = 96 as const;
63
+ /** Occupancy plus active-sample masks retained even by constant render-field bricks. */
64
+ export const PVOX_RENDER_BRICK_MASK_BYTES = 128 as const;
65
+ export const PVOX_MIN_ENCODED_SURFACE_SAMPLE_BYTES = 16 as const;
66
+ /** Collision-field samples contain the governed fixed-width signed-distance value. */
67
+ export const PVOX_MIN_ENCODED_COLLISION_SAMPLE_BYTES = 4 as const;
68
+ /** Collision bricks retain the same bounded 512-voxel occupancy/sample masks. */
69
+ export const PVOX_COLLISION_BRICK_MASK_BYTES = 128 as const;
70
+ export const PVOX_PARTITION_RECORD_BYTES = 128 as const;
71
+ export const PVOX_LOD_RECORD_BYTES = 128 as const;
72
+ export const PVOX_ROOT_RECORD_BYTES = 64 as const;
73
+ export const PVOX_LEVEL_SPAN_RECORD_BYTES = 32 as const;
74
+ export const PVOX_HIERARCHY_NODE_RECORD_BYTES = 32 as const;
75
+ export const PVOX_SURFACE_PROPERTY_RECORD_BYTES = 128 as const;
76
+ export const PVOX_PHYSICAL_PROPERTY_RECORD_BYTES = 256 as const;
77
+ export const PVOX_PHYSICAL_EVIDENCE_RECORD_BYTES = 96 as const;
78
+ export const PVOX_MATERIAL_REGION_RECORD_BYTES = 128 as const;
79
+ export const PVOX_INTERIOR_LAYER_RECORD_BYTES = 64 as const;
80
+ export const PVOX_MASS_PROPERTY_RECORD_BYTES = 128 as const;
81
+ export const PVOX_BOND_RECORD_BYTES = 32 as const;
82
+ export const PVOX_MIN_STATIC_SECTION_COUNT = 13 as const;
83
+ export const PVOX_COLLISION_SECTION_COUNT = 5 as const;
84
+ export const PVOX_PHYSICAL_REGION_INVENTORY_VERSION = "plasius.pvox-physical-region-inventory/1" as const;
85
+ /** Closed v1 page families. Field pages require scoped spatial keys; structural pages forbid them. */
86
+ export const PVOX_PAGE_KINDS = Object.freeze(["metadata", "lod-structure", "render-field", "collision-field"] as const);
87
+ /** Domain separators for the independently implemented PVOX 1.0 hash preimages. */
88
+ export const PVOX_SECTION_HASH_DOMAIN = "PVOX-SECTION-V1\0" as const;
89
+ export const PVOX_DIRECTORY_HASH_DOMAIN = "PVOX-DIRECTORY-V1\0" as const;
90
+ export const PVOX_PAGE_SET_HASH_DOMAIN = "PVOX-PAGESET-V1\0" as const;
91
+ export const PVOX_ROOT_HASH_DOMAIN = "PVOX-ROOT-V1\0" as const;
92
+ export const PVOX_BINARY_CLOSURE_HASH_DOMAIN = "PVOX-BINARY-CLOSURE-V1\0" as const;
93
+ export const PVOX_REQUEST_SEMANTIC_PROFILE_HASH_DOMAIN = "PVOX-REQUEST-SEMANTIC-PROFILE-V1\0" as const;
94
+ export const PVOX_RUNTIME_REQUEST_PROFILE_HASH_DOMAIN = "PVOX-RUNTIME-REQUEST-PROFILE-V1\0" as const;
95
+ export const PVOX_CAPABILITY_SET_HASH_DOMAIN = "PVOX-CAPABILITY-SET-V1\0" as const;
96
+ export const PVOX_PHYSICAL_INVENTORY_HASH_DOMAIN = "PVOX-PHYSICAL-INVENTORY-V1\0" as const;
97
+ export const PVOX_PHYSICAL_EVIDENCE_HASH_DOMAIN = "PVOX-PHYSICAL-EVIDENCE-V1\0" as const;
98
+ export const PVOX_RENDER_EVIDENCE_HASH_DOMAIN = "PVOX-RENDER-EVIDENCE-V1\0" as const;
99
+ export const PVOX_EVALUATION_CLOSURE_HASH_DOMAIN = "PVOX-EVALUATION-CLOSURE-V1\0" as const;
100
+ export const PVOX_PROCESSING_CLOSURE_HASH_DOMAIN = "PVOX-PROCESSING-CLOSURE-V1\0" as const;
101
+ export const PVOX_ASSEMBLY_CLOSURE_HASH_DOMAIN = "PVOX-ASSEMBLY-CLOSURE-V1\0" as const;
102
+ export const PVOX_CONFIRMATION_BINDING_HASH_DOMAIN = "PVOX-CONFIRMATION-BINDING-V1\0" as const;
103
+ export const PVOX_EDIT_OVERLAY_ROOT_HASH_DOMAIN = "PVOX-EDIT-OVERLAY-ROOT-V1\0" as const;
104
+ export const PVOX_EDIT_JOURNAL_HASH_DOMAIN = "PVOX-EDIT-JOURNAL-V1\0" as const;
105
+ export const PVOX_PUBLICATION_HASH_DOMAIN = "PVOX-PUBLICATION-V1\0" as const;
106
+ export const PVOX_HASH_DOMAINS = Object.freeze({
107
+ section: PVOX_SECTION_HASH_DOMAIN,
108
+ directory: PVOX_DIRECTORY_HASH_DOMAIN,
109
+ pageSet: PVOX_PAGE_SET_HASH_DOMAIN,
110
+ root: PVOX_ROOT_HASH_DOMAIN,
111
+ binaryClosure: PVOX_BINARY_CLOSURE_HASH_DOMAIN,
112
+ requestSemanticProfile: PVOX_REQUEST_SEMANTIC_PROFILE_HASH_DOMAIN,
113
+ runtimeRequestProfile: PVOX_RUNTIME_REQUEST_PROFILE_HASH_DOMAIN,
114
+ capabilitySet: PVOX_CAPABILITY_SET_HASH_DOMAIN,
115
+ physicalInventory: PVOX_PHYSICAL_INVENTORY_HASH_DOMAIN,
116
+ physicalEvidence: PVOX_PHYSICAL_EVIDENCE_HASH_DOMAIN,
117
+ renderEvidence: PVOX_RENDER_EVIDENCE_HASH_DOMAIN,
118
+ evaluationClosure: PVOX_EVALUATION_CLOSURE_HASH_DOMAIN,
119
+ processingClosure: PVOX_PROCESSING_CLOSURE_HASH_DOMAIN,
120
+ assemblyClosure: PVOX_ASSEMBLY_CLOSURE_HASH_DOMAIN,
121
+ confirmationBinding: PVOX_CONFIRMATION_BINDING_HASH_DOMAIN,
122
+ editOverlayRoot: PVOX_EDIT_OVERLAY_ROOT_HASH_DOMAIN,
123
+ editJournal: PVOX_EDIT_JOURNAL_HASH_DOMAIN,
124
+ publication: PVOX_PUBLICATION_HASH_DOMAIN,
125
+ } as const);
126
+ /** Machine-readable field order; integer fields are canonical little-endian and digests are raw 32 bytes. */
127
+ export const PVOX_HASH_PREIMAGE_LAYOUTS = Object.freeze({
128
+ section: Object.freeze(["domain", "type:u32le", "version:u16le", "encodedLength:u64le", "exactSectionBytes"] as const),
129
+ directory: Object.freeze(["domain", "entryCount:u16le", "entryBytes:u16le", "exactOrderedDirectoryBytes"] as const),
130
+ pageSet: Object.freeze(["domain", "pageCount:u32le", "ordered(pageIndex:u32le,offset:u64le,length:u32le,pageSha256:raw32)"] as const),
131
+ root: Object.freeze(["domain", "normalizedHeader:exact256bytes(sectionCount-at-12,reserved-58-59-zero,directoryHash-at-176-once,reserved-208-255-zero)", "ordered(sectionType:u32le,sectionVersion:u16le,sectionHash:raw32)"] as const),
132
+ binaryClosure: Object.freeze(["domain", "sourceContentHash:raw32", "canonicalDocumentHash:raw32", "compilationInputHash:raw32", "runtimeRequestProfileHash:raw32", "artifactSha256:raw32", "rootHash:raw32", "directoryHash:raw32", "pageSetHash:raw32"] as const),
133
+ requestSemanticProfile: Object.freeze(["domain", "utf8Length:u32le", "canonicalRequestProjection:rfc8785-utf8"] as const),
134
+ runtimeRequestProfile: Object.freeze(["domain", "utf8Length:u32le", "canonicalRuntimeProfileProjection:rfc8785-utf8"] as const),
135
+ capabilitySet: Object.freeze(["domain", "subjectBinaryClosureHash:raw32", "assessmentCount:u32le", "orderedCanonicalCapabilityAssessments:each-u32-length+rfc8785-utf8"] as const),
136
+ physicalInventory: Object.freeze(["domain", "inventoryVersion:utf8-length-prefixed", "subjectBinaryClosureHash:raw32", "validationEvidenceHash:raw32", "entryCount:u32le", "ordered(regionIndex:u32le,physicalPaletteIndex:u32le,regionId:utf8-length-prefixed,materialId:utf8-length-prefixed)"] as const),
137
+ physicalEvidence: Object.freeze(["domain", "physicalInventoryHash:raw32", "evidenceCount:u32le", "orderedCanonicalEvidence:each-u32-length+rfc8785-utf8"] as const),
138
+ renderEvidence: Object.freeze(["domain", "canonicalRenderEvidenceExcludingEvidenceHashAndAttestationLength:u32le", "canonicalRenderEvidenceExcludingEvidenceHashAndAttestation:rfc8785-utf8"] as const),
139
+ evaluationClosure: Object.freeze(["domain", "fidelityEvidenceHash:raw32", "physicalEvidenceHash:raw32", "capabilitySetHash:raw32", "renderEvidenceHash:raw32"] as const),
140
+ processingClosure: Object.freeze(["domain", "binaryClosureHash:raw32", "requestSemanticProfileHash:raw32", "canonicalTechnicalProfileLength:u32le", "canonicalTechnicalProfile:rfc8785-utf8", "pvoxValidationEvidenceHash:raw32", "physicalInventoryValidationEvidenceHash:raw32", "capabilitySetHash:raw32", "physicalInventoryHash:raw32", "physicalEvidenceHash:raw32", "massPropertiesEvidenceHash:raw32", "bondGraphEvidenceHash:raw32", "interiorLayerEvidenceHash:raw32", "compilerEvidenceHash:raw32", "fidelityEvidenceHash:raw32"] as const),
141
+ assemblyClosure: Object.freeze(["domain", "ownBinaryClosureHash:raw32", "childCount:u32le", "orderedCanonicalChildrenLength:u32le", "orderedCanonicalChildren:rfc8785-utf8"] as const),
142
+ confirmationBinding: Object.freeze(["domain", "canonicalConfirmationSubjectLength:u32le", "canonicalConfirmationSubjectExcluding(bindingHash,bindingHashAttestation):rfc8785-utf8"] as const),
143
+ editOverlayRoot: Object.freeze(["domain", "baseContentHash:raw32", "basePageSetHash:raw32", "placementId:utf8-length-prefixed", "gridVersion:utf8-length-prefixed", "expectedRevision:u64le", "resultingRevision:u64le", "previousJournalHash:raw32", "expectedRootHash:raw32", "patchCount:u32le", "orderedFullPatchAddressAndTransitions:rfc8785-each-u32-length"] as const),
144
+ editJournal: Object.freeze(["domain", "canonicalJournalLength:u32le", "canonicalJournalExcluding(journalHash,journalHashAttestation,resultingRootHashAttestation):rfc8785-utf8"] as const),
145
+ publication: Object.freeze(["domain", "canonicalPublicationLength:u32le", "canonicalPointerLastPublicationReceiptExcluding(publicationHash,publicationHashAttestation,publicationToken):rfc8785-utf8"] as const),
146
+ } as const);
147
+
148
+ /** Exact root hashing rule; `PVOX_ROOT_HEADER_LAYOUT_V1` exports every byte offset. */
149
+ export const PVOX_ROOT_HEADER_HASH_RULE = deepFreeze({
150
+ rootHashStorage: "external",
151
+ binaryClosureHashStorage: "external",
152
+ inlineReservedByteOffset: 58,
153
+ inlineReservedByteLength: 2,
154
+ directoryHashByteOffset: 176,
155
+ directoryHashByteLength: 32,
156
+ directoryHashTreatment: "include-as-stored",
157
+ reservedByteOffset: 208,
158
+ reservedByteLength: 48,
159
+ reservedByteValue: 0,
160
+ directoryHashOccurrencesInRootPreimage: 1,
161
+ } as const);
162
+
163
+ /** Registered PVOX 1.0 section identifiers; FourCC values are encoded little-endian. */
164
+ export const PVOX_SECTION_REGISTRY = deepFreeze({
165
+ PART: { type: 0x54524150, version: 1, recordBytes: PVOX_PARTITION_RECORD_BYTES, required: true },
166
+ LODS: { type: 0x53444f4c, version: 1, recordBytes: PVOX_LOD_RECORD_BYTES, required: true },
167
+ ROOT: { type: 0x544f4f52, version: 1, recordBytes: PVOX_ROOT_RECORD_BYTES, required: true },
168
+ LEVL: { type: 0x4c56454c, version: 1, recordBytes: PVOX_LEVEL_SPAN_RECORD_BYTES, required: true },
169
+ NODE: { type: 0x45444f4e, version: 1, recordBytes: PVOX_HIERARCHY_NODE_RECORD_BYTES, required: true },
170
+ BRIK: { type: 0x4b495242, version: 1, recordBytes: PVOX_MIN_BRICK_DESCRIPTOR_BYTES, required: true },
171
+ DATA: { type: 0x41544144, version: 1, recordBytes: 0, required: true },
172
+ SURF: { type: 0x46525553, version: 1, recordBytes: PVOX_SURFACE_PROPERTY_RECORD_BYTES, required: true },
173
+ PHYS: { type: 0x53594850, version: 1, recordBytes: PVOX_PHYSICAL_PROPERTY_RECORD_BYTES, required: true },
174
+ PEVI: { type: 0x49564550, version: 1, recordBytes: PVOX_PHYSICAL_EVIDENCE_RECORD_BYTES, required: true },
175
+ REGN: { type: 0x4e474552, version: 1, recordBytes: PVOX_MATERIAL_REGION_RECORD_BYTES, required: true },
176
+ LAYR: { type: 0x5259414c, version: 1, recordBytes: PVOX_INTERIOR_LAYER_RECORD_BYTES, required: true },
177
+ MASS: { type: 0x5353414d, version: 1, recordBytes: PVOX_MASS_PROPERTY_RECORD_BYTES, required: true },
178
+ BOND: { type: 0x444e4f42, version: 1, recordBytes: PVOX_BOND_RECORD_BYTES, required: false },
179
+ CROT: { type: 0x544f5243, version: 1, recordBytes: PVOX_ROOT_RECORD_BYTES, required: false },
180
+ CLEV: { type: 0x56454c43, version: 1, recordBytes: PVOX_LEVEL_SPAN_RECORD_BYTES, required: false },
181
+ CNOD: { type: 0x444f4e43, version: 1, recordBytes: PVOX_HIERARCHY_NODE_RECORD_BYTES, required: false },
182
+ CBRK: { type: 0x4b524243, version: 1, recordBytes: PVOX_MIN_BRICK_DESCRIPTOR_BYTES, required: false },
183
+ CDAT: { type: 0x54414443, version: 1, recordBytes: 0, required: false },
184
+ } as const);
185
+
186
+ /** Parent Feature rollout gate for every PVOX-backed path. */
187
+ export const PVOX_FEATURE_FLAG_ID = "asset.pipeline.pvox-models.enabled" as const;
188
+
189
+ /** Request policy for texture-free PVOX runtime artifacts. */
190
+ export const PVOX_MODEL_REQUEST_POLICY_ID = "pvox-world-v1" as const;
191
+
192
+ /** Governed PVOX fidelity profiles exposed to requesters. */
193
+ export const PVOX_FIDELITY_PROFILE_IDS = Object.freeze([
194
+ "precision-hero-v1",
195
+ "props-furniture-v1",
196
+ "rocks-organic-shells-v1",
197
+ "buildings-v1",
198
+ "deformable-v1",
199
+ ] as const);
200
+
201
+ /** Named runtime capability profiles; providers cannot define new values. */
202
+ export const PVOX_RUNTIME_CAPABILITY_PROFILE_IDS = Object.freeze([
203
+ "static-render-v1",
204
+ "world-editable-v1",
205
+ "deformable-v1",
206
+ ] as const);
207
+
208
+ /** Existing OAuth capabilities reused by PVOX MCP tools and resources. */
209
+ export const PVOX_REQUIRED_OAUTH_CAPABILITIES = Object.freeze([
210
+ "asset.catalog.request",
211
+ "asset.catalog.confirm",
212
+ "asset.catalog.review",
213
+ "asset.source.manage",
214
+ "asset.pipeline.mcp.manage",
215
+ ] as const);
216
+
217
+ /** Fail-closed default ceilings for one PVOX artifact and its public evidence. */
218
+ export const PVOX_DEFAULT_LIMITS = Object.freeze({
219
+ maximumArtifactBytes: 164 * 1024 * 1024,
220
+ maximumPages: 2_624,
221
+ maximumHierarchyDepth: 8,
222
+ maximumBricks: 524_288,
223
+ maximumLogicalVoxels: 268_435_456,
224
+ maximumEncodedSurfaceSamples: 8_388_608,
225
+ maximumHierarchyNodes: PVOX_MAX_HIERARCHY_NODES,
226
+ maximumSurfaceProperties: 65_536,
227
+ maximumPartitions: 256,
228
+ maximumLodCount: 4,
229
+ maximumCpuResidentBytes: 512 * 1024 * 1024,
230
+ maximumGpuResidentBytes: 512 * 1024 * 1024,
231
+ maximumPhysicalProperties: 64,
232
+ maximumPhysicalEvidenceEntries: 64 * 19,
233
+ maximumMaterialRegions: 64,
234
+ maximumInteriorLayers: 1_024,
235
+ maximumMassPropertyRecords: 256,
236
+ maximumBondRecords: 524_288,
237
+ maximumAssemblyChildren: 256,
238
+ } as const);
239
+
240
+ /** Source-only acquisition/decode ceilings. These never become runtime texture budgets. */
241
+ export const PVOX_DEFAULT_SOURCE_INGESTION_LIMITS = Object.freeze({
242
+ maximumDownloadBytes: 512 * 1024 * 1024,
243
+ maximumExpandedBytes: 2 * 1024 * 1024 * 1024,
244
+ maximumArchiveEntries: 10_000,
245
+ maximumSourceFiles: 20_000,
246
+ maximumDecodedTextureBytes: 512 * 1024 * 1024,
247
+ maximumTextureDimensionPx: 16_384,
248
+ } as const);
249
+
250
+ /** Exact v2 lifecycle, retaining every v1 state and adding PVOX progress. */
251
+ export const MODEL_RESOLUTION_V2_STATES = Object.freeze([
252
+ "searching-catalog",
253
+ "searching-providers",
254
+ "awaiting-provider-auth",
255
+ "downloading",
256
+ "quarantining",
257
+ "importing",
258
+ "processing",
259
+ "voxelizing",
260
+ "evaluating-fidelity",
261
+ "awaiting-material-review",
262
+ "rendering",
263
+ "evaluating",
264
+ "awaiting-rights-review",
265
+ "awaiting-confirmation",
266
+ "promoting",
267
+ "completed",
268
+ "unresolved",
269
+ "failed",
270
+ "cancelled",
271
+ ] as const);
272
+
273
+ export type ModelResolutionStateV2 = typeof MODEL_RESOLUTION_V2_STATES[number];
274
+
275
+ /** Deterministic projection used only by v1 compatibility wrappers. */
276
+ export const MODEL_RESOLUTION_V2_COMPATIBILITY_STATES: Readonly<
277
+ Record<ModelResolutionStateV2, ModelResolutionState>
278
+ > = Object.freeze({
279
+ "searching-catalog": "searching-catalog",
280
+ "searching-providers": "searching-providers",
281
+ "awaiting-provider-auth": "awaiting-provider-auth",
282
+ downloading: "searching-providers",
283
+ quarantining: "quarantining",
284
+ importing: "processing",
285
+ processing: "processing",
286
+ voxelizing: "processing",
287
+ "evaluating-fidelity": "evaluating",
288
+ "awaiting-material-review": "evaluating",
289
+ rendering: "rendering",
290
+ evaluating: "evaluating",
291
+ "awaiting-rights-review": "awaiting-rights-review",
292
+ "awaiting-confirmation": "awaiting-confirmation",
293
+ promoting: "promoting",
294
+ completed: "completed",
295
+ unresolved: "unresolved",
296
+ failed: "failed",
297
+ cancelled: "cancelled",
298
+ });
299
+
300
+ /** Runtime capabilities independently admitted by one PVOX artifact. */
301
+ export const PVOX_CAPABILITIES = Object.freeze([
302
+ "rendering",
303
+ "collision",
304
+ "destruction",
305
+ "thermal",
306
+ "moisture",
307
+ "fluid-boundary",
308
+ "deformation",
309
+ "animation",
310
+ "volume",
311
+ ] as const);
312
+
313
+ export type PvoxCapability = typeof PVOX_CAPABILITIES[number];
314
+ export type PvoxCapabilityStatus = "supported" | "unsupported" | "not-applicable";
315
+ export type PvoxGeometryMode = "solid" | "shell" | "mixed";
316
+ export type PvoxRequestedGeometryMode = PvoxGeometryMode | "auto";
317
+ export type PvoxFidelityProfileId = typeof PVOX_FIDELITY_PROFILE_IDS[number];
318
+ export type PvoxRuntimeCapabilityProfileId = typeof PVOX_RUNTIME_CAPABILITY_PROFILE_IDS[number];
319
+
320
+ /** Mesh/archive/texture budgets used only while a provider source is quarantined. */
321
+ export interface ModelSourceIngestionLimitsV2 {
322
+ readonly maximumDownloadBytes: number;
323
+ readonly maximumExpandedBytes: number;
324
+ readonly maximumArchiveEntries: number;
325
+ readonly maximumSourceFiles: number;
326
+ readonly maximumDecodedTextureBytes: number;
327
+ readonly maximumTextureDimensionPx: number;
328
+ }
329
+
330
+ /** Runtime-only, artifact-wide PVOX ceilings. Texture and triangle fields are intentionally absent. */
331
+ export interface PvoxRuntimeLimitsV1 {
332
+ readonly maximumArtifactBytes: number;
333
+ readonly maximumPages: number;
334
+ readonly maximumHierarchyDepth: number;
335
+ readonly maximumHierarchyNodes: number;
336
+ readonly maximumBricks: number;
337
+ readonly maximumLogicalVoxels: number;
338
+ readonly maximumEncodedSurfaceSamples: number;
339
+ readonly maximumSurfaceProperties: number;
340
+ readonly maximumPhysicalPaletteRecords: number;
341
+ readonly maximumPhysicalEvidenceEntries: number;
342
+ readonly maximumMaterialRegions: number;
343
+ readonly maximumInteriorLayers: number;
344
+ readonly maximumMassPropertyRecords: number;
345
+ readonly maximumBondRecords: number;
346
+ readonly maximumPartitions: number;
347
+ readonly maximumLodCount: number;
348
+ readonly maximumCpuResidentBytes: number;
349
+ readonly maximumGpuResidentBytes: number;
350
+ }
351
+
352
+ /** Governed PVOX representation, fidelity, capability and runtime-budget request. */
353
+ export interface PvoxRuntimeRequestProfileV1 {
354
+ readonly profileId: string;
355
+ readonly fidelityProfileId: PvoxFidelityProfileId;
356
+ readonly capabilityProfileId: PvoxRuntimeCapabilityProfileId;
357
+ readonly geometryMode: PvoxRequestedGeometryMode;
358
+ readonly requiredCapabilities: readonly PvoxCapability[];
359
+ readonly limits: PvoxRuntimeLimitsV1;
360
+ }
361
+
362
+ /** Technical constraints that remain meaningful after texture-free voxel compilation. */
363
+ export interface ModelHardConstraintsV2 {
364
+ readonly boundsMetres?: ModelBoundsMetres;
365
+ readonly dimensionsMetres?: ModelDimensionsMetres;
366
+ readonly collision?: ModelRequirementPolicy;
367
+ readonly partition?: "single" | "allowed" | "required";
368
+ }
369
+
370
+ /**
371
+ * Normalized request for the PVOX pipeline.
372
+ *
373
+ * Source-ingestion limits are deliberately independent from immutable PVOX runtime
374
+ * limits so an ingestion texture budget can never be interpreted as a runtime one.
375
+ */
376
+ export interface ModelRequestSpecV2 {
377
+ readonly contractVersion: typeof MODEL_RESOLUTION_V2_CONTRACT_VERSION;
378
+ readonly policyProfileId: typeof PVOX_MODEL_REQUEST_POLICY_ID;
379
+ readonly requestSemanticProfileHash: string;
380
+ readonly query: string;
381
+ readonly revision: number;
382
+ readonly locale?: string;
383
+ readonly rankerId?: string;
384
+ readonly hardConstraints: ModelHardConstraintsV2;
385
+ readonly softPreferences: ModelSoftPreferences;
386
+ readonly exclusions: readonly string[];
387
+ readonly sourceIngestionLimits: ModelSourceIngestionLimitsV2;
388
+ readonly pvoxRuntimeProfile: PvoxRuntimeRequestProfileV1;
389
+ }
390
+
391
+ /** Hard gates that semantic confidence can never override. Rights remain separate. */
392
+ export const PVOX_NON_OVERRIDABLE_GATE_KINDS = Object.freeze([
393
+ "malware-scan",
394
+ "source-format-validation",
395
+ "pvox-validation",
396
+ "fidelity-validation",
397
+ "physical-property-validation",
398
+ "renderer-validation",
399
+ "accessibility-review",
400
+ ] as const);
401
+
402
+ export type PvoxNonOverridableGateKind = typeof PVOX_NON_OVERRIDABLE_GATE_KINDS[number];
403
+
404
+ /** Physical fields that may be retained with localized PVOX material regions. */
405
+ export const PVOX_PHYSICAL_PROPERTIES = Object.freeze([
406
+ "density",
407
+ "hardness",
408
+ "tensile-strength",
409
+ "compressive-strength",
410
+ "shear-strength",
411
+ "fracture-energy",
412
+ "friction",
413
+ "restitution",
414
+ "thermal-conductivity",
415
+ "heat-capacity",
416
+ "thermal-expansion",
417
+ "ignition-temperature",
418
+ "melting-temperature",
419
+ "porosity",
420
+ "permeability",
421
+ "moisture-response",
422
+ "flammability",
423
+ "corrosion-rate",
424
+ "interior-thickness",
425
+ ] as const);
426
+
427
+ export type PhysicalPropertyKind = typeof PVOX_PHYSICAL_PROPERTIES[number];
428
+ export type PhysicalPropertyProvenance = "source" | "authored" | "derived" | "inferred" | "default";
429
+
430
+ export interface PvoxAuthenticatedResourceMetadata {
431
+ readonly uri: string;
432
+ readonly byteLength: number;
433
+ readonly sha256: string;
434
+ readonly contentType: typeof PVOX_CONTENT_TYPE;
435
+ readonly authenticated: true;
436
+ }
437
+
438
+ export interface PvoxPageSpatialKey {
439
+ readonly lodLevel: PvoxLodLevel;
440
+ readonly partitionIndex: number;
441
+ readonly hierarchyDepth: number;
442
+ readonly minimumMortonCode: string;
443
+ readonly maximumMortonCode: string;
444
+ }
445
+
446
+ export interface PvoxPageRecord {
447
+ readonly pageIndex: number;
448
+ readonly pageKind: typeof PVOX_PAGE_KINDS[number];
449
+ readonly byteOffset: number;
450
+ readonly byteLength: number;
451
+ readonly decodedByteLength: number;
452
+ readonly sha256: string;
453
+ readonly spatialKey?: PvoxPageSpatialKey;
454
+ }
455
+
456
+ export type PvoxLodLevel = 0 | 1 | 2 | 3;
457
+
458
+ export interface PvoxLodRecord {
459
+ readonly level: PvoxLodLevel;
460
+ readonly firstPageIndex: number;
461
+ readonly pageCount: number;
462
+ readonly brickCount: number;
463
+ readonly cellSizeMetres: number;
464
+ readonly maximumSurfaceErrorMetres: number;
465
+ readonly p99SurfaceErrorMetres: number;
466
+ readonly silhouetteIou: number;
467
+ readonly maximumContourDisplacementPx: number;
468
+ readonly renderedSsim: number;
469
+ readonly p95DeltaE2000: number;
470
+ readonly normalizedMaterialError: number;
471
+ }
472
+
473
+ /** Public authenticated metadata for one immutable PVOX binary closure. */
474
+ export interface PvoxAssetManifestV1 {
475
+ readonly manifestVersion: typeof PVOX_ASSET_MANIFEST_VERSION;
476
+ readonly representation: "pvox";
477
+ readonly formatVersion: typeof PVOX_FORMAT_VERSION;
478
+ readonly contentType: typeof PVOX_CONTENT_TYPE;
479
+ readonly fileExtension: typeof PVOX_FILE_EXTENSION;
480
+ readonly magic: typeof PVOX_MAGIC;
481
+ readonly brickEdgeVoxels: typeof PVOX_BRICK_EDGE_VOXELS;
482
+ readonly pageSizeBytes: typeof PVOX_PAGE_SIZE_BYTES;
483
+ readonly geometryMode: PvoxGeometryMode;
484
+ readonly sourceContentHash: string;
485
+ readonly canonicalDocumentHash: string;
486
+ readonly rootHash: string;
487
+ readonly directoryHash: string;
488
+ readonly pageSetHash: string;
489
+ readonly binaryClosureHash: string;
490
+ readonly validationEvidenceHash: string;
491
+ readonly compilationInputHash: string;
492
+ readonly runtimeRequestProfileHash: string;
493
+ readonly headerByteLength: typeof PVOX_HEADER_BYTE_LENGTH;
494
+ readonly directoryEntryByteLength: typeof PVOX_DIRECTORY_ENTRY_BYTE_LENGTH;
495
+ readonly sectionCount: number;
496
+ readonly maximumRunsPerBrick: typeof PVOX_MAX_RUNS_PER_BRICK;
497
+ readonly maximumLocalSamplesPerBrick: typeof PVOX_MAX_LOCAL_SAMPLES_PER_BRICK;
498
+ readonly maximumEncodedBrickPayloadBytes: typeof PVOX_MAX_ENCODED_BRICK_PAYLOAD_BYTES;
499
+ readonly coordinateSystem: typeof CANONICAL_MODEL_COORDINATE_SYSTEM;
500
+ readonly boundsMetres: ModelBoundsMetresV2;
501
+ readonly artifact: PvoxAuthenticatedResourceMetadata;
502
+ readonly pages: readonly PvoxPageRecord[];
503
+ readonly lods: readonly PvoxLodRecord[];
504
+ }
505
+
506
+ export type PvoxAssetManifest = PvoxAssetManifestV1;
507
+
508
+ export interface ModelBoundsMetresV2 {
509
+ readonly min: readonly [number, number, number];
510
+ readonly max: readonly [number, number, number];
511
+ }
512
+
513
+ export interface ModelDimensionsMetresV2 {
514
+ readonly width: number;
515
+ readonly height: number;
516
+ readonly depth: number;
517
+ }
518
+
519
+ /** PVOX runtime facts that intentionally contain no triangle or texture fields. */
520
+ export interface VoxelTechnicalProfile {
521
+ readonly boundsMetres: ModelBoundsMetresV2;
522
+ readonly dimensionsMetres: ModelDimensionsMetresV2;
523
+ readonly geometryMode: PvoxGeometryMode;
524
+ readonly artifactByteLength: number;
525
+ readonly pageCount: number;
526
+ /** Sum of render bricks across every retained spatial LOD in the artifact. */
527
+ readonly brickCount: number;
528
+ /** Artifact-wide render capacity; exactly `brickCount * 8 * 8 * 8`. */
529
+ readonly logicalVoxelCapacity: number;
530
+ /** Sum of encoded render-field surface samples across every retained LOD. */
531
+ readonly encodedSurfaceSampleCount: number;
532
+ readonly surfacePropertyCount: number;
533
+ readonly sectionCount: number;
534
+ readonly levelSpanRecordCount: number;
535
+ readonly physicalPaletteRecordCount: number;
536
+ readonly physicalEvidenceRecordCount: number;
537
+ readonly materialRegionCount: number;
538
+ readonly interiorLayerRecordCount: number;
539
+ readonly massPropertyRecordCount: number;
540
+ readonly bondRecordCount: number;
541
+ readonly collisionHierarchyNodeCount: number;
542
+ readonly collisionBrickCount: number;
543
+ readonly collisionLogicalVoxelCapacity: number;
544
+ readonly collisionEncodedSurfaceSampleCount: number;
545
+ readonly hierarchyDepth: number;
546
+ readonly hierarchyNodeCount: number;
547
+ readonly lodCount: number;
548
+ readonly cpuResidentByteLength: number;
549
+ readonly gpuResidentByteLength: number;
550
+ readonly hasCollision: boolean;
551
+ readonly partitionCount: number;
552
+ readonly maximumPartitionExtentMetres: number;
553
+ readonly maximumPartitionDiagonalMetres: number;
554
+ }
555
+
556
+ /** One independently evaluated runtime capability. */
557
+ export interface VoxelCapabilityAssessment {
558
+ readonly capability: PvoxCapability;
559
+ readonly status: PvoxCapabilityStatus;
560
+ readonly evaluatorId: string;
561
+ readonly evaluatorVersion: string;
562
+ readonly subjectContentHash: string;
563
+ readonly evidenceHash: string;
564
+ readonly reasonCodes: readonly string[];
565
+ readonly evaluatedAt: string;
566
+ }
567
+
568
+ /** One localized physical value with explicit origin and review evidence. */
569
+ export interface PhysicalPropertyEvidence {
570
+ readonly property: PhysicalPropertyKind;
571
+ readonly value: number;
572
+ readonly unit: string;
573
+ readonly provenance: PhysicalPropertyProvenance;
574
+ readonly confidence: number;
575
+ readonly subjectContentHash: string;
576
+ readonly regionId: string;
577
+ readonly materialId: string;
578
+ readonly policyId: typeof PVOX_PHYSICAL_PROPERTY_POLICY_ID;
579
+ readonly policyVersion: typeof PVOX_PHYSICAL_PROPERTY_POLICY_VERSION;
580
+ readonly evidenceHash: string;
581
+ readonly reviewedBy?: string;
582
+ readonly reviewedAt?: string;
583
+ readonly reviewToken?: string;
584
+ }
585
+
586
+ export interface PvoxPhysicalRegionInventoryEntry {
587
+ readonly regionIndex: number;
588
+ readonly physicalPaletteIndex: number;
589
+ readonly regionId: string;
590
+ readonly materialId: string;
591
+ }
592
+
593
+ /** Validator-attested authoritative material-region inventory read from PVOX REGN/PHYS records. */
594
+ export interface PvoxPhysicalRegionInventoryV1 {
595
+ readonly inventoryVersion: typeof PVOX_PHYSICAL_REGION_INVENTORY_VERSION;
596
+ readonly subjectContentHash: string;
597
+ readonly inventoryHash: string;
598
+ readonly inventoryHashAttestation: PvoxHashAttestation;
599
+ readonly validationEvidenceHash: string;
600
+ readonly entries: readonly PvoxPhysicalRegionInventoryEntry[];
601
+ }
602
+
603
+ export type PvoxReviewedStructuralEvidenceKind = "mass-properties" | "bond-graph" | "interior-layers";
604
+
605
+ export interface PvoxReviewedStructuralEvidence {
606
+ readonly kind: PvoxReviewedStructuralEvidenceKind;
607
+ readonly subjectContentHash: string;
608
+ readonly physicalInventoryHash: string;
609
+ readonly recordCount: number;
610
+ readonly evidenceHash: string;
611
+ readonly reviewerId: string;
612
+ readonly reviewedAt: string;
613
+ readonly reviewToken: string;
614
+ }
615
+
616
+ /** Structurally cross-bound digest evidence; token verification remains an authenticated host responsibility. */
617
+ export interface PvoxHashAttestation {
618
+ readonly algorithm: "sha256";
619
+ readonly domain: typeof PVOX_HASH_DOMAINS[keyof typeof PVOX_HASH_DOMAINS];
620
+ readonly digest: string;
621
+ readonly attestationToken: string;
622
+ }
623
+
624
+ /** V1 assessment plus the exact normalized V2 request subject ranked by that assessment. */
625
+ export type PvoxModelMatchAssessmentV2 = ModelMatchAssessment & Readonly<{
626
+ requestSemanticProfileHash: string;
627
+ requestSemanticProfileCanonical: string;
628
+ evidenceHash: string;
629
+ }>;
630
+
631
+ /** Immutable comparison of PVOX output with the verified canonical document. */
632
+ export interface PvoxFidelityEvidence {
633
+ readonly profileId: PvoxFidelityProfileId;
634
+ readonly surfaceProfileId: Exclude<PvoxFidelityProfileId, "deformable-v1">;
635
+ readonly profileVersion: typeof PVOX_FIDELITY_PROFILE_VERSION;
636
+ readonly outcome: "passed" | "blocked";
637
+ readonly canonicalDocumentHash: string;
638
+ readonly pvoxBinaryClosureHash: string;
639
+ readonly evaluatedDiagonalMetres: number;
640
+ readonly lod0CellSizeMetres: number;
641
+ readonly maximumAllowedSurfaceErrorMetres: number;
642
+ readonly maximumSurfaceErrorMetres: number;
643
+ readonly p99AllowedSurfaceErrorMetres: number;
644
+ readonly p99SurfaceErrorMetres: number;
645
+ readonly minimumSilhouetteIou: number;
646
+ readonly silhouetteIou: number;
647
+ readonly maximumAllowedContourDisplacementPx: number;
648
+ readonly maximumContourDisplacementPx: number;
649
+ readonly minimumRenderedSsim: number;
650
+ readonly renderedSsim: number;
651
+ readonly maximumP95DeltaE2000: number;
652
+ readonly p95DeltaE2000: number;
653
+ readonly maximumNormalizedMaterialError: number;
654
+ readonly normalizedMaterialError: number;
655
+ readonly evidenceHash: string;
656
+ readonly evaluatedAt: string;
657
+ readonly decisionToken: string;
658
+ }
659
+
660
+ export type PvoxEditOperation = "insert" | "replace" | "remove";
661
+
662
+ export interface PvoxBrickPatchDescriptor {
663
+ readonly fieldKind: "render-field" | "collision-field";
664
+ readonly lodLevel: PvoxLodLevel;
665
+ readonly partitionIndex: number;
666
+ readonly hierarchyDepth: number;
667
+ readonly mortonCode: string;
668
+ readonly expectedPageIndex: number;
669
+ readonly expectedPageHash: string;
670
+ readonly operation: PvoxEditOperation;
671
+ readonly expectedBrickHash: string;
672
+ readonly resultingBrickHash: string;
673
+ readonly resultingPageHash: string;
674
+ }
675
+
676
+ /** Placement-scoped, sequential copy-on-write mutation journal. */
677
+ export interface PvoxEditJournal {
678
+ readonly journalVersion: typeof PVOX_EDIT_JOURNAL_VERSION;
679
+ readonly baseContentHash: string;
680
+ readonly basePageSetHash: string;
681
+ readonly placementId: string;
682
+ readonly gridVersion: string;
683
+ readonly operationId: string;
684
+ readonly expectedRevision: number;
685
+ readonly resultingRevision: number;
686
+ readonly previousJournalHash: string;
687
+ readonly expectedRootHash: string;
688
+ readonly resultingRootHash: string;
689
+ readonly resultingRootHashAttestation: PvoxHashAttestation;
690
+ readonly patches: readonly PvoxBrickPatchDescriptor[];
691
+ readonly dirtyBoundsMetres: ModelBoundsMetresV2;
692
+ readonly massDeltaKg: number;
693
+ readonly journalHash: string;
694
+ readonly journalHashAttestation: PvoxHashAttestation;
695
+ readonly recordedAt: string;
696
+ }
697
+
698
+ /** Authenticated placement state required to validate a standalone non-genesis CAS edit. */
699
+ export interface PvoxEditJournalCurrentState {
700
+ readonly baseContentHash: string;
701
+ readonly basePageSetHash: string;
702
+ readonly placementId: string;
703
+ readonly gridVersion: string;
704
+ readonly revision: number;
705
+ readonly rootHash: string;
706
+ readonly latestJournalHash: string;
707
+ readonly pageHashes: readonly Readonly<{ pageIndex: number; sha256: string }>[];
708
+ }
709
+
710
+ export interface PvoxCompilerEvidence {
711
+ readonly compilerId: string;
712
+ readonly compilerVersion: string;
713
+ readonly sourceFormat: string;
714
+ readonly sourceContentHash: string;
715
+ readonly canonicalDocumentHash: string;
716
+ readonly compilationInputHash: string;
717
+ readonly runtimeRequestProfileHash: string;
718
+ readonly outputContentHash: string;
719
+ readonly evidenceHash: string;
720
+ readonly compiledAt: string;
721
+ }
722
+
723
+ export interface PvoxGateEvidence {
724
+ readonly kind: PvoxNonOverridableGateKind;
725
+ readonly outcome: "passed" | "blocked";
726
+ readonly validatorId: string;
727
+ readonly validatorVersion: string;
728
+ readonly subjectContentHash: string;
729
+ readonly evidenceHash: string;
730
+ readonly reasonCodes: readonly string[];
731
+ readonly evaluatedAt: string;
732
+ readonly attestationToken: string;
733
+ }
734
+
735
+ export interface PvoxCatalogAssemblyChildRef {
736
+ readonly disposition: "existing";
737
+ readonly representation: "pvox";
738
+ readonly asset: ModelAssetRef;
739
+ readonly processingManifestId: string;
740
+ readonly processingClosureHash: string;
741
+ readonly binaryClosureHash: string;
742
+ readonly processingManifestUri: string;
743
+ }
744
+
745
+ export interface PvoxStagedAssemblyChildRef {
746
+ readonly disposition: "staged-derived";
747
+ readonly representation: "pvox";
748
+ readonly derivedId: string;
749
+ readonly kind: "leaf";
750
+ readonly contentHash: string;
751
+ readonly binaryClosureHash: string;
752
+ readonly processingClosureHash: string;
753
+ readonly processingManifestId: string;
754
+ readonly processingManifestUri: string;
755
+ }
756
+
757
+ export interface PvoxAssemblyChild {
758
+ readonly instanceId: string;
759
+ readonly parentInstanceId?: string;
760
+ readonly assetRef: PvoxCatalogAssemblyChildRef | PvoxStagedAssemblyChildRef;
761
+ readonly transform: ModelTransform;
762
+ }
763
+
764
+ /** Additive PVOX-only processing manifest. The GLB v1 manifest is unchanged. */
765
+ export interface ModelProcessingManifestV2 {
766
+ readonly contractVersion: typeof MODEL_RESOLUTION_V2_CONTRACT_VERSION;
767
+ readonly representation: "pvox";
768
+ readonly manifestId: string;
769
+ readonly resolutionId: string;
770
+ readonly candidateId: string;
771
+ readonly kind: ModelAssetKind;
772
+ readonly requestProfileId: string;
773
+ readonly requestSemanticProfileHash: string;
774
+ readonly capabilityProfileId: PvoxRuntimeCapabilityProfileId;
775
+ readonly contentHash: string;
776
+ readonly binaryClosureHash: string;
777
+ readonly processingClosureHash: string;
778
+ readonly processingClosureAttestation: PvoxHashAttestation;
779
+ readonly assemblyClosureHash: string;
780
+ readonly assemblyClosureAttestation: PvoxHashAttestation;
781
+ readonly compilationInputHash: string;
782
+ readonly runtimeRequestProfileHash: string;
783
+ readonly coordinateSystem: typeof CANONICAL_MODEL_COORDINATE_SYSTEM;
784
+ readonly pvox: PvoxAssetManifestV1;
785
+ readonly technicalProfile: VoxelTechnicalProfile;
786
+ readonly requiredCapabilities: readonly PvoxCapability[];
787
+ readonly capabilities: readonly VoxelCapabilityAssessment[];
788
+ readonly capabilityEvidenceSetHash: string;
789
+ readonly capabilityEvidenceSetAttestation: PvoxHashAttestation;
790
+ readonly physicalRegionInventory: PvoxPhysicalRegionInventoryV1;
791
+ readonly physicalProperties: readonly PhysicalPropertyEvidence[];
792
+ readonly physicalEvidenceHash: string;
793
+ readonly physicalEvidenceAttestation: PvoxHashAttestation;
794
+ readonly massPropertiesEvidence: PvoxReviewedStructuralEvidence;
795
+ readonly bondGraphEvidence: PvoxReviewedStructuralEvidence;
796
+ readonly interiorLayerEvidence: PvoxReviewedStructuralEvidence;
797
+ readonly converter: PvoxCompilerEvidence;
798
+ readonly fidelity: PvoxFidelityEvidence;
799
+ readonly children: readonly PvoxAssemblyChild[];
800
+ readonly processedAt: string;
801
+ }
802
+
803
+ export interface PvoxRenderEvidence {
804
+ readonly renderId: string;
805
+ readonly representation: "pvox";
806
+ readonly lodLevel: 0;
807
+ readonly traversalBackend: "native-pvox";
808
+ readonly rendererId: typeof PVOX_NATIVE_RENDERER_ID;
809
+ readonly rendererVersion: string;
810
+ readonly rendererQualificationHash: string;
811
+ readonly settingsId: typeof PVOX_NATIVE_REVIEW_SETTINGS_ID;
812
+ readonly settingsVersion: string;
813
+ readonly cameraQualificationHash: string;
814
+ readonly processingManifestId: string;
815
+ readonly processingClosureHash: string;
816
+ readonly pvoxContentHash: string;
817
+ readonly pvoxRootHash: string;
818
+ readonly pvoxDirectoryHash: string;
819
+ readonly pvoxPageSetHash: string;
820
+ readonly viewSha256s: readonly [string, string, string, string];
821
+ readonly renderedAt: string;
822
+ readonly evidenceHash: string;
823
+ readonly evidenceHashAttestation: PvoxHashAttestation;
824
+ }
825
+
826
+ /** Exact subject covered by a backend-issued candidate confirmation token. */
827
+ export interface ModelCandidateConfirmationBindingV2 {
828
+ readonly bindingHash: string;
829
+ readonly bindingHashAttestation: PvoxHashAttestation;
830
+ readonly resolutionId: string;
831
+ readonly requesterId: string;
832
+ readonly request: ModelRequestSpecV2;
833
+ readonly requestSemanticProfileHash: string;
834
+ readonly requestRevision: number;
835
+ readonly candidateId: string;
836
+ readonly candidateContentHash: string;
837
+ readonly candidateAssetRef: ModelCandidateAssetRef;
838
+ readonly match: PvoxModelMatchAssessmentV2;
839
+ readonly provenance: ModelProvenance;
840
+ readonly rights: ModelRightsAssessment;
841
+ readonly sourceContentHash: string;
842
+ readonly binaryClosureHash: string;
843
+ readonly pvoxRootHash: string;
844
+ readonly pvoxDirectoryHash: string;
845
+ readonly pvoxPageSetHash: string;
846
+ readonly compilationInputHash: string;
847
+ readonly runtimeRequestProfileHash: string;
848
+ readonly processingClosureHash: string;
849
+ readonly assemblyClosureHash: string;
850
+ readonly evaluationClosureHash: string;
851
+ readonly capabilityEvidenceSetHash: string;
852
+ readonly hardGates: readonly PvoxGateEvidence[];
853
+ readonly viewSha256s: readonly [string, string, string, string];
854
+ readonly rightsDecisionId: string;
855
+ readonly fidelityEvidenceHash: string;
856
+ readonly physicalEvidenceHash: string;
857
+ readonly rendererId: string;
858
+ readonly rendererVersion: string;
859
+ readonly rendererEvidenceHash: string;
860
+ readonly renderEvidence: PvoxRenderEvidence;
861
+ }
862
+
863
+ interface ModelCandidateV2Base {
864
+ readonly contractVersion: typeof MODEL_RESOLUTION_V2_CONTRACT_VERSION;
865
+ readonly resolutionId: string;
866
+ readonly requesterId: string;
867
+ readonly candidateId: string;
868
+ readonly request: ModelRequestSpecV2;
869
+ readonly assetRef: ModelCandidateAssetRef;
870
+ readonly match: PvoxModelMatchAssessmentV2;
871
+ readonly provenance: ModelProvenance;
872
+ readonly rights: ModelRightsAssessment;
873
+ readonly processingManifest: ModelProcessingManifestV2;
874
+ readonly views: ModelConfirmationViews;
875
+ readonly renderEvidence: PvoxRenderEvidence;
876
+ readonly evaluationClosureHash: string;
877
+ readonly evaluationClosureAttestation: PvoxHashAttestation;
878
+ readonly hardGates: readonly PvoxGateEvidence[];
879
+ }
880
+
881
+ export interface ConfirmableModelCandidateV2 extends ModelCandidateV2Base {
882
+ readonly admissionStatus: "confirmable";
883
+ readonly confirmationBinding: ModelCandidateConfirmationBindingV2;
884
+ readonly confirmationToken: string;
885
+ readonly confirmationRequired: true;
886
+ }
887
+
888
+ export interface DiagnosticModelCandidateV2 extends ModelCandidateV2Base {
889
+ readonly admissionStatus: "diagnostic";
890
+ readonly blockingReasonCodes: readonly string[];
891
+ readonly confirmationRequired: false;
892
+ }
893
+
894
+ export type ModelCandidateV2 = ConfirmableModelCandidateV2 | DiagnosticModelCandidateV2;
895
+
896
+ export interface ModelCandidateConfirmationV2 {
897
+ readonly contractVersion: typeof MODEL_RESOLUTION_V2_CONTRACT_VERSION;
898
+ readonly confirmationId: string;
899
+ readonly resolutionId: string;
900
+ readonly requesterId: string;
901
+ readonly candidateId: string;
902
+ readonly confirmationToken: string;
903
+ readonly confirmationBindingHash: string;
904
+ readonly viewSha256s: readonly [string, string, string, string];
905
+ readonly confirmedBy: string;
906
+ readonly confirmedAt: string;
907
+ readonly semanticRiskAccepted: boolean;
908
+ }
909
+
910
+ export interface ModelPromotionReceiptV2 {
911
+ readonly contractVersion: typeof MODEL_RESOLUTION_V2_CONTRACT_VERSION;
912
+ readonly promotionId: string;
913
+ readonly resolutionId: string;
914
+ readonly requesterId: string;
915
+ readonly candidateId: string;
916
+ readonly proposalId: string;
917
+ readonly candidateAssetRef: ModelCandidateAssetRef;
918
+ readonly confirmationId: string;
919
+ readonly confirmationBindingHash: string;
920
+ readonly processingManifestId: string;
921
+ readonly processingContentHash: string;
922
+ readonly processingClosureHash: string;
923
+ readonly assemblyClosureHash: string;
924
+ readonly assemblyChildClosureHashes: readonly string[];
925
+ readonly finalAssetRef: ModelAssetRef;
926
+ readonly creditsRecordHash: string;
927
+ readonly catalogRowHash: string;
928
+ readonly indexSnapshotHash: string;
929
+ readonly catalogPointerEtag: string;
930
+ readonly indexPointerEtag: string;
931
+ readonly publicationState: "pointer-last-complete";
932
+ readonly creditsWrittenAt: string;
933
+ readonly catalogRowWrittenAt: string;
934
+ readonly indexSnapshotWrittenAt: string;
935
+ readonly pointerPublishedAt: string;
936
+ readonly publicationHash: string;
937
+ readonly publicationHashAttestation: PvoxHashAttestation;
938
+ readonly promotedAt: string;
939
+ readonly publicationToken: string;
940
+ }
941
+
942
+ export interface ModelRefinementQuestionV2 {
943
+ readonly questionId: string;
944
+ readonly prompt: string;
945
+ readonly reasonCodes: readonly string[];
946
+ }
947
+
948
+ export interface ModelResolutionV2 {
949
+ readonly contractVersion: typeof MODEL_RESOLUTION_V2_CONTRACT_VERSION;
950
+ readonly resolutionId: string;
951
+ readonly requesterId: string;
952
+ readonly request: ModelRequestSpecV2;
953
+ readonly attempts: number;
954
+ readonly state: ModelResolutionStateV2;
955
+ readonly compatibilityState: ModelResolutionState;
956
+ readonly candidates: readonly ModelCandidateV2[];
957
+ readonly bestCandidate?: ModelCandidateV2;
958
+ readonly confirmation?: ModelCandidateConfirmationV2;
959
+ readonly promotionReceipt?: ModelPromotionReceiptV2;
960
+ readonly refinementQuestions: readonly ModelRefinementQuestionV2[];
961
+ readonly finalAssetRef?: ModelAssetRef;
962
+ readonly stateReasonCode?: string;
963
+ readonly createdAt: string;
964
+ readonly updatedAt: string;
965
+ }
966
+
967
+ type UnknownRecord = Record<string, unknown>;
968
+
969
+ const SHA256 = /^[a-f0-9]{64}$/u;
970
+ const TOKEN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
971
+ const PATH_SEGMENT = /^(?!\.{1,2}$)[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$/u;
972
+ const ATTESTATION = /^[A-Za-z0-9_-]{32,256}$/u;
973
+ const REASON_CODE = /^[a-z0-9][a-z0-9._:-]{0,127}$/u;
974
+ const MORTON_CODE = /^[a-f0-9]{16}$/u;
975
+ const ISO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u;
976
+ const PVOX_RESOLUTION_RESOURCE = /^mcp:\/\/models\/resolutions\/([A-Za-z0-9._~-]{1,128})\/candidates\/([A-Za-z0-9._~-]{1,128})\/artifacts\/sha256\/([a-f0-9]{64})\.pvox$/u;
977
+ const PVOX_CATALOG_RESOURCE = /^mcp:\/\/models\/catalog\/([a-z0-9]+(?:-[a-z0-9]+)*)\/versions\/([A-Za-z0-9][A-Za-z0-9._-]{0,127})\/artifacts\/sha256\/([a-f0-9]{64})\.pvox$/u;
978
+ const DIRECT_URL = /(?:https?:\/\/|\bwww\.)/iu;
979
+
980
+ function hasUnsafeControlCharacters(value: string): boolean {
981
+ return [...value].some((character) => {
982
+ const codePoint = character.codePointAt(0) ?? 0;
983
+ return codePoint <= 0x1f
984
+ || (codePoint >= 0x7f && codePoint <= 0x9f)
985
+ || codePoint === 0x2028
986
+ || codePoint === 0x2029;
987
+ });
988
+ }
989
+
990
+ function deepFreeze<T>(value: T, seen = new WeakSet<object>()): T {
991
+ if (value === null || typeof value !== "object") return value;
992
+ const object = value as object;
993
+ if (seen.has(object)) return value;
994
+ seen.add(object);
995
+ for (const child of Object.values(value as UnknownRecord)) deepFreeze(child, seen);
996
+ return Object.freeze(value);
997
+ }
998
+
999
+ function asRecord(value: unknown, fieldName: string): UnknownRecord {
1000
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
1001
+ throw new Error(`${fieldName} must be a plain object.`);
1002
+ }
1003
+ const prototype = Object.getPrototypeOf(value);
1004
+ if (prototype !== Object.prototype && prototype !== null) {
1005
+ throw new Error(`${fieldName} must be a plain object.`);
1006
+ }
1007
+ const keys = Reflect.ownKeys(value);
1008
+ if (keys.some((key) => typeof key !== "string")) {
1009
+ throw new Error(`${fieldName} must contain enumerable string-keyed data properties only.`);
1010
+ }
1011
+ for (const key of keys as string[]) {
1012
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1013
+ if (descriptor === undefined || descriptor.enumerable !== true || !("value" in descriptor)) {
1014
+ throw new Error(`${fieldName} must contain enumerable string-keyed data properties only.`);
1015
+ }
1016
+ }
1017
+ return value as UnknownRecord;
1018
+ }
1019
+
1020
+ function exactKeys(record: UnknownRecord, allowed: readonly string[], fieldName: string): void {
1021
+ const set = new Set(allowed);
1022
+ const unsupported = Object.keys(record).find((key) => !set.has(key));
1023
+ if (unsupported !== undefined) throw new Error(`${fieldName} contains an unsupported field.`);
1024
+ }
1025
+
1026
+ function asDenseArray(value: unknown, fieldName: string, minimum: number, maximum: number): readonly unknown[] {
1027
+ if (!Array.isArray(value) || value.length < minimum || value.length > maximum) {
1028
+ throw new Error(`${fieldName} must be a bounded dense array.`);
1029
+ }
1030
+ const ownKeys = Reflect.ownKeys(value);
1031
+ if (ownKeys.some((key) => typeof key === "symbol" || (key !== "length" && !/^(?:0|[1-9]\d*)$/u.test(key)))) {
1032
+ throw new Error(`${fieldName} must be a bounded dense array of data elements.`);
1033
+ }
1034
+ for (let index = 0; index < value.length; index += 1) {
1035
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
1036
+ if (descriptor === undefined || descriptor.enumerable !== true || !("value" in descriptor)) {
1037
+ throw new Error(`${fieldName} must be a bounded dense array of data elements.`);
1038
+ }
1039
+ }
1040
+ return value;
1041
+ }
1042
+
1043
+ function assertDataOnlyTree(value: unknown, fieldName: string, seen = new WeakSet<object>(), depth = 0): void {
1044
+ if (value === null || typeof value !== "object") return;
1045
+ if (depth > 32) throw new Error(`${fieldName} data nesting exceeds the contract limit.`);
1046
+ if (seen.has(value)) throw new Error(`${fieldName} must not contain cyclic data.`);
1047
+ seen.add(value);
1048
+ if (Array.isArray(value)) {
1049
+ const entries = asDenseArray(value, fieldName, 0, 10_000);
1050
+ for (let index = 0; index < entries.length; index += 1) {
1051
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index))!;
1052
+ assertDataOnlyTree(descriptor.value, `${fieldName}[${index}]`, seen, depth + 1);
1053
+ }
1054
+ } else {
1055
+ const record = asRecord(value, fieldName);
1056
+ for (const key of Object.keys(record)) {
1057
+ const descriptor = Object.getOwnPropertyDescriptor(record, key)!;
1058
+ assertDataOnlyTree(descriptor.value, `${fieldName}.${key}`, seen, depth + 1);
1059
+ }
1060
+ }
1061
+ seen.delete(value);
1062
+ }
1063
+
1064
+ function requireString(value: unknown, fieldName: string, maximum = 512): string {
1065
+ if (
1066
+ typeof value !== "string"
1067
+ || value.length === 0
1068
+ || value.length > maximum
1069
+ || value.trim() !== value
1070
+ || hasUnsafeControlCharacters(value)
1071
+ ) throw new Error(`${fieldName} must be a bounded safe string.`);
1072
+ return value;
1073
+ }
1074
+
1075
+ function requireToken(value: unknown, fieldName: string): string {
1076
+ const output = requireString(value, fieldName, 128);
1077
+ if (!TOKEN.test(output) || DIRECT_URL.test(output)) throw new Error(`${fieldName} must be a bounded token.`);
1078
+ return output;
1079
+ }
1080
+
1081
+ function requirePathSegment(value: unknown, fieldName: string): string {
1082
+ const output = requireString(value, fieldName, 128);
1083
+ if (!PATH_SEGMENT.test(output)) throw new Error(`${fieldName} must be a safe path segment.`);
1084
+ return output;
1085
+ }
1086
+
1087
+ function requireReasonCode(value: unknown, fieldName: string): string {
1088
+ const output = requireString(value, fieldName, 128);
1089
+ if (!REASON_CODE.test(output)) throw new Error(`${fieldName} must be a reason code.`);
1090
+ return output;
1091
+ }
1092
+
1093
+ function requireSha256(value: unknown, fieldName: string): string {
1094
+ if (typeof value !== "string" || !SHA256.test(value)) throw new Error(`${fieldName} must be a lowercase SHA-256 digest.`);
1095
+ return value;
1096
+ }
1097
+
1098
+ function requireAttestation(value: unknown, fieldName: string): string {
1099
+ if (typeof value !== "string" || !ATTESTATION.test(value)) throw new Error(`${fieldName} must be a bounded attestation token.`);
1100
+ return value;
1101
+ }
1102
+
1103
+ function requireInteger(value: unknown, fieldName: string, minimum: number, maximum: number): number {
1104
+ if (!Number.isSafeInteger(value) || (value as number) < minimum || (value as number) > maximum) {
1105
+ throw new Error(`${fieldName} must be an integer from ${minimum} to ${maximum}.`);
1106
+ }
1107
+ return value as number;
1108
+ }
1109
+
1110
+ function requireNumber(value: unknown, fieldName: string, minimum: number, maximum: number): number {
1111
+ if (typeof value !== "number" || !Number.isFinite(value) || value < minimum || value > maximum) {
1112
+ throw new Error(`${fieldName} must be a finite number from ${minimum} to ${maximum}.`);
1113
+ }
1114
+ return value;
1115
+ }
1116
+
1117
+ function requireBoolean(value: unknown, fieldName: string): boolean {
1118
+ if (typeof value !== "boolean") throw new Error(`${fieldName} must be boolean.`);
1119
+ return value;
1120
+ }
1121
+
1122
+ function requireTimestamp(value: unknown, fieldName: string): string {
1123
+ if (typeof value !== "string" || !ISO_TIMESTAMP.test(value) || !Number.isFinite(Date.parse(value))) {
1124
+ throw new Error(`${fieldName} must be a canonical UTC timestamp.`);
1125
+ }
1126
+ return value;
1127
+ }
1128
+
1129
+ function requireEnum<const T extends readonly string[]>(value: unknown, allowed: T, fieldName: string): T[number] {
1130
+ if (typeof value !== "string" || !(allowed as readonly string[]).includes(value)) {
1131
+ throw new Error(`${fieldName} must be one of ${allowed.join(", ")}.`);
1132
+ }
1133
+ return value as T[number];
1134
+ }
1135
+
1136
+ function requireReasonCodes(value: unknown, fieldName: string): readonly string[] {
1137
+ const entries = asDenseArray(value, fieldName, 0, 32);
1138
+ const result = entries.map((entry, index) => requireReasonCode(entry, `${fieldName}[${index}]`));
1139
+ if (new Set(result).size !== result.length) throw new Error(`${fieldName} must not contain duplicates.`);
1140
+ return result;
1141
+ }
1142
+
1143
+ function requireShaTuple(value: unknown, fieldName: string): readonly [string, string, string, string] {
1144
+ const entries = asDenseArray(value, fieldName, 4, 4);
1145
+ return [
1146
+ requireSha256(entries[0], `${fieldName}[0]`),
1147
+ requireSha256(entries[1], `${fieldName}[1]`),
1148
+ requireSha256(entries[2], `${fieldName}[2]`),
1149
+ requireSha256(entries[3], `${fieldName}[3]`),
1150
+ ];
1151
+ }
1152
+
1153
+ function requireVector(value: unknown, fieldName: string): readonly [number, number, number] {
1154
+ const entries = asDenseArray(value, fieldName, 3, 3);
1155
+ return [
1156
+ requireNumber(entries[0], `${fieldName}[0]`, -PVOX_MAX_ABSOLUTE_COORDINATE_METRES, PVOX_MAX_ABSOLUTE_COORDINATE_METRES),
1157
+ requireNumber(entries[1], `${fieldName}[1]`, -PVOX_MAX_ABSOLUTE_COORDINATE_METRES, PVOX_MAX_ABSOLUTE_COORDINATE_METRES),
1158
+ requireNumber(entries[2], `${fieldName}[2]`, -PVOX_MAX_ABSOLUTE_COORDINATE_METRES, PVOX_MAX_ABSOLUTE_COORDINATE_METRES),
1159
+ ];
1160
+ }
1161
+
1162
+ function createBounds(value: unknown, fieldName: string): ModelBoundsMetresV2 {
1163
+ const record = asRecord(value, fieldName);
1164
+ exactKeys(record, ["min", "max"], fieldName);
1165
+ const min = requireVector(record.min, `${fieldName}.min`);
1166
+ const max = requireVector(record.max, `${fieldName}.max`);
1167
+ if (min.some((coordinate, index) => coordinate >= max[index]!)) throw new Error(`${fieldName} minimum values must be below maximum values.`);
1168
+ return { min, max };
1169
+ }
1170
+
1171
+ function approximatelyEqual(left: number, right: number): boolean {
1172
+ return Math.abs(left - right) <= Math.max(1e-6, Math.abs(left) * 1e-6, Math.abs(right) * 1e-6);
1173
+ }
1174
+
1175
+ function createHashAttestation(
1176
+ value: unknown,
1177
+ expectedDomain: PvoxHashAttestation["domain"],
1178
+ expectedDigest: string,
1179
+ fieldName: string,
1180
+ ): PvoxHashAttestation {
1181
+ const record = asRecord(value, fieldName);
1182
+ exactKeys(record, ["algorithm", "domain", "digest", "attestationToken"], fieldName);
1183
+ if (record.algorithm !== "sha256" || record.domain !== expectedDomain) {
1184
+ throw new Error(`${fieldName} must use the governed SHA-256 domain.`);
1185
+ }
1186
+ const digest = requireSha256(record.digest, `${fieldName}.digest`);
1187
+ if (digest !== expectedDigest) throw new Error(`${fieldName}.digest must match its bound contract hash.`);
1188
+ return {
1189
+ algorithm: "sha256",
1190
+ domain: expectedDomain,
1191
+ digest,
1192
+ attestationToken: requireAttestation(record.attestationToken, `${fieldName}.attestationToken`),
1193
+ };
1194
+ }
1195
+
1196
+ /** Canonical JSON used only as a versioned hash preimage; SHA-256 is computed by the producing service. */
1197
+ export function canonicalizePvoxHashProjectionV1(value: unknown): string {
1198
+ assertDataOnlyTree(value, "PVOX hash projection");
1199
+ return canonicalizeGpuContract(value);
1200
+ }
1201
+
1202
+ /** Exact full-v2 request projection excluding only its externally computed digest. */
1203
+ export function canonicalizeModelRequestSemanticProfileV1(request: ModelRequestSpecV2): string {
1204
+ return canonicalizePvoxHashProjectionV1({
1205
+ contractVersion: request.contractVersion,
1206
+ policyProfileId: request.policyProfileId,
1207
+ query: request.query,
1208
+ revision: request.revision,
1209
+ ...(request.locale === undefined ? {} : { locale: request.locale }),
1210
+ ...(request.rankerId === undefined ? {} : { rankerId: request.rankerId }),
1211
+ hardConstraints: request.hardConstraints,
1212
+ softPreferences: request.softPreferences,
1213
+ exclusions: request.exclusions,
1214
+ sourceIngestionLimits: request.sourceIngestionLimits,
1215
+ pvoxRuntimeProfile: request.pvoxRuntimeProfile,
1216
+ });
1217
+ }
1218
+
1219
+ function alignPvoxSectionBytes(value: number): number {
1220
+ return Math.ceil(value / PVOX_SECTION_ALIGNMENT_BYTES) * PVOX_SECTION_ALIGNMENT_BYTES;
1221
+ }
1222
+
1223
+ /** Conservative exact fixed-record lower bound for a declared PVOX technical profile. */
1224
+ export function calculatePvoxMinimumArtifactByteLength(profile: VoxelTechnicalProfile): number {
1225
+ const sectionPayloads = [
1226
+ profile.partitionCount * PVOX_PARTITION_RECORD_BYTES,
1227
+ profile.lodCount * PVOX_LOD_RECORD_BYTES,
1228
+ profile.partitionCount * profile.lodCount * PVOX_ROOT_RECORD_BYTES,
1229
+ profile.levelSpanRecordCount * PVOX_LEVEL_SPAN_RECORD_BYTES,
1230
+ profile.hierarchyNodeCount * PVOX_HIERARCHY_NODE_RECORD_BYTES,
1231
+ profile.brickCount * PVOX_MIN_BRICK_DESCRIPTOR_BYTES,
1232
+ profile.brickCount * PVOX_RENDER_BRICK_MASK_BYTES
1233
+ + profile.encodedSurfaceSampleCount * PVOX_MIN_ENCODED_SURFACE_SAMPLE_BYTES,
1234
+ profile.surfacePropertyCount * PVOX_SURFACE_PROPERTY_RECORD_BYTES,
1235
+ profile.physicalPaletteRecordCount * PVOX_PHYSICAL_PROPERTY_RECORD_BYTES,
1236
+ profile.physicalEvidenceRecordCount * PVOX_PHYSICAL_EVIDENCE_RECORD_BYTES,
1237
+ profile.materialRegionCount * PVOX_MATERIAL_REGION_RECORD_BYTES,
1238
+ profile.interiorLayerRecordCount * PVOX_INTERIOR_LAYER_RECORD_BYTES,
1239
+ profile.massPropertyRecordCount * PVOX_MASS_PROPERTY_RECORD_BYTES,
1240
+ profile.bondRecordCount * PVOX_BOND_RECORD_BYTES,
1241
+ ...(profile.hasCollision ? [
1242
+ profile.partitionCount * profile.lodCount * PVOX_ROOT_RECORD_BYTES,
1243
+ profile.levelSpanRecordCount * PVOX_LEVEL_SPAN_RECORD_BYTES,
1244
+ profile.collisionHierarchyNodeCount * PVOX_HIERARCHY_NODE_RECORD_BYTES,
1245
+ profile.collisionBrickCount * PVOX_MIN_BRICK_DESCRIPTOR_BYTES,
1246
+ profile.collisionBrickCount * PVOX_COLLISION_BRICK_MASK_BYTES
1247
+ + profile.collisionEncodedSurfaceSampleCount * PVOX_MIN_ENCODED_COLLISION_SAMPLE_BYTES,
1248
+ ] : []),
1249
+ ];
1250
+ return alignPvoxSectionBytes(PVOX_HEADER_BYTE_LENGTH + profile.sectionCount * PVOX_DIRECTORY_ENTRY_BYTE_LENGTH)
1251
+ + sectionPayloads.reduce((total, byteLength) => total + alignPvoxSectionBytes(byteLength), 0);
1252
+ }
1253
+
1254
+ function assertFloorCentredBounds(bounds: ModelBoundsMetresV2, fieldName: string): void {
1255
+ if (!approximatelyEqual(bounds.min[1], 0)
1256
+ || !approximatelyEqual(bounds.min[0] + bounds.max[0], 0)
1257
+ || !approximatelyEqual(bounds.min[2] + bounds.max[2], 0)) {
1258
+ throw new Error(`${fieldName} must use a floor-centred origin with centred X/Z bounds.`);
1259
+ }
1260
+ }
1261
+
1262
+ function createDimensions(value: unknown, fieldName: string): ModelDimensionsMetresV2 {
1263
+ const record = asRecord(value, fieldName);
1264
+ exactKeys(record, ["width", "height", "depth"], fieldName);
1265
+ return {
1266
+ width: requireNumber(record.width, `${fieldName}.width`, Number.EPSILON, 1_000_000_000),
1267
+ height: requireNumber(record.height, `${fieldName}.height`, Number.EPSILON, 1_000_000_000),
1268
+ depth: requireNumber(record.depth, `${fieldName}.depth`, Number.EPSILON, 1_000_000_000),
1269
+ };
1270
+ }
1271
+
1272
+ function assertCoordinateSystem(value: unknown, fieldName: string): void {
1273
+ const record = asRecord(value, fieldName);
1274
+ exactKeys(record, ["unit", "upAxis", "forwardAxis", "origin", "outwardFaceWinding"], fieldName);
1275
+ for (const [key, expected] of Object.entries(CANONICAL_MODEL_COORDINATE_SYSTEM)) {
1276
+ if (record[key] !== expected) throw new Error(`${fieldName} must use the canonical model coordinate system.`);
1277
+ }
1278
+ }
1279
+
1280
+ type PvoxResourceScope =
1281
+ | { readonly kind: "resolution"; readonly resolutionId: string; readonly candidateId: string; readonly sha256: string }
1282
+ | { readonly kind: "catalog"; readonly assetId: string; readonly version: string; readonly sha256: string };
1283
+
1284
+ function parsePvoxResourceScope(uri: string): PvoxResourceScope {
1285
+ const resolutionMatch = PVOX_RESOLUTION_RESOURCE.exec(uri);
1286
+ if (resolutionMatch !== null) {
1287
+ return {
1288
+ kind: "resolution",
1289
+ resolutionId: requirePathSegment(resolutionMatch[1], "PVOX resource resolutionId"),
1290
+ candidateId: requirePathSegment(resolutionMatch[2], "PVOX resource candidateId"),
1291
+ sha256: requireSha256(resolutionMatch[3], "PVOX resource hash"),
1292
+ };
1293
+ }
1294
+ const catalogMatch = PVOX_CATALOG_RESOURCE.exec(uri);
1295
+ if (catalogMatch !== null) {
1296
+ return {
1297
+ kind: "catalog",
1298
+ assetId: requirePathSegment(catalogMatch[1], "PVOX resource assetId"),
1299
+ version: assertImmutableAssetVersion(catalogMatch[2]),
1300
+ sha256: requireSha256(catalogMatch[3], "PVOX resource hash"),
1301
+ };
1302
+ }
1303
+ throw new Error("PVOX artifact URI must be an authenticated canonical PVOX MCP resource.");
1304
+ }
1305
+
1306
+ function createResource(value: unknown): PvoxAuthenticatedResourceMetadata {
1307
+ const record = asRecord(value, "PvoxAssetManifestV1.artifact");
1308
+ exactKeys(record, ["uri", "byteLength", "sha256", "contentType", "authenticated"], "PvoxAssetManifestV1.artifact");
1309
+ const uri = requireString(record.uri, "PvoxAssetManifestV1.artifact.uri", 512);
1310
+ if (uri.includes("%") || uri.includes("?") || uri.includes("#") || uri.includes("..") || DIRECT_URL.test(uri)) {
1311
+ throw new Error("PvoxAssetManifestV1.artifact.uri must be an authenticated canonical MCP resource.");
1312
+ }
1313
+ const scope = parsePvoxResourceScope(uri);
1314
+ const sha256 = requireSha256(record.sha256, "PvoxAssetManifestV1.artifact.sha256");
1315
+ if (scope.sha256 !== sha256) throw new Error("PvoxAssetManifestV1.artifact URI hash must match sha256.");
1316
+ if (record.contentType !== PVOX_CONTENT_TYPE) throw new Error("PvoxAssetManifestV1.artifact.contentType must be the PVOX content type.");
1317
+ if (record.authenticated !== true) throw new Error("PvoxAssetManifestV1.artifact must be authenticated.");
1318
+ return {
1319
+ uri,
1320
+ byteLength: requireInteger(record.byteLength, "PvoxAssetManifestV1.artifact.byteLength", 1, PVOX_DEFAULT_LIMITS.maximumArtifactBytes),
1321
+ sha256,
1322
+ contentType: PVOX_CONTENT_TYPE,
1323
+ authenticated: true,
1324
+ };
1325
+ }
1326
+
1327
+ function createPage(value: unknown, index: number): PvoxPageRecord {
1328
+ const fieldName = `PvoxAssetManifestV1.pages[${index}]`;
1329
+ const record = asRecord(value, fieldName);
1330
+ const pageKind = requireEnum(record.pageKind, PVOX_PAGE_KINDS, `${fieldName}.pageKind`);
1331
+ exactKeys(record, ["pageIndex", "pageKind", "byteOffset", "byteLength", "decodedByteLength", "sha256", "spatialKey"], fieldName);
1332
+ const byteLength = requireInteger(record.byteLength, `${fieldName}.byteLength`, PVOX_PAGE_SIZE_BYTES, PVOX_PAGE_SIZE_BYTES);
1333
+ const decodedByteLength = requireInteger(record.decodedByteLength, `${fieldName}.decodedByteLength`, PVOX_PAGE_SIZE_BYTES, PVOX_PAGE_SIZE_BYTES);
1334
+ if (decodedByteLength !== byteLength) throw new Error(`${fieldName} decodedByteLength must equal byteLength because PVOX pages are never compressed.`);
1335
+ let spatialKey: PvoxPageSpatialKey | undefined;
1336
+ if (record.spatialKey !== undefined) {
1337
+ const spatialRecord = asRecord(record.spatialKey, `${fieldName}.spatialKey`);
1338
+ exactKeys(spatialRecord, ["lodLevel", "partitionIndex", "hierarchyDepth", "minimumMortonCode", "maximumMortonCode"], `${fieldName}.spatialKey`);
1339
+ const minimumMortonCode = requireString(spatialRecord.minimumMortonCode, `${fieldName}.spatialKey.minimumMortonCode`, 16);
1340
+ const maximumMortonCode = requireString(spatialRecord.maximumMortonCode, `${fieldName}.spatialKey.maximumMortonCode`, 16);
1341
+ if (!MORTON_CODE.test(minimumMortonCode) || !MORTON_CODE.test(maximumMortonCode) || minimumMortonCode > maximumMortonCode) {
1342
+ throw new Error(`${fieldName} spatial Morton range must be canonical and ordered.`);
1343
+ }
1344
+ spatialKey = {
1345
+ lodLevel: requireInteger(spatialRecord.lodLevel, `${fieldName}.spatialKey.lodLevel`, 0, PVOX_DEFAULT_LIMITS.maximumLodCount - 1) as PvoxLodLevel,
1346
+ partitionIndex: requireInteger(spatialRecord.partitionIndex, `${fieldName}.spatialKey.partitionIndex`, 0, PVOX_DEFAULT_LIMITS.maximumPartitions - 1),
1347
+ hierarchyDepth: requireInteger(spatialRecord.hierarchyDepth, `${fieldName}.spatialKey.hierarchyDepth`, 0, PVOX_DEFAULT_LIMITS.maximumHierarchyDepth),
1348
+ minimumMortonCode,
1349
+ maximumMortonCode,
1350
+ };
1351
+ }
1352
+ const spatialPage = pageKind === "render-field" || pageKind === "collision-field";
1353
+ if (spatialPage !== (spatialKey !== undefined)) {
1354
+ throw new Error(`${fieldName} render/collision field pages require spatialKey; metadata/LOD-structure pages forbid it.`);
1355
+ }
1356
+ return {
1357
+ pageIndex: requireInteger(record.pageIndex, `${fieldName}.pageIndex`, 0, PVOX_DEFAULT_LIMITS.maximumPages - 1),
1358
+ pageKind,
1359
+ byteOffset: requireInteger(record.byteOffset, `${fieldName}.byteOffset`, 0, PVOX_DEFAULT_LIMITS.maximumArtifactBytes - 1),
1360
+ byteLength,
1361
+ decodedByteLength,
1362
+ sha256: requireSha256(record.sha256, `${fieldName}.sha256`),
1363
+ ...(spatialKey === undefined ? {} : { spatialKey }),
1364
+ };
1365
+ }
1366
+
1367
+ function createLod(value: unknown, index: number, pageCount: number): PvoxLodRecord {
1368
+ const fieldName = `PvoxAssetManifestV1.lods[${index}]`;
1369
+ const record = asRecord(value, fieldName);
1370
+ exactKeys(record, [
1371
+ "level", "firstPageIndex", "pageCount", "brickCount", "cellSizeMetres",
1372
+ "maximumSurfaceErrorMetres", "p99SurfaceErrorMetres", "silhouetteIou",
1373
+ "maximumContourDisplacementPx", "renderedSsim", "p95DeltaE2000",
1374
+ "normalizedMaterialError",
1375
+ ], fieldName);
1376
+ const firstPageIndex = requireInteger(record.firstPageIndex, `${fieldName}.firstPageIndex`, 0, pageCount - 1);
1377
+ const lodPageCount = requireInteger(record.pageCount, `${fieldName}.pageCount`, 1, pageCount);
1378
+ if (firstPageIndex + lodPageCount > pageCount) throw new Error(`${fieldName} page range exceeds the PVOX page set.`);
1379
+ const maximumSurfaceErrorMetres = requireNumber(record.maximumSurfaceErrorMetres, `${fieldName}.maximumSurfaceErrorMetres`, 0, 1_000_000);
1380
+ const p99SurfaceErrorMetres = requireNumber(record.p99SurfaceErrorMetres, `${fieldName}.p99SurfaceErrorMetres`, 0, maximumSurfaceErrorMetres);
1381
+ return {
1382
+ level: requireInteger(record.level, `${fieldName}.level`, 0, 3) as PvoxLodLevel,
1383
+ firstPageIndex,
1384
+ pageCount: lodPageCount,
1385
+ brickCount: requireInteger(record.brickCount, `${fieldName}.brickCount`, 1, PVOX_DEFAULT_LIMITS.maximumBricks),
1386
+ cellSizeMetres: requireNumber(record.cellSizeMetres, `${fieldName}.cellSizeMetres`, Number.EPSILON, 1_000_000),
1387
+ maximumSurfaceErrorMetres,
1388
+ p99SurfaceErrorMetres,
1389
+ silhouetteIou: requireNumber(record.silhouetteIou, `${fieldName}.silhouetteIou`, 0, 1),
1390
+ maximumContourDisplacementPx: requireNumber(record.maximumContourDisplacementPx, `${fieldName}.maximumContourDisplacementPx`, 0, 1_000_000),
1391
+ renderedSsim: requireNumber(record.renderedSsim, `${fieldName}.renderedSsim`, 0, 1),
1392
+ p95DeltaE2000: requireNumber(record.p95DeltaE2000, `${fieldName}.p95DeltaE2000`, 0, 100),
1393
+ normalizedMaterialError: requireNumber(record.normalizedMaterialError, `${fieldName}.normalizedMaterialError`, 0, 1),
1394
+ };
1395
+ }
1396
+
1397
+ function createSourceIngestionLimitsV2(value: unknown): ModelSourceIngestionLimitsV2 {
1398
+ const fieldName = "ModelRequestSpecV2.sourceIngestionLimits";
1399
+ const record = asRecord(value, fieldName);
1400
+ exactKeys(record, [
1401
+ "maximumDownloadBytes",
1402
+ "maximumExpandedBytes",
1403
+ "maximumArchiveEntries",
1404
+ "maximumSourceFiles",
1405
+ "maximumDecodedTextureBytes",
1406
+ "maximumTextureDimensionPx",
1407
+ ], fieldName);
1408
+ const limits: ModelSourceIngestionLimitsV2 = {
1409
+ maximumDownloadBytes: requireInteger(record.maximumDownloadBytes, `${fieldName}.maximumDownloadBytes`, 1, PVOX_DEFAULT_SOURCE_INGESTION_LIMITS.maximumDownloadBytes),
1410
+ maximumExpandedBytes: requireInteger(record.maximumExpandedBytes, `${fieldName}.maximumExpandedBytes`, 1, PVOX_DEFAULT_SOURCE_INGESTION_LIMITS.maximumExpandedBytes),
1411
+ maximumArchiveEntries: requireInteger(record.maximumArchiveEntries, `${fieldName}.maximumArchiveEntries`, 1, PVOX_DEFAULT_SOURCE_INGESTION_LIMITS.maximumArchiveEntries),
1412
+ maximumSourceFiles: requireInteger(record.maximumSourceFiles, `${fieldName}.maximumSourceFiles`, 1, PVOX_DEFAULT_SOURCE_INGESTION_LIMITS.maximumSourceFiles),
1413
+ maximumDecodedTextureBytes: requireInteger(record.maximumDecodedTextureBytes, `${fieldName}.maximumDecodedTextureBytes`, 1, PVOX_DEFAULT_SOURCE_INGESTION_LIMITS.maximumDecodedTextureBytes),
1414
+ maximumTextureDimensionPx: requireInteger(record.maximumTextureDimensionPx, `${fieldName}.maximumTextureDimensionPx`, 1, PVOX_DEFAULT_SOURCE_INGESTION_LIMITS.maximumTextureDimensionPx),
1415
+ };
1416
+ if (limits.maximumExpandedBytes < limits.maximumDownloadBytes) {
1417
+ throw new Error(`${fieldName}.maximumExpandedBytes must cover maximumDownloadBytes.`);
1418
+ }
1419
+ if (limits.maximumSourceFiles < limits.maximumArchiveEntries) {
1420
+ throw new Error(`${fieldName}.maximumSourceFiles must cover maximumArchiveEntries.`);
1421
+ }
1422
+ if (limits.maximumDecodedTextureBytes > limits.maximumExpandedBytes) {
1423
+ throw new Error(`${fieldName}.maximumDecodedTextureBytes must not exceed maximumExpandedBytes.`);
1424
+ }
1425
+ return limits;
1426
+ }
1427
+
1428
+ function createPvoxRuntimeLimitsV1(value: unknown): PvoxRuntimeLimitsV1 {
1429
+ const fieldName = "ModelRequestSpecV2.pvoxRuntimeProfile.limits";
1430
+ const record = asRecord(value, fieldName);
1431
+ exactKeys(record, [
1432
+ "maximumArtifactBytes",
1433
+ "maximumPages",
1434
+ "maximumHierarchyDepth",
1435
+ "maximumHierarchyNodes",
1436
+ "maximumBricks",
1437
+ "maximumLogicalVoxels",
1438
+ "maximumEncodedSurfaceSamples",
1439
+ "maximumSurfaceProperties",
1440
+ "maximumPhysicalPaletteRecords",
1441
+ "maximumPhysicalEvidenceEntries",
1442
+ "maximumMaterialRegions",
1443
+ "maximumInteriorLayers",
1444
+ "maximumMassPropertyRecords",
1445
+ "maximumBondRecords",
1446
+ "maximumPartitions",
1447
+ "maximumLodCount",
1448
+ "maximumCpuResidentBytes",
1449
+ "maximumGpuResidentBytes",
1450
+ ], fieldName);
1451
+ const limits: PvoxRuntimeLimitsV1 = {
1452
+ maximumArtifactBytes: requireInteger(record.maximumArtifactBytes, `${fieldName}.maximumArtifactBytes`, 1, PVOX_DEFAULT_LIMITS.maximumArtifactBytes),
1453
+ maximumPages: requireInteger(record.maximumPages, `${fieldName}.maximumPages`, 1, PVOX_DEFAULT_LIMITS.maximumPages),
1454
+ maximumHierarchyDepth: requireInteger(record.maximumHierarchyDepth, `${fieldName}.maximumHierarchyDepth`, 0, PVOX_DEFAULT_LIMITS.maximumHierarchyDepth),
1455
+ maximumHierarchyNodes: requireInteger(record.maximumHierarchyNodes, `${fieldName}.maximumHierarchyNodes`, 1, PVOX_DEFAULT_LIMITS.maximumHierarchyNodes),
1456
+ maximumBricks: requireInteger(record.maximumBricks, `${fieldName}.maximumBricks`, 1, PVOX_DEFAULT_LIMITS.maximumBricks),
1457
+ maximumLogicalVoxels: requireInteger(record.maximumLogicalVoxels, `${fieldName}.maximumLogicalVoxels`, 1, PVOX_DEFAULT_LIMITS.maximumLogicalVoxels),
1458
+ maximumEncodedSurfaceSamples: requireInteger(record.maximumEncodedSurfaceSamples, `${fieldName}.maximumEncodedSurfaceSamples`, 1, PVOX_DEFAULT_LIMITS.maximumEncodedSurfaceSamples),
1459
+ maximumSurfaceProperties: requireInteger(record.maximumSurfaceProperties, `${fieldName}.maximumSurfaceProperties`, 1, PVOX_DEFAULT_LIMITS.maximumSurfaceProperties),
1460
+ maximumPhysicalPaletteRecords: requireInteger(record.maximumPhysicalPaletteRecords, `${fieldName}.maximumPhysicalPaletteRecords`, 1, PVOX_DEFAULT_LIMITS.maximumPhysicalProperties),
1461
+ maximumPhysicalEvidenceEntries: requireInteger(record.maximumPhysicalEvidenceEntries, `${fieldName}.maximumPhysicalEvidenceEntries`, 1, PVOX_DEFAULT_LIMITS.maximumPhysicalEvidenceEntries),
1462
+ maximumMaterialRegions: requireInteger(record.maximumMaterialRegions, `${fieldName}.maximumMaterialRegions`, 1, PVOX_DEFAULT_LIMITS.maximumMaterialRegions),
1463
+ maximumInteriorLayers: requireInteger(record.maximumInteriorLayers, `${fieldName}.maximumInteriorLayers`, 1, PVOX_DEFAULT_LIMITS.maximumInteriorLayers),
1464
+ maximumMassPropertyRecords: requireInteger(record.maximumMassPropertyRecords, `${fieldName}.maximumMassPropertyRecords`, 1, PVOX_DEFAULT_LIMITS.maximumMassPropertyRecords),
1465
+ maximumBondRecords: requireInteger(record.maximumBondRecords, `${fieldName}.maximumBondRecords`, 0, PVOX_DEFAULT_LIMITS.maximumBondRecords),
1466
+ maximumPartitions: requireInteger(record.maximumPartitions, `${fieldName}.maximumPartitions`, 1, PVOX_DEFAULT_LIMITS.maximumPartitions),
1467
+ maximumLodCount: requireInteger(record.maximumLodCount, `${fieldName}.maximumLodCount`, 1, PVOX_DEFAULT_LIMITS.maximumLodCount),
1468
+ maximumCpuResidentBytes: requireInteger(record.maximumCpuResidentBytes, `${fieldName}.maximumCpuResidentBytes`, 1, PVOX_DEFAULT_LIMITS.maximumCpuResidentBytes),
1469
+ maximumGpuResidentBytes: requireInteger(record.maximumGpuResidentBytes, `${fieldName}.maximumGpuResidentBytes`, 1, PVOX_DEFAULT_LIMITS.maximumGpuResidentBytes),
1470
+ };
1471
+ if (limits.maximumArtifactBytes > limits.maximumPages * PVOX_PAGE_SIZE_BYTES) {
1472
+ throw new Error(`${fieldName}.maximumPages cannot contain maximumArtifactBytes.`);
1473
+ }
1474
+ if (limits.maximumLogicalVoxels > limits.maximumBricks * PVOX_BRICK_EDGE_VOXELS ** 3) {
1475
+ throw new Error(`${fieldName}.maximumLogicalVoxels cannot exceed the requested brick capacity.`);
1476
+ }
1477
+ if (limits.maximumEncodedSurfaceSamples > limits.maximumLogicalVoxels) {
1478
+ throw new Error(`${fieldName}.maximumEncodedSurfaceSamples cannot exceed maximumLogicalVoxels.`);
1479
+ }
1480
+ return limits;
1481
+ }
1482
+
1483
+ function createPvoxRuntimeRequestProfileV1(value: unknown): PvoxRuntimeRequestProfileV1 {
1484
+ const fieldName = "ModelRequestSpecV2.pvoxRuntimeProfile";
1485
+ const record = asRecord(value, fieldName);
1486
+ exactKeys(record, ["profileId", "fidelityProfileId", "capabilityProfileId", "geometryMode", "requiredCapabilities", "limits"], fieldName);
1487
+ const requestedCapabilityEntries = asDenseArray(record.requiredCapabilities, `${fieldName}.requiredCapabilities`, 1, PVOX_CAPABILITIES.length);
1488
+ const requiredCapabilities = requestedCapabilityEntries.map((entry, index) => requireEnum(entry, PVOX_CAPABILITIES, `${fieldName}.requiredCapabilities[${index}]`));
1489
+ if (new Set(requiredCapabilities).size !== requiredCapabilities.length) {
1490
+ throw new Error(`${fieldName}.requiredCapabilities must not contain duplicates.`);
1491
+ }
1492
+ if (!requiredCapabilities.includes("rendering")) {
1493
+ throw new Error(`${fieldName}.requiredCapabilities must include rendering.`);
1494
+ }
1495
+ const capabilityProfileId = requireEnum(record.capabilityProfileId, PVOX_RUNTIME_CAPABILITY_PROFILE_IDS, `${fieldName}.capabilityProfileId`);
1496
+ const profileRequirements: Readonly<Record<PvoxRuntimeCapabilityProfileId, readonly PvoxCapability[]>> = {
1497
+ "static-render-v1": ["rendering"],
1498
+ "world-editable-v1": ["rendering", "collision", "destruction", "thermal", "moisture", "fluid-boundary"],
1499
+ "deformable-v1": ["rendering", "deformation", "animation"],
1500
+ };
1501
+ if (profileRequirements[capabilityProfileId].some((capability) => !requiredCapabilities.includes(capability))) {
1502
+ throw new Error(`${fieldName}.requiredCapabilities do not satisfy capabilityProfileId.`);
1503
+ }
1504
+ const limits = createPvoxRuntimeLimitsV1(record.limits);
1505
+ if (capabilityProfileId === "world-editable-v1"
1506
+ && limits.maximumPhysicalEvidenceEntries < limits.maximumMaterialRegions * PVOX_PHYSICAL_PROPERTIES.length) {
1507
+ throw new Error(`${fieldName}.limits must budget a complete physical-property matrix for every allowed material region.`);
1508
+ }
1509
+ return {
1510
+ profileId: requireToken(record.profileId, `${fieldName}.profileId`),
1511
+ fidelityProfileId: requireEnum(record.fidelityProfileId, PVOX_FIDELITY_PROFILE_IDS, `${fieldName}.fidelityProfileId`),
1512
+ capabilityProfileId,
1513
+ geometryMode: requireEnum(record.geometryMode, ["solid", "shell", "mixed", "auto"] as const, `${fieldName}.geometryMode`),
1514
+ requiredCapabilities,
1515
+ limits,
1516
+ };
1517
+ }
1518
+
1519
+ /** Validate and deeply freeze one normalized PVOX model request. */
1520
+ export function createModelRequestSpecV2(input: unknown): ModelRequestSpecV2 {
1521
+ const fieldName = "ModelRequestSpecV2";
1522
+ const record = asRecord(input, fieldName);
1523
+ assertDataOnlyTree(record, fieldName);
1524
+ exactKeys(record, [
1525
+ "contractVersion",
1526
+ "policyProfileId",
1527
+ "requestSemanticProfileHash",
1528
+ "query",
1529
+ "revision",
1530
+ "locale",
1531
+ "rankerId",
1532
+ "hardConstraints",
1533
+ "softPreferences",
1534
+ "exclusions",
1535
+ "sourceIngestionLimits",
1536
+ "pvoxRuntimeProfile",
1537
+ ], fieldName);
1538
+ if (record.contractVersion !== MODEL_RESOLUTION_V2_CONTRACT_VERSION) {
1539
+ throw new Error(`${fieldName}.contractVersion is unsupported.`);
1540
+ }
1541
+ if (record.policyProfileId !== PVOX_MODEL_REQUEST_POLICY_ID) {
1542
+ throw new Error(`${fieldName}.policyProfileId must be ${PVOX_MODEL_REQUEST_POLICY_ID}.`);
1543
+ }
1544
+ const hardConstraintsRecord = asRecord(record.hardConstraints ?? {}, `${fieldName}.hardConstraints`);
1545
+ exactKeys(hardConstraintsRecord, ["boundsMetres", "dimensionsMetres", "collision", "partition"], `${fieldName}.hardConstraints`);
1546
+ const softPreferencesRecord = asRecord(record.softPreferences ?? {}, `${fieldName}.softPreferences`);
1547
+ for (const field of ["materials", "colors", "tags"] as const) {
1548
+ if (softPreferencesRecord[field] !== undefined) {
1549
+ asDenseArray(softPreferencesRecord[field], `${fieldName}.softPreferences.${field}`, 0, 32);
1550
+ }
1551
+ }
1552
+ const exclusions = asDenseArray(record.exclusions ?? [], `${fieldName}.exclusions`, 0, 32);
1553
+ const normalizedV1 = createModelRequestSpec({
1554
+ query: record.query,
1555
+ revision: record.revision,
1556
+ ...(record.locale === undefined ? {} : { locale: record.locale }),
1557
+ ...(record.rankerId === undefined ? {} : { rankerId: record.rankerId }),
1558
+ hardConstraints: hardConstraintsRecord,
1559
+ softPreferences: softPreferencesRecord,
1560
+ exclusions,
1561
+ });
1562
+ const hardConstraints: ModelHardConstraintsV2 = {
1563
+ ...(normalizedV1.hardConstraints.boundsMetres === undefined ? {} : { boundsMetres: normalizedV1.hardConstraints.boundsMetres }),
1564
+ ...(normalizedV1.hardConstraints.dimensionsMetres === undefined ? {} : { dimensionsMetres: normalizedV1.hardConstraints.dimensionsMetres }),
1565
+ ...(normalizedV1.hardConstraints.collision === undefined ? {} : { collision: normalizedV1.hardConstraints.collision }),
1566
+ ...(normalizedV1.hardConstraints.partition === undefined ? {} : { partition: normalizedV1.hardConstraints.partition }),
1567
+ };
1568
+ return deepFreeze({
1569
+ contractVersion: MODEL_RESOLUTION_V2_CONTRACT_VERSION,
1570
+ policyProfileId: PVOX_MODEL_REQUEST_POLICY_ID,
1571
+ requestSemanticProfileHash: requireSha256(record.requestSemanticProfileHash, `${fieldName}.requestSemanticProfileHash`),
1572
+ query: normalizedV1.query,
1573
+ revision: normalizedV1.revision,
1574
+ ...(normalizedV1.locale === undefined ? {} : { locale: normalizedV1.locale }),
1575
+ ...(normalizedV1.rankerId === undefined ? {} : { rankerId: normalizedV1.rankerId }),
1576
+ hardConstraints,
1577
+ softPreferences: normalizedV1.softPreferences,
1578
+ exclusions: normalizedV1.exclusions,
1579
+ sourceIngestionLimits: createSourceIngestionLimitsV2(record.sourceIngestionLimits),
1580
+ pvoxRuntimeProfile: createPvoxRuntimeRequestProfileV1(record.pvoxRuntimeProfile),
1581
+ });
1582
+ }
1583
+
1584
+ /** Validate and deeply freeze authenticated PVOX artifact metadata. */
1585
+ export function createPvoxAssetManifestV1(input: unknown): PvoxAssetManifestV1 {
1586
+ const record = asRecord(input, "PvoxAssetManifestV1");
1587
+ exactKeys(record, [
1588
+ "manifestVersion", "representation", "formatVersion", "contentType", "fileExtension",
1589
+ "magic", "brickEdgeVoxels", "pageSizeBytes", "geometryMode", "sourceContentHash",
1590
+ "canonicalDocumentHash", "rootHash", "directoryHash", "pageSetHash", "binaryClosureHash",
1591
+ "validationEvidenceHash", "compilationInputHash", "runtimeRequestProfileHash",
1592
+ "headerByteLength", "directoryEntryByteLength", "sectionCount", "maximumRunsPerBrick",
1593
+ "maximumLocalSamplesPerBrick", "maximumEncodedBrickPayloadBytes",
1594
+ "coordinateSystem", "boundsMetres", "artifact", "pages", "lods",
1595
+ ], "PvoxAssetManifestV1");
1596
+ if (record.manifestVersion !== PVOX_ASSET_MANIFEST_VERSION) throw new Error("PvoxAssetManifestV1.manifestVersion is unsupported.");
1597
+ if (record.representation !== "pvox") throw new Error("PvoxAssetManifestV1.representation must be pvox.");
1598
+ const formatVersion = asRecord(record.formatVersion, "PvoxAssetManifestV1.formatVersion");
1599
+ exactKeys(formatVersion, ["major", "minor"], "PvoxAssetManifestV1.formatVersion");
1600
+ if (formatVersion.major !== PVOX_FORMAT_VERSION.major || formatVersion.minor !== PVOX_FORMAT_VERSION.minor) throw new Error("PvoxAssetManifestV1.formatVersion is unsupported.");
1601
+ if (record.contentType !== PVOX_CONTENT_TYPE || record.fileExtension !== PVOX_FILE_EXTENSION || record.magic !== PVOX_MAGIC) {
1602
+ throw new Error("PvoxAssetManifestV1 must declare the canonical PVOX content type, extension, and magic.");
1603
+ }
1604
+ if (record.brickEdgeVoxels !== PVOX_BRICK_EDGE_VOXELS || record.pageSizeBytes !== PVOX_PAGE_SIZE_BYTES) {
1605
+ throw new Error("PvoxAssetManifestV1 must use fixed 8-cubed bricks and 64-KiB pages.");
1606
+ }
1607
+ assertCoordinateSystem(record.coordinateSystem, "PvoxAssetManifestV1.coordinateSystem");
1608
+ const boundsMetres = createBounds(record.boundsMetres, "PvoxAssetManifestV1.boundsMetres");
1609
+ assertFloorCentredBounds(boundsMetres, "PvoxAssetManifestV1.boundsMetres");
1610
+ const artifact = createResource(record.artifact);
1611
+ if (artifact.byteLength % PVOX_PAGE_SIZE_BYTES !== 0) throw new Error("PvoxAssetManifestV1 artifact byteLength must contain whole 64-KiB pages.");
1612
+ const pageEntries = asDenseArray(record.pages, "PvoxAssetManifestV1.pages", 1, PVOX_DEFAULT_LIMITS.maximumPages);
1613
+ const pages = pageEntries.map(createPage);
1614
+ let expectedOffset = 0;
1615
+ const previousSpatialMaximumByScope = new Map<string, string>();
1616
+ for (const [index, current] of pages.entries()) {
1617
+ if (current.pageIndex !== index) throw new Error("PvoxAssetManifestV1 pageIndex values must be contiguous from zero.");
1618
+ if (current.byteOffset !== expectedOffset || current.byteOffset % PVOX_PAGE_SIZE_BYTES !== 0) {
1619
+ throw new Error("PvoxAssetManifestV1 page byteOffset values must be contiguous and page aligned.");
1620
+ }
1621
+ const spatialKey = current.spatialKey;
1622
+ if (spatialKey !== undefined) {
1623
+ const scope = `${current.pageKind}\u0000${spatialKey.lodLevel}\u0000${spatialKey.partitionIndex}`;
1624
+ const previousMaximum = previousSpatialMaximumByScope.get(scope);
1625
+ if (previousMaximum !== undefined && spatialKey.minimumMortonCode <= previousMaximum) {
1626
+ throw new Error("PvoxAssetManifestV1 page Morton ranges must be ordered and non-overlapping within each kind, LOD, and partition.");
1627
+ }
1628
+ previousSpatialMaximumByScope.set(scope, spatialKey.maximumMortonCode);
1629
+ }
1630
+ expectedOffset += current.byteLength;
1631
+ }
1632
+ if (expectedOffset !== artifact.byteLength) throw new Error("PvoxAssetManifestV1 pages must exactly cover the artifact bytes.");
1633
+ const lodEntries = asDenseArray(record.lods, "PvoxAssetManifestV1.lods", 1, 4);
1634
+ const lods = lodEntries.map((entry, index) => createLod(entry, index, pages.length));
1635
+ for (const [index, current] of lods.entries()) {
1636
+ if (current.level !== index) throw new Error("PvoxAssetManifestV1 LOD levels must be contiguous from LOD0.");
1637
+ const previous = lods[index - 1];
1638
+ if (previous !== undefined && (
1639
+ current.brickCount > previous.brickCount
1640
+ || current.cellSizeMetres < previous.cellSizeMetres
1641
+ || current.maximumSurfaceErrorMetres < previous.maximumSurfaceErrorMetres
1642
+ || current.p99SurfaceErrorMetres < previous.p99SurfaceErrorMetres
1643
+ )) throw new Error("PvoxAssetManifestV1 LOD complexity and errors must be monotonic.");
1644
+ if (pages.slice(current.firstPageIndex, current.firstPageIndex + current.pageCount).some(({ pageKind, spatialKey }) => pageKind !== "render-field" || spatialKey?.lodLevel !== current.level)) {
1645
+ throw new Error("PvoxAssetManifestV1 LOD page ranges must reference render-field pages in the matching LOD scope.");
1646
+ }
1647
+ }
1648
+ const sourceContentHash = requireSha256(record.sourceContentHash, "PvoxAssetManifestV1.sourceContentHash");
1649
+ const canonicalDocumentHash = requireSha256(record.canonicalDocumentHash, "PvoxAssetManifestV1.canonicalDocumentHash");
1650
+ const compilationInputHash = requireSha256(record.compilationInputHash, "PvoxAssetManifestV1.compilationInputHash");
1651
+ const runtimeRequestProfileHash = requireSha256(record.runtimeRequestProfileHash, "PvoxAssetManifestV1.runtimeRequestProfileHash");
1652
+ if (new Set([sourceContentHash, canonicalDocumentHash, compilationInputHash, runtimeRequestProfileHash]).size !== 4) {
1653
+ throw new Error("PvoxAssetManifestV1 source, canonical document, compilation input, and runtime request profile hashes must be distinct attestations.");
1654
+ }
1655
+ if (record.headerByteLength !== PVOX_HEADER_BYTE_LENGTH
1656
+ || record.directoryEntryByteLength !== PVOX_DIRECTORY_ENTRY_BYTE_LENGTH
1657
+ || record.maximumRunsPerBrick !== PVOX_MAX_RUNS_PER_BRICK
1658
+ || record.maximumLocalSamplesPerBrick !== PVOX_MAX_LOCAL_SAMPLES_PER_BRICK
1659
+ || record.maximumEncodedBrickPayloadBytes !== PVOX_MAX_ENCODED_BRICK_PAYLOAD_BYTES) {
1660
+ throw new Error("PvoxAssetManifestV1 must declare the fixed PVOX 1.0 layout and brick codec limits.");
1661
+ }
1662
+ return deepFreeze({
1663
+ manifestVersion: PVOX_ASSET_MANIFEST_VERSION,
1664
+ representation: "pvox",
1665
+ formatVersion: PVOX_FORMAT_VERSION,
1666
+ contentType: PVOX_CONTENT_TYPE,
1667
+ fileExtension: PVOX_FILE_EXTENSION,
1668
+ magic: PVOX_MAGIC,
1669
+ brickEdgeVoxels: PVOX_BRICK_EDGE_VOXELS,
1670
+ pageSizeBytes: PVOX_PAGE_SIZE_BYTES,
1671
+ geometryMode: requireEnum(record.geometryMode, ["solid", "shell", "mixed"] as const, "PvoxAssetManifestV1.geometryMode"),
1672
+ sourceContentHash,
1673
+ canonicalDocumentHash,
1674
+ rootHash: requireSha256(record.rootHash, "PvoxAssetManifestV1.rootHash"),
1675
+ directoryHash: requireSha256(record.directoryHash, "PvoxAssetManifestV1.directoryHash"),
1676
+ pageSetHash: requireSha256(record.pageSetHash, "PvoxAssetManifestV1.pageSetHash"),
1677
+ binaryClosureHash: requireSha256(record.binaryClosureHash, "PvoxAssetManifestV1.binaryClosureHash"),
1678
+ validationEvidenceHash: requireSha256(record.validationEvidenceHash, "PvoxAssetManifestV1.validationEvidenceHash"),
1679
+ compilationInputHash,
1680
+ runtimeRequestProfileHash,
1681
+ headerByteLength: PVOX_HEADER_BYTE_LENGTH,
1682
+ directoryEntryByteLength: PVOX_DIRECTORY_ENTRY_BYTE_LENGTH,
1683
+ sectionCount: requireInteger(record.sectionCount, "PvoxAssetManifestV1.sectionCount", PVOX_MIN_STATIC_SECTION_COUNT, PVOX_MAX_SECTIONS),
1684
+ maximumRunsPerBrick: PVOX_MAX_RUNS_PER_BRICK,
1685
+ maximumLocalSamplesPerBrick: PVOX_MAX_LOCAL_SAMPLES_PER_BRICK,
1686
+ maximumEncodedBrickPayloadBytes: PVOX_MAX_ENCODED_BRICK_PAYLOAD_BYTES,
1687
+ coordinateSystem: CANONICAL_MODEL_COORDINATE_SYSTEM,
1688
+ boundsMetres,
1689
+ artifact,
1690
+ pages,
1691
+ lods,
1692
+ });
1693
+ }
1694
+
1695
+ /** Compatibility alias retaining the explicit v1 manifest implementation. */
1696
+ export function createPvoxAssetManifest(input: unknown): PvoxAssetManifestV1 {
1697
+ return createPvoxAssetManifestV1(input);
1698
+ }
1699
+
1700
+ /** Validate and deeply freeze a PVOX runtime technical profile. */
1701
+ export function createVoxelTechnicalProfile(input: unknown): VoxelTechnicalProfile {
1702
+ const record = asRecord(input, "VoxelTechnicalProfile");
1703
+ exactKeys(record, [
1704
+ "boundsMetres", "dimensionsMetres", "geometryMode", "artifactByteLength", "pageCount",
1705
+ "brickCount", "logicalVoxelCapacity", "encodedSurfaceSampleCount", "surfacePropertyCount", "sectionCount",
1706
+ "levelSpanRecordCount", "physicalPaletteRecordCount", "physicalEvidenceRecordCount", "materialRegionCount",
1707
+ "interiorLayerRecordCount", "massPropertyRecordCount", "bondRecordCount", "hierarchyDepth", "hierarchyNodeCount", "lodCount",
1708
+ "collisionHierarchyNodeCount", "collisionBrickCount", "collisionLogicalVoxelCapacity", "collisionEncodedSurfaceSampleCount",
1709
+ "cpuResidentByteLength", "gpuResidentByteLength", "hasCollision", "partitionCount",
1710
+ "maximumPartitionExtentMetres", "maximumPartitionDiagonalMetres",
1711
+ ], "VoxelTechnicalProfile");
1712
+ const boundsMetres = createBounds(record.boundsMetres, "VoxelTechnicalProfile.boundsMetres");
1713
+ assertFloorCentredBounds(boundsMetres, "VoxelTechnicalProfile.boundsMetres");
1714
+ const dimensionsMetres = createDimensions(record.dimensionsMetres, "VoxelTechnicalProfile.dimensionsMetres");
1715
+ const derived = boundsMetres.max.map((coordinate, index) => coordinate - boundsMetres.min[index]!) as [number, number, number];
1716
+ const supplied = [dimensionsMetres.width, dimensionsMetres.height, dimensionsMetres.depth];
1717
+ if (derived.some((coordinate, index) => Math.abs(coordinate - supplied[index]!) > Math.max(1e-6, Math.abs(coordinate) * 1e-6))) {
1718
+ throw new Error("VoxelTechnicalProfile dimensionsMetres must match boundsMetres.");
1719
+ }
1720
+ const artifactByteLength = requireInteger(record.artifactByteLength, "VoxelTechnicalProfile.artifactByteLength", 1, PVOX_DEFAULT_LIMITS.maximumArtifactBytes);
1721
+ const pageCount = requireInteger(record.pageCount, "VoxelTechnicalProfile.pageCount", 1, PVOX_DEFAULT_LIMITS.maximumPages);
1722
+ if (artifactByteLength !== pageCount * PVOX_PAGE_SIZE_BYTES) {
1723
+ throw new Error("VoxelTechnicalProfile artifactByteLength must equal pageCount multiplied by the fixed PVOX page size.");
1724
+ }
1725
+ const brickCount = requireInteger(record.brickCount, "VoxelTechnicalProfile.brickCount", 1, PVOX_DEFAULT_LIMITS.maximumBricks);
1726
+ const logicalVoxelCapacity = requireInteger(record.logicalVoxelCapacity, "VoxelTechnicalProfile.logicalVoxelCapacity", 1, PVOX_DEFAULT_LIMITS.maximumLogicalVoxels);
1727
+ if (logicalVoxelCapacity !== brickCount * PVOX_BRICK_EDGE_VOXELS ** 3) {
1728
+ throw new Error("VoxelTechnicalProfile logicalVoxelCapacity must equal brickCount multiplied by 512 voxels per brick.");
1729
+ }
1730
+ const encodedSurfaceSampleCount = requireInteger(record.encodedSurfaceSampleCount, "VoxelTechnicalProfile.encodedSurfaceSampleCount", 1, PVOX_DEFAULT_LIMITS.maximumEncodedSurfaceSamples);
1731
+ if (encodedSurfaceSampleCount > logicalVoxelCapacity) {
1732
+ throw new Error("VoxelTechnicalProfile encodedSurfaceSampleCount cannot exceed logicalVoxelCapacity.");
1733
+ }
1734
+ const surfacePropertyCount = requireInteger(record.surfacePropertyCount, "VoxelTechnicalProfile.surfacePropertyCount", 1, PVOX_DEFAULT_LIMITS.maximumSurfaceProperties);
1735
+ const sectionCount = requireInteger(record.sectionCount, "VoxelTechnicalProfile.sectionCount", PVOX_MIN_STATIC_SECTION_COUNT, PVOX_MAX_SECTIONS);
1736
+ const hierarchyDepth = requireInteger(record.hierarchyDepth, "VoxelTechnicalProfile.hierarchyDepth", 0, PVOX_DEFAULT_LIMITS.maximumHierarchyDepth);
1737
+ const hierarchyNodeCount = requireInteger(record.hierarchyNodeCount, "VoxelTechnicalProfile.hierarchyNodeCount", 1, PVOX_DEFAULT_LIMITS.maximumHierarchyNodes);
1738
+ const lodCount = requireInteger(record.lodCount, "VoxelTechnicalProfile.lodCount", 1, PVOX_DEFAULT_LIMITS.maximumLodCount);
1739
+ const partitionCount = requireInteger(record.partitionCount, "VoxelTechnicalProfile.partitionCount", 1, PVOX_DEFAULT_LIMITS.maximumPartitions);
1740
+ const levelSpanRecordCount = requireInteger(record.levelSpanRecordCount, "VoxelTechnicalProfile.levelSpanRecordCount", partitionCount * lodCount, partitionCount * lodCount * (hierarchyDepth + 1));
1741
+ const physicalPaletteRecordCount = requireInteger(record.physicalPaletteRecordCount, "VoxelTechnicalProfile.physicalPaletteRecordCount", 1, PVOX_DEFAULT_LIMITS.maximumPhysicalProperties);
1742
+ const physicalEvidenceRecordCount = requireInteger(record.physicalEvidenceRecordCount, "VoxelTechnicalProfile.physicalEvidenceRecordCount", 0, PVOX_DEFAULT_LIMITS.maximumPhysicalEvidenceEntries);
1743
+ const materialRegionCount = requireInteger(record.materialRegionCount, "VoxelTechnicalProfile.materialRegionCount", 1, PVOX_DEFAULT_LIMITS.maximumMaterialRegions);
1744
+ const interiorLayerRecordCount = requireInteger(record.interiorLayerRecordCount, "VoxelTechnicalProfile.interiorLayerRecordCount", 0, PVOX_DEFAULT_LIMITS.maximumInteriorLayers);
1745
+ const massPropertyRecordCount = requireInteger(record.massPropertyRecordCount, "VoxelTechnicalProfile.massPropertyRecordCount", 0, PVOX_DEFAULT_LIMITS.maximumMassPropertyRecords);
1746
+ const bondRecordCount = requireInteger(record.bondRecordCount, "VoxelTechnicalProfile.bondRecordCount", 0, PVOX_DEFAULT_LIMITS.maximumBondRecords);
1747
+ const hasCollision = requireBoolean(record.hasCollision, "VoxelTechnicalProfile.hasCollision");
1748
+ const collisionHierarchyNodeCount = requireInteger(record.collisionHierarchyNodeCount, "VoxelTechnicalProfile.collisionHierarchyNodeCount", hasCollision ? 1 : 0, hasCollision ? PVOX_DEFAULT_LIMITS.maximumHierarchyNodes : 0);
1749
+ const collisionBrickCount = requireInteger(record.collisionBrickCount, "VoxelTechnicalProfile.collisionBrickCount", hasCollision ? 1 : 0, hasCollision ? PVOX_DEFAULT_LIMITS.maximumBricks : 0);
1750
+ const collisionLogicalVoxelCapacity = requireInteger(record.collisionLogicalVoxelCapacity, "VoxelTechnicalProfile.collisionLogicalVoxelCapacity", hasCollision ? PVOX_BRICK_EDGE_VOXELS ** 3 : 0, hasCollision ? PVOX_DEFAULT_LIMITS.maximumLogicalVoxels : 0);
1751
+ if (collisionLogicalVoxelCapacity !== collisionBrickCount * PVOX_BRICK_EDGE_VOXELS ** 3) {
1752
+ throw new Error("VoxelTechnicalProfile collisionLogicalVoxelCapacity must equal collisionBrickCount multiplied by 512.");
1753
+ }
1754
+ const collisionEncodedSurfaceSampleCount = requireInteger(record.collisionEncodedSurfaceSampleCount, "VoxelTechnicalProfile.collisionEncodedSurfaceSampleCount", hasCollision ? 1 : 0, hasCollision ? PVOX_DEFAULT_LIMITS.maximumEncodedSurfaceSamples : 0);
1755
+ if (collisionEncodedSurfaceSampleCount > collisionLogicalVoxelCapacity) {
1756
+ throw new Error("VoxelTechnicalProfile collision encoded sample count cannot exceed its logical voxel capacity.");
1757
+ }
1758
+ const requiredSectionCount = PVOX_MIN_STATIC_SECTION_COUNT + (bondRecordCount > 0 ? 1 : 0) + (hasCollision ? PVOX_COLLISION_SECTION_COUNT : 0);
1759
+ if (sectionCount !== requiredSectionCount) {
1760
+ throw new Error("VoxelTechnicalProfile sectionCount must exactly represent the closed static, optional bond, and optional collision section registry.");
1761
+ }
1762
+ const maximumDimensionMetres = Math.max(...supplied);
1763
+ const fullDiagonalMetres = Math.hypot(...supplied);
1764
+ const maximumPartitionExtentMetres = requireNumber(record.maximumPartitionExtentMetres, "VoxelTechnicalProfile.maximumPartitionExtentMetres", Number.EPSILON, maximumDimensionMetres + Math.max(1e-9, maximumDimensionMetres * 1e-9));
1765
+ const maximumPartitionDiagonalMetres = requireNumber(record.maximumPartitionDiagonalMetres, "VoxelTechnicalProfile.maximumPartitionDiagonalMetres", maximumPartitionExtentMetres, fullDiagonalMetres + Math.max(1e-9, fullDiagonalMetres * 1e-9));
1766
+ if (partitionCount === 1 && (!approximatelyEqual(maximumPartitionExtentMetres, maximumDimensionMetres) || !approximatelyEqual(maximumPartitionDiagonalMetres, fullDiagonalMetres))) {
1767
+ throw new Error("VoxelTechnicalProfile single-partition extent and diagonal must cover the complete bounds.");
1768
+ }
1769
+ const profile: VoxelTechnicalProfile = {
1770
+ boundsMetres,
1771
+ dimensionsMetres,
1772
+ geometryMode: requireEnum(record.geometryMode, ["solid", "shell", "mixed"] as const, "VoxelTechnicalProfile.geometryMode"),
1773
+ artifactByteLength,
1774
+ pageCount,
1775
+ brickCount,
1776
+ logicalVoxelCapacity,
1777
+ encodedSurfaceSampleCount,
1778
+ surfacePropertyCount,
1779
+ sectionCount,
1780
+ levelSpanRecordCount,
1781
+ physicalPaletteRecordCount,
1782
+ physicalEvidenceRecordCount,
1783
+ materialRegionCount,
1784
+ interiorLayerRecordCount,
1785
+ massPropertyRecordCount,
1786
+ bondRecordCount,
1787
+ hierarchyDepth,
1788
+ hierarchyNodeCount,
1789
+ lodCount,
1790
+ collisionHierarchyNodeCount,
1791
+ collisionBrickCount,
1792
+ collisionLogicalVoxelCapacity,
1793
+ collisionEncodedSurfaceSampleCount,
1794
+ cpuResidentByteLength: requireInteger(record.cpuResidentByteLength, "VoxelTechnicalProfile.cpuResidentByteLength", 1, PVOX_DEFAULT_LIMITS.maximumCpuResidentBytes),
1795
+ gpuResidentByteLength: requireInteger(record.gpuResidentByteLength, "VoxelTechnicalProfile.gpuResidentByteLength", 1, PVOX_DEFAULT_LIMITS.maximumGpuResidentBytes),
1796
+ hasCollision,
1797
+ partitionCount,
1798
+ maximumPartitionExtentMetres,
1799
+ maximumPartitionDiagonalMetres,
1800
+ };
1801
+ const minimumArtifactByteLength = calculatePvoxMinimumArtifactByteLength(profile);
1802
+ if (minimumArtifactByteLength > artifactByteLength) {
1803
+ throw new Error(`VoxelTechnicalProfile fixed PVOX records require at least ${minimumArtifactByteLength} artifact bytes.`);
1804
+ }
1805
+ return deepFreeze(profile);
1806
+ }
1807
+
1808
+ /** Validate and deeply freeze one PVOX capability decision. */
1809
+ export function createVoxelCapabilityAssessment(input: unknown): VoxelCapabilityAssessment {
1810
+ const record = asRecord(input, "VoxelCapabilityAssessment");
1811
+ exactKeys(record, ["capability", "status", "evaluatorId", "evaluatorVersion", "subjectContentHash", "evidenceHash", "reasonCodes", "evaluatedAt"], "VoxelCapabilityAssessment");
1812
+ const status = requireEnum(record.status, ["supported", "unsupported", "not-applicable"] as const, "VoxelCapabilityAssessment.status");
1813
+ const reasonCodes = requireReasonCodes(record.reasonCodes, "VoxelCapabilityAssessment.reasonCodes");
1814
+ if (status === "supported" && reasonCodes.length !== 0) throw new Error("Supported capability evidence must not contain failure reason codes.");
1815
+ if (status !== "supported" && reasonCodes.length === 0) throw new Error("Unavailable capability evidence requires a reason code.");
1816
+ return deepFreeze({
1817
+ capability: requireEnum(record.capability, PVOX_CAPABILITIES, "VoxelCapabilityAssessment.capability"),
1818
+ status,
1819
+ evaluatorId: requireToken(record.evaluatorId, "VoxelCapabilityAssessment.evaluatorId"),
1820
+ evaluatorVersion: assertImmutableAssetVersion(record.evaluatorVersion),
1821
+ subjectContentHash: requireSha256(record.subjectContentHash, "VoxelCapabilityAssessment.subjectContentHash"),
1822
+ evidenceHash: requireSha256(record.evidenceHash, "VoxelCapabilityAssessment.evidenceHash"),
1823
+ reasonCodes,
1824
+ evaluatedAt: requireTimestamp(record.evaluatedAt, "VoxelCapabilityAssessment.evaluatedAt"),
1825
+ });
1826
+ }
1827
+
1828
+ export const PVOX_PHYSICAL_PROPERTY_POLICIES: Readonly<Record<PhysicalPropertyKind, Readonly<{ unit: string; minimum: number; maximum: number }>>> = deepFreeze({
1829
+ density: { unit: "kg/m3", minimum: Number.EPSILON, maximum: 50_000 },
1830
+ hardness: { unit: "Pa", minimum: Number.EPSILON, maximum: 1e12 },
1831
+ "tensile-strength": { unit: "Pa", minimum: Number.EPSILON, maximum: 1e12 },
1832
+ "compressive-strength": { unit: "Pa", minimum: Number.EPSILON, maximum: 1e12 },
1833
+ "shear-strength": { unit: "Pa", minimum: Number.EPSILON, maximum: 1e12 },
1834
+ "fracture-energy": { unit: "J/m2", minimum: Number.EPSILON, maximum: 1e9 },
1835
+ friction: { unit: "1", minimum: 0, maximum: 1 },
1836
+ restitution: { unit: "1", minimum: 0, maximum: 1 },
1837
+ "thermal-conductivity": { unit: "W/(m*K)", minimum: 0, maximum: 1e5 },
1838
+ "heat-capacity": { unit: "J/(kg*K)", minimum: 0, maximum: 1e7 },
1839
+ "thermal-expansion": { unit: "1/K", minimum: -1, maximum: 1 },
1840
+ "ignition-temperature": { unit: "K", minimum: Number.EPSILON, maximum: 100_000 },
1841
+ "melting-temperature": { unit: "K", minimum: Number.EPSILON, maximum: 100_000 },
1842
+ porosity: { unit: "1", minimum: 0, maximum: 1 },
1843
+ permeability: { unit: "m2", minimum: 0, maximum: 1 },
1844
+ "moisture-response": { unit: "1", minimum: 0, maximum: 1 },
1845
+ flammability: { unit: "1", minimum: 0, maximum: 1 },
1846
+ "corrosion-rate": { unit: "m/s", minimum: 0, maximum: 1 },
1847
+ "interior-thickness": { unit: "m", minimum: Number.EPSILON, maximum: 1_000_000 },
1848
+ });
1849
+
1850
+ /** Validate and deeply freeze one localized physical-property value. */
1851
+ export function createPhysicalPropertyEvidence(input: unknown): PhysicalPropertyEvidence {
1852
+ const record = asRecord(input, "PhysicalPropertyEvidence");
1853
+ exactKeys(record, ["property", "value", "unit", "provenance", "confidence", "subjectContentHash", "regionId", "materialId", "policyId", "policyVersion", "evidenceHash", "reviewedBy", "reviewedAt", "reviewToken"], "PhysicalPropertyEvidence");
1854
+ const property = requireEnum(record.property, PVOX_PHYSICAL_PROPERTIES, "PhysicalPropertyEvidence.property");
1855
+ const policy = PVOX_PHYSICAL_PROPERTY_POLICIES[property];
1856
+ if (record.policyId !== PVOX_PHYSICAL_PROPERTY_POLICY_ID || record.policyVersion !== PVOX_PHYSICAL_PROPERTY_POLICY_VERSION) {
1857
+ throw new Error("PhysicalPropertyEvidence must use the governed immutable physical-property policy.");
1858
+ }
1859
+ assertImmutableAssetVersion(record.policyVersion);
1860
+ if (record.unit !== policy.unit) throw new Error("PhysicalPropertyEvidence unit must match its governed property policy.");
1861
+ const provenance = requireEnum(record.provenance, ["source", "authored", "derived", "inferred", "default"] as const, "PhysicalPropertyEvidence.provenance");
1862
+ const reviewedBy = record.reviewedBy === undefined ? undefined : requireToken(record.reviewedBy, "PhysicalPropertyEvidence.reviewedBy");
1863
+ const reviewedAt = record.reviewedAt === undefined ? undefined : requireTimestamp(record.reviewedAt, "PhysicalPropertyEvidence.reviewedAt");
1864
+ const reviewToken = record.reviewToken === undefined ? undefined : requireAttestation(record.reviewToken, "PhysicalPropertyEvidence.reviewToken");
1865
+ if (new Set([reviewedBy, reviewedAt, reviewToken].map((entry) => entry === undefined)).size !== 1) throw new Error("PhysicalPropertyEvidence signed review identity, timestamp, and token must be supplied together.");
1866
+ if (provenance === "inferred" && reviewedBy === undefined) throw new Error("Inferred PhysicalPropertyEvidence requires human review.");
1867
+ return deepFreeze({
1868
+ property,
1869
+ value: requireNumber(record.value, "PhysicalPropertyEvidence.value", policy.minimum, policy.maximum),
1870
+ unit: policy.unit,
1871
+ provenance,
1872
+ confidence: requireNumber(record.confidence, "PhysicalPropertyEvidence.confidence", 0, 1),
1873
+ subjectContentHash: requireSha256(record.subjectContentHash, "PhysicalPropertyEvidence.subjectContentHash"),
1874
+ regionId: requireToken(record.regionId, "PhysicalPropertyEvidence.regionId"),
1875
+ materialId: requireToken(record.materialId, "PhysicalPropertyEvidence.materialId"),
1876
+ policyId: PVOX_PHYSICAL_PROPERTY_POLICY_ID,
1877
+ policyVersion: PVOX_PHYSICAL_PROPERTY_POLICY_VERSION,
1878
+ evidenceHash: requireSha256(record.evidenceHash, "PhysicalPropertyEvidence.evidenceHash"),
1879
+ ...(reviewedBy === undefined ? {} : { reviewedBy, reviewedAt, reviewToken }),
1880
+ });
1881
+ }
1882
+
1883
+ /** Validate the validator-attested inventory that makes omitted PVOX material regions detectable. */
1884
+ export function createPvoxPhysicalRegionInventoryV1(input: unknown): PvoxPhysicalRegionInventoryV1 {
1885
+ const fieldName = "PvoxPhysicalRegionInventoryV1";
1886
+ const record = asRecord(input, fieldName);
1887
+ exactKeys(record, ["inventoryVersion", "subjectContentHash", "inventoryHash", "inventoryHashAttestation", "validationEvidenceHash", "entries"], fieldName);
1888
+ if (record.inventoryVersion !== PVOX_PHYSICAL_REGION_INVENTORY_VERSION) throw new Error(`${fieldName}.inventoryVersion is unsupported.`);
1889
+ const entriesInput = asDenseArray(record.entries, `${fieldName}.entries`, 1, PVOX_DEFAULT_LIMITS.maximumMaterialRegions);
1890
+ const entries = entriesInput.map((entry, index): PvoxPhysicalRegionInventoryEntry => {
1891
+ const entryName = `${fieldName}.entries[${index}]`;
1892
+ const item = asRecord(entry, entryName);
1893
+ exactKeys(item, ["regionIndex", "physicalPaletteIndex", "regionId", "materialId"], entryName);
1894
+ return {
1895
+ regionIndex: requireInteger(item.regionIndex, `${entryName}.regionIndex`, 0, PVOX_DEFAULT_LIMITS.maximumMaterialRegions - 1),
1896
+ physicalPaletteIndex: requireInteger(item.physicalPaletteIndex, `${entryName}.physicalPaletteIndex`, 0, PVOX_DEFAULT_LIMITS.maximumPhysicalProperties - 1),
1897
+ regionId: requireToken(item.regionId, `${entryName}.regionId`),
1898
+ materialId: requireToken(item.materialId, `${entryName}.materialId`),
1899
+ };
1900
+ });
1901
+ if (entries.some((entry, index) => entry.regionIndex !== index)) throw new Error(`${fieldName} regionIndex values must be contiguous and canonically ordered.`);
1902
+ const pairs = entries.map(({ regionId, materialId }) => `${regionId}\u0000${materialId}`);
1903
+ if (new Set(pairs).size !== pairs.length) throw new Error(`${fieldName} region/material pairs must be unique.`);
1904
+ const inventoryHash = requireSha256(record.inventoryHash, `${fieldName}.inventoryHash`);
1905
+ return deepFreeze({
1906
+ inventoryVersion: PVOX_PHYSICAL_REGION_INVENTORY_VERSION,
1907
+ subjectContentHash: requireSha256(record.subjectContentHash, `${fieldName}.subjectContentHash`),
1908
+ inventoryHash,
1909
+ inventoryHashAttestation: createHashAttestation(record.inventoryHashAttestation, PVOX_PHYSICAL_INVENTORY_HASH_DOMAIN, inventoryHash, `${fieldName}.inventoryHashAttestation`),
1910
+ validationEvidenceHash: requireSha256(record.validationEvidenceHash, `${fieldName}.validationEvidenceHash`),
1911
+ entries,
1912
+ });
1913
+ }
1914
+
1915
+ function createReviewedStructuralEvidence(
1916
+ input: unknown,
1917
+ expectedKind: PvoxReviewedStructuralEvidenceKind,
1918
+ expectedSubjectHash: string,
1919
+ expectedInventoryHash: string,
1920
+ expectedRecordCount: number,
1921
+ fieldName: string,
1922
+ ): PvoxReviewedStructuralEvidence {
1923
+ const record = asRecord(input, fieldName);
1924
+ exactKeys(record, ["kind", "subjectContentHash", "physicalInventoryHash", "recordCount", "evidenceHash", "reviewerId", "reviewedAt", "reviewToken"], fieldName);
1925
+ if (record.kind !== expectedKind) throw new Error(`${fieldName}.kind must be ${expectedKind}.`);
1926
+ const subjectContentHash = requireSha256(record.subjectContentHash, `${fieldName}.subjectContentHash`);
1927
+ const physicalInventoryHash = requireSha256(record.physicalInventoryHash, `${fieldName}.physicalInventoryHash`);
1928
+ const recordCount = requireInteger(record.recordCount, `${fieldName}.recordCount`, 0, PVOX_DEFAULT_LIMITS.maximumBondRecords);
1929
+ if (subjectContentHash !== expectedSubjectHash || physicalInventoryHash !== expectedInventoryHash || recordCount !== expectedRecordCount) {
1930
+ throw new Error(`${fieldName} must bind the exact PVOX closure, physical inventory, and binary record count.`);
1931
+ }
1932
+ return deepFreeze({
1933
+ kind: expectedKind,
1934
+ subjectContentHash,
1935
+ physicalInventoryHash,
1936
+ recordCount,
1937
+ evidenceHash: requireSha256(record.evidenceHash, `${fieldName}.evidenceHash`),
1938
+ reviewerId: requireToken(record.reviewerId, `${fieldName}.reviewerId`),
1939
+ reviewedAt: requireTimestamp(record.reviewedAt, `${fieldName}.reviewedAt`),
1940
+ reviewToken: requireAttestation(record.reviewToken, `${fieldName}.reviewToken`),
1941
+ });
1942
+ }
1943
+
1944
+ /** Validate a measured fidelity decision and derive its non-overridable outcome. */
1945
+ export function createPvoxFidelityEvidence(input: unknown): PvoxFidelityEvidence {
1946
+ const record = asRecord(input, "PvoxFidelityEvidence");
1947
+ exactKeys(record, [
1948
+ "profileId", "surfaceProfileId", "profileVersion", "outcome", "canonicalDocumentHash",
1949
+ "pvoxBinaryClosureHash", "evaluatedDiagonalMetres", "lod0CellSizeMetres", "maximumAllowedSurfaceErrorMetres",
1950
+ "maximumSurfaceErrorMetres", "p99AllowedSurfaceErrorMetres", "p99SurfaceErrorMetres",
1951
+ "minimumSilhouetteIou", "silhouetteIou", "maximumAllowedContourDisplacementPx",
1952
+ "maximumContourDisplacementPx", "minimumRenderedSsim", "renderedSsim",
1953
+ "maximumP95DeltaE2000", "p95DeltaE2000", "maximumNormalizedMaterialError",
1954
+ "normalizedMaterialError", "evidenceHash", "evaluatedAt", "decisionToken",
1955
+ ], "PvoxFidelityEvidence");
1956
+ const profileId = requireEnum(record.profileId, PVOX_FIDELITY_PROFILE_IDS, "PvoxFidelityEvidence.profileId");
1957
+ const surfaceProfileId = requireEnum(record.surfaceProfileId, [
1958
+ "precision-hero-v1",
1959
+ "props-furniture-v1",
1960
+ "rocks-organic-shells-v1",
1961
+ "buildings-v1",
1962
+ ] as const, "PvoxFidelityEvidence.surfaceProfileId");
1963
+ if (profileId !== "deformable-v1" && profileId !== surfaceProfileId) {
1964
+ throw new Error("PvoxFidelityEvidence surfaceProfileId must match its governed profileId.");
1965
+ }
1966
+ if (record.profileVersion !== PVOX_FIDELITY_PROFILE_VERSION) {
1967
+ throw new Error("PvoxFidelityEvidence.profileVersion must use the governed profile version.");
1968
+ }
1969
+ assertImmutableAssetVersion(record.profileVersion);
1970
+ const evaluatedDiagonalMetres = requireNumber(record.evaluatedDiagonalMetres, "PvoxFidelityEvidence.evaluatedDiagonalMetres", Number.EPSILON, 1_000_000_000);
1971
+ const lod0CellSizeMetres = requireNumber(record.lod0CellSizeMetres, "PvoxFidelityEvidence.lod0CellSizeMetres", Number.EPSILON, 1_000_000);
1972
+ const profileFormula: Readonly<Record<typeof surfaceProfileId, readonly [number, number]>> = {
1973
+ "precision-hero-v1": [0.00025, 0.0001],
1974
+ "props-furniture-v1": [0.0005, 0.00025],
1975
+ "rocks-organic-shells-v1": [0.001, 0.0005],
1976
+ "buildings-v1": [0.002, 0.0001],
1977
+ };
1978
+ const [absoluteFloor, diagonalFactor] = profileFormula[surfaceProfileId];
1979
+ const governedP99Ceiling = Math.max(absoluteFloor, evaluatedDiagonalMetres * diagonalFactor);
1980
+ const governedMaximumCeiling = Math.min(3 * governedP99Ceiling, 1.5 * lod0CellSizeMetres);
1981
+ const suppliedP99Ceiling = requireNumber(record.p99AllowedSurfaceErrorMetres, "PvoxFidelityEvidence.p99AllowedSurfaceErrorMetres", 0, 1_000_000);
1982
+ const suppliedMaximumCeiling = requireNumber(record.maximumAllowedSurfaceErrorMetres, "PvoxFidelityEvidence.maximumAllowedSurfaceErrorMetres", 0, 1_000_000);
1983
+ if (!approximatelyEqual(suppliedP99Ceiling, governedP99Ceiling) || !approximatelyEqual(suppliedMaximumCeiling, governedMaximumCeiling)) {
1984
+ throw new Error("PvoxFidelityEvidence surface ceilings must match the governed profile formula and LOD0 cell limit.");
1985
+ }
1986
+ const minimumSilhouetteIou = requireNumber(record.minimumSilhouetteIou, "PvoxFidelityEvidence.minimumSilhouetteIou", 0, 1);
1987
+ const maximumAllowedContourDisplacementPx = requireNumber(record.maximumAllowedContourDisplacementPx, "PvoxFidelityEvidence.maximumAllowedContourDisplacementPx", 0, 1_000_000);
1988
+ const minimumRenderedSsim = requireNumber(record.minimumRenderedSsim, "PvoxFidelityEvidence.minimumRenderedSsim", 0, 1);
1989
+ const maximumP95DeltaE2000 = requireNumber(record.maximumP95DeltaE2000, "PvoxFidelityEvidence.maximumP95DeltaE2000", 0, 100);
1990
+ const maximumNormalizedMaterialError = requireNumber(record.maximumNormalizedMaterialError, "PvoxFidelityEvidence.maximumNormalizedMaterialError", 0, 1);
1991
+ if (!approximatelyEqual(minimumSilhouetteIou, 0.995)
1992
+ || !approximatelyEqual(maximumAllowedContourDisplacementPx, 0.5)
1993
+ || !approximatelyEqual(minimumRenderedSsim, 0.98)
1994
+ || !approximatelyEqual(maximumP95DeltaE2000, 3)
1995
+ || !approximatelyEqual(maximumNormalizedMaterialError, 1 / 255)) {
1996
+ throw new Error("PvoxFidelityEvidence must use the governed silhouette, contour, render, colour, and material thresholds.");
1997
+ }
1998
+ const output = {
1999
+ profileId,
2000
+ surfaceProfileId,
2001
+ profileVersion: PVOX_FIDELITY_PROFILE_VERSION,
2002
+ canonicalDocumentHash: requireSha256(record.canonicalDocumentHash, "PvoxFidelityEvidence.canonicalDocumentHash"),
2003
+ pvoxBinaryClosureHash: requireSha256(record.pvoxBinaryClosureHash, "PvoxFidelityEvidence.pvoxBinaryClosureHash"),
2004
+ evaluatedDiagonalMetres,
2005
+ lod0CellSizeMetres,
2006
+ maximumAllowedSurfaceErrorMetres: governedMaximumCeiling,
2007
+ maximumSurfaceErrorMetres: requireNumber(record.maximumSurfaceErrorMetres, "PvoxFidelityEvidence.maximumSurfaceErrorMetres", 0, 1_000_000),
2008
+ p99AllowedSurfaceErrorMetres: governedP99Ceiling,
2009
+ p99SurfaceErrorMetres: requireNumber(record.p99SurfaceErrorMetres, "PvoxFidelityEvidence.p99SurfaceErrorMetres", 0, 1_000_000),
2010
+ minimumSilhouetteIou,
2011
+ silhouetteIou: requireNumber(record.silhouetteIou, "PvoxFidelityEvidence.silhouetteIou", 0, 1),
2012
+ maximumAllowedContourDisplacementPx,
2013
+ maximumContourDisplacementPx: requireNumber(record.maximumContourDisplacementPx, "PvoxFidelityEvidence.maximumContourDisplacementPx", 0, 1_000_000),
2014
+ minimumRenderedSsim,
2015
+ renderedSsim: requireNumber(record.renderedSsim, "PvoxFidelityEvidence.renderedSsim", 0, 1),
2016
+ maximumP95DeltaE2000,
2017
+ p95DeltaE2000: requireNumber(record.p95DeltaE2000, "PvoxFidelityEvidence.p95DeltaE2000", 0, 100),
2018
+ maximumNormalizedMaterialError,
2019
+ normalizedMaterialError: requireNumber(record.normalizedMaterialError, "PvoxFidelityEvidence.normalizedMaterialError", 0, 1),
2020
+ evidenceHash: requireSha256(record.evidenceHash, "PvoxFidelityEvidence.evidenceHash"),
2021
+ evaluatedAt: requireTimestamp(record.evaluatedAt, "PvoxFidelityEvidence.evaluatedAt"),
2022
+ decisionToken: requireAttestation(record.decisionToken, "PvoxFidelityEvidence.decisionToken"),
2023
+ };
2024
+ const passed = output.maximumSurfaceErrorMetres <= output.maximumAllowedSurfaceErrorMetres
2025
+ && output.p99SurfaceErrorMetres <= output.p99AllowedSurfaceErrorMetres
2026
+ && output.silhouetteIou >= output.minimumSilhouetteIou
2027
+ && output.maximumContourDisplacementPx <= output.maximumAllowedContourDisplacementPx
2028
+ && output.renderedSsim >= output.minimumRenderedSsim
2029
+ && output.p95DeltaE2000 <= output.maximumP95DeltaE2000
2030
+ && output.normalizedMaterialError <= output.maximumNormalizedMaterialError;
2031
+ const outcome = requireEnum(record.outcome, ["passed", "blocked"] as const, "PvoxFidelityEvidence.outcome");
2032
+ if (outcome !== (passed ? "passed" : "blocked")) throw new Error("PvoxFidelityEvidence outcome must match its measured fidelity thresholds.");
2033
+ return deepFreeze({ ...output, outcome });
2034
+ }
2035
+
2036
+ function createPvoxEditJournalCurrentState(
2037
+ input: unknown,
2038
+ baseManifest: PvoxAssetManifestV1,
2039
+ ): PvoxEditJournalCurrentState {
2040
+ const fieldName = "PvoxEditJournalCurrentState";
2041
+ const record = asRecord(input, fieldName);
2042
+ exactKeys(record, ["baseContentHash", "basePageSetHash", "placementId", "gridVersion", "revision", "rootHash", "latestJournalHash", "pageHashes"], fieldName);
2043
+ const pageEntries = asDenseArray(record.pageHashes, `${fieldName}.pageHashes`, baseManifest.pages.length, baseManifest.pages.length);
2044
+ const pageHashes = pageEntries.map((entry, index) => {
2045
+ const entryName = `${fieldName}.pageHashes[${index}]`;
2046
+ const page = asRecord(entry, entryName);
2047
+ exactKeys(page, ["pageIndex", "sha256"], entryName);
2048
+ const pageIndex = requireInteger(page.pageIndex, `${entryName}.pageIndex`, 0, baseManifest.pages.length - 1);
2049
+ if (pageIndex !== index) throw new Error(`${fieldName}.pageHashes must be complete and ordered by pageIndex.`);
2050
+ return { pageIndex, sha256: requireSha256(page.sha256, `${entryName}.sha256`) };
2051
+ });
2052
+ return deepFreeze({
2053
+ baseContentHash: requireSha256(record.baseContentHash, `${fieldName}.baseContentHash`),
2054
+ basePageSetHash: requireSha256(record.basePageSetHash, `${fieldName}.basePageSetHash`),
2055
+ placementId: requireToken(record.placementId, `${fieldName}.placementId`),
2056
+ gridVersion: requireToken(record.gridVersion, `${fieldName}.gridVersion`),
2057
+ revision: requireInteger(record.revision, `${fieldName}.revision`, 1, Number.MAX_SAFE_INTEGER),
2058
+ rootHash: requireSha256(record.rootHash, `${fieldName}.rootHash`),
2059
+ latestJournalHash: requireSha256(record.latestJournalHash, `${fieldName}.latestJournalHash`),
2060
+ pageHashes,
2061
+ });
2062
+ }
2063
+
2064
+ /** Validate and deeply freeze one placement-specific edit journal. */
2065
+ export function createPvoxEditJournal(
2066
+ input: unknown,
2067
+ baseManifestInput: unknown,
2068
+ currentStateInput?: unknown,
2069
+ ): PvoxEditJournal {
2070
+ const record = asRecord(input, "PvoxEditJournal");
2071
+ exactKeys(record, ["journalVersion", "baseContentHash", "basePageSetHash", "placementId", "gridVersion", "operationId", "expectedRevision", "resultingRevision", "previousJournalHash", "expectedRootHash", "resultingRootHash", "resultingRootHashAttestation", "patches", "dirtyBoundsMetres", "massDeltaKg", "journalHash", "journalHashAttestation", "recordedAt"], "PvoxEditJournal");
2072
+ if (record.journalVersion !== PVOX_EDIT_JOURNAL_VERSION) throw new Error("PvoxEditJournal.journalVersion is unsupported.");
2073
+ const baseManifest = createPvoxAssetManifestV1(baseManifestInput);
2074
+ const baseContentHash = requireSha256(record.baseContentHash, "PvoxEditJournal.baseContentHash");
2075
+ const basePageSetHash = requireSha256(record.basePageSetHash, "PvoxEditJournal.basePageSetHash");
2076
+ if (baseContentHash !== baseManifest.artifact.sha256 || basePageSetHash !== baseManifest.pageSetHash) {
2077
+ throw new Error("PvoxEditJournal must bind the exact base PVOX artifact and page inventory.");
2078
+ }
2079
+ const expectedRevision = requireInteger(record.expectedRevision, "PvoxEditJournal.expectedRevision", 0, Number.MAX_SAFE_INTEGER - 1);
2080
+ const resultingRevision = requireInteger(record.resultingRevision, "PvoxEditJournal.resultingRevision", 1, Number.MAX_SAFE_INTEGER);
2081
+ if (resultingRevision !== expectedRevision + 1) throw new Error("PvoxEditJournal revisions must be sequential.");
2082
+ const previousJournalHash = requireSha256(record.previousJournalHash, "PvoxEditJournal.previousJournalHash");
2083
+ if ((expectedRevision === 0) !== (previousJournalHash === PVOX_EDIT_JOURNAL_GENESIS_HASH)) {
2084
+ throw new Error("PvoxEditJournal genesis revision must use only the canonical genesis previousJournalHash.");
2085
+ }
2086
+ const expectedRootHash = requireSha256(record.expectedRootHash, "PvoxEditJournal.expectedRootHash");
2087
+ const resultingRootHash = requireSha256(record.resultingRootHash, "PvoxEditJournal.resultingRootHash");
2088
+ if (expectedRootHash === resultingRootHash) throw new Error("PvoxEditJournal copy-on-write operation must change the overlay root hash.");
2089
+ const placementId = requireToken(record.placementId, "PvoxEditJournal.placementId");
2090
+ const gridVersion = requireToken(record.gridVersion, "PvoxEditJournal.gridVersion");
2091
+ let currentState: PvoxEditJournalCurrentState | undefined;
2092
+ if (expectedRevision === 0) {
2093
+ if (currentStateInput !== undefined) throw new Error("PvoxEditJournal genesis must not supply a non-genesis current state.");
2094
+ if (expectedRootHash !== baseManifest.rootHash) throw new Error("PvoxEditJournal genesis expectedRootHash must equal the authenticated base PVOX rootHash.");
2095
+ } else {
2096
+ if (currentStateInput === undefined) throw new Error("PvoxEditJournal non-genesis validation requires the authenticated current placement state.");
2097
+ currentState = createPvoxEditJournalCurrentState(currentStateInput, baseManifest);
2098
+ if (currentState.baseContentHash !== baseContentHash
2099
+ || currentState.basePageSetHash !== basePageSetHash
2100
+ || currentState.placementId !== placementId
2101
+ || currentState.gridVersion !== gridVersion
2102
+ || currentState.revision !== expectedRevision
2103
+ || currentState.rootHash !== expectedRootHash
2104
+ || currentState.latestJournalHash !== previousJournalHash) {
2105
+ throw new Error("PvoxEditJournal non-genesis CAS subject must match the authenticated current placement state.");
2106
+ }
2107
+ }
2108
+ const patchEntries = asDenseArray(record.patches, "PvoxEditJournal.patches", 1, PVOX_MAX_EDIT_PATCHES);
2109
+ const patches = patchEntries.map((entry, index): PvoxBrickPatchDescriptor => {
2110
+ const fieldName = `PvoxEditJournal.patches[${index}]`;
2111
+ const patch = asRecord(entry, fieldName);
2112
+ exactKeys(patch, ["fieldKind", "lodLevel", "partitionIndex", "hierarchyDepth", "mortonCode", "expectedPageIndex", "expectedPageHash", "operation", "expectedBrickHash", "resultingBrickHash", "resultingPageHash"], fieldName);
2113
+ const mortonCode = requireString(patch.mortonCode, `${fieldName}.mortonCode`, 16);
2114
+ if (!MORTON_CODE.test(mortonCode)) throw new Error(`${fieldName}.mortonCode must be a canonical Morton code.`);
2115
+ const operation = requireEnum(patch.operation, ["insert", "replace", "remove"] as const, `${fieldName}.operation`);
2116
+ const expectedBrickHash = requireSha256(patch.expectedBrickHash, `${fieldName}.expectedBrickHash`);
2117
+ const resultingBrickHash = requireSha256(patch.resultingBrickHash, `${fieldName}.resultingBrickHash`);
2118
+ const expectedPageHash = requireSha256(patch.expectedPageHash, `${fieldName}.expectedPageHash`);
2119
+ const resultingPageHash = requireSha256(patch.resultingPageHash, `${fieldName}.resultingPageHash`);
2120
+ if (operation === "replace" && (expectedBrickHash === PVOX_EDIT_JOURNAL_GENESIS_HASH || resultingBrickHash === PVOX_EDIT_JOURNAL_GENESIS_HASH || expectedBrickHash === resultingBrickHash)) throw new Error(`${fieldName} replacement must change one non-empty brick hash to another.`);
2121
+ if (operation === "insert" && (expectedBrickHash !== PVOX_EDIT_JOURNAL_GENESIS_HASH || resultingBrickHash === PVOX_EDIT_JOURNAL_GENESIS_HASH)) throw new Error(`${fieldName} insertion must change an empty brick to a non-empty result.`);
2122
+ if (operation === "remove" && (expectedBrickHash === PVOX_EDIT_JOURNAL_GENESIS_HASH || resultingBrickHash !== PVOX_EDIT_JOURNAL_GENESIS_HASH)) throw new Error(`${fieldName} removal must change a non-empty expected brick to the empty result.`);
2123
+ if (expectedPageHash === resultingPageHash) throw new Error(`${fieldName} copy-on-write patch must change the page hash.`);
2124
+ const fieldKind = requireEnum(patch.fieldKind, ["render-field", "collision-field"] as const, `${fieldName}.fieldKind`);
2125
+ const lodLevel = requireInteger(patch.lodLevel, `${fieldName}.lodLevel`, 0, PVOX_DEFAULT_LIMITS.maximumLodCount - 1) as PvoxLodLevel;
2126
+ const partitionIndex = requireInteger(patch.partitionIndex, `${fieldName}.partitionIndex`, 0, PVOX_DEFAULT_LIMITS.maximumPartitions - 1);
2127
+ const hierarchyDepth = requireInteger(patch.hierarchyDepth, `${fieldName}.hierarchyDepth`, 0, PVOX_DEFAULT_LIMITS.maximumHierarchyDepth);
2128
+ const expectedPageIndex = requireInteger(patch.expectedPageIndex, `${fieldName}.expectedPageIndex`, 0, baseManifest.pages.length - 1);
2129
+ const basePage = baseManifest.pages[expectedPageIndex]!;
2130
+ if (basePage.pageKind !== fieldKind
2131
+ || basePage.spatialKey?.lodLevel !== lodLevel
2132
+ || basePage.spatialKey.partitionIndex !== partitionIndex
2133
+ || basePage.spatialKey.hierarchyDepth !== hierarchyDepth
2134
+ || mortonCode < basePage.spatialKey.minimumMortonCode
2135
+ || mortonCode > basePage.spatialKey.maximumMortonCode
2136
+ || (expectedRevision === 0 && expectedPageHash !== basePage.sha256)
2137
+ || (currentState !== undefined && currentState.pageHashes[expectedPageIndex]!.sha256 !== expectedPageHash)) {
2138
+ throw new Error(`${fieldName} must address and CAS the exact base PVOX page inventory.`);
2139
+ }
2140
+ return {
2141
+ fieldKind,
2142
+ lodLevel,
2143
+ partitionIndex,
2144
+ hierarchyDepth,
2145
+ mortonCode,
2146
+ expectedPageIndex,
2147
+ expectedPageHash,
2148
+ operation,
2149
+ expectedBrickHash,
2150
+ resultingBrickHash,
2151
+ resultingPageHash,
2152
+ };
2153
+ });
2154
+ const patchAddress = (patch: PvoxBrickPatchDescriptor): string => [
2155
+ patch.fieldKind,
2156
+ String(patch.lodLevel).padStart(2, "0"),
2157
+ String(patch.partitionIndex).padStart(6, "0"),
2158
+ String(patch.hierarchyDepth).padStart(2, "0"),
2159
+ patch.mortonCode,
2160
+ ].join("\u0000");
2161
+ for (let index = 1; index < patches.length; index += 1) {
2162
+ if (patchAddress(patches[index]!) <= patchAddress(patches[index - 1]!)) throw new Error("PvoxEditJournal patches must be unique and ordered by field, LOD, partition, depth, and Morton code.");
2163
+ }
2164
+ if (new Set(patches.map(({ expectedPageIndex }) => expectedPageIndex)).size !== patches.length) {
2165
+ throw new Error("PvoxEditJournal permits only one coherent patch transition per page.");
2166
+ }
2167
+ const journalHash = requireSha256(record.journalHash, "PvoxEditJournal.journalHash");
2168
+ return deepFreeze({
2169
+ journalVersion: PVOX_EDIT_JOURNAL_VERSION,
2170
+ baseContentHash,
2171
+ basePageSetHash,
2172
+ placementId,
2173
+ gridVersion,
2174
+ operationId: requireToken(record.operationId, "PvoxEditJournal.operationId"),
2175
+ expectedRevision,
2176
+ resultingRevision,
2177
+ previousJournalHash,
2178
+ expectedRootHash,
2179
+ resultingRootHash,
2180
+ resultingRootHashAttestation: createHashAttestation(record.resultingRootHashAttestation, PVOX_EDIT_OVERLAY_ROOT_HASH_DOMAIN, resultingRootHash, "PvoxEditJournal.resultingRootHashAttestation"),
2181
+ patches,
2182
+ dirtyBoundsMetres: createBounds(record.dirtyBoundsMetres, "PvoxEditJournal.dirtyBoundsMetres"),
2183
+ massDeltaKg: requireNumber(record.massDeltaKg, "PvoxEditJournal.massDeltaKg", -Number.MAX_VALUE, Number.MAX_VALUE),
2184
+ journalHash,
2185
+ journalHashAttestation: createHashAttestation(record.journalHashAttestation, PVOX_EDIT_JOURNAL_HASH_DOMAIN, journalHash, "PvoxEditJournal.journalHashAttestation"),
2186
+ recordedAt: requireTimestamp(record.recordedAt, "PvoxEditJournal.recordedAt"),
2187
+ });
2188
+ }
2189
+
2190
+ /** Validate an ordered, gap-free CAS journal chain for one placement overlay. */
2191
+ export function createPvoxEditJournalChain(input: unknown, baseManifestInput: unknown): readonly PvoxEditJournal[] {
2192
+ const entries = asDenseArray(input, "PvoxEditJournalChain", 1, 10_000);
2193
+ const baseManifest = createPvoxAssetManifestV1(baseManifestInput);
2194
+ const journals: PvoxEditJournal[] = [];
2195
+ const operationIds = new Set<string>();
2196
+ const currentPageHashes = new Map(baseManifest.pages.map(({ pageIndex, sha256 }) => [pageIndex, sha256]));
2197
+ for (const entry of entries) {
2198
+ const previous = journals.at(-1);
2199
+ const currentState: PvoxEditJournalCurrentState | undefined = previous === undefined ? undefined : {
2200
+ baseContentHash: previous.baseContentHash,
2201
+ basePageSetHash: previous.basePageSetHash,
2202
+ placementId: previous.placementId,
2203
+ gridVersion: previous.gridVersion,
2204
+ revision: previous.resultingRevision,
2205
+ rootHash: previous.resultingRootHash,
2206
+ latestJournalHash: previous.journalHash,
2207
+ pageHashes: baseManifest.pages.map(({ pageIndex }) => ({ pageIndex, sha256: currentPageHashes.get(pageIndex)! })),
2208
+ };
2209
+ const journal = createPvoxEditJournal(entry, baseManifest, currentState);
2210
+ if (previous === undefined && (journal.expectedRevision !== 0 || journal.previousJournalHash !== PVOX_EDIT_JOURNAL_GENESIS_HASH)) {
2211
+ throw new Error("PvoxEditJournalChain must begin at the canonical genesis revision.");
2212
+ }
2213
+ if (previous !== undefined && Date.parse(journal.recordedAt) < Date.parse(previous.recordedAt)) {
2214
+ throw new Error("PvoxEditJournalChain entries must form one chronological revision/root/hash chain.");
2215
+ }
2216
+ if (operationIds.has(journal.operationId)) throw new Error("PvoxEditJournalChain operation IDs must be unique to prevent replay.");
2217
+ operationIds.add(journal.operationId);
2218
+ for (const patch of journal.patches) {
2219
+ if (currentPageHashes.get(patch.expectedPageIndex) !== patch.expectedPageHash) {
2220
+ throw new Error("PvoxEditJournalChain patch expectedPageHash must match the current page CAS state.");
2221
+ }
2222
+ currentPageHashes.set(patch.expectedPageIndex, patch.resultingPageHash);
2223
+ }
2224
+ journals.push(journal);
2225
+ }
2226
+ return deepFreeze(journals);
2227
+ }
2228
+
2229
+ function createCompilerEvidence(value: unknown): PvoxCompilerEvidence {
2230
+ const record = asRecord(value, "ModelProcessingManifestV2.converter");
2231
+ exactKeys(record, ["compilerId", "compilerVersion", "sourceFormat", "sourceContentHash", "canonicalDocumentHash", "compilationInputHash", "runtimeRequestProfileHash", "outputContentHash", "evidenceHash", "compiledAt"], "ModelProcessingManifestV2.converter");
2232
+ return {
2233
+ compilerId: requireToken(record.compilerId, "ModelProcessingManifestV2.converter.compilerId"),
2234
+ compilerVersion: assertImmutableAssetVersion(record.compilerVersion),
2235
+ sourceFormat: requireToken(record.sourceFormat, "ModelProcessingManifestV2.converter.sourceFormat"),
2236
+ sourceContentHash: requireSha256(record.sourceContentHash, "ModelProcessingManifestV2.converter.sourceContentHash"),
2237
+ canonicalDocumentHash: requireSha256(record.canonicalDocumentHash, "ModelProcessingManifestV2.converter.canonicalDocumentHash"),
2238
+ compilationInputHash: requireSha256(record.compilationInputHash, "ModelProcessingManifestV2.converter.compilationInputHash"),
2239
+ runtimeRequestProfileHash: requireSha256(record.runtimeRequestProfileHash, "ModelProcessingManifestV2.converter.runtimeRequestProfileHash"),
2240
+ outputContentHash: requireSha256(record.outputContentHash, "ModelProcessingManifestV2.converter.outputContentHash"),
2241
+ evidenceHash: requireSha256(record.evidenceHash, "ModelProcessingManifestV2.converter.evidenceHash"),
2242
+ compiledAt: requireTimestamp(record.compiledAt, "ModelProcessingManifestV2.converter.compiledAt"),
2243
+ };
2244
+ }
2245
+
2246
+ /** Validate and deeply freeze one non-overridable PVOX gate. */
2247
+ export function createPvoxGateEvidence(input: unknown): PvoxGateEvidence {
2248
+ const record = asRecord(input, "PvoxGateEvidence");
2249
+ exactKeys(record, ["kind", "outcome", "validatorId", "validatorVersion", "subjectContentHash", "evidenceHash", "reasonCodes", "evaluatedAt", "attestationToken"], "PvoxGateEvidence");
2250
+ const outcome = requireEnum(record.outcome, ["passed", "blocked"] as const, "PvoxGateEvidence.outcome");
2251
+ const reasonCodes = requireReasonCodes(record.reasonCodes, "PvoxGateEvidence.reasonCodes");
2252
+ if (outcome === "blocked" && reasonCodes.length === 0) throw new Error("Blocked PvoxGateEvidence requires a reason code.");
2253
+ return deepFreeze({
2254
+ kind: requireEnum(record.kind, PVOX_NON_OVERRIDABLE_GATE_KINDS, "PvoxGateEvidence.kind"),
2255
+ outcome,
2256
+ validatorId: requireToken(record.validatorId, "PvoxGateEvidence.validatorId"),
2257
+ validatorVersion: assertImmutableAssetVersion(record.validatorVersion),
2258
+ subjectContentHash: requireSha256(record.subjectContentHash, "PvoxGateEvidence.subjectContentHash"),
2259
+ evidenceHash: requireSha256(record.evidenceHash, "PvoxGateEvidence.evidenceHash"),
2260
+ reasonCodes,
2261
+ evaluatedAt: requireTimestamp(record.evaluatedAt, "PvoxGateEvidence.evaluatedAt"),
2262
+ attestationToken: requireAttestation(record.attestationToken, "PvoxGateEvidence.attestationToken"),
2263
+ });
2264
+ }
2265
+
2266
+ function createTransform(value: unknown, fieldName: string): ModelTransform {
2267
+ const record = asRecord(value, fieldName);
2268
+ exactKeys(record, ["translationMetres", "rotationQuaternion", "scale"], fieldName);
2269
+ const translationMetres = requireVector(record.translationMetres, `${fieldName}.translationMetres`);
2270
+ const rotationEntries = asDenseArray(record.rotationQuaternion, `${fieldName}.rotationQuaternion`, 4, 4);
2271
+ const rotationQuaternion = rotationEntries.map((entry, index) => requireNumber(entry, `${fieldName}.rotationQuaternion[${index}]`, -1, 1)) as [number, number, number, number];
2272
+ if (Math.abs(Math.hypot(...rotationQuaternion) - 1) > 1e-4) throw new Error(`${fieldName}.rotationQuaternion must be normalized.`);
2273
+ const scale = requireVector(record.scale, `${fieldName}.scale`);
2274
+ if (scale.some((entry) => entry <= 0)) throw new Error(`${fieldName}.scale values must be positive.`);
2275
+ return { translationMetres, rotationQuaternion, scale };
2276
+ }
2277
+
2278
+ function createAssemblyChild(value: unknown, index: number, resolutionId: string, candidateId: string): PvoxAssemblyChild {
2279
+ const fieldName = `ModelProcessingManifestV2.children[${index}]`;
2280
+ const record = asRecord(value, fieldName);
2281
+ exactKeys(record, ["instanceId", "parentInstanceId", "assetRef", "transform"], fieldName);
2282
+ const childRef = asRecord(record.assetRef, `${fieldName}.assetRef`);
2283
+ let assetRef: PvoxAssemblyChild["assetRef"];
2284
+ if (childRef.disposition === "staged-derived") {
2285
+ exactKeys(childRef, ["disposition", "representation", "derivedId", "kind", "contentHash", "binaryClosureHash", "processingClosureHash", "processingManifestId", "processingManifestUri"], `${fieldName}.assetRef`);
2286
+ if (childRef.kind !== "leaf" || childRef.representation !== "pvox") throw new Error(`${fieldName}.assetRef must be a PVOX leaf.`);
2287
+ const derivedId = requirePathSegment(childRef.derivedId, `${fieldName}.assetRef.derivedId`);
2288
+ const expectedUri = `mcp://models/resolutions/${resolutionId}/candidates/${candidateId}/derived/${derivedId}/processing-manifest`;
2289
+ if (childRef.processingManifestUri !== expectedUri) throw new Error(`${fieldName}.assetRef.processingManifestUri must match the exact staged-derived PVOX identity.`);
2290
+ assetRef = {
2291
+ disposition: "staged-derived",
2292
+ representation: "pvox",
2293
+ derivedId,
2294
+ kind: "leaf",
2295
+ contentHash: requireSha256(childRef.contentHash, `${fieldName}.assetRef.contentHash`),
2296
+ binaryClosureHash: requireSha256(childRef.binaryClosureHash, `${fieldName}.assetRef.binaryClosureHash`),
2297
+ processingClosureHash: requireSha256(childRef.processingClosureHash, `${fieldName}.assetRef.processingClosureHash`),
2298
+ processingManifestId: requireToken(childRef.processingManifestId, `${fieldName}.assetRef.processingManifestId`),
2299
+ processingManifestUri: expectedUri,
2300
+ };
2301
+ } else {
2302
+ exactKeys(childRef, ["disposition", "representation", "asset", "processingManifestId", "binaryClosureHash", "processingClosureHash", "processingManifestUri"], `${fieldName}.assetRef`);
2303
+ if (childRef.disposition !== "existing" || childRef.representation !== "pvox") throw new Error(`${fieldName}.assetRef must use an existing or staged-derived PVOX disposition.`);
2304
+ assertDataOnlyTree(childRef.asset, `${fieldName}.assetRef.asset`);
2305
+ const asset = createModelAssetRef(childRef.asset);
2306
+ if (asset.kind !== "leaf") throw new Error(`${fieldName}.assetRef must reference a leaf.`);
2307
+ const expectedUri = `mcp://models/catalog/${asset.assetId}/versions/${asset.version}/processing-manifest`;
2308
+ if (childRef.processingManifestUri !== expectedUri) throw new Error(`${fieldName}.assetRef.processingManifestUri must match the immutable catalog PVOX identity.`);
2309
+ assetRef = {
2310
+ disposition: "existing",
2311
+ representation: "pvox",
2312
+ asset,
2313
+ processingManifestId: requireToken(childRef.processingManifestId, `${fieldName}.assetRef.processingManifestId`),
2314
+ binaryClosureHash: requireSha256(childRef.binaryClosureHash, `${fieldName}.assetRef.binaryClosureHash`),
2315
+ processingClosureHash: requireSha256(childRef.processingClosureHash, `${fieldName}.assetRef.processingClosureHash`),
2316
+ processingManifestUri: expectedUri,
2317
+ };
2318
+ }
2319
+ const parentInstanceId = record.parentInstanceId === undefined ? undefined : requireToken(record.parentInstanceId, `${fieldName}.parentInstanceId`);
2320
+ return {
2321
+ instanceId: requireToken(record.instanceId, `${fieldName}.instanceId`),
2322
+ ...(parentInstanceId === undefined ? {} : { parentInstanceId }),
2323
+ assetRef,
2324
+ transform: createTransform(record.transform, `${fieldName}.transform`),
2325
+ };
2326
+ }
2327
+
2328
+ function assertAssembly(children: readonly PvoxAssemblyChild[]): void {
2329
+ const byId = new Map(children.map((child) => [child.instanceId, child]));
2330
+ if (byId.size !== children.length) throw new Error("ModelProcessingManifestV2 assembly instance IDs must be unique.");
2331
+ if (children.some((child, index) => index > 0 && child.instanceId <= children[index - 1]!.instanceId)) throw new Error("ModelProcessingManifestV2 assembly children must be canonically ordered by instanceId.");
2332
+ if (children.filter(({ parentInstanceId }) => parentInstanceId === undefined).length !== 1) throw new Error("ModelProcessingManifestV2 assembly must contain exactly one root child.");
2333
+ const stagedDerivedIds = children.flatMap(({ assetRef }) => assetRef.disposition === "staged-derived" ? [assetRef.derivedId] : []);
2334
+ if (new Set(stagedDerivedIds).size !== stagedDerivedIds.length) throw new Error("ModelProcessingManifestV2 staged-derived child IDs must be unique.");
2335
+ for (const child of children) {
2336
+ const visited = new Set<string>();
2337
+ let current: PvoxAssemblyChild | undefined = child;
2338
+ let depth = 0;
2339
+ while (current?.parentInstanceId !== undefined) {
2340
+ if (current.parentInstanceId === current.instanceId || visited.has(current.instanceId)) throw new Error("ModelProcessingManifestV2 assembly must be acyclic.");
2341
+ visited.add(current.instanceId);
2342
+ current = byId.get(current.parentInstanceId);
2343
+ if (current === undefined) throw new Error("ModelProcessingManifestV2 assembly parent must exist.");
2344
+ depth += 1;
2345
+ if (depth > 16) throw new Error("ModelProcessingManifestV2 assembly depth exceeds the limit.");
2346
+ }
2347
+ }
2348
+ }
2349
+
2350
+ const PVOX_CAPABILITY_PHYSICAL_PROPERTIES: Readonly<Partial<Record<PvoxCapability, readonly PhysicalPropertyKind[]>>> = Object.freeze({
2351
+ destruction: ["density", "hardness", "tensile-strength", "compressive-strength", "shear-strength", "fracture-energy", "friction", "restitution", "interior-thickness"],
2352
+ thermal: ["thermal-conductivity", "heat-capacity", "thermal-expansion", "ignition-temperature", "melting-temperature", "flammability"],
2353
+ moisture: ["porosity", "permeability", "moisture-response", "corrosion-rate"],
2354
+ "fluid-boundary": ["porosity", "permeability"],
2355
+ });
2356
+
2357
+ const NON_DEFAULT_CRITICAL_PROPERTIES = new Set<PhysicalPropertyKind>([
2358
+ "density",
2359
+ "hardness",
2360
+ "tensile-strength",
2361
+ "compressive-strength",
2362
+ "shear-strength",
2363
+ "fracture-energy",
2364
+ "interior-thickness",
2365
+ "ignition-temperature",
2366
+ "melting-temperature",
2367
+ ]);
2368
+
2369
+ /** Validate and deeply freeze a PVOX-only processing manifest. */
2370
+ export function createModelProcessingManifestV2(input: unknown): ModelProcessingManifestV2 {
2371
+ const record = asRecord(input, "ModelProcessingManifestV2");
2372
+ exactKeys(record, [
2373
+ "contractVersion", "representation", "manifestId", "resolutionId", "candidateId", "kind",
2374
+ "requestProfileId", "requestSemanticProfileHash", "capabilityProfileId", "contentHash", "binaryClosureHash",
2375
+ "processingClosureHash", "processingClosureAttestation", "assemblyClosureHash", "assemblyClosureAttestation",
2376
+ "compilationInputHash", "runtimeRequestProfileHash", "coordinateSystem", "pvox", "technicalProfile",
2377
+ "requiredCapabilities", "capabilities", "capabilityEvidenceSetHash", "capabilityEvidenceSetAttestation",
2378
+ "physicalRegionInventory", "physicalProperties", "physicalEvidenceHash", "physicalEvidenceAttestation",
2379
+ "massPropertiesEvidence", "bondGraphEvidence", "interiorLayerEvidence", "converter", "fidelity", "children", "processedAt",
2380
+ ], "ModelProcessingManifestV2");
2381
+ if (record.contractVersion !== MODEL_RESOLUTION_V2_CONTRACT_VERSION) throw new Error("ModelProcessingManifestV2.contractVersion is unsupported.");
2382
+ if (record.representation !== "pvox") throw new Error("ModelProcessingManifestV2.representation must be pvox.");
2383
+ const resolutionId = requirePathSegment(record.resolutionId, "ModelProcessingManifestV2.resolutionId");
2384
+ const candidateId = requirePathSegment(record.candidateId, "ModelProcessingManifestV2.candidateId");
2385
+ assertCoordinateSystem(record.coordinateSystem, "ModelProcessingManifestV2.coordinateSystem");
2386
+ const pvox = createPvoxAssetManifestV1(record.pvox);
2387
+ const resourceScope = parsePvoxResourceScope(pvox.artifact.uri);
2388
+ if (resourceScope.kind === "resolution" && (resourceScope.resolutionId !== resolutionId || resourceScope.candidateId !== candidateId)) {
2389
+ throw new Error("ModelProcessingManifestV2 PVOX resource namespace must match resolutionId and candidateId.");
2390
+ }
2391
+ const technicalProfile = createVoxelTechnicalProfile(record.technicalProfile);
2392
+ const contentHash = requireSha256(record.contentHash, "ModelProcessingManifestV2.contentHash");
2393
+ const binaryClosureHash = requireSha256(record.binaryClosureHash, "ModelProcessingManifestV2.binaryClosureHash");
2394
+ const processingClosureHash = requireSha256(record.processingClosureHash, "ModelProcessingManifestV2.processingClosureHash");
2395
+ const assemblyClosureHash = requireSha256(record.assemblyClosureHash, "ModelProcessingManifestV2.assemblyClosureHash");
2396
+ const requestSemanticProfileHash = requireSha256(record.requestSemanticProfileHash, "ModelProcessingManifestV2.requestSemanticProfileHash");
2397
+ const compilationInputHash = requireSha256(record.compilationInputHash, "ModelProcessingManifestV2.compilationInputHash");
2398
+ const runtimeRequestProfileHash = requireSha256(record.runtimeRequestProfileHash, "ModelProcessingManifestV2.runtimeRequestProfileHash");
2399
+ if (contentHash !== pvox.artifact.sha256) throw new Error("ModelProcessingManifestV2 contentHash must match the PVOX artifact.");
2400
+ if (binaryClosureHash !== pvox.binaryClosureHash) throw new Error("ModelProcessingManifestV2 binaryClosureHash must match the PVOX binary closure.");
2401
+ if (compilationInputHash !== pvox.compilationInputHash || runtimeRequestProfileHash !== pvox.runtimeRequestProfileHash) {
2402
+ throw new Error("ModelProcessingManifestV2 compilation input and runtime request profile hashes must match the PVOX header bindings.");
2403
+ }
2404
+ const artifactRenderBrickCount = pvox.lods.reduce((total, lodRecord) => total + lodRecord.brickCount, 0);
2405
+ if (
2406
+ technicalProfile.artifactByteLength !== pvox.artifact.byteLength
2407
+ || technicalProfile.pageCount !== pvox.pages.length
2408
+ || technicalProfile.sectionCount !== pvox.sectionCount
2409
+ || technicalProfile.brickCount !== artifactRenderBrickCount
2410
+ || technicalProfile.lodCount !== pvox.lods.length
2411
+ || technicalProfile.geometryMode !== pvox.geometryMode
2412
+ || JSON.stringify(technicalProfile.boundsMetres) !== JSON.stringify(pvox.boundsMetres)
2413
+ ) throw new Error("ModelProcessingManifestV2 technicalProfile must match the PVOX artifact.");
2414
+ if (!pvox.pages.some(({ pageKind }) => pageKind === "metadata")) throw new Error("ModelProcessingManifestV2 PVOX must contain a metadata page for its header and directory.");
2415
+ if (technicalProfile.hierarchyNodeCount > 0 && !pvox.pages.some(({ pageKind }) => pageKind === "lod-structure")) {
2416
+ throw new Error("ModelProcessingManifestV2 hierarchy nodes require a lod-structure page.");
2417
+ }
2418
+ const collisionPages = pvox.pages.filter(({ pageKind }) => pageKind === "collision-field");
2419
+ if (technicalProfile.hasCollision !== (collisionPages.length > 0)) {
2420
+ throw new Error("ModelProcessingManifestV2 collision technical claims require matching collision-field pages and forbid unclaimed collision pages.");
2421
+ }
2422
+ const maximumGridExtentMetres = pvox.lods[0]!.cellSizeMetres * PVOX_BRICK_EDGE_VOXELS * 2 ** technicalProfile.hierarchyDepth;
2423
+ if (technicalProfile.maximumPartitionExtentMetres > maximumGridExtentMetres && !approximatelyEqual(technicalProfile.maximumPartitionExtentMetres, maximumGridExtentMetres)) {
2424
+ throw new Error("ModelProcessingManifestV2 partition extent exceeds the LOD0 cell and hierarchy grid capacity; use smaller fidelity-driven partitions.");
2425
+ }
2426
+ const capabilityEntries = asDenseArray(record.capabilities, "ModelProcessingManifestV2.capabilities", PVOX_CAPABILITIES.length, PVOX_CAPABILITIES.length);
2427
+ const capabilities = capabilityEntries.map(createVoxelCapabilityAssessment);
2428
+ const byCapability = new Map(capabilities.map((entry) => [entry.capability, entry]));
2429
+ if (byCapability.size !== PVOX_CAPABILITIES.length || PVOX_CAPABILITIES.some((name) => !byCapability.has(name))) throw new Error("ModelProcessingManifestV2 must include every PVOX capability exactly once.");
2430
+ const orderedCapabilities = PVOX_CAPABILITIES.map((name) => byCapability.get(name)!);
2431
+ if (orderedCapabilities.some((entry) => entry.subjectContentHash !== binaryClosureHash)) throw new Error("ModelProcessingManifestV2 capability evidence must bind the binaryClosureHash.");
2432
+ const capabilityEvidenceSetHash = requireSha256(record.capabilityEvidenceSetHash, "ModelProcessingManifestV2.capabilityEvidenceSetHash");
2433
+ const capabilityEvidenceSetAttestation = createHashAttestation(
2434
+ record.capabilityEvidenceSetAttestation,
2435
+ PVOX_CAPABILITY_SET_HASH_DOMAIN,
2436
+ capabilityEvidenceSetHash,
2437
+ "ModelProcessingManifestV2.capabilityEvidenceSetAttestation",
2438
+ );
2439
+ const requiredCapabilityEntries = asDenseArray(record.requiredCapabilities, "ModelProcessingManifestV2.requiredCapabilities", 1, PVOX_CAPABILITIES.length);
2440
+ const requiredCapabilities = requiredCapabilityEntries.map((entry, index) => requireEnum(entry, PVOX_CAPABILITIES, `ModelProcessingManifestV2.requiredCapabilities[${index}]`));
2441
+ if (new Set(requiredCapabilities).size !== requiredCapabilities.length) throw new Error("ModelProcessingManifestV2.requiredCapabilities must be unique.");
2442
+ if (!requiredCapabilities.includes("rendering")) throw new Error("ModelProcessingManifestV2 rendering capability is always required.");
2443
+ if (technicalProfile.hasCollision !== (byCapability.get("collision")?.status === "supported")) throw new Error("ModelProcessingManifestV2 collision capability must match technicalProfile.hasCollision.");
2444
+ if (technicalProfile.hasCollision && collisionPages.every(({ spatialKey }) => spatialKey?.lodLevel !== 0)) {
2445
+ throw new Error("ModelProcessingManifestV2 supported collision requires LOD0 collision-field evidence.");
2446
+ }
2447
+ const physicalRegionInventory = createPvoxPhysicalRegionInventoryV1(record.physicalRegionInventory);
2448
+ if (physicalRegionInventory.subjectContentHash !== binaryClosureHash
2449
+ || physicalRegionInventory.validationEvidenceHash !== pvox.validationEvidenceHash
2450
+ || physicalRegionInventory.entries.length !== technicalProfile.materialRegionCount
2451
+ || physicalRegionInventory.entries.some(({ physicalPaletteIndex }) => physicalPaletteIndex >= technicalProfile.physicalPaletteRecordCount)) {
2452
+ throw new Error("ModelProcessingManifestV2 authoritative physical region inventory must bind the PVOX validator, closure, region count, and palette records.");
2453
+ }
2454
+ const physicalEntries = asDenseArray(record.physicalProperties, "ModelProcessingManifestV2.physicalProperties", 0, PVOX_DEFAULT_LIMITS.maximumPhysicalEvidenceEntries);
2455
+ const physicalProperties = physicalEntries.map(createPhysicalPropertyEvidence);
2456
+ const byRegionProperty = new Map(physicalProperties.map((entry) => [`${entry.regionId}\u0000${entry.materialId}\u0000${entry.property}`, entry]));
2457
+ if (byRegionProperty.size !== physicalProperties.length) throw new Error("ModelProcessingManifestV2 physical properties must be unique per region, material, and property.");
2458
+ if (physicalProperties.some((entry) => entry.subjectContentHash !== binaryClosureHash)) throw new Error("ModelProcessingManifestV2 physical evidence must bind the PVOX binary closure subject.");
2459
+ const authoritativePairs = physicalRegionInventory.entries.map(({ regionId, materialId }) => `${regionId}\u0000${materialId}`);
2460
+ const authoritativePairSet = new Set(authoritativePairs);
2461
+ if (physicalProperties.some(({ regionId, materialId }) => !authoritativePairSet.has(`${regionId}\u0000${materialId}`))) {
2462
+ throw new Error("ModelProcessingManifestV2 physical evidence cannot introduce a region absent from the authoritative inventory.");
2463
+ }
2464
+ const authoritativeRegionIndex = new Map(authoritativePairs.map((pair, index) => [pair, index]));
2465
+ const physicalPropertyIndex = new Map(PVOX_PHYSICAL_PROPERTIES.map((property, index) => [property, index]));
2466
+ let previousPhysicalOrder = -1;
2467
+ for (const evidence of physicalProperties) {
2468
+ const regionIndex = authoritativeRegionIndex.get(`${evidence.regionId}\u0000${evidence.materialId}`)!;
2469
+ const propertyIndex = physicalPropertyIndex.get(evidence.property)!;
2470
+ const order = regionIndex * PVOX_PHYSICAL_PROPERTIES.length + propertyIndex;
2471
+ if (order <= previousPhysicalOrder) {
2472
+ throw new Error("ModelProcessingManifestV2 physical properties must be canonically ordered by inventory regionIndex and governed property order.");
2473
+ }
2474
+ previousPhysicalOrder = order;
2475
+ }
2476
+ for (const capability of PVOX_CAPABILITIES) {
2477
+ if (byCapability.get(capability)?.status !== "supported") continue;
2478
+ if ((PVOX_CAPABILITY_PHYSICAL_PROPERTIES[capability]?.length ?? 0) > 0 && authoritativePairs.length === 0) {
2479
+ throw new Error(`Supported ${capability} capability requires governed physical evidence.`);
2480
+ }
2481
+ for (const pair of authoritativePairs) {
2482
+ const [regionId, materialId] = pair.split("\u0000");
2483
+ for (const property of PVOX_CAPABILITY_PHYSICAL_PROPERTIES[capability] ?? []) {
2484
+ const evidence = byRegionProperty.get(`${regionId}\u0000${materialId}\u0000${property}`);
2485
+ if (evidence === undefined || evidence.confidence < 0.8 || (NON_DEFAULT_CRITICAL_PROPERTIES.has(property) && evidence.provenance === "default")) {
2486
+ throw new Error(`Supported ${capability} capability requires governed ${property} evidence for every material region with confidence at least 0.80 and no critical defaults.`);
2487
+ }
2488
+ }
2489
+ }
2490
+ }
2491
+ if (physicalProperties.length !== technicalProfile.physicalEvidenceRecordCount) {
2492
+ throw new Error("ModelProcessingManifestV2 authoritative material-region evidence count must match the PVOX PEVI record count.");
2493
+ }
2494
+ const physicalEvidenceHash = requireSha256(record.physicalEvidenceHash, "ModelProcessingManifestV2.physicalEvidenceHash");
2495
+ const physicalEvidenceAttestation = createHashAttestation(
2496
+ record.physicalEvidenceAttestation,
2497
+ PVOX_PHYSICAL_EVIDENCE_HASH_DOMAIN,
2498
+ physicalEvidenceHash,
2499
+ "ModelProcessingManifestV2.physicalEvidenceAttestation",
2500
+ );
2501
+ const massPropertiesEvidence = createReviewedStructuralEvidence(
2502
+ record.massPropertiesEvidence,
2503
+ "mass-properties",
2504
+ binaryClosureHash,
2505
+ physicalRegionInventory.inventoryHash,
2506
+ technicalProfile.massPropertyRecordCount,
2507
+ "ModelProcessingManifestV2.massPropertiesEvidence",
2508
+ );
2509
+ const bondGraphEvidence = createReviewedStructuralEvidence(
2510
+ record.bondGraphEvidence,
2511
+ "bond-graph",
2512
+ binaryClosureHash,
2513
+ physicalRegionInventory.inventoryHash,
2514
+ technicalProfile.bondRecordCount,
2515
+ "ModelProcessingManifestV2.bondGraphEvidence",
2516
+ );
2517
+ const interiorLayerEvidence = createReviewedStructuralEvidence(
2518
+ record.interiorLayerEvidence,
2519
+ "interior-layers",
2520
+ binaryClosureHash,
2521
+ physicalRegionInventory.inventoryHash,
2522
+ technicalProfile.interiorLayerRecordCount,
2523
+ "ModelProcessingManifestV2.interiorLayerEvidence",
2524
+ );
2525
+ if (byCapability.get("destruction")?.status === "supported"
2526
+ && (technicalProfile.massPropertyRecordCount === 0
2527
+ || technicalProfile.bondRecordCount === 0
2528
+ || technicalProfile.interiorLayerRecordCount === 0)) {
2529
+ throw new Error("ModelProcessingManifestV2 destruction capability requires reviewed mass, bond-graph, and interior-layer records.");
2530
+ }
2531
+ const converter = createCompilerEvidence(record.converter);
2532
+ if (
2533
+ converter.sourceContentHash !== pvox.sourceContentHash
2534
+ || converter.canonicalDocumentHash !== pvox.canonicalDocumentHash
2535
+ || converter.compilationInputHash !== compilationInputHash
2536
+ || converter.runtimeRequestProfileHash !== runtimeRequestProfileHash
2537
+ || converter.outputContentHash !== contentHash
2538
+ ) throw new Error("ModelProcessingManifestV2 converter hashes must match source, canonical document, compilation input, runtime request profile, and PVOX outputContentHash.");
2539
+ const fidelity = createPvoxFidelityEvidence(record.fidelity);
2540
+ const diagonalMetres = technicalProfile.maximumPartitionDiagonalMetres;
2541
+ if (fidelity.canonicalDocumentHash !== pvox.canonicalDocumentHash
2542
+ || fidelity.pvoxBinaryClosureHash !== binaryClosureHash
2543
+ || !approximatelyEqual(fidelity.evaluatedDiagonalMetres, diagonalMetres)
2544
+ || !approximatelyEqual(fidelity.lod0CellSizeMetres, pvox.lods[0]!.cellSizeMetres)) {
2545
+ throw new Error("ModelProcessingManifestV2 fidelity evidence must bind the canonical document, PVOX closure, evaluated bounds, and LOD0 cell size.");
2546
+ }
2547
+ const lod0 = pvox.lods[0]!;
2548
+ if (!approximatelyEqual(fidelity.maximumSurfaceErrorMetres, lod0.maximumSurfaceErrorMetres)
2549
+ || !approximatelyEqual(fidelity.p99SurfaceErrorMetres, lod0.p99SurfaceErrorMetres)
2550
+ || !approximatelyEqual(fidelity.silhouetteIou, lod0.silhouetteIou)
2551
+ || !approximatelyEqual(fidelity.maximumContourDisplacementPx, lod0.maximumContourDisplacementPx)
2552
+ || !approximatelyEqual(fidelity.renderedSsim, lod0.renderedSsim)
2553
+ || !approximatelyEqual(fidelity.p95DeltaE2000, lod0.p95DeltaE2000)
2554
+ || !approximatelyEqual(fidelity.normalizedMaterialError, lod0.normalizedMaterialError)) {
2555
+ throw new Error("ModelProcessingManifestV2 fidelity evidence must exactly match every governed PVOX LOD0 metric.");
2556
+ }
2557
+ const childEntries = asDenseArray(record.children, "ModelProcessingManifestV2.children", 0, PVOX_DEFAULT_LIMITS.maximumAssemblyChildren);
2558
+ const children = childEntries.map((entry, index) => createAssemblyChild(entry, index, resolutionId, candidateId));
2559
+ const kind = requireEnum(record.kind, ["leaf", "assembly"] as const, "ModelProcessingManifestV2.kind");
2560
+ if (kind === "leaf" && children.length !== 0) throw new Error("A leaf ModelProcessingManifestV2 cannot contain children.");
2561
+ if (kind === "leaf" && technicalProfile.partitionCount !== 1) throw new Error("A leaf ModelProcessingManifestV2 must have one partition.");
2562
+ if (kind === "leaf" && assemblyClosureHash === binaryClosureHash) throw new Error("A leaf ModelProcessingManifestV2 requires a distinct domain-separated empty-child assemblyClosureHash.");
2563
+ if (kind === "assembly" && children.length === 0) throw new Error("An assembly ModelProcessingManifestV2 requires children.");
2564
+ if (kind === "assembly") assertAssembly(children);
2565
+ const assemblyClosureAttestation = createHashAttestation(record.assemblyClosureAttestation, PVOX_ASSEMBLY_CLOSURE_HASH_DOMAIN, assemblyClosureHash, "ModelProcessingManifestV2.assemblyClosureAttestation");
2566
+ const processingClosureAttestation = createHashAttestation(
2567
+ record.processingClosureAttestation,
2568
+ PVOX_PROCESSING_CLOSURE_HASH_DOMAIN,
2569
+ processingClosureHash,
2570
+ "ModelProcessingManifestV2.processingClosureAttestation",
2571
+ );
2572
+ const processedAt = requireTimestamp(record.processedAt, "ModelProcessingManifestV2.processedAt");
2573
+ const latestEvidence = Math.max(
2574
+ Date.parse(converter.compiledAt),
2575
+ Date.parse(fidelity.evaluatedAt),
2576
+ ...orderedCapabilities.map(({ evaluatedAt }) => Date.parse(evaluatedAt)),
2577
+ ...physicalProperties.flatMap(({ reviewedAt }) => reviewedAt === undefined ? [] : [Date.parse(reviewedAt)]),
2578
+ Date.parse(massPropertiesEvidence.reviewedAt),
2579
+ Date.parse(bondGraphEvidence.reviewedAt),
2580
+ Date.parse(interiorLayerEvidence.reviewedAt),
2581
+ );
2582
+ if (Date.parse(processedAt) < latestEvidence) throw new Error("ModelProcessingManifestV2.processedAt must follow all processing evidence.");
2583
+ return deepFreeze({
2584
+ contractVersion: MODEL_RESOLUTION_V2_CONTRACT_VERSION,
2585
+ representation: "pvox",
2586
+ manifestId: requireToken(record.manifestId, "ModelProcessingManifestV2.manifestId"),
2587
+ resolutionId,
2588
+ candidateId,
2589
+ kind,
2590
+ requestProfileId: requireToken(record.requestProfileId, "ModelProcessingManifestV2.requestProfileId"),
2591
+ requestSemanticProfileHash,
2592
+ capabilityProfileId: requireEnum(record.capabilityProfileId, PVOX_RUNTIME_CAPABILITY_PROFILE_IDS, "ModelProcessingManifestV2.capabilityProfileId"),
2593
+ contentHash,
2594
+ binaryClosureHash,
2595
+ processingClosureHash,
2596
+ processingClosureAttestation,
2597
+ assemblyClosureHash,
2598
+ assemblyClosureAttestation,
2599
+ compilationInputHash,
2600
+ runtimeRequestProfileHash,
2601
+ coordinateSystem: CANONICAL_MODEL_COORDINATE_SYSTEM,
2602
+ pvox,
2603
+ technicalProfile,
2604
+ requiredCapabilities,
2605
+ capabilities: orderedCapabilities,
2606
+ capabilityEvidenceSetHash,
2607
+ capabilityEvidenceSetAttestation,
2608
+ physicalRegionInventory,
2609
+ physicalProperties,
2610
+ physicalEvidenceHash,
2611
+ physicalEvidenceAttestation,
2612
+ massPropertiesEvidence,
2613
+ bondGraphEvidence,
2614
+ interiorLayerEvidence,
2615
+ converter,
2616
+ fidelity,
2617
+ children,
2618
+ processedAt,
2619
+ });
2620
+ }
2621
+
2622
+ function createCandidateAssetRef(value: unknown): ModelCandidateAssetRef {
2623
+ const record = asRecord(value, "ModelCandidateV2.assetRef");
2624
+ if (record.disposition === "existing") {
2625
+ exactKeys(record, ["disposition", "kind", "contentHash", "asset"], "ModelCandidateV2.assetRef");
2626
+ assertDataOnlyTree(record.asset, "ModelCandidateV2.assetRef.asset");
2627
+ const asset = createModelAssetRef(record.asset);
2628
+ if (record.kind !== asset.kind || record.contentHash !== asset.contentHash) throw new Error("ModelCandidateV2 existing assetRef must match the immutable asset.");
2629
+ return { disposition: "existing", kind: asset.kind, contentHash: asset.contentHash, asset };
2630
+ }
2631
+ exactKeys(record, ["disposition", "proposalId", "kind", "contentHash"], "ModelCandidateV2.assetRef");
2632
+ if (record.disposition !== "proposed") throw new Error("ModelCandidateV2.assetRef disposition is unsupported.");
2633
+ return {
2634
+ disposition: "proposed",
2635
+ proposalId: requireToken(record.proposalId, "ModelCandidateV2.assetRef.proposalId"),
2636
+ kind: requireEnum(record.kind, ["leaf", "assembly"] as const, "ModelCandidateV2.assetRef.kind"),
2637
+ contentHash: requireSha256(record.contentHash, "ModelCandidateV2.assetRef.contentHash"),
2638
+ };
2639
+ }
2640
+
2641
+ function createViews(value: unknown, resolutionId: string, candidateId: string): ModelConfirmationViews {
2642
+ const entries = asDenseArray(value, "ModelCandidateV2.views", 4, 4);
2643
+ const parsed = entries.map((entry, index) => {
2644
+ const fieldName = `ModelCandidateV2.views[${index}]`;
2645
+ const record = asRecord(entry, fieldName);
2646
+ exactKeys(record, ["kind", "imageUri", "sha256", "contentType", "width", "height"], fieldName);
2647
+ const kind = requireEnum(record.kind, MODEL_CONFIRMATION_VIEW_KINDS, `${fieldName}.kind`);
2648
+ const expectedUri = `mcp://models/resolutions/${resolutionId}/candidates/${candidateId}/${kind}.png`;
2649
+ if (record.imageUri !== expectedUri) throw new Error(`${fieldName}.imageUri must match the candidate view identity.`);
2650
+ if (record.contentType !== "image/png" || record.width !== MODEL_CONFIRMATION_VIEW_SIZE_PX || record.height !== MODEL_CONFIRMATION_VIEW_SIZE_PX) throw new Error(`${fieldName} must be an authenticated 1024x1024 PNG.`);
2651
+ return { kind, imageUri: expectedUri, sha256: requireSha256(record.sha256, `${fieldName}.sha256`), contentType: "image/png" as const, width: MODEL_CONFIRMATION_VIEW_SIZE_PX, height: MODEL_CONFIRMATION_VIEW_SIZE_PX };
2652
+ });
2653
+ if (parsed.some((entry, index) => entry.kind !== MODEL_CONFIRMATION_VIEW_KINDS[index])) throw new Error("ModelCandidateV2.views must be ordered front, left, top, isometric.");
2654
+ return parsed as unknown as ModelConfirmationViews;
2655
+ }
2656
+
2657
+ function createRenderEvidence(value: unknown): PvoxRenderEvidence {
2658
+ const record = asRecord(value, "ModelCandidateV2.renderEvidence");
2659
+ exactKeys(record, ["renderId", "representation", "lodLevel", "traversalBackend", "rendererId", "rendererVersion", "rendererQualificationHash", "settingsId", "settingsVersion", "cameraQualificationHash", "processingManifestId", "processingClosureHash", "pvoxContentHash", "pvoxRootHash", "pvoxDirectoryHash", "pvoxPageSetHash", "viewSha256s", "renderedAt", "evidenceHash", "evidenceHashAttestation"], "ModelCandidateV2.renderEvidence");
2660
+ if (record.representation !== "pvox" || record.lodLevel !== 0 || record.traversalBackend !== "native-pvox") {
2661
+ throw new Error("ModelCandidateV2 render evidence must use native PVOX LOD0 traversal.");
2662
+ }
2663
+ if (record.rendererId !== PVOX_NATIVE_RENDERER_ID || record.settingsId !== PVOX_NATIVE_REVIEW_SETTINGS_ID) {
2664
+ throw new Error("ModelCandidateV2 render evidence must use the governed native renderer and review settings.");
2665
+ }
2666
+ const evidenceHash = requireSha256(record.evidenceHash, "ModelCandidateV2.renderEvidence.evidenceHash");
2667
+ return {
2668
+ renderId: requireToken(record.renderId, "ModelCandidateV2.renderEvidence.renderId"),
2669
+ representation: "pvox",
2670
+ lodLevel: 0,
2671
+ traversalBackend: "native-pvox",
2672
+ rendererId: PVOX_NATIVE_RENDERER_ID,
2673
+ rendererVersion: assertImmutableAssetVersion(record.rendererVersion),
2674
+ rendererQualificationHash: requireSha256(record.rendererQualificationHash, "ModelCandidateV2.renderEvidence.rendererQualificationHash"),
2675
+ settingsId: PVOX_NATIVE_REVIEW_SETTINGS_ID,
2676
+ settingsVersion: assertImmutableAssetVersion(record.settingsVersion),
2677
+ cameraQualificationHash: requireSha256(record.cameraQualificationHash, "ModelCandidateV2.renderEvidence.cameraQualificationHash"),
2678
+ processingManifestId: requireToken(record.processingManifestId, "ModelCandidateV2.renderEvidence.processingManifestId"),
2679
+ processingClosureHash: requireSha256(record.processingClosureHash, "ModelCandidateV2.renderEvidence.processingClosureHash"),
2680
+ pvoxContentHash: requireSha256(record.pvoxContentHash, "ModelCandidateV2.renderEvidence.pvoxContentHash"),
2681
+ pvoxRootHash: requireSha256(record.pvoxRootHash, "ModelCandidateV2.renderEvidence.pvoxRootHash"),
2682
+ pvoxDirectoryHash: requireSha256(record.pvoxDirectoryHash, "ModelCandidateV2.renderEvidence.pvoxDirectoryHash"),
2683
+ pvoxPageSetHash: requireSha256(record.pvoxPageSetHash, "ModelCandidateV2.renderEvidence.pvoxPageSetHash"),
2684
+ viewSha256s: requireShaTuple(record.viewSha256s, "ModelCandidateV2.renderEvidence.viewSha256s"),
2685
+ renderedAt: requireTimestamp(record.renderedAt, "ModelCandidateV2.renderEvidence.renderedAt"),
2686
+ evidenceHash,
2687
+ evidenceHashAttestation: createHashAttestation(record.evidenceHashAttestation, PVOX_RENDER_EVIDENCE_HASH_DOMAIN, evidenceHash, "ModelCandidateV2.renderEvidence.evidenceHashAttestation"),
2688
+ };
2689
+ }
2690
+
2691
+ /** Extend a released-v1 assessment without changing its closed v1 factory. */
2692
+ export function createPvoxModelMatchAssessmentV2(input: unknown): PvoxModelMatchAssessmentV2 {
2693
+ const fieldName = "PvoxModelMatchAssessmentV2";
2694
+ const record = asRecord(input, fieldName);
2695
+ assertDataOnlyTree(record, fieldName);
2696
+ exactKeys(record, [
2697
+ "contractVersion", "score", "assurance", "hardConstraintPass", "exactMatch", "reasonCodes", "ranker",
2698
+ "fidelityWarnings", "request", "candidateId", "candidateContentHash", "requestSemanticProfileHash",
2699
+ "requestSemanticProfileCanonical", "evidenceHash",
2700
+ ], fieldName);
2701
+ const assessment = createModelMatchAssessment({
2702
+ ...(record.contractVersion === undefined ? {} : { contractVersion: record.contractVersion }),
2703
+ score: record.score,
2704
+ ...(record.assurance === undefined ? {} : { assurance: record.assurance }),
2705
+ hardConstraintPass: record.hardConstraintPass,
2706
+ exactMatch: record.exactMatch,
2707
+ reasonCodes: record.reasonCodes,
2708
+ ranker: record.ranker,
2709
+ fidelityWarnings: record.fidelityWarnings,
2710
+ request: record.request,
2711
+ candidateId: record.candidateId,
2712
+ candidateContentHash: record.candidateContentHash,
2713
+ });
2714
+ return deepFreeze({
2715
+ ...assessment,
2716
+ requestSemanticProfileHash: requireSha256(record.requestSemanticProfileHash, `${fieldName}.requestSemanticProfileHash`),
2717
+ requestSemanticProfileCanonical: requireString(record.requestSemanticProfileCanonical, `${fieldName}.requestSemanticProfileCanonical`, 65_536),
2718
+ evidenceHash: requireSha256(record.evidenceHash, `${fieldName}.evidenceHash`),
2719
+ });
2720
+ }
2721
+
2722
+ function createConfirmationBinding(value: unknown): ModelCandidateConfirmationBindingV2 {
2723
+ const record = asRecord(value, "ModelCandidateV2.confirmationBinding");
2724
+ assertDataOnlyTree(record, "ModelCandidateV2.confirmationBinding");
2725
+ exactKeys(record, ["bindingHash", "bindingHashAttestation", "resolutionId", "requesterId", "request", "requestSemanticProfileHash", "requestRevision", "candidateId", "candidateContentHash", "candidateAssetRef", "match", "provenance", "rights", "sourceContentHash", "binaryClosureHash", "pvoxRootHash", "pvoxDirectoryHash", "pvoxPageSetHash", "compilationInputHash", "runtimeRequestProfileHash", "processingClosureHash", "assemblyClosureHash", "evaluationClosureHash", "capabilityEvidenceSetHash", "hardGates", "viewSha256s", "rightsDecisionId", "fidelityEvidenceHash", "physicalEvidenceHash", "rendererId", "rendererVersion", "rendererEvidenceHash", "renderEvidence"], "ModelCandidateV2.confirmationBinding");
2726
+ const bindingHash = requireSha256(record.bindingHash, "ModelCandidateV2.confirmationBinding.bindingHash");
2727
+ const hardGateEntries = asDenseArray(record.hardGates, "ModelCandidateV2.confirmationBinding.hardGates", PVOX_NON_OVERRIDABLE_GATE_KINDS.length, PVOX_NON_OVERRIDABLE_GATE_KINDS.length);
2728
+ return {
2729
+ bindingHash,
2730
+ bindingHashAttestation: createHashAttestation(record.bindingHashAttestation, PVOX_CONFIRMATION_BINDING_HASH_DOMAIN, bindingHash, "ModelCandidateV2.confirmationBinding.bindingHashAttestation"),
2731
+ resolutionId: requirePathSegment(record.resolutionId, "ModelCandidateV2.confirmationBinding.resolutionId"),
2732
+ requesterId: requireToken(record.requesterId, "ModelCandidateV2.confirmationBinding.requesterId"),
2733
+ request: createModelRequestSpecV2(record.request),
2734
+ requestSemanticProfileHash: requireSha256(record.requestSemanticProfileHash, "ModelCandidateV2.confirmationBinding.requestSemanticProfileHash"),
2735
+ requestRevision: requireInteger(record.requestRevision, "ModelCandidateV2.confirmationBinding.requestRevision", 0, MODEL_REQUEST_MAX_REVISION),
2736
+ candidateId: requirePathSegment(record.candidateId, "ModelCandidateV2.confirmationBinding.candidateId"),
2737
+ candidateContentHash: requireSha256(record.candidateContentHash, "ModelCandidateV2.confirmationBinding.candidateContentHash"),
2738
+ candidateAssetRef: createCandidateAssetRef(record.candidateAssetRef),
2739
+ match: createPvoxModelMatchAssessmentV2(record.match),
2740
+ provenance: createModelProvenance(record.provenance),
2741
+ rights: createModelRightsAssessment(record.rights),
2742
+ sourceContentHash: requireSha256(record.sourceContentHash, "ModelCandidateV2.confirmationBinding.sourceContentHash"),
2743
+ binaryClosureHash: requireSha256(record.binaryClosureHash, "ModelCandidateV2.confirmationBinding.binaryClosureHash"),
2744
+ pvoxRootHash: requireSha256(record.pvoxRootHash, "ModelCandidateV2.confirmationBinding.pvoxRootHash"),
2745
+ pvoxDirectoryHash: requireSha256(record.pvoxDirectoryHash, "ModelCandidateV2.confirmationBinding.pvoxDirectoryHash"),
2746
+ pvoxPageSetHash: requireSha256(record.pvoxPageSetHash, "ModelCandidateV2.confirmationBinding.pvoxPageSetHash"),
2747
+ compilationInputHash: requireSha256(record.compilationInputHash, "ModelCandidateV2.confirmationBinding.compilationInputHash"),
2748
+ runtimeRequestProfileHash: requireSha256(record.runtimeRequestProfileHash, "ModelCandidateV2.confirmationBinding.runtimeRequestProfileHash"),
2749
+ processingClosureHash: requireSha256(record.processingClosureHash, "ModelCandidateV2.confirmationBinding.processingClosureHash"),
2750
+ assemblyClosureHash: requireSha256(record.assemblyClosureHash, "ModelCandidateV2.confirmationBinding.assemblyClosureHash"),
2751
+ evaluationClosureHash: requireSha256(record.evaluationClosureHash, "ModelCandidateV2.confirmationBinding.evaluationClosureHash"),
2752
+ capabilityEvidenceSetHash: requireSha256(record.capabilityEvidenceSetHash, "ModelCandidateV2.confirmationBinding.capabilityEvidenceSetHash"),
2753
+ hardGates: hardGateEntries.map(createPvoxGateEvidence),
2754
+ viewSha256s: requireShaTuple(record.viewSha256s, "ModelCandidateV2.confirmationBinding.viewSha256s"),
2755
+ rightsDecisionId: requireToken(record.rightsDecisionId, "ModelCandidateV2.confirmationBinding.rightsDecisionId"),
2756
+ fidelityEvidenceHash: requireSha256(record.fidelityEvidenceHash, "ModelCandidateV2.confirmationBinding.fidelityEvidenceHash"),
2757
+ physicalEvidenceHash: requireSha256(record.physicalEvidenceHash, "ModelCandidateV2.confirmationBinding.physicalEvidenceHash"),
2758
+ rendererId: requireToken(record.rendererId, "ModelCandidateV2.confirmationBinding.rendererId"),
2759
+ rendererVersion: assertImmutableAssetVersion(record.rendererVersion),
2760
+ rendererEvidenceHash: requireSha256(record.rendererEvidenceHash, "ModelCandidateV2.confirmationBinding.rendererEvidenceHash"),
2761
+ renderEvidence: createRenderEvidence(record.renderEvidence),
2762
+ };
2763
+ }
2764
+
2765
+ function assertRequestMatchesProcessing(request: ModelRequestSpecV2, processing: ModelProcessingManifestV2): void {
2766
+ const profile = request.pvoxRuntimeProfile;
2767
+ const technical = processing.technicalProfile;
2768
+ if (processing.requestProfileId !== profile.profileId
2769
+ || processing.requestSemanticProfileHash !== request.requestSemanticProfileHash
2770
+ || processing.capabilityProfileId !== profile.capabilityProfileId
2771
+ || processing.fidelity.profileId !== profile.fidelityProfileId) {
2772
+ throw new Error("ModelCandidateV2 processing profile IDs must match the exact v2 request.");
2773
+ }
2774
+ if (profile.geometryMode !== "auto" && profile.geometryMode !== technical.geometryMode) {
2775
+ throw new Error("ModelCandidateV2 processing geometryMode must match the exact v2 request.");
2776
+ }
2777
+ const requestedCapabilities = new Set(profile.requiredCapabilities);
2778
+ if (requestedCapabilities.size !== processing.requiredCapabilities.length
2779
+ || processing.requiredCapabilities.some((capability) => !requestedCapabilities.has(capability))) {
2780
+ throw new Error("ModelCandidateV2 processing requiredCapabilities must exactly match the v2 request.");
2781
+ }
2782
+ const limits = profile.limits;
2783
+ if (technical.artifactByteLength > limits.maximumArtifactBytes
2784
+ || technical.pageCount > limits.maximumPages
2785
+ || technical.hierarchyDepth > limits.maximumHierarchyDepth
2786
+ || technical.hierarchyNodeCount + technical.collisionHierarchyNodeCount > limits.maximumHierarchyNodes
2787
+ || technical.brickCount + technical.collisionBrickCount > limits.maximumBricks
2788
+ || technical.logicalVoxelCapacity + technical.collisionLogicalVoxelCapacity > limits.maximumLogicalVoxels
2789
+ || technical.encodedSurfaceSampleCount + technical.collisionEncodedSurfaceSampleCount > limits.maximumEncodedSurfaceSamples
2790
+ || technical.surfacePropertyCount > limits.maximumSurfaceProperties
2791
+ || technical.physicalPaletteRecordCount > limits.maximumPhysicalPaletteRecords
2792
+ || technical.physicalEvidenceRecordCount > limits.maximumPhysicalEvidenceEntries
2793
+ || technical.materialRegionCount > limits.maximumMaterialRegions
2794
+ || technical.interiorLayerRecordCount > limits.maximumInteriorLayers
2795
+ || technical.massPropertyRecordCount > limits.maximumMassPropertyRecords
2796
+ || technical.bondRecordCount > limits.maximumBondRecords
2797
+ || technical.partitionCount > limits.maximumPartitions
2798
+ || technical.lodCount > limits.maximumLodCount
2799
+ || technical.cpuResidentByteLength > limits.maximumCpuResidentBytes
2800
+ || technical.gpuResidentByteLength > limits.maximumGpuResidentBytes) {
2801
+ throw new Error("ModelCandidateV2 PVOX technical profile exceeds an exact request runtime limit.");
2802
+ }
2803
+ const constraints = request.hardConstraints;
2804
+ if (constraints.boundsMetres !== undefined
2805
+ && (constraints.boundsMetres.min.some((value, index) => !approximatelyEqual(value, technical.boundsMetres.min[index]!))
2806
+ || constraints.boundsMetres.max.some((value, index) => !approximatelyEqual(value, technical.boundsMetres.max[index]!)))) {
2807
+ throw new Error("ModelCandidateV2 PVOX bounds do not satisfy the exact request.");
2808
+ }
2809
+ if (constraints.dimensionsMetres !== undefined
2810
+ && (!approximatelyEqual(constraints.dimensionsMetres.width, technical.dimensionsMetres.width)
2811
+ || !approximatelyEqual(constraints.dimensionsMetres.height, technical.dimensionsMetres.height)
2812
+ || !approximatelyEqual(constraints.dimensionsMetres.depth, technical.dimensionsMetres.depth))) {
2813
+ throw new Error("ModelCandidateV2 PVOX dimensions do not satisfy the exact request.");
2814
+ }
2815
+ if ((constraints.collision === "required" && !technical.hasCollision)
2816
+ || (constraints.collision === "forbidden" && technical.hasCollision)) {
2817
+ throw new Error("ModelCandidateV2 PVOX collision capability does not satisfy the exact request.");
2818
+ }
2819
+ if ((constraints.partition === "single" && technical.partitionCount !== 1)
2820
+ || (constraints.partition === "required" && technical.partitionCount < 2)) {
2821
+ throw new Error("ModelCandidateV2 PVOX partitioning does not satisfy the exact request.");
2822
+ }
2823
+ }
2824
+
2825
+ /** Validate a complete PVOX candidate and every non-overridable binding. */
2826
+ export function createModelCandidateV2(input: unknown): ModelCandidateV2 {
2827
+ const record = asRecord(input, "ModelCandidateV2");
2828
+ assertDataOnlyTree(record, "ModelCandidateV2");
2829
+ const commonKeys = ["contractVersion", "resolutionId", "requesterId", "candidateId", "request", "admissionStatus", "assetRef", "match", "provenance", "rights", "processingManifest", "views", "renderEvidence", "evaluationClosureHash", "evaluationClosureAttestation", "hardGates", "confirmationRequired"] as const;
2830
+ const admissionStatus = requireEnum(record.admissionStatus, ["confirmable", "diagnostic"] as const, "ModelCandidateV2.admissionStatus");
2831
+ exactKeys(record, admissionStatus === "confirmable" ? [...commonKeys, "confirmationBinding", "confirmationToken"] : [...commonKeys, "blockingReasonCodes"], "ModelCandidateV2");
2832
+ if (record.contractVersion !== MODEL_RESOLUTION_V2_CONTRACT_VERSION) throw new Error("ModelCandidateV2.contractVersion is unsupported.");
2833
+ const resolutionId = requirePathSegment(record.resolutionId, "ModelCandidateV2.resolutionId");
2834
+ const requesterId = requireToken(record.requesterId, "ModelCandidateV2.requesterId");
2835
+ const candidateId = requirePathSegment(record.candidateId, "ModelCandidateV2.candidateId");
2836
+ const request = createModelRequestSpecV2(record.request);
2837
+ const assetRef = createCandidateAssetRef(record.assetRef);
2838
+ assertDataOnlyTree(record.match, "ModelCandidateV2.match");
2839
+ assertDataOnlyTree(record.provenance, "ModelCandidateV2.provenance");
2840
+ assertDataOnlyTree(record.rights, "ModelCandidateV2.rights");
2841
+ const match = createPvoxModelMatchAssessmentV2(record.match);
2842
+ assertImmutableAssetVersion(match.ranker.version);
2843
+ assertImmutableAssetVersion(match.ranker.calibrationVersion);
2844
+ const provenance = createModelProvenance(record.provenance);
2845
+ const rights = createModelRightsAssessment(record.rights);
2846
+ assertImmutableAssetVersion(rights.policyVersion);
2847
+ const processingManifest = createModelProcessingManifestV2(record.processingManifest);
2848
+ if (processingManifest.resolutionId !== resolutionId || processingManifest.candidateId !== candidateId || processingManifest.kind !== assetRef.kind || processingManifest.contentHash !== assetRef.contentHash) throw new Error("ModelCandidateV2 processing manifest must match candidate identity and contentHash.");
2849
+ if (match.candidateId !== candidateId || match.candidateContentHash !== assetRef.contentHash) throw new Error("ModelCandidateV2 match must bind the exact candidate content.");
2850
+ if (match.requestSemanticProfileHash !== request.requestSemanticProfileHash) throw new Error("ModelCandidateV2 semantic assessment must bind the complete normalized v2 request profile hash.");
2851
+ if (match.requestSemanticProfileCanonical !== canonicalizeModelRequestSemanticProfileV1(request)) throw new Error("ModelCandidateV2 semantic assessment must bind the exact canonical v2 request profile.");
2852
+ if (match.request.query !== request.query || match.request.revision !== request.revision || match.request.locale !== request.locale || match.request.rankerId !== request.rankerId) throw new Error("ModelCandidateV2 match must bind the exact v2 request semantics and revision.");
2853
+ assertRequestMatchesProcessing(request, processingManifest);
2854
+ if (provenance.contentHash !== processingManifest.pvox.sourceContentHash) throw new Error("ModelCandidateV2 provenance must bind the PVOX sourceContentHash.");
2855
+ if (rights.sourceId !== provenance.sourceId || rights.sourceAssetId !== provenance.sourceAssetId || rights.sourceContentHash !== provenance.contentHash) throw new Error("ModelCandidateV2 rights evidence must bind the exact source.");
2856
+ const resourceScope = parsePvoxResourceScope(processingManifest.pvox.artifact.uri);
2857
+ if (assetRef.disposition === "proposed" && (resourceScope.kind !== "resolution" || resourceScope.resolutionId !== resolutionId || resourceScope.candidateId !== candidateId)) {
2858
+ throw new Error("ModelCandidateV2 proposed PVOX resource must use its resolution/candidate namespace.");
2859
+ }
2860
+ if (assetRef.disposition === "existing" && (resourceScope.kind !== "catalog" || resourceScope.assetId !== assetRef.asset.assetId || resourceScope.version !== assetRef.asset.version)) {
2861
+ throw new Error("ModelCandidateV2 existing asset must prove the same immutable catalog PVOX representation.");
2862
+ }
2863
+ const views = createViews(record.views, resolutionId, candidateId);
2864
+ const renderEvidence = createRenderEvidence(record.renderEvidence);
2865
+ if (
2866
+ renderEvidence.processingManifestId !== processingManifest.manifestId
2867
+ || renderEvidence.processingClosureHash !== processingManifest.processingClosureHash
2868
+ || renderEvidence.pvoxContentHash !== processingManifest.contentHash
2869
+ || renderEvidence.pvoxRootHash !== processingManifest.pvox.rootHash
2870
+ || renderEvidence.pvoxDirectoryHash !== processingManifest.pvox.directoryHash
2871
+ || renderEvidence.pvoxPageSetHash !== processingManifest.pvox.pageSetHash
2872
+ || renderEvidence.viewSha256s.some((digest, index) => digest !== views[index]!.sha256)
2873
+ ) throw new Error("ModelCandidateV2 renderer evidence must bind the PVOX manifest and ordered views.");
2874
+ if (Date.parse(renderEvidence.renderedAt) < Date.parse(processingManifest.processedAt)) throw new Error("ModelCandidateV2 render evidence must follow processing.");
2875
+ const evaluationClosureHash = requireSha256(record.evaluationClosureHash, "ModelCandidateV2.evaluationClosureHash");
2876
+ const evaluationClosureAttestation = createHashAttestation(
2877
+ record.evaluationClosureAttestation,
2878
+ PVOX_EVALUATION_CLOSURE_HASH_DOMAIN,
2879
+ evaluationClosureHash,
2880
+ "ModelCandidateV2.evaluationClosureAttestation",
2881
+ );
2882
+ const gateEntries = asDenseArray(record.hardGates, "ModelCandidateV2.hardGates", PVOX_NON_OVERRIDABLE_GATE_KINDS.length, PVOX_NON_OVERRIDABLE_GATE_KINDS.length);
2883
+ const gates = gateEntries.map(createPvoxGateEvidence);
2884
+ const byGate = new Map(gates.map((entry) => [entry.kind, entry]));
2885
+ if (byGate.size !== PVOX_NON_OVERRIDABLE_GATE_KINDS.length || PVOX_NON_OVERRIDABLE_GATE_KINDS.some((kind) => !byGate.has(kind))) throw new Error("ModelCandidateV2 must contain every non-overridable hard gate exactly once.");
2886
+ const orderedGates = PVOX_NON_OVERRIDABLE_GATE_KINDS.map((kind) => byGate.get(kind)!);
2887
+ const concreteGateHashes: Readonly<Partial<Record<PvoxNonOverridableGateKind, string>>> = {
2888
+ "source-format-validation": processingManifest.converter.evidenceHash,
2889
+ "pvox-validation": processingManifest.pvox.validationEvidenceHash,
2890
+ "fidelity-validation": processingManifest.fidelity.evidenceHash,
2891
+ "physical-property-validation": processingManifest.physicalEvidenceHash,
2892
+ "renderer-validation": renderEvidence.evidenceHash,
2893
+ };
2894
+ for (const gate of orderedGates) {
2895
+ const expectedHash = gate.kind === "malware-scan" || gate.kind === "source-format-validation" ? provenance.contentHash : processingManifest.binaryClosureHash;
2896
+ if (gate.subjectContentHash !== expectedHash) throw new Error(`ModelCandidateV2 ${gate.kind} hard gate binds the wrong subject.`);
2897
+ const concreteHash = concreteGateHashes[gate.kind];
2898
+ if (concreteHash !== undefined && gate.evidenceHash !== concreteHash) throw new Error(`ModelCandidateV2 ${gate.kind} hard gate must bind its concrete evidence hash.`);
2899
+ }
2900
+ const fidelityGate = byGate.get("fidelity-validation")!;
2901
+ if (fidelityGate.outcome !== processingManifest.fidelity.outcome) throw new Error("ModelCandidateV2 fidelity gate outcome must match measured fidelity evidence.");
2902
+ const blockingReasonCodes = [
2903
+ ...(match.assurance === "none" ? ["semantic-assurance-none"] : []),
2904
+ ...(!match.hardConstraintPass ? ["semantic-hard-constraints-failed"] : []),
2905
+ ...(!["allowed", "attribution-required"].includes(rights.status) ? [`rights:${rights.status}`] : []),
2906
+ ...processingManifest.requiredCapabilities.flatMap((capability) => {
2907
+ const evidence = processingManifest.capabilities.find((entry) => entry.capability === capability)!;
2908
+ return evidence.status === "supported" ? [] : [`capability:${capability}:${evidence.status}`];
2909
+ }),
2910
+ ...orderedGates.flatMap((gate) => gate.outcome === "passed" ? [] : gate.reasonCodes.map((reason) => `${gate.kind}:${reason}`)),
2911
+ ];
2912
+ const shouldBeConfirmable = blockingReasonCodes.length === 0;
2913
+ if ((admissionStatus === "confirmable") !== shouldBeConfirmable) throw new Error("ModelCandidateV2 admissionStatus must reflect all rights, semantic, capability, fidelity, and hard-gate blockers.");
2914
+ const common = {
2915
+ contractVersion: MODEL_RESOLUTION_V2_CONTRACT_VERSION,
2916
+ resolutionId,
2917
+ requesterId,
2918
+ candidateId,
2919
+ request,
2920
+ assetRef,
2921
+ match,
2922
+ provenance,
2923
+ rights,
2924
+ processingManifest,
2925
+ views,
2926
+ renderEvidence,
2927
+ evaluationClosureHash,
2928
+ evaluationClosureAttestation,
2929
+ hardGates: orderedGates,
2930
+ };
2931
+ if (admissionStatus === "diagnostic") {
2932
+ if (record.confirmationRequired !== false) throw new Error("Diagnostic ModelCandidateV2 confirmationRequired must be false.");
2933
+ if (record.blockingReasonCodes !== undefined) {
2934
+ const suppliedEntries = asDenseArray(record.blockingReasonCodes, "ModelCandidateV2.blockingReasonCodes", 1, 256);
2935
+ const suppliedBlockingReasons = suppliedEntries.map((entry, index) => requireReasonCode(entry, `ModelCandidateV2.blockingReasonCodes[${index}]`));
2936
+ if (JSON.stringify(suppliedBlockingReasons) !== JSON.stringify(blockingReasonCodes)) throw new Error("Diagnostic ModelCandidateV2 blockingReasonCodes must be derived from its evidence.");
2937
+ }
2938
+ return deepFreeze({ ...common, admissionStatus: "diagnostic", blockingReasonCodes, confirmationRequired: false });
2939
+ }
2940
+ if (record.confirmationRequired !== true) throw new Error("Confirmable ModelCandidateV2 confirmationRequired must be true.");
2941
+ const confirmationBinding = createConfirmationBinding(record.confirmationBinding);
2942
+ const expectedBinding = {
2943
+ resolutionId,
2944
+ requesterId,
2945
+ requestSemanticProfileHash: request.requestSemanticProfileHash,
2946
+ requestRevision: match.request.revision,
2947
+ candidateId,
2948
+ candidateContentHash: assetRef.contentHash,
2949
+ sourceContentHash: provenance.contentHash,
2950
+ binaryClosureHash: processingManifest.binaryClosureHash,
2951
+ pvoxRootHash: processingManifest.pvox.rootHash,
2952
+ pvoxDirectoryHash: processingManifest.pvox.directoryHash,
2953
+ pvoxPageSetHash: processingManifest.pvox.pageSetHash,
2954
+ compilationInputHash: processingManifest.compilationInputHash,
2955
+ runtimeRequestProfileHash: processingManifest.runtimeRequestProfileHash,
2956
+ processingClosureHash: processingManifest.processingClosureHash,
2957
+ assemblyClosureHash: processingManifest.assemblyClosureHash,
2958
+ evaluationClosureHash,
2959
+ capabilityEvidenceSetHash: processingManifest.capabilityEvidenceSetHash,
2960
+ rightsDecisionId: rights.decisionId,
2961
+ fidelityEvidenceHash: processingManifest.fidelity.evidenceHash,
2962
+ physicalEvidenceHash: processingManifest.physicalEvidenceHash,
2963
+ rendererId: renderEvidence.rendererId,
2964
+ rendererVersion: renderEvidence.rendererVersion,
2965
+ rendererEvidenceHash: renderEvidence.evidenceHash,
2966
+ };
2967
+ for (const [key, expected] of Object.entries(expectedBinding)) {
2968
+ if (confirmationBinding[key as keyof typeof confirmationBinding] !== expected) throw new Error(`ModelCandidateV2 confirmationBinding.${key} must match candidate evidence.`);
2969
+ }
2970
+ if (JSON.stringify(confirmationBinding.request) !== JSON.stringify(request)) throw new Error("ModelCandidateV2 confirmationBinding.request must match the exact v2 request.");
2971
+ if (JSON.stringify(confirmationBinding.candidateAssetRef) !== JSON.stringify(assetRef)) throw new Error("ModelCandidateV2 confirmationBinding candidate asset identity must match the exact proposal or catalog asset.");
2972
+ if (JSON.stringify(confirmationBinding.match) !== JSON.stringify(match)) throw new Error("ModelCandidateV2 confirmationBinding must bind the complete semantic match evidence.");
2973
+ if (JSON.stringify(confirmationBinding.provenance) !== JSON.stringify(provenance)) throw new Error("ModelCandidateV2 confirmationBinding must bind the complete provider provenance identity.");
2974
+ if (JSON.stringify(confirmationBinding.rights) !== JSON.stringify(rights)) throw new Error("ModelCandidateV2 confirmationBinding must bind the complete normalized rights decision.");
2975
+ if (JSON.stringify(confirmationBinding.renderEvidence) !== JSON.stringify(renderEvidence)) throw new Error("ModelCandidateV2 confirmationBinding must bind the complete native renderer evidence.");
2976
+ if (JSON.stringify(confirmationBinding.hardGates) !== JSON.stringify(orderedGates)) throw new Error("ModelCandidateV2 confirmationBinding must contain the complete ordered hard-gate evidence set.");
2977
+ if (confirmationBinding.viewSha256s.some((digest, index) => digest !== views[index]!.sha256)) throw new Error("ModelCandidateV2 confirmationBinding view hashes must match candidate views.");
2978
+ const confirmationToken = requireAttestation(record.confirmationToken, "ModelCandidateV2.confirmationToken");
2979
+ if (confirmationBinding.bindingHashAttestation.attestationToken !== confirmationToken) throw new Error("ModelCandidateV2 confirmation token must attest the exact confirmation binding hash.");
2980
+ return deepFreeze({
2981
+ ...common,
2982
+ admissionStatus: "confirmable",
2983
+ confirmationBinding,
2984
+ confirmationToken,
2985
+ confirmationRequired: true,
2986
+ });
2987
+ }
2988
+
2989
+ /** Return whether a fully validated candidate still satisfies every hard gate. */
2990
+ export function isModelCandidateV2Confirmable(candidate: ModelCandidateV2): boolean {
2991
+ return candidate.admissionStatus === "confirmable"
2992
+ && candidate.confirmationRequired
2993
+ && candidate.match.hardConstraintPass
2994
+ && candidate.match.assurance !== "none"
2995
+ && (candidate.rights.status === "allowed" || candidate.rights.status === "attribution-required")
2996
+ && candidate.processingManifest.fidelity.outcome === "passed"
2997
+ && candidate.hardGates.length === PVOX_NON_OVERRIDABLE_GATE_KINDS.length
2998
+ && candidate.hardGates.every(({ outcome }) => outcome === "passed")
2999
+ && candidate.processingManifest.requiredCapabilities.every((name) => candidate.processingManifest.capabilities.find((entry) => entry.capability === name)?.status === "supported");
3000
+ }
3001
+
3002
+ /** Validate one requester confirmation against the exact PVOX review subject. */
3003
+ export function createModelCandidateConfirmationV2(input: unknown, expectedCandidate: ModelCandidateV2, expectedResolutionId: string): ModelCandidateConfirmationV2 {
3004
+ const record = asRecord(input, "ModelCandidateConfirmationV2");
3005
+ exactKeys(record, ["contractVersion", "confirmationId", "resolutionId", "requesterId", "candidateId", "confirmationToken", "confirmationBindingHash", "viewSha256s", "confirmedBy", "confirmedAt", "semanticRiskAccepted"], "ModelCandidateConfirmationV2");
3006
+ if (record.contractVersion !== MODEL_RESOLUTION_V2_CONTRACT_VERSION) throw new Error("ModelCandidateConfirmationV2.contractVersion is unsupported.");
3007
+ const candidate = createModelCandidateV2(expectedCandidate);
3008
+ if (candidate.admissionStatus !== "confirmable") throw new Error("ModelCandidateConfirmationV2 cannot confirm a diagnostic candidate.");
3009
+ const resolutionId = requirePathSegment(expectedResolutionId, "ModelCandidateConfirmationV2 expected resolutionId");
3010
+ if (candidate.resolutionId !== resolutionId || record.resolutionId !== resolutionId || record.requesterId !== candidate.requesterId || record.candidateId !== candidate.candidateId) throw new Error("ModelCandidateConfirmationV2 must match the candidate, requester, and resolution.");
3011
+ if (record.confirmationToken !== candidate.confirmationToken) throw new Error("ModelCandidateConfirmationV2 confirmationToken must match the candidate.");
3012
+ if (record.confirmationBindingHash !== candidate.confirmationBinding.bindingHash) throw new Error("ModelCandidateConfirmationV2 confirmation binding must match the candidate.");
3013
+ const viewSha256s = requireShaTuple(record.viewSha256s, "ModelCandidateConfirmationV2.viewSha256s");
3014
+ if (viewSha256s.some((digest, index) => digest !== candidate.views[index]!.sha256)) throw new Error("ModelCandidateConfirmationV2 view hashes must match candidate evidence.");
3015
+ if (!isModelCandidateV2Confirmable(candidate)) throw new Error("ModelCandidateConfirmationV2 candidate is not confirmable because a hard gate is blocked.");
3016
+ const semanticRiskAccepted = requireBoolean(record.semanticRiskAccepted, "ModelCandidateConfirmationV2.semanticRiskAccepted");
3017
+ if ((candidate.match.assurance === "low") !== semanticRiskAccepted) throw new Error("Only low semantic assurance requires and permits semantic risk acceptance.");
3018
+ const confirmedAt = requireTimestamp(record.confirmedAt, "ModelCandidateConfirmationV2.confirmedAt");
3019
+ const latestEvidence = Math.max(
3020
+ Date.parse(candidate.provenance.capturedAt), Date.parse(candidate.rights.reviewedAt),
3021
+ Date.parse(candidate.processingManifest.processedAt), Date.parse(candidate.renderEvidence.renderedAt),
3022
+ ...candidate.hardGates.map(({ evaluatedAt }) => Date.parse(evaluatedAt)),
3023
+ );
3024
+ if (Date.parse(confirmedAt) < latestEvidence) throw new Error("ModelCandidateConfirmationV2 confirmation must follow all required evidence.");
3025
+ const confirmedBy = requireString(record.confirmedBy, "ModelCandidateConfirmationV2.confirmedBy", 256);
3026
+ if (DIRECT_URL.test(confirmedBy)) throw new Error("ModelCandidateConfirmationV2.confirmedBy must not expose a URL.");
3027
+ if (confirmedBy !== candidate.requesterId) throw new Error("ModelCandidateConfirmationV2 confirmedBy must be the requester bound to the resolution.");
3028
+ return deepFreeze({
3029
+ contractVersion: MODEL_RESOLUTION_V2_CONTRACT_VERSION,
3030
+ confirmationId: requireToken(record.confirmationId, "ModelCandidateConfirmationV2.confirmationId"),
3031
+ resolutionId,
3032
+ requesterId: candidate.requesterId,
3033
+ candidateId: candidate.candidateId,
3034
+ confirmationToken: candidate.confirmationToken,
3035
+ confirmationBindingHash: candidate.confirmationBinding.bindingHash,
3036
+ viewSha256s,
3037
+ confirmedBy,
3038
+ confirmedAt,
3039
+ semanticRiskAccepted,
3040
+ });
3041
+ }
3042
+
3043
+ /** Narrow an unknown state to the additive PVOX lifecycle. */
3044
+ export function isModelResolutionStateV2(value: unknown): value is ModelResolutionStateV2 {
3045
+ return typeof value === "string" && (MODEL_RESOLUTION_V2_STATES as readonly string[]).includes(value);
3046
+ }
3047
+
3048
+ /** Project one v2 state for legacy wrappers without changing v1 state enums. */
3049
+ export function toLegacyModelResolutionState(state: ModelResolutionStateV2): ModelResolutionState {
3050
+ if (!isModelResolutionStateV2(state)) throw new Error("Unknown ModelResolutionStateV2 state.");
3051
+ return MODEL_RESOLUTION_V2_COMPATIBILITY_STATES[state];
3052
+ }
3053
+
3054
+ function candidatesEquivalent(left: ModelCandidateV2, right: ModelCandidateV2): boolean {
3055
+ return JSON.stringify(left) === JSON.stringify(right);
3056
+ }
3057
+
3058
+ function createModelPromotionReceiptV2(
3059
+ input: unknown,
3060
+ resolutionId: string,
3061
+ candidate: ConfirmableModelCandidateV2,
3062
+ confirmation: ModelCandidateConfirmationV2,
3063
+ finalAssetRef: ModelAssetRef,
3064
+ ): ModelPromotionReceiptV2 {
3065
+ if (candidate.assetRef.disposition !== "proposed") throw new Error("ModelPromotionReceiptV2 is only valid for a proposed candidate.");
3066
+ const record = asRecord(input, "ModelPromotionReceiptV2");
3067
+ exactKeys(record, [
3068
+ "contractVersion", "promotionId", "resolutionId", "requesterId", "candidateId", "proposalId", "candidateAssetRef", "confirmationId", "confirmationBindingHash",
3069
+ "processingManifestId", "processingContentHash", "processingClosureHash", "assemblyClosureHash", "assemblyChildClosureHashes", "finalAssetRef", "creditsRecordHash",
3070
+ "catalogRowHash", "indexSnapshotHash", "catalogPointerEtag", "indexPointerEtag", "publicationState",
3071
+ "creditsWrittenAt", "catalogRowWrittenAt", "indexSnapshotWrittenAt", "pointerPublishedAt",
3072
+ "publicationHash", "publicationHashAttestation", "promotedAt", "publicationToken",
3073
+ ], "ModelPromotionReceiptV2");
3074
+ if (record.contractVersion !== MODEL_RESOLUTION_V2_CONTRACT_VERSION || record.publicationState !== "pointer-last-complete") {
3075
+ throw new Error("ModelPromotionReceiptV2 must use the v2 contract and prove pointer-last completion.");
3076
+ }
3077
+ if (record.resolutionId !== resolutionId
3078
+ || record.requesterId !== candidate.requesterId
3079
+ || record.candidateId !== candidate.candidateId
3080
+ || record.proposalId !== candidate.assetRef.proposalId
3081
+ || record.confirmationId !== confirmation.confirmationId
3082
+ || record.confirmationBindingHash !== confirmation.confirmationBindingHash
3083
+ || record.processingManifestId !== candidate.processingManifest.manifestId
3084
+ || record.processingContentHash !== candidate.processingManifest.contentHash
3085
+ || record.processingClosureHash !== candidate.processingManifest.processingClosureHash
3086
+ || record.assemblyClosureHash !== candidate.processingManifest.assemblyClosureHash) {
3087
+ throw new Error("ModelPromotionReceiptV2 must bind the exact proposal, confirmation, processing content, and PVOX assembly closure.");
3088
+ }
3089
+ assertDataOnlyTree(record.candidateAssetRef, "ModelPromotionReceiptV2.candidateAssetRef");
3090
+ const candidateAssetRef = createCandidateAssetRef(record.candidateAssetRef);
3091
+ if (JSON.stringify(candidateAssetRef) !== JSON.stringify(candidate.assetRef)) throw new Error("ModelPromotionReceiptV2 must bind the exact proposed candidate identity.");
3092
+ const childClosureEntries = asDenseArray(record.assemblyChildClosureHashes, "ModelPromotionReceiptV2.assemblyChildClosureHashes", 0, PVOX_DEFAULT_LIMITS.maximumAssemblyChildren);
3093
+ const assemblyChildClosureHashes = childClosureEntries.map((entry, index) => requireSha256(entry, `ModelPromotionReceiptV2.assemblyChildClosureHashes[${index}]`));
3094
+ const expectedChildClosureHashes = candidate.processingManifest.children.map(({ assetRef }) => assetRef.binaryClosureHash);
3095
+ if (JSON.stringify(assemblyChildClosureHashes) !== JSON.stringify(expectedChildClosureHashes)) throw new Error("ModelPromotionReceiptV2 child closure sequence must match the canonical assembly order.");
3096
+ assertDataOnlyTree(record.finalAssetRef, "ModelPromotionReceiptV2.finalAssetRef");
3097
+ const receiptAssetRef = createModelAssetRef(record.finalAssetRef);
3098
+ if (JSON.stringify(receiptAssetRef) !== JSON.stringify(finalAssetRef)) throw new Error("ModelPromotionReceiptV2 finalAssetRef must match the completed resolution.");
3099
+ const creditsWrittenAt = requireTimestamp(record.creditsWrittenAt, "ModelPromotionReceiptV2.creditsWrittenAt");
3100
+ const catalogRowWrittenAt = requireTimestamp(record.catalogRowWrittenAt, "ModelPromotionReceiptV2.catalogRowWrittenAt");
3101
+ const indexSnapshotWrittenAt = requireTimestamp(record.indexSnapshotWrittenAt, "ModelPromotionReceiptV2.indexSnapshotWrittenAt");
3102
+ const pointerPublishedAt = requireTimestamp(record.pointerPublishedAt, "ModelPromotionReceiptV2.pointerPublishedAt");
3103
+ const promotedAt = requireTimestamp(record.promotedAt, "ModelPromotionReceiptV2.promotedAt");
3104
+ const publicationSequence = [confirmation.confirmedAt, creditsWrittenAt, catalogRowWrittenAt, indexSnapshotWrittenAt, pointerPublishedAt, promotedAt].map(Date.parse);
3105
+ if (publicationSequence.some((time, index) => index > 0 && time < publicationSequence[index - 1]!)) {
3106
+ throw new Error("ModelPromotionReceiptV2 must prove credits, catalog row, index snapshot, and pointers were published in pointer-last order.");
3107
+ }
3108
+ const publicationHash = requireSha256(record.publicationHash, "ModelPromotionReceiptV2.publicationHash");
3109
+ const publicationHashAttestation = createHashAttestation(record.publicationHashAttestation, PVOX_PUBLICATION_HASH_DOMAIN, publicationHash, "ModelPromotionReceiptV2.publicationHashAttestation");
3110
+ const publicationToken = requireAttestation(record.publicationToken, "ModelPromotionReceiptV2.publicationToken");
3111
+ if (publicationHashAttestation.attestationToken !== publicationToken) throw new Error("ModelPromotionReceiptV2 publication token must attest the exact publication hash.");
3112
+ return deepFreeze({
3113
+ contractVersion: MODEL_RESOLUTION_V2_CONTRACT_VERSION,
3114
+ promotionId: requireToken(record.promotionId, "ModelPromotionReceiptV2.promotionId"),
3115
+ resolutionId,
3116
+ requesterId: candidate.requesterId,
3117
+ candidateId: candidate.candidateId,
3118
+ proposalId: candidate.assetRef.proposalId,
3119
+ candidateAssetRef,
3120
+ confirmationId: confirmation.confirmationId,
3121
+ confirmationBindingHash: confirmation.confirmationBindingHash,
3122
+ processingManifestId: candidate.processingManifest.manifestId,
3123
+ processingContentHash: candidate.processingManifest.contentHash,
3124
+ processingClosureHash: candidate.processingManifest.processingClosureHash,
3125
+ assemblyClosureHash: candidate.processingManifest.assemblyClosureHash,
3126
+ assemblyChildClosureHashes,
3127
+ finalAssetRef: receiptAssetRef,
3128
+ creditsRecordHash: requireSha256(record.creditsRecordHash, "ModelPromotionReceiptV2.creditsRecordHash"),
3129
+ catalogRowHash: requireSha256(record.catalogRowHash, "ModelPromotionReceiptV2.catalogRowHash"),
3130
+ indexSnapshotHash: requireSha256(record.indexSnapshotHash, "ModelPromotionReceiptV2.indexSnapshotHash"),
3131
+ catalogPointerEtag: requireToken(record.catalogPointerEtag, "ModelPromotionReceiptV2.catalogPointerEtag"),
3132
+ indexPointerEtag: requireToken(record.indexPointerEtag, "ModelPromotionReceiptV2.indexPointerEtag"),
3133
+ publicationState: "pointer-last-complete",
3134
+ creditsWrittenAt,
3135
+ catalogRowWrittenAt,
3136
+ indexSnapshotWrittenAt,
3137
+ pointerPublishedAt,
3138
+ publicationHash,
3139
+ publicationHashAttestation,
3140
+ promotedAt,
3141
+ publicationToken,
3142
+ });
3143
+ }
3144
+
3145
+ function createModelRefinementQuestionV2(input: unknown, index: number): ModelRefinementQuestionV2 {
3146
+ const fieldName = `ModelResolutionV2.refinementQuestions[${index}]`;
3147
+ const record = asRecord(input, fieldName);
3148
+ exactKeys(record, ["questionId", "prompt", "reasonCodes"], fieldName);
3149
+ return deepFreeze({
3150
+ questionId: requireToken(record.questionId, `${fieldName}.questionId`),
3151
+ prompt: requireString(record.prompt, `${fieldName}.prompt`, 500),
3152
+ reasonCodes: requireReasonCodes(record.reasonCodes, `${fieldName}.reasonCodes`),
3153
+ });
3154
+ }
3155
+
3156
+ /** Validate one durable PVOX model-resolution snapshot. */
3157
+ export function createModelResolutionV2(input: unknown): ModelResolutionV2 {
3158
+ const record = asRecord(input, "ModelResolutionV2");
3159
+ exactKeys(record, ["contractVersion", "resolutionId", "requesterId", "request", "attempts", "state", "compatibilityState", "candidates", "bestCandidate", "confirmation", "promotionReceipt", "refinementQuestions", "finalAssetRef", "stateReasonCode", "createdAt", "updatedAt"], "ModelResolutionV2");
3160
+ if (record.contractVersion !== MODEL_RESOLUTION_V2_CONTRACT_VERSION) throw new Error("ModelResolutionV2.contractVersion is unsupported.");
3161
+ const resolutionId = requirePathSegment(record.resolutionId, "ModelResolutionV2.resolutionId");
3162
+ const requesterId = requireToken(record.requesterId, "ModelResolutionV2.requesterId");
3163
+ const request = createModelRequestSpecV2(record.request);
3164
+ const state = requireEnum(record.state, MODEL_RESOLUTION_V2_STATES, "ModelResolutionV2.state");
3165
+ const compatibilityState = toLegacyModelResolutionState(state);
3166
+ if (record.compatibilityState !== undefined && record.compatibilityState !== compatibilityState) throw new Error("ModelResolutionV2 compatibilityState must match its deterministic legacy projection.");
3167
+ const candidateEntries = asDenseArray(record.candidates, "ModelResolutionV2.candidates", 0, 20);
3168
+ const candidates = candidateEntries.map(createModelCandidateV2);
3169
+ if (new Set(candidates.map(({ candidateId }) => candidateId)).size !== candidates.length) throw new Error("ModelResolutionV2 candidate IDs must be unique.");
3170
+ if (candidates.some((candidate) => candidate.resolutionId !== resolutionId || candidate.requesterId !== requesterId || JSON.stringify(candidate.request) !== JSON.stringify(request))) throw new Error("ModelResolutionV2 candidates must bind the exact requester, v2 resolution request, and profiles.");
3171
+ const bestCandidate = record.bestCandidate === undefined ? undefined : createModelCandidateV2(record.bestCandidate);
3172
+ if (bestCandidate !== undefined && !candidates.some((candidate) => candidatesEquivalent(candidate, bestCandidate))) throw new Error("ModelResolutionV2 bestCandidate must be one exact candidate in the resolution.");
3173
+ if (state === "awaiting-confirmation" && (bestCandidate === undefined || !isModelCandidateV2Confirmable(bestCandidate))) throw new Error("ModelResolutionV2 awaiting-confirmation requires one confirmable bestCandidate.");
3174
+ const refinementEntries = asDenseArray(record.refinementQuestions, "ModelResolutionV2.refinementQuestions", 0, 3);
3175
+ const refinementQuestions = refinementEntries.map(createModelRefinementQuestionV2);
3176
+ if (new Set(refinementQuestions.map(({ questionId }) => questionId)).size !== refinementQuestions.length) throw new Error("ModelResolutionV2 refinement question IDs must be unique.");
3177
+ if (state === "awaiting-confirmation" && bestCandidate?.match.assurance === "low" && refinementQuestions.length === 0) throw new Error("Low-assurance ModelResolutionV2 requires refinement questions.");
3178
+ if (state === "completed" && record.finalAssetRef === undefined) throw new Error("ModelResolutionV2 completed state requires finalAssetRef.");
3179
+ const confirmation = record.confirmation === undefined
3180
+ ? undefined
3181
+ : bestCandidate === undefined
3182
+ ? (() => { throw new Error("ModelResolutionV2 confirmation requires bestCandidate."); })()
3183
+ : createModelCandidateConfirmationV2(record.confirmation, bestCandidate, resolutionId);
3184
+ if (["promoting", "completed"].includes(state) && confirmation === undefined) throw new Error(`ModelResolutionV2 ${state} requires confirmation.`);
3185
+ if (!["promoting", "completed"].includes(state) && confirmation !== undefined) throw new Error("ModelResolutionV2 confirmation is only allowed while promoting or completed.");
3186
+ if (record.finalAssetRef !== undefined) assertDataOnlyTree(record.finalAssetRef, "ModelResolutionV2.finalAssetRef");
3187
+ const finalAssetRef = record.finalAssetRef === undefined ? undefined : createModelAssetRef(record.finalAssetRef);
3188
+ if (state === "completed" && finalAssetRef === undefined) throw new Error("ModelResolutionV2 completed state requires finalAssetRef.");
3189
+ if (state !== "completed" && finalAssetRef !== undefined) throw new Error("ModelResolutionV2 finalAssetRef is only allowed for completed state.");
3190
+ if (finalAssetRef !== undefined && bestCandidate !== undefined) {
3191
+ if (finalAssetRef.kind !== bestCandidate.assetRef.kind || finalAssetRef.contentHash !== bestCandidate.assetRef.contentHash) {
3192
+ throw new Error("ModelResolutionV2 finalAssetRef must match bestCandidate.");
3193
+ }
3194
+ if (bestCandidate.assetRef.disposition === "existing") {
3195
+ const existingAsset = bestCandidate.assetRef.asset;
3196
+ if (finalAssetRef.assetId !== existingAsset.assetId
3197
+ || finalAssetRef.version !== existingAsset.version
3198
+ || finalAssetRef.runtimeManifestUri !== existingAsset.runtimeManifestUri) {
3199
+ throw new Error("ModelResolutionV2 finalAssetRef must preserve the exact existing immutable asset identity.");
3200
+ }
3201
+ }
3202
+ }
3203
+ let promotionReceipt: ModelPromotionReceiptV2 | undefined;
3204
+ if (record.promotionReceipt !== undefined) {
3205
+ if (state !== "completed" || finalAssetRef === undefined || confirmation === undefined || bestCandidate?.admissionStatus !== "confirmable" || bestCandidate.assetRef.disposition !== "proposed") {
3206
+ throw new Error("ModelResolutionV2 promotionReceipt is only allowed for a completed confirmed proposed candidate.");
3207
+ }
3208
+ promotionReceipt = createModelPromotionReceiptV2(record.promotionReceipt, resolutionId, bestCandidate, confirmation, finalAssetRef);
3209
+ }
3210
+ if (state === "completed" && bestCandidate?.assetRef.disposition === "proposed" && promotionReceipt === undefined) {
3211
+ throw new Error("ModelResolutionV2 completed proposed candidates require a pointer-last promotionReceipt.");
3212
+ }
3213
+ if (bestCandidate?.assetRef.disposition === "existing" && promotionReceipt !== undefined) throw new Error("ModelResolutionV2 existing selections must not include a promotionReceipt.");
3214
+ const stateReasonCode = record.stateReasonCode === undefined ? undefined : requireReasonCode(record.stateReasonCode, "ModelResolutionV2.stateReasonCode");
3215
+ if (["unresolved", "failed", "cancelled"].includes(state) && stateReasonCode === undefined) throw new Error(`ModelResolutionV2 ${state} state requires stateReasonCode.`);
3216
+ if (!["unresolved", "failed", "cancelled"].includes(state) && stateReasonCode !== undefined) throw new Error("ModelResolutionV2 stateReasonCode is reserved for terminal non-success states.");
3217
+ const createdAt = requireTimestamp(record.createdAt, "ModelResolutionV2.createdAt");
3218
+ const updatedAt = requireTimestamp(record.updatedAt, "ModelResolutionV2.updatedAt");
3219
+ if (Date.parse(updatedAt) < Date.parse(createdAt)) throw new Error("ModelResolutionV2.updatedAt must not precede createdAt.");
3220
+ if (confirmation !== undefined && Date.parse(confirmation.confirmedAt) > Date.parse(updatedAt)) {
3221
+ throw new Error("ModelResolutionV2 confirmation confirmedAt must not exceed updatedAt.");
3222
+ }
3223
+ if (promotionReceipt !== undefined && confirmation !== undefined && (Date.parse(promotionReceipt.promotedAt) < Date.parse(confirmation.confirmedAt) || Date.parse(promotionReceipt.promotedAt) > Date.parse(updatedAt))) {
3224
+ throw new Error("ModelResolutionV2 promotionReceipt must follow confirmation and not exceed updatedAt.");
3225
+ }
3226
+ return deepFreeze({
3227
+ contractVersion: MODEL_RESOLUTION_V2_CONTRACT_VERSION,
3228
+ resolutionId,
3229
+ requesterId,
3230
+ request,
3231
+ attempts: requireInteger(record.attempts, "ModelResolutionV2.attempts", 1, 12),
3232
+ state,
3233
+ compatibilityState,
3234
+ candidates,
3235
+ ...(bestCandidate === undefined ? {} : { bestCandidate }),
3236
+ ...(confirmation === undefined ? {} : { confirmation }),
3237
+ ...(promotionReceipt === undefined ? {} : { promotionReceipt }),
3238
+ refinementQuestions,
3239
+ ...(finalAssetRef === undefined ? {} : { finalAssetRef }),
3240
+ ...(stateReasonCode === undefined ? {} : { stateReasonCode }),
3241
+ createdAt,
3242
+ updatedAt,
3243
+ });
3244
+ }