Files
crewli/dev-docs/ARCH-CONSOLIDATION-WS1-REPORT.md

41 KiB
Raw Blame History

title, description, status, owner, created
title description status owner created
ARCH Consolidation — WS-1 Discovery Report Read-only architecture scan output for the April 2026 consolidation sprint draft architect 2026-04-24

WS-1 — Discovery Pass Report

Output of the read-only architecture scan kicked off by /dev-docs/ARCH-CONSOLIDATION-2026-04.md WS-1. Read together with the consolidation charter before WS-2 starts. This report is discovery-only: no code, no migrations, no tests were changed.

0. Scan scope and method

  • Date of scan: 2026-04-24
  • Branch: main
  • Commit scanned: f9cc8c0a1be4093c7508df90dcdbe12ebbdb390a
  • Tools used: grep (via Bash), targeted Read on migrations/models/services, four parallel Explore sub-agents for breadth. No AST-grep, no static-analysis tooling.
  • In scope: api/database/migrations/, api/app/Models/ (incl. FormBuilder/), api/app/Models/Scopes/, api/app/Services/, api/app/Http/Controllers/, api/app/Http/Resources/, api/app/Http/Requests/, api/app/Listeners/, api/app/Observers/, api/app/Policies/, api/app/Providers/, api/app/Events/, api/app/FormBuilder/, api/config/.
  • Explicitly NOT scanned: apps/app/, apps/portal/, packages/, vendor/, node_modules/, resources/vuexy-admin-*/, api/tests/, VitePress docs at /docs/.

1. Summary table

ID Category Target Classification Proposed WS
A-01 A organisation_user documented-exception-confirm-in-ws4 ws-4
A-02 A event_user_roles documented-exception-confirm-in-ws4 ws-4
A-03 A crowd_list_persons documented-exception-confirm-in-ws4 ws-4
A-04 A event_person_activations documented-exception-confirm-in-ws4 ws-4
A-05 A user_organisation_tags documented-exception-confirm-in-ws4 ws-4
A-06 A person_section_preferences documented-exception-confirm-in-ws4 ws-4
A-07 A mfa_backup_codes documented-exception-confirm-in-ws4 ws-4
A-08 A mfa_email_codes documented-exception-confirm-in-ws4 ws-4
A-09 A form_submission_section_statuses documented-exception-confirm-in-ws4 ws-4
A-10 A form_values documented-exception-confirm-in-ws4 ws-4
A-11 A form_value_options documented-exception-confirm-in-ws4 ws-4
B-01 B form_fields.binding scheduled-ws5 ws-5a
B-02 B form_fields.validation_rules scheduled-ws5 ws-5b
B-03 B form_fields.conditional_logic scheduled-ws5 ws-5c
B-04 B form_fields.options scheduled-ws5 ws-5d
B-05 B form_field_library.options needs-architect-decision ws-5d?
B-06 B form_field_library.validation_rules needs-architect-decision ws-5b?
B-07 B form_field_library.default_binding needs-architect-decision ws-5a?
B-08 B form_fields.role_restrictions needs-architect-decision ws-5 / backlog
B-09 B form_fields.translations opaque-keep
B-10 B form_field_library.translations opaque-keep
B-11 B form_schemas.settings opaque-keep
B-12 B form_submissions.schema_snapshot opaque-keep
B-13 B form_templates.schema_snapshot opaque-keep
B-14 B form_values.value opaque-keep
B-15 B form_webhook_deliveries.payload_snapshot opaque-keep
B-16 B shifts.events_during_shift opaque-keep
B-17 B persons.custom_fields opaque-keep
B-18 B person_identity_matches.match_details opaque-keep
B-19 B user_profiles.settings opaque-keep
B-20 B organisations.settings opaque-keep
B-21 B events.recurrence_exceptions opaque-keep
B-22 B activity_log.properties / .attribute_changes opaque-keep (Spatie-owned)
C-01 C form_schemas.owner_type / owner_id domain-justified-keep
C-02 C form_submissions.subject_type / subject_id domain-justified-keep
C-03 C activity_log.subject/causer_type domain-justified-keep (Spatie) ws-7 (index)
C-04 C personal_access_tokens.tokenable_* (Sanctum) needs-architect-decision ws-8
D-01 D Missing migrations for ~30 SCHEMA-documented tables scope-addition-ws-8 ws-8
D-02 D OrganisationScope has no form_schema_id / chain-via-FK strategy scope-addition-ws-4 / needs-decision ws-4 / new-ws
D-03 D Form-builder child models not applying OrganisationScope scope-addition-ws-4 ws-4
D-04 D ShiftAssignment / ShiftWaitlist / VolunteerAvailability without scope scope-addition-ws-4 ws-4
D-05 D Person has no SoftDeletes reachable in model (verify) standalone-fix (verify) ws-4
D-06 D activity_log polymorph columns lack (type, id) index scope-addition-ws-7 ws-7
D-07 D Morph map in AppServiceProvider registers 6 owner types, only event used standalone-fix (docs) ws-8
D-08 D form_submissions.subject_type allow-list lives in two places (config/form_subjects.php + morph map) standalone-fix ws-2 / ws-8

