Skip to content

The flow projection emitted UUIDs into a node that resolves names - #1707

Merged
rubenvdlinde merged 6 commits into
developmentfrom
feat/workflow-definitions-to-flows
Sep 3, 2026
Merged

The flow projection emitted UUIDs into a node that resolves names#1707
rubenvdlinde merged 6 commits into
developmentfrom
feat/workflow-definitions-to-flows

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Found while starting phase 2 of workflow-definitions-to-flow, by reading what a live template stores rather than what the seed files do.

The contract, stated twice

DossiqTxSetStatusNode's own description:

Move the case to a status of its case type, named rather than referenced by id.

…because a statusType uuid is minted per installation, so a shipped flow carrying one is portable nowhere. The migrator's docblock says the same thing.

The data says otherwise

The seed files store statuses as names — which is what the migrator was written against. The stored objects do not: the seeder resolves those names to statusType uuids on import. So on any live instance, fromStatus, toStatus and step.status are all uuids.

Measured on a running instance: 9 of 9 step statuses and every transition endpoint were uuids, and the two sets overlapped completely.

So every flow this projection has produced carries uuids into a node that resolves names. Those flows could not have moved a case.

Why nothing caught it

The projections arrive disabled, deliberately, so the old runtime keeps driving cases. A disabled flow never runs, and changed-surfaces.spec.ts asserts only that the projected flows exist and are switched off.

The defect was unobservable by construction. That is precisely why phase 2 had to look at the stored data before adding anything to the projection.

The fix

  • statusNames() indexes statusType uuid → name, once per pass rather than per template.
  • statusName() resolves one reference and passes an unresolvable value through, so a seed-shaped template whose endpoints are already names projects exactly as before.

Both cases are tested. The uuid test fails against the old code; the seed-shape test guards the passthrough so the fix cannot break the case it was built to preserve.

Verified live

Re-projected all 10 flows on a running instance and read them back:

sample statuses: ['… Ontvangen', '… In behandeling', '… Afgehandeld']
VERDICT: NAMES — fixed

2796 PHPUnit tests, phpcs clean.

Also in this branch

