homegraph 1.5.3 → 1.5.4
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 +26 -1
- package/README.md +51 -5
- 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-snapshot.d.ts.map +1 -1
- package/dist/arkui/migrate-snapshot.js +58 -34
- 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/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 +503 -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/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 +36 -101
- package/dist/mcp/server-instructions.js.map +1 -1
- package/dist/mcp/tools.d.ts +33 -2
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +1403 -266
- 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 +47 -7
- package/dist/search/query-utils.d.ts.map +1 -1
- package/dist/search/query-utils.js +335 -109
- 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/package.json +6 -6
package/dist/mcp/tools.js
CHANGED
|
@@ -11,6 +11,7 @@ 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;
|
|
@@ -43,6 +44,7 @@ const generated_detection_1 = require("../extraction/generated-detection");
|
|
|
43
44
|
const arkts_1 = require("../extraction/languages/arkts");
|
|
44
45
|
const explore_session_state_1 = require("./explore-session-state");
|
|
45
46
|
const explore_dedup_1 = require("./explore-dedup");
|
|
47
|
+
const explore_repeat_guard_1 = require("./explore-repeat-guard");
|
|
46
48
|
const dynamic_boundaries_1 = require("./dynamic-boundaries");
|
|
47
49
|
const query_cache_1 = require("./query-cache");
|
|
48
50
|
const diff_impact_1 = require("./diff-impact");
|
|
@@ -278,25 +280,19 @@ function getExploreBudget(fileCount) {
|
|
|
278
280
|
function getExploreOutputBudget(fileCount) {
|
|
279
281
|
// Tiered budget, scaled to project size. The budget is a CEILING (relevance
|
|
280
282
|
// 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.
|
|
283
|
+
// tool-result cap (~25K chars).
|
|
284
|
+
//
|
|
285
|
+
// Default = universal mid-lean locator (anchors + spine digests ~12K): enough
|
|
286
|
+
// evidence to stop a Read storm, not a multi-file body dump. Set
|
|
287
|
+
// HOMEGRAPH_EXPLORE_FULL_SOURCE=1 for the prior fatter shape.
|
|
288
|
+
// Invariant: a larger tier must never get a smaller `maxCharsPerFile` than a
|
|
289
|
+
// smaller tier.
|
|
290
|
+
const digest = exploreLocatorDigestEnabled();
|
|
290
291
|
if (fileCount < 150) {
|
|
291
292
|
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,
|
|
293
|
+
maxOutputChars: digest ? 10000 : 13000,
|
|
294
|
+
defaultMaxFiles: digest ? 2 : 4,
|
|
295
|
+
maxCharsPerFile: digest ? 2500 : 3800,
|
|
300
296
|
gapThreshold: 7,
|
|
301
297
|
maxSymbolsInFileHeader: 5,
|
|
302
298
|
maxEdgesPerRelationshipKind: 4,
|
|
@@ -309,10 +305,9 @@ function getExploreOutputBudget(fileCount) {
|
|
|
309
305
|
}
|
|
310
306
|
if (fileCount < 500) {
|
|
311
307
|
return {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
maxCharsPerFile: 3800,
|
|
308
|
+
maxOutputChars: digest ? 12000 : 18000,
|
|
309
|
+
defaultMaxFiles: digest ? 2 : 5,
|
|
310
|
+
maxCharsPerFile: digest ? 2800 : 3800,
|
|
316
311
|
gapThreshold: 8,
|
|
317
312
|
maxSymbolsInFileHeader: 6,
|
|
318
313
|
maxEdgesPerRelationshipKind: 6,
|
|
@@ -325,53 +320,48 @@ function getExploreOutputBudget(fileCount) {
|
|
|
325
320
|
}
|
|
326
321
|
if (fileCount < 5000) {
|
|
327
322
|
return {
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
includeBudgetNote: true,
|
|
341
|
-
excludeLowValueFiles: false,
|
|
323
|
+
// Mid-lean: anchors + 1–2 spine digests. Per-file ≥ smaller tiers.
|
|
324
|
+
maxOutputChars: digest ? 12000 : 18000,
|
|
325
|
+
defaultMaxFiles: digest ? 2 : 6,
|
|
326
|
+
maxCharsPerFile: digest ? 3000 : 6500,
|
|
327
|
+
gapThreshold: digest ? 10 : 12,
|
|
328
|
+
maxSymbolsInFileHeader: digest ? 8 : 10,
|
|
329
|
+
maxEdgesPerRelationshipKind: digest ? 6 : 10,
|
|
330
|
+
includeRelationships: false,
|
|
331
|
+
includeAdditionalFiles: false,
|
|
332
|
+
includeCompletenessSignal: false,
|
|
333
|
+
includeBudgetNote: false,
|
|
334
|
+
excludeLowValueFiles: true,
|
|
342
335
|
};
|
|
343
336
|
}
|
|
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.
|
|
337
|
+
// Large + very-large: same mid-lean ceiling; scale via getExploreBudget calls.
|
|
348
338
|
if (fileCount < 15000) {
|
|
349
339
|
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:
|
|
340
|
+
maxOutputChars: digest ? 12000 : 18000,
|
|
341
|
+
defaultMaxFiles: digest ? 2 : 6,
|
|
342
|
+
maxCharsPerFile: digest ? 3000 : 7000,
|
|
343
|
+
gapThreshold: digest ? 10 : 15,
|
|
344
|
+
maxSymbolsInFileHeader: digest ? 8 : 12,
|
|
345
|
+
maxEdgesPerRelationshipKind: digest ? 6 : 12,
|
|
346
|
+
includeRelationships: false,
|
|
347
|
+
includeAdditionalFiles: false,
|
|
348
|
+
includeCompletenessSignal: false,
|
|
349
|
+
includeBudgetNote: false,
|
|
350
|
+
excludeLowValueFiles: true,
|
|
361
351
|
};
|
|
362
352
|
}
|
|
363
353
|
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:
|
|
354
|
+
maxOutputChars: digest ? 12000 : 18000,
|
|
355
|
+
defaultMaxFiles: digest ? 2 : 6,
|
|
356
|
+
maxCharsPerFile: digest ? 3000 : 7000,
|
|
357
|
+
gapThreshold: digest ? 10 : 15,
|
|
358
|
+
maxSymbolsInFileHeader: digest ? 8 : 12,
|
|
359
|
+
maxEdgesPerRelationshipKind: digest ? 6 : 12,
|
|
360
|
+
includeRelationships: false,
|
|
361
|
+
includeAdditionalFiles: false,
|
|
362
|
+
includeCompletenessSignal: false,
|
|
363
|
+
includeBudgetNote: false,
|
|
364
|
+
excludeLowValueFiles: true,
|
|
375
365
|
};
|
|
376
366
|
}
|
|
377
367
|
/**
|
|
@@ -384,13 +374,15 @@ function getExploreOutputBudget(fileCount) {
|
|
|
384
374
|
* implemented" questions even when the first answer was already enough.
|
|
385
375
|
*/
|
|
386
376
|
function tightenExploreBudgetForQuery(budget, query, opts) {
|
|
377
|
+
const digest = exploreLocatorDigestEnabled();
|
|
387
378
|
const hasFlow = opts?.hasFlowPath === true;
|
|
388
379
|
if (hasFlow && ((0, query_utils_1.queryAsMechanismSurvey)(query) || (0, query_utils_1.queryAsCrossModuleFlowSurvey)(query))) {
|
|
389
380
|
return {
|
|
390
381
|
...budget,
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
382
|
+
// Mid-lean: keep enough spine to avoid a Read storm after explore.
|
|
383
|
+
maxOutputChars: Math.min(budget.maxOutputChars, digest ? 11000 : 12000),
|
|
384
|
+
defaultMaxFiles: Math.min(budget.defaultMaxFiles, 2),
|
|
385
|
+
maxCharsPerFile: Math.min(budget.maxCharsPerFile, digest ? 3000 : 4500),
|
|
394
386
|
includeRelationships: false,
|
|
395
387
|
includeAdditionalFiles: false,
|
|
396
388
|
includeCompletenessSignal: false,
|
|
@@ -402,12 +394,11 @@ function tightenExploreBudgetForQuery(budget, query, opts) {
|
|
|
402
394
|
if (!local && !compact && hasFlow)
|
|
403
395
|
return budget;
|
|
404
396
|
if (!local && !compact && hasFlow === false) {
|
|
405
|
-
// Generic no-flow explore: still trim meta + file count a bit.
|
|
406
397
|
return {
|
|
407
398
|
...budget,
|
|
408
|
-
maxOutputChars: Math.min(budget.maxOutputChars,
|
|
409
|
-
defaultMaxFiles: Math.min(budget.defaultMaxFiles,
|
|
410
|
-
maxCharsPerFile: Math.min(budget.maxCharsPerFile,
|
|
399
|
+
maxOutputChars: Math.min(budget.maxOutputChars, digest ? 10000 : 10000),
|
|
400
|
+
defaultMaxFiles: Math.min(budget.defaultMaxFiles, 2),
|
|
401
|
+
maxCharsPerFile: Math.min(budget.maxCharsPerFile, digest ? 2800 : 4000),
|
|
411
402
|
includeRelationships: false,
|
|
412
403
|
includeAdditionalFiles: false,
|
|
413
404
|
includeCompletenessSignal: false,
|
|
@@ -421,9 +412,9 @@ function tightenExploreBudgetForQuery(budget, query, opts) {
|
|
|
421
412
|
|| (0, query_utils_1.queryAsTypeLifecycleSurvey)(query);
|
|
422
413
|
return {
|
|
423
414
|
...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),
|
|
415
|
+
maxOutputChars: Math.min(budget.maxOutputChars, lean ? (digest ? 5000 : 4800) : local ? (digest ? 6000 : 7000) : (digest ? 8000 : 9000)),
|
|
416
|
+
defaultMaxFiles: Math.min(budget.defaultMaxFiles, 2),
|
|
417
|
+
maxCharsPerFile: Math.min(budget.maxCharsPerFile, lean ? (digest ? 2200 : 2200) : local ? (digest ? 2800 : 3500) : (digest ? 3000 : 4000)),
|
|
427
418
|
includeRelationships: false,
|
|
428
419
|
includeAdditionalFiles: false,
|
|
429
420
|
includeCompletenessSignal: false,
|
|
@@ -444,6 +435,14 @@ function tightenExploreBudgetForQuery(budget, query, opts) {
|
|
|
444
435
|
function exploreLineNumbersEnabled() {
|
|
445
436
|
return process.env.HOMEGRAPH_EXPLORE_LINENUMS !== '0';
|
|
446
437
|
}
|
|
438
|
+
/**
|
|
439
|
+
* Locator mid-lean mode (default ON): explore returns anchors + spine digests
|
|
440
|
+
* (~12K) — enough to avoid a Read storm, not a multi-file body dump.
|
|
441
|
+
* Set `HOMEGRAPH_EXPLORE_FULL_SOURCE=1` to restore the previous fatter Source.
|
|
442
|
+
*/
|
|
443
|
+
function exploreLocatorDigestEnabled() {
|
|
444
|
+
return process.env.HOMEGRAPH_EXPLORE_FULL_SOURCE !== '1';
|
|
445
|
+
}
|
|
447
446
|
/**
|
|
448
447
|
* Adaptive explore sizing (default ON). `homegraph_explore` skeletonizes OFF-SPINE
|
|
449
448
|
* polymorphic-sibling files — a file whose class is one of ≥3 interchangeable
|
|
@@ -840,21 +839,28 @@ exports.tools = [
|
|
|
840
839
|
},
|
|
841
840
|
{
|
|
842
841
|
name: 'homegraph_explore',
|
|
843
|
-
description: '
|
|
844
|
-
'
|
|
845
|
-
'
|
|
846
|
-
'
|
|
847
|
-
'
|
|
848
|
-
'
|
|
849
|
-
'
|
|
850
|
-
'
|
|
851
|
-
'
|
|
842
|
+
description: 'PRIMARY entry for understanding THIS repo before you edit or answer structural questions. ' +
|
|
843
|
+
'Returns call paths + compact line-numbered source for the relevant symbols. Required: `query`. ' +
|
|
844
|
+
'CALL FIRST (alone, no parallel Grep/Read) when you will change an existing codebase — pass the user task or domain keywords ' +
|
|
845
|
+
'(page/module/feature/component words); locate where to edit before writing code. Also CALL FIRST for how/wired questions, ' +
|
|
846
|
+
'named Type/Component/Page/Dialog, Type.member, click→handler, inheritance/subtypes, declaration/attribute sites, ' +
|
|
847
|
+
'ALL_CAPS constant / field-mutex usages, path-module NAPI/exports or inter-deps, and in-repo @kit/@ohos *usages/dependencies* ' +
|
|
848
|
+
'(which files import a named export — not the SDK feature catalog). PascalCase names optional when domain keywords suffice. ' +
|
|
849
|
+
'Prefer callers/node when one named symbol is already enough. ' +
|
|
850
|
+
'DO NOT call for topic file-lists with no Type/file, literal copy hunts / pure existence compares with no anchors, ' +
|
|
851
|
+
'official-docs-only asks, empty-project-from-scratch scaffolds, git history, or media/binary asset inventories — those return Skip. ' +
|
|
852
|
+
'@kit / OHOS API questions ARE in scope when the SDK API graph is available — call explore (usages or API symbols). ' +
|
|
853
|
+
'Literal string/pattern hunts → Grep; media assets → Glob; git history → git. ' +
|
|
854
|
+
'One explore; then answer or edit from Source + trail — do not re-grep/node/read the same symbols. ' +
|
|
855
|
+
'Overlapping paraphrase explores are refused (name a new Type/file/@kit to continue). ' +
|
|
856
|
+
'Busy/partial → retry ONCE with the named Next anchor or ONE narrow Grep — not a Grep/node storm '
|
|
857
|
+
+ '(session refuses further explore / depth fan-out after Partial).',
|
|
852
858
|
inputSchema: {
|
|
853
859
|
type: 'object',
|
|
854
860
|
properties: {
|
|
855
861
|
query: {
|
|
856
862
|
type: 'string',
|
|
857
|
-
description: 'Required.
|
|
863
|
+
description: 'Required. For pre-edit orientation or how/mechanism: pass the user task or domain keywords (page/module/feature words). ' +
|
|
858
864
|
'For named flows, include Type / Type.member / component names. For @kit, ask usages (depend/import sites), not SDK catalogs.',
|
|
859
865
|
},
|
|
860
866
|
maxFiles: {
|
|
@@ -1706,6 +1712,16 @@ class ToolHandler {
|
|
|
1706
1712
|
}
|
|
1707
1713
|
}
|
|
1708
1714
|
}
|
|
1715
|
+
// Post-Partial depth fuse: Partial→node×N is the same failure class as
|
|
1716
|
+
// explore×N. Shape-driven only — one depth call, then refuse.
|
|
1717
|
+
if (sessionState
|
|
1718
|
+
&& (toolName === 'homegraph_node'
|
|
1719
|
+
|| toolName === 'homegraph_callers'
|
|
1720
|
+
|| toolName === 'homegraph_callees')) {
|
|
1721
|
+
const depthRefuse = this.tryDepthToolFuse(toolName, args, sessionState);
|
|
1722
|
+
if (depthRefuse)
|
|
1723
|
+
return depthRefuse;
|
|
1724
|
+
}
|
|
1709
1725
|
// Session-tracked explore must not hit the MCP query cache: a cache hit
|
|
1710
1726
|
// would re-serve the first call's full source and defeat CG-18 dedup.
|
|
1711
1727
|
const skipCacheForSession = toolName === 'homegraph_explore' && !!sessionState;
|
|
@@ -1764,10 +1780,16 @@ class ToolHandler {
|
|
|
1764
1780
|
: null)
|
|
1765
1781
|
?? this.tryCompactLocalSymbolExplore(cgFast, q, rootFast);
|
|
1766
1782
|
if (fast) {
|
|
1767
|
-
|
|
1768
|
-
|
|
1783
|
+
// Fast explore must still file Partial/ANSWER meta into the session
|
|
1784
|
+
// so explore + depth fuses see it (textResult-only paths used to skip).
|
|
1785
|
+
let served = fast;
|
|
1786
|
+
if (toolName === 'homegraph_explore' && sessionState) {
|
|
1787
|
+
served = this.takeExploreEmission(this.ensureExploreEmission(fast, rootFast, q), sessionState);
|
|
1788
|
+
}
|
|
1789
|
+
if (cacheEnabled && cacheKey && cacheQueries && cacheIndex && !served.isError) {
|
|
1790
|
+
cacheIndex.setEntry(cacheQueries, cacheKey, toolName, served);
|
|
1769
1791
|
}
|
|
1770
|
-
const withWorktree = this.withWorktreeNotice(
|
|
1792
|
+
const withWorktree = this.withWorktreeNotice(served, projectPath);
|
|
1771
1793
|
return this.withStalenessNotice(withWorktree, projectPath);
|
|
1772
1794
|
}
|
|
1773
1795
|
}
|
|
@@ -1786,6 +1808,13 @@ class ToolHandler {
|
|
|
1786
1808
|
// `-32001`). Fast-path surveys run inside the worker via executeReadTool.
|
|
1787
1809
|
const raw = await this.runReadToolWithDeadline(toolName, dispatchArgs);
|
|
1788
1810
|
const result = this.takeExploreEmission(raw, sessionState);
|
|
1811
|
+
if (sessionState
|
|
1812
|
+
&& !result.isError
|
|
1813
|
+
&& (toolName === 'homegraph_node'
|
|
1814
|
+
|| toolName === 'homegraph_callers'
|
|
1815
|
+
|| toolName === 'homegraph_callees')) {
|
|
1816
|
+
this.noteDepthToolUse(args, sessionState);
|
|
1817
|
+
}
|
|
1789
1818
|
if (cacheEnabled && cacheKey && cacheQueries && cacheIndex && !result.isError) {
|
|
1790
1819
|
cacheIndex.setEntry(cacheQueries, cacheKey, toolName, result);
|
|
1791
1820
|
}
|
|
@@ -1832,6 +1861,43 @@ class ToolHandler {
|
|
|
1832
1861
|
}
|
|
1833
1862
|
return copy;
|
|
1834
1863
|
}
|
|
1864
|
+
/**
|
|
1865
|
+
* After a counted Partial explore, refuse further `node` / callers / callees
|
|
1866
|
+
* once the session's one allowed depth drill is spent.
|
|
1867
|
+
*/
|
|
1868
|
+
tryDepthToolFuse(toolName, args, sessionState) {
|
|
1869
|
+
try {
|
|
1870
|
+
const cg = this.getHomeGraph(args.projectPath);
|
|
1871
|
+
const root = cg.getProjectRoot();
|
|
1872
|
+
const decision = (0, explore_repeat_guard_1.decideDepthToolFuse)(sessionState.forProject(root), sessionState.depthToolCount(root), toolName);
|
|
1873
|
+
if (!decision.refuse)
|
|
1874
|
+
return null;
|
|
1875
|
+
const hint = typeof args.symbol === 'string'
|
|
1876
|
+
? args.symbol
|
|
1877
|
+
: typeof args.file === 'string'
|
|
1878
|
+
? args.file
|
|
1879
|
+
: undefined;
|
|
1880
|
+
return this.textResult((0, explore_repeat_guard_1.formatDepthToolRefuse)(decision, toolName, hint));
|
|
1881
|
+
}
|
|
1882
|
+
catch {
|
|
1883
|
+
return null;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
/** Count a successful depth-tool call when the latest explore was Partial. */
|
|
1887
|
+
noteDepthToolUse(args, sessionState) {
|
|
1888
|
+
try {
|
|
1889
|
+
const cg = this.getHomeGraph(args.projectPath);
|
|
1890
|
+
const root = cg.getProjectRoot();
|
|
1891
|
+
const prior = sessionState.forProject(root);
|
|
1892
|
+
const last = prior?.calls.length
|
|
1893
|
+
? [...prior.calls].reverse().find((c) => (c.responseBytes || 0) >= 400)
|
|
1894
|
+
: undefined;
|
|
1895
|
+
if (!last?.partial)
|
|
1896
|
+
return;
|
|
1897
|
+
sessionState.recordDepthTool(root);
|
|
1898
|
+
}
|
|
1899
|
+
catch { /* bookkeeping only */ }
|
|
1900
|
+
}
|
|
1835
1901
|
/**
|
|
1836
1902
|
* Record an explore call's emission into the caller's session state and strip
|
|
1837
1903
|
* it from the result (CG-17).
|
|
@@ -1849,6 +1915,25 @@ class ToolHandler {
|
|
|
1849
1915
|
delete result[explore_session_state_1.EXPLORE_EMISSION_KEY];
|
|
1850
1916
|
if (sessionState) {
|
|
1851
1917
|
try {
|
|
1918
|
+
const prior = sessionState.forProject(emission.projectRoot);
|
|
1919
|
+
const priorPartials = (prior?.calls ?? []).filter((c) => c.partial === true && (c.responseBytes || 0) >= 400);
|
|
1920
|
+
const metaPartial = emission.partial === true
|
|
1921
|
+
|| (emission.partial === undefined
|
|
1922
|
+
&& (0, explore_repeat_guard_1.inferExplorePartialMeta)(result.content?.[0]?.text ?? '').partial);
|
|
1923
|
+
if (metaPartial && priorPartials.length >= 1) {
|
|
1924
|
+
const text = result.content?.[0]?.text ?? '';
|
|
1925
|
+
if (text && !/Second Partial — stop HomeGraph/i.test(text)) {
|
|
1926
|
+
const stop = (0, explore_repeat_guard_1.formatSecondPartialStopFooter)(emission.nextAnchor);
|
|
1927
|
+
result = {
|
|
1928
|
+
...result,
|
|
1929
|
+
content: [
|
|
1930
|
+
{ type: 'text', text: text + stop },
|
|
1931
|
+
...result.content.slice(1),
|
|
1932
|
+
],
|
|
1933
|
+
};
|
|
1934
|
+
emission.responseBytes = (result.content[0]?.text ?? text).length;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1852
1937
|
sessionState.record(emission);
|
|
1853
1938
|
}
|
|
1854
1939
|
catch { /* bookkeeping only — never fail a served call */ }
|
|
@@ -1881,7 +1966,7 @@ class ToolHandler {
|
|
|
1881
1966
|
return this.queryPool.run(toolName, args, {
|
|
1882
1967
|
// Static Partial only — never DB/FTS on the soft-timeout callback
|
|
1883
1968
|
// (any sync work here can freeze the MCP transport → empty -32001).
|
|
1884
|
-
onSoftTimeout: () => this.deadlineBusyResult(deadlineMs),
|
|
1969
|
+
onSoftTimeout: () => this.deadlineBusyResult(deadlineMs, args),
|
|
1885
1970
|
});
|
|
1886
1971
|
}
|
|
1887
1972
|
return (async () => {
|
|
@@ -1894,7 +1979,7 @@ class ToolHandler {
|
|
|
1894
1979
|
// Keep this timer ref'd so a blocked sync stretch still flushes Partial
|
|
1895
1980
|
// once the event loop runs again — do not call wrapPartialBusyResult here.
|
|
1896
1981
|
timer = setTimeout(() => {
|
|
1897
|
-
resolve(this.deadlineBusyResult(deadlineMs));
|
|
1982
|
+
resolve(this.deadlineBusyResult(deadlineMs, args));
|
|
1898
1983
|
}, deadlineMs);
|
|
1899
1984
|
});
|
|
1900
1985
|
try {
|
|
@@ -1905,13 +1990,15 @@ class ToolHandler {
|
|
|
1905
1990
|
clearTimeout(timer);
|
|
1906
1991
|
}
|
|
1907
1992
|
}
|
|
1908
|
-
/** Success-shaped busy note —
|
|
1909
|
-
deadlineBusyResult(deadlineMs) {
|
|
1993
|
+
/** Success-shaped busy note — anchors from query when present; no DB. */
|
|
1994
|
+
deadlineBusyResult(deadlineMs, args) {
|
|
1910
1995
|
const secs = Math.max(1, Math.round(deadlineMs / 1000));
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1996
|
+
const query = typeof args?.query === 'string' ? args.query : undefined;
|
|
1997
|
+
return this.textResult((0, explore_repeat_guard_1.formatPartialExploreGuidance)({
|
|
1998
|
+
seconds: secs,
|
|
1999
|
+
query,
|
|
2000
|
+
why: `HomeGraph hit its ${secs}s response deadline / is busy (MCP clients typically kill the call at ~60s with an empty timeout)`,
|
|
2001
|
+
}));
|
|
1915
2002
|
}
|
|
1916
2003
|
/**
|
|
1917
2004
|
* Run a single read tool to completion and return its raw {@link ToolResult},
|
|
@@ -2130,7 +2217,8 @@ class ToolHandler {
|
|
|
2130
2217
|
+ includeBlock
|
|
2131
2218
|
+ '\n\n> Caller listing complete — answer from this list'
|
|
2132
2219
|
+ (includeBlock ? ' + include/import visibility' : '')
|
|
2133
|
-
+ ';
|
|
2220
|
+
+ '; do not re-Grep/Read these call sites.'
|
|
2221
|
+
+ '\n> For **all methods of a Type**, prefer one `homegraph_explore` with the Type + “哪些方法被调用” — do **not** fan out `homegraph_callers` per method.'
|
|
2134
2222
|
+ '\n> If another type\'s visibility matters, pass **both** names to `homegraph_explore` (not callers alone).'
|
|
2135
2223
|
+ note + filterNote;
|
|
2136
2224
|
return this.textResult(this.truncateOutput(formatted));
|
|
@@ -2138,23 +2226,50 @@ class ToolHandler {
|
|
|
2138
2226
|
// Multiple DISTINCT definitions (#764): one section per definition so an
|
|
2139
2227
|
// agent never mistakes one app's callers for another's. Narrow with
|
|
2140
2228
|
// `file` to focus a single definition.
|
|
2229
|
+
// Hard stop when the name is still massively ambiguous — dumping dozens of
|
|
2230
|
+
// definition sections is a token bomb and teaches a Grep/Read storm.
|
|
2231
|
+
const MAX_CALLER_DEF_GROUPS = 8;
|
|
2232
|
+
if (groups.length > MAX_CALLER_DEF_GROUPS) {
|
|
2233
|
+
const sample = groups.slice(0, MAX_CALLER_DEF_GROUPS).map((g) => {
|
|
2234
|
+
const h = g[0];
|
|
2235
|
+
return `- \`${h.qualifiedName || h.name}\` (${h.kind}) — \`${h.filePath}:${h.startLine || '?'}\``;
|
|
2236
|
+
});
|
|
2237
|
+
return this.textResult([
|
|
2238
|
+
`**Callers of ${symbol} — ${groups.length} distinct definitions (too ambiguous to expand).**`,
|
|
2239
|
+
'',
|
|
2240
|
+
'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.',
|
|
2241
|
+
'',
|
|
2242
|
+
'Sample definitions:',
|
|
2243
|
+
...sample,
|
|
2244
|
+
groups.length > MAX_CALLER_DEF_GROUPS
|
|
2245
|
+
? `- … +${groups.length - MAX_CALLER_DEF_GROUPS} more`
|
|
2246
|
+
: '',
|
|
2247
|
+
].filter(Boolean).join('\n') + filterNote);
|
|
2248
|
+
}
|
|
2141
2249
|
const lines = [
|
|
2142
2250
|
`**Callers of ${symbol} — ${groups.length} distinct definitions (narrow with \`file\`)**`,
|
|
2143
2251
|
];
|
|
2252
|
+
let anyCallers = false;
|
|
2144
2253
|
for (const group of groups) {
|
|
2145
2254
|
const { callers, labels } = collect(group);
|
|
2146
2255
|
lines.push('', this.definitionHeading(group));
|
|
2147
2256
|
if (callers.length === 0) {
|
|
2148
|
-
lines.push('- (no callers)');
|
|
2257
|
+
lines.push('- (no callers indexed)');
|
|
2149
2258
|
continue;
|
|
2150
2259
|
}
|
|
2260
|
+
anyCallers = true;
|
|
2151
2261
|
for (const node of callers.slice(0, limit)) {
|
|
2152
2262
|
const location = node.startLine ? `:${node.startLine}` : '';
|
|
2153
2263
|
const label = labels.get(node.id);
|
|
2154
2264
|
lines.push(`- ${node.name} (${node.kind}) - ${node.filePath}${location}${label ? ` — via ${label}` : ''}`);
|
|
2155
2265
|
}
|
|
2156
2266
|
}
|
|
2157
|
-
|
|
2267
|
+
const footer = anyCallers
|
|
2268
|
+
? '\n\n> Caller listing complete — answer from this list; no read/grep needed.'
|
|
2269
|
+
: '\n\n> **No call edges indexed** for these definitions (common for callbacks / bus registration). '
|
|
2270
|
+
+ 'HomeGraph located the symbol(s) above — a **narrow** Grep for the registration site is OK; '
|
|
2271
|
+
+ 'do not open a repo-wide Grep storm. Prefer `homegraph_explore` with a concrete registrar Type if known.';
|
|
2272
|
+
return this.textResult(this.truncateOutput(lines.join('\n') + filterNote + footer));
|
|
2158
2273
|
}
|
|
2159
2274
|
/**
|
|
2160
2275
|
* Handle homegraph_callees
|
|
@@ -2212,6 +2327,22 @@ class ToolHandler {
|
|
|
2212
2327
|
return this.textResult(this.truncateOutput(formatted));
|
|
2213
2328
|
}
|
|
2214
2329
|
// Multiple DISTINCT definitions (#764): per-definition sections.
|
|
2330
|
+
const MAX_CALLEE_DEF_GROUPS = 8;
|
|
2331
|
+
if (groups.length > MAX_CALLEE_DEF_GROUPS) {
|
|
2332
|
+
const sample = groups.slice(0, MAX_CALLEE_DEF_GROUPS).map((g) => {
|
|
2333
|
+
const h = g[0];
|
|
2334
|
+
return `- \`${h.qualifiedName || h.name}\` (${h.kind}) — \`${h.filePath}:${h.startLine || '?'}\``;
|
|
2335
|
+
});
|
|
2336
|
+
return this.textResult([
|
|
2337
|
+
`**Callees of ${symbol} — ${groups.length} distinct definitions (too ambiguous to expand).**`,
|
|
2338
|
+
'',
|
|
2339
|
+
'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.',
|
|
2340
|
+
'',
|
|
2341
|
+
'Sample definitions:',
|
|
2342
|
+
...sample,
|
|
2343
|
+
`- … +${groups.length - MAX_CALLEE_DEF_GROUPS} more`,
|
|
2344
|
+
].join('\n') + filterNote);
|
|
2345
|
+
}
|
|
2215
2346
|
const lines = [
|
|
2216
2347
|
`**Callees of ${symbol} — ${groups.length} distinct definitions (narrow with \`file\`)**`,
|
|
2217
2348
|
];
|
|
@@ -3141,12 +3272,19 @@ class ToolHandler {
|
|
|
3141
3272
|
buildImportSitesSection(cg, query, projectRoot) {
|
|
3142
3273
|
const kitTerms = (0, query_utils_1.extractKitModuleNamesFromQuery)(query);
|
|
3143
3274
|
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
|
-
|
|
3275
|
+
// Named kit export (taskpool) stays; NL leftovers must not become focusExports.
|
|
3276
|
+
// When a *Kit is named and there is no real submodule, search by kit path only.
|
|
3277
|
+
const kitSubmodules = (0, query_utils_1.extractKitSubmoduleNamesFromQuery)(query);
|
|
3278
|
+
let focusExports = [
|
|
3145
3279
|
...new Set([
|
|
3146
|
-
...
|
|
3280
|
+
...kitSubmodules,
|
|
3147
3281
|
...depSymbols.filter((s) => s.length >= 4),
|
|
3148
3282
|
]),
|
|
3149
3283
|
];
|
|
3284
|
+
if (kitTerms.length > 0 && kitSubmodules.length === 0) {
|
|
3285
|
+
// Drop non-distinctive leftovers; kit module path is the inventory key.
|
|
3286
|
+
focusExports = focusExports.filter((s) => (0, query_utils_1.isDistinctiveIdentifier)(s) && s.length >= 5);
|
|
3287
|
+
}
|
|
3150
3288
|
const kitSearchTerms = (0, query_utils_1.extractImportSearchTerms)(query);
|
|
3151
3289
|
// Named export/API + kit (or usage-survey intent) → return the full matching list.
|
|
3152
3290
|
const completeInventory = focusExports.length > 0
|
|
@@ -3544,7 +3682,7 @@ class ToolHandler {
|
|
|
3544
3682
|
...types.map((t) => t.toLowerCase()),
|
|
3545
3683
|
].filter((n) => n.length >= 4);
|
|
3546
3684
|
if (needles.length === 0)
|
|
3547
|
-
return { section: '', hitCount: 0 };
|
|
3685
|
+
return { section: '', hitCount: 0, shellOnly: false };
|
|
3548
3686
|
const rel = (p) => p.replace(/\\/g, '/');
|
|
3549
3687
|
const inScope = (fp) => {
|
|
3550
3688
|
const low = rel(fp).toLowerCase();
|
|
@@ -3583,12 +3721,16 @@ class ToolHandler {
|
|
|
3583
3721
|
const fileLines = content.split('\n');
|
|
3584
3722
|
for (let i = 0; i < fileLines.length; i++) {
|
|
3585
3723
|
const line = fileLines[i] ?? '';
|
|
3586
|
-
// napi_define_function /
|
|
3724
|
+
// napi_define_function / properties table rows:
|
|
3725
|
+
// {"draw", nullptr, PluginManager::NapiDraw, ...}
|
|
3726
|
+
// (qualified C++ method names — `\w+` alone misses `::`)
|
|
3587
3727
|
const m = line.match(/napi_define_(?:function|property)\s*\([^,]+,\s*["']([\w]+)["']/i)
|
|
3588
|
-
|| line.match(/\{\s*["']([\w]+)["']\s*,\s*(?:nullptr|NULL)
|
|
3728
|
+
|| line.match(/\{\s*["']([\w]+)["']\s*,\s*(?:nullptr|NULL)\s*,\s*[\w:]+/i)
|
|
3589
3729
|
|| line.match(/\.?(?:exports|export)\s*[:=].*["']([\w]+)["']/i);
|
|
3590
3730
|
if (!m?.[1] || m[1].length < 2)
|
|
3591
3731
|
continue;
|
|
3732
|
+
if (/^(?:nullptr|NULL|napi_default|env|exports)$/i.test(m[1]))
|
|
3733
|
+
continue;
|
|
3592
3734
|
const key = `${m[1]}:${rel(r.node.filePath)}`;
|
|
3593
3735
|
if (seen.has(key))
|
|
3594
3736
|
continue;
|
|
@@ -3600,7 +3742,7 @@ class ToolHandler {
|
|
|
3600
3742
|
snippet: line.trim().slice(0, 100),
|
|
3601
3743
|
});
|
|
3602
3744
|
}
|
|
3603
|
-
// Also list Export / Init method nodes in-scope.
|
|
3745
|
+
// Also list Export / Init method nodes in-scope (entry shells — not the API surface).
|
|
3604
3746
|
if (/^(?:Export|Init|NapiInit)$/i.test(r.node.name)) {
|
|
3605
3747
|
const key = `fn:${r.node.name}:${rel(r.node.filePath)}`;
|
|
3606
3748
|
if (!seen.has(key)) {
|
|
@@ -3615,24 +3757,34 @@ class ToolHandler {
|
|
|
3615
3757
|
}
|
|
3616
3758
|
}
|
|
3617
3759
|
}
|
|
3760
|
+
const apiExports = exports.filter((e) => !/^(?:Export|Init|NapiInit)$/i.test(e.name));
|
|
3761
|
+
const shellOnly = apiExports.length === 0 && exports.length > 0;
|
|
3618
3762
|
const lines = [
|
|
3619
3763
|
'**NAPI / native export survey**',
|
|
3620
3764
|
'',
|
|
3621
|
-
|
|
3765
|
+
shellOnly
|
|
3766
|
+
? `> Found Init/Export shells under \`${needles.slice(0, 4).join('`, `')}\` but no \`napi_define_properties\` / descriptor API names. `
|
|
3767
|
+
+ '**Partial locator** — Read the Init body / \`napi_property_descriptor\` table next; do **not** ANSWER NOW as a full API list.'
|
|
3768
|
+
: `> In-repo NAPI surface for \`${needles.slice(0, 4).join('`, `')}\`. **ANSWER NOW** — do not Grep \`napi_define\` again.`,
|
|
3622
3769
|
'',
|
|
3623
3770
|
];
|
|
3624
3771
|
if (exports.length === 0) {
|
|
3625
3772
|
lines.push('- No `napi_define_*` / Export descriptors found under the named path/Type.');
|
|
3626
3773
|
lines.push('');
|
|
3627
|
-
return { section: lines.join('\n'), hitCount: 0 };
|
|
3774
|
+
return { section: lines.join('\n'), hitCount: 0, shellOnly: false };
|
|
3628
3775
|
}
|
|
3629
|
-
|
|
3776
|
+
const ranked = shellOnly ? exports : (apiExports.length > 0 ? apiExports : exports);
|
|
3777
|
+
for (const e of ranked.slice(0, 30)) {
|
|
3630
3778
|
lines.push(`- \`${e.name}\` — \`${e.file}:${e.line}\` \`${e.snippet}\``);
|
|
3631
3779
|
}
|
|
3632
|
-
if (
|
|
3633
|
-
lines.push(`- … and ${
|
|
3780
|
+
if (ranked.length > 30)
|
|
3781
|
+
lines.push(`- … and ${ranked.length - 30} more`);
|
|
3634
3782
|
lines.push('');
|
|
3635
|
-
return {
|
|
3783
|
+
return {
|
|
3784
|
+
section: lines.join('\n'),
|
|
3785
|
+
hitCount: Math.min(ranked.length, 30),
|
|
3786
|
+
shellOnly,
|
|
3787
|
+
};
|
|
3636
3788
|
}
|
|
3637
3789
|
/**
|
|
3638
3790
|
* Named `.d.ts` wrap — where the basename is imported / called in-repo.
|
|
@@ -3779,7 +3931,7 @@ class ToolHandler {
|
|
|
3779
3931
|
hits.set(fp, prev);
|
|
3780
3932
|
matched = true;
|
|
3781
3933
|
}
|
|
3782
|
-
if (!matched && /Toggle|Switch|
|
|
3934
|
+
if (!matched && /Toggle|Switch|Manager/i.test(r.node.name)) {
|
|
3783
3935
|
hits.set(fp, {
|
|
3784
3936
|
lines: [r.node.startLine],
|
|
3785
3937
|
snippet: (r.node.signature || r.node.name).slice(0, 100),
|
|
@@ -3811,28 +3963,59 @@ class ToolHandler {
|
|
|
3811
3963
|
}
|
|
3812
3964
|
buildDataSourceSection(cg, query) {
|
|
3813
3965
|
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 };
|
|
3966
|
+
if (anchors.length === 0) {
|
|
3967
|
+
return { section: '', edgeCount: 0, sdkImportCount: 0, strongCount: 0 };
|
|
3968
|
+
}
|
|
3816
3969
|
const rel = (p) => p.replace(/\\/g, '/');
|
|
3817
|
-
//
|
|
3818
|
-
const
|
|
3970
|
+
// ArkTS anonymous methods (`%AM0$foo`) are graph-internal — never "system services".
|
|
3971
|
+
const isAnonName = (n) => /^%AM\d+/i.test(n);
|
|
3972
|
+
// Strong service surface — NOT bare "badge" (that matched local getAllBadge / %AM*$badge*).
|
|
3973
|
+
const STRONG_SERVICE_RE = /@ohos\.|@kit\.|ohos\.|notificationManager|notificationSubscribe|SubscribeManager|CommonEvent|telephony|bundleManager|wantAgent|osAccount|AppAccount|distributedAccount|accountManager|vibrator/i;
|
|
3974
|
+
/** Domain kinship for sibling/writer Managers — stem match or service surface (not bare "ability"). */
|
|
3975
|
+
const relatedToAnchor = (stem, sig) => {
|
|
3976
|
+
const sl = stem.toLowerCase();
|
|
3977
|
+
const blob = sig.toLowerCase();
|
|
3978
|
+
if (sl.length >= 3 && blob.includes(sl))
|
|
3979
|
+
return true;
|
|
3980
|
+
if (STRONG_SERVICE_RE.test(sig))
|
|
3981
|
+
return true;
|
|
3982
|
+
if (/notification|numbadge|subscribe|commonevent/i.test(blob))
|
|
3983
|
+
return true;
|
|
3984
|
+
// Account* anchors may relate via account path without sharing the stem letters.
|
|
3985
|
+
if (/^account$/i.test(stem) && /account|osaccount|appaccount/i.test(blob))
|
|
3986
|
+
return true;
|
|
3987
|
+
return false;
|
|
3988
|
+
};
|
|
3819
3989
|
const lines = ['**Data sources / upstream services**', ''];
|
|
3820
|
-
|
|
3821
|
-
const
|
|
3990
|
+
const sdkIds = new Set();
|
|
3991
|
+
const strongIds = new Set();
|
|
3992
|
+
const addEdge = (bucket, node, via, rank, kind, opts) => {
|
|
3822
3993
|
if ((0, query_utils_1.isTestFile)(node.filePath))
|
|
3823
3994
|
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))
|
|
3995
|
+
if (isAnonName(node.name))
|
|
3828
3996
|
return;
|
|
3829
|
-
|
|
3997
|
+
const sig = `${node.name} ${node.filePath} ${node.signature || ''}`;
|
|
3998
|
+
if (opts?.importAlways) {
|
|
3999
|
+
if (!/@ohos\.|@kit\.|ohos\./i.test(sig))
|
|
4000
|
+
return;
|
|
4001
|
+
}
|
|
4002
|
+
else if (kind === 'strong') {
|
|
4003
|
+
// caller / related / strong-callee paths already pre-filtered
|
|
4004
|
+
}
|
|
4005
|
+
else if (!STRONG_SERVICE_RE.test(sig)) {
|
|
3830
4006
|
return;
|
|
4007
|
+
}
|
|
3831
4008
|
const prev = bucket.get(node.id);
|
|
3832
4009
|
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
|
-
|
|
4010
|
+
bucket.set(node.id, { name: node.name, file: rel(node.filePath), line: node.startLine, via, rank, kind });
|
|
4011
|
+
if (kind === 'sdk') {
|
|
4012
|
+
sdkIds.add(node.id);
|
|
4013
|
+
strongIds.delete(node.id);
|
|
4014
|
+
}
|
|
4015
|
+
else {
|
|
4016
|
+
if (!sdkIds.has(node.id))
|
|
4017
|
+
strongIds.add(node.id);
|
|
4018
|
+
}
|
|
3836
4019
|
}
|
|
3837
4020
|
};
|
|
3838
4021
|
for (const name of anchors.slice(0, 3)) {
|
|
@@ -3844,13 +4027,14 @@ class ToolHandler {
|
|
|
3844
4027
|
if (e.kind !== 'contains')
|
|
3845
4028
|
continue;
|
|
3846
4029
|
const m = cg.getNode(e.target);
|
|
3847
|
-
if (m && (m.kind === 'method' || m.kind === 'function'))
|
|
4030
|
+
if (m && (m.kind === 'method' || m.kind === 'function') && !isAnonName(m.name))
|
|
3848
4031
|
methods.push(m);
|
|
3849
4032
|
}
|
|
3850
4033
|
if (methods.length === 0) {
|
|
3851
4034
|
for (const n of cg.getNodesByName(name)) {
|
|
3852
|
-
if (n.filePath === cls.filePath && (n.kind === 'method' || n.kind === 'function'))
|
|
4035
|
+
if (n.filePath === cls.filePath && (n.kind === 'method' || n.kind === 'function') && !isAnonName(n.name)) {
|
|
3853
4036
|
methods.push(n);
|
|
4037
|
+
}
|
|
3854
4038
|
}
|
|
3855
4039
|
}
|
|
3856
4040
|
// 1) Same-file @ohos / kit imports — the usual system-service surface.
|
|
@@ -3858,26 +4042,26 @@ class ToolHandler {
|
|
|
3858
4042
|
for (const r of cg.searchNodes('@ohos', { kinds: ['import'], limit: 40 })) {
|
|
3859
4043
|
if (r.node.filePath !== cls.filePath)
|
|
3860
4044
|
continue;
|
|
3861
|
-
addEdge(upstream, r.node, 'import', 0, { importAlways: true });
|
|
4045
|
+
addEdge(upstream, r.node, 'import', 0, 'sdk', { importAlways: true });
|
|
3862
4046
|
}
|
|
3863
4047
|
for (const r of cg.searchNodes('@kit', { kinds: ['import'], limit: 40 })) {
|
|
3864
4048
|
if (r.node.filePath !== cls.filePath)
|
|
3865
4049
|
continue;
|
|
3866
|
-
addEdge(upstream, r.node, 'import', 0, { importAlways: true });
|
|
4050
|
+
addEdge(upstream, r.node, 'import', 0, 'sdk', { importAlways: true });
|
|
3867
4051
|
}
|
|
3868
4052
|
for (const term of [
|
|
3869
|
-
'notification', '
|
|
3870
|
-
'account', 'osAccount', 'AppAccount', 'distributedAccount',
|
|
4053
|
+
'notification', 'subscribe', 'bundle', 'ability',
|
|
4054
|
+
'account', 'osAccount', 'AppAccount', 'distributedAccount', 'telephony',
|
|
3871
4055
|
]) {
|
|
3872
4056
|
for (const r of cg.searchNodes(term, { kinds: ['import'], limit: 30 })) {
|
|
3873
4057
|
if (r.node.filePath !== cls.filePath)
|
|
3874
4058
|
continue;
|
|
3875
|
-
addEdge(upstream, r.node, 'import', 1, { importAlways: true });
|
|
4059
|
+
addEdge(upstream, r.node, 'import', 1, 'sdk', { importAlways: true });
|
|
3876
4060
|
}
|
|
3877
4061
|
}
|
|
3878
4062
|
}
|
|
3879
4063
|
catch { /* */ }
|
|
3880
|
-
// 2) Callees that look like
|
|
4064
|
+
// 2) Callees that look like SDK / system service APIs (not local cache helpers).
|
|
3881
4065
|
for (const method of methods.slice(0, 25)) {
|
|
3882
4066
|
let callees = [];
|
|
3883
4067
|
try {
|
|
@@ -3887,10 +4071,57 @@ class ToolHandler {
|
|
|
3887
4071
|
continue;
|
|
3888
4072
|
}
|
|
3889
4073
|
for (const c of callees) {
|
|
3890
|
-
if (c?.node)
|
|
3891
|
-
|
|
4074
|
+
if (!c?.node || isAnonName(c.node.name))
|
|
4075
|
+
continue;
|
|
4076
|
+
const sig = `${c.node.name} ${c.node.filePath} ${c.node.signature || ''}`;
|
|
4077
|
+
if (!STRONG_SERVICE_RE.test(sig))
|
|
4078
|
+
continue;
|
|
4079
|
+
addEdge(upstream, c.node, method.name, 2, 'strong');
|
|
3892
4080
|
}
|
|
3893
4081
|
}
|
|
4082
|
+
// 3) Writers: other Managers that call this Manager's update/set/on APIs.
|
|
4083
|
+
const stem = name.replace(/(?:Manager|Service|Handler|Store|Provider|Controller)$/i, '');
|
|
4084
|
+
for (const method of methods.slice(0, 20)) {
|
|
4085
|
+
if (!/^(?:update|set|on|subscribe|refresh|init|add|put)/i.test(method.name))
|
|
4086
|
+
continue;
|
|
4087
|
+
let callers = [];
|
|
4088
|
+
try {
|
|
4089
|
+
callers = cg.getCallers(method.id);
|
|
4090
|
+
}
|
|
4091
|
+
catch {
|
|
4092
|
+
continue;
|
|
4093
|
+
}
|
|
4094
|
+
for (const c of callers.slice(0, 12)) {
|
|
4095
|
+
if (!c?.node || isAnonName(c.node.name) || (0, query_utils_1.isTestFile)(c.node.filePath))
|
|
4096
|
+
continue;
|
|
4097
|
+
if (c.node.filePath === cls.filePath)
|
|
4098
|
+
continue;
|
|
4099
|
+
const sig = `${c.node.name} ${c.node.filePath}`;
|
|
4100
|
+
if (!/(?:Manager|Service|Handler|Controller)$/i.test(c.node.name))
|
|
4101
|
+
continue;
|
|
4102
|
+
if (!relatedToAnchor(stem, sig))
|
|
4103
|
+
continue;
|
|
4104
|
+
addEdge(upstream, c.node, `caller→${method.name}`, 1, 'strong');
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
// 4) Sibling *Manager types in the same domain (stem match).
|
|
4108
|
+
if (stem.length >= 3) {
|
|
4109
|
+
try {
|
|
4110
|
+
for (const r of cg.searchNodes(stem, { kinds: ['class', 'struct', 'component'], limit: 24 })) {
|
|
4111
|
+
const n = r.node;
|
|
4112
|
+
if (n.name === name || (0, query_utils_1.isTestFile)(n.filePath) || isAnonName(n.name))
|
|
4113
|
+
continue;
|
|
4114
|
+
if (!/(?:Manager|Service|Handler|Controller)$/i.test(n.name))
|
|
4115
|
+
continue;
|
|
4116
|
+
if (!relatedToAnchor(stem, `${n.name} ${n.filePath}`))
|
|
4117
|
+
continue;
|
|
4118
|
+
// Prefer Subscribe / notification writers over weakly related Ability Managers.
|
|
4119
|
+
const rank = /Subscribe|notification/i.test(`${n.name} ${n.filePath}`) ? 2 : 3;
|
|
4120
|
+
addEdge(upstream, n, 'related Manager', rank, 'strong');
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
catch { /* */ }
|
|
4124
|
+
}
|
|
3894
4125
|
if (upstream.size === 0)
|
|
3895
4126
|
continue;
|
|
3896
4127
|
lines.push(`### \`${cls.name}\` (\`${rel(cls.filePath)}\`)`);
|
|
@@ -3901,20 +4132,380 @@ class ToolHandler {
|
|
|
3901
4132
|
if (ranked.length > 15)
|
|
3902
4133
|
lines.push(`- … and ${ranked.length - 15} more upstream symbol(s)`);
|
|
3903
4134
|
lines.push('');
|
|
3904
|
-
lines.push('> Prefer `@ohos.*` / notification / bundle imports as the system-service answer; local helpers are secondary.');
|
|
3905
|
-
lines.push('');
|
|
3906
4135
|
}
|
|
3907
4136
|
}
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
4137
|
+
const sdkImportCount = sdkIds.size;
|
|
4138
|
+
const strongCount = strongIds.size;
|
|
4139
|
+
const edgeCount = sdkImportCount + strongCount;
|
|
4140
|
+
const hasHardAnswer = sdkImportCount > 0 && !(0, query_utils_1.queryAsDataSourceDistinguishAsk)(query);
|
|
4141
|
+
const hasLocator = edgeCount > 0;
|
|
4142
|
+
if (!hasLocator) {
|
|
4143
|
+
lines.push('- No `@ohos`/`@kit` imports or related service Managers indexed for the named type. ' +
|
|
4144
|
+
'Treat the Manager definition as the **locator anchor**; ONE **narrow** Grep inside that file ' +
|
|
4145
|
+
'(or its callers) for `@ohos` / service imports is OK — not a repo-wide storm.');
|
|
3911
4146
|
lines.push('');
|
|
3912
|
-
|
|
4147
|
+
lines.push('> **Partial locator** — do **not** treat local cache/RDB helpers as the system service. ' +
|
|
4148
|
+
'Prefer `homegraph_callers` on `update*` / `set*` members, or a sibling `*Manager` / `*Subscribe*Manager`.');
|
|
4149
|
+
lines.push('');
|
|
4150
|
+
return { section: lines.join('\n'), edgeCount: 0, sdkImportCount: 0, strongCount: 0 };
|
|
4151
|
+
}
|
|
4152
|
+
if (hasHardAnswer) {
|
|
4153
|
+
lines.push('> Data-source survey — **ANSWER NOW** from system `@ohos`/`@kit` imports above first. ' +
|
|
4154
|
+
'Do not Grep/search/callers the same Manager for the service name.');
|
|
4155
|
+
}
|
|
4156
|
+
else {
|
|
4157
|
+
lines.push('> **Partial locator** — related writers / sibling Managers above are anchors, not a full answer. ' +
|
|
4158
|
+
'ONE tighter explore with a named Type, or ONE narrow Grep for `@ohos` if still missing — ' +
|
|
4159
|
+
'do **not** fan out `homegraph_node` / callers. Do **not** ANSWER NOW from local cache helpers.');
|
|
3913
4160
|
}
|
|
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
4161
|
lines.push('');
|
|
3917
|
-
return { section: lines.join('\n'), edgeCount };
|
|
4162
|
+
return { section: lines.join('\n'), edgeCount, sdkImportCount, strongCount };
|
|
4163
|
+
}
|
|
4164
|
+
/**
|
|
4165
|
+
* Event-type inventory + which *Manager files import / handle them.
|
|
4166
|
+
* Prefers enum members on the named `*Event` Type; softens ANSWER NOW when
|
|
4167
|
+
* the graph only has noisy Type/file hits without a member list.
|
|
4168
|
+
*/
|
|
4169
|
+
buildEventDispatchSection(cg, query, projectRoot) {
|
|
4170
|
+
const types = (0, query_utils_1.extractTypeNamesFromQuery)(query).filter((t) => /Event$/i.test(t) || /Mgr$/i.test(t) || /Manager$/i.test(t));
|
|
4171
|
+
const primaryEvents = types.filter((t) => /Event$/i.test(t));
|
|
4172
|
+
if (types.length === 0) {
|
|
4173
|
+
return { section: '', hitCount: 0, eventCount: 0, handlerCount: 0, memberCount: 0, complete: false };
|
|
4174
|
+
}
|
|
4175
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
4176
|
+
const eventNames = new Set();
|
|
4177
|
+
const eventFiles = new Set();
|
|
4178
|
+
const enumMembers = [];
|
|
4179
|
+
const seenMember = new Set();
|
|
4180
|
+
const considerEventFile = (fp, preferredName) => {
|
|
4181
|
+
eventFiles.add(fp);
|
|
4182
|
+
if (/Event$/i.test(preferredName))
|
|
4183
|
+
eventNames.add(preferredName);
|
|
4184
|
+
try {
|
|
4185
|
+
for (const sib of cg.getNodesInFile(fp)) {
|
|
4186
|
+
if ((0, query_utils_1.isTestFile)(sib.filePath))
|
|
4187
|
+
continue;
|
|
4188
|
+
if (sib.kind === 'enum_member') {
|
|
4189
|
+
const key = `${sib.name}:${rel(sib.filePath)}`;
|
|
4190
|
+
if (seenMember.has(key))
|
|
4191
|
+
continue;
|
|
4192
|
+
seenMember.add(key);
|
|
4193
|
+
enumMembers.push({ name: sib.name, file: rel(sib.filePath), line: sib.startLine || 1 });
|
|
4194
|
+
}
|
|
4195
|
+
// Only add sibling Types that are themselves *Event (avoid file-wide enum spam).
|
|
4196
|
+
if ((sib.kind === 'enum' || sib.kind === 'class' || sib.kind === 'type_alias' || sib.kind === 'interface')
|
|
4197
|
+
&& /Event$/i.test(sib.name)) {
|
|
4198
|
+
eventNames.add(sib.name);
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
}
|
|
4202
|
+
catch { /* */ }
|
|
4203
|
+
};
|
|
4204
|
+
for (const name of (primaryEvents.length > 0 ? primaryEvents : types).slice(0, 4)) {
|
|
4205
|
+
let nodes = [];
|
|
4206
|
+
try {
|
|
4207
|
+
nodes = cg.getNodesByName(name);
|
|
4208
|
+
}
|
|
4209
|
+
catch {
|
|
4210
|
+
nodes = [];
|
|
4211
|
+
}
|
|
4212
|
+
// File / export-only Event Types: getNodesByName can miss while search finds the file.
|
|
4213
|
+
if (nodes.length === 0 && /Event$/i.test(name)) {
|
|
4214
|
+
try {
|
|
4215
|
+
for (const r of cg.searchNodes(name, { limit: 24 })) {
|
|
4216
|
+
if ((0, query_utils_1.isTestFile)(r.node.filePath))
|
|
4217
|
+
continue;
|
|
4218
|
+
const base = r.node.filePath.replace(/\\/g, '/').split('/').pop() || '';
|
|
4219
|
+
const stem = base.replace(/\.(ets|ts|d\.ts)$/i, '');
|
|
4220
|
+
if (r.node.name === name || stem === name || r.node.kind === 'file') {
|
|
4221
|
+
nodes.push(r.node);
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
catch { /* */ }
|
|
4226
|
+
}
|
|
4227
|
+
for (const n of nodes) {
|
|
4228
|
+
if ((0, query_utils_1.isTestFile)(n.filePath))
|
|
4229
|
+
continue;
|
|
4230
|
+
if (/Event$/i.test(n.name)
|
|
4231
|
+
|| n.kind === 'enum'
|
|
4232
|
+
|| n.kind === 'class'
|
|
4233
|
+
|| n.kind === 'type_alias'
|
|
4234
|
+
|| n.kind === 'interface'
|
|
4235
|
+
|| n.kind === 'file'
|
|
4236
|
+
|| (n.kind === 'export' && /Event$/i.test(name))) {
|
|
4237
|
+
const label = /Event$/i.test(n.name) ? n.name : name;
|
|
4238
|
+
if (/Event$/i.test(label))
|
|
4239
|
+
eventNames.add(label);
|
|
4240
|
+
considerEventFile(n.filePath, label);
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4244
|
+
if (eventNames.size === 0 && enumMembers.length === 0) {
|
|
4245
|
+
return {
|
|
4246
|
+
section: [
|
|
4247
|
+
'**Event types → Manager / handler sites**',
|
|
4248
|
+
'',
|
|
4249
|
+
'> **Partial locator** — no event Type/enum exports indexed for the named anchors. '
|
|
4250
|
+
+ '`homegraph_node` / ONE explore on the Event Type file, or ONE Grep for enum members / '
|
|
4251
|
+
+ '`case` branches — not a repo-wide storm. Do **not** ANSWER NOW.',
|
|
4252
|
+
'',
|
|
4253
|
+
].join('\n'),
|
|
4254
|
+
hitCount: 0,
|
|
4255
|
+
eventCount: 0,
|
|
4256
|
+
handlerCount: 0,
|
|
4257
|
+
memberCount: 0,
|
|
4258
|
+
complete: false,
|
|
4259
|
+
};
|
|
4260
|
+
}
|
|
4261
|
+
const lines = [
|
|
4262
|
+
'**Event types → Manager / handler sites**',
|
|
4263
|
+
'',
|
|
4264
|
+
];
|
|
4265
|
+
if (enumMembers.length > 0) {
|
|
4266
|
+
lines.push('### Event enum members (indexed)');
|
|
4267
|
+
for (const m of enumMembers.slice(0, 40)) {
|
|
4268
|
+
lines.push(`- \`${m.name}\` — \`${m.file}:${m.line}\``);
|
|
4269
|
+
}
|
|
4270
|
+
if (enumMembers.length > 40)
|
|
4271
|
+
lines.push(`- … and ${enumMembers.length - 40} more`);
|
|
4272
|
+
lines.push('');
|
|
4273
|
+
}
|
|
4274
|
+
lines.push('### Event Type anchors');
|
|
4275
|
+
const sortedEvents = [...eventNames].sort((a, b) => a.localeCompare(b));
|
|
4276
|
+
for (const ev of sortedEvents.slice(0, 12)) {
|
|
4277
|
+
let loc = '';
|
|
4278
|
+
try {
|
|
4279
|
+
const n = cg.getNodesByName(ev).find((x) => !(0, query_utils_1.isTestFile)(x.filePath));
|
|
4280
|
+
if (n)
|
|
4281
|
+
loc = ` — \`${rel(n.filePath)}:${n.startLine}\``;
|
|
4282
|
+
}
|
|
4283
|
+
catch { /* */ }
|
|
4284
|
+
if (!loc) {
|
|
4285
|
+
for (const fp of eventFiles) {
|
|
4286
|
+
const base = rel(fp).split('/').pop() || '';
|
|
4287
|
+
const stem = base.replace(/\.(ets|ts|d\.ts)$/i, '');
|
|
4288
|
+
if (stem === ev || base.includes(ev)) {
|
|
4289
|
+
loc = ` — \`${rel(fp)}\``;
|
|
4290
|
+
break;
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
lines.push(`- \`${ev}\`${loc}`);
|
|
4295
|
+
}
|
|
4296
|
+
lines.push('');
|
|
4297
|
+
// Search handlers by primary Event names (+ a few members), not every noisy sibling.
|
|
4298
|
+
const searchKeys = [
|
|
4299
|
+
...sortedEvents.slice(0, 6),
|
|
4300
|
+
...enumMembers.slice(0, 12).map((m) => m.name),
|
|
4301
|
+
];
|
|
4302
|
+
const hits = new Map();
|
|
4303
|
+
for (const key of searchKeys) {
|
|
4304
|
+
let results = [];
|
|
4305
|
+
try {
|
|
4306
|
+
results = cg.searchNodes(key, { limit: 36 });
|
|
4307
|
+
}
|
|
4308
|
+
catch {
|
|
4309
|
+
continue;
|
|
4310
|
+
}
|
|
4311
|
+
for (const r of results) {
|
|
4312
|
+
const fp = rel(r.node.filePath);
|
|
4313
|
+
if ((0, query_utils_1.isTestFile)(fp))
|
|
4314
|
+
continue;
|
|
4315
|
+
if (eventFiles.has(r.node.filePath) && (r.node.name === key || r.node.kind === 'enum_member'))
|
|
4316
|
+
continue;
|
|
4317
|
+
const isMgr = /Manager|Handler|Dispatcher|Controller/i.test(fp)
|
|
4318
|
+
|| /Manager|Handler|Dispatcher|Controller/i.test(r.node.name);
|
|
4319
|
+
if (!isMgr && r.node.kind !== 'import')
|
|
4320
|
+
continue;
|
|
4321
|
+
const abs = (0, path_1.isAbsolute)(r.node.filePath)
|
|
4322
|
+
? r.node.filePath
|
|
4323
|
+
: (0, path_1.join)(projectRoot, r.node.filePath);
|
|
4324
|
+
let snippet = (r.node.signature || r.node.name).slice(0, 100);
|
|
4325
|
+
let lineHasKey = false;
|
|
4326
|
+
try {
|
|
4327
|
+
const content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
4328
|
+
const fileLines = content.split('\n');
|
|
4329
|
+
const li = Math.max(0, (r.node.startLine || 1) - 1);
|
|
4330
|
+
const line = fileLines[li] ?? '';
|
|
4331
|
+
lineHasKey = line.includes(key);
|
|
4332
|
+
if (lineHasKey || /import/i.test(line))
|
|
4333
|
+
snippet = line.trim().slice(0, 120);
|
|
4334
|
+
}
|
|
4335
|
+
catch { /* */ }
|
|
4336
|
+
// Drop weak FTS hits that neither import nor mention the key on the line.
|
|
4337
|
+
if (!lineHasKey && r.node.kind !== 'import' && r.node.name !== key)
|
|
4338
|
+
continue;
|
|
4339
|
+
const prev = hits.get(fp) ?? {
|
|
4340
|
+
file: fp,
|
|
4341
|
+
line: r.node.startLine || 1,
|
|
4342
|
+
snippet,
|
|
4343
|
+
score: 0,
|
|
4344
|
+
events: new Set(),
|
|
4345
|
+
};
|
|
4346
|
+
prev.events.add(key);
|
|
4347
|
+
prev.score += isMgr ? 3 : 1;
|
|
4348
|
+
if (r.node.kind === 'import')
|
|
4349
|
+
prev.score += 2;
|
|
4350
|
+
if (lineHasKey)
|
|
4351
|
+
prev.score += 2;
|
|
4352
|
+
if (!prev.snippet && snippet)
|
|
4353
|
+
prev.snippet = snippet;
|
|
4354
|
+
hits.set(fp, prev);
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
const ranked = [...hits.values()].sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
|
|
4358
|
+
lines.push('### Manager / handler files referencing these events');
|
|
4359
|
+
const memberCount = enumMembers.length;
|
|
4360
|
+
// Complete only when we have enum members (the usual "有哪些类型") + ≥1 handler,
|
|
4361
|
+
// or a small tight Type set with strong handlers — never a huge Type dump alone.
|
|
4362
|
+
const complete = ranked.length > 0
|
|
4363
|
+
&& ((memberCount > 0 && memberCount <= 48)
|
|
4364
|
+
|| (sortedEvents.length > 0 && sortedEvents.length <= 4 && ranked.length >= 1 && ranked[0].score >= 5));
|
|
4365
|
+
if (ranked.length === 0) {
|
|
4366
|
+
lines.push('- No Manager import/use sites indexed for these event types.');
|
|
4367
|
+
lines.push('');
|
|
4368
|
+
lines.push('> **Partial locator** — event anchors above only. '
|
|
4369
|
+
+ '`homegraph_node` the Event file or ONE Grep for handler/`case` branches — do **not** ANSWER NOW as a full dispatch map.');
|
|
4370
|
+
}
|
|
4371
|
+
else {
|
|
4372
|
+
for (const h of ranked.slice(0, 24)) {
|
|
4373
|
+
const evs = [...h.events].slice(0, 4).join(', ');
|
|
4374
|
+
lines.push(h.snippet
|
|
4375
|
+
? `- \`${h.file}:${h.line}\` ← \`${evs}\` \`${h.snippet}\``
|
|
4376
|
+
: `- \`${h.file}:${h.line}\` ← \`${evs}\``);
|
|
4377
|
+
}
|
|
4378
|
+
if (ranked.length > 24)
|
|
4379
|
+
lines.push(`- … and ${ranked.length - 24} more handler file(s)`);
|
|
4380
|
+
lines.push('');
|
|
4381
|
+
if (complete) {
|
|
4382
|
+
lines.push('> Event→Manager inventory — **ANSWER NOW** from enum members + handler files above; '
|
|
4383
|
+
+ 'ONE narrow Grep only if a specific `case` branch is still missing.');
|
|
4384
|
+
}
|
|
4385
|
+
else {
|
|
4386
|
+
lines.push('> **Partial locator** — handler files are anchors, but enum-member coverage may be incomplete. '
|
|
4387
|
+
+ '`homegraph_node` the Event Type / ONE Grep for members or `case` — do **not** repo-storm; do **not** ANSWER NOW as a full map.');
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
lines.push('');
|
|
4391
|
+
return {
|
|
4392
|
+
section: lines.join('\n'),
|
|
4393
|
+
hitCount: ranked.length + memberCount,
|
|
4394
|
+
eventCount: Math.max(sortedEvents.length, memberCount > 0 ? 1 : 0),
|
|
4395
|
+
handlerCount: ranked.length,
|
|
4396
|
+
memberCount,
|
|
4397
|
+
complete,
|
|
4398
|
+
};
|
|
4399
|
+
}
|
|
4400
|
+
/**
|
|
4401
|
+
* ≥3 named Types + dependency ask — file locations + cross-imports only.
|
|
4402
|
+
* Avoids fat multi-body compact that hits the soft deadline (D60).
|
|
4403
|
+
*/
|
|
4404
|
+
buildMultiTypeDependencySection(cg, query) {
|
|
4405
|
+
const types = (0, query_utils_1.extractTypeNamesFromQuery)(query).filter((t) => t.length >= 8
|
|
4406
|
+
|| /(?:Manager|Installer|Parser|Service|Controller|Handler|Provider|Store)$/i.test(t));
|
|
4407
|
+
if (types.length < 3)
|
|
4408
|
+
return { section: '', hitCount: 0 };
|
|
4409
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
4410
|
+
const locs = [];
|
|
4411
|
+
const fileByType = new Map();
|
|
4412
|
+
for (const name of types.slice(0, 8)) {
|
|
4413
|
+
let nodes = [];
|
|
4414
|
+
try {
|
|
4415
|
+
nodes = cg.getNodesByName(name).filter((n) => !(0, query_utils_1.isTestFile)(n.filePath)
|
|
4416
|
+
&& !(0, arkts_1.isOhosApiFilePath)(n.filePath)
|
|
4417
|
+
&& !/\.d\.ts$/i.test(n.filePath)
|
|
4418
|
+
&& (n.kind === 'class' || n.kind === 'struct' || n.kind === 'interface' || n.kind === 'component'));
|
|
4419
|
+
}
|
|
4420
|
+
catch {
|
|
4421
|
+
continue;
|
|
4422
|
+
}
|
|
4423
|
+
if (nodes.length === 0) {
|
|
4424
|
+
try {
|
|
4425
|
+
for (const r of cg.searchNodes(name, { limit: 12 })) {
|
|
4426
|
+
if ((0, query_utils_1.isTestFile)(r.node.filePath) || (0, arkts_1.isOhosApiFilePath)(r.node.filePath))
|
|
4427
|
+
continue;
|
|
4428
|
+
if (r.node.name === name || r.node.kind === 'class') {
|
|
4429
|
+
nodes.push(r.node);
|
|
4430
|
+
break;
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
}
|
|
4434
|
+
catch { /* */ }
|
|
4435
|
+
}
|
|
4436
|
+
const n = nodes[0];
|
|
4437
|
+
if (!n)
|
|
4438
|
+
continue;
|
|
4439
|
+
locs.push({ name, file: rel(n.filePath), line: n.startLine || 1 });
|
|
4440
|
+
fileByType.set(name, rel(n.filePath));
|
|
4441
|
+
}
|
|
4442
|
+
if (locs.length === 0)
|
|
4443
|
+
return { section: '', hitCount: 0 };
|
|
4444
|
+
const lines = [
|
|
4445
|
+
'**Named-Type dependency inventory** (locations + cross-file imports — no full bodies)',
|
|
4446
|
+
'',
|
|
4447
|
+
'### Types (indexed)',
|
|
4448
|
+
];
|
|
4449
|
+
for (const l of locs) {
|
|
4450
|
+
lines.push(`- \`${l.name}\` — \`${l.file}:${l.line}\``);
|
|
4451
|
+
}
|
|
4452
|
+
lines.push('');
|
|
4453
|
+
// Cross edges: does Type A's file import/reference Type B?
|
|
4454
|
+
const edges = [];
|
|
4455
|
+
for (const a of locs) {
|
|
4456
|
+
let fileNodes = [];
|
|
4457
|
+
try {
|
|
4458
|
+
fileNodes = cg.getNodesInFile(a.file);
|
|
4459
|
+
}
|
|
4460
|
+
catch {
|
|
4461
|
+
continue;
|
|
4462
|
+
}
|
|
4463
|
+
const blob = fileNodes
|
|
4464
|
+
.filter((n) => n.kind === 'import' || n.kind === 'export')
|
|
4465
|
+
.map((n) => `${n.name} ${n.signature || ''}`)
|
|
4466
|
+
.join('\n');
|
|
4467
|
+
for (const b of locs) {
|
|
4468
|
+
if (a.name === b.name)
|
|
4469
|
+
continue;
|
|
4470
|
+
if (blob.includes(b.name) || fileNodes.some((n) => n.name === b.name && n.kind === 'import')) {
|
|
4471
|
+
edges.push(`- \`${a.name}\` → \`${b.name}\` (import/ref in \`${a.file}\`)`);
|
|
4472
|
+
}
|
|
4473
|
+
}
|
|
4474
|
+
// Also check callers between types (light).
|
|
4475
|
+
try {
|
|
4476
|
+
const seed = cg.getNodesByName(a.name).find((n) => rel(n.filePath) === a.file);
|
|
4477
|
+
if (!seed)
|
|
4478
|
+
continue;
|
|
4479
|
+
for (const { node: c } of cg.getCallers(seed.id).slice(0, 20)) {
|
|
4480
|
+
if ((0, query_utils_1.isTestFile)(c.filePath))
|
|
4481
|
+
continue;
|
|
4482
|
+
for (const b of locs) {
|
|
4483
|
+
if (b.name === a.name)
|
|
4484
|
+
continue;
|
|
4485
|
+
if (c.name === b.name || rel(c.filePath) === fileByType.get(b.name)) {
|
|
4486
|
+
edges.push(`- \`${b.name}\` → calls/uses \`${a.name}\` (\`${rel(c.filePath)}:${c.startLine}\`)`);
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
catch { /* */ }
|
|
4492
|
+
}
|
|
4493
|
+
lines.push('### Cross links (imports / callers)');
|
|
4494
|
+
if (edges.length === 0) {
|
|
4495
|
+
lines.push('- No import/caller edges indexed between these Types yet.');
|
|
4496
|
+
lines.push('');
|
|
4497
|
+
lines.push('> **Partial locator** — Type files above are anchors. '
|
|
4498
|
+
+ '`homegraph_node` / `homegraph_callees` one Type at a time; do **not** ANSWER NOW as a full graph.');
|
|
4499
|
+
}
|
|
4500
|
+
else {
|
|
4501
|
+
const uniq = [...new Set(edges)].slice(0, 40);
|
|
4502
|
+
lines.push(...uniq);
|
|
4503
|
+
lines.push('');
|
|
4504
|
+
lines.push('> Multi-Type dependency inventory — **ANSWER NOW** from locations + edges above; '
|
|
4505
|
+
+ 'only `homegraph_node` one Type if a body detail is still missing.');
|
|
4506
|
+
}
|
|
4507
|
+
lines.push('');
|
|
4508
|
+
return { section: lines.join('\n'), hitCount: locs.length + edges.length };
|
|
3918
4509
|
}
|
|
3919
4510
|
/**
|
|
3920
4511
|
* Main-thread fast path for inventory surveys — skips the worker queue.
|
|
@@ -3952,6 +4543,25 @@ class ToolHandler {
|
|
|
3952
4543
|
tryFastInventoryExplore(cg, query, projectRoot) {
|
|
3953
4544
|
if (!(0, query_utils_1.shouldTryFastInventoryExplore)(query))
|
|
3954
4545
|
return null;
|
|
4546
|
+
// Multi-Type dependency asks: inventory-only early exit (avoids fat compact / 25s busy).
|
|
4547
|
+
if ((0, query_utils_1.queryAsMultiTypeDependencySurvey)(query)) {
|
|
4548
|
+
const multi = this.buildMultiTypeDependencySection(cg, query);
|
|
4549
|
+
if (multi.section) {
|
|
4550
|
+
const soft = /Partial locator/i.test(multi.section);
|
|
4551
|
+
return this.textResult([
|
|
4552
|
+
`**Exploration: ${query}**`,
|
|
4553
|
+
'',
|
|
4554
|
+
soft
|
|
4555
|
+
? `Named-Type dependency inventory — **${multi.hitCount}** locator hit(s). `
|
|
4556
|
+
+ '**Partial locator** — locations above; ONE tighter explore with a named Type, '
|
|
4557
|
+
+ 'or ONE narrow Grep — do **not** fan out `homegraph_node` / callees.'
|
|
4558
|
+
: `Named-Type dependency inventory — **${multi.hitCount}** locator hit(s). `
|
|
4559
|
+
+ '**ANSWER NOW** from locations + cross links; one `homegraph_node` only if a body detail is still missing.',
|
|
4560
|
+
'',
|
|
4561
|
+
multi.section,
|
|
4562
|
+
].join('\n'));
|
|
4563
|
+
}
|
|
4564
|
+
}
|
|
3955
4565
|
const lines = [`**Exploration: ${query}**`, '', ''];
|
|
3956
4566
|
const summaryLineIdx = 2;
|
|
3957
4567
|
const inheritanceOnly = (0, query_utils_1.shouldBuildInheritanceSurvey)(query);
|
|
@@ -4007,6 +4617,9 @@ class ToolHandler {
|
|
|
4007
4617
|
|| (0, query_utils_1.queryIsTypeNameFocus)(query)
|
|
4008
4618
|
|| (0, query_utils_1.queryAsInheritanceSurvey)(query)
|
|
4009
4619
|
|| (0, query_utils_1.queryAsDataSourceSurvey)(query)
|
|
4620
|
+
|| (0, query_utils_1.queryAsEventDispatchSurvey)(query)
|
|
4621
|
+
|| (0, query_utils_1.queryAsOutOfRepoSdkCatalog)(query)
|
|
4622
|
+
|| (0, query_utils_1.queryAsKitModuleCapabilitySurvey)(query)
|
|
4010
4623
|
|| (0, query_utils_1.shouldBuildApiUsageSurvey)(query)
|
|
4011
4624
|
|| (0, query_utils_1.queryAsInRepoSystemCapabilityHowto)(query)
|
|
4012
4625
|
|| (0, query_utils_1.queryAsReturnValueConsumerSurvey)(query)
|
|
@@ -4065,7 +4678,7 @@ class ToolHandler {
|
|
|
4065
4678
|
lines.push(moduleDepResult.section);
|
|
4066
4679
|
const moduleExportResult = (0, query_utils_1.queryAsModuleExportSurvey)(query)
|
|
4067
4680
|
? this.buildModuleExportSurveySection(cg, query, projectRoot)
|
|
4068
|
-
: { section: '', hitCount: 0 };
|
|
4681
|
+
: { section: '', hitCount: 0, shellOnly: false };
|
|
4069
4682
|
if (moduleExportResult.section)
|
|
4070
4683
|
lines.push(moduleExportResult.section);
|
|
4071
4684
|
const dtsWrapResult = (0, query_utils_1.queryAsDtsWrapSurvey)(query)
|
|
@@ -4080,9 +4693,14 @@ class ToolHandler {
|
|
|
4080
4693
|
lines.push(controlSyncResult.section);
|
|
4081
4694
|
const dataSourceResult = (0, query_utils_1.queryAsDataSourceSurvey)(query)
|
|
4082
4695
|
? this.buildDataSourceSection(cg, query)
|
|
4083
|
-
: { section: '', edgeCount: 0 };
|
|
4696
|
+
: { section: '', edgeCount: 0, sdkImportCount: 0, strongCount: 0 };
|
|
4084
4697
|
if (dataSourceResult.section)
|
|
4085
4698
|
lines.push(dataSourceResult.section);
|
|
4699
|
+
const eventDispatchResult = (0, query_utils_1.queryAsEventDispatchSurvey)(query)
|
|
4700
|
+
? this.buildEventDispatchSection(cg, query, projectRoot)
|
|
4701
|
+
: { section: '', hitCount: 0, eventCount: 0, handlerCount: 0, memberCount: 0, complete: false };
|
|
4702
|
+
if (eventDispatchResult.section)
|
|
4703
|
+
lines.push(eventDispatchResult.section);
|
|
4086
4704
|
const hoverResult = (0, query_utils_1.shouldBuildHoverHandlerSurvey)(query)
|
|
4087
4705
|
? this.buildHoverHandlerSurveySection(cg, query, projectRoot)
|
|
4088
4706
|
: { section: '', hitCount: 0 };
|
|
@@ -4095,7 +4713,8 @@ class ToolHandler {
|
|
|
4095
4713
|
const skipFlow = typeSurface || (0, query_utils_1.queryAsCallerOrMethodSurvey)(query)
|
|
4096
4714
|
|| (0, query_utils_1.queryAsDeclarationSiteSurvey)(query)
|
|
4097
4715
|
|| (0, query_utils_1.queryAsInRepoSystemCapabilityHowto)(query)
|
|
4098
|
-
|| (0, query_utils_1.queryAsReturnValueConsumerSurvey)(query)
|
|
4716
|
+
|| (0, query_utils_1.queryAsReturnValueConsumerSurvey)(query)
|
|
4717
|
+
|| (0, query_utils_1.queryAsEventDispatchSurvey)(query);
|
|
4099
4718
|
const flow = skipFlow
|
|
4100
4719
|
? { pathNodeIds: new Set(), text: '' }
|
|
4101
4720
|
: this.buildFlowFromNamedSymbols(cg, query);
|
|
@@ -4138,7 +4757,8 @@ class ToolHandler {
|
|
|
4138
4757
|
|| apiUsageResult.section || dataSourceResult.section || hoverResult.section || inheritanceSection
|
|
4139
4758
|
|| callerSection || memberSection || configSection
|
|
4140
4759
|
|| systemCapResult.section || declarationResult.section || moduleDepResult.section
|
|
4141
|
-
|| moduleExportResult.section || dtsWrapResult.section || controlSyncResult.section
|
|
4760
|
+
|| moduleExportResult.section || dtsWrapResult.section || controlSyncResult.section
|
|
4761
|
+
|| eventDispatchResult.section;
|
|
4142
4762
|
if (!hasAnySection)
|
|
4143
4763
|
return null;
|
|
4144
4764
|
// Data-source / API usage inventories are complete without source dumps —
|
|
@@ -4147,10 +4767,19 @@ class ToolHandler {
|
|
|
4147
4767
|
return finishCompact(`System-capability howto — **${systemCapResult.hitCount}** in-repo site(s). **ANSWER NOW** from \`@ohos\`/\`System\` call sites above.`);
|
|
4148
4768
|
}
|
|
4149
4769
|
// Declaration intent must NOT fall through to a fat explore / include dump when empty.
|
|
4770
|
+
// Sparse hit lists are Partial locator — hard ANSWER NOW caused D40-style grep storms.
|
|
4150
4771
|
if (declarationResult.attempted) {
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4772
|
+
const inventoryAsk = /哪些|分别|各自|哪些文件|id\b|声明|绑定/i.test(query);
|
|
4773
|
+
const sparse = declarationResult.hitCount < 3
|
|
4774
|
+
|| (inventoryAsk && declarationResult.hitCount < 6);
|
|
4775
|
+
if (declarationResult.hitCount === 0) {
|
|
4776
|
+
return finishCompact('Declaration-site survey complete (0 sites). **Partial locator** — ONE Grep for the Type constructor is OK.');
|
|
4777
|
+
}
|
|
4778
|
+
if (sparse) {
|
|
4779
|
+
return finishCompact(`Declaration-site survey — **${declarationResult.hitCount}** site(s) (may be incomplete). ` +
|
|
4780
|
+
'**Partial locator** — narrow Grep for more `id` / constructor sites OK; do not repo-storm.');
|
|
4781
|
+
}
|
|
4782
|
+
return finishCompact(`Declaration-site survey — **${declarationResult.hitCount}** site(s). **ANSWER NOW** from ids + bindings above; do not Grep the same Type.`);
|
|
4154
4783
|
}
|
|
4155
4784
|
if (controlSyncResult.section) {
|
|
4156
4785
|
return finishCompact(`Control/Toggle state-sync survey — **${controlSyncResult.hitCount}** file(s). **ANSWER NOW** from Manager/Service sites above.`);
|
|
@@ -4161,7 +4790,10 @@ class ToolHandler {
|
|
|
4161
4790
|
: `Kit module **in-repo usage** — **${kitUsageResult.symbolCount}** imported symbol(s). **ANSWER NOW**; do not Grep the same \`@kit\` path.`);
|
|
4162
4791
|
}
|
|
4163
4792
|
if (moduleExportResult.section) {
|
|
4164
|
-
return finishCompact(
|
|
4793
|
+
return finishCompact(moduleExportResult.shellOnly
|
|
4794
|
+
? `NAPI / native export survey — Init/Export shells only (**${moduleExportResult.hitCount}**). `
|
|
4795
|
+
+ '**Partial locator** — Read \`napi_property_descriptor\` / \`napi_define_properties\` next.'
|
|
4796
|
+
: `NAPI / native export survey — **${moduleExportResult.hitCount}** export(s). **ANSWER NOW**; do not Grep napi_define again.`);
|
|
4165
4797
|
}
|
|
4166
4798
|
if (dtsWrapResult.section) {
|
|
4167
4799
|
return finishCompact(`\`.d.ts\` wrap call sites — **${dtsWrapResult.hitCount}** file(s). **ANSWER NOW**; do not Grep the wrap basename again.`);
|
|
@@ -4170,10 +4802,36 @@ class ToolHandler {
|
|
|
4170
4802
|
return finishCompact(`Module dependency / cycle survey — **ANSWER NOW** from the edges above; do not glob every oh-package.`);
|
|
4171
4803
|
}
|
|
4172
4804
|
// Data-source intent must stop even when edges=0 (empty still beats a 20k dump).
|
|
4805
|
+
// Hard ANSWER NOW only when same-file @ohos/@kit imports exist — weak/local
|
|
4806
|
+
// edges previously caused false "system service" answers (BadgeManager → %AM*).
|
|
4173
4807
|
if (dataSourceResult.section && (0, query_utils_1.queryAsDataSourceSurvey)(query)) {
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4808
|
+
if ((0, query_utils_1.queryAsDataSourceDistinguishAsk)(query)) {
|
|
4809
|
+
return finishCompact(dataSourceResult.sdkImportCount > 0
|
|
4810
|
+
? `Data-source survey — **${dataSourceResult.sdkImportCount}** \`@ohos\`/\`@kit\` import(s) as anchors. `
|
|
4811
|
+
+ '**Partial locator** — imports alone do not explain unify/distinguish; Read those Manager bodies next.'
|
|
4812
|
+
: 'Data-source survey — **Partial locator** for unify/distinguish; drill related Managers / narrow Grep next.');
|
|
4813
|
+
}
|
|
4814
|
+
if (dataSourceResult.sdkImportCount > 0) {
|
|
4815
|
+
return finishCompact(`Data-source survey — **${dataSourceResult.sdkImportCount}** \`@ohos\`/\`@kit\` import(s). **ANSWER NOW** from those first.`);
|
|
4816
|
+
}
|
|
4817
|
+
if (dataSourceResult.strongCount > 0) {
|
|
4818
|
+
return finishCompact(`Data-source survey — **${dataSourceResult.strongCount}** related writer/sibling Manager(s). ` +
|
|
4819
|
+
'**Partial locator** — drill those Types next; do not ANSWER NOW from local cache helpers.');
|
|
4820
|
+
}
|
|
4821
|
+
return finishCompact('Data-source survey complete (0 SDK imports). **Partial locator** — use the note above; ' +
|
|
4822
|
+
'ONE narrow Grep/`homegraph_callers` on update* — not a repo-wide storm.');
|
|
4823
|
+
}
|
|
4824
|
+
if (eventDispatchResult.section && (0, query_utils_1.queryAsEventDispatchSurvey)(query)) {
|
|
4825
|
+
if (eventDispatchResult.complete) {
|
|
4826
|
+
return finishCompact(`Event→Manager dispatch inventory — **${eventDispatchResult.memberCount || eventDispatchResult.eventCount}** `
|
|
4827
|
+
+ `type/member(s), **${eventDispatchResult.handlerCount}** handler file(s). **ANSWER NOW** from the list above.`);
|
|
4828
|
+
}
|
|
4829
|
+
return finishCompact(eventDispatchResult.handlerCount > 0 || eventDispatchResult.eventCount > 0
|
|
4830
|
+
? `Event→Manager survey — **Partial locator** (`
|
|
4831
|
+
+ `${eventDispatchResult.memberCount} member(s), ${eventDispatchResult.handlerCount} handler file(s)). `
|
|
4832
|
+
+ 'ONE narrow Grep for members/`case`, or one tighter explore — do **not** ANSWER NOW as a full map; no node fan-out.'
|
|
4833
|
+
: 'Event→Manager survey — 0 event types / handlers indexed. **Partial locator** — do **not** ANSWER NOW; '
|
|
4834
|
+
+ 'ONE Grep for enum members / `case` branches OK.');
|
|
4177
4835
|
}
|
|
4178
4836
|
if (apiUsageResult.fileCount > 0 && !dataSourceResult.section) {
|
|
4179
4837
|
return finishCompact(`API usage survey — **${apiUsageResult.fileCount}** file(s). **ANSWER NOW** from the list above.`);
|
|
@@ -4219,8 +4877,11 @@ class ToolHandler {
|
|
|
4219
4877
|
if (apiUsageResult.fileCount > 0) {
|
|
4220
4878
|
return finishCompact(`API usage survey — **${apiUsageResult.fileCount}** file(s). **ANSWER NOW** from the list above.`);
|
|
4221
4879
|
}
|
|
4222
|
-
if (dataSourceResult.
|
|
4223
|
-
return finishCompact(`Data-source survey — **${dataSourceResult.
|
|
4880
|
+
if (dataSourceResult.sdkImportCount > 0) {
|
|
4881
|
+
return finishCompact(`Data-source survey — **${dataSourceResult.sdkImportCount}** \`@ohos\`/\`@kit\` import(s). **ANSWER NOW.**`);
|
|
4882
|
+
}
|
|
4883
|
+
if (dataSourceResult.strongCount > 0) {
|
|
4884
|
+
return finishCompact(`Data-source survey — **${dataSourceResult.strongCount}** related writer/sibling(s). **Partial locator** — drill next.`);
|
|
4224
4885
|
}
|
|
4225
4886
|
if (inheritanceListed) {
|
|
4226
4887
|
return finishCompact('Inheritance survey above lists all direct subtypes found. **ANSWER NOW** — do not grep `extends`.');
|
|
@@ -4281,6 +4942,7 @@ class ToolHandler {
|
|
|
4281
4942
|
const isTestPath = (p) => /(^|\/)(tests?|spec)\//i.test(p) || /\.(test|spec)\./i.test(p);
|
|
4282
4943
|
const fileNodes = new Map();
|
|
4283
4944
|
const seedIds = new Set();
|
|
4945
|
+
const domainPathTokens = (0, query_utils_1.mechanismDomainPathTokens)(query);
|
|
4284
4946
|
const addNode = (n) => {
|
|
4285
4947
|
if ((0, arkts_1.isOhosApiFilePath)(n.filePath))
|
|
4286
4948
|
return;
|
|
@@ -4294,6 +4956,10 @@ class ToolHandler {
|
|
|
4294
4956
|
list.push(n);
|
|
4295
4957
|
fileNodes.set(n.filePath, list);
|
|
4296
4958
|
};
|
|
4959
|
+
// 1) Domain Manager/Service inventory first (static AA class nodes) — locator job.
|
|
4960
|
+
const managerHits = this.collectDomainRoleSymbols(cg, query, domainPathTokens);
|
|
4961
|
+
for (const n of managerHits)
|
|
4962
|
+
addNode(n);
|
|
4297
4963
|
const domainTermsEarly = (0, query_utils_1.extractDomainSearchTerms)(query);
|
|
4298
4964
|
const distinctiveEarly = domainTermsEarly.filter((t) => /^[\x00-\x7F]+$/.test(t) && !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(t.toLowerCase()));
|
|
4299
4965
|
// Distinctive-token IMPORT seeds first — `@ohos.convertxml` / `XmlParse*` before
|
|
@@ -4323,7 +4989,7 @@ class ToolHandler {
|
|
|
4323
4989
|
}
|
|
4324
4990
|
}
|
|
4325
4991
|
catch { /* optional */ }
|
|
4326
|
-
if (seedIds.size >=
|
|
4992
|
+
if (seedIds.size >= 20)
|
|
4327
4993
|
break;
|
|
4328
4994
|
}
|
|
4329
4995
|
}
|
|
@@ -4347,8 +5013,6 @@ class ToolHandler {
|
|
|
4347
5013
|
}
|
|
4348
5014
|
if (seedIds.size < 2 || distinctiveEarly.length > 0) {
|
|
4349
5015
|
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
5016
|
const distinctive = distinctiveEarly;
|
|
4353
5017
|
const ranked = distinctive.length > 0
|
|
4354
5018
|
? [
|
|
@@ -4382,12 +5046,8 @@ class ToolHandler {
|
|
|
4382
5046
|
const nameHit = nameLc.includes(termLc);
|
|
4383
5047
|
const pathHit = pathLc.includes(termLc);
|
|
4384
5048
|
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
5049
|
if (termIsVerb && !pathHit)
|
|
4388
5050
|
continue;
|
|
4389
|
-
// Distinctive token: require name/path/signature to actually mention it
|
|
4390
|
-
// (drops FTS neighbors that only share an unrelated Parser suffix).
|
|
4391
5051
|
if (!termIsVerb && distinctive.length > 0 && !nameHit && !pathHit && !sigHit)
|
|
4392
5052
|
continue;
|
|
4393
5053
|
const entryOk = r.node.kind === 'import'
|
|
@@ -4397,29 +5057,34 @@ class ToolHandler {
|
|
|
4397
5057
|
if (!entryOk)
|
|
4398
5058
|
continue;
|
|
4399
5059
|
addNode(r.node);
|
|
4400
|
-
if (seedIds.size >=
|
|
5060
|
+
if (seedIds.size >= 24)
|
|
4401
5061
|
break;
|
|
4402
5062
|
}
|
|
4403
|
-
if (seedIds.size >=
|
|
5063
|
+
if (seedIds.size >= 16)
|
|
4404
5064
|
break;
|
|
4405
5065
|
}
|
|
4406
|
-
// Prefer seeds that mention a distinctive token when we have any.
|
|
4407
5066
|
if (distinctive.length > 0 && seedIds.size > 0) {
|
|
4408
5067
|
const keep = new Set();
|
|
4409
5068
|
for (const id of seedIds) {
|
|
4410
5069
|
const n = cg.getNode(id);
|
|
4411
5070
|
if (!n)
|
|
4412
5071
|
continue;
|
|
5072
|
+
// Always keep domain Manager/Service hits — they are the locator answer.
|
|
5073
|
+
if ((0, query_utils_1.isDomainRoleSymbol)(n.name, n.filePath, domainPathTokens)) {
|
|
5074
|
+
keep.add(id);
|
|
5075
|
+
continue;
|
|
5076
|
+
}
|
|
4413
5077
|
const line = n.kind === 'import' ? (0, query_utils_1.resolveImportLineFromNode)(n, projectRoot) : '';
|
|
4414
5078
|
const blob = `${n.name}\n${n.filePath}\n${n.signature || ''}\n${line}`.toLowerCase();
|
|
4415
5079
|
if (distinctive.some((t) => blob.includes(t.toLowerCase())))
|
|
4416
5080
|
keep.add(id);
|
|
5081
|
+
if (domainPathTokens.some((t) => blob.includes(t)))
|
|
5082
|
+
keep.add(id);
|
|
4417
5083
|
}
|
|
4418
5084
|
if (keep.size > 0) {
|
|
4419
5085
|
for (const id of [...seedIds]) {
|
|
4420
|
-
if (!keep.has(id))
|
|
5086
|
+
if (!keep.has(id))
|
|
4421
5087
|
seedIds.delete(id);
|
|
4422
|
-
}
|
|
4423
5088
|
}
|
|
4424
5089
|
for (const [fp, nodes] of [...fileNodes.entries()]) {
|
|
4425
5090
|
const filtered = nodes.filter((n) => keep.has(n.id));
|
|
@@ -4435,18 +5100,33 @@ class ToolHandler {
|
|
|
4435
5100
|
return null;
|
|
4436
5101
|
const seeds = (0, query_utils_1.extractMechanismEntrySeeds)(query);
|
|
4437
5102
|
const flow = this.buildFlowFromNamedSymbols(cg, `${query} ${seeds.join(' ')}`);
|
|
5103
|
+
const hasFlowPath = flow.pathNodeIds.size > 1;
|
|
5104
|
+
const managerSection = this.formatDomainRoleInventory(managerHits.length > 0
|
|
5105
|
+
? managerHits
|
|
5106
|
+
: [...seedIds].map((id) => cg.getNode(id)).filter((n) => !!n && (0, query_utils_1.isDomainRoleSymbol)(n.name, n.filePath, domainPathTokens)));
|
|
5107
|
+
const hasStrongManager = managerSection.strongCount > 0;
|
|
5108
|
+
// Inventory without a connected spine is a locator — hard ANSWER NOW caused
|
|
5109
|
+
// Partial→re-explore / Grep storms after a correct first anchor.
|
|
5110
|
+
const mechanismClosed = hasFlowPath && (hasStrongManager || seeds.length >= 2);
|
|
4438
5111
|
const lines = [
|
|
4439
5112
|
`**Exploration: ${query}**`,
|
|
4440
5113
|
'',
|
|
4441
|
-
|
|
5114
|
+
mechanismClosed
|
|
5115
|
+
? '> **ANSWER NOW from this response** when the Manager inventory + mechanism spine cover the ask. '
|
|
5116
|
+
+ 'Do **not** Grep/Read listed files in parallel — Source below is authoritative.'
|
|
5117
|
+
: '> **Partial locator** — Manager / domain inventory below are anchors, not a full mechanism closure. '
|
|
5118
|
+
+ 'Pick **ONE** concrete `*Manager` / next-hop name from the list for a tighter explore — '
|
|
5119
|
+
+ 'do **not** re-explore a paraphrase of the same keyword bag. '
|
|
5120
|
+
+ 'At most one `homegraph_node` after Partial; prefer ONE narrow Grep for unlisted wiring — not a repo storm. '
|
|
5121
|
+
+ 'Do **not** ANSWER NOW yet.',
|
|
4442
5122
|
'',
|
|
4443
5123
|
`Mechanism anchors: **${seedIds.size}** symbol(s) — lightweight explore (seed + flow spine).`,
|
|
4444
5124
|
'',
|
|
4445
5125
|
];
|
|
4446
|
-
|
|
4447
|
-
|
|
5126
|
+
if (managerSection.text)
|
|
5127
|
+
lines.push(managerSection.text);
|
|
4448
5128
|
const seedImportFocus = (0, query_utils_1.extractMechanismEntrySeeds)(query).filter((s) => !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(s.toLowerCase())
|
|
4449
|
-
&&
|
|
5129
|
+
&& s.length >= 5);
|
|
4450
5130
|
const distinctiveDeps = (0, query_utils_1.extractDependencySymbolsFromQuery)(query)
|
|
4451
5131
|
.filter((s) => !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(s.toLowerCase()));
|
|
4452
5132
|
const importQuery = (seedImportFocus.length > 0
|
|
@@ -4454,19 +5134,19 @@ class ToolHandler {
|
|
|
4454
5134
|
: distinctiveDeps.length > 0
|
|
4455
5135
|
? distinctiveDeps
|
|
4456
5136
|
: (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(' ');
|
|
5137
|
+
.filter((t) => /^[\x00-\x7F]+$/.test(t) && !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(t.toLowerCase()) && t.length >= 4)).slice(0, 4).join(' ');
|
|
4458
5138
|
const importResult = importQuery
|
|
4459
5139
|
? this.buildImportSitesSection(cg, importQuery, projectRoot)
|
|
4460
5140
|
: { section: '', siteCount: 0, compactListing: false };
|
|
4461
|
-
// Light path: keep ≤8 import bullets (full inventory belongs to kit/API surveys).
|
|
4462
5141
|
if (importResult.section) {
|
|
4463
|
-
const trimmed = this.trimLightImportSection(importResult.section,
|
|
5142
|
+
const trimmed = this.trimLightImportSection(importResult.section, 5);
|
|
4464
5143
|
if (trimmed)
|
|
4465
5144
|
lines.push(trimmed);
|
|
4466
5145
|
}
|
|
4467
|
-
if (flow.text)
|
|
5146
|
+
if (flow.text) {
|
|
5147
|
+
lines.push('**Mechanism spine**', '');
|
|
4468
5148
|
lines.push(flow.text);
|
|
4469
|
-
|
|
5149
|
+
}
|
|
4470
5150
|
if ((0, query_utils_1.queryAsNativeRenderThreadSurvey)(query)) {
|
|
4471
5151
|
const cmakeHits = [];
|
|
4472
5152
|
try {
|
|
@@ -4474,12 +5154,6 @@ class ToolHandler {
|
|
|
4474
5154
|
const fp = f.path.replace(/\\/g, '/');
|
|
4475
5155
|
if (!/CMakeLists\.txt$/i.test(fp))
|
|
4476
5156
|
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
5157
|
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, f.path);
|
|
4484
5158
|
if (!abs)
|
|
4485
5159
|
continue;
|
|
@@ -4510,27 +5184,38 @@ class ToolHandler {
|
|
|
4510
5184
|
lines.push('');
|
|
4511
5185
|
}
|
|
4512
5186
|
}
|
|
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)
|
|
5187
|
+
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 >= 4);
|
|
5188
|
+
const apiSeeds = (0, query_utils_1.extractMechanismEntrySeeds)(query)
|
|
5189
|
+
.map((s) => s.toLowerCase())
|
|
5190
|
+
.filter((s) => s.length >= 5 && !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(s));
|
|
4515
5191
|
const fileScores = new Map();
|
|
4516
5192
|
for (const [fp, nodes] of fileNodes) {
|
|
4517
5193
|
if ((0, arkts_1.isOhosApiFilePath)(fp))
|
|
4518
5194
|
continue;
|
|
5195
|
+
if ((0, query_utils_1.isTestFile)(fp))
|
|
5196
|
+
continue;
|
|
4519
5197
|
let score = 0;
|
|
4520
|
-
const fpLc = fp.toLowerCase();
|
|
5198
|
+
const fpLc = fp.toLowerCase().replace(/\\/g, '/');
|
|
4521
5199
|
if (distinctiveForScore.some((t) => fpLc.includes(t.toLowerCase())))
|
|
4522
5200
|
score += 30;
|
|
4523
|
-
|
|
4524
|
-
if (apiSeeds.some((s) => s.length >= 6 && fpLc.includes(s)))
|
|
5201
|
+
if (apiSeeds.some((s) => fpLc.includes(s)))
|
|
4525
5202
|
score += 40;
|
|
4526
|
-
|
|
4527
|
-
|
|
5203
|
+
for (const t of domainPathTokens) {
|
|
5204
|
+
if (t.length >= 4 && fpLc.includes(`/${t}/`))
|
|
5205
|
+
score += 60;
|
|
5206
|
+
if (t.length >= 4 && fpLc.includes(t))
|
|
5207
|
+
score += 20;
|
|
5208
|
+
}
|
|
4528
5209
|
for (const n of nodes) {
|
|
4529
5210
|
const line = n.kind === 'import' ? (0, query_utils_1.resolveImportLineFromNode)(n, projectRoot) : '';
|
|
4530
5211
|
const blob = `${n.name}\n${n.signature || ''}\n${line}`.toLowerCase();
|
|
5212
|
+
if ((0, query_utils_1.isDomainRoleSymbol)(n.name, n.filePath, domainPathTokens))
|
|
5213
|
+
score += 80;
|
|
5214
|
+
if (/Manager|Subscriber|Listener$/i.test(n.name) && /\/manager\//i.test(fpLc))
|
|
5215
|
+
score += 40;
|
|
4531
5216
|
if (distinctiveForScore.some((t) => blob.includes(t.toLowerCase())))
|
|
4532
5217
|
score += 20;
|
|
4533
|
-
if (apiSeeds.some((s) =>
|
|
5218
|
+
if (apiSeeds.some((s) => blob.includes(s)))
|
|
4534
5219
|
score += 25;
|
|
4535
5220
|
if (n.kind === 'import'
|
|
4536
5221
|
&& distinctiveForScore.some((t) => {
|
|
@@ -4539,7 +5224,7 @@ class ToolHandler {
|
|
|
4539
5224
|
})) {
|
|
4540
5225
|
score += 20;
|
|
4541
5226
|
}
|
|
4542
|
-
if (
|
|
5227
|
+
if (/@kit\.|@ohos\./i.test(line))
|
|
4543
5228
|
score += 30;
|
|
4544
5229
|
if (flow.pathNodeIds.has(n.id))
|
|
4545
5230
|
score += 20;
|
|
@@ -4548,57 +5233,278 @@ class ToolHandler {
|
|
|
4548
5233
|
else
|
|
4549
5234
|
score += 5;
|
|
4550
5235
|
}
|
|
5236
|
+
// Demote incidental restore/settings hits when a domain path token exists.
|
|
5237
|
+
if (domainPathTokens.length > 0 && /restore|settings|statusbar/i.test(fpLc)
|
|
5238
|
+
&& !domainPathTokens.some((t) => fpLc.includes(`/${t}/`))) {
|
|
5239
|
+
score = Math.max(0, score - 40);
|
|
5240
|
+
}
|
|
4551
5241
|
fileScores.set(fp, score);
|
|
4552
5242
|
}
|
|
4553
5243
|
const sortedFiles = [...fileScores.entries()]
|
|
4554
5244
|
.filter(([, s]) => s > 0)
|
|
4555
5245
|
.sort((a, b) => b[1] - a[1])
|
|
4556
5246
|
.slice(0, 3);
|
|
4557
|
-
|
|
4558
|
-
|
|
5247
|
+
// Prefer digests for the best domain-role Type (not import-site top file).
|
|
5248
|
+
const nextMgr = (0, explore_repeat_guard_1.pickBestDomainRoleAnchor)(managerHits.map((n) => ({
|
|
5249
|
+
name: n.name,
|
|
5250
|
+
kind: n.kind,
|
|
5251
|
+
filePath: n.filePath,
|
|
5252
|
+
startLine: n.startLine,
|
|
5253
|
+
})), query, domainPathTokens);
|
|
5254
|
+
if (nextMgr?.filePath) {
|
|
5255
|
+
const pref = nextMgr.filePath;
|
|
5256
|
+
const rest = sortedFiles.filter(([fp]) => fp !== pref);
|
|
5257
|
+
const prefScore = fileScores.get(pref) ?? 100;
|
|
5258
|
+
sortedFiles.length = 0;
|
|
5259
|
+
sortedFiles.push([pref, prefScore], ...rest.slice(0, 2));
|
|
5260
|
+
}
|
|
5261
|
+
const topIsStrong = sortedFiles.some(([fp, s]) => {
|
|
5262
|
+
if (s < 80)
|
|
5263
|
+
return false;
|
|
5264
|
+
const nodes = fileNodes.get(fp) ?? [];
|
|
5265
|
+
return nodes.some((n) => (0, query_utils_1.isDomainRoleSymbol)(n.name, n.filePath, domainPathTokens));
|
|
5266
|
+
});
|
|
5267
|
+
lines.push('**Source digests**', '');
|
|
5268
|
+
lines.push('> Spine / top-hit digests — treat as already Read for these symbols. '
|
|
5269
|
+
+ 'ONE narrow Grep/Read only for residual unindexed wiring; do not dump these files again.');
|
|
4559
5270
|
lines.push('');
|
|
5271
|
+
// Anchor table before digests (universal mid-lean shape).
|
|
5272
|
+
if (sortedFiles.length > 0) {
|
|
5273
|
+
lines.push('**Anchors**', '');
|
|
5274
|
+
for (const [fp] of sortedFiles.slice(0, 6)) {
|
|
5275
|
+
const nodes = fileNodes.get(fp) ?? [];
|
|
5276
|
+
const names = nodes
|
|
5277
|
+
.filter((n) => n.kind !== 'import' && n.kind !== 'export')
|
|
5278
|
+
.slice(0, 4)
|
|
5279
|
+
.map((n) => `\`${n.name}\`(${n.kind}${n.startLine ? `:${n.startLine}` : ''})`)
|
|
5280
|
+
.join(', ');
|
|
5281
|
+
lines.push(`- \`${fp}\`${names ? ` — ${names}` : ''}`);
|
|
5282
|
+
}
|
|
5283
|
+
lines.push('');
|
|
5284
|
+
}
|
|
4560
5285
|
let totalChars = lines.join('\n').length;
|
|
4561
5286
|
let filesRendered = 0;
|
|
5287
|
+
const digest = exploreLocatorDigestEnabled();
|
|
5288
|
+
const perFileCap = digest ? 2800 : 2000;
|
|
5289
|
+
const totalCap = digest ? 6500 : 4_800;
|
|
4562
5290
|
for (const [fp] of sortedFiles) {
|
|
4563
|
-
|
|
4564
|
-
if (filesRendered >= 2 || totalChars > 4_800)
|
|
5291
|
+
if (filesRendered >= 2 || totalChars > totalCap)
|
|
4565
5292
|
break;
|
|
4566
|
-
const chunk = this.renderLightMechanismSource(projectRoot, fp, fileNodes.get(fp) ?? [],
|
|
5293
|
+
const chunk = this.renderLightMechanismSource(projectRoot, fp, fileNodes.get(fp) ?? [], perFileCap);
|
|
4567
5294
|
if (!chunk)
|
|
4568
5295
|
continue;
|
|
4569
5296
|
lines.push(chunk);
|
|
4570
5297
|
totalChars += chunk.length;
|
|
4571
5298
|
filesRendered++;
|
|
4572
5299
|
}
|
|
4573
|
-
if (filesRendered === 0 && !importResult.section && !flow.text)
|
|
5300
|
+
if (filesRendered === 0 && !importResult.section && !flow.text && !managerSection.text)
|
|
4574
5301
|
return null;
|
|
5302
|
+
// Soft-close when inventory + at least one digest exist — enough to answer
|
|
5303
|
+
// with ONE narrow Grep; avoid Partial→re-explore churn.
|
|
5304
|
+
const digestCoversNext = !!(nextMgr && sortedFiles.some(([fp]) => fp === nextMgr.filePath)
|
|
5305
|
+
&& filesRendered > 0);
|
|
5306
|
+
const softClose = hasStrongManager && filesRendered >= 1
|
|
5307
|
+
&& (digestCoversNext || managerHits.length >= 2 || topIsStrong);
|
|
5308
|
+
const closedAnswer = softClose || mechanismClosed || (hasFlowPath && topIsStrong);
|
|
4575
5309
|
lines.push('---');
|
|
4576
|
-
|
|
4577
|
-
'
|
|
4578
|
-
|
|
5310
|
+
if (mechanismClosed || (hasFlowPath && topIsStrong)) {
|
|
5311
|
+
lines.push('> **Mechanism explore complete — ANSWER NOW** from the Manager inventory + spine + digests above. '
|
|
5312
|
+
+ 'Do **not** call `homegraph_node`, Read, or Grep for symbols/files already shown. '
|
|
5313
|
+
+ 'Only one tighter `homegraph_explore` if a **named** Type essential to the answer is still missing.');
|
|
5314
|
+
}
|
|
5315
|
+
else if (softClose) {
|
|
5316
|
+
lines.push((0, explore_repeat_guard_1.formatMechanismSoftCloseFooter)(nextMgr));
|
|
5317
|
+
}
|
|
5318
|
+
else {
|
|
5319
|
+
const topFp = sortedFiles[0]?.[0];
|
|
5320
|
+
const topNodes = topFp ? (fileNodes.get(topFp) ?? []) : [];
|
|
5321
|
+
const topSym = topNodes.find((n) => n.kind !== 'import' && n.kind !== 'export' && n.startLine > 0)
|
|
5322
|
+
?? topNodes[0];
|
|
5323
|
+
const nextCaption = nextMgr
|
|
5324
|
+
? (0, explore_repeat_guard_1.formatNextAnchorCaption)(nextMgr)
|
|
5325
|
+
: topSym
|
|
5326
|
+
? (0, explore_repeat_guard_1.formatNextAnchorCaption)({
|
|
5327
|
+
name: topSym.name,
|
|
5328
|
+
kind: topSym.kind,
|
|
5329
|
+
filePath: topSym.filePath,
|
|
5330
|
+
startLine: topSym.startLine,
|
|
5331
|
+
})
|
|
5332
|
+
: topFp
|
|
5333
|
+
? `\`${topFp}\``
|
|
5334
|
+
: 'the top Manager / file in the inventory above';
|
|
5335
|
+
lines.push((0, explore_repeat_guard_1.formatMechanismPartialFooter)(nextCaption));
|
|
5336
|
+
}
|
|
5337
|
+
// Opening banner is emitted before digests; rewrite so soft-close never
|
|
5338
|
+
// pairs a Partial header with an ANSWER footer (agent Grep-storm trigger).
|
|
5339
|
+
if (closedAnswer) {
|
|
5340
|
+
const openIdx = lines.findIndex((l) => l.includes('**Partial locator**') || l.includes('**ANSWER NOW from this response**'));
|
|
5341
|
+
if (openIdx >= 0) {
|
|
5342
|
+
lines[openIdx] = softClose && !(mechanismClosed || (hasFlowPath && topIsStrong))
|
|
5343
|
+
? (0, explore_repeat_guard_1.formatMechanismSoftCloseHeader)()
|
|
5344
|
+
: ('> **ANSWER NOW from this response** when the Manager inventory + mechanism spine cover the ask. '
|
|
5345
|
+
+ 'Do **not** Grep/Read listed files in parallel — Source below is authoritative.');
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
4579
5348
|
lines.push('');
|
|
4580
|
-
|
|
5349
|
+
const text = lines.join('\n');
|
|
5350
|
+
return this.exploreResult(text, {
|
|
5351
|
+
projectRoot,
|
|
5352
|
+
query,
|
|
5353
|
+
files: [],
|
|
5354
|
+
sourceBytes: 0,
|
|
5355
|
+
responseBytes: text.length,
|
|
5356
|
+
// Soft-close must not trip Partial→depth fuse / next-anchor churn.
|
|
5357
|
+
partial: closedAnswer ? false : undefined,
|
|
5358
|
+
nextAnchor: closedAnswer ? undefined : (nextMgr?.name),
|
|
5359
|
+
});
|
|
5360
|
+
}
|
|
5361
|
+
/**
|
|
5362
|
+
* Static AA class/interface nodes that look like domain Managers/Services.
|
|
5363
|
+
*/
|
|
5364
|
+
collectDomainRoleSymbols(cg, query, domainTokens) {
|
|
5365
|
+
const tokens = domainTokens.length > 0
|
|
5366
|
+
? domainTokens
|
|
5367
|
+
: (0, query_utils_1.mechanismDomainPathTokens)(query);
|
|
5368
|
+
if (tokens.length === 0)
|
|
5369
|
+
return [];
|
|
5370
|
+
const byId = new Map();
|
|
5371
|
+
const tryAdd = (n) => {
|
|
5372
|
+
if ((0, arkts_1.isOhosApiFilePath)(n.filePath) || (0, query_utils_1.isTestFile)(n.filePath))
|
|
5373
|
+
return;
|
|
5374
|
+
if (n.kind !== 'class' && n.kind !== 'struct' && n.kind !== 'interface' && n.kind !== 'component')
|
|
5375
|
+
return;
|
|
5376
|
+
if (!(0, query_utils_1.isDomainRoleSymbol)(n.name, n.filePath, tokens))
|
|
5377
|
+
return;
|
|
5378
|
+
byId.set(n.id, n);
|
|
5379
|
+
};
|
|
5380
|
+
for (const seed of (0, query_utils_1.extractMechanismEntrySeeds)(query)) {
|
|
5381
|
+
if (seed.startsWith('@'))
|
|
5382
|
+
continue;
|
|
5383
|
+
try {
|
|
5384
|
+
for (const n of cg.getNodesByName(seed))
|
|
5385
|
+
tryAdd(n);
|
|
5386
|
+
}
|
|
5387
|
+
catch { /* */ }
|
|
5388
|
+
}
|
|
5389
|
+
for (const t of tokens.slice(0, 6)) {
|
|
5390
|
+
let hits = [];
|
|
5391
|
+
try {
|
|
5392
|
+
hits = cg.searchNodes(t, { kinds: ['class', 'struct', 'interface', 'component'], limit: 30 });
|
|
5393
|
+
}
|
|
5394
|
+
catch {
|
|
5395
|
+
continue;
|
|
5396
|
+
}
|
|
5397
|
+
for (const r of hits)
|
|
5398
|
+
tryAdd(r.node);
|
|
5399
|
+
}
|
|
5400
|
+
// Also probe Manager suffix via FTS when a domain token is present.
|
|
5401
|
+
for (const suffix of ['Manager', 'Subscriber', 'Listener']) {
|
|
5402
|
+
let hits = [];
|
|
5403
|
+
try {
|
|
5404
|
+
hits = cg.searchNodes(suffix, { kinds: ['class', 'interface', 'component'], limit: 40 });
|
|
5405
|
+
}
|
|
5406
|
+
catch {
|
|
5407
|
+
continue;
|
|
5408
|
+
}
|
|
5409
|
+
for (const r of hits) {
|
|
5410
|
+
if (tokens.some((t) => {
|
|
5411
|
+
const tl = t.toLowerCase();
|
|
5412
|
+
const blob = `${r.node.name}\n${r.node.filePath}`.toLowerCase();
|
|
5413
|
+
return blob.includes(tl);
|
|
5414
|
+
})) {
|
|
5415
|
+
tryAdd(r.node);
|
|
5416
|
+
}
|
|
5417
|
+
}
|
|
5418
|
+
}
|
|
5419
|
+
const themeFocus = tokens.some((t) => /theme/i.test(t));
|
|
5420
|
+
return [...byId.values()]
|
|
5421
|
+
.filter((n) => {
|
|
5422
|
+
if (!themeFocus)
|
|
5423
|
+
return true;
|
|
5424
|
+
const blob = `${n.name}\n${n.filePath}`.toLowerCase().replace(/\\/g, '/');
|
|
5425
|
+
// Theme-pack questions: drop app-install / desktop-download Managers.
|
|
5426
|
+
if (/themeservice|themepack|\/theme|themepackparser|themeactivate|themeinfo|themevip/i.test(blob)) {
|
|
5427
|
+
return true;
|
|
5428
|
+
}
|
|
5429
|
+
if (/appinstall|installermanager|installdatamanager|installevent|downloadaction/i.test(blob)) {
|
|
5430
|
+
return false;
|
|
5431
|
+
}
|
|
5432
|
+
return true;
|
|
5433
|
+
})
|
|
5434
|
+
.sort((a, b) => {
|
|
5435
|
+
const score = (n) => {
|
|
5436
|
+
let s = 0;
|
|
5437
|
+
const fp = n.filePath.toLowerCase().replace(/\\/g, '/');
|
|
5438
|
+
if (/\/manager\//i.test(fp))
|
|
5439
|
+
s += 5;
|
|
5440
|
+
if (/Subscribe|Listener|Bridge/i.test(n.name))
|
|
5441
|
+
s += 3;
|
|
5442
|
+
if (/Manager$/i.test(n.name))
|
|
5443
|
+
s += 2;
|
|
5444
|
+
for (const t of tokens) {
|
|
5445
|
+
if (fp.includes(`/${t}/`))
|
|
5446
|
+
s += 8;
|
|
5447
|
+
if (n.name.toLowerCase().includes(t))
|
|
5448
|
+
s += 4;
|
|
5449
|
+
}
|
|
5450
|
+
if (themeFocus) {
|
|
5451
|
+
if (/themeservice|themepack/i.test(fp) || /ThemePack|ThemePackage|ThemeActivate|ThemeInfo/i.test(n.name)) {
|
|
5452
|
+
s += 25;
|
|
5453
|
+
}
|
|
5454
|
+
if (/appinstall|bigfolder.*download/i.test(fp) && !/theme/i.test(fp + n.name)) {
|
|
5455
|
+
s -= 20;
|
|
5456
|
+
}
|
|
5457
|
+
}
|
|
5458
|
+
return s;
|
|
5459
|
+
};
|
|
5460
|
+
return score(b) - score(a) || a.name.localeCompare(b.name);
|
|
5461
|
+
})
|
|
5462
|
+
.slice(0, 12);
|
|
4581
5463
|
}
|
|
4582
|
-
|
|
5464
|
+
formatDomainRoleInventory(nodes) {
|
|
5465
|
+
if (nodes.length === 0)
|
|
5466
|
+
return { text: '', strongCount: 0 };
|
|
5467
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
5468
|
+
const lines = [
|
|
5469
|
+
'**Domain Manager / Service inventory** (static index — locator anchors)',
|
|
5470
|
+
'',
|
|
5471
|
+
'> Prefer these Types for the next explore or the answer. Do not Grep the domain noun across the repo.',
|
|
5472
|
+
'',
|
|
5473
|
+
];
|
|
5474
|
+
for (const n of nodes.slice(0, 10)) {
|
|
5475
|
+
lines.push(`- \`${n.name}\` (${n.kind}) — \`${rel(n.filePath)}:${n.startLine}\``);
|
|
5476
|
+
}
|
|
5477
|
+
if (nodes.length > 10)
|
|
5478
|
+
lines.push(`- … and ${nodes.length - 10} more`);
|
|
5479
|
+
lines.push('');
|
|
5480
|
+
return { text: lines.join('\n'), strongCount: nodes.length };
|
|
5481
|
+
}
|
|
5482
|
+
/** Cap import bullets inside a light-mechanism response; prefer non-test paths. */
|
|
4583
5483
|
trimLightImportSection(section, maxBullets) {
|
|
4584
5484
|
const lines = section.split('\n');
|
|
4585
|
-
const
|
|
4586
|
-
|
|
4587
|
-
|
|
5485
|
+
const header = [];
|
|
5486
|
+
const prod = [];
|
|
5487
|
+
const test = [];
|
|
4588
5488
|
for (const line of lines) {
|
|
4589
5489
|
if (line.startsWith('- `')) {
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
5490
|
+
const pathMatch = line.match(/^- `([^`]+)`/);
|
|
5491
|
+
const fp = pathMatch?.[1]?.split(':')[0] ?? '';
|
|
5492
|
+
if (fp && (0, query_utils_1.isTestFile)(fp))
|
|
5493
|
+
test.push(line);
|
|
5494
|
+
else
|
|
5495
|
+
prod.push(line);
|
|
4595
5496
|
}
|
|
4596
|
-
if (/^\s*- … and \d+ more/i.test(line))
|
|
5497
|
+
else if (/^\s*- … and \d+ more/i.test(line)) {
|
|
4597
5498
|
continue;
|
|
4598
|
-
|
|
5499
|
+
}
|
|
5500
|
+
else {
|
|
5501
|
+
header.push(line);
|
|
5502
|
+
}
|
|
4599
5503
|
}
|
|
4600
|
-
|
|
4601
|
-
|
|
5504
|
+
const chosen = [...prod, ...test].slice(0, maxBullets);
|
|
5505
|
+
const out = [...header, ...chosen];
|
|
5506
|
+
if (prod.length + test.length > maxBullets) {
|
|
5507
|
+
out.push(`- … (light-mechanism: showing ${maxBullets} sites, tests deprioritized — **ANSWER NOW**, do not Grep the same import)`);
|
|
4602
5508
|
}
|
|
4603
5509
|
return out.join('\n');
|
|
4604
5510
|
}
|
|
@@ -4892,16 +5798,104 @@ class ToolHandler {
|
|
|
4892
5798
|
// skip edge trail for this seed
|
|
4893
5799
|
}
|
|
4894
5800
|
}
|
|
5801
|
+
// Prefer dropping SDK `.d.ts` seeds entirely when any in-repo impl exists for
|
|
5802
|
+
// the same name; if remaining source files are stub-only, soften ANSWER NOW.
|
|
5803
|
+
// Named Type that only exists as stub → soft even when NL leftovers add project seeds (D59).
|
|
5804
|
+
const isStubPath = (fp) => (0, arkts_1.isOhosApiFilePath)(fp) || /\.d\.ts$/i.test(fp);
|
|
5805
|
+
let stubSeedCount = 0;
|
|
5806
|
+
let projectSeedCount = 0;
|
|
5807
|
+
for (const id of seedIds) {
|
|
5808
|
+
try {
|
|
5809
|
+
const n = cg.getNode(id);
|
|
5810
|
+
if (!n)
|
|
5811
|
+
continue;
|
|
5812
|
+
if (isStubPath(n.filePath))
|
|
5813
|
+
stubSeedCount++;
|
|
5814
|
+
else
|
|
5815
|
+
projectSeedCount++;
|
|
5816
|
+
}
|
|
5817
|
+
catch { /* */ }
|
|
5818
|
+
}
|
|
5819
|
+
const primaryNames = (0, query_utils_1.extractTypeNamesFromQuery)(query).filter((t) => !(0, query_utils_1.isFrameworkUiDecoratorName)(t));
|
|
5820
|
+
let primaryStubOnly = false;
|
|
5821
|
+
for (const name of primaryNames.slice(0, 4)) {
|
|
5822
|
+
let nodes = [];
|
|
5823
|
+
try {
|
|
5824
|
+
nodes = cg.getNodesByName(name).filter((n) => !(0, query_utils_1.isTestFile)(n.filePath));
|
|
5825
|
+
}
|
|
5826
|
+
catch {
|
|
5827
|
+
continue;
|
|
5828
|
+
}
|
|
5829
|
+
if (nodes.length === 0)
|
|
5830
|
+
continue;
|
|
5831
|
+
if (nodes.every((n) => isStubPath(n.filePath))) {
|
|
5832
|
+
primaryStubOnly = true;
|
|
5833
|
+
break;
|
|
5834
|
+
}
|
|
5835
|
+
}
|
|
5836
|
+
// Drop stub bodies from the source dump when a project def of the same name exists.
|
|
5837
|
+
// If the *named* Type is stub-only, keep soft framing even if other project seeds remain.
|
|
5838
|
+
const projectFilesPresent = [...fileNodes.keys()].some((fp) => !isStubPath(fp));
|
|
5839
|
+
if (projectFilesPresent && !primaryStubOnly) {
|
|
5840
|
+
for (const fp of [...fileNodes.keys()]) {
|
|
5841
|
+
if (isStubPath(fp))
|
|
5842
|
+
fileNodes.delete(fp);
|
|
5843
|
+
}
|
|
5844
|
+
}
|
|
5845
|
+
const stubHeavy = primaryStubOnly
|
|
5846
|
+
|| (stubSeedCount > 0 && projectSeedCount === 0)
|
|
5847
|
+
|| (fileNodes.size > 0 && [...fileNodes.keys()].every((fp) => isStubPath(fp)));
|
|
5848
|
+
const implAsk = /状态机|state\s*machine|foreground|background|回调|callback|转换|trigger/i.test(query);
|
|
5849
|
+
// Property / resource provenance ($r vs download) — compact body alone is rarely enough.
|
|
5850
|
+
const resourceProvenanceAsk = /\$r\b|系统资源|网络下载|resource\s+vs|vs\.?\s*network|download\s+or|preview|预览|图片加载|loadImage|PixelMap|Image\s*\(/i.test(query);
|
|
5851
|
+
// Multi-Type bags without a connected flow are coarse locators — never fake-complete.
|
|
5852
|
+
const multiTypeThin = typeNames.length >= 2
|
|
5853
|
+
&& !(0, query_utils_1.queryHasNamedMemberFocus)(query)
|
|
5854
|
+
&& !(0, query_utils_1.queryAsNamedComponentAction)(query);
|
|
4895
5855
|
const lines = [
|
|
4896
5856
|
`**Exploration: ${query}**`,
|
|
4897
5857
|
'',
|
|
4898
|
-
|
|
5858
|
+
stubHeavy
|
|
5859
|
+
? '> **Partial locator** — indexed hits are SDK / `.d.ts` stubs, not in-repo implementations. '
|
|
5860
|
+
+ 'Prefer `homegraph_search` / ONE explore for `*State*` / impl class names; '
|
|
5861
|
+
+ 'ONE narrow Grep for in-repo impl names is OK — do not ANSWER NOW from the stub alone.'
|
|
5862
|
+
: resourceProvenanceAsk
|
|
5863
|
+
? '> **Partial locator** — component shells below are anchors. '
|
|
5864
|
+
+ 'Confirm `$r` / Image / download call sites in the dialog body (or ONE narrow Grep for `$r` / `http`) before ANSWER NOW.'
|
|
5865
|
+
: multiTypeThin
|
|
5866
|
+
? '> **Partial locator** — multi-Type bag below is a coarse anchor set. '
|
|
5867
|
+
+ '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.'
|
|
5868
|
+
: '> **Coarse locate complete.** Treat Source + trail as already Read for these symbols. '
|
|
5869
|
+
+ 'Answer from them; do **not** re-Grep/Read/node the same symbols. '
|
|
5870
|
+
+ 'ONE narrow Grep is OK only for residual literals / unindexed wiring — not a repo-wide storm.',
|
|
4899
5871
|
'',
|
|
4900
5872
|
`Local-symbol focus: **${seedIds.size}** seed(s)` +
|
|
4901
5873
|
(neighborIds.size > 0 ? `, **${neighborIds.size}** caller/callee neighbor(s)` : '') +
|
|
4902
5874
|
' — compact explore (definition + edge trail, no related-file dump).',
|
|
4903
5875
|
'',
|
|
4904
5876
|
];
|
|
5877
|
+
if (stubHeavy && implAsk) {
|
|
5878
|
+
lines.push('> Stub-only compact — state/callback answers usually live in project `.ets` / `.ts` under the feature, not `@ohos.*.d.ts`.', '');
|
|
5879
|
+
}
|
|
5880
|
+
// Bare `BadgeManager`-style compact: surface real @ohos/@kit / sibling Managers.
|
|
5881
|
+
// Never dump anonymous `%AM*` / local cache callees as "upstream services".
|
|
5882
|
+
const managerSeeds = names.filter((n) => /(?:Manager|Service|Handler|Store)$/i.test(n));
|
|
5883
|
+
if (managerSeeds.length > 0) {
|
|
5884
|
+
const ds = this.buildDataSourceSection(cg, `${managerSeeds.join(' ')} 数据来源于哪个系统服务`);
|
|
5885
|
+
if (ds.section) {
|
|
5886
|
+
lines.push(ds.section);
|
|
5887
|
+
if (ds.sdkImportCount === 0) {
|
|
5888
|
+
// Soften hard compact complete — cache-layer Managers otherwise look "done".
|
|
5889
|
+
const softIdx = lines.findIndex((l) => l.includes('**Coarse locate complete.**') || l.includes('**ANSWER NOW from this response.**'));
|
|
5890
|
+
if (softIdx >= 0) {
|
|
5891
|
+
lines[softIdx] =
|
|
5892
|
+
'> **Partial locator** — Manager body + trail below are anchors. ' +
|
|
5893
|
+
'Do **not** treat local cache/RDB helpers as the system service; ' +
|
|
5894
|
+
'follow related Managers / `homegraph_callers` on `update*` next, or ONE narrow Grep for the service name.';
|
|
5895
|
+
}
|
|
5896
|
+
}
|
|
5897
|
+
}
|
|
5898
|
+
}
|
|
4905
5899
|
// Blast radius for entry seeds (locations only) — same signal full explore
|
|
4906
5900
|
// always-on section gives, so bare-name edits know what to update/verify.
|
|
4907
5901
|
{
|
|
@@ -4970,10 +5964,22 @@ class ToolHandler {
|
|
|
4970
5964
|
}
|
|
4971
5965
|
if (trailBullets > 0) {
|
|
4972
5966
|
trail.push('');
|
|
4973
|
-
trail.push(
|
|
5967
|
+
trail.push(stubHeavy
|
|
5968
|
+
? '> Trail above may reference stubs — prefer in-repo impl files; do not ANSWER NOW from `.d.ts` alone.'
|
|
5969
|
+
: '> Prefer these call/use sites + Source below. Do not re-Grep/Read the same symbols; '
|
|
5970
|
+
+ 'ONE narrow Grep only for residual unindexed wiring.');
|
|
4974
5971
|
trail.push('');
|
|
4975
5972
|
lines.push(...trail);
|
|
4976
5973
|
}
|
|
5974
|
+
else if (!stubHeavy && !resourceProvenanceAsk && !multiTypeThin) {
|
|
5975
|
+
// No graph trail — coarse locate only; do not claim completeness.
|
|
5976
|
+
const softIdx = lines.findIndex((l) => l.includes('**Coarse locate complete.**'));
|
|
5977
|
+
if (softIdx >= 0) {
|
|
5978
|
+
lines[softIdx] =
|
|
5979
|
+
'> **Partial locator** — definition anchors below, no call/use trail in the graph. '
|
|
5980
|
+
+ 'ONE tighter explore on a callee/caller name, or ONE narrow Grep for registration — not a repo-wide storm.';
|
|
5981
|
+
}
|
|
5982
|
+
}
|
|
4977
5983
|
}
|
|
4978
5984
|
// When the graph has no call edges (common for static Type.member reads),
|
|
4979
5985
|
// fall back to a bounded indexed-file text scan for `name(` usages so the
|
|
@@ -5179,7 +6185,7 @@ class ToolHandler {
|
|
|
5179
6185
|
// (FormStack…) burns tokens and invites a follow-up Read of isExpired.
|
|
5180
6186
|
? 1
|
|
5181
6187
|
: ((0, query_utils_1.queryAsNamedComponentAction)(query) || componentSurface || usageFiles.size > 0 || bridge)
|
|
5182
|
-
? (uiCluster ? 2 : (componentSurface ? 2 : 3))
|
|
6188
|
+
? (uiCluster ? 2 : (componentSurface ? 2 : (exploreLocatorDigestEnabled() ? 2 : 3)))
|
|
5183
6189
|
: 2;
|
|
5184
6190
|
const ranked = [...fileNodes.entries()]
|
|
5185
6191
|
.filter(([, nodes]) => {
|
|
@@ -5208,26 +6214,35 @@ class ToolHandler {
|
|
|
5208
6214
|
})
|
|
5209
6215
|
.sort((a, b) => b.score - a.score || a.fp.localeCompare(b.fp))
|
|
5210
6216
|
.slice(0, maxFiles);
|
|
5211
|
-
|
|
5212
|
-
lines.push(
|
|
6217
|
+
const digest = exploreLocatorDigestEnabled();
|
|
6218
|
+
lines.push(digest ? '**Source digests**' : '**Source Code**', '');
|
|
6219
|
+
lines.push(stubHeavy
|
|
6220
|
+
? '> Stub digests only — treat as Partial locator. Do **not** ANSWER NOW from `.d.ts` alone; search in-repo impl next.'
|
|
6221
|
+
: digest
|
|
6222
|
+
? '> Short digests — treat as already Read for these symbols. Answer from anchors + digests; '
|
|
6223
|
+
+ 'ONE narrow Grep only for residual unindexed wiring.'
|
|
6224
|
+
: '> Line-numbered source — treat as already Read. **ANSWER NOW** — do not Read/Grep/search/explore/node the same symbols again.');
|
|
5213
6225
|
lines.push('');
|
|
5214
6226
|
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
|
|
6227
|
+
// Locator-digest ceilings (0012): keep compact under Grep/Read stacks.
|
|
6228
|
+
const maxTotal = digest
|
|
6229
|
+
? (bareMemberOnly ? 3600 : componentSurface || uiCluster ? 5500 : memberFocus && usageFiles.size > 0 ? 5000 : 4800)
|
|
6230
|
+
: (bareMemberOnly
|
|
5227
6231
|
? 4500
|
|
5228
|
-
:
|
|
5229
|
-
?
|
|
5230
|
-
:
|
|
6232
|
+
: componentSurface || uiCluster
|
|
6233
|
+
? 7000
|
|
6234
|
+
: memberFocus && usageFiles.size > 0
|
|
6235
|
+
? 6500
|
|
6236
|
+
: 5500);
|
|
6237
|
+
const maxPerFile = digest
|
|
6238
|
+
? (bareMemberOnly ? 2000 : componentSurface || uiCluster ? 2800 : memberFocus ? 2500 : 2800)
|
|
6239
|
+
: (bareMemberOnly
|
|
6240
|
+
? 2800
|
|
6241
|
+
: componentSurface || uiCluster
|
|
6242
|
+
? 4500
|
|
6243
|
+
: memberFocus
|
|
6244
|
+
? 2800
|
|
6245
|
+
: 3200);
|
|
5231
6246
|
let rendered = 0;
|
|
5232
6247
|
for (const { fp, nodes } of ranked) {
|
|
5233
6248
|
if (rendered >= maxFiles || totalChars > maxTotal)
|
|
@@ -6013,7 +7028,7 @@ class ToolHandler {
|
|
|
6013
7028
|
for (const typeName of types) {
|
|
6014
7029
|
let hits = [];
|
|
6015
7030
|
try {
|
|
6016
|
-
hits = cg.searchNodes(typeName, { limit:
|
|
7031
|
+
hits = cg.searchNodes(typeName, { limit: 200 });
|
|
6017
7032
|
}
|
|
6018
7033
|
catch {
|
|
6019
7034
|
continue;
|
|
@@ -6036,16 +7051,29 @@ class ToolHandler {
|
|
|
6036
7051
|
continue;
|
|
6037
7052
|
files.set(abs, rel(r.node.filePath));
|
|
6038
7053
|
}
|
|
7054
|
+
// Import sites of the Type — JSX construction often lives in the same file.
|
|
7055
|
+
try {
|
|
7056
|
+
for (const r of cg.searchNodes(typeName, { kinds: ['import'], limit: 80 })) {
|
|
7057
|
+
if ((0, query_utils_1.isTestFile)(r.node.filePath) || (0, arkts_1.isOhosApiFilePath)(r.node.filePath))
|
|
7058
|
+
continue;
|
|
7059
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
|
|
7060
|
+
if (!abs || !(0, fs_1.existsSync)(abs))
|
|
7061
|
+
continue;
|
|
7062
|
+
files.set(abs, rel(r.node.filePath));
|
|
7063
|
+
}
|
|
7064
|
+
}
|
|
7065
|
+
catch { /* optional */ }
|
|
6039
7066
|
// 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
|
|
7067
|
+
// site — expand to sibling .ets under the same feature/staticcommon/product roots
|
|
7068
|
+
// already touched by hits/imports.
|
|
6042
7069
|
const featureRoots = new Set();
|
|
6043
7070
|
for (const fp of files.values()) {
|
|
6044
|
-
const m = fp.match(/^(feature\/[^/]+|staticcommon\/[^/]+)/);
|
|
7071
|
+
const m = fp.match(/^(feature\/[^/]+|staticcommon\/[^/]+|product\/[^/]+)/);
|
|
6045
7072
|
if (m?.[1])
|
|
6046
7073
|
featureRoots.add(m[1]);
|
|
6047
7074
|
}
|
|
6048
|
-
|
|
7075
|
+
const inventoryAsk = /哪些|分别|各自|哪些文件|id\b|声明/i.test(query);
|
|
7076
|
+
if (featureRoots.size > 0 && featureRoots.size <= (inventoryAsk ? 24 : 6)) {
|
|
6049
7077
|
try {
|
|
6050
7078
|
for (const f of cg.getFiles()) {
|
|
6051
7079
|
const fp = rel(f.path);
|
|
@@ -6119,23 +7147,32 @@ class ToolHandler {
|
|
|
6119
7147
|
section: [
|
|
6120
7148
|
'**Declaration / id / native-binding survey**',
|
|
6121
7149
|
'',
|
|
6122
|
-
`> No UI/native construction sites for \`${types.join('`, `')}\` in the index. **
|
|
7150
|
+
`> No UI/native construction sites for \`${types.join('`, `')}\` in the index. **Partial locator** — ` +
|
|
7151
|
+
`ONE Grep for \`<${types[0]}\` / \`${types[0]}(\` is OK; do not repo-storm.`,
|
|
6123
7152
|
'',
|
|
6124
7153
|
].join('\n'),
|
|
6125
7154
|
hitCount: 0,
|
|
6126
7155
|
attempted: true,
|
|
6127
7156
|
};
|
|
6128
7157
|
}
|
|
7158
|
+
const hitCount = Math.min(rows.length, 20);
|
|
7159
|
+
// Sparse inventories (1–2 sites) are incomplete for "哪些文件/id/分别" — never hard ANSWER NOW.
|
|
7160
|
+
const inventoryAsk = /哪些|分别|各自|哪些文件|id\b|声明|绑定/i.test(query);
|
|
7161
|
+
const sparse = hitCount < 3 || (inventoryAsk && hitCount < 6);
|
|
7162
|
+
const guidance = sparse
|
|
7163
|
+
? `> Construction / JSX / \`id\` / native register sites above (**${hitCount}**). **Partial locator** — ` +
|
|
7164
|
+
'list may be incomplete; ONE narrow Grep for more `id:` / `XComponent(` sites is OK.'
|
|
7165
|
+
: '> Construction / JSX / `id` / native register sites. **ANSWER NOW** — do not Grep the same Type again.';
|
|
6129
7166
|
return {
|
|
6130
7167
|
section: [
|
|
6131
7168
|
'**Declaration / id / native-binding survey**',
|
|
6132
7169
|
'',
|
|
6133
|
-
|
|
7170
|
+
guidance,
|
|
6134
7171
|
'',
|
|
6135
7172
|
...rows.slice(0, 20),
|
|
6136
7173
|
'',
|
|
6137
7174
|
].join('\n'),
|
|
6138
|
-
hitCount
|
|
7175
|
+
hitCount,
|
|
6139
7176
|
attempted: true,
|
|
6140
7177
|
};
|
|
6141
7178
|
}
|
|
@@ -6733,11 +7770,24 @@ class ToolHandler {
|
|
|
6733
7770
|
}
|
|
6734
7771
|
const cg = this.getHomeGraph(args.projectPath);
|
|
6735
7772
|
const projectRoot = cg.getProjectRoot();
|
|
7773
|
+
// Same-bag / call-budget refuse (session view injected by execute).
|
|
7774
|
+
const sessionPrior = (0, explore_session_state_1.viewForProject)((0, explore_session_state_1.readExploreSessionView)(args), projectRoot);
|
|
7775
|
+
const repeat = (0, explore_repeat_guard_1.decideExploreRepeat)(sessionPrior, query);
|
|
7776
|
+
if (repeat.refuse) {
|
|
7777
|
+
const text = (0, explore_repeat_guard_1.formatExploreRepeatRefuse)(repeat, query);
|
|
7778
|
+
return this.exploreResult(text, {
|
|
7779
|
+
projectRoot,
|
|
7780
|
+
query,
|
|
7781
|
+
files: [],
|
|
7782
|
+
sourceBytes: 0,
|
|
7783
|
+
responseBytes: text.length,
|
|
7784
|
+
});
|
|
7785
|
+
}
|
|
6736
7786
|
const compactLocal = this.tryFastInventoryExplore(cg, query, projectRoot)
|
|
6737
7787
|
?? this.tryLightMechanismExplore(cg, query, projectRoot)
|
|
6738
7788
|
?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot);
|
|
6739
7789
|
if (compactLocal)
|
|
6740
|
-
return compactLocal;
|
|
7790
|
+
return this.ensureExploreEmission(compactLocal, projectRoot, query);
|
|
6741
7791
|
// Light mechanism already attempted above.
|
|
6742
7792
|
// Resolve adaptive output budget from project size.
|
|
6743
7793
|
// largest-tier defaults if stats aren't available, which preserves
|
|
@@ -7406,7 +8456,11 @@ class ToolHandler {
|
|
|
7406
8456
|
'production handlers are out of scope unless explicitly referenced in the test.');
|
|
7407
8457
|
lines.push('');
|
|
7408
8458
|
}
|
|
7409
|
-
const importResult = (0, query_utils_1.queryAsksKitInstallDeps)(query)
|
|
8459
|
+
const importResult = ((0, query_utils_1.queryAsksKitInstallDeps)(query)
|
|
8460
|
+
|| (0, query_utils_1.queryAsEventDispatchSurvey)(query)
|
|
8461
|
+
|| (0, query_utils_1.queryAsOutOfRepoSdkCatalog)(query)
|
|
8462
|
+
|| (0, query_utils_1.queryAsKitModuleCapabilitySurvey)(query)
|
|
8463
|
+
|| (0, query_utils_1.queryAsDataSourceSurvey)(query))
|
|
7410
8464
|
? { section: '', siteCount: 0, compactListing: false }
|
|
7411
8465
|
: this.buildImportSitesSection(cg, query, projectRoot);
|
|
7412
8466
|
if (importResult.section)
|
|
@@ -7434,9 +8488,14 @@ class ToolHandler {
|
|
|
7434
8488
|
lines.push(apiUsageResult.section);
|
|
7435
8489
|
const dataSourceResult = (0, query_utils_1.queryAsDataSourceSurvey)(query)
|
|
7436
8490
|
? this.buildDataSourceSection(cg, query)
|
|
7437
|
-
: { section: '', edgeCount: 0 };
|
|
8491
|
+
: { section: '', edgeCount: 0, sdkImportCount: 0, strongCount: 0 };
|
|
7438
8492
|
if (dataSourceResult.section)
|
|
7439
8493
|
lines.push(dataSourceResult.section);
|
|
8494
|
+
const eventDispatchResult = (0, query_utils_1.queryAsEventDispatchSurvey)(query)
|
|
8495
|
+
? this.buildEventDispatchSection(cg, query, projectRoot)
|
|
8496
|
+
: { section: '', hitCount: 0, eventCount: 0, handlerCount: 0, memberCount: 0, complete: false };
|
|
8497
|
+
if (eventDispatchResult.section)
|
|
8498
|
+
lines.push(eventDispatchResult.section);
|
|
7440
8499
|
const importInventoryFilter = (0, query_utils_1.hasImportInventoryFilter)(query);
|
|
7441
8500
|
const multiAnchor = (0, query_utils_1.queryNamesMultipleExploreAnchors)(query) || crossModuleFlow;
|
|
7442
8501
|
const mechanismSurvey = (0, query_utils_1.queryAsMechanismSurvey)(query);
|
|
@@ -7521,6 +8580,12 @@ class ToolHandler {
|
|
|
7521
8580
|
if (configSection) {
|
|
7522
8581
|
return finishCompact('Config/manifest content above — answer from it directly.');
|
|
7523
8582
|
}
|
|
8583
|
+
if (eventDispatchResult.section && (0, query_utils_1.queryAsEventDispatchSurvey)(query)) {
|
|
8584
|
+
if (eventDispatchResult.complete) {
|
|
8585
|
+
return finishCompact(`Event→Manager dispatch inventory — **${eventDispatchResult.handlerCount}** handler file(s). **ANSWER NOW**.`);
|
|
8586
|
+
}
|
|
8587
|
+
return finishCompact('Event→Manager survey incomplete. **Partial locator** — do **not** ANSWER NOW as a full dispatch map.');
|
|
8588
|
+
}
|
|
7524
8589
|
if (kitUsageResult.section) {
|
|
7525
8590
|
return finishCompact(`Kit module **in-repo usage** survey — **${kitUsageResult.symbolCount}** imported symbol(s). ` +
|
|
7526
8591
|
'Not an SDK catalog. **ANSWER NOW** from the usage list; do not Grep the same `@kit` path.');
|
|
@@ -7534,8 +8599,11 @@ class ToolHandler {
|
|
|
7534
8599
|
if (listedTypeMethods && callerBulletCount > 0) {
|
|
7535
8600
|
return finishCompact(`Caller inventory — **${callerBulletCount}** site(s) for listed methods. **ANSWER NOW**; do not fan out callers per method.`);
|
|
7536
8601
|
}
|
|
7537
|
-
if (dataSourceResult.section && dataSourceResult.
|
|
7538
|
-
return finishCompact(`Data-source survey — **${dataSourceResult.
|
|
8602
|
+
if (dataSourceResult.section && dataSourceResult.sdkImportCount > 0) {
|
|
8603
|
+
return finishCompact(`Data-source survey — **${dataSourceResult.sdkImportCount}** \`@ohos\`/\`@kit\` import(s). **ANSWER NOW.**`);
|
|
8604
|
+
}
|
|
8605
|
+
if (dataSourceResult.section && dataSourceResult.strongCount > 0) {
|
|
8606
|
+
return finishCompact(`Data-source survey — **${dataSourceResult.strongCount}** related writer/sibling(s). **Partial locator** — drill next.`);
|
|
7539
8607
|
}
|
|
7540
8608
|
if (inheritanceListed) {
|
|
7541
8609
|
return finishCompact('Inheritance survey above lists all direct subtypes found. **ANSWER NOW** — do not grep `extends`.');
|
|
@@ -7645,15 +8713,6 @@ class ToolHandler {
|
|
|
7645
8713
|
}
|
|
7646
8714
|
return false;
|
|
7647
8715
|
};
|
|
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
8716
|
let totalChars = lines.join('\n').length;
|
|
7658
8717
|
let filesIncluded = 0;
|
|
7659
8718
|
// Paths we actually render source for below. Drives the curated header count
|
|
@@ -7712,9 +8771,19 @@ class ToolHandler {
|
|
|
7712
8771
|
if (onSpine.length > 0)
|
|
7713
8772
|
filesToRender = onSpine;
|
|
7714
8773
|
}
|
|
7715
|
-
const sourceFileCap =
|
|
7716
|
-
|
|
7717
|
-
|
|
8774
|
+
const sourceFileCap = (() => {
|
|
8775
|
+
const digest = exploreLocatorDigestEnabled();
|
|
8776
|
+
if (limitSingleFile && filesToRender.length === 1)
|
|
8777
|
+
return 1;
|
|
8778
|
+
if (digest) {
|
|
8779
|
+
// Locator digest: at most 2 body digests (spine/named); rest stay as path anchors.
|
|
8780
|
+
if (localDetail)
|
|
8781
|
+
return Math.min(maxFiles, 2);
|
|
8782
|
+
if (hasFlowPath)
|
|
8783
|
+
return Math.min(maxFiles, 2);
|
|
8784
|
+
return Math.min(maxFiles, 2);
|
|
8785
|
+
}
|
|
8786
|
+
return localDetail
|
|
7718
8787
|
? Math.min(maxFiles, 2)
|
|
7719
8788
|
: crossModuleFlow && hasFlowPath
|
|
7720
8789
|
? Math.min(maxFiles, 5)
|
|
@@ -7725,6 +8794,38 @@ class ToolHandler {
|
|
|
7725
8794
|
: !hasFlowPath
|
|
7726
8795
|
? Math.min(maxFiles, 3)
|
|
7727
8796
|
: maxFiles;
|
|
8797
|
+
})();
|
|
8798
|
+
// Path-only anchors for files we will not dump as Source (digest mode).
|
|
8799
|
+
if (exploreLocatorDigestEnabled()) {
|
|
8800
|
+
const anchorCap = Math.min(filesToRender.length, 10);
|
|
8801
|
+
lines.push('**Anchors**', '');
|
|
8802
|
+
for (const [fp, group] of filesToRender.slice(0, anchorCap)) {
|
|
8803
|
+
const names = group.nodes
|
|
8804
|
+
.filter((n) => n.kind !== 'import' && n.kind !== 'export')
|
|
8805
|
+
.slice(0, 4)
|
|
8806
|
+
.map((n) => `\`${n.name}\`(${n.kind}${n.startLine ? `:${n.startLine}` : ''})`)
|
|
8807
|
+
.join(', ');
|
|
8808
|
+
lines.push(`- \`${fp}\`${names ? ` — ${names}` : ''}`);
|
|
8809
|
+
}
|
|
8810
|
+
if (filesToRender.length > sourceFileCap) {
|
|
8811
|
+
lines.push('');
|
|
8812
|
+
lines.push(`> Body digests only for the top **${sourceFileCap}** file(s) below; remaining anchors are path-only.`);
|
|
8813
|
+
}
|
|
8814
|
+
lines.push('');
|
|
8815
|
+
}
|
|
8816
|
+
lines.push(exploreLocatorDigestEnabled() ? '**Source digests**' : '**Source Code**');
|
|
8817
|
+
lines.push('');
|
|
8818
|
+
// Recorded so the drift pass below (#1474) can append a per-file exception
|
|
8819
|
+
// to this guarantee after the render loop knows which files drifted.
|
|
8820
|
+
const verbatimHeaderIdx = lines.length;
|
|
8821
|
+
lines.push(exploreLocatorDigestEnabled()
|
|
8822
|
+
? '> Spine / top-hit digests — treat as already Read. '
|
|
8823
|
+
+ 'Answer from Flow + Anchors + digests when you can; ONE narrow Grep for residual unindexed wiring. '
|
|
8824
|
+
+ 'Do not re-dump these files via Read/`homegraph_node`.'
|
|
8825
|
+
: '> Line-numbered source — treat as already Read. Answer from Flow + Source below when you can; ' +
|
|
8826
|
+
'do not re-read/grep these files, and do not fan out `homegraph_node` / search for symbols already shown. ' +
|
|
8827
|
+
'Another `homegraph_explore` with tighter names only if a needed symbol is missing.');
|
|
8828
|
+
lines.push('');
|
|
7728
8829
|
for (const [filePath, group] of filesToRender) {
|
|
7729
8830
|
if (filesIncluded >= sourceFileCap)
|
|
7730
8831
|
break;
|
|
@@ -8183,7 +9284,7 @@ class ToolHandler {
|
|
|
8183
9284
|
// token cost on small projects. We pick clusters in priority order
|
|
8184
9285
|
// until the per-file char cap is hit. Truly enormous single clusters
|
|
8185
9286
|
// get tail-trimmed with a marker.
|
|
8186
|
-
const contextPadding = 3;
|
|
9287
|
+
const contextPadding = exploreLocatorDigestEnabled() ? 2 : 3;
|
|
8187
9288
|
// Language-neutral separator (no `//` — not a comment in Python, Ruby,
|
|
8188
9289
|
// etc.). With line numbers on, the line-number jump also signals the gap.
|
|
8189
9290
|
const GAP_MARKER = '\n\n... (gap) ...\n\n';
|
|
@@ -8193,10 +9294,11 @@ class ToolHandler {
|
|
|
8193
9294
|
// for any per-file cap and gets dropped, so the agent Reads the method back —
|
|
8194
9295
|
// the exact gap this closes. Bounded, so a god-method can't blow the budget yet
|
|
8195
9296
|
// the spine's call still appears in context.
|
|
8196
|
-
const
|
|
8197
|
-
const
|
|
8198
|
-
const
|
|
8199
|
-
const
|
|
9297
|
+
const digest = exploreLocatorDigestEnabled();
|
|
9298
|
+
const OVERSIZE_SPINE_LINES = digest ? 120 : 200;
|
|
9299
|
+
const OVERSIZE_BODY_LINES = digest ? 100 : 160;
|
|
9300
|
+
const SPINE_WINDOW = digest ? 18 : 28; // lines each side of the next-hop call site
|
|
9301
|
+
const BODY_WINDOW = digest ? 24 : 40;
|
|
8200
9302
|
const buildSectionRange = (c) => {
|
|
8201
9303
|
const span = c.end - c.start + 1;
|
|
8202
9304
|
if (c.hasSpine && c.spineCallLine && span > OVERSIZE_SPINE_LINES) {
|
|
@@ -8268,7 +9370,7 @@ class ToolHandler {
|
|
|
8268
9370
|
// path is the answer), but bounded — at most ~2.5× the per-file cap and never
|
|
8269
9371
|
// past what's left of the total output cap — so a pathological long in-file
|
|
8270
9372
|
// spine can't run away or starve co-flow files entirely.
|
|
8271
|
-
const SPINE_CEILING = Math.min(budget.maxCharsPerFile * 2.5, remainingEnvelope);
|
|
9373
|
+
const SPINE_CEILING = Math.min(budget.maxCharsPerFile * (exploreLocatorDigestEnabled() ? 1.5 : 2.5), remainingEnvelope);
|
|
8272
9374
|
const chosenIndices = new Set();
|
|
8273
9375
|
let projectedChars = 0;
|
|
8274
9376
|
for (const rc of rankedClusters) {
|
|
@@ -8554,8 +9656,39 @@ class ToolHandler {
|
|
|
8554
9656
|
* calling session's state and deletes it — see {@link EXPLORE_EMISSION_KEY}.
|
|
8555
9657
|
*/
|
|
8556
9658
|
exploreResult(text, emission) {
|
|
9659
|
+
const meta = (0, explore_repeat_guard_1.inferExplorePartialMeta)(text);
|
|
8557
9660
|
const result = this.textResult(text);
|
|
8558
|
-
result[explore_session_state_1.EXPLORE_EMISSION_KEY] =
|
|
9661
|
+
result[explore_session_state_1.EXPLORE_EMISSION_KEY] = {
|
|
9662
|
+
...emission,
|
|
9663
|
+
partial: emission.partial ?? meta.partial,
|
|
9664
|
+
nextAnchor: emission.nextAnchor ?? meta.nextAnchor,
|
|
9665
|
+
};
|
|
9666
|
+
return result;
|
|
9667
|
+
}
|
|
9668
|
+
/** Attach a minimal session emission when a compact/light path used textResult. */
|
|
9669
|
+
ensureExploreEmission(result, projectRoot, query) {
|
|
9670
|
+
if (result[explore_session_state_1.EXPLORE_EMISSION_KEY]) {
|
|
9671
|
+
const em = result[explore_session_state_1.EXPLORE_EMISSION_KEY];
|
|
9672
|
+
if (em.partial === undefined || !em.nextAnchor) {
|
|
9673
|
+
const meta = (0, explore_repeat_guard_1.inferExplorePartialMeta)(result.content?.[0]?.text ?? '');
|
|
9674
|
+
if (em.partial === undefined)
|
|
9675
|
+
em.partial = meta.partial;
|
|
9676
|
+
if (!em.nextAnchor && meta.nextAnchor)
|
|
9677
|
+
em.nextAnchor = meta.nextAnchor;
|
|
9678
|
+
}
|
|
9679
|
+
return result;
|
|
9680
|
+
}
|
|
9681
|
+
const text = result.content?.[0]?.text ?? '';
|
|
9682
|
+
const meta = (0, explore_repeat_guard_1.inferExplorePartialMeta)(text);
|
|
9683
|
+
result[explore_session_state_1.EXPLORE_EMISSION_KEY] = {
|
|
9684
|
+
projectRoot,
|
|
9685
|
+
query,
|
|
9686
|
+
files: [],
|
|
9687
|
+
sourceBytes: 0,
|
|
9688
|
+
responseBytes: text.length,
|
|
9689
|
+
partial: meta.partial,
|
|
9690
|
+
nextAnchor: meta.nextAnchor,
|
|
9691
|
+
};
|
|
8559
9692
|
return result;
|
|
8560
9693
|
}
|
|
8561
9694
|
/**
|
|
@@ -9686,7 +10819,11 @@ class ToolHandler {
|
|
|
9686
10819
|
const tail = lastQualifierPart(symbol);
|
|
9687
10820
|
if (tail && tail !== symbol) {
|
|
9688
10821
|
try {
|
|
9689
|
-
|
|
10822
|
+
// Qualified miss: re-probe by bare member, but ONLY keep rows that
|
|
10823
|
+
// still match the FULL qualifier. Never `matchesSymbol(n, tail)` —
|
|
10824
|
+
// that is a bare-name match and turns `Foo.init` into every `init`
|
|
10825
|
+
// in the monorepo (hundreds of definitions → token bomb).
|
|
10826
|
+
pool = cg.getNodesByName(tail).filter((n) => this.matchesSymbol(n, symbol));
|
|
9690
10827
|
}
|
|
9691
10828
|
catch {
|
|
9692
10829
|
pool = [];
|