Counts per category: A: 11, B: 22, C: 4, D: 8 (total findings: 45).


2. Category A — Non-ULID primary keys

ULID baseline established by SCHEMA §3.5.11 Rule 1 and ARCH §3 besluit 5 ("ULID consistent overal, ook pivots"). All other business tables (40+ tables) use ULID correctly and are not listed individually. Only the non-ULID tables appear below.

A-01 — organisation_user

  • Current PK type: int AI (via $table->id())
  • Source of truth: api/database/migrations/2026_04_07_210000_create_organisation_user_table.php:14 — inline comment // int AI for join performance. No model (pure pivot).
  • SCHEMA.md section: §3.5.1 — documented as pivot, no ULID mandate.
  • Classification: documented-exception-confirm-in-ws4
  • Note: Pure pivot (user ↔ organisation). ARCH §3 besluit 5 demands ULID everywhere including pivots; this is the canonical example of a decision to re-confirm or reverse.

A-02 — event_user_roles

  • Current PK type: int AI
  • Source of truth: api/database/migrations/2026_04_07_240000_create_event_user_roles_table.php:14 — comment // int AI for join performance. No model.
  • SCHEMA.md section: §3.5.1 — pivot-like (event ↔ user ↔ role).
  • Classification: documented-exception-confirm-in-ws4

A-03 — crowd_list_persons

  • Current PK type: int AI
  • Source of truth: api/database/migrations/2026_04_08_240000_create_crowd_list_persons_table.php:14. No model.
  • SCHEMA.md section: §3.5.5 — pivot (crowd list ↔ person).
  • Classification: documented-exception-confirm-in-ws4

A-04 — event_person_activations

  • Current PK type: int AI
  • Source of truth: api/database/migrations/2026_04_08_410000_create_event_person_activations_table.php:14. No model.
  • SCHEMA.md section: §3.5.5 — join table (event ↔ person) tracking sub-event activation.
  • Classification: documented-exception-confirm-in-ws4

A-05 — user_organisation_tags

  • Current PK type: int AI
  • Source of truth: api/database/migrations/2026_04_10_110000_create_user_organisation_tags_table.php:14. Model: api/app/Models/UserOrganisationTag.phpdoes not use HasUlids.
  • SCHEMA.md section: §3.5.5a — pivot (user ↔ organisation ↔ person_tag).
  • Classification: documented-exception-confirm-in-ws4
  • Note: A model exists here, unlike A-01/A-02/A-03/A-04. If WS-4 confirms the ULID mandate for pivots, this migration is non-trivial: existing keys referenced by the model must be rewritten.

A-06 — person_section_preferences

  • Current PK type: int AI
  • Source of truth: api/database/migrations/2026_04_12_200003_create_person_section_preferences_table.php:14. Model: api/app/Models/PersonSectionPreference.php — no HasUlids.
  • SCHEMA.md section: §3.5.5b.
  • Classification: documented-exception-confirm-in-ws4

A-07 — mfa_backup_codes

  • Current PK type: int AI
  • Source of truth: api/database/migrations/2026_04_15_200001_create_mfa_backup_codes_table.php:14. Model: api/app/Models/MfaBackupCode.php — no HasUlids.
  • SCHEMA.md section: §3.5.1a.
  • Classification: documented-exception-confirm-in-ws4
  • Note: Security-adjacent table. Potential rationale: ephemeral rows, high turnover, single-owner lookup. Flag for WS-4 explicit decision.

A-08 — mfa_email_codes

  • Current PK type: int AI
  • Source of truth: api/database/migrations/2026_04_15_200003_create_mfa_email_codes_table.php:14. Model: api/app/Models/MfaEmailCode.php — no HasUlids.
  • SCHEMA.md section: §3.5.1a.
  • Classification: documented-exception-confirm-in-ws4

A-09 — form_submission_section_statuses

  • Current PK type: int AI
  • Source of truth: api/database/migrations/2026_04_19_100007_create_form_submission_section_statuses_table.php:14. Model: api/app/Models/FormBuilder/FormSubmissionSectionStatus.php — no HasUlids.
  • SCHEMA.md section: §3.5.12.
  • Classification: documented-exception-confirm-in-ws4

