Introduce tryAlter for preventive state validation (PR-B1)#1643
Merged
Conversation
Implement PR-B1 of the de-event-sourcing plan: * Add the `tryAlter` extension for `TransactionalEntity` — validate-before-apply on a scratch copy of the live builder, returning `List<ConstraintViolation>`. Setter-thrown `ValidationException`s (e.g. `(set_once)`) are folded into the returned list; a failed attempt never dirties the live builder. * Fix the two decisions deferred to PR-B1 in ADR D9: merge mechanics (`clear()` + `mergeFrom`, the `Transaction.initAll` pattern) and Java-caller access (the `TransactionalEntityExtensions` facade; a same-name `protected` member would shadow the extension in Kotlin overload resolution). * Cover the ADR-listed cases with Kotlin tests, including dispatch-level proof via a real `ProcessManagerRepository` that a withheld change is not stored, and a Java-bridge spec pinning the facade usage. * Add the `StockKeeper` fixture context mirroring the ADR D9 samples. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a validate-before-apply state mutation helper (tryAlter) for TransactionalEntity, enabling receptors (immediately: ProcessManagers; later: aggregates) to probe and apply state changes only when they pass the same constraint-validation path used at transaction commit.
Changes:
- Introduced
tryAlter { ... }extension that mutates a scratch builder, validates the candidate state, and merges into the live builder only when clean; folds setter-thrownValidationExceptions into returned violations. - Added Kotlin + Java test coverage for the new API, including integration-style dispatch tests via a real
ProcessManagerRepository, plus dedicated test fixtures/protos. - Bumped project/dependency versions and refreshed ADR + dependency reports accordingly.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| version.gradle.kts | Bumps published snapshot version to 2.0.0-SNAPSHOT.401. |
| server/src/main/kotlin/io/spine/server/entity/TransactionalEntityExts.kt | Adds tryAlter extension with scratch-mutate + validate + merge semantics. |
| server/src/test/kotlin/io/spine/server/entity/TransactionalEntityExtensionsSpec.kt | Adds Kotlin tests covering tryAlter behavior and repository dispatch persistence outcomes. |
| server/src/test/java/io/spine/server/entity/TransactionalEntityExtensionsJavaSpec.java | Adds Java-facing tests verifying tryAlter is usable via the TransactionalEntityExtensions facade. |
| server/src/testFixtures/kotlin/io/spine/server/entity/given/tryalter/StockContext.kt | Introduces StockKeeper/repo fixture demonstrating preventive validation via tryAlter in a reactor. |
| server/src/testFixtures/proto/spine/test/tryalter/stock.proto | Adds constrained Stock state proto used in fixtures/tests (min, set_once). |
| server/src/testFixtures/proto/spine/test/tryalter/events.proto | Adds event protos for the tryAlter fixture scenario. |
| server/src/testFixtures/proto/spine/test/tryalter/commands.proto | Adds command proto for the tryAlter fixture scenario. |
| docs/dependencies/pom.xml | Updates documented artifact/dependency versions (incl. validation runtime to .447). |
| docs/dependencies/dependencies.md | Regenerates dependency/license report for the bumped snapshot version. |
| docs/adr/0001-aggregates-without-event-sourcing.md | Records PR-B1 implementation notes for ADR D9 (merge mechanics + Java access decision). |
| buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt | Bumps validation dependency version constant to 2.0.0-SNAPSHOT.447. |
| .agents/tasks/de-event-sourcing-plan.md | Updates the delivery plan document content/formatting in line with the ADR decisions. |
armiol
approved these changes
Jul 6, 2026
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1643 +/- ##
==========================================
+ Coverage 88.32% 88.35% +0.03%
==========================================
Files 1082 1083 +1
Lines 23620 23648 +28
Branches 1080 1082 +2
==========================================
+ Hits 20863 20895 +32
+ Misses 2365 2360 -5
- Partials 392 393 +1 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements PR-B1 of the de-event-sourcing delivery plan per ADR 0001, D9 — the additive, non-breaking slice of Phase B.
ProcessManagers benefit immediately; aggregates adopt the same idiom with the PR-B2 cutover.What changed
New
tryAlterextension forTransactionalEntity(TransactionalEntityExts.kt), next toupdate {}/alter {}— validate-before-apply:clone()) of the live builder;checkEntityState— the same path the transaction commit uses, so a clean probe verdict cannot be contradicted at commit;clear()+mergeFrom, theTransaction.initAllpattern); otherwise returns the violations and leaves the live builder untouched.Setter-thrown
io.spine.validation.ValidationExceptions —(set_once)today — are folded into the returned list, so setter-time and build-time constraints surface uniformly. Any other exception propagates unchanged.The two decisions ADR D9 deferred to PR-B1, now recorded in the ADR implementation notes:
clear()+mergeFrom(candidate)— no newTransactionmutator needed.TransactionalEntityExtensionsfacade. A same-nameprotectedmember takingConsumer<B>would shadow the extension in Kotlin subclasses (a member beats an extension in overload resolution), breaking thetryAlter { … }receiver-lambda idiom at every Kotlin call site.Tests (12, all green):
TransactionalEntityExtensionsSpec.kt— the ADR-listed cases: clean apply, violation return withfield_path,(set_once)folding, consecutive-call composition, exception propagation, and no-trace-after-commit (changed() == false— the exact store-gate predicate). Plus two dispatch-level tests through a realProcessManagerRepository: a clean call is stored; a withheld change leaves the process manager unstored, with persisted state and version unchanged.TransactionalEntityExtensionsJavaSpec.java— Java-bridge suite pinning the facade usage (tryAlter(entity, b -> { …; return Unit.INSTANCE; })).StockKeeperprocess manager +spine/test/tryalterprotos mirroring the ADR D9 samples (in_stockwith(min).value = "0",(set_once)vendor), so the tests double as documentation of the receptor idioms.Validation dependency consumed at
2.0.0-SNAPSHOT.447(bumped on this branch), carryingValidatingBuilder.validate()from Addvalidate()probe toValidatingBuildervalidation#316 (the D9 amendment companion). Verified present in the artifact; linked from thetryAlterKDoc.tryAlteritself does not depend on it.Reviewer notes
tryAlterreaches theprotectedbuilder()/checkEntityStatevia same-package access, exactly like the existingupdate/alter(hence non-inline, per the API note).StockKeepertoTransactionalEntityto reach package-private members — javac does not inherit package-private members across packages (Kotlin resolves them by declaring-class package)../gradlew clean build --no-build-cache+./gradlew build dokkaGenerate— green; reviewersspine-code-review,kotlin-engineer,review-docs,dependency-audit— all approve.2.0.0-SNAPSHOT.401) and regenerated dependency reports are separate commits, per convention.🤖 Generated with Claude Code