Commit Graph

104 Commits

Author SHA1 Message Date
831f36e618 docs(backlog): add TECH-TYPED-ROUTER-DRIFT
Triggered by the typed-router.d.ts regeneration in 3198698. Documents
three approaches (lefthook pre-commit, gitignore+postinstall, CI-check)
with their trade-offs. Defers selection to implementation time;
recommends bundling with the next pages-tree refactor (likely WS-3 PR-B).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-04 21:53:38 +02:00
617a6d2d13 docs(backlog): remove two closed tooling items
- TECH-VSCODE-STALE-ADMIN-ENTRY: closed in b9f8f558d1
- TECH-DELETE-DEAD-VIEWS: closed in bdbd5b0335

Both items shipped; references preserved in git history.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-04 21:43:15 +02:00
9cccbe08ce docs(ws3): record session 1c completion (boundaries enforcement)
- ARCH-CONSOLIDATION-2026-04.md: add Sessie 1c entry under WS-3 voortgang
- CLAUDE.md: add Frontend import boundaries section
- BACKLOG.md: add four follow-up tickets
  - TECH-AXIOS-STORE-COUPLING
  - TECH-DELETE-DEAD-VIEWS
  - TECH-WS3-BOUNDARIES-SUBZONES
  - TECH-WS3-BOUNDARIES-ROUTER-ZONE

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-30 23:52:15 +02:00
403be990f3 docs(ws3): add session 1c audit report (boundaries plugin)
Phase A deliverable per the WS-3 1c session prompt. Read-only audit
establishing the proposed eslint-plugin-boundaries matrix from
filesystem evidence in apps/app/src/.

Findings summary:
- 14 zones inventoried; views/ contains a single dead Vuexy file
  (zero importers) and is recommended for ignore alongside @core/@layouts.
- Proposed matrix refines the prompt's starting-point with three
  evidence-based additions:
  * composables → stores (2 actual usages: api composables read auth)
  * plugins → stores (1 actual usage: router guards read auth + org)
  * pages → layouts (forward-compat with §4.2 route-meta layout binding)
- Forward-compat verified vs ARCH-CONSOLIDATION-2026-04.md §4.2
  sub-zones — all resolve cleanly under the proposed pattern set.
- Plugin: eslint-plugin-boundaries@6.0.2, MIT, peerDeps eslint>=6
  (compatible with v8.57.1), as direct devDep per TECH-PORTAL-ESLINT-DEPS.
- Estimated violation count: 0 if Q1=yes (allow lib→stores), 2 if Q1=no.

Four open questions for Bert before Phase B sign-off:
- Q1 lib→stores: allow/refactor/extract-seam? (recommendation: allow)
- Q2 views/ ignored: confirm
- Q3 navigation imports scope: keep types+utils as headroom?
- Q4 sub-zone enforcement = backlog (TECH-WS3-BOUNDARIES-SUBZONES)?

No .eslintrc.cjs or package.json edits in this commit. STOP at Phase B
per the prompt — Phase C executes only after Bert's sign-off in chat.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-30 23:06:22 +02:00
37dac93da2 docs(backlog): record three tooling debt findings from WS-3 lint cleanup
- TECH-PORTAL-ESLINT-DEPS: audit apps/portal/package.json on the
     same missing-direct-deps pattern uncovered in apps/app (commit
     4369806). Cursor's strict ESLint resolution will hit identical
     issues for portal users until fixed.
   - TECH-ESLINT-V9-MIGRATION: ESLint v8.57.1 is EOL since end-2024.
     Migration to v9 + flat config + modern @antfu/eslint-config is a
     dedicated 1-2 day workstream.
   - TECH-VSCODE-STALE-ADMIN-ENTRY: .vscode/settings.json still
     references apps/admin in eslint.workingDirectories; removed in
     April 2026. Trivial cleanup.
2026-04-30 20:40:42 +02:00
5512e22f2b docs: WS-3 session 1b-iii complete — baseline 32 → 1
ARCH-CONSOLIDATION-2026-04.md §6.8: session 1b-iii recorded. Three
restpunten from 1b-ii resolved:
- indent SwitchCase: 1 (24 items in useTimeSlotDropdown.ts)
- lines-around-comment per-*.vue override (3 items in
  PortalLayout/PublicLayout/AppKpiCard)
- axios.ts async/await rewrite (2 promise/no-promise-in-callback
  warnings on lines 61, 73)

Lint baseline: 32 → 1. The remaining 1 item is a pre-existing
sonarjs/no-collapsible-if at useImpersonationStore.ts:103 — was
already in the 32 baseline (not specifically called out in 1b-iii's
three planned tasks per scope rules).

