<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ruslan Gilmullin</title>
    <description>The latest articles on DEV Community by Ruslan Gilmullin (@mellonis).</description>
    <link>https://dev.to/mellonis</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F337859%2F01b619ce-df73-44c6-8308-a516030a5462.jpeg</url>
      <title>DEV Community: Ruslan Gilmullin</title>
      <link>https://dev.to/mellonis</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mellonis"/>
    <language>en</language>
    <item>
      <title>Two majors, one README, one demo: two cheap design reviews</title>
      <dc:creator>Ruslan Gilmullin</dc:creator>
      <pubDate>Tue, 19 May 2026 08:35:03 +0000</pubDate>
      <link>https://dev.to/mellonis/two-majors-one-readme-one-demo-two-cheap-design-reviews-2pgn</link>
      <guid>https://dev.to/mellonis/two-majors-one-readme-one-demo-two-cheap-design-reviews-2pgn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Writing the docs is what surfaced both mistakes. There’s a meta-lesson in there about how docs are the cheapest design review you can run, but that’s another post.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is “that other post” — and the first thing it has to do is correct the teaser. The teaser oversells. Docs caught one of the two mistakes. The other was caught by the first real consumer of the API, which I was building in parallel. The two reviews worked in tandem: docs review the shape of an API, the consumer reviews the use of it. Together they catch what tests can’t see.&lt;/p&gt;

&lt;p&gt;If you ship anything behind an interface — a library, a CLI, any entity behind a contract — these are the two reviews you don’t want to skip.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F74qgkrikcjjpt6eihwk5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F74qgkrikcjjpt6eihwk5.webp" alt="Screenshot of demo.machines.mellonis.ru running a Brainfuck UTM: multi-tape view on the left, engine source on the right" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://mellonis.ru/en/articles/three-majors-two-mistakes/" rel="noopener noreferrer"&gt;“Three majors, two mistakes”&lt;/a&gt; covers the engine and the v4 pause API — &lt;code&gt;onStep&lt;/code&gt;, &lt;code&gt;onDebugBreak&lt;/code&gt;, per-state &lt;code&gt;debug&lt;/code&gt; flags — in detail. I’ll lean on it here without rehashing it. The v4 → v6 trajectory shipped three breaking majors: a hook rename, a halt-semantics hardening, and a dispatch-tick collapse. The first two surfaced in the demo. The third surfaced in the docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demo case: v4 → v5
&lt;/h2&gt;

&lt;p&gt;While shipping v4 I started building &lt;a href="https://demo.machines.mellonis.ru" rel="noopener noreferrer"&gt;&lt;code&gt;machines-demo&lt;/code&gt;&lt;/a&gt; — an interactive Turing-machine debugger and the engine's first non-test client.&lt;/p&gt;

&lt;p&gt;The demo is a natural first consumer: it has a dual purpose. The product goal — public distribution and showing the engine in action. The technical goal — road-testing changes and validating concepts against a live API surface. Both goals make building the demo an integral part of the release cycle, not an optional add-on.&lt;/p&gt;

&lt;p&gt;The demo used both hooks at once: &lt;code&gt;onStep&lt;/code&gt; populated a per-iteration command buffer for the trace UI; &lt;code&gt;onDebugBreak&lt;/code&gt; drove the pause/resume cycle.&lt;/p&gt;

&lt;p&gt;The demo built. The tests passed. But it was uncomfortable to write, for two reasons.&lt;/p&gt;

&lt;p&gt;First, &lt;code&gt;onDebugBreak&lt;/code&gt;’s after-fire came with data from the previous yield — the same data the previous &lt;code&gt;onStep&lt;/code&gt; had already shown. The demo processed the same thing twice, and the question “why two hooks for one event?” was being asked not by me but by the code itself, in a way. I filed &lt;a href="https://github.com/mellonis/turing-machine-js/issues/109" rel="noopener noreferrer"&gt;&lt;code&gt;turing-machine-js#109&lt;/code&gt;&lt;/a&gt; as an RFC about the relationship between these hooks, listing four sketches; the resolution narrowed to &lt;em&gt;naming&lt;/em&gt;. &lt;code&gt;onDebugBreak&lt;/code&gt; framed the purpose of use as “debugging”, while the consumer’s verb was “pause”. Shameless rename, no alias. v5.&lt;/p&gt;

