Three models are each given the same one-paragraph task in identical copies of the example, unsupervised: add an operation that toggles a boolean on a customer and expose it over HTTP. Beside the task sits an acceptance test the reviewer wrote first, accept.sh, and the agent is told its work is done when the test prints PASS on every criterion. The test asserts what the endpoint must do and what the checker and the rehearsal must say; it never asserts how the tree is written. One thing it reports without judging: the tree's access rule, Writes are for registrars, names its write operations one by one, and the toggle is a write the rule does not name. Whether a toggle is a write the rule should cover is each agent's to decide, and the gate accepts either answer. This page records what each did, what each decided, what it cost, and the gate's own verdict on each result, run again by the reviewer.
{
"$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 stay public. 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#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"
}
}
}
Before the run, each copy gained a port of its own, the CLAUDE.md and hooks that `wilanis init` writes for a consumer tree, the graph-idiom skill and the Stop hook this harness carries provisionally (issues #494 and #495), a hook that keeps the agent inside its copy, the acceptance test, and a git commit as the baseline every diff below is measured against. The `active` flag the task toggles is a field of `Customer` as the example ships it, so nothing was planted. `wilanis check` passed on every copy and the gate rejected every copy on the criteria about the endpoint and nothing else. The rule above is the one already in the tree; nothing was added to it.
This is an unsupervised run. Nobody will answer a question, so do not ask one: decide, act, and report what you decided and why.
You are working in a wilanis project at <arena>/<model>. It is a registry of customers: records with a name, an email address and a tier, kept in a store, served over HTTP with sign-in and policies. Everything in it is JSON documents; there is no code to write. Read its CLAUDE.md and README.md first; they say how the language works and what commands exist. Work only inside that directory. The wilanis packages under its node_modules are installed software: use them through the `wilanis` command (`npx wilanis ...` from inside the directory, or the npm scripts in package.json), do not read or change their sources.
## The task
Customers have an optional boolean field `active`. Add an operation that toggles it: calling it on a customer whose `active` is false or missing sets it to true, and calling it on a customer whose `active` is true sets it to false. Expose the operation as an HTTP endpoint on the running tree, and validate it with curl.
## The gate
`accept.sh` in the directory is the acceptance test, written by the reviewer. Your work is done when
`bash accept.sh <METHOD> '<ROUTE>'` (your endpoint's verb and route, the route with the literal `{id}`)
prints PASS for every criterion and ends with ACCEPTED. Read it before you start: every criterion says
what the endpoint must do. Do not edit it. Run it as often as you like.
## How to run and validate
- `npm run check` judges the whole tree. `npm run rehearse -- --profile local` runs every trigger and every branch with effects stubbed.
- The tree serves under the `local` profile, in memory, on port <port>. Start it in the background with:
`CUSTOMERS_JWT_SECRET=arena-secret CUSTOMERS_DATABASE_URL=postgres://unused npm run start -- --profile local > /tmp/arena-<model>.log 2>&1 &`
and read the log for `Listen: ok`. Stop it with `kill $(lsof -ti tcp:<port>)` when you are done, never with `pkill` or any kill by name, since other trees serve on this machine; make sure nothing you started is still running when you finish. Never leave the directory: no `cd` out of it, no command on a path outside it.
- The README says who can sign in and how (`bo`, `cy`, `ana`) and shows the curl calls for registering a customer and for signing in. Use `sed` rather than `jq` to pull a token out of a response; `jq` may not be installed.
- Validate with curl every behaviour you consider part of the operation, including what happens when the toggle should not go through, and paste the exact commands and responses in your report.
## The report
When finished, report in this order: what you added or changed, file by file; every decision you had to make that the task did not settle for you, what you chose and why; the full output of your final `bash accept.sh ...` run; the curl commands you ran beyond it and their exact responses; and anything you left undone.
#!/usr/bin/env bash
# The gate. Your work is accepted when every criterion below prints PASS.
#
# bash accept.sh <METHOD> <ROUTE> e.g. bash accept.sh POST '/customers/{id}/active'
#
# METHOD and ROUTE are the endpoint you expose; ROUTE contains the literal {id}. The script judges the
# tree the way a reviewer would: it runs the checker and the rehearsal, reads what they say, starts the
# tree under the local profile, and drives the endpoint with curl. It never reads your documents to see
# how you did it, except to confirm that no rule a human wrote has changed in what it states.
# Do not edit this file. Read it: every criterion says what the endpoint must do.
set -uo pipefail
METHOD=${1:?METHOD}; ROUTE=${2:?ROUTE, with the literal id placeholder in braces}
cd "$(dirname "$0")"
PORT=$(node -e 'const p=require("./project.json");process.stdout.write(String(p.plugins.find(x=>x.use==="@http").settings.port))')
BASE=$(git rev-list --max-parents=0 HEAD)
H="http://localhost:$PORT"
LOG=$(mktemp -t accept)
pass=0; fail=0
ok() { pass=$((pass+1)); printf 'PASS %s\n' "$1"; }
bad() { fail=$((fail+1)); printf 'FAIL %s\n expected: %s\n got: %s\n' "$1" "$2" "$(printf '%s' "$3" | head -c 300)"; }
check() { # check <name> <expected-description> <actual> <grep-pattern>
if printf '%s' "$3" | grep -qE -- "$4"; then ok "$1"; else bad "$1" "$2" "$3"; fi
}
status() { curl -s -g -o /dev/null -w '%{http_code}' "$@"; }
body() { curl -s -g --max-time 10 "$@"; }
echo "== The tree at rest"
CHECK=$(npx wilanis check . 2>&1 | tail -3)
check "1 wilanis check passes" "ok: N documents" "$CHECK" '^ok: [0-9]+ documents'
REH=$(npx wilanis rehearse . --profile local 2>&1)
check "2 every rehearsed branch settles" "every branch settled" "$(printf '%s' "$REH" | grep -E 'every branch settled|NEVER RUN|BROKE|BLOCKED' | head -3)" 'every branch settled'
printf 'INFO 3 the rehearsal says: %s (the rule held at 5 triggers before your change; this line is reported, not judged)\n' "$(printf '%s' "$REH" | grep 'Writes are for registrars' | sed 's/^ *//')"
for inv in features/customers/domain/a-customer-is-reachable.invariant.json; do
before=$(git show "$BASE:$inv" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.stdout.write(JSON.parse(s).holds.when))')
after=$(node -e 'process.stdout.write(require("./'"$inv"'").holds.when)')
check "4 the rule in $(basename "$inv" .invariant.json) states what it stated" "$before" "$after" "^$(printf '%s' "$before" | sed 's/[][\.*^$|()+?{}\\]/\\&/g')\$"
done
before=$(git show "$BASE:features/customers/domain/writes-are-for-registrars.invariant.json" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.stdout.write(JSON.parse(s).access.requires.policy))')
after=$(node -e 'process.stdout.write(require("./features/customers/domain/writes-are-for-registrars.invariant.json").access.requires.policy)')
check "5 the access rule still requires the same policy" "$before" "$after" "^$(printf '%s' "$before" | sed 's/[][\.*^$|()+?{}\\]/\\&/g')\$"
echo "== The tree serving"
# stop whatever listens on this tree's port and nothing else: other trees on this machine are not ours to kill
for pid in $(lsof -ti tcp:$PORT 2>/dev/null); do kill "$pid" 2>/dev/null; done; sleep 1
CUSTOMERS_JWT_SECRET=accept-secret CUSTOMERS_DATABASE_URL=postgres://unused npx wilanis start . --profile local > "$LOG" 2>&1 &
PID=$!
for i in $(seq 1 30); do grep -q "Listen: ok" "$LOG" && break; sleep 1; done
check "6 the tree starts and listens" "startup 7/7 Listen: ok" "$(grep -E 'Listen: ok|refused|missing' "$LOG" | head -2)" 'Listen: ok'
tok() { body -X POST $H/api/v1/auth-employees -H 'content-type: application/json' -d "{\"username\":\"$1\",\"password\":\"$1-pass\"}" | sed -n 's/.*"accessToken":"\([^"]*\)".*/\1/p'; }
BO=$(tok bo); CY=$(tok cy)
# signing in writes the session under .wilanis/ and the reload watcher sees it (wilanis-js#490); wait for
# both reloads, one per sign-in, before recording, or the memory store starts over under the calls below
for i in $(seq 1 8); do [ "$(grep -c '^reload: [0-9]' "$LOG")" -ge 2 ] && break; sleep 1; done; sleep 1
R() { printf '%s' "$ROUTE" | sed "s/{id}/$1/"; }
REC1=$(body -X POST $H/customers -H "authorization: Bearer $BO" -H 'content-type: application/json' -d '{"name":"Ada Lovelace","email":"ada@example.com","tier":"silver"}' -w ' [%{http_code}]')
REC2=$(body -X POST $H/customers -H "authorization: Bearer $BO" -H 'content-type: application/json' -d '{"name":"Grace Hopper","email":"grace@example.com","tier":"bronze"}' -w ' [%{http_code}]')
ADAID=$(printf '%s' "$REC1" | sed -n 's/.*"id":"\([^"]*\)".*/\1/p'); GRACEID=$(printf '%s' "$REC2" | sed -n 's/.*"id":"\([^"]*\)".*/\1/p')
check "7 two customers registered as bo, one silver and one bronze" "two 201 answers with ids" "$REC1 // $REC2" '^\{"id":"[^"]+".*\[201\] // \{"id":"[^"]+".*\[201\]$'
echo "== The endpoint"
r=$(body -X "$METHOD" "$H$(R "$ADAID")" -H "authorization: Bearer $BO" -w ' [%{http_code}]')
check "8 toggling a silver customer as bo answers 200 and shows active true" '{"id":...,"active":true} [200]' "$r" '"active":true.*\[200\]$'
r=$(body -X "$METHOD" "$H$(R "$ADAID")" -H "authorization: Bearer $BO" -w ' [%{http_code}]')
check "9 toggling them again answers 200 and shows active false" '{"id":...,"active":false} [200]' "$r" '"active":false.*\[200\]$'
r=$(body "$H/customers/$ADAID" -w ' [%{http_code}]')
check "10 reading the customer back shows their active field" '{"id":...,"active":false} [200]' "$r" '"active":false.*\[200\]$'
r=$(body -X "$METHOD" "$H$(R "$GRACEID")" -H "authorization: Bearer $BO" -w ' [%{http_code}]')
check "11 toggling the bronze customer as bo answers 200 and shows active true" '{"id":...,"tier":"bronze",...,"active":true} [200]' "$r" '"active":true.*\[200\]$'
r=$(body "$H/customers/$GRACEID" -w ' [%{http_code}]')
check "12 the bronze customer reads back active" '{"id":...,"active":true} [200]' "$r" '"active":true.*\[200\]$'
r=$(body "$H/customers" -w ' [%{http_code}]')
check "13 the list still answers 200 with both customers" '[{...},{...}] [200]' "$r" "$ADAID.*$GRACEID.*\[200\]$|$GRACEID.*$ADAID.*\[200\]$"
r=$(body -X "$METHOD" "$H$(R "$ADAID")" -H "authorization: Bearer $CY" -w ' [%{http_code}]')
check "14 cy, an employee without the registrar role, is refused 403 forbidden" '{"reason":"forbidden",...} [403]' "$r" '"reason":"forbidden".*\[403\]$'
r=$(body -X "$METHOD" "$H$(R "$ADAID")" -w ' [%{http_code}]')
check "15 no token is refused 401 anonymous" '{"reason":"anonymous",...} [401]' "$r" '"reason":"anonymous".*\[401\]$'
r=$(body -X "$METHOD" "$H$(R nope)" -H "authorization: Bearer $BO" -w ' [%{http_code}]')
check "16 an unknown id is refused 404 missing" '{"reason":"missing",...} [404]' "$r" '"reason":"missing".*\[404\]$'
r=$(body -X "$METHOD" "$H$(R "$ADAID")" -H "authorization: Bearer $BO" -w ' [%{http_code}]')
check "17 a third toggle activates the silver customer again" '{"id":...,"active":true} [200]' "$r" '"active":true.*\[200\]$'
r=$(body "$H/customers.csv" -H "authorization: Bearer $BO")
check "18 the export carries the flag: the silver customer's row ends in true" "$ADAID,...,true" "$(printf '%s' "$r" | grep "$ADAID")" "^$ADAID,.*,true\s*$"
kill $PID 2>/dev/null; wait $PID 2>/dev/null
echo "== Verdict"
if [ "$fail" -eq 0 ]; then echo "ACCEPTED $pass criteria pass"; exit 0; else echo "REJECTED $fail of $((pass+fail)) criteria fail"; exit 1; fi
Every cell is derived from the finished tree against its baseline commit by facts.py: the checker and the rehearsal run again, the new trigger's document read, the invariants compared with what the baseline held, the gate run again with the trigger's own verb and route. Green is what the tree wanted, red is not; a cell with no colour is a decision the task left to the agent and the gate accepts either way.
| Read from the tree against its baseline | Claude Haiku 4.5 | Claude Sonnet 5 | Claude Opus 5 |
|---|---|---|---|
| Did the gate accept the work, run again by the reviewer | ACCEPTED 17 criteria pass | ACCEPTED 17 criteria pass | ACCEPTED 17 criteria pass |
| Does wilanis check pass | ok: 198 documents | ok: 198 documents | ok: 198 documents |
| Does every rehearsed branch settle | yes | yes | yes |
| The rehearsal's Writes are for registrars holds at | 6 trigger(s), from 5 | 6 trigger(s), from 5 | 6 trigger(s), from 5 |
| The new trigger | POST /customers/{id}/active | POST /customers/{id}/active | POST /customers/{id}/active |
| Policies it attaches, in order | employees-only, can-register | employees-only, can-register | employees-only, can-register |
| Its refusal map | missing 404, upstream 502, anonymous 401, invalid_credential 401, forbidden 403, invariant 500 | missing 404, upstream 502, anonymous 401, invalid_credential 401, forbidden 403, invariant 500 | missing 404, upstream 502, anonymous 401, invalid_credential 401, forbidden 403, invariant 500 |
| Its out | CustomerView | CustomerView | CustomerView |
| The operation it fires | customer.port.json#toggle | customer.port.json#toggleActive | customer.port.json#toggleActive |
| Did the access rule's over gain that operation | yes: @features/customers/domain/customer.port.json#toggle | yes: @features/customers/domain/customer.port.json#toggleActive | yes: @features/customers/domain/customer.port.json#toggleActive |
| Did any invariant's when or requires change | none | none | none |
| Did CustomerView gain active, so a client can see what it toggled | yes | yes | yes |
| Bindings of the port that meet the operation | 3 of 3 | 3 of 3 | 3 of 3 |
| Files added | features/customers/data/kept-toggle-postgres.graph.json features/customers/data/kept-toggle.graph.json features/customers/data/rest-toggle.graph.json features/customers/edge/toggle-customer.trigger.json | features/customers/data/toggle-active-kept-postgres.graph.json features/customers/data/toggle-active-kept.graph.json features/customers/data/toggle-active-row.graph.json features/customers/edge/toggle-active-customer.trigger.json | features/customers/data/toggle-active-postgres.graph.json features/customers/data/toggle-active-row.graph.json features/customers/data/toggle-active.graph.json features/customers/edge/toggle-active.trigger.json |
| Files changed | features/customers/data/customers-postgres.binding.json features/customers/data/customers-rest.binding.json features/customers/data/customers-store.binding.json features/customers/domain/Customer.shape.json features/customers/domain/customer.port.json features/customers/domain/writes-are-for-registrars.invariant.json | features/customers/data/customers-postgres.binding.json features/customers/data/customers-rest.binding.json features/customers/data/customers-store.binding.json features/customers/domain/Customer.shape.json features/customers/domain/customer.port.json features/customers/domain/writes-are-for-registrars.invariant.json | README.md features/customers/data/customers-postgres.binding.json features/customers/data/customers-rest.binding.json features/customers/data/customers-store.binding.json features/customers/domain/Customer.shape.json features/customers/domain/customer.port.json features/customers/domain/writes-are-for-registrars.invariant.json features/customers/edge/IdRequest.shape.json |
Measured from each transcript (timestamps and token usage as the API reported them, each API message counted once), from git in each copy against the committed baseline, and from the check results the agent read. Cost is at API list price from the table at the top of parse.py: input, output, cache write at 1.25× input, cache read at 0.1× input.
| Claude Haiku 4.5 | Claude Sonnet 5 | Claude Opus 5 | |
|---|---|---|---|
| Wall clock | 7 min 26 s | 5 min 54 s | 6 min 30 s |
| Tool calls | 96 | 43 | 28 |
| of which Bash / Read / Edit / Write | 24 / 27 / 40 / 5 | 28 / 4 / 7 / 3 | 27 / 0 / 0 / 0 |
| Assistant turns with text | 18 | 14 | 9 |
| Check rounds with refusals | 9 | 0 | 0 |
| Refusals seen, total | 81 | 0 | 0 |
| Distinct refusal codes met | D001 G008 G009 L001 L007 R001 T005 T006 X211 | – | – |
| Gate verdicts seen, in order | REJECTED 1 of 17 criteria fail ACCEPTED 17 criteria pass | REJECTED 1 of 17 criteria fail ACCEPTED 17 criteria pass | ACCEPTED 17 criteria pass ACCEPTED 17 criteria pass |
| Stops blocked by the hook | 0 | 0 | 0 |
| Files added / changed | 4 / 6 | 4 / 6 | 4 / 8 |
| Lines in new files | 583 | 616 | 615 |
| Diff against the baseline | 6 files changed, 26 insertions(+), 7 deletions(-) | 10 files changed, 640 insertions(+), 5 deletions(-) | 12 files changed, 668 insertions(+), 13 deletions(-) |
| Rewrites of one file through Edit or Write, max (0 when files were written from the shell) | 13 | 1 | 0 |
| Output tokens | 56k | 30k | 25k |
| Input tokens, uncached | 777 | 88 | 54 |
| Cache writes / reads | 118k / 8535k | 104k / 4250k | 93k / 2244k |
| Cost at API list price | $1.28 (the CLI said $1.37) | $1.41 (the CLI said $1.57) | $2.33 (the CLI said $2.68) |
== The tree at rest PASS 1 wilanis check passes PASS 2 every rehearsed branch settles INFO 3 the rehearsal says: Writes are for registrars holds at 6 trigger(s) (the rule held at 5 triggers before your change; this line is reported, not judged) PASS 4 the rule in a-customer-is-reachable states what it stated PASS 5 the access rule still requires the same policy == The tree serving PASS 6 the tree starts and listens PASS 7 two customers registered as bo, one silver and one bronze == The endpoint PASS 8 toggling a silver customer as bo answers 200 and shows active true PASS 9 toggling them again answers 200 and shows active false PASS 10 reading the customer back shows their active field PASS 11 toggling the bronze customer as bo answers 200 and shows active true PASS 12 the bronze customer reads back active PASS 13 the list still answers 200 with both customers PASS 14 cy, an employee without the registrar role, is refused 403 forbidden PASS 15 no token is refused 401 anonymous PASS 16 an unknown id is refused 404 missing PASS 17 a third toggle activates the silver customer again PASS 18 the export carries the flag: the silver customer's row ends in true == Verdict ACCEPTED 17 criteria pass
== The tree at rest PASS 1 wilanis check passes PASS 2 every rehearsed branch settles INFO 3 the rehearsal says: Writes are for registrars holds at 6 trigger(s) (the rule held at 5 triggers before your change; this line is reported, not judged) PASS 4 the rule in a-customer-is-reachable states what it stated PASS 5 the access rule still requires the same policy == The tree serving PASS 6 the tree starts and listens PASS 7 two customers registered as bo, one silver and one bronze == The endpoint PASS 8 toggling a silver customer as bo answers 200 and shows active true PASS 9 toggling them again answers 200 and shows active false PASS 10 reading the customer back shows their active field PASS 11 toggling the bronze customer as bo answers 200 and shows active true PASS 12 the bronze customer reads back active PASS 13 the list still answers 200 with both customers PASS 14 cy, an employee without the registrar role, is refused 403 forbidden PASS 15 no token is refused 401 anonymous PASS 16 an unknown id is refused 404 missing PASS 17 a third toggle activates the silver customer again PASS 18 the export carries the flag: the silver customer's row ends in true == Verdict ACCEPTED 17 criteria pass
== The tree at rest PASS 1 wilanis check passes PASS 2 every rehearsed branch settles INFO 3 the rehearsal says: Writes are for registrars holds at 6 trigger(s) (the rule held at 5 triggers before your change; this line is reported, not judged) PASS 4 the rule in a-customer-is-reachable states what it stated PASS 5 the access rule still requires the same policy == The tree serving PASS 6 the tree starts and listens PASS 7 two customers registered as bo, one silver and one bronze == The endpoint PASS 8 toggling a silver customer as bo answers 200 and shows active true PASS 9 toggling them again answers 200 and shows active false PASS 10 reading the customer back shows their active field PASS 11 toggling the bronze customer as bo answers 200 and shows active true PASS 12 the bronze customer reads back active PASS 13 the list still answers 200 with both customers PASS 14 cy, an employee without the registrar role, is refused 403 forbidden PASS 15 no token is refused 401 anonymous PASS 16 an unknown id is refused 404 missing PASS 17 a third toggle activates the silver customer again PASS 18 the export carries the flag: the silver customer's row ends in true == Verdict ACCEPTED 17 criteria pass
Run by the reviewer after each agent had stopped, against a fresh start of each tree: sign in as bo and cy, wait for the reload the sign-in causes (#490), register one silver and one bronze customer as bo, then the same nine calls in the same order with the same bodies. Tokens are masked in the commands shown. A tree that does not check does not start, so every curl below then answers [000].
## git churn
6 files changed, 26 insertions(+), 7 deletions(-)
M features/customers/data/customers-postgres.binding.json
M features/customers/data/customers-rest.binding.json
M features/customers/data/customers-store.binding.json
M features/customers/domain/Customer.shape.json
M features/customers/domain/customer.port.json
M features/customers/domain/writes-are-for-registrars.invariant.json
?? features/customers/data/kept-toggle-postgres.graph.json
?? features/customers/data/kept-toggle.graph.json
?? features/customers/data/rest-toggle.graph.json
?? features/customers/edge/toggle-customer.trigger.json
## check
ok: 198 documents
## rehearse (tail)
every branch settled -- 57 branch(es), 26 decision(s), 17 graph(s).
3 invariant(s) declared:
A customer is reachable proved at 0 site(s), guarded at 18
Writes are for registrars holds at 6 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. Effects are stubbed, so no request left this process.
## the new route in map
@features/customers/edge/toggle-customer.trigger.json (@http/http.trigger-kind.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#toggle
@features/customers/data/kept-toggle.graph.json
asked @storage/store.port.json#get (effect) → store @features/customers/data/customers.store.json customers (get)
checkExists [switch → checkActive | missing]
checkActive [switch → deactivate | activate]
activate @storage/store.port.json#patch (effect) → store @features/customers/data/customers.store.json customers (patch)
checkActivate [switch → activateResult | failActivate]
activateResult @std/object.port.json#make
deactivate @storage/store.port.json#patch (effect) → store @features/customers/data/customers.store.json customers (patch)
checkDeactivate [switch → deactivateResult | failDeactivate]
deactivateResult @std/object.port.json#make
--
unreached under local @features/customers/data/kept-toggle-postgres.graph.json bound by @features/customers/data/customers-postgres.binding.json
unreached under local @features/customers/data/kept-update-postgres.graph.json bound by @features/customers/data/customers-postgres.binding.json
unreached under local @features/customers/data/rest-toggle.graph.json bound by @features/customers/data/customers-rest.binding.json
unreached under local @features/customers/data/update-row.graph.json bound by @features/customers/data/customers-rest.binding.json
## start
startup 1/8 Prepare the customer store: ok
startup 2/8 Make the customers whole: ok
startup 3/8 Reach the customer store: ok
startup 4/8 Reach the guard's memory: ok
startup 5/8 Watch for changes: ok
startup 6/8 Keep the schedule: ok
startup 7/8 Export traces: ok
http: listening on :8111 -- DELETE /customers/{id} → @customers/domain/customer.port.json#remove, DELETE /customers → @customers/domain/cust
startup 8/8 Listen: ok
## reload lines so far
reload: watching /Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku -- an edit is served once it passes wilanis check
registered silver customer 9b23bef1-557d-40dc-9db5-af374c763563 and bronze customer 4a624ab3-ac07-4a39-b2d4-db988c769a8d as bo
### 1 toggle a silver customer as bo (expect active true)
$ curl -X POST http://localhost:8111/customers/9b23bef1-557d-40dc-9db5-af374c763563/active -H authorization: Bearer $BO
{"id":"9b23bef1-557d-40dc-9db5-af374c763563","name":"Ada Lovelace","email":"ada@example.com","tier":"silver","active":true} [200]
### 2 toggle them again as bo (expect active false)
$ curl -X POST http://localhost:8111/customers/9b23bef1-557d-40dc-9db5-af374c763563/active -H authorization: Bearer $BO
{"id":"9b23bef1-557d-40dc-9db5-af374c763563","name":"Ada Lovelace","email":"ada@example.com","tier":"silver","active":false} [200]
### 3 read it back
$ curl http://localhost:8111/customers/9b23bef1-557d-40dc-9db5-af374c763563
{"id":"9b23bef1-557d-40dc-9db5-af374c763563","name":"Ada Lovelace","email":"ada@example.com","tier":"silver","active":false} [200]
### 4 toggle a bronze customer as bo
$ curl -X POST http://localhost:8111/customers/4a624ab3-ac07-4a39-b2d4-db988c769a8d/active -H authorization: Bearer $BO
{"id":"4a624ab3-ac07-4a39-b2d4-db988c769a8d","name":"Grace Hopper","email":"grace@example.com","tier":"bronze","active":true} [200]
### 5 read the bronze customer back (was anything written?)
$ curl http://localhost:8111/customers/4a624ab3-ac07-4a39-b2d4-db988c769a8d
{"id":"4a624ab3-ac07-4a39-b2d4-db988c769a8d","name":"Grace Hopper","email":"grace@example.com","tier":"bronze","active":true} [200]
### 6 toggle as cy, a viewer without the registrar role
$ curl -X POST http://localhost:8111/customers/9b23bef1-557d-40dc-9db5-af374c763563/active -H authorization: Bearer $CY
{"reason":"forbidden","message":"registering customers takes the registrar role"} [403]
### 7 toggle with no token
$ curl -X POST http://localhost:8111/customers/9b23bef1-557d-40dc-9db5-af374c763563/active
{"reason":"anonymous","message":"sign in first: no token was presented"} [401]
### 8 toggle a customer that does not exist
$ curl -X POST http://localhost:8111/customers/nope/active -H authorization: Bearer $BO
{"reason":"missing","message":"no customer nope"} [404]
### 9 list: which customers are active now
$ curl http://localhost:8111/customers
[{"id":"9b23bef1-557d-40dc-9db5-af374c763563","name":"Ada Lovelace","email":"ada@example.com","tier":"silver","active":false},{"id":"4a624ab3-ac07-4a39-b2d4-db988c769a8d","name":"Grace Hopper","email":"grace@example.com","tier":"bronze","active":true}] [200]
## log tail
POST /api/v1/auth-employees → 200 (24ms, @access/domain/access.port.json#signInEmployee done)
POST /customers → 201 (4ms, @customers/domain/customer.port.json#submit done)
POST /customers → 201 (1ms, @customers/domain/customer.port.json#submit done)
POST /customers/9b23bef1-557d-40dc-9db5-af374c763563/active → 200 (1ms, @customers/domain/customer.port.json#toggle done)
POST /customers/9b23bef1-557d-40dc-9db5-af374c763563/active → 200 (1ms, @customers/domain/customer.port.json#toggle done)
GET /customers/9b23bef1-557d-40dc-9db5-af374c763563 → 200 (1ms, @customers/domain/customer.port.json#get done)
POST /customers/4a624ab3-ac07-4a39-b2d4-db988c769a8d/active → 200 (1ms, @customers/domain/customer.port.json#toggle done)
GET /customers/4a624ab3-ac07-4a39-b2d4-db988c769a8d → 200 (0ms, @customers/domain/customer.port.json#get done)
POST /customers/9b23bef1-557d-40dc-9db5-af374c763563/active → 403 (1ms, @customers/domain/customer.port.json#toggle failed)
POST /customers/9b23bef1-557d-40dc-9db5-af374c763563/active → 401 (1ms, @customers/domain/customer.port.json#toggle failed)
POST /customers/nope/active → 404 (1ms, @customers/domain/customer.port.json#toggle failed)
GET /customers → 200 (2ms, @customers/domain/customer.port.json#list done)
## invariant files touched?
features/customers/domain/writes-are-for-registrars.invariant.json
## git churn
10 files changed, 640 insertions(+), 5 deletions(-)
## check
ok: 198 documents
## rehearse (tail)
every branch settled -- 57 branch(es), 26 decision(s), 17 graph(s).
3 invariant(s) declared:
A customer is reachable proved at 0 site(s), guarded at 18
Writes are for registrars holds at 6 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. Effects are stubbed, so no request left this process.
## the new route in map
@features/customers/edge/toggle-active-customer.trigger.json (@http/http.trigger-kind.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#toggleActive
@features/customers/data/toggle-active-kept.graph.json
asked @storage/store.port.json#get (effect) → store @features/customers/data/customers.store.json customers (get)
found [switch → wasSet | missing]
wasSet [switch → unset | set]
set @storage/store.port.json#patch (effect) → store @features/customers/data/customers.store.json customers (patch)
unset @storage/store.port.json#patch (effect) → store @features/customers/data/customers.store.json customers (patch)
setRoute [switch → setRow | vanishedOnSet]
--
unreached under local @features/customers/data/toggle-active-kept-postgres.graph.json bound by @features/customers/data/customers-postgres.binding.json
unreached under local @features/customers/data/toggle-active-row.graph.json bound by @features/customers/data/customers-rest.binding.json
unreached under local @features/customers/data/update-row.graph.json bound by @features/customers/data/customers-rest.binding.json
## start
startup 1/8 Prepare the customer store: ok
startup 2/8 Make the customers whole: ok
startup 3/8 Reach the customer store: ok
startup 4/8 Reach the guard's memory: ok
startup 5/8 Watch for changes: ok
startup 6/8 Keep the schedule: ok
startup 7/8 Export traces: ok
http: listening on :8112 -- DELETE /customers/{id} → @customers/domain/customer.port.json#remove, DELETE /customers → @customers/domain/cust
startup 8/8 Listen: ok
## reload lines so far
reload: watching /Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/sonnet -- an edit is served once it passes wilanis check
registered silver customer 117ad31a-6aa5-4124-be70-6feb3515c96d and bronze customer 42567c40-5a96-426e-9314-b5f6b4a84db9 as bo
### 1 toggle a silver customer as bo (expect active true)
$ curl -X POST http://localhost:8112/customers/117ad31a-6aa5-4124-be70-6feb3515c96d/active -H authorization: Bearer $BO
{"id":"117ad31a-6aa5-4124-be70-6feb3515c96d","name":"Ada Lovelace","email":"ada@example.com","tier":"silver","active":true} [200]
### 2 toggle them again as bo (expect active false)
$ curl -X POST http://localhost:8112/customers/117ad31a-6aa5-4124-be70-6feb3515c96d/active -H authorization: Bearer $BO
{"id":"117ad31a-6aa5-4124-be70-6feb3515c96d","name":"Ada Lovelace","email":"ada@example.com","tier":"silver","active":false} [200]
### 3 read it back
$ curl http://localhost:8112/customers/117ad31a-6aa5-4124-be70-6feb3515c96d
{"id":"117ad31a-6aa5-4124-be70-6feb3515c96d","name":"Ada Lovelace","email":"ada@example.com","tier":"silver","active":false} [200]
### 4 toggle a bronze customer as bo
$ curl -X POST http://localhost:8112/customers/42567c40-5a96-426e-9314-b5f6b4a84db9/active -H authorization: Bearer $BO
{"id":"42567c40-5a96-426e-9314-b5f6b4a84db9","name":"Grace Hopper","email":"grace@example.com","tier":"bronze","active":true} [200]
### 5 read the bronze customer back (was anything written?)
$ curl http://localhost:8112/customers/42567c40-5a96-426e-9314-b5f6b4a84db9
{"id":"42567c40-5a96-426e-9314-b5f6b4a84db9","name":"Grace Hopper","email":"grace@example.com","tier":"bronze","active":true} [200]
### 6 toggle as cy, a viewer without the registrar role
$ curl -X POST http://localhost:8112/customers/117ad31a-6aa5-4124-be70-6feb3515c96d/active -H authorization: Bearer $CY
{"reason":"forbidden","message":"registering customers takes the registrar role"} [403]
### 7 toggle with no token
$ curl -X POST http://localhost:8112/customers/117ad31a-6aa5-4124-be70-6feb3515c96d/active
{"reason":"anonymous","message":"sign in first: no token was presented"} [401]
### 8 toggle a customer that does not exist
$ curl -X POST http://localhost:8112/customers/nope/active -H authorization: Bearer $BO
{"reason":"missing","message":"no customer nope"} [404]
### 9 list: which customers are active now
$ curl http://localhost:8112/customers
[{"id":"117ad31a-6aa5-4124-be70-6feb3515c96d","name":"Ada Lovelace","email":"ada@example.com","tier":"silver","active":false},{"id":"42567c40-5a96-426e-9314-b5f6b4a84db9","name":"Grace Hopper","email":"grace@example.com","tier":"bronze","active":true}] [200]
## log tail
POST /api/v1/auth-employees → 200 (23ms, @access/domain/access.port.json#signInEmployee done)
POST /customers → 201 (3ms, @customers/domain/customer.port.json#submit done)
POST /customers → 201 (2ms, @customers/domain/customer.port.json#submit done)
POST /customers/117ad31a-6aa5-4124-be70-6feb3515c96d/active → 200 (1ms, @customers/domain/customer.port.json#toggleActive done)
POST /customers/117ad31a-6aa5-4124-be70-6feb3515c96d/active → 200 (1ms, @customers/domain/customer.port.json#toggleActive done)
GET /customers/117ad31a-6aa5-4124-be70-6feb3515c96d → 200 (1ms, @customers/domain/customer.port.json#get done)
POST /customers/42567c40-5a96-426e-9314-b5f6b4a84db9/active → 200 (1ms, @customers/domain/customer.port.json#toggleActive done)
GET /customers/42567c40-5a96-426e-9314-b5f6b4a84db9 → 200 (1ms, @customers/domain/customer.port.json#get done)
POST /customers/117ad31a-6aa5-4124-be70-6feb3515c96d/active → 403 (1ms, @customers/domain/customer.port.json#toggleActive failed)
POST /customers/117ad31a-6aa5-4124-be70-6feb3515c96d/active → 401 (1ms, @customers/domain/customer.port.json#toggleActive failed)
POST /customers/nope/active → 404 (1ms, @customers/domain/customer.port.json#toggleActive failed)
GET /customers → 200 (1ms, @customers/domain/customer.port.json#list done)
## invariant files touched?
features/customers/domain/writes-are-for-registrars.invariant.json
## git churn
12 files changed, 668 insertions(+), 13 deletions(-)
## check
ok: 198 documents
## rehearse (tail)
every branch settled -- 57 branch(es), 26 decision(s), 17 graph(s).
3 invariant(s) declared:
A customer is reachable proved at 0 site(s), guarded at 18
Writes are for registrars holds at 6 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. Effects are stubbed, so no request left this process.
## the new route in map
@features/customers/edge/toggle-active.trigger.json (@http/http.trigger-kind.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#toggleActive
@features/customers/data/toggle-active.graph.json
asked @storage/store.port.json#get (effect) → store @features/customers/data/customers.store.json customers (get)
found [switch → wasActive | missing]
wasActive [switch → close | open]
open @storage/store.port.json#patch (effect) → store @features/customers/data/customers.store.json customers (patch)
close @storage/store.port.json#patch (effect) → store @features/customers/data/customers.store.json customers (patch)
setRoute [switch → setRow | vanishedOnSet]
unsetRoute [switch → unsetRow | vanishedOnUnset]
setRow @std/object.port.json#make
unsetRow @std/object.port.json#make
--
unreached under local @features/customers/data/toggle-active-postgres.graph.json bound by @features/customers/data/customers-postgres.binding.json
unreached under local @features/customers/data/toggle-active-row.graph.json bound by @features/customers/data/customers-rest.binding.json
unreached under local @features/customers/data/update-row.graph.json bound by @features/customers/data/customers-rest.binding.json
## start
startup 1/8 Prepare the customer store: ok
startup 2/8 Make the customers whole: ok
startup 3/8 Reach the customer store: ok
startup 4/8 Reach the guard's memory: ok
startup 5/8 Watch for changes: ok
startup 6/8 Keep the schedule: ok
startup 7/8 Export traces: ok
http: listening on :8113 -- DELETE /customers/{id} → @customers/domain/customer.port.json#remove, DELETE /customers → @customers/domain/cust
startup 8/8 Listen: ok
## reload lines so far
reload: watching /Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/opus -- an edit is served once it passes wilanis check
registered silver customer 4302c704-9604-46a6-b945-4d435600fafe and bronze customer f65b1a2a-3359-4911-ba79-0bbf41bf30ff as bo
### 1 toggle a silver customer as bo (expect active true)
$ curl -X POST http://localhost:8113/customers/4302c704-9604-46a6-b945-4d435600fafe/active -H authorization: Bearer $BO
{"id":"4302c704-9604-46a6-b945-4d435600fafe","name":"Ada Lovelace","email":"ada@example.com","tier":"silver","active":true} [200]
### 2 toggle them again as bo (expect active false)
$ curl -X POST http://localhost:8113/customers/4302c704-9604-46a6-b945-4d435600fafe/active -H authorization: Bearer $BO
{"id":"4302c704-9604-46a6-b945-4d435600fafe","name":"Ada Lovelace","email":"ada@example.com","tier":"silver","active":false} [200]
### 3 read it back
$ curl http://localhost:8113/customers/4302c704-9604-46a6-b945-4d435600fafe
{"id":"4302c704-9604-46a6-b945-4d435600fafe","name":"Ada Lovelace","email":"ada@example.com","tier":"silver","active":false} [200]
### 4 toggle a bronze customer as bo
$ curl -X POST http://localhost:8113/customers/f65b1a2a-3359-4911-ba79-0bbf41bf30ff/active -H authorization: Bearer $BO
{"id":"f65b1a2a-3359-4911-ba79-0bbf41bf30ff","name":"Grace Hopper","email":"grace@example.com","tier":"bronze","active":true} [200]
### 5 read the bronze customer back (was anything written?)
$ curl http://localhost:8113/customers/f65b1a2a-3359-4911-ba79-0bbf41bf30ff
{"id":"f65b1a2a-3359-4911-ba79-0bbf41bf30ff","name":"Grace Hopper","email":"grace@example.com","tier":"bronze","active":true} [200]
### 6 toggle as cy, a viewer without the registrar role
$ curl -X POST http://localhost:8113/customers/4302c704-9604-46a6-b945-4d435600fafe/active -H authorization: Bearer $CY
{"reason":"forbidden","message":"registering customers takes the registrar role"} [403]
### 7 toggle with no token
$ curl -X POST http://localhost:8113/customers/4302c704-9604-46a6-b945-4d435600fafe/active
{"reason":"anonymous","message":"sign in first: no token was presented"} [401]
### 8 toggle a customer that does not exist
$ curl -X POST http://localhost:8113/customers/nope/active -H authorization: Bearer $BO
{"reason":"missing","message":"no customer nope"} [404]
### 9 list: which customers are active now
$ curl http://localhost:8113/customers
[{"id":"4302c704-9604-46a6-b945-4d435600fafe","name":"Ada Lovelace","email":"ada@example.com","tier":"silver","active":false},{"id":"f65b1a2a-3359-4911-ba79-0bbf41bf30ff","name":"Grace Hopper","email":"grace@example.com","tier":"bronze","active":true}] [200]
## log tail
POST /api/v1/auth-employees → 200 (23ms, @access/domain/access.port.json#signInEmployee done)
POST /customers → 201 (3ms, @customers/domain/customer.port.json#submit done)
POST /customers → 201 (1ms, @customers/domain/customer.port.json#submit done)
POST /customers/4302c704-9604-46a6-b945-4d435600fafe/active → 200 (1ms, @customers/domain/customer.port.json#toggleActive done)
POST /customers/4302c704-9604-46a6-b945-4d435600fafe/active → 200 (2ms, @customers/domain/customer.port.json#toggleActive done)
GET /customers/4302c704-9604-46a6-b945-4d435600fafe → 200 (1ms, @customers/domain/customer.port.json#get done)
POST /customers/f65b1a2a-3359-4911-ba79-0bbf41bf30ff/active → 200 (1ms, @customers/domain/customer.port.json#toggleActive done)
GET /customers/f65b1a2a-3359-4911-ba79-0bbf41bf30ff → 200 (0ms, @customers/domain/customer.port.json#get done)
POST /customers/4302c704-9604-46a6-b945-4d435600fafe/active → 403 (1ms, @customers/domain/customer.port.json#toggleActive failed)
POST /customers/4302c704-9604-46a6-b945-4d435600fafe/active → 401 (0ms, @customers/domain/customer.port.json#toggleActive failed)
POST /customers/nope/active → 404 (1ms, @customers/domain/customer.port.json#toggleActive failed)
GET /customers → 200 (2ms, @customers/domain/customer.port.json#list done)
## invariant files touched?
features/customers/domain/writes-are-for-registrars.invariant.json
Every tool call in order, the agent's own words between them, and each check that refused, expandable. Nothing is edited; long outputs are cut at 1200 characters.
/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/accept.sh/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/README.md/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/domainfind /Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/domain -type f | head -20/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/domain/Customer.shape.jsonfind /Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/edge -type f | sort/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/edge/update-customer.trigger.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/edge/delete-customer.trigger.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/edge/IdRequest.shape.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/edge/CustomerView.shape.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/domain/customer.port.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/domain/customer.port.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/domain/register-customer.graph.jsonfind /Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data -type f | sort/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/customers-store.binding.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-update.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/domain/CustomerUpdate.shape.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/demo/arena/.runs/haiku/features/customers/domain/export-customers.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/domain/export-customers.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/write-csv.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/domain/customer.port.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/domain/toggle-customer.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/domain/toggle-customer.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/edge/toggle-customer.trigger.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-get.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/domain/CustomerRef.shape.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/customers-rest.binding.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/update-row.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/rest-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-update-postgres.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle-postgres.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/customers-store.binding.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/customers-rest.binding.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/customers-postgres.binding.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/customers-postgres.binding.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/domain/writes-are-for-registrars.invariant.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/domain/writes-are-for-registrars.invariant.jsonnpm run check 2>&1 | tail -20L007 @features/customers/domain/toggle-customer.graph.json
graph only forwards its input to '@customers/domain/customer.port.json#toggle'
→ it adds no rule of its own; fire @customers/domain/customer.port.json#toggle from the trigger and delete this graph
L001 @features/customers/domain/toggle-customer.graph.json#in
in names edge shape '@customers/edge/IdRequest.shape.json' from the core layer
→ core speaks core shapes; a trigger or binding translates edge to core
R001 @features/customers/data/customers-rest.binding.json#operations/toggle/graph
unknown graph '@customers/data/rest-toggle.graph.json'
→ wilanis ls graph
T006 @features/customers/edge/toggle-customer.trigger.json#settings/response/refusals/upstream
settings.response.refusals maps reason 'upstream', but nothing this trigger fires, gates on or identifies with refuses with it
→ remove it, or spell the reason the way the graph does
X211 @features/customers/data/kept-toggle-postgres.graph.json#nodes/patch/in/changes/active
where a patch changes 'active', active: expected boolean
→ active is boolean
X211 @features/customers/data/kept-toggle.graph.json#nodes/patch/in/changes/active …/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/edge/toggle-customer.trigger.jsonls -la /Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/*toggle* 2>&1rm /Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/domain/toggle-customer.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle-postgres.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/rest-toggle.graph.jsonnpm run check 2>&1 | tail -30→ see https://raw.githubusercontent.com/wilanis/wilanis-js/main/packages/core/schemas/graph.schema.json
D001 features/customers/data/rest-toggle.graph.json#nodes/check-deactivate/id
must match "^[a-z][A-Za-z0-9_]*$" -- A field, input, operation, resolver or node identifier.
→ see https://raw.githubusercontent.com/wilanis/wilanis-js/main/packages/core/schemas/graph.schema.json
D001 features/customers/data/rest-toggle.graph.json#nodes/check-deactivate/rules/0/to
must match "^[a-z][A-Za-z0-9_]*$" -- A field, input, operation, resolver or node identifier.
→ see https://raw.githubusercontent.com/wilanis/wilanis-js/main/packages/core/schemas/graph.schema.json
D001 features/customers/data/rest-toggle.graph.json#nodes/activate-ok/id
must match "^[a-z][A-Za-z0-9_]*$" -- A field, input, operation, resolver or node identifier.
→ see https://raw.githubusercontent.com/wilanis/wilanis-js/main/packages/core/schemas/graph.schema.json
D001 features/customers/data/rest-toggle.graph.json#nodes/deactivate-ok/id
must match "^[a-z][A-Za-z0-9_]*$" -- A field, input, operation, resolver or node identifier.
→ see https://raw.githubusercontent.com/wilanis/wilanis-js/main/ …/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle-postgres.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/rest-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/rest-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/rest-toggle.graph.jsonnpm run build 2>&1 | grep -E "(tsc|error)" | head -20npm run 2>&1 | grep -E "^\s+(check|rehearse|start)"npm run check 2>&1 | grep -A 1 "refusal"T005 @features/customers/edge/toggle-customer.trigger.json#settings/response/refusals
@features/customers/data/rest-toggle.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
T006 @features/customers/edge/toggle-customer.trigger.json#settings/response/refusals/invariant
settings.response.refusals maps reason 'invariant', but nothing this trigger fires, gates on or identifies with refuses with it
→ remove it, or spell the reason the way the graph does
--
12 refusal(s)/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/edge/update-customer.trigger.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/domain/a-customer-is-reachable.invariant.jsongrep -l "invariant" /Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/domain/*.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/edge/toggle-customer.trigger.jsonnpm run check 2>&1 | tail -5G009 @features/customers/data/rest-toggle.graph.json#nodes/checkDeactivate
node 'failed' is routed by both 'checkActivate' and 'checkDeactivate'
→ a node has one router
10 refusal(s)/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/rest-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/rest-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/rest-toggle.graph.jsonnpm run check 2>&1 | tail -10node 'activate' is read by nothing
→ wire its result into another node, or name it in out.from
G008 @features/customers/data/kept-toggle.graph.json#nodes/deactivate
node 'deactivate' is read by nothing
→ wire its result into another node, or name it in out.from
G009 @features/customers/data/rest-toggle.graph.json#nodes/checkGet
routes to unknown node 'failed'
→ name a node declared under nodes, or add it
9 refusal(s)/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/rest-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/rest-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/rest-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle-postgres.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle-postgres.graph.jsonnpm run check 2>&1 | tail -5T005 @features/customers/edge/toggle-customer.trigger.json#settings/response/refusals
@features/customers/data/kept-toggle.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
5 refusal(s)/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/edge/toggle-customer.trigger.jsonnpm run check 2>&1 | tail -3npm run check 2>&1 | grep -B 2 "4 refusal"npm run check 2>&1 | grep -E "^[A-Z][0-9]+" | head -10G004 @features/customers/data/kept-toggle-postgres.graph.json#nodes/activateResult/in/value G004 @features/customers/data/kept-toggle-postgres.graph.json#nodes/deactivateResult/in/value G004 @features/customers/data/kept-toggle.graph.json#nodes/activateResult/in/value G004 @features/customers/data/kept-toggle.graph.json#nodes/deactivateResult/in/value
/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle-postgres.graph.jsonnpm run check 2>&1 | tail -3npm run check 2>&1 | grep "^G009"G009 @features/customers/data/kept-toggle-postgres.graph.json#nodes/checkActivate G009 @features/customers/data/kept-toggle-postgres.graph.json#nodes/checkDeactivate G009 @features/customers/data/kept-toggle.graph.json#nodes/checkActivate G009 @features/customers/data/kept-toggle.graph.json#nodes/checkDeactivate
/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle-postgres.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle-postgres.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/data/kept-toggle-postgres.graph.jsonnpm run check 2>&1 | tail -3npm run rehearse -- --profile local 2>&1 | tail -10bash accept.sh POST '/customers/{id}/active' 2>&1/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/haiku/features/customers/domain/Customer.shape.jsonnpm run check 2>&1 | tail -1bash accept.sh POST '/customers/{id}/active' 2>&1cat README.md && echo ---ACCEPT--- && cat accept.sh/Users/rfontes/.claude/projects/-Users-rfontes-dev-wilanis-js-docs-demo-arena--runs-sonnet/00346a47-cc51-4ffd-a614-e5a5f2c10fee/tool-results/b02jr7v6r.txtfind features/customers -type f | sortfor f in features/customers/edge/update-customer.trigger.json features/customers/edge/UpdateRequest.shape.json features/customers/edge/UpdateBody.shape.json features/customers/domain/Customer.shape.json features/customers/domain/CustomerUpdate.shape.json; do echo "== $f =="; cat "$f"; echo; donefor f in features/customers/edge/CustomerView.shape.json features/customers/edge/get-customer.trigger.json features/customers/edge/IdRequest.shape.json features/customers/data/customers-store.binding.json; do echo "== $f =="; cat "$f"; echo; donefor f in features/customers/data/kept-update.graph.json features/customers/data/kept-get.graph.json features/customers/data/update-row.graph.json features/customers/data/get-row.graph.json; do echo "== $f =="; cat "$f"; echo; donecat features/customers/data/customers-rest.binding.json{"skill": "wilanis-graphs"}cat features/customers/data/customers-postgres.binding.json && echo ---STORE--- && cat features/customers/data/customers-postgres.store.json && echo ---KEPTUPDATEPG--- && cat features/customers/data/kept-update-postgres.graph.json && echo ---STORELOCAL--- && cat features/customers/data/customers.stocat features/customers/edge/CustomerRow.shape.json && echo ---CREATEROW--- && cat features/customers/data/create-row.graph.json && echo ---CustomerRef--- && cat features/customers/domain/CustomerRef.shape.jsoncat features/customers/domain/writes-are-for-registrars.invariant.json && echo ---ACR--- && cat features/customers/domain/a-customer-is-reachable.invariant.jsonfind . -path ./node_modules -prune -o -name 'http.port.json' -print 2>/dev/null; find node_modules/@wilanis/plugin-http -name 'http.port.json' | xargs catfind node_modules/@wilanis/plugin-http -iname '*.json' | head -30 && echo --- && npx wilanis describe @http/http.port.json . 2>&1 | head -80cat features/customers/edge/delete-customer.trigger.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/sonnet/features/customers/domain/customer.port.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/sonnet/features/customers/domain/customer.port.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/sonnet/features/customers/data/toggle-active-kept.graph.jsonsed \
-e 's#"customers.store.json#"customers-postgres.store.json#g' \
-e 's#under the local profile: read the record#behind the same operation as @customers/data/toggle-active-kept.graph.json, against the store kept in PostgreSQL: read the record#' \
features/customers/data/toggle-active-kept./Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/sonnet/features/customers/data/toggle-active-kept-postgres.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/sonnet/features/customers/data/toggle-active-kept-postgres.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/sonnet/features/customers/data/toggle-active-row.graph.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/sonnet/features/customers/data/customers-store.binding.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/sonnet/features/customers/data/customers-postgres.binding.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/sonnet/features/customers/data/customers-rest.binding.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/sonnet/features/customers/edge/toggle-active-customer.trigger.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/sonnet/features/customers/domain/writes-are-for-registrars.invariant.jsoncat features/customers/data/write-csv.graph.jsonnpm run check 2>&1 | tail -60npm run rehearse -- --profile local 2>&1 | tail -80bash accept.sh POST '/customers/{id}/active' 2>&1 | tail -40git log --oneline -- features/customers/domain/Customer.shape.json && git diff --stat/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/sonnet/features/customers/domain/Customer.shape.json/Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/sonnet/features/customers/domain/Customer.shape.jsonnpm run check 2>&1 | tail -10 && bash accept.sh POST '/customers/{id}/active' 2>&1 | tail -25lsof -ti tcp:8112 && echo "something still listening" || echo "port free"CUSTOMERS_JWT_SECRET=arena-secret CUSTOMERS_DATABASE_URL=postgres://unused npm run start -- --profile local > /tmp/arena-sonnet.log 2>&1 &
disown
for i in $(seq 1 30); do grep -q "Listen: ok" /tmp/arena-sonnet.log && break; sleep 1; done
tail -20 /tmp/arena-sonnet.logsleep 2
BO=$(curl -s -X POST localhost:8112/api/v1/auth-employees -H 'content-type: application/json' -d '{"username":"bo","password":"bo-pass"}' | sed -n 's/.*"accessToken":"\([^"]*\)".*/\1/p')
echo "BO token: ${BO:0:20}..."
echo "-- register a gold customer with a note --"
GOLD=$(curl -s -X POST lcat features/customers/edge/RegisterRequest.shape.jsonBO=$(curl -s -X POST localhost:8112/api/v1/auth-employees -H 'content-type: application/json' -d '{"username":"bo","password":"bo-pass"}' | sed -n 's/.*"accessToken":"\([^"]*\)".*/\1/p')
echo "-- register Marie, gold, no note (invariant requires a note only for gold; expect a refusal) --"
curl -s -wkill $(lsof -ti tcp:8112) 2>/dev/null; sleep 1; lsof -ti tcp:8112 && echo "still up" || echo "port free"git status --shortgit remote -v && git rev-parse --show-toplevel && git branch -vvgit add features/customers && git commit -m "$(cat <<'EOF'
Add a toggleActive operation and its endpoint
Customer already carried an optional active field; nothing flipped it. Add
customer.port.json#toggleActive (read, decide from the current value, write
the other one), bind it in all three profills -la && echo "=== README ===" && cat README.mdcat accept.shfind features connections -type f | sort && echo "=== project.json ===" && cat project.jsoncd /Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/opus/features/customers && for f in domain/Customer.shape.json domain/CustomerRecord.shape.json domain/CustomerUpdate.shape.json edge/CustomerView.shape.json edge/CustomerRow.shape.json domain/writes-are-for-registrars.invariant.json domain/a-cufor f in data/kept-get.graph.json data/kept-update.graph.json data/customers-store.binding.json data/customers.store.json data/write-csv.graph.json edge/update-customer.trigger.json edge/get-customer.trigger.json edge/request.resolvers.json; do echo "=== $f ==="; cat $f; donecd /Users/rfontes/dev/wilanis-js/docs/demo/arena/.runs/opus && for f in features/customers/data/customers-rest.binding.json features/customers/data/update-row.graph.json features/customers/data/get-row.graph.json features/customers/data/kept-update-postgres.graph.json features/customers/data/customenpx wilanis describe @blob/csv.port.json 2>&1 | head -60for f in features/customers/edge/register-customer.trigger.json features/customers/edge/IdRequest.shape.json features/customers/domain/CustomerRef.shape.json features/customers/domain/register-customer.graph.json features/customers/domain/list-customers.graph.json; do echo "=== $f ==="; cat $f; done{"skill": "wilanis-graphs"}node -e '
const fs=require("fs");
const p="features/customers/domain/Customer.shape.json";
const d=JSON.parse(fs.readFileSync(p,"utf8"));
const f=d.fields; const active=f.active; delete f.active; f.active=active;
active.description="whether the account is open for business; absent reads as inactive.node -e '
const fs=require("fs");
const p="features/customers/domain/customer.port.json";
const d=JSON.parse(fs.readFileSync(p,"utf8"));
const ops=d.operations; const out={};
for (const [k,v] of Object.entries(ops)) {
out[k]=v;
if (k==="remove") out.toggleActive={
description:"Open one custonode -e '
const fs=require("fs");
const store = (storePath, note) => ({
$schema: "https://raw.githubusercontent.com/wilanis/wilanis-js/main/packages/core/schemas/graph.schema.json",
label: "Toggle what is kept",
description: "Data graph behind customer.toggleActive over a store: read the recornode -e '
const fs=require("fs");
const conn="@connections/customers-api.connection.json";
const put=(id,label,value)=>({ type:"@wilanis/node/run.schema.json", id, label, run:"@http/http.port.json#request",
in:{ body:{ active:value }, connection:conn, method:"PUT", path:"/customer/{{in.id}}", consnode -e '
const fs=require("fs");
const bind=(p,graph,description)=>{
const d=JSON.parse(fs.readFileSync(p,"utf8"));
const out={};
for (const [k,v] of Object.entries(d.operations)) { out[k]=v; if (k==="remove") out.toggleActive = description ? {description,graph} : {graph}; }
d.operations=oucat > features/customers/edge/toggle-active.trigger.json <<'JSON'
{
"$schema": "https://raw.githubusercontent.com/wilanis/wilanis-js/main/packages/core/schemas/trigger.schema.json",
"label": "POST /customers/{id}/active",
"description": "POST /customers/{id}/active: the id from the route, no bnpx wilanis rehearse . --profile local 2>&1 | tail -40npx wilanis rehearse . --profile local 2>&1 | grep -A9 'toggle-active'for pid in $(lsof -ti tcp:8113 2>/dev/null); do kill "$pid"; done; sleep 1; CUSTOMERS_JWT_SECRET=arena-secret CUSTOMERS_DATABASE_URL=postgres://unused npm run start -- --profile local > /tmp/arena-opus.log 2>&1 &
sleep 12; grep -E 'Listen: ok|refused|error' /tmp/arena-opus.log | headH=http://localhost:8113
tok() { curl -s -X POST $H/api/v1/auth-employees -H 'content-type: application/json' -d "{\"username\":\"$1\",\"password\":\"$1-pass\"}" | sed -n 's/.*"accessToken":"\([^"]*\)".*/\1/p'; }
BO=$(tok bo); CY=$(tok cy)
sleep 4
echo "--- register"
R1=$(curl -s -X POST $H/customersfor pid in $(lsof -ti tcp:8113 2>/dev/null); do kill "$pid"; done; sleep 1; bash accept.sh POST '/customers/{id}/active' 2>&1node -e '
const fs=require("fs"); let r=fs.readFileSync("README.md","utf8");
const sub=(a,b)=>{ if(!r.includes(a)) { console.error("MISS: "+a.slice(0,60)); process.exit(1);} r=r.split(a).join(b); };
sub("# GET /customers[?tier=], POST /customers, GET|PUT|DELETE /customers/{id}, DELETE /customers, GEnode -e '
const fs=require("fs"); let r=fs.readFileSync("README.md","utf8");
const anchor="## A rule about every customer";
const para = `## Opening and closing an account\n\n\`POST /customers/{id}/active\` flips one customer'"'"'s \`active\`: an account that was closed -- or that never\ncarried thefor pid in $(lsof -ti tcp:8113 2>/dev/null); do kill "$pid"; done; sleep 1; CUSTOMERS_JWT_SECRET=arena-secret CUSTOMERS_DATABASE_URL=postgres://unused npm run start -- --profile local > /tmp/arena-opus.log 2>&1 &
sleep 12; grep -E 'Listen: ok' /tmp/arena-opus.logH=http://localhost:8113
BO=$(curl -s -X POST $H/api/v1/auth-employees -H 'content-type: application/json' -d '{"username":"bo","password":"bo-pass"}' | sed -n 's/.*"accessToken":"\([^"]*\)".*/\1/p')
ANA=$(curl -s -X POST $H/api/v1/auth-customers -H 'content-type: application/json' -d '{"username":"afor p in live production; do echo "== $p"; npx wilanis rehearse . --profile $p 2>&1 | grep -E 'toggle-active|every branch settled|NEVER RUN|BROKE|BLOCKED' | head -20; donefor pid in $(lsof -ti tcp:8113 2>/dev/null); do kill "$pid"; done; sleep 1; bash accept.sh POST '/customers/{id}/active' 2>&1; echo "--- listeners left on 8113:"; lsof -ti tcp:8113 || echo nonegit status --short && git add -A && git commit -q -F - <<'MSG' && git log --oneline -1
Toggle a customer's active flag over a route
POST /customers/{id}/active flips one customer's active field: closed or
absent becomes open, open becomes closed. The route sends nothing but the
id, so the next valugit remote -v && git status -sb | head -1Excellent! All acceptance tests pass. Let me create a summary of the work completed.
## Summary
I've successfully implemented the customer account toggle feature for the wilanis demo project. Here's what was added:
### Files Added or Modified
**New Files Created:**
1. `/features/customers/edge/toggle-customer.trigger.json` - HTTP POST trigger at `/customers/{id}/active`
2. `/features/customers/data/kept-toggle.graph.json` - Data layer implementation for store binding
3. `/features/customers/data/kept-toggle-postgres.graph.json` - Data layer implementation for postgres binding
4. `/features/customers/data/rest-toggle.graph.json` - Data layer implementation for REST API binding
**Modified Files:**
1. `/features/customers/domain/customer.port.json` - Added `toggle` operation
2. `/features/customers/domain/Customer.shape.json` - Reordered fields so `active` appears last (for CSV export)
3. `/features/customers/domain/writes-are-for-registrars.invariant.json` - Added toggle to operations requiring registrar policy
4. `/features/customers/data/customers-store.binding.json` - Added toggle operation binding
5. `/features/customers/data/customers-rest.binding.json` - Added toggle operation binding
6. `/features/customers/data/customers-postgres.binding.json` - Added toggle operation binding
### Key Decisions
1. **HTTP Method**: Chose POST rather than PATCH, as POST is more suitable for action endpoints that change state.
2. **Operation Design**: Created a single `toggle` operation that only requires a customer ID. The operation intelligently determines the new state (absent/false → true, true → false) at the data layer.
3. **Field Ordering**: Reordered the Customer shape to place `active` as the last field, ensuring CSV exports end with the active flag value for proper test validation.
4. **Error Handling**: The endpoint maps all relevant refusals:
- 404 for missing customers
- 401 for anonymous callers
- 403 for forbidden (non-registrar employees)
- 502 for upstream API failures (REST profile)
- 500 for invariant violations
5. **Access Control**: Toggle requires the same registrar policy (`can-register`) as other write operations, enforced through the `writes-are-for-registrars` invariant.
### Test Results
All 17 acceptance criteria pass:
- Criteria 1-5: Schema/compiler checks pass
- Criterion 6: Server starts and listens
- Criterion 7: Test customers register successfully
- Criteria 8-13: Toggle endpoint works, toggling switches active between true/false, read-back confirms state, list still works
- Criteria 14-16: Access control enforced (non-registrar refused 403, no token refused 401, missing customer 404)
- Criterion 17: Multiple toggles work correctly
- Criterion 18: CSV export includes active field properly
Committed locally (no remote is configured for this repo, so there's nothing to push).
## Report
### What I added or changed
- **`features/customers/domain/customer.port.json`** — added the `toggleActive` operation: accepts `id`, returns `Customer`, described as flipping `active` (false/absent → true, true → false), failing when there's no such customer.
- **`features/customers/domain/writes-are-for-registrars.invariant.json`** — added `customer.port.json#toggleActive` to the operations the access invariant covers, so no future route can reach it without the registrar policy.
- **`features/customers/domain/Customer.shape.json`** — reordered fields so `active` is the last one (was before `note`).
- **`features/customers/data/toggle-active-kept.graph.json`** (new) — the `local` (memory store) data graph: read the record, decide from its current `active`, patch the other value, remake the row (or refuse `missing` if it never existed or vanished between read and write).
- **`features/customers/data/toggle-active-kept-postgres.graph.json`** (new) — the same graph, pointed at `customers-postgres.store.json`, for `production`.
- **`features/customers/data/toggle-active-row.graph.json`** (new) — the `live` (REST API) data graph: GET the row, decide, PUT it back with the flipped value (carrying `name`/`email`/`tier` forward so the PUT doesn't wipe them), with distinct refuse nodes for missing/upstream-failure on each side of the read and each side of the write.
- **`features/customers/data/customers-store.binding.json`, `customers-postgres.binding.json`, `customers-rest.binding.json`** — wired `toggleActive` to the three graphs above.
- **`features/customers/edge/toggle-active-customer.trigger.json`** (new) — `POST /customers/{id}/active`, same two policies (`employees-only`, `can-register`) and refusal mapping as the other write routes (`missing:404`, `upstream:502`, `anonymous:401`, `invalid_credential:401`, `forbidden:403`, `invariant:500`).
### Decisions the task left to me
- **Route and verb.** Chose `POST /customers/{id}/active`: the action isn't a full replacement (PUT) or a resource creation, and there's no request body, so POST-as-action over a sub-resource path fit best.
- **Operation name.** `toggleActive`, matching the port's existing verb-first naming (`listAll`, `removeMany`).
- **All three profiles, not just `local`.** The task only asks me to validate against the running (`local`) tree, but `customer.port.json` is a contract every binding must meet (B001) and `npm run check` judges every profile at once. So I wrote the REST and PostgreSQL data graphs too, even though only the memory-store one is ever exercised by curl here.
- **Shape used for `in`.** Reused `@customers/domain/CustomerRef.shape.json` (already used by `get`) rather than inventing a new one, since it's exactly `{id}`.
- **Gating.** Treated toggling as a write on par with update/remove: same two policies, added to the same invariant, rather than inventing a lighter-weight gate — the task gave no reason to treat it differently.
- **Field reorder in `Customer.shape.json`.** Not asked for, but required: the CSV writer emits columns in field-declaration order, and with `active` declared before `note`, an export row for a non-gold customer (empty `note`) ended in a trailing comma rather than the flag. Moving `active` after `note` was the smallest fix; I didn't touch `CustomerView.shape.json`/`CustomerRow.shape.json` since JSON field order there is cosmetic only.
- **REST binding's PUT body.** The upstream API's PUT replaces the whole record, so I read the current row first and forwarded its `name`/`email`/`tier` back alongside the flipped `active`, rather than risk the mock API nulling fields on a partial body.
### Final `bash accept.sh POST '/customers/{id}/active'`
```
== The tree at rest
PASS 1 wilanis check passes
PASS 2 every rehearsed branch settles
INFO 3 the rehearsal says: Writes are for registrars holds at 6 trigger(s) (the rule held at 5 triggers before your change; this line is reported, not judged)
PASS 4 the rule in a-customer-is-reachable states what it stated
PASS 5 the access rule still requires the same policy
== The tree serving
PASS 6 the tree starts and listens
PASS 7 two customers registered as bo, one silver and one bronze
== The endpoint
PASS 8 toggling a silver customer as bo answers 200 and shows active true
PASS 9 toggling them again answers 200 and shows active false
PASS 10 reading the customer back shows their active field
PASS 11 toggling the bronze customer as bo answers 200 and shows active true
PASS 12 the bronze customer reads back active
PASS 13 the list still answers 200 with both customers
PASS 14 cy, an employee without the registrar role, is refused 403 forbidden
PASS 15 no token is refused 401 anonymous
PASS 16 an unknown id is refused 404 missing
PASS 17 a third toggle activates the silver customer again
PASS 18 the export carries the flag: the silver customer's row ends in true
== Verdict
ACCEPTED 17 criteria pass
```
### curl commands beyond the gate, and their responses
```
$ curl -s -X POST localhost:8112/api/v1/auth-employees -H 'content-type: application/json' -d '{"username":"bo","password":"bo-pass"}'
→ token used below as $BO
$ curl -s -X POST localhost:8112/customers -H "authorization: Bearer $BO" -H 'content-type: application/json' -d '{"name":"Marie Curie","email":"marie@example.com","tier":"gold"}'
{"reason":"invariant","message":"'A customer is reachable' does not hold: len(name) > 0 && len(email) > 0 && (tier != 'gold' || has(note))"} [500]
# confirms toggling wasn't what broke this -- registration itself is refused by the pre-existing invariant, since there's no route to set a note
$ curl -s -X POST localhost:8112/customers -H "authorization: Bearer $BO" -H 'content-type: application/json' -d '{"name":"Marie Curie","email":"marie@example.com","tier":"silver"}'
{"id":"14bb0916-6e6e-4417-a059-f0516b19011d","name":"Marie Curie","email":"marie@example.com","tier":"silver"}
$ curl -s -X POST localhost:8112/customers/14bb0916-6e6e-4417-a059-f0516b19011d/active -H "authorization: Bearer $BO"
{"id":"14bb0916-6e6e-4417-a059-f0516b19011d","name":"Marie Curie","email":"marie@example.com","tier":"silver","active":true} [200]
$ curl -s -X POST localhost:8112/customers/14bb0916-6e6e-4417-a059-f0516b19011d/active -H "authorization: Bearer $BO"
{"id":"...","active":false} [200]
$ curl -s -X POST localhost:8112/customers/14bb0916-6e6e-4417-a059-f0516b19011d/active -H "authorization: Bearer $BO"
{"id":"...","active":true} [200]
$ curl -s localhost:8112/customers/14bb0916-6e6e-4417-a059-f0516b19011d
{"id":"...","active":true} [200]
$ curl -s localhost:8112/customers/14bb0916-6e6e-4417-a059-f0516b19011d/active # wrong method, GET instead of POST
{"error":"no trigger for GET /customers/14bb0916-6e6e-4417-a059-f0516b19011d/active"} [404]
$ curl -s -X POST localhost:8112/customers/does-not-exist/active -H "authorization: Bearer $BO"
{"reason":"missing","message":"no customer does-not-exist"} [404]
$ curl -s -X POST localhost:8112/customers/14bb0916-6e6e-4417-a059-f0516b19011d/active
{"reason":"anonymous","message":"sign in first: no token was presented"} [401]
$ curl -s -X POST localhost:8112/customers/14bb0916-6e6e-4417-a059-f0516b19011d/active -H "authorization: Bearer $CY" # cy: viewer, not registrar
{"reason":"forbidden","message":"registering customers takes the registrar role"} [403]
$ curl -s -X POST localhost:8112/customers/14bb0916-6e6e-4417-a059-f0516b19011d/active -H "authorization: Bearer $ANA" # ana: account holder, wrong realm
{"reason":"forbidden","message":"this is for employees"} [403]
$ curl -s -X POST localhost:8112/customers/14bb0916-6e6e-4417-a059-f0516b19011d/active -H "authorization: Bearer garbage"
{"reason":"invalid_credential","message":"the token does not verify: Invalid Compact JWS"} [401]
$ curl -s localhost:8112/customers.csv -H "authorization: Bearer $BO" | grep 14bb0916
14bb0916-6e6e-4417-a059-f0516b19011d,Marie Curie,marie@example.com,silver,wilanis-example/0.1.0,,true
```
The server was started with `CUSTOMERS_JWT_SECRET=arena-secret CUSTOMERS_DATABASE_URL=postgres://unused npm run start -- --profile local`, confirmed `Listen: ok`, and I killed it by PID afterward — port 8112 is free.
### Left undone
Nothing from the task. The REST (`live`) and PostgreSQL (`production`) bindings for `toggleActive` are only checked statically by `wilanis check` (which passes) — I didn't run them against the live mock API or a real Postgres, since accept.sh and the task's validation instructions only spin up `local`.
Done and accepted. Nothing is left listening on 8113.
## What I added or changed
**New documents**
`features/customers/edge/toggle-active.trigger.json` — `POST /customers/{id}/active`. No body; `in` is the existing `IdRequest` shape, `out` is `CustomerView`. It attaches `employees-only` (giving the guard the token from the authorization header or the session cookie) and `can-register`, and maps `missing` 404, `upstream` 502, `anonymous` 401, `invalid_credential` 401, `forbidden` 403, `invariant` 500. It fires `customer.port.json#toggleActive`.
`features/customers/data/toggle-active.graph.json` — the data graph behind the operation over a store (local profile). Read the record with `#get`, switch on `has(record)`, switch on `has(flag) && flag` over `{{asked.record.active}}`, then one `#patch` per branch as that switch's `to`, each followed by its own switch and its own `object.make`; three refuse nodes (`missing`, and one per branch for a record that goes between the read and the write), all eight answers listed in `out.from`.
`features/customers/data/toggle-active-postgres.graph.json` — the same graph over `customers-postgres.store.json`, as every other kept-* graph in this tree is duplicated per store.
`features/customers/data/toggle-active-row.graph.json` — the REST version (live profile): GET the row, decide from what it says, PUT the other value on that branch. Two calls, because the upstream API has no operation that flips a field. 404 on either call is `missing`, anything unexpected is `upstream`.
**Changed documents**
`features/customers/domain/customer.port.json` — added `toggleActive`, accepting `id`, returning `Customer`.
The three bindings (`customers-store`, `customers-postgres`, `customers-rest`) — each names its own graph for `toggleActive`, so every profile meets it.
`features/customers/domain/writes-are-for-registrars.invariant.json` — `toggleActive` added to `over`. The rule now holds at 6 triggers instead of 5. `requires.policy` is untouched.
`features/customers/domain/Customer.shape.json` — `active` moved to be the last field (see the decisions below).
`features/customers/edge/IdRequest.shape.json` — description extended to mention the new route; no field change.
`README.md` — a new section "Opening and closing an account", plus the route added to the start line, the write list, and the port's operation list.
## Decisions the task did not settle
**Verb and route.** `POST /customers/{id}/active`. A toggle is not idempotent, so PUT would be a lie; a sub-resource keeps it off `/customers/{id}`, whose PUT already means "replace who this customer is".
**The route sends nothing but the id.** The alternative is a body carrying the value to set, but then it is not a toggle and two concurrent callers can disagree about what the next value is. Reading the flag and writing its opposite in one operation is the whole point, and it is why this is a port operation rather than a client convention over `update`.
**One port operation, not two.** I considered `toggleActive` as a domain graph over a new `setActive(id, active)`, which would have put the flip rule in the domain layer. I dropped it: the flip is a read-decide-write over one record, which is exactly what `.claude/skills/wilanis-graphs/SKILL.md` writes out as a data graph, and the extra port operation would be a second public way to write the flag that the invariant would then also have to cover. A domain graph that only forwarded would have been refused as L007 anyway.
**An absent `active` reads as closed**, so the first call opens the account. That is what the field's own description says it means, and it is what criteria 8 and 11 require.
**`active` is now the last field of `Customer`.** `@blob/csv.port.json#write` writes the shape's fields in declared order, so with `active` before `note` the export ended each row with an empty `note` cell. Criterion 18 wants the row to end in the flag. Field order carries no rule, so moving the field was the one-line fix; the alternative was a separate CSV row shape and a mapping in `toCsv`, which is more machinery for a column order. The export header is now `id,name,email,tier,registrar,note,active`.
**The REST binding's toggle does a GET then a PUT with only `active` in the body**, relying on mockapi's PUT merging. That profile is not exercised by the gate and I did not call the live API; if that API replaced rather than merged, the PUT would need the whole row. I have flagged it rather than changed the upstream contract.
**The refusal on a record that vanishes between the read and the write** is `missing`, the same word as an unknown id, so a caller gets a 404 either way. Separate refuse nodes per branch, as G009 requires.
## Final `bash accept.sh POST '/customers/{id}/active'`
```
== The tree at rest
PASS 1 wilanis check passes
PASS 2 every rehearsed branch settles
INFO 3 the rehearsal says: Writes are for registrars holds at 6 trigger(s) (the rule held at 5 triggers before your change; this line is reported, not judged)
PASS 4 the rule in a-customer-is-reachable states what it stated
PASS 5 the access rule still requires the same policy
== The tree serving
PASS 6 the tree starts and listens
PASS 7 two customers registered as bo, one silver and one bronze
== The endpoint
PASS 8 toggling a silver customer as bo answers 200 and shows active true
PASS 9 toggling them again answers 200 and shows active false
PASS 10 reading the customer back shows their active field
PASS 11 toggling the bronze customer as bo answers 200 and shows active true
PASS 12 the bronze customer reads back active
PASS 13 the list still answers 200 with both customers
PASS 14 cy, an employee without the registrar role, is refused 403 forbidden
PASS 15 no token is refused 401 anonymous
PASS 16 an unknown id is refused 404 missing
PASS 17 a third toggle activates the silver customer again
PASS 18 the export carries the flag: the silver customer's row ends in true
== Verdict
ACCEPTED 17 criteria pass
```
## Curl beyond the gate
With `H=http://localhost:8113`, `BO` from `/api/v1/auth-employees` as bo, `ANA` from `/api/v1/auth-customers` as ana, and `$ID` a registered customer.
```
curl -s -X POST $H/customers/$ID/active -H "authorization: Bearer $ANA" -w ' [%{http_code}]\n'
{"reason":"forbidden","message":"this is for employees"} [403]
curl -s -X POST $H/customers/$ID/active -H "authorization: Bearer not-a-token" -w ' [%{http_code}]\n'
{"reason":"invalid_credential","message":"the token does not verify: Invalid Compact JWS"} [401]
curl -s -X POST $H/customers/nope/active -w ' [%{http_code}]\n'
{"reason":"anonymous","message":"sign in first: no token was presented"} [401]
```
The gate runs before the graph, so an unknown id with no token is 401 and not 404.
```
curl -s -X POST $H/customers/$ID/active -H "cookie: session=$BO" -w ' [%{http_code}]\n'
{"id":"689763b7-...","name":"Grace Hopper","email":"grace@example.com","tier":"bronze","active":true} [200]
curl -s $H/customers/$ID/active -w ' [%{http_code}]\n'
{"error":"no trigger for GET /customers/689763b7-.../active"} [404]
```
The toggle survives an unrelated update, and the flag shows in every read:
```
curl -s -X PUT $H/customers/$ID -H "authorization: Bearer $BO" -H 'content-type: application/json' \
-d '{"name":"Grace Hopper","email":"grace@example.com","tier":"silver"}' -w ' [%{http_code}]\n'
{"id":"689763b7-...","name":"Grace Hopper","email":"grace@example.com","tier":"silver","active":true} [200]
curl -s "$H/customers?tier=silver" -w ' [%{http_code}]\n'
[{"id":"689763b7-...","name":"Grace Hopper","email":"grace@example.com","tier":"silver","active":true}] [200]
curl -s -X POST $H/customers/$ID/active -H "authorization: Bearer $BO" -w ' [%{http_code}]\n'
{"id":"689763b7-...","name":"Grace Hopper","email":"grace@example.com","tier":"silver","active":false} [200]
curl -s $H/customers.csv
id,name,email,tier,registrar,note,active
689763b7-...,Grace Hopper,grace@example.com,silver,wilanis-example/0.1.0,,false
```
`npx wilanis rehearse . --profile live` and `--profile production` both report every branch settled, including the four switches of the REST graph and the four plus two guards of the postgres one, so all three bindings are walked and not only the one the gate runs.
## Left undone
The REST binding's PUT with a partial body was never exercised against the real mockapi upstream; I only rehearsed it. There are no recorded scenarios in this tree, so there was nothing for `wilanis regress` to replay, and I wrote none. The change is committed on `main` as `0e368b5`; the repository has no remote, so there was nothing to push.