@turing-machine-js/visuals 7.0.0-alpha.6.1 → 7.0.0-alpha.7

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +237 -11
  3. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -4,6 +4,10 @@ All notable changes to this package 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.7] - 2026-05-30
8
+
9
+ Lockstep re-alignment with the engine 7.0.0-alpha.7 bump (engine [#213](https://github.com/mellonis/turing-machine-js/issues/213) `CallFrame` extraction + [#223](https://github.com/mellonis/turing-machine-js/issues/223) `toMermaid` framed-wrapper emit fix). No source or behavior changes in this package since alpha.6.1. Peer dep `@turing-machine-js/machine` widened `^7.0.0-alpha.6` → `^7.0.0-alpha.7`.
10
+
7
11
  ## [7.0.0-alpha.6.1] - 2026-05-30
8
12
 
9
13
  ### Added
package/README.md CHANGED
@@ -1,21 +1,247 @@
1
1
  # @turing-machine-js/visuals
2
2
 
3
- Pure highlight + graph-indexing logic for [`@turing-machine-js/machine`](../machine). No DOM, no Svelte, no Mermaid — consumers bring their own renderer and DOM applier.
3
+ Pure highlight + graph-indexing logic for [`@turing-machine-js/machine`](../machine) — plus a renderer-agnostic edge-label formatter and a `recordSnippet` artifact recorder for prerecorded playback (article embeds, landing-page panels, terminal tools). No DOM, no Svelte, no Mermaid — consumers bring their own renderer.
4
4
 
5
- ## Scope
5
+ ## When to reach for this
6
6
 
7
- Types and pure functions for:
8
- - Indexing an engine `Graph` for wrapper/bare lookup (`indexGraph`, `bareIdOf`, `highlightExpand`).
9
- - Applying highlight + indicator operations against a renderer-agnostic `HighlightOps` interface (`applyHighlight`, `applyIndicator`).
10
-
11
- See [`docs/graph-highlight-and-breakpoints.md`](./docs/graph-highlight-and-breakpoints.md) for the full set of rules these functions satisfy.
12
-
13
- ## Versioning
14
-
15
- Lockstep with `@turing-machine-js/machine`.
7
+ - You have an engine `Graph` (from `State.toGraph(initialState, tapeBlock)`) and want to render it with **runtime highlight cues** — `from → edge → to` triples that animate as the machine steps, breakpoint dots, frame-active subgraph marks, pulse-on-revisit. Implement a small `HighlightOps` object that wraps your renderer's DOM/canvas/ANSI primitives; `applyHighlight` decides *what* to highlight, your `HighlightOps` decides *how*.
8
+ - You want a logged step's notation to **line up byte-for-byte** with the rendered state graph's edge labels. `formatStepNotation` mirrors the engine's `toMermaid` edge-label vocabulary; same `[reads] → [writes]/[moves]`, same `'X'` / `B` / `*='X'` / `K='X'` / `E` shortcuts.
9
+ - You want to **record a machine run** as a self-contained playback artifact — for an article embed, a landing-page panel, a snapshot test, anything that needs to replay a run without booting an engine. `recordSnippet` produces a `Snippet` with one `Frame` per iter (tape state + per-tape `{ movement, read, write }` command + highlight + optional log line); a player walks frames forward AND backward without recomputing deltas.
16
10
 
17
11
  ## Install
18
12
 
19
13
  ```sh
20
14
  npm install @turing-machine-js/visuals @turing-machine-js/machine
21
15
  ```
16
+
17
+ Both prereleases on npm `next`:
18
+
19
+ ```sh
20
+ npm install @turing-machine-js/visuals@next @turing-machine-js/machine@next
21
+ ```
22
+
23
+ Peer-deps on `@turing-machine-js/machine@^7.0.0-alpha.6` — visuals follows engine v7 alphas with occasional visuals-only patches (`7.0.0-alpha.6.1` added the formatter primitives + token surface on top of the lockstep alpha.6 engine release).
24
+
25
+ ## Public API
26
+
27
+ ### Types
28
+
29
+ ```ts
30
+ // Highlight contract
31
+ type NodeKey = number | 'idle';
32
+ type HighlightClass = 'mg-highlight-from' | 'mg-highlight-to' | 'mg-highlight-strong';
33
+ interface HighlightOps {
34
+ addNodeClass(id: NodeKey, cls: HighlightClass): void;
35
+ highlightEdge(fromKey: string, toKey: string): void;
36
+ markFrameActive(frameId: number): void;
37
+ pulse(id: NodeKey): void;
38
+ scrollIntoView(id: NodeKey): void;
39
+ }
40
+ interface IndicatorOps {
41
+ setBreakpoint(id: NodeKey, on: boolean): void;
42
+ }
43
+ type GraphHighlight = {
44
+ fromId: number | 'idle';
45
+ toId: number | null;
46
+ strong: 'from' | 'to';
47
+ paused: boolean;
48
+ };
49
+ type GraphIndexes = { /* node→frame, frame→wrappers, frame→label, etc. */ };
50
+
51
+ // Recording artifact
52
+ type TapeSnapshot = { symbols: string[]; position: number };
53
+ type StepCommand = { movement: 'L' | 'R' | 'S'; symbol: string | null };
54
+ type Frame = {
55
+ step: number;
56
+ tape: TapeSnapshot[];
57
+ commands?: { movement: 'L' | 'R' | 'S'; read: string; write: string }[];
58
+ highlight: GraphHighlight | null;
59
+ log?: string;
60
+ };
61
+ type Snippet = {
62
+ version: 1;
63
+ name?: string;
64
+ graph: Graph;
65
+ alphabets: string[][];
66
+ frames: Frame[];
67
+ };
68
+
69
+ // Token surface (renderer-agnostic alternative to formatStepNotation strings)
70
+ type ReadToken =
71
+ | { kind: 'literal'; symbol: string }
72
+ | { kind: 'blank' }
73
+ | { kind: 'wildcard'; symbol: string };
74
+ type WriteToken =
75
+ | { kind: 'literal'; symbol: string }
76
+ | { kind: 'erase' }
77
+ | { kind: 'keep'; readContext?: { symbol: string; isBlank: boolean } };
78
+ type StepTokens = {
79
+ reads: readonly ReadToken[] | null; // null = manual-Apply path (no transition fired)
80
+ writes: readonly WriteToken[];
81
+ moves: readonly ('L' | 'R' | 'S')[];
82
+ };
83
+ ```
84
+
85
+ ### Functions
86
+
87
+ ```ts
88
+ indexGraph(graph): GraphIndexes
89
+ applyHighlight(highlight, graph, indexes, prev, ops): { nextPrev }
90
+ applyIndicator(breakpoints, graph, ops): void
91
+ bareIdOf(id, graph): number
92
+ highlightExpand(id, graph): number[]
93
+ equivalentIds(id, graph): number[]
94
+ recordingOps(): { highlight: HighlightOps; indicator: IndicatorOps; record: RecordedOp[] }
95
+
96
+ // Formatters
97
+ formatStepNotation(reads, commands, blanks, matchKinds?): string
98
+ tokenizeStep(reads, commands, blanks, matchKinds?): StepTokens
99
+ formatTape(tape): string
100
+ formatCommand(tapeCommand): string // alpha.6 single-command formatter; kept for back-compat
101
+ formatStep(machineState): string // alpha.6 MachineState-based formatter; kept for back-compat
102
+
103
+ // Recording
104
+ recordSnippet({ machine, initialState, graph, alphabets, name?, maxSteps?, log? }): Snippet
105
+ ```
106
+
107
+ The 16-rule contract `applyHighlight` satisfies is documented at [`docs/graph-highlight-and-breakpoints.md`](./docs/graph-highlight-and-breakpoints.md).
108
+
109
+ ## Example: applying highlight in a DOM renderer
110
+
111
+ ```ts
112
+ import {
113
+ applyHighlight,
114
+ indexGraph,
115
+ type HighlightOps,
116
+ type GraphHighlight,
117
+ } from '@turing-machine-js/visuals';
118
+ import { State } from '@turing-machine-js/machine';
119
+
120
+ // 1. Build your machine + Graph elsewhere (typical Svelte / React / vanilla code).
121
+ const graph = State.toGraph(initialState, tapeBlock);
122
+ const indexes = indexGraph(graph);
123
+
124
+ // 2. Tiny DOM applier (illustrative — your renderer probably has a richer one).
125
+ function domOps(svgRoot: SVGSVGElement): HighlightOps {
126
+ const node = (id: number | 'idle') =>
127
+ svgRoot.querySelector(`g.node[data-id="${id === 'idle' ? 'idle' : `s${id}`}"]`);
128
+ return {
129
+ addNodeClass: (id, cls) => node(id)?.classList.add(cls),
130
+ highlightEdge: (from, to) =>
131
+ svgRoot
132
+ .querySelector(`path[data-id^="L_${from}_${to}_"]`)
133
+ ?.classList.add('mg-highlight-edge'),
134
+ markFrameActive: (frameId) =>
135
+ svgRoot.querySelector(`g.cluster[data-id="w_${frameId}"]`)?.classList.add('mg-frame-active'),
136
+ pulse: (id) => node(id)?.classList.add('mg-pulse'),
137
+ scrollIntoView: (id) => node(id)?.scrollIntoView({ block: 'nearest', behavior: 'smooth' }),
138
+ };
139
+ }
140
+
141
+ // 3. On each engine pause / step: wipe previous highlight from the DOM, then apply the new one.
142
+ // HighlightOps is purely additive — the consumer is responsible for clearing
143
+ // `mg-highlight-*` classes before calling applyHighlight.
144
+ let prev = null;
145
+ function onMachineStep(highlight: GraphHighlight | null, svgRoot: SVGSVGElement) {
146
+ // Wipe.
147
+ for (const el of svgRoot.querySelectorAll(
148
+ '.mg-highlight-from, .mg-highlight-to, .mg-highlight-strong, .mg-highlight-edge, .mg-frame-active',
149
+ )) {
150
+ el.classList.remove(
151
+ 'mg-highlight-from',
152
+ 'mg-highlight-to',
153
+ 'mg-highlight-strong',
154
+ 'mg-highlight-edge',
155
+ 'mg-frame-active',
156
+ );
157
+ }
158
+ if (!highlight) return;
159
+ const result = applyHighlight(highlight, graph, indexes, prev, domOps(svgRoot));
160
+ prev = result.nextPrev;
161
+ }
162
+ ```
163
+
164
+ ## Example: rendering a step's edge-label notation
165
+
166
+ ```ts
167
+ import { formatStepNotation, type StepCommand } from '@turing-machine-js/visuals';
168
+
169
+ const commands: StepCommand[] = [
170
+ { movement: 'R', symbol: 'b' }, // write 'b', move right
171
+ { movement: 'L', symbol: null }, // keep current symbol, move left
172
+ ];
173
+ const reads = ['a', 'x'];
174
+ const blanks = [' ', ' '];
175
+ const matchKinds = ['literal', 'wildcard'] as const;
176
+
177
+ formatStepNotation(reads, commands, blanks, matchKinds);
178
+ // => "['a',*='x'] → ['b',K='x']/[R,L]"
179
+ ```
180
+
181
+ For manual-Apply rendering (no transition fired), pass `reads: null`:
182
+
183
+ ```ts
184
+ formatStepNotation(null, [{ movement: 'R', symbol: 'b' }], [' '], null);
185
+ // => "['b']/[R]"
186
+ ```
187
+
188
+ ## Example: tokenize for custom (non-string) rendering
189
+
190
+ ```ts
191
+ import { tokenizeStep } from '@turing-machine-js/visuals';
192
+
193
+ const tokens = tokenizeStep(['a'], [{ movement: 'R', symbol: 'b' }], [' '], ['literal']);
194
+ // tokens.reads: [{ kind: 'literal', symbol: 'a' }]
195
+ // tokens.writes: [{ kind: 'literal', symbol: 'b' }]
196
+ // tokens.moves: ['R']
197
+
198
+ // Now render however you want — HTML spans with CSS classes, ANSI escapes, JSON, etc.
199
+ function readToHtml(t: typeof tokens.reads[0]) {
200
+ if (t.kind === 'wildcard') return `<span class="cell-wildcard">${t.symbol}</span>`;
201
+ if (t.kind === 'blank') return `<span class="cell-blank">␣</span>`;
202
+ return `<span class="cell-literal">${t.symbol}</span>`;
203
+ }
204
+ ```
205
+
206
+ ## Example: recording a snippet
207
+
208
+ ```ts
209
+ import { recordSnippet, formatStep } from '@turing-machine-js/visuals';
210
+ import {
211
+ Alphabet, Tape, TapeBlock, TuringMachine, State, haltState, movements,
212
+ } from '@turing-machine-js/machine';
213
+
214
+ const alphabet = new Alphabet([' ', 'a', 'b']);
215
+ const tape = new Tape({ alphabet, symbols: ['a', 'a', 'a'] });
216
+ const tapeBlock = TapeBlock.fromTapes([tape]);
217
+ const machine = new TuringMachine({ tapeBlock });
218
+ const initialState = new State({
219
+ [tapeBlock.symbol(['a'])]: { command: [{ symbol: 'b', movement: movements.right }] },
220
+ [tapeBlock.symbol([' '])]: { nextState: haltState },
221
+ });
222
+
223
+ const snippet = recordSnippet({
224
+ machine,
225
+ initialState,
226
+ graph: State.toGraph(initialState, tapeBlock),
227
+ alphabets: [[' ', 'a', 'b']],
228
+ name: 'replace a with b',
229
+ log: (m) => formatStep(m),
230
+ });
231
+
232
+ // snippet.frames[0] — initial state (no commands, highlight null)
233
+ // snippet.frames[N] — post-iter snapshot with per-tape commands { movement, read, write }
234
+ // and a graph highlight ready to feed applyHighlight()
235
+ // snippet.graph — the same Graph the player should render
236
+ // snippet.alphabets — per-tape alphabet symbols (single-tape: length 1)
237
+ ```
238
+
239
+ `Frame.commands` carries both `read` and `write` per tape so a player can step forward (write `write`, move per `movement`, flash if `write !== read`) AND backward (move opposite of `movement`, restore `read`) without diffing neighbouring frames.
240
+
241
+ ## Versioning
242
+
243
+ Engine + builder + libraries bump in **lockstep** via `lerna version`. **Visuals breaks lockstep for additive consumer-package patches** (e.g., `7.0.0-alpha.6.1` added formatter primitives + the token surface without bumping the engine — there were no engine changes to justify ghost releases). Semver-prerelease caret semantics make this safe: peer `^7.0.0-alpha.6` accepts `7.0.0-alpha.6.1+` without consumers having to widen anything. When the next engine alpha needs to ship, all 5 packages bump back to lockstep.
244
+
245
+ ## License
246
+
247
+ [GPL-3.0-or-later](../../LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turing-machine-js/visuals",
3
- "version": "7.0.0-alpha.6.1",
3
+ "version": "7.0.0-alpha.7",
4
4
  "description": "Pure highlight + graph-indexing logic for @turing-machine-js/machine — no DOM, no renderer.",
5
5
  "engines": {
6
6
  "npm": ">=7.0.0"
@@ -25,7 +25,7 @@
25
25
  "visualization"
26
26
  ],
27
27
  "peerDependencies": {
28
- "@turing-machine-js/machine": "^7.0.0-alpha.6"
28
+ "@turing-machine-js/machine": "^7.0.0-alpha.7"
29
29
  },
30
30
  "scripts": {
31
31
  "build": "tsc --build --verbose tsconfig.build.json && node ../../scripts/build-node-entries.mjs --package=@turing-machine-js/visuals",
@@ -42,5 +42,5 @@
42
42
  "default": "./dist/index.mjs"
43
43
  }
44
44
  },
45
- "gitHead": "4f5364293038aabea742235ea0cec2b135f0d6b7"
45
+ "gitHead": "130d4fd8b964da21a408af2986295e8000828f78"
46
46
  }