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

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.
@@ -1,58 +0,0 @@
1
- /**
2
- * Walk the engine graph once and build all derived lookups. Cheap;
3
- * intended to run on every Build (graph identity changes per build).
4
- */
5
- export function indexGraph(graph) {
6
- const nodeFrameMap = new Map();
7
- const frameWrappersMap = new Map();
8
- const frameLabelToId = new Map();
9
- if (!graph)
10
- return { nodeFrameMap, frameWrappersMap, frameLabelToId };
11
- for (const node of Object.values(graph.nodes)) {
12
- if (node.frameId !== null)
13
- nodeFrameMap.set(node.id, node.frameId);
14
- }
15
- // For each wrapper, append to its bare's frame entry. Multiple wrappers
16
- // can share the same bare with different overrides; we record them all
17
- // so the return-chain passes can highlight every candidate.
18
- for (const node of Object.values(graph.nodes)) {
19
- if (!node.isWrapper || node.bareStateId === null)
20
- continue;
21
- const bare = graph.nodes[node.bareStateId];
22
- if (!bare || bare.frameId === null)
23
- continue;
24
- const entry = { wrapperId: node.id, overrideId: node.overriddenHaltStateId };
25
- const arr = frameWrappersMap.get(bare.frameId);
26
- if (arr)
27
- arr.push(entry);
28
- else
29
- frameWrappersMap.set(bare.frameId, [entry]);
30
- }
31
- // Cluster label reconstruction: mirrors the engine's `toMermaid` emit
32
- // (`callable subtree of NAME` for single-bare frames, `callable scope:
33
- // A ∪ B ∪ …` for union frames; bare names sorted by id). Consumers
34
- // need this to map mermaid's rendered cluster (whose own SVG id is the
35
- // useless literal `[object Object]`) back to a frameId.
36
- const bareIds = new Set();
37
- for (const n of Object.values(graph.nodes)) {
38
- if (n.isWrapper && n.bareStateId !== null)
39
- bareIds.add(n.bareStateId);
40
- }
41
- const frameToBareNames = new Map();
42
- for (const n of Object.values(graph.nodes).sort((a, b) => a.id - b.id)) {
43
- if (n.isWrapper || n.isHaltMarker || n.frameId === null)
44
- continue;
45
- if (!bareIds.has(n.id))
46
- continue;
47
- const arr = frameToBareNames.get(n.frameId) ?? [];
48
- arr.push(n.name);
49
- frameToBareNames.set(n.frameId, arr);
50
- }
51
- for (const [frameId, names] of frameToBareNames) {
52
- const label = names.length > 1
53
- ? `callable scope: ${names.join(' ∪ ')}`
54
- : `callable subtree of ${names[0] ?? frameId}`;
55
- frameLabelToId.set(label, frameId);
56
- }
57
- return { nodeFrameMap, frameWrappersMap, frameLabelToId };
58
- }
@@ -1,76 +0,0 @@
1
- /**
2
- * Normalize an engine `GraphNode.id` to its canonical representative for
3
- * breakpoint-class lookups (machines-demo#37). Wrappers produced by
4
- * `State.withOverriddenHaltState` share `#debugRef` with their bare state
5
- * engine-side (turing-machine-js v7 `State.ts`: `state.#debugRef =
6
- * bare.#debugRef`), so they form a single breakpoint from the user's POV.
7
- * This collapses any wrapper id to its bare's id; non-wrapper ids return
8
- * self. Used so the demo can store ONE canonical id per equivalence class
9
- * in its breakpoint set, and expand to all class members for indicator
10
- * rendering — keeping the worker-side toggle count to one per class
11
- * (multiple toggles on the shared ref would double-flip).
12
- */
13
- export function bareIdOf(id, graph) {
14
- if (!graph)
15
- return id;
16
- // Halt markers (negative ids, one per frame) are visualization sentinels;
17
- // at runtime they all collapse to the haltState singleton (id 0). For
18
- // breakpoint purposes they're a single class — setting BP on any
19
- // halt-related node sets it on the global haltState.
20
- if (id < 0)
21
- return 0;
22
- const node = graph.nodes[id];
23
- if (node && node.isWrapper && node.bareStateId !== null) {
24
- return node.bareStateId;
25
- }
26
- return id;
27
- }
28
- /**
29
- * Asymmetric expansion for the highlight effect (machines-demo#37).
30
- * Wrapper → `[wrapper, bare]` (the wrapper-entry pause is visually joined
31
- * to its bare, since the user thinks of them as one call site).
32
- * Bare → `[bare]` only (when the engine is genuinely on the bare — e.g. a
33
- * loop iter — the wrapper is not the "active" state and shouldn't get the
34
- * strong highlight).
35
- * Non-wrapper / non-bare ids return `[id]`.
36
- */
37
- export function highlightExpand(id, graph) {
38
- if (!graph)
39
- return [id];
40
- const node = graph.nodes[id];
41
- if (node?.isWrapper && node.bareStateId !== null) {
42
- return [id, node.bareStateId];
43
- }
44
- return [id];
45
- }
46
- /**
47
- * All GraphNode ids in the same breakpoint equivalence class as `id`.
48
- * Symmetric — gives consumers the full list of nodes that share an engine
49
- * breakpoint, regardless of which class member is the input. Used by the
50
- * context-menu's "Shared with" info line so the user can see at a glance
51
- * which other nodes flip together.
52
- *
53
- * Halt class (canonical id 0): the halt singleton + every halt marker in
54
- * the graph. Wrapper/bare class: the bare + every wrapper pointing at it.
55
- * Singleton classes (regular states, idle sentinel proxies) return just
56
- * the input id.
57
- */
58
- export function equivalentIds(id, graph) {
59
- if (!graph)
60
- return [id];
61
- const canonical = bareIdOf(id, graph);
62
- if (canonical === 0) {
63
- const ids = [0];
64
- for (const node of Object.values(graph.nodes)) {
65
- if (node.isHaltMarker)
66
- ids.push(node.id);
67
- }
68
- return ids;
69
- }
70
- const result = new Set([canonical]);
71
- for (const node of Object.values(graph.nodes)) {
72
- if (node.isWrapper && node.bareStateId === canonical)
73
- result.add(node.id);
74
- }
75
- return [...result];
76
- }
@@ -1,36 +0,0 @@
1
- /**
2
- * Contract between the pure highlight logic (`applyHighlight`,
3
- * `applyIndicator`) and any consumer that actually renders the graph
4
- * (Svelte component, vanilla embed, server-side snapshot, etc.).
5
- *
6
- * The pure functions decide *what* should happen (which node gets a class,
7
- * which edge lights up, where to pulse); the consumer's `HighlightOps`
8
- * implementation decides *how* (DOM mutation, recording for tests, etc.).
9
- *
10
- * See `docs/graph-highlight-and-breakpoints.md` for the rules each
11
- * implementation must respect.
12
- */
13
- /**
14
- * Build a recording `HighlightOps` + `IndicatorOps` pair plus the shared
15
- * `record` array of calls in invocation order. Used by tests to assert
16
- * what the pure logic would have done without running a real DOM.
17
- *
18
- * Snapshot-friendly: the record contains only plain JSON-serializable
19
- * values (no DOM nodes, no function refs).
20
- */
21
- export function recordingOps() {
22
- const record = [];
23
- return {
24
- record,
25
- highlight: {
26
- addNodeClass(id, cls) { record.push({ op: 'addNodeClass', id, cls }); },
27
- highlightEdge(fromKey, toKey) { record.push({ op: 'highlightEdge', fromKey, toKey }); },
28
- markFrameActive(frameId) { record.push({ op: 'markFrameActive', frameId }); },
29
- pulse(id) { record.push({ op: 'pulse', id }); },
30
- scrollIntoView(id) { record.push({ op: 'scrollIntoView', id }); },
31
- },
32
- indicator: {
33
- setBreakpoint(id, on) { record.push({ op: 'setBreakpoint', id, on }); },
34
- },
35
- };
36
- }
package/dist/index.js DELETED
@@ -1,6 +0,0 @@
1
- export { recordingOps } from './highlightOps';
2
- export { bareIdOf, highlightExpand, equivalentIds } from './graphUtils';
3
- export { indexGraph } from './graphIndexes';
4
- export { applyHighlight, applyIndicator } from './applyHighlight';
5
- export { formatCommand, formatStep } from './format';
6
- export { recordSnippet } from './recordSnippet';
@@ -1,92 +0,0 @@
1
- import { haltState, } from '@turing-machine-js/machine';
2
- import { MOVEMENT_LETTER } from './format';
3
- import { bareIdOf } from './graphUtils';
4
- const DEFAULT_MAX_STEPS = 1000;
5
- function snapshotTapes(machine) {
6
- return machine.tapeBlock.tapes.map((t) => ({
7
- symbols: [...t.symbols],
8
- position: t.position,
9
- }));
10
- }
11
- function deriveCommands(m) {
12
- return m.movements.map((mv, i) => ({
13
- movement: MOVEMENT_LETTER.get(mv) ?? 'S',
14
- read: m.currentSymbols[i],
15
- // nextSymbols is already resolved (keep → current symbol, erase → blank);
16
- // when write === read the command was a keep (UI suppresses the flash).
17
- write: m.nextSymbols[i],
18
- }));
19
- }
20
- function deriveHighlight(m, graph) {
21
- return {
22
- fromId: bareIdOf(m.state.id, graph),
23
- toId: m.nextState === haltState ? 0 : m.nextState.id,
24
- strong: 'from',
25
- paused: false,
26
- };
27
- }
28
- /**
29
- * Record a full machine run into a `Snippet` — a self-contained playback
30
- * artifact suitable for embeds, articles, or landing-page panels.
31
- *
32
- * The returned snippet contains one frame per iteration plus a frame-0
33
- * initial-state snapshot. Recording stops when the machine halts or when
34
- * `maxSteps` iterations have been consumed (default 1000).
35
- *
36
- * Tape-timing note: `runStepByStep` yields BEFORE applying its command
37
- * (the command is applied after the yield resumes). The recorder uses a
38
- * one-step-delayed snapshot so each frame's `tape` reflects the
39
- * post-command state for that frame's iter.
40
- */
41
- export function recordSnippet(opts) {
42
- const { machine, initialState, graph, alphabets, name, maxSteps = DEFAULT_MAX_STEPS, log, } = opts;
43
- const frames = [
44
- { step: 0, tape: snapshotTapes(machine), highlight: null },
45
- ];
46
- // pending holds everything for the frame whose tape snapshot is not yet
47
- // available (because applyCommand hasn't fired yet). It is flushed at the
48
- // start of the NEXT iter (when the tape reflects the previous command) and
49
- // after the loop (when the final command has been applied).
50
- let pending = null;
51
- let prev = null;
52
- try {
53
- for (const m of machine.runStepByStep({ initialState, stepsLimit: maxSteps })) {
54
- // At this point applyCommand for the PREVIOUS iter has already run
55
- // (the generator called applyCommand before looping back to yield).
56
- // So the current tape state = post-command of the previous iter.
57
- if (pending !== null) {
58
- frames.push({ ...pending, tape: snapshotTapes(machine) });
59
- }
60
- const commands = deriveCommands(m);
61
- const highlight = deriveHighlight(m, graph);
62
- const logLine = log ? log(m, prev) : undefined;
63
- pending = {
64
- step: m.step,
65
- commands,
66
- highlight,
67
- ...(logLine !== undefined ? { log: logLine } : {}),
68
- };
69
- prev = m;
70
- }
71
- }
72
- catch (e) {
73
- // runStepByStep throws 'Long execution' when stepsLimit is hit.
74
- // At that point applyCommand for the last yielded iter has run, so the
75
- // tape is in the post-command state we want for the pending frame.
76
- if (!(e instanceof Error) || e.message !== 'Long execution') {
77
- throw e;
78
- }
79
- }
80
- // Flush the last pending frame. After the loop (or after the catch), the
81
- // tape reflects the post-command state of the final yielded iter.
82
- if (pending !== null) {
83
- frames.push({ ...pending, tape: snapshotTapes(machine) });
84
- }
85
- return {
86
- version: 1,
87
- ...(name !== undefined ? { name } : {}),
88
- graph,
89
- alphabets,
90
- frames,
91
- };
92
- }
package/dist/types.js DELETED
@@ -1 +0,0 @@
1
- export {};
@@ -1,331 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { readFileSync } from 'node:fs';
3
- import { resolve, dirname } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { applyHighlight, applyIndicator } from './applyHighlight';
6
- import { indexGraph } from './graphIndexes';
7
- import { recordingOps, type RecordedOp } from './highlightOps';
8
- import type { GraphHighlight } from './types';
9
- import type { Graph } from '@turing-machine-js/machine';
10
-
11
- /**
12
- * Rule tests for `applyHighlight`. Section numbers mirror
13
- * `docs/graph-highlight-and-breakpoints.md` — any rule change must
14
- * update both the doc and the matching `describe` block here.
15
- *
16
- * Fixtures come from `tests/fixtures/graphs/`, which are committed
17
- * snapshots of `State.toGraph` output for each bundled example
18
- * (regenerable via `REGEN_FIXTURES=1 npm test`). Using real engine output
19
- * means these tests also catch any drift between engine emit and
20
- * rule expectations.
21
- *
22
- * Helpful fixture ids for `turing-callable-subtree`:
23
- * bare walkToBlank = 3 (frameId 3)
24
- * writeMarker (override) = 4
25
- * wrapper walkToBlank(writeMarker) = 5 (bareStateId 3, overriddenHaltStateId 4)
26
- * halt singleton = 0
27
- * frame halt marker = -3 (id = -frameId)
28
- */
29
-
30
- const __filename = fileURLToPath(import.meta.url);
31
- const __dirname = dirname(__filename);
32
- const FIXTURE_DIR = resolve(__dirname, './fixtures/graphs');
33
-
34
- function loadGraph(name: string): Graph {
35
- return JSON.parse(readFileSync(resolve(FIXTURE_DIR, `${name}.json`), 'utf-8'));
36
- }
37
-
38
- /** Run `applyHighlight` with a recording ops impl, return the ordered ops list. */
39
- function run(highlight: GraphHighlight | null, graph: Graph, prev: number | 'idle' | null = null): {
40
- ops: RecordedOp[];
41
- next: number | 'idle' | null;
42
- } {
43
- const indexes = indexGraph(graph);
44
- const { highlight: opsImpl, record } = recordingOps();
45
- const { nextPrevStrongId } = applyHighlight(highlight, graph, indexes, prev, opsImpl);
46
- return { ops: record, next: nextPrevStrongId };
47
- }
48
-
49
- describe('applyHighlight', () => {
50
- describe('null/empty highlight', () => {
51
- it('returns no-op with nextPrev=null when highlight is null', () => {
52
- const g = loadGraph('turing-callable-subtree');
53
- const { ops, next } = run(null, g, 3);
54
- expect(ops).toEqual([]);
55
- expect(next).toBeNull();
56
- });
57
- });
58
-
59
- describe('§5 halt-target retargeting', () => {
60
- it('rewrites toId=0 to -frameId when fromId is in a frame', () => {
61
- const g = loadGraph('turing-callable-subtree');
62
- // Bare walkToBlank (id 3, frameId 3) halts → engine reports toId=0,
63
- // should retarget to -3 (frame halt marker).
64
- const { ops } = run({ fromId: 3, toId: 0, strong: 'from', paused: false }, g);
65
- const classOps = ops.filter((o) => o.op === 'addNodeClass');
66
- // halt marker -3 gets highlight-to; halt singleton 0 does not.
67
- expect(classOps).toContainEqual({ op: 'addNodeClass', id: -3, cls: 'mg-highlight-to' });
68
- expect(classOps).not.toContainEqual({ op: 'addNodeClass', id: 0, cls: 'mg-highlight-to' });
69
- });
70
-
71
- it('does NOT retarget when fromId is outside any frame', () => {
72
- const g = loadGraph('turing-callable-subtree');
73
- // writeMarker (id 4) is the wrapper's override-target; sits outside
74
- // the frame (frameId: null), so its halt→halt-singleton stays as id 0.
75
- const { ops } = run({ fromId: 4, toId: 0, strong: 'from', paused: false }, g);
76
- const classOps = ops.filter((o) => o.op === 'addNodeClass');
77
- expect(classOps).toContainEqual({ op: 'addNodeClass', id: 0, cls: 'mg-highlight-to' });
78
- });
79
- });
80
-
81
- describe('§2 + §3 wrapper/bare equivalence (asymmetric)', () => {
82
- it('wrapper-strong expands to [wrapper, bare] — both get highlight-to + strong', () => {
83
- const g = loadGraph('turing-callable-subtree');
84
- // Wrapper-entry pause: idle → wrapper(5), pauseBefore strong=to.
85
- const { ops } = run({ fromId: 'idle', toId: 5, strong: 'to', paused: true }, g);
86
- const classOps = ops.filter((o) => o.op === 'addNodeClass');
87
- expect(classOps).toContainEqual({ op: 'addNodeClass', id: 5, cls: 'mg-highlight-to' });
88
- expect(classOps).toContainEqual({ op: 'addNodeClass', id: 5, cls: 'mg-highlight-strong' });
89
- expect(classOps).toContainEqual({ op: 'addNodeClass', id: 3, cls: 'mg-highlight-to' });
90
- expect(classOps).toContainEqual({ op: 'addNodeClass', id: 3, cls: 'mg-highlight-strong' });
91
- });
92
-
93
- it('bare-strong stays bare only — does NOT light up wrapper', () => {
94
- const g = loadGraph('turing-callable-subtree');
95
- // Bare-loop pause: prev=bare(3), to=bare(3), pauseBefore strong=to.
96
- const { ops } = run({ fromId: 3, toId: 3, strong: 'to', paused: true }, g);
97
- const classOps = ops.filter((o) => o.op === 'addNodeClass');
98
- expect(classOps).toContainEqual({ op: 'addNodeClass', id: 3, cls: 'mg-highlight-to' });
99
- expect(classOps).toContainEqual({ op: 'addNodeClass', id: 3, cls: 'mg-highlight-strong' });
100
- // Wrapper 5 gets NOTHING from the to-side expansion.
101
- expect(classOps).not.toContainEqual({ op: 'addNodeClass', id: 5, cls: 'mg-highlight-to' });
102
- expect(classOps).not.toContainEqual({ op: 'addNodeClass', id: 5, cls: 'mg-highlight-strong' });
103
- });
104
- });
105
-
106
- describe('§6 source return chain (toId < 0)', () => {
107
- it('lights up return arrow + wrapper + override edge + override target', () => {
108
- const g = loadGraph('turing-callable-subtree');
109
- // Just-fired halt-bound transition: from=bare(3), to=halt-marker(-3),
110
- // strong=from (pause-after-style).
111
- const { ops } = run({ fromId: 3, toId: -3, strong: 'from', paused: true }, g);
112
- const classOps = ops.filter((o) => o.op === 'addNodeClass');
113
- const edgeOps = ops.filter((o) => o.op === 'highlightEdge');
114
- // Return arrow w_3 → wrapper(5).
115
- expect(edgeOps).toContainEqual({ op: 'highlightEdge', fromKey: 'w_3', toKey: 's5' });
116
- // Wrapper gets highlight-to.
117
- expect(classOps).toContainEqual({ op: 'addNodeClass', id: 5, cls: 'mg-highlight-to' });
118
- // Wrapper → override edge.
119
- expect(edgeOps).toContainEqual({ op: 'highlightEdge', fromKey: 's5', toKey: 's4' });
120
- // Override (writeMarker) gets highlight-to.
121
- expect(classOps).toContainEqual({ op: 'addNodeClass', id: 4, cls: 'mg-highlight-to' });
122
- });
123
- });
124
-
125
- describe('§7 destination return chain (paused at override after pop)', () => {
126
- it('lights the full path bare → halt-marker → return → wrapper → override + frame', () => {
127
- const g = loadGraph('turing-callable-subtree');
128
- // Paused-before writeMarker: prev=bare(3) (last yield was bare's halt),
129
- // current=writeMarker(4). strong=to. Bare is in frame 3; wrapper 5's
130
- // override is 4 → matches.
131
- const { ops } = run({ fromId: 3, toId: 4, strong: 'to', paused: true }, g);
132
- const classOps = ops.filter((o) => o.op === 'addNodeClass');
133
- const edgeOps = ops.filter((o) => o.op === 'highlightEdge');
134
- const frameOps = ops.filter((o) => o.op === 'markFrameActive');
135
-
136
- // Halt marker lit.
137
- expect(classOps).toContainEqual({ op: 'addNodeClass', id: -3, cls: 'mg-highlight-to' });
138
- // bare → halt-marker edge.
139
- expect(edgeOps).toContainEqual({ op: 'highlightEdge', fromKey: 's3', toKey: 'c3' });
140
- // Return arrow.
141
- expect(edgeOps).toContainEqual({ op: 'highlightEdge', fromKey: 'w_3', toKey: 's5' });
142
- // Wrapper highlight-to.
143
- expect(classOps).toContainEqual({ op: 'addNodeClass', id: 5, cls: 'mg-highlight-to' });
144
- // Wrapper → override edge.
145
- expect(edgeOps).toContainEqual({ op: 'highlightEdge', fromKey: 's5', toKey: 's4' });
146
- // Frame active (override sits outside frame, but the chain fired so
147
- // we still mark the frame to show "we came out of THIS subtree").
148
- expect(frameOps).toContainEqual({ op: 'markFrameActive', frameId: 3 });
149
- });
150
-
151
- it('does NOT fire when fromId is not in any frame', () => {
152
- const g = loadGraph('turing-callable-subtree');
153
- // From idle → writeMarker(4): no frame on the from side, no chain.
154
- const { ops } = run({ fromId: 'idle', toId: 4, strong: 'to', paused: true }, g);
155
- const edgeOps = ops.filter((o) => o.op === 'highlightEdge');
156
- expect(edgeOps).not.toContainEqual({ op: 'highlightEdge', fromKey: 's3', toKey: 'c3' });
157
- expect(edgeOps).not.toContainEqual({ op: 'highlightEdge', fromKey: 'w_3', toKey: 's5' });
158
- });
159
- });
160
-
161
- describe('§8 halt singleton (toId === 0, no retarget)', () => {
162
- it('marks the halt singleton with highlight-to + strong', () => {
163
- const g = loadGraph('turing-callable-subtree');
164
- // writeMarker (id 4, no frame) → halt singleton (0). strong=from
165
- // (pause-after applying writeMarker's command).
166
- const { ops } = run({ fromId: 4, toId: 0, strong: 'from', paused: true }, g);
167
- const classOps = ops.filter((o) => o.op === 'addNodeClass');
168
- expect(classOps).toContainEqual({ op: 'addNodeClass', id: 0, cls: 'mg-highlight-to' });
169
- // strong is 'from', so halt singleton is not strong.
170
- expect(classOps).not.toContainEqual({ op: 'addNodeClass', id: 0, cls: 'mg-highlight-strong' });
171
- });
172
- });
173
-
174
- describe('§9 frame-active', () => {
175
- it('marks the frame when canonical strong is inside it (wrapper case)', () => {
176
- const g = loadGraph('turing-callable-subtree');
177
- // Wrapper-entry pause: strong=wrapper(5). bareIdOf(5)=3. nodeFrameMap[3]=3.
178
- const { ops } = run({ fromId: 'idle', toId: 5, strong: 'to', paused: true }, g);
179
- const frameOps = ops.filter((o) => o.op === 'markFrameActive');
180
- expect(frameOps).toContainEqual({ op: 'markFrameActive', frameId: 3 });
181
- });
182
-
183
- it('marks the frame when bare is strong', () => {
184
- const g = loadGraph('turing-callable-subtree');
185
- const { ops } = run({ fromId: 3, toId: 3, strong: 'to', paused: true }, g);
186
- const frameOps = ops.filter((o) => o.op === 'markFrameActive');
187
- expect(frameOps).toContainEqual({ op: 'markFrameActive', frameId: 3 });
188
- });
189
-
190
- it('does NOT mark a frame when strong is outside any frame', () => {
191
- const g = loadGraph('turing-callable-subtree');
192
- // writeMarker (4, no frame) is strong.
193
- const { ops } = run({ fromId: 4, toId: 0, strong: 'from', paused: true }, g);
194
- const frameOps = ops.filter((o) => o.op === 'markFrameActive');
195
- expect(frameOps).toEqual([]);
196
- });
197
- });
198
-
199
- describe('§10 wrapper-entry call edge', () => {
200
- it('highlights the wrapper → bare call edge when to-side expanded to both', () => {
201
- const g = loadGraph('turing-callable-subtree');
202
- const { ops } = run({ fromId: 'idle', toId: 5, strong: 'to', paused: true }, g);
203
- const edgeOps = ops.filter((o) => o.op === 'highlightEdge');
204
- expect(edgeOps).toContainEqual({ op: 'highlightEdge', fromKey: 's5', toKey: 's3' });
205
- });
206
-
207
- it('does NOT highlight a call edge when to-side is bare only', () => {
208
- const g = loadGraph('turing-callable-subtree');
209
- const { ops } = run({ fromId: 3, toId: 3, strong: 'to', paused: true }, g);
210
- const edgeOps = ops.filter((o) => o.op === 'highlightEdge');
211
- // No s5→s3 call edge (bare doesn't expand to include wrapper).
212
- expect(edgeOps).not.toContainEqual({ op: 'highlightEdge', fromKey: 's5', toKey: 's3' });
213
- });
214
- });
215
-
216
- describe('§11 pause-revisit pulse (raw, not canonical)', () => {
217
- it('does NOT pulse on wrapper-pause → bare-pause transition', () => {
218
- const g = loadGraph('turing-callable-subtree');
219
- // First pause: wrapper-entry. prev=null → no pulse. nextPrev=5 (wrapper).
220
- const r1 = run({ fromId: 'idle', toId: 5, strong: 'to', paused: true }, g, null);
221
- expect(r1.ops.find((o) => o.op === 'pulse')).toBeUndefined();
222
- expect(r1.next).toBe(5);
223
- // Second pause: bare loop iter. prev=5 (wrapper), strong=3 (bare).
224
- // 5 !== 3 → NO pulse, even though they share #debugRef.
225
- const r2 = run({ fromId: 3, toId: 3, strong: 'to', paused: true }, g, r1.next);
226
- expect(r2.ops.find((o) => o.op === 'pulse')).toBeUndefined();
227
- expect(r2.next).toBe(3);
228
- });
229
-
230
- it('pulses on same-state revisit (bare → bare)', () => {
231
- const g = loadGraph('turing-callable-subtree');
232
- const r1 = run({ fromId: 3, toId: 3, strong: 'to', paused: true }, g, null);
233
- expect(r1.ops.find((o) => o.op === 'pulse')).toBeUndefined();
234
- const r2 = run({ fromId: 3, toId: 3, strong: 'to', paused: true }, g, r1.next);
235
- expect(r2.ops).toContainEqual({ op: 'pulse', id: 3 });
236
- });
237
-
238
- it('does NOT update prev on non-paused (idle / RUNNING_AUTO) events', () => {
239
- const g = loadGraph('turing-callable-subtree');
240
- const r = run({ fromId: 3, toId: 3, strong: 'from', paused: false }, g, 5);
241
- expect(r.next).toBe(5); // unchanged
242
- });
243
-
244
- it('resets prev to null on null highlight', () => {
245
- const g = loadGraph('turing-callable-subtree');
246
- const r = run(null, g, 3);
247
- expect(r.next).toBeNull();
248
- });
249
- });
250
-
251
- describe('scroll-into-view', () => {
252
- it('scrolls to the strong node', () => {
253
- const g = loadGraph('turing-callable-subtree');
254
- const { ops } = run({ fromId: 3, toId: 4, strong: 'to', paused: true }, g);
255
- expect(ops).toContainEqual({ op: 'scrollIntoView', id: 4 });
256
- });
257
-
258
- it('does not scroll when strong is null (no highlight)', () => {
259
- const g = loadGraph('turing-callable-subtree');
260
- const { ops } = run(null, g);
261
- expect(ops.find((o) => o.op === 'scrollIntoView')).toBeUndefined();
262
- });
263
- });
264
-
265
- describe('applyIndicator (§2 canonical breakpoint + §3 indicator class)', () => {
266
- it('marks every member of the equivalence class when canonical id is in set', () => {
267
- const g = loadGraph('turing-callable-subtree');
268
- const { indicator, record } = recordingOps();
269
- // BP set has the bare id (3). Both wrapper (5) and bare (3) should turn on.
270
- applyIndicator(new Set([3]), g, [3, 4, 5, -3, 0, 'idle'], indicator);
271
- const onIds = record
272
- .filter((r): r is Extract<RecordedOp, { op: 'setBreakpoint' }> => r.op === 'setBreakpoint' && r.on)
273
- .map((r) => r.id);
274
- expect(onIds).toContain(3);
275
- expect(onIds).toContain(5);
276
- // writeMarker (4), halt-marker (-3), halt (0), idle: never on.
277
- expect(onIds).not.toContain(4);
278
- expect(onIds).not.toContain(-3);
279
- expect(onIds).not.toContain(0);
280
- expect(onIds).not.toContain('idle');
281
- });
282
-
283
- it('clears every node when the set is empty', () => {
284
- const g = loadGraph('turing-callable-subtree');
285
- const { indicator, record } = recordingOps();
286
- applyIndicator(new Set(), g, [3, 4, 5, 'idle'], indicator);
287
- const onCount = record.filter((r) => r.op === 'setBreakpoint' && r.on).length;
288
- expect(onCount).toBe(0);
289
- // Every node still gets an explicit off call (idempotent clears).
290
- expect(record.length).toBe(4);
291
- });
292
-
293
- // machines-demo#37 — halt singleton (id 0) is a valid breakpoint
294
- // target (engine-wide haltState). The set stores canonical id 0;
295
- // both the halt singleton AND every halt marker (negative ids; one
296
- // per frame) collapse to that class via `bareIdOf(-N, g) === 0`.
297
- it('marks halt singleton AND every halt marker when 0 is in the set', () => {
298
- const g = loadGraph('turing-callable-subtree');
299
- const { indicator, record } = recordingOps();
300
- applyIndicator(new Set([0]), g, [3, 4, 5, -3, 0, 'idle'], indicator);
301
- const onIds = record
302
- .filter((r): r is Extract<RecordedOp, { op: 'setBreakpoint' }> => r.op === 'setBreakpoint' && r.on)
303
- .map((r) => r.id);
304
- expect(onIds).toContain(0); // halt singleton
305
- expect(onIds).toContain(-3); // halt marker for frame 3
306
- // Regular and wrapper / bare nodes: never on.
307
- expect(onIds).not.toContain(3);
308
- expect(onIds).not.toContain(4);
309
- expect(onIds).not.toContain(5);
310
- expect(onIds).not.toContain('idle');
311
- });
312
- });
313
-
314
- describe('regression: simple machines (no callable subtree)', () => {
315
- it('lights from + to + strong + edge for a plain RUNNING_AUTO tick', () => {
316
- const g = loadGraph('turing-replace-b');
317
- // Pick any two ids that have a transition between them.
318
- const nodes = Object.values(g.nodes).filter((n) => !n.isHalt && !n.isHaltMarker);
319
- const src = nodes.find((n) => n.transitions.length > 0)!;
320
- const transition = src.transitions[0];
321
- const dst = transition.nextStateId;
322
- const { ops } = run(
323
- { fromId: src.id, toId: dst, strong: 'from', paused: false },
324
- g,
325
- );
326
- const classOps = ops.filter((o) => o.op === 'addNodeClass');
327
- expect(classOps).toContainEqual({ op: 'addNodeClass', id: src.id, cls: 'mg-highlight-from' });
328
- expect(classOps).toContainEqual({ op: 'addNodeClass', id: src.id, cls: 'mg-highlight-strong' });
329
- });
330
- });
331
- });