Final WS-5d cleanup. The JSON columns that have been unread since
commit 3 are now physically dropped on both source tables. Their
canonical rich-shape lives in form_field_options, accessed
exclusively through the morphMany relation.
Defensive sweep: any lingering translations.{locale}.options key in
either source table's translations bag is stripped. Commit 2's
backfill should already have done so exhaustively; this is
belt-and-braces.
Rollback re-creates the columns as nullable JSON but leaves them
empty. Pair with commit 2's rollback to restore the pre-WS-5d data
shape on every owner row.
The commit-3 getOptionsAttribute accessor-bridge on FormField +
FormFieldLibrary is removed — Eloquent's getAttribute() resolution
now naturally falls through to the morphMany relation since there's
no underlying column to shadow it. New regression test
FormFieldOptionsAccessTest asserts $field->options resolves to an
Eloquent Collection of FormFieldOption instances and lazy-loads in
exactly 2 queries (1 parent + 1 lazy-load options) on a fresh fetch
without with() preload. Same trio for FormFieldLibrary.
Migration step-count tests in WS-5a/b/c bumped by 1 to account for
the new drop_form_field_options_json_columns migration on the
rollback stack.
Documentation:
- SCHEMA.md v2.6: form_field_options table documented; options row
removed from form_fields and form_field_library; morphMany
relations updated; cross-references to ARCH-FORM-BUILDER §17.6
and addendum §Q3 WS-5d Uitvoering added on both source-table
docblocks.
- ARCH-FORM-BUILDER.md v1.8: new §17.6 "Field options (relational)"
mirrors the §17.4 / §17.5 relational-sibling structure with
sub-sections 17.6.1 rationale, 17.6.2 table + catalogue, 17.6.3
service / scope / cascade / activity log, 17.6.4 snapshot
embedding, 17.6.5 external API contract. Existing Webhooks
section renumbered from §17.6 to §17.7.
- ARCH-CONSOLIDATION-ADDENDUM-2026-04-24.md: "Uitvoering — WS-5d
(2026-04-27)" section added. Eight paragraphs covering the
snapshot atomic rewrite, strict-fail backfill dispatch, dual
activity-log emit, four-sibling base-class extraction warrant,
commit 0 dead-code precondition, the temporary getOptionsAttribute
accessor-bridge pattern (with reusability note for future
JSON→relational refactors), the dev-seeder vergoedingstype RADIO
normalisation (drift correction explicitly distinguished from the
parallel apps/app RegistrationFieldTemplate description domain),
and the WS-5 family completion note.
- BACKLOG.md: FORM-BUILDER-LIBRARY-AUDIT-LOG entry extended to four
services (adds library.options_replaced); new
FORM-BUILDER-MORPH-SCOPE-BASE-CLASS entry added as the WS-5d
follow-up now that all four concrete morph-scope siblings exist.
Tests: 1193 → 1208 green (+15 across commits 3+4+5; this commit alone:
+2 from the regression test).
This completes the WS-5 family.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
350 lines
36 KiB
Markdown
350 lines
36 KiB
Markdown
---
|
||
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 1–5 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 1–3 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.**
|
||
|
||
---
|
||
|
||
## 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.
|