docs(rfc-ws-6): v1.3.1 + ARCH-BINDINGS v1.2 — drift closure pre-D1 implementation

Three code-vs-docs drifts surfaced by the 2026-05-08 v1.3-delta audit.
None changes architecture; all three close the gap between code on main
(845b6e6) and the v1.3 amendment text.

- RFC §3 (Q1): apply_status enumerations updated to four cases (added
  PARTIAL alongside PENDING/COMPLETED/FAILED). PARTIAL is the
  BindingPassResult outcome when the pass committed with mixed
  per-binding outcomes; not a separate runtime path. Long-term direction
  remains BACKLOG PARTIAL-BINDING-SUCCESS.
- ARCH-BINDINGS §5.6: new "PARTIAL handling" subsection clarifying the
  gate treats PARTIAL identically to FAILED until partial-success work
  lands. The gate code itself was already correct (strict equality on
  COMPLETED); this closes the explanatory gap.
- ARCH-BINDINGS §7.1: status-columns table extended with apply_completed_at
  row. Intro line updated. Retry-service asymmetry noted as D2 follow-up
  (FormFailureRetryService::recordFailure currently does not write
  apply_completed_at; D2 fixes this).

RFC v1.3 -> v1.3.1; ARCH-BINDINGS v1.1 -> v1.2.

Refs: dev-docs/RFC-WS-6.md, dev-docs/ARCH-BINDINGS.md, dev-docs/BACKLOG.md (PARTIAL-BINDING-SUCCESS, unchanged)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-08 01:32:19 +02:00
parent 845b6e6a0e
commit b2558791e6
2 changed files with 36 additions and 6 deletions

View File