A-10 — form_values

  • Current PK type: int AI
  • Source of truth: api/database/migrations/2026_04_19_100009_create_form_values_table.php:15 — explicit comment: // Integer AI PK: EAV table, joined heavily — int joins faster than ULID joins (ARCH §4.4). Model: api/app/Models/FormBuilder/FormValue.php — no HasUlids.
  • SCHEMA.md section: §3.5.12 (form_values).
  • Classification: documented-exception-confirm-in-ws4
  • Note: The migration's inline comment references ARCH §4.4 — a rationale predating the consolidation charter. ARCH-CONSOLIDATION §3 besluit 5 overrides that rationale unless the architect confirms the EAV performance exception.

A-11 — form_value_options

  • Current PK type: int AI
  • Source of truth: api/database/migrations/2026_04_19_100010_create_form_value_options_table.php:14. Model: api/app/Models/FormBuilder/FormValueOption.php — no HasUlids.
  • SCHEMA.md section: §3.5.12.
  • Classification: documented-exception-confirm-in-ws4
  • Note: Populated by FormValueObserver; rebuilt on every form value save. Volatile pivot data.

Category A — summary

  • Total non-ULID tables: 11 business tables + 14 framework/Spatie/Laravel tables (all expected, not listed).
  • Undocumented violations: none — every non-ULID PK either has an inline migration comment justifying it, or is a pure pivot without a model.
  • WS-4 decision needed: all 11 tables. ARCH §3 besluit 5 says "ULID consistent overal, ook pivots" without exceptions — so the migration comments stating "int AI for join performance" are now in conflict with the charter. WS-4 must explicitly confirm (retain exceptions) or reverse (migrate all).

3. Category B — JSON columns with queryable data

Rule 2 whitelist (SCHEMA §3.5.11): opaque config, free-text arrays, rider data, submission diff snapshots, events_during_shift. No JSON path query (whereJsonContains, JSON_EXTRACT, ->>, whereJsonLength, etc.) was found anywhere in the backend. Every JSON column is read as a whole blob via the cast array. Classification therefore leans heavily on charter decisions and column intent, not on observed query patterns.

Note on legacy tables: migration 2026_04_20_100000_drop_remaining_legacy_registration_tables.php already dropped registration_form_fields, registration_field_templates, and person_field_values. Their JSON columns are not part of the current schema and are not listed here.

B-01 — form_fields.binding

  • Current use: Conditional logic binding config (computed fields, dynamic visibility). Read as blob, validated/applied during submission in FormValueService.
  • Sample usage: api/app/Services/FormBuilder/FormValueService.php (accesses $field->binding)
  • Classification: scheduled-ws5 (ARCH §3 besluit 6, WS-5a)
  • Proposed target: new form_field_bindings table per ARCH §6.1.

B-02 — form_fields.validation_rules

  • Current use: Field validation schema (min/max, regex, required, tag_categories). Read as blob, passed to FormFieldRuleBuilder for runtime validation.
  • Sample usage: api/app/Services/FormBuilder/FormValueService.php (is_array($field->validation_rules)…)
  • Classification: scheduled-ws5 (WS-5b)
  • Proposed target: form_field_validation_rules table per ARCH §6.6.

B-03 — form_fields.conditional_logic

  • Current use: Skip-logic and visibility rules (if X=Y, show this field). Read as blob to extract dependent field slugs.
  • Sample usage: api/app/Services/FormBuilder/FormFieldService.php (extractConditionSlugs())
  • Classification: scheduled-ws5 (WS-5c)
  • Proposed target: form_field_conditional_logic table. Charter flags AND/OR tree shape as sub-decision during WS-5c.

B-04 — form_fields.options

  • Current use: Field-specific UI metadata (select choices, radio buttons). Read as whole blob in resources.
  • Sample usage: api/app/Http/Resources/FormBuilder/FormFieldResource.php
  • Classification: scheduled-ws5 (WS-5d)
  • Proposed target: form_field_options table per ARCH §6.6.

B-05 — form_field_library.options

  • Current use: Library-level default UI options copied to new fields at instantiation.
  • Sample usage: api/app/Services/FormBuilder/FormFieldService.php
  • Classification: needs-architect-decision
  • Proposed target: if WS-5d splits form_fields.options into its own table, should the library-level defaults follow the same split or remain inline? Two consistent outcomes: (a) split both (library becomes a template row in the new options table), (b) keep library JSON as opaque "seed-data" and hydrate on instantiation. Needs a call before WS-5d starts.

