@plasius/gpu-model-core
v0.4.4
Published
Canonical GPU model document, resource graph, diagnostics, and adapter contracts.
Maintainers
Readme
@plasius/gpu-model-core
Canonical model document, resource graph, diagnostics, and adapter contracts.
This repository is the dedicated package boundary defined by ADR 0094.
Diagnostics, repairs and conversion loss
evaluateGpuModelDiagnostics validates a format adapter's completed report.
It defaults to strict mode, rejecting every standard violation. Tolerant and
forensic modes accept only supported rules with repairApplied: true, which
attests that the adapter performed and verified the exact action exposed by
GPU_MODEL_REPAIR_RULES. It does not parse a model or perform repairs.
Continue to validate the repaired document with validateGpuModelDocument.
import { evaluateGpuModelDiagnostics } from "@plasius/gpu-model-core";
const report = evaluateGpuModelDiagnostics({
standard: "gltf-glb",
mode: "tolerant",
issues: [{
kind: "violation",
ruleId: "missing-normals",
code: "NORMAL_REQUIRED",
path: "/meshes/0/primitives/0/attributes/NORMAL",
message: "Required normals were missing",
originalValue: null,
repairApplied: true, // Adapter already generated and validated normals.
}],
});
// report.accepted === true; report.repairs contains the complete repair record.Blocking errors, repairs, unsupported data, warnings and loss are separate
kind variants. Unknown rules and unsupported data block every mode. Repairs
preserve originalValue, action, severity, affectedPath, confidence, and
ruleId. The supported profiles are gltf-glb, obj-mtl, fbx, and cad-bim;
OBJ cannot use the skin-weight repair rule. The exported policy text records
the upstream rule; executable contract actions deliberately narrow ambiguous
fallbacks: invalid primitives are dropped (never clamped), unavailable textures
remain unresolved, and malformed material references use the canonical default.
These three fallbacks require a kind: "loss" issue at the same path. Each loss
states effect: "dropped" | "approximated"; any loss blocks acceptance unless
allowLoss: true is explicitly supplied, including in strict mode.
Forensic mode requires rawSource, a relevant, scrubbed JSON snapshot. Other
modes reject that field to prevent unintended retention. Source evidence is
separate from ordinary diagnostics and remains available even when conversion
is rejected. Represent source NaN/Infinity as descriptive strings, not numbers.
Never include credentials, signed URLs or unrelated personal data, and never
log raw evidence or repair snapshots. The package performs no logging, I/O,
persistence or credential detection. Use serialized JSON as the boundary for
untrusted executable producers; JavaScript Proxies are not inert data.
GPU_MODEL_DIAGNOSTIC_LIMITS bounds the entire input to 1 MiB of JSON UTF-8,
4096 issues, 16384 JSON values, depth 12, 1024 members per evidence container
and 65536 UTF-16 units per string. All ceilings apply together. Optional
maxEvidenceBytes lowers the aggregate byte ceiling, including report fields
and the limit itself. Overflow, unknown keys, getters, cycles, sparse arrays,
exotic objects and malformed reports throw a static GpuModelDiagnosticsError
without echoing caller data. Successful output is deeply frozen and owned by
the package; caller objects remain mutable and unchanged.
The parent gpu.model.conversion.enabled flag governs adapter adoption. Keep
it disabled until adapter-specific conformance tests pass; rollback disables
the flag and restores strict mode. This API changes no stored flag and adds no
UI capability. Synthetic contract fixtures cover glTF, OBJ, FBX and CAD reports;
actual parsing and geometry verification belong to their format adapters.
See ADR-0008.
Canonical conversion resource graph
createGpuModelResourceGraph projects a privately verified GpuModelDocument
into a frozen dependency graph for resources, accessors, textures, materials,
skeletons (including joints), skins, animations, provenance and conversion
packages. It reuses the canonical values and immutable Blob payloads. Material
bindings retain samplers and intended usage; texture values retain their hashes,
MIME types, paths, colour spaces and export embedding policy.
import { createGpuModelResourceGraph, gpuModelResourceKey,
type GpuModelDocument } from "@plasius/gpu-model-core";
declare const document: GpuModelDocument; // From the verification factory.
const graph = createGpuModelResourceGraph(document, {
resources: document.resources.map(resource => ({
resourceId: resource.id,
location: { kind: "embedded" },
exportEmbeddingPolicy: "inherit-source",
})),
packages: [{ id: "conversion", members: document.resources.map(resource => ({
kind: "resource", id: resource.id,
})) }],
});
const conversion = graph.nodes.find(node =>
node.key === gpuModelResourceKey("package", "conversion"));Supply exactly one binding per document resource. For external origins use
location: { kind: "external", relativePath: "textures/albedo.png" }; the bytes
must already have been resolved and verified. Paths use portable ASCII letters,
digits, dots, underscores, hyphens and slash separators, with no absolute paths,
traversal, URL syntax, device names or trailing dots. Components are at most 255
characters and paths at most 1024. Case-insensitive duplicate paths and file versus
directory collisions fail. Descriptors do not authorize network/file access.
Resource export policies are inherit-source, prefer-embedded,
prefer-external and forbid-embedding. The last requires an external origin.
Exporters must satisfy resource and texture restrictions together, resolve
preferences for the target, enforce destination-root confinement and validate
their actual output. The graph neither writes packages nor proves export success.
Asset catalogue admission and promotion remain owned by @plasius/asset-contracts.
gpuModelResourceKey(kind, id) encodes a JSON tuple, independent of declaration
order and locale. Logical IDs are distinct from byte-content hashes. Nodes and
dependency keys are sorted; joint/animation sample order stays intact. Package
members are typed references, may include packages, and must form a DAG. Missing,
repeated or cyclic references fail with fixed GpuModelResourceGraphError.code
values. Canonical document validation still owns scene and semantic references.
Animation target associations remain in canonical values, outside dependency
edges. This is a resource dependency graph, not a replacement scene graph.
GPU_MODEL_RESOURCE_GRAPH_LIMITS caps 65536 nodes, 262144 dependency declarations
(including repeated semantic dependencies before deduplication), and 256 packages.
These ceilings may reject a large otherwise-valid canonical document. Construction
performs no I/O, retries, logging or hashing; input accessors and malformed
containers fail closed. Do not treat executable JavaScript Proxies as inert input.
isGpuModelResourceGraph recognizes only locally constructed graphs. Structured
clones must be rebuilt from a reverified document. The graph retains authored
metadata and provenance: it is not a redaction boundary or a public logging format.
Adoption inherits the remotely evaluated gpu.model.conversion.enabled flag;
rollback disables conversion adoption and pins the prior package release. No
stored flag or UI capability changes here. See
ADR-0009.
Implementation tracker
The canonical conversion architecture is defined by ADR 0094 in plasius-ltd-site. The package implementation work is split into Project-tracked Tasks:
- Bootstrap package boundary
- Canonical GPU model document schema
- Diagnostics and repair contracts
- Canonical model resource graph
- Adapter capability and conversion registry contracts
- Verified static-model document for the ChatGPT PVOX demo
The general package Tasks inherit gpu.model.conversion.enabled from Feature
#1148 and remain independent of format-specific loaders and renderers. Task #13
is an additive dependent slice under Feature #2012 and also requires
asset.pipeline.pvox-models.enabled before its compiler projection can run.
Canonical document v1
GpuModelDocument is the immutable, renderer-neutral hand-off between source
format adapters, model processing, and render bridges. Raw resources cross an
asynchronous verification boundary before document construction: an injected
worker port streams every Blob through payload inspection and SHA-256 digest
verification. Only privately attested resources are accepted by the synchronous
factory. This prevents a shape-compatible object, forged digest string, or
structured clone from claiming that bytes were verified.
import {
CANONICAL_GPU_MODEL_COORDINATE_SYSTEM,
GPU_MODEL_DOCUMENT_SCHEMA_VERSION,
createAndVerifyGpuModelDocument,
type GpuModelResourceVerificationPort,
} from "@plasius/gpu-model-core";
declare const untrustedAdapterOutput: unknown;
declare const verificationPort: GpuModelResourceVerificationPort;
const document = await createAndVerifyGpuModelDocument(
untrustedAdapterOutput,
verificationPort,
);
document.schemaVersion === GPU_MODEL_DOCUMENT_SCHEMA_VERSION; // true
document.coordinateSystem === CANONICAL_GPU_MODEL_COORDINATE_SYSTEM; // trueAdapter output includes roots, nodes, verified resources, accessors, meshes, materials, first-class texture fidelity nodes, distinct skeletons, joints and skins, blend shapes, animation clips, analytic geometry, bounds, provenance, diagnostics, and bounded JSON metadata. Material texture transforms, colour space, usage, extension payloads, authored rig weights, clip timing, and source metadata survive the canonical boundary.
Verification applies non-raiseable defaults of 100 MiB per resource, 164 MiB
aggregate resources, 64 MiB aggregate images, and 4096 pixels per image axis.
It verifies detected MIME and image dimensions before hashing that image, then
constructs the returned Blob from the exact digest-verified byte snapshot.
One internal deadline signal is propagated to both worker-port calls and every
stream chunk. Re-verifying an attested resource or document is idempotent, but
still honours cancellation and re-applies any tighter byte/image/aggregate
limits using retained inspection evidence.
The document validator then reads only the private snapshot to reject
non-finite accessor data, false min/max claims, out-of-range indices and skin
joint indices, mismatched weights, texture bindings whose required
TEXCOORD_n is absent from a material-using primitive, and bounds that do not
match canonical POSITION bytes under affine world transforms. Payload,
index, and instanced world-geometry work has aggregate ceilings; accessor
evidence, mesh primitive/position indexes, and repeated position/index scans
are cached within one validation. Scene ancestry uses one bounded interval
index rather than per-joint parent walks. Blend-shape delta/accessor work is
bounded across the document and reuses the verified payload evidence.
createGpuModelDocument remains available for trusted composition code that
already holds privately verified resources. isGpuModelDocument narrows only
values returned by a factory; canCreateGpuModelDocument is the non-narrowing
probe. A structured clone deliberately loses private attestation and must pass
through createAndVerifyGpuModelDocument again. Format parsing, renderer
uploads, catalogue rights, promotion identity, tolerant repair, and target loss
reporting stay outside this Task's package boundary.
Bounded static PVOX demo projection
Task #13 adds a deliberately narrower compiler hand-off without changing the
general document owned by Task #3. Consumers first create a privately verified
GpuModelDocument, evaluate the remote
asset.pipeline.pvox-models.enabled flag, and then request a compiler-safe
world-triangle projection:
import {
createGpuModelStaticDemoCompilerInput,
type GpuModelDocument,
} from "@plasius/gpu-model-core";
declare const verifiedDocument: GpuModelDocument;
declare const pvoxModelsEnabled: boolean; // remote flag evaluation
const compilerInput = await createGpuModelStaticDemoCompilerInput(
verifiedDocument,
{ pvoxModelsEnabled },
);
compilerInput.canonicalDocumentHash; // SHA-256 of canonical document bytes
compilerInput.worldTriangles; // verified positions, normals, bounds and regionsThe profile accepts at most 200,000 explicit world-space triangles, 4,096
nodes, 16,384 primitives, 4,096 fixed-factor materials, and 16 MiB of verified
buffer resources. Every world-space coordinate additionally has a fixed,
non-raiseable magnitude ceiling of 1,048,576 metres, with consistent axis
extent and diagonal bounds. The compiler also checks every computed triangle
coordinate against that ceiling; security bounds use tight absolute equality
instead of coordinate-relative tolerance. It accepts only rigid triangles, opaque texture-free
metallic-roughness or unlit materials, invertible transforms, and geometry
whose verified bounds prove metre/Y-up/-Z-forward/floor-centred normalization.
Skins, animation, morphs, analytic geometry, images, texture bindings, custom
material workflows, strips, lines, and points fail closed.
encodeGpuModelDocumentCanonical sorts object keys while preserving semantic
array order. Verified resource bytes are bound through their privately checked
SHA-256 identities, so canonical encoding does not duplicate source payloads.
hashGpuModelDocumentCanonical provides the stable whole-document identity.
The projection retains safe source/converter hashes and material-region IDs,
but deliberately excludes source URLs and arbitrary metadata from the PVOX
compiler surface.
The dependency flow is:
strict source adapter
-> createAndVerifyGpuModelDocument (Task #3 / Feature #1148)
-> createGpuModelStaticDemoCompilerInput (Task #13 / Feature #2012)
-> @plasius/gpu-model-voxelThis bounded demo contract does not implement provider acquisition, general source repair, native sparse GPU traversal, deformation, destruction, or the full production Partner-to-PVOX acceptance plan. See the static demo compiler-input design.
Rollout
- Canonical-document feature flag:
gpu.model.conversion.enabled - Static PVOX demo feature flag:
asset.pipeline.pvox-models.enabled - Capability: none for this package-only layer
- Rollback: disable
asset.pipeline.pvox-models.enabledto stop new demo projections, disablegpu.model.conversion.enabledfor the broader conversion boundary, and keep consumers pinned to the last validated package release
Development
Requires Node.js 24 and npm.
npm ci
npm run typecheck
npm test
npm run lint
npm run build
npm run pack:checkLicense
Apache-2.0. See LICENSE, SECURITY.md, and the files under legal/.
Release integrity
CI keeps the administrative contributor registry outside Git and npm package
artifacts using normalized path checks and sealed-tar revalidation. The
GitHub-hosted Node.js 24.18.0 release path follows the released
@plasius/schema v1.4.2 template: release metadata lands through a protected
pull request, exact-main CI must pass, and the immutable tarball is published
through npm OIDC. Package-specific adaptations are limited to GitHub-hosted CI
while the organisation runner is unavailable, current Node-24-compatible action
majors with best-effort Codecov CLI upload, and a protected-merge retry when
repository auto-merge is unavailable. Version 0.1.0 may use the explicit,
time-limited bootstrap_first_publish production gate only while the package is absent;
that credential is removed after the npm trusted publisher binding is active.
Adapter contracts and canonical conversion
ModelAdapter exposes metadata, inspect, load, validate and export.
Register one explicit lowercase format ID per adapter using
createModelConversionRegistry. Core has no concrete format parsers, IO
resolvers, renderer dependencies, format sniffing, discovery or caches. Those
belong to the dedicated format/runtime packages; inject adapters after a runtime
has checked the remote gpu.model.conversion.enabled flag. Disable that flag
and pin the prior public version to roll back adoption.
import {
createModelConversionRegistry, type ModelAdapter,
type ModelSource, type ModelTarget,
} from "@plasius/gpu-model-core";
// Applications supply installed, conformance-tested adapters.
async function convert(sourceAdapter: ModelAdapter, targetAdapter: ModelAdapter) {
const registry = createModelConversionRegistry([sourceAdapter, targetAdapter]);
const source: ModelSource = {
kind: "uint8-array", bytes: new Uint8Array([1, 2, 3]),
fileNameHint: "synthetic.input",
};
const target: ModelTarget = { kind: "array-buffer" };
const result = await registry.convert({
sourceFormat: sourceAdapter.metadata.formatId,
targetFormat: targetAdapter.metadata.formatId,
source, target,
}, { mode: "strict", allowLoss: false, timeoutMs: 30_000 });
if (!result.accepted) return result.diagnostics;
return result.exported;
}Conversion calls source load, checks the privately verified canonical document,
records target capability losses, calls target validate(document), and finally
calls target export. Same-format conversion follows the same route. It never
calls inspect automatically, so a single-use stream is not consumed twice.
registry.inspect, load, validate and export also enforce the boundary
individually; direct export includes canonical capability/validation preflight.
Unsupported formats or IO kinds are rejected before adapter work. There is no
public direct-format converter hook, and extra registration keys are rejected.
Use plain adapter objects with the four operations as own methods.
The following are the supported declarative forms. IO hints remain optional; paths, URLs and stream handles confer no authorization from core. The injected runtime must enforce filesystem roots, network allowlists, size limits while streaming, credentials, output overwrite policy, and atomic staging/commit. Never log headers or signed URLs; scrub adapter diagnostic/evidence text before calling the result factory. Core produces fixed error codes with no input or exception causes, but it cannot identify secrets embedded in arbitrary adapter messages.
import type { ModelSource, ModelTarget } from "@plasius/gpu-model-core";
const sources: ModelSource[] = [
{ kind: "file-path", path: "/models/synthetic.input" },
{ kind: "url", url: "https://example.invalid/model", headers: { Accept: "application/octet-stream" } },
{ kind: "blob-storage-url", url: "https://example.invalid/model", credentialMode: "runtime-resolved" },
{ kind: "blob", blob: new Blob(["synthetic"]), fileNameHint: "synthetic.input" },
{ kind: "array-buffer", bytes: new ArrayBuffer(4) },
{ kind: "uint8-array", bytes: new Uint8Array(4), mimeTypeHint: "application/octet-stream" },
{ kind: "stream", stream: new ReadableStream<Uint8Array>(), byteLengthHint: 4 },
];
const targets: ModelTarget[] = [
{ kind: "file-path", path: "/exports/synthetic.output", overwrite: false },
{ kind: "blob-storage-url", url: "https://example.invalid/output", contentType: "application/octet-stream", metadata: { purpose: "synthetic" } },
{ kind: "blob", mimeType: "application/octet-stream", fileName: "synthetic.output" },
{ kind: "array-buffer" },
{ kind: "uint8-array" },
{ kind: "stream", stream: new WritableStream<Uint8Array>() },
{ kind: "package", packageFormat: "zip", destinationHint: "synthetic.zip", includeSourcePayload: false },
];HTTP(S) URLs with inline username/password are rejected. Storage descriptors
support anonymous, signed and runtime-resolved credential modes; values are
passed only to the injected adapter. Browser Web streams and Node
AsyncIterable<Uint8Array> readable / structural write+end writable streams
are supported without importing Node types into the public declarations.
Memory input bytes are copied; Blob values are immutable, and streams remain
opaque capabilities. Direct memory descriptors are capped at 64 MiB. Larger
models should use streams with runtime-enforced budgets and worker isolation.
Adapters construct their shared envelope with createModelResultBase:
import { createModelResultBase, type ModelOperationOptions } from "@plasius/gpu-model-core";
function completedEvidence(options: ModelOperationOptions = {}) {
const mode = options.mode ?? "strict";
return createModelResultBase({
standard: "gltf-glb", // Use the adapter's actual diagnostic profile.
mode, allowLoss: options.allowLoss ?? false, issues: [],
...(mode === "forensic" ? { rawSource: { synthetic: true } } : {}),
});
}
// load: { ...completedEvidence(options), document: verifiedDocument }
// inspect: { ...completedEvidence(options), capabilities: metadata, detectedFormat: metadata.formatId }
// validate: { ...completedEvidence(options), valid: true }
// export: { ...completedEvidence(options), target: target.kind, output: optionalMemoryPayload }The factory reuses evaluateGpuModelDiagnostics; its immutable report is privately
recorded. Spread the returned base unchanged into an operation result. Structural
clones and invented report/projection arrays are rejected. warnings projects
warning-severity diagnostics, repairs retains completed repair evidence, and
lossReports retains dropped/approximated fidelity. The registry checks the
caller's mode and loss consent independently, so an adapter cannot grant itself
permission. Unknown/unsupported semantics remain blocking in all modes.
ModelLoadResult.document is optional only for refused operations; accepted
loads require a privately verified document from this core module instance.
ModelExportResult.output optionally carries Blob, ArrayBuffer or Uint8Array
matching the selected memory target. A ResourcePackageManifest describes
portable relative paths, roles and content types; duplicate identities/paths and
unsafe paths are rejected. It is not proof of output bytes or promotion.
Canonical resource identity remains in GpuModelDocument and the separately
available resource graph. Cross-worker/module clones must be reverified and
reports rebuilt before registry use.
Registry results retain per-stage diagnostic reports, preserving different source
and target profiles, plus aggregate diagnostic/repair/loss arrays. The synthetic
capabilities stage uses the core gltf-glb report profile solely as a container
for format-neutral loss records; it applies no format-specific repair rules.
It reports animation, rig/skin and analytic-geometry downgrade when the target's
capability flags cannot preserve those values. Other prospective losses must be
reported by target validation. Adapters must stage writes and reject unconsented
loss before committing; core cannot undo an injected adapter's side effects.
Always check accepted, including after export, and do not equate a manifest or
adapter result with independently verified publication.
Each registry allows at most 128 adapters and eight concurrent operations
(configurable downward with maxConcurrent). Each operation or whole conversion
has a 30-second default deadline, configurable up to five minutes, and supports
AbortSignal. No retries occur. Cancellation and timeouts stop subsequent stages;
an unresponsive operation keeps its capacity slot until the underlying promise
settles. Synchronous parser code cannot be preempted, so isolate untrusted code
in runtime workers. Descriptor/report/manifest limits bound core work; raw
JavaScript Proxies and injected adapter/stream implementations are trusted code,
not a sandbox boundary.
The IO examples are covered by tests/adapter-registry.test.ts; all TypeScript
blocks in this section are additionally compiled against the built public
package during release verification. See ADR-0010
for compatibility with the initial site design and package ownership boundaries.
