Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Flow-facing storage work relies on the engine's native runAs scoping; the
local wrapper retires.

## MODIFIED Requirements
## ADDED Requirements

### Requirement: Flow storage work runs under the engine's native scoping

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@

The task-completion resume travels through the engine's guarded signal seam.

## RENAMED Requirements

- FROM: `### Requirement: A task may hold a suspended flow run, and completing it resumes that run @e2e exclude the resume mechanism is asserted by service tests; the user-visible half is covered by the case-flow e2e journey`
- TO: `### Requirement: A completed task resumes its run through the guarded seam`

## MODIFIED Requirements

### Requirement: A completed task resumes its run through the guarded seam
Expand Down Expand Up @@ -41,11 +46,23 @@ the seam call shape is pinned by TaskCompletionResumeListenerTest.
`@e2e exclude` the guard itself is OpenRegister's, mutation-tested there; the
listener's obedience is unit-pinned (TaskCompletionResumeListenerTest).

#### Scenario: A task whose run has gone still completes quietly
#### Scenario: A task whose run has gone is still completable

- **GIVEN** a completed task naming a run uuid the engine cannot resolve
- **WHEN** the seam refuses with RUN_NOT_FOUND
- **THEN** the completion stands and the refusal is recorded as information, not an error

`@e2e exclude` requires deleting a run out from under a task mid-journey;
unit-pinned (TaskCompletionResumeListenerTest).

#### Scenario: A task without a run resumes nothing

- **WHEN** a task recording no run is completed
- **THEN** the task is completed normally
- **AND** no run is resumed and no error is raised

#### Scenario: Completing a task twice resumes once

- **WHEN** an already-completed task is completed again
- **THEN** the run is not resumed a second time
- **AND** the run does not advance past the step twice
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ A human step advances on the state of the task it created, so a completion
whose wake was refused or lost is delivered by the next heartbeat instead of
wedging the run forever.

## MODIFIED Requirements
## ADDED Requirements

### Requirement: An ask advances on its task, not on a signal

Expand Down Expand Up @@ -90,8 +90,6 @@ read the answer given to the first.
`@e2e exclude` the shape of a value passed between flow steps; pinned by
AskPersonHeartbeatRecoveryTest and DossiqAskPersonNodeTest.

## ADDED Requirements

### Requirement: A flow-engine test may drive the real engine

The unit suite SHALL be able to run a test against OpenRegister's real flow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ the next requirement.

@e2e exclude Requires the OR ObjectsProvider pipeline and NC search UI; provider behaviour is covered by openregister's own unified-search-provider e2e suite.

#### Scenario: Non-flagged schema absent from search

- **GIVEN** a `decisionType` config object exists
- **WHEN** a user searches for its title
- **THEN** it does not appear in unified search results (schema not flagged searchable)

@e2e exclude Same rationale — declarative flag asserted by unit test on the register JSON.

#### Scenario: A schema flagged out stays out

- **GIVEN** a schema flagged `searchable: false` in the register definition
Expand All @@ -46,8 +54,25 @@ the next requirement.

@e2e exclude Enforced and tested in openregister (provider security contract); dossiq adds no code path.

## REMOVED Requirements

### Requirement: Deep links for searchable schemas

**Reason:** its only scenario pins the deep-link table to five schemas by name,
including `voorstel` and the route `/apps/dossiq/voorstellen/{uuid}`. That page
was deleted when a voorstel became a case, so the scenario asserts a route the
manifest no longer declares, and a list of schema slugs goes stale every time
the table grows: this change adds two more. The replacement checks the property
that matters instead, that every deep link names a route that exists. Written
as a removal plus an addition rather than a rewrite in place so the dropped
scenario is dropped on the record.

**Migration:** none. The requirement below covers the same deep-link table.

## ADDED Requirements

### Requirement: Deep links resolve to real pages

Every dossiq schema that has a standalone detail page SHALL have a `deepLinks`
entry in `src/manifest.json` mapping `(dossiq, <schemaSlug>)` to that route,
and the entry's `urlTemplate` SHALL name a route the manifest carries.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## MODIFIED Requirements
## ADDED Requirements

### Requirement: The Contacts index lists the people dossiq knows (REQ-ID-4)

Expand Down Expand Up @@ -32,8 +32,6 @@ carry the measurement as a note on the page.
- **THEN** it SHALL render no folder pane
- **AND** the navigation SHALL offer the Organisations index without expanding anything

## ADDED Requirements

### Requirement: The Organisations index lists the organisations dossiq knows (REQ-ID-6)

