Skip to documentation

SDK and error contracts

Runbook

Runbook What red looks like, captured for real. Every failure below was produced by breaking a scratch copy of this repository and running the gate — nothing here is imagined output. Make such a copy the same way when you need to reproduce

Runbook

What red looks like, captured for real. Every failure below was produced by breaking a scratch copy of this repository and running the gate — nothing here is imagined output. Make such a copy the same way when you need to reproduce one:

SCRATCH=$(mktemp -d)
git archive HEAD | tar -x -C "$SCRATCH"
cd "$SCRATCH"

The gate is one command, node ci/check.mjs; the healthy run is in walkthrough-conformance. Each section: symptom, meaning, action.

A runtime drifted from the catalogue

The scratch copy's js/codes.mjs was edited by hand — rate_limit's severity changed from warning to error, the exact class of defect five of six hand-kept copies developed in the wild. The gate, in full:

$ node ci/check.mjs
FAIL  generated code matches the catalogue
        unchanged  rust/src/codes.rs
unchanged  python/wisent_errors/codes.py
DRIFTED    js/codes.mjs
unchanged  swift/Sources/WisentErrors/Codes.swift

generated code does not match the catalogue; run codegen/generate.mjs
ok    schema and catalogue name the same codes
ok    schema and catalogue name the same severities
ok    the failure point pattern is stated once
ok    every golden envelope obeys the schema
FAIL  runtimes agree
        ok    credential-refused  (js, python, rust, swift)
FAIL  throttled
  expected  {"failure_point":"brama.dispatch.bounded-rotation","error_code":"rate_limit","service":"brama","impact":"one model request","severity":"warning","retryable":true,"outage":false,"detail":"all bounded 'codex' credentials unavailable for agent"}
  js        {"failure_point":"brama.dispatch.bounded-rotation","error_code":"rate_limit","service":"brama","impact":"one model request","severity":"error","retryable":true,"outage":false,"detail":"all bounded 'codex' credentials unavailable for agent"}
ok    store-unreachable  (js, python, rust, swift)
FAIL  nested-refusal
  expected  {"failure_point":"brama.dispatch.bounded-rotation","error_code":"rate_limit","service":"brama","impact":"one model request","severity":"warning","retryable":true,"outage":false,"detail":"all bounded 'claude-code' credentials unavailable for agent","cause":{"failure_point":"brama.gateway.oauth-refresh","error_code":"auth","service":"brama","impact":"one credential refresh","severity":"warning","retryable":false,"outage":false,"detail":"invalid_grant -- Refresh token not found or invalid"}}
  js        {"failure_point":"brama.dispatch.bounded-rotation","error_code":"rate_limit","service":"brama","impact":"one model request","severity":"error","retryable":true,"outage":false,"detail":"all bounded 'claude-code' credentials unavailable for agent","cause":{"failure_point":"brama.gateway.oauth-refresh","error_code":"auth","service":"brama","impact":"one credential refresh","severity":"warning","retryable":false,"outage":false,"detail":"invalid_grant -- Refresh token not found or invalid"}}
ok    silent-skip  (js, python, rust, swift)
ok    unattributed  (js, python, rust, swift)
ok    quiet-refusal  (js, python, rust, swift)
ok    single-segment  (js, python, rust, swift)
ok    deep-segment  (js, python, rust, swift)
ok    salvaged-malformed  (js, python, rust, swift)
ok    salvaged-absent  (js, python, rust, swift)
TABLE DIFFERS  python vs js
  python    code=rate_limit	severity=warning	retryable=true	outage=false	http_status=429	exit_code=69	operator_summary=an upstream is throttling us
  js        code=rate_limit	severity=error	retryable=true	outage=false	http_status=429	exit_code=69	operator_summary=an upstream is throttling us
TABLE DIFFERS  rust vs js
  rust      code=rate_limit	severity=warning	retryable=true	outage=false	http_status=429	exit_code=69	operator_summary=an upstream is throttling us
  js        code=rate_limit	severity=error	retryable=true	outage=false	http_status=429	exit_code=69	operator_summary=an upstream is throttling us
TABLE DIFFERS  swift vs js
  swift     code=rate_limit	severity=warning	retryable=true	outage=false	http_status=429	exit_code=69	operator_summary=an upstream is throttling us
  js        code=rate_limit	severity=error	retryable=true	outage=false	http_status=429	exit_code=69	operator_summary=an upstream is throttling us
ok    derived vocabulary  (45 rows, js, python, rust, swift)

11 case(s) + 45 vocabulary row(s), 5 failing

2 check(s) failed
$ echo $?
1

