jev-agent-tools 0.1.4 → 0.3.0
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/CHANGELOG.md +106 -1
- package/CONTRIBUTING.md +43 -0
- package/README.md +58 -17
- package/SECURITY.md +43 -0
- package/dist/adapters/analysis-context.js +75 -0
- package/dist/adapters/ask-files.js +198 -0
- package/dist/adapters/ask-proof.js +200 -0
- package/dist/adapters/ask-syntax.js +385 -0
- package/dist/adapters/canonical-path.js +17 -0
- package/dist/adapters/command.js +234 -0
- package/dist/adapters/docs.js +192 -0
- package/dist/adapters/evidence-context.js +119 -0
- package/dist/adapters/exec.js +207 -0
- package/dist/adapters/files.js +418 -0
- package/dist/adapters/find.js +150 -0
- package/dist/adapters/git-base.js +32 -0
- package/dist/adapters/git-inventory.js +71 -0
- package/dist/adapters/git.js +483 -0
- package/dist/adapters/locate-file.js +197 -0
- package/dist/adapters/output-lines.js +46 -0
- package/dist/adapters/private-storage.js +106 -0
- package/dist/adapters/risk-callers.js +429 -0
- package/dist/adapters/runner-version.js +78 -0
- package/dist/adapters/shell.js +92 -0
- package/dist/adapters/syntax.js +187 -0
- package/dist/adapters/test-inventory.js +139 -0
- package/dist/adapters/usage.js +20 -0
- package/dist/adapters/utf8.js +47 -0
- package/dist/configuration.js +267 -0
- package/dist/constants.js +140 -0
- package/dist/core/ask-closure.js +282 -0
- package/dist/core/ask-proof.js +1 -0
- package/dist/core/ask-references.js +278 -0
- package/dist/core/asks.js +507 -0
- package/dist/core/batches.js +65 -0
- package/dist/core/command-output.js +224 -0
- package/dist/core/diff.js +178 -0
- package/dist/core/docs.js +302 -0
- package/dist/core/find.js +108 -0
- package/dist/core/git.js +1 -0
- package/dist/core/imports.js +550 -0
- package/dist/core/integrity.js +45 -0
- package/dist/core/lexical.js +132 -0
- package/dist/core/locate.js +169 -0
- package/dist/core/output.js +137 -0
- package/dist/core/pointer.js +29 -0
- package/dist/core/result-report.js +302 -0
- package/dist/core/risk-callers.js +851 -0
- package/dist/core/runner-version.js +45 -0
- package/dist/core/secret-path.js +34 -0
- package/dist/core/sections.js +230 -0
- package/dist/core/state.js +51 -0
- package/dist/core/syntax.js +1 -0
- package/dist/core/test-commands.js +334 -0
- package/dist/core/test-coverage.js +74 -0
- package/dist/core/test-discovery.js +1382 -0
- package/dist/core/test-evidence.js +527 -0
- package/dist/core/test-state.js +81 -0
- package/dist/core/truncate.js +12 -0
- package/dist/core/units.js +349 -0
- package/dist/describe.js +23 -0
- package/dist/guide.js +33 -0
- package/dist/host.js +24 -0
- package/dist/jev/client.js +456 -0
- package/dist/jev/pool.js +54 -0
- package/dist/jev/types.js +1 -0
- package/dist/mcp/main.js +124 -0
- package/dist/mcp/protocol.js +210 -0
- package/dist/mcp/tools.js +129 -0
- package/dist/presets/docs.js +62 -0
- package/dist/presets/risk.js +179 -0
- package/dist/presets/spec.js +81 -0
- package/dist/presets/witnesses.js +249 -0
- package/dist/render.js +114 -0
- package/dist/report-schema.js +1356 -0
- package/dist/result-types.js +1 -0
- package/dist/result.js +3 -0
- package/dist/runtime.js +1 -0
- package/dist/session.js +147 -0
- package/dist/texts/ask-files.js +3 -0
- package/dist/texts/ask.js +4 -0
- package/dist/texts/check-diff.js +20 -0
- package/dist/texts/configuration.js +1 -0
- package/dist/texts/find.js +19 -0
- package/dist/texts/guide.js +3 -0
- package/dist/texts/instructions.js +72 -0
- package/dist/texts/locate.js +15 -0
- package/dist/texts/select-tests.js +4 -0
- package/dist/tools/ask-files.js +450 -0
- package/dist/tools/ask-schema.js +70 -0
- package/dist/tools/ask.js +1147 -0
- package/dist/tools/check-diff.js +594 -0
- package/dist/tools/docs-check.js +408 -0
- package/dist/tools/find.js +682 -0
- package/dist/tools/locate.js +602 -0
- package/dist/tools/review-report.js +230 -0
- package/dist/tools/select-tests.js +821 -0
- package/dist/tools/spec-check.js +263 -0
- package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +31 -0
- package/docs/adr/0002-one-http-protocol-across-hosts.md +17 -0
- package/docs/adr/0003-explicit-scope-conservative-automation.md +19 -0
- package/docs/adr/0004-compiled-typed-intents.md +19 -0
- package/docs/adr/0005-evidence-construction-before-judgment.md +19 -0
- package/docs/adr/0006-visible-uncertainty-constrained-controls.md +21 -0
- package/docs/adr/0007-bounded-evidence-visible-limits.md +21 -0
- package/docs/adr/0008-static-test-discovery-conservative-plans.md +19 -0
- package/docs/adr/0009-session-cache-requested-model-identity.md +17 -0
- package/docs/adr/0010-mcp-server-thin-host.md +23 -0
- package/docs/agent-instructions.md +120 -0
- package/docs/design.md +16 -4
- package/docs/mcp.md +233 -0
- package/docs/tools/jev_ask.md +8 -5
- package/docs/tools/jev_ask_files.md +2 -1
- package/docs/tools/jev_check_diff.md +4 -1
- package/docs/tools/jev_find_files.md +2 -1
- package/docs/tools/jev_locate_in_file.md +5 -0
- package/docs/tools/jev_select_tests.md +4 -1
- package/package.json +19 -4
- package/rules/jev-ask.md +22 -1
- package/server.json +57 -0
- package/src/adapters/ask-files.ts +11 -3
- package/src/adapters/ask-proof.ts +69 -11
- package/src/adapters/canonical-path.ts +18 -0
- package/src/adapters/command.ts +102 -36
- package/src/adapters/docs.ts +33 -14
- package/src/adapters/evidence-context.ts +169 -0
- package/src/adapters/exec.ts +226 -0
- package/src/adapters/files.ts +146 -16
- package/src/adapters/find.ts +37 -7
- package/src/adapters/git-base.ts +7 -1
- package/src/adapters/git.ts +61 -8
- package/src/adapters/locate-file.ts +51 -9
- package/src/adapters/private-storage.ts +155 -0
- package/src/adapters/risk-callers.ts +7 -2
- package/src/adapters/shell.ts +113 -0
- package/src/adapters/test-inventory.ts +12 -4
- package/src/configuration.ts +55 -14
- package/src/constants.ts +37 -5
- package/src/core/ask-references.ts +262 -146
- package/src/core/asks.ts +79 -7
- package/src/core/command-output.ts +17 -1
- package/src/core/import-boundaries.ts +8 -3
- package/src/core/locate.ts +8 -5
- package/src/core/output.ts +34 -0
- package/src/core/result-report.ts +410 -0
- package/src/core/secret-path.ts +37 -0
- package/src/core/state.ts +8 -1
- package/src/core/units.ts +3 -2
- package/src/host.ts +11 -0
- package/src/index.ts +3 -0
- package/src/jev/client.ts +66 -16
- package/src/jev/types.ts +24 -3
- package/src/mcp/main.ts +135 -0
- package/src/mcp/protocol.ts +332 -0
- package/src/mcp/tools.ts +179 -0
- package/src/render.ts +109 -0
- package/src/report-schema.ts +1380 -0
- package/src/result-types.ts +234 -0
- package/src/result.ts +4 -1
- package/src/runtime.ts +6 -0
- package/src/session.ts +59 -0
- package/src/setup.ts +13 -5
- package/src/texts/ask-files.ts +4 -1
- package/src/texts/ask.ts +8 -1
- package/src/texts/check-diff.ts +7 -4
- package/src/texts/find.ts +8 -2
- package/src/texts/guide.ts +8 -16
- package/src/texts/instructions.ts +98 -0
- package/src/texts/locate.ts +8 -2
- package/src/texts/run-end.ts +2 -2
- package/src/texts/select-tests.ts +4 -1
- package/src/tools/ask-files.ts +311 -18
- package/src/tools/ask.ts +722 -95
- package/src/tools/check-diff.ts +337 -31
- package/src/tools/docs-check.ts +241 -38
- package/src/tools/find.ts +389 -29
- package/src/tools/locate.ts +387 -25
- package/src/tools/review-report.ts +308 -0
- package/src/tools/select-tests.ts +484 -23
- package/src/tools/spec-check.ts +194 -19
|
@@ -23,7 +23,7 @@ export async function collectAskFiles(
|
|
|
23
23
|
paths: readonly string[],
|
|
24
24
|
signal?: AbortSignal,
|
|
25
25
|
exec?: GitExec,
|
|
26
|
-
): Promise<Result<{ files: AskFile[]; skipped: string[] }>> {
|
|
26
|
+
): Promise<Result<{ files: AskFile[]; skipped: string[]; secret: string[] }>> {
|
|
27
27
|
if (!paths.length)
|
|
28
28
|
return {
|
|
29
29
|
ok: false,
|
|
@@ -42,6 +42,7 @@ export async function collectAskFiles(
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
const skipped = new Set<string>();
|
|
45
|
+
const secret = new Set<string>();
|
|
45
46
|
const candidates = new Set<string>();
|
|
46
47
|
try {
|
|
47
48
|
let inventory: Set<string> | undefined;
|
|
@@ -63,7 +64,9 @@ export async function collectAskFiles(
|
|
|
63
64
|
inventory,
|
|
64
65
|
);
|
|
65
66
|
if (!admission.ok) {
|
|
66
|
-
|
|
67
|
+
const entry = `${path} (${admission.error})`;
|
|
68
|
+
skipped.add(entry);
|
|
69
|
+
if (admission.cause === "secret_pattern") secret.add(entry);
|
|
67
70
|
return;
|
|
68
71
|
}
|
|
69
72
|
if (path.split("/").some((part) => excludedDirectories.has(part)))
|
|
@@ -229,7 +232,12 @@ export async function collectAskFiles(
|
|
|
229
232
|
};
|
|
230
233
|
}
|
|
231
234
|
}
|
|
232
|
-
return {
|
|
235
|
+
return {
|
|
236
|
+
ok: true,
|
|
237
|
+
files,
|
|
238
|
+
skipped: [...skipped].sort(),
|
|
239
|
+
secret: [...secret].sort(),
|
|
240
|
+
};
|
|
233
241
|
} catch (error) {
|
|
234
242
|
return { ok: false, error: `Cannot expand paths: ${String(error)}` };
|
|
235
243
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { relative, resolve } from "node:path";
|
|
1
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
2
|
import { STATE_MAX_CHARS, TIMEOUT_MS } from "../constants.ts";
|
|
3
3
|
import type { GitExec } from "../core/git.ts";
|
|
4
4
|
import type { ImportSource } from "../core/imports.ts";
|
|
5
5
|
import type { Result } from "../result.ts";
|
|
6
|
-
import { collectFiles } from "./files.ts";
|
|
6
|
+
import { checkFileAdmission, collectFiles } from "./files.ts";
|
|
7
7
|
import { verifyGitUtf8 } from "./utf8.ts";
|
|
8
8
|
|
|
9
9
|
export interface AskRepository {
|
|
10
10
|
root: string;
|
|
11
11
|
known: ReadonlySet<string>;
|
|
12
|
+
beforeKnown: ReadonlySet<string>;
|
|
12
13
|
baseSha?: string;
|
|
13
14
|
read(path: string): Promise<Result<ImportSource>>;
|
|
14
15
|
readBefore(path: string): Promise<Result<{ text: string | null }>>;
|
|
@@ -69,18 +70,20 @@ export async function collectAskRepository(
|
|
|
69
70
|
for (const path of ignored.stdout.split("\0")) known.delete(path);
|
|
70
71
|
const baseSha = revision?.stdout.trim();
|
|
71
72
|
const basePaths = baseSha
|
|
72
|
-
? await exec(
|
|
73
|
-
"git",
|
|
74
|
-
["ls-tree", "-r", "--name-only", "-z", baseSha],
|
|
75
|
-
gitOptions,
|
|
76
|
-
)
|
|
73
|
+
? await exec("git", ["ls-tree", "-r", "-z", baseSha], gitOptions)
|
|
77
74
|
: undefined;
|
|
78
75
|
if (basePaths && (basePaths.code !== 0 || basePaths.killed))
|
|
79
76
|
return {
|
|
80
77
|
ok: false,
|
|
81
78
|
error: basePaths.stderr || "Cannot list base paths.",
|
|
82
79
|
};
|
|
83
|
-
const
|
|
80
|
+
const beforeEntries = new Map(
|
|
81
|
+
(basePaths?.stdout.split("\0").filter(Boolean) ?? []).map((entry) => [
|
|
82
|
+
entry.slice(entry.indexOf("\t") + 1),
|
|
83
|
+
entry,
|
|
84
|
+
]),
|
|
85
|
+
);
|
|
86
|
+
const beforeKnown = new Set(beforeEntries.keys());
|
|
84
87
|
const reads = new Map<string, Promise<Result<ImportSource>>>();
|
|
85
88
|
const beforeReads = new Map<
|
|
86
89
|
string,
|
|
@@ -90,6 +93,7 @@ export async function collectAskRepository(
|
|
|
90
93
|
ok: true,
|
|
91
94
|
root,
|
|
92
95
|
known,
|
|
96
|
+
beforeKnown,
|
|
93
97
|
baseSha,
|
|
94
98
|
read(path) {
|
|
95
99
|
let pending = reads.get(path);
|
|
@@ -117,9 +121,53 @@ export async function collectAskRepository(
|
|
|
117
121
|
let pending = beforeReads.get(path);
|
|
118
122
|
if (!pending) {
|
|
119
123
|
pending = (async () => {
|
|
120
|
-
if (!baseSha || !beforeKnown.has(path))
|
|
121
|
-
return { ok: true as const, text: null };
|
|
122
124
|
try {
|
|
125
|
+
if (isAbsolute(path) || path.split(/[\\/]/).includes(".."))
|
|
126
|
+
return {
|
|
127
|
+
ok: false as const,
|
|
128
|
+
cause: "forbidden_path" as const,
|
|
129
|
+
error: `Path must remain inside the repository: ${path}.`,
|
|
130
|
+
};
|
|
131
|
+
const admission = await checkFileAdmission(root, path);
|
|
132
|
+
if (!admission.ok) return admission;
|
|
133
|
+
const ignoredPath = await exec(
|
|
134
|
+
"git",
|
|
135
|
+
["check-ignore", "--no-index", "--", path],
|
|
136
|
+
gitOptions,
|
|
137
|
+
);
|
|
138
|
+
if (ignoredPath.killed || ![0, 1].includes(ignoredPath.code))
|
|
139
|
+
return {
|
|
140
|
+
ok: false as const,
|
|
141
|
+
cause: "git_failure" as const,
|
|
142
|
+
error: `${path}: historical admission unavailable`,
|
|
143
|
+
};
|
|
144
|
+
if (ignoredPath.code === 0)
|
|
145
|
+
return {
|
|
146
|
+
ok: false as const,
|
|
147
|
+
cause: "ignored_path" as const,
|
|
148
|
+
error: `${path}: gitignored: not sent to Jev`,
|
|
149
|
+
};
|
|
150
|
+
if (!baseSha)
|
|
151
|
+
return {
|
|
152
|
+
ok: false as const,
|
|
153
|
+
error: `${path}: comparison base unavailable`,
|
|
154
|
+
};
|
|
155
|
+
if (!beforeKnown.has(path))
|
|
156
|
+
return known.has(path)
|
|
157
|
+
? { ok: true as const, text: null }
|
|
158
|
+
: {
|
|
159
|
+
ok: false as const,
|
|
160
|
+
error: `${path}: not in current or comparison base inventory`,
|
|
161
|
+
};
|
|
162
|
+
const entry = beforeEntries.get(path) ?? "";
|
|
163
|
+
if (!/^(?:100644|100755) blob /.test(entry))
|
|
164
|
+
return {
|
|
165
|
+
ok: false as const,
|
|
166
|
+
cause: entry.startsWith("120000 ")
|
|
167
|
+
? ("symlink" as const)
|
|
168
|
+
: ("file_unavailable" as const),
|
|
169
|
+
error: `${path}: not a regular file at comparison base: not sent to Jev`,
|
|
170
|
+
};
|
|
123
171
|
const object = await exec(
|
|
124
172
|
"git",
|
|
125
173
|
[
|
|
@@ -172,10 +220,20 @@ export async function collectAskRepository(
|
|
|
172
220
|
ok: false as const,
|
|
173
221
|
error: `before evidence unavailable for ${path}`,
|
|
174
222
|
};
|
|
223
|
+
if (loaded.stdout.includes("\0"))
|
|
224
|
+
return {
|
|
225
|
+
ok: false as const,
|
|
226
|
+
cause: "binary_or_non_utf8" as const,
|
|
227
|
+
error: `${path}: binary content: not sent to Jev`,
|
|
228
|
+
};
|
|
175
229
|
const decoded = verifyGitUtf8(loaded.stdout, id, size);
|
|
176
230
|
return decoded.ok
|
|
177
231
|
? decoded
|
|
178
|
-
: {
|
|
232
|
+
: {
|
|
233
|
+
ok: false as const,
|
|
234
|
+
cause: "binary_or_non_utf8" as const,
|
|
235
|
+
error: `${path}: ${decoded.error}`,
|
|
236
|
+
};
|
|
179
237
|
} catch (error) {
|
|
180
238
|
return { ok: false as const, error: String(error) };
|
|
181
239
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { realpath } from "node:fs";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
const nativeRealpath = promisify(realpath.native);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Canonical form of an existing path: resolves symlinks and, on Windows,
|
|
8
|
+
* expands 8.3 short names (C:\Users\NAME~1) so the path relates correctly to
|
|
9
|
+
* `git rev-parse --show-toplevel`, which always reports the long form. Falls
|
|
10
|
+
* back to the input when the path cannot be resolved.
|
|
11
|
+
*/
|
|
12
|
+
export async function canonicalPath(path: string): Promise<string> {
|
|
13
|
+
try {
|
|
14
|
+
return await nativeRealpath(path);
|
|
15
|
+
} catch {
|
|
16
|
+
return path;
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/adapters/command.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import type { GitExec } from "../core/git.ts";
|
|
22
22
|
import type { Result } from "../result.ts";
|
|
23
23
|
import { outputLines } from "./output-lines.ts";
|
|
24
|
+
import { resolveShell } from "./shell.ts";
|
|
24
25
|
|
|
25
26
|
export interface CommandOutput {
|
|
26
27
|
command: string;
|
|
@@ -31,12 +32,47 @@ export interface CommandOutput {
|
|
|
31
32
|
compressed: boolean;
|
|
32
33
|
truncated: boolean;
|
|
33
34
|
}
|
|
35
|
+
const TRUNCATION_MARK = `…[line truncated at ${OUTPUT_LINE_MAX_CHARS} chars]`;
|
|
36
|
+
/** Shortest key prefix worth redacting when a line cut leaves only its start. */
|
|
37
|
+
const SECRET_PREFIX_MIN = 4;
|
|
38
|
+
/**
|
|
39
|
+
* Replace every occurrence of `secret` in `text`, counting replacements. A
|
|
40
|
+
* line cut at OUTPUT_LINE_MAX_CHARS can end inside the key; that trailing key
|
|
41
|
+
* prefix is redacted too.
|
|
42
|
+
*/
|
|
43
|
+
export function redactSecret(
|
|
44
|
+
text: string,
|
|
45
|
+
secret: string | undefined,
|
|
46
|
+
): { text: string; count: number } {
|
|
47
|
+
if (!secret) return { text, count: 0 };
|
|
48
|
+
const parts = text.split(secret);
|
|
49
|
+
let result = parts.join("[redacted]");
|
|
50
|
+
let count = parts.length - 1;
|
|
51
|
+
if (result.endsWith(TRUNCATION_MARK)) {
|
|
52
|
+
const kept = result.slice(0, -TRUNCATION_MARK.length);
|
|
53
|
+
const min = Math.min(SECRET_PREFIX_MIN, secret.length);
|
|
54
|
+
for (
|
|
55
|
+
let length = Math.min(secret.length - 1, kept.length);
|
|
56
|
+
length >= min;
|
|
57
|
+
length--
|
|
58
|
+
) {
|
|
59
|
+
if (kept.endsWith(secret.slice(0, length))) {
|
|
60
|
+
result = `${kept.slice(0, -length)}[redacted]${TRUNCATION_MARK}`;
|
|
61
|
+
count++;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { text: result, count };
|
|
67
|
+
}
|
|
34
68
|
export async function captureCommand(
|
|
35
69
|
exec: GitExec,
|
|
36
70
|
cwd: string,
|
|
37
71
|
command: string,
|
|
38
72
|
timeoutS = ASK_TIMEOUT_S,
|
|
39
73
|
signal?: AbortSignal,
|
|
74
|
+
/** Configured Jev API key, replaced in the captured command and output. */
|
|
75
|
+
secret?: string,
|
|
40
76
|
): Promise<
|
|
41
77
|
Result<{
|
|
42
78
|
output: CommandOutput;
|
|
@@ -47,9 +83,19 @@ export async function captureCommand(
|
|
|
47
83
|
selectedPassages: boolean;
|
|
48
84
|
assertion: boolean;
|
|
49
85
|
targets: FailureTarget[];
|
|
50
|
-
|
|
86
|
+
/** Occurrences of `secret` replaced with `[redacted]`. */
|
|
87
|
+
redactions: number;
|
|
88
|
+
}> & {
|
|
89
|
+
commandExecution: "not_started" | "unknown" | "finished";
|
|
90
|
+
commandExitCode?: number | null;
|
|
91
|
+
commandTimedOut?: boolean;
|
|
92
|
+
}
|
|
51
93
|
> {
|
|
52
94
|
const directory = await mkdtemp(join(tmpdir(), "jev-output-"));
|
|
95
|
+
let commandExecution: "not_started" | "unknown" | "finished" = "not_started";
|
|
96
|
+
let completion:
|
|
97
|
+
| { commandExitCode: number | null; commandTimedOut: boolean }
|
|
98
|
+
| undefined;
|
|
53
99
|
try {
|
|
54
100
|
await chmod(directory, 0o700);
|
|
55
101
|
const stdoutPath = join(directory, "stdout");
|
|
@@ -62,39 +108,40 @@ export async function captureCommand(
|
|
|
62
108
|
killed: boolean;
|
|
63
109
|
};
|
|
64
110
|
try {
|
|
111
|
+
const shell = resolveShell();
|
|
112
|
+
// Fail closed: never spawn a bare name that PATH could resolve to WSL.
|
|
113
|
+
if (!shell.ok)
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
cause: "file_unavailable",
|
|
117
|
+
commandExecution: "not_started",
|
|
118
|
+
error: shell.error,
|
|
119
|
+
};
|
|
120
|
+
commandExecution = "unknown";
|
|
65
121
|
executed = await exec(
|
|
66
|
-
|
|
122
|
+
shell.executable,
|
|
67
123
|
[
|
|
68
|
-
|
|
69
|
-
"bash",
|
|
124
|
+
...shell.prefix,
|
|
70
125
|
"-c",
|
|
71
|
-
|
|
126
|
+
`${shell.scriptPrefix}exec >"$1" 2>"$2"; set --; (\n${command}\n)\nexit $?`,
|
|
72
127
|
"jev",
|
|
73
128
|
stdoutPath,
|
|
74
129
|
stderrPath,
|
|
75
130
|
],
|
|
76
131
|
{ cwd, timeout: timeoutS * 1000, signal },
|
|
77
132
|
);
|
|
133
|
+
commandExecution = "finished";
|
|
134
|
+
completion = {
|
|
135
|
+
commandExitCode: executed.killed ? null : executed.code,
|
|
136
|
+
commandTimedOut: executed.killed,
|
|
137
|
+
};
|
|
78
138
|
} catch (error) {
|
|
79
139
|
signal?.throwIfAborted();
|
|
80
140
|
return {
|
|
81
|
-
ok:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
timed_out: false,
|
|
86
|
-
stdout: "",
|
|
87
|
-
stderr: `Command executable unavailable: ${String(error)}`,
|
|
88
|
-
compressed: true,
|
|
89
|
-
truncated: false,
|
|
90
|
-
},
|
|
91
|
-
originalBytes: 0,
|
|
92
|
-
compressedChars: 0,
|
|
93
|
-
lineOmittedChars: 0,
|
|
94
|
-
shapeLimitExceeded: false,
|
|
95
|
-
selectedPassages: false,
|
|
96
|
-
assertion: false,
|
|
97
|
-
targets: [],
|
|
141
|
+
ok: false,
|
|
142
|
+
cause: "file_unavailable",
|
|
143
|
+
commandExecution,
|
|
144
|
+
error: `Command executable unavailable: ${String(error)}`,
|
|
98
145
|
};
|
|
99
146
|
}
|
|
100
147
|
signal?.throwIfAborted();
|
|
@@ -116,6 +163,9 @@ export async function captureCommand(
|
|
|
116
163
|
if (sizes.some((size) => size > OUTPUT_FILE_MAX_BYTES))
|
|
117
164
|
return {
|
|
118
165
|
ok: false,
|
|
166
|
+
cause: "evidence_too_large",
|
|
167
|
+
commandExecution: "finished",
|
|
168
|
+
...completion,
|
|
119
169
|
error: `output exceeded ${OUTPUT_FILE_MAX_BYTES} bytes per stream; narrow command`,
|
|
120
170
|
};
|
|
121
171
|
const targets: FailureTarget[] = [];
|
|
@@ -125,19 +175,23 @@ export async function captureCommand(
|
|
|
125
175
|
let lineOmittedChars = 0;
|
|
126
176
|
let shapeLimitExceeded = false;
|
|
127
177
|
const texts: string[] = [];
|
|
178
|
+
let redactions = 0;
|
|
179
|
+
const redacted = (text: string): string => {
|
|
180
|
+
const result = redactSecret(text, secret);
|
|
181
|
+
redactions += result.count;
|
|
182
|
+
return result.text;
|
|
183
|
+
};
|
|
128
184
|
for (const [index, path] of [stdoutPath, stderrPath].entries()) {
|
|
129
185
|
if (!sizes[index]) {
|
|
130
|
-
texts.push(index === 0 ? executed.stdout : executed.stderr);
|
|
186
|
+
texts.push(redacted(index === 0 ? executed.stdout : executed.stderr));
|
|
131
187
|
continue;
|
|
132
188
|
}
|
|
133
189
|
const frequencies = new Map<string, number>();
|
|
134
190
|
let shapeLimit = false;
|
|
135
191
|
for await (const raw of outputLines(path, signal)) {
|
|
136
192
|
signal?.throwIfAborted();
|
|
137
|
-
const line = cleanOutput(raw);
|
|
138
|
-
linesTruncated ||= raw.endsWith(
|
|
139
|
-
`…[line truncated at ${OUTPUT_LINE_MAX_CHARS} chars]`,
|
|
140
|
-
);
|
|
193
|
+
const line = redactSecret(cleanOutput(raw), secret).text;
|
|
194
|
+
linesTruncated ||= raw.endsWith(TRUNCATION_MARK);
|
|
141
195
|
const shape = lineShape(line);
|
|
142
196
|
if (
|
|
143
197
|
!frequencies.has(shape) &&
|
|
@@ -184,17 +238,17 @@ export async function captureCommand(
|
|
|
184
238
|
lineOmittedChars += chars;
|
|
185
239
|
})) {
|
|
186
240
|
signal?.throwIfAborted();
|
|
187
|
-
linesTruncated ||= raw.endsWith(
|
|
188
|
-
|
|
189
|
-
);
|
|
190
|
-
compressor.line(
|
|
241
|
+
linesTruncated ||= raw.endsWith(TRUNCATION_MARK);
|
|
242
|
+
// Redact before compression, failure windows and state assembly.
|
|
243
|
+
const line = redacted(cleanOutput(raw));
|
|
244
|
+
compressor.line(line);
|
|
191
245
|
if (!failureSeen) {
|
|
192
|
-
failureWindow.push(
|
|
246
|
+
failureWindow.push(line);
|
|
193
247
|
if (failureWindow.length > OUTPUT_FAILURE_WINDOW_LINES + 1)
|
|
194
248
|
failureWindow.shift();
|
|
195
249
|
if (isAnchor(raw)) failureSeen = true;
|
|
196
250
|
} else if (afterFailure++ < OUTPUT_FAILURE_WINDOW_LINES) {
|
|
197
|
-
failureWindow.push(
|
|
251
|
+
failureWindow.push(line);
|
|
198
252
|
if (!secondSeen && afterFailure > 1 && isFailingTestsHeader(raw)) {
|
|
199
253
|
secondSeen = true;
|
|
200
254
|
afterSecond = 0;
|
|
@@ -203,10 +257,10 @@ export async function captureCommand(
|
|
|
203
257
|
if (isFailingTestsHeader(raw)) {
|
|
204
258
|
secondSeen = true;
|
|
205
259
|
afterSecond = 0;
|
|
206
|
-
failureWindow.push(
|
|
260
|
+
failureWindow.push(line);
|
|
207
261
|
}
|
|
208
262
|
} else if (afterSecond++ < OUTPUT_FAILURE_WINDOW_LINES) {
|
|
209
|
-
failureWindow.push(
|
|
263
|
+
failureWindow.push(line);
|
|
210
264
|
}
|
|
211
265
|
}
|
|
212
266
|
compressor.finish();
|
|
@@ -217,8 +271,10 @@ export async function captureCommand(
|
|
|
217
271
|
}
|
|
218
272
|
return {
|
|
219
273
|
ok: true,
|
|
274
|
+
commandExecution: "finished",
|
|
275
|
+
...completion,
|
|
220
276
|
output: {
|
|
221
|
-
command,
|
|
277
|
+
command: redacted(command),
|
|
222
278
|
exit_code: executed.killed ? null : executed.code,
|
|
223
279
|
timed_out: executed.killed,
|
|
224
280
|
stdout: texts[0] ?? "",
|
|
@@ -233,6 +289,16 @@ export async function captureCommand(
|
|
|
233
289
|
selectedPassages: false,
|
|
234
290
|
assertion: signature === "assertion",
|
|
235
291
|
targets,
|
|
292
|
+
redactions,
|
|
293
|
+
};
|
|
294
|
+
} catch (error) {
|
|
295
|
+
signal?.throwIfAborted();
|
|
296
|
+
return {
|
|
297
|
+
ok: false,
|
|
298
|
+
cause: "file_unavailable",
|
|
299
|
+
commandExecution,
|
|
300
|
+
...completion,
|
|
301
|
+
error: `Command capture unavailable: ${String(error)}`,
|
|
236
302
|
};
|
|
237
303
|
} finally {
|
|
238
304
|
await rm(directory, { recursive: true, force: true });
|
package/src/adapters/docs.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "../core/docs.ts";
|
|
6
6
|
import type { GitExec } from "../core/git.ts";
|
|
7
7
|
import type { Result } from "../result.ts";
|
|
8
|
+
import type { Cause } from "../result-types.ts";
|
|
8
9
|
import {
|
|
9
10
|
checkFileAdmission,
|
|
10
11
|
openRepoFile,
|
|
@@ -20,7 +21,7 @@ export interface DocsInventory {
|
|
|
20
21
|
cwd: string;
|
|
21
22
|
files: DocsSource[];
|
|
22
23
|
tracked: ReadonlySet<string>;
|
|
23
|
-
limits: { path: string; reason: string }[];
|
|
24
|
+
limits: { path: string; reason: string; cause?: Cause }[];
|
|
24
25
|
read: (path: string) => Promise<DocsSource | undefined>;
|
|
25
26
|
}
|
|
26
27
|
/** Inventory paths eagerly, but load only Markdown/configuration and subsequently reached sources. */
|
|
@@ -37,6 +38,7 @@ export async function collectDocsInventory(
|
|
|
37
38
|
if (root.code || root.killed)
|
|
38
39
|
return {
|
|
39
40
|
ok: false,
|
|
41
|
+
cause: signal?.aborted ? "cancelled" : "git_failure",
|
|
40
42
|
error: root.stderr.trim() || "Repository root not found.",
|
|
41
43
|
};
|
|
42
44
|
cwd = root.stdout.trim();
|
|
@@ -48,6 +50,7 @@ export async function collectDocsInventory(
|
|
|
48
50
|
if (list.code || list.killed)
|
|
49
51
|
return {
|
|
50
52
|
ok: false,
|
|
53
|
+
cause: signal?.aborted ? "cancelled" : "git_failure",
|
|
51
54
|
error: list.stderr.trim() || "Unable to inventory tracked files.",
|
|
52
55
|
};
|
|
53
56
|
const tracked = new Set(list.stdout.split("\0").filter(Boolean));
|
|
@@ -70,7 +73,11 @@ export async function collectDocsInventory(
|
|
|
70
73
|
inventory: tracked,
|
|
71
74
|
});
|
|
72
75
|
if (!opened.ok) {
|
|
73
|
-
limits.push({
|
|
76
|
+
limits.push({
|
|
77
|
+
path,
|
|
78
|
+
reason: opened.error,
|
|
79
|
+
...(opened.cause ? { cause: opened.cause } : {}),
|
|
80
|
+
});
|
|
74
81
|
return undefined;
|
|
75
82
|
}
|
|
76
83
|
const handle = opened.handle;
|
|
@@ -168,18 +175,22 @@ export function docsDeclarationSearch(
|
|
|
168
175
|
const path = candidates[index++];
|
|
169
176
|
if (path === undefined) return;
|
|
170
177
|
const location = await resolveInsideRepo(inventory.cwd, path);
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
(
|
|
174
|
-
await checkFileAdmission(
|
|
178
|
+
const admission = location.ok
|
|
179
|
+
? await checkFileAdmission(
|
|
175
180
|
inventory.cwd,
|
|
176
181
|
location.rel,
|
|
177
182
|
exec,
|
|
178
183
|
signal,
|
|
179
184
|
inventory.tracked,
|
|
180
185
|
)
|
|
181
|
-
|
|
182
|
-
if (
|
|
186
|
+
: location;
|
|
187
|
+
if (admission.ok) paths.push(path);
|
|
188
|
+
else if (admission.cause === "secret_pattern")
|
|
189
|
+
inventory.limits.push({
|
|
190
|
+
path,
|
|
191
|
+
reason: admission.error,
|
|
192
|
+
cause: admission.cause,
|
|
193
|
+
});
|
|
183
194
|
}
|
|
184
195
|
}),
|
|
185
196
|
);
|
|
@@ -188,12 +199,20 @@ export function docsDeclarationSearch(
|
|
|
188
199
|
const paths = await admittedPaths;
|
|
189
200
|
if (!paths.length) return new Map();
|
|
190
201
|
const patterns = ["-e", docsDeclarationPattern(names)];
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
202
|
+
let result:
|
|
203
|
+
| { stdout: string; stderr: string; code: number; killed: boolean }
|
|
204
|
+
| undefined;
|
|
205
|
+
try {
|
|
206
|
+
result = await exec("rg", ["--json", ...patterns, "--", ...paths], {
|
|
207
|
+
cwd: inventory.cwd,
|
|
208
|
+
timeout: TIMEOUT_MS,
|
|
209
|
+
signal,
|
|
210
|
+
});
|
|
211
|
+
} catch {
|
|
212
|
+
// A missing or unstartable rg is an unavailable search, not a failure.
|
|
213
|
+
signal?.throwIfAborted();
|
|
214
|
+
}
|
|
215
|
+
if (!result || result.killed || (result.code !== 0 && result.code !== 1)) {
|
|
197
216
|
inventory.limits.push({
|
|
198
217
|
path: "documentation",
|
|
199
218
|
reason: "declaration search unavailable",
|