@counterfact-labs/cli 0.2.6 → 0.2.9
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/README.md +20 -0
- package/bundle/client-release.json +1 -1
- package/dist/adapters/analysis-results.js +87 -0
- package/dist/adapters/combined-results.js +27 -0
- package/dist/adapters/completion-lock.js +95 -0
- package/dist/adapters/completion.js +132 -17
- package/dist/adapters/entry-files.js +129 -0
- package/dist/adapters/entry-packing.js +161 -0
- package/dist/adapters/entry-selection.js +57 -0
- package/dist/adapters/files.js +111 -5
- package/dist/adapters/git.js +10 -1
- package/dist/adapters/relay-plan.js +5 -7
- package/dist/adapters/release.js +4 -1
- package/dist/adapters/results.js +18 -3
- package/dist/adapters/selection.js +114 -0
- package/dist/adapters/settings.js +62 -4
- package/dist/adapters/source-relay-plan.js +7 -0
- package/dist/analysis-core/analysis/code-agents/skipped-files.js +1 -1
- package/dist/analysis-core/analysis/file-analysis-plan.js +1 -0
- package/dist/analysis-core/analysis/file-dependencies.js +1 -0
- package/dist/analysis-core/analysis/finding-activity-switch.js +1 -0
- package/dist/analysis-core/analysis/finding-identity.js +1 -0
- package/dist/analysis-core/analysis/finding-quality/policy.js +1 -0
- package/dist/analysis-core/analysis/folder-analysis-plan.js +1 -0
- package/dist/analysis-core/analysis/passes/full-file-passes.js +1 -0
- package/dist/analysis-core/analysis/project-architecture.js +1 -0
- package/dist/analysis-core/analysis/schemas.js +1 -1
- package/dist/analysis-core/chunks/2AASSZRO.js +35 -0
- package/dist/analysis-core/chunks/2YREYGOG.js +1 -0
- package/dist/analysis-core/chunks/33HRXJ5Q.js +1 -0
- package/dist/analysis-core/chunks/33IGHOYT.js +1 -0
- package/dist/analysis-core/chunks/4PT5C4IT.js +7 -0
- package/dist/analysis-core/chunks/4RXRAB5S.js +1 -0
- package/dist/analysis-core/chunks/4UDQBDDE.js +1 -0
- package/dist/analysis-core/chunks/{OPMA32A6.js → 5B5UCWD3.js} +1 -1
- package/dist/analysis-core/chunks/6YCABS5T.js +1 -0
- package/dist/analysis-core/chunks/CQQ4TTXE.js +1 -0
- package/dist/analysis-core/chunks/CTLHKFPH.js +19 -0
- package/dist/analysis-core/chunks/F54DOWKN.js +6 -0
- package/dist/analysis-core/chunks/FRXOQ2M6.js +3 -0
- package/dist/analysis-core/chunks/HCPO6QPU.js +1 -0
- package/dist/analysis-core/chunks/HHFDNY6V.js +14 -0
- package/dist/analysis-core/chunks/{3JWEFCTJ.js → HVQJKPQ4.js} +1 -1
- package/dist/analysis-core/chunks/INVTU73D.js +1 -0
- package/dist/analysis-core/chunks/JEWBPIQI.js +1 -0
- package/dist/analysis-core/chunks/JGK37AWN.js +1 -0
- package/dist/analysis-core/chunks/LFYTD7JF.js +1 -0
- package/dist/analysis-core/chunks/LMUXOKIM.js +1 -0
- package/dist/analysis-core/chunks/LOBIDUCA.js +1 -0
- package/dist/analysis-core/chunks/M5YWJY7P.js +1 -0
- package/dist/analysis-core/chunks/MGVIKA74.js +1 -0
- package/dist/analysis-core/chunks/MKYLK6DB.js +1 -0
- package/dist/analysis-core/chunks/MZOKZ3N6.js +1 -0
- package/dist/analysis-core/chunks/NXHVBJL4.js +1 -0
- package/dist/analysis-core/chunks/OKBK3JL3.js +1 -0
- package/dist/analysis-core/chunks/PBHHWFR6.js +1 -0
- package/dist/analysis-core/chunks/Q5WR6B2H.js +2 -0
- package/dist/analysis-core/chunks/QVXARKP7.js +1 -0
- package/dist/analysis-core/chunks/RQTNTIR4.js +1 -0
- package/dist/analysis-core/chunks/RUVMFAST.js +1 -0
- package/dist/analysis-core/chunks/S27VIQMZ.js +1 -0
- package/dist/analysis-core/chunks/{YDBO7P4X.js → S27Y2TP5.js} +1 -1
- package/dist/analysis-core/chunks/SJESFYBA.js +10 -0
- package/dist/analysis-core/chunks/SP6XFHNH.js +1 -0
- package/dist/analysis-core/chunks/VDUFPTDD.js +1 -0
- package/dist/analysis-core/chunks/W24SFYZM.js +67 -0
- package/dist/analysis-core/chunks/YTHF2E2S.js +1 -0
- package/dist/analysis-core/chunks/YWL52FVW.js +2 -0
- package/dist/analysis-core/contracts/analysis-selection.js +1 -0
- package/dist/analysis-core/contracts/analysis-size.js +1 -0
- package/dist/analysis-core/contracts/execution-request.js +1 -0
- package/dist/analysis-core/contracts/intake-finding.js +1 -0
- package/dist/analysis-core/contracts/model-call-identity.js +1 -0
- package/dist/analysis-core/contracts/module-resolution.js +1 -0
- package/dist/analysis-core/contracts/relay-analysis.js +1 -1
- package/dist/analysis-core/contracts/requests.js +1 -1
- package/dist/analysis-core/contracts/selection-paths.js +1 -0
- package/dist/analysis-core/conventions/analysis-fit.js +1 -1
- package/dist/analysis-core/conventions/file-admission.js +1 -1
- package/dist/analysis-core/conventions/read-limits.js +1 -0
- package/dist/analysis-core/import/calls/modules.js +1 -0
- package/dist/analysis-core/import/entry-selection.js +1 -0
- package/dist/analysis-core/import/outside-loader-sources.js +1 -1
- package/dist/analysis-core/index.js +3 -3
- package/dist/analysis-core/requests/answer-formats.js +1 -1
- package/dist/analysis-core/requests/local-plan.js +1 -1
- package/dist/analysis-core/requests/planned-limits.js +1 -0
- package/dist/analysis-core/requests/registry.js +1 -1
- package/dist/analysis-core/requests/template-version.js +1 -1
- package/dist/analysis-core/requests/tools.js +1 -1
- package/dist/analysis-core/settings.js +1 -1
- package/dist/analysis-core/stages/engine-entry.js +1 -1
- package/dist/bundle/seal/engine.js +1 -1
- package/dist/cli/analysis-selection.js +133 -0
- package/dist/cli/api.js +1 -1
- package/dist/cli/commands/analyze.js +82 -14
- package/dist/cli/commands/skipped.js +65 -0
- package/dist/cli/completion-delivery.js +63 -0
- package/dist/cli/completion-recovery.js +68 -14
- package/dist/cli/context.js +4 -2
- package/dist/cli/finding-display.js +102 -7
- package/dist/cli/finding-settings.js +3 -1
- package/dist/cli/intake-payload.js +26 -2
- package/dist/cli/main.js +37 -11
- package/dist/cli/mcp-failure.js +6 -5
- package/dist/cli/mcp.js +191 -26
- package/dist/cli/prior-completion.js +35 -0
- package/dist/cli/relay-check.js +260 -110
- package/dist/cli/relay-failure.js +32 -0
- package/dist/cli/relay-preparation.js +72 -21
- package/dist/cli/result-check.js +31 -12
- package/dist/cli/results-upload.js +131 -0
- package/dist/cli/saved-results.js +10 -0
- package/dist/cli/scope-choices.js +57 -0
- package/dist/cli/sign-in.js +2 -2
- package/dist/cli/skipped-files.js +22 -0
- package/dist/cli/summary.js +15 -1
- package/dist/contract/agent-choice.js +25 -2
- package/dist/contract/cli-api-messages.js +3 -0
- package/dist/contract/cli-refusals.js +39 -5
- package/dist/contract/cli-version.js +1 -1
- package/dist/contract/console-url.js +3 -0
- package/dist/contract/console.js +24 -40
- package/dist/contract/engine.js +1 -1
- package/dist/contract/messages.js +4 -3
- package/dist/contract/project-architecture.js +1 -0
- package/dist/contract/scope-choice.js +98 -0
- package/dist/contract/selection-metadata.js +9 -0
- package/dist/contract/selection-offer.js +40 -0
- package/dist/gatekeeper/cli-api.js +70 -4
- package/dist/gatekeeper/console.js +55 -28
- package/dist/gatekeeper/model.js +5 -23
- package/dist/gatekeeper/trust/policy.js +2 -1
- package/package.json +5 -3
- package/dist/analysis-core/analysis/source-notes/types.js +0 -1
- package/dist/analysis-core/chunks/2PSNOSJ2.js +0 -1
- package/dist/analysis-core/chunks/377MLK4M.js +0 -21
- package/dist/analysis-core/chunks/5DGPLXYB.js +0 -1
- package/dist/analysis-core/chunks/6B2HILSY.js +0 -1
- package/dist/analysis-core/chunks/6QY5BV5B.js +0 -14
- package/dist/analysis-core/chunks/6U2IR4XB.js +0 -1
- package/dist/analysis-core/chunks/DLPVYWHZ.js +0 -1
- package/dist/analysis-core/chunks/FOKDE64Y.js +0 -1
- package/dist/analysis-core/chunks/FSLWD2PE.js +0 -1
- package/dist/analysis-core/chunks/K2GNDX3W.js +0 -1
- package/dist/analysis-core/chunks/KTNA2KTN.js +0 -7
- package/dist/analysis-core/chunks/KWECQJ6X.js +0 -1
- package/dist/analysis-core/chunks/NIOS7U6C.js +0 -41
- package/dist/analysis-core/chunks/PECX3OXB.js +0 -1
- package/dist/analysis-core/chunks/PYKAERZZ.js +0 -1
- package/dist/analysis-core/chunks/QFY6A3MS.js +0 -66
- package/dist/analysis-core/chunks/SKVEBOGA.js +0 -1
- package/dist/analysis-core/chunks/STI4JE7L.js +0 -1
- package/dist/analysis-core/chunks/SVMEWGD3.js +0 -1
- package/dist/analysis-core/chunks/X7WZAA7S.js +0 -1
- package/dist/analysis-core/chunks/ZTOVBAG7.js +0 -1
- package/dist/analysis-core/data/finding-types.js +0 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { formatSavedResults } from "../saved-results.js";
|
|
2
|
+
import { skipped } from "./skipped.js";
|
|
1
3
|
import { loadDevelopmentRelay } from "../development-relay.js";
|
|
2
4
|
import { loadInstalledRelease } from "../../adapters/release.js";
|
|
3
5
|
import { cliRefusals, START_FAILURE_MESSAGE } from "../../contract/cli-refusals.js";
|
|
@@ -10,7 +12,13 @@ import { GITIGNORE_OUTPUT_NOTICE } from "../../adapters/gitignore-output.js";
|
|
|
10
12
|
import { readFile } from "node:fs/promises";
|
|
11
13
|
import { resolve } from "node:path";
|
|
12
14
|
import { loadAgentProposal } from "../../contract/agent-proposal.js";
|
|
13
|
-
import {
|
|
15
|
+
import { fileScopeOf } from "../../analysis-core/analysis/file-analysis-plan.js";
|
|
16
|
+
import { folderPartsEnabled } from "../../analysis-core/settings.js";
|
|
17
|
+
import { captureArchitectureSettings, scopeChoicesEnabled } from "../../adapters/settings.js";
|
|
18
|
+
import { resolveRunSelection } from "../analysis-selection.js";
|
|
19
|
+
import { renderScopeChoices } from "../scope-choices.js";
|
|
20
|
+
import { progressLine } from "../../contract/scope-choice.js";
|
|
21
|
+
import { consoleCapabilities } from "../../gatekeeper/console.js";
|
|
14
22
|
// Only a real pause says "paused"; an unreviewed release or an upload-only server cannot start.
|
|
15
23
|
const unavailable = START_FAILURE_MESSAGE;
|
|
16
24
|
function requireLocal(value) {
|
|
@@ -30,29 +38,70 @@ function requireLocal(value) {
|
|
|
30
38
|
// A local-capable client never falls back to sending source files to the hosted upload API.
|
|
31
39
|
if (value.mode !== "local")
|
|
32
40
|
throw new ApiError(503, "unavailable", unavailable);
|
|
41
|
+
return consoleCapabilities(value);
|
|
33
42
|
}
|
|
34
|
-
export function parseAnalyzeOptions(args) {
|
|
35
|
-
let path, proposalFile;
|
|
43
|
+
export function parseAnalyzeOptions(args, choices = false, multiple = false) {
|
|
44
|
+
let path, proposalFile, scopeFile;
|
|
45
|
+
let skipped;
|
|
46
|
+
let scope;
|
|
47
|
+
const paths = [];
|
|
36
48
|
for (let index = 0; index < args.length; index++) {
|
|
37
49
|
const arg = args[index];
|
|
38
|
-
if (
|
|
50
|
+
if (["--skipped", "--skipped-plan", "--show-skipped"].includes(arg) && skipped === undefined) {
|
|
51
|
+
skipped = arg === "--skipped" ? "analyze" : arg === "--skipped-plan" ? "plan" : "files";
|
|
52
|
+
}
|
|
53
|
+
else if (arg === "--proposal" && proposalFile === undefined) {
|
|
39
54
|
const value = args[++index];
|
|
40
55
|
if (!value || value.startsWith("-"))
|
|
41
56
|
throw new Error();
|
|
42
57
|
proposalFile = value;
|
|
43
58
|
}
|
|
44
|
-
else if (
|
|
45
|
-
|
|
59
|
+
else if ((choices || args[index + 1]?.startsWith("[")) && arg === "--scope" && scope === undefined && scopeFile === undefined) {
|
|
60
|
+
const value = args[++index];
|
|
61
|
+
if (!value || value.startsWith("-"))
|
|
62
|
+
throw new Error();
|
|
63
|
+
if (value.startsWith("["))
|
|
64
|
+
scope = JSON.parse(value);
|
|
65
|
+
else
|
|
66
|
+
scopeFile = value;
|
|
67
|
+
}
|
|
68
|
+
else if (!arg.startsWith("-") && (path === undefined || multiple)) {
|
|
69
|
+
path ??= arg;
|
|
70
|
+
paths.push(arg);
|
|
71
|
+
}
|
|
46
72
|
else
|
|
47
73
|
throw new Error();
|
|
48
74
|
}
|
|
49
|
-
|
|
75
|
+
if (skipped && (paths.length > 1 || proposalFile || scope !== undefined || scopeFile !== undefined))
|
|
76
|
+
throw new Error();
|
|
77
|
+
return { path: path ?? ".", options: { ...(paths.length > 1 ? { paths } : {}), ...(scope === undefined ? {} : { scope }), ...(proposalFile === undefined ? {} : { proposalFile }), ...(scopeFile === undefined ? {} : { scopeFile }), ...(skipped ? { skipped } : {}) } };
|
|
50
78
|
}
|
|
51
79
|
export async function analyze(path, options, ctx) {
|
|
80
|
+
if (Array.isArray(options.scope) && (options.proposal !== undefined || options.proposalFile !== undefined)) {
|
|
81
|
+
throw new ApiError(400, "invalid_body", cliRefusals.invalid_body);
|
|
82
|
+
}
|
|
83
|
+
if (options.skipped)
|
|
84
|
+
return skipped(path, options.skipped, ctx, analyze, options.surface);
|
|
52
85
|
const startup = ctx.startup;
|
|
86
|
+
let settings = startup.settings;
|
|
87
|
+
if (Array.isArray(options.scope)) {
|
|
88
|
+
try {
|
|
89
|
+
if (!folderPartsEnabled(settings))
|
|
90
|
+
throw new Error();
|
|
91
|
+
const scope = fileScopeOf({ ...settings, CF_PART_FILES: JSON.stringify(options.scope) });
|
|
92
|
+
if (!scope?.length)
|
|
93
|
+
throw new Error();
|
|
94
|
+
settings = Object.freeze({ ...settings, CF_PART_FILES: JSON.stringify(scope) });
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
throw new ApiError(400, "invalid_body", cliRefusals.invalid_body);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const scope = Array.isArray(options.scope) ? undefined : options.scope;
|
|
53
101
|
const proposal = await loadAgentProposal(async () => options.proposalFile === undefined ? options.proposal
|
|
54
102
|
: JSON.parse(await readFile(resolve(ctx.cwd ?? process.cwd(), options.proposalFile), "utf8")), startup.settings);
|
|
55
|
-
|
|
103
|
+
const mode = await (ctx.consoleMode?.() ?? ctx.api.request("checks/mode"));
|
|
104
|
+
const capabilities = requireLocal(mode);
|
|
56
105
|
const development = startup.transport.CF_RELAY_DEV === "1" ? await loadDevelopmentRelay(startup.transport) : undefined;
|
|
57
106
|
const release = development?.release ?? ctx.relayRuntime?.release ?? await loadInstalledRelease();
|
|
58
107
|
if (release.projectContext !== "checks")
|
|
@@ -60,7 +109,8 @@ export async function analyze(path, options, ctx) {
|
|
|
60
109
|
const token = await ctx.token?.();
|
|
61
110
|
if (!token)
|
|
62
111
|
throw new ApiError(401, "unauthorized", cliApiMessages.unauthorized);
|
|
63
|
-
const
|
|
112
|
+
const selection = await resolveRunSelection(path, ctx.cwd ?? process.cwd(), settings, { ...options, scope });
|
|
113
|
+
const folder = selection?.path ?? resolveAgentFolder(path, ctx.cwd);
|
|
64
114
|
const config = Object.freeze({ ...startup.transport, CF_RELAY_URL: startup.transport.CF_RELAY_URL ?? release.relayUrl,
|
|
65
115
|
CF_LOCAL_RELAY: "1", CF_CONSOLE_URL: ctx.api.baseUrl, CF_CONSOLE_TOKEN: token });
|
|
66
116
|
const controller = new AbortController();
|
|
@@ -72,13 +122,28 @@ export async function analyze(path, options, ctx) {
|
|
|
72
122
|
ctx.write("\r\u001b[2K");
|
|
73
123
|
progressShown = false;
|
|
74
124
|
} };
|
|
125
|
+
let choice;
|
|
75
126
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
127
|
+
const code = await (ctx.relayRuntime?.run ?? runRelayCheck)(folder, {
|
|
128
|
+
analyzedFolder: options.analyzedFolder ?? path,
|
|
129
|
+
settings: captureArchitectureSettings(settings, config), config, release, skippedFilesSupported: capabilities.skippedFiles,
|
|
130
|
+
consoleSupportsActivity: capabilities.findingActivity, consoleSupportsArchitecture: capabilities.architecture,
|
|
131
|
+
cwd: ctx.cwd, connect: development?.connect ?? ctx.relayRuntime?.connect,
|
|
132
|
+
...(ctx.relayRuntime?.selectionProvider ? { selectionProvider: ctx.relayRuntime.selectionProvider } : {}),
|
|
78
133
|
...(proposal ? { proposal } : {}),
|
|
134
|
+
...(selection ? { selection } : {}), ...(scope ? { scope } : {}),
|
|
135
|
+
...(options.addToAnalysisId ? { addToAnalysisId: options.addToAnalysisId, expectedProjectId: options.expectedProjectId } : {}),
|
|
136
|
+
...(options.retryUpload ? { retryUpload: true } : {}),
|
|
79
137
|
signal: ctx.signal ? AbortSignal.any([controller.signal, ctx.signal]) : controller.signal,
|
|
80
138
|
stdout() { }, stderr() { },
|
|
81
|
-
onStart: () => { ctx.onCheckStart?.(); ctx.write(
|
|
139
|
+
onStart: () => { ctx.onCheckStart?.(); ctx.write(`\r\u001b[2K${progressLine(selection, "terminal")}`); progressShown = true; },
|
|
140
|
+
...(scopeChoicesEnabled(startup.settings) ? { onScopeChoice: (value) => {
|
|
141
|
+
choice = value;
|
|
142
|
+
clearProgress();
|
|
143
|
+
ctx.onScopeChoice?.(value);
|
|
144
|
+
} } : {}),
|
|
145
|
+
onNotice: message => { clearProgress(); ctx.write(`${message}\n`); },
|
|
146
|
+
onSaving: () => { clearProgress(); ctx.write("\r\u001b[2KSaving the results…"); progressShown = true; },
|
|
82
147
|
onLocalSaved: result => {
|
|
83
148
|
if (result.ignoreAdded) {
|
|
84
149
|
clearProgress();
|
|
@@ -95,10 +160,13 @@ export async function analyze(path, options, ctx) {
|
|
|
95
160
|
onSaved: result => {
|
|
96
161
|
clearProgress();
|
|
97
162
|
ctx.onCheckComplete?.(result);
|
|
98
|
-
|
|
99
|
-
|
|
163
|
+
if (!options.deferResults)
|
|
164
|
+
ctx.write(formatSavedResults(result, settings, ctx.findingNumbers, !options.addToAnalysisId, { agentReadableResults: ctx.findingSettings.agentReadableResults, surface: options.surface, analyzedFolder: options.analyzedFolder ?? path }));
|
|
100
165
|
},
|
|
101
166
|
});
|
|
167
|
+
if (choice && !ctx.onScopeChoice && selection)
|
|
168
|
+
ctx.write(await renderScopeChoices(choice, selection));
|
|
169
|
+
return code;
|
|
102
170
|
}
|
|
103
171
|
finally {
|
|
104
172
|
clearProgress();
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { followupSettings, skippedFilesEnabled } from "../../analysis-core/settings.js";
|
|
2
|
+
import { loadAnalysisResults } from "../../adapters/analysis-results.js";
|
|
3
|
+
import { readLocalFolder } from "../../adapters/files.js";
|
|
4
|
+
import { loadCompletion } from "../../adapters/completion.js";
|
|
5
|
+
import { sizeSkippedFiles } from "../../analysis-core/analysis/code-agents/skipped-files.js";
|
|
6
|
+
import { followupPlanText } from "../skipped-files.js";
|
|
7
|
+
import { resolveAgentFolder } from "../agent-folder.js";
|
|
8
|
+
import { formatSavedResults } from "../saved-results.js";
|
|
9
|
+
export async function skipped(path, action, ctx, run, surface) {
|
|
10
|
+
if (!skippedFilesEnabled(ctx.startup.settings))
|
|
11
|
+
throw new Error("Skipped-file analyses are not enabled.");
|
|
12
|
+
const folder = resolveAgentFolder(path, ctx.cwd);
|
|
13
|
+
let latest;
|
|
14
|
+
let mode;
|
|
15
|
+
const deferred = { ...ctx, consoleMode: ctx.consoleMode ?? (() => mode ??= ctx.api.request("checks/mode")),
|
|
16
|
+
onCheckComplete: (result) => { latest = result; } };
|
|
17
|
+
const publish = () => {
|
|
18
|
+
if (!latest)
|
|
19
|
+
return;
|
|
20
|
+
ctx.onCheckComplete?.(latest);
|
|
21
|
+
ctx.write(formatSavedResults(latest, ctx.startup.settings, ctx.findingNumbers, false, { agentReadableResults: ctx.findingSettings.agentReadableResults, surface }));
|
|
22
|
+
};
|
|
23
|
+
// Finish an already completed upload before planning; its reading must not be purchased again.
|
|
24
|
+
const pending = await loadCompletion(folder);
|
|
25
|
+
const unsettled = pending?.uploaded ? await loadAnalysisResults(folder).then(saved => saved.context.pending?.checkId === pending.payload.checkId).catch(() => false) : false;
|
|
26
|
+
if (pending && !pending.finished && (!pending.uploaded || unsettled)) {
|
|
27
|
+
const recovered = await run(folder, { retryUpload: true, deferResults: true, surface }, deferred);
|
|
28
|
+
if (recovered !== 0) {
|
|
29
|
+
if (action === "analyze")
|
|
30
|
+
publish();
|
|
31
|
+
return recovered;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
let saved;
|
|
35
|
+
try {
|
|
36
|
+
saved = await loadAnalysisResults(folder);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new Error("The saved analysis is missing or unreadable. Run an analysis of this folder first.");
|
|
40
|
+
}
|
|
41
|
+
const paths = sizeSkippedFiles(saved.report.refusedFiles).map(file => file.path);
|
|
42
|
+
if (action === "files") {
|
|
43
|
+
ctx.write(paths.length ? `${paths.join("\n")}\n` : "No skipped files remain to analyze.\n");
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
const settings = followupSettings(ctx.startup.settings, saved.context.settings);
|
|
47
|
+
const source = await readLocalFolder(settings, folder, undefined, { planSkipped: paths });
|
|
48
|
+
const plan = source.filePlan;
|
|
49
|
+
ctx.write(followupPlanText(plan));
|
|
50
|
+
if (action === "plan")
|
|
51
|
+
return 0;
|
|
52
|
+
// Groups share the same local result files; save one completely before starting the next.
|
|
53
|
+
for (const group of plan.groups) {
|
|
54
|
+
ctx.signal?.throwIfAborted();
|
|
55
|
+
const scoped = { ...deferred,
|
|
56
|
+
startup: { ...ctx.startup, settings: Object.freeze({ ...settings, CF_PART_FILES: JSON.stringify(group.files) }) } };
|
|
57
|
+
const code = await run(folder, { deferResults: true, surface, addToAnalysisId: saved.context.analysisId, expectedProjectId: saved.context.projectId }, scoped);
|
|
58
|
+
if (code !== 0) {
|
|
59
|
+
publish();
|
|
60
|
+
return code;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
publish();
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { OutputPathError } from "../adapters/output-paths.js";
|
|
2
|
+
import { FINISH_FAILURE_MESSAGE } from "../contract/cli-refusals.js";
|
|
3
|
+
import { completionRecoveryDecision, observedCompletionTiming } from "./completion-recovery.js";
|
|
4
|
+
import { ResultsUploadError } from "./results-upload.js";
|
|
5
|
+
/** Keep delivery ahead of report writes, including when private storage is unavailable. */
|
|
6
|
+
export async function deliverCompletedResults(operations) {
|
|
7
|
+
const result = { uploaded: false };
|
|
8
|
+
let storageError, uploadError;
|
|
9
|
+
try {
|
|
10
|
+
await operations.persist(false);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
storageError = error;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
result.saved = await operations.submit();
|
|
17
|
+
result.uploaded = true;
|
|
18
|
+
try {
|
|
19
|
+
await operations.persist(true);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
storageError ??= error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
uploadError = error;
|
|
27
|
+
if (error instanceof ResultsUploadError) {
|
|
28
|
+
result.outcome = error.outcome;
|
|
29
|
+
result.timing = error.timing;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
result.paths = await operations.saveReports();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
result.localErrorMessage = "The local report couldn't be written.";
|
|
37
|
+
}
|
|
38
|
+
if (storageError) {
|
|
39
|
+
const message = storageError instanceof OutputPathError ? storageError.message : FINISH_FAILURE_MESSAGE;
|
|
40
|
+
result.localErrorMessage = result.localErrorMessage ? `${message}\n${result.localErrorMessage}` : message;
|
|
41
|
+
}
|
|
42
|
+
result.error = storageError ?? uploadError ?? (result.localErrorMessage ? new Error(result.localErrorMessage) : undefined);
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
/** Cleanup consumes the same decision as upload and writes only while the runner owns the folder. */
|
|
46
|
+
export async function recoverCompletedResults(input, operations) {
|
|
47
|
+
const timing = observedCompletionTiming(input.timing ?? {}, input.statusLookup, input.payload);
|
|
48
|
+
const decision = completionRecoveryDecision({ ...input, timing });
|
|
49
|
+
if (input.canWrite) {
|
|
50
|
+
try {
|
|
51
|
+
if (decision.action === "saved")
|
|
52
|
+
await operations.markUploaded();
|
|
53
|
+
else if (decision.action === "give-up")
|
|
54
|
+
await operations.retire();
|
|
55
|
+
else if (decision.action === "retry" && input.retryEnabled && input.completionStored)
|
|
56
|
+
await operations.keepPending?.(timing);
|
|
57
|
+
}
|
|
58
|
+
catch { /* A local write cannot change a confirmed server outcome. */ }
|
|
59
|
+
}
|
|
60
|
+
const recoveryMessage = "message" in decision && (input.completionStored || decision.action === "give-up") ? decision.message : undefined;
|
|
61
|
+
const message = input.localErrorMessage && recoveryMessage ? `${input.localErrorMessage}\n${recoveryMessage}` : recoveryMessage ?? input.localErrorMessage;
|
|
62
|
+
return { decision, message };
|
|
63
|
+
}
|
|
@@ -1,18 +1,72 @@
|
|
|
1
|
-
|
|
1
|
+
import { cliRefusals, NOT_ALLOWED_MESSAGE, SIGN_IN_EXPIRED_MESSAGE, CHECK_CONFLICT_MESSAGE, PENDING_RESULTS_MESSAGE, UNSAVED_RESULTS_MESSAGE } from "../contract/cli-refusals.js";
|
|
2
|
+
export const OTHER_SETUP_RESULTS_MESSAGE = "A saved result from another Counterfact setup was left untouched.";
|
|
3
|
+
export const COMPLETION_SETTINGS_MISMATCH_MESSAGE = "This folder has a saved result for a different Counterfact project or address. Run the analysis again with the original settings to save it.";
|
|
2
4
|
export const matchesCheck = (status, identity) => status.checkId === identity.checkId && status.projectId === identity.projectId;
|
|
5
|
+
export function observedCompletionTiming(timing, lookup, payload) {
|
|
6
|
+
const status = lookup?.kind === "found" ? lookup.status : undefined;
|
|
7
|
+
if (!status || !payload || !matchesCheck(status, payload) || !status.absoluteExpiresAt)
|
|
8
|
+
return timing;
|
|
9
|
+
return { ...timing, absoluteExpiresAt: Math.min(timing.absoluteExpiresAt ?? Infinity, Date.parse(status.absoluteExpiresAt)) };
|
|
10
|
+
}
|
|
11
|
+
export function completionDeadline(timing, lookup, payload, outcome) {
|
|
12
|
+
const observed = observedCompletionTiming(timing, lookup, payload);
|
|
13
|
+
const reachable = lookup?.kind === "found" || lookup?.kind === "failed" && lookup.httpStatus !== undefined
|
|
14
|
+
|| outcome && "httpStatus" in outcome && outcome.httpStatus !== undefined;
|
|
15
|
+
return Math.min(observed.completedAt === undefined ? Infinity : observed.completedAt + 2 * 60 * 60_000, reachable ? Infinity : observed.absoluteExpiresAt ?? Infinity);
|
|
16
|
+
}
|
|
17
|
+
export function completionRetryDeadline(timing, lookup, payload, outcome) {
|
|
18
|
+
return Math.min(completionDeadline(timing, lookup, payload, outcome), timing.retryStartedAt === undefined ? Infinity : timing.retryStartedAt + 10 * 60_000);
|
|
19
|
+
}
|
|
20
|
+
/** HTTP status owns certainty; proxy text and unfamiliar server codes cannot discard paid work. */
|
|
3
21
|
export function completionRecoveryDecision(input) {
|
|
22
|
+
const { statusLookup, uploadOutcome, payload, retryEnabled, timing = {}, now = Date.now() } = input;
|
|
23
|
+
const uploadSaved = uploadOutcome.kind === "saved" || uploadOutcome.kind === "decided" && uploadOutcome.decision.action === "saved";
|
|
4
24
|
if (input.localMismatch)
|
|
5
|
-
return {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
if (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
25
|
+
return !retryEnabled ? { action: "retry", automatic: false, message: COMPLETION_SETTINGS_MISMATCH_MESSAGE }
|
|
26
|
+
: !uploadSaved && now >= completionDeadline(timing) ? { action: "give-up" }
|
|
27
|
+
: { action: "leave", message: OTHER_SETUP_RESULTS_MESSAGE };
|
|
28
|
+
const status = statusLookup.kind === "found" ? statusLookup.status : undefined;
|
|
29
|
+
const matching = status && matchesCheck(status, payload);
|
|
30
|
+
if (uploadSaved
|
|
31
|
+
|| retryEnabled && uploadOutcome.kind === "refused" && uploadOutcome.httpStatus !== undefined && uploadOutcome.httpStatus >= 200 && uploadOutcome.httpStatus < 300
|
|
32
|
+
|| matching && status.uploadState === "saved")
|
|
33
|
+
return { action: "saved" };
|
|
34
|
+
// With retry off, trusted terminal status retires unsaved work using the original refusal.
|
|
35
|
+
if (!retryEnabled)
|
|
36
|
+
return matching && status.uploadState === "missing" && ["closed", "expired"].includes(status.state)
|
|
37
|
+
? { action: "give-up" } : { action: "retry", automatic: false };
|
|
38
|
+
if (uploadOutcome.kind === "decided" && uploadOutcome.decision.action === "give-up")
|
|
39
|
+
return uploadOutcome.decision;
|
|
40
|
+
if (status && !matching)
|
|
41
|
+
return { action: "give-up", code: "check_conflict", message: CHECK_CONFLICT_MESSAGE };
|
|
42
|
+
if (now >= completionDeadline(timing, statusLookup, payload, uploadOutcome)) {
|
|
43
|
+
return { action: "give-up", message: UNSAVED_RESULTS_MESSAGE };
|
|
44
|
+
}
|
|
45
|
+
const refusals = [uploadOutcome.kind === "refused" ? uploadOutcome : undefined]
|
|
46
|
+
.filter(value => value !== undefined);
|
|
47
|
+
const uncertain = ({ code, httpStatus, transportFailure }) => transportFailure || (httpStatus === undefined
|
|
48
|
+
? code === "unavailable"
|
|
49
|
+
: httpStatus === 408 || httpStatus === 429 || httpStatus >= 500 && httpStatus <= 599 || httpStatus === 409 && code === "reservation_closing"
|
|
50
|
+
|| httpStatus < 400 || httpStatus >= 600);
|
|
51
|
+
const refusal = refusals.find(value => !uncertain(value));
|
|
52
|
+
if (refusal) {
|
|
53
|
+
// Guidance is explicit per code; copy edits never decide whether results survive.
|
|
54
|
+
const messages = {
|
|
55
|
+
check_conflict: CHECK_CONFLICT_MESSAGE,
|
|
56
|
+
not_allowed: NOT_ALLOWED_MESSAGE,
|
|
57
|
+
unauthorized: SIGN_IN_EXPIRED_MESSAGE,
|
|
58
|
+
invalid_body: cliRefusals.invalid_body,
|
|
59
|
+
cli_update_required: cliRefusals.cli_update_required,
|
|
60
|
+
local_checks_paused: cliRefusals.local_checks_paused,
|
|
61
|
+
};
|
|
62
|
+
return { action: "give-up", code: refusal.code, message: Object.hasOwn(messages, refusal.code)
|
|
63
|
+
? refusal.code === "check_conflict" ? messages[refusal.code] : `${messages[refusal.code]}\n${UNSAVED_RESULTS_MESSAGE}`
|
|
64
|
+
: UNSAVED_RESULTS_MESSAGE };
|
|
65
|
+
}
|
|
66
|
+
if (matching && status.uploadState === "missing" && status.settlement === "released")
|
|
67
|
+
return { action: "give-up",
|
|
68
|
+
code: status.state === "expired" ? "reservation_expired" : "reservation_closed", message: UNSAVED_RESULTS_MESSAGE };
|
|
69
|
+
if (uploadOutcome.kind === "decided")
|
|
70
|
+
return uploadOutcome.decision;
|
|
71
|
+
return { action: "retry", automatic: now < completionRetryDeadline(timing, statusLookup, payload, uploadOutcome) && refusals.some(value => uncertain(value)), message: PENDING_RESULTS_MESSAGE };
|
|
18
72
|
}
|
package/dist/cli/context.js
CHANGED
|
@@ -8,18 +8,20 @@ import { userEmail } from "./git.js";
|
|
|
8
8
|
import { terminalSignInInput } from "./sign-in-input.js";
|
|
9
9
|
import { readFindingSettings } from "./finding-settings.js";
|
|
10
10
|
export function commandContext(options = {}) {
|
|
11
|
-
const startup = readCliSettings(options.env, options.emailSignIn);
|
|
11
|
+
const startup = options.startup ?? readCliSettings(options.env, options.emailSignIn);
|
|
12
12
|
const homeDir = options.homeDir ?? homedir();
|
|
13
13
|
const baseUrl = (options.baseUrl ?? startup.baseUrl ?? DEFAULT_URL).replace(/\/+$/, "");
|
|
14
|
-
const api = new ApiClient(baseUrl, () => readCredentials(homeDir, api.baseUrl), undefined, options.signal);
|
|
14
|
+
const api = new ApiClient(baseUrl, () => readCredentials(homeDir, api.baseUrl), undefined, options.signal, startup.transport.CF_CLI_TRANSPORT_ERRORS !== "0");
|
|
15
15
|
const write = options.write ?? (text => { process.stdout.write(text); });
|
|
16
16
|
const findingSettings = options.findingSettings ?? readFindingSettings(options.env);
|
|
17
17
|
return {
|
|
18
18
|
findingNumbers: findingSettings.numbersEnabled,
|
|
19
|
+
findingSettings,
|
|
19
20
|
signInRetry: (options.env ?? process.env).CF_SIGN_IN_RETRY !== "0",
|
|
20
21
|
startup, signal: options.signal, relayRuntime: options.relayRuntime, cwd: options.cwd ?? process.cwd(),
|
|
21
22
|
token: () => readCredentials(homeDir, api.baseUrl), onCheckComplete: options.onCheckComplete, onCheckStart: options.onCheckStart, onAgentChoice: options.onAgentChoice,
|
|
22
23
|
onCheckFailure: options.onCheckFailure,
|
|
24
|
+
onScopeChoice: options.onScopeChoice,
|
|
23
25
|
api, homeDir, write, openBrowser: options.openBrowser ?? openBrowser,
|
|
24
26
|
signInInput: options.signInInput ?? terminalSignInInput(), readEmail: options.readEmail ?? userEmail,
|
|
25
27
|
sleep: options.sleep ?? (async (ms) => { await delay(ms, undefined, { signal: options.signal }); }), now: options.now ?? Date.now,
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
import { groupByActivity, groupByAudience } from "../analysis-core/analysis/schemas.js";
|
|
2
3
|
export const TEAM_WORKFLOW_HEADING = "Team workflow";
|
|
4
|
+
export const notActiveCountLine = (count) => `${count} more ${count === 1 ? "finding is" : "findings are"} about code that doesn't run today (not active)`;
|
|
5
|
+
export const activityAdvice = (activity) => activity.state === "dead" ? "Remove it (cleanup)."
|
|
6
|
+
: `Fix this before turning on ${activity.gate}, or remove it if you don't plan to use it.`;
|
|
3
7
|
export const formatFindingNumber = (seq) => `F-${seq}`;
|
|
4
8
|
/** Intake returns one id per payload finding in order; the optional number mapping is joined by id. */
|
|
5
9
|
export function savedFindings(result, report) {
|
|
@@ -7,28 +11,119 @@ export function savedFindings(result, report) {
|
|
|
7
11
|
return result.findingIds.map((id, index) => {
|
|
8
12
|
const numbered = numbers.get(id), local = report[index];
|
|
9
13
|
return { id, ...(numbered ? { seq: numbered.seq, number: formatFindingNumber(numbered.seq) } : {}),
|
|
10
|
-
title: local?.title ?? "", ...(local?.audience === undefined ? {} : { audience: local.audience })
|
|
14
|
+
title: local?.title ?? "", ...(local?.audience === undefined ? {} : { audience: local.audience }),
|
|
15
|
+
...(local?.activity === undefined ? {} : { activity: local.activity }),
|
|
16
|
+
...(local?.activity === undefined || local.fixes === undefined ? {} : { fixes: local.fixes.map(({ title, approach }) => ({ title: title ?? "", approach: approach ?? "" })) }),
|
|
17
|
+
...(local?.severity === undefined ? {} : { severity: local.severity }),
|
|
18
|
+
...(local?.site === undefined ? {} : { site: local.site }),
|
|
19
|
+
...(local?.fixes === undefined ? {} : { shortFix: shortFix(local.fixes[0]) }) };
|
|
11
20
|
});
|
|
12
21
|
}
|
|
22
|
+
function findingLabel(value, enabled, index, labels) {
|
|
23
|
+
return labels === "position" ? `${index + 1}.` : enabled && value.seq !== undefined ? formatFindingNumber(value.seq) : value.id ?? `${index + 1}.`;
|
|
24
|
+
}
|
|
13
25
|
function findingLine(value, enabled, index, labels, audienceEnabled) {
|
|
14
26
|
// The off switch preserves the original Markdown summary, including whitespace.
|
|
15
27
|
if (labels === "position" && !audienceEnabled)
|
|
16
28
|
return `${index + 1}. ${value.title}`;
|
|
17
29
|
const title = value.title.replace(/[\r\n\u2028\u2029]+/g, " ");
|
|
18
|
-
const label =
|
|
30
|
+
const label = findingLabel(value, enabled, index, labels);
|
|
19
31
|
return `${label}${title ? ` ${title}` : ""}`;
|
|
20
32
|
}
|
|
21
|
-
export function findingDisplayRows(findings, { numbersEnabled = true, audienceEnabled, labels }) {
|
|
22
|
-
const
|
|
33
|
+
export function findingDisplayRows(findings, { numbersEnabled = true, audienceEnabled, notActiveEnabled = false, labels }) {
|
|
34
|
+
const { active } = groupByActivity(findings, notActiveEnabled);
|
|
35
|
+
const groups = audienceEnabled ? groupByAudience(active) : { users: active, team: [] };
|
|
23
36
|
return [...groups.users, ...groups.team].map((finding, index) => ({ finding,
|
|
24
37
|
label: findingLine(finding, numbersEnabled, index, labels, audienceEnabled), startsTeamGroup: groups.team.length > 0 && index === groups.users.length }));
|
|
25
38
|
}
|
|
26
|
-
|
|
39
|
+
function legacyFindingList(result, options) {
|
|
40
|
+
const groups = groupByActivity(result.findings ?? [], options.notActiveEnabled === true);
|
|
41
|
+
const collapsed = groups.notActive.length ? notActiveCountLine(groups.notActive.length) + "\n" : "";
|
|
27
42
|
if (options.numbersEnabled !== false && result.findings?.some(value => value.seq !== undefined)) {
|
|
28
|
-
return "Findings:\n" + findingDisplayRows(result.findings, { ...options, labels: "reference" }).map(row => `${row.startsTeamGroup ? `${TEAM_WORKFLOW_HEADING}:\n` : ""}${row.label}\n`).join("");
|
|
43
|
+
return "Findings:\n" + findingDisplayRows(result.findings, { ...options, labels: "reference" }).map(row => `${row.startsTeamGroup ? `${TEAM_WORKFLOW_HEADING}:\n` : ""}${row.label}\n`).join("") + collapsed;
|
|
29
44
|
}
|
|
45
|
+
if (collapsed)
|
|
46
|
+
return (groups.active.length ? `Finding ids: ${groups.active.map(f => f.id).join(", ")}\n` : "") + collapsed;
|
|
30
47
|
return result.findingIds?.length ? `Finding ids: ${result.findingIds.join(", ")}\n` : "";
|
|
31
48
|
}
|
|
49
|
+
export function formatFindingActivity(activity) {
|
|
50
|
+
return `${activity.state === "dead" ? "Dead" : "Switched off"}: ${activity.reason}\n${activityAdvice(activity)}\n`;
|
|
51
|
+
}
|
|
52
|
+
// This is a presentation budget, separate from the size of the saved report.
|
|
53
|
+
export const AGENT_FINDING_LIST_BUDGET = 12_000;
|
|
54
|
+
function shortFix(fix) {
|
|
55
|
+
return [fix?.summary, fix?.title, fix?.approach].find(text => text?.trim()) ?? "";
|
|
56
|
+
}
|
|
57
|
+
function compactText(value, limit) {
|
|
58
|
+
const text = stripVTControlCharacters(value).replace(/[\p{Cc}\s]+/gu, " ").replace(/\s*\u2014\s*/g, ", ").trim();
|
|
59
|
+
if (text.length <= limit)
|
|
60
|
+
return text;
|
|
61
|
+
// Avoid ending on half of a supplementary Unicode character.
|
|
62
|
+
return Array.from(text.slice(0, limit - 3)).filter(char => !/^[\ud800-\udbff]$/.test(char)).join("") + "...";
|
|
63
|
+
}
|
|
64
|
+
function sentence(text) { return /[.!?]$/.test(text) ? text : `${text}.`; }
|
|
65
|
+
function compactFinding(value) {
|
|
66
|
+
const compact = legacyFinding(value);
|
|
67
|
+
// Full fixes remain in saved detail; compact lists use only the existing short fix.
|
|
68
|
+
delete compact.fixes;
|
|
69
|
+
return { ...compact, title: compactText(value.title, 160) || "Untitled finding",
|
|
70
|
+
severity: compactText(value.severity ?? "", 24) || "severity unavailable",
|
|
71
|
+
...(value.site ? { site: { path: compactText(value.site.path, 240), line: value.site.line } } : {}),
|
|
72
|
+
shortFix: compactText(value.shortFix ?? "", 240) || "No suggested fix available." };
|
|
73
|
+
}
|
|
74
|
+
function legacyFinding(value) {
|
|
75
|
+
const original = { ...value };
|
|
76
|
+
delete original.severity;
|
|
77
|
+
delete original.site;
|
|
78
|
+
delete original.shortFix;
|
|
79
|
+
return original;
|
|
80
|
+
}
|
|
81
|
+
export function findingDetailReference(value, projectId) {
|
|
82
|
+
return { id: value.seq !== undefined ? formatFindingNumber(value.seq) : value.id, ...(projectId ? { projectId } : {}) };
|
|
83
|
+
}
|
|
84
|
+
export function formatFindingContinuation(next) {
|
|
85
|
+
return `Next finding: get_finding(${JSON.stringify(next)})\n`;
|
|
86
|
+
}
|
|
87
|
+
export function presentSavedFindings(result, options) {
|
|
88
|
+
const rows = findingDisplayRows(result.findings ?? [], { ...options, labels: "reference" });
|
|
89
|
+
const orderedReferences = rows.flatMap(({ finding }) => finding.id ? [{ id: finding.id, seq: finding.seq }] : []);
|
|
90
|
+
if (options.agentReadableResults === false || options.numbersEnabled === false || !rows.length) {
|
|
91
|
+
return { text: legacyFindingList(result, options), findings: rows.map(row => legacyFinding(row.finding)),
|
|
92
|
+
findingIds: result.findingIds ?? [], nextFinding: undefined, orderedReferences };
|
|
93
|
+
}
|
|
94
|
+
const { notActive } = groupByActivity(result.findings ?? [], options.notActiveEnabled === true);
|
|
95
|
+
const collapsed = notActive.length ? notActiveCountLine(notActive.length) + "\n" : "";
|
|
96
|
+
let text = "Findings:\n";
|
|
97
|
+
const findings = [];
|
|
98
|
+
let nextFinding;
|
|
99
|
+
for (const [index, row] of rows.entries()) {
|
|
100
|
+
const value = compactFinding(row.finding);
|
|
101
|
+
const label = findingLabel(value, true, index, "reference");
|
|
102
|
+
const site = value.site ? `${value.site.path}:${value.site.line ?? "?"}` : "File unavailable";
|
|
103
|
+
const line = `${row.startsTeamGroup ? `${TEAM_WORKFLOW_HEADING}:\n` : ""}${label} (${value.severity}) ${sentence(value.title)} ${site}. Fix: ${value.shortFix}\n`;
|
|
104
|
+
const next = rows[index + 1]?.finding;
|
|
105
|
+
const footer = next ? overflowLine(rows.length - index - 1, next, options) : "";
|
|
106
|
+
const candidate = { text: text + line + footer + collapsed, findings: [...findings, value],
|
|
107
|
+
findingIds: [...findings, value].flatMap(finding => finding.id ? [finding.id] : []),
|
|
108
|
+
...(next ? { nextFinding: findingDetailReference(next, options.projectId) } : {}) };
|
|
109
|
+
if (JSON.stringify(candidate).length > AGENT_FINDING_LIST_BUDGET) {
|
|
110
|
+
nextFinding = findingDetailReference(row.finding, options.projectId);
|
|
111
|
+
text += overflowLine(rows.length - index, row.finding, options);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
text += line;
|
|
115
|
+
findings.push(value);
|
|
116
|
+
}
|
|
117
|
+
return { text: text + collapsed, findings, findingIds: findings.flatMap(value => value.id ? [value.id] : []), nextFinding, orderedReferences };
|
|
118
|
+
}
|
|
119
|
+
function overflowLine(count, next, options) {
|
|
120
|
+
const guidance = options.surface === "mcp" ? `Next: get_finding(${JSON.stringify(findingDetailReference(next, options.projectId))}); follow nextFinding for the rest.`
|
|
121
|
+
: `Run counterfact-labs finding ${next.id} for the next one, or open Full results below.`;
|
|
122
|
+
return `${count} more ${count === 1 ? "finding" : "findings"}. ${guidance}\n`;
|
|
123
|
+
}
|
|
124
|
+
export function formatSavedFindings(result, options) {
|
|
125
|
+
return presentSavedFindings(result, options).text;
|
|
126
|
+
}
|
|
32
127
|
export function formatWaitingFixes(items, enabled, needsProject) {
|
|
33
128
|
if (!items.length)
|
|
34
129
|
return "No waiting fixes.";
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { findingAudienceEnabled } from "../analysis-core/analysis/finding-audience-switch.js";
|
|
2
|
+
import { notActiveEnabled } from "../analysis-core/analysis/finding-activity-switch.js";
|
|
2
3
|
export function readFindingSettings(env = process.env, bundle) {
|
|
3
4
|
return { numbersEnabled: env.CF_FINDING_NUMBERS !== "0", fixesEnabled: env.CF_WAITING_FIXES !== "0",
|
|
4
|
-
audienceEnabled: findingAudienceEnabled(env, bundle)
|
|
5
|
+
audienceEnabled: findingAudienceEnabled(env, bundle), notActiveEnabled: notActiveEnabled(env),
|
|
6
|
+
agentReadableResults: env.CF_AGENT_READABLE_RESULTS !== "0" };
|
|
5
7
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { skippedFilesEnabled } from "../analysis-core/settings.js";
|
|
2
|
+
import { fingerprintOf } from "../analysis-core/analysis/finding-identity.js";
|
|
1
3
|
import { resultSafetyCheckEnabled } from "../analysis-core/settings.js";
|
|
2
4
|
import { localFindingsPayloadSchema } from "../contract/console.js";
|
|
3
5
|
import { projectPublicFinding } from "../contract/public-report.js";
|
|
4
6
|
import { checkIntakeFindings } from "./result-check.js";
|
|
5
7
|
import { RESULTS_TOO_LARGE_MESSAGE } from "../contract/cli-refusals.js";
|
|
8
|
+
import { projectArchitecture } from "../contract/project-architecture.js";
|
|
6
9
|
export function buildIntakePayload(identity, agent, report, options = {}) {
|
|
7
10
|
const repo = (key) => key === null || key === report.repoKey ? agent.repository : key;
|
|
8
11
|
let findings = report.findings.map(finding => projectPublicFinding(finding, repo));
|
|
@@ -19,8 +22,29 @@ export function buildIntakePayload(identity, agent, report, options = {}) {
|
|
|
19
22
|
resultCheck = counts;
|
|
20
23
|
findingIndexes = acceptedIndexes;
|
|
21
24
|
}
|
|
22
|
-
const
|
|
25
|
+
const metadata = !skippedFilesEnabled(options.settings ?? {}) ? {} : {
|
|
26
|
+
...(options.coverage ? { coverage: options.coverage } : {}),
|
|
27
|
+
...(options.addToAnalysisId ? { addToAnalysisId: options.addToAnalysisId } : {}),
|
|
28
|
+
...(options.findingAssessments ? { findingAssessments: findingIndexes.flatMap((index, at) => {
|
|
29
|
+
const assessment = options.findingAssessments.find(row => row.fingerprint === fingerprintOf(report.findings[index]));
|
|
30
|
+
return assessment ? [{ ...assessment, fingerprint: fingerprintOf(findings[at]) }] : [];
|
|
31
|
+
}) } : {}),
|
|
32
|
+
};
|
|
33
|
+
const findingActivities = findings.map(finding => finding.activity);
|
|
34
|
+
// Older consoles use a strict schema. Local grouping survives without sending an unsupported field.
|
|
35
|
+
if (options.consoleSupportsActivity !== true)
|
|
36
|
+
findings = findings.map(finding => {
|
|
37
|
+
const legacy = { ...finding };
|
|
38
|
+
delete legacy.activity;
|
|
39
|
+
return legacy;
|
|
40
|
+
});
|
|
41
|
+
const architecture = options.consoleSupportsArchitecture === true && options.settings?.CF_LOCAL_ARCHITECTURE === "1"
|
|
42
|
+
? projectArchitecture(report.graph) : undefined;
|
|
43
|
+
let result = localFindingsPayloadSchema.safeParse({ ...identity, agent, findings, ...metadata, ...(architecture ? { architecture } : {}) });
|
|
44
|
+
// Optional map metadata must not crowd otherwise valid findings out of the total upload allowance.
|
|
45
|
+
if (!result.success && architecture)
|
|
46
|
+
result = localFindingsPayloadSchema.safeParse({ ...identity, agent, findings, ...metadata });
|
|
23
47
|
if (!result.success)
|
|
24
48
|
throw new Error(RESULTS_TOO_LARGE_MESSAGE);
|
|
25
|
-
return { payload: result.data, resultCheck, findingIndexes };
|
|
49
|
+
return { payload: result.data, resultCheck, findingIndexes, findingActivities };
|
|
26
50
|
}
|