TramAI - governed AI workflows for Java and Kotlin

Adding an Approval State

There are two independent state machines. Decide first which one the new state belongs to — the changes are not comparable in size.

State machineShapeWhere
ApprovalStatus — the decision outcomePENDING → {APPROVED, DENIED, TIMED_OUT}; everything except PENDING is terminaltramai-core/src/main/kotlin/dev/tramai/core/approval/ApprovalStatus.kt:9-14
ApprovalContinuationStatus — the resume lifecyclePENDING, CLAIMED, COMPLETED, EXPIRED, CANCELLED_UNCERTAIN, CANCELLEDtramai-core/src/main/kotlin/dev/tramai/core/approval/ApprovalContinuationStatus.kt:3-10

Adding a decision outcome is a wide, compatibility-sensitive change across the gateway and gate. Adding a continuation-lifecycle state is narrower — and is what most real changes are.

The Extension Points

  • Transitions — ApprovalTransition (sealed Approve / Deny / Timeout, each with targetStatus()): tramai-core/.../approval/ApprovalTransition.kt:9-48.
  • Persistence — ApprovalStore (create must produce a PENDING v0 record; transition(approvalId, expectedVersion, transition) is optimistic-concurrency; consumeApprovedOrReplay distinguishes strict-fresh from exact-replay) and ApprovalContinuationStore (claimForExecution is the only path exposing raw arguments).
  • Lifecycle oracles — ApprovalLifecycleModel.kt and ApprovalContinuationLifecycleModel.kt in tramai-testing. These are deliberately independent of production code: they are the specification, and the code must match them.
  • Audit SPI — ApprovalLifecycleAuditEmitter.kt in tramai-core. Never emit raw arguments or tokens.

Files That Change

LayerFile
Enumtramai-core/.../approval/ApprovalContinuationStatus.kt (or ApprovalStatus.kt)
DTO serializationtramai-persistence-file/.../PersistedDtos.kt
Store implementationsInMemoryApprovalContinuationStore.kt (tramai-security), FileApprovalContinuationStore.kt (file), JdbcApprovalContinuationStore.kt (jdbc)
CoordinatorsApprovalResumeCoordinator.kt, ContinuationClaimService.kt (validateBindings requires status == PENDING), ApprovalSuspensionCoordinator.kt (creates a hardcoded PENDING)
GateDefaultApprovalGateCoordinator.kt (authorization requires consumed.status == APPROVED)
Gateway mappingDefaultApprovalGateway.kt (toGatewayResult — an unmapped status silently falls into Suspended)
Audit emittertramai-security/.../audit/AuditEngineApprovalLifecycleAuditEmitter.kt (fixed enforcement point and decision strings per callback)
Lifecycle oraclesApprovalLifecycleModel.kt, ApprovalContinuationLifecycleModel.kt
TCK invariant tablesApprovalStoreTck.kt, ApprovalContinuationStoreTck.kt
Action generatorsApprovalLifecycleActionGenerator.kt, ApprovalContinuationLifecycleActionGenerator.kt

All four implementation layers plus the DTOs must change together. A state added to one store but not the others is a partial implementation that the TCK will reject.

Mandatory Contract Tests

OracleWhere
ApprovalStoreTcktramai-testing/src/testFixtures/kotlin/dev/tramai/testing/persistence/approval/ApprovalStoreTck.kt — transition matrix, consumption and exact-replay semantics, a model-based property check, a wrong-version matrix, a race schedule
ApprovalContinuationStoreTck.../approval/continuation/ApprovalContinuationStoreTck.kt — claim, exactly-once argument release, expiry, cancel, complete, recovery, sweep, races, model property
Lifecycle oracles.../approval/ApprovalLifecycleModel.kt, .../approval/continuation/ApprovalContinuationLifecycleModel.kt
Action generatorsApprovalLifecycleActionGenerator.kt, ApprovalContinuationLifecycleActionGenerator.kt plus their unit tests
Enrolment gatesApprovalStoreTckEnrollmentArchitectureTest.kt, ApprovalContinuationStoreTckEnrollmentArchitectureTest.kt in tramai-testing/src/test/kotlin/dev/tramai/testing/

Concrete runners exist for the in-memory, file, and JDBC implementations; the enrolment gates require them to stay named per implementation.

Invariants A New State Must Not Break

  • Continuation version ceiling is ≤ 2 per continuation, with field shape varying per status. A new state that increments the version breaks existing stored records.
  • Exactly-once argument release. Raw arguments are exposed once, via claim.
  • Rejected actions do not mutate durable state. Late claim and cancel are the exception: they do mutate (PENDING → EXPIRED) before raising the typed failure.
  • ApprovalSuspensionCoordinator creates continuations hardcoded PENDING, and ContinuationClaimService.validateBindings requires PENDING. A pre-claim state breaks that path until the check is deliberately relaxed.
  • Expiry semantics stay as-is unless the new state explicitly needs new rules: decision-legal while now < expiresAt, timeout-legal while now >= expiresAt and still PENDING. Changing that is a contract change, not a state addition.
  • Outcome mapping whens must be explicit. DefaultApprovalGateway.toGatewayResult and the gate coordinator's APPROVED checks treat unknown statuses as suspended or denied — a new state needs an explicit branch or authorization silently fails.
  • Audit event strings are a compatibility surface. External tooling consumes the fixed enforcement point and decision strings in AuditEngineApprovalLifecycleAuditEmitter.kt.
  • Store methods rethrow CancellationException unchanged.
  • Replay-envelope security is untouched by a state addition: ReplayEnvelopeFactory and ReplayEnvelopeValidator stay as they are.

Verification

./gradlew verifyPr
./gradlew verifyChangePolicy -PchangeClass=runtime-behaviour
./gradlew :tramai-security:test :tramai-persistence-jdbc:test \
  :tramai-persistence-file:test :tramai-testing:test :tramai-engine:test --tests '*Tck*'

What Not To Change In This Pull Request

  • The TCKs. They are oracles, not documentation; they fail on drift by design.
  • The lifecycle oracles. If the code disagrees, the code is wrong — do not edit the model to match.
  • config/quality/0.6.0-baseline.json. Adding an enum constant can shift scanner cardinality; if verifyChangePolicy flags it, stop and report rather than editing the baseline in the same pull request.

Common Mistakes

SymptomCause
TCK fails on one backend onlyA state added to one store implementation and not the other three, or missing DTO serialization
Stored records fail TCK assertionsThe continuation version ceiling was exceeded
New status silently becomes SuspendedNo explicit branch in toGatewayResult
Authorization always deniesThe gate's APPROVED check was not extended for the new state
Lifecycle model tests fail while implementation tests passThe oracle was edited instead of the code