Shipmind Labs

An overlap is not a separate problem from insufficient rest. It is rest equal to zero, measured negative.

We hit this while building shift-planner, our open-source rostering library. Two checks looked independent: shifts must not overlap, and consecutive shifts for one worker must leave a minimum gap. Two rules, two error types, two code paths. That framing is wrong, and it costs you in the places you do not look.

A roster carries its shifts plus a rest value, defaulting to zero. Construction refuses a negative rest outright, sorts the shifts by start, then end, then worker, and walks them once, keeping the latest shift per worker. For two consecutive shifts of one worker the order of checks is fixed: overlap gets tested first, and only if the two do not share time does the gap get compared against rest.

The part worth copying is the type relationship. Our overlap error subclasses the insufficient-rest error. A clash is the zero-rest case of a single rule, and it still keeps its own message built from the overlapping span. Both errors carry the same payload (first shift, second shift, required rest). Code that handles rest violations handles clashes for free. Code that cares about the difference still can. Nobody has to remember to catch both.

The second part is where the rule lives. Equality passes: exactly the required rest between one shift ending and the next starting is legal, and touching shifts stay legal only while rest is zero. And when you grow a roster, the add and extend operations hand the existing rest value to the new roster. A grown roster cannot shed its own rule, and the object you were holding keeps its own shifts unchanged.

The same field exists one level up, on a plan, with the same default and the same negative check. A plan builds its roster from its own shifts and its own rest. When a plan is applied to an existing roster, the result takes the maximum of the two rest values. Drawing a plan onto a roster can tighten the constraint. It cannot loosen it.

That asymmetry was a deliberate choice, and it is the one we would defend hardest. Monotonic constraints are probably the only kind you can reason about in a system that composes. If combining two valid objects could produce something weaker than either input, every guarantee becomes local, and you are back to reading the whole call graph to know what is enforced.

Most validation bugs we have chased in production were not wrong predicates. They were correct predicates that some path forgot to run, or two near-identical predicates that drifted apart over a year of edits. Collapsing a special case into its general rule removes that drift by construction.

Somewhere in your own domain there are two validations that turn out to be one rule and its boundary.

Was this useful?

Building something similar?

or email hello@shipmindlabs.com