fix(archunit): EntityVersionParity checks member-level @Version, not class-level (#315) #12
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/entity-version-parity-field-level"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fix: EntityVersionParity must check member-level
@Version, not class-level (#315)The defect
EntityVersionParity.entitiesMustCarryVersion()used:But
jakarta.persistence.@Versionis aFIELD/METHOD-target annotation — it is never present on the class itself. Soshould().beAnnotatedWith(Version.class)matched no entity and the rule failed for every@Entityin every consumer service. user-service (PR #8) and calendar-service (PR #8) each had to ship a local correctedEntityVersionParityArchTestinstead of wiring this shared rule.The fix
Replace the class-level
should()with a customArchCondition<JavaClass>that inspects members:getAllFields()/getAllMethods()include inherited members, so JOINED /@MappedSuperclasshierarchies that carry the version on the root satisfy the rule for every subclass@Entity(the exact inheritance shape calendar-service navigated manually). Field-access and property-access (@Versiongetter) are both covered. The.that()filters (@Immutable,..audit..,..replica..),.because(...), andallowEmptyShould(true)are unchanged.Self-test (the v1.0 scaffold's deferred "PR-PLATFORM-5")
EntityVersionParitySelfTestevaluates the rule against explicitly-imported fixture packages:@MappedSuperclass, and accessor@Versionentities + an@Immutableappend-only entity (exclusion).@Entitywith no@Version.Each package is imported separately so the negative fixture never pollutes the positive evaluation. The positive case is the regression guard — it fails under the old class-level rule and passes under the fix.
Verification
mvn -B -pl archunit-rules test(JDK17) → BUILD SUCCESS, 2 tests, 0 warnings (parent pomfailOnWarning=true).Follow-up (separate)
Once this lands + the
1.1.0-SNAPSHOTis re-published to the Forgejo Maven registry, convert the localEntityVersionParityArchTestcopies in user-service + calendar (and use the shared rule in family/notification/admin) to the fixed shared rule.Show previous round
hib-pr-reviewer review — PR #12 (affinity-intelligence-rework/im2be-platform-libs)
Round 1 — head
19130e5fbfc0, basemain, triggeropenedTL;DR: CONDITIONAL_APPROVE — 0 agreed findings; verified and kept 2 minor (test fixture JPA false-negative, vacuous-pass guard) and 1 info (exclusion coverage gap) from unique-to-one reviewers.
Summary
Arbitration summary
This is the first run for PR #12. No prior Memora history. 0 agreed findings (A and B flagged entirely different concerns). All 3 findings are unique-to-one-reviewer and required verification.
A1 — KEPT (verified).
VersionedAccessorEntity.javaline 11 has@Idon theidfield; line 16 has@VersionongetVersion(). Under JPA §2.3.2, placing@Idon a field selects field-based access for the entity, which means method-level annotations are ignored at runtime. Confirmed by reading the file:@Idis on line 11,@Versionis on line 16, and no@Access(AccessType.PROPERTY)is present. The ArchUnit condition finds@Versionon the method via bytecode inspection, but JPA would silently skip it — a documented false-negative class. KEPT as minor.B1 — KEPT (verified).
EntityVersionParitySelfTest.javaline 34 callsnew ClassFileImporter().importPackages(POSITIVE)with no guard assertion before evaluation.EntityVersionParity.javaline 70 confirms.allowEmptyShould(true). If the fixture package fails to load (typo, classpath issue, compile failure), the rule evaluates zero entities, produces no violations, andassertThat(result.hasViolation()).isFalse()passes vacuously without testing anything. KEPT as minor.B2 — KEPT (verified, info). The test file has no fixture or assertion exercising the
..audit../..replica..package-exclusion paths. Confirmed by readingEntityVersionParitySelfTest.java— only@Immutableexclusion is covered. Reviewer correctly self-classifies as pre-existing, info-only, and deferred. KEPT as info.Outcome: 0 agreed, 3 unique-to-one verified and kept (2 minor, 1 info). No blocking issues.
Blast Radius
All changes are confined to a single ArchUnit rules module (
archunit-rules). The production code change introduces a private staticArchConditionfactory inEntityVersionParity.java; the public API signature (entitiesMustCarryVersion()) is unchanged. All other modified files are test fixtures with no cross-module surface.BLAST_SCORE: 2/10
CI status (head
19130e5fbfc0)Overall: ✓ success
2 checks: 2 pending
Findings (3)
[MINOR] Mixed JPA access mode:
@Idon field +@Versionon getter without@Access(PROPERTY)— getter annotation is silently ignored by JPAarchunit-rules/src/test/java/com/aim2be/platform/archunit/fixtures/positive/VersionedAccessorEntity.java:11
@Idis placed on theidfield (line 11), which selects field-based access for this entity per JPA §2.3.2. Under field-based access, only field-level annotations are considered persistent state; the@Versionannotation ongetVersion()(line 16) is silently ignored by Hibernate/EclipseLink at runtime. The ArchUnit condition correctly finds@Versionon the method via bytecode inspection — so the fixture passes the rule — but a production deployment of this entity would have no effective optimistic locking.This is the rule's principal false-negative class: the heuristic finds
@Versionon some method, but JPA may never honour it.Option A — keep property-access intent (add
@Accessand move@Id):Option B — annotate the field instead:
Option A is preferable if the test is meant to cover the
getAllMethods()path of theArchCondition.[MINOR]
passesForCompliantFixturescan pass vacuously when fixture classes fail to loadarchunit-rules/src/test/java/com/aim2be/platform/archunit/EntityVersionParitySelfTest.java:34
Line 34 calls
new ClassFileImporter().importPackages(POSITIVE)and then evaluates the rule. BecauseEntityVersionParity.entitiesMustCarryVersion()is built with.allowEmptyShould(true)(EntityVersionParity.java:70), a zero-class import — caused by a package-name typo, a compile failure that drops fixture classes from the output, or a classpath ordering issue — produces zero rule candidates, zero violations, andresult.hasViolation()returnsfalse. The assertion at lines 36–38 then passes without exercising any entity at all.Add a guard assertion immediately after line 34, before
evaluate:This pins the test to at least one known entity, making a vacuous pass impossible.
[INFO] No fixture or assertion for
..audit../..replica..package exclusionsarchunit-rules/src/test/java/com/aim2be/platform/archunit/EntityVersionParitySelfTest.java:29
The rule at EntityVersionParity.java:64 explicitly excludes two package patterns (
..audit..,..replica..). An unversioned@Entityin those packages should pass the rule, but no fixture class demonstrates this and no test asserts it. A future maintainer who inadvertently removes or narrows theresideOutsideOfPackagesclause would have no regression signal.This is a pre-existing omission predating this PR; deferring to a follow-on issue is reasonable. Low priority.
Verdict
CONDITIONAL_APPROVE
hib-pr-reviewer • round 1 • 3 findings (2m/1i) • 2026-05-29T09:00:43.064Z → 2026-05-29T09:02:43.335Z • posted-as: pr-reviewer-bot
#315 — R1 response
Project: affinity-intelligence-rework/im2be-platform-libs • PR #12 • R1 response (human-applied) • head
6634d56• 2026-05-29T09:05:46ZTL;DR: all 3 findings ADDRESSED (2 minor + 1 info).
mvn -B -pl archunit-rules testgreen (2 tests, 0 warnings). No defends.Summary
R1 was CONDITIONAL_APPROVE (0 agreed; 2 minor + 1 info kept). All three were legitimate test-quality findings and are fixed in
6634d56— the production rule (EntityVersionParity.java) is unchanged; only the self-test fixtures were hardened.Findings
VersionedAccessorEntityhad@Idon the field (→ field-access) +@Versionon the getter, which JPA silently ignores — a false-negative-shaped fixture. Rewrote it as coherent property-access (@Access(AccessType.PROPERTY),@Id+@Versionon getters) so thegetAllMethods()path is exercised by a JPA-valid entity.passesForCompliantFixturescould pass vacuously on an empty import (typo/compile/classpath) givenallowEmptyShould(true). AddedJavaClasses.contain(...)guards pinningVersionedFieldEntity(+ the audit/replica exclusion fixtures), and a symmetric guard on the negative test'sUnversionedEntity.@Entityfixtures infixtures/positive/audit+fixtures/positive/replica. The positive test imports the wholepositivepackage, so its no-violation assertion now exercises both..audit../..replica..exclusion branches — a regressedresideOutsideOfPackagesclause would make these unversioned entities violate.contain()guards confirm they're evaluated.Verdict
All findings resolved (no defends). Self-test now covers: field-access / inherited-
@MappedSuperclass/ property-access@Version, the@Immutableexclusion, and the..audit../..replica..package exclusions; plus negative + vacuous-pass guards. Requesting re-review.Footer
platform-libs • PR #12 • R1-response • fixed 3 / defended 0 • head
6634d56• 2026-05-29T09:05:46ZR1 CONDITIONAL_APPROVE (0 agreed; 2 minor + 1 info kept). Addressed all 3: (1) MINOR VersionedAccessorEntity had @Id on field (-> field-access) + @Version on the getter (silently ignored by JPA) — made it coherent property-access (@Access(PROPERTY), @Id + @Version on getters) so the getAllMethods() path is a JPA-valid fixture, not a false-negative. (2) MINOR passesForCompliantFixtures could pass vacuously on an empty import (+ allowEmptyShould(true)) — added JavaClasses.contain() guards pinning known fixtures (and the negative test's UnversionedEntity). (3) INFO no ..audit../..replica.. exclusion coverage — added unversioned @Entity fixtures in positive/audit + positive/replica subpackages; the positive test's no-violation now exercises both exclusion branches (a regressed exclusion would make them violate), with contain() guards confirming they are evaluated. Verification: - mvn -B -pl archunit-rules test (JDK17) -> BUILD SUCCESS, 2 tests, 0 warningsShow previous round
hib-pr-reviewer review — PR #12 (affinity-intelligence-rework/im2be-platform-libs)
Round 2 — head
6634d56ab4af, basemain, triggersynchronizeTL;DR: CONDITIONAL_APPROVE — 1 agreed minor finding kept (EntityVersionParitySelfTest.java:47 conjunctive guard); 3 prior round-1 findings confirmed resolved; 0 findings dropped.
Summary
Arbitration — Round 2
Memora recall: No prior run history found (both queries returned empty); this is the create-new fallback path. Memora write was attempted but blocked by permission constraints.
Reconciliation: Both A and B produced exactly 1 finding each, citing the same file (
EntityVersionParitySelfTest.java), the same line (47), and materially identical descriptions of the&&-inside-assertThatdiagnostic-quality issue. Per Rule 1 this is an agreed finding and is kept without further debate.Verification (Rule 1 / Rule 2):
Readof the file at HEAD confirms the exact code at line 47:The
&&is evaluated as a plain Java boolean beforeassertThatreceives it, so AssertJ can only reportexpected: true but was: false— it cannot identify which class was absent. The finding is real and the citations are accurate.Prior round-1 findings: All 3 were resolved per both reviewers (VersionedAccessorEntity mixed-access fix, vacuous-pass guard, audit/replica fixture assertions) — not re-raised.
Result: 1 agreed finding kept (minor), 0 dropped.
Blast Radius
The diff touches one ArchUnit rule method and a set of new test fixtures under
archunit-rules/src/test. No production code paths or exported API surfaces are modified. The blast radius is limited to the static-analysis build phase.BLAST_SCORE: 3/10
CI status (head
6634d56ab4af)Overall: ✓ success
2 checks: 2 pending
Findings (1)
[MINOR] Conjunctive
&&insideassertThathides which exclusion fixture failed to loadarchunit-rules/src/test/java/com/aim2be/platform/archunit/EntityVersionParitySelfTest.java:47
The
&&short-circuits and collapses the twobooleanresults into a single value beforeassertThatsees anything. When this guard fires in CI the failure message reads onlyexpected: <true> but was: <false>under the combined description — it does not identify which class was absent. Since the purpose of this guard is diagnostic (preventing a silent vacuous pass), losing per-class granularity defeats half its value.Replace with two independent assertions:
Each assertion now names the missing class in the failure message, making build-log triage immediate.
Verdict
CONDITIONAL_APPROVE
hib-pr-reviewer • round 2 • 1 finding (1m) • 2026-05-29T09:09:16.117Z → 2026-05-29T09:10:30.784Z • posted-as: pr-reviewer-bot
#315 — R2 response
Project: affinity-intelligence-rework/im2be-platform-libs • PR #12 • R2 response (human-applied) • head
81820d8• 2026-05-29T09:11:53ZTL;DR: the 1 R2 minor FIXED (split the conjunctive guard); R1's 3 findings confirmed resolved.
mvn -B -pl archunit-rules testgreen (2 tests, 0 warnings). No defends.Findings
EntityVersionParitySelfTest:47 — the&&collapsed bothcontain()checks into one boolean beforeassertThat, so a CI failure couldn't name which exclusion fixture was absent. Split into two independently-.as(...)-described assertions (AuditTrailEntity/ReplicaProjectionEntity) for immediate triage.Verdict
Resolved (no defends). The production rule is unchanged across the R-cycle; only the self-test hardened (R1: property-access fixture + vacuous-pass guards + exclusion coverage; R2: per-class guard granularity). Requesting re-review.
Footer
platform-libs • PR #12 • R2-response • fixed 1 / defended 0 • head
81820d8• 2026-05-29T09:11:53ZShow previous round
hib-pr-reviewer review — PR #12 (affinity-intelligence-rework/im2be-platform-libs)
Round 3 — head
81820d884a65, basemain, triggersynchronizeTL;DR: CONDITIONAL_APPROVE — kept 1 agreed finding (VersionedViaSuperclassEntity guard gap) plus 1 verified unique-to-A finding (ImmutableAppendOnlyEntity guard gap), combined into one minor finding; no blocking issues.
Summary
Reconciliation — Round 3
Memora: No prior run history found (despite round 3); created new entry (ID 330).
Verification performed:
EntityVersionParitySelfTest.javaat HEAD (75 lines). Guard block confirmed at lines 44–52: onlyVersionedFieldEntity,AuditTrailEntity, andReplicaProjectionEntityare asserted;VersionedViaSuperclassEntityandImmutableAppendOnlyEntityare absent from both the import list and the guard block.VersionedViaSuperclassEntity(inherits@VersionfromVersionedMappedSuperclass) andImmutableAppendOnlyEntity(@Immutableexclusion) both exist and are well-formed.Agreed finding kept (Rule 1): Both A and B independently flagged that
VersionedViaSuperclassEntity— the class exercising the very regression path this PR fixes — has no presence assertion in the guard block.Unique-to-A finding verified and kept (Rule 2): A also flagged
ImmutableAppendOnlyEntityhaving no guard assertion. Confirmed at HEAD: the class is not imported and not asserted. The@Immutable-exclusion path it exercises can silently disappear without any signal. Finding is valid.Combined into one finding at line 52 (end of guard block, the natural insertion point for both additions). Severity kept as minor — both reviewers classified them minor.
Prior-round closed items confirmed resolved: R1 mixed-access
@Version, R1 vacuous-pass risk, R1 audit/replica fixture gaps, R2 conjunctive&&masking individual exclusion failures — all confirmed absent from HEAD.Implementation correctness uncontested:
haveAFieldOrAccessorAnnotatedWithVersion()usinggetAllFields()/getAllMethods()is correct.Blast Radius
The diff is self-contained within the
archunit-rulesmodule: one production class (EntityVersionParity) gains a private condition method, and a new self-test with eight fixture classes is added. The rule applies transitively to every@Entityin any codebase that imports this library, but the change is additive — it only fixes a previously broken (always-violated) rule — so existing callers are unaffected at runtime. No cross-module dependencies are modified.BLAST_SCORE: 3/10
CI status (head
81820d884a65)Overall: ✓ success
2 checks: 2 pending
Findings (1)
[MINOR] Guard block missing presence assertions for
VersionedViaSuperclassEntity(agreed A+B) andImmutableAppendOnlyEntity(verified unique-to-A)archunit-rules/src/test/java/com/aim2be/platform/archunit/EntityVersionParitySelfTest.java:52
The
passesForCompliantFixturesguard (lines 44–52) asserts only thatVersionedFieldEntity,AuditTrailEntity, andReplicaProjectionEntitywere imported. Two fixture classes whose silent disappearance would leave key branches untested are unguarded:VersionedViaSuperclassEntity— the entity whose inherited@Version(viaVersionedMappedSuperclass) is the core regression target of this entire PR. If this class or its superclass fails to compile/land on the classpath, thegetAllFields()inherited-member code path goes silently unexercised while the test reports green (both reviewers independently agreed).ImmutableAppendOnlyEntity— the@Immutable-exclusion fixture. If it disappears, the@Immutableexclusion branch is never evaluated and the test still passes viaallowEmptyShould(true)semantics (verified unique-to-A, confirmed at HEAD).Add after line 52:
Also add the corresponding imports:
Verdict
CONDITIONAL_APPROVE
hib-pr-reviewer • round 3 • 1 finding (1m) • 2026-05-29T09:13:55.558Z → 2026-05-29T09:15:42.947Z • posted-as: pr-reviewer-bot
#315 — R3 response
Project: affinity-intelligence-rework/im2be-platform-libs • PR #12 • R3 response (human-applied) • head
c195022• 2026-05-29T09:17:53ZTL;DR: R3 minor FIXED comprehensively — added presence guards for ALL remaining positive fixtures (not just the 2 flagged), so every rule branch is provably exercised.
mvn -B -pl archunit-rules testgreen (2 tests, 0 warnings).Findings
VersionedViaSuperclassEntity(inherited@Version, the core regression target),VersionedAccessorEntity(property-access path),ImmutableAppendOnlyEntity(@Immutableexclusion) — i.e. the 2 flagged plus the accessor fixture (the would-be next gap). All six branches (field / inherited / accessor / @Immutable / ..audit.. / ..replica..) + the negative are now guard-confirmed, so none can silently go unexercised underallowEmptyShould.Verdict
Resolved. Production rule (
EntityVersionParity.java) unchanged across the entire R-cycle — every round refined only the self-test's non-vacuity guarantees (R1: property-access fixture + initial guards + exclusion coverage; R2: per-class guard granularity; R3: complete per-fixture guarding). The rule + its self-test are now complete. Requesting re-review.Footer
platform-libs • PR #12 • R3-response • fixed 1 (superset) / defended 0 • head
c195022• 2026-05-29T09:17:53ZShow previous round
hib-pr-reviewer review — PR #12 (affinity-intelligence-rework/im2be-platform-libs)
Round 4 — head
c195022e0832, basemain, triggersynchronizeTL;DR: CONDITIONAL_APPROVE — kept 1 verified minor unique-to-B finding (grammar error in violation message); all 5 carry-over findings from rounds 1–3 confirmed resolved by both reviewers.
Summary
Reconciliation Summary
Recalled Memora context: No prior run memories found for this PR or submodule tags — creating new entry (round 4 fallback path). Memora persistence failed due to server-side auto-tag validation error (
memora/issuesnot in allowlist); all other reconciliation steps completed.Actions taken:
EntityVersionParity.java):Readconfirmed line 99 readsentity.getName() + " is a @Entity but declares no @Version field or accessor". The article"a"before"@Entity"(vowel sound) is grammatically incorrect; it should be"an". Reviewer B is correct.EntityVersionParitySelfTest.java):Readconfirmed thefailsForUnversionedEntitytest asserts only.contains("no @Version field or accessor")— a suffix match that does not cover the prefix article, so the typo is not caught by any test.Result: Kept 1 verified unique-to-B minor finding; dropped 0.
Blast Radius
The diff touches a single ArchUnit rule module (
archunit-rules) and its companion test fixtures. The production change is confined to one private static method inEntityVersionParity; the test additions are all net-new fixture classes. No exported API surface or protocol contract is modified.BLAST_SCORE: 2/10
CI status (head
c195022e0832)Overall: ✓ success
2 checks: 2 pending
Findings (1)
[MINOR] Violation message uses "a @Entity" — should be "an @Entity"
archunit-rules/src/main/java/com/aim2be/platform/archunit/EntityVersionParity.java:99
Line 99 reads:
@Entitybegins with a vowel sound, so the indefinite article must bean. The existingfailsForUnversionedEntitytest (line 85) only asserts.contains("no @Version field or accessor")— a suffix match — so this grammatical error in the user-visible violation message is not caught by any test. Fix:Verdict
CONDITIONAL_APPROVE
hib-pr-reviewer • round 4 • 1 finding (1m) • 2026-05-29T09:20:39.682Z → 2026-05-29T09:21:54.018Z • posted-as: pr-reviewer-bot
hib-pr-reviewer review — PR #12 (affinity-intelligence-rework/im2be-platform-libs)
Round 5 — head
48e28f7b630e, basemain, triggersynchronizeTL;DR: NO_NEW_FINDINGS — No new findings this round.
Summary
[quorum-converged] A=0 = B=0. ## Round 5 review — all prior findings addressed
Verified against HEAD files at the checkout path. Every open finding from rounds 1–4 is resolved:
VersionedAccessorEntitymixed JPA access (field@Id+ getter@Versionwithout@Access(PROPERTY))@Access(AccessType.PROPERTY)and both@Id/@Versionare on getterspassesForCompliantFixturesvacuous passassertThat(positive.contain(...)).isTrue()guards, one per fixture classAuditTrailEntityandReplicaProjectionEntitypresent at lines 50–55&&hiding which guard failedassertThatcallsVersionedViaSuperclassEntityandImmutableAppendOnlyEntity"is an @Entity but declares no @Version field or accessor"The core fix (
haveAFieldOrAccessorAnnotatedWithVersion) correctly usesgetAllFields()andgetAllMethods()(both walk the full inheritance chain), so@MappedSuperclass-inherited private fields are visible to ArchUnit's bytecode reader and the rule correctly clearsVersionedViaSuperclassEntity. No new actionable issues found.CI status (head
48e28f7b630e)Overall: ✓ success
2 checks: 2 pending
Findings
No new findings this round.
Quorum converged on empty findings (A + B both returned 0).
Verdict
NO_NEW_FINDINGS
hib-pr-reviewer • round 5 • 0 findings • 2026-05-29T09:23:48.700Z → 2026-05-29T09:26:01.309Z • posted-as: pr-reviewer-bot • [bookkeeping fallback]