feat(dedup): PR-PLATFORM-2 — compound-PK processed_kafka_events impl #2

Merged
hibryda merged 11 commits from feat/pr-platform-2-processed-events into main 2026-05-27 02:25:08 +02:00
Owner

Summary

Replaces v1.0 scaffold stubs in processed-kafka-events/ with the production
implementation specified in ADR-0014
§D-4 (compound-PK dedup contract) + §L-7 / §L-8 (pg_partman weekly RANGE +
90-day retention) + §L-10 (week_start derived from event timestamp) +
§L-12 (per-pod broadcast dedup via scope suffix).

Public API

Symbol Purpose
ProcessedKafkaEvent JPA @Entity, @EmbeddedId composite PK, @Immutable (append-only — no @Version required).
ProcessedKafkaEventId @Embeddable Java 17 record — composite PK (consumer_scope_id, event_id, week_start).
ProcessedKafkaEventRepository Spring Data JPA + atomic native insertOrNothing(...) (ON CONFLICT DO NOTHING).
DedupGuard tryClaim(scope, eventId, eventTime, topic, partition, offset)boolean. Own @Transactional boundary.
DedupMetricsBinder Counter im2be_dedup_claims_total{consumer_scope_id, outcome} + gauge im2be_dedup_active_partitions_total{consumer_scope_id}.
DedupAutoConfiguration @ConditionalOnProperty("im2be.dedup.enabled", "true").
DedupProperties im2be.dedup.{enabled, default-scope-id} with @NotBlank validation.

Key design decisions

  • @EmbeddedId over @IdClass: composite-key record gives equals /
    hashCode / toString / Serializable for free — Hibernate 6.6 (Spring
    Boot 3.5.14) supports records as @Embeddable.
  • eventId as text, not UUID: aligns with ADR-0014's mint-agnostic
    contract (UUIDv7 for new events, UUIDv3 for deterministic events like
    TicketExpired). The dedup table only needs equality.
  • week_start derived from event timestamp, NOT ingestion: stabilises
    idempotency under reprocessing (replay a topic from offset 0 a week later
    → same compound keys → same dedup decisions). UTC-normalised — documented
    • tested with the CEST/UTC boundary case.
  • Own @Transactional boundary on tryClaim: business-logic failure
    must NOT roll back the dedup mark. Operators wanting at-least-once-with-
    rollback should NOT use this guard.
  • Native query, no SpEL: @Param-bound positional native query for
    auditability; the SpEL-on-native-query pattern is fragile and not
    needed here.
  • Provided-scope deps for spring-kafka / micrometer / OTel: the library
    stays light, consumers supply their compile-scoped versions matching
    their Actuator / Kafka starter choices.

Test plan

  • mvn -B clean install — BUILD SUCCESS, zero [WARNING] lines (rule 62).
  • mvn -B -pl processed-kafka-events test20 unit tests green:
    • ProcessedKafkaEventIdTest (5) — record equals / hashCode /
      Serializable round-trip.
    • WeekStartDerivationTest (5) — Monday boundary cases incl.
      UTC-Sunday-vs-CET-Monday.
    • DedupGuardTest (7) — mocked repo verifies claimed/duplicate outcomes,
      week_start derivation flows into insertOrNothing, blank/null arg
      validation.
    • DedupAutoConfigurationTest (3) — ApplicationContextRunner with H2:
      disabled by default → no beans; enabled → all beans present.
  • mvn -B -pl processed-kafka-events verify4 IT tests green
    against a real PostgreSQL 15 Testcontainer:
    • Happy path: same key twice → first true, second false.
    • Scope independence: same event_id + week, different scope → both true.
    • Week-start independence (L-10 boundary): same scope + event_id, two
      different weeks → both true.
    • Concurrent race: 10 threads, exactly 1 winner.

README

processed-kafka-events/README.md carries the full opt-in template,
pg_partman setup SQL (premake=4, 90d retention, daily pg_cron), sample
@KafkaListener usage, and observability surface.

Cross-references

  • ADR-0014 §D-4 / §D-10 / §L-7 / §L-8 / §L-10 / §L-12 (meta-repo
    im2be-mono)
  • .planning/26-stage-b-outbox-parity.md — Wave PLATFORM rollout
  • Sibling: outbox-publisher (producer side; PR-PLATFORM-1)
## Summary Replaces v1.0 scaffold stubs in `processed-kafka-events/` with the production implementation specified in [ADR-0014](https://git.hemoglobina.store/affinity-intelligence-rework/im2be-mono/src/branch/main/docs/decisions/0014-outbox-record-version-parity.md) §D-4 (compound-PK dedup contract) + §L-7 / §L-8 (pg_partman weekly RANGE + 90-day retention) + §L-10 (week_start derived from event timestamp) + §L-12 (per-pod broadcast dedup via scope suffix). ## Public API | Symbol | Purpose | |---|---| | `ProcessedKafkaEvent` | JPA `@Entity`, `@EmbeddedId` composite PK, `@Immutable` (append-only — no `@Version` required). | | `ProcessedKafkaEventId` | `@Embeddable` Java 17 `record` — composite PK `(consumer_scope_id, event_id, week_start)`. | | `ProcessedKafkaEventRepository` | Spring Data JPA + atomic native `insertOrNothing(...)` (`ON CONFLICT DO NOTHING`). | | `DedupGuard` | `tryClaim(scope, eventId, eventTime, topic, partition, offset)` → `boolean`. Own `@Transactional` boundary. | | `DedupMetricsBinder` | Counter `im2be_dedup_claims_total{consumer_scope_id, outcome}` + gauge `im2be_dedup_active_partitions_total{consumer_scope_id}`. | | `DedupAutoConfiguration` | `@ConditionalOnProperty("im2be.dedup.enabled", "true")`. | | `DedupProperties` | `im2be.dedup.{enabled, default-scope-id}` with `@NotBlank` validation. | ## Key design decisions - **`@EmbeddedId` over `@IdClass`**: composite-key record gives `equals` / `hashCode` / `toString` / `Serializable` for free — Hibernate 6.6 (Spring Boot 3.5.14) supports records as `@Embeddable`. - **`eventId` as `text`, not `UUID`**: aligns with ADR-0014's mint-agnostic contract (UUIDv7 for new events, UUIDv3 for deterministic events like `TicketExpired`). The dedup table only needs equality. - **`week_start` derived from event timestamp, NOT ingestion**: stabilises idempotency under reprocessing (replay a topic from offset 0 a week later → same compound keys → same dedup decisions). UTC-normalised — documented + tested with the CEST/UTC boundary case. - **Own `@Transactional` boundary on `tryClaim`**: business-logic failure must NOT roll back the dedup mark. Operators wanting at-least-once-with- rollback should NOT use this guard. - **Native query, no SpEL**: `@Param`-bound positional native query for auditability; the SpEL-on-native-query pattern is fragile and not needed here. - **Provided-scope deps for spring-kafka / micrometer / OTel**: the library stays light, consumers supply their compile-scoped versions matching their Actuator / Kafka starter choices. ## Test plan - [x] `mvn -B clean install` — BUILD SUCCESS, **zero `[WARNING]` lines** (rule 62). - [x] `mvn -B -pl processed-kafka-events test` — **20 unit tests green**: - `ProcessedKafkaEventIdTest` (5) — record `equals` / `hashCode` / `Serializable` round-trip. - `WeekStartDerivationTest` (5) — Monday boundary cases incl. UTC-Sunday-vs-CET-Monday. - `DedupGuardTest` (7) — mocked repo verifies claimed/duplicate outcomes, week_start derivation flows into `insertOrNothing`, blank/null arg validation. - `DedupAutoConfigurationTest` (3) — `ApplicationContextRunner` with H2: disabled by default → no beans; enabled → all beans present. - [x] `mvn -B -pl processed-kafka-events verify` — **4 IT tests green** against a real PostgreSQL 15 Testcontainer: - Happy path: same key twice → first `true`, second `false`. - Scope independence: same `event_id` + week, different scope → both `true`. - Week-start independence (L-10 boundary): same scope + `event_id`, two different weeks → both `true`. - Concurrent race: 10 threads, exactly 1 winner. ## README `processed-kafka-events/README.md` carries the full opt-in template, pg_partman setup SQL (`premake=4`, 90d retention, daily pg_cron), sample `@KafkaListener` usage, and observability surface. ## Cross-references - ADR-0014 §D-4 / §D-10 / §L-7 / §L-8 / §L-10 / §L-12 (meta-repo `im2be-mono`) - `.planning/26-stage-b-outbox-parity.md` — Wave PLATFORM rollout - Sibling: `outbox-publisher` (producer side; PR-PLATFORM-1)
feat(dedup): PR-PLATFORM-2 — compound-PK processed_kafka_events impl
Some checks failed
im2be-platform-libs CI / mvn install (pull_request) Failing after 2s
im2be-platform-libs CI / mvn verify (main only) (pull_request) Has been skipped
c05766fecb
Replace v1.0 scaffold stubs in processed-kafka-events with the production
implementation specified in ADR-0014 §D-4 / §L-7 / §L-8 / §L-10 / §L-12.

Entity model
- ProcessedKafkaEvent: @Entity, @EmbeddedId composite key, @Immutable
  (append-only — no @Version required, carved out of EntityVersionParity
  ArchRule by @Immutable). Indexed on (topic, partition) for forensics.
- ProcessedKafkaEventId: @Embeddable record (Hibernate 6.6 supports
  records as embeddable IDs). Components: consumer_scope_id, event_id
  (UUID rendered as text), week_start (LocalDate). Serializable.

Repository
- Atomic insertOrNothing(...) via native query with
  `ON CONFLICT (consumer_scope_id, event_id, week_start) DO NOTHING`,
  positional @Param binding (no SpEL) for auditability. Returns 1 on
  first insert, 0 on conflict — the dedup signal.
- countByIdConsumerScopeIdAndIdWeekStart for the active-partitions gauge.
- "offset" quoted (PG reserved word).

DedupGuard service
- tryClaim(scope, eventId, eventTime, topic, partition, offset) → boolean.
- Week-start derivation: UTC-normalised, TemporalAdjusters.previousOrSame(MONDAY)
  — Sunday 22:30 UTC = previous Monday (NOT local-CEST Monday). L-10
  stability boundary documented in Javadoc.
- Own @Transactional boundary so business-logic failure does NOT roll
  back the dedup mark.
- OTel span `im2be.dedup.try_claim` with attrs scope, event_id, week_start,
  topic, partition, offset, outcome. Micrometer counter
  `im2be_dedup_claims_total{consumer_scope_id, outcome}`.

Autoconfig + properties
- DedupAutoConfiguration: @ConditionalOnProperty("im2be.dedup.enabled"),
  self-scoped @EntityScan + @EnableJpaRepositories, @ConditionalOnMissingBean
  fallbacks for Clock + Tracer. Brings spring-boot-starter-validation for
  @NotBlank.
- DedupProperties: im2be.dedup.{enabled,default-scope-id} with @NotBlank.
- DedupMetricsBinder: counter + lazy-registered gauge polling
  countByIdConsumerScopeIdAndIdWeekStart at the current week.

Tests (24 total, all green)
- ProcessedKafkaEventIdTest (5): record equals/hashCode + Serializable round-trip.
- WeekStartDerivationTest (5): UTC-Sunday→prev-Monday boundary; Wednesday
  → that week; CET-Monday-vs-UTC-Sunday.
- DedupGuardTest (7): mocked repo; happy claimed/duplicate paths; week_start
  derivation flows into insertOrNothing; blank/null arg validation.
- DedupAutoConfigurationTest (3): ApplicationContextRunner with H2;
  disabled by default → no beans; enabled → DedupGuard + Binder + Properties.
- DedupGuardIT (4, Testcontainers + PG 15): happy path + scope independence
  + week-start independence (L-10 boundary) + concurrent race (10 threads,
  exactly 1 winner).

Maven deps added
- spring-boot-starter-validation (compile — drives @NotBlank).
- spring-kafka, micrometer-core, opentelemetry-api (provided — consumers
  bring their compile-scoped versions).
- Test: testcontainers postgresql + junit-jupiter (1.20.4), flyway-core +
  flyway-database-postgresql, postgresql JDBC, h2, micrometer simple
  registry (transitively).
- maven-failsafe-plugin executions wired so `mvn verify` triggers the IT.

README at processed-kafka-events/README.md covers: purpose, 3-paragraph
overview, public API table, opt-in property setup, full pg_partman weekly
RANGE + retention + pg_cron SQL template, sample @KafkaListener usage,
observability surface, cross-refs to ADR-0014 + .planning/26.

Build verification (rule 62):
- mvn -B clean install → BUILD SUCCESS
- grep -c '^\[WARNING\]' /tmp/dedup-build.log → 0
- mvn -B -pl processed-kafka-events test → 20 unit tests green
- mvn -B -pl processed-kafka-events verify → 4 IT tests green against
  real PostgreSQL 15 Testcontainer
chore: refire review (webhook delivery restored)
Some checks failed
im2be-platform-libs CI / mvn install (pull_request) Failing after 3s
im2be-platform-libs CI / mvn verify (main only) (pull_request) Has been skipped
e366f11f01

Superseded by round 2.

Show previous round

hib-pr-reviewer review — PR #2 (affinity-intelligence-rework/im2be-platform-libs)

Round 1 — head e366f11f0104, base main, trigger synchronize

TL;DR: NEEDS_WORK — kept 2 agreed major findings, verified 1 unique-B major and 2 unique-A minors; no findings dropped.

Summary

Arbiter reconciliation — round 1

No prior run history in Memora (first arbitration for this PR). All five reconciled findings were verified against HEAD source files before being kept.

Agreed findings kept (2): Both A and B independently flagged the @Transactional propagation defect (exact same file + line) and the dead defaultScopeId property. These are the strongest signals.

Unique-to-B finding verified and kept (1): B's observation that @Validated is placed on DedupAutoConfiguration rather than on DedupProperties itself is confirmed. DedupProperties.java has no @Validated annotation; Spring Boot's ConfigurationPropertiesBindingPostProcessor only enforces JSR-303 constraints when @Validated is present on the @ConfigurationProperties class. The Javadoc on line 38 explicitly claims the validation is 'wired in DedupAutoConfiguration' — the mechanism is mis-understood. This is a distinct defect from finding #2 (which is about the property being consumed at all).

Unique-to-A findings verified and kept (2): Instant.now() at DedupGuard.java:159 is confirmed — the Clock bean exists and reaches DedupMetricsBinder but never DedupGuard. Public setters on ProcessedKafkaEvent are confirmed; the entity uses field access and is annotated @Immutable, making the setters both misleading and unneeded.

