Skip to content

feat: establish the C++ SDK with typed failures and runtime introspection - #3

Merged
snkmcb merged 2 commits into
mainfrom
feat/cpp-sdk-foundation
Sep 5, 2026
Merged

snkmcb merged 2 commits into
mainfrom
feat/cpp-sdk-foundation

Conversation

@snkmcb

@snkmcb snkmcb commented Sep 5, 2026

Copy link
Copy Markdown
Member

Implements the first slice of P2 in current priorities: Result<T>, the published diagnostic codes, the runtime_info schema, and open.

P0 (compose the vector capability) is blocked outside this repository — ghcr.io/animu-sphere/usd-vector-plugins is not published, so there is no OCI digest to pin — and P1 is sequenced after it, so this takes P2 instead.

Two lanes

Lane Target Needs OpenUSD Provides
core usdgeospatial::core no Result<T>, Diagnostic, runtime_info
usd usdgeospatial::sdk yes open

The split is not cosmetic. Describing a runtime must not require loading it: an application that finds no runtime, or the wrong one, has to be able to say so. runtime_info therefore reads the materialized prefix's own metadata/composition.lock.json rather than asking OpenUSD, which also lets CI check the diagnostics, result, and introspection contracts on a runner with no composed runtime.

What open adds

It returns an ordinary pxr::UsdStageRefPtr — OpenUSD stays reachable below it. What it replaces is the null pointer plus loose TF_ERROR text, by naming each condition separately:

{
  "code": "UGEO-E031",
  "name": "capability_unavailable",
  "category": "asset",
  "subsystem": "sdk",
  "message": "this composition installs no file format for the extension",
  "details": {
    "uri": "boundaries.geojson",
    "extension": "geojson",
    "capability": "usd-fileformat:geojson"
  }
}

The capability detail uses the same name the manifest and the runtime metadata use, so a consumer can report exactly which capability a composition would have to add. GeoJSON is the honest example: it is P0, so this composition genuinely cannot open one, and the SDK's own test asserts that.

Codes are published in docs/reference/diagnostics.md. There is deliberately no generic unknown.

The runtime-info contract is held from both sides

  • sdk/core/tests/test_core.cpp asserts the SDK produces exactly the committed sdk/core/tests/data/runtime-info.json.
  • tests/tooling asserts that same document satisfies schemas/runtime-info.v1.json, agrees with the lock it was read from (including the target decomposition, via runtime_metadata.target_identity), and lists the same codes sdk/core/src/diagnostics.cpp defines.

Neither side can move alone. Drift detection was checked by mutation: a changed toolchain, host Python version, bundled flag, USD version, and a renamed or renumbered code all fail.

tools/sdk_env.py

ost runtime exec replaces the environment, which is what makes an acceptance result a claim about the composition alone. A native executable cannot use it: the composed OpenUSD links the host CPython (usd_python against python313.dll) and this composition deliberately bundles no interpreter, so such a process fails to load before it runs a line. tools/sdk_env.py prepends the same paths instead. ost runtime exec stays the runner for anything whose result is a claim about the runtime.

Verification

Run clean, from a fresh compose of the committed lock:

  • core lane, no OpenUSD anywhere: 80 checks pass
  • USD lane against the composed prefix: ctest 2/2, including opening fixtures/three-points.copc
  • tests/native-consumer against the installed headers and exported CMake package: ctest 2/2
  • python tools/validate_metadata.py: all checks pass; tests/tooling: 15/15

CI gains a sdk-core job. The OpenUSD lane needs a composed prefix and a matching toolchain, so it belongs with acceptance rather than with the per-commit checks.

Known limits

  • inspect and formats are not implemented. formats first needs a decision on whether it reports composed capabilities, OpenUSD's registered extensions, or the intersection — those differ exactly when a plugin fails to load, which is when the answer matters most.
  • The SDK was built and tested with MSVC 14.51, not the msvc143 its target names; no msvc143 toolset was available on the development machine. sdk/README.md states that building with the target's toolchain is the supported configuration.
  • stage_open_failed and runtime_metadata_unreadable have no test that reaches them through a real malformed asset or an unreadable file. Both are recorded in the roadmap.

