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.
Files changed (132) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/README.md +51 -5
  3. package/dist/addons/dynamic-import.d.ts +18 -0
  4. package/dist/addons/dynamic-import.d.ts.map +1 -0
  5. package/dist/addons/dynamic-import.js +25 -0
  6. package/dist/addons/dynamic-import.js.map +1 -0
  7. package/dist/addons/init-template.d.ts +15 -0
  8. package/dist/addons/init-template.d.ts.map +1 -0
  9. package/dist/addons/init-template.js +290 -0
  10. package/dist/addons/init-template.js.map +1 -0
  11. package/dist/addons/loader.d.ts +18 -0
  12. package/dist/addons/loader.d.ts.map +1 -0
  13. package/dist/addons/loader.js +88 -0
  14. package/dist/addons/loader.js.map +1 -0
  15. package/dist/addons/manager.d.ts +106 -0
  16. package/dist/addons/manager.d.ts.map +1 -0
  17. package/dist/addons/manager.js +437 -0
  18. package/dist/addons/manager.js.map +1 -0
  19. package/dist/addons/paths.d.ts +22 -0
  20. package/dist/addons/paths.d.ts.map +1 -0
  21. package/dist/addons/paths.js +65 -0
  22. package/dist/addons/paths.js.map +1 -0
  23. package/dist/addons/registry.d.ts +38 -0
  24. package/dist/addons/registry.d.ts.map +1 -0
  25. package/dist/addons/registry.js +160 -0
  26. package/dist/addons/registry.js.map +1 -0
  27. package/dist/addons/semver.d.ts +21 -0
  28. package/dist/addons/semver.d.ts.map +1 -0
  29. package/dist/addons/semver.js +79 -0
  30. package/dist/addons/semver.js.map +1 -0
  31. package/dist/addons/types.d.ts +51 -0
  32. package/dist/addons/types.d.ts.map +1 -0
  33. package/dist/addons/types.js +14 -0
  34. package/dist/addons/types.js.map +1 -0
  35. package/dist/addons/validate.d.ts +38 -0
  36. package/dist/addons/validate.d.ts.map +1 -0
  37. package/dist/addons/validate.js +165 -0
  38. package/dist/addons/validate.js.map +1 -0
  39. package/dist/arkui/migrate-snapshot.d.ts.map +1 -1
  40. package/dist/arkui/migrate-snapshot.js +58 -34
  41. package/dist/arkui/migrate-snapshot.js.map +1 -1
  42. package/dist/bin/addon-commands.d.ts +27 -0
  43. package/dist/bin/addon-commands.d.ts.map +1 -0
  44. package/dist/bin/addon-commands.js +254 -0
  45. package/dist/bin/addon-commands.js.map +1 -0
  46. package/dist/bin/homegraph.js +6 -1
  47. package/dist/bin/homegraph.js.map +1 -1
  48. package/dist/bin/node-version-check.d.ts +1 -1
  49. package/dist/bin/node-version-check.js +1 -1
  50. package/dist/db/sqlite-adapter.d.ts +2 -3
  51. package/dist/db/sqlite-adapter.d.ts.map +1 -1
  52. package/dist/db/sqlite-adapter.js +2 -3
  53. package/dist/db/sqlite-adapter.js.map +1 -1
  54. package/dist/extraction/languages/arkts.d.ts +14 -0
  55. package/dist/extraction/languages/arkts.d.ts.map +1 -1
  56. package/dist/extraction/languages/arkts.js +78 -32
  57. package/dist/extraction/languages/arkts.js.map +1 -1
  58. package/dist/index.d.ts +1 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js.map +1 -1
  61. package/dist/mcp/explore-repeat-guard.d.ts +123 -0
  62. package/dist/mcp/explore-repeat-guard.d.ts.map +1 -0
  63. package/dist/mcp/explore-repeat-guard.js +503 -0
  64. package/dist/mcp/explore-repeat-guard.js.map +1 -0
  65. package/dist/mcp/explore-session-state.d.ts +20 -0
  66. package/dist/mcp/explore-session-state.d.ts.map +1 -1
  67. package/dist/mcp/explore-session-state.js +31 -0
  68. package/dist/mcp/explore-session-state.js.map +1 -1
  69. package/dist/mcp/proxy.d.ts.map +1 -1
  70. package/dist/mcp/proxy.js +15 -4
  71. package/dist/mcp/proxy.js.map +1 -1
  72. package/dist/mcp/query-pool.d.ts.map +1 -1
  73. package/dist/mcp/query-pool.js +10 -13
  74. package/dist/mcp/query-pool.js.map +1 -1
  75. package/dist/mcp/server-instructions.d.ts +5 -7
  76. package/dist/mcp/server-instructions.d.ts.map +1 -1
  77. package/dist/mcp/server-instructions.js +36 -101
  78. package/dist/mcp/server-instructions.js.map +1 -1
  79. package/dist/mcp/tools.d.ts +33 -2
  80. package/dist/mcp/tools.d.ts.map +1 -1
  81. package/dist/mcp/tools.js +1403 -266
  82. package/dist/mcp/tools.js.map +1 -1
  83. package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  84. package/dist/resolution/callback-synthesizer.js +166 -0
  85. package/dist/resolution/callback-synthesizer.js.map +1 -1
  86. package/dist/search/query-utils.d.ts +47 -7
  87. package/dist/search/query-utils.d.ts.map +1 -1
  88. package/dist/search/query-utils.js +335 -109
  89. package/dist/search/query-utils.js.map +1 -1
  90. package/dist/spec/build/scan.js +1 -1
  91. package/dist/spec/build/scan.js.map +1 -1
  92. package/dist/spec/build/scope-resolver.d.ts +24 -5
  93. package/dist/spec/build/scope-resolver.d.ts.map +1 -1
  94. package/dist/spec/build/scope-resolver.js +76 -10
  95. package/dist/spec/build/scope-resolver.js.map +1 -1
  96. package/dist/spec/config.d.ts +8 -0
  97. package/dist/spec/config.d.ts.map +1 -1
  98. package/dist/spec/config.js +3 -0
  99. package/dist/spec/config.js.map +1 -1
  100. package/dist/spec/evolve/pipeline.d.ts.map +1 -1
  101. package/dist/spec/evolve/pipeline.js +2 -1
  102. package/dist/spec/evolve/pipeline.js.map +1 -1
  103. package/dist/spec/git/commits.d.ts +8 -3
  104. package/dist/spec/git/commits.d.ts.map +1 -1
  105. package/dist/spec/git/commits.js +12 -11
  106. package/dist/spec/git/commits.js.map +1 -1
  107. package/dist/spec/mine/addon/adapter.d.ts +45 -0
  108. package/dist/spec/mine/addon/adapter.d.ts.map +1 -0
  109. package/dist/spec/mine/addon/adapter.js +118 -0
  110. package/dist/spec/mine/addon/adapter.js.map +1 -0
  111. package/dist/spec/mine/addon/render.d.ts +20 -0
  112. package/dist/spec/mine/addon/render.d.ts.map +1 -0
  113. package/dist/spec/mine/addon/render.js +41 -0
  114. package/dist/spec/mine/addon/render.js.map +1 -0
  115. package/dist/spec/mine/addon/types.d.ts +70 -0
  116. package/dist/spec/mine/addon/types.d.ts.map +1 -0
  117. package/dist/spec/mine/addon/types.js +14 -0
  118. package/dist/spec/mine/addon/types.js.map +1 -0
  119. package/dist/spec/mine/generator.d.ts +5 -1
  120. package/dist/spec/mine/generator.d.ts.map +1 -1
  121. package/dist/spec/mine/generator.js +69 -7
  122. package/dist/spec/mine/generator.js.map +1 -1
  123. package/dist/spec/mine/pipeline.d.ts.map +1 -1
  124. package/dist/spec/mine/pipeline.js +5 -1
  125. package/dist/spec/mine/pipeline.js.map +1 -1
  126. package/dist/spec/mine/scanner.d.ts +1 -0
  127. package/dist/spec/mine/scanner.d.ts.map +1 -1
  128. package/dist/spec/mine/scanner.js +1 -1
  129. package/dist/spec/mine/scanner.js.map +1 -1
  130. package/dist/spec/types.d.ts +1 -1
  131. package/dist/spec/types.d.ts.map +1 -1
  132. 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). Above that, the host externalizes the result
