@bli-cockpit/cli 0.2.54 → 0.2.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/adapters/attribution-core-fallbacks.js +247 -0
  2. package/dist/adapters/attribution-core-paths.js +182 -0
  3. package/dist/adapters/attribution-core-score.js +159 -0
  4. package/dist/adapters/attribution-core-types.js +13 -0
  5. package/dist/adapters/attribution-core.js +13 -565
  6. package/dist/adapters/claude-attribution-discovery.js +186 -0
  7. package/dist/adapters/claude-attribution-score.js +204 -0
  8. package/dist/adapters/claude-attribution-signals.js +180 -0
  9. package/dist/adapters/claude-attribution-types.js +25 -0
  10. package/dist/adapters/claude-attribution.js +14 -569
  11. package/dist/commands/doctor-access.js +129 -0
  12. package/dist/commands/doctor-pipeline.js +326 -0
  13. package/dist/commands/doctor-registration.js +105 -0
  14. package/dist/commands/doctor-report.js +111 -0
  15. package/dist/commands/doctor-update.js +120 -0
  16. package/dist/commands/doctor.js +8 -753
  17. package/dist/commands/heartbeat.js +8 -0
  18. package/dist/commands/jarvis-contracts.js +8 -0
  19. package/dist/commands/jarvis-render.js +413 -0
  20. package/dist/commands/jarvis-turn.js +305 -0
  21. package/dist/commands/jarvis.js +23 -698
  22. package/dist/commands/local-args-collector-setup.js +250 -0
  23. package/dist/commands/local-args-collector-status.js +227 -0
  24. package/dist/commands/local-args-collector-work.js +175 -0
  25. package/dist/commands/local-args-collector.js +19 -624
  26. package/dist/commands/local-args-tower-admin.js +456 -0
  27. package/dist/commands/local-args-tower-chat.js +194 -0
  28. package/dist/commands/local-args-tower-pages.js +314 -0
  29. package/dist/commands/local-args-tower.js +13 -880
  30. package/dist/commands/local-help.js +10 -2
  31. package/dist/commands/onboard-completion.js +136 -0
  32. package/dist/commands/onboard-flows.js +165 -0
  33. package/dist/commands/onboard-setup.js +102 -0
  34. package/dist/commands/onboard.js +5 -392
  35. package/dist/commands/public-root.js +1 -1
  36. package/dist/commands/session-sync-counters.js +55 -0
  37. package/dist/commands/session-sync-health.js +8 -1
  38. package/dist/commands/session-sync-plan.js +47 -7
  39. package/dist/commands/session-sync-scan.js +4 -4
  40. package/dist/commands/session-sync.js +6 -0
  41. package/dist/commands/settings-render.js +27 -0
  42. package/dist/commands/sync-followups.js +5 -1
  43. package/dist/commands/sync.js +5 -1
  44. package/dist/commands/team-device-reasons.js +16 -0
  45. package/dist/commands/team.js +87 -7
  46. package/dist/evidence-upload-client.js +14 -763
  47. package/dist/evidence-upload-object.js +181 -0
  48. package/dist/evidence-upload-plan.js +233 -0
  49. package/dist/evidence-upload-terminal.js +309 -0
  50. package/dist/evidence-upload-transport.js +104 -0
  51. package/dist/spool/local-spool-io.js +122 -0
  52. package/dist/spool/local-spool-mutations.js +174 -0
  53. package/dist/spool/local-spool-parse.js +143 -0
  54. package/dist/spool/local-spool-types.js +22 -0
  55. package/dist/spool/local-spool.js +20 -426
  56. package/dist/upload-evidence-delivery-offer.js +144 -0
  57. package/dist/upload-evidence-delivery-reconcile.js +134 -0
  58. package/dist/upload-evidence-delivery-summary.js +205 -0
  59. package/dist/upload-evidence-delivery.js +12 -482
  60. package/package.json +3 -3
