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

38 KiB
Raw Blame History

title, description, status, owner, created, supersedes-sections-of
title description status owner created supersedes-sections-of
ARCH Consolidation — Architect Decisions Addendum (2026-04-24) Beslissingen op de 6 architect-vragen uit het WS-1 rapport binding architect 2026-04-24 /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):

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_prioritiesrule_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.