@plasius/asset-contracts 0.4.6 → 0.6.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 +42 -0
- package/README.md +122 -0
- package/dist/index.cjs +1293 -954
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +100 -1
- package/dist/index.d.ts +100 -1
- package/dist/index.js +1258 -935
- package/dist/index.js.map +1 -1
- package/docs/adrs/adr-0009-model-resource-manifest-qualification.md +74 -0
- package/docs/adrs/adr-0010-public-conversion-diagnostics.md +61 -0
- package/docs/adrs/index.md +2 -0
- package/docs/tdrs/index.md +2 -0
- package/docs/tdrs/tdr-0004-model-resource-manifest-qualification.md +70 -0
- package/docs/tdrs/tdr-0005-public-conversion-diagnostics.md +51 -0
- package/package.json +3 -2
- package/src/index.ts +2 -0
- package/src/model-conversion-diagnostics.ts +185 -0
- package/src/model-resource-manifest.ts +198 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# ADR 0009: Additive Model Resource Manifest Qualification
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-10-24
|
|
5
|
+
- Tracking: package Task 48; site Feature 1153 / Epic 1147
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
The existing file and manifest factories validate descriptor syntax. They do
|
|
10
|
+
not bind a model-core `ResourcePackageManifest` to the exact exported file set
|
|
11
|
+
or prove that the declared entrypoint exists. Tightening the existing factories
|
|
12
|
+
would change their public compatibility contract, including callers which
|
|
13
|
+
construct descriptors before storage bytes exist.
|
|
14
|
+
|
|
15
|
+
## Decision
|
|
16
|
+
|
|
17
|
+
Add `qualifyModelResourceManifest` as a separate, opt-in asynchronous boundary.
|
|
18
|
+
Reuse `AssetFileDescriptor`, `AssetManifest`, their existing factories, the
|
|
19
|
+
published model-core package inventory type, and gpu-shader's SHA-256 helper.
|
|
20
|
+
The qualifier requires one inventory entry and one byte payload for each
|
|
21
|
+
descriptor, including the entrypoint. It checks portable relative paths,
|
|
22
|
+
case-insensitive and file/directory collisions, canonical declared MIME types,
|
|
23
|
+
safe integer byte lengths, actual lengths and SHA-256 digests. Output file
|
|
24
|
+
order uses locale-independent code-unit comparison.
|
|
25
|
+
|
|
26
|
+
Snapshot all metadata and byte inputs before the first hash. Apply fixed count,
|
|
27
|
+
depth and byte ceilings, reject shared byte storage, and check cancellation and
|
|
28
|
+
deadline around each bounded hash. Web Crypto cannot cancel an in-flight
|
|
29
|
+
digest; a deadline is checked again before any successful return. No partial
|
|
30
|
+
manifest is returned on failure. Errors contain fixed codes, not caller data.
|
|
31
|
+
|
|
32
|
+
Keep roles, sourceAdapter and source/derived paths unchanged. Embedded resources
|
|
33
|
+
remain inside their exported entrypoint; they are not invented as separate
|
|
34
|
+
archive files. A verified model-core fixture independently checks embedded and
|
|
35
|
+
external resource identity and source provenance. This boundary neither parses
|
|
36
|
+
formats nor proves that a file internally references every required resource.
|
|
37
|
+
Task 51 owns promotion/resource closure, Task 50 owns detailed role mapping,
|
|
38
|
+
Task 12 owns animation/source relationship schemas and Task 23 owns shader ABI
|
|
39
|
+
qualification. Typed model envelopes still use the existing specialized factory.
|
|
40
|
+
|
|
41
|
+
Use exact published `@plasius/gpu-model-core@0.7.0` as a direct dependency because
|
|
42
|
+
the public input declarations reference its inventory type. The import is
|
|
43
|
+
type-only: no core parser, renderer or model runtime is loaded by this helper.
|
|
44
|
+
The full qualification audit additionally found the pre-existing development
|
|
45
|
+
AJV dependency `fast-uri@3.1.8` affected by newly published URI-normalization
|
|
46
|
+
advisories. Raise its existing override floor from `^3.1.6` to `^3.1.9` and
|
|
47
|
+
lock fixed `3.1.9`, retaining major 3. This targeted audit-blocker remediation
|
|
48
|
+
does not add runtime URI processing. No other dependency versions change.
|
|
49
|
+
|
|
50
|
+
## Alternatives and consequences
|
|
51
|
+
|
|
52
|
+
Changing legacy factories would break staged descriptor construction. Parsing
|
|
53
|
+
glTF/FBX here would duplicate adapter responsibilities. Treating descriptor
|
|
54
|
+
hashes as byte evidence would allow mismatches to survive. The new boundary
|
|
55
|
+
provides byte/inventory qualification only; storage, malware inspection,
|
|
56
|
+
format validity and promotion remain downstream responsibilities. Qualification
|
|
57
|
+
is evidence at call time, not an unforgeable promotion attestation.
|
|
58
|
+
|
|
59
|
+
## Rollout, tests and release
|
|
60
|
+
|
|
61
|
+
The backend remote evaluator owns `gpu.model.conversion.enabled`. Callers opt
|
|
62
|
+
into this helper when enabled; disabled or rollback paths retain their existing
|
|
63
|
+
factories. This library performs no live flag lookup or state mutation. There
|
|
64
|
+
is no UI or entitlement capability. Tests cover enabled/disabled/rollback
|
|
65
|
+
composition, legacy compatibility, canonical resource identity, malformed
|
|
66
|
+
inputs, fixed budgets, cancellation, deadlines and mutation isolation.
|
|
67
|
+
|
|
68
|
+
Update README, Unreleased changelog and TDR 0004. Require Node 24 frozen install,
|
|
69
|
+
tests/privacy, coverage and changed-source LCOV, lint/types/build, audits, pack
|
|
70
|
+
gates and packed/public ESM/CJS/types qualification. Release an additive minor
|
|
71
|
+
version through the existing PR, exact-main push CI and hosted
|
|
72
|
+
`cd.yml`/`production` OIDC path from ADR 0007. Verify public registry integrity
|
|
73
|
+
and provenance before downstream adoption. This Task authorizes no site
|
|
74
|
+
deployment or change to any existing release gate.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# ADR 0010: Public Conversion Diagnostics
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-10-24
|
|
5
|
+
- Tracking: asset-contracts Task49; site Feature1153 / Epic1147
|
|
6
|
+
|
|
7
|
+
## Context and decision
|
|
8
|
+
|
|
9
|
+
Existing ModelConverterDiagnostic/ModelConversionLoss and signed processing,
|
|
10
|
+
fidelity, rights, human review and promotion contracts already cover their
|
|
11
|
+
boundaries. Model-core0.7.0 owns canonical strict/tolerant/forensic policy,
|
|
12
|
+
applied repairs, unsupported data and loss consent. Its private diagnostics may
|
|
13
|
+
contain original values, rawSource, private paths and free-form adapter text.
|
|
14
|
+
They cannot be forwarded as public contract evidence.
|
|
15
|
+
|
|
16
|
+
Add qualifyModelConversionDiagnostics as an opt-in async boundary. Lazy import
|
|
17
|
+
the existing exact published core dependency and reuse its evaluator; do not
|
|
18
|
+
implement another acceptance policy or format parser. Inert bounded snapshots
|
|
19
|
+
precede the first await. Project fixed category codes/messages, exact public
|
|
20
|
+
resource bindings and existing ModelProvenance. Root diagnostics use source
|
|
21
|
+
identity; unmatched non-root paths and contradictory URI identities reject.
|
|
22
|
+
Only recognized completed repairs expose core-owned rule IDs. Discard adapter
|
|
23
|
+
codes/text/private paths, original values and forensic snapshots. Unknown
|
|
24
|
+
adapter codes become fixed category codes; unknown repair rules still block.
|
|
25
|
+
|
|
26
|
+
Explicit failure takes precedence, then unsupported, then canonical rejection,
|
|
27
|
+
then warning/clean success. Denied loss remains blocking in the legacy converter
|
|
28
|
+
loss array. Detailed output and existing-shape converter arrays are frozen.
|
|
29
|
+
Technical acceptance is not approval. No existing gate or factory is weakened.
|
|
30
|
+
|
|
31
|
+
## Alternatives, costs and limits
|
|
32
|
+
|
|
33
|
+
Regex redaction cannot reliably scrub arbitrary provider text. Tightening
|
|
34
|
+
legacy factories would break staged consumers. Copying the canonical evaluator
|
|
35
|
+
would create policy drift. Requiring eager core loading would add avoidable
|
|
36
|
+
first-load work. Lazy import is asynchronous and may load a bundler chunk; module
|
|
37
|
+
loading cannot be cancelled, but the wait is bounded. Bounded synchronous work
|
|
38
|
+
checks deadlines before/after and cannot be preempted. Resource/provenance
|
|
39
|
+
binding is a trusted-caller assertion; byte evidence and promotion remain
|
|
40
|
+
separate. No dependency versions change. Tasks12/23/50/51 retain animation,
|
|
41
|
+
shader ABI, role and promotion-closure scope respectively.
|
|
42
|
+
|
|
43
|
+
## Rollout, NFR and release
|
|
44
|
+
|
|
45
|
+
Backend evaluator gpu.model.conversion.enabled controls consumer adoption;
|
|
46
|
+
disabled/rollback callers retain legacy contracts. No service lookup or flag
|
|
47
|
+
mutation in this library, no UI entitlement capability. Test both branches and
|
|
48
|
+
rollback, malformed metadata, stable codes/serialization, privacy canaries,
|
|
49
|
+
resource identity, mutation, limits, deadline/cancellation and real processing
|
|
50
|
+
manifest composition. Existing independent gate tests remain active.
|
|
51
|
+
|
|
52
|
+
Security/privacy use allowlisted output and value-free errors. Fixed work and
|
|
53
|
+
input budgets, no retries/I/O/global state address reliability/performance and
|
|
54
|
+
scalability; immutable typed JSON and reused factories address maintenance and
|
|
55
|
+
testability. Fixed codes support safe observability without logging. UI/SEO and
|
|
56
|
+
durable storage controls are N/A. Require Node24 frozen install, full tests and
|
|
57
|
+
changed-source LCOV>=80%, types/lint/build, full/runtime audits, tree/privacy/pack
|
|
58
|
+
and packed/public ESM/CJS/types/browser consumers. Update README/Unreleased/TDR.
|
|
59
|
+
Release additive minor through PR, exact-main push CI, existing hosted
|
|
60
|
+
cd.yml/main/production OIDC, registry bytes/provenance and consumer checks.
|
|
61
|
+
No site deployment is authorized or required by this package-only Task.
|
package/docs/adrs/index.md
CHANGED
|
@@ -8,3 +8,5 @@
|
|
|
8
8
|
- [ADR 0006: Additive PVOX Model-Resolution Contracts](./adr-0006-pvox-model-resolution-contracts.md)
|
|
9
9
|
- [ADR 0007: Hosted OIDC Package Publication](./adr-0007-hosted-oidc-package-publication.md)
|
|
10
10
|
- [ADR 0008: Package-Owned Issue Tracking](./adr-0008-package-owned-issue-tracking.md)
|
|
11
|
+
- [ADR 0009: Additive Model Resource Manifest Qualification](./adr-0009-model-resource-manifest-qualification.md)
|
|
12
|
+
- [ADR 0010: Public Conversion Diagnostics](./adr-0010-public-conversion-diagnostics.md)
|
package/docs/tdrs/index.md
CHANGED
|
@@ -3,3 +3,5 @@
|
|
|
3
3
|
- [TDR 0001: WGSL Shader Asset Envelope Validation](./tdr-0001-wgsl-shader-asset-envelope-validation.md)
|
|
4
4
|
- [TDR 0002: Exact-Version Validation Routing](./tdr-0002-exact-version-validation-routing.md)
|
|
5
5
|
- [TDR 0003: PVOX Validation and Confirmation Binding](./tdr-0003-pvox-validation-and-confirmation-binding.md)
|
|
6
|
+
- [TDR 0004: Model Resource Manifest Qualification](./tdr-0004-model-resource-manifest-qualification.md)
|
|
7
|
+
- [TDR 0005: Public Conversion Diagnostics](./tdr-0005-public-conversion-diagnostics.md)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# TDR 0004: Model Resource Manifest Qualification
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-10-24
|
|
5
|
+
- Tracking: asset-contracts Task 48; site Feature 1153
|
|
6
|
+
- Decision: ADR 0009
|
|
7
|
+
|
|
8
|
+
## Inputs and sequence
|
|
9
|
+
|
|
10
|
+
1. Accept an existing base or typed model `AssetManifest`, the published
|
|
11
|
+
model-core `ResourcePackageManifest`, and exact exported `Uint8Array` files
|
|
12
|
+
with independent content-type declarations. Optional inputs are native
|
|
13
|
+
AbortSignal and a 1–60,000 ms deadline (default 10,000 ms).
|
|
14
|
+
2. Reject exotic metadata records, accessors, symbols, prototype keys, sparse
|
|
15
|
+
arrays and unsupported root fields without executing metadata getters.
|
|
16
|
+
Snapshot metadata using fixed count/depth/string/key limits.
|
|
17
|
+
3. Reuse the existing base or specialized model factory. Keep every existing
|
|
18
|
+
role, source adapter and specialized field. Do not tighten legacy factories.
|
|
19
|
+
4. Require portable relative paths, unique case-insensitive names and no
|
|
20
|
+
file/directory prefix collision. The entrypoint must be a descriptor path.
|
|
21
|
+
Reject unsafe integer counts and fixed file/aggregate budget overruns.
|
|
22
|
+
5. Require one canonical package entry per descriptor, unique bounded logical
|
|
23
|
+
ids, supported core inventory roles, and MIME declaration agreement when
|
|
24
|
+
the optional inventory declaration exists. Do not map core roles to storage
|
|
25
|
+
roles or infer content type from a filename.
|
|
26
|
+
6. Require one actual payload per descriptor with exact byte length and MIME
|
|
27
|
+
declaration. Read byte lengths and buffers through typed-array intrinsics,
|
|
28
|
+
rejecting shared storage. Only after all budgets and actual lengths pass,
|
|
29
|
+
snapshot bytes before any asynchronous operation.
|
|
30
|
+
7. Sequentially compute SHA-256 through gpu-shader's existing Web Crypto helper.
|
|
31
|
+
Check signal/deadline before and after every digest; compare exact lowercase
|
|
32
|
+
digests. Return no result on any failure. Sequential hashing avoids
|
|
33
|
+
unbounded concurrency; no filesystem, network, log, retry or caller callback
|
|
34
|
+
is introduced. Deadline expiration cannot interrupt the platform digest,
|
|
35
|
+
but it prevents a successful return after the operation finishes.
|
|
36
|
+
8. Return frozen metadata and frozen descriptors sorted by UTF-16 code units.
|
|
37
|
+
The result is not a storage receipt or promotion attestation. A caller must
|
|
38
|
+
retain the qualified bytes unchanged at its own durable boundary.
|
|
39
|
+
|
|
40
|
+
## Canonical fixture and source fidelity
|
|
41
|
+
|
|
42
|
+
Tests import the published model-core package, independently verify a triangle
|
|
43
|
+
document, and build its resource graph. The derived position buffer is embedded
|
|
44
|
+
in a glTF data URI; the original PNG remains an external exported file. Compare
|
|
45
|
+
each resource's byte content/hash with its payload and exported location, and
|
|
46
|
+
bind document source provenance to the entrypoint digest. Embedded data is not
|
|
47
|
+
listed as a fictitious archive file. The fixture does not grant production
|
|
48
|
+
format-parser, texture-role or promotion-closure authority.
|
|
49
|
+
|
|
50
|
+
## Rollout, compatibility and NFR evidence
|
|
51
|
+
|
|
52
|
+
Callers compose this helper behind backend-evaluated
|
|
53
|
+
`gpu.model.conversion.enabled`; disabled and rollback paths continue the legacy
|
|
54
|
+
factory. Test that enabled qualification rejects mismatched bytes while both
|
|
55
|
+
fallback paths preserve legacy behavior. No live control mutation is needed.
|
|
56
|
+
Security/privacy tests cover malformed shapes, value-free failures, traversal,
|
|
57
|
+
collisions, byte/hash/MIME drift, shared buffers and input mutation. Fixed
|
|
58
|
+
budgets and sequential work address reliability/performance; frozen snapshots
|
|
59
|
+
and exact inventory types address maintainability/testability. UI accessibility,
|
|
60
|
+
SEO and storage/encryption controls are N/A to this in-memory library boundary.
|
|
61
|
+
|
|
62
|
+
The public type requires exact `@plasius/gpu-model-core@0.7.0`; the import is
|
|
63
|
+
type-only. A full-audit failure also requires the targeted development-only
|
|
64
|
+
fast-uri override floor `^3.1.6` to `^3.1.9` and lock `3.1.8` to fixed `3.1.9`;
|
|
65
|
+
retain the existing AJV/major-3 dependency contract and rerun all gates. No
|
|
66
|
+
other dependency versions change. Require frozen Node 24 install,
|
|
67
|
+
full tests/privacy/coverage and changed-source LCOV, lint/types/build, dependency
|
|
68
|
+
tree/audits, packed ESM/CJS/types/browser-safe import, and public-registry
|
|
69
|
+
artifact/provenance checks. Ship an additive minor release via existing
|
|
70
|
+
`cd.yml`/`main`/`production` OIDC governance. Site adoption is separate.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# TDR 0005: Public Conversion Diagnostics
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-10-24
|
|
5
|
+
- Tracking: asset-contracts Task49 / site Feature1153
|
|
6
|
+
- Decision: ADR0010
|
|
7
|
+
|
|
8
|
+
## Input and evaluation
|
|
9
|
+
|
|
10
|
+
Accept outcome completed/unsupported/failed, existing public source provenance,
|
|
11
|
+
canonical GpuModelDiagnosticInput and exact private path/public resource bindings.
|
|
12
|
+
Snapshot inert JSON (no getters, symbols, sparse arrays, exotic prototypes or
|
|
13
|
+
cycles) with depth12/16,384values/2,048-characterstrings/262,144aggregatecharacters;
|
|
14
|
+
records/arrays have hard1024member limits. Limit issues/resources to100. Reserve
|
|
15
|
+
one issue slot for non-completed outcomes. Reject duplicate paths and conflicting
|
|
16
|
+
resource declarations for a URI. Every non-root diagnostic needs a binding;
|
|
17
|
+
root uses source. Reuse existing provenance/resource factories and fixed errors.
|
|
18
|
+
|
|
19
|
+
After snapshot, lazy-import published exact model-core0.7.0 and evaluate canonical
|
|
20
|
+
policy. Do not trust a caller-supplied accepted flag or re-evaluate repairs here.
|
|
21
|
+
Project core categories into fixed public codes/text; only accepted repairs expose
|
|
22
|
+
core rule IDs. Drop arbitrary source code/message/path, originalValue/rawSource.
|
|
23
|
+
Explicit failed outcome wins, then unsupported kind/outcome, rejected policy,
|
|
24
|
+
warning/clean success. Canonical consent controls loss severity; denied losses
|
|
25
|
+
remain blocking for existing fidelity derivation. Return frozen version1 JSON
|
|
26
|
+
with technicalAccepted, source, standard/mode, detailed diagnostics and exact
|
|
27
|
+
legacy converter diagnostic/loss arrays. No approval/receipt is synthesized.
|
|
28
|
+
|
|
29
|
+
Native AbortSignal and deadline1–60,000ms(default10,000) cover the operation.
|
|
30
|
+
Race lazy import against bounded timer/abort; clean timer/listener in finally.
|
|
31
|
+
Check deadlines around bounded synchronous snapshot/evaluation; neither loader
|
|
32
|
+
nor synchronous JS can be cancelled mid-execution. No network/filesystem/logging,
|
|
33
|
+
retry or callback dependency is added. Input references are caller assertions,
|
|
34
|
+
not byte verification; Task48/51 retain manifest/promotion responsibilities.
|
|
35
|
+
|
|
36
|
+
## Requirements-based validation and delivery
|
|
37
|
+
|
|
38
|
+
Tests first cover status precedence, strict/tolerant/applied and unknown repairs,
|
|
39
|
+
allowed/denied dropped+approximated loss, fixed messages/codes, forensic redaction,
|
|
40
|
+
source/resource binding, JSON serialization and mutation isolation. Test invalid
|
|
41
|
+
codes/kinds/shapes, accessors/prototypes/symbols/sparse/cyclic inputs, limits and
|
|
42
|
+
options, cancellation/deadline and legacy array ceilings. Compose actual
|
|
43
|
+
ModelProcessingManifest evidence and verify independent fidelity/hard/rights
|
|
44
|
+
gates still reject missing or contradictory evidence. Preserve existing tests.
|
|
45
|
+
|
|
46
|
+
Backend gpu.model.conversion.enabled adoption enabled/disabled/rollback branches
|
|
47
|
+
are tested without live changes; no capability. No dependency versions change.
|
|
48
|
+
Run Node24 frozen full gates, >=80%changed-source LCOV, pack/consumer/audit,
|
|
49
|
+
then PR CI, merge, exact-main CI and approved hosted cd.yml/main/production OIDC.
|
|
50
|
+
Verify registry immutable bytes/provenance/signatures and actual public consumers
|
|
51
|
+
before Done. README/Unreleased and ADR/TDR indices ship with additive minor.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plasius/asset-contracts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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,
|
|
@@ -103,12 +103,13 @@
|
|
|
103
103
|
"overrides": {
|
|
104
104
|
"brace-expansion": "5.0.12",
|
|
105
105
|
"esbuild": "^0.28.1",
|
|
106
|
-
"fast-uri": "^3.1.
|
|
106
|
+
"fast-uri": "^3.1.9",
|
|
107
107
|
"minimatch": "^10.2.1",
|
|
108
108
|
"nanoid": "3.3.18",
|
|
109
109
|
"postcss": "8.5.26"
|
|
110
110
|
},
|
|
111
111
|
"dependencies": {
|
|
112
|
+
"@plasius/gpu-model-core": "0.7.0",
|
|
112
113
|
"@plasius/gpu-shader": "^0.1.10"
|
|
113
114
|
}
|
|
114
115
|
}
|
package/src/index.ts
CHANGED
|
@@ -38,6 +38,8 @@ export * from "./model-resolution.js";
|
|
|
38
38
|
export * from "./pvox-hash-preimage.js";
|
|
39
39
|
export * from "./pvox-model-resolution.js";
|
|
40
40
|
export * from "./pvox-model-resolution-schema.js";
|
|
41
|
+
export * from "./model-resource-manifest.js";
|
|
42
|
+
export * from "./model-conversion-diagnostics.js";
|
|
41
43
|
export {
|
|
42
44
|
GPU_SHADER_STORE_FEATURE_FLAG,
|
|
43
45
|
GPU_SHADER_STYLE_SELECTION_CAPABILITY,
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type { GpuModelDiagnosticInput, GpuModelDiagnosticMode, GpuModelDiagnosticStandard } from "@plasius/gpu-model-core";
|
|
2
|
+
import {
|
|
3
|
+
createModelProvenance, createModelResourceRef,
|
|
4
|
+
type ModelProvenance, type ModelResourceRef, type ModelConverterDiagnostic, type ModelConversionLoss,
|
|
5
|
+
} from "./model-resolution.js";
|
|
6
|
+
|
|
7
|
+
/** Hard ceilings for public conversion evidence, including private input metadata. */
|
|
8
|
+
export const MODEL_CONVERSION_DIAGNOSTIC_LIMITS = Object.freeze({ issues: 100, resources: 100, values: 16_384, depth: 12, string: 2048, characters: 262_144, timeoutMs: 60_000 });
|
|
9
|
+
|
|
10
|
+
/** Exact private canonical path to an opaque, credential-free public resource. */
|
|
11
|
+
export interface ModelConversionResourceBinding {
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly resource: ModelResourceRef;
|
|
14
|
+
}
|
|
15
|
+
/** Canonical evaluator input; arbitrary adapter text is never projected. */
|
|
16
|
+
export interface ModelConversionDiagnosticsInput {
|
|
17
|
+
readonly outcome: "completed" | "unsupported" | "failed";
|
|
18
|
+
readonly source: ModelProvenance;
|
|
19
|
+
readonly report: GpuModelDiagnosticInput;
|
|
20
|
+
readonly resources: readonly ModelConversionResourceBinding[];
|
|
21
|
+
}
|
|
22
|
+
/** Cancellation and deadline cover snapshotting, lazy import and evaluation. */
|
|
23
|
+
export interface ModelConversionDiagnosticsOptions {
|
|
24
|
+
readonly signal?: AbortSignal;
|
|
25
|
+
readonly timeoutMs?: number;
|
|
26
|
+
}
|
|
27
|
+
/** Stable public codes; adapter-supplied codes are never treated as this vocabulary. */
|
|
28
|
+
export type ModelConversionDiagnosticCode = "conversion.warning" | "conversion.repair" | "conversion.loss.dropped" | "conversion.loss.approximated" | "conversion.unsupported" | "conversion.blocked" | "conversion.failed";
|
|
29
|
+
/** Technical evidence only; this is never human, rights, security or promotion approval. */
|
|
30
|
+
export interface QualifiedModelConversionDiagnostic extends ModelConverterDiagnostic {
|
|
31
|
+
readonly code: ModelConversionDiagnosticCode;
|
|
32
|
+
readonly resource?: ModelResourceRef;
|
|
33
|
+
readonly effect?: "dropped" | "approximated";
|
|
34
|
+
readonly fallback?: string;
|
|
35
|
+
}
|
|
36
|
+
/** Deterministic JSON projection plus legacy-compatible converter evidence fields. */
|
|
37
|
+
export interface QualifiedModelConversionDiagnostics {
|
|
38
|
+
readonly contractVersion: 1;
|
|
39
|
+
readonly status: "succeeded" | "warning" | "unsupported" | "failed";
|
|
40
|
+
readonly technicalAccepted: boolean;
|
|
41
|
+
readonly standard: GpuModelDiagnosticStandard;
|
|
42
|
+
readonly mode: GpuModelDiagnosticMode;
|
|
43
|
+
readonly source: ModelProvenance;
|
|
44
|
+
readonly diagnostics: readonly QualifiedModelConversionDiagnostic[];
|
|
45
|
+
readonly converter: {
|
|
46
|
+
readonly diagnostics: readonly ModelConverterDiagnostic[];
|
|
47
|
+
readonly losses: readonly ModelConversionLoss[];
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Fixed, value-free error; never includes adapter/provider exception text. */
|
|
51
|
+
export class ModelConversionDiagnosticsError extends TypeError {
|
|
52
|
+
constructor(public readonly code: "invalid-input" | "limit-exceeded" | "unbound-resource" | "aborted" | "deadline-exceeded") {
|
|
53
|
+
super(`Invalid model conversion diagnostics: ${code}`);
|
|
54
|
+
this.name = "ModelConversionDiagnosticsError";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function fail(code: ModelConversionDiagnosticsError["code"] = "invalid-input"): never { throw new ModelConversionDiagnosticsError(code); }
|
|
58
|
+
|
|
59
|
+
function fields(value: unknown): Record<string, unknown> {
|
|
60
|
+
if (!value || typeof value !== "object" || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) fail();
|
|
61
|
+
const result: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
62
|
+
const keys = Reflect.ownKeys(value);
|
|
63
|
+
if (keys.length > 1024) fail("limit-exceeded");
|
|
64
|
+
for (const key of keys) {
|
|
65
|
+
if (typeof key !== "string" || key.length > 128 || ["__proto__", "prototype", "constructor"].includes(key)) fail();
|
|
66
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key)!;
|
|
67
|
+
if (!("value" in descriptor) || !descriptor.enumerable) fail();
|
|
68
|
+
result[key] = descriptor.value;
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
function only(record: Record<string, unknown>, keys: readonly string[]): void {
|
|
73
|
+
if (Object.keys(record).some((key) => !keys.includes(key))) fail();
|
|
74
|
+
}
|
|
75
|
+
function snapshot(value: unknown, budget: { values: number; characters: number }, depth = 0): unknown {
|
|
76
|
+
if (--budget.values < 0 || depth > MODEL_CONVERSION_DIAGNOSTIC_LIMITS.depth) fail("limit-exceeded");
|
|
77
|
+
if (value === null || typeof value === "boolean") return value;
|
|
78
|
+
if (typeof value === "string") {
|
|
79
|
+
budget.characters -= value.length;
|
|
80
|
+
if (value.length > MODEL_CONVERSION_DIAGNOSTIC_LIMITS.string || budget.characters < 0) fail("limit-exceeded");
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
if (Object.getPrototypeOf(value) !== Array.prototype || value.length > 1024) fail("limit-exceeded");
|
|
86
|
+
if (Reflect.ownKeys(value).length !== value.length + 1) fail();
|
|
87
|
+
const result = [];
|
|
88
|
+
for (let i = 0; i < value.length; i++) {
|
|
89
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(i));
|
|
90
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) fail();
|
|
91
|
+
result.push(snapshot(descriptor.value, budget, depth + 1));
|
|
92
|
+
}
|
|
93
|
+
return Object.freeze(result);
|
|
94
|
+
}
|
|
95
|
+
const record = fields(value);
|
|
96
|
+
for (const key of Object.keys(record)) {
|
|
97
|
+
budget.characters -= key.length;
|
|
98
|
+
if (budget.characters < 0) fail("limit-exceeded");
|
|
99
|
+
record[key] = snapshot(record[key], budget, depth + 1);
|
|
100
|
+
}
|
|
101
|
+
return Object.freeze(record);
|
|
102
|
+
}
|
|
103
|
+
function legacy(value: QualifiedModelConversionDiagnostic): ModelConverterDiagnostic {
|
|
104
|
+
return Object.freeze({ severity: value.severity, code: value.code, message: value.message });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Evaluate canonical diagnostics and project fixed public text. Unknown adapter codes
|
|
109
|
+
* are intentionally discarded; unknown repair rules remain blocking in model-core.
|
|
110
|
+
* Exact path bindings are assertions by the trusted caller, not byte attestations.
|
|
111
|
+
*/
|
|
112
|
+
export async function qualifyModelConversionDiagnostics(input: unknown, options: ModelConversionDiagnosticsOptions = {}): Promise<QualifiedModelConversionDiagnostics> {
|
|
113
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
114
|
+
let signal: AbortSignal | undefined;
|
|
115
|
+
let onAbort: (() => void) | undefined;
|
|
116
|
+
try {
|
|
117
|
+
const start = performance.now();
|
|
118
|
+
const controls = fields(options);
|
|
119
|
+
only(controls, ["signal", "timeoutMs"]);
|
|
120
|
+
const timeoutMs = controls.timeoutMs === undefined ? 10_000 : controls.timeoutMs;
|
|
121
|
+
if (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MODEL_CONVERSION_DIAGNOSTIC_LIMITS.timeoutMs) fail();
|
|
122
|
+
signal = controls.signal as AbortSignal | undefined;
|
|
123
|
+
const aborted = Object.getOwnPropertyDescriptor(AbortSignal.prototype, "aborted")!.get!;
|
|
124
|
+
const check = () => {
|
|
125
|
+
if (signal !== undefined && aborted.call(signal)) fail("aborted");
|
|
126
|
+
if (performance.now() - start >= timeoutMs) fail("deadline-exceeded");
|
|
127
|
+
};
|
|
128
|
+
check();
|
|
129
|
+
const args = fields(snapshot(input, { values: MODEL_CONVERSION_DIAGNOSTIC_LIMITS.values, characters: MODEL_CONVERSION_DIAGNOSTIC_LIMITS.characters }));
|
|
130
|
+
only(args, ["outcome", "source", "report", "resources"]);
|
|
131
|
+
if (!["completed", "unsupported", "failed"].includes(args.outcome as string)) fail();
|
|
132
|
+
const source = createModelProvenance(args.source);
|
|
133
|
+
const reportInput = fields(args.report);
|
|
134
|
+
// Reserve room for the outcome diagnostic in legacy arrays limited to 100.
|
|
135
|
+
const maximumIssues = MODEL_CONVERSION_DIAGNOSTIC_LIMITS.issues - (args.outcome === "completed" ? 0 : 1);
|
|
136
|
+
if (!Array.isArray(reportInput.issues) || reportInput.issues.length > maximumIssues) fail("limit-exceeded");
|
|
137
|
+
if (!Array.isArray(args.resources) || args.resources.length > MODEL_CONVERSION_DIAGNOSTIC_LIMITS.resources) fail("limit-exceeded");
|
|
138
|
+
const resources = new Map<string, ModelResourceRef>();
|
|
139
|
+
const identities = new Map<string, string>();
|
|
140
|
+
for (const value of args.resources) {
|
|
141
|
+
const binding = fields(value);
|
|
142
|
+
only(binding, ["path", "resource"]);
|
|
143
|
+
if (typeof binding.path !== "string" || !binding.path.startsWith("/") || binding.path === "/" || resources.has(binding.path)) fail();
|
|
144
|
+
const resource = createModelResourceRef(binding.resource);
|
|
145
|
+
const identity = JSON.stringify(resource);
|
|
146
|
+
if (identities.has(resource.uri) && identities.get(resource.uri) !== identity) fail();
|
|
147
|
+
identities.set(resource.uri, identity);
|
|
148
|
+
resources.set(binding.path, resource);
|
|
149
|
+
}
|
|
150
|
+
check();
|
|
151
|
+
const interrupted = new Promise<never>((_resolve, reject) => {
|
|
152
|
+
timer = setTimeout(() => reject(new ModelConversionDiagnosticsError("deadline-exceeded")), Math.max(1, timeoutMs - (performance.now() - start)));
|
|
153
|
+
onAbort = () => reject(new ModelConversionDiagnosticsError("aborted"));
|
|
154
|
+
if (signal) AbortSignal.prototype.addEventListener.call(signal, "abort", onAbort, { once: true });
|
|
155
|
+
});
|
|
156
|
+
const core = await Promise.race([import("@plasius/gpu-model-core"), interrupted]);
|
|
157
|
+
check();
|
|
158
|
+
const report = core.evaluateGpuModelDiagnostics(reportInput);
|
|
159
|
+
const diagnostics: QualifiedModelConversionDiagnostic[] = report.diagnostics.map((diagnostic) => {
|
|
160
|
+
const resource = resources.get(diagnostic.path);
|
|
161
|
+
if (diagnostic.path !== "/" && !resource) fail("unbound-resource");
|
|
162
|
+
const base = resource ? { resource } : {};
|
|
163
|
+
switch (diagnostic.kind) {
|
|
164
|
+
case "warning": return Object.freeze({ ...base, severity: "warning", code: "conversion.warning", message: "Conversion completed with an advisory." });
|
|
165
|
+
case "repair": return Object.freeze({ ...base, severity: "warning", code: "conversion.repair", message: "Conversion applied a canonical repair fallback.", fallback: diagnostic.ruleId });
|
|
166
|
+
case "loss": return Object.freeze({ ...base, severity: reportInput.allowLoss === true ? "warning" : "blocking", code: `conversion.loss.${diagnostic.effect}`, message: diagnostic.effect === "dropped" ? "Conversion dropped source data." : "Conversion approximated source data.", effect: diagnostic.effect });
|
|
167
|
+
case "unsupported": return Object.freeze({ ...base, severity: "blocking", code: "conversion.unsupported", message: "Conversion encountered unsupported data." });
|
|
168
|
+
case "blocking-error": return Object.freeze({ ...base, severity: "blocking", code: "conversion.blocked", message: "Conversion did not satisfy canonical validation policy." });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
if (args.outcome !== "completed") diagnostics.push(Object.freeze({ severity: "blocking", code: args.outcome === "failed" ? "conversion.failed" : "conversion.unsupported", message: args.outcome === "failed" ? "Conversion failed." : "Conversion encountered unsupported data." }));
|
|
172
|
+
const technicalAccepted = args.outcome === "completed" && report.accepted;
|
|
173
|
+
const status = args.outcome === "failed" ? "failed" : args.outcome === "unsupported" || report.diagnostics.some((item) => item.kind === "unsupported") ? "unsupported" : !technicalAccepted ? "failed" : diagnostics.length ? "warning" : "succeeded";
|
|
174
|
+
const losses = diagnostics.filter((item) => item.effect !== undefined).map(legacy);
|
|
175
|
+
check();
|
|
176
|
+
return Object.freeze({ contractVersion: 1, status, technicalAccepted, standard: report.standard, mode: report.mode, source,
|
|
177
|
+
diagnostics: Object.freeze(diagnostics), converter: Object.freeze({ diagnostics: Object.freeze(diagnostics.filter((item) => item.effect === undefined).map(legacy)), losses: Object.freeze(losses) }) });
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (error instanceof ModelConversionDiagnosticsError) throw error;
|
|
180
|
+
return fail();
|
|
181
|
+
} finally {
|
|
182
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
183
|
+
if (signal && onAbort) AbortSignal.prototype.removeEventListener.call(signal, "abort", onAbort);
|
|
184
|
+
}
|
|
185
|
+
}
|