Pick a different movement pattern every time a sweep is triggered, so the
motion varies across the day instead of repeating one shape.
- strategies.ts: add the `random` sentinel, `SELECTABLE_PATTERN_NAMES`,
`isSelectablePattern`, and `createRandomPicker`. `random` is deliberately
NOT a registry entry: it has no path of its own, so `STRATEGIES` stays a
total lookup and `PATTERN_NAMES` keeps listing only real generators. The
picker is a closure over `last`, giving a uniform draw that never returns
the same pattern twice in a row. Building CANONICAL_PATTERNS from the
selectable list makes both validation boundaries accept `random` (and
loose spellings) for free, and extends the normalization-collision
assertion to cover the sentinel.
- cli.ts: add `-r`/`--random` plus an exported `selectPattern` holding the
conflict rule. `-r` is sugar for `--pattern random`, so the two agreeing
is a no-op while `-r -p arc` is rejected as contradictory. The flag folds
into `pattern`, so ConfigOverrides, resolveConfig, and move.ts are
untouched. `parseCliArgs` now takes its argv as an optional parameter so
the flag surface is testable without process.argv.
- keeper.ts: resolve `random` via the picker once per trigger, before the
loop-mode branch, so a pick holds for a whole loop run rather than
changing mid-run. runKeeper builds one picker for the process, so the
no-repeat memory spans sweeps minutes apart. Because the pick is a real
strategy, --verbose logs the concrete pattern name and a pick with an
infinite loopPath still bounces edge-to-edge under --loop.
- config.ts / configFile.ts: accept the sentinel where a pattern is valid,
and quote the selectable list in errors. No `random` boolean config key —
the file spells it "pattern": "random".
executor.ts and move.ts needed no changes.
Tests: new tests/cli.test.ts (the file had no coverage before) covering the
flag surface and the conflict rule; picker tests pinning the no-repeat and
full-registry-coverage properties; keeper tests pinning once-per-trigger and
once-per-loop-run.
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.
Mermaid treats ';' as a statement separator, so the label
'lastPos (== start; re-sync)' terminated mid-line and the parser then
expected an arrow (the 'Parse error on line 59' the preview showed).
Removed the semicolon and, defensively, replaced 'Iterable<Point>' with
plain text so the angle brackets aren't interpreted as an HTML tag in the
rendered label.