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
//! Stale duplicate-record dedup for `reconcile_on_boot` (#2306).
//!
//! Why: a project can accrue two conflicting non-`Decommissioned` records — one
//! canonical (resolved `workspace_path`) and one adopted record permanently
//! stuck at `cwd`/`workspace_path = /unknown` (adopted before #2163's
//! `get_pane_cwd` resolution existed). `reconcile_on_boot` only resolves
//! `/unknown` when a LIVE pane is discovered; it never revisits a `/unknown`
//! record once its pane is gone, and nothing collapses N records per project,
//! so `tm` keeps re-attaching to the wrong/stale one. #2001/#2004 (zombie
//! reconcile) and #2148 (non-destructive resume) are single-record and do not
//! cover this.
//!
//! **Grouping caveat (post-review, #2306):** `source_id` is derived from the
//! git remote and is per-REPOSITORY
//! ([`super::adopt::derive_source_id_for_record`]), not per-workspace — every
//! concurrent worktree session of one repo shares the same `source_id`. A
//! naive "keep only the newest per group" policy would therefore collapse
//! *legitimate concurrent sessions*, and worse: an in-project worktree record
//! is `workspace_owned = false` BY DESIGN, yet `decommission` still runs
//! `git worktree remove --force` (+ branch delete) for such a path via
//! [`super::decommission::is_session_worktree`] — so a naive policy would
//! destroy real, possibly-uncommitted work. See the eligibility rule below,
//! which is the actual fix for that danger.
//!
//! What: groups non-`Decommissioned` records by `source_id` (fallback: resolved
//! `workspace_path`). Within a quiesced group (no member has a live tmux
//! session — see the live-group guard), a record is only ever a dedup
//! **loser** when it is unresolved-or-dead: [`is_resolved_existing`] is
//! `false` for it (no `workspace_path`, the `/unknown` sentinel, or a path
//! that no longer exists on disk). A record with a resolved, still-existing
//! `workspace_path` is NEVER a loser — regardless of group size or recency —
//! because such records are the concurrent-session case the naive policy got
//! wrong. Consequently:
//! - a group with 2+ resolved-existing members collapses nothing among them;
//! only its unresolved/dead members (if any) are decommissioned, and only
//! because a resolved-existing (or live) member supersedes them;
//! - a group with ZERO resolved-existing members (e.g. `/unknown`-only or
//! all-dead) keeps the most-recently-active/created member and
//! decommissions the rest — there is nothing on disk to lose in that case.
//!
//! An unresolved/dead loser is additionally rechecked against the SAME
//! [`is_resolved_existing`] predicate immediately before decommission runs
//! (belt-and-suspenders against a TOCTOU race); a loser that fails the
//! recheck is skipped with a `warn!`, never decommissioned.
//!
//! Test: `plan_dedup_*`, `is_resolved_existing_*`, and `reconcile_dedup_*` in
//! `dedup_tests.rs`.
use Ordering;
use ;
use Path;
use ;
use ;
use ;
/// Grouping key for a record, or `None` if it cannot be grouped.
///
/// Why: two records for the SAME project must land in the same group so a
/// quiesced duplicate can be collapsed; a record with neither a `source_id`
/// nor a `workspace_path` (a bare `/unknown` with no provenance) is not safely
/// attributable to any project and is therefore left alone. NOTE: `source_id`
/// is per-repository, not per-workspace (see module docs) — the safety
/// boundary against over-grouping is [`is_resolved_existing`] at selection
/// time, not this key.
/// What: prefers `source_id`; falls back to the `workspace_path` string. The
/// key is namespaced (`src:` / `ws:`) so a source id can never collide with a
/// path.
/// Test: covered indirectly by `plan_dedup_distinct_source_ids_untouched` and
/// `plan_dedup_unknown_only_group_keeps_most_recent`.
/// True when `record.workspace_path` is a resolved, real, currently-existing
/// directory on disk.
///
/// Why (#2306 review fix): this is THE safety boundary for dedup. A record
/// that passes this check represents a legitimate on-disk workspace — which
/// may be one of several concurrent sessions of the SAME repo, since
/// `source_id` groups by repository, not by workspace. Such a record must
/// never be treated as a stale duplicate, no matter the group size or its
/// recency relative to siblings — an in-project worktree record is
/// `workspace_owned = false` by design, so the `workspace_owned` flag alone
/// cannot distinguish "safe to decommission" from "real work that would be
/// `git worktree remove --force`'d". Only a record that FAILS this check (no
/// `workspace_path`, the literal `/unknown` sentinel, or a path that no
/// longer exists — e.g. already removed by other means) is eligible to be a
/// dedup loser.
/// What: `true` iff `workspace_path` is `Some(p)`, `p != Path::new("/unknown")`,
/// and `p.exists()`.
/// Test: `is_resolved_existing_true_for_real_dir`,
/// `is_resolved_existing_false_for_missing_dir`,
/// `is_resolved_existing_false_for_none_and_unknown_sentinel`.
pub
/// Order two records by recency only (greater = more recently active/created).
///
/// Why: used ONLY to break a tie among unresolved/dead candidates in a group
/// with zero resolved-existing members — there is nothing on disk to lose in
/// that case, so "most recently touched" is a reasonable survivor heuristic.
/// What: ranks by `last_activity_at` (`None` sorts earliest), then
/// `created_at`.
/// Test: `plan_dedup_unknown_only_group_keeps_most_recent`.
/// Plan which records to decommission to dedup stale duplicates (#2306).
///
/// Why: the decision must be a pure function of the current record set, disk
/// state (via [`is_resolved_existing`]'s `exists()` check), and the live-tmux
/// name set so it is trivially unit-testable without a real store or tmux
/// driver (the #1790 no-real-tmux guard).
/// What: groups non-`Decommissioned` records via [`group_key`]; skips any
/// group of size <2 or with a live-tmux member. For the remaining (quiesced,
/// size>1) groups, partitions members into resolved-existing vs.
/// unresolved/dead via [`is_resolved_existing`]. If ANY member is
/// resolved-existing, every unresolved/dead member is a loser (the
/// resolved-existing members are NEVER touched, even if there are several).
/// If NONE are resolved-existing, the most-recent member (by [`by_recency`])
/// survives and every other member is a loser.
/// Test: the `plan_dedup_*` tests in `dedup_tests.rs`.
pub