0 The tree and its two rules
What the agent does
Nothing yet. The tree is the example as it ships, and it is judged before anyone touches it.
npx wilanis check .
What the tree answers
ok: 226 documents, IR v1, runtime reads v1
Why it matters when an agent writes it
Every one of the 226 is a JSON document and none is code, so there is nothing an agent can write that the checker does not read whole. The two sentences above are the rules a human wrote; step 1 is where the first is caught, step 7 the second. Neither is repeated anywhere else in the tree.
1 The agent scaffolds a route
What the agent does
The task is one line: add a way to archive a customer. The agent finds the remove operation on the customer port and scaffolds a route that fires it.
npx wilanis new trigger features/customers/edge/archive-customer . \ --run '@customers/domain/customer.port.json#remove' --kind '@http/http.trigger-kind.json' wrote features/customers/edge/archive-customer.trigger.json
What it wrote, features/customers/edge/archive-customer.trigger.json:
{
"$schema": "https://raw.githubusercontent.com/wilanis/wilanis-js/main/packages/core/schemas/trigger.schema.json",
"description": "TODO",
"kind": "@http/http.trigger-kind.json",
"settings": {
"route": "/todo",
"method": "GET",
"produces": "application/json"
},
"fire": {
"run": "@customers/domain/customer.port.json#remove"
}
}
Then the whole tree is judged.
npx wilanis check .
What the tree answers
T002 @features/customers/edge/archive-customer.trigger.json#in
'@customers/domain/customer.port.json#remove' takes {id: string} but the trigger declares no in
→ declare in on the trigger; wilanis describe @customers/domain/customer.port.json#remove shows what it takes
T002 @features/customers/edge/archive-customer.trigger.json#out
'@customers/domain/customer.port.json#remove' answers @features/customers/domain/Customer.shape.json but the trigger declares no out
→ declare out on the trigger, or fire an operation that answers nothing
A006 @features/customers/edge/archive-customer.trigger.json#policies
@features/customers/data/customers.store.json reads context.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 'local')
→ gate this trigger with a policy whose proves lists "context.session.attributes.tenant", or drop required from the resolver and route around its absence
A006 @features/customers/edge/archive-customer.trigger.json#policies
@features/customers/data/customers-postgres.store.json reads context.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 "context.session.attributes.tenant", or drop required from the resolver and route around its absence
T005 @features/customers/edge/archive-customer.trigger.json#settings/response/refusals
@features/customers/data/delete-row.graph.json may refuse with reason 'invariant', which settings.response.refusals does not map
→ add "invariant" under settings.response.refusals: how this trigger answers that outcome
T005 @features/customers/edge/archive-customer.trigger.json#settings/response/refusals
@features/customers/data/delete-row.graph.json may refuse with reason 'missing', which settings.response.refusals does not map
→ add "missing" under settings.response.refusals: how this trigger answers that outcome
T005 @features/customers/edge/archive-customer.trigger.json#settings/response/refusals
@features/customers/data/delete-row.graph.json may refuse with reason 'upstream', which settings.response.refusals does not map
→ add "upstream" under settings.response.refusals: how this trigger answers that outcome
I001 @features/customers/edge/archive-customer.trigger.json#policies
trigger reaches @features/customers/domain/customer.port.json#remove, which 'Writes are for registrars' (@features/customers/domain/writes-are-for-registrars.invariant.json) gates with @access/edge/can-register.policy.json, but attaches no such policy
→ attach "@access/edge/can-register.policy.json" under policies, or take @features/customers/domain/customer.port.json#remove out of the invariant's over
8 refusal(s)
Why it matters when an agent writes it
The agent never read the invariant, and the refusal does not need it to: I001 names the file the agent wrote a second ago, the rule it broke by its label, the file the rule lives in, and two edits, either of which would fix it. Each refusal has a code that is stable, a path inside the file (#policies, #settings/response/refusals) and a hint that is an edit, which is the whole of what an agent's loop needs: write, check, edit.
2 The agent follows the hints
What the agent does
Five of the eight hints are shape and status: the agent declares what the route takes and answers and maps the three reasons the operation can end with. It leaves the policy alone, since nothing yet told it why.
{
"$schema": "https://raw.githubusercontent.com/wilanis/wilanis-js/main/packages/core/schemas/trigger.schema.json",
"label": "POST /customers/{id}/archive",
"description": "Pasted over features/customers/edge/archive-customer.trigger.json in beat 3 of docs/demo.md: the route with its shapes and refusals filled in as the T002 and T005 hints said, and still no policy, so one refusal is left, I001.",
"kind": "@http/http.trigger-kind.json",
"settings": {
"route": "/customers/{id}/archive",
"method": "POST",
"produces": "application/json",
"response": {
"refusals": {
"missing": 404,
"upstream": 502,
"invariant": 500
}
}
},
"in": "@customers/edge/IdRequest.shape.json",
"out": "@customers/edge/CustomerView.shape.json",
"fire": {
"run": "@customers/domain/customer.port.json#remove",
"in": {
"id": "{{context.params.id}}"
}
}
}
Then the whole tree is judged.
npx wilanis check .
What the tree answers
A006 @features/customers/edge/archive-customer.trigger.json#policies
@features/customers/data/customers.store.json reads context.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 'local')
→ gate this trigger with a policy whose proves lists "context.session.attributes.tenant", or drop required from the resolver and route around its absence
A006 @features/customers/edge/archive-customer.trigger.json#policies
@features/customers/data/customers-postgres.store.json reads context.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 "context.session.attributes.tenant", or drop required from the resolver and route around its absence
I001 @features/customers/edge/archive-customer.trigger.json#policies
trigger reaches @features/customers/domain/customer.port.json#remove, which 'Writes are for registrars' (@features/customers/domain/writes-are-for-registrars.invariant.json) gates with @access/edge/can-register.policy.json, but attaches no such policy
→ attach "@access/edge/can-register.policy.json" under policies, or take @features/customers/domain/customer.port.json#remove out of the invariant's over
3 refusal(s)
Why it matters when an agent writes it
One round of edits took five refusals to zero and touched nothing else, because each hint said where and what. What remains is not about the route's shape but about who may call it: I001, a rule of the tree, says the same thing it said before, since the rule did not move; and the two A006, one per profile that listens and keeps a store, say the customer stores keep each tenant's rows apart, so the route needs a policy that proves the caller's session carries a tenant. All three point at #policies.
3 The agent does what the hint says, and no more
What the agent does
The I001 hint said: attach "@access/edge/can-register.policy.json" under policies. The agent adds exactly that line.
{
"$schema": "https://raw.githubusercontent.com/wilanis/wilanis-js/main/packages/core/schemas/trigger.schema.json",
"label": "POST /customers/{id}/archive",
"description": "Pasted over features/customers/edge/archive-customer.trigger.json in beat 3 of docs/demo.md: the route with its shapes and refusals filled in as the T002 and T005 hints said, and still no policy, so one refusal is left, I001.",
"kind": "@http/http.trigger-kind.json",
"settings": {
"route": "/customers/{id}/archive",
"method": "POST",
"produces": "application/json",
"response": {
"refusals": {
"missing": 404,
"upstream": 502,
"invariant": 500
}
}
},
"in": "@customers/edge/IdRequest.shape.json",
"out": "@customers/edge/CustomerView.shape.json",
"policies": [
"@access/edge/can-register.policy.json"
],
"fire": {
"run": "@customers/domain/customer.port.json#remove",
"in": {
"id": "{{context.params.id}}"
}
}
}
Then the whole tree is judged.
npx wilanis check .
What the tree answers
A005 @features/customers/edge/archive-customer.trigger.json#policies/0
policy '@features/access/edge/can-register.policy.json' reads context.principal, which the guard hands once it verified a token, but no attachment on this trigger gives one
→ write { "policy": "@access/edge/can-register.policy.json", "in": { "token": "{{context.headers.authorization}}" } } -- the read is where this kind hands the credential
T005 @features/customers/edge/archive-customer.trigger.json#settings/response/refusals
@features/access/domain/require-registrar.graph.json may refuse with reason 'forbidden', which settings.response.refusals does not map
→ add "forbidden" under settings.response.refusals: how this trigger answers that outcome
T005 @features/customers/edge/archive-customer.trigger.json#settings/response/refusals
@features/access/domain/require-registrar.graph.json may refuse with reason 'anonymous', which settings.response.refusals does not map
→ add "anonymous" under settings.response.refusals: how this trigger answers that outcome
3 refusal(s)
Why it matters when an agent writes it
Fixing one refusal surfaced three the agent could not have seen: the policy reads who is calling, and nothing on this route hands the guard a token to find out (A005, with the JSON to paste); and a gated route can now end forbidden or anonymous, which the route must map (T005). A rule catching a route is not one refusal but a conversation, and every turn of it is an edit the agent can make without reading a manual.
4 The finished route
What the agent does
The policy given the token as the hint wrote it, and 403, 401, 401 for forbidden, anonymous and invalid_credential.
{
"$schema": "https://raw.githubusercontent.com/wilanis/wilanis-js/main/packages/core/schemas/trigger.schema.json",
"label": "POST /customers/{id}/archive",
"description": "Pasted over features/customers/edge/archive-customer.trigger.json at the end of beat 3 of docs/demo.md: the finished route. The registrar policy is attached and given the token as the A005 hint said, and the two ways a gated route can end, forbidden and anonymous, are mapped as the T005 hints said; invalid_credential comes with them, since a bad token is a way in too.",
"kind": "@http/http.trigger-kind.json",
"settings": {
"route": "/customers/{id}/archive",
"method": "POST",
"produces": "application/json",
"response": {
"refusals": {
"missing": 404,
"upstream": 502,
"anonymous": 401,
"invalid_credential": 401,
"forbidden": 403,
"invariant": 500
}
}
},
"in": "@customers/edge/IdRequest.shape.json",
"out": "@customers/edge/CustomerView.shape.json",
"policies": [
{
"policy": "@access/edge/can-register.policy.json",
"in": {
"token": "{{context.headers.authorization}}"
}
}
],
"fire": {
"run": "@customers/domain/customer.port.json#remove",
"in": {
"id": "{{context.params.id}}"
}
}
}
Then the whole tree is judged.
npx wilanis check .
What the tree answers
ok: 227 documents, IR v1, runtime reads v1
Why it matters when an agent writes it
Three rounds, and the tree checks. The agent phrased the route its own way, and the invariant is the one sentence it could not talk its way past: however the route was written, it reached remove, and every way to remove is gated or refused. The human wrote that sentence once; the agent met it without ever opening the file.
5 Tests nobody wrote
What the agent does
No test was written for the route. The rehearsal runs every route, every policy and every branch of every switch with the effects stubbed.
npx wilanis rehearse . --profile local
Then the map: how a request flows, and what gates it.
npx wilanis map . --profile local
What the tree answers
features/access/domain/require-registrar switch 'isRegistrar' 3/3 branches ok when has(principal) && 'registrar' in principal.roles answered from 'granted' ok when has(principal) refused on purpose at 'forbidden' as forbidden: "registering customers takes the registrar role" ok anything else refused on purpose at 'anonymous' as anonymous: "sign in first: no token was presented" every branch settled -- 68 branch(es), 25 decision(s), 19 graph(s). 4 invariant(s) declared: A customer is reachable proved at 0 site(s), guarded at 30 An address names its domain proved at 0 site(s), guarded at 30 Writes are for registrars holds at 7 trigger(s) The session is the caller's holds at 3 trigger(s) "refused on purpose" is a refuse node the graph declares: a designed outcome with a reason the trigger maps, not a fault. A fault a switch catches is the graph deciding what breaking means. Effects are stubbed, so no request left this process.
the whole rehearsal
customers/domain/customer.port.json#enqueueRemoval (no branches) answers hello/domain/greeting.port.json#hello (no branches) answers access/domain/access.port.json#preferences (no branches) answers access/domain/access.port.json#savePreferences (no branches) answers access/domain/access.port.json#signOut (no branches) answers features/customers/data/kept-remove switch 'wasThere' 2/2 branches ok when has(record) answered from 'customer:made' ok anything else refused on purpose at 'noCustomer' as missing: "no customer hotel403" features/customers/data/kept-remove guard 'customer:check' A customer is reachable; An address names its domain 3/3 branches ok holds answered from 'customer' ok violated refused on purpose at 'customer:violated' as invariant: "'A customer is reachable' does not hold: len(name) > 0 && len(email) > 0 && (tier != 'gold' || has(note))" ok violated refused on purpose at 'customer:violated:2' as invariant: "'An address names its domain' does not hold: email contains '@'" features/customers/data/kept-list-every guard 'in:check' A customer is reachable; An address names its domain 3/3 branches ok holds answered from 'in:ok' ok violated refused on purpose at 'in:violated' as invariant: "'A customer is reachable' does not hold: len(name) > 0 && len(email) > 0 && (tier != 'gold' || has(note))" ok violated refused on purpose at 'in:violated:2' as invariant: "'An address names its domain' does not hold: email contains '@'" features/customers/data/kept-list guard 'in:check' A customer is reachable; An address names its domain 3/3 branches ok holds answered from 'in:ok' ok violated refused on purpose at 'in:violated' as invariant: "'A customer is reachable' does not hold: len(name) > 0 && len(email) > 0 && (tier != 'gold' || has(note))" ok violated refused on purpose at 'in:violated:2' as invariant: "'An address names its domain' does not hold: email contains '@'" features/customers/data/write-csv guard 'in:check' A customer is reachable; An address names its domain 3/3 branches ok holds held upstream by guard 'in:check' of 'customers' in features/customers/data/kept-list, which judged this value first ok violated held upstream by guard 'in:check' of 'customers' in features/customers/data/kept-list, which judged this value first ok violated held upstream by guard 'in:check' of 'customers' in features/customers/data/kept-list, which judged this value first 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: "no customer hotel403" features/customers/data/kept-get guard 'customer:check' A customer is reachable; An address names its domain 3/3 branches ok holds answered from 'customer' ok violated refused on purpose at 'customer:violated' as invariant: "'A customer is reachable' does not hold: len(name) > 0 && len(email) > 0 && (tier != 'gold' || has(note))" ok violated refused on purpose at 'customer:violated:2' as invariant: "'An address names its domain' does not hold: email contains '@'" features/customers/data/store-and-latest (atomic) switch 'bothWritten' 3/3 branches ok when has(violated) refused on purpose at 'repeated' as conflict: "a customer already uses Zulu-9 @ (juliet527)", rolled back ok when has(record) && has(mark) answered from 'customer:made' ok anything else refused on purpose at 'nothingWritten' as upstream: "the store answered no record for Zulu-9 @", rolled back features/customers/data/store-and-latest (atomic) guard 'in:check' A customer is reachable; An address names its domain 3/3 branches ok holds held upstream by guard 'customer:check' in features/customers/domain/register-customer, which judged this value first ok violated held upstream by guard 'customer:check' in features/customers/domain/register-customer, which judged this value first ok violated held upstream by guard 'customer:check' in features/customers/domain/register-customer, which judged this value first features/customers/data/store-and-latest (atomic) guard 'customer:check' A customer is reachable; An address names its domain 3/3 branches ok holds answered from 'customer' ok violated refused on purpose at 'customer:violated' as invariant: "'A customer is reachable' does not hold: len(name) > 0 && len(email) > 0 && (tier != 'gold' || has(note))", rolled back ok violated refused on purpose at 'customer:violated:2' as invariant: "'An address names its domain' does not hold: email contains '@'", rolled back features/customers/domain/register-customer guard 'customer:check' A customer is reachable; An address names its domain 3/3 branches ok holds answered from 'customer' ok violated refused on purpose at 'customer:violated' as invariant: "'A customer is reachable' does not hold: len(name) > 0 && len(email) > 0 && (tier != 'gold' || has(note))" ok violated refused on purpose at 'customer:violated:2' as invariant: "'An address names its domain' does not hold: email contains '@'" features/customers/domain/list-customers switch 'tierGiven' 2/2 branches ok when has(tier) answered from 'byTier' ok anything else answered from 'all' features/customers/data/kept-list-by-tier guard 'in:check' A customer is reachable; An address names its domain 3/3 branches ok holds answered from 'in:ok' ok violated refused on purpose at 'in:violated' as invariant: "'A customer is reachable' does not hold: len(name) > 0 && len(email) > 0 && (tier != 'gold' || has(note))" ok violated refused on purpose at 'in:violated:2' as invariant: "'An address names its domain' does not hold: email contains '@'" features/customers/data/keep-customer switch 'outcome' 3/3 branches ok when has(violated) refused on purpose at 'repeated' as conflict: "a customer already uses bravo @ (juliet527)" ok when has(record) answered from 'kept:made' ok anything else refused on purpose at 'nothingWritten' as upstream: "the store answered no record for hotel403" features/customers/data/keep-customer guard 'in:check' A customer is reachable; An address names its domain 3/3 branches ok holds held upstream by guard 'customer:check' in features/customers/domain/update-customer, which judged this value first ok violated held upstream by guard 'customer:check' in features/customers/domain/update-customer, which judged this value first ok violated held upstream by guard 'customer:check' in features/customers/domain/update-customer, which judged this value first features/customers/data/keep-customer guard 'kept:check' A customer is reachable; An address names its domain 3/3 branches ok holds answered from 'kept' ok violated refused on purpose at 'kept:violated' as invariant: "'A customer is reachable' does not hold: len(name) > 0 && len(email) > 0 && (tier != 'gold' || has(note))" ok violated refused on purpose at 'kept:violated:2' as invariant: "'An address names its domain' does not hold: email contains '@'" features/customers/domain/update-customer guard 'customer:check' A customer is reachable; An address names its domain 3/3 branches ok holds answered from 'customer' ok violated refused on purpose at 'customer:violated' as invariant: "'A customer is reachable' does not hold: len(name) > 0 && len(email) > 0 && (tier != 'gold' || has(note))" ok violated refused on purpose at 'customer:violated:2' as invariant: "'An address names its domain' does not hold: email contains '@'" features/access/domain/sign-in-customer switch 'verdict' 3/3 branches ok when status == 'verified' && has(identity) answered from 'issued' ok when status == 'rejected' refused on purpose at 'rejected' as bad_credentials: "the username or password is wrong" ok anything else refused on purpose at 'unavailable' as directory_unavailable: "the directory could not be asked" features/access/domain/sign-in-employee switch 'verdict' 3/3 branches ok when status == 'verified' && has(identity) answered from 'issued' ok when status == 'rejected' refused on purpose at 'rejected' as bad_credentials: "the username or password is wrong" ok anything else refused on purpose at 'unavailable' as directory_unavailable: "the directory could not be asked" features/access/data/issue-code switch 'wasThere' 2/2 branches ok when issued == true && has(id) && has(code) && has(expiresAt) answered from 'code' ok anything else refused on purpose at 'unknown' as unknown_challenge: "challenge delta is unknown or has expired" features/access/domain/refresh switch 'wasGood' 2/2 branches ok when refreshed == true && has(tokens) answered from 'renewed' ok anything else refused on purpose at 'invalid' as invalid_refresh: "the refresh token is unknown, spent or expired" features/access/domain/require-registrar switch 'isRegistrar' 3/3 branches ok when has(principal) && 'registrar' in principal.roles answered from 'granted' ok when has(principal) refused on purpose at 'forbidden' as forbidden: "registering customers takes the registrar role" ok anything else refused on purpose at 'anonymous' as anonymous: "sign in first: no token was presented" 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' as forbidden: "this is for employees" ok anything else refused on purpose at 'anonymous' as anonymous: "sign in first: no token was presented" features/access/domain/require-otp switch 'codeAnswered' 2/2 branches ok when has(challenge) && challenge.verified == true answered from 'granted' ok anything else refused on purpose at 'noCode' as otp: "this command needs a one-time code" features/access/domain/require-signed-in switch 'anyoneThere' 2/2 branches ok when has(principal) answered from 'granted' ok anything else refused on purpose at 'anonymous' as anonymous: "sign in first: no token was presented" every branch settled -- 68 branch(es), 25 decision(s), 19 graph(s). 4 invariant(s) declared: A customer is reachable proved at 0 site(s), guarded at 30 An address names its domain proved at 0 site(s), guarded at 30 Writes are for registrars holds at 7 trigger(s) The session is the caller's holds at 3 trigger(s) "refused on purpose" is a refuse node the graph declares: a designed outcome with a reason the trigger maps, not a fault. A fault a switch catches is the graph deciding what breaking means. Effects are stubbed, so no request left this process.
@features/customers/edge/archive-customer.trigger.json (@http/http.trigger-kind.json) route "/customers/{id}/archive", method "POST", produces "application/json"
gated by @features/access/edge/can-register.policy.json → @access/domain/access.port.json#requireRegistrar given token
holds @features/customers/domain/writes-are-for-registrars.invariant.json through @features/access/edge/can-register.policy.json
@customers/domain/customer.port.json#remove
@features/customers/data/kept-remove.graph.json
removed @storage/store.port.json#remove (effect) → store @features/customers/data/customers.store.json customers (remove), scoped by tenant
wasThere [switch → customer | noCustomer]
customer @std/object.port.json#make
noCustomer @std/outcome.port.json#refuse
the whole map
@features/customers/edge/archive-customer.trigger.json (@http/http.trigger-kind.json) route "/customers/{id}/archive", method "POST", produces "application/json"
gated by @features/access/edge/can-register.policy.json → @access/domain/access.port.json#requireRegistrar given token
holds @features/customers/domain/writes-are-for-registrars.invariant.json through @features/access/edge/can-register.policy.json
@customers/domain/customer.port.json#remove
@features/customers/data/kept-remove.graph.json
removed @storage/store.port.json#remove (effect) → store @features/customers/data/customers.store.json customers (remove), scoped by tenant
wasThere [switch → customer | noCustomer]
customer @std/object.port.json#make
noCustomer @std/outcome.port.json#refuse
@features/customers/edge/delete-customer.trigger.json (@http/http.trigger-kind.json) route "/customers/{id}", method "DELETE", produces "application/json"
gated by @features/access/edge/employees-only.policy.json → @access/domain/access.port.json#requireEmployee given token
gated by @features/access/edge/can-register.policy.json → @access/domain/access.port.json#requireRegistrar
holds @features/customers/domain/writes-are-for-registrars.invariant.json through @features/access/edge/can-register.policy.json
@customers/domain/customer.port.json#remove
@features/customers/data/kept-remove.graph.json
removed @storage/store.port.json#remove (effect) → store @features/customers/data/customers.store.json customers (remove), scoped by tenant
wasThere [switch → customer | noCustomer]
customer @std/object.port.json#make
noCustomer @std/outcome.port.json#refuse
@features/customers/edge/delete-customers.trigger.json (@http/http.trigger-kind.json) route "/customers", method "DELETE", consumes "application/json", produces "application/json"
gated by @features/access/edge/employees-only.policy.json → @access/domain/access.port.json#requireEmployee given token
gated by @features/access/edge/can-register.policy.json → @access/domain/access.port.json#requireRegistrar
holds @features/customers/domain/writes-are-for-registrars.invariant.json through @features/access/edge/can-register.policy.json
@customers/domain/customer.port.json#removeMany
@features/customers/domain/remove-customers.graph.json
removed @customers/domain/customer.port.json#remove
@features/customers/data/customers-store.binding.json#remove
@features/customers/data/kept-remove.graph.json
removed @storage/store.port.json#remove (effect) → store @features/customers/data/customers.store.json customers (remove), scoped by tenant
wasThere [switch → customer | noCustomer]
customer @std/object.port.json#make
noCustomer @std/outcome.port.json#refuse
@features/customers/edge/digest.trigger.json (@cli/cli.trigger-kind.json) command "digest"
gated by @features/access/edge/employees-only.policy.json → @access/domain/access.port.json#requireEmployee given token
@customers/domain/customer.port.json#digest
@features/customers/domain/digest.graph.json
all @customers/domain/customer.port.json#listEvery
@features/customers/data/customers-store.binding.json#listEvery
@features/customers/data/kept-list-every.graph.json
customers @storage/store.port.json#find (effect) → store @features/customers/data/customers.store.json everyCustomer (find)
lines @std/text.port.json#fill
joined @std/text.port.json#join
count @std/list.port.json#count
active [keeps]
activeCount @std/list.port.json#count
digest @std/object.port.json#make
@features/customers/edge/enqueue-removal.trigger.json (@http/http.trigger-kind.json) route "/customers/{id}/removal", method "POST", produces "application/json"
gated by @features/access/edge/employees-only.policy.json → @access/domain/access.port.json#requireEmployee given token
gated by @features/access/edge/can-register.policy.json → @access/domain/access.port.json#requireRegistrar
@customers/domain/customer.port.json#enqueueRemoval
@features/customers/data/publish-removal.graph.json
published @queue/queue.port.json#publish (effect) → @connections/jobs.connection.json queue "removals" → @features/customers/edge/remove-queued.trigger.json
queued @std/object.port.json#make
@features/customers/edge/export-customers.trigger.json (@http/http.trigger-kind.json) route "/customers.csv", method "GET", produces "text/csv"
gated by @features/access/edge/signed-in.policy.json → @access/domain/access.port.json#requireSignedIn given token
@customers/domain/customer.port.json#export
@features/customers/domain/export-customers.graph.json
all @customers/domain/customer.port.json#listAll
@features/customers/data/customers-store.binding.json#listAll
@features/customers/data/kept-list.graph.json
customers @storage/store.port.json#find (effect) → store @features/customers/data/customers.store.json customers (find), scoped by tenant
file @customers/domain/customer.port.json#toCsv
@features/customers/data/customers-store.binding.json#toCsv
@features/customers/data/write-csv.graph.json
file @blob/csv.port.json#write (effect)
@features/customers/edge/get-customer.trigger.json (@http/http.trigger-kind.json) route "/customers/{id}", method "GET", produces "application/json"
gated by @features/access/edge/signed-in.policy.json → @access/domain/access.port.json#requireSignedIn given token
@customers/domain/customer.port.json#get
@features/customers/data/kept-get.graph.json
stored @storage/store.port.json#get (effect) → store @features/customers/data/customers.store.json customers (get), scoped by tenant
isKept [switch → customer | noCustomer]
customer @std/object.port.json#make
noCustomer @std/outcome.port.json#refuse
@features/customers/edge/import-customers.trigger.json (@http/http.trigger-kind.json) route "/customers.csv", method "POST", consumes "text/csv", produces "application/json"
gated by @features/access/edge/employees-only.policy.json → @access/domain/access.port.json#requireEmployee given token
gated by @features/access/edge/can-register.policy.json → @access/domain/access.port.json#requireRegistrar
holds @features/customers/domain/writes-are-for-registrars.invariant.json through @features/access/edge/can-register.policy.json
@customers/domain/customer.port.json#import
@features/customers/domain/import-customers.graph.json
drafts @customers/domain/customer.port.json#parseDrafts
@features/customers/data/customers-store.binding.json#parseDrafts
@features/customers/data/parse-drafts.graph.json
drafts @blob/csv.port.json#parse (effect)
recorded @customers/domain/customer.port.json#registerAll
@features/customers/data/customers-store.binding.json#registerAll
@features/customers/domain/register-all.graph.json [atomic]
recorded @customers/domain/customer.port.json#submit
@features/customers/data/customers-store.binding.json#submit
@features/customers/domain/register-customer.graph.json
id @customers/domain/customer.port.json#nextId
@features/customers/data/customers-store.binding.json#nextId
@features/customers/data/next-id.graph.json
key @storage/store.port.json#newKey (effect) → store @features/customers/data/customers.store.json customers (newKey)
customer @std/object.port.json#make
registered @customers/domain/customer.port.json#register
@features/customers/data/customers-store.binding.json#register
@features/customers/data/store-and-latest.graph.json [atomic]
stored @storage/store.port.json#put (effect) → store @features/customers/data/customers.store.json customers (put), scoped by tenant
tierLatest @std/object.port.json#make
latest @storage/store.port.json#put (effect) → store @features/customers/data/customers.store.json latest (put)
bothWritten [switch → repeated | customer | nothingWritten]
customer @std/object.port.json#make
repeated @std/outcome.port.json#refuse
nothingWritten @std/outcome.port.json#refuse
@features/customers/edge/list-customers.trigger.json (@http/http.trigger-kind.json) route "/customers", method "GET", produces "application/json"
gated by @features/access/edge/signed-in.policy.json → @access/domain/access.port.json#requireSignedIn given token
@customers/domain/customer.port.json#list
@features/customers/domain/list-customers.graph.json
tierGiven [switch → byTier | all]
byTier @customers/domain/customer.port.json#listByTier
@features/customers/data/customers-store.binding.json#listByTier
@features/customers/data/kept-list-by-tier.graph.json
customers @storage/store.port.json#find (effect) → store @features/customers/data/customers.store.json customers (find), scoped by tenant
all @customers/domain/customer.port.json#listAll
@features/customers/data/customers-store.binding.json#listAll
@features/customers/data/kept-list.graph.json
customers @storage/store.port.json#find (effect) → store @features/customers/data/customers.store.json customers (find), scoped by tenant
@features/customers/edge/register-customer.trigger.json (@http/http.trigger-kind.json) route "/customers", method "POST", consumes "application/json", produces "application/json"
gated by @features/access/edge/employees-only.policy.json → @access/domain/access.port.json#requireEmployee given token
gated by @features/access/edge/can-register.policy.json → @access/domain/access.port.json#requireRegistrar
holds @features/customers/domain/writes-are-for-registrars.invariant.json through @features/access/edge/can-register.policy.json
@customers/domain/customer.port.json#submit
@features/customers/domain/register-customer.graph.json
id @customers/domain/customer.port.json#nextId
@features/customers/data/customers-store.binding.json#nextId
@features/customers/data/next-id.graph.json
key @storage/store.port.json#newKey (effect) → store @features/customers/data/customers.store.json customers (newKey)
customer @std/object.port.json#make
registered @customers/domain/customer.port.json#register
@features/customers/data/customers-store.binding.json#register
@features/customers/data/store-and-latest.graph.json [atomic]
stored @storage/store.port.json#put (effect) → store @features/customers/data/customers.store.json customers (put), scoped by tenant
tierLatest @std/object.port.json#make
latest @storage/store.port.json#put (effect) → store @features/customers/data/customers.store.json latest (put)
bothWritten [switch → repeated | customer | nothingWritten]
customer @std/object.port.json#make
repeated @std/outcome.port.json#refuse
nothingWritten @std/outcome.port.json#refuse
@features/customers/edge/remove-queued.trigger.json (@queue/queue.trigger-kind.json) connection "@connections/jobs.connection.json", queue "removals", maxAttempts 5, backoffMs 1000
sent by @features/customers/data/publish-removal.graph.json#published
gated by @features/access/edge/employees-only.policy.json → @access/domain/access.port.json#requireEmployee given token
gated by @features/access/edge/can-register.policy.json → @access/domain/access.port.json#requireRegistrar
holds @features/customers/domain/writes-are-for-registrars.invariant.json through @features/access/edge/can-register.policy.json
@customers/domain/customer.port.json#remove
@features/customers/data/kept-remove.graph.json
removed @storage/store.port.json#remove (effect) → store @features/customers/data/customers.store.json customers (remove), scoped by tenant
wasThere [switch → customer | noCustomer]
customer @std/object.port.json#make
noCustomer @std/outcome.port.json#refuse
@features/customers/edge/update-customer.trigger.json (@http/http.trigger-kind.json) route "/customers/{id}", method "PUT", consumes "application/json", produces "application/json"
gated by @features/access/edge/employees-only.policy.json → @access/domain/access.port.json#requireEmployee given token
gated by @features/access/edge/can-register.policy.json → @access/domain/access.port.json#requireRegistrar
holds @features/customers/domain/writes-are-for-registrars.invariant.json through @features/access/edge/can-register.policy.json
@customers/domain/customer.port.json#update
@features/customers/domain/update-customer.graph.json
current @customers/domain/customer.port.json#get
@features/customers/data/customers-store.binding.json#get
@features/customers/data/kept-get.graph.json
stored @storage/store.port.json#get (effect) → store @features/customers/data/customers.store.json customers (get), scoped by tenant
isKept [switch → customer | noCustomer]
customer @std/object.port.json#make
noCustomer @std/outcome.port.json#refuse
customer @std/object.port.json#merge
kept @customers/domain/customer.port.json#keep
@features/customers/data/customers-store.binding.json#keep
@features/customers/data/keep-customer.graph.json
stored @storage/store.port.json#put (effect) → store @features/customers/data/customers.store.json customers (put), scoped by tenant
outcome [switch → repeated | kept | nothingWritten]
kept @std/object.port.json#make
repeated @std/outcome.port.json#refuse
nothingWritten @std/outcome.port.json#refuse
@features/hello/edge/hello-gated.trigger.json (@cli/cli.trigger-kind.json) command "hello-gated"
gated by @features/access/edge/otp-verified.policy.json → @access/domain/access.port.json#requireOtp given challenge
@hello/domain/greeting.port.json#hello
@features/hello/domain/greet.graph.json
said @std/text.port.json#fill
greeting @std/object.port.json#make
@features/access/edge/auth-customers.trigger.json (@http/http.trigger-kind.json) route "/api/v1/auth-customers", method "POST", consumes "application/json", produces "application/json"
@access/domain/access.port.json#signInCustomer
@features/access/domain/sign-in-customer.graph.json
checked @access/domain/identity.port.json#verifyCustomer
@features/directories/data/identity.binding.json#verifyCustomer → @auth/identity.port.json#verify
verdict [switch → issued | rejected | unavailable]
issued @access/domain/identity.port.json#issue
@features/directories/data/identity.binding.json#issue → @auth/token.port.json#issue
rejected @std/outcome.port.json#refuse
unavailable @std/outcome.port.json#refuse
@features/access/edge/auth-employees.trigger.json (@http/http.trigger-kind.json) route "/api/v1/auth-employees", method "POST", consumes "application/json", produces "application/json"
@access/domain/access.port.json#signInEmployee
@features/access/domain/sign-in-employee.graph.json
checked @access/domain/identity.port.json#verifyEmployee
@features/directories/data/identity.binding.json#verifyEmployee → @auth/identity.port.json#verify
verdict [switch → issued | rejected | unavailable]
issued @access/domain/identity.port.json#issue
@features/directories/data/identity.binding.json#issue → @auth/token.port.json#issue
rejected @std/outcome.port.json#refuse
unavailable @std/outcome.port.json#refuse
@features/access/edge/get-preferences.trigger.json (@http/http.trigger-kind.json) route "/api/v1/me/preferences", method "GET", produces "application/json"
gated by @features/access/edge/signed-in.policy.json → @access/domain/access.port.json#requireSignedIn given token
holds @features/directories/domain/the-session-is-the-callers.invariant.json through @features/access/edge/signed-in.policy.json (proves context.principal)
@access/domain/access.port.json#preferences
@features/access/data/read-session.graph.json
attributes @auth/session.port.json#get (effect)
@features/access/edge/issue-otp.trigger.json (@cli/cli.trigger-kind.json) command "issue-otp"
@access/domain/access.port.json#deliverCode
@features/access/data/issue-code.graph.json
issued @auth/challenge.port.json#issue (effect)
wasThere [switch → code | unknown]
code @std/object.port.json#make
unknown @std/outcome.port.json#refuse
@features/access/edge/refresh.trigger.json (@http/http.trigger-kind.json) route "/api/v1/token/refresh", method "POST", consumes "application/json", produces "application/json"
@access/domain/access.port.json#refresh
@features/access/domain/refresh.graph.json
traded @access/domain/identity.port.json#refresh
@features/directories/data/identity.binding.json#refresh → @auth/token.port.json#refresh
wasGood [switch → renewed | invalid]
renewed @std/object.port.json#make
invalid @std/outcome.port.json#refuse
@features/access/edge/set-preferences.trigger.json (@http/http.trigger-kind.json) route "/api/v1/me/preferences", method "PUT", consumes "application/json", produces "application/json"
gated by @features/access/edge/signed-in.policy.json → @access/domain/access.port.json#requireSignedIn given token
holds @features/directories/domain/the-session-is-the-callers.invariant.json through @features/access/edge/signed-in.policy.json (proves context.principal)
@access/domain/access.port.json#savePreferences
@features/access/data/write-theme.graph.json
written @auth/session.port.json#set (effect)
@features/access/edge/sign-out.trigger.json (@http/http.trigger-kind.json) route "/api/v1/sign-out", method "POST", produces "application/json"
gated by @features/access/edge/signed-in.policy.json → @access/domain/access.port.json#requireSignedIn given token
holds @features/directories/domain/the-session-is-the-callers.invariant.json through @features/access/edge/signed-in.policy.json (proves context.principal)
@access/domain/access.port.json#signOut
@features/access/data/end-session.graph.json
ended @auth/session.port.json#end (effect)
unreached under local @features/customers/data/delete-row.graph.json bound by @features/customers/data/customers-rest.binding.json
unreached under local @features/customers/data/get-row.graph.json bound by @features/customers/data/customers-rest.binding.json
unreached under local @features/customers/data/kept-get-postgres.graph.json bound by @features/customers/data/customers-postgres.binding.json
unreached under local @features/customers/data/kept-remove-postgres.graph.json bound by @features/customers/data/customers-postgres.binding.jsonWhy it matters when an agent writes it
This is the test suite the agent did not write. The three branches of the registrar decision each ran to a declared outcome, and the summary counts the new route among the seven the invariant holds at, up from six before the agent began. The map says in one line what step 1 said as a refusal: the route holds the invariant, through the policy.
Note
map today prints the graphs of all three bindings under #remove and ?? lines under nested domain calls although a profile was given (#481), and ends with 17 orphan lines for graphs the tree does reach (#488). Both are shown as printed.
6 Live
What the agent does
Serve it. The key the tree signs its tokens with is the one secret it reads from the environment, and the only thing this run needed from outside the tree.
npm run start -- --profile local
No token:
curl -s -X POST localhost:8099/customers/x/archive
As cy, who holds the viewer group and not registrar:
TOKEN=$(curl -s -X POST localhost:8099/api/v1/auth-employees -H 'content-type: application/json' \
-d '{"username":"cy","password":"cy-pass"}' | sed -n 's/.*"accessToken":"\([^"]*\)".*/\1/p')
curl -s -X POST localhost:8099/customers/x/archive -H "authorization: Bearer $TOKEN"
As bo, a registrar: register a customer, then archive them.
TOKEN=$(curl -s -X POST localhost:8099/api/v1/auth-employees -H 'content-type: application/json' \
-d '{"username":"bo","password":"bo-pass"}' | sed -n 's/.*"accessToken":"\([^"]*\)".*/\1/p')
curl -s -X POST localhost:8099/customers -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"name":"Ada Lovelace","email":"ada@example.com","tier":"bronze"}'
curl -s -X POST localhost:8099/customers/$ID/archive -H "authorization: Bearer $TOKEN"
What the tree answers
401 {"reason":"anonymous","message":"sign in first: no token was presented"}
As cy, beside the rehearsal's forbidden line from step 5:
403 {"reason":"forbidden","message":"registering customers takes the registrar role"}
ok when has(principal) refused on purpose at 'forbidden' as forbidden: "registering customers takes the registrar role"
201 {"id":"156190ba-6a86-40da-afdb-0756e3ef8c7b","name":"Ada Lovelace","email":"ada@example.com","tier":"bronze"}
200 {"id":"156190ba-6a86-40da-afdb-0756e3ef8c7b","name":"Ada Lovelace","email":"ada@example.com","tier":"bronze"}
Why it matters when an agent writes it
The 403 and the rehearsal line are the same branch and the same sentence: the rehearsal ran it with the effects stubbed before the server was up, the route mapped the reason to a status in step 4, and the guard, the policy and the graph did the rest. The route the agent wrote never saw the token; the guard verified it before any graph ran. No document validates a token and no graph checks access, so there is nothing of that kind an agent can get wrong.
7 All of it or none of it
What the agent does
A file of five customers whose fifth row is not a customer: the address is empty.
name,email,tier Ada Lovelace,ada@example.com,bronze Grace Hopper,grace@example.com,silver Alan Turing,alan@example.com,bronze Edsger Dijkstra,edsger@example.com,silver Barbara Liskov,,silver
Import it as bo, then list.
curl -s -X POST localhost:8099/customers.csv -H 'content-type: text/csv' -H "authorization: Bearer $TOKEN" \ --data-binary @customers.bad.csv curl -s localhost:8099/customers -H "authorization: Bearer $TOKEN"
What the tree answers
500 {"reason":"invariant","message":"'A customer is reachable' does not hold: len(name) > 0 && len(email) > 0 && (tier != 'gold' || has(note))"}
200 []
The line in features/customers/domain/register-all.graph.json that made it so:
"atomic": true,
Why it matters when an agent writes it
Four good rows went in before the fifth refused, and bo's tenant holds none of them. The 500 carries the second rule in its own words, and nobody wrote that message. The graph says one word, atomic, and the compiler refused to accept it anywhere the effects could not be one transaction, so the word is checked rather than trusted. An agent that writes a graph with that word gets the promise or a refusal, never a half-written store.
8 An edit that never reaches the serving tree
What the agent does
With the server still running, the agent puts the step 2 file back over the route: the one without the policy.
cp archive-customer.step2.trigger.json features/customers/edge/archive-customer.trigger.json
Then, with the finished file back in place:
cp archive-customer.step3.trigger.json features/customers/edge/archive-customer.trigger.json
What the tree answers
The server's log, and the route while the bad file sat on disk:
reload refused, still serving the last good tree:
A006 @features/customers/edge/archive-customer.trigger.json#policies
@features/customers/data/customers.store.json reads context.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 'local')
→ gate this trigger with a policy whose proves lists "context.session.attributes.tenant", or drop required from the resolver and route around its absence
A006 @features/customers/edge/archive-customer.trigger.json#policies
@features/customers/data/customers-postgres.store.json reads context.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 "context.session.attributes.tenant", or drop required from the resolver and route around its absence
I001 @features/customers/edge/archive-customer.trigger.json#policies
trigger reaches @features/customers/domain/customer.port.json#remove, which 'Writes are for registrars' (@features/customers/domain/writes-are-for-registrars.invariant.json) gates with @access/edge/can-register.policy.json, but attaches no such policy
→ attach "@access/edge/can-register.policy.json" under policies, or take @features/customers/domain/customer.port.json#remove out of the invariant's over
401 {"reason":"anonymous","message":"sign in first: no token was presented"}
reload: 227 documents, serving the new tree
Why it matters when an agent writes it
The edit never reached the serving tree. A document that does not pass wilanis check is reported in the log with the same code and hint the agent saw in step 1, and the last good tree keeps answering, so an agent editing a tree that is live cannot make the write public for even one request. The check that judged the file at rest is the one that guards it while it serves.