282
- // to a file the agent then Reads back — re-introducing a read AND the
283
- // cache-write cost — which is exactly what a 35K vscode explore did in the
284
- // n=4 README A/B. So even large repos cap at ~24K: the answer is the handful
285
- // of ~100-line flow windows the agent would have grep-located and read (it
286
- // natively reads ~6–9 files, median 100-line ranges), NOT a sprawl of 12
287
- // files. Concentration onto the flow emerges from this cap + the named-file-
288
- // first sort dropping peripheral files. Invariant: a larger tier must never
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
- // ITER3: revert iter2's aggressive body shrink (forced Read fallback —
293
- // the per-file 2.5K cap pushed the agent to Read instead of node).
294
- // Back to the iter1 shape (13K/4/3.8K) but keep the test-file
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
- // ITER3: same revert/keep-filter pattern as <150.
313
- maxOutputChars: 18000,
314
- defaultMaxFiles: 5,
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
- // ~150-line per-file window (the native read unit) × ~6 files, capped at
329
- // the ~24K inline ceiling so the response is never externalized. Per-file
330
- // stays ≥ the <500 tier (3800) — monotonic.
331
- maxOutputChars: 24000,
332
- defaultMaxFiles: 8,
333
- maxCharsPerFile: 6500,
334
- gapThreshold: 12,
335
- maxSymbolsInFileHeader: 10,
336
- maxEdgesPerRelationshipKind: 10,
337
- includeRelationships: true,
338
- includeAdditionalFiles: true,
339
- includeCompletenessSignal: true,
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 repos: SAME ~24K inline ceiling (a bigger response just
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: 24000,
351
- defaultMaxFiles: 8,
352
- maxCharsPerFile: 7000,
353
- gapThreshold: 15,
354
- maxSymbolsInFileHeader: 15,
355
- maxEdgesPerRelationshipKind: 15,
356
- includeRelationships: true,
357
- includeAdditionalFiles: true,
358
- includeCompletenessSignal: true,
359
- includeBudgetNote: true,
360
- excludeLowValueFiles: false,
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: 24000,
365
- defaultMaxFiles: 8,
366
- maxCharsPerFile: 7000,
367
- gapThreshold: 15,
368
- maxSymbolsInFileHeader: 15,
369
- maxEdgesPerRelationshipKind: 15,
370
- includeRelationships: true,
371
- includeAdditionalFiles: true,
372
- includeCompletenessSignal: true,
373
- includeBudgetNote: true,
374
- excludeLowValueFiles: false,
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
- maxOutputChars: Math.min(budget.maxOutputChars, 12000),
392
- defaultMaxFiles: Math.min(budget.defaultMaxFiles, 4),
393
- maxCharsPerFile: Math.min(budget.maxCharsPerFile, 4500),
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, 14000),
409
- defaultMaxFiles: Math.min(budget.defaultMaxFiles, 3),
410
- maxCharsPerFile: Math.min(budget.maxCharsPerFile, 5000),
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, lean ? 2 : local ? 2 : 3),
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: 'In-repo multi-file / mechanism tool (call paths + compact line-numbered source). Required: `query`. ' +
844
- 'CALL FIRST for how/wired questions — pass the question or domain keywords; PascalCase names optional. ' +
845
- 'Also CALL FIRST (alone, no parallel Grep) for named Type/Component/Page/Dialog, Type.member, click→handler, inheritance/subtypes, ' +
846
- 'declaration/attribute sites, ALL_CAPS constant / field-mutex usages, path-module NAPI/exports or inter-deps, ' +
847
- 'and in-repo @kit/@ohos *usages/dependencies* (which files import a named export — not the SDK feature catalog). ' +
848
- 'On the first turn call explore alone (no parallel Grep). Prefer callers/node when one named symbol is already enough. ' +
849
- 'DO NOT call for topic file-lists, concept/UI-behavior with no named anchors, literal copy hunts, or SDK/@kit *feature catalogs* — those return Skip guidance. ' +
850
- 'Literal string/pattern hunts → Grep. ' +
851
- 'One explore; answer from Source + trail; treat as already Read — do not re-grep/node/read the same symbols. Busy/partial → retry same explore once.',
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. Prefer the user question or domain keywords for how/mechanism. ' +
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
- if (cacheEnabled && cacheKey && cacheQueries && cacheIndex && !fast.isError) {
1768
- cacheIndex.setEntry(cacheQueries, cacheKey, toolName, fast);
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(fast, projectPath);
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 — no DB, never blocks the transport. */
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
- return this.textResult(`⚠️ **Partial result** — HomeGraph hit its ${secs}s response deadline / is busy ` +
1912
- `(MCP clients typically kill the call at ~60s with an empty timeout). ` +
1913
- `This is NOT an error. Retry ONE \`homegraph_explore\` with concrete symbol/file names from the question — ` +
1914
- `do not fire search+explore or node+callers+callees in parallel, and do not grep/read symbols you already named.`);
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
- + '; no read/grep needed.'
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
- return this.textResult(this.truncateOutput(lines.join('\n') + filterNote + '\n\n> Caller listing complete — answer from this list; no read/grep needed.'));
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
- const focusExports = [
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
- ...(0, query_utils_1.extractKitSubmoduleNamesFromQuery)(query),
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 / exports / Descriptor { "foo", … }
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)?\s*,\s*\w+/i)
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
- `> In-repo NAPI surface for \`${needles.slice(0, 4).join('`, `')}\`. **ANSWER NOW** — do not Grep \`napi_define\` again.`,
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
- for (const e of exports.slice(0, 30)) {
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 (exports.length > 30)
3633
- lines.push(`- … and ${exports.length - 30} more`);
3780
+ if (ranked.length > 30)
3781
+ lines.push(`- … and ${ranked.length - 30} more`);
3634
3782
  lines.push('');
3635
- return { section: lines.join('\n'), hitCount: Math.min(exports.length, 30) };
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|Hotspot|Manager/i.test(r.node.name)) {
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
- // Prefer system/SDK / service APIs — not local UI helpers (getBadgeOffsetX).
3818
- const SERVICE_RE = /notification|@ohos|ans|subscribe|publish|bundle|ability|vibrator|telephony|NumBadge|notificationManager|badgeManager|wantAgent|distributed|account|osAccount|AppAccount|distributedAccount|userId|getOsAccount/i;
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
- let edgeCount = 0;
3821
- const addEdge = (bucket, node, via, rank, opts) => {
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
- const sig = `${node.name} ${node.filePath} ${node.signature || ''}`;
3825
- // Same-file @ohos/@kit imports are always data-source candidates (BadgeManager
3826
- // → notificationManager), even when the import local name fails SERVICE_RE.
3827
- if (!opts?.importAlways && !SERVICE_RE.test(sig))
3995
+ if (isAnonName(node.name))
3828
3996
  return;
3829
- if (opts?.importAlways && !/@ohos\.|@kit\.|ohos\./i.test(sig))
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 (!prev)
3835
- edgeCount++;
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', 'badge', 'NumBadge', 'subscribe', 'bundle', 'ability',
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 services/APIs.
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
- addEdge(upstream, c.node, method.name, 2);
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
- if (edgeCount === 0) {
3909
- lines.push('- No `@ohos`/`@kit` / service-like upstream edges indexed for the named Manager. ' +
3910
- '**ANSWER NOW** from the Manager definition file (prefer in-repo `AccountManager` / `*Manager.ets`, not SDK `.d.ts`).');
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
- return { section: lines.join('\n'), edgeCount: 0 };
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
- return finishCompact(declarationResult.hitCount > 0
4152
- ? `Declaration-site survey — **${declarationResult.hitCount}** site(s). **ANSWER NOW** from ids + bindings above; do not Grep the same Type.`
4153
- : 'Declaration-site survey complete (0 sites). **ANSWER NOW** from the note above.');
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(`NAPI / native export survey — **${moduleExportResult.hitCount}** export(s). **ANSWER NOW**; do not Grep napi_define again.`);
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
- return finishCompact(dataSourceResult.edgeCount > 0
4175
- ? `Data-source survey — **${dataSourceResult.edgeCount}** upstream symbol(s). **ANSWER NOW** from system \`@ohos\`/\`@kit\` imports first.`
4176
- : 'Data-source survey complete (0 service edges). **ANSWER NOW** from the note + Manager defs above; do not Grep broadly.');
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.edgeCount > 0) {
4223
- return finishCompact(`Data-source survey — **${dataSourceResult.edgeCount}** upstream symbol(s). **ANSWER NOW.**`);
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 >= 16)
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 >= 16)
5060
+ if (seedIds.size >= 24)
4401
5061
  break;
4402
5062
  }