You find an organisation without already holding a case that names it. The
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,12 @@ evaluate.
Renamed from "PRIORITY and ANY hit policies are explicitly unsupported" and
"Expression grammar is a closed, safe subset" respectively.

## MODIFIED Requirements

### Requirement: PRIORITY and ANY hit policies are evaluated by OpenRegister
The system MUST evaluate a decision table declaring `PRIORITY` or `ANY` rather
than refusing it. `PRIORITY` MUST return the matching rule with the highest
priority, breaking ties by declaration order. `ANY` MUST return the shared
output of the matching rules and MUST raise `hit_policy_violation` when they
disagree, because a table declaring `ANY` asserts that its overlapping rules
agree.
## RENAMED Requirements

This replaces the previous requirement that both were rejected with
`hit_policy_not_implemented`. That requirement described a limitation of
dossiq's own engine, not a decision about DMN. The schema has offered all five
policies in its enum throughout, so the refusal was visible to users as a form
that offered a choice the engine would not honour.
- FROM: `### Requirement: Expression grammar is a closed, safe subset`
- TO: `### Requirement: Expression grammar is a closed, safe subset, owned by OpenRegister`

#### Scenario: PRIORITY returns the highest-priority match
- **GIVEN** a decision table with `hitPolicy: PRIORITY` and matching rules of priority 1, 10 and 5
- **WHEN** it is evaluated
- **THEN** the rule with priority 10 MUST win

#### Scenario: ANY refuses rules that disagree
- **GIVEN** a table with `hitPolicy: ANY` and two matching rules with different outputs
- **WHEN** it is evaluated
- **THEN** the system MUST raise `hit_policy_violation` and MUST NOT pick one

#### Scenario: A PRIORITY table is no longer refused for its policy
- **GIVEN** any decision table declaring `hitPolicy: PRIORITY`
- **WHEN** it is evaluated
- **THEN** the system MUST NOT return `hit_policy_not_implemented`, and MUST
fail only on the table's own contents if those are invalid
## MODIFIED Requirements

### Requirement: Expression grammar is a closed, safe subset, owned by OpenRegister
The system MUST evaluate rule input-entry expressions using only a fixed,
Expand All @@ -57,6 +32,16 @@ nobody is reading.
The behaviour is unchanged: the grammar matrix that proves it moved to
openregister with the class, unaltered.

#### Scenario: Exclusive range boundary does not match
- **GIVEN** an input entry `(25000..40000]`
- **WHEN** the input value is exactly `25000`
- **THEN** the entry MUST NOT match

#### Scenario: Set membership matches one of the listed values
- **GIVEN** an input entry `in (gold, silver, bronze)` on a `string` input
- **WHEN** the input value is `silver`
- **THEN** the entry MUST match

#### Scenario: Range expression matches inclusively
- **GIVEN** an input entry `[0..25000]` on a `number`-typed input
- **WHEN** the input value is exactly `25000`
Expand All @@ -71,3 +56,53 @@ openregister with the class, unaltered.
- **GIVEN** dossiq's source tree
- **THEN** it MUST contain no class implementing the unary-test grammar or the
hit policies, and MUST resolve both from OpenRegister

## REMOVED Requirements

### Requirement: PRIORITY and ANY hit policies are explicitly unsupported

**Reason:** the requirement said dossiq's engine refuses `PRIORITY` and `ANY`
with `hit_policy_not_implemented`, and its scenario "PRIORITY hit policy is
rejected" asserts that refusal. dossiq no longer has an engine to refuse with:
`lib/Service/Dmn/DecisionEngine.php` is deleted and evaluation runs in
OpenRegister, which implements both policies. The schema has offered all five in
its enum throughout, so the refusal was a limitation of one implementation
rather than a decision about DMN. Written as a removal plus an addition rather
than a rewrite in place so the dropped scenario is dropped on the record.

**Migration:** a table declaring `PRIORITY` or `ANY` that used to come back
`hit_policy_not_implemented` now evaluates. `PRIORITY` returns the
highest-priority match, `ANY` raises `hit_policy_violation` when the matching
rules disagree.

## ADDED Requirements

### Requirement: PRIORITY and ANY hit policies are evaluated by OpenRegister
The system MUST evaluate a decision table declaring `PRIORITY` or `ANY` rather
than refusing it. `PRIORITY` MUST return the matching rule with the highest
priority, breaking ties by declaration order. `ANY` MUST return the shared
output of the matching rules and MUST raise `hit_policy_violation` when they
disagree, because a table declaring `ANY` asserts that its overlapping rules
agree.

