refactor(capture-kit): encode post-gesture stabilization baseline provenance

Merged
TH
2d ago
+225-149

Summary

Encode post-gesture stabilization baseline provenance in a discriminated union so a post-action rebased baseline can never be treated as pre-action evidence — the bug class of #3868 (a backend flip mid-poll laundered a rebased baseline into a bogus stale/no-effect claim). Replaces the baselineRebased boolean, the parallel baseline locals, and the needsBaselineDistrust parameter with PostGestureBaseline (pre-action | post-action variants carrying signature + comparison key together), a PostGestureStabilityDecision union where only accept-stale carries the pre-action payload, and a pre-action-typed baseline on the session pending record whose sole writer is the distrust authorization. 8 files. Behavior and timing preserved exactly: rebase only at the quiet-pair checkpoint, the rebasing pair cannot settle, further key changes rebase again, same budgets/minPolls/timeout/marker retirement. No CLI/API change. Ref #3868, #3875.

Production LOC (3 modules): 1037 → 1065 (+28); the invariant moves from boolean plumbing to types and their documentation.

Validation

Tested SHA: 2d0c931c2f (base 6ea9f81733, merged with origin/main 483a130bd2).

  • pnpm check:affected --run --head 2d0c931c2f: all runnable checks passed, exactCommit receipt (typecheck/layering/fallow/build/vitest-related). Prior receipts at 54fe25e662 and ea374a29ff lineage also clean.
  • tsc -b tsconfig.typecheck.json clean; a scratch demo of six invalid states was rejected by typecheck (post-action into pending.baseline, payload access on distrust, keyless baseline, etc.); the durable half now lives as an @ts-expect-error test in post-gesture-stability.test.ts.
  • Focused suites 54/54; packages/daemon/src/core/__tests__ + packages/capture-kit/src 1487 passed.
  • Historical red: #3868's regression tests applied to dffcd864c3^ fail there ("a quiet pair on a rebased baseline settles instead of distrusting it to the cap").
  • Mutations on head each fail existing tests: rebase keeps pre-action (6 fail); settle on the rebasing pair (5 fail); subset classifier replaces full-surface equality (2 fail).
  • Live simulator (iPhone 17 Pro / iOS 27, owned UDID, forced private-ax route, recorded at ea374a29ff; the later merge touched only Swift runner test files and one daemon test, no TS runtime): a no-op scroll's pending baseline reaches post_gesture_snapshot_stale_accept (6 attempts, ~4s cap) with the no-effect warning — identical to base (same phase, attempts, ~3.9–4.0s). A poll whose serving surface flips mid-loop logs post_gesture_snapshot_baseline_rebased then settles via post_gesture_snapshot_stabilized (3 attempts, 1770ms) with no stale accept and no no-effect claim — the #3868 class bounded at the rebase, live. Settled surfaces return in 449–550ms on base and head alike.

CI obligations: device lanes as configured. Unresolved risks: none identified beyond shared-type churn.

🤖 Generated with Claude Code