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
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
//! Project-derived trusty-search index identity — a PARTITIONING key
//! (epic #4207, replacing the approach closed as won't-do in #4063).
//!
//! Why: the index id in use today is either the bare directory basename
//! ([`crate::index_id::derive_index_id`]) — which collides for any two
//! unrelated checkouts that happen to share a directory name — or, for a
//! tm-managed session, the session/worktree UUID, which binds a service
//! identity to ephemeral writer isolation: N concurrent writers on one repo
//! produce N independently-stale indexes, none shareable, and the id is
//! unguessable so BASE_PM's instruction to pass the project name silently
//! 404s. Both defects need ONE deterministic id derived from the PROJECT.
//!
//! **The contract that sank the previous attempt — state it before reading
//! any code.** trusty-search's registry is one-`root_path`-per-id (see
//! `trusty-mpm`'s `session_manager::search_gc`), so an index id must
//! *partition*: one id maps to exactly ONE content tree. [`RepoIdentity`]'s
//! own module doc says it is a *grouping* key — deliberately shared by every
//! facet (live checkout, `.base` clone, each session worktree) of one repo.
//! #4063 promoted that grouping key into the partitioning slot, and each
//! review round found another pair of genuinely different roots sharing it:
//! linked worktrees, then sibling clones of one repo, then the
//! daemon-unreachable path resolving forward onto the new id. This module
//! partitions **by construction** instead of by patching cases: the canonical
//! content-tree root path is a hashed component of every id, so two distinct
//! trees can never collide no matter how their git metadata relates. The
//! grouping key is retained as a *component* (and as the public
//! [`ProjectIdentity::origin`] field), not as the whole id.
//!
//! What: [`ProjectIdentity`] is the three-part identity the owner specified on
//! #4207 — origin (which repository), root (which content tree of it), and
//! operator (which account operates it — the triple's "gh user", realised
//! offline as git's configured `user.email`, not a GitHub login).
//! [`ProjectIdentity::derive`] is the one
//! I/O entry point (git shell-outs + one `canonicalize`);
//! [`ProjectIdentity::index_id`] is a pure, dependency-free function over
//! those three fields, so every collision case is testable without a daemon,
//! a registry, or live state. [`derive_project_index_id`] is the one-call
//! convenience wrapper. Nothing here is wired into `ensure_project_indexed`,
//! `trusty-search serve`, or the daemon's resolution path — this slice is the
//! derivation only.
//!
//! **Constraint inherited by the migration slice (do NOT implement here).**
//! The ~7 currently-registered indexes must be migrated by rename/alias, never
//! by re-index (this workspace alone is ~105k chunks / 2.1 GB redb). Whatever
//! resolver performs that migration MUST fail **safe**: when the daemon or the
//! persisted registry cannot be consulted, fall back to the known-good LEGACY
//! id. It must never fail *forward* onto an id that does not exist yet —
//! #4063 did exactly that at `search_index.rs:106-108` and silently orphaned a
//! session's search for the session's entire life, with no self-heal.
//!
//! Discoverability note: because the id is always suffixed, it is not
//! guessable from the project name alone. That is deliberate — a guessable id
//! cannot partition. The #4207 self-description slice (MCP `instructions` at
//! initialize, `404` → available-index signpost) is what makes ids findable;
//! the two changes are complements, and neither substitutes for the other.
//!
//! Test: `cargo test -p trusty-common --features unconditional-only --
//! project_index_id` — the sibling `project_index_id_tests.rs` covers sibling
//! clones, linked worktrees, differing operators, remoteless directories,
//! symlinked roots, determinism, the id charset, and the origin/operator drift
//! cases enumerated in [`ProjectIdentity::index_id`]'s `Known limitation`
//! block.
//!
//! [`RepoIdentity`]: crate::repo_identity::RepoIdentity
use ;
use Command;
use crateRepoIdentity;
use crateslugify_string;
/// Version tag mixed into every digest preimage.
///
/// Why: the derivation rule will eventually change (a new component, a
/// different framing). Mixing a version tag into the digest preimage means a
/// future `v2` can never re-point an ALREADY-REGISTERED id at different
/// content — every id moves wholesale into a disjoint space. That is the
/// property the migration slice actually needs.
/// What: the literal `"v1"`. Note what this does NOT give you: the emitted id
/// carries no version marker, so a v1 id and a v2 id are indistinguishable by
/// inspection. A migration that must classify ids has to track the scheme
/// out-of-band (or a future version must emit the tag in the id itself).
/// Test: `index_id_is_pinned_for_a_known_input` fails if this value changes.
const SCHEME_VERSION: &str = "v1";
/// Maximum length of the human-readable label prefix of an index id.
///
/// Why: an index id is a single URL path segment and appears in filenames and
/// log lines; an unbounded owner/repo pair makes both unwieldy. Truncation is
/// safe because uniqueness lives entirely in the digest suffix, never in the
/// label.
/// What: 48 characters.
const MAX_LABEL_LEN: usize = 48;
/// Placeholder label used when a project yields no slug-able name at all.
///
/// Why: `index_id` must never return a string that starts with `-` or is only
/// a digest; a stable placeholder keeps the id readable and well-formed.
/// What: the literal `"project"`.
const FALLBACK_LABEL: &str = "project";
/// The three-part identity of one indexable project.
///
/// Why: #4207 specifies identity as origin + root + gh user. Modelling those as
/// explicit fields — rather than folding them into an opaque derivation — makes
/// each component's job checkable in isolation and lets [`Self::index_id`] stay
/// a pure function, so the collision cases that killed #4063 are testable
/// without any live daemon or registry state.
/// What: `origin` is the repo-level GROUPING key ([`RepoIdentity`], `None`
/// outside a git repo); `root` is the canonical absolute content-tree root and
/// is the component that makes the derived id a PARTITIONING key; `operator` is
/// the account operating this checkout. All three are hashed; only `origin`
/// (or the root basename) contributes to the readable label.
///
/// `root` is the most stable of the three, but it is NOT immutable — it is
/// fixed only under ordinary GIT operations (#4269). Renaming or moving the
/// directory moves it, and `origin` and `operator` are both live git state.
/// See the `Known limitation` block on [`Self::index_id`] for the enumerated
/// drift cases and what each costs.
/// Test: `sibling_clones_of_same_repo_derive_distinct_ids`,
/// `linked_worktrees_of_same_repo_derive_distinct_ids`,
/// `different_operators_derive_distinct_ids`.
/// Derive the trusty-search index id for the project containing `start`.
///
/// Why: nearly every call site wants the id, not the identity; a one-call
/// wrapper keeps them from re-implementing the two-step derive-then-render and
/// drifting apart. Callers that also need the repo-level grouping key (to relate
/// several indexes of one repo) should use [`ProjectIdentity::derive`] and read
/// [`ProjectIdentity::origin`] instead of deriving it a second time.
/// What: [`ProjectIdentity::derive`] followed by [`ProjectIdentity::index_id`].
/// Performs the same best-effort git I/O as `derive`; never panics.
/// Test: `derivation_is_deterministic_across_calls`,
/// `sibling_clones_of_same_repo_derive_distinct_ids`.
/// Resolve the operator identity for the checkout at `root`, offline.
///
/// Why: #4207 specifies the account as an identity component, but the id must be
/// derivable with no network — `gh api user` would make every derivation a round
/// trip and a flake. Git's own configured identity is the offline signal that
/// actually varies per account, and a repo-local `user.email` override is
/// precisely how a second account's checkout is configured in practice.
///
/// **Deliberately reads NO environment variable.** An earlier revision honoured
/// a `TRUSTY_GH_USER` override; it was removed rather than merely tested,
/// because it made derivation non-hermetic in the one way that matters here —
/// two live callers on the SAME tree at the SAME instant deriving different ids
/// purely from differing process environments. The trusty-mpm daemon (launchd
/// plist env) and a `tm` CLI (shell env) are exactly that pair, so the override
/// would have re-created the "callers silently diverge" failure (#1373) that is
/// this module's whole reason to exist. Covering both branches with tests would
/// have pinned the divergence, not removed it. Eliminating the branch removes
/// the failure class.
/// What: `git -C <root> config --get user.email`, which already applies git's
/// repo-local-over-global precedence. `None` when git resolves no identity or is
/// unavailable. Returns a git email address, NOT a GitHub login.
///
/// Residual non-hermeticity, disclosed rather than claimed away: git's own
/// config resolution still depends on `HOME` / `GIT_CONFIG_GLOBAL`, so callers
/// running under materially different environments can still disagree when the
/// repo sets no local `user.email`; and `--get` on a multi-valued key returns
/// the LAST entry. The wiring slice must therefore guarantee uniform
/// environment inheritance across daemon and CLI — a partial rollout partitions
/// one tree into two indexes.
/// Test: `resolve_operator_identity_prefers_repo_local_git_identity`,
/// `resolve_operator_identity_ignores_ambient_env_override`.
/// Append one length-framed record to a digest preimage.
///
/// Why: concatenating fields with a separator is not injective — the exact
/// defect flagged on #4063's `owner-repo` join. An explicit byte length makes
/// the boundary unambiguous regardless of the field's own contents.
/// What: writes `<decimal len>:<bytes>` to `buf`.
/// Test: covered by `label_ambiguity_does_not_collide`.
pub
/// Append an optional field with an explicit presence tag.
///
/// Why: `None` and `Some("")` must not encode identically, or a project with no
/// origin would share a digest with one whose origin resolved to an empty
/// string.
/// What: `Some(v)` writes `+` then [`push_field`]; `None` writes a bare `-`.
/// Test: covered by `directory_without_git_origin_derives_stable_id`.
/// Return a path's raw bytes for hashing.
///
/// Why: `to_string_lossy` maps every invalid byte sequence to the same
/// replacement character, so two genuinely different non-UTF-8 paths could hash
/// identically — a partitioning-key violation, however rare. Hashing the raw
/// OS bytes removes the case entirely on unix.
/// What: the underlying `OsStr` bytes on unix; the lossy UTF-8 encoding
/// elsewhere (Windows `OsStr` is UTF-16 and has no byte view).
/// Test: covered by `sibling_clones_of_same_repo_derive_distinct_ids`.
pub
/// FNV-1a 64-bit digest with a final avalanche mix.
///
/// Why: the id must be byte-identical across processes, machines, and toolchain
/// versions, which rules out `std`'s `DefaultHasher` (explicitly not
/// stability-guaranteed). A cryptographic digest would work but `sha2` is an
/// *optional* dependency of this crate, and an identity that changes with a
/// feature flag is worse than no identity at all — so this stays dependency-free
/// and unconditional. Not a security boundary: nothing here defends against a
/// chosen-preimage attacker, only against accidental collision.
/// (`memory_core::analytics::fnv1a_hash` is the same algorithm but lives behind
/// the `memory-core` feature and takes `&str`, so it is unusable here.)
/// What: standard FNV-1a over `bytes`, then a splitmix64 finalizer so that
/// paths differing in a single character still differ in most output bits.
/// Test: `digest_is_stable_for_identical_inputs`.
pub