From b2558791e6c33e1bcff846c1202ee99b8be71e0c Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 01:32:19 +0200 Subject: [PATCH] =?UTF-8?q?docs(rfc-ws-6):=20v1.3.1=20+=20ARCH-BINDINGS=20?= =?UTF-8?q?v1.2=20=E2=80=94=20drift=20closure=20pre-D1=20implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- dev-docs/ARCH-BINDINGS.md | 24 ++++++++++++++++++++---- dev-docs/RFC-WS-6.md | 18 ++++++++++++++++-- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/dev-docs/ARCH-BINDINGS.md b/dev-docs/ARCH-BINDINGS.md index 34697281..c7ace2a8 100644 --- a/dev-docs/ARCH-BINDINGS.md +++ b/dev-docs/ARCH-BINDINGS.md @@ -1,6 +1,6 @@ # 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) **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. +#### 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 @@ -480,16 +488,19 @@ If the outer (failure-record) transaction throws, the `Log::error` call still fi ### 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 | |---|---|---|---|---| | `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) | | `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`. +`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 ```php @@ -497,18 +508,22 @@ enum ApplyStatus: string { case PENDING = 'pending'; case COMPLETED = 'completed'; + case PARTIAL = 'partial'; 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. State transitions: ``` NULL ──(submission of binding-bearing schema)──▶ pending -pending ──(apply succeeds)──▶ completed -pending ──(apply throws)──▶ failed +pending ──(apply succeeds, all bindings)──▶ completed +pending ──(apply commits, mixed per-binding outcomes)──▶ partial +pending ──(apply throws / deadline exceeded)──▶ failed failed ──(retry succeeds)──▶ completed 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.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 diff --git a/dev-docs/RFC-WS-6.md b/dev-docs/RFC-WS-6.md index 8f128289..177077bb 100644 --- a/dev-docs/RFC-WS-6.md +++ b/dev-docs/RFC-WS-6.md @@ -3,8 +3,8 @@ ## 1. Status - **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 -- **Version:** v1.3 +- **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.1 - **Owner:** Bert Hausmans - **Origin:** Architectural session 2026-04-25 (Claude Chat) — 13 design decisions, 4 refinements, 3 observations - **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 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` **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. - **§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. +- 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.