cargoless-core 0.2.0

The cargoless daemon: filesystem watcher, rust-analyzer wrapper, green/red model, build orchestration.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
//! #5 driver-loop — open-set → LSP-verb lowering + per-WT switch
//! composition (Model R Stream C #158, I/O-shell increment-2: the pure
//! correctness core).
//!
//! ## What this is
//!
//! The multiplex driver moves the **one shared rust-analyzer** between
//! worktrees by applying each WT's `overlay::OverlaySet` (the files that
//! differ from base) as LSP document overlays. #5's pure core
//! ([`overlay::diff`], now on main) computes the *minimal isolation-safe
//! delta* between two overlay-sets. This module is the next pure layer:
//! turning that delta into the **minimal correct LSP verb sequence**,
//! tracking the open-set RA actually holds — the driver-state #5's
//! design doc deliberately kept *out* of `overlay` (whether a path needs
//! `didOpen` vs `didChange` depends on what the multiplexer has sent RA
//! so far, which is orthogonal to the set-difference).
//!
//! Pure: no `LspClient`, no I/O, no flycheck wait. The flycheck-end
//! barrier + threaded driver loop + live `LspClient` calls (the F8-redo
//! isolation guarantee — never snapshot W's verdict until W's overlay +
//! flycheck have settled) are I/O-shell increment-3. This split is the
//! same pure-core-first structure that made `overlay::diff` /
//! `cluster::hash` structurally backstoppable: the load-bearing
//! correctness (verb minimality + no-stale-open across a WT switch) is
//! exhaustively unit-tested here; the shell just executes the verbs +
//! enforces the barrier.

use std::collections::BTreeSet;
use std::path::{Path, PathBuf};

use crate::Diagnostic;
use crate::overlay::{OverlayOp, OverlaySet, diff};

/// The LSP verb the I/O shell (increment-3) sends `LspClient` for one
/// lowered overlay op. `DidOpen`/`DidChange`/`DidClose` map 1:1 to
/// `lsp::LspClient::{did_open,did_change,did_close}` (the `did_close`
/// primitive landed in increment-1).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LspVerb {
    /// RA has not seen this URI yet — open it with `content`.
    DidOpen { path: PathBuf, content: String },
    /// RA already has this URI open — replace its content (full-doc
    /// sync, matching `lsp::LspClient::did_change`'s v0 model).
    DidChange { path: PathBuf, content: String },
    /// Revert RA to base/on-disk for this URI (the isolation op — a
    /// stale prior-WT overlay that is NOT closed here is exactly how
    /// worktree V's content would contaminate worktree W's verdict).
    DidClose { path: PathBuf },
}

/// Tracks which URIs RA currently holds an overlay open for, plus which
/// overlay-set is applied. Pure: feed it `OverlaySet` selections via
/// [`switch_to`](Self::switch_to); it emits the minimal correct
/// [`LspVerb`] sequence and maintains the open-set. No I/O.
#[derive(Debug, Clone, Default)]
pub struct OverlayMultiplexer {
    /// Paths RA currently has an overlay open for. `BTreeSet` ⇒
    /// deterministic iteration (reproducible inspection/tests).
    open: BTreeSet<PathBuf>,
    /// The overlay-set currently applied to RA (last switched-to WT's
    /// set; empty = base state, RA sees on-disk for everything).
    applied: OverlaySet,
}

impl OverlayMultiplexer {
    /// Fresh multiplexer at base state (nothing open, base applied).
    pub fn new() -> Self {
        Self::default()
    }

    /// Lower one op-list to LSP verbs, updating the open-set.
    ///
    /// * `Apply` first time (path not open) ⇒ `DidOpen` + track;
    /// * `Apply` while already open ⇒ `DidChange`;
    /// * `Close` ⇒ `DidClose` + untrack.
    ///
    /// The untrack is defensively idempotent: by `overlay::diff`'s proven
    /// Close-completeness every `Close` path was a previous `Apply` (hence
    /// open), so `remove` always finds it — but a redundant `Close`
    /// cannot corrupt state (set-remove of an absent key is a no-op, and
    /// a `didClose` of an un-open URI is an RA no-op).
    fn lower(&mut self, ops: &[OverlayOp]) -> Vec<LspVerb> {
        let mut out = Vec::with_capacity(ops.len());
        for op in ops {
            match op {
                OverlayOp::Close { path } => {
                    self.open.remove(path);
                    out.push(LspVerb::DidClose { path: path.clone() });
                }
                OverlayOp::Apply { path, content } => {
                    // `insert` returns true iff the key was NOT present —
                    // i.e. RA has not seen this URI ⇒ first-time open.
                    if self.open.insert(path.clone()) {
                        out.push(LspVerb::DidOpen {
                            path: path.clone(),
                            content: content.clone(),
                        });
                    } else {
                        out.push(LspVerb::DidChange {
                            path: path.clone(),
                            content: content.clone(),
                        });
                    }
                }
            }
        }
        out
    }

