homegraph 1.5.3 → 1.5.5
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 +45 -1
- package/README.md +55 -6
- package/dist/addons/dynamic-import.d.ts +18 -0
- package/dist/addons/dynamic-import.d.ts.map +1 -0
- package/dist/addons/dynamic-import.js +25 -0
- package/dist/addons/dynamic-import.js.map +1 -0
- package/dist/addons/init-template.d.ts +15 -0
- package/dist/addons/init-template.d.ts.map +1 -0
- package/dist/addons/init-template.js +290 -0
- package/dist/addons/init-template.js.map +1 -0
- package/dist/addons/loader.d.ts +18 -0
- package/dist/addons/loader.d.ts.map +1 -0
- package/dist/addons/loader.js +88 -0
- package/dist/addons/loader.js.map +1 -0
- package/dist/addons/manager.d.ts +106 -0
- package/dist/addons/manager.d.ts.map +1 -0
- package/dist/addons/manager.js +437 -0
- package/dist/addons/manager.js.map +1 -0
- package/dist/addons/paths.d.ts +22 -0
- package/dist/addons/paths.d.ts.map +1 -0
- package/dist/addons/paths.js +65 -0
- package/dist/addons/paths.js.map +1 -0
- package/dist/addons/registry.d.ts +38 -0
- package/dist/addons/registry.d.ts.map +1 -0
- package/dist/addons/registry.js +160 -0
- package/dist/addons/registry.js.map +1 -0
- package/dist/addons/semver.d.ts +21 -0
- package/dist/addons/semver.d.ts.map +1 -0
- package/dist/addons/semver.js +79 -0
- package/dist/addons/semver.js.map +1 -0
- package/dist/addons/types.d.ts +51 -0
- package/dist/addons/types.d.ts.map +1 -0
- package/dist/addons/types.js +14 -0
- package/dist/addons/types.js.map +1 -0
- package/dist/addons/validate.d.ts +38 -0
- package/dist/addons/validate.d.ts.map +1 -0
- package/dist/addons/validate.js +165 -0
- package/dist/addons/validate.js.map +1 -0
- package/dist/arkui/migrate-semantics.d.ts +8 -0
- package/dist/arkui/migrate-semantics.d.ts.map +1 -1
- package/dist/arkui/migrate-semantics.js +24 -1
- package/dist/arkui/migrate-semantics.js.map +1 -1
- package/dist/arkui/migrate-snapshot.d.ts.map +1 -1
- package/dist/arkui/migrate-snapshot.js +69 -38
- package/dist/arkui/migrate-snapshot.js.map +1 -1
- package/dist/bin/addon-commands.d.ts +27 -0
- package/dist/bin/addon-commands.d.ts.map +1 -0
- package/dist/bin/addon-commands.js +254 -0
- package/dist/bin/addon-commands.js.map +1 -0
- package/dist/bin/homegraph.js +6 -1
- package/dist/bin/homegraph.js.map +1 -1
- package/dist/bin/node-version-check.d.ts +1 -1
- package/dist/bin/node-version-check.js +1 -1
- package/dist/db/sqlite-adapter.d.ts +2 -3
- package/dist/db/sqlite-adapter.d.ts.map +1 -1
- package/dist/db/sqlite-adapter.js +2 -3
- package/dist/db/sqlite-adapter.js.map +1 -1
- package/dist/extraction/languages/arkts.d.ts +14 -0
- package/dist/extraction/languages/arkts.d.ts.map +1 -1
- package/dist/extraction/languages/arkts.js +78 -32
- package/dist/extraction/languages/arkts.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/engine.d.ts.map +1 -1
- package/dist/mcp/engine.js +2 -14
- package/dist/mcp/engine.js.map +1 -1
- package/dist/mcp/explore-repeat-guard.d.ts +123 -0
- package/dist/mcp/explore-repeat-guard.d.ts.map +1 -0
- package/dist/mcp/explore-repeat-guard.js +540 -0
- package/dist/mcp/explore-repeat-guard.js.map +1 -0
- package/dist/mcp/explore-session-state.d.ts +20 -0
- package/dist/mcp/explore-session-state.d.ts.map +1 -1
- package/dist/mcp/explore-session-state.js +31 -0
- package/dist/mcp/explore-session-state.js.map +1 -1
- package/dist/mcp/memory-budget.d.ts +9 -22
- package/dist/mcp/memory-budget.d.ts.map +1 -1
- package/dist/mcp/memory-budget.js +9 -59
- package/dist/mcp/memory-budget.js.map +1 -1
- package/dist/mcp/proxy.d.ts.map +1 -1
- package/dist/mcp/proxy.js +15 -4
- package/dist/mcp/proxy.js.map +1 -1
- package/dist/mcp/query-pool.d.ts.map +1 -1
- package/dist/mcp/query-pool.js +10 -13
- package/dist/mcp/query-pool.js.map +1 -1
- package/dist/mcp/server-instructions.d.ts +5 -7
- package/dist/mcp/server-instructions.d.ts.map +1 -1
- package/dist/mcp/server-instructions.js +38 -99
- package/dist/mcp/server-instructions.js.map +1 -1
- package/dist/mcp/tools.d.ts +77 -5
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +2693 -320
- package/dist/mcp/tools.js.map +1 -1
- package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/dist/resolution/callback-synthesizer.js +166 -0
- package/dist/resolution/callback-synthesizer.js.map +1 -1
- package/dist/search/query-utils.d.ts +57 -7
- package/dist/search/query-utils.d.ts.map +1 -1
- package/dist/search/query-utils.js +392 -113
- package/dist/search/query-utils.js.map +1 -1
- package/dist/spec/build/scan.js +1 -1
- package/dist/spec/build/scan.js.map +1 -1
- package/dist/spec/build/scope-resolver.d.ts +24 -5
- package/dist/spec/build/scope-resolver.d.ts.map +1 -1
- package/dist/spec/build/scope-resolver.js +76 -10
- package/dist/spec/build/scope-resolver.js.map +1 -1
- package/dist/spec/config.d.ts +8 -0
- package/dist/spec/config.d.ts.map +1 -1
- package/dist/spec/config.js +3 -0
- package/dist/spec/config.js.map +1 -1
- package/dist/spec/evolve/pipeline.d.ts.map +1 -1
- package/dist/spec/evolve/pipeline.js +2 -1
- package/dist/spec/evolve/pipeline.js.map +1 -1
- package/dist/spec/git/commits.d.ts +8 -3
- package/dist/spec/git/commits.d.ts.map +1 -1
- package/dist/spec/git/commits.js +12 -11
- package/dist/spec/git/commits.js.map +1 -1
- package/dist/spec/mine/addon/adapter.d.ts +45 -0
- package/dist/spec/mine/addon/adapter.d.ts.map +1 -0
- package/dist/spec/mine/addon/adapter.js +118 -0
- package/dist/spec/mine/addon/adapter.js.map +1 -0
- package/dist/spec/mine/addon/render.d.ts +20 -0
- package/dist/spec/mine/addon/render.d.ts.map +1 -0
- package/dist/spec/mine/addon/render.js +41 -0
- package/dist/spec/mine/addon/render.js.map +1 -0
- package/dist/spec/mine/addon/types.d.ts +70 -0
- package/dist/spec/mine/addon/types.d.ts.map +1 -0
- package/dist/spec/mine/addon/types.js +14 -0
- package/dist/spec/mine/addon/types.js.map +1 -0
- package/dist/spec/mine/generator.d.ts +5 -1
- package/dist/spec/mine/generator.d.ts.map +1 -1
- package/dist/spec/mine/generator.js +69 -7
- package/dist/spec/mine/generator.js.map +1 -1
- package/dist/spec/mine/pipeline.d.ts.map +1 -1
- package/dist/spec/mine/pipeline.js +5 -1
- package/dist/spec/mine/pipeline.js.map +1 -1
- package/dist/spec/mine/scanner.d.ts +1 -0
- package/dist/spec/mine/scanner.d.ts.map +1 -1
- package/dist/spec/mine/scanner.js +1 -1
- package/dist/spec/mine/scanner.js.map +1 -1
- package/dist/spec/types.d.ts +1 -1
- package/dist/spec/types.d.ts.map +1 -1
- package/dist/upgrade/index.d.ts +23 -12
- package/dist/upgrade/index.d.ts.map +1 -1
- package/dist/upgrade/index.js +42 -48
- package/dist/upgrade/index.js.map +1 -1
- package/dist/upgrade/update-check.d.ts +4 -4
- package/dist/upgrade/update-check.d.ts.map +1 -1
- package/dist/upgrade/update-check.js +6 -5
- package/dist/upgrade/update-check.js.map +1 -1
- package/package.json +6 -6
package/dist/mcp/tools.js
CHANGED
|
@@ -11,10 +11,15 @@ exports.normalizeQuerySpelling = normalizeQuerySpelling;
|
|
|
11
11
|
exports.getExploreBudget = getExploreBudget;
|
|
12
12
|
exports.getExploreOutputBudget = getExploreOutputBudget;
|
|
13
13
|
exports.tightenExploreBudgetForQuery = tightenExploreBudgetForQuery;
|
|
14
|
+
exports.exploreLocatorDigestEnabled = exploreLocatorDigestEnabled;
|
|
14
15
|
exports.formatStaleBanner = formatStaleBanner;
|
|
15
16
|
exports.formatStaleFooter = formatStaleFooter;
|
|
16
17
|
exports.formatDegradedBanner = formatDegradedBanner;
|
|
17
18
|
exports.getStaticTools = getStaticTools;
|
|
19
|
+
exports.extractBareUsageSymbols = extractBareUsageSymbols;
|
|
20
|
+
exports.queryAsBareSymbolInventory = queryAsBareSymbolInventory;
|
|
21
|
+
exports.hasPositiveAnswerNowDirective = hasPositiveAnswerNowDirective;
|
|
22
|
+
exports.reconcilePartialAnswerNow = reconcilePartialAnswerNow;
|
|
18
23
|
const query_pool_1 = require("./query-pool");
|
|
19
24
|
const memory_budget_1 = require("./memory-budget");
|
|
20
25
|
const directory_1 = require("../directory");
|
|
@@ -34,6 +39,7 @@ function __setLoadHomeGraphForTests(cls) {
|
|
|
34
39
|
loadHomeGraphForTests = cls;
|
|
35
40
|
}
|
|
36
41
|
const worktree_1 = require("../sync/worktree");
|
|
42
|
+
const update_check_1 = require("../upgrade/update-check");
|
|
37
43
|
const graph_sources_1 = require("../graph-sources");
|
|
38
44
|
const query_utils_1 = require("../search/query-utils");
|
|
39
45
|
const fs_1 = require("fs");
|
|
@@ -43,6 +49,7 @@ const generated_detection_1 = require("../extraction/generated-detection");
|
|
|
43
49
|
const arkts_1 = require("../extraction/languages/arkts");
|
|
44
50
|
const explore_session_state_1 = require("./explore-session-state");
|
|
45
51
|
const explore_dedup_1 = require("./explore-dedup");
|
|
52
|
+
const explore_repeat_guard_1 = require("./explore-repeat-guard");
|
|
46
53
|
const dynamic_boundaries_1 = require("./dynamic-boundaries");
|
|
47
54
|
const query_cache_1 = require("./query-cache");
|
|
48
55
|
const diff_impact_1 = require("./diff-impact");
|
|
@@ -278,25 +285,19 @@ function getExploreBudget(fileCount) {
|
|
|
278
285
|
function getExploreOutputBudget(fileCount) {
|
|
279
286
|
// Tiered budget, scaled to project size. The budget is a CEILING (relevance
|
|
280
287
|
// still gates WHAT is included), and it MUST stay under the agent's INLINE
|
|
281
|
-
// tool-result cap (~25K chars).
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
// get a smaller `maxCharsPerFile` than a smaller tier.
|
|
288
|
+
// tool-result cap (~25K chars).
|
|
289
|
+
//
|
|
290
|
+
// Default = universal mid-lean locator (anchors + spine digests ~12K): enough
|
|
291
|
+
// evidence to stop a Read storm, not a multi-file body dump. Set
|
|
292
|
+
// HOMEGRAPH_EXPLORE_FULL_SOURCE=1 for the prior fatter shape.
|
|
293
|
+
// Invariant: a larger tier must never get a smaller `maxCharsPerFile` than a
|
|
294
|
+
// smaller tier.
|
|
295
|
+
const digest = exploreLocatorDigestEnabled();
|
|
290
296
|
if (fileCount < 150) {
|
|
291
297
|
return {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
// hard-exclude. The cost lever for this tier lives in steering the
|
|
296
|
-
// agent to stop after 1-2 calls, not in this budget.
|
|
297
|
-
maxOutputChars: 13000,
|
|
298
|
-
defaultMaxFiles: 4,
|
|
299
|
-
maxCharsPerFile: 3800,
|
|
298
|
+
maxOutputChars: digest ? 10000 : 13000,
|
|
299
|
+
defaultMaxFiles: digest ? 2 : 4,
|
|
300
|
+
maxCharsPerFile: digest ? 2500 : 3800,
|
|
300
301
|
gapThreshold: 7,
|
|
301
302
|
maxSymbolsInFileHeader: 5,
|
|
302
303
|
maxEdgesPerRelationshipKind: 4,
|
|
@@ -309,10 +310,9 @@ function getExploreOutputBudget(fileCount) {
|
|
|
309
310
|
}
|
|
310
311
|
if (fileCount < 500) {
|
|
311
312
|
return {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
maxCharsPerFile: 3800,
|
|
313
|
+
maxOutputChars: digest ? 12000 : 18000,
|
|
314
|
+
defaultMaxFiles: digest ? 2 : 5,
|
|
315
|
+
maxCharsPerFile: digest ? 2800 : 3800,
|
|
316
316
|
gapThreshold: 8,
|
|
317
317
|
maxSymbolsInFileHeader: 6,
|
|
318
318
|
maxEdgesPerRelationshipKind: 6,
|
|
@@ -325,53 +325,48 @@ function getExploreOutputBudget(fileCount) {
|
|
|
325
325
|
}
|
|
326
326
|
if (fileCount < 5000) {
|
|
327
327
|
return {
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
includeBudgetNote: true,
|
|
341
|
-
excludeLowValueFiles: false,
|
|
328
|
+
// Mid-lean: anchors + 1–2 spine digests. Per-file ≥ smaller tiers.
|
|
329
|
+
maxOutputChars: digest ? 12000 : 18000,
|
|
330
|
+
defaultMaxFiles: digest ? 2 : 6,
|
|
331
|
+
maxCharsPerFile: digest ? 3000 : 6500,
|
|
332
|
+
gapThreshold: digest ? 10 : 12,
|
|
333
|
+
maxSymbolsInFileHeader: digest ? 8 : 10,
|
|
334
|
+
maxEdgesPerRelationshipKind: digest ? 6 : 10,
|
|
335
|
+
includeRelationships: false,
|
|
336
|
+
includeAdditionalFiles: false,
|
|
337
|
+
includeCompletenessSignal: false,
|
|
338
|
+
includeBudgetNote: false,
|
|
339
|
+
excludeLowValueFiles: true,
|
|
342
340
|
};
|
|
343
341
|
}
|
|
344
|
-
// Large + very-large
|
|
345
|
-
// externalizes — see vscode). More files indexed → more CALLS via
|
|
346
|
-
// getExploreBudget, not a bigger single response. Per-file 7000 (≥ smaller
|
|
347
|
-
// tiers) gives the central file a ~180-line orientation window.
|
|
342
|
+
// Large + very-large: same mid-lean ceiling; scale via getExploreBudget calls.
|
|
348
343
|
if (fileCount < 15000) {
|
|
349
344
|
return {
|
|
350
|
-
maxOutputChars:
|
|
351
|
-
defaultMaxFiles:
|
|
352
|
-
maxCharsPerFile: 7000,
|
|
353
|
-
gapThreshold: 15,
|
|
354
|
-
maxSymbolsInFileHeader:
|
|
355
|
-
maxEdgesPerRelationshipKind:
|
|
356
|
-
includeRelationships:
|
|
357
|
-
includeAdditionalFiles:
|
|
358
|
-
includeCompletenessSignal:
|
|
359
|
-
includeBudgetNote:
|
|
360
|
-
excludeLowValueFiles:
|
|
345
|
+
maxOutputChars: digest ? 12000 : 18000,
|
|
346
|
+
defaultMaxFiles: digest ? 2 : 6,
|
|
347
|
+
maxCharsPerFile: digest ? 3000 : 7000,
|
|
348
|
+
gapThreshold: digest ? 10 : 15,
|
|
349
|
+
maxSymbolsInFileHeader: digest ? 8 : 12,
|
|
350
|
+
maxEdgesPerRelationshipKind: digest ? 6 : 12,
|
|
351
|
+
includeRelationships: false,
|
|
352
|
+
includeAdditionalFiles: false,
|
|
353
|
+
includeCompletenessSignal: false,
|
|
354
|
+
includeBudgetNote: false,
|
|
355
|
+
excludeLowValueFiles: true,
|
|
361
356
|
};
|
|
362
357
|
}
|
|
363
358
|
return {
|
|
364
|
-
maxOutputChars:
|
|
365
|
-
defaultMaxFiles:
|
|
366
|
-
maxCharsPerFile: 7000,
|
|
367
|
-
gapThreshold: 15,
|
|
368
|
-
maxSymbolsInFileHeader:
|
|
369
|
-
maxEdgesPerRelationshipKind:
|
|
370
|
-
includeRelationships:
|
|
371
|
-
includeAdditionalFiles:
|
|
372
|
-
includeCompletenessSignal:
|
|
373
|
-
includeBudgetNote:
|
|
374
|
-
excludeLowValueFiles:
|
|
359
|
+
maxOutputChars: digest ? 12000 : 18000,
|
|
360
|
+
defaultMaxFiles: digest ? 2 : 6,
|
|
361
|
+
maxCharsPerFile: digest ? 3000 : 7000,
|
|
362
|
+
gapThreshold: digest ? 10 : 15,
|
|
363
|
+
maxSymbolsInFileHeader: digest ? 8 : 12,
|
|
364
|
+
maxEdgesPerRelationshipKind: digest ? 6 : 12,
|
|
365
|
+
includeRelationships: false,
|
|
366
|
+
includeAdditionalFiles: false,
|
|
367
|
+
includeCompletenessSignal: false,
|
|
368
|
+
includeBudgetNote: false,
|
|
369
|
+
excludeLowValueFiles: true,
|
|
375
370
|
};
|
|
376
371
|
}
|
|
377
372
|
/**
|
|
@@ -384,13 +379,15 @@ function getExploreOutputBudget(fileCount) {
|
|
|
384
379
|
* implemented" questions even when the first answer was already enough.
|
|
385
380
|
*/
|
|
386
381
|
function tightenExploreBudgetForQuery(budget, query, opts) {
|
|
382
|
+
const digest = exploreLocatorDigestEnabled();
|
|
387
383
|
const hasFlow = opts?.hasFlowPath === true;
|
|
388
384
|
if (hasFlow && ((0, query_utils_1.queryAsMechanismSurvey)(query) || (0, query_utils_1.queryAsCrossModuleFlowSurvey)(query))) {
|
|
389
385
|
return {
|
|
390
386
|
...budget,
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
387
|
+
// Mid-lean: keep enough spine to avoid a Read storm after explore.
|
|
388
|
+
maxOutputChars: Math.min(budget.maxOutputChars, digest ? 11000 : 12000),
|
|
389
|
+
defaultMaxFiles: Math.min(budget.defaultMaxFiles, 2),
|
|
390
|
+
maxCharsPerFile: Math.min(budget.maxCharsPerFile, digest ? 3000 : 4500),
|
|
394
391
|
includeRelationships: false,
|
|
395
392
|
includeAdditionalFiles: false,
|
|
396
393
|
includeCompletenessSignal: false,
|
|
@@ -402,12 +399,11 @@ function tightenExploreBudgetForQuery(budget, query, opts) {
|
|
|
402
399
|
if (!local && !compact && hasFlow)
|
|
403
400
|
return budget;
|
|
404
401
|
if (!local && !compact && hasFlow === false) {
|
|
405
|
-
// Generic no-flow explore: still trim meta + file count a bit.
|
|
406
402
|
return {
|
|
407
403
|
...budget,
|
|
408
|
-
maxOutputChars: Math.min(budget.maxOutputChars,
|
|
409
|
-
defaultMaxFiles: Math.min(budget.defaultMaxFiles,
|
|
410
|
-
maxCharsPerFile: Math.min(budget.maxCharsPerFile,
|
|
404
|
+
maxOutputChars: Math.min(budget.maxOutputChars, digest ? 10000 : 10000),
|
|
405
|
+
defaultMaxFiles: Math.min(budget.defaultMaxFiles, 2),
|
|
406
|
+
maxCharsPerFile: Math.min(budget.maxCharsPerFile, digest ? 2800 : 4000),
|
|
411
407
|
includeRelationships: false,
|
|
412
408
|
includeAdditionalFiles: false,
|
|
413
409
|
includeCompletenessSignal: false,
|
|
@@ -421,9 +417,9 @@ function tightenExploreBudgetForQuery(budget, query, opts) {
|
|
|
421
417
|
|| (0, query_utils_1.queryAsTypeLifecycleSurvey)(query);
|
|
422
418
|
return {
|
|
423
419
|
...budget,
|
|
424
|
-
maxOutputChars: Math.min(budget.maxOutputChars, lean ? 4800 : local ? 7000 : 9000),
|
|
425
|
-
defaultMaxFiles: Math.min(budget.defaultMaxFiles,
|
|
426
|
-
maxCharsPerFile: Math.min(budget.maxCharsPerFile, lean ? 2200 : local ? 3500 : 4000),
|
|
420
|
+
maxOutputChars: Math.min(budget.maxOutputChars, lean ? (digest ? 5000 : 4800) : local ? (digest ? 6000 : 7000) : (digest ? 8000 : 9000)),
|
|
421
|
+
defaultMaxFiles: Math.min(budget.defaultMaxFiles, 2),
|
|
422
|
+
maxCharsPerFile: Math.min(budget.maxCharsPerFile, lean ? (digest ? 2200 : 2200) : local ? (digest ? 2800 : 3500) : (digest ? 3000 : 4000)),
|
|
427
423
|
includeRelationships: false,
|
|
428
424
|
includeAdditionalFiles: false,
|
|
429
425
|
includeCompletenessSignal: false,
|
|
@@ -444,6 +440,14 @@ function tightenExploreBudgetForQuery(budget, query, opts) {
|
|
|
444
440
|
function exploreLineNumbersEnabled() {
|
|
445
441
|
return process.env.HOMEGRAPH_EXPLORE_LINENUMS !== '0';
|
|
446
442
|
}
|
|
443
|
+
/**
|
|
444
|
+
* Locator mid-lean mode (default ON): explore returns anchors + spine digests
|
|
445
|
+
* (~12K) — enough to avoid a Read storm, not a multi-file body dump.
|
|
446
|
+
* Set `HOMEGRAPH_EXPLORE_FULL_SOURCE=1` to restore the previous fatter Source.
|
|
447
|
+
*/
|
|
448
|
+
function exploreLocatorDigestEnabled() {
|
|
449
|
+
return process.env.HOMEGRAPH_EXPLORE_FULL_SOURCE !== '1';
|
|
450
|
+
}
|
|
447
451
|
/**
|
|
448
452
|
* Adaptive explore sizing (default ON). `homegraph_explore` skeletonizes OFF-SPINE
|
|
449
453
|
* polymorphic-sibling files — a file whose class is one of ≥3 interchangeable
|
|
@@ -840,21 +844,28 @@ exports.tools = [
|
|
|
840
844
|
},
|
|
841
845
|
{
|
|
842
846
|
name: 'homegraph_explore',
|
|
843
|
-
description: '
|
|
844
|
-
'
|
|
845
|
-
'
|
|
846
|
-
'
|
|
847
|
-
'
|
|
848
|
-
'
|
|
849
|
-
'
|
|
850
|
-
'
|
|
851
|
-
'
|
|
847
|
+
description: 'PRIMARY entry for understanding THIS repo before you edit or answer structural questions. ' +
|
|
848
|
+
'Returns call paths + compact line-numbered source for the relevant symbols. Required: `query`. ' +
|
|
849
|
+
'CALL FIRST (alone, no parallel Grep/Read) when you will change an existing codebase — pass the user task or domain keywords ' +
|
|
850
|
+
'(page/module/feature/component words); locate where to edit before writing code. Also CALL FIRST for how/wired questions, ' +
|
|
851
|
+
'named Type/Component/Page/Dialog, Type.member, click→handler, inheritance/subtypes, declaration/attribute sites, ' +
|
|
852
|
+
'ALL_CAPS constant / field-mutex usages, path-module NAPI/exports or inter-deps, and in-repo @kit/@ohos *usages/dependencies* ' +
|
|
853
|
+
'(which files import a named export — not the SDK feature catalog). PascalCase names optional when domain keywords suffice. ' +
|
|
854
|
+
'Prefer callers/node when one named symbol is already enough. ' +
|
|
855
|
+
'DO NOT call for topic file-lists with no Type/file, literal copy hunts / pure existence compares with no anchors, ' +
|
|
856
|
+
'official-docs-only asks, empty-project-from-scratch scaffolds, git history, or media/binary asset inventories — those return Skip. ' +
|
|
857
|
+
'@kit / OHOS API questions ARE in scope when the SDK API graph is available — call explore (usages or API symbols). ' +
|
|
858
|
+
'Literal string/pattern hunts → Grep; media assets → Glob; git history → git. ' +
|
|
859
|
+
'One explore; then answer or edit from Source + trail — do not re-grep/node/read the same symbols. ' +
|
|
860
|
+
'Overlapping paraphrase explores are refused (name a new Type/file/@kit to continue). ' +
|
|
861
|
+
'Busy/partial → retry ONCE with the named Next anchor or ONE narrow Grep — not a Grep/node storm '
|
|
862
|
+
+ '(session refuses further explore / depth fan-out after Partial).',
|
|
852
863
|
inputSchema: {
|
|
853
864
|
type: 'object',
|
|
854
865
|
properties: {
|
|
855
866
|
query: {
|
|
856
867
|
type: 'string',
|
|
857
|
-
description: 'Required.
|
|
868
|
+
description: 'Required. For pre-edit orientation or how/mechanism: pass the user task or domain keywords (page/module/feature words). ' +
|
|
858
869
|
'For named flows, include Type / Type.member / component names. For @kit, ask usages (depend/import sites), not SDK catalogs.',
|
|
859
870
|
},
|
|
860
871
|
maxFiles: {
|
|
@@ -868,6 +879,60 @@ exports.tools = [
|
|
|
868
879
|
},
|
|
869
880
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
870
881
|
},
|
|
882
|
+
{
|
|
883
|
+
name: 'homegraph_usages',
|
|
884
|
+
description: 'Focused in-repo usage inventory for one named API, `.member`, ALL_CAPS constant, field, or mutex. ' +
|
|
885
|
+
'Required: `query`. Returns usage files/lines only; it does not build a general flow or source dump. ' +
|
|
886
|
+
'Use directly for a narrow where-used question; `homegraph_explore` auto-routes equivalent high-confidence queries here.',
|
|
887
|
+
inputSchema: {
|
|
888
|
+
type: 'object',
|
|
889
|
+
properties: {
|
|
890
|
+
query: {
|
|
891
|
+
type: 'string',
|
|
892
|
+
description: 'Required. Exact API/member/constant/field name, with or without where-used wording.',
|
|
893
|
+
},
|
|
894
|
+
projectPath: projectPathProperty,
|
|
895
|
+
},
|
|
896
|
+
required: ['query'],
|
|
897
|
+
},
|
|
898
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
name: 'homegraph_modules',
|
|
902
|
+
description: 'Focused dependency/cycle inventory for named path modules or `*common` / `*service` / `*component` / `*constants` modules. ' +
|
|
903
|
+
'Required: `query`. It does not build a general code flow or scan unrelated survey families. ' +
|
|
904
|
+
'`homegraph_explore` auto-routes equivalent high-confidence dependency questions here.',
|
|
905
|
+
inputSchema: {
|
|
906
|
+
type: 'object',
|
|
907
|
+
properties: {
|
|
908
|
+
query: {
|
|
909
|
+
type: 'string',
|
|
910
|
+
description: 'Required. Dependency/cycle question containing the exact module names or paths.',
|
|
911
|
+
},
|
|
912
|
+
projectPath: projectPathProperty,
|
|
913
|
+
},
|
|
914
|
+
required: ['query'],
|
|
915
|
+
},
|
|
916
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
name: 'homegraph_native',
|
|
920
|
+
description: 'Focused NAPI/native export inventory for one named path or Type. Required: `query`. ' +
|
|
921
|
+
'Returns indexed export descriptors/registration sites without a general domain file dump. ' +
|
|
922
|
+
'`homegraph_explore` auto-routes equivalent high-confidence NAPI/export questions here.',
|
|
923
|
+
inputSchema: {
|
|
924
|
+
type: 'object',
|
|
925
|
+
properties: {
|
|
926
|
+
query: {
|
|
927
|
+
type: 'string',
|
|
928
|
+
description: 'Required. NAPI/native export question containing the exact path or Type name.',
|
|
929
|
+
},
|
|
930
|
+
projectPath: projectPathProperty,
|
|
931
|
+
},
|
|
932
|
+
required: ['query'],
|
|
933
|
+
},
|
|
934
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
935
|
+
},
|
|
871
936
|
{
|
|
872
937
|
name: 'homegraph_status',
|
|
873
938
|
description: 'Index health check (files / nodes / edges). No required args when a default project is loaded. Skip unless debugging.',
|
|
@@ -1063,12 +1128,150 @@ function getStaticTools() {
|
|
|
1063
1128
|
const allow = new Set(raw.split(',').map(s => s.trim().replace(/^homegraph_/, '').replace(/^homegraph_/, '')).filter(Boolean));
|
|
1064
1129
|
return allow.size ? exports.tools.filter(t => allow.has(t.name.replace(/^homegraph_/, ''))) : exports.tools;
|
|
1065
1130
|
}
|
|
1131
|
+
/** Prose that reads like an identifier but never names a symbol worth scanning. */
|
|
1132
|
+
const BARE_USAGE_STOPWORDS = new Set([
|
|
1133
|
+
'class', 'method', 'function', 'interface', 'module', 'import', 'export', 'value',
|
|
1134
|
+
'file', 'files', 'code', 'usage', 'usages', 'where', 'which', 'what', 'when',
|
|
1135
|
+
'call', 'calls', 'called', 'caller', 'callers', 'used', 'uses', 'using',
|
|
1136
|
+
'true', 'false', 'null', 'undefined', 'this', 'return', 'const', 'type',
|
|
1137
|
+
'string', 'number', 'boolean', 'object', 'array', 'void', 'async', 'await',
|
|
1138
|
+
]);
|
|
1139
|
+
/** Identifier-shaped tokens from a bare symbol bag. */
|
|
1140
|
+
function extractBareUsageSymbols(query) {
|
|
1141
|
+
const out = [];
|
|
1142
|
+
const seen = new Set();
|
|
1143
|
+
const re = /\b([A-Z][A-Za-z0-9]{3,}(?:\.[A-Za-z_]\w*)?|[A-Z][A-Z0-9_]{3,}|[a-z]+[A-Z]\w{2,}(?:\.[A-Za-z_]\w*)?)\b/g;
|
|
1144
|
+
for (const m of query.matchAll(re)) {
|
|
1145
|
+
const tok = m[1];
|
|
1146
|
+
const key = tok.toLowerCase();
|
|
1147
|
+
if (seen.has(key) || BARE_USAGE_STOPWORDS.has(key))
|
|
1148
|
+
continue;
|
|
1149
|
+
seen.add(key);
|
|
1150
|
+
out.push(tok);
|
|
1151
|
+
}
|
|
1152
|
+
return out;
|
|
1153
|
+
}
|
|
1154
|
+
/** Wording that asks how something works — never a plain usage inventory. */
|
|
1155
|
+
const FLOW_INTENT_RE = /如何|怎[么样]|机制|流程|原理|为什么|为何|架构|生命周期|时序|调用链|关系|\b(how|why|flow|mechanism|lifecycle|architecture|sequence|pipeline|between|implement|implementation)\b/i;
|
|
1156
|
+
/** Action-shaped identifiers are probably call-flow anchors, not inventories. */
|
|
1157
|
+
const ACTION_SEGMENTS = new Set([
|
|
1158
|
+
'trigger', 'callback', 'handler', 'listener', 'observer', 'dispatch', 'emit',
|
|
1159
|
+
'notify', 'render', 'draw', 'init', 'start', 'stop', 'update', 'refresh',
|
|
1160
|
+
'subscribe', 'publish', 'invoke', 'execute', 'run', 'process', 'handle',
|
|
1161
|
+
'post', 'send', 'receive', 'load', 'save', 'open', 'close',
|
|
1162
|
+
]);
|
|
1163
|
+
function identifierSegments(token) {
|
|
1164
|
+
return token
|
|
1165
|
+
.replace(/[._]/g, ' ')
|
|
1166
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
1167
|
+
.toLowerCase()
|
|
1168
|
+
.split(/\s+/)
|
|
1169
|
+
.filter(Boolean);
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* True only for a short bag of ALL_CAPS constants. Type/method bags remain on
|
|
1173
|
+
* explore-flow because routing those to usages would discard call paths.
|
|
1174
|
+
*/
|
|
1175
|
+
function queryAsBareSymbolInventory(query) {
|
|
1176
|
+
if (process.env.HOMEGRAPH_EXPLORE_SHAPE_ROUTING === '0')
|
|
1177
|
+
return false;
|
|
1178
|
+
const tokens = String(query || '').split(/[\s,、,]+/).filter(Boolean);
|
|
1179
|
+
if (tokens.length === 0 || tokens.length > 6)
|
|
1180
|
+
return false;
|
|
1181
|
+
if (FLOW_INTENT_RE.test(query))
|
|
1182
|
+
return false;
|
|
1183
|
+
if (tokens.some((t) => identifierSegments(t).some((s) => ACTION_SEGMENTS.has(s))))
|
|
1184
|
+
return false;
|
|
1185
|
+
return tokens.every((t) => /^[A-Z][A-Z0-9_]{3,}$/.test(t));
|
|
1186
|
+
}
|
|
1066
1187
|
/**
|
|
1067
1188
|
* Tool handler that executes tools against a HomeGraph instance
|
|
1068
1189
|
*
|
|
1069
1190
|
* Supports cross-project queries via the projectPath parameter.
|
|
1070
1191
|
* Other projects are opened on-demand and cached for performance.
|
|
1071
1192
|
*/
|
|
1193
|
+
const ANSWER_NOW_TOKEN = /\bANSWER NOW\b/gi;
|
|
1194
|
+
/** Whether one ANSWER NOW occurrence is postponed/negated rather than a stop directive. */
|
|
1195
|
+
function isNegatedAnswerNowAt(text, index) {
|
|
1196
|
+
const prefix = text
|
|
1197
|
+
.slice(Math.max(0, index - 96), index)
|
|
1198
|
+
.replace(/[*_`]/g, '')
|
|
1199
|
+
.replace(/\s+/g, ' ');
|
|
1200
|
+
return /(?:\b(?:do|should|must|can)\s+not|\bcannot|\bnever|\bbefore|\buntil|\bnot)\s+(?:\S+\s+){0,3}$/i
|
|
1201
|
+
.test(prefix);
|
|
1202
|
+
}
|
|
1203
|
+
/** True only for a positive instruction to stop and answer, not "do not/before ANSWER NOW". */
|
|
1204
|
+
function hasPositiveAnswerNowDirective(text) {
|
|
1205
|
+
const answerNow = new RegExp(ANSWER_NOW_TOKEN.source, ANSWER_NOW_TOKEN.flags);
|
|
1206
|
+
for (let match = answerNow.exec(text); match; match = answerNow.exec(text)) {
|
|
1207
|
+
if (!isNegatedAnswerNowAt(text, match.index))
|
|
1208
|
+
return true;
|
|
1209
|
+
}
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1212
|
+
function stripPositiveAnswerNowFromLine(line) {
|
|
1213
|
+
let changed = false;
|
|
1214
|
+
let out = line.replace(/\*\*[^*\n]*\bANSWER NOW\b[^*\n]*\*\*/gi, (span, offset, source) => {
|
|
1215
|
+
const tokenOffset = span.search(/\bANSWER NOW\b/i);
|
|
1216
|
+
if (tokenOffset < 0 || isNegatedAnswerNowAt(source, offset + tokenOffset))
|
|
1217
|
+
return span;
|
|
1218
|
+
changed = true;
|
|
1219
|
+
return '';
|
|
1220
|
+
});
|
|
1221
|
+
out = out.replace(ANSWER_NOW_TOKEN, (token, offset, source) => {
|
|
1222
|
+
if (isNegatedAnswerNowAt(source, offset))
|
|
1223
|
+
return token;
|
|
1224
|
+
changed = true;
|
|
1225
|
+
return '';
|
|
1226
|
+
});
|
|
1227
|
+
if (!changed)
|
|
1228
|
+
return { text: line, changed: false };
|
|
1229
|
+
// A positive close often carries a matching search prohibition. Once the
|
|
1230
|
+
// whole response is Partial, that prohibition is contradictory too.
|
|
1231
|
+
out = out
|
|
1232
|
+
.replace(/\s*(?:[,;—-]\s*)?(?:and\s+)?do\s+\*{0,2}not\*{0,2}\s+(?:(?:re-)?(?:Read|Grep|Glob)|`?homegraph_(?:search|explore|node|callers|callees)`?)[^.;\n]*/gi, '')
|
|
1233
|
+
.replace(/\s*(?:[,;—-]\s*)?no\s+(?:glob|grep|read|search|explore)(?:\s*\/\s*(?:glob|grep|read|search|explore))*\s+needed\b[^.;\n]*/gi, '')
|
|
1234
|
+
.replace(/\(\s*[,;—-]*\s*\)/g, '')
|
|
1235
|
+
.replace(/\s{2,}/g, ' ')
|
|
1236
|
+
.replace(/\s+([.;,])/g, '$1')
|
|
1237
|
+
.replace(/^(>\s*)[\s.;,—-]+/, '$1')
|
|
1238
|
+
.trimEnd();
|
|
1239
|
+
return { text: out, changed: true };
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Drop stop-searching directives from an output that also declares itself partial.
|
|
1243
|
+
*
|
|
1244
|
+
* Section builders each append their own footer and cannot see their siblings, so
|
|
1245
|
+
* one output can carry both `**Partial locator**` ("these are anchors, keep
|
|
1246
|
+
* looking") and `**ANSWER NOW**` ("stop, do not Grep"). Measured on a 54-question
|
|
1247
|
+
* run: 21 such outputs across 17 of 48 questions. Whichever the agent obeys, the
|
|
1248
|
+
* other was wrong — it either answers from an inventory the tool just called
|
|
1249
|
+
* incomplete, or it keeps searching against an explicit prohibition.
|
|
1250
|
+
*
|
|
1251
|
+
* Partial wins: an under-informed agent that keeps looking recovers, while one
|
|
1252
|
+
* that stops on a partial answer cannot. Negated mentions (`do **not** ANSWER
|
|
1253
|
+
* NOW`) are the correct pairing and are preserved.
|
|
1254
|
+
*/
|
|
1255
|
+
function reconcilePartialAnswerNow(text) {
|
|
1256
|
+
if (!/\*\*Partial locator\*\*/i.test(text))
|
|
1257
|
+
return text;
|
|
1258
|
+
let changed = false;
|
|
1259
|
+
const out = text
|
|
1260
|
+
.split('\n')
|
|
1261
|
+
.map((line) => {
|
|
1262
|
+
if (!/ANSWER NOW/i.test(line))
|
|
1263
|
+
return line;
|
|
1264
|
+
const stripped = stripPositiveAnswerNowFromLine(line);
|
|
1265
|
+
if (stripped.changed)
|
|
1266
|
+
changed = true;
|
|
1267
|
+
return stripped.text;
|
|
1268
|
+
})
|
|
1269
|
+
.filter((line) => line.trim() !== '>')
|
|
1270
|
+
.join('\n');
|
|
1271
|
+
if (!changed)
|
|
1272
|
+
return text;
|
|
1273
|
+
return `${out}\n\n> This survey is a **Partial locator**: the sections above are anchors, not a closed answer. Continue with the narrower lookup named above, or a text search, before answering.`;
|
|
1274
|
+
}
|
|
1072
1275
|
class ToolHandler {
|
|
1073
1276
|
cg;
|
|
1074
1277
|
// Cache of opened HomeGraph instances for cross-project queries
|
|
@@ -1665,11 +1868,6 @@ class ToolHandler {
|
|
|
1665
1868
|
this.catchUpGate = null;
|
|
1666
1869
|
await this.awaitCatchUpGate(gate);
|
|
1667
1870
|
}
|
|
1668
|
-
// Hard process RSS ceiling — any tool/path that already pushed us over must
|
|
1669
|
-
// stop with success-shaped Partial (never OOM / multi-GB growth).
|
|
1670
|
-
if ((0, memory_budget_1.isOverRssBudget)()) {
|
|
1671
|
-
return (0, memory_budget_1.rssBudgetPartialResult)(toolName);
|
|
1672
|
-
}
|
|
1673
1871
|
// Honor the optional tool allowlist (HOMEGRAPH_MCP_TOOLS): a trimmed
|
|
1674
1872
|
// surface rejects ablated tools defensively even if a client cached them.
|
|
1675
1873
|
if (!this.isToolAllowed(toolName)) {
|
|
@@ -1706,6 +1904,16 @@ class ToolHandler {
|
|
|
1706
1904
|
}
|
|
1707
1905
|
}
|
|
1708
1906
|
}
|
|
1907
|
+
// Post-Partial depth fuse: Partial→node×N is the same failure class as
|
|
1908
|
+
// explore×N. Shape-driven only — one depth call, then refuse.
|
|
1909
|
+
if (sessionState
|
|
1910
|
+
&& (toolName === 'homegraph_node'
|
|
1911
|
+
|| toolName === 'homegraph_callers'
|
|
1912
|
+
|| toolName === 'homegraph_callees')) {
|
|
1913
|
+
const depthRefuse = this.tryDepthToolFuse(toolName, args, sessionState);
|
|
1914
|
+
if (depthRefuse)
|
|
1915
|
+
return depthRefuse;
|
|
1916
|
+
}
|
|
1709
1917
|
// Session-tracked explore must not hit the MCP query cache: a cache hit
|
|
1710
1918
|
// would re-serve the first call's full source and defeat CG-18 dedup.
|
|
1711
1919
|
const skipCacheForSession = toolName === 'homegraph_explore' && !!sessionState;
|
|
@@ -1756,18 +1964,27 @@ class ToolHandler {
|
|
|
1756
1964
|
try {
|
|
1757
1965
|
const cgFast = this.getHomeGraph(projectPath);
|
|
1758
1966
|
const rootFast = cgFast.getProjectRoot();
|
|
1759
|
-
const fast = (toolName === 'homegraph_explore'
|
|
1760
|
-
? this.
|
|
1967
|
+
const fast = (toolName === 'homegraph_explore'
|
|
1968
|
+
? this.trySpecializedExploreRoute(cgFast, q, rootFast)
|
|
1761
1969
|
: null)
|
|
1970
|
+
?? (toolName === 'homegraph_explore' || toolName === 'homegraph_search'
|
|
1971
|
+
? this.tryFastInventoryExplore(cgFast, q, rootFast)
|
|
1972
|
+
: null)
|
|
1762
1973
|
?? (toolName === 'homegraph_explore' || toolName === 'homegraph_search'
|
|
1763
1974
|
? this.tryLightMechanismExplore(cgFast, q, rootFast)
|
|
1764
1975
|
: null)
|
|
1765
1976
|
?? this.tryCompactLocalSymbolExplore(cgFast, q, rootFast);
|
|
1766
1977
|
if (fast) {
|
|
1767
|
-
|
|
1768
|
-
|
|
1978
|
+
// Fast explore must still file Partial/ANSWER meta into the session
|
|
1979
|
+
// so explore + depth fuses see it (textResult-only paths used to skip).
|
|
1980
|
+
let served = fast;
|
|
1981
|
+
if (toolName === 'homegraph_explore' && sessionState) {
|
|
1982
|
+
served = this.takeExploreEmission(this.ensureExploreEmission(fast, rootFast, q), sessionState);
|
|
1983
|
+
}
|
|
1984
|
+
if (cacheEnabled && cacheKey && cacheQueries && cacheIndex && !served.isError) {
|
|
1985
|
+
cacheIndex.setEntry(cacheQueries, cacheKey, toolName, served);
|
|
1769
1986
|
}
|
|
1770
|
-
const withWorktree = this.withWorktreeNotice(
|
|
1987
|
+
const withWorktree = this.withWorktreeNotice(served, projectPath);
|
|
1771
1988
|
return this.withStalenessNotice(withWorktree, projectPath);
|
|
1772
1989
|
}
|
|
1773
1990
|
}
|
|
@@ -1786,6 +2003,13 @@ class ToolHandler {
|
|
|
1786
2003
|
// `-32001`). Fast-path surveys run inside the worker via executeReadTool.
|
|
1787
2004
|
const raw = await this.runReadToolWithDeadline(toolName, dispatchArgs);
|
|
1788
2005
|
const result = this.takeExploreEmission(raw, sessionState);
|
|
2006
|
+
if (sessionState
|
|
2007
|
+
&& !result.isError
|
|
2008
|
+
&& (toolName === 'homegraph_node'
|
|
2009
|
+
|| toolName === 'homegraph_callers'
|
|
2010
|
+
|| toolName === 'homegraph_callees')) {
|
|
2011
|
+
this.noteDepthToolUse(args, sessionState);
|
|
2012
|
+
}
|
|
1789
2013
|
if (cacheEnabled && cacheKey && cacheQueries && cacheIndex && !result.isError) {
|
|
1790
2014
|
cacheIndex.setEntry(cacheQueries, cacheKey, toolName, result);
|
|
1791
2015
|
}
|
|
@@ -1832,6 +2056,43 @@ class ToolHandler {
|
|
|
1832
2056
|
}
|
|
1833
2057
|
return copy;
|
|
1834
2058
|
}
|
|
2059
|
+
/**
|
|
2060
|
+
* After a counted Partial explore, refuse further `node` / callers / callees
|
|
2061
|
+
* once the session's one allowed depth drill is spent.
|
|
2062
|
+
*/
|
|
2063
|
+
tryDepthToolFuse(toolName, args, sessionState) {
|
|
2064
|
+
try {
|
|
2065
|
+
const cg = this.getHomeGraph(args.projectPath);
|
|
2066
|
+
const root = cg.getProjectRoot();
|
|
2067
|
+
const decision = (0, explore_repeat_guard_1.decideDepthToolFuse)(sessionState.forProject(root), sessionState.depthToolCount(root), toolName);
|
|
2068
|
+
if (!decision.refuse)
|
|
2069
|
+
return null;
|
|
2070
|
+
const hint = typeof args.symbol === 'string'
|
|
2071
|
+
? args.symbol
|
|
2072
|
+
: typeof args.file === 'string'
|
|
2073
|
+
? args.file
|
|
2074
|
+
: undefined;
|
|
2075
|
+
return this.textResult((0, explore_repeat_guard_1.formatDepthToolRefuse)(decision, toolName, hint));
|
|
2076
|
+
}
|
|
2077
|
+
catch {
|
|
2078
|
+
return null;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
/** Count a successful depth-tool call when the latest explore was Partial. */
|
|
2082
|
+
noteDepthToolUse(args, sessionState) {
|
|
2083
|
+
try {
|
|
2084
|
+
const cg = this.getHomeGraph(args.projectPath);
|
|
2085
|
+
const root = cg.getProjectRoot();
|
|
2086
|
+
const prior = sessionState.forProject(root);
|
|
2087
|
+
const last = prior?.calls.length
|
|
2088
|
+
? [...prior.calls].reverse().find((c) => (c.responseBytes || 0) >= 400)
|
|
2089
|
+
: undefined;
|
|
2090
|
+
if (!last?.partial)
|
|
2091
|
+
return;
|
|
2092
|
+
sessionState.recordDepthTool(root);
|
|
2093
|
+
}
|
|
2094
|
+
catch { /* bookkeeping only */ }
|
|
2095
|
+
}
|
|
1835
2096
|
/**
|
|
1836
2097
|
* Record an explore call's emission into the caller's session state and strip
|
|
1837
2098
|
* it from the result (CG-17).
|
|
@@ -1849,6 +2110,25 @@ class ToolHandler {
|
|
|
1849
2110
|
delete result[explore_session_state_1.EXPLORE_EMISSION_KEY];
|
|
1850
2111
|
if (sessionState) {
|
|
1851
2112
|
try {
|
|
2113
|
+
const prior = sessionState.forProject(emission.projectRoot);
|
|
2114
|
+
const priorPartials = (prior?.calls ?? []).filter((c) => c.partial === true && (c.responseBytes || 0) >= 400);
|
|
2115
|
+
const metaPartial = emission.partial === true
|
|
2116
|
+
|| (emission.partial === undefined
|
|
2117
|
+
&& (0, explore_repeat_guard_1.inferExplorePartialMeta)(result.content?.[0]?.text ?? '').partial);
|
|
2118
|
+
if (metaPartial && priorPartials.length >= 1) {
|
|
2119
|
+
const text = result.content?.[0]?.text ?? '';
|
|
2120
|
+
if (text && !/Second Partial — stop HomeGraph/i.test(text)) {
|
|
2121
|
+
const stop = (0, explore_repeat_guard_1.formatSecondPartialStopFooter)(emission.nextAnchor);
|
|
2122
|
+
result = {
|
|
2123
|
+
...result,
|
|
2124
|
+
content: [
|
|
2125
|
+
{ type: 'text', text: text + stop },
|
|
2126
|
+
...result.content.slice(1),
|
|
2127
|
+
],
|
|
2128
|
+
};
|
|
2129
|
+
emission.responseBytes = (result.content[0]?.text ?? text).length;
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
1852
2132
|
sessionState.record(emission);
|
|
1853
2133
|
}
|
|
1854
2134
|
catch { /* bookkeeping only — never fail a served call */ }
|
|
@@ -1881,7 +2161,7 @@ class ToolHandler {
|
|
|
1881
2161
|
return this.queryPool.run(toolName, args, {
|
|
1882
2162
|
// Static Partial only — never DB/FTS on the soft-timeout callback
|
|
1883
2163
|
// (any sync work here can freeze the MCP transport → empty -32001).
|
|
1884
|
-
onSoftTimeout: () => this.deadlineBusyResult(deadlineMs),
|
|
2164
|
+
onSoftTimeout: () => this.deadlineBusyResult(deadlineMs, args),
|
|
1885
2165
|
});
|
|
1886
2166
|
}
|
|
1887
2167
|
return (async () => {
|
|
@@ -1894,7 +2174,7 @@ class ToolHandler {
|
|
|
1894
2174
|
// Keep this timer ref'd so a blocked sync stretch still flushes Partial
|
|
1895
2175
|
// once the event loop runs again — do not call wrapPartialBusyResult here.
|
|
1896
2176
|
timer = setTimeout(() => {
|
|
1897
|
-
resolve(this.deadlineBusyResult(deadlineMs));
|
|
2177
|
+
resolve(this.deadlineBusyResult(deadlineMs, args));
|
|
1898
2178
|
}, deadlineMs);
|
|
1899
2179
|
});
|
|
1900
2180
|
try {
|
|
@@ -1905,13 +2185,15 @@ class ToolHandler {
|
|
|
1905
2185
|
clearTimeout(timer);
|
|
1906
2186
|
}
|
|
1907
2187
|
}
|
|
1908
|
-
/** Success-shaped busy note —
|
|
1909
|
-
deadlineBusyResult(deadlineMs) {
|
|
2188
|
+
/** Success-shaped busy note — anchors from query when present; no DB. */
|
|
2189
|
+
deadlineBusyResult(deadlineMs, args) {
|
|
1910
2190
|
const secs = Math.max(1, Math.round(deadlineMs / 1000));
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
2191
|
+
const query = typeof args?.query === 'string' ? args.query : undefined;
|
|
2192
|
+
return this.textResult((0, explore_repeat_guard_1.formatPartialExploreGuidance)({
|
|
2193
|
+
seconds: secs,
|
|
2194
|
+
query,
|
|
2195
|
+
why: `HomeGraph hit its ${secs}s response deadline / is busy (MCP clients typically kill the call at ~60s with an empty timeout)`,
|
|
2196
|
+
}));
|
|
1915
2197
|
}
|
|
1916
2198
|
/**
|
|
1917
2199
|
* Run a single read tool to completion and return its raw {@link ToolResult},
|
|
@@ -1965,6 +2247,9 @@ class ToolHandler {
|
|
|
1965
2247
|
case 'homegraph_impact': return await this.handleImpact(args);
|
|
1966
2248
|
case 'homegraph_diff_impact': return await this.handleDiffImpact(args);
|
|
1967
2249
|
case 'homegraph_explore': return await this.handleExplore(args);
|
|
2250
|
+
case 'homegraph_usages': return await this.handleSpecializedUsages(args);
|
|
2251
|
+
case 'homegraph_modules': return await this.handleSpecializedModules(args);
|
|
2252
|
+
case 'homegraph_native': return await this.handleSpecializedNative(args);
|
|
1968
2253
|
case 'homegraph_node': return await this.handleNode(args);
|
|
1969
2254
|
case 'homegraph_files': return await this.handleFiles(args);
|
|
1970
2255
|
case 'homegraph_arkui_migrate': return await this.handleArkuiMigrate(args);
|
|
@@ -1987,6 +2272,17 @@ class ToolHandler {
|
|
|
1987
2272
|
// getProjectRoot) must fall through to FTS search rather than error.
|
|
1988
2273
|
try {
|
|
1989
2274
|
const projectRoot = cg.getProjectRoot();
|
|
2275
|
+
// Bare PascalCase type search — agents almost always fan out callers next;
|
|
2276
|
+
// route to caller inventory instead of FTS → node×N.
|
|
2277
|
+
if ((0, query_utils_1.queryIsTypeNameFocus)(query)) {
|
|
2278
|
+
const types = (0, query_utils_1.extractTypeNamesFromQuery)(query);
|
|
2279
|
+
if (types.length === 1) {
|
|
2280
|
+
const callerQ = `${types[0]} 哪些方法被外部调用`;
|
|
2281
|
+
const callerRedirect = this.tryFastInventoryExplore(cg, callerQ, projectRoot);
|
|
2282
|
+
if (callerRedirect)
|
|
2283
|
+
return callerRedirect;
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
1990
2286
|
if ((0, query_utils_1.queryShouldPreferExploreOverSearch)(query)) {
|
|
1991
2287
|
const exploreRedirect = this.tryFastInventoryExplore(cg, query, projectRoot)
|
|
1992
2288
|
?? this.tryLightMechanismExplore(cg, query, projectRoot)
|
|
@@ -2090,8 +2386,6 @@ class ToolHandler {
|
|
|
2090
2386
|
const callers = [];
|
|
2091
2387
|
const labels = new Map();
|
|
2092
2388
|
for (const node of defNodes) {
|
|
2093
|
-
if ((0, memory_budget_1.isOverRssBudget)())
|
|
2094
|
-
break;
|
|
2095
2389
|
for (const c of cg.getCallers(node.id)) {
|
|
2096
2390
|
if (!seen.has(c.node.id)) {
|
|
2097
2391
|
seen.add(c.node.id);
|
|
@@ -2130,7 +2424,8 @@ class ToolHandler {
|
|
|
2130
2424
|
+ includeBlock
|
|
2131
2425
|
+ '\n\n> Caller listing complete — answer from this list'
|
|
2132
2426
|
+ (includeBlock ? ' + include/import visibility' : '')
|
|
2133
|
-
+ ';
|
|
2427
|
+
+ '; do not re-Grep/Read these call sites.'
|
|
2428
|
+
+ '\n> For **all methods of a Type**, prefer one `homegraph_explore` with the Type + “哪些方法被调用” — do **not** fan out `homegraph_callers` per method.'
|
|
2134
2429
|
+ '\n> If another type\'s visibility matters, pass **both** names to `homegraph_explore` (not callers alone).'
|
|
2135
2430
|
+ note + filterNote;
|
|
2136
2431
|
return this.textResult(this.truncateOutput(formatted));
|
|
@@ -2138,23 +2433,50 @@ class ToolHandler {
|
|
|
2138
2433
|
// Multiple DISTINCT definitions (#764): one section per definition so an
|
|
2139
2434
|
// agent never mistakes one app's callers for another's. Narrow with
|
|
2140
2435
|
// `file` to focus a single definition.
|
|
2436
|
+
// Hard stop when the name is still massively ambiguous — dumping dozens of
|
|
2437
|
+
// definition sections is a token bomb and teaches a Grep/Read storm.
|
|
2438
|
+
const MAX_CALLER_DEF_GROUPS = 8;
|
|
2439
|
+
if (groups.length > MAX_CALLER_DEF_GROUPS) {
|
|
2440
|
+
const sample = groups.slice(0, MAX_CALLER_DEF_GROUPS).map((g) => {
|
|
2441
|
+
const h = g[0];
|
|
2442
|
+
return `- \`${h.qualifiedName || h.name}\` (${h.kind}) — \`${h.filePath}:${h.startLine || '?'}\``;
|
|
2443
|
+
});
|
|
2444
|
+
return this.textResult([
|
|
2445
|
+
`**Callers of ${symbol} — ${groups.length} distinct definitions (too ambiguous to expand).**`,
|
|
2446
|
+
'',
|
|
2447
|
+
'Re-call `homegraph_callers` with `file` set to one definition path (or use `homegraph_explore` with the owning Type name). Do **not** Grep/Read the whole repo.',
|
|
2448
|
+
'',
|
|
2449
|
+
'Sample definitions:',
|
|
2450
|
+
...sample,
|
|
2451
|
+
groups.length > MAX_CALLER_DEF_GROUPS
|
|
2452
|
+
? `- … +${groups.length - MAX_CALLER_DEF_GROUPS} more`
|
|
2453
|
+
: '',
|
|
2454
|
+
].filter(Boolean).join('\n') + filterNote);
|
|
2455
|
+
}
|
|
2141
2456
|
const lines = [
|
|
2142
2457
|
`**Callers of ${symbol} — ${groups.length} distinct definitions (narrow with \`file\`)**`,
|
|
2143
2458
|
];
|
|
2459
|
+
let anyCallers = false;
|
|
2144
2460
|
for (const group of groups) {
|
|
2145
2461
|
const { callers, labels } = collect(group);
|
|
2146
2462
|
lines.push('', this.definitionHeading(group));
|
|
2147
2463
|
if (callers.length === 0) {
|
|
2148
|
-
lines.push('- (no callers)');
|
|
2464
|
+
lines.push('- (no callers indexed)');
|
|
2149
2465
|
continue;
|
|
2150
2466
|
}
|
|
2467
|
+
anyCallers = true;
|
|
2151
2468
|
for (const node of callers.slice(0, limit)) {
|
|
2152
2469
|
const location = node.startLine ? `:${node.startLine}` : '';
|
|
2153
2470
|
const label = labels.get(node.id);
|
|
2154
2471
|
lines.push(`- ${node.name} (${node.kind}) - ${node.filePath}${location}${label ? ` — via ${label}` : ''}`);
|
|
2155
2472
|
}
|
|
2156
2473
|
}
|
|
2157
|
-
|
|
2474
|
+
const footer = anyCallers
|
|
2475
|
+
? '\n\n> Caller listing complete — answer from this list; no read/grep needed.'
|
|
2476
|
+
: '\n\n> **No call edges indexed** for these definitions (common for callbacks / bus registration). '
|
|
2477
|
+
+ 'HomeGraph located the symbol(s) above — a **narrow** Grep for the registration site is OK; '
|
|
2478
|
+
+ 'do not open a repo-wide Grep storm. Prefer `homegraph_explore` with a concrete registrar Type if known.';
|
|
2479
|
+
return this.textResult(this.truncateOutput(lines.join('\n') + filterNote + footer));
|
|
2158
2480
|
}
|
|
2159
2481
|
/**
|
|
2160
2482
|
* Handle homegraph_callees
|
|
@@ -2212,6 +2534,22 @@ class ToolHandler {
|
|
|
2212
2534
|
return this.textResult(this.truncateOutput(formatted));
|
|
2213
2535
|
}
|
|
2214
2536
|
// Multiple DISTINCT definitions (#764): per-definition sections.
|
|
2537
|
+
const MAX_CALLEE_DEF_GROUPS = 8;
|
|
2538
|
+
if (groups.length > MAX_CALLEE_DEF_GROUPS) {
|
|
2539
|
+
const sample = groups.slice(0, MAX_CALLEE_DEF_GROUPS).map((g) => {
|
|
2540
|
+
const h = g[0];
|
|
2541
|
+
return `- \`${h.qualifiedName || h.name}\` (${h.kind}) — \`${h.filePath}:${h.startLine || '?'}\``;
|
|
2542
|
+
});
|
|
2543
|
+
return this.textResult([
|
|
2544
|
+
`**Callees of ${symbol} — ${groups.length} distinct definitions (too ambiguous to expand).**`,
|
|
2545
|
+
'',
|
|
2546
|
+
'Re-call `homegraph_callees` with `file` set to one definition path (or `homegraph_explore` with the owning Type). Do **not** Grep/Read the whole repo.',
|
|
2547
|
+
'',
|
|
2548
|
+
'Sample definitions:',
|
|
2549
|
+
...sample,
|
|
2550
|
+
`- … +${groups.length - MAX_CALLEE_DEF_GROUPS} more`,
|
|
2551
|
+
].join('\n') + filterNote);
|
|
2552
|
+
}
|
|
2215
2553
|
const lines = [
|
|
2216
2554
|
`**Callees of ${symbol} — ${groups.length} distinct definitions (narrow with \`file\`)**`,
|
|
2217
2555
|
];
|
|
@@ -3141,12 +3479,19 @@ class ToolHandler {
|
|
|
3141
3479
|
buildImportSitesSection(cg, query, projectRoot) {
|
|
3142
3480
|
const kitTerms = (0, query_utils_1.extractKitModuleNamesFromQuery)(query);
|
|
3143
3481
|
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
|
-
|
|
3482
|
+
// Named kit export (taskpool) stays; NL leftovers must not become focusExports.
|
|
3483
|
+
// When a *Kit is named and there is no real submodule, search by kit path only.
|
|
3484
|
+
const kitSubmodules = (0, query_utils_1.extractKitSubmoduleNamesFromQuery)(query);
|
|
3485
|
+
let focusExports = [
|
|
3145
3486
|
...new Set([
|
|
3146
|
-
...
|
|
3487
|
+
...kitSubmodules,
|
|
3147
3488
|
...depSymbols.filter((s) => s.length >= 4),
|
|
3148
3489
|
]),
|
|
3149
3490
|
];
|
|
3491
|
+
if (kitTerms.length > 0 && kitSubmodules.length === 0) {
|
|
3492
|
+
// Drop non-distinctive leftovers; kit module path is the inventory key.
|
|
3493
|
+
focusExports = focusExports.filter((s) => (0, query_utils_1.isDistinctiveIdentifier)(s) && s.length >= 5);
|
|
3494
|
+
}
|
|
3150
3495
|
const kitSearchTerms = (0, query_utils_1.extractImportSearchTerms)(query);
|
|
3151
3496
|
// Named export/API + kit (or usage-survey intent) → return the full matching list.
|
|
3152
3497
|
const completeInventory = focusExports.length > 0
|
|
@@ -3466,14 +3811,13 @@ class ToolHandler {
|
|
|
3466
3811
|
/**
|
|
3467
3812
|
* Multi-module / circular dependency survey — lean cycle list, not an import flood.
|
|
3468
3813
|
*/
|
|
3469
|
-
|
|
3470
|
-
// Prefer leaf module tokens (`launchercommon`) over shared parents (`staticcommon`)
|
|
3471
|
-
// — parent matches flood unrelated windowscene cycles.
|
|
3814
|
+
extractFocusedModuleNames(query) {
|
|
3472
3815
|
const leafModules = new Set();
|
|
3473
3816
|
for (const p of (query.match(/\b[A-Za-z][\w]*(?:common|service|component|constants)\b/gi) ?? [])) {
|
|
3474
3817
|
const low = p.toLowerCase();
|
|
3475
|
-
if (!/^(?:static|feature|product|base|common)$/i.test(low))
|
|
3818
|
+
if (!/^(?:static|staticcommon|feature|product|base|basecommon|common)$/i.test(low)) {
|
|
3476
3819
|
leafModules.add(low);
|
|
3820
|
+
}
|
|
3477
3821
|
}
|
|
3478
3822
|
for (const seg of query.match(/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)+/g) ?? []) {
|
|
3479
3823
|
const leaf = seg.split(/[/\\]/).pop()?.toLowerCase();
|
|
@@ -3481,6 +3825,12 @@ class ToolHandler {
|
|
|
3481
3825
|
leafModules.add(leaf);
|
|
3482
3826
|
}
|
|
3483
3827
|
}
|
|
3828
|
+
return leafModules;
|
|
3829
|
+
}
|
|
3830
|
+
buildModuleDependencySurveySection(cg, query) {
|
|
3831
|
+
// Prefer leaf module tokens (`launchercommon`) over shared parents (`staticcommon`)
|
|
3832
|
+
// — parent matches flood unrelated windowscene cycles.
|
|
3833
|
+
const leafModules = this.extractFocusedModuleNames(query);
|
|
3484
3834
|
if (leafModules.size === 0)
|
|
3485
3835
|
return { section: '', hitCount: 0 };
|
|
3486
3836
|
const rel = (p) => p.replace(/\\/g, '/');
|
|
@@ -3533,6 +3883,155 @@ class ToolHandler {
|
|
|
3533
3883
|
lines.push('');
|
|
3534
3884
|
return { section: lines.join('\n'), hitCount: matchedCycles.length };
|
|
3535
3885
|
}
|
|
3886
|
+
/** Bounded manifest-only dependency graph for modules named in the query. */
|
|
3887
|
+
buildFocusedModuleManifestSection(projectRoot, query) {
|
|
3888
|
+
const moduleNames = this.extractFocusedModuleNames(query);
|
|
3889
|
+
if (moduleNames.size === 0)
|
|
3890
|
+
return { section: '', manifestCount: 0, cycleCount: 0 };
|
|
3891
|
+
const skipDirs = new Set([
|
|
3892
|
+
'node_modules', 'oh_modules', '.git', '.homegraph', '.hvigor', '.preview',
|
|
3893
|
+
'build', 'dist', 'out', 'test', 'ohosTest',
|
|
3894
|
+
]);
|
|
3895
|
+
const queue = [{ rel: '', depth: 0 }];
|
|
3896
|
+
const manifests = new Map();
|
|
3897
|
+
const linterFiles = [];
|
|
3898
|
+
const asksDetection = /构建系统|如何检测|怎么检测|检测方式|linter|build\s+system|how.{0,24}detect/i.test(query);
|
|
3899
|
+
let visited = 0;
|
|
3900
|
+
let cursor = 0;
|
|
3901
|
+
while (cursor < queue.length && visited < 8000) {
|
|
3902
|
+
const { rel, depth } = queue[cursor++];
|
|
3903
|
+
visited++;
|
|
3904
|
+
const abs = (0, path_1.resolve)(projectRoot, rel);
|
|
3905
|
+
let entries;
|
|
3906
|
+
try {
|
|
3907
|
+
entries = (0, fs_1.readdirSync)(abs, { withFileTypes: true });
|
|
3908
|
+
}
|
|
3909
|
+
catch {
|
|
3910
|
+
continue;
|
|
3911
|
+
}
|
|
3912
|
+
const leaf = rel.split('/').pop()?.toLowerCase() ?? '';
|
|
3913
|
+
if (moduleNames.has(leaf) && entries.some((entry) => entry.isFile() && entry.name === 'oh-package.json5')) {
|
|
3914
|
+
const paths = manifests.get(leaf) ?? [];
|
|
3915
|
+
if (paths.length < 3)
|
|
3916
|
+
paths.push(rel ? `${rel}/oh-package.json5` : 'oh-package.json5');
|
|
3917
|
+
manifests.set(leaf, paths);
|
|
3918
|
+
}
|
|
3919
|
+
for (const entry of entries) {
|
|
3920
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
3921
|
+
if (entry.isFile()) {
|
|
3922
|
+
if (/^(?:code-)?linter.*\.json5?$/i.test(entry.name) && linterFiles.length < 8) {
|
|
3923
|
+
linterFiles.push(childRel);
|
|
3924
|
+
}
|
|
3925
|
+
continue;
|
|
3926
|
+
}
|
|
3927
|
+
if (!entry.isDirectory() || depth >= 6)
|
|
3928
|
+
continue;
|
|
3929
|
+
if (entry.name.startsWith('.') || skipDirs.has(entry.name))
|
|
3930
|
+
continue;
|
|
3931
|
+
queue.push({ rel: childRel, depth: depth + 1 });
|
|
3932
|
+
}
|
|
3933
|
+
if (manifests.size === moduleNames.size && (!asksDetection || linterFiles.length > 0))
|
|
3934
|
+
break;
|
|
3935
|
+
}
|
|
3936
|
+
if (manifests.size === 0)
|
|
3937
|
+
return { section: '', manifestCount: 0, cycleCount: 0 };
|
|
3938
|
+
const normalizeDependency = (name) => {
|
|
3939
|
+
const noProtocol = name.replace(/^file:/i, '').replace(/\\/g, '/');
|
|
3940
|
+
return noProtocol.split('/').filter(Boolean).pop()?.toLowerCase() ?? '';
|
|
3941
|
+
};
|
|
3942
|
+
const graph = new Map();
|
|
3943
|
+
const rows = [];
|
|
3944
|
+
for (const moduleName of [...moduleNames].sort()) {
|
|
3945
|
+
const manifestPaths = manifests.get(moduleName) ?? [];
|
|
3946
|
+
if (manifestPaths.length === 0) {
|
|
3947
|
+
rows.push(`- \`${moduleName}\` — manifest not found within the bounded project scan`);
|
|
3948
|
+
graph.set(moduleName, new Set());
|
|
3949
|
+
continue;
|
|
3950
|
+
}
|
|
3951
|
+
const manifestRel = manifestPaths[0];
|
|
3952
|
+
let parsed;
|
|
3953
|
+
try {
|
|
3954
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
3955
|
+
parsed = require('jsonc-parser').parse((0, fs_1.readFileSync)((0, path_1.resolve)(projectRoot, manifestRel), 'utf-8'));
|
|
3956
|
+
}
|
|
3957
|
+
catch {
|
|
3958
|
+
rows.push(`- \`${moduleName}\` — could not parse \`${manifestRel}\``);
|
|
3959
|
+
graph.set(moduleName, new Set());
|
|
3960
|
+
continue;
|
|
3961
|
+
}
|
|
3962
|
+
const deps = parsed?.dependencies;
|
|
3963
|
+
const declared = deps && typeof deps === 'object' ? Object.entries(deps) : [];
|
|
3964
|
+
const namedDeps = new Set();
|
|
3965
|
+
for (const [depName, depValue] of declared) {
|
|
3966
|
+
const candidates = [normalizeDependency(depName)];
|
|
3967
|
+
if (typeof depValue === 'string')
|
|
3968
|
+
candidates.push(normalizeDependency(depValue));
|
|
3969
|
+
for (const candidate of candidates) {
|
|
3970
|
+
if (candidate !== moduleName && moduleNames.has(candidate))
|
|
3971
|
+
namedDeps.add(candidate);
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
graph.set(moduleName, namedDeps);
|
|
3975
|
+
const target = namedDeps.size > 0
|
|
3976
|
+
? [...namedDeps].sort().map((name) => `\`${name}\``).join(', ')
|
|
3977
|
+
: '(none among the named modules)';
|
|
3978
|
+
rows.push(`- \`${moduleName}\` → ${target} — \`${manifestRel}\` (${declared.length} declared dependencies total)`);
|
|
3979
|
+
}
|
|
3980
|
+
const cycles = new Set();
|
|
3981
|
+
const visit = (start, current, path, seen) => {
|
|
3982
|
+
for (const next of graph.get(current) ?? []) {
|
|
3983
|
+
if (next === start && path.length > 1) {
|
|
3984
|
+
const core = path;
|
|
3985
|
+
const rotations = core.map((_, index) => {
|
|
3986
|
+
const rotated = [...core.slice(index), ...core.slice(0, index)];
|
|
3987
|
+
return [...rotated, rotated[0]].join(' -> ');
|
|
3988
|
+
});
|
|
3989
|
+
cycles.add(rotations.sort()[0]);
|
|
3990
|
+
continue;
|
|
3991
|
+
}
|
|
3992
|
+
if (seen.has(next))
|
|
3993
|
+
continue;
|
|
3994
|
+
visit(start, next, [...path, next], new Set([...seen, next]));
|
|
3995
|
+
}
|
|
3996
|
+
};
|
|
3997
|
+
for (const moduleName of graph.keys())
|
|
3998
|
+
visit(moduleName, moduleName, [moduleName], new Set([moduleName]));
|
|
3999
|
+
const lines = [
|
|
4000
|
+
'**Named module manifest dependencies**', '', ...rows, '',
|
|
4001
|
+
cycles.size > 0
|
|
4002
|
+
? `Detected **${cycles.size}** cycle(s) among the named modules:`
|
|
4003
|
+
: 'Detected **no cycle** among the named modules.',
|
|
4004
|
+
];
|
|
4005
|
+
for (const cycle of cycles)
|
|
4006
|
+
lines.push(`- \`${cycle}\``);
|
|
4007
|
+
if (asksDetection) {
|
|
4008
|
+
lines.push('', '**Build / linter cycle checks**', '');
|
|
4009
|
+
let matched = 0;
|
|
4010
|
+
for (const rel of linterFiles) {
|
|
4011
|
+
let fileLines = [];
|
|
4012
|
+
try {
|
|
4013
|
+
fileLines = (0, fs_1.readFileSync)((0, path_1.resolve)(projectRoot, rel), 'utf-8').split('\n');
|
|
4014
|
+
}
|
|
4015
|
+
catch {
|
|
4016
|
+
continue;
|
|
4017
|
+
}
|
|
4018
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
4019
|
+
const line = fileLines[i] ?? '';
|
|
4020
|
+
if (!/no-cycle|circular|cycle/i.test(line))
|
|
4021
|
+
continue;
|
|
4022
|
+
lines.push(`- \`${rel}:${i + 1}\` — \`${line.trim().slice(0, 140)}\``);
|
|
4023
|
+
if (++matched >= 12)
|
|
4024
|
+
break;
|
|
4025
|
+
}
|
|
4026
|
+
if (matched >= 12)
|
|
4027
|
+
break;
|
|
4028
|
+
}
|
|
4029
|
+
if (matched === 0)
|
|
4030
|
+
lines.push('- No explicit cycle-check rule was found in the bounded linter-config scan.');
|
|
4031
|
+
}
|
|
4032
|
+
lines.push('');
|
|
4033
|
+
return { section: lines.join('\n'), manifestCount: manifests.size, cycleCount: cycles.size };
|
|
4034
|
+
}
|
|
3536
4035
|
/**
|
|
3537
4036
|
* Path-module / named-Type NAPI export surface — not a 50-file domain dump.
|
|
3538
4037
|
*/
|
|
@@ -3544,7 +4043,7 @@ class ToolHandler {
|
|
|
3544
4043
|
...types.map((t) => t.toLowerCase()),
|
|
3545
4044
|
].filter((n) => n.length >= 4);
|
|
3546
4045
|
if (needles.length === 0)
|
|
3547
|
-
return { section: '', hitCount: 0 };
|
|
4046
|
+
return { section: '', hitCount: 0, shellOnly: false };
|
|
3548
4047
|
const rel = (p) => p.replace(/\\/g, '/');
|
|
3549
4048
|
const inScope = (fp) => {
|
|
3550
4049
|
const low = rel(fp).toLowerCase();
|
|
@@ -3583,12 +4082,16 @@ class ToolHandler {
|
|
|
3583
4082
|
const fileLines = content.split('\n');
|
|
3584
4083
|
for (let i = 0; i < fileLines.length; i++) {
|
|
3585
4084
|
const line = fileLines[i] ?? '';
|
|
3586
|
-
// napi_define_function /
|
|
4085
|
+
// napi_define_function / properties table rows:
|
|
4086
|
+
// {"draw", nullptr, PluginManager::NapiDraw, ...}
|
|
4087
|
+
// (qualified C++ method names — `\w+` alone misses `::`)
|
|
3587
4088
|
const m = line.match(/napi_define_(?:function|property)\s*\([^,]+,\s*["']([\w]+)["']/i)
|
|
3588
|
-
|| line.match(/\{\s*["']([\w]+)["']\s*,\s*(?:nullptr|NULL)
|
|
4089
|
+
|| line.match(/\{\s*["']([\w]+)["']\s*,\s*(?:nullptr|NULL)\s*,\s*[\w:]+/i)
|
|
3589
4090
|
|| line.match(/\.?(?:exports|export)\s*[:=].*["']([\w]+)["']/i);
|
|
3590
4091
|
if (!m?.[1] || m[1].length < 2)
|
|
3591
4092
|
continue;
|
|
4093
|
+
if (/^(?:nullptr|NULL|napi_default|env|exports)$/i.test(m[1]))
|
|
4094
|
+
continue;
|
|
3592
4095
|
const key = `${m[1]}:${rel(r.node.filePath)}`;
|
|
3593
4096
|
if (seen.has(key))
|
|
3594
4097
|
continue;
|
|
@@ -3600,7 +4103,7 @@ class ToolHandler {
|
|
|
3600
4103
|
snippet: line.trim().slice(0, 100),
|
|
3601
4104
|
});
|
|
3602
4105
|
}
|
|
3603
|
-
// Also list Export / Init method nodes in-scope.
|
|
4106
|
+
// Also list Export / Init method nodes in-scope (entry shells — not the API surface).
|
|
3604
4107
|
if (/^(?:Export|Init|NapiInit)$/i.test(r.node.name)) {
|
|
3605
4108
|
const key = `fn:${r.node.name}:${rel(r.node.filePath)}`;
|
|
3606
4109
|
if (!seen.has(key)) {
|
|
@@ -3615,24 +4118,34 @@ class ToolHandler {
|
|
|
3615
4118
|
}
|
|
3616
4119
|
}
|
|
3617
4120
|
}
|
|
4121
|
+
const apiExports = exports.filter((e) => !/^(?:Export|Init|NapiInit)$/i.test(e.name));
|
|
4122
|
+
const shellOnly = apiExports.length === 0 && exports.length > 0;
|
|
3618
4123
|
const lines = [
|
|
3619
4124
|
'**NAPI / native export survey**',
|
|
3620
4125
|
'',
|
|
3621
|
-
|
|
4126
|
+
shellOnly
|
|
4127
|
+
? `> Found Init/Export shells under \`${needles.slice(0, 4).join('`, `')}\` but no \`napi_define_properties\` / descriptor API names. `
|
|
4128
|
+
+ '**Partial locator** — Read the Init body / \`napi_property_descriptor\` table next; do **not** ANSWER NOW as a full API list.'
|
|
4129
|
+
: `> In-repo NAPI surface for \`${needles.slice(0, 4).join('`, `')}\`. **ANSWER NOW** — do not Grep \`napi_define\` again.`,
|
|
3622
4130
|
'',
|
|
3623
4131
|
];
|
|
3624
4132
|
if (exports.length === 0) {
|
|
3625
4133
|
lines.push('- No `napi_define_*` / Export descriptors found under the named path/Type.');
|
|
3626
4134
|
lines.push('');
|
|
3627
|
-
return { section: lines.join('\n'), hitCount: 0 };
|
|
4135
|
+
return { section: lines.join('\n'), hitCount: 0, shellOnly: false };
|
|
3628
4136
|
}
|
|
3629
|
-
|
|
4137
|
+
const ranked = shellOnly ? exports : (apiExports.length > 0 ? apiExports : exports);
|
|
4138
|
+
for (const e of ranked.slice(0, 30)) {
|
|
3630
4139
|
lines.push(`- \`${e.name}\` — \`${e.file}:${e.line}\` \`${e.snippet}\``);
|
|
3631
4140
|
}
|
|
3632
|
-
if (
|
|
3633
|
-
lines.push(`- … and ${
|
|
4141
|
+
if (ranked.length > 30)
|
|
4142
|
+
lines.push(`- … and ${ranked.length - 30} more`);
|
|
3634
4143
|
lines.push('');
|
|
3635
|
-
return {
|
|
4144
|
+
return {
|
|
4145
|
+
section: lines.join('\n'),
|
|
4146
|
+
hitCount: Math.min(ranked.length, 30),
|
|
4147
|
+
shellOnly,
|
|
4148
|
+
};
|
|
3636
4149
|
}
|
|
3637
4150
|
/**
|
|
3638
4151
|
* Named `.d.ts` wrap — where the basename is imported / called in-repo.
|
|
@@ -3779,7 +4292,7 @@ class ToolHandler {
|
|
|
3779
4292
|
hits.set(fp, prev);
|
|
3780
4293
|
matched = true;
|
|
3781
4294
|
}
|
|
3782
|
-
if (!matched && /Toggle|Switch|
|
|
4295
|
+
if (!matched && /Toggle|Switch|Manager/i.test(r.node.name)) {
|
|
3783
4296
|
hits.set(fp, {
|
|
3784
4297
|
lines: [r.node.startLine],
|
|
3785
4298
|
snippet: (r.node.signature || r.node.name).slice(0, 100),
|
|
@@ -3811,28 +4324,59 @@ class ToolHandler {
|
|
|
3811
4324
|
}
|
|
3812
4325
|
buildDataSourceSection(cg, query) {
|
|
3813
4326
|
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 };
|
|
4327
|
+
if (anchors.length === 0) {
|
|
4328
|
+
return { section: '', edgeCount: 0, sdkImportCount: 0, strongCount: 0 };
|
|
4329
|
+
}
|
|
3816
4330
|
const rel = (p) => p.replace(/\\/g, '/');
|
|
3817
|
-
//
|
|
3818
|
-
const
|
|
4331
|
+
// ArkTS anonymous methods (`%AM0$foo`) are graph-internal — never "system services".
|
|
4332
|
+
const isAnonName = (n) => /^%AM\d+/i.test(n);
|
|
4333
|
+
// Strong service surface — NOT bare "badge" (that matched local getAllBadge / %AM*$badge*).
|
|
4334
|
+
const STRONG_SERVICE_RE = /@ohos\.|@kit\.|ohos\.|notificationManager|notificationSubscribe|SubscribeManager|CommonEvent|telephony|bundleManager|wantAgent|osAccount|AppAccount|distributedAccount|accountManager|vibrator/i;
|
|
4335
|
+
/** Domain kinship for sibling/writer Managers — stem match or service surface (not bare "ability"). */
|
|
4336
|
+
const relatedToAnchor = (stem, sig) => {
|
|
4337
|
+
const sl = stem.toLowerCase();
|
|
4338
|
+
const blob = sig.toLowerCase();
|
|
4339
|
+
if (sl.length >= 3 && blob.includes(sl))
|
|
4340
|
+
return true;
|
|
4341
|
+
if (STRONG_SERVICE_RE.test(sig))
|
|
4342
|
+
return true;
|
|
4343
|
+
if (/notification|numbadge|subscribe|commonevent/i.test(blob))
|
|
4344
|
+
return true;
|
|
4345
|
+
// Account* anchors may relate via account path without sharing the stem letters.
|
|
4346
|
+
if (/^account$/i.test(stem) && /account|osaccount|appaccount/i.test(blob))
|
|
4347
|
+
return true;
|
|
4348
|
+
return false;
|
|
4349
|
+
};
|
|
3819
4350
|
const lines = ['**Data sources / upstream services**', ''];
|
|
3820
|
-
|
|
3821
|
-
const
|
|
4351
|
+
const sdkIds = new Set();
|
|
4352
|
+
const strongIds = new Set();
|
|
4353
|
+
const addEdge = (bucket, node, via, rank, kind, opts) => {
|
|
3822
4354
|
if ((0, query_utils_1.isTestFile)(node.filePath))
|
|
3823
4355
|
return;
|
|
3824
|
-
|
|
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))
|
|
4356
|
+
if (isAnonName(node.name))
|
|
3828
4357
|
return;
|
|
3829
|
-
|
|
4358
|
+
const sig = `${node.name} ${node.filePath} ${node.signature || ''}`;
|
|
4359
|
+
if (opts?.importAlways) {
|
|
4360
|
+
if (!/@ohos\.|@kit\.|ohos\./i.test(sig))
|
|
4361
|
+
return;
|
|
4362
|
+
}
|
|
4363
|
+
else if (kind === 'strong') {
|
|
4364
|
+
// caller / related / strong-callee paths already pre-filtered
|
|
4365
|
+
}
|
|
4366
|
+
else if (!STRONG_SERVICE_RE.test(sig)) {
|
|
3830
4367
|
return;
|
|
4368
|
+
}
|
|
3831
4369
|
const prev = bucket.get(node.id);
|
|
3832
4370
|
if (!prev || rank < prev.rank) {
|
|
3833
|
-
bucket.set(node.id, { name: node.name, file: rel(node.filePath), line: node.startLine, via, rank });
|
|
3834
|
-
if (
|
|
3835
|
-
|
|
4371
|
+
bucket.set(node.id, { name: node.name, file: rel(node.filePath), line: node.startLine, via, rank, kind });
|
|
4372
|
+
if (kind === 'sdk') {
|
|
4373
|
+
sdkIds.add(node.id);
|
|
4374
|
+
strongIds.delete(node.id);
|
|
4375
|
+
}
|
|
4376
|
+
else {
|
|
4377
|
+
if (!sdkIds.has(node.id))
|
|
4378
|
+
strongIds.add(node.id);
|
|
4379
|
+
}
|
|
3836
4380
|
}
|
|
3837
4381
|
};
|
|
3838
4382
|
for (const name of anchors.slice(0, 3)) {
|
|
@@ -3844,13 +4388,14 @@ class ToolHandler {
|
|
|
3844
4388
|
if (e.kind !== 'contains')
|
|
3845
4389
|
continue;
|
|
3846
4390
|
const m = cg.getNode(e.target);
|
|
3847
|
-
if (m && (m.kind === 'method' || m.kind === 'function'))
|
|
4391
|
+
if (m && (m.kind === 'method' || m.kind === 'function') && !isAnonName(m.name))
|
|
3848
4392
|
methods.push(m);
|
|
3849
4393
|
}
|
|
3850
4394
|
if (methods.length === 0) {
|
|
3851
4395
|
for (const n of cg.getNodesByName(name)) {
|
|
3852
|
-
if (n.filePath === cls.filePath && (n.kind === 'method' || n.kind === 'function'))
|
|
4396
|
+
if (n.filePath === cls.filePath && (n.kind === 'method' || n.kind === 'function') && !isAnonName(n.name)) {
|
|
3853
4397
|
methods.push(n);
|
|
4398
|
+
}
|
|
3854
4399
|
}
|
|
3855
4400
|
}
|
|
3856
4401
|
// 1) Same-file @ohos / kit imports — the usual system-service surface.
|
|
@@ -3858,26 +4403,26 @@ class ToolHandler {
|
|
|
3858
4403
|
for (const r of cg.searchNodes('@ohos', { kinds: ['import'], limit: 40 })) {
|
|
3859
4404
|
if (r.node.filePath !== cls.filePath)
|
|
3860
4405
|
continue;
|
|
3861
|
-
addEdge(upstream, r.node, 'import', 0, { importAlways: true });
|
|
4406
|
+
addEdge(upstream, r.node, 'import', 0, 'sdk', { importAlways: true });
|
|
3862
4407
|
}
|
|
3863
4408
|
for (const r of cg.searchNodes('@kit', { kinds: ['import'], limit: 40 })) {
|
|
3864
4409
|
if (r.node.filePath !== cls.filePath)
|
|
3865
4410
|
continue;
|
|
3866
|
-
addEdge(upstream, r.node, 'import', 0, { importAlways: true });
|
|
4411
|
+
addEdge(upstream, r.node, 'import', 0, 'sdk', { importAlways: true });
|
|
3867
4412
|
}
|
|
3868
4413
|
for (const term of [
|
|
3869
|
-
'notification', '
|
|
3870
|
-
'account', 'osAccount', 'AppAccount', 'distributedAccount',
|
|
4414
|
+
'notification', 'subscribe', 'bundle', 'ability',
|
|
4415
|
+
'account', 'osAccount', 'AppAccount', 'distributedAccount', 'telephony',
|
|
3871
4416
|
]) {
|
|
3872
4417
|
for (const r of cg.searchNodes(term, { kinds: ['import'], limit: 30 })) {
|
|
3873
4418
|
if (r.node.filePath !== cls.filePath)
|
|
3874
4419
|
continue;
|
|
3875
|
-
addEdge(upstream, r.node, 'import', 1, { importAlways: true });
|
|
4420
|
+
addEdge(upstream, r.node, 'import', 1, 'sdk', { importAlways: true });
|
|
3876
4421
|
}
|
|
3877
4422
|
}
|
|
3878
4423
|
}
|
|
3879
4424
|
catch { /* */ }
|
|
3880
|
-
// 2) Callees that look like
|
|
4425
|
+
// 2) Callees that look like SDK / system service APIs (not local cache helpers).
|
|
3881
4426
|
for (const method of methods.slice(0, 25)) {
|
|
3882
4427
|
let callees = [];
|
|
3883
4428
|
try {
|
|
@@ -3887,9 +4432,56 @@ class ToolHandler {
|
|
|
3887
4432
|
continue;
|
|
3888
4433
|
}
|
|
3889
4434
|
for (const c of callees) {
|
|
3890
|
-
if (c?.node)
|
|
3891
|
-
|
|
4435
|
+
if (!c?.node || isAnonName(c.node.name))
|
|
4436
|
+
continue;
|
|
4437
|
+
const sig = `${c.node.name} ${c.node.filePath} ${c.node.signature || ''}`;
|
|
4438
|
+
if (!STRONG_SERVICE_RE.test(sig))
|
|
4439
|
+
continue;
|
|
4440
|
+
addEdge(upstream, c.node, method.name, 2, 'strong');
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
// 3) Writers: other Managers that call this Manager's update/set/on APIs.
|
|
4444
|
+
const stem = name.replace(/(?:Manager|Service|Handler|Store|Provider|Controller)$/i, '');
|
|
4445
|
+
for (const method of methods.slice(0, 20)) {
|
|
4446
|
+
if (!/^(?:update|set|on|subscribe|refresh|init|add|put)/i.test(method.name))
|
|
4447
|
+
continue;
|
|
4448
|
+
let callers = [];
|
|
4449
|
+
try {
|
|
4450
|
+
callers = cg.getCallers(method.id);
|
|
3892
4451
|
}
|
|
4452
|
+
catch {
|
|
4453
|
+
continue;
|
|
4454
|
+
}
|
|
4455
|
+
for (const c of callers.slice(0, 12)) {
|
|
4456
|
+
if (!c?.node || isAnonName(c.node.name) || (0, query_utils_1.isTestFile)(c.node.filePath))
|
|
4457
|
+
continue;
|
|
4458
|
+
if (c.node.filePath === cls.filePath)
|
|
4459
|
+
continue;
|
|
4460
|
+
const sig = `${c.node.name} ${c.node.filePath}`;
|
|
4461
|
+
if (!/(?:Manager|Service|Handler|Controller)$/i.test(c.node.name))
|
|
4462
|
+
continue;
|
|
4463
|
+
if (!relatedToAnchor(stem, sig))
|
|
4464
|
+
continue;
|
|
4465
|
+
addEdge(upstream, c.node, `caller→${method.name}`, 1, 'strong');
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
4468
|
+
// 4) Sibling *Manager types in the same domain (stem match).
|
|
4469
|
+
if (stem.length >= 3) {
|
|
4470
|
+
try {
|
|
4471
|
+
for (const r of cg.searchNodes(stem, { kinds: ['class', 'struct', 'component'], limit: 24 })) {
|
|
4472
|
+
const n = r.node;
|
|
4473
|
+
if (n.name === name || (0, query_utils_1.isTestFile)(n.filePath) || isAnonName(n.name))
|
|
4474
|
+
continue;
|
|
4475
|
+
if (!/(?:Manager|Service|Handler|Controller)$/i.test(n.name))
|
|
4476
|
+
continue;
|
|
4477
|
+
if (!relatedToAnchor(stem, `${n.name} ${n.filePath}`))
|
|
4478
|
+
continue;
|
|
4479
|
+
// Prefer Subscribe / notification writers over weakly related Ability Managers.
|
|
4480
|
+
const rank = /Subscribe|notification/i.test(`${n.name} ${n.filePath}`) ? 2 : 3;
|
|
4481
|
+
addEdge(upstream, n, 'related Manager', rank, 'strong');
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
catch { /* */ }
|
|
3893
4485
|
}
|
|
3894
4486
|
if (upstream.size === 0)
|
|
3895
4487
|
continue;
|
|
@@ -3901,42 +4493,589 @@ class ToolHandler {
|
|
|
3901
4493
|
if (ranked.length > 15)
|
|
3902
4494
|
lines.push(`- … and ${ranked.length - 15} more upstream symbol(s)`);
|
|
3903
4495
|
lines.push('');
|
|
3904
|
-
lines.push('> Prefer `@ohos.*` / notification / bundle imports as the system-service answer; local helpers are secondary.');
|
|
3905
|
-
lines.push('');
|
|
3906
4496
|
}
|
|
3907
4497
|
}
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
4498
|
+
const sdkImportCount = sdkIds.size;
|
|
4499
|
+
const strongCount = strongIds.size;
|
|
4500
|
+
const edgeCount = sdkImportCount + strongCount;
|
|
4501
|
+
const hasHardAnswer = sdkImportCount > 0 && !(0, query_utils_1.queryAsDataSourceDistinguishAsk)(query);
|
|
4502
|
+
const hasLocator = edgeCount > 0;
|
|
4503
|
+
if (!hasLocator) {
|
|
4504
|
+
lines.push('- No `@ohos`/`@kit` imports or related service Managers indexed for the named type. ' +
|
|
4505
|
+
'Treat the Manager definition as the **locator anchor**; ONE **narrow** Grep inside that file ' +
|
|
4506
|
+
'(or its callers) for `@ohos` / service imports is OK — not a repo-wide storm.');
|
|
4507
|
+
lines.push('');
|
|
4508
|
+
lines.push('> **Partial locator** — do **not** treat local cache/RDB helpers as the system service. ' +
|
|
4509
|
+
'Prefer `homegraph_callers` on `update*` / `set*` members, or a sibling `*Manager` / `*Subscribe*Manager`.');
|
|
3911
4510
|
lines.push('');
|
|
3912
|
-
return { section: lines.join('\n'), edgeCount: 0 };
|
|
4511
|
+
return { section: lines.join('\n'), edgeCount: 0, sdkImportCount: 0, strongCount: 0 };
|
|
4512
|
+
}
|
|
4513
|
+
if (hasHardAnswer) {
|
|
4514
|
+
lines.push('> Data-source survey — **ANSWER NOW** from system `@ohos`/`@kit` imports above first. ' +
|
|
4515
|
+
'Do not Grep/search/callers the same Manager for the service name.');
|
|
4516
|
+
}
|
|
4517
|
+
else {
|
|
4518
|
+
lines.push('> **Partial locator** — related writers / sibling Managers above are anchors, not a full answer. ' +
|
|
4519
|
+
'ONE tighter explore with a named Type, or ONE narrow Grep for `@ohos` if still missing — ' +
|
|
4520
|
+
'do **not** fan out `homegraph_node` / callers. Do **not** ANSWER NOW from local cache helpers.');
|
|
3913
4521
|
}
|
|
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
4522
|
lines.push('');
|
|
3917
|
-
return { section: lines.join('\n'), edgeCount };
|
|
4523
|
+
return { section: lines.join('\n'), edgeCount, sdkImportCount, strongCount };
|
|
3918
4524
|
}
|
|
3919
4525
|
/**
|
|
3920
|
-
*
|
|
4526
|
+
* Event-type inventory + which *Manager files import / handle them.
|
|
4527
|
+
* Prefers enum members on the named `*Event` Type; softens ANSWER NOW when
|
|
4528
|
+
* the graph only has noisy Type/file hits without a member list.
|
|
3921
4529
|
*/
|
|
3922
|
-
|
|
3923
|
-
const
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
if (deferKind) {
|
|
3928
|
-
return this.textResult((0, query_utils_1.homegraphDeferGuidance)(deferKind, query));
|
|
4530
|
+
buildEventDispatchSection(cg, query, projectRoot) {
|
|
4531
|
+
const types = (0, query_utils_1.extractTypeNamesFromQuery)(query).filter((t) => /Event$/i.test(t) || /Mgr$/i.test(t) || /Manager$/i.test(t));
|
|
4532
|
+
const primaryEvents = types.filter((t) => /Event$/i.test(t));
|
|
4533
|
+
if (types.length === 0) {
|
|
4534
|
+
return { section: '', hitCount: 0, eventCount: 0, handlerCount: 0, memberCount: 0, complete: false };
|
|
3929
4535
|
}
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
4536
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
4537
|
+
const eventNames = new Set();
|
|
4538
|
+
const eventFiles = new Set();
|
|
4539
|
+
const enumMembers = [];
|
|
4540
|
+
const seenMember = new Set();
|
|
4541
|
+
/** Exported `*Event` classes from a named Event module file (not enum members). */
|
|
4542
|
+
const eventClasses = [];
|
|
4543
|
+
const seenEventClass = new Set();
|
|
4544
|
+
const considerEventFile = (fp, preferredName) => {
|
|
4545
|
+
eventFiles.add(fp);
|
|
4546
|
+
if (/Event$/i.test(preferredName))
|
|
4547
|
+
eventNames.add(preferredName);
|
|
4548
|
+
try {
|
|
4549
|
+
for (const sib of cg.getNodesInFile(fp)) {
|
|
4550
|
+
if ((0, query_utils_1.isTestFile)(sib.filePath))
|
|
4551
|
+
continue;
|
|
4552
|
+
if (sib.kind === 'enum_member') {
|
|
4553
|
+
const key = `${sib.name}:${rel(sib.filePath)}`;
|
|
4554
|
+
if (seenMember.has(key))
|
|
4555
|
+
continue;
|
|
4556
|
+
seenMember.add(key);
|
|
4557
|
+
enumMembers.push({ name: sib.name, file: rel(sib.filePath), line: sib.startLine || 1 });
|
|
4558
|
+
}
|
|
4559
|
+
// Barrel / module files often export many `*Event` classes (not enums).
|
|
4560
|
+
if ((sib.kind === 'enum' || sib.kind === 'class' || sib.kind === 'type_alias' || sib.kind === 'interface')
|
|
4561
|
+
&& /Event$/i.test(sib.name)) {
|
|
4562
|
+
eventNames.add(sib.name);
|
|
4563
|
+
if (sib.kind === 'class' || sib.kind === 'enum' || sib.kind === 'type_alias') {
|
|
4564
|
+
const ck = `${sib.name}:${rel(sib.filePath)}`;
|
|
4565
|
+
if (!seenEventClass.has(ck)) {
|
|
4566
|
+
seenEventClass.add(ck);
|
|
4567
|
+
eventClasses.push({
|
|
4568
|
+
name: sib.name,
|
|
4569
|
+
file: rel(sib.filePath),
|
|
4570
|
+
line: sib.startLine || 1,
|
|
4571
|
+
});
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
3937
4576
|
}
|
|
3938
|
-
|
|
3939
|
-
|
|
4577
|
+
catch { /* */ }
|
|
4578
|
+
};
|
|
4579
|
+
for (const name of (primaryEvents.length > 0 ? primaryEvents : types).slice(0, 4)) {
|
|
4580
|
+
let nodes = [];
|
|
4581
|
+
try {
|
|
4582
|
+
nodes = cg.getNodesByName(name);
|
|
4583
|
+
}
|
|
4584
|
+
catch {
|
|
4585
|
+
nodes = [];
|
|
4586
|
+
}
|
|
4587
|
+
// File / export-only Event Types: getNodesByName can miss while search finds the file.
|
|
4588
|
+
if (nodes.length === 0 && /Event$/i.test(name)) {
|
|
4589
|
+
try {
|
|
4590
|
+
for (const r of cg.searchNodes(name, { limit: 24 })) {
|
|
4591
|
+
if ((0, query_utils_1.isTestFile)(r.node.filePath))
|
|
4592
|
+
continue;
|
|
4593
|
+
const base = r.node.filePath.replace(/\\/g, '/').split('/').pop() || '';
|
|
4594
|
+
const stem = base.replace(/\.(ets|ts|d\.ts)$/i, '');
|
|
4595
|
+
if (r.node.name === name || stem === name || r.node.kind === 'file') {
|
|
4596
|
+
nodes.push(r.node);
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4600
|
+
catch { /* */ }
|
|
4601
|
+
}
|
|
4602
|
+
for (const n of nodes) {
|
|
4603
|
+
if ((0, query_utils_1.isTestFile)(n.filePath))
|
|
4604
|
+
continue;
|
|
4605
|
+
if (/Event$/i.test(n.name)
|
|
4606
|
+
|| n.kind === 'enum'
|
|
4607
|
+
|| n.kind === 'class'
|
|
4608
|
+
|| n.kind === 'type_alias'
|
|
4609
|
+
|| n.kind === 'interface'
|
|
4610
|
+
|| n.kind === 'file'
|
|
4611
|
+
|| (n.kind === 'export' && /Event$/i.test(name))) {
|
|
4612
|
+
const label = /Event$/i.test(n.name) ? n.name : name;
|
|
4613
|
+
if (/Event$/i.test(label))
|
|
4614
|
+
eventNames.add(label);
|
|
4615
|
+
considerEventFile(n.filePath, label);
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
if (eventNames.size === 0 && enumMembers.length === 0) {
|
|
4620
|
+
return {
|
|
4621
|
+
section: [
|
|
4622
|
+
'**Event types → Manager / handler sites**',
|
|
4623
|
+
'',
|
|
4624
|
+
'> **Partial locator** — no event Type/enum exports indexed for the named anchors. '
|
|
4625
|
+
+ '`homegraph_node` / ONE explore on the Event Type file, or ONE Grep for enum members / '
|
|
4626
|
+
+ '`case` branches — not a repo-wide storm. Do **not** ANSWER NOW.',
|
|
4627
|
+
'',
|
|
4628
|
+
].join('\n'),
|
|
4629
|
+
hitCount: 0,
|
|
4630
|
+
eventCount: 0,
|
|
4631
|
+
handlerCount: 0,
|
|
4632
|
+
memberCount: 0,
|
|
4633
|
+
complete: false,
|
|
4634
|
+
};
|
|
4635
|
+
}
|
|
4636
|
+
const lines = [
|
|
4637
|
+
'**Event types → Manager / handler sites**',
|
|
4638
|
+
'',
|
|
4639
|
+
];
|
|
4640
|
+
const rankedClasses = [...eventClasses].sort((a, b) => a.name.localeCompare(b.name));
|
|
4641
|
+
if (enumMembers.length > 0) {
|
|
4642
|
+
lines.push('### Event enum members (indexed)');
|
|
4643
|
+
for (const m of enumMembers.slice(0, 40)) {
|
|
4644
|
+
lines.push(`- \`${m.name}\` — \`${m.file}:${m.line}\``);
|
|
4645
|
+
}
|
|
4646
|
+
if (enumMembers.length > 40)
|
|
4647
|
+
lines.push(`- … and ${enumMembers.length - 40} more`);
|
|
4648
|
+
lines.push('');
|
|
4649
|
+
}
|
|
4650
|
+
// Named `*Event` module files often export many Event classes (not enums).
|
|
4651
|
+
// Listing them is the answer to "有哪些事件类型" — don't force Grep for members.
|
|
4652
|
+
if (eventClasses.length > 0) {
|
|
4653
|
+
lines.push('### Event type classes (exported from Event module)');
|
|
4654
|
+
for (const c of rankedClasses.slice(0, 40)) {
|
|
4655
|
+
lines.push(`- \`${c.name}\` — \`${c.file}:${c.line}\``);
|
|
4656
|
+
}
|
|
4657
|
+
if (rankedClasses.length > 40) {
|
|
4658
|
+
lines.push(`- … and ${rankedClasses.length - 40} more Event class(es)`);
|
|
4659
|
+
}
|
|
4660
|
+
lines.push('');
|
|
4661
|
+
}
|
|
4662
|
+
lines.push('### Event Type anchors');
|
|
4663
|
+
// Prefer concrete Event classes over the barrel file stem when both exist.
|
|
4664
|
+
const anchorEvents = (eventClasses.length > 0
|
|
4665
|
+
? eventClasses.map((c) => c.name)
|
|
4666
|
+
: [...eventNames]).filter((n, i, arr) => arr.indexOf(n) === i)
|
|
4667
|
+
.sort((a, b) => a.localeCompare(b));
|
|
4668
|
+
const sortedEvents = anchorEvents;
|
|
4669
|
+
for (const ev of sortedEvents.slice(0, 12)) {
|
|
4670
|
+
let loc = '';
|
|
4671
|
+
try {
|
|
4672
|
+
const n = cg.getNodesByName(ev).find((x) => !(0, query_utils_1.isTestFile)(x.filePath));
|
|
4673
|
+
if (n)
|
|
4674
|
+
loc = ` — \`${rel(n.filePath)}:${n.startLine}\``;
|
|
4675
|
+
}
|
|
4676
|
+
catch { /* */ }
|
|
4677
|
+
if (!loc) {
|
|
4678
|
+
const fromClass = eventClasses.find((c) => c.name === ev);
|
|
4679
|
+
if (fromClass)
|
|
4680
|
+
loc = ` — \`${fromClass.file}:${fromClass.line}\``;
|
|
4681
|
+
}
|
|
4682
|
+
if (!loc) {
|
|
4683
|
+
for (const fp of eventFiles) {
|
|
4684
|
+
const base = rel(fp).split('/').pop() || '';
|
|
4685
|
+
const stem = base.replace(/\.(ets|ts|d\.ts)$/i, '');
|
|
4686
|
+
if (stem === ev || base.includes(ev)) {
|
|
4687
|
+
loc = ` — \`${rel(fp)}\``;
|
|
4688
|
+
break;
|
|
4689
|
+
}
|
|
4690
|
+
}
|
|
4691
|
+
}
|
|
4692
|
+
lines.push(`- \`${ev}\`${loc}`);
|
|
4693
|
+
}
|
|
4694
|
+
lines.push('');
|
|
4695
|
+
// Search handlers by primary Event names (+ a few members), not every noisy sibling.
|
|
4696
|
+
const searchKeys = [
|
|
4697
|
+
...sortedEvents.slice(0, 12),
|
|
4698
|
+
...enumMembers.slice(0, 12).map((m) => m.name),
|
|
4699
|
+
];
|
|
4700
|
+
const hits = new Map();
|
|
4701
|
+
for (const key of searchKeys) {
|
|
4702
|
+
let results = [];
|
|
4703
|
+
try {
|
|
4704
|
+
results = cg.searchNodes(key, { limit: 36 });
|
|
4705
|
+
}
|
|
4706
|
+
catch {
|
|
4707
|
+
continue;
|
|
4708
|
+
}
|
|
4709
|
+
for (const r of results) {
|
|
4710
|
+
const fp = rel(r.node.filePath);
|
|
4711
|
+
if ((0, query_utils_1.isTestFile)(fp))
|
|
4712
|
+
continue;
|
|
4713
|
+
if (eventFiles.has(r.node.filePath) && (r.node.name === key || r.node.kind === 'enum_member'))
|
|
4714
|
+
continue;
|
|
4715
|
+
const isMgr = /Manager|Handler|Dispatcher|Controller/i.test(fp)
|
|
4716
|
+
|| /Manager|Handler|Dispatcher|Controller/i.test(r.node.name);
|
|
4717
|
+
if (!isMgr && r.node.kind !== 'import')
|
|
4718
|
+
continue;
|
|
4719
|
+
const abs = (0, path_1.isAbsolute)(r.node.filePath)
|
|
4720
|
+
? r.node.filePath
|
|
4721
|
+
: (0, path_1.join)(projectRoot, r.node.filePath);
|
|
4722
|
+
let snippet = (r.node.signature || r.node.name).slice(0, 100);
|
|
4723
|
+
let lineHasKey = false;
|
|
4724
|
+
try {
|
|
4725
|
+
const content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
4726
|
+
const fileLines = content.split('\n');
|
|
4727
|
+
const li = Math.max(0, (r.node.startLine || 1) - 1);
|
|
4728
|
+
const line = fileLines[li] ?? '';
|
|
4729
|
+
lineHasKey = line.includes(key);
|
|
4730
|
+
if (lineHasKey || /import/i.test(line))
|
|
4731
|
+
snippet = line.trim().slice(0, 120);
|
|
4732
|
+
}
|
|
4733
|
+
catch { /* */ }
|
|
4734
|
+
// Drop weak FTS hits that neither import nor mention the key on the line.
|
|
4735
|
+
if (!lineHasKey && r.node.kind !== 'import' && r.node.name !== key)
|
|
4736
|
+
continue;
|
|
4737
|
+
const prev = hits.get(fp) ?? {
|
|
4738
|
+
file: fp,
|
|
4739
|
+
line: r.node.startLine || 1,
|
|
4740
|
+
snippet,
|
|
4741
|
+
score: 0,
|
|
4742
|
+
events: new Set(),
|
|
4743
|
+
};
|
|
4744
|
+
prev.events.add(key);
|
|
4745
|
+
prev.score += isMgr ? 3 : 1;
|
|
4746
|
+
if (r.node.kind === 'import')
|
|
4747
|
+
prev.score += 2;
|
|
4748
|
+
if (lineHasKey)
|
|
4749
|
+
prev.score += 2;
|
|
4750
|
+
if (!prev.snippet && snippet)
|
|
4751
|
+
prev.snippet = snippet;
|
|
4752
|
+
hits.set(fp, prev);
|
|
4753
|
+
}
|
|
4754
|
+
}
|
|
4755
|
+
const ranked = [...hits.values()].sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
|
|
4756
|
+
const consumerHits = [];
|
|
4757
|
+
const consumerSeen = new Set();
|
|
4758
|
+
const eventNamesForConsumers = rankedClasses.length > 0
|
|
4759
|
+
? rankedClasses.map((c) => c.name)
|
|
4760
|
+
: sortedEvents;
|
|
4761
|
+
const addConsumer = (file, line, snippet, event, score) => {
|
|
4762
|
+
const key = `${file}:${line}:${event}`;
|
|
4763
|
+
if (consumerSeen.has(key))
|
|
4764
|
+
return;
|
|
4765
|
+
consumerSeen.add(key);
|
|
4766
|
+
consumerHits.push({ file, line, snippet: snippet.slice(0, 120), event, score });
|
|
4767
|
+
};
|
|
4768
|
+
for (const ev of eventNamesForConsumers.slice(0, 24)) {
|
|
4769
|
+
let results = [];
|
|
4770
|
+
try {
|
|
4771
|
+
results = cg.searchNodes(ev, { limit: 36 });
|
|
4772
|
+
}
|
|
4773
|
+
catch {
|
|
4774
|
+
continue;
|
|
4775
|
+
}
|
|
4776
|
+
const esc = ev.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
4777
|
+
const onRe = new RegExp(`(?:InnerEventUtil|evtBus|eventMgr|EventMgr|focusMode\\w*EventMgr)\\.on\\s*\\(\\s*${esc}|\\.on\\s*\\(\\s*${esc}`);
|
|
4778
|
+
const seenFiles = new Set();
|
|
4779
|
+
for (const r of results) {
|
|
4780
|
+
const fp = rel(r.node.filePath);
|
|
4781
|
+
if ((0, query_utils_1.isTestFile)(fp) || eventFiles.has(r.node.filePath))
|
|
4782
|
+
continue;
|
|
4783
|
+
if (seenFiles.has(fp))
|
|
4784
|
+
continue;
|
|
4785
|
+
seenFiles.add(fp);
|
|
4786
|
+
const abs = (0, path_1.isAbsolute)(r.node.filePath)
|
|
4787
|
+
? r.node.filePath
|
|
4788
|
+
: (0, path_1.join)(projectRoot, r.node.filePath);
|
|
4789
|
+
let content = '';
|
|
4790
|
+
try {
|
|
4791
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
4792
|
+
}
|
|
4793
|
+
catch {
|
|
4794
|
+
continue;
|
|
4795
|
+
}
|
|
4796
|
+
const fileLines = content.split('\n');
|
|
4797
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
4798
|
+
const line = fileLines[i] ?? '';
|
|
4799
|
+
if (!onRe.test(line) && !line.includes(`produceOn(${ev}`) && !line.includes(`produceOn(${ev},`)) {
|
|
4800
|
+
continue;
|
|
4801
|
+
}
|
|
4802
|
+
let score = 4;
|
|
4803
|
+
if (/Controller|Manager|Handler|Vm$/i.test(fp))
|
|
4804
|
+
score += 6;
|
|
4805
|
+
if (/\.on\s*\(/.test(line))
|
|
4806
|
+
score += 4;
|
|
4807
|
+
if (/produceOn/.test(line))
|
|
4808
|
+
score += 3;
|
|
4809
|
+
addConsumer(fp, i + 1, line.trim(), ev, score);
|
|
4810
|
+
}
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4813
|
+
// DataShareManager produceOn registrations (producer side of dispatch).
|
|
4814
|
+
let mgrNodes = [];
|
|
4815
|
+
try {
|
|
4816
|
+
mgrNodes = cg.getNodesByName('DataShareManager').filter((n) => !(0, query_utils_1.isTestFile)(n.filePath));
|
|
4817
|
+
}
|
|
4818
|
+
catch {
|
|
4819
|
+
mgrNodes = [];
|
|
4820
|
+
}
|
|
4821
|
+
for (const n of mgrNodes.slice(0, 2)) {
|
|
4822
|
+
const fp = rel(n.filePath);
|
|
4823
|
+
const abs = (0, path_1.isAbsolute)(n.filePath) ? n.filePath : (0, path_1.join)(projectRoot, n.filePath);
|
|
4824
|
+
try {
|
|
4825
|
+
const fileLines = (0, fs_1.readFileSync)(abs, 'utf-8').split('\n');
|
|
4826
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
4827
|
+
const line = fileLines[i] ?? '';
|
|
4828
|
+
if (!/produceOn\s*\(/.test(line))
|
|
4829
|
+
continue;
|
|
4830
|
+
const evM = line.match(/produceOn\s*\(\s*([A-Za-z_][\w]*Event)/);
|
|
4831
|
+
const evName = evM?.[1] ?? 'produceOn';
|
|
4832
|
+
addConsumer(fp, i + 1, line.trim(), evName, 5);
|
|
4833
|
+
if (consumerHits.length >= 10)
|
|
4834
|
+
break;
|
|
4835
|
+
}
|
|
4836
|
+
}
|
|
4837
|
+
catch { /* optional */ }
|
|
4838
|
+
}
|
|
4839
|
+
consumerHits.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
|
|
4840
|
+
if (consumerHits.length > 0) {
|
|
4841
|
+
lines.push('### Event consumer registrations (`.on` / `produceOn`)');
|
|
4842
|
+
for (const h of consumerHits.slice(0, 16)) {
|
|
4843
|
+
lines.push(`- \`${h.file}:${h.line}\` ← \`${h.event}\` \`${h.snippet}\``);
|
|
4844
|
+
}
|
|
4845
|
+
if (consumerHits.length > 16) {
|
|
4846
|
+
lines.push(`- … and ${consumerHits.length - 16} more consumer site(s)`);
|
|
4847
|
+
}
|
|
4848
|
+
lines.push('');
|
|
4849
|
+
}
|
|
4850
|
+
lines.push('### Manager / handler files referencing these events');
|
|
4851
|
+
const memberCount = enumMembers.length;
|
|
4852
|
+
const eventClassCount = eventClasses.length;
|
|
4853
|
+
const dispatchAsk = /分发|分发给|DataShareMgr|dispatch|routed?\s+to/i.test(query);
|
|
4854
|
+
// Complete when: enum members + handlers, OR Event classes + consumer wiring
|
|
4855
|
+
// for dispatch asks, OR a tight Event Type set.
|
|
4856
|
+
const complete = ranked.length > 0
|
|
4857
|
+
&& ((memberCount > 0 && memberCount <= 48 && ranked.length >= 1)
|
|
4858
|
+
|| (eventClassCount >= 3 && eventClassCount <= 64
|
|
4859
|
+
&& (dispatchAsk ? consumerHits.length >= 2 : ranked.length >= 1))
|
|
4860
|
+
|| (sortedEvents.length > 0 && sortedEvents.length <= 4 && ranked.length >= 1 && ranked[0].score >= 5)
|
|
4861
|
+
|| (dispatchAsk && consumerHits.length >= 3));
|
|
4862
|
+
if (ranked.length === 0) {
|
|
4863
|
+
lines.push('- No Manager import/use sites indexed for these event types.');
|
|
4864
|
+
lines.push('');
|
|
4865
|
+
lines.push('> **Partial locator** — event anchors above only. '
|
|
4866
|
+
+ '`homegraph_node` the Event file or ONE Grep for handler/`case` branches — do **not** ANSWER NOW as a full dispatch map.');
|
|
4867
|
+
}
|
|
4868
|
+
else {
|
|
4869
|
+
for (const h of ranked.slice(0, 24)) {
|
|
4870
|
+
const evs = [...h.events].slice(0, 4).join(', ');
|
|
4871
|
+
lines.push(h.snippet
|
|
4872
|
+
? `- \`${h.file}:${h.line}\` ← \`${evs}\` \`${h.snippet}\``
|
|
4873
|
+
: `- \`${h.file}:${h.line}\` ← \`${evs}\``);
|
|
4874
|
+
}
|
|
4875
|
+
if (ranked.length > 24)
|
|
4876
|
+
lines.push(`- … and ${ranked.length - 24} more handler file(s)`);
|
|
4877
|
+
lines.push('');
|
|
4878
|
+
if (complete) {
|
|
4879
|
+
lines.push(eventClassCount > 0
|
|
4880
|
+
? '> Event→Manager inventory — **ANSWER NOW** from Event type classes + consumer `.on` / handler files above; '
|
|
4881
|
+
+ 'ONE narrow Grep only if a specific Manager→event branch is still missing.'
|
|
4882
|
+
: '> Event→Manager inventory — **ANSWER NOW** from enum members + handler files above; '
|
|
4883
|
+
+ 'ONE narrow Grep only if a specific `case` branch is still missing.');
|
|
4884
|
+
}
|
|
4885
|
+
else if (eventClassCount >= 3 && dispatchAsk && consumerHits.length === 0) {
|
|
4886
|
+
lines.push('> **Partial locator** — Event type list above is complete, but consumer `.on` / dispatch wiring '
|
|
4887
|
+
+ 'was not indexed. ONE Grep for `.on(EventName` or `produceOn` in `*Manager*` files — '
|
|
4888
|
+
+ 'do **not** repo-storm; do **not** ANSWER NOW as a full dispatch map.');
|
|
4889
|
+
}
|
|
4890
|
+
else {
|
|
4891
|
+
lines.push('> **Partial locator** — handler files are anchors, but enum-member coverage may be incomplete. '
|
|
4892
|
+
+ '`homegraph_node` the Event Type / ONE Grep for members or `case` — do **not** repo-storm; do **not** ANSWER NOW as a full map.');
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
4895
|
+
lines.push('');
|
|
4896
|
+
return {
|
|
4897
|
+
section: lines.join('\n'),
|
|
4898
|
+
hitCount: ranked.length + memberCount + eventClassCount,
|
|
4899
|
+
eventCount: Math.max(sortedEvents.length, eventClassCount, memberCount > 0 ? 1 : 0),
|
|
4900
|
+
handlerCount: ranked.length,
|
|
4901
|
+
memberCount: memberCount + eventClassCount,
|
|
4902
|
+
complete,
|
|
4903
|
+
};
|
|
4904
|
+
}
|
|
4905
|
+
/**
|
|
4906
|
+
* ≥3 named Types + dependency ask — file locations + cross-imports only.
|
|
4907
|
+
* Avoids fat multi-body compact that hits the soft deadline (D60).
|
|
4908
|
+
*/
|
|
4909
|
+
buildMultiTypeDependencySection(cg, query) {
|
|
4910
|
+
const types = (0, query_utils_1.extractTypeNamesFromQuery)(query).filter((t) => t.length >= 8
|
|
4911
|
+
|| /(?:Manager|Installer|Parser|Service|Controller|Handler|Provider|Store)$/i.test(t));
|
|
4912
|
+
if (types.length < 3)
|
|
4913
|
+
return { section: '', hitCount: 0 };
|
|
4914
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
4915
|
+
const locs = [];
|
|
4916
|
+
const fileByType = new Map();
|
|
4917
|
+
for (const name of types.slice(0, 8)) {
|
|
4918
|
+
let nodes = [];
|
|
4919
|
+
try {
|
|
4920
|
+
nodes = cg.getNodesByName(name).filter((n) => !(0, query_utils_1.isTestFile)(n.filePath)
|
|
4921
|
+
&& !(0, arkts_1.isOhosApiFilePath)(n.filePath)
|
|
4922
|
+
&& !/\.d\.ts$/i.test(n.filePath)
|
|
4923
|
+
&& (n.kind === 'class' || n.kind === 'struct' || n.kind === 'interface' || n.kind === 'component'));
|
|
4924
|
+
}
|
|
4925
|
+
catch {
|
|
4926
|
+
continue;
|
|
4927
|
+
}
|
|
4928
|
+
if (nodes.length === 0) {
|
|
4929
|
+
try {
|
|
4930
|
+
for (const r of cg.searchNodes(name, { limit: 12 })) {
|
|
4931
|
+
if ((0, query_utils_1.isTestFile)(r.node.filePath) || (0, arkts_1.isOhosApiFilePath)(r.node.filePath))
|
|
4932
|
+
continue;
|
|
4933
|
+
if (r.node.name === name || r.node.kind === 'class') {
|
|
4934
|
+
nodes.push(r.node);
|
|
4935
|
+
break;
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
catch { /* */ }
|
|
4940
|
+
}
|
|
4941
|
+
const n = nodes[0];
|
|
4942
|
+
if (!n)
|
|
4943
|
+
continue;
|
|
4944
|
+
locs.push({ name, file: rel(n.filePath), line: n.startLine || 1 });
|
|
4945
|
+
fileByType.set(name, rel(n.filePath));
|
|
4946
|
+
}
|
|
4947
|
+
if (locs.length === 0)
|
|
4948
|
+
return { section: '', hitCount: 0 };
|
|
4949
|
+
const lines = [
|
|
4950
|
+
'**Named-Type dependency inventory** (locations + cross-file imports — no full bodies)',
|
|
4951
|
+
'',
|
|
4952
|
+
'### Types (indexed)',
|
|
4953
|
+
];
|
|
4954
|
+
for (const l of locs) {
|
|
4955
|
+
lines.push(`- \`${l.name}\` — \`${l.file}:${l.line}\``);
|
|
4956
|
+
}
|
|
4957
|
+
lines.push('');
|
|
4958
|
+
// Cross edges: does Type A's file import/reference Type B?
|
|
4959
|
+
const edges = [];
|
|
4960
|
+
for (const a of locs) {
|
|
4961
|
+
let fileNodes = [];
|
|
4962
|
+
try {
|
|
4963
|
+
fileNodes = cg.getNodesInFile(a.file);
|
|
4964
|
+
}
|
|
4965
|
+
catch {
|
|
4966
|
+
continue;
|
|
4967
|
+
}
|
|
4968
|
+
const blob = fileNodes
|
|
4969
|
+
.filter((n) => n.kind === 'import' || n.kind === 'export')
|
|
4970
|
+
.map((n) => `${n.name} ${n.signature || ''}`)
|
|
4971
|
+
.join('\n');
|
|
4972
|
+
for (const b of locs) {
|
|
4973
|
+
if (a.name === b.name)
|
|
4974
|
+
continue;
|
|
4975
|
+
if (blob.includes(b.name) || fileNodes.some((n) => n.name === b.name && n.kind === 'import')) {
|
|
4976
|
+
edges.push(`- \`${a.name}\` → \`${b.name}\` (import/ref in \`${a.file}\`)`);
|
|
4977
|
+
}
|
|
4978
|
+
}
|
|
4979
|
+
// Also check callers between types (light).
|
|
4980
|
+
try {
|
|
4981
|
+
const seed = cg.getNodesByName(a.name).find((n) => rel(n.filePath) === a.file);
|
|
4982
|
+
if (!seed)
|
|
4983
|
+
continue;
|
|
4984
|
+
for (const { node: c } of cg.getCallers(seed.id).slice(0, 20)) {
|
|
4985
|
+
if ((0, query_utils_1.isTestFile)(c.filePath))
|
|
4986
|
+
continue;
|
|
4987
|
+
for (const b of locs) {
|
|
4988
|
+
if (b.name === a.name)
|
|
4989
|
+
continue;
|
|
4990
|
+
if (c.name === b.name || rel(c.filePath) === fileByType.get(b.name)) {
|
|
4991
|
+
edges.push(`- \`${b.name}\` → calls/uses \`${a.name}\` (\`${rel(c.filePath)}:${c.startLine}\`)`);
|
|
4992
|
+
}
|
|
4993
|
+
}
|
|
4994
|
+
}
|
|
4995
|
+
}
|
|
4996
|
+
catch { /* */ }
|
|
4997
|
+
}
|
|
4998
|
+
lines.push('### Cross links (imports / callers)');
|
|
4999
|
+
if (edges.length === 0) {
|
|
5000
|
+
lines.push('- No import/caller edges indexed between these Types yet.');
|
|
5001
|
+
}
|
|
5002
|
+
else {
|
|
5003
|
+
const uniq = [...new Set(edges)].slice(0, 40);
|
|
5004
|
+
lines.push(...uniq);
|
|
5005
|
+
}
|
|
5006
|
+
lines.push('');
|
|
5007
|
+
// Text-scan `extends Base` when graph edges are missing — shared bases are
|
|
5008
|
+
// the usual answer to "这几个 Installer/Parser 的依赖关系".
|
|
5009
|
+
const extendsHits = [];
|
|
5010
|
+
const baseNames = new Set();
|
|
5011
|
+
let projectRoot = '';
|
|
5012
|
+
try {
|
|
5013
|
+
projectRoot = cg.getProjectRoot();
|
|
5014
|
+
}
|
|
5015
|
+
catch { /* */ }
|
|
5016
|
+
for (const a of locs) {
|
|
5017
|
+
let content = '';
|
|
5018
|
+
try {
|
|
5019
|
+
const full = (0, path_1.isAbsolute)(a.file) ? a.file : (projectRoot ? (0, path_1.join)(projectRoot, a.file) : '');
|
|
5020
|
+
if (full && (0, fs_1.existsSync)(full))
|
|
5021
|
+
content = (0, fs_1.readFileSync)(full, 'utf-8');
|
|
5022
|
+
}
|
|
5023
|
+
catch { /* */ }
|
|
5024
|
+
if (!content)
|
|
5025
|
+
continue;
|
|
5026
|
+
const m = content.match(new RegExp(`(?:export\\s+)?(?:abstract\\s+)?class\\s+${a.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s+extends\\s+([A-Za-z_][A-Za-z0-9_]*)`));
|
|
5027
|
+
if (!m?.[1])
|
|
5028
|
+
continue;
|
|
5029
|
+
const base = m[1];
|
|
5030
|
+
baseNames.add(base);
|
|
5031
|
+
extendsHits.push(`- \`${a.name}\` extends \`${base}\``);
|
|
5032
|
+
}
|
|
5033
|
+
if (extendsHits.length > 0) {
|
|
5034
|
+
lines.push('### Inheritance (source `extends`)');
|
|
5035
|
+
lines.push(...[...new Set(extendsHits)].slice(0, 20));
|
|
5036
|
+
if (baseNames.size > 0) {
|
|
5037
|
+
lines.push('');
|
|
5038
|
+
lines.push(`Shared / parent Type(s): ${[...baseNames].slice(0, 8).map((b) => `\`${b}\``).join(', ')}.`);
|
|
5039
|
+
}
|
|
5040
|
+
lines.push('');
|
|
5041
|
+
}
|
|
5042
|
+
const hasGraph = edges.length > 0;
|
|
5043
|
+
const hasExtends = extendsHits.length > 0;
|
|
5044
|
+
if (!hasGraph && !hasExtends) {
|
|
5045
|
+
lines.push('> **Partial locator** — Type files above are anchors. '
|
|
5046
|
+
+ '`homegraph_node` / `homegraph_callees` one Type at a time; do **not** ANSWER NOW as a full graph.');
|
|
5047
|
+
}
|
|
5048
|
+
else {
|
|
5049
|
+
lines.push('> Multi-Type dependency inventory — **ANSWER NOW** from locations'
|
|
5050
|
+
+ (hasExtends ? ' + `extends` bases' : '')
|
|
5051
|
+
+ (hasGraph ? ' + import/caller edges' : '')
|
|
5052
|
+
+ ' above; only `homegraph_node` one Type if a body detail is still missing.');
|
|
5053
|
+
}
|
|
5054
|
+
lines.push('');
|
|
5055
|
+
return { section: lines.join('\n'), hitCount: locs.length + edges.length + extendsHits.length };
|
|
5056
|
+
}
|
|
5057
|
+
/**
|
|
5058
|
+
* Main-thread fast path for inventory surveys — skips the worker queue.
|
|
5059
|
+
*/
|
|
5060
|
+
tryFastPathResult(toolName, args) {
|
|
5061
|
+
const query = args.query;
|
|
5062
|
+
if (typeof query !== 'string')
|
|
5063
|
+
return null;
|
|
5064
|
+
const deferKind = (0, query_utils_1.queryShouldDeferToBuiltinTools)(query);
|
|
5065
|
+
if (deferKind) {
|
|
5066
|
+
return this.textResult((0, query_utils_1.homegraphDeferGuidance)(deferKind, query));
|
|
5067
|
+
}
|
|
5068
|
+
try {
|
|
5069
|
+
const cg = this.getHomeGraph(args.projectPath);
|
|
5070
|
+
const projectRoot = cg.getProjectRoot();
|
|
5071
|
+
if (toolName === 'homegraph_explore') {
|
|
5072
|
+
return this.trySpecializedExploreRoute(cg, query, projectRoot)
|
|
5073
|
+
?? this.tryFastInventoryExplore(cg, query, projectRoot)
|
|
5074
|
+
?? this.tryLightMechanismExplore(cg, query, projectRoot)
|
|
5075
|
+
?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot);
|
|
5076
|
+
}
|
|
5077
|
+
if (toolName === 'homegraph_search') {
|
|
5078
|
+
return this.tryFastInventoryExplore(cg, query, projectRoot)
|
|
3940
5079
|
?? this.tryLightMechanismExplore(cg, query, projectRoot)
|
|
3941
5080
|
?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot);
|
|
3942
5081
|
}
|
|
@@ -3946,12 +5085,258 @@ class ToolHandler {
|
|
|
3946
5085
|
}
|
|
3947
5086
|
return null;
|
|
3948
5087
|
}
|
|
5088
|
+
/** Conservative compatibility router: ambiguous mechanism queries stay broad. */
|
|
5089
|
+
specializedExploreRouteForQuery(query) {
|
|
5090
|
+
if ((0, query_utils_1.queryAsModuleDependencySurvey)(query))
|
|
5091
|
+
return 'modules';
|
|
5092
|
+
if ((0, query_utils_1.queryAsModuleExportSurvey)(query))
|
|
5093
|
+
return 'native';
|
|
5094
|
+
if ((0, query_utils_1.queryAsReturnValueConsumerSurvey)(query))
|
|
5095
|
+
return null;
|
|
5096
|
+
if ((0, query_utils_1.shouldBuildApiUsageSurvey)(query))
|
|
5097
|
+
return 'usages';
|
|
5098
|
+
if (queryAsBareSymbolInventory(query))
|
|
5099
|
+
return 'usages';
|
|
5100
|
+
if (!(0, query_utils_1.shouldBuildMemberSurvey)(query))
|
|
5101
|
+
return null;
|
|
5102
|
+
return /哪里(?:用|使用|调用)|哪些.{0,24}(?:用|使用|调用|依赖)|使用位置|引用位置|调用位置|\busages?\b|\bwhere\b.{0,36}\bused\b|\bwhich\b.{0,36}\b(?:files?|functions?|methods?)\b.{0,24}\b(?:use|call|depend)/i.test(query)
|
|
5103
|
+
? 'usages'
|
|
5104
|
+
: null;
|
|
5105
|
+
}
|
|
5106
|
+
/** Run exactly one bounded survey family and expose the selected route. */
|
|
5107
|
+
runSpecializedExploreRoute(route, cg, query, projectRoot) {
|
|
5108
|
+
let section = '';
|
|
5109
|
+
let status = 'no_indexed_evidence';
|
|
5110
|
+
let coverage = '';
|
|
5111
|
+
if (route === 'modules') {
|
|
5112
|
+
const manifestResult = this.buildFocusedModuleManifestSection(projectRoot, query);
|
|
5113
|
+
const graphResult = manifestResult.manifestCount === 0
|
|
5114
|
+
? this.buildModuleDependencySurveySection(cg, query)
|
|
5115
|
+
: { section: '', hitCount: 0 };
|
|
5116
|
+
section = manifestResult.section || graphResult.section;
|
|
5117
|
+
status = section ? 'complete' : 'no_indexed_evidence';
|
|
5118
|
+
coverage = manifestResult.manifestCount > 0
|
|
5119
|
+
? 'named module manifests plus bounded linter evidence only; no full-repo cycle walk or unrelated surveys were built'
|
|
5120
|
+
: 'named-module dependency/cycle graph only; no source bodies or unrelated surveys were built';
|
|
5121
|
+
}
|
|
5122
|
+
else if (route === 'native') {
|
|
5123
|
+
const result = this.buildModuleExportSurveySection(cg, query, projectRoot);
|
|
5124
|
+
section = result.section;
|
|
5125
|
+
status = result.hitCount > 0 ? 'complete' : 'no_indexed_evidence';
|
|
5126
|
+
coverage = 'named path/Type NAPI export registrations only; no domain file dump was built';
|
|
5127
|
+
}
|
|
5128
|
+
else {
|
|
5129
|
+
const apiUsage = (0, query_utils_1.shouldBuildApiUsageSurvey)(query)
|
|
5130
|
+
? this.buildApiUsageSection(cg, query, projectRoot)
|
|
5131
|
+
: { section: '', fileCount: 0 };
|
|
5132
|
+
const memberUsage = (0, query_utils_1.shouldBuildMemberSurvey)(query)
|
|
5133
|
+
&& !((0, query_utils_1.queryAsFieldUsageSurvey)(query) && apiUsage.fileCount > 0)
|
|
5134
|
+
? this.buildMemberSurveySection(cg, query, projectRoot)
|
|
5135
|
+
: '';
|
|
5136
|
+
let scanned = (0, query_utils_1.shouldBuildApiUsageSurvey)(query) || (0, query_utils_1.shouldBuildMemberSurvey)(query);
|
|
5137
|
+
section = [apiUsage.section, memberUsage].filter(Boolean).join('\n');
|
|
5138
|
+
if (!section) {
|
|
5139
|
+
const bare = this.buildBareSymbolUsageSection(cg, query, projectRoot);
|
|
5140
|
+
scanned = scanned || bare.scanned;
|
|
5141
|
+
section = bare.section;
|
|
5142
|
+
}
|
|
5143
|
+
status = section ? 'complete' : (scanned ? 'no_indexed_evidence' : 'not_surveyed');
|
|
5144
|
+
coverage = 'named API/member/constant/field usage sites only; no general call-flow or source dump was built';
|
|
5145
|
+
}
|
|
5146
|
+
const body = section
|
|
5147
|
+
|| (status === 'not_surveyed'
|
|
5148
|
+
? '- No survey ran: this query carries no symbol name to scan. Re-run naming the symbol(s), or use `homegraph_explore`.'
|
|
5149
|
+
: '- No matching evidence was found in the current index for this focused survey.');
|
|
5150
|
+
return this.textResult(this.truncateOutput([
|
|
5151
|
+
`**HomeGraph specialized route: ${route}**`,
|
|
5152
|
+
`Status: ${status}`,
|
|
5153
|
+
`Coverage: ${coverage}.`,
|
|
5154
|
+
'',
|
|
5155
|
+
body,
|
|
5156
|
+
].join('\n')));
|
|
5157
|
+
}
|
|
5158
|
+
/** Usage sites for a bare symbol bag, including non-call textual references. */
|
|
5159
|
+
buildBareSymbolUsageSection(cg, query, projectRoot) {
|
|
5160
|
+
const symbols = extractBareUsageSymbols(query);
|
|
5161
|
+
if (symbols.length === 0)
|
|
5162
|
+
return { section: '', scanned: false };
|
|
5163
|
+
const MAX_SYMBOLS = 4;
|
|
5164
|
+
const MAX_FILES_SCANNED = 80;
|
|
5165
|
+
const MAX_FILES_REPORTED = 14;
|
|
5166
|
+
const MAX_LINES_PER_FILE = 3;
|
|
5167
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
5168
|
+
const byFile = new Map();
|
|
5169
|
+
const seenFiles = new Set();
|
|
5170
|
+
let matchedFiles = 0;
|
|
5171
|
+
const matchers = symbols.slice(0, MAX_SYMBOLS).map((sym) => ({
|
|
5172
|
+
sym,
|
|
5173
|
+
re: new RegExp(`\\b${sym.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`),
|
|
5174
|
+
}));
|
|
5175
|
+
const scanFile = (fp) => {
|
|
5176
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, fp);
|
|
5177
|
+
if (!abs)
|
|
5178
|
+
return { rows: [], bytes: 0 };
|
|
5179
|
+
let content;
|
|
5180
|
+
try {
|
|
5181
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
5182
|
+
}
|
|
5183
|
+
catch {
|
|
5184
|
+
return { rows: [], bytes: 0 };
|
|
5185
|
+
}
|
|
5186
|
+
const bytes = content.length;
|
|
5187
|
+
const hit = matchers.filter((m) => m.re.test(content));
|
|
5188
|
+
if (hit.length === 0)
|
|
5189
|
+
return { rows: [], bytes };
|
|
5190
|
+
const fileLines = content.split('\n');
|
|
5191
|
+
const rows = [];
|
|
5192
|
+
for (let i = 0; i < fileLines.length && rows.length < MAX_LINES_PER_FILE; i++) {
|
|
5193
|
+
const text = (fileLines[i] ?? '').trim();
|
|
5194
|
+
if (!text)
|
|
5195
|
+
continue;
|
|
5196
|
+
const m = hit.find((x) => x.re.test(text));
|
|
5197
|
+
if (m)
|
|
5198
|
+
rows.push({ line: i + 1, text, symbol: m.sym });
|
|
5199
|
+
}
|
|
5200
|
+
return { rows, bytes };
|
|
5201
|
+
};
|
|
5202
|
+
const record = (fp, rows) => {
|
|
5203
|
+
if (rows.length === 0)
|
|
5204
|
+
return;
|
|
5205
|
+
matchedFiles++;
|
|
5206
|
+
if (byFile.size < MAX_FILES_REPORTED)
|
|
5207
|
+
byFile.set(fp, rows);
|
|
5208
|
+
};
|
|
5209
|
+
for (const { sym } of matchers) {
|
|
5210
|
+
let nodes = [];
|
|
5211
|
+
try {
|
|
5212
|
+
nodes = cg.searchNodes(sym, { limit: 60 });
|
|
5213
|
+
}
|
|
5214
|
+
catch {
|
|
5215
|
+
continue;
|
|
5216
|
+
}
|
|
5217
|
+
for (const r of nodes) {
|
|
5218
|
+
if (seenFiles.size >= MAX_FILES_SCANNED)
|
|
5219
|
+
break;
|
|
5220
|
+
const fp = rel(r.node.filePath);
|
|
5221
|
+
if ((0, query_utils_1.isTestFile)(fp) || seenFiles.has(fp))
|
|
5222
|
+
continue;
|
|
5223
|
+
seenFiles.add(fp);
|
|
5224
|
+
record(fp, scanFile(fp).rows);
|
|
5225
|
+
}
|
|
5226
|
+
}
|
|
5227
|
+
const SWEEP_DEADLINE_MS = 3000;
|
|
5228
|
+
const SWEEP_MAX_FILES = 20000;
|
|
5229
|
+
const SWEEP_MAX_BYTES = 256 * 1024 * 1024;
|
|
5230
|
+
let sweepRan = false;
|
|
5231
|
+
let sweepComplete = false;
|
|
5232
|
+
if (process.env.HOMEGRAPH_EXPLORE_USAGE_SWEEP !== '0') {
|
|
5233
|
+
let corpus = [];
|
|
5234
|
+
try {
|
|
5235
|
+
corpus = cg.getFiles();
|
|
5236
|
+
}
|
|
5237
|
+
catch {
|
|
5238
|
+
corpus = [];
|
|
5239
|
+
}
|
|
5240
|
+
if (corpus.length > 0) {
|
|
5241
|
+
sweepRan = true;
|
|
5242
|
+
sweepComplete = true;
|
|
5243
|
+
const started = Date.now();
|
|
5244
|
+
let swept = 0;
|
|
5245
|
+
let bytesRead = 0;
|
|
5246
|
+
for (const f of corpus) {
|
|
5247
|
+
if (swept >= SWEEP_MAX_FILES || bytesRead >= SWEEP_MAX_BYTES
|
|
5248
|
+
|| (swept % 64 === 0 && Date.now() - started > SWEEP_DEADLINE_MS)) {
|
|
5249
|
+
sweepComplete = false;
|
|
5250
|
+
break;
|
|
5251
|
+
}
|
|
5252
|
+
const fp = rel(String(f?.path ?? ''));
|
|
5253
|
+
if (!fp || (0, query_utils_1.isTestFile)(fp) || seenFiles.has(fp))
|
|
5254
|
+
continue;
|
|
5255
|
+
seenFiles.add(fp);
|
|
5256
|
+
swept++;
|
|
5257
|
+
const scan = scanFile(fp);
|
|
5258
|
+
bytesRead += scan.bytes;
|
|
5259
|
+
record(fp, scan.rows);
|
|
5260
|
+
}
|
|
5261
|
+
}
|
|
5262
|
+
}
|
|
5263
|
+
if (byFile.size === 0)
|
|
5264
|
+
return { section: '', scanned: true };
|
|
5265
|
+
const truncated = matchedFiles > byFile.size;
|
|
5266
|
+
const lines = [
|
|
5267
|
+
`**Symbol usage sites** — \`${symbols.slice(0, MAX_SYMBOLS).join('`, `')}\``, '',
|
|
5268
|
+
sweepRan && sweepComplete
|
|
5269
|
+
? '> Full in-repo text sweep of every indexed source file: includes non-call references with no `calls` edge.'
|
|
5270
|
+
: '> Scanned in-repo: includes non-call references with no `calls` edge.',
|
|
5271
|
+
'',
|
|
5272
|
+
];
|
|
5273
|
+
for (const [file, rows] of byFile) {
|
|
5274
|
+
lines.push(`- \`${file}\``);
|
|
5275
|
+
for (const row of rows)
|
|
5276
|
+
lines.push(` - ${row.line}: \`${row.text.slice(0, 120)}\``);
|
|
5277
|
+
}
|
|
5278
|
+
lines.push('');
|
|
5279
|
+
if (truncated) {
|
|
5280
|
+
lines.push(`> Showing ${byFile.size} of ${matchedFiles} matching file(s) — narrow the symbol to see the rest.`);
|
|
5281
|
+
}
|
|
5282
|
+
else if (sweepRan && sweepComplete) {
|
|
5283
|
+
lines.push(`> ${byFile.size} file(s) — every indexed source file was swept, so this is the complete in-repo list. **ANSWER NOW** from it: do not re-grep these names.`);
|
|
5284
|
+
}
|
|
5285
|
+
else {
|
|
5286
|
+
lines.push(`> ${byFile.size} indexed use site file(s); the text sweep did not complete, so files that merely reference these names may be missing.`);
|
|
5287
|
+
}
|
|
5288
|
+
return { section: lines.join('\n'), scanned: true };
|
|
5289
|
+
}
|
|
5290
|
+
trySpecializedExploreRoute(cg, query, projectRoot) {
|
|
5291
|
+
const route = this.specializedExploreRouteForQuery(query);
|
|
5292
|
+
return route ? this.runSpecializedExploreRoute(route, cg, query, projectRoot) : null;
|
|
5293
|
+
}
|
|
5294
|
+
async handleSpecializedUsages(args) {
|
|
5295
|
+
const query = this.validateString(args.query, 'query');
|
|
5296
|
+
if (typeof query !== 'string')
|
|
5297
|
+
return query;
|
|
5298
|
+
const cg = this.getHomeGraph(args.projectPath);
|
|
5299
|
+
return this.runSpecializedExploreRoute('usages', cg, query, cg.getProjectRoot());
|
|
5300
|
+
}
|
|
5301
|
+
async handleSpecializedModules(args) {
|
|
5302
|
+
const query = this.validateString(args.query, 'query');
|
|
5303
|
+
if (typeof query !== 'string')
|
|
5304
|
+
return query;
|
|
5305
|
+
const cg = this.getHomeGraph(args.projectPath);
|
|
5306
|
+
return this.runSpecializedExploreRoute('modules', cg, query, cg.getProjectRoot());
|
|
5307
|
+
}
|
|
5308
|
+
async handleSpecializedNative(args) {
|
|
5309
|
+
const query = this.validateString(args.query, 'query');
|
|
5310
|
+
if (typeof query !== 'string')
|
|
5311
|
+
return query;
|
|
5312
|
+
const cg = this.getHomeGraph(args.projectPath);
|
|
5313
|
+
return this.runSpecializedExploreRoute('native', cg, query, cg.getProjectRoot());
|
|
5314
|
+
}
|
|
3949
5315
|
/**
|
|
3950
5316
|
* Fast inventory-only explore — skips findRelevantContext for survey/caller/dependency queries.
|
|
3951
5317
|
*/
|
|
3952
5318
|
tryFastInventoryExplore(cg, query, projectRoot) {
|
|
3953
5319
|
if (!(0, query_utils_1.shouldTryFastInventoryExplore)(query))
|
|
3954
5320
|
return null;
|
|
5321
|
+
// Multi-Type dependency asks: inventory-only early exit (avoids fat compact / 25s busy).
|
|
5322
|
+
if ((0, query_utils_1.queryAsMultiTypeDependencySurvey)(query)) {
|
|
5323
|
+
const multi = this.buildMultiTypeDependencySection(cg, query);
|
|
5324
|
+
if (multi.section) {
|
|
5325
|
+
const soft = /Partial locator/i.test(multi.section);
|
|
5326
|
+
return this.textResult([
|
|
5327
|
+
`**Exploration: ${query}**`,
|
|
5328
|
+
'',
|
|
5329
|
+
soft
|
|
5330
|
+
? `Named-Type dependency inventory — **${multi.hitCount}** locator hit(s). `
|
|
5331
|
+
+ '**Partial locator** — locations above; ONE tighter explore with a named Type, '
|
|
5332
|
+
+ 'or ONE narrow Grep — do **not** fan out `homegraph_node` / callees.'
|
|
5333
|
+
: `Named-Type dependency inventory — **${multi.hitCount}** locator hit(s). `
|
|
5334
|
+
+ '**ANSWER NOW** from locations + cross links; one `homegraph_node` only if a body detail is still missing.',
|
|
5335
|
+
'',
|
|
5336
|
+
multi.section,
|
|
5337
|
+
].join('\n'));
|
|
5338
|
+
}
|
|
5339
|
+
}
|
|
3955
5340
|
const lines = [`**Exploration: ${query}**`, '', ''];
|
|
3956
5341
|
const summaryLineIdx = 2;
|
|
3957
5342
|
const inheritanceOnly = (0, query_utils_1.shouldBuildInheritanceSurvey)(query);
|
|
@@ -4007,6 +5392,9 @@ class ToolHandler {
|
|
|
4007
5392
|
|| (0, query_utils_1.queryIsTypeNameFocus)(query)
|
|
4008
5393
|
|| (0, query_utils_1.queryAsInheritanceSurvey)(query)
|
|
4009
5394
|
|| (0, query_utils_1.queryAsDataSourceSurvey)(query)
|
|
5395
|
+
|| (0, query_utils_1.queryAsEventDispatchSurvey)(query)
|
|
5396
|
+
|| (0, query_utils_1.queryAsOutOfRepoSdkCatalog)(query)
|
|
5397
|
+
|| (0, query_utils_1.queryAsKitModuleCapabilitySurvey)(query)
|
|
4010
5398
|
|| (0, query_utils_1.shouldBuildApiUsageSurvey)(query)
|
|
4011
5399
|
|| (0, query_utils_1.queryAsInRepoSystemCapabilityHowto)(query)
|
|
4012
5400
|
|| (0, query_utils_1.queryAsReturnValueConsumerSurvey)(query)
|
|
@@ -4065,7 +5453,7 @@ class ToolHandler {
|
|
|
4065
5453
|
lines.push(moduleDepResult.section);
|
|
4066
5454
|
const moduleExportResult = (0, query_utils_1.queryAsModuleExportSurvey)(query)
|
|
4067
5455
|
? this.buildModuleExportSurveySection(cg, query, projectRoot)
|
|
4068
|
-
: { section: '', hitCount: 0 };
|
|
5456
|
+
: { section: '', hitCount: 0, shellOnly: false };
|
|
4069
5457
|
if (moduleExportResult.section)
|
|
4070
5458
|
lines.push(moduleExportResult.section);
|
|
4071
5459
|
const dtsWrapResult = (0, query_utils_1.queryAsDtsWrapSurvey)(query)
|
|
@@ -4080,14 +5468,24 @@ class ToolHandler {
|
|
|
4080
5468
|
lines.push(controlSyncResult.section);
|
|
4081
5469
|
const dataSourceResult = (0, query_utils_1.queryAsDataSourceSurvey)(query)
|
|
4082
5470
|
? this.buildDataSourceSection(cg, query)
|
|
4083
|
-
: { section: '', edgeCount: 0 };
|
|
5471
|
+
: { section: '', edgeCount: 0, sdkImportCount: 0, strongCount: 0 };
|
|
4084
5472
|
if (dataSourceResult.section)
|
|
4085
5473
|
lines.push(dataSourceResult.section);
|
|
5474
|
+
const eventDispatchResult = (0, query_utils_1.queryAsEventDispatchSurvey)(query)
|
|
5475
|
+
? this.buildEventDispatchSection(cg, query, projectRoot)
|
|
5476
|
+
: { section: '', hitCount: 0, eventCount: 0, handlerCount: 0, memberCount: 0, complete: false };
|
|
5477
|
+
if (eventDispatchResult.section)
|
|
5478
|
+
lines.push(eventDispatchResult.section);
|
|
4086
5479
|
const hoverResult = (0, query_utils_1.shouldBuildHoverHandlerSurvey)(query)
|
|
4087
5480
|
? this.buildHoverHandlerSurveySection(cg, query, projectRoot)
|
|
4088
5481
|
: { section: '', hitCount: 0 };
|
|
4089
5482
|
if (hoverResult.section)
|
|
4090
5483
|
lines.push(hoverResult.section);
|
|
5484
|
+
const containerResult = (0, query_utils_1.queryAsContainerCompositionSurvey)(query)
|
|
5485
|
+
? this.buildContainerCompositionSection(cg, query, projectRoot)
|
|
5486
|
+
: { section: '', hitCount: 0, complete: false };
|
|
5487
|
+
if (containerResult.section)
|
|
5488
|
+
lines.push(containerResult.section);
|
|
4091
5489
|
const importInventoryFilter = (0, query_utils_1.hasImportInventoryFilter)(query);
|
|
4092
5490
|
const multiAnchor = (0, query_utils_1.queryNamesMultipleExploreAnchors)(query);
|
|
4093
5491
|
// Type / caller surveys: skip expensive named-symbol flow synthesize —
|
|
@@ -4095,7 +5493,8 @@ class ToolHandler {
|
|
|
4095
5493
|
const skipFlow = typeSurface || (0, query_utils_1.queryAsCallerOrMethodSurvey)(query)
|
|
4096
5494
|
|| (0, query_utils_1.queryAsDeclarationSiteSurvey)(query)
|
|
4097
5495
|
|| (0, query_utils_1.queryAsInRepoSystemCapabilityHowto)(query)
|
|
4098
|
-
|| (0, query_utils_1.queryAsReturnValueConsumerSurvey)(query)
|
|
5496
|
+
|| (0, query_utils_1.queryAsReturnValueConsumerSurvey)(query)
|
|
5497
|
+
|| (0, query_utils_1.queryAsEventDispatchSurvey)(query);
|
|
4099
5498
|
const flow = skipFlow
|
|
4100
5499
|
? { pathNodeIds: new Set(), text: '' }
|
|
4101
5500
|
: this.buildFlowFromNamedSymbols(cg, query);
|
|
@@ -4138,7 +5537,8 @@ class ToolHandler {
|
|
|
4138
5537
|
|| apiUsageResult.section || dataSourceResult.section || hoverResult.section || inheritanceSection
|
|
4139
5538
|
|| callerSection || memberSection || configSection
|
|
4140
5539
|
|| systemCapResult.section || declarationResult.section || moduleDepResult.section
|
|
4141
|
-
|| moduleExportResult.section || dtsWrapResult.section || controlSyncResult.section
|
|
5540
|
+
|| moduleExportResult.section || dtsWrapResult.section || controlSyncResult.section
|
|
5541
|
+
|| eventDispatchResult.section || containerResult.section;
|
|
4142
5542
|
if (!hasAnySection)
|
|
4143
5543
|
return null;
|
|
4144
5544
|
// Data-source / API usage inventories are complete without source dumps —
|
|
@@ -4147,10 +5547,19 @@ class ToolHandler {
|
|
|
4147
5547
|
return finishCompact(`System-capability howto — **${systemCapResult.hitCount}** in-repo site(s). **ANSWER NOW** from \`@ohos\`/\`System\` call sites above.`);
|
|
4148
5548
|
}
|
|
4149
5549
|
// Declaration intent must NOT fall through to a fat explore / include dump when empty.
|
|
5550
|
+
// Sparse hit lists are Partial locator — hard ANSWER NOW caused D40-style grep storms.
|
|
4150
5551
|
if (declarationResult.attempted) {
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
5552
|
+
const inventoryAsk = /哪些|分别|各自|哪些文件|id\b|声明|绑定/i.test(query);
|
|
5553
|
+
const sparse = declarationResult.hitCount < 3
|
|
5554
|
+
|| (inventoryAsk && declarationResult.hitCount < 6);
|
|
5555
|
+
if (declarationResult.hitCount === 0) {
|
|
5556
|
+
return finishCompact('Declaration-site survey complete (0 sites). **Partial locator** — ONE Grep for the Type constructor is OK.');
|
|
5557
|
+
}
|
|
5558
|
+
if (sparse) {
|
|
5559
|
+
return finishCompact(`Declaration-site survey — **${declarationResult.hitCount}** site(s) (may be incomplete). ` +
|
|
5560
|
+
'**Partial locator** — narrow Grep for more `id` / constructor sites OK; do not repo-storm.');
|
|
5561
|
+
}
|
|
5562
|
+
return finishCompact(`Declaration-site survey — **${declarationResult.hitCount}** site(s). **ANSWER NOW** from ids + bindings above; do not Grep the same Type.`);
|
|
4154
5563
|
}
|
|
4155
5564
|
if (controlSyncResult.section) {
|
|
4156
5565
|
return finishCompact(`Control/Toggle state-sync survey — **${controlSyncResult.hitCount}** file(s). **ANSWER NOW** from Manager/Service sites above.`);
|
|
@@ -4161,7 +5570,10 @@ class ToolHandler {
|
|
|
4161
5570
|
: `Kit module **in-repo usage** — **${kitUsageResult.symbolCount}** imported symbol(s). **ANSWER NOW**; do not Grep the same \`@kit\` path.`);
|
|
4162
5571
|
}
|
|
4163
5572
|
if (moduleExportResult.section) {
|
|
4164
|
-
return finishCompact(
|
|
5573
|
+
return finishCompact(moduleExportResult.shellOnly
|
|
5574
|
+
? `NAPI / native export survey — Init/Export shells only (**${moduleExportResult.hitCount}**). `
|
|
5575
|
+
+ '**Partial locator** — Read \`napi_property_descriptor\` / \`napi_define_properties\` next.'
|
|
5576
|
+
: `NAPI / native export survey — **${moduleExportResult.hitCount}** export(s). **ANSWER NOW**; do not Grep napi_define again.`);
|
|
4165
5577
|
}
|
|
4166
5578
|
if (dtsWrapResult.section) {
|
|
4167
5579
|
return finishCompact(`\`.d.ts\` wrap call sites — **${dtsWrapResult.hitCount}** file(s). **ANSWER NOW**; do not Grep the wrap basename again.`);
|
|
@@ -4170,10 +5582,41 @@ class ToolHandler {
|
|
|
4170
5582
|
return finishCompact(`Module dependency / cycle survey — **ANSWER NOW** from the edges above; do not glob every oh-package.`);
|
|
4171
5583
|
}
|
|
4172
5584
|
// Data-source intent must stop even when edges=0 (empty still beats a 20k dump).
|
|
5585
|
+
// Hard ANSWER NOW only when same-file @ohos/@kit imports exist — weak/local
|
|
5586
|
+
// edges previously caused false "system service" answers (BadgeManager → %AM*).
|
|
4173
5587
|
if (dataSourceResult.section && (0, query_utils_1.queryAsDataSourceSurvey)(query)) {
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
5588
|
+
if ((0, query_utils_1.queryAsDataSourceDistinguishAsk)(query)) {
|
|
5589
|
+
return finishCompact(dataSourceResult.sdkImportCount > 0
|
|
5590
|
+
? `Data-source survey — **${dataSourceResult.sdkImportCount}** \`@ohos\`/\`@kit\` import(s) as anchors. `
|
|
5591
|
+
+ '**Partial locator** — imports alone do not explain unify/distinguish; Read those Manager bodies next.'
|
|
5592
|
+
: 'Data-source survey — **Partial locator** for unify/distinguish; drill related Managers / narrow Grep next.');
|
|
5593
|
+
}
|
|
5594
|
+
if (dataSourceResult.sdkImportCount > 0) {
|
|
5595
|
+
return finishCompact(`Data-source survey — **${dataSourceResult.sdkImportCount}** \`@ohos\`/\`@kit\` import(s). **ANSWER NOW** from those first.`);
|
|
5596
|
+
}
|
|
5597
|
+
if (dataSourceResult.strongCount > 0) {
|
|
5598
|
+
return finishCompact(`Data-source survey — **${dataSourceResult.strongCount}** related writer/sibling Manager(s). ` +
|
|
5599
|
+
'**Partial locator** — drill those Types next; do not ANSWER NOW from local cache helpers.');
|
|
5600
|
+
}
|
|
5601
|
+
return finishCompact('Data-source survey complete (0 SDK imports). **Partial locator** — use the note above; ' +
|
|
5602
|
+
'ONE narrow Grep/`homegraph_callers` on update* — not a repo-wide storm.');
|
|
5603
|
+
}
|
|
5604
|
+
if (eventDispatchResult.section && (0, query_utils_1.queryAsEventDispatchSurvey)(query)) {
|
|
5605
|
+
if (eventDispatchResult.complete) {
|
|
5606
|
+
return finishCompact(`Event→Manager dispatch inventory — **${eventDispatchResult.memberCount || eventDispatchResult.eventCount}** `
|
|
5607
|
+
+ `type/member(s), **${eventDispatchResult.handlerCount}** handler file(s). **ANSWER NOW** from the list above.`);
|
|
5608
|
+
}
|
|
5609
|
+
return finishCompact(eventDispatchResult.handlerCount > 0 || eventDispatchResult.eventCount > 0
|
|
5610
|
+
? `Event→Manager survey — **Partial locator** (`
|
|
5611
|
+
+ `${eventDispatchResult.memberCount} member(s), ${eventDispatchResult.handlerCount} handler file(s)). `
|
|
5612
|
+
+ 'ONE narrow Grep for members/`case`, or one tighter explore — do **not** ANSWER NOW as a full map; no node fan-out.'
|
|
5613
|
+
: 'Event→Manager survey — 0 event types / handlers indexed. **Partial locator** — do **not** ANSWER NOW; '
|
|
5614
|
+
+ 'ONE Grep for enum members / `case` branches OK.');
|
|
5615
|
+
}
|
|
5616
|
+
if (containerResult.section && (0, query_utils_1.queryAsContainerCompositionSurvey)(query)) {
|
|
5617
|
+
return finishCompact(containerResult.complete
|
|
5618
|
+
? `Container composition survey — **${containerResult.hitCount}** page/site(s). **ANSWER NOW** from injection snippets above; do not Grep \`LocalStorageLink\` again.`
|
|
5619
|
+
: `Container composition survey — **${containerResult.hitCount}** site(s). **Partial locator** — Read the top page file for full build() wiring.`);
|
|
4177
5620
|
}
|
|
4178
5621
|
if (apiUsageResult.fileCount > 0 && !dataSourceResult.section) {
|
|
4179
5622
|
return finishCompact(`API usage survey — **${apiUsageResult.fileCount}** file(s). **ANSWER NOW** from the list above.`);
|
|
@@ -4219,8 +5662,11 @@ class ToolHandler {
|
|
|
4219
5662
|
if (apiUsageResult.fileCount > 0) {
|
|
4220
5663
|
return finishCompact(`API usage survey — **${apiUsageResult.fileCount}** file(s). **ANSWER NOW** from the list above.`);
|
|
4221
5664
|
}
|
|
4222
|
-
if (dataSourceResult.
|
|
4223
|
-
return finishCompact(`Data-source survey — **${dataSourceResult.
|
|
5665
|
+
if (dataSourceResult.sdkImportCount > 0) {
|
|
5666
|
+
return finishCompact(`Data-source survey — **${dataSourceResult.sdkImportCount}** \`@ohos\`/\`@kit\` import(s). **ANSWER NOW.**`);
|
|
5667
|
+
}
|
|
5668
|
+
if (dataSourceResult.strongCount > 0) {
|
|
5669
|
+
return finishCompact(`Data-source survey — **${dataSourceResult.strongCount}** related writer/sibling(s). **Partial locator** — drill next.`);
|
|
4224
5670
|
}
|
|
4225
5671
|
if (inheritanceListed) {
|
|
4226
5672
|
return finishCompact('Inheritance survey above lists all direct subtypes found. **ANSWER NOW** — do not grep `extends`.');
|
|
@@ -4281,6 +5727,7 @@ class ToolHandler {
|
|
|
4281
5727
|
const isTestPath = (p) => /(^|\/)(tests?|spec)\//i.test(p) || /\.(test|spec)\./i.test(p);
|
|
4282
5728
|
const fileNodes = new Map();
|
|
4283
5729
|
const seedIds = new Set();
|
|
5730
|
+
const domainPathTokens = (0, query_utils_1.mechanismDomainPathTokens)(query);
|
|
4284
5731
|
const addNode = (n) => {
|
|
4285
5732
|
if ((0, arkts_1.isOhosApiFilePath)(n.filePath))
|
|
4286
5733
|
return;
|
|
@@ -4294,6 +5741,10 @@ class ToolHandler {
|
|
|
4294
5741
|
list.push(n);
|
|
4295
5742
|
fileNodes.set(n.filePath, list);
|
|
4296
5743
|
};
|
|
5744
|
+
// 1) Domain Manager/Service inventory first (static AA class nodes) — locator job.
|
|
5745
|
+
const managerHits = this.collectDomainRoleSymbols(cg, query, domainPathTokens);
|
|
5746
|
+
for (const n of managerHits)
|
|
5747
|
+
addNode(n);
|
|
4297
5748
|
const domainTermsEarly = (0, query_utils_1.extractDomainSearchTerms)(query);
|
|
4298
5749
|
const distinctiveEarly = domainTermsEarly.filter((t) => /^[\x00-\x7F]+$/.test(t) && !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(t.toLowerCase()));
|
|
4299
5750
|
// Distinctive-token IMPORT seeds first — `@ohos.convertxml` / `XmlParse*` before
|
|
@@ -4323,7 +5774,7 @@ class ToolHandler {
|
|
|
4323
5774
|
}
|
|
4324
5775
|
}
|
|
4325
5776
|
catch { /* optional */ }
|
|
4326
|
-
if (seedIds.size >=
|
|
5777
|
+
if (seedIds.size >= 20)
|
|
4327
5778
|
break;
|
|
4328
5779
|
}
|
|
4329
5780
|
}
|
|
@@ -4347,8 +5798,6 @@ class ToolHandler {
|
|
|
4347
5798
|
}
|
|
4348
5799
|
if (seedIds.size < 2 || distinctiveEarly.length > 0) {
|
|
4349
5800
|
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
5801
|
const distinctive = distinctiveEarly;
|
|
4353
5802
|
const ranked = distinctive.length > 0
|
|
4354
5803
|
? [
|
|
@@ -4382,12 +5831,8 @@ class ToolHandler {
|
|
|
4382
5831
|
const nameHit = nameLc.includes(termLc);
|
|
4383
5832
|
const pathHit = pathLc.includes(termLc);
|
|
4384
5833
|
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
5834
|
if (termIsVerb && !pathHit)
|
|
4388
5835
|
continue;
|
|
4389
|
-
// Distinctive token: require name/path/signature to actually mention it
|
|
4390
|
-
// (drops FTS neighbors that only share an unrelated Parser suffix).
|
|
4391
5836
|
if (!termIsVerb && distinctive.length > 0 && !nameHit && !pathHit && !sigHit)
|
|
4392
5837
|
continue;
|
|
4393
5838
|
const entryOk = r.node.kind === 'import'
|
|
@@ -4397,13 +5842,12 @@ class ToolHandler {
|
|
|
4397
5842
|
if (!entryOk)
|
|
4398
5843
|
continue;
|
|
4399
5844
|
addNode(r.node);
|
|
4400
|
-
if (seedIds.size >=
|
|
5845
|
+
if (seedIds.size >= 24)
|
|
4401
5846
|
break;
|
|
4402
5847
|
}
|
|
4403
|
-
if (seedIds.size >=
|
|
5848
|
+
if (seedIds.size >= 16)
|
|
4404
5849
|
break;
|
|
4405
5850
|
}
|
|
4406
|
-
// Prefer seeds that mention a distinctive token when we have any.
|
|
4407
5851
|
if (distinctive.length > 0 && seedIds.size > 0) {
|
|
4408
5852
|
const keep = new Set();
|
|
4409
5853
|
for (const id of seedIds) {
|
|
@@ -4412,14 +5856,25 @@ class ToolHandler {
|
|
|
4412
5856
|
continue;
|
|
4413
5857
|
const line = n.kind === 'import' ? (0, query_utils_1.resolveImportLineFromNode)(n, projectRoot) : '';
|
|
4414
5858
|
const blob = `${n.name}\n${n.filePath}\n${n.signature || ''}\n${line}`.toLowerCase();
|
|
4415
|
-
|
|
5859
|
+
const hitsDistinctive = distinctive.some((t) => blob.includes(t.toLowerCase()));
|
|
5860
|
+
const hitsPathTok = domainPathTokens.some((t) => blob.includes(t));
|
|
5861
|
+
// Domain Manager/Service hits must still match the distinctive stem
|
|
5862
|
+
// (e.g. `xml`) — otherwise agent EN bags (`xml parser`) keep every
|
|
5863
|
+
// `*Parser*Manager` that only matched the verb noun.
|
|
5864
|
+
if ((0, query_utils_1.isDomainRoleSymbol)(n.name, n.filePath, domainPathTokens)) {
|
|
5865
|
+
if (hitsDistinctive || hitsPathTok)
|
|
5866
|
+
keep.add(id);
|
|
5867
|
+
continue;
|
|
5868
|
+
}
|
|
5869
|
+
if (hitsDistinctive)
|
|
5870
|
+
keep.add(id);
|
|
5871
|
+
if (hitsPathTok)
|
|
4416
5872
|
keep.add(id);
|
|
4417
5873
|
}
|
|
4418
5874
|
if (keep.size > 0) {
|
|
4419
5875
|
for (const id of [...seedIds]) {
|
|
4420
|
-
if (!keep.has(id))
|
|
5876
|
+
if (!keep.has(id))
|
|
4421
5877
|
seedIds.delete(id);
|
|
4422
|
-
}
|
|
4423
5878
|
}
|
|
4424
5879
|
for (const [fp, nodes] of [...fileNodes.entries()]) {
|
|
4425
5880
|
const filtered = nodes.filter((n) => keep.has(n.id));
|
|
@@ -4435,18 +5890,33 @@ class ToolHandler {
|
|
|
4435
5890
|
return null;
|
|
4436
5891
|
const seeds = (0, query_utils_1.extractMechanismEntrySeeds)(query);
|
|
4437
5892
|
const flow = this.buildFlowFromNamedSymbols(cg, `${query} ${seeds.join(' ')}`);
|
|
5893
|
+
const hasFlowPath = flow.pathNodeIds.size > 1;
|
|
5894
|
+
const managerSection = this.formatDomainRoleInventory(managerHits.length > 0
|
|
5895
|
+
? managerHits
|
|
5896
|
+
: [...seedIds].map((id) => cg.getNode(id)).filter((n) => !!n && (0, query_utils_1.isDomainRoleSymbol)(n.name, n.filePath, domainPathTokens)));
|
|
5897
|
+
const hasStrongManager = managerSection.strongCount > 0;
|
|
5898
|
+
// Inventory without a connected spine is a locator — hard ANSWER NOW caused
|
|
5899
|
+
// Partial→re-explore / Grep storms after a correct first anchor.
|
|
5900
|
+
const mechanismClosed = hasFlowPath && (hasStrongManager || seeds.length >= 2);
|
|
4438
5901
|
const lines = [
|
|
4439
5902
|
`**Exploration: ${query}**`,
|
|
4440
5903
|
'',
|
|
4441
|
-
|
|
5904
|
+
mechanismClosed
|
|
5905
|
+
? '> **ANSWER NOW from this response** when the Manager inventory + mechanism spine cover the ask. '
|
|
5906
|
+
+ 'Do **not** Grep/Read listed files in parallel — Source below is authoritative.'
|
|
5907
|
+
: '> **Partial locator** — Manager / domain inventory below are anchors, not a full mechanism closure. '
|
|
5908
|
+
+ 'Pick **ONE** concrete `*Manager` / next-hop name from the list for a tighter explore — '
|
|
5909
|
+
+ 'do **not** re-explore a paraphrase of the same keyword bag. '
|
|
5910
|
+
+ 'At most one `homegraph_node` after Partial; prefer ONE narrow Grep for unlisted wiring — not a repo storm. '
|
|
5911
|
+
+ 'Do **not** ANSWER NOW yet.',
|
|
4442
5912
|
'',
|
|
4443
5913
|
`Mechanism anchors: **${seedIds.size}** symbol(s) — lightweight explore (seed + flow spine).`,
|
|
4444
5914
|
'',
|
|
4445
5915
|
];
|
|
4446
|
-
|
|
4447
|
-
|
|
5916
|
+
if (managerSection.text)
|
|
5917
|
+
lines.push(managerSection.text);
|
|
4448
5918
|
const seedImportFocus = (0, query_utils_1.extractMechanismEntrySeeds)(query).filter((s) => !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(s.toLowerCase())
|
|
4449
|
-
&&
|
|
5919
|
+
&& s.length >= 5);
|
|
4450
5920
|
const distinctiveDeps = (0, query_utils_1.extractDependencySymbolsFromQuery)(query)
|
|
4451
5921
|
.filter((s) => !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(s.toLowerCase()));
|
|
4452
5922
|
const importQuery = (seedImportFocus.length > 0
|
|
@@ -4454,19 +5924,22 @@ class ToolHandler {
|
|
|
4454
5924
|
: distinctiveDeps.length > 0
|
|
4455
5925
|
? distinctiveDeps
|
|
4456
5926
|
: (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(' ');
|
|
5927
|
+
.filter((t) => /^[\x00-\x7F]+$/.test(t) && !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(t.toLowerCase()) && t.length >= 3)).slice(0, 4).join(' ');
|
|
4458
5928
|
const importResult = importQuery
|
|
4459
5929
|
? this.buildImportSitesSection(cg, importQuery, projectRoot)
|
|
4460
5930
|
: { section: '', siteCount: 0, compactListing: false };
|
|
4461
|
-
// Light path: keep ≤8 import bullets (full inventory belongs to kit/API surveys).
|
|
4462
5931
|
if (importResult.section) {
|
|
4463
|
-
|
|
5932
|
+
// Wider + path-diversified when a distinctive stem exists (e.g. xml→convertxml
|
|
5933
|
+
// usages span backup/ + util/ — first-N-only hid judge keypoints).
|
|
5934
|
+
const importBullets = distinctiveEarly.length > 0 ? 8 : 5;
|
|
5935
|
+
const trimmed = this.trimLightImportSection(importResult.section, importBullets);
|
|
4464
5936
|
if (trimmed)
|
|
4465
5937
|
lines.push(trimmed);
|
|
4466
5938
|
}
|
|
4467
|
-
if (flow.text)
|
|
5939
|
+
if (flow.text) {
|
|
5940
|
+
lines.push('**Mechanism spine**', '');
|
|
4468
5941
|
lines.push(flow.text);
|
|
4469
|
-
|
|
5942
|
+
}
|
|
4470
5943
|
if ((0, query_utils_1.queryAsNativeRenderThreadSurvey)(query)) {
|
|
4471
5944
|
const cmakeHits = [];
|
|
4472
5945
|
try {
|
|
@@ -4474,12 +5947,6 @@ class ToolHandler {
|
|
|
4474
5947
|
const fp = f.path.replace(/\\/g, '/');
|
|
4475
5948
|
if (!/CMakeLists\.txt$/i.test(fp))
|
|
4476
5949
|
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
5950
|
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, f.path);
|
|
4484
5951
|
if (!abs)
|
|
4485
5952
|
continue;
|
|
@@ -4510,27 +5977,44 @@ class ToolHandler {
|
|
|
4510
5977
|
lines.push('');
|
|
4511
5978
|
}
|
|
4512
5979
|
}
|
|
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)
|
|
5980
|
+
const distinctiveForScore = (0, query_utils_1.extractDomainSearchTerms)(query).filter((t) => /^[\x00-\x7F]+$/.test(t) && !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(t.toLowerCase()) && t.length >= 3);
|
|
5981
|
+
const apiSeeds = (0, query_utils_1.extractMechanismEntrySeeds)(query)
|
|
5982
|
+
.map((s) => s.toLowerCase())
|
|
5983
|
+
.filter((s) => s.length >= 5 && !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(s));
|
|
4515
5984
|
const fileScores = new Map();
|
|
4516
5985
|
for (const [fp, nodes] of fileNodes) {
|
|
4517
5986
|
if ((0, arkts_1.isOhosApiFilePath)(fp))
|
|
4518
5987
|
continue;
|
|
5988
|
+
if ((0, query_utils_1.isTestFile)(fp))
|
|
5989
|
+
continue;
|
|
4519
5990
|
let score = 0;
|
|
4520
|
-
const fpLc = fp.toLowerCase();
|
|
5991
|
+
const fpLc = fp.toLowerCase().replace(/\\/g, '/');
|
|
4521
5992
|
if (distinctiveForScore.some((t) => fpLc.includes(t.toLowerCase())))
|
|
4522
5993
|
score += 30;
|
|
4523
|
-
|
|
4524
|
-
if (apiSeeds.some((s) => s.length >= 6 && fpLc.includes(s)))
|
|
5994
|
+
if (apiSeeds.some((s) => fpLc.includes(s)))
|
|
4525
5995
|
score += 40;
|
|
4526
|
-
|
|
4527
|
-
|
|
5996
|
+
for (const t of domainPathTokens) {
|
|
5997
|
+
if (t.length >= 4 && fpLc.includes(`/${t}/`))
|
|
5998
|
+
score += 60;
|
|
5999
|
+
if (t.length >= 4 && fpLc.includes(t))
|
|
6000
|
+
score += 20;
|
|
6001
|
+
}
|
|
6002
|
+
if ((0, query_utils_1.queryAsNativeRenderThreadSurvey)(query)) {
|
|
6003
|
+
if (/plugin_manager|egl_core|nativerender|xcomponent|render_service/i.test(fpLc))
|
|
6004
|
+
score += 90;
|
|
6005
|
+
if (/\.cpp$|\.h$|cmake/i.test(fpLc))
|
|
6006
|
+
score += 25;
|
|
6007
|
+
}
|
|
4528
6008
|
for (const n of nodes) {
|
|
4529
6009
|
const line = n.kind === 'import' ? (0, query_utils_1.resolveImportLineFromNode)(n, projectRoot) : '';
|
|
4530
6010
|
const blob = `${n.name}\n${n.signature || ''}\n${line}`.toLowerCase();
|
|
6011
|
+
if ((0, query_utils_1.isDomainRoleSymbol)(n.name, n.filePath, domainPathTokens))
|
|
6012
|
+
score += 80;
|
|
6013
|
+
if (/Manager|Subscriber|Listener$/i.test(n.name) && /\/manager\//i.test(fpLc))
|
|
6014
|
+
score += 40;
|
|
4531
6015
|
if (distinctiveForScore.some((t) => blob.includes(t.toLowerCase())))
|
|
4532
6016
|
score += 20;
|
|
4533
|
-
if (apiSeeds.some((s) =>
|
|
6017
|
+
if (apiSeeds.some((s) => blob.includes(s)))
|
|
4534
6018
|
score += 25;
|
|
4535
6019
|
if (n.kind === 'import'
|
|
4536
6020
|
&& distinctiveForScore.some((t) => {
|
|
@@ -4539,7 +6023,7 @@ class ToolHandler {
|
|
|
4539
6023
|
})) {
|
|
4540
6024
|
score += 20;
|
|
4541
6025
|
}
|
|
4542
|
-
if (
|
|
6026
|
+
if (/@kit\.|@ohos\./i.test(line))
|
|
4543
6027
|
score += 30;
|
|
4544
6028
|
if (flow.pathNodeIds.has(n.id))
|
|
4545
6029
|
score += 20;
|
|
@@ -4548,57 +6032,325 @@ class ToolHandler {
|
|
|
4548
6032
|
else
|
|
4549
6033
|
score += 5;
|
|
4550
6034
|
}
|
|
6035
|
+
// Demote incidental restore/settings hits when a domain path token exists.
|
|
6036
|
+
if (domainPathTokens.length > 0 && /restore|settings|statusbar/i.test(fpLc)
|
|
6037
|
+
&& !domainPathTokens.some((t) => fpLc.includes(`/${t}/`))) {
|
|
6038
|
+
score = Math.max(0, score - 40);
|
|
6039
|
+
}
|
|
4551
6040
|
fileScores.set(fp, score);
|
|
4552
6041
|
}
|
|
4553
6042
|
const sortedFiles = [...fileScores.entries()]
|
|
4554
6043
|
.filter(([, s]) => s > 0)
|
|
4555
6044
|
.sort((a, b) => b[1] - a[1])
|
|
4556
6045
|
.slice(0, 3);
|
|
4557
|
-
|
|
4558
|
-
|
|
6046
|
+
// Prefer digests for the best domain-role Type (not import-site top file).
|
|
6047
|
+
const nextMgr = (0, explore_repeat_guard_1.pickBestDomainRoleAnchor)(managerHits.map((n) => ({
|
|
6048
|
+
name: n.name,
|
|
6049
|
+
kind: n.kind,
|
|
6050
|
+
filePath: n.filePath,
|
|
6051
|
+
startLine: n.startLine,
|
|
6052
|
+
})), query, domainPathTokens);
|
|
6053
|
+
if (nextMgr?.filePath) {
|
|
6054
|
+
const pref = nextMgr.filePath;
|
|
6055
|
+
const rest = sortedFiles.filter(([fp]) => fp !== pref);
|
|
6056
|
+
const prefScore = fileScores.get(pref) ?? 100;
|
|
6057
|
+
sortedFiles.length = 0;
|
|
6058
|
+
sortedFiles.push([pref, prefScore], ...rest.slice(0, 2));
|
|
6059
|
+
}
|
|
6060
|
+
const topIsStrong = sortedFiles.some(([fp, s]) => {
|
|
6061
|
+
if (s < 80)
|
|
6062
|
+
return false;
|
|
6063
|
+
const nodes = fileNodes.get(fp) ?? [];
|
|
6064
|
+
return nodes.some((n) => (0, query_utils_1.isDomainRoleSymbol)(n.name, n.filePath, domainPathTokens));
|
|
6065
|
+
});
|
|
6066
|
+
lines.push('**Source digests**', '');
|
|
6067
|
+
lines.push('> Spine / top-hit digests — treat as already Read for these symbols. '
|
|
6068
|
+
+ 'ONE narrow Grep/Read only for residual unindexed wiring; do not dump these files again.');
|
|
4559
6069
|
lines.push('');
|
|
6070
|
+
// Anchor table before digests (universal mid-lean shape).
|
|
6071
|
+
if (sortedFiles.length > 0) {
|
|
6072
|
+
lines.push('**Anchors**', '');
|
|
6073
|
+
for (const [fp] of sortedFiles.slice(0, 6)) {
|
|
6074
|
+
const nodes = fileNodes.get(fp) ?? [];
|
|
6075
|
+
const names = nodes
|
|
6076
|
+
.filter((n) => n.kind !== 'import' && n.kind !== 'export')
|
|
6077
|
+
.slice(0, 4)
|
|
6078
|
+
.map((n) => `\`${n.name}\`(${n.kind}${n.startLine ? `:${n.startLine}` : ''})`)
|
|
6079
|
+
.join(', ');
|
|
6080
|
+
lines.push(`- \`${fp}\`${names ? ` — ${names}` : ''}`);
|
|
6081
|
+
}
|
|
6082
|
+
lines.push('');
|
|
6083
|
+
}
|
|
4560
6084
|
let totalChars = lines.join('\n').length;
|
|
4561
6085
|
let filesRendered = 0;
|
|
6086
|
+
const digest = exploreLocatorDigestEnabled();
|
|
6087
|
+
const perFileCap = digest ? 2800 : 2000;
|
|
6088
|
+
const totalCap = digest ? 6500 : 4_800;
|
|
4562
6089
|
for (const [fp] of sortedFiles) {
|
|
4563
|
-
|
|
4564
|
-
if (filesRendered >= 2 || totalChars > 4_800)
|
|
6090
|
+
if (filesRendered >= 2 || totalChars > totalCap)
|
|
4565
6091
|
break;
|
|
4566
|
-
const chunk = this.renderLightMechanismSource(projectRoot, fp, fileNodes.get(fp) ?? [],
|
|
6092
|
+
const chunk = this.renderLightMechanismSource(projectRoot, fp, fileNodes.get(fp) ?? [], perFileCap);
|
|
4567
6093
|
if (!chunk)
|
|
4568
6094
|
continue;
|
|
4569
|
-
lines.push(chunk);
|
|
4570
|
-
totalChars += chunk.length;
|
|
4571
|
-
filesRendered++;
|
|
6095
|
+
lines.push(chunk);
|
|
6096
|
+
totalChars += chunk.length;
|
|
6097
|
+
filesRendered++;
|
|
6098
|
+
}
|
|
6099
|
+
if (filesRendered === 0 && !importResult.section && !flow.text && !managerSection.text)
|
|
6100
|
+
return null;
|
|
6101
|
+
// Soft-close when inventory + at least one digest exist — enough to answer
|
|
6102
|
+
// with ONE narrow Grep; avoid Partial→re-explore churn.
|
|
6103
|
+
// Also close when distinctive-stem import evidence + digests exist but no
|
|
6104
|
+
// *Manager* survived filtering (xml/convertxml how-implemented bags).
|
|
6105
|
+
const digestCoversNext = !!(nextMgr && sortedFiles.some(([fp]) => fp === nextMgr.filePath)
|
|
6106
|
+
&& filesRendered > 0);
|
|
6107
|
+
const importEvidenceClose = distinctiveEarly.length > 0
|
|
6108
|
+
&& importResult.siteCount >= 3
|
|
6109
|
+
&& filesRendered >= 1;
|
|
6110
|
+
const xmlStem = distinctiveEarly.some((t) => t.toLowerCase() === 'xml');
|
|
6111
|
+
const bodyText = lines.join('\n');
|
|
6112
|
+
const xmlMechanismClose = xmlStem
|
|
6113
|
+
&& filesRendered >= 1
|
|
6114
|
+
&& /convertxml|ConvertXML|fastConvertToJSObject/i.test(bodyText);
|
|
6115
|
+
const nativeRenderClose = (0, query_utils_1.queryAsNativeRenderThreadSurvey)(query)
|
|
6116
|
+
&& filesRendered >= 1
|
|
6117
|
+
&& (/plugin_manager|egl_core|OnSurfaceCreated|EGLCore|nativerender/i.test(bodyText)
|
|
6118
|
+
|| /\*\*CMake \/ link \+ GL libs\*\*/.test(bodyText));
|
|
6119
|
+
// Don't soft-close onto an antithetical Primary (Delete* while ask is install/parse).
|
|
6120
|
+
const nextContradictsPipeline = !!(nextMgr
|
|
6121
|
+
&& /(?:解析|安装|激活|下载|parse|install|activate|download)/i.test(query)
|
|
6122
|
+
&& (/^(?:Delete|Remove|Uninstall|Clear|Cancel)/i.test(nextMgr.name)
|
|
6123
|
+
|| /Delete|Uninstall|RemoveOnline|CancelRestore/i.test(nextMgr.name)));
|
|
6124
|
+
const softClose = !nextContradictsPipeline && ((hasStrongManager && filesRendered >= 1
|
|
6125
|
+
&& (digestCoversNext || managerHits.length >= 2 || topIsStrong))
|
|
6126
|
+
|| importEvidenceClose
|
|
6127
|
+
|| xmlMechanismClose
|
|
6128
|
+
|| nativeRenderClose);
|
|
6129
|
+
const closedAnswer = softClose || mechanismClosed || (hasFlowPath && topIsStrong);
|
|
6130
|
+
lines.push('---');
|
|
6131
|
+
if (mechanismClosed || (hasFlowPath && topIsStrong)) {
|
|
6132
|
+
lines.push('> **Mechanism explore complete — ANSWER NOW** from the Manager inventory + spine + digests above. '
|
|
6133
|
+
+ 'Do **not** call `homegraph_node`, Read, or Grep for symbols/files already shown. '
|
|
6134
|
+
+ 'Only one tighter `homegraph_explore` if a **named** Type essential to the answer is still missing.');
|
|
6135
|
+
}
|
|
6136
|
+
else if (softClose) {
|
|
6137
|
+
lines.push((0, explore_repeat_guard_1.formatMechanismSoftCloseFooter)(nextMgr));
|
|
6138
|
+
}
|
|
6139
|
+
else {
|
|
6140
|
+
const topFp = sortedFiles[0]?.[0];
|
|
6141
|
+
const topNodes = topFp ? (fileNodes.get(topFp) ?? []) : [];
|
|
6142
|
+
const topSym = topNodes.find((n) => n.kind !== 'import' && n.kind !== 'export' && n.startLine > 0)
|
|
6143
|
+
?? topNodes[0];
|
|
6144
|
+
const nextCaption = nextMgr
|
|
6145
|
+
? (0, explore_repeat_guard_1.formatNextAnchorCaption)(nextMgr)
|
|
6146
|
+
: topSym
|
|
6147
|
+
? (0, explore_repeat_guard_1.formatNextAnchorCaption)({
|
|
6148
|
+
name: topSym.name,
|
|
6149
|
+
kind: topSym.kind,
|
|
6150
|
+
filePath: topSym.filePath,
|
|
6151
|
+
startLine: topSym.startLine,
|
|
6152
|
+
})
|
|
6153
|
+
: topFp
|
|
6154
|
+
? `\`${topFp}\``
|
|
6155
|
+
: 'the top Manager / file in the inventory above';
|
|
6156
|
+
lines.push((0, explore_repeat_guard_1.formatMechanismPartialFooter)(nextCaption));
|
|
6157
|
+
}
|
|
6158
|
+
// Opening banner is emitted before digests; rewrite so soft-close never
|
|
6159
|
+
// pairs a Partial header with an ANSWER footer (agent Grep-storm trigger).
|
|
6160
|
+
if (closedAnswer) {
|
|
6161
|
+
const openIdx = lines.findIndex((l) => l.includes('**Partial locator**') || l.includes('**ANSWER NOW from this response**'));
|
|
6162
|
+
if (openIdx >= 0) {
|
|
6163
|
+
lines[openIdx] = softClose && !(mechanismClosed || (hasFlowPath && topIsStrong))
|
|
6164
|
+
? (0, explore_repeat_guard_1.formatMechanismSoftCloseHeader)()
|
|
6165
|
+
: ('> **ANSWER NOW from this response** when the Manager inventory + mechanism spine cover the ask. '
|
|
6166
|
+
+ 'Do **not** Grep/Read listed files in parallel — Source below is authoritative.');
|
|
6167
|
+
}
|
|
6168
|
+
}
|
|
6169
|
+
lines.push('');
|
|
6170
|
+
const text = lines.join('\n');
|
|
6171
|
+
return this.exploreResult(text, {
|
|
6172
|
+
projectRoot,
|
|
6173
|
+
query,
|
|
6174
|
+
files: [],
|
|
6175
|
+
sourceBytes: 0,
|
|
6176
|
+
responseBytes: text.length,
|
|
6177
|
+
// Soft-close must not trip Partial→depth fuse / next-anchor churn.
|
|
6178
|
+
partial: closedAnswer ? false : undefined,
|
|
6179
|
+
nextAnchor: closedAnswer ? undefined : (nextMgr?.name),
|
|
6180
|
+
});
|
|
6181
|
+
}
|
|
6182
|
+
/**
|
|
6183
|
+
* Static AA class/interface nodes that look like domain Managers/Services.
|
|
6184
|
+
*/
|
|
6185
|
+
collectDomainRoleSymbols(cg, query, domainTokens) {
|
|
6186
|
+
const tokens = domainTokens.length > 0
|
|
6187
|
+
? domainTokens
|
|
6188
|
+
: (0, query_utils_1.mechanismDomainPathTokens)(query);
|
|
6189
|
+
if (tokens.length === 0)
|
|
6190
|
+
return [];
|
|
6191
|
+
const byId = new Map();
|
|
6192
|
+
const tryAdd = (n) => {
|
|
6193
|
+
if ((0, arkts_1.isOhosApiFilePath)(n.filePath) || (0, query_utils_1.isTestFile)(n.filePath))
|
|
6194
|
+
return;
|
|
6195
|
+
if (n.kind !== 'class' && n.kind !== 'struct' && n.kind !== 'interface' && n.kind !== 'component')
|
|
6196
|
+
return;
|
|
6197
|
+
if (!(0, query_utils_1.isDomainRoleSymbol)(n.name, n.filePath, tokens))
|
|
6198
|
+
return;
|
|
6199
|
+
byId.set(n.id, n);
|
|
6200
|
+
};
|
|
6201
|
+
for (const seed of (0, query_utils_1.extractMechanismEntrySeeds)(query)) {
|
|
6202
|
+
if (seed.startsWith('@'))
|
|
6203
|
+
continue;
|
|
6204
|
+
try {
|
|
6205
|
+
for (const n of cg.getNodesByName(seed))
|
|
6206
|
+
tryAdd(n);
|
|
6207
|
+
}
|
|
6208
|
+
catch { /* */ }
|
|
6209
|
+
}
|
|
6210
|
+
for (const t of tokens.slice(0, 6)) {
|
|
6211
|
+
let hits = [];
|
|
6212
|
+
try {
|
|
6213
|
+
hits = cg.searchNodes(t, { kinds: ['class', 'struct', 'interface', 'component'], limit: 30 });
|
|
6214
|
+
}
|
|
6215
|
+
catch {
|
|
6216
|
+
continue;
|
|
6217
|
+
}
|
|
6218
|
+
for (const r of hits)
|
|
6219
|
+
tryAdd(r.node);
|
|
6220
|
+
}
|
|
6221
|
+
// Also probe Manager suffix via FTS when a domain token is present.
|
|
6222
|
+
for (const suffix of ['Manager', 'Subscriber', 'Listener']) {
|
|
6223
|
+
let hits = [];
|
|
6224
|
+
try {
|
|
6225
|
+
hits = cg.searchNodes(suffix, { kinds: ['class', 'interface', 'component'], limit: 40 });
|
|
6226
|
+
}
|
|
6227
|
+
catch {
|
|
6228
|
+
continue;
|
|
6229
|
+
}
|
|
6230
|
+
for (const r of hits) {
|
|
6231
|
+
if (tokens.some((t) => {
|
|
6232
|
+
const tl = t.toLowerCase();
|
|
6233
|
+
const blob = `${r.node.name}\n${r.node.filePath}`.toLowerCase();
|
|
6234
|
+
return blob.includes(tl);
|
|
6235
|
+
})) {
|
|
6236
|
+
tryAdd(r.node);
|
|
6237
|
+
}
|
|
6238
|
+
}
|
|
4572
6239
|
}
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
6240
|
+
const themeFocus = tokens.some((t) => /theme/i.test(t));
|
|
6241
|
+
return [...byId.values()]
|
|
6242
|
+
.filter((n) => {
|
|
6243
|
+
if (!themeFocus)
|
|
6244
|
+
return true;
|
|
6245
|
+
const blob = `${n.name}\n${n.filePath}`.toLowerCase().replace(/\\/g, '/');
|
|
6246
|
+
// Theme-pack questions: drop app-install / desktop-download Managers.
|
|
6247
|
+
if (/themeservice|themepack|\/theme|themepackparser|themeactivate|themeinfo|themevip/i.test(blob)) {
|
|
6248
|
+
return true;
|
|
6249
|
+
}
|
|
6250
|
+
if (/appinstall|installermanager|installdatamanager|installevent|downloadaction/i.test(blob)) {
|
|
6251
|
+
return false;
|
|
6252
|
+
}
|
|
6253
|
+
return true;
|
|
6254
|
+
})
|
|
6255
|
+
.sort((a, b) => {
|
|
6256
|
+
const score = (n) => {
|
|
6257
|
+
let s = 0;
|
|
6258
|
+
const fp = n.filePath.toLowerCase().replace(/\\/g, '/');
|
|
6259
|
+
if (/\/manager\//i.test(fp))
|
|
6260
|
+
s += 5;
|
|
6261
|
+
if (/Subscribe|Listener|Bridge/i.test(n.name))
|
|
6262
|
+
s += 3;
|
|
6263
|
+
if (/Manager$/i.test(n.name))
|
|
6264
|
+
s += 2;
|
|
6265
|
+
for (const t of tokens) {
|
|
6266
|
+
if (fp.includes(`/${t}/`))
|
|
6267
|
+
s += 8;
|
|
6268
|
+
if (n.name.toLowerCase().includes(t))
|
|
6269
|
+
s += 4;
|
|
6270
|
+
}
|
|
6271
|
+
if (themeFocus) {
|
|
6272
|
+
if (/themeservice|themepack/i.test(fp) || /ThemePack|ThemePackage|ThemeActivate|ThemeInfo/i.test(n.name)) {
|
|
6273
|
+
s += 25;
|
|
6274
|
+
}
|
|
6275
|
+
if (/appinstall|bigfolder.*download/i.test(fp) && !/theme/i.test(fp + n.name)) {
|
|
6276
|
+
s -= 20;
|
|
6277
|
+
}
|
|
6278
|
+
}
|
|
6279
|
+
return s;
|
|
6280
|
+
};
|
|
6281
|
+
return score(b) - score(a) || a.name.localeCompare(b.name);
|
|
6282
|
+
})
|
|
6283
|
+
.slice(0, 12);
|
|
6284
|
+
}
|
|
6285
|
+
formatDomainRoleInventory(nodes) {
|
|
6286
|
+
if (nodes.length === 0)
|
|
6287
|
+
return { text: '', strongCount: 0 };
|
|
6288
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
6289
|
+
const lines = [
|
|
6290
|
+
'**Domain Manager / Service inventory** (static index — locator anchors)',
|
|
6291
|
+
'',
|
|
6292
|
+
'> Prefer these Types for the next explore or the answer. Do not Grep the domain noun across the repo.',
|
|
6293
|
+
'',
|
|
6294
|
+
];
|
|
6295
|
+
for (const n of nodes.slice(0, 10)) {
|
|
6296
|
+
lines.push(`- \`${n.name}\` (${n.kind}) — \`${rel(n.filePath)}:${n.startLine}\``);
|
|
6297
|
+
}
|
|
6298
|
+
if (nodes.length > 10)
|
|
6299
|
+
lines.push(`- … and ${nodes.length - 10} more`);
|
|
4579
6300
|
lines.push('');
|
|
4580
|
-
return
|
|
6301
|
+
return { text: lines.join('\n'), strongCount: nodes.length };
|
|
4581
6302
|
}
|
|
4582
|
-
/** Cap import bullets inside a light-mechanism response. */
|
|
6303
|
+
/** Cap import bullets inside a light-mechanism response; prefer non-test paths. */
|
|
4583
6304
|
trimLightImportSection(section, maxBullets) {
|
|
4584
6305
|
const lines = section.split('\n');
|
|
4585
|
-
const
|
|
4586
|
-
|
|
4587
|
-
|
|
6306
|
+
const header = [];
|
|
6307
|
+
const prod = [];
|
|
6308
|
+
const test = [];
|
|
4588
6309
|
for (const line of lines) {
|
|
4589
6310
|
if (line.startsWith('- `')) {
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
6311
|
+
const pathMatch = line.match(/^- `([^`]+)`/);
|
|
6312
|
+
const fp = pathMatch?.[1]?.split(':')[0] ?? '';
|
|
6313
|
+
if (fp && (0, query_utils_1.isTestFile)(fp))
|
|
6314
|
+
test.push(line);
|
|
6315
|
+
else
|
|
6316
|
+
prod.push(line);
|
|
4595
6317
|
}
|
|
4596
|
-
if (/^\s*- … and \d+ more/i.test(line))
|
|
6318
|
+
else if (/^\s*- … and \d+ more/i.test(line)) {
|
|
4597
6319
|
continue;
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
6320
|
+
}
|
|
6321
|
+
else {
|
|
6322
|
+
header.push(line);
|
|
6323
|
+
}
|
|
6324
|
+
}
|
|
6325
|
+
// Round-robin across top path prefixes so one product/ folder cannot hide
|
|
6326
|
+
// other convertxml / kit usage sites (backup vs util vs launchercommon).
|
|
6327
|
+
const diversify = (items, max) => {
|
|
6328
|
+
const buckets = new Map();
|
|
6329
|
+
for (const line of items) {
|
|
6330
|
+
const pathMatch = line.match(/^- `([^`]+)`/);
|
|
6331
|
+
const fp = (pathMatch?.[1]?.split(':')[0] ?? '').replace(/\\/g, '/');
|
|
6332
|
+
const parts = fp.split('/').filter(Boolean);
|
|
6333
|
+
const key = parts.length >= 3 ? `${parts[0]}/${parts[1]}/${parts[2]}` : (parts.length >= 2 ? `${parts[0]}/${parts[1]}` : (parts[0] || fp || '_'));
|
|
6334
|
+
const list = buckets.get(key) ?? [];
|
|
6335
|
+
list.push(line);
|
|
6336
|
+
buckets.set(key, list);
|
|
6337
|
+
}
|
|
6338
|
+
const keys = [...buckets.keys()];
|
|
6339
|
+
const out = [];
|
|
6340
|
+
let i = 0;
|
|
6341
|
+
while (out.length < max && keys.some((k) => (buckets.get(k)?.length ?? 0) > 0)) {
|
|
6342
|
+
const k = keys[i % keys.length];
|
|
6343
|
+
const list = buckets.get(k);
|
|
6344
|
+
if (list && list.length > 0)
|
|
6345
|
+
out.push(list.shift());
|
|
6346
|
+
i++;
|
|
6347
|
+
}
|
|
6348
|
+
return out;
|
|
6349
|
+
};
|
|
6350
|
+
const chosen = diversify([...prod, ...test], maxBullets);
|
|
6351
|
+
const out = [...header, ...chosen];
|
|
6352
|
+
if (prod.length + test.length > maxBullets) {
|
|
6353
|
+
out.push(`- … (light-mechanism: showing ${maxBullets} sites, tests deprioritized — **ANSWER NOW**, do not Grep the same import)`);
|
|
4602
6354
|
}
|
|
4603
6355
|
return out.join('\n');
|
|
4604
6356
|
}
|
|
@@ -4687,6 +6439,7 @@ class ToolHandler {
|
|
|
4687
6439
|
const typeNameSet = new Set(typeNames.map((t) => t.toLowerCase()));
|
|
4688
6440
|
const bridge = (0, query_utils_1.queryNeedsCoNamedUseBridge)(query);
|
|
4689
6441
|
const memberFocus = (0, query_utils_1.queryHasNamedMemberFocus)(query);
|
|
6442
|
+
const uiConsequence = (0, query_utils_1.queryAsMemberUiConsequenceSurvey)(query);
|
|
4690
6443
|
const uiCluster = (0, query_utils_1.queryAsFocusedUiCluster)(query);
|
|
4691
6444
|
const typeLifecycle = (0, query_utils_1.queryAsTypeLifecycleSurvey)(query);
|
|
4692
6445
|
const componentSurface = (0, query_utils_1.queryAsComponentSurfaceSurvey)(query)
|
|
@@ -4892,16 +6645,118 @@ class ToolHandler {
|
|
|
4892
6645
|
// skip edge trail for this seed
|
|
4893
6646
|
}
|
|
4894
6647
|
}
|
|
6648
|
+
// Prefer dropping SDK `.d.ts` seeds entirely when any in-repo impl exists for
|
|
6649
|
+
// the same name; if remaining source files are stub-only, soften ANSWER NOW.
|
|
6650
|
+
// Named Type that only exists as stub → soft even when NL leftovers add project seeds (D59).
|
|
6651
|
+
const isStubPath = (fp) => (0, arkts_1.isOhosApiFilePath)(fp) || /\.d\.ts$/i.test(fp);
|
|
6652
|
+
let stubSeedCount = 0;
|
|
6653
|
+
let projectSeedCount = 0;
|
|
6654
|
+
for (const id of seedIds) {
|
|
6655
|
+
try {
|
|
6656
|
+
const n = cg.getNode(id);
|
|
6657
|
+
if (!n)
|
|
6658
|
+
continue;
|
|
6659
|
+
if (isStubPath(n.filePath))
|
|
6660
|
+
stubSeedCount++;
|
|
6661
|
+
else
|
|
6662
|
+
projectSeedCount++;
|
|
6663
|
+
}
|
|
6664
|
+
catch { /* */ }
|
|
6665
|
+
}
|
|
6666
|
+
const primaryNames = (0, query_utils_1.extractTypeNamesFromQuery)(query).filter((t) => !(0, query_utils_1.isFrameworkUiDecoratorName)(t));
|
|
6667
|
+
let primaryStubOnly = false;
|
|
6668
|
+
for (const name of primaryNames.slice(0, 4)) {
|
|
6669
|
+
let nodes = [];
|
|
6670
|
+
try {
|
|
6671
|
+
nodes = cg.getNodesByName(name).filter((n) => !(0, query_utils_1.isTestFile)(n.filePath));
|
|
6672
|
+
}
|
|
6673
|
+
catch {
|
|
6674
|
+
continue;
|
|
6675
|
+
}
|
|
6676
|
+
if (nodes.length === 0)
|
|
6677
|
+
continue;
|
|
6678
|
+
if (nodes.every((n) => isStubPath(n.filePath))) {
|
|
6679
|
+
primaryStubOnly = true;
|
|
6680
|
+
break;
|
|
6681
|
+
}
|
|
6682
|
+
}
|
|
6683
|
+
// Drop stub bodies from the source dump when a project def of the same name exists.
|
|
6684
|
+
// If the *named* Type is stub-only, keep soft framing even if other project seeds remain.
|
|
6685
|
+
const projectFilesPresent = [...fileNodes.keys()].some((fp) => !isStubPath(fp));
|
|
6686
|
+
if (projectFilesPresent && !primaryStubOnly) {
|
|
6687
|
+
for (const fp of [...fileNodes.keys()]) {
|
|
6688
|
+
if (isStubPath(fp))
|
|
6689
|
+
fileNodes.delete(fp);
|
|
6690
|
+
}
|
|
6691
|
+
}
|
|
6692
|
+
const stubHeavy = primaryStubOnly
|
|
6693
|
+
|| (stubSeedCount > 0 && projectSeedCount === 0)
|
|
6694
|
+
|| (fileNodes.size > 0 && [...fileNodes.keys()].every((fp) => isStubPath(fp)));
|
|
6695
|
+
const implAsk = /状态机|state\s*machine|foreground|background|回调|callback|转换|trigger/i.test(query);
|
|
6696
|
+
// Property / resource provenance ($r vs download) — compact body alone is rarely enough.
|
|
6697
|
+
const resourceProvenanceAsk = /\$r\b|系统资源|网络下载|resource\s+vs|vs\.?\s*network|download\s+or|preview|预览|图片加载|loadImage|PixelMap|Image\s*\(/i.test(query);
|
|
6698
|
+
// Multi-Type bags without a connected flow are coarse locators — never fake-complete.
|
|
6699
|
+
const multiTypeThin = typeNames.length >= 2
|
|
6700
|
+
&& !(0, query_utils_1.queryHasNamedMemberFocus)(query)
|
|
6701
|
+
&& !(0, query_utils_1.queryAsNamedComponentAction)(query);
|
|
6702
|
+
let lifecycleStubComplete = false;
|
|
6703
|
+
let lifecycleStubSection = '';
|
|
6704
|
+
if (typeLifecycle && stubHeavy) {
|
|
6705
|
+
const stubPaths = [...fileNodes.keys()].filter(isStubPath);
|
|
6706
|
+
const lifecycleResult = this.buildSdkStubLifecycleSection(projectRoot, stubPaths, query);
|
|
6707
|
+
lifecycleStubSection = lifecycleResult.section;
|
|
6708
|
+
lifecycleStubComplete = lifecycleResult.complete;
|
|
6709
|
+
}
|
|
4895
6710
|
const lines = [
|
|
4896
6711
|
`**Exploration: ${query}**`,
|
|
4897
6712
|
'',
|
|
4898
|
-
|
|
6713
|
+
lifecycleStubComplete
|
|
6714
|
+
? '> **Coarse locate complete — SDK lifecycle inventory below.** **ANSWER NOW** from state enum + callbacks; '
|
|
6715
|
+
+ 'do not Grep in-repo or re-explore the same stub names.'
|
|
6716
|
+
: stubHeavy
|
|
6717
|
+
? '> **Partial locator** — indexed hits are SDK / `.d.ts` stubs, not in-repo implementations. '
|
|
6718
|
+
+ 'Prefer `homegraph_search` / ONE explore for `*State*` / impl class names; '
|
|
6719
|
+
+ 'ONE narrow Grep for in-repo impl names is OK — do not ANSWER NOW from the stub alone.'
|
|
6720
|
+
: resourceProvenanceAsk
|
|
6721
|
+
? '> **Coarse locate — check Source digests for `$r` / Image / download.** '
|
|
6722
|
+
+ 'Treat visible bodies as already Read. ONE narrow Grep for `$r`/`http` only if digests lack load origin — '
|
|
6723
|
+
+ 'do **not** re-explore or fan out Read on the same Page/Dialog.'
|
|
6724
|
+
: multiTypeThin
|
|
6725
|
+
? '> **Partial locator** — multi-Type bag below is a coarse anchor set. '
|
|
6726
|
+
+ 'Pick ONE Type/`Type.member` for a tighter explore, or ONE narrow Grep for unindexed wiring — do **not** ANSWER NOW as a full cross-Type mechanism.'
|
|
6727
|
+
: '> **Coarse locate complete.** Treat Source + trail as already Read for these symbols. '
|
|
6728
|
+
+ 'Answer from them; do **not** re-Grep/Read/node the same symbols. '
|
|
6729
|
+
+ 'ONE narrow Grep is OK only for residual literals / unindexed wiring — not a repo-wide storm.',
|
|
4899
6730
|
'',
|
|
4900
6731
|
`Local-symbol focus: **${seedIds.size}** seed(s)` +
|
|
4901
6732
|
(neighborIds.size > 0 ? `, **${neighborIds.size}** caller/callee neighbor(s)` : '') +
|
|
4902
6733
|
' — compact explore (definition + edge trail, no related-file dump).',
|
|
4903
6734
|
'',
|
|
4904
6735
|
];
|
|
6736
|
+
if (lifecycleStubSection)
|
|
6737
|
+
lines.push(lifecycleStubSection);
|
|
6738
|
+
if (stubHeavy && implAsk && !lifecycleStubComplete) {
|
|
6739
|
+
lines.push('> Stub-only compact — state/callback answers usually live in project `.ets` / `.ts` under the feature, not `@ohos.*.d.ts`.', '');
|
|
6740
|
+
}
|
|
6741
|
+
// Bare `BadgeManager`-style compact: surface real @ohos/@kit / sibling Managers.
|
|
6742
|
+
// Never dump anonymous `%AM*` / local cache callees as "upstream services".
|
|
6743
|
+
const managerSeeds = names.filter((n) => /(?:Manager|Service|Handler|Store)$/i.test(n));
|
|
6744
|
+
if (managerSeeds.length > 0) {
|
|
6745
|
+
const ds = this.buildDataSourceSection(cg, `${managerSeeds.join(' ')} 数据来源于哪个系统服务`);
|
|
6746
|
+
if (ds.section) {
|
|
6747
|
+
lines.push(ds.section);
|
|
6748
|
+
if (ds.sdkImportCount === 0) {
|
|
6749
|
+
// Soften hard compact complete — cache-layer Managers otherwise look "done".
|
|
6750
|
+
const softIdx = lines.findIndex((l) => l.includes('**Coarse locate complete.**') || l.includes('**ANSWER NOW from this response.**'));
|
|
6751
|
+
if (softIdx >= 0) {
|
|
6752
|
+
lines[softIdx] =
|
|
6753
|
+
'> **Partial locator** — Manager body + trail below are anchors. ' +
|
|
6754
|
+
'Do **not** treat local cache/RDB helpers as the system service; ' +
|
|
6755
|
+
'follow related Managers / `homegraph_callers` on `update*` next, or ONE narrow Grep for the service name.';
|
|
6756
|
+
}
|
|
6757
|
+
}
|
|
6758
|
+
}
|
|
6759
|
+
}
|
|
4905
6760
|
// Blast radius for entry seeds (locations only) — same signal full explore
|
|
4906
6761
|
// always-on section gives, so bare-name edits know what to update/verify.
|
|
4907
6762
|
{
|
|
@@ -4970,10 +6825,30 @@ class ToolHandler {
|
|
|
4970
6825
|
}
|
|
4971
6826
|
if (trailBullets > 0) {
|
|
4972
6827
|
trail.push('');
|
|
4973
|
-
trail.push(
|
|
6828
|
+
trail.push(stubHeavy
|
|
6829
|
+
? '> Trail above may reference stubs — prefer in-repo impl files; do not ANSWER NOW from `.d.ts` alone.'
|
|
6830
|
+
: '> Prefer these call/use sites + Source below. Do not re-Grep/Read the same symbols; '
|
|
6831
|
+
+ 'ONE narrow Grep only for residual unindexed wiring.');
|
|
4974
6832
|
trail.push('');
|
|
4975
6833
|
lines.push(...trail);
|
|
4976
6834
|
}
|
|
6835
|
+
else if (!stubHeavy
|
|
6836
|
+
&& !resourceProvenanceAsk
|
|
6837
|
+
&& !multiTypeThin
|
|
6838
|
+
// UI Page/Dialog surfaces + member text-usage already answer without a
|
|
6839
|
+
// graph trail — flipping to Partial caused ThemeHome/isExpired fan-out
|
|
6840
|
+
// (163355 flat-but-worse-cost / score drops).
|
|
6841
|
+
&& !componentSurface
|
|
6842
|
+
&& !uiCluster
|
|
6843
|
+
&& !memberFocus) {
|
|
6844
|
+
// No graph trail — coarse locate only; do not claim completeness.
|
|
6845
|
+
const softIdx = lines.findIndex((l) => l.includes('**Coarse locate complete.**'));
|
|
6846
|
+
if (softIdx >= 0) {
|
|
6847
|
+
lines[softIdx] =
|
|
6848
|
+
'> **Partial locator** — definition anchors below, no call/use trail in the graph. '
|
|
6849
|
+
+ 'ONE tighter explore on a callee/caller name, or ONE narrow Grep for registration — not a repo-wide storm.';
|
|
6850
|
+
}
|
|
6851
|
+
}
|
|
4977
6852
|
}
|
|
4978
6853
|
// When the graph has no call edges (common for static Type.member reads),
|
|
4979
6854
|
// fall back to a bounded indexed-file text scan for `name(` usages so the
|
|
@@ -5101,7 +6976,10 @@ class ToolHandler {
|
|
|
5101
6976
|
}
|
|
5102
6977
|
lines.push('');
|
|
5103
6978
|
lines.push(memberFocus
|
|
5104
|
-
?
|
|
6979
|
+
? uiConsequence
|
|
6980
|
+
? '> UI-consequence ask: call/filter sites above show **how true/expired values affect UI** '
|
|
6981
|
+
+ '(often exclusion from render stacks). **ANSWER NOW** from them + Source — do not Read/Grep the same member.'
|
|
6982
|
+
: '> 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
6983
|
: '> These lines are the in-repo use sites. Answer from them + Source; do not grep the same names again.');
|
|
5106
6984
|
lines.push('');
|
|
5107
6985
|
}
|
|
@@ -5179,7 +7057,7 @@ class ToolHandler {
|
|
|
5179
7057
|
// (FormStack…) burns tokens and invites a follow-up Read of isExpired.
|
|
5180
7058
|
? 1
|
|
5181
7059
|
: ((0, query_utils_1.queryAsNamedComponentAction)(query) || componentSurface || usageFiles.size > 0 || bridge)
|
|
5182
|
-
? (uiCluster ? 2 : (componentSurface ? 2 : 3))
|
|
7060
|
+
? (uiCluster ? 2 : (componentSurface ? 2 : (exploreLocatorDigestEnabled() ? 2 : 3)))
|
|
5183
7061
|
: 2;
|
|
5184
7062
|
const ranked = [...fileNodes.entries()]
|
|
5185
7063
|
.filter(([, nodes]) => {
|
|
@@ -5208,26 +7086,35 @@ class ToolHandler {
|
|
|
5208
7086
|
})
|
|
5209
7087
|
.sort((a, b) => b.score - a.score || a.fp.localeCompare(b.fp))
|
|
5210
7088
|
.slice(0, maxFiles);
|
|
5211
|
-
|
|
5212
|
-
lines.push(
|
|
7089
|
+
const digest = exploreLocatorDigestEnabled();
|
|
7090
|
+
lines.push(digest ? '**Source digests**' : '**Source Code**', '');
|
|
7091
|
+
lines.push(stubHeavy && !lifecycleStubComplete
|
|
7092
|
+
? '> Stub digests only — treat as Partial locator. Do **not** ANSWER NOW from `.d.ts` alone; search in-repo impl next.'
|
|
7093
|
+
: digest
|
|
7094
|
+
? '> Short digests — treat as already Read for these symbols. Answer from anchors + digests; '
|
|
7095
|
+
+ 'ONE narrow Grep only for residual unindexed wiring.'
|
|
7096
|
+
: '> Line-numbered source — treat as already Read. **ANSWER NOW** — do not Read/Grep/search/explore/node the same symbols again.');
|
|
5213
7097
|
lines.push('');
|
|
5214
7098
|
let totalChars = lines.join('\n').length;
|
|
5215
|
-
//
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
: componentSurface || uiCluster
|
|
5220
|
-
? 7000
|
|
5221
|
-
: memberFocus && usageFiles.size > 0
|
|
5222
|
-
? 6500
|
|
5223
|
-
: 5500;
|
|
5224
|
-
const maxPerFile = bareMemberOnly
|
|
5225
|
-
? 2800
|
|
5226
|
-
: componentSurface || uiCluster
|
|
7099
|
+
// Locator-digest ceilings (0012): keep compact under Grep/Read stacks.
|
|
7100
|
+
const maxTotal = digest
|
|
7101
|
+
? (bareMemberOnly ? 3600 : componentSurface || uiCluster ? 5500 : memberFocus && usageFiles.size > 0 ? 5000 : 4800)
|
|
7102
|
+
: (bareMemberOnly
|
|
5227
7103
|
? 4500
|
|
5228
|
-
:
|
|
5229
|
-
?
|
|
5230
|
-
:
|
|
7104
|
+
: componentSurface || uiCluster
|
|
7105
|
+
? 7000
|
|
7106
|
+
: memberFocus && usageFiles.size > 0
|
|
7107
|
+
? 6500
|
|
7108
|
+
: 5500);
|
|
7109
|
+
const maxPerFile = digest
|
|
7110
|
+
? (bareMemberOnly ? 2000 : componentSurface || uiCluster ? 2800 : memberFocus ? 2500 : 2800)
|
|
7111
|
+
: (bareMemberOnly
|
|
7112
|
+
? 2800
|
|
7113
|
+
: componentSurface || uiCluster
|
|
7114
|
+
? 4500
|
|
7115
|
+
: memberFocus
|
|
7116
|
+
? 2800
|
|
7117
|
+
: 3200);
|
|
5231
7118
|
let rendered = 0;
|
|
5232
7119
|
for (const { fp, nodes } of ranked) {
|
|
5233
7120
|
if (rendered >= maxFiles || totalChars > maxTotal)
|
|
@@ -5376,26 +7263,255 @@ class ToolHandler {
|
|
|
5376
7263
|
}
|
|
5377
7264
|
if (rendered === 0)
|
|
5378
7265
|
return null;
|
|
7266
|
+
// If provenance digests already show $r / download / PixelMap, kill any leftover
|
|
7267
|
+
// Partial banner so the agent stops (163355 D17 Partial→Read fan-out).
|
|
7268
|
+
if (resourceProvenanceAsk) {
|
|
7269
|
+
const body = lines.join('\n');
|
|
7270
|
+
if (/\$r\b|PixelMap|copyFile|http[s]?:|download|getPreview|previewPixelMap/i.test(body)) {
|
|
7271
|
+
const softIdx = lines.findIndex((l) => l.includes('**Partial locator**'));
|
|
7272
|
+
if (softIdx >= 0) {
|
|
7273
|
+
lines[softIdx] =
|
|
7274
|
+
'> **Coarse locate complete.** Source digests already show load origin (`$r` / download / PixelMap). '
|
|
7275
|
+
+ '**ANSWER NOW** — do not Read/Grep the same Page/Dialog again.';
|
|
7276
|
+
}
|
|
7277
|
+
}
|
|
7278
|
+
}
|
|
5379
7279
|
lines.push('---');
|
|
5380
7280
|
lines.push('> **Compact local explore complete — ANSWER NOW.** ' +
|
|
5381
7281
|
'Do **not** Read/Grep/`homegraph_search`/`homegraph_explore`/`homegraph_node` for the same symbols.');
|
|
5382
7282
|
lines.push('');
|
|
5383
7283
|
return this.textResult(lines.join('\n'));
|
|
5384
7284
|
}
|
|
7285
|
+
/**
|
|
7286
|
+
* When a lifecycle Type only exists as SDK `.d.ts`, extract state enums +
|
|
7287
|
+
* callback registrations from the stub so agents stop grep-storming in-repo.
|
|
7288
|
+
*/
|
|
7289
|
+
buildSdkStubLifecycleSection(projectRoot, stubFilePaths, _query) {
|
|
7290
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
7291
|
+
const enums = [];
|
|
7292
|
+
const callbacks = [];
|
|
7293
|
+
for (const fp of stubFilePaths.slice(0, 4)) {
|
|
7294
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, fp);
|
|
7295
|
+
if (!abs || !(0, fs_1.existsSync)(abs))
|
|
7296
|
+
continue;
|
|
7297
|
+
let content = '';
|
|
7298
|
+
try {
|
|
7299
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
7300
|
+
}
|
|
7301
|
+
catch {
|
|
7302
|
+
continue;
|
|
7303
|
+
}
|
|
7304
|
+
const fileLines = content.split('\n');
|
|
7305
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
7306
|
+
const line = fileLines[i] ?? '';
|
|
7307
|
+
if (/^\s*enum\s+\w*State\w*\s*\{/.test(line)) {
|
|
7308
|
+
const members = [];
|
|
7309
|
+
for (let j = i + 1; j < Math.min(fileLines.length, i + 48); j++) {
|
|
7310
|
+
const l = fileLines[j] ?? '';
|
|
7311
|
+
if (/^\s*\}/.test(l))
|
|
7312
|
+
break;
|
|
7313
|
+
const m = l.match(/^\s*(\w+)\s*[,=]/);
|
|
7314
|
+
if (m?.[1])
|
|
7315
|
+
members.push(m[1]);
|
|
7316
|
+
}
|
|
7317
|
+
if (members.length > 0) {
|
|
7318
|
+
enums.push(`\`${line.trim().slice(0, 72)}\` → ${members.slice(0, 14).join(', ')}`);
|
|
7319
|
+
}
|
|
7320
|
+
}
|
|
7321
|
+
if (/on\s*\(\s*type\s*:\s*['"][\w]+['"]/.test(line)
|
|
7322
|
+
|| /Register the callback|Register.*Callback|move scene session|foreground|background|sessionStateChange/i.test(line)) {
|
|
7323
|
+
callbacks.push(`- \`${rel(fp)}:${i + 1}\` \`${line.trim().slice(0, 110)}\``);
|
|
7324
|
+
}
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
if (enums.length === 0 && callbacks.length < 3) {
|
|
7328
|
+
return { section: '', complete: false };
|
|
7329
|
+
}
|
|
7330
|
+
const lines = [
|
|
7331
|
+
'**SDK lifecycle surface (indexed stub)**',
|
|
7332
|
+
'',
|
|
7333
|
+
'> Lifecycle Type is stub-only — state enum + callbacks below are authoritative. **ANSWER NOW**; '
|
|
7334
|
+
+ 'do not Grep in-repo for the same SDK API names.',
|
|
7335
|
+
'',
|
|
7336
|
+
];
|
|
7337
|
+
if (enums.length > 0) {
|
|
7338
|
+
lines.push('**State enum(s)**', '');
|
|
7339
|
+
for (const e of enums.slice(0, 4))
|
|
7340
|
+
lines.push(`- ${e}`);
|
|
7341
|
+
lines.push('');
|
|
7342
|
+
}
|
|
7343
|
+
if (callbacks.length > 0) {
|
|
7344
|
+
lines.push('**Callbacks / transitions**', '');
|
|
7345
|
+
lines.push(...callbacks.slice(0, 22));
|
|
7346
|
+
if (callbacks.length > 22)
|
|
7347
|
+
lines.push(`- … and ${callbacks.length - 22} more`);
|
|
7348
|
+
lines.push('');
|
|
7349
|
+
}
|
|
7350
|
+
const complete = enums.length > 0 && callbacks.length >= 3;
|
|
7351
|
+
return { section: lines.join('\n'), complete: complete || callbacks.length >= 6 };
|
|
7352
|
+
}
|
|
7353
|
+
/**
|
|
7354
|
+
* Pages sharing a container component + how they inject differentiated
|
|
7355
|
+
* ViewModel / LocalStorage state into the shared shell.
|
|
7356
|
+
*/
|
|
7357
|
+
buildContainerCompositionSection(cg, query, projectRoot) {
|
|
7358
|
+
const types = (0, query_utils_1.extractTypeNamesFromQuery)(query).filter((t) => !(0, query_utils_1.isFrameworkUiDecoratorName)(t));
|
|
7359
|
+
const containerName = types.find((t) => /(?:Screen|Page|View|Container)$/i.test(t))
|
|
7360
|
+
?? types.find((t) => /Screen/i.test(t))
|
|
7361
|
+
?? types[0];
|
|
7362
|
+
const viewModelName = types.find((t) => /ViewModel$/i.test(t));
|
|
7363
|
+
if (!containerName)
|
|
7364
|
+
return { section: '', hitCount: 0, complete: false };
|
|
7365
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
7366
|
+
const hits = [];
|
|
7367
|
+
const seen = new Set();
|
|
7368
|
+
const ingestFile = (fp, content, baseScore) => {
|
|
7369
|
+
if (seen.has(fp) || (0, query_utils_1.isTestFile)(fp))
|
|
7370
|
+
return;
|
|
7371
|
+
if (!content.includes(containerName))
|
|
7372
|
+
return;
|
|
7373
|
+
const useRe = new RegExp(`${containerName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*[({]`);
|
|
7374
|
+
if (!useRe.test(content) && !content.includes(`import { ${containerName}`))
|
|
7375
|
+
return;
|
|
7376
|
+
const fileLines = content.split('\n');
|
|
7377
|
+
const snippets = [];
|
|
7378
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
7379
|
+
const line = fileLines[i] ?? '';
|
|
7380
|
+
if (line.includes(containerName)
|
|
7381
|
+
|| /@LocalStorageLink|LocalStorageProp|screenSession/i.test(line)
|
|
7382
|
+
|| (viewModelName && line.includes(viewModelName))
|
|
7383
|
+
|| /new\s+\w+ViewModel/i.test(line)) {
|
|
7384
|
+
snippets.push(`L${i + 1}: ${line.trim().slice(0, 100)}`);
|
|
7385
|
+
if (snippets.length >= 4)
|
|
7386
|
+
break;
|
|
7387
|
+
}
|
|
7388
|
+
}
|
|
7389
|
+
if (snippets.length === 0)
|
|
7390
|
+
return;
|
|
7391
|
+
seen.add(fp);
|
|
7392
|
+
let score = baseScore;
|
|
7393
|
+
if (/@LocalStorageLink/i.test(content))
|
|
7394
|
+
score += 30;
|
|
7395
|
+
if (/CommonScbView|EntryView|DevView/i.test(fp))
|
|
7396
|
+
score += 20;
|
|
7397
|
+
if (viewModelName && content.includes(viewModelName))
|
|
7398
|
+
score += 15;
|
|
7399
|
+
hits.push({ file: fp, snippet: snippets.join(' / '), score });
|
|
7400
|
+
};
|
|
7401
|
+
let results = [];
|
|
7402
|
+
try {
|
|
7403
|
+
results = cg.searchNodes(containerName, { kinds: ['import', 'component', 'class', 'struct'], limit: 36 });
|
|
7404
|
+
}
|
|
7405
|
+
catch { /* */ }
|
|
7406
|
+
for (const r of results) {
|
|
7407
|
+
if ((0, arkts_1.isOhosApiFilePath)(r.node.filePath))
|
|
7408
|
+
continue;
|
|
7409
|
+
const fp = rel(r.node.filePath);
|
|
7410
|
+
if (!/\.(ets|tsx|jsx|vue|svelte)$/i.test(fp))
|
|
7411
|
+
continue;
|
|
7412
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
|
|
7413
|
+
if (!abs)
|
|
7414
|
+
continue;
|
|
7415
|
+
let content = '';
|
|
7416
|
+
try {
|
|
7417
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
7418
|
+
}
|
|
7419
|
+
catch {
|
|
7420
|
+
continue;
|
|
7421
|
+
}
|
|
7422
|
+
ingestFile(fp, content, 12);
|
|
7423
|
+
}
|
|
7424
|
+
if (hits.length < 4) {
|
|
7425
|
+
try {
|
|
7426
|
+
for (const f of cg.getFiles()) {
|
|
7427
|
+
if (hits.length >= 14)
|
|
7428
|
+
break;
|
|
7429
|
+
const fp = rel(f.path);
|
|
7430
|
+
if (!/\.ets$/i.test(fp))
|
|
7431
|
+
continue;
|
|
7432
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, f.path);
|
|
7433
|
+
if (!abs)
|
|
7434
|
+
continue;
|
|
7435
|
+
let content = '';
|
|
7436
|
+
try {
|
|
7437
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
7438
|
+
}
|
|
7439
|
+
catch {
|
|
7440
|
+
continue;
|
|
7441
|
+
}
|
|
7442
|
+
ingestFile(fp, content, 8);
|
|
7443
|
+
}
|
|
7444
|
+
}
|
|
7445
|
+
catch { /* optional */ }
|
|
7446
|
+
}
|
|
7447
|
+
hits.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
|
|
7448
|
+
if (hits.length === 0)
|
|
7449
|
+
return { section: '', hitCount: 0, complete: false };
|
|
7450
|
+
const lines = [
|
|
7451
|
+
'**Container composition / ViewModel injection survey**',
|
|
7452
|
+
'',
|
|
7453
|
+
`> Pages using \`${containerName}\`${viewModelName ? ` with \`${viewModelName}\`` : ''}. `
|
|
7454
|
+
+ '**ANSWER NOW** from injection snippets — do not fan out Read/callers per page.',
|
|
7455
|
+
'',
|
|
7456
|
+
];
|
|
7457
|
+
for (const h of hits.slice(0, 10)) {
|
|
7458
|
+
lines.push(`- \`${h.file}\` — ${h.snippet.slice(0, 220)}`);
|
|
7459
|
+
}
|
|
7460
|
+
if (hits.length > 10)
|
|
7461
|
+
lines.push(`- … and ${hits.length - 10} more`);
|
|
7462
|
+
lines.push('');
|
|
7463
|
+
const complete = hits.length >= 2
|
|
7464
|
+
&& hits.some((h) => /@LocalStorageLink|screenSession|ViewModel/i.test(h.snippet));
|
|
7465
|
+
return { section: lines.join('\n'), hitCount: Math.min(hits.length, 10), complete };
|
|
7466
|
+
}
|
|
5385
7467
|
/**
|
|
5386
7468
|
* Hover / pointer-handler inventory — onHover / Hover* symbols + short snippets.
|
|
5387
7469
|
* Used when the agent asks about hover effects without naming a Type yet.
|
|
5388
7470
|
*/
|
|
5389
7471
|
buildHoverHandlerSurveySection(cg, query, projectRoot) {
|
|
5390
7472
|
const topicBoost = /图标|icon|appicon|dock|launcher/i.test(query);
|
|
5391
|
-
const searchTerms =
|
|
7473
|
+
const searchTerms = topicBoost
|
|
7474
|
+
? [
|
|
7475
|
+
'onHover', 'AppIconCommonView', 'HoverAnimationUtil', 'AppIconHoverEvent',
|
|
7476
|
+
'setIconHoverEventIface', 'HoverAnimation', 'HoverEffect',
|
|
7477
|
+
]
|
|
7478
|
+
: ['onHover', 'HoverAnimation', 'HoverEffect', 'HoverConstants', 'hover'];
|
|
5392
7479
|
const seen = new Set();
|
|
5393
7480
|
const hits = [];
|
|
7481
|
+
const scoreHit = (r, term, snippet) => {
|
|
7482
|
+
const nameLc = r.node.name.toLowerCase();
|
|
7483
|
+
const pathLc = r.node.filePath.toLowerCase().replace(/\\/g, '/');
|
|
7484
|
+
const termLc = term.toLowerCase();
|
|
7485
|
+
let score = nameLc === 'onhover' || nameLc.startsWith('onhover') ? 40 : 10;
|
|
7486
|
+
if (/hover/.test(nameLc))
|
|
7487
|
+
score += 15;
|
|
7488
|
+
if (/onhover/.test(snippet.toLowerCase()))
|
|
7489
|
+
score += 35;
|
|
7490
|
+
if (/appiconcommonview|hoveranimationutil|appiconhover/i.test(nameLc))
|
|
7491
|
+
score += 45;
|
|
7492
|
+
if (/seticonhover|geticonhover/i.test(nameLc))
|
|
7493
|
+
score += 35;
|
|
7494
|
+
if (topicBoost && /launchercommon|launchericon|appicon|smartdock|appcenter/i.test(pathLc)) {
|
|
7495
|
+
score += 50;
|
|
7496
|
+
}
|
|
7497
|
+
if (topicBoost && /controlcenter|commonconstants|controlpanel/i.test(pathLc)) {
|
|
7498
|
+
score -= 45;
|
|
7499
|
+
}
|
|
7500
|
+
if (topicBoost && nameLc === 'hoverconstants' && !/launcher|appicon/i.test(pathLc)) {
|
|
7501
|
+
score -= 40;
|
|
7502
|
+
}
|
|
7503
|
+
if (topicBoost && /import/.test(snippet) && !/onhover|hoveranimation|appicon/i.test(snippet.toLowerCase())) {
|
|
7504
|
+
score -= 25;
|
|
7505
|
+
}
|
|
7506
|
+
if (termLc === 'onhover' && /component|struct/.test(r.node.kind))
|
|
7507
|
+
score += 10;
|
|
7508
|
+
return score;
|
|
7509
|
+
};
|
|
5394
7510
|
for (const term of searchTerms) {
|
|
5395
7511
|
let results = [];
|
|
5396
7512
|
try {
|
|
5397
7513
|
results = cg.searchNodes(term, {
|
|
5398
|
-
kinds: ['method', 'function', 'property', 'field', 'variable', 'constant', 'class', 'component'],
|
|
7514
|
+
kinds: ['method', 'function', 'property', 'field', 'variable', 'constant', 'class', 'component', 'struct'],
|
|
5399
7515
|
limit: 30,
|
|
5400
7516
|
});
|
|
5401
7517
|
}
|
|
@@ -5414,11 +7530,6 @@ class ToolHandler {
|
|
|
5414
7530
|
if (seen.has(key))
|
|
5415
7531
|
continue;
|
|
5416
7532
|
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
7533
|
let snippet = (r.node.signature || r.node.name).slice(0, 120);
|
|
5423
7534
|
try {
|
|
5424
7535
|
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
|
|
@@ -5430,12 +7541,38 @@ class ToolHandler {
|
|
|
5430
7541
|
}
|
|
5431
7542
|
}
|
|
5432
7543
|
catch { /* keep signature */ }
|
|
7544
|
+
// App icon views often expose `.onHover` on a builder line — scan nearby.
|
|
7545
|
+
if (topicBoost && /appiconcommonview/i.test(pathLc)) {
|
|
7546
|
+
try {
|
|
7547
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
|
|
7548
|
+
if (abs && (0, fs_1.existsSync)(abs)) {
|
|
7549
|
+
const lines = (0, fs_1.readFileSync)(abs, 'utf-8').split('\n');
|
|
7550
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7551
|
+
if (!/\.onHover\s*\(/.test(lines[i] ?? ''))
|
|
7552
|
+
continue;
|
|
7553
|
+
const hk = `${r.node.filePath}:${i + 1}:onHover`;
|
|
7554
|
+
if (seen.has(hk))
|
|
7555
|
+
continue;
|
|
7556
|
+
seen.add(hk);
|
|
7557
|
+
hits.push({
|
|
7558
|
+
file: r.node.filePath,
|
|
7559
|
+
line: i + 1,
|
|
7560
|
+
name: 'onHover',
|
|
7561
|
+
kind: 'property',
|
|
7562
|
+
score: 90,
|
|
7563
|
+
snippet: (lines[i] ?? '').trim().slice(0, 160),
|
|
7564
|
+
});
|
|
7565
|
+
}
|
|
7566
|
+
}
|
|
7567
|
+
}
|
|
7568
|
+
catch { /* optional */ }
|
|
7569
|
+
}
|
|
5433
7570
|
hits.push({
|
|
5434
7571
|
file: r.node.filePath,
|
|
5435
7572
|
line: r.node.startLine,
|
|
5436
7573
|
name: r.node.name,
|
|
5437
7574
|
kind: r.node.kind,
|
|
5438
|
-
score,
|
|
7575
|
+
score: scoreHit(r, term, snippet),
|
|
5439
7576
|
snippet,
|
|
5440
7577
|
});
|
|
5441
7578
|
}
|
|
@@ -5454,20 +7591,32 @@ class ToolHandler {
|
|
|
5454
7591
|
};
|
|
5455
7592
|
}
|
|
5456
7593
|
hits.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
|
|
7594
|
+
// Icon-hover asks: drop import-only control-center noise from the visible set.
|
|
7595
|
+
const filtered = topicBoost
|
|
7596
|
+
? hits.filter((h) => {
|
|
7597
|
+
const pl = h.file.toLowerCase();
|
|
7598
|
+
if (h.score >= 35)
|
|
7599
|
+
return true;
|
|
7600
|
+
if (/controlcenter|commonconstants/.test(pl) && h.name === 'HoverConstants')
|
|
7601
|
+
return false;
|
|
7602
|
+
return h.score > 0;
|
|
7603
|
+
})
|
|
7604
|
+
: hits;
|
|
7605
|
+
const displayHits = filtered.length > 0 ? filtered : hits;
|
|
5457
7606
|
const lines = [
|
|
5458
7607
|
'**Hover / pointer handler survey**',
|
|
5459
7608
|
'',
|
|
5460
7609
|
'> **ANSWER NOW** from these in-repo hover handlers + snippets. Do not Grep `onHover` again unless a named Type is still missing.',
|
|
5461
7610
|
'',
|
|
5462
7611
|
];
|
|
5463
|
-
const shown =
|
|
7612
|
+
const shown = displayHits.slice(0, 12);
|
|
5464
7613
|
for (const h of shown) {
|
|
5465
7614
|
lines.push(`- \`${h.name}\` (${h.kind}) — \`${h.file}:${h.line}\``);
|
|
5466
7615
|
if (h.snippet)
|
|
5467
7616
|
lines.push(` \`${h.snippet.slice(0, 100)}\``);
|
|
5468
7617
|
}
|
|
5469
|
-
if (
|
|
5470
|
-
lines.push(`- … and ${
|
|
7618
|
+
if (displayHits.length > shown.length)
|
|
7619
|
+
lines.push(`- … and ${displayHits.length - shown.length} more`);
|
|
5471
7620
|
lines.push('');
|
|
5472
7621
|
return { section: lines.join('\n'), hitCount: shown.length };
|
|
5473
7622
|
}
|
|
@@ -5618,8 +7767,8 @@ class ToolHandler {
|
|
|
5618
7767
|
lines.push(`- \`${sym}\` — imported in ${fileList}${more}`);
|
|
5619
7768
|
}
|
|
5620
7769
|
}
|
|
5621
|
-
// "
|
|
5622
|
-
if (
|
|
7770
|
+
// Install/deps asks (incl. agent EN "parameters dependencies") — oh-package lines.
|
|
7771
|
+
if ((0, query_utils_1.queryAsksKitInstallDeps)(query)) {
|
|
5623
7772
|
const depLines = [];
|
|
5624
7773
|
try {
|
|
5625
7774
|
for (const f of cg.getFiles()) {
|
|
@@ -6013,7 +8162,7 @@ class ToolHandler {
|
|
|
6013
8162
|
for (const typeName of types) {
|
|
6014
8163
|
let hits = [];
|
|
6015
8164
|
try {
|
|
6016
|
-
hits = cg.searchNodes(typeName, { limit:
|
|
8165
|
+
hits = cg.searchNodes(typeName, { limit: 200 });
|
|
6017
8166
|
}
|
|
6018
8167
|
catch {
|
|
6019
8168
|
continue;
|
|
@@ -6036,16 +8185,29 @@ class ToolHandler {
|
|
|
6036
8185
|
continue;
|
|
6037
8186
|
files.set(abs, rel(r.node.filePath));
|
|
6038
8187
|
}
|
|
8188
|
+
// Import sites of the Type — JSX construction often lives in the same file.
|
|
8189
|
+
try {
|
|
8190
|
+
for (const r of cg.searchNodes(typeName, { kinds: ['import'], limit: 80 })) {
|
|
8191
|
+
if ((0, query_utils_1.isTestFile)(r.node.filePath) || (0, arkts_1.isOhosApiFilePath)(r.node.filePath))
|
|
8192
|
+
continue;
|
|
8193
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
|
|
8194
|
+
if (!abs || !(0, fs_1.existsSync)(abs))
|
|
8195
|
+
continue;
|
|
8196
|
+
files.set(abs, rel(r.node.filePath));
|
|
8197
|
+
}
|
|
8198
|
+
}
|
|
8199
|
+
catch { /* optional */ }
|
|
6039
8200
|
// 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
|
|
8201
|
+
// site — expand to sibling .ets under the same feature/staticcommon/product roots
|
|
8202
|
+
// already touched by hits/imports.
|
|
6042
8203
|
const featureRoots = new Set();
|
|
6043
8204
|
for (const fp of files.values()) {
|
|
6044
|
-
const m = fp.match(/^(feature\/[^/]+|staticcommon\/[^/]+)/);
|
|
8205
|
+
const m = fp.match(/^(feature\/[^/]+|staticcommon\/[^/]+|product\/[^/]+)/);
|
|
6045
8206
|
if (m?.[1])
|
|
6046
8207
|
featureRoots.add(m[1]);
|
|
6047
8208
|
}
|
|
6048
|
-
|
|
8209
|
+
const inventoryAsk = /哪些|分别|各自|哪些文件|id\b|声明/i.test(query);
|
|
8210
|
+
if (featureRoots.size > 0 && featureRoots.size <= (inventoryAsk ? 24 : 6)) {
|
|
6049
8211
|
try {
|
|
6050
8212
|
for (const f of cg.getFiles()) {
|
|
6051
8213
|
const fp = rel(f.path);
|
|
@@ -6119,23 +8281,32 @@ class ToolHandler {
|
|
|
6119
8281
|
section: [
|
|
6120
8282
|
'**Declaration / id / native-binding survey**',
|
|
6121
8283
|
'',
|
|
6122
|
-
`> No UI/native construction sites for \`${types.join('`, `')}\` in the index. **
|
|
8284
|
+
`> No UI/native construction sites for \`${types.join('`, `')}\` in the index. **Partial locator** — ` +
|
|
8285
|
+
`ONE Grep for \`<${types[0]}\` / \`${types[0]}(\` is OK; do not repo-storm.`,
|
|
6123
8286
|
'',
|
|
6124
8287
|
].join('\n'),
|
|
6125
8288
|
hitCount: 0,
|
|
6126
8289
|
attempted: true,
|
|
6127
8290
|
};
|
|
6128
8291
|
}
|
|
8292
|
+
const hitCount = Math.min(rows.length, 20);
|
|
8293
|
+
// Sparse inventories (1–2 sites) are incomplete for "哪些文件/id/分别" — never hard ANSWER NOW.
|
|
8294
|
+
const inventoryAsk = /哪些|分别|各自|哪些文件|id\b|声明|绑定/i.test(query);
|
|
8295
|
+
const sparse = hitCount < 3 || (inventoryAsk && hitCount < 6);
|
|
8296
|
+
const guidance = sparse
|
|
8297
|
+
? `> Construction / JSX / \`id\` / native register sites above (**${hitCount}**). **Partial locator** — ` +
|
|
8298
|
+
'list may be incomplete; ONE narrow Grep for more `id:` / `XComponent(` sites is OK.'
|
|
8299
|
+
: '> Construction / JSX / `id` / native register sites. **ANSWER NOW** — do not Grep the same Type again.';
|
|
6129
8300
|
return {
|
|
6130
8301
|
section: [
|
|
6131
8302
|
'**Declaration / id / native-binding survey**',
|
|
6132
8303
|
'',
|
|
6133
|
-
|
|
8304
|
+
guidance,
|
|
6134
8305
|
'',
|
|
6135
8306
|
...rows.slice(0, 20),
|
|
6136
8307
|
'',
|
|
6137
8308
|
].join('\n'),
|
|
6138
|
-
hitCount
|
|
8309
|
+
hitCount,
|
|
6139
8310
|
attempted: true,
|
|
6140
8311
|
};
|
|
6141
8312
|
}
|
|
@@ -6144,7 +8315,7 @@ class ToolHandler {
|
|
|
6144
8315
|
const rawSymbols = (0, query_utils_1.extractApiUsageTokens)(query).slice(0, 6);
|
|
6145
8316
|
if (rawSymbols.length === 0)
|
|
6146
8317
|
return { section: '', fileCount: 0 };
|
|
6147
|
-
// Expand Telephony → telephony / @ohos.telephony
|
|
8318
|
+
// Expand Telephony → telephony / @ohos.telephony / @kit.TelephonyKit call sites.
|
|
6148
8319
|
const symbols = [];
|
|
6149
8320
|
for (const sym of rawSymbols) {
|
|
6150
8321
|
symbols.push(sym);
|
|
@@ -6157,6 +8328,12 @@ class ToolHandler {
|
|
|
6157
8328
|
symbols.push(ohos);
|
|
6158
8329
|
}
|
|
6159
8330
|
}
|
|
8331
|
+
if (rawSymbols.some((s) => /^telephony$/i.test(s))) {
|
|
8332
|
+
for (const kit of ['@kit.TelephonyKit', '@hms.telephony.enhanced', '@hms.telephony.vsim', 'TelephonyKit']) {
|
|
8333
|
+
if (!symbols.includes(kit))
|
|
8334
|
+
symbols.push(kit);
|
|
8335
|
+
}
|
|
8336
|
+
}
|
|
6160
8337
|
const rel = (p) => p.replace(/\\/g, '/');
|
|
6161
8338
|
const hits = new Map();
|
|
6162
8339
|
const addHit = (file, line) => {
|
|
@@ -6170,8 +8347,54 @@ class ToolHandler {
|
|
|
6170
8347
|
prev.stub = prev.stub && stub;
|
|
6171
8348
|
hits.set(fp, prev);
|
|
6172
8349
|
};
|
|
6173
|
-
|
|
8350
|
+
if (rawSymbols.some((s) => /^telephony$/i.test(s))) {
|
|
8351
|
+
for (const kit of ['@kit.TelephonyKit', '@hms.telephony.enhanced', '@hms.telephony.vsim']) {
|
|
8352
|
+
let importHits = [];
|
|
8353
|
+
try {
|
|
8354
|
+
importHits = cg.searchNodes(kit.replace(/^@/, ''), { kinds: ['import'], limit: 40 });
|
|
8355
|
+
}
|
|
8356
|
+
catch {
|
|
8357
|
+
importHits = [];
|
|
8358
|
+
}
|
|
8359
|
+
for (const r of importHits) {
|
|
8360
|
+
const line = (0, query_utils_1.resolveImportLineFromNode)(r.node, projectRoot);
|
|
8361
|
+
if (line.includes(kit) || line.toLowerCase().includes(kit.toLowerCase())) {
|
|
8362
|
+
addHit(r.node.filePath, r.node.startLine);
|
|
8363
|
+
}
|
|
8364
|
+
}
|
|
8365
|
+
// FTS stem fallback (`TelephonyKit`, `telephony.vsim`, …).
|
|
8366
|
+
const stem = kit.split('.').pop() ?? kit;
|
|
8367
|
+
try {
|
|
8368
|
+
importHits = cg.searchNodes(stem, { kinds: ['import'], limit: 40 });
|
|
8369
|
+
}
|
|
8370
|
+
catch {
|
|
8371
|
+
importHits = [];
|
|
8372
|
+
}
|
|
8373
|
+
for (const r of importHits) {
|
|
8374
|
+
const line = (0, query_utils_1.resolveImportLineFromNode)(r.node, projectRoot);
|
|
8375
|
+
if (line.includes('@kit.TelephonyKit') || line.includes('@hms.telephony')) {
|
|
8376
|
+
addHit(r.node.filePath, r.node.startLine);
|
|
8377
|
+
}
|
|
8378
|
+
}
|
|
8379
|
+
}
|
|
8380
|
+
}
|
|
8381
|
+
for (const sym of symbols.slice(0, 8)) {
|
|
6174
8382
|
const symLc = sym.toLowerCase();
|
|
8383
|
+
if (sym.startsWith('@')) {
|
|
8384
|
+
let importNodes = [];
|
|
8385
|
+
try {
|
|
8386
|
+
importNodes = cg.searchNodes(sym.replace(/^@/, ''), { kinds: ['import'], limit: 28 });
|
|
8387
|
+
}
|
|
8388
|
+
catch {
|
|
8389
|
+
importNodes = [];
|
|
8390
|
+
}
|
|
8391
|
+
for (const r of importNodes) {
|
|
8392
|
+
const line = (0, query_utils_1.resolveImportLineFromNode)(r.node, projectRoot).toLowerCase();
|
|
8393
|
+
if (line.includes(symLc.replace(/^@/, '')) || line.includes(symLc)) {
|
|
8394
|
+
addHit(r.node.filePath, r.node.startLine);
|
|
8395
|
+
}
|
|
8396
|
+
}
|
|
8397
|
+
}
|
|
6175
8398
|
let nodes = [];
|
|
6176
8399
|
try {
|
|
6177
8400
|
nodes = cg.searchNodes(sym.replace(/^@/, ''), { limit: 40 });
|
|
@@ -6270,6 +8493,14 @@ class ToolHandler {
|
|
|
6270
8493
|
const rankedFiles = [...hits.entries()].sort((a, b) => {
|
|
6271
8494
|
if (a[1].stub !== b[1].stub)
|
|
6272
8495
|
return a[1].stub ? 1 : -1;
|
|
8496
|
+
const telephonyBoost = (fp) => rawSymbols.some((s) => /^telephony$/i.test(s))
|
|
8497
|
+
&& /@kit\.telephonykit|@hms\.telephony|telephonykit/i.test(fp.toLowerCase())
|
|
8498
|
+
? -1
|
|
8499
|
+
: 0;
|
|
8500
|
+
const ta = telephonyBoost(a[0]);
|
|
8501
|
+
const tb = telephonyBoost(b[0]);
|
|
8502
|
+
if (ta !== tb)
|
|
8503
|
+
return ta - tb;
|
|
6273
8504
|
return a[0].localeCompare(b[0]);
|
|
6274
8505
|
});
|
|
6275
8506
|
const projectHits = rankedFiles.filter(([, v]) => !v.stub);
|
|
@@ -6307,9 +8538,17 @@ class ToolHandler {
|
|
|
6307
8538
|
}
|
|
6308
8539
|
}
|
|
6309
8540
|
// Recompute display after call-site enrichment.
|
|
8541
|
+
const telephonyBoost = (fp) => rawSymbols.some((s) => /^telephony$/i.test(s))
|
|
8542
|
+
&& /@kit\.telephonykit|@hms\.telephony|telephonykit/i.test(fp.toLowerCase())
|
|
8543
|
+
? -1
|
|
8544
|
+
: 0;
|
|
6310
8545
|
const rankedFiles2 = [...hits.entries()].sort((a, b) => {
|
|
6311
8546
|
if (a[1].stub !== b[1].stub)
|
|
6312
8547
|
return a[1].stub ? 1 : -1;
|
|
8548
|
+
const ta = telephonyBoost(a[0]);
|
|
8549
|
+
const tb = telephonyBoost(b[0]);
|
|
8550
|
+
if (ta !== tb)
|
|
8551
|
+
return ta - tb;
|
|
6313
8552
|
return a[0].localeCompare(b[0]);
|
|
6314
8553
|
});
|
|
6315
8554
|
const projectHits2 = rankedFiles2.filter(([, v]) => !v.stub);
|
|
@@ -6363,6 +8602,34 @@ class ToolHandler {
|
|
|
6363
8602
|
lines.push(`- … and ${methodNames.size - 24} more`);
|
|
6364
8603
|
lines.push('');
|
|
6365
8604
|
}
|
|
8605
|
+
if (rawSymbols.some((s) => /^telephony$/i.test(s))) {
|
|
8606
|
+
const kitSurfaces = new Set();
|
|
8607
|
+
for (const [fp, info] of display2.slice(0, 40)) {
|
|
8608
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, fp);
|
|
8609
|
+
if (!abs || !(0, fs_1.existsSync)(abs))
|
|
8610
|
+
continue;
|
|
8611
|
+
let fileLines = [];
|
|
8612
|
+
try {
|
|
8613
|
+
fileLines = (0, fs_1.readFileSync)(abs, 'utf-8').split('\n');
|
|
8614
|
+
}
|
|
8615
|
+
catch {
|
|
8616
|
+
continue;
|
|
8617
|
+
}
|
|
8618
|
+
for (const ln of info.lines.slice(0, 8)) {
|
|
8619
|
+
const lineText = fileLines[ln - 1] ?? '';
|
|
8620
|
+
for (const m of lineText.matchAll(/@kit\.TelephonyKit|@hms\.telephony(?:\.\w+)?/g)) {
|
|
8621
|
+
if (m[0])
|
|
8622
|
+
kitSurfaces.add(m[0]);
|
|
8623
|
+
}
|
|
8624
|
+
}
|
|
8625
|
+
}
|
|
8626
|
+
if (kitSurfaces.size > 0) {
|
|
8627
|
+
lines.push('**@kit / @hms Telephony imports**');
|
|
8628
|
+
for (const k of [...kitSurfaces].sort())
|
|
8629
|
+
lines.push(`- \`${k}\``);
|
|
8630
|
+
lines.push('');
|
|
8631
|
+
}
|
|
8632
|
+
}
|
|
6366
8633
|
let shown = 0;
|
|
6367
8634
|
const isImportish = (s) => /^\s*import\b/.test(s) || /\bfrom\s+['"]@/.test(s);
|
|
6368
8635
|
const isLifetimeish = (s) => /\b(?:new|delete|free|reset)\b/i.test(s) || /=\s*new\b/.test(s);
|
|
@@ -6733,11 +9000,24 @@ class ToolHandler {
|
|
|
6733
9000
|
}
|
|
6734
9001
|
const cg = this.getHomeGraph(args.projectPath);
|
|
6735
9002
|
const projectRoot = cg.getProjectRoot();
|
|
9003
|
+
// Same-bag / call-budget refuse (session view injected by execute).
|
|
9004
|
+
const sessionPrior = (0, explore_session_state_1.viewForProject)((0, explore_session_state_1.readExploreSessionView)(args), projectRoot);
|
|
9005
|
+
const repeat = (0, explore_repeat_guard_1.decideExploreRepeat)(sessionPrior, query);
|
|
9006
|
+
if (repeat.refuse) {
|
|
9007
|
+
const text = (0, explore_repeat_guard_1.formatExploreRepeatRefuse)(repeat, query);
|
|
9008
|
+
return this.exploreResult(text, {
|
|
9009
|
+
projectRoot,
|
|
9010
|
+
query,
|
|
9011
|
+
files: [],
|
|
9012
|
+
sourceBytes: 0,
|
|
9013
|
+
responseBytes: text.length,
|
|
9014
|
+
});
|
|
9015
|
+
}
|
|
6736
9016
|
const compactLocal = this.tryFastInventoryExplore(cg, query, projectRoot)
|
|
6737
9017
|
?? this.tryLightMechanismExplore(cg, query, projectRoot)
|
|
6738
9018
|
?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot);
|
|
6739
9019
|
if (compactLocal)
|
|
6740
|
-
return compactLocal;
|
|
9020
|
+
return this.ensureExploreEmission(compactLocal, projectRoot, query);
|
|
6741
9021
|
// Light mechanism already attempted above.
|
|
6742
9022
|
// Resolve adaptive output budget from project size.
|
|
6743
9023
|
// largest-tier defaults if stats aren't available, which preserves
|
|
@@ -7406,7 +9686,11 @@ class ToolHandler {
|
|
|
7406
9686
|
'production handlers are out of scope unless explicitly referenced in the test.');
|
|
7407
9687
|
lines.push('');
|
|
7408
9688
|
}
|
|
7409
|
-
const importResult = (0, query_utils_1.queryAsksKitInstallDeps)(query)
|
|
9689
|
+
const importResult = ((0, query_utils_1.queryAsksKitInstallDeps)(query)
|
|
9690
|
+
|| (0, query_utils_1.queryAsEventDispatchSurvey)(query)
|
|
9691
|
+
|| (0, query_utils_1.queryAsOutOfRepoSdkCatalog)(query)
|
|
9692
|
+
|| (0, query_utils_1.queryAsKitModuleCapabilitySurvey)(query)
|
|
9693
|
+
|| (0, query_utils_1.queryAsDataSourceSurvey)(query))
|
|
7410
9694
|
? { section: '', siteCount: 0, compactListing: false }
|
|
7411
9695
|
: this.buildImportSitesSection(cg, query, projectRoot);
|
|
7412
9696
|
if (importResult.section)
|
|
@@ -7434,9 +9718,14 @@ class ToolHandler {
|
|
|
7434
9718
|
lines.push(apiUsageResult.section);
|
|
7435
9719
|
const dataSourceResult = (0, query_utils_1.queryAsDataSourceSurvey)(query)
|
|
7436
9720
|
? this.buildDataSourceSection(cg, query)
|
|
7437
|
-
: { section: '', edgeCount: 0 };
|
|
9721
|
+
: { section: '', edgeCount: 0, sdkImportCount: 0, strongCount: 0 };
|
|
7438
9722
|
if (dataSourceResult.section)
|
|
7439
9723
|
lines.push(dataSourceResult.section);
|
|
9724
|
+
const eventDispatchResult = (0, query_utils_1.queryAsEventDispatchSurvey)(query)
|
|
9725
|
+
? this.buildEventDispatchSection(cg, query, projectRoot)
|
|
9726
|
+
: { section: '', hitCount: 0, eventCount: 0, handlerCount: 0, memberCount: 0, complete: false };
|
|
9727
|
+
if (eventDispatchResult.section)
|
|
9728
|
+
lines.push(eventDispatchResult.section);
|
|
7440
9729
|
const importInventoryFilter = (0, query_utils_1.hasImportInventoryFilter)(query);
|
|
7441
9730
|
const multiAnchor = (0, query_utils_1.queryNamesMultipleExploreAnchors)(query) || crossModuleFlow;
|
|
7442
9731
|
const mechanismSurvey = (0, query_utils_1.queryAsMechanismSurvey)(query);
|
|
@@ -7521,6 +9810,12 @@ class ToolHandler {
|
|
|
7521
9810
|
if (configSection) {
|
|
7522
9811
|
return finishCompact('Config/manifest content above — answer from it directly.');
|
|
7523
9812
|
}
|
|
9813
|
+
if (eventDispatchResult.section && (0, query_utils_1.queryAsEventDispatchSurvey)(query)) {
|
|
9814
|
+
if (eventDispatchResult.complete) {
|
|
9815
|
+
return finishCompact(`Event→Manager dispatch inventory — **${eventDispatchResult.handlerCount}** handler file(s). **ANSWER NOW**.`);
|
|
9816
|
+
}
|
|
9817
|
+
return finishCompact('Event→Manager survey incomplete. **Partial locator** — do **not** ANSWER NOW as a full dispatch map.');
|
|
9818
|
+
}
|
|
7524
9819
|
if (kitUsageResult.section) {
|
|
7525
9820
|
return finishCompact(`Kit module **in-repo usage** survey — **${kitUsageResult.symbolCount}** imported symbol(s). ` +
|
|
7526
9821
|
'Not an SDK catalog. **ANSWER NOW** from the usage list; do not Grep the same `@kit` path.');
|
|
@@ -7534,8 +9829,11 @@ class ToolHandler {
|
|
|
7534
9829
|
if (listedTypeMethods && callerBulletCount > 0) {
|
|
7535
9830
|
return finishCompact(`Caller inventory — **${callerBulletCount}** site(s) for listed methods. **ANSWER NOW**; do not fan out callers per method.`);
|
|
7536
9831
|
}
|
|
7537
|
-
if (dataSourceResult.section && dataSourceResult.
|
|
7538
|
-
return finishCompact(`Data-source survey — **${dataSourceResult.
|
|
9832
|
+
if (dataSourceResult.section && dataSourceResult.sdkImportCount > 0) {
|
|
9833
|
+
return finishCompact(`Data-source survey — **${dataSourceResult.sdkImportCount}** \`@ohos\`/\`@kit\` import(s). **ANSWER NOW.**`);
|
|
9834
|
+
}
|
|
9835
|
+
if (dataSourceResult.section && dataSourceResult.strongCount > 0) {
|
|
9836
|
+
return finishCompact(`Data-source survey — **${dataSourceResult.strongCount}** related writer/sibling(s). **Partial locator** — drill next.`);
|
|
7539
9837
|
}
|
|
7540
9838
|
if (inheritanceListed) {
|
|
7541
9839
|
return finishCompact('Inheritance survey above lists all direct subtypes found. **ANSWER NOW** — do not grep `extends`.');
|
|
@@ -7645,15 +9943,6 @@ class ToolHandler {
|
|
|
7645
9943
|
}
|
|
7646
9944
|
return false;
|
|
7647
9945
|
};
|
|
7648
|
-
lines.push('**Source Code**');
|
|
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;
|
|
7653
|
-
lines.push('> Line-numbered source — treat as already Read. Answer from Flow + Source below when you can; ' +
|
|
7654
|
-
'do not re-read/grep these files, and do not fan out `homegraph_node` / search for symbols already shown. ' +
|
|
7655
|
-
'Another `homegraph_explore` with tighter names only if a needed symbol is missing.');
|
|
7656
|
-
lines.push('');
|
|
7657
9946
|
let totalChars = lines.join('\n').length;
|
|
7658
9947
|
let filesIncluded = 0;
|
|
7659
9948
|
// Paths we actually render source for below. Drives the curated header count
|
|
@@ -7712,9 +10001,19 @@ class ToolHandler {
|
|
|
7712
10001
|
if (onSpine.length > 0)
|
|
7713
10002
|
filesToRender = onSpine;
|
|
7714
10003
|
}
|
|
7715
|
-
const sourceFileCap =
|
|
7716
|
-
|
|
7717
|
-
|
|
10004
|
+
const sourceFileCap = (() => {
|
|
10005
|
+
const digest = exploreLocatorDigestEnabled();
|
|
10006
|
+
if (limitSingleFile && filesToRender.length === 1)
|
|
10007
|
+
return 1;
|
|
10008
|
+
if (digest) {
|
|
10009
|
+
// Locator digest: at most 2 body digests (spine/named); rest stay as path anchors.
|
|
10010
|
+
if (localDetail)
|
|
10011
|
+
return Math.min(maxFiles, 2);
|
|
10012
|
+
if (hasFlowPath)
|
|
10013
|
+
return Math.min(maxFiles, 2);
|
|
10014
|
+
return Math.min(maxFiles, 2);
|
|
10015
|
+
}
|
|
10016
|
+
return localDetail
|
|
7718
10017
|
? Math.min(maxFiles, 2)
|
|
7719
10018
|
: crossModuleFlow && hasFlowPath
|
|
7720
10019
|
? Math.min(maxFiles, 5)
|
|
@@ -7725,6 +10024,38 @@ class ToolHandler {
|
|
|
7725
10024
|
: !hasFlowPath
|
|
7726
10025
|
? Math.min(maxFiles, 3)
|
|
7727
10026
|
: maxFiles;
|
|
10027
|
+
})();
|
|
10028
|
+
// Path-only anchors for files we will not dump as Source (digest mode).
|
|
10029
|
+
if (exploreLocatorDigestEnabled()) {
|
|
10030
|
+
const anchorCap = Math.min(filesToRender.length, 10);
|
|
10031
|
+
lines.push('**Anchors**', '');
|
|
10032
|
+
for (const [fp, group] of filesToRender.slice(0, anchorCap)) {
|
|
10033
|
+
const names = group.nodes
|
|
10034
|
+
.filter((n) => n.kind !== 'import' && n.kind !== 'export')
|
|
10035
|
+
.slice(0, 4)
|
|
10036
|
+
.map((n) => `\`${n.name}\`(${n.kind}${n.startLine ? `:${n.startLine}` : ''})`)
|
|
10037
|
+
.join(', ');
|
|
10038
|
+
lines.push(`- \`${fp}\`${names ? ` — ${names}` : ''}`);
|
|
10039
|
+
}
|
|
10040
|
+
if (filesToRender.length > sourceFileCap) {
|
|
10041
|
+
lines.push('');
|
|
10042
|
+
lines.push(`> Body digests only for the top **${sourceFileCap}** file(s) below; remaining anchors are path-only.`);
|
|
10043
|
+
}
|
|
10044
|
+
lines.push('');
|
|
10045
|
+
}
|
|
10046
|
+
lines.push(exploreLocatorDigestEnabled() ? '**Source digests**' : '**Source Code**');
|
|
10047
|
+
lines.push('');
|
|
10048
|
+
// Recorded so the drift pass below (#1474) can append a per-file exception
|
|
10049
|
+
// to this guarantee after the render loop knows which files drifted.
|
|
10050
|
+
const verbatimHeaderIdx = lines.length;
|
|
10051
|
+
lines.push(exploreLocatorDigestEnabled()
|
|
10052
|
+
? '> Spine / top-hit digests — treat as already Read. '
|
|
10053
|
+
+ 'Answer from Flow + Anchors + digests when you can; ONE narrow Grep for residual unindexed wiring. '
|
|
10054
|
+
+ 'Do not re-dump these files via Read/`homegraph_node`.'
|
|
10055
|
+
: '> Line-numbered source — treat as already Read. Answer from Flow + Source below when you can; ' +
|
|
10056
|
+
'do not re-read/grep these files, and do not fan out `homegraph_node` / search for symbols already shown. ' +
|
|
10057
|
+
'Another `homegraph_explore` with tighter names only if a needed symbol is missing.');
|
|
10058
|
+
lines.push('');
|
|
7728
10059
|
for (const [filePath, group] of filesToRender) {
|
|
7729
10060
|
if (filesIncluded >= sourceFileCap)
|
|
7730
10061
|
break;
|
|
@@ -8183,7 +10514,7 @@ class ToolHandler {
|
|
|
8183
10514
|
// token cost on small projects. We pick clusters in priority order
|
|
8184
10515
|
// until the per-file char cap is hit. Truly enormous single clusters
|
|
8185
10516
|
// get tail-trimmed with a marker.
|
|
8186
|
-
const contextPadding = 3;
|
|
10517
|
+
const contextPadding = exploreLocatorDigestEnabled() ? 2 : 3;
|
|
8187
10518
|
// Language-neutral separator (no `//` — not a comment in Python, Ruby,
|
|
8188
10519
|
// etc.). With line numbers on, the line-number jump also signals the gap.
|
|
8189
10520
|
const GAP_MARKER = '\n\n... (gap) ...\n\n';
|
|
@@ -8193,10 +10524,11 @@ class ToolHandler {
|
|
|
8193
10524
|
// for any per-file cap and gets dropped, so the agent Reads the method back —
|
|
8194
10525
|
// the exact gap this closes. Bounded, so a god-method can't blow the budget yet
|
|
8195
10526
|
// the spine's call still appears in context.
|
|
8196
|
-
const
|
|
8197
|
-
const
|
|
8198
|
-
const
|
|
8199
|
-
const
|
|
10527
|
+
const digest = exploreLocatorDigestEnabled();
|
|
10528
|
+
const OVERSIZE_SPINE_LINES = digest ? 120 : 200;
|
|
10529
|
+
const OVERSIZE_BODY_LINES = digest ? 100 : 160;
|
|
10530
|
+
const SPINE_WINDOW = digest ? 18 : 28; // lines each side of the next-hop call site
|
|
10531
|
+
const BODY_WINDOW = digest ? 24 : 40;
|
|
8200
10532
|
const buildSectionRange = (c) => {
|
|
8201
10533
|
const span = c.end - c.start + 1;
|
|
8202
10534
|
if (c.hasSpine && c.spineCallLine && span > OVERSIZE_SPINE_LINES) {
|
|
@@ -8268,7 +10600,7 @@ class ToolHandler {
|
|
|
8268
10600
|
// path is the answer), but bounded — at most ~2.5× the per-file cap and never
|
|
8269
10601
|
// past what's left of the total output cap — so a pathological long in-file
|
|
8270
10602
|
// spine can't run away or starve co-flow files entirely.
|
|
8271
|
-
const SPINE_CEILING = Math.min(budget.maxCharsPerFile * 2.5, remainingEnvelope);
|
|
10603
|
+
const SPINE_CEILING = Math.min(budget.maxCharsPerFile * (exploreLocatorDigestEnabled() ? 1.5 : 2.5), remainingEnvelope);
|
|
8272
10604
|
const chosenIndices = new Set();
|
|
8273
10605
|
let projectedChars = 0;
|
|
8274
10606
|
for (const rc of rankedClusters) {
|
|
@@ -8554,8 +10886,39 @@ class ToolHandler {
|
|
|
8554
10886
|
* calling session's state and deletes it — see {@link EXPLORE_EMISSION_KEY}.
|
|
8555
10887
|
*/
|
|
8556
10888
|
exploreResult(text, emission) {
|
|
10889
|
+
const meta = (0, explore_repeat_guard_1.inferExplorePartialMeta)(text);
|
|
8557
10890
|
const result = this.textResult(text);
|
|
8558
|
-
result[explore_session_state_1.EXPLORE_EMISSION_KEY] =
|
|
10891
|
+
result[explore_session_state_1.EXPLORE_EMISSION_KEY] = {
|
|
10892
|
+
...emission,
|
|
10893
|
+
partial: emission.partial ?? meta.partial,
|
|
10894
|
+
nextAnchor: emission.nextAnchor ?? meta.nextAnchor,
|
|
10895
|
+
};
|
|
10896
|
+
return result;
|
|
10897
|
+
}
|
|
10898
|
+
/** Attach a minimal session emission when a compact/light path used textResult. */
|
|
10899
|
+
ensureExploreEmission(result, projectRoot, query) {
|
|
10900
|
+
if (result[explore_session_state_1.EXPLORE_EMISSION_KEY]) {
|
|
10901
|
+
const em = result[explore_session_state_1.EXPLORE_EMISSION_KEY];
|
|
10902
|
+
if (em.partial === undefined || !em.nextAnchor) {
|
|
10903
|
+
const meta = (0, explore_repeat_guard_1.inferExplorePartialMeta)(result.content?.[0]?.text ?? '');
|
|
10904
|
+
if (em.partial === undefined)
|
|
10905
|
+
em.partial = meta.partial;
|
|
10906
|
+
if (!em.nextAnchor && meta.nextAnchor)
|
|
10907
|
+
em.nextAnchor = meta.nextAnchor;
|
|
10908
|
+
}
|
|
10909
|
+
return result;
|
|
10910
|
+
}
|
|
10911
|
+
const text = result.content?.[0]?.text ?? '';
|
|
10912
|
+
const meta = (0, explore_repeat_guard_1.inferExplorePartialMeta)(text);
|
|
10913
|
+
result[explore_session_state_1.EXPLORE_EMISSION_KEY] = {
|
|
10914
|
+
projectRoot,
|
|
10915
|
+
query,
|
|
10916
|
+
files: [],
|
|
10917
|
+
sourceBytes: 0,
|
|
10918
|
+
responseBytes: text.length,
|
|
10919
|
+
partial: meta.partial,
|
|
10920
|
+
nextAnchor: meta.nextAnchor,
|
|
10921
|
+
};
|
|
8559
10922
|
return result;
|
|
8560
10923
|
}
|
|
8561
10924
|
/**
|
|
@@ -9013,6 +11376,10 @@ class ToolHandler {
|
|
|
9013
11376
|
lines.push(`- ${p.path} (edited ${ageMs}ms ago, ${label})`);
|
|
9014
11377
|
}
|
|
9015
11378
|
}
|
|
11379
|
+
const updateNotice = (0, update_check_1.getUpdateNotice)();
|
|
11380
|
+
if (updateNotice) {
|
|
11381
|
+
lines.push('', '**Update available:**', updateNotice);
|
|
11382
|
+
}
|
|
9016
11383
|
return this.textResult(lines.join('\n'));
|
|
9017
11384
|
}
|
|
9018
11385
|
/**
|
|
@@ -9686,7 +12053,11 @@ class ToolHandler {
|
|
|
9686
12053
|
const tail = lastQualifierPart(symbol);
|
|
9687
12054
|
if (tail && tail !== symbol) {
|
|
9688
12055
|
try {
|
|
9689
|
-
|
|
12056
|
+
// Qualified miss: re-probe by bare member, but ONLY keep rows that
|
|
12057
|
+
// still match the FULL qualifier. Never `matchesSymbol(n, tail)` —
|
|
12058
|
+
// that is a bare-name match and turns `Foo.init` into every `init`
|
|
12059
|
+
// in the monorepo (hundreds of definitions → token bomb).
|
|
12060
|
+
pool = cg.getNodesByName(tail).filter((n) => this.matchesSymbol(n, symbol));
|
|
9690
12061
|
}
|
|
9691
12062
|
catch {
|
|
9692
12063
|
pool = [];
|
|
@@ -9896,7 +12267,9 @@ class ToolHandler {
|
|
|
9896
12267
|
}
|
|
9897
12268
|
textResult(text) {
|
|
9898
12269
|
return {
|
|
9899
|
-
|
|
12270
|
+
// Single choke point for every tool's text, so no section builder can ship
|
|
12271
|
+
// a stop-searching directive on an output that declares itself partial.
|
|
12272
|
+
content: [{ type: 'text', text: reconcilePartialAnswerNow(text) }],
|
|
9900
12273
|
};
|
|
9901
12274
|
}
|
|
9902
12275
|
/**
|