Notes | CaseHub - Work
The Producer That Forgot Its Own Interfaces
When I filed #267 during the MongoDB completion work, the shape of the bug was already clear: CrossTenantProducer hardcodes three JPA concrete types. The cross-tenant path — cleanup jobs, timer rec...
Things in the wrong place
A session spent finding things that weren’t where they were supposed to be — and putting one of them somewhere it also shouldn’t have been.
Filtering Out the Enum
Filtering Out the Enum
The Enum That Was Always a Path
The Enum That Was Always a Path
Every Query Is a Question of Trust
I’d been putting off multi-tenancy. Not because it was hard to understand — add a
column, filter by it — but because “add a column” in a system with 20+ stores across
three persistence backends mea...
The module that was never just for testing
Five in-memory store implementations have lived in casehub-work-testing since
the project’s first weeks. The name said test artifact. The CDI annotation —
@Alternative @Priority(1) — said Tier 2 ov...
Reviewing your way to fewer surprises
The last two issues on my S-scale backlog — conditional outcomes (#177) and
JSON Merge Patch for templates (#199) — went through four rounds of spec review
before a line of code was written. I went...
Delegation API debt from #245
CI had been red on casehubio/work for a while. Not a flaky test — four separate failures, all from the same root cause: WorkItemService.delegate() grew a DeclineTarget parameter in issue #245, and ...
The 35 chapters DESIGN.md didn't know about
I went in thinking this was a documentation restructuring job. DESIGN.md had 27 numbered phases; the plan was to map those phases to ARC42STORIES.MD chapters, write the layer entries from code, del...
States That Existed on Paper
States That Existed on Paper
The Sweep and What Stuck
The issue-235-sxs-sweep branch was a batch of seven small-to-medium items. Six shipped. One (#234 — route inbound messages to WorkItem creation) came off the list: blocked on casehub-connectors-cor...
The build that kept giving
The sweep was done. The branch was closed. Then I ran mvn install.
The sweep that wasn't done
The branch was supposed to be closed. Handover said so. Seven items swept,
committed, pushed upstream.
The sweep that wasn't done
The branch was supposed to be closed. Handover said so. Seven items swept,
committed, pushed upstream.
The date in the field
The ExclusionPolicy javadoc had a forward reference to issue #185 where proper extension guidance should be. I wanted that resolved: what the activation pattern looks like, what the excludedUsers f...
The string that looked like a string
The problem with "legal_review" is that it looks exactly like "legal-review". One underscore. WorkBroker does exact string matching — if the engine puts "legal-review" in a case definition and the ...
The Audit Entry That Couldn't Exist
The spec said there was no FK constraint on audit_entry.work_item_id.
Extend Was Incomplete
BreachDecision.Extend has been in the sealed hierarchy since we added SlaBreachPolicy — a record that says “give them more time.” Until today, returning it from your policy did nothing. The breach ...
SNAPSHOT Drift: Ten Imports, Two Lessons
The CI failure that kicked off this session was a compile error in casehub-work-ledger: cannot find symbol: class ActorType, location: package io.casehub.ledger.api.model. The cause took thirty sec...
What a Void SPI Costs You
EscalationPolicy.escalate() returned void. That’s a small thing until you need to do something after the policy runs.
The Decision the Policy Returns
EscalationPolicy.escalate() was always a lie. It pretended to be a hook, but it was really fire-and-forget: the expiry service called it and moved on with no way to know what happened next. Escalat...
The Hook That Passed Everything
The pre-push hook checks for squash candidates before anything leaves the machine. The patterns in it look reasonable: ^docs\(claude\):, ^fix\(test\):, ^chore:. It had been in place for weeks. Ever...
The Params That Did Nothing
There’s a particular kind of bug that’s easy to miss in review: code that compiles, ships, and runs fine — it just doesn’t do what it says it does.
The retry that wasn't
The backlog is clear. Everything filed as S or M this session is done — six issues
including one I’d underestimated. That felt satisfying until the code reviewer
flagged a bug I hadn’t suspected.
A record can't say no
WorkItemCreateRequest had 24 parameters. A new field meant adding it to the end
and passing null at every call site you could find. Sometimes you missed one. The
record was too honest about its own...
Policy owns the reason
ExclusionPolicy.isExcluded() returned a boolean. It answered the wrong question.
Template as type system — and a merge that lied
The last OHT gaps are closed. WorkItemTemplate now carries the full type
contract: named outcomes, JSON Schema for payload and resolution, and an
explicit exclusion list for conflict-of-interest en...
The parent gets the callerRef
A prompt arrived at the start describing three bugs in casehub-work-ledger
— JSON built with String.format(), a null guard missing on eventSuffix(),
eight tests with wrong expectations. I looked at...
SWF doesn't have a human
The previous entry covered the HumanTask binding design — the sealed
BindingTarget, the outbound HumanTaskScheduleHandler, the outputMapping
round-trip. Claude had built the implementation in a sep...
The half that was missing
The outbound path from engine binding to WorkItem creation was missing — tracing why WorkerProvisioner couldn't work, and designing HumanTaskScheduleHandler as the correct event-driven hook.
Speech acts, priority names, and a Panache coupling
NormativeResolution maps GitHub and Jira close events to speech-act semantics, CRITICAL gets renamed URGENT after finding it borrows ITSM severity vocabulary into a task scheduling context where it...
The default that bit us, and why defaults matter
Three unrelated CI failures arrive at once — a Friday business-hours assertion, a SNAPSHOT API drift, and a multi-instance default whose effects only appear under concurrent load — all sharing the ...
Delegates, transactions, and not building a parallel universe
castTo(PgConnection.class) fails immediately because Mutiny codegen produces sibling hierarchies, not parent-child — the subscription needs the raw Vert.x delegate to call notificationHandler().
Three bugs hiding behind the wrong error
The BackPressureFailure filling the CI log is noise — the actual failure is an OptimisticLockException buried further down, caused by the M-of-N coordinator racing against itself during concurrent ...
Three audit findings and a wrong mental model
Three audit findings include a String.format JSON builder that silently corrupts output on non-trivial actor IDs, a bug the auditor described with the wrong mechanism, and a Bayesian mental model e...
Distributed SSE and the infrastructure tax
Redis for distributed SSE fan-out is rejected because it would force two new infrastructure pieces on users who have neither Redis nor MongoDB — a pluggable WorkItemEventBroadcaster SPI defers the ...
M-of-N parallel WorkItems and a rule that needed rewriting
M-of-N parallel WorkItems ships after rewriting LAYERING.md — the rule "if it touches another WorkItem it's orchestration" was already broken by cascade cancellation, so the real test is whether it...
Optional by design, and a PostgreSQL test that told the truth
The PostgreSQL Testcontainer test finds a HQL date-truncation query that passes on H2 but fails on PostgreSQL — catching a dialect incompatibility that would have reached production undetected.
SLAs, signals, and a connector library
Business-hours SLA and outbound notifications ship, and a notification routing detail that starts as a one-line decision grows into casehub-connectors — a separate repository born from the question...
The primitive and the orchestrator
A data-driven spawn rule table is rejected before a line of code — the right answer is that subprocess orchestration belongs to CaseHub, and quarkus-work should expose the primitive that CaseHub re...
The substrate and what grows on it
Splitting quarkus-work-api into pure contract and quarkus-work-core with CDI implementations reveals that WorkBroker and WorkItems are specialisations of a shared Work substrate that CaseHub also n...
The filter that grew into a contract
A confidence-gated routing rule that could be wired in twenty lines becomes quarkus-work-filter-registry instead — because inline logic is a trap that becomes load-bearing and unremovable.
Aligning with the SDK — storage, expressions, and a hidden test class
Studying the SWF SDK storage abstraction confirms that WorkItemRepository needs multi-field inbox queries — Map doesn't express them cleanly — so a richer query object replaces a direct SWF port.
Examples that prove it
Four runnable examples — expense approval, regulated credit decision, AI content moderation, and document review — together exercise every ledger capability, and the agent: prefix convention derive...
The accountability layer — complexity must not leak
The ledger becomes a CDI observer on WorkItemLifecycleEvent — invisible when absent, zero overhead, zero tables — with hash chain and decision context on by default for GDPR Article 22 compliance.
Building WorkItems.From Scaffold to Native in One Session
Six phases of quarkus-work ship in one session — the WS-HumanTask research expands the WorkItem model to 27 fields before a line of code is written, and TDD catches two real bugs in Phase 2.