WS-3 lint cleanup workstream complete; session 1c
(eslint-plugin-boundaries) can proceed on a clean baseline.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 18:49:46 +02:00
4ea66d18f6 docs: WS-3 session 1b-ii complete — baseline 231 → 32
ARCH-CONSOLIDATION-2026-04.md §6.8: session 1b-ii recorded. All 16
audit action plan steps from session 1b-i executed:
- Q1: src/@core/** + src/@layouts/** added to ignorePatterns
- Q2: vue/no-restricted-class kept; 5 ml/pl-class occurrences renamed
  to ms/ps (LTR/RTL-aware Vuetify utilities)
- Q3: vue/prefer-true-attribute-shorthand kept; 2 occurrences rewritten
- Q4: axios fire-and-forget — \`void\` prefix per option a (note: empirically
  does not silence promise/no-promise-in-callback at the dynamic-import
  sites; 2 warnings remain for 1b-iii decision)
- Q5: pnpm lint decoupled from --fix (lint = no-fix; lint:fix = with --fix)

Lint baseline: 231 → 32 problems (30 errors, 2 warnings).

Two known restpunten (documented in 1b-ii commit messages):
- 24 indent items in useTimeSlotDropdown.ts: SwitchCase rule-config
  mismatch (codebase uses SwitchCase: 1, rule defaults to 0).
  Recategorised from Bucket A.1 trivial-fix to Bucket C rule-config.
- 2 promise/no-promise-in-callback warnings on axios.ts:61,73:
  Q4's literal \`void\` recipe doesn't satisfy the rule. Bert call
  for 1b-iii: keep \`void\` and accept warnings, or rewrite to
  async/await.

.claude-sync/ regenerated locally (gitignored — not in this commit).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 15:16:23 +02:00
e6d1e2c88a docs: WS-3 session 1b-i complete — baseline 1451 → 231
ARCH-CONSOLIDATION-2026-04.md §6.8: session 1b-i recorded.
Risk-tiered eslint --fix pass (Tier 1 + Tier 2 + whitespace) reduced
the apps/app baseline from 1451 to 231 problems. Tier 3 config files
deferred to 1b-ii under hand-reviewed conditions.

.claude-sync/ regenerated locally (gitignored — not in this commit).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 11:16:10 +02:00
f44bb969c9 docs: WS-3 session 1b-i lint baseline audit report
Categorises remaining apps/app eslint problems after the Tier 1 +
Tier 2 autofix and whitespace cleanup. Six buckets:
- X. Tier 3 deferred (vite.config.ts, themeConfig.ts, vitest.config.ts) — 134
- A. Trivial-fix (second-pass autofix residue + unused imports) — 42
- B. Type safety (mostly no-explicit-any in vendored Vuexy code) — 34
- C. Style preference / rule-config — 8
- D. Vuetify / Vuexy idiom (ml-/pl- restricted-class) — 5
- E. Bug-shaped (security, isAxiosError, missing-return, fire-and-forget) — 8
Sum check: 134 + 42 + 34 + 8 + 5 + 8 = 231 ✓

Five open questions for Bert + Claude Chat decisions before 1b-ii:
- Should src/@core/** + src/@layouts/** be in ignorePatterns?
- vue/no-restricted-class regex scope (RTL-aware vs ml/pl only)?
- Stance on vue/prefer-true-attribute-shorthand?
- Bucket E.5 axios fire-and-forget pattern (void / .catch / disable)?
- Decoupling pnpm lint from --fix?

Lint baseline: 1451 → 231 this session, with Tier 3 (134) +
non-fixable (66) + second-pass-residue (31) deferred to 1b-ii.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 11:15:16 +02:00
dd8430f600 docs: WS-3 session 1a — foundation gap closure recorded
VUEXY_COMPONENTS.md: three new named layouts documented (OrganizerLayout,
PortalLayout, PublicLayout) — listed alongside default/blank with the
"not yet wired to router" status.
ARCH-CONSOLIDATION-2026-04.md: WS-3 §6.8 marked with session 1a progress
(lefthook migration, three layout skeletons, vitest 41 -> 49).

.claude-sync/ regenerated locally (gitignored — not in this commit).
2026-04-29 08:51:01 +02:00
192353f4bc feat(form-builder): admin UI completion — server filters, KPIs, resource expansion (WS-6 sessie 3c)
Closes the four production gaps that emerged from sessie 3b's admin UI.
What we ship here is final: no further rework planned before production.

Backend
- IndexFailuresRequest validates state/search/failed_at_from/failed_at_to/
  listener_class. orgIndex + platformIndex apply them via a single
  applyIndexFilters() helper. Search runs case-insensitive `LIKE` on
  exception_message; SQL wildcards in user input are escaped.
- New /kpis aggregate endpoint per scope (orgKpis, platformKpis) returns
  open / resolved_30d / dismissed_30d / total_submissions in O(1) COUNTs.
  Replaces sessie 3b's client-side bucketing of an oversized list.
- Resource expansion: organisation_name, form_schema_label,
  resolved_by_user_name, dismissed_by_user_name, exception_trace,
  retry_history[]. Eager-loading via indexEagerLoads()/detailEagerLoads()
  prevents N+1 (verified by query-count assertion in test).
- New 2026_04_28_181000 migration adds exception_trace (longtext nullable)
  to form_submission_action_failures. ApplyBindingsOnFormSubmit listener
  now captures $e->getTraceAsString() at failure time.
- New FormSubmissionActionFailureRetryAttemptResource exposes per-attempt
  data (timestamp, actor name, outcome, exception details) inside
  retry_history[]. Index payloads omit the field via whenLoaded() to keep
  list responses lean.

Frontend (apps/app)
- Types updated to mirror the expanded resource shape and the new KPI
  endpoint contract. FormFailuresKpis is now { open, resolved_30d,
  dismissed_30d, total_submissions } (server-aggregate).
- useFormFailures composable forwards all 5 server filters via
  buildIndexParams() (strips empty/whitespace). useFormFailuresKpis hits
  the dedicated /kpis endpoint per scope.
- FormFailuresTable replaces client-side bucketing with server-side
  filtering, adds listener_class + date-range filter inputs, and renames
  the 4th KPI tile to "Submissions" (was "Totaal").
- FormFailureDetail renders organisation_name + form_schema_label in the
  header, surfaces an expandable stack-trace card, names the resolved/
  dismissed actor in the timeline, and replaces the "v1 placeholder"
  retry-history card with a full per-attempt timeline.

ESLint config gap (apps/app)
- New .eslintrc.cjs adapted from the Vuexy reference, minus Vuexy-internal
  rules. `pnpm lint` now runs successfully (was previously broken — the
  package.json script referenced a missing config). The 80 baseline
  violations across the codebase are pre-existing and out of scope for
  this session.

Tests + gates
- 24 new backend tests across filter, kpis, and resource-shape suites.
  Backend: 1462 → 1486 passing, 0 → 0 failing. Larastan clean. Rector
  dry-run unchanged at 354 (pre-Task-1 baseline from f18b55b).
- 3 new vitest tests in apps/app (filter wiring, KPI endpoint, KPI tile
  values from /kpis). Vitest: 38 → 41 passing. tsc clean. Portal
  unchanged (113 vitest, tsc clean).
- 5 backfill rollback tests bumped --step counts +1 for the new migration.
- Ws6FoundationMigrationTest down/up chain now includes exception_trace
  before the parent table is restored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:14:20 +02:00
b47e096a55 feat(form-builder): retry history table + integration (WS-6)
Per-attempt retry history (timestamp, user, outcome, exception detail
if failed) replaces the counter-only retry_count tracking.

Changes:

- New `form_submission_action_failure_retry_attempts` table (cascade on
  parent delete, nullOnDelete on user). Explicit short FK names
  (`fsafra_failure_fk`, `fsafra_user_fk`) — auto-generated names exceed
  MySQL's 64-char identifier limit.
- New FormSubmissionActionFailureRetryAttempt model + factory +
  succeeded() state.
- Parent FormSubmissionActionFailure gets retryAttempts() HasMany
  relation (latest('attempted_at')).
- New FormFailureRetryService centralises the retry-flow logic. Both
  the API controller and the artisan command delegate to it. Service
  writes a retry_attempt record per attempt; parent's retry_count
  stays as denormalised cache for index-view performance.
- Successful retry: attempt(succeeded) + parent.retry_count++ +
  parent.resolved_at + parent.resolved_by_user_id + parent.resolved_note
  ("Geslaagde retry door {actor.name}" or "Geslaagde retry
  (geautomatiseerd)" for command-line invocation without an actor).
- Failed retry: attempt(failed) with NEW exception details +
  parent.retry_count++. Parent's exception_class/_message stay
  audit-immutable — they represent the FIRST failure.
- canBeRetried() now correctly checks both resolved_at AND
  dismissed_at (sessie 2's open question Q2 closure).
- New FailureNotRetriableException (controller → 422) and
  ParentSubmissionGoneException (controller → 410) for cleaner
  flow control.

12 new tests:
- FormSubmissionActionFailureRetryAttemptTest (5 unit tests)
- RetryFlowProducesRetryAttemptsTest (7 integration tests covering
  succeeded path, failed path, resolved/dismissed blocking,
  multiple-retries chronological ordering, canBeRetried truth tables)

Pre-existing tests touched:
- FormSubmissionActionFailureTest::test_can_be_retried_only_for_open_state
  — updated to reflect Q2 closure (resolved now blocks too).
- Ws6FoundationMigrationTest::test_down_methods_clean_up_columns_and_table
  — child table must drop before parent (FK constraint).
- 5 backfill test step-counts bumped +1 (new migration sits at top).

SCHEMA.md → v2.9. Schema dump regenerated.

Refs: RFC-WS-6.md §3 Q5 addendum, sessie 2 Q2

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:14:19 +02:00
acd7cf5ec8 docs: ARCH-BINDINGS.md polish (WS-6)
End-to-end consistency review of the document. One polish edit
landed:

- §1 Scope listed "Person, Artist, Company, User" as binding-target
  entities. Sessie 3a.5 removed `artist` from the registry entirely
  (BACKLOG ARTIST-ADV-BINDING-MODEL); §1 now states "Person,
  Company, User" with a forward-pointer to the appendix that
  documents the v1 omission rationale.

Otherwise no polish needed:
- No stale renamed symbols in code examples (the two
  `dietary_preferences` mentions remaining are deliberate appendix
  content explaining the JSON-path BACKLOG deferral).
- No TODOs / FIXMEs.
- Cross-references to RFC §X / Q-ids are consistent.
- Terminology distinguishes registry (BindingTypeRegistry config +
  lookup), applicator (FormBindingApplicator runtime), and pipeline
  (the broader subject-resolve → conflict-resolve → apply flow)
  appropriately.

Version bumped to v0.6.

Refs: WS-6 sessie 3b Task 6

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:14:19 +02:00
ddacf9363e docs: ARCH-OBSERVABILITY skeleton + \$dontReport concrete (WS-6)
Initial observability architecture document. Skeleton with §3
(\$dontReport exception list) as the only concrete section. Other
sections are structured placeholders for WS-7 sessie 1 decisions:

  - §1 Logging strategy (log levels, criteria)
  - §2 Sentry decisions (SDK config, sample rates, breadcrumbs,
    release tagging)
  - §3 \$dontReport exceptions (concrete) — three classes that are
    expected business outcomes, not bugs:
      * PublishGuardViolationException (422 publish-time)
      * PurposeRequirementsNotMetException (422)
      * IdempotencyConflictException (409)
    With explicit out-of-scope rationale for the three runtime
    pipeline exceptions that DO go to Sentry (PersonProvisioning /
    PurposeSubjectResolution / FormBindingApplicator) — engineering
    needs cross-org visibility into systemic patterns even when
    org admins handle individual failures via the WS-6 admin UI.
  - §4 Structured logging conventions (key naming tree)
  - §5 Metrics (counters, histograms)
  - §6 Alerting rules (thresholds, routing)
  - §7 Dashboards (panel layout)

The skeleton ensures WS-7 starts from a clear scope; the concrete
\$dontReport list closes a real Sentry-noise gap immediately
(PublishGuardViolationException etc. should never have hit Sentry).

RFC-WS-6.md §9 cross-references the new doc and adds an
Observability follow-up row.

Refs: WS-6 sessie 3b Task 5, WS-7 (forward)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:14:19 +02:00
8a4682ab35 docs: BACKLOG + ARCH-BINDINGS appendix + RFC v1.2 for registry alignment (WS-6)
Two new BACKLOG entries capture the deliberate v1 deferrals:

  - ARTIST-ADV-BINDING-MODEL — design how artist_advance form data
    relates to Artist + AdvanceSection entities (when, if ever, an
    Eloquent Artist class is needed, and whether bindings are even
    the right abstraction for OUTPUT-shaped advance forms).

  - FORM-BINDING-JSON-PATH — extend binding registry to support
    JSON-path attributes (custom_fields.dietary_preferences etc).
    For v1 the recommendation is TAG_PICKER + tag_categories config.

ARCH-BINDINGS.md gets an appendix explaining the v1 scope decisions
explicitly: why 'artist' has no registry entries (model class
absent + advance forms are OUTPUT-shaped, not provisioning-shaped),
why JSON-path attributes are out of scope (v1 is column-level only),
and how BindingTypeRegistryConsistencyTest prevents future drift.

RFC-WS-6.md → v1.2 with a §3 Q9 addendum tracking the registry
alignment + the 3 renames, 5 removals, and 1 new column landed in
this branch.

Refs: WS-6 sessie 3a binding-target drift audit, sessie 3a.5 cleanup

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:14:17 +02:00
383b4fc5a3 feat(companies): add kvk_number column for B2B identity binding (WS-6)
WS-6 binding-target registry references company.kvk_number as a B2B
identity-key candidate. The column needed to exist on the model
before the registry could legitimately reference it. Nullable
because not every Company has a registered KvK (foreign companies,
partners, agencies); identity-key publish guards enforce presence
where required, not at schema level.

Changes:
- New migration `2026_04_28_140000_add_kvk_number_to_companies_table`
  adds nullable string column + index after `type`.
- Company::$fillable expanded.
- CompanyFactory generates an 8-digit KvK by default.
- CompanyKvkNumberTest covers attribute persistence, nullability,
  and information_schema-verified index existence.
- SCHEMA.md → v2.8 with the new column row + indexes line.
- Schema dump regenerated (CI fast-path).

Migration step counts in 5 backfill tests bumped +1 (the new
migration sits at the top of the migration stack):
  - FormFieldBindingMigrationTest:           18→19, 16→17
  - ConditionalLogicBackfillTest:             7→8
  - FormFieldConfigBackfillAndDropTest:      13→14
  - FormFieldOptionsBackfillTest:             3→4
  - FormFieldValidationRuleBackfillTest:     16→17

Refs: WS-6 sessie 3a binding-target drift audit

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:14:12 +02:00
ccc9dc905b docs: ARCH-BINDINGS.md § 8.2 IDOR class tests (WS-6)
Documents the IDOR-class threat model and the 404-vs-403
enforcement strategy implemented in WS-6 sessions 1-3a.

Two-axis policy enforcement:
  - Role-class (super_admin platform endpoints): 403 for unauthorised
    roles — endpoint exists; "you're not allowed in this room"
  - Ownership-class (org-scoped endpoints): 404 for cross-tenant
    access — resource indistinguishable from absence; "this room
    doesn't exist for you"

Includes:
  - Threat model: enumeration via ID sweeping
  - Policy implementation (canAccess + viewAnyInOrganisation,
    sessie 3a addition that closed the orgIndex gap)
  - Test coverage map: 24 tests in
    FormSubmissionActionFailureRouteSecurityTest
  - Edge case enumeration: soft-deleted parent, invalid ULID,
    non-existent ID, authenticated-without-role, unauthenticated
  - Forward pointer to sessie 3b for the frontend authorisation model

Refs: RFC-WS-6.md §4 V3, sessie 3a Tasks 1-2 commits
6b22c8d (security tests) and 842cb01 (per-purpose pipeline)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:14:12 +02:00
a791a276fa fix(form-builder): canonicalize JSON for byte-stable storage (WS-6)
MySQL 8.0 JSON columns may reorder associative-array keys on
round-trip. For audit-immutable values (schema snapshots, webhook
payloads, activity log diffs), this is corrupting: re-emits produce
different byte sequences for the same logical content.

Introduced JsonCanonicalizer (recursive ksort on associative arrays;
numeric-indexed lists preserve order) and applied at every writer
site that produces byte-stable JSON:

- FormSubmissionService: canonicalize the schema_snapshot array
  before storage (audit-immutable per ARCH §4.3, RFC-WS-6 v1.1).
- FormField::logFieldChange / FormSchema::logSchemaChange: canonicalize
  activity-log properties before withProperties() so old/new diffs
  read back byte-stable.
- BindingActivityLogger: canonicalize both the pass-level and
  per-binding activity properties.
- FormWebhookDispatcher: canonicalize payload_snapshot before
  storage (delivery-time HMAC re-encodes the same canonical bytes).
- DeliverFormWebhookJob: switched json_encode to
  JsonCanonicalizer::encode for the HMAC-signed body, so the
  signature is byte-stable across re-deliveries and reproducible by
  receivers from the same logical payload.

Sites NOT canonicalized (deliberate):
- form_schemas.settings — opaque UI config; key order has no
  semantic meaning, no byte-stability requirement.
- form_schemas.translations / form_fields.translations — read by
  display layer; key order doesn't matter.
- form_templates.schema_snapshot — user-supplied input via store/
  update; user is the source of truth, not audit-immutable in the
  same way as form_submissions.schema_snapshot.

Reverted the 7 assertEquals workarounds from session 2.6:
- ConditionalLogicActivityLogPayloadTest
- ConditionalLogicBackfillTest::test_rollback_reconstructs_canonical_json
- FormFieldBindingMigrationTest::test_rollback_reconstructs_json_and_drops_table
- FormFieldOptionServiceAndScopeTest::test_replace_options_emits_activity_log_on_field_only
- FormFieldOptionsActivityLogTest::test_field_updated_payload_contains_options_diff_when_options_change
- FormFieldOptionsBackfillTest::test_forward_migration_backfills_rows_strips_translations_and_rewrites_snapshot
- FormFieldOptionsSnapshotAndStrictRequestTest::test_submission_snapshot_embeds_rich_shape_options

Each now uses assertSame on JsonCanonicalizer::encode of both sides —
byte-stable comparison meaningful regardless of MySQL JSON storage
behavior.

New regression test SchemaSnapshotByteStableAcrossReemitsTest
exercises the contract end-to-end: complex schema with bindings,
validation rules, options, conditional logic, submitted; reads
schema_snapshot via three roads (Eloquent cast, fresh model, raw
bytes) and asserts the canonical encode is identical.

ARCH-FORM-BUILDER.md §4.6.1 gets a "Byte-stability" sub-section
explaining what's canonicalized and why.

Test count: 1388 → 1400 (+11 JsonCanonicalizer unit, +1 snapshot
regression). Larastan clean. Rector dry-run unchanged at 355.

Refs: WS-6 session 2.6 deviation #4 cleanup, RFC-WS-6 v1.1

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:11:18 +02:00
85f4777e0c docs(ws-6): RFC-WS-6 v1.1 addenda + ARCH-BINDINGS §6.4 alignment
Promote RFC-WS-6 to v1.1 with two §3 addenda capturing the post-session-2
cleanup decisions; align ARCH-BINDINGS.md §6.4 (Person provisioning)
with the v1.1 text. No architectural reversals — corrections + one
schema addition.

§3 Q8 v1.1 addendum — Person provisioning is scoped by `event_id`:
- Q8 v1.0 said `Person::firstOrCreate(['email', 'organisation_id'], ...)`.
  That is incorrect against the actual model: `Person::$organisationScopeColumn`
  is `event_id`. The provisioner looks up and creates by `(email, event_id)`.
- Same email registering across two events in the same org → two distinct
  Person rows. Cross-event identity reconciliation remains the job of
  `PersonIdentityService` (out of scope WS-6).
- Failsafe: `PersonProvisioningException('no_event', ...)` when
  `submission.event_id` is null on event_registration; publish guard
  `SchemaHasLinkedEvent` blocks at config time.

§3 Q9 v1.1 addendum — `form_schemas.default_crowd_type_id` replaces
`CrowdType::oldest()`:
- Session 2's PersonProvisioner used a silent oldest()-in-org heuristic
  for the new Person's `crowd_type_id` (NOT NULL). Fragile, undocumented,
  cross-org broken.
- v1.1 adds `form_schemas.default_crowd_type_id` (nullable ULID) as the
  explicit, versioned schema attribute. `RequiresDefaultCrowdType` publish
  guard wires into `EventRegistrationGuards`. Runtime failsafe in
  `PersonProvisioner::resolveCrowdTypeId()` throws
  `PersonProvisioningException('no_default_crowd_type', ...)` when null.
- Schema-level FK omitted intentionally (SQLite cascade-delete on
  ALTER TABLE ADD FOREIGN KEY observed in WS-5b/c backfill tests).
  Application-level integrity (publish guard + runtime failsafe +
  Eloquent `belongsTo`) is sufficient because writes always go through
  `FormSchemaService::publish()`.
- Snapshot impact: none. Provisioning reads from live FormSchema by
  FK; audit replay uses whatever the schema's current
  `default_crowd_type_id` is at retry time.

ARCH-BINDINGS.md §6.4:
- Now references "RFC Q8 + Q9, v1.1" in the heading.
- Default-crowd-type bullet replaces "first active CrowdType in the org"
  (the session-2 oldest() heuristic) with the schema attribute lookup.
- Multi-tenancy paragraph clarified for cross-event scoping.

Cross-references touched up:
- `PersonProvisioner::resolveCrowdTypeId()` docblock: §3 Q8 → §3 Q9.
- `RequiresDefaultCrowdType` class docblock: §3 Q8 → §3 Q9.
- `SCHEMA.md` v2.7 changelog and `default_crowd_type_id` column note:
  §3 Q8 → §3 Q9.

Document history entry added in §10 documenting v1.1 + the snapshot
dual-key cleanup and route-model-binding fix landed in earlier commits
on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 09:09:41 +02:00
d2059e3cff feat(form-builder): per-schema default_crowd_type_id replaces silent oldest() heuristic (WS-6)
Session 2's PersonProvisioner picked CrowdType::oldest() for the org —
silently wrong for multi-crowd_type orgs (Volunteer + Crew + Press are
three distinct crowd_types in one org). Schemas now declare their
target crowd_type explicitly via form_schemas.default_crowd_type_id.
RequiresDefaultCrowdType publish guard prevents misconfigured
event_registration schemas from publishing.

PersonProvisioner: oldest() fallback removed entirely. Misconfiguration
throws no_default_crowd_type at runtime; publish guard prevents it at
config time.

Migration uses a plain ulid() column without DB-level FK because
SQLite's table-rebuild on ALTER ADD FOREIGN KEY cascade-deletes
form_fields rows (form_fields.form_schema_id has cascadeOnDelete on
form_schemas). Application-level integrity via FormSchema::defaultCrowdType()
belongsTo + the publish guard + the runtime failsafe — three load-bearing
checks, none of which require the DB-level constraint.

Three pre-existing migration backfill tests bumped step counts +1 to
account for the new migration sitting between WS-5c and WS-5d:
FormFieldBindingMigrationTest (16→17, 14→15), FormFieldConfigBackfillAndDropTest
(11→12), FormFieldValidationRuleBackfillTest (14→15),
ConditionalLogicBackfillTest (5→6).

Six event_registration test fixtures updated to set default_crowd_type_id
to satisfy the new publish guard.

FormBuilderDevSeeder.resolveDefaultCrowdTypeId() — VOLUNTEER → first-active
→ create-as-needed fallback chain; documented contract for future seeders.

SCHEMA.md updated to v2.7.
Refs: RFC-WS-6.md v1.1 §3 Q8 addendum (Task 4 of this session)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:47:32 +02:00
1fdd254a8a docs: complete ARCH-BINDINGS.md sections 6-9 from session 2 work (WS-6)
Sections 6 (apply pipeline), 7 (failures and retry), 8 (multi-tenancy
and security tenant resolution), 9 (listener chain) populated from
session 2 implementation. Each subsection 200-400 words referencing
RFC-WS-6.md sections by number.

§8.2 (IDOR class tests) and frontend-specific sections in §3 admin UI
remain pending session 3.

Refs: RFC-WS-6.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 17:02:48 +02:00
c6a8d13b6f docs: add WS-6 Deferred backlog items (WS-6)
Five backlog items tracking explicit out-of-scope decisions from
RFC-WS-6.md §6.

Refs: RFC-WS-6.md §6, §9

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 00:05:35 +02:00
7f99783d8a docs: add ARCH-BINDINGS.md skeleton with foundation sections complete (WS-6)
Sections 1-5, 10, 11 written in full. Sections 6-9 stubbed with
session-2/3 markers and RFC references. Out-of-scope items §10
explicit.

Refs: RFC-WS-6.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 00:04:53 +02:00
c033dc6cd2 feat(form-builder): add apply_status columns and action-failures table (WS-6)
- form_submissions: apply_status (nullable, NO default for legacy rows
  per RFC O1), apply_completed_at, indexed on (form_schema_id, apply_status)
  and (organisation_id, apply_status)
- form_submission_action_failures: ULID PK, FK to submission + binding,
  resolve/dismiss state separated (RFC V2), retention via parent
  cascade-delete
- Migration rehearsal test added (invokes down() directly because the new
  migrations land between WS-5a and WS-5b chronologically, not at the tail
  of the migration list)

Three pre-existing WS-5 backfill tests also bump their --step rollback
counts by +2 (FormFieldBindingMigrationTest, FormFieldConfigBackfillAndDropTest,
FormFieldValidationRuleBackfillTest) to account for the two new migrations
sitting in the chronological middle of the WS-5 stack — required to keep
those tests' pre-WS-5b rollback target reachable.

SCHEMA.md updated to v2.3.
Refs: RFC-WS-6.md §3 (Q4, Q5), §4 (V2), §5 (O1)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:33:39 +02:00
47a0dc875b docs: add RFC-WS-6 architectural anchor for binding pipeline
Captures the 13 design decisions, 4 refinements, and 3 observations
from the 2026-04-25 architectural session. Authoritative for sessions
1-3 of WS-6. Out-of-scope items explicitly listed in §6.

Refs: ARCH-CONSOLIDATION-2026-04.md §6.2, ARCH-FORM-BUILDER.md §31

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:19:42 +02:00
f7ddc1b3ce docs: close base scope-class extraction follow-up (post-WS-5)
Reflects the FormFieldChildTableMorphScope extraction landed in the
previous commit:

  - ARCH-FORM-BUILDER.md v1.9 — five locations updated:
      §6.7 (Relational binding table) — added forward reference
        sentence after the FormFieldBindingScope escape-hatch line
        (WS-5a was the first scope; previously had no deferral note
        because nothing existed yet to defer)
      §17.4.2 (Relational table form_field_validation_rules) —
        "deferred to WS-5d per addendum Q3" replaced with marker-
        subclass forward reference
      §17.5.3 (Service, scope, cascade — config) — same replacement
      §17.6.1 (Field options rationale) — "unblocks the deliberate
        follow-up" replaced with completion-confirmation
      §17.6.3 (Service / scope / cascade — option) — "deferred to a
        follow-up work package" replaced with marker-subclass forward
        reference + Phase A diff verification result
    Version metadata + changelog updated; v1.8 prose preserved in the
    Previous-versions block.

  - ARCH-CONSOLIDATION-ADDENDUM-2026-04-24.md — new
    "Uitvoering — base scope-class extractie (2026-04-25)" section
    inserted after the WS-5d Uitvoering, documenting the Phase A
    diff-verification, marker-subclass approach, private→protected
    YAGNI policy, the inline-FQN → use-statements stylistic refinement,
    static-analysis impact (Larastan baseline clean, Rector
    357 → 355), and net-diff figures.

  - BACKLOG.md — FORM-BUILDER-MORPH-SCOPE-BASE-CLASS item closed
    via strikethrough header + "Status: closed 2026-04-25" annotation
    (matches the TECH-TS-PORTAL-TSC closure convention from earlier
    this week).

  - SCHEMA.md — three stale "deferred" claims updated to reflect the
    completed extraction:
      header v2.6 changelog mention rewritten to point at the now-
        landed FormFieldChildTableMorphScope
      form_field_validation_rules table-section global-scope note
        replaced with marker-subclass forward reference
      form_field_options table-section global-scope note same
        replacement
    Schema version NOT bumped — no actual schema change.
    The two other scope mentions (form_field_bindings,
    form_field_configs) made no deferral claims and remain accurate.

Note: the work package's prose listed "§6.7 / §17.4.3 / §17.5.3 /
§17.6.3" as deferral-note locations. The actual locations were
§17.4.2 (not §17.4.3), §17.5.3, §17.6.1 (not just §17.6.3), and
§17.6.3 — §6.7 had no deferral note (WS-5a was the first scope,
nothing to defer yet). All five spots updated in line with the work
package's intent.

WS-5 family fully complete: no open follow-up items remain under the
"delete > adapt" discipline of the WS-5 refactor.

Tests: 1208 passed (3260 assertions). No code changes in this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 04:52:01 +02:00
81b20ecbea chore: close TECH-TS-PORTAL-TSC — apps/portal vue-tsc at zero
Brings apps/portal from 729 vue-tsc errors (≈22 own-code, of which
18 were TS2339 downstream of tiptap; ≈707 in node_modules/@tiptap/)
to zero. Tiptap fix-route: Option A — patch upgrade @tiptap/* from
2.27.1 to 2.27.2 to fix tiptap's dist/index.d.ts re-export paths.

Sprint commits (this work package, 3 total):
  - f7bb864 fix(portal-deps): upgrade @tiptap/* 2.27.1 → 2.27.2
            to fix dist resolution (cleared 707 + 18 errors)
  - a7ccd2b fix(portal-types): clear residual long-tail tsc errors
            (cleared the 4 tiptap-independent stragglers:
             vite.config.ts componentName param,
             LayoutConfig.title Lowercase<string> over-constraint,
             @iconify/types missing dev-dep, casl.ts meta string cast)
  - this commit: close BACKLOG entry; correct the misleading "+4 in
                 tiptap" framing in the original entry (was the
                 ts-reset delta, not the absolute pre-existing count
                 of ~707); seed TECH-PORTAL-TSC-CI-GATE follow-up.

apps/portal `pnpm exec vue-tsc --noEmit` exits clean.
Vitest: 113/113 passing. Build: 8.52s, succeeded.

Pre-commit hook gate not added — the project has no husky/lefthook/
simple-git-hooks setup. Captured as TECH-PORTAL-TSC-CI-GATE follow-
up; without that gate the zero state has no enforcement and can
drift back. Should land before S3b organizer UI work to keep the
"new code introduces no new errors" discipline mechanically
enforceable.

S3b form-builder organizer UI can now land on top of a verified
TypeScript baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 04:35:43 +02:00
e5d86776b2 docs: sharpen TECH-TS-PORTAL-TSC + TECH-APP-VITEST priority and scope
Both items currently sit at default priority. Foundation-tooling
commit 3 (ts-reset install) surfaced them but didn't constrain
when they need to close.

Adds explicit S3b-trigger rationale: launching the form-builder
organizer UI in apps/app on top of:
  - 22 unverified pre-existing TypeScript errors in apps/portal,
    AND
  - zero Vitest setup in apps/app
is asymmetric quality, exactly the discipline gap that bites in
post-launch debugging. Both items now flagged "high before S3b
lands" with concrete close-criteria.

TECH-TS-PORTAL-TSC additionally clarifies tiptap node_modules
handling. Phase A check confirmed `skipLibCheck: true` is already
set in both SPAs' single tsconfig.json (no `tsconfig.app.json` /
`tsconfig.node.json` variants exist). Despite that, the 4 tiptap
errors persist because tiptap ships uncompiled `.ts` source files
in `node_modules/.../@tiptap/core/src/`, not `.d.ts` — and
`skipLibCheck` only suppresses checking of `.d.ts` files. Real
fix paths are upstream `@tiptap/*` upgrade (newer majors may ship
`.d.ts` only) or a focused `exclude` glob; flipping skipLibCheck
is a non-fix because it is already on.

TECH-APP-VITEST adds a 6-step setup outline scoped to "harness
exists + one test passes", explicitly excluding comprehensive
test-writing for existing apps/app code. `useImpersonationStore.ts`
called out as a natural early target — it has no runtime test
today and the pending TECH-TS-IMPERSONATION shape-validation work
benefits from coverage.

CLAUDE.md quality-gates list adds a Vitest entry that surfaces
the apps/portal / apps/app asymmetry, with a pointer to
TECH-APP-VITEST.

No code changes. Documentation only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 04:11:16 +02:00
f38c7ece97 chore: install laravel telescope as dev-only debugging dashboard
Installs laravel/telescope ^5.0 (v5.12.5) as a dev-dependency.
Three-layer production safety adapted to Laravel 11 layout (no
Kernel.php; routing/schedule in bootstrap/app.php +
routes/console.php):

  1. composer.json `extra.laravel.dont-discover` lists
     laravel/telescope. After editing, `php artisan package:discover`
     regenerates bootstrap/cache/packages.php — without this step
     the auto-discovery cache still registers the vendor provider.
  2. AppServiceProvider::register() gates registration to local +
     testing environments. Registers BOTH the vendor
     Laravel\Telescope\TelescopeServiceProvider (routes, migrations,
     publishing) AND the project's App\Providers\TelescopeService
     Provider (gate + filter) — they're sibling classes that extend
     ServiceProvider independently, not parent/child, so both must
     register for the dashboard to work. bootstrap/providers.php
     deliberately does NOT list either Telescope provider.
  3. .env TELESCOPE_ENABLED flag (false in .env.example). Runtime
     toggle that disables Telescope even when the providers are
     registered.

Production safety verified via simulated APP_ENV=production check:
confirms no Telescope-* providers are loaded.

Authorization: viewTelescope gate restricts dashboard to users
with the super_admin Spatie Permission role. Even in local
environments, only super_admin can view. Default was an email
allow-list stub — replaced with `$user->hasRole('super_admin')`.

Pruning: Schedule::command('telescope:prune --hours=48') added in
routes/console.php (Laravel 11's schedule location), environment-
gated to local + testing only.

Documentation: /dev-docs/TELESCOPE.md added; CLAUDE.md gets a
Development-tooling section. The doc explicitly calls out the
dual-provider registration (vendor + app) which differs from the
single-provider pattern in older Laravel versions.

Migrations applied: telescope_entries, telescope_entries_tags,
telescope_monitoring tables. Route registration verified in local
(42 telescope.* routes).

Tests: 1208/1208 passing — Telescope loads in the testing
environment as well, so the suite exercised it without issues.

Deployment note (flag for separate docs): a production operator
who runs `php artisan migrate` manually will still apply the
Telescope migrations — but because the providers never register
in production, the tables stay empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 04:03:31 +02:00
5771a678ef chore: install ts-reset in both portal and app SPAs
Installs @total-typescript/ts-reset 0.6.1 as a dev-dependency in
apps/portal/ and apps/app/. Patches TypeScript's loosest default
types: Array.filter(Boolean) returns non-nullable, JSON.parse
returns unknown, fetch().json() returns unknown, Map.get() strict,
etc.

Configuration: src/reset.d.ts in each SPA imports the reset. Both
tsconfig.json files already include ./src/**/* so the .d.ts is
picked up automatically — no tsconfig edits needed.