design.md records the answer to the phase-2 blocker: FlowTimerService::arm() fits a dossiq step field for field, but has no production caller anywhere in OpenRegister — every ->arm( is in its own unit test. So a per-step SLA cannot yet be carried by flow JSON, and retiring workflowTemplate must wait for the engine to wire its own timers.

🤖 Generated with Claude Code

Conduction Release Bot and others added 4 commits September 2, 2026 15:31
…s one blocker

Task 4 of workflow-definitions-to-flow was deferred because the definition
carries per-step SLAs, checklists and roles the projection does not. That
reason was about the MIGRATOR, not the engine, and it is now partly out of
date: graphOf() reads transitions and never touches steps, while OpenRegister
has since gained homes for most of what a step holds.

This records the mapping field by field against what was actually read in
OpenRegister's source. Two corrections to the obvious guess, both of which
would have shipped a projection that looked complete:

  - the config key is formRequireChecklist, not requireChecklist; the short
    name would validate as unknown and the checklist would simply not appear
  - dueAt is ADVISORY by its own help text, so projecting a step SLA onto it
    would show a date and escalate nothing

And one genuine blocker, recorded as OPEN rather than assumed away. A business
timer is a runtime record armed against a subject, not a field in a flow
document, and UserTaskNode's config keys name no escalation ladder. So how a
projection DECLARES an SLA and its ladder is unanswered, and writing the
projection before it is answered would drop the escalation while reporting
success — the exact failure this change exists to avoid.
…thing arms it"

`FlowTimerService::arm()` takes exactly what a dossiq step holds — sla
{value,unit}, calendar, ladder, escalationRules, purpose, legalEffect,
onExpiry, extensionMax — bound to a subject with optional runUuid/nodeId.
Field for field, the model fits.

But it has NO production caller. Measured across OpenRegister's whole tree:
every `->arm(` lives in FlowTimerServiceTest. Not UserTaskNode, not the flow
engine, not a listener, and no DI registration in AppInfo. The capability is
built, tested, and unwired.

So a per-step SLA cannot be carried by writing flow JSON: nothing reads an SLA
off a node and arms a timer. Either OpenRegister wires UserTaskNode to arm on
open and cancel on completion (the right home, ADR-065), or dossiq arms it from
a listener — which puts timer orchestration back in a leaf app, the thing this
change exists to stop.

Consequence recorded in the sequencing: the projection can carry every other
field, and retiring workflowTemplate CANNOT complete until the timer is wired
upstream. Retiring the only home of a per-step SLA while nothing else can hold
it is the exact forgetting this document opens by warning about.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…olves names

Found while starting phase 2, by reading what a LIVE template actually stores
rather than what the seed files do.

`DossiqTxSetStatusNode` states its contract in its own description: it moves a
case to a status of its case type "named rather than referenced by id", because
a statusType uuid is minted per installation and a flow carrying one is
portable nowhere. The migrator's docblock says the same.

The data says otherwise. The SEED files store statuses as names, which is what
this migrator was written against. The STORED objects do not — the seeder
resolves those names to statusType uuids on import — so on any live instance
`fromStatus`, `toStatus` and `step.status` are all uuids. Measured on a running
instance: 9 of 9 step statuses and every transition endpoint.

So every flow the projection has produced carries uuids into a node that
resolves names. Those flows could not have moved a case.

WHY NOTHING CAUGHT IT: the projections arrive DISABLED, deliberately, so the
old runtime keeps driving cases. A disabled flow never runs, and the e2e
asserts only that the projected flows exist and are switched off. The defect
was unobservable by construction — which is exactly why phase 2 had to look at
the data before adding to it.

`statusNames()` indexes statusType uuid => name once per pass; `statusName()`
resolves one reference and PASSES AN UNRESOLVABLE VALUE THROUGH, so a
seed-shaped template whose endpoints are already names projects exactly as
before. Both cases are tested, and the uuid test fails against the old code.

Verified live: re-projected all 10 flows and read them back — every
`dossiq.setStatus` node now carries a name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The coverage ratchet was right: `statusNames()` added branches nothing
exercised — an empty index, and a statusType row with no name.

Both are behaviours worth pinning rather than coverage padding:

  - an empty index must PASS REFERENCES THROUGH, because that is the
    documented degradation and it is what keeps a seed-shaped template
    projecting when statusTypes cannot be read;
  - a row with no name must be SKIPPED rather than indexed as empty. Mapping a
    uuid to '' is worse than leaving it unresolved: the node would be handed
    nothing to resolve instead of something it can refuse.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/dossiq @ 0c27612

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
check-schema-l10n
check-l10n-js
composer ✅ 106/106
npm ✅ 540/540
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 06:01 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/dossiq @ 6eb3745

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
check-schema-l10n
check-l10n-js
composer ✅ 106/106
npm ✅ 540/540
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 06:06 UTC

Download the full PDF report from the workflow artifacts.

The ratchet's numbers, read rather than guessed at: 28 statements added,
21 covered, 7 not. The seven were the two paths where the status resolution
gives up — an unconfigured statusType schema, and a read that throws.

Both are worth pinning for the same reason: losing the resolution costs
readability in the projected flow, while losing the PROJECTION costs the
migration. Each test asserts the flow is still written, not merely that
nothing exploded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/dossiq @ 9b43b55

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
check-schema-l10n
check-l10n-js
composer ✅ 106/106
npm ✅ 540/540
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 06:59 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit 00bb699 into development Sep 3, 2026
49 checks passed
@rubenvdlinde
rubenvdlinde deleted the feat/workflow-definitions-to-flows branch September 3, 2026 06:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant