Files
crewli/dev-docs/ARCH-CONSOLIDATION-ADDENDUM-2026-04-24.md
bert.hausmans f7ddc1b3ce docs: close base scope-class extraction follow-up (post-WS-5)
Reflects the FormFieldChildTableMorphScope extraction landed in the
previous commit:

  - ARCH-FORM-BUILDER.md v1.9 — five locations updated:
      §6.7 (Relational binding table) — added forward reference
        sentence after the FormFieldBindingScope escape-hatch line
        (WS-5a was the first scope; previously had no deferral note
        because nothing existed yet to defer)
      §17.4.2 (Relational table form_field_validation_rules) —
        "deferred to WS-5d per addendum Q3" replaced with marker-
        subclass forward reference
      §17.5.3 (Service, scope, cascade — config) — same replacement
      §17.6.1 (Field options rationale) — "unblocks the deliberate
        follow-up" replaced with completion-confirmation
      §17.6.3 (Service / scope / cascade — option) — "deferred to a
        follow-up work package" replaced with marker-subclass forward
        reference + Phase A diff verification result
    Version metadata + changelog updated; v1.8 prose preserved in the
    Previous-versions block.

  - ARCH-CONSOLIDATION-ADDENDUM-2026-04-24.md — new
    "Uitvoering — base scope-class extractie (2026-04-25)" section
    inserted after the WS-5d Uitvoering, documenting the Phase A
    diff-verification, marker-subclass approach, private→protected
    YAGNI policy, the inline-FQN → use-statements stylistic refinement,
    static-analysis impact (Larastan baseline clean, Rector
    357 → 355), and net-diff figures.

  - BACKLOG.md — FORM-BUILDER-MORPH-SCOPE-BASE-CLASS item closed
    via strikethrough header + "Status: closed 2026-04-25" annotation
    (matches the TECH-TS-PORTAL-TSC closure convention from earlier
    this week).

  - SCHEMA.md — three stale "deferred" claims updated to reflect the
    completed extraction:
      header v2.6 changelog mention rewritten to point at the now-
        landed FormFieldChildTableMorphScope
      form_field_validation_rules table-section global-scope note
        replaced with marker-subclass forward reference
      form_field_options table-section global-scope note same
        replacement
    Schema version NOT bumped — no actual schema change.
    The two other scope mentions (form_field_bindings,
    form_field_configs) made no deferral claims and remain accurate.

Note: the work package's prose listed "§6.7 / §17.4.3 / §17.5.3 /
§17.6.3" as deferral-note locations. The actual locations were
§17.4.2 (not §17.4.3), §17.5.3, §17.6.1 (not just §17.6.3), and
§17.6.3 — §6.7 had no deferral note (WS-5a was the first scope,
nothing to defer yet). All five spots updated in line with the work
package's intent.

WS-5 family fully complete: no open follow-up items remain under the
"delete > adapt" discipline of the WS-5 refactor.

Tests: 1208 passed (3260 assertions). No code changes in this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 04:52:01 +02:00

