# sim.pflow.xyz — cost-benefit decisions, simulated sim.pflow.xyz turns any decision with competing costs and benefits into a what-if machine. Should I hire another barista, add a van, or build this feature? Describe the decision as a Petri net — staffing, queues, equipment, patience, or a backlog gated by cost-benefit review — store it with one POST, and every model gets the same instruments: seeded scenario comparison, disruption injection, calibration against your own event log, synthetic event-log datasets, structural diagnostics that rank every knob by measured influence on the declared objective, signed run receipts, and one-click publishing into your own Google Sheets with live charts. The diagnostics pass is the point, not a side feature: it has found capacity that *hurts* the objective as often as capacity that helps — a café's pantry stock, a hospital's ED bays, a software team's engineering headcount past the point where the evaluation gate can use it well. "Add more" gets checked instead of assumed, across any domain you can express as tokens moving through places. Staffing a café and deciding what to build next are the same kind of question here: a flow with a cost, a benefit, and a knob someone wants to turn without knowing what it actually does to the outcome. Three properties hold everywhere: - **Content-addressed and immutable.** The id a POST returns is the hash of the model. A changed model is a new id; old ids stay valid forever. - **Pure, seeded reads.** Scenarios never mutate anything. Same model, same scenario, same seed → byte-identical result, on any revision. - **Honest enforcement.** Every number comes from a discrete engine (stochastic simulation, SSA) that honors what most simulators quietly relax: read arcs gate without consuming, inhibitors block above thresholds, capacities bound post-firing, and a queue is a prerequisite, never an accelerant. Where a reading cannot enforce something, the response names it (`caveats`, `gating`) instead of proceeding silently. Built on the pflow.xyz Petri-net tooling (https://pflow.xyz). Sibling services: https://pilot.pflow.xyz (analysis and code generation MCP) and https://book.pflow.xyz (the book). ## Model format Flat JSON. Ids are identifiers (`[a-z_][a-z0-9_]*`), rates are per hour. ```json { "name": "helpdesk", "description": "Tickets arrive, agents resolve them, impatient users give up.", "places": [ { "id": "queue", "initial": 0 }, { "id": "agents", "initial": 2 }, { "id": "working", "initial": 0 }, { "id": "resolved", "initial": 0 }, { "id": "abandoned", "initial": 0 } ], "transitions": [ { "id": "arrive", "rate": 10 }, { "id": "pickup", "rate": 720 }, { "id": "resolve", "rate": 4 }, { "id": "give_up", "rate": 1.5 } ], "arcs": [ { "from": "arrive", "to": "queue" }, { "from": "queue", "to": "pickup" }, { "from": "agents", "to": "pickup" }, { "from": "pickup", "to": "working" }, { "from": "working", "to": "resolve" }, { "from": "resolve", "to": "agents" }, { "from": "resolve", "to": "resolved" }, { "from": "queue", "to": "give_up" }, { "from": "give_up", "to": "abandoned" } ] } ``` Notes: - A source transition (no input arcs) is a demand: `rate` is arrivals per hour. - A service is two firings: an instant **pickup** (declared rate ≥ 100, its observed gap is the queue wait) that seizes a resource from a pool, and a kinetic completion that returns it. The pool is the staffing knob. - A **hazard** (abandonment, spoilage, timeout) competes for the same queue place with its own rate — patience is a knob too. - `{"from","to","weight","type":"read"}` gates without consuming; `"type":"inhibitor"` blocks above `weight`. `capacity` on a place bounds it after firing. - Tag a terminal place `outcome: loss`, `outcome: served`, or `outcome: none` (a meter, e.g. instance-hours) when structural inference would misread it. - The full field list is a JSON-LD context at https://sim.pflow.xyz/context/model/v1 with a glossary at https://sim.pflow.xyz/ns/v1. `GET /api/models/{id}` with `Accept: application/ld+json` (or `?ld=1`) returns the wrapped form; the plain form is what the id hashes. - Every other JSON-LD document (`sim_diagnose`, `sim_classify`, `/api/models/{id}/classification`, `/api/lineage/{id}`) carries https://sim.pflow.xyz/ns/v1/context as its `@context`; pass `inline_context: true` (MCP) or `?context=inline` (HTTP) to embed the map. ## The declared metamodel Four optional blocks move knowledge out of scenarios and into the model, where every reading can see it. Each has a CC0 showcase in the commons. - **`simulation.objective`** — a numeric expression over the final marking, e.g. `"delivered - 0.2*van_hours"`. Diagnostics rank every knob on the objective instead of the inferred loss count, so tradeoffs (timeouts vs instance-hours, service vs overtime) become visible. Pair with `outcome: none` on the meter place. Showcase: **delivery-fleet**. - **`parameters`** — named structural variables bound to arc weights (several arcs move together and must agree) or place capacities: batch sizes, shelf sizes, boat capacity. Scenarios set them with `"params": {...}`; diagnostics probe each one unit each way, because a *smaller* batch can be the win. Showcase: **bakery-oven**. - **`stages: k`** on a transition — Erlang-k (phase-type) service time via an exact structural expansion that folds back to the original vocabulary. Narrow by design: one kinetic weight-1 input from a dedicated place nothing else touches. Showcase: **car-wash**. - **`schedule`** on a transition — a piecewise-constant day shape (the lunch rush, the overnight lull) carried by the model. Scenario schedules override per transition; diagnostics probe a scheduled source by scaling the whole shape ×1.5. Showcase: **food-truck**. Also declared, not derived: `players` (a 1v1 turn-based game over the objective — `maximizes`, `turnPlace`, per-player transitions), `view` (the presentation intent as prose, hash-safe), and `views[]` (screens as validated projections with roles board | dashboard | controls | advisor | trust | log). The `/evaluate` reading and published apps consume these. ## HTTP API Everything under `/api/` is JSON. Public reads are CORS-open. Writes that create or publish require a Google sign-in (`/auth/google/login`) or an MCP OAuth token. ``` GET /api/catalog curated consoles + the commons GET /api/components subnet template registry POST /api/models store a model → {"id","url"} GET /api/models models visible to you GET /api/models/{id} the model (plain or JSON-LD) DELETE /api/models/{id} owner only; refused once dedicated POST /api/models/{id}/license dedicate: CC0-1.0 | CC-BY-4.0 | CC-BY-SA-4.0 (irrevocable) GET /api/models/{id}/rates declared knobs (pools, sources, patience, parameters) + caveats POST /api/models/{id}/scenario one hypothetical POST /api/models/{id}/scenario/compare several, on one server-enforced seed GET /api/models/{id}/dataset synthetic event log ?seed=&cases=&format=csv|jsonl GET /api/models/{id}/diagnose fitness gates + knob influence ?hours=&realizations= (≤200) GET /api/models/{id}/classification parameter classes (fungible sets, families) as JSON-LD GET /api/models/{id}/lineage what this model was derived from GET /api/models/{id}/layout auto-layout coordinates POST /api/models/{id}/verify deadlock-free, bounded, mutex, invariants, reachability GET /api/models/{id}/invariants P-invariants (conservation laws) and T-invariants (cycles) POST /api/models/{id}/crosscheck every reading against every other; divergences named POST /api/models/{id}/evaluate score a player's next moves (elimination | incidence | search) POST /api/models/{id}/calibrate event log CSV → NEW model with learned rates + conformance POST /api/models/{id}/compose instantiate a component onto this model → NEW model POST /api/models/{id}/receipt scenario + Ed25519 run receipt POST /api/models/{id}/publish → the signed-in user's Google Sheets GET /api/receipts/key public verification key POST /api/receipts/verify check signature, replay, report drift GET /app/{id} owner-published single-file app (CSP sandbox) GET /api/me/sheets sheets you have published ``` ### Scenario body ```json { "hours": 8, "realizations": 16, "seed": 7, "marking": { "agents": 3 }, "rates": { "arrive": 14 }, "schedule": { "arrive": [ {"hours": 2, "rate": 6}, {"hours": 2, "rate": 20}, {"hours": 4, "rate": 8} ] }, "params": { "batch": 6 } } ``` `marking` overrides initial tokens (staffing, stock), `rates` overrides per-hour rates (demand, patience, equipment down = rate 0), `schedule` is piecewise constant per transition, `params` sets declared structural parameters. The response carries per-place trajectories, served/lost totals, utilisation, contended-time accounting (what the run actually waited on, classified structurally), and `caveats` for anything the engine could not honour. `/scenario/compare` takes `{"seed", "scenarios": [{name, ...}]}`; pass `"summary": true` for totals only. ### Quick start ```bash # store ID=$(curl -s -X POST https://sim.pflow.xyz/api/models \ -H 'Content-Type: application/json' -d @model.json | jq -r .id) # ask it a hypothetical (pure read, seeded) curl -s -X POST https://sim.pflow.xyz/api/models/$ID/scenario \ -d '{"hours":8,"realizations":16,"seed":7,"marking":{"agents":3}}' # does the model hold up before you trust its numbers? curl -s "https://sim.pflow.xyz/api/models/$ID/diagnose?hours=8&realizations=24" # a dataset you can process-mine curl -s "https://sim.pflow.xyz/api/models/$ID/dataset?seed=1&cases=400&format=csv" > log.csv ``` ## MCP server `https://sim.pflow.xyz/mcp` — Streamable HTTP, JSON-RPC, OAuth 2.1 with PKCE and dynamic client registration (`/oauth/register`, `/oauth/authorize`, `/oauth/token`; the authorize step signs in with Google). Access tokens last 24h, refresh tokens 90d. Listed on the MCP registry as `xyz.pflow.sim/whatif`. Tools (all take a stored model `id` unless noted): - **sim_list_models** — the catalog, the commons (with license), and your own (marked mine). - **sim_get_model** — a stored model's full JSON. - **sim_create_model** — store a model (no id) → content id; structural validation returns every reason at once. Private to you until licensed. - **sim_delete_model** — delete a model you created (refused once dedicated). - **sim_license_model** — dedicate to the commons: CC0-1.0, CC-BY-4.0 or CC-BY-SA-4.0. Irrevocable; the model becomes publicly listed. - **sim_supersede_model** — hide an older version of your model from listings behind a newer id; id and dedication untouched. - **sim_components** — the subnet template registry (arrivals, service, hazard, inventory, decision, mailbox, datastore) with ports, params and the discipline notes explaining their shape. - **sim_compose** — instantiate a component, optionally onto an existing model's places, as a NEW model with lineage. - **sim_scenario** — one seeded what-if: marking, rates, schedule, params. - **sim_compare** — several scenarios on one shared seed, side by side (summaries by default; `full=true` for series). - **sim_dataset** — synthetic event log (case-per-arrival; CSV or JSONL) that replays cleanly against its own generating net. - **sim_calibrate** — upload your event log (`case_id, activity, timestamp`; activities = transition ids) → NEW model with learned per-hour rates plus a conformance report (`fittingPercent`, worst traces named). Instant pickups keep their declared rate. Read the fitness before trusting the rates. - **sim_diagnose** — fitness gates (mass balance, dormant sources, staffing knee, does anything bind) and every knob ranked by measured, signed influence at more than one operating point; parameter classes discovered. - **sim_classify** — parameter classes (fungible sets, families) as JSON-LD with empty annotation slots. - **sim_refine** — edit what the model says (tags such as `outcome:`, asserted classes) and re-derive; NEW model with lineage. - **sim_verify** — declared properties: deadlock-free, bounded, mutual exclusion, invariant expressions, reachable / unreachable targets. - **sim_invariants** — Farkas P-invariants (conservation laws a trust panel should show) and T-invariants (firing cycles). - **sim_crosscheck** — every applicable reading against the others: SSA means vs the mean-field ODE, algebraic invariants vs simulated means, game search vs elimination; divergence named with its reason. - **sim_evaluate** — for game-schema models, score a player's legal next moves: `elimination` (rate-0 ablation), `incidence` (closed-form live-drain counts), or `search` (memoized alpha-beta over the declared game). - **sim_receipt** — run a scenario and get an Ed25519 receipt over (model id, canonical scenario, result hash, service revision); anyone can verify it at `/api/receipts/verify`. - **sim_publish** — publish a model into the signed-in user's Google Sheets. - **sim_publish_app** — publish the single-file HTML app generated from a model's `view` prompt (≤512 KB, must reference its model id, no external scripts); served at `/app/{id}` in a sandboxed origin. - **sim_my_sheets** — the sheets you have published, with URLs. A productive loop: `sim_components` → `sim_compose` (two or three calls build a service-with-hazard) → `sim_diagnose` (do the gates pass? which knobs bind?) → `sim_compare` (price the candidate changes on one seed) → `sim_dataset` / `sim_calibrate` (meet reality) → `sim_license_model` → `sim_publish`. ## Google Sheets publishing A published spreadsheet is a first-class surface, not an export. `sim_publish` (or `POST /api/models/{id}/publish`, or the Publish to Sheets button on the landing page) creates a workbook in **your** Drive, under the `drive.file` grant — the service never owns user files — containing: - the **model itself** in sheet form (places, transitions, arcs, parameters, objective, players, views — rows that round-trip byte-identically back to the model); - a server-run **scenario** as data tabs (trajectory, totals, contention); - **native charts** for the trajectory and contention analysis; - for models honest to solve in cells (pure-kinetic, no inhibitors), a **live formula tab** you can re-solve by editing a rate; otherwise a refusal tab saying why. Published sheets link back to the model id and survive independently of the service. `sim_my_sheets` / `GET /api/me/sheets` lists yours. ## Consoles and apps - `https://sim.pflow.xyz/` — landing: the catalog with Open console / Open app / Dataset / Publish to Sheets per entry. - `https://sim.pflow.xyz/whatif/?id={id}` — the generic what-if console for any stored model: knobs from `/rates`, disruptions, schedules, seeded side-by-side comparison. `vet-clinic` is the reference. - `https://sim.pflow.xyz/build/` — guided model builder (sign-in). - `https://sim.pflow.xyz/app/{id}` — owner-published apps, served with `Content-Security-Policy: sandbox allow-scripts` in an opaque origin: no cookies, no session, only the CORS-open public reads. ## The commons Models dedicated under CC0 / CC-BY appear in `/api/catalog` for everyone. Current showcases and classics (ids are stable): | Model | Id | Demonstrates | |---|---|---| | feature-lab | `47db64e54d24ca0600360c8c` | cost-benefit for software decisions: evaluation gate, capacity-constrained build, telemetry validate/kill, evolutionary feedback loop | | delivery-fleet | `e38fc49777b731cfbc2764b9` | declared objective | | bakery-oven | `830937feca631188b3c421ed` | declared parameters (batch size) | | car-wash | `735dd8109ce265a6de2c37a3` | stages (Erlang-k service) | | food-truck | `5159060ebfc150ea61b8e7a6` | model-carried schedule | | machine-shop | `0cf9c060120daabde879d9ac` | operator-as-player: budget + decisions, `/evaluate` as advisor | | stockroom | `912d422e3401b804cfd037ef` | reorder point via inhibitor | | ward-flow | `f77fd416451ed8fe90e45aad` | ED bays vs ward beds | | outbreak | `7562102d5bd4d9dbecda892a` | stochastic SIR; fizzle probability SSA vs ODE | | bank-run | `340aba697b5ff61e416ee4d1` | contagion retold financially | | retry-storm | `d1b2e93309238d29e0cb7748` | spike + retries = collapse; capped retries recover | | build-farm | `5f40aa647867876483b18fc2` | flaky tests vs more runners | | autoscaler | `61ded8e1b68c47e38e6c0162` | elastic pool, instance-hours meter | | beer-game | `5a89df1945953e1fa41ddb4c` | where safety stock should live | | blood-bank | `51a1f196e033c9876076fc51` | donation drive vs storage | | triage-priority-clinic | `6125314dc1087f55e55c2317` | structural priority via inhibitor | | claims-desk, loan-book, invoice-collections | see catalog | operational-finance flows | | launch-plan | `089fb4145543503191914706` | schedule risk with a rework loop | | ferry-dock | `2e11344b4fd312aa0613295d` | batch departures, impatient queue | | tic-tac-toe | `5a08c67862a2eb55799f5229` | game schema: players, objective, `/evaluate` search | Curated (no id hash): `vet-clinic`, `cafe`, `predator-prey`. ## Modeling discipline, in brief - Model only the contested flow. A path that "pays on its own" reads as abandonment to the classifier. - Let the run build its own stock within the horizon; seeding a large initial stock breaks the conservation gate. - Token splits (one in, two out) fail mass balance; use read arcs for gates. - An influence below the reported noise is dice, not a small effect. Raise `realizations` or `hours` before concluding a model is inert. - Rate-0 transitions are dormant to scenarios and fire only when a player chooses them — that is how decisions enter a model. ## Operational notes - `/healthz` is reserved at the Cloud Run edge; external probes should use `/api/catalog`. - Seed 0 is a fixed seed by design, not "random". - Receipts verified under a newer service revision that diverge are reported as engine drift, not forgery.