Commit Graph

129 Commits

Author SHA1 Message Date
5c42f27b26 fix: whitelist GlitchTip ingest host in CSP connect-src
PR-3 follow-up. Live smoke surfaced that the @sentry/vue SDK was
running correctly and emitting events, but Crewli's strict
connect-src directive blocked every POST at the browser layer. No
fallback — events evaporated silently with a CSP-violation log in
DevTools console only.

Updated locations (audited the CSP surface; only two locations actually
need the whitelist):

- apps/app/index.html — dev meta CSP, adds http://localhost:8200 to
  connect-src so local dev hits the docker-compose GlitchTip stack.
- deploy/nginx/csp-spa.conf — prod organizer SPA CSP, adds
  https://monitoring.hausdesign.nl to BOTH the report-only and enforce
  add_header lines so a future flip between modes can't silently break
  observability.

NOT updated (deviation from prompt):

- api/config/security.php — the API CSP is `default-src 'none';
  frame-ancestors 'none'` for JSON responses. Browsers don't enforce
  connect-src on JSON contexts (no document, no fetch origin). Adding
  connect-src would be semantically a no-op and confuse the deny-by-
  default policy.

Regression guard: tests/Feature/Security/CspConnectsToObservabilityTest.
Reads both the dev meta tag and the prod nginx conf directly (the SPA's
CSP is not Laravel-served, so $this->get() can't reach it). Apply-with-
revert verified: stashing both fixes makes both cases fail with a clear
"Refused to connect because it violates the following CSP directive"
hint; popping the stash restores green.

SECURITY_AUDIT.md A13-9 updated with a WS-7 follow-up note documenting
the GlitchTip whitelist as an explicit security control: outgoing
observability traffic restricted to a single known host.

Test count 1549 to 1551. Larastan + Pint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 18:36:05 +02:00
53ae1a686c docs: WS-7 PR-3 acceptance criteria progress
WS-7 PR-3 commit 4. RFC §6 acceptance criteria 4, 5, 6 now satisfied
by the frontend SDK PR; entries marked  with brief implementation
references.

Updated criterion 4 to reference Crewli's actual token-based portal
paths (/portal/advance/:token, /register/:public_token) instead of the
RFC's speculative /p/* — the contextBinding guard detects via
route.meta.public + route.meta.context which is the canonical Crewli
signal already used by other guards.

Added a "Voortgang (mei 2026)" subsection at the end of §6 mapping
each PR to the acceptance criteria it closed, plus what remains for
PR-4 (live smoke, ARCH-OBSERVABILITY.md, alerting config, retention
config, SECURITY_AUDIT.md update).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 18:00:54 +02:00
dee140193e test: regression guards for listener registration uniqueness + always-present binary tags
Drie regression-tests die de klasse fouten uit PR-2 nazorg empirisch
voorkomen:

1. test_authenticated_listener_registered_exactly_once
2. test_token_authenticated_listener_registered_exactly_once
3. test_job_processing_tag_listener_registered_exactly_once
   — vangen OBS-8 patroon (auto-discovery + explicit listen samen) plus
   accidentally-removed registrations door toekomstige refactors. Walk
   Event::getRawListeners() en faalt met count != 1 met een duidelijke
   message ("auto-discovery re-enabled? OR explicit Event::listen
   missing?"). Empirisch geverifieerd: zowel duplicate als missing
   registratie wordt gevangen.

4. test_impersonation_active_tag_invariant_on_captured_events
   — RFC §3.6 binary signal invariant op een echte HTTP request flow.
   Vangt regressie waar de baseline-tag-binding verdwijnt.

BACKLOG.md OBS-8 entry toegevoegd en gemarkeerd als Resolved met
verwijzing naar de drie commits van deze sessie + architecturaal
pattern (explicit > implicit voor observability-kritische bindings).

Test count 1545 to 1549. Larastan + Pint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 17:35:11 +02:00
0379016c7e docs: WS-7 PR-2 follow-up — RFC §3.6 + §3.14 + BACKLOG OBS entries
RFC §3.6 — context tagging tabel volledig vervangen na de PR-2 follow-up
architecturale fixes. Belangrijkste wijzigingen:
- Tag-binding gesplitst in route-scope (BindSentryRouteContext middleware)
  en auth-scope (AuthScopeContextListener op Authenticated event).
- Nieuwe actor_scope tag (organisation/platform/user/anonymous).
- Multi-tenant invariant verfijnd: organisation_id is altijd correct
  gerelateerd aan actor_scope in plaats van "altijd aanwezig". Platform-
  routes zonder org-context worden niet meer gefabriceerd; default
  authenticated user-scope omitt organisation_id (Crewli's User<->Organisation
  is many-to-many, geen reliable single-org hint).
- impersonation.* tags expliciet gedocumenteerd als afkomstig uit
  HandleImpersonation middleware (post-swap), niet uit auth-listener.
- ActorType waarden bijgewerkt na verwijdering van VOLUNTEER case.

RFC §3.14 — status-note toegevoegd dat D-06 indexes al via Spatie's
nullableMorphs default-migratie zijn aangemaakt, met regression-guard
verwijzing.

§6 acceptance criterium 12 markeert D-06 als al voldaan.

BACKLOG.md krijgt vier nieuwe OBS-entries:
- OBS-1: VOLUNTEER actor_type promotion wanneer rol komt
- OBS-4: PHPUnit metadata deprecation cleanup pre-PHPUnit-12
- OBS-6: sentry-laravel install gap awareness + bootstrap test
- OBS-7: custom render handlers report() invariant + coverage

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:05:42 +02:00
932788c643 docs: glitchtip runbook + setup + RFC §3.1 dev amendment
Operational docs for the GlitchTip stack landed in the previous two
commits.

- dev-docs/GLITCHTIP.md: new runbook covering local dev, project
  provisioning + DSN-to-vault flow, production deploy on
  monitoring.hausdesign.nl (DNS, DirectAdmin Let's Encrypt, Apache
  reverse proxy with WS upgrade), backup install + restore drill,
  smoke tests, troubleshooting.
- dev-docs/SETUP.md: services table now includes GlitchTip; new
  docker/glitchtip/.env subsection points at the runbook.
- dev-docs/RFC-WS-7-OBSERVABILITY.md §3.1: amended to record that the
  same compose file drives local dev (Mailpit at bm_mailpit:1025), so
  prod and dev cannot drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 08:15:27 +02:00
25888a232b Updated the URL of Glitchtip 2026-05-06 07:42:09 +02:00
1e394879aa docs: RFC-WS-7 observability foundation (GlitchTip)
Two charter amendments from the original WS-7 brief:

- Sentry -> GlitchTip (self-hosted, protocol-compatible). Same
  Sentry SDKs on backend (sentry-laravel) and frontend
  (@sentry/vue), pointed at a self-hosted GlitchTip DSN. Avoids
  Sentry SaaS pricing and keeps event data on infrastructure
  Bert controls.
- Performance monitoring out of scope (errors-only). WS-7
  delivers exception capture + alerting + scrubbing + RBAC
  only. APM/tracing/spans deferred to a later workstream if
  ever needed; pre-launch with no users, the cost/benefit
  doesn't justify it now.

RFC-as-first-commit pattern (per WS-6) so the scope-alignment
document is in main before any infra/code changes land.
2026-05-06 07:32:12 +02:00
1437829501 chore(backlog): close TECH-DOCS-APPS-PORTAL-PURGE
WS-3 PR-C delivered the per-file DELETE/REWRITE/KEEP_AND_PURGE
matrix on all 9 files referenced in the entry, plus the
out-of-scope post-edit-eslint.sh hook fix. Doc-rot removed:
~80 KB of obsolete bootstrap and prompt-template content.

Single SPA, single cookie, single deploy host. WS-3 complete.
2026-05-06 02:14:46 +02:00
d33c119d75 chore(docs): delete obsolete bootstrap and prompt-template docs
Five files removed, all describing project states that no longer
apply post-WS-TOOLING-001:

- .cursor/instructions.md (8.4 KB): Phase 1-4 roadmap with all
  checkboxes empty; Phase 1 has been done for ~6 months. Broken
  'make portal' target. Content overlaps with CLAUDE.md.
- .cursor/ARCHITECTURE.md (18.9 KB): pre-WS-3 framing (dual SPA,
  dual cookies, dual SANCTUM_STATEFUL_DOMAINS) AND pre-Form-Builder
  schema (volunteer_profiles, public_forms with JSON fields). All
  six sections superseded by SCHEMA.md, AUTH_ARCHITECTURE.md,
  design-document.md, API.md, 102_multi_tenancy.mdc.
- dev-docs/MASTER_PROMPT_CC.md (13 KB): 'paste this above every task'
  workflow superseded by auto-loaded CLAUDE.md and structured
  Claude Chat-authored prompts. Stale dual-SPA + pre-Form-Builder
  assumptions throughout.
- dev-docs/MASTER_PROMPT_CURSOR.md (7.5 KB): same workflow obsoletion;
  Cursor is now IDE-only (Claude Code does all implementation).
  .cursor/rules/ system handles IDE-level guidance.
- dev-docs/dev-guide.md (32 KB): bootstrap-from-scratch document
  containing embedded snapshots of pre-Form-Builder CLAUDE.md,
  pre-Form-Builder SCHEMA.md, pre-Form-Builder API.md as
  copy-paste templates. Section 5 prompts pre-WS-TOOLING-001 era.
  Section 6 (agents) overlaps with CLAUDE_CODE_TOOLING.md.

Total: ~80 KB doc-rot removed.

Cross-reference check found four files outside the deleted set
referencing the deleted paths; all updated in the same commit:

- README.md: Documentation table rebuilt around CLAUDE.md +
  dev-docs/* (also dropped stale resources/design/ row pointing
  at a directory that no longer exists, and corrected docs/*
  paths to dev-docs/*)
- dev-docs/CLAUDE_DESKTOP_SETUP.md: dropped MASTER_PROMPT_CC,
  MASTER_PROMPT_CURSOR, dev-guide entries from the
  bewust-verwijderd exclusion list; updated Gerelateerd pointer
  from dev-guide.md -> SETUP.md
- dev-docs/ARCH-CONSOLIDATION-2026-04.md: updated future-distribution
  pointer from dev-guide.md -> SETUP.md (sprint briefing is
  historical so the change is purely doc-hygiene)
- dev-docs/VIBE_CODING_CHECKLIST.md: removed Dev guide row from
  the bestandspaden table

Remaining references in dev-docs/BACKLOG.md (lines 862-869) live
inside the TECH-DOCS-APPS-PORTAL-PURGE entry that closes in the
next commit.

Canonical replacements: dev-docs/SETUP.md (rewritten this PR),
CLAUDE.md, CLAUDE_CODE_TOOLING.md, and the ARCH-*.md series.
2026-05-06 02:14:10 +02:00
bea66a58e6 chore(docs): purge apps/portal mention from CLAUDE_CODE_TOOLING.md
Single-line fix in the hooks reference table. The post-edit-eslint
hook used to scope to apps/app/ or apps/portal/; post-WS-3 there's
only apps/app/.

Code change in the hook script itself lands in the next commit.
2026-05-06 01:51:37 +02:00
5d4132785f chore(docs): rewrite SETUP.md as continue-existing-project guide
Replaces the bootstrap-from-scratch document (Step 2 told readers to
run 'composer create-project laravel/laravel api' on an existing
codebase) with a continue-existing-project guide.

Scope: prerequisites, first-time setup (clone + install + .env + migrate),
daily workflow (three terminals + optional queue worker), env-config
explained, common tasks (test/migrate/route:list/build), documentation
reference linking the dev-docs/ canonical files, troubleshooting.

Drops apps/portal references throughout (single SPA at port 5174).
Drops dual-port SANCTUM_STATEFUL_DOMAINS guidance. Replaces .cursor/
instructions reference with /CLAUDE.md as auto-loaded source of truth.
2026-05-06 01:50:01 +02:00
812cc17460 docs(auth): reflect single-cookie architecture; close A13-3
dev-docs/AUTH_ARCHITECTURE.md (v1.0 → v2.0):
- Title section updated to single-SPA / single-cookie reality
- Client Applications table collapsed to one row
- Cookie Specification table collapsed to one row (crewli_app_token)
- Token Lifecycle / Validation section: Origin-based resolution
  language removed; middleware described as origin-agnostic
- Cross-app isolation paragraph removed (no second app)
- Configuration Reference table marks FRONTEND_PORTAL_URL as legacy,
  pointing at TECH-FRONTEND-URL-CONSOLIDATE
- New §11 "History" preserves the pre-WS-3 dual-cookie context for
  future readers, mentions PR-B2a + PR-B2b roles in the unwind

dev-docs/BACKLOG.md — three new entries:
- TECH-FRONTEND-URL-CONSOLIDATE: refactor email controllers to drop
  per-app URL map (EmailChangeController, PasswordResetController,
  PersonController) — low priority, code-cleanliness only
- TECH-DOCS-APPS-PORTAL-PURGE: sweep apps/portal references from
  briefing/tooling docs (.cursor/, MASTER_PROMPT_*, SETUP, dev-guide,
  CLAUDE_CODE_TOOLING) — single chore(docs) PR, low priority
- OPS — DNS retirement of portal.crewli.app — operational task,
  deferred until traffic monitoring confirms zero usage

dev-docs/SECURITY_AUDIT.md:
- A13-1 narrative actualised: pre-WS-3 dual-cookie context kept as
  history, status flipped to RESOLVED (the localStorage→httpOnly
  migration shipped earlier in the consolidation arc)
- A13-3: status flipped to RESOLVED by WS-3 PR-B2b; description
  rewritten to reflect the new postLoginRedirect.ts validator and
  the 16 spec coverage
- Priority remediation table item 8 strikes through A13-3

Backend test suite: 1487 passed (unchanged from Commit 2 baseline).
Frontend: 223 passed (unchanged from Commit 1 baseline).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 00:29:26 +02:00
538072241e docs(backlog): WS-TOOLING-001 done + 3 follow-up tech items 2026-05-06 00:08:09 +02:00
b1773ea1f3 docs(claude): document rm -rf rule in hooks reference table 2026-05-05 23:56:21 +02:00
735ba2c8d9 docs: add CLAUDE_CODE_TOOLING.md and cross-reference from CLAUDE.md
Documents the deterministic guard-rail layer: hooks reference,
crewli-reviewer subagent usage, three slash commands, how to test
hooks, how to disable temporarily, and the binding design principle
(settings/hooks deterministic, CLAUDE.md advisory — never duplicate).
2026-05-05 23:25:03 +02:00
68f1e6f80c Merge pull request 'WS-3 PR-B2a: auth + routing consolidation (single SPA, dual axios, context-aware guards)' (#5) from feat/ws-3-pr-b2a-auth-routing-consolidation into main
Reviewed-on: #5
2026-05-05 22:43:52 +02:00
145d0cbdad docs(backlog): add ARCH-API-RESPONSE-VALIDATION workstream entry
Workstream-sized item geborgt voor uniforme typed + runtime-validated
contracts op de API-grens (backend PHP Enums, frontend Zod schemas,
codegen TS types). Scope, sequentie (post-PR-C/WS-7, pre-RFC-FORM-BUILDER-UI),
en open beslissingen vastgelegd. Verwijst naar dev-docs/ARCH-API-VALIDATION.md
skeleton voor architectuur-detail.

Voorkomt dat S3b technische schuld stapelt — landt vóór RFC-FORM-BUILDER-UI
zodat nieuwe composables vanaf dag één het gevalideerde patroon consumeren.
2026-05-05 22:32:05 +02:00
babbbd97cb docs(arch): add ARCH-API-VALIDATION.md skeleton — uniform API response validation workstream 2026-05-05 22:17:27 +02:00
a2760ffd64 feat(auth): add contexts + platform.is_super_admin to /auth/me, factory role-category states
Additive enrichment to MeResource — existing fields untouched, MeTest stays green.
New fields:
- contexts.available: list<'portal'|'organizer'> derived from Person + Organisation memberships
- contexts.default: precedence super_admin > organizer > portal > fallback portal
- platform.is_super_admin: bool promoted from app_roles
- organisations[].roles: 1-element array form alongside the legacy scalar role,
  forward-compatible for the multi-role pivot work tracked in TECH-PIVOT-ROLES-MULTI

UserFactory gains volunteer(), orgAdmin(), volunteerAndOrganizer(), superAdmin()
state methods — codified role categories for reuse across future workstreams.

Adds forbidden.vue placeholder (PublicLayout) for the context-failure landing in
the upcoming guard rewrite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:15:10 +02:00
d1503ceadf docs(vuexy): update VUEXY_COMPONENTS.md for post-PR-B1 single-SPA reality
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:33:33 +02:00
a4281df021 docs(arch): add ARCH-BINDINGS.md — canonical reference for FormBindingApplicator pipeline (WS-8a) 2026-05-05 20:22:11 +02:00
deb75ee500 docs(backlog): add TECH-FORM-BUILDER-INTEGRATION-TEST-NAME-COVERAGE
Records the naming-vs-coverage gap surfaced during WS-6 closure
verification: ARCH-FORM-BUILDER §31 references five integration
contract tests by name that don't exist under those filenames in
api/tests/Feature/FormBuilder/Integration/. Coverage may be intact
under different filenames; only the §31 naming index is stale.

Low priority — defer to whoever next touches FormBuilder
integration tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 23:52:44 +02:00
d709da7858 docs(ws-6): record completion and verification
WS-6 (FormBindingApplicator pipeline) is fully landed in main —
sessions 1, 2, and 3 all merged. Verification on 2026-05-04
confirmed every RFC-WS-6.md §7 deliverable plus the v1.1/v1.2
addenda. Backend test suite green at 1486 tests, above the RFC
§8 target of 1445-1465.

Adds a closure-marker note documenting what's verified in main
and adds a single status line under §6.2 of the consolidation
plan pointing at it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 23:52:37 +02:00
4197df2b2f docs: close TECH-AXIOS-STORE-COUPLING and add TECH-AXIOS-INTERCEPTOR-TESTS
Removes the closed TECH-AXIOS-STORE-COUPLING entry from BACKLOG.md
(the structural decoupling landed in 53f6a7b + 26a92b3). The
git-history search `git log --grep=TECH-AXIOS-STORE-COUPLING`
remains the durable closure record, per the backlog hygiene
convention.

Adds a follow-up entry TECH-AXIOS-INTERCEPTOR-TESTS that captures
all four acceptance scenarios (X-Organisation-Id header
injection, 401 auth-fail flow, 403+impersonation_ended revocation
flow, 4xx/5xx error toast). Phase A audit found that none of
these is tested today; the refactor is gedragsneutraal so no
regression was introduced, but the gap is real and should not
silently outlive the refactor that made it visible. Priority
medium per Bert's Phase B sign-off.

Appends the debt-closed sentence to the Sessie 1c entry in
ARCH-CONSOLIDATION-2026-04.md, citing commit 53f6a7b.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-04 22:24:05 +02:00
5eac201d88 docs(refactor): audit axios↔store coupling for decoupling work
Phase A of TECH-AXIOS-STORE-COUPLING. Read-only inventory of the
four lib/axios.ts → stores/ touchpoints (lines 3, 5, 63, 75 carry
per-line boundary disables), plugin load-ordering analysis, test
coverage matrix, consumer audit (30 importers, all using the
`apiClient` named export), and Vite mixed-import warning
confirmation.

Surfaces four open questions for Phase B sign-off:
  Q1 callback-injection vs event-bus  → recommends callback-injection
  Q2 location of `Deps` type          → recommends inside lib/axios.ts
  Q3 test scope for this session      → recommends defer to backlog
  Q4 plugin filename                  → recommends 3.axios-bindings.ts

No code changed. No BACKLOG.md edit. Awaiting Bert's Phase B
sign-off before implementing Phase C.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-04 22:06:20 +02:00
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