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>
This commit is contained in:
237
dev-docs/ARCH-BINDINGS.md
Normal file
237
dev-docs/ARCH-BINDINGS.md
Normal file
@@ -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<string>`
|
||||||
|
- `attributesFor(entity): list<string>`
|
||||||
|
- `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
|
||||||
Reference in New Issue
Block a user