@@ -1,7 +1,3 @@
1
- import crypto from "node:crypto";
2
- import { realpathSync } from "node:fs";
3
- import path from "node:path";
4
- import { normalizeGitOrigin, repoFingerprintFromLocalRoot, repoFingerprintFromOrigin, repoLabelFromOrigin, stableWorktreeFingerprint, } from "../repo-identity.js";
5
1
  /**
6
2
  * Shared deterministic attribution core for agent session transcripts (Codex
7
3
  * and Claude Code). Both adapters extract source-shaped signals, normalize them
@@ -13,565 +9,17 @@ import { normalizeGitOrigin, repoFingerprintFromLocalRoot, repoFingerprintFromOr
13
9
  * A session is attributed only when exactly one worktree clearly wins (best
14
10
  * score ≥ threshold AND beats the runner-up by the margin). Close calls stay
15
11
  * ambiguous instead of being duplicated across repos or guessed.
12
+ *
13
+ * This file is the table of contents; every responsibility below lives in an
14
+ * `attribution-core-*.ts` sibling and is re-exported here so the module's
15
+ * public surface never moves:
16
+ * -types scoring weights, thresholds, and shared shapes (BLI-3637)
17
+ * -score scoreSignalsAgainstWorktrees, the entry point
18
+ * -fallbacks folder/known-repo/transcript-origin rescue paths + terminal
19
+ * reasons, tried in order once no worktree wins outright
20
+ * -paths cross-platform path containment/worktree matching, plus the
21
+ * session-id/hash/score-clamp value normalizers beside them
16
22
  */
