I could have built this twice. ModuleOrderStore is why I didn't.

The first hands-on device QA of the J→K→L→M arc ran on AnF, code-signed against my Apple Development identity and installed on hardware for the first time since the rearrangeable module work landed. I pulled on the Home dashboard’s top module. It slid. I dragged it below the next block. It settled. I switched to Accounts and pulled on the same module type there. Same drag handles, same snap behaviour, same persistence across a cold launch.

That confirmed Phase L. Getting there was a decision made at the start of the sprint about where state should live.

The problem with per-screen order

Home has stacked modules. Accounts has stacked modules. When both screens became rearrangeable candidates, the naive implementation was obvious: give each screen its own order store. Home reads from one, Accounts reads from another. User drags a module on Home; Home’s store updates. Accounts is unaware. User opens Accounts; Accounts reads from its own store.

That’s not wrong exactly. It’s expensive across time. Two state managers means two persistence layers. Two persistence layers means two codepaths to update when the persistence strategy changes. If a module type appears on both screens and the user expects consistent ordering, you’re synchronising two stores instead of reading one.

Add a third screen with stacked modules and the pattern compounds. A fourth and it’s a maintenance surface, not a feature.

The alternative is to decide early that module order is an app-wide primitive rather than a per-screen concern. That decision has a cost at Phase L (slightly more considered up-front design) and an ongoing saving on every subsequent screen that gains stacked modules.

ModuleOrderStore

ModuleOrderStore lives in Modules/UI/Navigation. It’s a @MainActor ObservableObject singleton, mirroring the structural pattern PaletteStore had already established elsewhere in the codebase for app-wide UI state. Injectable: test contexts substitute a fresh instance rather than the shared global, so the singleton doesn’t create a test isolation problem.

The store carries one job: persist and vend module order across every screen that needs it. Home and Accounts both read from the same instance. When you drag a module on Home, the store updates once. No sync step is required, no Accounts-side handler runs, no event bus carries the change. Both screens are observers of the same source.

The reason this works without a sync step is that the store is the source. Not a source. The source.

What Phase L shipped

Phase L was the implementation sprint: drag gesture, order persistence, and injection into both screens. After it landed, the rearrangeable module contract existed in the simulator, ready for the first complete hardware pass of the arc.

The J→K→L→M arc covered a lot of ground: Energy Dock navigation, the Accounts screen, the rearrangeable modules, and the design DNA sweep. Phase L’s ordering engine had to hold across all of it. Any of the later phases could have broken the drag behaviour through a layout change or a screen restructure. None did. The drag handle behaviour on both screens was untouched from Phase L through Phase O.

What the QA round found

Six issues across three areas.

The Home gamification blocks (Quests, Level, Streak) were non-interactive labels. Phase N gave each a detail screen and made them tappable. The Streak block’s “Check in” button was routing to the transaction form instead of registering a check-in directly; Phase N fixed the routing. QA issues #1, #2, #3.

The Accounts screen’s daily-activity strip became a month calendar in Phase O. The design deepened mid-sprint: the original ask was the calendar view; the full-screen → day mode transition came in during the design pass once the first level was visible. That gave three navigation levels (strip, full-screen calendar, day mode) where the spec had implied one. QA issues #4, #7, #8.

Phase M ran across everything in parallel: a DNA sweep that brought the laggard screens up to the Cursed Energy design language Accounts and Forecast had already adopted. The inconsistency had been in the original spec; Phase M closed it.

Hardware changes the question

None of the six issues surfaced in the simulator. They surfaced on hardware, which is the correct order. Simulator passes check correctness at the logic layer; hardware passes check correctness at the interaction layer: whether a drag gesture fires reliably, whether a tap target is the right size, whether a navigation transition feels right at the actual frame rate.

The gamification routing bug, “Check in” opening the transaction form, was a correctness error the simulator would have caught if it had been tested there. It wasn’t. That’s the honest account. The device QA round on AnF was the first complete pass of the arc.

Running everything together on hardware before declaring the arc done is the right sequence. Not because the simulator is useless. Because it answers a different set of questions.

The singleton tradeoff

The injectable singleton has one known weakness: if two features ever need genuinely different orderings of the same module type on the same screen simultaneously, the model breaks. That scenario doesn’t exist in this app. Designing for it would be over-engineering against a requirement that isn’t in the spec.

Out of scope: per-account order overrides, iCloud sync of module order, order migration on app reinstall. None of those are deferred; they’re not in scope.

The testability concern, the usual objection to singletons, is handled by injection. ModuleOrderStore is the default, not a static constant accessed directly. Test contexts swap it out. The singleton is a runtime default, not a design constraint.

The extractable principle

PaletteStore established the pattern: app-wide UI state lives in a singleton primitive. ModuleOrderStore follows it for module ordering. When a third screen gains stacked modules, it follows it again. Adding a new consumer is wiring, not construction.

That’s the difference between a pattern and a duplication. Duplication means the second screen gets a copy of the first screen’s order store. A third screen gets a copy of that. Pattern means there’s one implementation and every screen is a client.

The cost of choosing pattern over duplication is one deliberate decision at Phase L. The saving is everything that doesn’t need to be built, synced, or refactored later.

The arc in summary

Four phases. L built the ordering engine. M standardised the design language. N made the gamification blocks interactive with correct routing. O deepened the Accounts calendar to three navigation levels, one of those added mid-design.

Device QA on AnF. Six issues. All six closed.

The ordering engine was untouched from Phase L to Phase O. That’s what a well-placed primitive looks like: it holds while everything else moves.

All writing