B-06 — form_field_library.validation_rules

  • Classification: needs-architect-decision — same shape of decision as B-05, but for validation rules.

B-07 — form_field_library.default_binding

  • Classification: needs-architect-decision — same shape as B-05, but for bindings.
  • Note: The migration column name (default_binding) suggests it was designed to pre-fill a binding on new fields. If WS-5a's form_field_bindings table is the target, the natural mapping is "library rows may define default bindings which are cloned". Architect to confirm.

B-08 — form_fields.role_restrictions

  • Current use: Role-based visibility (portal-only, admin-only) read in FieldAccessService.
  • Sample usage: api/app/Services/FormBuilder/FieldAccessService.php ($field->role_restrictions…)
  • Classification: needs-architect-decision
  • Reason: Not in ARCH's committed WS-5 split list, but semantically akin to a queryable enum set. If WS-5 is expanded to "everything structured on form_fields becomes relational", this one belongs with them; if WS-5 is kept to the four committed splits, it stays JSON.

B-09 / B-10 — form_fields.translations, form_field_library.translations

  • Current use: I18n label/help text overrides per field. Read-only, per-field UI.
  • Classification: opaque-keep — translations are flat key-value bags by locale, not queried anywhere. Splitting them into a relational translations table would be speculative.

B-11 — form_schemas.settings

  • Current use: Form-level configuration blob. Charter explicitly (§3 besluit 6) says form_schemas.settings should shrink to "echt opaque config (rendering hints)" after WS-5 completes. Today the blob is small and read as-is.
  • Classification: opaque-keep — provided WS-5 a/b/c/d land. Re-confirm at end of WS-5 that nothing queryable has crept back in.

B-12 / B-13 / B-15 — form_submissions.schema_snapshot, form_templates.schema_snapshot, form_webhook_deliveries.payload_snapshot

  • Current use: Immutable snapshots for audit/replay.
  • Classification: opaque-keep — SCHEMA.md Rule 2 explicitly allows submission diff snapshots.

B-14 — form_values.value

  • Current use: EAV storage of actual answers. Filtering is done via denormalised value_indexed, value_number, value_date typed columns; the value JSON itself is read as a blob.
  • Classification: opaque-keep
  • Note: This is an intentional EAV+denormalised-filter-column pattern. Valid as long as the typed columns cover all filter cases; WS-5/WS-6 should verify that when bindings read answers, they read from the typed columns or from the cast array, not via JSON path.

B-16 — shifts.events_during_shift

  • Classification: opaque-keep — whitelisted in Rule 2.

B-17 — persons.custom_fields

  • Current use: Legacy registration-time extras (section preferences and similar). Read as blob.
  • Sample usage: api/app/Http/Controllers/Api/V1/ShiftAssignmentController.php (accesses custom_fields['section_preferences'])
  • Classification: opaque-keep — but flag for WS-6 FormBindingApplicator: once bindings land, custom_fields becomes redundant for bindable attributes. Track shrinkage after WS-6 lands so this column can be reassessed (deletion → BACKLOG, not sprint scope).

B-18 — person_identity_matches.match_details

  • Classification: opaque-keep — scoring metadata, never queried on nested keys.

B-19 — user_profiles.settings

  • Classification: opaque-keep.

B-20 — organisations.settings

  • Classification: opaque-keep.

B-21 — events.recurrence_exceptions

  • Current use: Array of excluded dates (RFC 5545 EXDATE pattern).
  • Classification: opaque-keep — flat date list, never queried on positions.

B-22 — activity_log.properties / attribute_changes

  • Classification: opaque-keep (Spatie-managed).

Category B — summary

  • Queryable-must-split found in practice: 0. No whereJsonContains, JSON_EXTRACT, or ->> usage exists anywhere in the backend.
  • Scheduled WS-5 splits confirmed: 4 (form_fields.binding, .validation_rules, .conditional_logic, .options).
  • Open architect decisions: 4 (form_field_library.options, .validation_rules, .default_binding, form_fields.role_restrictions).
  • Genuinely opaque (keep): 14.

4. Category C — Polymorphic relations

C-01 — form_schemas.owner_type / owner_id

  • Defined in: api/database/migrations/2026_04_19_100002_create_form_schemas_table.php:1819. Model: api/app/Models/FormBuilder/FormSchema.php:92 (owner()).
  • Allowed types (observed): morph map registers event, user, user_profile, person, company, organisation. Only event is actively used in queries (FilterRegistryController, PublicFormController). The other five are placeholders for future purposes (USER_PROFILE etc.).
  • Morph map status: fully registered in api/app/Providers/AppServiceProvider.php:73122 via Relation::enforceMorphMap(...).
  • Index on (type, id): yes — $table->index(['owner_type', 'owner_id'], 'fs_owner_idx').
  • Classification: domain-justified-keep (ARCH §1 P4).

