GCC Build OSv0
/api

Decision Lifecycledecision

Lifecycle state machine for Decision nodes. Encodes multi-step approval chains per Q2 (Build Plan §1, §1.1, §6.7) using a step-counter on the Decision node's attributes. Reversal of a Locked decision requires a same-transaction MemoryEventRecorded of type 'reversal_reason' per Scope v2 §1A.4.6 and Build Plan §6.7.5 / §6.9.7.

Node type
Decision
Initial state
Open
Terminal states
Superseded

Full machine

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://gcc.bootminds.com/ontology/state-machines/decision.json",
  "title": "Decision Lifecycle",
  "description": "Lifecycle state machine for Decision nodes. Encodes multi-step approval chains per Q2 (Build Plan §1, §1.1, §6.7) using a step-counter on the Decision node's attributes. Reversal of a Locked decision requires a same-transaction MemoryEventRecorded of type 'reversal_reason' per Scope v2 §1A.4.6 and Build Plan §6.7.5 / §6.9.7.",
  "node_type": "Decision",
  "initial_state": "Open",
  "terminal_states": [
    "Superseded"
  ],
  "x-step-counter-attributes": {
    "current_approval_step": {
      "type": "integer",
      "minimum": 0,
      "description": "0-indexed pointer into ordered_approver_chain. Set to 0 on the Open->InReview transition. Incremented by each DecisionStepApproved event. Reset to 0 on a Reversed->Open re-open."
    },
    "ordered_approver_chain": {
      "type": "array",
      "items": {
        "type": "string",
        "format": "uuid"
      },
      "minItems": 1,
      "description": "Ordered list of approver stakeholder UUIDs. Length N = number of approval steps required to move Open -> ... -> Decided. Set at the Open -> InReview transition and immutable for the duration of that approval cycle; a Reversed->Open re-open MAY substitute a new chain."
    }
  },
  "x-multi-step-approval-encoding": "Two transitions share the trigger 'DecisionStepApproved'. Their guards are mutually exclusive on the pre-transition value of current_approval_step relative to len(ordered_approver_chain). The runtime emits 'DecisionStepApproved' for every approval; the FINAL step (the one that completes the chain) deterministically results in the InReview -> Decided transition rather than a self-loop. A simulator MUST evaluate guards against the Decision node's current_approval_step attribute BEFORE applying the increment side-effect.",
  "x-step-counter-side-effects": [
    {
      "transition": "Open -> InReview",
      "effect": "Set current_approval_step = 0. ordered_approver_chain MUST be non-empty when this transition fires; len(ordered_approver_chain) is N (the configured depth)."
    },
    {
      "transition": "InReview -> InReview (DecisionStepApproved)",
      "effect": "current_approval_step := current_approval_step + 1."
    },
    {
      "transition": "InReview -> Decided (DecisionStepApproved)",
      "effect": "current_approval_step := current_approval_step + 1 (now equals len(ordered_approver_chain))."
    },
    {
      "transition": "Reversed -> Open",
      "effect": "Reset current_approval_step = 0. ordered_approver_chain MAY be replaced (re-decision is a fresh approval cycle)."
    }
  ],
  "states": {
    "Open": {
      "description": "Decision has been opened. Rationale, options, RACI, and the ordered approver chain are being assembled. No approvals collected yet."
    },
    "InReview": {
      "description": "Approval chain in progress. The Decision carries current_approval_step (0-indexed) and ordered_approver_chain. Each DecisionStepApproved event advances the counter; the final step transitions to Decided."
    },
    "Decided": {
      "description": "All N approvers in ordered_approver_chain have approved. Decision is recorded but not yet locked; minor edits and Memory Event capture may still occur before lock."
    },
    "Locked": {
      "description": "Decision is locked as the system-of-record. Downstream dependencies treat the decision as committed. Exit only via Reversed (with reversal_reason memory event in same transaction) or Superseded (replaced by a new Decision)."
    },
    "Reversed": {
      "description": "A previously Locked decision has been reversed. Requires a same-transaction MemoryEventRecorded of type 'reversal_reason' (Scope v2 §1A.4.6, Build Plan §6.7.5). May be re-opened for a fresh approval cycle via the Reversed -> Open transition."
    },
    "Superseded": {
      "description": "Decision has been replaced by a different Decision (linked via a 'supersedes' edge from the new Decision to this one). Terminal: no further transitions in normal flow."
    }
  },
  "transitions": [
    {
      "from": "Open",
      "to": "InReview",
      "trigger_event": "DecisionInReview",
      "requires_role": null,
      "requires_events": [],
      "guard_description": "ordered_approver_chain MUST be set and non-empty at the moment of transition. current_approval_step is initialised to 0. Any user with edit access on the Decision may move it into review (no specific RACI role required), consistent with v1.2 §6.7."
    },
    {
      "from": "InReview",
      "to": "InReview",
      "trigger_event": "DecisionStepApproved",
      "requires_role": "approver",
      "requires_events": [],
      "guard_description": "Self-loop for non-final approval steps. Guard: current_approval_step < len(ordered_approver_chain) - 1 (evaluated PRE-increment). The actor MUST be the stakeholder at ordered_approver_chain[current_approval_step]; out-of-order approvals are rejected (Build Plan §6.7 'out-of-order rejected'). Side-effect: current_approval_step += 1."
    },
    {
      "from": "InReview",
      "to": "Decided",
      "trigger_event": "DecisionStepApproved",
      "requires_role": "approver",
      "requires_events": [],
      "guard_description": "Final approval step. Guard: current_approval_step == len(ordered_approver_chain) - 1 (evaluated PRE-increment, i.e. this approval IS the last one). The actor MUST be the stakeholder at ordered_approver_chain[current_approval_step]. Side-effect: current_approval_step += 1 (now equals len(ordered_approver_chain), signalling chain completion)."
    },
    {
      "from": "Decided",
      "to": "Locked",
      "trigger_event": "DecisionLocked",
      "requires_role": "decision_maker",
      "requires_events": [],
      "guard_description": "Decision-maker (the single accountable D in RACI) locks the decision, committing it as system-of-record. Locking prompts the Memory Event capture flow (Scope v2 §1A.6.2) but those captures are not gating conditions for the lock itself."
    },
    {
      "from": "Locked",
      "to": "Reversed",
      "trigger_event": "DecisionReversed",
      "requires_role": "approver",
      "requires_events": [
        {
          "event_type": "MemoryEventRecorded",
          "same_transaction": true,
          "filter": {
            "type": "reversal_reason"
          }
        }
      ],
      "guard_description": "Reversal of a locked decision is gated on (a) an approver acting (not merely the decision_maker) per Scope v2 §1A.4.6, and (b) a MemoryEventRecorded event of type 'reversal_reason' committed in the SAME database transaction as the DecisionReversed event (Build Plan §6.7.5, §6.9.7). The reversal flow UI is responsible for collecting and atomically writing both events; the runtime rejects a DecisionReversed event whose transaction does not also contain the qualifying memory event."
    },
    {
      "from": "Locked",
      "to": "Superseded",
      "trigger_event": "DecisionSuperseded",
      "requires_role": "decision_maker",
      "requires_events": [],
      "guard_description": "Marking a decision as Superseded requires a new Decision node to exist and a 'supersedes' edge from the new Decision to this one (cardinality/edge enforcement lives in edge-types.json, not here). Action is taken by the decision_maker of the superseding Decision in the typical case. No same-transaction Memory Event is mandated, but Programme Memory capture is strongly encouraged via UI."
    },
    {
      "from": "Reversed",
      "to": "Open",
      "trigger_event": "DecisionInReview",
      "requires_role": null,
      "requires_events": [],
      "guard_description": "Re-open for re-decision after reversal. Resets current_approval_step to 0; ordered_approver_chain MAY be re-specified for the fresh cycle. The same trigger event 'DecisionInReview' is reused as for Open->InReview; a simulator distinguishes the two paths by the from-state (Open vs Reversed)."
    }
  ],
  "x-decisions-surfaced": [
    "InReview -> Decided is reached on the FINAL DecisionStepApproved event (not a separate DecisionDecided event). Rationale: the approval chain's completion is itself the decision-being-made; introducing a second event would be redundant bookkeeping and would risk a window in which the chain is complete but the decision is not yet Decided. Implementations MAY emit a derived projection-level 'DecisionDecided' indicator for UI/notification purposes, but it is NOT an operational event in the events table.",
    "Reversed is NOT terminal: it has an outbound transition (Reversed -> Open) that begins a fresh approval cycle. Locked is terminal-ish in that no forward progress is possible, but it has two outbound transitions (to Reversed and to Superseded). The single state with no outbound transitions in normal flow is Superseded."
  ]
}