Implements D1 and D2 from docs/lsp-file-watcher-framing.md (revision 1,
approved 2026-08-10). The user ruled that the walking survives this
lane; D3 gets its own framing.
D1. resolve_watcher now returns (base, pattern, form) and the watch
record carries the form. Per LSP, a plain-string glob matches the
file's ABSOLUTE path while a RelativePattern's pattern is relative to
its base --- and resolve_watcher discarded the distinction, so every
downstream consumer matched relatively. Real servers send absolute
globs: rust-analyzer was never told about any file change (all six of
its globs absolute), and gopls saw go.mod but never a .go edit. The
match subject is chosen in start_file_watcher --- form "absolute"
matches base .. "/" .. rel, form "relative" matches rel unchanged.
scan_tree still walks in relative terms; only the string handed to the
matcher changes.
D2. register_file_watchers now cancels the outgoing record list before
file_watchers[skey][reg.id] = recs drops the only reference to it. The
cancel loop is factored into cancel_watch_records, shared with
unregister_file_watchers, so the two paths cannot diverge.
rust-analyzer registers the same id twice with no unregister between
--- previously 12 concurrent pollers, 6 permanently uncancellable.
Verification, per the framing's plan. Three new fake-LSP modes and
tests beside m4_24, each mutation-tested against the defect it names:
- filewatchabs registers a plain-string absolute glob whose relative
reading matches nothing. Red before D1 (reverting the match subject
fails exactly this test), green after.
- filewatchflat registers a RelativePattern without a leading **/
(*.txt at the base) --- F2's guard. Matching every form absolutely
fails exactly this test, so the obvious wrong fix cannot land green.
It also pins that a base-level pattern does not match into
subdirectories.
- filewatchrereg registers the same id twice (rust-analyzer's shape).
The witness is observable polling, not table shape: f.old exists on
disk before either .new event lands, so a leaked watcher at the same
250ms cadence reports it before the second positive. Reverting D2
fails exactly this test, the leaked .old event visible in .received.
m4_24 stayed green under all three mutations --- the framing's F1
finding (the existing test is insensitive to D1 in both directions),
confirmed rather than assumed. It is kept unchanged.
The framing doc records the approval and the answered ruling; the
active-work lane moves to IMPLEMENTED with the verification results.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>