wilanis · one measured run · 2026-09-25

An agent added a tenant-scoped, signed-in route in 2 min 14 s

Claude Sonnet, a fresh copy of the example tree, one sentence of task. It wrote two JSON files. The compiler refused three drafts and accepted the fourth, each verdict in under a second.

0.5 scheck, whole tree221 documents
0.8 srehearse56 branches
0.5 sregress100 scenarios
44 sSonnet reading15 files
90 sSonnet writing4 checks, 2 files

Apple M3 Max, Node 24, npx wilanis cold each time. Every output below is pasted from that run, taken at f3b61a4.

wilanis check

148 ways a tree can be wrong, all found before anything runs

Every refusal names the file, the JSON path inside it, and the edit that fixes it. It judges every profile at once, so the production wiring is checked on a laptop with no database.

Example refusal · L009

A Redis lease inside a database transaction

Registering a customer writes two rows in one "atomic": true graph. Suppose someone adds a Redis lease so two registrations of the same tier never race:

{
  "id": "tierLease",
  "run": "@redis/lease.port.json#acquire",
  "in": { "key": "tier:{{in.tier}}", "ttlMs": 5000 }
}

The transaction can roll back. The lease cannot. If the second write fails, the rows vanish and Redis still says the tier is held for five seconds. Check, 0.64 s:

G008  @features/customers/data/store-and-latest.graph.json#nodes/tierLease
    node 'tierLease' is read by nothing
    → nothing routes to 'tierLease', so it runs on every branch: make it the 'to' of a switch rule, or read its answer from every branch
L009  @features/customers/data/store-and-latest.graph.json#nodes/tierLease
    atomic graph '@features/customers/data/store-and-latest.graph.json' reaches '@redis/lease.port.json#acquire', which cannot take part in a transaction (profile 'local')
    → read or send that outside the transaction: in the caller for a domain graph, in a graph of its own for a data graph
L009  @features/customers/data/store-and-latest.graph.json#nodes/tierLease
    atomic graph '@features/customers/domain/register-all.graph.json' reaches '@redis/lease.port.json#acquire', which cannot take part in a transaction (profile 'local')
    → read or send that outside the transaction: in the caller for a domain graph, in a graph of its own for a data graph

3 refusal(s)

Three findings from one node. The lease is outside the transaction it sits in. The CSV import's batch graph, two documents up, reaches it too and makes the same promise. And nobody reads whether the lease was actually taken.

wilanis ships no Redis plugin yet (RFC 0030 plans a Redis cache kind, not a lease). The run used a 20-line stub plugin declaring one port with no transactional marker, which is all the rule looks at. Marking the example's CSV import atomic trips the same rule with @http/http.port.json#request and @blob/csv.port.json#parse.

Claude Sonnet · the first write

“Add GET /customers/gold: the gold customers of the caller's tenant.”

Picked because it is one route, yet it touches sign-in, tenant isolation, three storage backends and an invariant. 44 s of reading before the first write. In order:

  1. CLAUDE.md, 150 lines that wilanis init writes: the loop, the layers, every refusal family
  2. list-customers.trigger.json, the nearest existing GET route, to copy
  3. customer.port.json, where it found listByTier already exists
  4. request.resolvers.json and customers.store.json, where the tenant comes from
  5. three bindings, to confirm listByTier is met under every profile
  6. shapes and the two invariants, to see what else the route owes

Then 90 s of write, check, fix:

"policies": [{ "policy": "@access/edge/signed-in.policy.json",
               "in": { "token": ["{{request.headers.authorization}}", "{{request.cookies.session}}"] } }],
"fire": { "run": "@customers/domain/customer.port.json#listByTier", "in": { "tier": "gold" } }

What it wrote

  • one trigger, 38 lines of JSON
  • one edge shape, 12 lines

What it never had to write

  • a handler, a SQL query, a REST call
  • a WHERE tenant = ?
  • token parsing

Delete its policies and the tree refuses to build, 0.6 s:

A006  @features/customers/edge/list-gold-customers.trigger.json#policies
    @features/customers/data/customers-postgres.store.json reads request.session.attributes.tenant as required, but trigger kind '@http/http.trigger-kind.json' hands it only sometimes and no policy of this trigger proves it (profile 'production')
    → gate this trigger with a policy whose proves lists "request.session.attributes.tenant", or drop required from the resolver and route around its absence
… the same for 'local' and 'production-scheduler', plus two T006 for the 401s nothing can now reach

5 refusal(s)

wilanis rehearse · fuzz · regress

Every branch run, every run replayed, in under a second each

rehearse fires every trigger with stubbed effects and solves inputs for every branch of every switch, including the invariant guards the compiler inserted. Nothing leaves the process.

$ npx wilanis rehearse . --profile local                                   0.78 s
features/customers/data/kept-get  switch 'isKept'  2/2 branches
  ok  when has(record)  answered from 'customer:made'
  ok  anything else     refused on purpose at 'noCustomer' as missing
features/access/domain/require-employee  switch 'isEmployee'  3/3 branches
  ok  when has(principal) && principal.realm == 'employee'  answered from 'granted'
  ok  when has(principal)                                   refused on purpose at 'forbidden'
  ok  anything else                                         refused on purpose at 'anonymous'
…
every branch settled -- 56 branch(es), 25 decision(s), 19 graph(s).

fuzz records runs as scenarios; regress replays them and diffs node by node. One template changed in the digest graph, a change that still type-checks:

$ npx wilanis regress . --profile local                                    0.52 s
@scenarios/digest.2.scenario.json: DIFF output changed; op.lines: out changed; op.joined: out changed; op.digest: out changed
@scenarios/digest.3.scenario.json: DIFF output changed; …
@scenarios/digest.4.scenario.json: DIFF output changed; …
97 more: same

What unit tests usually hold

  • the handler calls the right repository
  • types line up across layers
  • each if branch is reached
  • the 401 fires without a token
  • the output did not change

Who holds it in a wilanis tree

  • no handler exists; check proves the wiring
  • check, under every profile
  • rehearse, which solves for each rule
  • check (A, T) and rehearse
  • regress, naming the node that moved

A tree has no code of its own to unit-test. The code lives in plugins, tested once in their own packages. What rehearsal cannot tell you is how the real Postgres or upstream API answers, since it stubs them; that stays an integration test.

One microservice

The customers service

wilanis runs on Node today; the other languages show what each plugin would wrap in a runtime written there. None exists yet.

JSON TREE Triggers 15 routes 1 queue worker 3 commands Policies signed-in, can-register employees-only, otp Domain ports business graphs shapes invariants Data bindings data graphs stores Engine stateless, runs what check approved Caller Clock nothing scheduled Removals queue a Postgres table Customers API REST, profile live PostgreSQL S3 bucket when blobs names one CSV files OTel collector