&lt;p&gt;Second, the demo’s UI gained a “pause before halt” scenario — letting the user glimpse the machine’s final state before it shuts down. The natural implementation: a &lt;code&gt;debug&lt;/code&gt; flag on &lt;code&gt;haltState&lt;/code&gt; itself. The first test case set &lt;code&gt;haltState.debug = { before: true, after: true }&lt;/code&gt; because the symmetry looked right. Only &lt;code&gt;before&lt;/code&gt; fired. Worse: the after-fire on the iteration that &lt;em&gt;led to&lt;/em&gt; halt never reached the consumer — the loop exited as soon as &lt;code&gt;state.isHalt&lt;/code&gt; became &lt;code&gt;true&lt;/code&gt;. &lt;a href="https://github.com/mellonis/turing-machine-js/issues/108" rel="noopener noreferrer"&gt;&lt;code&gt;turing-machine-js#108&lt;/code&gt;&lt;/a&gt; split it in two: restore the lost after-fire (bug); throw on assignment to &lt;code&gt;haltState.debug.after&lt;/code&gt; (API).&lt;/p&gt;

&lt;p&gt;Both complaints came from the consumer side. The demo didn’t surface a code bug — it surfaced an &lt;em&gt;API interaction format&lt;/em&gt;. The names didn’t fit the use. Being permissive in input didn’t match what the user was trying to do. The tests verified behavior against the engine’s own internal model — ok, green. The demo verified behavior against the consumer’s mental model — and produced two specific complaints (not ok).&lt;/p&gt;

&lt;h2&gt;
  
  
  The docs case: v5 → v6
&lt;/h2&gt;

&lt;p&gt;v5 shipped. The README needed updating. The new dispatch-order section had to explain — in words — when each of &lt;code&gt;onPause(before)&lt;/code&gt;, &lt;code&gt;onStep&lt;/code&gt;, and &lt;code&gt;onPause(after)&lt;/code&gt; fired relative to the iteration they described.&lt;/p&gt;

&lt;p&gt;The first honest paragraph went something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;onPause(after, K)&lt;/code&gt; fires on iteration K+1’s yield, with the payload substituted from iteration K’s snapshot, before &lt;code&gt;onPause(before, K+1)&lt;/code&gt; or &lt;code&gt;onStep(K+1)&lt;/code&gt; fire.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I stared at that sentence for a while. There was no shorter version. The reader wasn’t supposed to need a sentence about substitution.&lt;/p&gt;

&lt;p&gt;The code worked. The tests passed. The demo consumed the hooks correctly. The mistake wasn’t in any of those — it was in the &lt;em&gt;shape&lt;/em&gt; of the dispatch, and that shape was only visible when you had to put it into words.&lt;/p&gt;

&lt;p&gt;The fix collapsed the lifecycle: &lt;code&gt;before(K) → step(K) → after(K)&lt;/code&gt; on the same yield. No substitution. No cross-iteration scheduling. No final drain for the halting iteration (in “Three majors, two mistakes” I called it “post-loop drain”; I’d probably shorten it to “final drain” now). The README paragraph now reads:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On iteration K’s yield, hooks fire in lifecycle order.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And that’s the kind of sentence the reader glides past without effort. &lt;a href="https://github.com/mellonis/turing-machine-js/issues/119" rel="noopener noreferrer"&gt;&lt;code&gt;turing-machine-js#119&lt;/code&gt;&lt;/a&gt; shipped as v6.&lt;/p&gt;

&lt;p&gt;A direct quote from the previous article:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The code worked, the tests passed, and the docs were correct. The shape was just wrong.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What I’d add: the docs were correct only on the condition that the reader accepted three explanatory sentences they shouldn’t have had to read. That’s not “correct docs” — that’s docs apologizing for the shape.&lt;/p&gt;

&lt;p&gt;The docs review caught the &lt;em&gt;shape&lt;/em&gt;, not the &lt;em&gt;use&lt;/em&gt;. The demo worked. The tests passed. This time, only &lt;em&gt;prose pressure&lt;/em&gt; uncovered the issues hiding in the implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three reviews, three layers
&lt;/h2&gt;