C-02 — form_submissions.subject_type / subject_id

  • Defined in: api/database/migrations/2026_04_19_100006_create_form_submissions_table.php:2021. Model: FormSubmission.php:80 (subject()). Inverse example: Person.php:108 (formSubmissions()).
  • Allowed types (observed): person, user, user_profile, company, organisation, event — same morph map keys as C-01. Active uses in FieldAccessService, FormSubmissionService (via morphKeyFor()).
  • Morph map status: registered (same provider).
  • Index on (type, id): yes — fs_subject_idx.
  • Classification: domain-justified-keep (ARCH §1 P4).

C-03 — activity_log.subject_type/subject_id and causer_type/causer_id

  • Defined in: api/database/migrations/2026_04_07_120000_create_activity_log_table.php:17, 19 via nullableMorphs().
  • Allowed types (observed): ~23 domain models + 8 form-builder models, all registered in the single enforceMorphMap call.
  • Morph map status: fully registered; Activity::saving hook in AppServiceProvider.php:145161 adds impersonation context.
  • Index on (type, id): nonullableMorphs() does NOT auto-create a (type, id) composite index, unlike morphs() and ulidMorphs(). The pair is unindexed on both subject_* and causer_*.
  • Classification: domain-justified-keep (Spatie-framework); WS-7 should add indexes if/when the activity log is queried by subject.

C-04 — personal_access_tokens.tokenable_type / tokenable_id

  • Defined in: api/database/migrations/2026_04_07_100000_create_personal_access_tokens_table.php:15 via ulidMorphs('tokenable').
  • Allowed types (observed): Sanctum default (User in practice); no morph-map entry.
  • Morph map status: NOT in enforceMorphMap — Sanctum uses FQCN in the DB.
  • Index on (type, id): yes — ulidMorphs() auto-indexes.
  • Classification: needs-architect-decision
  • Note: Low urgency. If the team wants strict morph-map consistency ("no FQCN in DB anywhere"), add a user alias and document in ARCH. Otherwise leave Sanctum alone. Decide in WS-8 when docs are rewritten.

Category C — summary

  • Polymorphs found: 4 relation pairs (2 domain, 2 framework).
  • Domain polymorphs both justified and correctly indexed: C-01, C-02.
  • Framework polymorphs: C-03 (Spatie), C-04 (Sanctum).
  • Only real action item: C-03 missing (type, id) index — candidate scope-addition for WS-7.
  • No inconsistency: no logical relation is modelled polymorphically in one place and typed-FK in another. No unused single-target polymorphs (C-01 has only event in active use today, but the other five owner types are committed purpose targets per ARCH §3 besluit 4, so keeping it polymorphic is justified).

5. Category D — Other architecture smells

D-01 — Many tables documented in SCHEMA.md have no migration

  • Location: SCHEMA.md §3.5.4, §3.5.6, §3.5.7, §3.5.8, §3.5.9 vs api/database/migrations/.
  • Smell: The following tables are fully documented in SCHEMA.md but no Schema::create(...) migration exists for them:
    • §3.5.4: volunteer_festival_history, post_festival_evaluations, festival_retrospectives
    • §3.5.6: accreditation_categories, accreditation_items, event_accreditation_items, accreditation_assignments, access_zones, access_zone_days, person_access_zones
    • §3.5.7: performances, stages, stage_days, advance_submissions, artist_contacts, artist_riders, itinerary_items
    • §3.5.8: briefing_templates, briefings, briefing_sends, communication_campaigns, messages, message_replies, broadcast_messages, broadcast_message_targets
    • §3.5.9: check_ins, show_day_absence_alerts, scanners, inventory_items, event_info_blocks, event_info_block_crowd_types, production_requests, material_requests
  • Why it matters this sprint: SCHEMA.md is the charter's stated "truth for 'as built'" (§3). Today it documents roughly 30 tables that don't exist. Rule 4 of §3.5.11 even prescribes required indexes on check_ins, briefing_sends, performances, advance_sections, person_section_preferences — two of those (check_ins, briefing_sends) have no table. WS-8's SCHEMA rewrite needs to either (a) remove these and move them to a separate "Planned modules" document, or (b) mark each row as STATUS: planned. Either way, SCHEMA.md cannot be trusted as-is during WS-4 through WS-7.
  • Proposed workstream: ws-8 (primary); also informs ws-4 scope bounds (we don't migrate PKs on tables that don't exist).
  • Classification: scope-addition-ws-8

