4 — How a change to the program lands
Written: 2026-08-15 · Established by: reading src/store/file-slice.ts,
src/transforms/apply.ts, src/transforms/guard.ts, src/transforms/TransformDialogShell.tsx and
src/viewer/useBrushStroke.ts at commit 93a9a67.
Every write to the program is a whole-text replacement followed by a re-parse. There is no
incremental update of the model, and no path that edits moves or operations directly. Five very
different-looking controls — a cleanup dialog, a check's guided fix, the backplot brush, a
click-to-delete, and typing in the editor — all converge on two store actions.
| Box | Where it lives |
|---|---|
| Cleanup dialogs | src/transforms — one *Dialog.tsx per tool |
| Guided fix | src/sidebar/FixDialog.tsx |
| Backplot brush | src/viewer/useBrushStroke.ts |
| Click to delete | deleteBlock in src/store/file-slice.ts |
applyEdits |
src/transforms/apply.ts |
refusalReason |
src/transforms/guard.ts |
applyTransform, editFullFile, replaceFile |
src/store/file-slice.ts |
| Export and diff | src/export |
Why the two store actions differ by exactly one flag
replaceFile takes a remount boolean, and it separates a write the user is making from one being
made for them. Typing must not bump historyRevision, because remounting the editor mid-keystroke
yanks the caret. A transform or a backplot delete must bump it, or the change never appears in the
uncontrolled editor at all.
Three properties of applyEdits that are easy to get wrong
Edits are applied back to front, so an earlier edit's replacement cannot shift the line numbers a later edit is expressed in.
Block indices are converted to line indices through GCodeBlock.lineIndex, never used directly.
They happen to be equal today, because tokenize maps lines to blocks one to one — and nothing may
assume it. A transform splicing the wrong lines is the worst failure this code has available.
The replacement inherits the file's line endings. Every large fixture here is CRLF, and a merged line written with a bare LF would leave a file that is half one and half the other, which some controls read as a stray character rather than as whitespace.
The guard is on the store action, not on the button
refusalReason runs inside applyTransform, so a refusal survives a refactor of the UI —
ADR-0005. It takes a
TransformKind of 'writes', 'consumes' or 'deletes', and the three are gated differently:
subprograms block the first two and not the third, because a deletion has no computed geometry to be
silently wrong about in the invocations it did not preview.
What re-parsing costs, and why it is still the design
test-fixtures/README.md puts the three large fixtures at roughly 230–250 ms to parse, and that is
paid on every applied transform. Not re-measured for this diagram — treat it as the order of
magnitude rather than a current number. It buys one model with one construction path, so a transform
cannot leave moves and
raw disagreeing. Undo is the same mechanism in reverse: the history stack holds text, not diffs.