From 7f99783d8ae11729ea735bfc95ab69fa230ee0d7 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Sun, 26 Apr 2026 00:04:53 +0200 Subject: [PATCH] docs: add ARCH-BINDINGS.md skeleton with foundation sections complete (WS-6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- dev-docs/ARCH-BINDINGS.md | 237 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 dev-docs/ARCH-BINDINGS.md diff --git a/dev-docs/ARCH-BINDINGS.md b/dev-docs/ARCH-BINDINGS.md new file mode 100644 index 00000000..f5c86ce7 --- /dev/null +++ b/dev-docs/ARCH-BINDINGS.md @@ -0,0 +1,237 @@ +# ARCH-BINDINGS.md — Form Builder Binding Pipeline + +## Status + +- v0.1 (skeleton) — 2026-04-25 +- Owner: Bert +- Authoritative for the binding pipeline architecture, complementing + ARCH-FORM-BUILDER.md §17 and §31. +- Decisions originate from RFC-WS-6.md. + +## 1. Scope + +The binding pipeline turns a submitted form (`form_submissions`) into +durable writes against domain entities — Person, Artist, Company, User +— via per-field bindings configured in `form_field_bindings` (WS-5a). + +This document describes the end-to-end flow from +`FormSubmissionSubmitted` event to the post-pipeline state where the +domain has been updated, identity-match has been triggered, queued +listeners (tag sync, shifts, webhooks, mail) have fired, and any +binding-level failures have been recorded as +`form_submission_action_failures` rows. + +The library is a **template-source, not a live link.** When a +`form_field` is created from a `FormFieldLibrary` entry with bindings, +the library's bindings are **copied** into `form_field_bindings` rows +with `owner_type = 'form_field'`. Library updates do not propagate to +existing field instances (RFC §3 Q11). Future re-sync admin action +tracked in BACKLOG: `FORM-LIBRARY-RESYNC`. + +## 2. Schema + +### 2.1 form_field_bindings (WS-5a) + +Polymorphic owner (`owner_type` ∈ {`form_field`, `form_field_library`}, +`owner_id` ULID), `target_entity` × `target_attribute` columns, +`merge_strategy` (Overwrite/Append/Replace/FirstWriteWins), `trust_level` +(0-100, default 50), `is_identity_key` bool. UNIQUE on +`(owner_type, owner_id, target_entity, target_attribute)`. See +`SCHEMA.md §3.5.12`. + +### 2.2 form_submissions.apply_status / apply_completed_at (WS-6) + +Two new columns added in `2026_04_25_140000_extend_form_submissions_with_apply_status`: + +- `apply_status` — string(20) nullable, no default. Values: + `pending|completed|partial|failed`. Drives admin "open work" filtering. + NULL legacy rows are excluded from open-work views by design (RFC O1). +- `apply_completed_at` — timestamp nullable, set when applicator + finishes (success or fail). + +Indexed on `(form_schema_id, apply_status)` and +`(organisation_id, apply_status)` for dashboard queries. + +### 2.3 form_submission_action_failures (WS-6) + +Audit table for binding-pipeline failures (RFC §3 Q5). One row per +listener invocation that failed, with retry / resolve / dismiss state +fields. No `organisation_id` column — tenant scope flows via +`form_submission_id → form_submissions.organisation_id`. Cascade-delete +through the parent submission. Resolve and Dismiss are mutually +exclusive workflows (RFC V2): a failure has at most one of +`resolved_at` or `dismissed_at`. + +Full DDL in `SCHEMA.md §3.5.12`. + +## 3. Value objects and enums + +`App\FormBuilder\Bindings\` (final readonly classes): + +- `ResolvedBinding` — output of session 2's `BindingConflictResolver`. + One winning binding per `(target_entity, target_attribute)` group. + Carries `valueIsExplicit` to distinguish "user explicitly cleared" + from "field skipped by conditional logic" (RFC §3 Q7). +- `BindingApplicationResult` — result of applying one resolved binding. + Sealed via `succeeded()` / `failed()` named constructors so consumers + cannot synthesise impossible states. +- `BindingPassResult` — aggregate result of one applicator pass. + `applyStatus()` maps to `ApplyStatus` enum per RFC §3 Q4 rules: + empty applications → `COMPLETED`, all OK → `COMPLETED`, all failed + → `FAILED`, mixed → `PARTIAL`. +- `BindingTargetMeta` — single config row from + `config/form_builder/binding_targets.php`. + +Enums in `App\Enums\FormBuilder\`: + +- `ApplyStatus` (RFC §3 Q4) — `PENDING|COMPLETED|PARTIAL|FAILED`. + Helpers: `isTerminal()`, `isOpen()`, `label()`. +- `DismissalReasonType` (RFC §4 V2) — six cases. `manually_resolved` + is intentionally absent: Resolve and Dismiss are different workflows. +- `BindingTargetType` (RFC §4 V1) — `SCALAR|COLLECTION|RELATION`. + Storage shape only; PHP type and identity-key eligibility live on + `BindingTargetMeta`. +- `FormFieldBindingMergeStrategy` (existing, WS-5a) — extended in WS-6 + with `nullWinnerBehaviour()` and `isValidForScalarTargets()`. + +## 4. BindingTypeRegistry + +`App\FormBuilder\Bindings\BindingTypeRegistry` is the single source of +truth for the storage shape of binding-target attributes. Config-driven +(`config/form_builder/binding_targets.php`), singleton-bound in +`AppServiceProvider`. + +Public surface: + +- `resolve(entity, attribute): BindingTargetMeta` +- `isKnown(entity, attribute): bool` +- `isIdentityKeyEligible(entity, attribute): bool` +- `entities(): list` +- `attributesFor(entity): list` +- `validateAppendStrategy(entity, attribute, strategy)` — throws + `InvalidBindingTargetException` when `strategy=Append` is paired + with a non-COLLECTION target. + +The registry is **not** name-suffix matching (e.g. "ends with `_tags`"). +Convention-not-contract is rejected because it silently misclassifies +attributes that don't follow the convention or accidentally match it. + +## 5. PublishGuard framework + +Per-purpose schema validation. `FormSchemaService::publish()` calls, +in order: + +1. `assertRequiredBindingsPresent()` (existing) — every required + binding path on the purpose's `required_bindings` list is bound. + Throws `PurposeRequirementsNotMetException`. +2. `assertPublishGuardsSatisfied()` (new) — every guard returned by + the purpose's `PurposeGuardProvider` evaluates to passed. Throws + `PublishGuardViolationException` carrying ALL violations sorted + lexicographically by `code()`. + +### Guard catalog + +| Class | code() | Universal? | +|---|---|---| +| `RequiresIdentityKeyBinding(entity, attribute)` | `requires_identity_key_binding:{entity}:{attribute}` | no | +| `MaxOneIdentityKeyPerTargetEntity` | `max_one_identity_key_per_target_entity` | yes | +| `RequiresFieldType(type, minCount)` | `requires_field_type:{type}` | no | +| `SchemaHasLinkedEvent` | `schema_has_linked_event` | sub-guard | +| `TagCategoriesConfiguredOnAllPickers` | `tag_categories_configured_on_all_pickers` | sub-guard | +| `IdentityKeyBindingsOnlyInFirstSection` | `identity_key_bindings_only_in_first_section` | yes | +| `AppendStrategyRequiresCollectionTarget` | `append_strategy_requires_collection_target` | yes | +| `NoAmbiguousTrustLevels` | `no_ambiguous_trust_levels` | yes | +| `ConditionalRequirement(predicate, sub, code)` | `conditional:{caller-supplied}` | composer | + +### Per-purpose providers + +| Purpose | Provider | Purpose-specific guards | +|---|---|---| +| `event_registration` | `EventRegistrationGuards` | `RequiresIdentityKeyBinding(person, email)`, `RequiresFieldType(EMAIL,1)`, `Conditional(AVAILABILITY_PICKER → SchemaHasLinkedEvent)`, `Conditional(TAG_PICKER → TagCategoriesConfiguredOnAllPickers)` | +| `artist_advance` | `ArtistAdvanceGuards` | (universal only — artist resolved via portal token) | +| `supplier_intake` | `SupplierIntakeGuards` | (universal only — company via production_request) | +| `post_event_evaluation` | `PostEventEvaluationGuards` | (universal only — person via auth) | +| `incident_report` | `IncidentReportGuards` | (universal only — anonymous-allowed) | +| `signature_contract` | `SignatureContractGuards` | (universal only — user via auth) | +| `user_profile` | `UserProfileGuards` | (universal only — user via auth) | + +Universal guards (`MaxOneIdentityKeyPerTargetEntity`, +`AppendStrategyRequiresCollectionTarget`, `NoAmbiguousTrustLevels`, +`IdentityKeyBindingsOnlyInFirstSection`) wire into every purpose. The +section guard is a cheap no-op when `section_level_submit=false`, but +remains active at publish time for schemas that flip it on later. + +i18n message keys live in +`api/lang/nl/form_builder_publish_guards.php`. Dutch only for v1. + +## 6. Apply pipeline + +### 6.1 Snapshot vs. live (RFC Q6) + +> Populated in session 2 — see RFC-WS-6.md §3 Q6. + +### 6.2 Conflict resolution (RFC Q7) + +> Populated in session 2 — see RFC-WS-6.md §3 Q7. + +### 6.3 Apply algorithm and merge-strategy null matrix (RFC V1, Q7) + +> Populated in session 2 — see RFC-WS-6.md §4 V1 + §3 Q7. + +### 6.4 Person provisioning (RFC Q8) + +> Populated in session 2 — see RFC-WS-6.md §3 Q8. + +### 6.5 Per-purpose subject resolution (RFC Q9) + +> Populated in session 2 — see RFC-WS-6.md §3 Q9. + +### 6.6 Section-level apply stub (RFC Q10) + +> Populated in session 2 — see RFC-WS-6.md §3 Q10. + +### 6.7 Activity log granularity (RFC Q12) + +> Populated in session 2 — see RFC-WS-6.md §3 Q12. + +## 7. Failures and retry + +### 7.1 Two-transaction pattern (RFC Q4) + +> Populated in session 2 — see RFC-WS-6.md §3 Q4. + +### 7.2 Retry, resolve, dismiss flows (RFC V2) + +> Populated in session 2 — see RFC-WS-6.md §4 V2. + +## 8. Multi-tenancy and security + +### 8.1 FK-chain tenant resolution (RFC V3) + +> Populated in session 2 — see RFC-WS-6.md §4 V3. + +### 8.2 IDOR class tests + +> Populated in session 3 — see RFC-WS-6.md §4 V3. + +## 9. Listener chain + +> Populated in session 2 — see RFC-WS-6.md §3 Q1, Q2, Q3. + +## 10. Out of scope (v1) + +- **Composite identity-key resolution** (single-key only) — BACKLOG: + `FORM-BINDING-COMPOSITE-IDENTITY` +- **Library-binding runtime cascade** — BACKLOG: `FORM-LIBRARY-RESYNC` +- **Append on scalar targets** — collection-only by design (RFC V1) +- **Active section-level apply** — stub only, activated when + ARTIST_ADVANCE feature work begins (BACKLOG: `ARTIST-ADV-SECTION-APPLY`) +- **Daily failure digest mailable** — depends on notification framework +- **Wall-clock concurrent load testing** — BACKLOG: `LOAD-TEST-FOUNDATION` + +## 11. Related docs + +- `RFC-WS-6.md` — design decisions +- `ARCH-FORM-BUILDER.md` §17, §31 +- `ARCH-CONSOLIDATION-2026-04.md` §6.1, §6.2 +- `SCHEMA.md` §3.5.12