D-02 — OrganisationScope strategies don't cover form_schema_id or any FK chain beyond one level

  • Location: api/app/Models/Scopes/OrganisationScope.php (whole file). Acknowledged explicitly in the class comment of api/app/Models/FormBuilder/FormSchemaWebhook.php:1322: "OrganisationScope's column strategies (organisation_id / event_id / festival_section_id) do not cover form_schema_id — extending the scope to a new strategy is out of scope for S1."
  • Smell: The scope supports only three strategies: direct organisation_id, via event_id, via festival_section_id. Form-builder child tables (form_schema_sections, form_fields, form_submissions, form_values, form_value_options, form_submission_delegations, form_submission_section_statuses, form_schema_webhooks, form_webhook_deliveries) all scope via form_schema_id or deeper — none of which the scope can resolve. The FormSchemaWebhook comment calls out the caller discipline required: "NEVER query FormSchemaWebhook::query() without an eager constraint."
  • Why it matters this sprint: The architecture's tenancy guarantee (CLAUDE.md > Multi-tenancy: "Every query on event data MUST scope on organisation_id") relies on developer discipline for these models instead of on a global scope. One forgotten eager constraint in a future controller is a cross-org data leak. WS-6 adds a new service (FormBindingApplicator) that will traverse submission → schema → org — exactly the path the scope doesn't currently model.
  • Proposed workstream: ws-4 (extend scope with a new strategy: form_schema_id and possibly a generic through_fk_chain strategy). Alternative: new-ws if the architect decides to solve this more broadly (e.g., declarative scope chain in models).
  • Classification: scope-addition-ws-4 (or needs-architect-decision for the "extend strategy" vs "add-on new-ws" split).

D-03 — Form-builder child models don't register OrganisationScope

  • Location: api/app/Models/FormBuilder/FormSchemaSection.php, FormField.php, FormSubmission.php, FormValue.php, FormValueOption.php, FormSubmissionDelegation.php, FormSubmissionSectionStatus.php, FormSchemaWebhook.php, FormWebhookDelivery.php.
  • Smell: None of the 9 form-builder child models call static::addGlobalScope(new OrganisationScope()). FormSchema, FormFieldLibrary, FormTemplate do register it (direct organisation_id). The child models rely on the caller-discipline workaround in D-02.
  • Why it matters this sprint: Same as D-02 (tenancy leak risk via direct Model::query() calls), but here the fix does not require extending the scope class — it requires either adopting a new strategy once D-02 is resolved, or denormalising organisation_id onto the child tables (which ARCH §3 besluit 3 already commits to for form_submissions).
  • Proposed workstream: ws-4 (coupled to D-02). Note that charter §3 besluit 3 already commits to adding organisation_id to form_submissions — so FormSubmission is trivially fixable by the existing organisation_id strategy. The remaining 8 models still need D-02's extension.
  • Classification: scope-addition-ws-4

D-04 — Non-form-builder event-data models missing OrganisationScope

  • Location: ShiftAssignment.php, ShiftWaitlist.php, VolunteerAvailability.php, PersonSectionPreference.php, PersonIdentityMatch.php, UserInvitation.php, EmailChangeRequest.php, EmailLog.php, UserOrganisationTag.php, ImpersonationSession.php, MfaBackupCode.php, MfaEmailCode.php, TrustedDevice.php, UserProfile.php, OrganisationEmailSettings.php, OrganisationEmailTemplate.php.
  • Smell: Several of these are event-data (ShiftAssignment, ShiftWaitlist, VolunteerAvailability, PersonSectionPreference, PersonIdentityMatch) and should apply OrganisationScope via one of the existing strategies (festival_section_id, event_id on parents). Others (MfaBackupCode, MfaEmailCode, TrustedDevice, UserProfile, OrganisationEmailSettings/Templates, EmailLog, EmailChangeRequest, UserInvitation, ImpersonationSession) are user-scoped or organisation-scoped audit/admin data where the scope's suitability depends on intent (is this a per-user record, a per-org admin record, or a cross-org super-admin record?).
  • Why it matters this sprint: At minimum the five event-data models (ShiftAssignment, ShiftWaitlist, VolunteerAvailability, PersonSectionPreference, PersonIdentityMatch) are tenancy-leak-prone and in the hot path for WS-5 and WS-6 work. Fixing them now with existing strategies (festival_section_id via parent Shift; event_id on the model; or denormalised organisation_id for the Person-identity models) is low-cost and unblocks safer refactor work downstream.
  • Proposed workstream: ws-4 for event-data models. User/admin-scoped items: backlog with a separate ticket per model.
  • Classification: scope-addition-ws-4