No findings were dropped.

Blast Radius

The diff touches 15 files within a single Spring Boot autoconfiguration library module (processed-kafka-events). The library is consumed by multiple aim2be services via autoconfiguration (im2be.dedup.enabled=true), so the @Transactional propagation bug and the dead property silently affect every downstream service that enables the module. Changes are self-contained within the dedup package, but the exported API surface (DedupGuard.tryClaim, DedupProperties binding) amplifies the impact across the platform.

BLAST_SCORE: 5/10

Risk Indicators

Indicator Value
Sensitive functions DedupGuard.tryClaim, ProcessedKafkaEventRepository.insertOrNothing
Migration touched
Test delta
Dependency changes

CI status (head e366f11f0104)

Overall: ✗ failure

2 checks: 2 pending

Check State Link
im2be-platform-libs CI / mvn install (pull_request) pending details
im2be-platform-libs CI / mvn verify (main only) (pull_request) pending details

Findings (5)

[MAJOR] @Transactional uses default REQUIRED propagation — documented "own transaction" guarantee is broken

processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupGuard.java:141

Agreed — both A and B independently identified this defect at the same line.

Line 141 reads:

@Transactional
public boolean tryClaim(

The Javadoc (lines 118–122) and README both guarantee: "the INSERT runs in its own transaction so that a subsequent business-logic failure does NOT roll back the dedup mark." With the default PROPAGATION_REQUIRED, when the caller already holds an open transaction (any @Transactional service method or Kafka listener decorated with @Transactional), tryClaim joins that outer transaction. A rollback in the caller's business logic rolls back the dedup INSERT, allowing the next redelivery attempt to find no row and re-claim the event — the exact duplicate-delivery failure the library is designed to prevent.

Fix: change to @Transactional(propagation = Propagation.REQUIRES_NEW) so the INSERT commits independently of the caller's transaction context:

@Transactional(propagation = Propagation.REQUIRES_NEW)
public boolean tryClaim(...) {

Note: REQUIRES_NEW suspends the caller's transaction for the duration of the INSERT, incurring a nested-connection cost on XA datasources. The existing DedupGuardTest mocks the repository and DedupGuardIT runs each call in an undecorated test method — neither catches this regression; a dedicated IT with an outer @Transactional test method is recommended to lock in the fix.

[MAJOR] @NotBlank defaultScopeId is validated but never consumed — dead mandatory config property

processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupProperties.java:40

Agreed — both A and B independently identified this defect.

DedupProperties.defaultScopeId carries @NotBlank (line 40, value defaults to ""). The Javadoc on lines 18–19 asserts it "is read by DedupGuard when callers use the no-scope overload." Verified:

  1. No such overload exists. DedupGuard has exactly one tryClaim signature — the 6-argument form requiring an explicit consumerScopeId. There is no 5-argument no-scope variant.
  2. getDefaultScopeId() is called exclusively from the test (DedupAutoConfigurationTest.java:68) — never from production code.
  3. DedupAutoConfiguration.dedupGuard() (lines 100–107) does not inject DedupProperties into DedupGuard, so the guard has no path to the property even at runtime.

Consequence: every service that sets im2be.dedup.enabled=true must also supply im2be.dedup.default-scope-id (or face a ConstraintViolationException if the @NotBlank is ever enforced — see finding 3), yet the value has zero effect on dedup behaviour.

Fix options:

  • Option A (recommended per docs): Add the 5-argument overload to DedupGuard, inject DedupProperties via the constructor, wire it from DedupAutoConfiguration.dedupGuard(…).
  • Option B: Remove defaultScopeId from DedupProperties and all documentation until the overload is implemented. Mark it @Preview / optional if you want to reserve the field.

[MAJOR] @Validated is on DedupAutoConfiguration, not on DedupProperties@NotBlank binding-time validation is silently unenforced

processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupProperties.java:23

Unique to reviewer B — verified by inspection.

DedupProperties.java is annotated only with @ConfigurationProperties(prefix = "im2be.dedup") (line 23); there is no @Validated at the class level. The @Validated annotation lives on DedupAutoConfiguration (line 43 of that file), which activates Spring AOP method-level validation on @Bean factory methods — it does NOT trigger ConfigurationPropertiesBindingPostProcessor / ConfigurationPropertiesJsr303Validator validation on the bound properties object.

The Javadoc on DedupProperties line 38 claims the @NotBlank is "evaluated by the standard Spring Boot @Validated machinery wired in DedupAutoConfiguration" — this is incorrect. Without @Validated directly on the @ConfigurationProperties class, im2be.dedup.enabled=true with a missing or blank default-scope-id starts the application silently with defaultScopeId = "", making the advertised startup guard a no-op.

This is a distinct defect from finding #2 (property never consumed). Even once an overload is added (fixing #2), the validation gate must also be fixed or the overload would silently call tryClaim with a blank scope.

Fix: add @Validated directly to DedupProperties:

@Validated
@ConfigurationProperties(prefix = "im2be.dedup")
public class DedupProperties {

[MINOR] Instant.now() bypasses the injected ClockfirstSeenAt is not test-controllable

processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupGuard.java:159

Unique to reviewer A — verified by inspection.

Line 159:

final Instant now = Instant.now();

This now is passed directly to repository.insertOrNothing(…) as the firstSeenAt argument. DedupAutoConfiguration registers a @ConditionalOnMissingBean Clock dedupClock() (returning Clock.systemUTC()) precisely so test and staging contexts can inject a fixed clock. DedupMetricsBinder correctly receives this clock via its constructor. However DedupGuard's constructor accepts only (repository, metrics, tracer) — the clock is never wired through.

Fix: add Clock to DedupGuard's constructor, inject from DedupAutoConfiguration.dedupGuard(…), and replace the direct call:

// DedupAutoConfiguration
public DedupGuard dedupGuard(
        final ProcessedKafkaEventRepository repository,
        final DedupMetricsBinder metrics,
        final Tracer tracer,
        final Clock clock) {
    return new DedupGuard(repository, metrics, tracer, clock);
}

// DedupGuard line 159
final Instant now = clock.instant();

This lets DedupGuardTest pin firstSeenAt in assertions without racing against wall-clock time.

[MINOR] Public setters on a Hibernate @Immutable field-access entity are misleading and unneeded

processed-kafka-events/src/main/java/com/aim2be/platform/dedup/ProcessedKafkaEvent.java:125

Unique to reviewer A — verified by inspection.

ProcessedKafkaEvent is annotated @Immutable (line 55) and uses field access (the @EmbeddedId annotation is on the id field at line 59-60, not on a getter). Hibernate populates fields directly at hydration time and never dirty-checks or flushes updates on @Immutable entities.

Lines 125–158 expose public setters for id, firstSeenAt, topic, partition, and offset. These are:

  • Unneeded by JPA — field access means Hibernate bypasses setters entirely during hydration.
  • Contradictory to @Immutable — any caller can mutate an entity instance that is semantically append-only post-insert.
  • Misleading to library consumers — the README documents "rows are immutable post-insert" but the Java API suggests otherwise.

Fix: narrow all setter visibility to package-private (drop the public modifier). If external test builders need construction, promote the existing ProcessedKafkaEvent(id, topic, partition, offset) convenience constructor and rely on that instead of setters.

Verdict

NEEDS_WORK


hib-pr-reviewer • round 1 • 5 findings (3M/2m) • 2026-05-26T22:47:17.667Z → 2026-05-26T22:50:40.148Z • posted-as: pr-reviewer-bot

<!-- hib-pr-reviewer collapsed --> > _Superseded by round 2._ <details> <summary>Show previous round</summary> ## hib-pr-reviewer review — PR #2 (affinity-intelligence-rework/im2be-platform-libs) **Round 1** — head `e366f11f0104`, base `main`, trigger `synchronize` **TL;DR:** NEEDS_WORK — kept 2 agreed major findings, verified 1 unique-B major and 2 unique-A minors; no findings dropped. ### Summary ## Arbiter reconciliation — round 1 No prior run history in Memora (first arbitration for this PR). All five reconciled findings were verified against HEAD source files before being kept. **Agreed findings kept (2):** Both A and B independently flagged the `@Transactional` propagation defect (exact same file + line) and the dead `defaultScopeId` property. These are the strongest signals. **Unique-to-B finding verified and kept (1):** B's observation that `@Validated` is placed on `DedupAutoConfiguration` rather than on `DedupProperties` itself is confirmed. `DedupProperties.java` has no `@Validated` annotation; Spring Boot's `ConfigurationPropertiesBindingPostProcessor` only enforces JSR-303 constraints when `@Validated` is present on the `@ConfigurationProperties` class. The Javadoc on line 38 explicitly claims the validation is 'wired in `DedupAutoConfiguration`' — the mechanism is mis-understood. This is a distinct defect from finding #2 (which is about the property being consumed at all). **Unique-to-A findings verified and kept (2):** `Instant.now()` at DedupGuard.java:159 is confirmed — the `Clock` bean exists and reaches `DedupMetricsBinder` but never `DedupGuard`. Public setters on `ProcessedKafkaEvent` are confirmed; the entity uses field access and is annotated `@Immutable`, making the setters both misleading and unneeded. No findings were dropped. ### Blast Radius The diff touches 15 files within a single Spring Boot autoconfiguration library module (`processed-kafka-events`). The library is consumed by multiple aim2be services via autoconfiguration (`im2be.dedup.enabled=true`), so the `@Transactional` propagation bug and the dead property silently affect every downstream service that enables the module. Changes are self-contained within the dedup package, but the exported API surface (`DedupGuard.tryClaim`, `DedupProperties` binding) amplifies the impact across the platform. **BLAST_SCORE: 5/10** ### Risk Indicators | Indicator | Value | |---|---| | Sensitive functions | `DedupGuard.tryClaim`, `ProcessedKafkaEventRepository.insertOrNothing` | | Migration touched | — | | Test delta | — | | Dependency changes | — | ### CI status (head `e366f11f0104`) **Overall: ✗ failure** 2 checks: 2 pending | Check | State | Link | |---|---|---| | im2be-platform-libs CI / mvn install (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/13/jobs/0) | | im2be-platform-libs CI / mvn verify (main only) (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/13/jobs/1) | ### Findings (5) #### **[MAJOR]** `@Transactional` uses default REQUIRED propagation — documented "own transaction" guarantee is broken _processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupGuard.java:141_ **Agreed — both A and B independently identified this defect at the same line.** Line 141 reads: ```java @Transactional public boolean tryClaim( ``` The Javadoc (lines 118–122) and README both guarantee: *"the INSERT runs in its **own** transaction so that a subsequent business-logic failure does NOT roll back the dedup mark."* With the default `PROPAGATION_REQUIRED`, when the caller already holds an open transaction (any `@Transactional` service method or Kafka listener decorated with `@Transactional`), `tryClaim` **joins** that outer transaction. A rollback in the caller's business logic rolls back the dedup `INSERT`, allowing the next redelivery attempt to find no row and re-claim the event — the exact duplicate-delivery failure the library is designed to prevent. **Fix:** change to `@Transactional(propagation = Propagation.REQUIRES_NEW)` so the INSERT commits independently of the caller's transaction context: ```java @Transactional(propagation = Propagation.REQUIRES_NEW) public boolean tryClaim(...) { ``` Note: `REQUIRES_NEW` suspends the caller's transaction for the duration of the INSERT, incurring a nested-connection cost on XA datasources. The existing `DedupGuardTest` mocks the repository and `DedupGuardIT` runs each call in an undecorated test method — neither catches this regression; a dedicated IT with an outer `@Transactional` test method is recommended to lock in the fix. #### **[MAJOR]** `@NotBlank defaultScopeId` is validated but never consumed — dead mandatory config property _processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupProperties.java:40_ **Agreed — both A and B independently identified this defect.** `DedupProperties.defaultScopeId` carries `@NotBlank` (line 40, value defaults to `""`). The Javadoc on lines 18–19 asserts it *"is read by `DedupGuard` when callers use the no-scope overload."* Verified: 1. **No such overload exists.** `DedupGuard` has exactly one `tryClaim` signature — the 6-argument form requiring an explicit `consumerScopeId`. There is no 5-argument no-scope variant. 2. **`getDefaultScopeId()` is called exclusively from the test** (`DedupAutoConfigurationTest.java:68`) — never from production code. 3. **`DedupAutoConfiguration.dedupGuard()`** (lines 100–107) does not inject `DedupProperties` into `DedupGuard`, so the guard has no path to the property even at runtime. Consequence: every service that sets `im2be.dedup.enabled=true` must also supply `im2be.dedup.default-scope-id` (or face a `ConstraintViolationException` if the `@NotBlank` is ever enforced — see finding 3), yet the value has zero effect on dedup behaviour. **Fix options:** - **Option A (recommended per docs):** Add the 5-argument overload to `DedupGuard`, inject `DedupProperties` via the constructor, wire it from `DedupAutoConfiguration.dedupGuard(…)`. - **Option B:** Remove `defaultScopeId` from `DedupProperties` and all documentation until the overload is implemented. Mark it `@Preview` / optional if you want to reserve the field. #### **[MAJOR]** `@Validated` is on `DedupAutoConfiguration`, not on `DedupProperties` — `@NotBlank` binding-time validation is silently unenforced _processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupProperties.java:23_ **Unique to reviewer B — verified by inspection.** `DedupProperties.java` is annotated only with `@ConfigurationProperties(prefix = "im2be.dedup")` (line 23); there is no `@Validated` at the class level. The `@Validated` annotation lives on `DedupAutoConfiguration` (line 43 of that file), which activates Spring AOP **method-level** validation on `@Bean` factory methods — it does NOT trigger `ConfigurationPropertiesBindingPostProcessor` / `ConfigurationPropertiesJsr303Validator` validation on the bound properties object. The Javadoc on `DedupProperties` line 38 claims the `@NotBlank` is *"evaluated by the standard Spring Boot `@Validated` machinery wired in `DedupAutoConfiguration`"* — this is incorrect. Without `@Validated` directly on the `@ConfigurationProperties` class, `im2be.dedup.enabled=true` with a missing or blank `default-scope-id` starts the application silently with `defaultScopeId = ""`, making the advertised startup guard a no-op. This is a distinct defect from finding #2 (property never consumed). Even once an overload is added (fixing #2), the validation gate must also be fixed or the overload would silently call `tryClaim` with a blank scope. **Fix:** add `@Validated` directly to `DedupProperties`: ```java @Validated @ConfigurationProperties(prefix = "im2be.dedup") public class DedupProperties { ``` #### **[MINOR]** `Instant.now()` bypasses the injected `Clock` — `firstSeenAt` is not test-controllable _processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupGuard.java:159_ **Unique to reviewer A — verified by inspection.** Line 159: ```java final Instant now = Instant.now(); ``` This `now` is passed directly to `repository.insertOrNothing(…)` as the `firstSeenAt` argument. `DedupAutoConfiguration` registers a `@ConditionalOnMissingBean Clock dedupClock()` (returning `Clock.systemUTC()`) precisely so test and staging contexts can inject a fixed clock. `DedupMetricsBinder` correctly receives this clock via its constructor. However `DedupGuard`'s constructor accepts only `(repository, metrics, tracer)` — the clock is never wired through. **Fix:** add `Clock` to `DedupGuard`'s constructor, inject from `DedupAutoConfiguration.dedupGuard(…)`, and replace the direct call: ```java // DedupAutoConfiguration public DedupGuard dedupGuard( final ProcessedKafkaEventRepository repository, final DedupMetricsBinder metrics, final Tracer tracer, final Clock clock) { return new DedupGuard(repository, metrics, tracer, clock); } // DedupGuard line 159 final Instant now = clock.instant(); ``` This lets `DedupGuardTest` pin `firstSeenAt` in assertions without racing against wall-clock time. #### **[MINOR]** Public setters on a Hibernate `@Immutable` field-access entity are misleading and unneeded _processed-kafka-events/src/main/java/com/aim2be/platform/dedup/ProcessedKafkaEvent.java:125_ **Unique to reviewer A — verified by inspection.** `ProcessedKafkaEvent` is annotated `@Immutable` (line 55) and uses **field access** (the `@EmbeddedId` annotation is on the `id` field at line 59-60, not on a getter). Hibernate populates fields directly at hydration time and never dirty-checks or flushes updates on `@Immutable` entities. Lines 125–158 expose `public` setters for `id`, `firstSeenAt`, `topic`, `partition`, and `offset`. These are: - **Unneeded by JPA** — field access means Hibernate bypasses setters entirely during hydration. - **Contradictory to `@Immutable`** — any caller can mutate an entity instance that is semantically append-only post-insert. - **Misleading to library consumers** — the README documents "rows are immutable post-insert" but the Java API suggests otherwise. **Fix:** narrow all setter visibility to `package-private` (drop the `public` modifier). If external test builders need construction, promote the existing `ProcessedKafkaEvent(id, topic, partition, offset)` convenience constructor and rely on that instead of setters. ### Verdict **NEEDS_WORK** --- <sub>hib-pr-reviewer • round 1 • 5 findings (3M/2m) • 2026-05-26T22:47:17.667Z → 2026-05-26T22:50:40.148Z • posted-as: pr-reviewer-bot</sub> </details>
fix(dedup): R2 — 5 R1 findings (3 MAJOR + 2 MINOR)
Some checks failed
im2be-platform-libs CI / mvn install (pull_request) Failing after 2s
im2be-platform-libs CI / mvn verify (main only) (pull_request) Has been skipped
295aeaabac
Addresses PR-PLATFORM-2 R1 NEEDS_WORK with 5 findings:

- MAJOR: @Transactional REQUIRED → REQUIRES_NEW on DedupGuard.tryClaim
  (docstring promised 'own transaction'; REQUIRED would join outer txn
  and cause rollback-on-business-failure to undo the dedup mark — the
  opposite of dedup intent).
- MAJOR: defaultScopeId was @NotBlank but never consumed (dead config).
  Added DedupGuard.tryClaim no-scope overload that uses defaultScopeId;
  property now optional (empty = no default; overload throws ISE).
- MAJOR: @Validated was on DedupAutoConfiguration, not on DedupProperties.
  Moved to the properties class so jakarta.validation constraints fire
  at binding time (forward-looking; no constraints today).
- MINOR: Instant.now() bypassed any injected Clock. Added Clock to
  DedupGuard constructor; autoconfig wires Clock.systemUTC() bean
  (@ConditionalOnMissingBean so tests override). DedupGuardTest uses
  Clock.fixed for deterministic firstSeenAt.
- MINOR: Public setters on ProcessedKafkaEvent removed. Field-access
  Hibernate hydration does not need them; setters were misleading on
  a conceptually immutable post-INSERT entity.

Superseded by round 3.

Show previous round

hib-pr-reviewer review — PR #2 (affinity-intelligence-rework/im2be-platform-libs)

Round 2 — head 295aeaabac56, base main, trigger synchronize

TL;DR: NEEDS_WORK — kept 2 findings (1 major agreed by both reviewers, 1 minor unique-to-B verified); both confirmed by direct file reads at HEAD.

Summary

Round 2 Arbitration — reconciliation summary

Recalled Memora context: no prior run history found (new store). Memora persistence attempted 4 times; all failed with Tag 'memora/issues' is not in the allowed tag list (auto-classifier is assigning a tag not in this instance's allowlist — not a content or logic error on my part).

Verification performed:

  • Read DedupGuard.java in full. Confirmed line 236–247: the 5-arg no-scope overload carries no @Transactional annotation; line 247 is a bare return tryClaim(defaultScopeId, …) self-call that cannot route through the Spring CGLIB proxy. The 6-arg overload at line 160 carries @Transactional(propagation = Propagation.REQUIRES_NEW) — but that annotation is unreachable via the self-call. Major finding confirmed at stated line.
  • Read DedupAutoConfiguration.java in full. Confirmed line 67: return GlobalOpenTelemetry.getTracer("com.aim2be.platform.dedup") — evaluated eagerly at bean-creation time with no @AutoConfigureAfter ordering guard and no OpenTelemetry bean injection. Minor finding confirmed at stated line.

Reconciliation decisions:

  1. Major finding (A+B agreed, same file+line+body) — KEPT per Rule 1.
  2. Minor finding (unique to B) — KEPT after verification per Rule 2. Code at line 67 does exactly what B described.

Total kept: 2 (1 major, 1 minor). No findings dropped.

Blast Radius

The diff is contained within the processed-kafka-events module (15 files), but DedupGuard.tryClaim is the single public dedup API surface that every consuming @KafkaListener calls. The no-scope overload is the usage pattern shown in the README and is likely the most common entry point across all consumers. A broken transactional guarantee at this layer propagates silently to every service in the platform that adopts the library.

BLAST_SCORE: 6/10

Risk Indicators

Indicator Value
Sensitive functions DedupGuard.tryClaim, ProcessedKafkaEventRepository.insertOrNothing
Migration touched
Test delta
Dependency changes

CI status (head 295aeaabac56)

Overall: ✗ failure

2 checks: 2 pending

Check State Link
im2be-platform-libs CI / mvn install (pull_request) pending details
im2be-platform-libs CI / mvn verify (main only) (pull_request) pending details

Findings (2)

[MAJOR] No-scope tryClaim overload bypasses @Transactional(REQUIRES_NEW) via self-invocation — dedup isolation guarantee silently broken for the primary consumer API

processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupGuard.java:247

The 5-arg convenience overload (line 236) carries no @Transactional annotation. Its sole delegation — return tryClaim(defaultScopeId, eventId, eventTime, topic, partition, offset) at line 247 — is a this.-call that routes to the raw target object, not the Spring CGLIB proxy. The @Transactional(propagation = Propagation.REQUIRES_NEW) declared on the 6-arg method at line 160 is therefore never triggered when a caller enters through the no-scope path.

Impact: a @KafkaListener method that already runs inside a transaction (the common case) and calls the no-scope overload will execute the dedup INSERT inside the caller's ambient transaction. If a downstream business-logic failure rolls that transaction back, the dedup mark is rolled back too — allowing the same event to be reprocessed on the next retry. The README and class Javadoc explicitly guarantee the opposite ("a subsequent business-logic failure does NOT roll back the dedup mark").

Fix: annotate the no-scope overload with @Transactional(propagation = Propagation.REQUIRES_NEW):

@Transactional(propagation = Propagation.REQUIRES_NEW)
public boolean tryClaim(
        final String eventId,
        final Instant eventTime,
        final String topic,
        final int partition,
        final long offset) {
    if (defaultScopeId == null || defaultScopeId.isBlank()) {  }
    return tryClaim(defaultScopeId, eventId, eventTime, topic, partition, offset);
}

Spring intercepts the outer call via proxy, suspending any ambient transaction and starting a fresh REQUIRES_NEW one. The inner self-call to the 6-arg method then participates in that already-active transaction; the 6-arg method's own annotation is bypassed by self-invocation but the surrounding transaction already satisfies the contract.

Test gap: DedupGuardIT does not cover the scenario of calling the no-scope overload from within an ambient transaction and verifying that an outer rollback does NOT roll back the dedup mark. This case should be added alongside the fix.

[MINOR] dedupTracer() snapshots GlobalOpenTelemetry eagerly at bean-creation time — permanently noop tracer when OTel SDK autoconfiguration runs after DedupAutoConfiguration

processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupAutoConfiguration.java:67

Line 67: return GlobalOpenTelemetry.getTracer("com.aim2be.platform.dedup"); is evaluated once at Spring context-refresh time. GlobalOpenTelemetry.getTracer() captures the OpenTelemetry SDK that is globally registered at that moment — it does not produce a lazy-delegating proxy. If the consuming service's OTel SDK autoconfiguration (e.g. opentelemetry-spring-boot-starter's OpenTelemetryAutoConfiguration) runs after DedupAutoConfiguration — which is possible because no @AutoConfigureAfter ordering constraint is declared on the class — GlobalOpenTelemetry.get() still holds the noop SDK at call time, and dedupTracer() returns a permanently-noop Tracer. All im2be.dedup.try_claim spans are silently dropped with no runtime error.

Scope: this only bites the programmatic-SDK path. When the OTel Java Agent is used, the agent pre-wires GlobalOpenTelemetry before any Spring beans are created, so ordering is irrelevant.

Fix: accept an optional OpenTelemetry bean parameter and fall back to the global only when no bean is present:

@Bean
@ConditionalOnMissingBean
public Tracer dedupTracer(
        @Autowired(required = false) final io.opentelemetry.api.OpenTelemetry openTelemetry) {
    final io.opentelemetry.api.OpenTelemetry otel =
            (openTelemetry != null) ? openTelemetry : GlobalOpenTelemetry.get();
    return otel.getTracer("com.aim2be.platform.dedup");
}

Spring DI guarantees the injected OpenTelemetry bean is fully initialised before this factory method fires; no @AutoConfigureAfter on an optional dependency is needed.

Verdict

NEEDS_WORK


hib-pr-reviewer • round 2 • 2 findings (1M/1m) • 2026-05-26T23:01:15.166Z → 2026-05-26T23:03:20.287Z • posted-as: pr-reviewer-bot

<!-- hib-pr-reviewer collapsed --> > _Superseded by round 3._ <details> <summary>Show previous round</summary> ## hib-pr-reviewer review — PR #2 (affinity-intelligence-rework/im2be-platform-libs) **Round 2** — head `295aeaabac56`, base `main`, trigger `synchronize` **TL;DR:** NEEDS_WORK — kept 2 findings (1 major agreed by both reviewers, 1 minor unique-to-B verified); both confirmed by direct file reads at HEAD. ### Summary ## Round 2 Arbitration — reconciliation summary Recalled Memora context: no prior run history found (new store). Memora persistence attempted 4 times; all failed with `Tag 'memora/issues' is not in the allowed tag list` (auto-classifier is assigning a tag not in this instance's allowlist — not a content or logic error on my part). **Verification performed:** - Read `DedupGuard.java` in full. Confirmed line 236–247: the 5-arg no-scope overload carries **no `@Transactional` annotation**; line 247 is a bare `return tryClaim(defaultScopeId, …)` self-call that cannot route through the Spring CGLIB proxy. The 6-arg overload at line 160 carries `@Transactional(propagation = Propagation.REQUIRES_NEW)` — but that annotation is unreachable via the self-call. **Major finding confirmed at stated line.** - Read `DedupAutoConfiguration.java` in full. Confirmed line 67: `return GlobalOpenTelemetry.getTracer("com.aim2be.platform.dedup")` — evaluated eagerly at bean-creation time with no `@AutoConfigureAfter` ordering guard and no `OpenTelemetry` bean injection. **Minor finding confirmed at stated line.** **Reconciliation decisions:** 1. Major finding (A+B agreed, same file+line+body) — KEPT per Rule 1. 2. Minor finding (unique to B) — KEPT after verification per Rule 2. Code at line 67 does exactly what B described. Total kept: 2 (1 major, 1 minor). No findings dropped. ### Blast Radius The diff is contained within the `processed-kafka-events` module (15 files), but `DedupGuard.tryClaim` is the single public dedup API surface that every consuming `@KafkaListener` calls. The no-scope overload is the usage pattern shown in the README and is likely the most common entry point across all consumers. A broken transactional guarantee at this layer propagates silently to every service in the platform that adopts the library. **BLAST_SCORE: 6/10** ### Risk Indicators | Indicator | Value | |---|---| | Sensitive functions | `DedupGuard.tryClaim`, `ProcessedKafkaEventRepository.insertOrNothing` | | Migration touched | — | | Test delta | — | | Dependency changes | — | ### CI status (head `295aeaabac56`) **Overall: ✗ failure** 2 checks: 2 pending | Check | State | Link | |---|---|---| | im2be-platform-libs CI / mvn install (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/17/jobs/0) | | im2be-platform-libs CI / mvn verify (main only) (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/17/jobs/1) | ### Findings (2) #### **[MAJOR]** No-scope `tryClaim` overload bypasses `@Transactional(REQUIRES_NEW)` via self-invocation — dedup isolation guarantee silently broken for the primary consumer API _processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupGuard.java:247_ The 5-arg convenience overload (line 236) carries no `@Transactional` annotation. Its sole delegation — `return tryClaim(defaultScopeId, eventId, eventTime, topic, partition, offset)` at line 247 — is a `this.`-call that routes to the raw target object, not the Spring CGLIB proxy. The `@Transactional(propagation = Propagation.REQUIRES_NEW)` declared on the 6-arg method at line 160 is therefore **never triggered** when a caller enters through the no-scope path. **Impact:** a `@KafkaListener` method that already runs inside a transaction (the common case) and calls the no-scope overload will execute the dedup INSERT inside the caller's ambient transaction. If a downstream business-logic failure rolls that transaction back, the dedup mark is rolled back too — allowing the same event to be reprocessed on the next retry. The README and class Javadoc explicitly guarantee the opposite ("a subsequent business-logic failure does NOT roll back the dedup mark"). **Fix:** annotate the no-scope overload with `@Transactional(propagation = Propagation.REQUIRES_NEW)`: ```java @Transactional(propagation = Propagation.REQUIRES_NEW) public boolean tryClaim( final String eventId, final Instant eventTime, final String topic, final int partition, final long offset) { if (defaultScopeId == null || defaultScopeId.isBlank()) { … } return tryClaim(defaultScopeId, eventId, eventTime, topic, partition, offset); } ``` Spring intercepts the outer call via proxy, suspending any ambient transaction and starting a fresh `REQUIRES_NEW` one. The inner self-call to the 6-arg method then participates in that already-active transaction; the 6-arg method's own annotation is bypassed by self-invocation but the surrounding transaction already satisfies the contract. **Test gap:** `DedupGuardIT` does not cover the scenario of calling the no-scope overload from within an ambient transaction and verifying that an outer rollback does NOT roll back the dedup mark. This case should be added alongside the fix. #### **[MINOR]** `dedupTracer()` snapshots `GlobalOpenTelemetry` eagerly at bean-creation time — permanently noop tracer when OTel SDK autoconfiguration runs after `DedupAutoConfiguration` _processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupAutoConfiguration.java:67_ Line 67: `return GlobalOpenTelemetry.getTracer("com.aim2be.platform.dedup");` is evaluated once at Spring context-refresh time. `GlobalOpenTelemetry.getTracer()` captures the `OpenTelemetry` SDK that is globally registered at that moment — it does not produce a lazy-delegating proxy. If the consuming service's OTel SDK autoconfiguration (e.g. `opentelemetry-spring-boot-starter`'s `OpenTelemetryAutoConfiguration`) runs after `DedupAutoConfiguration` — which is possible because no `@AutoConfigureAfter` ordering constraint is declared on the class — `GlobalOpenTelemetry.get()` still holds the noop SDK at call time, and `dedupTracer()` returns a permanently-noop `Tracer`. All `im2be.dedup.try_claim` spans are silently dropped with no runtime error. **Scope:** this only bites the programmatic-SDK path. When the OTel Java Agent is used, the agent pre-wires `GlobalOpenTelemetry` before any Spring beans are created, so ordering is irrelevant. **Fix:** accept an optional `OpenTelemetry` bean parameter and fall back to the global only when no bean is present: ```java @Bean @ConditionalOnMissingBean public Tracer dedupTracer( @Autowired(required = false) final io.opentelemetry.api.OpenTelemetry openTelemetry) { final io.opentelemetry.api.OpenTelemetry otel = (openTelemetry != null) ? openTelemetry : GlobalOpenTelemetry.get(); return otel.getTracer("com.aim2be.platform.dedup"); } ``` Spring DI guarantees the injected `OpenTelemetry` bean is fully initialised before this factory method fires; no `@AutoConfigureAfter` on an optional dependency is needed. ### Verdict **NEEDS_WORK** --- <sub>hib-pr-reviewer • round 2 • 2 findings (1M/1m) • 2026-05-26T23:01:15.166Z → 2026-05-26T23:03:20.287Z • posted-as: pr-reviewer-bot</sub> </details>
fix(dedup): R3 — self-invocation gotcha + eager tracer snapshot
Some checks failed
im2be-platform-libs CI / mvn install (pull_request) Failing after 2s
im2be-platform-libs CI / mvn verify (main only) (pull_request) Has been skipped
bbdcd13c36
Addresses PR-PLATFORM-2 R2 NEEDS_WORK:

- MAJOR: no-scope tryClaim overload called this.tryClaim(scope, ...) which
  is self-invocation and silently bypasses Spring's @Transactional proxy.
  Refactored: extracted shared tryClaimInternal() private method; both
  public overloads carry @Transactional(REQUIRES_NEW) and call the
  internal method, so the proxy intercepts EACH public entry without any
  self-invocation between them.
- MINOR: dedupTracer() called GlobalOpenTelemetry.getTracer() at bean
  creation, snapshotting a noop tracer permanently when OTel SDK
  auto-configuration runs AFTER DedupAutoConfiguration. Switched to
  ObjectProvider<OpenTelemetry> injection — defers the lookup until
  first method invocation, by which time the SDK auto-config has
  registered the OpenTelemetry bean. Falls back to OpenTelemetry.noop()
  when no SDK is on the classpath.

Superseded by round 4.

Show previous round

hib-pr-reviewer review — PR #2 (affinity-intelligence-rework/im2be-platform-libs)

Round 3 — head bbdcd13c363d, base main, trigger synchronize

TL;DR: CONDITIONAL_APPROVE — kept 3 unique-to-B findings (all verified at HEAD): eager OTel snapshot with incorrect Javadoc, missing no-scope overload tests with stale comment, and stale README @NotBlank claim; all prior R1/R2 findings confirmed resolved.

Summary

Reviewer A's JSON was truncated and returned [] formally (0 findings), though prose echoed the same OTel eager-resolution issue as B's Finding 1. All prior R1/R2 findings confirmed resolved by both reviewers. All three B findings were verified against HEAD:

  1. DedupAutoConfiguration.java:77-79otelProvider.getIfAvailable(OpenTelemetry::noop).getTracer(...) executes synchronously inside the @Bean method body; the Tracer is a concrete snapshot taken at ApplicationContext.refresh() time. The Javadoc at lines 64-65 incorrectly states the lookup is "deferred until this bean's first method invocation." No @AutoConfigureAfter is present. Finding confirmed — kept as MINOR.

  2. DedupGuardTest.java:56-57 — Comment reads // own test below (DedupGuardNoScopeOverloadTest).; a Glob across the entire checkout found zero files matching DedupGuardNoScopeOverloadTest*. The no-scope tryClaim overload (which carries the R2F1 self-invocation fix) has zero unit or integration test coverage. Finding confirmed — kept as MINOR.

  3. README.md:52 — Row states @NotBlank validation on default-scope-id when enabled; DedupProperties.java has no jakarta constraint on defaultScopeId (the field defaults to "" and is intentionally optional). Finding confirmed — kept as INFO.

Memora persistence attempted but failed due to system-level tag allowlist rejection; run summary not stored.

Blast Radius

This module is a Spring Boot autoconfiguration library exported for consumption by multiple aim2be services. The DedupAutoConfiguration wires beans shared across all consumers, and DedupGuard is the single dedup API entry point. Incorrect OTel bean ordering affects every consuming service's tracing fidelity at startup; the missing test gap leaves a behaviorally-critical overload unvalidated before it ships to all consumers.

BLAST_SCORE: 5/10

Risk Indicators

Indicator Value
Sensitive functions dedupTracer, insertOrNothing, tryClaimInternal
Migration touched yes
Test delta
Dependency changes

CI status (head bbdcd13c363d)

Overall: ✗ failure

2 checks: 2 pending

Check State Link
im2be-platform-libs CI / mvn install (pull_request) pending details
im2be-platform-libs CI / mvn verify (main only) (pull_request) pending details

Findings (3)

[MINOR] ObjectProvider.getIfAvailable() is called eagerly at bean-creation time; Javadoc incorrectly claims deferred resolution; @AutoConfigureAfter is absent

processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupAutoConfiguration.java:78

otelProvider.getIfAvailable(OpenTelemetry::noop).getTracer("com.aim2be.platform.dedup") at line 78 executes synchronously inside the @Bean factory method, which runs during ApplicationContext.refresh(). The resulting Tracer is a concrete snapshot taken at bean-creation time — if the OTel SDK auto-configuration's OpenTelemetry bean has not yet been registered at that moment, getIfAvailable() returns the noop fallback permanently.

The Javadoc at lines 64–65 states: "ObjectProvider is used so the lookup is deferred until this bean's first method invocation" — this is factually wrong. The ObjectProvider is resolved immediately, not lazily.

No @AutoConfigureAfter is present on DedupAutoConfiguration, so initialization order relative to any OTel SDK starter is undefined.

Recommended fix (Option A — no hot-path overhead): add a class-level ordering hint:

@AutoConfigureAfter(name =
    "io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration")

Using name (not the class literal) avoids a hard compile dependency on the OTel instrumentation starter. Also correct the Javadoc to drop the incorrect "deferred" claim.

Option B (true lazy resolution): inject ObjectProvider<OpenTelemetry> into DedupGuard itself and call otelProvider.getIfAvailable(OpenTelemetry::noop).getTracer(...) on each spanBuilder invocation — resilient to any ordering scenario but adds a provider lookup per tryClaim call.

[MINOR] Stale comment references non-existent DedupGuardNoScopeOverloadTest; no-scope tryClaim overload has zero test coverage

processed-kafka-events/src/test/java/com/aim2be/platform/dedup/DedupGuardTest.java:57

Lines 56–57 read:

// null defaultScopeId — the no-scope tryClaim overload is exercised in its
// own test below (DedupGuardNoScopeOverloadTest).

DedupGuardNoScopeOverloadTest does not exist anywhere in the repository (confirmed via Glob across the full checkout). The no-scope tryClaim(String, Instant, String, int, long) overload — which carries the R2F1 fix by calling tryClaimInternal instead of the public 6-arg overload — is entirely untested in both unit and integration tests.

Proposed fix: add a DedupGuardNoScopeOverloadTest (or extend DedupGuardTest) covering at minimum:

  1. No-scope tryClaim with a configured default scope delegates to insertOrNothing with that scope and returns the repository result correctly.
  2. No-scope tryClaim with null (or blank) defaultScopeId throws IllegalStateException with a message mentioning im2be.dedup.default-scope-id.
  3. Remove or correct the stale comment at line 56–57 regardless.

[INFO] README public-API table incorrectly claims @NotBlank validation on default-scope-id

processed-kafka-events/README.md:52

README.md line 52 states:

| `DedupProperties` | `im2be.dedup.*` config binding. `@NotBlank` validation on `default-scope-id` when enabled. |

DedupProperties.java has no @NotBlank (or any jakarta constraint) on defaultScopeId — the field defaults to "" and is intentionally optional (the R1 fix removed the over-constraint). The README still advertises the removed constraint, misleading consumers into believing a misconfigured blank default-scope-id is caught at startup rather than at the first no-scope tryClaim call.

Proposed fix: update line 52 to reflect the actual behaviour:

| `DedupProperties` | `im2be.dedup.*` config binding. `default-scope-id` is optional; when blank, the no-scope `tryClaim` overload throws `IllegalStateException` at runtime. |

Verdict

CONDITIONAL_APPROVE


hib-pr-reviewer • round 3 • 3 findings (2m/1i) • 2026-05-26T23:12:16.550Z → 2026-05-26T23:14:28.199Z • posted-as: pr-reviewer-bot

<!-- hib-pr-reviewer collapsed --> > _Superseded by round 4._ <details> <summary>Show previous round</summary> ## hib-pr-reviewer review — PR #2 (affinity-intelligence-rework/im2be-platform-libs) **Round 3** — head `bbdcd13c363d`, base `main`, trigger `synchronize` **TL;DR:** CONDITIONAL_APPROVE — kept 3 unique-to-B findings (all verified at HEAD): eager OTel snapshot with incorrect Javadoc, missing no-scope overload tests with stale comment, and stale README @NotBlank claim; all prior R1/R2 findings confirmed resolved. ### Summary Reviewer A's JSON was truncated and returned `[]` formally (0 findings), though prose echoed the same OTel eager-resolution issue as B's Finding 1. All prior R1/R2 findings confirmed resolved by both reviewers. All three B findings were verified against HEAD: 1. **DedupAutoConfiguration.java:77-79** — `otelProvider.getIfAvailable(OpenTelemetry::noop).getTracer(...)` executes synchronously inside the `@Bean` method body; the `Tracer` is a concrete snapshot taken at `ApplicationContext.refresh()` time. The Javadoc at lines 64-65 incorrectly states the lookup is "deferred until this bean's first method invocation." No `@AutoConfigureAfter` is present. Finding confirmed — kept as MINOR. 2. **DedupGuardTest.java:56-57** — Comment reads `// own test below (DedupGuardNoScopeOverloadTest).`; a `Glob` across the entire checkout found zero files matching `DedupGuardNoScopeOverloadTest*`. The no-scope `tryClaim` overload (which carries the R2F1 self-invocation fix) has zero unit or integration test coverage. Finding confirmed — kept as MINOR. 3. **README.md:52** — Row states `@NotBlank validation on default-scope-id when enabled`; `DedupProperties.java` has no jakarta constraint on `defaultScopeId` (the field defaults to `""` and is intentionally optional). Finding confirmed — kept as INFO. Memora persistence attempted but failed due to system-level tag allowlist rejection; run summary not stored. ### Blast Radius This module is a Spring Boot autoconfiguration library exported for consumption by multiple aim2be services. The `DedupAutoConfiguration` wires beans shared across all consumers, and `DedupGuard` is the single dedup API entry point. Incorrect OTel bean ordering affects every consuming service's tracing fidelity at startup; the missing test gap leaves a behaviorally-critical overload unvalidated before it ships to all consumers. **BLAST_SCORE: 5/10** ### Risk Indicators | Indicator | Value | |---|---| | Sensitive functions | `dedupTracer`, `insertOrNothing`, `tryClaimInternal` | | Migration touched | yes | | Test delta | — | | Dependency changes | — | ### CI status (head `bbdcd13c363d`) **Overall: ✗ failure** 2 checks: 2 pending | Check | State | Link | |---|---|---| | im2be-platform-libs CI / mvn install (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/19/jobs/0) | | im2be-platform-libs CI / mvn verify (main only) (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/19/jobs/1) | ### Findings (3) #### **[MINOR]** `ObjectProvider.getIfAvailable()` is called eagerly at bean-creation time; Javadoc incorrectly claims deferred resolution; `@AutoConfigureAfter` is absent _processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupAutoConfiguration.java:78_ `otelProvider.getIfAvailable(OpenTelemetry::noop).getTracer("com.aim2be.platform.dedup")` at line 78 executes synchronously inside the `@Bean` factory method, which runs during `ApplicationContext.refresh()`. The resulting `Tracer` is a concrete snapshot taken at bean-creation time — if the OTel SDK auto-configuration's `OpenTelemetry` bean has not yet been registered at that moment, `getIfAvailable()` returns the noop fallback permanently. The Javadoc at lines 64–65 states: _"ObjectProvider is used so the lookup is deferred until this bean's first method invocation"_ — this is factually wrong. The `ObjectProvider` is resolved immediately, not lazily. No `@AutoConfigureAfter` is present on `DedupAutoConfiguration`, so initialization order relative to any OTel SDK starter is undefined. **Recommended fix (Option A — no hot-path overhead):** add a class-level ordering hint: ```java @AutoConfigureAfter(name = "io.opentelemetry.instrumentation.spring.autoconfigure.OpenTelemetryAutoConfiguration") ``` Using `name` (not the class literal) avoids a hard compile dependency on the OTel instrumentation starter. Also correct the Javadoc to drop the incorrect "deferred" claim. **Option B (true lazy resolution):** inject `ObjectProvider<OpenTelemetry>` into `DedupGuard` itself and call `otelProvider.getIfAvailable(OpenTelemetry::noop).getTracer(...)` on each `spanBuilder` invocation — resilient to any ordering scenario but adds a provider lookup per `tryClaim` call. #### **[MINOR]** Stale comment references non-existent `DedupGuardNoScopeOverloadTest`; no-scope `tryClaim` overload has zero test coverage _processed-kafka-events/src/test/java/com/aim2be/platform/dedup/DedupGuardTest.java:57_ Lines 56–57 read: ```java // null defaultScopeId — the no-scope tryClaim overload is exercised in its // own test below (DedupGuardNoScopeOverloadTest). ``` `DedupGuardNoScopeOverloadTest` does not exist anywhere in the repository (confirmed via Glob across the full checkout). The no-scope `tryClaim(String, Instant, String, int, long)` overload — which carries the R2F1 fix by calling `tryClaimInternal` instead of the public 6-arg overload — is entirely untested in both unit and integration tests. **Proposed fix:** add a `DedupGuardNoScopeOverloadTest` (or extend `DedupGuardTest`) covering at minimum: 1. No-scope `tryClaim` with a configured default scope delegates to `insertOrNothing` with that scope and returns the repository result correctly. 2. No-scope `tryClaim` with `null` (or blank) `defaultScopeId` throws `IllegalStateException` with a message mentioning `im2be.dedup.default-scope-id`. 3. Remove or correct the stale comment at line 56–57 regardless. #### **[INFO]** README public-API table incorrectly claims `@NotBlank` validation on `default-scope-id` _processed-kafka-events/README.md:52_ README.md line 52 states: ``` | `DedupProperties` | `im2be.dedup.*` config binding. `@NotBlank` validation on `default-scope-id` when enabled. | ``` `DedupProperties.java` has no `@NotBlank` (or any jakarta constraint) on `defaultScopeId` — the field defaults to `""` and is intentionally optional (the R1 fix removed the over-constraint). The README still advertises the removed constraint, misleading consumers into believing a misconfigured blank `default-scope-id` is caught at startup rather than at the first no-scope `tryClaim` call. **Proposed fix:** update line 52 to reflect the actual behaviour: ``` | `DedupProperties` | `im2be.dedup.*` config binding. `default-scope-id` is optional; when blank, the no-scope `tryClaim` overload throws `IllegalStateException` at runtime. | ``` ### Verdict **CONDITIONAL_APPROVE** --- <sub>hib-pr-reviewer • round 3 • 3 findings (2m/1i) • 2026-05-26T23:12:16.550Z → 2026-05-26T23:14:28.199Z • posted-as: pr-reviewer-bot</sub> </details>
fix(dedup): R4 — 3 R3 findings (deferred tracer Supplier + nested no-scope tests + README)
Some checks failed
im2be-platform-libs CI / mvn install (pull_request) Failing after 2s
im2be-platform-libs CI / mvn verify (main only) (pull_request) Has been skipped
c28328d2dc
Addresses PR-PLATFORM-2 R3 CONDITIONAL_APPROVE:

- MINOR: my R3 ObjectProvider<OpenTelemetry>.getIfAvailable() still
  resolved eagerly at bean-factory-method time. Real deferred resolution
  requires per-call lookup. Refactored: DedupGuard now takes
  Supplier<Tracer>; the autoconfig builds the supplier as a lambda that
  invokes otelProvider.getIfAvailable(noop).getTracer(...) ON EVERY
  tryClaim call. A late-registered OTel SDK becomes effective without
  bean re-creation. Removed the @Bean Tracer altogether.
- MINOR: added @Nested NoScopeOverloadTest class with 3 coverage cases:
  (a) null default scope throws IllegalStateException; (b) blank default
  same; (c) configured default delegates with the default scope id
  applied. Stale comment promising 'DedupGuardNoScopeOverloadTest'
  replaced with the inline @Nested class location.
- INFO: README public-API table for DedupProperties no longer claims
  @NotBlank — the constraint was removed in R2 when the field became
  optional (no-scope overload throws at runtime instead). Replaced with
  accurate description: 'default-scope-id is optional; @Validated is on
  the class for future jakarta constraints'.

Superseded by round 5.

Show previous round

hib-pr-reviewer review — PR #2 (affinity-intelligence-rework/im2be-platform-libs)

Round 4 — head c28328d2dc48, base main, trigger synchronize

TL;DR: CONDITIONAL_APPROVE — kept 2 minor findings (1 agreed A+B: stale GlobalOpenTelemetry Javadoc with broken @link; 1 unique-to-A verified: README mislabels DedupGuard as @Service); all 10 prior-round findings confirmed resolved.

Summary

Round 4 Arbitration

Prior findings (R1–R3, 10 total): Both A and B independently confirm all ten are resolved. Not re-raised.

Agreed finding (A+B): Both reviewers flag DedupAutoConfiguration.java Javadoc using {@link GlobalOpenTelemetry} as the OTel fallback. A cites line 27, B cites line 26. Read confirms the text lives at line 26. Grep confirms GlobalOpenTelemetry has zero import statements in the file — only io.opentelemetry.api.OpenTelemetry and io.opentelemetry.api.trace.Tracer are imported. The actual runtime fallback (line 111) is otelProvider.getIfAvailable(OpenTelemetry::noop). B's body is richer (broken @link reference + Java-Agent span-loss implication) and is used as the canonical text. Kept as minor.

Unique-to-A finding: README.md line 49 labels DedupGuard as @Service. Grep on DedupGuard.java for @Service|@Component returned no matches — the bean is registered exclusively through the @Bean factory in DedupAutoConfiguration. Finding verified. Kept as minor.

Final tally: A=2, B=1 → kept=2, dropped=0. Memora persistence attempted; server-side tag allowlist blocked creation (auto-assigned tag memora/issues is not in allowlist).

Blast Radius

Both findings are documentation-only defects in a new, self-contained autoconfiguration module. The GlobalOpenTelemetry Javadoc error has a latent behavioral implication (lost OTel spans for Java-Agent deployments) but requires no schema or runtime change to fix. The README label error is cosmetic. Neither touches shared infrastructure, exported contracts, or migration files.

BLAST_SCORE: 2/10

CI status (head c28328d2dc48)

Overall: ✗ failure

2 checks: 2 pending

Check State Link
im2be-platform-libs CI / mvn install (pull_request) pending details
im2be-platform-libs CI / mvn verify (main only) (pull_request) pending details

Findings (2)

processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupAutoConfiguration.java:26

Line 26 reads:

 *   <li>{@link Tracer} (fallback)  bound to {@link GlobalOpenTelemetry} when
 *       no application-supplied tracer is on the context</li>

GlobalOpenTelemetry is neither imported in this file (confirmed: no import for it; only io.opentelemetry.api.OpenTelemetry and io.opentelemetry.api.trace.Tracer are imported) nor referenced in the implementation. The actual deferred supplier at line 111 is:

() -> otelProvider.getIfAvailable(OpenTelemetry::noop)
        .getTracer("com.aim2be.platform.dedup");

Two correctness problems:

  1. {@link GlobalOpenTelemetry} is an unresolvable Javadoc reference — will emit a Javadoc warning/broken link.
  2. Services using the OTel Java Agent (which self-registers in GlobalOpenTelemetry, not as a Spring bean) will get OpenTelemetry.noop() from getIfAvailable(), silently discarding all im2be.dedup.try_claim spans.

Fix option A (accurate noop documentation — simplest):

 *   <li>{@link Tracer} (fallback)  a noop tracer when no
 *       {@link OpenTelemetry} bean is present in the application context.
 *       Services providing an OTel bean (e.g. via the Spring Boot OTel starter)
 *       get real spans; services relying solely on the OTel Java Agent
 *       (which registers in {@code GlobalOpenTelemetry}, not as a Spring bean)
 *       will not emit dedup spans.</li>

Fix option B (restore Java-Agent compatibility in the lambda):

final Supplier<Tracer> tracerSupplier =
    () -> otelProvider
            .getIfAvailable(GlobalOpenTelemetry::get)
            .getTracer("com.aim2be.platform.dedup");

Option B is safe inside the deferred lambda (the R2 bug was the eager snapshot, not GlobalOpenTelemetry per se) and should be preferred if any aim2be service uses the Java Agent.

[MINOR] README public-API table incorrectly labels DedupGuard as @Service

processed-kafka-events/README.md:49

Line 49 in the public-API table reads:

| `DedupGuard` | `@Service` — `tryClaim(...)` returns `true` iff this consumer won the race. |

DedupGuard carries no @Service (or @Component) annotation — confirmed by Grep on DedupGuard.java (zero matches for @Service|@Component). The bean is registered exclusively through the @Bean factory method in DedupAutoConfiguration. This distinction matters: consumers scanning for @Service beans to exclude or decorate will not find DedupGuard.

Fix:

| `DedupGuard` | Spring bean (registered via `DedupAutoConfiguration`) — `tryClaim(...)` returns `true` iff this consumer won the race. |

Verdict

CONDITIONAL_APPROVE


hib-pr-reviewer • round 4 • 2 findings (2m) • 2026-05-26T23:25:47.716Z → 2026-05-26T23:27:48.043Z • posted-as: pr-reviewer-bot

<!-- hib-pr-reviewer collapsed --> > _Superseded by round 5._ <details> <summary>Show previous round</summary> ## hib-pr-reviewer review — PR #2 (affinity-intelligence-rework/im2be-platform-libs) **Round 4** — head `c28328d2dc48`, base `main`, trigger `synchronize` **TL;DR:** CONDITIONAL_APPROVE — kept 2 minor findings (1 agreed A+B: stale GlobalOpenTelemetry Javadoc with broken @link; 1 unique-to-A verified: README mislabels DedupGuard as @Service); all 10 prior-round findings confirmed resolved. ### Summary ## Round 4 Arbitration **Prior findings (R1–R3, 10 total):** Both A and B independently confirm all ten are resolved. Not re-raised. **Agreed finding (A+B):** Both reviewers flag `DedupAutoConfiguration.java` Javadoc using `{@link GlobalOpenTelemetry}` as the OTel fallback. A cites line 27, B cites line 26. `Read` confirms the text lives at **line 26**. `Grep` confirms `GlobalOpenTelemetry` has zero import statements in the file — only `io.opentelemetry.api.OpenTelemetry` and `io.opentelemetry.api.trace.Tracer` are imported. The actual runtime fallback (line 111) is `otelProvider.getIfAvailable(OpenTelemetry::noop)`. B's body is richer (broken `@link` reference + Java-Agent span-loss implication) and is used as the canonical text. Kept as **minor**. **Unique-to-A finding:** README.md line 49 labels `DedupGuard` as `@Service`. `Grep` on `DedupGuard.java` for `@Service|@Component` returned no matches — the bean is registered exclusively through the `@Bean` factory in `DedupAutoConfiguration`. Finding verified. Kept as **minor**. **Final tally:** A=2, B=1 → kept=2, dropped=0. Memora persistence attempted; server-side tag allowlist blocked creation (auto-assigned tag `memora/issues` is not in allowlist). ### Blast Radius Both findings are documentation-only defects in a new, self-contained autoconfiguration module. The GlobalOpenTelemetry Javadoc error has a latent behavioral implication (lost OTel spans for Java-Agent deployments) but requires no schema or runtime change to fix. The README label error is cosmetic. Neither touches shared infrastructure, exported contracts, or migration files. **BLAST_SCORE: 2/10** ### CI status (head `c28328d2dc48`) **Overall: ✗ failure** 2 checks: 2 pending | Check | State | Link | |---|---|---| | im2be-platform-libs CI / mvn install (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/21/jobs/0) | | im2be-platform-libs CI / mvn verify (main only) (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/21/jobs/1) | ### Findings (2) #### **[MINOR]** Javadoc claims `GlobalOpenTelemetry` fallback but code uses `OpenTelemetry::noop` — broken `@link` + silent trace loss for Java-Agent deployments _processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupAutoConfiguration.java:26_ Line 26 reads: ```java * <li>{@link Tracer} (fallback) — bound to {@link GlobalOpenTelemetry} when * no application-supplied tracer is on the context</li> ``` `GlobalOpenTelemetry` is neither imported in this file (confirmed: no import for it; only `io.opentelemetry.api.OpenTelemetry` and `io.opentelemetry.api.trace.Tracer` are imported) nor referenced in the implementation. The actual deferred supplier at line 111 is: ```java () -> otelProvider.getIfAvailable(OpenTelemetry::noop) .getTracer("com.aim2be.platform.dedup"); ``` Two correctness problems: 1. `{@link GlobalOpenTelemetry}` is an unresolvable Javadoc reference — will emit a Javadoc warning/broken link. 2. Services using the OTel Java Agent (which self-registers in `GlobalOpenTelemetry`, not as a Spring bean) will get `OpenTelemetry.noop()` from `getIfAvailable()`, silently discarding all `im2be.dedup.try_claim` spans. **Fix option A** (accurate noop documentation — simplest): ```java * <li>{@link Tracer} (fallback) — a noop tracer when no * {@link OpenTelemetry} bean is present in the application context. * Services providing an OTel bean (e.g. via the Spring Boot OTel starter) * get real spans; services relying solely on the OTel Java Agent * (which registers in {@code GlobalOpenTelemetry}, not as a Spring bean) * will not emit dedup spans.</li> ``` **Fix option B** (restore Java-Agent compatibility in the lambda): ```java final Supplier<Tracer> tracerSupplier = () -> otelProvider .getIfAvailable(GlobalOpenTelemetry::get) .getTracer("com.aim2be.platform.dedup"); ``` Option B is safe inside the deferred lambda (the R2 bug was the *eager* snapshot, not `GlobalOpenTelemetry` per se) and should be preferred if any aim2be service uses the Java Agent. #### **[MINOR]** README public-API table incorrectly labels `DedupGuard` as `@Service` _processed-kafka-events/README.md:49_ Line 49 in the public-API table reads: ``` | `DedupGuard` | `@Service` — `tryClaim(...)` returns `true` iff this consumer won the race. | ``` `DedupGuard` carries **no** `@Service` (or `@Component`) annotation — confirmed by `Grep` on `DedupGuard.java` (zero matches for `@Service|@Component`). The bean is registered exclusively through the `@Bean` factory method in `DedupAutoConfiguration`. This distinction matters: consumers scanning for `@Service` beans to exclude or decorate will not find `DedupGuard`. **Fix:** ```markdown | `DedupGuard` | Spring bean (registered via `DedupAutoConfiguration`) — `tryClaim(...)` returns `true` iff this consumer won the race. | ``` ### Verdict **CONDITIONAL_APPROVE** --- <sub>hib-pr-reviewer • round 4 • 2 findings (2m) • 2026-05-26T23:25:47.716Z → 2026-05-26T23:27:48.043Z • posted-as: pr-reviewer-bot</sub> </details>
fix(dedup): R5 — GlobalOpenTelemetry middle-fallback (Java Agent support) + README label
Some checks failed
im2be-platform-libs CI / mvn install (pull_request) Failing after 2s
im2be-platform-libs CI / mvn verify (main only) (pull_request) Has been skipped
364bee6378
Addresses PR-PLATFORM-2 R4 CONDITIONAL_APPROVE:

- MINOR: tracer supplier fallback was OpenTelemetry::noop directly. Java
  Agent deployments register OTel via GlobalOpenTelemetry (NOT as a Spring
  bean), so my noop fallback silently emitted zero spans even when the
  agent was active. Added GlobalOpenTelemetry.get() as the middle step:
  Spring bean → GlobalOpenTelemetry → noop. Class Javadoc updated to
  reflect the 3-tier fallback order; import GlobalOpenTelemetry restored.
- MINOR: README public-API table no longer claims DedupGuard is
  @Service-annotated — the autoconfig wires it as a @Bean instead.

Superseded by round 6.

Show previous round

hib-pr-reviewer review — PR #2 (affinity-intelligence-rework/im2be-platform-libs)

Round 5 — head 364bee6378c6, base main, trigger synchronize

TL;DR: CONDITIONAL_APPROVE — kept 2 unique-to-one minor findings (both verified via Read); no blocking issues; all 12 prior-round findings confirmed resolved.

Summary

Arbiter reconciliation — Round 5

Both reviewers confirmed all 12 prior-round findings (R1–R4) are resolved. Each submitted exactly one new, unique-to-one minor finding.

Finding A (unique to A) — DedupAutoConfiguration.java:123 dead null guard on GlobalOpenTelemetry.get(): verified via Read at lines 121–125. Code reads return global != null ? global : OpenTelemetry.noop(); exactly as cited. The OTel Java API specifies GlobalOpenTelemetry.get() never returns null; the else branch is unreachable. Kept — severity minor.

Finding B (unique to B) — DedupGuardIT.java:56 floating postgres:15-alpine tag: verified via Read at line 56. new PostgreSQLContainer<>("postgres:15-alpine") confirmed exactly as cited; floating mutable tag can silently shift under CI. Kept — severity minor.

No agreed findings. No findings dropped. Memora persistence attempted; tool permissions not available in this session (fallback noted in arbiter log).

Blast Radius

The diff introduces a new Spring Boot autoconfiguration library module (processed-kafka-events) exported to all consuming services in the mono-repo. Changes span 15 files across production beans, JPA entities, a custom repository, Micrometer metrics, OTel tracing, test infrastructure, and README. Any misconfiguration in the autoconfiguration or entity model propagates to every consumer that opts in via im2be.dedup.enabled=true.

BLAST_SCORE: 5/10

Risk Indicators

Indicator Value
Sensitive functions DedupGuard.tryClaim, DedupGuard.tryClaimInternal, ProcessedKafkaEventRepository.insertOrNothing, DedupAutoConfiguration.dedupGuard
Migration touched yes
Test delta
Dependency changes

CI status (head 364bee6378c6)

Overall: ✗ failure

2 checks: 2 pending

Check State Link
im2be-platform-libs CI / mvn install (pull_request) pending details
im2be-platform-libs CI / mvn verify (main only) (pull_request) pending details

Findings (2)

[MINOR] GlobalOpenTelemetry.get() never returns null — null guard is dead code

processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupAutoConfiguration.java:123

Lines 121–125 read:

final OpenTelemetry otel = otelProvider.getIfAvailable(
    () -> {
        final OpenTelemetry global = GlobalOpenTelemetry.get();
        return global != null ? global : OpenTelemetry.noop(); // ← dead branch
    });

GlobalOpenTelemetry.get() is specified by the OTel Java API (io.opentelemetry:opentelemetry-api) to never return null — when no SDK or Java Agent has registered a global instance, the method returns its own OpenTelemetry.noop() singleton. The ternary's else branch is therefore unreachable and misleads readers into thinking the API can return null.

Simplify the factory to:

final OpenTelemetry otel = otelProvider.getIfAvailable(GlobalOpenTelemetry::get);

This is shorter, self-documents the three-tier intent (Spring-bean → GlobalOpenTelemetry.get() → noop via OTel's own fallback), and removes the misleading null check. Update the comment at line 117 accordingly: → noop (GlobalOpenTelemetry.get() returns its own noop when no SDK/Agent is registered).

[MINOR] Unpinned mutable Docker image tag postgres:15-alpine

processed-kafka-events/src/test/java/com/aim2be/platform/dedup/DedupGuardIT.java:56

new PostgreSQLContainer<>("postgres:15-alpine")

postgres:15-alpine is a floating tag: Docker Hub silently re-points it whenever a new 15.x patch ships. If the tag moves mid-sprint, CI may start failing (or, worse, pass on changed Postgres behaviour) without any change in this repository.

Proposed fix — pin to the specific patch that was verified when writing the IT:

new PostgreSQLContainer<>("postgres:15.7-alpine")

or, for stricter reproducibility, annotate with a digest comment:

new PostgreSQLContainer<>("postgres:15.7-alpine") // sha256:... at time of PR

This matches the Testcontainers community best-practice of immutable image references in test infrastructure.

Verdict

CONDITIONAL_APPROVE


hib-pr-reviewer • round 5 • 2 findings (2m) • 2026-05-26T23:35:28.319Z → 2026-05-26T23:36:43.807Z • posted-as: pr-reviewer-bot

<!-- hib-pr-reviewer collapsed --> > _Superseded by round 6._ <details> <summary>Show previous round</summary> ## hib-pr-reviewer review — PR #2 (affinity-intelligence-rework/im2be-platform-libs) **Round 5** — head `364bee6378c6`, base `main`, trigger `synchronize` **TL;DR:** CONDITIONAL_APPROVE — kept 2 unique-to-one minor findings (both verified via Read); no blocking issues; all 12 prior-round findings confirmed resolved. ### Summary ## Arbiter reconciliation — Round 5 Both reviewers confirmed all 12 prior-round findings (R1–R4) are resolved. Each submitted exactly one new, unique-to-one minor finding. **Finding A** (unique to A) — `DedupAutoConfiguration.java:123` dead null guard on `GlobalOpenTelemetry.get()`: verified via `Read` at lines 121–125. Code reads `return global != null ? global : OpenTelemetry.noop();` exactly as cited. The OTel Java API specifies `GlobalOpenTelemetry.get()` never returns `null`; the else branch is unreachable. **Kept** — severity minor. **Finding B** (unique to B) — `DedupGuardIT.java:56` floating `postgres:15-alpine` tag: verified via `Read` at line 56. `new PostgreSQLContainer<>("postgres:15-alpine")` confirmed exactly as cited; floating mutable tag can silently shift under CI. **Kept** — severity minor. No agreed findings. No findings dropped. Memora persistence attempted; tool permissions not available in this session (fallback noted in arbiter log). ### Blast Radius The diff introduces a new Spring Boot autoconfiguration library module (`processed-kafka-events`) exported to all consuming services in the mono-repo. Changes span 15 files across production beans, JPA entities, a custom repository, Micrometer metrics, OTel tracing, test infrastructure, and README. Any misconfiguration in the autoconfiguration or entity model propagates to every consumer that opts in via `im2be.dedup.enabled=true`. **BLAST_SCORE: 5/10** ### Risk Indicators | Indicator | Value | |---|---| | Sensitive functions | `DedupGuard.tryClaim`, `DedupGuard.tryClaimInternal`, `ProcessedKafkaEventRepository.insertOrNothing`, `DedupAutoConfiguration.dedupGuard` | | Migration touched | yes | | Test delta | — | | Dependency changes | — | ### CI status (head `364bee6378c6`) **Overall: ✗ failure** 2 checks: 2 pending | Check | State | Link | |---|---|---| | im2be-platform-libs CI / mvn install (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/23/jobs/0) | | im2be-platform-libs CI / mvn verify (main only) (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/23/jobs/1) | ### Findings (2) #### **[MINOR]** `GlobalOpenTelemetry.get()` never returns null — null guard is dead code _processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupAutoConfiguration.java:123_ Lines 121–125 read: ```java final OpenTelemetry otel = otelProvider.getIfAvailable( () -> { final OpenTelemetry global = GlobalOpenTelemetry.get(); return global != null ? global : OpenTelemetry.noop(); // ← dead branch }); ``` `GlobalOpenTelemetry.get()` is specified by the OTel Java API (`io.opentelemetry:opentelemetry-api`) to **never** return `null` — when no SDK or Java Agent has registered a global instance, the method returns its own `OpenTelemetry.noop()` singleton. The ternary's `else` branch is therefore unreachable and misleads readers into thinking the API can return null. Simplify the factory to: ```java final OpenTelemetry otel = otelProvider.getIfAvailable(GlobalOpenTelemetry::get); ``` This is shorter, self-documents the three-tier intent (Spring-bean → `GlobalOpenTelemetry.get()` → noop via OTel's own fallback), and removes the misleading null check. Update the comment at line 117 accordingly: `→ noop (GlobalOpenTelemetry.get() returns its own noop when no SDK/Agent is registered)`. #### **[MINOR]** Unpinned mutable Docker image tag `postgres:15-alpine` _processed-kafka-events/src/test/java/com/aim2be/platform/dedup/DedupGuardIT.java:56_ ```java new PostgreSQLContainer<>("postgres:15-alpine") ``` `postgres:15-alpine` is a floating tag: Docker Hub silently re-points it whenever a new 15.x patch ships. If the tag moves mid-sprint, CI may start failing (or, worse, pass on changed Postgres behaviour) without any change in this repository. **Proposed fix** — pin to the specific patch that was verified when writing the IT: ```java new PostgreSQLContainer<>("postgres:15.7-alpine") ``` or, for stricter reproducibility, annotate with a digest comment: ```java new PostgreSQLContainer<>("postgres:15.7-alpine") // sha256:... at time of PR ``` This matches the Testcontainers community best-practice of immutable image references in test infrastructure. ### Verdict **CONDITIONAL_APPROVE** --- <sub>hib-pr-reviewer • round 5 • 2 findings (2m) • 2026-05-26T23:35:28.319Z → 2026-05-26T23:36:43.807Z • posted-as: pr-reviewer-bot</sub> </details>
fix(dedup): R6 — remove dead null guard + pin Testcontainers PG image digest
Some checks failed
im2be-platform-libs CI / mvn install (pull_request) Failing after 3s
im2be-platform-libs CI / mvn verify (main only) (pull_request) Has been skipped
0434132367
Addresses PR-PLATFORM-2 R5 CONDITIONAL_APPROVE:

- MINOR: GlobalOpenTelemetry.get() never returns null (returns
  OpenTelemetry.noop() itself when unset per the OTel API contract). The
  null guard added in R5 was dead code; collapsed to a direct
  method-reference 'GlobalOpenTelemetry::get' with an inline comment
  explaining the no-null contract.
- MINOR: postgres:15-alpine was an unpinned mutable tag — a Docker Hub
  re-publish could swap the minor version mid-CI-run, masking schema-
  compat regressions. Pinned to digest @sha256:f9cbc6... with a comment
  documenting how to refresh the digest.

Superseded by round 7.

Show previous round

hib-pr-reviewer review — PR #2 (affinity-intelligence-rework/im2be-platform-libs)

Round 6 — head 043413236768, base main, trigger synchronize

TL;DR: CONDITIONAL_APPROVE — kept 2 minor unique-to-one findings (both code-verified); all 14 prior-round findings confirmed resolved.

Summary

Arbiter Reconciliation — Round 6

Memora context: No prior run history found (first time arbiter is invoked with Memora for this PR); Memora persistence calls failed due to tag allowlist/permission errors — noted but non-blocking for the arbitration.

Prior findings (R1–R5): Both reviewers independently confirmed all 14 prior-round findings are resolved. No prior findings re-raised.

New findings — A=1, B=1, both unique-to-one; both verified against HEAD.

Finding A — @Modifying insertOrNothing lacks @Transactional

Read ProcessedKafkaEventRepository.java lines 47–64: confirmed @Modifying + @Query with no @Transactional on the method. DedupGuard.java grep confirmed the production call at line 220 is always inside @Transactional(REQUIRES_NEW) — so the current path is safe. However any direct caller outside a write-transaction context (test utility, migration script, future caller) will inherit SimpleJpaRepository's class-level @Transactional(readOnly = true) and fail at the PostgreSQL driver level. Kept.

Finding B — No serialVersionUID on ProcessedKafkaEventId

Read ProcessedKafkaEventId.java lines 43–52: confirmed implements Serializable with no serialVersionUID field. Record body closes at line 52 with no static SVUID. The risk of SVUID drift on component rename (e.g. L2 cache, Infinispan) is real. Kept.

Outcome: 2 minor findings kept, 0 dropped, no blocking issues.

Blast Radius

All 15 changed files are confined to the processed-kafka-events submodule. The module is a library distributed to consuming services, so its exported surface (DedupGuard, ProcessedKafkaEventRepository) is used beyond this repo, but the blast radius of the two remaining minor issues is low — neither affects the primary call path.

BLAST_SCORE: 3/10

Risk Indicators

Indicator Value
Sensitive functions insertOrNothing, tryClaimInternal, tryClaim
Migration touched yes
Test delta
Dependency changes

CI status (head 043413236768)

Overall: ✗ failure

2 checks: 2 pending

Check State Link
im2be-platform-libs CI / mvn install (pull_request) pending details
im2be-platform-libs CI / mvn verify (main only) (pull_request) pending details

Findings (2)

[MINOR] @Modifying on insertOrNothing has no @Transactional — direct callers inherit read-only semantics

processed-kafka-events/src/main/java/com/aim2be/platform/dedup/ProcessedKafkaEventRepository.java:47

Lines 47–64 declare @Modifying + @Query with no @Transactional on the method. Spring Data JPA's SimpleJpaRepository carries a class-level @Transactional(readOnly = true); custom query methods without their own annotation inherit it.

Current production path is safe: DedupGuard.tryClaimInternal (called from both public overloads at lines 166 and 264 of DedupGuard.java, each annotated @Transactional(REQUIRES_NEW)) always provides a write transaction that insertOrNothing joins.

Risk for future callers: Any direct call to repository.insertOrNothing(...) made outside a write transaction — e.g., a test utility, an ad-hoc migration script, or a future service method — will execute inside readOnly = true. PostgreSQL rejects this at the driver level: ERROR: cannot execute INSERT in a read-only transaction.

Fix: add @Transactional (default REQUIRED propagation) to the method declaration:

@Transactional
@Modifying
@Query(
    value = "INSERT INTO processed_kafka_events ...",
    nativeQuery = true)
int insertOrNothing(...);

When called from the existing REQUIRES_NEW context the transaction join is idempotent; when called standalone a proper write transaction is created automatically.

[MINOR] No serialVersionUID on Serializable record used as JPA @EmbeddedId

processed-kafka-events/src/main/java/com/aim2be/platform/dedup/ProcessedKafkaEventId.java:51

Line 51: LocalDate weekStart) implements Serializable { — the record implements Serializable but declares no serialVersionUID. Java computes the SVUID from the class signature (component names + types).

If any component is later renamed (e.g. consumerScopeIdscopeId), the auto-computed SVUID silently changes. Any previously serialized instances — e.g. from a Hibernate L2 cache backed by Java serialization (EhCache, Infinispan), or a distributed session store — would throw java.io.InvalidClassException on deserialization.

ProcessedKafkaEventIdTest.serialization_roundTripsCleanly() validates the current round-trip but cannot catch SVUID drift across refactors.

Fix — records permit static fields:

public record ProcessedKafkaEventId(
        @Column(name = "consumer_scope_id", nullable = false, length = 128)
        String consumerScopeId,
        @Column(name = "event_id", nullable = false, length = 64)
        String eventId,
        @Column(name = "week_start", nullable = false)
        LocalDate weekStart) implements Serializable {

    private static final long serialVersionUID = 1L;
}

This pins the contract at v1 — any future breaking structural change should increment it.

Verdict

CONDITIONAL_APPROVE


hib-pr-reviewer • round 6 • 2 findings (2m) • 2026-05-26T23:52:48.400Z → 2026-05-26T23:55:03.791Z • posted-as: pr-reviewer-bot

<!-- hib-pr-reviewer collapsed --> > _Superseded by round 7._ <details> <summary>Show previous round</summary> ## hib-pr-reviewer review — PR #2 (affinity-intelligence-rework/im2be-platform-libs) **Round 6** — head `043413236768`, base `main`, trigger `synchronize` **TL;DR:** CONDITIONAL_APPROVE — kept 2 minor unique-to-one findings (both code-verified); all 14 prior-round findings confirmed resolved. ### Summary ## Arbiter Reconciliation — Round 6 **Memora context**: No prior run history found (first time arbiter is invoked with Memora for this PR); Memora persistence calls failed due to tag allowlist/permission errors — noted but non-blocking for the arbitration. **Prior findings (R1–R5)**: Both reviewers independently confirmed all 14 prior-round findings are resolved. No prior findings re-raised. **New findings — A=1, B=1, both unique-to-one; both verified against HEAD.** ### Finding A — `@Modifying insertOrNothing` lacks `@Transactional` Read `ProcessedKafkaEventRepository.java` lines 47–64: confirmed `@Modifying` + `@Query` with no `@Transactional` on the method. `DedupGuard.java` grep confirmed the production call at line 220 is always inside `@Transactional(REQUIRES_NEW)` — so the current path is safe. However any direct caller outside a write-transaction context (test utility, migration script, future caller) will inherit `SimpleJpaRepository`'s class-level `@Transactional(readOnly = true)` and fail at the PostgreSQL driver level. **Kept.** ### Finding B — No `serialVersionUID` on `ProcessedKafkaEventId` Read `ProcessedKafkaEventId.java` lines 43–52: confirmed `implements Serializable` with no `serialVersionUID` field. Record body closes at line 52 with no static SVUID. The risk of SVUID drift on component rename (e.g. L2 cache, Infinispan) is real. **Kept.** **Outcome**: 2 minor findings kept, 0 dropped, no blocking issues. ### Blast Radius All 15 changed files are confined to the `processed-kafka-events` submodule. The module is a library distributed to consuming services, so its exported surface (DedupGuard, ProcessedKafkaEventRepository) is used beyond this repo, but the blast radius of the two remaining minor issues is low — neither affects the primary call path. **BLAST_SCORE: 3/10** ### Risk Indicators | Indicator | Value | |---|---| | Sensitive functions | `insertOrNothing`, `tryClaimInternal`, `tryClaim` | | Migration touched | yes | | Test delta | — | | Dependency changes | — | ### CI status (head `043413236768`) **Overall: ✗ failure** 2 checks: 2 pending | Check | State | Link | |---|---|---| | im2be-platform-libs CI / mvn install (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/25/jobs/0) | | im2be-platform-libs CI / mvn verify (main only) (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/25/jobs/1) | ### Findings (2) #### **[MINOR]** `@Modifying` on `insertOrNothing` has no `@Transactional` — direct callers inherit read-only semantics _processed-kafka-events/src/main/java/com/aim2be/platform/dedup/ProcessedKafkaEventRepository.java:47_ Lines 47–64 declare `@Modifying` + `@Query` with no `@Transactional` on the method. Spring Data JPA's `SimpleJpaRepository` carries a class-level `@Transactional(readOnly = true)`; custom query methods without their own annotation inherit it. **Current production path is safe**: `DedupGuard.tryClaimInternal` (called from both public overloads at lines 166 and 264 of `DedupGuard.java`, each annotated `@Transactional(REQUIRES_NEW)`) always provides a write transaction that `insertOrNothing` joins. **Risk for future callers**: Any direct call to `repository.insertOrNothing(...)` made outside a write transaction — e.g., a test utility, an ad-hoc migration script, or a future service method — will execute inside `readOnly = true`. PostgreSQL rejects this at the driver level: `ERROR: cannot execute INSERT in a read-only transaction`. **Fix**: add `@Transactional` (default `REQUIRED` propagation) to the method declaration: ```java @Transactional @Modifying @Query( value = "INSERT INTO processed_kafka_events ...", nativeQuery = true) int insertOrNothing(...); ``` When called from the existing `REQUIRES_NEW` context the transaction join is idempotent; when called standalone a proper write transaction is created automatically. #### **[MINOR]** No `serialVersionUID` on `Serializable` record used as JPA `@EmbeddedId` _processed-kafka-events/src/main/java/com/aim2be/platform/dedup/ProcessedKafkaEventId.java:51_ Line 51: `LocalDate weekStart) implements Serializable {` — the record implements `Serializable` but declares no `serialVersionUID`. Java computes the SVUID from the class signature (component names + types). If any component is later renamed (e.g. `consumerScopeId` → `scopeId`), the auto-computed SVUID silently changes. Any previously serialized instances — e.g. from a Hibernate L2 cache backed by Java serialization (EhCache, Infinispan), or a distributed session store — would throw `java.io.InvalidClassException` on deserialization. `ProcessedKafkaEventIdTest.serialization_roundTripsCleanly()` validates the current round-trip but cannot catch SVUID drift across refactors. **Fix** — records permit static fields: ```java public record ProcessedKafkaEventId( @Column(name = "consumer_scope_id", nullable = false, length = 128) String consumerScopeId, @Column(name = "event_id", nullable = false, length = 64) String eventId, @Column(name = "week_start", nullable = false) LocalDate weekStart) implements Serializable { private static final long serialVersionUID = 1L; } ``` This pins the contract at v1 — any future breaking structural change should increment it. ### Verdict **CONDITIONAL_APPROVE** --- <sub>hib-pr-reviewer • round 6 • 2 findings (2m) • 2026-05-26T23:52:48.400Z → 2026-05-26T23:55:03.791Z • posted-as: pr-reviewer-bot</sub> </details>
fix(dedup): R7 — @Transactional on insertOrNothing + serialVersionUID on @EmbeddedId
Some checks failed
im2be-platform-libs CI / mvn install (pull_request) Failing after 2s
im2be-platform-libs CI / mvn verify (main only) (pull_request) Has been skipped
114ae20802
Addresses PR-PLATFORM-2 R6 CONDITIONAL_APPROVE:

- MINOR: @Modifying insertOrNothing now also carries @Transactional so
  direct callers (test fixtures bypassing DedupGuard.tryClaim) don't
  inherit read-only semantics from a surrounding Spring data-jpa
  read-only-by-default proxy. DedupGuard's tryClaim already wraps with
  @Transactional(REQUIRES_NEW) so the production path was correct, but
  the repository method is now safe to invoke standalone.
- MINOR: ProcessedKafkaEventId record now declares serialVersionUID=1L
  explicitly. Auto-generated record UIDs are JVM-implementation-defined
  and would change with compiler/JVM upgrades — Hibernate L2 + Redis-
  backed JPA second-level caches sometimes serialise composite keys, and
  an InvalidClassException on a cache hit after JVM upgrade is the
  failure mode pinning serialVersionUID prevents.

Superseded by round 8.

Show previous round

hib-pr-reviewer review — PR #2 (affinity-intelligence-rework/im2be-platform-libs)

Round 7 — head 114ae2080225, base main, trigger synchronize

TL;DR: CONDITIONAL_APPROVE — kept 1 unique-to-A minor finding (verified at HEAD); B had 0 findings; all 16 prior-round issues confirmed resolved.

Summary

Arbitration — Round 7

Memora recall: No prior run history found in store (create-new path attempted; MCP tag-allowlist error prevented persistence — server auto-assigns memora/issues which is not in the allowlist). Proceeding to arbitration.

Finding disposition:

Finding Reviewer Action
Gauge supplier lambda held via WeakReference — NaN after GC A only VERIFIED & KEPT

Verification of unique-to-A finding: Read DedupMetricsBinder.java at HEAD. Line 104-109 confirms:

activePartitionGauges.computeIfAbsent(consumerScopeId, scope ->
        Gauge.builder(GAUGE_NAME, () -> currentWeekCount(scope))
                .description("dedup row count for the current week per scope")
                .tag(TAG_SCOPE, scope)
                .register(registry));

The two-argument Gauge.builder(String, Supplier<Number>) overload makes the supplier the state object, held by DefaultGauge via WeakReference. The activePartitionGauges map stores the Gauge instance, not the supplier — so the lambda () -> currentWeekCount(scope) has no surviving strong reference and is eligible for GC. Post-collection the gauge silently returns NaN. Reviewer A's fix (use the three-argument form with this as the strongly-reachable state object) is the canonical Micrometer pattern for long-lived gauges. Finding is grounded and kept.

Reviewer B returned zero findings — divergence is genuine, not a formatting artifact.

Prior-round closures: All 16 findings from rounds 1–6 were confirmed resolved by the round-7 reviewers and are not re-raised here.

Blast Radius

The affected code is confined to DedupMetricsBinder, a single metrics-binding bean within the processed-kafka-events module. The bug silently degrades observability (gauge reads NaN) but does not affect core dedup correctness, the compound-PK claim logic, or any downstream consumer. Impact is limited to Prometheus/Grafana dashboards relying on im2be_dedup_active_partitions_total.

BLAST_SCORE: 3/10

Risk Indicators

Indicator Value
Sensitive functions
Migration touched yes
Test delta
Dependency changes

CI status (head 114ae2080225)

Overall: ✗ failure

2 checks: 2 pending

Check State Link
im2be-platform-libs CI / mvn install (pull_request) pending details
im2be-platform-libs CI / mvn verify (main only) (pull_request) pending details

Findings (1)

[MINOR] Gauge supplier lambda held only via WeakReference — silently emits NaN after GC

processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupMetricsBinder.java:105

Gauge.builder(GAUGE_NAME, () -> currentWeekCount(scope))

Micrometer's Gauge.builder(String, Supplier<Number>) two-argument overload makes the supplier the state object (T), held by DefaultGauge via new WeakReference<>(supplier). The activePartitionGauges map stores the returned Gauge instance, not the supplier — so the lambda () -> currentWeekCount(scope) has no other strong reference. After any GC cycle the weak reference is cleared and DefaultGauge.value() returns Double.NaN silently.

Fix — use the three-argument form that puts this (the long-lived Spring bean) as the weakly-referenced state object; the supplier is then a strongly-held ToDoubleFunction:

activePartitionGauges.computeIfAbsent(consumerScopeId, scope ->
        Gauge.builder(GAUGE_NAME, this,
                binder -> binder.currentWeekCount(scope))
                .description("dedup row count for the current week per scope")
                .tag(TAG_SCOPE, scope)
                .register(registry));

The bean singleton is reachable for the application's lifetime, so the gauge will never spuriously return NaN. The scope string is captured strongly by the state-function lambda (held by the DefaultGauge).

Verdict

CONDITIONAL_APPROVE


hib-pr-reviewer • round 7 • 1 finding (1m) • 2026-05-27T00:02:27.782Z → 2026-05-27T00:03:54.642Z • posted-as: pr-reviewer-bot

<!-- hib-pr-reviewer collapsed --> > _Superseded by round 8._ <details> <summary>Show previous round</summary> ## hib-pr-reviewer review — PR #2 (affinity-intelligence-rework/im2be-platform-libs) **Round 7** — head `114ae2080225`, base `main`, trigger `synchronize` **TL;DR:** CONDITIONAL_APPROVE — kept 1 unique-to-A minor finding (verified at HEAD); B had 0 findings; all 16 prior-round issues confirmed resolved. ### Summary ## Arbitration — Round 7 **Memora recall:** No prior run history found in store (create-new path attempted; MCP tag-allowlist error prevented persistence — server auto-assigns `memora/issues` which is not in the allowlist). Proceeding to arbitration. **Finding disposition:** | Finding | Reviewer | Action | |---|---|---| | Gauge supplier lambda held via WeakReference — NaN after GC | A only | **VERIFIED & KEPT** | **Verification of unique-to-A finding:** Read `DedupMetricsBinder.java` at HEAD. Line 104-109 confirms: ```java activePartitionGauges.computeIfAbsent(consumerScopeId, scope -> Gauge.builder(GAUGE_NAME, () -> currentWeekCount(scope)) .description("dedup row count for the current week per scope") .tag(TAG_SCOPE, scope) .register(registry)); ``` The two-argument `Gauge.builder(String, Supplier<Number>)` overload makes the supplier the state object, held by `DefaultGauge` via `WeakReference`. The `activePartitionGauges` map stores the `Gauge` instance, **not** the supplier — so the lambda `() -> currentWeekCount(scope)` has no surviving strong reference and is eligible for GC. Post-collection the gauge silently returns `NaN`. Reviewer A's fix (use the three-argument form with `this` as the strongly-reachable state object) is the canonical Micrometer pattern for long-lived gauges. Finding is grounded and kept. **Reviewer B** returned zero findings — divergence is genuine, not a formatting artifact. **Prior-round closures:** All 16 findings from rounds 1–6 were confirmed resolved by the round-7 reviewers and are not re-raised here. ### Blast Radius The affected code is confined to `DedupMetricsBinder`, a single metrics-binding bean within the `processed-kafka-events` module. The bug silently degrades observability (gauge reads NaN) but does not affect core dedup correctness, the compound-PK claim logic, or any downstream consumer. Impact is limited to Prometheus/Grafana dashboards relying on `im2be_dedup_active_partitions_total`. **BLAST_SCORE: 3/10** ### Risk Indicators | Indicator | Value | |---|---| | Sensitive functions | — | | Migration touched | yes | | Test delta | — | | Dependency changes | — | ### CI status (head `114ae2080225`) **Overall: ✗ failure** 2 checks: 2 pending | Check | State | Link | |---|---|---| | im2be-platform-libs CI / mvn install (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/27/jobs/0) | | im2be-platform-libs CI / mvn verify (main only) (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/27/jobs/1) | ### Findings (1) #### **[MINOR]** Gauge supplier lambda held only via WeakReference — silently emits NaN after GC _processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupMetricsBinder.java:105_ ```java Gauge.builder(GAUGE_NAME, () -> currentWeekCount(scope)) ``` Micrometer's `Gauge.builder(String, Supplier<Number>)` two-argument overload makes the supplier the *state object* (`T`), held by `DefaultGauge` via `new WeakReference<>(supplier)`. The `activePartitionGauges` map stores the returned `Gauge` instance, not the supplier — so the lambda `() -> currentWeekCount(scope)` has no other strong reference. After any GC cycle the weak reference is cleared and `DefaultGauge.value()` returns `Double.NaN` silently. **Fix** — use the three-argument form that puts `this` (the long-lived Spring bean) as the weakly-referenced state object; the supplier is then a strongly-held `ToDoubleFunction`: ```java activePartitionGauges.computeIfAbsent(consumerScopeId, scope -> Gauge.builder(GAUGE_NAME, this, binder -> binder.currentWeekCount(scope)) .description("dedup row count for the current week per scope") .tag(TAG_SCOPE, scope) .register(registry)); ``` The bean singleton is reachable for the application's lifetime, so the gauge will never spuriously return `NaN`. The `scope` string is captured strongly by the state-function lambda (held by the `DefaultGauge`). ### Verdict **CONDITIONAL_APPROVE** --- <sub>hib-pr-reviewer • round 7 • 1 finding (1m) • 2026-05-27T00:02:27.782Z → 2026-05-27T00:03:54.642Z • posted-as: pr-reviewer-bot</sub> </details>
fix(dedup): R8 — gauge supplier strong-reference fix (R7 MINOR)
Some checks failed
im2be-platform-libs CI / mvn install (pull_request) Failing after 2s
im2be-platform-libs CI / mvn verify (main only) (pull_request) Has been skipped
dc557d7509
Gauge.builder(name, Supplier) holds the supplier weakly. Inline-lambda
suppliers with no surviving strong reference get GC'd, after which the
gauge silently emits NaN. Switched to the (name, obj, ToDoubleFunction)
overload passing 'this' (the singleton @Bean) — Spring's context keeps
the binder strongly referenced for the application lifetime, so the
gauge stays live.

Superseded by round 9.

Show previous round

hib-pr-reviewer review — PR #2 (affinity-intelligence-rework/im2be-platform-libs)

Round 8 — head dc557d750900, base main, trigger synchronize

TL;DR: CONDITIONAL_APPROVE — 1 minor finding kept this round.

Summary

Arbiter reconciled 1 (A) + 0 (B) → 1 findings.

CI status (head dc557d750900)

Overall: ✗ failure

2 checks: 2 pending

Check State Link
im2be-platform-libs CI / mvn install (pull_request) pending details
im2be-platform-libs CI / mvn verify (main only) (pull_request) pending details

Findings (1)

processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupGuard.java:246

Lines 246–247:

 * No-scope convenience overload that delegates to
 * {@link #tryClaim(String, String, Instant, String, int, long)} using the

The {@link} resolves to the public 6-arg tryClaim overload. But line 281:

return tryClaimInternal(defaultScopeId, eventId, eventTime, topic, partition, offset);

…and the accompanying inline comment (lines 276–280) explicitly confirms the method calls tryClaimInternal, NOT the public overload, specifically to avoid self-invocation proxy bypass (@Transactional R2 fix).

A reader following the {@link} will believe a public→public delegation occurs and may incorrectly conclude the R2 proxy-bypass regression is still present. The tryClaimInternal Javadoc at lines 177–184 further contradicts the stale link.

Proposed fix — first paragraph of the no-scope overload Javadoc:

 * No-scope convenience overload that delegates to the private
 * {@code tryClaimInternal} core (NOT the public explicit-scope overload;
 * publicpublic self-invocation would bypass the {@code @Transactional}
 * proxy  see R2 MAJOR fix) using the {@code defaultScopeId} configured
 * at construction time (typically bound from
 * {@code im2be.dedup.default-scope-id}).

Verdict

CONDITIONAL_APPROVE


hib-pr-reviewer • round 8 • 1 finding (1m) • 2026-05-27T00:14:15.611Z → 2026-05-27T00:15:42.846Z • posted-as: pr-reviewer-bot • [bookkeeping fallback]

<!-- hib-pr-reviewer collapsed --> > _Superseded by round 9._ <details> <summary>Show previous round</summary> ## hib-pr-reviewer review — PR #2 (affinity-intelligence-rework/im2be-platform-libs) **Round 8** — head `dc557d750900`, base `main`, trigger `synchronize` **TL;DR:** CONDITIONAL_APPROVE — 1 minor finding kept this round. ### Summary Arbiter reconciled 1 (A) + 0 (B) → 1 findings. ### CI status (head `dc557d750900`) **Overall: ✗ failure** 2 checks: 2 pending | Check | State | Link | |---|---|---| | im2be-platform-libs CI / mvn install (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/30/jobs/0) | | im2be-platform-libs CI / mvn verify (main only) (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/30/jobs/1) | ### Findings (1) #### **[MINOR]** No-scope `tryClaim` Javadoc `{@link}` is stale — points to public explicit-scope overload, but implementation calls private `tryClaimInternal` _processed-kafka-events/src/main/java/com/aim2be/platform/dedup/DedupGuard.java:246_ Lines 246–247: ```java * No-scope convenience overload that delegates to * {@link #tryClaim(String, String, Instant, String, int, long)} using the ``` The `{@link}` resolves to the public 6-arg `tryClaim` overload. But line 281: ```java return tryClaimInternal(defaultScopeId, eventId, eventTime, topic, partition, offset); ``` …and the accompanying inline comment (lines 276–280) explicitly confirms the method calls `tryClaimInternal`, NOT the public overload, specifically to avoid self-invocation proxy bypass (`@Transactional` R2 fix). A reader following the `{@link}` will believe a public→public delegation occurs and may incorrectly conclude the R2 proxy-bypass regression is still present. The `tryClaimInternal` Javadoc at lines 177–184 further contradicts the stale link. **Proposed fix — first paragraph of the no-scope overload Javadoc:** ```java * No-scope convenience overload that delegates to the private * {@code tryClaimInternal} core (NOT the public explicit-scope overload; * public→public self-invocation would bypass the {@code @Transactional} * proxy — see R2 MAJOR fix) using the {@code defaultScopeId} configured * at construction time (typically bound from * {@code im2be.dedup.default-scope-id}). ``` ### Verdict **CONDITIONAL_APPROVE** --- <sub>hib-pr-reviewer • round 8 • 1 finding (1m) • 2026-05-27T00:14:15.611Z → 2026-05-27T00:15:42.846Z • posted-as: pr-reviewer-bot • [bookkeeping fallback]</sub> </details>
fix(dedup): R9 — clarify no-scope tryClaim Javadoc references private core (R8 MINOR)
Some checks failed
im2be-platform-libs CI / mvn install (pull_request) Failing after 2s
im2be-platform-libs CI / mvn verify (main only) (pull_request) Has been skipped
7058c52cd7
The R5 fix routed both public tryClaim overloads through the private
tryClaimInternal core to avoid the @Transactional self-invocation gotcha,
but the no-scope overload's Javadoc still said it 'delegates to' the
explicit-scope overload via @link. Reworded to point at tryClaimInternal
+ explain why the indirection exists (proxy semantics).

hib-pr-reviewer review — PR #2 (affinity-intelligence-rework/im2be-platform-libs)

Round 9 — head 7058c52cd7e5, base main, trigger synchronize

TL;DR: CONDITIONAL_APPROVE — kept 1 verified INFO finding (README test count 20 vs actual 23); all 18 prior R1–R8 findings confirmed resolved, no blocking issues.

Summary

Arbitration Round 9

Reconciliation actions:

  • Reviewer A submitted 1 INFO finding; Reviewer B submitted 0 findings.
  • Unique-to-A finding verified via Read (README line 188 confirmed: unit tests (4 classes, 20 tests)) and Grep (actual @Test counts per file).
  • The nested NoScopeOverloadTest class in DedupGuardTest.java uses fully-qualified @org.junit.jupiter.api.Test annotations (not just @Test), so a simple @Test grep returns only 7 outer tests. With the 3 nested tests included, the true unit-test count is 23 — confirming the README undercount of 3. Finding kept.
  • All 18 prior R1–R8 findings independently confirmed resolved by both reviewers. No re-raises.
  • Memora persistence attempted; tag-allowlist error on server prevented write (not a findings-affecting issue).

Kept: 1 (INFO). Dropped: 0.

Blast Radius

The only actionable finding is a documentation string in README.md. The diff itself touches a single new submodule (processed-kafka-events) with no cross-module API surface changes. Blast radius is minimal — limited to consumers of this library's documentation.

BLAST_SCORE: 1/10

Risk Indicators

Indicator Value
Sensitive functions
Migration touched yes
Test delta
Dependency changes

CI status (head 7058c52cd7e5)

Overall: ✗ failure

2 checks: 2 pending

Check State Link
im2be-platform-libs CI / mvn install (pull_request) pending details
im2be-platform-libs CI / mvn verify (main only) (pull_request) pending details

Findings (1)

[INFO] README unit-test count is off by 3 (says 20, actual 23)

processed-kafka-events/README.md:188

Line 188: mvn … test # unit tests (4 classes, 20 tests) under-counts by 3.

Verified test counts at HEAD:

  • DedupGuardTest outer class: 7 @Test methods (lines 113–215)
  • DedupGuardTest.NoScopeOverloadTest nested class: 3 @org.junit.jupiter.api.Test methods (lines 71, 84, 96) — these use the fully-qualified annotation and are missed by a plain @Test grep, which is why the discrepancy is easy to overlook.
  • DedupAutoConfigurationTest: 3 @Test methods
  • ProcessedKafkaEventIdTest: 5 @Test methods
  • WeekStartDerivationTest: 5 @Test methods

Total = 23. Change 20 tests23 tests to keep the quick-start accurate.

Verdict

CONDITIONAL_APPROVE


hib-pr-reviewer • round 9 • 1 finding (1i) • 2026-05-27T00:21:56.093Z → 2026-05-27T00:24:08.337Z • posted-as: pr-reviewer-bot

## hib-pr-reviewer review — PR #2 (affinity-intelligence-rework/im2be-platform-libs) **Round 9** — head `7058c52cd7e5`, base `main`, trigger `synchronize` **TL;DR:** CONDITIONAL_APPROVE — kept 1 verified INFO finding (README test count 20 vs actual 23); all 18 prior R1–R8 findings confirmed resolved, no blocking issues. ### Summary ## Arbitration Round 9 **Reconciliation actions:** - Reviewer A submitted 1 INFO finding; Reviewer B submitted 0 findings. - Unique-to-A finding verified via `Read` (README line 188 confirmed: `unit tests (4 classes, 20 tests)`) and `Grep` (actual `@Test` counts per file). - The nested `NoScopeOverloadTest` class in `DedupGuardTest.java` uses fully-qualified `@org.junit.jupiter.api.Test` annotations (not just `@Test`), so a simple `@Test` grep returns only 7 outer tests. With the 3 nested tests included, the true unit-test count is 23 — confirming the README undercount of 3. Finding **kept**. - All 18 prior R1–R8 findings independently confirmed resolved by both reviewers. No re-raises. - Memora persistence attempted; tag-allowlist error on server prevented write (not a findings-affecting issue). **Kept:** 1 (INFO). **Dropped:** 0. ### Blast Radius The only actionable finding is a documentation string in README.md. The diff itself touches a single new submodule (processed-kafka-events) with no cross-module API surface changes. Blast radius is minimal — limited to consumers of this library's documentation. **BLAST_SCORE: 1/10** ### Risk Indicators | Indicator | Value | |---|---| | Sensitive functions | — | | Migration touched | yes | | Test delta | — | | Dependency changes | — | ### CI status (head `7058c52cd7e5`) **Overall: ✗ failure** 2 checks: 2 pending | Check | State | Link | |---|---|---| | im2be-platform-libs CI / mvn install (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/33/jobs/0) | | im2be-platform-libs CI / mvn verify (main only) (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/33/jobs/1) | ### Findings (1) #### **[INFO]** README unit-test count is off by 3 (says 20, actual 23) _processed-kafka-events/README.md:188_ Line 188: `mvn … test # unit tests (4 classes, 20 tests)` under-counts by 3. Verified test counts at HEAD: - `DedupGuardTest` outer class: **7** `@Test` methods (lines 113–215) - `DedupGuardTest.NoScopeOverloadTest` nested class: **3** `@org.junit.jupiter.api.Test` methods (lines 71, 84, 96) — these use the fully-qualified annotation and are missed by a plain `@Test` grep, which is why the discrepancy is easy to overlook. - `DedupAutoConfigurationTest`: **3** `@Test` methods - `ProcessedKafkaEventIdTest`: **5** `@Test` methods - `WeekStartDerivationTest`: **5** `@Test` methods Total = **23**. Change `20 tests` → `23 tests` to keep the quick-start accurate. ### Verdict **CONDITIONAL_APPROVE** --- <sub>hib-pr-reviewer • round 9 • 1 finding (1i) • 2026-05-27T00:21:56.093Z → 2026-05-27T00:24:08.337Z • posted-as: pr-reviewer-bot</sub>
fix(dedup): R10 — correct README unit-test count (20 → 23) (R9 INFO)
Some checks failed
im2be-platform-libs CI / mvn install (pull_request) Failing after 2s
im2be-platform-libs CI / mvn verify (main only) (pull_request) Has been skipped
eab01f43bc
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
affinity-intelligence-rework/im2be-platform-libs!2
No description provided.