&lt;p&gt;Here’s what each one checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tests&lt;/strong&gt; verify code against itself. Internal consistency. The engine yields what the engine should yield. Green tests are the baseline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first real consumer&lt;/strong&gt; verifies code against a consumer’s mental model. &lt;em&gt;Does the API format match what the user is trying to do?&lt;/em&gt; Reviewing real interaction with the API forced API changes: a rename and a halt-after restriction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The docs as *prose&lt;/strong&gt;* (not JSDoc — a connected README narrative) verify code against the author’s own explanation. &lt;em&gt;Does the design have an honest one-paragraph description?&lt;/em&gt; Without one, we had to look hard at the substitution dance — and abandon it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each review catches what the layer below misses. Tests don’t catch shape; the consumer doesn’t catch interaction problems it can skillfully bypass; the docs don’t catch UX wrinkles they don’t have to mention.&lt;/p&gt;

&lt;p&gt;The cost is asymmetric. Building a real consumer is the most expensive of the three reviews; writing the docs is the cheapest. Still, even the most expensive costs less than reworking the API after the problems surface for users — hence “cheap” in the title. The real consumer has to be built &lt;em&gt;before&lt;/em&gt; the major cut — it’s the only review that makes the API comfortable to use. Docs then paper over what’s left.&lt;/p&gt;

&lt;h2&gt;
  
  
  Heuristics for the next major
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build the first real consumer before cutting the major.&lt;/strong&gt; Not a test fixture — a consumer with its own mental model. The difficulties it encounters are the same difficulties your users will encounter later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the dispatch-order paragraph before locking the dispatch order.&lt;/strong&gt; If you can’t describe in one sentence what fires when, the dispatch is wrong. Decisions of this kind belong at the design stage, not documentation — when not a single line of code has been written yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the docs back as a stranger.&lt;/strong&gt; If a paragraph reads as an apology for the shape, the docs are working — they’ve &lt;em&gt;revealed&lt;/em&gt; a shape mistake. Fix the shape, not the paragraph.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three majors. One README. One demo. The tests had nothing to say.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Code: &lt;a href="https://github.com/mellonis/turing-machine-js" rel="noopener noreferrer"&gt;&lt;code&gt;turing-machine-js&lt;/code&gt;&lt;/a&gt; (engine) and &lt;a href="https://demo.machines.mellonis.ru" rel="noopener noreferrer"&gt;&lt;code&gt;machines-demo&lt;/code&gt;&lt;/a&gt; (the first non-test consumer where v4 → v5 surfaced).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>design</category>
      <category>softwareengineering</category>
      <category>writing</category>
    </item>
    <item>
      <title>Three majors, two mistakes: designing a pause API for a Turing-machine interpreter</title>
      <dc:creator>Ruslan Gilmullin</dc:creator>
      <pubDate>Tue, 19 May 2026 08:34:35 +0000</pubDate>
      <link>https://dev.to/mellonis/three-majors-two-mistakes-designing-a-pause-api-for-a-turing-machine-interpreter-137j</link>
      <guid>https://dev.to/mellonis/three-majors-two-mistakes-designing-a-pause-api-for-a-turing-machine-interpreter-137j</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmfch2qzlchz0zwrbi9dy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmfch2qzlchz0zwrbi9dy.webp" alt="Screenshot of demo.machines.mellonis.ru running a Turing machine" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I spent the last two weeks shipping four breaking major versions of &lt;a href="https://github.com/mellonis/turing-machine-js" rel="noopener noreferrer"&gt;&lt;code&gt;@turing-machine-js/machine&lt;/code&gt;&lt;/a&gt; — v3, v4, v5, v6 — and the most interesting part wasn’t any single feature. It was watching the same API surface (a pause/breakpoint hook on the run loop) get redesigned twice in three versions, each time because the previous shape exposed something it shouldn’t have.&lt;/p&gt;