    /// **The load-bearing pure path.** Switch the one shared RA from the
    /// currently-applied overlay to `target` (the next worktree's
    /// overlay-set). Composes `overlay::diff` (the proven minimal +
    /// isolation-correct delta — Closes strictly before Applies, every
    /// prev-only path Closed) with open-set lowering.
    ///
    /// Post-conditions (the cross-worktree isolation guarantee, in pure
    /// form — the I/O shell adds only the flycheck barrier on top):
    /// * `self.applied == *target`;
    /// * the open-set equals `target`'s key-set — **no path that was
    ///   overlaid for the previous worktree but not `target` remains
    ///   open** (a stale-V overlay surviving into W's analysis is exactly
    ///   the contamination the one-RA-multiplex must never allow;
    ///   `overlay::diff` proves the Close set, `lower` executes it as
    ///   `DidClose` + untrack);
    /// * re-selecting the already-applied set emits **zero** verbs
    ///   (minimality — no needless RA re-analysis).
    pub fn switch_to(&mut self, target: &OverlaySet) -> Vec<LspVerb> {
        let ops = diff(&self.applied, target);
        let verbs = self.lower(&ops);
        self.applied = target.clone();
        verbs
    }

    /// Paths RA currently has an overlay open for (deterministic order).
    /// Inspection/tests; the I/O shell does not need it (it just sends
    /// the verbs `switch_to` returns).
    pub fn open_paths(&self) -> impl Iterator<Item = &Path> {
        self.open.iter().map(PathBuf::as_path)
    }

    /// Forget ALL driver state — clear the open-set **and** the applied
    /// overlay-set. Use exactly when the underlying rust-analyzer
    /// process was **(re)started**: a freshly-spawned RA has *nothing*
    /// open, so the multiplexer must not believe any prior overlay is
    /// still live.
    ///
    /// ## #3 capstone-wire incr-0 — the Supervisor-respawn seam, closed
    /// ## structurally
    ///
    /// `analyzer::Supervisor` does AC#6 *transparent* RA restart. Without
    /// this, the next [`switch_to`](Self::switch_to)`(target)` would
    /// `overlay::diff` against the **stale** `applied` and lower an
    /// already-tracked path to `DidChange` (or no-op) — but the fresh RA
    /// never opened it, so it never receives that worktree's overlay ⇒ a
    /// **wrong / cross-WT verdict** (the very spatial-isolation failure
    /// the multiplexer exists to prevent, re-introduced at the respawn
    /// seam). After `reset`, `switch_to(target)` diffs against the empty
    /// applied set ⇒ every `target` path is an `Apply`, and `lower` sees
    /// an empty open-set ⇒ every path is a `DidOpen` (never a
    /// `DidChange`/no-op against a file the fresh RA never opened). The
    /// live serve-loop calls this from the Supervisor `on_spawn` hook, so
    /// respawn-overlay-correctness is a property of the program shape,
    /// not a remembered step.
    pub fn reset(&mut self) {
        self.open.clear();
        self.applied = OverlaySet::new();
    }
}

