@plasius/asset-contracts 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +114 -2
- package/dist/index.cjs +474 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +131 -2
- package/dist/index.d.ts +131 -2
- package/dist/index.js +453 -87
- package/dist/index.js.map +1 -1
- package/docs/adrs/adr-0011-reloadable-texture-resource-roles.md +92 -0
- package/docs/adrs/adr-0012-model-resource-promotion-closure.md +92 -0
- package/docs/adrs/index.md +2 -0
- package/docs/tdrs/index.md +2 -0
- package/docs/tdrs/tdr-0006-reloadable-texture-resource-roles.md +91 -0
- package/docs/tdrs/tdr-0007-model-resource-promotion-closure.md +63 -0
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/model-resource-promotion.ts +219 -0
- package/src/model-texture-resources.ts +215 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# ADR 0011: Reloadable Texture and Resource Roles
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-10-24
|
|
5
|
+
- Tracking: asset-contracts Task 50; site Feature 1153 / Epic 1147
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Released AssetFileRole already defines model, binary, texture and metadata.
|
|
10
|
+
Task 48 qualifies exact file inventory/bytes, and model-core 0.7.0 owns verified
|
|
11
|
+
resources, material-to-texture references and authored interpretation metadata.
|
|
12
|
+
Neither boundary maps the canonical inventory to storage roles or records
|
|
13
|
+
separate GPU upload/atlas exports while retaining original resource identities.
|
|
14
|
+
Legacy descriptor construction must remain compatible.
|
|
15
|
+
|
|
16
|
+
## Decision
|
|
17
|
+
|
|
18
|
+
Add opt-in async qualifyModelTextureResources. Compose the existing
|
|
19
|
+
qualifyModelResourceManifest and accept only a privately branded model-core
|
|
20
|
+
GpuModelResourceGraph. Reuse canonical materials/textures, including samplers,
|
|
21
|
+
color spaces, usages, transforms, embedding policy and authored sourceMetadata.
|
|
22
|
+
Never reimplement the canonical schemas or parse a model format here.
|
|
23
|
+
|
|
24
|
+
Use the matching module-format core guard through a normal named import. A
|
|
25
|
+
new graph is already immutable/verified. Dynamically importing ESM from the
|
|
26
|
+
CJS build would select a different private WeakSet and reject legitimate CJS
|
|
27
|
+
require-created graphs. This correctness requirement makes lazy loading this
|
|
28
|
+
guard inappropriate. Core remains external; bundlers can remove unused exports
|
|
29
|
+
with sideEffects=false. Existing diagnostic evaluation retains its dynamic
|
|
30
|
+
import. Consumers must produce graphs using the same installed core instance
|
|
31
|
+
and module format as this package (ESM import with ESM, require with CJS).
|
|
32
|
+
|
|
33
|
+
Map canonical mesh entries to model/binary, material/animation to JSON metadata,
|
|
34
|
+
texture to supported image texture, and auxiliary to binary or JSON metadata.
|
|
35
|
+
Existing role values are unchanged. Explicit source bindings match every graph
|
|
36
|
+
resource exactly once. External files match graph path/hash/length/MIME;
|
|
37
|
+
embedded resources retain independent identity and name their owning model
|
|
38
|
+
entrypoint. Do not invent separate embedded archive entries or replace source
|
|
39
|
+
hashes with entrypoint hashes. Preserve original conversion provenance.
|
|
40
|
+
|
|
41
|
+
A derived record has gpu-upload or texture-atlas semantics, a distinct exported
|
|
42
|
+
file path and unique original sourceResourceIds. Uploads use auxiliary/binary
|
|
43
|
+
buffer files; atlases use texture/image files and image sources. Reject missing
|
|
44
|
+
sources, output collisions, source overwrite, unknown roles and incompatible
|
|
45
|
+
MIME declarations. Output is frozen deterministic version-1 JSON with no Blob
|
|
46
|
+
or renderer allocation. Canonical material/texture arrays retain their order.
|
|
47
|
+
|
|
48
|
+
## Limits and trust
|
|
49
|
+
|
|
50
|
+
Snapshot caller metadata and exact bytes before awaiting. Reuse Task 48 path,
|
|
51
|
+
collision, byte/hash and inventory gates. Apply fixed resource/derived/graph,
|
|
52
|
+
metadata value/character/depth and deadline limits. Honor native cancellation;
|
|
53
|
+
use no I/O, retries, mutable registry, arbitrary callbacks or logging. Hashing
|
|
54
|
+
cannot be interrupted inside Web Crypto; checks prevent successful late return.
|
|
55
|
+
Errors expose fixed codes only. Synchronous bounded work is checked before and
|
|
56
|
+
after; it cannot be preempted by a JavaScript timer.
|
|
57
|
+
|
|
58
|
+
Authored sourceMetadata, material names/extensions and original texture paths
|
|
59
|
+
are deliberately preserved trusted package content. The output is NOT a
|
|
60
|
+
public-safe diagnostics response. Adapters must minimize/sanitize authored
|
|
61
|
+
metadata before public distribution; use Task 49's separate diagnostic
|
|
62
|
+
projection for public technical messages. Core-brand validation cannot be
|
|
63
|
+
replaced by shape checks or serialization; reloaded canonical payloads must be
|
|
64
|
+
verified again by core before a new graph is created.
|
|
65
|
+
|
|
66
|
+
Embedded ownership, internal model references and derivation algorithms remain
|
|
67
|
+
trusted adapter assertions. Exact byte qualification does not prove semantic
|
|
68
|
+
format validity, inspect derived image signatures, verify a GPU upload layout,
|
|
69
|
+
attest an atlas algorithm, or authorize promotion. Tasks 12/23/51 retain
|
|
70
|
+
animation metadata, shader ABI and promotion closure. No existing approval gate
|
|
71
|
+
changes. No dependency/version/peer/override changes are required.
|
|
72
|
+
|
|
73
|
+
## Alternatives, rollout and delivery
|
|
74
|
+
|
|
75
|
+
Adding storage role enums would duplicate existing roles and break closed
|
|
76
|
+
consumers. Tightening legacy factories would break staged construction.
|
|
77
|
+
Flattening derived output over authored textures would lose reload fidelity.
|
|
78
|
+
Copying core schemas or accepting a structural graph would discard verification.
|
|
79
|
+
|
|
80
|
+
The backend evaluator for gpu.model.conversion.enabled controls consumer
|
|
81
|
+
adoption. Enabled callers opt into this qualifier; disabled/rollback callers
|
|
82
|
+
retain existing factories. The library performs no flag lookup/mutation. No UI
|
|
83
|
+
or entitlement capability applies. Test both branches and rollback.
|
|
84
|
+
|
|
85
|
+
Require requirements-first round-trip/contract tests, malformed input/role/MIME
|
|
86
|
+
and missing/duplicate identity cases, deterministic/frozen output, cancellation,
|
|
87
|
+
deadline and bounded metadata tests. Run Node 24 frozen install, full tests and
|
|
88
|
+
privacy, coverage >=80% and changed-source LCOV, lint/types/build, audits/tree,
|
|
89
|
+
privacy/pack and packed/public ESM/CJS/types/browser consumers. Release additive
|
|
90
|
+
minor through source PR, exact-main push CI and existing two-phase
|
|
91
|
+
cd.yml/main/production hosted OIDC governance (ADR 0007), then verify registry
|
|
92
|
+
integrity/provenance. Site adoption/deployment remains separate.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# ADR 0012: Model Resource Promotion Closure
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-10-24
|
|
5
|
+
- Tracking: asset-contracts Task 51; site Feature 1153 / Epic 1147
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Released resource and texture qualifiers verify complete inventories and bytes,
|
|
10
|
+
retain original resource identities and distinguish derived output. Conversion
|
|
11
|
+
qualification evaluates raw canonical diagnostics. Typed GPU promotion already
|
|
12
|
+
checks bytes, exact versions and GPU compatibility envelopes. ModelCandidate
|
|
13
|
+
and its confirmation already enforce rights, security, fidelity, render and
|
|
14
|
+
human gates. Published asset-pipeline owns orchestration. Missing composition
|
|
15
|
+
must not create an alternate approval route or duplicate those contracts.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
Add opt-in createModelResourcePromotionRecord. Supply a typed model promotion,
|
|
20
|
+
complete source inventory/bytes, existing output texture qualification inputs,
|
|
21
|
+
raw diagnostic input, exact resource-to-package/path bindings and the existing
|
|
22
|
+
candidate/confirmation. Reuse all existing factories and qualifiers. Snapshot
|
|
23
|
+
inert metadata and both byte sets before awaiting; retain only frozen metadata
|
|
24
|
+
in output, never bytes, Blobs or GPU objects. No dependency update is required.
|
|
25
|
+
|
|
26
|
+
Qualify both complete packages, exact immutable distinct source/output
|
|
27
|
+
identities, graph and candidate converter identity/version/source format, source
|
|
28
|
+
entrypoint digest and converted output digest. Require re-evaluated diagnostic
|
|
29
|
+
and loss arrays to equal candidate evidence. Bind every projected diagnostic
|
|
30
|
+
resource and candidate LOD/collision/fidelity resource to a qualified file's
|
|
31
|
+
path/hash/length/MIME; runtime resources use the output package and LOD0 names
|
|
32
|
+
its entrypoint. Existing catalog candidates also match target asset/version.
|
|
33
|
+
Explicit bindings avoid deriving trust from opaque URI strings.
|
|
34
|
+
|
|
35
|
+
Require a passed review without blocking findings; use the existing candidate
|
|
36
|
+
confirmation factory for rights/security/fidelity/render/human policy. Reuse
|
|
37
|
+
createGpuAssetPromotionRecord for the exact copied output bytes and typed GPU
|
|
38
|
+
envelope. Bind the HTTPS runtime URI suffix to assetId/version/entrypoint;
|
|
39
|
+
existing credential/query/fragment restrictions still apply. These are local
|
|
40
|
+
contract checks. Hosted services must authenticate approvers, verify signed
|
|
41
|
+
tokens and validate evidence issuers; this library grants no approval authority.
|
|
42
|
+
|
|
43
|
+
The separate package closureHash is SHA-256 over existing canonical GPU JSON
|
|
44
|
+
serialization of version, qualified source/output, evaluated diagnostics and
|
|
45
|
+
sorted resource bindings. It includes all file descriptors, original/derived
|
|
46
|
+
lineage and authored reload metadata. It excludes promotion events and
|
|
47
|
+
candidate/confirmation tokens so renewed approval does not change package
|
|
48
|
+
identity. Preserve existing leaf/assembly processing closureHash semantics.
|
|
49
|
+
|
|
50
|
+
Rollback requires locally qualified prior/current results through a private
|
|
51
|
+
WeakSet, then requalifies target bytes and all independent gates. Compare exact
|
|
52
|
+
prior package closure, manifest and runtime URI; enforce same asset/channel,
|
|
53
|
+
distinct current/target versions and rollbackOfVersion=current.version. Emit
|
|
54
|
+
both exact closure identities. No version, historical record or runtime pointer
|
|
55
|
+
is mutated. Forward calls reject rollback metadata. JSON clones are not local
|
|
56
|
+
qualification: reconstruct and requalify persisted inputs before use. The
|
|
57
|
+
WeakSet is process/module-instance evidence only, not a durable signature or
|
|
58
|
+
trusted history database. Hosted storage owns create-only immutable writes,
|
|
59
|
+
verified historical records, concurrency control and pointer-last transactions.
|
|
60
|
+
|
|
61
|
+
## NFRs, alternatives and limits
|
|
62
|
+
|
|
63
|
+
Fixed inert metadata/count/character/depth and byte ceilings, sequential package
|
|
64
|
+
qualification, native abort/default 10s deadline (1–60s), bounded underlying
|
|
65
|
+
hashing, no network/retries/logging. A digest cannot be preempted; phase checks
|
|
66
|
+
prevent late successful return. Normalize even hostile thrown proxies to fixed
|
|
67
|
+
value-free errors. Authored package metadata and candidate content remain
|
|
68
|
+
trusted data requiring privacy minimization; only diagnostic projections are
|
|
69
|
+
public-safe. No raw forensic diagnostics or provider strings are retained.
|
|
70
|
+
|
|
71
|
+
Tightening legacy factories would break staged construction. Copying pipeline
|
|
72
|
+
or candidate policy would drift from released owners. Accepting structural or
|
|
73
|
+
serialized history as qualified would lose the local byte-check evidence.
|
|
74
|
+
Persisting bytes in a mutable qualification registry would add memory and
|
|
75
|
+
lifecycle ownership; use bounded ephemeral copies and weak membership instead.
|
|
76
|
+
|
|
77
|
+
This boundary does not parse internal model references, prove embedding or
|
|
78
|
+
atlas algorithms, revalidate shader ABI policy (Task 23), supply animation
|
|
79
|
+
schemas (Task 12), verify external signatures, or perform storage/promotion.
|
|
80
|
+
Existing exact immutable assembly child refs stay in candidate evidence; their
|
|
81
|
+
separate catalog packages remain hosted resolver responsibilities.
|
|
82
|
+
|
|
83
|
+
Backend evaluator gpu.model.conversion.enabled controls adoption. Enabled
|
|
84
|
+
callers use the opt-in composition; disabled/rollback adoption uses legacy
|
|
85
|
+
factories. No flag service I/O/mutation or UI capability belongs in this library.
|
|
86
|
+
Requirements-first tests cover complete real glTF/GLB fixtures, byte drift,
|
|
87
|
+
evidence/resource/gate drift, exact rollback, deterministic hashes, immutable
|
|
88
|
+
snapshots and malformed/limit/cancel/deadline cases. Qualify packed and public
|
|
89
|
+
ESM/CJS/types/browser consumers and released asset-pipeline orchestration.
|
|
90
|
+
Release additive minor through existing source PR/exact-main push CI and
|
|
91
|
+
two-phase cd.yml/main/production hosted OIDC path, then verify registry
|
|
92
|
+
integrity/provenance before Done. No site deployment is authorized by Task 51.
|
package/docs/adrs/index.md
CHANGED
|
@@ -10,3 +10,5 @@
|
|
|
10
10
|
- [ADR 0008: Package-Owned Issue Tracking](./adr-0008-package-owned-issue-tracking.md)
|
|
11
11
|
- [ADR 0009: Additive Model Resource Manifest Qualification](./adr-0009-model-resource-manifest-qualification.md)
|
|
12
12
|
- [ADR 0010: Public Conversion Diagnostics](./adr-0010-public-conversion-diagnostics.md)
|
|
13
|
+
- [ADR 0011: Reloadable Texture and Resource Roles](./adr-0011-reloadable-texture-resource-roles.md)
|
|
14
|
+
- [ADR 0012: Model Resource Promotion Closure](./adr-0012-model-resource-promotion-closure.md)
|
package/docs/tdrs/index.md
CHANGED
|
@@ -5,3 +5,5 @@
|
|
|
5
5
|
- [TDR 0003: PVOX Validation and Confirmation Binding](./tdr-0003-pvox-validation-and-confirmation-binding.md)
|
|
6
6
|
- [TDR 0004: Model Resource Manifest Qualification](./tdr-0004-model-resource-manifest-qualification.md)
|
|
7
7
|
- [TDR 0005: Public Conversion Diagnostics](./tdr-0005-public-conversion-diagnostics.md)
|
|
8
|
+
- [TDR 0006: Reloadable Texture and Resource Qualification](./tdr-0006-reloadable-texture-resource-roles.md)
|
|
9
|
+
- [TDR 0007: Model Resource Promotion Closure](./tdr-0007-model-resource-promotion-closure.md)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# TDR 0006: Reloadable Texture and Resource Qualification
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-10-24
|
|
5
|
+
- Tracking: asset-contracts Task 50; site Feature 1153
|
|
6
|
+
- Decision: ADR 0011
|
|
7
|
+
|
|
8
|
+
## Sequence and role policy
|
|
9
|
+
|
|
10
|
+
1. Read inert root/options records without executing getters. Verify the graph
|
|
11
|
+
with the matching ESM/CJS core instance's private brand, not a shape cast.
|
|
12
|
+
2. Snapshot inventory, resource bindings and derived records with shared fixed
|
|
13
|
+
budgets before awaiting. Reject unknown fields, accessors, symbols, prototype
|
|
14
|
+
keys, sparse/exotic arrays, non-finite values and malformed IDs.
|
|
15
|
+
3. Invoke existing qualifyModelResourceManifest, which synchronously snapshots
|
|
16
|
+
manifest metadata and all bounded exact byte payloads before its first hash.
|
|
17
|
+
Race the operation against native cancellation/deadline and clean listeners.
|
|
18
|
+
4. Map every inventory entry to an existing storage role and MIME policy:
|
|
19
|
+
|
|
20
|
+
| Canonical role | Storage role | MIME policy |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| mesh | model | model/*, application/json, application/octet-stream, text/plain |
|
|
23
|
+
| mesh | binary | application/octet-stream, application/vnd.plasius.gpu-buffer |
|
|
24
|
+
| material or animation | metadata | application/json |
|
|
25
|
+
| texture | texture | image/png, image/jpeg, image/webp, image/ktx2 |
|
|
26
|
+
| auxiliary | binary or metadata | buffer MIME for binary; application/json for metadata |
|
|
27
|
+
|
|
28
|
+
5. Require exactly one binding per graph resource. For external resources,
|
|
29
|
+
compare exact graph path, verified content hash/length/MIME and appropriate
|
|
30
|
+
image/buffer role. Embedded resources bind only the model entrypoint and
|
|
31
|
+
retain their independent hash/length/MIME rather than the container's hash.
|
|
32
|
+
6. Validate each derived output's distinct existing file and original source
|
|
33
|
+
IDs. gpu-upload requires auxiliary/binary with buffer MIME. texture-atlas
|
|
34
|
+
requires texture with image MIME and image sources. Never overwrite a bound
|
|
35
|
+
source file or reassign a source identity to derived bytes. Multiple authored
|
|
36
|
+
texture interpretations may share one original image resource.
|
|
37
|
+
7. Bound canonical authored material/texture/provenance metadata, retain its
|
|
38
|
+
already immutable values and emit JSON without Blob payloads. Sort resource
|
|
39
|
+
bindings by resourceId, derivatives by path and source-ID sets by UTF-16
|
|
40
|
+
code units. Preserve canonical array order and original provenance.
|
|
41
|
+
|
|
42
|
+
This is declarative role qualification, not a parser, MIME sniffer, GPU-layout
|
|
43
|
+
validator or promotion attestation. Derived records are optional (empty array);
|
|
44
|
+
caller-declared derivation/embedding is not cryptographic algorithm evidence.
|
|
45
|
+
|
|
46
|
+
## Fixed limits and errors
|
|
47
|
+
|
|
48
|
+
Resource bindings: 16,384; derived records: 4,096; core graph nodes: 65,536.
|
|
49
|
+
Shared metadata budgets: 262,144 values, 2,097,152 UTF-16 characters, depth 16,
|
|
50
|
+
2,048 characters/string and 128/key; arrays at most 16,384. IDs use 128-character
|
|
51
|
+
canonical tokens. The existing byte qualifier also applies its own limits:
|
|
52
|
+
16,384 files, 64 MiB/file, 128 MiB total and portable-path rules. Default deadline
|
|
53
|
+
10 seconds, configurable integer 1–60,000 ms. Cancellation/timer cover the
|
|
54
|
+
async boundary and checks surround bounded synchronous phases. Web Crypto work
|
|
55
|
+
cannot be interrupted but never yields a successful result after cancellation
|
|
56
|
+
or expiry. Work is sequential and introduces no retry or storage/network I/O.
|
|
57
|
+
|
|
58
|
+
ModelTextureResourceError exposes invalid-input, limit-exceeded,
|
|
59
|
+
unverified-graph, package-mismatch, resource-mismatch, role-mismatch, aborted or
|
|
60
|
+
deadline-exceeded. Upstream errors are normalized without caller values.
|
|
61
|
+
|
|
62
|
+
## Verification, rollout and limitations
|
|
63
|
+
|
|
64
|
+
Synthetic fixtures independently verify core image/buffer bytes; canonical
|
|
65
|
+
material references require a real UV accessor. Compare external hashes and
|
|
66
|
+
reloaded material JSON with core metadata; serialize and reconstruct canonical
|
|
67
|
+
materials/textures, then verify core accepts the reloaded document. An embedded
|
|
68
|
+
image fixture uses an actual data URI without a fictitious exported source
|
|
69
|
+
image. Derivative hashes differ from the source identity. Test duplicate/missing
|
|
70
|
+
resources, role/MIME drift, unknown roles, graph clone rejection, multiple
|
|
71
|
+
interpretations, getters/proxies, mutation isolation, limits, deadline/abort and
|
|
72
|
+
legacy enabled/disabled/rollback composition. Qualify both typed model and
|
|
73
|
+
legacy envelopes; do not modify shader ABI or animation schemas.
|
|
74
|
+
|
|
75
|
+
Trusted authored sourceMetadata is intentionally preserved; callers own privacy
|
|
76
|
+
minimization before distribution. Public diagnostics use the separate Task 49
|
|
77
|
+
projection. UI accessibility/SEO/browser UI and durable storage controls are N/A
|
|
78
|
+
here. Security/privacy: value-free failures, bounded inert input and exact bytes.
|
|
79
|
+
Performance/reliability: fixed budgets, sequential hashing, no GPU allocation,
|
|
80
|
+
I/O or retry. Maintainability/testability: existing schemas/qualifier reuse and
|
|
81
|
+
immutable deterministic JSON. Compatibility: additive API, unchanged legacy
|
|
82
|
+
factories, dependency versions and existing approval gates.
|
|
83
|
+
|
|
84
|
+
The source-of-truth backend evaluator gpu.model.conversion.enabled controls
|
|
85
|
+
adoption. Disable/rollback retains legacy factories; no service call or mutation
|
|
86
|
+
inside this package and capability N/A. Normal module-format core guard import
|
|
87
|
+
preserves private brand identity across ESM/CJS. Verify packed/public ESM/CJS
|
|
88
|
+
consumers with their matching core entry and browser bundling/execution. Release
|
|
89
|
+
through existing PR/exact-main CI/cd.yml/production hosted OIDC route, verify
|
|
90
|
+
public registry integrity/provenance and dependency signatures. No site release
|
|
91
|
+
or backend secretref mutation is authorized by this Task.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# TDR 0007: Model Resource Promotion Qualification
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-10-24
|
|
5
|
+
- Tracking: asset-contracts Task 51; site Feature 1153
|
|
6
|
+
- Decision: ADR 0012
|
|
7
|
+
|
|
8
|
+
1. Read inert root/options records; reject getters, symbols, exotic/sparse
|
|
9
|
+
arrays, prototype keys and unknown fields. Snapshot all metadata, validate
|
|
10
|
+
existing candidate and confirmation, and copy both bounded byte inventories
|
|
11
|
+
before awaiting. Keep the original privately verified core graph.
|
|
12
|
+
2. Qualify source inventory/bytes, then output graph/resources/derived metadata,
|
|
13
|
+
then re-evaluate raw canonical diagnostics with the released evaluator.
|
|
14
|
+
3. Match immutable distinct asset/version identities; typed output manifest and
|
|
15
|
+
promotion manifest are equal with files sorted by path. Match graph and
|
|
16
|
+
candidate converter ID/version/source format, exact source and output hashes,
|
|
17
|
+
source provenance and diagnostics/losses. A warning is allowed only when the
|
|
18
|
+
candidate carries the same freshly evaluated warning; unsupported/failed or
|
|
19
|
+
unconsented loss never grants promotion.
|
|
20
|
+
4. Validate unique explicit URI bindings to source/output file descriptors.
|
|
21
|
+
Compare every runtime LOD/collision/fidelity resource and projected diagnostic
|
|
22
|
+
resource exactly. LOD0 must bind the output entrypoint. Core still owns
|
|
23
|
+
embedded resources and original/derived graph relationships; no parser here.
|
|
24
|
+
5. Require review passed with no blocking findings, existing candidate gates
|
|
25
|
+
and exact confirmation. Check typed GPU manifest equality, ABI envelopes and
|
|
26
|
+
canonical JSON file bytes through createGpuAssetPromotionRecord. Runtime URI
|
|
27
|
+
must end in /assetId/version/entrypoint and satisfy existing immutable HTTPS
|
|
28
|
+
policy. Hosted verifiers still authenticate signatures and approvers.
|
|
29
|
+
6. SHA-256 canonicalize version/source/output/diagnostics/sorted bindings. Leave
|
|
30
|
+
events, candidate and confirmation out of package identity, allowing fresh
|
|
31
|
+
approval for the exact prior package. Retain candidate/confirmation alongside
|
|
32
|
+
the closure for callers' independent evidence checks.
|
|
33
|
+
7. For rolled-back outcome require private local membership for both prior and
|
|
34
|
+
current; exact prior closure/manifest/runtime URI, same asset/channel, target
|
|
35
|
+
prior version and rollbackOfVersion=current version. Emit from/to exact
|
|
36
|
+
closure identities and never mutate history. Forward calls reject history.
|
|
37
|
+
Persisted JSON must be reconstructed and qualified again; storage owns
|
|
38
|
+
authenticated historical admission, immutable create-only writes and CAS.
|
|
39
|
+
|
|
40
|
+
Limits: 1,024 public resource bindings; 262,144 metadata values; 2,097,152 UTF-16
|
|
41
|
+
characters; depth 24; 2,048 characters/string and 128/key. Existing byte ceilings
|
|
42
|
+
apply independently to source and output (16,384 files, 64 MiB/file,128 MiB per
|
|
43
|
+
package). Copies are bounded and ephemeral; upstream qualifiers also copy
|
|
44
|
+
payloads. No retained byte cache or mutable durable registry. Package phases
|
|
45
|
+
are sequential; the final existing typed factory retains its bounded file-set
|
|
46
|
+
hashing behavior. Default deadline 10s, configured integer1–60,000ms; cancellation
|
|
47
|
+
and timer race cover async work, phase checks cover bounded synchronous work.
|
|
48
|
+
No retry/I/O or logging. Value-free errors normalize hostile upstream proxies.
|
|
49
|
+
|
|
50
|
+
NFR evidence: requirements-based real GLB header/JSON and source fixtures,
|
|
51
|
+
exact hashes/bytes, positive warnings, stale diagnostics, missing/tampered
|
|
52
|
+
resources, alias/identity/URI drift, review/rights/security/GPU/human gates,
|
|
53
|
+
brand and rollback mismatch, fresh confirmation, deterministic inventory order,
|
|
54
|
+
copy/freeze isolation, malformed/limits/deadlines/cancel and flag adoption tests.
|
|
55
|
+
Run full Node24 frozen tests/privacy/coverage>=80% with all changed sources in
|
|
56
|
+
LCOV, types/lint/build/tree/full/runtime audits/privacy/pack/drypublish. Isolated
|
|
57
|
+
packed/public ESM/CJS/types/browser consumers use released asset-pipeline to
|
|
58
|
+
verify state transitions still require review and approval. Accessibility/SEO
|
|
59
|
+
N/A library; trusted metadata requires caller minimization; observability uses
|
|
60
|
+
stable codes without logging. Existing package boundaries/API remain stable.
|
|
61
|
+
Remote flag gpu.model.conversion.enabled uses backend evaluator; capability N/A.
|
|
62
|
+
Release via approved cd.yml/main/production/OIDC with exact CI and public
|
|
63
|
+
registry/provenance/consumer verification. No site/backend secretref change.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plasius/asset-contracts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Canonical contracts for Plasius asset jobs, model resolution, immutable WGSL shader assets, reviews, and promotion records.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,8 @@ export * from "./pvox-hash-preimage.js";
|
|
|
39
39
|
export * from "./pvox-model-resolution.js";
|
|
40
40
|
export * from "./pvox-model-resolution-schema.js";
|
|
41
41
|
export * from "./model-resource-manifest.js";
|
|
42
|
+
export * from "./model-texture-resources.js";
|
|
43
|
+
export * from "./model-resource-promotion.js";
|
|
42
44
|
export * from "./model-conversion-diagnostics.js";
|
|
43
45
|
export {
|
|
44
46
|
GPU_SHADER_STORE_FEATURE_FLAG,
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { canonicalizeGpuContract, computeSha256 } from "@plasius/gpu-shader";
|
|
2
|
+
import { assertImmutableAssetVersion } from "./asset-version.js";
|
|
3
|
+
import { qualifyModelResourceManifest, MODEL_RESOURCE_MANIFEST_LIMITS, type ModelResourceManifestQualificationInput, type ModelResourceFileInput } from "./model-resource-manifest.js";
|
|
4
|
+
import { qualifyModelTextureResources, type ModelTextureResourceQualificationInput, type QualifiedModelTextureResources } from "./model-texture-resources.js";
|
|
5
|
+
import { qualifyModelConversionDiagnostics, type ModelConversionDiagnosticsInput, type QualifiedModelConversionDiagnostics } from "./model-conversion-diagnostics.js";
|
|
6
|
+
import { createModelCandidate, createModelCandidateConfirmation, createModelResourceRef, type ModelCandidate, type ModelCandidateConfirmation, type ModelResourceRef } from "./model-resolution.js";
|
|
7
|
+
import { createModelAssetManifest, createGpuAssetPromotionRecord, type AssetManifest, type AssetPromotionRecord, type ModelAssetManifest } from "./index.js";
|
|
8
|
+
|
|
9
|
+
/** Hard ceilings supplement the existing per-package byte and graph limits. */
|
|
10
|
+
export const MODEL_RESOURCE_PROMOTION_LIMITS = Object.freeze({ bindings: 1024, values: 262_144, characters: 2_097_152, depth: 24, timeoutMs: 60_000 });
|
|
11
|
+
/** Explicit location of a public evidence resource inside one qualified package. */
|
|
12
|
+
export interface ModelPromotionResourceBinding {
|
|
13
|
+
readonly resource: ModelResourceRef;
|
|
14
|
+
readonly package: "source" | "output";
|
|
15
|
+
readonly filePath: string;
|
|
16
|
+
}
|
|
17
|
+
/** Existing contracts composed without granting durable storage or signature authority. */
|
|
18
|
+
export interface ModelResourcePromotionInput {
|
|
19
|
+
readonly promotion: AssetPromotionRecord<ModelAssetManifest>;
|
|
20
|
+
readonly source: Omit<ModelResourceManifestQualificationInput, "signal" | "timeoutMs">;
|
|
21
|
+
readonly output: ModelTextureResourceQualificationInput;
|
|
22
|
+
readonly diagnostics: ModelConversionDiagnosticsInput;
|
|
23
|
+
readonly resourceBindings: readonly ModelPromotionResourceBinding[];
|
|
24
|
+
readonly candidate: ModelCandidate;
|
|
25
|
+
readonly confirmation: ModelCandidateConfirmation;
|
|
26
|
+
readonly rollback?: { readonly prior: QualifiedModelResourcePromotion; readonly current: QualifiedModelResourcePromotion };
|
|
27
|
+
}
|
|
28
|
+
/** Native controls cover the entire composition, with no I/O, retries or pointer writes. */
|
|
29
|
+
export interface ModelResourcePromotionOptions { readonly signal?: AbortSignal; readonly timeoutMs?: number }
|
|
30
|
+
/** Frozen call-time evidence. Requalify persisted JSON; a clone has no local qualification brand. */
|
|
31
|
+
export interface QualifiedModelResourcePromotion {
|
|
32
|
+
readonly contractVersion: 1;
|
|
33
|
+
readonly promotion: AssetPromotionRecord<ModelAssetManifest>;
|
|
34
|
+
readonly source: AssetManifest;
|
|
35
|
+
readonly output: QualifiedModelTextureResources;
|
|
36
|
+
readonly diagnostics: QualifiedModelConversionDiagnostics;
|
|
37
|
+
readonly resourceBindings: readonly ModelPromotionResourceBinding[];
|
|
38
|
+
readonly candidate: ModelCandidate;
|
|
39
|
+
readonly confirmation: ModelCandidateConfirmation;
|
|
40
|
+
/** Separate package closure digest; preserves legacy candidate leaf/assembly hash semantics. */
|
|
41
|
+
readonly closureHash: string;
|
|
42
|
+
readonly rollback?: { readonly fromVersion: string; readonly fromClosureHash: string; readonly toVersion: string; readonly toClosureHash: string };
|
|
43
|
+
}
|
|
44
|
+
/** Stable value-free failure; upstream exceptions and authored values are never included. */
|
|
45
|
+
export class ModelResourcePromotionError extends TypeError {
|
|
46
|
+
constructor(public readonly code: "invalid-input" | "limit-exceeded" | "package-mismatch" | "evidence-mismatch" | "gate-blocked" | "rollback-mismatch" | "aborted" | "deadline-exceeded") {
|
|
47
|
+
super(`Invalid model resource promotion: ${code}`); this.name = "ModelResourcePromotionError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const qualified = new WeakSet<object>();
|
|
51
|
+
function fail(code: ModelResourcePromotionError["code"] = "invalid-input"): never { throw new ModelResourcePromotionError(code); }
|
|
52
|
+
function fields(value: unknown, allowed?: readonly string[]): Record<string, unknown> {
|
|
53
|
+
if (!value || typeof value !== "object" || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) fail();
|
|
54
|
+
const keys = Reflect.ownKeys(value); if (keys.length > 1024) fail("limit-exceeded");
|
|
55
|
+
const result: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
56
|
+
for (const key of keys) {
|
|
57
|
+
if (typeof key !== "string" || key.length > 128 || ["__proto__", "constructor", "prototype"].includes(key) || (allowed && !allowed.includes(key))) fail();
|
|
58
|
+
const property = Object.getOwnPropertyDescriptor(value, key)!;
|
|
59
|
+
if (!("value" in property) || !property.enumerable) fail();
|
|
60
|
+
result[key] = property.value;
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
function array(value: unknown, maximum: number): unknown[] {
|
|
65
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) fail();
|
|
66
|
+
if (value.length > maximum) fail("limit-exceeded");
|
|
67
|
+
if (Reflect.ownKeys(value).length !== value.length + 1) fail();
|
|
68
|
+
return Array.from({ length: value.length }, (_, i) => {
|
|
69
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(i));
|
|
70
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) fail();
|
|
71
|
+
return descriptor.value;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function snapshot(value: unknown, budget: { values: number; characters: number }, depth = 0): unknown {
|
|
75
|
+
if (--budget.values < 0 || depth > MODEL_RESOURCE_PROMOTION_LIMITS.depth) fail("limit-exceeded");
|
|
76
|
+
if (value === null || value === undefined || typeof value === "boolean") return value;
|
|
77
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
78
|
+
if (typeof value === "string") {
|
|
79
|
+
budget.characters -= value.length;
|
|
80
|
+
if (value.length > 2048 || budget.characters < 0) fail("limit-exceeded"); return value;
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(value)) return Object.freeze(array(value, MODEL_RESOURCE_MANIFEST_LIMITS.files).map((v) => snapshot(v, budget, depth + 1)));
|
|
83
|
+
const result = fields(value);
|
|
84
|
+
for (const key of Object.keys(result)) {
|
|
85
|
+
budget.characters -= key.length; if (budget.characters < 0) fail("limit-exceeded");
|
|
86
|
+
result[key] = snapshot(result[key], budget, depth + 1);
|
|
87
|
+
}
|
|
88
|
+
return Object.freeze(result);
|
|
89
|
+
}
|
|
90
|
+
const typedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype);
|
|
91
|
+
const byteLengthGetter = Object.getOwnPropertyDescriptor(typedArrayPrototype, "byteLength")!.get!;
|
|
92
|
+
const bufferGetter = Object.getOwnPropertyDescriptor(typedArrayPrototype, "buffer")!.get!;
|
|
93
|
+
function copyFiles(value: unknown): readonly ModelResourceFileInput[] {
|
|
94
|
+
let total = 0;
|
|
95
|
+
const files = array(value, MODEL_RESOURCE_MANIFEST_LIMITS.files).map((v) => {
|
|
96
|
+
const file = fields(v, ["path", "bytes", "contentType"]); const bytes = file.bytes;
|
|
97
|
+
if (typeof file.path !== "string" || typeof file.contentType !== "string" || !(bytes instanceof Uint8Array)
|
|
98
|
+
|| Object.getPrototypeOf(bytes) !== Uint8Array.prototype || !(bufferGetter.call(bytes) instanceof ArrayBuffer)) fail();
|
|
99
|
+
const length = byteLengthGetter.call(bytes) as number;
|
|
100
|
+
total += length;
|
|
101
|
+
if (length > MODEL_RESOURCE_MANIFEST_LIMITS.fileBytes || total > MODEL_RESOURCE_MANIFEST_LIMITS.totalBytes) fail("limit-exceeded");
|
|
102
|
+
return { path: file.path, bytes, contentType: file.contentType };
|
|
103
|
+
});
|
|
104
|
+
return files.map((f) => ({ ...f, bytes: new Uint8Array(f.bytes) }));
|
|
105
|
+
}
|
|
106
|
+
const same = (a: unknown, b: unknown): boolean => canonicalizeGpuContract(a) === canonicalizeGpuContract(b);
|
|
107
|
+
const entry = (m: AssetManifest) => m.files.find((f) => f.path === m.entrypoint)!;
|
|
108
|
+
const identity = (m: AssetManifest): string => `${m.assetId}/${m.version}`;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Qualifies complete source/output inventories and conversion lineage before
|
|
112
|
+
* composing existing typed promotion, rights, hard-gate and human confirmation
|
|
113
|
+
* contracts. Tokens remain trusted hosted assertions; this is not a signature
|
|
114
|
+
* verifier, parser, storage transaction or permission grant. Rollback rechecks
|
|
115
|
+
* target bytes/gates and requires locally qualified exact prior/current records.
|
|
116
|
+
*/
|
|
117
|
+
export async function createModelResourcePromotionRecord(input: ModelResourcePromotionInput, options: ModelResourcePromotionOptions = {}): Promise<QualifiedModelResourcePromotion> {
|
|
118
|
+
let timer: ReturnType<typeof setTimeout> | undefined; let signal: AbortSignal | undefined; let onAbort: (() => void) | undefined;
|
|
119
|
+
try {
|
|
120
|
+
const started = performance.now(); const controls = fields(options, ["signal", "timeoutMs"]);
|
|
121
|
+
const timeoutMs = controls.timeoutMs === undefined ? 10_000 : controls.timeoutMs;
|
|
122
|
+
if (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MODEL_RESOURCE_PROMOTION_LIMITS.timeoutMs) fail();
|
|
123
|
+
signal = controls.signal as AbortSignal | undefined;
|
|
124
|
+
const aborted = Object.getOwnPropertyDescriptor(AbortSignal.prototype, "aborted")!.get!;
|
|
125
|
+
const check = () => { if (signal !== undefined && aborted.call(signal)) fail("aborted"); if (performance.now() - started >= timeoutMs) fail("deadline-exceeded"); };
|
|
126
|
+
check();
|
|
127
|
+
const root = fields(input, ["promotion", "source", "output", "diagnostics", "resourceBindings", "candidate", "confirmation", "rollback"]);
|
|
128
|
+
const budget = { values: MODEL_RESOURCE_PROMOTION_LIMITS.values, characters: MODEL_RESOURCE_PROMOTION_LIMITS.characters };
|
|
129
|
+
const snap = (v: unknown) => snapshot(v, budget);
|
|
130
|
+
const promotionInput = snap(root.promotion) as ModelResourcePromotionInput["promotion"];
|
|
131
|
+
const sourceInput = fields(root.source, ["manifest", "packageManifest", "files"]);
|
|
132
|
+
const outputInput = fields(root.output, ["manifest", "packageManifest", "files", "graph", "resources", "derived"]);
|
|
133
|
+
const sourceMetadata = { manifest: snap(sourceInput.manifest) as AssetManifest, packageManifest: snap(sourceInput.packageManifest) as ModelResourceManifestQualificationInput["packageManifest"] };
|
|
134
|
+
const outputMetadata = { manifest: snap(outputInput.manifest) as AssetManifest, packageManifest: snap(outputInput.packageManifest) as ModelTextureResourceQualificationInput["packageManifest"],
|
|
135
|
+
graph: outputInput.graph as ModelTextureResourceQualificationInput["graph"], resources: snap(outputInput.resources) as ModelTextureResourceQualificationInput["resources"], derived: snap(outputInput.derived) as ModelTextureResourceQualificationInput["derived"] };
|
|
136
|
+
const rawDiagnostics = snap(root.diagnostics);
|
|
137
|
+
const candidate = createModelCandidate(snap(root.candidate));
|
|
138
|
+
const confirmation = createModelCandidateConfirmation(snap(root.confirmation), candidate, candidate.resolutionId);
|
|
139
|
+
const bindings = array(snap(root.resourceBindings), MODEL_RESOURCE_PROMOTION_LIMITS.bindings).map((v): ModelPromotionResourceBinding => {
|
|
140
|
+
const b = fields(v, ["resource", "package", "filePath"]);
|
|
141
|
+
if ((b.package !== "source" && b.package !== "output") || typeof b.filePath !== "string") fail();
|
|
142
|
+
return Object.freeze({ resource: createModelResourceRef(b.resource), package: b.package, filePath: b.filePath });
|
|
143
|
+
}).sort((a, b) => a.resource.uri < b.resource.uri ? -1 : a.resource.uri > b.resource.uri ? 1 : 0);
|
|
144
|
+
const history = root.rollback === undefined ? undefined : fields(root.rollback, ["prior", "current"]);
|
|
145
|
+
if (history && (!qualified.has(history.prior as object) || !qualified.has(history.current as object))) fail("rollback-mismatch");
|
|
146
|
+
const prior = history?.prior as QualifiedModelResourcePromotion | undefined;
|
|
147
|
+
const current = history?.current as QualifiedModelResourcePromotion | undefined;
|
|
148
|
+
const sourceFiles = copyFiles(sourceInput.files); const outputFiles = copyFiles(outputInput.files);
|
|
149
|
+
check();
|
|
150
|
+
const interrupted = new Promise<never>((_resolve, reject) => {
|
|
151
|
+
timer = setTimeout(() => reject(new ModelResourcePromotionError("deadline-exceeded")), Math.max(1, timeoutMs - (performance.now() - started)));
|
|
152
|
+
onAbort = () => reject(new ModelResourcePromotionError("aborted"));
|
|
153
|
+
if (signal) AbortSignal.prototype.addEventListener.call(signal, "abort", onAbort, { once: true });
|
|
154
|
+
});
|
|
155
|
+
// Sequential package hashing limits digest concurrency. Every caller byte was
|
|
156
|
+
// copied above; only inert snapshots and the immutable core graph cross await.
|
|
157
|
+
const work = async () => {
|
|
158
|
+
const source = await qualifyModelResourceManifest({ ...sourceMetadata, files: sourceFiles, signal, timeoutMs }); check();
|
|
159
|
+
const output = await qualifyModelTextureResources({ ...outputMetadata, files: outputFiles }, { signal, timeoutMs }); check();
|
|
160
|
+
const diagnostics = await qualifyModelConversionDiagnostics(rawDiagnostics, { signal, timeoutMs }); check();
|
|
161
|
+
assertImmutableAssetVersion(source.version); assertImmutableAssetVersion(output.manifest.version);
|
|
162
|
+
if (identity(source) === identity(output.manifest) || !same(output.manifest, createModelAssetManifest({ ...promotionInput.manifest, files: [...promotionInput.manifest.files].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0) })) || output.manifest.assetKind !== "model") fail("package-mismatch");
|
|
163
|
+
const converter = candidate.processingManifest.converter;
|
|
164
|
+
if (!diagnostics.technicalAccepted || !same(diagnostics.source, candidate.provenance)
|
|
165
|
+
|| diagnostics.source.contentHash !== entry(source).sha256 || output.source.sourceContentHash !== entry(source).sha256
|
|
166
|
+
|| converter.sourceContentHash !== entry(source).sha256 || converter.outputContentHash !== entry(output.manifest).sha256
|
|
167
|
+
|| converter.id !== output.source.converterId || converter.version !== output.source.converterVersion
|
|
168
|
+
|| converter.sourceFormat !== output.source.sourceFormat || !same(diagnostics.converter.diagnostics, converter.diagnostics) || !same(diagnostics.converter.losses, converter.losses)) fail("evidence-mismatch");
|
|
169
|
+
const byUri = new Map<string, ModelPromotionResourceBinding>();
|
|
170
|
+
for (const binding of bindings) {
|
|
171
|
+
const file = (binding.package === "source" ? source : output.manifest).files.find((f) => f.path === binding.filePath);
|
|
172
|
+
if (!file || byUri.has(binding.resource.uri) || file.sha256 !== binding.resource.sha256 || file.byteLength !== binding.resource.byteLength || file.contentType !== binding.resource.contentType) fail("evidence-mismatch");
|
|
173
|
+
byUri.set(binding.resource.uri, binding);
|
|
174
|
+
}
|
|
175
|
+
const requireResource = (resource: ModelResourceRef, outputOnly = false) => {
|
|
176
|
+
const binding = byUri.get(resource.uri);
|
|
177
|
+
if (!binding || !same(binding.resource, resource) || (outputOnly && binding.package !== "output")) fail("evidence-mismatch");
|
|
178
|
+
return binding;
|
|
179
|
+
};
|
|
180
|
+
for (const d of diagnostics.diagnostics) if (d.resource) requireResource(d.resource);
|
|
181
|
+
for (const lod of candidate.processingManifest.lods) requireResource(lod.resource, true);
|
|
182
|
+
if (candidate.processingManifest.collision.resource) requireResource(candidate.processingManifest.collision.resource, true);
|
|
183
|
+
for (const f of candidate.processingManifest.fidelityEvidence) if (f.evidenceResource) requireResource(f.evidenceResource, true);
|
|
184
|
+
if (requireResource(candidate.processingManifest.lods[0]!.resource, true).filePath !== output.manifest.entrypoint) fail("evidence-mismatch");
|
|
185
|
+
if (candidate.assetRef.disposition === "existing" && (candidate.assetRef.asset.assetId !== output.manifest.assetId || candidate.assetRef.asset.version !== output.manifest.version)) fail("evidence-mismatch");
|
|
186
|
+
if (promotionInput.sourceAdapter !== output.manifest.sourceAdapter || promotionInput.reviewReport.passed !== true || promotionInput.reviewReport.findings.some((f) => f.severity === "blocking")) fail("gate-blocked");
|
|
187
|
+
const expectedSuffix = `/${output.manifest.assetId}/${output.manifest.version}/${output.manifest.entrypoint}`;
|
|
188
|
+
const runtimeUri = new URL(promotionInput.runtimeManifestUri);
|
|
189
|
+
if (!runtimeUri.pathname.endsWith(expectedSuffix)) fail("evidence-mismatch");
|
|
190
|
+
const promotion = await createGpuAssetPromotionRecord(promotionInput, new Map(outputFiles.map((f) => [f.path, f.bytes]))); check();
|
|
191
|
+
// Promotion event/time/approver are intentionally outside this digest, so
|
|
192
|
+
// an exact rollback can reuse the same package with a new approval event.
|
|
193
|
+
const closure = { contractVersion: 1 as const, source, output, diagnostics, resourceBindings: Object.freeze(bindings), candidate, confirmation };
|
|
194
|
+
const closureHash = await computeSha256(new TextEncoder().encode(canonicalizeGpuContract({ contractVersion: 1, source, output, diagnostics, resourceBindings: bindings }))); check();
|
|
195
|
+
let rollback: QualifiedModelResourcePromotion["rollback"];
|
|
196
|
+
if (promotion.outcome === "rolled-back") {
|
|
197
|
+
if (!prior || !current || prior.promotion.assetId !== promotion.assetId || current.promotion.assetId !== promotion.assetId
|
|
198
|
+
|| prior.promotion.runtimeChannel !== promotion.runtimeChannel || current.promotion.runtimeChannel !== promotion.runtimeChannel
|
|
199
|
+
|| promotion.version !== prior.promotion.version || promotion.version === current.promotion.version || promotion.rollbackOfVersion !== current.promotion.version
|
|
200
|
+
|| closureHash !== prior.closureHash || !same(promotion.manifest, prior.promotion.manifest)
|
|
201
|
+
|| promotion.runtimeManifestUri !== prior.promotion.runtimeManifestUri) fail("rollback-mismatch");
|
|
202
|
+
rollback = Object.freeze({ fromVersion: current.promotion.version, fromClosureHash: current.closureHash, toVersion: prior.promotion.version, toClosureHash: prior.closureHash });
|
|
203
|
+
} else if (history || promotion.rollbackOfVersion !== undefined) fail("rollback-mismatch");
|
|
204
|
+
const result = Object.freeze({ ...closure, promotion, closureHash, ...(rollback ? { rollback } : {}) });
|
|
205
|
+
qualified.add(result); return result;
|
|
206
|
+
};
|
|
207
|
+
return await Promise.race([work(), interrupted]);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
// Even a proxy thrown by an inert-input inspection must not escape with
|
|
210
|
+
// authored exception text. Never invoke an upstream error's accessors.
|
|
211
|
+
let code: unknown;
|
|
212
|
+
try { code = Object.getOwnPropertyDescriptor(error, "code")?.value; } catch { return fail(); }
|
|
213
|
+
if (["invalid-input", "limit-exceeded", "package-mismatch", "evidence-mismatch", "gate-blocked", "rollback-mismatch", "aborted", "deadline-exceeded"].includes(code as string)) return fail(code as ModelResourcePromotionError["code"]);
|
|
214
|
+
return fail();
|
|
215
|
+
} finally {
|
|
216
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
217
|
+
if (signal && onAbort) AbortSignal.prototype.removeEventListener.call(signal, "abort", onAbort);
|
|
218
|
+
}
|
|
219
|
+
}
|