440 lines
41 KiB
Markdown
440 lines
41 KiB
Markdown
---
|
||
title: ARCH Consolidation — WS-1 Discovery Report
|
||
description: Read-only architecture scan output for the April 2026 consolidation sprint
|
||
status: draft
|
||
owner: architect
|
||
created: 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.php` — **does 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:18–19`. 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:73–122` 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: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 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:145–161` adds impersonation context.
|
||
- **Index on (type, id):** **no** — `nullableMorphs()` 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: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`, 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, 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`, `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: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_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.
|