D-05 — Verify Person has SoftDeletes (SCHEMA Rule 3 YES list)

  • Location: api/app/Models/Person.php — verify trait usage.
  • Smell: Rule 3 lists persons in the soft-delete-YES list. Most of the YES-list models were spot-checked OK. Person.php was not directly verified line-by-line during the scan — before WS-2 start, verify use SoftDeletes is present on both the model and as $table->softDeletes() in the create_persons_table.php migration.
  • Why it matters this sprint: If Person lacks soft delete but SCHEMA says it should have it, the ARTIST_ADVANCE / EVENT_REGISTRATION workflows (WS-6) cannot rely on documented recovery semantics.
  • Proposed workstream: ws-4 (trivial verification + fix if needed).
  • Classification: standalone-fix (verify first)

D-06 — activity_log morph columns lack (type, id) composite indexes

  • Location: api/database/migrations/2026_04_07_120000_create_activity_log_table.php:17, 19nullableMorphs() does not auto-index.
  • Smell: If any admin feature needs "show all activity for this person" or "what did user X do", the query is a full-scan on subject_type/causer_type.
  • Why it matters this sprint: Observability (WS-7) is likely to surface queries that need these indexes — better to add them while we're already migrating.
  • Proposed workstream: ws-7 (observability foundation likely drives the first such queries).
  • Classification: scope-addition-ws-7

D-07 — Morph map registers 6 owner types for form_schemas, only one (event) in practice

  • Location: api/app/Providers/AppServiceProvider.php:73122; usage: api/app/Http/Controllers/Api/V1/FilterRegistryController.php, PublicFormController.php.
  • Smell: event, user, user_profile, person, company, organisation are all registered as form_schemas.owner_type possibilities, but five of the six are never assigned anywhere. Charter §3 besluit 4 commits to seven purposes (EVENT_REGISTRATION, ARTIST_ADVANCE, SUPPLIER_INTAKE, POST_EVENT_EVALUATION, INCIDENT_REPORT, SIGNATURE_CONTRACT, USER_PROFILE) — so the registered types are forward-looking, not dead.
  • Why it matters this sprint: Not an architectural issue today. Documentation item for WS-8: explicitly link the morph-map entries to the purpose registry so that reviewers can see why each owner type is registered even when unused. Avoids a future "why is organisation in the morph map?" half-day of archaeology.
  • Proposed workstream: ws-8 (docs).
  • Classification: standalone-fix

