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.
import init, { WasmEngine, opencodifier_version } from "./wasm/opencodifier_wasm.js";
await init();
const engine = new WasmEngine();
JSON.parse(engine.identity());
// { "engine_semver": "0.4.0", "graph_version": 1, "calibration_version": 0,
// "embedding_model": "none", "model_id": "relational-v1" }
opencodifier_version(); // crate version, independent of the engine identity
| Method | Input | Returns | Notes |
|---|---|---|---|
decide(json) | one request | decision 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 + request | decision 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.
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.
| Route | Body | Returns |
|---|---|---|
POST /v1/decide |
one request | decision (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 request | validation result — no decision is made |
POST /v1/chat/completions |
OpenAI shape | OpenAI 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.
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.
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
# one decision, native format, from stdin
opencodifier decide --input request.json
# the same server the API docs describe
opencodifier serve [--addr 127.0.0.1:8177] [--allow-remote]
# validate a declarative graph
opencodifier graph validate --input graph.json
The decide subcommand accepts --format (native default) exactly
like the HTTP header — one set of adapters, three doors in.
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.