= core-transitions

== purpose

Defines the workflow graph model and transition resolution machinery. A workflow is a directed graph of stages with conditional edges; this module provides the types to construct, validate, and navigate that graph. It also handles cycle detection, policy binding for cycles, and mapping stage execution results to events. This module is the "navigation system" — it decides where to go next, not how to execute the work.

== responsibilities

* Defines `WorkflowGraph` — a directed graph of `StageConfig` nodes connected by `TransitionEdge` edges
* Defines `StageConfig` — configuration for a single stage: required model capabilities, token budget, generation config, allowed tools, produced artifact slots, needed dependencies
* Defines `TransitionCondition` — a functional interface that evaluates whether a transition should fire, given an `EvaluationContext`
* Provides built-in conditions: `AlwaysTrue`, `VariableEquals`, `ArtifactPresent`, `ArtifactAbsent`, `ArtifactValidated`, `AllOf`, `AnyOf`, `Not`
* Implements `TransitionResolver` / `DefaultTransitionResolver` — evaluates outgoing edges from the current stage and returns a `TransitionDecision` (Move, Stay, Blocked, NoMatch)
* Defines `StageExecutor` interface and `StageExecutionRequest`/`StageExecutionResult` types — abstraction for executing a single stage
* Defines `EvaluationContext` — carries sessionId, currentStageId, artifact states, and variables for condition evaluation
* Implements deterministic cycle detection: `CycleExtractor`, `DeterministicAdjacencyBuilder`, `CycleDfs`, `CycleCanonicalizer`
* Defines cycle policy types: `CyclePolicy` (Retry, Refinement, Approval), `CyclePolicyBinding`, `CycleSignature`, `CycleSignatureFactory`
* Provides `CyclePolicyResolver` — looks up policy bindings for detected cycles
* Provides `PolicyValidation` — validates that all known cycles have policy bindings
* Provides `StageExecutionEventMapper` / `DefaultStageExecutionEventMapper` — converts stage execution results into `TransitionExecutedEvent`, `StageCompletedEvent`, and `StageFailedEvent`
* Provides `PromptResolver` — a functional interface for loading prompt templates by path

== non-responsibilities

* Does not execute stages — stage execution is delegated to `StageExecutor` implementations (in `core:kernel` or infrastructure)
* Does not interact with the event store directly — it defines types used by the kernel
* Does not own session lifecycle or orchestration loop
* Does not define the inference, context, or tool execution infrastructure
* Does not enforce cycle policies at runtime — policy enforcement is the kernel's responsibility

== key types

=== WorkflowGraph
* **kind**: data class
* **purpose**: a complete workflow definition as a directed graph of stages
* **fields**: id, stages (Map<StageId, StageConfig>), transitions (Set<TransitionEdge>), start (StageId)
* **invariants**: id is non-blank; start stage must exist in stages

=== StageConfig
* **kind**: data class
* **purpose**: configuration for a single workflow stage
* **fields**: requiredCapabilities (Set<ModelCapability>), tokenBudget, generationConfig (temperature, topP, maxTokens), allowedTools, maxRetries, produces (List<TypedArtifactSlot>), needs (Set<ArtifactId>), metadata (Map<String, String>)

=== TransitionEdge
* **kind**: data class
* **purpose**: a directed edge between two stages with a condition
* **fields**: id (TransitionId), from (StageId), to (StageId), condition (TransitionCondition)

=== TransitionCondition
* **kind**: fun interface
* **purpose**: evaluates to true/false given an `EvaluationContext`
* **method**: `evaluate(context: EvaluationContext): Boolean`

=== TransitionResolver
* **kind**: interface
* **purpose**: given a WorkflowGraph and EvaluationContext, produces a TransitionDecision

=== DefaultTransitionResolver
* **kind**: class
* **purpose**: iterates outgoing edges from the current stage in deterministic order, evaluates each condition, returns the first matching Move or Stay/NoMatch

=== TransitionDecision
* **kind**: sealed interface
* **purpose**: the result of resolving which transition to take
* **variants**:
  * `Move(transitionId, to)` — follow this edge to the target stage
  * `Stay` — no condition matched, remain at current stage
  * `Blocked(reason)` — a condition explicitly blocked the transition
  * `NoMatch` — no outgoing edges exist from the current stage

=== EvaluationContext
* **kind**: data class
* **purpose**: inputs for condition evaluation
* **fields**: sessionId, currentStage (StageId), artifacts (Map<ArtifactId, ArtifactState>), variables (Map<String, String>)

=== StageExecutor
* **kind**: interface
* **purpose**: executes a single stage and returns success or failure

=== StageExecutionRequest
* **kind**: data class
* **purpose**: parameters for executing a stage
* **fields**: sessionId, from (StageId), to (StageId), transitionId, context (EvaluationContext)

=== StageExecutionResult
* **kind**: sealed interface
* **purpose**: outcome of stage execution
* **variants**: Success(producedArtifacts), Failure(reason, retryable)

