The previous entry ended at 99 tests. I wasn’t done.

A fair comparison of the two LoopControl implementations — ours and treblereel’s original — turned up four genuine gaps we hadn’t addressed. I want to be precise about what “genuine” means here: not design differences, not things we chose differently, but capabilities the prior implementation had that ours simply lacked.

BlackboardPlanConfigurer — treblereel’s CasePlanModelRegistry let you declare stages for a case definition at design time. We had no equivalent. PR-F adds a CDI SPI: implement @ApplicationScoped BlackboardPlanConfigurer, override configure(CasePlanModel, PlanExecutionContext), and the loop control calls it exactly once when a case instance starts.

Strict PlanItem lifecycle — treblereel threw IllegalStateException on invalid transitions. Ours silently accepted anything via a raw setStatus(). PR-G replaces the setter with markRunning(), markCompleted(), markFaulted(), markCancelled() — each validates the current state and throws if wrong. The implementation caught something: several tests were jumping directly from PENDING to COMPLETED, bypassing RUNNING entirely. That’s a test design flaw the strict API correctly surfaces.

SubCase — a stage item representing a child case definition, with a completion strategy mapping child case status back to stage item status. Pure data model — the engine integration stays in the future epic. PR-H ports it for feature parity.

Stage entry validation — treblereel’s Stage builder required an entry condition. Ours silently used null, meaning “activates every cycle.” PR-I adds Stage.builder(name) which throws IllegalStateException if build() is called without entryCondition(). Stage.alwaysActivate(name) replaces Stage.create(name) for cases where always-activating is the intent — explicit, not silent.

Then the deeper comparison.

Reading treblereel’s PlanningStrategyLoopControl carefully, I noticed it split eligible bindings into staged (worker assigned to a stage) and free-floating (worker not in any stage). Staged bindings were blocked when their stage wasn’t active. I’d assumed this was a consequence of the architecture we’d replaced. I was wrong about the origin.

The Kogito flexible process blog posts explained the reasoning. This is the CMMN ad-hoc process model: you declare all possible workflow fragments at design time. Stages act as scope containers. When a stage activates, its declared fragments become available. When it doesn’t, they’re invisible to the selection loop. Free-floating fragments are always available — monitoring, error handling, anything that should run regardless of stage state.

The decision was how to opt into this. We considered a flag on CasePlanModel, a mode enum, and a convention. The convention wins: if a Stage has binding names declared, it’s a gating stage. If not, it’s lifecycle-only. The presence of stage.addBinding("trigger-name") IS the opt-in. Three modes from one mechanism — pure choreography, fully gated, hybrid — with no flag and no enum.

ADR-0002 captures the reasoning. PR-J implements it: Stage.addBinding(), withBinding(), builder().binding(), and gating logic in PlanningStrategyLoopControl.select() — with a fast path when no stages have declared any bindings (pure choreography, zero overhead).

120 tests became 136. Ten QE PRs open (#91–#100). The new implementation is now strictly superior in every dimension the comparison identified.


<
Previous Post
Idle is not one thing
>
Next Post
Ecosystem Mapping, PRs F-J, ADR-0002