D-08 — form_submissions.subject_type allow-list in two places

  • Location: api/config/form_subjects.php (used by StoreFormSubmissionRequest validation) vs api/app/Providers/AppServiceProvider.php:73122 (morph map).
  • Smell: Both files list the allowed subject types. They are expected to stay in sync, per a comment in AppServiceProvider, but the sync is enforced by developer discipline only — no test or compile-time guard.
  • Why it matters this sprint: WS-2 (purpose registry) is the natural place to consolidate. If purposes define their subject_type via value object (per charter §6.4), then both config/form_subjects.php and the morph map entries can be derived from the same source.
  • Proposed workstream: ws-2 (consolidate into purpose registry), or ws-8 (docs-only, if the de-dupe isn't done).
  • Classification: scope-addition-ws-2

Category D — summary

  • Tenancy-leak-prone models without scope: 14 (9 form-builder + 5 other event-data). Fixable within WS-4 provided OrganisationScope gets a form_schema_id strategy.
  • Scope class itself needs extension: 1 (D-02).
  • Indexes missing on framework polymorph: 1 (activity_log).
  • SCHEMA.md drift: ~30 tables documented without migrations — a WS-8 problem, not a WS-4 problem.
  • Duplication between config and morph map: 1 (D-08).
  • Verification needed before fix: 1 (Person soft-delete, D-05).

6. Scope implications

WS-2 — Purpose registry

  • Scope: confirmed + minor expansion.
  • Addition: D-08 — merge config/form_subjects.php into the purpose registry so that subject_type per purpose comes from one source. Lightweight.

WS-3 — Eén SPA consolidatie

  • Scope: unchanged by WS-1. Frontend-only workstream; this scan did not cover it.

WS-4 — ULID consistency + denormalized submission columns

  • Scope: significantly expanded.
  • Confirmed in initial list: pivot-ULID migration for A-01 through A-11 (if architect reaffirms besluit 5). organisation_user, event_user_roles, crowd_list_persons, event_person_activations were implied but not explicit; mfa_backup_codes, mfa_email_codes and user_organisation_tags are additions to the initial charter expectations. form_values and form_value_options carry the strongest inline-comment justification to keep — flag them as the first exceptions the architect must rule on.
  • Added: D-02 (extend OrganisationScope with form_schema_id strategy), D-03 (register scope on 9 form-builder child models), D-04 (register scope on 5 event-data non-form models), D-05 (verify Person soft-delete). Denormalised organisation_id on form_submissions (besluit 3) makes D-03 easier for FormSubmission specifically.
  • Risk: WS-4 as written was 2-3 days. With D-02/D-03/D-04 additions, expect 4-5 days.

WS-5 — JSON-kolom-opsplitsing

  • Scope: confirmed for WS-5a/b/c/d on form_fields. Open questions: B-05, B-06, B-07 (library-level JSON mirrors) and B-08 (role_restrictions) — architect to rule before WS-5 starts.
  • Minimum viable: if architect rules "library stays JSON, role_restrictions stays JSON", WS-5 is exactly the 4 committed splits.
  • Maximum scope: if architect rules "all three library columns + role_restrictions also split", that adds 4 more sub-workstreams to WS-5.

WS-6 — FormBindingApplicator

  • Scope: confirmed. No surprises.
  • Addition: when the applicator writes back to persons.custom_fields (B-17), document the shrinkage plan so B-17 can move from opaque-keep to deprecated-remove in a later sprint.

WS-7 — Observability foundation

  • Scope: confirmed + small addition.
  • Addition: D-06 — add (subject_type, subject_id) and (causer_type, causer_id) indexes on activity_log when the observability stack starts asking questions of the log.

WS-8 — Documentation consolidation

  • Scope: expanded significantly.
  • Addition D-01 is the big one. SCHEMA.md has ~30 tables documented without migrations. WS-8 must decide: (a) extract them into /dev-docs/ARCH-PLANNED-MODULES.md and reduce SCHEMA.md to built-only tables, or (b) add a status: planned marker per row. Either way WS-8 is no longer "rewrite v2.0 of SCHEMA.md" but "reduce SCHEMA.md to reality + separate planned-module doc".
  • Additions D-07, D-08 (partial), C-04: all docs/clarification work that lands naturally here.
  • Risk: WS-8 as written was 2-3 days. With D-01 added, expect 4-5 days; the "planned modules" document is non-trivial because it has to preserve the indexing/soft-delete intent SCHEMA.md captured.

Candidates for backlog or a new workstream

  • None require a new workstream — all findings map cleanly onto WS-2/4/5/7/8. D-02 might warrant a mini-WS ("scope extension") if the architect wants to solve the FK-chain problem generically rather than adding a form_schema_id strategy.

7. Questions for the architect

Ordered by how much they block downstream scoping; answer before WS-2 starts.

  1. Re-confirm or reverse ARCH §3 besluit 5 ("ULID overal, ook pivots") for the 11 non-ULID tables in Category A. Pivots with inline "int AI for join performance" comments (A-01 A-04) are straightforward pivot-exception candidates. form_values and form_value_options (A-10, A-11) cite ARCH §4.4 in the migration itself as performance rationale — is that rationale still valid, or overridden by the consolidation charter? Either answer is fine; both must be explicit.

  2. Does OrganisationScope grow a new strategy, or do we denormalise organisation_id onto every form-builder child table? D-02 + D-03 are the same problem from two angles. A new form_schema_id strategy is one migration to the scope class. Denormalising columns is ~8 migrations plus backfill. Pick one approach for WS-4.

  3. Scope of WS-5: four committed columns, or also the three library mirrors (B-05/06/07) and role_restrictions (B-08)? Keeping WS-5 to just form_fields is the minimum charter commitment; expanding to library + role_restrictions makes the form-builder relational story complete in one sprint but adds 3-4 days.

  4. personal_access_tokens.tokenable (C-04): add a morph-map alias for consistency, or leave Sanctum's default FQCN-in-DB pattern untouched? Cosmetic; pick a direction so WS-8 can document it.

  5. WS-8 SCHEMA.md rewrite: "built-tables-only + separate planned-modules doc", or "single file with status: planned markers"? D-01 means WS-8 has to pick a shape before starting the rewrite.

  6. form_submissions.subject_type allow-list (D-08): consolidate under WS-2 purpose registry, or leave the two-file pattern and add a CI test that keeps them in sync? Either works; WS-2 scope depends on the answer.