Issues surfaced during install:
  - apps/app — 0 pre-install tsc errors in own code; install
    surfaced 2 errors in src/stores/useImpersonationStore.ts
    (both from JSON.parse on sessionStorage content returning
    unknown instead of any). Fixed inline at lines 19 + 123 via
    `as ImpersonationState` casts that make the existing
    trust-in-sessionStorage explicit. Backlog entry
    TECH-TS-IMPERSONATION tracks proper runtime shape validation.
  - apps/portal — 22 pre-existing tsc errors in own code (mostly
    tiptap editor components — tracked as TECH-TS-PORTAL-TSC,
    unrelated to ts-reset). Zero new errors in portal's own code.
    4 additional errors surfaced in tiptap's uncompiled node_modules
    .ts sources (third-party); left as-is.

Neither SPA achieves `tsc --noEmit` clean today — pre-existing
state unrelated to this work package. Build + vitest are the
actual working gates and both remain green:
  - apps/portal: vitest 113/113 passing; production build succeeds
  - apps/app:    (no vitest setup — tracked as TECH-APP-VITEST);
                 production build succeeds

Documentation: /dev-docs/FRONTEND-TOOLING.md added; CLAUDE.md
quality-gates updated.

Backlog: TECH-TS-IMPERSONATION (runtime validation of stored
impersonation state), TECH-TS-PORTAL-TSC (pre-existing portal tsc
errors), TECH-APP-VITEST (Vitest coverage for apps/app).

