@counterfact-labs/cli 0.2.9 → 0.2.11
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 +4 -5
- package/bundle/client-release.json +1 -1
- package/dist/adapters/analysis-results.js +10 -5
- package/dist/adapters/completion.js +75 -41
- package/dist/adapters/config-directory.js +5 -0
- package/dist/adapters/files.js +25 -10
- package/dist/adapters/legacy-reports.js +48 -0
- package/dist/adapters/output-paths.js +85 -5
- package/dist/adapters/relay-plan.js +7 -5
- package/dist/adapters/result-receipt.js +78 -0
- package/dist/adapters/results.js +5 -8
- package/dist/adapters/settings.js +10 -4
- package/dist/adapters/storage-identity.js +27 -0
- package/dist/analysis-core/analysis/code-agents/skipped-files.js +1 -1
- package/dist/analysis-core/analysis/file-analysis-plan.js +1 -1
- package/dist/analysis-core/analysis/file-dependencies.js +1 -1
- package/dist/analysis-core/analysis/finding-quality/policy.js +1 -1
- package/dist/analysis-core/analysis/folder-analysis-plan.js +1 -1
- package/dist/analysis-core/analysis/passes/full-file-passes.js +1 -1
- package/dist/analysis-core/analysis/project-architecture.js +1 -1
- package/dist/analysis-core/analysis/schemas.js +1 -1
- package/dist/analysis-core/chunks/{5B5UCWD3.js → 2IQN26TC.js} +1 -1
- package/dist/analysis-core/chunks/{MZOKZ3N6.js → 2J3PWUPG.js} +1 -1
- package/dist/analysis-core/chunks/33EUZF25.js +1 -0
- package/dist/analysis-core/chunks/{3GUKUEEJ.js → 5JG3ZRNJ.js} +1 -1
- package/dist/analysis-core/chunks/{FRXOQ2M6.js → 6E4K7JTI.js} +1 -1
- package/dist/analysis-core/chunks/{4RXRAB5S.js → 6JTHFDH3.js} +1 -1
- package/dist/analysis-core/chunks/{4UDQBDDE.js → 6W6B7VSP.js} +1 -1
- package/dist/analysis-core/chunks/{4PT5C4IT.js → AIS3FHQR.js} +7 -7
- package/dist/analysis-core/chunks/CNQZLSAP.js +1 -0
- package/dist/analysis-core/chunks/{YWL52FVW.js → EJ2IZUN2.js} +2 -2
- package/dist/analysis-core/chunks/{LOBIDUCA.js → GOBOE2SC.js} +1 -1
- package/dist/analysis-core/chunks/GYXDN5W4.js +1 -0
- package/dist/analysis-core/chunks/GZSUTDGX.js +1 -0
- package/dist/analysis-core/chunks/HB5BTGTI.js +6 -0
- package/dist/analysis-core/chunks/HH3HDT5K.js +73 -0
- package/dist/analysis-core/chunks/I4F56VMG.js +1 -0
- package/dist/analysis-core/chunks/{RQTNTIR4.js → I5F7S63K.js} +1 -1
- package/dist/analysis-core/chunks/ICJV6KIZ.js +11 -0
- package/dist/analysis-core/chunks/JAPVLU4Z.js +1 -0
- package/dist/analysis-core/chunks/{33IGHOYT.js → JCVB6IC4.js} +1 -1
- package/dist/analysis-core/chunks/{33HRXJ5Q.js → JLW44GIU.js} +1 -1
- package/dist/analysis-core/chunks/JS6HTVYX.js +1 -0
- package/dist/analysis-core/chunks/{JGK37AWN.js → JWQXG5RB.js} +1 -1
- package/dist/analysis-core/chunks/LOODGUMW.js +1 -0
- package/dist/analysis-core/chunks/{HVQJKPQ4.js → NWYNJHY4.js} +1 -1
- package/dist/analysis-core/chunks/{HHFDNY6V.js → OHDC3L6V.js} +1 -1
- package/dist/analysis-core/chunks/OXGKALKM.js +1 -0
- package/dist/analysis-core/chunks/{MJGB25S5.js → P5FCHJ7W.js} +1 -1
- package/dist/analysis-core/chunks/PISOGSJF.js +1 -0
- package/dist/analysis-core/chunks/{CTLHKFPH.js → QENYFKG3.js} +8 -8
- package/dist/analysis-core/chunks/{2YREYGOG.js → QY57UR6E.js} +1 -1
- package/dist/analysis-core/chunks/QYYN4YKB.js +35 -0
- package/dist/analysis-core/chunks/UYG422QR.js +0 -0
- package/dist/analysis-core/chunks/V7GZHSVR.js +1 -0
- package/dist/analysis-core/chunks/{JEWBPIQI.js → VBGQKYIT.js} +1 -1
- package/dist/analysis-core/chunks/{S27Y2TP5.js → VVSHUUWX.js} +1 -1
- package/dist/analysis-core/chunks/VX6LYNXV.js +1 -0
- package/dist/analysis-core/chunks/{JKWTHRUR.js → YDSB7UOR.js} +1 -1
- package/dist/analysis-core/chunks/ZLUI3EHB.js +1 -0
- package/dist/analysis-core/contracts/execution-request.js +1 -1
- package/dist/analysis-core/contracts/intake-finding.js +1 -1
- package/dist/analysis-core/contracts/model-errors.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/conventions/analysis-fit.js +1 -1
- package/dist/analysis-core/conventions/file-admission.js +1 -1
- package/dist/analysis-core/errors.js +1 -1
- package/dist/analysis-core/import/calls/modules.js +1 -1
- package/dist/analysis-core/import/entry-selection.js +1 -1
- package/dist/analysis-core/import/outside-loader-sources.js +1 -1
- package/dist/analysis-core/index.js +1 -1
- package/dist/analysis-core/llm/cache-policy.js +1 -1
- package/dist/analysis-core/llm/cost.js +1 -1
- package/dist/analysis-core/llm/structured-output-error.js +1 -1
- package/dist/analysis-core/requests/answer-formats.js +1 -1
- package/dist/analysis-core/requests/frame-budget.js +1 -0
- package/dist/analysis-core/requests/local-plan.js +1 -1
- package/dist/analysis-core/requests/planned-limits.js +1 -1
- 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/analysis-core/wording.js +1 -0
- package/dist/cli/analysis-preview.js +22 -0
- package/dist/cli/analysis-selection.js +6 -8
- package/dist/cli/commands/analyze.js +29 -26
- package/dist/cli/commands/login.js +5 -0
- package/dist/cli/compatibility.js +39 -0
- package/dist/cli/completion-delivery.js +4 -3
- package/dist/cli/completion-recovery.js +25 -23
- package/dist/cli/credentials.js +3 -2
- package/dist/cli/finding-display.js +3 -2
- package/dist/cli/git.js +5 -2
- package/dist/cli/intake-payload.js +1 -2
- package/dist/cli/main.js +8 -2
- package/dist/cli/mcp.js +35 -10
- package/dist/cli/prior-completion.js +1 -1
- package/dist/cli/relay-check.js +88 -30
- package/dist/cli/relay-failure.js +19 -11
- package/dist/cli/relay-preparation.js +9 -6
- package/dist/cli/scope-choices.js +5 -4
- package/dist/cli/skipped-files.js +2 -1
- package/dist/cli/suggested-email.js +6 -1
- package/dist/cli/summary.js +2 -1
- package/dist/contract/agent-choice.js +2 -0
- package/dist/contract/cli-refusals.js +105 -9
- package/dist/contract/cli-version.js +1 -1
- package/dist/contract/console.js +2 -1
- package/dist/contract/email-sign-in-messages.js +2 -1
- package/dist/contract/scope-choice.js +4 -1
- package/dist/gatekeeper/client.js +11 -2
- package/dist/gatekeeper/connection-exchange.js +19 -4
- package/dist/gatekeeper/connection.js +15 -6
- package/dist/gatekeeper/console.js +10 -17
- package/dist/gatekeeper/model.js +13 -3
- package/package.json +1 -1
- package/dist/adapters/gitignore-output.js +0 -41
- package/dist/adapters/source-relay-plan.js +0 -7
- package/dist/analysis-core/chunks/2AASSZRO.js +0 -35
- package/dist/analysis-core/chunks/CQQ4TTXE.js +0 -1
- package/dist/analysis-core/chunks/F54DOWKN.js +0 -6
- package/dist/analysis-core/chunks/HCPO6QPU.js +0 -1
- package/dist/analysis-core/chunks/J4AXGE5W.js +0 -1
- package/dist/analysis-core/chunks/LFYTD7JF.js +0 -1
- package/dist/analysis-core/chunks/LMUXOKIM.js +0 -1
- package/dist/analysis-core/chunks/M5YWJY7P.js +0 -1
- package/dist/analysis-core/chunks/MGVIKA74.js +0 -1
- package/dist/analysis-core/chunks/NXHVBJL4.js +0 -1
- package/dist/analysis-core/chunks/OKBK3JL3.js +0 -1
- package/dist/analysis-core/chunks/SJESFYBA.js +0 -10
- package/dist/analysis-core/chunks/W24SFYZM.js +0 -67
- package/dist/analysis-core/chunks/YTHF2E2S.js +0 -1
package/README.md
CHANGED
|
@@ -28,8 +28,8 @@ npx tsx cli/check.ts /path/to/agent --output /path/to/results
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
Analysis makes paid model calls and needs a provider connection. The command reads source files without modifying
|
|
31
|
-
them, prints a readable summary, and writes `findings.json` and `summary.md`
|
|
32
|
-
by default. `--output` chooses
|
|
31
|
+
them, prints a readable summary, and writes `findings.json` and `summary.md` under
|
|
32
|
+
`~/.config/counterfact-labs/reports/<repository-id>/` by default. `--output` chooses a directory outside the analyzed repository. Reports can contain source excerpts; existing reports at those
|
|
33
33
|
paths are replaced after a completed analysis. Exit status is zero when analysis completes, including when it finds
|
|
34
34
|
problems, and nonzero for invalid arguments, a missing key or a failed run.
|
|
35
35
|
|
|
@@ -112,8 +112,7 @@ An over-limit selection returns included and omitted files for the user to
|
|
|
112
112
|
choose. The coding agent must ask before narrowing coverage. Terminal offers
|
|
113
113
|
exit with status 3 and save a private scope JSON file for `--scope <json-file>`;
|
|
114
114
|
MCP returns a successful `choose_scope` response. Each retry uses the saved
|
|
115
|
-
snapshot and the ordinary admission checks.
|
|
116
|
-
parent at `.counterfact/files/<entry-path-hash>/` and include selection metadata.
|
|
115
|
+
snapshot and the ordinary admission checks. Reports for a file live under `~/.config/counterfact-labs/reports/<repository-id>/files/<entry-path-hash>/` and include selection metadata.
|
|
117
116
|
|
|
118
117
|
The archive also ships `bundle/client-release.json` with its public Ed25519 verification key and local/relay versions,
|
|
119
118
|
and the gatekeeper's public production trust policy. Production trust has no environment override. With exactly
|
|
@@ -164,7 +163,7 @@ The command uses the existing saved sign-in and sends `X-Counterfact-CLI-Version
|
|
|
164
163
|
responses stop the analysis; the command never falls back to uploading files. Settings are captured at command start.
|
|
165
164
|
An analysis reads the agent's files as they are on disk, including uncommitted changes, and also works in folders without Git.
|
|
166
165
|
The console owns prices, expressed in millionths of a dollar.
|
|
167
|
-
|
|
166
|
+
The CLI leaves the repository and its `.gitignore` unchanged. Older in-repository reports remain readable for recovery.
|
|
168
167
|
|
|
169
168
|
The installed command uses the relay address in `bundle/client-release.json` and saved sign-in. The relay runner sends
|
|
170
169
|
repository facts and the admitted file list (paths and sizes, without contents or absolute checkout paths) to
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"verificationKey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAbBab1PYdmBKXxCvoJ47toJhQvxPevo9nlZnb0zbDWoc=\n-----END PUBLIC KEY-----\n",
|
|
3
3
|
"relayVersion": "relay-1.0.1",
|
|
4
|
-
"localVersion": "0.2.
|
|
4
|
+
"localVersion": "0.2.11",
|
|
5
5
|
"relayUrl": "https://relay.counterfactlabs.com:8443",
|
|
6
6
|
"projectContext": "checks"
|
|
7
7
|
}
|
|
@@ -4,7 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { findingActivitySchema } from "../analysis-core/analysis/schemas.js";
|
|
5
5
|
import { snapshotSettings } from "../analysis-core/settings.js";
|
|
6
6
|
import { publicReportSchema } from "../contract/public-report.js";
|
|
7
|
-
import { outputPaths,
|
|
7
|
+
import { outputPaths, reportOutputDirectory, reportReadPaths, writeReportFiles } from "./output-paths.js";
|
|
8
8
|
import { savedFindings } from "../cli/finding-display.js";
|
|
9
9
|
import { rankFindings } from "../analysis-core/analysis/finding-quality/policy.js";
|
|
10
10
|
import { fingerprintOf } from "../analysis-core/analysis/finding-identity.js";
|
|
@@ -37,7 +37,7 @@ async function readJson(path) {
|
|
|
37
37
|
}
|
|
38
38
|
/** Saved results are the authority for which omissions still need an analysis. */
|
|
39
39
|
export async function loadAnalysisResults(folder, outputDir) {
|
|
40
|
-
const [reportPath, contextPath] = await
|
|
40
|
+
const { paths: [reportPath, contextPath] } = await reportReadPaths(folder, ["findings.json", "analysis-context.json"], outputDir);
|
|
41
41
|
const report = publicReportSchema.parse(await readJson(reportPath));
|
|
42
42
|
const { reportHash, ...context } = contextSchema.parse(await readJson(contextPath));
|
|
43
43
|
if (reportHash !== hashReport(report))
|
|
@@ -45,17 +45,22 @@ export async function loadAnalysisResults(folder, outputDir) {
|
|
|
45
45
|
return { report, context };
|
|
46
46
|
}
|
|
47
47
|
export async function loadPublicReport(folder, outputDir) {
|
|
48
|
-
const [path] = await
|
|
48
|
+
const { paths: [path] } = await reportReadPaths(folder, ["findings.json"], outputDir);
|
|
49
49
|
return publicReportSchema.parse(await readJson(path));
|
|
50
50
|
}
|
|
51
51
|
/** Only non-secret analysis choices may survive into a later scoped run. */
|
|
52
52
|
export async function saveAnalysisContext(folder, context, outputDir) {
|
|
53
|
-
|
|
53
|
+
outputDir = await reportOutputDirectory(folder, outputDir);
|
|
54
|
+
const { paths: [path], legacy } = await reportReadPaths(folder, ["findings.json"], outputDir);
|
|
55
|
+
const report = publicReportSchema.parse(await readJson(path));
|
|
54
56
|
const value = contextSchema.parse({ ...context, reportHash: hashReport(report), settings: snapshotSettings(context.settings) });
|
|
55
|
-
|
|
57
|
+
// A legacy report stays read-only; settlement carries its validated findings into the home store.
|
|
58
|
+
await writeReportFiles(folder, { ...(legacy ? { "findings.json": `${JSON.stringify(report, null, 2)}\n` } : {}),
|
|
59
|
+
"analysis-context.json": `${JSON.stringify(value)}\n` }, outputDir);
|
|
56
60
|
}
|
|
57
61
|
function hashReport(report) { return createHash("sha256").update(JSON.stringify(report)).digest("hex"); }
|
|
58
62
|
export async function invalidateAnalysisContext(folder, outputDir) {
|
|
63
|
+
outputDir = await reportOutputDirectory(folder, outputDir);
|
|
59
64
|
try {
|
|
60
65
|
const [path] = await outputPaths(folder, ["analysis-context.json"], outputDir);
|
|
61
66
|
await unlink(path);
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { lstat, open, readdir, realpath } from "node:fs/promises";
|
|
3
|
+
import { lstat, open, readdir, realpath, unlink } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import { basename, dirname, join } from "node:path";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { LOCAL_FINDINGS_BODY_LIMIT, localFindingsPayloadSchema } from "../contract/console.js";
|
|
8
8
|
import { OutputPathError, outputPaths, writeOutputFiles } from "./output-paths.js";
|
|
9
|
-
import {
|
|
10
|
-
import { runGit } from "./git.js";
|
|
11
|
-
import { resolveFolderPath } from "./folder-path.js";
|
|
9
|
+
import { legacyFilesTracked, legacyReportDirectory } from "./legacy-reports.js";
|
|
12
10
|
import { matchesSnapshotProvenance } from "./snapshot.js";
|
|
13
11
|
import { acquireCompletionLock } from "./completion-lock.js";
|
|
12
|
+
import { storageIdentity } from "./storage-identity.js";
|
|
13
|
+
import { configDirectory } from "./config-directory.js";
|
|
14
14
|
const record = z.strictObject({ version: z.literal(1), consoleOrigin: z.url().optional(), uploaded: z.boolean().optional(),
|
|
15
15
|
finished: z.boolean().optional(), completedAt: z.number().finite().nonnegative().optional(),
|
|
16
16
|
absoluteExpiresAt: z.number().finite().nonnegative().optional(), payload: localFindingsPayloadSchema });
|
|
@@ -23,32 +23,11 @@ const storedRecord = z.union([records, retiredRecord, selectedRetiredRecord]);
|
|
|
23
23
|
const maxBytes = LOCAL_FINDINGS_BODY_LIMIT + 1024;
|
|
24
24
|
/** Recovery belongs to this checkout and origin, and survives edits and new commits. */
|
|
25
25
|
export async function completionPath(folder, identity) {
|
|
26
|
-
const root = await
|
|
27
|
-
let repository = null;
|
|
28
|
-
let common;
|
|
29
|
-
try {
|
|
30
|
-
common = (await runGit(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"])).toString("utf8").trim();
|
|
31
|
-
}
|
|
32
|
-
// Standalone folders need no Git identity; a usable checkout must never silently lose its identity.
|
|
33
|
-
catch (error) {
|
|
34
|
-
if (!await standaloneGitFailure(root, error))
|
|
35
|
-
throw error;
|
|
36
|
-
}
|
|
37
|
-
if (common !== undefined) {
|
|
38
|
-
let origin = "";
|
|
39
|
-
try {
|
|
40
|
-
origin = (await runGit(root, ["config", "--get", "remote.origin.url"])).toString("utf8").trim();
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
if (error.code !== 1)
|
|
44
|
-
throw error;
|
|
45
|
-
}
|
|
46
|
-
repository = [await realpath(common), origin];
|
|
47
|
-
}
|
|
26
|
+
const [root, repository] = await storageIdentity(folder);
|
|
48
27
|
const coordinates = identity ? [root, repository, selectionIdentity.parse(identity)] : [root, repository];
|
|
49
28
|
const key = createHash("sha256").update(JSON.stringify(coordinates)).digest("hex");
|
|
50
29
|
const home = await realpath(os.homedir());
|
|
51
|
-
return { home, path: join(home, "
|
|
30
|
+
return { home, path: join(configDirectory(home), "completions", `${key}.json`) };
|
|
52
31
|
}
|
|
53
32
|
export function matchesCompletionSetup(saved, setup) {
|
|
54
33
|
return saved.consoleOrigin === setup.consoleOrigin && (setup.projectId === undefined || saved.payload.projectId === setup.projectId);
|
|
@@ -140,19 +119,12 @@ export async function loadCompletion(folder, options = {}) {
|
|
|
140
119
|
// Repository-owned records cannot supply recovery data, even with matching provenance.
|
|
141
120
|
let legacy;
|
|
142
121
|
try {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
122
|
+
const directory = legacyReportDirectory(folder);
|
|
123
|
+
await lstat(join(directory, "completion.json"));
|
|
124
|
+
// Without Git there is no provenance to verify, so standalone records are ignored too.
|
|
125
|
+
if (await legacyFilesTracked(folder, [join(directory, "completion.json")]) !== false)
|
|
146
126
|
return null;
|
|
147
|
-
|
|
148
|
-
catch (error) {
|
|
149
|
-
// Without Git there is no provenance to verify, and uncertain legacy data must not block analysis.
|
|
150
|
-
if (await standaloneGitFailure(folder, error))
|
|
151
|
-
return null;
|
|
152
|
-
if (error.code !== 1)
|
|
153
|
-
throw error;
|
|
154
|
-
}
|
|
155
|
-
const [path] = await outputPaths(folder, ["completion.json"]);
|
|
127
|
+
const [path] = await outputPaths(folder, ["completion.json"], directory);
|
|
156
128
|
const { value, modifiedAt } = await readCompletion(path);
|
|
157
129
|
if (!("payload" in value))
|
|
158
130
|
return null;
|
|
@@ -214,14 +186,76 @@ function isRetiredCopyName(record, name) {
|
|
|
214
186
|
const digest = name.startsWith(`${record}.`) && name.endsWith(".json") ? name.slice(record.length + 1, -".json".length) : "";
|
|
215
187
|
return /^[a-f0-9]{64}$/.test(digest);
|
|
216
188
|
}
|
|
189
|
+
/** Bound private archives across folders; the ordinary reports remain the readable result. */
|
|
190
|
+
async function pruneRetainedCompletions(home, directory) {
|
|
191
|
+
await outputPaths(home, [], directory);
|
|
192
|
+
const copies = [];
|
|
193
|
+
for (const name of await readdir(directory)) {
|
|
194
|
+
if (!/^[a-f0-9-]{36}\.[a-f0-9]{64}\.json$/.test(name))
|
|
195
|
+
continue;
|
|
196
|
+
try {
|
|
197
|
+
const [path] = await outputPaths(home, [name], directory);
|
|
198
|
+
copies.push({ path, modifiedAt: (await lstat(path)).mtimeMs });
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (error.code !== "ENOENT")
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
copies.sort((a, b) => b.modifiedAt - a.modifiedAt || a.path.localeCompare(b.path));
|
|
206
|
+
for (const copy of copies.slice(10)) {
|
|
207
|
+
try {
|
|
208
|
+
await outputPaths(home, [basename(copy.path)], directory);
|
|
209
|
+
await unlink(copy.path);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (error.code !== "ENOENT")
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
217
|
/** The empty marker also prevents a retired legacy copy from being imported again. */
|
|
218
|
-
export async function discardCompletion(folder, recordPath, identity) {
|
|
218
|
+
export async function discardCompletion(folder, recordPath, identity, options = {}) {
|
|
219
219
|
const { home, path } = await completionPath(folder, identity);
|
|
220
220
|
const destination = recordPath ?? path;
|
|
221
221
|
if (dirname(destination) !== dirname(path) || destination !== path && !isRetiredCopyName(basename(path, ".json"), basename(destination)))
|
|
222
222
|
throw new OutputPathError();
|
|
223
|
+
let retainedPath;
|
|
224
|
+
if (options.keepCopy !== false) {
|
|
225
|
+
try {
|
|
226
|
+
// This is a local backup, excluded from automatic upload recovery.
|
|
227
|
+
await outputPaths(home, [basename(destination)], dirname(path));
|
|
228
|
+
const saved = await readCompletion(destination, identity).catch(error => {
|
|
229
|
+
if (error.code !== "ENOENT")
|
|
230
|
+
throw error;
|
|
231
|
+
return undefined;
|
|
232
|
+
});
|
|
233
|
+
if (saved && "payload" in saved.value) {
|
|
234
|
+
const bytes = JSON.stringify(saved.value);
|
|
235
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
236
|
+
const retained = join(dirname(path), "retained");
|
|
237
|
+
await writeOutputFiles(home, { [`${saved.value.payload.checkId}.${digest}.json`]: bytes }, retained);
|
|
238
|
+
await pruneRetainedCompletions(home, retained);
|
|
239
|
+
retainedPath = join(retained, `${saved.value.payload.checkId}.${digest}.json`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
// A readable report is enough to retire refused work and let a new analysis start.
|
|
244
|
+
// Without one, leave the pending payload intact if its backup cannot be written.
|
|
245
|
+
if (!options.reportStored)
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
223
249
|
const value = identity ? selectedRetiredRecord.parse({ version: 2, finished: true, ...identity }) : { version: 1, finished: true };
|
|
224
|
-
|
|
250
|
+
try {
|
|
251
|
+
await writeOutputFiles(home, { [basename(destination)]: JSON.stringify(value) }, dirname(path));
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
// Replacement can succeed before directory sync fails; the confirmed backup still identifies the results.
|
|
255
|
+
if (!retainedPath)
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
return retainedPath;
|
|
225
259
|
}
|
|
226
260
|
export async function lockCompletion(folder, onLost, identity) {
|
|
227
261
|
const { home, path } = await completionPath(folder, identity);
|
package/dist/adapters/files.js
CHANGED
|
@@ -176,22 +176,41 @@ async function readFolder(settings, folder, admission = {}, options = {}) {
|
|
|
176
176
|
const byPath = new Map(entries.map(entry => [entry.path, entry]));
|
|
177
177
|
const files = Object.create(null), visited = new Set(), unavailable = new Set();
|
|
178
178
|
const paths = planParts ? entries.map(entry => entry.path) : [...options.planSkipped];
|
|
179
|
-
const metadataPaths = new Set();
|
|
179
|
+
const metadataPaths = new Set(), metadataOnly = new Set();
|
|
180
180
|
let metadataBytes = 0;
|
|
181
|
+
const reserveMetadata = (path, size) => {
|
|
182
|
+
if (metadataPaths.has(path))
|
|
183
|
+
return true;
|
|
184
|
+
if (metadataPaths.size >= moduleResolutionLimits.settingsFiles || size > moduleResolutionLimits.settingsBytes - metadataBytes)
|
|
185
|
+
return false;
|
|
186
|
+
metadataPaths.add(path);
|
|
187
|
+
metadataBytes += size;
|
|
188
|
+
return true;
|
|
189
|
+
};
|
|
190
|
+
let metadata = settings.CF_LOCAL_IMPORT_RESOLUTION === "0" ? [] : ModuleResolver.settingsFiles(listing, new Map(), paths, settings);
|
|
181
191
|
let pending = paths, dependencies = {}, retainedBytes = 0;
|
|
182
192
|
while (pending.length) {
|
|
183
193
|
const next = [...new Set(pending)].filter(path => !visited.has(path));
|
|
184
194
|
next.forEach(path => visited.add(path));
|
|
185
195
|
await mapLimit(next, 4, async (path) => {
|
|
186
196
|
const entry = byPath.get(path);
|
|
187
|
-
if (!entry
|
|
197
|
+
if (!entry)
|
|
188
198
|
return;
|
|
199
|
+
if (!admitFile(settings, entry, { ...admission, purpose: "proposal", route: "local-folder" }).admitted) {
|
|
200
|
+
const pythonMetadata = metadata.includes(path) && ModuleResolver.isPythonSettingsFile(path);
|
|
201
|
+
if (!pythonMetadata || !admitFile(settings, entry, { ...admission, purpose: "entry", route: "local-folder" }).admitted)
|
|
202
|
+
return;
|
|
203
|
+
// Metadata can describe imports without becoming an analysis candidate.
|
|
204
|
+
metadataOnly.add(path);
|
|
205
|
+
if (!reserveMetadata(path, entry.size))
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
189
208
|
if (planParts && entry.size > READ_LIMITS.archive.bytes - retainedBytes) {
|
|
190
209
|
unavailable.add(path);
|
|
191
210
|
return;
|
|
192
211
|
}
|
|
193
212
|
try {
|
|
194
|
-
const text = await read(entry, "proposal");
|
|
213
|
+
const text = await read(entry, metadataOnly.has(path) ? "entry" : "proposal");
|
|
195
214
|
const size = text === null ? 0 : Buffer.byteLength(text);
|
|
196
215
|
if (text === null || planParts && size > READ_LIMITS.archive.bytes - retainedBytes)
|
|
197
216
|
unavailable.add(path);
|
|
@@ -212,20 +231,16 @@ async function readFolder(settings, folder, admission = {}, options = {}) {
|
|
|
212
231
|
}
|
|
213
232
|
});
|
|
214
233
|
dependencies = fileDependencies({ entries, files, settings });
|
|
215
|
-
|
|
234
|
+
metadata = settings.CF_LOCAL_IMPORT_RESOLUTION === "0" ? [] : ModuleResolver.settingsFiles(listing, new Map(Object.entries(files)), Object.keys(files), settings);
|
|
216
235
|
const settingsPaths = metadata.filter(path => {
|
|
217
236
|
if (visited.has(path) || metadataPaths.has(path))
|
|
218
237
|
return false;
|
|
219
238
|
const size = byPath.get(path)?.size;
|
|
220
|
-
|
|
221
|
-
return false;
|
|
222
|
-
metadataPaths.add(path);
|
|
223
|
-
metadataBytes += size;
|
|
224
|
-
return true;
|
|
239
|
+
return size !== undefined && reserveMetadata(path, size);
|
|
225
240
|
});
|
|
226
241
|
pending = [...settingsPaths, ...Object.values(dependencies).flat()].filter(path => !visited.has(path));
|
|
227
242
|
}
|
|
228
|
-
const plannedEntries = entries.filter(entry => planParts ? Object.hasOwn(files, entry.path) : !unavailable.has(entry.path)).map(entry => Object.hasOwn(files, entry.path)
|
|
243
|
+
const plannedEntries = entries.filter(entry => !metadataOnly.has(entry.path) && (planParts ? Object.hasOwn(files, entry.path) : !unavailable.has(entry.path))).map(entry => Object.hasOwn(files, entry.path)
|
|
229
244
|
? { ...entry, size: Buffer.byteLength(files[entry.path]) } : entry);
|
|
230
245
|
const input = { entries: plannedEntries, paths: planParts ? plannedEntries.map(entry => entry.path) : paths,
|
|
231
246
|
inputBytes: Object.fromEntries(Object.entries(files).map(([path, text]) => [path, fileEvidenceBytes(path, text)])),
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
2
|
+
import { resolveFolderPath } from "./folder-path.js";
|
|
3
|
+
import { runGit } from "./git.js";
|
|
4
|
+
import { standaloneGitFailure } from "./repository-root.js";
|
|
5
|
+
/** Only default report locations from earlier releases may supply a fallback. */
|
|
6
|
+
export function legacyReportDirectory(folder, selected) {
|
|
7
|
+
const legacy = join(folder, ".counterfact");
|
|
8
|
+
if (selected === undefined)
|
|
9
|
+
return legacy;
|
|
10
|
+
const parts = relative(selected.storeRoot, resolve(selected.directory)).split(sep);
|
|
11
|
+
const digest = (part) => /^[a-f0-9]{64}$/.test(part);
|
|
12
|
+
if (parts.length === 1 && digest(parts[0]))
|
|
13
|
+
return legacy;
|
|
14
|
+
if (parts.length === 3 && digest(parts[0]) && parts[1] === "files" && digest(parts[2]))
|
|
15
|
+
return join(legacy, "files", parts[2]);
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
/** Repository files cannot impersonate saved results; null means there is no Git provenance. */
|
|
19
|
+
export async function legacyFilesTracked(folder, paths) {
|
|
20
|
+
const root = await resolveFolderPath(folder);
|
|
21
|
+
const canonical = await Promise.all(paths.map(async (path) => join(await resolveFolderPath(dirname(path)), basename(path))));
|
|
22
|
+
const args = ["ls-files", "--", ...canonical.map(path => relative(root, path))];
|
|
23
|
+
try {
|
|
24
|
+
let tracked;
|
|
25
|
+
try {
|
|
26
|
+
tracked = await runGit(root, [args[0], "--with-tree=HEAD", ...args.slice(1)]);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code !== 128)
|
|
30
|
+
throw error;
|
|
31
|
+
// An unborn branch has an index but no HEAD tree to compare against.
|
|
32
|
+
const head = (await runGit(root, ["rev-parse", "--verify", "--quiet", "HEAD"]).catch(error => {
|
|
33
|
+
if (error.code !== 1)
|
|
34
|
+
throw error;
|
|
35
|
+
return Buffer.alloc(0);
|
|
36
|
+
})).length;
|
|
37
|
+
if (head)
|
|
38
|
+
throw error;
|
|
39
|
+
tracked = await runGit(root, args);
|
|
40
|
+
}
|
|
41
|
+
return tracked.length > 0;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (await standaloneGitFailure(root, error))
|
|
45
|
+
return null;
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -1,12 +1,54 @@
|
|
|
1
1
|
import { chmod, lstat, mkdir, realpath } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import os from "node:os";
|
|
2
4
|
import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
3
|
-
import {
|
|
5
|
+
import { OUTPUT_PATH_MESSAGE, REPORT_HOME_IN_REPOSITORY_MESSAGE, REPORT_OUTPUT_IN_REPOSITORY_MESSAGE } from "../contract/cli-refusals.js";
|
|
4
6
|
import { resolveFolderPath } from "./folder-path.js";
|
|
5
7
|
import { durableWrite } from "./durable-write.js";
|
|
6
|
-
|
|
8
|
+
import { storageIdentity } from "./storage-identity.js";
|
|
9
|
+
import { gitRepositoryRoot } from "./repository-root.js";
|
|
10
|
+
import { runGit } from "./git.js";
|
|
11
|
+
import { configDirectory } from "./config-directory.js";
|
|
12
|
+
import { legacyFilesTracked, legacyReportDirectory } from "./legacy-reports.js";
|
|
13
|
+
/** The home store uses the same checkout coordinates and home convention as completion recovery. */
|
|
14
|
+
export async function reportDirectory(folder, entries) {
|
|
15
|
+
const key = createHash("sha256").update(JSON.stringify(await storageIdentity(folder))).digest("hex");
|
|
16
|
+
const directory = join(reportStoreRoot(await realpath(os.homedir())), key);
|
|
17
|
+
if (entries === undefined)
|
|
18
|
+
return directory;
|
|
19
|
+
const hash = createHash("sha256").update(typeof entries === "string" ? entries : JSON.stringify(entries)).digest("hex");
|
|
20
|
+
return join(directory, "files", hash);
|
|
21
|
+
}
|
|
22
|
+
/** Only reads may fall back to reports left by earlier CLI releases. */
|
|
23
|
+
export async function reportReadPaths(folder, names, outputDir) {
|
|
24
|
+
try {
|
|
25
|
+
const paths = await outputPaths(folder, names, outputDir);
|
|
26
|
+
if (paths.length)
|
|
27
|
+
await lstat(paths[0]);
|
|
28
|
+
return { paths, legacy: false };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (!missing(error))
|
|
32
|
+
throw error;
|
|
33
|
+
const legacy = legacyReportDirectory(folder, outputDir === undefined ? undefined
|
|
34
|
+
: { directory: outputDir, storeRoot: reportStoreRoot(await realpath(os.homedir())) });
|
|
35
|
+
if (!legacy)
|
|
36
|
+
throw error;
|
|
37
|
+
const paths = await outputPaths(folder, names, legacy);
|
|
38
|
+
// Files supplied by the repository must never stand in for this user's saved analysis.
|
|
39
|
+
if (await legacyFilesTracked(folder, paths))
|
|
40
|
+
throw new OutputPathError();
|
|
41
|
+
return { paths, legacy: true };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
7
44
|
export class OutputPathError extends Error {
|
|
8
|
-
constructor() {
|
|
9
|
-
super(
|
|
45
|
+
constructor(message = OUTPUT_PATH_MESSAGE) {
|
|
46
|
+
super(message);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export class ReportLocationError extends OutputPathError {
|
|
50
|
+
constructor(homeInRepository) {
|
|
51
|
+
super(homeInRepository ? REPORT_HOME_IN_REPOSITORY_MESSAGE : REPORT_OUTPUT_IN_REPOSITORY_MESSAGE);
|
|
10
52
|
}
|
|
11
53
|
}
|
|
12
54
|
const missing = (error) => error.code === "ENOENT";
|
|
@@ -14,15 +56,48 @@ const within = (base, path) => {
|
|
|
14
56
|
const rel = relative(base, path);
|
|
15
57
|
return rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
16
58
|
};
|
|
59
|
+
const reportStoreRoot = (home) => join(configDirectory(home), "reports");
|
|
60
|
+
/** Report storage must leave the entire analyzed checkout read-only, even when it contains the home directory. */
|
|
61
|
+
export async function reportOutputDirectory(folder, outputDir) {
|
|
62
|
+
const defaultStore = outputDir === undefined;
|
|
63
|
+
outputDir ??= await reportDirectory(folder);
|
|
64
|
+
const root = await resolveFolderPath(folder);
|
|
65
|
+
const repository = await gitRepositoryRoot(async (args) => (await runGit(root, args)).toString("utf8"), root);
|
|
66
|
+
let existing = resolve(outputDir), suffix = "";
|
|
67
|
+
for (;;) {
|
|
68
|
+
try {
|
|
69
|
+
existing = resolve(await realpath(existing), suffix);
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (!missing(error) || dirname(existing) === existing)
|
|
74
|
+
throw new OutputPathError();
|
|
75
|
+
suffix = join(parse(existing).base, suffix);
|
|
76
|
+
existing = dirname(existing);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const boundary = repository ? await realpath(repository) : root;
|
|
80
|
+
if (within(boundary, existing)) {
|
|
81
|
+
throw new ReportLocationError(defaultStore || within(boundary, await realpath(os.homedir())));
|
|
82
|
+
}
|
|
83
|
+
// Keep the selected spelling so output admission can still refuse symlink components.
|
|
84
|
+
return resolve(outputDir);
|
|
85
|
+
}
|
|
17
86
|
/** Validate the selected anchor and every output component before creating or replacing files. */
|
|
18
87
|
export async function outputPaths(folder, names, outputDir, create = false) {
|
|
19
88
|
const selected = resolve(folder), root = await resolveFolderPath(folder);
|
|
20
|
-
const requested = resolve(outputDir ??
|
|
89
|
+
const requested = resolve(outputDir ?? await reportDirectory(root));
|
|
21
90
|
let directory = within(selected, requested) ? resolve(root, relative(selected, requested)) : requested;
|
|
22
91
|
const external = !within(root, directory);
|
|
23
92
|
const userUid = process.getuid?.();
|
|
24
93
|
let systemUid;
|
|
25
94
|
let anchor = root;
|
|
95
|
+
// Home storage starts at the user's boundary; arbitrary --output paths need no home lookup.
|
|
96
|
+
if (directory.includes(`${sep}${reportStoreRoot("")}${sep}`)) {
|
|
97
|
+
const home = await realpath(os.homedir());
|
|
98
|
+
if (within(reportStoreRoot(home), directory))
|
|
99
|
+
anchor = home;
|
|
100
|
+
}
|
|
26
101
|
while (!within(anchor, directory))
|
|
27
102
|
anchor = dirname(anchor);
|
|
28
103
|
try {
|
|
@@ -83,6 +158,7 @@ export async function outputPaths(folder, names, outputDir, create = false) {
|
|
|
83
158
|
}
|
|
84
159
|
}
|
|
85
160
|
export async function writeOutputFiles(folder, files, outputDir) {
|
|
161
|
+
outputDir ??= await reportDirectory(folder);
|
|
86
162
|
const names = Object.keys(files);
|
|
87
163
|
const paths = await outputPaths(folder, names, outputDir, true);
|
|
88
164
|
if (paths.length)
|
|
@@ -93,3 +169,7 @@ export async function writeOutputFiles(folder, files, outputDir) {
|
|
|
93
169
|
}
|
|
94
170
|
return paths;
|
|
95
171
|
}
|
|
172
|
+
/** Every report writer enforces the repository boundary, including direct library calls. */
|
|
173
|
+
export async function writeReportFiles(folder, files, outputDir) {
|
|
174
|
+
return writeOutputFiles(folder, files, await reportOutputDirectory(folder, outputDir));
|
|
175
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { bundleContentHash } from "../bundle/seal/signature.js";
|
|
2
2
|
import { checkPlannedCallsAgainstBundle } from "../bundle/seal/planned-calls.js";
|
|
3
|
+
import { localSourcePlan } from "../analysis-core/requests/local-plan.js";
|
|
3
4
|
import { relayPlanSchema } from "../contract/console.js";
|
|
4
|
-
/** Callers authenticate the
|
|
5
|
-
export function
|
|
5
|
+
/** Callers authenticate the release before reading files; only this owner turns source facts into a paid plan. */
|
|
6
|
+
export function prepareRelayPlan({ bundle, tools, relayVersion, settings, source }) {
|
|
7
|
+
const { files, calls } = localSourcePlan(settings, source);
|
|
6
8
|
checkPlannedCallsAgainstBundle(bundle, calls);
|
|
7
|
-
return relayPlanSchema.parse({ bundleVersion:
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
return { files, plan: relayPlanSchema.parse({ bundleVersion: tools.bundleVersion,
|
|
10
|
+
bundleHash: bundleContentHash(bundle.sealed), relayVersion,
|
|
11
|
+
calls: calls.map(({ routeId, model, calls, maxInputBytes, maxInputTokens, maxOutputTokens }) => ({ routeId, model, calls, maxInputBytes, maxInputTokens, maxOutputTokens })) }) };
|
|
10
12
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { open, realpath } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { basename, dirname } from "node:path";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { outputPaths, reportOutputDirectory, writeOutputFiles } from "./output-paths.js";
|
|
8
|
+
export const resultReceiptSchema = z.strictObject({ directory: z.string().min(1).max(16384),
|
|
9
|
+
hashes: z.tuple([z.string().regex(/^[a-f0-9]{64}$/), z.string().regex(/^[a-f0-9]{64}$/)]) });
|
|
10
|
+
const storedReceiptSchema = z.strictObject({ checkId: z.uuid(), reports: resultReceiptSchema });
|
|
11
|
+
// Older CLIs strictly parse completion records and only scan sibling slots ending in a hex digest.
|
|
12
|
+
const receiptName = (recordPath) => `${basename(recordPath, ".json")}.reports.json`;
|
|
13
|
+
export async function saveResultReceipt(recordPath, checkId, reports) {
|
|
14
|
+
const home = await realpath(os.homedir());
|
|
15
|
+
const bytes = JSON.stringify(storedReceiptSchema.parse({ checkId, reports }));
|
|
16
|
+
await writeOutputFiles(home, { [receiptName(recordPath)]: bytes }, dirname(recordPath));
|
|
17
|
+
}
|
|
18
|
+
/** An absent or unreadable receipt cannot prevent recovering the completion payload. */
|
|
19
|
+
export async function loadResultReceipt(recordPath, checkId) {
|
|
20
|
+
let file;
|
|
21
|
+
try {
|
|
22
|
+
const home = await realpath(os.homedir());
|
|
23
|
+
const [path] = await outputPaths(home, [receiptName(recordPath)], dirname(recordPath));
|
|
24
|
+
file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
25
|
+
const limit = 128 * 1024;
|
|
26
|
+
const info = await file.stat();
|
|
27
|
+
if (!info.isFile() || info.size > limit)
|
|
28
|
+
return undefined;
|
|
29
|
+
const bytes = Buffer.alloc(limit + 1);
|
|
30
|
+
const { bytesRead } = await file.read(bytes, 0, bytes.length, 0);
|
|
31
|
+
if (bytesRead > limit)
|
|
32
|
+
return undefined;
|
|
33
|
+
const value = storedReceiptSchema.parse(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, bytesRead))));
|
|
34
|
+
return value.checkId === checkId ? value.reports : undefined;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
await file?.close();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Hash both durable reports so a later run cannot mistake an older report for this result. */
|
|
44
|
+
export async function readResultReceipt(folder, outputDir) {
|
|
45
|
+
const directory = await reportOutputDirectory(folder, outputDir);
|
|
46
|
+
const [findingsPath, summaryPath] = await outputPaths(folder, ["findings.json", "summary.md"], directory);
|
|
47
|
+
const hashes = [];
|
|
48
|
+
const limit = 64 * 1024 * 1024;
|
|
49
|
+
for (const path of [findingsPath, summaryPath]) {
|
|
50
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
51
|
+
try {
|
|
52
|
+
const info = await file.stat();
|
|
53
|
+
if (!info.isFile() || info.size > limit)
|
|
54
|
+
throw new Error("Invalid saved analysis");
|
|
55
|
+
const hash = createHash("sha256"), buffer = Buffer.alloc(64 * 1024);
|
|
56
|
+
let total = 0;
|
|
57
|
+
for (;;) {
|
|
58
|
+
const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, limit - total + 1), null);
|
|
59
|
+
if (!bytesRead)
|
|
60
|
+
break;
|
|
61
|
+
total += bytesRead;
|
|
62
|
+
if (total > limit)
|
|
63
|
+
throw new Error("Invalid saved analysis");
|
|
64
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
65
|
+
}
|
|
66
|
+
hashes.push(hash.digest("hex"));
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await file.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { paths: { findingsPath, summaryPath }, receipt: resultReceiptSchema.parse({ directory, hashes }) };
|
|
73
|
+
}
|
|
74
|
+
/** Only the exact reports recorded after completion may replace a failed backup. */
|
|
75
|
+
export async function verifiedResultPaths(folder, receipt) {
|
|
76
|
+
const current = await readResultReceipt(folder, receipt.directory);
|
|
77
|
+
return current.receipt.hashes.every((hash, index) => hash === receipt.hashes[index]) ? current.paths : undefined;
|
|
78
|
+
}
|
package/dist/adapters/results.js
CHANGED
|
@@ -2,10 +2,9 @@ import { skippedFilesEnabled } from "../analysis-core/settings.js";
|
|
|
2
2
|
import { combineLocalReports } from "./combined-results.js";
|
|
3
3
|
import { invalidateAnalysisContext, loadPublicReport } from "./analysis-results.js";
|
|
4
4
|
import { formatSummary } from "../cli/summary.js";
|
|
5
|
-
import {
|
|
6
|
-
import { writeOutputFiles } from "./output-paths.js";
|
|
5
|
+
import { writeReportFiles } from "./output-paths.js";
|
|
7
6
|
import { projectPublicReport } from "../contract/public-report.js";
|
|
8
|
-
import { INCLUDED_FILES_CAPTION, INCOMPLETE_IMPORTS_MESSAGE,
|
|
7
|
+
import { INCLUDED_FILES_CAPTION, INCOMPLETE_IMPORTS_MESSAGE, selectionCoverageDescription } from "../contract/scope-choice.js";
|
|
9
8
|
export async function saveLocalResults(folder, report, refusedFiles, outputDir, options) {
|
|
10
9
|
const next = projectPublicReport(report, refusedFiles);
|
|
11
10
|
const publicReport = options.append && skippedFilesEnabled(options.settings ?? {})
|
|
@@ -15,14 +14,12 @@ export async function saveLocalResults(folder, report, refusedFiles, outputDir,
|
|
|
15
14
|
const scope = options.scope;
|
|
16
15
|
const incomplete = options.selection && !options.selection.completeness.complete
|
|
17
16
|
? [INCOMPLETE_IMPORTS_MESSAGE, "", INCLUDED_FILES_CAPTION, ...options.includedPaths.map(path => ` ${path}`), "", ""].join("\n") : "";
|
|
18
|
-
const coverage = scope && scope.imports !== "all" ?
|
|
19
|
-
|
|
20
|
-
const [findingsPath, summaryPath] = await writeOutputFiles(folder, {
|
|
17
|
+
const coverage = scope && scope.imports !== "all" ? selectionCoverageDescription(scope.entry, Array.isArray(scope.imports) ? scope.imports.length : 0) + "\n\n" : "";
|
|
18
|
+
const [findingsPath, summaryPath] = await writeReportFiles(folder, {
|
|
21
19
|
"findings.json": `${JSON.stringify(publicReport, null, 2)}\n`,
|
|
22
20
|
"summary.md": incomplete + coverage + formatSummary(publicReport, { ...options, analyzedFolder: options.analyzedFolder ?? folder, skippedOffer: !options.append, skippedFilesEnabled: skippedFilesEnabled(options.settings ?? {}) }),
|
|
23
21
|
...(options.selection ? { "selection.json": JSON.stringify({ schemaVersion: 1, selection: options.selection,
|
|
24
22
|
...(scope ? { scope } : {}) }, null, 2) + "\n" } : {}),
|
|
25
23
|
}, outputDir);
|
|
26
|
-
|
|
27
|
-
return { findingsPath, summaryPath, ignoreAdded };
|
|
24
|
+
return { findingsPath, summaryPath };
|
|
28
25
|
}
|