feat(contract): give every op and every app-raised code a home in the registry - #18
Conversation
… registry The registry gains an ops block naming what a cmd frame may carry — site.mode.set and battery.hold — and the scope each demands. Both sides hand-wrote that pair and nothing compared them. The OP_ constants and the simulator's authorisation table are now read back against the block. client_errors takes in E_NO_ACK, E_NO_ANSWER and E_BAD_BODY, the three app-raised codes that sat outside the file whose header claims every such code has a home there. They join the RETRYABLE table, so the existing set-equality check now pins their retry answers too. Contract-pair: srcfl/ftw@agent/registry-ops-client-errors Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
ftw-webapp | 64edf1d | Aug 09 2026, 02:52 PM |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 64edf1db63
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // went away — the session reconnects on its own and the same ask can go | ||
| // again. A bad body is the box and the app disagreeing about a route, and | ||
| // asking again gets the same answer. | ||
| E_NO_ACK: true, |
There was a problem hiding this comment.
Do not assume a missing acknowledgement means no execution
When a command reaches the box but its acknowledgement is lost or delayed beyond the five-second deadline, the box may already have acted even though the client raises E_NO_ACK. Session.command() generates a new UUID for every invocation and discards the original pending entry on timeout, so a retry does not reuse the box's idempotency key and may submit the intent twice. Preserve and resend the original command ID before classifying this as retryable, or avoid presenting an ordinary retry.
Useful? React with 👍 / 👎.
| expect(entries.length, 'no ops were found in the simulator to check').toBe(declared.size) | ||
| for (const [, constant, scope] of entries) { | ||
| const wire = ops[constant!] | ||
| expect(wire, `the simulator's ${constant} is not a constant in messages.ts`).toBeDefined() | ||
| expect(scope, `the simulator demands ${scope} for ${wire}`).toBe(declared.get(wire!)) |
There was a problem hiding this comment.
Compare simulator operation names as a set
This count plus per-entry check does not prove that every registry operation occurs in OP_SCOPES: if the simulator accidentally contains [OP_SET_MODE] twice and omits [OP_BATTERY_HOLD], the count remains two and both loop iterations validate the same correct mapping. The resulting runtime object lacks the omitted authorization entry, yet this contract test passes; resolve each constant to its wire name and compare the complete wire-name/scope map while rejecting duplicates.
Useful? React with 👍 / 👎.
Contract-pair: srcfl/ftw@agent/registry-ops-client-errors
Pairs with srcfl/ftw#868 — the two copies of
contract/registry.yamlare one file and must merge together.What
An
opsblock in the registry.site.mode.setandbattery.holdcrossed the wire as hand-written strings on both sides —OP_SET_MODEandOP_BATTERY_HOLDhere,OpSetModebeside the box's dispatcher — and the scope each op demands was written twice more: in this app's simulator (OP_SCOPESinsrc/lib/sim/box.ts) and in the box'sdefaultOps(). Nothing compared any of the four. The block now names each op and the scope its grant must carry.tests/registry-contract.test.tsreads it all back: theOP_constants must be the registry's ops exactly, both directions; every op's scope must be a declared scope; and the simulator's authorisation table must demand the registry's scope for every op. The box's real table is checked on its own side, so the two tables can now disagree with each other only by first disagreeing with the registry — which CI compares byte for byte.battery.holdstays declared-but-unimplemented on the box: an app may say it, the box answersE_UNKNOWN_OP, and a box test pins that rejection.Homes in
client_errors. The block's header says every app-raised code has a home there. Three did not:E_NO_ACK(session.ts's ack deadline),E_NO_ANSWERandE_BAD_BODY(box-api.ts, plus oneE_BAD_BODYsite in access.svelte.ts). They follow the pathE_RESPONSE_TOO_LARGEtook — an entry with a retry answer, and a row in theRETRYABLEtable, which the existing set-equality test now pins in both directions.The retry answers:
E_NO_ACKis retryable because no ack means the box never took the intent, so asking again cannot act twice.E_NO_ANSWERis retryable because the wire went away and the session reconnects on its own.E_BAD_BODYis not — the box and the app disagree about a route, and asking again gets the same answer.Tests
command operationsdescribe block; each check was verified red under mutation before landing green.npm run verifypasses (types, 728 tests, production build).check-contract-drift.mjsagainst the box branch: 12110 bytes, byte for byte.🤖 Generated with Claude Code