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,45 @@
|
|
|
1
|
+
export function runnerVersion(value) {
|
|
2
|
+
return typeof value === "string" &&
|
|
3
|
+
/^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/.test(value)
|
|
4
|
+
? value
|
|
5
|
+
: undefined;
|
|
6
|
+
}
|
|
7
|
+
/** Parse only resolved lock versions, never dependency ranges or executable configuration. */
|
|
8
|
+
export function lockedRunnerVersion(runner, path, text) {
|
|
9
|
+
if (path.endsWith("package-lock.json")) {
|
|
10
|
+
try {
|
|
11
|
+
const lock = JSON.parse(text);
|
|
12
|
+
return runnerVersion(lock.packages?.[`node_modules/${runner}`]?.version ??
|
|
13
|
+
lock.dependencies?.[runner]?.version);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (path.endsWith("bun.lock")) {
|
|
20
|
+
const match = new RegExp(`"${runner}"\\s*:\\s*\\[\\s*"${runner}@(\\d+\\.\\d+\\.\\d+(?:-[\\w.-]+)?)"`).exec(text);
|
|
21
|
+
return runnerVersion(match?.[1]);
|
|
22
|
+
}
|
|
23
|
+
if (path.endsWith("pnpm-lock.yaml")) {
|
|
24
|
+
const rootImporter = /(?:^|\n) {2}\.:\s*\n([\s\S]*?)(?=\n {2}[^ ]|\n[^ ]|$)/.exec(text)?.[1] ??
|
|
25
|
+
"";
|
|
26
|
+
const block = new RegExp(`(?:^|\\n)\\s{4,}${runner}:\\s*\\n\\s+specifier:[^\\n]*\\n\\s+version:\\s*['"]?([^\\s'"(]+)`).exec(rootImporter);
|
|
27
|
+
return runnerVersion(block?.[1]);
|
|
28
|
+
}
|
|
29
|
+
if (path.endsWith("yarn.lock")) {
|
|
30
|
+
const expression = new RegExp(`(?:^|\\n)["']?${runner}@[^\\n]*:\\s*\\n(?:[^\\n]*\\n)*?\\s+version[ :]+["']?([^\\s"']+)`, "g");
|
|
31
|
+
const versions = new Set([...text.matchAll(expression)].map((match) => runnerVersion(match[1])));
|
|
32
|
+
return versions.size === 1 ? [...versions][0] : undefined;
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
// Only identity-smoked release lines can receive native name filters.
|
|
37
|
+
export const VERIFIED_NAME_FILTERS = {
|
|
38
|
+
vitest: [
|
|
39
|
+
{ version: /^3\.2\.\d+$/, separator: " " },
|
|
40
|
+
{ version: /^4\.1\.\d+$/, separator: " " },
|
|
41
|
+
{ version: /^5\.0\.\d+$/, separator: " > " },
|
|
42
|
+
],
|
|
43
|
+
jest: [{ version: /^30\.\d+\.\d+$/, separator: " " }],
|
|
44
|
+
mocha: [{ version: /^11\.8\.0$/, separator: " " }],
|
|
45
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Base-name globs of likely secret files, matched case-insensitively. */
|
|
2
|
+
export const SECRET_NAME_GLOBS = [
|
|
3
|
+
".env",
|
|
4
|
+
".env.*",
|
|
5
|
+
"*.pem",
|
|
6
|
+
"id_rsa*",
|
|
7
|
+
"*.p12",
|
|
8
|
+
"credentials*",
|
|
9
|
+
"secrets*",
|
|
10
|
+
];
|
|
11
|
+
/** Value-free templates that stay admitted despite the `.env.*` rule. */
|
|
12
|
+
const templates = [".env.example", ".env.sample", ".env.template"];
|
|
13
|
+
/**
|
|
14
|
+
* True when a path's base name matches SECRET_NAME_GLOBS (any depth,
|
|
15
|
+
* case-insensitive) and is not a value-free `.env` template. Such files are
|
|
16
|
+
* refused before their content is read, on every evidence admission path;
|
|
17
|
+
* there is no per-call override.
|
|
18
|
+
*/
|
|
19
|
+
export function isSecretPath(path) {
|
|
20
|
+
const name = (path.split(/[\\/]/).pop() ?? "").toLowerCase();
|
|
21
|
+
if (templates.includes(name))
|
|
22
|
+
return false;
|
|
23
|
+
return (name === ".env" ||
|
|
24
|
+
name.startsWith(".env.") ||
|
|
25
|
+
name.endsWith(".pem") ||
|
|
26
|
+
name.startsWith("id_rsa") ||
|
|
27
|
+
name.endsWith(".p12") ||
|
|
28
|
+
name.startsWith("credentials") ||
|
|
29
|
+
name.startsWith("secrets"));
|
|
30
|
+
}
|
|
31
|
+
/** Refusal text for a secret-named path: names the path, never its content. */
|
|
32
|
+
export function secretRefusal(path) {
|
|
33
|
+
return `${path}: secret file name (secret_pattern): content not read, not sent to Jev`;
|
|
34
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { LOCATE_SECTION_MAX_LINES, LOCATE_SECTION_MIN_LINES, LOCATE_WINDOW_LINES, } from "../constants.js";
|
|
2
|
+
/** Exact Markdown heading sections; callers may apply their own display grouping. */
|
|
3
|
+
export function markdownSections(text) {
|
|
4
|
+
const lines = text.split("\n");
|
|
5
|
+
if (lines.at(-1) === "")
|
|
6
|
+
lines.pop();
|
|
7
|
+
const headings = [];
|
|
8
|
+
let fence;
|
|
9
|
+
let frontMatter = lines[0]?.trim() === "---";
|
|
10
|
+
let html;
|
|
11
|
+
let previousText = false;
|
|
12
|
+
for (let index = 0; index < lines.length; index++) {
|
|
13
|
+
const line = lines[index] ?? "";
|
|
14
|
+
if (frontMatter) {
|
|
15
|
+
if (index > 0 && /^(?:---|\.\.\.)\s*$/.test(line))
|
|
16
|
+
frontMatter = false;
|
|
17
|
+
previousText = false;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (html) {
|
|
21
|
+
if (html === "blank" ? !line.trim() : line.toLowerCase().includes(html))
|
|
22
|
+
html = undefined;
|
|
23
|
+
previousText = false;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const marker = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
|
|
27
|
+
if (fence) {
|
|
28
|
+
if (marker &&
|
|
29
|
+
marker[1]?.[0] === fence.character &&
|
|
30
|
+
marker[1].length >= fence.length &&
|
|
31
|
+
!marker[2]?.trim())
|
|
32
|
+
fence = undefined;
|
|
33
|
+
previousText = false;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (marker && !(marker[1]?.[0] === "`" && marker[2]?.includes("`"))) {
|
|
37
|
+
fence = {
|
|
38
|
+
character: marker[1]?.[0] ?? "`",
|
|
39
|
+
length: marker[1]?.length ?? 3,
|
|
40
|
+
};
|
|
41
|
+
previousText = false;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const block = /^ {0,3}<(script|pre|style|textarea)(?:\s|>|$)/i.exec(line);
|
|
45
|
+
const closing = line.trimStart().startsWith("<!--")
|
|
46
|
+
? "-->"
|
|
47
|
+
: block
|
|
48
|
+
? `</${block[1]?.toLowerCase()}>`
|
|
49
|
+
: /^ {0,3}<\/?(?:address|article|aside|blockquote|div|dl|fieldset|figure|footer|form|h[1-6]|header|hr|li|main|nav|ol|p|section|table|tbody|td|th|tr|ul)(?:\s|\/?>|$)/i.test(line)
|
|
50
|
+
? "blank"
|
|
51
|
+
: undefined;
|
|
52
|
+
if (closing) {
|
|
53
|
+
if (closing === "blank" || !line.toLowerCase().includes(closing))
|
|
54
|
+
html = closing;
|
|
55
|
+
previousText = false;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const heading = /^ {0,3}#{1,6}(?:[\t ]+|$)(.*?)\s*$/.exec(line);
|
|
59
|
+
if (heading) {
|
|
60
|
+
headings.push({
|
|
61
|
+
start: index + 1,
|
|
62
|
+
level: line.trimStart().match(/^#+/)?.[0].length ?? 1,
|
|
63
|
+
label: (heading[1] ?? "").replace(/[\t ]+#+[\t ]*$/, "").trim(),
|
|
64
|
+
});
|
|
65
|
+
previousText = false;
|
|
66
|
+
}
|
|
67
|
+
else if (previousText && /^ {0,3}(?:=+|-+)\s*$/.test(line)) {
|
|
68
|
+
headings.push({
|
|
69
|
+
start: index,
|
|
70
|
+
level: line.trimStart().startsWith("=") ? 1 : 2,
|
|
71
|
+
label: lines[index - 1]?.trim() ?? "",
|
|
72
|
+
});
|
|
73
|
+
previousText = false;
|
|
74
|
+
}
|
|
75
|
+
else
|
|
76
|
+
previousText = Boolean(line.trim()) && !/^ {4}|^\t/.test(line);
|
|
77
|
+
}
|
|
78
|
+
return headings.map((heading, index) => {
|
|
79
|
+
const end = (headings[index + 1]?.start ?? lines.length + 1) - 1;
|
|
80
|
+
return {
|
|
81
|
+
id: `S${index + 1}`,
|
|
82
|
+
...heading,
|
|
83
|
+
end,
|
|
84
|
+
text: lines.slice(heading.start - 1, end).join("\n"),
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** Produces disjoint, exhaustive ranges; labels never replace the underlying evidence. */
|
|
89
|
+
export function sectionFile(path, text, declarations) {
|
|
90
|
+
const lines = text.split("\n");
|
|
91
|
+
if (lines.at(-1) === "")
|
|
92
|
+
lines.pop();
|
|
93
|
+
if (!lines.length)
|
|
94
|
+
return { sections: [], lines: 0, method: "windows" };
|
|
95
|
+
const boundaries = [];
|
|
96
|
+
let method = "windows";
|
|
97
|
+
if (declarations?.length) {
|
|
98
|
+
const sorted = [...declarations].sort((a, b) => a.start - b.start || b.end - a.end);
|
|
99
|
+
const ancestors = [];
|
|
100
|
+
for (let index = 0; index < sorted.length; index++) {
|
|
101
|
+
const declaration = sorted[index];
|
|
102
|
+
if (!declaration)
|
|
103
|
+
continue;
|
|
104
|
+
while ((ancestors.at(-1)?.end ?? Infinity) < declaration.end)
|
|
105
|
+
ancestors.pop();
|
|
106
|
+
const enclosing = ancestors.at(-1);
|
|
107
|
+
const suppressed = enclosing &&
|
|
108
|
+
enclosing.end - enclosing.start + 1 <= LOCATE_SECTION_MAX_LINES;
|
|
109
|
+
if (!suppressed && boundaries.at(-1)?.start !== declaration.start)
|
|
110
|
+
boundaries.push({ start: declaration.start, label: declaration.name });
|
|
111
|
+
ancestors.push(declaration);
|
|
112
|
+
if (!suppressed && declaration.end < lines.length)
|
|
113
|
+
boundaries.push({
|
|
114
|
+
start: declaration.end + 1,
|
|
115
|
+
label: `${enclosing?.name ?? declaration.name} continuation`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
boundaries.sort((a, b) => a.start - b.start ||
|
|
119
|
+
Number(a.label.endsWith(" continuation")) -
|
|
120
|
+
Number(b.label.endsWith(" continuation")));
|
|
121
|
+
for (let index = boundaries.length - 1; index > 0; index--)
|
|
122
|
+
if (boundaries[index]?.start === boundaries[index - 1]?.start)
|
|
123
|
+
boundaries.splice(index, 1);
|
|
124
|
+
method = "syntax";
|
|
125
|
+
}
|
|
126
|
+
else if (/\.(?:md|mdx|markdown)$/i.test(path)) {
|
|
127
|
+
for (const section of markdownSections(text))
|
|
128
|
+
boundaries.push({ start: section.start, label: section.label });
|
|
129
|
+
if (boundaries.length)
|
|
130
|
+
method = "markdown";
|
|
131
|
+
}
|
|
132
|
+
if (!boundaries.length) {
|
|
133
|
+
let pythonClass;
|
|
134
|
+
lines.forEach((line, index) => {
|
|
135
|
+
const indentation = line.length - line.trimStart().length;
|
|
136
|
+
if (path.endsWith(".py") &&
|
|
137
|
+
line.trim() &&
|
|
138
|
+
!line.trimStart().startsWith("#")) {
|
|
139
|
+
if (pythonClass && indentation <= pythonClass.indent)
|
|
140
|
+
pythonClass = undefined;
|
|
141
|
+
const className = /^\s*class\s+(\w+)/.exec(line)?.[1];
|
|
142
|
+
if (className) {
|
|
143
|
+
pythonClass = {
|
|
144
|
+
name: className,
|
|
145
|
+
indent: indentation,
|
|
146
|
+
start: index + 1,
|
|
147
|
+
seenMethod: false,
|
|
148
|
+
};
|
|
149
|
+
boundaries.push({ start: index + 1, label: className });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const declaration = /^\s*(?:(?:export|default|async|public|private|protected|static|abstract)\s+)*(?:(?:function\s*\*?|class|interface|enum|type|def|fn|func|struct|impl)\s+([\w.$]+)|(?:const|let|var)\s+([\w$]+)\s*=|([\w$]+)\s*\([^;]*\)\s*(?::[^=]+)?\s*\{)/.exec(line);
|
|
154
|
+
if (!declaration)
|
|
155
|
+
return;
|
|
156
|
+
const indent = line.length - line.trimStart().length;
|
|
157
|
+
if (indent > 4 ||
|
|
158
|
+
/^(?:if|for|while|switch|catch)$/.test(declaration[3] ?? ""))
|
|
159
|
+
return;
|
|
160
|
+
let start = index + 1;
|
|
161
|
+
if (path.endsWith(".py") &&
|
|
162
|
+
pythonClass &&
|
|
163
|
+
/^\s*(?:async\s+)?def\s/.test(line) &&
|
|
164
|
+
!pythonClass.seenMethod) {
|
|
165
|
+
boundaries.pop();
|
|
166
|
+
pythonClass.seenMethod = true;
|
|
167
|
+
start = pythonClass.start;
|
|
168
|
+
}
|
|
169
|
+
boundaries.push({
|
|
170
|
+
start,
|
|
171
|
+
label: pythonClass
|
|
172
|
+
? `${pythonClass.name}.${declaration[1] ?? "method"}`
|
|
173
|
+
: (declaration[1] ?? declaration[2] ?? declaration[3] ?? "section"),
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
if (boundaries.length)
|
|
177
|
+
method = "heuristic";
|
|
178
|
+
}
|
|
179
|
+
if (!boundaries.length) {
|
|
180
|
+
for (let start = 1; start <= lines.length; start += LOCATE_WINDOW_LINES)
|
|
181
|
+
boundaries.push({
|
|
182
|
+
start,
|
|
183
|
+
label: `lines ${start}-${Math.min(lines.length, start + LOCATE_WINDOW_LINES - 1)}`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
const first = boundaries[0];
|
|
188
|
+
if (first)
|
|
189
|
+
first.start = 1;
|
|
190
|
+
}
|
|
191
|
+
const ranges = boundaries.map((boundary, index) => ({
|
|
192
|
+
...boundary,
|
|
193
|
+
end: (boundaries[index + 1]?.start ?? lines.length + 1) - 1,
|
|
194
|
+
}));
|
|
195
|
+
// Split oversized leaf nodes without losing a line or inventing syntax.
|
|
196
|
+
const chunks = ranges.flatMap((range) => {
|
|
197
|
+
const result = [];
|
|
198
|
+
const size = method === "windows" ? LOCATE_WINDOW_LINES : LOCATE_SECTION_MAX_LINES;
|
|
199
|
+
for (let start = range.start; start <= range.end; start += size)
|
|
200
|
+
result.push({
|
|
201
|
+
start,
|
|
202
|
+
end: Math.min(range.end, start + size - 1),
|
|
203
|
+
label: range.label,
|
|
204
|
+
});
|
|
205
|
+
return result;
|
|
206
|
+
});
|
|
207
|
+
const merged = [];
|
|
208
|
+
for (const chunk of chunks) {
|
|
209
|
+
const previous = merged.at(-1);
|
|
210
|
+
if (method !== "windows" &&
|
|
211
|
+
previous &&
|
|
212
|
+
(chunk.end - chunk.start + 1 < LOCATE_SECTION_MIN_LINES ||
|
|
213
|
+
previous.end - previous.start + 1 < LOCATE_SECTION_MIN_LINES) &&
|
|
214
|
+
chunk.end - previous.start + 1 <= LOCATE_SECTION_MAX_LINES) {
|
|
215
|
+
previous.end = chunk.end;
|
|
216
|
+
previous.label += ` / ${chunk.label}`;
|
|
217
|
+
}
|
|
218
|
+
else
|
|
219
|
+
merged.push({ ...chunk });
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
lines: lines.length,
|
|
223
|
+
method,
|
|
224
|
+
sections: merged.map((range, index) => ({
|
|
225
|
+
id: `S${index + 1}`,
|
|
226
|
+
...range,
|
|
227
|
+
text: lines.slice(range.start - 1, range.end).join("\n"),
|
|
228
|
+
})),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ASK_NOTE_MAX_CHARS, STATE_MAX_CHARS } from "../constants.js";
|
|
2
|
+
import { isRecord } from "../result.js";
|
|
3
|
+
const reservedKeys = [
|
|
4
|
+
"files",
|
|
5
|
+
"files_before",
|
|
6
|
+
"base_sha",
|
|
7
|
+
"closure",
|
|
8
|
+
"evidence",
|
|
9
|
+
"output",
|
|
10
|
+
];
|
|
11
|
+
export function assembleState(note, files) {
|
|
12
|
+
if (note !== undefined && note.length > ASK_NOTE_MAX_CHARS)
|
|
13
|
+
return {
|
|
14
|
+
ok: false,
|
|
15
|
+
error: `state exceeds ${ASK_NOTE_MAX_CHARS} chars (${note.length}); shorten the note.`,
|
|
16
|
+
};
|
|
17
|
+
let state = {};
|
|
18
|
+
if (note !== undefined) {
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = JSON.parse(note);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
parsed = note;
|
|
25
|
+
}
|
|
26
|
+
const reserved = isRecord(parsed)
|
|
27
|
+
? reservedKeys.find((key) => Object.hasOwn(parsed, key))
|
|
28
|
+
: undefined;
|
|
29
|
+
if (reserved)
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
error: `state contains reserved key ${reserved}; rename it in your note and use paths for files read by code.`,
|
|
33
|
+
};
|
|
34
|
+
if (isRecord(parsed))
|
|
35
|
+
state = parsed;
|
|
36
|
+
else
|
|
37
|
+
state.state = parsed;
|
|
38
|
+
}
|
|
39
|
+
const noteSize = JSON.stringify(state).length;
|
|
40
|
+
if (Object.keys(files).length)
|
|
41
|
+
state.files = files;
|
|
42
|
+
const size = JSON.stringify(state).length;
|
|
43
|
+
if (size > STATE_MAX_CHARS)
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
error: `serialized state exceeds ${STATE_MAX_CHARS} chars (${size}); serialized parts: note ${noteSize}, ${Object.entries(files)
|
|
47
|
+
.map(([path, text]) => `${path} ${JSON.stringify({ [path]: text }).length}`)
|
|
48
|
+
.join(", ")}. Split the situation into smaller states or provide smaller source files; files are never truncated.`,
|
|
49
|
+
};
|
|
50
|
+
return { ok: true, state };
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|