diff --git a/docs/Architecture_Note_Amendments.md b/docs/Architecture_Note_Amendments.md new file mode 100644 index 0000000..1cdf1d8 --- /dev/null +++ b/docs/Architecture_Note_Amendments.md @@ -0,0 +1,236 @@ +# Architecture Note — Amendments + +**Reference:** *Multi-Layer Atomic Settlement Architecture for SBLC Issuance and +Payment Coordination* (Draft 1.0). +**Purpose:** Three amendments identified during the CurrenciCombo gap-analysis +(§2 of `docs/ADRs` / gap-analysis note) that tighten the contract between the +note and the orchestrator implementation landing in PRs A–G. + +These amendments are **normative**: where the text here conflicts with the +original draft, this document takes precedence. + +--- + +## Amendment 1 — §5.1 Transaction Coordinator (trust model) + +### Problem + +The original §5.1 names the Transaction Coordinator but does not specify **who +runs it** or **what trust assumptions the other participants must accept** to +use it. In a multi-bank SBLC + payment flow this is not a detail — the +Coordinator holds the state registry, issues `transaction.prepared` +instructions, and decides `COMMITTED` vs `ABORTED`. Whoever runs it is, in +effect, the workflow authority. + +Three candidate topologies exist: + +1. **Single-party hosted** — one participant (e.g. the issuing bank, the + beneficiary's bank, or a shared utility) runs a single Coordinator instance + and the rest consume its API. +2. **Federated** — each participant runs their own Coordinator; they reach + consensus over the state via signed events exchanged peer-to-peer (the + architecture note §7 normalised events). +3. **Neutral third-party utility** — a non-participant (e.g. a FinTech utility, + a central bank–adjacent entity, or an SRO) runs the Coordinator under a + published operating model. + +### Amendment text (replaces §5.1) + +> **5.1 Transaction Coordinator.** Central orchestration service that manages +> the lifecycle of a transaction instance. The operator of the Coordinator +> SHALL be named in the governing documents (§4.1) as the *Workflow Authority*. +> The Workflow Authority: +> +> - is a single named legal entity for any given transaction; +> - MUST be a participant in, or a party contractually bound to, that +> transaction's governing documents; +> - MUST NOT be the same entity that provides the Identity and Authorization +> Service (§5.8) or the Ledger Anchor (§5.7) — separation of the control +> plane, the trust anchor, and the audit anchor is a requirement, not an +> option; +> - MUST publish its operating model, availability commitments, and exception +> escalation paths to all participants; +> - MUST sign every state transition it records to the State Registry (§5.6) +> with a key bound to its identity in the Identity and Authorization +> Service; participants verify those signatures before accepting a state as +> canonical. +> +> CurrenciCombo's reference topology is (1) **single-party hosted**, where +> the issuing bank of the SBLC operates the Coordinator and the payment-side +> bank, beneficiary, and applicant consume its API. Federated (2) and +> neutral-utility (3) topologies are out of scope for v1 but are not +> prohibited — they can be layered on top by replacing the Coordinator +> implementation while preserving the API surface. + +### Implementation impact + +- **`orchestrator` (CurrenciCombo)**: the orchestrator IS the Coordinator. + `NOTARY_REGISTRY_ADDRESS` (Ledger Anchor) and the signing key used for the + event bus (§5.8 / PR D `EVENT_BUS_SECRET`) must be held in separate key + stores. PR A's SoD matrix (`stateMachine.ts`) already prevents a single + actor from driving the 4 SoD-gated transitions. +- **Operational**: the config (`orchestrator/src/config/env.ts`) SHOULD grow + a `WORKFLOW_AUTHORITY_NAME` + `WORKFLOW_AUTHORITY_JWK_URL` pair so + consumers can resolve and verify the Coordinator's identity without + out-of-band trust. Tracked as a follow-up ticket; not blocking. + +--- + +## Amendment 2 — §9.2 Commit Rule ("accepted ≠ settled") + +### Problem + +§9.2 currently reads: + +> A transaction may enter **COMMITTED** only when: +> - the instrument leg has produced valid dispatch evidence +> - the payment leg has produced valid settlement or **accepted completion +> evidence** +> - all key transaction attributes reconcile against expected values +> - no outstanding exception blocks remain + +The phrase "accepted completion evidence" is too loose. In SWIFT and ISO 20022 +terms, **acceptance is not settlement**: + +| Message | Meaning | Is settlement? | +|------------------|-------------------------------------------------|----------------| +| `pacs.002 ACCP` | Instruction technically accepted by receiver | **No** | +| `pacs.002 ACSP` | Accepted, settlement in process | **No** | +| `pacs.002 ACSC` | Accepted, settlement completed | Yes | +| `camt.025 ACCP` | Receipt: accepted | **No** | +| `camt.025 ACSC` | Receipt: settlement completed | Yes | +| `camt.054 CRDT` | Account credit notification | Yes (on receiver) | +| MT910 | Confirmation of credit | Yes | +| MT900 | Confirmation of debit | Yes (on sender) | + +Treating `ACCP` as sufficient for `COMMITTED` introduces a window where the +Coordinator has locked-in the issuance but the payment has not cleared — the +exact failure mode the two-phase commit was meant to prevent. + +### Amendment text (replaces §9.2) + +> **9.2 Commit Rule.** A transaction may enter **COMMITTED** only when **all** +> of the following are true: +> +> 1. The instrument leg has produced valid dispatch evidence: an authenticated +> `MT760` issuance acknowledgment or an ISO 20022 instrument-specific +> equivalent, signed and time-stamped. +> 2. The payment leg has produced valid **settlement** evidence — not merely +> acceptance. Valid settlement evidence is one of: +> - `pacs.002` with status `ACSC` on the pacs.009 / pacs.008 interbank +> leg; +> - `camt.025` with status `ACSC`; +> - `camt.054` credit notification referencing the expected +> `EndToEndIdentification`, `InstructedAmount`, and `Currency`; +> - An `MT910` (credit confirmation) on the beneficiary side or an +> `MT900` (debit confirmation) on the originator side with matching +> transaction reference and amount. +> 3. The Coordinator has run the `VALIDATING` phase (§4.3 / PR B) and all +> reconciliation checks have passed — in particular amount, currency, +> credit/debit direction, and end-to-end identifier. +> 4. No outstanding exception blocks remain in the Exception Manager (§5.9 / +> PR B). +> +> `ACCP` / `ACSP` / `PDNG` statuses SHALL NOT satisfy (2) on their own. If +> only acceptance-level evidence has arrived and the settlement-deadline +> timer has not expired, the transaction remains in `VALIDATING`. On timer +> expiry, the Coordinator transitions to `ABORTED` under §9.3 timing-exception +> rules. + +### Implementation impact + +- **`orchestrator/src/services/swift/camt.ts` (PR E)**: `parseCamt025` already + distinguishes `ACCP | ACSC | ACSP | RJCT | PDNG`. The + `ExecutionCoordinator` must only accept `ACSC` (camt.025) or `CRDT` + (camt.054 matching reconciliation) as the settlement trigger. **Tracked + follow-up**: wire this into `executionCoordinator.validatePlan()` so that + `VALIDATING → COMMITTED` is blocked when the latest camt message is + `ACCP` / `ACSP`. Current code does not reference these statuses yet; + correctness is preserved today only because the mocked dispatch always + synthesises `ACSC`. +- **`orchestrator/src/services/exceptionManager.ts` (PR B)**: add a + `Timing.settlementDeadlineExpired` class routed to `ABORTED`. Current + taxonomy has generic `Timing.dispatchTimeout` / `acknowledgmentDelay` + which is too coarse for this distinction. + +--- + +## Amendment 3 — §4.1 UNWIND_PENDING matrix (MT760 irrevocability) + +### Problem + +§8.1 defines a single `UNWIND_PENDING` state after `ABORTED`, and §11 Phase 6 +says "if needed, initiate unwind process". This glosses over a hard banking +fact: **an issued MT760 guarantee/SBLC is irrevocable under UCP 600 / URDG +758** once it has been dispatched to the beneficiary's bank. The set of +"unwind" actions is therefore not uniform — it depends on *which leg* has +progressed how far. + +The original note's state diagram implies `UNWIND_PENDING` is reachable from +any `ABORTED`, regardless of what was already dispatched. That is +operationally wrong: if the MT760 has left the issuing bank and been +authenticated by the beneficiary's bank, the instrument itself **cannot be +withdrawn unilaterally** — it can only be discharged (on expiry or on +beneficiary release) or replaced by a counter-instrument. + +### Amendment text (adds §4.1.1 and refines §8.1 / §11 Phase 6) + +> **4.1.1 Instrument irrevocability matrix.** The `UNWIND_PENDING` state +> SHALL NOT imply that the instrument leg is reversible. The set of unwind +> actions available depends on the observable state of each leg at the +> moment of `ABORTED`: +> +> | Instrument-leg observable state | Instrument unwind action | +> |--------------------------------------------|--------------------------| +> | `instrument.dispatched` not yet emitted | **Withdraw** — cancel before dispatch. No counter-instrument needed. | +> | `instrument.dispatched` emitted, `instrument.acknowledged` not yet emitted | **Recall request** — non-binding; beneficiary's bank MAY reject. If rejected, fall through to "acknowledged" row. | +> | `instrument.acknowledged` emitted | **Irrevocable — no unwind available.** The instrument stands until expiry (§11 tenor) or beneficiary-side release. The only control-plane actions are (a) accelerated expiry on mutual written consent, (b) issuance of a **counter-guarantee** from the beneficiary of the original instrument back to the applicant, (c) legal discharge via governing-law procedure. | +> +> | Payment-leg observable state | Payment unwind action | +> |--------------------------------------------|--------------------------| +> | `payment.dispatched` not yet emitted | **Withhold** — do not dispatch. | +> | `payment.dispatched` emitted, `payment.accepted` not yet emitted | **Recall** (`pacs.028` request-to-modify / `camt.056` request-for-cancellation). Best-effort. | +> | `payment.accepted` but not `payment.settled` | **Recall before settlement** — MAY succeed depending on receiver bank's processing window. | +> | `payment.settled` | **Return payment** — requires a fresh, separately-instructed return payment (`pacs.009` in reverse direction). Not an unwind of the original; a compensating transfer. | +> +> `UNWIND_PENDING` is a **state of the orchestrator**, not a guarantee that +> the underlying banking artefacts can be reversed. On entry to +> `UNWIND_PENDING`, the Coordinator SHALL record in the State Registry the +> observable state of each leg at the moment of `ABORTED` and the unwind +> action selected from the matrix above. + +### Implementation impact + +- **`orchestrator/src/services/stateMachine.ts` (PR A)**: the transition + table is unchanged (`ABORTED → UNWIND_PENDING → CLOSED` remains valid). + What changes is the *payload* recorded on the `ABORTED → UNWIND_PENDING` + transition — the `reason` field must include the instrument-leg and + payment-leg observable states. +- **`orchestrator/src/services/execution.ts`**: on entry to + `UNWIND_PENDING`, the Coordinator must select and persist the unwind + actions per the matrix. **Tracked follow-up**: this requires the + ExecutionCoordinator to consume real SWIFT events (PR E outbound + + inbound parsers are in place; wiring them to drive `instrument.dispatched` + / `instrument.acknowledged` / `payment.*` events is a separate + coordinator-focused PR). +- **Portal `/transactions` page (PR G)**: the audit-trail card already + renders `reason` inline; no UI change required. The unwind-action + tracking will naturally surface as additional event rows. + +--- + +## Summary of downstream tickets + +Tracked as separate work items, not blockers for A–G: + +1. `WORKFLOW_AUTHORITY_NAME` + JWK URL in orchestrator env (Amendment 1). +2. Wire `executionCoordinator.validatePlan()` to discriminate `ACCP`/`ACSP` + from `ACSC`/`CRDT` using the PR E parsers (Amendment 2). +3. Add `Timing.settlementDeadlineExpired` to the Exception taxonomy + (Amendment 2). +4. Capture instrument-leg and payment-leg observable state in the + `ABORTED → UNWIND_PENDING` transition `reason` field (Amendment 3). +5. Persist the selected unwind action per the matrix in Amendment 3. + +None of the five items regress A–G — they extend behaviour on top of +already-landed structures.