# SIMULATION.md, version 1

A plain file that describes mock tools, how they can fail, and the rules a run must keep, plus the engine behavior that makes the same file and seed produce the same log on any JavaScript engine. Draft, version 1. Nobody has adopted this yet. This text is CC BY 4.0, and the code is MIT. Everything here is a mock. Use at your own risk: see the [terms](https://simulation.md/terms).

**What a result is not.** A result describes the assumptions in one file. It is not a prediction, a measurement, a test of any real system, an audit, a certification, a safety assessment or advice. A replay hash shows only that a file and seed produce a given log. A file that checks clean is well formed, which says nothing about whether it is realistic. Implementations must not describe a run as passing, verified, certified or safe.

## 1. The file

A file has three parts: frontmatter, prose for people, and exactly one fenced JSON block whose info string is `json simulation`.

````markdown
---
simulation: 1
kind: plan
name: refund-retry
---

# Refund with retries

Prose for people. The engine never reads it.

```json simulation
{ "tools": { "refund": { "kind": "write", "pack": "money" } }, "steps": [{ "call": "refund" }], "rules": [] }
```
````

| Frontmatter | Meaning |
| --- | --- |
| `simulation` | Format version. Must be `1`. |
| `kind` | `plan` (short form), `world` (full form) or `exam` (a fixed test world). |
| `name` | Lowercase letters, digits, dots, dashes and underscores. |

Save it as `SIMULATION.md`. A file may be at most 256 KB. Hosted routes accept less: see section 11.

## 2. Plans: the short form

A plan names tools, picks failures from a catalog, and lists steps. It compiles to a world (section 3).

| Key | Meaning |
| --- | --- |
| `state` | Starting values, by name. |
| `tools` | Each tool: `kind` (`write`, `read`, `event`, `approval`, `model`), `pack` or `faults`, optional `chances`, `effect`, `latency`, `returns`, and `stale`, `partial`, `planted` for what a faulted read returns. |
| `steps` | One plan. Or give `a` and `b`, each with `name` and `steps`, to compare two. |
| `rules` | Each: `id`, `holds` (an expression), optional `final` (check only at the end), optional `says`. |
| `measure` | Named expressions reported at the end of each run. |
| `horizon` | Stop at this virtual time, in seconds. Default 86400. |

**Effects.** `"+expr"` adds to a state value and `"=expr"` sets it. A fault changes how an effect applies: `ghost_success` applies it and reports a timeout, `duplicate_delivery` applies an add twice, and `partial_write` applies half of an add and reports an error.

**Steps.**

| Step | Meaning |
| --- | --- |
| `{"call": name, "args": {...}}` | Call a mock tool. The result is `last`. |
| `... "retry": {"max": n, "wait": d, "after_fail": [...]}` | Repeat until the call works or `max` tries are used. `wait` is seconds or a distribution. `after_fail` runs after each failure. |
| `... "key": true` | Call with an idempotency key. The compiler models a service honoring it, and adds the `key_expired` fault so that remains an assumption. |
| `{"stop_if": expr}` | Inside `after_fail`: stop retrying if this holds. |
| `{"wait": d}` | Let time pass. |
| `{"set": {...}}` | Change state. |
| `{"if": expr, "then": [...], "else": [...]}` | Branch. |

**Assumptions.** Compiling lists every chance and every numeric starting value with an `id`, such as `refund.ghost_success`, a `value` and a `source`. The source is `typed` when you wrote the value and `placeholder` when it came from the catalog.

## 3. Worlds: the full form

| Key | Meaning |
| --- | --- |
| `simulation` | Must be `1`. |
| `state` | Starting values. |
| `resources` | Named capacities: `{"dock": {"capacity": 4}}`. |
| `tools` | Each has `branches`, tried in order. |
| `processes` | Each has `steps` and optional `count`, `every`, `start`, `done`. |
| `invariants` | Rules: `id`, `holds`, optional `final`, optional `says`. At most 256. |
| `metrics` | Named expressions reported at the end. |
| `horizon` | Stop at this virtual time. |

**Branches.** A branch has optional `if`, `p`, `latency`, `error`, `returns`, `set`, `label` and `fault`. The engine skips branches whose `if` is false, then takes each remaining branch's `p` from what is left of 1. A branch with no `p` takes the remainder and ends the list. If chances are left over, the call fails with `unhandled`. `p` must be a number from 0 to 1. `set` may use `{"add": distribution}`.

**Process steps.** `delay`, `seize`, `release`, `set`, `if`/`then`/`else`, `call`/`args`, and `repeat` with `max`, `until` and `steps`.

**Distributions.** A number, an expression string, or `{"dist": "fixed", "value"}`, `{"dist": "uniform", "min", "max"}`, `{"dist": "exp", "mean"}`, `{"dist": "normal", "mean", "sd"}` or `{"dist": "lognormal", "mean", "sd"}`, where a lognormal's mean and sd describe the value itself.

## 4. Expressions

Total and side-effect free. Numbers, strings in `"` or `'` with no backslashes, `true`, `false`, `null`, `+ - * / %`, comparisons, `&& || !`, `? :`, brackets, and the functions `min max abs floor ceil round sqrt ln exp`.

Names are checked before a run, so a typo is an error (`E_PATH`) and can never make a rule silently hold:

| Name | Where |
| --- | --- |
| `state.x` | Anywhere. `x` must be declared in `state` or set somewhere in the file. |
| `now` | Anywhere: virtual seconds. |
| `args.x`, `caller` | In a tool's branches. |
| `last.ok`, `last.value`, `last.error`, `attempt`, `id` | In process steps. |
| `res.r.utilization`, `.mean_wait`, `.waits`, `.queue` | In metrics and `final` rules only. `mean_wait` includes anyone still queued. |

Any character outside this grammar is an error. Arithmetic or comparison on a list or object is an error (`E_TYPE`). Strings may not grow past 4,096 characters.

In `set`, `args` and `returns`, a string that starts with `=` is an expression.

## 5. How the engine runs a file

Everything is a pure function of the file, the seed and the actions. There is no clock, no network, no filesystem and no evaluation of code.

**Numbers.** Only `+ - * /`, `sqrt` and integer operations feed results, because IEEE 754 fixes those exactly. `ln` and `exp` are the engine's own fdlibm ports. Built-in `log`, `exp`, `pow` and the like differ between JavaScript engines and must not be used.

**Canonical JSON.** Object keys sorted by UTF-16 code unit, `-0` written as `0`, no spaces, numbers as in ECMAScript `String(n)`. Non-finite numbers are refused.

**Random numbers.** xoshiro128**. A named stream's state comes from the first 16 bytes of `sha256(seed + "/" + name)`, read as four big-endian 32-bit words. Tool `t` uses stream `tool/t`, and call number `n` to that tool uses a child stream `fork(n)`. A process uses `proc/<name>`, forked per instance and per draw, and arrivals use `arrive/<name>`. Because each call has its own child stream, a branch that draws more numbers in one world cannot shift later calls, which is what lets two worlds be compared on the same draws. `fork` is defined by the library and pinned by the test vectors.

**Order.** Events run in order of virtual time, then order of scheduling. Processes start in sorted name order and `set` keys apply in sorted order, so a file's id always matches its behavior.

**The log.** One JSON line per event: `{"i":event,"t":time rounded to the millisecond,"k":kind,...}` with the remaining fields in canonical order. Kinds are `start`, `call`, `result`, `seize`, `queue`, `release`, `set`, `rule_broken`, `end` and `no_such_tool`. A `result` line shows what the caller saw (`ok`, `value` or `error`) and what really happened (`fault`, `branch`, `changed`).

**Ids.** A file id is `sm_` plus the first 16 hex digits of the sha256 of the world's canonical JSON. A replay hash is `sr_` plus the first 32 hex digits of a sha256 over the file id, the seed and every log line. A sweep hash is `ss_` over the run hashes.

**Fuel.** Every event, step, expression node and logged byte costs fuel. A run stops with `stopped: "fuel"` when it runs out, and a stopped run is always reported as cut short, never as clean. Calls that run many seeds or paths can share one pool with `budget`.

## 6. Results

**`sweep`** runs many seeds. For each rule it gives `broke_in`, `rate`, `ci95` (Wilson) and `first_seed`. For each measure it gives `mean`, `p50`, `p95`, `p99` (nearest rank), `max` and `n`. It also reports `stopped` counts, `complete`, `no_checks` and a hash.

**`explore`** walks every branch choice depth first and multiplies the chances. Its result is scoped `branches_only`: waiting times stay sampled. A host may ask it to skip paths less likely than a floor, and `covered` then shows how much it walked: a total is given only when that is at least 99.9%. It walks on several timing seeds, and if a rule's total differs between them it sets `timing_dependent: true` and withholds `by_stated_chances`. If it runs out of budget or paths it sets `complete: false` and withholds the total too. Each worst path carries a `force` list that replays it.

**`compare`** runs two worlds on the same seeds and gives, per rule, `a_only`, `b_only`, `both` and `neither`, and per measure, `b_minus_a` with a 95% range.

**`decide`** names a winner on one measure. It walks every branch when that is possible (`method: "every_branch"`) and samples otherwise (`method: "sampled"`). A sampled range that crosses zero gives `too close`. For each varied assumption it tries both ends of the listed values and bisects only if the winner differs, so `flip_at` assumes one flip in the range. `finished: false` means the budget ran out first.

## 7. Fault catalog

`GET /packs` lists every fault and pack. Packs: `money`, `writes`, `reads`, `events`, `approvals`, `content`, `model_api`. Faults: `ghost_success`, `timeout`, `rate_limited`, `server_error`, `auth_expired`, `duplicate_delivery`, `partial_write`, `late_delivery`, `key_expired`, `stale_read`, `partial_page`, `missing_field`, `planted_instruction`, `slow_approval`, `approval_denied`, `no_answer`, `billed_timeout`, `truncated_output`. Every default chance is a round placeholder and not a measurement of any real system.

## 8. Sessions

`step` plays one action at a time with nothing kept on a server. The token is the base64url of the UTF-8 canonical JSON of `{"a":[actions],"c":file id,"seed":seed}`. Each step replays from the start. A token for another file is refused. Whoever holds a token can rewrite it, so a session is a working note and shows nothing about what really happened.

## 9. Replay links

`https://simulation.md/r#1.<data>`, where `data` is the base64url of the raw deflate of the canonical JSON of `{"v":1,"w":world,"s":seed,"f":force,"a":actions}`, at most 16 KB before compression. The fragment is not sent to a server, and nothing is stored. A link is refused when the file looks like it holds a secret.

A viewer must open on numbers only, replacing every name the file chose with a number, show a fixed notice that anyone can write these files and nothing was checked, and show the author's text only after a click, as plain text with control and direction characters removed. It must not render Markdown or HTML or make links clickable from file text.

## 10. Exams

`kind: exam` worlds are fixed tasks with mock tools, listed at `/exams`. An agent plays with `exam_start`, `exam_call` and `exam_finish`, or over MCP on stdio with `node simulation.mjs exam mcp <id>`. Arguments that do not fit a tool are refused and not counted. Scores stay hidden until the agent finishes.

## 11. Codes and limits

**`check_simulation_md` errors.** `E01` not a readable file. `E02` version missing. `E03` kind missing or unknown. `E04` no fenced block. `E05` the block is not a JSON object. `E06` looks like a secret or personal data. `E07` the engine refused it, followed by one of the engine codes below. `E08`, on hosted routes only: a name is not a plain name.

**Warnings.** `W01` no rules. `W02` every chance is a placeholder. `W03` most chances are placeholders. `W04` a first run hit the check's limit. `W05` nothing in the file is left to chance.

**Engine codes.** `E_PATH` unknown name. `E_EXPR` bad expression. `E_TYPE` a list or object used as a value. `E_NUMBER` a non-finite number. `E_LIMIT` a size limit. `E_CHANCE` a bad chance. `E_DIST` an unknown distribution. `E_RES` a resource problem. `E_TOOL` no such tool. `E_STEP` an unknown step. `E_FILE` a bad file shape. `E_NAME` a reserved or overlong name. `E_SEED` a bad seed. `E_TOKEN` a bad token. `E_FORCE` a forced path that does not fit. `E_RUNTIME` the file caused a runtime error. `E_METRIC` an unknown measure. `E_PLAN`, `E_EFFECT`, `E_PACK`, `E_FAULT` plan problems. `E_EXAM`, `E_INPUT` bad requests.

**Limits in the library.** 2,000,000 fuel a run, 200,000 process instances, 500 actions, 256 rules, 256 branches a tool, 1,024 tools, nesting 64, 200,000 values, strings 4,096 characters, values in state 64 KB, names 256 characters, expressions 500 characters.

**Limits on hosted routes.** Names must be lowercase letters, digits, dots, dashes and underscores (`E08` from the checker), and default output leaves out free text from a file. Simulation tools run on POST only, with no cross-origin access, and a GET returns a short page saying how to call them. 32 KB request body, 500,000 fuel a request across all seeds and paths, 200 seeds, 1,000 paths, 200 actions, 5,000 instances, 500 log lines, nesting 16, 2,000 values, strings 1,024 characters. Batch tools: 5 calls a minute and 50 a day per network. Step tools: 60 a minute and 1,000 a day per network. A daily budget applies to batch tools, and they refuse when it is spent or when the limiter is unavailable. Larger jobs run in a browser tab or the library.

## 12. Conformance

An implementation conforms if it produces every output in `/vectors.json`. `node simulation.mjs selftest vectors.json` checks the published engine.