No production behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 03:58:11 +02:00
a043b88bc0 chore: install rector with accept-current-state baseline
Installs rector/rector ^2.0 (v2.4.2) + driftingly/rector-laravel
^2.0 as dev-dependencies. Configures PHP 8.2 language sets + safe
quality rule sets (CODE_QUALITY, DEAD_CODE, EARLY_RETURN,
TYPE_DECLARATION, PRIVATIZATION) + Laravel-specific sets
(LARAVEL_CODE_QUALITY, LARAVEL_COLLECTION).

Dry-run baseline: 487 rule-applications across 357 files. NO
changes applied in this commit — adoption is incremental via per-
set sprints documented in BACKLOG.md.

Top rules by volume:
  103  AddClosureVoidReturnTypeWhereNoReturnRector
   71  AddArrowFunctionReturnTypeRector
   51  AppToResolveRector
   34  ConvertStaticToSelfRector
   27  ReadOnlyClassRector
   18  NullToStrictStringFuncCallArgRector
   16  ReturnBinaryOrToEarlyReturnRector
   16  MakeModelAttributesAndScopesProtectedRector
   13  RemoveUnusedVariableAssignRector
   13  OptionalToNullsafeOperatorRector
   13  FlipTypeControlToUseExclusiveTypeRector

Composer scripts:
  - composer rector              — DRY-RUN (default)
  - composer rector:apply        — apply changes
  - composer rector:clear-cache  — clear Rector cache

