diff --git a/CLAUDE.md b/CLAUDE.md index 5b9e817..be089b6 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. @@ -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 @@ -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,14 +57,14 @@ 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**: 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 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.) @@ -74,21 +74,45 @@ 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 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.) -The upstream v5/v6 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. + +**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 `(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: - **`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 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/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/lerna.json b/lerna.json index e573776..e9b371b 100644 --- a/lerna.json +++ b/lerna.json @@ -3,6 +3,6 @@ "packages": [ "packages/*" ], - "version": "6.4.0", + "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 3cc37fc..28e0568 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,18 +11,18 @@ "packages/*" ], "dependencies": { - "@turing-machine-js/machine": "^6.4.0" + "@turing-machine-js/machine": "^7.0.0" }, "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" ], @@ -2740,9 +2662,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", + "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" @@ -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": { @@ -10441,16 +10185,16 @@ }, "packages/machine": { "name": "@post-machine-js/machine", - "version": "6.4.0", + "version": "7.0.0", "license": "GPL-3.0-or-later", "devDependencies": { - "@turing-machine-js/machine": "^6.4.0" + "@turing-machine-js/machine": "^7.0.0" }, "engines": { "npm": ">=7.0.0" }, "peerDependencies": { - "@turing-machine-js/machine": "^6.4.0" + "@turing-machine-js/machine": "^7.0.0" } } } diff --git a/package.json b/package.json index 250b070..30eda38 100644 --- a/package.json +++ b/package.json @@ -20,16 +20,16 @@ ], "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": "^6.4.0" + "@turing-machine-js/machine": "^7.0.0" } } diff --git a/packages/machine/CHANGELOG.md b/packages/machine/CHANGELOG.md index 048761d..9ed91d1 100644 --- a/packages/machine/CHANGELOG.md +++ b/packages/machine/CHANGELOG.md @@ -4,6 +4,285 @@ 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`. + +**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.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 + +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`. + +**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`. + +**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`. + +**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`. + +**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 ac21d0c..a3369fc 100644 --- a/packages/machine/README.md +++ b/packages/machine/README.md @@ -16,10 +16,11 @@ 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) +- [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)
@@ -51,60 +52,45 @@ machine.replaceTapeWith(new Tape({ symbols: ['*', '*', ' '], })); -await machine.run(); +machine.run(); 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 %% alphabets: [[" ","*"]] s0(((halt))) - s1(("10")) + s1["10
main"] 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 + classDef tag_main fill:#dbeafe,stroke:#1e40af + class s1 tag_main ``` -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 -- `(("label"))` — entry state (the one passed as `initialState`) -- `["label"]` — intermediate state - -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. +- `["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. 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.) -**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. +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 @@ -115,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()` / **`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. **Properties.** @@ -160,7 +147,7 @@ machine.replaceTapeWith(new Tape({ symbols: ['#', '#', '.'], })); -await machine.run(); +machine.run(); console.log(machine.tape.symbols.join('').replace(/\.+$/, '')); // ### ``` @@ -194,6 +181,89 @@ 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
main"] + s2["20"] + s3["30"] + idle([idle]) + idle -. enter .-> s1 + 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`). + +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
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.) + +
+ +
+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
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. + +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: @@ -212,7 +282,7 @@ machine.replaceTapeWith(new Tape({ position: 0, })); -await machine.run(); +machine.run(); console.log(machine.tape.symbols.join('').trim()); // ** ``` @@ -245,56 +315,50 @@ machine.replaceTapeWith(new Tape({ symbols: ['*', '*', ' '], })); -await machine.run(); +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 `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. - -
-Same graph, as the engine actually emits. The subroutine and the wrapping withOverrodeHaltState are visible: +The state graph as the engine emits it — the subroutine and the wrapping `withOverriddenHaltState` composition are visible: ```mermaid flowchart TD %% alphabets: [[" ","*"]] s0(((halt))) - 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 - s8 -. onHalt .-> s7 - 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"`). -- `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). + s3["1~2"] + s5["2"] + s4[["rightToBlank::1(1~2)
main"]] + idle([idle]) + subgraph w_1["callable subtree of rightToBlank::1"] + s1["rightToBlank::1
rightToBlank"] + s2["rightToBlank::2"] + c1(((halt))) + end + 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 + 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. @@ -325,7 +389,7 @@ extend.replaceTapeWith(new Tape({ position: 1, })); -await extend.run(); +extend.run(); console.log(extend.tape.symbols.join('')); // *** ``` @@ -333,9 +397,9 @@ 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: +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 | |-------------------|----------|------------------------------------------------------------------------------------------| @@ -354,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. @@ -376,25 +438,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,18 +469,176 @@ 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 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 a `runStepByStep()` yield or a `debugRun()` session event (runtime). See [Path-based resolver](#path-based-resolver) and [MachineState shape](#machinestate-shape). + +### 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'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: -### Forward-compatibility with engine v7 +- **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. -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. +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). + +**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 + 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 +``` + +```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 + +```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 @@ -440,9 +660,28 @@ 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
main"] + 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 + 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.) + +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` @@ -483,7 +722,74 @@ 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 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: + +
+inline — flat 4-state graph, no composition + +```mermaid +flowchart TD +%% alphabets: [[" ","*"]] + s0(((halt))) + s1["10
main"] + 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 + 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. + +
+ +
+withSubroutine — wrapper + callable subtree + continuation + +```mermaid +flowchart TD +%% alphabets: [[" ","*"]] + s0(((halt))) + s6["10~20"] + s8["20"] + s7[["walkToBlank::1(10~20)
main"]] + idle([idle]) + subgraph w_4["callable subtree of walkToBlank::1"] + s4["walkToBlank::1
walkToBlank"] + s5["walkToBlank::2"] + c4(((halt))) + end + 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 + 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`: +- **`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 (`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.) + +
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. @@ -511,7 +817,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. @@ -534,9 +840,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`: @@ -554,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`: @@ -573,9 +881,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 the `debugRun()` session's pause filter. The pause fires on the AFTER side of the iter whose transition leads to halt. + Management: ```javascript @@ -584,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 @@ -600,16 +910,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 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, 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/package.json b/packages/machine/package.json index a0f413e..47aebef 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", "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" }, "devDependencies": { - "@turing-machine-js/machine": "^6.4.0" + "@turing-machine-js/machine": "^7.0.0" }, "main": "dist/index.cjs", "module": "dist/index.mjs", diff --git a/packages/machine/test/breakpoints.spec.ts b/packages/machine/src/breakpoints.spec.ts similarity index 85% rename from packages/machine/test/breakpoints.spec.ts rename to packages/machine/src/breakpoints.spec.ts index cc3e9af..5dfeac2 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`', () => { @@ -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(); @@ -203,25 +203,20 @@ 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(); }); 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), @@ -231,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(); }); @@ -250,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(); }); @@ -266,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(); }); @@ -295,9 +296,13 @@ describe('lockdown redirect — direct state.debug writes', () => { }).toThrow(/ambiguous.*'10'.*'30'/); }); - test('haltState debug write throws (no PostMachine context for redirect)', () => { + test('haltState writes go to the engine setter — boolean OK, object throws', () => { + 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/callGraph.spec.ts b/packages/machine/src/callGraph.spec.ts new file mode 100644 index 0000000..eb59ab6 --- /dev/null +++ b/packages/machine/src/callGraph.spec.ts @@ -0,0 +1,137 @@ +import {describe, expect, test} from 'vitest'; + +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`) +// 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/src/callGraph.ts b/packages/machine/src/callGraph.ts new file mode 100644 index 0000000..0271634 --- /dev/null +++ b/packages/machine/src/callGraph.ts @@ -0,0 +1,246 @@ +// 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}; +} + +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/PostDebugSession.ts b/packages/machine/src/classes/PostDebugSession.ts new file mode 100644 index 0000000..f62de06 --- /dev/null +++ b/packages/machine/src/classes/PostDebugSession.ts @@ -0,0 +1,299 @@ +import { + DebugSession as EngineDebugSession, + type MachineState as EngineMachineState, + type PauseInfo, + type PausedMachineState as EnginePausedMachineState, + State, + haltState, +} from '@turing-machine-js/machine'; +import { formatPath, normalizeScope, 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'; + +/** 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 + : E extends 'pause' + ? (machineState: PostPausedMachineState) => void | Promise + : (machineState: MachineState) => void | Promise; + +type ListenerMap = { + pause: Array<(m: PostPausedMachineState) => 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; + /** 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[]; + 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) => { + // 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, + }; + 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 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; + } + // 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 + // 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). + 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', () => { + this.#pendingStepInstruction = null; + this.#lastPausedPath = null; + 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(); + } + + /** + * 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 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 { + 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 — 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); + 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 + } + // 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 + // 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: EnginePausedMachineState, wrapped: MachineState): boolean { + const cause = raw.pause.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/test/custom-alphabet.spec.ts b/packages/machine/src/classes/PostMachine.custom-alphabet.spec.ts similarity index 96% rename from packages/machine/test/custom-alphabet.spec.ts rename to packages/machine/src/classes/PostMachine.custom-alphabet.spec.ts index 4648121..a817413 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)', () => { @@ -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.debugger.spec.ts b/packages/machine/src/classes/PostMachine.debugger.spec.ts new file mode 100644 index 0000000..ec6f358 --- /dev/null +++ b/packages/machine/src/classes/PostMachine.debugger.spec.ts @@ -0,0 +1,454 @@ +// 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, + 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({ + 10: check(20, 30), + 20: right(10), + 30: mark, + 40: stop, + }); + } + + test('run() returns a Promise', () => { + const machine = buildWalkAndMark(); + + machine.replaceTapeWith(new Tape({ + alphabet: machine.tape.alphabet, + symbols: ['*', '*', ' '], + })); + + const result = machine.run(); + // v7: run() is sync, returns void. + expect(result).toBeUndefined(); + }); + + test('run() is synchronous — tape is final immediately after the call', () => { + const machine = buildWalkAndMark(); + + machine.replaceTapeWith(new Tape({ + alphabet: machine.tape.alphabet, + symbols: ['*', '*', ' '], + })); + + // Before run runs, the tape should be the input. + expect(machine.tape.symbols.join('').trim()).toBe('**'); + + machine.run(); + + expect(machine.tape.symbols.join('').trim()).toBe('***'); + }); + + test('onStep still observes every step', async () => { + const machine = buildWalkAndMark(); + + machine.replaceTapeWith(new Tape({ + alphabet: machine.tape.alphabet, + symbols: ['*', '*', ' '], + })); + + const seen: number[] = []; + for (const s of machine.runStepByStep()) { seen.push(s.step); } + + expect(seen.length).toBeGreaterThan(0); + // Steps are 1-indexed and monotonically increasing. + expect(seen[0]).toBe(1); + expect(seen[seen.length - 1]).toBe(seen.length); + }); +}); + +describe('PostMachine — onPause forwarding', () => { + test('onPause fires when state.debug is set on a reachable state', async () => { + const machine = new PostMachine({ + 10: check(20, 30), + 20: right(10), + 30: mark, + 40: stop, + }); + + machine.replaceTapeWith(new Tape({ + alphabet: machine.tape.alphabet, + symbols: ['*', '*', ' '], + })); + + // 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: Array<{side: string; cause: string}> = []; + const session = machine.debugRun(); + 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]).toEqual({ side: 'before', cause: 'breakpoint' }); + }); + + test('run() awaits an async onPause before resolving', async () => { + const machine = new PostMachine({ + 10: check(20, 30), + 20: right(10), + 30: mark, + 40: stop, + }); + + machine.replaceTapeWith(new Tape({ + alphabet: machine.tape.alphabet, + symbols: ['*', '*', ' '], + })); + + machine.initialState.debug = { before: true }; + + let asyncCallbackResolved = false; + const session = machine.debugRun(); + session.on('pause', async () => { + await new Promise((r) => setTimeout(r, 10)); + asyncCallbackResolved = true; + session.continue(); + }); + await session.start(); + + // If start() resolved before the async callback finished, this would be false. + 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'); + }); + + 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('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'), + 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(); + 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); + }); +}); diff --git a/packages/machine/test/examples.spec.ts b/packages/machine/src/classes/PostMachine.examples.spec.ts similarity index 55% rename from packages/machine/test/examples.spec.ts rename to packages/machine/src/classes/PostMachine.examples.spec.ts index ef9346f..fab6e77 100644 --- a/packages/machine/test/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, + $tag, call, check, left, mark, noop, right, stop, toMermaid, summarizePostMachine, equivalentPostMachines, @@ -13,7 +13,7 @@ import { formatPath, type MachineState, type Path, -} from '../src/index'; +} from '../index'; describe('packages/machine/README.md', () => { describe('Constants', () => { @@ -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()) @@ -61,6 +61,81 @@ 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). + // 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]`. + 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. + // 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). + 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)); + + // 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"'); + 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({ @@ -74,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()) @@ -146,24 +221,33 @@ 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). + // 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).toContain('(("rightToBlank>1~2"))'); - - // The dotted onHalt edge — the override path back from the subroutine. - expect(mermaid).toMatch(/s\d+ -\. onHalt \.-> s\d+/); + expect(mermaid).toMatch(/subgraph w_\d+\["callable subtree of rightToBlank::1"\]/); + // Top-level entry auto-tag `main` (#86). + expect(mermaid).toContain('[["rightToBlank::1(1~2)
main"]]'); + // Subroutine-entry auto-tag (#86). + expect(mermaid).toContain('["rightToBlank::1
rightToBlank"]'); + // No bare `rightToBlank` (without `::1`) node label. + expect(mermaid).not.toMatch(/s\d+\["rightToBlank"\]/); + + // 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/); // 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 '*' + // 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+/); + expect(mermaid).toMatch(/s\d+ -- "\[\*\] → \['\*'\]\/\[S\]" --> s\d+/); }); test('** → marks first blank to make *** (single subroutine, single call)', async () => { @@ -183,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()) @@ -215,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('')) @@ -240,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(/\.+$/, '')) @@ -249,7 +333,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,12 +341,12 @@ 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)'); }); }); - 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, @@ -270,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); @@ -293,6 +375,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 { @@ -327,18 +457,24 @@ 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. + // 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"]'); // 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+/); }); }); @@ -370,10 +506,42 @@ describe('packages/machine/README.md', () => { expect(a.compositionEdgeCount).toBe(0); expect(a.maxCompositionDepth).toBe(0); + // `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); 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. + // Under #85 the hopper is dropped — the wrapper wraps walkToBlank::1 + // 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)
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+/); + expect(subMermaid).not.toMatch(/onHalt/); }); }); @@ -394,7 +562,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 }); @@ -417,7 +585,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), @@ -431,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); }); @@ -455,7 +624,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/test/machine-state.spec.ts b/packages/machine/src/classes/PostMachine.machine-state.spec.ts similarity index 78% rename from packages/machine/test/machine-state.spec.ts rename to packages/machine/src/classes/PostMachine.machine-state.spec.ts index aaccafc..28c8a19 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 () => { @@ -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(); @@ -17,13 +17,13 @@ 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, }); 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,22 +62,25 @@ 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]); }); test('subroutine body instruction has fully-qualified arrivalPath', async () => { + // 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: 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. + 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; - 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(); }); @@ -87,9 +90,9 @@ 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 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/naming.spec.ts b/packages/machine/src/classes/PostMachine.naming.spec.ts similarity index 63% rename from packages/machine/test/naming.spec.ts rename to packages/machine/src/classes/PostMachine.naming.spec.ts index bc8306f..84f1c06 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"', () => { @@ -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,14 +108,18 @@ 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 }); }); +// 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. @@ -126,20 +131,22 @@ 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'); + // 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); + // 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: { @@ -148,16 +155,21 @@ describe('PostMachine — subroutine body and hopper names', () => { inner: { 1: mark }, }, }); - // Top wrapper composite uses the top-level hopper name "outer". - expect(machine.initialState.name).toBe('outer>10~halt'); + // outer's first instruction is `call('inner')` — that produces a wrapper, + // 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)'); 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); + // 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); }); }); @@ -172,13 +184,13 @@ 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); + // `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); expect(names.has('foo::2')).toBe(true); - expect(names.has('foo::bar::1')).toBe(true); }); test('group inside subroutine — inner indices namespaced', () => { @@ -190,8 +202,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 +219,10 @@ 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); + // `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); }); @@ -225,9 +240,10 @@ describe('PostMachine — combined naming scenarios', () => { }, }); const names = collectNames(machine); - // Each scope hops accumulate in the prefix. + // 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); - // Body inner at outer::inner::1 calls deepest; the call composite there: - expect(names.has('outer::inner::deepest>outer::inner::1~halt')).toBe(true); + expect(names.has('outer::inner::deepest')).toBe(false); }); }); diff --git a/packages/machine/test/machine.spec.ts b/packages/machine/src/classes/PostMachine.spec.ts similarity index 85% rename from packages/machine/test/machine.spec.ts rename to packages/machine/src/classes/PostMachine.spec.ts index c3b2198..4d13993 100644 --- a/packages/machine/test/machine.spec.ts +++ b/packages/machine/src/classes/PostMachine.spec.ts @@ -1,8 +1,21 @@ 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'; + +// 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) { + const { matchedTransition, ...rest } = arg as Record; + void matchedTransition; + return rest; + } + return arg; + })); +} describe('constructor', () => { test('no instructions', () => { @@ -219,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)}`, () => { @@ -374,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); @@ -400,14 +411,16 @@ 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); - 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)); }); }); @@ -438,23 +451,19 @@ 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(); - expect(onStepMock1).toHaveBeenCalledTimes(3); - expect(onStepMock2).toHaveBeenCalledTimes(3); - expect(onStepMock3).toHaveBeenCalledTimes(3); - expect(onStepMock1.mock.calls).toEqual(onStepMock2.mock.calls); - expect(onStepMock2.mock.calls).toEqual(onStepMock3.mock.calls); + 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.) + expect(onStepMock1).toHaveBeenCalledTimes(2); + expect(onStepMock2).toHaveBeenCalledTimes(2); + expect(onStepMock3).toHaveBeenCalledTimes(2); + expect(stripMatchedTransition(onStepMock1.mock.calls)) + .toEqual(stripMatchedTransition(onStepMock2.mock.calls)); + expect(stripMatchedTransition(onStepMock2.mock.calls)) + .toEqual(stripMatchedTransition(onStepMock3.mock.calls)); }); }); @@ -472,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('***'); @@ -530,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) => ( @@ -560,11 +569,14 @@ 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); - expect(onStepMock).toHaveBeenCalledTimes(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(' '); const nextSymbolHistory = onStepMock.mock.calls.map((aCall) => aCall[0].nextSymbols[0]); @@ -592,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); @@ -637,17 +646,14 @@ 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 regExp = /\(/; const machine1StateIdList = machine1OnStepMock.mock.calls .map((args) => args[0].state.name) .filter((name) => regExp.test(name)) @@ -675,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); @@ -817,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('**'); @@ -838,7 +843,7 @@ describe('run tests', () => { ], }); - await expect(machine.run()).resolves.toBeUndefined(); + expect(() => machine.run()).not.toThrow(); expect(machine.tape.symbols.join('').trim()) .toBe('* *'); diff --git a/packages/machine/test/state-at.spec.ts b/packages/machine/src/classes/PostMachine.state-at.spec.ts similarity index 97% rename from packages/machine/test/state-at.spec.ts rename to packages/machine/src/classes/PostMachine.state-at.spec.ts index d5dca26..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', () => { @@ -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', () => { 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/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/src/classes/PostMachine.ts b/packages/machine/src/classes/PostMachine.ts index f7e3cc8..bcbeb3f 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, @@ -19,10 +21,11 @@ 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'; +import { analyzeLocalCallGraph } from '../callGraph'; import { type Breakpoint, type BreakpointFilter, @@ -72,9 +75,9 @@ 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; 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. @@ -100,79 +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(); - - // 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. - 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 { @@ -227,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({ @@ -278,32 +249,94 @@ export class PostMachine extends TuringMachine { ...subroutinesDataFromUpperScope, ...localSubroutinesData, }; + // Cycle-aware hopper construction (#85). + // + // 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 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]), + ); + 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); @@ -383,6 +416,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'); } @@ -413,11 +452,16 @@ 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, }, }, 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'); } @@ -438,6 +482,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; @@ -496,6 +557,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); @@ -576,12 +662,11 @@ 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']; - }); + // 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; } #onUserDebugWrite(state: State, value: unknown): void { diff --git a/packages/machine/src/commands.spec.ts b/packages/machine/src/commands.spec.ts new file mode 100644 index 0000000..691dbd5 --- /dev/null +++ b/packages/machine/src/commands.spec.ts @@ -0,0 +1,151 @@ +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('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, + 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. + machine.run(); + + expect(machine.tape.symbols[0]).toBe('*'); + }); +}); diff --git a/packages/machine/src/commands.ts b/packages/machine/src/commands.ts index 2b400a9..c2f4f91 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, }, @@ -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; } @@ -339,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..a2b9bb8 100644 --- a/packages/machine/src/index.ts +++ b/packages/machine/src/index.ts @@ -1,11 +1,5 @@ -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); export { Tape, @@ -29,7 +23,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, diff --git a/packages/machine/test/lockdown.spec.ts b/packages/machine/src/lockdown.spec.ts similarity index 68% rename from packages/machine/test/lockdown.spec.ts rename to packages/machine/src/lockdown.spec.ts index e8e63e5..a3f70cd 100644 --- a/packages/machine/test/lockdown.spec.ts +++ b/packages/machine/src/lockdown.spec.ts @@ -2,9 +2,8 @@ import { describe, expect, test } from 'vitest'; import { State, ifOtherSymbol, haltState } from '@turing-machine-js/machine'; import { installStateLockdown, - installHaltLockdown, withLockdownEscape, -} from '../src/lockdown'; +} from './lockdown'; describe('installStateLockdown', () => { function makeState(): State { @@ -77,35 +76,27 @@ 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(); }); }); -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', () => { + 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..44df2a1 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 { @@ -20,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) }; @@ -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 }; diff --git a/packages/machine/test/path.spec.ts b/packages/machine/src/path.spec.ts similarity index 95% rename from packages/machine/test/path.spec.ts rename to packages/machine/src/path.spec.ts index 1e78ff7..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', () => { @@ -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/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/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/debugger.spec.ts b/packages/machine/test/debugger.spec.ts deleted file mode 100644 index caefc8a..0000000 --- a/packages/machine/test/debugger.spec.ts +++ /dev/null @@ -1,125 +0,0 @@ -// 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). - -import { - PostMachine, - Tape, - type MachineState, - check, mark, right, stop, -} from '../src/index'; - -describe('PostMachine — async run', () => { - function buildWalkAndMark(): PostMachine { - return new PostMachine({ - 10: check(20, 30), - 20: right(10), - 30: mark, - 40: stop, - }); - } - - test('run() returns a Promise', () => { - const machine = buildWalkAndMark(); - - machine.replaceTapeWith(new Tape({ - alphabet: machine.tape.alphabet, - symbols: ['*', '*', ' '], - })); - - const result = machine.run(); - expect(result).toBeInstanceOf(Promise); - return result; // ensure jest waits for halt - }); - - test('run() resolves only after the machine halts', async () => { - const machine = buildWalkAndMark(); - - machine.replaceTapeWith(new Tape({ - alphabet: machine.tape.alphabet, - symbols: ['*', '*', ' '], - })); - - // Before run resolves, the tape should be the input. - expect(machine.tape.symbols.join('').trim()).toBe('**'); - - await machine.run(); - - expect(machine.tape.symbols.join('').trim()).toBe('***'); - }); - - test('onStep still observes every step', async () => { - const machine = buildWalkAndMark(); - - machine.replaceTapeWith(new Tape({ - alphabet: machine.tape.alphabet, - symbols: ['*', '*', ' '], - })); - - const seen: number[] = []; - await machine.run({ - onStep: (s: MachineState) => { seen.push(s.step); }, - }); - - expect(seen.length).toBeGreaterThan(0); - // Steps are 1-indexed and monotonically increasing. - expect(seen[0]).toBe(1); - expect(seen[seen.length - 1]).toBe(seen.length); - }); -}); - -describe('PostMachine — onPause forwarding', () => { - test('onPause fires when state.debug is set on a reachable state', async () => { - const machine = new PostMachine({ - 10: check(20, 30), - 20: right(10), - 30: mark, - 40: stop, - }); - - machine.replaceTapeWith(new Tape({ - alphabet: machine.tape.alphabet, - 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. - machine.initialState.debug = { before: true }; - - const seen: MachineState[] = []; - await machine.run({ - onPause: (s) => { seen.push(s); }, - }); - - expect(seen.length).toBeGreaterThan(0); - expect(seen[0].debugBreak).toEqual({ before: true }); - }); - - test('run() awaits an async onPause before resolving', async () => { - const machine = new PostMachine({ - 10: check(20, 30), - 20: right(10), - 30: mark, - 40: stop, - }); - - machine.replaceTapeWith(new Tape({ - alphabet: machine.tape.alphabet, - symbols: ['*', '*', ' '], - })); - - machine.initialState.debug = { before: true }; - - let asyncCallbackResolved = false; - await machine.run({ - onPause: async () => { - await new Promise((r) => setTimeout(r, 10)); - asyncCallbackResolved = true; - }, - }); - - // If run() resolved before the async callback finished, this would be false. - expect(asyncCallbackResolved).toBe(true); - }); -}); 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/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('******'); 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: {