Command contract: the fields toilville-design#181/#182 require and no surface declares #22
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Toilville/rituals-api-spec#22
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The wish/parlay command decisions in toilville-design#181 and #182 are accepted and closed. Both rest on one rule:
The command contract declares none of them. That is what this issue is for.
This is not spec drift
I measured all three surfaces. They agree with each other:
core.wish_available_commands→ritual-coreAvailableCommandopenapi.yamlAvailableCommandcommand,label,to_status,required_inputs,input_schema,formforge_handlers/wish_command.rs:138)AvailableParlayCommandaddsdecisionand is otherwise identical. So the spec is an accurate description of what the server serves — nothing here is a spec bug. The contract simply has no vocabulary for what the renderers were told to read, andCLAUDE.md's rule ("REST endpoints must matchritual-server") means the spec PR is last, not first.Sequence for every item below: spel migration (the catalog columns and
core.wish_available_commands' projection) → ritual-engine (AvailableCommand,CommandResult, passthrough) → this repo. Same order the0.62.0–0.67.0entries inCHANGELOG.mdalready record.What to add
1.
severity— per command#182 is the authoritative list, and it is five values, not four:
none, Simple, Review, Ceremony, Destructive.nonemeans no confirmation.none. Validation rejects it; the renderer does not default it.2.
rank— per commandThe vocabulary already exists and needs no new members:
SemanticIntent.ACTION_PRIMARY/ACTION_SECONDARY. Overflow ("the rest go to More") is renderer-derived from available space and is not declared.Three conditions are contract errors, rejected at validation, never silently repaired:
severity;Destructivecommand declared primary.The third is the one case where a renderer would otherwise have to override a declared rank, against the opening rule that it never derives placement — hence an error rather than a demotion.
3.
importance— per commandcritical | high | normal | low. The vocabulary exists asImportanceLevel, but only onBlockSemantics, i.e. per block. A command is aButtonElement, and there is no way today to say "this command is secondary, critical" while its neighbour is primary.criticalalone does not forbid being primary.Client-side counterpart: toilville_rituals#577.
4.
locked, with a reason and a next step — per commandSo locked is a third state beside present-and-usable and absent, and the reason is text, not tooltip-only. A locked command keeps its place in the order, cannot be primary or default, and cannot be activated.
This is the mechanism that replaces client-side tier derivation — see item 5.
5.
min_tieronButtonElement: both clients decode a field this contract forbidsschemas/forge_block.json'sButtonElementisadditionalProperties: falseand declares exactlytype,text,action_id,style,url,disabled. It declares neithermin_tiernorintent. Both clients decode both:packages/udg/lib/src/blocks/forge_block.dart:71—minTier: json['min_tier'] as int?, andintentDesign/ForgeBlock/ForgeBlock.swift:344,349, and it enforcesmin_tier(ActionsBlockView.swift:156)A server sending either field would emit a block that fails validation against its own schema, while the clients are built to read them. One of the two has to move. Since #181 retires client-side tier derivation in favour of item 4, the options are:
min_tierandintentonButtonElementas a transitional fallback for specs that predate the command contract, with a deprecation note pointing atlocked; orMy answer is on toilville_rituals#578, which also covers the client-side parity defect (Flutter decodes and never gates; macOS gates and lets the gated button be activated, which #181 forbids).
6. A
timeout, so Pending can reach FailedToday nothing exits Pending. The sent command is disabled so it cannot be sent twice, there is no optimistic update, and no timeout is defined — so if the result never arrives, the command stays disabled indefinitely and
Failed, the only state carrying a recovery action, is unreachable.POST …/commandsis synchronous, so this is about transport and session failure, not a long-running job.Decide whether this is one documented contract-wide constant or a per-command value, and say so in
openapi.yamlrather than leaving it to each client.7. A distinct
conflictoutcomeOutcomeisapplied | noop | rejected | duplicatein all three surfaces (wish_command.rs:106,WishCommandResult.outcome,ParlayCommandResult.outcome).#187's "conflicts with a newer version" state therefore depends on this and on nothing else — ville recorded it there as an engineering dependency rather than a design deliverable. Note
DiffStatusalready has aconflictmember (schemas/forge_block.json:800) for the diff block; this is the command-result analogue and should read consistently with it.8.
errorson a command result, field-linked where possibleIntentRunResultalready carrieserrors: [string]for a rejection.WishCommandResultandParlayCommandResultcarry onlyreason. Addingerrorsbrings them level; the "tied to a field" part needs the link to be expressible at all, so decide whether an error names theinputskey it belongs to.9. The lineage proof line on an applied result
CommandResultcarries no lineage reference, so a renderer cannot show the proof line without a second lookup it has no key for. The entry is already written —GET /api/v1/eventsstreams the fact — so this is about returning its identifier (seq_num, or the entry id/hash) on the result.Already satisfied — recording so nobody re-opens it
The client mints
command_id.WishCommandRequest.command_idis documented as "Idempotency key … Generated when absent", andritual-coreexposeswith_command_id. #181's "a retry after a transport or session failure reuses the same command ID" therefore works, provided the client mints the id rather than letting the server generate one — if the server generates it, a transport failure means the key was never learned and the retry applies twice. That is a client rule, not a contract change, and it belongs in whichever client issue implements retry.Acceptance
openapi.yamland, where it belongs to the block vocabulary, inschemas/forge_block.json, withschemas/index.jsonandCHANGELOG.mdupdated the way the0.62.0–0.67.0entries do it.ButtonElementand the two clients agree.CLAUDE.md.Blocks
min_tierparity (items 4, 5)Design side is closed: toilville-design #178, #181, #182 and #187 are decided and accepted. Nothing here needs a design answer.