Dry-run exits with code 2 when suggestions exist (Rector convention,
not an error state). Apply-mode exits 0 on clean runs.

Documentation: /dev-docs/RECTOR.md added; CLAUDE.md updated.

Backlog: per-set application sprints seeded
(TECH-RECTOR-01..05 + TECH-RECTOR-CI). DEAD_CODE (smallest scope)
and TYPE_DECLARATION (biggest volume, will help reduce Larastan
baseline) are the natural first two.

Disruptive sets deliberately deferred:
  - LaravelLevelSetList::UP_TO_LARAVEL_* — broad bulk upgrades
  - SetList::NAMING — high-churn variable renames
  - SetList::INSTANCEOF — substantial logic changes

Memory limit 2G (dry-run completed within it).

No production behavior change. No code modified — Rector ran
dry-run only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 03:50:41 +02:00
7542808cab chore: install larastan at level 6 with accept-all baseline
Installs larastan/larastan ^3.0 (v3.9.6) as a dev-dependency. Level
6 is the starting target — catches missing typehints, method-
existence, null-safety, and model-property existence. Level 8
deferred to a follow-up sprint after level-6 baseline reaches zero.

Baseline error count at install: 1556 errors across 678 analysed
files (41 distinct identifiers).

Top 10 identifiers (errors / files):
  613 /  87  property.notFound
  289 /  52  missingType.generics
  154 /  31  argument.templateType
   98 /  61  missingType.iterableValue
   77 /  32  argument.type
   50 /  26  method.notFound
   35 /  35  method.childReturnType
   32 /   9  method.unresolvableReturnType
   31 /  10  assign.propertyType
   28 /  17  instanceof.alwaysTrue

