@debugai/mcp 2.4.2 → 2.5.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/README.md +17 -0
- package/dist/backend.d.ts +11 -0
- package/dist/gatherWorkspaceFacts.d.ts +64 -0
- package/dist/gatherWorkspaceFacts.js +369 -0
- package/dist/tools/debugError.js +12 -0
- package/dist/workspaceFacts.d.ts +336 -0
- package/dist/workspaceFacts.js +634 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -233,6 +233,23 @@ Recent server-side work, all of it live for agents already:
|
|
|
233
233
|
- pytest, unittest, jest, vitest and mocha output yields real file paths, so a
|
|
234
234
|
failing test gets cross-file context instead of none.
|
|
235
235
|
|
|
236
|
+
**2.5.0** - The server now reads what your project declares and sends it with the
|
|
237
|
+
error: dependency names and the versions your own manifests pin, including
|
|
238
|
+
`.csproj` for .NET and nested manifests in a monorepo. The engine could never see
|
|
239
|
+
your disk, so it used to guess at third-party APIs - it now knows when a version
|
|
240
|
+
is pinned exactly, and says it does not know when nothing pins it. `.env` is
|
|
241
|
+
never opened; only its existence is reported.
|
|
242
|
+
|
|
243
|
+
**2.4.2**: the bundled server prints the memory it advertises. This package and
|
|
244
|
+
the copy bundled in the VS Code extension are the same server, and only this one
|
|
245
|
+
was rendering the recurrence count and confirmed fix that both of them promise an
|
|
246
|
+
agent at connect time. The section is now one module both copies call, so there
|
|
247
|
+
is nothing left to keep in step by hand.
|
|
248
|
+
|
|
249
|
+
**2.4.1**: two tool descriptions repaired. An em-dash strip had cut both of them
|
|
250
|
+
mid-sentence, which matters more here than it reads: a tool description is the
|
|
251
|
+
only thing an agent sees before deciding whether to call this server at all.
|
|
252
|
+
|
|
236
253
|
**2.4.0**: `report_outcome` gains `unused`. An agent that solved the problem its
|
|
237
254
|
own way had to report `failed`, which marks a fix as tried and beaten when
|
|
238
255
|
nothing of ours was ever run. Both tool surfaces, the npm server and the one
|
package/dist/backend.d.ts
CHANGED
|
@@ -6,6 +6,17 @@ export interface DebugRequest {
|
|
|
6
6
|
file_path?: string;
|
|
7
7
|
project_id?: string;
|
|
8
8
|
framework_hint?: string;
|
|
9
|
+
/**
|
|
10
|
+
* What this repository declares, read off disk by the local server.
|
|
11
|
+
* Optional on purpose: an older gateway ignores an unknown field, and a newer
|
|
12
|
+
* engine treats its absence as "we do not know" rather than "there is none".
|
|
13
|
+
* See ../gatherWorkspaceFacts.ts.
|
|
14
|
+
*
|
|
15
|
+
* Typed as an opaque object rather than a named shape: this module's job is
|
|
16
|
+
* to forward JSON, and importing the fact type here would couple the
|
|
17
|
+
* transport to a contract that gains fields independently of it.
|
|
18
|
+
*/
|
|
19
|
+
workspace_facts?: object;
|
|
9
20
|
}
|
|
10
21
|
export interface DebugEdit {
|
|
11
22
|
file: string;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type WorkspaceFacts } from './workspaceFacts.js';
|
|
2
|
+
/**
|
|
3
|
+
* Opened by exact name only. `.env` is absent on purpose - see the header.
|
|
4
|
+
*
|
|
5
|
+
* Load bearing, and it took a mutation pass to make it so: the reads used to be
|
|
6
|
+
* driven by three hard-coded `joined('package.json')` calls, which meant adding
|
|
7
|
+
* `.env` to this Set changed no behaviour and turned no test red. A guard that
|
|
8
|
+
* cannot be broken by editing it is not a guard. Everything now iterates this
|
|
9
|
+
* list, and `drift.test.ts` pins its exact contents.
|
|
10
|
+
*/
|
|
11
|
+
export declare const MANIFEST_FILES: readonly ["package.json", "requirements.txt", "pyproject.toml"];
|
|
12
|
+
/** A manifest larger than this is not a manifest we can use. */
|
|
13
|
+
export declare const MAX_MANIFEST_BYTES: number;
|
|
14
|
+
/** Directories visited. A monorepo must cost a bounded walk, not a full one. */
|
|
15
|
+
export declare const MAX_DIRS = 400;
|
|
16
|
+
/** How deep. Deep enough for `services/api/src`, shallow enough to stay cheap. */
|
|
17
|
+
export declare const MAX_DEPTH = 6;
|
|
18
|
+
/** Python files collected for `local_modules`. Matches the extension's cap. */
|
|
19
|
+
export declare const MAX_PY_FILES = 600;
|
|
20
|
+
/** .NET project files read. Six services is a normal solution; twelve is plenty. */
|
|
21
|
+
export declare const MAX_CSPROJ = 12;
|
|
22
|
+
/**
|
|
23
|
+
* Nested manifests read, beyond the root ones.
|
|
24
|
+
*
|
|
25
|
+
* A monorepo keeps its real dependencies in subdirectories - this repository's
|
|
26
|
+
* root `package.json` declares exactly one package while `apps/web` declares
|
|
27
|
+
* forty, and salman's react-dom error was about `web/package.json` in a tree
|
|
28
|
+
* whose root is a .NET solution. Reading only the root would have made the
|
|
29
|
+
* feature answer confidently about the wrong half of his project.
|
|
30
|
+
*/
|
|
31
|
+
export declare const MAX_NESTED_MANIFESTS = 8;
|
|
32
|
+
/** Wall clock. A slow or network filesystem costs the facts, never the debug. */
|
|
33
|
+
export declare const DEADLINE_MS = 1500;
|
|
34
|
+
/**
|
|
35
|
+
* How long a gathered set stays good.
|
|
36
|
+
*
|
|
37
|
+
* The VS Code extension has cached these on the same TTL since 2026-09-10 (it
|
|
38
|
+
* shares the framework hint's window) and this copy shipped without one - an
|
|
39
|
+
* agent working through a failing build calls `debug_error` repeatedly, and
|
|
40
|
+
* every call was walking the tree again. Five minutes is also the CORRECTNESS
|
|
41
|
+
* bound, and it is the reason the TTL is short rather than long: install a
|
|
42
|
+
* package and the facts are stale, so the window has to be small enough that a
|
|
43
|
+
* wrong answer cannot outlive the mistake by much.
|
|
44
|
+
*/
|
|
45
|
+
export declare const CACHE_TTL_MS: number;
|
|
46
|
+
/** Test seam. Never called in production. */
|
|
47
|
+
export declare function __resetWorkspaceFactsCache(): void;
|
|
48
|
+
/** What the gatherer adds on top of the shared pure contract. */
|
|
49
|
+
export interface GatheredFacts extends WorkspaceFacts {
|
|
50
|
+
/**
|
|
51
|
+
* A `.env` (or `.env.*`) sits at the project root.
|
|
52
|
+
*
|
|
53
|
+
* Recorded because two of the 76 fixes in the 2026-09-13 corpus turned on it
|
|
54
|
+
* - one workspace had only `.env.example`, another had a deployed value that
|
|
55
|
+
* differed from the local file - and because "there is one" is the whole of
|
|
56
|
+
* what can be safely known. The contents are never read.
|
|
57
|
+
*/
|
|
58
|
+
has_env_file: boolean;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Gather what this repository declares. Returns null when there is nothing
|
|
62
|
+
* worth sending, when the root is unusable, or on any failure at all.
|
|
63
|
+
*/
|
|
64
|
+
export declare function gatherWorkspaceFacts(root: string | null | undefined): GatheredFacts | null;
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read this project's manifests off disk, so the engine stops guessing.
|
|
3
|
+
*
|
|
4
|
+
* ## The hole
|
|
5
|
+
*
|
|
6
|
+
* `workspaceFacts.ts` (pure, beside this file) has existed since 2026-09-10 and
|
|
7
|
+
* answers the question every wrong diagnosis needed: what is actually on this
|
|
8
|
+
* machine. It runs in the VS Code extension only. It has never run here.
|
|
9
|
+
*
|
|
10
|
+
* A read of 76 production `debug_feedback.actual_fix` rows on 2026-09-13 put
|
|
11
|
+
* numbers on that: the largest single class of wrong answer, 24%, needed a fact
|
|
12
|
+
* about the MACHINE rather than about the error - an interpreter, a declared
|
|
13
|
+
* dependency, an installed version. And 54 of the last 83 debugs arrived over
|
|
14
|
+
* MCP, where none of those facts are sent.
|
|
15
|
+
*
|
|
16
|
+
* The clearest case is one line of one file on 2026-09-12. Four consecutive
|
|
17
|
+
* answers guessed at `Elastic.Clients.Elasticsearch` constructor signatures and
|
|
18
|
+
* a fifth invented `.AddPassiveHealthCheckPolicies()` on YARP, at confidence 92,
|
|
19
|
+
* while the user's own `.csproj` said `Version="2.3.0"` the whole time - and
|
|
20
|
+
* this process was running inside that repository.
|
|
21
|
+
*
|
|
22
|
+
* ## Why the server reads them rather than asking the agent
|
|
23
|
+
*
|
|
24
|
+
* Same reason `sourceContext.ts` reads the files a trace names: this is a local
|
|
25
|
+
* process with the filesystem the engine lacks, and an agent that has to be
|
|
26
|
+
* asked for something will be asked differently by every client. Reading costs
|
|
27
|
+
* one bounded walk; asking costs a contract with every MCP host there is.
|
|
28
|
+
*
|
|
29
|
+
* ## What it refuses to do
|
|
30
|
+
*
|
|
31
|
+
* The security model here is an EXACT-NAME allowlist, and it has to be, because
|
|
32
|
+
* the extension allowlist that protects `sourceContext.ts` does not apply: a
|
|
33
|
+
* manifest has no source extension. Only the names in `MANIFEST_FILES` are ever
|
|
34
|
+
* opened, plus files matching `PROJECT_EXT` whose names vary by project.
|
|
35
|
+
*
|
|
36
|
+
* `.env` is deliberately NOT in that list, and never will be. It is the single
|
|
37
|
+
* most useful-looking file here and it holds secrets; two production rows had a
|
|
38
|
+
* live Stripe key and a Discord bot token pasted into an error box already, and
|
|
39
|
+
* we are not going to be the ones who send the next one. Its EXISTENCE is a
|
|
40
|
+
* fact worth having and costs nothing - `has_env_file` says so without reading
|
|
41
|
+
* a byte.
|
|
42
|
+
*
|
|
43
|
+
* Everything resolves inside the project root after symlinks, the walk is
|
|
44
|
+
* depth- and count-bounded, and every read is size-capped. A repository is
|
|
45
|
+
* untrusted input; it was cloned, not written.
|
|
46
|
+
*
|
|
47
|
+
* ## Never throws
|
|
48
|
+
*
|
|
49
|
+
* Returns `null` rather than raising, on every path. A debug that fails because
|
|
50
|
+
* the fact-gatherer tripped over a permissions error would be strictly worse
|
|
51
|
+
* than the guessing this replaces.
|
|
52
|
+
*/
|
|
53
|
+
import { readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
54
|
+
import { join, resolve, extname, relative, sep } from 'node:path';
|
|
55
|
+
import { buildWorkspaceFacts } from './workspaceFacts.js';
|
|
56
|
+
/**
|
|
57
|
+
* Opened by exact name only. `.env` is absent on purpose - see the header.
|
|
58
|
+
*
|
|
59
|
+
* Load bearing, and it took a mutation pass to make it so: the reads used to be
|
|
60
|
+
* driven by three hard-coded `joined('package.json')` calls, which meant adding
|
|
61
|
+
* `.env` to this Set changed no behaviour and turned no test red. A guard that
|
|
62
|
+
* cannot be broken by editing it is not a guard. Everything now iterates this
|
|
63
|
+
* list, and `drift.test.ts` pins its exact contents.
|
|
64
|
+
*/
|
|
65
|
+
export const MANIFEST_FILES = [
|
|
66
|
+
'package.json',
|
|
67
|
+
'requirements.txt',
|
|
68
|
+
'pyproject.toml',
|
|
69
|
+
];
|
|
70
|
+
function isManifestName(name) {
|
|
71
|
+
return MANIFEST_FILES.includes(name);
|
|
72
|
+
}
|
|
73
|
+
/** .NET project files. Names vary per project, so these go by extension. */
|
|
74
|
+
const PROJECT_EXT = new Set(['.csproj', '.fsproj', '.vbproj']);
|
|
75
|
+
/** Never descended into. Same list the chunker and sourceContext use. */
|
|
76
|
+
const SKIP_DIRS = new Set([
|
|
77
|
+
'node_modules', '.git', '__pycache__', '.venv', 'venv', 'env', '.virtualenv',
|
|
78
|
+
'dist', 'build', 'out', '.next', '.nuxt', 'coverage', 'target', 'bin', 'obj',
|
|
79
|
+
'.idea', '.vscode', '.cache', 'vendor', 'site-packages',
|
|
80
|
+
]);
|
|
81
|
+
/** A manifest larger than this is not a manifest we can use. */
|
|
82
|
+
export const MAX_MANIFEST_BYTES = 512 * 1024;
|
|
83
|
+
/** Directories visited. A monorepo must cost a bounded walk, not a full one. */
|
|
84
|
+
export const MAX_DIRS = 400;
|
|
85
|
+
/** How deep. Deep enough for `services/api/src`, shallow enough to stay cheap. */
|
|
86
|
+
export const MAX_DEPTH = 6;
|
|
87
|
+
/** Python files collected for `local_modules`. Matches the extension's cap. */
|
|
88
|
+
export const MAX_PY_FILES = 600;
|
|
89
|
+
/** .NET project files read. Six services is a normal solution; twelve is plenty. */
|
|
90
|
+
export const MAX_CSPROJ = 12;
|
|
91
|
+
/**
|
|
92
|
+
* Nested manifests read, beyond the root ones.
|
|
93
|
+
*
|
|
94
|
+
* A monorepo keeps its real dependencies in subdirectories - this repository's
|
|
95
|
+
* root `package.json` declares exactly one package while `apps/web` declares
|
|
96
|
+
* forty, and salman's react-dom error was about `web/package.json` in a tree
|
|
97
|
+
* whose root is a .NET solution. Reading only the root would have made the
|
|
98
|
+
* feature answer confidently about the wrong half of his project.
|
|
99
|
+
*/
|
|
100
|
+
export const MAX_NESTED_MANIFESTS = 8;
|
|
101
|
+
/** Wall clock. A slow or network filesystem costs the facts, never the debug. */
|
|
102
|
+
export const DEADLINE_MS = 1500;
|
|
103
|
+
/**
|
|
104
|
+
* How long a gathered set stays good.
|
|
105
|
+
*
|
|
106
|
+
* The VS Code extension has cached these on the same TTL since 2026-09-10 (it
|
|
107
|
+
* shares the framework hint's window) and this copy shipped without one - an
|
|
108
|
+
* agent working through a failing build calls `debug_error` repeatedly, and
|
|
109
|
+
* every call was walking the tree again. Five minutes is also the CORRECTNESS
|
|
110
|
+
* bound, and it is the reason the TTL is short rather than long: install a
|
|
111
|
+
* package and the facts are stale, so the window has to be small enough that a
|
|
112
|
+
* wrong answer cannot outlive the mistake by much.
|
|
113
|
+
*/
|
|
114
|
+
export const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
115
|
+
let cached = null;
|
|
116
|
+
/** Test seam. Never called in production. */
|
|
117
|
+
export function __resetWorkspaceFactsCache() {
|
|
118
|
+
cached = null;
|
|
119
|
+
}
|
|
120
|
+
function insideRoot(candidate, root) {
|
|
121
|
+
let realRoot;
|
|
122
|
+
let realCandidate;
|
|
123
|
+
try {
|
|
124
|
+
realRoot = realpathSync(root);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
realCandidate = realpathSync(candidate);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const rel = relative(realRoot, realCandidate);
|
|
136
|
+
return rel === '' || (!rel.startsWith('..') && !rel.startsWith(sep + '..'));
|
|
137
|
+
}
|
|
138
|
+
function readCapped(path, root) {
|
|
139
|
+
try {
|
|
140
|
+
if (!insideRoot(path, root)) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const st = statSync(path);
|
|
144
|
+
if (!st.isFile() || st.size > MAX_MANIFEST_BYTES) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return readFileSync(path, 'utf8');
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* One bounded pass over the tree.
|
|
155
|
+
*
|
|
156
|
+
* Breadth-first so that a shallow, relevant directory is always visited before
|
|
157
|
+
* a deep one - when the caps bite, what survives is the top of the project
|
|
158
|
+
* rather than whichever branch the walk happened to enter first.
|
|
159
|
+
*/
|
|
160
|
+
function walk(root, deadline) {
|
|
161
|
+
const out = {
|
|
162
|
+
pythonFiles: [], pythonTruncated: false,
|
|
163
|
+
csprojPaths: [], csprojTruncated: false,
|
|
164
|
+
nested: [], nestedTruncated: false,
|
|
165
|
+
rootDirs: [], hasEnvFile: false,
|
|
166
|
+
};
|
|
167
|
+
const queue = [{ dir: root, depth: 0 }];
|
|
168
|
+
let visited = 0;
|
|
169
|
+
while (queue.length > 0) {
|
|
170
|
+
if (Date.now() > deadline) {
|
|
171
|
+
out.pythonTruncated = true;
|
|
172
|
+
out.csprojTruncated = true;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
if (visited >= MAX_DIRS) {
|
|
176
|
+
out.pythonTruncated = true;
|
|
177
|
+
out.csprojTruncated = true;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
const next = queue.shift();
|
|
181
|
+
if (!next) {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
const { dir, depth } = next;
|
|
185
|
+
visited += 1;
|
|
186
|
+
let entries;
|
|
187
|
+
try {
|
|
188
|
+
entries = readdirSync(dir, { withFileTypes: true })
|
|
189
|
+
.map(d => ({ name: d.name, isDir: d.isDirectory() }));
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
for (const e of entries) {
|
|
195
|
+
// A symlink reports `isDirectory() === false`, so a link to a directory
|
|
196
|
+
// is never descended into - which was accidental protection until a
|
|
197
|
+
// mutation pass showed the containment test passing for that reason
|
|
198
|
+
// rather than for the check it was written to exercise. A symlinked FILE
|
|
199
|
+
// does fall through to the file branch below, gets collected, and is
|
|
200
|
+
// stopped by `insideRoot` inside `readCapped`. Both halves are needed and
|
|
201
|
+
// both are now pinned by their own test.
|
|
202
|
+
if (e.isDir) {
|
|
203
|
+
if (depth === 0) {
|
|
204
|
+
out.rootDirs.push(e.name);
|
|
205
|
+
}
|
|
206
|
+
if (SKIP_DIRS.has(e.name) || e.name.startsWith('.')) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (depth + 1 <= MAX_DEPTH) {
|
|
210
|
+
queue.push({ dir: join(dir, e.name), depth: depth + 1 });
|
|
211
|
+
}
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (depth === 0 && (e.name === '.env' || e.name.startsWith('.env.'))) {
|
|
215
|
+
// Recorded, never opened. See the header.
|
|
216
|
+
out.hasEnvFile = true;
|
|
217
|
+
}
|
|
218
|
+
if (depth > 0 && isManifestName(e.name)) {
|
|
219
|
+
if (out.nested.length < MAX_NESTED_MANIFESTS) {
|
|
220
|
+
out.nested.push({ name: e.name, path: join(dir, e.name) });
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
out.nestedTruncated = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const ext = extname(e.name).toLowerCase();
|
|
227
|
+
if (ext === '.py') {
|
|
228
|
+
if (out.pythonFiles.length < MAX_PY_FILES) {
|
|
229
|
+
out.pythonFiles.push(relative(root, join(dir, e.name)));
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
out.pythonTruncated = true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else if (PROJECT_EXT.has(ext)) {
|
|
236
|
+
if (out.csprojPaths.length < MAX_CSPROJ) {
|
|
237
|
+
out.csprojPaths.push(join(dir, e.name));
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
out.csprojTruncated = true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Gather what this repository declares. Returns null when there is nothing
|
|
249
|
+
* worth sending, when the root is unusable, or on any failure at all.
|
|
250
|
+
*/
|
|
251
|
+
export function gatherWorkspaceFacts(root) {
|
|
252
|
+
try {
|
|
253
|
+
if (!root || typeof root !== 'string') {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const abs = resolve(root);
|
|
257
|
+
// Keyed by root, not merely time: a single agent process can be pointed at
|
|
258
|
+
// more than one repository, and serving one project's manifests for another
|
|
259
|
+
// would be worse than sending nothing at all.
|
|
260
|
+
const now = Date.now();
|
|
261
|
+
if (cached && cached.root === abs && (now - cached.at) < CACHE_TTL_MS) {
|
|
262
|
+
return cached.facts;
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
if (!statSync(abs).isDirectory()) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// Deliberately NOT cached. A root that does not exist yet may exist on
|
|
271
|
+
// the next call (a clone finishing, a folder being opened), and caching
|
|
272
|
+
// the failure would hold the feature off for five minutes for no reason.
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
const deadline = Date.now() + DEADLINE_MS;
|
|
276
|
+
const walked = walk(abs, deadline);
|
|
277
|
+
const manifest = (name) => isManifestName(name) ? readCapped(join(abs, name), abs) : null;
|
|
278
|
+
// Nested manifests, concatenated per KIND into the same string the root one
|
|
279
|
+
// occupies. Concatenation is safe for every format here and keeps the pure
|
|
280
|
+
// builder's contract untouched: requirements.txt is line-oriented, and both
|
|
281
|
+
// JSON and TOML parsers in `workspaceFacts.ts` are regex-shaped rather than
|
|
282
|
+
// strict, by the design documented there. A package.json that fails to
|
|
283
|
+
// parse contributes nothing rather than breaking the set.
|
|
284
|
+
const extraByKind = {};
|
|
285
|
+
for (const n of walked.nested) {
|
|
286
|
+
if (Date.now() > deadline) {
|
|
287
|
+
walked.nestedTruncated = true;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
const text = readCapped(n.path, abs);
|
|
291
|
+
if (text === null) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
(extraByKind[n.name] ||= []).push(text);
|
|
295
|
+
}
|
|
296
|
+
const joined = (name) => {
|
|
297
|
+
const root = manifest(name);
|
|
298
|
+
const extra = extraByKind[name] || [];
|
|
299
|
+
if (root === null && extra.length === 0) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
if (name === 'package.json') {
|
|
303
|
+
// JSON cannot be concatenated. Merge the dependency blocks instead,
|
|
304
|
+
// which is the only part `depsFromPackageJson` and its version twin
|
|
305
|
+
// read. Anything unparseable is skipped, never thrown.
|
|
306
|
+
const merged = {};
|
|
307
|
+
for (const text of [root, ...extra]) {
|
|
308
|
+
if (text === null) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
let pkg;
|
|
312
|
+
try {
|
|
313
|
+
pkg = JSON.parse(text);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (!pkg || typeof pkg !== 'object') {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
322
|
+
const block = pkg[field];
|
|
323
|
+
if (!block || typeof block !== 'object' || Array.isArray(block)) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
merged[field] = { ...block, ...(merged[field] || {}) }; // first file wins
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return JSON.stringify(merged);
|
|
330
|
+
}
|
|
331
|
+
return [root, ...extra].filter((t) => t !== null).join('\n');
|
|
332
|
+
};
|
|
333
|
+
const csprojFiles = [];
|
|
334
|
+
for (const p of walked.csprojPaths) {
|
|
335
|
+
if (Date.now() > deadline) {
|
|
336
|
+
walked.csprojTruncated = true;
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
const text = readCapped(p, abs);
|
|
340
|
+
if (text !== null) {
|
|
341
|
+
csprojFiles.push(text);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const facts = buildWorkspaceFacts({
|
|
345
|
+
packageJson: joined('package.json'),
|
|
346
|
+
requirementsTxt: joined('requirements.txt'),
|
|
347
|
+
pyprojectToml: joined('pyproject.toml'),
|
|
348
|
+
csprojFiles,
|
|
349
|
+
// A manifest list we stopped enumerating declares more than we can see,
|
|
350
|
+
// and a name missing from a short list must prove nothing.
|
|
351
|
+
csprojTruncated: walked.csprojTruncated || walked.nestedTruncated,
|
|
352
|
+
pythonFiles: walked.pythonFiles,
|
|
353
|
+
pythonFilesTruncated: walked.pythonTruncated,
|
|
354
|
+
rootDirs: walked.rootDirs,
|
|
355
|
+
// Deliberately absent: `python_interpreter`. VS Code's Python extension
|
|
356
|
+
// reports which interpreter is SELECTED; this process has no equivalent
|
|
357
|
+
// and could only report its own `process.execPath`, which is node. A
|
|
358
|
+
// wrong fact is worse than a missing one - the engine treats absence as
|
|
359
|
+
// "we do not know" and says so, which is true here.
|
|
360
|
+
pythonInterpreter: null,
|
|
361
|
+
});
|
|
362
|
+
const out = { ...facts, has_env_file: walked.hasEnvFile };
|
|
363
|
+
cached = { root: abs, at: now, facts: out };
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
package/dist/tools/debugError.js
CHANGED
|
@@ -5,6 +5,7 @@ import { resolveAuth } from './authGate.js';
|
|
|
5
5
|
import { getProjectId, getProjectRoot } from '../project.js';
|
|
6
6
|
import { memorySection } from '../memorySection.js';
|
|
7
7
|
import { resolveSourceContext } from '../sourceContext.js';
|
|
8
|
+
import { gatherWorkspaceFacts } from '../gatherWorkspaceFacts.js';
|
|
8
9
|
// Tri-state verification labeling (docs/plan-v2-contract-phase1.md §1).
|
|
9
10
|
// The null case is rendered ON PURPOSE: a confidence number nothing checked
|
|
10
11
|
// must never look the same as one that was mechanically verified.
|
|
@@ -96,6 +97,16 @@ export function registerDebugError(server, config) {
|
|
|
96
97
|
}
|
|
97
98
|
const snippet = codeSnippet ?? resolved?.snippet;
|
|
98
99
|
const effectiveFilePath = filePath ?? resolved?.files[0]?.label;
|
|
100
|
+
// What this repository declares - dependency names and the versions its
|
|
101
|
+
// own manifests pin. The engine cannot read a disk and this process is
|
|
102
|
+
// sitting on the one that matters.
|
|
103
|
+
//
|
|
104
|
+
// Unconditional, unlike the source resolution above: an agent's snippet
|
|
105
|
+
// can replace what WE would have read, but no agent sends a .csproj, and
|
|
106
|
+
// 24% of the wrong answers in the 2026-09-13 corpus needed exactly this.
|
|
107
|
+
// Bounded and total - see ../gatherWorkspaceFacts.ts - so the worst case
|
|
108
|
+
// is null and a prompt identical to today's.
|
|
109
|
+
const workspaceFacts = gatherWorkspaceFacts(getProjectRoot());
|
|
99
110
|
try {
|
|
100
111
|
const result = await callDebugBackend({
|
|
101
112
|
error_message: errorText,
|
|
@@ -108,6 +119,7 @@ export function registerDebugError(server, config) {
|
|
|
108
119
|
// from the git repo root so it agrees with the VS Code extension's
|
|
109
120
|
// md5(workspaceFolder) for the same checkout. See ../project.ts.
|
|
110
121
|
project_id: getProjectId() ?? undefined,
|
|
122
|
+
workspace_facts: workspaceFacts ?? undefined,
|
|
111
123
|
// framework_hint deliberately omitted: a language ('python') is not a
|
|
112
124
|
// framework ('fastapi'), and sending it bypasses the engine's
|
|
113
125
|
// framework detection — FastAPI/React errors lose their expert hints.
|