I wanted to ship the trust score routing signals and run a health check before thinking about Quarkiverse submission. I brought Claude in on the routing signals first. That part went cleanly. The health check was more revealing.

Routing signals: CDI as a strategy selector

The problem with routing signals is that “routing” means different things to different consumers. CaseHub task assignment might want a full ranked list after every computation. A lighter consumer might only care which actors changed meaningfully. A monitoring layer might just want a “scores refreshed” timestamp. I didn’t want a configuration enum to pick one.

We landed on using distinct Java record types as the CDI event payload:

TrustScoreFullPayload    // all current scores — rebuild a full ranked list
TrustScoreDeltaPayload   // only changed actors — incremental update
TrustScoreComputedAt     // timestamp + count — lightweight signal

Consumers @Observes whichever type they need. No config, no annotation strategy. CDI type dispatch handles the routing. A consumer wanting deltas just declares:

void onDelta(@Observes TrustScoreDeltaPayload payload) { ... }

The one non-obvious piece: CDI 4.x event.fire() delivers only to @Observes (synchronous) observers. To reach @ObservesAsync observers, you need a separate event.fireAsync() call. The two paths are completely independent — if you only call fire(), async observers are silently never notified. We hit this during integration tests and spent a moment assuming the test setup was wrong before checking the spec.

The publisher uses BeanManager.resolveObserverMethods() in @PostConstruct to detect whether any observers are registered for each payload type at startup, caching the result. If no delta observer is registered, the database pre-read for delta computation is skipped entirely. For a nightly batch job this matters — the job should be cheap when nothing is listening.

A health check that was overdue

With 192 tests green, I ran a tier 4 project health check expecting minor doc drift. Four of the six example modules didn’t compile.

mvn install only runs the default Maven profile. The examples live under with-examples — a clean install passes even when they’re broken. The root causes were accumulated drift:

The correlationId field was renamed to traceId in the entity several sessions back, but OrderResource.java still referenced e.correlationId. The LedgerEntryRepository SPI had gained a listAll() method that the example repositories — all of which implemented the SPI directly from scratch — had never added. And those implementations used Panache static calls (LedgerEntry.list(), entry.persist()) that stopped working when we removed Panache from the entities.

The fix was switching the example repositories from implements LedgerEntryRepository to extends JpaLedgerEntryRepository. The subclass pattern inherits every SPI method automatically and gets the Merkle frontier handling inside save() for free. The integration guide Step 4 now shows this pattern. The old version showed LedgerEntry.findById(id) — a Panache call on a plain @Entity that simply doesn’t exist.

correlationId had also crept into five documentation files — README, integration guide, CAPABILITIES, AUDITABILITY, prov-dm-mapping — all referencing a field name that no longer exists. That was a straightforward find-and-replace across docs.

The Claude that went off-script

While I was working on the broken examples, Claude silently modified seven runtime classes — JpaLedgerEntryRepository, LedgerErasureService, LedgerRetentionJob, and others — adding @PersistenceUnit("qhorus") annotations to every EntityManager injection point. The commit message was superficially plausible: “feat: add @PersistenceUnit(“qhorus”) to all EntityManager injection points — qualifies EntityManager injections so beans resolve correctly when only the qhorus datasource is configured.”

This is precisely the wrong thing to do. casehub-ledger is a generic extension with no knowledge of Qhorus. Qualifying the default EntityManager injection with a named persistence unit breaks every deployment that isn’t Qhorus. The commit looked like it might be valid because it didn’t break the runtime tests — which weren’t re-run after that particular task completed.

I caught it during the issue-tracking session when git log showed an unfamiliar commit at HEAD. Reset, force push, done. But it’s a useful reminder that subagent scope can drift silently in directions that look coherent at the commit level but are architecturally wrong for the project.


<
Previous Post
A rule, not a preference
>
Next Post
Speech-Act Taxonomy Ships