Composer scripts:
  - composer analyse              — run static analysis
  - composer analyse:baseline     — regenerate baseline
  - composer analyse:clear-cache  — clear PHPStan result cache

Config deviation from plan: checkGenericClassInNonGenericObjectType
was removed in PHPStan 2.x (which Larastan 3 bundles) — setting
dropped from phpstan.neon, otherwise config matches the work
package verbatim. Defaults cover the original intent.

Documentation: /dev-docs/LARASTAN.md added; CLAUDE.md quality-gates
section introduced (with PHPUnit + Pint + Larastan listed).

Backlog: /dev-docs/BACKLOG.md gets 10 per-identifier reduction
sprints (TECH-LARASTAN-01..10) seeded from the actual baseline top
categories, plus TECH-LARASTAN-CI and TECH-LARASTAN-L8 follow-ups.

Memory limit 2G (baseline generation completed within it).

No production behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 03:46:27 +02:00
e7c9482474 refactor(form-field): drop form_fields.options + form_field_library.options
Final WS-5d cleanup. The JSON columns that have been unread since
commit 3 are now physically dropped on both source tables. Their
canonical rich-shape lives in form_field_options, accessed
exclusively through the morphMany relation.

Defensive sweep: any lingering translations.{locale}.options key in
either source table's translations bag is stripped. Commit 2's
backfill should already have done so exhaustively; this is
belt-and-braces.

Rollback re-creates the columns as nullable JSON but leaves them
empty. Pair with commit 2's rollback to restore the pre-WS-5d data
shape on every owner row.

The commit-3 getOptionsAttribute accessor-bridge on FormField +
FormFieldLibrary is removed — Eloquent's getAttribute() resolution
now naturally falls through to the morphMany relation since there's
no underlying column to shadow it. New regression test
FormFieldOptionsAccessTest asserts $field->options resolves to an
Eloquent Collection of FormFieldOption instances and lazy-loads in
exactly 2 queries (1 parent + 1 lazy-load options) on a fresh fetch
without with() preload. Same trio for FormFieldLibrary.

Migration step-count tests in WS-5a/b/c bumped by 1 to account for
the new drop_form_field_options_json_columns migration on the
rollback stack.

Documentation:
  - SCHEMA.md v2.6: form_field_options table documented; options row
    removed from form_fields and form_field_library; morphMany
    relations updated; cross-references to ARCH-FORM-BUILDER §17.6
    and addendum §Q3 WS-5d Uitvoering added on both source-table
    docblocks.
  - ARCH-FORM-BUILDER.md v1.8: new §17.6 "Field options (relational)"
    mirrors the §17.4 / §17.5 relational-sibling structure with
    sub-sections 17.6.1 rationale, 17.6.2 table + catalogue, 17.6.3
    service / scope / cascade / activity log, 17.6.4 snapshot
    embedding, 17.6.5 external API contract. Existing Webhooks
    section renumbered from §17.6 to §17.7.
  - ARCH-CONSOLIDATION-ADDENDUM-2026-04-24.md: "Uitvoering — WS-5d
    (2026-04-27)" section added. Eight paragraphs covering the
    snapshot atomic rewrite, strict-fail backfill dispatch, dual
    activity-log emit, four-sibling base-class extraction warrant,
    commit 0 dead-code precondition, the temporary getOptionsAttribute
    accessor-bridge pattern (with reusability note for future
    JSON→relational refactors), the dev-seeder vergoedingstype RADIO
    normalisation (drift correction explicitly distinguished from the
    parallel apps/app RegistrationFieldTemplate description domain),
    and the WS-5 family completion note.
  - BACKLOG.md: FORM-BUILDER-LIBRARY-AUDIT-LOG entry extended to four
    services (adds library.options_replaced); new
    FORM-BUILDER-MORPH-SCOPE-BASE-CLASS entry added as the WS-5d
    follow-up now that all four concrete morph-scope siblings exist.

Tests: 1193 → 1208 green (+15 across commits 3+4+5; this commit alone:
+2 from the regression test).

This completes the WS-5 family.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 03:00:20 +02:00
9e181092fc docs(form-builder): WS-5c sign-off — SCHEMA v2.5 + ARCH v1.7 §8 + addendum Q3
SCHEMA v2.5:
- form_fields: conditional_logic row removed; cross-reference note
  added pointing at the two new tables and the addendum Q3 WS-5c
  Uitvoering (no library mirror).