&lt;p&gt;This post is the post-mortem. If you’re designing pause/step/breakpoint APIs for a generator-loop interpreter, scheduler, or DSL runtime, the mistakes I made are easy to make and worth seeing in someone else’s code first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The engine runs a Turing machine. Internally it's a generator: each &lt;code&gt;yield&lt;/code&gt; corresponds to one step (one transition firing on one tape symbol). The driver loop looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onStep&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;runStepByStep&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;initialState&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;onStep&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isHalt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;MachineState&lt;/code&gt; (&lt;code&gt;m&lt;/code&gt;) is a snapshot per iteration: the state about to execute, the current and next symbols, the moves the heads will make. A consumer (a logger, a UI, a test) can hang off &lt;code&gt;onStep&lt;/code&gt; to observe.&lt;/p&gt;

&lt;p&gt;What I wanted to add in v4 was a way to &lt;em&gt;pause&lt;/em&gt; execution at chosen points — like a breakpoint in a debugger. Any &lt;code&gt;State&lt;/code&gt; should be able to carry a &lt;code&gt;debug&lt;/code&gt; config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;myState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;symA&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;symA&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...and the run loop should give the consumer a chance to inspect the machine and decide when to resume.&lt;/p&gt;

&lt;h2&gt;
  
  
  v4: ship it
&lt;/h2&gt;

&lt;p&gt;The v4 shape was straightforward. I made &lt;code&gt;run()&lt;/code&gt; async, added an &lt;code&gt;onDebugBreak&lt;/code&gt; hook, and routed it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onStep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* every step */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;onDebugBreak&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debugBreak&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;before:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debugBreak&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;after:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// hold here until promise resolves&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The per-state &lt;code&gt;debug&lt;/code&gt; field is mutable. &lt;code&gt;state.debug = { before: true }&lt;/code&gt; sets a pre-step break; &lt;code&gt;before: [symA]&lt;/code&gt; filters by what the head is reading. The hook is awaited, so any consumer can implement “freeze until the human clicks Resume” by simply not resolving the promise.&lt;/p&gt;

&lt;p&gt;I also wanted breakpoints on halt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;haltState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;  &lt;span class="c1"&gt;// pause before exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That worked. Symbol-list filters on &lt;code&gt;haltState&lt;/code&gt; were silent no-ops, because &lt;code&gt;haltState&lt;/code&gt; has no head symbol to filter by. Fine, I thought — be permissive in input, the wildcard &lt;code&gt;true&lt;/code&gt; is the one that matters.&lt;/p&gt;

&lt;p&gt;Two things in this design were wrong. I didn’t notice either until I started writing docs and a UI on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 1: the hook describes the engine, not the consumer
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;onDebugBreak&lt;/code&gt; reads, on paper, like a perfectly reasonable name. The engine fires a “debug break”; you hook into it.&lt;/p&gt;

&lt;p&gt;But what does the consumer &lt;em&gt;do&lt;/em&gt; in that hook? They pause. They inspect. They wait for input. They resume. The hook isn’t really notifying you that a thing happened — it’s offering you a cooperation point.&lt;/p&gt;

&lt;p&gt;The name &lt;code&gt;onDebugBreak&lt;/code&gt; carries one specific framing: “debugging”. But the same hook is exactly what you want for a step-through visualization, a slow-motion playback control, an animation tween, a “press space to advance” UI. None of those are debugging. They’re all &lt;em&gt;pausing&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This sounds like a small thing. It’s not, because once consumers see the name they design around it: their code path is called &lt;code&gt;handleDebugBreak&lt;/code&gt;, their state machine has a &lt;code&gt;debugging&lt;/code&gt; boolean, their UI button says “Stop debugging”. The name leaks into every consumer’s vocabulary.&lt;/p&gt;