🤖 Generated with Claude Code

snkmcb and others added 2 commits September 5, 2026 17:00
…tion

Implement the first slice of the SDK that docs/design/sdk.md describes and
docs/roadmap/current.md schedules as P2: Result<T>, the published diagnostic
codes, the runtime_info schema, and open.

The SDK builds in two lanes. The core lane compiles with no OpenUSD present
and carries the diagnostics, the result type, and runtime_info; the OpenUSD
lane carries open. The split is not cosmetic: describing a runtime must not
require loading it, so runtime_info reads the materialized prefix's own
metadata/composition.lock.json and can report a missing or wrong runtime
before any OpenUSD library loads. It is also what lets CI check these
contracts on a runner with no composed runtime.

open returns an ordinary pxr::UsdStageRefPtr. What it adds is a typed failure
in place of the null pointer and loose TF_ERROR text OpenUSD returns, naming
each condition separately: an unhandled URI scheme, a format the composition
does not install (reported as the capability a composition would have to add,
such as usd-fileformat:geojson), an unresolved asset, and a stage OpenUSD
declined to compose.

The runtime-info contract is held from both sides. sdk/core/tests/test_core.cpp
asserts the SDK produces exactly the committed document in
sdk/core/tests/data/runtime-info.json, and tests/tooling asserts that the same
document satisfies schemas/runtime-info.v1.json, agrees with the lock it was
read from, and lists the same codes sdk/core/src/diagnostics.cpp defines.
Neither side can move alone.

Add tools/sdk_env.py, which prepends a composition's search paths instead of
replacing the environment. The composed OpenUSD links the host CPython and
this composition deliberately bundles no interpreter, so a native executable
started under `ost runtime exec` cannot load. That runner stays the one for
anything whose result is a claim about the runtime.

tests/native-consumer now also builds against the installed public headers
through the exported CMake package, so an incomplete install set fails there
rather than in review.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six findings from the review of the SDK, in the order they matter:

to_json promised a document conforming to schemas/runtime-info.v1.json while
validating only one of the fields the schema constrains. A lock missing a
provider digest serialized as `"artifact": ""` and failed the schema at
whoever consumed the output rather than where the cause was known.
from_lock_json now checks all three identities, every component kind, version,
and artifact, and every capability version and artifact, and decompose_target
requires a toolchain that starts with a letter as the schema spells it.

open took its TfErrorMark after the calls that trigger resolver and plugin
registration, so errors from a plugin that failed to load -- the case that
explains a missing capability -- were neither reported nor cleared, and spilled
onto stderr or into a caller's own mark. The mark is now taken before the first
call into OpenUSD and cleared on every path, and the three conditions the SDK
decides for itself carry that text as an `openusd` detail.

The JSON reader converted numbers with std::strtod, which follows LC_NUMERIC: a
host that had called setlocale on a comma-decimal machine would have rejected
any fractional number, and with it the whole lock. Numbers are now scanned by
the JSON grammar, which also rejects malformed values such as `1-2e+3` that the
old scan accepted, and converted through the classic locale.

runtime_info materialized the entire 6.2 MB lock to read twenty fields, on the
path documented as the cheap probe a caller runs before OpenUSD loads. The
parser takes an optional member filter, and runtime_info keeps only the three
top-level members it reads plus the destination of each install entry. Parsing
the committed lock drops from 24.4 ms to 6.1 ms and from roughly a hundred
thousand nodes to about four thousand.

tools/sdk_env.py --print omitted the settings variables that the runner
applies, so it described an environment the runner does not build.

docs/roadmap/current.md listed running the SDK lanes in CI as outstanding while
the same change added the core lane job; only the OpenUSD lane remains.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@snkmcb
snkmcb merged commit 52f22a5 into main Sep 5, 2026
2 checks passed
@snkmcb
snkmcb deleted the feat/cpp-sdk-foundation branch September 5, 2026 12:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant