Skip to content

chore(openspec): rewrite publication-clearance-via-anonymise as event-driven#149

Open
rjzondervan wants to merge 2 commits into
developmentfrom
chore/openspec/rewrite-publication-clearance-via-anonymise
Open

chore(openspec): rewrite publication-clearance-via-anonymise as event-driven#149
rjzondervan wants to merge 2 commits into
developmentfrom
chore/openspec/rewrite-publication-clearance-via-anonymise

Conversation

@rjzondervan
Copy link
Copy Markdown
Member

Summary

Rewrites openspec/changes/publication-clearance-via-anonymise to use the event-driven EntityRelationDecisionUpdatedEvent from OpenRegister PR #1503's amend as the consent-creation trigger, instead of the originally-proposed unredactedEntities[] field on the anonymise endpoint.

Why the rewrite was needed. Two flaws in the original proposal:

  1. Two decision channels for the same operator decision. Wave 1.3 (entity-relation-grondslagen) already gives operators a per-entity primitive via skipAnonymization on the EntityRelation row. Operationally "publish this entity unredacted" IS skipAnonymization: true; a parallel unredactedEntities[] array just duplicated the surface.
  2. 422-on-every-first-anonymise for WOO-pending entities. Putting consent creation at anonymise time meant every entity needing the standard 28-day WOO workflow would 422 on first try, leaving operators unable to run anonymise without first running anonymise. The 28-day clock has to start at decision time, not at anonymise time.

New design

  • The trigger is OCA\OpenRegister\Event\EntityRelationDecisionUpdatedEvent (added in OR PR #1503's amend). DocuDesk's listener catches skipAnonymization: false → true and calls ConsentService::createConsentRequest() synchronously.
  • PolicyMatchService inside createConsentRequest handles all three outcomes — standing-consent match auto-resolves to consent_given; no-match starts the 28-day clock at decision time with pending; prohibition match throws PolicyRejectedException, which the listener handles by reversing the PATCH and dispatching a Nextcloud notification to the acting user.
  • The anonymise endpoint shape is unchanged. It gains a defensive runtime check that returns 422 only when a skip-marked relation has a pending consent whose objection window is still open. Closed-window and auto-resolved cases proceed normally — no more 422-on-every-first-anonymise.
  • createConsentRequest becomes idempotent on (documentId, entityKey) for scope: "document" records, so multiple relations for the same entity (multiple text positions of one person) collapse to one consent record.
  • Two new typed exceptions: PolicyRejectedException (consent service → listener, signals prohibition) and BlockingConsentException (service → controller, drives the 422 mapping).

Files

Five spec files rewritten:

  • proposal.md — new framing; out-of-scope list updated.
  • design.md — 7 decision sections (event-driven trigger; idempotency; three PolicyMatchService outcomes; PATCH reversal + notification for prohibition matches; defensive anonymise-time check with blocking-state truth table; widget client-side prohibition pre-check; notification dispatch stays stubbed).
  • tasks.md — restructured around the new architecture.
  • specs/anonymization/spec.md — slimmed to just the defensive runtime check with the full blocking-state truth table.
  • specs/consent-management/spec.md — six new requirements covering listener subscription, idempotency, the typed exception, PATCH reversal, generic-failure handling, notification stub.

Dependencies

Test plan

  • openspec validate publication-clearance-via-anonymise clean.
  • Spec text cross-references the correct OR event class + DD service names (EntityRelationDecisionUpdatedEvent, EntityRelationMapper::updateDecisionMetadata, PolicyMatchService::match, ConsentService::createConsentRequest).
  • Architect / reviewer confirms the event-driven trigger story addresses the 422-on-every-first-anonymise concern and the duplicate-decision-channel concern from the original review.

…-driven

The original spec proposed extending the anonymise endpoint with a
parallel `unredactedEntities[]` field as the trigger for consent
creation. Two flaws on review:

  1. Two decision channels for the same operator decision. Wave 1.3
     (entity-relation-grondslagen) already gives operators a per-entity
     primitive via skipAnonymization on the EntityRelation row.
     Operationally "publish this entity unredacted" IS skipAnonymization=true;
     a parallel unredactedEntities[] just duplicated the surface.
  2. 422-on-every-first-anonymise for WOO-pending entities. Putting
     consent creation at anonymise time meant every entity needing the
     standard 28-day WOO workflow would 422 on first try, leaving
     operators unable to run anonymise without first running anonymise.
     The 28-day clock has to start at decision time, not at anonymise
     time.

This rewrite collapses the two channels and moves the trigger to the
new EntityRelationDecisionUpdatedEvent (OR — added in PR #1503's
amend). DocuDesk's listener catches skipAnonymization false→true and
calls createConsentRequest synchronously. PolicyMatchService inside
createConsentRequest handles all three outcomes: standing-consent
match → auto-resolved consent_given; no match → pending +
objectionDeadline computed now (28-day clock starts at decision
time); prohibition match → PolicyRejectedException → listener
reverses the PATCH + dispatches a Nextcloud notification.

The anonymise endpoint shape is unchanged. It gains a defensive
runtime check: 422 only when a skip-marked relation has a pending
consent record whose objection window is still open. Closed-window
and auto-resolved cases proceed normally.

createConsentRequest becomes idempotent on (documentId, entityKey)
for scope=document records, so multiple relations for the same
entity (multiple text positions of "Jan Janssen") collapse to one
consent record.

Five spec files rewritten:

  - proposal.md: new framing; out-of-scope list updated.
  - design.md: 7 decision sections (event-driven trigger; idempotency;
    three PolicyMatchService outcomes; PATCH reversal + notification
    for prohibition matches; defensive anonymise-time check with
    blocking-state truth table; widget client-side prohibition
    pre-check; notification dispatch stubbed).
  - tasks.md: restructured around the new architecture (event listener;
    new PolicyRejectedException + BlockingConsentException; ConsentService
    idempotency; defensive check; widget pre-check; tests; docs).
  - specs/anonymization/spec.md: slimmed to just the defensive runtime
    check with the full blocking-state truth table.
  - specs/consent-management/spec.md: six new requirements covering
    listener subscription, idempotency, the typed exception, PATCH
    reversal, generic-failure handling, notification stub.

Hard deps: OR PR #1503 (the event class) + DD PR #147
(entity-publication-policies — PolicyMatchService). No code change
yet — that lands when both deps merge.

openspec validate clean.
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/docudesk @ 45e590d

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 108/108
npm
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-05-15 09:26 UTC

Download the full PDF report from the workflow artifacts.

…/rewrite-publication-clearance-via-anonymise

# Conflicts:
#	openspec/changes/publication-clearance-via-anonymise/design.md
#	openspec/changes/publication-clearance-via-anonymise/proposal.md
#	openspec/changes/publication-clearance-via-anonymise/tasks.md
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/docudesk @ cf88439

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 108/108
npm ✅ 529/529
PHPUnit
Newman ⏭️
Playwright ⏭️

Coverage: 0% (0/10 statements)


Quality workflow — 2026-05-19 03:23 UTC

Download the full PDF report from the workflow artifacts.

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.

2 participants