jev-agent-tools 0.1.3
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 +66 -0
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/docs/design.md +187 -0
- package/docs/tools/jev_ask.md +92 -0
- package/docs/tools/jev_ask_files.md +80 -0
- package/docs/tools/jev_check_diff.md +77 -0
- package/docs/tools/jev_find_files.md +59 -0
- package/docs/tools/jev_locate_in_file.md +51 -0
- package/docs/tools/jev_select_tests.md +59 -0
- package/package.json +85 -0
- package/rules/jev-ask.md +4 -0
- package/src/adapters/analysis-context.ts +100 -0
- package/src/adapters/ask-files.ts +236 -0
- package/src/adapters/ask-proof.ts +202 -0
- package/src/adapters/ask-syntax.ts +463 -0
- package/src/adapters/command.ts +240 -0
- package/src/adapters/docs.ts +222 -0
- package/src/adapters/files.ts +411 -0
- package/src/adapters/find.ts +151 -0
- package/src/adapters/git-base.ts +32 -0
- package/src/adapters/git-inventory.ts +94 -0
- package/src/adapters/git.ts +525 -0
- package/src/adapters/locate-file.ts +197 -0
- package/src/adapters/output-lines.ts +50 -0
- package/src/adapters/risk-callers.ts +525 -0
- package/src/adapters/runner-version.ts +102 -0
- package/src/adapters/syntax.ts +229 -0
- package/src/adapters/test-inventory.ts +168 -0
- package/src/adapters/usage.ts +21 -0
- package/src/adapters/utf8.ts +57 -0
- package/src/constants.ts +109 -0
- package/src/core/ask-closure.ts +419 -0
- package/src/core/ask-proof.ts +32 -0
- package/src/core/ask-references.ts +249 -0
- package/src/core/asks.ts +616 -0
- package/src/core/batches.ts +83 -0
- package/src/core/command-output.ts +249 -0
- package/src/core/diff.ts +226 -0
- package/src/core/docs.ts +399 -0
- package/src/core/find.ts +157 -0
- package/src/core/git.ts +5 -0
- package/src/core/import-boundaries.ts +102 -0
- package/src/core/imports.ts +691 -0
- package/src/core/integrity.ts +64 -0
- package/src/core/lexical.ts +130 -0
- package/src/core/locate.ts +213 -0
- package/src/core/output.ts +264 -0
- package/src/core/pointer.ts +51 -0
- package/src/core/risk-callers.ts +1270 -0
- package/src/core/runner-version.ts +66 -0
- package/src/core/sections.ts +269 -0
- package/src/core/state.ts +53 -0
- package/src/core/syntax.ts +8 -0
- package/src/core/test-commands.ts +430 -0
- package/src/core/test-coverage.ts +103 -0
- package/src/core/test-discovery.ts +1695 -0
- package/src/core/test-evidence.ts +649 -0
- package/src/core/test-state.ts +99 -0
- package/src/core/truncate.ts +14 -0
- package/src/core/units.ts +531 -0
- package/src/describe.ts +26 -0
- package/src/guide.ts +42 -0
- package/src/host.ts +22 -0
- package/src/index.ts +40 -0
- package/src/jev/client.ts +505 -0
- package/src/jev/pool.ts +60 -0
- package/src/jev/types.ts +60 -0
- package/src/presets/docs.ts +85 -0
- package/src/presets/risk.ts +263 -0
- package/src/presets/spec.ts +111 -0
- package/src/presets/witnesses.ts +313 -0
- package/src/render.ts +45 -0
- package/src/result.ts +4 -0
- package/src/run-end.ts +157 -0
- package/src/runtime.ts +16 -0
- package/src/session.ts +106 -0
- package/src/texts/ask-files.ts +2 -0
- package/src/texts/ask.ts +4 -0
- package/src/texts/check-diff.ts +24 -0
- package/src/texts/configuration.ts +2 -0
- package/src/texts/find.ts +14 -0
- package/src/texts/guide.ts +16 -0
- package/src/texts/locate.ts +10 -0
- package/src/texts/run-end.ts +13 -0
- package/src/texts/select-tests.ts +3 -0
- package/src/tools/ask-files.ts +263 -0
- package/src/tools/ask-schema.ts +116 -0
- package/src/tools/ask.ts +925 -0
- package/src/tools/check-diff.ts +510 -0
- package/src/tools/docs-check.ts +399 -0
- package/src/tools/find.ts +529 -0
- package/src/tools/locate.ts +369 -0
- package/src/tools/select-tests.ts +746 -0
- package/src/tools/spec-check.ts +210 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
CLOSURE_MAX_CHARS,
|
|
4
|
+
STATE_MAX_CHARS,
|
|
5
|
+
UNIT_MAX_CHARS,
|
|
6
|
+
} from "../constants.ts";
|
|
7
|
+
import type { Json, State } from "../jev/types.ts";
|
|
8
|
+
import type {
|
|
9
|
+
ProofBinding,
|
|
10
|
+
ProofDeclaration,
|
|
11
|
+
ProofSyntax,
|
|
12
|
+
} from "./ask-proof.ts";
|
|
13
|
+
import type { ImportGraph } from "./imports.ts";
|
|
14
|
+
import type { Limitation } from "./output.ts";
|
|
15
|
+
import { truncate } from "./truncate.ts";
|
|
16
|
+
|
|
17
|
+
interface Candidate {
|
|
18
|
+
declaration: ProofDeclaration;
|
|
19
|
+
priority: number;
|
|
20
|
+
depth: number;
|
|
21
|
+
typeOnly: boolean;
|
|
22
|
+
}
|
|
23
|
+
export function buildAskClosure({
|
|
24
|
+
graph,
|
|
25
|
+
syntax,
|
|
26
|
+
paths,
|
|
27
|
+
intentions,
|
|
28
|
+
state,
|
|
29
|
+
}: {
|
|
30
|
+
graph: ImportGraph;
|
|
31
|
+
syntax: ProofSyntax;
|
|
32
|
+
paths: readonly string[];
|
|
33
|
+
intentions: string;
|
|
34
|
+
state: State;
|
|
35
|
+
}): {
|
|
36
|
+
state: State;
|
|
37
|
+
added: readonly { path: string; name: string; chars: number }[];
|
|
38
|
+
omitted: readonly { path: string; name: string; reason: string }[];
|
|
39
|
+
limitations: readonly Limitation[];
|
|
40
|
+
} {
|
|
41
|
+
const limitations: Limitation[] = [];
|
|
42
|
+
const dependencyLimit = (
|
|
43
|
+
path: string,
|
|
44
|
+
dependency: string,
|
|
45
|
+
cause: string,
|
|
46
|
+
): Limitation => {
|
|
47
|
+
const nonRelative =
|
|
48
|
+
/^(?:unresolved|ambiguous) import$/.test(cause) &&
|
|
49
|
+
dependency.length > 0 &&
|
|
50
|
+
!dependency.startsWith(".") &&
|
|
51
|
+
!dependency.startsWith("/") &&
|
|
52
|
+
!dependency.startsWith("sys.path");
|
|
53
|
+
return {
|
|
54
|
+
path,
|
|
55
|
+
dependency,
|
|
56
|
+
cause: `${cause}${nonRelative ? " (external or standard-library, or unresolved repository alias)" : ""}`,
|
|
57
|
+
fact: `${path} : ${dependency}, ${cause}${nonRelative ? "; external or standard-library dependency (or unresolved repository alias), not admitted as a repository file" : ""}`,
|
|
58
|
+
next: nonRelative
|
|
59
|
+
? "Provide the dependency contract in state or a repository-local implementation in paths; do not read outside the repository."
|
|
60
|
+
: "Provide the dependency explicitly in paths.",
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
const relevant = new Set(paths);
|
|
64
|
+
const candidates = new Map<string, Candidate>();
|
|
65
|
+
const originalFiles =
|
|
66
|
+
state.files &&
|
|
67
|
+
typeof state.files === "object" &&
|
|
68
|
+
!Array.isArray(state.files)
|
|
69
|
+
? state.files
|
|
70
|
+
: {};
|
|
71
|
+
const contained = (path: string) =>
|
|
72
|
+
paths.includes(path) || Object.hasOwn(originalFiles, path);
|
|
73
|
+
const declarations = (path: string, name: string) =>
|
|
74
|
+
syntax.declarations.filter(
|
|
75
|
+
(item) =>
|
|
76
|
+
item.path === path &&
|
|
77
|
+
(name === "*" ||
|
|
78
|
+
item.name === name ||
|
|
79
|
+
item.name.startsWith(`${name}.`)),
|
|
80
|
+
);
|
|
81
|
+
const mentioned = (name: string) =>
|
|
82
|
+
intentions.split(/[^\p{L}\p{N}_$.]+/u).includes(name);
|
|
83
|
+
const add = (
|
|
84
|
+
declaration: ProofDeclaration,
|
|
85
|
+
direct: boolean,
|
|
86
|
+
depth: number,
|
|
87
|
+
typeOnly = false,
|
|
88
|
+
) => {
|
|
89
|
+
if (contained(declaration.path)) return;
|
|
90
|
+
const changed =
|
|
91
|
+
declaration.before !== undefined &&
|
|
92
|
+
declaration.before !== declaration.text;
|
|
93
|
+
const priority =
|
|
94
|
+
mentioned(declaration.name) || direct ? 0 : changed ? 1 : 2;
|
|
95
|
+
const key = `${declaration.path} (${declaration.name})`;
|
|
96
|
+
const previous = candidates.get(key);
|
|
97
|
+
if (
|
|
98
|
+
!previous ||
|
|
99
|
+
priority < previous.priority ||
|
|
100
|
+
(priority === previous.priority &&
|
|
101
|
+
(Number(typeOnly) < Number(previous.typeOnly) ||
|
|
102
|
+
(typeOnly === previous.typeOnly && depth < previous.depth)))
|
|
103
|
+
)
|
|
104
|
+
candidates.set(key, { declaration, priority, depth, typeOnly });
|
|
105
|
+
};
|
|
106
|
+
const target = (binding: ProofBinding): string | undefined => {
|
|
107
|
+
if (!binding.specifier) return binding.path;
|
|
108
|
+
const edges = graph.edges.get(binding.path) ?? [];
|
|
109
|
+
const python = binding.path.endsWith(".py");
|
|
110
|
+
let base: string;
|
|
111
|
+
if (python) {
|
|
112
|
+
const relative = binding.specifier.match(/^\.+/)?.[0].length ?? 0;
|
|
113
|
+
base = relative
|
|
114
|
+
? posix.join(
|
|
115
|
+
posix.dirname(binding.path),
|
|
116
|
+
...Array.from({ length: relative - 1 }, () => ".."),
|
|
117
|
+
binding.specifier.slice(relative).replaceAll(".", "/"),
|
|
118
|
+
)
|
|
119
|
+
: binding.specifier.replaceAll(".", "/");
|
|
120
|
+
} else
|
|
121
|
+
base =
|
|
122
|
+
binding.specifier.startsWith(".") || binding.data
|
|
123
|
+
? posix.join(posix.dirname(binding.path), binding.specifier)
|
|
124
|
+
: binding.specifier;
|
|
125
|
+
base = posix.normalize(base);
|
|
126
|
+
const alternatives = python
|
|
127
|
+
? [
|
|
128
|
+
base,
|
|
129
|
+
`${base}.py`,
|
|
130
|
+
`${base}/__init__.py`,
|
|
131
|
+
`${base}/${binding.imported}.py`,
|
|
132
|
+
]
|
|
133
|
+
: [
|
|
134
|
+
base,
|
|
135
|
+
...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"].map(
|
|
136
|
+
(extension) => base.replace(/\.js$/, "") + extension,
|
|
137
|
+
),
|
|
138
|
+
...["ts", "tsx", "js", "jsx"].map(
|
|
139
|
+
(extension) => `${base}/index.${extension}`,
|
|
140
|
+
),
|
|
141
|
+
];
|
|
142
|
+
const matches = edges.filter((edge) =>
|
|
143
|
+
alternatives.some(
|
|
144
|
+
(alternative) =>
|
|
145
|
+
edge === alternative ||
|
|
146
|
+
(!binding.specifier.startsWith(".") &&
|
|
147
|
+
edge.endsWith(`/${alternative}`)),
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
if (matches.length === 1) return matches[0];
|
|
151
|
+
limitations.push(
|
|
152
|
+
dependencyLimit(
|
|
153
|
+
binding.path,
|
|
154
|
+
binding.specifier,
|
|
155
|
+
matches.length > 1 ? "ambiguous import" : "unresolved import",
|
|
156
|
+
),
|
|
157
|
+
);
|
|
158
|
+
return undefined;
|
|
159
|
+
};
|
|
160
|
+
const resolve = (
|
|
161
|
+
path: string,
|
|
162
|
+
name: string,
|
|
163
|
+
direct: boolean,
|
|
164
|
+
depth: number,
|
|
165
|
+
visited: Set<string>,
|
|
166
|
+
typeOnly = false,
|
|
167
|
+
) => {
|
|
168
|
+
relevant.add(path);
|
|
169
|
+
const key = `${path}:${name}`;
|
|
170
|
+
if (visited.has(key)) return;
|
|
171
|
+
visited.add(key);
|
|
172
|
+
const found = declarations(path, name);
|
|
173
|
+
for (const declaration of found) add(declaration, direct, depth, typeOnly);
|
|
174
|
+
const exports = syntax.bindings.filter(
|
|
175
|
+
(item) =>
|
|
176
|
+
item.path === path &&
|
|
177
|
+
item.reexport &&
|
|
178
|
+
(item.local === name || item.local === "*" || name === "*"),
|
|
179
|
+
);
|
|
180
|
+
if (!found.length && !exports.length && !contained(path))
|
|
181
|
+
limitations.push({
|
|
182
|
+
fact: `${path} (${name}) : used declaration not found`,
|
|
183
|
+
next: "Provide the declaration explicitly in paths.",
|
|
184
|
+
});
|
|
185
|
+
for (const binding of exports) {
|
|
186
|
+
const destination = target(binding);
|
|
187
|
+
if (destination)
|
|
188
|
+
resolve(
|
|
189
|
+
destination,
|
|
190
|
+
binding.imported === "*" ? name : binding.imported,
|
|
191
|
+
direct,
|
|
192
|
+
depth,
|
|
193
|
+
visited,
|
|
194
|
+
typeOnly,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
for (const path of paths) {
|
|
199
|
+
const used =
|
|
200
|
+
syntax.uses?.get(path) ??
|
|
201
|
+
syntax.declarations
|
|
202
|
+
.filter((item) => item.path === path)
|
|
203
|
+
.flatMap((item) => item.usedNames);
|
|
204
|
+
for (const binding of syntax.bindings.filter(
|
|
205
|
+
(item) =>
|
|
206
|
+
item.path === path &&
|
|
207
|
+
(!item.reexport || path.endsWith("__init__.py")) &&
|
|
208
|
+
(item.used ?? used.includes(item.local)),
|
|
209
|
+
)) {
|
|
210
|
+
const destination = target(binding);
|
|
211
|
+
if (!destination) continue;
|
|
212
|
+
if (binding.data) {
|
|
213
|
+
relevant.add(destination);
|
|
214
|
+
const source = syntax.sources?.find(
|
|
215
|
+
(item) => item.path === destination,
|
|
216
|
+
);
|
|
217
|
+
if (source)
|
|
218
|
+
add(
|
|
219
|
+
{
|
|
220
|
+
path: destination,
|
|
221
|
+
name: "data",
|
|
222
|
+
text: source.text,
|
|
223
|
+
...(syntax.before && Object.hasOwn(syntax.before, destination)
|
|
224
|
+
? { before: syntax.before[destination] }
|
|
225
|
+
: {}),
|
|
226
|
+
usedNames: [],
|
|
227
|
+
},
|
|
228
|
+
true,
|
|
229
|
+
1,
|
|
230
|
+
);
|
|
231
|
+
else
|
|
232
|
+
limitations.push({
|
|
233
|
+
fact: `${destination} : data missing from the inventory`,
|
|
234
|
+
next: "Provide this file in paths.",
|
|
235
|
+
});
|
|
236
|
+
} else if (binding.imported === "*" && binding.local !== "*") {
|
|
237
|
+
const members = used.filter((item) =>
|
|
238
|
+
item.startsWith(`${binding.local}.`),
|
|
239
|
+
);
|
|
240
|
+
for (const name of members)
|
|
241
|
+
resolve(
|
|
242
|
+
destination,
|
|
243
|
+
name.slice(binding.local.length + 1),
|
|
244
|
+
true,
|
|
245
|
+
1,
|
|
246
|
+
new Set(),
|
|
247
|
+
binding.valueMembers
|
|
248
|
+
? !binding.valueMembers.includes(name)
|
|
249
|
+
: binding.typeOnly,
|
|
250
|
+
);
|
|
251
|
+
if (!members.length)
|
|
252
|
+
limitations.push({
|
|
253
|
+
fact: `${path} : namespace ${binding.local} used without a statically resolved member`,
|
|
254
|
+
next: "Provide the namespace declarations explicitly.",
|
|
255
|
+
});
|
|
256
|
+
} else
|
|
257
|
+
resolve(
|
|
258
|
+
destination,
|
|
259
|
+
binding.imported,
|
|
260
|
+
true,
|
|
261
|
+
1,
|
|
262
|
+
new Set(),
|
|
263
|
+
binding.typeOnly,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (
|
|
267
|
+
path.endsWith(".py") &&
|
|
268
|
+
/(^|\/)(test[^/]*|[^/]*_test)\.py$/.test(path)
|
|
269
|
+
) {
|
|
270
|
+
const fixtures = syntax.declarations.filter(
|
|
271
|
+
(item) =>
|
|
272
|
+
item.fixture &&
|
|
273
|
+
posix.basename(item.path) === "conftest.py" &&
|
|
274
|
+
(posix.dirname(item.path) === "." ||
|
|
275
|
+
path.startsWith(`${posix.dirname(item.path)}/`)),
|
|
276
|
+
);
|
|
277
|
+
// Nearest fixture shadows ancestors, as in pytest.
|
|
278
|
+
fixtures.sort((a, b) => b.path.length - a.path.length);
|
|
279
|
+
const selected = new Set<string>();
|
|
280
|
+
for (const fixture of fixtures) {
|
|
281
|
+
if (
|
|
282
|
+
selected.has(fixture.name) ||
|
|
283
|
+
(!fixture.autouse && !used.includes(fixture.name))
|
|
284
|
+
)
|
|
285
|
+
continue;
|
|
286
|
+
selected.add(fixture.name);
|
|
287
|
+
relevant.add(fixture.path);
|
|
288
|
+
add(fixture, true, 1);
|
|
289
|
+
for (const name of fixture.usedNames)
|
|
290
|
+
for (const helper of declarations(fixture.path, name))
|
|
291
|
+
add(helper, false, 1);
|
|
292
|
+
for (const binding of syntax.bindings.filter(
|
|
293
|
+
(item) =>
|
|
294
|
+
item.path === fixture.path &&
|
|
295
|
+
fixture.usedNames.includes(item.local),
|
|
296
|
+
)) {
|
|
297
|
+
const destination = target(binding);
|
|
298
|
+
if (destination)
|
|
299
|
+
resolve(destination, binding.imported, false, 1, new Set());
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
for (const limit of syntax.limits)
|
|
305
|
+
if ([...relevant].some((path) => limit.fact.includes(path)))
|
|
306
|
+
limitations.push(limit);
|
|
307
|
+
for (const limit of graph.limits)
|
|
308
|
+
if (relevant.has(limit.path))
|
|
309
|
+
limitations.push(
|
|
310
|
+
dependencyLimit(limit.path, limit.specifier, `${limit.kind} import`),
|
|
311
|
+
);
|
|
312
|
+
const ordered = [...candidates.values()].sort(
|
|
313
|
+
(a, b) =>
|
|
314
|
+
a.priority - b.priority ||
|
|
315
|
+
Number(a.typeOnly) - Number(b.typeOnly) ||
|
|
316
|
+
Number(
|
|
317
|
+
b.declaration.before !== undefined &&
|
|
318
|
+
b.declaration.before !== b.declaration.text,
|
|
319
|
+
) -
|
|
320
|
+
Number(
|
|
321
|
+
a.declaration.before !== undefined &&
|
|
322
|
+
a.declaration.before !== a.declaration.text,
|
|
323
|
+
) ||
|
|
324
|
+
a.depth - b.depth ||
|
|
325
|
+
a.declaration.path.localeCompare(b.declaration.path) ||
|
|
326
|
+
a.declaration.name.localeCompare(b.declaration.name),
|
|
327
|
+
);
|
|
328
|
+
const existing =
|
|
329
|
+
state.closure &&
|
|
330
|
+
typeof state.closure === "object" &&
|
|
331
|
+
!Array.isArray(state.closure)
|
|
332
|
+
? state.closure
|
|
333
|
+
: {};
|
|
334
|
+
const closure: Record<string, Json> = { ...existing };
|
|
335
|
+
let nextState = state;
|
|
336
|
+
let budget = JSON.stringify(existing).length - 2;
|
|
337
|
+
const added: { path: string; name: string; chars: number }[] = [];
|
|
338
|
+
const omitted: { path: string; name: string; reason: string }[] = [];
|
|
339
|
+
for (const { declaration } of ordered) {
|
|
340
|
+
const { path, name } = declaration;
|
|
341
|
+
const key = `${path} (${name})`;
|
|
342
|
+
const paired =
|
|
343
|
+
declaration.before !== undefined &&
|
|
344
|
+
declaration.before !== declaration.text;
|
|
345
|
+
if (Object.hasOwn(closure, key)) continue;
|
|
346
|
+
const beforeBudget =
|
|
347
|
+
!paired || declaration.before == null
|
|
348
|
+
? 0
|
|
349
|
+
: declaration.before.length + declaration.text.length > UNIT_MAX_CHARS
|
|
350
|
+
? Math.floor(UNIT_MAX_CHARS / 2)
|
|
351
|
+
: declaration.before.length;
|
|
352
|
+
const before = !paired
|
|
353
|
+
? undefined
|
|
354
|
+
: declaration.before == null
|
|
355
|
+
? declaration.before
|
|
356
|
+
: truncate(declaration.before, beforeBudget);
|
|
357
|
+
const after = truncate(
|
|
358
|
+
declaration.text,
|
|
359
|
+
UNIT_MAX_CHARS - (before?.length ?? 0),
|
|
360
|
+
);
|
|
361
|
+
const sliced =
|
|
362
|
+
after.length < declaration.text.length ||
|
|
363
|
+
(paired && (before?.length ?? 0) < (declaration.before?.length ?? 0));
|
|
364
|
+
const value: Json = !paired
|
|
365
|
+
? after
|
|
366
|
+
: { before: before ?? null, after: declaration.deleted ? null : after };
|
|
367
|
+
const chars = after.length + (before?.length ?? 0);
|
|
368
|
+
const contribution =
|
|
369
|
+
JSON.stringify(key).length +
|
|
370
|
+
1 +
|
|
371
|
+
JSON.stringify(value).length +
|
|
372
|
+
(Object.keys(closure).length ? 1 : 0);
|
|
373
|
+
const candidateState = { ...state, closure: { ...closure, [key]: value } };
|
|
374
|
+
const reason =
|
|
375
|
+
budget + contribution > CLOSURE_MAX_CHARS
|
|
376
|
+
? "closure budget exceeded"
|
|
377
|
+
: JSON.stringify(candidateState).length > STATE_MAX_CHARS
|
|
378
|
+
? "serialized state limit exceeded"
|
|
379
|
+
: undefined;
|
|
380
|
+
if (reason) {
|
|
381
|
+
omitted.push({ path, name, reason });
|
|
382
|
+
limitations.push({
|
|
383
|
+
fact: `${key} : declaration omitted, ${reason}`,
|
|
384
|
+
next: "Provide this declaration in a smaller call.",
|
|
385
|
+
});
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
closure[key] = value;
|
|
389
|
+
nextState = candidateState;
|
|
390
|
+
budget += contribution;
|
|
391
|
+
added.push({ path, name, chars });
|
|
392
|
+
if (sliced)
|
|
393
|
+
limitations.push({
|
|
394
|
+
fact: `${key} : sliced declaration (${chars} characters)`,
|
|
395
|
+
next: "Read the complete declaration before drawing conclusions about an omitted part.",
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
limitations.push({
|
|
399
|
+
fact: "Import closure only, semantic depth 1; files without an import link are not checked.",
|
|
400
|
+
next: "Provide configuration, documentation or data dependencies not linked by imports in paths.",
|
|
401
|
+
});
|
|
402
|
+
const seen = new Map<string, Limitation>();
|
|
403
|
+
for (const limit of limitations) {
|
|
404
|
+
const key = JSON.stringify(
|
|
405
|
+
limit.path
|
|
406
|
+
? [limit.path, limit.cause, limit.dependency]
|
|
407
|
+
: [limit.fact, limit.next],
|
|
408
|
+
);
|
|
409
|
+
const previous = seen.get(key);
|
|
410
|
+
if (previous) {
|
|
411
|
+
if (limit.cause === "dynamic import") {
|
|
412
|
+
previous.count = (previous.count ?? 1) + 1;
|
|
413
|
+
previous.fact = `${limit.fact} (${previous.count} occurrences)`;
|
|
414
|
+
}
|
|
415
|
+
} else seen.set(key, { ...limit });
|
|
416
|
+
}
|
|
417
|
+
const unique = [...seen.values()];
|
|
418
|
+
return { state: nextState, added, omitted, limitations: unique };
|
|
419
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ImportSource } from "./imports.ts";
|
|
2
|
+
import type { Limitation } from "./output.ts";
|
|
3
|
+
|
|
4
|
+
export interface ProofDeclaration {
|
|
5
|
+
path: string;
|
|
6
|
+
name: string;
|
|
7
|
+
text: string;
|
|
8
|
+
before?: string | null;
|
|
9
|
+
usedNames: readonly string[];
|
|
10
|
+
fixture?: boolean;
|
|
11
|
+
autouse?: boolean;
|
|
12
|
+
deleted?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface ProofBinding {
|
|
15
|
+
path: string;
|
|
16
|
+
local: string;
|
|
17
|
+
imported: string;
|
|
18
|
+
specifier: string;
|
|
19
|
+
reexport: boolean;
|
|
20
|
+
used?: boolean;
|
|
21
|
+
typeOnly?: boolean;
|
|
22
|
+
valueMembers?: readonly string[];
|
|
23
|
+
data?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface ProofSyntax {
|
|
26
|
+
declarations: readonly ProofDeclaration[];
|
|
27
|
+
bindings: readonly ProofBinding[];
|
|
28
|
+
limits: readonly Limitation[];
|
|
29
|
+
sources?: readonly ImportSource[];
|
|
30
|
+
uses?: ReadonlyMap<string, readonly string[]>;
|
|
31
|
+
before?: Readonly<Record<string, string | null>>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { ASK_MAX_FILES } from "../constants.ts";
|
|
2
|
+
import type { Question, State } from "../jev/types.ts";
|
|
3
|
+
import type { CompiledAsks } from "./asks.ts";
|
|
4
|
+
|
|
5
|
+
export interface UnresolvedAskReference {
|
|
6
|
+
reference: string;
|
|
7
|
+
reason: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const fileSelector =
|
|
11
|
+
/\b(files|files_before)\s*\[\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')\s*\]/g;
|
|
12
|
+
const selector =
|
|
13
|
+
/\b[A-Za-z_$][\w$]*(?:(?:\s*\.\s*[A-Za-z_$][\w$]*)|(?:\s*\[\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\d+)\s*\]))*/g;
|
|
14
|
+
|
|
15
|
+
function unquote(value: string): string {
|
|
16
|
+
if (value.startsWith('"')) {
|
|
17
|
+
try {
|
|
18
|
+
const parsed: unknown = JSON.parse(value);
|
|
19
|
+
if (typeof parsed === "string") return parsed;
|
|
20
|
+
} catch {
|
|
21
|
+
return value.slice(1, -1);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return value.slice(1, -1).replace(/\\(['\\])/g, "$1");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function looksLikePath(value: string): boolean {
|
|
28
|
+
if (/\s/.test(value) || /^(?:[a-z]+:|www\.)/i.test(value)) return false;
|
|
29
|
+
if (
|
|
30
|
+
!/[A-Za-z_]/.test(value) ||
|
|
31
|
+
/^(?:state|docs?|files|files_before)\./.test(value)
|
|
32
|
+
)
|
|
33
|
+
return false;
|
|
34
|
+
if (/^(?:e\.g|i\.e|Node\.js|Next\.js|Vue\.js)$/i.test(value)) return false;
|
|
35
|
+
return (
|
|
36
|
+
/^(?:\.?\.?\/)?[\w.@+-]+(?:\/[\w.@+-]+)+$/.test(value) ||
|
|
37
|
+
/^[\w@+-][\w.@+-]*\.(?:[cm]?[jt]sx?|py|json|jsonc|ya?ml|toml|ini|cfg|conf|md|mdx|txt|rst|html?|css|scss|sql|sh|bash|rs|go|java|rb|php|c|h|cpp|hpp|vue|svelte|xml|csv|env|lock)$/i.test(
|
|
38
|
+
value,
|
|
39
|
+
) ||
|
|
40
|
+
/^\.[A-Za-z][\w.-]*$/.test(value) ||
|
|
41
|
+
/^(?:README|LICENSE|LICENCE|Makefile|Dockerfile|CHANGELOG|AGENTS|CLAUDE)(?:\.[\w-]+)?$/.test(
|
|
42
|
+
value,
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Explicit paths only: numbers, dates and URLs are not evidence references. */
|
|
48
|
+
export function citedPaths(text: string): string[] {
|
|
49
|
+
const found = new Set<string>();
|
|
50
|
+
const withoutUrls = text.replace(
|
|
51
|
+
/\b(?:[a-z][a-z\d+.-]*:\/\/|www\.)[^\s<>"'`]+/gi,
|
|
52
|
+
" ",
|
|
53
|
+
);
|
|
54
|
+
const withoutSelectors = withoutUrls.replace(
|
|
55
|
+
fileSelector,
|
|
56
|
+
(_whole, _root: string, quoted: string) => {
|
|
57
|
+
found.add(unquote(quoted));
|
|
58
|
+
return " ";
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
const withoutFields = withoutSelectors.replace(selector, (value) =>
|
|
62
|
+
value.includes("[") || /^(?:state|docs?|files|files_before)\./.test(value)
|
|
63
|
+
? " "
|
|
64
|
+
: value,
|
|
65
|
+
);
|
|
66
|
+
const unquoted = withoutFields.replace(
|
|
67
|
+
/`([^`\n]+)`|"([^"\n]+)"|'([^'\n]+)'/g,
|
|
68
|
+
(
|
|
69
|
+
_whole,
|
|
70
|
+
backtick: string | undefined,
|
|
71
|
+
double: string | undefined,
|
|
72
|
+
single: string | undefined,
|
|
73
|
+
) => {
|
|
74
|
+
const value = backtick ?? double ?? single ?? "";
|
|
75
|
+
if (looksLikePath(value)) {
|
|
76
|
+
found.add(value);
|
|
77
|
+
return " ";
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
for (const match of unquoted.matchAll(/[\w.@+-]+(?:\/[\w.@+-]+)*/g)) {
|
|
83
|
+
const value = match[0].replace(/[.,;:!?]+$/, "");
|
|
84
|
+
if (
|
|
85
|
+
looksLikePath(value) &&
|
|
86
|
+
(!value.includes("/") ||
|
|
87
|
+
looksLikePath(value.slice(value.lastIndexOf("/") + 1)))
|
|
88
|
+
)
|
|
89
|
+
found.add(value);
|
|
90
|
+
}
|
|
91
|
+
return [...found];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function questionText(question: Question): string {
|
|
95
|
+
const criteria = question.criteria;
|
|
96
|
+
return [
|
|
97
|
+
question.instructions,
|
|
98
|
+
...(criteria ? Object.values(criteria) : []),
|
|
99
|
+
].join("\n");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function readingTexts(plan: CompiledAsks): { id: string; text: string }[] {
|
|
103
|
+
return plan.readings.map((reading) => ({
|
|
104
|
+
id: reading.id,
|
|
105
|
+
text: [reading.id, reading.twin, ...Object.values(reading.controls ?? {})]
|
|
106
|
+
.flatMap((id) =>
|
|
107
|
+
id && plan.questions[id] ? [questionText(plan.questions[id])] : [],
|
|
108
|
+
)
|
|
109
|
+
.join("\n"),
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function normalized(path: string): string {
|
|
114
|
+
return path.replace(/^(?:\.\/)+/, "");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function resolveAskReferences(
|
|
118
|
+
plan: CompiledAsks,
|
|
119
|
+
note: string | undefined,
|
|
120
|
+
files: Readonly<Record<string, string>>,
|
|
121
|
+
inventory: readonly string[],
|
|
122
|
+
): {
|
|
123
|
+
additions: readonly { reference: string; path: string }[];
|
|
124
|
+
unresolved: readonly UnresolvedAskReference[];
|
|
125
|
+
} {
|
|
126
|
+
const references = new Set(
|
|
127
|
+
readingTexts(plan).flatMap(({ text }) => citedPaths(text)),
|
|
128
|
+
);
|
|
129
|
+
for (const reference of citedPaths(note ?? "")) references.add(reference);
|
|
130
|
+
const paths = [...new Set([...inventory, ...Object.keys(files)])];
|
|
131
|
+
const exact = new Map(paths.map((path) => [normalized(path), path]));
|
|
132
|
+
const selected = new Set(Object.keys(files).map(normalized));
|
|
133
|
+
const additions: { reference: string; path: string }[] = [];
|
|
134
|
+
const unresolved: UnresolvedAskReference[] = [];
|
|
135
|
+
for (const reference of references) {
|
|
136
|
+
if (Object.hasOwn(files, reference)) continue;
|
|
137
|
+
const key = normalized(reference);
|
|
138
|
+
const direct = exact.get(key);
|
|
139
|
+
const matches = direct
|
|
140
|
+
? [direct]
|
|
141
|
+
: paths.filter((path) => normalized(path).endsWith(`/${key}`));
|
|
142
|
+
if (matches.length !== 1) {
|
|
143
|
+
unresolved.push({
|
|
144
|
+
reference,
|
|
145
|
+
reason: `${reference} is cited but missing from state (${matches.length ? "ambiguous path" : "not found"})`,
|
|
146
|
+
});
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const path = matches[0];
|
|
150
|
+
if (path === undefined) continue;
|
|
151
|
+
if (!selected.has(normalized(path)) && selected.size >= ASK_MAX_FILES) {
|
|
152
|
+
unresolved.push({
|
|
153
|
+
reference,
|
|
154
|
+
reason: `${reference} is cited but missing from state (ASK_MAX_FILES limit: ${ASK_MAX_FILES})`,
|
|
155
|
+
});
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
selected.add(normalized(path));
|
|
159
|
+
additions.push({ reference, path });
|
|
160
|
+
}
|
|
161
|
+
return { additions, unresolved };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function object(value: unknown): value is Record<string, unknown> {
|
|
165
|
+
return value !== null && typeof value === "object";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function unpack(value: unknown): unknown {
|
|
169
|
+
if (typeof value !== "string" || !/^[\s]*[[{]/.test(value)) return value;
|
|
170
|
+
try {
|
|
171
|
+
return JSON.parse(value) as unknown;
|
|
172
|
+
} catch {
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function empty(value: unknown): boolean {
|
|
178
|
+
return (
|
|
179
|
+
value === null ||
|
|
180
|
+
(typeof value === "string" && value.trim() === "") ||
|
|
181
|
+
(object(value) && Object.keys(value).length === 0)
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function emptyReference(text: string, state: State): string | undefined {
|
|
186
|
+
// File references can be plain paths, not just selectors.
|
|
187
|
+
const files = state.files;
|
|
188
|
+
if (object(files)) {
|
|
189
|
+
for (const reference of citedPaths(text)) {
|
|
190
|
+
if (Object.hasOwn(files, reference) && empty(files[reference]))
|
|
191
|
+
return reference;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const match of text.matchAll(selector)) {
|
|
195
|
+
const parts = [
|
|
196
|
+
...match[0].matchAll(
|
|
197
|
+
/[A-Za-z_$][\w$]*|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\d+/g,
|
|
198
|
+
),
|
|
199
|
+
].map(([part]) => (/^["']/.test(part) ? unquote(part) : part));
|
|
200
|
+
const root = parts[0];
|
|
201
|
+
if (root === undefined) continue;
|
|
202
|
+
let value: unknown;
|
|
203
|
+
let offset: number;
|
|
204
|
+
if (root === "state") {
|
|
205
|
+
value = state;
|
|
206
|
+
offset = 1;
|
|
207
|
+
} else if (Object.hasOwn(state, root)) {
|
|
208
|
+
value = state[root];
|
|
209
|
+
offset = 1;
|
|
210
|
+
} else continue;
|
|
211
|
+
let present = true;
|
|
212
|
+
for (const part of parts.slice(offset)) {
|
|
213
|
+
value = unpack(value);
|
|
214
|
+
if (!object(value) || !Object.hasOwn(value, part)) {
|
|
215
|
+
present = false;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
value = value[part];
|
|
219
|
+
}
|
|
220
|
+
// A file absent from the base proves creation; it is not empty evidence.
|
|
221
|
+
if (root === "files_before" && value === null) continue;
|
|
222
|
+
if (present && empty(unpack(value))) return match[0].trim();
|
|
223
|
+
}
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function blockedAskReadings(
|
|
228
|
+
plan: CompiledAsks,
|
|
229
|
+
state: State,
|
|
230
|
+
unresolved: readonly UnresolvedAskReference[],
|
|
231
|
+
note?: string,
|
|
232
|
+
): ReadonlyMap<string, string> {
|
|
233
|
+
const blocked = new Map<string, string>();
|
|
234
|
+
const notePaths = new Set(citedPaths(note ?? ""));
|
|
235
|
+
const noteEmpty = emptyReference(note ?? "", state);
|
|
236
|
+
for (const { id, text } of readingTexts(plan)) {
|
|
237
|
+
const paths = new Set(citedPaths(text));
|
|
238
|
+
const missing = unresolved.find(
|
|
239
|
+
({ reference }) => paths.has(reference) || notePaths.has(reference),
|
|
240
|
+
);
|
|
241
|
+
if (missing) {
|
|
242
|
+
blocked.set(id, missing.reason);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const reference = noteEmpty ?? emptyReference(text, state);
|
|
246
|
+
if (reference) blocked.set(id, `${reference} is empty`);
|
|
247
|
+
}
|
|
248
|
+
return blocked;
|
|
249
|
+
}
|