--- 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. --- ## 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. Volgende stap: prompt opstellen voor WS-2 (Purpose registry) met Q6-consolidatie als integraal onderdeel van de werkstroom.