412 lines
38 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: ARCH Consolidation — Architect Decisions Addendum (2026-04-24)
description: Beslissingen op de 6 architect-vragen uit het WS-1 rapport
status: binding
owner: architect
created: 2026-04-24
supersedes-sections-of: /dev-docs/ARCH-CONSOLIDATION-2026-04.md §5 (scope inschattingen)
---
# ARCH Consolidation — Architect Decisions Addendum (2026-04-24)
## Context
Op 2026-04-24 leverde WS-1 (opsporings-pas) een rapport met 45 bevindingen en 6 architect-vragen. Dit addendum legt de beantwoording van die vragen vast én corrigeert waar nodig de werkstroom-scope. Dit document is **bindend** voor WS-2 t/m WS-8.
Bronnen:
- `/dev-docs/ARCH-CONSOLIDATION-2026-04.md` — sprint charter (§1 principes, §3 vastgelegde besluiten, §5 werkstromen)
- `/dev-docs/ARCH-CONSOLIDATION-WS1-REPORT.md` — opsporings-rapport (45 bevindingen)
Het charter blijft ongewijzigd. §5 inschattingen worden in dit addendum herzien. §3 vastgestelde besluiten worden bevestigd (niet gewijzigd) — de architect-precisering op besluit 5 die tijdens review was overwogen is verworpen: besluit 5 blijft exact zoals geformuleerd.
## Samenvatting beslissingen
| Q | Onderwerp | Besluit |
|---|-----------|---------|
| Q1 | ULID voor 11 non-ULID tabellen | **Alle 11 migreren naar ULID.** Charter §3 besluit 5 blijft exact zoals geformuleerd; geen exceptions. |
| Q2 | OrganisationScope strategie | **FK-chain scope extension.** Denormalisatie alleen op tabellen die rapportage-hot zijn. Charter §3 besluit 3 bevestigd als enige uitzondering op basis van meetbaar rapportage-gebruik. |
| Q3 | WS-5 scope | **Vier committed splits op `form_fields` + drie library-mirror splits.** `role_restrictions` blijft JSON. |
| Q4 | Sanctum `personal_access_tokens` morph | **Ongewijzigd laten.** Framework-polymorfie valt buiten de domain morph-map. Documenteren in WS-8. |
| Q5 | SCHEMA.md rewrite-shape | **Aparte `/dev-docs/ARCH-PLANNED-MODULES.md`** voor tabellen zonder migratie. SCHEMA.md wordt strict "waarheid van wat bestaat in de database". |
| Q6 | `subject_type` allow-list | **Consolideren onder WS-2 purpose registry.** Compile-time test dwingt morph-map-alignment met purpose-registry af. |
---
## Q1 — ULID voor 11 non-ULID tabellen
**Besluit:** Alle 11 tabellen uit Categorie A van het WS-1 rapport worden naar ULID gemigreerd. Charter §3 besluit 5 ("ULID consistent overal, ook pivots, ook logging-tabellen") blijft ongewijzigd. Geen exceptions.
**Tabellen:**
| ID | Tabel | Categorie |
|----|-------|-----------|
| A-01 | `organisation_user` | Pure pivot, geen model |
| A-02 | `event_user_roles` | Pure pivot, geen model |
| A-03 | `crowd_list_persons` | Pure pivot, geen model |
| A-04 | `event_person_activations` | Pure pivot, geen model |
| A-05 | `user_organisation_tags` | Pivot met model |
| A-06 | `person_section_preferences` | Pivot met model |
| A-07 | `mfa_backup_codes` | Ephemeral data |
| A-08 | `mfa_email_codes` | Ephemeral data |
| A-09 | `form_submission_section_statuses` | Pivot met model |
| A-10 | `form_values` | EAV hot path |
| A-11 | `form_value_options` | EAV hot path |
**Motivatie:**
- **Pre-launch window:** geen data-migratie, geen downtime, geen backfill-pijn. Dit is het enige moment waarop consistentie gratis is. Een half jaar later met productie-data kost hetzelfde werk weekenden met downtime.
- **Performance-argumenten zijn theoretisch op Crewli's schaal:** EAV join-performance tussen int en ULID keys is verwaarloosbaar bij de volumes waar Crewli zich op richt (enkele miljoenen `form_values` per festival-jaar, ruim binnen MySQL + goede indexen op ULID).
- **Consistentie-winst is concreet en blijvend:** elke uitzondering introduceert cognitieve belasting ("waarom is deze tabel anders?") die besluit 5 juist wil voorkomen.
- **Valstrik 2 (gold-plating) niet van toepassing:** dit is geen elegantie-verbetering, dit is uitvoering van een reeds genomen besluit.
**Scope-impact:** WS-4 migreert 11 PK's in plaats van 0 of 2.
**Migratie-notes voor WS-4:**
- Voor tabellen met een model (`UserOrganisationTag`, `PersonSectionPreference`, `MfaBackupCode`, `MfaEmailCode`, `FormSubmissionSectionStatus`, `FormValue`, `FormValueOption`): `HasUlids` trait toevoegen.
- Voor pure pivots zonder model (`organisation_user`, `event_user_roles`, `crowd_list_persons`, `event_person_activations`): `$table->ulid('id')->primary()` in de migratie, geen model nodig.
- Inline migratie-commentaar "int AI for join performance" wordt vervangen door de ULID-migratie zelf; geen extra rationale-documentatie in de migratie.
---
## Q2 — OrganisationScope strategie
**Besluit:** `OrganisationScope` wordt uitgebreid met een declaratieve FK-chain strategy. Denormalisatie van `organisation_id` blijft beperkt tot tabellen die **meetbaar rapportage-hot** zijn.
**Concreet:**
- `form_submissions` behoudt denormalized `organisation_id` (per charter §3 besluit 3 — bevestigd als bewuste exception voor rapportage-queries: dashboards, CSV-exports, aggregerende counts over duizenden rijen).
- Andere form-builder child tables krijgen **geen eigen `organisation_id` kolom**; zij gebruiken FK-chain via hun parent:
- `form_schema_sections` → via `form_schemas.organisation_id`
- `form_fields` → via `form_schemas.organisation_id`
- `form_values` → via `form_submissions.organisation_id` (denormalized parent)
- `form_value_options` → via `form_submissions.organisation_id`
- `form_submission_section_statuses` → via `form_submissions.organisation_id`
- `form_submission_delegations` → via `form_submissions.organisation_id`
- `form_schema_webhooks` → via `form_schemas.organisation_id`
- `form_webhook_deliveries` → via `form_submissions.organisation_id`
**Shape van de scope-extensie (concept — implementatie-details in WS-4):**
```php
class FormField extends Model
{
use HasUlids;
protected static function tenantScopeStrategy(): array
{
return ['via' => FormSchema::class, 'fk' => 'form_schema_id'];
}
}
```
`OrganisationScope` leest de strategy en bouwt de JOIN automatisch op.
**Motivatie:**
- **Normalisatie-zuiverheid:** `organisation_id` heeft één bron van waarheid (`form_schemas`, `events`, enz.). Denormalisatie introduceert synchronisatie-risico en dubbele boekhouding.
- **Single responsibility:** `OrganisationScope` blijft *de* multi-tenant guard. Geen gedeelde verantwoordelijkheid tussen observer + scope over 8 tabellen.
- **Toekomstbestendig:** hetzelfde patroon werkt straks automatisch voor accreditation, briefings en andere modules met sub-tabellen. Eén scope-klasse uitbreiding, geen migraties per module.
- **Denormalisatie-uitzondering gerechtvaardigd:** `form_submissions` is rapportage-hot. De denormalisatie is een meetbare performance-optimalisatie voor aggregerende queries, geen "veilig-voor-het-geval-dat" patroon.
**Rapportage-hotness criterium:** een tabel krijgt alleen denormalized `organisation_id` als er regelmatig aggregerende queries rechtstreeks op draaien (counts, group-by, exports over duizenden rijen). Alle andere tabellen gebruiken FK-chain via hun parent.
**Scope-impact:** WS-4 krijgt één scope-klasse uitbreiding + scope-registratie op 9 form-builder child models + 5 event-data models (uit WS-1 rapport D-04: `ShiftAssignment`, `ShiftWaitlist`, `VolunteerAvailability`, `PersonSectionPreference`, `PersonIdentityMatch`). Geen 8 extra kolom-migraties.
---
## Q3 — WS-5 scope
**Besluit:** WS-5 splitst de vier committed kolommen op `form_fields` (per charter §3 besluit 6) én de drie library-mirrors op `form_field_library`. `role_restrictions` blijft JSON.
**Te splitsen:**
| Bron kolom | Doel tabel | Sub-WS |
|------------|-----------|--------|
| `form_fields.binding` | `form_field_bindings` | WS-5a |
| `form_fields.validation_rules` | `form_field_validation_rules` | WS-5b |
| `form_fields.conditional_logic` | `form_field_conditional_logic` | WS-5c |
| `form_fields.options` | `form_field_options` | WS-5d |
| `form_field_library.default_binding` | `form_field_bindings` (met owner discriminator) | WS-5a |
| `form_field_library.validation_rules` | `form_field_validation_rules` (met owner discriminator) | WS-5b |
| `form_field_library.options` | `form_field_options` (met owner discriminator) | WS-5d |
**Blijft JSON:**
- `form_fields.role_restrictions` — kleine set Spatie rol-strings. Geen FK-partner mogelijk (rollen zijn strings in Spatie-permission, geen aparte tabel-PK's). Niet queryable in practice. Relationele splitsing levert geen architectuur-winst.
- `form_fields.translations`, `form_field_library.translations` — flat key-value bags per locale, nooit queried. Splitsen is speculatief.
**Motivatie:**
- **Consistent relationeel form-builder domein:** library en fields delen dezelfde onderliggende tabellen voor options, bindings en validation rules. Twee stijlen in hetzelfde domein is inconsistent.
- **Library-entries krijgen een discriminator** (exacte shape in WS-5a — ofwel `owner_type` enum (`field|library`), ofwel een paar nullable FK's (`form_field_id` OR `form_field_library_id`)). Discriminator-keuze wordt in WS-5a besloten op basis van query-patterns.
- **`FormFieldService::insertFromLibrary` kopieert rijen** tussen library-entries en field-entries in plaats van JSON te hydrateren. Natuurlijk werk binnen WS-5; geen aparte PR.
**Scope-impact:** WS-5 wordt ~7-8 dagen in plaats van 4-6. Drie extra sub-werkstromen voor de library-mirrors passen binnen de bestaande WS-5a/b/d PR-structuur (library valt onder dezelfde PR als de corresponderende field-split).
### Uitvoering — WS-5a (2026-04-24)
**Discriminator-keuze:** polymorphe morph (`owner_type` enum met waarden `form_field` en `form_field_library`, `owner_id` ULID). Het alternatief (twee nullable FK-kolommen `form_field_id` / `form_field_library_id`) is verworpen:
- **MySQL 8 heeft geen partial-unique-support** waarmee je per FK-kolom exclusief uniek kunt zijn; dat maakt de paired-FK-vorm in deze engine fundamenteel onhandig.
- **Consistentie over WS-5b/c/d**: `form_field_validation_rules`, `form_field_conditional_logic` en `form_field_options` gebruiken dezelfde owner-discriminator shape per Q3. Eén idiomatisch patroon over de hele familie wint van per-tabel workarounds.
- **Morph-map aliassen** `form_field` en `form_field_library` stonden al geregistreerd in `AppServiceProvider::registerMorphMap()` voor activity-log doeleinden; WS-5a heeft ze zonder extra werk hergebruikt.
**Scope-enforcement:** `OrganisationScope` (Q2 FK-chain resolver) kan geen morph-parent walken. WS-5a levert `FormFieldBindingScope` als sibling-scope die een UNION bouwt over beide owner-ketens (`form_field → form_schema → organisation_id` `form_field_library → organisation_id`). Zie ARCH-FORM-BUILDER §6.7.
**Service-grens:** `FormFieldBindingService` is de enige schrijver. `FormFieldService::insertFromLibrary` kopieert rijen via `copyBindings`, niet JSON (Q3 row-copy mandaat). Snapshot-writer en API-resources lezen via `toJsonShape` zodat het externe JSON-contract ongewijzigd blijft.
**Git-log kanttekening commit 3.** De 1Password signer gaf herhaalde "failed to fill whole buffer" errors op de lange HEREDOC body van de bedoelde commit-message; de uiteindelijke commit landde met alleen de titel (`refactor(form-builder): pre-publish check reads form_field_bindings; drop binding JSON columns`, SHA `61719bf`). De volledige rationale — pre-publish check switch van JSON naar relationele query, kolom-drops op `form_fields.binding` en `form_field_library.default_binding`, factory/resource/form-request cleanup, fixture-rewrites — staat in de WS-5a completion notitie, niet in `git show 61719bf`. Amenden is niet geprobeerd: CLAUDE.md verbiedt signed-commit amenden.
### Uitvoering — WS-5b (2026-04-25)
WS-5b splitst `form_fields.validation_rules` en `form_field_library.validation_rules` naar een relationele tabel `form_field_validation_rules`. Rij-shape: `owner_type` / `owner_id` (polymorphic morph, hergebruik van de WS-5a aliassen) + `rule_type` string(40, app-enforced enum `FormFieldValidationRuleType`) + `parameters` JSON per rule-type. Rationale: rule_type is de queryable dimensie ("welke velden hanteren regex?"), waarden zijn heterogeen (int voor min_length, string voor regex, array voor mime_types) waardoor JSON *per rij* passend is, niet voor de hele bag.
**Seed-scan resultaat (Phase A).** Zes distinct top-level keys geobserveerd in seeders/factories/tests/resource-code-paden: `min`, `max`, `regex`, `unique`, `max_priorities`, `tag_categories`. `required` bestond niet in de wild (kolom `is_required` was al de bron van waarheid). Enum-catalogus uitbreiding beperkt tot de vijftien validatie-cases; non-validatie-keys verplaatsen naar een parallelle tabel.
**Strict-enterprise scope-uitbreiding.** Tijdens de decision gate is WS-5b uitgebreid met een parallelle tabel `form_field_configs` (zie ARCH §17.5, landed in WS-5b commit 5). Rationale: `tag_categories` en `storage_disk` zijn renderings-/upload-configuratie, geen validatie-regels. Ze opnemen in `form_field_validation_rules` zou de tabel-naam semantisch vervuilen — precies het type drift dat deze sprint opruimt. Eén extra tabel, één extra enum, één extra service, één extra scope; géén scope-creep verder dan dat paar.
**Key-translaties bij backfill.**
- `required`, `unique`: WARN-log + skip. Kolom-duplicaten van `is_required` / `is_unique`. In WS-5b commit 3 is de `FormValueService::$rules['unique']` fallback verwijderd — `is_unique` is nu de enige bron.
- `max_priorities``rule_type = max_selected`: semantisch gelijk (cap op entries in list-valued veld); twee enum-cases voor één semantiek = rot. Seeder (`FormBuilderDevSeeder`) en test-assertie (`PublicFormSeederTest:190`) aangepast om de canonieke naam te gebruiken. Geen bridging in `toJsonShape`; historische snapshots blijven onaangeraakt.
- Ambigu `min` / `max`: field-type dispatch — NUMBER → `min_value`/`max_value`; TEXT/TEXTAREA/EMAIL/PHONE/URL → `min_length`/`max_length`; DATE/DATETIME → `date_min`/`date_max`; anders **FAIL** migratie. Type-inappropriate gebruik van `min`/`max` is seed-data-bug; force correction, absorbeer niet stilletjes.
- `tag_categories`, `storage_disk`: skipped door de validation-rules backfill met INFO log; commit 5's configs-backfill pickt ze op in `form_field_configs`.
**Activity log-conventie.** Per §6.7 WS-5a: `field.validation_rules_replaced` wordt alleen geëmit op FormField-subject, niet op FormFieldLibrary. Library-niveau audits leven elders; bewuste convergentie met de binding-pattern.
**Scope-sibling.** `FormFieldValidationRuleScope` is een near-duplicaat van `FormFieldBindingScope` (zelfde UNION-over-two-owner-chains shape). Base-class extractie is uitgesteld tot WS-5d (waar het derde sibling `form_field_options` een gedeelde vorm moet onthullen); premature abstractie uit twee is nog steeds premature. Cascade-observer `FormFieldBindingsCascadeObserver` is hernoemd naar `FormFieldChildTablesCascadeObserver` en ruimt nu drie child-tabellen op (bindings, validation-rules, en — vanaf commit 5 — configs).
**Strict validator op save (commit 3).** De vier FormRequests (`StoreFormFieldRequest`, `UpdateFormFieldRequest`, `StoreFormFieldLibraryRequest`, `UpdateFormFieldLibraryRequest`) accepteren `validation_rules` nu als array-of-spec-objects (`[{rule_type, parameters, error_message_key?}, ...]`). Semantische validatie (enum-case + parameter-shape + callback-registry) loopt via `FormFieldValidationRuleService::assertSpecsValid()` in een `after()` hook, zodat bad specs 422 geven vóór enige write. Controllers schrijven niet langer `validation_rules` naar de JSON-kolom; writes gaan uitsluitend via `replaceRules()`.
**Configs-tabel landing (commit 5).** `form_field_configs` is de parallelle tabel voor non-validation configuratie — `tag_categories` (TAG_PICKER-opties filter) en `storage_disk` (upload disk selector). Zelfde polymorphic-morph shape, eigen `FormFieldConfigType` enum, eigen `FormFieldConfigService`, eigen `FormFieldConfigScope`. De backfill-migratie pickt deze twee keys uit de pre-WS-5b JSON bag (die commit 2 welbewust skipte) en plaatst ze in `form_field_configs` vóór de JSON-kolomdrop in `2026_04_25_120002`. De hernoemde `FormFieldChildTablesCascadeObserver` dekt nu drie child-tabellen (bindings, validation_rules, configs) op owner delete.
**Drie scope-siblings.** `FormFieldBindingScope` + `FormFieldValidationRuleScope` + `FormFieldConfigScope` — dezelfde UNION-over-two-owner-chains shape, drie near-duplicaten. Base-class extractie blijft uitgesteld tot WS-5d waar `form_field_options` als vierde sibling landt en het echte "wat varieert" zichtbaar zou moeten worden. Abstractie uit drie kopieën is nog steeds premature wanneer de vierde concrete implementatie aanstaande is.
**Breaking change frontend-contract (commit 5).** De JSON-contract wijziging landde atomair in commit 5 — geen bridging compatibility layer per de "Breaking change acceptance" clause in ARCH-FORM-BUILDER §0. Vier portal Vue-componenten gemigreerd naar de canonieke key-namen (`min_value`/`max_value`/`max_length`/`max_selected`). De `tag_categories` / `storage_disk` reads in resources bleven binnen de backend — de portal SPA had deze keys niet direct in gebruik, dus de hypothetische `field.configs.tag_categories.categories` frontend-migratie bleef beperkt tot documentatie (ARCH §17.5.5) tot een frontend consumer het aanroept.
**JSON-kolommen gedropt.** `form_fields.validation_rules` en `form_field_library.validation_rules` dropten in `2026_04_25_120002`. SCHEMA.md v2.4 verwerkte de drop plus de nieuwe `form_field_configs`-sectie; ARCH-FORM-BUILDER bumped naar v1.6 met een volledig nieuwe §17.5. De rollback-path "roll back WS-5b commits 15 together" reconstrueert beide JSON-bag bestemmingen mergen validatie-rules en configs terug naar één bag per rij — niet te verwarren met de per-migratie partial rollback die niet ondersteund is.
**Afronding WS-5b.** 5 commits, baseline tests 1047 → volledig groen na commit 5. WS-5b is hiermee compleet; scope-sibling extractie en WS-5c (`conditional_logic`) / WS-5d (`options`) zijn separate work packages.
### Uitvoering — WS-5c (2026-04-26)
WS-5c splitst `form_fields.conditional_logic` JSON naar een relationele boom over twee tabellen: `form_field_conditional_logic_groups` (AND/OR nodes met `parent_group_id` voor nesting) + `form_field_conditional_logic_conditions` (leaves met `field_slug` / `comparison_operator` / `value` JSON). Tree-structuur met interleaved mixed children (groups en conditions als siblings onder één ouder), sort-order per positie binnen parent.
**Geen library-mirror — Q3 expliciet.** `form_field_library` draagt geen conditional_logic en komt niet in scope. Eenvoudige FK `form_field_id` op de groups-tabel; geen polymorphic morph, geen `owner_type`/`owner_id` kolommen. `FormFieldService::insertFromLibrary` propageert geen conditional_logic — library-entries dragen die state niet.
**Twee-tabel tree-structuur.** Alternatief (single table met nullable `value`/`operator` per rij-rol) zou semantische vervuiling hetzelfde type rot oproepen dat WS-5b's configs-split weg haalde. Groepen en condities zijn semantisch anders — één tabel per concept, interleaving via sort_order bij read-time in `toJsonShape`. Geen `sort_order` drift tussen siblings.
**OrganisationScope cap verhoging van 3 naar 5.** De conditions-scope-chain (`condition → group → field → schema → organisation_id`) is 4 hops; de group-chain is 3. De oude cap van 3 zou bij conditions throwen via `TenantScopeResolutionException`. Raising naar 5 dekt de ketens en geeft headroom voor toekomstige diepere trees zonder `form_field_id` op conditions te denormaliseren (drift-risico). Infrastructurele wijziging — niet beperkt tot WS-5c; volgende WS's mogen diepere chains declareren als dat architectuur-winst oplevert.
**Cycle detection behoud contract.** `FormFieldService::assertNoConditionalCycle` werd vervangen door `FormFieldConditionalLogicService::assertNoCycles`. Zelfde algoritme (DFS over sibling-adjacency), andere implementatie-site: leest de relationele tree, niet meer JSON. Tree-interne cycles zijn structureel onmogelijk via `parent_group_id` adjacency-list.
**Activity log dual-events.** `field.updated` (met gereconstrueerde `old.conditional_logic` / `new.conditional_logic` via `toJsonShape`) + `field.conditional_logic_replaced` (semantisch). FormField subject-only, conform §6.7 WS-5a en §17.4.2 WS-5b. Library-niveau is silent — er IS geen library conditional_logic om te loggen.
**Snapshot + API resource shape ongewijzigd.** Externe JSON-contract blijft byte-gelijk aan pre-WS-5c via `toJsonShape`. Geen frontend breaking change — de portal's `evaluateConditionalLogic` composable blijft dezelfde `{show_when: {...}}` struct consumeren.
**Strict validator op save (commit 3).** `StoreFormFieldRequest` / `UpdateFormFieldRequest` accepteren `conditional_logic` nu als array-shape, niet JSON string. `after()` hook roept `FormFieldConditionalLogicService::assertSpecsValid` aan en rejects unknown operators, root conditions, empty groups, en malformed children als 422 vóór enige write.
**Drie scope-siblings + twee FK-chain strategy users.** `FormFieldBindingScope` + `FormFieldValidationRuleScope` + `FormFieldConfigScope` zijn nog steeds de drie morph-based UNION-scope near-duplicaten (base-class extractie blijft uitgesteld tot WS-5d). WS-5c voegt twee FK-chain declaratieve strategy models toe (`FormFieldConditionalLogicGroup`, `FormFieldConditionalLogicCondition`) — die tellen niet voor de morph-sibling extractie-discipline. WS-5d's `form_field_options` wordt het vierde polymorphic-morph sibling en beslist het "abstract of niet".
**JSON-kolom gedropt.** `form_fields.conditional_logic` gedropt in `2026_04_26_100003`. Geen library-equivalent om te droppen. Rollback-path: "roll back WS-5c commits 13 samen" — de backfill `down()` reconstrueert JSON terug naar de (via commit 3 rollback teruggekomen) kolom.
**Afronding WS-5c.** 4 commits, baseline tests 1104 → 1148 volledig groen na commit 3 (drop-column). Breaking change acceptance: geen bridging compatibility layer — de portal blijft onaangeraakt omdat het externe JSON-contract identiek is. WS-5d (`options`) is het laatste WS-5-werkpakket.
### Uitvoering — WS-5d (2026-04-27)
WS-5d splitst zowel `form_fields.options` als `form_field_library.options` naar één polymorfe relationele tabel `form_field_options`. Vierde en laatste WS-5-sibling. Rij-shape: `owner_type` / `owner_id` (morph alias-hergebruik uit WS-5a) + `value` string(255) + `label` string(255) + `sort_order` uint + `translations` JSON nullable per BCP-47 locale. UNIQUE-index `ffo_owner_value_unique` op `(owner_type, owner_id, value)` is de seed-bug-guard: dubbele waarden per veld hebben geen semantiek en moeten zowel op service-niveau (`assertSpecsValid`) als op DB-niveau falen.
**Snapshot-rewrite atomair in commit 2.** Per zero-compromise directive — geen reader-tolerantie voor pre-WS-5d shape in commit 3 onwards. Iedere bestaande `form_submissions.schema_snapshot.fields[*].options` en `form_templates.schema_snapshot.fields[*].options` is in dezelfde transactie als de `form_field_options` backfill herschreven naar de rich-shape `[{value, label, sort_order, translations?}, ...]`. De parallelle `translations.{locale}.options[]` arrays zijn in één pass van zowel de bron-tabellen als de snapshots gestript — die data leeft nu op de optie-rij zelf in zijn eigen `translations` JSON bag.
**Strict-fail backfill dispatch.** Mirrort §17.4.4 / §8.7 conventie. De backfill-migratie faalt hard op: (a) `field_type ∉ {RADIO, SELECT, MULTISELECT, CHECKBOX_LIST}` met non-null `options` (post-WS-5b TAG_PICKER seed-bug indicator), (b) niet-flat-string-array option shape, (c) translations.{locale}.options[] length mismatch, (d) niet-string of >255-char translated labels, (e) elk overgebleven `translations.{locale}.options` na step C. "Fix at source, don't absorb silently" — net als WS-5b's min/max field-type dispatch.
**Activity log dual-event op FormField subject only.** Per §6.7 / §17.4.2 / §17.6.3: `field.updated` met gereconstrueerde `old.options` / `new.options` (byte-equal JSON-compare gate om cosmetische false positives te vermijden) plus semantische `field.options_replaced` vanuit `FormFieldOptionService::replaceOptions`. Library-subject writes blijven silent. Diff-key wordt weggelaten wanneer alleen label/sort_order verandert — geen ruis voor downstream activity-log consumers.
**Vier scope-siblings → base-class extractie nu warranted.** Met `FormFieldOptionScope` als vierde concrete UNION-over-two-owner-chains implementatie naast `FormFieldBindingScope` / `FormFieldValidationRuleScope` / `FormFieldConfigScope`, kan de "wat varieert" vraag eindelijk empirisch beantwoord worden. Base-class extractie is bewust uitgesteld naar een separate follow-up PR — niet meegenomen in WS-5d om de scope strakt te houden. Alle vier siblings zijn near-duplicate; abstractie uit vier kopieën is concrete refactoring, niet meer premature.
**Commit 0 dead-code precondition.** WS-5d landde op een commit-0 cleanup van `MigrateLegacyFormsData` + `VerifyFormsDataIntegrity` console commands (Pre-form-builder registration_form_fields → form_* tabel migrators). Beide commands waren no-ops sinds de S2a drop van de legacy registration tabellen, en `MigrateLegacyFormsData:225` schreef `$rff->options` direct naar `form_fields.options` — die kolom dropt in commit 5, dus de migrator zou breaking compileren. CLAUDE.md "delete > adapt" — orphaned commands ruimen op vóór ze in de weg zitten.
**Tijdelijke `getOptionsAttribute` accessor-bridge tijdens commit 3.** Eloquent's `getAttribute()` resolveert in volgorde: `attributes` array, casts, accessor mutators, dán pas relaties. Zolang de `form_fields.options` JSON-kolom op de tabel bestaat (commits 3 → 5) komt elke `$field->options` access uit op de raw column-waarde (string/null), nooit op de morphMany-collectie — ook niet wanneer je `with('options')` eager-load. WS-5d commit 3 plakte een tijdelijke accessor-mutator op beide modellen die `$this->load('options')` aanroept en de relation-collectie teruggeeft, zodat resources / snapshot writer / FormFieldRuleBuilder `$f->options` als drop-in collectie konden gebruiken zonder `()->get()` overal. Commit 5 droppt zowel de kolom als de accessor; vanaf dat punt valt `getAttribute('options')` natuurlijk door naar de relatie. **Pattern voor toekomstige JSON→relationele refactors**: tijdens de overlap-fase een accessor-mutator die de relatie eager-loadt en teruggeeft, gedropt zodra de JSON-kolom verdwijnt. Niet een permanente oplossing — een commit-3-tot-commit-5 bridge.
**Dev-seeder normalisering van `vergoedingstype` RADIO veld.** De `FormBuilderDevSeeder` schreef pre-WS-5d een nested-object shape `[{label, description}, ...]` op de RADIO `vergoedingstype` veld — drift uit een vroege seed-iteratie die nooit gecorrigeerd is. ARCH §5.1's option-bearing field types (RADIO/SELECT/MULTISELECT/CHECKBOX_LIST) modeleren géén per-option description — de rich-shape is `{value, label, sort_order, translations?}`, descriptions niet voorzien. WS-5d commit 2 normaliseert dit naar flat string array `['Pro Deo', 'Entreeticket', 'Vrijwilligersvergoeding']`; de descriptions zijn drift en zijn dropped. **Niet te verwarren met `apps/app/src/components/{organisation,event}/RegistrationField*.vue`** — die consumeren `RegistrationFieldTemplate` (legacy S1-era `registration_field_templates` tabel) met eigen `normalized_options: [{label, description}]` shape; dat is een parallel domein met eigen API-endpoints en composables, orthogonaal aan WS-5d's `FormField` domein en bewust buiten scope.
**Afronding WS-5d.** 6 commits (commit 0 cleanup + 5 WS-5d core), baseline tests 1158 → 1208 volledig groen na commit 5. Breaking change acceptance: geen bridging compatibility layer — vier portal componenten (`FieldRadio`, `FieldSelect`, `FieldMultiselect`, `FieldCheckboxList`) gemigreerd naar `OptionSpec[]` rich-shape met locale-aware label-resolutie via `providePublicFormLocale` injectie en `resolveOptionLabel(option, locale)` helper in `@form-schema/types/formBuilder`. apps/app blijft onaangeraakt — `RegistrationField*.vue` componenten consumeren een ander legacy domein dat geen WS-5d migratie nodig heeft. **WS-5 familie compleet.**
### Uitvoering — base scope-class extractie (2026-04-25)
Sluit de WS-5-familie follow-up `FORM-BUILDER-MORPH-SCOPE-BASE-CLASS`
af. Vier concrete polymorfe morph-scope siblings landed across WS-5a/
b/d (`FormFieldBindingScope`, `FormFieldValidationRuleScope`,
`FormFieldConfigScope`, `FormFieldOptionScope`); de discipline-keuze
"abstract pas na vier concrete kopieën" werd door §17.4.2 / §17.5.3 /
§17.6.1 / §17.6.3 expliciet uitgesteld.
**Phase A diff-verificatie clean.** De vier concrete scope-files
waren byte-equal in `apply()` en `resolveOrganisationId()` (63 regels
per body, drie pairwise diffs leeg) — geen divergentie, geen
verborgen behavior-variatie tussen siblings. Het empirische
antwoord op "wat varieert daadwerkelijk?": niets. De abstractie is
een pure logica-deduplicatie.
**Aanpak: identity-preserving abstract base + marker subclasses.**
- `FormFieldChildTableMorphScope` (nieuw, abstract) bevat de volledige
UNION-over-two-owner-chains logica + de twee morph-alias-constanten
(`OWNER_TYPE_FIELD = 'form_field'`,
`OWNER_TYPE_LIBRARY = 'form_field_library'`) als private constants.
- De vier bestaande sibling-classes worden marker subclasses:
`final class X extends FormFieldChildTableMorphScope {}`. Lege body,
enkel-regel declaratie.
- Klasse-identiteit blijft behouden: alle bestaande
`Model::withoutGlobalScope(FormFieldXScope::class)` aanroepen in
cascade observers, backfill migraties en platform super_admin
paths werken zonder wijziging. Vier test-call-sites in de
`FormFieldXScopeTest` klassen werken eveneens zonder wijziging.
**Sichtbaarheid: `private` blijft.** De abstract class' helpers
blijven private. Als een toekomstige sibling alternatieve morph-
aliassen of owner-chains nodig heeft, promote dan
`private → protected` op dat moment. YAGNI-discipline: geen
flexibility-by-default.
**Geen tests gewijzigd.** De vier bestaande scope-tests
(`FormFieldBindingScopeTest`, `FormFieldValidationRuleScopeTest`,
`FormFieldConfigScopeTest`, `FormFieldOptionScopeTest`) verifiëren
publiek gedrag dat byte-equal blijft. Test + assertion tellingen
identiek pre- en post-refactor: 1208 / 3260.
**Net diff:** 5 files changed, +165 / -377. ≈219 regels duplication-
verlies over de vier sibling-files, ~125 regels toegevoegd in de
nieuwe base class, ~80 regels in de vier marker-subclass declaraties
combined.
**Static-analysis gates.** Larastan baseline blijft clean (geen
nieuwe errors buiten baseline; abstract base + marker subclasses
zijn type-compleet). Rector dry-run: 357 → 355 suggesties (kleine
reductie door dedupliceerde apply-bodies).
**Stylistisch: `use` statements i.p.v. inline FQNs.** De vier
originals gebruikten `\App\Models\Organisation` en `\App\Models\Event`
inline in `resolveOrganisationId()`. Bij de extractie zijn die
gemigreerd naar `use App\Models\Organisation;` / `use App\Models\Event;`
boven aan het bestand. Geen functionele wijziging, alleen leesbaarheid.
**WS-5-familie volledig afgerond.** Geen open follow-up items meer
onder de "delete > adapt" discipline van WS-5.
---
## Q4 — Sanctum `personal_access_tokens`
**Besluit:** De Sanctum-default voor `tokenable_type / tokenable_id` (FQCN in DB, geen morph-map entry) blijft ongewijzigd.
**Motivatie:**
- Framework-polymorfie valt buiten de domain morph-map conventie. Een alias toevoegen introduceert onderhoudsschuld bij elke Sanctum-upgrade.
- Domain polymorfie (`form_schemas.owner_type`, `form_submissions.subject_type`) blijft strict in morph-map geregistreerd; dit besluit raakt daar niet aan.
**Documentatie-actie (WS-8):** in de ARCH-documentatie wordt een expliciete regel opgenomen:
> De `enforceMorphMap`-conventie geldt voor domain polymorfe relaties. Framework-relaties (Sanctum `tokenable_type`, Spatie `activitylog.subject_type`, Spatie `activitylog.causer_type`) volgen hun framework-defaults en zijn expliciet uitgezonderd van de morph-map conventie.
---
## Q5 — SCHEMA.md rewrite-shape
**Besluit:** WS-8 extraheert alle tabellen-zonder-migratie uit SCHEMA.md naar een apart document `/dev-docs/ARCH-PLANNED-MODULES.md`. SCHEMA.md wordt strict "waarheid van wat bestaat in de database".
**Te verplaatsen tabellen** (uit WS-1 rapport bevinding D-01):
- §3.5.4 Volunteer Profile & History: `volunteer_festival_history`, `post_festival_evaluations`, `festival_retrospectives`
- §3.5.6 Accreditation Engine: `accreditation_categories`, `accreditation_items`, `event_accreditation_items`, `accreditation_assignments`, `access_zones`, `access_zone_days`, `person_access_zones`
- §3.5.7 Artists & Advancing: `performances`, `stages`, `stage_days`, `advance_submissions`, `artist_contacts`, `artist_riders`, `itinerary_items`
- §3.5.8 Communication & Briefings: `briefing_templates`, `briefings`, `briefing_sends`, `communication_campaigns`, `messages`, `message_replies`, `broadcast_messages`, `broadcast_message_targets`
- §3.5.9 Check-In & Operational: `check_ins`, `show_day_absence_alerts`, `scanners`, `inventory_items`, `event_info_blocks`, `event_info_block_crowd_types`, `production_requests`, `material_requests`
**Onderhoud-pattern:**
- Bij elke PR die een planned-module tabel aanmaakt verhuist de betreffende sectie van `ARCH-PLANNED-MODULES.md` naar `SCHEMA.md`. Dit wordt onderdeel van de PR-template checklist.
- `ARCH-PLANNED-MODULES.md` behoudt de index/soft-delete/FK-intent zoals vastgelegd in de originele SCHEMA.md rijen, zodat de planning-informatie niet verloren gaat.
- SCHEMA.md Rule 4 (required indexes) verwijst alleen nog naar bestaande tabellen na de rewrite.
**Motivatie:**
- **Enterprise documentatie-discipline:** onboarding developers zien direct wat er in de DB staat, zonder ruis van planning.
- **SCHEMA.md Rule 4 consistent:** required indexes kunnen niet meer verwijzen naar fantoom-tabellen.
- **Planned-modules eigen evolutie:** kan sneller bijgewerkt worden zonder SCHEMA-ruis.
**Scope-impact:** WS-8 wordt ~4-5 dagen in plaats van 2-3.
---
## Q6 — `subject_type` allow-list
**Besluit:** De allow-list uit `config/form_subjects.php` wordt opgeheven en geconsolideerd onder de purpose-registry in WS-2. `PurposeDefinition` heeft per charter §3 besluit 4 al een `subject_type` veld; dat wordt de enige bron van waarheid.
**Concreet (uitvoering in WS-2):**
- `config/form_subjects.php` verdwijnt na WS-2.
- `StoreFormSubmissionRequest` leidt toegestane `subject_type` waarden af uit `PurposeRegistry::allSubjectTypes()`.
- `AppServiceProvider::boot` bouwt de morph-map deels vanuit de purpose-registry (per purpose een subject-type entry) + framework-entries (activity-log subjects/causers, Sanctum uitgezonderd).
- **Compile-time guard:** een unit-test faalt als een `subject_type` in `PurposeRegistry` niet in de morph-map staat, of vice versa. Dit vervangt de huidige "developer discipline" afspraak.
**Motivatie:**
- **Eén bron van waarheid** voor subject-semantiek. Geen twee-bestanden-sync met impliciete koppeling.
- **Verstevigt charter §3 besluit 4:** `PurposeDefinition` wordt écht de volledige purpose-specificatie.
**Scope-impact:** WS-2 krijgt ~halve dag extra werk. De migratie zelf is klein; het compile-time consistency testje is het echte werk.
---
## Herziene werkstroom-scope en inschatting
Charter §5 inschattingen zijn op basis van dit addendum herzien:
| WS | Onderwerp | Charter inschatting | Herzien |
|----|-----------|---------------------|---------|
| WS-1 | Opsporings-pas | 1 dag | 1 dag (afgerond 2026-04-24) |
| WS-2 | Purpose registry | 2-3 dagen | 2-3 dagen + Q6 consolidatie (~halve dag) |
| WS-3 | Één SPA consolidatie | 3-5 dagen | 3-5 dagen (ongewijzigd) |
| WS-4 | ULID + denormalized submission columns + scope | 2-3 dagen | **5-6 dagen** (Q1 elf ULID migraties + Q2 scope extension + scope-registratie op 14 models + D-05 Person SoftDeletes verify) |
| WS-5 | JSON-kolom-opsplitsing | 4-6 dagen | **7-8 dagen** (Q3 library-mirrors toegevoegd aan WS-5a/b/d) |
| WS-6 | FormBindingApplicator | 4-5 dagen | 4-5 dagen (ongewijzigd) |
| WS-7 | Observability foundation | 2-3 dagen | 2-3 dagen + D-06 activity_log indexes |
| WS-8 | Documentatie-consolidatie | 2-3 dagen | **4-5 dagen** (Q4 framework-exception + Q5 planned-modules extractie + PK-decisions doc) |
**Totale herziene inschatting:** 28-38 dagen werk (charter had 22-32 dagen). Toename van ~6 dagen, volledig verklaard door de strict-enterprise invulling van Q1, Q2 en Q3.
**Werkstroom-volgorde ongewijzigd:** WS-1 → WS-2 → WS-3 (parallel mogelijk met WS-4/5) → WS-4 → WS-5 → WS-6 → WS-7 → WS-8.
---
## Openstaande bevindingen zonder architect-beslissing
WS-1 rapport Categorie D bevindingen die geen architect-beslissing vereisten en in de relevante werkstromen worden meegenomen:
| ID | Bevinding | Werkstroom |
|----|-----------|-----------|
| D-03 | Form-builder child models registreren OrganisationScope niet | WS-4 (als onderdeel van Q2 uitvoering) |
| D-04 (event-data subset) | ShiftAssignment / ShiftWaitlist / VolunteerAvailability / PersonSectionPreference / PersonIdentityMatch zonder scope | WS-4 |
| D-04 (user/admin subset) | MFA / TrustedDevice / UserProfile / EmailLog / OrganisationEmailSettings etc. | backlog, per-model ticket |
| D-05 | Person SoftDeletes verificatie | WS-4 |
| D-06 | activity_log `(subject_type, subject_id)` en `(causer_type, causer_id)` indexes | WS-7 |
| D-07 | Morph map forward-looking entries documentatie-link | WS-8 |
---
## Verwijzingen
- `/dev-docs/ARCH-CONSOLIDATION-2026-04.md` — sprint charter (§1 principes, §3 vastgestelde besluiten blijven ongewijzigd; §5 inschattingen herzien in dit addendum).
- `/dev-docs/ARCH-CONSOLIDATION-WS1-REPORT.md` — bron van alle 45 bevindingen waarnaar dit addendum verwijst.
---
## Sign-off
- **Architect review:** akkoord per Claude Chat sessie 2026-04-24, iteratief verscherpt over drie rondes (initial → strict-enterprise op Q1/Q3 → FK-chain correctie op Q2).
- **Product owner:** akkoord per Bert Hausmans 2026-04-24.
- **WS-5a afronding:** 2026-04-24 — relationele `form_field_bindings` tabel, polymorphic owner, snapshot-parity, JSON-kolommen gedropt.
- **WS-5b afronding:** 2026-04-25 — relationele `form_field_validation_rules` + parallel `form_field_configs` tabel; `validation_rules` JSON-kolommen gedropt; frontend-contract migratie naar canonieke key-namen landed in commit 5.
- **WS-5c afronding:** 2026-04-26 — relationele `form_field_conditional_logic_groups` + `form_field_conditional_logic_conditions` tree-tabellen; simple FK op FormField (geen library-mirror per Q3); `conditional_logic` JSON-kolom gedropt; `OrganisationScope` FK-chain cap verhoogd van 3 naar 5 hops; snapshot + API resource JSON-contract byte-identiek via `toJsonShape`.
Volgende stap: prompt opstellen voor WS-2 (Purpose registry) met Q6-consolidatie als integraal onderdeel van de werkstroom.