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
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { accessSync, constants } from "node:fs";
|
|
2
|
+
// Candidates are Windows paths whatever the host OS, so the helpers use win32
|
|
3
|
+
// semantics explicitly; the host-native `node:path` splits PATH on ":" on POSIX.
|
|
4
|
+
import { win32 } from "node:path";
|
|
5
|
+
function executable(path) {
|
|
6
|
+
try {
|
|
7
|
+
accessSync(path, constants.X_OK);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Windows ships `bash.exe` launchers for WSL in System32 (and SysWOW64,
|
|
16
|
+
* Sysnative) and in WindowsApps. They run in a Linux VM that cannot see Windows
|
|
17
|
+
* temporary paths, so they are never used for command evidence, including
|
|
18
|
+
* when named explicitly in JEV_TOOLS_BASH.
|
|
19
|
+
*/
|
|
20
|
+
export function isWslLauncher(path) {
|
|
21
|
+
const lower = win32.normalize(path).toLowerCase();
|
|
22
|
+
return (/\\windows\\(system32|syswow64|sysnative)\\/.test(lower) ||
|
|
23
|
+
lower.includes("\\microsoft\\windowsapps\\"));
|
|
24
|
+
}
|
|
25
|
+
/** Permitted Git for Windows bash candidates, most specific first. */
|
|
26
|
+
export function windowsBashCandidates(env) {
|
|
27
|
+
const candidates = [];
|
|
28
|
+
if (env.JEV_TOOLS_BASH?.trim())
|
|
29
|
+
candidates.push(env.JEV_TOOLS_BASH.trim());
|
|
30
|
+
const pathEntries = (env.PATH ?? env.Path ?? "")
|
|
31
|
+
.split(win32.delimiter)
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
for (const entry of pathEntries) {
|
|
34
|
+
candidates.push(win32.join(entry, "bash.exe"));
|
|
35
|
+
// Git\cmd\git.exe or Git\bin\git.exe on PATH implies Git\bin\bash.exe.
|
|
36
|
+
if (/[\\/](cmd|bin)$/i.test(entry))
|
|
37
|
+
candidates.push(win32.join(win32.dirname(entry), "bin", "bash.exe"));
|
|
38
|
+
}
|
|
39
|
+
for (const root of [
|
|
40
|
+
env.ProgramFiles,
|
|
41
|
+
env["ProgramFiles(x86)"],
|
|
42
|
+
env.LOCALAPPDATA && win32.join(env.LOCALAPPDATA, "Programs"),
|
|
43
|
+
])
|
|
44
|
+
if (root)
|
|
45
|
+
candidates.push(win32.join(root, "Git", "bin", "bash.exe"));
|
|
46
|
+
// Filter every source, so no candidate can reintroduce a WSL launcher.
|
|
47
|
+
return [...new Set(candidates)].filter((candidate) => win32.isAbsolute(candidate) && !isWslLauncher(candidate));
|
|
48
|
+
}
|
|
49
|
+
const UNAVAILABLE = "No permitted bash found: install Git for Windows or set JEV_TOOLS_BASH to the full path of a bash that is not the WSL launcher.";
|
|
50
|
+
/** Never handed to command children: the Jev API key stays with the extension. */
|
|
51
|
+
export const API_KEY_VARIABLE = "JEV_TOOLS_API_KEY";
|
|
52
|
+
/** Copy of `env` without the Jev API key (any case, for Windows). */
|
|
53
|
+
export function withoutApiKey(env) {
|
|
54
|
+
const copy = { ...env };
|
|
55
|
+
for (const key of Object.keys(copy))
|
|
56
|
+
if (key.toUpperCase() === API_KEY_VARIABLE)
|
|
57
|
+
delete copy[key];
|
|
58
|
+
return copy;
|
|
59
|
+
}
|
|
60
|
+
let cached;
|
|
61
|
+
/**
|
|
62
|
+
* POSIX hosts run `env -u JEV_TOOLS_API_KEY CI=1 bash -c`. Windows has no
|
|
63
|
+
* `env` and its default `bash` is the WSL launcher, so resolve Git for Windows
|
|
64
|
+
* bash (or JEV_TOOLS_BASH) to an absolute, permitted path, and unset the key
|
|
65
|
+
* and export CI inside the script. When none is found this fails closed: no
|
|
66
|
+
* bare name is returned, because spawning one would search PATH again without
|
|
67
|
+
* the WSL exclusion.
|
|
68
|
+
*/
|
|
69
|
+
export function resolveShell(platform = process.platform, env = process.env, isExecutable = executable) {
|
|
70
|
+
if (platform !== "win32")
|
|
71
|
+
return {
|
|
72
|
+
ok: true,
|
|
73
|
+
executable: "env",
|
|
74
|
+
prefix: ["-u", API_KEY_VARIABLE, "CI=1", "bash"],
|
|
75
|
+
scriptPrefix: "",
|
|
76
|
+
};
|
|
77
|
+
const useCache = isExecutable === executable && env === process.env;
|
|
78
|
+
if (useCache && cached)
|
|
79
|
+
return cached;
|
|
80
|
+
const found = windowsBashCandidates(env).find((path) => isExecutable(path));
|
|
81
|
+
const resolved = found
|
|
82
|
+
? {
|
|
83
|
+
ok: true,
|
|
84
|
+
executable: found,
|
|
85
|
+
prefix: [],
|
|
86
|
+
scriptPrefix: `unset ${API_KEY_VARIABLE}; export CI=1; `,
|
|
87
|
+
}
|
|
88
|
+
: { ok: false, error: UNAVAILABLE };
|
|
89
|
+
if (useCache && found)
|
|
90
|
+
cached = resolved;
|
|
91
|
+
return resolved;
|
|
92
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
let parserPromise;
|
|
2
|
+
/** Optional grammars resolve from this extension, not the repository under review. */
|
|
3
|
+
export function loadSyntaxParser() {
|
|
4
|
+
parserPromise ??= createParser();
|
|
5
|
+
return parserPromise;
|
|
6
|
+
}
|
|
7
|
+
/** Raw AST access shares grammar availability and registration with declaration extraction. */
|
|
8
|
+
export function loadSyntaxRootParser() {
|
|
9
|
+
parserPromise ??= createParser();
|
|
10
|
+
return parserPromise;
|
|
11
|
+
}
|
|
12
|
+
async function createParser() {
|
|
13
|
+
try {
|
|
14
|
+
const napi = await import("@ast-grep/napi");
|
|
15
|
+
let python = false;
|
|
16
|
+
try {
|
|
17
|
+
const language = await import("@ast-grep/lang-python");
|
|
18
|
+
napi.registerDynamicLanguage({ python: language.default });
|
|
19
|
+
python = true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
/* buildUnits reports the missing grammar for each affected file. */
|
|
23
|
+
}
|
|
24
|
+
const parser = {
|
|
25
|
+
supports: (path) => /\.[cm]?[jt]sx?$/.test(path) || (python && path.endsWith(".py")),
|
|
26
|
+
parseRaw(path, text) {
|
|
27
|
+
try {
|
|
28
|
+
const language = path.endsWith(".py")
|
|
29
|
+
? "python"
|
|
30
|
+
: /\.[cm]?tsx?$/.test(path)
|
|
31
|
+
? path.endsWith("tsx")
|
|
32
|
+
? napi.Lang.Tsx
|
|
33
|
+
: napi.Lang.TypeScript
|
|
34
|
+
: path.endsWith(".jsx")
|
|
35
|
+
? "jsx"
|
|
36
|
+
: napi.Lang.JavaScript;
|
|
37
|
+
const root = napi.parse(language, text).root();
|
|
38
|
+
return { ok: true, root };
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return { ok: false, error: "Source parser unavailable" };
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
parse(path, text) {
|
|
45
|
+
const parsed = this.parseRaw(path, text);
|
|
46
|
+
if (!parsed.ok)
|
|
47
|
+
return parsed;
|
|
48
|
+
return parsed.root.find({ rule: { kind: "ERROR" } })
|
|
49
|
+
? { ok: false, error: "Source syntax incomplete" }
|
|
50
|
+
: parsed;
|
|
51
|
+
},
|
|
52
|
+
declarations(path, text, options) {
|
|
53
|
+
const isPython = path.endsWith(".py");
|
|
54
|
+
const parsed = this.parseRaw(path, text);
|
|
55
|
+
if (!parsed.ok)
|
|
56
|
+
return { declarations: [], limits: [{ kind: "parse_failed" }] };
|
|
57
|
+
const root = parsed.root;
|
|
58
|
+
const result = [];
|
|
59
|
+
const limits = [];
|
|
60
|
+
const errors = root.findAll({ rule: { kind: "ERROR" } });
|
|
61
|
+
const declarationKind = /^(?:function_declaration|generator_function_declaration|method_definition|function_definition|lexical_declaration|variable_declaration|type_alias_declaration|interface_declaration|enum_declaration|class_declaration|class_definition)$/;
|
|
62
|
+
if (errors.some((error) => !error
|
|
63
|
+
.ancestors()
|
|
64
|
+
.some((node) => declarationKind.test(String(node.kind()))) &&
|
|
65
|
+
(error
|
|
66
|
+
.findAll({ rule: { regex: ".*" } })
|
|
67
|
+
.some((node) => declarationKind.test(String(node.kind()))) ||
|
|
68
|
+
/^(?:export\s+)?(?:function|const|let|var|class)\b/.test(error.text().trim()))))
|
|
69
|
+
return { declarations: [], limits: [{ kind: "parse_failed" }] };
|
|
70
|
+
const attributedErrors = new Set();
|
|
71
|
+
const source = text.split("\n");
|
|
72
|
+
const visit = (node) => {
|
|
73
|
+
const kind = String(node.kind());
|
|
74
|
+
// Most AST nodes cannot yield a declaration; avoid constructing their ancestor chains.
|
|
75
|
+
if (!/^(?:function_declaration|generator_function_declaration|method_definition|function_definition|lexical_declaration|variable_declaration|type_alias_declaration|interface_declaration|enum_declaration|expression_statement|class_declaration|class_definition)$/.test(kind)) {
|
|
76
|
+
for (const child of node.children())
|
|
77
|
+
visit(child);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const ancestors = node.ancestors();
|
|
81
|
+
const functionNode = /^(?:function_declaration|generator_function_declaration|method_definition|function_definition)$/.test(kind);
|
|
82
|
+
const variable = /^(?:lexical_declaration|variable_declaration|type_alias_declaration|interface_declaration|enum_declaration)$/.test(kind) &&
|
|
83
|
+
ancestors.every((a) => !/function|method|statement_block/.test(String(a.kind())));
|
|
84
|
+
const callableVariable = variable &&
|
|
85
|
+
node.find({
|
|
86
|
+
rule: {
|
|
87
|
+
any: [
|
|
88
|
+
{ kind: "arrow_function" },
|
|
89
|
+
{ kind: "function_expression" },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
}) !== null;
|
|
93
|
+
const assignment = isPython &&
|
|
94
|
+
kind === "expression_statement" &&
|
|
95
|
+
node.parent()?.kind() === "module" &&
|
|
96
|
+
node.children().some((child) => child.kind() === "assignment");
|
|
97
|
+
const classNode = options?.descendAbove !== undefined &&
|
|
98
|
+
/^(?:class_declaration|class_definition)$/.test(kind);
|
|
99
|
+
if (functionNode || variable || assignment || classNode) {
|
|
100
|
+
let envelope = node;
|
|
101
|
+
if (node.parent()?.kind() === "decorated_definition" ||
|
|
102
|
+
node.parent()?.kind() === "export_statement")
|
|
103
|
+
envelope = node.parent() ?? node;
|
|
104
|
+
const named = node.field("name") ?? node.find({ rule: { kind: "identifier" } });
|
|
105
|
+
const owner = ancestors
|
|
106
|
+
.filter((a) => /^(?:class_definition|class_declaration|internal_module|module)$/.test(String(a.kind())))
|
|
107
|
+
.reverse()
|
|
108
|
+
.map((a) => a.field("name")?.text())
|
|
109
|
+
.filter(Boolean);
|
|
110
|
+
let name = [...owner, named?.text() ?? "declaration"].join(".");
|
|
111
|
+
if (assignment)
|
|
112
|
+
name = node.text().split(/\s*[:=]/, 1)[0] ?? "declaration";
|
|
113
|
+
let start = envelope.range().start.line;
|
|
114
|
+
const body = node.field("body") ??
|
|
115
|
+
(callableVariable
|
|
116
|
+
? node
|
|
117
|
+
.find({
|
|
118
|
+
rule: {
|
|
119
|
+
any: [
|
|
120
|
+
{ kind: "arrow_function" },
|
|
121
|
+
{ kind: "function_expression" },
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
?.field("body")
|
|
126
|
+
: null);
|
|
127
|
+
const tokens = (part) => {
|
|
128
|
+
const children = part.children();
|
|
129
|
+
if (!children.length ||
|
|
130
|
+
/string|comment/.test(String(part.kind())))
|
|
131
|
+
return part.kind() === "comment" ? [] : [part.text()];
|
|
132
|
+
return children.flatMap(tokens);
|
|
133
|
+
};
|
|
134
|
+
const accessor = kind === "method_definition"
|
|
135
|
+
? node
|
|
136
|
+
.children()
|
|
137
|
+
.find((child) => /^(get|set)$/.test(String(child.kind())))
|
|
138
|
+
?.kind()
|
|
139
|
+
: undefined;
|
|
140
|
+
const end = envelope.range().end;
|
|
141
|
+
// Include the immediately preceding comment block without swallowing another declaration.
|
|
142
|
+
while (start > 0 &&
|
|
143
|
+
/^\s*(?:\/\/|\/\*|\*|\*\/|#)/.test(source[start - 1] ?? ""))
|
|
144
|
+
start--;
|
|
145
|
+
result.push({
|
|
146
|
+
kind: functionNode || callableVariable ? "function" : "declaration",
|
|
147
|
+
name,
|
|
148
|
+
type: accessor ? `${kind}:${accessor}` : kind,
|
|
149
|
+
signatureEnd: body
|
|
150
|
+
? body.range().start.line + (isPython ? 0 : 1)
|
|
151
|
+
: undefined,
|
|
152
|
+
body: body ? JSON.stringify(tokens(body)) : undefined,
|
|
153
|
+
exported: isPython
|
|
154
|
+
? !name.split(".").some((part) => part.startsWith("_"))
|
|
155
|
+
: ancestors.some((a) => a.kind() === "export_statement") ||
|
|
156
|
+
envelope.kind() === "export_statement",
|
|
157
|
+
start: start + 1,
|
|
158
|
+
end: end.line + (end.column === 0 ? 0 : 1),
|
|
159
|
+
});
|
|
160
|
+
const declarationErrors = envelope.findAll({
|
|
161
|
+
rule: { kind: "ERROR" },
|
|
162
|
+
});
|
|
163
|
+
if (declarationErrors.length) {
|
|
164
|
+
limits.push({ kind: "parse_partial", declaration: name });
|
|
165
|
+
for (const error of declarationErrors)
|
|
166
|
+
attributedErrors.add(error.id());
|
|
167
|
+
}
|
|
168
|
+
if (options?.descendAbove === undefined ||
|
|
169
|
+
end.line + (end.column === 0 ? 0 : 1) - start <=
|
|
170
|
+
options.descendAbove)
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
for (const child of node.children())
|
|
174
|
+
visit(child);
|
|
175
|
+
};
|
|
176
|
+
visit(root);
|
|
177
|
+
if (errors.some((error) => !attributedErrors.has(error.id())))
|
|
178
|
+
limits.push({ kind: "parse_partial" });
|
|
179
|
+
return { declarations: result, limits };
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
return parser;
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { lstat } from "node:fs/promises";
|
|
2
|
+
import { TEST_SOURCE_MAX_BYTES, TIMEOUT_MS } from "../constants.js";
|
|
3
|
+
import { isSecretPath } from "../core/secret-path.js";
|
|
4
|
+
import { checkFileAdmission, openRepoFile, resolveInsideRepo, } from "./files.js";
|
|
5
|
+
import { decodeUtf8 } from "./utf8.js";
|
|
6
|
+
export async function collectTestInventory(exec, cwd, signal) {
|
|
7
|
+
const root = await exec("git", ["rev-parse", "--show-toplevel"], {
|
|
8
|
+
cwd,
|
|
9
|
+
timeout: TIMEOUT_MS,
|
|
10
|
+
signal,
|
|
11
|
+
});
|
|
12
|
+
if (root.code !== 0 || root.killed)
|
|
13
|
+
return {
|
|
14
|
+
ok: false,
|
|
15
|
+
cause: signal?.aborted ? "cancelled" : "git_failure",
|
|
16
|
+
error: root.stderr || "Cannot identify repository root.",
|
|
17
|
+
};
|
|
18
|
+
cwd = root.stdout.replace(/\n$/, "");
|
|
19
|
+
const listed = await exec("git", [
|
|
20
|
+
"ls-files",
|
|
21
|
+
"--cached",
|
|
22
|
+
"--others",
|
|
23
|
+
"--exclude-standard",
|
|
24
|
+
"--full-name",
|
|
25
|
+
"-z",
|
|
26
|
+
], { cwd, timeout: TIMEOUT_MS, signal });
|
|
27
|
+
if (listed.code !== 0 || listed.killed)
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
cause: signal?.aborted ? "cancelled" : "git_failure",
|
|
31
|
+
error: listed.stderr || "Cannot inventory repository files.",
|
|
32
|
+
};
|
|
33
|
+
const ignored = await exec("git", ["ls-files", "--cached", "--ignored", "--exclude-standard", "-z"], { cwd, timeout: TIMEOUT_MS, signal });
|
|
34
|
+
if (ignored.code !== 0 || ignored.killed)
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
cause: signal?.aborted ? "cancelled" : "git_failure",
|
|
38
|
+
error: ignored.stderr || "Cannot identify ignored tracked files.",
|
|
39
|
+
};
|
|
40
|
+
const excluded = new Set(ignored.stdout.split("\0").filter(Boolean));
|
|
41
|
+
const listedPaths = listed.stdout
|
|
42
|
+
.split("\0")
|
|
43
|
+
.filter((path) => path &&
|
|
44
|
+
(!excluded.has(path) ||
|
|
45
|
+
/(?:^|\/)(?:package|tsconfig[^/]*)\.json$/.test(path)));
|
|
46
|
+
// Secret-named files are named exclusions, never inventory members to read.
|
|
47
|
+
const paths = listedPaths.filter((path) => !isSecretPath(path));
|
|
48
|
+
const admitted = new Set(paths);
|
|
49
|
+
const limits = listedPaths
|
|
50
|
+
.filter(isSecretPath)
|
|
51
|
+
.map((path) => ({ path, kind: "secret_pattern" }));
|
|
52
|
+
const cache = new Map();
|
|
53
|
+
for (let offset = 0; offset < paths.length; offset += 16) {
|
|
54
|
+
const inspected = await Promise.all(paths.slice(offset, offset + 16).map(async (path) => {
|
|
55
|
+
try {
|
|
56
|
+
signal?.throwIfAborted();
|
|
57
|
+
const admission = await checkFileAdmission(cwd, path, exec, signal, admitted);
|
|
58
|
+
if (!admission.ok)
|
|
59
|
+
return "unreadable";
|
|
60
|
+
const resolved = await resolveInsideRepo(cwd, path);
|
|
61
|
+
if (!resolved.ok)
|
|
62
|
+
return "unreadable";
|
|
63
|
+
const stat = await lstat(resolved.abs);
|
|
64
|
+
if (!stat.isFile())
|
|
65
|
+
return "unreadable";
|
|
66
|
+
return stat.size > TEST_SOURCE_MAX_BYTES
|
|
67
|
+
? "too_large"
|
|
68
|
+
: undefined;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return "unreadable";
|
|
72
|
+
}
|
|
73
|
+
}));
|
|
74
|
+
for (const [index, kind] of inspected.entries()) {
|
|
75
|
+
if (!kind)
|
|
76
|
+
continue;
|
|
77
|
+
const path = paths[offset + index];
|
|
78
|
+
if (path === undefined || cache.has(path))
|
|
79
|
+
continue;
|
|
80
|
+
limits.push({ path, kind });
|
|
81
|
+
cache.set(path, Promise.resolve(undefined));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const read = (path) => {
|
|
85
|
+
const cached = cache.get(path);
|
|
86
|
+
if (cached)
|
|
87
|
+
return cached;
|
|
88
|
+
const pending = (async () => {
|
|
89
|
+
let handle;
|
|
90
|
+
try {
|
|
91
|
+
signal?.throwIfAborted();
|
|
92
|
+
const opened = await openRepoFile(cwd, path, {
|
|
93
|
+
exec,
|
|
94
|
+
signal,
|
|
95
|
+
inventory: admitted,
|
|
96
|
+
});
|
|
97
|
+
if (!opened.ok)
|
|
98
|
+
return { path, kind: "unreadable" };
|
|
99
|
+
handle = opened.handle;
|
|
100
|
+
const stat = { size: opened.size };
|
|
101
|
+
if (stat.size > TEST_SOURCE_MAX_BYTES)
|
|
102
|
+
return { path, kind: "too_large" };
|
|
103
|
+
const bytes = Buffer.alloc(Math.min(stat.size + 1, TEST_SOURCE_MAX_BYTES + 1));
|
|
104
|
+
let count = 0;
|
|
105
|
+
while (count < bytes.length) {
|
|
106
|
+
signal?.throwIfAborted();
|
|
107
|
+
const part = await handle.read(bytes, count, bytes.length - count, null);
|
|
108
|
+
if (!part.bytesRead)
|
|
109
|
+
break;
|
|
110
|
+
count += part.bytesRead;
|
|
111
|
+
}
|
|
112
|
+
if (count === bytes.length)
|
|
113
|
+
return { path, kind: "too_large" };
|
|
114
|
+
if (bytes.subarray(0, count).includes(0))
|
|
115
|
+
return { path, text: "" };
|
|
116
|
+
const decoded = decodeUtf8(bytes.subarray(0, count));
|
|
117
|
+
if (!decoded.ok)
|
|
118
|
+
return { path, kind: "not UTF-8 text" };
|
|
119
|
+
const text = decoded.text;
|
|
120
|
+
return { path, text };
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return { path, kind: "unreadable" };
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
await handle?.close();
|
|
127
|
+
}
|
|
128
|
+
})().then((file) => {
|
|
129
|
+
if (file.kind) {
|
|
130
|
+
limits.push({ path: file.path, kind: file.kind });
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
return { path: file.path, text: file.text };
|
|
134
|
+
});
|
|
135
|
+
cache.set(path, pending);
|
|
136
|
+
return pending;
|
|
137
|
+
};
|
|
138
|
+
return { ok: true, cwd, paths, read, limits };
|
|
139
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function hostUsage(isOmp, usage) {
|
|
2
|
+
if (isOmp || !usage)
|
|
3
|
+
return {};
|
|
4
|
+
return {
|
|
5
|
+
usage: {
|
|
6
|
+
input: usage.inputTokens,
|
|
7
|
+
output: 0,
|
|
8
|
+
cacheRead: 0,
|
|
9
|
+
cacheWrite: 0,
|
|
10
|
+
totalTokens: usage.inputTokens,
|
|
11
|
+
cost: {
|
|
12
|
+
input: usage.costUsd,
|
|
13
|
+
output: 0,
|
|
14
|
+
cacheRead: 0,
|
|
15
|
+
cacheWrite: 0,
|
|
16
|
+
total: usage.costUsd,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { isUtf8 } from "node:buffer";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
/** Callers enforce byte budgets before decoding. BOM and legitimate controls are preserved. */
|
|
4
|
+
export function decodeUtf8(bytes) {
|
|
5
|
+
return isUtf8(bytes)
|
|
6
|
+
? {
|
|
7
|
+
ok: true,
|
|
8
|
+
text: Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("utf8"),
|
|
9
|
+
}
|
|
10
|
+
: { ok: false, error: "not UTF-8 text" };
|
|
11
|
+
}
|
|
12
|
+
/** Keep incomplete code points between chunks, but reject them at EOF. */
|
|
13
|
+
export function createUtf8Decoder() {
|
|
14
|
+
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
15
|
+
return (bytes) => {
|
|
16
|
+
try {
|
|
17
|
+
return {
|
|
18
|
+
ok: true,
|
|
19
|
+
text: decoder.decode(bytes, { stream: bytes !== undefined }),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return { ok: false, error: "not UTF-8 text" };
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Verify host-decoded text against the raw Git object; replacement cannot silently become evidence. */
|
|
28
|
+
export function verifyGitUtf8(text, id, size) {
|
|
29
|
+
const bytes = Buffer.from(text, "utf8");
|
|
30
|
+
if (size !== undefined && bytes.length !== size)
|
|
31
|
+
return { ok: false, error: "not UTF-8 text" };
|
|
32
|
+
if (id && /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(id)) {
|
|
33
|
+
const actual = createHash(id.length === 64 ? "sha256" : "sha1")
|
|
34
|
+
.update(`blob ${bytes.length}\0`)
|
|
35
|
+
.update(bytes)
|
|
36
|
+
.digest("hex");
|
|
37
|
+
if (actual !== id)
|
|
38
|
+
return { ok: false, error: "not UTF-8 text" };
|
|
39
|
+
}
|
|
40
|
+
else if (text.includes("\uFFFD")) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
error: "not verifiable as UTF-8 text (contains U+FFFD after host decoding)",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return decodeUtf8(bytes);
|
|
47
|
+
}
|