Files
command-center/agent/managed-skills/monolith-split-under-dist-pin/SKILL.md
T

4.8 KiB

name, description
name description
monolith-split-under-dist-pin Use when cutting a monolithic source file into modules behind a black-box test suite whose committed build artifact (dist) must stay byte-identical to src; one verified commit per cut, red-proof against the previous bundle. Not for feature work or doc-only edits.

Monolith split under a committed-bundle pin

Procedure that landed a 1354-line index.ts as 9 modules + 301 lines of wiring across 9 commits with zero red commits. Assumes: black-box test suite (tests drive the built bundle, not internals) and a pin test asserting the committed dist/ equals a fresh build of src/.

Per-cut loop (one commit per cut, always in this order)

  1. bun run build (or project equivalent) before staging — commit dist/ in the same commit as the moved src/, otherwise the pin test fails ("committed dist is byte-identical to a fresh build") and you must amend. Amending is fine only while the branch is unpushed.
  2. tsc --noEmit, then run the full suite async (bats tests/ > /tmp/x.log 2>&1; grep -E '^1\.\.').
  3. Never write to src/ while the suite runs. setup() rebuilds the bundle from src/, so a concurrent edit attributes someone else's half-written state to your run. Read-only prep (planning the next cut's exact line ranges) is the correct thing to do while waiting.
  4. Gate green → next cut. Gate red → fix, rebuild, amend, re-gate.

Cutting mechanics

  • Cut leaf/pure modules first (grammar, transcript rules, URL resolution, pure segmenter), then stateful ones (config, IO, pipeline, UI). Never let a module import index — if the new module needs something from index, that thing is a leaf and must move too (this is what forced segment.ts out early).
  • Extract in verbatim bodies: read the exact ranges, write them into the new file, then delete the same ranges. Only module docs, export keywords, and injected dependencies change. Keeps each commit reviewable as a move.
  • Edit tools that require "displayed lines" from the latest read snapshot: re-read the exact ranges under the current tag, then issue one edit call with all ops listed bottom-up (original line numbers throughout). Adjacent ranges merge into one CUT a=b; leaving a doubled blank line is the usual scar — one extra CUT next turn.
  • Mutable config must reach a long-lived object as getters, not a construction-time snapshot: menu handlers mutate cfg after construction, and a snapshot freezes the settings the UI exists to change.
  • After moving code, grep the old file for the moved names: stale call sites compile under bundlers that skip typechecking (stop() vs speaker.stop() was caught only by tsc, not by bun build).

Proving a new assertion is red-capable (before trusting it green)

Build the old bundle and point the test harness at it — no stash, no worktree juggling:

git show HEAD:dist/index.js > /tmp/old-dist.js   # bundle is self-contained
# rebuild the throwaway harness by extracting the driver the suite writes
sed -n "<heredoc-body-range>p" tests/behavior.bats > "$W/driver.ts"
DV_EXT=/tmp/old-dist.js <same env as the test> bun "$W/driver.ts" <scenario>   # expect RED
DV_EXT="$PWD/dist/index.js" <same env> bun "$W/driver.ts" <scenario>           # expect GREEN

Copy the project's real player/server stub verbatim — a simplified stub (e.g. one that skips writing the log file the driver counts) prints a false PLAYS 0 and looks like a failing fix. Pin the scratch dir to a fixed path; ls -dt /tmp/tmp.* picks up another session's directory.

Black-box test hygiene learned the hard way

  • A test that counts artifacts in the shared temp directory observes every other process on the box (live agent sessions synthesize into the same dankvoice-* dirs). Give the run a private TMPDIR="$WORK/scratch-tmp" and assert there; the assertion's sensitivity is proven by the fact it caught a real leftover while scoped globally.
  • Let preseeds reference the ephemeral fixture port with a placeholder (__PORT__) substituted by the driver, so a test can preseed a URL pointing at its own fixture server.
  • Refactor-only commits get no new test: re-asserting unchanged labels/order after a commit whose point is that they are unchanged is a tautology; the existing suite is the gate. New tests belong to behavior changes, red-first.
  • Tests must observe output, never internals: assert PLAYS/CALLS/CALL <url>/NOTIFY <text>, not that a variable was set.

Docs after a split (same branch, own commit)

Move each explanation to the file that now owns it (knob list → the module that reads env; state-machine prose → the module that implements it) and leave the entry point a one-line-per-module map. Then grep for pointers into the old file — a README saying "see header of src/index.ts" becomes a lie the moment the knobs move.