@bli-cockpit/cli 0.2.49 → 0.2.51
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/raw-evidence-claude-reader.js +108 -0
- package/dist/adapters/raw-evidence-codex-reader.js +147 -0
- package/dist/adapters/raw-evidence-collection-state.js +199 -0
- package/dist/adapters/raw-evidence-facts.js +338 -0
- package/dist/adapters/raw-evidence-git-diff-reader.js +187 -0
- package/dist/adapters/raw-evidence-image-reader.js +107 -0
- package/dist/adapters/raw-evidence-sanitize.js +56 -0
- package/dist/adapters/raw-evidence-transcript-file.js +182 -0
- package/dist/adapters/raw-evidence.js +63 -1183
- package/dist/commands/backfill-batches.js +34 -0
- package/dist/commands/backfill-candidates.js +54 -0
- package/dist/commands/backfill-checkpoint.js +101 -0
- package/dist/commands/backfill-command-line.js +70 -0
- package/dist/commands/backfill-evidence-outcomes.js +104 -0
- package/dist/commands/backfill-issues.js +265 -0
- package/dist/commands/backfill-output.js +75 -0
- package/dist/commands/backfill-plan.js +71 -0
- package/dist/commands/backfill-reasons.js +107 -0
- package/dist/commands/backfill-report.js +298 -0
- package/dist/commands/backfill-result.js +150 -0
- package/dist/commands/backfill-scan.js +274 -0
- package/dist/commands/backfill-scope.js +114 -0
- package/dist/commands/backfill-session-report.js +145 -0
- package/dist/commands/backfill-types.js +1 -0
- package/dist/commands/backfill-upload.js +212 -0
- package/dist/commands/backfill.js +41 -1961
- package/dist/commands/doctor.js +57 -0
- package/dist/commands/jarvis-trace.js +184 -0
- package/dist/commands/jarvis.js +144 -4
- package/dist/commands/local-args-collector.js +26 -0
- package/dist/commands/local-args-tower.js +21 -0
- package/dist/commands/local-args.js +3 -1
- package/dist/commands/local-help.js +19 -2
- package/dist/commands/local.js +3 -0
- package/dist/commands/memory-install-claude.js +294 -0
- package/dist/commands/memory-install-codex.js +205 -0
- package/dist/commands/memory-install-contract.js +286 -0
- package/dist/commands/memory-install-files.js +63 -0
- package/dist/commands/memory-install-skills.js +121 -0
- package/dist/commands/memory-install-toml.js +265 -0
- package/dist/commands/memory-install.js +465 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/sync-followups.js +105 -0
- package/dist/commands/sync.js +7 -1
- package/dist/local-state-attributed-target.js +75 -0
- package/dist/local-state-config.js +147 -0
- package/dist/local-state-files.js +59 -0
- package/dist/local-state-identity.js +73 -0
- package/dist/local-state-pairing.js +263 -0
- package/dist/local-state-paths.js +61 -0
- package/dist/local-state-session.js +68 -0
- package/dist/local-state-status.js +163 -0
- package/dist/local-state-work-context.js +190 -0
- package/dist/local-state.js +34 -848
- package/dist/tower-client.js +3 -2
- package/dist/tower-stream.js +57 -3
- package/package.json +2 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a person sees and is asked. Every line a human reads from
|
|
3
|
+
* `cockpit backfill` is written here: the dry-run review, the `--all`
|
|
4
|
+
* confirmation prompt, the final PASS/BLOCKED lines, and the skip/reason table
|
|
5
|
+
* that goes under all of them. A blocked or partial run always names its
|
|
6
|
+
* reason and prints the retry command; none of these paths may go silent.
|
|
7
|
+
*/
|
|
8
|
+
import { uploadableCandidates } from "./backfill-batches.js";
|
|
9
|
+
export function writeDryRunSummary(io, options) {
|
|
10
|
+
const uploadable = uploadableCandidates(options.candidates);
|
|
11
|
+
writeLine(io.stdout, `${options.dryRunOnly ? "DRY-RUN" : "Review"}: ${options.candidates.length} session(s), ${uploadable.length} uploadable, ${options.candidates.length - uploadable.length} skipped.`);
|
|
12
|
+
writeReasonTable(io, options.reasonCounts);
|
|
13
|
+
if (options.dryRunOnly) {
|
|
14
|
+
writeLine(io.stdout, "DRY-RUN: wrote nothing (no cursor, marker, report, or upload).");
|
|
15
|
+
}
|
|
16
|
+
writeLine(io.stdout, `Verify after upload: ${options.dashboardUrl}/my-work`);
|
|
17
|
+
}
|
|
18
|
+
export function writeHumanBackfillResult(result, io) {
|
|
19
|
+
if (result.status === "complete") {
|
|
20
|
+
writeLine(io.stdout, `PASS: ${result.counts.backfilled} backfilled, ${result.counts.skipped} skipped (table). Verify: ${result.verify_url}`);
|
|
21
|
+
writeReasonTable(io, result.counts.reasons);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (result.status === "partial") {
|
|
25
|
+
const at = result.blocked_at;
|
|
26
|
+
if (at) {
|
|
27
|
+
writeLine(io.stderr, `BLOCKED: ${at.what} at batch ${at.batch_index}/${at.batch_total}, ${at.done}/${at.total} done`);
|
|
28
|
+
}
|
|
29
|
+
writeLine(io.stderr, `Failure: ${result.failure_reason ?? "partial_backfill"}`);
|
|
30
|
+
writeLine(io.stderr, `Retry: ${result.retry_command}`);
|
|
31
|
+
writeLine(io.stderr, `Stopped: ${result.counts.remaining} remaining — rerun cockpit backfill to continue`);
|
|
32
|
+
writeReasonTable(io, result.counts.reasons);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const at = result.blocked_at;
|
|
36
|
+
if (at) {
|
|
37
|
+
writeLine(io.stderr, `BLOCKED: ${at.what} at batch ${at.batch_index}/${at.batch_total}, ${at.done}/${at.total} done`);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
writeLine(io.stderr, "BLOCKED: backfill could not run.");
|
|
41
|
+
}
|
|
42
|
+
writeLine(io.stderr, `Failure: ${result.failure_reason ?? "no_sessions"}`);
|
|
43
|
+
writeLine(io.stderr, `Retry: ${result.retry_command}`);
|
|
44
|
+
}
|
|
45
|
+
function writeReasonTable(io, reasons) {
|
|
46
|
+
if (reasons.length === 0)
|
|
47
|
+
return;
|
|
48
|
+
writeLine(io.stdout, "Skip/reason table:");
|
|
49
|
+
for (const reason of reasons) {
|
|
50
|
+
writeLine(io.stdout, `- ${reason.reason}: ${reason.count} (${reason.classification}; ${reason.note})`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export async function confirmAllBackfill(io) {
|
|
54
|
+
const answer = await readLine(io, "Proceed with --all backfill upload? [y/N] ");
|
|
55
|
+
return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
56
|
+
}
|
|
57
|
+
async function readLine(io, prompt) {
|
|
58
|
+
io.stdout.write(prompt);
|
|
59
|
+
io.stdin.setEncoding("utf8");
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const onData = (chunk) => {
|
|
62
|
+
io.stdin.removeListener("data", onData);
|
|
63
|
+
io.stdin.pause();
|
|
64
|
+
resolve(chunk);
|
|
65
|
+
};
|
|
66
|
+
io.stdin.resume();
|
|
67
|
+
io.stdin.on("data", onData);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
export function isInteractiveStdin(io) {
|
|
71
|
+
return Boolean(io.stdin.isTTY);
|
|
72
|
+
}
|
|
73
|
+
export function writeLine(stream, text) {
|
|
74
|
+
stream.write(`${text}\n`);
|
|
75
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLAN: given the scan, should this run actually upload anything right now?
|
|
3
|
+
* `--all` without `--yes` needs interactive confirmation; `--dry-run` reports
|
|
4
|
+
* what would happen and stops. Both terminal answers are results in their own
|
|
5
|
+
* right, so the operator still gets the full reason table either way.
|
|
6
|
+
*/
|
|
7
|
+
import { blockingScanIssues } from "./backfill-issues.js";
|
|
8
|
+
import { confirmAllBackfill, writeDryRunSummary } from "./backfill-output.js";
|
|
9
|
+
import { backfillResultBaseArgs, baseBackfillResult, } from "./backfill-result.js";
|
|
10
|
+
/**
|
|
11
|
+
* PLAN: given the scan, should this run actually upload anything right now?
|
|
12
|
+
* `--all` without `--yes` needs interactive confirmation; `--dry-run` reports
|
|
13
|
+
* what would happen and stops there. Either returns a terminal result;
|
|
14
|
+
* anything else proceeds to UPLOAD.
|
|
15
|
+
*/
|
|
16
|
+
export async function planBackfillRun(command, io, ctx) {
|
|
17
|
+
const { dashboardUrl, scan, reasonCounts } = ctx;
|
|
18
|
+
if (command.all && !command.yes) {
|
|
19
|
+
if (!command.json) {
|
|
20
|
+
writeDryRunSummary(io, {
|
|
21
|
+
candidates: scan.candidates,
|
|
22
|
+
reasonCounts,
|
|
23
|
+
dashboardUrl,
|
|
24
|
+
dryRunOnly: false,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const confirmed = await confirmAllBackfill(io);
|
|
28
|
+
if (!confirmed) {
|
|
29
|
+
return { kind: "result", result: confirmationDeclinedResult(command, ctx) };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (command.dryRun) {
|
|
33
|
+
if (!command.json) {
|
|
34
|
+
writeDryRunSummary(io, {
|
|
35
|
+
candidates: scan.candidates,
|
|
36
|
+
reasonCounts,
|
|
37
|
+
dashboardUrl,
|
|
38
|
+
dryRunOnly: true,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return { kind: "result", result: dryRunResult(command, ctx) };
|
|
42
|
+
}
|
|
43
|
+
return { kind: "proceed" };
|
|
44
|
+
}
|
|
45
|
+
/** The operator saw the `--all` review and said no. Nothing was written; everything remains. */
|
|
46
|
+
function confirmationDeclinedResult(command, ctx) {
|
|
47
|
+
const base = baseBackfillResult(command, backfillResultBaseArgs(ctx));
|
|
48
|
+
return {
|
|
49
|
+
...base,
|
|
50
|
+
status: "blocked",
|
|
51
|
+
counts: { ...base.counts, remaining: ctx.scan.candidates.length },
|
|
52
|
+
failure_reason: "confirmation_declined",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* What a `--dry-run` would have done. It completes unless the scan itself hit
|
|
57
|
+
* something blocking, because a dry run that reports "complete" over a scan
|
|
58
|
+
* that could not see all the history would be the confident wrong answer.
|
|
59
|
+
*/
|
|
60
|
+
function dryRunResult(command, ctx) {
|
|
61
|
+
const blockingIssues = blockingScanIssues(ctx.scan.issues);
|
|
62
|
+
return {
|
|
63
|
+
...baseBackfillResult(command, backfillResultBaseArgs(ctx)),
|
|
64
|
+
status: blockingIssues.length > 0 ? "partial" : "complete",
|
|
65
|
+
dry_run: true,
|
|
66
|
+
retry_command: ctx.retryCommand,
|
|
67
|
+
...(blockingIssues.length > 0
|
|
68
|
+
? { failure_reason: blockingIssues[0]?.reason }
|
|
69
|
+
: {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The skip-reason table printed under every backfill run: one row per reason,
|
|
3
|
+
* each saying whether a rerun can fix it and what would have to change.
|
|
4
|
+
* A reason with no verdict here is still reported — it falls back to the
|
|
5
|
+
* retryable default rather than going silent.
|
|
6
|
+
*/
|
|
7
|
+
import { increment } from "./backfill-issues.js";
|
|
8
|
+
export function reasonCountsFor(candidates, guardCounts, scanIssues) {
|
|
9
|
+
const counts = new Map();
|
|
10
|
+
for (const candidate of candidates)
|
|
11
|
+
increment(counts, candidate.reason);
|
|
12
|
+
for (const [reason, count] of guardCounts) {
|
|
13
|
+
counts.set(reason, Math.max(counts.get(reason) ?? 0, count));
|
|
14
|
+
}
|
|
15
|
+
for (const issue of scanIssues) {
|
|
16
|
+
counts.set(issue.reason, Math.max(counts.get(issue.reason) ?? 0, issue.count));
|
|
17
|
+
}
|
|
18
|
+
for (const required of ["file_too_large", "repo_not_on_disk"]) {
|
|
19
|
+
counts.set(required, counts.get(required) ?? 0);
|
|
20
|
+
}
|
|
21
|
+
return [...counts.entries()]
|
|
22
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
23
|
+
.map(([reason, count]) => ({
|
|
24
|
+
reason,
|
|
25
|
+
count,
|
|
26
|
+
...reasonClassification(reason),
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The skip-reason table printed under every backfill run. These strings are
|
|
31
|
+
* what a person reads when coverage is short, so each note says what would
|
|
32
|
+
* have to change, not merely that something went wrong. Reason labels here
|
|
33
|
+
* must match the ones the scan and the guards emit verbatim.
|
|
34
|
+
*/
|
|
35
|
+
const REASON_VERDICTS = new Map([
|
|
36
|
+
[
|
|
37
|
+
"secret_like_content_guard",
|
|
38
|
+
{
|
|
39
|
+
classification: "retryable",
|
|
40
|
+
note: "historical guard result; collector now masks and retries",
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
"secret_redaction_failed",
|
|
45
|
+
{
|
|
46
|
+
classification: "retryable",
|
|
47
|
+
note: "historical guard result; collector now masks and retries",
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
"file_too_large",
|
|
52
|
+
{
|
|
53
|
+
classification: "retryable",
|
|
54
|
+
note: "until the evidence file cap is raised",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
[
|
|
58
|
+
"repo_not_on_disk",
|
|
59
|
+
{ classification: "retryable", note: "repo must exist on disk" },
|
|
60
|
+
],
|
|
61
|
+
[
|
|
62
|
+
"cwd_not_a_repo",
|
|
63
|
+
{
|
|
64
|
+
classification: "permanent",
|
|
65
|
+
note: "cwd exists but is not a repo or folder workspace",
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
[
|
|
69
|
+
"multiple_transcript_origins",
|
|
70
|
+
{
|
|
71
|
+
classification: "permanent",
|
|
72
|
+
note: "multiple transcript origins; attribution is ambiguous",
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
[
|
|
76
|
+
"single_repo_folder_fallback",
|
|
77
|
+
{
|
|
78
|
+
classification: "permanent",
|
|
79
|
+
note: "uploadable single-repo folder workspace fallback",
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
[
|
|
83
|
+
"multi_repo_folder_workspace",
|
|
84
|
+
{
|
|
85
|
+
classification: "permanent",
|
|
86
|
+
note: "uploadable multi-repo folder workspace fallback",
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
]);
|
|
90
|
+
const DEFERRED_REASON_VERDICT = {
|
|
91
|
+
classification: "retryable",
|
|
92
|
+
note: "rerun cockpit backfill to continue",
|
|
93
|
+
};
|
|
94
|
+
const UNKNOWN_REASON_VERDICT = {
|
|
95
|
+
classification: "retryable",
|
|
96
|
+
note: "rerun after fixing source or collector state",
|
|
97
|
+
};
|
|
98
|
+
function reasonClassification(reason) {
|
|
99
|
+
const known = REASON_VERDICTS.get(reason);
|
|
100
|
+
if (known)
|
|
101
|
+
return known;
|
|
102
|
+
// A budget deferral is the one family, rather than one label: whichever
|
|
103
|
+
// budget ran out, the remedy is the same rerun.
|
|
104
|
+
if (reason.startsWith("deferred_"))
|
|
105
|
+
return DEFERRED_REASON_VERDICT;
|
|
106
|
+
return UNKNOWN_REASON_VERDICT;
|
|
107
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { BACKFILL_COMPLETION_RECHECK_MS, BACKFILL_COVERAGE_VERSION, recordBackfillScanCoverage, writeBackfillCompletionMarker, writeBackfillCursor, } from "../cursors/backfill-cursor.js";
|
|
2
|
+
import { describeError } from "../health-detail.js";
|
|
3
|
+
import { readLocalWorkContextForRepo, startLocalWorkContext, } from "../local-state.js";
|
|
4
|
+
import { postCodexSessionReport, } from "../upload.js";
|
|
5
|
+
import { candidateCursorKey } from "./backfill-candidates.js";
|
|
6
|
+
import { advanceBackfillCursorThroughResolvedPrefix, recordBackfillDurableSessionPointers, } from "./backfill-checkpoint.js";
|
|
7
|
+
import { blockingScanIssues } from "./backfill-issues.js";
|
|
8
|
+
import { backfillResultBaseArgs, baseBackfillResult, emptyReport, } from "./backfill-result.js";
|
|
9
|
+
import { buildBackfillSessionReport } from "./backfill-session-report.js";
|
|
10
|
+
/**
|
|
11
|
+
* REPORT: post the session report, record durable pointers, advance the
|
|
12
|
+
* cursor through the resolved contiguous prefix, decide completion, write the
|
|
13
|
+
* all-history completion marker when this run actually finished it, and
|
|
14
|
+
* assemble the final result. `remaining`/`completionBlocked` deliberately
|
|
15
|
+
* treat an oversized skip differently (BLI-2727): it still counts toward
|
|
16
|
+
* `remaining` so the JSON output never goes silent about it, but it never
|
|
17
|
+
* blocks completion on its own — every completion-gating computation below
|
|
18
|
+
* excludes it explicitly via `oversizedCandidateKeys`/`blockingScanIssues`.
|
|
19
|
+
*/
|
|
20
|
+
export async function reportBackfillOutcome(command, io, ctx, upload) {
|
|
21
|
+
let { blockedAt, failureReason } = upload;
|
|
22
|
+
const posted = await postBackfillSessionReport(command, io, ctx, upload);
|
|
23
|
+
if (!posted.acknowledged && !blockedAt) {
|
|
24
|
+
blockedAt = sessionReportStopPoint(upload);
|
|
25
|
+
failureReason ??= sessionReportFailureReason(posted);
|
|
26
|
+
}
|
|
27
|
+
if (posted.acknowledged) {
|
|
28
|
+
await checkpointResolvedBackfillProgress(ctx, upload);
|
|
29
|
+
}
|
|
30
|
+
const completion = summarizeBackfillCompletion({
|
|
31
|
+
ctx,
|
|
32
|
+
upload,
|
|
33
|
+
posted,
|
|
34
|
+
stoppedEarly: Boolean(blockedAt),
|
|
35
|
+
failureReason,
|
|
36
|
+
});
|
|
37
|
+
if (completion.status === "complete" && command.all) {
|
|
38
|
+
await writeAllHistoryCompletionMarker(ctx);
|
|
39
|
+
}
|
|
40
|
+
return assembleBackfillResult({
|
|
41
|
+
command,
|
|
42
|
+
ctx,
|
|
43
|
+
upload,
|
|
44
|
+
posted,
|
|
45
|
+
completion,
|
|
46
|
+
blockedAt,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Tells the server what this run observed about every session, uploaded or
|
|
51
|
+
* not. Its acknowledgement — not raw-evidence durability alone — is what makes
|
|
52
|
+
* a historical cursor position irreversible, so the caller gates the whole
|
|
53
|
+
* checkpoint on the `acknowledged` flag returned here.
|
|
54
|
+
*/
|
|
55
|
+
async function postBackfillSessionReport(command, io, ctx, upload) {
|
|
56
|
+
const sessions = buildBackfillSessionReport({
|
|
57
|
+
candidates: ctx.scan.candidates,
|
|
58
|
+
syncResults: upload.syncResults,
|
|
59
|
+
now: ctx.now,
|
|
60
|
+
});
|
|
61
|
+
if (sessions.length === 0) {
|
|
62
|
+
return {
|
|
63
|
+
sessions,
|
|
64
|
+
report: emptyReport("no_sessions_observed"),
|
|
65
|
+
acknowledged: true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const reportContext = await ensureBackfillReportContext({
|
|
69
|
+
homeDir: command.homeDir,
|
|
70
|
+
paths: ctx.paths,
|
|
71
|
+
collectionRoots: ctx.collectionRoots,
|
|
72
|
+
worktrees: ctx.worktrees,
|
|
73
|
+
candidates: ctx.scan.candidates,
|
|
74
|
+
});
|
|
75
|
+
const report = await postCodexSessionReport({
|
|
76
|
+
homeDir: command.homeDir,
|
|
77
|
+
repoRoot: reportContext.repoRoot,
|
|
78
|
+
dashboardUrl: ctx.dashboardUrl,
|
|
79
|
+
sessions,
|
|
80
|
+
fetch: io.fetch,
|
|
81
|
+
now: ctx.now,
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
sessions,
|
|
85
|
+
report,
|
|
86
|
+
acknowledged: report.posted && report.recorded_count >= sessions.length,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Where a run that uploaded cleanly but could not file its report stopped. */
|
|
90
|
+
function sessionReportStopPoint(upload) {
|
|
91
|
+
return {
|
|
92
|
+
what: "session report failed",
|
|
93
|
+
batch_index: upload.completedBatches,
|
|
94
|
+
batch_total: upload.batches.length,
|
|
95
|
+
done: upload.done,
|
|
96
|
+
total: upload.uploadable.length,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** A partial acknowledgement is its own reason; otherwise the post's own. */
|
|
100
|
+
function sessionReportFailureReason(posted) {
|
|
101
|
+
return posted.report.posted &&
|
|
102
|
+
posted.report.recorded_count < posted.sessions.length
|
|
103
|
+
? "session_report_ack_incomplete"
|
|
104
|
+
: posted.report.reason;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Makes this run's progress durable once the server has acknowledged it:
|
|
108
|
+
* session pointers first, then the backfill cursor through the contiguous
|
|
109
|
+
* resolved prefix. Raw-evidence durability is necessary but not sufficient —
|
|
110
|
+
* both writes wait on the report acknowledgement, because a cursor advanced
|
|
111
|
+
* past a session the server never recorded can never be walked back.
|
|
112
|
+
*/
|
|
113
|
+
async function checkpointResolvedBackfillProgress(ctx, upload) {
|
|
114
|
+
const { paths, scan, cursor, now } = ctx;
|
|
115
|
+
if (upload.durableCandidateKeys.size > 0) {
|
|
116
|
+
await recordBackfillDurableSessionPointers({
|
|
117
|
+
paths,
|
|
118
|
+
candidates: scan.candidates,
|
|
119
|
+
syncResults: upload.syncResults,
|
|
120
|
+
now,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const cursorAdvanced = advanceBackfillCursorThroughResolvedPrefix({
|
|
124
|
+
cursor,
|
|
125
|
+
candidates: scan.candidates,
|
|
126
|
+
durableCandidateKeys: upload.durableCandidateKeys,
|
|
127
|
+
retryableCandidateKeys: scan.retryable_candidate_keys,
|
|
128
|
+
discoveryComplete: !scan.issues.some((issue) => issue.scope === "global"),
|
|
129
|
+
now,
|
|
130
|
+
});
|
|
131
|
+
if (cursorAdvanced)
|
|
132
|
+
await writeBackfillCursor(paths, cursor);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Answers "is this backfill finished, and if not, why not" as pure arithmetic
|
|
136
|
+
* over what the scan found and what the upload resolved — no side effects, so
|
|
137
|
+
* the one place BLI-2727's oversized-skip rule is applied is readable in full.
|
|
138
|
+
* `remaining` is a reporting total that still counts oversized skips so the
|
|
139
|
+
* JSON never goes silent about them; every `*Blocking` count excludes them,
|
|
140
|
+
* because a file too large for the current cap is a permanent labeled fact
|
|
141
|
+
* that no rerun can resolve and completion must not wait on forever.
|
|
142
|
+
*/
|
|
143
|
+
function summarizeBackfillCompletion(options) {
|
|
144
|
+
const { scan, oversizedCandidateKeys } = options.ctx;
|
|
145
|
+
const { upload, posted } = options;
|
|
146
|
+
const unresolvedUploadableKeys = new Set(upload.uploadable
|
|
147
|
+
.filter((candidate) => !upload.durableCandidateKeys.has(candidateCursorKey(candidate)))
|
|
148
|
+
.map(candidateCursorKey));
|
|
149
|
+
const unresolvedUploadableBlocking = [...unresolvedUploadableKeys].filter((key) => !oversizedCandidateKeys.has(key)).length;
|
|
150
|
+
const unresolvedRetryableBlocking = [...scan.retryable_candidate_keys].filter((key) => !oversizedCandidateKeys.has(key)).length;
|
|
151
|
+
const remaining = countRemainingHistory({
|
|
152
|
+
scan,
|
|
153
|
+
unresolvedUploadableKeys,
|
|
154
|
+
posted,
|
|
155
|
+
});
|
|
156
|
+
const failed = Math.max(upload.failed, unresolvedUploadableBlocking);
|
|
157
|
+
const blockingIssues = blockingScanIssues(scan.issues);
|
|
158
|
+
const completionBlocked = blockingIssues.length > 0 ||
|
|
159
|
+
unresolvedUploadableBlocking > 0 ||
|
|
160
|
+
unresolvedRetryableBlocking > 0 ||
|
|
161
|
+
upload.deferred > 0 ||
|
|
162
|
+
failed > 0 ||
|
|
163
|
+
!posted.acknowledged;
|
|
164
|
+
let failureReason = options.failureReason;
|
|
165
|
+
if (!failureReason && completionBlocked) {
|
|
166
|
+
const retryableCandidateReason = scan.candidates.find((candidate) => scan.retryable_candidate_keys.has(candidateCursorKey(candidate)) &&
|
|
167
|
+
!oversizedCandidateKeys.has(candidateCursorKey(candidate)))?.reason;
|
|
168
|
+
failureReason =
|
|
169
|
+
blockingIssues[0]?.reason ??
|
|
170
|
+
(unresolvedUploadableBlocking > 0
|
|
171
|
+
? "durable_session_pointer_missing"
|
|
172
|
+
: (retryableCandidateReason ?? "backfill_incomplete"));
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
remaining,
|
|
176
|
+
failed,
|
|
177
|
+
status: options.stoppedEarly || completionBlocked ? "partial" : "complete",
|
|
178
|
+
failureReason,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* How much history a rerun still has to do. A reporting total, not a
|
|
183
|
+
* completion gate: it counts every unresolved candidate — oversized skips
|
|
184
|
+
* included, so `--json` never goes silent about them (BLI-2727) — plus what
|
|
185
|
+
* selection dropped, what discovery never saw, and the report itself when the
|
|
186
|
+
* server has not acknowledged it.
|
|
187
|
+
*/
|
|
188
|
+
function countRemainingHistory(options) {
|
|
189
|
+
const unresolvedKnownKeys = new Set([
|
|
190
|
+
...options.unresolvedUploadableKeys,
|
|
191
|
+
...options.scan.retryable_candidate_keys,
|
|
192
|
+
]);
|
|
193
|
+
const unseenGlobalFailures = options.scan.issues
|
|
194
|
+
.filter((issue) => issue.scope === "global")
|
|
195
|
+
.reduce((total, issue) => total + issue.count, 0);
|
|
196
|
+
const reportRetryable = options.posted.acknowledged
|
|
197
|
+
? 0
|
|
198
|
+
: // The cursor is intentionally all-or-nothing for the report. Even
|
|
199
|
+
// chunks already accepted by the server are retried idempotently when
|
|
200
|
+
// another required chunk lacks an acknowledgement.
|
|
201
|
+
Math.max(1, options.posted.sessions.length);
|
|
202
|
+
return (unresolvedKnownKeys.size +
|
|
203
|
+
options.scan.omitted_candidate_count +
|
|
204
|
+
unseenGlobalFailures +
|
|
205
|
+
reportRetryable);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Proof, for doctor and status, that archived history is covered. Only an
|
|
209
|
+
* `--all` run earns it: a bounded `--since-days` run may complete its
|
|
210
|
+
* requested window, but that is not proof of an all-history backfill.
|
|
211
|
+
*/
|
|
212
|
+
async function writeAllHistoryCompletionMarker(ctx) {
|
|
213
|
+
const { paths, cursor, sources, scopedCursor, scan, oversizedCandidateKeys, now } = ctx;
|
|
214
|
+
recordBackfillScanCoverage(cursor, sources, now, now);
|
|
215
|
+
await writeBackfillCursor(paths, cursor);
|
|
216
|
+
const oversizedCandidates = scan.candidates.filter((candidate) => oversizedCandidateKeys.has(candidateCursorKey(candidate)));
|
|
217
|
+
await writeBackfillCompletionMarker(paths, {
|
|
218
|
+
schema_version: "cockpit-backfill-complete.v2",
|
|
219
|
+
coverage_version: BACKFILL_COVERAGE_VERSION,
|
|
220
|
+
collection_scope_id: scopedCursor.collection_scope_id,
|
|
221
|
+
sources,
|
|
222
|
+
completed_at: now.toISOString(),
|
|
223
|
+
revalidate_after: new Date(now.getTime() + BACKFILL_COMPLETION_RECHECK_MS).toISOString(),
|
|
224
|
+
cursor,
|
|
225
|
+
...(oversizedCandidates.length > 0
|
|
226
|
+
? {
|
|
227
|
+
oversized_skips: {
|
|
228
|
+
reason: "file_too_large",
|
|
229
|
+
count: oversizedCandidates.length,
|
|
230
|
+
byte_sizes: oversizedCandidates.map((candidate) => candidate.byte_size),
|
|
231
|
+
},
|
|
232
|
+
}
|
|
233
|
+
: {}),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/** The `--json` payload: every stage's numbers merged onto the scan's baseline. */
|
|
237
|
+
function assembleBackfillResult(options) {
|
|
238
|
+
const { command, ctx, upload, posted, completion } = options;
|
|
239
|
+
const base = baseBackfillResult(command, backfillResultBaseArgs(ctx));
|
|
240
|
+
return {
|
|
241
|
+
...base,
|
|
242
|
+
status: completion.status,
|
|
243
|
+
retry_command: ctx.retryCommand,
|
|
244
|
+
counts: {
|
|
245
|
+
...base.counts,
|
|
246
|
+
backfilled: upload.backfilledSessions,
|
|
247
|
+
failed: completion.failed,
|
|
248
|
+
deferred: upload.deferred,
|
|
249
|
+
remaining: completion.remaining,
|
|
250
|
+
},
|
|
251
|
+
batches: {
|
|
252
|
+
total: upload.batches.length,
|
|
253
|
+
completed: upload.completedBatches,
|
|
254
|
+
failed: upload.failedBatches,
|
|
255
|
+
},
|
|
256
|
+
report: posted.report,
|
|
257
|
+
server_acknowledged: {
|
|
258
|
+
codex_session_report_recorded_count: posted.report.recorded_count,
|
|
259
|
+
raw_evidence_uploaded_object_count: upload.uploadedObjects,
|
|
260
|
+
raw_evidence_uploaded_chunk_count: upload.uploadedChunks,
|
|
261
|
+
},
|
|
262
|
+
blocked_at: options.blockedAt,
|
|
263
|
+
failure_reason: completion.failureReason,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
async function ensureBackfillReportContext(options) {
|
|
267
|
+
const candidateWorktree = options.candidates.find((candidate) => candidate.worktree)?.worktree;
|
|
268
|
+
const representative = candidateWorktree ?? options.worktrees[0] ?? null;
|
|
269
|
+
// A deleted repo or an approved folder workspace can legitimately produce
|
|
270
|
+
// report-only rows with no current git worktree. Use the exact approved root
|
|
271
|
+
// as a synthetic local context instead of falling through to process.cwd().
|
|
272
|
+
const repoRoot = representative?.repo_root ??
|
|
273
|
+
options.collectionRoots[0] ??
|
|
274
|
+
process.cwd();
|
|
275
|
+
try {
|
|
276
|
+
await readLocalWorkContextForRepo(options.paths, repoRoot);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Reading it can legitimately fail — there is no context yet, which is
|
|
280
|
+
// precisely why the next line creates one. That read is a probe and stays
|
|
281
|
+
// silent; the CREATE is the branch that has to speak (BLI-3238).
|
|
282
|
+
await startLocalWorkContext({
|
|
283
|
+
homeDir: options.homeDir,
|
|
284
|
+
repoRoot,
|
|
285
|
+
branch: representative?.branch,
|
|
286
|
+
}).catch((error) => {
|
|
287
|
+
// Context creation is best-effort here: postCodexSessionReport converts
|
|
288
|
+
// a remaining local-context failure into a retryable report reason. But
|
|
289
|
+
// that reason is `collector_not_ready`, which points the operator at
|
|
290
|
+
// setup rather than at whatever actually failed here.
|
|
291
|
+
console.error("[cockpit-backfill] could not start a local work context for the batch", JSON.stringify({
|
|
292
|
+
reason: "work_context_start_failed",
|
|
293
|
+
...describeError(error),
|
|
294
|
+
}));
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return { repoRoot };
|
|
298
|
+
}
|