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.
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.
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.
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.
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>
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).
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.
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>
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>
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>
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>
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>
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>
- 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>
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>
- 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.
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>
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>
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).
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>