WS-6 v1.3-delta — Closure docs-PR #12

Merged
bert.hausmans merged 4 commits from docs/ws-6-v1.3-delta-closure into main 2026-05-08 10:30:20 +02:00
3 changed files with 284 additions and 17 deletions

View File

@@ -451,28 +451,136 @@ te runnen. 1 regel, geen scope-impact.
### FORM-05 — Smart identity-match on public submission values
**Stub-status (S3a PR 2, 2026-04-23):** Public event_registration
submissions landen al met `identity_match_status='pending'` via de
bestaande `TriggerPersonIdentityMatchOnFormSubmit` listener. De portal
`IdentityMatchBanner` leest dit veld en toont de juiste copy. Contract
ligt vast in `tests/Feature/FormBuilder/Listeners/TriggerPersonIdentityMatchOnFormSubmitTest`.
**Stub-status (S3a PR 2, 2026-04-23; updated post-D2 2026-05-08):**
Post-D2 (RFC §Q1 v1.3 add 1) writes the initial
`identity_match_status='pending'` in `ApplyBindingsOnFormSubmit::handle`
inside the inner transaction. The queued
`TriggerPersonIdentityMatchOnFormSubmit` then writes the final
`'matched'` / `'pending'` / `'none'` status. FORM-05's
`detectMatchesByValues` extension targets the queued listener's path —
adds a value-based matcher branch when `$person->user_id === null` AND
`$matches->isEmpty()` (currently writes `'none'`), giving public
submitters a "we may have found you" path without first creating a
Person. De portal `IdentityMatchBanner` leest dit veld en toont de
juiste copy. Contract ligt vast in
`tests/Feature/FormBuilder/Listeners/TriggerPersonIdentityMatchOnFormSubmitTest`.
**Resterend werk (de eigenlijke FORM-05):** public form submissions
(subject_type=null) krijgen momenteel *altijd* 'pending' omdat er nog
geen Person bestaat om tegen te matchen. Breid uit met:
**Resterend werk (de eigenlijke FORM-05):** post-D2 (PR #11 `23a5696`)
schrijft de queued `TriggerPersonIdentityMatchOnFormSubmit` voor
self-registered public submissions (`registration_source='self'`)
standaard `'none'` zodra de exact-email-match in
`PersonIdentityService::detectMatches` leeg terugkomt — het bestaande
fuzzy-name fallback-pad wordt expliciet overgeslagen voor self-registered
Persons (zie `PersonIdentityService::detectMatches` regel 117119, *"skip
fuzzy matching for self-registered persons — they provided their own
email, so that's the canonical identity"*). Public submitters die een
typo in hun email maken of een ander email-account gebruiken dan eerder
bij de organisatie geregistreerd, krijgen daardoor geen "we may have
found you" signaal en moeten organiser-side handmatig gereconcilieerd
worden.
- Nieuwe methode op PersonIdentityService:
`detectMatchesByValues(array $values, string $organisationId): MatchResult`
- Een extra tak in `TriggerPersonIdentityMatchOnFormSubmit::resolveStatus`
die voor public submissions de values uit `FormSubmission->values`
extraheert (email / first_name / last_name via de schema binding),
deze methode aanroept, en 'matched' / 'pending' / 'none' schrijft.
Breid uit met:
- Nieuwe methode op `PersonIdentityService`:
```php
public function detectMatchesByValues(
array $values,
string $organisationId,
): Collection {
// Multi-signal match: email + first_name + last_name + DOB +
// optionele custom-field whitelist, gewogen naar
// betrouwbaarheid van de bron-velden. Werkt los van
// $person->registration_source, dus self-registered submissions
// krijgen óók fuzzy-name + DOB candidates terug.
}
```
- Een extra match-arm in de status-derivation van
`TriggerPersonIdentityMatchOnFormSubmit::handle()`. D2 inlinete de
status-derivation in `handle()` zelf — er is geen aparte
`resolveStatus` methode meer. De huidige logica:
```php
$matches = $this->identityService->detectMatches($person);
$status = match (true) {
$person->user_id !== null => 'matched',
$matches->isNotEmpty() => 'pending',
default => 'none',
};
```
wordt uitgebreid met een value-based fallback wanneer de Person geen
`user_id` heeft én de exact-email matcher leeg terugkomt:
```php
$matches = $this->identityService->detectMatches($person);
$valueMatches = $matches->isEmpty() && $person->user_id === null
? $this->identityService->detectMatchesByValues(
$this->extractFormValuesForMatching($submission),
$submission->organisation_id,
)
: collect();
$status = match (true) {
$person->user_id !== null => 'matched',
$matches->isNotEmpty() || $valueMatches->isNotEmpty() => 'pending',
default => 'none',
};
```
De `extractFormValuesForMatching` helper trekt de relevante velden
(email / first_name / last_name / date_of_birth + eventuele
whitelisted custom-fields) uit `FormSubmission->values` via de
schema-binding metadata.
Zo krijgt de portal-UX een betekenisvol signaal in plaats van een
constante 'pending'.
`'none'` voor elke public submitter die niet exact-email matched.
Prioriteit: Medium. Kan gebundeld worden met de organizer
`person_identity_matches` UI (ook nog een frontend gap).
**Prioriteit:** Medium. Bundel met de organizer
`person_identity_matches` UI (frontend gap) én met
`FRONTEND-ECHO-IDENTITY-MATCH-SUBSCRIPTION` (de portal banner update zo
dat hij de eindstatus real-time ontvangt).
---
### FRONTEND-ECHO-IDENTITY-MATCH-SUBSCRIPTION
**Aanleiding:** WS-6 v1.3-delta D2 (PR #11 `23a5696`, 2026-05-08) introduceerde
de `FormSubmissionIdentityMatchResolved` broadcast event class en bijbehorende
`submission.{id}` private channel. De backend dispatcht het event maar de
frontend portal IdentityMatchBanner subscribet nog niet — de banner refresht
momenteel via TanStack Query refetch-on-window-focus, wat een interim-pad is
maar niet de ontworpen UX.
**Wat:**
- Pinia store-extension OF inline `Echo.private('submission.${submissionId}')`
subscription in de IdentityMatchBanner component
- Op event-receipt: invalidate de TanStack Query cache key voor de submission
resource zodat de banner copy automatisch update naar de finale staat
('matched' / 'pending' / 'none')
- Authorization wordt al afgedwongen door `routes/channels.php` callback
(submitter-only voor nu — zie ook TECH-CHANNEL-AUTH-ORG-ADMIN)
- E2E test: simuleer een form submission, verify dat de banner copy in real-time
van "we're checking matches…" naar de finale status flipt zonder reload
**Vereisten:**
- Laravel Echo client setup in `apps/app/` (verifieren: bestaat al voor andere
channels of moet nog worden toegevoegd?)
- Soketi server in development docker-compose en in productie deployment
(verifieren: WS-7 noemt Soketi in stack maar is broadcast wiring al getest?)
**Trigger-conditie:** Naar voren halen wanneer (a) de UX van TanStack
refetch-on-focus klagende klanten oplevert, OF (b) de eerstvolgende form-builder
frontend sprint (RFC-FORM-BUILDER-UI / S3b) gepland wordt — neem het mee in
diezelfde sprint zodat alle Echo-wiring tegelijk landt.
**Estimate:** 0.51 dag (afhankelijk van of Echo client setup nog moet).
**Refs:** `dev-docs/RFC-WS-6.md` v1.3.1 §Q1 v1.3 add 2, `dev-docs/ARCH-BINDINGS.md`
v1.2 §5.3, `apps/app/src/...` (portal IdentityMatchBanner location to be
identified during implementation), `routes/channels.php`.
---
@@ -1019,6 +1127,50 @@ ARCH-discussie en RFC.
---
### HARD-DEADLINE-QUERY-TIMEOUT
**Aanleiding:** WS-6 v1.3-delta D2 (PR #11 `23a5696`, 2026-05-08) introduceerde
`FormBindingApplicator::withDeadline(int $seconds)` — een **soft** deadline via
post-call microtime check. Deze beschermt tegen "applicator runs slow over many
bindings" (de long tail) maar **kan niet onderbreken** tijdens een hangende
MySQL-query of een lock-for-update wait die langer duurt dan de deadline.
Voor enterprise SaaS op piekmomenten (vrijwilligersregistratie-windows,
festival check-in flows) is een hangende query een reëel risico — PHP-FPM
worker blokkeert, queue depth loopt op, downstream impact.
**Wat:**
Drie potentiële uitvoeringspaden, te kiezen tijdens RFC-fase:
1. **MySQL connection-level timeouts**: zet `read_timeout` en `write_timeout`
op de Eloquent connection config voor de inner-transaction context.
Eenvoudig, maar globale impact (alle queries via die connection krijgen de
timeout).
2. **Per-query `MAX_EXECUTION_TIME` hints**: MySQL 5.7+ ondersteunt
`/*+ MAX_EXECUTION_TIME(N) */` query hints voor SELECT statements. Niet
ondersteund door MariaDB. Gericht maar database-vendor-specifiek.
3. **`pcntl_alarm` + signal handler**: hard interrupt via SIGALRM in de queue
worker context (CLI sapi). Werkt niet in HTTP context (PHP-FPM disablet
pcntl in de meeste configuraties). Combineren met optie 1 voor HTTP context.
**Trigger-conditie:** Naar voren halen wanneer (a) eerste productie-incident
waar `FormBindingApplicatorTimeoutException` getrigerd wordt en de soft
deadline blijkt onvoldoende (post-mortem toont een hangende query), OF (b)
SLO-vereisten van een enterprise klant strikte response-time-garanties
opleggen die de soft deadline niet kan halen.
**Estimate:** 2-3 dagen (RFC + implementatie + tests + connection config voor
alle environments).
**Refs:** `dev-docs/RFC-WS-6.md` v1.3.1 §Q1 v1.3 add 4, `dev-docs/ARCH-BINDINGS.md`
v1.2 §5.3, `api/app/FormBuilder/Bindings/FormBindingApplicator.php` (soft
deadline implementation).
---
### ~~TECH-02 — scopeForFestival helper op Event model~~ ✅ OPGELOST
---
@@ -1040,6 +1192,7 @@ ARCH-discussie en RFC.
- ~~**WS-TOOLING-001**: Claude Code deterministic guard-rail layer (5 hooks, `crewli-reviewer` subagent op Opus 4.7, 3 slash commands `/sprint-status` `/review-multitenancy` `/sync-docs`, `dev-docs/CLAUDE_CODE_TOOLING.md`). 8/8 smoke tests groen, live integratie geverifieerd. Merge `ad36c06` op 2026-05-05. Follow-ups: TECH-HOOK-001, TECH-CMD-001, TECH-STYLE-001.~~ ✅
- ~~**TECH-DOCS-APPS-PORTAL-PURGE**: per-file DELETE/REWRITE/KEEP_AND_PURGE matrix uitgevoerd op alle 9 docs uit de oorspronkelijke entry, plus de `post-edit-eslint.sh` hook (out-of-scope vondst uit Phase A). Vijf obsolete docs verwijderd (`.cursor/instructions.md`, `.cursor/ARCHITECTURE.md`, `dev-docs/MASTER_PROMPT_CC.md`, `dev-docs/MASTER_PROMPT_CURSOR.md`, `dev-docs/dev-guide.md` — totaal ~80 KB). Drie herschreven (`SETUP.md`, `101_vue.mdc`, hook-script). Twee chirurgisch gepurgeerd (`102_multi_tenancy.mdc`, `CLAUDE_CODE_TOOLING.md`). Externe verwijzingen in README.md, CLAUDE_DESKTOP_SETUP.md, ARCH-CONSOLIDATION-2026-04.md en VIBE_CODING_CHECKLIST.md mee bijgewerkt. WS-3 PR-C op 2026-05-06. Single SPA, single cookie, single deploy host. WS-3 compleet.~~ ✅
- ~~**WS-7 Observability — closure (mei 2026)**: 4 PRs gemerged op `feat/ws-7-observability` (infra `5f6fc07`, backend SDK `bdb89a2..0379016`, frontend SDK `bc47783..5c42f27`, docs `754222f..e9da01f`). 1551 backend + 252 frontend tests groen. Acceptance criteria 1-14 voldaan; observability volledig operationeel op `monitoring.hausdesign.nl`. Implementation criteria 3, 4, 5, 6, 8, 11, 12, 13, 14 via PRs; operationele criteria 1, 2, 7, 9, 10 via deploy-checklist (DNS, TLS, superuser+2FA, prod DSNs, email-alerting, retention 90d, cron backup). Architecturale patronen vastgelegd in `dev-docs/ARCH-OBSERVABILITY.md` (730 regels) + 2 runbooks (`observability-triage.md`, `observability-erasure.md`). Twee GlitchTip projecten (`crewli-api` + `crewli-app`), één DSN per project, runtime context-split via `actor_scope` tag. Patronen: explicit > implicit listener registration, default-in-listener / override-in-middleware voor binary tags, tenant resolution chain (route-param → portal-token → super_admin platform → user fallback). Volgsporen: OBS-1, OBS-4, OBS-6, OBS-7, OBS-9 (zie "Observability follow-ups" sectie hieronder).~~ ✅
- ~~**WS-6 v1.3-delta — closure (mei 2026)**: Architecturele review-sessie 2026-05-07 identificeerde vijf verfijningen op RFC-WS-6 v1.2 (Q1 listener queueing, Q2 invariant cleanup, Q3 failure-UX additions, plus §19 BACKLOG-pointer). v1.3 amendement gecommit (`845b6e6`, 2026-05-07); v1.3.1 drift closure (`b255879`, 2026-05-08) sloot code-vs-docs gaten pre-implementation. Implementatie geland als D1 + D2: **D1** (PR #10 `c6f4d1b`) leverde de data-laag — `failure_response_code` kolom op `form_submissions`, abstract `FormBindingApplicatorException` hiërarchie + 4 reason-coded subclasses (`FormBindingSchemaConfigException`, `FormBindingInfraException`, `FormBindingApplicatorTimeoutException`, `FormBindingDataIntegrityException`), `IdentityMatchInvariantViolation` sibling, `FormBindingExceptionClassifier` helper, `FormSubmissionIdentityMatchResolved` broadcast event class, `FormFieldBindingMergeStrategy::validForTargetType` matrix method, cast + factory state. **D2** (PR #11 `23a5696`) wired alle building blocks in de listener-chain — `ApplyBindings` initial `pending` write + deadline wrapper + classifier in catch; `TriggerPersonIdentityMatch` queued + gating-invariant + invariant throw + broadcast dispatch; `routes/channels.php` + bootstrap routing (NIEUWE broadcast wiring, submitter-only auth); gating-invariant op `SyncTagPicker`; `AppServiceProvider::boot` v1.3 layout; `FormFailureRetryService::recordFailure` classifier + apply_completed_at symmetrie-fix; `apply_deadline_seconds` config key (default 5). Tests: pre-WS-6 baseline 1208 → pre-D1 1551 → post-D2 1621. 0 Larastan errors. Phase F (`ConditionalRequirement(public_token)` wrapper drop) was no-op — change had silently landed pre-D2. **Open follow-ups:** `TECH-CHANNEL-AUTH-ORG-ADMIN` (extend `submission.{id}` channel auth to org admins na Spatie Permission helper-audit); GlitchTip alert rule op `apply_status=failed AND form_schema.has_public_token=true` (operationele taak in GlitchTip web-UI op `monitoring.hausdesign.nl`; runbook procedure in `dev-docs/runbooks/observability-triage.md` §7); frontend Echo subscription voor `FormSubmissionIdentityMatchResolved` (separate frontend follow-up, out of WS-6 scope, backend-infra ready). `PARTIAL-BINDING-SUCCESS` en `FORM-SCHEMA-DRIFT-DETECTION` blijven open conform v1.3 amendement (trigger-condities nog niet gevuurd). Closure docs-PR: RFC-WS-6.md v1.3.1 implementation-status marker + §10 closure entry, ARCH-BINDINGS.md v1.2 onveranderd, runbook §7 toegevoegd.~~ ✅
---

View File

@@ -4,6 +4,7 @@
- **State:** Authoritative for sessions 1, 2, 3 of WS-6
- **Frozen:** 2026-04-25 (v1.0); refined post-session-2 cleanup as v1.1, then v1.2 (sessie 3a.5), then v1.3 (architectural review 2026-05-07); v1.3.1 (2026-05-08) — code-vs-docs drift closure pre-D1 implementation — see §10
- **Implementation status:** v1.3.1 fully implemented in main as of 2026-05-08 (D1: PR #10 `c6f4d1b`, D2: PR #11 `23a5696`)
- **Version:** v1.3.1
- **Owner:** Bert Hausmans
- **Origin:** Architectural session 2026-04-25 (Claude Chat) — 13 design decisions, 4 refinements, 3 observations
@@ -859,3 +860,4 @@ WS-7 sessie 1.
- **§Q3 — Strict-fail vs compatibility.** Spine confirmed unchanged. Three failure-UX additions: GlitchTip alert rule on `FormBindingApplicatorException` for production public flows; custom exception hierarchy (`FormBindingSchemaConfigException` / `FormBindingInfraException` / `FormBindingDataIntegrityException`) + `failure_response_code` column on `form_submissions` + `error_code` in HTTP response body; "all-or-nothing per pass" gets explicit BACKLOG entry `PARTIAL-BINDING-SUCCESS`. Fourth addition: schema-drift detection as separate BACKLOG entry `FORM-SCHEMA-DRIFT-DETECTION` (not v1.0 scope).
- Companion: `ARCH-BINDINGS.md` advances to v1.1 (twelve section edits per the v1.3 amendment companion table); `BACKLOG.md` adds `PARTIAL-BINDING-SUCCESS` and `FORM-SCHEMA-DRIFT-DETECTION` under Form Builder backlog. RFC-WS-6 v1.3, ARCH-BINDINGS v1.1, and the BACKLOG additions land in the same commit.
- 2026-05-08 — v1.3.1 — Pre-D1-implementation drift closure. (1) Updated apply_status enumerations throughout §3 to include the PARTIAL case (which exists in code per `BindingPassResult::applyStatus()` and was not anticipated by the v1.3 amendment author). (2) ARCH-BINDINGS §5.6 received a PARTIAL-handling clarification: gate treats PARTIAL identically to FAILED, deferring granular partial-success to BACKLOG `PARTIAL-BINDING-SUCCESS`. (3) ARCH-BINDINGS §7.1 status-columns table extended with `apply_completed_at` row + cross-reference to the D2 retry-service symmetry fix. No spine changes; no behaviour changes; documentation truth-in-naming. Companion: ARCH-BINDINGS.md advances to v1.2.
- 2026-05-08 — **v1.3-delta closure** — RFC v1.3.1 fully implemented in main. **D1** (PR #10 `c6f4d1b`, 2026-05-08) delivered the data-layer prerequisites: `failure_response_code` column on `form_submissions`, abstract `FormBindingApplicatorException` hierarchy with 4 reason-coded subclasses (`FormBindingSchemaConfigException`, `FormBindingInfraException`, `FormBindingApplicatorTimeoutException`, `FormBindingDataIntegrityException`), `IdentityMatchInvariantViolation` sibling DomainException, `FormBindingExceptionClassifier` helper, `FormSubmissionIdentityMatchResolved` broadcast event class, `FormFieldBindingMergeStrategy::validForTargetType` matrix method, plus cast + factory state. **D2** (PR #11 `23a5696`, 2026-05-08) wired the building blocks into the listener chain: `ApplyBindingsOnFormSubmit` writes initial `identity_match_status='pending'`, uses the deadline wrapper, and consumes the classifier in its outer-transaction catch block; `TriggerPersonIdentityMatchOnFormSubmit` becomes queued with the gating-invariant first statement, the strict invariant throw, and broadcast dispatch; `routes/channels.php` introduces broadcasting infrastructure (NEW wiring) with submitter-only authorization (org-admin extension tracked as BACKLOG `TECH-CHANNEL-AUTH-ORG-ADMIN`); queued listeners gain the `apply_status=COMPLETED` first-statement gate; `FormFailureRetryService::recordFailure` consumes the classifier and writes `apply_completed_at` for symmetry with the listener; `apply_deadline_seconds` config key (default 5) added; six existing tests adapted to the v1.3 layout. Test counts: pre-D1 baseline 1551 → post-D2 1621 (+70). 0 Larastan errors. The remaining v1.3 add (Q3 v1.3 add 1 — GlitchTip alert rule on `apply_status=failed AND form_schema.has_public_token=true`) is an operational task, configured in the GlitchTip web UI on `monitoring.hausdesign.nl` outside the code lifecycle; runbook procedure documented in `dev-docs/runbooks/observability-triage.md` §7. Frontend Echo subscription for `FormSubmissionIdentityMatchResolved` is a separate frontend follow-up, out of WS-6 scope.

View File

@@ -268,3 +268,115 @@ Voor incidents waar 1+ klant geraakt is, log een externe
incident-summary (klant, impact, timeline, fix) in een aparte
incident-tracker — niet in GlitchTip zelf, want GlitchTip data wordt
na 90 dagen gepurged.
---
## §7 Form-builder binding failures
### Context
`FormBindingApplicator` draait synchroon binnen
`ApplyBindingsOnFormSubmit::handle` voor elke form-submission. Als die
throwt — schema-misconfiguratie, infra-issue, data-integrity violation
of deadline-timeout — vangt de listener de throw op, schrijft een
`form_submission_action_failures` rij in de outer-transaction, en zet
`apply_status='failed'` op de parent-submission. GlitchTip vangt het
exception-event op met de `crewli-api` project-tags.
De alert-rule op `apply_status=failed AND form_schema.has_public_token=true`
brengt alleen de klant-zichtbare failures naar boven — organizer-driven
private-form failures zijn zichtbaar in de Form Failures admin-UI zonder
alert, omdat hun blast radius beperkt is tot één organisatie.
Per RFC-WS-6.md §Q3 v1.3 + ARCH-BINDINGS.md §6 (two-transaction pattern)
en §11 (failures lifecycle).
### 7.1 Eerste check — `failure_response_code`
De submission-rij draagt een denormalised classification-token in
`failure_response_code`. Vier waardes:
| Token | Oorzaak | Triage-pad |
|---|---|---|
| `schema_config_error` | Organizer-config issue (renamed kolom, deleted target-entity, ontbrekende identity-key binding die publish-guards toch doorlieten) | Contact de organizer van het schema; **NIET retryen** — retry produceert dezelfde exception |
| `temporary_error` | Infra-issue (DB-connection, lock-for-update wait, deadline-wrapper timeout) | Retry via `php artisan form-failures:retry --id={failure_ulid}` of admin-UI; als een tweede retry óók faalt, escaleer als infra-incident |
| `data_integrity_error` | Data-shape violation (type-mismatch, FK-violation, soft-deleted target-entity) | Onderzoek de failing binding's `target_entity` + `target_attribute` in `form_field_bindings`; lost meestal op als een schema-config issue verkleed als data |
| `unknown_error` | Iets buiten de binding-applicator hiërarchie (raw `\Throwable`, `IdentityMatchInvariantViolation`) | Triage via de GlitchTip exception-trace; dit is dev-investigation territory |
De exception-class op de action-failure rij is de canonical truth;
`failure_response_code` is de afgeleide classificatie die door de
response-renderer gebruikt wordt.
### 7.2 Tweede check — public-token presence
De GlitchTip event-tag `form_schema.has_public_token` onderscheidt:
| Tag-waarde | Betekenis | Severity |
|---|---|---|
| `true` | Public submission flow (vrijwilliger registratie-window, public form-fill) | Customer-impact bevestigd; mobiliseer binnen minuten tijdens active festival registratie |
| `false` | Organizer-driven private flow (managed crew roster, internal data import) | Blast radius beperkt tot één organisatie; onderzoek binnen uren |
### 7.3 Retry vs dismiss
Gebruik het artisan-command:
```bash
# Retry een enkele failure
php artisan form-failures:retry --id={failure_ulid}
# Retry alle open failures voor een submission
php artisan form-failures:retry --submission={submission_ulid}
# Dry-run eerst als onzeker
php artisan form-failures:retry --id={failure_ulid} --dry-run
```
Wanneer wel retryen:
- `temporary_error` — ja, bijna altijd; de deadline-wrapper of infra-hiccup
is waarschijnlijk inmiddels weg.
- `unknown_error` — alleen na dev-investigatie die bevestigt dat de
throw transient was.
Wanneer dismissen:
- `schema_config_error` nadat de organizer het schema heeft gefixt en
opnieuw heeft gesubmit — de originele failure-rij blijft staan als
audit, dismiss met reden `schema_deleted` of `binding_removed` per
het relevante geval.
- `data_integrity_error` nadat dev-investigatie het herleidt tot een
one-off data-state die niet meer geldt — dismiss met
`data_quality_issue`.
- Iedere failure voor een submission die de organizer expliciet als
duplicaat geclassificeerd heeft — dismiss met `duplicate_submission`.
De `DismissalReasonType` enum heeft zes cases; `OTHER` vereist een
free-text note. Per ARCH-BINDINGS §11.3.
### 7.4 Severe-failure escalatie
Als GlitchTip `>10` events in 1 uur toont gescoped op één
`form_schema_id`, behandel als P1:
1. Acknowledge de alert in het team-channel.
2. Identificeer het schema via `form_submission.form_schema_id` op een
willekeurige failed submission.
3. Pauzeer publieke toegang tot het schema tijdelijk — disable het
public token tot de root-cause geïdentificeerd is.
4. Standaard incident-triage geldt verder.
Dit patroon wijst bijna altijd op een publish-guard gap: een
schema-configuratie die de guards hadden moeten weigeren is er toch
doorheen geslipt, en elke submission ertegen produceert dezelfde
exception.
### 7.5 Cross-references
- [`RFC-WS-6.md`](../RFC-WS-6.md) v1.3.1 — volledige architectuur, in
het bijzonder §Q3 v1.3 additions.
- [`ARCH-BINDINGS.md`](../ARCH-BINDINGS.md) v1.2 — §6 two-transaction
pattern, §11 failures lifecycle, §11.3 dismissal reasons.
- [`observability-erasure.md`](./observability-erasure.md) — Art. 17
procedure als triage in een GDPR-deletion-verzoek omdraait.
- BACKLOG `TECH-CHANNEL-AUTH-ORG-ADMIN` — bekende follow-up voor
live-update channel-auth uitbreiding (org-admin scope).