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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user