From e85474e21ad3c1135d196fa94639471b653a1525 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Thu, 21 May 2026 02:03:24 +0300 Subject: [PATCH 01/34] v7 adoption: rename + paren naming + Mermaid overhaul Adopts engine v7 (alpha.1) across @post-machine-js/machine: - Rename consumer-side uses of `withOverrodeHaltState` to `withOverriddenHaltState` (closes #82). - Switch wrapper composite shape from `"A>B"` to `"A(B)"` in tests, README, naming-convention table, and the `Path` validator's rejection message (closes #83). - Adopt engine #138/#139 toMermaid emission overhaul: new edge-label vocabulary (`['x']`/`[B]`/`[*]`/`[K]`/`[E]` reads/writes, bracketed movements `[L]`/`[R]`/`[S]`), `idle -. enter .->` sentinel for the initial state, `subgraph w_N["halt frame"]` block for wrapper composites in place of single composite-named entry nodes. Pulled in because engine alpha.1 bundles all four upstream changes atomically; no separate post-side issue tracked this adoption. Engine devDep bumped to `^7.0.0-alpha.1` (root + packages/machine). Peer-dep widening and CHANGELOG entry intentionally deferred to the eventual `v7-0-0-alpha-1` bump PR per the v7 release-PR checklist. Quality gates: build clean, 267/267 tests, lint clean, typecheck clean, coverage 100/100/100/100. --- CLAUDE.md | 2 +- package-lock.json | 10 +-- package.json | 2 +- packages/machine/README.md | 82 +++++++++++---------- packages/machine/package.json | 2 +- packages/machine/src/classes/PostMachine.ts | 2 +- packages/machine/src/commands.ts | 2 +- packages/machine/src/path.ts | 4 +- packages/machine/test/examples.spec.ts | 45 ++++++----- packages/machine/test/machine-state.spec.ts | 2 +- packages/machine/test/machine.spec.ts | 2 +- packages/machine/test/naming.spec.ts | 58 ++++++++------- packages/machine/test/path.spec.ts | 6 +- packages/machine/test/state-at.spec.ts | 4 +- 14 files changed, 121 insertions(+), 102 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5b9e817..16358d5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,7 +57,7 @@ Key files: 5. **Group commands**: some producers throw if called from inside a "group" (`calledFromGroup` flag in `CommandContext`). This relates to PostMachine's grouping feature where multiple commands can be bundled into one logical instruction. Two distinct rules: `check`/`call`/`stop` reject group context unconditionally (regardless of form); the unary commands (`mark`/`erase`/`left`/`right`/`noop`) only reject the *indexed* form (`mark(20)` etc.) inside a group, because the explicit jump conflicts with the group's sequential fall-through semantics. -6. **Per-State debug lockdown (v6.1.0+)**: at the end of construction, `PostMachine` iterates `#stateToCandidatePaths.keys()` and calls `installStateLockdown(state, onUserWrite)` on every non-halt State. The installer replaces the engine's prototype `debug` accessor with an instance-level `Object.defineProperty`. Internal writes (from `#refreshStateDebug` / `#refreshHaltDebug`) run inside `withLockdownEscape` and delegate to the engine's prototype setter (which preserves the engine's `DebugConfig` wrapping + validation + shared-debugRef propagation across `withOverrodeHaltState` wrappers). User writes outside the escape go through the redirect handler: un-shared State → `setBreakpoint`/`clearBreakpoint`; shared State → throw with candidate-path list. `haltState` is locked module-globally in `src/index.ts` at module load — direct `haltState.debug = X` throws, no PostMachine context for a redirect. `state.isHalt` checks at the install site skip the engine's halt singleton (it has its own module-global lockdown). The lockdown does **not** use `Proxy` — that was tried during the v6.1.0 design phase and abandoned because engine utilities like `State.toGraph(arg, …)` read TS-downleveled private fields directly off the argument via `__classPrivateFieldGet`, which fails on a Proxy. +6. **Per-State debug lockdown (v6.1.0+)**: at the end of construction, `PostMachine` iterates `#stateToCandidatePaths.keys()` and calls `installStateLockdown(state, onUserWrite)` on every non-halt State. The installer replaces the engine's prototype `debug` accessor with an instance-level `Object.defineProperty`. Internal writes (from `#refreshStateDebug` / `#refreshHaltDebug`) run inside `withLockdownEscape` and delegate to the engine's prototype setter (which preserves the engine's `DebugConfig` wrapping + validation + shared-debugRef propagation across `withOverriddenHaltState` wrappers). User writes outside the escape go through the redirect handler: un-shared State → `setBreakpoint`/`clearBreakpoint`; shared State → throw with candidate-path list. `haltState` is locked module-globally in `src/index.ts` at module load — direct `haltState.debug = X` throws, no PostMachine context for a redirect. `state.isHalt` checks at the install site skip the engine's halt singleton (it has its own module-global lockdown). The lockdown does **not** use `Proxy` — that was tried during the v6.1.0 design phase and abandoned because engine utilities like `State.toGraph(arg, …)` read TS-downleveled private fields directly off the argument via `__classPrivateFieldGet`, which fails on a Proxy. ## Doc examples must be tested diff --git a/package-lock.json b/package-lock.json index 3cc37fc..422bd8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "packages/*" ], "dependencies": { - "@turing-machine-js/machine": "^6.4.0" + "@turing-machine-js/machine": "^7.0.0-alpha.1" }, "devDependencies": { "@tsconfig/recommended": "^1.0.13", @@ -2740,9 +2740,9 @@ } }, "node_modules/@turing-machine-js/machine": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@turing-machine-js/machine/-/machine-6.4.0.tgz", - "integrity": "sha512-kvpKl+qe9v4W7dBzBKQRM85++DgX9CAx1IG7Mvt1WCml2jSZCD+JOX2iqG5WQD05pWpP9RHUAdvXGwLkN3Q3Pg==", + "version": "7.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/@turing-machine-js/machine/-/machine-7.0.0-alpha.1.tgz", + "integrity": "sha512-BjzDYG1JuUqqLNG/ctMX/OHjuUJVR5AKtKkrgIUwNLdi/QAfxeXPcB4pvRbdVAFPpOG3JLyTZNyNCcc5wJnS3g==", "license": "GPL-3.0-or-later", "engines": { "npm": ">=7.0.0" @@ -10444,7 +10444,7 @@ "version": "6.4.0", "license": "GPL-3.0-or-later", "devDependencies": { - "@turing-machine-js/machine": "^6.4.0" + "@turing-machine-js/machine": "^7.0.0-alpha.1" }, "engines": { "npm": ">=7.0.0" diff --git a/package.json b/package.json index 250b070..c9d571a 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,6 @@ "vitest": "^4.1.5" }, "dependencies": { - "@turing-machine-js/machine": "^6.4.0" + "@turing-machine-js/machine": "^7.0.0-alpha.1" } } diff --git a/packages/machine/README.md b/packages/machine/README.md index ac21d0c..27fd14e 100644 --- a/packages/machine/README.md +++ b/packages/machine/README.md @@ -81,28 +81,31 @@ The `40: stop` instruction is elided in the graph — `stop` halts the machine, flowchart TD %% alphabets: [[" ","*"]] s0(((halt))) - s1(("10")) + s1["10"] s2["20"] s3["30"] - s1 -- "\* → ·/S" --> s2 - s1 -- "- → ·/S" --> s3 - s2 -- "* → ·/R" --> s1 - s3 -- "* → */S" --> s0 + idle([idle]) + idle -. enter .-> s1 + s1 -- "['*'] → [K]/[S]" --> s2 + s1 -- "[B] → [K]/[S]" --> s3 + s2 -- "[*] → [K]/[R]" --> s1 + s3 -- "[*] → ['*']/[S]" --> s0 ``` Reading the engine output: **Nodes.** Each `s\d+` is a Mermaid-internal node ID; the bracketed/parenthesized text is the state's display label. `s0` is always `haltState`. Node shapes: - `(((label)))` — halt state -- `(("label"))` — entry state (the one passed as `initialState`) -- `["label"]` — intermediate state +- `["label"]` — intermediate (and now also entry) state +- `([idle])` — the `idle` sentinel that marks the entry point via a dotted `idle -. enter .-> s_initial` edge +- (Wrapper composites use a `subgraph w_N["halt frame"]` block — see the [Subroutines](#subroutines) section.) -The labels are PostMachine's instruction-derived names — `"10"`, `"20"`, `"30"` map directly to the instruction indices in the program. The wrapper composite shape (`">"`) doesn't appear in this example because there are no calls or groups; see the [Subroutines](#subroutines) section for that. +The labels are PostMachine's instruction-derived names — `"10"`, `"20"`, `"30"` map directly to the instruction indices in the program. The wrapper composite shape (`"()"`) doesn't appear in this example because there are no calls or groups; see the [Subroutines](#subroutines) section for that. -**Edges.** Compact `read → write/move` syntax: -- **Read side**: `\*` is the literal mark symbol; `-` is `ifOtherSymbol` (the catch-all when there are also explicit symbol edges from the same state); `*` (without backslash) is the sole-transition match-all — used when a state has only one outgoing edge that matches everything. -- **Write side**: `·` is "keep" (no write); `*` is the literal mark symbol; ` ` (or whatever blank glyph the alphabet uses) is the literal blank. -- **Move**: `S` = stay, `L` = left, `R` = right. +**Edges.** Compact `read → write/move` syntax with bracketed tokens: +- **Read side**: `['*']` is the literal mark symbol; `[B]` is the blank symbol; `[*]` is `ifOtherSymbol` — the any-other catch-all (or sole-edge match-all on a state with only one outgoing transition). +- **Write side**: `[K]` is "keep" (no write); `[E]` is "erase" (write the blank); `['*']` (and `[' ']` for blank) is a literal symbol write. +- **Move**: `[S]` = stay, `[L]` = left, `[R]` = right. @@ -263,10 +266,10 @@ flowchart TD t2 -- "write *
(3 stops)" --> halt ``` -The `call('rightToBlank')` step at instruction 1 is built using the engine's `withOverrodeHaltState` composition primitive: the subroutine's halt is overridden to point at the next top-level instruction (instead of terminating the machine), so when the subroutine "halts" it actually returns to top-level execution at instruction 2. +The `call('rightToBlank')` step at instruction 1 is built using the engine's `withOverriddenHaltState` composition primitive: the subroutine's halt is overridden to point at the next top-level instruction (instead of terminating the machine), so when the subroutine "halts" it actually returns to top-level execution at instruction 2.
-Same graph, as the engine actually emits. The subroutine and the wrapping withOverrodeHaltState are visible: +Same graph, as the engine actually emits. The subroutine and the wrapping withOverriddenHaltState are visible: ```mermaid flowchart TD @@ -275,24 +278,29 @@ flowchart TD s5["rightToBlank::1"] s6["rightToBlank::2"] s7["1~2"] - s8(("rightToBlank>1~2")) s9["2"] - s5 -- "* → ·/R" --> s6 - s6 -- "\* → ·/S" --> s5 - s6 -- "- → ·/S" --> s0 - s7 -- "* → ·/S" --> s9 - s8 -- "* → ·/S" --> s5 + idle([idle]) + subgraph w_8["halt frame"] + s8[["rightToBlank"]] + c8(((halt))) + end + idle -. enter .-> s8 + s5 -- "[*] → [K]/[R]" --> s6 + s6 -- "['*'] → [K]/[S]" --> s5 + s6 -- "[B] → [K]/[S]" --> s0 + s7 -- "[*] → [K]/[S]" --> s9 + s8 -- "[*] → [K]/[S]" --> s5 s8 -. onHalt .-> s7 - s9 -- "* → */S" --> s0 + s9 -- "[*] → ['*']/[S]" --> s0 ``` Reading the engine output: -- The labels are PostMachine's instruction-derived names: `"rightToBlank::1"`/`"rightToBlank::2"` for the subroutine body, `"2"` for the top-level mark, `"1~2"` for the continuation, and the composite `"rightToBlank>1~2"` for the wrapper at top-level instruction 1. The `s\d+` node IDs are still auto-generated and shift between runs. -- `s8` is the top-level entry — `"rightToBlank>1~2"` is the `withOverrodeHaltState` wrapper notation: the subroutine entry hopper (named `"rightToBlank"`), with halt overridden to point at the continuation `"1~2"` (which forwards control from instruction 1 to instruction 2). -- `s5`/`s6` form the subroutine's internal cycle: `s5` is `right` (keep+R), `s6` is `check(1, 3)` (loops back on `*`, exits to halt on blank). -- The dotted `onHalt` edge `s8 -.→ s7` is the override in action: when control flow reaches the subroutine's halt, the engine pops back to `s7` (the continuation named `"1~2"`). +- The labels are PostMachine's instruction-derived names: `"rightToBlank::1"`/`"rightToBlank::2"` for the subroutine body, `"2"` for the top-level mark, `"1~2"` for the continuation, and the bare hopper name `"rightToBlank"` on the wrapped subroutine entry (the `[[…]]` double-square node `s8`). The wrapper's composite name `"rightToBlank(1~2)"` lives on the state's `.name` but isn't reproduced as a separate graph node — the `subgraph w_8["halt frame"]` block captures the same structural information in graph form. The `s\d+` node IDs are still auto-generated and shift between runs. +- `s8` (inside `w_8`) is the top-level entry — the engine's `idle -. enter .-> s8` edge marks it. The double-square `[[rightToBlank]]` shape signals "subroutine hopper" — i.e., a state wrapped by `withOverriddenHaltState`. The frame-local halt `c8` and the surrounding `subgraph w_8` together represent "halt within this subroutine's frame is the override target," replacing the v6 monolithic composite-named entry node. +- `s5`/`s6` form the subroutine's internal cycle: `s5` is `right` (keep+R), `s6` is `check(1, 3)` (loops back on `'*'`, exits to halt on blank). +- The dotted `onHalt` edge `s8 -.→ s7` is the override in action: when control flow reaches the subroutine's halt (the frame-local `c8`), the engine pops back to `s7` (the continuation named `"1~2"`). - `s7` is the continuation; it falls through (keep+S) to `s9`. -- `s9` is the `mark` instruction at top-level 2 (writes `*`, then transitions to halt — the trailing top-level `3: stop` is what produces that halt edge). +- `s9` is the `mark` instruction at top-level 2 (writes `'*'`, then transitions to halt — the trailing top-level `3: stop` is what produces that halt edge).
@@ -376,25 +384,25 @@ PostMachine names every state it constructs by instruction index, so `toMermaid` | Group at instr `O`, inner index `I` | `"O.I"` | `"foo::O.I"` | | Continuation: from `X` to `Y` | `"X~Y"` | `"foo::X~foo::Y"` | | Continuation: tail-position | `"X~halt"` | `"foo::X~halt"` | -| Call wrapper composite (engine auto-emits `>`) | `"sub>X~Y"` / `"sub>X~halt"` | `"foo::sub>foo::X~foo::Y"` | -| Group wrapper composite | `"O.1>O~Y"` / `"O.1>O~halt"` | `"foo::O.1>foo::O~foo::Y"` | +| Call wrapper composite (engine auto-wraps in parens) | `"sub(X~Y)"` / `"sub(X~halt)"` | `"foo::sub(foo::X~foo::Y)"` | +| Group wrapper composite | `"O.1(O~Y)"` / `"O.1(O~halt)"` | `"foo::O.1(foo::O~foo::Y)"` | **Separators in user-meaningful labels:** - `::` — subroutine scope (lexical nesting), like C++/Rust's scope-resolution operator. `foo::bar::1` reads as "instruction 1 inside subroutine `bar`, which is defined inside subroutine `foo`". - `.` — group inner-step ordinal. `50.1`, `50.2`, etc. are the sequential commands inside a group at instruction `50`. - `~` — continuation. `10~30` reads as "after the wrapper at instruction 10 finishes, forward to instruction 30". Tail-position uses `~halt`. -- `>` — engine-internal `withOverrodeHaltState` composition (outer state + override target). The engine auto-builds wrapper composites in this shape; user code never writes `>` directly. +- `(` / `)` — engine-internal `withOverriddenHaltState` composition (outer state wrapping the override target in parens). The engine auto-builds wrapper composites in this shape; user code never writes parens directly into state names. User-provided subroutine names are constrained to identifier characters (`/^[A-Z$_][A-Z0-9$_]*$/i`), so none of these separators can collide with user input. -**Reading a wrapper composite.** Example: `"foo>10~40"`. +**Reading a wrapper composite.** Example: `"foo(10~40)"`. -- Split at `>`: outer = `"foo"` (the subroutine hopper), override = `"10~40"` (the continuation state). +- The outer (bare) part is everything before the opening paren: `"foo"` (the subroutine hopper). The override is the parenthesized inner: `"10~40"` (the continuation state). - Split the override at `~`: caller = `"10"` (the call-site instruction), target = `"40"` (where control resumes). -So `"foo>10~40"` describes: "a wrapper around the `foo` subroutine entry, which on halt forwards from instruction 10 to instruction 40." +So `"foo(10~40)"` describes: "a wrapper around the `foo` subroutine entry, which on halt forwards from instruction 10 to instruction 40." -For a more complex example, `"outer::inner::deepest>outer::inner::1~halt"`: +For a more complex example, `"outer::inner::deepest(outer::inner::1~halt)"`: - Outer = `"outer::inner::deepest"` — a deeply-nested subroutine hopper (three levels of lexical nesting). - Override = `"outer::inner::1~halt"` — the call site at `outer::inner::1`, tail-position (forwards to halt). @@ -407,7 +415,7 @@ const m = new PostMachine({ 30: stop, foo: { 1: stop }, }); -// m.initialState.name === "foo>10~30" +// m.initialState.name === "foo(10~30)" ``` ### State sharing across structurally-identical instructions @@ -416,9 +424,9 @@ PostMachine caches state nodes by command shape, so two instructions producing s For programmatic lookup by instruction index, use `pm.candidatesFor(path)` (construction-time) or read `MachineState.candidatePaths` from an `onStep` / `onPause` callback (runtime). See [Path-based resolver](#path-based-resolver-v630) and [MachineState shape](#machinestate-shape-v620). -### Forward-compatibility with engine v7 +### Engine v7 alignment -Engine v7 (upstream `@turing-machine-js/machine`) plans to change the wrapper composite shape from `A>B` to `A(B)` (paren-based), and will likely forbid `(`, `)`, and `>` in user-provided state names. PostMachine's naming convention was designed to survive that change: none of our separators (`::`, `.`, `~`) are reserved by v7, so when the peer-dep bump lands, only the *wrapper composite emit* changes (e.g., `"foo>10~40"` becomes `"foo(10~40)"`). The names PostMachine constructs internally — and the rules in the table above — remain unchanged. +Engine v7 (upstream `@turing-machine-js/machine`) changed the wrapper composite shape from `A>B` to `A(B)` (paren-based). PostMachine's naming convention was designed to survive that change: none of our separators (`::`, `.`, `~`) collide with the new paren grammar, so only the *wrapper composite emit* shifted (e.g., the v6.x `"foo>10~40"` is now `"foo(10~40)"`). The names PostMachine constructs internally — and the rules in the table above — are unchanged. v7 also reshaped `toMermaid` output: wrapper composites now render as a `subgraph w_N["halt frame"]` block (with the bare hopper in `[[…]]` double-square brackets) instead of a single composite-named entry node. ## Introspection and equivalence @@ -483,7 +491,7 @@ console.log(b.stateCount, b.compositionEdgeCount, b.maxCompositionDepth); // 6 1 1 — subroutine: 2 more states; 1 composition edge from `call` (depth 1) ``` -Both programs do the same thing on the same input. This particular comparison is the **worst case for subroutines**: a single call site (no reuse benefit) with a small body — so the `withOverrodeHaltState` wrapper overhead per call (~2 states for the wrapper + routing intermediate) shows up as pure cost. Subroutines start saving states when reuse is real and the body amortizes the wrapper overhead — see the [extend example](#subroutines) above for symmetric variants and the [duplicate-marked-region example](../../README.md#an-example-with-subroutines) in the root README for true multi-call. +Both programs do the same thing on the same input. This particular comparison is the **worst case for subroutines**: a single call site (no reuse benefit) with a small body — so the `withOverriddenHaltState` wrapper overhead per call (~2 states for the wrapper + routing intermediate) shows up as pure cost. Subroutines start saving states when reuse is real and the body amortizes the wrapper overhead — see the [extend example](#subroutines) above for symmetric variants and the [duplicate-marked-region example](../../README.md#an-example-with-subroutines) in the root README for true multi-call. What `summarizePostMachine` actually surfaces is the **structural trade-off**, not just state count: `compositionEdgeCount` and `maxCompositionDepth` go to zero in the inline version (everything is one flat graph) and become non-zero with subroutines (`call` creates a composition edge; nesting goes deeper). Use those fields to reason about the structure of reuse independently of raw size. diff --git a/packages/machine/package.json b/packages/machine/package.json index a0f413e..634b186 100644 --- a/packages/machine/package.json +++ b/packages/machine/package.json @@ -31,7 +31,7 @@ "@turing-machine-js/machine": "^6.4.0" }, "devDependencies": { - "@turing-machine-js/machine": "^6.4.0" + "@turing-machine-js/machine": "^7.0.0-alpha.1" }, "main": "dist/index.cjs", "module": "dist/index.mjs", diff --git a/packages/machine/src/classes/PostMachine.ts b/packages/machine/src/classes/PostMachine.ts index f7e3cc8..85ef0a8 100644 --- a/packages/machine/src/classes/PostMachine.ts +++ b/packages/machine/src/classes/PostMachine.ts @@ -413,7 +413,7 @@ export class PostMachine extends TuringMachine { : `${instructionPrefix}${list[ix + 1]}`; const continuationName = `${callerName}~${targetName}`; - builtStates.set(String(instructionIndex), groupState.withOverrodeHaltState(new State({ + builtStates.set(String(instructionIndex), groupState.withOverriddenHaltState(new State({ [ifOtherSymbol]: { nextState, }, diff --git a/packages/machine/src/commands.ts b/packages/machine/src/commands.ts index 2b400a9..dbf870c 100644 --- a/packages/machine/src/commands.ts +++ b/packages/machine/src/commands.ts @@ -104,7 +104,7 @@ function callCommandStateProducer(this: { subroutineName: string; nextInstructio : `${instructionPrefix}${String(boundNextInstructionIndex)}`; const continuationName = `${callerName}~${targetName}`; - const state = subroutineInitialStates[subroutineName].withOverrodeHaltState(new State({ + const state = subroutineInitialStates[subroutineName].withOverriddenHaltState(new State({ [ifOtherSymbol]: { nextState, }, diff --git a/packages/machine/src/path.ts b/packages/machine/src/path.ts index 37453df..1fd9d9c 100644 --- a/packages/machine/src/path.ts +++ b/packages/machine/src/path.ts @@ -17,8 +17,8 @@ export function parsePath(s: string): Path { throw new Error(`invalid path: empty string`); } - if (s.includes('>')) { - throw new Error(`invalid path '${s}': contains '>', which is the engine's wrapper composite separator (not an instruction path)`); + if (s.includes('(') || s.includes(')')) { + throw new Error(`invalid path '${s}': contains '(' or ')', which marks the engine's wrapper composite (not an instruction path)`); } if (s.includes('~')) { diff --git a/packages/machine/test/examples.spec.ts b/packages/machine/test/examples.spec.ts index ef9346f..50a0a83 100644 --- a/packages/machine/test/examples.spec.ts +++ b/packages/machine/test/examples.spec.ts @@ -146,24 +146,25 @@ describe('packages/machine/README.md', () => { expect(mermaid).toContain('flowchart TD'); expect(mermaid).toContain('%% alphabets: [[" ","*"]]'); - // Halt + the entry-state with composite name. The wrapper at top-level instruction 1 - // is `call('rightToBlank')`, so the composite reads as `>` — - // here `"rightToBlank>1~2"` (subroutine hopper named after the subroutine, continuation - // forwards from instr 1 to instr 2). + // Halt + the entry — under engine v7 the wrapper composite is emitted as a + // halt-frame subgraph containing the bare hopper (double-square brackets `[[...]]`) + // and a frame-local halt node, not as a single composite-named round node. + // The composite `"rightToBlank(1~2)"` only lives on the wrapping State's `.name`. expect(mermaid).toContain('(((halt)))'); - expect(mermaid).toContain('(("rightToBlank>1~2"))'); + expect(mermaid).toMatch(/subgraph w_\d+\["halt frame"\]/); + expect(mermaid).toContain('[["rightToBlank"]]'); // The dotted onHalt edge — the override path back from the subroutine. expect(mermaid).toMatch(/s\d+ -\. onHalt \.-> s\d+/); // The subroutine's internal cycle: a right-move state and a check state - // that loops back on '*' and exits on the blank. - expect(mermaid).toMatch(/s\d+ -- "\* → ·\/R" --> s\d+/); // right (keep + R) - expect(mermaid).toMatch(/s\d+ -- "\\\* → ·\/S" --> s\d+/); // check on '*' - expect(mermaid).toMatch(/s\d+ -- "- → ·\/S" --> s\d+/); // check on blank (ifOtherSymbol) + // that loops back on '*' and exits on the blank. Engine v7 label vocabulary. + expect(mermaid).toMatch(/s\d+ -- "\[\*\] → \[K\]\/\[R\]" --> s\d+/); // right (keep + R) + expect(mermaid).toMatch(/s\d+ -- "\['\*'\] → \[K\]\/\[S\]" --> s\d+/); // check on '*' + expect(mermaid).toMatch(/s\d+ -- "\[B\] → \[K\]\/\[S\]" --> s\d+/); // check on blank // The mark instruction's edge: write '*', stay, transition to halt. - expect(mermaid).toMatch(/s\d+ -- "\* → \*\/S" --> s\d+/); + expect(mermaid).toMatch(/s\d+ -- "\[\*\] → \['\*'\]\/\[S\]" --> s\d+/); }); test('** → marks first blank to make *** (single subroutine, single call)', async () => { @@ -249,7 +250,7 @@ describe('packages/machine/README.md', () => { }); describe('Naming convention', () => { - test('Quick example: machine.initialState.name === "foo>10~30"', () => { + test('Quick example: machine.initialState.name === "foo(10~30)"', () => { const m = new PostMachine({ 10: call('foo', 30), 20: stop, @@ -257,8 +258,8 @@ describe('packages/machine/README.md', () => { foo: { 1: stop }, }); - // m.initialState.name === "foo>10~30" - expect(m.initialState.name).toBe('foo>10~30'); + // m.initialState.name === "foo(10~30)" + expect(m.initialState.name).toBe('foo(10~30)'); }); }); @@ -327,18 +328,22 @@ describe('packages/machine/README.md', () => { // Halt node (always literal "halt"). expect(mermaid).toContain('(((halt)))'); - // Initial state — double-paren entry shape with instruction-derived name. - expect(mermaid).toContain('(("10"))'); + // Initial state — square-bracket node shape; under engine v7 the entry is + // marked by a separate idle sentinel + dotted enter edge, not a double-paren shape. + expect(mermaid).toContain('["10"]'); + expect(mermaid).toContain('idle([idle])'); + expect(mermaid).toMatch(/idle -\. enter \.-> s\d+/); // Two intermediate states — square-bracket node shape with instruction-derived names. expect(mermaid).toContain('["20"]'); expect(mermaid).toContain('["30"]'); // Each of the 4 transitions described in the README's reading guide. - // Edge labels are exact as emitted; node IDs (s\d+) are not pinned. - expect(mermaid).toMatch(/s\d+ -- "\\\* → ·\/S" --> s\d+/); - expect(mermaid).toMatch(/s\d+ -- "- → ·\/S" --> s\d+/); - expect(mermaid).toMatch(/s\d+ -- "\* → ·\/R" --> s\d+/); - expect(mermaid).toMatch(/s\d+ -- "\* → \*\/S" --> s\d+/); + // Engine v7 edge-label vocabulary: ['x'] = literal symbol, [B] = blank, [*] = any-other, + // [K] = keep, [E] = erase; movements [L]/[R]/[S]. + expect(mermaid).toMatch(/s\d+ -- "\['\*'\] → \[K\]\/\[S\]" --> s\d+/); + expect(mermaid).toMatch(/s\d+ -- "\[B\] → \[K\]\/\[S\]" --> s\d+/); + expect(mermaid).toMatch(/s\d+ -- "\[\*\] → \[K\]\/\[R\]" --> s\d+/); + expect(mermaid).toMatch(/s\d+ -- "\[\*\] → \['\*'\]\/\[S\]" --> s\d+/); }); }); diff --git a/packages/machine/test/machine-state.spec.ts b/packages/machine/test/machine-state.spec.ts index aaccafc..eb0c55d 100644 --- a/packages/machine/test/machine-state.spec.ts +++ b/packages/machine/test/machine-state.spec.ts @@ -89,7 +89,7 @@ describe('PostMachine — wrapped MachineState', () => { const seen: MachineState[] = []; await m.run({ onStep: (s) => { seen.push(s); } }); // The second inner (mark) fires at 50.2 — the first inner (right) is wrapped - // by withOverrodeHaltState and therefore resolves to the outer group path {50} + // by withOverriddenHaltState and therefore resolves to the outer group path {50} // rather than {50.1}. The second inner state is unambiguously tagged 50.2. const groupInner = seen.find(s => s.arrivalPath.instructionIndex === 50 && s.arrivalPath.groupInstructionIndex === 2 diff --git a/packages/machine/test/machine.spec.ts b/packages/machine/test/machine.spec.ts index c3b2198..d6ce82b 100644 --- a/packages/machine/test/machine.spec.ts +++ b/packages/machine/test/machine.spec.ts @@ -647,7 +647,7 @@ describe('run tests', () => { onStep: (...args) => machine2OnStepMock(...args), })).rejects.toThrow('Long execution'); - const regExp = />/; + const regExp = /\(/; const machine1StateIdList = machine1OnStepMock.mock.calls .map((args) => args[0].state.name) .filter((name) => regExp.test(name)) diff --git a/packages/machine/test/naming.spec.ts b/packages/machine/test/naming.spec.ts index bc8306f..ee62019 100644 --- a/packages/machine/test/naming.spec.ts +++ b/packages/machine/test/naming.spec.ts @@ -34,14 +34,14 @@ describe('PostMachine — top-level atomic-command names', () => { }); describe('PostMachine — top-level call wrapper names', () => { - test('call wrapper composite reads as ">~"', () => { + test('call wrapper composite reads as "(~)"', () => { const machine = new PostMachine({ 10: call('foo', 30), 20: stop, 30: stop, foo: { 1: stop }, }); - expect(machine.initialState.name).toBe('foo>10~30'); + expect(machine.initialState.name).toBe('foo(10~30)'); }); test('tail-position call wrapper composite uses "halt"', () => { @@ -49,7 +49,7 @@ describe('PostMachine — top-level call wrapper names', () => { 10: call('foo'), foo: { 1: stop }, }); - expect(machine.initialState.name).toBe('foo>10~halt'); + expect(machine.initialState.name).toBe('foo(10~halt)'); }); test('call falling through to the next sequential instruction', () => { @@ -58,7 +58,7 @@ describe('PostMachine — top-level call wrapper names', () => { 20: stop, foo: { 1: stop }, }); - expect(machine.initialState.name).toBe('foo>10~20'); + expect(machine.initialState.name).toBe('foo(10~20)'); }); }); @@ -78,13 +78,14 @@ describe('PostMachine — group states and wrapper composite', () => { 60: stop, }); // The initialState is the group wrapper at instr 50. - // Composite: "50.1>50~60" (first inner of group ">" continuation from 50 to 60). - expect(machine.initialState.name).toBe('50.1>50~60'); + // Composite: "50.1(50~60)" (first inner of group wrapping continuation from 50 to 60). + expect(machine.initialState.name).toBe('50.1(50~60)'); const names = collectNames(machine); - // '50.1' is subsumed into the composite wrapper name '50.1>50~60' and - // does not appear as a separate graph node. - expect(names.has('50.1>50~60')).toBe(true); + // Under engine v7's flatter emit, the wrapper appears as the bare '50.1' node + // with an onHalt edge to the continuation '50~60'; the composite '50.1(50~60)' + // lives only on state.name, not as a graph node. + expect(names.has('50.1')).toBe(true); expect(names.has('50.2')).toBe(true); expect(names.has('50.3')).toBe(true); // 'stop' maps to haltState singleton — no separate named node for instruction 60. @@ -95,7 +96,7 @@ describe('PostMachine — group states and wrapper composite', () => { const machine = new PostMachine({ 50: [right, mark], }); - expect(machine.initialState.name).toBe('50.1>50~halt'); + expect(machine.initialState.name).toBe('50.1(50~halt)'); }); test('group inside a subroutine uses fully-qualified prefix', () => { @@ -107,8 +108,10 @@ describe('PostMachine — group states and wrapper composite', () => { }, }); const names = collectNames(machine); - // 'foo::1.1' is subsumed into the composite wrapper name — not a separate node. - expect(names.has('foo::1.1>foo::1~foo::2')).toBe(true); // group wrapper composite + // Under engine v7, the group wrapper appears as bare 'foo::1.1' with a separate + // continuation node 'foo::1~foo::2'; the composite name 'foo::1.1(foo::1~foo::2)' + // is only on state.name. + expect(names.has('foo::1.1')).toBe(true); expect(names.has('foo::1.2')).toBe(true); expect(names.has('foo::2')).toBe(true); expect(names.has('foo::1~foo::2')).toBe(true); // continuation @@ -127,7 +130,7 @@ describe('PostMachine — subroutine body and hopper names', () => { }, }); // Wrapper composite at top — hopper now named "foo". - expect(machine.initialState.name).toBe('foo>10~halt'); + expect(machine.initialState.name).toBe('foo(10~halt)'); // Subroutine body instructions are fully-qualified. const names = collectNames(machine); @@ -149,11 +152,12 @@ describe('PostMachine — subroutine body and hopper names', () => { }, }); // Top wrapper composite uses the top-level hopper name "outer". - expect(machine.initialState.name).toBe('outer>10~halt'); + expect(machine.initialState.name).toBe('outer(10~halt)'); const names = collectNames(machine); - // The nested call wrapper uses the fully-qualified hopper name "outer::inner". - expect(names.has('outer::inner>outer::1~outer::2')).toBe(true); + // The nested call wrapper appears as bare 'outer::inner' (fq hopper) under v7's + // flatter emit; composite 'outer::inner(outer::1~outer::2)' lives only on state.name. + expect(names.has('outer::inner')).toBe(true); // outer::2 is a plain mark instruction — it has its own named state. expect(names.has('outer::2')).toBe(true); // Body states of inner subroutine are fully-qualified. @@ -172,10 +176,9 @@ describe('PostMachine — combined naming scenarios', () => { }, }); const names = collectNames(machine); - // The wrapper inside foo for call('bar') has composite "foo::bar>foo::1~foo::2". - // This proves: foo::bar is the hopper name (fq-prefixed nested hopper), - // and the continuation foo::1~foo::2 uses foo's prefix for both caller and target. - expect(names.has('foo::bar>foo::1~foo::2')).toBe(true); + // Under v7, the wrapper inside foo for call('bar') appears as bare 'foo::bar' + // (fq-prefixed nested hopper) with a separate continuation 'foo::1~foo::2'. + expect(names.has('foo::bar')).toBe(true); expect(names.has('foo::1~foo::2')).toBe(true); expect(names.has('foo::2')).toBe(true); expect(names.has('foo::bar::1')).toBe(true); @@ -190,8 +193,9 @@ describe('PostMachine — combined naming scenarios', () => { }, }); const names = collectNames(machine); - // Group wrapper at foo::1: composite "foo::1.1>foo::1~foo::2". - expect(names.has('foo::1.1>foo::1~foo::2')).toBe(true); + // Group wrapper at foo::1 appears as bare 'foo::1.1' under v7; composite + // 'foo::1.1(foo::1~foo::2)' lives only on state.name. + expect(names.has('foo::1.1')).toBe(true); expect(names.has('foo::1.2')).toBe(true); // non-first inner — standalone expect(names.has('foo::1~foo::2')).toBe(true); // continuation expect(names.has('foo::2')).toBe(true); // next instruction in subroutine @@ -206,8 +210,9 @@ describe('PostMachine — combined naming scenarios', () => { }, }); const names = collectNames(machine); - // Wrapper at foo::1: "foo::bar>foo::1~halt" (tail position inside foo). - expect(names.has('foo::bar>foo::1~halt')).toBe(true); + // Wrapper at foo::1 appears as bare 'foo::bar' under v7; composite + // 'foo::bar(foo::1~halt)' (tail position inside foo) lives only on state.name. + expect(names.has('foo::bar')).toBe(true); expect(names.has('foo::1~halt')).toBe(true); expect(names.has('foo::bar::1')).toBe(true); }); @@ -227,7 +232,8 @@ describe('PostMachine — combined naming scenarios', () => { const names = collectNames(machine); // Each scope hops accumulate in the prefix. expect(names.has('outer::inner::deepest::1')).toBe(true); - // Body inner at outer::inner::1 calls deepest; the call composite there: - expect(names.has('outer::inner::deepest>outer::inner::1~halt')).toBe(true); + // Body inner at outer::inner::1 calls deepest. Under v7, the wrapper appears as + // bare 'outer::inner::deepest' (the fq hopper) with separate continuation. + expect(names.has('outer::inner::deepest')).toBe(true); }); }); diff --git a/packages/machine/test/path.spec.ts b/packages/machine/test/path.spec.ts index 1e78ff7..c4d2987 100644 --- a/packages/machine/test/path.spec.ts +++ b/packages/machine/test/path.spec.ts @@ -36,12 +36,12 @@ describe('parsePath — happy paths', () => { }); describe('parsePath — rejections', () => { - test('wrapper composite (contains >)', () => { - expect(() => parsePath('foo>10~30')).toThrow(/wrapper composite|not an instruction path/i); + test('wrapper composite (contains parens)', () => { + expect(() => parsePath('foo(10~30)')).toThrow(/wrapper composite|not an instruction path/i); }); test('group wrapper composite', () => { - expect(() => parsePath('50.1>50~60')).toThrow(/wrapper composite|not an instruction path/i); + expect(() => parsePath('50.1(50~60)')).toThrow(/wrapper composite|not an instruction path/i); }); test('continuation state (contains ~)', () => { diff --git a/packages/machine/test/state-at.spec.ts b/packages/machine/test/state-at.spec.ts index d5dca26..9e97393 100644 --- a/packages/machine/test/state-at.spec.ts +++ b/packages/machine/test/state-at.spec.ts @@ -104,9 +104,9 @@ describe('pm.stateAt — rejections', () => { expect(() => pm.stateAt('halt')).toThrow(/halt|not an instruction path/i); }); - test('wrapper composite (contains >) is rejected', () => { + test('wrapper composite (contains parens) is rejected', () => { const pm = new PostMachine({ 10: mark, 20: stop }); - expect(() => pm.stateAt('foo>10~20')).toThrow(); + expect(() => pm.stateAt('foo(10~20)')).toThrow(); }); test('continuation state (contains ~) is rejected', () => { From 8ebea080ceb9dd46e4d4237b278047645c366d60 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Thu, 21 May 2026 02:42:47 +0300 Subject: [PATCH 02/34] docs: drop stylized mermaid duplicates, link turing#173/#174 in narrative MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove the hand-drawn stylized Mermaid blocks from Quick Start and Subroutines sections; the engine-emit blocks (previously hidden in `
`) are now the single diagrams per section, mirroring the upstream engine README's convention. - Add upstream tracking references next to the diagram quirks readers will notice: `c_N` orphan + `onHalt` anchor on `s_N` (turing-machine-js#173), and the subroutine-body-visually-outside- the-frame question (turing-machine-js#174). The runtime divergence on `s6 → s0` is now called out explicitly in the reading guide rather than gestured at. - Adjust intro prose for both sections to reflect the single-diagram shape. --- packages/machine/README.md | 57 +++++++------------------------------- 1 file changed, 10 insertions(+), 47 deletions(-) diff --git a/packages/machine/README.md b/packages/machine/README.md index 27fd14e..f134f8c 100644 --- a/packages/machine/README.md +++ b/packages/machine/README.md @@ -57,25 +57,7 @@ console.log(machine.tape.symbols.join('').trim()); // *** Each instruction is a command. Used bare (`mark`, `right`, `erase`), it falls through to the next numbered instruction; called with an index (`mark(20)`), it jumps to instruction `20`. `check(ix1, ix0)` branches — `ix1` if the current cell is marked, else `ix0`. `stop` halts. -The state graph for the example above: - -```mermaid -flowchart TD - halt(((halt))) - s10(("**10:** check(20, 30)")) - s20["**20:** right(10)"] - s30["**30:** mark"] - - s10 -- "marked (*)" --> s20 - s10 -- "blank" --> s30 - s20 -- "→ R" --> s10 - s30 -- "write *
(40 stops)" --> halt -``` - -The `40: stop` instruction is elided in the graph — `stop` halts the machine, so the transition from `30: mark` flows straight to halt rather than through an intermediate state. - -
-Same graph, as the engine actually emits via toMermaid(State.toGraph(machine.initialState, machine.tapeBlock)): +The state graph for the example above (`toMermaid(State.toGraph(machine.initialState, machine.tapeBlock))`): ```mermaid flowchart TD @@ -92,7 +74,7 @@ flowchart TD s3 -- "[*] → ['*']/[S]" --> s0 ``` -Reading the engine output: +Reading the diagram: **Nodes.** Each `s\d+` is a Mermaid-internal node ID; the bracketed/parenthesized text is the state's display label. `s0` is always `haltState`. Node shapes: - `(((label)))` — halt state @@ -100,15 +82,13 @@ Reading the engine output: - `([idle])` — the `idle` sentinel that marks the entry point via a dotted `idle -. enter .-> s_initial` edge - (Wrapper composites use a `subgraph w_N["halt frame"]` block — see the [Subroutines](#subroutines) section.) -The labels are PostMachine's instruction-derived names — `"10"`, `"20"`, `"30"` map directly to the instruction indices in the program. The wrapper composite shape (`"()"`) doesn't appear in this example because there are no calls or groups; see the [Subroutines](#subroutines) section for that. +The labels are PostMachine's instruction-derived names — `"10"`, `"20"`, `"30"` map directly to the instruction indices in the program. The wrapper composite shape (`"()"`) doesn't appear in this example because there are no calls or groups; see the [Subroutines](#subroutines) section for that. The `40: stop` instruction is elided — `stop` halts the machine, so the transition from `30: mark` flows straight to halt rather than through an intermediate state. **Edges.** Compact `read → write/move` syntax with bracketed tokens: - **Read side**: `['*']` is the literal mark symbol; `[B]` is the blank symbol; `[*]` is `ifOtherSymbol` — the any-other catch-all (or sole-edge match-all on a state with only one outgoing transition). - **Write side**: `[K]` is "keep" (no write); `[E]` is "erase" (write the blank); `['*']` (and `[' ']` for blank) is a literal symbol write. - **Move**: `[S]` = stay, `[L]` = left, `[R]` = right. -
- ## Classes ### PostMachine @@ -252,24 +232,7 @@ await machine.run(); console.log(machine.tape.symbols.join('').trim()); // *** ``` -The state graph (top-level flow with the subroutine as a black box): - -```mermaid -flowchart TD - halt(((halt))) - t1(("**1:** call('rightToBlank')")) - t2["**2:** mark"] - sub[["rightToBlank
(walks right until blank)"]] - - t1 -- "enters" --> sub - sub -. "halts → return" .-> t2 - t2 -- "write *
(3 stops)" --> halt -``` - -The `call('rightToBlank')` step at instruction 1 is built using the engine's `withOverriddenHaltState` composition primitive: the subroutine's halt is overridden to point at the next top-level instruction (instead of terminating the machine), so when the subroutine "halts" it actually returns to top-level execution at instruction 2. - -
-Same graph, as the engine actually emits. The subroutine and the wrapping withOverriddenHaltState are visible: +The state graph as the engine emits it — the subroutine and the wrapping `withOverriddenHaltState` composition are visible: ```mermaid flowchart TD @@ -294,16 +257,16 @@ flowchart TD s9 -- "[*] → ['*']/[S]" --> s0 ``` -Reading the engine output: +The `call('rightToBlank')` step at instruction 1 is built using the engine's `withOverriddenHaltState` composition primitive: the subroutine's halt is overridden to point at the next top-level instruction (instead of terminating the machine), so when the subroutine "halts" it actually returns to top-level execution at instruction 2. + +Reading the diagram: - The labels are PostMachine's instruction-derived names: `"rightToBlank::1"`/`"rightToBlank::2"` for the subroutine body, `"2"` for the top-level mark, `"1~2"` for the continuation, and the bare hopper name `"rightToBlank"` on the wrapped subroutine entry (the `[[…]]` double-square node `s8`). The wrapper's composite name `"rightToBlank(1~2)"` lives on the state's `.name` but isn't reproduced as a separate graph node — the `subgraph w_8["halt frame"]` block captures the same structural information in graph form. The `s\d+` node IDs are still auto-generated and shift between runs. -- `s8` (inside `w_8`) is the top-level entry — the engine's `idle -. enter .-> s8` edge marks it. The double-square `[[rightToBlank]]` shape signals "subroutine hopper" — i.e., a state wrapped by `withOverriddenHaltState`. The frame-local halt `c8` and the surrounding `subgraph w_8` together represent "halt within this subroutine's frame is the override target," replacing the v6 monolithic composite-named entry node. -- `s5`/`s6` form the subroutine's internal cycle: `s5` is `right` (keep+R), `s6` is `check(1, 3)` (loops back on `'*'`, exits to halt on blank). -- The dotted `onHalt` edge `s8 -.→ s7` is the override in action: when control flow reaches the subroutine's halt (the frame-local `c8`), the engine pops back to `s7` (the continuation named `"1~2"`). +- `s8` (inside `w_8`) is the top-level entry — the engine's `idle -. enter .-> s8` edge marks it. The double-square `[[rightToBlank]]` shape signals "subroutine hopper" — i.e., a state wrapped by `withOverriddenHaltState`. The frame-local halt `c8` and the surrounding `subgraph w_8` together represent "halt within this subroutine's frame is the override target," replacing the v6 monolithic composite-named entry node. (Note: in this diagram `c8` has no incoming edges and the `onHalt` redirect is attached to `s8` rather than `c8` — tracked upstream as [turing-machine-js#173](https://github.com/mellonis/turing-machine-js/issues/173).) +- `s5`/`s6` form the subroutine's internal cycle: `s5` is `right` (keep+R), `s6` is `check(1, 3)` (loops back on `'*'`, exits to halt on blank). Note: `s6 → s0` is the literal `State` transition but **not** the runtime path — the wrapper's halt-stack intercepts and redirects to `s7`. The broader question of whether body states like `s5`/`s6` should visualize as inside the halt frame is tracked at [turing-machine-js#174](https://github.com/mellonis/turing-machine-js/issues/174). +- The dotted `onHalt` edge `s8 -.→ s7` is the override in action: when the wrapper's halt-stack intercepts a halt-bound transition from inside the subroutine, the engine pops back to `s7` (the continuation named `"1~2"`). - `s7` is the continuation; it falls through (keep+S) to `s9`. - `s9` is the `mark` instruction at top-level 2 (writes `'*'`, then transitions to halt — the trailing top-level `3: stop` is what produces that halt edge). -
- That's just syntax — for one call site, inlining is equivalent. Subroutines earn their keep when the same logic appears at multiple sites or when symmetric variants share a shape. Example: extend a marked region by one cell on each side, using mirrored `walkRightToBlank` / `walkLeftToBlank` helpers. ```javascript From 11308d9f1a03b50b72b20b84f143926b1236ba6a Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Thu, 21 May 2026 15:16:22 +0300 Subject: [PATCH 03/34] =?UTF-8?q?chore(release):=207.0.0-alpha.2=20?= =?UTF-8?q?=E2=80=94=20engine=20alpha.2=20adoption?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Combines the pending v7 adoption work (rename + paren naming + Mermaid emit) with engine alpha.2's emit-shape changes (#174 callable-subtree) into a single alpha.2 release. post-machine-js skips its own v7-alpha.1 since engine alpha.1 was superseded before any post-side adoption shipped. Engine bump: - devDep `@turing-machine-js/machine` ^7.0.0-alpha.1 → ^7.0.0-alpha.2 - peerDep `@turing-machine-js/machine` ^6.4.0 → ^7.0.0-alpha.2 Code adoption (was prepared against alpha.1, now reconciled to alpha.2): - consumer-side identifier rename `withOverrodeHaltState` → `withOverriddenHaltState` (#82, engine #149) - wrapper composite shape parser/formatter `>` → `(…)` (#83, engine #148) Engine alpha.2 emit deltas (no separate post-side issue): - README's `toMermaid` block regenerated for the callable-subtree shape: wrapper is now a `[[bare(continuation)]]` call site OUTSIDE the subgraph; the bare hopper + body live INSIDE `subgraph w_N["callable subtree of NAME"]`. Bold `==> "call"` from wrapper to bare; dotted `-. "return" .->` back to wrapper. The retired `-. onHalt .->` is gone. - Body's halt-bound transition now retargets to the frame's halt marker `cN` instead of the real `s0`. - `summarizePostMachine().stateCount` is +1 per call site (wrapper and bare are separate nodes now). The README's "Structural summary" example shifts from `6 1 1` to `7 1 1`; matching test assertion updated. Version bump via lerna: - @post-machine-js/machine: 6.4.0 → 7.0.0-alpha.2 CHANGELOG entry under `[7.0.0-alpha.2] - 2026-05-21` covers #82, #83, engine #174 adoption, peer-dep widening, and migration notes for consumers. Verification: - npm test — 267/267 pass - npm run lint — clean - npm run typecheck — clean - npm run test:coverage — 100/100/100/100 (above hard floor) - npm publish --dry-run --tag next — clean --- lerna.json | 2 +- package-lock.json | 14 +++---- package.json | 2 +- packages/machine/CHANGELOG.md | 38 ++++++++++++++++++ packages/machine/README.md | 55 ++++++++++++++------------ packages/machine/package.json | 6 +-- packages/machine/test/examples.spec.ts | 33 ++++++++++------ 7 files changed, 101 insertions(+), 49 deletions(-) diff --git a/lerna.json b/lerna.json index e573776..a18995b 100644 --- a/lerna.json +++ b/lerna.json @@ -3,6 +3,6 @@ "packages": [ "packages/*" ], - "version": "6.4.0", + "version": "7.0.0-alpha.2", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 422bd8b..da894ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "packages/*" ], "dependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.1" + "@turing-machine-js/machine": "^7.0.0-alpha.2" }, "devDependencies": { "@tsconfig/recommended": "^1.0.13", @@ -2740,9 +2740,9 @@ } }, "node_modules/@turing-machine-js/machine": { - "version": "7.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/@turing-machine-js/machine/-/machine-7.0.0-alpha.1.tgz", - "integrity": "sha512-BjzDYG1JuUqqLNG/ctMX/OHjuUJVR5AKtKkrgIUwNLdi/QAfxeXPcB4pvRbdVAFPpOG3JLyTZNyNCcc5wJnS3g==", + "version": "7.0.0-alpha.2", + "resolved": "https://registry.npmjs.org/@turing-machine-js/machine/-/machine-7.0.0-alpha.2.tgz", + "integrity": "sha512-mGtG/yXznBnBjiVP3TbW0TaEGsCKoYstJRopK+WL38pQabiEyLyOlvYZlVICfvKkeA6MLjkuVyE644QeSNf+YQ==", "license": "GPL-3.0-or-later", "engines": { "npm": ">=7.0.0" @@ -10441,16 +10441,16 @@ }, "packages/machine": { "name": "@post-machine-js/machine", - "version": "6.4.0", + "version": "7.0.0-alpha.2", "license": "GPL-3.0-or-later", "devDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.1" + "@turing-machine-js/machine": "^7.0.0-alpha.2" }, "engines": { "npm": ">=7.0.0" }, "peerDependencies": { - "@turing-machine-js/machine": "^6.4.0" + "@turing-machine-js/machine": "^7.0.0-alpha.2" } } } diff --git a/package.json b/package.json index c9d571a..6fcd466 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,6 @@ "vitest": "^4.1.5" }, "dependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.1" + "@turing-machine-js/machine": "^7.0.0-alpha.2" } } diff --git a/packages/machine/CHANGELOG.md b/packages/machine/CHANGELOG.md index 048761d..6bcf519 100644 --- a/packages/machine/CHANGELOG.md +++ b/packages/machine/CHANGELOG.md @@ -4,6 +4,44 @@ All notable changes to this project will be documented in this file. 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). +## [7.0.0-alpha.2] - 2026-05-21 + +First post-machine-js v7 pre-release — adopts engine `@turing-machine-js/machine@7.0.0-alpha.2`. **post-machine-js skips its own v7 alpha.1**: engine alpha.1 was superseded by alpha.2 (which refined the `toMermaid` emit before any post-side adoption shipped), so post-machine-js's first v7 prerelease goes straight to alpha.2 matching the engine's current alpha. Published to npm under the `next` dist-tag: `npm install @post-machine-js/machine@next`. + +**Pre-release — the API surface may still shift before stable v7.0.0.** Pin to a specific alpha for reproducibility: `@post-machine-js/machine@7.0.0-alpha.2`. + +### Changed + +- **Engine `withOverrodeHaltState` → `withOverriddenHaltState` adoption** ([#82](https://github.com/mellonis/post-machine-js/issues/82) — engine [#149](https://github.com/mellonis/turing-machine-js/issues/149)). Consumer-side references in `src/commands.ts`, `src/classes/PostMachine.ts`, README narrative, and root CLAUDE.md all switched to the renamed identifier. Hard cutover — no deprecated alias. + +- **Wrapper composite name format `>` → `(…)` adoption** ([#83](https://github.com/mellonis/post-machine-js/issues/83) — engine [#148](https://github.com/mellonis/turing-machine-js/issues/148)). Engine v7 changed wrapper composite shape from `A>B` to `A(B)`. PostMachine's `Path` separators (`::`, `.`, `~`) survive unchanged. `parsePath` now rejects `(`/`)` in user-provided state names (previously rejected `>`). Test assertions on `initialState.name` and graph node-name checks updated; README naming-convention table + "Reading a wrapper composite" section + "Reading the engine output" guide rewritten. + +- **`toMermaid` callable-subtree emit adoption** (engine [#174](https://github.com/mellonis/turing-machine-js/issues/174); no separate post-side issue — engine alpha.2 forced this). The wrapper composite is now a `[[bare(continuation)]]` call site OUTSIDE the subgraph; the callable subtree (`subgraph w_N["callable subtree of NAME"]`) contains the bare hopper + body states + a frame-local halt marker. Bold `==> "call"` arrow from wrapper to bare; dotted `-. "return" .->` from subgraph back to wrapper. The retired alpha.1 `-. onHalt .->` keyword no longer appears — wrapper-to-override is just a solid `-->` arrow. README's engine-emit Mermaid block regenerated. Test expectations updated. + + As a knock-on effect of separating wrapper/bare nodes, `summarizePostMachine` reports +1 `stateCount` per subroutine call site vs alpha.1. The example in the "Structural summary" section reports `7 1 1` (was `6 1 1` under alpha.1's collapsed-bare emit). + +### Compatibility + +- Peer dep `@turing-machine-js/machine` widened `^6.4.0` → `^7.0.0-alpha.2`. v4/v5/v6 engine majors are no longer supported on the v7 line — consumers must upgrade in lockstep. + +### Out of v7-alpha.2 (still pending for stable v7.0.0) + +- **[#72](https://github.com/mellonis/post-machine-js/issues/72)** — extend `defineProperty` lockdown to intermediate engine-graph states (continuations, hoppers, group wrappers). Construction-time tightening; doesn't affect runtime semantics for existing programs. + +### Migration + +For consumers updating from v6.x: + +**1. Engine identifier rename** — if you import `withOverrodeHaltState` directly from `@turing-machine-js/machine` (rare; PostMachine wraps it internally), rename to `withOverriddenHaltState`. + +**2. Wrapper composite shape in `state.name`** — `"foo>10~40"` is now `"foo(10~40)"`. Code that parses wrapper names by `>`-splitting needs to switch to paren-parsing. + +**3. State names with `(`/`)` rejected** — `new PostMachine({ "foo(bar)": { 1: stop } })` now throws. The collision is structural: paren is the new wrapper-composition delimiter. + +**4. `toMermaid` output format** — the wrapper now sits OUTSIDE the subgraph as a separate `[[…]]` node; the bare hopper is INSIDE the `callable subtree` subgraph as a regular `[…]` node. Body's halt-bound transitions land on the frame's halt marker `cN`, not on the real `s0` halt. If you render or pattern-match Mermaid output, the shape changed completely — see the README's "Reading the engine output" section. + +**5. `summarizePostMachine().stateCount` may shift** — each call site (`call(...)`) now contributes ONE more state to the count (the separate wrapper node). Existing assertions on exact stateCount need adjusting. + ## [6.4.0] - 2026-05-19 Adopts the engine's new [`onIter`](https://github.com/mellonis/turing-machine-js/pull/164) hook to fix a pre-existing `arrivalPath` ordering bug. **Version skips 6.2.0 and 6.3.0** — both were prepared but neither was published (see history note below). diff --git a/packages/machine/README.md b/packages/machine/README.md index f134f8c..3b5cc50 100644 --- a/packages/machine/README.md +++ b/packages/machine/README.md @@ -80,7 +80,7 @@ Reading the diagram: - `(((label)))` — halt state - `["label"]` — intermediate (and now also entry) state - `([idle])` — the `idle` sentinel that marks the entry point via a dotted `idle -. enter .-> s_initial` edge -- (Wrapper composites use a `subgraph w_N["halt frame"]` block — see the [Subroutines](#subroutines) section.) +- (Wrappers produced by `withOverriddenHaltState` use a `[[bare(continuation)]]` double-square node sitting OUTSIDE its callable subtree's `subgraph w_N["callable subtree of NAME"]` block — see the [Subroutines](#subroutines) section.) The labels are PostMachine's instruction-derived names — `"10"`, `"20"`, `"30"` map directly to the instruction indices in the program. The wrapper composite shape (`"()"`) doesn't appear in this example because there are no calls or groups; see the [Subroutines](#subroutines) section for that. The `40: stop` instruction is elided — `stop` halts the machine, so the transition from `30: mark` flows straight to halt rather than through an intermediate state. @@ -238,34 +238,37 @@ The state graph as the engine emits it — the subroutine and the wrapping `with flowchart TD %% alphabets: [[" ","*"]] s0(((halt))) - s5["rightToBlank::1"] - s6["rightToBlank::2"] - s7["1~2"] - s9["2"] + s4["1~2"] + s6["2"] + s5[["rightToBlank(1~2)"]] idle([idle]) - subgraph w_8["halt frame"] - s8[["rightToBlank"]] - c8(((halt))) + subgraph w_1["callable subtree of rightToBlank"] + s1["rightToBlank"] + s2["rightToBlank::1"] + s3["rightToBlank::2"] + c1(((halt))) end - idle -. enter .-> s8 - s5 -- "[*] → [K]/[R]" --> s6 - s6 -- "['*'] → [K]/[S]" --> s5 - s6 -- "[B] → [K]/[S]" --> s0 - s7 -- "[*] → [K]/[S]" --> s9 - s8 -- "[*] → [K]/[S]" --> s5 - s8 -. onHalt .-> s7 - s9 -- "[*] → ['*']/[S]" --> s0 + idle -. enter .-> s5 + s5 == "call" ==> s1 + w_1 -. "return" .-> s5 + s5 --> s4 + s1 -- "[*] → [K]/[S]" --> s2 + s2 -- "[*] → [K]/[R]" --> s3 + s3 -- "['*'] → [K]/[S]" --> s2 + s3 -- "[B] → [K]/[S]" --> c1 + s4 -- "[*] → [K]/[S]" --> s6 + s6 -- "[*] → ['*']/[S]" --> s0 ``` The `call('rightToBlank')` step at instruction 1 is built using the engine's `withOverriddenHaltState` composition primitive: the subroutine's halt is overridden to point at the next top-level instruction (instead of terminating the machine), so when the subroutine "halts" it actually returns to top-level execution at instruction 2. -Reading the diagram: -- The labels are PostMachine's instruction-derived names: `"rightToBlank::1"`/`"rightToBlank::2"` for the subroutine body, `"2"` for the top-level mark, `"1~2"` for the continuation, and the bare hopper name `"rightToBlank"` on the wrapped subroutine entry (the `[[…]]` double-square node `s8`). The wrapper's composite name `"rightToBlank(1~2)"` lives on the state's `.name` but isn't reproduced as a separate graph node — the `subgraph w_8["halt frame"]` block captures the same structural information in graph form. The `s\d+` node IDs are still auto-generated and shift between runs. -- `s8` (inside `w_8`) is the top-level entry — the engine's `idle -. enter .-> s8` edge marks it. The double-square `[[rightToBlank]]` shape signals "subroutine hopper" — i.e., a state wrapped by `withOverriddenHaltState`. The frame-local halt `c8` and the surrounding `subgraph w_8` together represent "halt within this subroutine's frame is the override target," replacing the v6 monolithic composite-named entry node. (Note: in this diagram `c8` has no incoming edges and the `onHalt` redirect is attached to `s8` rather than `c8` — tracked upstream as [turing-machine-js#173](https://github.com/mellonis/turing-machine-js/issues/173).) -- `s5`/`s6` form the subroutine's internal cycle: `s5` is `right` (keep+R), `s6` is `check(1, 3)` (loops back on `'*'`, exits to halt on blank). Note: `s6 → s0` is the literal `State` transition but **not** the runtime path — the wrapper's halt-stack intercepts and redirects to `s7`. The broader question of whether body states like `s5`/`s6` should visualize as inside the halt frame is tracked at [turing-machine-js#174](https://github.com/mellonis/turing-machine-js/issues/174). -- The dotted `onHalt` edge `s8 -.→ s7` is the override in action: when the wrapper's halt-stack intercepts a halt-bound transition from inside the subroutine, the engine pops back to `s7` (the continuation named `"1~2"`). -- `s7` is the continuation; it falls through (keep+S) to `s9`. -- `s9` is the `mark` instruction at top-level 2 (writes `'*'`, then transitions to halt — the trailing top-level `3: stop` is what produces that halt edge). +Reading the diagram (engine v7's callable-subtree emit): +- The labels are PostMachine's instruction-derived names: `"rightToBlank::1"`/`"rightToBlank::2"` for the subroutine body, `"2"` for the top-level mark, `"1~2"` for the continuation, the bare hopper name `"rightToBlank"` on the in-subgraph entry to the subtree, and the composite `"rightToBlank(1~2)"` on the wrapper itself (the `[[…]]` double-square node `s5`). The `s\d+` node IDs are still auto-generated and shift between runs. +- The wrapper `s5[["rightToBlank(1~2)"]]` is the **call site** — it sits OUTSIDE the subgraph. The `idle -. enter .-> s5` edge marks it as the top-level entry. The double-square `[[…]]` shape signals "wrapper" — a state produced by `withOverriddenHaltState`. The wrapper has no transitions of its own; it delegates to the bare via the bold `== "call" ==>` arrow. +- The `subgraph w_1["callable subtree of rightToBlank"]` is the **callable body** — it contains the hopper `s1`, the body states `s2`/`s3`, and a frame-local halt marker `c1`. The body's halt-bound transition (`s3 -- "[B]" --> c1`) lands on `c1`, not on the real `s0` halt. +- The dotted `w_1 -. "return" .-> s5` is the **return arrow** — when the body lands on `c1`, control returns to the wrapper `s5`. Then `s5 --> s4` (the solid wrapper-to-override arrow) hands off to the continuation. This replaces the alpha.1 `-. onHalt .->` keyword. +- `s4` is the continuation; it falls through (keep+S) to `s6`. +- `s6` is the `mark` instruction at top-level 2 (writes `'*'`, then transitions to halt — the trailing top-level `3: stop` is what produces that halt edge). That's just syntax — for one call site, inlining is equivalent. Subroutines earn their keep when the same logic appears at multiple sites or when symmetric variants share a shape. Example: extend a marked region by one cell on each side, using mirrored `walkRightToBlank` / `walkLeftToBlank` helpers. @@ -389,7 +392,7 @@ For programmatic lookup by instruction index, use `pm.candidatesFor(path)` (cons ### Engine v7 alignment -Engine v7 (upstream `@turing-machine-js/machine`) changed the wrapper composite shape from `A>B` to `A(B)` (paren-based). PostMachine's naming convention was designed to survive that change: none of our separators (`::`, `.`, `~`) collide with the new paren grammar, so only the *wrapper composite emit* shifted (e.g., the v6.x `"foo>10~40"` is now `"foo(10~40)"`). The names PostMachine constructs internally — and the rules in the table above — are unchanged. v7 also reshaped `toMermaid` output: wrapper composites now render as a `subgraph w_N["halt frame"]` block (with the bare hopper in `[[…]]` double-square brackets) instead of a single composite-named entry node. +Engine v7 (upstream `@turing-machine-js/machine`) changed the wrapper composite shape from `A>B` to `A(B)` (paren-based). PostMachine's naming convention was designed to survive that change: none of our separators (`::`, `.`, `~`) collide with the new paren grammar, so only the *wrapper composite emit* shifted (e.g., the v6.x `"foo>10~40"` is now `"foo(10~40)"`). The names PostMachine constructs internally — and the rules in the table above — are unchanged. v7's `toMermaid` output also adopted a callable-subtree model: the wrapper is a `[[bare(continuation)]]` call site OUTSIDE the subgraph, with a bold `==> "call"` arrow into the bare's subtree and a dotted `-. "return" .->` arrow back to the wrapper. Replaces v6.x's composite-named entry node. ## Introspection and equivalence @@ -451,10 +454,10 @@ console.log(a.stateCount, a.compositionEdgeCount, a.maxCompositionDepth); // 4 0 0 — inline: 4 states, no composition console.log(b.stateCount, b.compositionEdgeCount, b.maxCompositionDepth); -// 6 1 1 — subroutine: 2 more states; 1 composition edge from `call` (depth 1) +// 7 1 1 — subroutine: 3 more states; 1 composition edge from `call` (depth 1) ``` -Both programs do the same thing on the same input. This particular comparison is the **worst case for subroutines**: a single call site (no reuse benefit) with a small body — so the `withOverriddenHaltState` wrapper overhead per call (~2 states for the wrapper + routing intermediate) shows up as pure cost. Subroutines start saving states when reuse is real and the body amortizes the wrapper overhead — see the [extend example](#subroutines) above for symmetric variants and the [duplicate-marked-region example](../../README.md#an-example-with-subroutines) in the root README for true multi-call. +Both programs do the same thing on the same input. This particular comparison is the **worst case for subroutines**: a single call site (no reuse benefit) with a small body — so the `withOverriddenHaltState` wrapper overhead per call (~3 states under engine v7's callable-subtree emit: a separate wrapper node, the hopper bare, and the continuation) shows up as pure cost. Subroutines start saving states when reuse is real and the body amortizes the wrapper overhead — see the [extend example](#subroutines) above for symmetric variants and the [duplicate-marked-region example](../../README.md#an-example-with-subroutines) in the root README for true multi-call. What `summarizePostMachine` actually surfaces is the **structural trade-off**, not just state count: `compositionEdgeCount` and `maxCompositionDepth` go to zero in the inline version (everything is one flat graph) and become non-zero with subroutines (`call` creates a composition edge; nesting goes deeper). Use those fields to reason about the structure of reuse independently of raw size. diff --git a/packages/machine/package.json b/packages/machine/package.json index 634b186..30f942c 100644 --- a/packages/machine/package.json +++ b/packages/machine/package.json @@ -1,6 +1,6 @@ { "name": "@post-machine-js/machine", - "version": "6.4.0", + "version": "7.0.0-alpha.2", "description": "A convenient Post machine", "engines": { "npm": ">=7.0.0" @@ -28,10 +28,10 @@ "machine" ], "peerDependencies": { - "@turing-machine-js/machine": "^6.4.0" + "@turing-machine-js/machine": "^7.0.0-alpha.2" }, "devDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.1" + "@turing-machine-js/machine": "^7.0.0-alpha.2" }, "main": "dist/index.cjs", "module": "dist/index.mjs", diff --git a/packages/machine/test/examples.spec.ts b/packages/machine/test/examples.spec.ts index 50a0a83..d42a9b6 100644 --- a/packages/machine/test/examples.spec.ts +++ b/packages/machine/test/examples.spec.ts @@ -146,22 +146,30 @@ describe('packages/machine/README.md', () => { expect(mermaid).toContain('flowchart TD'); expect(mermaid).toContain('%% alphabets: [[" ","*"]]'); - // Halt + the entry — under engine v7 the wrapper composite is emitted as a - // halt-frame subgraph containing the bare hopper (double-square brackets `[[...]]`) - // and a frame-local halt node, not as a single composite-named round node. - // The composite `"rightToBlank(1~2)"` only lives on the wrapping State's `.name`. + // Halt + the entry — under engine v7's callable-subtree emit (alpha.2, + // #174) the wrapper is a separate `[[composite-name]]` node OUTSIDE the + // subgraph; the bare hopper is a regular `[name]` node INSIDE its + // callable subtree subgraph. Composite name `"rightToBlank(1~2)"` lives + // on the wrapper. expect(mermaid).toContain('(((halt)))'); - expect(mermaid).toMatch(/subgraph w_\d+\["halt frame"\]/); - expect(mermaid).toContain('[["rightToBlank"]]'); + expect(mermaid).toMatch(/subgraph w_\d+\["callable subtree of rightToBlank"\]/); + expect(mermaid).toContain('[["rightToBlank(1~2)"]]'); + expect(mermaid).toContain('["rightToBlank"]'); // bare inside the subgraph - // The dotted onHalt edge — the override path back from the subroutine. - expect(mermaid).toMatch(/s\d+ -\. onHalt \.-> s\d+/); + // Bold `== "call" ==>` from wrapper to bare + dotted `-. "return" .->` + // from subgraph back to wrapper. The retired alpha.1 `-. onHalt .->` + // keyword does not appear; wrapper-to-override is a regular solid arrow. + expect(mermaid).toMatch(/s\d+ == "call" ==> s\d+/); + expect(mermaid).toMatch(/w_\d+ -\. "return" \.-> s\d+/); + expect(mermaid).not.toMatch(/onHalt/); // The subroutine's internal cycle: a right-move state and a check state // that loops back on '*' and exits on the blank. Engine v7 label vocabulary. expect(mermaid).toMatch(/s\d+ -- "\[\*\] → \[K\]\/\[R\]" --> s\d+/); // right (keep + R) expect(mermaid).toMatch(/s\d+ -- "\['\*'\] → \[K\]\/\[S\]" --> s\d+/); // check on '*' - expect(mermaid).toMatch(/s\d+ -- "\[B\] → \[K\]\/\[S\]" --> s\d+/); // check on blank + // Body's halt-bound transition is retargeted to the frame's halt marker (c\d+), + // not the real s0 — that's the callable-subtree contract. + expect(mermaid).toMatch(/s\d+ -- "\[B\] → \[K\]\/\[S\]" --> c\d+/); // check on blank // The mark instruction's edge: write '*', stay, transition to halt. expect(mermaid).toMatch(/s\d+ -- "\[\*\] → \['\*'\]\/\[S\]" --> s\d+/); @@ -375,8 +383,11 @@ describe('packages/machine/README.md', () => { expect(a.compositionEdgeCount).toBe(0); expect(a.maxCompositionDepth).toBe(0); - // console.log(b.stateCount, b.compositionEdgeCount, b.maxCompositionDepth); // 6 1 1 - expect(b.stateCount).toBe(6); + // Engine alpha.2 (#174) emits wrappers as separate nodes from their + // bares; the subroutine adds 1 wrapper node on top of the bare hopper + + // body states + continuation + top-level mark. + // console.log(b.stateCount, b.compositionEdgeCount, b.maxCompositionDepth); // 7 1 1 + expect(b.stateCount).toBe(7); expect(b.compositionEdgeCount).toBe(1); expect(b.maxCompositionDepth).toBe(1); }); From 941efc1b89623d3c4492bca956cdb43903a6645a Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Thu, 21 May 2026 15:20:08 +0300 Subject: [PATCH 04/34] docs(CLAUDE): update peer-dep relationship section for v7 adoption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames the section header → and lists the three engine v7 changes that drove this release (#149, #148, #174). Fixes the dangling anchor link earlier in the file. Existing v5/v6 notes preserved as 'previously applied, still apply on v7'. --- CLAUDE.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 16358d5..b04d2d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co This is an npm-workspaces + Lerna monorepo with **one published package** so far: -- **`@post-machine-js/machine`** — a Post machine (a Turing-machine variant with a 2-symbol alphabet `{blank, mark}` and an instruction-numbered program model) implemented on top of `@turing-machine-js/machine`. The Turing engine is a **peer dependency** (see [Relationship to `@turing-machine-js/machine` v6.0.x](#relationship-to-turing-machine-jsmachine-v60x) below for the full version-relationship writeup). PostMachine pulls `State`, `TapeBlock`, `TuringMachine`, `Tape`, and several runtime singletons (`haltState`, `ifOtherSymbol`, the `movements` constants) from the engine. +- **`@post-machine-js/machine`** — a Post machine (a Turing-machine variant with a 2-symbol alphabet `{blank, mark}` and an instruction-numbered program model) implemented on top of `@turing-machine-js/machine`. The Turing engine is a **peer dependency** (see [Relationship to `@turing-machine-js/machine` v7.0.0-alpha.x](#relationship-to-turing-machine-jsmachine-v700-alphax) below for the full version-relationship writeup). PostMachine pulls `State`, `TapeBlock`, `TuringMachine`, `Tape`, and several runtime singletons (`haltState`, `ifOtherSymbol`, the `movements` constants) from the engine. ### How `PostMachine` maps to the Turing engine @@ -74,16 +74,22 @@ When adding or editing a README example, update the matching test in the same ch Non-README tests (sentinel-identity checks, internal plumbing) live in separately-named spec files (e.g. `v3.spec.ts`, `machine.spec.ts`), keeping the `examples.spec.ts` files purely doc-driven. -## Relationship to `@turing-machine-js/machine` v6.0.x +## Relationship to `@turing-machine-js/machine` v7.0.0-alpha.x `@turing-machine-js/machine` is declared as a **peer dependency** (and a devDependency for the in-repo build). Importantly, it must be a peer because the upstream library exposes two kinds of identity-sensitive surface that duplicate copies would break: - **Sentinel singletons** keyed by `Symbol(...)` — `haltState`, `ifOtherSymbol`, the members of `movements`, the members of `symbolCommands`. Equality checks (`=== haltState`, etc.) require the same physical object. - **Classes** — `Reference`, `State`, `TapeBlock`, `TuringMachine`, `Tape`, `Alphabet`. `instanceof` checks require shared constructor identity. -The current peer range is `^6.0.0`. **v4 and v5 are no longer supported** — a consumer still on those engine majors cannot install this package and must upgrade in lockstep. (post-machine-js skipped a v5 of its own — v6.0.0 is the first post release that crosses to engine v5/v6.) +The current peer range is `^7.0.0-alpha.2` (set in `@post-machine-js/machine@7.0.0-alpha.2`). **v4 / v5 / v6 are no longer supported on the v7 line** — a consumer still on those engine majors cannot install this package and must upgrade in lockstep. (post-machine-js skipped its own v5 and skipped its own v7-alpha.1; v7-alpha.2 is the first post prerelease that crosses to engine v7.) -The upstream v5/v6 changes that drove this release: +The engine v7 changes that drove this release: + +- **`withOverrodeHaltState` → `withOverriddenHaltState`** (engine [#149](https://github.com/mellonis/turing-machine-js/issues/149)). Consumer-side rename in `src/commands.ts`, `src/classes/PostMachine.ts`, and tests. +- **Wrapper composite shape `A>B` → `A(B)`** (engine [#148](https://github.com/mellonis/turing-machine-js/issues/148)). `parsePath` now rejects `(`/`)` in user-provided state names. The Post `Path` separators (`::`, `.`, `~`) survive unchanged. +- **`toMermaid` callable-subtree emit** (engine [#174](https://github.com/mellonis/turing-machine-js/issues/174)). The wrapper composite is now a `[[bare(continuation)]]` call site OUTSIDE the subgraph; the bare hopper + body live INSIDE `subgraph w_N["callable subtree of NAME"]`. Bold `==> "call"` arrow from wrapper to bare; dotted `-. "return" .->` from subgraph back to wrapper; retired `-. onHalt .->` (wrapper-to-override is now solid `-->`). Body's halt-bound transitions retarget to the frame's halt marker `cN`, not the real `s0`. Consequence: `summarizePostMachine().stateCount` is +1 per call site vs v6.x. + +Previous v5/v6 engine changes still apply unchanged on v7: - **`pm.run()` stays async.** Engine v4 introduced `Promise` return; v5/v6 didn't change that. Callers must still `await` it. - **`runStepByStep` stays unchanged.** Still a synchronous `Generator` (engine v6 narrowed the parent's generator return type back to `Generator`, matching post's existing override). From 83aeaf63f8e82421351da8cae06ec7ecd778586f Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Thu, 21 May 2026 15:49:54 +0300 Subject: [PATCH 05/34] docs(README): structural-summary diagrams + visualization fix + version-marker cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three doc improvements stacked on top of the alpha.2 release: 1. **Structural summary section gains side-by-side diagrams.** The inline-vs-subroutine comparison previously logged only the abstract `4 0 0` / `7 1 1` numbers. Two
blocks now show the engine emit for each machine, with prose pointing at the three extra nodes (wrapper, hopper bare, continuation) that drive the +3 stateCount delta. Readers SEE the trade-off, not just hear about it. 2. **Visualization section actually visualizes now.** The section was called "Visualization" but contained no Mermaid block — just a `console.log(mermaid.split('\n')[0])` and a stale cross-reference to a nonexistent `
` in Quick Start. Inlined the actual engine emit for the example machine. Updated the outdated #138/#139 reference to #174 with the bytewise-stable claim. 3. **Header-level (v6.1.0+) version markers stripped.** Headers like `## MachineState shape (v6.1.0+)` were useful during v6.x but lose meaning under v7-alpha.2's peer-dep range (`^7.0.0-alpha.2`). The CHANGELOG is the authoritative history. Inline history-context mentions (e.g., "stable as of v6.1.0; was experimental __onPause") are preserved — those explain API migrations and stay useful. 4. **Fixed broken anchor links** left by version-marker removal: `#machinestate-shape-v620` → `#machinestate-shape`, `#path-based-resolver-v630` → `#path-based-resolver`, `#breakpoints-v630` → `#breakpoints`. Tests added: the Structural summary test now also asserts the salient Mermaid shape features for both machines (no subgraph / no `[[…]]` wrapper on inline; `callable subtree of walkToBlank` subgraph + call / return arrows + retired-onHalt on withSubroutine). Verification: 267/267 tests pass, 100/100/100/100 coverage, lint clean. --- CLAUDE.md | 4 +- packages/machine/README.md | 95 +++++++++++++++++++++++--- packages/machine/test/examples.spec.ts | 24 +++++++ 3 files changed, 113 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b04d2d1..7e31b77 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,7 +57,7 @@ Key files: 5. **Group commands**: some producers throw if called from inside a "group" (`calledFromGroup` flag in `CommandContext`). This relates to PostMachine's grouping feature where multiple commands can be bundled into one logical instruction. Two distinct rules: `check`/`call`/`stop` reject group context unconditionally (regardless of form); the unary commands (`mark`/`erase`/`left`/`right`/`noop`) only reject the *indexed* form (`mark(20)` etc.) inside a group, because the explicit jump conflicts with the group's sequential fall-through semantics. -6. **Per-State debug lockdown (v6.1.0+)**: at the end of construction, `PostMachine` iterates `#stateToCandidatePaths.keys()` and calls `installStateLockdown(state, onUserWrite)` on every non-halt State. The installer replaces the engine's prototype `debug` accessor with an instance-level `Object.defineProperty`. Internal writes (from `#refreshStateDebug` / `#refreshHaltDebug`) run inside `withLockdownEscape` and delegate to the engine's prototype setter (which preserves the engine's `DebugConfig` wrapping + validation + shared-debugRef propagation across `withOverriddenHaltState` wrappers). User writes outside the escape go through the redirect handler: un-shared State → `setBreakpoint`/`clearBreakpoint`; shared State → throw with candidate-path list. `haltState` is locked module-globally in `src/index.ts` at module load — direct `haltState.debug = X` throws, no PostMachine context for a redirect. `state.isHalt` checks at the install site skip the engine's halt singleton (it has its own module-global lockdown). The lockdown does **not** use `Proxy` — that was tried during the v6.1.0 design phase and abandoned because engine utilities like `State.toGraph(arg, …)` read TS-downleveled private fields directly off the argument via `__classPrivateFieldGet`, which fails on a Proxy. +6. **Per-State debug lockdown**: at the end of construction, `PostMachine` iterates `#stateToCandidatePaths.keys()` and calls `installStateLockdown(state, onUserWrite)` on every non-halt State. The installer replaces the engine's prototype `debug` accessor with an instance-level `Object.defineProperty`. Internal writes (from `#refreshStateDebug` / `#refreshHaltDebug`) run inside `withLockdownEscape` and delegate to the engine's prototype setter (which preserves the engine's `DebugConfig` wrapping + validation + shared-debugRef propagation across `withOverriddenHaltState` wrappers). User writes outside the escape go through the redirect handler: un-shared State → `setBreakpoint`/`clearBreakpoint`; shared State → throw with candidate-path list. `haltState` is locked module-globally in `src/index.ts` at module load — direct `haltState.debug = X` throws, no PostMachine context for a redirect. `state.isHalt` checks at the install site skip the engine's halt singleton (it has its own module-global lockdown). The lockdown does **not** use `Proxy` — that was tried during the v6.1.0 design phase and abandoned because engine utilities like `State.toGraph(arg, …)` read TS-downleveled private fields directly off the argument via `__classPrivateFieldGet`, which fails on a Proxy. ## Doc examples must be tested @@ -94,7 +94,7 @@ Previous v5/v6 engine changes still apply unchanged on v7: - **`pm.run()` stays async.** Engine v4 introduced `Promise` return; v5/v6 didn't change that. Callers must still `await` it. - **`runStepByStep` stays unchanged.** Still a synchronous `Generator` (engine v6 narrowed the parent's generator return type back to `Generator`, matching post's existing override). - **`onPause` on `pm.run()`** (stable as of v6.1.0; was experimental `__onPause` in v6.0.0). Accepts `onPause?: (s: MachineState) => void | Promise` and forwards as the upstream `onPause` hook. The wrapper applies arrival-aware registry filtering: pauses fire only when `m.arrivalPath` (or a halt-arrival) matches a registered breakpoint. -- **Debugger primitives ARE wrapped (v6.1.0+).** `state.debug` and `haltState.debug` go through the per-State / module-global lockdown (see Subtlety 6 above). Construction-time writes funnel through `pm.setBreakpoint(target, filter)` / `pm.clearBreakpoint(target)` / `pm.clearBreakpoints()`; direct `state.debug = X` on an un-shared State auto-redirects to `setBreakpoint`. The engine-level concepts (filter shapes, `before → step → after` lifecycle, `haltState.debug.after` rejection) still apply — the lockdown is a thin layer in front, not a reimplementation. +- **Debugger primitives ARE wrapped.** `state.debug` and `haltState.debug` go through the per-State / module-global lockdown (see Subtlety 6 above). Construction-time writes funnel through `pm.setBreakpoint(target, filter)` / `pm.clearBreakpoint(target)` / `pm.clearBreakpoints()`; direct `state.debug = X` on an un-shared State auto-redirects to `setBreakpoint`. The engine-level concepts (filter shapes, `before → step → after` lifecycle, `haltState.debug.after` rejection) still apply — the lockdown is a thin layer in front, not a reimplementation. - **`run({ debug: boolean })` master switch (engine v5/#106).** Reachable via the upstream API; not wrapped at the PostMachine level. Useful in tests to suppress all `onPause` dispatches without unsetting `state.debug` assignments. - **v3 utility additions persist.** `State.toGraph`, `State.fromGraph`, `State.inspect`, `toMermaid`/`fromMermaid`, `summarize`/`summarizeGraph`, `equivalentOn`, and the `MachineState` type are all still re-exported from `@post-machine-js/machine`. v5/v6 added/refined debugger primitives without removing any v3 utilities. - **Post-aware wrappers persist unchanged.** `summarizePostMachine(machine)` and `equivalentPostMachines(reference, candidate, cases, options?)` remain the recommended path for typical usage. The bare upstream functions stay re-exported for advanced cases (e.g., comparing a PostMachine against a hand-rolled TuringMachine via `equivalentOn`). diff --git a/packages/machine/README.md b/packages/machine/README.md index 3b5cc50..ba01230 100644 --- a/packages/machine/README.md +++ b/packages/machine/README.md @@ -16,7 +16,7 @@ A Post machine — a 2-symbol Turing-machine variant with a numbered-instruction - [Commands](#commands) — [Classical](#classical-commands) · [Author's extensions](#authors-extensions) - [Grouped instructions](#grouped-instructions) - [Subroutines](#subroutines) -- [MachineState shape](#machinestate-shape-v620) +- [MachineState shape](#machinestate-shape) - [Naming convention](#naming-convention) - [Introspection and equivalence](#introspection-and-equivalence) — [Visualization](#visualization--tomermaid--statetograph) · [Structural summary](#structural-summary--summarizepostmachine) · [Behavioral equivalence](#behavioral-equivalence--equivalentpostmachines) - [Debugging](#debugging) @@ -307,7 +307,7 @@ The two helpers have the same shape — a `check`/move/loop pair — with mirror For a single subroutine called from MULTIPLE sites — the other archetypal use case — see the [duplicate-marked-region example](../../README.md#an-example-with-subroutines) in the root README. -## MachineState shape (v6.1.0+) +## MachineState shape PostMachine's `onStep` and `onPause` callbacks receive an extended `MachineState` with two additional fields: @@ -388,7 +388,7 @@ const m = new PostMachine({ PostMachine caches state nodes by command shape, so two instructions producing structurally-identical transitions (same command kind, same next-instruction target) share a single underlying `State` object. The shared state carries the name of the *first-processed* instruction. Behavior is identical regardless of which instruction control arrives through, but `MachineState.name` may report the canonical instruction's name rather than the caller's instruction index. -For programmatic lookup by instruction index, use `pm.candidatesFor(path)` (construction-time) or read `MachineState.candidatePaths` from an `onStep` / `onPause` callback (runtime). See [Path-based resolver](#path-based-resolver-v630) and [MachineState shape](#machinestate-shape-v620). +For programmatic lookup by instruction index, use `pm.candidatesFor(path)` (construction-time) or read `MachineState.candidatePaths` from an `onStep` / `onPause` callback (runtime). See [Path-based resolver](#path-based-resolver) and [MachineState shape](#machinestate-shape). ### Engine v7 alignment @@ -414,9 +414,26 @@ const mermaid = toMermaid(State.toGraph(machine.initialState, machine.tapeBlock) console.log(mermaid.split('\n')[0]); // flowchart TD ``` -The full rendered output is shown in the [Quick start](#quick-start) section's `
` block alongside the hand-drawn diagram, with a reading guide for the engine's compact `read → write/move` edge syntax. +The full rendered emit for this machine: -For the raw `Graph` as input to other tools (analysis, custom rendering, alternative serializations), use `State.toGraph(machine.initialState, machine.tapeBlock)` directly. The companion `fromMermaid` and `State.fromGraph` are also re-exported for round-trip workflows — load a Mermaid blob, get a `Graph` back, build a runnable machine from it. The round-trip is *behaviorally* lossless (same inputs produce same outputs) but not bytewise lossless because state IDs auto-reassign on each pass; see [turing-machine-js#138](https://github.com/mellonis/turing-machine-js/issues/138)/[#139](https://github.com/mellonis/turing-machine-js/issues/139) for the cleaner-emit and regression-test follow-ups on the upstream side. +```mermaid +flowchart TD +%% alphabets: [[" ","*"]] + s0(((halt))) + s1["10"] + s2["20"] + s3["30"] + idle([idle]) + idle -. enter .-> s1 + s1 -- "['*'] → [K]/[S]" --> s2 + s1 -- "[B] → [K]/[S]" --> s3 + s2 -- "[*] → [K]/[R]" --> s1 + s3 -- "[*] → ['*']/[S]" --> s0 +``` + +(Same machine as the [Quick start](#quick-start) example — see that section for the node/edge-shape reading guide.) + +For the raw `Graph` as input to other tools (analysis, custom rendering, alternative serializations), use `State.toGraph(machine.initialState, machine.tapeBlock)` directly. The companion `fromMermaid` and `State.fromGraph` are also re-exported for round-trip workflows — load a Mermaid blob, get a `Graph` back, build a runnable machine from it. Under engine v7 ([#174](https://github.com/mellonis/turing-machine-js/issues/174)), the round-trip is both behaviorally lossless AND bytewise stable for wrapped states (state IDs auto-reassign on each pass, but the emit shape — including shared-bare deduplication — is deterministic). ### Structural summary — `summarizePostMachine` @@ -459,6 +476,68 @@ console.log(b.stateCount, b.compositionEdgeCount, b.maxCompositionDepth); Both programs do the same thing on the same input. This particular comparison is the **worst case for subroutines**: a single call site (no reuse benefit) with a small body — so the `withOverriddenHaltState` wrapper overhead per call (~3 states under engine v7's callable-subtree emit: a separate wrapper node, the hopper bare, and the continuation) shows up as pure cost. Subroutines start saving states when reuse is real and the body amortizes the wrapper overhead — see the [extend example](#subroutines) above for symmetric variants and the [duplicate-marked-region example](../../README.md#an-example-with-subroutines) in the root README for true multi-call. +The two state graphs as the engine emits them — what the numbers above are summarizing: + +
+inline — flat 4-state graph, no composition + +```mermaid +flowchart TD +%% alphabets: [[" ","*"]] + s0(((halt))) + s1["10"] + s2["20"] + s3["30"] + idle([idle]) + idle -. enter .-> s1 + s1 -- "['*'] → [K]/[S]" --> s2 + s1 -- "[B] → [K]/[S]" --> s3 + s2 -- "[*] → [K]/[R]" --> s1 + s3 -- "[*] → ['*']/[S]" --> s0 +``` + +`s1` is `check`; on `'*'` it loops via `s2` (`right`); on blank it falls to `s3` (`mark`) → halt. Four nodes, one back-edge, zero subgraphs. + +
+ +
+withSubroutine — wrapper + callable subtree + continuation + +```mermaid +flowchart TD +%% alphabets: [[" ","*"]] + s0(((halt))) + s7["10~20"] + s9["20"] + s8[["walkToBlank(10~20)"]] + idle([idle]) + subgraph w_4["callable subtree of walkToBlank"] + s4["walkToBlank"] + s5["walkToBlank::1"] + s6["walkToBlank::2"] + c4(((halt))) + end + idle -. enter .-> s8 + s8 == "call" ==> s4 + w_4 -. "return" .-> s8 + s8 --> s7 + s4 -- "[*] → [K]/[S]" --> s5 + s5 -- "['*'] → [K]/[S]" --> s6 + s5 -- "[B] → [K]/[S]" --> c4 + s6 -- "[*] → [K]/[R]" --> s5 + s7 -- "[*] → [K]/[S]" --> s9 + s9 -- "[*] → ['*']/[S]" --> s0 +``` + +The three extra nodes vs inline that drive `stateCount: 4 → 7`: +- **`s8[["walkToBlank(10~20)"]]`** — the wrapper / call site, OUTSIDE the subgraph +- **`s4["walkToBlank"]`** — the bare hopper INSIDE the callable subtree, forwarded to from the wrapper's bold `== "call" ==>` arrow +- **`s7["10~20"]`** — the continuation that PostMachine synthesizes between the `call('walkToBlank')` site at instruction `10` and the next top-level instruction `20` + +The subroutine body (`s5`, `s6`) inside `subgraph w_4` mirrors `inline`'s `s1`, `s2` loop structurally — same algorithm, same internal back-edge. The extra cost is purely the wrapper machinery. `compositionEdgeCount: 0 → 1` and `maxCompositionDepth: 0 → 1` come from the single `withOverriddenHaltState` wrapper around `walkToBlank`. + +
+ What `summarizePostMachine` actually surfaces is the **structural trade-off**, not just state count: `compositionEdgeCount` and `maxCompositionDepth` go to zero in the inline version (everything is one flat graph) and become non-zero with subroutines (`call` creates a composition edge; nesting goes deeper). Use those fields to reason about the structure of reuse independently of raw size. `summarizePostMachine(machine)` is sugar for `summarize(machine.initialState, machine.tapeBlock)`. The bare `summarize` is also re-exported for callers who already hold a `(state, tapeBlock)` pair. @@ -485,7 +564,7 @@ Each case string is loaded onto a fresh clone of the originating PostMachine's t The bare `equivalentOn` is also re-exported. Use it directly when you need a non-PostMachine `Runnable` on either side (e.g., comparing a `PostMachine` against a hand-rolled `TuringMachine`). -## Path-based resolver (v6.1.0+) +## Path-based resolver `PostMachine` exposes three construction-time queries for addressing states by instruction path. @@ -508,9 +587,9 @@ pm.stateAt({ scope: 'sub', instructionIndex: 1 }); pm.stateAt({ scope: ['outer', 'inner'], instructionIndex: 1, groupInstructionIndex: 2 }); ``` -Returned States are the real engine States — `instanceof State`, usable with `State.toGraph`, `summarize`, and other engine utilities — but with `state.debug` set/get installed by PostMachine (see [Breakpoints](#breakpoints-v630) below). +Returned States are the real engine States — `instanceof State`, usable with `State.toGraph`, `summarize`, and other engine utilities — but with `state.debug` set/get installed by PostMachine (see [Breakpoints](#breakpoints) below). -## Breakpoints (v6.1.0+) +## Breakpoints Register pauses by instruction path or by `haltState`: diff --git a/packages/machine/test/examples.spec.ts b/packages/machine/test/examples.spec.ts index d42a9b6..5051425 100644 --- a/packages/machine/test/examples.spec.ts +++ b/packages/machine/test/examples.spec.ts @@ -390,6 +390,30 @@ describe('packages/machine/README.md', () => { expect(b.stateCount).toBe(7); expect(b.compositionEdgeCount).toBe(1); expect(b.maxCompositionDepth).toBe(1); + + // The README's Structural summary section shows both machines' + // toMermaid emits in
blocks alongside the counts. Pin the + // salient shape features so the doc claim "this is what the engine + // emits" doesn't go stale (looser-shape checks per existing repo + // convention; the engine repo has stricter normalized snapshots). + const inlineMermaid = toMermaid(State.toGraph(inline.initialState, inline.tapeBlock)); + const subMermaid = toMermaid(State.toGraph(withSubroutine.initialState, withSubroutine.tapeBlock)); + + // inline: flat graph, no subgraph, no wrapper (no `sN[[…]]` shape; the + // `[[` in `%% alphabets: [[" ","*"]]` is the JSON-stringified header). + expect(inlineMermaid).not.toMatch(/subgraph w_/); + expect(inlineMermaid).not.toMatch(/s\d+\[\[/); + expect(inlineMermaid).not.toMatch(/== "call" ==>/); + expect(inlineMermaid).toMatch(/idle -\. enter \.-> s\d+/); + + // withSubroutine: wrapper outside the subgraph + callable-subtree shape. + expect(subMermaid).toMatch(/subgraph w_\d+\["callable subtree of walkToBlank"\]/); + expect(subMermaid).toContain('[["walkToBlank(10~20)"]]'); // wrapper, composite name + expect(subMermaid).toContain('["walkToBlank"]'); // bare, inside subgraph + expect(subMermaid).toContain('["10~20"]'); // continuation + expect(subMermaid).toMatch(/s\d+ == "call" ==> s\d+/); // wrapper → bare + expect(subMermaid).toMatch(/w_\d+ -\. "return" \.-> s\d+/); + expect(subMermaid).not.toMatch(/onHalt/); }); }); From 8f2ef21f6fb9d8a1f12fb1a81ec2c8bdbe7cab36 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Thu, 21 May 2026 16:45:57 +0300 Subject: [PATCH 06/34] feat(PostMachine): drop subroutine hopper for acyclic plain-first-instruction case (#85) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PostMachine used to create a "hopper" State per subroutine — a stub that wrapped a `Reference` to the subroutine's first instruction, providing the forward-declaration anchor `withOverriddenHaltState` needs at the moment `call(...)` is processed. For the common case this indirection is dead weight. This commit drops the hopper for subroutines that: - don't participate in any cycle (Tarjan's SCC on the local call graph) - have a non-degenerate body (first instruction isn't `stop`) - have a non-wrapper first instruction (not a leading group `[…]` or leading `call(…)`) The hopper is retained otherwise: - cyclic subs need the forward-declaration anchor (mutual recursion `foo → bar → foo` continues to work via hoppers) - degenerate `{ 1: stop }` would wrap haltState directly, which the engine can't dispatch from - leading group/call would get unwrapped by engine #176's nested-wohs collapse, losing the inner wrapping's continuation Observable changes for hopper-dropped subs: - composite wrapper name: `foo(continuation)` → `foo::1(continuation)` - `summarizePostMachine().stateCount`: −1 per call site - `toMermaid` subgraph label: `"callable subtree of foo"` → `"callable subtree of foo::1"` - `onStep` callbacks: −1 per subroutine entry (the hopper used to fire its own `[*] → body₁` transition as a distinct step) New files: - packages/machine/src/callGraph.ts — Tarjan's SCC over local subroutine call graphs; identifies cyclic subs and emits a reverse-topological build order so acyclic subs' callees are built before their callers - packages/machine/test/callGraph.spec.ts — unit tests for the analyzer Changed: - packages/machine/src/commands.ts — `call()` producers are tagged in a WeakMap so the analyzer can read each producer's target subroutine name without invoking it - packages/machine/src/classes/PostMachine.ts — analyzes local call graph; creates hoppers ONLY for cyclic subs; processes subroutines in reverse-topological order; installs first-instruction State directly for hopper-dropped acyclic subs (with the safe-to-wrap fallback for haltState / wrapper first-instructions) Tests updated: - examples.spec.ts: Subroutines Mermaid emit + Structural summary counts - machine-state.spec.ts: arrivalPath test uses 2-instruction body - machine.spec.ts: call/sub-subroutines step counts (−1 per dropped hopper) - naming.spec.ts: composite names + body state names per new convention Docs: - packages/machine/README.md — Structural summary section's Mermaid block regenerated; narrative + counts reflect the new emit. Trailing caveat lists the four hopper-retained cases. - packages/machine/CHANGELOG.md — [7.0.0-alpha.3] entry with full migration walkthrough Version bump: - @post-machine-js/machine: 7.0.0-alpha.2 → 7.0.0-alpha.3 - Engine peer-dep unchanged: `^7.0.0-alpha.2` Verification: - npm test — 276/276 pass - npm run lint — clean - npm run typecheck — clean - npm run test:coverage — 100/100/100/100 - npm publish --dry-run --tag next — clean Closes #85. --- lerna.json | 2 +- package-lock.json | 2 +- packages/machine/CHANGELOG.md | 43 ++++ packages/machine/README.md | 47 ++-- packages/machine/package.json | 2 +- packages/machine/src/callGraph.ts | 248 ++++++++++++++++++++ packages/machine/src/classes/PostMachine.ts | 87 ++++++- packages/machine/src/commands.ts | 15 ++ packages/machine/test/callGraph.spec.ts | 137 +++++++++++ packages/machine/test/examples.spec.ts | 39 +-- packages/machine/test/machine-state.spec.ts | 11 +- packages/machine/test/machine.spec.ts | 21 +- packages/machine/test/naming.spec.ts | 56 +++-- 13 files changed, 627 insertions(+), 83 deletions(-) create mode 100644 packages/machine/src/callGraph.ts create mode 100644 packages/machine/test/callGraph.spec.ts diff --git a/lerna.json b/lerna.json index a18995b..0455ff2 100644 --- a/lerna.json +++ b/lerna.json @@ -3,6 +3,6 @@ "packages": [ "packages/*" ], - "version": "7.0.0-alpha.2", + "version": "7.0.0-alpha.3", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index da894ba..886a577 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10441,7 +10441,7 @@ }, "packages/machine": { "name": "@post-machine-js/machine", - "version": "7.0.0-alpha.2", + "version": "7.0.0-alpha.3", "license": "GPL-3.0-or-later", "devDependencies": { "@turing-machine-js/machine": "^7.0.0-alpha.2" diff --git a/packages/machine/CHANGELOG.md b/packages/machine/CHANGELOG.md index 6bcf519..bf50a00 100644 --- a/packages/machine/CHANGELOG.md +++ b/packages/machine/CHANGELOG.md @@ -4,6 +4,49 @@ All notable changes to this project will be documented in this file. 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). +## [7.0.0-alpha.3] - 2026-05-21 + +Third v7 pre-release. Drops the v6.x subroutine "hopper" State for the common case where it's not needed for forward-declaration ([#85](https://github.com/mellonis/post-machine-js/issues/85)). Engine peer-dep unchanged (`^7.0.0-alpha.2`). Published to npm under the `next` dist-tag: `npm install @post-machine-js/machine@next`. + +**Pre-release — the API surface may still shift before stable v7.0.0.** Pin to a specific alpha for reproducibility: `@post-machine-js/machine@7.0.0-alpha.3`. + +### Changed + +- **Subroutine hopper dropped for acyclic subroutines with plain first instruction** ([#85](https://github.com/mellonis/post-machine-js/issues/85)). PostMachine used to create a "hopper" State per subroutine — a stub State that wrapped a `Reference` to the subroutine's first instruction, providing a forward-declaration anchor for `withOverriddenHaltState`. For the common case, the hopper is now dropped: `call('foo')` wraps `foo::1` directly, saving one State per call site. + + The hopper is **retained** in three cases where dropping it would break the runtime: + - **Cyclic subroutines** (self-recursion or mutual recursion). Static call-graph analysis (Tarjan's SCC) identifies subroutines participating in cycles; the hopper provides the forward-declaration needed for `call('foo')` to wrap something at the moment of construction. Mutual recursion `foo → bar → foo` continues to work. + - **Degenerate body `{ 1: stop }`**. The first-instruction "State" would be `haltState` itself; wrapping `haltState` produces an empty `symbolToDataMap` and the engine throws at runtime. Hopper provides a meaningful intermediate. + - **Leading group `[…]` or leading `call(...)`**. The first-instruction State is itself a wrapper; engine's nested-wohs collapse (#176) would unwrap the inner wrapping when the outer wrapper applies, losing the group's or inner call's continuation. Hopper preserves the chain. + + Subroutines satisfying NONE of these — by far the common case — drop the hopper. + + Observable changes: + - **Composite wrapper name**: `foo(continuation)` → `foo::1(continuation)` for hopper-dropped subs. Accurately reflects the bare's identity. + - **`summarizePostMachine().stateCount`**: −1 per hopper-dropped subroutine. The "Structural summary" README example shifts from `7 1 1` (alpha.2) to `6 1 1`. + - **`toMermaid` subgraph label**: `"callable subtree of foo"` → `"callable subtree of foo::1"` for hopper-dropped subs. + - **`onStep` callbacks per subroutine entry**: −1 iteration (the hopper used to fire its own `[*] → body₁` transition as a separate step; under #85, the wrapper-of-body₁ executes body₁'s transitions directly). + +### Migration from alpha.2 + +**1. Wrapper composite name parser** — code that does `state.name.match(/^(\w+)\(/)` to extract the bare's name now sees `foo::1` for hopper-dropped subs (and still `foo` for hopper-retained ones). Use `state.bareStateId` (engine #174's GraphNode field) to identify the bare without parsing the name. + +**2. Test fixtures asserting `pm.initialState.name === 'foo(...)'`** — update to `'foo::1(...)'` for the hopper-dropped case. Or use a non-trivial body (multiple instructions) and assert on body-state names directly. + +**3. Test fixtures asserting exact `stateCount` or onStep call counts** — recompute under the new hopper-drop rules. + +**4. `pm.stateAt({ scope: ['foo'] })` or similar path lookups by subroutine name only** — under #85 there's no longer a graph node for the bare name in the hopper-dropped case. Lookups still resolve via the registry; behavior unchanged from a runtime perspective. + +### Out of v7-alpha.3 (still pending for stable v7.0.0) + +- **[#72](https://github.com/mellonis/post-machine-js/issues/72)** — extend `defineProperty` lockdown to intermediate engine-graph states. +- **[#86](https://github.com/mellonis/post-machine-js/issues/86)** — user-supplied tags/labels on states (Mermaid + debugger surfaces). +- **[#87](https://github.com/mellonis/post-machine-js/issues/87)** — README diagrams for `noop` and trailing-stop behaviors. + +### Compatibility + +- Engine peer dep unchanged: `^7.0.0-alpha.2`. + ## [7.0.0-alpha.2] - 2026-05-21 First post-machine-js v7 pre-release — adopts engine `@turing-machine-js/machine@7.0.0-alpha.2`. **post-machine-js skips its own v7 alpha.1**: engine alpha.1 was superseded by alpha.2 (which refined the `toMermaid` emit before any post-side adoption shipped), so post-machine-js's first v7 prerelease goes straight to alpha.2 matching the engine's current alpha. Published to npm under the `next` dist-tag: `npm install @post-machine-js/machine@next`. diff --git a/packages/machine/README.md b/packages/machine/README.md index ba01230..6424bc8 100644 --- a/packages/machine/README.md +++ b/packages/machine/README.md @@ -471,10 +471,10 @@ console.log(a.stateCount, a.compositionEdgeCount, a.maxCompositionDepth); // 4 0 0 — inline: 4 states, no composition console.log(b.stateCount, b.compositionEdgeCount, b.maxCompositionDepth); -// 7 1 1 — subroutine: 3 more states; 1 composition edge from `call` (depth 1) +// 6 1 1 — subroutine: 2 more states; 1 composition edge from `call` (depth 1) ``` -Both programs do the same thing on the same input. This particular comparison is the **worst case for subroutines**: a single call site (no reuse benefit) with a small body — so the `withOverriddenHaltState` wrapper overhead per call (~3 states under engine v7's callable-subtree emit: a separate wrapper node, the hopper bare, and the continuation) shows up as pure cost. Subroutines start saving states when reuse is real and the body amortizes the wrapper overhead — see the [extend example](#subroutines) above for symmetric variants and the [duplicate-marked-region example](../../README.md#an-example-with-subroutines) in the root README for true multi-call. +Both programs do the same thing on the same input. This particular comparison is the **worst case for subroutines**: a single call site (no reuse benefit) with a small body — so the `withOverriddenHaltState` wrapper overhead per call (~2 states under engine v7's callable-subtree emit + PostMachine's drop-acyclic-hopper rule from [#85](https://github.com/mellonis/post-machine-js/issues/85): the wrapper node and the continuation; the v6.x hopper is dropped for the common case of a plain leading command) shows up as pure cost. Subroutines start saving states when reuse is real and the body amortizes the wrapper overhead — see the [extend example](#subroutines) above for symmetric variants and the [duplicate-marked-region example](../../README.md#an-example-with-subroutines) in the root README for true multi-call. The two state graphs as the engine emits them — what the numbers above are summarizing: @@ -507,34 +507,33 @@ flowchart TD flowchart TD %% alphabets: [[" ","*"]] s0(((halt))) - s7["10~20"] - s9["20"] - s8[["walkToBlank(10~20)"]] + s6["10~20"] + s8["20"] + s7[["walkToBlank::1(10~20)"]] idle([idle]) - subgraph w_4["callable subtree of walkToBlank"] - s4["walkToBlank"] - s5["walkToBlank::1"] - s6["walkToBlank::2"] + subgraph w_4["callable subtree of walkToBlank::1"] + s4["walkToBlank::1"] + s5["walkToBlank::2"] c4(((halt))) end - idle -. enter .-> s8 - s8 == "call" ==> s4 - w_4 -. "return" .-> s8 - s8 --> s7 - s4 -- "[*] → [K]/[S]" --> s5 - s5 -- "['*'] → [K]/[S]" --> s6 - s5 -- "[B] → [K]/[S]" --> c4 - s6 -- "[*] → [K]/[R]" --> s5 - s7 -- "[*] → [K]/[S]" --> s9 - s9 -- "[*] → ['*']/[S]" --> s0 + idle -. enter .-> s7 + s7 == "call" ==> s4 + w_4 -. "return" .-> s7 + s7 --> s6 + s4 -- "['*'] → [K]/[S]" --> s5 + s4 -- "[B] → [K]/[S]" --> c4 + s5 -- "[*] → [K]/[R]" --> s4 + s6 -- "[*] → [K]/[S]" --> s8 + s8 -- "[*] → ['*']/[S]" --> s0 ``` -The three extra nodes vs inline that drive `stateCount: 4 → 7`: -- **`s8[["walkToBlank(10~20)"]]`** — the wrapper / call site, OUTSIDE the subgraph -- **`s4["walkToBlank"]`** — the bare hopper INSIDE the callable subtree, forwarded to from the wrapper's bold `== "call" ==>` arrow -- **`s7["10~20"]`** — the continuation that PostMachine synthesizes between the `call('walkToBlank')` site at instruction `10` and the next top-level instruction `20` +The two extra nodes vs inline that drive `stateCount: 4 → 6`: +- **`s7[["walkToBlank::1(10~20)"]]`** — the wrapper / call site, OUTSIDE the subgraph. Composite name `walkToBlank::1(10~20)` reflects that PostMachine drops the v6.x "hopper" anchor for acyclic subroutines with a plain first instruction (see [#85](https://github.com/mellonis/post-machine-js/issues/85)) — the wrapper wraps `walkToBlank::1` directly, saving one State. +- **`s6["10~20"]`** — the continuation that PostMachine synthesizes between the `call('walkToBlank')` site at instruction `10` and the next top-level instruction `20`. -The subroutine body (`s5`, `s6`) inside `subgraph w_4` mirrors `inline`'s `s1`, `s2` loop structurally — same algorithm, same internal back-edge. The extra cost is purely the wrapper machinery. `compositionEdgeCount: 0 → 1` and `maxCompositionDepth: 0 → 1` come from the single `withOverriddenHaltState` wrapper around `walkToBlank`. +The subroutine body (`s4`, `s5`) inside `subgraph w_4` mirrors `inline`'s `s1`, `s2` loop structurally — same algorithm, same internal back-edge. The extra cost is purely the wrapper + continuation machinery. `compositionEdgeCount: 0 → 1` and `maxCompositionDepth: 0 → 1` come from the single `withOverriddenHaltState` wrapper. + +(Subroutines with `1: stop`, a leading `call(...)`, a leading group `[...]`, or that participate in a call-graph cycle keep the hopper as a forward-declaration anchor. The common case — plain leading command — drops it.)
diff --git a/packages/machine/package.json b/packages/machine/package.json index 30f942c..f77cfc1 100644 --- a/packages/machine/package.json +++ b/packages/machine/package.json @@ -1,6 +1,6 @@ { "name": "@post-machine-js/machine", - "version": "7.0.0-alpha.2", + "version": "7.0.0-alpha.3", "description": "A convenient Post machine", "engines": { "npm": ">=7.0.0" diff --git a/packages/machine/src/callGraph.ts b/packages/machine/src/callGraph.ts new file mode 100644 index 0000000..e02d6ab --- /dev/null +++ b/packages/machine/src/callGraph.ts @@ -0,0 +1,248 @@ +// Static call-graph analysis for PostMachine subroutines (#85). +// +// At construction time, PostMachine creates a "hopper" State per subroutine — +// a stub State that wraps a `Reference` to the subroutine's first instruction. +// The hopper exists because `withOverriddenHaltState` needs a real State to +// wrap, and at the moment `call('foo')` is invoked the subroutine's body may +// not have been built yet (forward-reference / mutual-recursion case). +// +// For SUBROUTINES THAT DON'T PARTICIPATE IN CYCLES, the hopper is dead weight: +// if we build callees before callers, the first-instruction State exists by +// the time `call(...)` runs, and we can wrap it directly. The hopper-based +// indirection only earns its keep when there's a true cycle that no build +// order can break (a calls b, b calls a; or a calls a). +// +// This module identifies which local subroutines participate in cycles, so +// the PostMachine constructor can: +// 1. Create hoppers ONLY for cyclic subroutines. +// 2. Process subroutines in a build order such that each acyclic +// subroutine's callees (in the same scope) are built before it. +// +// The analysis is per-scope: nested subroutines participate in their own +// scope's analysis. A local sub calling an upper-scope sub is treated as a +// leaf edge (upper-scope subs are built before the local scope is even +// known, so they can't back-edge into local subs). + +import {callTargetOf} from './commands'; +import type {Instructions} from './commands'; +import type {CommandFn} from './consts'; + +export type CallGraphAnalysis = { + /** Subroutines that participate in cycles (mutual recursion or self-loop). */ + cyclicSet: Set; + /** + * Build order — Tarjan's SCC output is in reverse topological order, so + * the first item is a sink (no outgoing dependencies on later items). + * Processing in this order ensures each acyclic subroutine's local callees + * are built before it. + */ + buildOrder: string[]; +}; + +/** + * Analyzes a set of local subroutines and returns: + * - `cyclicSet`: subroutines that must keep their hopper (in non-trivial + * SCC or with self-loop). + * - `buildOrder`: order in which to recursively build them so that each + * acyclic sub's callees are built first. + * + * Edges to subroutines NOT in `localSubroutines` (upper-scope) are leaf + * edges — they don't affect cycle detection locally. + */ +export function analyzeLocalCallGraph( + localSubroutines: Record, +): CallGraphAnalysis { + const localNames = new Set(Object.keys(localSubroutines)); + + // Adjacency list: local-sub-name → set of LOCAL targets it calls. + const adj = new Map>(); + + for (const name of localNames) { + const allTargets = extractCallTargetsFromInstructions(localSubroutines[name]); + const localTargets = new Set([...allTargets].filter((t) => localNames.has(t))); + + adj.set(name, localTargets); + } + + // Tarjan's SCC algorithm. Outputs SCCs in reverse topological order — i.e., + // the first SCC in the result is a sink with no outgoing dependencies on + // later SCCs. Process in this order for correct build order. + const sccs = tarjanSCC(adj, localNames); + + const cyclicSet = new Set(); + + for (const scc of sccs) { + const isMultiNode = scc.length > 1; + + for (const member of scc) { + // `adj.get(member)` is always defined — every local name is keyed + // into `adj` in the loop above. The non-null assertion keeps the + // branch coverage at 100 without an unreachable `?? false` fallback. + const hasSelfLoop = adj.get(member)!.has(member); + + if (isMultiNode || hasSelfLoop) { + cyclicSet.add(member); + } + } + } + + // Build order is Tarjan's reverse-topological output flattened. Within an + // SCC, the order between members doesn't matter — all members are cyclic + // and use hoppers, so the build can proceed in any internal order. + const buildOrder: string[] = sccs.flatMap((scc) => scc); + + return {cyclicSet, buildOrder}; +} + +// Same as `extractCallTargets` above but with the right return type. The +// earlier function had a placeholder return; this is the real one. +function extractCallTargetsFromInstructions(instructions: Instructions): Set { + const targets = new Set(); + + // Malformed inputs (null/undefined/non-object) are caught downstream by + // PostMachine's instruction validation. The analyzer just returns no edges + // for them so the caller can proceed to its own throw site. + if (instructions === null || typeof instructions !== 'object') { + return targets; + } + + const visit = (value: unknown): void => { + if (typeof value === 'function') { + const target = callTargetOf(value as CommandFn); + + if (target !== undefined) { + targets.add(target); + } + } else if (Array.isArray(value)) { + for (const item of value) { + visit(item); + } + } + }; + + for (const key of Object.keys(instructions)) { + if (Number.isFinite(Number(key)) && key.trim() !== '') { + visit(instructions[key]); + } + } + + return targets; +} + +/** + * Tarjan's strongly-connected-components algorithm. + * + * Returns SCCs in REVERSE topological order — the first SCC has no outgoing + * edges to later SCCs, so it can be built first without forward references. + * Subsequent SCCs may depend on earlier ones. + * + * Iterative implementation to avoid recursion-depth issues on deep call + * chains (we never expect this to fire in practice but it's free defensive + * coverage). + */ +function tarjanSCC( + adj: Map>, + nodes: Set, +): string[][] { + const index = new Map(); + const lowlink = new Map(); + const onStack = new Set(); + const stack: string[] = []; + const sccs: string[][] = []; + let counter = 0; + + // Iterative DFS frame: (node, iteratorOverNeighbors). + type Frame = {node: string; neighbors: Iterator; pendingTarget: string | null}; + + const dfs = (start: string): void => { + const frames: Frame[] = []; + + index.set(start, counter); + lowlink.set(start, counter); + counter += 1; + stack.push(start); + onStack.add(start); + + // `adj.get(start)` is always defined — every `nodes` entry is keyed + // into `adj` by the caller before we start DFS. + frames.push({ + node: start, + neighbors: adj.get(start)!.values(), + pendingTarget: null, + }); + + while (frames.length > 0) { + const frame = frames[frames.length - 1]; + + // If we just returned from a child DFS, fold its lowlink into ours. + if (frame.pendingTarget !== null) { + lowlink.set( + frame.node, + Math.min(lowlink.get(frame.node)!, lowlink.get(frame.pendingTarget)!), + ); + frame.pendingTarget = null; + } + + const {value: next, done} = frame.neighbors.next(); + + if (done) { + // All neighbors visited — emit SCC if this node is a root. + if (lowlink.get(frame.node) === index.get(frame.node)) { + const scc: string[] = []; + + while (true) { + const popped = stack.pop()!; + + onStack.delete(popped); + scc.push(popped); + + if (popped === frame.node) break; + } + + sccs.push(scc); + } + + frames.pop(); + + // Tell parent which child we just returned from (for lowlink fold). + if (frames.length > 0) { + frames[frames.length - 1].pendingTarget = frame.node; + } + + continue; + } + + const target = next as string; + + if (!index.has(target)) { + // Tree edge — descend. + index.set(target, counter); + lowlink.set(target, counter); + counter += 1; + stack.push(target); + onStack.add(target); + + frames.push({ + node: target, + neighbors: adj.get(target)!.values(), + pendingTarget: null, + }); + } else if (onStack.has(target)) { + // Back edge to an ancestor in the current DFS tree. + lowlink.set( + frame.node, + Math.min(lowlink.get(frame.node)!, index.get(target)!), + ); + } + // else: cross edge to a finished SCC — ignore. + } + }; + + for (const node of nodes) { + if (!index.has(node)) { + dfs(node); + } + } + + return sccs; +} diff --git a/packages/machine/src/classes/PostMachine.ts b/packages/machine/src/classes/PostMachine.ts index 85ef0a8..e274ec5 100644 --- a/packages/machine/src/classes/PostMachine.ts +++ b/packages/machine/src/classes/PostMachine.ts @@ -23,6 +23,7 @@ import { } from '../commands'; import { instructionIndexValidator, subroutineNameValidator, validateSymbolPair } from '../validators'; import { installStateLockdown, withLockdownEscape } from '../lockdown'; +import { analyzeLocalCallGraph } from '../callGraph'; import { type Breakpoint, type BreakpointFilter, @@ -278,32 +279,98 @@ export class PostMachine extends TuringMachine { ...subroutinesDataFromUpperScope, ...localSubroutinesData, }; + // Cycle-aware hopper construction (#85). + // + // Static analysis of the local subroutine call graph identifies which + // subroutines participate in cycles (mutual recursion or self-loop). For + // those, we keep the v6.x hopper — a stub `State` that wraps a + // `Reference` to the subroutine's first instruction, providing the + // forward-declaration anchor that `withOverriddenHaltState` needs at the + // moment `call(...)` invocations are processed. + // + // Acyclic subroutines (the common case) skip the hopper. We process them + // in reverse-topological build order — sinks first — so by the time + // `call('X')` runs for an acyclic X, X's first-instruction State already + // exists and we wrap it directly. Net effect: -1 graph node per acyclic + // subroutine; the wrapper composite name becomes `X::1(continuation)` + // (accurately reflects the wrapped bare) instead of `X(continuation)`. + const localSubroutineInstructions: Record = Object.fromEntries( + Object.entries(localSubroutinesData).map(([name, data]) => [name, data.instructions]), + ); + const {cyclicSet, buildOrder} = analyzeLocalCallGraph(localSubroutineInstructions); + const subroutineInitialStates: Record = { ...subroutineInitialStatesFromUpperScope, - ...Object.keys(localSubroutinesData).reduce>((result, subroutineName) => ({ - ...result, - [subroutineName]: new State({ + }; + + // Create hoppers only for cyclic local subs. Acyclic entries are filled + // in after each acyclic sub's body is recursively built (below). + for (const subroutineName of Object.keys(localSubroutinesData)) { + if (cyclicSet.has(subroutineName)) { + subroutineInitialStates[subroutineName] = new State({ [ifOtherSymbol]: { nextState: localSubroutinesData[subroutineName].reference, }, - }, `${instructionPrefix}${subroutineName}`), - }), {}), - }; + }, `${instructionPrefix}${subroutineName}`); + } + } - Object.keys(localSubroutinesData).forEach((subroutineName) => { + // Build subroutines in reverse-topological order. Tarjan's SCC output + // (in `buildOrder`) starts with sinks — local subs with no outgoing + // calls to other local subs — so each sub's local callees are built (and + // present in `subroutineInitialStates`) before the sub itself is built. + for (const subroutineName of buildOrder) { const { reference, instructions: subroutineInstructions, } = subroutinesData[subroutineName]; - reference.bind(this.#buildInitialState({ + const firstInstructionState = this.#buildInitialState({ instructions: subroutineInstructions, subroutinesDataFromUpperScope: subroutinesData, subroutineInitialStatesFromUpperScope: subroutineInitialStates, instructionPrefix: `${instructionPrefix}${subroutineName}::`, scope: [...scope, subroutineName], - })); - }); + }); + + reference.bind(firstInstructionState); + + // Acyclic — install the first-instruction State as the subroutine's + // entry point IF it's safe to wrap. Two cases force a hopper fallback: + // + // 1. `firstInstructionState === haltState` (degenerate `{ 1: stop }` + // body). Wrapping haltState produces a State with an empty + // `symbolToDataMap`; the engine throws at runtime trying to + // resolve a transition. + // + // 2. `firstInstructionState` is itself a wrapper (group `[…]` or + // `call('bar')` as the subroutine's first instruction). Engine + // #176 collapses nested `withOverriddenHaltState` chains — the + // inner wrapping (group's own continuation, or the inner `call`'s + // continuation) gets unwrapped and lost when this wrapper is + // applied. Subsequent body instructions become unreachable. + // + // Both cases are detected by checking whether the first-instruction + // State is a plain bare (non-halt, no override). When it isn't, the + // hopper restores the invariant — it's a fresh State whose single + // `[ifOtherSymbol]` transition points at the first-instruction State, + // and wrapping the hopper preserves both the call-site continuation + // and any inner wrapping the first instruction already has. + if (!cyclicSet.has(subroutineName)) { + const canDropHopper = !firstInstructionState.isHalt + && firstInstructionState.overriddenHaltState === null; + + if (canDropHopper) { + subroutineInitialStates[subroutineName] = firstInstructionState; + } else { + subroutineInitialStates[subroutineName] = new State({ + [ifOtherSymbol]: { + nextState: firstInstructionState, + }, + }, `${instructionPrefix}${subroutineName}`); + } + } + } const instructionIndexList = Object.keys(instructionsCopy); diff --git a/packages/machine/src/commands.ts b/packages/machine/src/commands.ts index dbf870c..d21b33d 100644 --- a/packages/machine/src/commands.ts +++ b/packages/machine/src/commands.ts @@ -242,6 +242,20 @@ function stopCommandStateProducer(this: null, { calledFromGroup }: CommandContex return haltState; } +// WeakMap from `call('foo')`-produced state-producers to the subroutine name +// they reference. Used by the call-graph analyzer (#85 cycle detection — see +// `callGraph.ts`) to read each producer's target without invoking it. +const callTargets = new WeakMap(); + +/** + * Returns the subroutine name a `call(...)` producer targets, or `undefined` + * if the argument isn't a `call()` producer. Used at construction time to + * statically analyze which subroutines participate in cycles. + */ +export function callTargetOf(producer: CommandFn): string | undefined { + return callTargets.get(producer); +} + export function call(subroutineName: string, nextInstructionIndex?: number): (context: CommandContext) => State { const actualNextInstructionIndex = arguments.length === 1 ? defaultNextInstructionIndex : nextInstructionIndex; @@ -251,6 +265,7 @@ export function call(subroutineName: string, nextInstructionIndex?: number): (co }); commandsSet.add(actualCommand as CommandFn); + callTargets.set(actualCommand as CommandFn, subroutineName); return actualCommand as (context: CommandContext) => State; } diff --git a/packages/machine/test/callGraph.spec.ts b/packages/machine/test/callGraph.spec.ts new file mode 100644 index 0000000..c45997f --- /dev/null +++ b/packages/machine/test/callGraph.spec.ts @@ -0,0 +1,137 @@ +import {describe, expect, test} from 'vitest'; + +import {analyzeLocalCallGraph} from '../src/callGraph'; +import {call, mark, stop} from '../src/commands'; + +// Direct unit tests for the call-graph analyzer (#85). The full PostMachine +// integration tests (`machine.spec.ts`, `naming.spec.ts`, `examples.spec.ts`) +// exercise the analyzer indirectly through `new PostMachine(...)`, but those +// fixtures rarely hit the cyclic-SCC paths. These tests poke the algorithm +// directly to keep branch coverage at the repo's hard floor (100). + +describe('analyzeLocalCallGraph', () => { + test('empty input → no cyclic subs, no build order', () => { + const {cyclicSet, buildOrder} = analyzeLocalCallGraph({}); + + expect(cyclicSet.size).toBe(0); + expect(buildOrder).toEqual([]); + }); + + test('acyclic chain a → b → c → halt', () => { + const {cyclicSet, buildOrder} = analyzeLocalCallGraph({ + a: {1: call('b'), 2: stop}, + b: {1: call('c'), 2: stop}, + c: {1: mark}, + }); + + expect(cyclicSet.size).toBe(0); + // Build order is reverse-topological: sinks first. So c (no outgoing + // local calls) appears before b, and b before a. + expect(buildOrder.indexOf('c')).toBeLessThan(buildOrder.indexOf('b')); + expect(buildOrder.indexOf('b')).toBeLessThan(buildOrder.indexOf('a')); + }); + + test('mutual recursion a ↔ b — both marked cyclic', () => { + const {cyclicSet, buildOrder} = analyzeLocalCallGraph({ + a: {1: call('b'), 2: stop}, + b: {1: call('a'), 2: stop}, + }); + + expect(cyclicSet.has('a')).toBe(true); + expect(cyclicSet.has('b')).toBe(true); + expect(buildOrder).toContain('a'); + expect(buildOrder).toContain('b'); + }); + + test('self-recursion a → a — marked cyclic', () => { + const {cyclicSet} = analyzeLocalCallGraph({ + a: {1: call('a'), 2: stop}, + }); + + expect(cyclicSet.has('a')).toBe(true); + }); + + test('mixed: acyclic leaf + cyclic pair', () => { + const {cyclicSet} = analyzeLocalCallGraph({ + a: {1: call('b'), 2: call('leaf'), 3: stop}, + b: {1: call('a'), 2: stop}, + leaf: {1: mark}, + }); + + expect(cyclicSet.has('a')).toBe(true); + expect(cyclicSet.has('b')).toBe(true); + expect(cyclicSet.has('leaf')).toBe(false); + }); + + test('edges to non-local subs are leaf edges (no cycle detected)', () => { + // 'a' calls 'external' which isn't in the local map. The analyzer treats + // external as a leaf — no edge contributes to local cycle detection. + const {cyclicSet} = analyzeLocalCallGraph({ + a: {1: call('external'), 2: stop}, + }); + + expect(cyclicSet.size).toBe(0); + }); + + test('handles null instruction bodies without throwing', () => { + // PostMachine's downstream validation catches malformed inputs; the + // analyzer must not throw on them. + expect(() => analyzeLocalCallGraph({a: null as never})).not.toThrow(); + + const {cyclicSet} = analyzeLocalCallGraph({a: null as never}); + + expect(cyclicSet.size).toBe(0); + }); + + test('calls inside groups are also extracted', () => { + // `call` inside a group throws at PostMachine construction (per the + // group rules), but the analyzer is tolerant and walks group arrays so + // it can still classify malformed-but-syntactically-parsed inputs. + const {cyclicSet} = analyzeLocalCallGraph({ + a: {1: [mark, call('a')] as never, 2: stop}, + }); + + expect(cyclicSet.has('a')).toBe(true); + }); + + test('3-node cycle a → b → c → a', () => { + // Exercises the SCC algorithm's multi-element scc emit path. + const {cyclicSet, buildOrder} = analyzeLocalCallGraph({ + a: {1: call('b'), 2: stop}, + b: {1: call('c'), 2: stop}, + c: {1: call('a'), 2: stop}, + }); + + expect(cyclicSet.has('a')).toBe(true); + expect(cyclicSet.has('b')).toBe(true); + expect(cyclicSet.has('c')).toBe(true); + // All three end up in the same SCC (consecutive in build order). + expect(buildOrder.length).toBe(3); + }); + + test('non-function non-array number-keyed values are ignored', () => { + // Defensive: a string under a number key isn't a command. PostMachine's + // constructor catches this downstream; the analyzer just walks past it + // without contributing any edges. + const {cyclicSet} = analyzeLocalCallGraph({ + a: {1: 'not a command' as never, 2: stop}, + }); + + expect(cyclicSet.size).toBe(0); + }); + + test('cross edges (target already in a finished SCC) are ignored', () => { + // Graph: a→b, a→c, b→c. DFS from a visits b first (pushes), b→c (pushes + // c, c has no outgoing, c is its own SCC, pop). Back to b, b's SCC pops. + // Back to a, a→c: c is already indexed AND not on the stack → cross + // edge to a finished SCC. The analyzer ignores it. None of these are + // cycles, so cyclicSet stays empty. + const {cyclicSet} = analyzeLocalCallGraph({ + a: {1: call('b'), 2: call('c'), 3: stop}, + b: {1: call('c'), 2: stop}, + c: {1: mark}, + }); + + expect(cyclicSet.size).toBe(0); + }); +}); diff --git a/packages/machine/test/examples.spec.ts b/packages/machine/test/examples.spec.ts index 5051425..eafc370 100644 --- a/packages/machine/test/examples.spec.ts +++ b/packages/machine/test/examples.spec.ts @@ -146,15 +146,17 @@ describe('packages/machine/README.md', () => { expect(mermaid).toContain('flowchart TD'); expect(mermaid).toContain('%% alphabets: [[" ","*"]]'); - // Halt + the entry — under engine v7's callable-subtree emit (alpha.2, - // #174) the wrapper is a separate `[[composite-name]]` node OUTSIDE the - // subgraph; the bare hopper is a regular `[name]` node INSIDE its - // callable subtree subgraph. Composite name `"rightToBlank(1~2)"` lives - // on the wrapper. + // Halt + the entry — under engine v7's callable-subtree emit (#174) + // PLUS PostMachine's drop-acyclic-hopper change (#85), the wrapper at + // the call site wraps `rightToBlank::1` directly (not the v6.x hopper + // named `rightToBlank`). The subgraph label and composite name both + // reflect the bare's identity. expect(mermaid).toContain('(((halt)))'); - expect(mermaid).toMatch(/subgraph w_\d+\["callable subtree of rightToBlank"\]/); - expect(mermaid).toContain('[["rightToBlank(1~2)"]]'); - expect(mermaid).toContain('["rightToBlank"]'); // bare inside the subgraph + expect(mermaid).toMatch(/subgraph w_\d+\["callable subtree of rightToBlank::1"\]/); + expect(mermaid).toContain('[["rightToBlank::1(1~2)"]]'); + expect(mermaid).toContain('["rightToBlank::1"]'); // bare inside the subgraph + // The v6.x hopper named 'rightToBlank' is dropped (acyclic + plain first instr). + expect(mermaid).not.toContain('["rightToBlank"]'); // Bold `== "call" ==>` from wrapper to bare + dotted `-. "return" .->` // from subgraph back to wrapper. The retired alpha.1 `-. onHalt .->` @@ -383,11 +385,13 @@ describe('packages/machine/README.md', () => { expect(a.compositionEdgeCount).toBe(0); expect(a.maxCompositionDepth).toBe(0); - // Engine alpha.2 (#174) emits wrappers as separate nodes from their - // bares; the subroutine adds 1 wrapper node on top of the bare hopper + - // body states + continuation + top-level mark. - // console.log(b.stateCount, b.compositionEdgeCount, b.maxCompositionDepth); // 7 1 1 - expect(b.stateCount).toBe(7); + // Under #85, `walkToBlank` is acyclic + has a plain first instruction + // (check), so its hopper is dropped. The subroutine contributes 2 + // body states (walkToBlank::1, walkToBlank::2) + 1 continuation + + // the wrapper at instruction 10 + the top-level mark = 6 nodes + // (vs alpha.2's 7 with the hopper). + // console.log(b.stateCount, b.compositionEdgeCount, b.maxCompositionDepth); // 6 1 1 + expect(b.stateCount).toBe(6); expect(b.compositionEdgeCount).toBe(1); expect(b.maxCompositionDepth).toBe(1); @@ -407,9 +411,12 @@ describe('packages/machine/README.md', () => { expect(inlineMermaid).toMatch(/idle -\. enter \.-> s\d+/); // withSubroutine: wrapper outside the subgraph + callable-subtree shape. - expect(subMermaid).toMatch(/subgraph w_\d+\["callable subtree of walkToBlank"\]/); - expect(subMermaid).toContain('[["walkToBlank(10~20)"]]'); // wrapper, composite name - expect(subMermaid).toContain('["walkToBlank"]'); // bare, inside subgraph + // Under #85 the hopper is dropped — the wrapper wraps walkToBlank::1 + // directly, not a bare named 'walkToBlank'. + expect(subMermaid).toMatch(/subgraph w_\d+\["callable subtree of walkToBlank::1"\]/); + expect(subMermaid).toContain('[["walkToBlank::1(10~20)"]]'); // wrapper composite + expect(subMermaid).toContain('["walkToBlank::1"]'); // bare, inside subgraph + expect(subMermaid).not.toContain('["walkToBlank"]'); // hopper dropped expect(subMermaid).toContain('["10~20"]'); // continuation expect(subMermaid).toMatch(/s\d+ == "call" ==> s\d+/); // wrapper → bare expect(subMermaid).toMatch(/w_\d+ -\. "return" \.-> s\d+/); diff --git a/packages/machine/test/machine-state.spec.ts b/packages/machine/test/machine-state.spec.ts index eb0c55d..9513cf8 100644 --- a/packages/machine/test/machine-state.spec.ts +++ b/packages/machine/test/machine-state.spec.ts @@ -68,16 +68,21 @@ describe('PostMachine — wrapped MachineState', () => { }); test('subroutine body instruction has fully-qualified arrivalPath', async () => { + // Use a 2-instruction body so the second instruction is visited as its + // own iter (the first instruction's State is now the wrapper's bare + // under #85 — its arrivalPath is the call site, not the FQ subroutine + // path. The second body instruction always gets its own iter regardless + // of hopper-drop status). const m = new PostMachine({ 10: call('foo'), - foo: { 1: mark }, + foo: { 1: right, 2: mark }, }); const seen: MachineState[] = []; await m.run({ onStep: (s) => { seen.push(s); } }); - // After the call wrapper, control reaches foo::1. + // After the call wrapper executes foo::1, control reaches foo::2. const fooStep = seen.find(s => { const scope = s.arrivalPath.scope; - return Array.isArray(scope) && scope.join('::') === 'foo' && s.arrivalPath.instructionIndex === 1; + return Array.isArray(scope) && scope.join('::') === 'foo' && s.arrivalPath.instructionIndex === 2; }); expect(fooStep).toBeDefined(); }); diff --git a/packages/machine/test/machine.spec.ts b/packages/machine/test/machine.spec.ts index d6ce82b..161fab7 100644 --- a/packages/machine/test/machine.spec.ts +++ b/packages/machine/test/machine.spec.ts @@ -450,9 +450,15 @@ describe('run tests', () => { stepsLimit: 3, onStep: (...args) => onStepMock3(...args), })).resolves.toBeUndefined(); - expect(onStepMock1).toHaveBeenCalledTimes(3); - expect(onStepMock2).toHaveBeenCalledTimes(3); - expect(onStepMock3).toHaveBeenCalledTimes(3); + // Under #85, the subroutine's hopper is dropped (acyclic + plain first + // instruction noop). The previous "iter 1: hopper, iter 2: noop body, + // iter 3: halt" sequence collapses to "iter 1: wrapper-of-noop, iter 2: + // halt" — one fewer onStep call per machine. (Note: post-iter halt + // dispatches as the second call here because the wrapper-of-noop's + // halt-bound transition resolves to haltState directly.) + expect(onStepMock1).toHaveBeenCalledTimes(2); + expect(onStepMock2).toHaveBeenCalledTimes(2); + expect(onStepMock3).toHaveBeenCalledTimes(2); expect(onStepMock1.mock.calls).toEqual(onStepMock2.mock.calls); expect(onStepMock2.mock.calls).toEqual(onStepMock3.mock.calls); }); @@ -564,7 +570,14 @@ describe('run tests', () => { onStep: (...args) => onStepMock(...args), })).resolves.toBeUndefined(); - expect(onStepMock).toHaveBeenCalledTimes(8); + // Under #85, hoppers are dropped for `subroutineNameList[1]` (outer, + // first instr `mark`, acyclic) and the nested `subroutineNameList[0]` + // (first instr `erase`, acyclic). Outer `subroutineNameList[0]` keeps + // its hopper (the analyzer sees its body calling 'sub0' as a lexical + // self-reference, conservatively classifying it as cyclic — runtime + // would resolve through shadowing, but the static analyzer doesn't + // model scope shadowing). Net: 2 fewer onStep calls than v6.x's 8. + expect(onStepMock).toHaveBeenCalledTimes(6); expect(machine.tape.viewport[0]).toEqual(' '); const nextSymbolHistory = onStepMock.mock.calls.map((aCall) => aCall[0].nextSymbols[0]); diff --git a/packages/machine/test/naming.spec.ts b/packages/machine/test/naming.spec.ts index ee62019..686a768 100644 --- a/packages/machine/test/naming.spec.ts +++ b/packages/machine/test/naming.spec.ts @@ -129,20 +129,23 @@ describe('PostMachine — subroutine body and hopper names', () => { 3: mark, }, }); - // Wrapper composite at top — hopper now named "foo". - expect(machine.initialState.name).toBe('foo(10~halt)'); + // Acyclic subroutine + plain first instruction → hopper dropped (#85). + // The wrapper's bare is foo's first-instruction State (foo::1) directly; + // composite name reflects that. + expect(machine.initialState.name).toBe('foo::1(10~halt)'); // Subroutine body instructions are fully-qualified. const names = collectNames(machine); expect(names.has('foo::1')).toBe(true); expect(names.has('foo::2')).toBe(true); expect(names.has('foo::3')).toBe(true); + // Hopper dropped — no bare 'foo' node in the graph. + expect(names.has('foo')).toBe(false); }); - test('nested subroutines use fully-qualified hopper names', () => { - // outer::1 is a call (produces a wrapper composite, not a plain "outer::1" node); - // outer::2 is mark (produces a real state named "outer::2"). - // inner::1 is mark (produces a real state named "outer::inner::1"). + test('nested subroutines use fully-qualified instruction names', () => { + // outer::1 is a call (the inner call wraps a wrapper, so outer keeps its + // hopper); outer::2 is a plain mark; inner::1 is a plain mark. const machine = new PostMachine({ 10: call('outer'), outer: { @@ -151,17 +154,21 @@ describe('PostMachine — subroutine body and hopper names', () => { inner: { 1: mark }, }, }); - // Top wrapper composite uses the top-level hopper name "outer". + // outer's first instruction is `call('inner')` — that produces a wrapper, + // so #85's hopper-drop is blocked (engine #176 would unwrap the inner + // wrapping). outer keeps its hopper named "outer". expect(machine.initialState.name).toBe('outer(10~halt)'); const names = collectNames(machine); - // The nested call wrapper appears as bare 'outer::inner' (fq hopper) under v7's - // flatter emit; composite 'outer::inner(outer::1~outer::2)' lives only on state.name. - expect(names.has('outer::inner')).toBe(true); + // inner is acyclic with a plain first instruction (mark) → hopper dropped. + // No bare 'outer::inner' node; the inner-call wrapper composite is + // 'outer::inner::1(outer::1~outer::2)' and inner's body states have FQ names. + expect(names.has('outer::inner')).toBe(false); + expect(names.has('outer::inner::1')).toBe(true); // outer::2 is a plain mark instruction — it has its own named state. expect(names.has('outer::2')).toBe(true); - // Body states of inner subroutine are fully-qualified. - expect(names.has('outer::inner::1')).toBe(true); + // outer keeps its hopper (acyclic but first instr is a wrapper). + expect(names.has('outer')).toBe(true); }); }); @@ -176,12 +183,14 @@ describe('PostMachine — combined naming scenarios', () => { }, }); const names = collectNames(machine); - // Under v7, the wrapper inside foo for call('bar') appears as bare 'foo::bar' - // (fq-prefixed nested hopper) with a separate continuation 'foo::1~foo::2'. - expect(names.has('foo::bar')).toBe(true); + // Under #85, `bar` is acyclic + plain first instruction (mark) → hopper + // dropped; no bare 'foo::bar' node. The inner-call wrapper composite is + // 'foo::bar::1(foo::1~foo::2)' on state.name, with body state 'foo::bar::1' + // appearing as a node. + expect(names.has('foo::bar')).toBe(false); + expect(names.has('foo::bar::1')).toBe(true); expect(names.has('foo::1~foo::2')).toBe(true); expect(names.has('foo::2')).toBe(true); - expect(names.has('foo::bar::1')).toBe(true); }); test('group inside subroutine — inner indices namespaced', () => { @@ -210,9 +219,10 @@ describe('PostMachine — combined naming scenarios', () => { }, }); const names = collectNames(machine); - // Wrapper at foo::1 appears as bare 'foo::bar' under v7; composite - // 'foo::bar(foo::1~halt)' (tail position inside foo) lives only on state.name. - expect(names.has('foo::bar')).toBe(true); + // Under #85, `bar` is acyclic + plain first instruction → hopper dropped. + // No bare 'foo::bar' node; the bare 'foo::bar::1' is the wrapper's target, + // and the tail-position continuation is 'foo::1~halt'. + expect(names.has('foo::bar')).toBe(false); expect(names.has('foo::1~halt')).toBe(true); expect(names.has('foo::bar::1')).toBe(true); }); @@ -230,10 +240,10 @@ describe('PostMachine — combined naming scenarios', () => { }, }); const names = collectNames(machine); - // Each scope hops accumulate in the prefix. + // Each scope hops accumulate in the prefix. `deepest` is acyclic with a + // plain first instruction → hopper dropped (#85); only its body state + // 'outer::inner::deepest::1' appears in the graph. expect(names.has('outer::inner::deepest::1')).toBe(true); - // Body inner at outer::inner::1 calls deepest. Under v7, the wrapper appears as - // bare 'outer::inner::deepest' (the fq hopper) with separate continuation. - expect(names.has('outer::inner::deepest')).toBe(true); + expect(names.has('outer::inner::deepest')).toBe(false); }); }); From a36d86a40d9f16474a09ae9c8d63d6b0d9a18756 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Thu, 21 May 2026 16:57:44 +0300 Subject: [PATCH 07/34] chore: co-locate spec files with their source (engine-repo convention) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopts the spec-file storage convention from `mellonis/turing-machine-js`: `*.spec.ts` co-located next to the source file it tests, instead of batched in a separate `test/` directory. Matches the engine repo so contributors moving between the two see one consistent layout. File moves (all via `git mv` to preserve history): packages/machine/test/breakpoints.spec.ts → src/breakpoints.spec.ts packages/machine/test/callGraph.spec.ts → src/callGraph.spec.ts packages/machine/test/lockdown.spec.ts → src/lockdown.spec.ts packages/machine/test/path.spec.ts → src/path.spec.ts packages/machine/test/v3.spec.ts → src/v3.spec.ts packages/machine/test/helpers.ts → src/classes/PostMachine.test-helpers.ts packages/machine/test/machine.spec.ts → src/classes/PostMachine.spec.ts packages/machine/test/machine-state.spec.ts → src/classes/PostMachine.machine-state.spec.ts packages/machine/test/naming.spec.ts → src/classes/PostMachine.naming.spec.ts packages/machine/test/custom-alphabet.spec.ts → src/classes/PostMachine.custom-alphabet.spec.ts packages/machine/test/state-at.spec.ts → src/classes/PostMachine.state-at.spec.ts packages/machine/test/debugger.spec.ts → src/classes/PostMachine.debugger.spec.ts packages/machine/test/examples.spec.ts → src/classes/PostMachine.examples.spec.ts Files staying in test/: - `test/examples.spec.ts` (root README example tests — cross-package scope, kept at root like the engine's `test/round-trip.spec.ts`). Naming convention: - Top-level source modules: `.spec.ts` next to `.ts` (e.g. `breakpoints.ts` + `breakpoints.spec.ts`). - Classes with multiple test concerns: `..spec.ts` (e.g. `PostMachine.naming.spec.ts`, `PostMachine.debugger.spec.ts`). Matches the engine's `State.spec.ts` + `State.debug.spec.ts` pattern. Other changes: - `vitest.config.ts` include glob updated from `packages/*/test/**` to `packages/*/src/**/*.spec.ts`. Root `test/**/*.spec.ts` retained. - `packages/machine/test/tsconfig.json` removed (was used to type-check the now-removed test/ directory; the main src `tsconfig.json` already includes spec files for type-checking, and `tsconfig.build.json` already excludes `**/*.spec.ts` from the published build). - `CLAUDE.md` updated to describe the new convention. - Imports updated from `'../src/X'` to either `'./X'` or `'../X'` depending on the spec file's new depth. - `helpers.ts` → `PostMachine.test-helpers.ts`, placed next to its two consumers (`PostMachine.spec.ts`, `PostMachine.custom-alphabet.spec.ts`). Verification: - npm test — 278/278 pass - npm run lint — clean - npm run typecheck — clean - npm run test:coverage — 100/100/100/100 No code change. Pure file-organization refactor. --- CLAUDE.md | 6 +++--- packages/machine/{test => src}/breakpoints.spec.ts | 4 ++-- packages/machine/{test => src}/callGraph.spec.ts | 4 ++-- .../classes/PostMachine.custom-alphabet.spec.ts} | 4 ++-- .../classes/PostMachine.debugger.spec.ts} | 2 +- .../classes/PostMachine.examples.spec.ts} | 2 +- .../classes/PostMachine.machine-state.spec.ts} | 4 ++-- .../classes/PostMachine.naming.spec.ts} | 4 ++-- .../classes/PostMachine.spec.ts} | 6 +++--- .../classes/PostMachine.state-at.spec.ts} | 2 +- .../classes/PostMachine.test-helpers.ts} | 0 packages/machine/{test => src}/lockdown.spec.ts | 2 +- packages/machine/{test => src}/path.spec.ts | 2 +- packages/machine/{test => src}/v3.spec.ts | 2 +- packages/machine/test/tsconfig.json | 13 ------------- vitest.config.ts | 2 +- 16 files changed, 23 insertions(+), 36 deletions(-) rename packages/machine/{test => src}/breakpoints.spec.ts (99%) rename packages/machine/{test => src}/callGraph.spec.ts (97%) rename packages/machine/{test/custom-alphabet.spec.ts => src/classes/PostMachine.custom-alphabet.spec.ts} (98%) rename packages/machine/{test/debugger.spec.ts => src/classes/PostMachine.debugger.spec.ts} (99%) rename packages/machine/{test/examples.spec.ts => src/classes/PostMachine.examples.spec.ts} (99%) rename packages/machine/{test/machine-state.spec.ts => src/classes/PostMachine.machine-state.spec.ts} (98%) rename packages/machine/{test/naming.spec.ts => src/classes/PostMachine.naming.spec.ts} (99%) rename packages/machine/{test/machine.spec.ts => src/classes/PostMachine.spec.ts} (99%) rename packages/machine/{test/state-at.spec.ts => src/classes/PostMachine.state-at.spec.ts} (99%) rename packages/machine/{test/helpers.ts => src/classes/PostMachine.test-helpers.ts} (100%) rename packages/machine/{test => src}/lockdown.spec.ts (99%) rename packages/machine/{test => src}/path.spec.ts (99%) rename packages/machine/{test => src}/v3.spec.ts (99%) delete mode 100644 packages/machine/test/tsconfig.json diff --git a/CLAUDE.md b/CLAUDE.md index 7e31b77..b07a7ef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,11 +5,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Commands - `npm run build` — TypeScript project-references build (`tsc --build tsconfig.build.json`) followed by `scripts/build-node-entries.mjs`, which uses Rollup to repackage `dist/index.js` into `index.mjs` (ESM) and `index.cjs` (CJS). The Rollup step marks `@turing-machine-js/machine` as `external`, so the upstream Turing-machine engine stays as a runtime dependency. -- `npm test` — Vitest one-shot run (`vitest run`). Single root `vitest.config.ts`; tests are `test/**/*.spec.ts` (root README/example tests) plus `packages/*/test/**/*.spec.ts` (per-package). Vitest uses esbuild for TypeScript — no babel toolchain. +- `npm test` — Vitest one-shot run (`vitest run`). Single root `vitest.config.ts`; tests are co-located with source at `packages/*/src/**/*.spec.ts` (per-file unit + integration tests next to the module under test, matching the upstream engine's convention) plus `test/**/*.spec.ts` (root cross-package tests like README examples). Vitest uses esbuild for TypeScript — no babel toolchain. - `npm run test:watch` — Vitest in watch mode (`vitest`). - `npm run test:coverage` — `vitest run --coverage` using `@vitest/coverage-v8`. CI runs this and uploads `coverage/lcov.info` to Coveralls. Hard floors enforced in `vitest.config.ts`: **100 / 100 / 100 / 100** (statements / branches / functions / lines) — pinned to current actuals as of v6.4.0. Any new code paths must be exercised by tests; if a real regression makes 100 untenable, relax intentionally rather than letting drift slip through silently. - `npm run lint` — ESLint (flat config, `typescript-eslint` recommended). `dist/` is ignored. -- Run a single test: `npx vitest run packages/machine/test/machine.spec.ts -t "name"`. +- Run a single test: `npx vitest run packages/machine/src/classes/PostMachine.spec.ts -t "name"`. `npm` ≥ 7 is required (workspaces). Node 24 is what CI uses. @@ -64,7 +64,7 @@ Key files: Every executable code example in any `README.md` of this repo has a matching test in an `examples.spec.ts` co-located with that README: - Root `README.md` → `test/examples.spec.ts` -- `packages//README.md` → `packages//test/examples.spec.ts` +- `packages//README.md` → `packages//src/classes/.examples.spec.ts` (or another co-located location near the README's primary subject) (One `examples.spec.ts` per README. The repo will have N of them where N is the README count.) diff --git a/packages/machine/test/breakpoints.spec.ts b/packages/machine/src/breakpoints.spec.ts similarity index 99% rename from packages/machine/test/breakpoints.spec.ts rename to packages/machine/src/breakpoints.spec.ts index cc3e9af..571d497 100644 --- a/packages/machine/test/breakpoints.spec.ts +++ b/packages/machine/src/breakpoints.spec.ts @@ -5,12 +5,12 @@ import { Tape, haltState, mark, right, check, stop, -} from '../src/index'; +} from './index'; import { mergeBreakpointFilters, validateBreakpointFilter, type BreakpointFilter, -} from '../src/breakpoints'; +} from './breakpoints'; describe('mergeBreakpointFilters', () => { test('two `before: true` filters merge to `before: true`', () => { diff --git a/packages/machine/test/callGraph.spec.ts b/packages/machine/src/callGraph.spec.ts similarity index 97% rename from packages/machine/test/callGraph.spec.ts rename to packages/machine/src/callGraph.spec.ts index c45997f..eb59ab6 100644 --- a/packages/machine/test/callGraph.spec.ts +++ b/packages/machine/src/callGraph.spec.ts @@ -1,7 +1,7 @@ import {describe, expect, test} from 'vitest'; -import {analyzeLocalCallGraph} from '../src/callGraph'; -import {call, mark, stop} from '../src/commands'; +import {analyzeLocalCallGraph} from './callGraph'; +import {call, mark, stop} from './commands'; // Direct unit tests for the call-graph analyzer (#85). The full PostMachine // integration tests (`machine.spec.ts`, `naming.spec.ts`, `examples.spec.ts`) diff --git a/packages/machine/test/custom-alphabet.spec.ts b/packages/machine/src/classes/PostMachine.custom-alphabet.spec.ts similarity index 98% rename from packages/machine/test/custom-alphabet.spec.ts rename to packages/machine/src/classes/PostMachine.custom-alphabet.spec.ts index 4648121..1bfb2c0 100644 --- a/packages/machine/test/custom-alphabet.spec.ts +++ b/packages/machine/src/classes/PostMachine.custom-alphabet.spec.ts @@ -1,8 +1,8 @@ import { PostMachine, Tape, alphabet, blankSymbol, call, check, erase, left, mark, markSymbol, right, stop, summarizePostMachine, -} from '../src/index'; -import { getRandomInstructionIndex } from './helpers'; +} from '../index'; +import { getRandomInstructionIndex } from './PostMachine.test-helpers'; describe('PostMachine custom alphabet', () => { describe('default behavior (no options)', () => { diff --git a/packages/machine/test/debugger.spec.ts b/packages/machine/src/classes/PostMachine.debugger.spec.ts similarity index 99% rename from packages/machine/test/debugger.spec.ts rename to packages/machine/src/classes/PostMachine.debugger.spec.ts index caefc8a..9024fb0 100644 --- a/packages/machine/test/debugger.spec.ts +++ b/packages/machine/src/classes/PostMachine.debugger.spec.ts @@ -7,7 +7,7 @@ import { Tape, type MachineState, check, mark, right, stop, -} from '../src/index'; +} from '../index'; describe('PostMachine — async run', () => { function buildWalkAndMark(): PostMachine { diff --git a/packages/machine/test/examples.spec.ts b/packages/machine/src/classes/PostMachine.examples.spec.ts similarity index 99% rename from packages/machine/test/examples.spec.ts rename to packages/machine/src/classes/PostMachine.examples.spec.ts index eafc370..7cf1cf2 100644 --- a/packages/machine/test/examples.spec.ts +++ b/packages/machine/src/classes/PostMachine.examples.spec.ts @@ -13,7 +13,7 @@ import { formatPath, type MachineState, type Path, -} from '../src/index'; +} from '../index'; describe('packages/machine/README.md', () => { describe('Constants', () => { diff --git a/packages/machine/test/machine-state.spec.ts b/packages/machine/src/classes/PostMachine.machine-state.spec.ts similarity index 98% rename from packages/machine/test/machine-state.spec.ts rename to packages/machine/src/classes/PostMachine.machine-state.spec.ts index 9513cf8..24fb812 100644 --- a/packages/machine/test/machine-state.spec.ts +++ b/packages/machine/src/classes/PostMachine.machine-state.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest'; -import { PostMachine, mark, right, stop, call, parsePath } from '../src/index'; -import type { MachineState } from '../src/index'; +import { PostMachine, mark, right, stop, call, parsePath } from '../index'; +import type { MachineState } from '../index'; describe('PostMachine — wrapped MachineState', () => { test('onStep receives arrivalPath and candidatePaths', async () => { diff --git a/packages/machine/test/naming.spec.ts b/packages/machine/src/classes/PostMachine.naming.spec.ts similarity index 99% rename from packages/machine/test/naming.spec.ts rename to packages/machine/src/classes/PostMachine.naming.spec.ts index 686a768..766ef2d 100644 --- a/packages/machine/test/naming.spec.ts +++ b/packages/machine/src/classes/PostMachine.naming.spec.ts @@ -2,8 +2,8 @@ import { describe, expect, test } from 'vitest'; import { PostMachine, State, call, check, erase, left, mark, noop, right, stop, -} from '../src/index'; -import type { Graph } from '../src/index'; +} from '../index'; +import type { Graph } from '../index'; describe('PostMachine — top-level atomic-command names', () => { test('initialState has instruction-derived name "10"', () => { diff --git a/packages/machine/test/machine.spec.ts b/packages/machine/src/classes/PostMachine.spec.ts similarity index 99% rename from packages/machine/test/machine.spec.ts rename to packages/machine/src/classes/PostMachine.spec.ts index 161fab7..7f6cfc4 100644 --- a/packages/machine/test/machine.spec.ts +++ b/packages/machine/src/classes/PostMachine.spec.ts @@ -1,8 +1,8 @@ import { PostMachine, call, check, erase, left, mark, noop, right, stop, Tape, -} from '../src/index'; -import { subroutineNameValidator } from '../src/validators'; -import { getIxRange, getRandomInstructionIndex } from './helpers'; +} from '../index'; +import { subroutineNameValidator } from '../validators'; +import { getIxRange, getRandomInstructionIndex } from './PostMachine.test-helpers'; describe('constructor', () => { test('no instructions', () => { diff --git a/packages/machine/test/state-at.spec.ts b/packages/machine/src/classes/PostMachine.state-at.spec.ts similarity index 99% rename from packages/machine/test/state-at.spec.ts rename to packages/machine/src/classes/PostMachine.state-at.spec.ts index 9e97393..a55e107 100644 --- a/packages/machine/test/state-at.spec.ts +++ b/packages/machine/src/classes/PostMachine.state-at.spec.ts @@ -3,7 +3,7 @@ import { PostMachine, State, check, mark, right, stop, -} from '../src/index'; +} from '../index'; describe('pm.stateAt — happy paths', () => { test('top-level instruction by string', () => { diff --git a/packages/machine/test/helpers.ts b/packages/machine/src/classes/PostMachine.test-helpers.ts similarity index 100% rename from packages/machine/test/helpers.ts rename to packages/machine/src/classes/PostMachine.test-helpers.ts diff --git a/packages/machine/test/lockdown.spec.ts b/packages/machine/src/lockdown.spec.ts similarity index 99% rename from packages/machine/test/lockdown.spec.ts rename to packages/machine/src/lockdown.spec.ts index e8e63e5..61ba746 100644 --- a/packages/machine/test/lockdown.spec.ts +++ b/packages/machine/src/lockdown.spec.ts @@ -4,7 +4,7 @@ import { installStateLockdown, installHaltLockdown, withLockdownEscape, -} from '../src/lockdown'; +} from './lockdown'; describe('installStateLockdown', () => { function makeState(): State { diff --git a/packages/machine/test/path.spec.ts b/packages/machine/src/path.spec.ts similarity index 99% rename from packages/machine/test/path.spec.ts rename to packages/machine/src/path.spec.ts index c4d2987..3954ccf 100644 --- a/packages/machine/test/path.spec.ts +++ b/packages/machine/src/path.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { parsePath, formatPath, comparePathsCanonically } from '../src/path'; +import { parsePath, formatPath, comparePathsCanonically } from './path'; describe('parsePath — happy paths', () => { test('top-level instruction', () => { diff --git a/packages/machine/test/v3.spec.ts b/packages/machine/src/v3.spec.ts similarity index 99% rename from packages/machine/test/v3.spec.ts rename to packages/machine/src/v3.spec.ts index d5749fe..c4cf142 100644 --- a/packages/machine/test/v3.spec.ts +++ b/packages/machine/src/v3.spec.ts @@ -15,7 +15,7 @@ import { summarizePostMachine, equivalentPostMachines, check, mark, right, stop, -} from '../src/index'; +} from './index'; import { State as TuringState, toMermaid as turingToMermaid, diff --git a/packages/machine/test/tsconfig.json b/packages/machine/test/tsconfig.json deleted file mode 100644 index d182efe..0000000 --- a/packages/machine/test/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "noEmit": true, - "baseUrl": "..", - "rootDir": "..", - "paths": { - "@post-machine-js/machine": ["./src/index"], - "@turing-machine-js/machine": ["../../node_modules/@turing-machine-js/machine/dist/index"] - } - }, - "include": ["**/*.ts"] -} diff --git a/vitest.config.ts b/vitest.config.ts index 81b546f..c4b62ee 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ globals: true, clearMocks: true, include: [ - 'packages/*/test/**/*.spec.ts', + 'packages/*/src/**/*.spec.ts', 'test/**/*.spec.ts', ], coverage: { From 234b5c735b737f6e08b562e22686abfc886583e1 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Thu, 21 May 2026 17:03:27 +0300 Subject: [PATCH 08/34] docs(README): add diagrams for noop and trailing-stop behaviors (#87) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two PostMachine conventions were mentioned in prose but lacked the visual treatment that makes them concrete. Both now have collapsed
blocks alongside the existing Commands section, with matching test assertions in `PostMachine.examples.spec.ts`. ### noop in the graph (fall-through + unconditional jump) Two diagrams: 1. **Fall-through chain** — `{ 10: mark, 20: noop, 30: mark, 40: stop }` emits 3 reachable states. noop's signature `[*] → [K]/[S]` (read anything, keep cell, stay) is structurally distinguishable from the surrounding marks' `[*] → ['*']/[S]`. 2. **`noop(40)` as unconditional jump** — only the source instruction `10` appears; `20` and `30` are unreachable and silently dropped by `toGraph`. The trailing `40: stop` also elides (see below). Net diagram: a single state pointing directly at halt. ### Trailing stop doesn't get its own node `{ 10: mark, 20: stop }` emits ONE state (`s1["10"]`), not two — `stop` is a halt routing convention, not a State. Inline note covers the asymmetry that `pm.stateAt({ instructionIndex: 20 })` still resolves (to the canonical haltState singleton) even though the graph doesn't render a node for index 20. ### Tests Three new tests in `PostMachine.examples.spec.ts` under a new `Commands` describe block, mirroring the README structure: - `noop in a chain produces a single [K]/[S] state` — pins noop's emit signature alongside the surrounding marks - `noop(40) jumps directly; unreachable instructions are dropped` — verifies the unconditional-jump emit + the drop-unreachable behavior - `{ 10: mark, 20: stop } emits one state, not two` + companion test that `pm.stateAt` for the trailing-stop index resolves to haltState Shape-based assertions (`expect(mermaid).toContain('["10"]')`, regex on transition labels) — IDs aren't pinned, since the global State.#id counter isn't stable across test ordering. Verification: - npm test — 282/282 pass (279 prior + 3 new) - npm run lint — clean - npm run typecheck — clean - npm run test:coverage — 100/100/100/100 Closes #87. --- packages/machine/README.md | 77 +++++++++++++++++++ .../src/classes/PostMachine.examples.spec.ts | 74 +++++++++++++++++- 2 files changed, 150 insertions(+), 1 deletion(-) diff --git a/packages/machine/README.md b/packages/machine/README.md index 6424bc8..f22cdc0 100644 --- a/packages/machine/README.md +++ b/packages/machine/README.md @@ -177,6 +177,83 @@ The first table is the **canonical instruction set** of a Post(–Turing) machin `call` and the [Subroutines](#subroutines) feature add procedure-like reuse to the classical numbered-instruction model. `noop` is the placeholder of choice: useful for reserving instruction numbers in a worked example, padding a sketch, or as a labelled jump target. (Bare `noop` has no classical analog; `noop(ix)` corresponds to Post's unconditional jump.) +
+noop in the graph — fall-through and unconditional jump + +```javascript +const machine = new PostMachine({ + 10: mark, + 20: noop, + 30: mark, + 40: stop, +}); +``` + +```mermaid +flowchart TD +%% alphabets: [[" ","*"]] + s0(((halt))) + s1["10"] + s2["20"] + s3["30"] + idle([idle]) + idle -. enter .-> s1 + s1 -- "[*] → ['*']/[S]" --> s2 + s2 -- "[*] → [K]/[S]" --> s3 + s3 -- "[*] → ['*']/[S]" --> s0 +``` + +`s2` is the noop. Its single outgoing edge `[*] → [K]/[S]` is the signature: read anything, keep the cell (`K`, no write), stay in place (`S`, no move) — then fall through to instruction 30. The marks at `s1` and `s3` write `'*'` and move stay; the structural difference between a "useful" command and `noop` is the write cell (`'*'` vs `K`). + +Indexed form `noop(40)` rewires the fall-through to instruction 40 — Post's unconditional jump: + +```javascript +const machine = new PostMachine({ + 10: noop(40), + 20: mark, + 30: mark, + 40: stop, +}); +``` + +```mermaid +flowchart TD +%% alphabets: [[" ","*"]] + s0(((halt))) + s1["10"] + idle([idle]) + idle -. enter .-> s1 + s1 -- "[*] → [K]/[S]" --> s0 +``` + +Instruction `10` jumps directly to `40` (the trailing stop). Instructions `20` and `30` are unreachable — they don't appear in the graph at all. (`toGraph` only emits reachable states; unreachable ones are silently dropped.) + +
+ +
+Trailing stop doesn't get its own node + +```javascript +const machine = new PostMachine({ + 10: mark, + 20: stop, +}); +``` + +```mermaid +flowchart TD +%% alphabets: [[" ","*"]] + s0(((halt))) + s1["10"] + idle([idle]) + idle -. enter .-> s1 + s1 -- "[*] → ['*']/[S]" --> s0 +``` + +Notice: no `s20` node. `stop` halts the machine, so `s1` (`10: mark`) transitions directly to `s0(((halt)))` — there's no intermediate state for the `stop` instruction. The trailing `stop` is **elided** in the structural emit: it's a halt routing convention, not a State. + +The lookup API is asymmetric in a useful way — `pm.stateAt({ instructionIndex: 20 })` for this machine resolves to `haltState` (the canonical halt singleton), not `undefined`. The graph doesn't render `s20`, but the path still resolves.
+ ## Grouped instructions Several commands can share a single instruction number by passing them as an array: diff --git a/packages/machine/src/classes/PostMachine.examples.spec.ts b/packages/machine/src/classes/PostMachine.examples.spec.ts index 7cf1cf2..62e31fe 100644 --- a/packages/machine/src/classes/PostMachine.examples.spec.ts +++ b/packages/machine/src/classes/PostMachine.examples.spec.ts @@ -5,7 +5,7 @@ import { alphabet, blankSymbol, markSymbol, - call, check, left, mark, right, stop, + call, check, left, mark, noop, right, stop, toMermaid, summarizePostMachine, equivalentPostMachines, @@ -61,6 +61,78 @@ describe('packages/machine/README.md', () => { }); }); + describe('Commands', () => { + describe('noop in the graph — fall-through and unconditional jump', () => { + test('noop in a chain produces a single [K]/[S] state', () => { + const machine = new PostMachine({ + 10: mark, + 20: noop, + 30: mark, + 40: stop, + }); + + const mermaid = toMermaid(State.toGraph(machine.initialState, machine.tapeBlock)); + + // 3 reachable instructions (`40: stop` is elided — see trailing-stop test below). + expect(mermaid).toContain('["10"]'); + expect(mermaid).toContain('["20"]'); + expect(mermaid).toContain('["30"]'); + // noop's signature: `[K]/[S]` — keep, stay. Marks have `['*']/[S]`. + expect(mermaid).toMatch(/s\d+ -- "\[\*\] → \[K\]\/\[S\]" --> s\d+/); // noop + expect(mermaid).toMatch(/s\d+ -- "\[\*\] → \['\*'\]\/\[S\]" --> s\d+/); // mark + }); + + test('noop(40) jumps directly; unreachable instructions are dropped', () => { + const machine = new PostMachine({ + 10: noop(40), + 20: mark, + 30: mark, + 40: stop, + }); + + const mermaid = toMermaid(State.toGraph(machine.initialState, machine.tapeBlock)); + + // Only instruction 10 appears; 20/30 are unreachable, 40 = trailing stop. + expect(mermaid).toContain('["10"]'); + expect(mermaid).not.toContain('"20"'); + expect(mermaid).not.toContain('"30"'); + // 10's transition is noop's `[K]/[S]` straight to halt (s0). + expect(mermaid).toMatch(/s\d+ -- "\[\*\] → \[K\]\/\[S\]" --> s0/); + }); + }); + + describe('Trailing stop doesn\'t get its own node', () => { + test('{ 10: mark, 20: stop } emits one state, not two', () => { + const machine = new PostMachine({ + 10: mark, + 20: stop, + }); + + const mermaid = toMermaid(State.toGraph(machine.initialState, machine.tapeBlock)); + + expect(mermaid).toContain('["10"]'); + // No "20" label — the trailing stop is elided as a halt routing + // convention; instruction 10 transitions straight to s0(((halt))). + expect(mermaid).not.toContain('"20"'); + expect(mermaid).toMatch(/s\d+ -- "\[\*\] → \['\*'\]\/\[S\]" --> s0/); + }); + + test('pm.stateAt for a trailing-stop index resolves to haltState', () => { + const machine = new PostMachine({ + 10: mark, + 20: stop, + }); + + const stopState = machine.stateAt({instructionIndex: 20}); + + // Graph doesn't render `s20`, but the lookup API still resolves the + // path to the canonical haltState singleton. + expect(stopState).toBeDefined(); + expect(stopState!.isHalt).toBe(true); + }); + }); + }); + describe('Grouped instructions', () => { test('[mark, right, mark] under one label produces "**"', async () => { const machine = new PostMachine({ From 3209c0c8d3363b834aba9a197e8fcc2fdfe9067b Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Thu, 21 May 2026 21:26:34 +0300 Subject: [PATCH 09/34] feat: state tags + auto-tag policy (#86) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `$tag(...tags, command)` inline decorator wraps a command with one or more tags; rejects groups and bare-`$tag` misuse with helpful messages. - `pm.tag` / `pm.untag` / `pm.tagsOf` / `pm.findByTag` registry methods resolve paths and forward to engine `state.tag(...) / .untag(...) / .tags` — PostMachine doesn't maintain its own tag storage. - Auto-tag policy: each program/subroutine entry point is tagged at construction — top-level entry → `'main'`, subroutine entry → sub name. Halt-resolving entries (`stop`) are skipped so the tag doesn't leak via the globally-shared `haltState` singleton. - README gets a new `## Tags` section + updated example diagrams + rewritten Subroutines prose (was stale since alpha.3's hopper-drop). - Bumps to 7.0.0-alpha.4. Engine peer dep unchanged (`^7.0.0-alpha.3`). --- lerna.json | 2 +- package-lock.json | 14 +- package.json | 2 +- packages/machine/CHANGELOG.md | 39 ++++ packages/machine/README.md | 170 ++++++++++++++---- packages/machine/package.json | 6 +- .../src/classes/PostMachine.examples.spec.ts | 83 +++++++-- .../src/classes/PostMachine.tags.spec.ts | 158 ++++++++++++++++ packages/machine/src/classes/PostMachine.ts | 55 +++++- packages/machine/src/commands.spec.ts | 124 +++++++++++++ packages/machine/src/commands.ts | 80 +++++++++ packages/machine/src/index.ts | 2 +- 12 files changed, 677 insertions(+), 58 deletions(-) create mode 100644 packages/machine/src/classes/PostMachine.tags.spec.ts create mode 100644 packages/machine/src/commands.spec.ts diff --git a/lerna.json b/lerna.json index 0455ff2..32f00eb 100644 --- a/lerna.json +++ b/lerna.json @@ -3,6 +3,6 @@ "packages": [ "packages/*" ], - "version": "7.0.0-alpha.3", + "version": "7.0.0-alpha.4", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 886a577..58a8cf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "packages/*" ], "dependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.2" + "@turing-machine-js/machine": "^7.0.0-alpha.3" }, "devDependencies": { "@tsconfig/recommended": "^1.0.13", @@ -2740,9 +2740,9 @@ } }, "node_modules/@turing-machine-js/machine": { - "version": "7.0.0-alpha.2", - "resolved": "https://registry.npmjs.org/@turing-machine-js/machine/-/machine-7.0.0-alpha.2.tgz", - "integrity": "sha512-mGtG/yXznBnBjiVP3TbW0TaEGsCKoYstJRopK+WL38pQabiEyLyOlvYZlVICfvKkeA6MLjkuVyE644QeSNf+YQ==", + "version": "7.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@turing-machine-js/machine/-/machine-7.0.0-alpha.3.tgz", + "integrity": "sha512-+5FW2WRikdcXM2o7qKVnWeMCFuOMIAySV01ATZoFiYlz9o797+MFxVjpSmJ3KQNqTijeXutIPlVdQl2a0VHhXA==", "license": "GPL-3.0-or-later", "engines": { "npm": ">=7.0.0" @@ -10441,16 +10441,16 @@ }, "packages/machine": { "name": "@post-machine-js/machine", - "version": "7.0.0-alpha.3", + "version": "7.0.0-alpha.4", "license": "GPL-3.0-or-later", "devDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.2" + "@turing-machine-js/machine": "^7.0.0-alpha.3" }, "engines": { "npm": ">=7.0.0" }, "peerDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.2" + "@turing-machine-js/machine": "^7.0.0-alpha.3" } } } diff --git a/package.json b/package.json index 6fcd466..bac7b76 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,6 @@ "vitest": "^4.1.5" }, "dependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.2" + "@turing-machine-js/machine": "^7.0.0-alpha.3" } } diff --git a/packages/machine/CHANGELOG.md b/packages/machine/CHANGELOG.md index bf50a00..b15279b 100644 --- a/packages/machine/CHANGELOG.md +++ b/packages/machine/CHANGELOG.md @@ -4,6 +4,45 @@ All notable changes to this project will be documented in this file. 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). +## [7.0.0-alpha.4] - 2026-05-21 + +Fourth v7 pre-release. Adds user-supplied tags on states ([#86](https://github.com/mellonis/post-machine-js/issues/86)) — both an inline decorator at construction and a path-based registry post-construction — plus an auto-tag policy that marks each program's/subroutine's entry state. Engine peer-dep widened `^7.0.0-alpha.3` (the new `state.tag(...)` API was added by engine alpha.3). Published to npm under the `next` dist-tag: `npm install @post-machine-js/machine@next`. + +**Pre-release — the API surface may still shift before stable v7.0.0.** Pin to a specific alpha for reproducibility: `@post-machine-js/machine@7.0.0-alpha.4`. + +### Added + +- **`$tag(...tags, command)` inline decorator** ([#86](https://github.com/mellonis/post-machine-js/issues/86)). Wraps a command with one or more tags; tags apply to the resulting State via the engine's `state.tag(...)` API. The leading `$` flags it visually as a decorator (not a primitive command). Variadic — `$tag('hot', 'sampled', mark)` adds both tags. Rejects groups — `$tag('foo', [mark, right])` throws ("tag each member individually"). Rejects bare `$tag` (uninvoked) as an instruction with a message pointing at the correct form. Composes with indexed commands: `$tag('loop-head', check(20, 40))`, `$tag('subroutine-entry', call('foo'))`. + +- **Path-based tag registry on `PostMachine`** ([#86](https://github.com/mellonis/post-machine-js/issues/86)): + - `pm.tag(path, ...tags)` — add tags to the state at path + - `pm.untag(path, ...tags)` — remove tags (no-op if absent) + - `pm.tagsOf(path)` — frozen snapshot of the state's tags + - `pm.findByTag(tag)` — all paths whose state currently carries that tag + + All four resolve `path` the same way as `pm.stateAt` (string `'10'` / `'sub::1'` or object `{ instructionIndex: 10 }`). Throws on an unknown path. PostMachine does not maintain its own tag storage — all four forward to the engine's `state.tag(...)` / `.untag(...)` / `.tags` API. + +- **Auto-tag policy at construction** ([#86](https://github.com/mellonis/post-machine-js/issues/86)). PostMachine auto-tags the **entry point** of each program/subroutine: + - Top-level entry (first numbered instruction) → tag `'main'` + - Subroutine entry (first instruction of each subroutine body) → tag matching the subroutine name (`'sub'`, `'rightToBlank'`, …) + + Non-entry instructions and group inner states stay clean. Halt-resolving paths (`stop`-only entries) are skipped — `stop` resolves to the globally-shared engine `haltState` singleton, so tagging it would leak the tag across all PostMachine instances. Auto-tags compose with user tags; both accumulate on the same state. + +- **README `## Tags` section**. Documents `$tag`, the registry methods, the auto-tag policy, and the Mermaid output shape. Linked from the TOC and from the Subroutines section. + +### Changed + +- **README diagram outputs reflect auto-tag emit**. Existing example Mermaid blocks now show `
main` / `
rightToBlank` / `
walkToBlank` suffixes on entry-point node labels plus trailing `classDef tag_` + `class sN tag_` lines. The simple-subroutine `
` block was additionally rewritten to reflect the alpha.3 hopper-drop shape (`rightToBlank::1` bare + `rightToBlank::1(1~2)` composite wrapper) that had been left stale. + +### Compatibility + +- Engine peer dep unchanged: `^7.0.0-alpha.3` (already pinned at alpha.3 since the alpha.3 release; alpha.3's `state.tag(...)` / `.untag(...)` / `.tags` surface is what this release builds on). +- **Mermaid output ships auto-tag annotations by default.** Consumers that pin to exact diagram strings need to update their fixtures. The annotations are deterministic per machine. + +### Out of v7-alpha.4 (still pending for stable v7.0.0) + +- **[#72](https://github.com/mellonis/post-machine-js/issues/72)** — extend `defineProperty` lockdown to intermediate engine-graph states. + ## [7.0.0-alpha.3] - 2026-05-21 Third v7 pre-release. Drops the v6.x subroutine "hopper" State for the common case where it's not needed for forward-declaration ([#85](https://github.com/mellonis/post-machine-js/issues/85)). Engine peer-dep unchanged (`^7.0.0-alpha.2`). Published to npm under the `next` dist-tag: `npm install @post-machine-js/machine@next`. diff --git a/packages/machine/README.md b/packages/machine/README.md index f22cdc0..57d6de5 100644 --- a/packages/machine/README.md +++ b/packages/machine/README.md @@ -18,6 +18,7 @@ A Post machine — a 2-symbol Turing-machine variant with a numbered-instruction - [Subroutines](#subroutines) - [MachineState shape](#machinestate-shape) - [Naming convention](#naming-convention) +- [Tags](#tags) — [`$tag` decorator](#inline-tag-decorator) · [Registry](#post-construction-registry) · [Auto-tag policy](#auto-tag-policy) · [Mermaid output](#mermaid-output) - [Introspection and equivalence](#introspection-and-equivalence) — [Visualization](#visualization--tomermaid--statetograph) · [Structural summary](#structural-summary--summarizepostmachine) · [Behavioral equivalence](#behavioral-equivalence--equivalentpostmachines) - [Debugging](#debugging) - [Links](#links) @@ -63,7 +64,7 @@ The state graph for the example above (`toMermaid(State.toGraph(machine.initialS flowchart TD %% alphabets: [[" ","*"]] s0(((halt))) - s1["10"] + s1["10
main"] s2["20"] s3["30"] idle([idle]) @@ -72,6 +73,8 @@ flowchart TD s1 -- "[B] → [K]/[S]" --> s3 s2 -- "[*] → [K]/[R]" --> s1 s3 -- "[*] → ['*']/[S]" --> s0 + classDef tag_main fill:#dbeafe,stroke:#1e40af + class s1 tag_main ``` Reading the diagram: @@ -193,7 +196,7 @@ const machine = new PostMachine({ flowchart TD %% alphabets: [[" ","*"]] s0(((halt))) - s1["10"] + s1["10
main"] s2["20"] s3["30"] idle([idle]) @@ -201,6 +204,8 @@ flowchart TD s1 -- "[*] → ['*']/[S]" --> s2 s2 -- "[*] → [K]/[S]" --> s3 s3 -- "[*] → ['*']/[S]" --> s0 + classDef tag_main fill:#dbeafe,stroke:#1e40af + class s1 tag_main ``` `s2` is the noop. Its single outgoing edge `[*] → [K]/[S]` is the signature: read anything, keep the cell (`K`, no write), stay in place (`S`, no move) — then fall through to instruction 30. The marks at `s1` and `s3` write `'*'` and move stay; the structural difference between a "useful" command and `noop` is the write cell (`'*'` vs `K`). @@ -220,10 +225,12 @@ const machine = new PostMachine({ flowchart TD %% alphabets: [[" ","*"]] s0(((halt))) - s1["10"] + s1["10
main"] idle([idle]) idle -. enter .-> s1 s1 -- "[*] → [K]/[S]" --> s0 + classDef tag_main fill:#dbeafe,stroke:#1e40af + class s1 tag_main ``` Instruction `10` jumps directly to `40` (the trailing stop). Instructions `20` and `30` are unreachable — they don't appear in the graph at all. (`toGraph` only emits reachable states; unreachable ones are silently dropped.) @@ -244,10 +251,12 @@ const machine = new PostMachine({ flowchart TD %% alphabets: [[" ","*"]] s0(((halt))) - s1["10"] + s1["10
main"] idle([idle]) idle -. enter .-> s1 s1 -- "[*] → ['*']/[S]" --> s0 + classDef tag_main fill:#dbeafe,stroke:#1e40af + class s1 tag_main ``` Notice: no `s20` node. `stop` halts the machine, so `s1` (`10: mark`) transitions directly to `s0(((halt)))` — there's no intermediate state for the `stop` instruction. The trailing `stop` is **elided** in the structural emit: it's a halt routing convention, not a State. @@ -315,37 +324,40 @@ The state graph as the engine emits it — the subroutine and the wrapping `with flowchart TD %% alphabets: [[" ","*"]] s0(((halt))) - s4["1~2"] - s6["2"] - s5[["rightToBlank(1~2)"]] + s3["1~2"] + s5["2"] + s4[["rightToBlank::1(1~2)
main"]] idle([idle]) - subgraph w_1["callable subtree of rightToBlank"] - s1["rightToBlank"] - s2["rightToBlank::1"] - s3["rightToBlank::2"] + subgraph w_1["callable subtree of rightToBlank::1"] + s1["rightToBlank::1
rightToBlank"] + s2["rightToBlank::2"] c1(((halt))) end - idle -. enter .-> s5 - s5 == "call" ==> s1 - w_1 -. "return" .-> s5 - s5 --> s4 - s1 -- "[*] → [K]/[S]" --> s2 - s2 -- "[*] → [K]/[R]" --> s3 - s3 -- "['*'] → [K]/[S]" --> s2 - s3 -- "[B] → [K]/[S]" --> c1 - s4 -- "[*] → [K]/[S]" --> s6 - s6 -- "[*] → ['*']/[S]" --> s0 + idle -. enter .-> s4 + s4 == "call" ==> s1 + w_1 -. "return" .-> s4 + s4 --> s3 + s1 -- "[*] → [K]/[R]" --> s2 + s2 -- "['*'] → [K]/[S]" --> s1 + s2 -- "[B] → [K]/[S]" --> c1 + s3 -- "[*] → [K]/[S]" --> s5 + s5 -- "[*] → ['*']/[S]" --> s0 + classDef tag_main fill:#dbeafe,stroke:#1e40af + classDef tag_rightToBlank fill:#dbeafe,stroke:#1e40af + class s4 tag_main + class s1 tag_rightToBlank ``` The `call('rightToBlank')` step at instruction 1 is built using the engine's `withOverriddenHaltState` composition primitive: the subroutine's halt is overridden to point at the next top-level instruction (instead of terminating the machine), so when the subroutine "halts" it actually returns to top-level execution at instruction 2. -Reading the diagram (engine v7's callable-subtree emit): -- The labels are PostMachine's instruction-derived names: `"rightToBlank::1"`/`"rightToBlank::2"` for the subroutine body, `"2"` for the top-level mark, `"1~2"` for the continuation, the bare hopper name `"rightToBlank"` on the in-subgraph entry to the subtree, and the composite `"rightToBlank(1~2)"` on the wrapper itself (the `[[…]]` double-square node `s5`). The `s\d+` node IDs are still auto-generated and shift between runs. -- The wrapper `s5[["rightToBlank(1~2)"]]` is the **call site** — it sits OUTSIDE the subgraph. The `idle -. enter .-> s5` edge marks it as the top-level entry. The double-square `[[…]]` shape signals "wrapper" — a state produced by `withOverriddenHaltState`. The wrapper has no transitions of its own; it delegates to the bare via the bold `== "call" ==>` arrow. -- The `subgraph w_1["callable subtree of rightToBlank"]` is the **callable body** — it contains the hopper `s1`, the body states `s2`/`s3`, and a frame-local halt marker `c1`. The body's halt-bound transition (`s3 -- "[B]" --> c1`) lands on `c1`, not on the real `s0` halt. -- The dotted `w_1 -. "return" .-> s5` is the **return arrow** — when the body lands on `c1`, control returns to the wrapper `s5`. Then `s5 --> s4` (the solid wrapper-to-override arrow) hands off to the continuation. This replaces the alpha.1 `-. onHalt .->` keyword. -- `s4` is the continuation; it falls through (keep+S) to `s6`. -- `s6` is the `mark` instruction at top-level 2 (writes `'*'`, then transitions to halt — the trailing top-level `3: stop` is what produces that halt edge). +Reading the diagram (engine v7's callable-subtree emit + PostMachine's drop-acyclic-hopper rule from [#85](https://github.com/mellonis/post-machine-js/issues/85)): +- The labels are PostMachine's instruction-derived names: `"rightToBlank::1"`/`"rightToBlank::2"` for the subroutine body, `"2"` for the top-level mark, `"1~2"` for the continuation, and the composite `"rightToBlank::1(1~2)"` on the wrapper (the `[[…]]` double-square node `s4`). The `
main` and `
rightToBlank` suffixes are auto-tag annotations (#86) — the entry points of the top-level program and the subroutine, respectively. The `s\d+` node IDs are still auto-generated and shift between runs. +- The wrapper `s4[["rightToBlank::1(1~2)
main"]]` is the **call site** — it sits OUTSIDE the subgraph. The `idle -. enter .-> s4` edge marks it as the top-level entry; the auto-tag `main` reflects that role. The double-square `[[…]]` shape signals "wrapper" — a state produced by `withOverriddenHaltState`. The wrapper has no transitions of its own; it delegates to the bare via the bold `== "call" ==>` arrow. Under [#85](https://github.com/mellonis/post-machine-js/issues/85), the wrapper now wraps `rightToBlank::1` (the first instruction) directly — there is no v6.x "hopper" anchor for this acyclic-in-the-call-graph case. +- The `subgraph w_1["callable subtree of rightToBlank::1"]` is the **callable body** — it contains the bare entry `s1` (auto-tagged `rightToBlank` as the subroutine entry), the second-instruction state `s2`, and a frame-local halt marker `c1`. The body's halt-bound transition (`s2 -- "[B]" --> c1`) lands on `c1`, not on the real `s0` halt. +- The dotted `w_1 -. "return" .-> s4` is the **return arrow** — when the body lands on `c1`, control returns to the wrapper `s4`. Then `s4 --> s3` (the solid wrapper-to-override arrow) hands off to the continuation. This replaces the alpha.1 `-. onHalt .->` keyword. +- `s3` is the continuation; it falls through (keep+S) to `s5`. +- `s5` is the `mark` instruction at top-level 2 (writes `'*'`, then transitions to halt — the trailing top-level `3: stop` is what produces that halt edge). +- The trailing `classDef tag_main` / `classDef tag_rightToBlank` + `class` lines are auto-tag styling (see [Auto-tag policy](#auto-tag-policy)). That's just syntax — for one call site, inlining is equivalent. Subroutines earn their keep when the same logic appears at multiple sites or when symmetric variants share a shape. Example: extend a marked region by one cell on each side, using mirrored `walkRightToBlank` / `walkLeftToBlank` helpers. @@ -471,6 +483,92 @@ For programmatic lookup by instruction index, use `pm.candidatesFor(path)` (cons Engine v7 (upstream `@turing-machine-js/machine`) changed the wrapper composite shape from `A>B` to `A(B)` (paren-based). PostMachine's naming convention was designed to survive that change: none of our separators (`::`, `.`, `~`) collide with the new paren grammar, so only the *wrapper composite emit* shifted (e.g., the v6.x `"foo>10~40"` is now `"foo(10~40)"`). The names PostMachine constructs internally — and the rules in the table above — are unchanged. v7's `toMermaid` output also adopted a callable-subtree model: the wrapper is a `[[bare(continuation)]]` call site OUTSIDE the subgraph, with a bold `==> "call"` arrow into the bare's subtree and a dotted `-. "return" .->` arrow back to the wrapper. Replaces v6.x's composite-named entry node. +## Tags + +Tags are out-of-band string labels attached to states. They don't change runtime behavior — they layer semantic meaning over the auto-generated path-derived names ([Naming convention](#naming-convention)) for two surfaces: + +- **Mermaid diagrams** — `toMermaid` emits tags as `
`-suffixed annotations on node labels plus `classDef`/`class` lines for visual grouping. A reader who didn't write the program sees what the structurally important entry points are without re-deriving them from the instruction list. +- **Programmatic introspection** — `pm.findByTag(...)` retrieves paths by tag; debugger / analysis code can use tags as stable handles independent of state IDs. + +Three ways to apply tags: the **inline `$tag` decorator** at construction, the **`pm.tag` registry** post-construction, and the **auto-tag policy** which marks each program's / subroutine's entry point automatically. + +### Inline `$tag` decorator + +`$tag(...tags, command)` wraps a command with one or more tags. The tags apply to the resulting State; no extra graph node is created. The leading `$` flags it visually as a decorator (not a primitive command). + +```javascript +import { PostMachine, $tag, check, mark, right, stop } from '@post-machine-js/machine'; + +const machine = new PostMachine({ + 10: $tag('hot', check(20, 30)), // tag a single state + 20: $tag('loop-body', 'sampled', right(10)), // variadic — many tags at once + 30: mark, + 40: stop, +}); + +console.log(machine.tagsOf({ instructionIndex: 10 })); +// ['hot', 'main'] — inline 'hot' applied at producer time, then 'main' auto-tag +``` + +`$tag` rejects groups — `$tag('foo', [mark, right])` throws at construction. Tag each member individually: `[$tag('lift', mark), $tag('descend', right)]`. Passing bare `$tag` (without invoking it) as an instruction also throws with a helpful message. + +### Post-construction registry + +```typescript +pm.tag(path: Path | string, ...tags: string[]): void; +pm.untag(path: Path | string, ...tags: string[]): void; +pm.tagsOf(path: Path | string): readonly string[]; +pm.findByTag(tag: string): Path[]; +``` + +`tag` / `untag` are variadic (one call adds/removes any number of tags). `tagsOf` returns a frozen snapshot; `findByTag` returns all paths whose state currently carries that tag. All four resolve `path` the same way as [`pm.stateAt`](#path-based-resolver) — string form (`'10'`, `'sub::1'`) or object form (`{ instructionIndex: 10 }`). + +```javascript +import { PostMachine, mark, stop } from '@post-machine-js/machine'; + +const machine = new PostMachine({ 10: mark, 20: mark, 30: stop }); +machine.tag('10', 'checkpoint'); +machine.tag('20', 'checkpoint', 'hot'); + +console.log(machine.tagsOf('20')); // ['checkpoint', 'hot'] — no 'main' (20 is not the entry) +console.log(machine.findByTag('checkpoint').length); // 2 + +machine.untag('20', 'hot'); +console.log(machine.tagsOf('20')); // ['checkpoint'] +``` + +`pm.tag(...)` and `$tag(...)` compose: tags from both sources accumulate on the same state. Inline tags are applied at construction, before any post-construction `pm.tag` call sees the state. + +### Auto-tag policy + +At construction, PostMachine auto-tags the **entry point** of each program/subroutine: + +| Path | Auto-tag | +|---|---| +| Top-level entry (e.g., the first numbered instruction `1` or `10`) | `'main'` | +| Each subroutine's entry (e.g., `sub::1`, `rightToBlank::1`) | the subroutine name (e.g., `'sub'`, `'rightToBlank'`) | + +Non-entry instructions and group inner states stay clean. Halt-resolving paths (`stop`-only entries) are also skipped, because `stop` resolves to the engine's globally-shared `haltState` singleton — tagging it would leak across all PostMachine instances. The policy is mechanical and intentionally minimal: it anchors the structural roles without cluttering diagrams. + +```javascript +import { PostMachine, call, check, mark, right, stop } from '@post-machine-js/machine'; + +const machine = new PostMachine({ + 10: call('rightToBlank'), + 20: stop, + rightToBlank: { 1: check(2, 99), 2: right(1), 99: stop }, +}); + +console.log(machine.tagsOf('10')); // ['main'] — top-level entry +console.log(machine.tagsOf('rightToBlank::1')); // ['rightToBlank'] — subroutine entry +console.log(machine.tagsOf('rightToBlank::2')); // [] — body, non-entry +console.log(machine.findByTag('main').map((p) => p.instructionIndex)); // [10] +``` + +### Mermaid output + +When tags are present (auto-tag or user-applied), `toMermaid` emits them inline in node labels via `
` and as `classDef`/`class` lines for color grouping. The styling palette is hashed deterministically per tag name — same tag name → same color across runs. See the [Visualization](#visualization--tomermaid--statetograph) section below for full example output. + ## Introspection and equivalence The v3 utilities from [`@turing-machine-js/machine`](https://github.com/mellonis/turing-machine-js/tree/master/packages/machine) work directly against a `PostMachine`. For the two most common ones — `summarize` and `equivalentOn` — this package also ships Post-aware free-function wrappers (`summarizePostMachine`, `equivalentPostMachines`) that bind the standard arguments and hide the `getTapeBlock`-must-clone footgun. **Prefer the wrappers for typical use.** The bare upstream functions are still re-exported here for advanced cases. @@ -497,7 +595,7 @@ The full rendered emit for this machine: flowchart TD %% alphabets: [[" ","*"]] s0(((halt))) - s1["10"] + s1["10
main"] s2["20"] s3["30"] idle([idle]) @@ -506,6 +604,8 @@ flowchart TD s1 -- "[B] → [K]/[S]" --> s3 s2 -- "[*] → [K]/[R]" --> s1 s3 -- "[*] → ['*']/[S]" --> s0 + classDef tag_main fill:#dbeafe,stroke:#1e40af + class s1 tag_main ``` (Same machine as the [Quick start](#quick-start) example — see that section for the node/edge-shape reading guide.) @@ -562,7 +662,7 @@ The two state graphs as the engine emits them — what the numbers above are sum flowchart TD %% alphabets: [[" ","*"]] s0(((halt))) - s1["10"] + s1["10
main"] s2["20"] s3["30"] idle([idle]) @@ -571,6 +671,8 @@ flowchart TD s1 -- "[B] → [K]/[S]" --> s3 s2 -- "[*] → [K]/[R]" --> s1 s3 -- "[*] → ['*']/[S]" --> s0 + classDef tag_main fill:#dbeafe,stroke:#1e40af + class s1 tag_main ``` `s1` is `check`; on `'*'` it loops via `s2` (`right`); on blank it falls to `s3` (`mark`) → halt. Four nodes, one back-edge, zero subgraphs. @@ -586,10 +688,10 @@ flowchart TD s0(((halt))) s6["10~20"] s8["20"] - s7[["walkToBlank::1(10~20)"]] + s7[["walkToBlank::1(10~20)
main"]] idle([idle]) subgraph w_4["callable subtree of walkToBlank::1"] - s4["walkToBlank::1"] + s4["walkToBlank::1
walkToBlank"] s5["walkToBlank::2"] c4(((halt))) end @@ -602,6 +704,10 @@ flowchart TD s5 -- "[*] → [K]/[R]" --> s4 s6 -- "[*] → [K]/[S]" --> s8 s8 -- "[*] → ['*']/[S]" --> s0 + classDef tag_main fill:#dbeafe,stroke:#1e40af + classDef tag_walkToBlank fill:#ede9fe,stroke:#5b21b6 + class s7 tag_main + class s4 tag_walkToBlank ``` The two extra nodes vs inline that drive `stateCount: 4 → 6`: diff --git a/packages/machine/package.json b/packages/machine/package.json index f77cfc1..3b411d8 100644 --- a/packages/machine/package.json +++ b/packages/machine/package.json @@ -1,6 +1,6 @@ { "name": "@post-machine-js/machine", - "version": "7.0.0-alpha.3", + "version": "7.0.0-alpha.4", "description": "A convenient Post machine", "engines": { "npm": ">=7.0.0" @@ -28,10 +28,10 @@ "machine" ], "peerDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.2" + "@turing-machine-js/machine": "^7.0.0-alpha.3" }, "devDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.2" + "@turing-machine-js/machine": "^7.0.0-alpha.3" }, "main": "dist/index.cjs", "module": "dist/index.mjs", diff --git a/packages/machine/src/classes/PostMachine.examples.spec.ts b/packages/machine/src/classes/PostMachine.examples.spec.ts index 62e31fe..0b2b6f8 100644 --- a/packages/machine/src/classes/PostMachine.examples.spec.ts +++ b/packages/machine/src/classes/PostMachine.examples.spec.ts @@ -5,7 +5,7 @@ import { alphabet, blankSymbol, markSymbol, - call, check, left, mark, noop, right, stop, + $tag, call, check, left, mark, noop, right, stop, toMermaid, summarizePostMachine, equivalentPostMachines, @@ -74,7 +74,8 @@ describe('packages/machine/README.md', () => { const mermaid = toMermaid(State.toGraph(machine.initialState, machine.tapeBlock)); // 3 reachable instructions (`40: stop` is elided — see trailing-stop test below). - expect(mermaid).toContain('["10"]'); + // Entry-point auto-tag (#86) appends `
main` to the top-level entry's label. + expect(mermaid).toContain('["10
main"]'); expect(mermaid).toContain('["20"]'); expect(mermaid).toContain('["30"]'); // noop's signature: `[K]/[S]` — keep, stay. Marks have `['*']/[S]`. @@ -93,7 +94,8 @@ describe('packages/machine/README.md', () => { const mermaid = toMermaid(State.toGraph(machine.initialState, machine.tapeBlock)); // Only instruction 10 appears; 20/30 are unreachable, 40 = trailing stop. - expect(mermaid).toContain('["10"]'); + // Entry-point auto-tag (#86) appends `
main` to the top-level entry's label. + expect(mermaid).toContain('["10
main"]'); expect(mermaid).not.toContain('"20"'); expect(mermaid).not.toContain('"30"'); // 10's transition is noop's `[K]/[S]` straight to halt (s0). @@ -110,7 +112,8 @@ describe('packages/machine/README.md', () => { const mermaid = toMermaid(State.toGraph(machine.initialState, machine.tapeBlock)); - expect(mermaid).toContain('["10"]'); + // Entry-point auto-tag (#86) appends `
main` to the top-level entry's label. + expect(mermaid).toContain('["10
main"]'); // No "20" label — the trailing stop is elided as a halt routing // convention; instruction 10 transitions straight to s0(((halt))). expect(mermaid).not.toContain('"20"'); @@ -225,10 +228,14 @@ describe('packages/machine/README.md', () => { // reflect the bare's identity. expect(mermaid).toContain('(((halt)))'); expect(mermaid).toMatch(/subgraph w_\d+\["callable subtree of rightToBlank::1"\]/); - expect(mermaid).toContain('[["rightToBlank::1(1~2)"]]'); - expect(mermaid).toContain('["rightToBlank::1"]'); // bare inside the subgraph + // Wrapper bears the top-level entry auto-tag `main` (#86). + expect(mermaid).toContain('[["rightToBlank::1(1~2)
main"]]'); + // Bare inside the subgraph bears the subroutine-entry auto-tag (#86). + expect(mermaid).toContain('["rightToBlank::1
rightToBlank"]'); // The v6.x hopper named 'rightToBlank' is dropped (acyclic + plain first instr). - expect(mermaid).not.toContain('["rightToBlank"]'); + // The bare's display label is now `rightToBlank::1
rightToBlank`, so the + // bare 'rightToBlank' (without `::1`) does not appear as a node label. + expect(mermaid).not.toMatch(/s\d+\["rightToBlank"\]/); // Bold `== "call" ==>` from wrapper to bare + dotted `-. "return" .->` // from subgraph back to wrapper. The retired alpha.1 `-. onHalt .->` @@ -376,6 +383,54 @@ describe('packages/machine/README.md', () => { }); }); + describe('Tags', () => { + describe('Inline $tag decorator', () => { + test('$tag wraps a command and accumulates with the auto-tag', () => { + const machine = new PostMachine({ + 10: $tag('hot', check(20, 30)), + 20: $tag('loop-body', 'sampled', right(10)), + 30: mark, + 40: stop, + }); + + // Insertion order: inline tag applied at producer time, auto-tag last. + expect(machine.tagsOf({ instructionIndex: 10 })).toEqual(['hot', 'main']); + // Non-entry instruction — no auto-tag, just the two inline tags. + expect(machine.tagsOf({ instructionIndex: 20 })).toEqual(['loop-body', 'sampled']); + }); + }); + + describe('Post-construction registry', () => { + test('pm.tag / pm.untag / pm.tagsOf / pm.findByTag', () => { + const machine = new PostMachine({ 10: mark, 20: mark, 30: stop }); + machine.tag('10', 'checkpoint'); + machine.tag('20', 'checkpoint', 'hot'); + + // 20 is not the entry, so no 'main' — just user tags. + expect(machine.tagsOf('20')).toEqual(['checkpoint', 'hot']); + expect(machine.findByTag('checkpoint')).toHaveLength(2); + + machine.untag('20', 'hot'); + expect(machine.tagsOf('20')).toEqual(['checkpoint']); + }); + }); + + describe('Auto-tag policy', () => { + test('entry-only auto-tagging: top-level entry → "main", subroutine entry → sub name', () => { + const machine = new PostMachine({ + 10: call('rightToBlank'), + 20: stop, + rightToBlank: { 1: check(2, 99), 2: right(1), 99: stop }, + }); + + expect(machine.tagsOf('10')).toEqual(['main']); + expect(machine.tagsOf('rightToBlank::1')).toEqual(['rightToBlank']); + expect(machine.tagsOf('rightToBlank::2')).toEqual([]); + expect(machine.findByTag('main').map((p) => p.instructionIndex)).toEqual([10]); + }); + }); + }); + describe('Introspection and equivalence', () => { describe('Visualization — toMermaid + State.toGraph', () => { function buildQuickStart(): PostMachine { @@ -412,10 +467,12 @@ describe('packages/machine/README.md', () => { // Initial state — square-bracket node shape; under engine v7 the entry is // marked by a separate idle sentinel + dotted enter edge, not a double-paren shape. - expect(mermaid).toContain('["10"]'); + // Entry-point auto-tag (#86) appends `
main`. + expect(mermaid).toContain('["10
main"]'); expect(mermaid).toContain('idle([idle])'); expect(mermaid).toMatch(/idle -\. enter \.-> s\d+/); // Two intermediate states — square-bracket node shape with instruction-derived names. + // Non-entry instructions carry no auto-tag. expect(mermaid).toContain('["20"]'); expect(mermaid).toContain('["30"]'); @@ -484,11 +541,13 @@ describe('packages/machine/README.md', () => { // withSubroutine: wrapper outside the subgraph + callable-subtree shape. // Under #85 the hopper is dropped — the wrapper wraps walkToBlank::1 - // directly, not a bare named 'walkToBlank'. + // directly, not a bare named 'walkToBlank'. Under #86 the wrapper bears + // the top-level entry-point auto-tag `main`; the bare bears the + // subroutine-entry auto-tag `walkToBlank`. expect(subMermaid).toMatch(/subgraph w_\d+\["callable subtree of walkToBlank::1"\]/); - expect(subMermaid).toContain('[["walkToBlank::1(10~20)"]]'); // wrapper composite - expect(subMermaid).toContain('["walkToBlank::1"]'); // bare, inside subgraph - expect(subMermaid).not.toContain('["walkToBlank"]'); // hopper dropped + expect(subMermaid).toContain('[["walkToBlank::1(10~20)
main"]]'); // wrapper composite + expect(subMermaid).toContain('["walkToBlank::1
walkToBlank"]'); // bare, inside subgraph + expect(subMermaid).not.toMatch(/s\d+\["walkToBlank"\]/); // hopper dropped expect(subMermaid).toContain('["10~20"]'); // continuation expect(subMermaid).toMatch(/s\d+ == "call" ==> s\d+/); // wrapper → bare expect(subMermaid).toMatch(/w_\d+ -\. "return" \.-> s\d+/); diff --git a/packages/machine/src/classes/PostMachine.tags.spec.ts b/packages/machine/src/classes/PostMachine.tags.spec.ts new file mode 100644 index 0000000..f6c1778 --- /dev/null +++ b/packages/machine/src/classes/PostMachine.tags.spec.ts @@ -0,0 +1,158 @@ +import { describe, expect, test } from 'vitest'; +import { + PostMachine, + $tag, check, mark, right, stop, +} from '../index'; + +// Path-based tag registry + auto-tag policy (post-machine-js #86). +// +// API: +// pm.tag(path, ...tags) — add one or more tags to the state at path +// pm.untag(path, ...tags) — remove tags (no-op if not present) +// pm.tagsOf(path) — frozen snapshot of the state's tags +// pm.findByTag(tag) — all paths whose state carries that tag +// +// All four forward to the engine's `state.tag(...) / .untag(...) / .tags` +// API (engine #186). PostMachine does NOT maintain its own tag storage. +// +// Auto-tag policy (applied at construction): +// - The ENTRY POINT of the top-level program → tagged 'main' (e.g. path '1') +// - Each subroutine's entry state → tagged with the subroutine name (e.g. path 'alg::1' → 'alg') +// +// Only the entry-point state of each program/subroutine gets an auto-tag. +// Other top-level instructions and subroutine body instructions stay clean, +// keeping diagrams uncluttered while still anchoring the structural roles. + +describe('pm.tag / pm.untag / pm.tagsOf / pm.findByTag — registry API (#86)', () => { + test('pm.tag adds a tag to the state at path (string form)', () => { + const pm = new PostMachine({ 10: mark, 20: stop }); + pm.tag('10', 'hot'); + expect(pm.tagsOf('10')).toContain('hot'); + }); + + test('pm.tag adds a tag to the state at path (object form)', () => { + const pm = new PostMachine({ 10: mark, 20: stop }); + pm.tag({ instructionIndex: 10 }, 'hot'); + expect(pm.tagsOf({ instructionIndex: 10 })).toContain('hot'); + }); + + test('pm.tag is variadic — adds multiple tags in one call', () => { + const pm = new PostMachine({ 10: mark, 20: stop }); + pm.tag('10', 'hot', 'sampled', 'entry'); + expect(pm.tagsOf('10')).toEqual(expect.arrayContaining(['hot', 'sampled', 'entry'])); + }); + + test('pm.untag removes a tag (subsequent tagsOf no longer contains it)', () => { + const pm = new PostMachine({ 10: mark, 20: stop }); + pm.tag('10', 'hot'); + pm.untag('10', 'hot'); + expect(pm.tagsOf('10')).not.toContain('hot'); + }); + + test('pm.untag is a no-op for tags that were never added', () => { + const pm = new PostMachine({ 10: mark, 20: stop }); + expect(() => pm.untag('10', 'never-added')).not.toThrow(); + }); + + test('pm.tagsOf returns a frozen array', () => { + const pm = new PostMachine({ 10: mark, 20: stop }); + pm.tag('10', 'hot'); + const tags = pm.tagsOf('10'); + expect(Object.isFrozen(tags)).toBe(true); + }); + + test('pm.findByTag returns all paths carrying that tag', () => { + const pm = new PostMachine({ 10: mark, 20: mark, 30: stop }); + pm.tag('10', 'hot'); + pm.tag('20', 'hot'); + const paths = pm.findByTag('hot'); + expect(paths).toHaveLength(2); + expect(paths.some((p) => p.instructionIndex === 10)).toBe(true); + expect(paths.some((p) => p.instructionIndex === 20)).toBe(true); + }); + + test('pm.findByTag returns [] for an unknown tag', () => { + const pm = new PostMachine({ 10: mark, 20: stop }); + expect(pm.findByTag('never-added')).toEqual([]); + }); + + test('pm.tag throws on an unknown path', () => { + const pm = new PostMachine({ 10: mark, 20: stop }); + expect(() => pm.tag('99', 'hot')).toThrow(/does not resolve/); + }); + + test('user tags and inline $tag(...) decorator compose on the same state', () => { + const pm = new PostMachine({ + 10: $tag('inline', mark), + 20: stop, + }); + pm.tag('10', 'post-hoc'); + expect(pm.tagsOf('10')).toEqual(expect.arrayContaining(['inline', 'post-hoc'])); + }); +}); + +describe('auto-tag policy at construction (#86)', () => { + test('the top-level entry point is tagged "main"', () => { + const pm = new PostMachine({ 10: mark, 20: mark, 30: stop }); + expect(pm.tagsOf('10')).toContain('main'); + }); + + test('non-entry top-level instructions are NOT auto-tagged', () => { + const pm = new PostMachine({ 10: mark, 20: mark, 30: stop }); + expect(pm.tagsOf('20')).not.toContain('main'); + }); + + test('halt-resolving entry points are NOT auto-tagged', () => { + // `stop` resolves to the engine's haltState singleton, which is globally + // shared. Tagging it would leak the tag across all PostMachine instances + // — so auto-tag skips halt-resolving paths even when they're entries. + const pm = new PostMachine({ 10: stop }); + expect(pm.tagsOf('10')).not.toContain('main'); + }); + + test('subroutine entry state is tagged with the subroutine name', () => { + const pm = new PostMachine({ + 10: check(20, 30), + 20: right(10), + 30: stop, + rightToBlank: { 1: mark, 2: stop }, + }); + expect(pm.tagsOf('rightToBlank::1')).toContain('rightToBlank'); + }); + + test('subroutine body states (non-entry) are NOT auto-tagged with the sub name', () => { + const pm = new PostMachine({ + 10: stop, + sub: { 1: mark, 2: stop }, + }); + // Entry (instruction 1) carries the tag; subsequent body instructions do not. + expect(pm.tagsOf('sub::1')).toContain('sub'); + expect(pm.tagsOf('sub::2')).not.toContain('sub'); + }); + + test('subroutine entry is NOT tagged "main" (it belongs to the subroutine, not the top-level program)', () => { + const pm = new PostMachine({ + 10: stop, + sub: { 1: mark, 2: stop }, + }); + expect(pm.tagsOf('sub::1')).not.toContain('main'); + }); + + test('findByTag("main") returns just the top-level entry-point path', () => { + const pm = new PostMachine({ + 10: mark, + 20: mark, + 30: stop, + sub: { 1: mark, 2: stop }, + }); + const paths = pm.findByTag('main'); + expect(paths).toHaveLength(1); + expect(paths[0].instructionIndex).toBe(10); + }); + + test('user pm.tag composes with auto-tags — both coexist on the entry state', () => { + const pm = new PostMachine({ 10: mark, 20: stop }); + pm.tag('10', 'hot'); + expect(pm.tagsOf('10')).toEqual(expect.arrayContaining(['main', 'hot'])); + }); +}); diff --git a/packages/machine/src/classes/PostMachine.ts b/packages/machine/src/classes/PostMachine.ts index e274ec5..817664c 100644 --- a/packages/machine/src/classes/PostMachine.ts +++ b/packages/machine/src/classes/PostMachine.ts @@ -19,7 +19,7 @@ import { } from '../consts'; import type { CommandContext, Instructions } from '../commands'; import { - call, check, erase, left, mark, noop, right, stop, + $tag, call, check, erase, left, mark, noop, right, stop, } from '../commands'; import { instructionIndexValidator, subroutineNameValidator, validateSymbolPair } from '../validators'; import { installStateLockdown, withLockdownEscape } from '../lockdown'; @@ -450,6 +450,12 @@ export class PostMachine extends TuringMachine { .every((command) => commandsSet.has(command as CommandFn)); if (!areInstructionsInGroupValid) { + if (instruction.includes($tag as never)) { + throw new Error( + 'bare `$tag` decorator in a group — `$tag` must be invoked, ' + + 'e.g. `[$tag(\'hot\', mark), right]`', + ); + } throw new Error('invalid command in the group'); } @@ -485,6 +491,11 @@ export class PostMachine extends TuringMachine { nextState, }, }, continuationName))); + } else if (instruction === $tag) { + throw new Error( + 'bare `$tag` decorator passed as an instruction — `$tag` must be ' + + 'invoked, e.g. `10: $tag(\'hot\', mark)`', + ); } else { throw new Error('invalid instruction'); } @@ -505,6 +516,23 @@ export class PostMachine extends TuringMachine { }; this.#recordPath(state, path); + + // Auto-tag policy (#86). Only the ENTRY POINT of each program / + // subroutine gets an auto-tag — `1` for main, `alg::1` for subroutine + // `alg` — to keep diagrams uncluttered while still anchoring the + // structural roles. Group inner states and halt-resolving paths are + // skipped (halt is a globally-shared singleton and can't be safely + // tagged). + if ( + groupOuterInstructionIndex === undefined + && !state.isHalt + && Number(instructionIndexStr) === list[0] + ) { + const tagName = scope.length === 0 + ? 'main' + : scope[scope.length - 1]; + state.tag(tagName); + } }); return references[instructionIndexList[0]].ref; @@ -563,6 +591,31 @@ export class PostMachine extends TuringMachine { return this.#stateToCandidatePaths.get(state)!; } + tag(target: Path | string, ...tags: string[]): void { + const { state } = this.#resolveToState(target); + state.tag(...tags); + } + + untag(target: Path | string, ...tags: string[]): void { + const { state } = this.#resolveToState(target); + state.untag(...tags); + } + + tagsOf(target: Path | string): readonly string[] { + const { state } = this.#resolveToState(target); + return state.tags; + } + + findByTag(tag: string): Path[] { + const results: Path[] = []; + for (const [state, paths] of this.#stateToCandidatePaths) { + if (state.tags.includes(tag)) { + results.push(...paths); + } + } + return results; + } + setBreakpoint(target: BreakpointTarget, filter: BreakpointFilter): void { validateBreakpointFilter(filter); const resolved = this.#resolveBreakpointTarget(target); diff --git a/packages/machine/src/commands.spec.ts b/packages/machine/src/commands.spec.ts new file mode 100644 index 0000000..40179e3 --- /dev/null +++ b/packages/machine/src/commands.spec.ts @@ -0,0 +1,124 @@ +import {describe, expect, test} from 'vitest'; + +import {PostMachine, $tag, mark, noop, right, stop} from './index'; +import {State, toMermaid} from '@turing-machine-js/machine'; + +// Tests for the `$tag('label', command)` inline decorator (#86). +// +// The `$tag` decorator wraps a command producer (or bare constructor) with +// one or more tags. Tags get applied to the resulting State via the engine's +// `state.tag(...)` API (engine #186). Composes with indexed commands +// (`$tag('hot', check(20, 30))`), rejects groups (`$tag('foo', [mark, right])` +// throws — tag the inner commands individually instead). The `$` prefix +// flags it as a decorator (not a primitive command) at the call site. + +describe('$tag — inline tag decorator (#86)', () => { + test('tags a bare command (constructor form)', () => { + const machine = new PostMachine({ + 10: $tag('hot', mark), + 20: stop, + }); + + const state = machine.stateAt({instructionIndex: 10}); + + expect(state).toBeDefined(); + expect(state!.tags).toContain('hot'); + }); + + test('tags a command-with-explicit-jump (producer form)', () => { + const machine = new PostMachine({ + 10: $tag('hot', mark(30)), + 20: noop, + 30: stop, + }); + + const state = machine.stateAt({instructionIndex: 10}); + + expect(state).toBeDefined(); + expect(state!.tags).toContain('hot'); + }); + + test('variadic — multiple tags before the command', () => { + const machine = new PostMachine({ + 10: $tag('hot', 'sampled', 'entry', mark), + 20: stop, + }); + + const state = machine.stateAt({instructionIndex: 10}); + + expect(state!.tags).toEqual(expect.arrayContaining(['hot', 'sampled', 'entry'])); + }); + + test('rejects groups — `$tag(\'label\', [mark, right])` throws at construction', () => { + expect(() => new PostMachine({ + 10: $tag('hot', [mark, right] as never), + 20: stop, + })).toThrow(/group/i); + }); + + test('rejects calls with no tags', () => { + expect(() => new PostMachine({ + 10: ($tag as unknown as (...args: unknown[]) => unknown)(mark) as never, + 20: stop, + })).toThrow(/at least one tag/i); + }); + + test('rejects non-string tags', () => { + expect(() => new PostMachine({ + 10: ($tag as unknown as (...args: unknown[]) => unknown)(42, mark) as never, + 20: stop, + })).toThrow(/string/i); + }); + + test('rejects non-function final argument', () => { + // Final arg must be a command (function) or a group (array — handled by + // the group-rejection branch above). Anything else falls through to + // the "must be a command" throw. + expect(() => new PostMachine({ + 10: ($tag as unknown as (...args: unknown[]) => unknown)('hot', 42) as never, + 20: stop, + })).toThrow(/must be a command/); + }); + + test('rejects bare `$tag` at top level — must be invoked', () => { + expect(() => new PostMachine({ + 10: $tag as never, + 20: stop, + })).toThrow(/\$tag/); + }); + + test('rejects bare `$tag` inside a group — must be invoked', () => { + expect(() => new PostMachine({ + 10: [$tag as never], + 20: stop, + })).toThrow(/\$tag/); + }); + + test('tags appear in toMermaid output (engine #186 emit)', () => { + const machine = new PostMachine({ + 10: $tag('hot', mark), + 20: stop, + }); + + const mermaid = toMermaid(State.toGraph(machine.initialState, machine.tapeBlock)); + + // Engine #186 emits tags inline in node labels via `
` and as + // classDef/class lines for color grouping. Both should appear. + expect(mermaid).toContain('
'); + expect(mermaid).toContain('hot'); + expect(mermaid).toMatch(/classDef tag_hot /); + expect(mermaid).toMatch(/class s\d+ tag_hot/); + }); + + test('round-trip: machine reaches the tagged state and runs to completion', async () => { + const machine = new PostMachine({ + 10: $tag('hot', mark), + 20: stop, + }); + + // The tag doesn't affect runtime — the machine still halts normally. + await machine.run(); + + expect(machine.tape.symbols[0]).toBe('*'); + }); +}); diff --git a/packages/machine/src/commands.ts b/packages/machine/src/commands.ts index d21b33d..c2f4f91 100644 --- a/packages/machine/src/commands.ts +++ b/packages/machine/src/commands.ts @@ -354,3 +354,83 @@ commandsSet.add(mark as CommandFn); commandsSet.add(noop as CommandFn); commandsSet.add(right as CommandFn); commandsSet.add(stop as CommandFn); + +/** + * Inline `$tag` decorator (#86). Wraps a command (bare constructor like + * `mark` or already-bound producer like `mark(20)` / `call('foo')`) with + * one or more tags; tags are applied to the resulting State via the + * engine's `state.tag(...)` API (engine #186). The wrapped command's + * runtime behavior is unchanged — `$tag` is a decorator, not a primitive. + * The `$` prefix flags it as a decorator at the call site. + * + * Usage: + * - Wrap a bare command: `$tag('hot', mark)` + * - Wrap an indexed command:`$tag('loop-head', check(20, 40))` + * - Variadic tags: `$tag('hot', 'sampled', 'entry', mark)` + * - Compose with call: `$tag('subroutine-entry', call('foo'))` + * + * Does NOT compose with groups — `$tag('foo', [mark, right])` throws at + * construction. Tag each member individually instead: + * `10: [$tag('lift', mark), $tag('descend', right)]`. + */ +export function $tag(...args: unknown[]): CommandStateProducer { + if (args.length < 2) { + throw new Error('$tag() requires at least one tag and a command'); + } + + const tags = args.slice(0, -1); + const wrapped = args[args.length - 1]; + + for (const t of tags) { + if (typeof t !== 'string') { + throw new Error(`$tag() tags must be strings, got ${typeof t}: ${String(t)}`); + } + } + + if (Array.isArray(wrapped)) { + throw new Error( + '$tag() cannot wrap a group — groups and tags are incompatible. ' + + 'Tag each group member individually instead: `[$tag("lift", mark), $tag("descend", right)]`.', + ); + } + + if (typeof wrapped !== 'function') { + throw new Error( + `$tag() final argument must be a command, got ${typeof wrapped}`, + ); + } + + const stringTags = tags as string[]; + const wrappedFn = wrapped as CommandStateProducer | CommandConstructor; + + // Dispatch: if `wrappedFn` is a bare command constructor (mark/right/etc.), + // call it with `defaultNextInstructionIndex` to get the bound producer — + // same conversion PostMachine does at instruction-build time (see the + // `case erase: case left: …` switch in PostMachine.#buildInitialState). + // Producers already in `commandsSet` (mark(20), call('foo'), check(20, 30), + // $tag(...)) are invoked directly with context. `call`/`check` are excluded + // — bare references throw at PostMachine's dispatch, so they can't reach + // here without first being bound by the caller. + const isBareConstructor = wrappedFn === erase + || wrappedFn === left + || wrappedFn === mark + || wrappedFn === noop + || wrappedFn === right + || wrappedFn === stop; + + const taggedProducer: CommandStateProducer = (context) => { + const producer: CommandStateProducer = isBareConstructor + ? (wrappedFn as CommandConstructor)(defaultNextInstructionIndex) + : wrappedFn as CommandStateProducer; + + const state = producer(context); + + state.tag(...stringTags); + + return state; + }; + + commandsSet.add(taggedProducer as CommandFn); + + return taggedProducer; +} diff --git a/packages/machine/src/index.ts b/packages/machine/src/index.ts index d962608..c899f83 100644 --- a/packages/machine/src/index.ts +++ b/packages/machine/src/index.ts @@ -29,7 +29,7 @@ export type { } from '@turing-machine-js/machine'; export { alphabet, blankSymbol, markSymbol } from './consts'; export { - call, check, erase, left, mark, noop, right, stop, + $tag, call, check, erase, left, mark, noop, right, stop, } from './commands'; export type { Instructions, From b9545bad5fa23267569d0181d063e31b14490581 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Thu, 21 May 2026 23:03:13 +0300 Subject: [PATCH 10/34] test: pin per-member `$tag` inside a group (#86) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verifies the README's recommended workaround for the group-rejection case — wrap each group member individually with `$tag(...)`. Pins tagsOf for inner paths, the outer wrapper carrying only auto-tag `'main'`, and findByTag returning the group-inner path object. --- packages/machine/src/commands.spec.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/machine/src/commands.spec.ts b/packages/machine/src/commands.spec.ts index 40179e3..c487a8c 100644 --- a/packages/machine/src/commands.spec.ts +++ b/packages/machine/src/commands.spec.ts @@ -56,6 +56,33 @@ describe('$tag — inline tag decorator (#86)', () => { })).toThrow(/group/i); }); + test('per-member `$tag` inside a group works — tags apply to each inner state', () => { + // The recommended workaround for "tag inside a group" — wrap each + // member individually instead of wrapping the group as a whole. + const machine = new PostMachine({ + 10: [$tag('lift', mark), $tag('descend', right)], + 20: stop, + }); + + // Inner group instructions carry their per-member tags. + expect(machine.tagsOf({ instructionIndex: 10, groupInstructionIndex: 1 })) + .toEqual(['lift']); + expect(machine.tagsOf({ instructionIndex: 10, groupInstructionIndex: 2 })) + .toEqual(['descend']); + + // The outer group wrapper at path '10' is the top-level entry — it + // carries only the auto-tag 'main', not the inner-member tags. + expect(machine.tagsOf('10')).toEqual(['main']); + + // findByTag returns the group-inner path for each member tag. + expect(machine.findByTag('lift')).toEqual([ + { instructionIndex: 10, groupInstructionIndex: 1 }, + ]); + expect(machine.findByTag('descend')).toEqual([ + { instructionIndex: 10, groupInstructionIndex: 2 }, + ]); + }); + test('rejects calls with no tags', () => { expect(() => new PostMachine({ 10: ($tag as unknown as (...args: unknown[]) => unknown)(mark) as never, From f8838334e1392644a0ef9e2c12b9c2dead565725 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Thu, 21 May 2026 23:06:57 +0300 Subject: [PATCH 11/34] docs: separate example + diagram for both `$tag` forms (#86) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splits the Inline `$tag` decorator section into two named subsections, each with code AND a Mermaid diagram: - Wrapping a single command — the existing example, now with a diagram that illustrates tag composition (s1 carries both `tag_hot` from the inline `$tag` and `tag_main` from the auto-tag, rendered comma- separated in the label with two `class` directives). - Per-member in a group — new example showing the recommended workaround for the group-rejection case, with the diagram showing per-member tags landing inside the group's callable subtree. Both diagrams were probed from actual engine output, not handwritten. --- packages/machine/README.md | 76 +++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/packages/machine/README.md b/packages/machine/README.md index 57d6de5..e543ee6 100644 --- a/packages/machine/README.md +++ b/packages/machine/README.md @@ -496,11 +496,13 @@ Three ways to apply tags: the **inline `$tag` decorator** at construction, the * `$tag(...tags, command)` wraps a command with one or more tags. The tags apply to the resulting State; no extra graph node is created. The leading `$` flags it visually as a decorator (not a primitive command). +**Wrapping a single command.** The common case — one tag per state, applied per instruction: + ```javascript import { PostMachine, $tag, check, mark, right, stop } from '@post-machine-js/machine'; const machine = new PostMachine({ - 10: $tag('hot', check(20, 30)), // tag a single state + 10: $tag('hot', check(20, 30)), // tag a single state 20: $tag('loop-body', 'sampled', right(10)), // variadic — many tags at once 30: mark, 40: stop, @@ -510,7 +512,77 @@ console.log(machine.tagsOf({ instructionIndex: 10 })); // ['hot', 'main'] — inline 'hot' applied at producer time, then 'main' auto-tag ``` -`$tag` rejects groups — `$tag('foo', [mark, right])` throws at construction. Tag each member individually: `[$tag('lift', mark), $tag('descend', right)]`. Passing bare `$tag` (without invoking it) as an instruction also throws with a helpful message. +```mermaid +flowchart TD +%% alphabets: [[" ","*"]] + s0(((halt))) + s1["10
hot, main"] + s2["20
loop-body, sampled"] + s3["30"] + idle([idle]) + idle -. enter .-> s1 + s1 -- "['*'] → [K]/[S]" --> s2 + s1 -- "[B] → [K]/[S]" --> s3 + s2 -- "[*] → [K]/[R]" --> s1 + s3 -- "[*] → ['*']/[S]" --> s0 + classDef tag_hot fill:#dbeafe,stroke:#1e40af + classDef tag_loop-body fill:#fee2e2,stroke:#991b1b + classDef tag_main fill:#dbeafe,stroke:#1e40af + classDef tag_sampled fill:#ede9fe,stroke:#5b21b6 + class s1 tag_hot + class s2 tag_loop-body + class s1 tag_main + class s2 tag_sampled +``` + +`s1` carries two tags (`hot` from `$tag` + `main` from auto-tag) — the engine emits them comma-separated in the label and applies BOTH `classDef` lines via two `class s1 …` directives. Tag composition is additive. + +**Per-member in a group.** `$tag` rejects wrapping a group as a whole (`$tag('foo', [mark, right])` throws). Tag each member individually instead — the inner tags land on the per-member states inside the group's callable subtree: + +```javascript +import { PostMachine, $tag, mark, right, stop } from '@post-machine-js/machine'; + +const machine = new PostMachine({ + 10: [$tag('lift', mark), $tag('descend', right)], + 20: stop, +}); + +console.log(machine.tagsOf({ instructionIndex: 10, groupInstructionIndex: 1 })); +// ['lift'] +console.log(machine.tagsOf('10')); +// ['main'] — the outer wrapper at path '10' carries the auto-tag for the top-level entry +``` + +```mermaid +flowchart TD +%% alphabets: [[" ","*"]] + s0(((halt))) + s6["10~20"] + s7[["10.1(10~20)
main"]] + idle([idle]) + subgraph w_4["callable subtree of 10.1"] + s4["10.1
lift"] + s5["10.2
descend"] + c4(((halt))) + end + idle -. enter .-> s7 + s7 == "call" ==> s4 + w_4 -. "return" .-> s7 + s7 --> s6 + s4 -- "[*] → ['*']/[S]" --> s5 + s5 -- "[*] → [K]/[R]" --> c4 + s6 -- "[*] → [K]/[S]" --> s0 + classDef tag_descend fill:#fef3c7,stroke:#92400e + classDef tag_lift fill:#fee2e2,stroke:#991b1b + classDef tag_main fill:#dbeafe,stroke:#1e40af + class s5 tag_descend + class s4 tag_lift + class s7 tag_main +``` + +The group expands into a `withOverriddenHaltState` chain wrapped in a callable subtree (same shape as a subroutine call). The wrapper `s7` is the top-level entry (auto-tagged `main`); the inner states `s4` (`lift`) and `s5` (`descend`) carry their per-member tags inside the subgraph. The group's outer path `'10'` resolves to the wrapper; group-inner paths use the `{ instructionIndex: 10, groupInstructionIndex: N }` shape. + +Passing bare `$tag` (without invoking it) as an instruction or as a group member also throws — with a message pointing at the correct form. ### Post-construction registry From 50bc29db3f8504afe528e97c5da68c1ae46b4614 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Sat, 23 May 2026 13:10:00 +0300 Subject: [PATCH 12/34] =?UTF-8?q?chore:=20widen=20engine=20peer=20dep=20`^?= =?UTF-8?q?7.0.0-alpha.3`=20=E2=86=92=20`^7.0.0-alpha.4`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Engine `@turing-machine-js/machine` shipped 7.0.0-alpha.4 today with two upstream bug fixes that post-machine-js inherits transparently: - toMermaid HTML-entity-escapes user content in labels — fixes the edge-label parse error when an alphabet contains literal `"`, `<`, `>`, etc. (engine #194). Post-side `toMermaid(machine.initialState, machine.tapeBlock)` benefits immediately without code changes. - runStepByStep halt stack scoped to the call rather than the TuringMachine instance — fixes a memory leak / ghost-iteration when the same machine is reused across consecutive runStepByStep calls (engine #196). Post-side runStepByStep inherits the fix. Engine alpha.4 also adds State.collectStates (engine #195) and extracts toGraph/fromGraph to a sibling module (engine #180), but neither affects post-machine-js — the public State.toGraph / State.fromGraph statics still exist as delegates, and collectStates is an additive helper post doesn't currently surface to consumers. Updates: - packages/machine/package.json — peer dep AND devDep widened. - package.json (workspace root) — devDep widened so the locally- installed engine matches what the published peer dep requires. - package-lock.json — refreshed via `npm install`. - packages/machine/CHANGELOG.md — alpha.4 entry updated: - Date 2026-05-21 → 2026-05-23 (publish moves to today). - Engine peer-dep line spells out the from→to range and lists engine alpha.4's transparent fixes (#194 / #196). Tests: 315/315 still pass against engine 7.0.0-alpha.4. --- package-lock.json | 12 ++++++------ package.json | 2 +- packages/machine/CHANGELOG.md | 4 ++-- packages/machine/package.json | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 58a8cf9..4287ba6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "packages/*" ], "dependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.3" + "@turing-machine-js/machine": "^7.0.0-alpha.4" }, "devDependencies": { "@tsconfig/recommended": "^1.0.13", @@ -2740,9 +2740,9 @@ } }, "node_modules/@turing-machine-js/machine": { - "version": "7.0.0-alpha.3", - "resolved": "https://registry.npmjs.org/@turing-machine-js/machine/-/machine-7.0.0-alpha.3.tgz", - "integrity": "sha512-+5FW2WRikdcXM2o7qKVnWeMCFuOMIAySV01ATZoFiYlz9o797+MFxVjpSmJ3KQNqTijeXutIPlVdQl2a0VHhXA==", + "version": "7.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/@turing-machine-js/machine/-/machine-7.0.0-alpha.4.tgz", + "integrity": "sha512-zgSXyI81koOMv4RAJhZOnCHZx5od1/j6i6dKM1+++1lsiZIEe6Ou3fxckKp6o9QjihJXfIE8QzzXX1LiOERURQ==", "license": "GPL-3.0-or-later", "engines": { "npm": ">=7.0.0" @@ -10444,13 +10444,13 @@ "version": "7.0.0-alpha.4", "license": "GPL-3.0-or-later", "devDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.3" + "@turing-machine-js/machine": "^7.0.0-alpha.4" }, "engines": { "npm": ">=7.0.0" }, "peerDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.3" + "@turing-machine-js/machine": "^7.0.0-alpha.4" } } } diff --git a/package.json b/package.json index bac7b76..119a0fd 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,6 @@ "vitest": "^4.1.5" }, "dependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.3" + "@turing-machine-js/machine": "^7.0.0-alpha.4" } } diff --git a/packages/machine/CHANGELOG.md b/packages/machine/CHANGELOG.md index b15279b..4cab338 100644 --- a/packages/machine/CHANGELOG.md +++ b/packages/machine/CHANGELOG.md @@ -4,9 +4,9 @@ All notable changes to this project will be documented in this file. 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). -## [7.0.0-alpha.4] - 2026-05-21 +## [7.0.0-alpha.4] - 2026-05-23 -Fourth v7 pre-release. Adds user-supplied tags on states ([#86](https://github.com/mellonis/post-machine-js/issues/86)) — both an inline decorator at construction and a path-based registry post-construction — plus an auto-tag policy that marks each program's/subroutine's entry state. Engine peer-dep widened `^7.0.0-alpha.3` (the new `state.tag(...)` API was added by engine alpha.3). Published to npm under the `next` dist-tag: `npm install @post-machine-js/machine@next`. +Fourth v7 pre-release. Adds user-supplied tags on states ([#86](https://github.com/mellonis/post-machine-js/issues/86)) — both an inline decorator at construction and a path-based registry post-construction — plus an auto-tag policy that marks each program's/subroutine's entry state. Engine peer-dep widened `^7.0.0-alpha.2` → `^7.0.0-alpha.4` — alpha.3 added the `state.tag(...)` API this release builds on, and alpha.4 ships two upstream bug fixes that post inherits transparently (`toMermaid` HTML-entity-escapes user content in labels — fixes alphabet-with-`"` parse errors, [engine #194](https://github.com/mellonis/turing-machine-js/issues/194); `runStepByStep` halt stack scoped to the call, fixes a memory leak / ghost-iteration when the same machine is reused across calls, [engine #196](https://github.com/mellonis/turing-machine-js/issues/196)). Published to npm under the `next` dist-tag: `npm install @post-machine-js/machine@next`. **Pre-release — the API surface may still shift before stable v7.0.0.** Pin to a specific alpha for reproducibility: `@post-machine-js/machine@7.0.0-alpha.4`. diff --git a/packages/machine/package.json b/packages/machine/package.json index 3b411d8..c2011d0 100644 --- a/packages/machine/package.json +++ b/packages/machine/package.json @@ -28,10 +28,10 @@ "machine" ], "peerDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.3" + "@turing-machine-js/machine": "^7.0.0-alpha.4" }, "devDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.3" + "@turing-machine-js/machine": "^7.0.0-alpha.4" }, "main": "dist/index.cjs", "module": "dist/index.mjs", From d80abf6174d8fbf259952f1835a7971029a38129 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Mon, 25 May 2026 11:31:17 +0300 Subject: [PATCH 13/34] feat: drop module-load haltState lockdown (engine #207) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Engine 7.0.0-alpha.5 (turing-machine-js#207) collapses `haltState.debug` to a boolean and rejects object-shaped writes at the engine setter. The prior `installHaltLockdown` was funneling the (now-defunct) per-side DebugConfig through `withLockdownEscape`; with the boolean shape there's nothing to mediate, and the "per-PostMachine routing" benefit was syntactic only since haltState is a process-global singleton. - Drop `installHaltLockdown` from `lockdown.ts` and its module-load call in `src/index.ts`. State-side lockdown is unaffected. - Rewrite `PostMachine.#refreshHaltDebug` to write the boolean directly (no escape needed); the per-BP filter shape stays in `#breakpoints` for arrival-path filtering in the `onPause` wrapper. - Tests: `lockdown.spec.ts` + `breakpoints.spec.ts` swap the lockdown-error assertions for the engine's boolean-only error. `PostMachine.spec.ts` adds a `stripMatchedTransition` helper to keep cross-machine onStep equality semantic — engine #205 leaks process-global stateIds into `MachineState.matchedTransition.id`. - README + CLAUDE.md: rewrite Subtlety 6 + lockdown.ts blurb; README's halt-BP block documents the relaxed direct-write surface and the registry-bypass caveat. - Refresh lockfile to engine 7.0.0-alpha.5 (the existing `^7.0.0-alpha.4` range already accepts it via semver-prerelease). Peer-dep widen + CHANGELOG entry deferred to the v7 release PR per the v7 trajectory checklist. --- CLAUDE.md | 6 ++-- package-lock.json | 6 ++-- packages/machine/README.md | 18 ++++++---- packages/machine/src/breakpoints.spec.ts | 15 ++++++-- .../machine/src/classes/PostMachine.spec.ts | 30 +++++++++++++--- packages/machine/src/classes/PostMachine.ts | 20 +++++++---- packages/machine/src/index.ts | 22 ++++++++---- packages/machine/src/lockdown.spec.ts | 35 ++++++++----------- packages/machine/src/lockdown.ts | 23 +----------- 9 files changed, 103 insertions(+), 72 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b07a7ef..33a0fee 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,7 +42,7 @@ Key files: - `packages/machine/src/classes/PostMachine.ts` — the runtime; orchestrates the producers, manages references and subroutine support (`subroutineInitialStates`, `subroutineNameValidator`), owns the breakpoint registry (`#breakpoints`, `setBreakpoint`/`clearBreakpoint`/`clearBreakpoints`/`listBreakpoints`), and installs the per-State debug-config lockdown at the end of construction. - `packages/machine/src/path.ts` — `Path` type, `parsePath`/`formatPath`/`comparePathsCanonically`, the path-string validator. The canonical path string is the lookup key in `PostMachine.#pathToState` and the registry's path comparisons. - `packages/machine/src/breakpoints.ts` — `BreakpointFilter` / `BreakpointTarget` / `Breakpoint` types, `mergeBreakpointFilters` (filter union for shared States), `validateBreakpointFilter`. -- `packages/machine/src/lockdown.ts` — `installStateLockdown(state, onUserWrite)` + `installHaltLockdown(haltState)` + `withLockdownEscape(fn)`. Installs `Object.defineProperty` accessors on State and haltState that delegate to the engine's prototype `debug` getter/setter inside the escape (used by PostMachine's `#refreshStateDebug` / `#refreshHaltDebug`) and route user writes to a redirect handler or throw (haltState). +- `packages/machine/src/lockdown.ts` — `installStateLockdown(state, onUserWrite)` + `withLockdownEscape(fn)`. Installs `Object.defineProperty` accessors on per-PostMachine States that delegate to the engine's prototype `debug` getter/setter inside the escape (used by PostMachine's `#refreshStateDebug`) and route user writes to a redirect handler. The prior `installHaltLockdown(haltState)` + module-load install in `src/index.ts` were dropped alongside engine [#207](https://github.com/mellonis/turing-machine-js/issues/207): `haltState.debug` collapsed to a boolean, the per-side `DebugConfig` shape the lockdown was funneling no longer exists, and the "per-PostMachine routing" benefit was syntactic only (haltState is a process-global singleton). Direct `haltState.debug = boolean` writes now go straight to the engine setter; `pm.setBreakpoint(haltState, …)` still works for registry-aware halt pauses. - `packages/machine/src/validators.ts` — input validators for instruction indices and subroutine names. ### Subtleties worth knowing @@ -57,7 +57,7 @@ Key files: 5. **Group commands**: some producers throw if called from inside a "group" (`calledFromGroup` flag in `CommandContext`). This relates to PostMachine's grouping feature where multiple commands can be bundled into one logical instruction. Two distinct rules: `check`/`call`/`stop` reject group context unconditionally (regardless of form); the unary commands (`mark`/`erase`/`left`/`right`/`noop`) only reject the *indexed* form (`mark(20)` etc.) inside a group, because the explicit jump conflicts with the group's sequential fall-through semantics. -6. **Per-State debug lockdown**: at the end of construction, `PostMachine` iterates `#stateToCandidatePaths.keys()` and calls `installStateLockdown(state, onUserWrite)` on every non-halt State. The installer replaces the engine's prototype `debug` accessor with an instance-level `Object.defineProperty`. Internal writes (from `#refreshStateDebug` / `#refreshHaltDebug`) run inside `withLockdownEscape` and delegate to the engine's prototype setter (which preserves the engine's `DebugConfig` wrapping + validation + shared-debugRef propagation across `withOverriddenHaltState` wrappers). User writes outside the escape go through the redirect handler: un-shared State → `setBreakpoint`/`clearBreakpoint`; shared State → throw with candidate-path list. `haltState` is locked module-globally in `src/index.ts` at module load — direct `haltState.debug = X` throws, no PostMachine context for a redirect. `state.isHalt` checks at the install site skip the engine's halt singleton (it has its own module-global lockdown). The lockdown does **not** use `Proxy` — that was tried during the v6.1.0 design phase and abandoned because engine utilities like `State.toGraph(arg, …)` read TS-downleveled private fields directly off the argument via `__classPrivateFieldGet`, which fails on a Proxy. +6. **Per-State debug lockdown**: at the end of construction, `PostMachine` iterates `#stateToCandidatePaths.keys()` and calls `installStateLockdown(state, onUserWrite)` on every non-halt State. The installer replaces the engine's prototype `debug` accessor with an instance-level `Object.defineProperty`. Internal writes (from `#refreshStateDebug`) run inside `withLockdownEscape` and delegate to the engine's prototype setter (which preserves the engine's `DebugConfig` wrapping + validation + shared-debugRef propagation across `withOverriddenHaltState` wrappers). User writes outside the escape go through the redirect handler: un-shared State → `setBreakpoint`/`clearBreakpoint`; shared State → throw with candidate-path list. **haltState is NO LONGER locked** (dropped alongside engine [#207](https://github.com/mellonis/turing-machine-js/issues/207)) — direct `haltState.debug = boolean` writes from any context go straight to the engine setter. `#refreshHaltDebug` writes the boolean directly (no escape needed). `state.isHalt` checks at the install site still skip haltState (it's a singleton, not a per-PostMachine State). The lockdown does **not** use `Proxy` — that was tried during the v6.1.0 design phase and abandoned because engine utilities like `State.toGraph(arg, …)` read TS-downleveled private fields directly off the argument via `__classPrivateFieldGet`, which fails on a Proxy. ## Doc examples must be tested @@ -94,7 +94,7 @@ Previous v5/v6 engine changes still apply unchanged on v7: - **`pm.run()` stays async.** Engine v4 introduced `Promise` return; v5/v6 didn't change that. Callers must still `await` it. - **`runStepByStep` stays unchanged.** Still a synchronous `Generator` (engine v6 narrowed the parent's generator return type back to `Generator`, matching post's existing override). - **`onPause` on `pm.run()`** (stable as of v6.1.0; was experimental `__onPause` in v6.0.0). Accepts `onPause?: (s: MachineState) => void | Promise` and forwards as the upstream `onPause` hook. The wrapper applies arrival-aware registry filtering: pauses fire only when `m.arrivalPath` (or a halt-arrival) matches a registered breakpoint. -- **Debugger primitives ARE wrapped.** `state.debug` and `haltState.debug` go through the per-State / module-global lockdown (see Subtlety 6 above). Construction-time writes funnel through `pm.setBreakpoint(target, filter)` / `pm.clearBreakpoint(target)` / `pm.clearBreakpoints()`; direct `state.debug = X` on an un-shared State auto-redirects to `setBreakpoint`. The engine-level concepts (filter shapes, `before → step → after` lifecycle, `haltState.debug.after` rejection) still apply — the lockdown is a thin layer in front, not a reimplementation. +- **Debugger primitives ARE wrapped for non-halt States.** `state.debug` on per-PostMachine States goes through the per-State lockdown (see Subtlety 6 above). Construction-time writes funnel through `pm.setBreakpoint(target, filter)` / `pm.clearBreakpoint(target)` / `pm.clearBreakpoints()`; direct `state.debug = X` on an un-shared State auto-redirects to `setBreakpoint`. The engine-level concepts (filter shapes, `before → step → after` lifecycle) still apply — the lockdown is a thin layer in front, not a reimplementation. **`haltState.debug` is no longer wrapped** (engine [#207](https://github.com/mellonis/turing-machine-js/issues/207) collapsed it to a boolean and post-machine-js dropped the module-load halt-lockdown). Direct `haltState.debug = boolean` writes work; `pm.setBreakpoint(haltState, …)` still works for registry-aware halt pauses. Halt-imminent pause fires on the AFTER side of the iter whose transition leads to halt (per #207). - **`run({ debug: boolean })` master switch (engine v5/#106).** Reachable via the upstream API; not wrapped at the PostMachine level. Useful in tests to suppress all `onPause` dispatches without unsetting `state.debug` assignments. - **v3 utility additions persist.** `State.toGraph`, `State.fromGraph`, `State.inspect`, `toMermaid`/`fromMermaid`, `summarize`/`summarizeGraph`, `equivalentOn`, and the `MachineState` type are all still re-exported from `@post-machine-js/machine`. v5/v6 added/refined debugger primitives without removing any v3 utilities. - **Post-aware wrappers persist unchanged.** `summarizePostMachine(machine)` and `equivalentPostMachines(reference, candidate, cases, options?)` remain the recommended path for typical usage. The bare upstream functions stay re-exported for advanced cases (e.g., comparing a PostMachine against a hand-rolled TuringMachine via `equivalentOn`). diff --git a/package-lock.json b/package-lock.json index 4287ba6..ae45c96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2740,9 +2740,9 @@ } }, "node_modules/@turing-machine-js/machine": { - "version": "7.0.0-alpha.4", - "resolved": "https://registry.npmjs.org/@turing-machine-js/machine/-/machine-7.0.0-alpha.4.tgz", - "integrity": "sha512-zgSXyI81koOMv4RAJhZOnCHZx5od1/j6i6dKM1+++1lsiZIEe6Ou3fxckKp6o9QjihJXfIE8QzzXX1LiOERURQ==", + "version": "7.0.0-alpha.5", + "resolved": "https://registry.npmjs.org/@turing-machine-js/machine/-/machine-7.0.0-alpha.5.tgz", + "integrity": "sha512-OxVwi89HxvTc3H+YzBGfB6cSXkRWdkOL77T6sCAKkCn82Nxq8oTGcQUWLOR0+23s65rcpUflMQZbabtJzN5BXA==", "license": "GPL-3.0-or-later", "engines": { "npm": ">=7.0.0" diff --git a/packages/machine/README.md b/packages/machine/README.md index e543ee6..668bb0c 100644 --- a/packages/machine/README.md +++ b/packages/machine/README.md @@ -880,9 +880,11 @@ pm.setBreakpoint('10', { before: true, after: '*' }); Halt breakpoints: ```javascript -pm.setBreakpoint(haltState, { before: true }); // pause at halt entry +pm.setBreakpoint(haltState, { before: true }); // pause at halt entry (filter shape is decorative) ``` +> Engine [#207](https://github.com/mellonis/turing-machine-js/issues/207) collapsed `haltState.debug` to a `boolean` — halt has one meaningful pause moment. The `filter` shape passed to `pm.setBreakpoint(haltState, …)` is kept for API stability but is now decorative: any registered halt breakpoint enables the engine-level boolean, and the registry entry drives only the arrival-path filtering in PostMachine's `onPause` wrapper. The pause fires on the AFTER side of the iter whose transition leads to halt. + Management: ```javascript @@ -907,16 +909,20 @@ pm.stateAt('10').debug = null; // equivalent to pm.clearBreakpoint('10') ``` -Direct writes on `haltState` throw (no PostMachine context for a redirect): +Direct writes on `haltState` now go straight to the engine setter — the prior module-load halt-lockdown was dropped alongside engine [#207](https://github.com/mellonis/turing-machine-js/issues/207). Boolean writes are accepted; object writes throw the engine's boolean-only error: ```javascript -haltState.debug = { before: true }; -// throws — use pm.setBreakpoint(haltState, ...) +haltState.debug = true; // ok — enables the halt breakpoint +haltState.debug = false; // ok — disables +haltState.debug = null; // ok — alias of false +haltState.debug = { before: true }; // throws: "haltState.debug only accepts boolean..." ``` -This single-channel model preserves a global invariant: `pm.listBreakpoints()` is the source of truth for what will fire `onPause`. +Direct halt writes bypass PostMachine's registry — they enable the engine pause but `pm.listBreakpoints()` won't record them. Use `pm.setBreakpoint(haltState, …)` when arrival-path filtering or registry awareness matters; use the direct write for ad-hoc halt-pause toggling in tools that don't need the registry. + +This relaxed model preserves the single-channel invariant where it matters: `pm.listBreakpoints()` is still the source of truth for what PostMachine's `onPause` wrapper surfaces. The engine's pause itself is now an open channel — by design, since the halt-lockdown's "per-PostMachine routing" benefit was syntactic only (haltState is a process-global singleton). -For the underlying engine reference — filter shapes, ordering (`before → step → after` on the same yield as of engine v6), the `haltState.debug.after` rejection — see [Debugging breakpoints](https://github.com/mellonis/turing-machine-js/tree/master/packages/machine#debugging-breakpoints) in the upstream README. +For the underlying engine reference — filter shapes for non-halt states, the `before → step → after` per-iter lifecycle (engine v6), and the boolean `haltState.debug` API (engine #207) — see [Debugging breakpoints](https://github.com/mellonis/turing-machine-js/tree/master/packages/machine#debugging-breakpoints) in the upstream README. ## Links diff --git a/packages/machine/src/breakpoints.spec.ts b/packages/machine/src/breakpoints.spec.ts index 571d497..60166fb 100644 --- a/packages/machine/src/breakpoints.spec.ts +++ b/packages/machine/src/breakpoints.spec.ts @@ -295,9 +295,20 @@ describe('lockdown redirect — direct state.debug writes', () => { }).toThrow(/ambiguous.*'10'.*'30'/); }); - test('haltState debug write throws (no PostMachine context for redirect)', () => { + test('haltState debug write goes straight to the engine setter — no lockdown (post-machine-js dropped haltState lockdown alongside engine #207)', () => { + // Pre-7.0.0-alpha.5 of post-machine-js (with engine pre-#207), direct + // `haltState.debug = X` writes from user code threw a lockdown-specific + // error message. The lockdown was dropped along with engine #207 — its + // "per-PostMachine routing" benefit was syntactic only (haltState is a + // process-global singleton). User writes now hit the engine setter + // directly: boolean is accepted; object shapes throw the engine's + // boolean-only error. + haltState.debug = true; + expect(haltState.debug).toBe(true); + haltState.debug = false; expect(() => { + // @ts-expect-error — HaltState typed alias narrows to boolean | null haltState.debug = { before: true }; - }).toThrow(/setBreakpoint\(haltState/); + }).toThrow(/haltState\.debug only accepts boolean/); }); }); diff --git a/packages/machine/src/classes/PostMachine.spec.ts b/packages/machine/src/classes/PostMachine.spec.ts index 7f6cfc4..49509a8 100644 --- a/packages/machine/src/classes/PostMachine.spec.ts +++ b/packages/machine/src/classes/PostMachine.spec.ts @@ -4,6 +4,24 @@ import { import { subroutineNameValidator } from '../validators'; import { getIxRange, getRandomInstructionIndex } from './PostMachine.test-helpers'; +// `MachineState.matchedTransition.id` is `${stateId}.${patternIx}` and stateId +// is a process-global counter (turing-machine-js#205). Two PostMachines built +// from the same instructions get different stateIds, so cross-machine +// `toEqual` comparisons of onStep call records would diff on this field +// despite being functionally identical. The "last and next command" tests +// below compare three machines built from the same body — strip +// `matchedTransition` before comparing to keep the equivalence semantic. +function stripMatchedTransition(calls: unknown[][]): unknown[][] { + return calls.map((args) => args.map((arg) => { + if (arg && typeof arg === 'object' && 'matchedTransition' in arg) { + const { matchedTransition, ...rest } = arg as Record; + void matchedTransition; + return rest; + } + return arg; + })); +} + describe('constructor', () => { test('no instructions', () => { expect(() => { @@ -406,8 +424,10 @@ describe('run tests', () => { expect(onStepMock1).toHaveBeenCalledTimes(1); expect(onStepMock2).toHaveBeenCalledTimes(1); expect(onStepMock3).toHaveBeenCalledTimes(1); - expect(onStepMock1.mock.calls).toEqual(onStepMock2.mock.calls); - expect(onStepMock2.mock.calls).toEqual(onStepMock3.mock.calls); + expect(stripMatchedTransition(onStepMock1.mock.calls)) + .toEqual(stripMatchedTransition(onStepMock2.mock.calls)); + expect(stripMatchedTransition(onStepMock2.mock.calls)) + .toEqual(stripMatchedTransition(onStepMock3.mock.calls)); }); }); @@ -459,8 +479,10 @@ describe('run tests', () => { expect(onStepMock1).toHaveBeenCalledTimes(2); expect(onStepMock2).toHaveBeenCalledTimes(2); expect(onStepMock3).toHaveBeenCalledTimes(2); - expect(onStepMock1.mock.calls).toEqual(onStepMock2.mock.calls); - expect(onStepMock2.mock.calls).toEqual(onStepMock3.mock.calls); + expect(stripMatchedTransition(onStepMock1.mock.calls)) + .toEqual(stripMatchedTransition(onStepMock2.mock.calls)); + expect(stripMatchedTransition(onStepMock2.mock.calls)) + .toEqual(stripMatchedTransition(onStepMock3.mock.calls)); }); }); diff --git a/packages/machine/src/classes/PostMachine.ts b/packages/machine/src/classes/PostMachine.ts index 817664c..95b239a 100644 --- a/packages/machine/src/classes/PostMachine.ts +++ b/packages/machine/src/classes/PostMachine.ts @@ -696,12 +696,20 @@ export class PostMachine extends TuringMachine { } #refreshHaltDebug(): void { - const filters = this.#breakpoints - .filter((bp): bp is Extract => bp.kind === 'halt') - .map((bp) => bp.filter); - withLockdownEscape(() => { - haltState.debug = (filters.length > 0 ? mergeBreakpointFilters(filters) : null) as State['debug']; - }); + // turing-machine-js#207: `haltState.debug` is now a boolean. The legacy + // `mergeBreakpointFilters` returned a per-side DebugConfig object that + // the engine rejects at write time. Halt has one meaningful pause + // moment (post-triggering-iter), so any registered halt-BP collapses to + // "on"; absence collapses to "off". The per-BP `filter` shape kept in + // `#breakpoints` is now decorative for halt entries — it still drives + // arrival-path filtering in the onPause wrapper but doesn't shape the + // engine-level write. + // + // No `withLockdownEscape` needed — the module-load `installHaltLockdown` + // was dropped in this release; haltState writes go straight to the + // engine's setter (which under #207 accepts boolean). + const hasHaltBP = this.#breakpoints.some((bp) => bp.kind === 'halt'); + haltState.debug = hasHaltBP; } #onUserDebugWrite(state: State, value: unknown): void { diff --git a/packages/machine/src/index.ts b/packages/machine/src/index.ts index c899f83..d8ccbbc 100644 --- a/packages/machine/src/index.ts +++ b/packages/machine/src/index.ts @@ -1,11 +1,21 @@ -import { haltState as engineHaltState, type MachineState as EngineMachineState } from '@turing-machine-js/machine'; +import type { MachineState as EngineMachineState } from '@turing-machine-js/machine'; import type { Path } from './path'; -import { installHaltLockdown } from './lockdown'; -// Install lockdown on the engine's haltState singleton at module load. Direct -// `haltState.debug = X` writes throw; only pm.setBreakpoint(haltState, …) can -// modify it (via withLockdownEscape internally). -installHaltLockdown(engineHaltState); +// Prior versions installed a module-load lockdown on the engine's haltState +// singleton (`installHaltLockdown`) that threw on every direct +// `haltState.debug = X` write. Dropped because: +// - turing-machine-js#207 collapsed `haltState.debug` to a boolean, removing +// the per-side `DebugConfig` API the lockdown was implicitly funneling. +// - The "per-PostMachine routing" benefit was syntactic only — haltState is +// a process-global singleton; pm.setBreakpoint(haltState, …) just wrote +// the same global flag, didn't actually isolate per instance. +// - Module-load side-effect leaked into Turing-only consumers that imported +// post-machine-js purely for shared APIs but never constructed a +// PostMachine — they were blocked from writing haltState.debug for no +// benefit. +// State-level lockdown on PostMachine-constructed States is unaffected — that +// one DOES guard a real per-instance registry (#stateToCandidatePaths + +// #breakpoints) where direct writes would bypass arrival-path filtering. export { Tape, diff --git a/packages/machine/src/lockdown.spec.ts b/packages/machine/src/lockdown.spec.ts index 61ba746..affc8a4 100644 --- a/packages/machine/src/lockdown.spec.ts +++ b/packages/machine/src/lockdown.spec.ts @@ -2,7 +2,6 @@ import { describe, expect, test } from 'vitest'; import { State, ifOtherSymbol, haltState } from '@turing-machine-js/machine'; import { installStateLockdown, - installHaltLockdown, withLockdownEscape, } from './lockdown'; @@ -85,27 +84,23 @@ describe('installStateLockdown', () => { }); }); -describe('installHaltLockdown', () => { - test('user writes throw a halt-specific error', () => { - // Note: installHaltLockdown mutates the engine's haltState singleton. We install - // once here; later imports of haltState see the locked-down accessor. Tests in - // this file run in sequence within one Vitest worker, so the installation - // persists across tests in the same file but does not leak across spec files - // (each file gets its own module graph). - installHaltLockdown(haltState); - expect(() => { - haltState.debug = { before: true }; - }).toThrow(/setBreakpoint\(haltState/); +describe('haltState writes — no lockdown (post #207)', () => { + // The module-load `installHaltLockdown` was dropped in this release. Halt-BP + // writes go straight to the engine's setter, which (turing-machine-js#207) + // accepts boolean and throws on object shapes. + test('boolean writes pass through to the engine setter', () => { + haltState.debug = true; + expect(haltState.debug).toBe(true); + haltState.debug = false; + expect(haltState.debug).toBe(false); + haltState.debug = null; + expect(haltState.debug).toBe(false); }); - test('escape allows internal writes to haltState', () => { - withLockdownEscape(() => { + test('object writes throw the engine-level error, not a lockdown error', () => { + expect(() => { + // @ts-expect-error — HaltState typed alias only accepts boolean | null haltState.debug = { before: true }; - }); - expect(haltState.debug?.before).toBe(true); - // Clear so other tests in this file/run aren't affected. - withLockdownEscape(() => { - haltState.debug = null; - }); + }).toThrow(/haltState\.debug only accepts boolean/); }); }); diff --git a/packages/machine/src/lockdown.ts b/packages/machine/src/lockdown.ts index ce25efa..e59462c 100644 --- a/packages/machine/src/lockdown.ts +++ b/packages/machine/src/lockdown.ts @@ -4,10 +4,6 @@ const LOCKDOWN_ERROR = 'Use pm.setBreakpoint(target, filter) to enable breakpoints. ' + 'Direct state.debug assignment is disabled on objects returned by PostMachine.'; -const HALT_LOCKDOWN_ERROR = - 'Direct haltState.debug assignment is disabled. The halt singleton is shared ' - + 'across PostMachine instances, so use pm.setBreakpoint(haltState, filter).'; - let escapeDepth = 0; export function withLockdownEscape(fn: () => T): T { @@ -46,21 +42,4 @@ export function installStateLockdown(state: State, onUserWrite: DebugRedirectHan }); } -export function installHaltLockdown(haltState: State): void { - const proto = captureProtoDebugAccessor(haltState); - Object.defineProperty(haltState, 'debug', { - configurable: true, - get() { - return proto.get(); - }, - set(value: unknown) { - if (escapeDepth > 0) { - proto.set(value); - return; - } - throw new Error(HALT_LOCKDOWN_ERROR); - }, - }); -} - -export { LOCKDOWN_ERROR, HALT_LOCKDOWN_ERROR }; +export { LOCKDOWN_ERROR }; From d52534f9605a7eab656a0aba3a8e7c262b61a32f Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Mon, 25 May 2026 11:37:17 +0300 Subject: [PATCH 14/34] docs(PostMachine): update stale lockdown-loop comment The comment justifying the `if (state.isHalt) continue;` skip in the `installStateLockdown` loop still referenced the module-load halt lockdown that was dropped earlier in this PR. The skip is still correct, but for a different reason: haltState is a process-global singleton, and installing a per-instance lockdown on it would block other PostMachine instances and turing-only consumers from writing it. --- packages/machine/src/classes/PostMachine.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/machine/src/classes/PostMachine.ts b/packages/machine/src/classes/PostMachine.ts index 95b239a..830b4e4 100644 --- a/packages/machine/src/classes/PostMachine.ts +++ b/packages/machine/src/classes/PostMachine.ts @@ -73,9 +73,11 @@ export class PostMachine extends TuringMachine { paths.sort(comparePathsCanonically); } - // Install the lockdown on every constructed State (except haltState, which is - // locked module-globally with halt-specific semantics — it's shared across - // PostMachine instances, so per-instance lockdown would clobber across runs). + // Install the lockdown on every constructed State (except haltState — it's + // a process-global singleton; installing a per-instance lockdown would block + // other PostMachine instances and turing-only consumers from writing it. + // Direct `haltState.debug = boolean` writes go to the engine setter, which + // (turing-machine-js#207) accepts boolean and rejects object shapes). // Direct `state.debug = X` writes are redirected to setBreakpoint/clearBreakpoint // when the State has exactly one candidate path; ambiguous shared States throw. // Iterate over the unique-state keyspace so shared States aren't re-installed. From cdb842a6938b28e5311a6b283791f72ea64235a9 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Mon, 25 May 2026 11:41:45 +0300 Subject: [PATCH 15/34] docs: trim historical bloat from new comments PR-description archaeology ("was X before / dropped because Y") belongs in the commit message and PR body, not in source. Each comment now explains only what a reader of the current code needs to know. --- packages/machine/src/breakpoints.spec.ts | 9 +------- .../machine/src/classes/PostMachine.spec.ts | 9 ++------ packages/machine/src/classes/PostMachine.ts | 21 +++++-------------- packages/machine/src/index.ts | 20 +++++------------- packages/machine/src/lockdown.spec.ts | 5 +---- 5 files changed, 14 insertions(+), 50 deletions(-) diff --git a/packages/machine/src/breakpoints.spec.ts b/packages/machine/src/breakpoints.spec.ts index 60166fb..33da207 100644 --- a/packages/machine/src/breakpoints.spec.ts +++ b/packages/machine/src/breakpoints.spec.ts @@ -295,14 +295,7 @@ describe('lockdown redirect — direct state.debug writes', () => { }).toThrow(/ambiguous.*'10'.*'30'/); }); - test('haltState debug write goes straight to the engine setter — no lockdown (post-machine-js dropped haltState lockdown alongside engine #207)', () => { - // Pre-7.0.0-alpha.5 of post-machine-js (with engine pre-#207), direct - // `haltState.debug = X` writes from user code threw a lockdown-specific - // error message. The lockdown was dropped along with engine #207 — its - // "per-PostMachine routing" benefit was syntactic only (haltState is a - // process-global singleton). User writes now hit the engine setter - // directly: boolean is accepted; object shapes throw the engine's - // boolean-only error. + test('haltState writes go to the engine setter — boolean OK, object throws', () => { haltState.debug = true; expect(haltState.debug).toBe(true); haltState.debug = false; diff --git a/packages/machine/src/classes/PostMachine.spec.ts b/packages/machine/src/classes/PostMachine.spec.ts index 49509a8..2f0da87 100644 --- a/packages/machine/src/classes/PostMachine.spec.ts +++ b/packages/machine/src/classes/PostMachine.spec.ts @@ -4,13 +4,8 @@ import { import { subroutineNameValidator } from '../validators'; import { getIxRange, getRandomInstructionIndex } from './PostMachine.test-helpers'; -// `MachineState.matchedTransition.id` is `${stateId}.${patternIx}` and stateId -// is a process-global counter (turing-machine-js#205). Two PostMachines built -// from the same instructions get different stateIds, so cross-machine -// `toEqual` comparisons of onStep call records would diff on this field -// despite being functionally identical. The "last and next command" tests -// below compare three machines built from the same body — strip -// `matchedTransition` before comparing to keep the equivalence semantic. +// matchedTransition.id embeds process-global stateIds (turing-machine-js#205) +// — strip it for cross-machine call-record equality. function stripMatchedTransition(calls: unknown[][]): unknown[][] { return calls.map((args) => args.map((arg) => { if (arg && typeof arg === 'object' && 'matchedTransition' in arg) { diff --git a/packages/machine/src/classes/PostMachine.ts b/packages/machine/src/classes/PostMachine.ts index 830b4e4..cf4720f 100644 --- a/packages/machine/src/classes/PostMachine.ts +++ b/packages/machine/src/classes/PostMachine.ts @@ -74,10 +74,8 @@ export class PostMachine extends TuringMachine { } // Install the lockdown on every constructed State (except haltState — it's - // a process-global singleton; installing a per-instance lockdown would block - // other PostMachine instances and turing-only consumers from writing it. - // Direct `haltState.debug = boolean` writes go to the engine setter, which - // (turing-machine-js#207) accepts boolean and rejects object shapes). + // a process-global singleton; per-instance lockdown would block other + // PostMachine instances and turing-only consumers from writing it). // Direct `state.debug = X` writes are redirected to setBreakpoint/clearBreakpoint // when the State has exactly one candidate path; ambiguous shared States throw. // Iterate over the unique-state keyspace so shared States aren't re-installed. @@ -698,18 +696,9 @@ export class PostMachine extends TuringMachine { } #refreshHaltDebug(): void { - // turing-machine-js#207: `haltState.debug` is now a boolean. The legacy - // `mergeBreakpointFilters` returned a per-side DebugConfig object that - // the engine rejects at write time. Halt has one meaningful pause - // moment (post-triggering-iter), so any registered halt-BP collapses to - // "on"; absence collapses to "off". The per-BP `filter` shape kept in - // `#breakpoints` is now decorative for halt entries — it still drives - // arrival-path filtering in the onPause wrapper but doesn't shape the - // engine-level write. - // - // No `withLockdownEscape` needed — the module-load `installHaltLockdown` - // was dropped in this release; haltState writes go straight to the - // engine's setter (which under #207 accepts boolean). + // The per-BP `filter` is decorative for halt entries — it drives + // arrival-path filtering in the onPause wrapper, not the engine-level + // write. haltState.debug is a boolean (turing-machine-js#207). const hasHaltBP = this.#breakpoints.some((bp) => bp.kind === 'halt'); haltState.debug = hasHaltBP; } diff --git a/packages/machine/src/index.ts b/packages/machine/src/index.ts index d8ccbbc..cb845ea 100644 --- a/packages/machine/src/index.ts +++ b/packages/machine/src/index.ts @@ -1,21 +1,11 @@ import type { MachineState as EngineMachineState } from '@turing-machine-js/machine'; import type { Path } from './path'; -// Prior versions installed a module-load lockdown on the engine's haltState -// singleton (`installHaltLockdown`) that threw on every direct -// `haltState.debug = X` write. Dropped because: -// - turing-machine-js#207 collapsed `haltState.debug` to a boolean, removing -// the per-side `DebugConfig` API the lockdown was implicitly funneling. -// - The "per-PostMachine routing" benefit was syntactic only — haltState is -// a process-global singleton; pm.setBreakpoint(haltState, …) just wrote -// the same global flag, didn't actually isolate per instance. -// - Module-load side-effect leaked into Turing-only consumers that imported -// post-machine-js purely for shared APIs but never constructed a -// PostMachine — they were blocked from writing haltState.debug for no -// benefit. -// State-level lockdown on PostMachine-constructed States is unaffected — that -// one DOES guard a real per-instance registry (#stateToCandidatePaths + -// #breakpoints) where direct writes would bypass arrival-path filtering. +// haltState is intentionally NOT locked down — direct +// `haltState.debug = boolean` writes go to the engine +// setter (turing-machine-js#207). Per-PostMachine +// State lockdown is installed by PostMachine's +// constructor (see `installStateLockdown`). export { Tape, diff --git a/packages/machine/src/lockdown.spec.ts b/packages/machine/src/lockdown.spec.ts index affc8a4..668ddb4 100644 --- a/packages/machine/src/lockdown.spec.ts +++ b/packages/machine/src/lockdown.spec.ts @@ -84,10 +84,7 @@ describe('installStateLockdown', () => { }); }); -describe('haltState writes — no lockdown (post #207)', () => { - // The module-load `installHaltLockdown` was dropped in this release. Halt-BP - // writes go straight to the engine's setter, which (turing-machine-js#207) - // accepts boolean and throws on object shapes. +describe('haltState writes — no lockdown', () => { test('boolean writes pass through to the engine setter', () => { haltState.debug = true; expect(haltState.debug).toBe(true); From d28f7b16be58d5ccd31b28ab481257fb74e212b5 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Mon, 25 May 2026 12:22:46 +0300 Subject: [PATCH 16/34] docs: trim historical bloat + drop stale references in source comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit pass on source comments. Removes 'was X / pre-vN / previously / v6.x / Under #N (when #N is the change event)' narratives that belong in commits/PRs/CHANGELOG, not source. Each trimmed comment now explains only what a current-source reader needs. Bucket 2 (actively wrong): - callGraph.ts:97-98 — dropped comment referring to a non-existent 'extractCallTargets' function (only the 'FromInstructions' variant exists). - PostMachine.debugger.spec.ts:1 — onPause is stable since v6.1.0, not 'experimental'. Also dropped 'Per turing v4' qualifier on debug mutability note. - index.ts:4-8 — dropped 'haltState is intentionally NOT locked down' residue from the prior lockdown-drop PR. Bucket 1 (historical bloat, trimmed): - lockdown.ts:19 — dropped 'Engine v6' qualifier on the prototype-descriptor invariant. - PostMachine.ts — trimmed advanceTracking 'previously this lived in onStep' paragraph and the cycle-aware-hopper 'v6.x' / 'Net effect' framing. - breakpoints.spec.ts + lockdown.spec.ts — 3 'Engine v6.1+ (#150) returned X rather than literal null' framings; 14-line 'v6.1.0-v6.3.0 advanceTracking ordering bug ... Fixed in v6.4.0' walkthrough. - PostMachine.naming.spec.ts — 6 'Under #85 / v6.x hopper' framings; added one #85 anchor at the describe block instead. - PostMachine.examples.spec.ts — 'engine v7's callable-subtree emit (#174) PLUS #85' narrative, 'retired alpha.1 -. onHalt .-> keyword' reference, 'vs alpha.2's 7 with the hopper' delta; 4 '(v6.1.0+)' describe-title suffixes. - PostMachine.spec.ts — 'Under #85 ... previously "iter 1: hopper" sequence collapses' delta + 'Net: 2 fewer ... than v6.x's 8' delta. - PostMachine.machine-state.spec.ts — '(v6.4.0+)' test-name suffix and 'under #85' mid-sentence qualifier. What stayed: forward-looking issue refs (#85, #174, #207, etc. as anchors for current spec), WORKAROUND markers, non-obvious-invariant notes, analyzer-shadowing surprise. All 315 tests pass; lint + typecheck clean. --- packages/machine/src/breakpoints.spec.ts | 27 ++++++-------- packages/machine/src/callGraph.ts | 2 -- .../src/classes/PostMachine.debugger.spec.ts | 11 +++--- .../src/classes/PostMachine.examples.spec.ts | 36 ++++++++----------- .../classes/PostMachine.machine-state.spec.ts | 10 +++--- .../src/classes/PostMachine.naming.spec.ts | 28 +++++++-------- .../machine/src/classes/PostMachine.spec.ts | 27 ++++++-------- packages/machine/src/classes/PostMachine.ts | 31 ++++++---------- packages/machine/src/index.ts | 6 ---- packages/machine/src/lockdown.spec.ts | 5 ++- packages/machine/src/lockdown.ts | 4 +-- 11 files changed, 72 insertions(+), 115 deletions(-) diff --git a/packages/machine/src/breakpoints.spec.ts b/packages/machine/src/breakpoints.spec.ts index 33da207..4736c3c 100644 --- a/packages/machine/src/breakpoints.spec.ts +++ b/packages/machine/src/breakpoints.spec.ts @@ -149,9 +149,9 @@ describe('pm.clearBreakpoint / clearBreakpoints', () => { pm.setBreakpoint('10', { before: true }); pm.clearBreakpoint('10'); expect(pm.listBreakpoints()).toEqual([]); - // Engine v6.1+ (#150) returns an empty `DebugConfig` after a filters - // reset rather than `null`. Check the public getters directly — `undefined` - // is the "no filter set" sentinel (field type: `symbol[] | true | undefined`). + // state.debug after a clear is an empty DebugConfig — read filter getters + // directly; `undefined` is the "no filter set" sentinel + // (field type: `symbol[] | true | undefined`). const dbg = pm.stateAt('10').debug; expect(dbg.before).toBeUndefined(); expect(dbg.after).toBeUndefined(); @@ -184,7 +184,7 @@ describe('pm.clearBreakpoint / clearBreakpoints', () => { pm.setBreakpoint(haltState, { before: true }); pm.clearBreakpoints(); expect(pm.listBreakpoints()).toEqual([]); - // Engine v6.1+ (#150) — see clearBreakpoint test above. + // Empty DebugConfig after clear — see clearBreakpoint test above. const dbg = pm.stateAt('10').debug; expect(dbg.before).toBeUndefined(); expect(dbg.after).toBeUndefined(); @@ -209,19 +209,12 @@ describe('onPause — registry-aware filtering', () => { }); test('arrivalPath in after-fire onPause is the iter that just fired, not the next one', async () => { - // Regression test for the v6.1.0-v6.3.0 `advanceTracking` ordering bug. - // Before v6.4.0, PostMachine advanced `prev` inside its `onStep` wrapper, - // which runs BETWEEN engine v6's `onPause(before, K)` and `onPause(after, K)` - // dispatches on the same yield. So `onPause(after, K)` saw `prev = K` - // (advanced by onStep on the same iter) instead of `K-1` — and the path - // resolver "from K, with K's symbol, to ..." returned K+1's instruction, - // not K's. In this program, instruction 30's after-fire would have - // resolved to 40 (the next-fall-through instruction) instead of 30. - // - // Fixed in v6.4.0 by moving advanceTracking from the internal onStep - // wrapper to a new internal onIter wrapper, which fires at end-of-iter - // — after both onPause dispatches have already read their iter-correct - // prev. Engine onIter hook landed in turing-machine-js v6.4.0 (#163). + // Regression: after-fire onPause must see arrivalPath of the iter that + // fired, not the next one. Without end-of-iter prev advance, + // onPause(after, K) reads prev = K (advanced by onStep mid-iter) instead + // of K-1 — the path resolver then returns K+1's instruction. + // In this program, instruction 30's after-fire would resolve to 40 + // (next fall-through) instead of 30. const pm = new PostMachine({ 10: check(20, 30), 20: right(10), diff --git a/packages/machine/src/callGraph.ts b/packages/machine/src/callGraph.ts index e02d6ab..0271634 100644 --- a/packages/machine/src/callGraph.ts +++ b/packages/machine/src/callGraph.ts @@ -94,8 +94,6 @@ export function analyzeLocalCallGraph( return {cyclicSet, buildOrder}; } -// Same as `extractCallTargets` above but with the right return type. The -// earlier function had a placeholder return; this is the real one. function extractCallTargetsFromInstructions(instructions: Instructions): Set { const targets = new Set(); diff --git a/packages/machine/src/classes/PostMachine.debugger.spec.ts b/packages/machine/src/classes/PostMachine.debugger.spec.ts index 9024fb0..c9efa7c 100644 --- a/packages/machine/src/classes/PostMachine.debugger.spec.ts +++ b/packages/machine/src/classes/PostMachine.debugger.spec.ts @@ -1,6 +1,6 @@ -// PostMachine debugger surface — async run() semantics and the experimental -// onPause forwarding. Mirrors v3.spec.ts structure; both files -// stay non-README (README-driven tests live in examples.spec.ts). +// PostMachine debugger surface — async run() semantics and onPause forwarding. +// Mirrors v3.spec.ts structure; both files stay non-README (README-driven +// tests live in examples.spec.ts). import { PostMachine, @@ -82,9 +82,8 @@ describe('PostMachine — onPause forwarding', () => { symbols: ['*', '*', ' '], })); - // Attach a `before` breakpoint on the initial state. Per turing v4, - // setting `state.debug` is runtime-mutable; the upstream run() loop - // checks it on each iteration boundary. + // Attach a `before` breakpoint on the initial state. `state.debug` is + // runtime-mutable; the upstream run() loop checks it on each iter. machine.initialState.debug = { before: true }; const seen: MachineState[] = []; diff --git a/packages/machine/src/classes/PostMachine.examples.spec.ts b/packages/machine/src/classes/PostMachine.examples.spec.ts index 0b2b6f8..4c23068 100644 --- a/packages/machine/src/classes/PostMachine.examples.spec.ts +++ b/packages/machine/src/classes/PostMachine.examples.spec.ts @@ -221,25 +221,19 @@ describe('packages/machine/README.md', () => { expect(mermaid).toContain('flowchart TD'); expect(mermaid).toContain('%% alphabets: [[" ","*"]]'); - // Halt + the entry — under engine v7's callable-subtree emit (#174) - // PLUS PostMachine's drop-acyclic-hopper change (#85), the wrapper at - // the call site wraps `rightToBlank::1` directly (not the v6.x hopper - // named `rightToBlank`). The subgraph label and composite name both - // reflect the bare's identity. + // Acyclic + plain-first-instruction → wrapper wraps `rightToBlank::1` + // directly; subgraph label and composite name reflect the bare's identity. expect(mermaid).toContain('(((halt)))'); expect(mermaid).toMatch(/subgraph w_\d+\["callable subtree of rightToBlank::1"\]/); - // Wrapper bears the top-level entry auto-tag `main` (#86). + // Top-level entry auto-tag `main` (#86). expect(mermaid).toContain('[["rightToBlank::1(1~2)
main"]]'); - // Bare inside the subgraph bears the subroutine-entry auto-tag (#86). + // Subroutine-entry auto-tag (#86). expect(mermaid).toContain('["rightToBlank::1
rightToBlank"]'); - // The v6.x hopper named 'rightToBlank' is dropped (acyclic + plain first instr). - // The bare's display label is now `rightToBlank::1
rightToBlank`, so the - // bare 'rightToBlank' (without `::1`) does not appear as a node label. + // No bare `rightToBlank` (without `::1`) node label. expect(mermaid).not.toMatch(/s\d+\["rightToBlank"\]/); - // Bold `== "call" ==>` from wrapper to bare + dotted `-. "return" .->` - // from subgraph back to wrapper. The retired alpha.1 `-. onHalt .->` - // keyword does not appear; wrapper-to-override is a regular solid arrow. + // Bold `== "call" ==>` wrapper→bare + dotted `-. "return" .->` + // subgraph→wrapper. Wrapper-to-override is a regular solid arrow. expect(mermaid).toMatch(/s\d+ == "call" ==> s\d+/); expect(mermaid).toMatch(/w_\d+ -\. "return" \.-> s\d+/); expect(mermaid).not.toMatch(/onHalt/); @@ -352,7 +346,7 @@ describe('packages/machine/README.md', () => { }); }); - describe('MachineState shape (v6.1.0+)', () => { + describe('MachineState shape', () => { test('onStep receives arrivalPath and candidatePaths for a simple machine', async () => { const m = new PostMachine({ 10: mark, @@ -514,11 +508,9 @@ describe('packages/machine/README.md', () => { expect(a.compositionEdgeCount).toBe(0); expect(a.maxCompositionDepth).toBe(0); - // Under #85, `walkToBlank` is acyclic + has a plain first instruction - // (check), so its hopper is dropped. The subroutine contributes 2 - // body states (walkToBlank::1, walkToBlank::2) + 1 continuation + - // the wrapper at instruction 10 + the top-level mark = 6 nodes - // (vs alpha.2's 7 with the hopper). + // `walkToBlank` is acyclic + plain first instruction (check), so no + // hopper. 6 nodes: 2 body (walkToBlank::1, walkToBlank::2) + 1 + // continuation + wrapper at instruction 10 + top-level mark. // console.log(b.stateCount, b.compositionEdgeCount, b.maxCompositionDepth); // 6 1 1 expect(b.stateCount).toBe(6); expect(b.compositionEdgeCount).toBe(1); @@ -572,7 +564,7 @@ describe('packages/machine/README.md', () => { }); }); - describe('Path-based resolver (v6.1.0+)', () => { + describe('Path-based resolver', () => { test('top-level instruction is reachable by string and object path', () => { const pm = new PostMachine({ 10: mark, 20: stop }); @@ -595,7 +587,7 @@ describe('packages/machine/README.md', () => { }); }); - describe('Breakpoints (v6.1.0+)', () => { + describe('Breakpoints', () => { test('registered breakpoint fires onPause with arrivalPath', async () => { const pm = new PostMachine({ 10: check(20, 30), @@ -633,7 +625,7 @@ describe('packages/machine/README.md', () => { }); }); - describe('Lockdown semantics (v6.1.0+)', () => { + describe('Lockdown semantics', () => { test('direct write on un-shared State redirects to setBreakpoint', () => { const pm = new PostMachine({ 10: mark, 20: stop }); diff --git a/packages/machine/src/classes/PostMachine.machine-state.spec.ts b/packages/machine/src/classes/PostMachine.machine-state.spec.ts index 24fb812..4abc566 100644 --- a/packages/machine/src/classes/PostMachine.machine-state.spec.ts +++ b/packages/machine/src/classes/PostMachine.machine-state.spec.ts @@ -17,7 +17,7 @@ describe('PostMachine — wrapped MachineState', () => { } }); - test('onIter receives arrivalPath and candidatePaths, once per iter (v6.4.0+)', async () => { + test('onIter receives arrivalPath and candidatePaths, once per iter', async () => { const m = new PostMachine({ 10: mark, 20: stop, @@ -68,11 +68,9 @@ describe('PostMachine — wrapped MachineState', () => { }); test('subroutine body instruction has fully-qualified arrivalPath', async () => { - // Use a 2-instruction body so the second instruction is visited as its - // own iter (the first instruction's State is now the wrapper's bare - // under #85 — its arrivalPath is the call site, not the FQ subroutine - // path. The second body instruction always gets its own iter regardless - // of hopper-drop status). + // 2-instruction body so the second instruction visits as its own iter: + // the first instruction's State IS the wrapper's bare, so its + // arrivalPath is the call site (not the FQ subroutine path). const m = new PostMachine({ 10: call('foo'), foo: { 1: right, 2: mark }, diff --git a/packages/machine/src/classes/PostMachine.naming.spec.ts b/packages/machine/src/classes/PostMachine.naming.spec.ts index 766ef2d..84f1c06 100644 --- a/packages/machine/src/classes/PostMachine.naming.spec.ts +++ b/packages/machine/src/classes/PostMachine.naming.spec.ts @@ -118,6 +118,8 @@ describe('PostMachine — group states and wrapper composite', () => { }); }); +// Hopper-drop spec (#85): acyclic subroutines with a plain first instruction +// skip the hopper and wrap their first-instruction State directly. describe('PostMachine — subroutine body and hopper names', () => { test('subroutine inner states use fully-qualified names', () => { // Use mark/right/mark so all three instructions produce real (non-halt) states. @@ -129,8 +131,7 @@ describe('PostMachine — subroutine body and hopper names', () => { 3: mark, }, }); - // Acyclic subroutine + plain first instruction → hopper dropped (#85). - // The wrapper's bare is foo's first-instruction State (foo::1) directly; + // Wrapper's bare is foo's first-instruction State (foo::1) directly; // composite name reflects that. expect(machine.initialState.name).toBe('foo::1(10~halt)'); @@ -139,7 +140,7 @@ describe('PostMachine — subroutine body and hopper names', () => { expect(names.has('foo::1')).toBe(true); expect(names.has('foo::2')).toBe(true); expect(names.has('foo::3')).toBe(true); - // Hopper dropped — no bare 'foo' node in the graph. + // No bare 'foo' node in the graph. expect(names.has('foo')).toBe(false); }); @@ -155,7 +156,7 @@ describe('PostMachine — subroutine body and hopper names', () => { }, }); // outer's first instruction is `call('inner')` — that produces a wrapper, - // so #85's hopper-drop is blocked (engine #176 would unwrap the inner + // so the hopper-drop is blocked (engine #176 would unwrap the inner // wrapping). outer keeps its hopper named "outer". expect(machine.initialState.name).toBe('outer(10~halt)'); @@ -183,10 +184,9 @@ describe('PostMachine — combined naming scenarios', () => { }, }); const names = collectNames(machine); - // Under #85, `bar` is acyclic + plain first instruction (mark) → hopper - // dropped; no bare 'foo::bar' node. The inner-call wrapper composite is - // 'foo::bar::1(foo::1~foo::2)' on state.name, with body state 'foo::bar::1' - // appearing as a node. + // `bar` is acyclic + plain first instruction → no bare 'foo::bar' node. + // The inner-call wrapper composite is 'foo::bar::1(foo::1~foo::2)' on + // state.name, with body state 'foo::bar::1' appearing as a node. expect(names.has('foo::bar')).toBe(false); expect(names.has('foo::bar::1')).toBe(true); expect(names.has('foo::1~foo::2')).toBe(true); @@ -219,9 +219,9 @@ describe('PostMachine — combined naming scenarios', () => { }, }); const names = collectNames(machine); - // Under #85, `bar` is acyclic + plain first instruction → hopper dropped. - // No bare 'foo::bar' node; the bare 'foo::bar::1' is the wrapper's target, - // and the tail-position continuation is 'foo::1~halt'. + // `bar` is acyclic + plain first instruction → no bare 'foo::bar' node. + // The bare 'foo::bar::1' is the wrapper's target; tail continuation + // is 'foo::1~halt'. expect(names.has('foo::bar')).toBe(false); expect(names.has('foo::1~halt')).toBe(true); expect(names.has('foo::bar::1')).toBe(true); @@ -240,9 +240,9 @@ describe('PostMachine — combined naming scenarios', () => { }, }); const names = collectNames(machine); - // Each scope hops accumulate in the prefix. `deepest` is acyclic with a - // plain first instruction → hopper dropped (#85); only its body state - // 'outer::inner::deepest::1' appears in the graph. + // Each scope hop accumulates in the prefix. `deepest` is acyclic with + // a plain first instruction — no bare 'outer::inner::deepest' node, + // only its body state 'outer::inner::deepest::1' appears. expect(names.has('outer::inner::deepest::1')).toBe(true); expect(names.has('outer::inner::deepest')).toBe(false); }); diff --git a/packages/machine/src/classes/PostMachine.spec.ts b/packages/machine/src/classes/PostMachine.spec.ts index 2f0da87..dcdf838 100644 --- a/packages/machine/src/classes/PostMachine.spec.ts +++ b/packages/machine/src/classes/PostMachine.spec.ts @@ -232,10 +232,8 @@ describe('constructor', () => { .toThrow(`invalid subroutine name: '${invalidSubroutineName}'`); }); - // Regression: the regex used to be unanchored (/[A-Z$_][A-Z0-9$_]*/i), - // accepting any string CONTAINING a valid identifier substring. After the - // ^...$ anchor fix, leading-digit and embedded-space names are correctly - // rejected. + // Regression: subroutine-name regex must be fully anchored — reject + // leading-digit and embedded-space names. describe('subroutineNameValidator anchor regression', () => { ['1abc', 'foo bar', '$$ x', 'a/b', '!name'].forEach((name) => { test(`rejects ${JSON.stringify(name)}`, () => { @@ -465,12 +463,9 @@ describe('run tests', () => { stepsLimit: 3, onStep: (...args) => onStepMock3(...args), })).resolves.toBeUndefined(); - // Under #85, the subroutine's hopper is dropped (acyclic + plain first - // instruction noop). The previous "iter 1: hopper, iter 2: noop body, - // iter 3: halt" sequence collapses to "iter 1: wrapper-of-noop, iter 2: - // halt" — one fewer onStep call per machine. (Note: post-iter halt - // dispatches as the second call here because the wrapper-of-noop's - // halt-bound transition resolves to haltState directly.) + // 2 iters: wrapper-of-noop fires once, then post-iter halt dispatch. + // (Acyclic + plain-first-instruction subroutine wraps the body directly, + // no hopper iter.) expect(onStepMock1).toHaveBeenCalledTimes(2); expect(onStepMock2).toHaveBeenCalledTimes(2); expect(onStepMock3).toHaveBeenCalledTimes(2); @@ -587,13 +582,11 @@ describe('run tests', () => { onStep: (...args) => onStepMock(...args), })).resolves.toBeUndefined(); - // Under #85, hoppers are dropped for `subroutineNameList[1]` (outer, - // first instr `mark`, acyclic) and the nested `subroutineNameList[0]` - // (first instr `erase`, acyclic). Outer `subroutineNameList[0]` keeps - // its hopper (the analyzer sees its body calling 'sub0' as a lexical - // self-reference, conservatively classifying it as cyclic — runtime - // would resolve through shadowing, but the static analyzer doesn't - // model scope shadowing). Net: 2 fewer onStep calls than v6.x's 8. + // Outer `subroutineNameList[0]` keeps its hopper — the static analyzer + // sees its body calling 'sub0' as a lexical self-reference and + // conservatively classifies it as cyclic (runtime would resolve through + // shadowing, but the analyzer doesn't model scope shadowing). + // The other two subs are acyclic + plain-first-instruction → no hopper. expect(onStepMock).toHaveBeenCalledTimes(6); expect(machine.tape.viewport[0]).toEqual(' '); diff --git a/packages/machine/src/classes/PostMachine.ts b/packages/machine/src/classes/PostMachine.ts index cf4720f..bf2a7e3 100644 --- a/packages/machine/src/classes/PostMachine.ts +++ b/packages/machine/src/classes/PostMachine.ts @@ -116,14 +116,9 @@ export class PostMachine extends TuringMachine { let prevJsSymbol: symbol | null = null; const entryPath = this.#firstStepArrivalPath(); - // Tracking is owned by the internal onIter wrapper (engine v6.4.0+). - // onIter fires at end-of-iter — after both onPause(before, K) and - // onPause(after, K) have already read their iter-correct prev — so - // advancing here doesn't race those reads. Previously this lived in - // the internal onStep wrapper, which ran BETWEEN before- and after- - // fires on the same yield, causing after-fire arrivalPath to resolve - // against K's prev instead of K-1's. See tests/breakpoints.spec.ts - // "arrivalPath in after-fire onPause" for the regression case. + // Advance at end-of-iter (via the internal onIter wrapper) so both + // `onPause(before, K)` and `onPause(after, K)` read iter-correct prev. + // Advancing inside onStep would race after-fire arrivalPath resolution. const advanceTracking = (raw: EngineMachineState): void => { prevState = raw.state; prevJsSymbol = this.tapeBlock.symbol([raw.currentSymbols[0]]); @@ -281,19 +276,15 @@ export class PostMachine extends TuringMachine { }; // Cycle-aware hopper construction (#85). // - // Static analysis of the local subroutine call graph identifies which - // subroutines participate in cycles (mutual recursion or self-loop). For - // those, we keep the v6.x hopper — a stub `State` that wraps a - // `Reference` to the subroutine's first instruction, providing the - // forward-declaration anchor that `withOverriddenHaltState` needs at the - // moment `call(...)` invocations are processed. + // Subroutines in cycles (mutual recursion / self-loop) get a hopper — a + // stub `State` wrapping a `Reference` to the first instruction — to give + // `withOverriddenHaltState` a forward-declaration anchor when `call(...)` + // is processed. // - // Acyclic subroutines (the common case) skip the hopper. We process them - // in reverse-topological build order — sinks first — so by the time - // `call('X')` runs for an acyclic X, X's first-instruction State already - // exists and we wrap it directly. Net effect: -1 graph node per acyclic - // subroutine; the wrapper composite name becomes `X::1(continuation)` - // (accurately reflects the wrapped bare) instead of `X(continuation)`. + // Acyclic subroutines skip the hopper: processed in reverse-topological + // build order, so by the time `call('X')` runs for acyclic X, X's + // first-instruction State already exists and we wrap it directly. + // Composite name reflects the wrapped bare: `X::1(continuation)`. const localSubroutineInstructions: Record = Object.fromEntries( Object.entries(localSubroutinesData).map(([name, data]) => [name, data.instructions]), ); diff --git a/packages/machine/src/index.ts b/packages/machine/src/index.ts index cb845ea..a2b9bb8 100644 --- a/packages/machine/src/index.ts +++ b/packages/machine/src/index.ts @@ -1,12 +1,6 @@ import type { MachineState as EngineMachineState } from '@turing-machine-js/machine'; import type { Path } from './path'; -// haltState is intentionally NOT locked down — direct -// `haltState.debug = boolean` writes go to the engine -// setter (turing-machine-js#207). Per-PostMachine -// State lockdown is installed by PostMachine's -// constructor (see `installStateLockdown`). - export { Tape, State, diff --git a/packages/machine/src/lockdown.spec.ts b/packages/machine/src/lockdown.spec.ts index 668ddb4..a3f70cd 100644 --- a/packages/machine/src/lockdown.spec.ts +++ b/packages/machine/src/lockdown.spec.ts @@ -76,9 +76,8 @@ describe('installStateLockdown', () => { // After the inner escape ends, the outer is still active. s.debug = null; }); - // Engine v6.1+ (#150) returns an empty `DebugConfig` after `state.debug = null` - // (filters cleared) rather than literal `null`. Check the public getters - // directly — `undefined` is the "no filter set" sentinel. + // state.debug after a clear is an empty DebugConfig — read filter getters + // directly; `undefined` is the "no filter set" sentinel. expect(s.debug.before).toBeUndefined(); expect(s.debug.after).toBeUndefined(); }); diff --git a/packages/machine/src/lockdown.ts b/packages/machine/src/lockdown.ts index e59462c..44df2a1 100644 --- a/packages/machine/src/lockdown.ts +++ b/packages/machine/src/lockdown.ts @@ -16,8 +16,8 @@ export function withLockdownEscape(fn: () => T): T { } function captureProtoDebugAccessor(state: object): { get: () => unknown; set: (v: unknown) => void } { - // Engine v6: State.prototype owns `get debug() / set debug()`. The descriptor is - // always on the immediate prototype. + // State.prototype owns `get debug() / set debug()` — descriptor is on the + // immediate prototype. const proto = Object.getPrototypeOf(state); const desc = Object.getOwnPropertyDescriptor(proto, 'debug')!; return { get: desc.get!.bind(state), set: desc.set!.bind(state) }; From 78a782569f7fd1d612df56dbcc7a10ae7a07d989 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Mon, 25 May 2026 12:38:29 +0300 Subject: [PATCH 17/34] chore(release): 7.0.0-alpha.5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - @post-machine-js/machine: 7.0.0-alpha.4 → 7.0.0-alpha.5 - engine peer-dep widened: ^7.0.0-alpha.4 → ^7.0.0-alpha.5 - CHANGELOG entry documenting: dropped module-load haltState lockdown (#94), source-comment cleanup (#95), and behavior changes inherited from engine alpha.5 (#205 matchedTransition, #207 boolean haltState.debug + halt-imminent AFTER-side timing) --- lerna.json | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- packages/machine/CHANGELOG.md | 38 +++++++++++++++++++++++++++++++++++ packages/machine/package.json | 6 +++--- 5 files changed, 47 insertions(+), 9 deletions(-) diff --git a/lerna.json b/lerna.json index 32f00eb..6b9d6ee 100644 --- a/lerna.json +++ b/lerna.json @@ -3,6 +3,6 @@ "packages": [ "packages/*" ], - "version": "7.0.0-alpha.4", + "version": "7.0.0-alpha.5", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ae45c96..df29eb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "packages/*" ], "dependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.4" + "@turing-machine-js/machine": "^7.0.0-alpha.5" }, "devDependencies": { "@tsconfig/recommended": "^1.0.13", @@ -10441,16 +10441,16 @@ }, "packages/machine": { "name": "@post-machine-js/machine", - "version": "7.0.0-alpha.4", + "version": "7.0.0-alpha.5", "license": "GPL-3.0-or-later", "devDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.4" + "@turing-machine-js/machine": "^7.0.0-alpha.5" }, "engines": { "npm": ">=7.0.0" }, "peerDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.4" + "@turing-machine-js/machine": "^7.0.0-alpha.5" } } } diff --git a/package.json b/package.json index 119a0fd..9050421 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,6 @@ "vitest": "^4.1.5" }, "dependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.4" + "@turing-machine-js/machine": "^7.0.0-alpha.5" } } diff --git a/packages/machine/CHANGELOG.md b/packages/machine/CHANGELOG.md index 4cab338..fb02d35 100644 --- a/packages/machine/CHANGELOG.md +++ b/packages/machine/CHANGELOG.md @@ -4,6 +4,44 @@ All notable changes to this project will be documented in this file. 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). +## [7.0.0-alpha.5] - 2026-05-25 + +Fifth v7 pre-release. Drops the module-load haltState lockdown in lockstep with engine [#207](https://github.com/mellonis/turing-machine-js/issues/207) — the lockdown was funneling per-side `DebugConfig` writes through `withLockdownEscape`, but with engine alpha.5 collapsing `haltState.debug` to a `boolean`, there's nothing to mediate. Engine peer-dep widened `^7.0.0-alpha.4` → `^7.0.0-alpha.5`; consumers inherit per-iter `MachineState.matchedTransition` ([engine #205](https://github.com/mellonis/turing-machine-js/issues/205)) and the `GraphTransition.id` separator change (`-` → `.`, same issue) transparently. Published to npm under the `next` dist-tag: `npm install @post-machine-js/machine@next`. + +**Pre-release — the API surface may still shift before stable v7.0.0.** Pin to a specific alpha for reproducibility: `@post-machine-js/machine@7.0.0-alpha.5`. + +### Removed + +- **Module-load `installHaltLockdown(haltState)` from `src/index.ts`** ([PR #94](https://github.com/mellonis/post-machine-js/pull/94)). Direct `haltState.debug = boolean` writes from user code now go straight to the engine setter (which under engine #207 accepts boolean and throws on object shapes). The "per-PostMachine routing" benefit was syntactic only — `haltState` is a process-global singleton, so `pm.setBreakpoint(haltState, …)` wrote the same global flag regardless of instance. The module-load side-effect also leaked into turing-only consumers that imported `@post-machine-js/machine` for shared APIs but never constructed a PostMachine — they were blocked from writing `haltState.debug` for no benefit. + +- **`installHaltLockdown` + `HALT_LOCKDOWN_ERROR` exports from `src/lockdown.ts`**. State-side `installStateLockdown` + `withLockdownEscape` are unaffected — those guard a real per-PostMachine registry (`#stateToCandidatePaths` + `#breakpoints`) where direct writes would bypass arrival-path filtering. + +### Changed + +- **`PostMachine.#refreshHaltDebug` writes the boolean directly** ([PR #94](https://github.com/mellonis/post-machine-js/pull/94)). No `withLockdownEscape` needed — the engine setter accepts the write. The per-BP `filter` shape kept in `#breakpoints` is now decorative for halt entries (it still drives arrival-path filtering in the `onPause` wrapper but doesn't shape the engine-level write). + +- **`pm.setBreakpoint(haltState, filter)` filter shape is decorative**. Any registered halt-BP collapses to `haltState.debug = true`; absence collapses to `false`. The `filter` is kept for API stability and continues to drive the registry's arrival-path filtering, but no longer maps to a per-side write. + +### Docs + +- **`packages/machine/README.md`** — halt breakpoint section rewritten: documents the relaxed direct-write surface (`haltState.debug = true / false / null` accepted; object writes throw the engine's boolean-only error), the registry-bypass caveat (direct writes don't appear in `pm.listBreakpoints()`), and when to use direct vs `pm.setBreakpoint(haltState, …)`. + +- **`CLAUDE.md`** — Subtlety 6 + debugger-primitives section updated to reflect "haltState is NO LONGER locked", split halt vs non-halt lockdown coverage. + +- **Source-comment audit** ([PR #95](https://github.com/mellonis/post-machine-js/pull/95)). 11 files, -43 lines net. Removed "was X / pre-vN / v6.x hopper / Under #N" historical narratives that belonged in commits/PRs/CHANGELOG. Fixed two stale references: `callGraph.ts` mentioned a non-existent `extractCallTargets` function, and `PostMachine.debugger.spec.ts` called `onPause` "experimental" though it's been stable since v6.1.0. + +### Compatibility + +- **Engine peer-dep widened** `^7.0.0-alpha.4` → `^7.0.0-alpha.5`. Required — the source uses the new boolean `haltState.debug` API. Consumers on engine alpha.4 would see a runtime throw on the first halt-BP toggle. + +- **Breaking for consumers that relied on the lockdown throw.** Pre-alpha.5 post code that did `haltState.debug = { before: true }` from user code received a post-side "Direct haltState.debug assignment is disabled" error. That throw is gone; the same write now reaches the engine and throws the engine's "haltState.debug only accepts boolean" error instead. Whole-object boolean writes (`haltState.debug = true`) that previously threw the lockdown error now succeed. + +- **Behavior changes inherited from engine alpha.5**: halt-imminent pause moved from BEFORE side to AFTER side of the halt-triggering iter (engine #207); `MachineState.matchedTransition` added on every yield (engine #205); `GraphTransition.id` separator changed from `-` to `.` (engine #205). + +### Out of v7-alpha.5 (still pending for stable v7.0.0) + +- **[#72](https://github.com/mellonis/post-machine-js/issues/72)** — extend `defineProperty` lockdown to intermediate engine-graph states. + ## [7.0.0-alpha.4] - 2026-05-23 Fourth v7 pre-release. Adds user-supplied tags on states ([#86](https://github.com/mellonis/post-machine-js/issues/86)) — both an inline decorator at construction and a path-based registry post-construction — plus an auto-tag policy that marks each program's/subroutine's entry state. Engine peer-dep widened `^7.0.0-alpha.2` → `^7.0.0-alpha.4` — alpha.3 added the `state.tag(...)` API this release builds on, and alpha.4 ships two upstream bug fixes that post inherits transparently (`toMermaid` HTML-entity-escapes user content in labels — fixes alphabet-with-`"` parse errors, [engine #194](https://github.com/mellonis/turing-machine-js/issues/194); `runStepByStep` halt stack scoped to the call, fixes a memory leak / ghost-iteration when the same machine is reused across calls, [engine #196](https://github.com/mellonis/turing-machine-js/issues/196)). Published to npm under the `next` dist-tag: `npm install @post-machine-js/machine@next`. diff --git a/packages/machine/package.json b/packages/machine/package.json index c2011d0..96f874e 100644 --- a/packages/machine/package.json +++ b/packages/machine/package.json @@ -1,6 +1,6 @@ { "name": "@post-machine-js/machine", - "version": "7.0.0-alpha.4", + "version": "7.0.0-alpha.5", "description": "A convenient Post machine", "engines": { "npm": ">=7.0.0" @@ -28,10 +28,10 @@ "machine" ], "peerDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.4" + "@turing-machine-js/machine": "^7.0.0-alpha.5" }, "devDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.4" + "@turing-machine-js/machine": "^7.0.0-alpha.5" }, "main": "dist/index.cjs", "module": "dist/index.mjs", From 7b7c0e49ead9e07bf79c526dee306f8e4ebbcda9 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Mon, 25 May 2026 19:02:36 +0300 Subject: [PATCH 18/34] =?UTF-8?q?feat(adopt):=20PostMachine.run()=20?= =?UTF-8?q?=E2=86=92=20sync;=20new=20PostDebugSession=20(#97,=20engine=20#?= =?UTF-8?q?102)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopts engine v7 alpha.6's API split (engine PR mellonis/turing-machine-js#214): - PostMachine.run() is now sync, callback-free. Forwards to super.run() with just {initialState, stepsLimit}. - PostMachine.debugRun({stepsLimit?}) returns a PostDebugSession that wraps the engine DebugSession, adds post-specific machineState wrapping (arrivalPath / candidatePaths) and applies the breakpoint registry as a filter before forwarding pause events. - PostDebugSession exposes the same interactive surface as the engine session: pause/step/iter/halt events, continue / stepIn / stepOver / stepOut, external pause(), stop(), setRunInterval(ms). Test migration is intentionally NOT in this commit — there are ~29 test invocations using the v6 callback shape that need migration. They land in a follow-up commit on this branch (or a separate one) before this can land as alpha.6. Peer-dep widen and CHANGELOG also follow in the alpha.6 bump PR. --- .../machine/src/classes/PostDebugSession.ts | 169 ++++++++++++++++++ packages/machine/src/classes/PostMachine.ts | 93 +++------- 2 files changed, 198 insertions(+), 64 deletions(-) create mode 100644 packages/machine/src/classes/PostDebugSession.ts diff --git a/packages/machine/src/classes/PostDebugSession.ts b/packages/machine/src/classes/PostDebugSession.ts new file mode 100644 index 0000000..da9673e --- /dev/null +++ b/packages/machine/src/classes/PostDebugSession.ts @@ -0,0 +1,169 @@ +import { + DebugSession as EngineDebugSession, + type MachineState as EngineMachineState, + State, + haltState, +} from '@turing-machine-js/machine'; +import { formatPath, type Path } from '../path'; +import type { MachineState } from '../index'; +import type { Breakpoint } from '../breakpoints'; +import type { PostMachine } from './PostMachine'; + +export type PostDebugSessionEvent = 'pause' | 'step' | 'iter' | 'halt'; + +export type PostDebugSessionListener = + E extends 'halt' + ? () => void | Promise + : (machineState: MachineState) => void | Promise; + +type ListenerMap = { + pause: Array<(m: MachineState) => void | Promise>; + step: Array<(m: MachineState) => void | Promise>; + iter: Array<(m: MachineState) => void | Promise>; + halt: Array<() => void | Promise>; +}; + +export type PostDebugSessionParameter = { + stepsLimit?: number; +}; + +/** + * Interactive debugger session for `PostMachine`. Wraps the upstream engine + * `DebugSession`, adds post-specific MachineState wrapping (`arrivalPath`, + * `candidatePaths`), and applies the PostMachine's breakpoint registry as a + * filter before forwarding pause events. + * + * Construct via `pm.debugRun()` rather than instantiating directly — the + * factory wires up the prev-state tracking and registry reference. + */ +export class PostDebugSession { + readonly #postMachine: PostMachine; + readonly #engineSession: EngineDebugSession; + readonly #listeners: ListenerMap = { + pause: [], + step: [], + iter: [], + halt: [], + }; + #prevState: State | null = null; + #prevJsSymbol: symbol | null = null; + readonly #entryPath: Path; + readonly #wrap: (raw: EngineMachineState, prev: State | null, prevSym: symbol | null) => MachineState; + readonly #getBreakpoints: () => readonly Breakpoint[]; + readonly #tapeBlockSymbol: (pattern: [string]) => symbol; + + constructor(args: { + postMachine: PostMachine; + engineSession: EngineDebugSession; + entryPath: Path; + wrap: (raw: EngineMachineState, prev: State | null, prevSym: symbol | null) => MachineState; + getBreakpoints: () => readonly Breakpoint[]; + tapeBlockSymbol: (pattern: [string]) => symbol; + }) { + this.#postMachine = args.postMachine; + this.#engineSession = args.engineSession; + this.#entryPath = args.entryPath; + this.#wrap = args.wrap; + this.#getBreakpoints = args.getBreakpoints; + this.#tapeBlockSymbol = args.tapeBlockSymbol; + + // Wire engine events to wrap + dispatch via our own listener registry. + this.#engineSession.on('step', (raw) => { + const wrapped = this.#wrap(raw, this.#prevState, this.#prevJsSymbol); + for (const fn of this.#listeners.step) void fn(wrapped); + }); + this.#engineSession.on('pause', (raw) => { + const wrapped = this.#wrap(raw, this.#prevState, this.#prevJsSymbol); + // Apply post-machine breakpoint registry filter — fire only when the + // engine pause was triggered by a registered breakpoint (or by a + // step-mode endpoint / manual pause). + if (!this.#shouldFire(raw, wrapped)) { + this.#engineSession.continue(); + return; + } + for (const fn of this.#listeners.pause) void fn(wrapped); + }); + this.#engineSession.on('iter', (raw) => { + const wrapped = this.#wrap(raw, this.#prevState, this.#prevJsSymbol); + for (const fn of this.#listeners.iter) void fn(wrapped); + // Advance prev for the NEXT iter's wrapping. + this.#prevState = raw.state; + this.#prevJsSymbol = this.#tapeBlockSymbol([raw.currentSymbols[0]]); + }); + this.#engineSession.on('halt', () => { + for (const fn of this.#listeners.halt) void fn(); + }); + + // Touch unused fields to satisfy the noUnusedLocals heuristic — they exist + // for future wrappers that need to read them. + void this.#postMachine; + void this.#entryPath; + } + + on(event: E, listener: PostDebugSessionListener): this { + (this.#listeners[event] as Array>).push(listener); + return this; + } + + off(event: E, listener: PostDebugSessionListener): this { + const arr = this.#listeners[event] as Array>; + const ix = arr.indexOf(listener); + if (ix >= 0) arr.splice(ix, 1); + return this; + } + + start(): Promise { + return this.#engineSession.start(); + } + + stop(): void { + this.#engineSession.stop(); + } + + pause(): void { + this.#engineSession.pause(); + } + + continue(): void { + this.#engineSession.continue(); + } + + stepIn(): void { + this.#engineSession.stepIn(); + } + + stepOver(): void { + this.#engineSession.stepOver(); + } + + stepOut(): void { + this.#engineSession.stepOut(); + } + + setRunInterval(ms: number): void { + this.#engineSession.setRunInterval(ms); + } + + // Decide whether a raw engine pause should surface to post-machine pause + // listeners. Step-mode endpoints and manual pauses always pass through; + // breakpoint-cause pauses are filtered against the registry so only + // path-registered or halt-registered states fire. + #shouldFire(raw: EngineMachineState, wrapped: MachineState): boolean { + const cause = raw.debugBreak?.cause; + if (cause === 'step' || cause === 'manual') { + return true; + } + // cause: 'breakpoint' — apply registry filter. + const breakpoints = this.#getBreakpoints(); + const nextIsHalt = raw.nextState instanceof State && raw.nextState.isHalt; + if (nextIsHalt && breakpoints.some((bp) => bp.kind === 'halt')) { + return true; + } + const arrivalKey = formatPath(wrapped.arrivalPath); + return breakpoints.some((bp) => + bp.kind === 'instruction' && formatPath(bp.path) === arrivalKey); + } +} + +// Re-exported to keep import surfaces tight at the package boundary. +export { haltState }; diff --git a/packages/machine/src/classes/PostMachine.ts b/packages/machine/src/classes/PostMachine.ts index bf2a7e3..b73e459 100644 --- a/packages/machine/src/classes/PostMachine.ts +++ b/packages/machine/src/classes/PostMachine.ts @@ -1,5 +1,6 @@ import { Alphabet, + DebugSession as EngineDebugSession, type MachineState as EngineMachineState, Reference, State, @@ -9,6 +10,7 @@ import { ifOtherSymbol, haltState, } from '@turing-machine-js/machine'; +import { PostDebugSession } from './PostDebugSession'; import { blankSymbol as defaultBlankSymbol, commandsSet, @@ -101,74 +103,37 @@ export class PostMachine extends TuringMachine { this.tapeBlock.replaceTape(newTape); } - override async run({ - stepsLimit = 1e5, - onStep, - onPause, - onIter, - }: { - stepsLimit?: number; - onStep?: (machineState: MachineState) => void; - onPause?: (machineState: MachineState) => void | Promise; - onIter?: (machineState: MachineState) => void | Promise; - } = {}): Promise { - let prevState: State | null = null; - let prevJsSymbol: symbol | null = null; - const entryPath = this.#firstStepArrivalPath(); - - // Advance at end-of-iter (via the internal onIter wrapper) so both - // `onPause(before, K)` and `onPause(after, K)` read iter-correct prev. - // Advancing inside onStep would race after-fire arrivalPath resolution. - const advanceTracking = (raw: EngineMachineState): void => { - prevState = raw.state; - prevJsSymbol = this.tapeBlock.symbol([raw.currentSymbols[0]]); - }; - - // If the user provided any callback, our internal onIter wrapper must - // be registered to keep `prev` advancing — every callback (including the - // user's own onStep/onPause/onIter) receives the wrapped state which - // depends on prev. If no user callback is provided, no one observes - // wrapped state and we can skip the internal wrapper too, leaving the - // run to halt with zero per-iter await overhead. - const isAnyCallbackProvided = !!(onStep || onPause || onIter); + /** + * Run the machine to halt — pure execution, no observation. Sync, returns + * void. Matches the engine's v7 `run()` contract. + * + * For interactive debugging (breakpoints, step-in / step-over / step-out, + * throttle, click-pause), use `debugRun()` to construct a `PostDebugSession`. + */ + override run({ stepsLimit = 1e5 }: { stepsLimit?: number } = {}): void { + super.run({ initialState: this.#initialState, stepsLimit }); + } - return super.run({ + /** + * Return a `PostDebugSession` bound to this PostMachine. The session + * wraps each engine MachineState with post-specific `arrivalPath` / + * `candidatePaths`, and applies the PostMachine's breakpoint registry as + * a filter before forwarding pause events. + */ + debugRun({ stepsLimit = 1e5 }: { stepsLimit?: number } = {}): PostDebugSession { + const entryPath = this.#firstStepArrivalPath(); + const engineSession = new EngineDebugSession(this, { initialState: this.#initialState, stepsLimit, - onStep: onStep ? (raw) => { - const wrapped = this.#wrapMachineState(raw, prevState, prevJsSymbol, entryPath); - onStep(wrapped); - } : undefined, - onPause: onPause ? async (raw) => { - const wrapped = this.#wrapMachineState(raw, prevState, prevJsSymbol, entryPath); - if (this.#shouldFireOnPause(raw, wrapped)) { - await onPause(wrapped); - } - } : undefined, - onIter: isAnyCallbackProvided ? async (raw) => { - if (onIter) { - // Wrap with PRE-advance prev so the user's onIter sees the same - // arrivalPath as onPause(after, K) saw — both describing the - // arrival at iter K. - const wrapped = this.#wrapMachineState(raw, prevState, prevJsSymbol, entryPath); - await onIter(wrapped); - } - advanceTracking(raw); - } : undefined, }); - } - - #shouldFireOnPause(raw: EngineMachineState, wrapped: MachineState): boolean { - // Halt-arrival: engine pauses on the iteration whose nextState is halt, - // when haltState.debug is set. Yielded raw.state is the *previous* user - // instruction (e.g., 30 in `30: mark; 40: stop`), never haltState itself. - const nextIsHalt = raw.nextState instanceof State && raw.nextState.isHalt; - if (nextIsHalt && this.#breakpoints.some((bp) => bp.kind === 'halt')) { - return true; - } - const arrivalKey = formatPath(wrapped.arrivalPath); - return this.#breakpoints.some((bp) => - bp.kind === 'instruction' && formatPath(bp.path) === arrivalKey); + return new PostDebugSession({ + postMachine: this, + engineSession, + entryPath, + wrap: (raw, prev, prevSym) => this.#wrapMachineState(raw, prev, prevSym, entryPath), + getBreakpoints: () => this.#breakpoints, + tapeBlockSymbol: (pattern) => this.tapeBlock.symbol(pattern), + }); } override * runStepByStep({ stepsLimit = 1e5 }: { stepsLimit?: number } = {}): Generator { From c7ce869ad31491f440ae82e0ed922e7ef813b612 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Mon, 25 May 2026 19:10:16 +0300 Subject: [PATCH 19/34] test(adopt): migrate run({onPause/onStep/onIter}) call sites to runStepByStep / debugRun (#97) --- packages/machine/src/breakpoints.spec.ts | 16 +++-- .../src/classes/PostMachine.debugger.spec.ts | 35 +++++---- .../src/classes/PostMachine.examples.spec.ts | 19 +++-- .../classes/PostMachine.machine-state.spec.ts | 14 ++-- .../machine/src/classes/PostMachine.spec.ts | 72 +++++++------------ 5 files changed, 72 insertions(+), 84 deletions(-) diff --git a/packages/machine/src/breakpoints.spec.ts b/packages/machine/src/breakpoints.spec.ts index 4736c3c..5dfeac2 100644 --- a/packages/machine/src/breakpoints.spec.ts +++ b/packages/machine/src/breakpoints.spec.ts @@ -203,7 +203,9 @@ describe('onPause — registry-aware filtering', () => { pm.replaceTapeWith(new Tape({ alphabet: pm.tape.alphabet, symbols: ['*', '*', ' '] })); pm.setBreakpoint('30', { before: true }); const paused: number[] = []; - await pm.run({ onPause: (s) => { paused.push(s.arrivalPath.instructionIndex); } }); + const session = pm.debugRun(); + session.on('pause', (s) => { paused.push(s.arrivalPath.instructionIndex); session.continue(); }); + await session.start(); expect(paused).toContain(30); pm.clearBreakpoints(); }); @@ -224,7 +226,9 @@ describe('onPause — registry-aware filtering', () => { pm.replaceTapeWith(new Tape({ alphabet: pm.tape.alphabet, symbols: ['*', '*', ' '] })); pm.setBreakpoint('30', { after: true }); const paused: number[] = []; - await pm.run({ onPause: (s) => { paused.push(s.arrivalPath.instructionIndex); } }); + const session = pm.debugRun(); + session.on('pause', (s) => { paused.push(s.arrivalPath.instructionIndex); session.continue(); }); + await session.start(); expect(paused).toEqual([30]); pm.clearBreakpoints(); }); @@ -243,7 +247,9 @@ describe('onPause — registry-aware filtering', () => { // but PostMachine's wrapper sees arrival=10, no registered match → silent. pm.setBreakpoint('30', { before: true }); const paused: number[] = []; - await pm.run({ onPause: (s) => { paused.push(s.arrivalPath.instructionIndex); } }); + const session = pm.debugRun(); + session.on('pause', (s) => { paused.push(s.arrivalPath.instructionIndex); session.continue(); }); + await session.start(); expect(paused).toEqual([]); pm.clearBreakpoints(); }); @@ -259,7 +265,9 @@ describe('onPause — registry-aware filtering', () => { pm.replaceTapeWith(new Tape({ alphabet: pm.tape.alphabet, symbols: ['*', '*', ' '] })); pm.setBreakpoint(haltState, { before: true }); const paused: number[] = []; - await pm.run({ onPause: () => { paused.push(1); } }); + const session = pm.debugRun(); + session.on('pause', () => { paused.push(1); session.continue(); }); + await session.start(); expect(paused.length).toBeGreaterThan(0); pm.clearBreakpoints(); }); diff --git a/packages/machine/src/classes/PostMachine.debugger.spec.ts b/packages/machine/src/classes/PostMachine.debugger.spec.ts index c9efa7c..b3ff7f9 100644 --- a/packages/machine/src/classes/PostMachine.debugger.spec.ts +++ b/packages/machine/src/classes/PostMachine.debugger.spec.ts @@ -28,11 +28,11 @@ describe('PostMachine — async run', () => { })); const result = machine.run(); - expect(result).toBeInstanceOf(Promise); - return result; // ensure jest waits for halt + // v7: run() is sync, returns void. + expect(result).toBeUndefined(); }); - test('run() resolves only after the machine halts', async () => { + test('run() is synchronous — tape is final immediately after the call', () => { const machine = buildWalkAndMark(); machine.replaceTapeWith(new Tape({ @@ -40,10 +40,10 @@ describe('PostMachine — async run', () => { symbols: ['*', '*', ' '], })); - // Before run resolves, the tape should be the input. + // Before run runs, the tape should be the input. expect(machine.tape.symbols.join('').trim()).toBe('**'); - await machine.run(); + machine.run(); expect(machine.tape.symbols.join('').trim()).toBe('***'); }); @@ -57,9 +57,7 @@ describe('PostMachine — async run', () => { })); const seen: number[] = []; - await machine.run({ - onStep: (s: MachineState) => { seen.push(s.step); }, - }); + for (const s of machine.runStepByStep()) { seen.push(s.step); } expect(seen.length).toBeGreaterThan(0); // Steps are 1-indexed and monotonically increasing. @@ -87,12 +85,12 @@ describe('PostMachine — onPause forwarding', () => { machine.initialState.debug = { before: true }; const seen: MachineState[] = []; - await machine.run({ - onPause: (s) => { seen.push(s); }, - }); + const session = machine.debugRun(); + session.on('pause', (s) => { seen.push(s); session.continue(); }); + await session.start(); expect(seen.length).toBeGreaterThan(0); - expect(seen[0].debugBreak).toEqual({ before: true }); + expect(seen[0].debugBreak).toEqual({ before: true, cause: 'breakpoint' }); }); test('run() awaits an async onPause before resolving', async () => { @@ -111,14 +109,15 @@ describe('PostMachine — onPause forwarding', () => { machine.initialState.debug = { before: true }; let asyncCallbackResolved = false; - await machine.run({ - onPause: async () => { - await new Promise((r) => setTimeout(r, 10)); - asyncCallbackResolved = true; - }, + const session = machine.debugRun(); + session.on('pause', async () => { + await new Promise((r) => setTimeout(r, 10)); + asyncCallbackResolved = true; + session.continue(); }); + await session.start(); - // If run() resolved before the async callback finished, this would be false. + // If start() resolved before the async callback finished, this would be false. expect(asyncCallbackResolved).toBe(true); }); }); diff --git a/packages/machine/src/classes/PostMachine.examples.spec.ts b/packages/machine/src/classes/PostMachine.examples.spec.ts index 4c23068..a8a5d62 100644 --- a/packages/machine/src/classes/PostMachine.examples.spec.ts +++ b/packages/machine/src/classes/PostMachine.examples.spec.ts @@ -354,12 +354,10 @@ describe('packages/machine/README.md', () => { }); const steps: { arrivalPath: Path; candidatePaths: Path[] }[] = []; - await m.run({ - onStep: (s: MachineState) => { - // console.log('at:', s.arrivalPath, 'shared with:', s.candidatePaths); - steps.push({ arrivalPath: s.arrivalPath, candidatePaths: s.candidatePaths }); - }, - }); + for (const s of m.runStepByStep()) { + // console.log('at:', s.arrivalPath, 'shared with:', s.candidatePaths); + steps.push({ arrivalPath: s.arrivalPath, candidatePaths: s.candidatePaths }); + } // onStep fires once — for the `mark` transition at instruction 10. expect(steps).toHaveLength(1); @@ -601,11 +599,12 @@ describe('packages/machine/README.md', () => { pm.setBreakpoint('30', { before: true }); const paused: number[] = []; - await pm.run({ - onPause: (m: MachineState) => { - paused.push(m.arrivalPath.instructionIndex); - }, + const session = pm.debugRun(); + session.on('pause', (m: MachineState) => { + paused.push(m.arrivalPath.instructionIndex); + session.continue(); }); + await session.start(); expect(paused).toContain(30); }); diff --git a/packages/machine/src/classes/PostMachine.machine-state.spec.ts b/packages/machine/src/classes/PostMachine.machine-state.spec.ts index 4abc566..28c8a19 100644 --- a/packages/machine/src/classes/PostMachine.machine-state.spec.ts +++ b/packages/machine/src/classes/PostMachine.machine-state.spec.ts @@ -9,7 +9,7 @@ describe('PostMachine — wrapped MachineState', () => { 20: stop, }); const seen: MachineState[] = []; - await m.run({ onStep: (s) => { seen.push(s); } }); + for (const s of m.runStepByStep()) { seen.push(s); } expect(seen.length).toBeGreaterThan(0); for (const s of seen) { expect(s.arrivalPath).toBeDefined(); @@ -23,7 +23,7 @@ describe('PostMachine — wrapped MachineState', () => { 20: stop, }); const seen: MachineState[] = []; - await m.run({ onIter: async (s) => { seen.push(s); } }); + for (const s of m.runStepByStep()) { seen.push(s); } expect(seen.length).toBeGreaterThan(0); for (const s of seen) { expect(s.arrivalPath).toBeDefined(); @@ -39,7 +39,7 @@ describe('PostMachine — wrapped MachineState', () => { 20: stop, }); const seen: MachineState[] = []; - await m.run({ onStep: (s) => { seen.push(s); } }); + for (const s of m.runStepByStep()) { seen.push(s); } expect(seen[0].arrivalPath).toEqual(parsePath('10')); }); @@ -49,7 +49,7 @@ describe('PostMachine — wrapped MachineState', () => { 20: stop, }); const seen: MachineState[] = []; - await m.run({ onStep: (s) => { seen.push(s); } }); + for (const s of m.runStepByStep()) { seen.push(s); } expect(seen[0].candidatePaths).toEqual([parsePath('10')]); }); @@ -62,7 +62,7 @@ describe('PostMachine — wrapped MachineState', () => { 30: stop, }); const seen: MachineState[] = []; - await m.run({ onStep: (s) => { seen.push(s); } }); + for (const s of m.runStepByStep()) { seen.push(s); } expect(seen[0].candidatePaths.length).toBe(2); expect(seen[0].candidatePaths.map(p => p.instructionIndex)).toEqual([10, 20]); }); @@ -76,7 +76,7 @@ describe('PostMachine — wrapped MachineState', () => { foo: { 1: right, 2: mark }, }); const seen: MachineState[] = []; - await m.run({ onStep: (s) => { seen.push(s); } }); + for (const s of m.runStepByStep()) { seen.push(s); } // After the call wrapper executes foo::1, control reaches foo::2. const fooStep = seen.find(s => { const scope = s.arrivalPath.scope; @@ -90,7 +90,7 @@ describe('PostMachine — wrapped MachineState', () => { 50: [right, mark], }); const seen: MachineState[] = []; - await m.run({ onStep: (s) => { seen.push(s); } }); + for (const s of m.runStepByStep()) { seen.push(s); } // The second inner (mark) fires at 50.2 — the first inner (right) is wrapped // by withOverriddenHaltState and therefore resolves to the outer group path {50} // rather than {50.1}. The second inner state is unambiguously tagged 50.2. diff --git a/packages/machine/src/classes/PostMachine.spec.ts b/packages/machine/src/classes/PostMachine.spec.ts index dcdf838..4d13993 100644 --- a/packages/machine/src/classes/PostMachine.spec.ts +++ b/packages/machine/src/classes/PostMachine.spec.ts @@ -385,7 +385,7 @@ describe('run tests', () => { const exactStepCount = 49; - await machine.run({ stepsLimit: exactStepCount, onStep: () => onStepMock() }); + for (const _ of machine.runStepByStep({ stepsLimit: exactStepCount })) { void _; onStepMock(); } expect(machine.tape.symbols.join('').trim()).toBe('****'); expect(onStepMock).toHaveBeenCalledTimes(exactStepCount); @@ -411,9 +411,9 @@ describe('run tests', () => { const onStepMock2 = vi.fn(); const onStepMock3 = vi.fn(); - await expect(machine1.run({ stepsLimit: 1, onStep: (stepData) => onStepMock1(stepData) })).resolves.toBeUndefined(); - await expect(machine2.run({ stepsLimit: 1, onStep: (stepData) => onStepMock2(stepData) })).resolves.toBeUndefined(); - await expect(machine3.run({ stepsLimit: 1, onStep: (stepData) => onStepMock3(stepData) })).resolves.toBeUndefined(); + for (const s of machine1.runStepByStep({ stepsLimit: 1 })) onStepMock1(s); + for (const s of machine2.runStepByStep({ stepsLimit: 1 })) onStepMock2(s); + for (const s of machine3.runStepByStep({ stepsLimit: 1 })) onStepMock3(s); expect(onStepMock1).toHaveBeenCalledTimes(1); expect(onStepMock2).toHaveBeenCalledTimes(1); expect(onStepMock3).toHaveBeenCalledTimes(1); @@ -451,18 +451,9 @@ describe('run tests', () => { const onStepMock2 = vi.fn(); const onStepMock3 = vi.fn(); - await expect(machine1.run({ - stepsLimit: 3, - onStep: (...args) => onStepMock1(...args), - })).resolves.toBeUndefined(); - await expect(machine2.run({ - stepsLimit: 3, - onStep: (...args) => onStepMock2(...args), - })).resolves.toBeUndefined(); - await expect(machine3.run({ - stepsLimit: 3, - onStep: (...args) => onStepMock3(...args), - })).resolves.toBeUndefined(); + for (const s of machine1.runStepByStep({ stepsLimit: 3 })) onStepMock1(s); + for (const s of machine2.runStepByStep({ stepsLimit: 3 })) onStepMock2(s); + for (const s of machine3.runStepByStep({ stepsLimit: 3 })) onStepMock3(s); // 2 iters: wrapper-of-noop fires once, then post-iter halt dispatch. // (Acyclic + plain-first-instruction subroutine wraps the body directly, // no hopper iter.) @@ -490,7 +481,7 @@ describe('run tests', () => { [ixList[3]]: call(subroutineName), }); - await expect(machine.run()).resolves.toBeUndefined(); + expect(() => machine.run()).not.toThrow(); expect(machine.tape.symbols.join('').trim()) .toBe('***'); @@ -548,7 +539,7 @@ describe('run tests', () => { alphabet: machine.tape.alphabet, symbols: '*** *'.split(''), })); - await expect(machine.run()).resolves.toBeUndefined(); + expect(() => machine.run()).not.toThrow(); } expect(machineList.map((machine) => machine.tape.symbols.join('').trim())) .toEqual(machineList.map((_, ix) => ( @@ -578,9 +569,7 @@ describe('run tests', () => { const onStepMock = vi.fn(); - await expect(machine.run({ - onStep: (...args) => onStepMock(...args), - })).resolves.toBeUndefined(); + for (const s of machine.runStepByStep()) onStepMock(s); // Outer `subroutineNameList[0]` keeps its hopper — the static analyzer // sees its body calling 'sub0' as a lexical self-reference and @@ -615,15 +604,12 @@ describe('run tests', () => { const machine1OnStepMock = vi.fn(); const machine2OnStepMock = vi.fn(); - await expect(machine1.run({ - stepsLimit: 3, - onStep: (...args) => machine1OnStepMock(...args), - })).rejects.toThrow('Long execution'); - - await expect(machine2.run({ - stepsLimit: 3, - onStep: (...args) => machine2OnStepMock(...args), - })).rejects.toThrow('Long execution'); + expect(() => { + for (const s of machine1.runStepByStep({ stepsLimit: 3 })) machine1OnStepMock(s); + }).toThrow('Long execution'); + expect(() => { + for (const s of machine2.runStepByStep({ stepsLimit: 3 })) machine2OnStepMock(s); + }).toThrow('Long execution'); const machine1StateIdList = machine1OnStepMock.mock.calls.map((args) => args[0].state.id); const machine2StateIdList = machine2OnStepMock.mock.calls.map((args) => args[0].state.id); @@ -660,15 +646,12 @@ describe('run tests', () => { const machine1OnStepMock = vi.fn(); const machine2OnStepMock = vi.fn(); - await expect(machine1.run({ - stepsLimit: 10, - onStep: (...args) => machine1OnStepMock(...args), - })).rejects.toThrow('Long execution'); - - await expect(machine2.run({ - stepsLimit: 10, - onStep: (...args) => machine2OnStepMock(...args), - })).rejects.toThrow('Long execution'); + expect(() => { + for (const s of machine1.runStepByStep({ stepsLimit: 10 })) machine1OnStepMock(s); + }).toThrow('Long execution'); + expect(() => { + for (const s of machine2.runStepByStep({ stepsLimit: 10 })) machine2OnStepMock(s); + }).toThrow('Long execution'); const regExp = /\(/; const machine1StateIdList = machine1OnStepMock.mock.calls @@ -698,10 +681,9 @@ describe('run tests', () => { }); const machineOnStepMock = vi.fn(); - await expect(machine.run({ - stepsLimit: 10, - onStep: (...args) => machineOnStepMock(...args), - })).rejects.toThrow('Long execution'); + expect(() => { + for (const s of machine.runStepByStep({ stepsLimit: 10 })) machineOnStepMock(s); + }).toThrow('Long execution'); const machine1StateIdList = machineOnStepMock.mock.calls .map((args) => args[0].state.id); @@ -840,7 +822,7 @@ describe('run tests', () => { [ixList[2]]: erase, }); - await expect(machine.run()).resolves.toBeUndefined(); + expect(() => machine.run()).not.toThrow(); expect(machine.tape.symbols.join('').trim()) .toBe('**'); @@ -861,7 +843,7 @@ describe('run tests', () => { ], }); - await expect(machine.run()).resolves.toBeUndefined(); + expect(() => machine.run()).not.toThrow(); expect(machine.tape.symbols.join('').trim()) .toBe('* *'); From 44517b7474a59d950ab9073f4f8370c413ec4420 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Thu, 28 May 2026 22:35:55 +0300 Subject: [PATCH 20/34] adopt(post): engine pause descriptor {side,cause}; preserve internal accessor through wrap (#97) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Engine v7 moved breakpoint detection into DebugSession and reshaped the pause descriptor to `m.pause: {side, cause}` (was `m.debugBreak: {before?, after?, cause}`). PostDebugSession adoption: - #shouldFire reads `raw.pause.cause` (was `raw.debugBreak?.cause`); param typed `EnginePausedMachineState`. - New `PostPausedMachineState = MachineState & {pause}` for the post `pause` listener; the pause wire-up carries `raw.pause` onto the post-wrapped state. - debugger spec asserts `s.pause.{side,cause}`. Critical fix: PostMachine.runStepByStep's #wrapMachineState previously spread `{...raw}`, which silently dropped the engine's non-enumerable MACHINE_STATE_INTERNAL accessor. The engine's DebugSession reads that accessor (matched symbol + halt-imminence) to do detection — so once detection moved out of the generator, post's pause events stopped firing entirely. The Symbol is package-private (can't re-attach), so we now mutate `raw` in place (a fresh per-iter object) to add arrivalPath / candidatePaths while preserving the accessor. 315 tests pass. --- .../machine/src/classes/PostDebugSession.ts | 23 +++++++++++++++---- .../src/classes/PostMachine.debugger.spec.ts | 6 ++--- packages/machine/src/classes/PostMachine.ts | 12 +++++++++- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/packages/machine/src/classes/PostDebugSession.ts b/packages/machine/src/classes/PostDebugSession.ts index da9673e..8ccec4d 100644 --- a/packages/machine/src/classes/PostDebugSession.ts +++ b/packages/machine/src/classes/PostDebugSession.ts @@ -1,6 +1,8 @@ import { DebugSession as EngineDebugSession, type MachineState as EngineMachineState, + type PauseInfo, + type PausedMachineState as EnginePausedMachineState, State, haltState, } from '@turing-machine-js/machine'; @@ -11,13 +13,19 @@ import type { PostMachine } from './PostMachine'; export type PostDebugSessionEvent = 'pause' | 'step' | 'iter' | 'halt'; +/** A post-wrapped `MachineState` (arrivalPath / candidatePaths) plus the + * engine's one-sided pause descriptor — the payload of a `pause` event. */ +export type PostPausedMachineState = MachineState & { pause: PauseInfo }; + export type PostDebugSessionListener = E extends 'halt' ? () => void | Promise - : (machineState: MachineState) => void | Promise; + : E extends 'pause' + ? (machineState: PostPausedMachineState) => void | Promise + : (machineState: MachineState) => void | Promise; type ListenerMap = { - pause: Array<(m: MachineState) => void | Promise>; + pause: Array<(m: PostPausedMachineState) => void | Promise>; step: Array<(m: MachineState) => void | Promise>; iter: Array<(m: MachineState) => void | Promise>; halt: Array<() => void | Promise>; @@ -73,7 +81,12 @@ export class PostDebugSession { for (const fn of this.#listeners.step) void fn(wrapped); }); this.#engineSession.on('pause', (raw) => { - const wrapped = this.#wrap(raw, this.#prevState, this.#prevJsSymbol); + // raw is the engine's PausedMachineState — carry its `pause` descriptor + // onto the post-wrapped state so post pause listeners see {side, cause}. + const wrapped: PostPausedMachineState = { + ...this.#wrap(raw, this.#prevState, this.#prevJsSymbol), + pause: raw.pause, + }; // Apply post-machine breakpoint registry filter — fire only when the // engine pause was triggered by a registered breakpoint (or by a // step-mode endpoint / manual pause). @@ -148,8 +161,8 @@ export class PostDebugSession { // listeners. Step-mode endpoints and manual pauses always pass through; // breakpoint-cause pauses are filtered against the registry so only // path-registered or halt-registered states fire. - #shouldFire(raw: EngineMachineState, wrapped: MachineState): boolean { - const cause = raw.debugBreak?.cause; + #shouldFire(raw: EnginePausedMachineState, wrapped: MachineState): boolean { + const cause = raw.pause.cause; if (cause === 'step' || cause === 'manual') { return true; } diff --git a/packages/machine/src/classes/PostMachine.debugger.spec.ts b/packages/machine/src/classes/PostMachine.debugger.spec.ts index b3ff7f9..6a6be3d 100644 --- a/packages/machine/src/classes/PostMachine.debugger.spec.ts +++ b/packages/machine/src/classes/PostMachine.debugger.spec.ts @@ -84,13 +84,13 @@ describe('PostMachine — onPause forwarding', () => { // runtime-mutable; the upstream run() loop checks it on each iter. machine.initialState.debug = { before: true }; - const seen: MachineState[] = []; + const seen: Array<{side: string; cause: string}> = []; const session = machine.debugRun(); - session.on('pause', (s) => { seen.push(s); session.continue(); }); + session.on('pause', (s) => { seen.push({side: s.pause.side, cause: s.pause.cause}); session.continue(); }); await session.start(); expect(seen.length).toBeGreaterThan(0); - expect(seen[0].debugBreak).toEqual({ before: true, cause: 'breakpoint' }); + expect(seen[0]).toEqual({ side: 'before', cause: 'breakpoint' }); }); test('run() awaits an async onPause before resolving', async () => { diff --git a/packages/machine/src/classes/PostMachine.ts b/packages/machine/src/classes/PostMachine.ts index b73e459..bcbeb3f 100644 --- a/packages/machine/src/classes/PostMachine.ts +++ b/packages/machine/src/classes/PostMachine.ts @@ -188,7 +188,17 @@ export class PostMachine extends TuringMachine { } } const candidatePaths = this.#stateToCandidatePaths.get(raw.state) ?? []; - return { ...raw, arrivalPath, candidatePaths } as MachineState; + // Attach post fields IN PLACE rather than spreading into a new object. The + // engine stamps a non-enumerable `MACHINE_STATE_INTERNAL` Symbol accessor + // on each yield (halt-stack + matched symbol + halt-imminence) that the + // engine's DebugSession reads for breakpoint detection. A spread + // (`{...raw}`) silently drops that Symbol — and it's package-private, so + // we can't re-attach it. Mutating `raw` (a fresh per-iter object the engine + // doesn't retain) preserves the accessor while adding our fields. + const wrapped = raw as MachineState; + wrapped.arrivalPath = arrivalPath; + wrapped.candidatePaths = candidatePaths; + return wrapped; } #buildInitialState({ From a1b17c7118ebd4042bcf892e4f16d532d84abc6f Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Thu, 28 May 2026 22:36:38 +0300 Subject: [PATCH 21/34] lint(post): drop now-unused MachineState import in debugger spec --- packages/machine/src/classes/PostMachine.debugger.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/machine/src/classes/PostMachine.debugger.spec.ts b/packages/machine/src/classes/PostMachine.debugger.spec.ts index 6a6be3d..eef4008 100644 --- a/packages/machine/src/classes/PostMachine.debugger.spec.ts +++ b/packages/machine/src/classes/PostMachine.debugger.spec.ts @@ -5,7 +5,6 @@ import { PostMachine, Tape, - type MachineState, check, mark, right, stop, } from '../index'; From db608a4089dac4c39dea7a5f139f8a4ab37a829e Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Thu, 28 May 2026 23:58:43 +0300 Subject: [PATCH 22/34] chore(adopt): refresh lockfile to engine alpha.6 (#97) --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index df29eb0..96e19e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2740,9 +2740,9 @@ } }, "node_modules/@turing-machine-js/machine": { - "version": "7.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@turing-machine-js/machine/-/machine-7.0.0-alpha.5.tgz", - "integrity": "sha512-OxVwi89HxvTc3H+YzBGfB6cSXkRWdkOL77T6sCAKkCn82Nxq8oTGcQUWLOR0+23s65rcpUflMQZbabtJzN5BXA==", + "version": "7.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/@turing-machine-js/machine/-/machine-7.0.0-alpha.6.tgz", + "integrity": "sha512-xHlFzcKE1e6YUeYYua3seu59it8HElbOsxBPDzhpwTmixpOgrZgSB3ENs3KaisgEdROSaytlZw/6udWYQ2lj+w==", "license": "GPL-3.0-or-later", "engines": { "npm": ">=7.0.0" From bd58f3707f6b1c90ec48358a740abdf5f7f3eee3 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Fri, 29 May 2026 00:42:42 +0300 Subject: [PATCH 23/34] docs: align README + specs with the sync run() / debugRun() API Rewrite the PostMachine method table, MachineState-shape, and Breakpoints sections for the run() (sync) / debugRun() / runStepByStep() split adopted from engine #102, fix a dangling ToC anchor, and drop the now-pointless await on the synchronous run() across both READMEs and the specs. --- README.md | 6 +-- packages/machine/README.md | 45 ++++++++++--------- .../PostMachine.custom-alphabet.spec.ts | 16 +++---- .../src/classes/PostMachine.examples.spec.ts | 10 ++--- packages/machine/src/commands.spec.ts | 2 +- test/examples.spec.ts | 6 +-- 6 files changed, 43 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index f83ecb4..fa1b418 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ machine.replaceTapeWith(new Tape({ console.log(machine.tape.symbols.join('').trim()); // *** * -await machine.run(); +machine.run(); console.log(machine.tape.symbols.join('').trim()); // **** ``` @@ -101,7 +101,7 @@ machine.replaceTapeWith(new Tape({ console.log(machine.tape.symbols.join('').trim()); // * -await machine.run(); +machine.run(); console.log(machine.tape.symbols.join('').trim()); // ** @@ -114,7 +114,7 @@ machine.replaceTapeWith(new Tape({ console.log(machine.tape.symbols.join('').trim()); // *** -await machine.run(); +machine.run(); console.log(machine.tape.symbols.join('').trim()); // ****** ``` diff --git a/packages/machine/README.md b/packages/machine/README.md index 668bb0c..60eeed7 100644 --- a/packages/machine/README.md +++ b/packages/machine/README.md @@ -20,7 +20,7 @@ A Post machine — a 2-symbol Turing-machine variant with a numbered-instruction - [Naming convention](#naming-convention) - [Tags](#tags) — [`$tag` decorator](#inline-tag-decorator) · [Registry](#post-construction-registry) · [Auto-tag policy](#auto-tag-policy) · [Mermaid output](#mermaid-output) - [Introspection and equivalence](#introspection-and-equivalence) — [Visualization](#visualization--tomermaid--statetograph) · [Structural summary](#structural-summary--summarizepostmachine) · [Behavioral equivalence](#behavioral-equivalence--equivalentpostmachines) -- [Debugging](#debugging) +- [Breakpoints](#breakpoints) - [Links](#links)
@@ -52,7 +52,7 @@ machine.replaceTapeWith(new Tape({ symbols: ['*', '*', ' '], })); -await machine.run(); +machine.run(); console.log(machine.tape.symbols.join('').trim()); // *** ``` @@ -101,8 +101,9 @@ The runtime. Subclasses `TuringMachine` from `@turing-machine-js/machine`: the c **Constructor.** `new PostMachine(instructions, options?)` — `instructions` is the numbered-instruction map (with optional string-keyed subroutine groups); `options` is `{ blankSymbol?, markSymbol? }` (see [Custom symbols](#custom-symbols)). **Methods.** -- `run({ stepsLimit?, onStep?, onPause? } = {})` → `Promise`. Runs to halt or until `stepsLimit` (default `1e5`) is exhausted. `onStep(m: MachineState)` fires once per applied transition; `onPause` forwards to the engine's debugger (see [Debugging](#debugging)). -- `runStepByStep({ stepsLimit? } = {})` → `Generator`. Synchronous step-at-a-time execution; the consumer drives the loop with `for ... of` or `.next()`. +- `run({ stepsLimit? } = {})` → `void`. Runs to halt or until `stepsLimit` (default `1e5`) is exhausted. **Synchronous and callback-free as of v7.0.0-alpha.6** (adopting engine [#102](https://github.com/mellonis/turing-machine-js/issues/102)) — for per-step observation use `runStepByStep()`; for breakpoints, throttling, or interactive stepping use `debugRun()`. +- `debugRun({ stepsLimit? } = {})` → `PostDebugSession`. An interactive debugger session bound to this machine (see [Breakpoints](#breakpoints)). It emits `pause` / `step` / `iter` / `halt` events (`session.on(event, listener)`) and exposes `continue()` / `stepIn()` / `stepOver()` / `stepOut()` / `pause()` / `stop()` / `setRunInterval(ms)`; call `await session.start()` to begin. `pause`-event payloads carry `m.pause: { side: 'before' | 'after', cause: 'breakpoint' | 'step' | 'manual' }`. +- `runStepByStep({ stepsLimit? } = {})` → `Generator`. Synchronous step-at-a-time execution; the consumer drives the loop with `for ... of` or `.next()`. Pure iteration — it does no breakpoint detection (that lives in `debugRun()`). - `replaceTapeWith(newTape)` — swap the active tape. Build the new tape against `machine.tape.alphabet` so symbol identities match the machine's interned alphabet. **Properties.** @@ -146,7 +147,7 @@ machine.replaceTapeWith(new Tape({ symbols: ['#', '#', '.'], })); -await machine.run(); +machine.run(); console.log(machine.tape.symbols.join('').replace(/\.+$/, '')); // ### ``` @@ -281,7 +282,7 @@ machine.replaceTapeWith(new Tape({ position: 0, })); -await machine.run(); +machine.run(); console.log(machine.tape.symbols.join('').trim()); // ** ``` @@ -314,7 +315,7 @@ machine.replaceTapeWith(new Tape({ symbols: ['*', '*', ' '], })); -await machine.run(); +machine.run(); console.log(machine.tape.symbols.join('').trim()); // *** ``` @@ -388,7 +389,7 @@ extend.replaceTapeWith(new Tape({ position: 1, })); -await extend.run(); +extend.run(); console.log(extend.tape.symbols.join('')); // *** ``` @@ -398,7 +399,7 @@ For a single subroutine called from MULTIPLE sites — the other archetypal use ## MachineState shape -PostMachine's `onStep` and `onPause` callbacks receive an extended `MachineState` with two additional fields: +Every `MachineState` PostMachine yields — from `runStepByStep()` and from `debugRun()` session events (`step` / `iter` / `pause`) — is an extended `MachineState` with two additional fields: | Field | Type | Meaning | |-------------------|----------|------------------------------------------------------------------------------------------| @@ -417,11 +418,9 @@ const m = new PostMachine({ 20: stop, }); -await m.run({ - onStep: (s) => { - console.log('at:', s.arrivalPath, 'shared with:', s.candidatePaths); - }, -}); +for (const s of m.runStepByStep()) { + console.log('at:', s.arrivalPath, 'shared with:', s.candidatePaths); +} ``` The `Path` type and the `parsePath`/`formatPath` helpers are exported from `@post-machine-js/machine` — see the [Naming convention](#naming-convention) section for the path-string format. @@ -477,7 +476,7 @@ const m = new PostMachine({ PostMachine caches state nodes by command shape, so two instructions producing structurally-identical transitions (same command kind, same next-instruction target) share a single underlying `State` object. The shared state carries the name of the *first-processed* instruction. Behavior is identical regardless of which instruction control arrives through, but `MachineState.name` may report the canonical instruction's name rather than the caller's instruction index. -For programmatic lookup by instruction index, use `pm.candidatesFor(path)` (construction-time) or read `MachineState.candidatePaths` from an `onStep` / `onPause` callback (runtime). See [Path-based resolver](#path-based-resolver) and [MachineState shape](#machinestate-shape). +For programmatic lookup by instruction index, use `pm.candidatesFor(path)` (construction-time) or read `MachineState.candidatePaths` from a `runStepByStep()` yield or a `debugRun()` session event (runtime). See [Path-based resolver](#path-based-resolver) and [MachineState shape](#machinestate-shape). ### Engine v7 alignment @@ -861,11 +860,13 @@ pm.replaceTapeWith(new Tape({ alphabet: pm.tape.alphabet, symbols: ['*', '*', ' pm.setBreakpoint('30', { before: true }); -await pm.run({ - onPause: (m) => { - // m.arrivalPath === { instructionIndex: 30 } - }, +const session = pm.debugRun(); +session.on('pause', (m) => { + // m.arrivalPath === { instructionIndex: 30 } + // m.pause === { side: 'before', cause: 'breakpoint' } + session.continue(); }); +await session.start(); ``` Filters mirror the engine's `DebugConfig`: @@ -883,7 +884,7 @@ Halt breakpoints: pm.setBreakpoint(haltState, { before: true }); // pause at halt entry (filter shape is decorative) ``` -> Engine [#207](https://github.com/mellonis/turing-machine-js/issues/207) collapsed `haltState.debug` to a `boolean` — halt has one meaningful pause moment. The `filter` shape passed to `pm.setBreakpoint(haltState, …)` is kept for API stability but is now decorative: any registered halt breakpoint enables the engine-level boolean, and the registry entry drives only the arrival-path filtering in PostMachine's `onPause` wrapper. The pause fires on the AFTER side of the iter whose transition leads to halt. +> Engine [#207](https://github.com/mellonis/turing-machine-js/issues/207) collapsed `haltState.debug` to a `boolean` — halt has one meaningful pause moment. The `filter` shape passed to `pm.setBreakpoint(haltState, …)` is kept for API stability but is now decorative: any registered halt breakpoint enables the engine-level boolean, and the registry entry drives only the arrival-path filtering in the `debugRun()` session's pause filter. The pause fires on the AFTER side of the iter whose transition leads to halt. Management: @@ -893,7 +894,7 @@ pm.clearBreakpoint('10'); // remove a single registration pm.clearBreakpoints(); // remove all ``` -**State sharing.** When two instructions share an underlying State (hash dedup), setting a breakpoint on instruction 30 enables `state.debug` on the shared State — meaning the engine pauses on every visit. PostMachine's `onPause` wrapper consults the registry and *only* surfaces the pause when `m.arrivalPath` matches a registered path. Sibling-instruction visits silently resume. +**State sharing.** When two instructions share an underlying State (hash dedup), setting a breakpoint on instruction 30 enables `state.debug` on the shared State — meaning the engine pauses on every visit. The `debugRun()` session consults the registry and *only* surfaces the pause when `m.arrivalPath` matches a registered path. Sibling-instruction visits auto-continue. ### Lockdown semantics @@ -920,7 +921,7 @@ haltState.debug = { before: true }; // throws: "haltState.debug Direct halt writes bypass PostMachine's registry — they enable the engine pause but `pm.listBreakpoints()` won't record them. Use `pm.setBreakpoint(haltState, …)` when arrival-path filtering or registry awareness matters; use the direct write for ad-hoc halt-pause toggling in tools that don't need the registry. -This relaxed model preserves the single-channel invariant where it matters: `pm.listBreakpoints()` is still the source of truth for what PostMachine's `onPause` wrapper surfaces. The engine's pause itself is now an open channel — by design, since the halt-lockdown's "per-PostMachine routing" benefit was syntactic only (haltState is a process-global singleton). +This relaxed model preserves the single-channel invariant where it matters: `pm.listBreakpoints()` is still the source of truth for what the `debugRun()` session surfaces. The engine's pause itself is now an open channel — by design, since the halt-lockdown's "per-PostMachine routing" benefit was syntactic only (haltState is a process-global singleton). For the underlying engine reference — filter shapes for non-halt states, the `before → step → after` per-iter lifecycle (engine v6), and the boolean `haltState.debug` API (engine #207) — see [Debugging breakpoints](https://github.com/mellonis/turing-machine-js/tree/master/packages/machine#debugging-breakpoints) in the upstream README. diff --git a/packages/machine/src/classes/PostMachine.custom-alphabet.spec.ts b/packages/machine/src/classes/PostMachine.custom-alphabet.spec.ts index 1bfb2c0..a817413 100644 --- a/packages/machine/src/classes/PostMachine.custom-alphabet.spec.ts +++ b/packages/machine/src/classes/PostMachine.custom-alphabet.spec.ts @@ -82,7 +82,7 @@ describe('PostMachine custom alphabet', () => { symbols: ['.'], })); - await machine.run(); + machine.run(); expect(machine.tape.symbols.join('')).toBe('#'); }); @@ -98,7 +98,7 @@ describe('PostMachine custom alphabet', () => { symbols: ['#'], })); - await machine.run(); + machine.run(); expect(machine.tape.symbols.join('')).toBe('.'); }); @@ -121,7 +121,7 @@ describe('PostMachine custom alphabet', () => { symbols: ['#', '#', '.'], })); - await machine.run(); + machine.run(); expect(machine.tape.symbols.join('').replace(/\.+$/, '')).toBe('###'); }); @@ -141,7 +141,7 @@ describe('PostMachine custom alphabet', () => { position: 0, })); - await machine.run(); + machine.run(); // head moved right by one; tape contents unchanged expect(machine.tape.symbols.join('')).toBe('#.#'); @@ -167,7 +167,7 @@ describe('PostMachine custom alphabet', () => { symbols: ['#', '#', '.'], })); - await machine.run(); + machine.run(); expect(machine.tape.symbols.join('').replace(/\.+$/, '')).toBe('###'); }); @@ -187,7 +187,7 @@ describe('PostMachine custom alphabet', () => { symbols: ['.', '.'], })); - await machine.run(); + machine.run(); expect(machine.tape.symbols.join('')).toBe('#.'); }); @@ -225,7 +225,7 @@ describe('PostMachine custom alphabet', () => { symbols: ['•', '•', '␣'], })); - await machine.run(); + machine.run(); expect(machine.tape.symbols.join('').replace(/␣+$/, '')).toBe('•••'); }); @@ -244,7 +244,7 @@ describe('PostMachine custom alphabet', () => { position: 1, })); - await machine.run(); + machine.run(); // After one left move, head sits at position 0; symbols unchanged. expect(machine.tape.symbols.join('')).toBe('##'); diff --git a/packages/machine/src/classes/PostMachine.examples.spec.ts b/packages/machine/src/classes/PostMachine.examples.spec.ts index a8a5d62..fab6e77 100644 --- a/packages/machine/src/classes/PostMachine.examples.spec.ts +++ b/packages/machine/src/classes/PostMachine.examples.spec.ts @@ -53,7 +53,7 @@ describe('packages/machine/README.md', () => { symbols: ['*', '*', ' '], })); - await machine.run(); + machine.run(); // console.log(machine.tape.symbols.join('').trim()); // *** expect(machine.tape.symbols.join('').trim()) @@ -149,7 +149,7 @@ describe('packages/machine/README.md', () => { position: 0, })); - await machine.run(); + machine.run(); // console.log(machine.tape.symbols.join('').trim()); // ** expect(machine.tape.symbols.join('').trim()) @@ -267,7 +267,7 @@ describe('packages/machine/README.md', () => { symbols: ['*', '*', ' '], })); - await machine.run(); + machine.run(); // console.log(machine.tape.symbols.join('').trim()); // *** expect(machine.tape.symbols.join('').trim()) @@ -299,7 +299,7 @@ describe('packages/machine/README.md', () => { position: 1, })); - await extend.run(); + extend.run(); // console.log(extend.tape.symbols.join('')); // *** expect(extend.tape.symbols.join('')) @@ -324,7 +324,7 @@ describe('packages/machine/README.md', () => { symbols: ['#', '#', '.'], })); - await machine.run(); + machine.run(); // console.log(machine.tape.symbols.join('').replace(/\.+$/, '')); // ### expect(machine.tape.symbols.join('').replace(/\.+$/, '')) diff --git a/packages/machine/src/commands.spec.ts b/packages/machine/src/commands.spec.ts index c487a8c..691dbd5 100644 --- a/packages/machine/src/commands.spec.ts +++ b/packages/machine/src/commands.spec.ts @@ -144,7 +144,7 @@ describe('$tag — inline tag decorator (#86)', () => { }); // The tag doesn't affect runtime — the machine still halts normally. - await machine.run(); + machine.run(); expect(machine.tape.symbols[0]).toBe('*'); }); diff --git a/test/examples.spec.ts b/test/examples.spec.ts index 7e2caa6..2734ab5 100644 --- a/test/examples.spec.ts +++ b/test/examples.spec.ts @@ -31,7 +31,7 @@ describe('README.md', () => { expect(machine.tape.symbols.join('').trim()) .toBe('*** *'); - await machine.run(); + machine.run(); expect(machine.tape.symbols.join('').trim()) .toBe('****'); @@ -77,7 +77,7 @@ describe('README.md', () => { expect(machine.tape.symbols.join('').trim()) .toBe('*'); - await machine.run(); + machine.run(); expect(machine.tape.symbols.join('').trim()) .toBe('**'); @@ -92,7 +92,7 @@ describe('README.md', () => { expect(machine.tape.symbols.join('').trim()) .toBe('***'); - await machine.run(); + machine.run(); expect(machine.tape.symbols.join('').trim()) .toBe('******'); From e890960f7eec38229e77d5d1b41eeecb36908a1e Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Fri, 29 May 2026 00:42:42 +0300 Subject: [PATCH 24/34] chore(release): 7.0.0-alpha.6 --- lerna.json | 2 +- package-lock.json | 6 ++--- packages/machine/CHANGELOG.md | 45 +++++++++++++++++++++++++++++++++++ packages/machine/package.json | 6 ++--- 4 files changed, 52 insertions(+), 7 deletions(-) diff --git a/lerna.json b/lerna.json index 6b9d6ee..33d3137 100644 --- a/lerna.json +++ b/lerna.json @@ -3,6 +3,6 @@ "packages": [ "packages/*" ], - "version": "7.0.0-alpha.5", + "version": "7.0.0-alpha.6", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 96e19e0..cb6e64e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10441,16 +10441,16 @@ }, "packages/machine": { "name": "@post-machine-js/machine", - "version": "7.0.0-alpha.5", + "version": "7.0.0-alpha.6", "license": "GPL-3.0-or-later", "devDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.5" + "@turing-machine-js/machine": "^7.0.0-alpha.6" }, "engines": { "npm": ">=7.0.0" }, "peerDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.5" + "@turing-machine-js/machine": "^7.0.0-alpha.6" } } } diff --git a/packages/machine/CHANGELOG.md b/packages/machine/CHANGELOG.md index fb02d35..25462ea 100644 --- a/packages/machine/CHANGELOG.md +++ b/packages/machine/CHANGELOG.md @@ -4,6 +4,51 @@ All notable changes to this project will be documented in this file. 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). +## [7.0.0-alpha.6] - 2026-05-29 + +Adopts engine **v7.0.0-alpha.6** ([turing-machine-js#102](https://github.com/mellonis/turing-machine-js/issues/102)) — the debug-surface reshape. `PostMachine.run()` becomes synchronous and callback-free, a new `PostMachine.debugRun()` returns an interactive `PostDebugSession`, and the per-yield `m.debugBreak` is replaced by the engine's one-sided `m.pause: { side, cause }`. Published under the `next` dist-tag: `npm install @post-machine-js/machine@next`. + +> **Post alpha numbering is independent from the engine.** This `alpha.6` happens to match the engine `alpha.6` it adopts, but the two cycles are not lockstep — this is the second number coincidence after `alpha.5` (2026-05-25). Don't infer a lockstep relationship from the matching numbers. + +**Pre-release — the API surface may still shift before stable v7.0.0.** Pin to a specific alpha for reproducibility: `@post-machine-js/machine@7.0.0-alpha.6`. + +### Added + +- **`PostMachine.debugRun({ stepsLimit? })` → `PostDebugSession`** — the interactive debugger surface. Wraps the engine's `DebugSession`, re-adds the post-level `MachineState` fields (`arrivalPath` / `candidatePaths`), and applies the per-instruction breakpoint registry as a pause filter. Emits `pause` / `step` / `iter` / `halt` events (`session.on(event, listener)`); drive with `continue()` / `stepIn()` / `stepOver()` / `stepOut()` / `pause()` / `stop()` / `setRunInterval(ms)`; call `await session.start()` to begin. +- **`PostPausedMachineState`** — `MachineState & { pause: PauseInfo }`, the `pause`-event payload. `PauseInfo = { side: 'before' | 'after', cause: 'breakpoint' | 'step' | 'manual' }` (re-exported transitively from the engine). + +### Changed + +- **`PostMachine.run()` is synchronous and callback-free** — `run({ stepsLimit? }): void` (was `async … : Promise` accepting `onStep` / `onPause`). Mirrors the engine's `run()` change. **Breaking** for callers awaiting `run()` or passing callbacks — move per-step observation to `runStepByStep()` and breakpoints / stepping to `debugRun()`. + + ```js + // before (alpha.5): + await pm.run({ onPause: (m) => { /* m.debugBreak */ } }); + + // alpha.6: + const session = pm.debugRun(); + session.on('pause', (m) => { /* m.pause: { side, cause } */ session.continue(); }); + await session.start(); + ``` + +- **`runStepByStep()` is the pure-iteration observation path** — unchanged in shape (`Generator`), but it's now where you read `arrivalPath` / `candidatePaths` per step (the role the removed `onStep` callback played). + + ```js + for (const m of pm.runStepByStep()) { + // m.arrivalPath, m.candidatePaths + } + ``` + +- **Engine peer dependency widened** `^7.0.0-alpha.5` → `^7.0.0-alpha.6` — `debugRun()` requires the engine's `DebugSession`, new in engine alpha.6. + +### Removed + +- **`onStep` / `onPause` callbacks on `run()`** and the per-yield **`m.debugBreak`** descriptor — replaced by `debugRun()` events + `m.pause`. **Breaking from alpha.5.** + +### Docs + +- README: rewrote the `PostMachine` method table, MachineState-shape, and Breakpoints sections for the `run()` / `debugRun()` / `runStepByStep()` split; dropped the now-pointless `await` on the synchronous `run()` throughout the examples. + ## [7.0.0-alpha.5] - 2026-05-25 Fifth v7 pre-release. Drops the module-load haltState lockdown in lockstep with engine [#207](https://github.com/mellonis/turing-machine-js/issues/207) — the lockdown was funneling per-side `DebugConfig` writes through `withLockdownEscape`, but with engine alpha.5 collapsing `haltState.debug` to a `boolean`, there's nothing to mediate. Engine peer-dep widened `^7.0.0-alpha.4` → `^7.0.0-alpha.5`; consumers inherit per-iter `MachineState.matchedTransition` ([engine #205](https://github.com/mellonis/turing-machine-js/issues/205)) and the `GraphTransition.id` separator change (`-` → `.`, same issue) transparently. Published to npm under the `next` dist-tag: `npm install @post-machine-js/machine@next`. diff --git a/packages/machine/package.json b/packages/machine/package.json index 96f874e..c9d3012 100644 --- a/packages/machine/package.json +++ b/packages/machine/package.json @@ -1,6 +1,6 @@ { "name": "@post-machine-js/machine", - "version": "7.0.0-alpha.5", + "version": "7.0.0-alpha.6", "description": "A convenient Post machine", "engines": { "npm": ">=7.0.0" @@ -28,10 +28,10 @@ "machine" ], "peerDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.5" + "@turing-machine-js/machine": "^7.0.0-alpha.6" }, "devDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.5" + "@turing-machine-js/machine": "^7.0.0-alpha.6" }, "main": "dist/index.cjs", "module": "dist/index.mjs", From 3e676397d6a9df9ff93aed18e7c8822ffd041211 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Fri, 29 May 2026 00:59:47 +0300 Subject: [PATCH 25/34] test: restore 100% coverage for PostDebugSession (step controls + lifecycle) #100 shipped PostDebugSession without coverage for its step controls and lifecycle methods; v7-targeted PRs skip CI, so the gap only surfaced on the v7 -> master integration PR (#92), whose build fails the 100/100/100/100 floor. Cover stepIn/stepOver/stepOut, external pause()/stop(), setRunInterval, off() (incl. the unregistered-listener no-op), the halt-breakpoint #shouldFire path, and the step/halt listener-dispatch bodies. --- .../src/classes/PostMachine.debugger.spec.ts | 142 +++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/packages/machine/src/classes/PostMachine.debugger.spec.ts b/packages/machine/src/classes/PostMachine.debugger.spec.ts index eef4008..2564386 100644 --- a/packages/machine/src/classes/PostMachine.debugger.spec.ts +++ b/packages/machine/src/classes/PostMachine.debugger.spec.ts @@ -5,7 +5,8 @@ import { PostMachine, Tape, - check, mark, right, stop, + haltState, + call, check, mark, right, stop, } from '../index'; describe('PostMachine — async run', () => { @@ -120,3 +121,142 @@ describe('PostMachine — onPause forwarding', () => { expect(asyncCallbackResolved).toBe(true); }); }); + +describe('PostDebugSession — step controls and lifecycle', () => { + test('stepIn() forces a step-cause pause on the next instruction', async () => { + const machine = new PostMachine({ 10: mark, 20: mark, 30: stop }); + machine.setBreakpoint('10', { before: true }); + + const causes: string[] = []; + const session = machine.debugRun(); + let first = true; + session.on('pause', (m) => { + causes.push(m.pause.cause); + if (first) { first = false; session.stepIn(); } else { session.continue(); } + }); + await session.start(); + + expect(causes[0]).toBe('breakpoint'); + expect(causes[1]).toBe('step'); + }); + + test('stepOver() runs a called subroutine to completion, then pauses (cause: step)', async () => { + const machine = new PostMachine({ + 10: call('foo'), + 20: mark, + 30: stop, + foo: { 1: mark, 2: stop }, + }); + machine.setBreakpoint('10', { before: true }); + + const causes: string[] = []; + const session = machine.debugRun(); + let first = true; + session.on('pause', (m) => { + causes.push(m.pause.cause); + if (first) { first = false; session.stepOver(); } else { session.continue(); } + }); + await session.start(); + + expect(causes[0]).toBe('breakpoint'); + expect(causes).toContain('step'); + }); + + test('stepOut() from inside a subroutine pops the frame back to the caller level', async () => { + const machine = new PostMachine({ + 10: call('foo'), + 20: mark, + 30: stop, + foo: { 1: mark, 2: mark, 3: stop }, + }); + machine.setBreakpoint('10', { before: true }); + + const causes: string[] = []; + let phase = 0; + const session = machine.debugRun(); + session.on('pause', (m) => { + causes.push(m.pause.cause); + if (phase === 0) { phase = 1; session.stepIn(); } // descend into foo (depth >= 1) + else if (phase === 1) { phase = 2; session.stepOut(); } // pop foo's frame back to the caller + else { session.continue(); } + }); + await session.start(); + + expect(causes[0]).toBe('breakpoint'); + expect(causes.length).toBeGreaterThanOrEqual(2); + expect(causes.slice(1)).toContain('step'); + }); + + test('external pause() fires a manual-cause pause; setRunInterval throttles the run', async () => { + const machine = new PostMachine({ 10: mark, 20: mark, 30: mark, 40: stop }); + const session = machine.debugRun(); + session.setRunInterval(2); // slow enough to request a pause mid-run + + let requested = false; + const causes: string[] = []; + session.on('iter', (m) => { + if (m.step === 1 && !requested) { requested = true; session.pause(); } + }); + session.on('pause', (m) => { + causes.push(m.pause.cause); + session.continue(); + }); + await session.start(); + + expect(causes).toEqual(['manual']); + }); + + test('stop() from an iter listener terminates without firing halt', async () => { + const machine = new PostMachine({ 10: mark, 20: mark, 30: mark, 40: stop }); + const session = machine.debugRun(); + + let haltFired = false; + let stopped = false; + session.on('halt', () => { haltFired = true; }); + session.on('iter', () => { if (!stopped) { stopped = true; session.stop(); } }); + await session.start(); + + expect(haltFired).toBe(false); + }); + + test('off() removes a previously registered listener (and is a no-op for an unregistered one)', async () => { + const machine = new PostMachine({ 10: mark, 20: stop }); + const session = machine.debugRun(); + + let called = false; + const handler = () => { called = true; }; + session.on('halt', handler); + session.off('halt', handler); + // Removing a listener that was never registered is a no-op, not an error. + session.off('halt', () => { /* never registered */ }); + await session.start(); + + expect(called).toBe(false); + }); + + test('a halt breakpoint surfaces a breakpoint-cause pause', async () => { + const machine = new PostMachine({ 10: mark, 20: stop }); + machine.setBreakpoint(haltState, { before: true }); + + const causes: string[] = []; + const session = machine.debugRun(); + session.on('pause', (m) => { causes.push(m.pause.cause); session.continue(); }); + await session.start(); + + expect(causes).toContain('breakpoint'); + }); + + test('step and halt listeners fire during an uninterrupted run', async () => { + const machine = new PostMachine({ 10: mark, 20: stop }); + const session = machine.debugRun(); + + let steps = 0; + let halted = false; + session.on('step', () => { steps += 1; }); + session.on('halt', () => { halted = true; }); + await session.start(); + + expect(steps).toBeGreaterThan(0); + expect(halted).toBe(true); + }); +}); From 372cfb2782860f347db108fea1de53d4998c39f9 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Sat, 30 May 2026 19:15:50 +0300 Subject: [PATCH 26/34] docs(claude): refresh v7 engine-adoption notes for post alphas 3-6 --- CLAUDE.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 33a0fee..e37e001 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -81,14 +81,29 @@ Non-README tests (sentinel-identity checks, internal plumbing) live in separatel - **Sentinel singletons** keyed by `Symbol(...)` — `haltState`, `ifOtherSymbol`, the members of `movements`, the members of `symbolCommands`. Equality checks (`=== haltState`, etc.) require the same physical object. - **Classes** — `Reference`, `State`, `TapeBlock`, `TuringMachine`, `Tape`, `Alphabet`. `instanceof` checks require shared constructor identity. -The current peer range is `^7.0.0-alpha.2` (set in `@post-machine-js/machine@7.0.0-alpha.2`). **v4 / v5 / v6 are no longer supported on the v7 line** — a consumer still on those engine majors cannot install this package and must upgrade in lockstep. (post-machine-js skipped its own v5 and skipped its own v7-alpha.1; v7-alpha.2 is the first post prerelease that crosses to engine v7.) +The latest peer range is `^7.0.0-alpha.6` (set in `@post-machine-js/machine@7.0.0-alpha.6`). **v4 / v5 / v6 are no longer supported on the v7 line** — a consumer still on those engine majors cannot install this package and must upgrade in lockstep. (post-machine-js skipped its own v5 and skipped its own v7-alpha.1; v7-alpha.2 is the first post prerelease that crosses to engine v7.) -The engine v7 changes that drove this release: +Engine v7 alpha changes adopted in post v7 alphas (chronological): +**post alpha.2 (engine alpha.2):** - **`withOverrodeHaltState` → `withOverriddenHaltState`** (engine [#149](https://github.com/mellonis/turing-machine-js/issues/149)). Consumer-side rename in `src/commands.ts`, `src/classes/PostMachine.ts`, and tests. - **Wrapper composite shape `A>B` → `A(B)`** (engine [#148](https://github.com/mellonis/turing-machine-js/issues/148)). `parsePath` now rejects `(`/`)` in user-provided state names. The Post `Path` separators (`::`, `.`, `~`) survive unchanged. - **`toMermaid` callable-subtree emit** (engine [#174](https://github.com/mellonis/turing-machine-js/issues/174)). The wrapper composite is now a `[[bare(continuation)]]` call site OUTSIDE the subgraph; the bare hopper + body live INSIDE `subgraph w_N["callable subtree of NAME"]`. Bold `==> "call"` arrow from wrapper to bare; dotted `-. "return" .->` from subgraph back to wrapper; retired `-. onHalt .->` (wrapper-to-override is now solid `-->`). Body's halt-bound transitions retarget to the frame's halt marker `cN`, not the real `s0`. Consequence: `summarizePostMachine().stateCount` is +1 per call site vs v6.x. +**post alpha.3 (engine alpha.3, no functional engine adopt — internal):** +- Hopper drop ([#85](https://github.com/mellonis/post-machine-js/issues/85)) — acyclic subroutines with plain leading instructions no longer get a hopper State; Tarjan SCC on local call graph identifies cyclic subs (hopper retained). Common case wraps `foo::1` directly, saving one State per call site. Composite wrapper name shifts `foo(continuation)` → `foo::1(continuation)`. + +**post alpha.4 (engine alpha.3 still; #186 state-tags adopted):** +- **Path-based `pm.tag(...)` registry + inline `$tag(...)` decorator + auto-tag policy** ([#86](https://github.com/mellonis/post-machine-js/issues/86)) on top of engine [#186](https://github.com/mellonis/turing-machine-js/issues/186)'s state-tags surface. Note: post alpha.4 (state tags) shipped 2026-05-21 while engine alpha.4 (collectStates + bug fixes) shipped 2026-05-23 — same alpha-number, **NOT** lockstep. Post and engine alpha cycles are independent even when the numbers happen to coincide. + +**post alpha.5 (engine alpha.5; #207 haltState.debug → boolean):** +- **Dropped the module-load `haltState` lockdown** ([#94](https://github.com/mellonis/post-machine-js/pull/94)) now that engine [#207](https://github.com/mellonis/turing-machine-js/issues/207) collapsed `haltState.debug` to a boolean. The per-side `DebugConfig` the lockdown funneled no longer exists; direct `haltState.debug = boolean` goes straight to the engine setter. `pm.setBreakpoint(haltState, …)` still works for registry-aware halt pauses. + +**post alpha.6 (engine alpha.6; #102 DebugSession reshape + #213 CallFrame):** +- **Adopted the engine's debug-surface reshape**: `pm.run()` is now sync + callback-free; a new `pm.debugRun()` returns a `PostDebugSession` (wraps the engine `DebugSession`, re-adds `arrivalPath`/`candidatePaths`, applies the breakpoint registry as a pause filter, reads the one-sided `m.pause: {side, cause}` that replaced the per-yield `m.debugBreak`). +- `#wrapMachineState` switched from spread to in-place mutation so the engine's `MACHINE_STATE_INTERNAL` accessor survives the wrap (detection needs it). +- Engine [#213](https://github.com/mellonis/turing-machine-js/issues/213) (`CallFrame extends State`) is API-compatible — `instanceof State` preserved — and required no post-side change. + Previous v5/v6 engine changes still apply unchanged on v7: - **`pm.run()` stays async.** Engine v4 introduced `Promise` return; v5/v6 didn't change that. Callers must still `await` it. From 6e0afa19b52221c45f198450a3febdf71074cf9c Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Sat, 30 May 2026 20:51:02 +0300 Subject: [PATCH 27/34] docs(machine): hedge wrapper-placement prose for framed wrappers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Nodes" reading-guide bullet claimed wrappers always sit OUTSIDE their callable subtree's subgraph. Engine v7 fix (turing-machine-js#223) moves framed wrappers — those whose continuation chain participates in a caller's frame — INSIDE the owner frame's subgraph with the same [[…]] shape. Split the prose into top-level vs framed cases. --- packages/machine/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/machine/README.md b/packages/machine/README.md index 60eeed7..9663e99 100644 --- a/packages/machine/README.md +++ b/packages/machine/README.md @@ -83,7 +83,7 @@ Reading the diagram: - `(((label)))` — halt state - `["label"]` — intermediate (and now also entry) state - `([idle])` — the `idle` sentinel that marks the entry point via a dotted `idle -. enter .-> s_initial` edge -- (Wrappers produced by `withOverriddenHaltState` use a `[[bare(continuation)]]` double-square node sitting OUTSIDE its callable subtree's `subgraph w_N["callable subtree of NAME"]` block — see the [Subroutines](#subroutines) section.) +- (Wrappers produced by `withOverriddenHaltState` use a `[[bare(continuation)]]` double-square node. Top-level wrappers sit OUTSIDE their callable subtree's `subgraph w_N["callable subtree of NAME"]` block; wrappers whose continuation chain participates in a caller's frame render INSIDE the owner frame's subgraph with the same `[[…]]` shape — see the [Subroutines](#subroutines) section.) The labels are PostMachine's instruction-derived names — `"10"`, `"20"`, `"30"` map directly to the instruction indices in the program. The wrapper composite shape (`"()"`) doesn't appear in this example because there are no calls or groups; see the [Subroutines](#subroutines) section for that. The `40: stop` instruction is elided — `stop` halts the machine, so the transition from `30: mark` flows straight to halt rather than through an intermediate state. From 10dc2ba1b83afc671ffdd2942238346d734979ee Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Tue, 2 Jun 2026 10:38:29 +0300 Subject: [PATCH 28/34] feat(debug): PostDebugSession.stepInstruction() (7.0.0-alpha.7, #101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Advance to the next numbered Post instruction in the current scope. Position-independent: skips sub-step transitions inside groups (50.1 → 50.2) and descents into called scopes (call('foo') → foo::1) because those aren't numbered instructions in the current scope. Two rules cover the full semantics: 1. Advance until click-time (scope, instructionIndex) pair changes; sub-step transitions and sub-scope descents stay silent. 2. If there's no next numbered instruction in the current scope (you hit stop or fall through the end), the natural engine continuation fires — return to caller's continuation if inside a call/group, halt if at top level. Internally drives the engine via repeated stepIn; filters the resulting step-cause pauses via path comparison. Breakpoints mid-advance interrupt normally and consume the stepInstruction intent. Closes #101. --- package-lock.json | 12 +- packages/machine/CHANGELOG.md | 15 ++ packages/machine/package.json | 6 +- .../machine/src/classes/PostDebugSession.ts | 105 ++++++++++++- .../src/classes/PostMachine.debugger.spec.ts | 143 ++++++++++++++++++ 5 files changed, 271 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index cb6e64e..da78587 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2740,9 +2740,9 @@ } }, "node_modules/@turing-machine-js/machine": { - "version": "7.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/@turing-machine-js/machine/-/machine-7.0.0-alpha.6.tgz", - "integrity": "sha512-xHlFzcKE1e6YUeYYua3seu59it8HElbOsxBPDzhpwTmixpOgrZgSB3ENs3KaisgEdROSaytlZw/6udWYQ2lj+w==", + "version": "7.0.0-alpha.7", + "resolved": "https://registry.npmjs.org/@turing-machine-js/machine/-/machine-7.0.0-alpha.7.tgz", + "integrity": "sha512-HW2qEyjFmZbantxDguG1k8+FpjRSUk0vXNGXXVCgxCQeEMNJCndHAyOvLxp5VSk25/g8a+Q1rKwxpsc8z140UA==", "license": "GPL-3.0-or-later", "engines": { "npm": ">=7.0.0" @@ -10441,16 +10441,16 @@ }, "packages/machine": { "name": "@post-machine-js/machine", - "version": "7.0.0-alpha.6", + "version": "7.0.0-alpha.7", "license": "GPL-3.0-or-later", "devDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.6" + "@turing-machine-js/machine": "^7.0.0-alpha.7" }, "engines": { "npm": ">=7.0.0" }, "peerDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.6" + "@turing-machine-js/machine": "^7.0.0-alpha.7" } } } diff --git a/packages/machine/CHANGELOG.md b/packages/machine/CHANGELOG.md index 25462ea..4505364 100644 --- a/packages/machine/CHANGELOG.md +++ b/packages/machine/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file. 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). +## [7.0.0-alpha.7] - 2026-06-02 + +Adds **`PostDebugSession.stepInstruction()`** — a Post-level step control that advances to the next numbered instruction in the current scope. Resolves [#101](https://github.com/mellonis/post-machine-js/issues/101). Peer dep `@turing-machine-js/machine` widened `^7.0.0-alpha.6` → `^7.0.0-alpha.7`. Published under the `next` dist-tag: `npm install @post-machine-js/machine@next`. + +**Pre-release — the API surface may still shift before stable v7.0.0.** Pin to a specific alpha for reproducibility: `@post-machine-js/machine@7.0.0-alpha.7`. + +### Added + +- **`PostDebugSession.stepInstruction()`** ([#101](https://github.com/mellonis/post-machine-js/issues/101)) — advance to the next numbered Post instruction in the current scope. Skips sub-step transitions inside groups (`50.1` → `50.2`) and descents into called scopes (`call('foo')` → `foo::1`) because those aren't numbered instructions in the *current* scope. Two rules cover the full semantics: (1) advance until the click-time `(scope, instructionIndex)` pair changes — sub-step transitions and sub-scope descents stay silent; (2) if there's no next numbered instruction in the current scope (you hit `stop` or fall through the end), the natural engine continuation fires — return to caller's continuation if inside a call/group, halt if at top level. Position-independent: same behavior whether paused at an atomic instruction, a `call(...)` entry, a group entry, mid-group, or inside a called scope. Mirrors `stepIn`/`stepOver`/`stepOut` naming axis. If a registered breakpoint or external `pause()` fires mid-advance, it surfaces normally and consumes the stepInstruction intent. Internally drives the engine via repeated `stepIn`; filters the resulting step-cause pauses via path comparison against the click-time anchor. Throws if called without a paused state. + +### Compatibility + +- Peer dep `@turing-machine-js/machine` widened `^7.0.0-alpha.6` → `^7.0.0-alpha.7`. (Semver-prerelease caret on the alpha.6 range already accepted alpha.7+; the widening is the workspace convention.) +- Existing `stepIn` / `stepOver` / `stepOut` / `continue` / `pause` / `stop` / `setRunInterval` controls unchanged. + ## [7.0.0-alpha.6] - 2026-05-29 Adopts engine **v7.0.0-alpha.6** ([turing-machine-js#102](https://github.com/mellonis/turing-machine-js/issues/102)) — the debug-surface reshape. `PostMachine.run()` becomes synchronous and callback-free, a new `PostMachine.debugRun()` returns an interactive `PostDebugSession`, and the per-yield `m.debugBreak` is replaced by the engine's one-sided `m.pause: { side, cause }`. Published under the `next` dist-tag: `npm install @post-machine-js/machine@next`. diff --git a/packages/machine/package.json b/packages/machine/package.json index c9d3012..342e45f 100644 --- a/packages/machine/package.json +++ b/packages/machine/package.json @@ -1,6 +1,6 @@ { "name": "@post-machine-js/machine", - "version": "7.0.0-alpha.6", + "version": "7.0.0-alpha.7", "description": "A convenient Post machine", "engines": { "npm": ">=7.0.0" @@ -28,10 +28,10 @@ "machine" ], "peerDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.6" + "@turing-machine-js/machine": "^7.0.0-alpha.7" }, "devDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.6" + "@turing-machine-js/machine": "^7.0.0-alpha.7" }, "main": "dist/index.cjs", "module": "dist/index.mjs", diff --git a/packages/machine/src/classes/PostDebugSession.ts b/packages/machine/src/classes/PostDebugSession.ts index 8ccec4d..bab6ca7 100644 --- a/packages/machine/src/classes/PostDebugSession.ts +++ b/packages/machine/src/classes/PostDebugSession.ts @@ -6,7 +6,7 @@ import { State, haltState, } from '@turing-machine-js/machine'; -import { formatPath, type Path } from '../path'; +import { formatPath, normalizeScope, type Path } from '../path'; import type { MachineState } from '../index'; import type { Breakpoint } from '../breakpoints'; import type { PostMachine } from './PostMachine'; @@ -55,6 +55,15 @@ export class PostDebugSession { }; #prevState: State | null = null; #prevJsSymbol: symbol | null = null; + /** Latest pause's `arrivalPath`. Read by `stepInstruction()` to anchor the + * click-time `(scope, instructionIndex)` it must advance past. */ + #lastPausedPath: Path | null = null; + /** When set, the session is mid-`stepInstruction()` — the engine is being + * driven via repeated `stepIn` until the path's `(scope, instructionIndex)` + * differs from this anchor (sub-step transitions and descents into a + * sub-scope are silently passed through). Cleared on landing, halt, or + * any non-step interrupt. */ + #pendingStepInstruction: { scope: string[]; instructionIndex: number } | null = null; readonly #entryPath: Path; readonly #wrap: (raw: EngineMachineState, prev: State | null, prevSym: symbol | null) => MachineState; readonly #getBreakpoints: () => readonly Breakpoint[]; @@ -87,6 +96,28 @@ export class PostDebugSession { ...this.#wrap(raw, this.#prevState, this.#prevJsSymbol), pause: raw.pause, }; + this.#lastPausedPath = wrapped.arrivalPath; + + // stepInstruction internal filter — only on step-cause pauses (the + // kind our own stepInstruction() drives via repeated engine.stepIn). + // Other causes (breakpoint, manual) interrupt stepInstruction and + // surface through the normal path below. + if (this.#pendingStepInstruction !== null && raw.pause.cause === 'step') { + if (this.#stillInClickTimeInstruction(wrapped.arrivalPath, this.#pendingStepInstruction)) { + // Either a sub-step transition (same scope+index, group sub-step) + // or a descent into a sub-scope (call/group body) — keep stepping. + this.#engineSession.stepIn(); + return; + } + // Landed on a different (scope, instructionIndex) pair → surface. + this.#pendingStepInstruction = null; + } else if (this.#pendingStepInstruction !== null) { + // Non-step pause during stepInstruction — surfaces normally; the + // user sees the breakpoint / manual pause and the stepInstruction + // intent is consumed. + this.#pendingStepInstruction = null; + } + // Apply post-machine breakpoint registry filter — fire only when the // engine pause was triggered by a registered breakpoint (or by a // step-mode endpoint / manual pause). @@ -104,6 +135,8 @@ export class PostDebugSession { this.#prevJsSymbol = this.#tapeBlockSymbol([raw.currentSymbols[0]]); }); this.#engineSession.on('halt', () => { + this.#pendingStepInstruction = null; + this.#lastPausedPath = null; for (const fn of this.#listeners.halt) void fn(); }); @@ -153,10 +186,80 @@ export class PostDebugSession { this.#engineSession.stepOut(); } + /** + * Advance to the next **numbered Post instruction** in the current scope. + * + * `stepInstruction()` is the Post-level program-counter step — it skips + * sub-step transitions inside groups (`50.1` → `50.2`) and descents into + * called scopes (`call('foo')` → `foo::1`) because those aren't numbered + * instructions in the *current* scope's program. Two rules cover the + * whole semantics: + * + * 1. Advance until the click-time `(scope, instructionIndex)` pair + * changes. Sub-step transitions and sub-scope descents stay silent. + * 2. If there's no next numbered instruction in the current scope + * (you hit `stop` or fall through the end), the natural engine + * continuation fires — return to caller's continuation if inside + * a call/group, halt if at top level. + * + * Position-independent: same behavior whether you're at an atomic + * instruction, a `call(...)` entry, a group entry, mid-group, or any + * instruction inside a called scope. The "open question" about whether + * to descend into a callee's body is resolved by rule 1 — different + * scope = different "instruction" only when the scope is the current + * one's, otherwise the descent is silent and we run until we exit the + * call (which lands us on the caller's next numbered instruction). + * + * If a registered breakpoint or external `pause()` fires mid-advance, + * it surfaces normally and consumes the stepInstruction intent. + * + * Implementation: drives the engine via repeated `stepIn` internally; + * filters the resulting step-cause pauses via [[`Path`]] comparison + * against the click-time anchor. Resolves + * [post-machine-js#101](https://github.com/mellonis/post-machine-js/issues/101). + */ + stepInstruction(): void { + if (this.#lastPausedPath === null) { + throw new Error('stepInstruction: no paused state to advance from'); + } + this.#pendingStepInstruction = { + scope: normalizeScope(this.#lastPausedPath.scope), + instructionIndex: this.#lastPausedPath.instructionIndex, + }; + this.#engineSession.stepIn(); + } + setRunInterval(ms: number): void { this.#engineSession.setRunInterval(ms); } + /** stepInstruction filter — is `current` still inside the click-time + * instruction (so we keep silent-stepping)? Two cases are silent: + * exact match on `(scope, instructionIndex)` (sub-step inside the same + * numbered instruction — e.g. group sub-step `50.2` after a click at + * `50.1`), and any descent into a sub-scope (call/group body — `foo::1` + * after a click at `50: call('foo')`). Sibling and shallower scopes, + * or same-scope different-index, mean we landed on a new instruction + * and the silent stepping ends. */ + #stillInClickTimeInstruction( + current: Path, + click: { scope: string[]; instructionIndex: number }, + ): boolean { + const cur = normalizeScope(current.scope); + // Descended into a sub-scope (current scope strictly extends click scope). + if (cur.length > click.scope.length + && click.scope.every((s, i) => cur[i] === s)) { + return true; + } + // Same scope, same numbered index — sub-step within the same instruction. + if (cur.length === click.scope.length + && cur.every((s, i) => s === click.scope[i]) + && current.instructionIndex === click.instructionIndex) { + return true; + } + return false; + } + // Decide whether a raw engine pause should surface to post-machine pause // listeners. Step-mode endpoints and manual pauses always pass through; // breakpoint-cause pauses are filtered against the registry so only diff --git a/packages/machine/src/classes/PostMachine.debugger.spec.ts b/packages/machine/src/classes/PostMachine.debugger.spec.ts index 2564386..7876108 100644 --- a/packages/machine/src/classes/PostMachine.debugger.spec.ts +++ b/packages/machine/src/classes/PostMachine.debugger.spec.ts @@ -7,8 +7,14 @@ import { Tape, haltState, call, check, mark, right, stop, + formatPath, + type Path, } from '../index'; +function formatArrival(p: Path): string { + return formatPath(p); +} + describe('PostMachine — async run', () => { function buildWalkAndMark(): PostMachine { return new PostMachine({ @@ -187,6 +193,143 @@ describe('PostDebugSession — step controls and lifecycle', () => { expect(causes.slice(1)).toContain('step'); }); + describe('stepInstruction() — next-numbered-instruction in current scope (#101)', () => { + test('atomic → next numbered atomic in same scope', async () => { + const machine = new PostMachine({ 10: mark, 20: mark, 30: stop }); + machine.setBreakpoint('10', { before: true }); + const paths: string[] = []; + const session = machine.debugRun(); + session.on('pause', (m) => { + paths.push(formatArrival(m.arrivalPath)); + if (paths.length === 1) session.stepInstruction(); + else session.continue(); + }); + await session.start(); + expect(paths[0]).toBe('10'); + expect(paths[1]).toBe('20'); + }); + + test('at a call(…) entry, stepInstruction runs the call to completion and lands on caller’s next instruction', async () => { + const machine = new PostMachine({ + 10: call('foo'), + 20: mark, + 30: stop, + foo: { 1: mark, 2: stop }, + }); + machine.setBreakpoint('10', { before: true }); + const paths: string[] = []; + const session = machine.debugRun(); + session.on('pause', (m) => { + paths.push(formatArrival(m.arrivalPath)); + if (paths.length === 1) session.stepInstruction(); + else session.continue(); + }); + await session.start(); + expect(paths[0]).toBe('10'); + expect(paths[1]).toBe('20'); + }); + + test('inside a callee → next numbered in callee’s scope', async () => { + // stepIn from a call site executes the wrapper’s iter (which IS the + // bare’s first instruction) and lands BEFORE the callee’s second + // numbered instruction; from there, stepInstruction advances by one + // numbered index in the callee’s scope. + const machine = new PostMachine({ + 10: call('foo'), + 20: stop, + foo: { 1: mark, 2: mark, 3: stop }, + }); + machine.setBreakpoint('10', { before: true }); + const paths: string[] = []; + let phase = 0; + const session = machine.debugRun(); + session.on('pause', (m) => { + paths.push(formatArrival(m.arrivalPath)); + if (phase === 0) { phase = 1; session.stepIn(); } // descend into foo (lands at foo::2) + else if (phase === 1) { phase = 2; session.stepInstruction(); } // foo::2 → foo::3 + else session.continue(); + }); + await session.start(); + expect(paths[0]).toBe('10'); + expect(paths[1]).toBe('foo::2'); + expect(paths[2]).toBe('foo::3'); + }); + + test('inside a callee at last numbered → returns to caller’s continuation', async () => { + const machine = new PostMachine({ + 10: call('foo'), + 20: mark, + 30: stop, + foo: { 1: mark, 2: stop }, + }); + machine.setBreakpoint('10', { before: true }); + const paths: string[] = []; + let phase = 0; + const session = machine.debugRun(); + session.on('pause', (m) => { + paths.push(formatArrival(m.arrivalPath)); + if (phase === 0) { phase = 1; session.stepIn(); } // descend into foo (lands at foo::2 = stop) + else if (phase === 1) { phase = 2; session.stepInstruction(); } // pops foo back to caller's 20 + else session.continue(); + }); + await session.start(); + expect(paths[0]).toBe('10'); + expect(paths[1]).toBe('foo::2'); + expect(paths[2]).toBe('20'); + }); + + test('stepInstruction when next numbered is a terminal stop → halts', async () => { + // Engine doesn't pause before `stop` (it transitions to haltState + // directly, no before-iter pause point), so stepInstruction's + // "advance to next numbered" naturally falls through to halt when + // the next numbered is a stop at top level. + const machine = new PostMachine({ 10: mark, 20: stop }); + machine.setBreakpoint('10', { before: true }); + const paths: string[] = []; + let halted = false; + const session = machine.debugRun(); + session.on('pause', (m) => { + paths.push(formatArrival(m.arrivalPath)); + session.stepInstruction(); + }); + session.on('halt', () => { halted = true; }); + await session.start(); + expect(paths[0]).toBe('10'); + expect(paths.length).toBe(1); + expect(halted).toBe(true); + }); + + test('throws if called before any pause has fired', () => { + const machine = new PostMachine({ 10: mark, 20: stop }); + const session = machine.debugRun(); + expect(() => session.stepInstruction()).toThrow(/no paused state/); + }); + + test('a registered breakpoint mid-advance interrupts stepInstruction and surfaces normally', async () => { + const machine = new PostMachine({ + 10: call('foo'), + 20: mark, + 30: stop, + foo: { 1: mark, 2: mark, 3: stop }, + }); + machine.setBreakpoint('10', { before: true }); + machine.setBreakpoint('foo::2', { before: true }); + const paths: string[] = []; + const session = machine.debugRun(); + session.on('pause', (m) => { + paths.push(formatArrival(m.arrivalPath)); + if (paths.length === 1) session.stepInstruction(); + else session.continue(); + }); + await session.start(); + // First pause at the initial breakpoint on 10; stepInstruction tries + // to advance past 10's call('foo'), but the foo::2 breakpoint fires + // mid-advance and surfaces. + expect(paths[0]).toBe('10'); + expect(paths[1]).toBe('foo::2'); + }); + }); + test('external pause() fires a manual-cause pause; setRunInterval throttles the run', async () => { const machine = new PostMachine({ 10: mark, 20: mark, 30: mark, 40: stop }); const session = machine.debugRun(); From d7003baaf656207b8303b5054fbb985758236c95 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Tue, 2 Jun 2026 10:41:35 +0300 Subject: [PATCH 29/34] docs: post README + CLAUDE.md catch up to stepInstruction (alpha.7, #101) --- CLAUDE.md | 5 ++++- packages/machine/README.md | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e37e001..a5d7f68 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -81,7 +81,7 @@ Non-README tests (sentinel-identity checks, internal plumbing) live in separatel - **Sentinel singletons** keyed by `Symbol(...)` — `haltState`, `ifOtherSymbol`, the members of `movements`, the members of `symbolCommands`. Equality checks (`=== haltState`, etc.) require the same physical object. - **Classes** — `Reference`, `State`, `TapeBlock`, `TuringMachine`, `Tape`, `Alphabet`. `instanceof` checks require shared constructor identity. -The latest peer range is `^7.0.0-alpha.6` (set in `@post-machine-js/machine@7.0.0-alpha.6`). **v4 / v5 / v6 are no longer supported on the v7 line** — a consumer still on those engine majors cannot install this package and must upgrade in lockstep. (post-machine-js skipped its own v5 and skipped its own v7-alpha.1; v7-alpha.2 is the first post prerelease that crosses to engine v7.) +The latest peer range is `^7.0.0-alpha.7` (set in `@post-machine-js/machine@7.0.0-alpha.7`). **v4 / v5 / v6 are no longer supported on the v7 line** — a consumer still on those engine majors cannot install this package and must upgrade in lockstep. (post-machine-js skipped its own v5 and skipped its own v7-alpha.1; v7-alpha.2 is the first post prerelease that crosses to engine v7.) Engine v7 alpha changes adopted in post v7 alphas (chronological): @@ -104,6 +104,9 @@ Engine v7 alpha changes adopted in post v7 alphas (chronological): - `#wrapMachineState` switched from spread to in-place mutation so the engine's `MACHINE_STATE_INTERNAL` accessor survives the wrap (detection needs it). - Engine [#213](https://github.com/mellonis/turing-machine-js/issues/213) (`CallFrame extends State`) is API-compatible — `instanceof State` preserved — and required no post-side change. +**post alpha.7 (post-only; #101 stepInstruction):** +- **Added `PostDebugSession.stepInstruction()`** ([#101](https://github.com/mellonis/post-machine-js/issues/101)) — the Post-level program-counter step. Advances to the next numbered Post instruction in the *current* scope; sub-step transitions inside groups (`50.1 → 50.2`) and descents into called scopes (`call('foo') → foo::1`) stay silent because those aren't numbered instructions in the current scope. Two rules: (1) advance until click-time `(scope, instructionIndex)` pair changes; (2) if no next numbered exists in current scope (hit `stop` or fall through end), the natural engine continuation fires — caller's continuation if inside a call/group, halt at top level. Position-independent: same behavior at atomic instructions, call entries, group entries, mid-group, or inside callees. Implementation: drives engine via repeated `stepIn`, filters step-cause pauses by path comparison against the click-time anchor (`#stillInClickTimeInstruction` in `PostDebugSession.ts`). Breakpoints / external `pause()` mid-advance interrupt normally. Peer dep `^7.0.0-alpha.6 → ^7.0.0-alpha.7` widening (semver-prerelease caret already accepted alpha.7+; workspace convention). + Previous v5/v6 engine changes still apply unchanged on v7: - **`pm.run()` stays async.** Engine v4 introduced `Promise` return; v5/v6 didn't change that. Callers must still `await` it. diff --git a/packages/machine/README.md b/packages/machine/README.md index 9663e99..a3369fc 100644 --- a/packages/machine/README.md +++ b/packages/machine/README.md @@ -102,7 +102,7 @@ The runtime. Subclasses `TuringMachine` from `@turing-machine-js/machine`: the c **Methods.** - `run({ stepsLimit? } = {})` → `void`. Runs to halt or until `stepsLimit` (default `1e5`) is exhausted. **Synchronous and callback-free as of v7.0.0-alpha.6** (adopting engine [#102](https://github.com/mellonis/turing-machine-js/issues/102)) — for per-step observation use `runStepByStep()`; for breakpoints, throttling, or interactive stepping use `debugRun()`. -- `debugRun({ stepsLimit? } = {})` → `PostDebugSession`. An interactive debugger session bound to this machine (see [Breakpoints](#breakpoints)). It emits `pause` / `step` / `iter` / `halt` events (`session.on(event, listener)`) and exposes `continue()` / `stepIn()` / `stepOver()` / `stepOut()` / `pause()` / `stop()` / `setRunInterval(ms)`; call `await session.start()` to begin. `pause`-event payloads carry `m.pause: { side: 'before' | 'after', cause: 'breakpoint' | 'step' | 'manual' }`. +- `debugRun({ stepsLimit? } = {})` → `PostDebugSession`. An interactive debugger session bound to this machine (see [Breakpoints](#breakpoints)). It emits `pause` / `step` / `iter` / `halt` events (`session.on(event, listener)`) and exposes `continue()` / `stepIn()` / `stepOver()` / `stepOut()` / **`stepInstruction()`** / `pause()` / `stop()` / `setRunInterval(ms)`; call `await session.start()` to begin. `pause`-event payloads carry `m.pause: { side: 'before' | 'after', cause: 'breakpoint' | 'step' | 'manual' }`. **`stepInstruction()` (v7.0.0-alpha.7, [#101](https://github.com/mellonis/post-machine-js/issues/101))** is the Post-level program-counter step — advance to the next numbered instruction in the *current* scope; skips sub-step transitions inside groups (`50.1 → 50.2`) and descents into called scopes (`call('foo') → foo::1`) because those aren't numbered instructions in your program. Two rules cover the whole semantics: (1) advance until the click-time `(scope, instructionIndex)` pair changes; (2) if there's no next numbered instruction in the current scope, the natural engine continuation fires (return to caller's continuation inside a call/group, halt at top level). Throws if no paused state. - `runStepByStep({ stepsLimit? } = {})` → `Generator`. Synchronous step-at-a-time execution; the consumer drives the loop with `for ... of` or `.next()`. Pure iteration — it does no breakpoint detection (that lives in `debugRun()`). - `replaceTapeWith(newTape)` — swap the active tape. Build the new tape against `machine.tape.alphabet` so symbol identities match the machine's interned alphabet. From 3cd5724c01be5da38c073fafe79fcb502bb174d6 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Tue, 2 Jun 2026 11:04:59 +0300 Subject: [PATCH 30/34] fix(debug): stepInstruction handles nested calls + 100% coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI on the v7→master integration PR caught two issues in alpha.7's stepInstruction implementation: 1. Nested calls (foo calls bar from inside foo): the helper's scope-name comparison surfaced at bar::2 instead of running through bar's body to land at foo::3. Cause: arrivalPath.scope is the immediate callee (not the full stack), so 'sibling scope at same depth' (bar from foo's POV) was misclassified as 'different instruction.' 2. PostDebugSession.ts coverage dropped to 97.5% (lines 251, 258 uncovered) — branches that exercise non-empty click.scope weren't reached by the existing tests. Fix: classify scope changes by length relative to click, using Post's call stack discipline (returns always go through ancestor scopes): - cur.length > click.length → deeper, keep stepping - cur.length < click.length → shallower, surface (returned past click) - equal length: scope-content match → sub-step / land by index; scope-content differ → sibling callee at same depth, keep stepping New tests cover both nested-call (foo→bar→foo::3) and mid-group (10.2→10.3→20) cases, exercising the previously uncovered branches. Existing 'inside callee at last' test simplified to use foo: {1: stop} (single-instruction subroutine) so the test reflects the helper's shallower-scope detection path. --- .../machine/src/classes/PostDebugSession.ts | 62 ++++++++++++------- .../src/classes/PostMachine.debugger.spec.ts | 49 +++++++++++++++ 2 files changed, 87 insertions(+), 24 deletions(-) diff --git a/packages/machine/src/classes/PostDebugSession.ts b/packages/machine/src/classes/PostDebugSession.ts index bab6ca7..f62de06 100644 --- a/packages/machine/src/classes/PostDebugSession.ts +++ b/packages/machine/src/classes/PostDebugSession.ts @@ -104,12 +104,16 @@ export class PostDebugSession { // surface through the normal path below. if (this.#pendingStepInstruction !== null && raw.pause.cause === 'step') { if (this.#stillInClickTimeInstruction(wrapped.arrivalPath, this.#pendingStepInstruction)) { - // Either a sub-step transition (same scope+index, group sub-step) - // or a descent into a sub-scope (call/group body) — keep stepping. + // Either we're inside a deeper call/group than the click anchor + // (we'll return to click scope when it pops), OR we're at a + // sub-step transition within the same numbered instruction + // (group sub-step `10.2 → 10.3` keeps `(scope, instructionIndex)`). + // Keep stepping. this.#engineSession.stepIn(); return; } - // Landed on a different (scope, instructionIndex) pair → surface. + // Returned to the click-time scope at a different numbered index + // (or any other "landed on a new instruction" case) → surface. this.#pendingStepInstruction = null; } else if (this.#pendingStepInstruction !== null) { // Non-step pause during stepInstruction — surfaces normally; the @@ -214,8 +218,12 @@ export class PostDebugSession { * it surfaces normally and consumes the stepInstruction intent. * * Implementation: drives the engine via repeated `stepIn` internally; - * filters the resulting step-cause pauses via [[`Path`]] comparison - * against the click-time anchor. Resolves + * filters the resulting step-cause pauses against the click-time + * `(scope, instructionIndex)` anchor. Different-scope pauses (we + * descended into a call/group) keep stepping — the engine call stack + * guarantees a return to the click scope. Same-scope pauses surface + * unless `instructionIndex` matches the anchor (group sub-step + * `10.2 → 10.3` is silent). Resolves * [post-machine-js#101](https://github.com/mellonis/post-machine-js/issues/101). */ stepInstruction(): void { @@ -233,31 +241,37 @@ export class PostDebugSession { this.#engineSession.setRunInterval(ms); } - /** stepInstruction filter — is `current` still inside the click-time - * instruction (so we keep silent-stepping)? Two cases are silent: - * exact match on `(scope, instructionIndex)` (sub-step inside the same - * numbered instruction — e.g. group sub-step `50.2` after a click at - * `50.1`), and any descent into a sub-scope (call/group body — `foo::1` - * after a click at `50: call('foo')`). Sibling and shallower scopes, - * or same-scope different-index, mean we landed on a new instruction - * and the silent stepping ends. */ + /** stepInstruction filter — should we keep silent-stepping? + * + * Three regions relative to the click-time scope (Post's call stack + * discipline makes this deterministic — returns always go back to an + * ancestor scope, never to a sibling without first returning): + * + * - **Deeper than click** (`cur.length > click.length`) — we're inside + * a call/group descended from the click frame. Keep stepping. + * - **Shallower than click** (`cur.length < click.length`) — we've + * returned past the click frame (e.g. clicked inside `foo`, + * `foo::N=stop` popped back to main). Surface. + * - **Same depth as click** (`cur.length === click.length`) — either + * a sibling callee invoked at the same level (different `scope` + * contents — keep stepping; the return brings us back to click + * scope) or we're back in the click scope (matching `scope` + * contents — check `instructionIndex`: equal means sub-step + * transition inside the same numbered instruction, keep stepping; + * different means next numbered instruction, surface). */ #stillInClickTimeInstruction( current: Path, click: { scope: string[]; instructionIndex: number }, ): boolean { const cur = normalizeScope(current.scope); - // Descended into a sub-scope (current scope strictly extends click scope). - if (cur.length > click.scope.length - && click.scope.every((s, i) => cur[i] === s)) { - return true; - } - // Same scope, same numbered index — sub-step within the same instruction. - if (cur.length === click.scope.length - && cur.every((s, i) => s === click.scope[i]) - && current.instructionIndex === click.instructionIndex) { - return true; + if (cur.length > click.scope.length) return true; // deeper + if (cur.length < click.scope.length) return false; // shallower → surface + // Equal depth. + if (!cur.every((s, i) => s === click.scope[i])) { + return true; // sibling scope at same depth } - return false; + // Back in click scope — sub-step iff numbered index matches. + return current.instructionIndex === click.instructionIndex; } // Decide whether a raw engine pause should surface to post-machine pause diff --git a/packages/machine/src/classes/PostMachine.debugger.spec.ts b/packages/machine/src/classes/PostMachine.debugger.spec.ts index 7876108..ec6f358 100644 --- a/packages/machine/src/classes/PostMachine.debugger.spec.ts +++ b/packages/machine/src/classes/PostMachine.debugger.spec.ts @@ -305,6 +305,55 @@ describe('PostDebugSession — step controls and lifecycle', () => { expect(() => session.stepInstruction()).toThrow(/no paused state/); }); + test('mid-group → next numbered (group sub-steps stay silent)', async () => { + // stepInstruction from a group sub-step exercises the "same scope, + // same instructionIndex" branch of #stillInClickTimeInstruction — + // 10.2 → 10.3 has the same top-level index 10, so it stays silent; + // the surface fires only when we land on 20. `20: right` (not stop) + // because the engine doesn't pause before terminal stops. + const machine = new PostMachine({ 10: [mark, right, mark], 20: right, 30: stop }); + machine.setBreakpoint('10', { before: true }); + const paths: string[] = []; + let phase = 0; + const session = machine.debugRun(); + session.on('pause', (m) => { + paths.push(formatArrival(m.arrivalPath)); + if (phase === 0) { phase = 1; session.stepIn(); } // descend into group, lands at 10.2 + else if (phase === 1) { phase = 2; session.stepInstruction(); } // walk through 10.3 silently → 20 + else session.continue(); + }); + await session.start(); + expect(paths[0]).toBe('10'); + expect(paths[1]).toBe('10.2'); + expect(paths[2]).toBe('20'); + }); + + test('nested call from non-main scope → silent descent stays inside the outer click scope', async () => { + // stepInstruction from a non-main scope (foo) over a call('bar') + // exercises the "descended into sub-scope" branch with a non-empty + // click scope — every-callback fires, deeper-scope detection holds. + const machine = new PostMachine({ + 10: call('foo'), + 20: stop, + foo: { 1: mark, 2: call('bar'), 3: stop }, + bar: { 1: mark, 2: stop }, + }); + machine.setBreakpoint('10', { before: true }); + const paths: string[] = []; + let phase = 0; + const session = machine.debugRun(); + session.on('pause', (m) => { + paths.push(formatArrival(m.arrivalPath)); + if (phase === 0) { phase = 1; session.stepIn(); } // main → lands at foo::2 (the call to bar) + else if (phase === 1) { phase = 2; session.stepInstruction(); } // walk through bar silently → foo::3 + else session.continue(); + }); + await session.start(); + expect(paths[0]).toBe('10'); + expect(paths[1]).toBe('foo::2'); + expect(paths[2]).toBe('foo::3'); + }); + test('a registered breakpoint mid-advance interrupts stepInstruction and surfaces normally', async () => { const machine = new PostMachine({ 10: call('foo'), From 6a46c760ff455f3c3bdb284d376422a4820225b5 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Tue, 2 Jun 2026 11:09:40 +0300 Subject: [PATCH 31/34] =?UTF-8?q?deps:=20widen=20peer=20@turing-machine-js?= =?UTF-8?q?/machine=20^7.0.0-alpha.7=20=E2=86=92=20^7.0.0-alpha.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Engine alpha.8 (TapeSnapshot + tapeViewport move, #227) shipped on npm next today. Bump post's peer-dep widening for alpha.7 to track the latest engine alpha at ship time. Updates: package.json peer dep, package-lock.json, CHANGELOG entry, CLAUDE.md narrative + #stillIn- ClickTimeInstruction algorithm description. --- CLAUDE.md | 4 ++-- package-lock.json | 10 +++++----- packages/machine/CHANGELOG.md | 4 ++-- packages/machine/package.json | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a5d7f68..be089b6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -81,7 +81,7 @@ Non-README tests (sentinel-identity checks, internal plumbing) live in separatel - **Sentinel singletons** keyed by `Symbol(...)` — `haltState`, `ifOtherSymbol`, the members of `movements`, the members of `symbolCommands`. Equality checks (`=== haltState`, etc.) require the same physical object. - **Classes** — `Reference`, `State`, `TapeBlock`, `TuringMachine`, `Tape`, `Alphabet`. `instanceof` checks require shared constructor identity. -The latest peer range is `^7.0.0-alpha.7` (set in `@post-machine-js/machine@7.0.0-alpha.7`). **v4 / v5 / v6 are no longer supported on the v7 line** — a consumer still on those engine majors cannot install this package and must upgrade in lockstep. (post-machine-js skipped its own v5 and skipped its own v7-alpha.1; v7-alpha.2 is the first post prerelease that crosses to engine v7.) +The latest peer range is `^7.0.0-alpha.8` (set in `@post-machine-js/machine@7.0.0-alpha.7`). **v4 / v5 / v6 are no longer supported on the v7 line** — a consumer still on those engine majors cannot install this package and must upgrade in lockstep. (post-machine-js skipped its own v5 and skipped its own v7-alpha.1; v7-alpha.2 is the first post prerelease that crosses to engine v7.) Engine v7 alpha changes adopted in post v7 alphas (chronological): @@ -105,7 +105,7 @@ Engine v7 alpha changes adopted in post v7 alphas (chronological): - Engine [#213](https://github.com/mellonis/turing-machine-js/issues/213) (`CallFrame extends State`) is API-compatible — `instanceof State` preserved — and required no post-side change. **post alpha.7 (post-only; #101 stepInstruction):** -- **Added `PostDebugSession.stepInstruction()`** ([#101](https://github.com/mellonis/post-machine-js/issues/101)) — the Post-level program-counter step. Advances to the next numbered Post instruction in the *current* scope; sub-step transitions inside groups (`50.1 → 50.2`) and descents into called scopes (`call('foo') → foo::1`) stay silent because those aren't numbered instructions in the current scope. Two rules: (1) advance until click-time `(scope, instructionIndex)` pair changes; (2) if no next numbered exists in current scope (hit `stop` or fall through end), the natural engine continuation fires — caller's continuation if inside a call/group, halt at top level. Position-independent: same behavior at atomic instructions, call entries, group entries, mid-group, or inside callees. Implementation: drives engine via repeated `stepIn`, filters step-cause pauses by path comparison against the click-time anchor (`#stillInClickTimeInstruction` in `PostDebugSession.ts`). Breakpoints / external `pause()` mid-advance interrupt normally. Peer dep `^7.0.0-alpha.6 → ^7.0.0-alpha.7` widening (semver-prerelease caret already accepted alpha.7+; workspace convention). +- **Added `PostDebugSession.stepInstruction()`** ([#101](https://github.com/mellonis/post-machine-js/issues/101)) — the Post-level program-counter step. Advances to the next numbered Post instruction in the *current* scope; sub-step transitions inside groups (`50.1 → 50.2`) and descents into called scopes (`call('foo') → foo::1`) stay silent because those aren't numbered instructions in the current scope. Two rules: (1) advance until click-time `(scope, instructionIndex)` pair changes; (2) if no next numbered exists in current scope (hit `stop` or fall through end), the natural engine continuation fires — caller's continuation if inside a call/group, halt at top level. Position-independent: same behavior at atomic instructions, call entries, group entries, mid-group, or inside callees. Implementation: drives engine via repeated `stepIn`, filters step-cause pauses by `(scope, instructionIndex)` comparison against the click-time anchor (`#stillInClickTimeInstruction` in `PostDebugSession.ts`). The filter classifies by scope-length relative to click — deeper (longer) keeps stepping, shallower (shorter) surfaces, equal length checks scope-content + index — using Post's call-stack discipline (returns always go through ancestor scopes) to handle nested calls correctly. Breakpoints / external `pause()` mid-advance interrupt normally. Peer dep `^7.0.0-alpha.6 → ^7.0.0-alpha.8` widening (semver-prerelease caret already accepted alpha.7+; explicit widening jumps to the latest engine alpha at ship time per workspace convention). Previous v5/v6 engine changes still apply unchanged on v7: diff --git a/package-lock.json b/package-lock.json index da78587..e5e4331 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2740,9 +2740,9 @@ } }, "node_modules/@turing-machine-js/machine": { - "version": "7.0.0-alpha.7", - "resolved": "https://registry.npmjs.org/@turing-machine-js/machine/-/machine-7.0.0-alpha.7.tgz", - "integrity": "sha512-HW2qEyjFmZbantxDguG1k8+FpjRSUk0vXNGXXVCgxCQeEMNJCndHAyOvLxp5VSk25/g8a+Q1rKwxpsc8z140UA==", + "version": "7.0.0-alpha.8", + "resolved": "https://registry.npmjs.org/@turing-machine-js/machine/-/machine-7.0.0-alpha.8.tgz", + "integrity": "sha512-/h+Wdy63+siJlEj8jEiNVIiO84cOVaDufPmMHZJUf8LaGIJYjkttqfNkOv0fGOlUYQmDic/+D81w1OZygriuFQ==", "license": "GPL-3.0-or-later", "engines": { "npm": ">=7.0.0" @@ -10444,13 +10444,13 @@ "version": "7.0.0-alpha.7", "license": "GPL-3.0-or-later", "devDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.7" + "@turing-machine-js/machine": "^7.0.0-alpha.8" }, "engines": { "npm": ">=7.0.0" }, "peerDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.7" + "@turing-machine-js/machine": "^7.0.0-alpha.8" } } } diff --git a/packages/machine/CHANGELOG.md b/packages/machine/CHANGELOG.md index 4505364..68a2285 100644 --- a/packages/machine/CHANGELOG.md +++ b/packages/machine/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [7.0.0-alpha.7] - 2026-06-02 -Adds **`PostDebugSession.stepInstruction()`** — a Post-level step control that advances to the next numbered instruction in the current scope. Resolves [#101](https://github.com/mellonis/post-machine-js/issues/101). Peer dep `@turing-machine-js/machine` widened `^7.0.0-alpha.6` → `^7.0.0-alpha.7`. Published under the `next` dist-tag: `npm install @post-machine-js/machine@next`. +Adds **`PostDebugSession.stepInstruction()`** — a Post-level step control that advances to the next numbered instruction in the current scope. Resolves [#101](https://github.com/mellonis/post-machine-js/issues/101). Peer dep `@turing-machine-js/machine` widened `^7.0.0-alpha.6` → `^7.0.0-alpha.8`. Published under the `next` dist-tag: `npm install @post-machine-js/machine@next`. **Pre-release — the API surface may still shift before stable v7.0.0.** Pin to a specific alpha for reproducibility: `@post-machine-js/machine@7.0.0-alpha.7`. @@ -16,7 +16,7 @@ Adds **`PostDebugSession.stepInstruction()`** — a Post-level step control that ### Compatibility -- Peer dep `@turing-machine-js/machine` widened `^7.0.0-alpha.6` → `^7.0.0-alpha.7`. (Semver-prerelease caret on the alpha.6 range already accepted alpha.7+; the widening is the workspace convention.) +- Peer dep `@turing-machine-js/machine` widened `^7.0.0-alpha.6` → `^7.0.0-alpha.8`. (Semver-prerelease caret on the alpha.6 range already accepted alpha.7+; the widening is the workspace convention.) - Existing `stepIn` / `stepOver` / `stepOut` / `continue` / `pause` / `stop` / `setRunInterval` controls unchanged. ## [7.0.0-alpha.6] - 2026-05-29 diff --git a/packages/machine/package.json b/packages/machine/package.json index 342e45f..fb3fc49 100644 --- a/packages/machine/package.json +++ b/packages/machine/package.json @@ -28,10 +28,10 @@ "machine" ], "peerDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.7" + "@turing-machine-js/machine": "^7.0.0-alpha.8" }, "devDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.7" + "@turing-machine-js/machine": "^7.0.0-alpha.8" }, "main": "dist/index.cjs", "module": "dist/index.mjs", From 37cf510efc5e475ac4b5937efb0d0188a60105eb Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Wed, 3 Jun 2026 06:07:46 +0300 Subject: [PATCH 32/34] chore(deps): bump root devDeps + npm audit fix for v7.0.0 cut MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit devDep bumps (all patch/minor): - vitest + @vitest/coverage-v8 4.1.5 → 4.1.8 - rollup 4.60.3 → 4.61.0 - eslint 10.3.0 → 10.4.1 - typescript-eslint 8.59.2 → 8.60.1 - @types/node 25.6.2 → 25.9.1 `npm audit fix` clears 6 dev-only vulns (3 moderate / 3 high) under nx/lerna toolchain — no production-runtime exposure. Verified locally: build, test (332/332), lint, typecheck, coverage 100/100/100/100 floor held. Preparation for the v7.0.0 stable cut on PR #92. --- package-lock.json | 1092 +++++++++++++++++---------------------------- package.json | 12 +- 2 files changed, 424 insertions(+), 680 deletions(-) diff --git a/package-lock.json b/package-lock.json index e5e4331..11fd0d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,14 +15,14 @@ }, "devDependencies": { "@tsconfig/recommended": "^1.0.13", - "@types/node": "^25.6.2", - "@vitest/coverage-v8": "^4.1.5", - "eslint": "^10.3.0", + "@types/node": "^25.9.1", + "@vitest/coverage-v8": "^4.1.8", + "eslint": "^10.4.1", "lerna": "^9.0.7", - "rollup": "^4.60.3", + "rollup": "^4.61.0", "typescript": "^6.0.3", - "typescript-eslint": "^8.59.2", - "vitest": "^4.1.5" + "typescript-eslint": "^8.60.1", + "vitest": "^4.1.8" }, "engines": { "npm": ">=7.0.0" @@ -166,9 +166,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", - "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -202,9 +202,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", - "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -952,19 +952,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@npmcli/arborist/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@npmcli/fs": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", @@ -978,19 +965,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/@npmcli/fs/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@npmcli/git": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz", @@ -1051,19 +1025,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@npmcli/git/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@npmcli/git/node_modules/which": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", @@ -1195,19 +1156,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@npmcli/metavuln-calculator/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@npmcli/name-from-folder": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@npmcli/name-from-folder/-/name-from-folder-3.0.0.tgz", @@ -1335,19 +1283,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@npmcli/package-json/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@npmcli/promise-spawn": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", @@ -1509,23 +1444,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@nx/devkit/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@nx/nx-darwin-arm64": { - "version": "22.7.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-22.7.1.tgz", - "integrity": "sha512-m00ZmBn39VUgb0Ahhu5iY6D56ETdXjDbVnOz0XF3DacJrcLtq9sZ+cg1bj6PshqtvRWVg+zJRrZBU6vL7hGuFQ==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-22.7.5.tgz", + "integrity": "sha512-eoPtwx0qZqvRUD+VVOHm150AlSYwYoPxkDHBBGqKCn5nzPspb0lLWw8q83crM/L1M928YgK0WmGf3C++7eqsTA==", "cpu": [ "arm64" ], @@ -1537,9 +1459,9 @@ ] }, "node_modules/@nx/nx-darwin-x64": { - "version": "22.7.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-22.7.1.tgz", - "integrity": "sha512-DmD8Qow+Yt7Yrmjlz1AsfiwxW+0kRzg+6MY70+d7qChtD2bTzvA/k0ut8SMy+CxU3kxgUbKhGOtml5JDXoX2ww==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-22.7.5.tgz", + "integrity": "sha512-VLOn/ZoEn3HfjSj+yIHLCM56/el79r+9I28CkZNHaSXJQWZ3edSkcgcfYjVxCurpN2VEwDQHLBeFCH8M+lQ7wQ==", "cpu": [ "x64" ], @@ -1551,9 +1473,9 @@ ] }, "node_modules/@nx/nx-freebsd-x64": { - "version": "22.7.1", - "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-22.7.1.tgz", - "integrity": "sha512-HboVrUCHcuYTXtuX3dMyRszP7JO90ZVBLWgnmaM7jUM7jnllZjmezUMtpNHfN1GQbVFafJf/NBShDWsu9LuaUA==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-22.7.5.tgz", + "integrity": "sha512-LEVer/E2xfGvK9Go+imMQoEninOoq/38Z2bhV1SD3AThXrp1xaLFVkW5jQ6juebeVkAeztEoMLFlr576egS0vw==", "cpu": [ "x64" ], @@ -1565,9 +1487,9 @@ ] }, "node_modules/@nx/nx-linux-arm-gnueabihf": { - "version": "22.7.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-22.7.1.tgz", - "integrity": "sha512-5Gm8Y7L8WXMLUjHhiy1eqGz5/PiRw1YLanFg5audBNkZvH6Jkwzdpoz0dbeKjwMDHz4NmniUV1s76Th8VLWmiQ==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-22.7.5.tgz", + "integrity": "sha512-NP27EFGpmFJM6RL1Ey/AFJ7gA2xuqtIHaw6jjSNGvfrnZRUNaway30GrVaGGeODf0DsvAty/unqoBMPy6kDHbw==", "cpu": [ "arm" ], @@ -1579,9 +1501,9 @@ ] }, "node_modules/@nx/nx-linux-arm64-gnu": { - "version": "22.7.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-22.7.1.tgz", - "integrity": "sha512-GdgPYMfbijBRFJs1absL/9QdSNLsTAGdyKykDf9CaVxEMZ92VB+pncpX9Vn/ZBCSeeWTLF+bSK3UM5v+loIObQ==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-22.7.5.tgz", + "integrity": "sha512-QLnkJl3HkHsPfpLiNiAiMfpfAeFpic0U1diAxF8RqChOkCpQ7ulvyBVgE1UrQxvhd+gFQ3ed5RNDxtCRw8nTiw==", "cpu": [ "arm64" ], @@ -1593,9 +1515,9 @@ ] }, "node_modules/@nx/nx-linux-arm64-musl": { - "version": "22.7.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-22.7.1.tgz", - "integrity": "sha512-HyBgPtY1hyNTk8683nt7F29jh3lVdS/zul9vS0NgKeCSoYL3GRM3nLoTPynoHUxyVP/tWYOE3ymvnk92qYwL4Q==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-22.7.5.tgz", + "integrity": "sha512-cEP6KmwBgnb38+jTTaibWCjwXcHmigqhTfy0tN1be7WZr6bHxbqNLsXqKRN70PSNA3HouZcxw1cdRL8tqbPBBA==", "cpu": [ "arm64" ], @@ -1607,9 +1529,9 @@ ] }, "node_modules/@nx/nx-linux-x64-gnu": { - "version": "22.7.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-22.7.1.tgz", - "integrity": "sha512-bQBgRiEsanNvKcDOjVAUPjvcp0iDLofYYUL2af2iuCDxreLOej+J6MeA5bWTLNly5ly1d4voKGTqa+OsouVyLg==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-22.7.5.tgz", + "integrity": "sha512-tbaX1tZCSpGifDNBfDdEZAMxVF3Yg4bhFP/bm1needc0diqb+Zflc0u5tM5/6BWDMITQDwenJVsNiQ8ZdtJURA==", "cpu": [ "x64" ], @@ -1621,9 +1543,9 @@ ] }, "node_modules/@nx/nx-linux-x64-musl": { - "version": "22.7.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-22.7.1.tgz", - "integrity": "sha512-gcco2GjcAztF/fRcAgFxtWxrWDnQdNmPaAN9FTt1+qQ9RUSLvdL8bQxKx4Kd9N9T+gXPlrWhMkBkKbbV09+X1Q==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-22.7.5.tgz", + "integrity": "sha512-H0M7csOZIgPT822LqjxSXzf4MXRND15vIkAQe3F3Jlr3Si8LC3tzbL52aVcRfgb8MF/xOB5U47mSwxWt1M2bPQ==", "cpu": [ "x64" ], @@ -1635,9 +1557,9 @@ ] }, "node_modules/@nx/nx-win32-arm64-msvc": { - "version": "22.7.1", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-22.7.1.tgz", - "integrity": "sha512-IT9oEn0YQ83iPH7666aoPyTRsUzBIBJdBLMXeLX4I60fHPXWhUSGpfiLtIsgU2OfeOVb9hU9idwNh1wc4u9rWQ==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-22.7.5.tgz", + "integrity": "sha512-JTcZch9YAnDL1gbhqePz3DZ4x7iYemLn1yJzrjbbXAmXju2eiiJiZvJJHbV06+SP9HKXDT8RjTKuAWTdVxnHug==", "cpu": [ "arm64" ], @@ -1649,9 +1571,9 @@ ] }, "node_modules/@nx/nx-win32-x64-msvc": { - "version": "22.7.1", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-22.7.1.tgz", - "integrity": "sha512-P2zeSKXVH2Eiwsb8UfP2rMMS7//cHWpiO4M9zt6q0c4lI/hN1vXBciRKVWruGk9ZrWLHuhaMAhG94+MJtzKuRQ==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-22.7.5.tgz", + "integrity": "sha512-ngcMyHdBJ9FSz2nHdbZ7gtJlFq0O2b05sPAsVMkZ18CKzdaA1qrBDJfsMO49hPCny505eiT766+CkKdaCDl5kA==", "cpu": [ "x64" ], @@ -1837,9 +1759,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.128.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.128.0.tgz", - "integrity": "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==", + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", "dev": true, "license": "MIT", "funding": { @@ -1851,9 +1773,9 @@ "link": true }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.18.tgz", - "integrity": "sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", "cpu": [ "arm64" ], @@ -1868,9 +1790,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.18.tgz", - "integrity": "sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -1885,9 +1807,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.18.tgz", - "integrity": "sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -1902,9 +1824,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.18.tgz", - "integrity": "sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", "cpu": [ "x64" ], @@ -1919,9 +1841,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.18.tgz", - "integrity": "sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", "cpu": [ "arm" ], @@ -1936,9 +1858,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.18.tgz", - "integrity": "sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", "cpu": [ "arm64" ], @@ -1953,9 +1875,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.18.tgz", - "integrity": "sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", "cpu": [ "arm64" ], @@ -1970,9 +1892,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.18.tgz", - "integrity": "sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", "cpu": [ "ppc64" ], @@ -1987,9 +1909,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.18.tgz", - "integrity": "sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", "cpu": [ "s390x" ], @@ -2004,9 +1926,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.18.tgz", - "integrity": "sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", "cpu": [ "x64" ], @@ -2021,9 +1943,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.18.tgz", - "integrity": "sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", "cpu": [ "x64" ], @@ -2038,9 +1960,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.18.tgz", - "integrity": "sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", "cpu": [ "arm64" ], @@ -2055,9 +1977,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.18.tgz", - "integrity": "sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", "cpu": [ "wasm32" ], @@ -2138,9 +2060,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.18.tgz", - "integrity": "sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", "cpu": [ "arm64" ], @@ -2155,9 +2077,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.18.tgz", - "integrity": "sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", "cpu": [ "x64" ], @@ -2172,16 +2094,16 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz", - "integrity": "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", - "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.0.tgz", + "integrity": "sha512-dnxczajOqt0gesZlN5pGQ1s1imQVrsmCw5G2Ci4oM+0WvNz3pyRnlWrT7McoZIb8VlFwCawdmbWRmxRn7HI+VQ==", "cpu": [ "arm" ], @@ -2193,9 +2115,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", - "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.0.tgz", + "integrity": "sha512-Bp3JpGP00Vu3f238ivRrjf7z3xSzVPXqCmaJYA9t2c+c8vKYvOzmXF7LkkeUalTEGd6cZcSWe+PFIP3Vy48fRg==", "cpu": [ "arm64" ], @@ -2207,9 +2129,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", - "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.0.tgz", + "integrity": "sha512-zaYIpr670mUmmZ1tVzUFplbQbG7h3Gugx3L5FoqhsC2m/YnLlR1a7zVLmXNPy+iY1tFPEbNG+HHBXZGyId0G5w==", "cpu": [ "arm64" ], @@ -2221,9 +2143,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", - "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.0.tgz", + "integrity": "sha512-+P49fvkv2dSoeevUW+lgZ/I2JHSsJCK1Lyjj7Cu6E4UHG4tS9XIefzIjo5qhgELjAclnen1rLzK2PMKJdo+Dyg==", "cpu": [ "x64" ], @@ -2235,9 +2157,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", - "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.0.tgz", + "integrity": "sha512-l3FAAOyKJXH2ea6KNFN+MMgC/rnE94YGLXs2ehYqDcCoHt1DpvgWX75BhUJxN38XojP7Ul+4H8PRn7EdyqSDrw==", "cpu": [ "arm64" ], @@ -2249,9 +2171,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", - "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.0.tgz", + "integrity": "sha512-VokPN3TSctKj65cyCNPaUh4vMFA8awxOot/0sp+4J7ZlNRKQEhXhawqPwajoi8H5ZFt61i0ugZJuTKXBjGJ17Q==", "cpu": [ "x64" ], @@ -2263,9 +2185,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", - "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.0.tgz", + "integrity": "sha512-DxH0P3wxm+Yzs/p3zrk9dw1rURu8p0Nv5+MRK/L7OtnLNg5rLZraSBFZ8iUXOd9f2BlhJyEpIZUH/emjq4UJ4g==", "cpu": [ "arm" ], @@ -2277,9 +2199,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", - "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.0.tgz", + "integrity": "sha512-T6ZvMNe84kAz6TBWHC7hGAoEtzP1LWYw/AqayGWEF6uISt3Abk/st06LqRD9THd7Xz3NxzurUpzAuEAUbZf+nw==", "cpu": [ "arm" ], @@ -2291,9 +2213,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", - "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.0.tgz", + "integrity": "sha512-q/4hzvQkDs8b4jIBab1pnLiiM0ayTZsN2amBFPDzuyZxjEd4wDwx0UJFYM3cOZzSf5Kw8fnWSprJzIBMkcR44Q==", "cpu": [ "arm64" ], @@ -2305,9 +2227,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", - "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.0.tgz", + "integrity": "sha512-vvYWX3akdEAY6km+9wAqFDnk6pQsbJKVnj7xawcvs/+fdlYBGp+U+Qq/lLfpIxYIZvZLHMAKD9HLdacSx/r3dw==", "cpu": [ "arm64" ], @@ -2319,9 +2241,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", - "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.0.tgz", + "integrity": "sha512-DePa5cqOxDP/Zp0VOXpeWaGew5iIv5DXp9NYbzkX5PFQyWVX9184WCTh3hvr/7lhXo8ZVlbFLkz8+o/q1dU6gA==", "cpu": [ "loong64" ], @@ -2333,9 +2255,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", - "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.0.tgz", + "integrity": "sha512-LV8aWMB8UChglMCEzs7RkN0GsH29RJaLLqwm9fCIjlqwxQTiWAqNcc7wjBkH31hV0PU/yVxGYvrYsgfea2qw6g==", "cpu": [ "loong64" ], @@ -2347,9 +2269,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", - "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.0.tgz", + "integrity": "sha512-QoNSnwQtaeNu5grdBbsL0tt1uyl5EnS8DA8Mr3nluMXbhdQNyhN+G4tBax7VCdxLKj8YJ0/4OO9Ho84jMnJtKA==", "cpu": [ "ppc64" ], @@ -2361,9 +2283,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", - "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.0.tgz", + "integrity": "sha512-/zZp5MKapIIApE8trN8qLGNSiRN9TUoaUZ1cmVu4XnVdd5LQLOXTtyi+vtfUbNnT3iyjzpPqYeKXmvJ+gJGYWw==", "cpu": [ "ppc64" ], @@ -2375,9 +2297,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", - "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.0.tgz", + "integrity": "sha512-RbrzcD3aJ1k3UbtMRRBNwojdVVyXjuVAFTfn/xPa6EEl6GE9Sm/akPgFTb9aAC9pMKGJ6CtWxaGrqWcabH+ySg==", "cpu": [ "riscv64" ], @@ -2389,9 +2311,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", - "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.0.tgz", + "integrity": "sha512-ZF+onDsBso8PJf1XaG9lB+O9RnBpKGnY6OrzC4CSHrtC1jb6jWLTKK4bRqdoCXHd22gyr2hiYmEAm8Wns/BOCw==", "cpu": [ "riscv64" ], @@ -2403,9 +2325,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", - "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.0.tgz", + "integrity": "sha512-Atk0aSIk5Zx2Wuh9dgRQgLP0Koc8hOeYpbWryMXyk8G8/HmPkwPPkMqIIDhrXHHYqfUzSJA/I7IWSBv8xSmRBA==", "cpu": [ "s390x" ], @@ -2417,9 +2339,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", - "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.0.tgz", + "integrity": "sha512-0uMOcf3eZ5K+K4cYHkdxShFMPlPXCOdfDFEFn9dNYAEEd2cVvmOfH7zFgRVoDgmtQ1m9k5q7qfrHzyMAubKYUA==", "cpu": [ "x64" ], @@ -2431,9 +2353,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", - "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.0.tgz", + "integrity": "sha512-mvFtE4A/t/7hRJ7X8Ozmu8FsIkAUat2nzl12pgU337BRmq87AQUJztwHz2Zv5/tjo9/C95E66CK03SI/ToEDJw==", "cpu": [ "x64" ], @@ -2445,9 +2367,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", - "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.0.tgz", + "integrity": "sha512-z9b9+aTxvt8n2rNltMPvyaUfB8NJ+CVyOrGK/MdIKHx7B+lXmZpm/XbRsU7Rpf3fRqJ2uS6mBJiJveCtq8LHDg==", "cpu": [ "x64" ], @@ -2459,9 +2381,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", - "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.0.tgz", + "integrity": "sha512-jXaXFqKMehsOc+g8R6oo33RRC6w07G9jDBxAE5eAKX7mOcCbZloYIPNhfG9Wl+P9O9IWHFO4OJgPi1Ml2qkt7w==", "cpu": [ "arm64" ], @@ -2473,9 +2395,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", - "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.0.tgz", + "integrity": "sha512-OXNWVFocS2IA4+QplhTZZ2a+8hPZR7T8KuozsNmJKK8y7cp83StHvGksfHzPG3wczWTczyWHVQuqeiTUbjiyBg==", "cpu": [ "arm64" ], @@ -2487,9 +2409,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", - "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.0.tgz", + "integrity": "sha512-AlAbNtBO637LxSldqV43z0FfXoGfl2TW1DgAg/bs7aQswFbDewz2SJm3BUhiGfbOVtW571xbc9p+REdxhyN/Eg==", "cpu": [ "ia32" ], @@ -2501,9 +2423,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", - "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.0.tgz", + "integrity": "sha512-QRSrQXyJ1M4tjNXdR0/G/IgV6lzfQQJYBjlWIEYkY2Xs86DRl/iEpQ4blMDjJxSl7n19eDKKXMg0AmuBVYy8pQ==", "cpu": [ "x64" ], @@ -2515,9 +2437,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", - "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.0.tgz", + "integrity": "sha512-tkuFxhvKO/HlGd0VsINF6vHSYH8AF8W0TcNxKDK6JZmrehngFj78pToc8iemtnvwilDjs2G/qSzYFhe9U8q+fw==", "cpu": [ "x64" ], @@ -2784,7 +2706,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -2803,13 +2727,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", - "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.19.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/normalize-package-data": { @@ -2820,17 +2744,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", - "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", + "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/type-utils": "8.59.2", - "@typescript-eslint/utils": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/type-utils": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -2843,7 +2767,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.59.2", + "@typescript-eslint/parser": "^8.60.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -2859,16 +2783,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.2.tgz", - "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", + "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3" }, "engines": { @@ -2884,14 +2808,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", - "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", + "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.2", - "@typescript-eslint/types": "^8.59.2", + "@typescript-eslint/tsconfig-utils": "^8.60.1", + "@typescript-eslint/types": "^8.60.1", "debug": "^4.4.3" }, "engines": { @@ -2906,14 +2830,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz", - "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", + "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2" + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2924,9 +2848,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", - "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", + "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", "dev": true, "license": "MIT", "engines": { @@ -2941,15 +2865,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz", - "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", + "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/utils": "8.59.2", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/utils": "8.60.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -2966,9 +2890,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz", - "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", + "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", "dev": true, "license": "MIT", "engines": { @@ -2980,16 +2904,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", - "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", + "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.59.2", - "@typescript-eslint/tsconfig-utils": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/visitor-keys": "8.59.2", + "@typescript-eslint/project-service": "8.60.1", + "@typescript-eslint/tsconfig-utils": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -3007,23 +2931,10 @@ "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -3038,16 +2949,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.2.tgz", - "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", + "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.2", - "@typescript-eslint/types": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2" + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3062,13 +2973,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", - "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", + "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/types": "8.60.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -3080,14 +2991,14 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.5.tgz", - "integrity": "sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz", + "integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.5", + "@vitest/utils": "4.1.8", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -3101,8 +3012,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.5", - "vitest": "4.1.5" + "@vitest/browser": "4.1.8", + "vitest": "4.1.8" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -3121,16 +3032,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", - "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.5", - "@vitest/utils": "4.1.5", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -3139,13 +3050,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", - "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.5", + "@vitest/spy": "4.1.8", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -3166,9 +3077,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", - "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", "dev": true, "license": "MIT", "dependencies": { @@ -3179,13 +3090,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", - "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.5", + "@vitest/utils": "4.1.8", "pathe": "^2.0.3" }, "funding": { @@ -3193,14 +3104,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", - "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.5", - "@vitest/utils": "4.1.5", + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -3209,9 +3120,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", - "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", "dev": true, "license": "MIT", "funding": { @@ -3219,13 +3130,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", - "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.5", + "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -3434,13 +3345,13 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "dev": true, "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } @@ -3560,9 +3471,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -3707,19 +3618,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/cacache/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/cacache/node_modules/ssri": { "version": "13.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", @@ -4148,19 +4046,6 @@ "node": ">=14" } }, - "node_modules/conventional-changelog-writer/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/conventional-commits-filter": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-3.0.0.tgz", @@ -4218,6 +4103,8 @@ }, "node_modules/convert-source-map": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, @@ -4666,18 +4553,18 @@ } }, "node_modules/eslint": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz", - "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.1.tgz", + "integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.5.5", + "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.1", + "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -5004,9 +4891,9 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, "funding": [ { @@ -5365,19 +5252,6 @@ "node": ">=14" } }, - "node_modules/git-semver-tags/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/git-up": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/git-up/-/git-up-7.0.0.tgz", @@ -5735,19 +5609,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/init-package-json/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/inquirer": { "version": "12.9.6", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.9.6.tgz", @@ -6511,19 +6372,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/libnpmpublish/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -6894,19 +6742,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/make-fetch-happen": { "version": "15.0.2", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.2.tgz", @@ -7463,19 +7298,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/node-gyp/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-gyp/node_modules/which": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", @@ -7550,19 +7372,6 @@ "node": ">=10" } }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/normalize-package-data/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -7596,19 +7405,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm-install-checks/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/npm-normalize-package-bin": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", @@ -7635,19 +7431,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm-package-arg/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/npm-packlist": { "version": "10.0.3", "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.3.tgz", @@ -7711,19 +7494,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm-pick-manifest/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/npm-registry-fetch": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.0.tgz", @@ -7756,9 +7526,9 @@ } }, "node_modules/nx": { - "version": "22.7.1", - "resolved": "https://registry.npmjs.org/nx/-/nx-22.7.1.tgz", - "integrity": "sha512-SadJUQY57MiwRIetm9rhZhdpFeOe1Csib2Vg9C423Pw/h0fZE14qUo6+OBby9vLh5QCkRfRZ0WaHkeO5q6yNtA==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/nx/-/nx-22.7.5.tgz", + "integrity": "sha512-zoxsJabb33jl1QYnalDn0bicryrEBgSzdKp90d7VGGv/jDgzKrcLg/hw2ZxeYiOjWPIT/o8QNT9G9vTs4dv3AQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -7776,11 +7546,11 @@ "ansi-styles": "4.3.0", "argparse": "2.0.1", "asynckit": "0.4.0", - "axios": "1.15.0", + "axios": "1.16.0", "balanced-match": "4.0.3", "base64-js": "1.5.1", "bl": "4.1.0", - "brace-expansion": "5.0.2", + "brace-expansion": "5.0.6", "buffer": "5.7.1", "call-bind-apply-helpers": "1.0.2", "chalk": "4.1.2", @@ -7809,7 +7579,7 @@ "escape-string-regexp": "1.0.5", "figures": "3.2.0", "flat": "5.0.2", - "follow-redirects": "1.15.11", + "follow-redirects": "1.16.0", "form-data": "4.0.5", "fs-constants": "1.0.0", "function-bind": "1.1.2", @@ -7837,7 +7607,7 @@ "mime-db": "1.52.0", "mime-types": "2.1.35", "mimic-fn": "2.1.0", - "minimatch": "10.2.4", + "minimatch": "10.2.5", "minimist": "1.2.8", "npm-run-path": "4.0.1", "once": "1.4.0", @@ -7861,7 +7631,7 @@ "strip-bom": "3.0.0", "supports-color": "7.2.0", "tar-stream": "2.2.0", - "tmp": "0.2.4", + "tmp": "0.2.6", "tree-kill": "1.2.2", "tsconfig-paths": "4.2.0", "tslib": "2.8.1", @@ -7870,7 +7640,7 @@ "wrap-ansi": "7.0.0", "wrappy": "1.0.2", "y18n": "5.0.8", - "yaml": "2.8.0", + "yaml": "2.9.0", "yargs": "17.7.2", "yargs-parser": "21.1.1" }, @@ -7879,16 +7649,16 @@ "nx-cloud": "dist/bin/nx-cloud.js" }, "optionalDependencies": { - "@nx/nx-darwin-arm64": "22.7.1", - "@nx/nx-darwin-x64": "22.7.1", - "@nx/nx-freebsd-x64": "22.7.1", - "@nx/nx-linux-arm-gnueabihf": "22.7.1", - "@nx/nx-linux-arm64-gnu": "22.7.1", - "@nx/nx-linux-arm64-musl": "22.7.1", - "@nx/nx-linux-x64-gnu": "22.7.1", - "@nx/nx-linux-x64-musl": "22.7.1", - "@nx/nx-win32-arm64-msvc": "22.7.1", - "@nx/nx-win32-x64-msvc": "22.7.1" + "@nx/nx-darwin-arm64": "22.7.5", + "@nx/nx-darwin-x64": "22.7.5", + "@nx/nx-freebsd-x64": "22.7.5", + "@nx/nx-linux-arm-gnueabihf": "22.7.5", + "@nx/nx-linux-arm64-gnu": "22.7.5", + "@nx/nx-linux-arm64-musl": "22.7.5", + "@nx/nx-linux-x64-gnu": "22.7.5", + "@nx/nx-linux-x64-musl": "22.7.5", + "@nx/nx-win32-arm64-msvc": "22.7.5", + "@nx/nx-win32-x64-msvc": "22.7.5" }, "peerDependencies": { "@swc-node/register": "^1.11.1", @@ -7930,19 +7700,6 @@ "node": "20 || >=22" } }, - "node_modules/nx/node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/nx/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -7963,22 +7720,6 @@ "node": ">= 4" } }, - "node_modules/nx/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/nx/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -8443,19 +8184,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/pacote/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/pacote/node_modules/which": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", @@ -8654,9 +8382,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -8674,7 +8402,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -9177,14 +8905,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.18.tgz", - "integrity": "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.128.0", - "@rolldown/pluginutils": "1.0.0-rc.18" + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" @@ -9193,31 +8921,31 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.18", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.18", - "@rolldown/binding-darwin-x64": "1.0.0-rc.18", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.18", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.18", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.18", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.18", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.18", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.18", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.18", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.18", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.18", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.18", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.18", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" } }, "node_modules/rollup": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", - "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.0.tgz", + "integrity": "sha512-T9mWdbWfQtp0B5lv/HX+wrhYsmXRlcWnXXmJbXqKJhlRaoS6KMhq0gpyzW4UJfclcxrEdLnTgjT2NjruLONu0g==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@types/estree": "1.0.9" }, "bin": { "rollup": "dist/bin/rollup" @@ -9227,31 +8955,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.3", - "@rollup/rollup-android-arm64": "4.60.3", - "@rollup/rollup-darwin-arm64": "4.60.3", - "@rollup/rollup-darwin-x64": "4.60.3", - "@rollup/rollup-freebsd-arm64": "4.60.3", - "@rollup/rollup-freebsd-x64": "4.60.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", - "@rollup/rollup-linux-arm-musleabihf": "4.60.3", - "@rollup/rollup-linux-arm64-gnu": "4.60.3", - "@rollup/rollup-linux-arm64-musl": "4.60.3", - "@rollup/rollup-linux-loong64-gnu": "4.60.3", - "@rollup/rollup-linux-loong64-musl": "4.60.3", - "@rollup/rollup-linux-ppc64-gnu": "4.60.3", - "@rollup/rollup-linux-ppc64-musl": "4.60.3", - "@rollup/rollup-linux-riscv64-gnu": "4.60.3", - "@rollup/rollup-linux-riscv64-musl": "4.60.3", - "@rollup/rollup-linux-s390x-gnu": "4.60.3", - "@rollup/rollup-linux-x64-gnu": "4.60.3", - "@rollup/rollup-linux-x64-musl": "4.60.3", - "@rollup/rollup-openbsd-x64": "4.60.3", - "@rollup/rollup-openharmony-arm64": "4.60.3", - "@rollup/rollup-win32-arm64-msvc": "4.60.3", - "@rollup/rollup-win32-ia32-msvc": "4.60.3", - "@rollup/rollup-win32-x64-gnu": "4.60.3", - "@rollup/rollup-win32-x64-msvc": "4.60.3", + "@rollup/rollup-android-arm-eabi": "4.61.0", + "@rollup/rollup-android-arm64": "4.61.0", + "@rollup/rollup-darwin-arm64": "4.61.0", + "@rollup/rollup-darwin-x64": "4.61.0", + "@rollup/rollup-freebsd-arm64": "4.61.0", + "@rollup/rollup-freebsd-x64": "4.61.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.0", + "@rollup/rollup-linux-arm-musleabihf": "4.61.0", + "@rollup/rollup-linux-arm64-gnu": "4.61.0", + "@rollup/rollup-linux-arm64-musl": "4.61.0", + "@rollup/rollup-linux-loong64-gnu": "4.61.0", + "@rollup/rollup-linux-loong64-musl": "4.61.0", + "@rollup/rollup-linux-ppc64-gnu": "4.61.0", + "@rollup/rollup-linux-ppc64-musl": "4.61.0", + "@rollup/rollup-linux-riscv64-gnu": "4.61.0", + "@rollup/rollup-linux-riscv64-musl": "4.61.0", + "@rollup/rollup-linux-s390x-gnu": "4.61.0", + "@rollup/rollup-linux-x64-gnu": "4.61.0", + "@rollup/rollup-linux-x64-musl": "4.61.0", + "@rollup/rollup-openbsd-x64": "4.61.0", + "@rollup/rollup-openharmony-arm64": "4.61.0", + "@rollup/rollup-win32-arm64-msvc": "4.61.0", + "@rollup/rollup-win32-ia32-msvc": "4.61.0", + "@rollup/rollup-win32-x64-gnu": "4.61.0", + "@rollup/rollup-win32-x64-msvc": "4.61.0", "fsevents": "~2.3.2" } }, @@ -9303,6 +9031,19 @@ "dev": true, "license": "MIT" }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "dev": true, @@ -9696,9 +9437,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", - "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, "license": "MIT", "engines": { @@ -9733,9 +9474,9 @@ } }, "node_modules/tmp": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz", - "integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==", + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.6.tgz", + "integrity": "sha512-5sJPdPjfI5Kx+qbrDesxkglRBxW//g7hCsqspEjwkewGvBMGIKMOTKzLt1hFVJzyadba3lDUN20O9qhvbQUSTA==", "dev": true, "license": "MIT", "engines": { @@ -9865,16 +9606,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.59.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.2.tgz", - "integrity": "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.1.tgz", + "integrity": "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.59.2", - "@typescript-eslint/parser": "8.59.2", - "@typescript-eslint/typescript-estree": "8.59.2", - "@typescript-eslint/utils": "8.59.2" + "@typescript-eslint/eslint-plugin": "8.60.1", + "@typescript-eslint/parser": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/utils": "8.60.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9913,9 +9654,9 @@ } }, "node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, "license": "MIT" }, @@ -9986,17 +9727,17 @@ } }, "node_modules/vite": { - "version": "8.0.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.11.tgz", - "integrity": "sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.14", - "rolldown": "1.0.0-rc.18", - "tinyglobby": "^0.2.16" + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -10064,9 +9805,9 @@ } }, "node_modules/vite/node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -10081,19 +9822,19 @@ } }, "node_modules/vitest": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", - "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.5", - "@vitest/mocker": "4.1.5", - "@vitest/pretty-format": "4.1.5", - "@vitest/runner": "4.1.5", - "@vitest/snapshot": "4.1.5", - "@vitest/spy": "4.1.5", - "@vitest/utils": "4.1.5", + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -10121,12 +9862,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.5", - "@vitest/browser-preview": "4.1.5", - "@vitest/browser-webdriverio": "4.1.5", - "@vitest/coverage-istanbul": "4.1.5", - "@vitest/coverage-v8": "4.1.5", - "@vitest/ui": "4.1.5", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -10171,9 +9912,9 @@ } }, "node_modules/vitest/node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -10346,9 +10087,9 @@ } }, "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, "license": "ISC", "bin": { @@ -10356,6 +10097,9 @@ }, "engines": { "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yargs": { diff --git a/package.json b/package.json index 9050421..d3ebb5a 100644 --- a/package.json +++ b/package.json @@ -20,14 +20,14 @@ ], "devDependencies": { "@tsconfig/recommended": "^1.0.13", - "@types/node": "^25.6.2", - "@vitest/coverage-v8": "^4.1.5", - "eslint": "^10.3.0", + "@types/node": "^25.9.1", + "@vitest/coverage-v8": "^4.1.8", + "eslint": "^10.4.1", "lerna": "^9.0.7", - "rollup": "^4.60.3", + "rollup": "^4.61.0", "typescript": "^6.0.3", - "typescript-eslint": "^8.59.2", - "vitest": "^4.1.5" + "typescript-eslint": "^8.60.1", + "vitest": "^4.1.8" }, "dependencies": { "@turing-machine-js/machine": "^7.0.0-alpha.5" From 7aa4aee683793a5382d60c9d180ec84eef7ed251 Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Wed, 3 Jun 2026 06:18:32 +0300 Subject: [PATCH 33/34] release: v7.0.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stable v7 release. Bumps @post-machine-js/machine from 7.0.0-alpha.7 to 7.0.0. CHANGELOG entry consolidates the v7 trajectory across alphas 2-7 (adoption of engine v7 composition overhaul + debug-surface reshape, instruction-derived state names + arrivalPath, path-based tag registry, stepInstruction). Peer dep @turing-machine-js/machine stays at ^7.0.0-alpha.8 in this commit — widening to ^7.0.0 lands in a follow-up commit on this branch after engine 7.0.0 publishes to npm. --- lerna.json | 2 +- package-lock.json | 4 +-- package.json | 2 +- packages/machine/CHANGELOG.md | 61 +++++++++++++++++++++++++++++++++++ packages/machine/package.json | 2 +- 5 files changed, 66 insertions(+), 5 deletions(-) diff --git a/lerna.json b/lerna.json index 33d3137..e9b371b 100644 --- a/lerna.json +++ b/lerna.json @@ -3,6 +3,6 @@ "packages": [ "packages/*" ], - "version": "7.0.0-alpha.6", + "version": "7.0.0", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 11fd0d8..d30fccc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "packages/*" ], "dependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.5" + "@turing-machine-js/machine": "^7.0.0-alpha.8" }, "devDependencies": { "@tsconfig/recommended": "^1.0.13", @@ -10185,7 +10185,7 @@ }, "packages/machine": { "name": "@post-machine-js/machine", - "version": "7.0.0-alpha.7", + "version": "7.0.0", "license": "GPL-3.0-or-later", "devDependencies": { "@turing-machine-js/machine": "^7.0.0-alpha.8" diff --git a/package.json b/package.json index d3ebb5a..6c0b6d1 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,6 @@ "vitest": "^4.1.8" }, "dependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.5" + "@turing-machine-js/machine": "^7.0.0-alpha.8" } } diff --git a/packages/machine/CHANGELOG.md b/packages/machine/CHANGELOG.md index 68a2285..9ed91d1 100644 --- a/packages/machine/CHANGELOG.md +++ b/packages/machine/CHANGELOG.md @@ -4,6 +4,67 @@ All notable changes to this project will be documented in this file. 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). +## [7.0.0] - 2026-06-03 + +Stable v7. Adopts engine v7's composition-representation overhaul and reshaped debug surface. See alpha.2 through alpha.7 entries below for the step-by-step trajectory; this entry consolidates the cumulative public-API changes from v6.4.0. + +### Added + +- **Instruction-derived state names** (alpha.2-onwards, baseline from [#67](https://github.com/mellonis/post-machine-js/issues/67) in v6.1.0). Top-level instructions are labeled `"10"`, subroutine body instructions `"foo::1"`, group inners `"50.2"`, continuation states `"foo>10~30"`. Replaces the `id:N` global-counter placeholders. +- **`MachineState.arrivalPath` + `MachineState.candidatePaths`** ([#70](https://github.com/mellonis/post-machine-js/issues/70)) — runtime instruction-level context on every yield. `arrivalPath` reports the specific instruction the engine just transitioned through; `candidatePaths` exposes the full set of paths sharing the current State. +- **Path-based State resolver** ([#63](https://github.com/mellonis/post-machine-js/issues/63)) — `pm.stateAt(path)`, `pm.hasState(path)`, `pm.candidatesFor(path)`. Accepts string (`'foo::10.2'`) and object forms. +- **Per-instruction breakpoint registry** ([#59](https://github.com/mellonis/post-machine-js/issues/59)) — `pm.setBreakpoint(target, filter)`, `pm.clearBreakpoint(target)`, `pm.clearBreakpoints()`, `pm.listBreakpoints()`. +- **Path-based `pm.tag(...)` registry + inline `$tag(...)` decorator + auto-tag policy** ([#86](https://github.com/mellonis/post-machine-js/issues/86)) on top of engine [#186](https://github.com/mellonis/turing-machine-js/issues/186)'s state-tags surface. `pm.tag(path, ...tags)` / `pm.untag(path, ...tags)` / `pm.tagsOf(path)` / `pm.findByTag(tag)`. `$tag('hot', 'sampled', mark)` inline decorator. Auto-tags entry of each program (`'main'`) and subroutine (the subroutine name). +- **`pm.debugRun({ stepsLimit? })` → `PostDebugSession`** (engine [#102](https://github.com/mellonis/turing-machine-js/issues/102) adoption) — the interactive debugger surface. Wraps the engine's `DebugSession`, re-adds the post-level `MachineState` fields, and applies the per-instruction breakpoint registry as a pause filter. Emits `pause` / `step` / `iter` / `halt` events; drive with `continue()` / `stepIn()` / `stepOver()` / `stepOut()` / `pause()` / `stop()` / `setRunInterval(ms)`. +- **`PostPausedMachineState`** — `MachineState & { pause: PauseInfo }`, the `pause`-event payload. `PauseInfo = { side: 'before' | 'after', cause: 'breakpoint' | 'step' | 'manual' }`. +- **`PostDebugSession.stepInstruction()`** ([#101](https://github.com/mellonis/post-machine-js/issues/101)) — the Post-level program-counter step. Advances to the next numbered instruction in the *current* scope; sub-step transitions inside groups (`50.1 → 50.2`) and descents into called scopes (`call('foo') → foo::1`) stay silent. + +### Changed + +- **`withOverrodeHaltState` → `withOverriddenHaltState`** (engine [#149](https://github.com/mellonis/turing-machine-js/issues/149), post [#82](https://github.com/mellonis/post-machine-js/issues/82)). Consumer-side rename; hard cutover, no deprecated alias. +- **Wrapper composite shape `A>B` → `A(B)`** (engine [#148](https://github.com/mellonis/turing-machine-js/issues/148), post [#83](https://github.com/mellonis/post-machine-js/issues/83)). `parsePath` now rejects `(`/`)` in user-provided state names. Post's `Path` separators (`::`, `.`, `~`) survive unchanged. +- **Subroutine "hopper" State dropped for acyclic subroutines with plain leading instructions** ([#85](https://github.com/mellonis/post-machine-js/issues/85)). Common case wraps `foo::1` directly, saving one State per call site. Composite wrapper name shifts `foo(continuation)` → `foo::1(continuation)`. Hopper retained for cyclic subs (Tarjan SCC), degenerate `{ 1: stop }` bodies, and leading-group / leading-call cases. +- **`toMermaid` callable-subtree emit** (engine [#174](https://github.com/mellonis/turing-machine-js/issues/174)). The wrapper composite is a `[[bare(continuation)]]` call site OUTSIDE the subgraph; the bare + body live INSIDE `subgraph w_N["callable subtree of NAME"]`. Bold `==> "call"` / dotted `-. "return" .->` arrows; retired `-. onHalt .->`. +- **`PostMachine.run()` is synchronous and callback-free** — `run({ stepsLimit? }): void` (was `async … : Promise` accepting `onStep` / `onPause`). Mirrors the engine's `run()` change. +- **`runStepByStep()` is the pure-iteration observation path** — unchanged in shape (`Generator`), but it's now where you read `arrivalPath` / `candidatePaths` per step. +- **Module-load `haltState` lockdown dropped** ([PR #94](https://github.com/mellonis/post-machine-js/pull/94)) now that engine [#207](https://github.com/mellonis/turing-machine-js/issues/207) collapsed `haltState.debug` to a boolean. Direct `haltState.debug = boolean` writes go straight to the engine setter; `pm.setBreakpoint(haltState, …)` still works for registry-aware halt pauses. +- **Engine peer dependency widened** `^6.4.0` → `^7.0.0`. v4 / v5 / v6 engine majors are no longer supported. + +### Removed + +- **`__onPause` experimental prefix** — renamed to `onPause` in v6.1.0, fully removed in v7 (callback no longer on `run()` at all). +- **`onStep` / `onPause` callbacks on `pm.run()`** — replaced by `pm.debugRun()` events. Per-iter observation moves to `runStepByStep()`. +- **Per-yield `m.debugBreak` descriptor** — replaced by the one-sided `m.pause: { side, cause }` on the `pause` event only. + +### Migration + +```sh +npm install @turing-machine-js/machine@^7.0.0 @post-machine-js/machine@^7.0.0 +``` + +- Rename `withOverrodeHaltState` → `withOverriddenHaltState` everywhere. +- Drop the `await` on `pm.run()` calls — it's synchronous now. +- Callers using `onStep` / `onPause` callbacks on `pm.run()` move to `pm.debugRun()` event listeners: + + ```js + // before (v6.x): + await pm.run({ onPause: (m) => { /* m.debugBreak */ } }); + + // v7: + const session = pm.debugRun(); + session.on('pause', (m) => { /* m.pause: { side, cause } */ session.continue(); }); + await session.start(); + ``` + +- Consumers reading `m.debugBreak.before` / `m.debugBreak.after` move to `m.pause.side` (`'before'` | `'after'`) and `m.pause.cause` (`'breakpoint'` | `'step'` | `'manual'`). +- `haltState.debug = true` / `false` now works directly (the v6.1 lockdown is gone); object writes (`haltState.debug = { before: true }`) throw the engine's boolean-only error. The state-side `pm.setBreakpoint` / `pm.clearBreakpoint` API still exists for per-PostMachine routing on non-halt States. +- Code parsing wrapper composite names by `>`-splitting (`state.name.split('>')`) needs to switch to paren-parsing. +- Code asserting exact `summarizePostMachine().stateCount` may need to adjust — each call site contributes +1 state from the wrapper/bare separation, and −1 per hopper-dropped subroutine. + +### Out of v7.0.0 (deferred to v7.1) + +- **[#72](https://github.com/mellonis/post-machine-js/issues/72)** — extend `defineProperty` lockdown to intermediate engine-graph states (continuations, hoppers, group wrappers). + ## [7.0.0-alpha.7] - 2026-06-02 Adds **`PostDebugSession.stepInstruction()`** — a Post-level step control that advances to the next numbered instruction in the current scope. Resolves [#101](https://github.com/mellonis/post-machine-js/issues/101). Peer dep `@turing-machine-js/machine` widened `^7.0.0-alpha.6` → `^7.0.0-alpha.8`. Published under the `next` dist-tag: `npm install @post-machine-js/machine@next`. diff --git a/packages/machine/package.json b/packages/machine/package.json index fb3fc49..42b492f 100644 --- a/packages/machine/package.json +++ b/packages/machine/package.json @@ -1,6 +1,6 @@ { "name": "@post-machine-js/machine", - "version": "7.0.0-alpha.7", + "version": "7.0.0", "description": "A convenient Post machine", "engines": { "npm": ">=7.0.0" From f96858e7b314984822fa43c6771418e539c31dbb Mon Sep 17 00:00:00 2001 From: Ruslan Gilmullin Date: Wed, 3 Jun 2026 15:39:20 +0300 Subject: [PATCH 34/34] =?UTF-8?q?deps:=20widen=20@turing-machine-js/machin?= =?UTF-8?q?e=20peer=20+=20dep=20^7.0.0-alpha.8=20=E2=86=92=20^7.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Engine v7.0.0 stable is now on npm (latest dist-tag); widening the peer-dep range from the alpha pin to the stable caret. Mirrors widening in root dependencies and packages/machine peer + dev deps. Verified locally: build, test (332/332), lint, typecheck, coverage 100/100/100/100, publish dry-run confirms @post-machine-js/machine@7.0.0 publishes with tag latest. --- package-lock.json | 12 ++++++------ package.json | 2 +- packages/machine/package.json | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index d30fccc..28e0568 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "packages/*" ], "dependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.8" + "@turing-machine-js/machine": "^7.0.0" }, "devDependencies": { "@tsconfig/recommended": "^1.0.13", @@ -2662,9 +2662,9 @@ } }, "node_modules/@turing-machine-js/machine": { - "version": "7.0.0-alpha.8", - "resolved": "https://registry.npmjs.org/@turing-machine-js/machine/-/machine-7.0.0-alpha.8.tgz", - "integrity": "sha512-/h+Wdy63+siJlEj8jEiNVIiO84cOVaDufPmMHZJUf8LaGIJYjkttqfNkOv0fGOlUYQmDic/+D81w1OZygriuFQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@turing-machine-js/machine/-/machine-7.0.0.tgz", + "integrity": "sha512-6W/uCgrX//pf5epUihgpMAKAr9wIdlEISTYjO/SvaDpOwatcZu/3tDUeyb9kvgZAvQdNLF9JRhZm5N2IpqCM8g==", "license": "GPL-3.0-or-later", "engines": { "npm": ">=7.0.0" @@ -10188,13 +10188,13 @@ "version": "7.0.0", "license": "GPL-3.0-or-later", "devDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.8" + "@turing-machine-js/machine": "^7.0.0" }, "engines": { "npm": ">=7.0.0" }, "peerDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.8" + "@turing-machine-js/machine": "^7.0.0" } } } diff --git a/package.json b/package.json index 6c0b6d1..30eda38 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,6 @@ "vitest": "^4.1.8" }, "dependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.8" + "@turing-machine-js/machine": "^7.0.0" } } diff --git a/packages/machine/package.json b/packages/machine/package.json index 42b492f..47aebef 100644 --- a/packages/machine/package.json +++ b/packages/machine/package.json @@ -28,10 +28,10 @@ "machine" ], "peerDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.8" + "@turing-machine-js/machine": "^7.0.0" }, "devDependencies": { - "@turing-machine-js/machine": "^7.0.0-alpha.8" + "@turing-machine-js/machine": "^7.0.0" }, "main": "dist/index.cjs", "module": "dist/index.mjs",