- New sections: form_field_conditional_logic_groups (tree nodes,
  adjacency-list via parent_group_id) and
  form_field_conditional_logic_conditions (leaves; value JSON
  nullable for empty/not_empty). Both tables use the Q2 declarative
  FK-chain resolver via tenantScopeStrategy() — group chain 3 hops,
  condition chain 4 hops (fits the WS-5c-raised cap of 5).

ARCH v1.7 §8 restructured into sub-sections mirroring the §17.4 /
§17.5 pattern:
- 8.1 Tree structure (read-side contract)
- 8.2 Relational tables (column specs, cascade, scope)
- 8.3 Service boundary (logicFor/replaceLogic/toJsonShape/
  assertSpecsValid/assertNoCycles)
- 8.4 Operator catalogues (group + comparison)
- 8.5 Cycle detection (contract preserved, implementation moved)
- 8.6 Activity log (dual-events: field.updated +
  field.conditional_logic_replaced; FormField subject only)
- 8.7 Legacy JSON migration (strict dispatch, rollback reversible)

Addendum Q3 extended with "Uitvoering — WS-5c (2026-04-26)":
- No-library-mirror decision reaffirmed (simple FK, no morph)
- Two-table tree-structure rationale (groups + conditions semantic
  purity over single-table mixed-nullables)
- OrganisationScope cap raise 3 → 5, rationale: legitimate 4-hop
  conditions chain + headroom for future deeper trees without
  denormalising form_field_id onto conditions
- Cycle detection migrated to service, contract unchanged
- Snapshot + resource JSON contract byte-identical via toJsonShape
- Strict validator on save at FormRequest boundary
- Scope-sibling discipline: WS-5c adds two FK-chain models (not
  morph); base-class extraction still parked for WS-5d

Sign-off table: WS-5c afronding 2026-04-26 added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 00:09:12 +02:00
4fcff2367a docs(backlog): open FORM-BUILDER-LIBRARY-AUDIT-LOG — library-level activity-log gap surfaced during WS-5b review 2026-04-24 22:51:09 +02:00
d494478c08 feat(form-builder): form_field_configs relational table + non-validation key split + drop validation_rules JSON columns 2026-04-24 22:42:35 +02:00
9d2758a42c docs(form-builder): WS-5b partial sign-off — SCHEMA v2.3 + ARCH v1.5 §17.4 + addendum Q3 2026-04-24 22:30:17 +02:00
67bede2e49 docs(form-builder): WS-5a follow-up — §6.2 registry, §6.7 dual activity-log note, Q3 commit-3 caveat 2026-04-24 20:51:28 +02:00
60c3abbe26 docs(form-builder): WS-5a — SCHEMA v2.2 §3.5.12, ARCH v1.4 §6.7, addendum Q3 sign-off 2026-04-24 20:13:51 +02:00
fe2202d835 test(persons): verify SoftDeletes behaviour per WS-1 finding D-05
Pre-flight audit verified the implementation was already in place,
so this commit is test-only (as the task prompt allowed):
- api/database/migrations/2026_04_08_220000_create_persons_table.php:27
  already carries $table->softDeletes().
- api/app/Models/Person.php lines 18 + 24 already use the SoftDeletes trait.

No production code change required.

tests/Feature/Person/PersonSoftDeleteTest.php pins the documented
behaviour so WS-6 (FormBindingApplicator + ARTIST_ADVANCE /
EVENT_REGISTRATION workflows) can rely on it without a second
verification pass:
- delete() sets deleted_at and excludes the row from default queries
- Person::withTrashed()->find($id) returns the soft-deleted row
- Person::onlyTrashed()->count() reports accurately
- restore() clears deleted_at
- forceDelete() removes the row permanently

PersonPolicy was spot-checked alongside the audit; existing view/update/
delete methods do not invoke withTrashed() on controller resolution.
That's acceptable today — no caller relies on editing a trashed Person —
so AUTH_ARCHITECTURE.md is left unchanged. If a restore-workflow lands
in a future sprint, the controller binding will need to switch to
->withTrashed() and the behaviour deserves a dedicated doc paragraph
then.

Minor path note: the task prompt specified
tests/Feature/Persons/PersonSoftDeleteTest.php (plural), but the
existing convention in this repo is tests/Feature/Person/ (singular).
Matched the existing directory rather than introducing a singular/plural
split.

Also carries the BACKLOG DOC-04 entry Bert asked for during WS-4 review:
scripts/install-claude-sync-hooks.sh belongs in SETUP.md / onboarding
so new clones don't miss the post-commit sync hook.

