docs: add module documentation in AsciiDoc with PlantUML diagrams

Generated by per-group subagents covering all 52 modules across:
core (18), infrastructure (10), apps (5), interfaces (3),
plugins (7), testing (9)

Documentation format:
- AsciiDoc (.adoc) files in docs/modules/<group>/
- PlantUML (.puml) files in docs/diagrams/
- .adoc files reference diagrams via include:: directives

Each doc covers: purpose, responsibilities, non-responsibilities,
key types, event flow, integration points, invariants, PlantUML
diagram, known issues, and open questions.
This commit is contained in:
2026-05-26 16:59:21 +04:00
parent eadf23c945
commit 9734eec63c
103 changed files with 6045 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
= core-agents
== purpose
This module has no source files. It exists as a buildable Gradle module with a `build.gradle` file but no Kotlin source code in the `src/main/kotlin` tree. It is a placeholder for future agent-related functionality.
== responsibilities
* None (no source files)
== non-responsibilities
* Everything (no source files)
== key types
No types exist in this module.
== event flow
This module does not interact with events.
== integration points
No dependencies (empty build.gradle with no `dependencies` block beyond the default plugins).
== invariants
n/a
== PlantUML diagram
[plantuml, core-agents, "png"]
----
include::../../diagrams/core-agents.puml[]
----
== known issues
* The module compiles but produces no classes. If it is referenced as a dependency by another module, that module will fail to resolve imports.
== open questions
* What functionality is this module intended to contain? (Agent definitions, agent lifecycle, agent orchestration?)
* Should it be removed or marked explicitly as pending?
+123
View File
@@ -0,0 +1,123 @@
= core-approvals
== purpose
Evaluates whether a proposed operation should proceed by checking active grants and session approval mode, then returning an approval decision (auto-approved or pending human review). Tracks requests, decisions, and grants as event-sourced state.
== responsibilities
* Approving or rejecting operations based on tier, grants, and approval mode
* Tracking pending approval requests and resolved decisions
* Recording active grants (time-bounded permissions scoped to session/stage/project)
* Rebuilding approval state from the event log
* Providing a no-op engine for deterministic replay
== non-responsibilities
* Does not enforce policy — policy is absolute upstream
* Does not emit events — all events are consumed from the event store
* Does not validate whether the operation itself is valid (delegated to validation)
* Does not determine execution tiers — tiers are declared by tools
== key types
=== ApprovalReducer
* **kind**: interface
* **purpose**: Transforms approval state given a stored event.
=== DefaultApprovalReducer
* **kind**: class
* **purpose**: Handles `ApprovalRequestedEvent`, `ApprovalDecisionResolvedEvent`, `ApprovalGrantCreatedEvent`, `ApprovalGrantExpiredEvent`.
=== ApprovalProjector
* **kind**: class
* **purpose**: Wraps `ApprovalReducer` as a `Projection<ApprovalState>`.
=== DefaultApprovalRepository
* **kind**: class
* **purpose**: Rebuilds `ApprovalState` for a session via `EventReplayer`.
=== ApprovalState
* **kind**: data class
* **purpose**: In-memory projection of all approval-state for a session.
* **fields**: `requests`, `decisions`, `grants`
=== DomainApprovalRequest
* **kind**: data class
* **purpose**: A request for approval of a tiered operation.
* **fields**: `id` (ApprovalRequestId), `tier`, `validationReportId`, `riskSummaryId`, `timestamp`, `causationId`, `correlationId`, `userSteering`, `toolName`, `preview`
=== ApprovalDecision
* **kind**: data class
* **purpose**: The outcome of an approval evaluation.
* **fields**: `outcome` (ApprovalOutcome), `state` (ApprovalStatus), `tier`, `contextSnapshot`, `reason`, `userSteering`
=== ApprovalGrant
* **kind**: data class
* **purpose**: A time-bounded permission that bypasses approval for matching operations.
* **fields**: `id` (GrantId), `scope` (GrantScope), `permittedTiers`, `reason`, `timestamp`, `expiresAt`
=== ApprovalContext
* **kind**: data class
* **purpose**: Snapshot of identity, mode, and causality chain at decision time.
* **fields**: `identity` (ApprovalScopeIdentity), `mode` (ApprovalMode), `causationId`, `correlationId`
=== ApprovalScopeIdentity
* **kind**: data class
* **purpose**: Identifies the scope (session, stage, project) a decision applies to.
=== ApprovalEngine
* **kind**: interface
* **purpose**: Evaluates a request against grants and approval mode to produce a decision.
=== DefaultApprovalEngine
* **kind**: class
* **purpose**: Implements the tier-based approval logic. Checks grants first, then falls through to mode-based thresholds. Returns `PENDING` for requests above the approval threshold.
=== NoOpApprovalEngine
* **kind**: class
* **purpose**: Always returns `AUTO_APPROVED` with no logic. Used during replay to avoid live approval evaluation.
== event flow
*inbound:*
* `ApprovalRequestedEvent` → creates a `DomainApprovalRequest` in state
* `ApprovalDecisionResolvedEvent` → creates an `ApprovalDecision` in state
* `ApprovalGrantCreatedEvent` → creates an `ApprovalGrant` in state
* `ApprovalGrantExpiredEvent` → removes the grant from state
*outbound:*
* None. This module is purely a projection — events are emitted by the orchestrator and consumed here.
== integration points
* `:core:events` — `StoredEvent`, event payload types, shared identity types (`SessionId`, `ApprovalRequestId`, etc.)
* `:core:sessions` — `Projection`, `EventReplayer`, `ApprovalMode`
== invariants
* `ApprovalDecision.isFinal` is true only when `state != PENDING`
* `ApprovalDecision.isApproved` is true only when outcome is `APPROVED` or `AUTO_APPROVED`
* A grant with `expiresAt <= now` is never matched by `DefaultApprovalEngine`
* `NoOpApprovalEngine` is used exclusively on the replay path — never during live execution
* Request, decision, and grant IDs are globally unique; maps in `ApprovalState` are indexed by these IDs
== PlantUML diagram
[plantuml, core-approvals, "png"]
----
include::../../diagrams/core-approvals.puml[]
----
== known issues
* `ApprovalDecision.id` is nullable in the data class but `DefaultApprovalReducer` always assigns a non-null ID; `NoOpApprovalEngine` returns `null` for the ID, which could make downstream consumers handle both cases unnecessarily.
* `ApprovalContext` construction in `DefaultApprovalReducer` hardcodes `ApprovalMode.PROMPT` and null stage/project IDs because the event does not carry this information. The full context is only available at evaluation time.
== open questions
* Should `ApprovalContext` be embedded in the event payload rather than reconstructed during reduction?
* The `RiskSummaryId` on `DomainApprovalRequest` references data from the validation module — the two modules are separate and share no common risk model type.
@@ -0,0 +1,60 @@
= core-artifacts-store
== purpose
Defines the interface for binary blob storage of artifact payloads. Provides content-addressable put/get semantics and a flush-before-commit pattern for transactional consistency. It is the storage backend for artifact content, separate from artifact lifecycle metadata.
== responsibilities
* Storing binary artifact payloads with content-addressable IDs
* Retrieving binary artifact payloads by ID
* Supporting a transactional flush pattern: `flushBefore(commit)` ensures writes are durable before the commit action executes
== non-responsibilities
* Does not manage artifact lifecycle or metadata — those are owned by `:core:artifacts`
* Does not validate artifact payloads — validation is handled by `:core:validation`
* Does not implement the storage — this is an interface; implementations live in `:infrastructure:*`
* Does not track artifact provenance, lineage, or relationships
== key types
=== ArtifactStore
* **kind**: interface
* **purpose**: Binary blob storage for artifact payloads.
* **fields**: none
* **methods**: `put(bytes): ArtifactId`, `get(id): ByteArray?`, `flushBefore(commit)`
== event flow
This module does not interact with events. It is a pure storage interface with no event-sourced state.
== integration points
* `:core:events` — `ArtifactId` (used as the content-addressed key)
== invariants
* `ArtifactStore.put()` returns the same `ArtifactId` for the same bytes (content-addressable)
* `ArtifactStore.flushBefore(commit)` guarantees that all prior `put()` calls are durable before `commit` executes
* No event is emitted for storage operations — storage is a pure side-effect layer
== PlantUML diagram
[plantuml, core-artifacts-store, "png"]
----
include::../../diagrams/core-artifacts-store.puml[]
----
== known issues
* Only one source file exists (`ArtifactStore.kt`). There are no implementations, no default behaviour, and no in-memory fallback.
== open questions
* Should there be a default in-memory implementation for testing?
* Is `flushBefore(commit)` sufficient for all transactional patterns, or is a full begin/commit/rollback API needed?
* The module name `artifacts-store` uses a different package (`com.correx.core.artifactstore`) without the plural `artifacts`. This is inconsistent with the module name.
+151
View File
@@ -0,0 +1,151 @@
= core-artifacts
== purpose
Defines the artifact model — a structured output produced during a workflow stage. Manages artifact lifecycle phases (created → validating → validated/rejected → superseded/archived) as event-sourced state, provides a kind system for typed artifact payloads (file_written, process_result), and supports materialization of file artifacts to disk.
== responsibilities
* Defining the `Artifact` sealed interface and its common fields (id, session, stage, lifecycle phase, lineage, metadata)
* Managing artifact lifecycle phase transitions with state machine validation
* Tracking artifact lineage (parent IDs, relationships, validation report IDs)
* Defining and registering `ArtifactKind` implementations for typed payloads
* Providing JSON schema derivation for each artifact kind
* Materializing file artifacts to disk via `MaterializingArtifactWriter`
* Providing a repository interface for querying artifact state by session, stage, or ID
== non-responsibilities
* Does not manage binary blob storage — that is delegated to `:core:artifacts-store` via `ArtifactStore`
* Does not validate artifact payloads — validation is handled by `:core:validation` via `ArtifactPayloadValidator`
* Does not produce artifacts — they are created by the orchestrator or infrastructure adapters
* Does not manage the full artifact content — only metadata and lifecycle state are projected
== key types
=== Artifact
* **kind**: sealed interface
* **purpose**: Base type for all artifacts. Declares common fields.
* **fields**: `id`, `sessionId`, `stageId`, `schemaVersion`, `createdAt`, `lifecyclePhase`, `lineage`, `metadata`
=== ArtifactState
* **kind**: data class
* **purpose**: Event-sourced projection of a single artifact's lifecycle state.
* **fields**: `phase` (ArtifactLifecyclePhase), `lineage` (ArtifactLineage)
=== ArtifactReducer / DefaultArtifactReducer
* **kind**: interface / class
* **purpose**: Transforms `ArtifactState` given a stored event. Validates lifecycle phase transitions and returns `Result.Failure` for illegal transitions. Handles `ArtifactCreatedEvent`, `ArtifactValidatingEvent`, `ArtifactValidatedEvent`, `ArtifactRejectedEvent`, `ArtifactSupersededEvent`, `ArtifactArchivedEvent`, `ArtifactRelationshipAddedEvent`.
=== ArtifactProjector
* **kind**: class
* **purpose**: Wraps `ArtifactReducer` as a `Projection<ArtifactState>`. Invalid lifecycle transitions are logged as warnings and do not crash replay.
=== ArtifactLineage
* **kind**: data class
* **purpose**: Tracks an artifact's provenance.
* **fields**: `parentIds`, `relationships` (list of ArtifactRelationship), `validationReportIds`
=== ArtifactRelationship
* **kind**: data class
* **purpose**: A directed, typed relationship between two artifacts.
* **fields**: `sourceId`, `targetId`, `type` (ArtifactRelationshipType)
=== ArtifactKind
* **kind**: interface
* **purpose**: Declares a type of artifact with a payload serializer and JSON schema.
* **fields**: `id`, `payloadSerializer`, `llmEmitted`
=== ArtifactKindRegistry / DefaultArtifactKindRegistry
* **kind**: interface / class
* **purpose**: Maps artifact kind IDs to `ArtifactKind` instances. Pre-registers `FileWrittenKind` and `ProcessResultKind`.
=== FileWrittenKind
* **kind**: object
* **purpose**: Artifact kind for files written to disk. Declares a JSON schema with `path`, `content`, `mode`.
=== FileWrittenPayload
* **kind**: data class
* **purpose**: Input payload for a file write operation (before materialization).
* **fields**: `path`, `content`, `mode`
=== FileWrittenArtifact
* **kind**: data class
* **purpose**: Canonical form of a written file after materialization (content-addressed by hash).
* **fields**: `path`, `contentHash`, `size`, `mode`
=== ProcessResultKind
* **kind**: object
* **purpose**: Artifact kind for process execution results. Declares a JSON schema with `command`, `exitCode`, `stdout`, `stderr`, `durationMs`.
=== ProcessResultArtifact
* **kind**: data class
* **purpose**: Record of a process execution.
* **fields**: `command`, `exitCode`, `stdout`, `stderr`, `durationMs`
=== TypedArtifactSlot
* **kind**: data class
* **purpose**: Associates a named artifact slot with its expected kind.
* **fields**: `name` (ArtifactId), `kind` (ArtifactKind)
=== MaterializingArtifactWriter
* **kind**: interface
* **purpose**: Writes a `FileWrittenPayload` to disk within a sandbox root and returns a `FileWrittenArtifact`. Supports content-addressed storage by returning a hash.
=== MaterializationResult
* **kind**: sealed interface
* **purpose**: Outcome of a materialization attempt.
* **variants**: `Success(artifact, resolvedPath)`, `Failure(reason)`
=== ArtifactRepository
* **kind**: interface
* **purpose**: Queries artifact state by session, stage, or ID.
=== JsonSchema / JsonSchemaProperty
* **kind**: data classes
* **purpose**: JSON Schema representation for artifact kind derived schemas.
== event flow
*inbound:*
* `ArtifactCreatedEvent` → sets phase to `CREATED`
* `ArtifactValidatingEvent` → transitions from `CREATED` to `VALIDATING` (fails if phase is not `CREATED`)
* `ArtifactValidatedEvent` → transitions from `VALIDATING` to `VALIDATED`
* `ArtifactRejectedEvent` → transitions from `VALIDATING` to `REJECTED`
* `ArtifactSupersededEvent` → transitions from `VALIDATED` to `SUPERSEDED`
* `ArtifactArchivedEvent` → transitions from `VALIDATED` or `REJECTED` to `ARCHIVED`
* `ArtifactRelationshipAddedEvent` → adds a relationship to the lineage
*outbound:*
* None. This module is purely a projection.
== integration points
* `:core:events` — `StoredEvent`, artifact lifecycle event types, `ArtifactId`, `ArtifactLifecyclePhase`, `ArtifactRelationshipType`
== invariants
* Lifecycle phase transitions follow a strict state machine: `CREATED → VALIDATING → {VALIDATED, REJECTED}`, then `VALIDATED → {SUPERSEDED, ARCHIVED}`, `REJECTED → ARCHIVED`
* Invalid transitions return `Result.Failure` from the reducer — they do not crash the projector
* `ArtifactProjector` catches `IllegalStateException` from invalid transitions, logs a warning, and leaves state unchanged during replay
* `ArtifactKind` registrations are idempotent — the registry is a map, so re-registering overwrites the previous entry
== PlantUML diagram
[plantuml, core-artifacts, "png"]
----
include::../../diagrams/core-artifacts.puml[]
----
== known issues
* `Artifact` sealed interface has no concrete subclasses in the current codebase. The `artifactModule` serializers module has a polymorphic block for `Artifact::class` with no subclass registrations. Concrete artifact types are needed before polymorphic serialization works.
* `ArtifactState` is a single-artifact projection — each `ArtifactState` tracks one artifact's lifecycle. The `ArtifactRepository` interface returns maps keyed by `ArtifactId`, which suggests the caller is responsible for aggregating multiple states.
== open questions
* How are multiple artifact states (one per artifact) stored and queried? The repository returns maps, but the event replayer typically rebuilds a single state per session. The bridge between them is not visible in this module.
* What concrete `Artifact` subclasses are planned? (The sealed interface exists but has no implementations.)
+89
View File
@@ -0,0 +1,89 @@
= core-config
== purpose
Loads and represents the Correx application configuration from a TOML file. Provides a single entry point (`ConfigLoader`) that reads `~/.config/correx/config.toml` (or a path from `CORREX_CONFIG`) and returns a `CorrexConfig` data class with validated defaults.
== responsibilities
* Read configuration file from well-known filesystem path
* Parse TOML-format configuration with minimal manual parser
* Provide type-safe config objects with sensible defaults for all subsystems
* Silently fall back to defaults when config file is missing or unparseable
== non-responsibilities
* Does not validate configuration values semantically (beyond type coercion)
* Does not watch for file changes at runtime
* Does not manage environment variables beyond `CORREX_CONFIG`
* Does not expose configuration to remote clients
== key types
=== CorrexConfig
* **kind**: data class
* **purpose**: Root configuration object, aggregating all subsystem configs.
* **fields**: `server`, `tui`, `cli`, `approval`, `tools`
=== ServerConfig
* **kind**: data class
* **purpose**: Ktor HTTP server binding configuration.
* **fields**: `host` (default `"localhost"`), `port` (default `8080`)
=== TuiConfig
* **kind**: data class
* **purpose**: Terminal UI preferences.
* **fields**: `theme` (default `"dark"`), `sessionListLimit` (default `5`)
=== CliConfig
* **kind**: data class
* **purpose**: CLI output formatting preferences.
* **fields**: `defaultOutput` (default `"human"`)
=== ApprovalConfig
* **kind**: data class
* **purpose**: Approval gate timeouts.
* **fields**: `timeoutMs` (default `300_000L`)
=== ToolsConfig
* **kind**: data class
* **purpose**: Tool sandbox and execution configuration.
* **fields**: `sandboxRoot`, `workingDir`, `shellAllowedExecutables`, `defaultSystemPromptPath`
=== ConfigLoader
* **kind**: object
* **purpose**: Stateless loader that reads and parses the TOML config file.
* **fields**: none (singleton)
== event flow
*None.* Config is read eagerly at startup and does not participate in event sourcing.
== integration points
* `core:events` — `@Serializable` annotations on all config types
* `apps:cli`, `apps:server`, `core:approvals`, `core:tools` — consume `CorrexConfig` fields
== invariants
* ConfigLoader must never throw on missing or unparseable files — always returns `CorrexConfig()` with defaults
* All config types are `@Serializable` for potential persistence
== PlantUML diagram
[plantuml, core-config, "png"]
----
include::../../diagrams/core-config.puml[]
----
== known issues
* TOML parser is hand-written and only supports sections with simple `key = value` pairs. Arrays, inline tables, and multi-line strings are not supported.
== open questions
* Will config be migrated to a proper TOML library (e.g., `ktoml`)?
* Should config support hot-reload or remain load-once?
+145
View File
@@ -0,0 +1,145 @@
= core-context
== purpose
Assembles the context pack (a structured, layered representation of session state) that is fed to an LLM at inference time. Defines the layer hierarchy (L0L4), budgeting and compression strategies for fitting context within token limits, and tracks context build lifecycle as event-sourced state.
== responsibilities
* Defining the context layer hierarchy (L0 live execution, L1 stage-local, L2 compressed session memory, L3 durable project memory, L4 archival history)
* Building `ContextPack` instances from raw entries using `ContextPackBuilder`
* Compressing context entries to fit within a `TokenBudget` using strategy-specific algorithms
* Selecting entries for inference (decision point) from the context pack
* Tracking which context packs have been built for a session
* Defining steering notes — user-provided guidance attached to a session
== non-responsibilities
* Does not produce the entries that go into the context — those are produced by other modules
* Does not interact with inference providers — the built `ContextPack` is consumed by `:core:inference`
* Does not define the content of individual layers — only the structure and assembly
* Does not manage cross-session or durable project memory directly — only models L3/L4 as layer slots
== key types
=== ContextPack
* **kind**: data class
* **purpose**: A complete, layered context ready for inference. Includes budget tracking and compression metadata.
* **fields**: `id`, `sessionId`, `stageId`, `layers` (map of layer to entries), `budgetUsed`, `budgetLimit`, `compressionMetadata`
=== ContextLayer
* **kind**: enum
* **purpose**: Hierarchy level for context entries, from most immediate (L0) to most archival (L4).
* **variants**: `L0` (live execution), `L1` (stage-local), `L2` (compressed session memory), `L3` (durable project memory), `L4` (archival history)
=== ContextEntry
* **kind**: data class
* **purpose**: A single piece of context with source tracking.
* **fields**: `id`, `layer`, `content`, `sourceType`, `sourceId`, `tokenEstimate`
=== TokenBudget
* **kind**: data class
* **purpose**: Tracks token limit and consumption.
* **fields**: `limit`, `used`
=== CompressionMetadata
* **kind**: data class
* **purpose**: Records what compression strategies were applied, which layers were truncated, and how many entries were dropped.
=== ContextState
* **kind**: data class
* **purpose**: Event-sourced projection of context build status.
* **fields**: `builtPackIds`, `buildingInProgress`, `interrupted`
=== ContextPackBuilder
* **kind**: interface
* **purpose**: Builds a `ContextPack` from a list of entries within a budget.
=== DefaultContextPackBuilder
* **kind**: class
* **purpose**: Partitions entries into pinned (never dropped: steering notes, event history) and compressible. Applies per-source-type compression strategies and assembles the final pack.
=== DecisionPointBuilder
* **kind**: interface
* **purpose**: Selects which layers to send to the LLM for inference.
=== DefaultDecisionPointBuilder
* **kind**: class
* **purpose**: Returns only L0 and L1 entries — these are the only layers suitable for direct inference.
=== ContextCompressor
* **kind**: interface
* **purpose**: Compresses a list of entries to fit within a budget using a given strategy.
=== DefaultContextCompressor
* **kind**: class
* **purpose**: Implements five strategies: SteeringNote (retain all), EventHistory (retain all), Artifact (keep latest per sourceId), ToolLog (deduplicate content), Conversation (keep last N entries). Applies layer-based eviction (L2 before L1).
=== CompressionStrategy
* **kind**: sealed interface
* **purpose**: Declares how entries of a given source type should be compressed.
* **variants**:
* `ToolLog` — deduplicate identical content, drop oldest when over budget
* `Conversation(keepLast)` — retain last N entries verbatim
* `Artifact` — keep most recent entry per sourceId
* `SteeringNote` — always retained verbatim, never dropped
* `EventHistory` — always retained, already compressed facts
=== SteeringNote
* **kind**: data class
* **purpose**: User-provided guidance for a session, optionally scoped to a stage.
=== ContextReducer / DefaultContextReducer
* **kind**: interface / class
* **purpose**: Transforms `ContextState` given stored events. Handles `ContextBuildingStartedEvent`, `ContextPackBuiltEvent`, `ContextBuildingFailedEvent`, `ContextBuildingInterruptedEvent`.
=== ContextProjector
* **kind**: class
* **purpose**: Wraps `ContextReducer` as a `Projection<ContextState>`.
=== DefaultContextRepository
* **kind**: class
* **purpose**: Rebuilds `ContextState` for a session via `EventReplayer`.
== event flow
*inbound:*
* `ContextBuildingStartedEvent` → sets `buildingInProgress = true`
* `ContextPackBuiltEvent` → appends pack ID, resets building flags
* `ContextBuildingFailedEvent` → resets building flags
* `ContextBuildingInterruptedEvent` → sets `interrupted = true`, resets building in progress
*outbound:*
* None. This module is purely a projection — events are emitted by the orchestrator.
== integration points
* `:core:events` — `StoredEvent`, context lifecycle event types, `ContextPackId`, `ContextEntryId`, `SessionId`, `StageId`
* `:core:artifacts` — (listed in build.gradle but not directly referenced in source files; likely for future artifact-based context entries)
* `:core:sessions` — `Projection`, `EventReplayer`
== invariants
* Steering notes (`sourceType == "steeringNote"`) and event history (`sourceType == "eventHistory"`) are never dropped during compression
* L0, L3, and L4 entries are never evicted by `DefaultContextCompressor` — only L2 (first), then L1
* `CompressionMetadata` is non-authoritative — it records what happened during compression but original events remain the source of truth
* `ContextPack` is built per-stage per-session; it is not persisted independently — it is disposable and rebuilt on demand
== PlantUML diagram
[plantuml, core-context, "png"]
----
include::../../diagrams/core-context.puml[]
----
== known issues
* Build.gradle lists `:core:artifacts` as a dependency, but no artifact types are directly imported in the main source. May be a forward-looking dependency or unused.
== open questions
* Should `ContextPackBuilder` and `DecisionPointBuilder` be merged? They are always used together in the context assembly flow.
* L3 (durable project memory) and L4 (archival history) are defined in the layer enum but no code populates them — are they placeholders for future epics?
+243
View File
@@ -0,0 +1,243 @@
= core-events
== purpose
Foundation layer that defines the event schema, serialization infrastructure, identity primitives, and projection framework used by every other module in the system. All state is rebuilt from events, and this module provides the tools to define, store, serialize, and replay them. It also houses shared vocabulary types (SessionId, StageId, etc.) and cross-cutting value types (Tier, ApprovalStatus, RiskLevel) that would cause circular dependencies if defined elsewhere.
== responsibilities
* Defines the `EventPayload` sealed interface — all event types in the system implement it
* Provides `NewEvent` and `StoredEvent` envelopes with metadata (eventId, sessionId, timestamp, causationId, correlationId)
* Declares all concrete event types: session events, stage events, orchestration events, inference events, tool events, artifact events, approval events, context events, risk events
* Defines the `EventStore` interface for append and read operations on the event log
* Implements `EventDispatcher` as a convenience wrapper over `EventStore.append()`
* Defines shared identity types (`SessionId`, `EventId`, `StageId`, `TransitionId`, `ArtifactId`, etc.) and the underlying `TypeId` value class
* Contains the polymorphic serialization registry (`Serialization.kt`) — every `EventPayload` subclass must be registered here
* Provides `EventSerializer` / `JsonEventSerializer` for encode/decode
* Defines the projection framework (`Projection<S>`, `StateBuilder<S>`, `DefaultStateBuilder`, `EventReplayer<S>`, `DefaultEventReplayer`)
* Shares cross-module value types: `ApprovalOutcome`, `ApprovalStatus`, `GrantScope`, `Tier`, `UserSteering`, `RiskLevel`, `RiskAction`, `RiskSignal`, `RiskSummary`, `ArtifactLifecyclePhase`, `ArtifactRelationshipType`, `RetryPolicy`, `TokenUsage`, `OrchestrationState`, `OrchestrationStatus`
== non-responsibilities
* Does not own any domain logic beyond event definition and serialization
* Does not implement the `EventStore` — that is in `infrastructure` modules (SQLite, etc.)
* Does not define reducers or projectors for domain state (those live in `core:sessions`, `core:kernel`, etc.)
* Does not enforce causal consistency or idempotency — those are the store's or kernel's responsibility
* Does not handle event routing to subscribers — `EventDispatcher` emits but does not fan out
== key types
=== EventPayload
* **kind**: sealed interface
* **purpose**: base type for all domain events. Every event in the system implements this.
=== NewEvent
* **kind**: data class
* **purpose**: event prior to persistence, carries metadata and payload
* **fields**: metadata (EventMetadata), payload (EventPayload)
=== StoredEvent
* **kind**: data class
* **purpose**: event after persistence, adds sequence numbers
* **fields**: metadata, sequence (global sequence), sessionSequence (per-session monotonic), payload
=== EventMetadata
* **kind**: data class
* **purpose**: structured metadata attached to every event
* **fields**: eventId, sessionId, timestamp, schemaVersion, causationId, correlationId
=== EventStore
* **kind**: interface
* **purpose**: append events, read by session sequence, subscribe to live events, enumerate all events
=== EventDispatcher
* **kind**: class
* **purpose**: convenience wrapper that builds EventMetadata and calls EventStore.append
=== TypeId
* **kind**: value class (inline)
* **purpose**: type-safe identifier wrapper around String. Used for all identity types.
* **invariants**: non-blank, no leading/trailing whitespace
=== Projection&lt;S&gt;
* **kind**: interface
* **purpose**: fold function over events: `initial()` returns base state, `apply(state, event)` returns next state
=== StateBuilder&lt;S&gt;
* **kind**: interface
* **purpose**: builds state from a list of StoredEvents
=== DefaultStateBuilder&lt;S&gt;
* **kind**: class
* **purpose**: folds events through a Projection to produce state
=== EventReplayer&lt;S&gt;
* **kind**: interface
* **purpose**: rebuilds state for a given sessionId from the event store
=== DefaultEventReplayer&lt;S&gt;
* **kind**: class
* **purpose**: reads events from EventStore and folds through DefaultStateBuilder
=== EventSerializer
* **kind**: interface
* **purpose**: serialize/deserialize EventPayload to/from String
=== JsonEventSerializer
* **kind**: class
* **purpose**: JSON implementation of EventSerializer using kotlinx.serialization with the polymorphic event module
=== Tier
* **kind**: enum
* **purpose**: five-level trust tier (T0 through T4) for tool execution and approval routing
* **variants**: T0 (level 0, most trusted), T1, T2, T3, T4 (level 4, least trusted)
=== ApprovalStatus
* **kind**: sealed interface
* **variants**: PENDING, COMPLETED, TIMED_OUT
=== ApprovalOutcome
* **kind**: enum
* **variants**: APPROVED, REJECTED, AUTO_APPROVED
=== GrantScope
* **kind**: sealed interface
* **variants**: SESSION (entire session), STAGE(StageId), PROJECT(ProjectId)
=== RiskLevel
* **kind**: enum
* **variants**: LOW, MEDIUM, HIGH, CRITICAL
=== RiskAction
* **kind**: enum
* **variants**: PROCEED, PROMPT_USER, BLOCK
=== RiskSignal
* **kind**: sealed class
* **purpose**: discriminated signals that feed into risk assessment
* **variants**: CycleWithoutExit(cycleId), RepeatedFailure(reason, count), ValidationErrors(errorCount), InferenceTimeout(elapsedMs)
=== OrchestrationState
* **kind**: data class
* **purpose**: projection state for workflow orchestration
* **fields**: workflowId, currentStageId, status, retryCount, pauseReason, pendingApproval, failureReason, retryPolicy
=== OrchestrationStatus
* **kind**: enum
* **variants**: IDLE, RUNNING, PAUSED, COMPLETED, FAILED, CANCELED
=== RetryPolicy
* **kind**: data class
* **purpose**: maxAttempts and backoffMs for execution retry
=== TokenUsage
* **kind**: data class
* **purpose**: promptTokens + completionTokens (totalTokens computed)
=== AnyMapSerializer
* **kind**: object (KSerializer)
* **purpose**: kotlinx.serialization serializer for `Map<String, Any>` used by ToolRequest and ToolReceipt
=== Concrete event types
| Event class | Purpose |
|---|---|
| SessionStartedEvent | Session lifecycle began |
| SessionPausedEvent | Session was paused |
| SessionResumedEvent | Session was resumed |
| SessionCompletedEvent | Session ended normally |
| SessionFailedEvent | Session ended with error |
| StageStartedEvent | Workflow stage execution began |
| StageCompletedEvent | Workflow stage completed successfully |
| StageFailedEvent | Workflow stage failed |
| TransitionExecutedEvent | Transition between stages occurred |
| WorkflowStartedEvent | Entire workflow began |
| WorkflowCompletedEvent | Entire workflow completed |
| WorkflowFailedEvent | Entire workflow failed |
| OrchestrationPausedEvent | Orchestration blocked (approval, user request) |
| OrchestrationResumedEvent | Orchestration unblocked |
| RetryAttemptedEvent | Stage execution retry occurred |
| InferenceStartedEvent | LLM inference began |
| InferenceCompletedEvent | LLM inference completed with response |
| InferenceFailedEvent | LLM inference failed |
| InferenceTimeoutEvent | LLM inference timed out |
| ModelLoadedEvent | Model loaded into memory |
| ModelUnloadedEvent | Model unloaded from memory |
| ToolInvocationRequestedEvent | Tool call was requested by LLM |
| ToolExecutionStartedEvent | Tool execution began |
| ToolExecutionCompletedEvent | Tool execution completed with receipt |
| ToolExecutionFailedEvent | Tool execution failed |
| ToolExecutionRejectedEvent | Tool execution rejected by policy/approval |
| ToolInvokedEvent | Legacy tool invocation event |
| ApprovalRequestedEvent | Approval was requested |
| ApprovalDecisionResolvedEvent | Approval decision was made |
| ApprovalGrantCreatedEvent | Approval grant was created |
| ApprovalGrantExpiredEvent | Approval grant expired |
| ContextBuildingStartedEvent | Context pack construction began |
| ContextPackBuiltEvent | Context pack was built |
| CompressionAppliedEvent | Context compression was applied |
| LayerTruncatedEvent | Context layer was truncated |
| ContextBuildingFailedEvent | Context pack construction failed |
| ContextBuildingInterruptedEvent | Context building interrupted during replay |
| ArtifactCreatedEvent | Artifact was created |
| ArtifactValidatingEvent | Artifact validation began |
| ArtifactValidatedEvent | Artifact validation passed |
| ArtifactRejectedEvent | Artifact validation failed |
| ArtifactSupersededEvent | Artifact was superseded by another |
| ArtifactArchivedEvent | Artifact was archived |
| ArtifactRelationshipAddedEvent | Relationship between artifacts was recorded |
| SteeringNoteAddedEvent | User steering note was added |
| RiskAssessedEvent | Risk assessment was performed |
== event flow
*inbound:*
Events enter this module via `EventDispatcher.emit()` or `EventStore.append()`. Callers construct a `NewEvent` (or provide an `EventPayload` which the dispatcher wraps) and the store persists it.
*outbound:*
Events leave this module through `EventStore.read()`, `readFrom()`, `allEvents()`, or `subscribe()`/`subscribeAll()`. The serialization layer (`JsonEventSerializer`) converts `EventPayload` to/from JSON strings for storage or transport.
This module does not produce or consume events at a domain level — it is the plumbing through which all other modules' events flow.
== integration points
* `:core:sessions` — uses `EventStore`, `EventReplayer`, `Projection`, `StoredEvent`, session identity types, and `DefaultStateBuilder`
* `:core:kernel` — uses `EventStore`, `EventDispatcher`, `EventMetadata`, all event types, identity types, `Projection`, `EventReplayer`
* `:core:transitions` — uses identity types (`SessionId`, `StageId`, `TransitionId`, `ArtifactId`), `StoredEvent`
* `:core:inference` — uses identity types, `TokenUsage`, `EventStore`
* `:core:approvals` — uses identity types, `ApprovalOutcome`, `ApprovalStatus`, `GrantScope`, `Tier`, `UserSteering`
* `:core:validation` — uses identity types
* `:core:tools` — uses identity types, `Tier`, `ToolReceipt`, `ToolRequest`
* `:core:context` — uses identity types (`ContextPackId`, `ContextEntryId`)
* `:core:risk` — uses `RiskLevel`, `RiskAction`, `RiskSignal`, `RiskSummary`
* `:core:artifacts` — uses identity types, `StoredEvent`
* all `:infrastructure:*` modules — implement `EventStore`, use `StoredEvent`, `NewEvent`, `EventSerializer`
== invariants
* Every `EventPayload` subclass must be registered in the polymorphic block in `Serialization.kt`. Missing registration = silent runtime deserialization failure.
* `TypeId` values must be non-blank with no leading/trailing whitespace.
* `StoredEvent.sequence` must be globally monotonic; `sessionSequence` must be monotonic per session.
* Event identity is by `eventId` — stores must enforce idempotency by `eventId`.
* The projection infrastructure must be deterministic: the same events fed in the same order must produce the same state.
== PlantUML diagram
[plantuml, core-events, "png"]
----
include::../../diagrams/core-events.puml[]
----
== known issues
* `ToolInvokedEvent` (legacy stub event) coexists with the newer `ToolInvocationRequestedEvent` / `ToolExecutionStartedEvent` / `ToolExecutionCompletedEvent` chain. Both are registered in the polymorphic block.
* `RiskSummaryId.kt` is an empty file — the type alias is defined in `IdentityTypes.kt` instead.
* `OrchestrationState` and `OrchestrationStatus` are duplicated across `core:events` and `core:kernel` as separate copies with different packages. The `core:events` copies are the canonical serialized form; the `core:kernel` copies are empty stubs.
== open questions
* Should `StoredEvent` sequence numbers be assigned by the store or generated by the caller? Current interface suggests the store assigns them.
* `Projection` and `EventReplayer` live in the `core:events` module under the `com.correx.core.sessions.projections` package — this is a naming mismatch that may cause confusion.
+185
View File
@@ -0,0 +1,185 @@
= core-inference
== purpose
Abstracts LLM inference behind a provider interface, routes requests to healthy providers based on required capabilities, tracks inference lifecycle (started → completed/failed/timed-out) as event-sourced state, and defines the data types used to communicate with inference providers (requests, responses, tool definitions, generation config).
== responsibilities
* Defining the `InferenceProvider` contract for LLM inference calls
* Routing inference requests to the most suitable healthy provider via `InferenceRouter`
* Caching provider health checks with configurable TTL
* Tracking inference records and their status transitions
* Defining shared inference types (`InferenceRequest`, `InferenceResponse`, `GenerationConfig`, `ToolDefinition`, etc.)
* Providing tokenizer abstraction for model-family-specific tokenization
* Defining model capabilities (`Coding`, `ToolCalling`, `Reasoning`, etc.) for provider selection
== non-responsibilities
* Does not implement concrete providers — those live in `:infrastructure:*`
* Does not manage context construction — uses `ContextPack` from `:core:context`
* Does not handle tool execution — only models tool call declarations for the LLM
* Does not emit events for model loading failures — those are synchronous exceptions
== key types
=== InferenceProvider
* **kind**: interface
* **purpose**: Contract for a single LLM provider. Can infer, health-check, and report capabilities.
=== ProviderRegistry
* **kind**: interface
* **purpose**: Manages the set of registered `InferenceProvider`s. Supports capability-based resolution.
=== InferenceRouter
* **kind**: interface
* **purpose**: Resolves a provider for a given stage and set of required capabilities. Performs live health checks before selection.
=== DefaultInferenceRouter
* **kind**: class
* **purpose**: Uses a `RoutingStrategy` to select from healthy candidates. Caches health results with configurable TTL. Re-checks health post-selection to close TOCTOU window.
=== RoutingStrategy
* **kind**: fun interface
* **purpose**: Pure selection function — chooses a provider from candidates given required capabilities. Throws `NoEligibleProviderException` if no candidate qualifies.
=== InferenceRequest
* **kind**: data class
* **purpose**: Complete input for an LLM inference call. Includes context pack, generation config, tool definitions, and response format.
* **fields**: `requestId`, `sessionId`, `stageId`, `contextPack`, `generationConfig`, `timeout`, `responseFormat`, `tools`
=== InferenceResponse
* **kind**: data class
* **purpose**: Output of an LLM inference call.
* **fields**: `requestId`, `text`, `finishReason`, `tokensUsed`, `latencyMs`, `toolCalls`
=== GenerationConfig
* **kind**: data class
* **purpose**: Parameters for LLM generation — all fields required for deterministic replay.
* **fields**: `temperature`, `topP`, `maxTokens`, `stopSequences`, `seed`
=== FinishReason
* **kind**: sealed class
* **purpose**: Why inference finished.
* **variants**: `Stop`, `Length`, `ToolCall`, `Timeout`, `Cancelled`, `Error(message)`
=== ToolCallRequest
* **kind**: data class
* **purpose**: A tool call proposed by the LLM.
* **fields**: `function` (ToolCallFunction with name and arguments)
=== ToolDefinition
* **kind**: data class
* **purpose**: A tool declaration sent to the LLM as part of an inference request.
* **fields**: `function` (ToolFunction with name, description, parameters)
=== ModelCapability
* **kind**: sealed class
* **purpose**: Broad capability category for provider selection.
* **variants**: `Coding`, `ToolCalling`, `Reasoning`, `Summarization`, `General`
=== CapabilityScore
* **kind**: data class
* **purpose**: Scores a provider on a single capability (0.01.0).
=== ProviderHealth
* **kind**: sealed class
* **purpose**: Health status of a provider.
* **variants**: `Healthy`, `Degraded(reason)`, `Unavailable(reason)`
=== InferenceState
* **kind**: data class
* **purpose**: Event-sourced projection of all inference records for a session.
* **fields**: `records` (list of `InferenceRecord`)
=== InferenceRecord
* **kind**: data class
* **purpose**: Immutable record of a single inference call.
* **fields**: `requestId`, `sessionId`, `stageId`, `providerId`, `status`, `tokensUsed`, `latencyMs`, `failureReason`
=== InferenceStatus
* **kind**: enum
* **variants**: `STARTED`, `COMPLETED`, `FAILED`, `TIMED_OUT`
=== InferenceReducer / DefaultInferenceReducer
* **kind**: interface / class
* **purpose**: Transforms `InferenceState` given stored events. Handles `InferenceStartedEvent`, `InferenceCompletedEvent`, `InferenceFailedEvent`, `InferenceTimeoutEvent`.
=== InferenceProjector
* **kind**: class
* **purpose**: Wraps `InferenceReducer` as a `Projection<InferenceState>`.
=== InferenceRepository
* **kind**: class
* **purpose**: Rebuilds `InferenceState` for a session via `EventReplayer`.
=== Tokenizer
* **kind**: interface
* **purpose**: Model-family-specific tokenization. Provider-owned — two providers for the same capability may not share a tokenizer.
=== InferenceCancellationToken
* **kind**: interface
* **purpose**: Cancellation contract for inference. Honour cancellation cooperatively at checkpoints.
=== ResponseFormat
* **kind**: sealed interface
* **purpose**: Expected response format from the provider.
* **variants**: `Text`, `Json(schema)`
=== InferenceTimeout
* **kind**: value class
* **purpose**: Records a deadline duration for timeout cancellation.
=== CancellationReason
* **kind**: sealed class
* **purpose**: Why inference was cancelled.
* **variants**: `UserRequested`, `StageTimeout`, `SessionCancelled`, `ProviderEvicted`
=== ModelLoadException
* **kind**: class
* **purpose**: Thrown when model loading fails. No event is emitted.
== event flow
*inbound:*
* `InferenceStartedEvent` → adds a new record with status `STARTED`
* `InferenceCompletedEvent` → sets status to `COMPLETED`, records tokens and latency
* `InferenceFailedEvent` → sets status to `FAILED`, records failure reason
* `InferenceTimeoutEvent` → sets status to `TIMED_OUT`, records timeout as latency
*outbound:*
* None. This module is purely a projection — events are emitted by the provider or harness.
== integration points
* `:core:events` — `StoredEvent`, inference lifecycle event types, `InferenceRequestId`, `ProviderId`, `SessionId`, `StageId`
* `:core:context` — `ContextPack` (included in `InferenceRequest`)
* `:core:artifacts` — `JsonSchema` (used in `ResponseFormat.Json`)
== invariants
* `GenerationConfig` has no implicit defaults — all relevant fields must be explicitly specified for deterministic replay
* `RoutingStrategy.select()` is a pure function with no I/O or side effects
* `DefaultInferenceRouter` never returns a provider whose health check returns `Unavailable`
* Provider IDs must be unique across the registry
* `InferenceCancellationToken` direction is one-way: a timeout can cancel the token, but cancelling the token must not retroactively signal a timeout
* `ModelLoadException` does not emit an event — model loading failure is synchronous and happens before any event would be written
* `FinishReason.Cancelled` and `InferenceStatus.TIMED_OUT` are distinct: cancellation can happen for reasons other than timeout
== PlantUML diagram
[plantuml, core-inference, "png"]
----
include::../../diagrams/core-inference.puml[]
----
== known issues
None.
== open questions
None.
+189
View File
@@ -0,0 +1,189 @@
= core-kernel
== purpose
The orchestration engine that binds all core modules together into a running workflow. It owns the execution loop: resolve the next transition, execute the stage (build context, run inference, dispatch tool calls, validate), handle approvals, manage retries, and persist results as events. This is where the deterministic core meets nondeterministic inputs — the kernel is the decision-maker that translates LLM proposals and tool results into recorded state changes.
== responsibilities
* Defines `SessionOrchestrator` (abstract class) — the core execution loop shared by live and replay modes
* Implements `DefaultSessionOrchestrator` (concrete) — the live execution orchestrator with real inference, validation, risk assessment, and approval gating
* Implements `ReplayOrchestrator` — environment-independent replay using recorded events, with configurable `ReplayStrategy` (Full, SkipInference, SkipValidation)
* Provides `ApprovalGateway` interface — entry point for external approval decisions (CLI, server)
* Defines `OrchestrationReducer` / `DefaultOrchestrationReducer` — reduces workflow lifecycle events to `OrchestrationState`
* Provides `OrchestrationProjector` and `OrchestrationRepository` — rebuild orchestration state from events
* Defines `OrchestratorRepositories` and `OrchestratorEngines` — dependency injection data classes wiring all required services
* Implements `RetryCoordinator` / `DefaultRetryCoordinator` — records `RetryAttemptedEvent` and applies backoff delay
* Provides `ReplayInferenceProvider` — fake inference provider for replay that reads recorded `InferenceCompletedEvent` artifacts
* Defines `WorkflowResult` sealed interface (Completed, Failed, Cancelled) and `ReplayStrategy` (Full, SkipInference, SkipValidation)
* Defines `OrchestrationConfig` — retry policy, replay strategy, stage timeout, sandbox root, default system prompt path
* Provides `ReplayArtifactMissingException` — thrown when replay cannot find a recorded inference artifact
== non-responsibilities
* Does not define workflow graphs, stage configs, or transition conditions — those are in `:core:transitions`
* Does not define event types — those are in `:core:events`
* Does not implement the event store, inference providers, or tool executors — those are in `:infrastructure`
* Does not define session lifecycle state — that is in `:core:sessions`
* Does not define approval domain models — those are in `:core:approvals`
* Does not know about specific LLM APIs, shell commands, or filesystem layout
== key types
=== SessionOrchestrator
* **kind**: abstract class
* **purpose**: base orchestrator with shared stage execution logic (context building, inference, tool dispatch, approval handling, validation)
* **key methods**: `run(sessionId, graph, config)`, `cancel(sessionId)`, `executeStage(...)`, `runInference(...)`, `resolveTransition(...)`, `emit(...)`
* **fields**: repositories (OrchestratorRepositories), engines (OrchestratorEngines), artifactStore, cancellations map, pendingApprovals map
=== DefaultSessionOrchestrator
* **kind**: class
* **purpose**: live orchestrator. Uses `tailrec` step loop: resolve transition → execute stage → repeat. Integrates real inference, validation pipeline, risk assessment, and approval gating.
=== ReplayOrchestrator
* **kind**: class
* **purpose**: deterministic replay orchestrator. Overrides `runInference` and `mapValidationOutcome` to use recorded data instead of live services. Uses `NoOpApprovalEngine` and `NoOpRiskAssessor`.
=== OrchestratorRepositories
* **kind**: data class
* **purpose**: DI holder for all repository dependencies: eventStore, inferenceRepository, orchestrationRepository, sessionRepository, artifactRepository, approvalRepository
=== OrchestratorEngines
* **kind**: data class
* **purpose**: DI holder for all engine/service dependencies: transitionResolver, contextPackBuilder, inferenceRouter, validationPipeline, approvalEngine, riskAssessor, promptResolver, toolExecutor, toolRegistry
=== ApprovalGateway
* **kind**: interface
* **purpose**: single method `submitApprovalDecision(requestId, decision)` — bridge between external approval UI and the orchestrator's pending approval futures
=== OrchestrationReducer / DefaultOrchestrationReducer
* **kind**: interface / class
* **purpose**: reduces `WorkflowStartedEvent`, `WorkflowCompletedEvent`, `WorkflowFailedEvent`, `OrchestrationPausedEvent`, `OrchestrationResumedEvent`, `RetryAttemptedEvent` to `OrchestrationState`
=== OrchestrationProjector
* **kind**: class
* **purpose**: wraps `OrchestrationReducer` as a `Projection<OrchestrationState>`
=== OrchestrationRepository
* **kind**: class
* **purpose**: wraps `EventReplayer<OrchestrationState>` — `getState(sessionId)` returns the rebuilt orchestration state
=== OrchestrationConfig
* **kind**: data class
* **purpose**: runtime configuration for an orchestration run
* **fields**: retryPolicy, replayStrategy, stageTimeoutMs, sandboxRoot, defaultSystemPromptPath
=== WorkflowResult
* **kind**: sealed interface
* **purpose**: top-level result of a workflow execution
* **variants**: Completed(sessionId, terminalStageId), Failed(sessionId, reason, retryExhausted), Cancelled(sessionId)
=== ReplayStrategy
* **kind**: sealed interface
* **purpose**: controls replay behavior
* **variants**: Full (re-execute everything), SkipInference (use recorded artifacts), SkipValidation (trust recorded outcomes)
=== StageOutcome
* **kind**: sealed interface
* **purpose**: stage execution outcome model (success, validation failure, inference failure, approval required, cancelled)
* **variants**: Success(artifact), ValidationFailure(report, retryable), InferenceFailure(reason, retryable), ApprovalRequired(request), Cancelled
=== RetryCoordinator
* **kind**: interface
* **purpose**: decides whether to retry a failed stage
* **method**: `shouldRetry(sessionId, stageId, currentAttempt, policy, failureReason): Boolean`
=== DefaultRetryCoordinator
* **kind**: class
* **purpose**: emits `RetryAttemptedEvent`, applies backoff delay via `kotlinx.coroutines.delay`
=== ReplayInferenceProvider
* **kind**: class
* **purpose**: fake `InferenceProvider` for replay — finds the first `InferenceCompletedEvent` for the given stage and returns an empty-text response with recorded token/latency data
=== ReplayArtifactMissingException
* **kind**: class
* **purpose**: thrown when replay cannot find a recorded inference artifact for a given stage
=== InferenceResult (internal)
* **kind**: sealed interface (internal to SessionOrchestrator)
* **purpose**: internal inference outcome type
* **variants**: Success(response), Failed(reason), Cancelled
== event flow
*inbound (via ApprovalGateway):*
* `ApprovalDecision` submitted externally → kernel resolves pending approval `CompletableDeferred`
*outbound (via SessionOrchestrator.emit):*
The kernel emits events for every significant action:
Session lifecycle:
* `WorkflowStartedEvent` — workflow begins
* `WorkflowCompletedEvent` — workflow ends normally
* `WorkflowFailedEvent` — workflow ends with failure or cancellation
Orchestration lifecycle:
* `OrchestrationPausedEvent` — paused for approval or user request
* `OrchestrationResumedEvent` — unblocked
Stage execution:
* `InferenceStartedEvent` — LLM inference began
* `InferenceCompletedEvent` — LLM inference completed
* `InferenceFailedEvent` — LLM inference failed
* `InferenceTimeoutEvent` — LLM inference timed out
Tool execution:
* `ToolInvocationRequestedEvent` — tool call was requested (may require approval)
* `ArtifactCreatedEvent` / `ArtifactValidatingEvent` / `ArtifactValidatedEvent` — tool-produced artifacts
Approvals:
* `ApprovalRequestedEvent` — approval requested for tool or validation
* `RiskAssessedEvent` — risk assessment result
* `ApprovalDecisionResolvedEvent` — decision recorded
* `OrchestrationResumedEvent` (after approval pass) — resumed
== integration points
* `:core:events` — `EventStore`, `EventMetadata`, `NewEvent`, `EventId`, all event types, identity types, `Projection`, `EventReplayer`
* `:core:sessions` — `DefaultSessionRepository`, `Session`
* `:core:transitions` — `TransitionResolver`, `WorkflowGraph`, `StageConfig`, `StageExecutionResult`, `EvaluationContext`, `PromptResolver`
* `:core:validation` — `ValidationPipeline`, `ValidationContext`, `ValidationOutcome`
* `:core:approvals` — `DomainApprovalRequest`, `ApprovalDecision`, `NoOpApprovalEngine`, `DefaultApprovalRepository`
* `:core:context` — `ContextPackBuilder`, `ContextPack`, `ContextEntry`, `ContextLayer`, `TokenBudget`
* `:core:inference` — `InferenceRouter`, `InferenceRequest`, `InferenceResponse`, `InferenceRepository`, `GenerationConfig`, `ResponseFormat`, `ToolCallRequest`, `ToolDefinition`, `ToolFunction`, `FinishReason`, `ProviderHealth`
* `:core:tools` — `ToolExecutor`, `ToolResult`, `ToolRegistry`
* `:core:artifacts` — `ArtifactState`, `TypedArtifactSlot`, `ArtifactRepository`
* `:core:artifacts-store` — `ArtifactStore`
* `:core:risk` — `RiskAssessor`, `RiskContext`
== invariants
* `DefaultSessionOrchestrator.run()` is the only path for live workflow execution — replay uses `ReplayOrchestrator.run()` instead.
* Cancellation is cooperative: the orchestrator checks `isCancelled()` at the start of each step and after inference.
* Approval decisions are synchronous: the orchestrator blocks on `CompletableDeferred.await()` until `submitApprovalDecision()` is called.
* Retry is event-driven: each retry attempt emits a `RetryAttemptedEvent` that feeds back into `OrchestrationState.retryCount`.
* The step loop is `tailrec` — no stack overflow from deep workflows.
* Replay never calls live services: `ReplayOrchestrator` replaces `approvalEngine` with `NoOpApprovalEngine`, `riskAssessor` with `NoOpRiskAssessor`, and overrides `runInference` and `mapValidationOutcome`.
== PlantUML diagram
[plantuml, core-kernel, "png"]
----
include::../../diagrams/core-kernel.puml[]
----
== known issues
* `OrchestrationState.kt` and `OrchestrationStatus.kt` in this module are empty — the actual types live in `:core:events` under `com.correx.core.events.orchestration`. The kernel's `DefaultOrchestrationReducer` imports from the events module's copies.
* `StageOutcome` appears to be defined but unused within the kernel — the orchestrator uses `StageExecutionResult` (from `:core:transitions`) instead.
* `RetryPolicy.kt` in `core/kernel/execution/` is an empty file — the actual `RetryPolicy` type is in `:core:events`.
* `SessionOrchestrator` is marked abstract but has no abstract methods — `run()` and `cancel()` are abstract but the class could reasonably be concrete. The abstraction exists solely to share logic between `DefaultSessionOrchestrator` and `ReplayOrchestrator`.
== open questions
* The relationship between `StageOutcome` (in kernel) and `StageExecutionResult` (in transitions) is unclear — they model the same concept with different shapes. One may be dead code.
* The `sandboxRoot` field in `OrchestrationConfig` is declared but not used anywhere in the kernel source.
+51
View File
@@ -0,0 +1,51 @@
= core-observability
== purpose
Placeholder module reserved for observability primitives (metrics, structured logging, tracing). Currently contains no source files — the module is a Gradle subproject with no implementation.
== responsibilities
* Intended: define observability contracts (metric types, span interfaces)
* Intended: provide base abstractions used by `infra:telemetry`
== non-responsibilities
* Does not implement telemetry infrastructure — that belongs to `infra:telemetry`
* Does not collect runtime metrics directly
* Does not manage log output destinations
== key types
*None.* The module has no source files.
== event flow
*None.* No events are consumed or emitted. This is a placeholder.
== integration points
No integration points. The module has no dependencies beyond `core:events`.
== invariants
None — module is empty.
== PlantUML diagram
[plantuml, core-observability, "png"]
----
include::../../diagrams/core-observability.puml[]
----
== known issues
Module is a placeholder — no observability contract exists.
== open questions
* Which metrics framework will be used (Micrometer, OpenTelemetry, custom)?
* Will observability be event-sourced or side-channel?
+51
View File
@@ -0,0 +1,51 @@
= core-policies
== purpose
Placeholder module reserved for the policy evaluation layer. Policy evaluation determines whether a proposed operation is permitted based on configured rules. Currently contains no source files — the module is a Gradle subproject with no implementation.
== responsibilities
* Intended: evaluate operational policies against proposed actions
* Intended: enforce policy denials as terminal failures for the operation path
== non-responsibilities
* Does not perform risk assessment — that belongs to `core:risk`
* Does not gate approvals — that belongs to `core:approvals`
* Does not define workflow transitions — that belongs to `core:transitions`
== key types
*None.* The module has no source files.
== event flow
*None.* No events are consumed or emitted. This is a placeholder.
== integration points
No integration points. The module has no dependencies beyond `core:events`.
== invariants
None — module is empty.
== PlantUML diagram
[plantuml, core-policies, "png"]
----
include::../../diagrams/core-policies.puml[]
----
== known issues
Module is a placeholder — no policy evaluation logic exists.
== open questions
* What policy rule format will be used (declarative, scriptable, etc.)?
* Will policies be loaded from config or from the event store?
+88
View File
@@ -0,0 +1,88 @@
= core-risk
== purpose
Assesses operational risk at decision points by evaluating validation reports, orchestration state, and inference history. Produces a `RiskSummary` that drives approval tier selection and determines whether an operation may proceed, requires user prompting, or must be blocked.
== responsibilities
* Evaluate validation errors, cycle detection, repeated failures, and inference timeouts
* Map risk signals to a risk level (LOW, MEDIUM, HIGH, CRITICAL)
* Derive a recommended action (PROCEED, PROMPT_USER, BLOCK) from the risk level
* Provide a no-op implementation for deterministic replay paths
== non-responsibilities
* Does not enforce approvals — that belongs to `core:approvals`
* Does not define policy rules — that belongs to `core:policies`
* Does not emit events directly (signals are consumed by the approval layer)
* Does not mutate state
== key types
=== RiskAssessor
* **kind**: interface
* **purpose**: Contract for evaluating a `RiskContext` and producing a `RiskSummary`.
=== DefaultRiskAssessor
* **kind**: class
* **purpose**: Production implementation that inspects validation errors, cycles, retry exhaustion, and inference timeouts from the context.
=== NoOpRiskAssessor
* **kind**: class
* **purpose**: Returns `RiskSummary(LOW, emptyList(), PROCEED)` unconditionally. Used during deterministic replay to avoid running live risk assessment.
=== RiskContext
* **kind**: data class
* **purpose**: Immutable snapshot of all signals available for risk assessment at a decision point.
* **fields**: `validationReport` (validation issues for the current stage), `orchestrationState` (retry count, failure reason, retry policy), `inferenceState` (inference history for timeout detection)
=== TierMapping
* **kind**: file-level extension functions
* **purpose**: Maps `RiskLevel` to `Tier` (LOW→T1, MEDIUM→T2, HIGH→T3, CRITICAL→T4) and maps individual `RiskSignal` instances to their `RiskLevel`.
* **variants**:
* `RiskLevel.toApprovalTier()` — maps to `core:approvals` Tier
* `RiskLevel.toRiskAction()` — maps to `RiskAction` (PROCEED, PROMPT_USER, BLOCK); marked `internal`
* `RiskSignal.toRiskLevel()` — per-signal severity (ValidationErrors→MEDIUM, CycleWithoutExit→MEDIUM, InferenceTimeout→MEDIUM, RepeatedFailure→HIGH); marked `internal`
== event flow
*Inbound:* None directly. The `RiskContext` is assembled from:
* `ValidationReport` (produced by `core:validation`)
* `OrchestrationState` (from `core:kernel`)
* `InferenceState` (from `core:inference`)
*Outbound:* None. `RiskSummary` is returned synchronously to the caller (approval or orchestration layer).
== integration points
* `core:events.risk` — `RiskSummary`, `RiskLevel`, `RiskSignal`, `RiskAction` (event types)
* `core:approvals` — `Tier` enum
* `core:validation.model` — `ValidationReport`, `ValidationSeverity`
* `core:inference` — `InferenceState`, `InferenceStatus`
* `core:kernel` — `OrchestrationState`
== invariants
* `NoOpRiskAssessor` must be used on replay paths — live assessment on replay would produce non-deterministic results
* `RiskContext.orchestrationState` is populated from config before the first assessment call, not from `StageOutcome` (which lives in `core:kernel` and cannot be imported due to dependency direction)
== PlantUML diagram
[plantuml, core-risk, "png"]
----
include::../../diagrams/core-risk.puml[]
----
== known issues
* Signal-to-level mapping is hardcoded in `TierMapping.kt` and not configurable
* No support for custom or external risk signals
== open questions
* Should risk signal mapping become configurable or rule-driven?
* Should risk assessment emit events for audit trail purposes?
+144
View File
@@ -0,0 +1,144 @@
= core-router
== purpose
Provides a chat/steering interface for users to interact with a running workflow. Maintains its own event-sourced state (workflow status, L2 stage memory, conversation history) and builds a tailored context pack for routing inferences that decide the next workflow action.
== responsibilities
* Accepting user input via chat or steering mode
* Maintaining conversation history per session (in-memory, scoped to facade lifecycle)
* Tracking workflow lifecycle (idle, running, paused, completed, failed) from orchestration events
* Recording L2 stage memory entries (summaries of completed/failed stages)
* Building a `ContextPack` for routing decisions that includes system prompt, workflow status, conversation turns, and stage summaries
* Routing user inputs as steering notes when in STEERING mode (emitting `SteeringNoteAddedEvent`)
* Dispatching inference requests to a provider for router responses
== non-responsibilities
* Does not orchestrate workflows — only tracks state and provides an interface
* Does not produce L2 summaries — they are generated from event metadata
* Does not manage the full context assembly pipeline — only builds the router-specific subset
* Does not persist conversation history — it is held in-memory in the facade
== key types
=== RouterFacade
* **kind**: interface
* **purpose**: Entry point for user input. Returns a `RouterResponse`.
=== DefaultRouterFacade
* **kind**: class
* **purpose**: Coordinates repository, context builder, inference router, and event store to process user input. Supports `CHAT` and `STEERING` modes.
=== RouterRepository
* **kind**: interface
* **purpose**: Rebuilds `RouterState` for a session.
=== DefaultRouterRepository
* **kind**: class
* **purpose**: Rebuilds `RouterState` via `EventReplayer`.
=== RouterContextBuilder
* **kind**: interface
* **purpose**: Builds a router-specific `ContextPack` from `RouterState` and a token budget.
=== DefaultRouterContextBuilder
* **kind**: class
* **purpose**: Assembles L0 entries (system prompt, workflow status) and L1 entries (conversation history, stage summaries). Uses token budget with oldest-first eviction.
=== RouterReducer / DefaultRouterReducer
* **kind**: interface / class
* **purpose**: Transforms `RouterState` given stored events. Handles `WorkflowStartedEvent`, `WorkflowCompletedEvent`, `WorkflowFailedEvent`, `OrchestrationPausedEvent`, `OrchestrationResumedEvent`, `StageCompletedEvent`, `StageFailedEvent`.
=== RouterProjector
* **kind**: class
* **purpose**: Wraps `RouterReducer` as a `Projection<RouterState>`.
=== RouterState
* **kind**: data class
* **purpose**: Event-sourced state for the router.
* **fields**: `sessionId`, `workflowStatus`, `currentStageId`, `l2Memory` (list of `RouterL2Entry`), `conversationHistory` (list of `RouterTurn`)
=== RouterConfig
* **kind**: data class
* **purpose**: Configuration for the router.
* **fields**: `conversationKeepLast` (default 6), `tokenBudget` (default 4096)
=== RouterResponse
* **kind**: data class
* **purpose**: Response from the router to the user.
* **fields**: `content`, `steeringEmitted`
=== WorkflowStatus
* **kind**: enum
* **variants**: `IDLE`, `RUNNING`, `PAUSED`, `COMPLETED`, `FAILED`
=== StageOutcomeKind
* **kind**: enum
* **variants**: `SUCCESS`, `FAILURE`, `CANCELLED`
=== RouterL2Entry
* **kind**: data class
* **purpose**: A summary of a completed/failed stage for L2 memory.
* **fields**: `stageId`, `summary`, `outcome`, `timestamp`
=== RouterTurn
* **kind**: data class
* **purpose**: A single turn in the conversation history.
* **fields**: `role` (TurnRole), `content`, `timestamp`
=== TurnRole
* **kind**: enum
* **variants**: `USER`, `ROUTER`
=== ChatMode
* **kind**: enum
* **variants**: `CHAT`, `STEERING`
== event flow
*inbound:*
* `WorkflowStartedEvent` → sets session ID, status to `RUNNING`, current stage
* `WorkflowCompletedEvent` → sets status to `COMPLETED`, clears current stage
* `WorkflowFailedEvent` → sets status to `FAILED`, clears current stage
* `OrchestrationPausedEvent` → sets status to `PAUSED`
* `OrchestrationResumedEvent` → sets status to `RUNNING`
* `StageCompletedEvent` → appends L2 entry with outcome SUCCESS
* `StageFailedEvent` → appends L2 entry with outcome FAILURE, clears current stage
*outbound:*
* `SteeringNoteAddedEvent` → emitted by `DefaultRouterFacade` when mode is `STEERING` and user submits input
== integration points
* `:core:events` — `StoredEvent`, `SteeringNoteAddedEvent`, orchestration lifecycle event types, `EventStore`, `EventMetadata`, `EventId`, `SessionId`, `StageId`
* `:core:context` — `ContextPack`, `ContextEntry`, `ContextLayer`, `CompressionMetadata`, `TokenBudget`, `ContextEntryId`, `ContextPackId`
* `:core:inference` — `InferenceRouter`, `InferenceRequest`, `InferenceProvider`, `GenerationConfig`, `ModelCapability`, `ResponseFormat`
* `:core:sessions` — `Projection`, `EventReplayer`
== invariants
* Conversation history is held in-memory in `DefaultRouterFacade` — it is not rebuilt from events. On restart, conversation history is empty.
* `RouterTurn` is separate from the event-sourced conversation log — the reducer does not track individual turns, only L2 stage entries.
* L2 entries are created by the reducer from `StageCompletedEvent` and `StageFailedEvent` metadata, not from actual LLM-generated summaries.
* `RouterContextBuilder.L0` entries (system prompt, workflow status) are never dropped. L1 and L2 entries are evicted oldest-first under budget pressure.
== PlantUML diagram
[plantuml, core-router, "png"]
----
include::../../diagrams/core-router.puml[]
----
== known issues
* Conversation history is held in a `ConcurrentHashMap` in `DefaultRouterFacade` and is not event-sourced. This means router conversation context is lost on process restart. The reducer tracks event-derived L2 entries but not individual conversation turns.
== open questions
* Should conversation turns be event-sourced so the full conversation survives restarts?
* The `RouterTurn` timestamp is generated by `Clock.System.now()` on the facade side — should it be derived from event metadata instead?
+116
View File
@@ -0,0 +1,116 @@
= core-sessions
== purpose
Defines the session lifecycle model: session creation, status transitions (created → active → paused/completed/failed), and the projection infrastructure for rebuilding session state from events. Sessions are the top-level unit of work in Correx — a single workflow execution that produces artifacts and consumes LLM inference.
== responsibilities
* Defines `SessionState` (status, timestamps, invalid transition count) and `SessionStatus` enum (CREATED, ACTIVE, PAUSED, COMPLETED, FAILED)
* Implements `SessionReducer` (interface) and `DefaultSessionReducer` (concrete) — translates session and stage lifecycle events into `SessionState` changes
* Provides `SessionProjector` — adapts `SessionReducer` into the `Projection<SessionState>` interface for use with the replay infrastructure
* Provides `DefaultSessionRepository` — rebuilds `Session` (sessionId + state) from events via `EventReplayer`
* Provides `SessionCounterProjection` / `SessionCounterState` — a simple projection that counts events per session
* Defines `TransitionResult` sealed interface — models whether a status transition was applied or rejected
* Defines `ApprovalMode` enum (DENY, PROMPT, AUTO, YOLO) — the approval policy mode for a session
== non-responsibilities
* Does not produce events — all events are emitted by the kernel (`core:kernel`)
* Does not manage workflow graphs, transitions, or stage execution
* Does not interact with inference, tools, or context
* Does not enforce session lifecycle invariants — it only computes derived state from events
* Does not handle session persistence or storage
== key types
=== SessionStatus
* **kind**: enum
* **purpose**: lifecycle status of a session
* **variants**: CREATED, ACTIVE, PAUSED, COMPLETED, FAILED
=== SessionState
* **kind**: data class
* **purpose**: derived projection state for a session
* **fields**: status (SessionStatus), createdAt (Instant?), updatedAt (Instant?), invalidTransitions (Int)
=== Session
* **kind**: data class
* **purpose**: aggregate holding a sessionId and its current derived state
=== SessionReducer
* **kind**: interface
* **purpose**: reduces a `StoredEvent` into a `SessionState` transition
=== DefaultSessionReducer
* **kind**: class
* **purpose**: concrete reducer — maps `SessionStartedEvent` → ACTIVE, `SessionPausedEvent` → PAUSED, `SessionCompletedEvent` → COMPLETED, `SessionFailedEvent`/`StageFailedEvent` → FAILED, stage progress events → ACTIVE
=== SessionProjector
* **kind**: class
* **purpose**: wraps `SessionReducer` as a `Projection<SessionState>` with initial state = CREATED
=== DefaultSessionRepository
* **kind**: class
* **purpose**: wraps `EventReplayer<SessionState>` to provide `getSession(sessionId)` and `rebuild(sessionId)` returning a `Session`
=== TransitionResult
* **kind**: sealed interface
* **purpose**: result of attempting a session status transition
* **variants**: Applied(newState), Rejected
=== ApprovalMode
* **kind**: enum
* **purpose**: approval policy mode for a session
* **variants**: DENY (block all), PROMPT (ask user), AUTO (auto-approve), YOLO (skip approval entirely)
=== SessionCounterProjection / SessionCounterState
* **kind**: class / data class
* **purpose**: trivial projection counting events per session. Likely a diagnostic or testing utility.
== event flow
*inbound (consumed by reducer):*
* `SessionStartedEvent` → status becomes ACTIVE, createdAt recorded
* `SessionPausedEvent` → status becomes PAUSED
* `SessionResumedEvent` → status becomes ACTIVE
* `SessionCompletedEvent` → status becomes COMPLETED
* `SessionFailedEvent` → status becomes FAILED
* `StageStartedEvent` → status becomes ACTIVE
* `StageCompletedEvent` → status becomes ACTIVE
* `StageFailedEvent` → status becomes FAILED
* `TransitionExecutedEvent` → status becomes ACTIVE
*outbound:*
This module does not emit events. All events are emitted by `core:kernel`.
== integration points
* `:core:events` — `StoredEvent`, `Projection`, `EventReplayer`, `DefaultStateBuilder`, session event types (`SessionStartedEvent`, etc.), stage event types (`StageStartedEvent`, `StageCompletedEvent`, `StageFailedEvent`, `TransitionExecutedEvent`), identity types
* `:core:kernel` — uses `DefaultSessionRepository`, `Session`
== invariants
* `SessionState` is always derived from events via replay. It is never persisted or mutated directly.
* `createdAt` is set once from the first event's timestamp and never changes.
* `invalidTransitions` field exists in `SessionState` but is never incremented by `DefaultSessionReducer` — it is always 0.
* `SessionFailedEvent` and `StageFailedEvent` both map to FAILED status — there is no distinction in the session state between session-level and stage-level failure.
== PlantUML diagram
[plantuml, core-sessions, "png"]
----
include::../../diagrams/core-sessions.puml[]
----
== known issues
* `SessionState.invalidTransitions` is declared but never written by `DefaultSessionReducer`. It is always 0. Either it is a placeholder for future use or dead code.
* `TransitionResult` is defined but unused within the module — no callers in the current codebase reference it.
== open questions
* None.
+41
View File
@@ -0,0 +1,41 @@
= core-stages
== purpose
Placeholder module. No source files exist in this module. The build.gradle declares kotlin and serialization plugins with no dependencies. According to the project architecture, this module is intended to own stage execution logic, but the implementation has not been started.
== responsibilities
* None — no code exists.
== non-responsibilities
* All stage execution responsibility currently lives in `:core:kernel` (SessionOrchestrator.executeStage) and `:core:transitions` (StageConfig, StageExecutor interface, StageExecutionResult).
== key types
n/a — no source files.
== event flow
n/a — no source files.
== integration points
n/a — no source files.
== invariants
n/a — no source files.
== PlantUML diagram
n/a — no source files.
== known issues
* Empty module with no source files. The `src/` directory and `bin/` directory exist but contain nothing. Any stage-specific logic that should live here is instead implemented inline in `SessionOrchestrator.executeStage()` in `:core:kernel`, creating a ~270-line method.
== open questions
* What is the intended scope of this module? Likely to host the `StageExecutor` implementations and stage-specific orchestration logic that would extract the stage execution loop from the monolithic `SessionOrchestrator` class.
+126
View File
@@ -0,0 +1,126 @@
= core-tools
== purpose
Defines the contract for executable tools in Correx (their name, description, parameter schema, tier, and required capabilities), tracks the lifecycle of each tool invocation (requested → started → completed/failed/rejected) as event-sourced state, and provides a registry for resolving tools by name.
== responsibilities
* Defining the `Tool` interface that all tool implementations must satisfy
* Tracking tool invocation records and their status transitions
* Rebuilding tool invocation state from the event log
* Providing a registry for resolving tools by name
* Defining execution capabilities (file read, file write, network access, shell exec, process spawn)
* Validating tool requests against a tool's declared schema
== non-responsibilities
* Does not execute tools — execution is delegated to infrastructure adapters via `ToolExecutor`
* Does not manage approval — tools declare a tier; approval is handled by `:core:approvals`
* Does not emit events — all events are consumed from the event store
* Does not define tool implementations — only contracts and state tracking
== key types
=== Tool
* **kind**: interface
* **purpose**: Contract for an executable tool. Declares name, description, JSON parameter schema, execution tier, and required capabilities.
=== FileAffectingTool
* **kind**: interface (extends `Tool`)
* **purpose**: A tool that modifies files on disk. Adds `affectedPaths(request)` to track which files are touched.
=== ToolExecutor
* **kind**: interface
* **purpose**: Executes a `ToolRequest` and returns a `ToolResult`. The actual execution is delegated to infrastructure adapters.
=== ToolResult
* **kind**: sealed interface
* **purpose**: Outcome of a tool execution.
* **variants**:
* `Success(invocationId, output, exitCode, metadata)` — completed successfully
* `Failure(invocationId, reason, recoverable)` — failed; `recoverable` flag signals retryability
=== ToolCapability
* **kind**: enum
* **purpose**: Declares what a tool can do.
* **variants**: `FILE_READ`, `FILE_WRITE`, `NETWORK_ACCESS`, `SHELL_EXEC`, `PROCESS_SPAWN`
=== ValidationResult
* **kind**: sealed interface
* **purpose**: Whether a `ToolRequest` is structurally valid against a tool's schema.
* **variants**: `Valid`, `Invalid(reason)`
=== ToolRegistry
* **kind**: interface
* **purpose**: Resolves a tool by name and lists all registered tools.
=== ToolState
* **kind**: data class
* **purpose**: Event-sourced projection of all tool invocations for a session.
* **fields**: `invocations` (list of `ToolInvocationRecord`)
=== ToolInvocationRecord
* **kind**: data class
* **purpose**: Immutable record of a single tool invocation, including request, status, and receipt.
* **fields**: `invocationId`, `toolName`, `tier`, `request`, `status`, `receipt`, `requestedAt`, `completedAt`
=== ToolInvocationStatus
* **kind**: enum
* **purpose**: Lifecycle status of a tool invocation.
* **variants**: `REQUESTED`, `STARTED`, `COMPLETED`, `FAILED`, `REJECTED`
=== ToolReducer / DefaultToolReducer
* **kind**: interface / class
* **purpose**: Transforms `ToolState` given a stored event. Handles `ToolInvocationRequestedEvent`, `ToolExecutionStartedEvent`, `ToolExecutionCompletedEvent`, `ToolExecutionFailedEvent`, `ToolExecutionRejectedEvent`.
=== ToolProjector
* **kind**: class
* **purpose**: Wraps `ToolReducer` as a `Projection<ToolState>`.
=== DefaultToolRepository
* **kind**: class
* **purpose**: Rebuilds `ToolState` for a session via `EventReplayer`.
== event flow
*inbound:*
* `ToolInvocationRequestedEvent` → adds a new record with status `REQUESTED`
* `ToolExecutionStartedEvent` → sets status to `STARTED`
* `ToolExecutionCompletedEvent` → sets status to `COMPLETED`, attaches receipt
* `ToolExecutionFailedEvent` → sets status to `FAILED`
* `ToolExecutionRejectedEvent` → sets status to `REJECTED`
*outbound:*
* None. This module is purely a projection — events are emitted by the orchestrator.
== integration points
* `:core:events` — `StoredEvent`, `ToolRequest`, `ToolReceipt`, invocation lifecycle event types, `ToolInvocationId`
* `:core:approvals` — `Tier` (tools declare their execution tier)
* `:core:sessions` — `Projection`, `EventReplayer`
== invariants
* Status transitions are append-only: records are never removed from `ToolState`
* A record transitions through statuses monotonically: `REQUESTED → STARTED → {COMPLETED, FAILED, REJECTED}`
* `ToolInvocationRecord.completedAt` is always null until the invocation reaches a terminal status
* Each `ToolInvocationId` must be unique across all sessions
== PlantUML diagram
[plantuml, core-tools, "png"]
----
include::../../diagrams/core-tools.puml[]
----
== known issues
None.
== open questions
None.
+194
View File
@@ -0,0 +1,194 @@
= 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.
+156
View File
@@ -0,0 +1,156 @@
= core-validation
== purpose
Runs a pipeline of validators against a `ValidationContext` (containing the workflow graph, detected cycles, cycle policies, and session state) to produce a `ValidationReport`. If structural errors are found, the pipeline rejects immediately as non-retryable. If issues warrant human review, it signals `NeedsApproval`.
== responsibilities
* Validating workflow graph structure (start node exists, no dangling transitions)
* Validating transition integrity (endpoint references, deterministic ordering)
* Validating session temporal and status consistency
* Running semantic rules (e.g. cycle policy binding checks)
* Validating artifact payloads against their declared schemas
* Producing a `ValidationOutcome` (Passed, Rejected, or NeedsApproval)
* Determining whether validation failures are retryable
* Triggering approval requests when validation risk exceeds thresholds
== non-responsibilities
* Does not enforce approvals or block execution — only signals the need for approval
* Does not modify state or emit events — it is a pure validation function
* Does not manage the lifecycle of validation reports
* Does not validate LLM outputs directly — validates artifacts and graph structures
== key types
=== Validator
* **kind**: fun interface
* **purpose**: Performs a single validation pass and returns a `ValidationSection`.
=== ValidationPipeline
* **kind**: class
* **purpose**: Runs a list of validators in sequence. Short-circuits on structural errors (non-retryable rejection). Optionally evaluates an `ApprovalTrigger` after all validators pass.
* **fields**: `validators` (list), `approvalTrigger` (nullable)
=== ValidationOutcome
* **kind**: sealed interface
* **purpose**: Result of a validation run.
* **variants**:
* `Passed(report)` — validation succeeded
* `Rejected(report, retryable)` — validation failed; `retryable` is set by the validator and must not be overridden
* `NeedsApproval(request)` — validation passed but human approval is required
=== ValidationReport
* **kind**: data class
* **purpose**: Collection of `ValidationSection`s produced by a pipeline run.
=== ValidationSection
* **kind**: data class
* **purpose**: Named group of issues from a single validator.
* **fields**: `name`, `issues`, `metadata`
=== ValidationIssue
* **kind**: data class
* **purpose**: A single finding with machine-readable code, human message, severity, and optional location.
=== ValidationSeverity
* **kind**: enum
* **variants**: `INFO`, `WARNING`, `ERROR`
=== ValidationLocation
* **kind**: sealed interface
* **purpose**: Points to where an issue was found.
* **variants**:
* `Graph(stageId, transitionId)` — issue in workflow graph
* `Session(sessionId)` — issue in session state
=== ValidationContext
* **kind**: data class
* **purpose**: Input to validators, containing the workflow graph, cycles, policies, and session state.
* **fields**: `graph` (WorkflowGraph), `detectedCycles`, `cyclePolicies`, `sessionState`
=== GraphValidator
* **kind**: class
* **purpose**: Checks start node existence, dangling transitions, and records detected cycles as INFO issues.
=== TransitionValidator
* **kind**: class
* **purpose**: Checks transition endpoint integrity and deterministic ordering per source node.
=== SessionValidator
* **kind**: class
* **purpose**: Checks temporal consistency, negative invalid transition count, and suspicious failure states.
=== SemanticValidator
* **kind**: class
* **purpose**: Runs a list of `SemanticRule` instances.
=== SemanticRule
* **kind**: interface
* **purpose**: Validates a single semantic property.
=== CyclePolicyBindingRule
* **kind**: class
* **purpose**: Warns when detected cycles lack a policy binding. Only active when `requirePolicyForCycles` is true.
=== ArtifactPayloadValidator
* **kind**: class
* **purpose**: Validates artifact payloads (`FileWrittenArtifact`, `ProcessResultArtifact`) against their schemas by reading from `ArtifactStore`.
=== ApprovalTrigger
* **kind**: class
* **purpose**: Evaluates a `ValidationReport` and returns an `ApprovalRequest` if error count > 0 or cycle policies are missing.
=== ApprovalRequest (validation)
* **kind**: data class
* **purpose**: Signals that an operation requires human approval.
* **fields**: `sessionId` (SessionId?, nullable — may be absent at early validation stage), `riskSummary` (ValidationRiskStats), `validationReport`
=== ValidationRiskStats
* **kind**: data class
* **purpose**: Summary of error and warning counts plus cycle policy status.
== event flow
*inbound:*
* None. Validation is invoked by the orchestrator before executing operations. It does not subscribe to events.
*outbound:*
* None. Validation is a pure function — it returns a sealed outcome, it does not emit events.
== integration points
* `:core:events` — shared identity types (SessionId, StageId, etc.)
* `:core:sessions` — `SessionState`, `SessionStatus`
* `:core:transitions` — `WorkflowGraph`, `DetectedCycle`, `CyclePolicyBinding`, `TransitionEdge`, `TransitionOrdering`, `CycleSignatureFactory`
* `:core:artifacts` — `FileWrittenArtifact`, `ProcessResultArtifact`, `TypedArtifactSlot`
* `:core:artifacts-store` — `ArtifactStore`
== invariants
* `ValidationOutcome.Rejected.retryable` is set by the validator and must not be overridden by the orchestrator
* Structural validation errors (ERROR severity in the graph) produce `retryable = false`
* Pipeline short-circuits on the first ERROR severity issue — remaining validators are skipped
* `ApprovalRequest` produced by `ApprovalTrigger` is implicitly `Tier.T2 (REVERSIBLE)` — validation affects workflow progression, not external state
* The `RiskSummary` and `ValidationRiskStats` types are duplicate definitions of the same data
== PlantUML diagram
[plantuml, core-validation, "png"]
----
include::../../diagrams/core-validation.puml[]
----
== known issues
* `RiskSummary` and `ValidationRiskStats` are separate data classes with identical fields — likely a leftover from refactoring.
* `ApprovalRequest` lives in the validation module but is consumed by the approvals module, which has its own `DomainApprovalRequest` type. These are two different representations of the same concept with no shared type.
== open questions
* Should the `ApprovalRequest` produced by validation share a common type with the approvals module instead of being a validation-local class?
* Should validation produce structured IDs for reports and risk summaries that are meaningful to the approvals module?