4403
- if (seedIds.size >= 12)
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
- '> **ANSWER NOW from this response.** Do **not** Grep or Read listed files in parallel — Source below is authoritative.',
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
- // Import inventory: prefer concrete API seeds (convertxml / XmlParseUtil) over
4447
- // bare domain tokens like `xml` that flood every path containing the substring.
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
- && (s.length >= 6 || /convert|xml|parse|manager|service/i.test(s)));
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, 8);
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
- // Shared-lib + GLES/EGL thread: surface CMake link lines before bodies.
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).map((s) => s.toLowerCase());
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
- // Prefer real wrappers (XmlParseUtil / convertxml) over incidental Xml* paths.
4524
- if (apiSeeds.some((s) => s.length >= 6 && fpLc.includes(s)))
5201
+ if (apiSeeds.some((s) => fpLc.includes(s)))
4525
5202
  score += 40;
4526
- if (/convertxml|xmlparseutil/i.test(fpLc))
4527
- score += 50;
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) => s.length >= 6 && blob.includes(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 (/convertxml|@kit\.|@ohos\./i.test(line))
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
- lines.push('**Source Code**', '');
4558
- lines.push('> Line-numbered source below — treat as already Read. Do **not** grep/read/`homegraph_node` these files again.');
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
- // Token budget: light howto must stay ~3–5k so explore+answer can beat Grep/Read.
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) ?? [], 2000);
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
- lines.push('> **Mechanism explore complete — ANSWER NOW.** Sections above cover the wired mechanism. ' +
4577
- 'Do **not** call `homegraph_node`, Read, or Grep for symbols/files already shown (that multiplies tokens). ' +
4578
- 'Only one tighter `homegraph_explore` if a **named** symbol essential to the answer is still missing.');
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
- return this.textResult(lines.join('\n'));
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
- /** Cap import bullets inside a light-mechanism response. */
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 out = [];
4586
- let bullets = 0;
4587
- let truncated = false;
5485
+ const header = [];
5486
+ const prod = [];
5487
+ const test = [];
4588
5488
  for (const line of lines) {
4589
5489
  if (line.startsWith('- `')) {
4590
- if (bullets >= maxBullets) {
4591
- truncated = true;
4592
- continue;
4593
- }
4594
- bullets++;
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
- out.push(line);
5499
+ }
5500
+ else {
5501
+ header.push(line);
5502
+ }
4599
5503
  }