17
- export const SCORE_CWD_MATCH = 0.5;
18
- export const SCORE_WORKSPACE_ROOT_MATCH = 0.1;
19
- export const SCORE_ORIGIN_MATCH = 0.3;
20
- export const SCORE_BRANCH_MATCH = 0.15;
21
- export const SCORE_HEAD_SHA_MATCH = 0.05;
22
- export const ATTRIBUTION_MIN_SCORE = 0.4;
23
- export const ATTRIBUTION_MIN_MARGIN = 0.15;
24
- export const ATTRIBUTED_FALLBACK_MAX_SCORE = 0.39;
25
- export const SESSION_FILE_UUID_PATTERN = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
26
- export function scoreSignalsAgainstWorktrees(signals, worktrees, options = {}) {
27
- const originLabel = options.originLabel ?? "origin_url_match";
28
- const collectionRoots = collectionRootComparisonAliases(options.collectionRoots ?? []);
29
- const recordedPaths = [...signals.cwds, ...signals.workspaceRoots];
30
- const approvedRecordedPaths = recordedPathsWithinCollectionRoots(recordedPaths, collectionRoots);
31
- const scopedWorktrees = worktreesWithinCollectionScope(worktrees, collectionRoots);
32
- const cwdComparisonPaths = pathComparisonAliases(signals.cwds);
33
- const workspaceRootComparisonPaths = pathComparisonAliases(signals.workspaceRoots);
34
- const hasAnySignal = signals.cwds.length > 0 ||
35
- signals.workspaceRoots.length > 0 ||
36
- signals.originUrls.length > 0 ||
37
- signals.branches.length > 0 ||
38
- signals.headShas.length > 0;
39
- if (!hasAnySignal) {
40
- return unattributed("no_repo_signals");
41
- }
42
- // D6 deepest-root tie-break: a path signal can sit inside several candidate
43
- // roots when one root is nested inside another (e.g. Claude's
44
- // `<repo>/.claude/worktrees/<name>` linked worktrees, or any monorepo with a
45
- // nested package that is itself a worktree). Crediting every containing root
46
- // leaves the session ambiguous forever. Instead, only the DEEPEST containing
47
- // root earns the path score, which is also the most specific (correct) match.
48
- const cwdWinners = deepestContainers(cwdComparisonPaths, scopedWorktrees);
49
- const workspaceRootWinners = deepestContainers(workspaceRootComparisonPaths, scopedWorktrees);
50
- const scored = scopedWorktrees.map((worktree) => {
51
- const matched = [];
52
- let score = 0;
53
- let pathScore = 0;
54
- if (cwdWinners.has(worktree.worktree_fingerprint)) {
55
- score += SCORE_CWD_MATCH;
56
- pathScore += SCORE_CWD_MATCH;
57
- matched.push("cwd_match");
58
- }
59
- if (workspaceRootWinners.has(worktree.worktree_fingerprint)) {
60
- score += SCORE_WORKSPACE_ROOT_MATCH;
61
- pathScore += SCORE_WORKSPACE_ROOT_MATCH;
62
- matched.push("workspace_root_match");
63
- }
64
- if (worktree.repo_origin_url &&
65
- signals.originUrls.includes(worktree.repo_origin_url)) {
66
- score += SCORE_ORIGIN_MATCH;
67
- matched.push(originLabel);
68
- }
69
- if (signals.branches.includes(worktree.branch)) {
70
- score += SCORE_BRANCH_MATCH;
71
- matched.push("branch_match");
72
- }
73
- if (worktree.head_sha && signals.headShas.includes(worktree.head_sha)) {
74
- score += SCORE_HEAD_SHA_MATCH;
75
- matched.push("head_sha_match");
76
- }
77
- return { worktree, score, pathScore, matched };
78
- });
79
- scored.sort((a, b) => b.score - a.score);
80
- const best = scored[0];
81
- const secondBestScore = scored[1]?.score ?? 0;
82
- if (!best || best.score === 0) {
83
- if (approvedRecordedPaths.length > 0) {
84
- const folderFallback = fallbackToFolderWorkspaceForContainerCwd({
85
- signals,
86
- worktrees: scopedWorktrees,
87
- approvedRecordedPaths,
88
- });
89
- if (folderFallback)
90
- return folderFallback;
91
- const transcriptFallback = fallbackToTranscriptOriginForDeletedRepo({
92
- originLabel,
93
- signals,
94
- collectionRoots,
95
- pathExists: options.pathExists,
96
- });
97
- if (transcriptFallback) {
98
- return attachApprovedRootWorkspace({
99
- outcome: transcriptFallback,
100
- signals,
101
- collectionRoots,
102
- approvedRecordedPaths,
103
- });
104
- }
105
- return attachApprovedRootWorkspace({
106
- outcome: skipped(terminalReasonForRecordedPaths(approvedRecordedPaths, options.pathExists)),
107
- signals,
108
- collectionRoots,
109
- approvedRecordedPaths,
110
- });
111
- }
112
- const reason = signals.cwds.length > 0 || signals.workspaceRoots.length > 0
113
- ? "cwd_outside_scanned_worktrees"
114
- : "no_matching_worktree_signals";
115
- return unattributed(reason);
116
- }
117
- const score = clampScore(best.score);
118
- const pathScore = clampScore(best.pathScore);
119
- if (best.pathScore > 0 &&
120
- best.score >= ATTRIBUTION_MIN_SCORE &&
121
- best.score - secondBestScore >= ATTRIBUTION_MIN_MARGIN) {
122
- return {
123
- state: "attributed",
124
- reason: "deterministic_signal_match",
125
- signals: best.matched,
126
- attribution_score: score,
127
- path_score: pathScore,
128
- worktree: best.worktree,
129
- };
130
- }
131
- if (best.pathScore === 0) {
132
- if (collectionRoots.length > 0 && approvedRecordedPaths.length === 0) {
133
- return unattributed(recordedPaths.length > 0
134
- ? "cwd_outside_scanned_worktrees"
135
- : "no_matching_worktree_signals");
136
- }
137
- const fallback = fallbackToKnownRepoPrimaryWorkContext({
138
- originLabel,
139
- scored,
140
- signals,
141
- worktrees: scopedWorktrees,
142
- });
143
- if (fallback) {
144
- return attachApprovedRootWorkspace({
145
- outcome: fallback,
146
- signals,
147
- collectionRoots,
148
- approvedRecordedPaths,
149
- });
150
- }
151
- }
152
- return attachApprovedRootWorkspace({
153
- outcome: {
154
- state: "ambiguous",
155
- reason: best.score - secondBestScore < ATTRIBUTION_MIN_MARGIN
156
- ? "multiple_worktrees_close_scores"
157
- : "signal_score_below_threshold",
158
- signals: best.matched,
159
- attribution_score: score,
160
- path_score: pathScore,
161
- worktree: null,
162
- },
163
- signals,
164
- collectionRoots,
165
- approvedRecordedPaths,
166
- });
167
- }
168
- /**
169
- * Gives an inside-boundary terminal attribution a durable upload context
170
- * without pretending the session belongs to any discovered repository. The
171
- * original state, reason, signals, and scores are deliberately untouched: the
172
- * synthetic folder is transport identity, not stronger attribution.
173
- */
174
- function attachApprovedRootWorkspace(options) {
175
- if (options.outcome.worktree || options.approvedRecordedPaths.length === 0) {
176
- return options.outcome;
177
- }
178
- const approvedRoot = deepestApprovedRootForRecordedPaths(options.approvedRecordedPaths, options.collectionRoots);
179
- if (!approvedRoot)
180
- return options.outcome;
181
- const repoLabel = basenameForAttributionPath(approvedRoot);
182
- return {
183
- ...options.outcome,
184
- worktree: {
185
- requested_path: approvedRoot,
186
- repo_root: approvedRoot,
187
- repo_label: repoLabel,
188
- repo_fingerprint: repoFingerprintFromLocalRoot(approvedRoot),
189
- repo_origin_url: null,
190
- branch: options.signals.branches[0] ?? "unknown",
191
- head_sha: options.signals.headShas[0] ?? null,
192
- worktree_label: repoLabel,
193
- worktree_fingerprint: stableWorktreeFingerprint(approvedRoot),
194
- worktree_is_primary: true,
195
- },
196
- };
197
- }
198
- function deepestApprovedRootForRecordedPaths(recordedPaths, collectionRoots) {
199
- const candidates = collectionRoots.filter((root) => recordedPaths.some((recordedPath) => isPathWithin(recordedPath, root)));
200
- candidates.sort((a, b) => pathDepth(b) - pathDepth(a) ||
201
- normalizedAttributionPathKey(a).localeCompare(normalizedAttributionPathKey(b)));
202
- return candidates[0] ? resolveAttributionPath(candidates[0]) : null;
203
- }
204
- function unattributed(reason) {
205
- return {
206
- state: "unattributed",
207
- reason,
208
- signals: [],
209
- attribution_score: 0,
210
- path_score: 0,
211
- worktree: null,
212
- };
213
- }
214
- function skipped(reason) {
215
- return {
216
- state: "skipped",
217
- reason,
218
- signals: [],
219
- attribution_score: 0,
220
- path_score: 0,
221
- worktree: null,
222
- };
223
- }
224
- /**
225
- * Handles sessions launched from wrapper folders that intentionally contain
226
- * several repos. The wrapper path is the user's working context even though it
227
- * is not itself a git repo, and the consent boundary is unchanged because this
228
- * only runs after the recorded path was proven under an approved collection
229
- * root.
230
- */
231
- function fallbackToFolderWorkspaceForContainerCwd(options) {
232
- let bestFolder = null;
233
- for (const recordedPath of pathComparisonAliases(options.approvedRecordedPaths)) {
234
- const resolvedPath = resolveAttributionPath(recordedPath);
235
- const containedWorktrees = options.worktrees.filter((worktree) => isPathStrictlyWithin(worktree.repo_root, resolvedPath));
236
- if (containedWorktrees.length === 0)
237
- continue;
238
- const depth = pathDepth(resolvedPath);
239
- if (!bestFolder || depth > bestFolder.depth) {
240
- bestFolder = { resolvedPath, depth, containedWorktrees };
241
- }
242
- }
243
- if (!bestFolder)
244
- return null;
245
- const repoFingerprints = new Set(bestFolder.containedWorktrees.map((worktree) => worktree.repo_fingerprint));
246
- if (repoFingerprints.size === 1) {
247
- const primary = sortWorktreesForFolderFallback(bestFolder.containedWorktrees)[0];
248
- if (!primary)
249
- return null;
250
- return {
251
- state: "attributed_fallback",
252
- reason: "single_repo_folder_fallback",
253
- signals: ["single_repo_folder_fallback"],
254
- attribution_score: clampScore(Math.min(SCORE_CWD_MATCH, ATTRIBUTED_FALLBACK_MAX_SCORE)),
255
- path_score: 0,
256
- worktree: primary,
257
- };
258
- }
259
- const repoLabel = basenameForAttributionPath(bestFolder.resolvedPath);
260
- return {
261
- state: "attributed_fallback",
262
- reason: "multi_repo_folder_workspace",
263
- signals: ["multi_repo_folder_workspace"],
264
- attribution_score: clampScore(ATTRIBUTED_FALLBACK_MAX_SCORE),
265
- path_score: 0,
266
- worktree: {
267
- requested_path: bestFolder.resolvedPath,
268
- repo_root: bestFolder.resolvedPath,
269
- repo_label: repoLabel,
270
- repo_fingerprint: repoFingerprintFromLocalRoot(bestFolder.resolvedPath),
271
- repo_origin_url: null,
272
- branch: options.signals.branches[0] ?? "unknown",
273
- head_sha: options.signals.headShas[0] ?? null,
274
- worktree_label: repoLabel,
275
- worktree_fingerprint: stableWorktreeFingerprint(bestFolder.resolvedPath),
276
- worktree_is_primary: true,
277
- },
278
- };
279
- }
280
- function fallbackToKnownRepoPrimaryWorkContext(options) {
281
- const originCandidates = options.worktrees.filter((worktree) => worktree.repo_origin_url &&
282
- options.signals.originUrls.includes(worktree.repo_origin_url));
283
- if (originCandidates.length === 0)
284
- return null;
285
- const primaryByClone = originCandidates.filter((worktree) => worktree.worktree_is_primary);
286
- const cloneCandidates = primaryByClone.length > 0 ? primaryByClone : originCandidates;
287
- const cloneRoots = new Set(cloneCandidates.map((worktree) => normalizedAttributionPathKey(worktree.repo_root)));
288
- if (cloneRoots.size > 1) {
289
- return {
290
- state: "ambiguous",
291
- reason: "multiple_repos_share_origin",
292
- signals: [options.originLabel],
293
- attribution_score: ATTRIBUTED_FALLBACK_MAX_SCORE,
294
- path_score: 0,
295
- worktree: null,
296
- };
297
- }
298
- const primary = primaryByClone[0] ??
299
- originCandidates.find((worktree) => worktree.worktree_is_primary) ??
300
- originCandidates[0] ??
301
- null;
302
- if (!primary)
303
- return null;
304
- const primaryScore = options.scored.find((entry) => entry.worktree.worktree_fingerprint === primary.worktree_fingerprint);
305
- const signals = new Set([
306
- ...(primaryScore?.matched ?? [options.originLabel]),
307
- "repo_primary_fallback",
308
- ]);
309
- return {
310
- state: "attributed_fallback",
311
- reason: "known_repo_primary_fallback",
312
- signals: [...signals],
313
- attribution_score: clampScore(Math.min(primaryScore?.score ?? SCORE_ORIGIN_MATCH, ATTRIBUTED_FALLBACK_MAX_SCORE)),
314
- path_score: 0,
315
- worktree: primary,
316
- };
317
- }
318
- /**
319
- * Reconstructs a repo/worktree identity from transcript origin metadata only
320
- * after the caller has already proven a recorded cwd/workspace root sits under
321
- * an operator-approved collection root. The origin is never allowed to widen
322
- * consent by itself: it only rescues sessions for repos that were once inside
323
- * the approved scan boundary but have since been deleted from disk, where the
324
- * transcript is still durable enough to carry the normalized remote identity.
325
- */
326
- function fallbackToTranscriptOriginForDeletedRepo(options) {
327
- const origins = [
328
- ...new Set(options.signals.originUrls
329
- .map((origin) => normalizeGitOrigin(origin))
330
- .filter(Boolean)),
331
- ];
332
- if (origins.length === 0)
333
- return null;
334
- if (origins.length > 1)
335
- return skipped("multiple_transcript_origins");
336
- if (!options.pathExists)
337
- return null;
338
- // Prefer an explicitly recorded workspace root, but only after proving that
339
- // exact selected path remains inside the operator-approved boundary. A
340
- // different cwd being approved must never authorize an outside workspace
341
- // root (or vice versa).
342
- const recordedPath = [...options.signals.workspaceRoots, ...options.signals.cwds].find((value) => {
343
- if (!options.collectionRoots.some((root) => isPathWithin(value, root))) {
344
- return false;
345
- }
346
- try {
347
- return !options.pathExists?.(value);
348
- }
349
- catch {
350
- // Deliberately silent (BLI-3238). This is an existence PROBE and
351
- // failure is the answer: a path we cannot test is a path we cannot
352
- // claim is missing, so the candidate is passed over. The scorer is
353
- // pure and synchronous by design and has no channel to report on.
354
- return false;
355
- }
356
- }) ?? null;
357
- if (!recordedPath)
358
- return null;
359
- const resolvedPath = resolveAttributionPath(recordedPath);
360
- const origin = origins[0] ?? "";
361
- return {
362
- state: "attributed_fallback",
363
- reason: "transcript_origin_fallback",
364
- signals: ["transcript_origin_fallback", options.originLabel],
365
- attribution_score: clampScore(SCORE_ORIGIN_MATCH),
366
- path_score: 0,
367
- worktree: {
368
- requested_path: resolvedPath,
369
- repo_root: resolvedPath,
370
- repo_label: repoLabelFromOrigin(origin),
371
- repo_fingerprint: repoFingerprintFromOrigin(origin),
372
- repo_origin_url: origin,
373
- branch: options.signals.branches[0] ?? "unknown",
374
- head_sha: options.signals.headShas[0] ?? null,
375
- worktree_label: basenameForAttributionPath(resolvedPath),
376
- worktree_fingerprint: stableWorktreeFingerprint(resolvedPath),
377
- worktree_is_primary: false,
378
- },
379
- };
380
- }
381
- function terminalReasonForRecordedPaths(paths, pathExists) {
382
- // A recorded path under a collection root can mean two different things:
383
- // deleted/missing repo (retryable) or an existing non-repo folder. The latter
384
- // is common with wrapper-folder workflows, so adapters inject existence only
385
- // at the boundary and the pure scorer keeps the old label when no hook exists.
386
- if (!pathExists)
387
- return "repo_not_on_disk";
388
- const anyRecordedPathExists = paths.some((value) => {
389
- try {
390
- return pathExists(value);
391
- }
392
- catch {
393
- // Deliberately silent (BLI-3238), same probe as above: "cannot test"
394
- // folds into "does not exist", which is the conservative direction —
395
- // it keeps the older `repo_not_on_disk` label rather than inventing
396
- // `cwd_not_a_repo`.
397
- return false;
398
- }
399
- });
400
- return anyRecordedPathExists ? "cwd_not_a_repo" : "repo_not_on_disk";
401
- }
402
- /**
403
- * For each path signal, the single worktree whose root most specifically
404
- * contains it (the longest containing root). Returns the set of worktree
405
- * fingerprints that win at least one path signal — these are the only roots
406
- * credited with the path score (D6).
407
- */
408
- function deepestContainers(signalValues, worktrees) {
409
- const winners = new Set();
410
- for (const value of signalValues) {
411
- let best = null;
412
- let bestRootLength = -1;
413
- for (const worktree of worktrees) {
414
- if (!isPathWithin(value, worktree.repo_root))
415
- continue;
416
- const rootLength = normalizedAttributionPathKey(worktree.repo_root).length;
417
- if (rootLength > bestRootLength) {
418
- best = worktree;
419
- bestRootLength = rootLength;
420
- }
421
- }
422
- if (best)
423
- winners.add(best.worktree_fingerprint);
424
- }
425
- return winners;
426
- }
427
- export function isPathWithin(candidate, root) {
428
- return pathContainment(candidate, root, false);
429
- }
430
- function isPathStrictlyWithin(candidate, root) {
431
- return pathContainment(candidate, root, true);
432
- }
433
- function recordedPathsWithinCollectionRoots(paths, collectionRoots) {
434
- if (paths.length === 0 || collectionRoots.length === 0)
435
- return [];
436
- return paths.filter((value) => collectionRoots.some((root) => isPathWithin(value, root)));
437
- }
438
- function worktreesWithinCollectionScope(worktrees, collectionRoots) {
439
- if (collectionRoots.length === 0)
440
- return worktrees;
441
- const directlyApproved = new Set(worktrees
442
- .filter((worktree) => collectionRoots.some((root) => isPathWithin(worktree.repo_root, root)))
443
- .map((worktree) => worktree.worktree_fingerprint));
444
- const approvedRepoFingerprints = new Set(worktrees
445
- .filter((worktree) => directlyApproved.has(worktree.worktree_fingerprint))
446
- .map((worktree) => worktree.repo_fingerprint));
447
- return worktrees.filter((worktree) => directlyApproved.has(worktree.worktree_fingerprint) ||
448
- (isCodexManagedWorktreePath(worktree.repo_root) &&
449
- approvedRepoFingerprints.has(worktree.repo_fingerprint)));
450
- }
451
- function isCodexManagedWorktreePath(value) {
452
- const pathApi = attributionPathApi(value);
453
- const windowsPath = isWindowsAbsolutePath(value);
454
- const parts = pathApi.resolve(value).split(pathApi.sep).filter(Boolean);
455
- return parts.some((part, index) => {
456
- const codex = windowsPath ? part.toLowerCase() : part;
457
- const nextPart = parts[index + 1] ?? "";
458
- const worktrees = windowsPath ? nextPart.toLowerCase() : nextPart;
459
- return codex === ".codex" && worktrees === "worktrees";
460
- });
461
- }
462
- function sortWorktreesForFolderFallback(worktrees) {
463
- return [...worktrees].sort((a, b) => Number(b.worktree_is_primary) - Number(a.worktree_is_primary) ||
464
- normalizedAttributionPathKey(a.repo_root).localeCompare(normalizedAttributionPathKey(b.repo_root)) ||
465
- a.worktree_fingerprint.localeCompare(b.worktree_fingerprint));
466
- }
467
- function pathDepth(value) {
468
- const pathApi = attributionPathApi(value);
469
- return pathApi.resolve(value).split(pathApi.sep).filter(Boolean).length;
470
- }
471
- /**
472
- * Transcript paths describe the host that produced the session, which may not
473
- * be the host currently running tests or replaying evidence. Detect Windows
474
- * drive/UNC paths from the values themselves so attribution keeps win32
475
- * separator and case rules on macOS/Linux too.
476
- */
477
- function pathContainment(candidate, root, strict) {
478
- const candidateIsWindows = isWindowsAbsolutePath(candidate);
479
- const rootIsWindows = isWindowsAbsolutePath(root);
480
- if (candidateIsWindows !== rootIsWindows)
481
- return false;
482
- const pathApi = candidateIsWindows ? path.win32 : path.posix;
483
- const normalizedCandidate = pathApi.resolve(candidate);
484
- const normalizedRoot = pathApi.resolve(root);
485
- return normalizedPathContainment(normalizedCandidate, normalizedRoot, pathApi, strict);
486
- }
487
- function normalizedPathContainment(candidate, root, pathApi, strict) {
488
- const relative = pathApi.relative(root, candidate);
489
- if (relative === "")
490
- return !strict;
491
- return (relative !== ".." &&
492
- !relative.startsWith(`..${pathApi.sep}`) &&
493
- !pathApi.isAbsolute(relative));
494
- }
495
- function collectionRootComparisonAliases(roots) {
496
- return pathComparisonAliases(roots);
497
- }
498
- function pathComparisonAliases(values) {
499
- const aliases = [];
500
- const seen = new Set();
501
- for (const value of values) {
502
- for (const candidate of [
503
- resolveAttributionPath(value),
504
- canonicalExistingAttributionPath(value),
505
- ]) {
506
- if (!candidate)
507
- continue;
508
- const key = normalizedAttributionPathKey(candidate);
509
- if (seen.has(key))
510
- continue;
511
- seen.add(key);
512
- aliases.push(candidate);
513
- }
514
- }
515
- return aliases;
516
- }
517
- function canonicalExistingAttributionPath(value) {
518
- const windowsPath = isWindowsAbsolutePath(value);
519
- if (windowsPath !== (process.platform === "win32"))
520
- return null;
521
- try {
522
- return attributionPathApi(value).resolve(realpathSync(value));
523
- }
524
- catch {
525
- // Deliberately silent (BLI-3238). `realpathSync` is the test for "does
526
- // this path resolve to something real?", and `null` — "no canonical form"
527
- // — is the answer, not a degradation. Callers already treat a null here
528
- // as one more alias that does not apply.
529
- return null;
530
- }
531
- }
532
- function attributionPathApi(value) {
533
- return isWindowsAbsolutePath(value) ? path.win32 : path.posix;
534
- }
535
- function resolveAttributionPath(value) {
536
- return attributionPathApi(value).resolve(value);
537
- }
538
- function basenameForAttributionPath(value) {
539
- return attributionPathApi(value).basename(value);
540
- }
541
- function normalizedAttributionPathKey(value) {
542
- const resolved = resolveAttributionPath(value);
543
- return isWindowsAbsolutePath(value) ? resolved.toLowerCase() : resolved;
544
- }
545
- function isWindowsAbsolutePath(value) {
546
- return (/^[A-Za-z]:[\\/]/.test(value) ||
547
- /^[\\/]{2}[^\\/]+[\\/][^\\/]+(?:[\\/]|$)/.test(value));
548
- }
549
- export function clampScore(value) {
550
- return Math.min(1, Math.max(0, Number(value.toFixed(4))));
551
- }
552
- export function sessionIdFromFileName(fileName) {
553
- const match = fileName.match(SESSION_FILE_UUID_PATTERN);
554
- return match?.[1]?.toLowerCase() ?? null;
555
- }
556
- /**
557
- * Session ids come from file content and end up inside remote object keys, so
558
- * anything outside the safe charset falls back to a hash of the raw value
559
- * instead of poisoning every begin request in the batch.
560
- */
561
- export function sanitizeSessionId(value) {
562
- if (!value)
563
- return null;
564
- if (/^[A-Za-z0-9._-]{4,80}$/.test(value))
565
- return value;
566
- return shortHash(value);
567
- }
568
- export function shortHash(value) {
569
- return crypto
570
- .createHash("sha256")
571
- .update(value, "utf8")
572
- .digest("hex")
573
- .slice(0, 16);
574
- }
575
- export function sha256(value) {
576
- return crypto.createHash("sha256").update(value).digest("hex");
577
- }
23
+ export { SCORE_CWD_MATCH, SCORE_WORKSPACE_ROOT_MATCH, SCORE_ORIGIN_MATCH, SCORE_BRANCH_MATCH, SCORE_HEAD_SHA_MATCH, ATTRIBUTION_MIN_SCORE, ATTRIBUTION_MIN_MARGIN, ATTRIBUTED_FALLBACK_MAX_SCORE, SESSION_FILE_UUID_PATTERN, } from "./attribution-core-types.js";
24
+ export { scoreSignalsAgainstWorktrees } from "./attribution-core-score.js";
25
+ export { isPathWithin, clampScore, sessionIdFromFileName, sanitizeSessionId, shortHash, sha256, } from "./attribution-core-paths.js";