fix(wire): withdraw the BatchAction.DELETE declaration - #356
Merged
Conversation
`allowed_actions` named `delete` on three batch states, but no route, MCP tool or control could perform it. Under the `ui-capabilities` contract the frontend renders what the wire declares, so a declared action obliges every conforming client to offer it — naming one nothing can carry out makes the wire the source of a control that cannot work. `BatchService.delete` and `DELETABLE_STATES` are kept: the rule survives and the SDK still enforces it. What is withdrawn is the promise. The declaration comes back when the route does, in the same change. The rationale sits in a comment above `BatchAction` rather than in its docstring, because FastAPI copies a docstring verbatim into `openapi.json` as the schema's `description`.
JArmandoAnaya
enabled auto-merge (squash)
August 5, 2026 12:33
This was referenced Aug 5, 2026
JArmandoAnaya
added a commit
that referenced
this pull request
Aug 5, 2026
…g on it (#361) A branch, worktree, PR or "unpushed work" a prompt describes is a snapshot, not a fact. #228 was handed over as a local-only branch with no remote and no PR; another session had pushed, merged and cleaned it up inside the hour, so the only work left was a `git worktree prune`. #356's predicted rebase conflict had already dissolved the same way. The rule sits with worktree setup, because that is where a stale premise is cheapest to catch: one `git fetch --prune`, one `gh pr list --state all --head`, one `gh issue view --json state,closedAt`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
allowed_actionsdeclareddeleteondraft,approvedandin_annotation, but no route, MCP tool or control could perform it. The declaration is withdrawn:BatchActionloses itsDELETEmember andBATCH_GATESitsDELETABLE_STATESentry.BatchService.deleteandDELETABLE_STATESare kept. The rule survives and the SDK still enforces it — includingBatchImmutableon acompletedbatch. What is withdrawn is the promise, not the capability.Why option 1 (withdraw) rather than routing it
Recorded on #331 by Armando, 2026-08-05:
This was a declared-but-unreachable orphan: the capability contract failing from the wire side. The
ui-capabilitiesskill's rule is that the frontend renders what the wire declares, so an action named inallowed_actionsobliges every conforming client to offer a control for it. Withdrawing is one enum member and is reversed by adding it back in the same change as the route.The contract test needed no edit, and that is the point
tests/kernel/test_capabilities.pyderives its cases fromBatchActionitself, so it can only ever check members that exist — it cannot assert an absence. Two things follow, and both are in this diff rather than assumed:test_every_action_is_decided_by_exactly_one_sourceassertsset(BATCH_MOVES) | set(BATCH_GATES) == set(BatchAction). The suite therefore structurally forbids "keep the vocabulary, drop the gate" — withdrawing the member and its gate together was the only shape that passes.test_no_state_declares_delete_because_nothing_can_perform_it, which names the withdrawn string directly. Proving a member is gone takes spelling it.Where the rationale lives
In a
#comment aboveBatchAction, not in its docstring: FastAPI copies a docstring verbatim intoopenapi.jsonas the schemadescription. Writing it as a docstring first produced a nine-line RST-flavoured essay in the public contract withdouble backticksrendering literally. The docstring stays the one short sentence a client should read.The second roster — caught only by the browser suite
The withdrawal broke the e2e suite, and
tsccould not see it. There are two transcriptions of the batch action table:frontend/ui-core/src/testing/wire.fixtures.ts— typedBatchAction, so removing the member made it a compile error.frontend/app/e2e/_wire.ts— typedreadonly string[], so it compiled fine and served"delete"to the stub routes. The generatedchecks.tsthen rejected the payload inside a hook, which surfaces as a query that never resolves: every gallery spec timed out with "element(s) not found" rather than an assertion naming the cause.Both are now correct. This is the multi-roster trap in the refactor protocol's sweep rule — the phrase grep found one copy and the type system found one copy, and neither found the same one.
Found, not fixed
frontend/app/e2e/_wire.tsis held true by nothing. Its own docstring claimstests/kernel/test_capabilities.pyholds its values true; that test does not read this file. Typing it against the generatedBatchActionunion would convert this whole failure class from a 20-second timeout into a compile error, but@visionset/ui-coreexports only its package root, so it would mean widening the public API — a design change beyond this task. Worth an issue.fix/317-ingest-failure-name(PR kernel: an ingest failure names the item, not the server's directory (#317) #321, squash-merged 2026-08-05). Pruned separately as cleanup, not by this PR.batch-lifecycleskill — verified, unchangedThe task called for adding the live-trunk line if missing. It is not missing: #355 added it in decision 8 — "The projection is live: an edit inside an open batch reaches the trunk on save, not on promotion. Releases are unaffected — the manifest is a frozen blob." No skill change.
Test plan
Full
scripts/check.shcoverage, run in stages under the harness ceiling. Every exit code recorded:pytest tests/kernel tests/architecture tests/formats tests/jobspytest tests/server tests/cli tests/mcp tests/scripts tests/packaging tests/test_versioning.pypytest tests/examplesruff check .(whole tree, post-rebase)ruff format --check .mypy src/visionsetlint-importspnpm -r buildpnpm testpnpm -r lintpnpm generate:client:checkpnpm version:checkplaywright test(annotator + app e2e)playwright test -c playwright.cycle.config.ts(real server)2590 python tests. The pytest split was derived from
ls tests/at run time.Rebased onto
6c23930(#355).docs/batches.mdwas the expected conflict site; git merged it without a textual conflict, so the merged section was re-read end to end — #355's promotion-semantics text (lines 180–247) is intact and the withdrawal paragraph sits after the declaration table, forward-referencing the## Deleting a batchsection that documents the retained SDK method.Closes #331