homegraph 1.5.2 → 1.5.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 +131 -0
- package/README.md +15 -8
- package/dist/arkui/index.d.ts +4 -0
- package/dist/arkui/index.d.ts.map +1 -0
- package/dist/arkui/index.js +23 -0
- package/dist/arkui/index.js.map +1 -0
- package/dist/arkui/migrate-passage.d.ts +28 -0
- package/dist/arkui/migrate-passage.d.ts.map +1 -0
- package/dist/arkui/migrate-passage.js +310 -0
- package/dist/arkui/migrate-passage.js.map +1 -0
- package/dist/arkui/migrate-semantics.d.ts +47 -0
- package/dist/arkui/migrate-semantics.d.ts.map +1 -0
- package/dist/arkui/migrate-semantics.js +229 -0
- package/dist/arkui/migrate-semantics.js.map +1 -0
- package/dist/arkui/migrate-snapshot.d.ts +79 -0
- package/dist/arkui/migrate-snapshot.d.ts.map +1 -0
- package/dist/arkui/migrate-snapshot.js +340 -0
- package/dist/arkui/migrate-snapshot.js.map +1 -0
- package/dist/bin/fatal-handler.js +2 -2
- package/dist/bin/homegraph.js +105 -156
- package/dist/bin/homegraph.js.map +1 -1
- package/dist/bin/node-version-check.d.ts +6 -15
- package/dist/bin/node-version-check.d.ts.map +1 -1
- package/dist/bin/node-version-check.js +8 -37
- package/dist/bin/node-version-check.js.map +1 -1
- package/dist/db/index.d.ts +36 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +92 -6
- package/dist/db/index.js.map +1 -1
- package/dist/db/migrations.d.ts +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +15 -1
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/queries.d.ts +42 -0
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +177 -10
- package/dist/db/queries.js.map +1 -1
- package/dist/db/schema.sql +6 -1
- package/dist/db/sqlite-adapter.d.ts +8 -3
- package/dist/db/sqlite-adapter.d.ts.map +1 -1
- package/dist/db/sqlite-adapter.js +45 -3
- package/dist/db/sqlite-adapter.js.map +1 -1
- package/dist/extraction/extraction-version.d.ts +1 -1
- package/dist/extraction/extraction-version.js +1 -1
- package/dist/extraction/generated-detection.d.ts +42 -11
- package/dist/extraction/generated-detection.d.ts.map +1 -1
- package/dist/extraction/generated-detection.js +141 -20
- package/dist/extraction/generated-detection.js.map +1 -1
- package/dist/extraction/index.d.ts +42 -0
- package/dist/extraction/index.d.ts.map +1 -1
- package/dist/extraction/index.js +96 -0
- package/dist/extraction/index.js.map +1 -1
- package/dist/extraction/languages/arkts.d.ts +19 -2
- package/dist/extraction/languages/arkts.d.ts.map +1 -1
- package/dist/extraction/languages/arkts.js +460 -110
- package/dist/extraction/languages/arkts.js.map +1 -1
- package/dist/extraction/wasm-runtime-flags.d.ts.map +1 -1
- package/dist/extraction/wasm-runtime-flags.js +9 -6
- package/dist/extraction/wasm-runtime-flags.js.map +1 -1
- package/dist/graph-sources.d.ts +41 -0
- package/dist/graph-sources.d.ts.map +1 -0
- package/dist/graph-sources.js +82 -0
- package/dist/graph-sources.js.map +1 -0
- package/dist/index.d.ts +19 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +79 -9
- package/dist/index.js.map +1 -1
- package/dist/installer/index.d.ts.map +1 -1
- package/dist/installer/index.js +0 -48
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/targets/antigravity.js +1 -1
- package/dist/installer/targets/antigravity.js.map +1 -1
- package/dist/installer/targets/deveco.js +1 -1
- package/dist/installer/targets/deveco.js.map +1 -1
- package/dist/installer/targets/hermes.d.ts +1 -1
- package/dist/installer/targets/hermes.js +2 -2
- package/dist/installer/targets/hermes.js.map +1 -1
- package/dist/installer/targets/opencode.js +1 -1
- package/dist/installer/targets/opencode.js.map +1 -1
- package/dist/installer/targets/shared.js +1 -1
- package/dist/installer/targets/shared.js.map +1 -1
- package/dist/mcp/daemon-paths.d.ts +5 -4
- package/dist/mcp/daemon-paths.d.ts.map +1 -1
- package/dist/mcp/daemon-paths.js +28 -13
- package/dist/mcp/daemon-paths.js.map +1 -1
- package/dist/mcp/daemon-registry.d.ts.map +1 -1
- package/dist/mcp/daemon-registry.js +4 -1
- package/dist/mcp/daemon-registry.js.map +1 -1
- package/dist/mcp/daemon.d.ts +1 -1
- package/dist/mcp/daemon.js +1 -1
- package/dist/mcp/engine.d.ts.map +1 -1
- package/dist/mcp/engine.js +12 -3
- package/dist/mcp/engine.js.map +1 -1
- package/dist/mcp/explore-dedup.d.ts +137 -0
- package/dist/mcp/explore-dedup.d.ts.map +1 -0
- package/dist/mcp/explore-dedup.js +236 -0
- package/dist/mcp/explore-dedup.js.map +1 -0
- package/dist/mcp/explore-session-state.d.ts +217 -0
- package/dist/mcp/explore-session-state.d.ts.map +1 -0
- package/dist/mcp/explore-session-state.js +322 -0
- package/dist/mcp/explore-session-state.js.map +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +4 -7
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/proxy.d.ts.map +1 -1
- package/dist/mcp/proxy.js +5 -14
- package/dist/mcp/proxy.js.map +1 -1
- package/dist/mcp/query-worker.d.ts +1 -1
- package/dist/mcp/query-worker.js +1 -1
- package/dist/mcp/server-instructions.d.ts +7 -2
- package/dist/mcp/server-instructions.d.ts.map +1 -1
- package/dist/mcp/server-instructions.js +104 -28
- package/dist/mcp/server-instructions.js.map +1 -1
- package/dist/mcp/session.d.ts +14 -3
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +17 -14
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/startup-handshake.d.ts +1 -1
- package/dist/mcp/startup-handshake.js +1 -1
- package/dist/mcp/tools.d.ts +117 -7
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +3056 -503
- package/dist/mcp/tools.js.map +1 -1
- package/dist/resolution/frameworks/arkts-napi.d.ts +14 -0
- package/dist/resolution/frameworks/arkts-napi.d.ts.map +1 -1
- package/dist/resolution/frameworks/arkts-napi.js +169 -49
- package/dist/resolution/frameworks/arkts-napi.js.map +1 -1
- package/dist/search/query-utils.d.ts +116 -4
- package/dist/search/query-utils.d.ts.map +1 -1
- package/dist/search/query-utils.js +869 -60
- package/dist/search/query-utils.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/upgrade/index.d.ts +14 -26
- package/dist/upgrade/index.d.ts.map +1 -1
- package/dist/upgrade/index.js +28 -123
- package/dist/upgrade/index.js.map +1 -1
- package/dist/upgrade/remove-binary.d.ts +19 -19
- package/dist/upgrade/remove-binary.d.ts.map +1 -1
- package/dist/upgrade/remove-binary.js +20 -21
- package/dist/upgrade/remove-binary.js.map +1 -1
- package/dist/upgrade/update-check.d.ts +4 -3
- package/dist/upgrade/update-check.d.ts.map +1 -1
- package/dist/upgrade/update-check.js +4 -3
- package/dist/upgrade/update-check.js.map +1 -1
- package/package.json +12 -8
- package/dist/telemetry/index.d.ts +0 -143
- package/dist/telemetry/index.d.ts.map +0 -1
- package/dist/telemetry/index.js +0 -541
- package/dist/telemetry/index.js.map +0 -1
package/dist/mcp/tools.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* Defines the tools exposed by the HomeGraph MCP server.
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.ToolHandler = exports.tools = exports.PathRefusalError = exports.NotIndexedError = void 0;
|
|
8
|
+
exports.ToolHandler = exports.tools = exports.PathRefusalError = exports.GraphSourcesDisabledError = exports.NotIndexedError = void 0;
|
|
9
|
+
exports.__setLoadHomeGraphForTests = __setLoadHomeGraphForTests;
|
|
9
10
|
exports.normalizeQuerySpelling = normalizeQuerySpelling;
|
|
10
11
|
exports.getExploreBudget = getExploreBudget;
|
|
11
12
|
exports.getExploreOutputBudget = getExploreOutputBudget;
|
|
@@ -22,13 +23,26 @@ const directory_1 = require("../directory");
|
|
|
22
23
|
// schemas), but it must NOT drag in sqlite/query layers before the daemon binds;
|
|
23
24
|
// HomeGraph is pulled in only when a tool actually opens a project. require() is
|
|
24
25
|
// sync + cached (CommonJS build).
|
|
25
|
-
const loadHomeGraph = () => require('../index').default;
|
|
26
|
+
const loadHomeGraph = () => loadHomeGraphForTests ?? require('../index').default;
|
|
27
|
+
// Test seam (same pattern as the watcher's `__setFsWatchForTests`): vitest's
|
|
28
|
+
// module transform can't service the lazy `require('../index')` above, so
|
|
29
|
+
// in-process tests that exercise a genuine cross-project open (an explicit
|
|
30
|
+
// `projectPath` to a different project — issue #1474's repro shape) inject the
|
|
31
|
+
// already-imported class here. Never set outside tests.
|
|
32
|
+
let loadHomeGraphForTests = null;
|
|
33
|
+
function __setLoadHomeGraphForTests(cls) {
|
|
34
|
+
loadHomeGraphForTests = cls;
|
|
35
|
+
}
|
|
26
36
|
const worktree_1 = require("../sync/worktree");
|
|
37
|
+
const graph_sources_1 = require("../graph-sources");
|
|
27
38
|
const query_utils_1 = require("../search/query-utils");
|
|
28
39
|
const fs_1 = require("fs");
|
|
40
|
+
const crypto_1 = require("crypto");
|
|
29
41
|
const utils_1 = require("../utils");
|
|
30
42
|
const generated_detection_1 = require("../extraction/generated-detection");
|
|
31
43
|
const arkts_1 = require("../extraction/languages/arkts");
|
|
44
|
+
const explore_session_state_1 = require("./explore-session-state");
|
|
45
|
+
const explore_dedup_1 = require("./explore-dedup");
|
|
32
46
|
const dynamic_boundaries_1 = require("./dynamic-boundaries");
|
|
33
47
|
const query_cache_1 = require("./query-cache");
|
|
34
48
|
const diff_impact_1 = require("./diff-impact");
|
|
@@ -56,6 +70,10 @@ const sqlite_adapter_1 = require("../db/sqlite-adapter");
|
|
|
56
70
|
class NotIndexedError extends Error {
|
|
57
71
|
}
|
|
58
72
|
exports.NotIndexedError = NotIndexedError;
|
|
73
|
+
/** `HOMEGRAPH_SOURCES=none` (or equivalent) — success-shaped, not isError. */
|
|
74
|
+
class GraphSourcesDisabledError extends Error {
|
|
75
|
+
}
|
|
76
|
+
exports.GraphSourcesDisabledError = GraphSourcesDisabledError;
|
|
59
77
|
/**
|
|
60
78
|
* A security refusal (sensitive system path). Stays `isError: true` WITHOUT
|
|
61
79
|
* retry guidance — abandoning this path is the desired agent reaction.
|
|
@@ -114,6 +132,114 @@ function lastQualifierPart(symbol) {
|
|
|
114
132
|
const parts = symbol.split(/::|[./]/).filter((p) => p.length > 0);
|
|
115
133
|
return parts[parts.length - 1] ?? symbol;
|
|
116
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Page/Dialog surface digests — methods that answer "which UI children / how
|
|
137
|
+
* does preview load" without dumping the whole struct. Lifecycle alone is not
|
|
138
|
+
* enough: agents re-Read for PageMap / preview helpers when those are trimmed.
|
|
139
|
+
*/
|
|
140
|
+
function isUiSurfaceDigestMethod(name, includePreviewHelpers) {
|
|
141
|
+
if (/^(build|aboutToAppear|aboutToDisappear|PageMap)$/i.test(name))
|
|
142
|
+
return true;
|
|
143
|
+
if (!includePreviewHelpers)
|
|
144
|
+
return false;
|
|
145
|
+
return /preview|pixelmap|loadimage|getimage|decodeimage/i.test(name);
|
|
146
|
+
}
|
|
147
|
+
/** Prefer build/PageMap/preview over aboutToAppear when budget is tight. */
|
|
148
|
+
function surfaceMethodPriority(name) {
|
|
149
|
+
if (/^(build|PageMap)$/i.test(name))
|
|
150
|
+
return 0;
|
|
151
|
+
if (/preview|pixelmap|loadimage|getimage/i.test(name))
|
|
152
|
+
return 1;
|
|
153
|
+
return 2;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Structured UI/nav bullets extracted from surface method bodies so agents can
|
|
157
|
+
* ANSWER NOW without re-Reading for child Types / PageMap routes.
|
|
158
|
+
*/
|
|
159
|
+
function extractUiSurfaceInventory(fileLines, methods) {
|
|
160
|
+
const FRAMEWORK_TAG = new Set([
|
|
161
|
+
'Column', 'Row', 'Stack', 'Flex', 'Grid', 'List', 'ListItem', 'Scroll', 'Tabs', 'TabContent',
|
|
162
|
+
'Text', 'Image', 'Button', 'Blank', 'Divider', 'ForEach', 'LazyForEach', 'If', 'Else',
|
|
163
|
+
'RelativeContainer', 'Swiper', 'WaterFlow', 'GridItem', 'Span', 'SymbolGlyph',
|
|
164
|
+
]);
|
|
165
|
+
const comps = new Set();
|
|
166
|
+
const nav = new Set();
|
|
167
|
+
const media = [];
|
|
168
|
+
for (const m of methods) {
|
|
169
|
+
const from = Math.max(1, m.startLine);
|
|
170
|
+
const to = Math.min(fileLines.length, Math.max(m.endLine, m.startLine));
|
|
171
|
+
for (let i = from; i <= to; i++) {
|
|
172
|
+
const line = fileLines[i - 1] ?? '';
|
|
173
|
+
for (const mm of line.matchAll(/\b([A-Z][A-Za-z0-9_]{2,})\s*\(/g)) {
|
|
174
|
+
const id = mm[1];
|
|
175
|
+
if (FRAMEWORK_TAG.has(id) || (0, query_utils_1.isFrameworkUiDecoratorName)(id))
|
|
176
|
+
continue;
|
|
177
|
+
if (/^(Promise|Array|Map|Set|Date|Error|JSON|Object|Math|Number|String|Boolean)$/.test(id))
|
|
178
|
+
continue;
|
|
179
|
+
comps.add(id);
|
|
180
|
+
}
|
|
181
|
+
if (/PageMap|pushUrl|replaceUrl|pushPath|replacePath|router\./i.test(line)) {
|
|
182
|
+
const clipped = line.trim().replace(/\s+/g, ' ').slice(0, 120);
|
|
183
|
+
if (clipped)
|
|
184
|
+
nav.add(clipped);
|
|
185
|
+
}
|
|
186
|
+
for (const mm of line.matchAll(/\bConstants\.[A-Z][A-Z0-9_]+\b/g)) {
|
|
187
|
+
nav.add(mm[0]);
|
|
188
|
+
}
|
|
189
|
+
// Preview / Image load origin — $r vs file URI vs network.
|
|
190
|
+
if (/\$r\s*\(|Image\s*\(|PixelMap|createPixelMap|decode|tempFileUri|http|download|fileUri|getPreview|previewPixelMap|loadImage/i.test(line)) {
|
|
191
|
+
const clipped = line.trim().replace(/\s+/g, ' ').slice(0, 140);
|
|
192
|
+
if (clipped && media.length < 14) {
|
|
193
|
+
media.push(`L${i} (\`${m.name}\`): \`${clipped}\``);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (comps.size === 0 && nav.size === 0 && media.length === 0)
|
|
199
|
+
return '';
|
|
200
|
+
const out = ['**UI surface inventory (from build / PageMap / preview methods)**', ''];
|
|
201
|
+
if (comps.size > 0) {
|
|
202
|
+
out.push('UI / child Types:');
|
|
203
|
+
for (const c of [...comps].sort().slice(0, 24))
|
|
204
|
+
out.push(`- \`${c}\``);
|
|
205
|
+
if (comps.size > 24)
|
|
206
|
+
out.push(`- … and ${comps.size - 24} more`);
|
|
207
|
+
out.push('');
|
|
208
|
+
}
|
|
209
|
+
if (nav.size > 0) {
|
|
210
|
+
out.push('Navigation / PageMap / route constants:');
|
|
211
|
+
for (const n of [...nav].slice(0, 16))
|
|
212
|
+
out.push(`- ${n.startsWith('Constants.') ? `\`${n}\`` : n}`);
|
|
213
|
+
if (nav.size > 16)
|
|
214
|
+
out.push(`- … and ${nav.size - 16} more`);
|
|
215
|
+
out.push('');
|
|
216
|
+
}
|
|
217
|
+
if (media.length > 0) {
|
|
218
|
+
out.push('Image / preview load sites ($r / file / network cues):');
|
|
219
|
+
for (const row of media)
|
|
220
|
+
out.push(`- ${row}`);
|
|
221
|
+
out.push('');
|
|
222
|
+
}
|
|
223
|
+
out.push('> **ANSWER NOW** from this inventory + method digests below — do not Read/Grep for the same Page UI children, routes, or preview-load origin.');
|
|
224
|
+
out.push('');
|
|
225
|
+
return out.join('\n');
|
|
226
|
+
}
|
|
227
|
+
/** C++ `class Foo : public Bar` vs private `: Bar` — prefer public is-a for subtype lists. */
|
|
228
|
+
function cppExtendsLooksPublic(declLine, baseName) {
|
|
229
|
+
const esc = baseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
230
|
+
if (new RegExp(`:\\s*public\\s+(?:virtual\\s+)?${esc}\\b`).test(declLine))
|
|
231
|
+
return true;
|
|
232
|
+
if (new RegExp(`:\\s*protected\\s+(?:virtual\\s+)?${esc}\\b`).test(declLine))
|
|
233
|
+
return false;
|
|
234
|
+
if (new RegExp(`:\\s*private\\s+(?:virtual\\s+)?${esc}\\b`).test(declLine))
|
|
235
|
+
return false;
|
|
236
|
+
// `class Cluster : Rectangle` (no access) defaults to private for class.
|
|
237
|
+
if (new RegExp(`:\\s*${esc}\\b`).test(declLine) && /\bclass\b/.test(declLine))
|
|
238
|
+
return false;
|
|
239
|
+
if (new RegExp(`:\\s*${esc}\\b`).test(declLine) && /\bstruct\b/.test(declLine))
|
|
240
|
+
return true;
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
117
243
|
/**
|
|
118
244
|
* Normalize Erlang-native symbol spellings in an explore query into the shapes
|
|
119
245
|
* the rest of the pipeline already understands. Agents working Erlang code
|
|
@@ -252,11 +378,24 @@ function getExploreOutputBudget(fileCount) {
|
|
|
252
378
|
* Shrink explore ceilings for local-detail / no-flow named-symbol questions.
|
|
253
379
|
* Large repos otherwise dump ~24K related source that the agent then still
|
|
254
380
|
* greps/reads — the main token regression vs without-homegraph.
|
|
381
|
+
*
|
|
382
|
+
* Mechanism / cross-module flows also get a tighter ceiling: full 24K + a
|
|
383
|
+
* second explore/node/Read stack is what blows session tokens on "how is X
|
|
384
|
+
* implemented" questions even when the first answer was already enough.
|
|
255
385
|
*/
|
|
256
386
|
function tightenExploreBudgetForQuery(budget, query, opts) {
|
|
257
387
|
const hasFlow = opts?.hasFlowPath === true;
|
|
258
388
|
if (hasFlow && ((0, query_utils_1.queryAsMechanismSurvey)(query) || (0, query_utils_1.queryAsCrossModuleFlowSurvey)(query))) {
|
|
259
|
-
return
|
|
389
|
+
return {
|
|
390
|
+
...budget,
|
|
391
|
+
maxOutputChars: Math.min(budget.maxOutputChars, 12000),
|
|
392
|
+
defaultMaxFiles: Math.min(budget.defaultMaxFiles, 4),
|
|
393
|
+
maxCharsPerFile: Math.min(budget.maxCharsPerFile, 4500),
|
|
394
|
+
includeRelationships: false,
|
|
395
|
+
includeAdditionalFiles: false,
|
|
396
|
+
includeCompletenessSignal: false,
|
|
397
|
+
includeBudgetNote: false,
|
|
398
|
+
};
|
|
260
399
|
}
|
|
261
400
|
const local = (0, query_utils_1.queryAsLocalSymbolDetail)(query);
|
|
262
401
|
const compact = (0, query_utils_1.shouldUseCompactExploreBudget)(query);
|
|
@@ -277,11 +416,14 @@ function tightenExploreBudgetForQuery(budget, query, opts) {
|
|
|
277
416
|
}
|
|
278
417
|
if (!local && !compact)
|
|
279
418
|
return budget;
|
|
419
|
+
const lean = (0, query_utils_1.queryAsAssignedFlagImpactSurvey)(query)
|
|
420
|
+
|| (0, query_utils_1.queryAsFocusedUiCluster)(query)
|
|
421
|
+
|| (0, query_utils_1.queryAsTypeLifecycleSurvey)(query);
|
|
280
422
|
return {
|
|
281
423
|
...budget,
|
|
282
|
-
maxOutputChars: Math.min(budget.maxOutputChars, local ?
|
|
283
|
-
defaultMaxFiles: Math.min(budget.defaultMaxFiles, local ? 2 : 3),
|
|
284
|
-
maxCharsPerFile: Math.min(budget.maxCharsPerFile, local ?
|
|
424
|
+
maxOutputChars: Math.min(budget.maxOutputChars, lean ? 4800 : local ? 7000 : 9000),
|
|
425
|
+
defaultMaxFiles: Math.min(budget.defaultMaxFiles, lean ? 2 : local ? 2 : 3),
|
|
426
|
+
maxCharsPerFile: Math.min(budget.maxCharsPerFile, lean ? 2200 : local ? 3500 : 4000),
|
|
285
427
|
includeRelationships: false,
|
|
286
428
|
includeAdditionalFiles: false,
|
|
287
429
|
includeCompletenessSignal: false,
|
|
@@ -485,9 +627,9 @@ const READ_ONLY_ANNOTATIONS = {
|
|
|
485
627
|
/**
|
|
486
628
|
* All HomeGraph MCP tools
|
|
487
629
|
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
630
|
+
* Prefer the smallest tool that answers: callers/node for one named symbol,
|
|
631
|
+
* explore for multi-file flows. Skip HomeGraph entirely for topic file-lists,
|
|
632
|
+
* concept compares, SDK catalogs, and literal greps.
|
|
491
633
|
*
|
|
492
634
|
* All tools support cross-project queries via the optional `projectPath` parameter.
|
|
493
635
|
*/
|
|
@@ -495,7 +637,9 @@ exports.tools = [
|
|
|
495
637
|
{
|
|
496
638
|
name: 'homegraph_search',
|
|
497
639
|
description: 'LAST RESORT spelling lookup — locations only, no source. Required: `query` (e.g. "signIn"). ' +
|
|
498
|
-
'Prefer
|
|
640
|
+
'Prefer explore/callers/node when names are known. ' +
|
|
641
|
+
'DO NOT call for topic file-lists, concept compares, or SDK/@kit feature catalogs (those return Skip guidance). ' +
|
|
642
|
+
'Also skip literal string/pattern greps — use Grep instead. ' +
|
|
499
643
|
'Bare-name search may return a compact explore result instead of locations.',
|
|
500
644
|
inputSchema: {
|
|
501
645
|
type: 'object',
|
|
@@ -523,9 +667,8 @@ exports.tools = [
|
|
|
523
667
|
{
|
|
524
668
|
name: 'homegraph_callers',
|
|
525
669
|
description: 'Compact caller list for one NAMED in-repo symbol (no bodies). Required: `symbol` (e.g. "authenticate"). ' +
|
|
526
|
-
'
|
|
527
|
-
'DO NOT call for SDK catalogs,
|
|
528
|
-
'Prefer one explore over parallel callers+callees+node.',
|
|
670
|
+
'Cheaper than explore when you only need who-calls-X. For multi-file flows use homegraph_explore. ' +
|
|
671
|
+
'DO NOT call for SDK catalogs, topic file-lists, concept compares, or hypothetics.',
|
|
529
672
|
inputSchema: {
|
|
530
673
|
type: 'object',
|
|
531
674
|
properties: {
|
|
@@ -551,8 +694,8 @@ exports.tools = [
|
|
|
551
694
|
{
|
|
552
695
|
name: 'homegraph_callees',
|
|
553
696
|
description: 'Compact callee list for one NAMED in-repo symbol (no bodies). Required: `symbol` (e.g. "authenticate"). ' +
|
|
554
|
-
'
|
|
555
|
-
'
|
|
697
|
+
'Cheaper than explore when you only need what-X-calls. For multi-file flows use homegraph_explore. ' +
|
|
698
|
+
'DO NOT use for out-of-repo SDK catalogs or counterfactual analysis.',
|
|
556
699
|
inputSchema: {
|
|
557
700
|
type: 'object',
|
|
558
701
|
properties: {
|
|
@@ -649,10 +792,11 @@ exports.tools = [
|
|
|
649
792
|
name: 'homegraph_node',
|
|
650
793
|
description: 'Depth on ONE known in-repo symbol or indexed file — not a survey tool. ' +
|
|
651
794
|
'Required: pass `symbol` (symbol mode) OR `file` alone (file mode). ' +
|
|
795
|
+
'Cheaper than explore when you already know the name and only need one body. ' +
|
|
652
796
|
'FILE: `file` only → line-numbered source + dependents. ' +
|
|
653
797
|
'SYMBOL: body via includeCode + short trail; overloads return every body. ' +
|
|
654
|
-
'
|
|
655
|
-
'DO NOT crawl a feature with repeated node calls (prefer one explore). ' +
|
|
798
|
+
'DO NOT call after explore already returned that symbol/file (multiplies tokens). ' +
|
|
799
|
+
'DO NOT crawl a feature with repeated node calls (prefer one explore for flows). ' +
|
|
656
800
|
'Treat returned source as already Read.',
|
|
657
801
|
inputSchema: {
|
|
658
802
|
type: 'object',
|
|
@@ -696,16 +840,22 @@ exports.tools = [
|
|
|
696
840
|
},
|
|
697
841
|
{
|
|
698
842
|
name: 'homegraph_explore',
|
|
699
|
-
description: '
|
|
700
|
-
'CALL for
|
|
701
|
-
'
|
|
843
|
+
description: 'In-repo multi-file / mechanism tool (call paths + compact line-numbered source). Required: `query`. ' +
|
|
844
|
+
'CALL FIRST for how/wired questions — pass the question or domain keywords; PascalCase names optional. ' +
|
|
845
|
+
'Also CALL FIRST (alone, no parallel Grep) for named Type/Component/Page/Dialog, Type.member, click→handler, inheritance/subtypes, ' +
|
|
846
|
+
'declaration/attribute sites, ALL_CAPS constant / field-mutex usages, path-module NAPI/exports or inter-deps, ' +
|
|
847
|
+
'and in-repo @kit/@ohos *usages/dependencies* (which files import a named export — not the SDK feature catalog). ' +
|
|
848
|
+
'On the first turn call explore alone (no parallel Grep). Prefer callers/node when one named symbol is already enough. ' +
|
|
849
|
+
'DO NOT call for topic file-lists, concept/UI-behavior with no named anchors, literal copy hunts, or SDK/@kit *feature catalogs* — those return Skip guidance. ' +
|
|
850
|
+
'Literal string/pattern hunts → Grep. ' +
|
|
851
|
+
'One explore; answer from Source + trail; treat as already Read — do not re-grep/node/read the same symbols. Busy/partial → retry same explore once.',
|
|
702
852
|
inputSchema: {
|
|
703
853
|
type: 'object',
|
|
704
854
|
properties: {
|
|
705
855
|
query: {
|
|
706
856
|
type: 'string',
|
|
707
|
-
description: 'Required.
|
|
708
|
-
'For flows,
|
|
857
|
+
description: 'Required. Prefer the user question or domain keywords for how/mechanism. ' +
|
|
858
|
+
'For named flows, include Type / Type.member / component names. For @kit, ask usages (depend/import sites), not SDK catalogs.',
|
|
709
859
|
},
|
|
710
860
|
maxFiles: {
|
|
711
861
|
type: 'number',
|
|
@@ -729,6 +879,25 @@ exports.tools = [
|
|
|
729
879
|
},
|
|
730
880
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
731
881
|
},
|
|
882
|
+
{
|
|
883
|
+
name: 'homegraph_arkui_migrate',
|
|
884
|
+
description: 'ArkUI migrate / state-semantics snapshot in ONE call (components, state decorators + args, ' +
|
|
885
|
+
'data-passage types, Provide/Consume/Storage key channels, @Observed classes). ' +
|
|
886
|
+
'Required: `scope`. PRIMARY for ArkUI V1↔V2 migration decisions — do NOT stitch with explore. ' +
|
|
887
|
+
'Scope: component name, relative .ets path, or directory prefix. Returns JSON (no source bodies).',
|
|
888
|
+
inputSchema: {
|
|
889
|
+
type: 'object',
|
|
890
|
+
properties: {
|
|
891
|
+
scope: {
|
|
892
|
+
type: 'string',
|
|
893
|
+
description: 'Required. Component name (e.g. "ParentPage"), relative file (e.g. "pages/Index.ets"), or directory prefix.',
|
|
894
|
+
},
|
|
895
|
+
projectPath: projectPathProperty,
|
|
896
|
+
},
|
|
897
|
+
required: ['scope'],
|
|
898
|
+
},
|
|
899
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
900
|
+
},
|
|
732
901
|
{
|
|
733
902
|
name: 'homegraph_files',
|
|
734
903
|
description: 'Indexed directory tree (paths and symbol counts only — NO source). ' +
|
|
@@ -1115,6 +1284,10 @@ class ToolHandler {
|
|
|
1115
1284
|
* similar to how git finds .git/ directories.
|
|
1116
1285
|
*/
|
|
1117
1286
|
getHomeGraph(projectPath) {
|
|
1287
|
+
const sourcesMode = (0, graph_sources_1.resolveGraphSources)();
|
|
1288
|
+
if (!(0, graph_sources_1.graphSourceFlags)(sourcesMode).openProjectDb) {
|
|
1289
|
+
throw new GraphSourcesDisabledError((0, graph_sources_1.graphSourcesDisabledGuidance)(sourcesMode));
|
|
1290
|
+
}
|
|
1118
1291
|
if (!projectPath) {
|
|
1119
1292
|
if (!this.cg) {
|
|
1120
1293
|
const searched = this.defaultProjectHint ?? process.cwd();
|
|
@@ -1126,7 +1299,7 @@ class ToolHandler {
|
|
|
1126
1299
|
'explicitly:\n' +
|
|
1127
1300
|
' • Pass projectPath to the tool call, e.g. projectPath: "/absolute/path/to/your/project" ' +
|
|
1128
1301
|
'(any project that has a .homegraph/ — including a sub-project of a monorepo)\n' +
|
|
1129
|
-
' • Or add --path to the server\'s MCP config args: ["serve", "
|
|
1302
|
+
' • Or add --path to the server\'s MCP config args: ["serve", "mcp", "--path", "/absolute/path/to/your/project"]\n' +
|
|
1130
1303
|
'If a project simply has no index, use your built-in tools (Read/Grep/Glob) for THAT ' +
|
|
1131
1304
|
"project (the user can run 'homegraph init' there to enable it) — you can still query " +
|
|
1132
1305
|
'other indexed projects by projectPath in the same session.');
|
|
@@ -1177,7 +1350,7 @@ class ToolHandler {
|
|
|
1177
1350
|
const cached = this.projectCache.get(resolvedRoot);
|
|
1178
1351
|
if (cached)
|
|
1179
1352
|
return this.freshen(cached);
|
|
1180
|
-
const cg = loadHomeGraph().openSync(resolvedRoot);
|
|
1353
|
+
const cg = loadHomeGraph().openSync(resolvedRoot, { sources: sourcesMode });
|
|
1181
1354
|
this.projectCache.set(resolvedRoot, cg);
|
|
1182
1355
|
return cg;
|
|
1183
1356
|
}
|
|
@@ -1326,6 +1499,46 @@ class ToolHandler {
|
|
|
1326
1499
|
* Cost when nothing is pending — the common case — is one boolean check.
|
|
1327
1500
|
* No I/O, no parsing of markdown beyond a per-pending-file substring scan.
|
|
1328
1501
|
*/
|
|
1502
|
+
driftCache = new Map();
|
|
1503
|
+
static DRIFT_TTL_MS = 2000;
|
|
1504
|
+
/**
|
|
1505
|
+
* On-disk drift check for a single indexed file (issue #1474). The code
|
|
1506
|
+
* renderers slice CURRENT bytes at INDEXED line ranges; when the file
|
|
1507
|
+
* changed after its last index sync those ranges can point at a DIFFERENT
|
|
1508
|
+
* symbol's code. Freshness is verified here from data the index already
|
|
1509
|
+
* stores: one stat() (size + mtime); content hashed only on mismatch.
|
|
1510
|
+
*/
|
|
1511
|
+
isFileStaleOnDisk(cg, relPath, content) {
|
|
1512
|
+
let root;
|
|
1513
|
+
try {
|
|
1514
|
+
root = cg.getProjectRoot();
|
|
1515
|
+
}
|
|
1516
|
+
catch {
|
|
1517
|
+
return false;
|
|
1518
|
+
}
|
|
1519
|
+
const key = `${root}\0${relPath}`;
|
|
1520
|
+
const now = Date.now();
|
|
1521
|
+
const hit = this.driftCache.get(key);
|
|
1522
|
+
if (hit && now - hit.at < ToolHandler.DRIFT_TTL_MS)
|
|
1523
|
+
return hit.stale;
|
|
1524
|
+
let stale = false;
|
|
1525
|
+
try {
|
|
1526
|
+
const rec = cg.getFile(relPath);
|
|
1527
|
+
const absPath = rec ? (0, utils_1.validatePathWithinRoot)(root, relPath) : null;
|
|
1528
|
+
if (rec && absPath && (0, fs_1.existsSync)(absPath)) {
|
|
1529
|
+
const st = (0, fs_1.statSync)(absPath);
|
|
1530
|
+
if (st.size !== rec.size || Math.floor(st.mtimeMs) !== Math.floor(rec.modifiedAt)) {
|
|
1531
|
+
const data = content ?? (0, fs_1.readFileSync)(absPath, 'utf-8');
|
|
1532
|
+
stale = (0, crypto_1.createHash)('sha256').update(data).digest('hex') !== rec.contentHash;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
catch {
|
|
1537
|
+
stale = false;
|
|
1538
|
+
}
|
|
1539
|
+
this.driftCache.set(key, { at: now, stale });
|
|
1540
|
+
return stale;
|
|
1541
|
+
}
|
|
1329
1542
|
withStalenessNotice(result, projectPath) {
|
|
1330
1543
|
if (result.isError)
|
|
1331
1544
|
return result;
|
|
@@ -1432,9 +1645,12 @@ class ToolHandler {
|
|
|
1432
1645
|
return { ...result, content: [{ type: 'text', text: composed }, ...rest] };
|
|
1433
1646
|
}
|
|
1434
1647
|
/**
|
|
1435
|
-
* Execute a tool by name
|
|
1648
|
+
* Execute a tool by name.
|
|
1649
|
+
*
|
|
1650
|
+
* `sessionState` is the calling MCP session's explore history (CG-17). Omit
|
|
1651
|
+
* it (the CLI does) and explore behaves exactly as before, untracked.
|
|
1436
1652
|
*/
|
|
1437
|
-
async execute(toolName, args) {
|
|
1653
|
+
async execute(toolName, args, sessionState) {
|
|
1438
1654
|
try {
|
|
1439
1655
|
// Block the first tool call on the engine's post-open reconcile so we
|
|
1440
1656
|
// never serve rows for files deleted/edited while no MCP server was
|
|
@@ -1480,7 +1696,20 @@ class ToolHandler {
|
|
|
1480
1696
|
return check;
|
|
1481
1697
|
}
|
|
1482
1698
|
const projectPath = args.projectPath;
|
|
1483
|
-
|
|
1699
|
+
// Wrong question shapes → short Skip (before cache / graph work).
|
|
1700
|
+
if (toolName === 'homegraph_explore' || toolName === 'homegraph_search') {
|
|
1701
|
+
const qEarly = typeof args.query === 'string' ? args.query : '';
|
|
1702
|
+
if (qEarly) {
|
|
1703
|
+
const deferKind = (0, query_utils_1.queryShouldDeferToBuiltinTools)(qEarly);
|
|
1704
|
+
if (deferKind) {
|
|
1705
|
+
return this.textResult((0, query_utils_1.homegraphDeferGuidance)(deferKind, qEarly));
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
// Session-tracked explore must not hit the MCP query cache: a cache hit
|
|
1710
|
+
// would re-serve the first call's full source and defeat CG-18 dedup.
|
|
1711
|
+
const skipCacheForSession = toolName === 'homegraph_explore' && !!sessionState;
|
|
1712
|
+
const cacheEnabled = !skipCacheForSession && (0, query_cache_1.isMcpQueryCacheEnabled)() && (0, query_cache_1.isCacheableMcpTool)(toolName);
|
|
1484
1713
|
let cacheKey;
|
|
1485
1714
|
let cacheQueries;
|
|
1486
1715
|
let cacheIndex;
|
|
@@ -1530,10 +1759,10 @@ class ToolHandler {
|
|
|
1530
1759
|
const fast = (toolName === 'homegraph_explore' || toolName === 'homegraph_search'
|
|
1531
1760
|
? this.tryFastInventoryExplore(cgFast, q, rootFast)
|
|
1532
1761
|
: null)
|
|
1533
|
-
??
|
|
1534
|
-
?? (toolName === 'homegraph_explore'
|
|
1762
|
+
?? (toolName === 'homegraph_explore' || toolName === 'homegraph_search'
|
|
1535
1763
|
? this.tryLightMechanismExplore(cgFast, q, rootFast)
|
|
1536
|
-
: null)
|
|
1764
|
+
: null)
|
|
1765
|
+
?? this.tryCompactLocalSymbolExplore(cgFast, q, rootFast);
|
|
1537
1766
|
if (fast) {
|
|
1538
1767
|
if (cacheEnabled && cacheKey && cacheQueries && cacheIndex && !fast.isError) {
|
|
1539
1768
|
cacheIndex.setEntry(cacheQueries, cacheKey, toolName, fast);
|
|
@@ -1547,10 +1776,16 @@ class ToolHandler {
|
|
|
1547
1776
|
}
|
|
1548
1777
|
}
|
|
1549
1778
|
}
|
|
1779
|
+
// Explore also carries the session's own call history down (CG-17) and its
|
|
1780
|
+
// emission record back up. Both travel as plain properties — on the args
|
|
1781
|
+
// object down, on the ToolResult up — because either leg may cross a
|
|
1782
|
+
// structured-clone boundary into a worker.
|
|
1783
|
+
const dispatchArgs = this.withSessionView(toolName, args, sessionState);
|
|
1550
1784
|
// prefers the query pool so sync SQLite/CPU cannot freeze the transport
|
|
1551
1785
|
// (a frozen main loop prevents setTimeout deadlines from firing → empty
|
|
1552
1786
|
// `-32001`). Fast-path surveys run inside the worker via executeReadTool.
|
|
1553
|
-
const
|
|
1787
|
+
const raw = await this.runReadToolWithDeadline(toolName, dispatchArgs);
|
|
1788
|
+
const result = this.takeExploreEmission(raw, sessionState);
|
|
1554
1789
|
if (cacheEnabled && cacheKey && cacheQueries && cacheIndex && !result.isError) {
|
|
1555
1790
|
cacheIndex.setEntry(cacheQueries, cacheKey, toolName, result);
|
|
1556
1791
|
}
|
|
@@ -1564,6 +1799,9 @@ class ToolHandler {
|
|
|
1564
1799
|
if (err instanceof NotIndexedError) {
|
|
1565
1800
|
return this.textResult(err.message);
|
|
1566
1801
|
}
|
|
1802
|
+
if (err instanceof GraphSourcesDisabledError) {
|
|
1803
|
+
return this.textResult(err.message);
|
|
1804
|
+
}
|
|
1567
1805
|
// Security refusal: a clean error, no retry encouragement.
|
|
1568
1806
|
if (err instanceof PathRefusalError) {
|
|
1569
1807
|
return this.errorResult(err.message);
|
|
@@ -1573,6 +1811,50 @@ class ToolHandler {
|
|
|
1573
1811
|
'continue without homegraph for this task.');
|
|
1574
1812
|
}
|
|
1575
1813
|
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Attach the caller's session view to an explore call's args (CG-17), on a
|
|
1816
|
+
* COPY so the caller's object is never mutated. Nothing else sees it: a
|
|
1817
|
+
* non-explore tool, or a caller with no session state, gets the args
|
|
1818
|
+
* unchanged and pays nothing.
|
|
1819
|
+
*
|
|
1820
|
+
* A client that spells the internal key itself is stripped rather than
|
|
1821
|
+
* trusted — the view decides what source a later call may withhold, so it has
|
|
1822
|
+
* to come from the server's own record, never from the wire.
|
|
1823
|
+
*/
|
|
1824
|
+
withSessionView(toolName, args, sessionState) {
|
|
1825
|
+
if (!(explore_session_state_1.EXPLORE_SESSION_VIEW_ARG in args) && (!sessionState || toolName !== 'homegraph_explore')) {
|
|
1826
|
+
return args;
|
|
1827
|
+
}
|
|
1828
|
+
const copy = { ...args };
|
|
1829
|
+
delete copy[explore_session_state_1.EXPLORE_SESSION_VIEW_ARG];
|
|
1830
|
+
if (sessionState && toolName === 'homegraph_explore') {
|
|
1831
|
+
copy[explore_session_state_1.EXPLORE_SESSION_VIEW_ARG] = sessionState.view();
|
|
1832
|
+
}
|
|
1833
|
+
return copy;
|
|
1834
|
+
}
|
|
1835
|
+
/**
|
|
1836
|
+
* Record an explore call's emission into the caller's session state and strip
|
|
1837
|
+
* it from the result (CG-17).
|
|
1838
|
+
*
|
|
1839
|
+
* Unconditional strip: the property is internal, so it comes off even when
|
|
1840
|
+
* there is no session state to record it into (the CLI path) — that is what
|
|
1841
|
+
* keeps the agent-facing response byte-identical. Recording is wrapped
|
|
1842
|
+
* because a bookkeeping bug must never fail a tool call that already
|
|
1843
|
+
* succeeded.
|
|
1844
|
+
*/
|
|
1845
|
+
takeExploreEmission(result, sessionState) {
|
|
1846
|
+
const emission = result?.[explore_session_state_1.EXPLORE_EMISSION_KEY];
|
|
1847
|
+
if (emission === undefined)
|
|
1848
|
+
return result;
|
|
1849
|
+
delete result[explore_session_state_1.EXPLORE_EMISSION_KEY];
|
|
1850
|
+
if (sessionState) {
|
|
1851
|
+
try {
|
|
1852
|
+
sessionState.record(emission);
|
|
1853
|
+
}
|
|
1854
|
+
catch { /* bookkeeping only — never fail a served call */ }
|
|
1855
|
+
}
|
|
1856
|
+
return result;
|
|
1857
|
+
}
|
|
1576
1858
|
/**
|
|
1577
1859
|
* Dispatch a read tool with a hard deadline under the typical ~60s MCP client
|
|
1578
1860
|
* timeout.
|
|
@@ -1658,6 +1940,9 @@ class ToolHandler {
|
|
|
1658
1940
|
if (err instanceof NotIndexedError) {
|
|
1659
1941
|
return this.textResult(err.message);
|
|
1660
1942
|
}
|
|
1943
|
+
if (err instanceof GraphSourcesDisabledError) {
|
|
1944
|
+
return this.textResult(err.message);
|
|
1945
|
+
}
|
|
1661
1946
|
if (err instanceof PathRefusalError) {
|
|
1662
1947
|
return this.errorResult(err.message);
|
|
1663
1948
|
}
|
|
@@ -1682,6 +1967,7 @@ class ToolHandler {
|
|
|
1682
1967
|
case 'homegraph_explore': return await this.handleExplore(args);
|
|
1683
1968
|
case 'homegraph_node': return await this.handleNode(args);
|
|
1684
1969
|
case 'homegraph_files': return await this.handleFiles(args);
|
|
1970
|
+
case 'homegraph_arkui_migrate': return await this.handleArkuiMigrate(args);
|
|
1685
1971
|
case 'homegraph_spec_match': return await this.handleSpecMatch(args);
|
|
1686
1972
|
case 'homegraph_spec_find': return await this.handleSpecFind(args);
|
|
1687
1973
|
case 'homegraph_spec_trace': return await this.handleSpecTrace(args);
|
|
@@ -1692,6 +1978,10 @@ class ToolHandler {
|
|
|
1692
1978
|
const query = this.validateString(args.query, 'query');
|
|
1693
1979
|
if (typeof query !== 'string')
|
|
1694
1980
|
return query;
|
|
1981
|
+
const deferKind = (0, query_utils_1.queryShouldDeferToBuiltinTools)(query);
|
|
1982
|
+
if (deferKind) {
|
|
1983
|
+
return this.textResult((0, query_utils_1.homegraphDeferGuidance)(deferKind, query));
|
|
1984
|
+
}
|
|
1695
1985
|
const cg = this.getHomeGraph(args.projectPath);
|
|
1696
1986
|
// Explore redirect is best-effort — incomplete/faked graphs (or missing
|
|
1697
1987
|
// getProjectRoot) must fall through to FTS search rather than error.
|
|
@@ -1699,14 +1989,14 @@ class ToolHandler {
|
|
|
1699
1989
|
const projectRoot = cg.getProjectRoot();
|
|
1700
1990
|
if ((0, query_utils_1.queryShouldPreferExploreOverSearch)(query)) {
|
|
1701
1991
|
const exploreRedirect = this.tryFastInventoryExplore(cg, query, projectRoot)
|
|
1702
|
-
?? this.
|
|
1703
|
-
?? this.
|
|
1992
|
+
?? this.tryLightMechanismExplore(cg, query, projectRoot)
|
|
1993
|
+
?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot);
|
|
1704
1994
|
if (exploreRedirect)
|
|
1705
1995
|
return exploreRedirect;
|
|
1706
1996
|
}
|
|
1707
1997
|
const exploreRedirect = this.tryFastInventoryExplore(cg, query, projectRoot)
|
|
1708
|
-
?? this.
|
|
1709
|
-
?? this.
|
|
1998
|
+
?? this.tryLightMechanismExplore(cg, query, projectRoot)
|
|
1999
|
+
?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot);
|
|
1710
2000
|
if (exploreRedirect)
|
|
1711
2001
|
return exploreRedirect;
|
|
1712
2002
|
}
|
|
@@ -1731,8 +2021,8 @@ class ToolHandler {
|
|
|
1731
2021
|
// for "Send" surfaces the hand-written keeper before .pb.go stubs
|
|
1732
2022
|
// that share the name. Stable: only reorders generated vs. not.
|
|
1733
2023
|
const ranked = [...results].sort((a, b) => {
|
|
1734
|
-
const aGen = (
|
|
1735
|
-
const bGen = (
|
|
2024
|
+
const aGen = this.isGeneratedCandidate(cg, a.node.filePath) ? 1 : 0;
|
|
2025
|
+
const bGen = this.isGeneratedCandidate(cg, b.node.filePath) ? 1 : 0;
|
|
1736
2026
|
return aGen - bGen;
|
|
1737
2027
|
});
|
|
1738
2028
|
const formatted = this.formatSearchResults(ranked);
|
|
@@ -1820,13 +2110,28 @@ class ToolHandler {
|
|
|
1820
2110
|
if (groups.length === 1) {
|
|
1821
2111
|
const { callers, labels } = collect(groups[0]);
|
|
1822
2112
|
if (callers.length === 0) {
|
|
2113
|
+
// Empty callers often means registration/callback wiring (OnSurfaceChangedCB)
|
|
2114
|
+
// — a one-line "no callers" teaches Grep/node thrash. Prefer compact body +
|
|
2115
|
+
// text-usage sites when the symbol is a local anchor.
|
|
2116
|
+
try {
|
|
2117
|
+
const root = cg.getProjectRoot();
|
|
2118
|
+
const compact = this.tryCompactLocalSymbolExplore(cg, symbol, root);
|
|
2119
|
+
if (compact)
|
|
2120
|
+
return compact;
|
|
2121
|
+
}
|
|
2122
|
+
catch { /* fall through */ }
|
|
1823
2123
|
return this.textResult(`No callers found for "${symbol}"${allMatches.note}${filterNote}`);
|
|
1824
2124
|
}
|
|
1825
2125
|
// A successful `file` narrowing makes the multi-symbol aggregation note
|
|
1826
2126
|
// stale — suppress it.
|
|
1827
2127
|
const note = fileFilter && !filteredOut ? '' : allMatches.note;
|
|
2128
|
+
const includeBlock = this.formatCallerIncludeVisibility(cg, callers.slice(0, limit));
|
|
1828
2129
|
const formatted = this.formatNodeList(callers.slice(0, limit), `Callers of ${symbol}`, labels)
|
|
1829
|
-
+
|
|
2130
|
+
+ includeBlock
|
|
2131
|
+
+ '\n\n> Caller listing complete — answer from this list'
|
|
2132
|
+
+ (includeBlock ? ' + include/import visibility' : '')
|
|
2133
|
+
+ '; no read/grep needed.'
|
|
2134
|
+
+ '\n> If another type\'s visibility matters, pass **both** names to `homegraph_explore` (not callers alone).'
|
|
1830
2135
|
+ note + filterNote;
|
|
1831
2136
|
return this.textResult(this.truncateOutput(formatted));
|
|
1832
2137
|
}
|
|
@@ -2831,28 +3136,35 @@ class ToolHandler {
|
|
|
2831
3136
|
}
|
|
2832
3137
|
/**
|
|
2833
3138
|
* Import sites for @kit.* / *Kit module names — surfaces full `import { … } from '@kit.X'`
|
|
2834
|
-
* lines. When the query also names
|
|
3139
|
+
* lines. When the query also names an export/API token, only matching imports are listed.
|
|
2835
3140
|
*/
|
|
2836
3141
|
buildImportSitesSection(cg, query, projectRoot) {
|
|
2837
3142
|
const kitTerms = (0, query_utils_1.extractKitModuleNamesFromQuery)(query);
|
|
2838
|
-
const depSymbols = (0, query_utils_1.extractDependencySymbolsFromQuery)(query);
|
|
3143
|
+
const depSymbols = (0, query_utils_1.extractDependencySymbolsFromQuery)(query).filter((s) => !(0, query_utils_1.isMemberLikeIdentifier)(s) && !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(s.toLowerCase()));
|
|
3144
|
+
const focusExports = [
|
|
3145
|
+
...new Set([
|
|
3146
|
+
...(0, query_utils_1.extractKitSubmoduleNamesFromQuery)(query),
|
|
3147
|
+
...depSymbols.filter((s) => s.length >= 4),
|
|
3148
|
+
]),
|
|
3149
|
+
];
|
|
2839
3150
|
const kitSearchTerms = (0, query_utils_1.extractImportSearchTerms)(query);
|
|
3151
|
+
// Named export/API + kit (or usage-survey intent) → return the full matching list.
|
|
3152
|
+
const completeInventory = focusExports.length > 0
|
|
3153
|
+
&& (kitTerms.length > 0 || (0, query_utils_1.shouldBuildKitModuleUsageSurvey)(query) || (0, query_utils_1.shouldBuildApiUsageSurvey)(query));
|
|
2840
3154
|
const seen = new Set();
|
|
2841
3155
|
const sites = [];
|
|
2842
3156
|
const tryAdd = (node, lineText) => {
|
|
3157
|
+
if ((0, arkts_1.isOhosApiFilePath)(node.filePath))
|
|
3158
|
+
return;
|
|
2843
3159
|
const lineLc = lineText.toLowerCase();
|
|
3160
|
+
const focus = focusExports.length > 0 ? focusExports : depSymbols;
|
|
3161
|
+
if (focus.length > 0 && !focus.some((s) => lineLc.includes(s.toLowerCase())))
|
|
3162
|
+
return;
|
|
3163
|
+
// When a @kit module is named, require that kit — or @ohos.<focus> module path.
|
|
2844
3164
|
if (kitTerms.length > 0) {
|
|
2845
3165
|
const matchesKit = kitTerms.some((k) => lineLc.includes(`@kit.${k.toLowerCase()}`));
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
}
|
|
2849
|
-
if (depSymbols.length > 0) {
|
|
2850
|
-
const matchesSym = depSymbols.some((s) => lineLc.includes(s.toLowerCase()));
|
|
2851
|
-
if (!matchesSym)
|
|
2852
|
-
return;
|
|
2853
|
-
}
|
|
2854
|
-
if (depSymbols.includes('taskpool')) {
|
|
2855
|
-
if (!lineLc.includes('taskpool') || !lineLc.includes('@kit.arkts'))
|
|
3166
|
+
const matchesOhosModule = focusExports.some((s) => lineLc.includes(`@ohos.${s.toLowerCase()}`));
|
|
3167
|
+
if (!matchesKit && !matchesOhosModule)
|
|
2856
3168
|
return;
|
|
2857
3169
|
}
|
|
2858
3170
|
const key = `${node.filePath}:${node.startLine}`;
|
|
@@ -2862,10 +3174,10 @@ class ToolHandler {
|
|
|
2862
3174
|
sites.push({ file: node.filePath, line: node.startLine, lineText });
|
|
2863
3175
|
};
|
|
2864
3176
|
const resolveImportLine = (node) => (0, query_utils_1.resolveImportLineFromNode)(node, projectRoot);
|
|
2865
|
-
const importLimit = depSymbols.
|
|
2866
|
-
|
|
2867
|
-
// Symbol-first search:
|
|
2868
|
-
for (const sym of
|
|
3177
|
+
const importLimit = completeInventory ? 200 : focusExports.length > 0 || depSymbols.length > 0 ? 60 : 20;
|
|
3178
|
+
const searchSyms = focusExports.length > 0 ? focusExports : depSymbols;
|
|
3179
|
+
// Symbol-first search: named export hits `import { foo } from '@kit.X'`.
|
|
3180
|
+
for (const sym of searchSyms) {
|
|
2869
3181
|
let hits = [];
|
|
2870
3182
|
try {
|
|
2871
3183
|
hits = cg.searchNodes(sym, { kinds: ['import'], limit: importLimit });
|
|
@@ -2878,7 +3190,7 @@ class ToolHandler {
|
|
|
2878
3190
|
}
|
|
2879
3191
|
}
|
|
2880
3192
|
// Kit-module search (when no symbol filter, or to catch re-exports).
|
|
2881
|
-
if (sites.length === 0 ||
|
|
3193
|
+
if (sites.length === 0 || searchSyms.length === 0) {
|
|
2882
3194
|
for (const term of kitSearchTerms) {
|
|
2883
3195
|
const termLc = term.toLowerCase().replace(/^@kit\./, '');
|
|
2884
3196
|
let hits = [];
|
|
@@ -2892,8 +3204,8 @@ class ToolHandler {
|
|
|
2892
3204
|
const lineText = resolveImportLine(r.node);
|
|
2893
3205
|
if (!lineText.toLowerCase().includes(termLc))
|
|
2894
3206
|
continue;
|
|
2895
|
-
if (
|
|
2896
|
-
const matchesSym =
|
|
3207
|
+
if (searchSyms.length > 0) {
|
|
3208
|
+
const matchesSym = searchSyms.some((s) => lineText.toLowerCase().includes(s.toLowerCase()));
|
|
2897
3209
|
if (!matchesSym)
|
|
2898
3210
|
continue;
|
|
2899
3211
|
}
|
|
@@ -2906,11 +3218,11 @@ class ToolHandler {
|
|
|
2906
3218
|
}
|
|
2907
3219
|
sites.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
2908
3220
|
const importInventoryFilter = (0, query_utils_1.hasImportInventoryFilter)(query);
|
|
2909
|
-
const
|
|
2910
|
-
const
|
|
2911
|
-
const
|
|
3221
|
+
const compactListing = completeInventory || (0, query_utils_1.shouldCompactImportListing)(sites.length, importInventoryFilter);
|
|
3222
|
+
const cap = completeInventory ? sites.length : compactListing ? 40 : 15;
|
|
3223
|
+
const focusLabel = focusExports.length > 0 ? focusExports.map((s) => `\`${s}\``).join(', ') : 'queried symbol(s)';
|
|
2912
3224
|
const lines = compactListing
|
|
2913
|
-
? ['**Dependency list**', '', `Files importing
|
|
3225
|
+
? ['**Dependency list**', '', `Files importing ${focusLabel} (${sites.length} total):`, '']
|
|
2914
3226
|
: ['**Import sites**', ''];
|
|
2915
3227
|
for (const s of sites.slice(0, cap)) {
|
|
2916
3228
|
if (compactListing) {
|
|
@@ -2925,12 +3237,8 @@ class ToolHandler {
|
|
|
2925
3237
|
}
|
|
2926
3238
|
if (compactListing) {
|
|
2927
3239
|
lines.push('');
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
}
|
|
2931
|
-
else {
|
|
2932
|
-
lines.push('> Listing complete — answer from this section; no grep/read needed for the dependency set.');
|
|
2933
|
-
}
|
|
3240
|
+
lines.push(`> **ANSWER NOW.** Complete list of **${sites.length}** matching import site(s) for ${focusLabel}. ` +
|
|
3241
|
+
'Do **not** Grep/Read the same `@kit`/`@ohos` import pattern.');
|
|
2934
3242
|
}
|
|
2935
3243
|
lines.push('');
|
|
2936
3244
|
return { section: lines.join('\n'), siteCount: sites.length, compactListing };
|
|
@@ -2938,7 +3246,7 @@ class ToolHandler {
|
|
|
2938
3246
|
/**
|
|
2939
3247
|
* External-caller inventory for a named class — methods → who calls them (paths only).
|
|
2940
3248
|
*/
|
|
2941
|
-
buildCallerListingSection(cg, query) {
|
|
3249
|
+
buildCallerListingSection(cg, query, projectRoot) {
|
|
2942
3250
|
const typeNames = (0, query_utils_1.extractCallerSurveySymbols)(query);
|
|
2943
3251
|
if (typeNames.length === 0)
|
|
2944
3252
|
return '';
|
|
@@ -2947,11 +3255,34 @@ class ToolHandler {
|
|
|
2947
3255
|
// Applying this to every caller-survey query wiped same-file call sites
|
|
2948
3256
|
// (common in C++/ARK .cpp units) and fell through to a fat full explore.
|
|
2949
3257
|
const externalOnly = /\bexternal\b/i.test(query) || /外部/.test(query);
|
|
3258
|
+
const listedMethods = new Set((0, query_utils_1.extractListedTypeMethodsFromQuery)(query));
|
|
3259
|
+
const typeNameSet = new Set((0, query_utils_1.extractTypeNamesFromQuery)(query));
|
|
2950
3260
|
const lines = ['**Caller inventory**', ''];
|
|
2951
3261
|
let substantive = 0;
|
|
2952
3262
|
// Function / method symbols named directly (e.g. SortWidgets).
|
|
2953
|
-
|
|
2954
|
-
|
|
3263
|
+
// When Type + listed methods (BinaryGrid Set/Test/Fill), keep defs near the
|
|
3264
|
+
// owning Type's files (template methods often lack reliable contains edges).
|
|
3265
|
+
const ownerDirs = new Set();
|
|
3266
|
+
if (listedMethods.size > 0 && typeNameSet.size > 0) {
|
|
3267
|
+
for (const t of typeNameSet) {
|
|
3268
|
+
for (const n of cg.getNodesByName(t)) {
|
|
3269
|
+
if (n.kind === 'class' || n.kind === 'struct' || n.kind === 'component') {
|
|
3270
|
+
const fp = rel(n.filePath);
|
|
3271
|
+
ownerDirs.add(fp.split('/').slice(0, -1).join('/'));
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
for (const sym of typeNames.slice(0, 6)) {
|
|
3277
|
+
let funcs = cg.getNodesByName(sym).filter((n) => (n.kind === 'function' || n.kind === 'method') && !(0, query_utils_1.isTestFile)(n.filePath));
|
|
3278
|
+
if (listedMethods.has(sym) && ownerDirs.size > 0) {
|
|
3279
|
+
const near = funcs.filter((n) => {
|
|
3280
|
+
const dir = rel(n.filePath).split('/').slice(0, -1).join('/');
|
|
3281
|
+
return ownerDirs.has(dir);
|
|
3282
|
+
});
|
|
3283
|
+
if (near.length > 0)
|
|
3284
|
+
funcs = near;
|
|
3285
|
+
}
|
|
2955
3286
|
for (const fn of funcs.slice(0, 3)) {
|
|
2956
3287
|
let callers = [];
|
|
2957
3288
|
try {
|
|
@@ -2981,6 +3312,8 @@ class ToolHandler {
|
|
|
2981
3312
|
}
|
|
2982
3313
|
}
|
|
2983
3314
|
for (const typeName of typeNames.slice(0, 3)) {
|
|
3315
|
+
if (!typeNameSet.has(typeName) && listedMethods.size > 0)
|
|
3316
|
+
continue;
|
|
2984
3317
|
const classes = cg.getNodesByName(typeName)
|
|
2985
3318
|
.filter((n) => (n.kind === 'class' || n.kind === 'struct' || n.kind === 'component') && !(0, query_utils_1.isTestFile)(n.filePath));
|
|
2986
3319
|
for (const cls of classes.slice(0, 2)) {
|
|
@@ -2999,8 +3332,11 @@ class ToolHandler {
|
|
|
2999
3332
|
}
|
|
3000
3333
|
}
|
|
3001
3334
|
}
|
|
3335
|
+
const focusMethods = listedMethods.size > 0
|
|
3336
|
+
? methods.filter((m) => listedMethods.has(m.name))
|
|
3337
|
+
: methods;
|
|
3002
3338
|
const methodLines = [];
|
|
3003
|
-
for (const method of methods.slice(0, 25)) {
|
|
3339
|
+
for (const method of (focusMethods.length > 0 ? focusMethods : methods).slice(0, 25)) {
|
|
3004
3340
|
let callers = [];
|
|
3005
3341
|
try {
|
|
3006
3342
|
callers = cg.getCallers(method.id);
|
|
@@ -3034,166 +3370,617 @@ class ToolHandler {
|
|
|
3034
3370
|
lines.push('');
|
|
3035
3371
|
}
|
|
3036
3372
|
}
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
*/
|
|
3046
|
-
buildDataSourceSection(cg, query) {
|
|
3047
|
-
const anchors = (0, query_utils_1.extractTypeNamesFromQuery)(query).filter((n) => /Manager|Service|Handler|Store|Provider|Controller/i.test(n));
|
|
3048
|
-
if (anchors.length === 0)
|
|
3049
|
-
return { section: '', edgeCount: 0 };
|
|
3050
|
-
const rel = (p) => p.replace(/\\/g, '/');
|
|
3051
|
-
// Prefer system/SDK / service APIs — not local UI helpers (getBadgeOffsetX).
|
|
3052
|
-
const SERVICE_RE = /notification|@ohos|ans|subscribe|publish|bundle|ability|vibrator|telephony|NumBadge|notificationManager|badgeManager|wantAgent|distributed/i;
|
|
3053
|
-
const lines = ['**Data sources / upstream services**', ''];
|
|
3054
|
-
let edgeCount = 0;
|
|
3055
|
-
const addEdge = (bucket, node, via, rank) => {
|
|
3056
|
-
if ((0, query_utils_1.isTestFile)(node.filePath))
|
|
3057
|
-
return;
|
|
3058
|
-
const sig = `${node.name} ${node.filePath} ${node.signature || ''}`;
|
|
3059
|
-
if (!SERVICE_RE.test(sig))
|
|
3060
|
-
return;
|
|
3061
|
-
const prev = bucket.get(node.id);
|
|
3062
|
-
if (!prev || rank < prev.rank) {
|
|
3063
|
-
bucket.set(node.id, { name: node.name, file: rel(node.filePath), line: node.startLine, via, rank });
|
|
3064
|
-
if (!prev)
|
|
3065
|
-
edgeCount++;
|
|
3066
|
-
}
|
|
3067
|
-
};
|
|
3068
|
-
for (const name of anchors.slice(0, 3)) {
|
|
3069
|
-
const classes = cg.getNodesByName(name).filter((n) => (n.kind === 'class' || n.kind === 'struct' || n.kind === 'component') && !(0, query_utils_1.isTestFile)(n.filePath));
|
|
3070
|
-
for (const cls of classes.slice(0, 2)) {
|
|
3071
|
-
const upstream = new Map();
|
|
3072
|
-
const methods = [];
|
|
3073
|
-
for (const e of cg.getOutgoingEdges(cls.id)) {
|
|
3074
|
-
if (e.kind !== 'contains')
|
|
3075
|
-
continue;
|
|
3076
|
-
const m = cg.getNode(e.target);
|
|
3077
|
-
if (m && (m.kind === 'method' || m.kind === 'function'))
|
|
3078
|
-
methods.push(m);
|
|
3079
|
-
}
|
|
3080
|
-
if (methods.length === 0) {
|
|
3081
|
-
for (const n of cg.getNodesByName(name)) {
|
|
3082
|
-
if (n.filePath === cls.filePath && (n.kind === 'method' || n.kind === 'function'))
|
|
3083
|
-
methods.push(n);
|
|
3084
|
-
}
|
|
3085
|
-
}
|
|
3086
|
-
// 1) Same-file @ohos / kit imports — the usual system-service surface.
|
|
3373
|
+
// Text call-site fallback for return-value / member consumers when the graph
|
|
3374
|
+
// has no edges onto SDK/.d.ts methods (common for getConnectionState).
|
|
3375
|
+
if (substantive === 0 && (0, query_utils_1.queryAsReturnValueConsumerSurvey)(query)) {
|
|
3376
|
+
const root = projectRoot || '';
|
|
3377
|
+
for (const sym of typeNames.slice(0, 3)) {
|
|
3378
|
+
if (!/^(?:get|is|has|create|on)[A-Z]/.test(sym) && !(0, query_utils_1.isMemberLikeIdentifier)(sym))
|
|
3379
|
+
continue;
|
|
3380
|
+
let scanHits = [];
|
|
3087
3381
|
try {
|
|
3088
|
-
|
|
3089
|
-
if (r.node.filePath !== cls.filePath)
|
|
3090
|
-
continue;
|
|
3091
|
-
addEdge(upstream, r.node, 'import', 0);
|
|
3092
|
-
}
|
|
3093
|
-
for (const term of ['notification', 'badge', 'NumBadge', 'subscribe', 'bundle', 'ability']) {
|
|
3094
|
-
for (const r of cg.searchNodes(term, { kinds: ['import'], limit: 30 })) {
|
|
3095
|
-
if (r.node.filePath !== cls.filePath)
|
|
3096
|
-
continue;
|
|
3097
|
-
addEdge(upstream, r.node, 'import', 1);
|
|
3098
|
-
}
|
|
3099
|
-
}
|
|
3382
|
+
scanHits = cg.searchNodes(sym, { limit: 40 });
|
|
3100
3383
|
}
|
|
3101
|
-
catch {
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3384
|
+
catch {
|
|
3385
|
+
continue;
|
|
3386
|
+
}
|
|
3387
|
+
const esc = sym.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3388
|
+
const callRe = new RegExp(`\\.${esc}\\s*\\(`);
|
|
3389
|
+
const bareRe = new RegExp(`\\b${esc}\\s*\\(`);
|
|
3390
|
+
const seen = new Set();
|
|
3391
|
+
const bullets = [];
|
|
3392
|
+
for (const r of scanHits) {
|
|
3393
|
+
if ((0, query_utils_1.isTestFile)(r.node.filePath))
|
|
3394
|
+
continue;
|
|
3395
|
+
if (/\.d\.ts$/i.test(r.node.filePath) || (0, arkts_1.isOhosApiFilePath)(r.node.filePath))
|
|
3396
|
+
continue;
|
|
3397
|
+
const abs = root
|
|
3398
|
+
? (0, utils_1.validatePathWithinRoot)(root, r.node.filePath)
|
|
3399
|
+
: ((0, fs_1.existsSync)(r.node.filePath) ? r.node.filePath : null);
|
|
3400
|
+
if (!abs)
|
|
3401
|
+
continue;
|
|
3402
|
+
let fileLines = [];
|
|
3105
3403
|
try {
|
|
3106
|
-
|
|
3404
|
+
fileLines = (0, fs_1.readFileSync)(abs, 'utf-8').split('\n');
|
|
3107
3405
|
}
|
|
3108
3406
|
catch {
|
|
3109
3407
|
continue;
|
|
3110
3408
|
}
|
|
3111
|
-
for (
|
|
3112
|
-
|
|
3113
|
-
|
|
3409
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
3410
|
+
const lineText = fileLines[i] ?? '';
|
|
3411
|
+
if (!callRe.test(lineText) && !bareRe.test(lineText))
|
|
3412
|
+
continue;
|
|
3413
|
+
const key = `${rel(r.node.filePath)}:${i + 1}`;
|
|
3414
|
+
if (seen.has(key))
|
|
3415
|
+
continue;
|
|
3416
|
+
seen.add(key);
|
|
3417
|
+
bullets.push(`- \`${sym}\` ← \`${key}\` \`${lineText.trim().slice(0, 120)}\``);
|
|
3418
|
+
if (bullets.length >= 12)
|
|
3419
|
+
break;
|
|
3114
3420
|
}
|
|
3421
|
+
if (bullets.length >= 12)
|
|
3422
|
+
break;
|
|
3115
3423
|
}
|
|
3116
|
-
if (
|
|
3424
|
+
if (bullets.length > 0) {
|
|
3425
|
+
lines.push(`### \`${sym}\` (text call sites)`);
|
|
3426
|
+
lines.push(...bullets);
|
|
3427
|
+
lines.push('');
|
|
3428
|
+
substantive += bullets.length;
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
if (substantive === 0) {
|
|
3433
|
+
if ((0, query_utils_1.queryAsReturnValueConsumerSurvey)(query)) {
|
|
3434
|
+
return [
|
|
3435
|
+
'**Caller inventory**',
|
|
3436
|
+
'',
|
|
3437
|
+
'> No in-repo callers of the return-value member in the graph/index. **ANSWER NOW** — do not open SDK `.d.ts` stubs; Grep once for `.member(` only if needed.',
|
|
3438
|
+
'',
|
|
3439
|
+
].join('\n');
|
|
3440
|
+
}
|
|
3441
|
+
return '';
|
|
3442
|
+
}
|
|
3443
|
+
// Unique caller files — answers "哪些文件调用了 Type::methods" without Grep.
|
|
3444
|
+
if (listedMethods.size > 0 && substantive > 0) {
|
|
3445
|
+
const callerFiles = new Set();
|
|
3446
|
+
for (const line of lines) {
|
|
3447
|
+
const idx = line.indexOf('←');
|
|
3448
|
+
if (idx < 0)
|
|
3117
3449
|
continue;
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
for (const u of ranked.slice(0, 15)) {
|
|
3121
|
-
lines.push(`- \`${u.name}\` at \`${u.file}:${u.line}\` (via \`${u.via}\`)`);
|
|
3450
|
+
for (const m of line.slice(idx).matchAll(/`([^`]+?):(\d+)`/g)) {
|
|
3451
|
+
callerFiles.add(m[1]);
|
|
3122
3452
|
}
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
lines.push('');
|
|
3126
|
-
lines.push('> Prefer `@ohos.*` / notification / bundle imports as the system-service answer; local helpers are secondary.');
|
|
3453
|
+
}
|
|
3454
|
+
if (callerFiles.size > 0) {
|
|
3127
3455
|
lines.push('');
|
|
3456
|
+
lines.push('**Unique caller files**');
|
|
3457
|
+
for (const fp of [...callerFiles].sort()) {
|
|
3458
|
+
lines.push(`- \`${fp}\``);
|
|
3459
|
+
}
|
|
3128
3460
|
}
|
|
3129
3461
|
}
|
|
3130
|
-
|
|
3131
|
-
return { section: '', edgeCount: 0 };
|
|
3132
|
-
lines.push('> Data-source survey — answer from upstream symbols above; source bodies omitted.');
|
|
3462
|
+
lines.push('> Caller inventory complete — answer from this section.');
|
|
3133
3463
|
lines.push('');
|
|
3134
|
-
return
|
|
3464
|
+
return lines.join('\n');
|
|
3135
3465
|
}
|
|
3136
3466
|
/**
|
|
3137
|
-
*
|
|
3467
|
+
* Multi-module / circular dependency survey — lean cycle list, not an import flood.
|
|
3138
3468
|
*/
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3469
|
+
buildModuleDependencySurveySection(cg, query) {
|
|
3470
|
+
// Prefer leaf module tokens (`launchercommon`) over shared parents (`staticcommon`)
|
|
3471
|
+
// — parent matches flood unrelated windowscene cycles.
|
|
3472
|
+
const leafModules = new Set();
|
|
3473
|
+
for (const p of (query.match(/\b[A-Za-z][\w]*(?:common|service|component|constants)\b/gi) ?? [])) {
|
|
3474
|
+
const low = p.toLowerCase();
|
|
3475
|
+
if (!/^(?:static|feature|product|base|common)$/i.test(low))
|
|
3476
|
+
leafModules.add(low);
|
|
3477
|
+
}
|
|
3478
|
+
for (const seg of query.match(/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)+/g) ?? []) {
|
|
3479
|
+
const leaf = seg.split(/[/\\]/).pop()?.toLowerCase();
|
|
3480
|
+
if (leaf && leaf.length >= 4 && /common|service|component|constants/i.test(leaf)) {
|
|
3481
|
+
leafModules.add(leaf);
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
if (leafModules.size === 0)
|
|
3485
|
+
return { section: '', hitCount: 0 };
|
|
3486
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
3487
|
+
const hitLeaf = (fp) => {
|
|
3488
|
+
const low = fp.toLowerCase().replace(/\\/g, '/');
|
|
3489
|
+
for (const n of leafModules) {
|
|
3490
|
+
if (low.includes(`/${n}/`) || low.includes(`/${n}.`) || low.endsWith(`/${n}`))
|
|
3491
|
+
return n;
|
|
3492
|
+
}
|
|
3142
3493
|
return null;
|
|
3494
|
+
};
|
|
3495
|
+
let cycles = [];
|
|
3143
3496
|
try {
|
|
3144
|
-
|
|
3145
|
-
const projectRoot = cg.getProjectRoot();
|
|
3146
|
-
if (toolName === 'homegraph_explore') {
|
|
3147
|
-
return this.tryFastInventoryExplore(cg, query, projectRoot)
|
|
3148
|
-
?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot)
|
|
3149
|
-
?? this.tryLightMechanismExplore(cg, query, projectRoot);
|
|
3150
|
-
}
|
|
3151
|
-
if (toolName === 'homegraph_search') {
|
|
3152
|
-
return this.tryFastInventoryExplore(cg, query, projectRoot)
|
|
3153
|
-
?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot)
|
|
3154
|
-
?? this.tryLightMechanismExplore(cg, query, projectRoot);
|
|
3155
|
-
}
|
|
3497
|
+
cycles = cg.findCircularDependencies();
|
|
3156
3498
|
}
|
|
3157
3499
|
catch {
|
|
3158
|
-
|
|
3500
|
+
cycles = [];
|
|
3501
|
+
}
|
|
3502
|
+
const matchedCycles = cycles
|
|
3503
|
+
.map((c) => {
|
|
3504
|
+
const touched = new Set();
|
|
3505
|
+
const kept = [];
|
|
3506
|
+
for (const p of c) {
|
|
3507
|
+
const leaf = hitLeaf(p);
|
|
3508
|
+
if (leaf) {
|
|
3509
|
+
touched.add(leaf);
|
|
3510
|
+
kept.push(p);
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
return touched.size >= 2 ? kept : [];
|
|
3514
|
+
})
|
|
3515
|
+
.filter((c) => c.length >= 2)
|
|
3516
|
+
.slice(0, 8);
|
|
3517
|
+
const lines = [
|
|
3518
|
+
'**Module dependency / cycle survey**',
|
|
3519
|
+
'',
|
|
3520
|
+
`> Focused on leaf modules \`${[...leafModules].slice(0, 8).join('`, `')}\`. **ANSWER NOW** — do not glob every \`oh-package.json5\`.`,
|
|
3521
|
+
'',
|
|
3522
|
+
];
|
|
3523
|
+
if (matchedCycles.length === 0) {
|
|
3524
|
+
lines.push(`- No **circular** import cycle spanning **two or more** of the named leaf modules.`);
|
|
3525
|
+
lines.push('- (Cycles only inside an unrelated sibling under the same parent path are omitted.)');
|
|
3526
|
+
lines.push('');
|
|
3527
|
+
return { section: lines.join('\n'), hitCount: 1 };
|
|
3159
3528
|
}
|
|
3160
|
-
|
|
3529
|
+
lines.push(`Found **${matchedCycles.length}** cycle(s) spanning named leaf modules:`);
|
|
3530
|
+
for (const c of matchedCycles) {
|
|
3531
|
+
lines.push(`- ${c.map((p) => `\`${rel(p)}\``).join(' → ')}`);
|
|
3532
|
+
}
|
|
3533
|
+
lines.push('');
|
|
3534
|
+
return { section: lines.join('\n'), hitCount: matchedCycles.length };
|
|
3161
3535
|
}
|
|
3162
3536
|
/**
|
|
3163
|
-
*
|
|
3537
|
+
* Path-module / named-Type NAPI export surface — not a 50-file domain dump.
|
|
3164
3538
|
*/
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
const
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3539
|
+
buildModuleExportSurveySection(cg, query, projectRoot) {
|
|
3540
|
+
const paths = (0, query_utils_1.extractPathSegmentsFromQuery)(query);
|
|
3541
|
+
const types = (0, query_utils_1.extractTypeNamesFromQuery)(query).filter((t) => !(0, query_utils_1.isFrameworkUiDecoratorName)(t));
|
|
3542
|
+
const needles = [
|
|
3543
|
+
...paths.map((p) => p.replace(/\\/g, '/').toLowerCase()),
|
|
3544
|
+
...types.map((t) => t.toLowerCase()),
|
|
3545
|
+
].filter((n) => n.length >= 4);
|
|
3546
|
+
if (needles.length === 0)
|
|
3547
|
+
return { section: '', hitCount: 0 };
|
|
3548
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
3549
|
+
const inScope = (fp) => {
|
|
3550
|
+
const low = rel(fp).toLowerCase();
|
|
3551
|
+
return needles.some((n) => low.includes(n));
|
|
3552
|
+
};
|
|
3553
|
+
const exports = [];
|
|
3554
|
+
const seen = new Set();
|
|
3555
|
+
const seedNames = [
|
|
3556
|
+
'Export', 'NapiInit', 'napi_init', 'Init', 'napi_module_register',
|
|
3557
|
+
...types.slice(0, 3),
|
|
3558
|
+
];
|
|
3559
|
+
for (const seed of seedNames) {
|
|
3560
|
+
let hits = [];
|
|
3561
|
+
try {
|
|
3562
|
+
hits = cg.searchNodes(seed, { limit: 30 });
|
|
3563
|
+
}
|
|
3564
|
+
catch {
|
|
3565
|
+
continue;
|
|
3566
|
+
}
|
|
3567
|
+
for (const r of hits) {
|
|
3568
|
+
if (!inScope(r.node.filePath) || (0, query_utils_1.isTestFile)(r.node.filePath))
|
|
3569
|
+
continue;
|
|
3570
|
+
if (!/\.(cpp|cc|cxx|h|hpp)$/i.test(r.node.filePath) && !/napi/i.test(r.node.filePath)) {
|
|
3571
|
+
continue;
|
|
3572
|
+
}
|
|
3573
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
|
|
3574
|
+
if (!abs)
|
|
3575
|
+
continue;
|
|
3576
|
+
let content = '';
|
|
3577
|
+
try {
|
|
3578
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
3579
|
+
}
|
|
3580
|
+
catch {
|
|
3581
|
+
continue;
|
|
3582
|
+
}
|
|
3583
|
+
const fileLines = content.split('\n');
|
|
3584
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
3585
|
+
const line = fileLines[i] ?? '';
|
|
3586
|
+
// napi_define_function / exports / Descriptor { "foo", … }
|
|
3587
|
+
const m = line.match(/napi_define_(?:function|property)\s*\([^,]+,\s*["']([\w]+)["']/i)
|
|
3588
|
+
|| line.match(/\{\s*["']([\w]+)["']\s*,\s*(?:nullptr|NULL)?\s*,\s*\w+/i)
|
|
3589
|
+
|| line.match(/\.?(?:exports|export)\s*[:=].*["']([\w]+)["']/i);
|
|
3590
|
+
if (!m?.[1] || m[1].length < 2)
|
|
3591
|
+
continue;
|
|
3592
|
+
const key = `${m[1]}:${rel(r.node.filePath)}`;
|
|
3593
|
+
if (seen.has(key))
|
|
3594
|
+
continue;
|
|
3595
|
+
seen.add(key);
|
|
3596
|
+
exports.push({
|
|
3597
|
+
name: m[1],
|
|
3598
|
+
file: rel(r.node.filePath),
|
|
3599
|
+
line: i + 1,
|
|
3600
|
+
snippet: line.trim().slice(0, 100),
|
|
3601
|
+
});
|
|
3602
|
+
}
|
|
3603
|
+
// Also list Export / Init method nodes in-scope.
|
|
3604
|
+
if (/^(?:Export|Init|NapiInit)$/i.test(r.node.name)) {
|
|
3605
|
+
const key = `fn:${r.node.name}:${rel(r.node.filePath)}`;
|
|
3606
|
+
if (!seen.has(key)) {
|
|
3607
|
+
seen.add(key);
|
|
3608
|
+
exports.push({
|
|
3609
|
+
name: r.node.name,
|
|
3610
|
+
file: rel(r.node.filePath),
|
|
3611
|
+
line: r.node.startLine,
|
|
3612
|
+
snippet: (r.node.signature || r.node.name).slice(0, 100),
|
|
3613
|
+
});
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
const lines = [
|
|
3619
|
+
'**NAPI / native export survey**',
|
|
3620
|
+
'',
|
|
3621
|
+
`> In-repo NAPI surface for \`${needles.slice(0, 4).join('`, `')}\`. **ANSWER NOW** — do not Grep \`napi_define\` again.`,
|
|
3622
|
+
'',
|
|
3623
|
+
];
|
|
3624
|
+
if (exports.length === 0) {
|
|
3625
|
+
lines.push('- No `napi_define_*` / Export descriptors found under the named path/Type.');
|
|
3626
|
+
lines.push('');
|
|
3627
|
+
return { section: lines.join('\n'), hitCount: 0 };
|
|
3628
|
+
}
|
|
3629
|
+
for (const e of exports.slice(0, 30)) {
|
|
3630
|
+
lines.push(`- \`${e.name}\` — \`${e.file}:${e.line}\` \`${e.snippet}\``);
|
|
3631
|
+
}
|
|
3632
|
+
if (exports.length > 30)
|
|
3633
|
+
lines.push(`- … and ${exports.length - 30} more`);
|
|
3634
|
+
lines.push('');
|
|
3635
|
+
return { section: lines.join('\n'), hitCount: Math.min(exports.length, 30) };
|
|
3636
|
+
}
|
|
3637
|
+
/**
|
|
3638
|
+
* Named `.d.ts` wrap — where the basename is imported / called in-repo.
|
|
3639
|
+
*/
|
|
3640
|
+
buildDtsWrapSurveySection(cg, query, projectRoot) {
|
|
3641
|
+
const bases = (0, query_utils_1.extractFileBasenamesFromQuery)(query)
|
|
3642
|
+
.map((b) => b.replace(/\.d\.ts$/i, ''))
|
|
3643
|
+
.filter((b) => b.length >= 3);
|
|
3644
|
+
if (bases.length === 0 && /\.d\.ts\b/i.test(query)) {
|
|
3645
|
+
for (const m of query.matchAll(/\b([A-Za-z][\w]*)\.d\.ts\b/g)) {
|
|
3646
|
+
if (m[1])
|
|
3647
|
+
bases.push(m[1]);
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
if (bases.length === 0)
|
|
3651
|
+
return { section: '', hitCount: 0 };
|
|
3652
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
3653
|
+
const hits = new Map();
|
|
3654
|
+
for (const base of bases.slice(0, 3)) {
|
|
3655
|
+
let nodes = [];
|
|
3656
|
+
try {
|
|
3657
|
+
nodes = cg.searchNodes(base, { limit: 40 });
|
|
3658
|
+
}
|
|
3659
|
+
catch {
|
|
3660
|
+
continue;
|
|
3661
|
+
}
|
|
3662
|
+
const re = new RegExp(`(?:from\\s+['"].*${base}['"]|import\\s+.*\\b${base}\\b|\\b${base}\\.\\w+)`, 'i');
|
|
3663
|
+
const seen = new Set();
|
|
3664
|
+
for (const r of nodes) {
|
|
3665
|
+
if ((0, query_utils_1.isTestFile)(r.node.filePath))
|
|
3666
|
+
continue;
|
|
3667
|
+
const fp = rel(r.node.filePath);
|
|
3668
|
+
if (/\.d\.ts$/i.test(fp))
|
|
3669
|
+
continue;
|
|
3670
|
+
if (seen.has(fp))
|
|
3671
|
+
continue;
|
|
3672
|
+
seen.add(fp);
|
|
3673
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
|
|
3674
|
+
if (!abs)
|
|
3675
|
+
continue;
|
|
3676
|
+
let content = '';
|
|
3677
|
+
try {
|
|
3678
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
3679
|
+
}
|
|
3680
|
+
catch {
|
|
3681
|
+
continue;
|
|
3682
|
+
}
|
|
3683
|
+
const fileLines = content.split('\n');
|
|
3684
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
3685
|
+
const line = fileLines[i] ?? '';
|
|
3686
|
+
if (!re.test(line) && !line.includes(base))
|
|
3687
|
+
continue;
|
|
3688
|
+
if (!new RegExp(`\\b${base}\\b`, 'i').test(line))
|
|
3689
|
+
continue;
|
|
3690
|
+
const prev = hits.get(fp) ?? { lines: [], snippet: '' };
|
|
3691
|
+
if (!prev.lines.includes(i + 1))
|
|
3692
|
+
prev.lines.push(i + 1);
|
|
3693
|
+
if (!prev.snippet)
|
|
3694
|
+
prev.snippet = line.trim().slice(0, 100);
|
|
3695
|
+
hits.set(fp, prev);
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
const lines = [
|
|
3700
|
+
'**`.d.ts` wrap / call sites**',
|
|
3701
|
+
'',
|
|
3702
|
+
`> In-repo imports/uses of \`${bases.join('`, `')}\` (stub file itself omitted). **ANSWER NOW.**`,
|
|
3703
|
+
'',
|
|
3704
|
+
];
|
|
3705
|
+
if (hits.size === 0) {
|
|
3706
|
+
lines.push('- No in-repo import/call sites indexed for this wrap.');
|
|
3707
|
+
lines.push('');
|
|
3708
|
+
return { section: lines.join('\n'), hitCount: 0 };
|
|
3709
|
+
}
|
|
3710
|
+
for (const [fp, info] of [...hits.entries()].sort((a, b) => a[0].localeCompare(b[0])).slice(0, 28)) {
|
|
3711
|
+
const ls = info.lines.slice(0, 4).map((l) => `L${l}`).join(', ');
|
|
3712
|
+
lines.push(info.snippet
|
|
3713
|
+
? `- \`${fp}\` (${ls}) \`${info.snippet}\``
|
|
3714
|
+
: `- \`${fp}\` (${ls})`);
|
|
3715
|
+
}
|
|
3716
|
+
lines.push('');
|
|
3717
|
+
return { section: lines.join('\n'), hitCount: hits.size };
|
|
3718
|
+
}
|
|
3719
|
+
/**
|
|
3720
|
+
* Named Toggle/control state-sync — which files/modules mention the control
|
|
3721
|
+
* (Type may be missing from FTS; seed stems + text scan).
|
|
3722
|
+
*/
|
|
3723
|
+
buildNamedControlStateSyncSection(cg, query, projectRoot) {
|
|
3724
|
+
const types = (0, query_utils_1.extractTypeNamesFromQuery)(query).filter((t) => !(0, query_utils_1.isFrameworkUiDecoratorName)(t));
|
|
3725
|
+
if (types.length === 0)
|
|
3726
|
+
return { section: '', hitCount: 0 };
|
|
3727
|
+
const stems = new Set();
|
|
3728
|
+
for (const t of types) {
|
|
3729
|
+
stems.add(t);
|
|
3730
|
+
for (const m of t.matchAll(/[A-Z][a-z]+/g)) {
|
|
3731
|
+
if (m[0] && m[0].length >= 4)
|
|
3732
|
+
stems.add(m[0]);
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
3736
|
+
const hits = new Map();
|
|
3737
|
+
for (const stem of [...stems].slice(0, 6)) {
|
|
3738
|
+
let nodes = [];
|
|
3739
|
+
try {
|
|
3740
|
+
nodes = cg.searchNodes(stem, { limit: 35 });
|
|
3741
|
+
}
|
|
3742
|
+
catch {
|
|
3743
|
+
continue;
|
|
3744
|
+
}
|
|
3745
|
+
const re = new RegExp(`\\b${stem.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`);
|
|
3746
|
+
const seen = new Set();
|
|
3747
|
+
for (const r of nodes) {
|
|
3748
|
+
if ((0, query_utils_1.isTestFile)(r.node.filePath))
|
|
3749
|
+
continue;
|
|
3750
|
+
const fp = rel(r.node.filePath);
|
|
3751
|
+
if (seen.has(fp))
|
|
3752
|
+
continue;
|
|
3753
|
+
seen.add(fp);
|
|
3754
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
|
|
3755
|
+
if (!abs)
|
|
3756
|
+
continue;
|
|
3757
|
+
let content = '';
|
|
3758
|
+
try {
|
|
3759
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
3760
|
+
}
|
|
3761
|
+
catch {
|
|
3762
|
+
continue;
|
|
3763
|
+
}
|
|
3764
|
+
const fileLines = content.split('\n');
|
|
3765
|
+
let matched = false;
|
|
3766
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
3767
|
+
const line = fileLines[i] ?? '';
|
|
3768
|
+
if (!re.test(line) && !line.includes(stem))
|
|
3769
|
+
continue;
|
|
3770
|
+
const prev = hits.get(fp) ?? { lines: [], snippet: '', score: 0 };
|
|
3771
|
+
if (!prev.lines.includes(i + 1))
|
|
3772
|
+
prev.lines.push(i + 1);
|
|
3773
|
+
if (!prev.snippet)
|
|
3774
|
+
prev.snippet = line.trim().slice(0, 100);
|
|
3775
|
+
if (/Manager|Service|Store|Controller|Provider/i.test(fp) || /Manager|Service|Store/.test(line)) {
|
|
3776
|
+
prev.score += 5;
|
|
3777
|
+
}
|
|
3778
|
+
prev.score += 1;
|
|
3779
|
+
hits.set(fp, prev);
|
|
3780
|
+
matched = true;
|
|
3781
|
+
}
|
|
3782
|
+
if (!matched && /Toggle|Switch|Hotspot|Manager/i.test(r.node.name)) {
|
|
3783
|
+
hits.set(fp, {
|
|
3784
|
+
lines: [r.node.startLine],
|
|
3785
|
+
snippet: (r.node.signature || r.node.name).slice(0, 100),
|
|
3786
|
+
score: 2,
|
|
3787
|
+
});
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
const ranked = [...hits.entries()].sort((a, b) => b[1].score - a[1].score || a[0].localeCompare(b[0]));
|
|
3792
|
+
const lines = [
|
|
3793
|
+
'**Control / Toggle state-sync sites**',
|
|
3794
|
+
'',
|
|
3795
|
+
`> In-repo references to \`${types.join('`, `')}\` (and stems). Prefer Manager/Service files for \"which module guarantees consistency\". **ANSWER NOW.**`,
|
|
3796
|
+
'',
|
|
3797
|
+
];
|
|
3798
|
+
if (ranked.length === 0) {
|
|
3799
|
+
lines.push('- No indexed sites for the named control; answer from domain knowledge only if sure.');
|
|
3800
|
+
lines.push('');
|
|
3801
|
+
return { section: lines.join('\n'), hitCount: 0 };
|
|
3802
|
+
}
|
|
3803
|
+
for (const [fp, info] of ranked.slice(0, 24)) {
|
|
3804
|
+
const ls = info.lines.slice(0, 3).map((l) => `L${l}`).join(', ');
|
|
3805
|
+
lines.push(info.snippet
|
|
3806
|
+
? `- \`${fp}\` (${ls}) \`${info.snippet}\``
|
|
3807
|
+
: `- \`${fp}\` (${ls})`);
|
|
3808
|
+
}
|
|
3809
|
+
lines.push('');
|
|
3810
|
+
return { section: lines.join('\n'), hitCount: Math.min(ranked.length, 24) };
|
|
3811
|
+
}
|
|
3812
|
+
buildDataSourceSection(cg, query) {
|
|
3813
|
+
const anchors = (0, query_utils_1.extractTypeNamesFromQuery)(query).filter((n) => /Manager|Service|Handler|Store|Provider|Controller/i.test(n));
|
|
3814
|
+
if (anchors.length === 0)
|
|
3815
|
+
return { section: '', edgeCount: 0 };
|
|
3816
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
3817
|
+
// Prefer system/SDK / service APIs — not local UI helpers (getBadgeOffsetX).
|
|
3818
|
+
const SERVICE_RE = /notification|@ohos|ans|subscribe|publish|bundle|ability|vibrator|telephony|NumBadge|notificationManager|badgeManager|wantAgent|distributed|account|osAccount|AppAccount|distributedAccount|userId|getOsAccount/i;
|
|
3819
|
+
const lines = ['**Data sources / upstream services**', ''];
|
|
3820
|
+
let edgeCount = 0;
|
|
3821
|
+
const addEdge = (bucket, node, via, rank, opts) => {
|
|
3822
|
+
if ((0, query_utils_1.isTestFile)(node.filePath))
|
|
3823
|
+
return;
|
|
3824
|
+
const sig = `${node.name} ${node.filePath} ${node.signature || ''}`;
|
|
3825
|
+
// Same-file @ohos/@kit imports are always data-source candidates (BadgeManager
|
|
3826
|
+
// → notificationManager), even when the import local name fails SERVICE_RE.
|
|
3827
|
+
if (!opts?.importAlways && !SERVICE_RE.test(sig))
|
|
3828
|
+
return;
|
|
3829
|
+
if (opts?.importAlways && !/@ohos\.|@kit\.|ohos\./i.test(sig))
|
|
3830
|
+
return;
|
|
3831
|
+
const prev = bucket.get(node.id);
|
|
3832
|
+
if (!prev || rank < prev.rank) {
|
|
3833
|
+
bucket.set(node.id, { name: node.name, file: rel(node.filePath), line: node.startLine, via, rank });
|
|
3834
|
+
if (!prev)
|
|
3835
|
+
edgeCount++;
|
|
3836
|
+
}
|
|
3837
|
+
};
|
|
3838
|
+
for (const name of anchors.slice(0, 3)) {
|
|
3839
|
+
const classes = cg.getNodesByName(name).filter((n) => (n.kind === 'class' || n.kind === 'struct' || n.kind === 'component') && !(0, query_utils_1.isTestFile)(n.filePath));
|
|
3840
|
+
for (const cls of classes.slice(0, 2)) {
|
|
3841
|
+
const upstream = new Map();
|
|
3842
|
+
const methods = [];
|
|
3843
|
+
for (const e of cg.getOutgoingEdges(cls.id)) {
|
|
3844
|
+
if (e.kind !== 'contains')
|
|
3845
|
+
continue;
|
|
3846
|
+
const m = cg.getNode(e.target);
|
|
3847
|
+
if (m && (m.kind === 'method' || m.kind === 'function'))
|
|
3848
|
+
methods.push(m);
|
|
3849
|
+
}
|
|
3850
|
+
if (methods.length === 0) {
|
|
3851
|
+
for (const n of cg.getNodesByName(name)) {
|
|
3852
|
+
if (n.filePath === cls.filePath && (n.kind === 'method' || n.kind === 'function'))
|
|
3853
|
+
methods.push(n);
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
// 1) Same-file @ohos / kit imports — the usual system-service surface.
|
|
3857
|
+
try {
|
|
3858
|
+
for (const r of cg.searchNodes('@ohos', { kinds: ['import'], limit: 40 })) {
|
|
3859
|
+
if (r.node.filePath !== cls.filePath)
|
|
3860
|
+
continue;
|
|
3861
|
+
addEdge(upstream, r.node, 'import', 0, { importAlways: true });
|
|
3862
|
+
}
|
|
3863
|
+
for (const r of cg.searchNodes('@kit', { kinds: ['import'], limit: 40 })) {
|
|
3864
|
+
if (r.node.filePath !== cls.filePath)
|
|
3865
|
+
continue;
|
|
3866
|
+
addEdge(upstream, r.node, 'import', 0, { importAlways: true });
|
|
3867
|
+
}
|
|
3868
|
+
for (const term of [
|
|
3869
|
+
'notification', 'badge', 'NumBadge', 'subscribe', 'bundle', 'ability',
|
|
3870
|
+
'account', 'osAccount', 'AppAccount', 'distributedAccount',
|
|
3871
|
+
]) {
|
|
3872
|
+
for (const r of cg.searchNodes(term, { kinds: ['import'], limit: 30 })) {
|
|
3873
|
+
if (r.node.filePath !== cls.filePath)
|
|
3874
|
+
continue;
|
|
3875
|
+
addEdge(upstream, r.node, 'import', 1, { importAlways: true });
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
catch { /* */ }
|
|
3880
|
+
// 2) Callees that look like services/APIs.
|
|
3881
|
+
for (const method of methods.slice(0, 25)) {
|
|
3882
|
+
let callees = [];
|
|
3883
|
+
try {
|
|
3884
|
+
callees = cg.getCallees(method.id);
|
|
3885
|
+
}
|
|
3886
|
+
catch {
|
|
3887
|
+
continue;
|
|
3888
|
+
}
|
|
3889
|
+
for (const c of callees) {
|
|
3890
|
+
if (c?.node)
|
|
3891
|
+
addEdge(upstream, c.node, method.name, 2);
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
if (upstream.size === 0)
|
|
3895
|
+
continue;
|
|
3896
|
+
lines.push(`### \`${cls.name}\` (\`${rel(cls.filePath)}\`)`);
|
|
3897
|
+
const ranked = [...upstream.values()].sort((a, b) => a.rank - b.rank || a.name.localeCompare(b.name));
|
|
3898
|
+
for (const u of ranked.slice(0, 15)) {
|
|
3899
|
+
lines.push(`- \`${u.name}\` at \`${u.file}:${u.line}\` (via \`${u.via}\`)`);
|
|
3900
|
+
}
|
|
3901
|
+
if (ranked.length > 15)
|
|
3902
|
+
lines.push(`- … and ${ranked.length - 15} more upstream symbol(s)`);
|
|
3903
|
+
lines.push('');
|
|
3904
|
+
lines.push('> Prefer `@ohos.*` / notification / bundle imports as the system-service answer; local helpers are secondary.');
|
|
3905
|
+
lines.push('');
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
if (edgeCount === 0) {
|
|
3909
|
+
lines.push('- No `@ohos`/`@kit` / service-like upstream edges indexed for the named Manager. ' +
|
|
3910
|
+
'**ANSWER NOW** from the Manager definition file (prefer in-repo `AccountManager` / `*Manager.ets`, not SDK `.d.ts`).');
|
|
3911
|
+
lines.push('');
|
|
3912
|
+
return { section: lines.join('\n'), edgeCount: 0 };
|
|
3913
|
+
}
|
|
3914
|
+
lines.push('> Data-source survey — **ANSWER NOW** from upstream symbols above (system `@ohos`/`@kit` imports first). ' +
|
|
3915
|
+
'Do not Grep/search/callers the same Manager for the service name.');
|
|
3916
|
+
lines.push('');
|
|
3917
|
+
return { section: lines.join('\n'), edgeCount };
|
|
3918
|
+
}
|
|
3919
|
+
/**
|
|
3920
|
+
* Main-thread fast path for inventory surveys — skips the worker queue.
|
|
3921
|
+
*/
|
|
3922
|
+
tryFastPathResult(toolName, args) {
|
|
3923
|
+
const query = args.query;
|
|
3924
|
+
if (typeof query !== 'string')
|
|
3925
|
+
return null;
|
|
3926
|
+
const deferKind = (0, query_utils_1.queryShouldDeferToBuiltinTools)(query);
|
|
3927
|
+
if (deferKind) {
|
|
3928
|
+
return this.textResult((0, query_utils_1.homegraphDeferGuidance)(deferKind, query));
|
|
3929
|
+
}
|
|
3930
|
+
try {
|
|
3931
|
+
const cg = this.getHomeGraph(args.projectPath);
|
|
3932
|
+
const projectRoot = cg.getProjectRoot();
|
|
3933
|
+
if (toolName === 'homegraph_explore') {
|
|
3934
|
+
return this.tryFastInventoryExplore(cg, query, projectRoot)
|
|
3935
|
+
?? this.tryLightMechanismExplore(cg, query, projectRoot)
|
|
3936
|
+
?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot);
|
|
3937
|
+
}
|
|
3938
|
+
if (toolName === 'homegraph_search') {
|
|
3939
|
+
return this.tryFastInventoryExplore(cg, query, projectRoot)
|
|
3940
|
+
?? this.tryLightMechanismExplore(cg, query, projectRoot)
|
|
3941
|
+
?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot);
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
catch {
|
|
3945
|
+
return null;
|
|
3946
|
+
}
|
|
3947
|
+
return null;
|
|
3948
|
+
}
|
|
3949
|
+
/**
|
|
3950
|
+
* Fast inventory-only explore — skips findRelevantContext for survey/caller/dependency queries.
|
|
3951
|
+
*/
|
|
3952
|
+
tryFastInventoryExplore(cg, query, projectRoot) {
|
|
3953
|
+
if (!(0, query_utils_1.shouldTryFastInventoryExplore)(query))
|
|
3954
|
+
return null;
|
|
3955
|
+
const lines = [`**Exploration: ${query}**`, '', ''];
|
|
3956
|
+
const summaryLineIdx = 2;
|
|
3957
|
+
const inheritanceOnly = (0, query_utils_1.shouldBuildInheritanceSurvey)(query);
|
|
3958
|
+
const typeSurface = inheritanceOnly
|
|
3959
|
+
|| (0, query_utils_1.shouldBuildCallerInventory)(query)
|
|
3960
|
+
|| (0, query_utils_1.queryIsTypeNameFocus)(query);
|
|
3961
|
+
// Type / hierarchy / method-caller surveys: return inventory first — never a
|
|
3962
|
+
// definition dump or full-graph explore. Agent already named the type.
|
|
3963
|
+
// Exception: caller + co-named "definition visible" needs bodies (compact path).
|
|
3964
|
+
if (typeSurface && !(0, query_utils_1.queryNeedsCoNamedUseBridge)(query)) {
|
|
3965
|
+
const inheritanceSection = this.buildInheritanceSurveySection(cg, query);
|
|
3966
|
+
const callerSection = (0, query_utils_1.shouldBuildCallerInventory)(query)
|
|
3967
|
+
? this.buildCallerListingSection(cg, query, projectRoot)
|
|
3968
|
+
: '';
|
|
3969
|
+
const inheritanceListed = inheritanceSection
|
|
3970
|
+
? inheritanceSection.split('\n').filter((l) => l.startsWith('- `')).length > 0
|
|
3971
|
+
: false;
|
|
3972
|
+
const callerBulletCount = callerSection
|
|
3973
|
+
? callerSection.split('\n').filter((l) => l.startsWith('- ') && l.includes(' ← ')).length
|
|
3974
|
+
: 0;
|
|
3975
|
+
if (inheritanceListed || callerBulletCount > 0) {
|
|
3976
|
+
const parts = [
|
|
3190
3977
|
`**Exploration: ${query}**`,
|
|
3191
3978
|
'',
|
|
3192
3979
|
inheritanceListed && callerBulletCount > 0
|
|
3193
|
-
? 'Type surface: inheritance + method caller inventory below —
|
|
3980
|
+
? 'Type surface: inheritance + method caller inventory below — **ANSWER NOW**; do not grep `extends` and do not `homegraph_callers` each method.'
|
|
3194
3981
|
: inheritanceListed
|
|
3195
|
-
? 'Inheritance survey above lists all direct subtypes found.'
|
|
3196
|
-
: 'Caller inventory lists method→caller sites —
|
|
3982
|
+
? 'Inheritance survey above lists all direct subtypes found. **ANSWER NOW** — do not Grep `extends`.'
|
|
3983
|
+
: 'Caller inventory lists method→caller sites — **ANSWER NOW**; do not fan out `homegraph_callers` per method.',
|
|
3197
3984
|
];
|
|
3198
3985
|
if (inheritanceSection)
|
|
3199
3986
|
parts.push(inheritanceSection);
|
|
@@ -3201,47 +3988,128 @@ class ToolHandler {
|
|
|
3201
3988
|
parts.push(callerSection);
|
|
3202
3989
|
return this.textResult(parts.join('\n'));
|
|
3203
3990
|
}
|
|
3204
|
-
//
|
|
3205
|
-
|
|
3206
|
-
|
|
3991
|
+
// Empty inheritance graph: stop only for *explicit* subclass intent
|
|
3992
|
+
// (子类/extends/subclass). Bare `search("IntGrid")` must fall through to
|
|
3993
|
+
// compact definition — an empty subtype ANSWER NOW blocked visibility/use
|
|
3994
|
+
// questions that reuse a type name after callers().
|
|
3995
|
+
if ((0, query_utils_1.queryAsInheritanceSurvey)(query)) {
|
|
3996
|
+
return this.textResult([
|
|
3997
|
+
`**Exploration: ${query}**`,
|
|
3998
|
+
'',
|
|
3999
|
+
'No direct subtypes / inheritance edges indexed for the named type(s). **ANSWER NOW** — do not Grep `extends` unless you need a second opinion.',
|
|
4000
|
+
'',
|
|
4001
|
+
].join('\n'));
|
|
4002
|
+
}
|
|
4003
|
+
// Bare type-name focus with no subtypes — fall through to compact / other
|
|
4004
|
+
// inventory (definition + usage), not a fat full explore.
|
|
4005
|
+
}
|
|
4006
|
+
const importResult = (inheritanceOnly
|
|
4007
|
+
|| (0, query_utils_1.queryIsTypeNameFocus)(query)
|
|
4008
|
+
|| (0, query_utils_1.queryAsInheritanceSurvey)(query)
|
|
4009
|
+
|| (0, query_utils_1.queryAsDataSourceSurvey)(query)
|
|
4010
|
+
|| (0, query_utils_1.shouldBuildApiUsageSurvey)(query)
|
|
4011
|
+
|| (0, query_utils_1.queryAsInRepoSystemCapabilityHowto)(query)
|
|
4012
|
+
|| (0, query_utils_1.queryAsReturnValueConsumerSurvey)(query)
|
|
4013
|
+
|| (0, query_utils_1.queryAsDeclarationSiteSurvey)(query)
|
|
4014
|
+
|| (0, query_utils_1.queryAsModuleExportSurvey)(query)
|
|
4015
|
+
|| (0, query_utils_1.queryAsModuleDependencySurvey)(query)
|
|
4016
|
+
// Kit install-deps needs oh-package lines — skip bare import inventory.
|
|
4017
|
+
|| (0, query_utils_1.queryAsksKitInstallDeps)(query))
|
|
3207
4018
|
? { section: '', siteCount: 0, compactListing: false }
|
|
3208
4019
|
: this.buildImportSitesSection(cg, query, projectRoot);
|
|
3209
4020
|
if (importResult.section)
|
|
3210
4021
|
lines.push(importResult.section);
|
|
4022
|
+
// Kit usage section is redundant when a focused import inventory already listed
|
|
4023
|
+
// every matching `@kit`/`export` site (avoids a second dump + Grep temptation).
|
|
4024
|
+
// Install-deps questions always take the kit survey (oh-package + imports).
|
|
3211
4025
|
const kitUsageResult = (0, query_utils_1.shouldBuildKitModuleUsageSurvey)(query)
|
|
4026
|
+
&& ((0, query_utils_1.queryAsksKitInstallDeps)(query)
|
|
4027
|
+
|| !(importResult.compactListing && importResult.siteCount > 0))
|
|
3212
4028
|
? this.buildKitModuleUsageSection(cg, query, projectRoot)
|
|
3213
4029
|
: { section: '', symbolCount: 0 };
|
|
3214
4030
|
if (kitUsageResult.section)
|
|
3215
4031
|
lines.push(kitUsageResult.section);
|
|
3216
|
-
const domainFileResult = (0, query_utils_1.shouldBuildDomainFileSurvey)(query)
|
|
4032
|
+
const domainFileResult = (0, query_utils_1.shouldBuildDomainFileSurvey)(query) && !(0, query_utils_1.queryAsDataSourceSurvey)(query)
|
|
4033
|
+
&& !(0, query_utils_1.queryAsInRepoSystemCapabilityHowto)(query)
|
|
4034
|
+
&& !(0, query_utils_1.queryAsModuleDependencySurvey)(query)
|
|
4035
|
+
&& !(0, query_utils_1.queryAsModuleExportSurvey)(query)
|
|
4036
|
+
&& !(0, query_utils_1.queryAsDtsWrapSurvey)(query)
|
|
4037
|
+
&& !(0, query_utils_1.queryAsDeclarationSiteSurvey)(query)
|
|
3217
4038
|
? this.buildDomainFileSurveySection(cg, query)
|
|
3218
4039
|
: { section: '', fileCount: 0 };
|
|
3219
4040
|
if (domainFileResult.section)
|
|
3220
4041
|
lines.push(domainFileResult.section);
|
|
4042
|
+
const systemCapResult = (0, query_utils_1.queryAsInRepoSystemCapabilityHowto)(query)
|
|
4043
|
+
? this.buildSystemCapabilityHowtoSection(cg, query, projectRoot)
|
|
4044
|
+
: { section: '', hitCount: 0 };
|
|
4045
|
+
if (systemCapResult.section)
|
|
4046
|
+
lines.push(systemCapResult.section);
|
|
4047
|
+
const declarationResult = (0, query_utils_1.queryAsDeclarationSiteSurvey)(query)
|
|
4048
|
+
? this.buildDeclarationSiteSurveySection(cg, query, projectRoot)
|
|
4049
|
+
: { section: '', hitCount: 0, attempted: false };
|
|
4050
|
+
if (declarationResult.section)
|
|
4051
|
+
lines.push(declarationResult.section);
|
|
4052
|
+
// Kit usage surveys already list @kit import sites — a second API-usage dump
|
|
4053
|
+
// (taskpool × 32 files) doubles tokens without new signal.
|
|
3221
4054
|
const apiUsageResult = (0, query_utils_1.shouldBuildApiUsageSurvey)(query)
|
|
4055
|
+
&& !(0, query_utils_1.queryAsDeclarationSiteSurvey)(query)
|
|
4056
|
+
&& !(0, query_utils_1.shouldBuildKitModuleUsageSurvey)(query)
|
|
3222
4057
|
? this.buildApiUsageSection(cg, query, projectRoot)
|
|
3223
4058
|
: { section: '', fileCount: 0 };
|
|
3224
4059
|
if (apiUsageResult.section)
|
|
3225
4060
|
lines.push(apiUsageResult.section);
|
|
4061
|
+
const moduleDepResult = (0, query_utils_1.queryAsModuleDependencySurvey)(query)
|
|
4062
|
+
? this.buildModuleDependencySurveySection(cg, query)
|
|
4063
|
+
: { section: '', hitCount: 0 };
|
|
4064
|
+
if (moduleDepResult.section)
|
|
4065
|
+
lines.push(moduleDepResult.section);
|
|
4066
|
+
const moduleExportResult = (0, query_utils_1.queryAsModuleExportSurvey)(query)
|
|
4067
|
+
? this.buildModuleExportSurveySection(cg, query, projectRoot)
|
|
4068
|
+
: { section: '', hitCount: 0 };
|
|
4069
|
+
if (moduleExportResult.section)
|
|
4070
|
+
lines.push(moduleExportResult.section);
|
|
4071
|
+
const dtsWrapResult = (0, query_utils_1.queryAsDtsWrapSurvey)(query)
|
|
4072
|
+
? this.buildDtsWrapSurveySection(cg, query, projectRoot)
|
|
4073
|
+
: { section: '', hitCount: 0 };
|
|
4074
|
+
if (dtsWrapResult.section)
|
|
4075
|
+
lines.push(dtsWrapResult.section);
|
|
4076
|
+
const controlSyncResult = (0, query_utils_1.queryAsNamedControlStateSyncSurvey)(query)
|
|
4077
|
+
? this.buildNamedControlStateSyncSection(cg, query, projectRoot)
|
|
4078
|
+
: { section: '', hitCount: 0 };
|
|
4079
|
+
if (controlSyncResult.section)
|
|
4080
|
+
lines.push(controlSyncResult.section);
|
|
3226
4081
|
const dataSourceResult = (0, query_utils_1.queryAsDataSourceSurvey)(query)
|
|
3227
4082
|
? this.buildDataSourceSection(cg, query)
|
|
3228
4083
|
: { section: '', edgeCount: 0 };
|
|
3229
4084
|
if (dataSourceResult.section)
|
|
3230
4085
|
lines.push(dataSourceResult.section);
|
|
4086
|
+
const hoverResult = (0, query_utils_1.shouldBuildHoverHandlerSurvey)(query)
|
|
4087
|
+
? this.buildHoverHandlerSurveySection(cg, query, projectRoot)
|
|
4088
|
+
: { section: '', hitCount: 0 };
|
|
4089
|
+
if (hoverResult.section)
|
|
4090
|
+
lines.push(hoverResult.section);
|
|
3231
4091
|
const importInventoryFilter = (0, query_utils_1.hasImportInventoryFilter)(query);
|
|
3232
4092
|
const multiAnchor = (0, query_utils_1.queryNamesMultipleExploreAnchors)(query);
|
|
3233
4093
|
// Type / caller surveys: skip expensive named-symbol flow synthesize —
|
|
3234
4094
|
// it wanders unrelated dynamic edges and burns tokens.
|
|
3235
|
-
const skipFlow = typeSurface || (0, query_utils_1.queryAsCallerOrMethodSurvey)(query)
|
|
4095
|
+
const skipFlow = typeSurface || (0, query_utils_1.queryAsCallerOrMethodSurvey)(query)
|
|
4096
|
+
|| (0, query_utils_1.queryAsDeclarationSiteSurvey)(query)
|
|
4097
|
+
|| (0, query_utils_1.queryAsInRepoSystemCapabilityHowto)(query)
|
|
4098
|
+
|| (0, query_utils_1.queryAsReturnValueConsumerSurvey)(query);
|
|
3236
4099
|
const flow = skipFlow
|
|
3237
4100
|
? { pathNodeIds: new Set(), text: '' }
|
|
3238
4101
|
: this.buildFlowFromNamedSymbols(cg, query);
|
|
3239
4102
|
const hasFlowPath = flow.pathNodeIds.size > 0;
|
|
3240
4103
|
const inheritanceSection = !hasFlowPath && !multiAnchor
|
|
3241
4104
|
? this.buildInheritanceSurveySection(cg, query) : '';
|
|
3242
|
-
const
|
|
3243
|
-
|
|
4105
|
+
const listedTypeMethods = (0, query_utils_1.extractListedTypeMethodsFromQuery)(query).length > 0;
|
|
4106
|
+
// Type + listed methods (BinaryGrid Set/Test/Fill) must keep caller inventory even
|
|
4107
|
+
// when multi-anchor would otherwise skip it into a fat source dump.
|
|
4108
|
+
const callerSection = !hasFlowPath && (!multiAnchor || listedTypeMethods) && (0, query_utils_1.shouldBuildCallerInventory)(query)
|
|
4109
|
+
? this.buildCallerListingSection(cg, query, projectRoot) : '';
|
|
4110
|
+
// Field new/delete inventories already list text sites — skip redundant member scan.
|
|
3244
4111
|
const memberSection = !hasFlowPath && !multiAnchor && (0, query_utils_1.shouldBuildMemberSurvey)(query)
|
|
4112
|
+
&& !((0, query_utils_1.queryAsFieldUsageSurvey)(query) && apiUsageResult.fileCount > 0)
|
|
3245
4113
|
? this.buildMemberSurveySection(cg, query, projectRoot) : '';
|
|
3246
4114
|
const configSection = (0, query_utils_1.shouldBuildConfigSection)(query)
|
|
3247
4115
|
? this.buildConfigFileSection(cg, query, projectRoot) : '';
|
|
@@ -3267,10 +4135,60 @@ class ToolHandler {
|
|
|
3267
4135
|
? inheritanceSection.split('\n').filter((l) => l.startsWith('- `')).length > 0
|
|
3268
4136
|
: false;
|
|
3269
4137
|
const hasAnySection = importResult.section || kitUsageResult.section || domainFileResult.section
|
|
3270
|
-
|| apiUsageResult.section || dataSourceResult.section || inheritanceSection
|
|
3271
|
-
|| callerSection || memberSection || configSection
|
|
4138
|
+
|| apiUsageResult.section || dataSourceResult.section || hoverResult.section || inheritanceSection
|
|
4139
|
+
|| callerSection || memberSection || configSection
|
|
4140
|
+
|| systemCapResult.section || declarationResult.section || moduleDepResult.section
|
|
4141
|
+
|| moduleExportResult.section || dtsWrapResult.section || controlSyncResult.section;
|
|
3272
4142
|
if (!hasAnySection)
|
|
3273
4143
|
return null;
|
|
4144
|
+
// Data-source / API usage inventories are complete without source dumps —
|
|
4145
|
+
// stop here so agents do not also get a fat import list + follow-up Read.
|
|
4146
|
+
if (systemCapResult.hitCount > 0) {
|
|
4147
|
+
return finishCompact(`System-capability howto — **${systemCapResult.hitCount}** in-repo site(s). **ANSWER NOW** from \`@ohos\`/\`System\` call sites above.`);
|
|
4148
|
+
}
|
|
4149
|
+
// Declaration intent must NOT fall through to a fat explore / include dump when empty.
|
|
4150
|
+
if (declarationResult.attempted) {
|
|
4151
|
+
return finishCompact(declarationResult.hitCount > 0
|
|
4152
|
+
? `Declaration-site survey — **${declarationResult.hitCount}** site(s). **ANSWER NOW** from ids + bindings above; do not Grep the same Type.`
|
|
4153
|
+
: 'Declaration-site survey complete (0 sites). **ANSWER NOW** from the note above.');
|
|
4154
|
+
}
|
|
4155
|
+
if (controlSyncResult.section) {
|
|
4156
|
+
return finishCompact(`Control/Toggle state-sync survey — **${controlSyncResult.hitCount}** file(s). **ANSWER NOW** from Manager/Service sites above.`);
|
|
4157
|
+
}
|
|
4158
|
+
if (kitUsageResult.section && (0, query_utils_1.shouldBuildKitModuleUsageSurvey)(query)) {
|
|
4159
|
+
return finishCompact((0, query_utils_1.queryAsksKitInstallDeps)(query)
|
|
4160
|
+
? 'Kit install/deps survey — **ANSWER NOW** from imports + oh-package lines above; do not Grep oh-package again.'
|
|
4161
|
+
: `Kit module **in-repo usage** — **${kitUsageResult.symbolCount}** imported symbol(s). **ANSWER NOW**; do not Grep the same \`@kit\` path.`);
|
|
4162
|
+
}
|
|
4163
|
+
if (moduleExportResult.section) {
|
|
4164
|
+
return finishCompact(`NAPI / native export survey — **${moduleExportResult.hitCount}** export(s). **ANSWER NOW**; do not Grep napi_define again.`);
|
|
4165
|
+
}
|
|
4166
|
+
if (dtsWrapResult.section) {
|
|
4167
|
+
return finishCompact(`\`.d.ts\` wrap call sites — **${dtsWrapResult.hitCount}** file(s). **ANSWER NOW**; do not Grep the wrap basename again.`);
|
|
4168
|
+
}
|
|
4169
|
+
if (moduleDepResult.hitCount > 0 || ((0, query_utils_1.queryAsModuleDependencySurvey)(query) && moduleDepResult.section)) {
|
|
4170
|
+
return finishCompact(`Module dependency / cycle survey — **ANSWER NOW** from the edges above; do not glob every oh-package.`);
|
|
4171
|
+
}
|
|
4172
|
+
// Data-source intent must stop even when edges=0 (empty still beats a 20k dump).
|
|
4173
|
+
if (dataSourceResult.section && (0, query_utils_1.queryAsDataSourceSurvey)(query)) {
|
|
4174
|
+
return finishCompact(dataSourceResult.edgeCount > 0
|
|
4175
|
+
? `Data-source survey — **${dataSourceResult.edgeCount}** upstream symbol(s). **ANSWER NOW** from system \`@ohos\`/\`@kit\` imports first.`
|
|
4176
|
+
: 'Data-source survey complete (0 service edges). **ANSWER NOW** from the note + Manager defs above; do not Grep broadly.');
|
|
4177
|
+
}
|
|
4178
|
+
if (apiUsageResult.fileCount > 0 && !dataSourceResult.section) {
|
|
4179
|
+
return finishCompact(`API usage survey — **${apiUsageResult.fileCount}** file(s). **ANSWER NOW** from the list above.`);
|
|
4180
|
+
}
|
|
4181
|
+
if ((0, query_utils_1.queryAsReturnValueConsumerSurvey)(query) && callerBulletCount > 0) {
|
|
4182
|
+
return finishCompact(`Return-value consumers — **${callerBulletCount}** caller site(s). **ANSWER NOW** from the caller inventory; do not Grep the same member.`);
|
|
4183
|
+
}
|
|
4184
|
+
// Return-value intent with empty graph callers — stop before SDK .d.ts dumps.
|
|
4185
|
+
if ((0, query_utils_1.queryAsReturnValueConsumerSurvey)(query) && callerSection) {
|
|
4186
|
+
return finishCompact('Return-value consumer survey complete. **ANSWER NOW** from the inventory above; do not open SDK `.d.ts` stubs.');
|
|
4187
|
+
}
|
|
4188
|
+
// Type + listed methods (Set/Test/Fill) — multi-anchor would block omit-source; stop here.
|
|
4189
|
+
if (listedTypeMethods && callerBulletCount > 0) {
|
|
4190
|
+
return finishCompact(`Caller inventory — **${callerBulletCount}** site(s) for listed methods. **ANSWER NOW**; do not fan out callers per method.`);
|
|
4191
|
+
}
|
|
3274
4192
|
const omitSource = (0, query_utils_1.shouldOmitSourceBodies)({
|
|
3275
4193
|
importSiteCount: importResult.siteCount,
|
|
3276
4194
|
hasFilteredImports: importInventoryFilter && importResult.siteCount > 0,
|
|
@@ -3282,36 +4200,41 @@ class ToolHandler {
|
|
|
3282
4200
|
inheritanceListed,
|
|
3283
4201
|
domainFileCount: domainFileResult.fileCount,
|
|
3284
4202
|
dataSourceEdgeCount: dataSourceResult.edgeCount,
|
|
3285
|
-
}, hasFlowPath, multiAnchor);
|
|
4203
|
+
}, hasFlowPath, multiAnchor) || hoverResult.hitCount > 0;
|
|
3286
4204
|
if (!omitSource)
|
|
3287
4205
|
return null;
|
|
3288
4206
|
if (configSection)
|
|
3289
|
-
return finishCompact('Config/manifest content above — answer from it directly.');
|
|
4207
|
+
return finishCompact('Config/manifest content above — answer from it directly. **ANSWER NOW.**');
|
|
4208
|
+
if (hoverResult.section) {
|
|
4209
|
+
return finishCompact(`Hover/pointer handler survey — **${hoverResult.hitCount}** site(s). **ANSWER NOW** from the list + snippets; do not Grep \`onHover\` again.`);
|
|
4210
|
+
}
|
|
3290
4211
|
if (kitUsageResult.section) {
|
|
3291
|
-
return finishCompact(`Kit module usage survey — **${kitUsageResult.symbolCount}** imported symbol(s)
|
|
4212
|
+
return finishCompact(`Kit module **in-repo usage** survey — **${kitUsageResult.symbolCount}** imported symbol(s). ` +
|
|
4213
|
+
'This is not an SDK catalog. **ANSWER NOW** from the usage list; do not Grep the same `@kit` path.');
|
|
3292
4214
|
}
|
|
3293
4215
|
if (domainFileResult.fileCount > 0) {
|
|
3294
4216
|
return finishCompact(`Domain file survey — **${domainFileResult.fileCount}** related file(s) listed above. ` +
|
|
3295
|
-
'This is the exhaustive related-file inventory; no glob/search needed.');
|
|
4217
|
+
'This is the exhaustive related-file inventory; **ANSWER NOW** — no glob/search needed.');
|
|
3296
4218
|
}
|
|
3297
4219
|
if (apiUsageResult.fileCount > 0) {
|
|
3298
|
-
return finishCompact(`API usage survey — **${apiUsageResult.fileCount}** file(s)
|
|
4220
|
+
return finishCompact(`API usage survey — **${apiUsageResult.fileCount}** file(s). **ANSWER NOW** from the list above.`);
|
|
3299
4221
|
}
|
|
3300
4222
|
if (dataSourceResult.edgeCount > 0) {
|
|
3301
|
-
return finishCompact(`Data-source survey — **${dataSourceResult.edgeCount}** upstream symbol(s)
|
|
4223
|
+
return finishCompact(`Data-source survey — **${dataSourceResult.edgeCount}** upstream symbol(s). **ANSWER NOW.**`);
|
|
4224
|
+
}
|
|
4225
|
+
if (inheritanceListed) {
|
|
4226
|
+
return finishCompact('Inheritance survey above lists all direct subtypes found. **ANSWER NOW** — do not grep `extends`.');
|
|
3302
4227
|
}
|
|
3303
|
-
if (inheritanceListed)
|
|
3304
|
-
return finishCompact('Inheritance survey above lists all direct subtypes found.');
|
|
3305
4228
|
if (importResult.compactListing) {
|
|
3306
|
-
return finishCompact(`Listed **${importResult.siteCount}** import site(s)
|
|
4229
|
+
return finishCompact(`Listed **${importResult.siteCount}** import site(s). **ANSWER NOW** from the dependency list.`);
|
|
3307
4230
|
}
|
|
3308
4231
|
if (callerBulletCount >= 1) {
|
|
3309
|
-
return finishCompact(`Caller inventory lists **${callerBulletCount}** call site(s)
|
|
4232
|
+
return finishCompact(`Caller inventory lists **${callerBulletCount}** call site(s). **ANSWER NOW** from the section above.`);
|
|
3310
4233
|
}
|
|
3311
4234
|
if (memberFileCount >= 2) {
|
|
3312
|
-
return finishCompact(`Member/pattern usage in **${memberFileCount}** file(s)
|
|
4235
|
+
return finishCompact(`Member/pattern usage in **${memberFileCount}** file(s). **ANSWER NOW** from the inventory above.`);
|
|
3313
4236
|
}
|
|
3314
|
-
return finishCompact('Inventory sections above are complete for this query.');
|
|
4237
|
+
return finishCompact('Inventory sections above are complete for this query. **ANSWER NOW.**');
|
|
3315
4238
|
}
|
|
3316
4239
|
/**
|
|
3317
4240
|
* Render a compact symbol-bounded slice of one file (lightweight mechanism path).
|
|
@@ -3359,6 +4282,8 @@ class ToolHandler {
|
|
|
3359
4282
|
const fileNodes = new Map();
|
|
3360
4283
|
const seedIds = new Set();
|
|
3361
4284
|
const addNode = (n) => {
|
|
4285
|
+
if ((0, arkts_1.isOhosApiFilePath)(n.filePath))
|
|
4286
|
+
return;
|
|
3362
4287
|
if (isTestPath(n.filePath))
|
|
3363
4288
|
return;
|
|
3364
4289
|
if (n.kind !== 'import' && !STRUCTURE_KINDS.has(n.kind))
|
|
@@ -3369,6 +4294,39 @@ class ToolHandler {
|
|
|
3369
4294
|
list.push(n);
|
|
3370
4295
|
fileNodes.set(n.filePath, list);
|
|
3371
4296
|
};
|
|
4297
|
+
const domainTermsEarly = (0, query_utils_1.extractDomainSearchTerms)(query);
|
|
4298
|
+
const distinctiveEarly = domainTermsEarly.filter((t) => /^[\x00-\x7F]+$/.test(t) && !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(t.toLowerCase()));
|
|
4299
|
+
// Distinctive-token IMPORT seeds first — `@ohos.convertxml` / `XmlParse*` before
|
|
4300
|
+
// structure FTS fills the budget with unrelated Parser* hits.
|
|
4301
|
+
for (const term of distinctiveEarly.slice(0, 4)) {
|
|
4302
|
+
const termLc = term.toLowerCase();
|
|
4303
|
+
let hits = [];
|
|
4304
|
+
try {
|
|
4305
|
+
hits = cg.searchNodes(term, { kinds: ['import'], limit: 40 });
|
|
4306
|
+
}
|
|
4307
|
+
catch {
|
|
4308
|
+
continue;
|
|
4309
|
+
}
|
|
4310
|
+
for (const r of hits) {
|
|
4311
|
+
if ((0, arkts_1.isOhosApiFilePath)(r.node.filePath))
|
|
4312
|
+
continue;
|
|
4313
|
+
const line = (0, query_utils_1.resolveImportLineFromNode)(r.node, projectRoot).toLowerCase();
|
|
4314
|
+
const nameLc = r.node.name.toLowerCase();
|
|
4315
|
+
const sigLc = (r.node.signature || '').toLowerCase();
|
|
4316
|
+
if (!nameLc.includes(termLc) && !line.includes(termLc) && !sigLc.includes(termLc))
|
|
4317
|
+
continue;
|
|
4318
|
+
addNode(r.node);
|
|
4319
|
+
try {
|
|
4320
|
+
for (const n of cg.getNodesInFile(r.node.filePath)) {
|
|
4321
|
+
if (STRUCTURE_KINDS.has(n.kind) && n.name.toLowerCase().includes(termLc))
|
|
4322
|
+
addNode(n);
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4325
|
+
catch { /* optional */ }
|
|
4326
|
+
if (seedIds.size >= 16)
|
|
4327
|
+
break;
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
3372
4330
|
for (const seed of (0, query_utils_1.extractMechanismEntrySeeds)(query)) {
|
|
3373
4331
|
if (seed.startsWith('@')) {
|
|
3374
4332
|
let hits = [];
|
|
@@ -3387,35 +4345,91 @@ class ToolHandler {
|
|
|
3387
4345
|
addNode(n);
|
|
3388
4346
|
}
|
|
3389
4347
|
}
|
|
3390
|
-
if (seedIds.size < 2) {
|
|
3391
|
-
const domainTerms =
|
|
3392
|
-
|
|
3393
|
-
|
|
4348
|
+
if (seedIds.size < 2 || distinctiveEarly.length > 0) {
|
|
4349
|
+
const domainTerms = domainTermsEarly;
|
|
4350
|
+
// Prefer distinctive ASCII (xml) then Chinese nouns — never seed on bare verbs
|
|
4351
|
+
// when a distinctive token exists (xml parse → convertxml, not ParseNotification*).
|
|
4352
|
+
const distinctive = distinctiveEarly;
|
|
4353
|
+
const ranked = distinctive.length > 0
|
|
4354
|
+
? [
|
|
4355
|
+
...distinctive,
|
|
4356
|
+
...domainTerms.filter((t) => !/^[\x00-\x7F]+$/.test(t) && t.length >= 2 && t.length <= 8),
|
|
4357
|
+
]
|
|
4358
|
+
: [
|
|
4359
|
+
...domainTerms.filter((t) => /^[\x00-\x7F]+$/.test(t) && !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(t.toLowerCase())),
|
|
4360
|
+
...domainTerms.filter((t) => !/^[\x00-\x7F]+$/.test(t) && t.length >= 2 && t.length <= 8),
|
|
4361
|
+
...domainTerms.filter((t) => /^[\x00-\x7F]+$/.test(t) && query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(t.toLowerCase())),
|
|
4362
|
+
];
|
|
4363
|
+
for (const term of ranked.slice(0, 10)) {
|
|
3394
4364
|
let hits = [];
|
|
3395
4365
|
try {
|
|
3396
4366
|
hits = cg.searchNodes(term, {
|
|
3397
4367
|
kinds: ['class', 'struct', 'interface', 'function', 'method', 'import'],
|
|
3398
|
-
limit:
|
|
4368
|
+
limit: 20,
|
|
3399
4369
|
});
|
|
3400
4370
|
}
|
|
3401
4371
|
catch {
|
|
3402
4372
|
continue;
|
|
3403
4373
|
}
|
|
4374
|
+
const termIsVerb = query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(term.toLowerCase());
|
|
4375
|
+
const termLc = term.toLowerCase();
|
|
3404
4376
|
for (const r of hits) {
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
const
|
|
3408
|
-
|
|
3409
|
-
|
|
4377
|
+
if ((0, arkts_1.isOhosApiFilePath)(r.node.filePath))
|
|
4378
|
+
continue;
|
|
4379
|
+
const nameLc = r.node.name.toLowerCase();
|
|
4380
|
+
const pathLc = r.node.filePath.toLowerCase();
|
|
4381
|
+
const sigLc = (r.node.signature || '').toLowerCase();
|
|
4382
|
+
const nameHit = nameLc.includes(termLc);
|
|
4383
|
+
const pathHit = pathLc.includes(termLc);
|
|
4384
|
+
const sigHit = sigLc.includes(termLc);
|
|
4385
|
+
// Verb-only hits need path affinity; imports for verbs are dropped when
|
|
4386
|
+
// distinctive tokens exist (handled by ranked list above).
|
|
4387
|
+
if (termIsVerb && !pathHit)
|
|
4388
|
+
continue;
|
|
4389
|
+
// Distinctive token: require name/path/signature to actually mention it
|
|
4390
|
+
// (drops FTS neighbors that only share an unrelated Parser suffix).
|
|
4391
|
+
if (!termIsVerb && distinctive.length > 0 && !nameHit && !pathHit && !sigHit)
|
|
4392
|
+
continue;
|
|
4393
|
+
const entryOk = r.node.kind === 'import'
|
|
4394
|
+
|| ((r.node.kind === 'function' || r.node.kind === 'method' || r.node.kind === 'class'
|
|
4395
|
+
|| r.node.kind === 'interface' || r.node.kind === 'struct')
|
|
4396
|
+
&& (nameHit || pathHit));
|
|
3410
4397
|
if (!entryOk)
|
|
3411
4398
|
continue;
|
|
3412
4399
|
addNode(r.node);
|
|
3413
|
-
if (seedIds.size >=
|
|
4400
|
+
if (seedIds.size >= 16)
|
|
3414
4401
|
break;
|
|
3415
4402
|
}
|
|
3416
|
-
if (seedIds.size >=
|
|
4403
|
+
if (seedIds.size >= 12)
|
|
3417
4404
|
break;
|
|
3418
4405
|
}
|
|
4406
|
+
// Prefer seeds that mention a distinctive token when we have any.
|
|
4407
|
+
if (distinctive.length > 0 && seedIds.size > 0) {
|
|
4408
|
+
const keep = new Set();
|
|
4409
|
+
for (const id of seedIds) {
|
|
4410
|
+
const n = cg.getNode(id);
|
|
4411
|
+
if (!n)
|
|
4412
|
+
continue;
|
|
4413
|
+
const line = n.kind === 'import' ? (0, query_utils_1.resolveImportLineFromNode)(n, projectRoot) : '';
|
|
4414
|
+
const blob = `${n.name}\n${n.filePath}\n${n.signature || ''}\n${line}`.toLowerCase();
|
|
4415
|
+
if (distinctive.some((t) => blob.includes(t.toLowerCase())))
|
|
4416
|
+
keep.add(id);
|
|
4417
|
+
}
|
|
4418
|
+
if (keep.size > 0) {
|
|
4419
|
+
for (const id of [...seedIds]) {
|
|
4420
|
+
if (!keep.has(id)) {
|
|
4421
|
+
seedIds.delete(id);
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
for (const [fp, nodes] of [...fileNodes.entries()]) {
|
|
4425
|
+
const filtered = nodes.filter((n) => keep.has(n.id));
|
|
4426
|
+
if (filtered.length === 0)
|
|
4427
|
+
fileNodes.delete(fp);
|
|
4428
|
+
else
|
|
4429
|
+
fileNodes.set(fp, filtered);
|
|
4430
|
+
}
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
3419
4433
|
}
|
|
3420
4434
|
if (seedIds.size === 0)
|
|
3421
4435
|
return null;
|
|
@@ -3424,18 +4438,109 @@ class ToolHandler {
|
|
|
3424
4438
|
const lines = [
|
|
3425
4439
|
`**Exploration: ${query}**`,
|
|
3426
4440
|
'',
|
|
4441
|
+
'> **ANSWER NOW from this response.** Do **not** Grep or Read listed files in parallel — Source below is authoritative.',
|
|
4442
|
+
'',
|
|
3427
4443
|
`Mechanism anchors: **${seedIds.size}** symbol(s) — lightweight explore (seed + flow spine).`,
|
|
3428
4444
|
'',
|
|
3429
4445
|
];
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
4446
|
+
// Import inventory: prefer concrete API seeds (convertxml / XmlParseUtil) over
|
|
4447
|
+
// bare domain tokens like `xml` that flood every path containing the substring.
|
|
4448
|
+
const seedImportFocus = (0, query_utils_1.extractMechanismEntrySeeds)(query).filter((s) => !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(s.toLowerCase())
|
|
4449
|
+
&& (s.length >= 6 || /convert|xml|parse|manager|service/i.test(s)));
|
|
4450
|
+
const distinctiveDeps = (0, query_utils_1.extractDependencySymbolsFromQuery)(query)
|
|
4451
|
+
.filter((s) => !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(s.toLowerCase()));
|
|
4452
|
+
const importQuery = (seedImportFocus.length > 0
|
|
4453
|
+
? seedImportFocus
|
|
4454
|
+
: distinctiveDeps.length > 0
|
|
4455
|
+
? distinctiveDeps
|
|
4456
|
+
: (0, query_utils_1.extractDomainSearchTerms)(query)
|
|
4457
|
+
.filter((t) => /^[\x00-\x7F]+$/.test(t) && !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(t.toLowerCase()))).slice(0, 4).join(' ');
|
|
4458
|
+
const importResult = importQuery
|
|
4459
|
+
? this.buildImportSitesSection(cg, importQuery, projectRoot)
|
|
4460
|
+
: { section: '', siteCount: 0, compactListing: false };
|
|
4461
|
+
// Light path: keep ≤8 import bullets (full inventory belongs to kit/API surveys).
|
|
4462
|
+
if (importResult.section) {
|
|
4463
|
+
const trimmed = this.trimLightImportSection(importResult.section, 8);
|
|
4464
|
+
if (trimmed)
|
|
4465
|
+
lines.push(trimmed);
|
|
4466
|
+
}
|
|
3433
4467
|
if (flow.text)
|
|
3434
4468
|
lines.push(flow.text);
|
|
4469
|
+
// Shared-lib + GLES/EGL thread: surface CMake link lines before bodies.
|
|
4470
|
+
if ((0, query_utils_1.queryAsNativeRenderThreadSurvey)(query)) {
|
|
4471
|
+
const cmakeHits = [];
|
|
4472
|
+
try {
|
|
4473
|
+
for (const f of cg.getFiles()) {
|
|
4474
|
+
const fp = f.path.replace(/\\/g, '/');
|
|
4475
|
+
if (!/CMakeLists\.txt$/i.test(fp))
|
|
4476
|
+
continue;
|
|
4477
|
+
if (!/foldeffect|effectrender|render|egl/i.test(fp) && (0, query_utils_1.extractPathSegmentsFromQuery)(query).length > 0) {
|
|
4478
|
+
const segs = (0, query_utils_1.extractPathSegmentsFromQuery)(query);
|
|
4479
|
+
if (!segs.some((s) => fp.toLowerCase().includes(s.toLowerCase().split('/').pop() || s))) {
|
|
4480
|
+
// still allow any CMake with GLESv3/EGL
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, f.path);
|
|
4484
|
+
if (!abs)
|
|
4485
|
+
continue;
|
|
4486
|
+
let content = '';
|
|
4487
|
+
try {
|
|
4488
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
4489
|
+
}
|
|
4490
|
+
catch {
|
|
4491
|
+
continue;
|
|
4492
|
+
}
|
|
4493
|
+
if (!/GLES|EGL|OpenGL/i.test(content))
|
|
4494
|
+
continue;
|
|
4495
|
+
const fileLines = content.split('\n');
|
|
4496
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
4497
|
+
const line = fileLines[i] ?? '';
|
|
4498
|
+
if (/GLES|EGL|OpenGL|target_link|pthread|thread/i.test(line)) {
|
|
4499
|
+
cmakeHits.push(`- \`${fp}:${i + 1}\` \`${line.trim().slice(0, 100)}\``);
|
|
4500
|
+
}
|
|
4501
|
+
}
|
|
4502
|
+
if (cmakeHits.length >= 12)
|
|
4503
|
+
break;
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
catch { /* optional */ }
|
|
4507
|
+
if (cmakeHits.length > 0) {
|
|
4508
|
+
lines.push('**CMake / link + GL libs**', '');
|
|
4509
|
+
lines.push(...cmakeHits.slice(0, 12));
|
|
4510
|
+
lines.push('');
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
const distinctiveForScore = (0, query_utils_1.extractDomainSearchTerms)(query).filter((t) => /^[\x00-\x7F]+$/.test(t) && !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(t.toLowerCase()));
|
|
4514
|
+
const apiSeeds = (0, query_utils_1.extractMechanismEntrySeeds)(query).map((s) => s.toLowerCase());
|
|
3435
4515
|
const fileScores = new Map();
|
|
3436
4516
|
for (const [fp, nodes] of fileNodes) {
|
|
4517
|
+
if ((0, arkts_1.isOhosApiFilePath)(fp))
|
|
4518
|
+
continue;
|
|
3437
4519
|
let score = 0;
|
|
4520
|
+
const fpLc = fp.toLowerCase();
|
|
4521
|
+
if (distinctiveForScore.some((t) => fpLc.includes(t.toLowerCase())))
|
|
4522
|
+
score += 30;
|
|
4523
|
+
// Prefer real wrappers (XmlParseUtil / convertxml) over incidental Xml* paths.
|
|
4524
|
+
if (apiSeeds.some((s) => s.length >= 6 && fpLc.includes(s)))
|
|
4525
|
+
score += 40;
|
|
4526
|
+
if (/convertxml|xmlparseutil/i.test(fpLc))
|
|
4527
|
+
score += 50;
|
|
3438
4528
|
for (const n of nodes) {
|
|
4529
|
+
const line = n.kind === 'import' ? (0, query_utils_1.resolveImportLineFromNode)(n, projectRoot) : '';
|
|
4530
|
+
const blob = `${n.name}\n${n.signature || ''}\n${line}`.toLowerCase();
|
|
4531
|
+
if (distinctiveForScore.some((t) => blob.includes(t.toLowerCase())))
|
|
4532
|
+
score += 20;
|
|
4533
|
+
if (apiSeeds.some((s) => s.length >= 6 && blob.includes(s)))
|
|
4534
|
+
score += 25;
|
|
4535
|
+
if (n.kind === 'import'
|
|
4536
|
+
&& distinctiveForScore.some((t) => {
|
|
4537
|
+
const tLc = t.toLowerCase();
|
|
4538
|
+
return (line.includes(tLc) || blob.includes(tLc)) && (line.includes('@ohos.') || line.includes('@kit.'));
|
|
4539
|
+
})) {
|
|
4540
|
+
score += 20;
|
|
4541
|
+
}
|
|
4542
|
+
if (/convertxml|@kit\.|@ohos\./i.test(line))
|
|
4543
|
+
score += 30;
|
|
3439
4544
|
if (flow.pathNodeIds.has(n.id))
|
|
3440
4545
|
score += 20;
|
|
3441
4546
|
else if (flow.uniqueNamedNodeIds.has(n.id))
|
|
@@ -3448,17 +4553,17 @@ class ToolHandler {
|
|
|
3448
4553
|
const sortedFiles = [...fileScores.entries()]
|
|
3449
4554
|
.filter(([, s]) => s > 0)
|
|
3450
4555
|
.sort((a, b) => b[1] - a[1])
|
|
3451
|
-
.slice(0,
|
|
4556
|
+
.slice(0, 3);
|
|
3452
4557
|
lines.push('**Source Code**', '');
|
|
3453
|
-
lines.push('> Line-numbered source below — treat as already Read. Do not grep/read these files again;
|
|
3454
|
-
'another `homegraph_explore` with more symbol names is cheaper if you need more.');
|
|
4558
|
+
lines.push('> Line-numbered source below — treat as already Read. Do **not** grep/read/`homegraph_node` these files again.');
|
|
3455
4559
|
lines.push('');
|
|
3456
4560
|
let totalChars = lines.join('\n').length;
|
|
3457
4561
|
let filesRendered = 0;
|
|
3458
4562
|
for (const [fp] of sortedFiles) {
|
|
3459
|
-
|
|
4563
|
+
// Token budget: light howto must stay ~3–5k so explore+answer can beat Grep/Read.
|
|
4564
|
+
if (filesRendered >= 2 || totalChars > 4_800)
|
|
3460
4565
|
break;
|
|
3461
|
-
const chunk = this.renderLightMechanismSource(projectRoot, fp, fileNodes.get(fp) ?? [],
|
|
4566
|
+
const chunk = this.renderLightMechanismSource(projectRoot, fp, fileNodes.get(fp) ?? [], 2000);
|
|
3462
4567
|
if (!chunk)
|
|
3463
4568
|
continue;
|
|
3464
4569
|
lines.push(chunk);
|
|
@@ -3468,11 +4573,35 @@ class ToolHandler {
|
|
|
3468
4573
|
if (filesRendered === 0 && !importResult.section && !flow.text)
|
|
3469
4574
|
return null;
|
|
3470
4575
|
lines.push('---');
|
|
3471
|
-
lines.push('> **Mechanism explore complete
|
|
3472
|
-
'
|
|
4576
|
+
lines.push('> **Mechanism explore complete — ANSWER NOW.** Sections above cover the wired mechanism. ' +
|
|
4577
|
+
'Do **not** call `homegraph_node`, Read, or Grep for symbols/files already shown (that multiplies tokens). ' +
|
|
4578
|
+
'Only one tighter `homegraph_explore` if a **named** symbol essential to the answer is still missing.');
|
|
3473
4579
|
lines.push('');
|
|
3474
4580
|
return this.textResult(lines.join('\n'));
|
|
3475
4581
|
}
|
|
4582
|
+
/** Cap import bullets inside a light-mechanism response. */
|
|
4583
|
+
trimLightImportSection(section, maxBullets) {
|
|
4584
|
+
const lines = section.split('\n');
|
|
4585
|
+
const out = [];
|
|
4586
|
+
let bullets = 0;
|
|
4587
|
+
let truncated = false;
|
|
4588
|
+
for (const line of lines) {
|
|
4589
|
+
if (line.startsWith('- `')) {
|
|
4590
|
+
if (bullets >= maxBullets) {
|
|
4591
|
+
truncated = true;
|
|
4592
|
+
continue;
|
|
4593
|
+
}
|
|
4594
|
+
bullets++;
|
|
4595
|
+
}
|
|
4596
|
+
if (/^\s*- … and \d+ more/i.test(line))
|
|
4597
|
+
continue;
|
|
4598
|
+
out.push(line);
|
|
4599
|
+
}
|
|
4600
|
+
if (truncated) {
|
|
4601
|
+
out.push(`- … (light-mechanism: showing ${maxBullets} sites — **ANSWER NOW**, do not Grep the same import)`);
|
|
4602
|
+
}
|
|
4603
|
+
return out.join('\n');
|
|
4604
|
+
}
|
|
3476
4605
|
/**
|
|
3477
4606
|
* Compact explore for local-symbol behavior questions — skips findRelevantContext
|
|
3478
4607
|
* and caps to 1–2 defining files (avoids the ~24K related-file dump).
|
|
@@ -3485,8 +4614,26 @@ class ToolHandler {
|
|
|
3485
4614
|
// Multi-anchor flow bags (routeSave/onSave, aboutToAppear/build, thunk→thunk)
|
|
3486
4615
|
// must fall through to full explore so Flow / Dynamic-dispatch / adaptive
|
|
3487
4616
|
// sizing still surface — compact trail is not a substitute.
|
|
4617
|
+
// Mechanism / domain-mechanism bags belong to light-mechanism (or full explore),
|
|
4618
|
+
// never compact — agents rewrite "如何实现 xml 解析" to "xml parse" and would
|
|
4619
|
+
// otherwise seed every method named `parse`.
|
|
4620
|
+
// Light-mechanism owns domain howtos. Bare "how/如何" NL must NOT veto compact
|
|
4621
|
+
// when a local/flag/lifecycle shape already owns the query (flag impact was
|
|
4622
|
+
// falling through to a 20k Dynamic-dispatch dump).
|
|
4623
|
+
if ((0, query_utils_1.shouldTryLightMechanismExplore)(query))
|
|
4624
|
+
return null;
|
|
4625
|
+
if ((0, query_utils_1.queryAsMechanismSurvey)(query)
|
|
4626
|
+
&& !(0, query_utils_1.queryAsLocalSymbolDetail)(query)
|
|
4627
|
+
&& !(0, query_utils_1.queryAsAssignedFlagImpactSurvey)(query)
|
|
4628
|
+
&& !(0, query_utils_1.queryAsTypeLifecycleSurvey)(query)) {
|
|
4629
|
+
return null;
|
|
4630
|
+
}
|
|
3488
4631
|
const bareId = /^[A-Za-z_][\w]*$/.test(query.trim());
|
|
3489
|
-
if (!(0, query_utils_1.queryAsLocalSymbolDetail)(query)
|
|
4632
|
+
if (!(0, query_utils_1.queryAsLocalSymbolDetail)(query)
|
|
4633
|
+
&& !(0, query_utils_1.queryAsFocusedUiCluster)(query)
|
|
4634
|
+
&& !(0, query_utils_1.queryAsComponentSurfaceSurvey)(query)
|
|
4635
|
+
&& !bareId
|
|
4636
|
+
&& !(0, query_utils_1.queryHasFocusedNamedAnchors)(query)) {
|
|
3490
4637
|
return null;
|
|
3491
4638
|
}
|
|
3492
4639
|
const STRUCTURE_KINDS = new Set([
|
|
@@ -3496,17 +4643,28 @@ class ToolHandler {
|
|
|
3496
4643
|
const names = (0, query_utils_1.extractLocalDetailAnchors)(query).slice(0, 4);
|
|
3497
4644
|
if (names.length === 0)
|
|
3498
4645
|
return null;
|
|
3499
|
-
// Multi-anchor bags that form a Flow / Dynamic-dispatch section
|
|
3500
|
-
// full explore
|
|
3501
|
-
//
|
|
3502
|
-
|
|
3503
|
-
|
|
4646
|
+
// Multi-anchor bags that form a Flow / Dynamic-dispatch section normally fall
|
|
4647
|
+
// through to full explore. EXCEPT focused UI Type clusters / local-detail /
|
|
4648
|
+
// Type.member — full explore is a 15–30k dump for a local composition question.
|
|
4649
|
+
const stayOnCompact = (0, query_utils_1.queryAsFocusedUiCluster)(query)
|
|
4650
|
+
|| (0, query_utils_1.queryAsComponentSurfaceSurvey)(query)
|
|
4651
|
+
|| (0, query_utils_1.queryAsNamedComponentAction)(query)
|
|
4652
|
+
|| (0, query_utils_1.queryHasNamedMemberFocus)(query)
|
|
4653
|
+
|| (0, query_utils_1.queryAsLocalSymbolDetail)(query);
|
|
3504
4654
|
const rawTokens = query.split(/[\s,()[\]]+/).filter((t) => t.length >= 3 && /^[A-Za-z_][\w]*$/.test(t));
|
|
4655
|
+
// Multi-anchor: prefer full explore when Flow / Dynamic-dispatch has content.
|
|
4656
|
+
// Type.member / UI-cluster bags stay on compact UNLESS the flow surfaces
|
|
4657
|
+
// @Prop/@Link state transfers — those hops are the answer for decorator
|
|
4658
|
+
// dependency questions and must not be buried under a compact UI trail.
|
|
3505
4659
|
if (names.length >= 2 || rawTokens.length >= 2) {
|
|
3506
4660
|
try {
|
|
3507
4661
|
const flow = this.buildFlowFromNamedSymbols(cg, query);
|
|
3508
|
-
if (flow.text.length > 0)
|
|
3509
|
-
|
|
4662
|
+
if (flow.text.length > 0) {
|
|
4663
|
+
if (!stayOnCompact)
|
|
4664
|
+
return null;
|
|
4665
|
+
if (/state: @(?:Prop|Link)\b/.test(flow.text))
|
|
4666
|
+
return null;
|
|
4667
|
+
}
|
|
3510
4668
|
}
|
|
3511
4669
|
catch {
|
|
3512
4670
|
// Probe is best-effort — stay on compact if flow build fails.
|
|
@@ -3525,9 +4683,95 @@ class ToolHandler {
|
|
|
3525
4683
|
list.push(n);
|
|
3526
4684
|
fileNodes.set(n.filePath, list);
|
|
3527
4685
|
};
|
|
4686
|
+
const typeNames = (0, query_utils_1.extractTypeNamesFromQuery)(query).filter((t) => !(0, query_utils_1.isFrameworkUiDecoratorName)(t));
|
|
4687
|
+
const typeNameSet = new Set(typeNames.map((t) => t.toLowerCase()));
|
|
4688
|
+
const bridge = (0, query_utils_1.queryNeedsCoNamedUseBridge)(query);
|
|
4689
|
+
const memberFocus = (0, query_utils_1.queryHasNamedMemberFocus)(query);
|
|
4690
|
+
const uiCluster = (0, query_utils_1.queryAsFocusedUiCluster)(query);
|
|
4691
|
+
const typeLifecycle = (0, query_utils_1.queryAsTypeLifecycleSurvey)(query);
|
|
4692
|
+
const componentSurface = (0, query_utils_1.queryAsComponentSurfaceSurvey)(query)
|
|
4693
|
+
|| uiCluster
|
|
4694
|
+
|| typeLifecycle
|
|
4695
|
+
|| names.some(query_utils_1.queryLooksLikeUiComponentType);
|
|
4696
|
+
// Page/Dialog surface & UI clusters: trail only — pulling callee files ballooned
|
|
4697
|
+
// ThemeHome-style answers by 5–10k without adding the UI-child / nav answer.
|
|
4698
|
+
const surfaceTrailOnly = (componentSurface || uiCluster || typeLifecycle) && !memberFocus
|
|
4699
|
+
&& !(0, query_utils_1.queryAsNamedComponentAction)(query);
|
|
4700
|
+
const bareMemberOnly = bareId && (0, query_utils_1.isMemberLikeIdentifier)(query.trim());
|
|
4701
|
+
const ownedByNamedType = (n) => {
|
|
4702
|
+
if (typeNameSet.size === 0)
|
|
4703
|
+
return true;
|
|
4704
|
+
const qn = (n.qualifiedName || '').toLowerCase();
|
|
4705
|
+
if ([...typeNameSet].some((t) => qn === t || qn.startsWith(`${t}.`) || qn.startsWith(`${t}::`) || qn.includes(`::${t}.`) || qn.endsWith(`::${t}`))) {
|
|
4706
|
+
return true;
|
|
4707
|
+
}
|
|
4708
|
+
// Same file as a named type definition.
|
|
4709
|
+
try {
|
|
4710
|
+
for (const t of typeNames) {
|
|
4711
|
+
for (const owner of cg.getNodesByName(t)) {
|
|
4712
|
+
if ((owner.kind === 'class' || owner.kind === 'struct' || owner.kind === 'interface' || owner.kind === 'component')
|
|
4713
|
+
&& owner.filePath === n.filePath) {
|
|
4714
|
+
return true;
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
catch { /* */ }
|
|
4720
|
+
return false;
|
|
4721
|
+
};
|
|
3528
4722
|
for (const seed of names) {
|
|
3529
|
-
|
|
4723
|
+
const candidates = cg.getNodesByName(seed);
|
|
4724
|
+
const preferOwned = memberFocus
|
|
4725
|
+
&& (0, query_utils_1.isMemberLikeIdentifier)(seed)
|
|
4726
|
+
&& typeNameSet.size > 0;
|
|
4727
|
+
// Prefer in-repo implementations over SDK `.d.ts` / @ohos API stubs when both exist.
|
|
4728
|
+
const projectFirst = [...candidates].sort((a, b) => {
|
|
4729
|
+
const score = (n) => {
|
|
4730
|
+
let s = 0;
|
|
4731
|
+
if ((0, arkts_1.isOhosApiFilePath)(n.filePath) || /\.d\.ts$/i.test(n.filePath))
|
|
4732
|
+
s -= 50;
|
|
4733
|
+
if ((0, query_utils_1.isTestFile)(n.filePath))
|
|
4734
|
+
s -= 20;
|
|
4735
|
+
if (n.kind === 'class' || n.kind === 'struct' || n.kind === 'component')
|
|
4736
|
+
s += 5;
|
|
4737
|
+
return s;
|
|
4738
|
+
};
|
|
4739
|
+
return score(b) - score(a);
|
|
4740
|
+
});
|
|
4741
|
+
const filtered = preferOwned
|
|
4742
|
+
? (() => {
|
|
4743
|
+
const owned = projectFirst.filter(ownedByNamedType);
|
|
4744
|
+
return owned.length > 0 ? owned : projectFirst;
|
|
4745
|
+
})()
|
|
4746
|
+
: projectFirst;
|
|
4747
|
+
for (const n of filtered) {
|
|
4748
|
+
// Skip SDK stubs when a project definition of the same name exists.
|
|
4749
|
+
if (((0, arkts_1.isOhosApiFilePath)(n.filePath) || /\.d\.ts$/i.test(n.filePath))
|
|
4750
|
+
&& filtered.some((o) => o.name === n.name
|
|
4751
|
+
&& !(0, arkts_1.isOhosApiFilePath)(o.filePath)
|
|
4752
|
+
&& !/\.d\.ts$/i.test(o.filePath))) {
|
|
4753
|
+
continue;
|
|
4754
|
+
}
|
|
3530
4755
|
addNode(n);
|
|
4756
|
+
// Page/Component surface digests need build/aboutToAppear as their own
|
|
4757
|
+
// nodes — otherwise we only have the outer struct and dump the whole body.
|
|
4758
|
+
if (componentSurface || uiCluster || typeLifecycle) {
|
|
4759
|
+
if (n.kind === 'component' || n.kind === 'class' || n.kind === 'struct') {
|
|
4760
|
+
try {
|
|
4761
|
+
for (const e of cg.getOutgoingEdges(n.id)) {
|
|
4762
|
+
if (e.kind !== 'contains')
|
|
4763
|
+
continue;
|
|
4764
|
+
const child = cg.getNode(e.target);
|
|
4765
|
+
if (child
|
|
4766
|
+
&& (child.kind === 'method' || child.kind === 'function')
|
|
4767
|
+
&& isUiSurfaceDigestMethod(child.name, uiCluster)) {
|
|
4768
|
+
addNode(child);
|
|
4769
|
+
}
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
catch { /* */ }
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
3531
4775
|
if (seedIds.size >= 12)
|
|
3532
4776
|
break;
|
|
3533
4777
|
}
|
|
@@ -3554,33 +4798,69 @@ class ToolHandler {
|
|
|
3554
4798
|
const nearSeed = (fp) => seedPaths.some((sp) => sp === fp || pathAffinity(sp, fp));
|
|
3555
4799
|
// Neighbor policy (agents often search("Foo") alone — do not dump 11 callees):
|
|
3556
4800
|
// - bareId / caller-bridge: callers of PRIMARY name only, no callee fan-out
|
|
3557
|
-
// - UI action / pinpoint local-detail: callers + callees
|
|
3558
|
-
|
|
3559
|
-
|
|
4801
|
+
// - Type.member / UI action / pinpoint local-detail: callers + callees
|
|
4802
|
+
// - Type.member: expand on the *member* (isExpired), not only the Type, and
|
|
4803
|
+
// do not path-filter callers — UI marking often lives in another package.
|
|
4804
|
+
const callersOnly = surfaceTrailOnly
|
|
4805
|
+
|| bareMemberOnly
|
|
4806
|
+
|| (bareId && !componentSurface)
|
|
3560
4807
|
|| bridge
|
|
3561
|
-
|| ((0, query_utils_1.shouldBuildCallerInventory)(query) && !(0, query_utils_1.queryAsNamedComponentAction)(query))
|
|
4808
|
+
|| ((0, query_utils_1.shouldBuildCallerInventory)(query) && !(0, query_utils_1.queryAsNamedComponentAction)(query) && !memberFocus && !componentSurface)
|
|
4809
|
+
// Conditional wiring ("失败时还会…") — method body + callers, not logInfo fan-out.
|
|
4810
|
+
|| /失败时|还会被注册|还会被|still\s+(?:be\s+)?(?:register|call)|if\s+.+\s+fails?/i.test(query);
|
|
3562
4811
|
const primaryName = names[0];
|
|
4812
|
+
const memberNames = new Set([
|
|
4813
|
+
...(0, query_utils_1.extractMemberAccessFromQuery)(query).map((m) => m.member),
|
|
4814
|
+
...names.filter((n) => (0, query_utils_1.isMemberLikeIdentifier)(n)),
|
|
4815
|
+
]);
|
|
4816
|
+
// UI Type clusters: expand every named *Page/*Dialog seed — not only the first
|
|
4817
|
+
// token — so Page↔Dialog composition stays in one compact answer.
|
|
4818
|
+
const expandNameSet = new Set(uiCluster || componentSurface
|
|
4819
|
+
? [...typeNames, primaryName]
|
|
4820
|
+
: [primaryName]);
|
|
3563
4821
|
const expandIds = [...seedIds].filter((id) => {
|
|
3564
4822
|
try {
|
|
3565
|
-
|
|
4823
|
+
const n = cg.getNode(id);
|
|
4824
|
+
if (!n)
|
|
4825
|
+
return false;
|
|
4826
|
+
if (memberFocus && memberNames.size > 0)
|
|
4827
|
+
return memberNames.has(n.name);
|
|
4828
|
+
return expandNameSet.has(n.name);
|
|
3566
4829
|
}
|
|
3567
4830
|
catch {
|
|
3568
4831
|
return false;
|
|
3569
4832
|
}
|
|
3570
4833
|
});
|
|
4834
|
+
// Fall back to primary type seeds if member name wasn't indexed as its own node.
|
|
4835
|
+
const expandIdsFinal = expandIds.length > 0
|
|
4836
|
+
? expandIds
|
|
4837
|
+
: [...seedIds].filter((id) => {
|
|
4838
|
+
try {
|
|
4839
|
+
return cg.getNode(id)?.name === primaryName;
|
|
4840
|
+
}
|
|
4841
|
+
catch {
|
|
4842
|
+
return false;
|
|
4843
|
+
}
|
|
4844
|
+
});
|
|
3571
4845
|
// Pull one-hop neighbors into the focus set so the answer is graph-complete
|
|
3572
4846
|
// without dumping an import inventory for isExpired/onClick.
|
|
3573
4847
|
const neighborIds = new Set();
|
|
3574
|
-
for (const id of
|
|
4848
|
+
for (const id of expandIdsFinal) {
|
|
3575
4849
|
let seedPath = '';
|
|
3576
4850
|
try {
|
|
3577
4851
|
seedPath = cg.getNode(id)?.filePath ?? '';
|
|
3578
4852
|
}
|
|
3579
4853
|
catch { /* */ }
|
|
3580
4854
|
try {
|
|
3581
|
-
for (const { node: c } of cg.getCallers(id).slice(0, callersOnly ? 8 :
|
|
3582
|
-
|
|
4855
|
+
for (const { node: c } of cg.getCallers(id).slice(0, callersOnly ? 8 : 12)) {
|
|
4856
|
+
// Type.member / UI-marking: keep cross-package callers (path filter drops them).
|
|
4857
|
+
if (!memberFocus
|
|
4858
|
+
&& !(0, query_utils_1.queryAsNamedComponentAction)(query)
|
|
4859
|
+
&& !nearSeed(c.filePath)
|
|
4860
|
+
&& seedPath
|
|
4861
|
+
&& !pathAffinity(seedPath, c.filePath)) {
|
|
3583
4862
|
continue;
|
|
4863
|
+
}
|
|
3584
4864
|
if (!isTestPath(c.filePath) || (0, query_utils_1.queryAsTestOnlyInterpretation)(query)) {
|
|
3585
4865
|
neighborIds.add(c.id);
|
|
3586
4866
|
// List callers in the trail, but don't pull their whole files into Source
|
|
@@ -3591,8 +4871,16 @@ class ToolHandler {
|
|
|
3591
4871
|
}
|
|
3592
4872
|
if (!callersOnly) {
|
|
3593
4873
|
for (const { node: c } of cg.getCallees(id).slice(0, 10)) {
|
|
3594
|
-
|
|
4874
|
+
// Skip log*/hilog helpers — Export→logInfo homonyms ballooned seeds.
|
|
4875
|
+
if (/^log(?:Info|Error|Warn|Debug|Fatal)?$/i.test(c.name) || /^hilog$/i.test(c.name)) {
|
|
3595
4876
|
continue;
|
|
4877
|
+
}
|
|
4878
|
+
if (!memberFocus
|
|
4879
|
+
&& !nearSeed(c.filePath)
|
|
4880
|
+
&& seedPath
|
|
4881
|
+
&& !pathAffinity(seedPath, c.filePath)) {
|
|
4882
|
+
continue;
|
|
4883
|
+
}
|
|
3596
4884
|
if (!isTestPath(c.filePath) || (0, query_utils_1.queryAsTestOnlyInterpretation)(query)) {
|
|
3597
4885
|
neighborIds.add(c.id);
|
|
3598
4886
|
addNode(c);
|
|
@@ -3607,6 +4895,8 @@ class ToolHandler {
|
|
|
3607
4895
|
const lines = [
|
|
3608
4896
|
`**Exploration: ${query}**`,
|
|
3609
4897
|
'',
|
|
4898
|
+
'> **ANSWER NOW from this response.** Do **not** Grep/Read/search/explore/node/callers for the same symbols — trail + Source below are authoritative.',
|
|
4899
|
+
'',
|
|
3610
4900
|
`Local-symbol focus: **${seedIds.size}** seed(s)` +
|
|
3611
4901
|
(neighborIds.size > 0 ? `, **${neighborIds.size}** caller/callee neighbor(s)` : '') +
|
|
3612
4902
|
' — compact explore (definition + edge trail, no related-file dump).',
|
|
@@ -3637,7 +4927,7 @@ class ToolHandler {
|
|
|
3637
4927
|
{
|
|
3638
4928
|
const trail = ['**Call / use trail**', ''];
|
|
3639
4929
|
let trailBullets = 0;
|
|
3640
|
-
const trailIds = (callersOnly ?
|
|
4930
|
+
const trailIds = (callersOnly ? expandIdsFinal : [...seedIds]).slice(0, 6);
|
|
3641
4931
|
for (const id of trailIds) {
|
|
3642
4932
|
let seedNode;
|
|
3643
4933
|
try {
|
|
@@ -3651,7 +4941,7 @@ class ToolHandler {
|
|
|
3651
4941
|
let callers = [];
|
|
3652
4942
|
let callees = [];
|
|
3653
4943
|
try {
|
|
3654
|
-
callers = cg.getCallers(id).slice(0, 8);
|
|
4944
|
+
callers = cg.getCallers(id).slice(0, memberFocus ? 12 : 8);
|
|
3655
4945
|
}
|
|
3656
4946
|
catch { /* */ }
|
|
3657
4947
|
if (!callersOnly) {
|
|
@@ -3660,8 +4950,10 @@ class ToolHandler {
|
|
|
3660
4950
|
}
|
|
3661
4951
|
catch { /* */ }
|
|
3662
4952
|
}
|
|
3663
|
-
|
|
3664
|
-
|
|
4953
|
+
if (!memberFocus) {
|
|
4954
|
+
callers = callers.filter((c) => nearSeed(c.node.filePath) || pathAffinity(seedNode.filePath, c.node.filePath));
|
|
4955
|
+
callees = callees.filter((c) => nearSeed(c.node.filePath) || pathAffinity(seedNode.filePath, c.node.filePath));
|
|
4956
|
+
}
|
|
3665
4957
|
if (callers.length === 0 && callees.length === 0)
|
|
3666
4958
|
continue;
|
|
3667
4959
|
trail.push(`- \`${seedNode.name}\` (${seedNode.kind}) — ${seedNode.filePath}:${seedNode.startLine}`);
|
|
@@ -3678,7 +4970,7 @@ class ToolHandler {
|
|
|
3678
4970
|
}
|
|
3679
4971
|
if (trailBullets > 0) {
|
|
3680
4972
|
trail.push('');
|
|
3681
|
-
trail.push('>
|
|
4973
|
+
trail.push('> **ANSWER NOW** from this trail + Source below. Do not grep/search/read the same symbols again.');
|
|
3682
4974
|
trail.push('');
|
|
3683
4975
|
lines.push(...trail);
|
|
3684
4976
|
}
|
|
@@ -3689,15 +4981,20 @@ class ToolHandler {
|
|
|
3689
4981
|
const textUsageHits = [];
|
|
3690
4982
|
// Also scan when the agent asks upstream/registration — callee neighbors
|
|
3691
4983
|
// alone do not show Export / OH_NativeXComponent wiring sites.
|
|
3692
|
-
const needUsageScan =
|
|
3693
|
-
|
|
3694
|
-
|
|
4984
|
+
const needUsageScan = !componentSurface
|
|
4985
|
+
&& (neighborIds.size === 0
|
|
4986
|
+
|| memberFocus
|
|
4987
|
+
|| /上下游|上游|下游|注册|挂到|callback|upstream|downstream/i.test(query)
|
|
4988
|
+
|| names.every((n) => /(?:CB|Callback)$/i.test(n)));
|
|
3695
4989
|
if (needUsageScan) {
|
|
3696
4990
|
// Scan ALL focused anchors (incl. PascalCase callables) — previously only
|
|
3697
4991
|
// lowercase names were scanned, so OnSurfaceChangedCB registration sites
|
|
3698
4992
|
// never surfaced and agents fell through to callers/grep/read.
|
|
3699
4993
|
const typeSeeds = (0, query_utils_1.extractTypeNamesFromQuery)(query);
|
|
3700
|
-
|
|
4994
|
+
// Prefer member-like names for Type.member UI questions (isExpired over Type).
|
|
4995
|
+
const scanNames = memberFocus && memberNames.size > 0
|
|
4996
|
+
? [...memberNames].slice(0, 3)
|
|
4997
|
+
: names.slice(0, 3);
|
|
3701
4998
|
if (scanNames.length > 0) {
|
|
3702
4999
|
try {
|
|
3703
5000
|
const defLineKeys = new Set();
|
|
@@ -3707,57 +5004,90 @@ class ToolHandler {
|
|
|
3707
5004
|
defLineKeys.add(`${n.filePath}:${n.startLine}`);
|
|
3708
5005
|
}
|
|
3709
5006
|
}
|
|
3710
|
-
const
|
|
5007
|
+
const norm = (fp) => fp.replace(/\\/g, '/');
|
|
5008
|
+
const defDirs = new Set([...fileNodes.keys()].map((fp) => norm(fp).split('/').slice(0, 4).join('/')));
|
|
5009
|
+
// Large monorepos: never rely on getFiles() order + hard slice — Type.member
|
|
5010
|
+
// call sites (LauncherCardInfo.isExpired filters) live near the defining
|
|
5011
|
+
// package and must be scanned first or Grep wins.
|
|
5012
|
+
const rankScanFile = (fp) => {
|
|
5013
|
+
const n = norm(fp);
|
|
5014
|
+
let score = 0;
|
|
5015
|
+
if ([...defDirs].some((d) => n === d || n.startsWith(`${d}/`)))
|
|
5016
|
+
score += 100;
|
|
5017
|
+
if (typeSeeds.some((t) => n.toLowerCase().includes(t.toLowerCase())))
|
|
5018
|
+
score += 40;
|
|
5019
|
+
return score;
|
|
5020
|
+
};
|
|
3711
5021
|
const files = cg.getFiles()
|
|
3712
5022
|
.map((f) => f.path)
|
|
3713
|
-
.filter((fp) =>
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
const
|
|
3746
|
-
for (
|
|
3747
|
-
const
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
5023
|
+
.filter((fp) => !isTestPath(fp))
|
|
5024
|
+
.map((fp) => ({ fp, score: rankScanFile(fp) }))
|
|
5025
|
+
.sort((a, b) => b.score - a.score || a.fp.localeCompare(b.fp))
|
|
5026
|
+
.map((x) => x.fp);
|
|
5027
|
+
const hitCap = bareMemberOnly ? 6 : memberFocus ? 10 : 8;
|
|
5028
|
+
const scanFileList = (list, requireTypeMention) => {
|
|
5029
|
+
for (const fp of list) {
|
|
5030
|
+
if (textUsageHits.length >= hitCap)
|
|
5031
|
+
break;
|
|
5032
|
+
const absPath = (0, utils_1.validatePathWithinRoot)(projectRoot, fp);
|
|
5033
|
+
if (!absPath || !(0, fs_1.existsSync)(absPath))
|
|
5034
|
+
continue;
|
|
5035
|
+
let content;
|
|
5036
|
+
try {
|
|
5037
|
+
content = (0, fs_1.readFileSync)(absPath, 'utf-8');
|
|
5038
|
+
}
|
|
5039
|
+
catch {
|
|
5040
|
+
continue;
|
|
5041
|
+
}
|
|
5042
|
+
if (requireTypeMention
|
|
5043
|
+
&& typeSeeds.length > 0
|
|
5044
|
+
&& !typeSeeds.some((t) => content.includes(t))) {
|
|
5045
|
+
continue;
|
|
5046
|
+
}
|
|
5047
|
+
// Prefer files that also mention a co-named type when both present
|
|
5048
|
+
// (skip for single-anchor upstream/registration scans and Type.member).
|
|
5049
|
+
if (!memberFocus
|
|
5050
|
+
&& typeSeeds.length > 1
|
|
5051
|
+
&& names.length > 1
|
|
5052
|
+
&& !typeSeeds.some((t) => content.includes(t))) {
|
|
5053
|
+
continue;
|
|
5054
|
+
}
|
|
5055
|
+
const fileLines = content.split('\n');
|
|
5056
|
+
for (const sym of scanNames) {
|
|
5057
|
+
const escaped = sym.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
5058
|
+
// Match call OR bare identifier use; prefer Type.member when typed.
|
|
5059
|
+
const typeAlt = typeSeeds.length > 0
|
|
5060
|
+
? `(?:\\b(?:${typeSeeds.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})\\.)?`
|
|
5061
|
+
: '';
|
|
5062
|
+
const re = new RegExp(`${typeAlt}\\b${escaped}\\b`);
|
|
5063
|
+
const defRe = new RegExp(`^\\s*(?:export\\s+|public\\s+|private\\s+|protected\\s+|static\\s+|async\\s+)*${escaped}\\s*\\(`);
|
|
5064
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
5065
|
+
const lineText = fileLines[i] ?? '';
|
|
5066
|
+
if (!re.test(lineText))
|
|
5067
|
+
continue;
|
|
5068
|
+
if (defLineKeys.has(`${fp}:${i + 1}`))
|
|
5069
|
+
continue;
|
|
5070
|
+
// Skip the method's own definition line (no Type. prefix / no call punctuation).
|
|
5071
|
+
if (defRe.test(lineText) && !lineText.includes(`.${sym}`))
|
|
5072
|
+
continue;
|
|
5073
|
+
textUsageHits.push({ filePath: fp, line: i + 1, text: lineText.trim(), symbol: sym });
|
|
5074
|
+
if (!fileNodes.has(fp))
|
|
5075
|
+
fileNodes.set(fp, []);
|
|
5076
|
+
if (textUsageHits.length >= hitCap)
|
|
5077
|
+
break;
|
|
5078
|
+
}
|
|
5079
|
+
if (textUsageHits.length >= hitCap)
|
|
3756
5080
|
break;
|
|
3757
5081
|
}
|
|
3758
|
-
if (textUsageHits.length >= 12)
|
|
3759
|
-
break;
|
|
3760
5082
|
}
|
|
5083
|
+
};
|
|
5084
|
+
// Pass 1: same package / type-path affinity (score > 0), require Type mention when known.
|
|
5085
|
+
const nearFiles = files.filter((fp) => rankScanFile(fp) > 0).slice(0, memberFocus ? 80 : 40);
|
|
5086
|
+
scanFileList(nearFiles, memberFocus && typeSeeds.length > 0);
|
|
5087
|
+
// Pass 2: broader if still empty / thin (still bounded).
|
|
5088
|
+
if (textUsageHits.length < 3) {
|
|
5089
|
+
const rest = files.filter((fp) => rankScanFile(fp) === 0).slice(0, memberFocus ? 160 : 80);
|
|
5090
|
+
scanFileList(rest, false);
|
|
3761
5091
|
}
|
|
3762
5092
|
}
|
|
3763
5093
|
catch {
|
|
@@ -3770,9 +5100,14 @@ class ToolHandler {
|
|
|
3770
5100
|
lines.push(`- \`${h.symbol}\` — ${h.filePath}:${h.line} \`${h.text.slice(0, 120)}\``);
|
|
3771
5101
|
}
|
|
3772
5102
|
lines.push('');
|
|
3773
|
-
lines.push(
|
|
5103
|
+
lines.push(memberFocus
|
|
5104
|
+
? '> These lines are the in-repo **call/filter sites** for the named member. **ANSWER NOW** from them + Source — do not Grep/`homegraph_callers` the same member.'
|
|
5105
|
+
: '> These lines are the in-repo use sites. Answer from them + Source; do not grep the same names again.');
|
|
3774
5106
|
lines.push('');
|
|
3775
5107
|
}
|
|
5108
|
+
else if (memberFocus) {
|
|
5109
|
+
lines.push('> No in-repo text call sites found for this member beyond its definition. Answer from Source; avoid a broad Grep unless needed.', '');
|
|
5110
|
+
}
|
|
3776
5111
|
}
|
|
3777
5112
|
// Also surface #include / import lines that make a co-named type visible
|
|
3778
5113
|
// near the primary symbol's use sites (IntGrid ↔ SortWidgets shapes).
|
|
@@ -3827,39 +5162,72 @@ class ToolHandler {
|
|
|
3827
5162
|
}
|
|
3828
5163
|
}
|
|
3829
5164
|
const callerSection = (0, query_utils_1.shouldBuildCallerInventory)(query)
|
|
3830
|
-
? this.buildCallerListingSection(cg, query)
|
|
5165
|
+
? this.buildCallerListingSection(cg, query, projectRoot)
|
|
3831
5166
|
: '';
|
|
3832
5167
|
if (callerSection)
|
|
3833
5168
|
lines.push(callerSection);
|
|
3834
5169
|
// Prefer files whose nodes exactly match query names; then neighbor / usage files.
|
|
3835
5170
|
// Primary named symbol (first anchor) always ranks first — secondary types like
|
|
3836
5171
|
// IntGrid used to drown SortWidgets' defining .cpp via higher exact-count on the header.
|
|
5172
|
+
// UI clusters: co-occurrence of 2+ named Types beats a lonely homonym elsewhere.
|
|
3837
5173
|
const nameSet = new Set(names);
|
|
3838
5174
|
const usageFiles = new Set(textUsageHits.map((h) => h.filePath));
|
|
3839
|
-
const maxFiles =
|
|
3840
|
-
?
|
|
3841
|
-
:
|
|
5175
|
+
const maxFiles = bareMemberOnly
|
|
5176
|
+
? 2
|
|
5177
|
+
: memberFocus
|
|
5178
|
+
// Call/filter sites already listed above — dumping caller file bodies
|
|
5179
|
+
// (FormStack…) burns tokens and invites a follow-up Read of isExpired.
|
|
5180
|
+
? 1
|
|
5181
|
+
: ((0, query_utils_1.queryAsNamedComponentAction)(query) || componentSurface || usageFiles.size > 0 || bridge)
|
|
5182
|
+
? (uiCluster ? 2 : (componentSurface ? 2 : 3))
|
|
5183
|
+
: 2;
|
|
3842
5184
|
const ranked = [...fileNodes.entries()]
|
|
5185
|
+
.filter(([, nodes]) => {
|
|
5186
|
+
if (!memberFocus || memberNames.size === 0)
|
|
5187
|
+
return true;
|
|
5188
|
+
return nodes.some((n) => memberNames.has(n.name) || typeNameSet.has(n.name.toLowerCase()));
|
|
5189
|
+
})
|
|
3843
5190
|
.map(([fp, nodes]) => {
|
|
3844
5191
|
const exact = nodes.filter((n) => nameSet.has(n.name)).length;
|
|
5192
|
+
const typeHits = typeNames.filter((t) => nodes.some((n) => n.name === t)).length;
|
|
5193
|
+
const clusterCoOccur = typeHits >= 2 ? 250 : typeHits === 1 ? 80 : 0;
|
|
3845
5194
|
const primaryHit = nodes.some((n) => n.name === primaryName) ? 100 : 0;
|
|
3846
5195
|
const neighborHit = nodes.filter((n) => neighborIds.has(n.id)).length;
|
|
3847
|
-
const usageHit = usageFiles.has(fp) ? 15 : 0;
|
|
5196
|
+
const usageHit = usageFiles.has(fp) ? (memberFocus ? 60 : 15) : 0;
|
|
3848
5197
|
return {
|
|
3849
5198
|
fp,
|
|
3850
5199
|
nodes,
|
|
3851
5200
|
exact,
|
|
3852
|
-
score:
|
|
5201
|
+
score: (uiCluster || componentSurface ? clusterCoOccur : 0)
|
|
5202
|
+
+ primaryHit
|
|
5203
|
+
+ exact * 20
|
|
5204
|
+
+ neighborHit * 5
|
|
5205
|
+
+ usageHit
|
|
5206
|
+
+ nodes.length,
|
|
3853
5207
|
};
|
|
3854
5208
|
})
|
|
3855
5209
|
.sort((a, b) => b.score - a.score || a.fp.localeCompare(b.fp))
|
|
3856
5210
|
.slice(0, maxFiles);
|
|
3857
5211
|
lines.push('**Source Code**', '');
|
|
3858
|
-
lines.push('> Line-numbered source — treat as already Read.
|
|
5212
|
+
lines.push('> Line-numbered source — treat as already Read. **ANSWER NOW** — do not Read/Grep/search/explore/node the same symbols again.');
|
|
3859
5213
|
lines.push('');
|
|
3860
5214
|
let totalChars = lines.join('\n').length;
|
|
3861
|
-
|
|
3862
|
-
|
|
5215
|
+
// Lean ceilings: without-HG Grep/Read stacks were ~15k session tokens; a 10–14k
|
|
5216
|
+
// compact body + follow-up Read is what loses the token A/B.
|
|
5217
|
+
const maxTotal = bareMemberOnly
|
|
5218
|
+
? 4500
|
|
5219
|
+
: componentSurface || uiCluster
|
|
5220
|
+
? 7000
|
|
5221
|
+
: memberFocus && usageFiles.size > 0
|
|
5222
|
+
? 6500
|
|
5223
|
+
: 5500;
|
|
5224
|
+
const maxPerFile = bareMemberOnly
|
|
5225
|
+
? 2800
|
|
5226
|
+
: componentSurface || uiCluster
|
|
5227
|
+
? 4500
|
|
5228
|
+
: memberFocus
|
|
5229
|
+
? 2800
|
|
5230
|
+
: 3200;
|
|
3863
5231
|
let rendered = 0;
|
|
3864
5232
|
for (const { fp, nodes } of ranked) {
|
|
3865
5233
|
if (rendered >= maxFiles || totalChars > maxTotal)
|
|
@@ -3883,23 +5251,114 @@ class ToolHandler {
|
|
|
3883
5251
|
let start = 1;
|
|
3884
5252
|
let end = Math.min(fileLines.length, 120);
|
|
3885
5253
|
const usageLine = textUsageHits.find((h) => h.filePath === fp)?.line;
|
|
5254
|
+
const surfaceMethodsRaw = (componentSurface || uiCluster)
|
|
5255
|
+
? focusNodes
|
|
5256
|
+
.filter((n) => (n.kind === 'method' || n.kind === 'function') && isUiSurfaceDigestMethod(n.name, uiCluster))
|
|
5257
|
+
.sort((a, b) => surfaceMethodPriority(a.name) - surfaceMethodPriority(b.name) || a.startLine - b.startLine)
|
|
5258
|
+
: [];
|
|
5259
|
+
// When build/PageMap answer the surface question, skip lifecycle — it ate
|
|
5260
|
+
// the budget and agents re-Read PageMap anyway.
|
|
5261
|
+
const hasBuildOrPageMap = surfaceMethodsRaw.some((n) => /^(build|PageMap)$/i.test(n.name));
|
|
5262
|
+
const surfaceMethods = hasBuildOrPageMap
|
|
5263
|
+
? surfaceMethodsRaw.filter((n) => !/^(aboutToAppear|aboutToDisappear)$/i.test(n.name))
|
|
5264
|
+
: surfaceMethodsRaw;
|
|
5265
|
+
// Type.member: keep the member body, not the whole owning class dump.
|
|
5266
|
+
const memberOnlyNodes = memberFocus && memberNames.size > 0
|
|
5267
|
+
? focusNodes.filter((n) => memberNames.has(n.name) && (n.kind === 'method' || n.kind === 'function' || n.kind === 'property' || n.kind === 'field'))
|
|
5268
|
+
: [];
|
|
3886
5269
|
if (focusNodes.length > 0) {
|
|
3887
5270
|
const named = focusNodes.filter((n) => nameSet.has(n.name));
|
|
3888
5271
|
const use = named.length > 0 ? named : focusNodes.slice(0, 3);
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
5272
|
+
const surfaceNodes = componentSurface
|
|
5273
|
+
? use.filter((n) => (n.kind === 'component' || n.kind === 'class' || n.kind === 'struct'
|
|
5274
|
+
|| (0, query_utils_1.queryLooksLikeUiComponentType)(n.name))
|
|
5275
|
+
&& (typeNameSet.size === 0 || typeNameSet.has(n.name.toLowerCase()) || nameSet.has(n.name)))
|
|
5276
|
+
: [];
|
|
5277
|
+
if (surfaceNodes.length === 0 && componentSurface) {
|
|
5278
|
+
const one = use.find((n) => n.kind === 'component' || n.kind === 'class' || n.kind === 'struct'
|
|
5279
|
+
|| (0, query_utils_1.queryLooksLikeUiComponentType)(n.name)) ?? use[0];
|
|
5280
|
+
if (one)
|
|
5281
|
+
surfaceNodes.push(one);
|
|
5282
|
+
}
|
|
5283
|
+
if (memberOnlyNodes.length > 0) {
|
|
5284
|
+
start = Math.max(1, Math.min(...memberOnlyNodes.map((n) => n.startLine)) - 2);
|
|
5285
|
+
end = Math.min(fileLines.length, Math.max(...memberOnlyNodes.map((n) => n.endLine)) + 2);
|
|
5286
|
+
}
|
|
5287
|
+
else if (surfaceMethods.length > 0) {
|
|
5288
|
+
// Per-method chunks below — placeholder window unused.
|
|
5289
|
+
start = 1;
|
|
5290
|
+
end = 1;
|
|
5291
|
+
}
|
|
5292
|
+
else if (surfaceNodes.length > 0 && surfaceNodes.some((n) => n.endLine > n.startLine)) {
|
|
5293
|
+
start = Math.max(1, Math.min(...surfaceNodes.map((n) => n.startLine)) - 2);
|
|
5294
|
+
end = Math.min(fileLines.length, Math.max(...surfaceNodes.map((n) => n.endLine)) + 2);
|
|
5295
|
+
}
|
|
5296
|
+
else {
|
|
5297
|
+
start = Math.max(1, Math.min(...use.map((n) => n.startLine)) - 2);
|
|
5298
|
+
end = Math.min(fileLines.length, Math.max(...use.map((n) => n.endLine)) + 2);
|
|
5299
|
+
}
|
|
5300
|
+
// Keep window bounded (non-surface paths)
|
|
5301
|
+
if (surfaceMethods.length === 0 && (end - start + 1) * 40 > maxPerFile) {
|
|
3893
5302
|
end = Math.min(fileLines.length, start + Math.floor(maxPerFile / 40) - 1);
|
|
3894
5303
|
}
|
|
3895
5304
|
}
|
|
3896
5305
|
else if (usageLine) {
|
|
3897
|
-
start = Math.max(1, usageLine -
|
|
3898
|
-
end = Math.min(fileLines.length, usageLine +
|
|
5306
|
+
start = Math.max(1, usageLine - 10);
|
|
5307
|
+
end = Math.min(fileLines.length, usageLine + 18);
|
|
3899
5308
|
}
|
|
3900
5309
|
const header = fileSectionHeader(fp, focusNodes.map((n) => `${n.name}(${n.kind})`).slice(0, 6).join(', '));
|
|
3901
5310
|
const withLineNumbers = exploreLineNumbersEnabled();
|
|
3902
5311
|
const bodyLines = [header, '```' + (nodes[0]?.language || ''), ''];
|
|
5312
|
+
if (surfaceMethods.length > 0) {
|
|
5313
|
+
// Do NOT span aboutToAppear→build as one window — ThemeHome-sized pages
|
|
5314
|
+
// always trim before PageMap/build. Emit imports + each method separately,
|
|
5315
|
+
// prioritizing build/PageMap/preview.
|
|
5316
|
+
let lastImportLine = 0;
|
|
5317
|
+
for (let i = 0; i < Math.min(fileLines.length, 60); i++) {
|
|
5318
|
+
if (/^\s*import\s+/.test(fileLines[i] ?? ''))
|
|
5319
|
+
lastImportLine = i + 1;
|
|
5320
|
+
}
|
|
5321
|
+
const inventory = extractUiSurfaceInventory(fileLines, surfaceMethods);
|
|
5322
|
+
// Inventory is prepended outside the code fence below via chunk assembly.
|
|
5323
|
+
const methodBudget = Math.max(900, Math.floor((maxPerFile - Math.min(lastImportLine, 40) * 40) / Math.max(1, Math.min(surfaceMethods.length, 4))));
|
|
5324
|
+
if (lastImportLine > 0) {
|
|
5325
|
+
const importCap = Math.min(lastImportLine, 45);
|
|
5326
|
+
for (let i = 1; i <= importCap; i++) {
|
|
5327
|
+
const code = fileLines[i - 1] ?? '';
|
|
5328
|
+
bodyLines.push(withLineNumbers ? `${i}\t${code}` : code);
|
|
5329
|
+
}
|
|
5330
|
+
bodyLines.push(withLineNumbers ? `…\t// …` : '// …');
|
|
5331
|
+
}
|
|
5332
|
+
let methodsEmitted = 0;
|
|
5333
|
+
for (const m of surfaceMethods) {
|
|
5334
|
+
if (methodsEmitted >= 5)
|
|
5335
|
+
break;
|
|
5336
|
+
const mStart = Math.max(1, m.startLine - 1);
|
|
5337
|
+
let mEnd = Math.min(fileLines.length, m.endLine + 1);
|
|
5338
|
+
// Cap each method so later build/PageMap still fit.
|
|
5339
|
+
const maxLines = Math.max(25, Math.floor(methodBudget / 40));
|
|
5340
|
+
if (mEnd - mStart + 1 > maxLines) {
|
|
5341
|
+
mEnd = mStart + maxLines - 1;
|
|
5342
|
+
}
|
|
5343
|
+
bodyLines.push(withLineNumbers ? `…\t// --- ${m.name} ---` : `// --- ${m.name} ---`);
|
|
5344
|
+
for (let i = mStart; i <= mEnd; i++) {
|
|
5345
|
+
const code = fileLines[i - 1] ?? '';
|
|
5346
|
+
bodyLines.push(withLineNumbers ? `${i}\t${code}` : code);
|
|
5347
|
+
}
|
|
5348
|
+
methodsEmitted++;
|
|
5349
|
+
}
|
|
5350
|
+
bodyLines.push('```', '');
|
|
5351
|
+
let chunk = (inventory ? inventory + '\n' : '') + bodyLines.join('\n');
|
|
5352
|
+
if (chunk.length > maxPerFile + (inventory ? 1200 : 0)) {
|
|
5353
|
+
chunk = `${chunk.slice(0, maxPerFile + (inventory ? 1200 : 0))}\n... (trimmed — answer from inventory + visible methods; do not Read the rest)\n\`\`\`\n`;
|
|
5354
|
+
}
|
|
5355
|
+
if (totalChars + chunk.length > maxTotal && rendered > 0)
|
|
5356
|
+
break;
|
|
5357
|
+
lines.push(chunk);
|
|
5358
|
+
totalChars += chunk.length;
|
|
5359
|
+
rendered++;
|
|
5360
|
+
continue;
|
|
5361
|
+
}
|
|
3903
5362
|
for (let i = start; i <= end; i++) {
|
|
3904
5363
|
const code = fileLines[i - 1] ?? '';
|
|
3905
5364
|
bodyLines.push(withLineNumbers ? `${i}\t${code}` : code);
|
|
@@ -3907,7 +5366,7 @@ class ToolHandler {
|
|
|
3907
5366
|
bodyLines.push('```', '');
|
|
3908
5367
|
let chunk = bodyLines.join('\n');
|
|
3909
5368
|
if (chunk.length > maxPerFile) {
|
|
3910
|
-
chunk = `${chunk.slice(0, maxPerFile)}\n... (trimmed)\n\`\`\`\n`;
|
|
5369
|
+
chunk = `${chunk.slice(0, maxPerFile)}\n... (trimmed — answer from visible lines; do not Read the rest)\n\`\`\`\n`;
|
|
3911
5370
|
}
|
|
3912
5371
|
if (totalChars + chunk.length > maxTotal && rendered > 0)
|
|
3913
5372
|
break;
|
|
@@ -3918,11 +5377,100 @@ class ToolHandler {
|
|
|
3918
5377
|
if (rendered === 0)
|
|
3919
5378
|
return null;
|
|
3920
5379
|
lines.push('---');
|
|
3921
|
-
lines.push('> **Compact local explore complete
|
|
3922
|
-
'Do not
|
|
5380
|
+
lines.push('> **Compact local explore complete — ANSWER NOW.** ' +
|
|
5381
|
+
'Do **not** Read/Grep/`homegraph_search`/`homegraph_explore`/`homegraph_node` for the same symbols.');
|
|
3923
5382
|
lines.push('');
|
|
3924
5383
|
return this.textResult(lines.join('\n'));
|
|
3925
5384
|
}
|
|
5385
|
+
/**
|
|
5386
|
+
* Hover / pointer-handler inventory — onHover / Hover* symbols + short snippets.
|
|
5387
|
+
* Used when the agent asks about hover effects without naming a Type yet.
|
|
5388
|
+
*/
|
|
5389
|
+
buildHoverHandlerSurveySection(cg, query, projectRoot) {
|
|
5390
|
+
const topicBoost = /图标|icon|appicon|dock|launcher/i.test(query);
|
|
5391
|
+
const searchTerms = ['onHover', 'HoverAnimation', 'HoverEffect', 'HoverConstants', 'hover'];
|
|
5392
|
+
const seen = new Set();
|
|
5393
|
+
const hits = [];
|
|
5394
|
+
for (const term of searchTerms) {
|
|
5395
|
+
let results = [];
|
|
5396
|
+
try {
|
|
5397
|
+
results = cg.searchNodes(term, {
|
|
5398
|
+
kinds: ['method', 'function', 'property', 'field', 'variable', 'constant', 'class', 'component'],
|
|
5399
|
+
limit: 30,
|
|
5400
|
+
});
|
|
5401
|
+
}
|
|
5402
|
+
catch {
|
|
5403
|
+
continue;
|
|
5404
|
+
}
|
|
5405
|
+
for (const r of results) {
|
|
5406
|
+
if ((0, arkts_1.isOhosApiFilePath)(r.node.filePath) || (0, query_utils_1.isTestFile)(r.node.filePath))
|
|
5407
|
+
continue;
|
|
5408
|
+
const nameLc = r.node.name.toLowerCase();
|
|
5409
|
+
const pathLc = r.node.filePath.toLowerCase();
|
|
5410
|
+
const termLc = term.toLowerCase();
|
|
5411
|
+
if (!nameLc.includes(termLc) && !pathLc.includes(termLc))
|
|
5412
|
+
continue;
|
|
5413
|
+
const key = `${r.node.filePath}:${r.node.startLine}:${r.node.name}`;
|
|
5414
|
+
if (seen.has(key))
|
|
5415
|
+
continue;
|
|
5416
|
+
seen.add(key);
|
|
5417
|
+
let score = nameLc === 'onhover' || nameLc.startsWith('onhover') ? 40 : 10;
|
|
5418
|
+
if (/hover/.test(nameLc))
|
|
5419
|
+
score += 15;
|
|
5420
|
+
if (topicBoost && /icon|appicon|dock|launcher|smartdock|appcenter/i.test(pathLc))
|
|
5421
|
+
score += 25;
|
|
5422
|
+
let snippet = (r.node.signature || r.node.name).slice(0, 120);
|
|
5423
|
+
try {
|
|
5424
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
|
|
5425
|
+
if (abs && (0, fs_1.existsSync)(abs) && r.node.startLine > 0) {
|
|
5426
|
+
const lines = (0, fs_1.readFileSync)(abs, 'utf-8').split('\n');
|
|
5427
|
+
const from = Math.max(0, r.node.startLine - 1);
|
|
5428
|
+
const to = Math.min(lines.length, from + 4);
|
|
5429
|
+
snippet = lines.slice(from, to).map((l) => l.trim()).filter(Boolean).join(' / ').slice(0, 160);
|
|
5430
|
+
}
|
|
5431
|
+
}
|
|
5432
|
+
catch { /* keep signature */ }
|
|
5433
|
+
hits.push({
|
|
5434
|
+
file: r.node.filePath,
|
|
5435
|
+
line: r.node.startLine,
|
|
5436
|
+
name: r.node.name,
|
|
5437
|
+
kind: r.node.kind,
|
|
5438
|
+
score,
|
|
5439
|
+
snippet,
|
|
5440
|
+
});
|
|
5441
|
+
}
|
|
5442
|
+
}
|
|
5443
|
+
if (hits.length === 0) {
|
|
5444
|
+
return {
|
|
5445
|
+
section: [
|
|
5446
|
+
'**Hover / pointer handler survey**',
|
|
5447
|
+
'',
|
|
5448
|
+
'No `onHover` / Hover* handlers found in the indexed project graph.',
|
|
5449
|
+
'',
|
|
5450
|
+
'> Next: Grep `onHover` / `Hover` in `*.ets` if the index is incomplete.',
|
|
5451
|
+
'',
|
|
5452
|
+
].join('\n'),
|
|
5453
|
+
hitCount: 0,
|
|
5454
|
+
};
|
|
5455
|
+
}
|
|
5456
|
+
hits.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
|
|
5457
|
+
const lines = [
|
|
5458
|
+
'**Hover / pointer handler survey**',
|
|
5459
|
+
'',
|
|
5460
|
+
'> **ANSWER NOW** from these in-repo hover handlers + snippets. Do not Grep `onHover` again unless a named Type is still missing.',
|
|
5461
|
+
'',
|
|
5462
|
+
];
|
|
5463
|
+
const shown = hits.slice(0, 12);
|
|
5464
|
+
for (const h of shown) {
|
|
5465
|
+
lines.push(`- \`${h.name}\` (${h.kind}) — \`${h.file}:${h.line}\``);
|
|
5466
|
+
if (h.snippet)
|
|
5467
|
+
lines.push(` \`${h.snippet.slice(0, 100)}\``);
|
|
5468
|
+
}
|
|
5469
|
+
if (hits.length > shown.length)
|
|
5470
|
+
lines.push(`- … and ${hits.length - shown.length} more`);
|
|
5471
|
+
lines.push('');
|
|
5472
|
+
return { section: lines.join('\n'), hitCount: shown.length };
|
|
5473
|
+
}
|
|
3926
5474
|
/**
|
|
3927
5475
|
* @kit module capability survey — repo import/usage only (SDK defs are not indexed).
|
|
3928
5476
|
*/
|
|
@@ -3963,11 +5511,49 @@ class ToolHandler {
|
|
|
3963
5511
|
});
|
|
3964
5512
|
};
|
|
3965
5513
|
const resolveImportLine = (node) => (0, query_utils_1.resolveImportLineFromNode)(node, projectRoot);
|
|
5514
|
+
const tryPushImport = (node) => {
|
|
5515
|
+
if ((0, arkts_1.isOhosApiFilePath)(node.filePath))
|
|
5516
|
+
return;
|
|
5517
|
+
const lineText = resolveImportLine(node);
|
|
5518
|
+
const lineLc = lineText.toLowerCase();
|
|
5519
|
+
const symbols = parseImportedSymbols(lineText);
|
|
5520
|
+
if (!matchesSubmodule(lineText, symbols))
|
|
5521
|
+
return;
|
|
5522
|
+
if (kitTerms.length > 0) {
|
|
5523
|
+
const matchesKit = kitTerms.some((k) => lineLc.includes(`@kit.${k.toLowerCase()}`));
|
|
5524
|
+
if (!matchesKit)
|
|
5525
|
+
return;
|
|
5526
|
+
}
|
|
5527
|
+
const key = `${node.filePath}:${node.startLine}`;
|
|
5528
|
+
if (seen.has(key))
|
|
5529
|
+
return;
|
|
5530
|
+
seen.add(key);
|
|
5531
|
+
imports.push({
|
|
5532
|
+
file: node.filePath,
|
|
5533
|
+
line: node.startLine,
|
|
5534
|
+
lineText,
|
|
5535
|
+
symbols,
|
|
5536
|
+
});
|
|
5537
|
+
};
|
|
5538
|
+
// Prefer searching the named export/API (taskpool, util, …) — kit-only FTS
|
|
5539
|
+
// with a low limit misses focused import sites among popular @kit modules.
|
|
5540
|
+
const focusSearch = submodules.length > 0 ? submodules : [];
|
|
5541
|
+
for (const sym of focusSearch) {
|
|
5542
|
+
let hits = [];
|
|
5543
|
+
try {
|
|
5544
|
+
hits = cg.searchNodes(sym, { kinds: ['import'], limit: 200 });
|
|
5545
|
+
}
|
|
5546
|
+
catch {
|
|
5547
|
+
continue;
|
|
5548
|
+
}
|
|
5549
|
+
for (const r of hits)
|
|
5550
|
+
tryPushImport(r.node);
|
|
5551
|
+
}
|
|
3966
5552
|
for (const term of kitSearchTerms) {
|
|
3967
5553
|
const termLc = term.toLowerCase().replace(/^@kit\./, '');
|
|
3968
5554
|
let hits = [];
|
|
3969
5555
|
try {
|
|
3970
|
-
hits = cg.searchNodes(term, { kinds: ['import'], limit:
|
|
5556
|
+
hits = cg.searchNodes(term, { kinds: ['import'], limit: focusSearch.length > 0 ? 40 : 120 });
|
|
3971
5557
|
}
|
|
3972
5558
|
catch {
|
|
3973
5559
|
continue;
|
|
@@ -3976,19 +5562,7 @@ class ToolHandler {
|
|
|
3976
5562
|
const lineText = resolveImportLine(r.node);
|
|
3977
5563
|
if (!lineText.toLowerCase().includes(termLc) && !lineText.toLowerCase().includes('@kit.'))
|
|
3978
5564
|
continue;
|
|
3979
|
-
|
|
3980
|
-
if (!matchesSubmodule(lineText, symbols))
|
|
3981
|
-
continue;
|
|
3982
|
-
const key = `${r.node.filePath}:${r.node.startLine}`;
|
|
3983
|
-
if (seen.has(key))
|
|
3984
|
-
continue;
|
|
3985
|
-
seen.add(key);
|
|
3986
|
-
imports.push({
|
|
3987
|
-
file: r.node.filePath,
|
|
3988
|
-
line: r.node.startLine,
|
|
3989
|
-
lineText,
|
|
3990
|
-
symbols,
|
|
3991
|
-
});
|
|
5565
|
+
tryPushImport(r.node);
|
|
3992
5566
|
}
|
|
3993
5567
|
}
|
|
3994
5568
|
if (imports.length === 0) {
|
|
@@ -4014,6 +5588,7 @@ class ToolHandler {
|
|
|
4014
5588
|
symbolFiles.set(sym, set);
|
|
4015
5589
|
}
|
|
4016
5590
|
}
|
|
5591
|
+
const importCap = submodules.length > 0 ? 16 : 28;
|
|
4017
5592
|
const lines = [
|
|
4018
5593
|
'**Kit module usage (this repo)**',
|
|
4019
5594
|
'',
|
|
@@ -4022,25 +5597,73 @@ class ToolHandler {
|
|
|
4022
5597
|
`Imports from ${kitTerms.map((k) => `\`@kit.${k}\``).join(', ')} (${imports.length} site(s)):`,
|
|
4023
5598
|
'',
|
|
4024
5599
|
];
|
|
4025
|
-
for (const imp of imports.slice(0,
|
|
5600
|
+
for (const imp of imports.slice(0, importCap)) {
|
|
4026
5601
|
const symStr = imp.symbols.length > 0 ? imp.symbols.join(', ') : '(namespace)';
|
|
4027
|
-
lines.push(`- \`${imp.file}:${imp.line}\` — \`${imp.lineText}\` (${symStr})`);
|
|
5602
|
+
lines.push(`- \`${imp.file}:${imp.line}\` — \`${imp.lineText.slice(0, 120)}\` (${symStr})`);
|
|
4028
5603
|
}
|
|
4029
|
-
if (imports.length >
|
|
4030
|
-
lines.push(`- … and ${imports.length -
|
|
5604
|
+
if (imports.length > importCap)
|
|
5605
|
+
lines.push(`- … and ${imports.length - importCap} more import site(s)`);
|
|
4031
5606
|
if (symbolFiles.size > 0) {
|
|
4032
5607
|
lines.push('');
|
|
4033
5608
|
lines.push('**Symbols imported (unique):**');
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
5609
|
+
const focused = submodules.length > 0
|
|
5610
|
+
? [...symbolFiles.entries()].filter(([sym]) => submodules.some((sm) => sym.toLowerCase() === sm.toLowerCase() || sym.toLowerCase().startsWith(`${sm.toLowerCase()}.`)))
|
|
5611
|
+
: [...symbolFiles.entries()];
|
|
5612
|
+
const ranked = (focused.length > 0 ? focused : [...symbolFiles.entries()])
|
|
5613
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
5614
|
+
.slice(0, submodules.length > 0 ? 8 : 20);
|
|
5615
|
+
for (const [sym, files] of ranked) {
|
|
5616
|
+
const fileList = [...files].slice(0, 4).map((f) => `\`${f}\``).join(', ');
|
|
5617
|
+
const more = files.size > 4 ? ` +${files.size - 4} files` : '';
|
|
4037
5618
|
lines.push(`- \`${sym}\` — imported in ${fileList}${more}`);
|
|
4038
5619
|
}
|
|
4039
5620
|
}
|
|
5621
|
+
// "需要额外安装哪些依赖" — surface oh-package.json5 lines naming the kit.
|
|
5622
|
+
if (/额外安装|还需.*依赖|需要.*依赖|install(?:ing)?\s+(?:extra\s+)?deps?|which\s+deps?/i.test(query)) {
|
|
5623
|
+
const depLines = [];
|
|
5624
|
+
try {
|
|
5625
|
+
for (const f of cg.getFiles()) {
|
|
5626
|
+
const fp = f.path.replace(/\\/g, '/');
|
|
5627
|
+
if (!/oh-package\.json5?$/i.test(fp))
|
|
5628
|
+
continue;
|
|
5629
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, f.path);
|
|
5630
|
+
if (!abs)
|
|
5631
|
+
continue;
|
|
5632
|
+
let content = '';
|
|
5633
|
+
try {
|
|
5634
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
5635
|
+
}
|
|
5636
|
+
catch {
|
|
5637
|
+
continue;
|
|
5638
|
+
}
|
|
5639
|
+
if (!kitTerms.some((k) => content.toLowerCase().includes(k.toLowerCase())))
|
|
5640
|
+
continue;
|
|
5641
|
+
const fileLines = content.split('\n');
|
|
5642
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
5643
|
+
const line = fileLines[i] ?? '';
|
|
5644
|
+
if (!kitTerms.some((k) => line.toLowerCase().includes(k.toLowerCase())))
|
|
5645
|
+
continue;
|
|
5646
|
+
depLines.push(`- \`${fp}:${i + 1}\` \`${line.trim().slice(0, 100)}\``);
|
|
5647
|
+
}
|
|
5648
|
+
if (depLines.length >= 16)
|
|
5649
|
+
break;
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
catch { /* optional */ }
|
|
5653
|
+
lines.push('');
|
|
5654
|
+
lines.push('**oh-package dependencies naming this Kit**');
|
|
5655
|
+
if (depLines.length === 0) {
|
|
5656
|
+
lines.push('- No `oh-package.json5` dependency line naming this Kit was indexed. ' +
|
|
5657
|
+
'**ANSWER NOW**: calling `@kit.*` typically needs **no extra npm install** beyond the HarmonyOS SDK / DevEco kit — only what `oh-package.json5` already lists.');
|
|
5658
|
+
}
|
|
5659
|
+
else {
|
|
5660
|
+
lines.push(...depLines.slice(0, 16));
|
|
5661
|
+
}
|
|
5662
|
+
}
|
|
4040
5663
|
lines.push('');
|
|
4041
|
-
lines.push('> Kit usage survey complete —
|
|
5664
|
+
lines.push('> Kit usage survey complete — **ANSWER NOW** from this section; do not Grep `oh-package` / SDK docs again.');
|
|
4042
5665
|
lines.push('');
|
|
4043
|
-
return { section: lines.join('\n'), symbolCount: symbolFiles.size };
|
|
5666
|
+
return { section: lines.join('\n'), symbolCount: Math.max(symbolFiles.size, imports.length, 1) };
|
|
4044
5667
|
}
|
|
4045
5668
|
/** Disambiguate homonymous types (Configuration, Rectangle) when several defs exist. */
|
|
4046
5669
|
buildHomonymDefinitionsSection(cg, query) {
|
|
@@ -4072,20 +5695,51 @@ class ToolHandler {
|
|
|
4072
5695
|
const rel = (p) => p.replace(/\\/g, '/');
|
|
4073
5696
|
const lines = ['**Inheritance survey**', ''];
|
|
4074
5697
|
let listed = 0;
|
|
5698
|
+
const projectRoot = (() => {
|
|
5699
|
+
try {
|
|
5700
|
+
return cg.getProjectRoot();
|
|
5701
|
+
}
|
|
5702
|
+
catch {
|
|
5703
|
+
return '';
|
|
5704
|
+
}
|
|
5705
|
+
})();
|
|
4075
5706
|
for (const typeName of (0, query_utils_1.extractTypeNamesFromQuery)(query).slice(0, 2)) {
|
|
4076
5707
|
const bases = cg.getNodesByName(typeName).filter((n) => (n.kind === 'class' || n.kind === 'struct' || n.kind === 'interface') && !(0, query_utils_1.isTestFile)(n.filePath));
|
|
4077
5708
|
for (const base of bases.slice(0, 2)) {
|
|
4078
|
-
const
|
|
5709
|
+
const publicSubs = [];
|
|
5710
|
+
const otherSubs = [];
|
|
4079
5711
|
for (const e of cg.getIncomingEdges(base.id)) {
|
|
4080
5712
|
if (e.kind !== 'extends' && e.kind !== 'implements')
|
|
4081
5713
|
continue;
|
|
4082
5714
|
try {
|
|
4083
5715
|
const child = cg.getNode(e.source);
|
|
4084
|
-
if (child
|
|
4085
|
-
|
|
5716
|
+
if (!child || (0, query_utils_1.isTestFile)(child.filePath))
|
|
5717
|
+
continue;
|
|
5718
|
+
const isCpp = /\.(hpp|h|hh|hxx|cpp|cc|cxx)$/i.test(child.filePath);
|
|
5719
|
+
if (isCpp && projectRoot) {
|
|
5720
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, child.filePath);
|
|
5721
|
+
if (abs && (0, fs_1.existsSync)(abs) && child.startLine > 0) {
|
|
5722
|
+
try {
|
|
5723
|
+
const fileLines = (0, fs_1.readFileSync)(abs, 'utf-8').split('\n');
|
|
5724
|
+
// Class decl may span a few lines (`class Foo\n: public Bar`).
|
|
5725
|
+
const from = Math.max(0, child.startLine - 1);
|
|
5726
|
+
const decl = fileLines.slice(from, Math.min(fileLines.length, from + 4)).join(' ');
|
|
5727
|
+
const pub = cppExtendsLooksPublic(decl, base.name);
|
|
5728
|
+
if (pub === false) {
|
|
5729
|
+
otherSubs.push(child);
|
|
5730
|
+
continue;
|
|
5731
|
+
}
|
|
5732
|
+
}
|
|
5733
|
+
catch { /* keep as public-ish */ }
|
|
5734
|
+
}
|
|
5735
|
+
}
|
|
5736
|
+
publicSubs.push(child);
|
|
4086
5737
|
}
|
|
4087
5738
|
catch { /* skip */ }
|
|
4088
5739
|
}
|
|
5740
|
+
// Prefer public / language-default is-a; only fall back to private if
|
|
5741
|
+
// nothing else is indexed (avoids empty answers).
|
|
5742
|
+
const subs = publicSubs.length > 0 ? publicSubs : otherSubs;
|
|
4089
5743
|
if (subs.length === 0)
|
|
4090
5744
|
continue;
|
|
4091
5745
|
lines.push(`### Subtypes of \`${base.name}\` (\`${rel(base.filePath)}\`)`);
|
|
@@ -4095,6 +5749,10 @@ class ToolHandler {
|
|
|
4095
5749
|
}
|
|
4096
5750
|
if (subs.length > 30)
|
|
4097
5751
|
lines.push(`- … and ${subs.length - 30} more`);
|
|
5752
|
+
if (publicSubs.length > 0 && otherSubs.length > 0) {
|
|
5753
|
+
lines.push(`- _(also private/protected C++ inherits, omitted: ${otherSubs.map((s) => s.name).slice(0, 8).join(', ')})` +
|
|
5754
|
+
`${otherSubs.length > 8 ? ', …' : ''})_`);
|
|
5755
|
+
}
|
|
4098
5756
|
lines.push('');
|
|
4099
5757
|
}
|
|
4100
5758
|
}
|
|
@@ -4120,6 +5778,10 @@ class ToolHandler {
|
|
|
4120
5778
|
for (const m of query.matchAll(/(\.[a-zA-Z_][\w]*\s*\([^)]*\))/g)) {
|
|
4121
5779
|
patterns.push(m[1].replace(/\s+/g, ''));
|
|
4122
5780
|
}
|
|
5781
|
+
// Bare field / mutex names (m_eglMutex) — text-scan without a leading dot.
|
|
5782
|
+
for (const f of (0, query_utils_1.extractFieldLikeSymbolsFromQuery)(query)) {
|
|
5783
|
+
patterns.push(f);
|
|
5784
|
+
}
|
|
4123
5785
|
if (patterns.length === 0)
|
|
4124
5786
|
return '';
|
|
4125
5787
|
const rel = (p) => p.replace(/\\/g, '/');
|
|
@@ -4145,21 +5807,264 @@ class ToolHandler {
|
|
|
4145
5807
|
}
|
|
4146
5808
|
// Scan top FTS files for literal pattern in source
|
|
4147
5809
|
for (const pat of patterns.slice(0, 3)) {
|
|
4148
|
-
const literal = pat.startsWith('.')
|
|
5810
|
+
const literal = pat.startsWith('.') || pat.startsWith('m_') || /(?:Mutex|Lock)$/.test(pat)
|
|
5811
|
+
? pat
|
|
5812
|
+
: `.${pat}`;
|
|
4149
5813
|
const re = new RegExp(literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
4150
5814
|
let files = [];
|
|
4151
5815
|
try {
|
|
4152
|
-
files = cg.searchNodes(patterns[0].replace(/^\./, ''), { limit: 25 });
|
|
5816
|
+
files = cg.searchNodes(patterns[0].replace(/^\./, ''), { limit: 25 });
|
|
5817
|
+
}
|
|
5818
|
+
catch {
|
|
5819
|
+
continue;
|
|
5820
|
+
}
|
|
5821
|
+
for (const r of files) {
|
|
5822
|
+
if ((0, query_utils_1.isTestFile)(r.node.filePath))
|
|
5823
|
+
continue;
|
|
5824
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
|
|
5825
|
+
if (!abs)
|
|
5826
|
+
continue;
|
|
5827
|
+
let content;
|
|
5828
|
+
try {
|
|
5829
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
5830
|
+
}
|
|
5831
|
+
catch {
|
|
5832
|
+
continue;
|
|
5833
|
+
}
|
|
5834
|
+
const fileLines = content.split('\n');
|
|
5835
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
5836
|
+
if (re.test(fileLines[i] ?? ''))
|
|
5837
|
+
addHit(rel(r.node.filePath), i + 1);
|
|
5838
|
+
}
|
|
5839
|
+
}
|
|
5840
|
+
}
|
|
5841
|
+
// Field patterns with zero FTS: seed from co-named methods/types in the query.
|
|
5842
|
+
const fieldPats = patterns.filter((p) => p.startsWith('m_') || /(?:Mutex|Lock)$/.test(p));
|
|
5843
|
+
if (fieldPats.length > 0 && hits.size < 2) {
|
|
5844
|
+
const seeds = [
|
|
5845
|
+
...(0, query_utils_1.extractLocalDetailAnchors)(query),
|
|
5846
|
+
...(0, query_utils_1.extractTypeNamesFromQuery)(query),
|
|
5847
|
+
].filter((n) => !fieldPats.some((p) => p === n || p.endsWith(n)));
|
|
5848
|
+
const seen = new Set(hits.keys());
|
|
5849
|
+
for (const seed of seeds.slice(0, 5)) {
|
|
5850
|
+
let seedHits = [];
|
|
5851
|
+
try {
|
|
5852
|
+
seedHits = cg.searchNodes(seed, { limit: 20 });
|
|
5853
|
+
}
|
|
5854
|
+
catch {
|
|
5855
|
+
continue;
|
|
5856
|
+
}
|
|
5857
|
+
for (const r of seedHits) {
|
|
5858
|
+
if ((0, query_utils_1.isTestFile)(r.node.filePath))
|
|
5859
|
+
continue;
|
|
5860
|
+
const fp = rel(r.node.filePath);
|
|
5861
|
+
if (seen.has(fp))
|
|
5862
|
+
continue;
|
|
5863
|
+
seen.add(fp);
|
|
5864
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
|
|
5865
|
+
if (!abs)
|
|
5866
|
+
continue;
|
|
5867
|
+
let content;
|
|
5868
|
+
try {
|
|
5869
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
5870
|
+
}
|
|
5871
|
+
catch {
|
|
5872
|
+
continue;
|
|
5873
|
+
}
|
|
5874
|
+
const fileLines = content.split('\n');
|
|
5875
|
+
for (const pat of fieldPats) {
|
|
5876
|
+
const re = new RegExp(pat.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
5877
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
5878
|
+
if (re.test(fileLines[i] ?? ''))
|
|
5879
|
+
addHit(fp, i + 1);
|
|
5880
|
+
}
|
|
5881
|
+
}
|
|
5882
|
+
}
|
|
5883
|
+
}
|
|
5884
|
+
}
|
|
5885
|
+
if (hits.size === 0)
|
|
5886
|
+
return '';
|
|
5887
|
+
const lines = ['**Member / pattern usage**', ''];
|
|
5888
|
+
let fileCount = 0;
|
|
5889
|
+
for (const [fp, lineNos] of [...hits.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
5890
|
+
if (fileCount >= 40)
|
|
5891
|
+
break;
|
|
5892
|
+
const sorted = lineNos.sort((a, b) => a - b).slice(0, 6);
|
|
5893
|
+
const lineStr = sorted.join(', ');
|
|
5894
|
+
const more = lineNos.length > 6 ? ` +${lineNos.length - 6} lines` : '';
|
|
5895
|
+
lines.push(`- \`${fp}\` — lines ${lineStr}${more}`);
|
|
5896
|
+
fileCount++;
|
|
5897
|
+
}
|
|
5898
|
+
if (hits.size > 40)
|
|
5899
|
+
lines.push(`- … and ${hits.size - 40} more file(s)`);
|
|
5900
|
+
lines.push('');
|
|
5901
|
+
lines.push('> Pattern survey complete — answer from this list; run another explore naming missing symbols if needed.');
|
|
5902
|
+
lines.push('');
|
|
5903
|
+
return lines.join('\n');
|
|
5904
|
+
}
|
|
5905
|
+
/**
|
|
5906
|
+
* API call-site survey — where a named API/symbol (statfs, napi_*) appears in repo source.
|
|
5907
|
+
*/
|
|
5908
|
+
/**
|
|
5909
|
+
* In-repo how-to for system settings/capabilities (language/locale/…).
|
|
5910
|
+
* Lists concrete `@ohos.i18n` / `getSystemLanguage` call sites — not every
|
|
5911
|
+
* file that mentions the word "language".
|
|
5912
|
+
*/
|
|
5913
|
+
buildSystemCapabilityHowtoSection(cg, query, projectRoot) {
|
|
5914
|
+
const searchTerms = [
|
|
5915
|
+
'getSystemLanguage',
|
|
5916
|
+
'getSystemRegion',
|
|
5917
|
+
'System.getSystemLanguage',
|
|
5918
|
+
'@ohos.i18n',
|
|
5919
|
+
'i18n.System',
|
|
5920
|
+
];
|
|
5921
|
+
// Topic tokens from the question (language/locale) only as secondary filters.
|
|
5922
|
+
if (/\blanguage\b|语言/i.test(query))
|
|
5923
|
+
searchTerms.push('getSystemLanguage');
|
|
5924
|
+
if (/\blocale\b|地区/i.test(query))
|
|
5925
|
+
searchTerms.push('getSystemRegion');
|
|
5926
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
5927
|
+
const rows = [];
|
|
5928
|
+
const seen = new Set();
|
|
5929
|
+
for (const term of searchTerms) {
|
|
5930
|
+
let hits = [];
|
|
5931
|
+
try {
|
|
5932
|
+
hits = cg.searchNodes(term.replace(/^@/, ''), { limit: 35 });
|
|
5933
|
+
}
|
|
5934
|
+
catch {
|
|
5935
|
+
continue;
|
|
5936
|
+
}
|
|
5937
|
+
for (const r of hits) {
|
|
5938
|
+
if ((0, query_utils_1.isTestFile)(r.node.filePath) || (0, arkts_1.isOhosApiFilePath)(r.node.filePath))
|
|
5939
|
+
continue;
|
|
5940
|
+
if (/\.d\.ts$/i.test(r.node.filePath))
|
|
5941
|
+
continue;
|
|
5942
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
|
|
5943
|
+
if (!abs || !(0, fs_1.existsSync)(abs))
|
|
5944
|
+
continue;
|
|
5945
|
+
let content;
|
|
5946
|
+
try {
|
|
5947
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
5948
|
+
}
|
|
5949
|
+
catch {
|
|
5950
|
+
continue;
|
|
5951
|
+
}
|
|
5952
|
+
const fileLines = content.split('\n');
|
|
5953
|
+
const re = new RegExp(term.startsWith('@')
|
|
5954
|
+
? term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
5955
|
+
: `\\b${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`);
|
|
5956
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
5957
|
+
const lineText = fileLines[i] ?? '';
|
|
5958
|
+
if (!re.test(lineText))
|
|
5959
|
+
continue;
|
|
5960
|
+
// Prefer real get/import sites over comments.
|
|
5961
|
+
if (/getSystemLanguage|getSystemRegion|@ohos\.i18n|i18n\.System/i.test(lineText)
|
|
5962
|
+
|| /import\s+.*i18n/i.test(lineText)) {
|
|
5963
|
+
const key = `${r.node.filePath}:${i + 1}`;
|
|
5964
|
+
if (seen.has(key))
|
|
5965
|
+
continue;
|
|
5966
|
+
seen.add(key);
|
|
5967
|
+
rows.push(`- \`${rel(r.node.filePath)}:${i + 1}\` \`${lineText.trim().slice(0, 120)}\``);
|
|
5968
|
+
if (rows.length >= 16)
|
|
5969
|
+
break;
|
|
5970
|
+
}
|
|
5971
|
+
}
|
|
5972
|
+
if (rows.length >= 16)
|
|
5973
|
+
break;
|
|
5974
|
+
}
|
|
5975
|
+
if (rows.length >= 16)
|
|
5976
|
+
break;
|
|
5977
|
+
}
|
|
5978
|
+
if (rows.length === 0) {
|
|
5979
|
+
return {
|
|
5980
|
+
section: [
|
|
5981
|
+
'**System capability howto (in-repo)**',
|
|
5982
|
+
'',
|
|
5983
|
+
'No `getSystemLanguage` / `@ohos.i18n` call sites indexed. **ANSWER NOW** from SDK docs if the project does not wrap this API.',
|
|
5984
|
+
'',
|
|
5985
|
+
].join('\n'),
|
|
5986
|
+
hitCount: 0,
|
|
5987
|
+
};
|
|
5988
|
+
}
|
|
5989
|
+
return {
|
|
5990
|
+
section: [
|
|
5991
|
+
'**System capability howto (in-repo)**',
|
|
5992
|
+
'',
|
|
5993
|
+
'> Concrete call/import sites for system language/locale. **ANSWER NOW** — do not Grep `language` broadly.',
|
|
5994
|
+
'',
|
|
5995
|
+
...rows,
|
|
5996
|
+
'',
|
|
5997
|
+
].join('\n'),
|
|
5998
|
+
hitCount: rows.length,
|
|
5999
|
+
};
|
|
6000
|
+
}
|
|
6001
|
+
/**
|
|
6002
|
+
* Declaration / id / native-binding sites for a named UI/native Type (XComponent…).
|
|
6003
|
+
*/
|
|
6004
|
+
buildDeclarationSiteSurveySection(cg, query, projectRoot) {
|
|
6005
|
+
const types = (0, query_utils_1.extractTypeNamesFromQuery)(query)
|
|
6006
|
+
.filter((t) => !(0, query_utils_1.isFrameworkUiDecoratorName)(t))
|
|
6007
|
+
.slice(0, 3);
|
|
6008
|
+
if (types.length === 0)
|
|
6009
|
+
return { section: '', hitCount: 0, attempted: false };
|
|
6010
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
6011
|
+
const rows = [];
|
|
6012
|
+
const seen = new Set();
|
|
6013
|
+
for (const typeName of types) {
|
|
6014
|
+
let hits = [];
|
|
6015
|
+
try {
|
|
6016
|
+
hits = cg.searchNodes(typeName, { limit: 80 });
|
|
4153
6017
|
}
|
|
4154
6018
|
catch {
|
|
4155
6019
|
continue;
|
|
4156
6020
|
}
|
|
4157
|
-
|
|
6021
|
+
// Also pull same-name defs (component/class) even when FTS ranks them low.
|
|
6022
|
+
try {
|
|
6023
|
+
for (const n of cg.getNodesByName(typeName).slice(0, 40)) {
|
|
6024
|
+
hits.push({ node: n, score: 1 });
|
|
6025
|
+
}
|
|
6026
|
+
}
|
|
6027
|
+
catch { /* optional */ }
|
|
6028
|
+
const files = new Map(); // abs → rel
|
|
6029
|
+
for (const r of hits) {
|
|
4158
6030
|
if ((0, query_utils_1.isTestFile)(r.node.filePath))
|
|
4159
6031
|
continue;
|
|
6032
|
+
if ((0, arkts_1.isOhosApiFilePath)(r.node.filePath) && /\.d\.ts$/i.test(r.node.filePath))
|
|
6033
|
+
continue;
|
|
4160
6034
|
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
|
|
4161
|
-
if (!abs)
|
|
6035
|
+
if (!abs || !(0, fs_1.existsSync)(abs))
|
|
4162
6036
|
continue;
|
|
6037
|
+
files.set(abs, rel(r.node.filePath));
|
|
6038
|
+
}
|
|
6039
|
+
// Framework UI tags (XComponent…) often have no indexed symbol at the JSX
|
|
6040
|
+
// site — expand to sibling .ets under the same feature/staticcommon roots
|
|
6041
|
+
// already touched by native/header hits.
|
|
6042
|
+
const featureRoots = new Set();
|
|
6043
|
+
for (const fp of files.values()) {
|
|
6044
|
+
const m = fp.match(/^(feature\/[^/]+|staticcommon\/[^/]+)/);
|
|
6045
|
+
if (m?.[1])
|
|
6046
|
+
featureRoots.add(m[1]);
|
|
6047
|
+
}
|
|
6048
|
+
if (featureRoots.size > 0 && featureRoots.size <= 6) {
|
|
6049
|
+
try {
|
|
6050
|
+
for (const f of cg.getFiles()) {
|
|
6051
|
+
const fp = rel(f.path);
|
|
6052
|
+
if (!/\.ets$/i.test(fp) || (0, query_utils_1.isTestFile)(fp))
|
|
6053
|
+
continue;
|
|
6054
|
+
if (![...featureRoots].some((root) => fp.startsWith(`${root}/`)))
|
|
6055
|
+
continue;
|
|
6056
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, f.path);
|
|
6057
|
+
if (!abs || !(0, fs_1.existsSync)(abs))
|
|
6058
|
+
continue;
|
|
6059
|
+
files.set(abs, fp);
|
|
6060
|
+
}
|
|
6061
|
+
}
|
|
6062
|
+
catch { /* optional */ }
|
|
6063
|
+
}
|
|
6064
|
+
const typeRe = new RegExp(`\\b${typeName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`);
|
|
6065
|
+
const jsxRe = new RegExp(`<${typeName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`);
|
|
6066
|
+
const callRe = new RegExp(`${typeName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\(`);
|
|
6067
|
+
for (const [abs, fp] of files) {
|
|
4163
6068
|
let content;
|
|
4164
6069
|
try {
|
|
4165
6070
|
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
@@ -4168,68 +6073,123 @@ class ToolHandler {
|
|
|
4168
6073
|
continue;
|
|
4169
6074
|
}
|
|
4170
6075
|
const fileLines = content.split('\n');
|
|
6076
|
+
const isUi = /\.(ets|ts|tsx|jsx)$/i.test(fp);
|
|
6077
|
+
const isNative = /\.(cpp|cc|cxx|h|hpp)$/i.test(fp);
|
|
4171
6078
|
for (let i = 0; i < fileLines.length; i++) {
|
|
4172
|
-
|
|
4173
|
-
|
|
6079
|
+
const lineText = fileLines[i] ?? '';
|
|
6080
|
+
if (!typeRe.test(lineText))
|
|
6081
|
+
continue;
|
|
6082
|
+
const window = [lineText, fileLines[i + 1] ?? '', fileLines[i + 2] ?? ''].join('\n');
|
|
6083
|
+
const uiHit = isUi
|
|
6084
|
+
&& (jsxRe.test(lineText)
|
|
6085
|
+
|| callRe.test(lineText)
|
|
6086
|
+
|| (/id\s*[:=]/.test(window) && !/Log(?:Error|Info|Warn|Debug|Message)/i.test(lineText)));
|
|
6087
|
+
const nativeHit = isNative
|
|
6088
|
+
&& /OH_Native|RegisterCallback|Export\s*\(|napi_/i.test(window)
|
|
6089
|
+
&& !/Log(?:Error|Info|Warn|Debug|Message)/i.test(lineText);
|
|
6090
|
+
// Prefer construction / JSX / id / native bind; skip log-only mentions.
|
|
6091
|
+
if (!uiHit && !nativeHit)
|
|
6092
|
+
continue;
|
|
6093
|
+
const key = `${fp}:${i + 1}`;
|
|
6094
|
+
if (seen.has(key))
|
|
6095
|
+
continue;
|
|
6096
|
+
seen.add(key);
|
|
6097
|
+
const clip = [lineText, fileLines[i + 1] ?? '', fileLines[i + 2] ?? '']
|
|
6098
|
+
.map((l) => l.trim())
|
|
6099
|
+
.filter(Boolean)
|
|
6100
|
+
.join(' / ')
|
|
6101
|
+
.slice(0, 160);
|
|
6102
|
+
rows.push(`- \`${typeName}\` — \`${fp}:${i + 1}\` \`${clip}\``);
|
|
6103
|
+
if (rows.length >= 24)
|
|
6104
|
+
break;
|
|
4174
6105
|
}
|
|
6106
|
+
if (rows.length >= 24)
|
|
6107
|
+
break;
|
|
4175
6108
|
}
|
|
4176
|
-
|
|
4177
|
-
if (hits.size === 0)
|
|
4178
|
-
return '';
|
|
4179
|
-
const lines = ['**Member / pattern usage**', ''];
|
|
4180
|
-
let fileCount = 0;
|
|
4181
|
-
for (const [fp, lineNos] of [...hits.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
4182
|
-
if (fileCount >= 40)
|
|
6109
|
+
if (rows.length >= 24)
|
|
4183
6110
|
break;
|
|
4184
|
-
const sorted = lineNos.sort((a, b) => a - b).slice(0, 6);
|
|
4185
|
-
const lineStr = sorted.join(', ');
|
|
4186
|
-
const more = lineNos.length > 6 ? ` +${lineNos.length - 6} lines` : '';
|
|
4187
|
-
lines.push(`- \`${fp}\` — lines ${lineStr}${more}`);
|
|
4188
|
-
fileCount++;
|
|
4189
6111
|
}
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
6112
|
+
// Prefer UI (.ets) rows first so C++ includes don't dominate.
|
|
6113
|
+
rows.sort((a, b) => {
|
|
6114
|
+
const score = (s) => (/\.ets`/.test(s) ? 0 : /\.cpp`|\.h`/.test(s) ? 2 : 1);
|
|
6115
|
+
return score(a) - score(b);
|
|
6116
|
+
});
|
|
6117
|
+
if (rows.length === 0) {
|
|
6118
|
+
return {
|
|
6119
|
+
section: [
|
|
6120
|
+
'**Declaration / id / native-binding survey**',
|
|
6121
|
+
'',
|
|
6122
|
+
`> No UI/native construction sites for \`${types.join('`, `')}\` in the index. **ANSWER NOW** — do not Grep the same Type for "declaration"; at most one Grep for \`<${types[0]}\` if you must.`,
|
|
6123
|
+
'',
|
|
6124
|
+
].join('\n'),
|
|
6125
|
+
hitCount: 0,
|
|
6126
|
+
attempted: true,
|
|
6127
|
+
};
|
|
6128
|
+
}
|
|
6129
|
+
return {
|
|
6130
|
+
section: [
|
|
6131
|
+
'**Declaration / id / native-binding survey**',
|
|
6132
|
+
'',
|
|
6133
|
+
'> Construction / JSX / `id` / native register sites. **ANSWER NOW** — do not Grep the same Type again.',
|
|
6134
|
+
'',
|
|
6135
|
+
...rows.slice(0, 20),
|
|
6136
|
+
'',
|
|
6137
|
+
].join('\n'),
|
|
6138
|
+
hitCount: Math.min(rows.length, 20),
|
|
6139
|
+
attempted: true,
|
|
6140
|
+
};
|
|
4196
6141
|
}
|
|
4197
|
-
/**
|
|
4198
|
-
* API call-site survey — where a named API/symbol (statfs, napi_*) appears in repo source.
|
|
4199
|
-
*/
|
|
4200
6142
|
buildApiUsageSection(cg, query, projectRoot) {
|
|
4201
6143
|
// Include PascalCase SDK modules (Telephony) — deps-only missed them.
|
|
4202
|
-
const
|
|
4203
|
-
if (
|
|
6144
|
+
const rawSymbols = (0, query_utils_1.extractApiUsageTokens)(query).slice(0, 6);
|
|
6145
|
+
if (rawSymbols.length === 0)
|
|
4204
6146
|
return { section: '', fileCount: 0 };
|
|
6147
|
+
// Expand Telephony → telephony / @ohos.telephony so import call sites hit.
|
|
6148
|
+
const symbols = [];
|
|
6149
|
+
for (const sym of rawSymbols) {
|
|
6150
|
+
symbols.push(sym);
|
|
6151
|
+
if (/^[A-Z][A-Za-z0-9]+$/.test(sym) && sym.length >= 4) {
|
|
6152
|
+
const lc = sym.toLowerCase();
|
|
6153
|
+
if (!symbols.includes(lc))
|
|
6154
|
+
symbols.push(lc);
|
|
6155
|
+
const ohos = `@ohos.${lc}`;
|
|
6156
|
+
if (!symbols.includes(ohos))
|
|
6157
|
+
symbols.push(ohos);
|
|
6158
|
+
}
|
|
6159
|
+
}
|
|
4205
6160
|
const rel = (p) => p.replace(/\\/g, '/');
|
|
4206
6161
|
const hits = new Map();
|
|
4207
6162
|
const addHit = (file, line) => {
|
|
4208
6163
|
if ((0, query_utils_1.isTestFile)(file))
|
|
4209
6164
|
return;
|
|
4210
6165
|
const fp = rel(file);
|
|
4211
|
-
const
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
6166
|
+
const stub = /\.d\.ts$/i.test(fp) || /windowsceneinterfaces|@ohos\.|\/api\//i.test(fp);
|
|
6167
|
+
const prev = hits.get(fp) ?? { lines: [], stub };
|
|
6168
|
+
if (!prev.lines.includes(line))
|
|
6169
|
+
prev.lines.push(line);
|
|
6170
|
+
prev.stub = prev.stub && stub;
|
|
6171
|
+
hits.set(fp, prev);
|
|
4215
6172
|
};
|
|
4216
|
-
for (const sym of symbols.slice(0,
|
|
6173
|
+
for (const sym of symbols.slice(0, 6)) {
|
|
4217
6174
|
const symLc = sym.toLowerCase();
|
|
4218
6175
|
let nodes = [];
|
|
4219
6176
|
try {
|
|
4220
|
-
nodes = cg.searchNodes(sym, { limit: 40 });
|
|
6177
|
+
nodes = cg.searchNodes(sym.replace(/^@/, ''), { limit: 40 });
|
|
4221
6178
|
}
|
|
4222
6179
|
catch { /* skip */ }
|
|
4223
6180
|
for (const r of nodes) {
|
|
4224
6181
|
const n = r.node;
|
|
4225
|
-
|
|
6182
|
+
const hay = `${n.name} ${n.signature || ''} ${n.filePath}`.toLowerCase();
|
|
6183
|
+
if (hay.includes(symLc) || n.name.toLowerCase() === symLc) {
|
|
4226
6184
|
addHit(n.filePath, n.startLine);
|
|
4227
6185
|
}
|
|
4228
6186
|
}
|
|
4229
|
-
const wordRe = new RegExp(
|
|
6187
|
+
const wordRe = new RegExp(sym.startsWith('@')
|
|
6188
|
+
? sym.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
6189
|
+
: `\\b${sym.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
|
|
4230
6190
|
let scanHits = [];
|
|
4231
6191
|
try {
|
|
4232
|
-
scanHits = cg.searchNodes(sym, { limit:
|
|
6192
|
+
scanHits = cg.searchNodes(sym.replace(/^@/, ''), { limit: 35 });
|
|
4233
6193
|
}
|
|
4234
6194
|
catch {
|
|
4235
6195
|
continue;
|
|
@@ -4259,30 +6219,210 @@ class ToolHandler {
|
|
|
4259
6219
|
}
|
|
4260
6220
|
}
|
|
4261
6221
|
}
|
|
6222
|
+
// Field/mutex tokens often have zero FTS hits (not extracted as nodes). Seed
|
|
6223
|
+
// scan files from co-named methods/types in the query (drawOnSubThread…).
|
|
6224
|
+
const fieldSyms = (0, query_utils_1.extractFieldLikeSymbolsFromQuery)(query);
|
|
6225
|
+
if (fieldSyms.length > 0) {
|
|
6226
|
+
const seedNames = [
|
|
6227
|
+
...(0, query_utils_1.extractLocalDetailAnchors)(query),
|
|
6228
|
+
...(0, query_utils_1.extractTypeNamesFromQuery)(query),
|
|
6229
|
+
].filter((n) => !fieldSyms.includes(n) && n.length >= 4);
|
|
6230
|
+
const seenSeedFiles = new Set([...hits.keys()]);
|
|
6231
|
+
for (const seed of seedNames.slice(0, 5)) {
|
|
6232
|
+
let seedHits = [];
|
|
6233
|
+
try {
|
|
6234
|
+
seedHits = cg.searchNodes(seed, { limit: 20 });
|
|
6235
|
+
}
|
|
6236
|
+
catch {
|
|
6237
|
+
continue;
|
|
6238
|
+
}
|
|
6239
|
+
for (const r of seedHits) {
|
|
6240
|
+
if ((0, query_utils_1.isTestFile)(r.node.filePath))
|
|
6241
|
+
continue;
|
|
6242
|
+
const fp = rel(r.node.filePath);
|
|
6243
|
+
if (seenSeedFiles.has(fp))
|
|
6244
|
+
continue;
|
|
6245
|
+
seenSeedFiles.add(fp);
|
|
6246
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
|
|
6247
|
+
if (!abs)
|
|
6248
|
+
continue;
|
|
6249
|
+
let content;
|
|
6250
|
+
try {
|
|
6251
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
6252
|
+
}
|
|
6253
|
+
catch {
|
|
6254
|
+
continue;
|
|
6255
|
+
}
|
|
6256
|
+
const fileLines = content.split('\n');
|
|
6257
|
+
for (const field of fieldSyms) {
|
|
6258
|
+
const re = new RegExp(`\\b${field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`);
|
|
6259
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
6260
|
+
if (re.test(fileLines[i] ?? ''))
|
|
6261
|
+
addHit(fp, i + 1);
|
|
6262
|
+
}
|
|
6263
|
+
}
|
|
6264
|
+
}
|
|
6265
|
+
}
|
|
6266
|
+
}
|
|
4262
6267
|
if (hits.size === 0)
|
|
4263
6268
|
return { section: '', fileCount: 0 };
|
|
6269
|
+
// Prefer project call/import sites over SDK .d.ts stubs.
|
|
6270
|
+
const rankedFiles = [...hits.entries()].sort((a, b) => {
|
|
6271
|
+
if (a[1].stub !== b[1].stub)
|
|
6272
|
+
return a[1].stub ? 1 : -1;
|
|
6273
|
+
return a[0].localeCompare(b[0]);
|
|
6274
|
+
});
|
|
6275
|
+
const projectHits = rankedFiles.filter(([, v]) => !v.stub);
|
|
6276
|
+
const display = projectHits.length > 0 ? projectHits : rankedFiles;
|
|
6277
|
+
// In files that import the API, also record alias.method( call sites so
|
|
6278
|
+
// "which call methods" answers don't stop at import lines.
|
|
6279
|
+
const aliasCallRe = /\b((?:call|radio|data|sim|observer|telephony|i18n|sensor|wifi)(?:\.\w+)*)\.([A-Za-z_][\w]*)\s*\(/gi;
|
|
6280
|
+
const wantLifetime = (0, query_utils_1.queryAsFieldUsageSurvey)(query)
|
|
6281
|
+
&& /new|delete|allocat|释放|free/i.test(query);
|
|
6282
|
+
for (const [fp] of display.slice(0, 40)) {
|
|
6283
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, fp);
|
|
6284
|
+
if (!abs || !(0, fs_1.existsSync)(abs))
|
|
6285
|
+
continue;
|
|
6286
|
+
let fileLines = [];
|
|
6287
|
+
try {
|
|
6288
|
+
fileLines = (0, fs_1.readFileSync)(abs, 'utf-8').split('\n');
|
|
6289
|
+
}
|
|
6290
|
+
catch {
|
|
6291
|
+
continue;
|
|
6292
|
+
}
|
|
6293
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
6294
|
+
const lineText = fileLines[i] ?? '';
|
|
6295
|
+
if (aliasCallRe.test(lineText))
|
|
6296
|
+
addHit(fp, i + 1);
|
|
6297
|
+
aliasCallRe.lastIndex = 0;
|
|
6298
|
+
if (wantLifetime) {
|
|
6299
|
+
for (const sym of rawSymbols) {
|
|
6300
|
+
if (!lineText.includes(sym))
|
|
6301
|
+
continue;
|
|
6302
|
+
if (/\b(?:new|delete|free|reset)\b/i.test(lineText) || /=\s*new\b/.test(lineText)) {
|
|
6303
|
+
addHit(fp, i + 1);
|
|
6304
|
+
}
|
|
6305
|
+
}
|
|
6306
|
+
}
|
|
6307
|
+
}
|
|
6308
|
+
}
|
|
6309
|
+
// Recompute display after call-site enrichment.
|
|
6310
|
+
const rankedFiles2 = [...hits.entries()].sort((a, b) => {
|
|
6311
|
+
if (a[1].stub !== b[1].stub)
|
|
6312
|
+
return a[1].stub ? 1 : -1;
|
|
6313
|
+
return a[0].localeCompare(b[0]);
|
|
6314
|
+
});
|
|
6315
|
+
const projectHits2 = rankedFiles2.filter(([, v]) => !v.stub);
|
|
6316
|
+
const display2 = projectHits2.length > 0 ? projectHits2 : rankedFiles2;
|
|
6317
|
+
// Extract concrete API method names from snippets (telephony.call.xxx / radio.getY).
|
|
6318
|
+
const methodNames = new Set();
|
|
6319
|
+
for (const [fp, info] of display2.slice(0, 40)) {
|
|
6320
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, fp);
|
|
6321
|
+
if (!abs || !(0, fs_1.existsSync)(abs))
|
|
6322
|
+
continue;
|
|
6323
|
+
let fileLines = [];
|
|
6324
|
+
try {
|
|
6325
|
+
fileLines = (0, fs_1.readFileSync)(abs, 'utf-8').split('\n');
|
|
6326
|
+
}
|
|
6327
|
+
catch {
|
|
6328
|
+
continue;
|
|
6329
|
+
}
|
|
6330
|
+
for (const ln of info.lines.slice(0, 12)) {
|
|
6331
|
+
const lineText = fileLines[ln - 1] ?? '';
|
|
6332
|
+
for (const m of lineText.matchAll(/\b((?:call|radio|data|sim|observer|telephony)(?:\.\w+)*)\.([A-Za-z_][\w]*)\s*\(/gi)) {
|
|
6333
|
+
if (m[2] && m[2].length >= 2)
|
|
6334
|
+
methodNames.add(`${m[1]}.${m[2]}`);
|
|
6335
|
+
}
|
|
6336
|
+
for (const sym of rawSymbols) {
|
|
6337
|
+
const esc = sym.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
6338
|
+
const re = new RegExp(`(?:@ohos\\.)?${esc}(?:\\.[A-Za-z_][\\w]*)*\\.([A-Za-z_][\\w]*)\\s*\\(`, 'g');
|
|
6339
|
+
for (const m of lineText.matchAll(re)) {
|
|
6340
|
+
if (m[1] && m[1].length >= 2)
|
|
6341
|
+
methodNames.add(`${sym}.${m[1]}`);
|
|
6342
|
+
}
|
|
6343
|
+
// import X from '@ohos.telephony.call' → module surface
|
|
6344
|
+
const imp = lineText.match(new RegExp(`from\\s+['"]@ohos\\.${esc}(?:\\.(\\w+))?['"]`, 'i'));
|
|
6345
|
+
if (imp)
|
|
6346
|
+
methodNames.add(imp[1] ? `@ohos.${sym.toLowerCase()}.${imp[1]}` : `@ohos.${sym.toLowerCase()}`);
|
|
6347
|
+
}
|
|
6348
|
+
}
|
|
6349
|
+
}
|
|
4264
6350
|
const lines = [
|
|
4265
6351
|
'**API usage sites**',
|
|
4266
6352
|
'',
|
|
4267
|
-
`>
|
|
6353
|
+
`> In-repo references to \`${rawSymbols.join('`, `')}\` (${display2.length} file(s)` +
|
|
6354
|
+
`${projectHits2.length > 0 && projectHits2.length < rankedFiles2.length ? `; SDK stubs omitted` : ''}). ` +
|
|
6355
|
+
'**ANSWER NOW** from this list (+ snippets); do not Grep the same API again.',
|
|
4268
6356
|
'',
|
|
4269
6357
|
];
|
|
6358
|
+
if (methodNames.size > 0) {
|
|
6359
|
+
lines.push('**Called / imported API surfaces**');
|
|
6360
|
+
for (const m of [...methodNames].sort().slice(0, 24))
|
|
6361
|
+
lines.push(`- \`${m}\``);
|
|
6362
|
+
if (methodNames.size > 24)
|
|
6363
|
+
lines.push(`- … and ${methodNames.size - 24} more`);
|
|
6364
|
+
lines.push('');
|
|
6365
|
+
}
|
|
4270
6366
|
let shown = 0;
|
|
4271
|
-
|
|
4272
|
-
|
|
6367
|
+
const isImportish = (s) => /^\s*import\b/.test(s) || /\bfrom\s+['"]@/.test(s);
|
|
6368
|
+
const isLifetimeish = (s) => /\b(?:new|delete|free|reset)\b/i.test(s) || /=\s*new\b/.test(s);
|
|
6369
|
+
for (const [fp, info] of display2) {
|
|
6370
|
+
if (shown >= 28)
|
|
4273
6371
|
break;
|
|
4274
|
-
const
|
|
6372
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, fp);
|
|
6373
|
+
let fileLines = [];
|
|
6374
|
+
if (abs && (0, fs_1.existsSync)(abs)) {
|
|
6375
|
+
try {
|
|
6376
|
+
fileLines = (0, fs_1.readFileSync)(abs, 'utf-8').split('\n');
|
|
6377
|
+
}
|
|
6378
|
+
catch { /* */ }
|
|
6379
|
+
}
|
|
6380
|
+
// Prefer call / new-delete lines over bare imports for the displayed snippet.
|
|
6381
|
+
const rankedLines = [...info.lines].sort((a, b) => {
|
|
6382
|
+
const ta = fileLines[a - 1] ?? '';
|
|
6383
|
+
const tb = fileLines[b - 1] ?? '';
|
|
6384
|
+
const score = (t) => {
|
|
6385
|
+
let s = 0;
|
|
6386
|
+
if (wantLifetime && isLifetimeish(t))
|
|
6387
|
+
s += 4;
|
|
6388
|
+
if (/\.\w+\s*\(/.test(t) && !isImportish(t))
|
|
6389
|
+
s += 3;
|
|
6390
|
+
if (isImportish(t))
|
|
6391
|
+
s -= 2;
|
|
6392
|
+
return s;
|
|
6393
|
+
};
|
|
6394
|
+
return score(tb) - score(ta) || a - b;
|
|
6395
|
+
});
|
|
6396
|
+
// For field new/delete, surface both sides when present (not only `new`).
|
|
6397
|
+
let sortedLines = rankedLines.slice(0, 4);
|
|
6398
|
+
if (wantLifetime && fileLines.length > 0) {
|
|
6399
|
+
const news = rankedLines.filter((l) => /\bnew\b/i.test(fileLines[l - 1] ?? ''));
|
|
6400
|
+
const dels = rankedLines.filter((l) => /\bdelete\b|\bfree\b|=\s*nullptr/i.test(fileLines[l - 1] ?? ''));
|
|
6401
|
+
sortedLines = [...new Set([...news.slice(0, 2), ...dels.slice(0, 2), ...rankedLines])].slice(0, 4);
|
|
6402
|
+
}
|
|
6403
|
+
let snippet = '';
|
|
6404
|
+
if (fileLines.length > 0) {
|
|
6405
|
+
const ln = sortedLines[0];
|
|
6406
|
+
snippet = (fileLines[ln - 1] ?? '').trim().slice(0, 100);
|
|
6407
|
+
if (wantLifetime && sortedLines.length > 1) {
|
|
6408
|
+
const sn2 = (fileLines[sortedLines[1] - 1] ?? '').trim().slice(0, 80);
|
|
6409
|
+
if (sn2 && sn2 !== snippet)
|
|
6410
|
+
snippet = `${snippet}` + ` | ${sn2}`;
|
|
6411
|
+
}
|
|
6412
|
+
}
|
|
4275
6413
|
const lineStr = sortedLines.map((l) => `L${l}`).join(', ');
|
|
4276
|
-
const more =
|
|
4277
|
-
lines.push(
|
|
6414
|
+
const more = info.lines.length > sortedLines.length ? ` +${info.lines.length - sortedLines.length} more` : '';
|
|
6415
|
+
lines.push(snippet
|
|
6416
|
+
? `- \`${fp}\` (${lineStr}${more}) \`${snippet}\``
|
|
6417
|
+
: `- \`${fp}\` (${lineStr}${more})`);
|
|
4278
6418
|
shown++;
|
|
4279
6419
|
}
|
|
4280
|
-
if (
|
|
4281
|
-
lines.push(`- … and ${
|
|
6420
|
+
if (display2.length > shown)
|
|
6421
|
+
lines.push(`- … and ${display2.length - shown} more file(s)`);
|
|
4282
6422
|
lines.push('');
|
|
4283
|
-
lines.push(`> API usage survey complete — **${
|
|
6423
|
+
lines.push(`> API usage survey complete — **${display2.length}** file(s). **ANSWER NOW.**`);
|
|
4284
6424
|
lines.push('');
|
|
4285
|
-
return { section: lines.join('\n'), fileCount:
|
|
6425
|
+
return { section: lines.join('\n'), fileCount: display2.length };
|
|
4286
6426
|
}
|
|
4287
6427
|
/**
|
|
4288
6428
|
* Domain file inventory — related files, usage existence, or concept comparison.
|
|
@@ -4445,12 +6585,37 @@ class ToolHandler {
|
|
|
4445
6585
|
const callerFiles = [...new Set(uniq.map((n) => rel(n.filePath)))];
|
|
4446
6586
|
const testFiles = callerFiles.filter((f) => (0, query_utils_1.isTestFile)(f));
|
|
4447
6587
|
const nonTest = callerFiles.filter((f) => !(0, query_utils_1.isTestFile)(f));
|
|
6588
|
+
// Indirect test coverage via one hop of callers (tests that call a
|
|
6589
|
+
// direct caller) when nothing calls the root from a test file.
|
|
6590
|
+
let indirectTests = [];
|
|
6591
|
+
if (testFiles.length === 0) {
|
|
6592
|
+
const seenTest = new Set();
|
|
6593
|
+
for (const c of uniq.slice(0, 8)) {
|
|
6594
|
+
let next = [];
|
|
6595
|
+
try {
|
|
6596
|
+
next = cg.getCallers(c.id);
|
|
6597
|
+
}
|
|
6598
|
+
catch {
|
|
6599
|
+
continue;
|
|
6600
|
+
}
|
|
6601
|
+
for (const n of next) {
|
|
6602
|
+
const fp = rel(n.node.filePath);
|
|
6603
|
+
if ((0, query_utils_1.isTestFile)(fp) && !seenTest.has(fp)) {
|
|
6604
|
+
seenTest.add(fp);
|
|
6605
|
+
indirectTests.push(fp);
|
|
6606
|
+
}
|
|
6607
|
+
}
|
|
6608
|
+
}
|
|
6609
|
+
indirectTests = indirectTests.slice(0, FILE_CAP);
|
|
6610
|
+
}
|
|
4448
6611
|
const shown = nonTest.slice(0, FILE_CAP).map((f) => `\`${f}\``).join(', ');
|
|
4449
6612
|
const more = nonTest.length > FILE_CAP ? ` +${nonTest.length - FILE_CAP} more` : '';
|
|
4450
6613
|
const where = nonTest.length > 0 ? ` in ${shown}${more}` : '';
|
|
4451
6614
|
const tests = testFiles.length > 0
|
|
4452
6615
|
? `; tests: ${testFiles.slice(0, FILE_CAP).map((f) => `\`${f}\``).join(', ')}${testFiles.length > FILE_CAP ? ` +${testFiles.length - FILE_CAP}` : ''}`
|
|
4453
|
-
:
|
|
6616
|
+
: indirectTests.length > 0
|
|
6617
|
+
? `; tests (via callers): ${indirectTests.map((f) => `\`${f}\``).join(', ')}`
|
|
6618
|
+
: '; ⚠️ no covering tests found';
|
|
4454
6619
|
entries.push(`- \`${root.name}\` (${rel(root.filePath)}:${root.startLine}) — ${uniq.length} caller${uniq.length === 1 ? '' : 's'}${where}${tests}`);
|
|
4455
6620
|
}
|
|
4456
6621
|
if (entries.length === 0)
|
|
@@ -4562,15 +6727,18 @@ class ToolHandler {
|
|
|
4562
6727
|
// One normalization point so the flow-builder, relevance search, and
|
|
4563
6728
|
// ranking all see the same canonical spelling (Erlang `mod:fn/arity`).
|
|
4564
6729
|
const query = normalizeQuerySpelling(rawQuery);
|
|
6730
|
+
const deferKind = (0, query_utils_1.queryShouldDeferToBuiltinTools)(query);
|
|
6731
|
+
if (deferKind) {
|
|
6732
|
+
return this.textResult((0, query_utils_1.homegraphDeferGuidance)(deferKind, query));
|
|
6733
|
+
}
|
|
4565
6734
|
const cg = this.getHomeGraph(args.projectPath);
|
|
4566
6735
|
const projectRoot = cg.getProjectRoot();
|
|
4567
6736
|
const compactLocal = this.tryFastInventoryExplore(cg, query, projectRoot)
|
|
6737
|
+
?? this.tryLightMechanismExplore(cg, query, projectRoot)
|
|
4568
6738
|
?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot);
|
|
4569
6739
|
if (compactLocal)
|
|
4570
6740
|
return compactLocal;
|
|
4571
|
-
|
|
4572
|
-
if (lightMechanism)
|
|
4573
|
-
return lightMechanism;
|
|
6741
|
+
// Light mechanism already attempted above.
|
|
4574
6742
|
// Resolve adaptive output budget from project size.
|
|
4575
6743
|
// largest-tier defaults if stats aren't available, which preserves
|
|
4576
6744
|
// pre-#185 behavior for callers that hit the rare stats failure.
|
|
@@ -4994,8 +7162,10 @@ class ToolHandler {
|
|
|
4994
7162
|
// Test/spec/icon/i18n file detector — used both for the pre-sort hard
|
|
4995
7163
|
// filter (tiny tier) and the comparator deprioritization (all tiers).
|
|
4996
7164
|
const isLowValue = (p) => {
|
|
4997
|
-
const lp = p.toLowerCase();
|
|
4998
|
-
return (
|
|
7165
|
+
const lp = p.toLowerCase().replace(/\\/g, '/');
|
|
7166
|
+
return (
|
|
7167
|
+
// Top-level test/ / spec/ as well as nested …/tests?/…
|
|
7168
|
+
/(?:^|\/)(tests?|specs?|__tests?__|testdata|mocks?|fixtures?)\//.test(lp) ||
|
|
4999
7169
|
/_test\.go$/.test(lp) ||
|
|
5000
7170
|
/(?:^|\/)test_[^/]+\.py$/.test(lp) ||
|
|
5001
7171
|
/_test\.py$/.test(lp) ||
|
|
@@ -5009,6 +7179,10 @@ class ToolHandler {
|
|
|
5009
7179
|
/\bicons?\b/.test(lp) ||
|
|
5010
7180
|
/\bi18n\b/.test(lp));
|
|
5011
7181
|
};
|
|
7182
|
+
/** Hand-written ambient `.d.ts` — down-rank for behavior/flow questions (CG-28). */
|
|
7183
|
+
const isAmbientDts = (p) => /\.d\.ts$/i.test(p.replace(/\\/g, '/'));
|
|
7184
|
+
/** Path convention OR persisted content-banner flag. */
|
|
7185
|
+
const isGeneratedCandidate = (fp) => this.isGeneratedCandidate(cg, fp);
|
|
5012
7186
|
// Hard-exclude test/spec files (ALL tiers, not just tiny). One slipped test
|
|
5013
7187
|
// file dominates the per-file budget on small repos (cobra's `command_test.go`
|
|
5014
7188
|
// displaced `args.go`) AND wastes budget on large ones (Django's
|
|
@@ -5198,16 +7372,17 @@ class ToolHandler {
|
|
|
5198
7372
|
const bLow = isLowValue(bPath);
|
|
5199
7373
|
if (aLow !== bLow)
|
|
5200
7374
|
return aLow ? 1 : -1;
|
|
5201
|
-
// Deprioritize generated source (
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
// the response (the cosmos Q3 explore otherwise leads with
|
|
5205
|
-
// `expected_keepers_mocks.go`, displacing the real `tally.go` content
|
|
5206
|
-
// and forcing the agent to Read tally.go anyway).
|
|
5207
|
-
const aGen = (0, generated_detection_1.isGeneratedFile)(a[0]);
|
|
5208
|
-
const bGen = (0, generated_detection_1.isGeneratedFile)(b[0]);
|
|
7375
|
+
// Deprioritize generated source (path suffixes OR content banner).
|
|
7376
|
+
const aGen = isGeneratedCandidate(a[0]);
|
|
7377
|
+
const bGen = isGeneratedCandidate(b[0]);
|
|
5209
7378
|
if (aGen !== bGen)
|
|
5210
7379
|
return aGen ? 1 : -1;
|
|
7380
|
+
// Hand-written ambient `.d.ts` — demote unless the agent named a type
|
|
7381
|
+
// defined there (behavior questions want implementations, not typings).
|
|
7382
|
+
const aAmbient = isAmbientDts(a[0]) && !namedSeedFiles.has(a[0]) ? 1 : 0;
|
|
7383
|
+
const bAmbient = isAmbientDts(b[0]) && !namedSeedFiles.has(b[0]) ? 1 : 0;
|
|
7384
|
+
if (aAmbient !== bAmbient)
|
|
7385
|
+
return aAmbient - bAmbient;
|
|
5211
7386
|
if (a[1].score !== b[1].score)
|
|
5212
7387
|
return b[1].score - a[1].score;
|
|
5213
7388
|
return b[1].nodes.length - a[1].nodes.length;
|
|
@@ -5231,13 +7406,17 @@ class ToolHandler {
|
|
|
5231
7406
|
'production handlers are out of scope unless explicitly referenced in the test.');
|
|
5232
7407
|
lines.push('');
|
|
5233
7408
|
}
|
|
5234
|
-
const importResult =
|
|
7409
|
+
const importResult = (0, query_utils_1.queryAsksKitInstallDeps)(query)
|
|
7410
|
+
? { section: '', siteCount: 0, compactListing: false }
|
|
7411
|
+
: this.buildImportSitesSection(cg, query, projectRoot);
|
|
5235
7412
|
if (importResult.section)
|
|
5236
7413
|
lines.push(importResult.section);
|
|
5237
7414
|
const homonymSection = this.buildHomonymDefinitionsSection(cg, query);
|
|
5238
7415
|
if (homonymSection)
|
|
5239
7416
|
lines.push(homonymSection);
|
|
5240
7417
|
const kitUsageResult = (0, query_utils_1.shouldBuildKitModuleUsageSurvey)(query)
|
|
7418
|
+
&& ((0, query_utils_1.queryAsksKitInstallDeps)(query)
|
|
7419
|
+
|| !(importResult.compactListing && importResult.siteCount > 0))
|
|
5241
7420
|
? this.buildKitModuleUsageSection(cg, query, projectRoot)
|
|
5242
7421
|
: { section: '', symbolCount: 0 };
|
|
5243
7422
|
if (kitUsageResult.section)
|
|
@@ -5248,6 +7427,7 @@ class ToolHandler {
|
|
|
5248
7427
|
if (domainFileResult.section)
|
|
5249
7428
|
lines.push(domainFileResult.section);
|
|
5250
7429
|
const apiUsageResult = (0, query_utils_1.shouldBuildApiUsageSurvey)(query)
|
|
7430
|
+
&& !(0, query_utils_1.shouldBuildKitModuleUsageSurvey)(query)
|
|
5251
7431
|
? this.buildApiUsageSection(cg, query, projectRoot)
|
|
5252
7432
|
: { section: '', fileCount: 0 };
|
|
5253
7433
|
if (apiUsageResult.section)
|
|
@@ -5293,9 +7473,14 @@ class ToolHandler {
|
|
|
5293
7473
|
const localDetail = (0, query_utils_1.queryAsLocalSymbolDetail)(query);
|
|
5294
7474
|
const inheritanceSection = !hasFlowPath && !multiAnchor
|
|
5295
7475
|
? this.buildInheritanceSurveySection(cg, query) : '';
|
|
5296
|
-
const
|
|
5297
|
-
|
|
7476
|
+
const listedTypeMethods = (0, query_utils_1.extractListedTypeMethodsFromQuery)(query).length > 0;
|
|
7477
|
+
// Type + listed methods (BinaryGrid Set/Test/Fill) must keep caller inventory even
|
|
7478
|
+
// when multi-anchor would otherwise skip it into a fat source dump.
|
|
7479
|
+
const callerSection = !hasFlowPath && (!multiAnchor || listedTypeMethods) && (0, query_utils_1.shouldBuildCallerInventory)(query)
|
|
7480
|
+
? this.buildCallerListingSection(cg, query, projectRoot) : '';
|
|
7481
|
+
// Field new/delete inventories already list text sites — skip redundant member scan.
|
|
5298
7482
|
const memberSection = !hasFlowPath && !multiAnchor && (0, query_utils_1.shouldBuildMemberSurvey)(query)
|
|
7483
|
+
&& !((0, query_utils_1.queryAsFieldUsageSurvey)(query) && apiUsageResult.fileCount > 0)
|
|
5299
7484
|
? this.buildMemberSurveySection(cg, query, projectRoot) : '';
|
|
5300
7485
|
const configSection = (0, query_utils_1.shouldBuildConfigSection)(query)
|
|
5301
7486
|
? this.buildConfigFileSection(cg, query, projectRoot) : '';
|
|
@@ -5337,28 +7522,32 @@ class ToolHandler {
|
|
|
5337
7522
|
return finishCompact('Config/manifest content above — answer from it directly.');
|
|
5338
7523
|
}
|
|
5339
7524
|
if (kitUsageResult.section) {
|
|
5340
|
-
return finishCompact(`Kit module usage survey — **${kitUsageResult.symbolCount}** imported symbol(s)
|
|
7525
|
+
return finishCompact(`Kit module **in-repo usage** survey — **${kitUsageResult.symbolCount}** imported symbol(s). ` +
|
|
7526
|
+
'Not an SDK catalog. **ANSWER NOW** from the usage list; do not Grep the same `@kit` path.');
|
|
5341
7527
|
}
|
|
5342
7528
|
if (domainFileResult.section && domainFileResult.fileCount > 0) {
|
|
5343
|
-
return finishCompact(`Domain file survey — **${domainFileResult.fileCount}** related file(s) listed above.
|
|
7529
|
+
return finishCompact(`Domain file survey — **${domainFileResult.fileCount}** related file(s) listed above. **ANSWER NOW.**`);
|
|
5344
7530
|
}
|
|
5345
7531
|
if (apiUsageResult.section && apiUsageResult.fileCount > 0) {
|
|
5346
|
-
return finishCompact(`API usage survey — **${apiUsageResult.fileCount}** file(s)
|
|
7532
|
+
return finishCompact(`API usage survey — **${apiUsageResult.fileCount}** file(s). **ANSWER NOW** from the list above.`);
|
|
7533
|
+
}
|
|
7534
|
+
if (listedTypeMethods && callerBulletCount > 0) {
|
|
7535
|
+
return finishCompact(`Caller inventory — **${callerBulletCount}** site(s) for listed methods. **ANSWER NOW**; do not fan out callers per method.`);
|
|
5347
7536
|
}
|
|
5348
7537
|
if (dataSourceResult.section && dataSourceResult.edgeCount > 0) {
|
|
5349
|
-
return finishCompact(`Data-source survey — **${dataSourceResult.edgeCount}** upstream symbol(s)
|
|
7538
|
+
return finishCompact(`Data-source survey — **${dataSourceResult.edgeCount}** upstream symbol(s). **ANSWER NOW.**`);
|
|
5350
7539
|
}
|
|
5351
7540
|
if (inheritanceListed) {
|
|
5352
|
-
return finishCompact('Inheritance survey above lists all direct subtypes found —
|
|
7541
|
+
return finishCompact('Inheritance survey above lists all direct subtypes found. **ANSWER NOW** — do not grep `extends`.');
|
|
5353
7542
|
}
|
|
5354
7543
|
if (importResult.compactListing) {
|
|
5355
|
-
return finishCompact(`Listed **${importResult.siteCount}** import site(s)
|
|
7544
|
+
return finishCompact(`Listed **${importResult.siteCount}** import site(s). **ANSWER NOW** from the dependency list above.`);
|
|
5356
7545
|
}
|
|
5357
7546
|
if (callerBulletCount >= 1) {
|
|
5358
|
-
return finishCompact(`Caller inventory above lists **${callerBulletCount}** method(s)
|
|
7547
|
+
return finishCompact(`Caller inventory above lists **${callerBulletCount}** method(s). **ANSWER NOW.**`);
|
|
5359
7548
|
}
|
|
5360
7549
|
if (memberSection && memberFileCount >= 2) {
|
|
5361
|
-
return finishCompact(`Member/pattern usage in **${memberFileCount}** file(s)
|
|
7550
|
+
return finishCompact(`Member/pattern usage in **${memberFileCount}** file(s). **ANSWER NOW** from the inventory above.`);
|
|
5362
7551
|
}
|
|
5363
7552
|
}
|
|
5364
7553
|
// Blast radius only for structural flow / mechanism answers — skip on local-detail
|
|
@@ -5458,6 +7647,9 @@ class ToolHandler {
|
|
|
5458
7647
|
};
|
|
5459
7648
|
lines.push('**Source Code**');
|
|
5460
7649
|
lines.push('');
|
|
7650
|
+
// Recorded so the drift pass below (#1474) can append a per-file exception
|
|
7651
|
+
// to this guarantee after the render loop knows which files drifted.
|
|
7652
|
+
const verbatimHeaderIdx = lines.length;
|
|
5461
7653
|
lines.push('> Line-numbered source — treat as already Read. Answer from Flow + Source below when you can; ' +
|
|
5462
7654
|
'do not re-read/grep these files, and do not fan out `homegraph_node` / search for symbols already shown. ' +
|
|
5463
7655
|
'Another `homegraph_explore` with tighter names only if a needed symbol is missing.');
|
|
@@ -5468,6 +7660,28 @@ class ToolHandler {
|
|
|
5468
7660
|
// (#1046) — it must reflect what we show, not the raw candidate gather.
|
|
5469
7661
|
const renderedFilePaths = [];
|
|
5470
7662
|
let anyFileTrimmed = false;
|
|
7663
|
+
// Files that changed on disk after their last index sync (#1474).
|
|
7664
|
+
const staleRendered = [];
|
|
7665
|
+
const staleOmitted = [];
|
|
7666
|
+
// What this session has already been served for THIS project (CG-17), and
|
|
7667
|
+
// whether this call may act on it (CG-18). Dedup is off on the session's
|
|
7668
|
+
// first call by construction — there is nothing to point back AT — and off
|
|
7669
|
+
// entirely under `HOMEGRAPH_EXPLORE_DEDUP=0`.
|
|
7670
|
+
const priorCalls = (0, explore_session_state_1.viewForProject)((0, explore_session_state_1.readExploreSessionView)(args), projectRoot);
|
|
7671
|
+
const dedupEnabled = (0, explore_dedup_1.exploreDedupEnabled)() && (priorCalls?.calls.length ?? 0) > 0;
|
|
7672
|
+
const emittedByFile = new Map();
|
|
7673
|
+
const noteEmitted = (fp, ranges, bytes, fingerprint) => {
|
|
7674
|
+
const existing = emittedByFile.get(fp);
|
|
7675
|
+
if (existing) {
|
|
7676
|
+
existing.ranges.push(...ranges);
|
|
7677
|
+
existing.bytes += bytes;
|
|
7678
|
+
if (fingerprint)
|
|
7679
|
+
existing.fingerprint = fingerprint;
|
|
7680
|
+
}
|
|
7681
|
+
else {
|
|
7682
|
+
emittedByFile.set(fp, { ranges: [...ranges], bytes, fingerprint });
|
|
7683
|
+
}
|
|
7684
|
+
};
|
|
5471
7685
|
const limitSingleFile = (0, query_utils_1.shouldLimitToQueryNamedFile)(query, hasFlowPath, multiAnchor)
|
|
5472
7686
|
|| (interpretationQuery && queryFileBasenames.length === 1);
|
|
5473
7687
|
let filesToRender = sortedFiles;
|
|
@@ -5553,6 +7767,49 @@ class ToolHandler {
|
|
|
5553
7767
|
}
|
|
5554
7768
|
const fileLines = fileContent.split('\n');
|
|
5555
7769
|
const lang = group.nodes[0]?.language || '';
|
|
7770
|
+
// Disk-drift gate (#1474): every render branch below except whole-file
|
|
7771
|
+
// slices fileContent (CURRENT bytes) at INDEXED line ranges.
|
|
7772
|
+
const fileStale = this.isFileStaleOnDisk(cg, filePath, fileContent);
|
|
7773
|
+
// Cross-call dedup (CG-18). `served` is what THIS session already sent the
|
|
7774
|
+
// agent for THIS file, and it is empty unless the file still hashes to the
|
|
7775
|
+
// bytes those spans were sliced from — an edit between calls means the
|
|
7776
|
+
// agent's copy is wrong, so nothing is withheld.
|
|
7777
|
+
const fingerprint = (0, explore_dedup_1.fileFingerprint)(fileContent);
|
|
7778
|
+
const served = dedupEnabled ? (0, explore_dedup_1.servedRangesForFile)(priorCalls, filePath, fingerprint) : [];
|
|
7779
|
+
const withLineNumbers = exploreLineNumbersEnabled();
|
|
7780
|
+
const renderSpan = (r) => {
|
|
7781
|
+
const slice = fileLines.slice(r.start - 1, r.end).join('\n');
|
|
7782
|
+
return withLineNumbers ? numberSourceLines(slice, r.start) : slice;
|
|
7783
|
+
};
|
|
7784
|
+
/**
|
|
7785
|
+
* Emit one file's section — header, optional back-reference for held spans,
|
|
7786
|
+
* and the fence for what is new. Fully-held files become pointers and do
|
|
7787
|
+
* not consume a `maxFiles` slot (budget goes to unseen files).
|
|
7788
|
+
*/
|
|
7789
|
+
const emitFileSection = (opts) => {
|
|
7790
|
+
const folded = opts.covered.length > 0 && opts.body.length < explore_dedup_1.EXPLORE_DEDUP.MIN_DELTA_CHARS;
|
|
7791
|
+
const body = folded ? '' : opts.body;
|
|
7792
|
+
const ranges = folded ? [] : opts.ranges;
|
|
7793
|
+
lines.push(opts.header, '');
|
|
7794
|
+
totalChars += opts.header.length + 2;
|
|
7795
|
+
if (opts.covered.length > 0) {
|
|
7796
|
+
const pointer = (0, explore_dedup_1.formatBackReference)(filePath, opts.covered, (0, explore_dedup_1.symbolsInSpans)(group.nodes, opts.covered), { partial: body.length > 0 });
|
|
7797
|
+
lines.push(pointer, '');
|
|
7798
|
+
totalChars += pointer.length + 2;
|
|
7799
|
+
}
|
|
7800
|
+
if (body.length > 0) {
|
|
7801
|
+
lines.push('```' + lang, body, '```', '');
|
|
7802
|
+
totalChars += body.length + lang.length + 11;
|
|
7803
|
+
noteEmitted(filePath, [...ranges, ...opts.covered], body.length, fingerprint);
|
|
7804
|
+
renderedFilePaths.push(filePath);
|
|
7805
|
+
filesIncluded++;
|
|
7806
|
+
return;
|
|
7807
|
+
}
|
|
7808
|
+
// Fully held — pointer only; still record spans at zero bytes.
|
|
7809
|
+
noteEmitted(filePath, opts.covered, 0, fingerprint);
|
|
7810
|
+
renderedFilePaths.push(filePath);
|
|
7811
|
+
// Do not increment filesIncluded: slot goes to a file the agent has not seen.
|
|
7812
|
+
};
|
|
5556
7813
|
// Adaptive sizing (HOMEGRAPH_ADAPTIVE_EXPLORE, default on): collapse a file
|
|
5557
7814
|
// to a per-symbol view when it's a redundant member of a polymorphic family.
|
|
5558
7815
|
// Engages iff ALL hold:
|
|
@@ -5591,7 +7848,7 @@ class ToolHandler {
|
|
|
5591
7848
|
const onSpineGodFile = hasSpineNode
|
|
5592
7849
|
&& namedBodyChars > budget.maxCharsPerFile
|
|
5593
7850
|
&& group.nodes.some(n => CALLABLE_BODY.has(n.kind) && flow.uniqueNamedNodeIds.has(n.id) && !flow.pathNodeIds.has(n.id));
|
|
5594
|
-
if (adaptiveExploreEnabled() && flow.pathNodeIds.size > 0
|
|
7851
|
+
if (!fileStale && adaptiveExploreEnabled() && flow.pathNodeIds.size > 0
|
|
5595
7852
|
&& (onSpineGodFile || (!hasSpineNode && isPolymorphicSibling(group.nodes) && !spared))) {
|
|
5596
7853
|
const syms = group.nodes
|
|
5597
7854
|
.filter(n => n.kind !== 'import' && n.kind !== 'export' && n.startLine > 0)
|
|
@@ -5673,12 +7930,32 @@ class ToolHandler {
|
|
|
5673
7930
|
// a Read of the very file just skeletonized; on a central, wanted file
|
|
5674
7931
|
// (Session.swift, DataRequest.swift) that fired an over-investigation
|
|
5675
7932
|
// spiral (the agent Read the skeletonized file, then kept digging).
|
|
5676
|
-
//
|
|
7933
|
+
// AGENTS.md: explore output must never tell the agent to Read.
|
|
5677
7934
|
const tag = bodyIds.size > 0
|
|
5678
7935
|
? 'focused (the methods you named in full, the rest as signatures — homegraph_explore a signature by name for its body; do NOT Read)'
|
|
5679
7936
|
: 'skeleton (signatures only — homegraph_explore a name for its full body; do NOT Read)';
|
|
5680
|
-
|
|
5681
|
-
|
|
7937
|
+
const skelHeader = fileSectionHeader(filePath, `${names} · ${tag}`);
|
|
7938
|
+
// If this session already holds the whole file, replace the skeleton
|
|
7939
|
+
// with a pointer rather than re-serving signatures/bodies.
|
|
7940
|
+
const wholeRange = { start: 1, end: Math.max(1, fileLines.length) };
|
|
7941
|
+
const ddWhole = (0, explore_dedup_1.dedupeRange)(wholeRange, served);
|
|
7942
|
+
if (ddWhole.covered.length > 0 && ddWhole.emit.length === 0) {
|
|
7943
|
+
emitFileSection({
|
|
7944
|
+
header: skelHeader,
|
|
7945
|
+
body: '',
|
|
7946
|
+
ranges: [],
|
|
7947
|
+
covered: ddWhole.covered,
|
|
7948
|
+
});
|
|
7949
|
+
continue;
|
|
7950
|
+
}
|
|
7951
|
+
const skelBody = skel.join('\n');
|
|
7952
|
+
const bodyRanges = [...bodyIds].map((id) => {
|
|
7953
|
+
const n = group.nodes.find((x) => x.id === id);
|
|
7954
|
+
return n ? { start: n.startLine, end: n.endLine || n.startLine } : null;
|
|
7955
|
+
}).filter((r) => !!r);
|
|
7956
|
+
lines.push(skelHeader, '', '```' + lang, skelBody, '```', '');
|
|
7957
|
+
totalChars += skelBody.length + 120;
|
|
7958
|
+
noteEmitted(filePath, bodyRanges.length > 0 ? bodyRanges : [wholeRange], skelBody.length, fingerprint);
|
|
5682
7959
|
renderedFilePaths.push(filePath);
|
|
5683
7960
|
filesIncluded++;
|
|
5684
7961
|
continue;
|
|
@@ -5709,25 +7986,45 @@ class ToolHandler {
|
|
|
5709
7986
|
? Math.min(Math.max(0, budget.maxOutputChars - totalChars - 200), Math.round(budget.maxCharsPerFile * 1.5))
|
|
5710
7987
|
: budget.maxCharsPerFile * 3;
|
|
5711
7988
|
if (fileLines.length <= WHOLE_FILE_MAX_LINES && fileContent.length <= WHOLE_FILE_MAX_CHARS) {
|
|
5712
|
-
const
|
|
5713
|
-
|
|
7989
|
+
const wholeRange = { start: 1, end: Math.max(1, fileLines.length) };
|
|
7990
|
+
const dd = (0, explore_dedup_1.dedupeRange)(wholeRange, served);
|
|
5714
7991
|
const uniqSymbols = [...new Set(group.nodes
|
|
5715
7992
|
.filter(n => n.kind !== 'import' && n.kind !== 'export')
|
|
5716
7993
|
.map(n => `${n.name}(${n.kind})`))];
|
|
5717
7994
|
const headerNames = uniqSymbols.slice(0, budget.maxSymbolsInFileHeader);
|
|
5718
7995
|
const omitted = uniqSymbols.length - headerNames.length;
|
|
5719
|
-
|
|
5720
|
-
|
|
7996
|
+
// A drifted file rendered WHOLE is still correct (current bytes) —
|
|
7997
|
+
// only index-derived symbol list / line refs may be shifted (#1474).
|
|
7998
|
+
const staleSuffix = fileStale ? ' · ⚠ changed since last index sync — source below is current; the symbol list may be outdated' : '';
|
|
7999
|
+
const wholeHeader = fileSectionHeader(filePath, (omitted > 0 ? `${headerNames.join(', ')}, +${omitted} more` : headerNames.join(', ')) + staleSuffix);
|
|
8000
|
+
const emitParts = dd.emit.length > 0
|
|
8001
|
+
? dd.emit.map((r) => ({ range: r, text: renderSpan(r) }))
|
|
8002
|
+
: [];
|
|
8003
|
+
const wholeSection = emitParts.map((p) => p.text).join('\n');
|
|
8004
|
+
const emitRanges = emitParts.map((p) => p.range);
|
|
8005
|
+
if (!fileNecessary && totalChars + Math.max(wholeSection.length, 80) + 200 > budget.maxOutputChars && dd.covered.length === 0) {
|
|
5721
8006
|
// Don't slice a whole file mid-method: an incidental file that doesn't
|
|
5722
8007
|
// fit is skipped; a necessary one (below) renders in full. Half a file
|
|
5723
8008
|
// forces the Read this is meant to prevent.
|
|
5724
8009
|
anyFileTrimmed = true;
|
|
5725
8010
|
continue;
|
|
5726
8011
|
}
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
8012
|
+
emitFileSection({
|
|
8013
|
+
header: wholeHeader,
|
|
8014
|
+
body: wholeSection,
|
|
8015
|
+
ranges: emitRanges,
|
|
8016
|
+
covered: dd.covered,
|
|
8017
|
+
});
|
|
8018
|
+
if (fileStale && wholeSection.length > 0)
|
|
8019
|
+
staleRendered.push(filePath);
|
|
8020
|
+
continue;
|
|
8021
|
+
}
|
|
8022
|
+
// Drifted file too big for the whole-file window (#1474): omit rather
|
|
8023
|
+
// than risk a mis-sliced cluster/skeleton render.
|
|
8024
|
+
if (fileStale) {
|
|
8025
|
+
staleOmitted.push(filePath);
|
|
8026
|
+
lines.push(fileSectionHeader(filePath, '⚠ changed on disk after the last index sync — source omitted (indexed line ranges no longer match, so a slice could show the wrong code). Read this file directly for current content; the change is picked up on that project\'s next index sync.'), '');
|
|
8027
|
+
totalChars += 260;
|
|
5731
8028
|
continue;
|
|
5732
8029
|
}
|
|
5733
8030
|
// Cluster nearby symbols to avoid reading huge gaps between distant symbols.
|
|
@@ -5887,7 +8184,6 @@ class ToolHandler {
|
|
|
5887
8184
|
// until the per-file char cap is hit. Truly enormous single clusters
|
|
5888
8185
|
// get tail-trimmed with a marker.
|
|
5889
8186
|
const contextPadding = 3;
|
|
5890
|
-
const withLineNumbers = exploreLineNumbersEnabled();
|
|
5891
8187
|
// Language-neutral separator (no `//` — not a comment in Python, Ruby,
|
|
5892
8188
|
// etc.). With line numbers on, the line-number jump also signals the gap.
|
|
5893
8189
|
const GAP_MARKER = '\n\n... (gap) ...\n\n';
|
|
@@ -5898,29 +8194,38 @@ class ToolHandler {
|
|
|
5898
8194
|
// the exact gap this closes. Bounded, so a god-method can't blow the budget yet
|
|
5899
8195
|
// the spine's call still appears in context.
|
|
5900
8196
|
const OVERSIZE_SPINE_LINES = 200;
|
|
8197
|
+
const OVERSIZE_BODY_LINES = 160;
|
|
5901
8198
|
const SPINE_WINDOW = 28; // lines each side of the next-hop call site
|
|
5902
|
-
const
|
|
5903
|
-
|
|
8199
|
+
const BODY_WINDOW = 40;
|
|
8200
|
+
const buildSectionRange = (c) => {
|
|
8201
|
+
const span = c.end - c.start + 1;
|
|
8202
|
+
if (c.hasSpine && c.spineCallLine && span > OVERSIZE_SPINE_LINES) {
|
|
5904
8203
|
const call = c.spineCallLine;
|
|
5905
8204
|
const winStart = Math.max(c.start, call - SPINE_WINDOW);
|
|
5906
8205
|
const winEnd = Math.min(c.end, call + SPINE_WINDOW);
|
|
5907
|
-
const
|
|
5908
|
-
// Signature head, only when it sits clearly above the window (else the
|
|
5909
|
-
// window already covers the method opening).
|
|
8206
|
+
const ranges = [];
|
|
5910
8207
|
const headEnd = Math.min(c.start + 4, winStart - 2);
|
|
5911
|
-
if (headEnd >= c.start)
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
8208
|
+
if (headEnd >= c.start)
|
|
8209
|
+
ranges.push({ start: c.start, end: headEnd });
|
|
8210
|
+
ranges.push({ start: winStart, end: winEnd });
|
|
8211
|
+
return ranges;
|
|
8212
|
+
}
|
|
8213
|
+
// Long non-spine functions get a bounded window, not a full dump.
|
|
8214
|
+
if (!c.hasSpine && span > OVERSIZE_BODY_LINES) {
|
|
8215
|
+
const headEnd = Math.min(c.end, c.start + BODY_WINDOW);
|
|
8216
|
+
const midStart = Math.max(headEnd + 1, Math.floor((c.start + c.end) / 2) - Math.floor(BODY_WINDOW / 2));
|
|
8217
|
+
const midEnd = Math.min(c.end, midStart + BODY_WINDOW);
|
|
8218
|
+
const ranges = [{ start: c.start, end: headEnd }];
|
|
8219
|
+
if (midStart > headEnd)
|
|
8220
|
+
ranges.push({ start: midStart, end: midEnd });
|
|
8221
|
+
return ranges;
|
|
8222
|
+
}
|
|
8223
|
+
const start = Math.max(1, c.start - contextPadding);
|
|
8224
|
+
const end = Math.min(fileLines.length, c.end + contextPadding);
|
|
8225
|
+
return [{ start, end }];
|
|
8226
|
+
};
|
|
8227
|
+
const buildSection = (c) => {
|
|
8228
|
+
return buildSectionRange(c).map((r) => renderSpan(r)).join(GAP_MARKER);
|
|
5924
8229
|
};
|
|
5925
8230
|
// Rank clusters for inclusion under the per-file cap. Entry-point
|
|
5926
8231
|
// clusters come first: a cluster containing a query entry point
|
|
@@ -5951,18 +8256,19 @@ class ToolHandler {
|
|
|
5951
8256
|
return b.c.score - a.c.score;
|
|
5952
8257
|
return a.span - b.span;
|
|
5953
8258
|
});
|
|
5954
|
-
// Per-file budget
|
|
5955
|
-
//
|
|
5956
|
-
//
|
|
5957
|
-
//
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
const
|
|
8259
|
+
// Per-file budget fairness: reserve a fair share of remaining envelope for
|
|
8260
|
+
// files still waiting, so the first file cannot steal later files' budget.
|
|
8261
|
+
// Still capped by maxCharsPerFile; necessary/spine files can exceed via
|
|
8262
|
+
// SPINE_CEILING below.
|
|
8263
|
+
const filesLeft = Math.max(1, Math.min(maxFiles - filesIncluded, filesToRender.length - filesIncluded));
|
|
8264
|
+
const remainingEnvelope = Math.max(0, budget.maxOutputChars - totalChars - 200);
|
|
8265
|
+
const fairShare = Math.floor(remainingEnvelope / filesLeft);
|
|
8266
|
+
const fileBudget = Math.min(budget.maxCharsPerFile, Math.max(fairShare, Math.min(budget.maxCharsPerFile, remainingEnvelope)));
|
|
5961
8267
|
// Spine ceiling: a flow-path cluster may exceed the per-file cap (the call
|
|
5962
8268
|
// path is the answer), but bounded — at most ~2.5× the per-file cap and never
|
|
5963
8269
|
// past what's left of the total output cap — so a pathological long in-file
|
|
5964
8270
|
// spine can't run away or starve co-flow files entirely.
|
|
5965
|
-
const SPINE_CEILING = Math.min(budget.maxCharsPerFile * 2.5,
|
|
8271
|
+
const SPINE_CEILING = Math.min(budget.maxCharsPerFile * 2.5, remainingEnvelope);
|
|
5966
8272
|
const chosenIndices = new Set();
|
|
5967
8273
|
let projectedChars = 0;
|
|
5968
8274
|
for (const rc of rankedClusters) {
|
|
@@ -6041,21 +8347,46 @@ class ToolHandler {
|
|
|
6041
8347
|
anyFileTrimmed = true;
|
|
6042
8348
|
continue;
|
|
6043
8349
|
}
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
8350
|
+
// Apply session history to the chosen clusters' spans (CG-18).
|
|
8351
|
+
const intendedRanges = [];
|
|
8352
|
+
for (let i = 0; i < clusters.length; i++) {
|
|
8353
|
+
if (!chosenIndices.has(i))
|
|
8354
|
+
continue;
|
|
8355
|
+
intendedRanges.push(...buildSectionRange(clusters[i]));
|
|
8356
|
+
}
|
|
8357
|
+
const coveredParts = [];
|
|
8358
|
+
const emitParts = [];
|
|
8359
|
+
for (const range of intendedRanges) {
|
|
8360
|
+
const split = (0, explore_dedup_1.dedupeRange)(range, served);
|
|
8361
|
+
coveredParts.push(...split.covered);
|
|
8362
|
+
for (const r of split.emit)
|
|
8363
|
+
emitParts.push({ range: r, text: renderSpan(r) });
|
|
8364
|
+
}
|
|
8365
|
+
const dedupedBody = emitParts.map((p) => p.text).join(GAP_MARKER);
|
|
8366
|
+
const dedupedRanges = emitParts.map((p) => p.range);
|
|
8367
|
+
const covered = (0, explore_dedup_1.mergeRanges)(coveredParts);
|
|
8368
|
+
emitFileSection({
|
|
8369
|
+
header: fileHeader,
|
|
8370
|
+
body: dedupedBody.length > 0 ? dedupedBody : (covered.length > 0 ? '' : fileSection),
|
|
8371
|
+
ranges: dedupedRanges.length > 0 ? dedupedRanges : (covered.length > 0 ? [] : intendedRanges),
|
|
8372
|
+
covered,
|
|
8373
|
+
});
|
|
6053
8374
|
}
|
|
6054
8375
|
// The curated header count is computed from the files that SURVIVE the final
|
|
6055
8376
|
// truncation (see end of method) — `filesIncluded` can over-count when the
|
|
6056
8377
|
// hard ceiling drops trailing sections — so leave a sentinel here and fill it
|
|
6057
8378
|
// in once the output is final.
|
|
6058
8379
|
lines[summaryLineIdx] = SUMMARY_SENTINEL;
|
|
8380
|
+
// Drift epilogue (#1474). Everything actually rendered is still byte-accurate
|
|
8381
|
+
// (drifted files ship whole or not at all), but omitted files need Reading and
|
|
8382
|
+
// index-derived line references to drifted files may be shifted.
|
|
8383
|
+
if (staleOmitted.length > 0) {
|
|
8384
|
+
lines[verbatimHeaderIdx] += ' (Exception: files flagged "⚠ changed on disk" below drifted from the index after their last sync — their source is omitted rather than risk a mis-sliced block; Read those specific files.)';
|
|
8385
|
+
}
|
|
8386
|
+
const staleAll = [...new Set([...staleOmitted, ...staleRendered])];
|
|
8387
|
+
if (staleAll.length > 0) {
|
|
8388
|
+
lines.push('', `> ⚠ Changed on disk after the last index sync: ${staleAll.join(', ')}. Line numbers referencing ${staleAll.length === 1 ? 'this file' : 'these files'} elsewhere in this response (flow steps, blast radius, symbol lists) may be shifted until that project's next sync re-indexes ${staleAll.length === 1 ? 'it' : 'them'}.`);
|
|
8389
|
+
}
|
|
6059
8390
|
// Add remaining files as references (from both relevant and peripheral files).
|
|
6060
8391
|
// Small projects (per budget) skip this — the relevant story already fits
|
|
6061
8392
|
// in the source section, and a trailing pointer list is pure overhead.
|
|
@@ -6096,7 +8427,9 @@ class ToolHandler {
|
|
|
6096
8427
|
const stats = cg.getStats();
|
|
6097
8428
|
const callBudget = getExploreBudget(stats.fileCount);
|
|
6098
8429
|
lines.push('');
|
|
6099
|
-
lines.push(`> **
|
|
8430
|
+
lines.push(`> **Usually one explore is enough** (budget ≤${callBudget} for ${stats.fileCount.toLocaleString()} files). ` +
|
|
8431
|
+
'Answer from Flow + Source above. Extra explore/`homegraph_node`/Read for the **same** symbols multiplies tokens — ' +
|
|
8432
|
+
'only call again for a missing **named** symbol.');
|
|
6100
8433
|
}
|
|
6101
8434
|
catch {
|
|
6102
8435
|
// Stats unavailable — skip budget note
|
|
@@ -6114,16 +8447,54 @@ class ToolHandler {
|
|
|
6114
8447
|
const hardCeiling = Math.min(Math.round(budget.maxOutputChars * 1.5), 25000);
|
|
6115
8448
|
let finalText;
|
|
6116
8449
|
if (output.length > hardCeiling) {
|
|
6117
|
-
//
|
|
6118
|
-
//
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
8450
|
+
// Prefer dropping trailing notes ("Not shown above", completeness, budget)
|
|
8451
|
+
// over dropping a whole file's source — notes are recoverable, source isn't.
|
|
8452
|
+
let trimmed = output;
|
|
8453
|
+
const noteMarkers = [
|
|
8454
|
+
'\n**Not shown above — explore these names for their source**',
|
|
8455
|
+
'\n---\n> **Complete source for ',
|
|
8456
|
+
'\n> Some file sections were trimmed for size.',
|
|
8457
|
+
'\n> **Usually one explore is enough**',
|
|
8458
|
+
];
|
|
8459
|
+
for (const marker of noteMarkers) {
|
|
8460
|
+
if (trimmed.length <= hardCeiling)
|
|
8461
|
+
break;
|
|
8462
|
+
const at = trimmed.lastIndexOf(marker);
|
|
8463
|
+
if (at > hardCeiling * 0.4)
|
|
8464
|
+
trimmed = trimmed.slice(0, at).replace(/\n+$/, '');
|
|
8465
|
+
}
|
|
8466
|
+
if (trimmed.length > hardCeiling) {
|
|
8467
|
+
// Cut at a FILE-SECTION boundary so we drop whole trailing file-sections
|
|
8468
|
+
// rather than slicing through a method body. Prefer dropping sections for
|
|
8469
|
+
// files that are NOT agent-named / spine-necessary when possible.
|
|
8470
|
+
const cut = trimmed.slice(0, hardCeiling);
|
|
8471
|
+
let boundary = -1;
|
|
8472
|
+
let searchFrom = cut.length;
|
|
8473
|
+
while (searchFrom > hardCeiling * 0.5) {
|
|
8474
|
+
const at = cut.lastIndexOf('\n' + FILE_SECTION_PREFIX, searchFrom - 1);
|
|
8475
|
+
if (at < 0)
|
|
8476
|
+
break;
|
|
8477
|
+
// Extract path between **` and `**
|
|
8478
|
+
const after = cut.slice(at + 1 + FILE_SECTION_PREFIX.length);
|
|
8479
|
+
const endTick = after.indexOf('`');
|
|
8480
|
+
const fp = endTick > 0 ? after.slice(0, endTick) : '';
|
|
8481
|
+
const necessary = fp && (namedSeedFiles.has(fp) || entryFiles.has(fp) || centralFiles.has(fp));
|
|
8482
|
+
if (!necessary) {
|
|
8483
|
+
boundary = at;
|
|
8484
|
+
break;
|
|
8485
|
+
}
|
|
8486
|
+
searchFrom = at;
|
|
8487
|
+
}
|
|
8488
|
+
if (boundary < 0) {
|
|
8489
|
+
const lastSection = cut.lastIndexOf('\n' + FILE_SECTION_PREFIX);
|
|
8490
|
+
boundary = lastSection > hardCeiling * 0.5 ? lastSection : cut.lastIndexOf('\n');
|
|
8491
|
+
}
|
|
8492
|
+
const safe = boundary > 0 ? cut.slice(0, boundary) : cut;
|
|
8493
|
+
finalText = safe + '\n\n... (output truncated to budget; the source above is complete and verbatim — treat it as already Read. For uncovered files/symbols, run another homegraph_explore with their exact names — not grep/read/node for symbols already shown.)';
|
|
8494
|
+
}
|
|
8495
|
+
else {
|
|
8496
|
+
finalText = trimmed;
|
|
8497
|
+
}
|
|
6127
8498
|
}
|
|
6128
8499
|
else {
|
|
6129
8500
|
finalText = output;
|
|
@@ -6131,7 +8502,7 @@ class ToolHandler {
|
|
|
6131
8502
|
if (!finalText.includes('**Partial result**') && !finalText.includes('> **Explore complete**')) {
|
|
6132
8503
|
finalText +=
|
|
6133
8504
|
'\n\n> **Explore complete — ANSWER NOW.** Flow + Source above are authoritative for this query. ' +
|
|
6134
|
-
'Do **not** grep/read/`homegraph_node`/`homegraph_search` for the same symbols or files. ' +
|
|
8505
|
+
'Do **not** grep/read/`homegraph_node`/`homegraph_search` for the same symbols or files (multiplies tokens). ' +
|
|
6135
8506
|
'Only call one tighter `homegraph_explore` if a named symbol essential to the answer is missing.';
|
|
6136
8507
|
}
|
|
6137
8508
|
// Curated header (#1046): substitute the sentinel with the count of files
|
|
@@ -6152,7 +8523,40 @@ class ToolHandler {
|
|
|
6152
8523
|
? `Found ${shownSymbols} symbol${shownSymbols === 1 ? '' : 's'} across ${survivors.length} file${survivors.length === 1 ? '' : 's'}.`
|
|
6153
8524
|
: `Found ${subgraph.nodes.size} symbol${subgraph.nodes.size === 1 ? '' : 's'} across ${fileGroups.size} file${fileGroups.size === 1 ? '' : 's'}.`;
|
|
6154
8525
|
finalText = finalText.replace(SUMMARY_SENTINEL, summaryLine);
|
|
6155
|
-
|
|
8526
|
+
// Session record (CG-17): only the files that SURVIVED the hard ceiling —
|
|
8527
|
+
// a section the truncation dropped was never delivered, and recording it
|
|
8528
|
+
// would let a later call withhold source the agent has never seen.
|
|
8529
|
+
const emittedFiles = [];
|
|
8530
|
+
let sourceBytes = 0;
|
|
8531
|
+
for (const fp of survivors) {
|
|
8532
|
+
const emitted = emittedByFile.get(fp);
|
|
8533
|
+
if (!emitted || emitted.ranges.length === 0)
|
|
8534
|
+
continue;
|
|
8535
|
+
emittedFiles.push({
|
|
8536
|
+
path: fp,
|
|
8537
|
+
ranges: emitted.ranges,
|
|
8538
|
+
bytes: emitted.bytes,
|
|
8539
|
+
fingerprint: emitted.fingerprint,
|
|
8540
|
+
});
|
|
8541
|
+
sourceBytes += emitted.bytes;
|
|
8542
|
+
}
|
|
8543
|
+
return this.exploreResult(finalText, {
|
|
8544
|
+
projectRoot,
|
|
8545
|
+
query,
|
|
8546
|
+
files: emittedFiles,
|
|
8547
|
+
sourceBytes,
|
|
8548
|
+
responseBytes: finalText.length,
|
|
8549
|
+
});
|
|
8550
|
+
}
|
|
8551
|
+
/**
|
|
8552
|
+
* An explore response plus the record of what it emitted (CG-17). The record
|
|
8553
|
+
* rides the result only as far as {@link execute}, which files it into the
|
|
8554
|
+
* calling session's state and deletes it — see {@link EXPLORE_EMISSION_KEY}.
|
|
8555
|
+
*/
|
|
8556
|
+
exploreResult(text, emission) {
|
|
8557
|
+
const result = this.textResult(text);
|
|
8558
|
+
result[explore_session_state_1.EXPLORE_EMISSION_KEY] = emission;
|
|
8559
|
+
return result;
|
|
6156
8560
|
}
|
|
6157
8561
|
/**
|
|
6158
8562
|
* Handle homegraph_node
|
|
@@ -6406,6 +8810,11 @@ class ToolHandler {
|
|
|
6406
8810
|
}
|
|
6407
8811
|
/** Render one symbol: details + (optional) body/outline + its caller/callee trail. */
|
|
6408
8812
|
async renderNodeSection(cg, node, includeCode) {
|
|
8813
|
+
// Disk-drift gate (issue #1474): body is CURRENT bytes sliced at INDEXED
|
|
8814
|
+
// line ranges. Never emit a slice from a drifted file.
|
|
8815
|
+
if (this.isFileStaleOnDisk(cg, node.filePath)) {
|
|
8816
|
+
return this.renderStaleNodeSection(cg, node, includeCode);
|
|
8817
|
+
}
|
|
6409
8818
|
let code = null;
|
|
6410
8819
|
let outline = null;
|
|
6411
8820
|
if (includeCode) {
|
|
@@ -6422,6 +8831,48 @@ class ToolHandler {
|
|
|
6422
8831
|
}
|
|
6423
8832
|
return this.formatNodeDetails(node, code, outline) + this.formatTrail(cg, node);
|
|
6424
8833
|
}
|
|
8834
|
+
// Whole-file fallback caps for a drifted file (#1474).
|
|
8835
|
+
static STALE_WHOLE_FILE_MAX_LINES = 300;
|
|
8836
|
+
static STALE_WHOLE_FILE_MAX_CHARS = 12000;
|
|
8837
|
+
/**
|
|
8838
|
+
* homegraph_node render for a symbol whose file changed on disk after the
|
|
8839
|
+
* last index sync (issue #1474). Indexed line ranges are untrustworthy, so
|
|
8840
|
+
* no slice is emitted: small files get full CURRENT source; large ones get
|
|
8841
|
+
* an explicit notice.
|
|
8842
|
+
*/
|
|
8843
|
+
renderStaleNodeSection(cg, node, includeCode) {
|
|
8844
|
+
const lines = [
|
|
8845
|
+
`**${node.name}** (${node.kind})`,
|
|
8846
|
+
'',
|
|
8847
|
+
`**Location:** ${node.filePath}${node.startLine ? `:${node.startLine}` : ''} — ⚠ as of the last index sync; the file has changed on disk since, so this line may be shifted`,
|
|
8848
|
+
];
|
|
8849
|
+
if (node.signature) {
|
|
8850
|
+
lines.push(...formatNodeSignatureBlock(node.signature));
|
|
8851
|
+
}
|
|
8852
|
+
lines.push('');
|
|
8853
|
+
let embedded = false;
|
|
8854
|
+
if (includeCode) {
|
|
8855
|
+
try {
|
|
8856
|
+
const absPath = (0, utils_1.validatePathWithinRoot)(cg.getProjectRoot(), node.filePath);
|
|
8857
|
+
if (absPath && (0, fs_1.existsSync)(absPath) && !(0, utils_1.isConfigLeafNode)(node)) {
|
|
8858
|
+
const content = (0, fs_1.readFileSync)(absPath, 'utf-8');
|
|
8859
|
+
const body = content.replace(/\n+$/, '');
|
|
8860
|
+
if (body.length <= ToolHandler.STALE_WHOLE_FILE_MAX_CHARS &&
|
|
8861
|
+
body.split('\n').length <= ToolHandler.STALE_WHOLE_FILE_MAX_LINES) {
|
|
8862
|
+
lines.push(`> ⚠ \`${node.filePath}\` changed on disk after it was last indexed, so the indexed line range for this symbol may no longer match. Showing the file's full CURRENT source instead (Read-parity — treat it as already Read):`, '', '```' + (node.language || ''), numberSourceLines(body, 1), '```');
|
|
8863
|
+
embedded = true;
|
|
8864
|
+
}
|
|
8865
|
+
}
|
|
8866
|
+
}
|
|
8867
|
+
catch {
|
|
8868
|
+
/* fall through to the notice */
|
|
8869
|
+
}
|
|
8870
|
+
}
|
|
8871
|
+
if (!embedded) {
|
|
8872
|
+
lines.push(`> ⚠ \`${node.filePath}\` changed on disk after it was last indexed — the indexed line range for this symbol no longer reliably matches, so its body is omitted rather than risk showing a different symbol's code. For current content, call homegraph_node with \`file: "${node.filePath}"\` (no symbol; \`offset\`/\`limit\` narrow it like Read), or Read the file. The change is picked up automatically on that project's next index sync.`);
|
|
8873
|
+
}
|
|
8874
|
+
return lines.join('\n') + this.formatTrail(cg, node);
|
|
8875
|
+
}
|
|
6425
8876
|
/**
|
|
6426
8877
|
* Build the "trail" for a symbol: its direct callees (what it calls) and
|
|
6427
8878
|
* callers (what calls it), each with file:line — so homegraph_node doubles as
|
|
@@ -6466,6 +8917,16 @@ class ToolHandler {
|
|
|
6466
8917
|
* Handle homegraph_status
|
|
6467
8918
|
*/
|
|
6468
8919
|
async handleStatus(args) {
|
|
8920
|
+
const sourcesMode = (0, graph_sources_1.resolveGraphSources)();
|
|
8921
|
+
if (!(0, graph_sources_1.graphSourceFlags)(sourcesMode).openProjectDb) {
|
|
8922
|
+
return this.textResult([
|
|
8923
|
+
'**HomeGraph Status**',
|
|
8924
|
+
'',
|
|
8925
|
+
`**Graph sources:** ${sourcesMode}`,
|
|
8926
|
+
'',
|
|
8927
|
+
(0, graph_sources_1.graphSourcesDisabledGuidance)(sourcesMode),
|
|
8928
|
+
].join('\n'));
|
|
8929
|
+
}
|
|
6469
8930
|
let cg = this.getHomeGraph(args.projectPath);
|
|
6470
8931
|
// Same trick as withStalenessNotice — when an explicit projectPath
|
|
6471
8932
|
// resolves to the same project as the default session cg, prefer the
|
|
@@ -6493,7 +8954,9 @@ class ToolHandler {
|
|
|
6493
8954
|
if (mismatch) {
|
|
6494
8955
|
lines.push(`> ⚠ ${(0, worktree_1.worktreeMismatchWarning)(mismatch).replace(/\n/g, '\n> ')}`, '');
|
|
6495
8956
|
}
|
|
6496
|
-
lines.push(`**Files indexed:** ${stats.fileCount}`, `**Total nodes:** ${stats.nodeCount}`, `**Total edges:** ${stats.edgeCount}`, `**Database size:** ${(stats.dbSizeBytes / 1024 / 1024).toFixed(2)} MB
|
|
8957
|
+
lines.push(`**Files indexed:** ${stats.fileCount}`, `**Total nodes:** ${stats.nodeCount}`, `**Total edges:** ${stats.edgeCount}`, `**Database size:** ${(stats.dbSizeBytes / 1024 / 1024).toFixed(2)} MB`, ...(stats.walSizeBytes > 0
|
|
8958
|
+
? [`**WAL size:** ${(stats.walSizeBytes / 1024 / 1024).toFixed(2)} MB`]
|
|
8959
|
+
: []), `**Graph sources:** ${cg.getGraphSources()}`);
|
|
6497
8960
|
// Surface the active SQLite backend: node:sqlite → better-sqlite3 → wasm.
|
|
6498
8961
|
const backend = cg.getBackend();
|
|
6499
8962
|
if (backend === 'node-sqlite') {
|
|
@@ -6552,6 +9015,27 @@ class ToolHandler {
|
|
|
6552
9015
|
}
|
|
6553
9016
|
return this.textResult(lines.join('\n'));
|
|
6554
9017
|
}
|
|
9018
|
+
/**
|
|
9019
|
+
* Handle homegraph_arkui_migrate — one-shot ArkUI migrate / state snapshot (spec 0007).
|
|
9020
|
+
*/
|
|
9021
|
+
async handleArkuiMigrate(args) {
|
|
9022
|
+
const scope = this.validateString(args.scope, 'scope');
|
|
9023
|
+
if (typeof scope !== 'string')
|
|
9024
|
+
return scope;
|
|
9025
|
+
const cg = this.getHomeGraph(args.projectPath);
|
|
9026
|
+
try {
|
|
9027
|
+
const snapshot = cg.getArkUIMigrateSnapshot(scope);
|
|
9028
|
+
return this.textResult(JSON.stringify(snapshot, null, 2));
|
|
9029
|
+
}
|
|
9030
|
+
catch (err) {
|
|
9031
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
9032
|
+
return this.textResult([
|
|
9033
|
+
`No ArkUI migrate snapshot for scope "${scope}".`,
|
|
9034
|
+
message,
|
|
9035
|
+
'Pass a component name, relative .ets path, or directory prefix. Ensure the project is indexed (`homegraph init` / `index`).',
|
|
9036
|
+
].join('\n'));
|
|
9037
|
+
}
|
|
9038
|
+
}
|
|
6555
9039
|
/**
|
|
6556
9040
|
* Handle homegraph_files - get project file structure from the index
|
|
6557
9041
|
*/
|
|
@@ -7108,6 +9592,19 @@ class ToolHandler {
|
|
|
7108
9592
|
const segments = node.filePath.split('/').filter((s) => s.length > 0);
|
|
7109
9593
|
return containerHints.every((hint) => segments.some((seg) => seg === hint || seg.replace(/\.[^.]+$/, '') === hint));
|
|
7110
9594
|
}
|
|
9595
|
+
/**
|
|
9596
|
+
* Generated-file demotion signal: path convention OR persisted content banner.
|
|
9597
|
+
*/
|
|
9598
|
+
isGeneratedCandidate(cg, filePath) {
|
|
9599
|
+
if ((0, generated_detection_1.isGeneratedFile)(filePath))
|
|
9600
|
+
return true;
|
|
9601
|
+
try {
|
|
9602
|
+
return cg.getFile(filePath)?.generated === true;
|
|
9603
|
+
}
|
|
9604
|
+
catch {
|
|
9605
|
+
return false;
|
|
9606
|
+
}
|
|
9607
|
+
}
|
|
7111
9608
|
/**
|
|
7112
9609
|
* Find ALL definitions matching a name, ranked, so homegraph_node can return
|
|
7113
9610
|
* every overload instead of guessing one (the wrong guess → a Read). Keepers
|
|
@@ -7127,7 +9624,8 @@ class ToolHandler {
|
|
|
7127
9624
|
if (!isQualified) {
|
|
7128
9625
|
const exact = cg.getNodesByName(symbol);
|
|
7129
9626
|
if (exact.length > 0) {
|
|
7130
|
-
return [...exact].sort((a, b) => ((
|
|
9627
|
+
return [...exact].sort((a, b) => (this.isGeneratedCandidate(cg, a.filePath) ? 1 : 0) -
|
|
9628
|
+
(this.isGeneratedCandidate(cg, b.filePath) ? 1 : 0));
|
|
7131
9629
|
}
|
|
7132
9630
|
// No exact match — use the single top fuzzy result (e.g. a file basename).
|
|
7133
9631
|
const fuzzy = cg.searchNodes(symbol, { limit: 10 });
|
|
@@ -7152,10 +9650,9 @@ class ToolHandler {
|
|
|
7152
9650
|
// hit (#173); a bare name may use the single top fuzzy result.
|
|
7153
9651
|
return isQualified ? [] : results[0] ? [results[0].node] : [];
|
|
7154
9652
|
}
|
|
7155
|
-
// Down-rank generated files (.pb.go, .pulsar.go, _grpc.pb.go, …) so a flow
|
|
7156
|
-
// query prefers the keeper implementation over the protobuf-generated stub.
|
|
7157
9653
|
return [...exactMatches]
|
|
7158
|
-
.sort((a, b) => ((
|
|
9654
|
+
.sort((a, b) => (this.isGeneratedCandidate(cg, a.node.filePath) ? 1 : 0) -
|
|
9655
|
+
(this.isGeneratedCandidate(cg, b.node.filePath) ? 1 : 0))
|
|
7159
9656
|
.map((r) => r.node);
|
|
7160
9657
|
}
|
|
7161
9658
|
/**
|
|
@@ -7206,8 +9703,8 @@ class ToolHandler {
|
|
|
7206
9703
|
// /impact aggregation aligned (a query against "Send" returns the
|
|
7207
9704
|
// hand-written implementations before the protobuf scaffold).
|
|
7208
9705
|
const ranked = [...pool].sort((a, b) => {
|
|
7209
|
-
const aGen = (
|
|
7210
|
-
const bGen = (
|
|
9706
|
+
const aGen = this.isGeneratedCandidate(cg, a.filePath) ? 1 : 0;
|
|
9707
|
+
const bGen = this.isGeneratedCandidate(cg, b.filePath) ? 1 : 0;
|
|
7211
9708
|
return aGen - bGen;
|
|
7212
9709
|
});
|
|
7213
9710
|
const locations = ranked.map(n => `${n.kind} at ${n.filePath}:${n.startLine}`);
|
|
@@ -7253,6 +9750,62 @@ class ToolHandler {
|
|
|
7253
9750
|
}
|
|
7254
9751
|
return lines.join('\n');
|
|
7255
9752
|
}
|
|
9753
|
+
/**
|
|
9754
|
+
* At caller sites, list nearby `#include` / `import` lines — answers
|
|
9755
|
+
* "which call makes Type X's definition visible" without a second Grep/Read
|
|
9756
|
+
* when the agent only called `homegraph_callers`.
|
|
9757
|
+
*/
|
|
9758
|
+
formatCallerIncludeVisibility(cg, callers) {
|
|
9759
|
+
if (callers.length === 0)
|
|
9760
|
+
return '';
|
|
9761
|
+
let projectRoot = '';
|
|
9762
|
+
try {
|
|
9763
|
+
projectRoot = cg.getProjectRoot();
|
|
9764
|
+
}
|
|
9765
|
+
catch {
|
|
9766
|
+
return '';
|
|
9767
|
+
}
|
|
9768
|
+
const rows = [];
|
|
9769
|
+
const seen = new Set();
|
|
9770
|
+
for (const caller of callers.slice(0, 8)) {
|
|
9771
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, caller.filePath);
|
|
9772
|
+
if (!abs || !(0, fs_1.existsSync)(abs))
|
|
9773
|
+
continue;
|
|
9774
|
+
let content;
|
|
9775
|
+
try {
|
|
9776
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
9777
|
+
}
|
|
9778
|
+
catch {
|
|
9779
|
+
continue;
|
|
9780
|
+
}
|
|
9781
|
+
const fileLines = content.split('\n');
|
|
9782
|
+
const scanTo = Math.min(fileLines.length, Math.max(120, (caller.startLine || 1) + 5));
|
|
9783
|
+
for (let i = 0; i < scanTo; i++) {
|
|
9784
|
+
const lineText = fileLines[i] ?? '';
|
|
9785
|
+
if (!/^\s*(?:#\s*include|import\s)/.test(lineText))
|
|
9786
|
+
continue;
|
|
9787
|
+
const key = `${caller.filePath}:${i + 1}`;
|
|
9788
|
+
if (seen.has(key))
|
|
9789
|
+
continue;
|
|
9790
|
+
seen.add(key);
|
|
9791
|
+
rows.push(`- \`${caller.filePath}:${i + 1}\` \`${lineText.trim().slice(0, 140)}\``);
|
|
9792
|
+
if (rows.length >= 12)
|
|
9793
|
+
break;
|
|
9794
|
+
}
|
|
9795
|
+
if (rows.length >= 12)
|
|
9796
|
+
break;
|
|
9797
|
+
}
|
|
9798
|
+
if (rows.length === 0)
|
|
9799
|
+
return '';
|
|
9800
|
+
return [
|
|
9801
|
+
'',
|
|
9802
|
+
'**Include / import at caller files** (definition visibility cues)',
|
|
9803
|
+
'',
|
|
9804
|
+
...rows,
|
|
9805
|
+
'',
|
|
9806
|
+
'> Use these includes with the caller list when asking which call makes another type visible.',
|
|
9807
|
+
].join('\n');
|
|
9808
|
+
}
|
|
7256
9809
|
/**
|
|
7257
9810
|
* Relationship label for a non-`calls` edge in callers/callees lists. A
|
|
7258
9811
|
* function-as-value edge (#756) is the high-signal one: `callers(cb)`
|