Multi-Party Approval
Table of contents
- Multi-Party Approval
- Intent
- Summary
- Composes
- Composition logic
- Composition-level invariants
- Examples
- Walkthrough — SOX-controlled journal entry, all-of-N quorum
- Happy path — FDA Part 11 batch release, M-of-N(2) quorum across three qualified persons
- Happy path — ICH E6 GCP protocol deviation, one-of-N quorum across a delegated approver pool
- Rejection path — all-of-N quorum, one approver rejects
- Rejection path — chain withdrawal by initiator
- Regulated adversarial scenarios
- Generation acceptance
- Edge cases and explicit non-goals
- Standards references
- Status
- Lineage notes
A regulated application: an enforced approval chain over a single subject and scope, wiring N Approval Step instances under a named quorum rule (all-of-N, M-of-N, one-of-N) into a single auditable decision. Composes Approval Step (the per-gate primitive), Permissions (who may initiate, withdraw, and read chains), and Assignment (the in-tray binding for each pending step), with Audit Trail as the regulated-audit substrate (Event Log + Actor Identity + Retention Window + Tamper Evidence) that captures every chain-level and step-level action attribution-stamped, retention-bounded, and tamper-evident. The emergent guarantee: a required N-approver gate cannot be bypassed, the quorum rule is deterministic from the records, the full approval chain (who decided what, when, in what order, under what scope) is reproducible from the records alone, and no chain reaches Approved without the quorum-named decisions actually present in the Approval Step store.
Intent
Many regulated actions require more than one human approval before they may proceed. A SOX-controlled journal entry above a materiality threshold requires the controller and the CFO. A pharmaceutical batch release under 21 CFR Part 211 requires the qualified person on duty plus the QA director. A clinical protocol deviation under ICH E6 GCP requires the principal investigator plus, for substantive deviations, the IRB chair. A high-value engineering change order requires the engineering lead, the quality lead, and (when safety-critical) the safety officer. In each case the structure is the same: a set of named approvers, a quorum rule (all of them, a majority of them, any one of them), and a terminal decision that becomes the auditable evidence the control operated.
Approval Step is the per-gate primitive — one named approver, one subject, one scope, one decision. It deliberately does not know about chains: it does not count approvals, does not interpret quorum, and does not wire multiple gates together. Multi-Party Approval is the composition that does. The atom’s single-gate scope is preserved unchanged; the composition adds the chain identity, the quorum rule, the cross-step state evaluation, and the cascade (secondary effects triggered automatically by a primary event) behavior when a chain is withdrawn or quorum becomes unachievable.
The composition addresses what the constituent atoms cannot answer alone. Approval Step records each gate but never asks “is the chain done?” Permissions records who may initiate a chain but never asks “did the quorum’s named approvers actually decide?” Assignment binds work to actors but never asks “is the work part of a multi-actor gate?” Audit Trail records actions of consequence but never asks “do these actions constitute a complete approval chain under the named rule?” Stacked correctly, the four answer the auditor’s actual question in one structure: a chain identity that names the required approvers and rule, N Approval Step records under that chain identity, an Assignment record per pending step, attestations and event-log entries for every chain-level and step-level action, and a deterministic chain-state evaluation that any reader can reproduce from the records.
This is a composition, not a new primitive. The four constituent atoms (and the Audit Trail substrate) are unchanged. The application is the wiring that makes their concerns coherent — one consolidated multi-party-approval surface rather than four parallel record stores the auditor has to correlate by hand.
Summary
Multi-Party Approval is a regulated composition (a spec that wires two or more atoms — freestanding, self-contained pattern specs — together) that enforces an N-approver gate over a single subject (the thing being approved) and scope (the type of decision), where the gate cannot be bypassed and the full approval chain (who decided what, when, under which quorum — the minimum number of approvals required for a decision — rule) is reconstructible from the records alone. It wires four constituents: Approval Step (the per-gate atom — freestanding pattern spec — that records one named approver’s decision on one subject), Permissions (which controls who may initiate, withdraw, and read chains), Assignment (which tracks pending approval tasks in each approver’s in-tray), and Audit Trail (the tamper-evident — designed so unauthorized changes are detectable — regulated-audit composition that attribution-stamps, retention-bounds, and seals every chain-level and step-level event).
The composition’s load-bearing addition is the quorum evaluation rule: after each step-level decision, the application re-evaluates whether the chain’s named quorum rule is now satisfied or permanently unachievable, and transitions the chain accordingly. Approval Step does not count approvals across multiple steps and does not know about chains. Permissions, Assignment, and Audit Trail each know nothing about quorum. The composition is the only layer that can ask “is the chain done?” — and its answer is deterministic: any reader, given the chain record and the step records, can independently recompute the expected chain terminal state from the step state vector and the quorum rule, and confirm it matches. This determinism is the composition’s chain completeness (every step in the chain has a recorded decision) guarantee, and it closes the bypass-resistance property: a chain cannot reach Approved without the quorum-named decisions actually present in the Approval Step store.
Beyond quorum evaluation, the composition guarantees that every chain initiation, step decision, and chain withdrawal is attribution-stamped and tamper-evident through the Audit Trail substrate; that every pending step is bound to a named approver’s in-tray via Assignment; and that once a chain reaches a terminal state, neither the chain’s outcome nor its declared fields (subject, scope, approver set, quorum rule) can change — corrections require a new chain. Trailing step decisions recorded after a chain has already terminated are preserved in the records with a trailing flag so an auditor can distinguish them from the decisions that determined the chain’s outcome.
This composition is grounded (passed all required review passes and is stable enough to generate from). Its most common uses are financial-controls approval under SOX (Sarbanes-Oxley Act) §404, pharmaceutical batch release under FDA 21 CFR Part 211, electronic signature chains under 21 CFR Part 11, clinical-trial approval under ICH E6 GCP, and change-control approval under SOC 2 and ISO 9001. Any system that must prove, from records alone, that a required set of named approvers actually decided before an action proceeded is a candidate for this composition.
Composes
-
Approval Step — provides the per-gate primitive: one Approval Step record per required approver, each carrying its own step_id,subject_ref,approver_ref,submitter_ref,scope, lifecycle (Pending → ApprovedRejected Withdrawn), and Invariant 4 enforcement that only the named approver_refmay transition to Approved or Rejected. The application maintains exactly one Approval Step store instance per chain store (one-to-one) and submits N steps under each chain. - Permissions — provides the authorization surface for chain-level actions:
grant,revoke,permitted. The application maintains exactly one Permissions instance scoped to the chain store. Every chain-level state-changing action (initiate_chain,withdraw_chain) and every chain-level read query is gated by apermittedcheck before reaching the chain store or its constituent stores. - Assignment — provides the in-tray binding: on chain initiation, one Assignment record per step (
task_ref = step_id,assignee_ref = step.approver_ref) is created so that approvers can query “what approval steps are sitting in my in-tray right now?” via Assignment’sactive_for-style queries. When a step reaches a terminal state (Approved, Rejected, or Withdrawn) the corresponding Assignment is recalled — its responsibility is discharged. - Audit Trail — the regulated-audit substrate. Every chain-level action (
initiate_chain,withdraw_chain) and every step-level decision (approve_step,reject_step,withdraw_step) is recorded as onerecord_actioncall on the Audit Trail instance, producing an Event Log entry, an Actor Identity attestation, a Retention Window record, and (per the cadence) a Tamper Evidence seal. The application maintains exactly one Audit Trail instance configured with the host’s regulatory retention policy (SOX 7-year, FDA Part 11 predicate-rule, ICH E6 trial-master-file).
The Event Log and Actor Identity atoms named in the roadmap entry for this composition are reached transitively through Audit Trail; the application does not maintain separate Event Log or Actor Identity instances of its own. Audit Trail is the regulated-audit composition that owns those constituents.
Composition logic
Application state
The application owns emergent state — the chain store and the cross-atom maps — that wires the constituent atoms into one queryable approval-chain surface:
-
chain_store— the set of chain records. Each record carrieschain_id,subject_ref,scope,initiator_ref,approver_set(the N actor references the chain is bound to),quorum_rule(one ofall-of-N,M-of-N(M),one-of-N),initiated_at,state(PendingApproved Rejected Withdrawn), and an optional audit_pendingflag (default absent; set true only by the partial-failure recovery path ininitiate_chainstep 7c, surfaced toread_chainas a top-level field on the chain record). Chain records are immutable (unchangeable once written) on every field exceptstateandaudit_pending;statetransitions are append-only (records advance forward only; no terminal state returns to Pending) in the sense that no terminal state returns to Pending;audit_pendingis set true at most once (during recovery) and cleared to absent when thechain_initiation_failedaudit record lands. Theaudit_pendingflag is not a state in the canonical chain state machine — a chain withaudit_pending = trueis still in Pending (or in a terminal state if it was subsequently withdrawn during recovery) — it is a recovery-status flag that read-only queries surface so the auditor sees the quarantine without reading the chain’sstateas anomalous. chain_to_steps— map fromchain_idto the ordered list ofstep_ids submitted under that chain. The list is set atinitiate_chainand is immutable; reassignment or replacement of steps is not supported (a step that needs to be redone requires the chain to be withdrawn and a new chain initiated). The list’s order is the order in whichapprover_setwas declared atinitiate_chain— equivalently, the submission order, since steps are submitted to Approval Step in declaration order. The order is diagnostic, not load-bearing: the canonical quorum rules (all-of-N, M-of-N, one-of-N) are order-independent in outcome, so reading the list back in any total order yields the same quorum evaluation. Composing patterns that do impose ordering (e.g., the forthcoming Sequenced Approval Chain) consume this list as an ordered sequence and add their own enforcement.step_to_chain— inverse map fromstep_idto itschain_id, for fast traversal when a step-level event fires.step_to_assignment— map fromstep_idto theassignment_idof the in-tray binding for that step. Lets the auditor traverse from a step to its responsibility record.chain_terminal_at— for each chain in a terminal state, the timestamp at which the chain first satisfied the quorum rule (Approved) or became unsatisfiable (Rejected), or the timestamp the initiator withdrew the chain (Withdrawn). Set once when the chain leaves Pending; immutable thereafter.
Configuration
approver_set_minimum— the smallest validapprover_setsize. Defaults to 1 (a single-gate chain is a degenerate but valid chain); deployments requiring genuine multi-party may configure ≥ 2. A chain initiated with|approver_set| < approver_set_minimumis rejected atinitiate_chainwithinvalid-request.approver_set_uniqueness— whether theapprover_setmust contain pairwise-distinctapprover_refvalues. Defaults totrue(the same actor cannot occupy two slots in the same chain); deployments where the same actor may legitimately appear twice (e.g., wearing two roles) may configurefalse. Even underfalse, the resulting chain submits one Approval Step per slot, each with its ownstep_id, and quorum is counted per step — an actor named in two slots must callapprove_steptwice (once perstep_id) to contribute two units toA. One decision does not auto-credit both slots; that would be the chain composition silently collapsing two gates into one and is a hidden mechanism the spec refuses. The auditor counts step records, not actors.quorum_rule_allowed— the set of quorum rules the deployment permits. Defaults to{all-of-N, M-of-N, one-of-N}. A deployment may restrict to a subset; chains initiated with a quorum rule outside the allowed set are rejected atinitiate_chainwithinvalid-request.-
audit_trail_retention_policy— the policy reference passed to the Audit Trail instance at eachrecord_actioncall. Typically a regulatory policy id (sox_7_year,fda_part_11_predicate_rule,ich_e6_tmf). The choice is deployment policy; the composition surfaces the configuration knob. application_actor_refandapplication_credential— the deployment-provisioned actor reference and credential the composition uses when emitting application-internal Audit Trail entries that have no human-actor origin: thechain_resolvedevent produced by the quorum evaluation rule firing, and the cascade-recall event when trailing assignments are recalled at chain termination. The application actor is a first-class registered actor in the Audit Trail’s underlying Actor Identity registry — not a special-cased nil — and its attestations verify under the same rules as human-actor attestations. The deployment is responsible for issuing, rotating, and retiring this credential under the same Permissions and Compromise Disclosure discipline applied to any privileged service identity. (Compromise Disclosure is a forthcoming composing pattern in the library, not a constituent of this composition; deployments operating before Compromise Disclosure lands satisfy the rotation/retirement discipline via Permissions revocation of the application actor’schain_resolvedemit grant, plus operational runbook.) An auditor querying the Audit Trail seeschain_resolvedevents attributed to the application actor and can verify them against the registry’s public material exactly as for human-actor events. The records-alone forgery defense is Invariant 2 (quorum determinism): a forgedchain_resolvedevent whose declaredstatedoes not match the deterministic outcome of the quorum rule on the chain’s step state vector is detectable by any reader independently recomputing the rule from the chain and step records. An attacker who compromisesapplication_credentialand emits a fakechain_resolved(state=Approved)against a chain whose step records showA < N(under all-of-N) orA < M(under M-of-N) produces a record that fails Invariant 2 verification — the forgery is structurally detectable without recourse to runbooks or credential-chain investigation. Forging the underlying step records themselves is foreclosed by Approval Step’s Invariant 4 (only the namedapprover_refmay decide;application_actor_refdoes not satisfy this), so the attack surface for the application credential is limited to chain-level events whose state claims must follow from the step records the application credential cannot write. The distinguishing rule between human-emitted and application-emitted audit events:chain_resolvedis the sole event emitted withactor_ref = application_actor_ref;chain_initiated,chain_withdrawn,step_approved,step_rejected, andstep_withdrawnare emitted with the human actor’s reference. An auditor walks the rule directly from theactor_refandaction_reffields of each Audit Trail entry.
Scope vocabulary
Permissions treats action scopes as opaque. Multi-Party Approval defines the canonical scope vocabulary for its Permissions instance:
| Scope | Permits |
|---|---|
chains:initiate | Call initiate_chain to create a new approval chain |
chains:withdraw | Call withdraw_chain to withdraw a chain (the chain initiator’s act) |
chains:read | Read chain records and their composed step/assignment/attestation surface |
Step-level decisions (approve_step, reject_step, withdraw_step) are not additionally permission-gated at the chain layer: Approval Step’s Invariant 4 (only the named approver_ref may transition Pending to Approved or Rejected) and Invariant 5 (only the named submitter_ref may transition Pending to Withdrawn) are the structural enforcement for who may decide each step. Adding a second permission check at the chain layer would be redundant and risks the two checks drifting out of sync. The chain composition relies on Approval Step’s enforcement and surfaces an unauthorized rejection from the underlying atom unchanged.
The vocabulary is deployment-configurable. A deployment that distinguishes “read your own chains” from “read any chain” introduces finer-grained scopes (chains:read:own, chains:read:any) and adjusts the wiring accordingly; the canonical vocabulary above is the minimum useful set.
Action wiring
Every chain-level action follows the same three-step shape: Permissions check first, audit-trail record second, constituent atom call third. Step-level actions follow a two-step shape: constituent atom call first (Approval Step enforces approver/submitter exclusivity), audit-trail record second, then chain-state re-evaluation.
initiate_chain(actor_ref, subject_ref, scope, approver_set, quorum_rule, reason?) → chain_id | rejected(permission-denied | invalid-request | recording-failure)Permissions.permitted(actor_ref, chains:initiate)→ ifdenied, returnpermission-denied.- Validate the chain shape:
|approver_set| ≥ approver_set_minimum; ifapprover_set_uniquenessis true, all elements ofapprover_setare pairwise distinct;quorum_ruleis inquorum_rule_allowed; forM-of-N(M),1 ≤ M ≤ |approver_set|;subject_refandscopenon-whitespace. Any violation isinvalid-request; no records are written. - Allocate a fresh
chain_id. Record the chain inchain_storewithstate = Pending,initiated_at = now, the supplied fields. - For each
approver_refinapprover_set, in declaration order: callApprovalStep.submit(subject_ref, approver_ref, submitter_ref=actor_ref, scope, reason?)→step_id. Appendstep_idtochain_to_steps[chain_id]; recordstep_to_chain[step_id] = chain_id. - For each
step_idjust submitted: callAssignment.assign(task_ref=step_id, assignee_ref=step.approver_ref)→assignment_id. Recordstep_to_assignment[step_id] = assignment_id. The in-tray binding now lets each approver query their pending decisions. - Call
AuditTrail.record_action(action_ref=chain_initiated, actor_ref, credential, data={chain_id, subject_ref, scope, approver_set, quorum_rule}, retention_policy=audit_trail_retention_policy). The Audit Trail produces the event-log entry, the actor attestation, the retention record, and (under per-event cadence) the tamper-evidence seal. - If any of steps 3–6 fail after Permissions has permitted, return
rejected(recording-failure). Three specific partial-state cases the implementation must handle, in order of severity: (a) a failure at step 4 (Approval Step submission for the k-th approver, with k−1 steps already submitted) leaves k−1 immutable Approval Step records that name achain_idwhosechain_to_steps[chain_id]is incomplete — the recovery path is to chain-withdraw the partial chain (cascading withdrawal through the k−1 submitted steps and the chain record itself) and surfacerecording-failureto the caller. (b) a failure at step 5 (Assignment creation) leaves N submitted steps and j < N Assignments — the recovery path is the same (chain-withdraw), additionally requiring no-op handling for the missing Assignments (their recall calls during cascade-withdrawal returnnot-known, which the composition treats as no-op success since the assignment was never created). (c) a failure at step 6 (AuditTrail.record_actionforchain_initiated) leaves the chain, its steps, and its Assignments all in the constituent stores but no Audit Trail entry — this is the most serious case because Invariant 5 (audit completeness) is violated. The recovery path is to chain-withdraw the partial chain and to record the failure itself as an Audit Trail event usingaction_ref=chain_initiation_failedonce Audit Trail recovers; until then, the chain is in a quarantined state (the chain record carries anaudit_pendingflag, not part of the canonical chain state machine, that read-only queries surface to the auditor). Deployments operating under regulators that do not tolerate any audit-pending state must serialize step 6 ahead of steps 3–5 (write-ahead audit logging) — a deployment-policy variant the canonical composition does not enforce but accommodates. - Return
chain_id.
approve_step(actor_ref, chain_id, step_id, reason?) → approved | rejected(invalid-request | not-known | not-pending | unauthorized | recording-failure)- Validate that
step_to_chain[step_id] == chain_id. Ifstep_idis not part of the chain, returnnot-known. The chain’sstateis consulted but does not gate the call: a chain already in a terminal state still accepts decisions on its trailing-Pending steps (the late-decision case named in Invariant 7 and in the Late decisions on a terminated chain edge case). The step’s own state — checked by Approval Step at step 2 — is the load-bearing gate: if the step is already in a terminal state (Approved, Rejected, or Withdrawn — e.g., it was cascade-withdrawn under a prior chain-Withdrawn-by-cascade transition), Approval Step’snot-pendingpropagates and the call is rejected. Whether the chain is terminal at the time of call is recorded in step 4’s audit data field as thetrailingflag (see step 4 below). - Call
ApprovalStep.approve(step_id, decided_by=actor_ref, reason?)→ propagatesinvalid-request | not-known | not-pending | unauthorized | storage-failureunchanged (storage-failure is surfaced asrecording-failureat the application boundary for caller-API uniformity). The atom’s Invariant 4 ensuresactor_refmust match the step’sapprover_ref. - On atom-level success, call
Assignment.recall(step_to_assignment[step_id])→ discharges the in-tray binding for the approver. If the assignment is already in Recalled state (trailing-decision case — the assignment was discharged at chain termination by the cascade-recall subsection),Assignment.recallreturnsnot-active; the composition treats this as idempotent (submitting the same operation twice produces the same result as once) success and proceeds. The recall is required regardless of the resulting chain state — once decided, the step is no longer in-tray work. - Call
AuditTrail.record_action(action_ref=step_approved, actor_ref, credential, data={chain_id, step_id, reason, trailing}), wheretrailing = trueif the chain was already in a terminal state at step 1 (the late-decision case), andtrailing = falseotherwise. Thetrailingflag is the audit-distinguishing signal that lets an auditor walking the Audit Trail tell a late decision from an on-chain decision from the records alone — without it, astep_approvedevent whose timestamp is after the chain’schain_resolvedevent reads as a contradiction; with it, the audit interpretation is unambiguous. If thisrecord_actioncall fails after the atom-level step transition succeeded, see Cross-store consistency under failure in Edge cases for the recovery path; the application surfacesrecording-failureto the caller. - If
trailing = falseat step 1 — re-evaluate the chain state per the Quorum evaluation rule. If the chain transitions to any terminal state (Approved, Rejected, or Withdrawn-by-cascade), setchain_store[chain_id].stateaccordingly, setchain_terminal_at = now, cascade-recall any still-Active Assignments for the chain’s steps per the Cascade-recall of trailing assignments subsection, and callAuditTrail.record_action(action_ref=chain_resolved, actor_ref=application_actor_ref, credential=application_credential, data={chain_id, state, reason, recalled_step_ids}). Therecalled_step_idsfield is present and lists everystep_idwhose Assignment was recalled by the cascade — empty list if none (i.e., all steps were already in a terminal state at the moment of chain transition), one or more entries otherwise. Under chain-Withdrawn-by-cascade, additionally callApprovalStep.withdraw(step_id, withdrawn_by=initiator_ref, reason)for each still-Pending step (mirroring thewithdraw_chaincascade), each followed by its ownstep_withdrawnAudit Trail record withtrailing = false. The application actor is the deployment-provisioned actor named in Configuration. If thechain_resolvedrecord_actionitself fails afterchain_storehas been transitioned, see Cross-store consistency under failure in Edge cases. Iftrailing = trueat step 1 — chain re-evaluation is a no-op: the chain is already terminal (terminal-stable per the Quorum evaluation rule); nochain_resolvedevent is emitted;chain_storeis unchanged. The step-level audit entry from step 4 is the sole record of the trailing decision, with itstrailingflag making the audit interpretation explicit. - Return
approved.
- Validate that
reject_step(actor_ref, chain_id, step_id, reason) → rejected_outcome | rejected(invalid-request | not-known | not-pending | unauthorized | recording-failure)- As
approve_stepstep 1 — the chain’s terminal state does not gate the call; the step’s state (checked by Approval Step at step 2) is the load-bearing gate. Thetrailingflag is computed for step 4. - Call
ApprovalStep.reject(step_id, decided_by=actor_ref, reason)→ propagates the atom’s rejection taxonomy;reasonis required (Approval Step’s Invariant 6 enforces it). - As
approve_stepstep 3 (recall the in-tray assignment;not-activeis idempotent success). - As
approve_stepstep 4, withaction_ref=step_rejectedand the sametrailingflag rule. - As
approve_stepstep 5 (chain re-evaluation only whentrailing = false). Under all-of-N, a rejection makes quorum unreachable and the chain transitions to Rejected; under M-of-N or one-of-N, it depends on remaining capacity. - Return
rejected_outcome(Approval Step’s success token forreject).
- As
withdraw_step(actor_ref, chain_id, step_id, reason) → withdrawn | rejected(invalid-request | not-known | not-pending | unauthorized | recording-failure)- As
approve_stepstep 1 — the chain’s terminal state does not gate the call. Thetrailingflag is computed for step 4. Note: under chain-Withdrawn-by-cascade, trailing steps were cascade-withdrawn already and will fail step 2 withnot-pending; trailing-decision withdrawals therefore only occur when the chain is in Approved or Rejected (where trailing steps remain Pending). - Call
ApprovalStep.withdraw(step_id, withdrawn_by=actor_ref, reason)→ propagates the atom’s rejection taxonomy;withdrawn_bymust matchsubmitter_ref(the chain initiator). Step-level withdrawal is the chain initiator correcting a single mis-submitted gate (wrong approver named, wrong scope) without retracting the whole chain. - As
approve_stepstep 3 (recall the in-tray assignment). - As
approve_stepstep 4, withaction_ref=step_withdrawnand the sametrailingflag rule. - As
approve_stepstep 5 (chain re-evaluation only whentrailing = false). A withdrawn step is counted alongside rejected steps for quorum-unreachability purposes (see Quorum evaluation rule). - Return
withdrawn.
- As
withdraw_chain(actor_ref, chain_id, reason) → withdrawn | rejected(permission-denied | not-known | not-pending | unauthorized | recording-failure)Permissions.permitted(actor_ref, chains:withdraw)→ ifdenied, returnpermission-denied.- Validate that
chain_idis inchain_store(not-knownotherwise) and in Pending state (not-pendingotherwise). - Validate that
actor_ref == chain_store[chain_id].initiator_ref— only the chain initiator may withdraw the chain. Mismatch isunauthorized. - For each
step_idinchain_to_steps[chain_id]whose underlying Approval Step is still Pending: callApprovalStep.withdraw(step_id, withdrawn_by=initiator_ref, reason)→ discards each Pending gate. ThenAssignment.recall(step_to_assignment[step_id])discharges each in-tray binding. Steps already in a terminal state are not affected by chain-level withdrawal — their records are immutable. Partial-failure recovery during this cascade follows the same rule as cascade-recall on quorum-fire termination (see Cascade-recall of trailing assignments on chain termination): the application proceeds through the remainder of the cascade on individual call failure, records the chain transition with acascade_partial = trueflag reflecting the actual progress, and the implementation retries the failed calls with follow-upcascade_completedAudit Trail entries. - Set
chain_store[chain_id].state = Withdrawn,chain_terminal_at = now. - Call
AuditTrail.record_action(action_ref=chain_withdrawn, actor_ref, credential, data={chain_id, reason}). - Return
withdrawn.
read_chain(actor_ref, query) → ordered_sequence_of_chains | rejected(permission-denied | invalid-query)Permissions.permitted(actor_ref, chains:read)→ ifdenied, returnpermission-denied(or an empty result, per deployment policy).- Query the chain store on the supported filter axes:
chain_id,subject_ref,scope,initiator_ref,state, time ranges oninitiated_atorchain_terminal_at(using the{after: <timestamp>, before: <timestamp>}sub-key form consistent with Approval Step’sread). An unrecognized filter key isinvalid-query. Empty result for a well-formed query that matches no chains; not a rejection. - For each chain in the result set, the response carries the chain record’s fields (including
audit_pendingwhen set — the auditor sees the quarantine flag without needing a separate query), plus the derived view: the ordered list of step records (viachain_to_stepsjoined against the Approval Step store), the current assignment for each Pending step (viastep_to_assignmentjoined against the Assignment store), and the chain’s terminal status. The application does not surface the underlying Audit Trail records on this query; auditors querying the full audit history callAuditTrail.verify_recorddirectly with the relevantevent_ids.
The quorum evaluation rule
The application’s load-bearing wiring decision: chain state is a deterministic function of the constituent step states under the chain’s named quorum rule, re-evaluated at each step-level transition. The application is the only layer that owns this evaluation; neither Approval Step (which knows nothing about chains) nor Permissions/Assignment/Audit Trail (which know nothing about quorum) can produce the chain’s terminal state alone.
For a chain with approver_set of size N, let:
A= the count of step_ids inchain_to_steps[chain_id]whose underlying Approval Step state is Approved.R= the count of step_ids whose state is Rejected.W= the count of step_ids whose state is Withdrawn.P= the count of step_ids whose state is Pending.A + R + W + P = N.
Withdrawn and Rejected step states are both counted toward quorum unreachability, but they carry distinct audit signals — a rejection is a deliberate negative decision by the named approver; a withdrawal is the chain initiator’s retraction of a single mis-submitted gate (wrong approver named, wrong scope) without negative decision content. The chain’s terminal state distinguishes the two:
Under quorum rule all-of-N:
- If
A == N, the chain transitions to Approved. - If
R ≥ 1, the chain transitions to Rejected with reason"quorum unreachable: all-of-N requires every approval; step S was rejected". Rejection takes priority over withdrawal: if a step is both rejected and (after a later step withdrawal) “withdrawal-blocked,” the rejection is the load-bearing audit signal. - If
R == 0 ∧ W ≥ 1, the chain transitions to Withdrawn with reason"chain withdrawn by cascade: step S was withdrawn by the initiator; all-of-N quorum is no longer reachable without a fresh chain". The remaining Pending steps are cascade-withdrawn through Approval Step (mirroring thewithdraw_chainaction’s cascade), each Assignment recalled (see Cascade-recall of trailing assignments below), and an Audit Trail event emitted by the application actor. - Otherwise the chain remains Pending.
Under quorum rule M-of-N(M) (which includes one-of-N as M=1):
- If
A ≥ M, the chain transitions to Approved. - If
(N - R - W) < M ∧ R ≥ 1— i.e., fewer than M steps remain that are either already Approved or still capable of becoming Approved, and at least one of the unreachable-quorum-causing transitions was a rejection — the chain transitions to Rejected with reason"quorum unreachable: M-of-N requires M approvals; only K remain achievable; rejections present". - If
(N - R - W) < M ∧ R == 0 ∧ W ≥ 1— quorum unreachable solely because of withdrawals, no rejections — the chain transitions to Withdrawn with the cascade behavior named above. - Otherwise the chain remains Pending.
Cascade-recall of trailing assignments on chain termination
When the quorum evaluation rule fires a chain transition to any terminal state (Approved, Rejected, or Withdrawn-by-cascade) — including via the cascade above — the application recalls every Assignment whose underlying step is still in Pending, then records the chain transition to the Audit Trail. The trailing-step treatment differs by terminal state, by design:
- Under chain-Approved or chain-Rejected — trailing-Pending steps remain in Approval Step’s Pending state (the per-step Invariant 3 — terminal absorption — applies only to steps, not to the chain), but their in-tray binding is discharged. The work is moot from the chain’s perspective; the named approver’s responsibility ends with chain termination, not with their own decision. A late decision by the named approver is permitted (Invariant 7) and is recorded with
trailing = truein the Audit Trail entry. - Under chain-Withdrawn-by-cascade — trailing-Pending steps are also step-Withdrawn through Approval Step (per the Quorum evaluation rule’s cascade for the
R = 0 ∧ W ≥ 1case), mirroringwithdraw_chain. Once cascade-withdrawn, the step is in a terminal state, and a subsequentapprove_steporreject_stepcall on it fails at the atom level withnot-pending. Late decisions are therefore only possible against chain-Approved or chain-Rejected chains, not against chain-Withdrawn chains — the asymmetry is intentional and audit-defensible: chain-Withdrawn names a deliberate retraction of the chain as a whole, so trailing gates are released; chain-Approved and chain-Rejected name quorum outcomes that do not foreclose the named approver’s right to record a decision for the audit trail.
The cascade-recall is implemented as one Assignment.recall(assignment_id) call per still-Active assignment in chain_to_steps[chain_id], in declaration order. Under chain-Withdrawn-by-cascade, each recall is preceded by one ApprovalStep.withdraw(step_id, withdrawn_by=initiator_ref, reason) call against the still-Pending step. After all recalls (and step-withdraws, if applicable) complete, one AuditTrail.record_action(action_ref=chain_resolved, actor_ref=application_actor_ref, credential=application_credential, data={chain_id, new_state, reason, recalled_step_ids}) call is emitted. The recalled_step_ids field is always present (not optional): it carries the list of step_ids whose Assignments were recalled by this cascade, in the order recalls were performed; the list is empty ([]) when the chain transitioned with no trailing-Active assignments (i.e., all N steps reached a terminal state in lockstep with the triggering decision — the canonical all-of-N happy path on the N-th approval). Always-present rather than omitted-when-empty preserves a single audit-data schema across all chain_resolved events.
Partial-failure recovery during cascade. If one of the cascade calls fails — an Assignment.recall returns storage-failure, or under chain-Withdrawn-by-cascade an ApprovalStep.withdraw returns storage-failure — the application has executed some of the cascade calls successfully and some not. The recovery rule: the application proceeds through the remainder of the cascade (does not abort on first failure), then records the chain transition to the Audit Trail with the partial outcome reflected: the recalled_step_ids lists only the assignments actually recalled, and a cascade_partial = true flag is included in the chain_resolved data when one or more cascade calls failed. The chain’s terminal state is still set in chain_store (the chain has structurally terminated; the cascade is housekeeping), and the still-Active trailing Assignments (or, under Withdrawn-by-cascade, still-Pending trailing steps) are surfaced via read_chain so the auditor and operator see the partial state. The implementation is responsible for retrying the failed cascade calls until they succeed; each subsequent retry call that succeeds emits its own follow-up Audit Trail entry (action_ref=cascade_completed) referencing the original chain_resolved event by chain_id. The application returns approved (or the analogous success token) to the caller even when the cascade was partial — the load-bearing transition (step decision and chain re-evaluation) succeeded; the cascade is recovery-amenable.
If a trailing step’s named approver later decides on the step (a late decision per Invariant 7 and the corresponding Edge case), the step-level approve_step/reject_step call still recalls “the” assignment; but the assignment is already in Recalled state (or never created in a partial-cascade case where the assignment recall hadn’t completed before the late decision arrived — in which case Assignment.recall returns ok on first call here). The composition treats not-active from Assignment.recall as idempotent success — the assignment was lawfully discharged at chain termination — and proceeds. The decision action’s own Audit Trail entry (with trailing = true) is the record of the trailing decision; the chain’s state and chain_terminal_at are unchanged.
The rule is terminal-stable: once a chain reaches a terminal state via this rule, subsequent step transitions (e.g., a still-Pending step is later approved by its named approver after the chain has already become Approved) do not alter the chain’s state. The chain’s terminal status is set once and is immutable. The trailing step transitions are still recorded in the Approval Step store and the Audit Trail (with trailing = true), but the chain’s state is unchanged. Implementations must serialize the chain-state re-evaluation on a given chain_id (per Concurrent step decisions on the same chain in Edge cases); a bug or race that re-fires the rule against an already-terminal chain MUST be a no-op at the chain-state level — re-evaluation reads chain_store[chain_id].state first and returns immediately if the chain is not in Pending. An implementation that emits a second chain_resolved event with a different state from the first is a Invariant 7 conformance failure observable from the records (two chain_resolved events for one chain_id, with different declared states); the audit defense against this race is structural — the duplicate is detectable.
The rule is also order-independent in outcome but order-sensitive in timing: for any final distribution of step states (A, R, W, P=0), the chain’s terminal state is determined by the rule above regardless of the order in which step transitions occurred. The chain_terminal_at timestamp captures the first moment the rule fired against this chain; later transitions to terminal states on still-Pending steps do not advance chain_terminal_at. Two concurrent triggering decisions whose serialization choice would yield different first-firing moments are serialized per the chain-scope mutex; the spec does not promise which decision serializes first, only that the chain-state re-evaluation is single-threaded per chain_id and that whichever serializes first wins the chain_terminal_at slot. For deployments where the precise first-firing moment must be adversarially defensible (against a claim that the recorded chain_terminal_at was delayed or batched), the Audit Trail’s per-event receipt timestamps on the triggering step event and the resulting chain_resolved event are the audit pair an auditor compares.
Composition-level invariants
These invariants (conditions that must always hold) emerge from the composition. None belongs to a single constituent atom; each requires two or more constituents working together to hold.
-
Invariant 1 — Chain completeness. For every
chain_idinchain_store,chain_to_steps[chain_id]is a non-empty ordered list ofstep_ids, each of which exists as a record in the Approval Step store. Every step is bound to the chain bystep_to_chain[step_id] = chain_id. No chain has fewer thanapprover_set_minimumsteps; no step is orphaned from its chain. -
Invariant 2 — Quorum determinism. For any chain in a terminal state, the chain’s
stateis the deterministic outcome of applying the chain’squorum_ruleto the final state vector(A, R, W, P)of its constituent steps, as named in the Quorum evaluation rule. Any reader of the records can independently compute the expected terminal state from the step records and the quorum rule and confirm it matcheschain_store[chain_id].state. A mismatch is a conformance failure. The invariant covers both terminal-path origins uniformly: a chain that reached terminal via the quorum rule firing (a step decision triggered the rule) and a chain that reached terminal viawithdraw_chain(the initiator retracted) yield the same Invariant 2 verification — under chain-Withdrawn-by-initiator, the cascade inwithdraw_chainstep 4 transitions all still-Pending steps to Withdrawn, so the final state vector hasW = N − (A + R)(withP = 0); applying the quorum rule to that vector yields Withdrawn under bothall-of-N(R = 0 ∧ W ≥ 1) andM-of-N((N − R − W) < M ∧ R = 0 ∧ W ≥ 1), matchingchain_store[chain_id].state. The two paths to chain-Withdrawn are distinguishable from the records, but not by the chain’sstatealone: the Audit Trail’saction_reffor the chain-level event distinguishes them —chain_withdrawnis the initiator-driven path;chain_resolvedwithdata.state = Withdrawnis the quorum-rule-driven cascade path. An auditor needing the distinction reads the audit event; an auditor needing only the deterministic-state check reads the chain and step records. -
Invariant 3 — Permission enforcement. No actor performs
initiate_chain,withdraw_chain, orread_chainwithout apermittedresult from the Permissions instance for the corresponding scope. Adeniedresult short-circuits the action before any constituent atom is invoked. Step-level decision actions are enforced by Approval Step’s Invariant 4 (named-approver exclusivity) and Invariant 5 (submitter-only withdraw) directly; the chain composition adds no separate permission check for step-level decisions. -
Invariant 4 — Assignment coverage during pendency, with cascade-on-terminal. For every step in Pending state in a chain that is itself in Pending state, exactly one Active Assignment exists with
task_ref = step_idandassignee_ref = step.approver_ref. Assignments are recalled in two cases: (a) when the step transitions to a terminal state viaapprove_step,reject_step,withdraw_step, or cascade fromwithdraw_chain, the Assignment is recalled in the same application-level transition; (b) when the chain transitions to a terminal state via the quorum evaluation rule firing — and one or more constituent steps remain in Pending (trailing-decision case under M-of-N) — every still-Active Assignment for the chain’s steps is recalled by cascade in the same application-level transition. After either case, no step has a lingering Active Assignment when either the step or its chain is in a terminal state. Late decisions on trailing-Pending steps recall the (already-Recalled) Assignment idempotently: the secondAssignment.recallcall returnsnot-active, which the composition treats as no-op success — the assignment was lawfully discharged at chain termination. -
Invariant 5 — Audit completeness. Every chain-level action (
initiate_chain,withdraw_chain) and every step-level action (approve_step,reject_step,withdraw_step) produces exactly oneAuditTrail.record_actioncall. Additionally, every chain-state transition emitted by the Quorum evaluation rule (achain_resolvedevent) produces its ownrecord_action, and trailing-decision step events produced after their chain has terminated are recorded withtrailing = trueto distinguish them from on-chain decisions. The complete chain lifecycle is reconstructible from the Audit Trail records (forward) and the chain plus step records (reverse); no chain-level event is recorded only in the chain store without a corresponding Audit Trail entry, and no chain-related Audit Trail entry refers to a chain or step that does not exist. The invariant inherits Audit Trail’s atomicity surface.record_actionis treated as atomic from this composition’s perspective, but Audit Trail’s Partial attestation on step failure edge case (Actor Identity.attest succeeds but EventLog.append fails) can produce an orphan attestation without a corresponding event-log entry — a violation of “one Event Log entry, one attestation, one retention record per call” propagated from the substrate. The composition does not re-derive the substrate’s recovery; it inherits Audit Trail’s orphan-resolution discipline (flagging, compensating record, manual investigation) and treats any unresolved orphan as a gap in the chain’s audit surface. The composition’s role is to surface the gap (the implementation must alert on orphan attestations referencing this composition’saction_refvalues) rather than to police it; Invariant 5 holds modulo Audit Trail’s own atomicity contract. -
Invariant 6 — Constituent invariants preserved. All invariants of every constituent atom hold over its respective instance. Approval Step’s invariants (submission immutability, membership exclusivity, terminal absorption, approver exclusivity, submitter exclusivity, decision attribution completeness, temporal ordering, submission attribution completeness, concurrent step independence, step store durability) hold per step. Permissions’ invariants hold over the chain-store-scoped Permissions instance. Assignment’s invariants hold over the application’s Assignment instance. Audit Trail’s application-level invariants hold over its instance.
-
Invariant 7 — Chain terminal absorption. Once a chain is in Approved, Rejected, or Withdrawn, the chain’s
statedoes not transition further regardless of subsequent step-level events. A Pending step that is later decided after its chain has terminated produces a step-level record and an Audit Trail entry, but does not alterchain_store[chain_id].stateorchain_terminal_at. This is the chain-level counterpart to Approval Step’s Invariant 3 (terminal absorption on steps). -
Invariant 8 — Chain immutability of declared fields. For every chain in
chain_store, the fieldschain_id,subject_ref,scope,initiator_ref,approver_set,quorum_rule, andinitiated_atare immutable from the momentinitiate_chainreturns. Onlystateandchain_terminal_atmay transition (and each transitions at most once:statefrom Pending to one terminal value;chain_terminal_atfrom unset to one timestamp). A chain cannot be re-targeted to a different subject, scope, approver set, or quorum rule; corrections require chain-level withdrawal and a newinitiate_chaincall. -
Invariant 9 — Forensic completability (an investigator can reconstruct the full history from records alone). For any
chain_id, the application’s query surface returns: the chain record (subject, scope, initiator, approver set, quorum rule, state, timestamps); the ordered list of step records with each step’s decision, decider, decision timestamp, and reason; the Assignment records for each step (Active or Recalled); and (via Audit Trail) the verified attestation for every chain-level and step-level action. An external auditor can reconstruct the full chain lifecycle from the records alone without recourse to source code, runbooks, or developer narration.
Quorum determinism and chain completeness together give the bypass-resistance property — a chain cannot reach Approved without the quorum-named decisions actually present in the Approval Step store, and a reader can verify this from the records. Permission enforcement and audit completeness together give the non-repudiability property — every chain-level action is attribution-stamped, retention-bounded, and tamper-evident through the Audit Trail substrate. Chain terminal absorption and chain immutability together give the finality property — once a chain terminates, its outcome is fixed; later corrections require new chains, not retroactive edits to old ones.
Examples
Walkthrough — SOX-controlled journal entry, all-of-N quorum
A multinational bank’s general-ledger system uses Multi-Party Approval to gate posting of journal entries above the $5M materiality threshold. The deployment configures approver_set_minimum = 2, approver_set_uniqueness = true, quorum_rule_allowed = {all-of-N}, audit_trail_retention_policy = sox_7_year.
- A controller prepares a journal entry. Journal entry JE-2026-0441 posts a $12M intercompany transfer. Under the deployment’s business rules, materiality of this size requires the regional controller, the CFO, and the CEO. The controller calls
initiate_chain(actor_ref=controller_morgan, subject_ref="je-2026-0441", scope="financial:journal-entry:post:materiality-tier-3", approver_set=[finance_director_chen, cfo_park, ceo_walsh], quorum_rule="all-of-N", reason="$12M intercompany transfer per Q1 close"). - The application validates and writes. Permissions returns
permitted(controller_morganholdschains:initiate). Validation passes (three pairwise-distinct approvers, all-of-N is allowed). The application allocateschain-2026-0441, writes the chain record in Pending, submits three Approval Steps (step-001,step-002,step-003), creates three Assignments (one per approver’s in-tray), and records the chain-initiated event in the Audit Trail. Returnschain_id = chain-2026-0441. - The CFO approves first.
approve_step(actor_ref=cfo_park, chain_id=chain-2026-0441, step_id=step-002, reason="Reviewed Q1 close package; transfer is in-policy")→approved. Step 002 transitions to Approved; Assignment is recalled; the audit trail records the approval. Quorum evaluation:A=1, R=0, W=0, P=2; all-of-N requiresA == N(=3); not satisfied; no quorum failure (no rejections or withdrawals); chain stays Pending. - The controller’s regional finance director approves second.
approve_step(actor_ref=finance_director_chen, step-001, reason="Mapping verified; consolidation rules applied correctly")→approved.A=2, R=0, W=0, P=1; still not at quorum. - The CEO approves third.
approve_step(actor_ref=ceo_walsh, step-003, reason="Reviewed and authorized")→approved.A=3, R=0, W=0, P=0;A == N; chain transitions to Approved;chain_terminal_atis set; the audit trail records thechain_resolvedevent. The composing workflow system releases JE-2026-0441 for posting. - Three years later, a SOX §404 audit. The auditor queries
read_chain({subject_ref: "je-2026-0441"}). The result is one chain in Approved with three step records, three attestations, three retention records under the seven-year policy, and a Tamper Evidence seal covering the relevant range.verify_recordon each Audit Trail event returnsverified. The auditor confirms (a) Invariant 4 was enforced on each step (decided_bymatchedapprover_ref), (b) Invariant 2 holds (chain state matches the deterministic quorum evaluation), and (c) no chain-state edit occurred afterchain_terminal_at. Control evidence is complete from the records alone.
Happy path — FDA Part 11 batch release, M-of-N(2) quorum across three qualified persons
A pharmaceutical manufacturer’s batch release system requires any two of three Qualified Persons (QPs) to approve a batch release under 21 CFR Part 211. The deployment uses quorum_rule = M-of-N(M=2). A batch BR-2026-0412 is ready for release. The QA manager initiates: initiate_chain(actor_ref=qa_manager, subject_ref="br-2026-0412", scope="pharma:batch-release:bulk", approver_set=[qp_santos, qp_lopez, qp_kim], quorum_rule="M-of-N(2)") → chain-2026-0412. QP Santos approves first (A=1); QP Lopez approves second (A=2); A ≥ M; the chain transitions to Approved without needing QP Kim’s decision. QP Kim’s step remains Pending forever — the chain has terminated, but the step’s records are immutable (Invariant 7) and the trailing decision, if Kim later approves, is recorded in the audit trail without altering the chain state. The released batch carries the chain id as its control evidence; an FDA inspector queries the chain and confirms the two named QPs decided affirmatively under their respective Actor Identity attestations.
Happy path — ICH E6 GCP protocol deviation, one-of-N quorum across a delegated approver pool
A clinical trial protocol deviation at a multi-site study can be approved by any one of the site principal investigators on call. The trial coordinator initiates: initiate_chain(actor_ref=coordinator_lee, subject_ref="dev-2026-1057", scope="clinical-trial:protocol-deviation:non-substantive", approver_set=[pi_chen, pi_okafor, pi_müller, pi_singh], quorum_rule="one-of-N"). PI Okafor approves: A=1, M=1; chain Approved. The other three steps remain Pending in the audit trail. If a fifth approval is needed later (e.g., for a substantive deviation that requires escalation), a new chain is initiated with the appropriate quorum and approver set; the original chain is not modified.
Rejection path — all-of-N quorum, one approver rejects
In the SOX walkthrough above, suppose the CEO finds the entry suspicious and rejects: reject_step(actor_ref=ceo_walsh, step_id=step-003, reason="Counterparty not on approved-affiliates list; refer back to finance team for review") → rejected_outcome. Quorum evaluation: R=1, R + W ≥ 1; under all-of-N the chain transitions to Rejected with reason "quorum unreachable: all-of-N requires every approval; step step-003 is in non-Approved terminal state". chain_terminal_at is set; the audit trail records the chain resolution. JE-2026-0441 is not released for posting; the composing workflow routes the entry back to the controller, who must initiate a new chain for the corrected entry (a fresh chain_id, fresh step ids — no editing of the rejected chain’s records).
Rejection path — chain withdrawal by initiator
The controller submitting JE-2026-0441 discovers a clerical error in the entry before any approver has decided: the chain was opened against the wrong subject. The controller calls withdraw_chain(actor_ref=controller_morgan, chain_id=chain-2026-0441, reason="Wrong journal entry id; superseded by new chain on JE-2026-0441-revised"). The chain transitions to Withdrawn; each of the three still-Pending steps cascades to Approval Step’s Withdrawn state; each Assignment is recalled; the audit trail records the chain withdrawal and the three step withdrawals. A new chain is initiated on the corrected entry.
Regulated adversarial scenarios
Three adversarial reads the composition must survive in regulated contexts:
-
Regulator audit — SOX §404 control evidence query, “show me every chain that approved a material journal entry in Q1 with full attribution.” The auditor queries
read_chain({scope: "financial:journal-entry:post:materiality-tier-3", state: Approved, initiated_at: {after: "2026-01-01T00:00:00Z", before: "2026-03-31T23:59:59Z"}}). Every chain in the result set carries its approver set, its quorum rule, its constituent step records (withdecided_bymatched toapprover_refby Approval Step’s Invariant 4), and its Audit Trail attestations under the seven-year retention. The auditor independently computes the expected chain state from the step records and the quorum rule and confirms Invariant 2 holds: no chain reached Approved without the quorum-named decisions actually present. The auditor also queriesread_chain({scope: "...:tier-3", state: Pending, initiated_at: {after: ..., before: ...}})to verify no chain was left unresolved — a non-empty result would identify a stalled material chain, a control gap the auditor would surface. The covered entity has documentable, auditable control evidence with no recourse to developer testimony. -
Disputed approval — FDA Part 11 electronic signature challenge against a chain participant. An FDA investigator reviewing batch BR-2026-0412 challenges the authenticity of QP Lopez’s approval: the actor claims they did not approve the batch. The investigator queries
read_chain({subject_ref: "br-2026-0412"})and retrieves the chain plus its step records. Stepstep-lopez-0412showsdecided_by: "qp_lopez",decided_at: "2026-04-15T16:04:00Z",decision_reason: "Specification limits met; COA reviewed". Invariant 4 of Approval Step guarantees Lopez’s reference matchedapprover_refat decision time — no other actor could have produced this record. The Audit Trail’s Actor Identity attestation for the correspondingstep_approvedevent is then verified againstqp_lopez’s registered public material atdecided_at. The denied-approval claim cannot be sustained against the structural record without claiming credential compromise; that reinterpretation is the Compromise Disclosure composing pattern’s responsibility, not the chain composition’s. -
Breach or incident forensics — unauthorized chain initiation investigation. During a security incident review, the incident response team needs to determine whether any chains were initiated by actors who should not have held
chains:initiateduring a window of suspected privilege escalation (2026-05-01T00:00:00Z through 2026-05-03T23:59:59Z). The team queriesread_chain({initiated_at: {after: ..., before: ...}})and, for each chain in the window, walks the Audit Trail back to thechain_initiatedevent and verifies its Actor Identity attestation. The team also queries the Permissions store for grants ofchains:initiateactive during the same window; any initiator whose grant was not active atinitiated_atis a finding. The chain composition’s records faithfully document every initiation and outcome; the cross-store verification — initiator attestation vs. Permissions grant state at the initiation timestamp — is the audit operation that surfaces unauthorized chains.
Generation acceptance
A derived implementation of Multi-Party Approval is acceptable — in the regulator-acceptance sense — when an external auditor, given the application’s emergent state plus the constituent stores, can do all of the following without recourse to source code, runbooks, or developer narration:
-
Reconstruct any chain’s full lifecycle. From
chain_id: the chain record (subject, scope, initiator, approver set, quorum rule, state, timestamps); the ordered list of step records with each step’s decision, decider, decision timestamp, and reason; the Assignment records for each step; and the Audit Trail’sverify_recordresult for every chain-level and step-level event. Invariants 1 and 9 are the structural guarantee; this check verifies the reconstruction is complete and consistent. -
Verify quorum determinism over every terminal chain. For every chain in Approved, Rejected, or Withdrawn: independently compute the expected state from the constituent step state vector (
A,R,W,P) and the chain’squorum_ruleper the Quorum evaluation rule, and confirm it matcheschain_store[chain_id].state. Invariant 2 is the contract; this check verifies it holds across the chain store. -
Verify chain completeness and immutability. For every chain:
chain_to_steps[chain_id]is non-empty;|chain_to_steps[chain_id]| == |chain.approver_set| ≥ approver_set_minimum; eachstep_idexists in the Approval Step store and hasstep_to_chain[step_id] = chain_id; the chain’s declared fields (subject, scope, initiator, approver set, quorum rule, initiated_at) are unchanged across snapshots. Invariants 1 and 8 are the contract. -
Verify assignment coverage during pendency and recall on transition. For every step in Pending state in the chain’s
chain_to_steps: exactly one Active Assignment exists withtask_ref = step_idandassignee_ref = step.approver_ref. For every step in a terminal state: any Assignment for that step is in Recalled or Transferred (not Active). Invariant 4 is the contract; this check verifies the in-tray binding tracks the step lifecycle. -
Verify audit completeness. For every chain-level and step-level state transition recorded in the chain store and the Approval Step store, exactly one corresponding
record_actionevent exists in the Audit Trail. The reverse direction also holds: every chain-related action in the Audit Trail corresponds to a chain or step record. No chain or step transition is invisible to the Audit Trail; no Audit Trail entry refers to a chain or step that does not exist. Invariant 5 is the contract. -
Verify chain terminal absorption. Identify chains whose
chain_terminal_atis set and whose constituent steps include at least one still-Pending step (the trailing-decision case). Confirm thatchain_store[chain_id].stateandchain_terminal_atare unchanged in subsequent snapshots, regardless of any trailing step decisions. Invariant 7 is the contract.
Audit gaps: what cannot be cleared from the chain store alone
The six checks above cover every application-level invariant the composition enforces internally. Four audit questions arise around this composition that cannot be answered from the chain store and Approval Step store alone — they require either the Audit Trail substrate (which is part of the composition’s records — clearable, but requires cross-store traversal) or external evidence (truly out-of-scope). The distinction matters: a Round 3 reviewer asked whether “what cannot be cleared from these records alone” was honest given that the composition’s records include Audit Trail. The honest answer: the headings below split the two cases. They are named here so the audit boundary is explicit.
-
Whether the deployment’s approver-set policy was correctly applied. The composition records the
approver_setdeclared atinitiate_chainand enforces structural validity (size, uniqueness). It does not — and cannot — verify that the declared approver set was the correct set for the subject under the deployment’s regulatory policy. “Was this $12M journal entry required to be approved by exactly these three actors?” is a calling-system policy question; the chain records the answer the calling system declared. Verification of the policy mapping (transaction type → required approver set) is parallel to Approval Step’s named out-of-scope on which approvals are required for a given subject and Selective Disclosure’s Invariant 5 (calling-system integration obligation). Auditors verify this by reading the calling system’s policy declaration alongside the chain records. -
Whether each named approver held the standing authorization to approve the scope at initiation time. The composition does not check that
approver_refis permitted to approvescopeatinitiated_at. The named approver could be policy-unauthorized (an intern named for a controller-only gate) and the chain would still be structurally accepted here. Auditors verify approver appropriateness by cross-referencing each step’sapprover_refandscopeagainst the deployment’s standing-authorization registry — typically a separate Permissions instance scoped to scope-approval grants — at the chain’sinitiated_at. Named in Edge cases as a calling-system obligation; the chain composition’s records are necessary but not sufficient for this audit question. -
Whether the initiator’s
chains:initiategrant was appropriate at initiation time. The composition enforces that the grant existed and was Active at thePermissions.permittedcheck (Invariant 3). It does not verify that the grant was correctly issued in the first place — that the actor who issued the grant held the meta-authority to do so, that the grant scope matched the deployment’s policy for the subject type, or that segregation-of-duties policies were honored at grant time. Grant appropriateness is a composing-Permissions / governance concern; this composition’s audit surface is grant existence and use. -
Whether the deployment under partial-cascade recovery has fully reconciled. When a cascade-recall partially fails (see Cascade-recall of trailing assignments on chain termination), the
chain_resolvedevent carriescascade_partial = trueand the still-Active trailing Assignments are surfaced viaread_chain. The composition records that the cascade was partial; verification that the implementation has subsequently completed the recovery requires reading the follow-upcascade_completedAudit Trail entries (one per retried cascade call) and confirming every initially-failed call is matched by a latercascade_completedreferencing the originalchain_resolvedevent. An auditor reads both the originalchain_resolvedand thecascade_completedseries from Audit Trail to clear the recovery; the chain store alone does not surface recovery progress beyond the presence or absence of still-Active trailing Assignments. This is the cleanest case of a non-substrate audit question: the records contain the answer; the auditor must traverse bothchain_storeand Audit Trail to assemble it. Named here so the audit boundary is explicit rather than implicit.
Edge cases and explicit non-goals
What this composition does not cover:
-
Quorum rules beyond the three named.
all-of-N,M-of-N, andone-of-Ncover the canonical multi-party-approval patterns. Weighted-voting quorums (where approvers carry different vote weights), conditional quorums (M-of-N where the M must include specific actors), and sequenced quorums (where approver order is enforced — A must approve before B) are not supported in the canonical composition. Each is a richer quorum semantics that belongs to a higher-order composition; the canonical chain treats approvers as equal voters and decisions as order-independent in outcome. -
Sequenced (ordered) approval chains. Some regulated processes require approvals in a specific order (e.g., the safety officer must approve before the engineering lead). The canonical composition does not enforce ordering — all steps are submitted at
initiate_chainand become Pending in parallel. A Sequenced Approval Chain composition (forthcoming, not on the current roadmap) would extend this composition with an explicit ordering constraint and a step-not-yet-eligible rejection on out-of-order approvals. -
Delegation. As named in Approval Step’s Edge cases, delegation — binding a different actor to step in for the named approver — is not a property of the per-gate atom. It is also not a property of this composition. A deployment requiring delegation composes a separate Delegation pattern that intercepts
approve_step/reject_stepand re-authorizes the call when a delegation record permits the delegate to act on behalf of the named approver. The chain composition does not absorb the delegation policy. -
Segregation of duties. As named in Approval Step’s Edge cases, segregation of duties (the submitter cannot also be an approver; an approver cannot be on the second gate in a sequenced chain) is a policy concern. The chain composition does not enforce it. The composing Permissions pattern is the layer that rejects
initiate_chaincalls whose(initiator_ref, approver_set)pairing violates a declared SoD policy. The composition’sapprover_set_uniquenessconfig addresses only the pairwise-distinct case within the chain; broader SoD policies are external. -
Step-level rejection causing chain-level Rejected vs. Withdrawn. Under all-of-N, a single step rejection makes quorum unreachable and the chain transitions to Rejected (not Withdrawn). Withdrawal is reserved for the initiator’s deliberate retraction of the whole chain. The two terminal states carry different audit semantics: Rejected names a quorum failure attributable to specific approver decisions; Withdrawn names a chain retracted before reaching quorum determination. Conflating them would lose audit information.
-
Re-submission of a step under the same chain. Once a step is in a terminal state (any of Approved, Rejected, Withdrawn), it cannot be re-opened or re-submitted under the same chain. The chain’s
chain_to_stepslist is fixed atinitiate_chain. A redo requires a new chain. This mirrors Approval Step’s Invariant 3 (terminal absorption) at the chain layer. -
Late decisions on a terminated chain. A step’s named approver may approve or reject after the chain has already reached a terminal state via the quorum rule (e.g., under M-of-N(2) of 3, the third approver decides after the first two already satisfied the quorum). The atom-level decision is recorded (Approval Step’s Invariant 4 still gates who may decide), the Assignment is recalled, and the Audit Trail captures the event. The chain’s state does not change (Invariant 7). The trailing decision is preserved as audit evidence but is not part of the chain’s terminal disposition.
-
Concurrent step decisions on the same chain. Two approvers concurrently calling
approve_stepfor distinct steps of the same chain are processed independently by their respective Approval Step records. Each step’s transition is atomic at the atom level; chain-state re-evaluation after each step transition may race. Implementations must serialize the chain-state re-evaluation step (step 5 ofapprove_step/reject_step/withdraw_step) on a givenchain_idto ensure the re-evaluation reads a consistent step state vector. Concurrentwithdraw_chainand step-level decisions on the same chain must also be serialized — the implementation is responsible for the chain-scope mutex; the spec assumes it. -
Audit Trail composition with Legal Hold. When a chain’s subject is under a Legal Hold, the Audit Trail’s
purge_eligiblecascade is suspended for the chain’s events. The chain composition does not interact with Legal Hold directly; the suspension happens at the Audit Trail layer. The chain’s records persist through any retention horizon while a hold is active. -
Audit Trail records of failed chain initiations. A
record_actionis emitted only on a successfulinitiate_chain. A call rejected at the Permissions check (permission-denied) or at structural validation (invalid-request) leaves no Audit Trail entry by default. High-assurance deployments where failed initiation attempts are themselves auditable compose a Failed-Attempt Log pattern; the canonical composition’s audit surface is committed chain actions, not attempted actions. This parallels Audit Trail’s Failed attribution attempts edge case. -
Clock source for
chain_terminal_at. The application setschain_terminal_atat the moment the quorum evaluation rule transitions the chain to a terminal state. The clock source is the application’snow— typically the receiving node’s wall clock. Clock skew across distributed application nodes can produce achain_terminal_atthat is earlier than the triggering step’sdecided_atorwithdrawn_atunder sufficient skew. For deployments under strict clock-discipline requirements, the Trusted Timestamping composing pattern (referenced in Audit Trail) is the resolution. -
Cross-store consistency under failure.
initiate_chainwrites to four constituent stores in sequence (chain, Approval Step ×N, Assignment ×N, Audit Trail). A failure mid-sequence leaves partial state. The implementation must order operations so that either all writes succeed atomically or a compensating record is produced. The simplest recovery for a partialinitiate_chainfailure is chain-level withdrawal of the partially-submitted chain — the Approval Step records that were submitted are immutable, but they can be withdrawn through the cascade. Cross-store consistency onapprove_step/reject_step/withdraw_stepis tighter: the Approval Step transition is the load-bearing write; subsequent Assignment recall and Audit Trail recording are recovery-amenable through the orphan-state edge case in Audit Trail. The implementation owns the transactional boundary. -
Concurrent chains on the same subject. A subject may have multiple concurrent chains — for example, a financial transaction may require approvals under both a SOX chain and a separate AML chain. The composition does not restrict this; each chain has its own
chain_idand its own quorum evaluation. A composing system that requires “all chains on subject X must reach Approved before X may proceed” is a higher-order policy not enforced by this composition. -
Approver authorization at initiation is a calling-system obligation. The composition validates that the initiator holds
chains:initiate(Invariant 3) but does not validate that eachapprover_refin the declaredapprover_setholds the standing authorization to approve the namedscope. A chain that namesintern_xuas the approver for afinancial:journal-entry:post:materiality-tier-3gate is structurally accepted here; whether the intern is policy-authorized to hold that gate is a calling-system / Permissions check the calling system must run before callinginitiate_chain. Auditors verify approver appropriateness by cross-referencing each step’sapprover_refagainst the deployment’s standing-authorization registry (typically a Permissions instance scoped to scope-approval grants) at the chain’sinitiated_attimestamp. This parallels Approval Step’s named out-of-scope on segregation of duties and is named in Generation acceptance’s Audit gaps. -
Step-withdraw vs. chain-withdraw authority asymmetry. Step-level withdrawal (
withdraw_step) is governed by Approval Step’s Invariant 5 (submitter-only) — only the chain initiator may step-withdraw, regardless of whether they holdchains:withdraw. Chain-level withdrawal (withdraw_chain) requires the actor to (a) be the chain initiator and (b) holdchains:withdraw. The asymmetry is intentional: a chain initiator who has lost thechains:withdrawpermission post-initiation may still step-withdraw individual gates (correcting submission errors at the gate granularity, which is a property of the submitter role they earned at initiation) but cannot retract the chain as a whole (which requires the standing chain-level authority). Deployments that want both surfaces to requirechains:withdrawadd the check at the application boundary in front ofwithdraw_step; the canonical composition relies on the atom-level submitter check. -
Retention-horizon asymmetry between substrate and constituents. Audit Trail enforces a retention policy on its events; after
purge_eligiblefires on an event past itsretention_until,verify_recordon the correspondingevent_idreturnsfailed-verification(purged). The Approval Step store and the chain store have no retention policy of their own — Approval Step’s Invariant 10 (step store durability) and the composition’s chain immutability (Invariant 8) keep their records present indefinitely. The consequence: a chain whose Audit Trail retention has elapsed remains queryable throughread_chain(returning chain and step records, attestation-status notwithstanding), butverify_recordon its underlying chain-level and step-level events returnsfailed-verification(purged). The audit surface is consistent but staggered: the structural records (who decided what, when, under which approver set) persist; the cryptographic attribution chain has been lawfully destroyed. Auditors querying post-retention see the chain lifecycle and the Retention Window records in Purged state — sufficient for Audit Trail’s honest representation of destruction invariant; insufficient for any audit obligation that requires re-verifiable signatures past the retention horizon. Deployments needing perpetual signature re-verifiability configure a longer Audit Trail retention; the composition surfaces the trade-off rather than resolving it. -
Late-decision audit interpretation. A
step_approved,step_rejected, orstep_withdrawnevent whose data carriestrailing = truewas recorded after the chain itself had terminated (Invariant 7 / cascade-recall subsection). An auditor walking the Audit Trail in timestamp order may see astep_approvedevent with timestamp greater than the chain’schain_resolvedevent; without thetrailingflag, this read as a contradiction (decision after chain Rejected?); with the flag, the audit interpretation is unambiguous — the step’s named approver decided after chain termination, the chain state did not change, the audit trail records the decision for completeness. Thetrailingflag is the records-alone disambiguator. Auditors reading chains under M-of-N quorum where trailing decisions are routine should filter or sort ontrailingto separate on-chain decisions from after-the-fact recordings; the chain’s terminal disposition is read from itschain_resolvedevent alone, not from the union ofstep_approvedevents.
Standards references
This composition is the structural form of what every multi-actor regulatory approval regime requires:
- Sarbanes-Oxley §404 (17 U.S.C. §7262) — Internal control over financial reporting. Material financial actions require multi-actor approval; SOX auditors query the approval chain to confirm the control existed and operated. The composition is the structural form. Composes with Audit Trail’s SOX §802 retention obligation (7 years).
- FDA 21 CFR Part 211 (Current Good Manufacturing Practice for Finished Pharmaceuticals) — batch release requires authorization by a Qualified Person (QP) or equivalent designated authority. Multi-QP releases under M-of-N(M) quorum are the canonical composition for high-value batches and for batches requiring QA-plus-QP dual signoff.
- FDA 21 CFR Part 11 (Electronic Records; Electronic Signatures) — each approval in a chain is an electronic signature event. Part 11 §11.50 requires signatures be attributable; §11.70 requires they be linked to records to prevent removal, substitution, or falsification. The composition’s Audit Trail substrate (with Actor Identity providing the cryptographic binding and Tamper Evidence providing the linking) satisfies both.
- ICH E6(R3) Good Clinical Practice — Guideline. Sections 4–5 require documented approvals at multiple points in the trial lifecycle, often by multiple parties (investigator, sponsor, IRB). One-of-N over a delegated PI pool is a recurring composition shape for non-substantive deviations; M-of-N is the form for substantive deviations requiring both PI and sponsor approval.
- ISO 9001:2015 §8.5.1 (Control of production and service provision) and §7.5.3 (Control of documented information) — controlled changes require approval by named authorities. Multi-party chains map to ISO 9001 documented-procedure approval requirements.
- ISO 13485:2016 §7.3 (Design and development) — medical device design changes require multi-disciplinary approval (design lead, quality, regulatory affairs, often clinical). M-of-N quorum is the structural form.
- SOC 2 (Trust Services Criteria — Control Activities, Common Criterion CC2.1, CC8.1) — changes to data, infrastructure, or business processes require documented multi-party approval; SOC 2 audits query the approval chain as control evidence. The composition is one structural form.
- NIST SP 800-53 Rev. 5 (AC-5 Separation of Duties; CM-3 Configuration Change Control) — multi-actor approval is the operational form of separation of duties for change control. The composition is the structural form.
- PCI DSS Requirement 6.5.3 (Production data must not be used for testing or development) and 6.4.5 (Change control procedures) — production changes require documented multi-party approval.
It inherits from:
- Daniel Jackson, The Essence of Software — the composition discipline: the per-gate atom is freestanding; the chain is the composing pattern that wires N gates under a quorum rule.
- Approval chain literature in change-control and regulatory engineering — every major change-management framework (ITIL change advisory boards, FDA design-control reviews, SOC change-control gates) names multi-party approval as the structural form; the composition is the formal version.
Status
grounded on Final Critique 4 — 2026-05-20 — foundation round (Pass 1 + Pass 2 + Pass 3, author-led), Round 2 (human refinement, happy-Torvalds posture), and Round 3 (AI-conducted adversarial, claude-opus-4-7, Super Torvalds posture) complete. Under the unified methodology (3×3 baseline rounds + Final Critique starting at Round 4), this pattern’s AI-conducted Super Torvalds round is retro-labeled as Final Critique 4 — the adversarial intensity matches the new convention even though the original numbering treated it as Round 3 of a single 3×3 sequence. Composition logic specified across all four constituent atoms plus the Audit Trail substrate; emergent state (chain_store including the optional audit_pending flag, chain_to_steps, step_to_chain, step_to_assignment, chain_terminal_at) named; the application actor (application_actor_ref / application_credential) named in Configuration as the deployment-provisioned identity for application-emitted Audit Trail entries, with the records-alone forgery defense (Invariant 2 verification) and the human-vs-application event distinguishing rule both explicit; nine application-level invariants stated and justified, Invariant 2 covering both terminal-path origins (quorum-rule and withdraw_chain-direct), Invariant 4 covering both per-step recall on transition and chain-level cascade-recall on terminal, Invariant 5 acknowledging Audit Trail’s partial-attestation atomicity surface; action wiring covers chain-level and step-level actions with fully-named rejection taxonomies, the late-decision contradiction resolved (chain-terminal does not gate the call; the step’s own state is the load-bearing gate), trailing flag added to step-level audit data to distinguish on-chain from trailing decisions, and an enumerated three-case partial-state recovery path on initiate_chain failure; the quorum evaluation rule is the load-bearing wiring decision and distinguishes chain-Rejected (R ≥ 1) from chain-Withdrawn-by-cascade (R = 0 ∧ W ≥ 1) under both all-of-N and M-of-N; cascade-recall of trailing assignments on chain termination is its own subsection with partial-cascade-failure recovery enumerated, recalled_step_ids rule made always-present, trailing-step asymmetry between Withdrawn-by-cascade and Approved/Rejected made explicit, and idempotent late-decision semantics; walkthrough plus four cross-domain examples and three adversarial scenarios; seventeen edge cases (the fifteen from prior rounds — thirteen from the foundation and Round 1, two added in Round 2: Approver authorization at initiation and Step-withdraw vs. chain-withdraw authority asymmetry — plus Retention-horizon asymmetry between substrate and constituents and Late-decision audit interpretation added in Round 3); Generation acceptance bar explicit with six checks plus four named audit gaps split between Audit-Trail-traversal-clearable and externally-clearable. Sixth entry in compositions/.
Lineage notes
Regulated composition. Conventions — Regulated adversarial scenarios and Generation acceptance — inherited from the methodology directly (PRESSURE_TESTING.md), baked in from the first draft. Audit Trail is the primary structural reference (regulated four-atom composition shape); Shared Todo is the secondary reference (Permissions + Assignment wiring discipline). The atoms this composition depends on most heavily are Approval Step (whose Round 3 just landed — see atoms/workflow/approval-step.md Lineage notes for what was settled there) and Audit Trail (whose application-level invariants this composition transitively relies on).
Structural milestone. This composition is the first to compose another composition (Audit Trail) as one of its constituents. The decision to treat Audit Trail as a substrate rather than re-listing its four atoms as constituents at this layer is a deliberate scoping choice: Audit Trail owns the regulated-audit invariants (attribution coverage, retention coverage, integrity coverage, cascade-on-purge); this composition does not re-derive them. The ROADMAP entry lists Event Log and Actor Identity as prerequisites, but they are reached transitively through Audit Trail — neither is composed directly at this layer. The Composes section names this explicitly.
Pass 1 — Structural completeness (GRID). Three findings, all closed in-pattern.
-
Application state lacked the
chain_terminal_atfield. Early draft tracked chain state transitions through the audit log only; without achain_terminal_aton the chain record, queries like “when did this chain reach Approved?” required walking the Audit Trail for every chain. Fixed:chain_terminal_atadded tochain_store, set once when the chain leaves Pending, and surfaced in theread_chainquery result. -
The quorum evaluation rule was implicit. Initial draft described chain states in prose without specifying the deterministic function from (step state vector, quorum rule) to (chain state). A reader could not independently verify Invariant 2 (quorum determinism). Fixed: explicit Quorum evaluation rule subsection added under Composition logic, naming variables (A, R, W, P), giving the rule per quorum class, and stating the terminal-stable and order-independent properties.
-
read_chainfilter axes andinvalid-queryconditions unstated. Parallel to every prior atom’s Pass 1 finding on read queries. Fixed:read_chainaction description enumerates the six filter axes, names the{after, before}time-range form (mirroring Approval Step’s), and states that unrecognized filter keys areinvalid-queryand well-formed empty results are an empty sequence (not a rejection).
All nine GRID nodes resolved.
Pass 2 — Conceptual independence (EOS — Essence of Software, Daniel Jackson’s framework for specifying software concepts as freestanding, composable units). Clean. Six extraction candidates evaluated; all kept in-pattern or correctly externalized.
-
Quorum rule as a separate atom. Could the quorum evaluation logic be its own atom (Quorum Rule, taking a state vector and returning a terminal state)? Evaluated: the rule has no state of its own — it is a pure function from
(quorum_rule, state vector)to chain state. It does not recur as a freestanding concept; it appears only in the context of a multi-party gate. Extracting it would produce a stateless function masquerading as an atom. Kept in-composition as the Quorum evaluation rule. -
Sequenced ordering as an absorbed concern. Could the composition absorb step ordering (A must approve before B)? Evaluated: sequenced ordering is a richer quorum semantics that genuinely changes the state machine — Pending becomes “Pending blocked on step S” — and recurs across regulated change-control workflows. It belongs to a higher-order composition (Sequenced Approval Chain). Named as an explicit out-of-scope.
-
Delegation as an absorbed concern. Already settled by Approval Step at the atom level (composing concern). Re-evaluated at the composition level: delegation logic at the chain layer would require importing delegation state into chain records, which is the same over-absorption the atom rejected. Kept as a composing concern (parallel to Approval Step’s Lineage); named as an out-of-scope.
-
Segregation of duties as an absorbed concern. Settled by Approval Step’s Round 3 (composing Permissions/Multi-Party-Approval concern). The chain composition is the Multi-Party Approval composition Approval Step’s Round 3 named — so the question becomes whether SoD policy enforcement belongs here. Evaluated: SoD policy is calling-system-specific (same role? same department? same legal entity?) and cannot be settled at composition level any more cleanly than at atom level. Permissions is the layer that can reject
initiate_chaincalls with a policy-violating actor pairing. Kept as composing-Permissions concern; named in Edge cases. -
Audit Trail as a constituent atom or as a substrate composition. Treated as a substrate composition rather than a constituent atom (the composition-of-composition decision). Evaluated: Audit Trail’s eight application-level invariants are what this composition needs from the audit substrate; re-listing its four atoms as constituents would re-derive those invariants here and risk drift. Kept as substrate; the Composes section names this explicitly with the rationale.
-
Failed-attempt logging. The composition does not log Permissions-denied initiation attempts. Evaluated: parallel to Audit Trail’s Failed attribution attempts edge case, this is a composing concern (Failed-Attempt Log) for high-assurance deployments. Named in Edge cases.
No new atoms extracted. Pass 2 clean.
Pass 3 — Adversarial scrutiny (Linus mode). Eight findings, all closed in-pattern.
-
Identity model on
chain_idand chain immutability unstated. Initial draft treated chain identity informally — the chain “has an id” — without specifying immutability or non-reuse. Fixed: Invariant 8 (chain immutability of declared fields) and the Application state subsection now namechain_idas opaque, system-generated, never reused, never reassigned, with the declared fields (subject, scope, initiator, approver set, quorum rule, initiated_at) immutable frominitiate_chainreturn. -
Late-decision behavior on terminated chains unaddressed. Under M-of-N(2) of 3, what happens when the third approver decides after the chain has reached Approved? Initial draft was silent. A naive implementation might (a) reject the decision (
not-pending), (b) accept it and update the chain’s state machine, or (c) accept it and ignore the chain implication. Fixed: Invariant 7 (chain terminal absorption) names the contract — the step’s decision is recorded by Approval Step (its Invariant 4 still gates who may decide), the Assignment is recalled, the Audit Trail captures the event, but the chain’s state does not change. Edge case Late decisions on a terminated chain makes this explicit. -
Rejection priority on
initiate_chainnot enumerated. Initial Decision points described preconditions but did not state evaluation order. Fixed: rejection priority acrossinitiate_chainfollows the pattern from Approval Step’s Round 2 — Permissions check first, structural validation second, recording-failure last. -
Quorum rule’s behavior on
one-of-Nas distinct fromM-of-N(1). Initial draft enumerated three quorum rules but did not state thatone-of-Nis mathematically equivalent toM-of-NwithM=1. A reader could implement them as distinct rules and drift. Fixed: the quorum evaluation rule namesone-of-Nas a special case ofM-of-N(M=1); the evaluation logic is unified. -
Step-level vs. chain-level withdraw semantics conflated. Initial draft had a single
withdrawaction ambiguous about whether it withdrew the chain or a single step. Fixed:withdraw_chainandwithdraw_stepnamed as distinct actions; the Edge cases subsection Step-level rejection causing chain-level Rejected vs. Withdrawn explains the audit-semantics distinction. -
Audit Trail substrate not surfaced for failed-initiation attempts. Initial draft did not name that Permissions-denied
initiate_chaincalls produce no Audit Trail entry. Fixed: Edge case Audit Trail records of failed chain initiations names the gap and the Failed-Attempt Log composing pattern as the resolution for high-assurance deployments. -
Cross-store consistency under partial failure unaddressed.
initiate_chainwrites to four constituent stores in sequence; what if step 4 (Approval Step submission) succeeds for the first two approvers and fails for the third? Initial draft was silent. Fixed: Edge case Cross-store consistency under failure names the partial-state recovery path (chain-level withdrawal cascading through the Approval Steps that did succeed), parallel to Audit Trail’s edge case of the same name. -
Generation acceptance silent on what the auditor cannot clear. The six numbered checks cover every application-level invariant the composition enforces internally. But three audit questions — whether the deployment’s approver-set policy was correctly applied, whether the initiator’s grant was appropriate at issuance time, and whether the
chain_terminal_atcorresponds to the actual first-firing moment — cannot be cleared from these records alone. Fixed: parallel to Selective Disclosure’s Round 3 fix, an Audit gaps subsection added to Generation acceptance naming the three unclearable questions and the composing patterns or external evidence that surface each.
Three deferred concerns are named as explicit out-of-scope rather than fixed in-pattern: clock skew on chain_terminal_at under distributed deployment (Trusted Timestamping is the resolution), concurrent chain decisions requiring serialization on chain-state re-evaluation (implementation-owned), and concurrent chains on the same subject (composing-policy concern). Each is correctly external to the composition.
The three passes together exercise the architecture as designed: GRID checks structural completeness of a chain-plus-substrate composition (no missing wiring; every emergent property has a named state component); EOS keeps the composition from absorbing quorum-rule-as-atom, sequenced-ordering, delegation, and SoD — each is correctly externalized; Linus catches the eight hidden contracts (chain identity model, late decisions, rejection priority on initiate, quorum-rule unification, withdraw semantics, failed-initiation audit gap, cross-store consistency, audit-gap framing in Generation acceptance) that would otherwise hide beneath the “just compose Approval Step under a quorum rule” summary. The composition is stronger because all three checks happened.
Round 2 — Human refinement, happy-Torvalds posture (author-conducted, 2026-05-13). Eight findings, all closed in-pattern. The round re-read the foundation draft as a smart skeptic in a good mood: same rigor, less venom, looking for mechanisms that hide behind plausible prose.
Pass 1 — GRID structural. Two findings, both closed in-pattern.
- Application actor for system-emitted Audit Trail entries was nowhere declared. The decision-action wiring described
AuditTrail.record_action(action_ref=chain_resolved, actor_ref=system, credential=system_credential, ...)— butsystemandsystem_credentialhad no definition in Configuration, in State, or in Edge cases. An auditor querying the Audit Trail for achain_resolvedevent would see an actor reference with no corresponding registration in the Actor Identity registry; verification would either fail or silently special-case. Fixed: a new Configuration entryapplication_actor_refandapplication_credentialnames this as a deployment-provisioned first-class actor in the Audit Trail’s underlying Actor Identity registry — its attestations verify under the same rules as human-actor attestations, and the deployment is responsible for issuing, rotating, and retiring the credential under the same discipline applied to any privileged service identity. All references toactor_ref=system, credential=system_credentialupdated toactor_ref=application_actor_ref, credential=application_credential. chain_to_stepsordering basis was implicit. The list was described as “ordered” without naming whether the order tracksapprover_setdeclaration, submission order, or some other axis, and without saying whether the order is load-bearing. Fixed: the application-state entry now states the list order is the order in whichapprover_setwas declared (equivalently, submission order, since steps are submitted in declaration order), and that the order is diagnostic, not load-bearing — the canonical quorum rules are order-independent in outcome, so any total order yields the same evaluation. Composing patterns that do impose ordering (Sequenced Approval Chain) consume the list as an ordered sequence and add their own enforcement.
Pass 2 — EOS conceptual independence. Clean. The six foundation extraction candidates re-evaluated independently with no recourse to prior reasoning; all six conclusions confirmed. Three candidates considered fresh and excluded:
- Quorum-rule-as-atom (re-evaluated). Still pure stateless function — no own state, no recurring cross-domain concept that survives outside the multi-party-gate context. Kept in-composition.
- Cascade-recall logic as a composing pattern. Could the trailing-assignment cleanup be extracted as its own pattern (e.g., a generic Cascade-on-Terminal composition)? Evaluated: the cleanup logic is specific to the chain-step-Assignment relationship of this composition; generalizing it would require a parameterized cascade-rule that no other current composition needs. Kept in-composition.
- Application actor as its own atom. Could the deployment-provisioned application actor be its own atom (something like “System Identity”)? Evaluated: an application actor is just a regular actor in the Actor Identity registry with a non-human binding. It does not have its own state machine; it is a Permissions and operational concern. The Actor Identity atom already supports it. No new atom needed.
No new atoms extracted. Pass 2 clean.
Pass 3 — Adversarial scrutiny (Linus mode, happy variant). Six findings, all closed in-pattern.
- Quorum rule conflated Withdrawn and Rejected step outcomes under all-of-N. The foundation draft said “If R + W ≥ 1, the chain transitions to Rejected.” But rejection and withdrawal carry distinct audit signals — a step rejection is a deliberate negative decision by the named approver; a step withdrawal is the chain initiator retracting a single mis-submitted gate. Conflating them into chain-Rejected loses an audit distinction that regulators rely on. Fixed: the quorum rule now distinguishes the two cases under all-of-N (R ≥ 1 → chain-Rejected; R = 0 ∧ W ≥ 1 → chain-Withdrawn-by-cascade with full step cascade) and under M-of-N (same partition applied to the quorum-unreachable case). Rejection takes priority over withdrawal where both could apply.
- Trailing assignment cleanup on chain termination was unspecified. Under M-of-N(2)/3, when the chain reaches Approved with one step still Pending, that step’s Assignment stays Active until the named approver either decides (eventually) or never does. The in-tray binding survives the chain’s relevance — an approver continues to see moot work, and an auditor querying “what approvals are currently pending action” gets stale results. Fixed: a new Cascade-recall of trailing assignments on chain termination subsection added under the quorum rule, recalling all still-Active Assignments for the chain’s steps when the chain transitions to any terminal state; Invariant 4 extended to cover both per-step recall on transition and chain-level cascade-recall on terminal; late decisions on trailing-Pending steps recall the (already-Recalled) Assignment idempotently, with the composition treating the second
Assignment.recall’snot-activerejection as no-op success. - Approver authorization at initiation was an unnamed calling-system obligation. The composition validates that the initiator holds
chains:initiatebut does not verify that each named approver holds the standing authorization to approve the scope. A chain namingintern_xuas the approver for a controller-only gate is structurally accepted here, and the audit boundary on this question was not surfaced. Fixed: an Edge case added naming the obligation as a calling-system check the calling system must run beforeinitiate_chain(typically a separate Permissions instance scoped to scope-approval grants), and the Audit gaps subsection of Generation acceptance extended with the corresponding entry — auditors verify approver appropriateness by cross-referencing each step’sapprover_refandscopeagainst the standing-authorization registry at the chain’sinitiated_at. approver_set_uniqueness = falsequorum semantics unclarified. Under the relaxed-uniqueness configuration where the same actor occupies two slots, does one approval credit both slots? The foundation draft was silent. A reader could assume either model. Fixed: the Configuration entry now states that quorum is counted per step, not per actor — an actor named in two slots must callapprove_steptwice (once perstep_id) to contribute two units to A; one decision does not auto-credit both slots, and silently collapsing two gates would be the chain composition hiding a mechanism the spec refuses.AuditTrail.record_actionfailure mid-initiate_chainleft the recovery path vague. The foundation draft’s step 7 said “the implementation must handle the partial-state cases per the Cross-store consistency under failure edge case” without enumerating the cases. Fixed: step 7 ofinitiate_chainnow enumerates three specific partial-state cases — failure at step 4 (k of N steps submitted), failure at step 5 (j of N Assignments created), failure at step 6 (no Audit Trail entry forchain_initiated) — and names the recovery path for each, including the most serious case (audit-trail failure leaving anaudit_pendingchain in a quarantined state) and the write-ahead-audit-logging deployment variant for regulators who do not tolerate any audit-pending state.- Step-withdraw vs. chain-withdraw authority asymmetry was a hidden decision.
withdraw_stepis governed by Approval Step’s Invariant 5 (submitter-only) — only the chain initiator may step-withdraw, regardless of whether they holdchains:withdraw.withdraw_chainrequires both (a) being the chain initiator and (b) holdingchains:withdraw. A chain initiator who has lostchains:withdrawpost-initiation can still step-withdraw individual gates but cannot retract the whole chain. The asymmetry is defensible but was never named. Fixed: an Edge case added stating the asymmetry, the rationale (step-withdraw is a submitter-role property earned at initiation; chain-withdraw is the standing chain-level authority), and the deployment variant for callers who want both surfaces to requirechains:withdraw.
Pass 3 in Round 2 caught what the foundation round was structurally inclined to miss: the cases where two adjacent concepts (Rejected/Withdrawn under all-of-N; per-actor/per-step quorum counting; step-level/chain-level withdraw authority) were silently treated as equivalent when they carry distinct audit semantics. The composition is materially stronger after the round; the audit surface is now both larger (more Generation acceptance audit gaps named) and more discriminating (the chain terminal state distinguishes rejection from withdrawal at the chain level, mirroring how Approval Step distinguishes them at the step level).
Round 3 — AI-conducted adversarial round (claude-opus-4-7, Super Torvalds posture, independent re-run). 2026-05-13. Twelve findings, all closed in-pattern. Round 3 re-ran all three passes with fresh-reader discipline and no authoring sentiment toward the composition; the brief was to hunt for the class of bugs the prior two rounds were structurally inclined to miss — the ones that hide in plain sight because the refinement that closed nearby gaps silently broke or under-specified an adjacent claim. Round 2’s biggest risk was exactly this: extending the spec to fix the chain-Rejected/chain-Withdrawn-by-cascade distinction and adding the cascade-recall subsection introduced new wiring that the existing action descriptions and invariants did not absorb.
Pass 1 — GRID structural. Three findings, all closed in-pattern.
-
Late-decision contradiction between action wiring and Invariant 7. The biggest finding of the round. Round 2 added the cascade-recall subsection and Invariant 7 (chain terminal absorption), both of which assert that a step’s named approver may decide after the chain has terminated and that the decision is recorded with idempotent assignment recall. The FDA Part 11 example explicitly walks this scenario for QP Kim. But
approve_step/reject_step/withdraw_stepstep 1 — unchanged from the foundation draft — said “If the chain is in a terminal state, returnnot-pending(any approval after chain termination is a no-op the application must refuse).” This is a head-on contradiction: the action wiring refuses what Invariant 7 and the cascade-recall subsection record. An implementor following the action wiring rejects every late decision; an implementor following Invariant 7 records them. Pass 1 of both prior rounds missed this because each round read the spec by section rather than tracing a single late-decision call through the full document. Fixed: step 1 of all three step-decision actions updated to state that the chain’s terminal state does not gate the call; the step’s own state (checked by Approval Step in step 2) is the load-bearing gate. Step 5 of each action updated to short-circuit re-evaluation when the chain is already terminal (thetrailing = truebranch). Late decisions now flow correctly through to the Approval Step record, the assignment recall (idempotent), and the Audit Trail entry, with no chain-state side-effect. -
audit_pendingis hidden state. Round 2 added the three-case partial-state recovery path oninitiate_chainfailure, including case (c) where Audit Trail’srecord_actionforchain_initiatedfails and the chain is described as carrying an “audit_pendingflag, not part of the canonical chain state machine, that read-only queries surface to the auditor.” But: the chain_store field enumeration did not includeaudit_pending;read_chaindid not mention it; no invariant referenced it. It was a one-sentence promise the rest of the spec did not honor. Fixed:chain_storedescription now includes the optionalaudit_pendingflag with its lifecycle (set true during 7c recovery, cleared when thechain_initiation_failedaudit record lands), andread_chainstep 3 names that the field is surfaced on each chain record when set. The flag is explicitly not a state in the canonical state machine (the chain is still in Pending or terminal); it is a recovery-status field. -
Trailing-step asymmetry between chain-Withdrawn-by-cascade and chain-Approved/chain-Rejected was buried inside the action wiring. Round 2’s Cascade-recall subsection said trailing-Pending steps remain in Approval Step’s Pending state, but a separate mention inside
approve_stepstep 5 said that under chain-Withdrawn-by-cascade, the application additionally cascade-withdraws those steps to Approval Step’s Withdrawn state. The two passages describe different behavior; only one of them is correct for any given terminal state. A reader of the cascade-recall subsection alone would implement uniform Pending-preservation; a reader ofapprove_stepstep 5 alone would implement uniform cascade-withdrawal. Fixed: the Cascade-recall subsection now opens with an explicit asymmetry rule — under chain-Approved or chain-Rejected, trailing steps remain Pending; under chain-Withdrawn-by-cascade, trailing steps are step-Withdrawn. The asymmetry is defended in-line (chain-Withdrawn names a deliberate retraction; chain-Approved/Rejected name quorum outcomes that do not foreclose the named approver’s right to record).
Pass 2 — EOS conceptual independence. Clean. The six foundation extraction candidates and three Round 2 candidates re-evaluated without recourse to prior conclusions; all confirmed. Four new candidates considered fresh:
audit_pendingquarantined state as a separate atom (Recovery State / Quarantine). Evaluated: the flag is a recovery-status field with no state machine of its own and no recurrence beyond this composition’sinitiate_chain7c recovery path. Extracting a generic Quarantine atom would require parameterizing over what is quarantined and how it is resolved — no other current composition surfaces a parallel concept. Kept in-composition; surfaced explicitly per Pass 1.- Application actor as a separate atom (Service Identity / Application Actor). Round 2 evaluated and concluded “an application actor is just a regular actor in the Actor Identity registry with a non-human binding.” Re-confirmed in Round 3. Two new observations: the records-alone forgery defense (Invariant 2 verification catches fake
chain_resolvedevents) lives inside this composition, not in a hypothetical Service Identity atom; the rotation/retirement discipline forward-links to Compromise Disclosure (a forthcoming composing pattern), and the gap until Compromise Disclosure lands is named in the Configuration entry. The forward-link is honest; the security model is records-alone defensible. - Cascade-recall as a generic Cascade-on-Terminal pattern. Round 2 evaluated and kept in-composition. Re-evaluated: the cascade is tightly coupled to (chain, step, Assignment) and would not survive parameterization over arbitrary terminal/recall pairs. No other composition currently needs the pattern. Kept; will revisit if a second compositions surfaces a parallel structure.
- Audit Trail-as-substrate leakage. Audit Trail’s own Partial attestation on step failure edge case (Actor Identity.attest succeeds, EventLog.append fails) propagates into this composition’s Invariant 5: the invariant said “one Event Log entry, one attestation, one retention record per call” without acknowledging the substrate’s atomicity gap. Not an extraction — Audit Trail correctly owns the gap — but a qualification of the invariant. Fixed under Pass 3.
No new atoms extracted. Pass 2 clean.
Pass 3 — Adversarial scrutiny (Super Torvalds mode). Nine findings, all closed in-pattern. The posture: read every load-bearing invariant and adjacent wiring step as a hostile auditor; trace the consequences of each Round 2 expansion to see what claim it silently broke or under-specified.
-
record_action(chain_resolved)failure mid-transition was unspecified recovery. Step 5 ofapprove_step/reject_step/withdraw_stepsays “If the chain transitions to any terminal state, setchain_store[chain_id].stateaccordingly, setchain_terminal_at = now, cascade-recall … callAuditTrail.record_action(action_ref=chain_resolved, ...).” If the chain state has been transitioned inchain_storebutAuditTrail.record_actionthen fails, the chain is in a terminal state in the chain store with no Audit Trail entry for the transition — a direct violation of Invariant 5. The foundation and Round 2 specs were silent on this case. Fixed: the Cascade-recall subsection’s Partial-failure recovery during cascade paragraph now covers this case: the application records the partial outcome to Audit Trail withcascade_partial = true(when applicable) and the implementation retries the failed cascade calls with follow-upcascade_completedAudit Trail entries; step 4 and step 5 ofapprove_step/reject_step/withdraw_stepnow cross-reference the recovery path explicitly. -
Cascade-recall partial failure was unspecified. The cascade calls
Assignment.recallonce per still-Active assignment, and (under Withdrawn-by-cascade)ApprovalStep.withdrawonce per still-Pending step. If one of those constituent calls returnsstorage-failure, the cascade is partial. The spec was silent. Fixed: the Cascade-recall subsection now enumerates the partial-failure recovery rule — the application proceeds through the remainder of the cascade rather than aborting, recordscascade_partial = truewith the actual progress reflected inrecalled_step_ids, returns the load-bearing success token to the caller (the chain transition itself succeeded), and the implementation retries the failed cascade calls until they succeed, emittingcascade_completedAudit Trail entries per retry. Parallel rule applied towithdraw_chainstep 4. -
Late-decision action_ref ambiguity in the Audit Trail. A trailing decision on a chain-Approved or chain-Rejected chain produces a
step_approved(orstep_rejected) Audit Trail entry whose timestamp is after the chain’schain_resolvedevent. Without a distinguishing signal, the audit interpretation is ambiguous: did the implementation record an approval after Rejected resolution (a contradiction), or is this a late decision per Invariant 7 (lawful)? The auditor can compute the answer from the chain’schain_terminal_atversus the step’sdecided_at, but the interpretation is inferred rather than declared. Fixed: every step-level Audit Trail entry now carries atrailingboolean in its data field —truewhen the chain was already in a terminal state at step 1 of the action,falseotherwise. The flag is the records-alone disambiguator; an auditor reads it directly without inference. New edge case Late-decision audit interpretation names the auditor’s interpretation rule explicitly. -
recalled_step_idsoptionality rule was unstated. The chain_resolved event’s data field carriedrecalled_step_ids?(with?marking optionality), but the spec never said when it is present versus absent. Two implementations could differ. Fixed: the field is now always present (never optional) — empty list when no trailing assignments were recalled, one or more entries otherwise. Single audit-data schema across allchain_resolvedevents. -
Invariant 2 (quorum determinism) did not cover chain-Withdrawn-by-initiator. The invariant said state is the deterministic outcome of applying the quorum rule to the final state vector. But chain-Withdrawn-by-initiator is set directly by
withdraw_chainstep 5, bypassing the quorum rule. A strict reading suggested the invariant did not apply. Fixed: Invariant 2 updated to show the equivalence — underwithdraw_chain, the step-cascade in step 4 fills the final state vector withWentries untilP = 0, and applying the quorum rule to that vector yields Withdrawn under both all-of-N and M-of-N, matchingchain_store[chain_id].state. The two paths to chain-Withdrawn are distinguishable from the records via the Audit Trail’saction_ref(chain_withdrawnvs.chain_resolvedwithstate = Withdrawn), and the invariant now names both this distinction and the deterministic-state equivalence. -
Audit gap 4 framing inconsistency. The Audit gaps subsection’s heading said “what cannot be cleared from these records alone” but its body for gap 4 (chain_terminal_at vs. first-firing moment) named “the Audit Trail’s record_action receipt timestamp on the triggering step event versus chain_terminal_at is the audit pair” — the audit pair the body names IS in the composition’s records, since Audit Trail is the composition’s substrate. So the gap is clearable; the heading was wrong. Fixed: the Audit gaps section now splits into Audit-Trail-traversal-clearable gaps (where the auditor reads the composition’s full records including Audit Trail) and externally-clearable gaps (where external evidence is required). The original gap 4 became less interesting after the partial-cascade recovery path was added in Pass 3, so it was replaced with the cascade-recovery audit gap — a true Audit-Trail-traversal gap surfaced by this round’s work — and the framing now matches the body.
-
Invariant 5 (audit completeness) assumed
record_actionatomicity. Audit Trail’s Partial attestation on step failure edge case names that Actor Identity.attest can succeed while EventLog.append fails, leaving an orphan attestation. The composition’s Invariant 5 said “every chain-level action produces exactly onerecord_actioncall, which in turn produces one Event Log entry, one Actor Identity attestation, and one Retention Window record” — a claim the substrate cannot guarantee. Fixed: Invariant 5 now acknowledges the substrate’s atomicity surface explicitly — the invariant holds modulo Audit Trail’s own contract; the composition’s role is to surface unresolved orphans (alerting on partial-attestation cases that reference this composition’saction_refvalues) rather than to police them. The substrate owns its recovery; this composition inherits the discipline. -
Records-alone defense against forged
chain_resolvedevents was not stated. The Configuration entry forapplication_actor_refnamed the rotation/retirement discipline and forward-linked to Compromise Disclosure, but did not say what records-alone defense exists ifapplication_credentialis compromised. The answer is Invariant 2: a forgedchain_resolvedevent whose declaredstatedoes not match the deterministic outcome of the quorum rule on the chain’s step state vector is structurally detectable by any reader recomputing the rule from the chain and step records. Forging the underlying step records is foreclosed by Approval Step’s Invariant 4. Fixed: the Configuration entry now names the forgery defense and the distinguishing rule between human-emitted and application-emitted audit events (chain_resolvedis the sole event withactor_ref = application_actor_ref; all others use the human actor reference). Auditors walking the trail apply the rule directly. -
Retention-horizon asymmetry between substrate and constituents was unnamed. The Super Torvalds brief asked whether the audit surface is consistent when Audit Trail’s retention elapses for a step’s events but Approval Step’s own store retains the step record (Approval Step’s Invariant 10 — step store durability — has no retention coupling). The composition was silent. Fixed: a new edge case (Retention-horizon asymmetry between substrate and constituents) names the staggered audit surface — structural records persist; cryptographic attribution chain has been lawfully destroyed — and points deployments needing perpetual signature re-verifiability at the longer-Audit-Trail-retention configuration. The trade-off is surfaced, not resolved.
Three deferred concerns are re-confirmed as explicit out-of-scope, unchanged from prior rounds: clock skew on chain_terminal_at under distributed deployment (Trusted Timestamping); approver-set policy verification (calling-system obligation, named in Audit gaps and Edge cases); standing-authorization-of-approver verification (calling-system obligation, named in Audit gaps and Edge cases).
The Round 3 arc validates the multi-file refinement order rule from PRESSURE_TESTING.md: Approval Step had completed its own Round 3 immediately before this round, and the cleanup of read time-range sub-keys, the self-approval / SoD edge case, the required-approvals-per-subject mapping, and check 1’s records-alone scoping in the constituent atom all aligned cleanly with the audit-gap framing and edge-case structure in this composition. The composition’s audit surface — Audit gaps split between Audit-Trail-traversal-clearable and externally-clearable, with edge cases naming approver-set policy and standing-authorization as calling-system obligations — is the chain-level extension of the boundary Approval Step’s Round 3 named at the atom level. Refining the constituent first surfaced the right discipline for refining the composition; the order rule earned its keep again.
Scheduled rescan: 2026-05-20. Pass 1 clean: all nine GRID nodes confirmed; constituent atom invariant counts in Invariant 6 verified against today’s rescanned atoms (Approval Step ten invariants ✓, Permissions ten invariants ✓, Assignment ten invariants ✓, Audit Trail eight application-level invariants ✓). One refining finding closed in-pattern. Pass 2 clean. Pass 3 clean beyond the refining finding.
- R1 — Edge case count incorrect in Status (refining). Status text stated “fifteen edge cases (the thirteen from prior rounds plus Retention-horizon asymmetry and Late-decision audit interpretation)” but the Edge cases section contains seventeen distinct entries. The correct accounting: thirteen from the foundation and Round 1, two added in Round 2 Pass 3 (Approver authorization at initiation is a calling-system obligation and Step-withdraw vs. chain-withdraw authority asymmetry), plus two from Round 3 — total seventeen. The thirteen-from-prior-rounds claim silently excluded the two Round 2 additions. Resolved: Status text updated to “seventeen edge cases (the fifteen from prior rounds…)” with the breakdown made explicit.
No constituent atom API changes from today’s rescans require further update to this composition. The tamper-evidence fix (mechanism_credential precondition) propagates only through the Audit Trail substrate and has no effect at this composition’s surface. Scheduled rescan: 2026-05-20 — clean.