Reading it: DRIFTED js/codes.mjs names the file whose bytes are no longer what the catalogue derives; the FAIL <case> pairs show exactly which bytes differ and in which runtime (only the golden cases carrying a rate_limit envelope fail — severity":"error" where the column says "warning"); the TABLE DIFFERS triplets show the drifted vocabulary row against every other runtime. One caveat: the trailing ok derived vocabulary line records only that all four runtimes dumped a table — the TABLE DIFFERS lines above it are the verdict, and they are counted in 5 failing.

Action. A generated file is never edited by hand. If the drift was an accidental edit, git checkout -- <file>. If you meant to change the vocabulary, change catalogue/codes.json, run node codegen/generate.mjs (all four targets print written), update the golden column in tests/conformance/cases.tsv for affected cases, and rerun the gate.

The gate says DRIFTED, everything else is green

Same first check as above with the harness still passing — the drift is in a spot no golden case exercises (a comment, an operator summary). The meaning and the action are the same: the generator is the only writer of those files.

Schema and catalogue disagree

The scratch copy's schema/failure.schema.json gained a fourth severity:

$ node ci/check.mjs
ok    generated code matches the catalogue
ok    schema and catalogue name the same codes
FAIL  schema and catalogue name the same severities
        catalogue warning,error,critical vs schema warning,error,critical,fatal
ok    the failure point pattern is stated once
ok    every golden envelope obeys the schema
ok    runtimes agree (11 case(s) + 45 vocabulary row(s), 0 failing)

1 check(s) failed

A schema that disagrees with the table it validates is a second source of truth. The same check compares the code enums and the failure-point pattern (the failure point pattern is stated once).

Action. The catalogue is the source of truth; make the schema match it, never the reverse — unless the vocabulary change is intentional, in which case both files change in one commit and the golden cases with them.

RUNTIME MISSING

On a machine without the Rust and Swift toolchains (captured with a stripped PATH on a fresh scratch copy — no target/, no .build/):

$ node tests/conformance.mjs
could not build the Rust emitter -- the Rust runtime will be reported missing:
  spawnSync cargo ENOENT
could not build the Swift emitter -- the Swift runtime will be reported missing:
  error: command Applying debug entitlements to ./.build/arm64-apple-macosx/debug/emit failed: unable to spawn process 'codesign' (No such file or directory)
rust: could not run -- spawnSync /private/tmp/wisent-errors-scratch.9eLB/target/debug/emit ENOENT
swift: could not run -- spawnSync /private/tmp/wisent-errors-scratch.9eLB/.build/debug/emit ENOENT
RUNTIME MISSING  rust
RUNTIME MISSING  swift
ok    credential-refused  (js, python)
ok    throttled  (js, python)
ok    store-unreachable  (js, python)
ok    nested-refusal  (js, python)
ok    silent-skip  (js, python)
ok    unattributed  (js, python)
ok    quiet-refusal  (js, python)
ok    single-segment  (js, python)
ok    deep-segment  (js, python)
ok    salvaged-malformed  (js, python)
ok    salvaged-absent  (js, python)
TABLE MISSING  rust -- spawnSync /private/tmp/wisent-errors-scratch.9eLB/target/debug/emit ENOENT
TABLE MISSING  swift -- spawnSync /private/tmp/wisent-errors-scratch.9eLB/.build/debug/emit ENOENT

11 case(s) + 45 vocabulary row(s), 4 failing

A missing runtime is a failure, not a skip — deliberately. The could not build preamble says which fact about the machine is wrong (spawnSync cargo ENOENT: cargo is not installed or not on PATH; a codesign or SDK error: the Swift toolchain is broken); the ok lines still prove the runtimes that could run agree with the golden column, so a partial machine still gives partial evidence.

Action. Install the missing toolchain (node, python3, cargo, swift are the full set) and rerun. Never conclude anything from a run with MISSING lines except that those runtimes were not checked.

The guard reports sites in a consumer tree

$ node ci/no-handrolled-envelope.mjs /tmp/toy-shelf
2 site(s) name an envelope key outside the package:
  /tmp/toy-shelf/serve.mjs:14  failure_point: 'shelf.store.read',
  /tmp/toy-shelf/serve.mjs:15  error_code: 'store_down',
...

Sites, not verdicts. Read each line: a hand-built envelope gets migrated (walkthrough-adoption); a field declaration, a parsed operator-visible log line, or another vendor's own error_code (Azure has one) is a legitimate answer and stays. The full contract is in reference/tools.

A strict builder throws in production

TypeError: failurePoint "..." is not a dotted lowercase path (or the Python/Rust/Swift equivalent — the per-runtime references list every exact sentence) surfacing from an error path means a strict builder is running where only the salvage builder is safe. That is the one place the fix is in the consumer, not the data: use failureOrFallback / failure_or_fallback / Failure::or_fallback / Failure.orFallback there, and the violation will travel as a wisent_errors.* context note instead of an exception thrown mid-diagnosis (integration, step 4).

An envelope arrives with wisent_errors.* context keys

Not an error in the envelope — the envelope working as designed: some call site fed the salvage builder a malformed point, an off-catalogue code, or an empty service, and the violation traveled in the data. The keys name the defect (wisent_errors.failure_point: malformed, wisent_errors.error_code: "off-catalogue: panic", wisent_errors.service: absent); the failure_point is kept verbatim so you can find the call site. Fix the call site; the envelopes it already emitted stay honest.

Where the reporter's silence is the symptom

Swift desktop components report through WisentFailureReporter, which never fails the caller — so a missing report has exactly three causes: the intake pair unset and the token file unreadable (the no-op path), the intake unreachable within its five-second timeout, or the intake refusing the POST. All three are invisible to the app by design. Check the configuration resolution order in configuration, then the intake's own logs — a toy intake capture for comparison is in integrate/swift.