Matthew Dodd — home Search the stacks

Drill Routine

A browser simulator for Canadian Armed Forces drill: the engine rewritten before the backlog, a feature removed because it was wrong at the premise, a cloud design simplified before anything was deployed, and two incidents that each became a rule.

  • drillteamroutine.com
  • 2026
  • Architect and product owner
  • All work

What it does

Build a platoon or Colour party, sequence drill movements on a timeline, then play the routine back on a parade square measured in real paces.

Movements are modelled from the manual: turns, wheels, inclines, dressing, sizing, saluting, and the Chapter 7 formation and flank movements, with the commander and warrant officer marching their own correct paths rather than being dragged along with the body.

It ships with ready-made routines, including the March On! competition sequences for a platoon with and without arms and for a Colour party.

  • Per-member selection, so individuals can be given their own movements.
  • A timeline with explicit start ticks, so different members can move at the same time.
  • Scrub, step and pause playback; validation flags overlaps and broken dressing.
  • Routines save and reload locally as JSON.

Open drillteamroutine.com

Where it started

The first version ran one squad through a sequence of movements, one after another. The backlog needed members moving at the same time, several squads with their own staff, and a Colour party, and a single-squad sequential engine couldn't express any of that.

The decisions

Rewrite the engine before adding to it

Before any of the big content items, a second engine was built: a new data model, save format and UI around a deterministic timeline. Every movement has an explicit start tick and duration, and within a tick commands apply in a fixed order, so a routine plays back the same way every time. The first engine wasn't deleted. It stays in the repository as a reference: the new movement math was checked against it, and tests still run it.

Turned down: adding the backlog to the sequential engine, which couldn't move members at the same time or run more than one squad.

Model the drill from the manual

Movements are built from the CF Manual of Drill and Ceremonial (A-DH-201-000/PT-000), and from the Royal Canadian Sea Cadets' manual for Lee-Enfield drill, with chapter and paragraph citations in the code and in the documentation for each family of movements. The commander and the warrant officer walk their own paths in real drill beats, with turns, wheels and halts, instead of gliding along a straight line.

Remove what is wrong at the premise

Form Threes was first built as a reshape of the squad's geometry. The manual treats a change of formation as turns for the body and marches for the appointed ranks, so in August 2026 I rejected the feature at its premise, and it was removed before it ever shipped. That became a standing rule, and the drill-correct movements were later built out of footwork.

Turned down: patching the reshape until it looked right.

Simplify the platform before deploying it

The first design was a defence-in-depth AWS stack: CloudFront, S3, API Gateway and Lambda, WAF and KMS. When Canadian data residency turned out not to be required, in June 2026, the project went Cloudflare-only before anything was deployed on AWS: Pages for the site and the app, one Pages Function for issue reports, R2 for storage, and Terraform for all of it.

Turned down: the AWS design, whose extra layers answered threats that a single origin doesn't have.

Enforce the gates in the deploy

The repository is private on GitHub's free tier, which has no branch protection. Instead, the deploy re-runs every quality gate (tests, lint, formatting, types, the coverage ratchet and the end-to-end tests), and a test checks that it still does, so a red gate stops what ships.

Turned down: paying for a GitHub plan to get branch protection.

What went wrong

A stale document nearly deleted live infrastructure

In August 2026 the project's state documents had drifted seven pull requests behind, and they described the infrastructure folder as a stale AWS scaffold to delete or repurpose. It was live Terraform for deployed Cloudflare resources. It was caught before anything was deleted. The fix was a rule: the session documents never record a commit SHA, a test count or a changelog entry, because those go stale, and a test now enforces it.

A green deploy over a stale production

Release 1.12.1 reported success and passed its smoke checks while the live app was still serving 1.12.0. The checks looked at content and headers, which are the same from one release to the next. Production caught up 11 to 18 minutes later, but nothing had checked which build was live. Now the deploy reads the version it is shipping and waits, up to 12 minutes, for the live site to serve it, and it fails if the site never does.

The outcome

Version 1.0.0, the first stable release, shipped on 16 August 2026, and releases have continued since. Stable describes what ships, the platoon and the Colour party, not the whole backlog: the Band and Company formations stay behind a release gate until they're right.

Some things are deliberately left unbuilt, among them falling out of ranks, calling the roll, forming four or more ranks, and a third-party spam check that would add an outside origin to the content security policy.

The code

Excerpts from the source, which is private.

Source code of src/movements/footwork.js, lines 36 to 105: a walker route builder with march, wheel, turn, halt and hold steps, one beat per count, and a snap function that rounds headings to whole paces
src/movements/footwork.js: footwork as code. A route builder lays down one beat per count (march, wheel, turn, halt), and headings snap so a diagonal pace is still one pace.
Source code of test/escape.test.js, lines 19 to 59: Node tests for the single HTML escaper, checking each metacharacter, that the ampersand is escaped first, and that null and undefined become empty strings
test/escape.test.js: one HTML escaper, pinned by tests. The ampersand goes first so an entity can't be forged, and a missing label prints blank, not the word null.