This replaces the previous requirement that both were rejected with
`hit_policy_not_implemented`. That requirement described a limitation of
dossiq's own engine, not a decision about DMN. The schema has offered all five
policies in its enum throughout, so the refusal was visible to users as a form
that offered a choice the engine would not honour.

#### Scenario: PRIORITY returns the highest-priority match
- **GIVEN** a decision table with `hitPolicy: PRIORITY` and matching rules of priority 1, 10 and 5
- **WHEN** it is evaluated
- **THEN** the rule with priority 10 MUST win

#### Scenario: ANY refuses rules that disagree
- **GIVEN** a table with `hitPolicy: ANY` and two matching rules with different outputs
- **WHEN** it is evaluated
- **THEN** the system MUST raise `hit_policy_violation` and MUST NOT pick one

#### Scenario: A PRIORITY table is no longer refused for its policy
- **GIVEN** any decision table declaring `hitPolicy: PRIORITY`
- **WHEN** it is evaluated
- **THEN** the system MUST NOT return `hit_policy_not_implemented`, and MUST
fail only on the table's own contents if those are invalid
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,47 @@ unchanged. Only the mechanism changes: the non-existent
is replaced by an `IEventDispatcher` dispatch of `DecisionRequestedEvent` plus a `DecisionConcludedEvent`
listener that materialises the ZGW `Besluit`.

## MODIFIED Requirements
## REMOVED Requirements

**Reason (all three):** the three requirements were written against the decidesk
integration leaf, and this change retires that route: delegation now travels as
a typed `DecisionRequestedEvent` and the outcome comes back as
`DecisionConcludedEvent`. Every scenario under them names the leaf as the
mechanism ("raises a decidesk Decision" through `IntegrationService::getLeaf`,
"the leaf is not registered or returns an error"), a call dossiq no longer
makes. Carrying those scenarios forward would leave the spec asserting a
mechanism the code does not have, so each requirement is removed and re-added
under the same REQ id with the event contract in place of the leaf.

**Migration (all three):** none for a reader of the spec. The three requirements
below keep the REQ-PDCD-001, REQ-PDCD-002 and REQ-PDCD-003 ids and state the
same three rules: decisions are raised in decidesk, delegation fails closed when
decidesk does not answer, and the ZGW Besluit is materialised from the outcome.

### Requirement: REQ-PDCD-001 — Contract Decisions Are Raised As decidesk Decisions

**Reason:** replaced by the event-contract version below.

**Migration:** the leaf call `IntegrationService::getLeaf('decidesk')` becomes a
`dispatchTyped(DecisionRequestedEvent)`; the decision id is read from the
handled event rather than the leaf's return value.

### Requirement: REQ-PDCD-002 — Delegation Fails Closed When decidesk Is Unavailable

**Reason:** replaced by the event-contract version below.

**Migration:** the unavailability test moves from "the leaf is not registered or
returns an error" to "the event class does not exist, the event came back
unhandled, or it carries no decision id".

### Requirement: REQ-PDCD-003 — The ZGW Besluit Is Materialised From The decidesk Outcome

**Reason:** replaced by the event-contract version below.

**Migration:** the outcome arrives as a `DecisionConcludedEvent` on dossiq's
listener rather than being polled from the leaf.

## ADDED Requirements

### Requirement: REQ-PDCD-001 — Contract Decisions Are Raised As decidesk Decisions Via Events

Expand Down Expand Up @@ -46,7 +86,7 @@ state machine for the decision.

---

### Requirement: REQ-PDCD-002 — Delegation Fails Closed When decidesk Is Unavailable
### Requirement: REQ-PDCD-002 — Delegation fails closed when decidesk does not answer the event

dossiq SHALL fail closed when decidesk cannot handle the decision: if
`class_exists(\OCA\Decidesk\Event\DecisionRequestedEvent::class)` is false (decidesk not installed), OR
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ MUST be configurable via the dossiq admin settings UI.

**Feature tier**: MVP

#### Scenario: Payment signal generation

- **GIVEN** a `DwangsomBerekening` closes with a locked `definitievBedrag` and the burger's IBAN is known
- **WHEN** the payment signal is generated
- **THEN** a `DwangsomUitbetaling` SHALL be created with `bedrag`, `rekeninghouderNaam`, `iban`, `referentie` (zaakId + ingebrekestelling-date), `wettelijkeGrondslag` = "AWB 4:17 lid 2", `betaaldatumUiterlijk` = ingebrekestelling-date + 28 days, and `status` = `voorbereid`
- **AND** a `dwangsom-payment-signal` event SHALL be emitted to openconnector with the full metadata payload

