Files
correx/docs/modules/core/core-approvals.adoc
T
kami 9734eec63c 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.
2026-05-26 16:59:21 +04:00

124 lines
5.1 KiB
Plaintext

= 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.