A recorded trunk, a fork at now, and two simulated branches A solid line runs left to right and stops at a point marked NOW. The solid stroke is the RECORDED class: it is what actually happened, tagged observed. At NOW a bar stands across the line — nothing runs until a human accepts. Past NOW the line forks into two dotted rays that share that one baseline. Dotted is the PINNED class: byte-identical on replay. One ray, named main, rises to more violations; the other, named governed, stays flat on zero. Both are tagged simulated. The angle between the two rays, marked at the fork, is the measurement — which is what the word parallax means. RECORDED observed · it happened NOW nothing runs until you accept main governed PINNED · simulated PINNED · simulated Δ the measurement
Parallax run bef312a9 · class PINNED

scroll — you are scrubbing a log

Skip to the product

01 / 06

  1. Every operating decision is taken once.You choose, the world moves, and the alternative is never observed.
  2. Parallax gives you the one you did not take.Point it at a context. It proposes a model of what is actually in there, and hands it to you before anything runs.
  3. You accept it, or nothing happens.A model nobody reviewed should not be able to produce numbers that look authoritative. While a blocking question is open, it refuses to activate.
  4. Then fork the history and change one decision.Same initial state, same seed, one policy different. The log is append-only, so a branch costs nothing to create.
  5. Two lines out of one baseline.The angle between them is the measurement. That is what the word parallax means.
  6. And every number says how much of it was real.Typed observed or simulated at birth, carried into the receipt and into the API.
Parallax run bef312a9 · class PINNED · 12 simulated · 0 observed

Parallax · Apache-2.0 · the ontology simulation layer of the bstack

Simulation results you accept before they are active.

There is no staging environment for the way a business operates. You change a price, a refund policy, an escalation threshold — and you find out what happened once it has already happened, usually because a customer told you.

Parallax proposes a model of your context built from what is actually in it, waits for a human to accept that model, and only then rolls it forward under the decisions you are considering. The goal is not a simulator that is right. It is a simulator that cannot lie about being a simulator.

  1. 01PointAt a directory, an agent's own workspace, or a set of business tables.
  2. 02ProposeAn ontology assembled from what is in there — state, actions, and the questions it could not answer. Slots it cannot support come back empty.
  3. 03AcceptA human answers the blocking questions and accepts. This is the product, not a formality. Nothing runs before it, and the accepted object is minted behind a module-private symbol that is checked at runtime — so it cannot be forged and does not survive a JSON round-trip.
  4. 04RollFork the log at a point, change one decision, replay the same steps under the new policy.
  5. 05TypeEvery value carries observed or simulated, plus a class saying whether it can be re-derived at all.

Reproduce every number on this page

$ git clone https://github.com/broomva/skills && cd skills/skills/simulation/parallax/runtime
$ bun install
$ bun run demo            # the operators: run, observe, check, fork, prove
$ bun run demo:whatsapp   # the same thing as one thread, ending in a receipt file
$ bun run demo:live       # the same thread against the deployed hub, ending in a link it fetched first
$ bun run src/cli.ts help # the same capabilities at a terminal
$ bun run src/hub/serve.ts # the same capabilities over HTTP

Every figure below is printed by one of those two commands at seed 42 over a horizon of 12 steps. There are no customer deployments, no accuracy claims and no benchmark numbers on this page, because we have none.

Build state, stated plainly

Runs today

  • The runtime and its six operators
  • Append-only log with copy-on-write forking
  • The reproducibility lattice, and the policy certifier that enforces it
  • Conservation and safety invariant checking
  • The accept gate, brand-checked at runtime
  • The conversation layer as pure functions
  • The self-contained run receipt
  • A CLI, an HTTP hub and a tool surface over the same handlers
  • A second domain — a clinic appointment desk, the generality proof: same runtime, same six operators, a different transition and a different conservation law