#### Scenario: Missing or invalid IBAN blocks the signal

- **GIVEN** the burger's IBAN is missing or malformed
- **WHEN** `prepareBetaling` runs
- **THEN** the system SHALL raise an error and SHALL NOT emit a payment signal

#### Scenario: Payment confirmation callback updates status and notifies burger

- **GIVEN** the ERP sends a payment-confirmation callback via openconnector
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,18 @@ advance a case's status.
- AND the card MUST move to the "In behandeling" column
- AND if the update fails (e.g., permission denied), the card MUST return to its original column

#### Scenario DASH-V1-006d: Keyboard-only status transition (NEW)
#### Scenario DASH-V1-006d: Click on case card navigates to detail
- GIVEN a case card is visible on the board
- WHEN the user clicks the card (not drags it)
- THEN the system MUST navigate to the case detail view for that case

#### Scenario DASH-V1-006e: Empty column
- GIVEN no cases are currently in the "Besluitvorming" status
- WHEN the user views the Workflow Board
- THEN the "Besluitvorming" column MUST still be displayed with count "0"
- AND the column body MUST show an empty state placeholder

#### Scenario DASH-V1-006f: Keyboard-only status transition (NEW)

- GIVEN case "2026-0042" is in the "Ontvangen" column and the user is navigating with only a
keyboard (no mouse/touch)
Expand All @@ -46,7 +57,7 @@ advance a case's status.
- AND the card's existing "open case detail" keyboard activation (Enter/Space on the card body)
MUST remain unaffected by the new control

#### Scenario DASH-V1-006e: Drag path unchanged (NEW)
#### Scenario DASH-V1-006g: Drag path unchanged (NEW)

- GIVEN a mouse/touch user
- WHEN they drag a card between columns as in Scenario DASH-V1-006c
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
schema: spec-driven
created: 2026-09-03
skip_specs: true
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
<!-- SPDX-License-Identifier: EUPL-1.2 -->
# besluitvorming-workflow delta

**Already applied.** These four edits were written into
`openspec/specs/besluitvorming-workflow/spec.md` by the implementation commit
itself, af1ff19a: REQ-BVW-002 and REQ-BVW-003 are gone from the main spec, and
scenarios REQ-BVW-001-B and REQ-BVW-008-B no longer name a `parafeerroute` or a
`parafeeractie`. This file is the reasoning behind those edits, kept as the
record. It is written as prose rather than as replacement requirement blocks, so
the change archives with `--skip-specs`: re-applying it would rewrite four
requirements with their own rationale.


## REMOVED Requirements

### Removed Requirement: REQ-BVW-002 Parafering chain MUST activate automatically when a voorstel is submitted
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@ conclusion arrives; this is what a consumer consults when it did not.
surface of its own; pinned by ContractDecisionDelegationReadTest and end to end
through the real engine by tests/Unit/Flow/RequestDecisionHeartbeatRecoveryTest.php.

## MODIFIED Requirements

### Requirement: A decision step advances on its decision, not on a signal

`dossiq.requestDecision` SHALL raise exactly one decision on its first pass,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
## MODIFIED Requirements
## REMOVED Requirements

### Requirement: The tab strip fits one row and reads in work order (REQ-CDV-16)

**Reason:** the case page went from nine tabs to six, so the requirement this
replaces no longer describes the strip. Its three scenarios assert a five-tab
work order with Sub-cases, Locations, Appointments and Decisions trailing it,
and a tab hidden when its collection is empty. All three are false after the
consolidation: those four panels are now sections inside Related and Objects
and locations, and a section holding nothing renders a line of text inside a
tab the handler opened on purpose, which is why `visibleIf` is no longer
wanted. Carrying the scenarios forward would leave the spec asserting a strip
that is not on the page. Written as a removal plus an addition rather than a
rewrite in place so the three dropped scenarios are dropped on the record.

**Migration:** none. The requirement below carries the same REQ-CDV-16 id and
covers the same ground for the six-tab strip.

## ADDED Requirements

### Requirement: Six tabs hold every panel of the case (REQ-CDV-16)

You reach every panel of the case from one row of six tabs. The `case-panels`
widget on `CaseDetail` SHALL list exactly six tabs, in the order Data
(`case-core`), Documents (`case-documents-panel`), People
Expand Down
Loading
Loading