4600
- if (truncated) {
4601
- out.push(`- … (light-mechanism: showing ${maxBullets} sites — **ANSWER NOW**, do not Grep the same import)`);
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
- '> **ANSWER NOW from this response.** Do **not** Grep/Read/search/explore/node/callers for the same symbols — trail + Source below are authoritative.',
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('> **ANSWER NOW** from this trail + Source below. Do not grep/search/read the same symbols again.');
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
- lines.push('**Source Code**', '');
5212
- lines.push('> Line-numbered source — treat as already Read. **ANSWER NOW** — do not Read/Grep/search/explore/node the same symbols again.');
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
- // Lean ceilings: without-HG Grep/Read stacks were ~15k session tokens; a 10–14k
5216
- // compact body + follow-up Read is what loses the token A/B.
5217
- const maxTotal = bareMemberOnly
5218
- ? 4500
5219
- : componentSurface || uiCluster
5220
- ? 7000
5221
- : memberFocus && usageFiles.size > 0
5222
- ? 6500
5223
- : 5500;
5224
- const maxPerFile = bareMemberOnly
5225
- ? 2800
5226
- : componentSurface || uiCluster
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
- : memberFocus
5229
- ? 2800
5230
- : 3200;
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: 80 });
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 native/header hits.
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
- if (featureRoots.size > 0 && featureRoots.size <= 6) {
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. **ANSWER NOW** — do not Grep the same Type for "declaration"; at most one Grep for \`<${types[0]}\` if you must.`,
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
- '> Construction / JSX / `id` / native register sites. **ANSWER NOW** — do not Grep the same Type again.',
7170
+ guidance,
6134
7171
  '',
6135
7172
  ...rows.slice(0, 20),
6136
7173
  '',
6137
7174
  ].join('\n'),
6138
- hitCount: Math.min(rows.length, 20),
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.edgeCount > 0) {
7538
- return finishCompact(`Data-source survey — **${dataSourceResult.edgeCount}** upstream symbol(s). **ANSWER NOW.**`);
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 = limitSingleFile && filesToRender.length === 1
7716
- ? 1
7717
- : localDetail
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 OVERSIZE_SPINE_LINES = 200;
8197
- const OVERSIZE_BODY_LINES = 160;
8198
- const SPINE_WINDOW = 28; // lines each side of the next-hop call site
8199
- const BODY_WINDOW = 40;
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] = emission;
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
- pool = cg.getNodesByName(tail).filter((n) => this.matchesSymbol(n, symbol) || this.matchesSymbol(n, tail));
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 = [];