jev-agent-tools 0.1.4 → 0.2.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 +70 -1
- package/CONTRIBUTING.md +40 -0
- package/README.md +42 -9
- package/SECURITY.md +27 -0
- package/dist/adapters/analysis-context.js +75 -0
- package/dist/adapters/ask-files.js +189 -0
- package/dist/adapters/ask-proof.js +144 -0
- package/dist/adapters/ask-syntax.js +385 -0
- package/dist/adapters/canonical-path.js +17 -0
- package/dist/adapters/command.js +181 -0
- package/dist/adapters/docs.js +172 -0
- package/dist/adapters/exec.js +207 -0
- package/dist/adapters/files.js +293 -0
- package/dist/adapters/find.js +122 -0
- package/dist/adapters/git-base.js +26 -0
- package/dist/adapters/git-inventory.js +71 -0
- package/dist/adapters/git.js +439 -0
- package/dist/adapters/locate-file.js +159 -0
- package/dist/adapters/output-lines.js +46 -0
- package/dist/adapters/private-storage.js +98 -0
- package/dist/adapters/risk-callers.js +426 -0
- package/dist/adapters/runner-version.js +78 -0
- package/dist/adapters/shell.js +76 -0
- package/dist/adapters/syntax.js +187 -0
- package/dist/adapters/test-inventory.js +131 -0
- package/dist/adapters/usage.js +20 -0
- package/dist/adapters/utf8.js +47 -0
- package/dist/configuration.js +257 -0
- package/dist/constants.js +119 -0
- package/dist/core/ask-closure.js +282 -0
- package/dist/core/ask-proof.js +1 -0
- package/dist/core/ask-references.js +194 -0
- package/dist/core/asks.js +436 -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 +120 -0
- package/dist/core/pointer.js +29 -0
- package/dist/core/risk-callers.js +851 -0
- package/dist/core/runner-version.js +45 -0
- package/dist/core/sections.js +230 -0
- package/dist/core/state.js +44 -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 +434 -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 +187 -0
- package/dist/mcp/tools.js +116 -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 +42 -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 +1 -0
- package/dist/texts/ask.js +2 -0
- package/dist/texts/check-diff.js +17 -0
- package/dist/texts/configuration.js +1 -0
- package/dist/texts/find.js +14 -0
- package/dist/texts/guide.js +16 -0
- package/dist/texts/locate.js +10 -0
- package/dist/texts/select-tests.js +2 -0
- package/dist/tools/ask-files.js +217 -0
- package/dist/tools/ask-schema.js +70 -0
- package/dist/tools/ask.js +686 -0
- package/dist/tools/check-diff.js +402 -0
- package/dist/tools/docs-check.js +299 -0
- package/dist/tools/find.js +389 -0
- package/dist/tools/locate.js +303 -0
- package/dist/tools/select-tests.js +567 -0
- package/dist/tools/spec-check.js +166 -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 +91 -0
- package/docs/design.md +3 -3
- package/docs/mcp.md +231 -0
- package/package.json +19 -4
- package/server.json +57 -0
- package/src/adapters/canonical-path.ts +18 -0
- package/src/adapters/command.ts +7 -4
- package/src/adapters/exec.ts +226 -0
- package/src/adapters/private-storage.ts +143 -0
- package/src/adapters/risk-callers.ts +4 -2
- package/src/adapters/shell.ts +97 -0
- package/src/configuration.ts +39 -12
- package/src/constants.ts +11 -0
- package/src/core/command-output.ts +17 -1
- package/src/host.ts +11 -0
- package/src/jev/client.ts +12 -0
- package/src/jev/types.ts +6 -0
- package/src/mcp/main.ts +135 -0
- package/src/mcp/protocol.ts +282 -0
- package/src/mcp/tools.ts +166 -0
- package/src/session.ts +59 -0
- package/src/setup.ts +13 -5
- package/src/tools/ask-files.ts +5 -7
- package/src/tools/ask.ts +26 -22
- package/src/tools/check-diff.ts +8 -5
- package/src/tools/docs-check.ts +1 -0
- package/src/tools/find.ts +5 -2
- package/src/tools/locate.ts +5 -8
- package/src/tools/select-tests.ts +7 -4
- package/src/tools/spec-check.ts +1 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
export const STATE_MAX_CHARS = 80_000;
|
|
2
|
+
export const TEST_SOURCE_MAX_BYTES = STATE_MAX_CHARS * 16;
|
|
3
|
+
export const SELECT_FILE_SHARE = 0.8;
|
|
4
|
+
export const ASK_NOTE_MAX_CHARS = 8_000;
|
|
5
|
+
export const ASK_MAX_FILES = 20;
|
|
6
|
+
export const ASK_TIMEOUT_S = 60;
|
|
7
|
+
export const ASK_TIMEOUT_MAX_S = 300;
|
|
8
|
+
export const PROCESS_KILL_GRACE_MS = 2_000;
|
|
9
|
+
export const PROCESS_PIPE_GRACE_MS = 1_000;
|
|
10
|
+
export const PROCESS_TREE_TIMEOUT_MS = 5_000;
|
|
11
|
+
export const MCP_SHUTDOWN_FLUSH_TIMEOUT_MS = 1_000;
|
|
12
|
+
export const PRIVATE_STORAGE_TIMEOUT_MS = 20_000;
|
|
13
|
+
export const MCP_VALIDATION_MAX_ERRORS = 5;
|
|
14
|
+
export const MCP_PACKAGE_REPLY_TIMEOUT_MS = 20_000;
|
|
15
|
+
export const MCP_PACKAGE_EXIT_TIMEOUT_MS = 10_000;
|
|
16
|
+
export const MCP_PACKAGE_KILL_TIMEOUT_MS = 2_000;
|
|
17
|
+
export const MCP_PACKAGE_INSTALL_TIMEOUT_MS = 300_000;
|
|
18
|
+
export const MCP_PACKAGE_STDERR_MAX_CHARS = 2_000;
|
|
19
|
+
export const OUTPUT_REPEAT_MIN = 5;
|
|
20
|
+
export const OUTPUT_CHUNK_CHARS = 2_500;
|
|
21
|
+
export const OUTPUT_FIND_MAX_CALLS = 40;
|
|
22
|
+
export const OUTPUT_HEAD_SHARE = 0.05;
|
|
23
|
+
export const OUTPUT_TAIL_SHARE = 0.15;
|
|
24
|
+
// Unmeasured admission bound for each command output file; never truncate it silently.
|
|
25
|
+
export const OUTPUT_FILE_MAX_BYTES = 64 * 1024 * 1024;
|
|
26
|
+
export const OUTPUT_LINE_MAX_CHARS = 8_192;
|
|
27
|
+
export const OUTPUT_SHAPE_MAX_COUNT = 2_048;
|
|
28
|
+
export const OUTPUT_FAILURE_WINDOW_LINES = 20;
|
|
29
|
+
export const CHOICE_MAX_OPTIONS = 255;
|
|
30
|
+
export const MAX_FILES = 255;
|
|
31
|
+
export const FILE_MAX_KB = STATE_MAX_CHARS / 1_000;
|
|
32
|
+
export const SCORE_MIN_LEVELS = 2;
|
|
33
|
+
export const SCORE_MAX_LEVELS = 10;
|
|
34
|
+
export const TIMEOUT_MS = 10_000;
|
|
35
|
+
export const UNIT_MAX_CHARS = 6_000;
|
|
36
|
+
export const UNIT_CONTEXT_LINES = 8;
|
|
37
|
+
export const SELECT_MIN = 0.5;
|
|
38
|
+
export const BAND_BOOL_GRAY_A = 0.2;
|
|
39
|
+
export const BAND_BOOL_YES_MIN = 1 - BAND_BOOL_GRAY_A;
|
|
40
|
+
export const ORDER_REVERSE_BELOW = 0.85;
|
|
41
|
+
export const ORDER_DISAGREE_MIN = 0.1;
|
|
42
|
+
export const BAND_CHOICE_VERDICT_MIN = ORDER_REVERSE_BELOW;
|
|
43
|
+
export const CANNOT_TELL_MIN = 0.3;
|
|
44
|
+
export const REQUEST_MAX_TOKENS = 60_000;
|
|
45
|
+
export const RATE_PER_SECOND = 8;
|
|
46
|
+
export const CONCURRENCY = 8;
|
|
47
|
+
export const REQUEST_ATTEMPTS = 3;
|
|
48
|
+
export const RETRY_BASE_MS = 500;
|
|
49
|
+
export const RETRY_MAX_MS = 5_000;
|
|
50
|
+
export const HTTP_ERROR_MAX_CHARS = 300;
|
|
51
|
+
export const QUESTION_TOKENS = 1 / 3.3;
|
|
52
|
+
export const STATE_TOKENS = 0.283;
|
|
53
|
+
export const REQUEST_BASE_TOKENS = 456;
|
|
54
|
+
export const FLAG_MIN = 0.7;
|
|
55
|
+
export const DOCS_CHECK_MIN = 0.2;
|
|
56
|
+
export const DOCS_MAX_SECTIONS = 40;
|
|
57
|
+
export const HOOK_BUDGET_MS = 15_000;
|
|
58
|
+
/** Progressive collection time bound, not a coverage or wall-time guarantee. */
|
|
59
|
+
export const DOCS_COLLECT_BUDGET_MS = 750;
|
|
60
|
+
export const DOCS_NAME_MAX_FILES = 32;
|
|
61
|
+
/** Initial per-anchor traversal bound; shared edge cache remains unbounded. */
|
|
62
|
+
export const DOCS_ANCHOR_MAX_FILES = 32;
|
|
63
|
+
export const WITNESS_LURE_MAX = 0.15;
|
|
64
|
+
/** Small coverage decoys only; risk and large-witness policies remain unchanged. */
|
|
65
|
+
export const COVERAGE_WITNESS_LURE_MAX = 0.2;
|
|
66
|
+
export const WITNESS_LARGE_LURE_MAX = 0.25;
|
|
67
|
+
export const WITNESS_LARGE_CHARS = 13_000;
|
|
68
|
+
export const WITNESS_ETALON_MIN = 0.7;
|
|
69
|
+
export const WITNESS_AUTO_MIN_CELLS = 8;
|
|
70
|
+
export const CLOSURE_MAX_CHARS = 20_000;
|
|
71
|
+
export const CALLER_DISPLAY_MAX_SPANS = 8;
|
|
72
|
+
export const LIMIT_DISPLAY_MAX_PATHS = 4;
|
|
73
|
+
export const DOCS_DISPLAY_MAX_SECTIONS = 5;
|
|
74
|
+
export const FIND_NAME_CANDIDATES = 128;
|
|
75
|
+
export const FIND_NAME_BATCH = 64;
|
|
76
|
+
export const FIND_FILES_READ = 20;
|
|
77
|
+
export const FIND_READ_PER_CALL = 5;
|
|
78
|
+
export const FIND_EXCERPT_CHARS = 3_000;
|
|
79
|
+
export const FIND_POINTER_EXCERPT_CHARS = 1_500;
|
|
80
|
+
export const FIND_POINTER_MAX = 8;
|
|
81
|
+
export const FIND_NAME_GUARD_MIN = 0.9;
|
|
82
|
+
export const FIND_CONFIRM_MIN = 0.8;
|
|
83
|
+
export const FIND_GOAL_MIN_WORDS = 5;
|
|
84
|
+
export const FIND_QUICK_CANDIDATES = 32;
|
|
85
|
+
export const FIND_QUICK_READ = 8;
|
|
86
|
+
export const FIND_THOROUGH_CANDIDATES = 256;
|
|
87
|
+
export const FIND_THOROUGH_READ = 40;
|
|
88
|
+
export const FIND_EXCERPT_HEAD_CHARS = 1_000;
|
|
89
|
+
export const FIND_CONTENT_MIN = 0.5;
|
|
90
|
+
export const FIND_NAME_GUARD_TOP = 3;
|
|
91
|
+
export const FIND_PATH_WEIGHT = 3;
|
|
92
|
+
export const FIND_GREP_PAGE_SIZE = 1024;
|
|
93
|
+
export const FIND_READ_CHUNK_BYTES = 16_384;
|
|
94
|
+
export const LOCATE_MIN_KB = 19;
|
|
95
|
+
export const LOCATE_VERDICT_MIN = 0.7;
|
|
96
|
+
export const LOCATE_GRAY_MIN = 0.4;
|
|
97
|
+
export const LOCATE_SHRINK_TOP = 3;
|
|
98
|
+
export const LOCATE_SECTION_MAX_LINES = 150;
|
|
99
|
+
export const LOCATE_SECTION_MIN_LINES = 8;
|
|
100
|
+
export const LOCATE_WINDOW_LINES = 80;
|
|
101
|
+
export const LOCATE_LABEL_MAX_CHARS = 120;
|
|
102
|
+
export const LOCATE_WHOLE_MAX_CHARS = STATE_MAX_CHARS * 4;
|
|
103
|
+
export const LOCATE_WHOLE_MAX_BYTES = LOCATE_WHOLE_MAX_CHARS * 4;
|
|
104
|
+
export const LOCATE_READ_BUFFER_BYTES = 64 * 1024;
|
|
105
|
+
// Unmeasured engineering bound: leaves room for multi-window refinement and JSON metadata.
|
|
106
|
+
export const LOCATE_WINDOW_MAX_SERIALIZED_CHARS = STATE_MAX_CHARS / 4;
|
|
107
|
+
// Keep SHA-256 IDs plus options below Windows' command-line limit.
|
|
108
|
+
export const GIT_BLOB_BATCH_SIZE = 400;
|
|
109
|
+
export const RUNNER_PACKAGE_MAX_BYTES = 64 * 1024;
|
|
110
|
+
// ADR 0001: allowed local dependencies, including erased type imports.
|
|
111
|
+
export const IMPORT_LAYERS = {
|
|
112
|
+
core: ["core", "constants.ts", "result.ts", "jev/types.ts"],
|
|
113
|
+
presets: ["presets", "core", "constants.ts", "result.ts", "jev/types.ts"],
|
|
114
|
+
adapters: ["adapters", "core", "constants.ts", "result.ts", "jev/types.ts"],
|
|
115
|
+
jev: ["jev", "core", "constants.ts", "result.ts"],
|
|
116
|
+
texts: ["texts", "constants.ts", "presets"],
|
|
117
|
+
"constants.ts": [],
|
|
118
|
+
"result.ts": [],
|
|
119
|
+
};
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { CLOSURE_MAX_CHARS, STATE_MAX_CHARS, UNIT_MAX_CHARS, } from "../constants.js";
|
|
3
|
+
import { truncate } from "./truncate.js";
|
|
4
|
+
export function buildAskClosure({ graph, syntax, paths, intentions, state, }) {
|
|
5
|
+
const limitations = [];
|
|
6
|
+
const dependencyLimit = (path, dependency, cause) => {
|
|
7
|
+
const nonRelative = /^(?:unresolved|ambiguous) import$/.test(cause) &&
|
|
8
|
+
dependency.length > 0 &&
|
|
9
|
+
!dependency.startsWith(".") &&
|
|
10
|
+
!dependency.startsWith("/") &&
|
|
11
|
+
!dependency.startsWith("sys.path");
|
|
12
|
+
return {
|
|
13
|
+
path,
|
|
14
|
+
dependency,
|
|
15
|
+
cause: `${cause}${nonRelative ? " (external or standard-library, or unresolved repository alias)" : ""}`,
|
|
16
|
+
fact: `${path} : ${dependency}, ${cause}${nonRelative ? "; external or standard-library dependency (or unresolved repository alias), not admitted as a repository file" : ""}`,
|
|
17
|
+
next: nonRelative
|
|
18
|
+
? "Provide the dependency contract in state or a repository-local implementation in paths; do not read outside the repository."
|
|
19
|
+
: "Provide the dependency explicitly in paths.",
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
const relevant = new Set(paths);
|
|
23
|
+
const candidates = new Map();
|
|
24
|
+
const originalFiles = state.files &&
|
|
25
|
+
typeof state.files === "object" &&
|
|
26
|
+
!Array.isArray(state.files)
|
|
27
|
+
? state.files
|
|
28
|
+
: {};
|
|
29
|
+
const contained = (path) => paths.includes(path) || Object.hasOwn(originalFiles, path);
|
|
30
|
+
const declarations = (path, name) => syntax.declarations.filter((item) => item.path === path &&
|
|
31
|
+
(name === "*" ||
|
|
32
|
+
item.name === name ||
|
|
33
|
+
item.name.startsWith(`${name}.`)));
|
|
34
|
+
const mentioned = (name) => intentions.split(/[^\p{L}\p{N}_$.]+/u).includes(name);
|
|
35
|
+
const add = (declaration, direct, depth, typeOnly = false) => {
|
|
36
|
+
if (contained(declaration.path))
|
|
37
|
+
return;
|
|
38
|
+
const changed = declaration.before !== undefined &&
|
|
39
|
+
declaration.before !== declaration.text;
|
|
40
|
+
const priority = mentioned(declaration.name) || direct ? 0 : changed ? 1 : 2;
|
|
41
|
+
const key = `${declaration.path} (${declaration.name})`;
|
|
42
|
+
const previous = candidates.get(key);
|
|
43
|
+
if (!previous ||
|
|
44
|
+
priority < previous.priority ||
|
|
45
|
+
(priority === previous.priority &&
|
|
46
|
+
(Number(typeOnly) < Number(previous.typeOnly) ||
|
|
47
|
+
(typeOnly === previous.typeOnly && depth < previous.depth))))
|
|
48
|
+
candidates.set(key, { declaration, priority, depth, typeOnly });
|
|
49
|
+
};
|
|
50
|
+
const target = (binding) => {
|
|
51
|
+
if (!binding.specifier)
|
|
52
|
+
return binding.path;
|
|
53
|
+
const edges = graph.edges.get(binding.path) ?? [];
|
|
54
|
+
const python = binding.path.endsWith(".py");
|
|
55
|
+
let base;
|
|
56
|
+
if (python) {
|
|
57
|
+
const relative = binding.specifier.match(/^\.+/)?.[0].length ?? 0;
|
|
58
|
+
base = relative
|
|
59
|
+
? posix.join(posix.dirname(binding.path), ...Array.from({ length: relative - 1 }, () => ".."), binding.specifier.slice(relative).replaceAll(".", "/"))
|
|
60
|
+
: binding.specifier.replaceAll(".", "/");
|
|
61
|
+
}
|
|
62
|
+
else
|
|
63
|
+
base =
|
|
64
|
+
binding.specifier.startsWith(".") || binding.data
|
|
65
|
+
? posix.join(posix.dirname(binding.path), binding.specifier)
|
|
66
|
+
: binding.specifier;
|
|
67
|
+
base = posix.normalize(base);
|
|
68
|
+
const alternatives = python
|
|
69
|
+
? [
|
|
70
|
+
base,
|
|
71
|
+
`${base}.py`,
|
|
72
|
+
`${base}/__init__.py`,
|
|
73
|
+
`${base}/${binding.imported}.py`,
|
|
74
|
+
]
|
|
75
|
+
: [
|
|
76
|
+
base,
|
|
77
|
+
...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"].map((extension) => base.replace(/\.js$/, "") + extension),
|
|
78
|
+
...["ts", "tsx", "js", "jsx"].map((extension) => `${base}/index.${extension}`),
|
|
79
|
+
];
|
|
80
|
+
const matches = edges.filter((edge) => alternatives.some((alternative) => edge === alternative ||
|
|
81
|
+
(!binding.specifier.startsWith(".") &&
|
|
82
|
+
edge.endsWith(`/${alternative}`))));
|
|
83
|
+
if (matches.length === 1)
|
|
84
|
+
return matches[0];
|
|
85
|
+
limitations.push(dependencyLimit(binding.path, binding.specifier, matches.length > 1 ? "ambiguous import" : "unresolved import"));
|
|
86
|
+
return undefined;
|
|
87
|
+
};
|
|
88
|
+
const resolve = (path, name, direct, depth, visited, typeOnly = false) => {
|
|
89
|
+
relevant.add(path);
|
|
90
|
+
const key = `${path}:${name}`;
|
|
91
|
+
if (visited.has(key))
|
|
92
|
+
return;
|
|
93
|
+
visited.add(key);
|
|
94
|
+
const found = declarations(path, name);
|
|
95
|
+
for (const declaration of found)
|
|
96
|
+
add(declaration, direct, depth, typeOnly);
|
|
97
|
+
const exports = syntax.bindings.filter((item) => item.path === path &&
|
|
98
|
+
item.reexport &&
|
|
99
|
+
(item.local === name || item.local === "*" || name === "*"));
|
|
100
|
+
if (!found.length && !exports.length && !contained(path))
|
|
101
|
+
limitations.push({
|
|
102
|
+
fact: `${path} (${name}) : used declaration not found`,
|
|
103
|
+
next: "Provide the declaration explicitly in paths.",
|
|
104
|
+
});
|
|
105
|
+
for (const binding of exports) {
|
|
106
|
+
const destination = target(binding);
|
|
107
|
+
if (destination)
|
|
108
|
+
resolve(destination, binding.imported === "*" ? name : binding.imported, direct, depth, visited, typeOnly);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
for (const path of paths) {
|
|
112
|
+
const used = syntax.uses?.get(path) ??
|
|
113
|
+
syntax.declarations
|
|
114
|
+
.filter((item) => item.path === path)
|
|
115
|
+
.flatMap((item) => item.usedNames);
|
|
116
|
+
for (const binding of syntax.bindings.filter((item) => item.path === path &&
|
|
117
|
+
(!item.reexport || path.endsWith("__init__.py")) &&
|
|
118
|
+
(item.used ?? used.includes(item.local)))) {
|
|
119
|
+
const destination = target(binding);
|
|
120
|
+
if (!destination)
|
|
121
|
+
continue;
|
|
122
|
+
if (binding.data) {
|
|
123
|
+
relevant.add(destination);
|
|
124
|
+
const source = syntax.sources?.find((item) => item.path === destination);
|
|
125
|
+
if (source)
|
|
126
|
+
add({
|
|
127
|
+
path: destination,
|
|
128
|
+
name: "data",
|
|
129
|
+
text: source.text,
|
|
130
|
+
...(syntax.before && Object.hasOwn(syntax.before, destination)
|
|
131
|
+
? { before: syntax.before[destination] }
|
|
132
|
+
: {}),
|
|
133
|
+
usedNames: [],
|
|
134
|
+
}, true, 1);
|
|
135
|
+
else
|
|
136
|
+
limitations.push({
|
|
137
|
+
fact: `${destination} : data missing from the inventory`,
|
|
138
|
+
next: "Provide this file in paths.",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
else if (binding.imported === "*" && binding.local !== "*") {
|
|
142
|
+
const members = used.filter((item) => item.startsWith(`${binding.local}.`));
|
|
143
|
+
for (const name of members)
|
|
144
|
+
resolve(destination, name.slice(binding.local.length + 1), true, 1, new Set(), binding.valueMembers
|
|
145
|
+
? !binding.valueMembers.includes(name)
|
|
146
|
+
: binding.typeOnly);
|
|
147
|
+
if (!members.length)
|
|
148
|
+
limitations.push({
|
|
149
|
+
fact: `${path} : namespace ${binding.local} used without a statically resolved member`,
|
|
150
|
+
next: "Provide the namespace declarations explicitly.",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
else
|
|
154
|
+
resolve(destination, binding.imported, true, 1, new Set(), binding.typeOnly);
|
|
155
|
+
}
|
|
156
|
+
if (path.endsWith(".py") &&
|
|
157
|
+
/(^|\/)(test[^/]*|[^/]*_test)\.py$/.test(path)) {
|
|
158
|
+
const fixtures = syntax.declarations.filter((item) => item.fixture &&
|
|
159
|
+
posix.basename(item.path) === "conftest.py" &&
|
|
160
|
+
(posix.dirname(item.path) === "." ||
|
|
161
|
+
path.startsWith(`${posix.dirname(item.path)}/`)));
|
|
162
|
+
// Nearest fixture shadows ancestors, as in pytest.
|
|
163
|
+
fixtures.sort((a, b) => b.path.length - a.path.length);
|
|
164
|
+
const selected = new Set();
|
|
165
|
+
for (const fixture of fixtures) {
|
|
166
|
+
if (selected.has(fixture.name) ||
|
|
167
|
+
(!fixture.autouse && !used.includes(fixture.name)))
|
|
168
|
+
continue;
|
|
169
|
+
selected.add(fixture.name);
|
|
170
|
+
relevant.add(fixture.path);
|
|
171
|
+
add(fixture, true, 1);
|
|
172
|
+
for (const name of fixture.usedNames)
|
|
173
|
+
for (const helper of declarations(fixture.path, name))
|
|
174
|
+
add(helper, false, 1);
|
|
175
|
+
for (const binding of syntax.bindings.filter((item) => item.path === fixture.path &&
|
|
176
|
+
fixture.usedNames.includes(item.local))) {
|
|
177
|
+
const destination = target(binding);
|
|
178
|
+
if (destination)
|
|
179
|
+
resolve(destination, binding.imported, false, 1, new Set());
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
for (const limit of syntax.limits)
|
|
185
|
+
if ([...relevant].some((path) => limit.fact.includes(path)))
|
|
186
|
+
limitations.push(limit);
|
|
187
|
+
for (const limit of graph.limits)
|
|
188
|
+
if (relevant.has(limit.path))
|
|
189
|
+
limitations.push(dependencyLimit(limit.path, limit.specifier, `${limit.kind} import`));
|
|
190
|
+
const ordered = [...candidates.values()].sort((a, b) => a.priority - b.priority ||
|
|
191
|
+
Number(a.typeOnly) - Number(b.typeOnly) ||
|
|
192
|
+
Number(b.declaration.before !== undefined &&
|
|
193
|
+
b.declaration.before !== b.declaration.text) -
|
|
194
|
+
Number(a.declaration.before !== undefined &&
|
|
195
|
+
a.declaration.before !== a.declaration.text) ||
|
|
196
|
+
a.depth - b.depth ||
|
|
197
|
+
a.declaration.path.localeCompare(b.declaration.path) ||
|
|
198
|
+
a.declaration.name.localeCompare(b.declaration.name));
|
|
199
|
+
const existing = state.closure &&
|
|
200
|
+
typeof state.closure === "object" &&
|
|
201
|
+
!Array.isArray(state.closure)
|
|
202
|
+
? state.closure
|
|
203
|
+
: {};
|
|
204
|
+
const closure = { ...existing };
|
|
205
|
+
let nextState = state;
|
|
206
|
+
let budget = JSON.stringify(existing).length - 2;
|
|
207
|
+
const added = [];
|
|
208
|
+
const omitted = [];
|
|
209
|
+
for (const { declaration } of ordered) {
|
|
210
|
+
const { path, name } = declaration;
|
|
211
|
+
const key = `${path} (${name})`;
|
|
212
|
+
const paired = declaration.before !== undefined &&
|
|
213
|
+
declaration.before !== declaration.text;
|
|
214
|
+
if (Object.hasOwn(closure, key))
|
|
215
|
+
continue;
|
|
216
|
+
const beforeBudget = !paired || declaration.before == null
|
|
217
|
+
? 0
|
|
218
|
+
: declaration.before.length + declaration.text.length > UNIT_MAX_CHARS
|
|
219
|
+
? Math.floor(UNIT_MAX_CHARS / 2)
|
|
220
|
+
: declaration.before.length;
|
|
221
|
+
const before = !paired
|
|
222
|
+
? undefined
|
|
223
|
+
: declaration.before == null
|
|
224
|
+
? declaration.before
|
|
225
|
+
: truncate(declaration.before, beforeBudget);
|
|
226
|
+
const after = truncate(declaration.text, UNIT_MAX_CHARS - (before?.length ?? 0));
|
|
227
|
+
const sliced = after.length < declaration.text.length ||
|
|
228
|
+
(paired && (before?.length ?? 0) < (declaration.before?.length ?? 0));
|
|
229
|
+
const value = !paired
|
|
230
|
+
? after
|
|
231
|
+
: { before: before ?? null, after: declaration.deleted ? null : after };
|
|
232
|
+
const chars = after.length + (before?.length ?? 0);
|
|
233
|
+
const contribution = JSON.stringify(key).length +
|
|
234
|
+
1 +
|
|
235
|
+
JSON.stringify(value).length +
|
|
236
|
+
(Object.keys(closure).length ? 1 : 0);
|
|
237
|
+
const candidateState = { ...state, closure: { ...closure, [key]: value } };
|
|
238
|
+
const reason = budget + contribution > CLOSURE_MAX_CHARS
|
|
239
|
+
? "closure budget exceeded"
|
|
240
|
+
: JSON.stringify(candidateState).length > STATE_MAX_CHARS
|
|
241
|
+
? "serialized state limit exceeded"
|
|
242
|
+
: undefined;
|
|
243
|
+
if (reason) {
|
|
244
|
+
omitted.push({ path, name, reason });
|
|
245
|
+
limitations.push({
|
|
246
|
+
fact: `${key} : declaration omitted, ${reason}`,
|
|
247
|
+
next: "Provide this declaration in a smaller call.",
|
|
248
|
+
});
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
closure[key] = value;
|
|
252
|
+
nextState = candidateState;
|
|
253
|
+
budget += contribution;
|
|
254
|
+
added.push({ path, name, chars });
|
|
255
|
+
if (sliced)
|
|
256
|
+
limitations.push({
|
|
257
|
+
fact: `${key} : sliced declaration (${chars} characters)`,
|
|
258
|
+
next: "Read the complete declaration before drawing conclusions about an omitted part.",
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
limitations.push({
|
|
262
|
+
fact: "Import closure only, semantic depth 1; files without an import link are not checked.",
|
|
263
|
+
next: "Provide configuration, documentation or data dependencies not linked by imports in paths.",
|
|
264
|
+
});
|
|
265
|
+
const seen = new Map();
|
|
266
|
+
for (const limit of limitations) {
|
|
267
|
+
const key = JSON.stringify(limit.path
|
|
268
|
+
? [limit.path, limit.cause, limit.dependency]
|
|
269
|
+
: [limit.fact, limit.next]);
|
|
270
|
+
const previous = seen.get(key);
|
|
271
|
+
if (previous) {
|
|
272
|
+
if (limit.cause === "dynamic import") {
|
|
273
|
+
previous.count = (previous.count ?? 1) + 1;
|
|
274
|
+
previous.fact = `${limit.fact} (${previous.count} occurrences)`;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
else
|
|
278
|
+
seen.set(key, { ...limit });
|
|
279
|
+
}
|
|
280
|
+
const unique = [...seen.values()];
|
|
281
|
+
return { state: nextState, added, omitted, limitations: unique };
|
|
282
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { ASK_MAX_FILES } from "../constants.js";
|
|
2
|
+
const fileSelector = /\b(files|files_before)\s*\[\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')\s*\]/g;
|
|
3
|
+
const selector = /\b[A-Za-z_$][\w$]*(?:(?:\s*\.\s*[A-Za-z_$][\w$]*)|(?:\s*\[\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\d+)\s*\]))*/g;
|
|
4
|
+
function unquote(value) {
|
|
5
|
+
if (value.startsWith('"')) {
|
|
6
|
+
try {
|
|
7
|
+
const parsed = JSON.parse(value);
|
|
8
|
+
if (typeof parsed === "string")
|
|
9
|
+
return parsed;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return value.slice(1, -1);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return value.slice(1, -1).replace(/\\(['\\])/g, "$1");
|
|
16
|
+
}
|
|
17
|
+
function looksLikePath(value) {
|
|
18
|
+
if (/\s/.test(value) || /^(?:[a-z]+:|www\.)/i.test(value))
|
|
19
|
+
return false;
|
|
20
|
+
if (!/[A-Za-z_]/.test(value) ||
|
|
21
|
+
/^(?:state|docs?|files|files_before)\./.test(value))
|
|
22
|
+
return false;
|
|
23
|
+
if (/^(?:e\.g|i\.e|Node\.js|Next\.js|Vue\.js)$/i.test(value))
|
|
24
|
+
return false;
|
|
25
|
+
return (/^(?:\.?\.?\/)?[\w.@+-]+(?:\/[\w.@+-]+)+$/.test(value) ||
|
|
26
|
+
/^[\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(value) ||
|
|
27
|
+
/^\.[A-Za-z][\w.-]*$/.test(value) ||
|
|
28
|
+
/^(?:README|LICENSE|LICENCE|Makefile|Dockerfile|CHANGELOG|AGENTS|CLAUDE)(?:\.[\w-]+)?$/.test(value));
|
|
29
|
+
}
|
|
30
|
+
/** Explicit paths only: numbers, dates and URLs are not evidence references. */
|
|
31
|
+
export function citedPaths(text) {
|
|
32
|
+
const found = new Set();
|
|
33
|
+
const withoutUrls = text.replace(/\b(?:[a-z][a-z\d+.-]*:\/\/|www\.)[^\s<>"'`]+/gi, " ");
|
|
34
|
+
const withoutSelectors = withoutUrls.replace(fileSelector, (_whole, _root, quoted) => {
|
|
35
|
+
found.add(unquote(quoted));
|
|
36
|
+
return " ";
|
|
37
|
+
});
|
|
38
|
+
const withoutFields = withoutSelectors.replace(selector, (value) => value.includes("[") || /^(?:state|docs?|files|files_before)\./.test(value)
|
|
39
|
+
? " "
|
|
40
|
+
: value);
|
|
41
|
+
const unquoted = withoutFields.replace(/`([^`\n]+)`|"([^"\n]+)"|'([^'\n]+)'/g, (_whole, backtick, double, single) => {
|
|
42
|
+
const value = backtick ?? double ?? single ?? "";
|
|
43
|
+
if (looksLikePath(value)) {
|
|
44
|
+
found.add(value);
|
|
45
|
+
return " ";
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
});
|
|
49
|
+
for (const match of unquoted.matchAll(/[\w.@+-]+(?:\/[\w.@+-]+)*/g)) {
|
|
50
|
+
const value = match[0].replace(/[.,;:!?]+$/, "");
|
|
51
|
+
if (looksLikePath(value) &&
|
|
52
|
+
(!value.includes("/") ||
|
|
53
|
+
looksLikePath(value.slice(value.lastIndexOf("/") + 1))))
|
|
54
|
+
found.add(value);
|
|
55
|
+
}
|
|
56
|
+
return [...found];
|
|
57
|
+
}
|
|
58
|
+
function questionText(question) {
|
|
59
|
+
const criteria = question.criteria;
|
|
60
|
+
return [
|
|
61
|
+
question.instructions,
|
|
62
|
+
...(criteria ? Object.values(criteria) : []),
|
|
63
|
+
].join("\n");
|
|
64
|
+
}
|
|
65
|
+
function readingTexts(plan) {
|
|
66
|
+
return plan.readings.map((reading) => ({
|
|
67
|
+
id: reading.id,
|
|
68
|
+
text: [reading.id, reading.twin, ...Object.values(reading.controls ?? {})]
|
|
69
|
+
.flatMap((id) => id && plan.questions[id] ? [questionText(plan.questions[id])] : [])
|
|
70
|
+
.join("\n"),
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
function normalized(path) {
|
|
74
|
+
return path.replace(/^(?:\.\/)+/, "");
|
|
75
|
+
}
|
|
76
|
+
export function resolveAskReferences(plan, note, files, inventory) {
|
|
77
|
+
const references = new Set(readingTexts(plan).flatMap(({ text }) => citedPaths(text)));
|
|
78
|
+
for (const reference of citedPaths(note ?? ""))
|
|
79
|
+
references.add(reference);
|
|
80
|
+
const paths = [...new Set([...inventory, ...Object.keys(files)])];
|
|
81
|
+
const exact = new Map(paths.map((path) => [normalized(path), path]));
|
|
82
|
+
const selected = new Set(Object.keys(files).map(normalized));
|
|
83
|
+
const additions = [];
|
|
84
|
+
const unresolved = [];
|
|
85
|
+
for (const reference of references) {
|
|
86
|
+
if (Object.hasOwn(files, reference))
|
|
87
|
+
continue;
|
|
88
|
+
const key = normalized(reference);
|
|
89
|
+
const direct = exact.get(key);
|
|
90
|
+
const matches = direct
|
|
91
|
+
? [direct]
|
|
92
|
+
: paths.filter((path) => normalized(path).endsWith(`/${key}`));
|
|
93
|
+
if (matches.length !== 1) {
|
|
94
|
+
unresolved.push({
|
|
95
|
+
reference,
|
|
96
|
+
reason: `${reference} is cited but missing from state (${matches.length ? "ambiguous path" : "not found"})`,
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const path = matches[0];
|
|
101
|
+
if (path === undefined)
|
|
102
|
+
continue;
|
|
103
|
+
if (!selected.has(normalized(path)) && selected.size >= ASK_MAX_FILES) {
|
|
104
|
+
unresolved.push({
|
|
105
|
+
reference,
|
|
106
|
+
reason: `${reference} is cited but missing from state (ASK_MAX_FILES limit: ${ASK_MAX_FILES})`,
|
|
107
|
+
});
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
selected.add(normalized(path));
|
|
111
|
+
additions.push({ reference, path });
|
|
112
|
+
}
|
|
113
|
+
return { additions, unresolved };
|
|
114
|
+
}
|
|
115
|
+
function object(value) {
|
|
116
|
+
return value !== null && typeof value === "object";
|
|
117
|
+
}
|
|
118
|
+
function unpack(value) {
|
|
119
|
+
if (typeof value !== "string" || !/^[\s]*[[{]/.test(value))
|
|
120
|
+
return value;
|
|
121
|
+
try {
|
|
122
|
+
return JSON.parse(value);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function empty(value) {
|
|
129
|
+
return (value === null ||
|
|
130
|
+
(typeof value === "string" && value.trim() === "") ||
|
|
131
|
+
(object(value) && Object.keys(value).length === 0));
|
|
132
|
+
}
|
|
133
|
+
function emptyReference(text, state) {
|
|
134
|
+
// File references can be plain paths, not just selectors.
|
|
135
|
+
const files = state.files;
|
|
136
|
+
if (object(files)) {
|
|
137
|
+
for (const reference of citedPaths(text)) {
|
|
138
|
+
if (Object.hasOwn(files, reference) && empty(files[reference]))
|
|
139
|
+
return reference;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const match of text.matchAll(selector)) {
|
|
143
|
+
const parts = [
|
|
144
|
+
...match[0].matchAll(/[A-Za-z_$][\w$]*|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\d+/g),
|
|
145
|
+
].map(([part]) => (/^["']/.test(part) ? unquote(part) : part));
|
|
146
|
+
const root = parts[0];
|
|
147
|
+
if (root === undefined)
|
|
148
|
+
continue;
|
|
149
|
+
let value;
|
|
150
|
+
let offset;
|
|
151
|
+
if (root === "state") {
|
|
152
|
+
value = state;
|
|
153
|
+
offset = 1;
|
|
154
|
+
}
|
|
155
|
+
else if (Object.hasOwn(state, root)) {
|
|
156
|
+
value = state[root];
|
|
157
|
+
offset = 1;
|
|
158
|
+
}
|
|
159
|
+
else
|
|
160
|
+
continue;
|
|
161
|
+
let present = true;
|
|
162
|
+
for (const part of parts.slice(offset)) {
|
|
163
|
+
value = unpack(value);
|
|
164
|
+
if (!object(value) || !Object.hasOwn(value, part)) {
|
|
165
|
+
present = false;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
value = value[part];
|
|
169
|
+
}
|
|
170
|
+
// A file absent from the base proves creation; it is not empty evidence.
|
|
171
|
+
if (root === "files_before" && value === null)
|
|
172
|
+
continue;
|
|
173
|
+
if (present && empty(unpack(value)))
|
|
174
|
+
return match[0].trim();
|
|
175
|
+
}
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
export function blockedAskReadings(plan, state, unresolved, note) {
|
|
179
|
+
const blocked = new Map();
|
|
180
|
+
const notePaths = new Set(citedPaths(note ?? ""));
|
|
181
|
+
const noteEmpty = emptyReference(note ?? "", state);
|
|
182
|
+
for (const { id, text } of readingTexts(plan)) {
|
|
183
|
+
const paths = new Set(citedPaths(text));
|
|
184
|
+
const missing = unresolved.find(({ reference }) => paths.has(reference) || notePaths.has(reference));
|
|
185
|
+
if (missing) {
|
|
186
|
+
blocked.set(id, missing.reason);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const reference = noteEmpty ?? emptyReference(text, state);
|
|
190
|
+
if (reference)
|
|
191
|
+
blocked.set(id, `${reference} is empty`);
|
|
192
|
+
}
|
|
193
|
+
return blocked;
|
|
194
|
+
}
|