Skip to content

ADR-0030: Playbooks are packaged interpreted scripts against the Vinxi SDK

ADR-0030: Playbooks are packaged interpreted scripts against the Vinxi SDK

Status

Accepted. Refines ADR-0029 §3 (domain language as packaged data) — it does not supersede it; the grammar of 0029 stands.

Date

2026-07-04

Context

ADR-0029 §3 reserved one fixed slot for domain vocabulary — vinxi run <reality>.<playbook> — with playbooks “declared as data” in realities/<product>/playbooks/, marked future. Phases 1–3 shipped; this is the Phase-4 tail.

The question deferred by 0029 was what a playbook is. Real domain flows carry logic: seed-fiber-pon loops over four output legs; a future inject-cut raises an alarm on every ONT downstream of a cut. Encoding logic as a JSON manifest means inventing control flow (loops, parameters, then query-driven steps) inside JSON — a configuration format creeping toward an ad-hoc, badly specified scripting language (Greenspun’s tenth rule). Meanwhile the demo fixtures under vinxi dev already hand-roll an SDK: the create/link closures in dev_cmd.rs. There was a real SDK hiding in the harness.

Decision

A playbook is an interpreted JavaScript ES module, shipped in the reality package, run against a formalized Vinxi SDK.

  • Engine — QuickJS via rquickjs. Real ES2020 with async/promises (SDK verbs are awaited, no block-on wrinkle), ~1 MB, sandboxed. The dependency is confined to vinxi-cli — never referenced from another crate, never from a wasm build graph (there is no Rust wasm target today; the CLI is a leaf binary).
  • The Vinxi SDK is the seam. A scope-bound surface exposing the fixed kernel verbs (create/link today) that maps 1:1 to the syscalls. It is injected into the script as the vinxi object over a JSON-string boundary, so no JS handle is held across an await. Its shape is the seam for the future CLI-as-client-of-vinxi-kernel-server: an HTTP-client implementation swaps in behind the same verbs.
  • Module contract. A playbook exports meta (read for run --list/validation without executing the body) and a default async function (vinxi, args). Its return value becomes the one stdout Report.json; op-counts form the human summary. run --list discovers playbooks by scanning realities/*/playbooks/*.js.
  • Sandbox. The script can touch only vinxi.* and console.log→stderr — no filesystem, network, or clock. Blast radius equals the kernel verbs the operator already holds. A thrown JS error becomes a typed VinxiError; an SDK error is preserved with its code across the boundary.

This does not breach ADR-0029’s invariant. A .js file in the reality package, discovered and interpreted at runtime, is data on disk — not a compiled subcommand; it shares the trust and lifecycle model of the ontology .json documents beside it. The grammar stays closed (vinxi run), vocabulary stays open, nothing domain-specific is baked into the binary. 0029’s letter (“declared as data”) stretches to “packaged interpreted script”; its spirit holds exactly.

Alternatives Considered

  • A JSON manifest DSL (linear steps → args → loops → query-driven steps): the status-quo reading of “data.” Rejected — each increment reinvents a scripting primitive; the terminal form is a JSON workflow language nobody wants to author or maintain.
  • Rhai (pure-Rust embedded scripting): featherweight and sandboxed, but a bespoke language (not JS) and synchronous, forcing a block-on bridge to the async kernel.
  • Embedded Deno/V8: real JS/TS and async-native, but a ~20–50 MB binary embed pulling hundreds of crates — disproportionate for demo/seed macros.
  • Boa (pure-Rust JS): JS syntax at Rhai weight, but less mature with weak async support.
  • Compiled per-product subcommands / plugin binaries: the anti-pattern 0029 exists to prevent.

Consequences

  • seed-fiber-pon is now realities/networkaccess/playbooks/seed-fiber-pon.js (loops intact); vinxi dev seed-fiber-pon is retired. The inject-cut/correlate/prove/seed-trafficure fixtures stay in vinxi dev (they call kernel-core internals that are not verbs).
  • The SDK is write-only (create/link) for this slice — all the seed uses.
  • Deferred and ledgered in TODO.md § “CLI — agent-first grammar”: SDK reads (find/get/trace) → unlocks a dynamic inject-cut playbook; a JSON declarative front (desugaring to the same SDK calls); extracting the SDK to a vinxi-sdk crate + trait (embedded vs. HTTP-client) for CLI-as-client-of-server; import/multi-file playbooks; typed params schema validation; vinxi explain <playbook>.
  • rquickjs adds a small vendored C dependency (built with the system C compiler, no extra toolchain). It is confined to vinxi-cli, so it never enters a wasm build and never bloats another crate.