Designed, not built

  • The LLM adapter — today's actors are seeded and pure
  • A third domain, and a domain supplied by someone who is not us
  • The web console
  • A live WhatsApp number; the channel layer is pure functions with no transport attached

Not measured

  • Nothing here is calibrated against a real business
  • No accuracy percentage, because we cannot support one
  • No customer results, because there are no customers

01 · Intake

Point it at a context.

Three context classes, one intake, a separate proposer for each. Parallax reads what is already there. It does not ask you to model your operation first, because the model is the thing it is supposed to produce.

Class 01 — business data

A schema and its rows

Orders, ledgers, inventory, tickets. One state field and one insert action per table, and a blocking question per numeric parameter: rows, items, or currency. The conservation identity is usually already in there — money, stock, hours, seats.

Class 02 — agent workspace

An agent's own directory

The session is spawned with its working directory already set to the tenant's own folder, and the confinement keys off exactly that. Passing a derived path instead is denied, and a denied read looks like an empty directory rather than an error — so the default is to pass no path at all.

Class 03 — local filesystem

An arbitrary directory

Files, commits, exports, logs. No integration and no schema agreed in advance. Same proposer as the workspace class; the difference is who is allowed to name the root.

What the proposer actually does

One non-recursive directory read, then one stat per entry. It never opens a file. Entries beginning with a dot are skipped. From that it emits one state field per top-level directory, one per file extension, one action per directory, and one blocking question for every numeric parameter, because a numeric parameter with no unit fails closed at materialisation and the proposer cannot invent the unit.

PROPOSAL — what bun run demo:whatsapp emitted when pointed at the Parallax project directory on 2026-08-23
Slot Emitted Read from
state 14 fields 7 top-level directories, 7 distinct file extensions
actions 7 one per top-level directory, actor operator
invariants — empty — nothing in a directory listing implies a conservation law
blocking questions 7 every numeric parameter needs a unit before this can run
advisory questions 1 "Which quantity here is conserved?"
transition — you supply it — code, never a model; activate refuses without one

Those counts are a reading, not a specification. The row above said twelve fields and six actions this morning; a scripts/ directory and a render.yaml landed in it that afternoon and it then said fourteen and seven, with a different proposal id. Re-run it against your own context and it will disagree with this page again. That is the proposer working, not drifting — and it is why an acceptance is bound to the exact proposal it was given rather than to a name.

You are approving a model of your own operation, not a model's opinion of it. An empty slot is the correct answer when the context does not support one; a plausible guess is not.

02 · The gate

You accept it, on your phone, before anything runs.

WhatsApp because it is where Latin America already operates. It is a channel, not the product: renderProposal, parseReply and resolveAccept are pure functions over a proposal with no transport imported at all. A capability reachable by only one transport is a feature of that transport, so the CLI and the HTTP hub call these same three.

+57 301 775 8620

hola, quiero simular un cambio de precio antes de aplicarlo

Parallax — the proposal, as sent

*simulacro (proposed from agent-workspace)*
Read from your workspace. Nothing runs until you accept it.

*State* (14 fields)
  design_count = 0  <- directory design/
  landing_count = 0  <- directory landing/
  test_count = 0  <- directory test/
  out_count = 0  <- directory out/
  node_modules_count = 0  <- directory node_modules/
  scripts_count = 0  <- directory scripts/
  src_count = 0  <- directory src/
  files_md = 6  <- 6 file(s) with extension .md
  files_yaml = 1  <- 1 file(s) with extension .yaml
  files_none_ = 1  <- 1 file(s) with extension (none)
  files_jsonc = 1  <- 1 file(s) with extension .jsonc
  files_lock = 1  <- 1 file(s) with extension .lock
  files_json = 3  <- 3 file(s) with extension .json
  files_png = 1  <- 1 file(s) with extension .png

*Actions* (7)
  add_to_design(count)  by operator
  add_to_landing(count)  by operator
  add_to_test(count)  by operator
  add_to_out(count)  by operator
  add_to_node_modules(count)  by operator
  add_to_scripts(count)  by operator
  add_to_src(count)  by operator