&lt;p&gt;I wrote an RFC (&lt;a href="https://github.com/mellonis/turing-machine-js/issues/109" rel="noopener noreferrer"&gt;turing-machine-js#109&lt;/a&gt;) and renamed it in v5. Hard rename, no deprecation alias:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;await machine.run({
&lt;/span&gt;    initialState,
&lt;span class="gd"&gt;-   onDebugBreak: (m) =&amp;gt; { ... },
&lt;/span&gt;&lt;span class="gi"&gt;+   onPause: (m) =&amp;gt; { ... },
&lt;/span&gt;  });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payload field &lt;code&gt;m.debugBreak&lt;/code&gt; stayed — it’s metadata describing &lt;em&gt;why&lt;/em&gt; this yield fired (&lt;code&gt;{ before: true }&lt;/code&gt; or &lt;code&gt;{ after: true }&lt;/code&gt;), and “break” works fine inside the payload. But the hook name is the consumer’s contract: &lt;code&gt;onPause&lt;/code&gt; describes what &lt;em&gt;they&lt;/em&gt; do, not what the engine does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule for next time:&lt;/strong&gt; when naming a hook, name the consumer’s verb, not the engine’s event. Hooks are cooperation points, not event listeners.&lt;/p&gt;

&lt;h2&gt;
  
  
  Restriction: &lt;code&gt;haltState.debug.after&lt;/code&gt; is nonsense
&lt;/h2&gt;

&lt;p&gt;The second v4 mistake hid under an instinct to be permissive in input. &lt;code&gt;haltState.debug.after&lt;/code&gt; accepted writes silently. It just didn’t fire.&lt;/p&gt;

&lt;p&gt;Why? Because the &lt;code&gt;after&lt;/code&gt; semantics in this engine mean “fire on the iteration &lt;em&gt;after&lt;/em&gt; the one where the filter matched”. That makes perfect sense for normal states — you transition out of state K, the next iter (K+1) runs, &lt;em&gt;and at that yield&lt;/em&gt; the consumer is told “by the way, K’s after-filter fired last step”. But halt is terminal. There is no K+1 after halt. The &lt;code&gt;after&lt;/code&gt; event has nothing to anchor on.&lt;/p&gt;

&lt;p&gt;I’d silently swallowed &lt;code&gt;haltState.debug.after = true&lt;/code&gt; in v4. Worse, I’d let &lt;code&gt;{ before: true, after: true }&lt;/code&gt; through — half the assignment was meaningful, half was a no-op, and nothing told the consumer.&lt;/p&gt;