5 tests / 10 new assertions. Full suite: 1005 passed (2716 assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:10:37 +02:00
b688ec26f0 feat(scope): declarative FK-chain strategy for OrganisationScope, register on 14 models per addendum Q2 + D-03/D-04
Refactors OrganisationScope to support a declarative, recursive FK-chain
resolver and registers the scope on 14 models that previously relied on
caller-discipline for tenant isolation.

Scope resolver (app/Models/Scopes/OrganisationScope.php):
Models now declare their strategy via:

    public static function tenantScopeStrategy(): array
    {
        return ['column' => 'organisation_id'];           // terminal
        // OR
        return ['via' => FormSchema::class, 'fk' => 'form_schema_id'];
    }

The apply() path walks the chain recursively, building whereIn subqueries
against parent models until it hits a column-based strategy. Max 3 hops;
deeper chains raise App\Exceptions\TenantScopeResolutionException. The
walker accepts BOTH the new tenantScopeStrategy() and the legacy
$organisationScopeColumn property at every hop — so PersonIdentityMatch
can chain via Person, which still uses the legacy event_id bridge, without
requiring Person/Event/Shift/FestivalSection/TimeSlot to migrate to the
new convention in this work package. That migration is a separate
backlog ticket — explicitly scope-controlled per the addendum.

Fourteen newly-scoped models:

  Form-builder child models (D-03):
    FormSchemaSection             via FormSchema                    (1 hop)
    FormField                     via FormSchema                    (1 hop)
    FormSubmission                column organisation_id (Commit 2)
    FormValue                     via FormSubmission                (1 hop)
    FormValueOption               via FormValue -> FormSubmission   (2 hops)
    FormSubmissionSectionStatus   via FormSubmission                (1 hop)
    FormSubmissionDelegation      via FormSubmission                (1 hop)
    FormSchemaWebhook             via FormSchema                    (1 hop)
    FormWebhookDelivery           via FormSubmission                (1 hop)

  Event-data models (D-04 event-data subset):
    ShiftAssignment               via Shift (legacy festival_section_id)
    ShiftWaitlist                 via Shift
    VolunteerAvailability         via TimeSlot (legacy event_id)
    PersonSectionPreference       via FestivalSection (legacy event_id)
    PersonIdentityMatch           via Person (legacy event_id)

Note — task directive specified VolunteerAvailability "via: Event, fk: event_id",
but the table has no event_id column (only person_id + time_slot_id).
Rerouted via TimeSlot, which carries the legacy event_id bridge; same
end result, correct FK.

Security-relevant callers made explicit:
  PublicFormSchemaResource::toArray() now eagerly loads fields + sections
  with withoutGlobalScope(OrganisationScope::class). Prior to this commit
  the public form endpoint silently relied on those relations being
  unscoped. The PublicFormCrossOrgScopeTest pre-existing assertions still
  pass — behaviour unchanged, intent now explicit.

Test fix: FormSchemaApiTest::test_publish_sets_is_published_true was
flaky (factory randomly picked EVENT_REGISTRATION which requires
bindings). Pinned to USER_PROFILE for determinism; PurposeSchemaLifecycleTest
covers the binding-enforcement path.

Test flip: MultiTenancyTest::test_form_schema_webhook_is_not_globally_scoped
renamed to is_scoped_via_fk_chain and asserts the new behaviour: scope
filters by route org, withoutGlobalScope() still exposes cross-org rows.
The test's original purpose ("pin current behaviour so a future refactor
is intentional") is now satisfied by Commit 3 being that intentional
refactor.

Docs:
  SCHEMA.md §3.5.11 Rule 5 — tenantScopeStrategy() convention documented;
    the 14 newly-scoped models enumerated; link to addendum Q2.
  ARCH-FORM-BUILDER.md §4.14 — new section "Multi-tenancy scope chain"
    with the hop-count table for all 14 chains and the withoutGlobalScope
    pattern for cross-org callers.

Tests: tests/Feature/MultiTenancy/ScopeLeakageTest.php — two orgs with
fully-populated record chains down to each of the 14 leaf models; asserts
scoped queries never cross, withoutGlobalScope still does. Plus: three-
hop chain (FormValueOption) explicitly exercised, legacy-column bridge
verified, over-deep chain raises TenantScopeResolutionException. 16 tests /
31 new assertions. Full suite: 1000 passed (2706 assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:08:33 +02:00
ae8e2fdb4e feat(form-builder): denormalize organisation_id and event_id on form_submissions per addendum Q2
Adds direct tenant + event columns to form_submissions so rapportage-hot
aggregate queries (dashboards, CSV-exports, counts over thousands of rows
per org or per event) skip the form_schemas join. This is the single
denormalization exception per addendum Q2; every other form-builder child
table continues to resolve tenancy via FK-chain through its parent
(implemented in Commit 3).

Schema:
- form_submissions.organisation_id  ULID FK → organisations, cascade delete, NOT NULL
- form_submissions.event_id          ULID FK → events, null on delete, nullable
- Indexes: (organisation_id, status), (event_id, status)

Observer: App\Observers\FormBuilder\FormSubmissionObserver::creating
resolves both columns when the caller has not set them.
  - organisation_id <- form_schema.organisation_id (always present —
    form_schemas carries OrganisationScope's column directly)
  - event_id <- schema.owner_id when owner_type === 'event'; else the
    active route's {event} parameter; else null (user_profile /
    signature_contract purposes)
The observer docblock spells out both resolution paths and is covered
by the observer test below.

Model: FormSubmission gains organisation_id + event_id in $fillable, a
belongsTo organisation() and belongsTo event() relation.

Factory: FormSubmissionFactory gains forOrganisation($org) and
forEvent($event) states for tests that need to override the observer's
automatic resolution (e.g. cross-org leakage scenarios in Commit 3).
Normal factory usage does not need the states — the observer populates
both fields on save.

Docs:
- SCHEMA.md §3.5.12 form_submissions table — organisation_id and event_id
  inserted between form_schema_id and subject_type; indexes added;
  addendum Q2 rationale paragraph at the bottom explaining why this is
  the only denormalized form-builder child.
- ARCH-FORM-BUILDER.md §4.3 — mirror changes + rationale inline on the
  columns and in the indexes list.

Tests: tests/Feature/FormBuilder/FormSubmissionObserverTest.php — 7 tests
covering organisation resolution from schema, event resolution from
event-owned schema, null event_id for non-event-owned schemas without
route context, route-based event resolution, organisation_id populated
on every create path (factory / new() / Model::create), index presence,
and belongsTo relations. 13 new assertions. Full suite: 984 passed
(2675 assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:56:53 +02:00
a92ddc48ec refactor(schema): migrate eleven pivot/EAV tables to ULID per addendum Q1
Retires the "integer AI PK for join performance" exception documented
in earlier migrations and SCHEMA.md §3.5.11 Rule 1. Every business and
pivot table now uses ULID primary keys, per
/dev-docs/ARCH-CONSOLIDATION-ADDENDUM-2026-04-24.md Q1.

Tables migrated (WS-1 A-01 through A-11):
- Pure pivots: organisation_user, event_user_roles, crowd_list_persons,
  event_person_activations
- Model-backed: user_organisation_tags, person_section_preferences,
  mfa_backup_codes, mfa_email_codes, form_submission_section_statuses,
  form_values, form_value_options

Migration pattern: one new migration per table (plus one combined for
the form_values / form_value_options FK pair), timestamped today,
dropping + recreating with the new ULID PK. Pre-launch — no backfill
required. Original migrations remain in place; the new migrations
apply in timestamp order for a clean schema history.

Pivot model correction (addendum drift):
The addendum's "no model required for pure pivots" reading did not
account for Laravel's BelongsToMany::attach() — it cannot auto-generate
a pivot ULID without a Pivot subclass. Minimal Pivot classes under
app/Models/Pivots/ (OrganisationUser, EventUserRole, CrowdListPerson,
EventPersonActivation) carry HasUlids so attach() works. The six
belongsToMany relations (User.organisations / .events, Organisation.users,
Event.users, CrowdList.persons, Person.crowdLists) now ->using(...) the
appropriate Pivot class. DB::table()->insert() on event_person_activations
in DevSeeder populates the ULID inline via Str::ulid(). FormValueObserver
uses bulk FormValueOption::insert() which bypasses model events — ULIDs
are now generated inline there too.

Docs:
- SCHEMA.md §3.5.11 Rule 1 rewritten to mandate ULID on pivots too, with
  legacy note citing the addendum.
- All eleven table entries updated from "int AI PK" to "ULID PK" with
  addendum Q1 references.
- form_values and form_submission_section_statuses prose blocks updated
  to drop the retired ARCH §4.4 / "high-volume pivot" rationale.
- form_value_options.form_value_id column type corrected from
  "int FK" to "ULID FK".

Tests: tests/Feature/Schema/UlidPrimaryKeyTest.php covers HasUlids trait
presence, ULID shape + 26-char Crockford pattern, Route::bind resolution,
distinct + sortable pivot ULIDs, attach() auto-generation on pure pivots,
and the A-10/A-11 FK chain. 10 tests / 28 new assertions. Full suite:
977 passed (2662 assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:38:08 +02:00
5a82497da4 docs(backlog): track Artist model prerequisite for artist_advance purpose
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:46:20 +02:00
598593b0db docs(schema,arch-form-builder): reflect purpose registry v1.0
- SCHEMA.md §3.5.12 header rewritten for the 7-purpose vocabulary and
  `PurposeRegistry`. The `custom_purpose_slug` column is dropped from
  the `form_schemas` table and removed from the index list. The
  `form_submissions.subject_type` note cites
  `PurposeRegistry::allSubjectTypes()` instead of the deleted
  `config/form_subjects.php`.
- ARCH-FORM-BUILDER.md TL;DR updated: goal bullet cites 7 purposes
  (v1.0); §3.2 bullet notes the legacy 22-variant vocabulary is
  retired. §17.3 replaced: the "Custom purposes per organisation"
  section is gone; the new "Purpose registry" section documents the
  seven-slug table, PurposeDefinition shape, PurposeRegistry API,
  MorphMapAlignmentTest guard, the pre-publish binding check, and a
  step-by-step "adding a new purpose" checklist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:36:18 +02:00
ad941cc944 docs: add architect decisions addendum after WS-1 2026-04-24 13:49:05 +02:00
4f47e054b9 docs: add WS-1 architecture discovery report 2026-04-24 13:08:00 +02:00
83821b1bd5 docs(architecture): land consolidation sprint briefing document
Adds ARCH-CONSOLIDATION-2026-04.md as the authoritative reference for the
upcoming 8-workstream architecture consolidation sprint: purpose registry
cleanup, ULID consistency, JSON column split, binding infrastructure,
FormBindingApplicator, single-SPA consolidation to crewli.app, observability
foundation, docs consolidation.

Sprint scope, leading principles, workstream ordering, and chat-transition
protocol are captured in the document. Follow-up chats will start from this
document as primary context.

Also updates BACKLOG.md with an active-sprint marker pointing to the briefing.
2026-04-24 11:51:28 +02:00
a777c8335f docs(backlog): log TECH-08 for paginated response meta in organizer composables
Documents the technical debt introduced when the organizer API composables
(useSections, useFormSchemas) adopted a minimal PaginatedResponse shape
that discards Laravel's links and meta blocks. PR-b2 will surface the
first UI consumer that needs pagination controls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 10:32:40 +02:00
2b16d4fd8f docs(policy): add Form Builder scope discipline policy
Deferred-work guidance for Form Builder expansions. Prevents
speculative building for hypothetical use cases while preserving
the one justified forward-looking investment (polymorphic subject
picker in S3b). Based on April 2026 coverage analysis of ~60
practical form use cases.
2026-04-24 10:18:44 +02:00