41 KiB
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.mdWS-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), targetedReadon migrations/models/services, four parallelExploresub-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.php— does not useHasUlids. - 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— noHasUlids. - 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— noHasUlids. - 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— noHasUlids. - 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— noHasUlids. - 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— noHasUlids. - 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— noHasUlids. - 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_bindingstable 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
FormFieldRuleBuilderfor 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_rulestable 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_logictable. 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_optionstable 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.optionsinto 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'sform_field_bindingstable 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.settingsshould 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_datetyped columns; thevalueJSON 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(accessescustom_fields['section_preferences']) - Classification:
opaque-keep— but flag for WS-6 FormBindingApplicator: once bindings land,custom_fieldsbecomes 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:18–19. Model:api/app/Models/FormBuilder/FormSchema.php:92(owner()). - Allowed types (observed): morph map registers
event,user,user_profile,person,company,organisation. Onlyeventis 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:73–122viaRelation::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:20–21. 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 inFieldAccessService,FormSubmissionService(viamorphKeyFor()). - 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, 19vianullableMorphs(). - Allowed types (observed): ~23 domain models + 8 form-builder models, all registered in the single
enforceMorphMapcall. - Morph map status: fully registered;
Activity::savinghook inAppServiceProvider.php:145–161adds impersonation context. - Index on (type, id): no —
nullableMorphs()does NOT auto-create a(type, id)composite index, unlikemorphs()andulidMorphs(). The pair is unindexed on bothsubject_*andcauser_*. - 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:15viaulidMorphs('tokenable'). - Allowed types (observed): Sanctum default (
Userin 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
useralias 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
eventin 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
- §3.5.4:
- 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 asSTATUS: planned. Either way, SCHEMA.md cannot be trusted as-is during WS-4 through WS-7. - Proposed workstream:
ws-8(primary); also informsws-4scope 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 ofapi/app/Models/FormBuilder/FormSchemaWebhook.php:13–22: "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, viaevent_id, viafestival_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 viaform_schema_idor deeper — none of which the scope can resolve. TheFormSchemaWebhookcomment 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_idand possibly a genericthrough_fk_chainstrategy). Alternative:new-wsif the architect decides to solve this more broadly (e.g., declarative scope chain in models). - Classification:
scope-addition-ws-4(orneeds-architect-decisionfor 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,FormTemplatedo register it (directorganisation_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 denormalisingorganisation_idonto the child tables (which ARCH §3 besluit 3 already commits to forform_submissions). - Proposed workstream:
ws-4(coupled to D-02). Note that charter §3 besluit 3 already commits to addingorganisation_idtoform_submissions— soFormSubmissionis trivially fixable by the existingorganisation_idstrategy. 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 applyOrganisationScopevia one of the existing strategies (festival_section_id,event_idon 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_idvia parent Shift;event_idon the model; or denormalisedorganisation_idfor the Person-identity models) is low-cost and unblocks safer refactor work downstream. - Proposed workstream:
ws-4for event-data models. User/admin-scoped items:backlogwith 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
personsin the soft-delete-YES list. Most of the YES-list models were spot-checked OK.Person.phpwas not directly verified line-by-line during the scan — before WS-2 start, verifyuse SoftDeletesis present on both the model and as$table->softDeletes()in thecreate_persons_table.phpmigration. - Why it matters this sprint: If
Personlacks 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, 19—nullableMorphs()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:73–122; usage:api/app/Http/Controllers/Api/V1/FilterRegistryController.php,PublicFormController.php. - Smell:
event,user,user_profile,person,company,organisationare all registered asform_schemas.owner_typepossibilities, 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 isorganisationin 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 byStoreFormSubmissionRequestvalidation) vsapi/app/Providers/AppServiceProvider.php:73–122(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_typevia value object (per charter §6.4), then bothconfig/form_subjects.phpand the morph map entries can be derived from the same source. - Proposed workstream:
ws-2(consolidate into purpose registry), orws-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
OrganisationScopegets aform_schema_idstrategy. - 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 (
Personsoft-delete, D-05).
6. Scope implications
WS-2 — Purpose registry
- Scope: confirmed + minor expansion.
- Addition: D-08 — merge
config/form_subjects.phpinto the purpose registry so thatsubject_typeper 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_activationswere implied but not explicit;mfa_backup_codes,mfa_email_codesanduser_organisation_tagsare additions to the initial charter expectations.form_valuesandform_value_optionscarry the strongest inline-comment justification to keep — flag them as the first exceptions the architect must rule on. - Added: D-02 (extend
OrganisationScopewithform_schema_idstrategy), D-03 (register scope on 9 form-builder child models), D-04 (register scope on 5 event-data non-form models), D-05 (verifyPersonsoft-delete). Denormalisedorganisation_idonform_submissions(besluit 3) makes D-03 easier forFormSubmissionspecifically. - 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_restrictionsstays 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 fromopaque-keeptodeprecated-removein 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 onactivity_logwhen 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.mdand reduce SCHEMA.md to built-only tables, or (b) add astatus: plannedmarker 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_idstrategy.
7. Questions for the architect
Ordered by how much they block downstream scoping; answer before WS-2 starts.
-
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_valuesandform_value_options(A-10, A-11) citeARCH §4.4in 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. -
Does
OrganisationScopegrow a new strategy, or do we denormaliseorganisation_idonto every form-builder child table? D-02 + D-03 are the same problem from two angles. A newform_schema_idstrategy is one migration to the scope class. Denormalising columns is ~8 migrations plus backfill. Pick one approach for WS-4. -
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 justform_fieldsis the minimum charter commitment; expanding to library + role_restrictions makes the form-builder relational story complete in one sprint but adds 3-4 days. -
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. -
WS-8 SCHEMA.md rewrite: "built-tables-only + separate planned-modules doc", or "single file with
status: plannedmarkers"? D-01 means WS-8 has to pick a shape before starting the rewrite. -
form_submissions.subject_typeallow-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.