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.
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.
- LLayers and effects 12a business graph calling the database, a lock inside a transaction
- GGraphs 24an effect that runs on every branch, a retry on a POST
- AAccess 8a route reading the tenant with nothing proving who is calling
- IInvariants 6a new write route that skips the rule every other route follows
- TTriggers 10a refusal the route can reach but never maps to a status
- BBindings and profiles 12a port nobody implements under production
- CConnections and stores 19a unique constraint on a field the shape lacks
- PStatic fields and resolvers 6a resolver reading a request field no trigger kind hands
- DDocuments 12a trigger filed in the data layer
- XPlugin rules 35a session write outside the session shape, a cron that is not one
- SScenarios 3a recorded run pinning a refusal that never happened
- RReferences 1a path to a document that does not exist
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:
CLAUDE.md, 150 lines thatwilanis initwrites: the loop, the layers, every refusal familylist-customers.trigger.json, the nearest existing GET route, to copycustomer.port.json, where it foundlistByTieralready existsrequest.resolvers.jsonandcustomers.store.json, where the tenant comes from- three bindings, to confirm
listByTieris met under every profile - shapes and the two invariants, to see what else the route owes
Then 90 s of write, check, fix:
- T003 T002gave the operation a tier but declared no input shape
- L001reached for a core shape at the edge
- T002used the list route's shape, where tier is optional
- oka one-field edge shape; 223 documents check, 56 branches rehearse
"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
ifbranch is reached - the 401 fires without a token
- the output did not change
Who holds it in a wilanis tree
- no handler exists;
checkproves the wiring check, under every profilerehearse, which solves for each rulecheck(A, T) andrehearseregress, 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.