/// Attribute diagnostics to the worktree whose overlay is applied. Since
/// per-WT checks serialize through the one shared RA, every diagnostic
/// produced *while WT `wt`'s overlay is applied* belongs to `wt`. This is
/// the mechanical tag; the *temporal* correctness (only reading
/// diagnostics that belong to the just-switched WT) is the flycheck-end
/// barrier the I/O shell (increment-3) enforces — `overlay::diff` +
/// [`OverlayMultiplexer::switch_to`] guarantee the *overlay* is exactly
/// `wt`'s, the barrier guarantees the *diagnostics read* are post-settle.
pub fn tag_for_worktree<W: Clone>(wt: &W, diags: &[Diagnostic]) -> Vec<(W, Diagnostic)> {
    diags.iter().map(|d| (wt.clone(), d.clone())).collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    fn oset(pairs: &[(&str, &str)]) -> OverlaySet {
        OverlaySet::from_pairs(pairs.iter().map(|(p, c)| (*p, *c)))
    }

    fn opened(m: &OverlayMultiplexer) -> Vec<String> {
        m.open_paths()
            .map(|p| p.to_string_lossy().into_owned())
            .collect()
    }

    #[test]
    fn base_to_worktree_opens_all_first_time() {
        let mut m = OverlayMultiplexer::new();
        let verbs = m.switch_to(&oset(&[("a.rs", "A"), ("z.rs", "Z")]));
        // overlay::diff yields sorted Applies (a then z); first time ⇒
        // DidOpen each.
        assert_eq!(
            verbs,
            vec![
                LspVerb::DidOpen {
                    path: "a.rs".into(),
                    content: "A".into()
                },
                LspVerb::DidOpen {
                    path: "z.rs".into(),
                    content: "Z".into()
                },
            ]
        );
        assert_eq!(opened(&m), vec!["a.rs", "z.rs"]);
    }

    #[test]
    fn reselect_same_set_emits_nothing() {
        let mut m = OverlayMultiplexer::new();
        let s = oset(&[("a.rs", "A")]);
        m.switch_to(&s);
        assert_eq!(
            m.switch_to(&s),
            vec![],
            "re-select ⇒ zero verbs (minimality)"
        );
    }

    #[test]
    fn content_change_of_open_file_is_didchange_not_didopen() {
        let mut m = OverlayMultiplexer::new();
        m.switch_to(&oset(&[("a.rs", "old")]));
        let verbs = m.switch_to(&oset(&[("a.rs", "new")]));
        assert_eq!(
            verbs,
            vec![LspVerb::DidChange {
                path: "a.rs".into(),
                content: "new".into()
            }],
            "already-open path ⇒ DidChange"
        );
    }

    #[test]
    fn cross_worktree_switch_no_stale_open_isolation() {
        // THE load-bearing isolation case. WT-V applied, switch to WT-W:
        // V-only file MUST DidClose + leave the open-set; shared-changed
        // DidChange; identical untouched (no verb); W-only DidOpen. After
        // the switch NO V-only path may remain open (stale-V overlay in
        // W's analysis = the contamination one-RA-multiplex must never
        // allow).
        let mut m = OverlayMultiplexer::new();
        m.switch_to(&oset(&[
            ("only_v.rs", "V"),
            ("shared.rs", "v-ver"),
            ("common.rs", "same"),
        ]));
        let verbs = m.switch_to(&oset(&[
            ("shared.rs", "w-ver"),
            ("common.rs", "same"),
            ("only_w.rs", "W"),
        ]));
        assert_eq!(
            verbs,
            vec![
                // overlay::diff: Closes first (sorted), then Applies
                // (sorted). only_v Closed; only_w first-time DidOpen;
                // shared content-changed but already-open ⇒ DidChange;
                // common identical ⇒ no verb.
                LspVerb::DidClose {
                    path: "only_v.rs".into()
                },
                LspVerb::DidOpen {
                    path: "only_w.rs".into(),
                    content: "W".into()
                },
                LspVerb::DidChange {
                    path: "shared.rs".into(),
                    content: "w-ver".into()
                },
            ]
        );
        // The isolation post-condition: open-set is exactly W's keys; no
        // V-only path survives.
        assert_eq!(opened(&m), vec!["common.rs", "only_w.rs", "shared.rs"]);
        assert!(
            !opened(&m).iter().any(|p| p == "only_v.rs"),
            "no stale V-only overlay may remain open after switching to W"
        );
    }

    #[test]
    fn close_then_reapply_reopens() {
        // A path closed (WT no longer overlays it) then later re-applied
        // must DidOpen again — RA reverted it to base on the Close, so
        // re-overlaying is a fresh open, not a change.
        let mut m = OverlayMultiplexer::new();
        m.switch_to(&oset(&[("a.rs", "A1")]));
        m.switch_to(&OverlaySet::new()); // back to base ⇒ DidClose a.rs
        assert!(opened(&m).is_empty(), "base state ⇒ nothing open");
        let verbs = m.switch_to(&oset(&[("a.rs", "A2")]));
        assert_eq!(
            verbs,
            vec![LspVerb::DidOpen {
                path: "a.rs".into(),
                content: "A2".into()
            }],
            "re-apply after close ⇒ DidOpen (fresh), not DidChange"
        );
    }

    #[test]
    fn switch_to_base_closes_everything() {
        let mut m = OverlayMultiplexer::new();
        m.switch_to(&oset(&[("a.rs", "A"), ("b.rs", "B")]));
        let verbs = m.switch_to(&OverlaySet::new());
        assert_eq!(
            verbs,
            vec![
                LspVerb::DidClose {
                    path: "a.rs".into()
                },
                LspVerb::DidClose {
                    path: "b.rs".into()
                },
            ]
        );
        assert!(opened(&m).is_empty());
    }

    #[test]
    fn tag_for_worktree_attributes_each_diagnostic() {
        use crate::Severity;
        let d = Diagnostic {
            file_path: "physics/src/orbit.rs".into(),
            line: 1,
            col: 1,
            severity: Severity::Error,
            code: None,
            message: "x".into(),
            source: None,
        };
        let tagged = tag_for_worktree(&"wt-A".to_string(), std::slice::from_ref(&d));
        assert_eq!(tagged, vec![("wt-A".to_string(), d.clone())]);
        // Empty in ⇒ empty out (no fabricated attribution).
        assert!(tag_for_worktree(&"wt-A".to_string(), &[]).is_empty());
    }

    // --- reset() — the Supervisor-respawn seam (capstone-wire incr-0) ---

    #[test]
    fn reset_then_switch_reopens_all_not_didchange() {
        // THE respawn-correctness case. WT-A applied (RA has a.rs, z.rs
        // open). The RA process is restarted ⇒ reset(). Re-selecting the
        // SAME overlay-set MUST re-DidOpen both (the fresh RA never
        // opened them) — NOT no-op (the bug: stale `applied` ⇒ diff
        // yields nothing ⇒ fresh RA never receives the overlay ⇒ wrong
        // verdict).
        let mut m = OverlayMultiplexer::new();
        let s = oset(&[("a.rs", "A"), ("z.rs", "Z")]);
        m.switch_to(&s);
        assert_eq!(opened(&m), vec!["a.rs", "z.rs"]);
        m.reset();
        assert!(
            opened(&m).is_empty(),
            "reset clears the open-set (fresh RA has nothing open)"
        );
        let verbs = m.switch_to(&s);
        assert_eq!(
            verbs,
            vec![
                LspVerb::DidOpen {
                    path: "a.rs".into(),
                    content: "A".into()
                },
                LspVerb::DidOpen {
                    path: "z.rs".into(),
                    content: "Z".into()
                },
            ],
            "post-reset, the same set re-DidOpens every path (never no-op/DidChange)"
        );
        assert_eq!(opened(&m), vec!["a.rs", "z.rs"]);
    }

    #[test]
    fn reset_then_changed_content_is_didopen_not_didchange() {
        // A path that was open with old content, then RA restarts and
        // its content changed: must be DidOpen (fresh RA, fresh open),
        // NOT DidChange against a buffer the new RA never had.
        let mut m = OverlayMultiplexer::new();
        m.switch_to(&oset(&[("a.rs", "old")]));
        m.reset();
        let verbs = m.switch_to(&oset(&[("a.rs", "new")]));
        assert_eq!(
            verbs,
            vec![LspVerb::DidOpen {
                path: "a.rs".into(),
                content: "new".into()
            }],
            "post-reset a changed file is a fresh DidOpen, not DidChange"
        );
    }

    #[test]
    fn reset_on_fresh_multiplexer_is_a_noop() {
        // reset() must be total/idempotent — calling it on an
        // already-empty multiplexer (e.g. respawn before any switch)
        // changes nothing and the subsequent base switch is still empty.
        let mut m = OverlayMultiplexer::new();
        m.reset();
        assert!(opened(&m).is_empty());
        assert_eq!(m.switch_to(&OverlaySet::new()), vec![]);
        m.reset();
        assert!(opened(&m).is_empty());
    }

    #[test]
    fn reset_then_base_switch_emits_no_stale_close() {
        // After reset the applied set is empty, so switching to base
        // emits NOTHING — crucially NOT a DidClose for the pre-reset
        // paths (the fresh RA never had them open; a spurious didClose
        // of an unopened URI is wrong-shaped even if RA tolerates it).
        let mut m = OverlayMultiplexer::new();
        m.switch_to(&oset(&[("a.rs", "A"), ("b.rs", "B")]));
        m.reset();
        assert_eq!(
            m.switch_to(&OverlaySet::new()),
            vec![],
            "post-reset→base: no stale DidClose for never-opened (fresh-RA) files"
        );
    }
}