@turing-machine-js/machine 7.0.0-alpha.3 → 7.0.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,88 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [7.0.0-alpha.5] - 2026-05-25
8
+
9
+ Fifth v7 pre-release. Adds per-iter `matchedTransition` to `MachineState` so consumers can resolve the firing transition by graph id without re-deriving from `(source, nextState)` ambiguous pairs ([#205](https://github.com/mellonis/turing-machine-js/issues/205)), collapses `haltState.debug` to a `boolean` and moves the halt-imminent pause to the AFTER side of the halt-triggering iter so the wording + diagram cursor agree with the just-fired transition ([#207](https://github.com/mellonis/turing-machine-js/issues/207)), and renames the `GraphTransition.id` separator from `-` to `.` for parser-friendly splitting. Published under the `next` dist-tag: `npm install @turing-machine-js/machine@next`.
10
+
11
+ **Pre-release — the API surface may still shift before stable v7.0.0.** Pin to a specific alpha for reproducibility: `@turing-machine-js/machine@7.0.0-alpha.5`.
12
+
13
+ ### Added
14
+
15
+ - **`MachineState.matchedTransition`** ([#205](https://github.com/mellonis/turing-machine-js/issues/205)) — every yielded `MachineState` from `run` / `runStepByStep` now carries `{ id: string, matchKinds: ('wildcard' | 'literal')[] }`. `id` is `${stateId}.${transitionIx}` and resolves directly in `toGraph`'s output via `graph.nodes[stateId].transitions.find(t => t.id === ...)`. For wrapper-entry iters, `id` references the BARE's transition (wrappers delegate via `bareState`). `matchKinds` is per-tape, length = tape count; `'wildcard'` iff the winning alternative held `ifOtherSymbol` at that position. Eliminates `(source, nextState)` resolution ambiguity for tools doing exact-edge highlighting, per-transition coverage maps, or wildcard-aware log formatting.
16
+
17
+ - **`State.getMatchedTransition(symbol)`** ([#205](https://github.com/mellonis/turing-machine-js/issues/205)) — public method returning `{ nextState, matchedSymbol, ix }` for the symbol's transition. Same lookup `getNextState` does, plus the index — exposes the K used by `MachineState.matchedTransition.id` so callers needing both don't pay for the lookup twice.
18
+
19
+ - **`TapeBlock.patternKinds(symbol, currentSymbols?)`** ([#205](https://github.com/mellonis/turing-machine-js/issues/205)) — public method returning per-tape `'wildcard' | 'literal'` for the matched alternative's selector. The engine uses it internally to populate `matchedTransition.matchKinds`; exposed for consumers wiring custom dispatch.
20
+
21
+ - **`HaltState` typed alias** ([#207](https://github.com/mellonis/turing-machine-js/issues/207)) — the exported `haltState` singleton now carries a narrower TypeScript type (`State & { debug: boolean; ... }`) so `haltState.debug = true` is type-safe at the call site. Generic `State` references still see `DebugConfig` — runtime branches on `this.isHalt`.
22
+
23
+ ### Changed
24
+
25
+ - **`GraphTransition.id` separator: `-` → `.`** ([#205](https://github.com/mellonis/turing-machine-js/issues/205)). Was `${stateId}-${patternIx}`, now `${stateId}.${patternIx}`. Aligns with `matchedTransition.id` and makes the id splittable without colliding with user-defined state names that may contain `-`. **Breaking** for any consumer parsing the old separator; alpha-channel only.
26
+
27
+ - **`haltState.debug` is `boolean`** ([#207](https://github.com/mellonis/turing-machine-js/issues/207)). Was `DebugConfig` with per-side `{ before?, after? }` filters; under v7 it's a single `boolean` flag. `haltState.debug = true` enables; `false` / `null` disables. **Object-shaped writes throw** (`{ before: true }`, `{ after: true }`, etc.). The pause fires on the AFTER side of the iter whose transition leads to halt — `m.state` is the triggering state (not haltState), `m.debugBreak.after === true`. Diagram + log narratives read naturally: the halt-bound transition has fired when the pause lands, and halt is the next thing. **Breaking** — pre-alpha.5 consumers using `haltState.debug = { before: true }` must switch to `haltState.debug = true`.
28
+
29
+ ⚠️ **Chained-form `haltState.debug.before = true` silently no-ops in non-strict mode.** The getter returns the boolean; assigning `.before` to a primitive is a JS no-op outside strict mode (`TypeError` in strict). The engine setter never sees the write. Use the whole-object form (`haltState.debug = true` / `= false` / `= null`).
30
+
31
+ - **`State` internal: single `#getEntry` helper for `getCommand` / `getNextState` / `getMatchedTransition`** ([#206](https://github.com/mellonis/turing-machine-js/pull/206)). Internal refactor — no public API change. All three accessors now share one `.get()` lookup + unified throw message (`"No transition for symbol at state named ..."` — was per-method messages).
32
+
33
+ - **Internal: dead-code typeof check on `state.debug` in `runStepByStep` removed; `patternKinds` defensive paths now have dedicated test coverage** ([#210](https://github.com/mellonis/turing-machine-js/pull/210)). Engine-internal cleanup that bumps overall coverage to 99.35% statements / 96.58% branches / 100% functions / 99.48% lines.
34
+
35
+ ### Docs
36
+
37
+ - ⚠️ **README + CLAUDE.md** ([#209](https://github.com/mellonis/turing-machine-js/pull/209)) — warn that chained-form `haltState.debug.before = true` silently no-ops in non-strict mode (`new Function(...)`, `<script>` without `"use strict"`). Engine can't intercept (the write happens on a primitive after the getter returned); doc steers users to the whole-object form.
38
+
39
+ ## [7.0.0-alpha.4] - 2026-05-23
40
+
41
+ Fourth v7 pre-release. Adds an id-keyed lookup helper for the State graph ([#195](https://github.com/mellonis/turing-machine-js/issues/195)), fixes two upstream issues surfaced while wiring the new helper into downstream tooling — a `toMermaid` label-grammar bug ([#194](https://github.com/mellonis/turing-machine-js/issues/194)) and a halt-stack lifetime bug in `runStepByStep` ([#196](https://github.com/mellonis/turing-machine-js/issues/196)) — and extracts graph serialization into its own module without API change ([#180](https://github.com/mellonis/turing-machine-js/issues/180)). Published under the `next` dist-tag: `npm install @turing-machine-js/machine@next`.
42
+
43
+ **Pre-release — the API surface may still shift before stable v7.0.0.** Pin to a specific alpha for reproducibility: `@turing-machine-js/machine@7.0.0-alpha.4`.
44
+
45
+ ### Added
46
+
47
+ - **`State.collectStates(initialState, tapeBlock)`** ([#195](https://github.com/mellonis/turing-machine-js/issues/195)). Static helper that returns a `Map<number, {state: State; transitionSymbols: symbol[]}>` keyed by engine `GraphNode.id`. Lets downstream tooling (graph renderers, debugger panels) mutate `state.debug` on a specific State by numeric id, and set per-pattern breakpoints by `GraphTransition.id`. The K-th `transitionSymbols` slot is positionally aligned with the GraphTransition whose id is `${stateId}-${K}`, so a consumer holding `(stateId, patternIx)` from the rendered graph reaches the firing Symbol with no walk.
48
+
49
+ ```ts
50
+ const stateMap = State.collectStates(initial, tapeBlock);
51
+
52
+ // State-level breakpoint by id (any pattern fires).
53
+ stateMap.get(clickedStateId)!.state.debug.before = true;
54
+
55
+ // Per-pattern breakpoint by GraphTransition.id ("${stateId}-${patternIx}").
56
+ const [n, k] = clickedEdgeId.split('-').map(Number);
57
+ const entry = stateMap.get(n)!;
58
+ entry.state.debug.before = [entry.transitionSymbols[k]!];
59
+ ```
60
+
61
+ Coverage: regular / bare states get the full `[...#symbolToDataMap.keys()]` including `ifOtherSymbol` at its natural slot; wrappers and the halt singleton get empty `transitionSymbols`; synthetic halt markers (`isHaltMarker: true`, id `= -frameId`) are excluded — they all collapse to `haltState` at runtime, and the named consumer surfaces halt-pause via a separate UI control, not via clicks on halt glyphs. The halt singleton entry at id `0` is the process-wide `haltState` — toggling its `.debug` affects every machine in the runtime, same caveat as direct `haltState.debug` writes.
62
+
63
+ - **`StateMap` and `StateMapEntry` types** ([#195](https://github.com/mellonis/turing-machine-js/issues/195)). Exported from the package's public surface so TypeScript consumers can annotate `collectStates` results without re-deriving the shape.
64
+
65
+ ### Changed
66
+
67
+ - **Graph serialization extracted to `utilities/stateGraph.ts`** ([#180](https://github.com/mellonis/turing-machine-js/issues/180)). `State.toGraph` and `State.fromGraph` move out of the State class into a sibling module, alongside the new `collectStates`. Public surface preserved — `State.toGraph` / `State.fromGraph` remain as thin static delegates. State.ts shrank ~440 lines and now focuses on the runtime machinery (transitions, debug, halt-stack composition) rather than mixing in serialization concerns. A new `@internal` `STATE_INTERNAL` Symbol-keyed accessor on `State` gives sibling modules in `packages/machine/src` getter/setter access to private fields (`id`, `name`, `bareState`, `overriddenHaltState`, `symbolToDataMap`, `tags`); not re-exported from the public `index.ts`, so external consumers can't observe it.
68
+
69
+ ### Fixed
70
+
71
+ - **`toMermaid` edge labels containing literal `"` now parse correctly** ([#194](https://github.com/mellonis/turing-machine-js/issues/194)). Before: an alphabet that includes printable ASCII as literal symbols (e.g. a Brainfuck-flavored UTM whose data alphabet covers U+0020–U+007E) would emit an edge label like `s1 -- "['a'] → ['"']/[R]" --> s0`; Mermaid's parser terminated the string early on the inner `"`. After: user-supplied content (alphabet symbols, state names, tag names, frame bare names) is HTML-entity-escaped at the leaf — `&`, `"`, `<`, `>` to named entities; statement terminators (`\n`, `\r`), C0 controls minus `\t`, DEL, bidi controls, and lone UTF-16 surrogates to numeric entities. Printable Unicode (Cyrillic, CJK, accented Latin, etc.) passes through unchanged so non-ASCII alphabets stay readable in the emitted `.mmd`. `fromMermaid` mirrors with a single-pass entity decoder applied at the leaf, after structural parsing.
72
+
73
+ - **`runStepByStep` halt-stack is now run-scoped, not machine-scoped** ([#196](https://github.com/mellonis/turing-machine-js/issues/196)). Before: the `#stack` field on `TuringMachine` was an instance field; a build-time peek that didn't drain the generator (e.g. graph-construction utilities that ask for one yield to inspect the initial state) left leftover entries in the stack that were popped during the NEXT halt-bound transition, producing a "ghost iteration" and silently leaking memory across consecutive `runStepByStep` calls on the same machine. After: the halt stack is a local `const stack: State[] = []` declared inside `runStepByStep`, so each generator call starts with a clean stack and entries can't survive into the next call.
74
+
75
+ ### Compatibility
76
+
77
+ - Peer dep `@turing-machine-js/machine` widened `^7.0.0-alpha.3` → `^7.0.0-alpha.4` on `@turing-machine-js/builder`, `@turing-machine-js/library-binary-numbers`, `@turing-machine-js/library-binary-numbers-bare`.
78
+
79
+ ### Migration from alpha.3
80
+
81
+ Purely additive — no breaking changes. Existing code that doesn't call `State.collectStates` continues to work identically. `State.toGraph` / `State.fromGraph` behave identically (the delegate-to-stateGraph wiring is internal); no consumer changes needed.
82
+
83
+ The `STATE_INTERNAL` accessor is `@internal` and not part of the supported surface; ignore it unless you're authoring a sibling module inside `packages/machine/src`.
84
+
85
+ ### Out of v7-alpha.4 (still pending for stable v7.0.0)
86
+
87
+ - **[#102](https://github.com/mellonis/turing-machine-js/issues/102)** — debugger step-in / step-out / step-over primitives.
88
+
7
89
  ## [7.0.0-alpha.3] - 2026-05-21
8
90
 
9
91
  Third v7 pre-release. Adds first-class out-of-band tags on `State` ([#186](https://github.com/mellonis/turing-machine-js/issues/186)) — a metadata channel for visualization grouping and debugger labels that survives `toGraph` / `fromGraph` / `toMermaid` / `fromMermaid` round-trips. Driven by downstream [post-machine-js#86](https://github.com/mellonis/post-machine-js/issues/86), which will build a path-based registry and inline pseudo-command on top once this ships. Published under the `next` dist-tag: `npm install @turing-machine-js/machine@next`.
package/README.md CHANGED
@@ -242,6 +242,7 @@ Notable members and statics:
242
242
  - **`state.withOverriddenHaltState(other)`** — returns a copy whose would-be halt transitions fall through to `other`. The subroutine-call composition mechanism (see `library-binary-numbers/src/index.ts` for examples).
243
243
  - **`State.toGraph(state, tapeBlock)`** — walks the reachable graph from `state` and returns a serializable `Graph` (states, transitions, alphabets).
244
244
  - **`State.fromGraph(graph)`** — inverse of `toGraph`: rebuilds `State` instances + a fresh `TapeBlock` from a `Graph`. Round-trips together with `toMermaid` / `fromMermaid`.
245
+ - **`State.collectStates(state, tapeBlock)`** — walks the same graph and returns a `Map<number, {state, transitionSymbols}>` keyed by `GraphNode.id`. Use when downstream tooling holds a numeric id (e.g. a clicked node in a rendered graph) and needs the live `State` instance or the per-pattern `Symbol` for breakpoint setup. See [Setting breakpoints by graph id](#setting-breakpoints-by-graph-id).
245
246
 
246
247
  For visualization, pair `State.toGraph` with `toMermaid` to render the graph in any Mermaid-aware viewer (GitHub, VS Code, mermaid.live):
247
248
 
@@ -326,6 +327,7 @@ Each yielded `step` (`MachineState`) has these fields:
326
327
  | `movements` | `symbol[]` | per-tape head moves (`movements.left/right/stay`) |
327
328
  | `nextState` | `State` | the state that will execute next |
328
329
  | `debugBreak?` | `{ before?: true, after?: true }` | only set when `state.debug` matched on this iter — see *Debugging breakpoints* below |
330
+ | `matchedTransition` | `{ id: string, matchKinds: ('wildcard'\|'literal')[] }` | the transition the engine picked for this iter — see *Matched transition* below |
329
331
 
330
332
  `stepsLimit` (default `1e5`) guards against runaway loops — exceeding it throws.
331
333
 
@@ -345,6 +347,35 @@ Both APIs are first-class — `run()` is built on top of `runStepByStep()` (see
345
347
 
346
348
  **Don't split one logical flow across both APIs.** A consumer that wants stepwise UI *and* hook-driven breakpoints should use `run({ onStep, onPause, debug })` exclusively. Routing some operations through `runStepByStep()` and others through `run()` means `state.debug` only flows through one of the two paths — a subtle footgun where breakpoints silently disappear on whichever code path uses the generator directly. For per-iter throttle / "wait between steps" UIs, see [Throttle pattern](#throttle-pattern).
347
349
 
350
+ ### Matched transition
351
+
352
+ Every yielded `MachineState` carries a `matchedTransition` describing which transition the engine picked for that iter. The engine already resolves this via `state.getNextState(symbol)` internally; this field exposes the resolution to consumers so visualizations, log formatters, and coverage maps don't have to re-derive an ambiguous `(source, nextState)` pair (which collides when multiple transitions on the same source share a destination) or parse pattern strings from `toGraph`.
353
+
354
+ ```ts
355
+ matchedTransition: {
356
+ id: string; // resolvable in toGraph
357
+ matchKinds: ('wildcard' | 'literal')[]; // per-tape, length = tape count
358
+ }
359
+ ```
360
+
361
+ - **`id`** — `${stateId}.${transitionIx}`. Resolvable in `toGraph`'s output: `graph.nodes[stateId].transitions` has an entry with the matching `id`. For wrapper-entry iters (source is a wrapper produced by `withOverriddenHaltState`), `id` references the **bare's** transition — the wrapper's own `transitions` array in `toGraph` is empty because wrappers delegate, and the pattern actually lives on the bare. Detect by comparing `id.split('.')[0]` against `state.id`: different → wrapper delegation.
362
+
363
+ - **`matchKinds`** — per-tape match kind for the matched alternative's selector at each tape position. `'wildcard'` if the position held `ifOtherSymbol` (catch-all) in the winning alternative; `'literal'` if it held a specific symbol or symbol-list. Length always equals tape count.
364
+
365
+ Example use:
366
+
367
+ ```javascript
368
+ await machine.run({
369
+ initialState,
370
+ onStep: (m) => {
371
+ const wildcardPositions = m.matchedTransition.matchKinds // per-tape, e.g. ['wildcard', 'literal']
372
+ .map((k, i) => k === 'wildcard' ? i : -1)
373
+ .filter((i) => i >= 0);
374
+ console.log(`step ${m.step}: fired transition ${m.matchedTransition.id} (wildcards at tapes: ${wildcardPositions.join(',') || 'none'})`);
375
+ },
376
+ });
377
+ ```
378
+
348
379
  ## Subroutine composition with `withOverriddenHaltState`
349
380
 
350
381
  `state.withOverriddenHaltState(other)` returns a copy of `state` whose would-be halt transitions fall through to `other` at run time. The original is left untouched. This is the engine's only composition primitive — bigger machines are built by stacking smaller halt-on-completion subroutines.
@@ -466,14 +497,20 @@ myState.debug = { before: true };
466
497
  myState.debug = { before: [symA] };
467
498
  myState.debug = { before: [symA], after: [symA] };
468
499
 
469
- // Pause when the engine is about to enter halt (program exit OR subroutine pop):
470
- haltState.debug = { before: true };
500
+ // Pause when the engine is about to enter halt (program exit OR subroutine pop).
501
+ // haltState.debug is a `boolean` (#207) — halt is terminal, so there's only
502
+ // one meaningful pause moment (post-triggering-iter, before halt processing).
503
+ haltState.debug = true;
504
+ haltState.debug = false; // turn off
505
+ haltState.debug = null; // alias of false (reset)
471
506
 
472
- // Reset filters later — next read returns a fresh empty DebugConfig:
507
+ // Reset filters later on a regular state — next read returns a fresh empty DebugConfig:
473
508
  myState.debug = null;
474
509
  ```
475
510
 
476
- > ⚠️ **`haltState.debug.after` throws.** Halt is terminal there is no iteration-after-halt for an after-fire to anchor on. Assigning a truthy `.after` to `haltState.debug` (including `{ before: true, after: true }`) throws at write time. Symbol-list filters on `haltState.debug.before` are silent no-ops, since halt has no head symbol; only the wildcard `true` activates.
511
+ > ⚠️ **`haltState.debug` is `boolean`-only.** Any object-shaped write (`{ before: true }`, `{ after: true }`, `{ before: true, after: true }`) throws at write time. The pause fires on the AFTER side of the iter whose transition leads to halt — `m.state` is the triggering state (not haltState), `m.debugBreak.after === true`. Diagram + log narratives read naturally: the halt-bound transition has already fired when the pause lands, and halt is the next thing.
512
+
513
+ > ⚠️ **Chained-form `haltState.debug.before = true` doesn't throw in non-strict mode** — this is a JavaScript primitive quirk, not engine behavior. The getter returns the boolean `false`; assigning `.before` to that boolean is a no-op in non-strict mode (silent), a `TypeError` in strict mode. The engine setter only sees whole-object writes (`haltState.debug = X`), so it can't intercept the chained form. **Always use the whole-object form: `haltState.debug = true` / `= false` / `= null`.** Modules built with `tsc` / ESM run in strict mode by default and surface this throw; `new Function(...)` and `<script>` without `"use strict"` run non-strict and silently no-op the chained write.
477
514
 
478
515
  The `debug` field is mutable — toggle breakpoints at runtime without rebuilding the graph. The internal cell is shared with `state.withOverriddenHaltState(...)` wrappers, so an assignment on the original is visible from every wrapper. `state.debug` is always a `DebugConfig` instance (lazy-initialized on first read); plain-object input (`state.debug = { before: true }`) is wrapped in a fresh `DebugConfig` automatically. The instance itself is `Object.seal`-ed — typos like `state.debug.bofore = true` throw `TypeError` instead of silently creating a useless property. Per-property setters validate and freeze the stored array, so `state.debug.before.push(...)` also throws `TypeError`.
479
516
 
@@ -499,6 +536,36 @@ If `onPause` is not provided, breaks fire-and-resume invisibly — the trajector
499
536
 
500
537
  **Caveat:** `haltState` is a module-level singleton. Setting `haltState.debug` affects every machine in the process; clear in `afterEach` / `finally` for test isolation.
501
538
 
539
+ ### Setting breakpoints by graph id
540
+
541
+ Downstream UIs (graph renderers, debugger panels) often have only a numeric `GraphNode.id` — the user clicked a state node, or a transition edge in a rendered SVG. `State.collectStates(initial, tapeBlock)` returns a `Map` keyed by that numeric id, with the live `State` instance and the per-pattern `Symbol` array as its value:
542
+
543
+ ```ts
544
+ import { State, ifOtherSymbol } from '@turing-machine-js/machine';
545
+
546
+ const stateMap = State.collectStates(initial, tapeBlock);
547
+
548
+ // Toggle a state-level breakpoint by id (any pattern triggers).
549
+ const entry = stateMap.get(clickedStateId);
550
+ if (entry) {
551
+ entry.state.debug.before = true;
552
+ }
553
+
554
+ // Per-pattern breakpoint by GraphTransition.id — the contract is
555
+ // positional: `transitionSymbols[K]` is the Symbol that the
556
+ // `${stateId}-${K}` GraphTransition fires on.
557
+ const [n, k] = clickedEdgeId.split('-').map(Number);
558
+ const e = stateMap.get(n);
559
+ const sym = e?.transitionSymbols[k];
560
+ if (e && sym) {
561
+ e.state.debug.before = [sym];
562
+ }
563
+ ```
564
+
565
+ **Coverage rules:** regular / bare states get the full `[...#symbolToDataMap.keys()]` including `ifOtherSymbol` at its natural slot; wrappers and the halt singleton get empty `transitionSymbols`; synthetic halt markers (Graph nodes with `id = -frameId`, one per callable-subtree frame) are excluded from the map. See `State.collectStates` JSDoc for the full contract.
566
+
567
+ > ⚠️ `stateMap.get(0)!.state === haltState` — the entry at id `0` is the process-wide halt singleton. Toggling its `debug` affects every machine in the runtime, same caveat as direct `haltState.debug` writes.
568
+
502
569
  ### Throttle pattern
503
570
 
504
571
  For per-iter throttle / animation / "wait between steps" UIs, use the **`onIter`** hook — an awaited callback that fires once at the end of every iter, after both `onPause` dispatches on the same yield. It's the engine-native shape for per-iter coordination:
@@ -677,13 +744,15 @@ Reading `['0',*] → [K,'0']/[R,R]`:
677
744
  API surface changes since v3, in past tense so the timing of each piece is explicit:
678
745
 
679
746
  - **v4** — `run()` became async (`Promise<void>`). Per-state runtime breakpoints landed (`state.debug.before` / `state.debug.after`); `run()` accepted an `onDebugBreak` hook. `MachineState` exposed on each yield.
680
- - **v5** — `onDebugBreak` renamed to `onPause`. New `run({ debug: boolean })` master switch suppresses all `onPause` dispatches without unsetting `state.debug` assignments. Assigning a truthy `.after` to `haltState.debug` now throws at write time (halt is terminal — no iteration-after-halt to anchor on).
747
+ - **v5** — `onDebugBreak` renamed to `onPause`. New `run({ debug: boolean })` master switch suppresses all `onPause` dispatches without unsetting `state.debug` assignments. Assigning a truthy `.after` to `haltState.debug` now throws at write time (halt is terminal — no iteration-after-halt to anchor on). *Superseded in v7 by #207: `haltState.debug` is now `boolean`, all object-shaped writes throw.*
681
748
  - **v6** — Per-iter lifecycle reordered to `before → step → after`, all firing on the same yield. Previously `after` fired on iter K+1's tick with a `prevYield` substitution dance; that substitution is gone. The `MachineState.debugBreak` field shape is unchanged across all three versions.
682
749
  - **v6.1** — `state.debug` ergonomics: the field is now always a non-null `DebugConfig` instance (lazy-initialized on first read), so chained field writes like `state.debug.before = true` work on a fresh state without a prior whole-object assignment. The `DebugConfig` instance is `Object.seal`-ed, so typos like `state.debug.bofore = true` throw `TypeError` at write time instead of silently creating a useless property. `state.debug = null` continues to work but semantically means "reset filters" — the next read returns a fresh empty `DebugConfig` (#150).
683
750
  - **v6.2** *(superseded by v6.3.0)* — widened `onStep`'s signature to `(m) => void | Promise<void>` and added an inline `await onStep(...)` in the run loop, enabling throttle-in-`onStep` patterns. This overturned the docstring-stated contract that `onStep` is sync (microtask-free); the right place for per-iter throttling is `onPause` with self-rearm (see [Throttle pattern](#throttle-pattern)). Restored in v6.3.0.
684
751
  - **v6.3** — `onStep` reverted to its v6.0–v6.1 sync contract — `(m) => void`, called synchronously inside the run loop. The Throttle pattern section documents the engine-native shape for per-iter throttle / "wait between iters" UIs. No other API changes.
685
752
  - **v6.4** — New **`onIter`** hook on `run()`: awaited, fires once at the end of every iter (after both `onPause` dispatches on the same yield), unaffected by the `debug` master switch. Use for per-iter throttle / animation / coordination needing a suspend point; complements the existing sync `onStep` (tracing) and conditional `onPause` (user breakpoints). Three-hook contract is now `onStep` (sync, mid-iter) / `onPause` (awaited, on `state.debug` match) / `onIter` (awaited, end-of-iter). Additive — peer-deps unchanged. The v6.3.0 README's `onPause`-rearm throttle workaround is superseded.
686
- - **v7** *(latest alpha: alpha.3, 2026-05-21)* — Composition-representation overhaul + first-class state tags. **Pre-release on the `next` dist-tag:** `npm install @turing-machine-js/machine@next` (or pin `@7.0.0-alpha.3`). Stable v7.0.0 still pending [#102](https://github.com/mellonis/turing-machine-js/issues/102) (debugger step-in/over/out primitives). Highlights across alphas:
753
+ - **v7** *(latest alpha: alpha.4, 2026-05-23)* — Composition-representation overhaul + first-class state tags + id-keyed `State.collectStates` lookup. **Pre-release on the `next` dist-tag:** `npm install @turing-machine-js/machine@next` (or pin `@7.0.0-alpha.4`). Stable v7.0.0 still pending [#102](https://github.com/mellonis/turing-machine-js/issues/102) (debugger step-in/over/out primitives). Highlights across alphas:
754
+
755
+ **alpha.4** — **`State.collectStates(initial, tapeBlock)`** ([#195](https://github.com/mellonis/turing-machine-js/issues/195)) returns a `Map<number, {state, transitionSymbols}>` keyed by `GraphNode.id` so downstream tooling can mutate `state.debug` by numeric id and set per-pattern breakpoints by `GraphTransition.id`. Graph serialization extracted to `utilities/stateGraph.ts` with a Symbol-keyed `@internal` accessor on `State` ([#180](https://github.com/mellonis/turing-machine-js/issues/180); no public-API change — the `State.toGraph` / `.fromGraph` statics remain as thin delegates). Two upstream fixes: `toMermaid` HTML-entity-escapes user content in labels so alphabets containing `"`, `<`, etc. parse correctly ([#194](https://github.com/mellonis/turing-machine-js/issues/194)); `runStepByStep`'s halt stack is now run-scoped, fixing a memory leak / ghost-iteration when the same `TuringMachine` instance is reused across calls ([#196](https://github.com/mellonis/turing-machine-js/issues/196)). See [§Setting breakpoints by graph id](#setting-breakpoints-by-graph-id).
687
756
 
688
757
  **alpha.3** — first-class **State tags** ([#186](https://github.com/mellonis/turing-machine-js/issues/186)). `state.tag(...) / .untag(...) / .tags` API; `GraphNode.tags: string[]` round-trips through `toGraph`/`fromGraph`; `toMermaid` emits tags two ways simultaneously — inline via `<br>` in node labels (`sN["name<br>tag1, tag2"]`) and as `classDef`/`class` for color grouping. Tags live on the State instance (not on the shared `#symbolToDataMap`), so engine [#175](https://github.com/mellonis/turing-machine-js/issues/175) memoization doesn't leak tags across wrappers sharing a bare. See [§State tags](#state-tags).
689
758
 
@@ -3,8 +3,31 @@ import Reference from './Reference';
3
3
  import TapeBlock from './TapeBlock';
4
4
  import TapeCommand from './TapeCommand';
5
5
  import { type Graph } from '../utilities/graph';
6
+ import { type StateMap } from '../utilities/stateGraph';
6
7
  export declare const ifOtherSymbol: unique symbol;
7
8
  declare const validateDebugFilter: unique symbol;
9
+ /**
10
+ * @internal
11
+ *
12
+ * Package-private accessor key for sibling modules in
13
+ * `packages/machine/src` (e.g. `utilities/stateGraph.ts`, and the planned
14
+ * `utilities/stateCollect.ts` for #195). Re-exported from this module so
15
+ * sibling files can import it; intentionally NOT re-exported from the
16
+ * package's public `index.ts`, so downstream consumers don't see it on
17
+ * the supported surface.
18
+ *
19
+ * Calling `state[STATE_INTERNAL]()` returns a getter/setter view onto the
20
+ * State's private fields. Reads are live (they close over `this`), so the
21
+ * view stays in sync with subsequent mutations on the State. There's one
22
+ * mutating setter on the view — `name` — used exclusively by
23
+ * `fromGraph` to assign graph-sourced composite names (e.g. `A(target)`)
24
+ * that the public name validator would reject; see the JSDoc on the
25
+ * accessor itself.
26
+ *
27
+ * Designed in #180 with #195 in mind so its surface doesn't need to grow
28
+ * when `collectStates` lands.
29
+ */
30
+ export declare const STATE_INTERNAL: unique symbol;
8
31
  export declare class DebugConfig {
9
32
  #private;
10
33
  constructor(ownerState: State, initial?: {
@@ -50,12 +73,71 @@ export default class State {
50
73
  * code uses). Order matches insertion order of the underlying Set.
51
74
  */
52
75
  get tags(): readonly string[];
53
- /** @internal — invoked by DebugConfig setters via module-private symbol. */
76
+ /** @internal — invoked by DebugConfig setters via module-private symbol.
77
+ * Per #207, haltState no longer flows through DebugConfig (its `debug`
78
+ * setter rejects object writes before construction), so the validator
79
+ * only sees non-halt states here. */
54
80
  [validateDebugFilter](fieldName: 'before' | 'after', filter: readonly symbol[] | true | undefined): void;
55
81
  getSymbol(tapeBlock: TapeBlock): symbol;
56
82
  getCommand(symbol: symbol): Command;
57
83
  getNextState(symbol: symbol): State | Reference;
84
+ /**
85
+ * Like `getNextState`, but also returns the matched Symbol and its index
86
+ * in this State's transition declaration order (= the `K` in `toGraph`'s
87
+ * `${stateId}.${K}` transition ids). Used by `TuringMachine.runStepByStep`
88
+ * to populate `MachineState.matchedTransition` for #205 — exposes which
89
+ * transition fired so consumers (UIs, log tools, coverage maps) can
90
+ * resolve the firing edge without re-deriving from `(source, nextState)`,
91
+ * which is ambiguous when multiple transitions on the same source go to
92
+ * the same destination.
93
+ *
94
+ * Throws (matching `getNextState` / `getCommand`) when no entry exists for
95
+ * the symbol. For wrappers (states produced by `withOverriddenHaltState`):
96
+ * the symbol-to-data map is shared with the bare via `bareState`, so the
97
+ * returned `ix` is a valid position into BOTH the wrapper's and the
98
+ * bare's transition iteration order — they're the same map.
99
+ */
100
+ getMatchedTransition(symbol: symbol): {
101
+ nextState: State | Reference;
102
+ matchedSymbol: symbol;
103
+ ix: number;
104
+ };
58
105
  withOverriddenHaltState(overriddenHaltState: State): State;
106
+ /**
107
+ * @internal
108
+ *
109
+ * Package-private getter/setter view onto this State's private fields,
110
+ * for sibling modules in `packages/machine/src` (currently `stateGraph.ts`
111
+ * for `toGraph` / `fromGraph`, and the planned `stateCollect.ts` for
112
+ * #195's `collectStates`).
113
+ *
114
+ * Read access is live — the getters close over `this`, so the view
115
+ * stays in sync with subsequent mutations on this State. There's a
116
+ * single mutating setter on the view, `name`, which exists to let
117
+ * `fromGraph` assign graph-sourced composite names (e.g. `A(target)`)
118
+ * to freshly-constructed bare States. The constructor's name validator
119
+ * rejects parens (reserved as wrapper-composition delimiters in
120
+ * `withOverriddenHaltState`); the setter intentionally bypasses that
121
+ * check because the same delimiters appear in legitimate wrapper-bare
122
+ * names round-tripped through the graph.
123
+ *
124
+ * Returns a fresh view object on every call — cheap enough for the
125
+ * BFS-once-per-build callers, and avoids holding a reference object on
126
+ * every State instance. Keep this surface tight: callers should only
127
+ * read what they need. Adding fields here is a deliberate decision —
128
+ * each adds to the implicit contract sibling modules can rely on.
129
+ */
130
+ [STATE_INTERNAL](): {
131
+ readonly id: number;
132
+ name: string;
133
+ readonly bareState: State | null;
134
+ readonly overriddenHaltState: State | null;
135
+ readonly symbolToDataMap: Map<symbol, {
136
+ command: Command;
137
+ nextState: State | Reference;
138
+ }>;
139
+ readonly tags: ReadonlySet<string>;
140
+ };
59
141
  static inspect(state: State): {
60
142
  id: number;
61
143
  name: string;
@@ -76,12 +158,56 @@ export default class State {
76
158
  } | null;
77
159
  }>;
78
160
  };
161
+ /**
162
+ * Walks the reachable State graph from `initialState` and returns a
163
+ * serializable `Graph`. Thin delegate to `utilities/stateGraph.ts`'s
164
+ * `toGraph` (extracted in #180); see that module for the BFS shape and
165
+ * v7 callable-subtree emit semantics.
166
+ */
79
167
  static toGraph(initialState: State, tapeBlock: TapeBlock): Graph;
168
+ /**
169
+ * Inverse of `toGraph`: rebuilds a State graph and a fresh TapeBlock
170
+ * from a serialized `Graph`. Thin delegate to `utilities/stateGraph.ts`'s
171
+ * `fromGraph` (extracted in #180); see that module for the
172
+ * reconstruction pass shape (Reference pre-create, bare build, wrapper
173
+ * resolution via `withOverriddenHaltState`, ref binding).
174
+ */
80
175
  static fromGraph(graph: Graph): {
81
176
  start: State;
82
177
  tapeBlock: TapeBlock;
83
178
  states: Record<number, State>;
84
179
  };
180
+ /**
181
+ * Returns a `Map<number, {state, transitionSymbols}>` keyed by engine
182
+ * `GraphNode.id`, exposing the live `State` instance + per-pattern
183
+ * Symbol references for each node so downstream tooling can mutate
184
+ * `state.debug` by numeric id and set per-pattern breakpoints by
185
+ * `GraphTransition.id` (#195). Thin delegate to
186
+ * `utilities/stateGraph.ts`'s `collectStates`; see that module for
187
+ * the alignment contract, coverage rules, and halt-singleton warning.
188
+ */
189
+ static collectStates(initialState: State, tapeBlock: TapeBlock): StateMap;
85
190
  }
86
- export declare const haltState: State;
191
+ /**
192
+ * Typed alias for the haltState singleton (#207). Narrows `debug` from
193
+ * the generic-State `DebugConfig | boolean` union to plain `boolean`,
194
+ * giving compile-time type-safety at the singleton's call sites:
195
+ *
196
+ * ```ts
197
+ * haltState.debug = true; // ok
198
+ * haltState.debug = false; // ok
199
+ * haltState.debug = { before: true } // TS error
200
+ * const isOn = haltState.debug; // typed `boolean`
201
+ * ```
202
+ *
203
+ * Anyone holding a `State` reference that happens to BE the singleton (e.g.
204
+ * via `state.getNextState(sym).ref === haltState`) sees the wider `State`
205
+ * type; runtime throws guide them to the right shape. The singleton export
206
+ * is the canonical access path.
207
+ */
208
+ export type HaltState = State & {
209
+ get debug(): boolean;
210
+ set debug(value: boolean | null);
211
+ };
212
+ export declare const haltState: HaltState;
87
213
  export {};
@@ -23,6 +23,31 @@ export default class TapeBlock {
23
23
  currentSymbols?: string[];
24
24
  symbol: symbol;
25
25
  }): boolean;
26
+ /**
27
+ * For a Symbol returned by `this.symbol([...])` (or the catch-all
28
+ * `ifOtherSymbol`), returns the per-tape match kind for the
29
+ * **alternative that actually matched** given `currentSymbols`:
30
+ * `'wildcard'` if that tape position was `ifOtherSymbol` in the winning
31
+ * alternative, `'literal'` otherwise. Length always equals the tape
32
+ * count.
33
+ *
34
+ * Used by `TuringMachine.runStepByStep` to populate
35
+ * `MachineState.matchedTransition.matchKinds` for #205. The "winning
36
+ * alternative" disambiguation matters for alternations like
37
+ * `[[ifOtherSymbol, 'c'], ['a', 'b']]` — different alternatives can
38
+ * have different per-tape kinds, and only the alternative that matched
39
+ * the current head symbols is meaningful.
40
+ *
41
+ * - `ifOtherSymbol` (the State's catch-all transition fired): all
42
+ * positions are `'wildcard'`.
43
+ * - Symbol with patternList: find the first alternative that matches
44
+ * `currentSymbols` (same predicate as `isMatched`), return its
45
+ * per-position kinds.
46
+ * - Symbol with no winning alternative under the given `currentSymbols`
47
+ * (defensive — shouldn't happen if the caller resolved the Symbol via
48
+ * the State's normal matching): fall back to all `'literal'`.
49
+ */
50
+ patternKinds(symbol: symbol, currentSymbols?: string[]): ('wildcard' | 'literal')[];
26
51
  replaceTape(tape: Tape, tapeIx?: number): void;
27
52
  }
28
53
  export {};
@@ -26,6 +26,29 @@ export type MachineState = {
26
26
  before?: true;
27
27
  after?: true;
28
28
  };
29
+ /**
30
+ * The transition the engine picked for this iter (#205). Always present
31
+ * — `runStepByStep` resolves it at the very start of every iter via
32
+ * `state.getMatchedTransition(symbol)`, well before any callback fires.
33
+ *
34
+ * - `id` — resolvable in `toGraph`'s output: `graph.nodes[…].transitions`
35
+ * contains a `GraphTransition` whose `.id` equals this value. Format is
36
+ * `${stateId}.${transitionIx}`. **For wrapper-entry iters (`state` is
37
+ * produced by `withOverriddenHaltState`): the wrapper's own
38
+ * `transitions` array in `toGraph` is empty because wrappers delegate
39
+ * to the bare; this field carries the BARE's transition id, where the
40
+ * pattern actually lives.** Consumers can detect this case by
41
+ * comparing `id.split('.')[0]` against `state.id` — different = wrapper
42
+ * delegation.
43
+ * - `matchKinds` — per-tape match kind for the picked transition's
44
+ * pattern at each tape position. `'wildcard'` if the matched
45
+ * alternative had `ifOtherSymbol` at that position, `'literal'`
46
+ * otherwise. Length equals tape count.
47
+ */
48
+ matchedTransition: {
49
+ id: string;
50
+ matchKinds: ('wildcard' | 'literal')[];
51
+ };
29
52
  };
30
53
  export default class TuringMachine {
31
54
  #private;