feat(decisions): let a consumer read back a decision it raised - #1116
Merged
Conversation
Decidiq can be asked to raise a Decision and it announces the conclusion. It could not be ASKED what became of one, so a consumer whose DecisionConcludedEvent went missing had nothing to consult. DecisionStateRequestedEvent + DecisionStateRequestedListener close that, in the same synchronous request/response-over-the-bus shape DecisionRequestedEvent already uses. It is NOT a second delivery mechanism: the announcement stays how a conclusion arrives, and this is what a consumer reads when the announcement did not. It derives nothing of its own. The status comes from getOutcomeEnvelope() -- the same array DecisionConcludedEvent carries -- and the authorization from the REQ-DCDH-101 outcome-read rule the HTTP endpoint enforces. The bus carries no session, so the read is scoped to the uid the event names, and an event naming none is REFUSED rather than read as a system caller. No admin bypass on this path. DecisionIntegrationAuthorizationGuard gains resolveOutcomeReadAccess(), reporting allowed / denied / unresolved, and the boolean now delegates to it. An unreachable OpenRegister must not read as a refusal: it would fail a consumer's waiting run on an authorization error it never had. The HTTP behaviour is unchanged -- unresolved still collapses to false there.
Picks up the vendored manifest-schema sync, so the gate stops rejecting open-form properties the library has supported since 2.29.0.
rubenvdlinde
requested review from
Rem-Dam,
WilcoLouwerse,
bbrands02 and
rjzondervan
as code owners
September 3, 2026 14:24
Contributor
Quality Report — ConductionNL/decidiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-nav-ceiling | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-l10n-js | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| composer | ✅ | ✅ 104/104 | |||
| npm | ✅ | ✅ 537/537 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ✅ | ||||
| Playwright | ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-09-03 14:32 UTC
Download the full PDF report from the workflow artifacts.
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.
Why
dossiq.requestDecisionraises a decision in decidiq and then waits. When the conclusion event is missed — a refused signal, a worker that died between the write and the dispatch — the flow engine's heartbeat wakes, has nothing to consult, and re-suspends. The run wedges forever.openregister#3358 gave the engine the ability to recover by re-reading terminal state, and dossiq#1756 applied it to
dossiq.askPerson, which reads back its own case task.requestDecisioncould not follow, because decidiq had no way to answer "what happened to the decision I raised?" — it could accept a raise and announce a conclusion, and nothing in between.The seam
DecisionStateRequestedEvent, dispatched by the consumer and answered in place byDecisionStateRequestedListener. This follows the ADR-041 event recipe rather than adding an HTTP endpoint, for two reasons: the fleet's cross-app rule is events, not RPC, and the raise and the conclusion already travel that way — a third mechanism for the same conversation is how the double-engine defects in this programme started.It answers with exactly the three states a waiting node can act on: still open, decided (with the outcome), or terminal without a decision. That last one matters most: a withdrawn or cancelled decision is not "keep waiting", and a node that cannot tell those apart either strands or invents an answer.
Authorization is scoped, not bypassed.
DecisionIntegrationAuthorizationGuarddecides whether the asking context may see the decision it names, so the seam cannot become a way to enumerate decisions a caller has no business reading. It answers about one decision, by the reference the caller already holds.What this does not do
It does not deliver conclusions. The existing
ApprovalRouteConcludedEventpath is still how an answer normally arrives, and is unchanged. This is the recovery path only: it makes state readable so a heartbeat has something to consult, which is why nothing here duplicates the announcer.Tests
DecisionStateRequestedListenerTestcovers the three states plus the branches that decide whether an answer is given at all: an unknown reference, and a caller the guard refuses. The fakes refuse what live OpenRegister refuses, following the lesson from #1107 — a fake that accepts what production rejects cannot fail.Verification
phpunit 1386 green (30 pre-existing skips) · phpcs, psalm, phpstan clean · phpmd clean per subdirectory across both rulesets · hydra-gates v1.11.1
--scope-to-diff: all 32 applicable gates passed, and all 32 ran.Also takes hydra-gates v1.11.1 in
composer.lock, which picks up the vendored manifest-schema sync.The dossiq half —
requestDecisionre-reading through this seam with #1756's semantics — follows in its own PR, since it cannot be verified until this is ondevelopment.🤖 Generated with Claude Code