@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.
@@ -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.
@@ -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)
@@ -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.4.6",
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.6",
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
+ }