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,302 @@
|
|
|
1
|
+
import { DOCS_ANCHOR_MAX_FILES, DOCS_MAX_SECTIONS, DOCS_NAME_MAX_FILES, } from "../constants.js";
|
|
2
|
+
import { createImportGraphBuilder, importClosureLazy, } from "./imports.js";
|
|
3
|
+
import { markdownSections } from "./sections.js";
|
|
4
|
+
/** Sentences retain verbatim text, including Markdown and code; no paraphrase enters a pointer. */
|
|
5
|
+
export function docSentences(text) {
|
|
6
|
+
const result = [];
|
|
7
|
+
let fence;
|
|
8
|
+
const paragraphs = [];
|
|
9
|
+
let current = [];
|
|
10
|
+
for (const line of text.split("\n")) {
|
|
11
|
+
const marker = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
|
|
12
|
+
if (marker) {
|
|
13
|
+
if (!fence)
|
|
14
|
+
fence = {
|
|
15
|
+
marker: marker[1]?.[0] ?? "`",
|
|
16
|
+
length: marker[1]?.length ?? 3,
|
|
17
|
+
};
|
|
18
|
+
else if (marker[1]?.[0] === fence.marker &&
|
|
19
|
+
marker[1].length >= fence.length &&
|
|
20
|
+
!marker[2]?.trim())
|
|
21
|
+
fence = undefined;
|
|
22
|
+
current.push(line);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (!fence && !line.trim()) {
|
|
26
|
+
if (current.length)
|
|
27
|
+
paragraphs.push(current.join("\n"));
|
|
28
|
+
current = [];
|
|
29
|
+
}
|
|
30
|
+
else
|
|
31
|
+
current.push(line);
|
|
32
|
+
}
|
|
33
|
+
if (current.length)
|
|
34
|
+
paragraphs.push(current.join("\n"));
|
|
35
|
+
for (const paragraph of paragraphs) {
|
|
36
|
+
// A fenced block remains one exact piece; punctuation in code is never a sentence boundary.
|
|
37
|
+
const pieces = /^ {0,3}(?:`{3,}|~{3,})/m.test(paragraph)
|
|
38
|
+
? [paragraph]
|
|
39
|
+
: paragraph.split(/(?<=[.!?])(?:[\t ]+|\n)(?=[A-ZÀ-ÖØ-Þ`*])/u);
|
|
40
|
+
for (const text of pieces)
|
|
41
|
+
if (text.trim())
|
|
42
|
+
result.push({ id: `s${result.length + 1}`, text });
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
export function inlineDocIdentifiers(text) {
|
|
47
|
+
return [
|
|
48
|
+
...new Set([...text.matchAll(/(?<!`)`([A-Za-z_$][\w$]{2,})`(?!`)/g)].map((match) => match[1] ?? "")),
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
export function docsDeclarationPattern(name) {
|
|
52
|
+
const names = typeof name === "string" ? [name] : name;
|
|
53
|
+
const escaped = names
|
|
54
|
+
.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
55
|
+
.join("|");
|
|
56
|
+
return `(?:\\b(?:export|const|let|var|function|class|type|interface|enum|def)\\s+(?:(?:default|async|function|const|class)\\s+)*(${escaped})(?:[^\\w$]|$)|^\\s*(?:(?:public|private|protected|static|async)\\s+)*(${escaped})\\s*\\([^;]*\\)\\s*(?::[^={]+)?\\s*[{])`;
|
|
57
|
+
}
|
|
58
|
+
export function attributeDocsDeclarations(names, rows) {
|
|
59
|
+
const owners = new Map(names.map((name) => [name, new Set()]));
|
|
60
|
+
if (!names.length)
|
|
61
|
+
return new Map();
|
|
62
|
+
const pattern = new RegExp(docsDeclarationPattern(names), "gm");
|
|
63
|
+
for (const row of rows)
|
|
64
|
+
for (const match of row.text.matchAll(pattern)) {
|
|
65
|
+
const paths = owners.get(match[1] ?? match[2] ?? "");
|
|
66
|
+
if (paths && paths.size <= DOCS_NAME_MAX_FILES)
|
|
67
|
+
paths.add(row.path);
|
|
68
|
+
}
|
|
69
|
+
const result = new Map([...owners].map(([name, paths]) => [name, [...paths]]));
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
export async function collectDocsCandidates(files, units, sources) {
|
|
73
|
+
const candidates = [];
|
|
74
|
+
const omitted = [];
|
|
75
|
+
const limits = [];
|
|
76
|
+
const sections = files
|
|
77
|
+
.filter((file) => /\.(?:md|mdx|markdown)$/i.test(file.path))
|
|
78
|
+
.flatMap((file) => markdownSections(file.text).map((section) => ({
|
|
79
|
+
file: file.path,
|
|
80
|
+
identifiers: new Set(inlineDocIdentifiers(section.text)),
|
|
81
|
+
...section,
|
|
82
|
+
paths: [
|
|
83
|
+
...new Set((section.text.match(/[A-Za-z0-9_@./-]+/g) ?? [])
|
|
84
|
+
.map((path) => sources.known.has(path) ? path : path.replace(/\.+$/, ""))
|
|
85
|
+
.filter((path) => sources.known.has(path))),
|
|
86
|
+
],
|
|
87
|
+
})));
|
|
88
|
+
const selectedSections = new Set();
|
|
89
|
+
const reachable = new Map();
|
|
90
|
+
for (const unit of units) {
|
|
91
|
+
const selected = reachable.get(unit.file) ?? new Set();
|
|
92
|
+
selected.add(unit);
|
|
93
|
+
reachable.set(unit.file, selected);
|
|
94
|
+
}
|
|
95
|
+
const match = (paths, names) => {
|
|
96
|
+
// Paths outrank names within each proximity level; already selected sections never move.
|
|
97
|
+
for (const anchors of [paths, names]) {
|
|
98
|
+
if (!anchors.size)
|
|
99
|
+
continue;
|
|
100
|
+
for (let index = 0; index < sections.length; index++) {
|
|
101
|
+
if (selectedSections.has(index))
|
|
102
|
+
continue;
|
|
103
|
+
const section = sections[index];
|
|
104
|
+
if (!section)
|
|
105
|
+
continue;
|
|
106
|
+
const selected = new Set();
|
|
107
|
+
const identifiers = anchors === names ? section.identifiers : undefined;
|
|
108
|
+
for (const [anchor, values] of anchors) {
|
|
109
|
+
if (identifiers
|
|
110
|
+
? identifiers.has(anchor)
|
|
111
|
+
: section.text.includes(anchor))
|
|
112
|
+
for (const unit of values)
|
|
113
|
+
selected.add(unit);
|
|
114
|
+
}
|
|
115
|
+
if (!selected.size)
|
|
116
|
+
continue;
|
|
117
|
+
selectedSections.add(index);
|
|
118
|
+
const candidate = {
|
|
119
|
+
path: section.file,
|
|
120
|
+
heading: section.label,
|
|
121
|
+
start: section.start,
|
|
122
|
+
end: section.end,
|
|
123
|
+
sentences: docSentences(section.text),
|
|
124
|
+
units: units.filter((unit) => selected.has(unit)),
|
|
125
|
+
};
|
|
126
|
+
(candidates.length < DOCS_MAX_SECTIONS ? candidates : omitted).push(candidate);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const initialNames = new Map();
|
|
131
|
+
for (const unit of units) {
|
|
132
|
+
const selected = initialNames.get(unit.name) ?? new Set();
|
|
133
|
+
selected.add(unit);
|
|
134
|
+
initialNames.set(unit.name, selected);
|
|
135
|
+
}
|
|
136
|
+
for (const file of sources.changedFiles ?? []) {
|
|
137
|
+
const changed = file.hunks
|
|
138
|
+
.flatMap((hunk) => hunk.changes.flatMap((change) => [
|
|
139
|
+
...(file.before
|
|
140
|
+
?.split("\n")
|
|
141
|
+
.slice(change.beforeStart - 1, change.beforeStart - 1 + change.beforeCount) ?? []),
|
|
142
|
+
...(file.after
|
|
143
|
+
?.split("\n")
|
|
144
|
+
.slice(change.afterStart - 1, change.afterStart - 1 + change.afterCount) ?? []),
|
|
145
|
+
]))
|
|
146
|
+
.join("\n");
|
|
147
|
+
for (const section of sections)
|
|
148
|
+
for (const literal of section.text.matchAll(/(?<!`)`([^`\n]{3,})`(?!`)/g)) {
|
|
149
|
+
const text = literal[1] ?? "";
|
|
150
|
+
if (!/[^A-Za-z ]/.test(text) || !changed.includes(text))
|
|
151
|
+
continue;
|
|
152
|
+
const matching = units.filter((unit) => unit.file === file.path);
|
|
153
|
+
if (!matching.length)
|
|
154
|
+
continue;
|
|
155
|
+
const selected = initialNames.get(text) ?? new Set();
|
|
156
|
+
for (const unit of matching)
|
|
157
|
+
selected.add(unit);
|
|
158
|
+
initialNames.set(text, selected);
|
|
159
|
+
section.identifiers.add(text);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
match(reachable, initialNames);
|
|
163
|
+
if (candidates.length >= DOCS_MAX_SECTIONS) {
|
|
164
|
+
const unchecked = sections.filter((section, index) => !selectedSections.has(index) &&
|
|
165
|
+
(section.paths.length || section.identifiers.size));
|
|
166
|
+
if (unchecked.length)
|
|
167
|
+
limits.push({
|
|
168
|
+
path: "documentation",
|
|
169
|
+
kind: "collection_budget",
|
|
170
|
+
reason: "Candidate limit reached: documentation anchors unexplored.",
|
|
171
|
+
sections: unchecked.map((section) => `${section.file} § ${section.label}`),
|
|
172
|
+
});
|
|
173
|
+
return { candidates, omitted, limits };
|
|
174
|
+
}
|
|
175
|
+
const configuration = files.filter((file) => /(?:^|\/)(?:package|tsconfig[^/]*)\.json$/.test(file.path));
|
|
176
|
+
const cache = new Map();
|
|
177
|
+
const build = createImportGraphBuilder(configuration, sources.known, sources.lexical);
|
|
178
|
+
const modified = new Map(reachable);
|
|
179
|
+
const pending = sections.map((section, index) => ({ section, index }));
|
|
180
|
+
pending.sort((a, b) => Number(b.section.paths.length > 0) - Number(a.section.paths.length > 0));
|
|
181
|
+
const names = [
|
|
182
|
+
...new Set(pending.flatMap(({ section }) => [...section.identifiers])),
|
|
183
|
+
];
|
|
184
|
+
// The adapter performs one declarative search; no source is parsed to establish ownership.
|
|
185
|
+
const owners = sources.declarations
|
|
186
|
+
? await sources.declarations(names)
|
|
187
|
+
: new Map();
|
|
188
|
+
const anchors = new Map();
|
|
189
|
+
for (const { section, index } of pending) {
|
|
190
|
+
const paths = new Set(section.paths);
|
|
191
|
+
for (const name of section.identifiers) {
|
|
192
|
+
const pathsForName = owners.get(name) ?? [];
|
|
193
|
+
if (pathsForName.length === 1 &&
|
|
194
|
+
pathsForName.length <= DOCS_NAME_MAX_FILES)
|
|
195
|
+
for (const path of pathsForName)
|
|
196
|
+
paths.add(path);
|
|
197
|
+
}
|
|
198
|
+
for (const path of paths) {
|
|
199
|
+
const indices = anchors.get(path) ?? new Set();
|
|
200
|
+
indices.add(index);
|
|
201
|
+
anchors.set(path, indices);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const directory = (path) => path.slice(0, Math.max(0, path.lastIndexOf("/")));
|
|
205
|
+
const packageRoots = configuration
|
|
206
|
+
.filter((file) => /(?:^|\/)package\.json$/.test(file.path))
|
|
207
|
+
.map((file) => directory(file.path))
|
|
208
|
+
.filter(Boolean)
|
|
209
|
+
.sort((a, b) => b.length - a.length);
|
|
210
|
+
const packageRoot = (path) => packageRoots.find((root) => path.startsWith(`${root}/`));
|
|
211
|
+
const modifiedDirectories = new Set([...modified.keys()].map(directory));
|
|
212
|
+
const modifiedPackages = new Set([...modified.keys()].map(packageRoot).filter(Boolean));
|
|
213
|
+
const proximity = (path) => modifiedDirectories.has(directory(path))
|
|
214
|
+
? 0
|
|
215
|
+
: modifiedPackages.has(packageRoot(path))
|
|
216
|
+
? 1
|
|
217
|
+
: 2;
|
|
218
|
+
const orderedAnchors = [...anchors].sort(([a], [b]) => proximity(a) - proximity(b));
|
|
219
|
+
let interrupted = false;
|
|
220
|
+
const undecided = new Set([...anchors.values()].flatMap((indices) => [...indices]));
|
|
221
|
+
const remaining = new Map();
|
|
222
|
+
for (const indices of anchors.values())
|
|
223
|
+
for (const index of indices)
|
|
224
|
+
remaining.set(index, (remaining.get(index) ?? 0) + 1);
|
|
225
|
+
for (const [anchor, indices] of orderedAnchors) {
|
|
226
|
+
if (candidates.length >= DOCS_MAX_SECTIONS) {
|
|
227
|
+
interrupted = true;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
let visited = 0;
|
|
231
|
+
let anchorLimited = false;
|
|
232
|
+
const selected = new Set();
|
|
233
|
+
const closure = await importClosureLazy(sources.read, anchor, {
|
|
234
|
+
known: sources.known,
|
|
235
|
+
configuration,
|
|
236
|
+
cache,
|
|
237
|
+
build,
|
|
238
|
+
stop: (path) => {
|
|
239
|
+
if (path !== anchor && modified.has(path))
|
|
240
|
+
return true;
|
|
241
|
+
if (sources.shouldStop?.()) {
|
|
242
|
+
interrupted = true;
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
if (visited >= DOCS_ANCHOR_MAX_FILES) {
|
|
246
|
+
anchorLimited = true;
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
visited++;
|
|
250
|
+
return false;
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
for (const path of closure.paths)
|
|
254
|
+
for (const unit of modified.get(path) ?? [])
|
|
255
|
+
selected.add(unit);
|
|
256
|
+
if (selected.size) {
|
|
257
|
+
for (const index of indices) {
|
|
258
|
+
const section = sections[index];
|
|
259
|
+
if (!section)
|
|
260
|
+
continue;
|
|
261
|
+
if (selectedSections.has(index)) {
|
|
262
|
+
const existing = [...candidates, ...omitted].find((candidate) => candidate.path === section.file &&
|
|
263
|
+
candidate.start === section.start);
|
|
264
|
+
if (existing) {
|
|
265
|
+
const combined = new Set([...existing.units, ...selected]);
|
|
266
|
+
existing.units = units.filter((unit) => combined.has(unit));
|
|
267
|
+
}
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
selectedSections.add(index);
|
|
271
|
+
const candidate = {
|
|
272
|
+
path: section.file,
|
|
273
|
+
heading: section.label,
|
|
274
|
+
start: section.start,
|
|
275
|
+
end: section.end,
|
|
276
|
+
sentences: docSentences(section.text),
|
|
277
|
+
units: units.filter((unit) => selected.has(unit)),
|
|
278
|
+
};
|
|
279
|
+
(candidates.length < DOCS_MAX_SECTIONS ? candidates : omitted).push(candidate);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (!interrupted && !anchorLimited)
|
|
283
|
+
for (const index of indices) {
|
|
284
|
+
const count = (remaining.get(index) ?? 1) - 1;
|
|
285
|
+
remaining.set(index, count);
|
|
286
|
+
if (count === 0)
|
|
287
|
+
undecided.delete(index);
|
|
288
|
+
}
|
|
289
|
+
if (interrupted)
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
if (interrupted || undecided.size)
|
|
293
|
+
limits.push({
|
|
294
|
+
path: "documentation",
|
|
295
|
+
kind: "collection_budget",
|
|
296
|
+
reason: "Dependency exploration incomplete: collection budget, candidate limit, or per-anchor bound reached.",
|
|
297
|
+
sections: [...undecided]
|
|
298
|
+
.filter((index) => !selectedSections.has(index))
|
|
299
|
+
.map((index) => `${sections[index]?.file} § ${sections[index]?.label}`),
|
|
300
|
+
});
|
|
301
|
+
return { candidates, omitted, limits };
|
|
302
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { FIND_CONTENT_MIN, FIND_EXCERPT_HEAD_CHARS, FIND_NAME_GUARD_MIN, FIND_NAME_GUARD_TOP, FIND_POINTER_MAX, } from "../constants.js";
|
|
2
|
+
import { truncate } from "./truncate.js";
|
|
3
|
+
const stopWords = Object.fromEntries("the and for with from that this into where what which when how does should could would file files code find implement implementation une les des dans pour avec sur est qui que quoi comment fichier fichiers chercher"
|
|
4
|
+
.split(" ")
|
|
5
|
+
.map((word) => [word, true]));
|
|
6
|
+
export function contentWords(goal) {
|
|
7
|
+
return (goal.toLowerCase().match(/[\p{L}\p{N}_]+/gu) ?? []).filter((word) => word.length >= 3 && !stopWords[word]);
|
|
8
|
+
}
|
|
9
|
+
export function findKeywords(goal, keywords = []) {
|
|
10
|
+
return [
|
|
11
|
+
...new Set([
|
|
12
|
+
...contentWords(goal),
|
|
13
|
+
...keywords.map((word) => word.toLowerCase()).filter(Boolean),
|
|
14
|
+
]),
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
export function rankFiles(files) {
|
|
18
|
+
return [...files].sort((a, b) => b.p - a.p || a.path.localeCompare(b.path));
|
|
19
|
+
}
|
|
20
|
+
export function retainFiles(content, names) {
|
|
21
|
+
const retained = rankFiles(content.filter((file) => file.p >= FIND_CONTENT_MIN)).slice(0, FIND_POINTER_MAX);
|
|
22
|
+
for (const name of names.slice(0, FIND_NAME_GUARD_TOP)) {
|
|
23
|
+
const file = content.find((file) => file.path === name.path);
|
|
24
|
+
if (name.p >= FIND_NAME_GUARD_MIN &&
|
|
25
|
+
file &&
|
|
26
|
+
!retained.some((item) => item.path === file.path)) {
|
|
27
|
+
if (retained.length === FIND_POINTER_MAX)
|
|
28
|
+
retained.pop();
|
|
29
|
+
retained.push(file);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return retained;
|
|
34
|
+
}
|
|
35
|
+
export function pathAllowed(path, scope, exclude = []) {
|
|
36
|
+
const scopes = typeof scope === "string" ? [scope] : scope;
|
|
37
|
+
if (scopes?.length &&
|
|
38
|
+
!scopes.some((root) => {
|
|
39
|
+
const normalized = root.replace(/^\.\//u, "").replace(/\/$/u, "");
|
|
40
|
+
return (normalized === "." ||
|
|
41
|
+
path === normalized ||
|
|
42
|
+
path.startsWith(`${normalized}/`));
|
|
43
|
+
}))
|
|
44
|
+
return false;
|
|
45
|
+
return !exclude.some((glob) => {
|
|
46
|
+
const pattern = glob
|
|
47
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
48
|
+
.replace(/\*\*/g, "<<<recursive>>>")
|
|
49
|
+
.replace(/\*/g, "[^/]*")
|
|
50
|
+
.replace(/\?/g, "[^/]")
|
|
51
|
+
.replace(/<<<recursive>>>\//g, "(?:.*/)?")
|
|
52
|
+
.replace(/<<<recursive>>>/g, ".*");
|
|
53
|
+
return new RegExp(`^${pattern}$`, "u").test(path);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export function createExcerptCollector(keywords, limit) {
|
|
57
|
+
let head = "", tail = "", best = "", bestScore = -1, total = 0, whole = "";
|
|
58
|
+
const separator = "\n…\n";
|
|
59
|
+
const room = limit - FIND_EXCERPT_HEAD_CHARS - separator.length;
|
|
60
|
+
return {
|
|
61
|
+
push(chunk) {
|
|
62
|
+
const previousTotal = total;
|
|
63
|
+
total += chunk.length;
|
|
64
|
+
if (total <= limit)
|
|
65
|
+
whole += chunk;
|
|
66
|
+
else
|
|
67
|
+
whole = "";
|
|
68
|
+
if (head.length < FIND_EXCERPT_HEAD_CHARS)
|
|
69
|
+
head = truncate(head + chunk, FIND_EXCERPT_HEAD_CHARS);
|
|
70
|
+
const consumedByHead = Math.max(0, Math.min(chunk.length, FIND_EXCERPT_HEAD_CHARS - previousTotal));
|
|
71
|
+
const buffer = tail + chunk.slice(consumedByHead);
|
|
72
|
+
for (let start = 0; start < buffer.length; start += Math.max(1, Math.floor(room / 4))) {
|
|
73
|
+
const safeStart = start > 0 &&
|
|
74
|
+
buffer.charCodeAt(start) >= 0xdc00 &&
|
|
75
|
+
buffer.charCodeAt(start) <= 0xdfff
|
|
76
|
+
? start + 1
|
|
77
|
+
: start;
|
|
78
|
+
const window = truncate(buffer.slice(safeStart), room);
|
|
79
|
+
const lower = window.toLowerCase();
|
|
80
|
+
const score = keywords.reduce((sum, word) => {
|
|
81
|
+
let count = 0, offset = 0;
|
|
82
|
+
let match = lower.indexOf(word, offset);
|
|
83
|
+
while (match !== -1) {
|
|
84
|
+
count++;
|
|
85
|
+
offset = match + word.length;
|
|
86
|
+
match = lower.indexOf(word, offset);
|
|
87
|
+
}
|
|
88
|
+
return sum + count;
|
|
89
|
+
}, 0);
|
|
90
|
+
if (score > bestScore) {
|
|
91
|
+
best = window;
|
|
92
|
+
bestScore = score;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
let start = Math.max(0, buffer.length - room);
|
|
96
|
+
if (start > 0 &&
|
|
97
|
+
buffer.charCodeAt(start) >= 0xdc00 &&
|
|
98
|
+
buffer.charCodeAt(start) <= 0xdfff)
|
|
99
|
+
start++;
|
|
100
|
+
tail = buffer.slice(start);
|
|
101
|
+
},
|
|
102
|
+
finish() {
|
|
103
|
+
if (total <= limit)
|
|
104
|
+
return whole;
|
|
105
|
+
return truncate(head + separator + best, limit);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
package/dist/core/git.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|