Files
Move/docs/execution-happy-path.md
T
nokeo08 c8942bb380 Collapse bounds policies to reflect-only; drop abort and clamp
The executor kept every commanded point on-screen via a per-strategy
BoundsPolicy of abort / clamp / reflect. Measured against the real
strategies, the other two earned nothing: abort truncated a sweep at the
first edge (line on a narrow screen ran only 90 of 250 steps), and clamp
could park the cursor against an edge (a monotonic ramp stalled 162 steps
in a row) -- both counter to the program's whole purpose of keeping the
cursor moving. reflect bounces off the edge and keeps going, and is
already what line/diagonal need in loop mode. arc's declared clamp was
provably dead code (it clamps its own endpoint, so no sample ever leaves
the screen).

Collapse to reflect-only:
- strategies.ts: remove the BoundsPolicy type and the `bounds` field from
  the interface and all six strategies. Keep the local clamp() helper --
  it's arc's endpoint geometry, not an on-screen policy; docstring says so.
- executor.ts: resolveTarget loses its policy parameter and its null
  return and just reflects both axes; delete clampInt; SweepOutcome drops
  "aborted"; ExecuteOptions drops `bounds`; remove the Out of bounds log.
- keeper.ts: loopOpts is now { restore: false, loop: true } -- the
  reflect override added with loop mode is redundant.
- tests: drop the abort-outcome, clamp, and bounds-override tests; simplify
  fixed() to take no policy; add a regression test that a monotonic ramp
  past an edge never yields two identical points in a row (the guarantee
  that motivated removing clamp).

Behavior is unchanged for every pattern at normal cursor positions
(verified: line's normal sweep is byte-identical). The only differences
are at a screen edge, where motion now bounces instead of stopping. No
config keys, flags, or pattern names changed.

Docs updated to match, including in-code comments, the README strategies
table (Bounds column removed) and verbose description, the sequence
diagram (resolveTarget signature + getPosition/width ordering + a loop-mode
note), and a CHANGELOG Changed entry.
2026-08-17 15:53:49 -05:00

4.3 KiB

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.

sequenceDiagram
    autonumber
    participant Entry as move.ts
    participant Keeper as runKeeper
    participant Sim as simulateActivity
    participant Strat as Strategy<br/>(e.g. line)
    participant Exec as executePath
    participant Dev as Device<br/>(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)<br/>now - lastActivity ≥ moveInterval → fire
    end

    Keeper->>Sim: simulateActivity(config, log, dev)
    Sim->>Dev: width()
    Dev-->>Sim: width
    Sim->>Dev: height()
    Dev-->>Sim: height
    Sim->>Dev: getPosition()
    Dev-->>Sim: start
    Note over Sim: strategy = STRATEGIES[config.pattern]<br/>ctx = { start, width, height, rng }
    Sim->>Exec: executePath(strategy, ctx, dev, log, config)

    Exec->>Strat: path(ctx)
    Strat-->>Exec: iterable of Points

    loop for each target point (clean run)
        Exec->>Exec: resolveTarget(target) → point (reflected on-screen)
        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 (equals start, re-synced)
    Note over Keeper: lastActivity = now<br/>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).
  • On-screen confinement is uniform. resolveTarget reflects any coordinate past a screen edge back inside the travel range — the sole, per-pattern-independent policy. A strategy emits ideal geometry and never bounds its own output.

Loop mode (--loop)

This diagram is the single-sweep path (config.loop === false). Under --loop, simulateActivity instead repeats the step loop until the user interrupts: a pattern with an infinite loopPath (line, diagonal) runs that one never-ending path, while the others chain their finite path cycle after cycle, re-reading the cursor as the next start each time. The restore in the final step is skipped (restore: false), so successive cycles flow from where the last left off. Everything else — reflection, pacing, and the per-step interrupt check — is identical to the sweep traced above.