one canonical IR four surfaces: WASM · HTTP · MCP · CLI stable error-code taxonomy

The API surface.

Every interface over the same canonical decision IR. Wire formats are adapters — native JSON here, OpenAI and Anthropic shapes at the edges — and the runtime's refusals are part of the contract: typed error codes, HTTP 200 abstentions, and per-item isolation in batches. This page documents only what the code actually implements.

JavaScript / WASM

The engine as an ES module

The same binary this site's playground runs. Everything is synchronous after init(); all methods take and return JSON strings, so there is no FFI surface to misuse and no worker required.

WasmEngine methods
MethodInputReturnsNotes
decide(json)one requestdecision JSON The full pipeline on the zero-ML stack. Run it →
decide_batch(json){"requests":[…]}, ≤ 16 {"count", "results":[…]} Shared cache across the batch; each item is {"response"} or {"error"} — one bad item never fails the rest. Same envelope as POST /v1/batch and MCP codify_batch.
validate_graph(json)graph {"version","nodes"} Structural check; cycles are graph.cycle.
run_graph(graph, request)graph + requestdecision JSON Executes a validated graph. A node kind needing a backend this build lacks returns engine.missing_backend — it never silently substitutes.
identity()—identity JSON The exact versions that decide cache keys and reproduce decisions.

Error semantics

Every method is Result<String, JsValue>: failures throw with the standard error envelope as the exception value — the same {"error":{"code","message"}} body the HTTP API returns. There is no third channel: no partial results, no console-only warnings.

What the WASM build does not include

The zero-ML stack (rules → cache → filter → lexical → choice) and the graph engine. Rungs 5–7 — embedding similarity, the decision model, the verifier — require inference backends a browser tab does not ship; graphs that need them return engine.missing_backend. That refusal is the honest behavior, not a gap.

HTTP

Loopback server, decision-shaped routes

opencodifier serve binds 127.0.0.1:8177 by default (--addr to change; any non-loopback bind additionally requires --allow-remote). Request and response bodies are UTF-8 JSON with a 1 MiB cap.

HTTP routes
RouteBodyReturns
POST /v1/decide one requestdecision (or refusal envelope)
POST /v1/batch {"requests":[…]} ≤ 16 {"count","results"} with per-item isolation
POST /v1/graph/validate graph{"version","nodes"}
POST /v1/graph/run {"graph", "request"}decision
POST /v1/validate one requestvalidation result — no decision is made
POST /v1/chat/completions OpenAI shapeOpenAI shape — see below
GET /v1/models —the engine identity as a model row
GET /v1/capabilities —decision kinds, endpoints, limits, cache state, identity
GET /v1/healthz —liveness

Wire formats — the x-opencodifier-format header

Applies to the decision routes. Values: native (default when the header is absent), openai, anthropic, jev. Everything is normalized into the canonical IR before the pipeline sees it; unknown values are refused with schema.invalid_value rather than silently coerced.

OpenAI compatibility route

POST /v1/chat/completions accepts OpenAI-format messages (the concatenated message text becomes the decision state) and answers with a completion whose JSON content — when response_format declares a strict schema — is the typed decision: choice, confidence, and the refusal cases expressed as schema properties. Existing OpenAI-speaking clients get decisions without learning a new envelope; prose-generation fields are refused with schema.unsupported_generation_field, because pretending to generate prose is how wrappers lie.

Status codes

Decisions — including abstentions — are 200. Refusals are 400 with the error envelope; oversize bodies and unknown routes are the framework's own errors. An abstention never masquerades as a failure and a failure never masquerades as a low-confidence decision.

Error taxonomy

Every refusal has a stable code

One envelope everywhere — {"error": {"code": "…", "message": "…"}} — and the same codes across WASM, HTTP, MCP, and CLI. Messages are typed, never an echo of your payload. These are the codes the runtime can emit today:

ir.* — canonical IR validation

empty_field · empty_candidates · duplicate_candidate · too_few_levels · duplicate_level · invalid_policy · invalid_id · invalid_probability · distribution_not_normalized · empty_questions

schema.* — adapter layer

unsupported_construct · unsupported_generation_field · missing_field · invalid_type · invalid_value · limit_exceeded · empty_questions · invalid_json

graph.* — graph engine

cycle

engine.* — execution

missing_backend

http.* — the HTTP layer itself

remote_bind_forbidden · bind_failed · serve_failed — the only three codes the web layer owns; everything else is relayed verbatim from below.

MCP + CLI

Agents and terminals

MCP — six tools, stdin/stdout

opencodifier mcp serve speaks stdio only — no network transport, matching the local-first posture. The tools: codify_decide, codify_batch (the ≤16-request envelope), codify_graph, codify_validate, codify_verify, and codify_explain.

CLI

The decide subcommand accepts --format (native default) exactly like the HTTP header — one set of adapters, three doors in.

Try it

Every shape above runs in your tab

The playground embeds the WASM engine with these exact fixtures preloaded — including the refusals. No server, no key, no network.

Buttons hand the fixture to the playground and jump there — the decision then runs on this site's embedded WASM engine, entirely in your browser.