*Before this can run* (7)
  1. What unit is "count" measured in for add_to_design? Materialisation fails closed without it.
  2. What unit is "count" measured in for add_to_landing? Materialisation fails closed without it.
  3. What unit is "count" measured in for add_to_test? Materialisation fails closed without it.
  4. What unit is "count" measured in for add_to_out? Materialisation fails closed without it.
  5. What unit is "count" measured in for add_to_node_modules? Materialisation fails closed without it.
  6. What unit is "count" measured in for add_to_scripts? Materialisation fails closed without it.
  7. What unit is "count" measured in for add_to_src? Materialisation fails closed without it.

*Worth answering*
  - Which quantity here is conserved? A conservation invariant is the cheapest oracle available and this proposal has none.

Reply with the numbered answers, then ACCEPT. Reply REJECT to discard.
ref 8e59eea705cc

+57 301 775 8620

1. unidades
2. unidades
3. unidades
4. unidades
5. unidades
6. unidades
7. unidades
sí, dale

Parallax

aceptado. nada corrió hasta ahora.

Parallax — the ungoverned run

corrí 12 pasos. 10 violaciones.

Parallax — the same history, one thing changed

con el gobernador: 10 -> 0 violaciones.

Parallax

listo. el recibo: out/run-bef312a9.html
Captured verbatim from bun run demo:whatsapp on 2026-08-23, against the project directory as it stood before Parallax moved into the skills monorepo — at that point the checkout was named simulacro and design/, landing/, src/, test/ and scripts/ were siblings inside it. None of those names describe the current layout, and the capture is deliberately not restaged to match: it is evidence of a run that happened, and editing a transcript to agree with a later tree would make it a reconstruction wearing the word "verbatim". Re-run today it reports the directory it is actually standing in, which is the whole point of the mechanism — the proposer names what it found, not what we would prefer it were called. It names the file it wrote, because a local run has no server to serve a URL from; bun run demo:live drives the deployed hub instead and ends on a /r/:id link it has already fetched. Naming a URL nobody was asked to serve is the failure this page exists to refuse, and an earlier version of this transcript did exactly that. The state fields are read from the directory itself, so the counts and the ref move whenever the tree does; the storefront numbers further down do not, because they come from a fixed domain at a fixed seed.

Reading a human reply

Reject wins over accept

A reply containing both an acceptance word and a rejection word is a rejection, because the safe reading is the one that does not start running things. Bare no anywhere in the message counts. And \b is unreliable next to accented characters, so the matcher classes codepoints instead — otherwise silently never matches and one language's acceptance word disappears.

What acceptance produces

An object you cannot forge

activate is the only way to mint an accepted ontology, and the brand is a module-private symbol checked at runtime rather than a structural field. It deliberately does not survive a JSON round-trip: trust cannot be serialised, it has to be re-minted in the process that will execute it.

What comes back

A receipt, not a number

The run writes out/run-bef312a9.html, 10.0 KB, self-contained by construction — no external CSS, fonts, scripts or images, because anything it fetched at view time would be a claim the reader cannot check.

03 · Trajectory

Roll it forward, then fork it and change one thing.

The log is append-only, so a fork is a branch record with a parent and a fork point, and creating a future costs nothing. Only evaluating one costs anything. Two branches that share a past can be compared, because a tick means the same thing on both.

Here both branches replay the same twelve steps from the same initial state at the same seed. The only difference is a shield installed in front of the policy, which refuses an action whose post-state would violate an invariant.

