The route written a year later

An agent given a one-line task writes a route fast and plausibly; what it lacks is the rule it never read and the consequence it cannot see. In this tree the rules are two sentences a human wrote once, in files of their own, and the compiler holds every document to them before anything runs, answering each miss with a code, the file, the path inside it, and the edit that fixes it. This page is the record of one real run: an agent adds a write route to the example, and every output below is what the tree printed.

features/customers/domain/writes-are-for-registrars.invariant.json — caught at step 1
{
  "$schema": "https://raw.githubusercontent.com/wilanis/wilanis-js/main/packages/core/schemas/invariant.schema.json",
  "label": "Writes are for registrars",
  "description": "Every trigger that can change a customer attaches the registrar policy. Reads take any signed-in caller, of their own tenant. Without this the six write triggers are six coincidences: a new route firing customer.update and forgetting the policy would check clean, and the write would be public.",
  "access": {
    "over": [
      "@customers/domain/customer.port.json#register",
      "@customers/domain/customer.port.json#update",
      "@customers/domain/customer.port.json#keep",
      "@customers/domain/customer.port.json#remove",
      "@customers/domain/customer.port.json#removeMany",
      "@customers/domain/customer.port.json#submit",
      "@customers/domain/customer.port.json#import"
    ],
    "requires": {
      "policy": "@access/edge/can-register.policy.json"
    }
  }
}
features/customers/domain/a-customer-is-reachable.invariant.json — caught at step 7
{
  "$schema": "https://raw.githubusercontent.com/wilanis/wilanis-js/main/packages/core/schemas/invariant.schema.json",
  "label": "A customer is reachable",
  "description": "A customer can always be reached, and a gold account always says why it is gold: a name and an address are never empty, and a customer of the gold tier carries the note explaining it. Every graph that makes or takes a customer is held to this, wherever the value comes from.",
  "holds": {
    "on": "@customers/domain/Customer.shape.json",
    "when": "len(name) > 0 && len(email) > 0 && (tier != 'gold' || has(note))"
  }
}

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.json

Why 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.