&lt;p&gt;In v5 I made both throw at write time (&lt;a href="https://github.com/mellonis/turing-machine-js/issues/108" rel="noopener noreferrer"&gt;turing-machine-js#108&lt;/a&gt; part 2):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- haltState.debug = { before: true, after: true }; // v5: throws — 'after' on halt has nothing to anchor on
&lt;/span&gt;&lt;span class="gi"&gt;+ haltState.debug = { before: true };
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule for next time:&lt;/strong&gt; if a configuration has no semantics, throw early. Silently no-op input &lt;em&gt;looks&lt;/em&gt; user-friendly until a consumer spends an afternoon wondering why their UI doesn’t fire on halt. “Be permissive” is a false economy when permissiveness silences a real mistake.&lt;/p&gt;

&lt;p&gt;While I was in there, I also fixed a related bug: the halting iter’s &lt;em&gt;own&lt;/em&gt; &lt;code&gt;after&lt;/code&gt; filter wasn’t firing either (&lt;a href="https://github.com/mellonis/turing-machine-js/issues/108" rel="noopener noreferrer"&gt;turing-machine-js#108&lt;/a&gt; part 1). The driver loop exited as soon as &lt;code&gt;state.isHalt&lt;/code&gt; became true — and the after-event from the previous iter, which would normally fire on this final yield, got dropped on the floor. v5 added a post-loop drain to fire it.&lt;/p&gt;

&lt;p&gt;I’ll come back to this.&lt;/p&gt;

&lt;p&gt;And one v5 bonus: a master switch on &lt;code&gt;run()&lt;/code&gt; for the whole pause system.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;onPause&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;false&lt;/code&gt;, all pause-fires are suppressed regardless of what &lt;code&gt;state.debug&lt;/code&gt; says across the graph (&lt;a href="https://github.com/mellonis/turing-machine-js/issues/106" rel="noopener noreferrer"&gt;turing-machine-js#106&lt;/a&gt;). You can A/B “debug mode” without rewriting the graph or clearing every &lt;code&gt;state.debug&lt;/code&gt; field. The flag dispatches &lt;code&gt;onPause&lt;/code&gt;; the underlying &lt;code&gt;m.debugBreak&lt;/code&gt; payload field still populates on the generator’s yields (it’s a property of the iteration, not of how &lt;code&gt;run()&lt;/code&gt; chose to surface it).&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 2: the substitution dance
&lt;/h2&gt;

&lt;p&gt;This is the one I’m proudest to have caught, because the code worked, the tests passed, and the docs were correct. The shape was just wrong.&lt;/p&gt;

&lt;p&gt;In v4 and v5, &lt;code&gt;after K&lt;/code&gt; (the after-fire for iteration K) actually fired on iteration &lt;strong&gt;K+1&lt;/strong&gt;’s yield. The driver loop carried a &lt;code&gt;pendingAfterFromPrev&lt;/code&gt; flag across yields, and on the next iter’s yield it dispatched the &lt;code&gt;after&lt;/code&gt; hook &lt;em&gt;first&lt;/em&gt;, then the &lt;code&gt;before&lt;/code&gt; hook, then &lt;code&gt;onStep&lt;/code&gt;. The hook for K’s after-fire had to see K’s state — not K+1’s. So I &lt;em&gt;substituted&lt;/em&gt; the previous yield’s snapshot into the &lt;code&gt;m.state&lt;/code&gt; field of K+1’s yield, just for the duration of the &lt;code&gt;after&lt;/code&gt; dispatch.&lt;/p&gt;

&lt;p&gt;This worked. But it had three knock-on effects:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The substitution leaked.&lt;/strong&gt; Consumers wanted access to the un-substituted, “real” iteration state (the one the &lt;code&gt;step&lt;/code&gt; hook saw) — see &lt;a href="https://github.com/mellonis/turing-machine-js/issues/107" rel="noopener noreferrer"&gt;turing-machine-js#107&lt;/a&gt;. Some users were reading raw &lt;code&gt;MachineState.debugBreak&lt;/code&gt; from &lt;code&gt;runStepByStep&lt;/code&gt; directly and were surprised that &lt;code&gt;m.state&lt;/code&gt; referred to the iter on which the after-fired-from, not the iter that produced it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The halt case needed a special path.&lt;/strong&gt; As mentioned: the halting iter’s own &lt;code&gt;after&lt;/code&gt; fires &lt;em&gt;after&lt;/em&gt; the loop exits. v5 added a post-loop drain to handle it. That drain is its own code path, with its own substitution, separate from the in-loop dispatch.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The generator’s return type widened.&lt;/strong&gt; Because the post-loop drain needed to &lt;em&gt;return&lt;/em&gt; a final yielded value out of the iterator, &lt;code&gt;runStepByStep&lt;/code&gt;’s return type became &lt;code&gt;Generator&amp;lt;MachineState, MachineState | null&amp;gt;&lt;/code&gt; — a yield type &lt;em&gt;and&lt;/em&gt; a return type. The canonical &lt;code&gt;for..of&lt;/code&gt; consumer doesn’t see the return value, but anyone reading the type signature now had to understand why there were two slots.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All of this came from one design choice: dispatching &lt;code&gt;after K&lt;/code&gt; on iter K+1’s yield instead of on iter K’s own yield.&lt;/p&gt;

&lt;p&gt;In v6 I collapsed it (&lt;a href="https://github.com/mellonis/turing-machine-js/issues/119" rel="noopener noreferrer"&gt;turing-machine-js#119&lt;/a&gt;). The per-iter lifecycle is now plainly &lt;code&gt;before → step → after&lt;/code&gt;. The &lt;code&gt;after&lt;/code&gt; fire rides on the same yield as the iter that produced it. No substitution. No &lt;code&gt;pendingAfterFromPrev&lt;/code&gt;. No post-loop drain. The generator return type narrows back to &lt;code&gt;Generator&amp;lt;MachineState&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// v6&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onStep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* unchanged */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;onPause&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debugBreak&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;before:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debugBreak&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;after:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dispatch order &lt;em&gt;across&lt;/em&gt; hooks changed (any test asserting &lt;code&gt;pause(after K-1) → pause(before K) → step(K)&lt;/code&gt; had to flip to &lt;code&gt;pause(before K) → step(K) → pause(after K)&lt;/code&gt;), but the set of dispatched calls and per-iter semantics are unchanged. Consumers treating the hooks as independent observers see no change at all (&lt;a href="https://github.com/mellonis/turing-machine-js/issues/107" rel="noopener noreferrer"&gt;turing-machine-js#107&lt;/a&gt; — “expose un-substituted state” — disappeared as a problem: there’s no substitution to expose around).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule for next time:&lt;/strong&gt; if your hook payload requires substituting state from a different iteration than the one that’s yielding, the dispatch sequence is wrong. Lifecycle phases (&lt;code&gt;before&lt;/code&gt;, &lt;code&gt;step&lt;/code&gt;, &lt;code&gt;after&lt;/code&gt;) belong on the iter they describe. The substitution wasn’t a clever trick — it was a leak telling me the events were on the wrong tick.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I’d carry to the next pause/breakpoint API
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Name hooks for the consumer's verb, not the engine's event.&lt;/strong&gt; &lt;code&gt;onPause&lt;/code&gt;, not &lt;code&gt;onDebugBreak&lt;/code&gt;. &lt;code&gt;onProgress&lt;/code&gt;, not &lt;code&gt;onChunkReceived&lt;/code&gt;. The name leaks into every consumer's mental model — pick the framing you want them to inherit.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Throw on impossible configurations early.&lt;/strong&gt; “Permissive in input” is fine for shapes that &lt;em&gt;might&lt;/em&gt; be meaningful; it's a trap for shapes that &lt;em&gt;can't&lt;/em&gt; be. &lt;code&gt;haltState.debug.after = true&lt;/code&gt; had no possible semantics. Silently swallowing it cost an afternoon to one user before I noticed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Per-iter phases belong on the iter they describe.&lt;/strong&gt; If you're tempted to dispatch &lt;code&gt;after K&lt;/code&gt; on K+1's yield, ask what payload you have to substitute to make it look right. The substitution is the signal.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A master switch is cheap and worth it.&lt;/strong&gt; A boolean on the entry point to flip the whole feature off (without rewriting graph-level flags) makes the feature safe to ship to production and toggle in tests. &lt;code&gt;run({ debug: false })&lt;/code&gt; was one of the smaller v5 adds and one of the most-used by downstream consumers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Breaking changes are worth the major bump.&lt;/strong&gt; Three majors in two weeks sounds expensive. It isn't, if the lockstep downstream is one repo (in my case &lt;a href="https://github.com/mellonis/post-machine-js" rel="noopener noreferrer"&gt;&lt;code&gt;@post-machine-js/machine&lt;/code&gt;&lt;/a&gt;, which bumped its peer dep &lt;code&gt;^4.0.0&lt;/code&gt; → &lt;code&gt;^6.0.0&lt;/code&gt; and renamed its own internal &lt;code&gt;__onDebugBreak&lt;/code&gt; → &lt;code&gt;__onPause&lt;/code&gt; in step). API ergonomics compound across every consumer for the life of the library — pay the migration cost while the API is young.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The v6 shape is the one I should have shipped in v4. I didn't have the vocabulary then — I was thinking “expose the engine's events” instead of “design the consumer's contract”. Writing the docs is what surfaced both mistakes. There's a meta-lesson in there about how docs are the cheapest design review you can run, but that's another post.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Code:&lt;/em&gt; &lt;a href="https://github.com/mellonis/turing-machine-js" rel="noopener noreferrer"&gt;&lt;code&gt;turing-machine-js&lt;/code&gt;&lt;/a&gt; &lt;em&gt;(engine) and&lt;/em&gt; &lt;a href="https://github.com/mellonis/post-machine-js" rel="noopener noreferrer"&gt;&lt;code&gt;post-machine-js&lt;/code&gt;&lt;/a&gt; &lt;em&gt;(a Post machine built on top, kept in lockstep). The interactive demo at&lt;/em&gt; &lt;a href="https://demo.machines.mellonis.ru" rel="noopener noreferrer"&gt;demo.machines.mellonis.ru&lt;/a&gt; &lt;em&gt;consumes the v6&lt;/em&gt; &lt;code&gt;onPause&lt;/code&gt; &lt;em&gt;hook for its Step / Run / Stop controls.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>architecture</category>
      <category>javascript</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
