fix(web): keep the plan card to decisions it can actually answer

Review follow-ups on the presentation intent.

The card claimed any single-question request declaring the intent, then sent one
of two labels — so a producer offering a third option, or a multi-select batch,
lost answers the generic flow would have shown. That contradicts the intent's
own contract, so `planReviewOf` now claims only a binary single choice and
leaves everything else to the flow that can express it.

`ask()` also rejects a plan-review intent on a question with no `detail`: the
intent declares detail IS the plan, and without one a honouring UI asks the
user to approve something invisible. The client keeps its own fallback — it sits
downstream of a wire boundary — but the misconfiguration now fails at the asker.

`planReviewOf` stops being a value export of the client contract face (client
export discipline: pure helpers stay internal; the tests already import it
relatively), and the ui-question README fallback list, both languages, now
states every condition the code enforces.
This commit is contained in:
creatixchu
2026-07-30 19:38:18 +08:00
parent 132294fe8d
commit 6d7bd7e703
18 changed files with 73 additions and 34 deletions
@@ -95,6 +95,13 @@ describe('planReviewOf', () => {
...questions()[0] as object, intent: { kind: 'plan-review', approve: 'Ship it' },
}]],
['an intent with no options at all', () => [{ ...questions()[0] as object, options: undefined }]],
// Two buttons cannot send a third label or a combination, and the generic
// flow can: an intent never costs the user a reachable answer.
['a third option the card could not offer', () => [{
...questions()[0] as object,
options: [{ label: 'Approve' }, { label: 'Keep planning' }, { label: 'Start over' }],
}]],
['a multi-select decision', () => [{ ...questions()[0] as object, multiSelect: true }]],
])('declines %s, leaving the request to the generic flow', (_case, build) => {
expect(planReviewOf(build() as never)).toBeUndefined()
})