@plasius/asset-contracts 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.
@@ -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.
@@ -9,3 +9,5 @@
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
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)
13
+ - [ADR 0011: Reloadable Texture and Resource Roles](./adr-0011-reloadable-texture-resource-roles.md)
@@ -4,3 +4,5 @@
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
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)
8
+ - [TDR 0006: Reloadable Texture and Resource Qualification](./tdr-0006-reloadable-texture-resource-roles.md)
@@ -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.
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasius/asset-contracts",
3
- "version": "0.5.0",
3
+ "version": "0.7.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-conversion-diagnostics.js";
42
44
  export {
43
45
  GPU_SHADER_STORE_FEATURE_FLAG,
44
46
  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
+ }