=== TransitionConditionEvaluator
* **kind**: fun interface
* **purpose**: evaluates a TransitionCondition against an EvaluationContext

=== StageExecutionEventMapper
* **kind**: interface
* **purpose**: converts StageExecutionRequest + StageExecutionResult into a list of EventPayloads

=== DefaultStageExecutionEventMapper
* **kind**: class
* **purpose**: produces TransitionExecutedEvent + StageCompletedEvent (on success) or TransitionExecutedEvent + StageFailedEvent (on failure)

=== CyclePolicy
* **kind**: sealed interface
* **purpose**: what to do when a cycle is detected
* **variants**: Retry(maxAttempts), Refinement(maxIterations), Approval(timeoutMs)

=== CycleSignature
* **kind**: data class
* **purpose**: canonical identity for a cycle — sorted set of nodes and edges
* **fields**: nodes (SortedSet<StageId>), edges (SortedSet<Pair<StageId, StageId>>)

=== CyclePolicyBinding
* **kind**: data class
* **purpose**: binds a CycleSignature to a CyclePolicy

=== CyclePolicyResolver
* **kind**: class
* **purpose**: looks up the policy for a given cycle signature from a set of bindings

=== PolicyValidation
* **kind**: class
* **purpose**: validates that all known cycles have policy bindings, returns list of errors

=== CycleExtractor
* **kind**: class (internal)
* **purpose**: runs DFS on the workflow graph to detect cycles

=== CycleCanonicalizer
* **kind**: object (internal)
* **purpose**: normalizes detected cycles by rotating to the minimum node and deduplicating

=== PromptResolver
* **kind**: fun interface
* **purpose**: resolves a prompt path string to its content

=== Built-in conditions

| Type | evaluates to true when |
|---|---|
| `AlwaysTrue` | unconditional |
| `VariableEquals(key, value)` | `context.variables[key] == value` |
| `ArtifactPresent(artifactId)` | artifact exists in `context.artifacts` |
| `ArtifactAbsent(artifactId)` | artifact absent from `context.artifacts` |
| `ArtifactValidated(artifactId)` | artifact phase == VALIDATED |
| `AllOf(conditions)` | all sub-conditions evaluate to true |
| `AnyOf(conditions)` | any sub-condition evaluates to true |
| `Not(condition)` | sub-condition evaluates to false |

== event flow

*inbound:*
This module does not consume events directly. The kernel provides event data to the resolver via `EvaluationContext`.

*outbound:*
This module does not emit events. It defines `StageExecutionEventMapper` which the kernel uses to produce:
* `TransitionExecutedEvent` — always emitted after a transition resolves
* `StageCompletedEvent` — on execution success
* `StageFailedEvent` — on execution failure

== integration points

* `:core:events` — identity types (`SessionId`, `StageId`, `TransitionId`, `ArtifactId`), event types (`TransitionExecutedEvent`, `StageCompletedEvent`, `StageFailedEvent`), `SessionId`
* `:core:inference` — `GenerationConfig`, `ModelCapability` (used in `StageConfig`)
* `:core:artifacts` — `TypedArtifactSlot` (used in `StageConfig.produces`), `ArtifactState` (used in `EvaluationContext`)
* `:core:kernel` — implements `StageExecutor`, uses `TransitionResolver`, `WorkflowGraph`, `StageExecutionResult`, `EvaluationContext`, `PromptResolver`, `StageExecutionEventMapper`

== invariants

* `WorkflowGraph.start` must be a key in `WorkflowGraph.stages` (enforced by require).
* `WorkflowGraph.id` must be non-blank.
* Transition edges from a given stage are evaluated in a deterministic order (sorted by from, id, to).
* Cycle detection is deterministic: DFS traversal order is fixed by sorted adjacency list.
* Detected cycles are canonicalized: rotated to the minimum node and deduplicated.
* `StageConfig.maxRetries` is a configuration value only — actual retry logic is in the kernel.

== PlantUML diagram

[plantuml, core-transitions, "png"]
----
include::../../diagrams/core-transitions.puml[]
----




== known issues

* `StageConfig` imports `TypedArtifactSlot` from `:core:artifacts` and `GenerationConfig`/`ModelCapability` from `:core:inference`, creating cross-core dependencies. The build.gradle explicitly declares these as project dependencies, violating the "no cross-core" convention noted in CLAUDE.md.
* `GraphOrdering.sortedStages()` and `sortedTransitions()` are marked `internal` but appear unused within the module — the ordering logic is duplicated in `DeterministicAdjacencyBuilder` and `TransitionOrdering`.

== open questions

* Should cycle policies be enforced at the transitions level (before execution) or at the orchestration level (during execution)? Currently, the module provides the policy model but enforcement is in the kernel's retry coordinator.
* The relationship between `StageConfig.maxRetries` (in this module) and `RetryPolicy.maxAttempts` (in `:core:events`) is unclear — they may be redundant.
