@praneeth_54/agentdoctor 3.0.2 → 3.0.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 +33 -2
- package/README.md +5 -4
- package/dist/agent/chat/deterministic.d.ts +2 -0
- package/dist/agent/chat/deterministic.js +91 -3
- package/dist/agent/chat/response.js +25 -4
- package/dist/agent/chat/service.d.ts +1 -0
- package/dist/agent/chat/service.js +28 -10
- package/dist/agent/chat/types.d.ts +3 -0
- package/dist/agent/context/brain-evidence.d.ts +26 -0
- package/dist/agent/context/brain-evidence.js +296 -0
- package/dist/agent/context/retrieve.d.ts +7 -1
- package/dist/agent/context/retrieve.js +204 -63
- package/dist/agent/progress.d.ts +10 -0
- package/dist/agent/progress.js +4 -0
- package/dist/cli/ask-progress.d.ts +17 -0
- package/dist/cli/ask-progress.js +85 -0
- package/dist/cli/commands/chat.d.ts +6 -0
- package/dist/cli/commands/chat.js +16 -0
- package/dist/cli/program.js +2 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/core/understanding/brain/storage/store.js +0 -1
- package/package.json +1 -1
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { loadLatestBrain, searchBrain } from "../../core/brain-cli/service.js";
|
|
3
|
+
const QUERY_STOPWORDS = new Set([
|
|
4
|
+
"a",
|
|
5
|
+
"an",
|
|
6
|
+
"the",
|
|
7
|
+
"is",
|
|
8
|
+
"are",
|
|
9
|
+
"was",
|
|
10
|
+
"were",
|
|
11
|
+
"what",
|
|
12
|
+
"which",
|
|
13
|
+
"who",
|
|
14
|
+
"whom",
|
|
15
|
+
"whose",
|
|
16
|
+
"where",
|
|
17
|
+
"when",
|
|
18
|
+
"why",
|
|
19
|
+
"how",
|
|
20
|
+
"does",
|
|
21
|
+
"do",
|
|
22
|
+
"did",
|
|
23
|
+
"can",
|
|
24
|
+
"could",
|
|
25
|
+
"should",
|
|
26
|
+
"would",
|
|
27
|
+
"will",
|
|
28
|
+
"of",
|
|
29
|
+
"to",
|
|
30
|
+
"for",
|
|
31
|
+
"in",
|
|
32
|
+
"on",
|
|
33
|
+
"at",
|
|
34
|
+
"by",
|
|
35
|
+
"with",
|
|
36
|
+
"from",
|
|
37
|
+
"about",
|
|
38
|
+
"into",
|
|
39
|
+
"this",
|
|
40
|
+
"that",
|
|
41
|
+
"these",
|
|
42
|
+
"those",
|
|
43
|
+
"my",
|
|
44
|
+
"our",
|
|
45
|
+
"your",
|
|
46
|
+
"its",
|
|
47
|
+
"project",
|
|
48
|
+
"projects",
|
|
49
|
+
"module",
|
|
50
|
+
"modules",
|
|
51
|
+
"component",
|
|
52
|
+
"components",
|
|
53
|
+
"belong",
|
|
54
|
+
"belongs",
|
|
55
|
+
"provide",
|
|
56
|
+
"provides",
|
|
57
|
+
"explain",
|
|
58
|
+
"architecture",
|
|
59
|
+
"structure",
|
|
60
|
+
"overview",
|
|
61
|
+
"system",
|
|
62
|
+
"please",
|
|
63
|
+
"tell",
|
|
64
|
+
"me",
|
|
65
|
+
"describe",
|
|
66
|
+
]);
|
|
67
|
+
const PATHISH = /(?:^|[\s(])((?:[\w.-]+\/)+[\w.-]+\.(?:ts|tsx|js|jsx|mjs|cjs|py|php|go|rs|java|kt|dart|rb|cs|md|json|yml|yaml|toml))(?:$|[\s),:;])/i;
|
|
68
|
+
function scoreBrainHitText(text, kind) {
|
|
69
|
+
let score = 0;
|
|
70
|
+
if (/PROVIDES|CONTAINS/i.test(text))
|
|
71
|
+
score += 0.45;
|
|
72
|
+
if (/\bmodule\b/i.test(text) || kind === "component")
|
|
73
|
+
score += 0.25;
|
|
74
|
+
if (/is-domain/i.test(text))
|
|
75
|
+
score += 0.1;
|
|
76
|
+
if (/has-risk/i.test(text))
|
|
77
|
+
score -= 0.35;
|
|
78
|
+
if (/unclear-ownership/i.test(text))
|
|
79
|
+
score -= 0.15;
|
|
80
|
+
return score;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* AgentDoctor control-plane under `.agentdoctor/**` — usable internally,
|
|
84
|
+
* never as ordinary VERIFIED application source evidence.
|
|
85
|
+
*/
|
|
86
|
+
export function isControlPlaneRelativePath(relativePath) {
|
|
87
|
+
const norm = relativePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
88
|
+
return norm === ".agentdoctor" || norm.startsWith(".agentdoctor/");
|
|
89
|
+
}
|
|
90
|
+
/** Significant tokens from a natural-language project question. */
|
|
91
|
+
export function extractQueryTerms(query) {
|
|
92
|
+
const raw = query
|
|
93
|
+
.split(/[^a-zA-Z0-9_./-]+/)
|
|
94
|
+
.map((t) => t.trim())
|
|
95
|
+
.filter((t) => t.length >= 2 && !QUERY_STOPWORDS.has(t.toLowerCase()));
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
const out = [];
|
|
98
|
+
for (const t of raw) {
|
|
99
|
+
const key = t.toLowerCase();
|
|
100
|
+
if (seen.has(key))
|
|
101
|
+
continue;
|
|
102
|
+
seen.add(key);
|
|
103
|
+
out.push(t);
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
function looksLikeRepoRelativePath(locator) {
|
|
108
|
+
const n = locator.replace(/\\/g, "/");
|
|
109
|
+
if (!n || n.includes("\0") || path.isAbsolute(n) || n.split("/").includes(".."))
|
|
110
|
+
return false;
|
|
111
|
+
if (isControlPlaneRelativePath(n))
|
|
112
|
+
return false;
|
|
113
|
+
if (n.includes(".private/") || n.startsWith("AgentDoctorOS/"))
|
|
114
|
+
return false;
|
|
115
|
+
// Prefer file-like or directory module paths (contain a slash or extension).
|
|
116
|
+
return n.includes("/") || /\.\w{1,8}$/.test(n);
|
|
117
|
+
}
|
|
118
|
+
function collectPathsFromHitText(text, into) {
|
|
119
|
+
let match;
|
|
120
|
+
const re = new RegExp(PATHISH.source, "gi");
|
|
121
|
+
while ((match = re.exec(text)) !== null) {
|
|
122
|
+
const p = match[1]?.replace(/\\/g, "/");
|
|
123
|
+
if (p && looksLikeRepoRelativePath(p))
|
|
124
|
+
into.add(p);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Search the latest usable Project Brain for query-relevant claims/components.
|
|
129
|
+
* Brain hits are INFERRED structural evidence — never auto-upgraded to VERIFIED.
|
|
130
|
+
* Stale ownership-boundary brains load as null (same as CLI brain status).
|
|
131
|
+
*/
|
|
132
|
+
export async function collectBrainEvidenceForQuery(root, query, options) {
|
|
133
|
+
const maxHits = options?.maxHits ?? 24;
|
|
134
|
+
const limitations = [];
|
|
135
|
+
const brain = await loadLatestBrain(root);
|
|
136
|
+
if (!brain) {
|
|
137
|
+
return {
|
|
138
|
+
brain: null,
|
|
139
|
+
hits: [],
|
|
140
|
+
citations: [],
|
|
141
|
+
candidateSourcePaths: [],
|
|
142
|
+
limitations: ["No usable project brain snapshot; brain evidence skipped."],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const terms = extractQueryTerms(query);
|
|
146
|
+
const hitMap = new Map();
|
|
147
|
+
const absorb = (hits, boost = 0) => {
|
|
148
|
+
for (const h of hits) {
|
|
149
|
+
const ranked = {
|
|
150
|
+
...h,
|
|
151
|
+
score: h.score + boost + scoreBrainHitText(h.text, h.kind),
|
|
152
|
+
};
|
|
153
|
+
const prev = hitMap.get(h.id);
|
|
154
|
+
if (!prev || ranked.score > prev.score)
|
|
155
|
+
hitMap.set(h.id, ranked);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
// Full query first (matches CLI `brain search`), then significant terms.
|
|
159
|
+
absorb(searchBrain(brain, query), 0.3);
|
|
160
|
+
for (const term of terms) {
|
|
161
|
+
absorb(searchBrain(brain, term), 0.2);
|
|
162
|
+
}
|
|
163
|
+
// Architecture / overview with weak terms: surface top modules + provides/contains claims.
|
|
164
|
+
if (terms.length === 0 || /architecture|structure|overview|explain (the )?project/i.test(query)) {
|
|
165
|
+
const seenNames = new Set();
|
|
166
|
+
for (const c of brain.components) {
|
|
167
|
+
if (c.type === "domain-surface") {
|
|
168
|
+
const key = c.name.toLowerCase();
|
|
169
|
+
if (seenNames.has(key))
|
|
170
|
+
continue;
|
|
171
|
+
seenNames.add(key);
|
|
172
|
+
}
|
|
173
|
+
if (c.type !== "module" && c.type !== "domain-surface" && c.type !== "entrypoint")
|
|
174
|
+
continue;
|
|
175
|
+
const id = c.id;
|
|
176
|
+
const score = c.type === "module" ? 0.85 : c.type === "entrypoint" ? 0.75 : 0.55;
|
|
177
|
+
const prev = hitMap.get(id);
|
|
178
|
+
if (!prev || score > prev.score) {
|
|
179
|
+
hitMap.set(id, {
|
|
180
|
+
kind: "component",
|
|
181
|
+
id,
|
|
182
|
+
text: `${c.name} (${c.type})`.slice(0, 240),
|
|
183
|
+
score,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if (hitMap.size >= maxHits * 2)
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
for (const claim of brain.claims) {
|
|
190
|
+
if (!/PROVIDES|CONTAINS|is-domain/i.test(claim.predicate))
|
|
191
|
+
continue;
|
|
192
|
+
const text = `${claim.subject} ${claim.predicate} ${claim.object}`;
|
|
193
|
+
const score = /PROVIDES|CONTAINS/i.test(claim.predicate) ? 0.9 : 0.6;
|
|
194
|
+
if (!hitMap.has(claim.id) || (hitMap.get(claim.id)?.score ?? 0) < score) {
|
|
195
|
+
hitMap.set(claim.id, {
|
|
196
|
+
kind: "claim",
|
|
197
|
+
id: claim.id,
|
|
198
|
+
text: text.slice(0, 240),
|
|
199
|
+
score,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (hitMap.size >= maxHits * 3)
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const hitsRaw = [...hitMap.values()].sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
207
|
+
// Collapse duplicate domain-surface/module labels (same name, many component ids).
|
|
208
|
+
const hits = [];
|
|
209
|
+
const seenLabels = new Set();
|
|
210
|
+
for (const h of hitsRaw) {
|
|
211
|
+
if (h.kind === "component") {
|
|
212
|
+
const labelKey = h.text.replace(/^comp_[a-f0-9]+\s+/i, "").toLowerCase();
|
|
213
|
+
if (seenLabels.has(labelKey))
|
|
214
|
+
continue;
|
|
215
|
+
seenLabels.add(labelKey);
|
|
216
|
+
}
|
|
217
|
+
hits.push(h);
|
|
218
|
+
if (hits.length >= maxHits)
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
const candidateSourcePaths = new Set();
|
|
222
|
+
const evidenceById = new Map(brain.evidence.map((e) => [e.id, e]));
|
|
223
|
+
const componentById = new Map(brain.components.map((c) => [c.id, c]));
|
|
224
|
+
const claimById = new Map(brain.claims.map((c) => [c.id, c]));
|
|
225
|
+
for (const hit of hits) {
|
|
226
|
+
collectPathsFromHitText(hit.text, candidateSourcePaths);
|
|
227
|
+
if (hit.kind === "component") {
|
|
228
|
+
const comp = componentById.get(hit.id);
|
|
229
|
+
if (comp?.path && looksLikeRepoRelativePath(comp.path)) {
|
|
230
|
+
candidateSourcePaths.add(comp.path.replace(/\\/g, "/"));
|
|
231
|
+
}
|
|
232
|
+
for (const eid of comp?.evidenceIds ?? []) {
|
|
233
|
+
const ev = evidenceById.get(eid);
|
|
234
|
+
if (ev &&
|
|
235
|
+
(ev.kind === "path" || ev.kind === "file-presence") &&
|
|
236
|
+
looksLikeRepoRelativePath(ev.locator)) {
|
|
237
|
+
candidateSourcePaths.add(ev.locator.replace(/\\/g, "/"));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (hit.kind === "claim") {
|
|
242
|
+
const claim = claimById.get(hit.id);
|
|
243
|
+
for (const eid of claim?.evidenceIds ?? []) {
|
|
244
|
+
const ev = evidenceById.get(eid);
|
|
245
|
+
if (ev &&
|
|
246
|
+
(ev.kind === "path" || ev.kind === "file-presence") &&
|
|
247
|
+
looksLikeRepoRelativePath(ev.locator)) {
|
|
248
|
+
candidateSourcePaths.add(ev.locator.replace(/\\/g, "/"));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Subject/object may themselves be module directory paths.
|
|
252
|
+
if (claim && looksLikeRepoRelativePath(claim.subject)) {
|
|
253
|
+
candidateSourcePaths.add(claim.subject.replace(/\\/g, "/"));
|
|
254
|
+
}
|
|
255
|
+
if (claim && looksLikeRepoRelativePath(claim.object)) {
|
|
256
|
+
candidateSourcePaths.add(claim.object.replace(/\\/g, "/"));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Prefer source paths that match query terms (e.g. Proxyshield → proxyshield/...).
|
|
261
|
+
// Deprioritize editor/control noise and has-risk-only paths when better options exist.
|
|
262
|
+
const termLower = terms.map((t) => t.toLowerCase());
|
|
263
|
+
const rankedPaths = [...candidateSourcePaths].sort((a, b) => {
|
|
264
|
+
const score = (p) => {
|
|
265
|
+
let s = 0;
|
|
266
|
+
const lower = p.toLowerCase();
|
|
267
|
+
if (termLower.some((t) => lower.includes(t)))
|
|
268
|
+
s += 5;
|
|
269
|
+
if (/\.(php|ts|tsx|js|py)$/i.test(p))
|
|
270
|
+
s += 2;
|
|
271
|
+
if (p.startsWith(".cursor/") || p.startsWith(".agentdoctor/"))
|
|
272
|
+
s -= 6;
|
|
273
|
+
if (/\.htaccess$/i.test(p))
|
|
274
|
+
s -= 1;
|
|
275
|
+
return s;
|
|
276
|
+
};
|
|
277
|
+
return score(b) - score(a) || a.localeCompare(b);
|
|
278
|
+
});
|
|
279
|
+
const citations = hits.map((h) => ({
|
|
280
|
+
source: "brain",
|
|
281
|
+
evidenceType: "project-brain",
|
|
282
|
+
confidence: "INFERRED",
|
|
283
|
+
excerpt: h.text,
|
|
284
|
+
note: `brain:${h.kind}:${h.id}`,
|
|
285
|
+
}));
|
|
286
|
+
if (hits.length === 0) {
|
|
287
|
+
limitations.push("Project brain loaded but no claims/components matched this query.");
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
brain,
|
|
291
|
+
hits,
|
|
292
|
+
citations,
|
|
293
|
+
candidateSourcePaths: rankedPaths.slice(0, 20),
|
|
294
|
+
limitations,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ContextBundle } from "./types.js";
|
|
2
2
|
import type { RepositoryGraph } from "../../platform/types.js";
|
|
3
|
+
import { type AskProgressReporter } from "../progress.js";
|
|
3
4
|
export interface RetrieveContextOptions {
|
|
4
5
|
root: string;
|
|
5
6
|
query: string;
|
|
@@ -12,11 +13,16 @@ export interface RetrieveContextOptions {
|
|
|
12
13
|
builder?: string;
|
|
13
14
|
astFilesParsed?: number;
|
|
14
15
|
};
|
|
16
|
+
/** Max owned source files to excerpt (after brain + includePaths). */
|
|
17
|
+
maxSourceFiles?: number;
|
|
18
|
+
/** Optional progress reporter (real operation boundaries only). */
|
|
19
|
+
progress?: AskProgressReporter;
|
|
15
20
|
}
|
|
16
21
|
/**
|
|
17
22
|
* Retrieve a budgeted project context pack for the agent.
|
|
18
|
-
*
|
|
23
|
+
* Pipeline: Brain claims → owned source paths → graph plan → ranked excerpts.
|
|
19
24
|
* Hostile paths are rejected via resolveSafeRepoPath + project ownership
|
|
20
25
|
* (containment alone must not promote .private / AgentDoctorOS / nested repos).
|
|
26
|
+
* `.agentdoctor/**` is control-plane and must not become VERIFIED application source evidence.
|
|
21
27
|
*/
|
|
22
28
|
export declare function retrieveProjectContext(options: RetrieveContextOptions): Promise<ContextBundle>;
|
|
@@ -5,22 +5,112 @@ import { buildIntelligenceGraph } from "../../intelligence/graph/build.js";
|
|
|
5
5
|
import { assertProjectOwnedRepoPath, ProjectOwnershipError } from "../../project/ownership.js";
|
|
6
6
|
import { resolveRepoRoot } from "../../utils/path.js";
|
|
7
7
|
import { resolveSafeRepoPath, PathEscapeError } from "../../security/paths.js";
|
|
8
|
+
import { collectBrainEvidenceForQuery, extractQueryTerms, isControlPlaneRelativePath, } from "./brain-evidence.js";
|
|
9
|
+
import { noopAskProgress } from "../progress.js";
|
|
8
10
|
function estimateTokens(text) {
|
|
9
11
|
return Math.ceil(text.length / 4);
|
|
10
12
|
}
|
|
13
|
+
async function tryReadOwnedExcerpt(root, rel, maxExcerpt) {
|
|
14
|
+
const norm = rel.split(path.sep).join("/");
|
|
15
|
+
if (isControlPlaneRelativePath(norm)) {
|
|
16
|
+
return {
|
|
17
|
+
source: "repository",
|
|
18
|
+
path: norm,
|
|
19
|
+
evidenceType: "metadata",
|
|
20
|
+
confidence: "UNKNOWN",
|
|
21
|
+
note: "control_plane: not application source evidence",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const abs = resolveSafeRepoPath(root, rel);
|
|
26
|
+
await assertProjectOwnedRepoPath(root, abs, rel);
|
|
27
|
+
// Directories (module roots) are not source excerpts.
|
|
28
|
+
const st = await fs.stat(abs);
|
|
29
|
+
if (st.isDirectory()) {
|
|
30
|
+
return {
|
|
31
|
+
source: "repository",
|
|
32
|
+
path: norm,
|
|
33
|
+
evidenceType: "metadata",
|
|
34
|
+
confidence: "INFERRED",
|
|
35
|
+
note: "brain-referenced directory (no file excerpt)",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const text = await fs.readFile(abs, "utf8");
|
|
39
|
+
const excerpt = text.slice(0, maxExcerpt);
|
|
40
|
+
return {
|
|
41
|
+
source: "repository",
|
|
42
|
+
path: norm,
|
|
43
|
+
evidenceType: "source-code",
|
|
44
|
+
confidence: "VERIFIED",
|
|
45
|
+
excerpt,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error instanceof PathEscapeError) {
|
|
50
|
+
return {
|
|
51
|
+
source: "repository",
|
|
52
|
+
path: norm,
|
|
53
|
+
evidenceType: "metadata",
|
|
54
|
+
confidence: "UNKNOWN",
|
|
55
|
+
note: "path_escape: rejected",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (error instanceof ProjectOwnershipError) {
|
|
59
|
+
return {
|
|
60
|
+
source: "repository",
|
|
61
|
+
path: norm,
|
|
62
|
+
evidenceType: "metadata",
|
|
63
|
+
confidence: "UNKNOWN",
|
|
64
|
+
note: `ownership_denied: ${error.ownership}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
source: "repository",
|
|
69
|
+
path: norm,
|
|
70
|
+
evidenceType: "metadata",
|
|
71
|
+
confidence: "UNKNOWN",
|
|
72
|
+
note: error instanceof Error ? error.message : String(error),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
11
76
|
/**
|
|
12
77
|
* Retrieve a budgeted project context pack for the agent.
|
|
13
|
-
*
|
|
78
|
+
* Pipeline: Brain claims → owned source paths → graph plan → ranked excerpts.
|
|
14
79
|
* Hostile paths are rejected via resolveSafeRepoPath + project ownership
|
|
15
80
|
* (containment alone must not promote .private / AgentDoctorOS / nested repos).
|
|
81
|
+
* `.agentdoctor/**` is control-plane and must not become VERIFIED application source evidence.
|
|
16
82
|
*/
|
|
17
83
|
export async function retrieveProjectContext(options) {
|
|
18
84
|
const root = resolveRepoRoot(options.root);
|
|
19
85
|
const query = options.query.trim() || "project overview";
|
|
20
86
|
const maxExcerpt = options.maxExcerptChars ?? 1_200;
|
|
87
|
+
const maxSourceFiles = options.maxSourceFiles ?? 10;
|
|
21
88
|
const citations = [];
|
|
22
89
|
const limitations = [];
|
|
23
90
|
const chunks = [];
|
|
91
|
+
const terms = extractQueryTerms(query);
|
|
92
|
+
const progress = options.progress ?? noopAskProgress;
|
|
93
|
+
progress.stage("search", "start");
|
|
94
|
+
let brainPack;
|
|
95
|
+
try {
|
|
96
|
+
brainPack = await collectBrainEvidenceForQuery(root, query);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
progress.stage("search", "fail", error instanceof Error ? error.message : String(error));
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
const brainDetail = brainPack.hits.length > 0
|
|
103
|
+
? `${brainPack.hits.length} relevant claim${brainPack.hits.length === 1 ? "" : "s"}`
|
|
104
|
+
: "no matching claims";
|
|
105
|
+
progress.stage("search", "ok", brainDetail);
|
|
106
|
+
limitations.push(...brainPack.limitations);
|
|
107
|
+
for (const c of brainPack.citations) {
|
|
108
|
+
citations.push(c);
|
|
109
|
+
if (c.excerpt) {
|
|
110
|
+
chunks.push(`--- BRAIN (${c.confidence}) ---\n${c.excerpt}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
progress.stage("retrieve", "start");
|
|
24
114
|
let graph = options.graph;
|
|
25
115
|
try {
|
|
26
116
|
if (!graph) {
|
|
@@ -28,94 +118,145 @@ export async function retrieveProjectContext(options) {
|
|
|
28
118
|
}
|
|
29
119
|
}
|
|
30
120
|
catch (error) {
|
|
121
|
+
progress.stage("retrieve", "fail", error instanceof Error ? error.message : String(error));
|
|
31
122
|
limitations.push(`Graph unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
123
|
+
if (citations.length === 0) {
|
|
124
|
+
citations.push({
|
|
125
|
+
source: "repository",
|
|
126
|
+
evidenceType: "metadata",
|
|
127
|
+
confidence: "UNKNOWN",
|
|
128
|
+
note: "Repository graph could not be built and no brain evidence matched.",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const rendered = [
|
|
132
|
+
`Project root: ${root}`,
|
|
133
|
+
`Query: ${query}`,
|
|
134
|
+
`Brain hits: ${brainPack.hits.length}`,
|
|
135
|
+
"",
|
|
136
|
+
...chunks,
|
|
137
|
+
].join("\n");
|
|
32
138
|
return {
|
|
33
139
|
root,
|
|
34
140
|
query,
|
|
35
|
-
citations
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
evidenceType: "metadata",
|
|
39
|
-
confidence: "UNKNOWN",
|
|
40
|
-
note: "Repository graph could not be built for this query.",
|
|
41
|
-
},
|
|
42
|
-
],
|
|
43
|
-
rendered: `Query: ${query}\n(No graph context available.)`,
|
|
44
|
-
estimatedTokens: 32,
|
|
141
|
+
citations,
|
|
142
|
+
rendered,
|
|
143
|
+
estimatedTokens: estimateTokens(rendered),
|
|
45
144
|
limitations,
|
|
46
145
|
};
|
|
47
146
|
}
|
|
147
|
+
// Prefer graph nodes that match query terms; deprioritize vendor/minified noise.
|
|
48
148
|
const plan = await planContext({
|
|
49
149
|
root,
|
|
50
150
|
graph,
|
|
51
|
-
query,
|
|
52
|
-
budgetTokens: options.budgetTokens ?? 6_000,
|
|
151
|
+
query: terms.length ? terms.join(" ") : query,
|
|
152
|
+
budgetTokens: Math.min(options.budgetTokens ?? 6_000, 4_000),
|
|
53
153
|
});
|
|
54
154
|
limitations.push(...plan.limitations);
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
155
|
+
const orderedPaths = [];
|
|
156
|
+
const seenPaths = new Set();
|
|
157
|
+
const pushPath = (p) => {
|
|
158
|
+
const norm = p.replace(/\\/g, "/");
|
|
159
|
+
if (!norm || seenPaths.has(norm) || isControlPlaneRelativePath(norm))
|
|
160
|
+
return;
|
|
161
|
+
seenPaths.add(norm);
|
|
162
|
+
orderedPaths.push(norm);
|
|
163
|
+
};
|
|
164
|
+
// 1) Brain-derived source paths (highest priority for module questions)
|
|
165
|
+
for (const p of brainPack.candidateSourcePaths)
|
|
166
|
+
pushPath(p);
|
|
167
|
+
// 2) Caller include paths
|
|
168
|
+
for (const p of options.includePaths ?? [])
|
|
169
|
+
pushPath(p);
|
|
170
|
+
// 3) Graph plan — prefer term matches, skip minified/vendor-ish
|
|
171
|
+
const planSorted = [...plan.selected].sort((a, b) => {
|
|
172
|
+
const score = (p) => {
|
|
173
|
+
let s = 0;
|
|
174
|
+
const lower = p.toLowerCase();
|
|
175
|
+
if (terms.some((t) => lower.includes(t.toLowerCase())))
|
|
176
|
+
s += 5;
|
|
177
|
+
if (/\.min\.js$/i.test(p) || /node_modules|vendor|assets\/.*\.umd/i.test(p))
|
|
178
|
+
s -= 4;
|
|
179
|
+
if (/\.(ts|tsx|js|jsx|py|php|go|rs)$/i.test(p))
|
|
180
|
+
s += 1;
|
|
181
|
+
return s;
|
|
182
|
+
};
|
|
183
|
+
return score(b.path) - score(a.path) || b.relevance - a.relevance;
|
|
184
|
+
});
|
|
185
|
+
for (const s of planSorted)
|
|
186
|
+
pushPath(s.path);
|
|
187
|
+
progress.stage("retrieve", "ok", `${Math.min(orderedPaths.length, maxSourceFiles)} candidate source${orderedPaths.length === 1 ? "" : "s"}`);
|
|
188
|
+
progress.stage("verify", "start");
|
|
189
|
+
let sourceExcerpts = 0;
|
|
190
|
+
let ownedVerified = 0;
|
|
191
|
+
try {
|
|
192
|
+
for (const rel of orderedPaths) {
|
|
193
|
+
if (sourceExcerpts >= maxSourceFiles)
|
|
194
|
+
break;
|
|
195
|
+
const citation = await tryReadOwnedExcerpt(root, rel, maxExcerpt);
|
|
196
|
+
if (!citation)
|
|
94
197
|
continue;
|
|
198
|
+
citations.push(citation);
|
|
199
|
+
if (citation.confidence === "VERIFIED" && citation.excerpt && citation.path) {
|
|
200
|
+
chunks.push(`--- FILE ${citation.path} (VERIFIED excerpt) ---\n${citation.excerpt}`);
|
|
201
|
+
sourceExcerpts += 1;
|
|
202
|
+
ownedVerified += 1;
|
|
95
203
|
}
|
|
96
|
-
citations.push({
|
|
97
|
-
source: "repository",
|
|
98
|
-
path: rel,
|
|
99
|
-
evidenceType: "metadata",
|
|
100
|
-
confidence: "UNKNOWN",
|
|
101
|
-
note: error instanceof Error ? error.message : String(error),
|
|
102
|
-
});
|
|
103
204
|
}
|
|
104
205
|
}
|
|
105
|
-
|
|
206
|
+
catch (error) {
|
|
207
|
+
progress.stage("verify", "fail", error instanceof Error ? error.message : String(error));
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
progress.stage("verify", "ok", ownedVerified > 0
|
|
211
|
+
? `${ownedVerified} owned source${ownedVerified === 1 ? "" : "s"}`
|
|
212
|
+
: "no owned source excerpts");
|
|
213
|
+
const hasBrain = brainPack.citations.length > 0;
|
|
214
|
+
const hasVerifiedSource = citations.some((c) => c.confidence === "VERIFIED" && c.evidenceType === "source-code");
|
|
215
|
+
if (!hasBrain && !hasVerifiedSource) {
|
|
106
216
|
citations.push({
|
|
107
217
|
source: "repository",
|
|
108
218
|
evidenceType: "metadata",
|
|
109
219
|
confidence: "UNKNOWN",
|
|
110
|
-
note: "No
|
|
220
|
+
note: "No brain claims or owned source excerpts matched this query within the context budget.",
|
|
111
221
|
});
|
|
112
222
|
limitations.push("No file excerpts retrieved — answer must use UNKNOWN where evidence is missing.");
|
|
113
223
|
}
|
|
224
|
+
else if (hasBrain && !hasVerifiedSource) {
|
|
225
|
+
limitations.push("Brain structural evidence available; no owned source excerpts resolved for this query.");
|
|
226
|
+
}
|
|
227
|
+
// Compact graph hint: only term-matching nodes (not raw first-N noise).
|
|
228
|
+
const graphHints = [];
|
|
229
|
+
if (terms.length > 0) {
|
|
230
|
+
const termLower = terms.map((t) => t.toLowerCase());
|
|
231
|
+
for (const n of graph.nodes) {
|
|
232
|
+
if (!n.path || !n.label)
|
|
233
|
+
continue;
|
|
234
|
+
const blob = `${n.label} ${n.path}`.toLowerCase();
|
|
235
|
+
if (!termLower.some((t) => blob.includes(t)))
|
|
236
|
+
continue;
|
|
237
|
+
if (/\.min\.js$/i.test(n.path) || /chart\.umd|node_modules/i.test(n.path))
|
|
238
|
+
continue;
|
|
239
|
+
graphHints.push(`${n.kind}: ${n.label} @ ${n.path}`);
|
|
240
|
+
if (graphHints.length >= 6)
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (graphHints.length) {
|
|
245
|
+
chunks.push(`--- GRAPH (INFERRED, query-filtered) ---\n${graphHints.join("\n")}`);
|
|
246
|
+
citations.push({
|
|
247
|
+
source: "graph",
|
|
248
|
+
evidenceType: "graph",
|
|
249
|
+
confidence: "INFERRED",
|
|
250
|
+
excerpt: graphHints.join("\n"),
|
|
251
|
+
note: "query-filtered graph nodes",
|
|
252
|
+
});
|
|
253
|
+
}
|
|
114
254
|
const rendered = [
|
|
115
255
|
`Project root: ${root}`,
|
|
116
256
|
`Query: ${query}`,
|
|
117
|
-
`
|
|
118
|
-
`
|
|
257
|
+
`Brain hits: ${brainPack.hits.length}`,
|
|
258
|
+
`Selected source files: ${sourceExcerpts}`,
|
|
259
|
+
`Budget tokens: ${plan.budgetTokens} (plan est. ${plan.estimatedTokens})`,
|
|
119
260
|
"",
|
|
120
261
|
...chunks,
|
|
121
262
|
].join("\n");
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress hooks for ask/context retrieval.
|
|
3
|
+
* Presentation lives in CLI; agent code only reports real operation boundaries.
|
|
4
|
+
*/
|
|
5
|
+
export type AskProgressStageId = "understand" | "search" | "retrieve" | "verify" | "prepare";
|
|
6
|
+
export interface AskProgressReporter {
|
|
7
|
+
stage(id: AskProgressStageId, state: "start" | "ok" | "fail", detail?: string): void;
|
|
8
|
+
stop(): void;
|
|
9
|
+
}
|
|
10
|
+
export declare const noopAskProgress: AskProgressReporter;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AskProgressReporter, AskProgressStageId } from "../agent/progress.js";
|
|
2
|
+
export type { AskProgressReporter, AskProgressStageId };
|
|
3
|
+
export interface CreateAskProgressOptions {
|
|
4
|
+
/** When false, all methods are no-ops (JSON / quiet / tests). */
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
/** Defaults to stderr — keeps stdout free for answers / JSON. */
|
|
7
|
+
stream?: NodeJS.WritableStream;
|
|
8
|
+
/** Override TTY detection (tests). Defaults to stream.isTTY. */
|
|
9
|
+
isTTY?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Human-readable ask progress. Stages must be driven by real operations.
|
|
13
|
+
* Animation uses setInterval only for frame updates while work is in flight —
|
|
14
|
+
* never to invent delay.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createAskProgress(options?: CreateAskProgressOptions): AskProgressReporter;
|
|
17
|
+
export declare function silentAskProgress(): AskProgressReporter;
|