From 41903ebaf1d25143d988cf972b5723b747944dad Mon Sep 17 00:00:00 2001 From: nokeo08 Date: Fri, 14 Aug 2026 13:28:43 -0500 Subject: [PATCH] docs: add execution happy-path sequence diagram --- README.md | 4 ++ docs/execution-happy-path.md | 88 ++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 docs/execution-happy-path.md diff --git a/README.md b/README.md index 6b24359..51a708a 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,9 @@ file with `--config`. ## How it works +For a step-by-step trace of a clean sweep, see the +[execution happy-path sequence diagram](docs/execution-happy-path.md). + The source lives under `src/`, split into an entry point plus logic modules: @@ -358,6 +361,7 @@ move --help | `src/device.ts` | `Device` I/O seam over nut.js (`Point`, `createNutDevice`); the only nut.js importer. | | `src/strategies.ts` | Pure movement-pattern generators, the strategy registry, and name validation. | | `src/executor.ts` | `executePath` driver: bounds policy, pacing, interrupt detection, restore. | +| `docs/execution-happy-path.md` | Sequence diagram + invariants for a clean sweep. | | `package.json` | Bun project manifest. Single runtime dep: `@nut-tree-fork/nut-js`. | | `tsconfig.json` | Strict TypeScript config tuned for Bun (ESNext, bundler resolution). | | `bun.lock` | Bun's lockfile. Commit this. | diff --git a/docs/execution-happy-path.md b/docs/execution-happy-path.md new file mode 100644 index 0000000..57f0f16 --- /dev/null +++ b/docs/execution-happy-path.md @@ -0,0 +1,88 @@ +# Execution: the happy path + +This traces one full idle-triggered sweep that completes cleanly — the +"happy path" where the machine is idle long enough to fire, the configured +pattern runs to exhaustion, and no real user activity interrupts it. + +For the module breakdown and the three seams (`device` / `strategies` / +`executor`), see the "How it works" section of the [README](../README.md). + +```mermaid +sequenceDiagram + autonumber + participant Entry as move.ts + participant Keeper as runKeeper + participant Sim as simulateActivity + participant Strat as Strategy
(e.g. line) + participant Exec as executePath + participant Dev as Device
(nut.js) + + Note over Entry: startup (args → config) + Entry->>Entry: parseCliArgs() + Entry->>Entry: loadConfigFile() + Entry->>Entry: resolveConfig(file, cli) + Entry->>Keeper: runKeeper(config) + + Keeper->>Dev: createNutDevice() + Note right of Dev: sets mouse.config.autoDelayMs = 0 + Keeper->>Keeper: log.info(banner) + Keeper->>Dev: getPosition() + Dev-->>Keeper: lastPos + Note over Keeper: lastActivity = now + + loop every checkInterval (until idle long enough) + Keeper->>Dev: sleep(checkInterval) + Keeper->>Dev: getPosition() + Dev-->>Keeper: pos + Note over Keeper: pos == lastPos (no user movement)
now - lastActivity ≥ moveInterval → fire + end + + Keeper->>Sim: simulateActivity(config, log, dev) + Sim->>Dev: getPosition() + Dev-->>Sim: start + Sim->>Dev: width() + Dev-->>Sim: width + Sim->>Dev: height() + Dev-->>Sim: height + Note over Sim: strategy = STRATEGIES[config.pattern]
ctx = { start, width, height, rng } + Sim->>Exec: executePath(strategy, ctx, dev, log, config) + + Exec->>Strat: path(ctx) + Strat-->>Exec: Iterable + + loop for each target point (clean run) + Exec->>Exec: resolveTarget(bounds, target) → point + Exec->>Dev: setPosition(point) + Exec->>Dev: sleep(stepDelay) + Exec->>Dev: getPosition() + Dev-->>Exec: current + Note over Exec: |current - point| ≤ 2px → not the user, continue + end + + Note over Exec: path exhausted, no interruption + Exec->>Dev: setPosition(round(start)) + Note right of Exec: restore cursor to origin + Exec-->>Sim: "completed" + Sim-->>Keeper: (done) + + Keeper->>Dev: getPosition() + Dev-->>Keeper: lastPos (== start; re-sync) + Note over Keeper: lastActivity = now
loop continues +``` + +## Invariants this path relies on + +- **`createNutDevice()` is the only nut.js touchpoint.** It disables nut.js's + 100ms auto-delay so `executePath` owns cadence via `stepDelay`. +- **The strategy is pure.** `path(ctx)` yields ideal points from geometry + alone (`start` / `width` / `height` / `rng`); it never touches the device, + which is what makes every pattern unit-testable without a screen. +- **Every step re-reads the cursor** and compares it against the *commanded* + point (not the strategy's ideal, possibly fractional target) within a 2px + tolerance. On the happy path each check passes, so the loop runs to + exhaustion. A mismatch beyond tolerance is real user activity and returns + `"interrupted"` without restoring — the branch this diagram omits. +- **Clean completion restores the cursor to `round(start)`.** That is why the + follow-up `getPosition()` in `runKeeper` re-syncs `lastPos` to the origin as + a no-op, and the next idle check sees no net movement (so the synthetic + sweep is never mistaken for the user returning).