WORLD whatsapp-storefront FORK main@seq0 HORIZON 12 SEED 42 POLICY eager-agent VIOLATIONS main 10 → governed 0
Two branches out of one accepted baseline, and the angle between them A vertical axis on the left measures violations accumulated, from zero at the bottom to twelve at the top. A horizontal axis measures the twelve steps of the run. Both branches start from the same point: the accepted ontology at sequence zero, with zero violations. The branch named main rises to ten violations by step twelve. The branch named governed, drawn in the accent colour, stays flat on zero for the whole run. Because both rays start at the same point and end at step twelve, the angle between them is exactly the difference between ten and zero, which is the measurement. Both rays are drawn dotted, which is the stroke for the PINNED reproducibility class, because that is the class both branches actually demonstrated. VIOLATIONS ACCUMULATED 10 5 0 0 6 12 STEP horizon 12 · seed 42 · same policy, one shield ACCEPTED ONTOLOGY · seq 0 · initial state Δ 10 violations main 10 violations PINNED · simulated governed 0 violations PINNED · simulated

Stroke — reproducibility class

  • RECORDEDhappened, or unrepeatable — not produced by this run
  • STABLEreproducible in distribution, not in bytes
  • PINNEDbyte-identical on replay — both branches above

Tag — origin

  • observedcame from the referent — 0 of 12 steps here
  • simulatedwe generated it — 12 of 12 steps here
The two axes are deliberately not the same axis, and collapsing them loses one. Stroke answers "if we run it again, do we get the same thing?" Tag answers "did this come from the referent, or did we generate it?" A real customer message is observed and unrepeatable; a seeded scripted actor is simulated and perfectly repeatable. Neither predicts the other.
Both rays leave the same point and both end at step 12, so the angle you see is exactly the difference between the two endpoint numbers — 10 and 0 — at the same scale the y axis is labelled with. Nothing about the angle is chosen for the drawing. Two of the three strokes are shown in the legend and drawn nowhere, because this run did not produce a RECORDED or a STABLE branch, and drawing one to fill out the figure is the failure this page exists to refuse.
BRANCHES — printed by bun run demo:whatsapp, storefront demo world, not a customer result
Branch Policy Class Steps Violations Trace
main eager-agent PINNED 12 10 435b1748860d
governed governed(eager-agent) PINNED 12 0 c20ae410b61c

What the fork changed, verbatim

# the diff between the two end states
inventory: {"arepa_kit":10,"cafe_500g":3,"panela":-1} -> {"arepa_kit":10,"cafe_500g":3,"panela":0}
promised:  ORD-000, ORD-003, ORD-006, ORD-009 -> ORD-000, ORD-003, ORD-009

# step 006 of the governed branch, from the receipt's own trajectory table
006  governor  refuse  {"blocked":"promise","order":"ORD-006","sku":"panela","qty":1}  simulated

The ungoverned agent promises panela it does not have, and every step after that carries the consequence: no_phantom_promise from sequence 7 onward, then inventory_nonneg from sequence 9 once the fulfilment lands. Ten violations across twelve steps. The governed branch refuses the promise at step 6 and finishes with none, at the cost of one order it did not take.

04 · Typing

Every answer is typed observed or simulated.

A figure that came out of your history and a figure that came out of a projection are not the same kind of thing, and a system that renders them identically is lying by layout. So the type is attached at birth and travels with the value. An untagged value can never be separated later, because the information needed to do it was discarded when the value was created.

What the receipt says about itself, unprompted

# out/run-bef312a9.html, verbatim
Every invariant held for the whole run. Nothing here was observed;
the entire trajectory is simulated. Against what actually happened:
10 fewer violations.

How much of this was real
0 observed · 12 simulated — a value derived from anything simulated is
simulated, however much observed data went in beside it.

Zero percent observed is not a flattering number and the receipt leads with it. That is the whole design: contamination flows one way, so the honest report of a fully simulated run is that it is a fully simulated run.

OBJECTIVES — the receipt's own scoring table for run bef312a9
Objective Value Constraint Origin
orders_promised 3 admissible simulated
violations 0 admissible simulated

Constraints are hard, so admissibility sits beside the score rather than inside it: a trajectory with any violation comes back marked inadmissible, and the number cannot be quoted as though it were achievable.

Replay is a hash comparison, not a claim

# bun run demo
main  seed 42   435b1748860d979af9b4344a008301ec
main  seed 42   435b1748860d979af9b4344a008301ec   IDENTICAL
main  seed 43   6d240f8451d14d60af3ac636631e0ffe   DIVERGED (correct)

# bun run demo:whatsapp
trace  main=435b1748860d   governed=c20ae410b61c

A policy is not allowed to certify itself

# bun run demo:whatsapp — the eviction test, before anything is trusted
declared PINNED  ->  demonstrated PINNED   holds
produced an identical proposal across 3 trials at seed 42

# bun run demo — swap in an actor that is not pinned
unpinned actor -> branch class STABLE - the replay claim is withdrawn automatically

A class a policy declares about itself is not evidence, so it is measured: run the policy repeatedly against an identical probe and compare what comes out. A policy that cannot reproduce its own proposal under a fixed seed is demoted in code, whatever it declared. The floor is STABLE — the gate refuses an unearned PINNED, it does not invent a RECORDED.

And this is a filter, not a proof, with a miss rate worth stating rather than hiding. A nondeterministic policy escapes whenever all trials happen to collide: for a policy with k roughly equiprobable outputs that is about k−(trials−1), which is 4% at k=5 over 3 trials. The source says so in the function's own comment: a policy that passes certification has not been proven pure. No finite test can do that. It has only failed to be caught.

The claim is withdrawn by the runtime, not by a person remembering to withdraw it. That is the only version of this guarantee worth having — and stating the bound is part of it, because a guarantee whose failure rate is hidden is a claim about a claim.

05 · Surface

The agent is a user, not a client library.

Every capability a human can reach, an agent reaches through the same functions, with the same authorisation, and with failures that are values rather than sentences to parse. Nothing throws. Error types are per-operation and never one global union, because a caller cannot recover from what it cannot distinguish.

// a refusal is a value with a shape, and the shape is the same everywhere
{
  "ok": false,
  "error": {
    "code": "BLOCKING_QUESTIONS_OPEN",
    "reason": "7 blocking question(s) must be answered before this ontology can run",
    "detail": { "slots": ["action.add_to_design.count", "action.add_to_landing.count", /* … */] }
  }
}

That is what activate returns when the human has not answered. The detail names the exact slots, so the bot can ask again for the one thing it lacks rather than restarting the conversation. These are the codes the core, the accept gate and the conversation layer can return:

SOURCE_UNREADABLE SOURCE_EMPTY UNSUPPORTED_SOURCE BLOCKING_QUESTIONS_OPEN NO_TRANSITION NO_INVARIANTS NOT_ACCEPTED POLICY_THREW POLICY_EMPTY EMPTY_TRAJECTORY OBJECTIVE_THREW UNANSWERED_BLOCKING UNKNOWN_QUESTION EMPTY_REPLY

Two of them are the gate itself. NOT_ACCEPTED is returned by the only supported way to read the world out of an ontology, and the brand is checked there at runtime rather than only by the type system — a compile-time-only brand is documentation, and as any, plain JavaScript, or a value crossing a deserialisation boundary all walk straight past it. NO_INVARIANTS reads, in full:

"a domain with no invariants has no verifier, and an unverifiable
 simulation is the thing this system exists to refuse"

The hub and the tool surface add their own codes in the same shape: a stable code, a reason a human can read, and a detail a machine can branch on. There is no second error vocabulary for the programmatic path, because there is no second path.

The same capabilities, three ways in

# at a terminal — a typed refusal exits 2, an unexpected throw exits 1,
# and a script can branch on the code rather than on a message
$ bun run src/cli.ts accept
{"code":"MISSING_FLAG","reason":"accept needs --proposal",
 "detail":{"command":"accept","flag":"proposal","usage":"parallax accept --proposal <id> …"}}
exit 2

# over HTTP — same values, same codes, no translation layer
$ PORT=8791 bun run src/hub/serve.ts
parallax hub 0.1.0 listening on http://0.0.0.0:8791 (health: /health)

$ curl -s localhost:8791/health
{"ok":true,"version":"0.1.0","commit":"local","uptimeSeconds":2}

# deployed, the same route reports the commit it is running --
# `version` is a source constant and a deploy API reports intent, so the
# commit is the only field on this response a stale image cannot fake
$ curl -s https://parallax-hub.onrender.com/health
{"ok":true,"version":"0.1.0","commit":"d5516b00f7f6a5cf49e60249b3874eea23f9c279","uptimeSeconds":41}

$ curl -s -X POST localhost:8791/api/ontology/accept \
       -d '{"proposalId":"nope","answers":{},"acceptedBy":"you"}'
{"code":"UNKNOWN_PROPOSAL",
 "reason":"this hub has no proposal with that id; propose again and accept the proposal you get back",
 "detail":{"proposalId":"nope"}}

The hub answers /health, /api/ontology/propose, /api/ontology/accept, /api/run, /api/whatsapp/turn and /r/:id, and serves this page from the same process. The refusal body above also repeats the same object under an error key. The CLI's subcommands are propose, accept, run, receipt, status and reject, over the same handler functions the tool surface calls.

The one place the surfaces deliberately diverge is --root. An arbitrary absolute root is safe at a terminal, because the person typing the path is the confinement. It is absent from every tool schema, because inside a sandboxed session a derived path is denied and a denied read comes back as an empty directory rather than an error — so a wrong path would look like an empty workspace. Same capability, different confinement, and the difference is stated rather than assumed.

06 · Objections

Reasonable things to distrust about this.

It is still a simulation. Why would I trust the number?

Do not trust the number. Check what it says about itself. This run reports 0 of 12 steps observed in its own receipt, unprompted. Its policy's declared class was measured against a repeated probe instead of believed. And the filter doing the measuring publishes its own miss rate — about 4% at k=5 over 3 trials. Nothing here is calibrated against a real business, and there is no accuracy figure on this page because we cannot support one.

Is a language model computing my numbers?

No. The transition function and the invariants are code — activate refuses an ontology that supplies neither. A model's only job is reading a mess into typed observations, and nothing downstream of that projection is a model's opinion. Today the actors are seeded and pure; the LLM adapter is designed and not built.

What if the proposed ontology is wrong for my business?

Then you answer the questions differently, or reject it, and nothing activates while a blocking question is open. What you cannot do today is accept part of it: acceptance is all-or-nothing plus the set of answered slots. Editing a proposal produces a new proposal with a new hash, which is the honest behaviour and also the more annoying one.

We already A/B test.

A/B testing spends real customers and real weeks, and it cannot evaluate a decision you only make once. A fork is a branch record. The two compose: use this to decide what is worth testing for real.

Our context is a mess.

Then the proposal comes back smaller and emptier. The one it made of the Parallax project directory proposed zero invariants and seven blocking questions, and said so in the message rather than filling the gaps with something plausible. An empty slot is information about your context, not a failure of the tool.

07 · Start

Bring one context and one thing that must never be true.

An ontology is a record, not a codebase: what the state is, what actions exist, how an event folds into the state, and what must always hold. Two of those four are code and always will be. Everything else is the same runtime, and the runtime does not change when a domain arrives.

What comes back is the receipt: a plain-language verdict, the branch class, what the policy declared against what it demonstrated, the violation count, the observed-versus-simulated split, per-objective admissibility, the full step-by-step trajectory, and the trace hash you replay it with.

$ git clone https://github.com/broomva/skills && cd skills/skills/simulation/parallax/runtime
$ bun install && bun run demo:whatsapp
# writes out/run-bef312a9.html — open it, it is the receipt above
$ bun run src/hub/serve.ts
# serves this page, /health, the four /api routes and /r/:id