@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.
- package/dist/adapters/attribution-core-fallbacks.js +247 -0
- package/dist/adapters/attribution-core-paths.js +182 -0
- package/dist/adapters/attribution-core-score.js +159 -0
- package/dist/adapters/attribution-core-types.js +13 -0
- package/dist/adapters/attribution-core.js +13 -565
- package/dist/adapters/claude-attribution-discovery.js +186 -0
- package/dist/adapters/claude-attribution-score.js +204 -0
- package/dist/adapters/claude-attribution-signals.js +180 -0
- package/dist/adapters/claude-attribution-types.js +25 -0
- package/dist/adapters/claude-attribution.js +14 -569
- package/dist/commands/doctor-access.js +129 -0
- package/dist/commands/doctor-pipeline.js +326 -0
- package/dist/commands/doctor-registration.js +105 -0
- package/dist/commands/doctor-report.js +111 -0
- package/dist/commands/doctor-update.js +120 -0
- package/dist/commands/doctor.js +8 -753
- package/dist/commands/heartbeat.js +8 -0
- package/dist/commands/jarvis-contracts.js +8 -0
- package/dist/commands/jarvis-render.js +413 -0
- package/dist/commands/jarvis-turn.js +305 -0
- package/dist/commands/jarvis.js +23 -698
- package/dist/commands/local-args-collector-setup.js +250 -0
- package/dist/commands/local-args-collector-status.js +227 -0
- package/dist/commands/local-args-collector-work.js +175 -0
- package/dist/commands/local-args-collector.js +19 -624
- package/dist/commands/local-args-tower-admin.js +456 -0
- package/dist/commands/local-args-tower-chat.js +194 -0
- package/dist/commands/local-args-tower-pages.js +314 -0
- package/dist/commands/local-args-tower.js +13 -880
- package/dist/commands/local-help.js +10 -2
- package/dist/commands/onboard-completion.js +136 -0
- package/dist/commands/onboard-flows.js +165 -0
- package/dist/commands/onboard-setup.js +102 -0
- package/dist/commands/onboard.js +5 -392
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-counters.js +55 -0
- package/dist/commands/session-sync-health.js +8 -1
- package/dist/commands/session-sync-plan.js +47 -7
- package/dist/commands/session-sync-scan.js +4 -4
- package/dist/commands/session-sync.js +6 -0
- package/dist/commands/settings-render.js +27 -0
- package/dist/commands/sync-followups.js +5 -1
- package/dist/commands/sync.js +5 -1
- package/dist/commands/team-device-reasons.js +16 -0
- package/dist/commands/team.js +87 -7
- package/dist/evidence-upload-client.js +14 -763
- package/dist/evidence-upload-object.js +181 -0
- package/dist/evidence-upload-plan.js +233 -0
- package/dist/evidence-upload-terminal.js +309 -0
- package/dist/evidence-upload-transport.js +104 -0
- package/dist/spool/local-spool-io.js +122 -0
- package/dist/spool/local-spool-mutations.js +174 -0
- package/dist/spool/local-spool-parse.js +143 -0
- package/dist/spool/local-spool-types.js +22 -0
- package/dist/spool/local-spool.js +20 -426
- package/dist/upload-evidence-delivery-offer.js +144 -0
- package/dist/upload-evidence-delivery-reconcile.js +134 -0
- package/dist/upload-evidence-delivery-summary.js +205 -0
- package/dist/upload-evidence-delivery.js +12 -482
- 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
|
|
18
|
-
export
|
|
19
|
-
export
|
|
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";
|