@@ -1,6 +1,6 @@
# ARCH-BINDINGS.md — FormBindingApplicator Pipeline # ARCH-BINDINGS.md — FormBindingApplicator Pipeline
**Version:** v1.1 — incorporates RFC-WS-6 v1.3 refinements (architectural review 2026-05-07) **Version:** v1.2 — incorporates RFC-WS-6 v1.3.1 drift closure (2026-05-08)
**Aligned to:** `main` HEAD `4a84b9e` (post WS-6 closure, 2026-05-04) **Aligned to:** `main` HEAD `4a84b9e` (post WS-6 closure, 2026-05-04)
**Authoritative:** Yes. This document supersedes `RFC-WS-6.md` for ongoing maintenance — RFC-WS-6 remains as a historical record of the 2026-04-25 design session that produced this architecture. **Authoritative:** Yes. This document supersedes `RFC-WS-6.md` for ongoing maintenance — RFC-WS-6 remains as a historical record of the 2026-04-25 design session that produced this architecture.
@@ -365,6 +365,14 @@ The gate is enforced by the §5.5 listener-ordering test: a contributor adding a
The gate uses `fresh()` rather than the in-memory event submission because the inner-transaction commit + outer-transaction failure-record write happens between event dispatch and queued-listener execution. The DB has the canonical `apply_status` value; the in-memory model does not. The gate uses `fresh()` rather than the in-memory event submission because the inner-transaction commit + outer-transaction failure-record write happens between event dispatch and queued-listener execution. The DB has the canonical `apply_status` value; the in-memory model does not.
#### PARTIAL handling
`apply_status=partial` means the inner transaction committed but at least one binding in the pass failed (per `BindingPassResult::applyStatus()`). The gate skips PARTIAL submissions identically to FAILED submissions: queued listeners cannot safely assume the subject + bindings are coherent. For example, `SyncTagPickerSelectionsOnSubmit` rebuilds `user_organisation_tags` against a Person whose tag-binding may have been the binding that failed; running the listener would propagate the partial state into derived data.
Until BACKLOG `PARTIAL-BINDING-SUCCESS` lands, the gate's positive branch is restricted to `COMPLETED` only. The PARTIAL row stays visible in the failures UI through the per-binding failure rows that `FormBindingApplicator` writes inside `BindingPassResult::failures()`, so admins can triage without losing audit data.
If a future contributor extends the gate to accept PARTIAL, the structural test asserting "skip on every non-COMPLETED state" must be updated alongside the listener changes — never one without the other.
--- ---
## 6. Atomicity — two-transaction failure-write pattern ## 6. Atomicity — two-transaction failure-write pattern
@@ -480,16 +488,19 @@ If the outer (failure-record) transaction throws, the `Log::error` call still fi
### 7.1 Two independent concerns ### 7.1 Two independent concerns
`form_submissions` carries two status columns and one failure-classification column that the pipeline manages: `form_submissions` carries two status columns, one failure-classification column, and one apply-finalisation timestamp that the pipeline manages:
| Column | Type | Default | Indexed | Managed by | | Column | Type | Default | Indexed | Managed by |
|---|---|---|---|---| |---|---|---|---|---|
| `apply_status` | string(20) nullable | NULL | yes | `ApplyBindingsOnFormSubmit` (sync), retry/resolve flows | | `apply_status` | string(20) nullable | NULL | yes | `ApplyBindingsOnFormSubmit` (sync), retry/resolve flows |
| `identity_match_status` | string(20) nullable | NULL | yes | `ApplyBindingsOnFormSubmit` writes initial `'pending'` for `subject_type='person'`; `TriggerPersonIdentityMatchOnFormSubmit` (queued) writes final state; `PersonIdentityService::confirmMatch` (manual) | | `identity_match_status` | string(20) nullable | NULL | yes | `ApplyBindingsOnFormSubmit` writes initial `'pending'` for `subject_type='person'`; `TriggerPersonIdentityMatchOnFormSubmit` (queued) writes final state; `PersonIdentityService::confirmMatch` (manual) |
| `failure_response_code` | string(40) nullable | NULL | yes | `ApplyBindingsOnFormSubmit` outer-transaction failure path. Values: `schema_config_error`, `temporary_error`, `data_integrity_error`, `unknown_error` (per RFC-WS-6 §Q3 v1.3 addition 2). Serialized into API response body when `apply_status=failed` so the frontend can render contextual copy. | | `failure_response_code` | string(40) nullable | NULL | yes | `ApplyBindingsOnFormSubmit` outer-transaction failure path. Values: `schema_config_error`, `temporary_error`, `data_integrity_error`, `unknown_error` (per RFC-WS-6 §Q3 v1.3 addition 2). Serialized into API response body when `apply_status=failed` so the frontend can render contextual copy. |
| `apply_completed_at` | timestamp nullable | NULL | no | `ApplyBindingsOnFormSubmit::handle` (writes `now()` in both the inner-transaction success path AND the outer-transaction failure path, paired with the corresponding `apply_status` write) and `FormFailureRetryService::retry` (success path). Set whenever `apply_status` transitions to a terminal state (COMPLETED, PARTIAL, or FAILED). NULL means apply has never run on this submission. Used by the failures UI to render "Failed at …" / "Completed at …" copy without joining to `form_submission_action_failures`. |
They are **independent**. A submission can be `apply_status=completed` AND `identity_match_status=pending` — the bindings landed, but the organizer hasn't confirmed which existing Person this submission's just-provisioned Person should link to. Both states must be visible in the UI as separate signals, not collapsed into a single "submission status" indicator. `failure_response_code` is null on success and populated only on `apply_status=failed`. They are **independent**. A submission can be `apply_status=completed` AND `identity_match_status=pending` — the bindings landed, but the organizer hasn't confirmed which existing Person this submission's just-provisioned Person should link to. Both states must be visible in the UI as separate signals, not collapsed into a single "submission status" indicator. `failure_response_code` is null on success and populated only on `apply_status=failed`.
`FormFailureRetryService::recordFailure` does NOT currently write `apply_completed_at` on the retry-failure path; this asymmetry is fixed as part of the v1.3-delta D2 work (RFC v1.3 §Q3 addition 2 implementation).
### 7.2 `ApplyStatus` enum ### 7.2 `ApplyStatus` enum
```php ```php
@@ -497,18 +508,22 @@ enum ApplyStatus: string
{ {
case PENDING = 'pending'; case PENDING = 'pending';
case COMPLETED = 'completed'; case COMPLETED = 'completed';
case PARTIAL = 'partial';
case FAILED = 'failed'; case FAILED = 'failed';
} }
``` ```
`PARTIAL` is not a separate runtime path through `ApplyBindingsOnFormSubmit::handle` — it is the value `BindingPassResult::applyStatus()` returns when the pass committed but at least one individual binding failed (mixed per-binding outcomes inside a single applicator pass). Per RFC-WS-6 §Q3 v1.3 addition 3, granular partial-success handling is BACKLOG `PARTIAL-BINDING-SUCCESS`. Until that work lands, `PARTIAL` is treated identically to `FAILED` by the queued-listener gate (see §5.6).
The column itself defaults to `NULL` rather than `'pending'`. This is deliberate: NULL means **this schema has no bindings to apply** (e.g., a `feedback` submission with no entity-bound fields). Using `pending` as the default would force every unrelated submission into a pipeline-state it has no business in. The column itself defaults to `NULL` rather than `'pending'`. This is deliberate: NULL means **this schema has no bindings to apply** (e.g., a `feedback` submission with no entity-bound fields). Using `pending` as the default would force every unrelated submission into a pipeline-state it has no business in.
State transitions: State transitions:
``` ```
NULL ──(submission of binding-bearing schema)──▶ pending NULL ──(submission of binding-bearing schema)──▶ pending
pending ──(apply succeeds)──▶ completed pending ──(apply succeeds, all bindings)──▶ completed
pending ──(apply throws)──▶ failed pending ──(apply commits, mixed per-binding outcomes)──▶ partial
pending ──(apply throws / deadline exceeded)──▶ failed
failed ──(retry succeeds)──▶ completed failed ──(retry succeeds)──▶ completed
failed ──(retry throws)──▶ failed (action_failure row gets new attempts entry) failed ──(retry throws)──▶ failed (action_failure row gets new attempts entry)
``` ```
@@ -1546,6 +1561,7 @@ When an item moves from out-of-scope to in-scope (e.g., artist_advance work begi
|---|---|---|---| |---|---|---|---|
| v1.0 | 2026-05-05 | Bert Hausmans (architect), Claude Chat (drafting) | Initial canonical reference, post-WS-6. Aligned to main HEAD `4a84b9e`. | | v1.0 | 2026-05-05 | Bert Hausmans (architect), Claude Chat (drafting) | Initial canonical reference, post-WS-6. Aligned to main HEAD `4a84b9e`. |
| v1.1 | 2026-05-07 | Bert Hausmans (architect), Claude Chat (drafting) | Incorporates RFC-WS-6 v1.3 refinements (architectural review 2026-05-07). Twelve section edits, one new section: header version note; §5.1 SYNC chain reduced to one listener; §5.2 listener catalogue updated (TriggerPersonIdentityMatch sync→queued, gating-invariant cross-refs, deadline wrapper, failure_response_code mirror); §5.3 rewritten as "Why SYNC for ApplyBindings only" with deadline-wrapper note; §5.5 listener-ordering test extended for queueing + gating introspection; **§5.6 (new) Queued-listener gating invariant**; §6.2 deadline-wrapper + failure_response_code notes; §7.1 storage table extended with `failure_response_code`; §7.3 failsafe-pad replaced with explicit invariant statement; §8.5 `RequiresIdentityKeyBinding` becomes unconditional for `event_registration`; §11.1 cross-ref to `failure_response_code` companion column; §19 Partial-binding row points at `PARTIAL-BINDING-SUCCESS`, new row for `FORM-SCHEMA-DRIFT-DETECTION`. Spine unchanged (pre-publish guards, strict service / log-and-swallow listener, two-transaction pattern, sync ApplyBindings, snapshot-isolation, single-identity-key per target_entity). | | v1.1 | 2026-05-07 | Bert Hausmans (architect), Claude Chat (drafting) | Incorporates RFC-WS-6 v1.3 refinements (architectural review 2026-05-07). Twelve section edits, one new section: header version note; §5.1 SYNC chain reduced to one listener; §5.2 listener catalogue updated (TriggerPersonIdentityMatch sync→queued, gating-invariant cross-refs, deadline wrapper, failure_response_code mirror); §5.3 rewritten as "Why SYNC for ApplyBindings only" with deadline-wrapper note; §5.5 listener-ordering test extended for queueing + gating introspection; **§5.6 (new) Queued-listener gating invariant**; §6.2 deadline-wrapper + failure_response_code notes; §7.1 storage table extended with `failure_response_code`; §7.3 failsafe-pad replaced with explicit invariant statement; §8.5 `RequiresIdentityKeyBinding` becomes unconditional for `event_registration`; §11.1 cross-ref to `failure_response_code` companion column; §19 Partial-binding row points at `PARTIAL-BINDING-SUCCESS`, new row for `FORM-SCHEMA-DRIFT-DETECTION`. Spine unchanged (pre-publish guards, strict service / log-and-swallow listener, two-transaction pattern, sync ApplyBindings, snapshot-isolation, single-identity-key per target_entity). |
| v1.2 | 2026-05-08 | Bert Hausmans (architect), Claude Code (drafting) | §5.6 gained the PARTIAL-handling subsection; §7.1 gained the `apply_completed_at` row + intro update + retry-service asymmetry note; §7.2 enum block updated to four cases (added `PARTIAL`) with explanatory paragraph + state-transition diagram updated. No structural changes. Companion: RFC-WS-6 v1.3.1. |
### 20.3 Update protocol ### 20.3 Update protocol

View File

@@ -3,8 +3,8 @@
## 1. Status ## 1. Status
- **State:** Authoritative for sessions 1, 2, 3 of WS-6 - **State:** Authoritative for sessions 1, 2, 3 of WS-6
- **Frozen:** 2026-04-25 (v1.0); refined post-session-2 cleanup as v1.1, then v1.2 (sessie 3a.5), then v1.3 (architectural review 2026-05-07) — see §10 - **Frozen:** 2026-04-25 (v1.0); refined post-session-2 cleanup as v1.1, then v1.2 (sessie 3a.5), then v1.3 (architectural review 2026-05-07); v1.3.1 (2026-05-08) — code-vs-docs drift closure pre-D1 implementation — see §10
- **Version:** v1.3 - **Version:** v1.3.1
- **Owner:** Bert Hausmans - **Owner:** Bert Hausmans
- **Origin:** Architectural session 2026-04-25 (Claude Chat) — 13 design decisions, 4 refinements, 3 observations - **Origin:** Architectural session 2026-04-25 (Claude Chat) — 13 design decisions, 4 refinements, 3 observations
- **Related:** - **Related:**
@@ -160,6 +160,19 @@ massive person pools — neither expected in v1.0).
This addition guarantees that no submission can hang the public flow for more than This addition guarantees that no submission can hang the public flow for more than
a bounded interval. Slow paths surface as failures, not as hung connections. a bounded interval. Slow paths surface as failures, not as hung connections.
#### Q1 v1.3.1 clarification — `apply_status` four-case enumeration
`apply_status` has four states:
- `pending` — apply has started but the inner transaction has not committed
- `completed` — every binding in the pass succeeded; the inner transaction committed
- `partial` — at least one binding in the pass failed AND at least one succeeded; the inner transaction committed (per `BindingPassResult::applyStatus()`); see BACKLOG `PARTIAL-BINDING-SUCCESS` for the long-term direction
- `failed` — every binding failed OR the deadline-wrapper threw; the inner transaction rolled back; an entry exists in `form_submission_action_failures`
NULL means apply has not yet run on this submission.
`PARTIAL` is not a separate runtime path through `ApplyBindingsOnFormSubmit::handle` — it is the value `BindingPassResult::applyStatus()` returns when the pass committed but at least one individual binding failed. Per RFC v1.3 §Q3 addition 3, granular partial-success handling is BACKLOG `PARTIAL-BINDING-SUCCESS`. Until that work lands, `PARTIAL` is treated identically to `FAILED` by the queued-listener gate (see ARCH-BINDINGS §5.6).
### Q2 — Refactor of `TriggerPersonIdentityMatchOnFormSubmit` ### Q2 — Refactor of `TriggerPersonIdentityMatchOnFormSubmit`
**Decision (v1.3):** **Remove** the "no subject → pending" failsafe path. Replace with **Decision (v1.3):** **Remove** the "no subject → pending" failsafe path. Replace with
@@ -845,3 +858,4 @@ WS-7 sessie 1.
- **§Q2 — `TriggerPersonIdentityMatchOnFormSubmit` refactor.** Failsafe-pad ("no subject → pending" logged warning) replaced with explicit invariant + strict throw routed via `form_submission_action_failures`. Companion: `RequiresIdentityKeyBinding('person', 'email')` wires unconditionally for `event_registration` (drop the `ConditionalRequirement(public_token !== null)` wrapper). Companion: `FormSubmissionResource.identity_match=null` made an explicit contract for non-person purposes. - **§Q2 — `TriggerPersonIdentityMatchOnFormSubmit` refactor.** Failsafe-pad ("no subject → pending" logged warning) replaced with explicit invariant + strict throw routed via `form_submission_action_failures`. Companion: `RequiresIdentityKeyBinding('person', 'email')` wires unconditionally for `event_registration` (drop the `ConditionalRequirement(public_token !== null)` wrapper). Companion: `FormSubmissionResource.identity_match=null` made an explicit contract for non-person purposes.
- **§Q3 — Strict-fail vs compatibility.** Spine confirmed unchanged. Three failure-UX additions: GlitchTip alert rule on `FormBindingApplicatorException` for production public flows; custom exception hierarchy (`FormBindingSchemaConfigException` / `FormBindingInfraException` / `FormBindingDataIntegrityException`) + `failure_response_code` column on `form_submissions` + `error_code` in HTTP response body; "all-or-nothing per pass" gets explicit BACKLOG entry `PARTIAL-BINDING-SUCCESS`. Fourth addition: schema-drift detection as separate BACKLOG entry `FORM-SCHEMA-DRIFT-DETECTION` (not v1.0 scope). - **§Q3 — Strict-fail vs compatibility.** Spine confirmed unchanged. Three failure-UX additions: GlitchTip alert rule on `FormBindingApplicatorException` for production public flows; custom exception hierarchy (`FormBindingSchemaConfigException` / `FormBindingInfraException` / `FormBindingDataIntegrityException`) + `failure_response_code` column on `form_submissions` + `error_code` in HTTP response body; "all-or-nothing per pass" gets explicit BACKLOG entry `PARTIAL-BINDING-SUCCESS`. Fourth addition: schema-drift detection as separate BACKLOG entry `FORM-SCHEMA-DRIFT-DETECTION` (not v1.0 scope).
- Companion: `ARCH-BINDINGS.md` advances to v1.1 (twelve section edits per the v1.3 amendment companion table); `BACKLOG.md` adds `PARTIAL-BINDING-SUCCESS` and `FORM-SCHEMA-DRIFT-DETECTION` under Form Builder backlog. RFC-WS-6 v1.3, ARCH-BINDINGS v1.1, and the BACKLOG additions land in the same commit. - Companion: `ARCH-BINDINGS.md` advances to v1.1 (twelve section edits per the v1.3 amendment companion table); `BACKLOG.md` adds `PARTIAL-BINDING-SUCCESS` and `FORM-SCHEMA-DRIFT-DETECTION` under Form Builder backlog. RFC-WS-6 v1.3, ARCH-BINDINGS v1.1, and the BACKLOG additions land in the same commit.
- 2026-05-08 — v1.3.1 — Pre-D1-implementation drift closure. (1) Updated apply_status enumerations throughout §3 to include the PARTIAL case (which exists in code per `BindingPassResult::applyStatus()` and was not anticipated by the v1.3 amendment author). (2) ARCH-BINDINGS §5.6 received a PARTIAL-handling clarification: gate treats PARTIAL identically to FAILED, deferring granular partial-success to BACKLOG `PARTIAL-BINDING-SUCCESS`. (3) ARCH-BINDINGS §7.1 status-columns table extended with `apply_completed_at` row + cross-reference to the D2 retry-service symmetry fix. No spine changes; no behaviour changes; documentation truth-in-naming. Companion: ARCH-BINDINGS.md advances to v1.2.