homegraph 1.5.6 → 1.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +10 -4
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +120 -15
- package/dist/context/index.js.map +1 -1
- package/dist/extraction/languages/arkts.d.ts +25 -3
- package/dist/extraction/languages/arkts.d.ts.map +1 -1
- package/dist/extraction/languages/arkts.js +184 -22
- package/dist/extraction/languages/arkts.js.map +1 -1
- package/dist/mcp/evidence-rendering.d.ts +10 -0
- package/dist/mcp/evidence-rendering.d.ts.map +1 -0
- package/dist/mcp/evidence-rendering.js +71 -0
- package/dist/mcp/evidence-rendering.js.map +1 -0
- package/dist/mcp/explore-repeat-guard.d.ts +8 -13
- package/dist/mcp/explore-repeat-guard.d.ts.map +1 -1
- package/dist/mcp/explore-repeat-guard.js +54 -114
- package/dist/mcp/explore-repeat-guard.js.map +1 -1
- package/dist/mcp/explore-session-state.d.ts +26 -4
- package/dist/mcp/explore-session-state.d.ts.map +1 -1
- package/dist/mcp/explore-session-state.js +50 -4
- package/dist/mcp/explore-session-state.js.map +1 -1
- package/dist/mcp/query-cache.d.ts +1 -1
- package/dist/mcp/query-cache.d.ts.map +1 -1
- package/dist/mcp/query-cache.js +86 -3
- package/dist/mcp/query-cache.js.map +1 -1
- package/dist/mcp/server-instructions.d.ts +2 -2
- package/dist/mcp/server-instructions.d.ts.map +1 -1
- package/dist/mcp/server-instructions.js +34 -21
- package/dist/mcp/server-instructions.js.map +1 -1
- package/dist/mcp/tools.d.ts +19 -3
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +690 -107
- package/dist/mcp/tools.js.map +1 -1
- package/dist/search/literal-evidence.d.ts +44 -0
- package/dist/search/literal-evidence.d.ts.map +1 -0
- package/dist/search/literal-evidence.js +278 -0
- package/dist/search/literal-evidence.js.map +1 -0
- package/dist/search/query-plan-provider.d.ts +6 -0
- package/dist/search/query-plan-provider.d.ts.map +1 -0
- package/dist/search/query-plan-provider.js +289 -0
- package/dist/search/query-plan-provider.js.map +1 -0
- package/dist/search/query-plan.d.ts +98 -0
- package/dist/search/query-plan.d.ts.map +1 -0
- package/dist/search/query-plan.js +334 -0
- package/dist/search/query-plan.js.map +1 -0
- package/dist/search/query-utils.d.ts +6 -1
- package/dist/search/query-utils.d.ts.map +1 -1
- package/dist/search/query-utils.js +64 -5
- package/dist/search/query-utils.js.map +1 -1
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/mcp/tools.js
CHANGED
|
@@ -21,6 +21,8 @@ exports.queryAsBareSymbolInventory = queryAsBareSymbolInventory;
|
|
|
21
21
|
exports.hasPositiveAnswerNowDirective = hasPositiveAnswerNowDirective;
|
|
22
22
|
exports.reconcilePartialAnswerNow = reconcilePartialAnswerNow;
|
|
23
23
|
const query_pool_1 = require("./query-pool");
|
|
24
|
+
const evidence_rendering_1 = require("./evidence-rendering");
|
|
25
|
+
const query_plan_1 = require("../search/query-plan");
|
|
24
26
|
const memory_budget_1 = require("./memory-budget");
|
|
25
27
|
const directory_1 = require("../directory");
|
|
26
28
|
// Lazy-load the heavy HomeGraph chain off the MCP startup path — see the same
|
|
@@ -99,6 +101,20 @@ const MAX_OUTPUT_LENGTH = 15000;
|
|
|
99
101
|
* far beyond any realistic legitimate query.
|
|
100
102
|
*/
|
|
101
103
|
const MAX_INPUT_LENGTH = 10_000;
|
|
104
|
+
// Server-owned, structured-clone-safe request context. Never accept these from MCP clients.
|
|
105
|
+
const QUERY_PLAN_ARG = '__homegraphQueryPlan';
|
|
106
|
+
const QUERY_DEADLINE_ARG = '__homegraphQueryDeadlineAt';
|
|
107
|
+
const QUERY_STARTED_ARG = '__homegraphQueryStartedAt';
|
|
108
|
+
const QUERY_INDEX_STATE_ARG = '__homegraphQueryIndexState';
|
|
109
|
+
const QUERY_FAST_ATTEMPTED_ARG = '__homegraphQueryFastAttempted';
|
|
110
|
+
function readQueryPlan(args) {
|
|
111
|
+
const plan = args[QUERY_PLAN_ARG];
|
|
112
|
+
return plan?.version === query_plan_1.QUERY_PLAN_VERSION && typeof plan.originalQuery === 'string'
|
|
113
|
+
&& typeof plan.canonicalQuery === 'string' && Array.isArray(plan.steps) ? plan : undefined;
|
|
114
|
+
}
|
|
115
|
+
function planFeature(plan, name, query, fallback) {
|
|
116
|
+
return plan?.features[name] ?? fallback(query);
|
|
117
|
+
}
|
|
102
118
|
/** Example values for success-shaped bad-arg guidance (keyed by arg name). */
|
|
103
119
|
const BAD_ARG_EXAMPLES = {
|
|
104
120
|
query: 'authenticate login',
|
|
@@ -843,95 +859,107 @@ exports.tools = [
|
|
|
843
859
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
844
860
|
},
|
|
845
861
|
{
|
|
846
|
-
name: '
|
|
847
|
-
description: 'PRIMARY
|
|
848
|
-
'
|
|
849
|
-
'
|
|
850
|
-
'
|
|
851
|
-
'named Type/Component/Page/Dialog, Type.member, click→handler, inheritance/subtypes, declaration/attribute sites, ' +
|
|
852
|
-
'ALL_CAPS constant / field-mutex usages, path-module NAPI/exports or inter-deps, and in-repo @kit/@ohos *usages/dependencies* ' +
|
|
853
|
-
'(which files import a named export — not the SDK feature catalog). PascalCase names optional when domain keywords suffice. ' +
|
|
854
|
-
'Prefer callers/node when one named symbol is already enough. ' +
|
|
855
|
-
'DO NOT call for topic file-lists with no Type/file, literal copy hunts / pure existence compares with no anchors, ' +
|
|
856
|
-
'official-docs-only asks, empty-project-from-scratch scaffolds, git history, or media/binary asset inventories — those return Skip. ' +
|
|
857
|
-
'@kit / OHOS API questions ARE in scope when the SDK API graph is available — call explore (usages or API symbols). ' +
|
|
858
|
-
'Literal string/pattern hunts → Grep; media assets → Glob; git history → git. ' +
|
|
859
|
-
'One explore; then answer or edit from Source + trail — do not re-grep/node/read the same symbols. ' +
|
|
860
|
-
'Overlapping paraphrase explores are refused (name a new Type/file/@kit to continue). ' +
|
|
861
|
-
'Busy/partial → retry ONCE with the named Next anchor or ONE narrow Grep — not a Grep/node storm '
|
|
862
|
-
+ '(session refuses further explore / depth fan-out after Partial).',
|
|
862
|
+
name: 'homegraph_usages',
|
|
863
|
+
description: 'PRIMARY first tool for a narrow WHERE-USED question about one named API, `.member`, ALL_CAPS constant, field, or mutex. ' +
|
|
864
|
+
'Choose this instead of homegraph_explore when the requested answer is usage/reference locations. ' +
|
|
865
|
+
'Required: `query`. Returns usage files/lines only; it does not build a general flow or source dump. ' +
|
|
866
|
+
'`homegraph_explore` auto-routes equivalent high-confidence queries here only for compatibility.',
|
|
863
867
|
inputSchema: {
|
|
864
868
|
type: 'object',
|
|
865
869
|
properties: {
|
|
866
870
|
query: {
|
|
867
871
|
type: 'string',
|
|
868
|
-
description: 'Required.
|
|
869
|
-
'For named flows, include Type / Type.member / component names. For @kit, ask usages (depend/import sites), not SDK catalogs.',
|
|
870
|
-
},
|
|
871
|
-
maxFiles: {
|
|
872
|
-
type: 'number',
|
|
873
|
-
description: 'Maximum number of files to include source code from (default: 12)',
|
|
874
|
-
default: 12,
|
|
872
|
+
description: 'Required. Exact API/member/constant/field name, with or without where-used wording.',
|
|
875
873
|
},
|
|
876
874
|
projectPath: projectPathProperty,
|
|
877
875
|
},
|
|
878
876
|
required: ['query'],
|
|
879
877
|
},
|
|
880
|
-
annotations: READ_ONLY_ANNOTATIONS,
|
|
878
|
+
annotations: { ...READ_ONLY_ANNOTATIONS, title: 'HomeGraph Where-used Inventory' },
|
|
881
879
|
},
|
|
882
880
|
{
|
|
883
|
-
name: '
|
|
884
|
-
description: '
|
|
885
|
-
'
|
|
886
|
-
'
|
|
881
|
+
name: 'homegraph_modules',
|
|
882
|
+
description: 'PRIMARY first tool for a narrow DEPENDENCY/CYCLE question about named path modules or `*common` / `*service` / `*component` / `*constants` modules. ' +
|
|
883
|
+
'Choose this instead of homegraph_explore when the requested answer is module topology. ' +
|
|
884
|
+
'Required: `query`. It does not build a general code flow or scan unrelated survey families. ' +
|
|
885
|
+
'`homegraph_explore` auto-routes equivalent high-confidence dependency questions here only for compatibility.',
|
|
887
886
|
inputSchema: {
|
|
888
887
|
type: 'object',
|
|
889
888
|
properties: {
|
|
890
889
|
query: {
|
|
891
890
|
type: 'string',
|
|
892
|
-
description: 'Required.
|
|
891
|
+
description: 'Required. Dependency/cycle question containing the exact module names or paths.',
|
|
893
892
|
},
|
|
894
893
|
projectPath: projectPathProperty,
|
|
895
894
|
},
|
|
896
895
|
required: ['query'],
|
|
897
896
|
},
|
|
898
|
-
annotations: READ_ONLY_ANNOTATIONS,
|
|
897
|
+
annotations: { ...READ_ONLY_ANNOTATIONS, title: 'HomeGraph Module Dependencies' },
|
|
899
898
|
},
|
|
900
899
|
{
|
|
901
|
-
name: '
|
|
902
|
-
description: '
|
|
903
|
-
'
|
|
904
|
-
'
|
|
900
|
+
name: 'homegraph_native',
|
|
901
|
+
description: 'PRIMARY first tool for a narrow NAPI/NATIVE EXPORT or registration question about one named path or Type. ' +
|
|
902
|
+
'Choose this instead of homegraph_explore when the requested answer is the ArkTS↔native export surface. Required: `query`. ' +
|
|
903
|
+
'Returns indexed export descriptors/registration sites without a general domain file dump. ' +
|
|
904
|
+
'`homegraph_explore` auto-routes equivalent high-confidence NAPI/export questions here only for compatibility.',
|
|
905
905
|
inputSchema: {
|
|
906
906
|
type: 'object',
|
|
907
907
|
properties: {
|
|
908
908
|
query: {
|
|
909
909
|
type: 'string',
|
|
910
|
-
description: 'Required.
|
|
910
|
+
description: 'Required. NAPI/native export question containing the exact path or Type name.',
|
|
911
911
|
},
|
|
912
912
|
projectPath: projectPathProperty,
|
|
913
913
|
},
|
|
914
914
|
required: ['query'],
|
|
915
915
|
},
|
|
916
|
-
annotations: READ_ONLY_ANNOTATIONS,
|
|
916
|
+
annotations: { ...READ_ONLY_ANNOTATIONS, title: 'HomeGraph Native Exports' },
|
|
917
917
|
},
|
|
918
918
|
{
|
|
919
|
-
name: '
|
|
920
|
-
description: '
|
|
921
|
-
'
|
|
922
|
-
'
|
|
919
|
+
name: 'homegraph_explore',
|
|
920
|
+
description: 'GENERAL PRIMARY entry for understanding THIS repo before you edit or answer structural questions. ' +
|
|
921
|
+
'For an explicit narrow where-used, named module dependency/cycle, or NAPI/native export inventory, choose ' +
|
|
922
|
+
'homegraph_usages, homegraph_modules, or homegraph_native instead; this tool keeps conservative auto-routing only for compatibility. ' +
|
|
923
|
+
'Returns call paths + compact line-numbered source for the relevant symbols. Required: `query`. ' +
|
|
924
|
+
'CALL FIRST (alone, no parallel Grep/Read) when you will change an existing codebase — pass the user task or domain keywords ' +
|
|
925
|
+
'(page/module/feature/component words); locate where to edit before writing code. Also CALL FIRST for how/wired questions, ' +
|
|
926
|
+
'named Type/Component/Page/Dialog, Type.member, click→handler, inheritance/subtypes, declaration/attribute sites, ' +
|
|
927
|
+
'or a cross-symbol mechanism/flow. Use the named focused tools for exact usage, module-topology, or native-export inventories. ' +
|
|
928
|
+
'PascalCase names optional when domain keywords suffice. ' +
|
|
929
|
+
'For edits preserve the requested action, target product/module and exclusions; taskContext may carry the full task. ' +
|
|
930
|
+
'Prefer callers/node when one named symbol is already enough. ' +
|
|
931
|
+
'DO NOT call for topic file-lists with no Type/file, literal copy hunts / pure existence compares with no anchors, ' +
|
|
932
|
+
'official-docs-only asks, empty-project-from-scratch scaffolds, git history, or media/binary asset inventories — those return Skip. ' +
|
|
933
|
+
'@kit / OHOS API questions ARE in scope when the SDK API graph is available — exact where-used → homegraph_usages; ' +
|
|
934
|
+
'mechanism/API-symbol flow → homegraph_explore. ' +
|
|
935
|
+
'Literal string/pattern hunts → Grep; media assets → Glob; git history → git. ' +
|
|
936
|
+
'One explore; then answer or edit from Source + trail — do not re-grep/node/read the same symbols. ' +
|
|
937
|
+
'Overlapping paraphrase explores are refused (name a new Type/file/@kit to continue). ' +
|
|
938
|
+
'Busy/partial → retry ONCE with the named Next anchor or ONE narrow Grep — not a Grep/node storm '
|
|
939
|
+
+ '(session refuses further explore / depth fan-out after Partial).',
|
|
923
940
|
inputSchema: {
|
|
924
941
|
type: 'object',
|
|
925
942
|
properties: {
|
|
926
943
|
query: {
|
|
927
944
|
type: 'string',
|
|
928
|
-
description: 'Required.
|
|
945
|
+
description: 'Required. For pre-edit orientation or how/mechanism: pass the user task or domain keywords (page/module/feature words). ' +
|
|
946
|
+
'For named flows, include Type / Type.member / component names. For @kit mechanism/flow, include module/export tokens; ' +
|
|
947
|
+
'use homegraph_usages for a narrow import/usage inventory, not SDK catalogs.',
|
|
948
|
+
},
|
|
949
|
+
taskContext: {
|
|
950
|
+
type: 'string',
|
|
951
|
+
description: 'Optional original task, including requested changes, product/module scope, exclusions and acceptance requirements. Not source evidence; bounded to 4000 characters.',
|
|
952
|
+
},
|
|
953
|
+
maxFiles: {
|
|
954
|
+
type: 'number',
|
|
955
|
+
description: 'Maximum number of files to include source code from (default: 12)',
|
|
956
|
+
default: 12,
|
|
929
957
|
},
|
|
930
958
|
projectPath: projectPathProperty,
|
|
931
959
|
},
|
|
932
960
|
required: ['query'],
|
|
933
961
|
},
|
|
934
|
-
annotations: READ_ONLY_ANNOTATIONS,
|
|
962
|
+
annotations: { ...READ_ONLY_ANNOTATIONS, title: 'HomeGraph General Explore' },
|
|
935
963
|
},
|
|
936
964
|
{
|
|
937
965
|
name: 'homegraph_status',
|
|
@@ -1277,10 +1305,15 @@ function reconcilePartialAnswerNow(text) {
|
|
|
1277
1305
|
if (!/\*\*Partial locator\*\*/i.test(text))
|
|
1278
1306
|
return text;
|
|
1279
1307
|
let changed = false;
|
|
1308
|
+
let fenced = false;
|
|
1280
1309
|
const out = text
|
|
1281
1310
|
.split('\n')
|
|
1282
1311
|
.map((line) => {
|
|
1283
|
-
if (
|
|
1312
|
+
if (/^\s*```/.test(line)) {
|
|
1313
|
+
fenced = !fenced;
|
|
1314
|
+
return line;
|
|
1315
|
+
}
|
|
1316
|
+
if (fenced || !/ANSWER NOW/i.test(line))
|
|
1284
1317
|
return line;
|
|
1285
1318
|
const stripped = stripPositiveAnswerNowFromLine(line);
|
|
1286
1319
|
if (stripped.changed)
|
|
@@ -1291,7 +1324,7 @@ function reconcilePartialAnswerNow(text) {
|
|
|
1291
1324
|
.join('\n');
|
|
1292
1325
|
if (!changed)
|
|
1293
1326
|
return text;
|
|
1294
|
-
return `${out}\n\n> This survey is a **Partial locator**: the sections above are anchors, not a closed answer.
|
|
1327
|
+
return `${out}\n\n> This survey is a **Partial locator**: the sections above are anchors, not a closed answer. Inspect missing source with a focused lookup, continue the requested edits, and verify the resulting code.`;
|
|
1295
1328
|
}
|
|
1296
1329
|
class ToolHandler {
|
|
1297
1330
|
cg;
|
|
@@ -1482,7 +1515,9 @@ class ToolHandler {
|
|
|
1482
1515
|
'homegraph_diff_impact',
|
|
1483
1516
|
'homegraph_project',
|
|
1484
1517
|
]);
|
|
1485
|
-
|
|
1518
|
+
// An explicit host selection overrides the default size-based surface.
|
|
1519
|
+
// Otherwise small ArkTS repos silently lose requested specialized tools.
|
|
1520
|
+
if (!allow && stats.fileCount < TINY_REPO_FILE_THRESHOLD) {
|
|
1486
1521
|
visible = visible.filter(t => TINY_REPO_CORE_TOOLS.has(t.name));
|
|
1487
1522
|
}
|
|
1488
1523
|
return visible.map(tool => {
|
|
@@ -1876,6 +1911,10 @@ class ToolHandler {
|
|
|
1876
1911
|
* it (the CLI does) and explore behaves exactly as before, untracked.
|
|
1877
1912
|
*/
|
|
1878
1913
|
async execute(toolName, args, sessionState) {
|
|
1914
|
+
const requestStartedAt = Date.now();
|
|
1915
|
+
args = { ...args };
|
|
1916
|
+
for (const key of [QUERY_PLAN_ARG, QUERY_DEADLINE_ARG, QUERY_STARTED_ARG, QUERY_INDEX_STATE_ARG, QUERY_FAST_ATTEMPTED_ARG])
|
|
1917
|
+
delete args[key];
|
|
1879
1918
|
try {
|
|
1880
1919
|
// Block the first tool call on the engine's post-open reconcile so we
|
|
1881
1920
|
// never serve rows for files deleted/edited while no MCP server was
|
|
@@ -1916,13 +1955,37 @@ class ToolHandler {
|
|
|
1916
1955
|
return check;
|
|
1917
1956
|
}
|
|
1918
1957
|
const projectPath = args.projectPath;
|
|
1958
|
+
if (toolName === 'homegraph_explore' && process.env.HOMEGRAPH_QUERY_PLANNER !== 'off') {
|
|
1959
|
+
const query = this.validateString(args.query, 'query');
|
|
1960
|
+
if (typeof query !== 'string')
|
|
1961
|
+
return query;
|
|
1962
|
+
const cg = this.getHomeGraph(projectPath);
|
|
1963
|
+
const gated = this.maybeDeepToolPhaseGate(cg, toolName);
|
|
1964
|
+
if (gated)
|
|
1965
|
+
return gated;
|
|
1966
|
+
// Refused repeats must not spend a model call or re-serve cached evidence.
|
|
1967
|
+
if (sessionState) {
|
|
1968
|
+
const repeat = (0, explore_repeat_guard_1.decideExploreRepeat)(sessionState.forProject(cg.getProjectRoot()), query);
|
|
1969
|
+
if (this.shouldRefuseRepeatedEvidence(repeat, cg.getProjectRoot())) {
|
|
1970
|
+
return this.textResult((0, explore_repeat_guard_1.formatExploreRepeatRefuse)(repeat, query));
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
args[QUERY_STARTED_ARG] = requestStartedAt;
|
|
1974
|
+
args[QUERY_DEADLINE_ARG] = Date.now() + (0, query_pool_1.resolveToolDeadlineMs)();
|
|
1975
|
+
args[QUERY_PLAN_ARG] = await (0, query_plan_1.planQuery)(query, {
|
|
1976
|
+
deadlineAt: args[QUERY_DEADLINE_ARG],
|
|
1977
|
+
taskContext: (0, query_plan_1.mergeQueryPlanTaskContext)(process.env.HOMEGRAPH_QUERY_TASK_CONTEXT, typeof args.taskContext === 'string' ? args.taskContext : undefined),
|
|
1978
|
+
validateAnchor: (anchor) => this.isExactPlanAnchor(cg, anchor),
|
|
1979
|
+
});
|
|
1980
|
+
args[QUERY_INDEX_STATE_ARG] = `${cg.getBuildPhase()}:${cg.getStats().nodeCount}:${cg.getLastIndexedAt() ?? 0}`;
|
|
1981
|
+
}
|
|
1919
1982
|
// Wrong question shapes → short Skip (before cache / graph work).
|
|
1920
1983
|
if (toolName === 'homegraph_explore' || toolName === 'homegraph_search') {
|
|
1921
1984
|
const qEarly = typeof args.query === 'string' ? args.query : '';
|
|
1922
1985
|
if (qEarly) {
|
|
1923
1986
|
const deferKind = (0, query_utils_1.queryShouldDeferToBuiltinTools)(qEarly);
|
|
1924
|
-
if (deferKind) {
|
|
1925
|
-
return this.textResult((0, query_utils_1.homegraphDeferGuidance)(deferKind, qEarly));
|
|
1987
|
+
if (deferKind && readQueryPlan(args)?.source !== 'llm') {
|
|
1988
|
+
return this.withQueryPlanDiagnostics(this.textResult((0, query_utils_1.homegraphDeferGuidance)(deferKind, qEarly)), args);
|
|
1926
1989
|
}
|
|
1927
1990
|
}
|
|
1928
1991
|
}
|
|
@@ -1939,7 +2002,9 @@ class ToolHandler {
|
|
|
1939
2002
|
// Session-tracked explore must not hit the MCP query cache: a cache hit
|
|
1940
2003
|
// would re-serve the first call's full source and defeat CG-18 dedup.
|
|
1941
2004
|
const skipCacheForSession = toolName === 'homegraph_explore' && !!sessionState;
|
|
1942
|
-
const
|
|
2005
|
+
const requestPlan = readQueryPlan(args);
|
|
2006
|
+
const cacheEnabled = !skipCacheForSession && requestPlan?.source !== 'llm'
|
|
2007
|
+
&& !requestPlan?.telemetry.fallbackReason && (0, query_cache_1.isMcpQueryCacheEnabled)() && (0, query_cache_1.isCacheableMcpTool)(toolName);
|
|
1943
2008
|
let cacheKey;
|
|
1944
2009
|
let cacheQueries;
|
|
1945
2010
|
let cacheIndex;
|
|
@@ -1959,7 +2024,8 @@ class ToolHandler {
|
|
|
1959
2024
|
cacheKey = (0, query_cache_1.buildMcpQueryCacheKey)(toolName, args, fileCount);
|
|
1960
2025
|
const cached = cacheIndex.getEntry(cacheQueries, cacheKey);
|
|
1961
2026
|
if (cached) {
|
|
1962
|
-
const
|
|
2027
|
+
const diagnosed = this.withQueryPlanDiagnostics(cached, args, true);
|
|
2028
|
+
const withWorktree = this.withWorktreeNotice(diagnosed, projectPath);
|
|
1963
2029
|
return this.withStalenessNotice(withWorktree, projectPath);
|
|
1964
2030
|
}
|
|
1965
2031
|
}
|
|
@@ -1993,22 +2059,25 @@ class ToolHandler {
|
|
|
1993
2059
|
// main connection. Serving them here — before the query-pool offload —
|
|
1994
2060
|
// avoids cold-worker / wedged-daemon paths that otherwise surface as empty
|
|
1995
2061
|
// MCP client `-32001` (the handler itself is fine; the transport times out).
|
|
1996
|
-
if (toolName === 'homegraph_explore' || toolName === 'homegraph_search')
|
|
2062
|
+
if ((toolName === 'homegraph_explore' || toolName === 'homegraph_search')
|
|
2063
|
+
&& (!requestPlan || (requestPlan.source === 'rules' && requestPlan.steps.length === 1
|
|
2064
|
+
&& !requestPlan.telemetry.requestCount))) {
|
|
1997
2065
|
const q = typeof args.query === 'string' ? args.query : '';
|
|
1998
2066
|
if (q) {
|
|
1999
2067
|
try {
|
|
2000
2068
|
const cgFast = this.getHomeGraph(projectPath);
|
|
2001
2069
|
const rootFast = cgFast.getProjectRoot();
|
|
2002
|
-
const fast = (
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
?? (toolName === 'homegraph_explore' || toolName === 'homegraph_search'
|
|
2006
|
-
? this.tryFastInventoryExplore(cgFast, q, rootFast)
|
|
2070
|
+
const fast = requestPlan ? this.tryPlannedFastPath(cgFast, requestPlan, rootFast) :
|
|
2071
|
+
(toolName === 'homegraph_explore'
|
|
2072
|
+
? this.trySpecializedExploreRoute(cgFast, q, rootFast)
|
|
2007
2073
|
: null)
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2074
|
+
?? (toolName === 'homegraph_explore' || toolName === 'homegraph_search'
|
|
2075
|
+
? this.tryFastInventoryExplore(cgFast, q, rootFast)
|
|
2076
|
+
: null)
|
|
2077
|
+
?? (toolName === 'homegraph_explore' || toolName === 'homegraph_search'
|
|
2078
|
+
? this.tryLightMechanismExplore(cgFast, q, rootFast)
|
|
2079
|
+
: null)
|
|
2080
|
+
?? this.tryCompactLocalSymbolExplore(cgFast, q, rootFast);
|
|
2012
2081
|
if (fast) {
|
|
2013
2082
|
// Fast explore must still file Partial/ANSWER meta into the session
|
|
2014
2083
|
// so explore + depth fuses see it (textResult-only paths used to skip).
|
|
@@ -2016,12 +2085,15 @@ class ToolHandler {
|
|
|
2016
2085
|
if (toolName === 'homegraph_explore' && sessionState) {
|
|
2017
2086
|
served = this.takeExploreEmission(this.ensureExploreEmission(fast, rootFast, q), sessionState);
|
|
2018
2087
|
}
|
|
2088
|
+
served = this.withQueryPlanDiagnostics(served, args);
|
|
2019
2089
|
if (cacheEnabled && cacheKey && cacheQueries && cacheIndex && !served.isError) {
|
|
2020
2090
|
cacheIndex.setEntry(cacheQueries, cacheKey, toolName, served);
|
|
2021
2091
|
}
|
|
2022
2092
|
const withWorktree = this.withWorktreeNotice(served, projectPath);
|
|
2023
2093
|
return this.withStalenessNotice(withWorktree, projectPath);
|
|
2024
2094
|
}
|
|
2095
|
+
if (requestPlan)
|
|
2096
|
+
args[QUERY_FAST_ATTEMPTED_ARG] = true;
|
|
2025
2097
|
}
|
|
2026
2098
|
catch {
|
|
2027
2099
|
// Not indexed / path issue — fall through to normal dispatch.
|
|
@@ -2037,7 +2109,7 @@ class ToolHandler {
|
|
|
2037
2109
|
// (a frozen main loop prevents setTimeout deadlines from firing → empty
|
|
2038
2110
|
// `-32001`). Fast-path surveys run inside the worker via executeReadTool.
|
|
2039
2111
|
const raw = await this.runReadToolWithDeadline(toolName, dispatchArgs);
|
|
2040
|
-
const result = this.takeExploreEmission(raw, sessionState);
|
|
2112
|
+
const result = this.withQueryPlanDiagnostics(this.takeExploreEmission(raw, sessionState), args);
|
|
2041
2113
|
if (sessionState
|
|
2042
2114
|
&& !result.isError
|
|
2043
2115
|
&& (toolName === 'homegraph_node'
|
|
@@ -2113,16 +2185,67 @@ class ToolHandler {
|
|
|
2113
2185
|
return null;
|
|
2114
2186
|
}
|
|
2115
2187
|
}
|
|
2188
|
+
/** A repeated query is duplicate evidence only while its source is unchanged. */
|
|
2189
|
+
shouldRefuseRepeatedEvidence(decision, projectRoot) {
|
|
2190
|
+
if (!decision.refuse)
|
|
2191
|
+
return false;
|
|
2192
|
+
// Source changes do not reset the total retrieval budget.
|
|
2193
|
+
if (decision.reason !== 'overlap')
|
|
2194
|
+
return true;
|
|
2195
|
+
const files = decision.matched?.files.filter((file) => file.bytes > 0 && file.ranges.length > 0) ?? [];
|
|
2196
|
+
if (files.length === 0 || files.length > 24)
|
|
2197
|
+
return false;
|
|
2198
|
+
let remainingBytes = 2 * 1024 * 1024;
|
|
2199
|
+
for (const file of files) {
|
|
2200
|
+
if (!file.fingerprint)
|
|
2201
|
+
return false;
|
|
2202
|
+
const absolute = (0, utils_1.validatePathWithinRoot)(projectRoot, file.path);
|
|
2203
|
+
if (!absolute)
|
|
2204
|
+
return false;
|
|
2205
|
+
let descriptor;
|
|
2206
|
+
try {
|
|
2207
|
+
const stat = (0, fs_1.statSync)(absolute);
|
|
2208
|
+
const limit = Math.min(1024 * 1024, remainingBytes);
|
|
2209
|
+
if (!stat.isFile() || stat.size > limit)
|
|
2210
|
+
return false;
|
|
2211
|
+
// Fixed-size reads also remain bounded if the file grows after stat.
|
|
2212
|
+
descriptor = (0, fs_1.openSync)(absolute, 'r');
|
|
2213
|
+
const buffer = Buffer.alloc(stat.size + 1);
|
|
2214
|
+
let bytes = 0;
|
|
2215
|
+
while (bytes < buffer.length) {
|
|
2216
|
+
const read = (0, fs_1.readSync)(descriptor, buffer, bytes, buffer.length - bytes, bytes);
|
|
2217
|
+
if (read === 0)
|
|
2218
|
+
break;
|
|
2219
|
+
bytes += read;
|
|
2220
|
+
}
|
|
2221
|
+
if (bytes !== stat.size)
|
|
2222
|
+
return false;
|
|
2223
|
+
remainingBytes -= bytes;
|
|
2224
|
+
if ((0, explore_dedup_1.fileFingerprint)(buffer.subarray(0, bytes).toString('utf8')) !== file.fingerprint)
|
|
2225
|
+
return false;
|
|
2226
|
+
}
|
|
2227
|
+
catch {
|
|
2228
|
+
return false;
|
|
2229
|
+
}
|
|
2230
|
+
finally {
|
|
2231
|
+
if (descriptor !== undefined) {
|
|
2232
|
+
try {
|
|
2233
|
+
(0, fs_1.closeSync)(descriptor);
|
|
2234
|
+
}
|
|
2235
|
+
catch { /* bookkeeping only */ }
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
return true;
|
|
2240
|
+
}
|
|
2116
2241
|
/** Count a successful depth-tool call when the latest explore was Partial. */
|
|
2117
2242
|
noteDepthToolUse(args, sessionState) {
|
|
2118
2243
|
try {
|
|
2119
2244
|
const cg = this.getHomeGraph(args.projectPath);
|
|
2120
2245
|
const root = cg.getProjectRoot();
|
|
2121
2246
|
const prior = sessionState.forProject(root);
|
|
2122
|
-
const last = prior?.calls.length
|
|
2123
|
-
|
|
2124
|
-
: undefined;
|
|
2125
|
-
if (!last?.partial)
|
|
2247
|
+
const last = prior?.calls[prior.calls.length - 1];
|
|
2248
|
+
if (!last || (0, explore_session_state_1.inferExploreEvidenceStatus)(last) === 'complete')
|
|
2126
2249
|
return;
|
|
2127
2250
|
sessionState.recordDepthTool(root);
|
|
2128
2251
|
}
|
|
@@ -2142,17 +2265,22 @@ class ToolHandler {
|
|
|
2142
2265
|
const emission = result?.[explore_session_state_1.EXPLORE_EMISSION_KEY];
|
|
2143
2266
|
if (emission === undefined)
|
|
2144
2267
|
return result;
|
|
2268
|
+
emission.evidenceStatus = (0, explore_session_state_1.inferExploreEvidenceStatus)(emission);
|
|
2269
|
+
emission.partial = emission.evidenceStatus !== 'complete';
|
|
2270
|
+
result._meta = { ...result._meta, homegraphEvidence: { status: emission.evidenceStatus,
|
|
2271
|
+
files: emission.files, locatedNodes: emission.locatedNodes, coveredObligations: emission.coveredObligations,
|
|
2272
|
+
uncoveredObligations: emission.uncoveredObligations } };
|
|
2145
2273
|
delete result[explore_session_state_1.EXPLORE_EMISSION_KEY];
|
|
2146
2274
|
if (sessionState) {
|
|
2147
2275
|
try {
|
|
2148
2276
|
const prior = sessionState.forProject(emission.projectRoot);
|
|
2149
|
-
const priorPartials = (prior?.calls ?? []).filter((c) =>
|
|
2277
|
+
const priorPartials = (prior?.calls ?? []).filter((c) => (0, explore_session_state_1.inferExploreEvidenceStatus)(c) !== 'complete');
|
|
2150
2278
|
const metaPartial = emission.partial === true
|
|
2151
2279
|
|| (emission.partial === undefined
|
|
2152
2280
|
&& (0, explore_repeat_guard_1.inferExplorePartialMeta)(result.content?.[0]?.text ?? '').partial);
|
|
2153
2281
|
if (metaPartial && priorPartials.length >= 1) {
|
|
2154
2282
|
const text = result.content?.[0]?.text ?? '';
|
|
2155
|
-
if (text && !/Second Partial
|
|
2283
|
+
if (text && !/Second Partial/i.test(text)) {
|
|
2156
2284
|
const stop = (0, explore_repeat_guard_1.formatSecondPartialStopFooter)(emission.nextAnchor);
|
|
2157
2285
|
result = {
|
|
2158
2286
|
...result,
|
|
@@ -2185,13 +2313,19 @@ class ToolHandler {
|
|
|
2185
2313
|
* the success-shaped reply never flushed → empty client `-32001`.
|
|
2186
2314
|
*/
|
|
2187
2315
|
async runReadToolWithDeadline(toolName, args) {
|
|
2188
|
-
const deadlineMs =
|
|
2316
|
+
const deadlineMs = typeof args[QUERY_DEADLINE_ARG] === 'number'
|
|
2317
|
+
? Math.max(0, Math.min((0, query_pool_1.resolveToolDeadlineMs)(), args[QUERY_DEADLINE_ARG] - Date.now()))
|
|
2318
|
+
: (0, query_pool_1.resolveToolDeadlineMs)();
|
|
2319
|
+
if (deadlineMs <= 0)
|
|
2320
|
+
return this.deadlineBusyResult((0, query_pool_1.resolveToolDeadlineMs)(), args);
|
|
2189
2321
|
const light = toolName === 'homegraph_search'
|
|
2190
2322
|
|| toolName === 'homegraph_node'
|
|
2191
2323
|
|| toolName === 'homegraph_callers'
|
|
2192
2324
|
|| toolName === 'homegraph_callees'
|
|
2193
2325
|
|| toolName === 'homegraph_files'
|
|
2194
|
-
|| toolName === 'homegraph_project'
|
|
2326
|
+
|| toolName === 'homegraph_project'
|
|
2327
|
+
// Project maps may be built on demand: never run them on a read worker.
|
|
2328
|
+
|| (readQueryPlan(args)?.steps.some((step) => step.intent === 'overview') ?? false);
|
|
2195
2329
|
const work = () => {
|
|
2196
2330
|
if (!light && this.queryPool && this.queryPool.healthy) {
|
|
2197
2331
|
return this.queryPool.run(toolName, args, {
|
|
@@ -2246,6 +2380,14 @@ class ToolHandler {
|
|
|
2246
2380
|
*/
|
|
2247
2381
|
async executeReadTool(toolName, args) {
|
|
2248
2382
|
try {
|
|
2383
|
+
if (toolName !== 'homegraph_project' && toolName !== 'homegraph_status') {
|
|
2384
|
+
const gate = this.maybeDeepToolPhaseGate(this.getHomeGraph(args.projectPath), toolName);
|
|
2385
|
+
if (gate)
|
|
2386
|
+
return gate;
|
|
2387
|
+
}
|
|
2388
|
+
const plan = toolName === 'homegraph_explore' ? readQueryPlan(args) : undefined;
|
|
2389
|
+
if (plan)
|
|
2390
|
+
return await this.executeQueryPlan(args, plan);
|
|
2249
2391
|
// Compact inventory / one-symbol surveys — safe on the worker (keeps the
|
|
2250
2392
|
// daemon main loop free). Never run these unprotected on the MCP transport
|
|
2251
2393
|
// thread: they can block long enough for the client to emit empty `-32001`.
|
|
@@ -5131,9 +5273,261 @@ class ToolHandler {
|
|
|
5131
5273
|
lines.push('');
|
|
5132
5274
|
return { section: lines.join('\n'), hitCount: locs.length + edges.length + extendsHits.length };
|
|
5133
5275
|
}
|
|
5134
|
-
/**
|
|
5135
|
-
|
|
5136
|
-
|
|
5276
|
+
/** Exact validation for model-proposed names: a fuzzy hit is not proof. */
|
|
5277
|
+
isExactPlanAnchor(cg, anchor) {
|
|
5278
|
+
if (!anchor || anchor.length > 256)
|
|
5279
|
+
return false;
|
|
5280
|
+
try {
|
|
5281
|
+
const matches = cg.searchNodes(anchor, { limit: 12 });
|
|
5282
|
+
return matches.some(({ node }) => node.name === anchor || node.qualifiedName === anchor
|
|
5283
|
+
|| node.filePath.replace(/\\/g, '/') === anchor.replace(/\\/g, '/'));
|
|
5284
|
+
}
|
|
5285
|
+
catch {
|
|
5286
|
+
return false;
|
|
5287
|
+
}
|
|
5288
|
+
}
|
|
5289
|
+
/** Bind only declarations returned by this step, not input or global fuzzy hits. */
|
|
5290
|
+
locatedPlanBindings(cg, result, inherited) {
|
|
5291
|
+
const seen = new Set(inherited.map((binding) => binding.id));
|
|
5292
|
+
const declarations = new Set(inherited.map((binding) => `${binding.filePath}:${binding.startLine}:${binding.name}`));
|
|
5293
|
+
const located = [];
|
|
5294
|
+
for (const receipt of (result[explore_session_state_1.EXPLORE_EMISSION_KEY]?.locatedNodes ?? []).slice(0, 32)) {
|
|
5295
|
+
if (seen.has(receipt.id) || declarations.has(`${receipt.filePath}:${receipt.startLine}:${receipt.name}`))
|
|
5296
|
+
continue;
|
|
5297
|
+
const node = cg.getNode(receipt.id);
|
|
5298
|
+
if (!node || ['file', 'import', 'export', 'parameter'].includes(node.kind)
|
|
5299
|
+
|| node.name === 'constructor' || node.name.startsWith('%AM') || node.filePath.includes('@dummy')
|
|
5300
|
+
|| node.name !== receipt.name || node.filePath !== receipt.filePath || node.startLine !== receipt.startLine
|
|
5301
|
+
|| node.qualifiedName !== receipt.qualifiedName)
|
|
5302
|
+
continue;
|
|
5303
|
+
seen.add(node.id);
|
|
5304
|
+
declarations.add(`${node.filePath}:${node.startLine}:${node.name}`);
|
|
5305
|
+
located.push({ id: node.id, name: node.name, qualifiedName: node.qualifiedName,
|
|
5306
|
+
filePath: node.filePath, startLine: node.startLine });
|
|
5307
|
+
if (located.length >= 8)
|
|
5308
|
+
break;
|
|
5309
|
+
}
|
|
5310
|
+
return located;
|
|
5311
|
+
}
|
|
5312
|
+
withQueryPlanDiagnostics(result, args, cacheHit = false) {
|
|
5313
|
+
const plan = readQueryPlan(args);
|
|
5314
|
+
if (!plan)
|
|
5315
|
+
return result;
|
|
5316
|
+
const prior = result._meta?.homegraphQueryPlan;
|
|
5317
|
+
const started = typeof args[QUERY_STARTED_ARG] === 'number' ? args[QUERY_STARTED_ARG] : Date.now();
|
|
5318
|
+
return { ...result, _meta: { ...result._meta, homegraphQueryPlan: {
|
|
5319
|
+
...prior, version: plan.version, source: plan.source, intent: plan.intent, route: plan.route,
|
|
5320
|
+
confidence: plan.confidence,
|
|
5321
|
+
plannerSeeds: { anchors: plan.anchors, searchTerms: plan.searchTerms, literalTexts: plan.literalTexts,
|
|
5322
|
+
sourceScope: plan.sourceScope, relation: plan.relation },
|
|
5323
|
+
hasTaskContext: !!plan.taskContext,
|
|
5324
|
+
matchedFeatures: Object.entries(plan.features).filter(([, matched]) => matched).map(([name]) => name).slice(0, 12),
|
|
5325
|
+
planningMs: plan.telemetry.durationMs, durationMs: Math.max(plan.telemetry.durationMs, Date.now() - started),
|
|
5326
|
+
modelRequests: plan.telemetry.requestCount ?? 0,
|
|
5327
|
+
planningEligible: plan.telemetry.decision?.eligible,
|
|
5328
|
+
planningReason: plan.telemetry.decision?.reason,
|
|
5329
|
+
skip_reason: plan.telemetry.decision?.eligible === false ? plan.telemetry.decision.reason : undefined,
|
|
5330
|
+
ruleRoute: plan.telemetry.decision?.ruleRoute,
|
|
5331
|
+
inputTokens: plan.telemetry.inputTokens ?? (plan.telemetry.requestCount ? null : 0),
|
|
5332
|
+
outputTokens: plan.telemetry.outputTokens ?? (plan.telemetry.requestCount ? null : 0),
|
|
5333
|
+
fallbackReason: plan.telemetry.fallbackReason, cacheHit,
|
|
5334
|
+
steps: cacheHit ? [] : prior?.steps ?? [{ id: plan.steps[0]?.id, intent: plan.intent,
|
|
5335
|
+
status: result.isError ? 'failed'
|
|
5336
|
+
: (0, explore_repeat_guard_1.inferExplorePartialMeta)(result.content[0]?.text ?? '').partial ? 'partial'
|
|
5337
|
+
: /Status: (?:no_indexed_evidence|not_surveyed)|No relevant code|Skip HomeGraph/i.test(result.content[0]?.text ?? '')
|
|
5338
|
+
? 'no_evidence' : 'evidence', resolvedAnchors: [] }],
|
|
5339
|
+
} } };
|
|
5340
|
+
}
|
|
5341
|
+
/** Select once; legacy section builders consume the same canonical query/features. */
|
|
5342
|
+
tryPlannedFastPath(cg, plan, root) {
|
|
5343
|
+
const query = plan.canonicalQuery;
|
|
5344
|
+
if (plan.route === 'usages' || plan.route === 'modules' || plan.route === 'native') {
|
|
5345
|
+
return this.runSpecializedExploreRoute(plan.route, cg, query, root, plan);
|
|
5346
|
+
}
|
|
5347
|
+
// These legacy paths re-extract seeds from text and cannot consume bound
|
|
5348
|
+
// node identity / step hints. Model general/flow plans use full explore;
|
|
5349
|
+
// rule/default and specialized routes retain their existing fast behavior.
|
|
5350
|
+
if (plan.source === 'llm' && (plan.intent === 'general' || plan.intent === 'flow'))
|
|
5351
|
+
return null;
|
|
5352
|
+
return this.tryFastInventoryExplore(cg, query, root, plan)
|
|
5353
|
+
?? this.tryLightMechanismExplore(cg, query, root, plan)
|
|
5354
|
+
?? this.tryCompactLocalSymbolExplore(cg, query, root, plan);
|
|
5355
|
+
}
|
|
5356
|
+
/** Internal execution only: no recursive MCP calls, model requests or new deadline. */
|
|
5357
|
+
async executePlannedStep(args, plan) {
|
|
5358
|
+
const cg = this.getHomeGraph(args.projectPath);
|
|
5359
|
+
if (plan.intent === 'overview') {
|
|
5360
|
+
return this.handleProject({ projectPath: args.projectPath });
|
|
5361
|
+
}
|
|
5362
|
+
const gate = this.maybeDeepToolPhaseGate(cg, 'homegraph_explore');
|
|
5363
|
+
if (gate)
|
|
5364
|
+
return gate;
|
|
5365
|
+
const query = plan.canonicalQuery;
|
|
5366
|
+
const defer = (0, query_utils_1.queryShouldDeferToBuiltinTools)(query);
|
|
5367
|
+
if (defer && plan.source === 'rules')
|
|
5368
|
+
return this.textResult((0, query_utils_1.homegraphDeferGuidance)(defer, query));
|
|
5369
|
+
// A typed relationship still needs a real target. Discover source from the
|
|
5370
|
+
// same bounded hints first; do not turn an unanchored usage request into an
|
|
5371
|
+
// empty survey or claim that its reference obligation has been covered.
|
|
5372
|
+
if (plan.source === 'llm' && plan.route === 'usages'
|
|
5373
|
+
&& !plan.anchors.length && !plan.bindings?.length) {
|
|
5374
|
+
const result = await this.handleExplore({ ...args, query, [QUERY_PLAN_ARG]: plan });
|
|
5375
|
+
const served = this.ensureExploreEmission(result, cg.getProjectRoot(), plan.originalQuery);
|
|
5376
|
+
const emission = served[explore_session_state_1.EXPLORE_EMISSION_KEY];
|
|
5377
|
+
emission.partial = true;
|
|
5378
|
+
emission.evidenceStatus = emission.sourceBytes > 0 ? 'partial' : 'empty';
|
|
5379
|
+
emission.coveredObligations = [];
|
|
5380
|
+
emission.uncoveredObligations = [plan.relation ?? 'incoming_references'];
|
|
5381
|
+
if (served.content[0]?.type === 'text')
|
|
5382
|
+
served.content[0].text =
|
|
5383
|
+
'**Reference target discovery — partial**\nSource candidates follow. Verify the relevant target before surveying its references; the reference obligation remains open.\n\n'
|
|
5384
|
+
+ served.content[0].text;
|
|
5385
|
+
return served;
|
|
5386
|
+
}
|
|
5387
|
+
const fast = args[QUERY_FAST_ATTEMPTED_ARG] === true && plan.source === 'rules'
|
|
5388
|
+
? null : this.tryPlannedFastPath(cg, plan, cg.getProjectRoot());
|
|
5389
|
+
if (fast)
|
|
5390
|
+
return fast;
|
|
5391
|
+
return this.handleExplore({ ...args, query, [QUERY_PLAN_ARG]: plan });
|
|
5392
|
+
}
|
|
5393
|
+
async executeQueryPlan(args, plan) {
|
|
5394
|
+
const cg = this.getHomeGraph(args.projectPath);
|
|
5395
|
+
const root = cg.getProjectRoot();
|
|
5396
|
+
const deadline = typeof args[QUERY_DEADLINE_ARG] === 'number'
|
|
5397
|
+
? args[QUERY_DEADLINE_ARG] : Date.now() + (0, query_pool_1.resolveToolDeadlineMs)();
|
|
5398
|
+
const prior = (0, explore_session_state_1.viewForProject)((0, explore_session_state_1.readExploreSessionView)(args), root);
|
|
5399
|
+
const repeat = (0, explore_repeat_guard_1.decideExploreRepeat)(prior, plan.originalQuery);
|
|
5400
|
+
if (this.shouldRefuseRepeatedEvidence(repeat, root)) {
|
|
5401
|
+
return this.textResult((0, explore_repeat_guard_1.formatExploreRepeatRefuse)(repeat, plan.originalQuery));
|
|
5402
|
+
}
|
|
5403
|
+
const multi = plan.steps.length > 1;
|
|
5404
|
+
let outputBudget = Math.min(MAX_OUTPUT_LENGTH, getExploreOutputBudget(cg.getStats().fileCount).maxOutputChars);
|
|
5405
|
+
if (!Number.isFinite(outputBudget))
|
|
5406
|
+
outputBudget = MAX_OUTPUT_LENGTH;
|
|
5407
|
+
const constraints = [plan.originalQuery, plan.taskContext].filter(Boolean).join('\n');
|
|
5408
|
+
const constraintLimit = Math.min(2000, Math.floor(outputBudget / 5));
|
|
5409
|
+
const constraintNotice = constraints.length > constraintLimit
|
|
5410
|
+
? constraints.slice(0, constraintLimit) + '\n[Constraint display shortened; the original task remains authoritative.]'
|
|
5411
|
+
: constraints;
|
|
5412
|
+
const preamble = [
|
|
5413
|
+
'**Planned exploration — Partial locator**',
|
|
5414
|
+
'> **Partial locator** — scoped subquestion evidence, not proof that every requirement in the original question is covered.',
|
|
5415
|
+
'**Task constraints (not search seeds or verified source evidence)**\n' + constraintNotice,
|
|
5416
|
+
].join('\n\n');
|
|
5417
|
+
const pieces = [];
|
|
5418
|
+
const diagnostics = [];
|
|
5419
|
+
const bindings = new Map();
|
|
5420
|
+
const files = [];
|
|
5421
|
+
const seenQueries = new Set();
|
|
5422
|
+
let single;
|
|
5423
|
+
let used = multi ? preamble.length + 400 : 400;
|
|
5424
|
+
for (const step of plan.steps.slice(0, 3)) {
|
|
5425
|
+
const started = Date.now();
|
|
5426
|
+
const diagnostic = { id: step.id, intent: step.intent, status: 'pending', resolvedAnchors: [],
|
|
5427
|
+
locatedNodes: [], durationMs: 0 };
|
|
5428
|
+
diagnostics.push(diagnostic);
|
|
5429
|
+
if (Date.now() >= deadline || used >= outputBudget) {
|
|
5430
|
+
diagnostic.status = 'budget_exhausted';
|
|
5431
|
+
pieces.push(`**Step ${step.id}: ${step.intent}** — not executed: shared request budget exhausted.`);
|
|
5432
|
+
continue;
|
|
5433
|
+
}
|
|
5434
|
+
if (step.dependsOn.some((id) => !bindings.get(id)?.length)) {
|
|
5435
|
+
diagnostic.status = 'dependency_unresolved';
|
|
5436
|
+
pieces.push(`**Step ${step.id}: ${step.intent}** — not executed: predecessor supplied no resolved symbol anchors.`);
|
|
5437
|
+
continue;
|
|
5438
|
+
}
|
|
5439
|
+
const resolved = step.dependsOn.flatMap((id) => bindings.get(id) ?? []);
|
|
5440
|
+
// Keep rule-only single queries byte-compatible; only model subplans are adapted.
|
|
5441
|
+
const compiled = plan.source === 'rules' && !multi ? plan : {
|
|
5442
|
+
...(0, query_plan_1.compileQueryPlanStep)(plan, step, resolved.map((node) => node.qualifiedName || node.name)), bindings: resolved,
|
|
5443
|
+
};
|
|
5444
|
+
if (seenQueries.has(`${compiled.intent}:${compiled.canonicalQuery}`)) {
|
|
5445
|
+
diagnostic.status = 'duplicate_skipped';
|
|
5446
|
+
pieces.push(`**Step ${step.id}: ${step.intent}** — duplicate query skipped.`);
|
|
5447
|
+
continue;
|
|
5448
|
+
}
|
|
5449
|
+
seenQueries.add(`${compiled.intent}:${compiled.canonicalQuery}`);
|
|
5450
|
+
try {
|
|
5451
|
+
const result = await this.executePlannedStep({ ...args, [QUERY_DEADLINE_ARG]: deadline }, compiled);
|
|
5452
|
+
const body = result.content.map((part) => part.text).join('\n');
|
|
5453
|
+
const candidates = result.isError ? [] : this.locatedPlanBindings(cg, result, resolved);
|
|
5454
|
+
const childEmission = result[explore_session_state_1.EXPLORE_EMISSION_KEY];
|
|
5455
|
+
const hasLocalEvidence = candidates.length > 0 || (childEmission?.sourceBytes ?? 0) > 0;
|
|
5456
|
+
diagnostic.status = result.isError ? 'failed'
|
|
5457
|
+
: childEmission?.evidenceStatus === 'complete' ? 'evidence'
|
|
5458
|
+
: hasLocalEvidence ? (childEmission?.partial ? 'partial' : 'evidence')
|
|
5459
|
+
: /HarmonyOS SDK API|ohos-sdk:/.test(body) ? 'sdk_only' : 'no_evidence';
|
|
5460
|
+
if (!multi) {
|
|
5461
|
+
diagnostic.locatedNodes = candidates;
|
|
5462
|
+
diagnostic.resolvedAnchors = candidates.map((node) => node.name);
|
|
5463
|
+
single = result;
|
|
5464
|
+
break;
|
|
5465
|
+
}
|
|
5466
|
+
// A child cannot declare the original multi-part question answered.
|
|
5467
|
+
let fenced = false;
|
|
5468
|
+
const neutralBody = body.split('\n').filter((line) => {
|
|
5469
|
+
if (/^\s*```/.test(line)) {
|
|
5470
|
+
fenced = !fenced;
|
|
5471
|
+
return true;
|
|
5472
|
+
}
|
|
5473
|
+
return fenced || !/ANSWER NOW|Compact local explore complete|Do \*\*not\*\* (?:Read|Grep)|ONE tighter|ONE narrow/i.test(line);
|
|
5474
|
+
}).join('\n');
|
|
5475
|
+
const cap = Math.max(0, Math.floor((outputBudget - used) / Math.max(1, plan.steps.length - diagnostics.length + 1)) - 100);
|
|
5476
|
+
// Keep the grounded location receipt before source trimming. Only the
|
|
5477
|
+
// identities visibly delivered here may be consumed by the next step.
|
|
5478
|
+
let receipt = '';
|
|
5479
|
+
const visibleBindings = [];
|
|
5480
|
+
const inline = (value) => value.replace(/[`\r\n]/g, ' ');
|
|
5481
|
+
for (const candidate of candidates) {
|
|
5482
|
+
const next = (receipt ? '' : '**Located source candidates — relevance still needs verification**\n')
|
|
5483
|
+
+ `- \`${inline(candidate.qualifiedName || candidate.name)}\` — \`${inline(candidate.filePath)}:${candidate.startLine}\`\n`;
|
|
5484
|
+
if (receipt.length + next.length > Math.min(1600, Math.floor(cap / 2)))
|
|
5485
|
+
break;
|
|
5486
|
+
receipt += next;
|
|
5487
|
+
visibleBindings.push(candidate);
|
|
5488
|
+
}
|
|
5489
|
+
diagnostic.locatedNodes = visibleBindings;
|
|
5490
|
+
diagnostic.resolvedAnchors = visibleBindings.map((node) => node.name);
|
|
5491
|
+
bindings.set(step.id, visibleBindings);
|
|
5492
|
+
const bodyCap = Math.max(0, cap - receipt.length - 2);
|
|
5493
|
+
const trimmed = neutralBody.length > bodyCap;
|
|
5494
|
+
const kept = (0, evidence_rendering_1.trimEvidenceAtLine)(neutralBody, bodyCap);
|
|
5495
|
+
if (trimmed)
|
|
5496
|
+
diagnostic.status = 'partial';
|
|
5497
|
+
else
|
|
5498
|
+
files.push(...(result[explore_session_state_1.EXPLORE_EMISSION_KEY]?.files ?? []));
|
|
5499
|
+
const piece = `**Step ${step.id}: ${step.intent}** (${diagnostic.status})\n${receipt}\n${kept}`;
|
|
5500
|
+
pieces.push(piece);
|
|
5501
|
+
used += piece.length;
|
|
5502
|
+
}
|
|
5503
|
+
catch (error) {
|
|
5504
|
+
if (error instanceof PathRefusalError)
|
|
5505
|
+
throw error;
|
|
5506
|
+
diagnostic.status = 'failed';
|
|
5507
|
+
pieces.push(`**Step ${step.id}: ${step.intent}** — retrieval failed; other step evidence is retained.`);
|
|
5508
|
+
}
|
|
5509
|
+
finally {
|
|
5510
|
+
diagnostic.durationMs = Date.now() - started;
|
|
5511
|
+
}
|
|
5512
|
+
// Yield between synchronous graph stages so deadlines and MCP I/O can run.
|
|
5513
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
5514
|
+
}
|
|
5515
|
+
const result = single ?? this.textResult([preamble, ...pieces].join('\n\n').slice(0, outputBudget));
|
|
5516
|
+
const served = this.ensureExploreEmission(result, root, plan.originalQuery);
|
|
5517
|
+
const emission = served[explore_session_state_1.EXPLORE_EMISSION_KEY];
|
|
5518
|
+
emission.query = plan.originalQuery;
|
|
5519
|
+
if (multi) {
|
|
5520
|
+
emission.partial = true;
|
|
5521
|
+
emission.evidenceStatus = 'partial';
|
|
5522
|
+
emission.coveredObligations = diagnostics.filter((step) => step.status === 'evidence').map((step) => step.id);
|
|
5523
|
+
emission.uncoveredObligations = diagnostics.filter((step) => step.status !== 'evidence').map((step) => step.id);
|
|
5524
|
+
emission.files = files;
|
|
5525
|
+
emission.sourceBytes = files.reduce((sum, file) => sum + file.bytes, 0);
|
|
5526
|
+
}
|
|
5527
|
+
served._meta = { ...served._meta, homegraphQueryPlan: { steps: diagnostics } };
|
|
5528
|
+
return served;
|
|
5529
|
+
}
|
|
5530
|
+
/** Main-thread fast path for inventory surveys — skips the worker queue. */
|
|
5137
5531
|
tryFastPathResult(toolName, args) {
|
|
5138
5532
|
const query = args.query;
|
|
5139
5533
|
if (typeof query !== 'string')
|
|
@@ -5181,11 +5575,13 @@ class ToolHandler {
|
|
|
5181
5575
|
: null;
|
|
5182
5576
|
}
|
|
5183
5577
|
/** Run exactly one bounded survey family and expose the selected route. */
|
|
5184
|
-
runSpecializedExploreRoute(route, cg, query, projectRoot) {
|
|
5578
|
+
runSpecializedExploreRoute(route, cg, query, projectRoot, plan) {
|
|
5185
5579
|
let section = '';
|
|
5186
5580
|
let status = 'no_indexed_evidence';
|
|
5187
5581
|
let coverage = '';
|
|
5188
5582
|
if (route === 'modules') {
|
|
5583
|
+
if (plan?.relation === 'module_imports')
|
|
5584
|
+
return this.renderModuleImports(cg, plan);
|
|
5189
5585
|
const manifestResult = this.buildFocusedModuleManifestSection(projectRoot, query);
|
|
5190
5586
|
const graphResult = manifestResult.manifestCount === 0
|
|
5191
5587
|
? this.buildModuleDependencySurveySection(cg, query)
|
|
@@ -5203,10 +5599,10 @@ class ToolHandler {
|
|
|
5203
5599
|
coverage = 'named path/Type NAPI export registrations only; no domain file dump was built';
|
|
5204
5600
|
}
|
|
5205
5601
|
else {
|
|
5206
|
-
const apiUsage = (
|
|
5602
|
+
const apiUsage = planFeature(plan, 'shouldBuildApiUsageSurvey', query, query_utils_1.shouldBuildApiUsageSurvey)
|
|
5207
5603
|
? this.buildApiUsageSection(cg, query, projectRoot)
|
|
5208
5604
|
: { section: '', fileCount: 0 };
|
|
5209
|
-
const memberUsage = (
|
|
5605
|
+
const memberUsage = planFeature(plan, 'shouldBuildMemberSurvey', query, query_utils_1.shouldBuildMemberSurvey)
|
|
5210
5606
|
&& !((0, query_utils_1.queryAsFieldUsageSurvey)(query) && apiUsage.fileCount > 0)
|
|
5211
5607
|
? this.buildMemberSurveySection(cg, query, projectRoot)
|
|
5212
5608
|
: '';
|
|
@@ -5224,13 +5620,43 @@ class ToolHandler {
|
|
|
5224
5620
|
|| (status === 'not_surveyed'
|
|
5225
5621
|
? '- No survey ran: this query carries no symbol name to scan. Re-run naming the symbol(s), or use `homegraph_explore`.'
|
|
5226
5622
|
: '- No matching evidence was found in the current index for this focused survey.');
|
|
5227
|
-
|
|
5623
|
+
const text = this.truncateOutput([
|
|
5228
5624
|
`**HomeGraph specialized route: ${route}**`,
|
|
5229
5625
|
`Status: ${status}`,
|
|
5230
5626
|
`Coverage: ${coverage}.`,
|
|
5231
5627
|
'',
|
|
5232
5628
|
body,
|
|
5233
|
-
].join('\n'))
|
|
5629
|
+
].join('\n'));
|
|
5630
|
+
return this.exploreResult(text, { projectRoot, query, files: [], sourceBytes: 0,
|
|
5631
|
+
responseBytes: text.length, evidenceStatus: status === 'complete' ? 'complete' : 'empty',
|
|
5632
|
+
partial: status !== 'complete', coveredObligations: status === 'complete' ? [plan?.relation ?? route] : [],
|
|
5633
|
+
uncoveredObligations: status !== 'complete' ? [plan?.relation ?? route] : [] });
|
|
5634
|
+
}
|
|
5635
|
+
/** Directed file import witnesses for an explicit module-import relation. */
|
|
5636
|
+
renderModuleImports(cg, plan) {
|
|
5637
|
+
const scope = [...new Set([...plan.anchors, ...(plan.bindings ?? []).map(node => node.filePath)])]
|
|
5638
|
+
.map(value => value.replace(/\\/g, '/')).filter(Boolean);
|
|
5639
|
+
const paths = cg.getFiles().map(file => file.path).filter(file => scope.some(anchor => file === anchor || file.startsWith(anchor + '/') || file.split('/').includes(anchor)));
|
|
5640
|
+
const rows = [];
|
|
5641
|
+
let scanned = 0;
|
|
5642
|
+
for (const file of paths.slice(0, 120)) {
|
|
5643
|
+
scanned++;
|
|
5644
|
+
for (const target of cg.getFileDependencies(file)) {
|
|
5645
|
+
rows.push(`- \`${file}\` imports → \`${target}\``);
|
|
5646
|
+
if (rows.length >= 40)
|
|
5647
|
+
break;
|
|
5648
|
+
}
|
|
5649
|
+
if (rows.length >= 40)
|
|
5650
|
+
break;
|
|
5651
|
+
}
|
|
5652
|
+
const partial = scanned < paths.length || rows.length >= 40;
|
|
5653
|
+
const text = ['**Directed module imports**',
|
|
5654
|
+
rows.length ? rows.join('\n') : 'No indexed import edges found for the supplied module paths.',
|
|
5655
|
+
`Coverage: ${scanned} of ${paths.length} scoped files scanned; cycle analysis was not requested.`].join('\n\n');
|
|
5656
|
+
return this.exploreResult(text, { projectRoot: cg.getProjectRoot(), query: plan.originalQuery,
|
|
5657
|
+
files: [], sourceBytes: 0, responseBytes: text.length,
|
|
5658
|
+
evidenceStatus: !rows.length ? 'empty' : partial ? 'partial' : 'complete',
|
|
5659
|
+
partial: partial || !rows.length });
|
|
5234
5660
|
}
|
|
5235
5661
|
/** Usage sites for a bare symbol bag, including non-call textual references. */
|
|
5236
5662
|
buildBareSymbolUsageSection(cg, query, projectRoot) {
|
|
@@ -5392,8 +5818,8 @@ class ToolHandler {
|
|
|
5392
5818
|
/**
|
|
5393
5819
|
* Fast inventory-only explore — skips findRelevantContext for survey/caller/dependency queries.
|
|
5394
5820
|
*/
|
|
5395
|
-
tryFastInventoryExplore(cg, query, projectRoot) {
|
|
5396
|
-
if (!(
|
|
5821
|
+
tryFastInventoryExplore(cg, query, projectRoot, plan) {
|
|
5822
|
+
if (!planFeature(plan, 'shouldTryFastInventoryExplore', query, query_utils_1.shouldTryFastInventoryExplore))
|
|
5397
5823
|
return null;
|
|
5398
5824
|
// Multi-Type dependency asks: inventory-only early exit (avoids fat compact / busy timeout).
|
|
5399
5825
|
if ((0, query_utils_1.queryAsMultiTypeDependencySurvey)(query)) {
|
|
@@ -5797,8 +6223,8 @@ class ToolHandler {
|
|
|
5797
6223
|
* findRelevantContext. Fast enough for MCP budget; complete enough to avoid
|
|
5798
6224
|
* agent grep/read loops (token savings).
|
|
5799
6225
|
*/
|
|
5800
|
-
tryLightMechanismExplore(cg, query, projectRoot) {
|
|
5801
|
-
if (!(
|
|
6226
|
+
tryLightMechanismExplore(cg, query, projectRoot, plan) {
|
|
6227
|
+
if (!planFeature(plan, 'shouldTryLightMechanismExplore', query, query_utils_1.shouldTryLightMechanismExplore))
|
|
5802
6228
|
return null;
|
|
5803
6229
|
const STRUCTURE_KINDS = new Set(['class', 'struct', 'interface', 'component', 'method', 'function']);
|
|
5804
6230
|
const isTestPath = (p) => /(^|\/)(tests?|spec)\//i.test(p) || /\.(test|spec)\./i.test(p);
|
|
@@ -6435,7 +6861,7 @@ class ToolHandler {
|
|
|
6435
6861
|
* Compact explore for local-symbol behavior questions — skips findRelevantContext
|
|
6436
6862
|
* and caps to 1–2 defining files (avoids the ~24K related-file dump).
|
|
6437
6863
|
*/
|
|
6438
|
-
tryCompactLocalSymbolExplore(cg, query, projectRoot) {
|
|
6864
|
+
tryCompactLocalSymbolExplore(cg, query, projectRoot, plan) {
|
|
6439
6865
|
// Inventory runs *before* this on the call sites. Do not refuse compact
|
|
6440
6866
|
// merely because inventory *intent* matched — empty inventory must fall
|
|
6441
6867
|
// through here (bare callbacks like OnSurfaceChangedCB).
|
|
@@ -6449,8 +6875,13 @@ class ToolHandler {
|
|
|
6449
6875
|
// Light-mechanism owns domain howtos. Bare "how/如何" NL must NOT veto compact
|
|
6450
6876
|
// when a local/flag/lifecycle shape already owns the query (flag impact was
|
|
6451
6877
|
// falling through to a 20k Dynamic-dispatch dump).
|
|
6452
|
-
if ((
|
|
6878
|
+
if (planFeature(plan, 'shouldTryLightMechanismExplore', query, query_utils_1.shouldTryLightMechanismExplore))
|
|
6879
|
+
return null;
|
|
6880
|
+
// Explicit `…/Foo.ets` path asks need file-scoped full explore, not compact
|
|
6881
|
+
// trails seeded from path-segment homonyms (`/order/` → `order` property).
|
|
6882
|
+
if ((0, query_utils_1.shouldLimitToQueryNamedFile)(query, false, (0, query_utils_1.queryNamesMultipleExploreAnchors)(query))) {
|
|
6453
6883
|
return null;
|
|
6884
|
+
}
|
|
6454
6885
|
if ((0, query_utils_1.queryAsMechanismSurvey)(query)
|
|
6455
6886
|
&& !(0, query_utils_1.queryAsLocalSymbolDetail)(query)
|
|
6456
6887
|
&& !(0, query_utils_1.queryAsAssignedFlagImpactSurvey)(query)
|
|
@@ -9071,8 +9502,10 @@ class ToolHandler {
|
|
|
9071
9502
|
// One normalization point so the flow-builder, relevance search, and
|
|
9072
9503
|
// ranking all see the same canonical spelling (Erlang `mod:fn/arity`).
|
|
9073
9504
|
const query = normalizeQuerySpelling(rawQuery);
|
|
9505
|
+
const plan = readQueryPlan(args);
|
|
9506
|
+
const feature = (name, fallback) => planFeature(plan, name, query, fallback);
|
|
9074
9507
|
const deferKind = (0, query_utils_1.queryShouldDeferToBuiltinTools)(query);
|
|
9075
|
-
if (deferKind) {
|
|
9508
|
+
if (deferKind && plan?.source !== 'llm') {
|
|
9076
9509
|
return this.textResult((0, query_utils_1.homegraphDeferGuidance)(deferKind, query));
|
|
9077
9510
|
}
|
|
9078
9511
|
const cg = this.getHomeGraph(args.projectPath);
|
|
@@ -9080,7 +9513,7 @@ class ToolHandler {
|
|
|
9080
9513
|
// Same-bag / call-budget refuse (session view injected by execute).
|
|
9081
9514
|
const sessionPrior = (0, explore_session_state_1.viewForProject)((0, explore_session_state_1.readExploreSessionView)(args), projectRoot);
|
|
9082
9515
|
const repeat = (0, explore_repeat_guard_1.decideExploreRepeat)(sessionPrior, query);
|
|
9083
|
-
if (repeat
|
|
9516
|
+
if (!plan && this.shouldRefuseRepeatedEvidence(repeat, projectRoot)) {
|
|
9084
9517
|
const text = (0, explore_repeat_guard_1.formatExploreRepeatRefuse)(repeat, query);
|
|
9085
9518
|
return this.exploreResult(text, {
|
|
9086
9519
|
projectRoot,
|
|
@@ -9090,7 +9523,8 @@ class ToolHandler {
|
|
|
9090
9523
|
responseBytes: text.length,
|
|
9091
9524
|
});
|
|
9092
9525
|
}
|
|
9093
|
-
|
|
9526
|
+
// A planned step already tried fast paths once, before entering full explore.
|
|
9527
|
+
const compactLocal = plan ? null : this.tryFastInventoryExplore(cg, query, projectRoot)
|
|
9094
9528
|
?? this.tryLightMechanismExplore(cg, query, projectRoot)
|
|
9095
9529
|
?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot);
|
|
9096
9530
|
if (compactLocal)
|
|
@@ -9110,19 +9544,53 @@ class ToolHandler {
|
|
|
9110
9544
|
const explicitMaxFiles = typeof args.maxFiles === 'number' && !Number.isNaN(args.maxFiles);
|
|
9111
9545
|
let maxFiles = (0, utils_1.clamp)(args.maxFiles || budget.defaultMaxFiles, 1, 20);
|
|
9112
9546
|
const queryFileBasenames = (0, query_utils_1.extractFileBasenamesFromQuery)(query);
|
|
9113
|
-
const interpretationQuery = (
|
|
9114
|
-
const testOnlyInterpretation = (
|
|
9115
|
-
const crossModuleFlow = (
|
|
9547
|
+
const interpretationQuery = feature('queryAsInterpretationSurvey', query_utils_1.queryAsInterpretationSurvey);
|
|
9548
|
+
const testOnlyInterpretation = feature('queryAsTestOnlyInterpretation', query_utils_1.queryAsTestOnlyInterpretation);
|
|
9549
|
+
const crossModuleFlow = feature('queryAsCrossModuleFlowSurvey', query_utils_1.queryAsCrossModuleFlowSurvey) || plan?.intent === 'flow';
|
|
9116
9550
|
// Step 1: Find relevant context with generous parameters.
|
|
9117
9551
|
const contextOpts = interpretationQuery && queryFileBasenames.length === 1
|
|
9118
9552
|
? { searchLimit: 6, traversalDepth: 2, maxNodes: 60, minScore: 0.25 }
|
|
9119
9553
|
: { searchLimit: 8, traversalDepth: 3, maxNodes: 200, minScore: 0.2 };
|
|
9120
9554
|
const contextQuery = interpretationQuery && queryFileBasenames.length === 1
|
|
9121
9555
|
? `${queryFileBasenames[0]} ${query}`
|
|
9122
|
-
:
|
|
9123
|
-
|
|
9556
|
+
: queryFileBasenames.length === 1
|
|
9557
|
+
? `${queryFileBasenames[0]} ${query}`
|
|
9558
|
+
: query;
|
|
9559
|
+
const subgraph = await cg.findRelevantContext(contextQuery, {
|
|
9560
|
+
...contextOpts,
|
|
9561
|
+
...(plan && (plan.source === 'llm' || plan.literalTexts?.length) ? { retrievalHints: {
|
|
9562
|
+
symbols: plan.anchors.filter((anchor) => !(plan.bindings ?? []).some((node) => anchor === node.name || anchor === node.qualifiedName)),
|
|
9563
|
+
searchTerms: plan.searchTerms, literalTexts: plan.literalTexts, sourceScope: plan.sourceScope, nodeIds: (plan.bindings ?? []).map((node) => node.id),
|
|
9564
|
+
} } : {}),
|
|
9565
|
+
});
|
|
9566
|
+
// Path-first: always seed nodes from an explicit `Foo.ets` basename so a
|
|
9567
|
+
// CJK-only ask + path (or a shared prop like showSearchIcon) cannot leave
|
|
9568
|
+
// the named file out of the subgraph / digests.
|
|
9569
|
+
if (queryFileBasenames.length > 0 && plan?.sourceScope !== 'sdk') {
|
|
9570
|
+
for (const base of queryFileBasenames.slice(0, 3)) {
|
|
9571
|
+
let hits = [];
|
|
9572
|
+
try {
|
|
9573
|
+
hits = cg.searchNodes(base, { limit: 50 });
|
|
9574
|
+
}
|
|
9575
|
+
catch {
|
|
9576
|
+
continue;
|
|
9577
|
+
}
|
|
9578
|
+
for (const r of hits) {
|
|
9579
|
+
if (!(0, query_utils_1.fileMatchesQueryBasename)(r.node.filePath, [base]))
|
|
9580
|
+
continue;
|
|
9581
|
+
if (!subgraph.nodes.has(r.node.id)) {
|
|
9582
|
+
subgraph.nodes.set(r.node.id, r.node);
|
|
9583
|
+
subgraph.roots.push(r.node.id);
|
|
9584
|
+
}
|
|
9585
|
+
}
|
|
9586
|
+
}
|
|
9587
|
+
}
|
|
9588
|
+
const literalSource = this.renderLiteralSource(cg, subgraph);
|
|
9124
9589
|
if (subgraph.nodes.size === 0) {
|
|
9125
|
-
|
|
9590
|
+
const text = literalSource.text || `No relevant code found for "${query}"`;
|
|
9591
|
+
return this.exploreResult(text, { projectRoot, query, files: literalSource.files,
|
|
9592
|
+
sourceBytes: literalSource.files.reduce((sum, file) => sum + file.bytes, 0), responseBytes: text.length,
|
|
9593
|
+
locatedNodes: literalSource.nodes, partial: true, evidenceStatus: literalSource.text ? 'partial' : 'empty' });
|
|
9126
9594
|
}
|
|
9127
9595
|
// Seed import nodes for @kit.* / *Kit names (and named symbols like taskpool).
|
|
9128
9596
|
const importTerms = (0, query_utils_1.extractImportSearchTerms)(query);
|
|
@@ -9261,8 +9729,8 @@ class ToolHandler {
|
|
|
9261
9729
|
namedParts.push(m[3]);
|
|
9262
9730
|
}
|
|
9263
9731
|
const tokens = [...new Set([
|
|
9264
|
-
...namedParts,
|
|
9265
|
-
...query.split(/[\s,()[\]]+/)
|
|
9732
|
+
...(plan?.source === 'llm' ? plan.anchors : namedParts),
|
|
9733
|
+
...(plan?.source === 'llm' ? plan.anchors.join(' ') : query).split(/[\s,()[\]]+/)
|
|
9266
9734
|
.map((t) => t.replace(FILE_EXT, '').trim())
|
|
9267
9735
|
.filter((t) => t.length >= 3 && /^[A-Za-z_$][\w$]*(?:(?:::|\.)[\w$]+)*$/.test(t)),
|
|
9268
9736
|
])].slice(0, 16);
|
|
@@ -9289,7 +9757,8 @@ class ToolHandler {
|
|
|
9289
9757
|
const isQual = /[.\/]|::/.test(t);
|
|
9290
9758
|
const raw = isQual ? this.findAllSymbols(cg, t).nodes : cg.getNodesByName(t);
|
|
9291
9759
|
let cands = raw
|
|
9292
|
-
.filter((n) => SEED_KINDS.has(n.kind) && !isTestPath(n.filePath)
|
|
9760
|
+
.filter((n) => SEED_KINDS.has(n.kind) && !isTestPath(n.filePath)
|
|
9761
|
+
&& !(plan?.sourceScope === 'local' && (0, arkts_1.isOhosApiFilePath)(n.filePath)))
|
|
9293
9762
|
.sort((a, b) => {
|
|
9294
9763
|
// Prefer callables over types when both share a name, then body size.
|
|
9295
9764
|
const ac = CALLABLE.has(a.kind) ? 1 : 0;
|
|
@@ -9504,6 +9973,8 @@ class ToolHandler {
|
|
|
9504
9973
|
}
|
|
9505
9974
|
fileGroups.set(node.filePath, group);
|
|
9506
9975
|
}
|
|
9976
|
+
for (const group of fileGroups.values())
|
|
9977
|
+
group.nodes = (0, evidence_rendering_1.canonicalSourceDeclarations)(group.nodes);
|
|
9507
9978
|
if (testOnlyInterpretation) {
|
|
9508
9979
|
for (const [, group] of fileGroups) {
|
|
9509
9980
|
const fp = group.nodes[0]?.filePath ?? '';
|
|
@@ -9699,6 +10170,15 @@ class ToolHandler {
|
|
|
9699
10170
|
const sortedFiles = relevantFiles.sort((a, b) => {
|
|
9700
10171
|
const aPath = a[0].toLowerCase();
|
|
9701
10172
|
const bPath = b[0].toLowerCase();
|
|
10173
|
+
if (plan?.sourceScope === 'local') {
|
|
10174
|
+
const sdkOrder = Number((0, arkts_1.isOhosApiFilePath)(a[0])) - Number((0, arkts_1.isOhosApiFilePath)(b[0]));
|
|
10175
|
+
if (sdkOrder)
|
|
10176
|
+
return sdkOrder;
|
|
10177
|
+
}
|
|
10178
|
+
const literalOrder = Number((subgraph.literalEvidence?.hits ?? []).some((hit) => hit.filePath === b[0]))
|
|
10179
|
+
- Number((subgraph.literalEvidence?.hits ?? []).some((hit) => hit.filePath === a[0]));
|
|
10180
|
+
if (literalOrder)
|
|
10181
|
+
return literalOrder;
|
|
9702
10182
|
// Query-named file (LocationController.ets in the question) before partial
|
|
9703
10183
|
// substring matches (control.ets matching "Controller" inside LocationController).
|
|
9704
10184
|
const aExactBase = (0, query_utils_1.fileMatchesQueryBasename)(a[0], queryFileBasenames) ? 1 : 0;
|
|
@@ -9758,6 +10238,8 @@ class ToolHandler {
|
|
|
9758
10238
|
'',
|
|
9759
10239
|
];
|
|
9760
10240
|
const summaryLineIdx = 2;
|
|
10241
|
+
if (literalSource.text)
|
|
10242
|
+
lines.push(literalSource.text);
|
|
9761
10243
|
if (testOnlyInterpretation) {
|
|
9762
10244
|
lines.push('> **Test-file scope only** — answer from the named `.test.ets` file below; ' +
|
|
9763
10245
|
'production handlers are out of scope unless explicitly referenced in the test.');
|
|
@@ -9782,30 +10264,30 @@ class ToolHandler {
|
|
|
9782
10264
|
: { section: '', symbolCount: 0 };
|
|
9783
10265
|
if (kitUsageResult.section)
|
|
9784
10266
|
lines.push(kitUsageResult.section);
|
|
9785
|
-
const domainFileResult = (
|
|
10267
|
+
const domainFileResult = feature('shouldBuildDomainFileSurvey', query_utils_1.shouldBuildDomainFileSurvey)
|
|
9786
10268
|
? this.buildDomainFileSurveySection(cg, query)
|
|
9787
10269
|
: { section: '', fileCount: 0 };
|
|
9788
10270
|
if (domainFileResult.section)
|
|
9789
10271
|
lines.push(domainFileResult.section);
|
|
9790
|
-
const apiUsageResult = (
|
|
9791
|
-
&& !(
|
|
10272
|
+
const apiUsageResult = feature('shouldBuildApiUsageSurvey', query_utils_1.shouldBuildApiUsageSurvey)
|
|
10273
|
+
&& !feature('shouldBuildKitModuleUsageSurvey', query_utils_1.shouldBuildKitModuleUsageSurvey)
|
|
9792
10274
|
? this.buildApiUsageSection(cg, query, projectRoot)
|
|
9793
10275
|
: { section: '', fileCount: 0 };
|
|
9794
10276
|
if (apiUsageResult.section)
|
|
9795
10277
|
lines.push(apiUsageResult.section);
|
|
9796
|
-
const dataSourceResult = (
|
|
10278
|
+
const dataSourceResult = feature('queryAsDataSourceSurvey', query_utils_1.queryAsDataSourceSurvey)
|
|
9797
10279
|
? this.buildDataSourceSection(cg, query)
|
|
9798
10280
|
: { section: '', edgeCount: 0, sdkImportCount: 0, strongCount: 0 };
|
|
9799
10281
|
if (dataSourceResult.section)
|
|
9800
10282
|
lines.push(dataSourceResult.section);
|
|
9801
|
-
const eventDispatchResult = (
|
|
10283
|
+
const eventDispatchResult = feature('queryAsEventDispatchSurvey', query_utils_1.queryAsEventDispatchSurvey)
|
|
9802
10284
|
? this.buildEventDispatchSection(cg, query, projectRoot)
|
|
9803
10285
|
: { section: '', hitCount: 0, eventCount: 0, handlerCount: 0, memberCount: 0, complete: false };
|
|
9804
10286
|
if (eventDispatchResult.section)
|
|
9805
10287
|
lines.push(eventDispatchResult.section);
|
|
9806
10288
|
const importInventoryFilter = (0, query_utils_1.hasImportInventoryFilter)(query);
|
|
9807
10289
|
const multiAnchor = (0, query_utils_1.queryNamesMultipleExploreAnchors)(query) || crossModuleFlow;
|
|
9808
|
-
const mechanismSurvey = (
|
|
10290
|
+
const mechanismSurvey = feature('queryAsMechanismSurvey', query_utils_1.queryAsMechanismSurvey);
|
|
9809
10291
|
// Flow path — computed before omit-source so graph connectivity drives the decision,
|
|
9810
10292
|
// not question-text keyword matching. Mechanism/cross-module surveys augment the
|
|
9811
10293
|
// query with seeded entry symbol names so buildFlowFromNamedSymbols can connect them.
|
|
@@ -10036,6 +10518,17 @@ class ToolHandler {
|
|
|
10036
10518
|
const priorCalls = (0, explore_session_state_1.viewForProject)((0, explore_session_state_1.readExploreSessionView)(args), projectRoot);
|
|
10037
10519
|
const dedupEnabled = (0, explore_dedup_1.exploreDedupEnabled)() && (priorCalls?.calls.length ?? 0) > 0;
|
|
10038
10520
|
const emittedByFile = new Map();
|
|
10521
|
+
// Session emissions may include prior-call coverage or skeleton envelopes.
|
|
10522
|
+
// Dependency receipts instead require *fresh*, actually printed source and
|
|
10523
|
+
// a whole surviving file section (never the tail cut by the hard ceiling).
|
|
10524
|
+
const freshRangesByFile = new Map();
|
|
10525
|
+
const sourceEndByFile = new Map();
|
|
10526
|
+
const noteFreshSource = (fp, ranges) => {
|
|
10527
|
+
if (plan?.source !== 'llm' || !ranges.length)
|
|
10528
|
+
return;
|
|
10529
|
+
freshRangesByFile.set(fp, [...(freshRangesByFile.get(fp) ?? []), ...ranges]);
|
|
10530
|
+
sourceEndByFile.set(fp, lines.join('\n').length);
|
|
10531
|
+
};
|
|
10039
10532
|
const noteEmitted = (fp, ranges, bytes, fingerprint) => {
|
|
10040
10533
|
const existing = emittedByFile.get(fp);
|
|
10041
10534
|
if (existing) {
|
|
@@ -10207,6 +10700,7 @@ class ToolHandler {
|
|
|
10207
10700
|
}
|
|
10208
10701
|
if (body.length > 0) {
|
|
10209
10702
|
lines.push('```' + lang, body, '```', '');
|
|
10703
|
+
noteFreshSource(filePath, ranges);
|
|
10210
10704
|
totalChars += body.length + lang.length + 11;
|
|
10211
10705
|
noteEmitted(filePath, [...ranges, ...opts.covered], body.length, fingerprint);
|
|
10212
10706
|
renderedFilePaths.push(filePath);
|
|
@@ -10293,6 +10787,7 @@ class ToolHandler {
|
|
|
10293
10787
|
// signature line (capped, with a "+N more" tail so the structure map of a
|
|
10294
10788
|
// god-file doesn't itself bloat the budget).
|
|
10295
10789
|
const skel = [];
|
|
10790
|
+
const freshSkelRanges = [];
|
|
10296
10791
|
let coveredUntil = 0; // skip symbols already inside an emitted body
|
|
10297
10792
|
let sigCount = 0, sigDropped = 0;
|
|
10298
10793
|
const SIG_MAX = Math.max(12, budget.maxSymbolsInFileHeader * 2);
|
|
@@ -10303,6 +10798,7 @@ class ToolHandler {
|
|
|
10303
10798
|
const end = n.endLine;
|
|
10304
10799
|
const body = fileLines.slice(n.startLine - 1, end).join('\n');
|
|
10305
10800
|
skel.push(exploreLineNumbersEnabled() ? numberSourceLines(body, n.startLine) : body);
|
|
10801
|
+
freshSkelRanges.push({ start: n.startLine, end });
|
|
10306
10802
|
coveredUntil = end;
|
|
10307
10803
|
}
|
|
10308
10804
|
else {
|
|
@@ -10325,6 +10821,7 @@ class ToolHandler {
|
|
|
10325
10821
|
if (sig) {
|
|
10326
10822
|
skel.push(exploreLineNumbersEnabled() ? `${lineNo}\t${sig}` : sig);
|
|
10327
10823
|
sigCount++;
|
|
10824
|
+
freshSkelRanges.push({ start: lineNo, end: lineNo });
|
|
10328
10825
|
}
|
|
10329
10826
|
}
|
|
10330
10827
|
}
|
|
@@ -10362,6 +10859,7 @@ class ToolHandler {
|
|
|
10362
10859
|
return n ? { start: n.startLine, end: n.endLine || n.startLine } : null;
|
|
10363
10860
|
}).filter((r) => !!r);
|
|
10364
10861
|
lines.push(skelHeader, '', '```' + lang, skelBody, '```', '');
|
|
10862
|
+
noteFreshSource(filePath, freshSkelRanges);
|
|
10365
10863
|
totalChars += skelBody.length + 120;
|
|
10366
10864
|
noteEmitted(filePath, bodyRanges.length > 0 ? bodyRanges : [wholeRange], skelBody.length, fingerprint);
|
|
10367
10865
|
renderedFilePaths.push(filePath);
|
|
@@ -10394,7 +10892,15 @@ class ToolHandler {
|
|
|
10394
10892
|
? Math.min(Math.max(0, budget.maxOutputChars - totalChars - 200), Math.round(budget.maxCharsPerFile * 1.5))
|
|
10395
10893
|
: budget.maxCharsPerFile * 3;
|
|
10396
10894
|
if (fileLines.length <= WHOLE_FILE_MAX_LINES && fileContent.length <= WHOLE_FILE_MAX_CHARS) {
|
|
10397
|
-
|
|
10895
|
+
let sourceStart = 0;
|
|
10896
|
+
if (fileLines[0]?.trim().startsWith('/*')) {
|
|
10897
|
+
const endComment = fileLines.findIndex((line) => line.includes('*/'));
|
|
10898
|
+
if (endComment >= 0 && endComment < 40)
|
|
10899
|
+
sourceStart = endComment + 1;
|
|
10900
|
+
}
|
|
10901
|
+
while (sourceStart < fileLines.length - 1 && !fileLines[sourceStart]?.trim())
|
|
10902
|
+
sourceStart++;
|
|
10903
|
+
const wholeRange = { start: sourceStart + 1, end: Math.max(1, fileLines.length) };
|
|
10398
10904
|
const dd = (0, explore_dedup_1.dedupeRange)(wholeRange, served);
|
|
10399
10905
|
const uniqSymbols = [...new Set(group.nodes
|
|
10400
10906
|
.filter(n => n.kind !== 'import' && n.kind !== 'export')
|
|
@@ -10855,6 +11361,7 @@ class ToolHandler {
|
|
|
10855
11361
|
const output = flow.text + lines.join('\n');
|
|
10856
11362
|
const hardCeiling = Math.min(Math.round(budget.maxOutputChars * 1.5), 25000);
|
|
10857
11363
|
let finalText;
|
|
11364
|
+
let sourceCutoff = output.length;
|
|
10858
11365
|
if (output.length > hardCeiling) {
|
|
10859
11366
|
// Prefer dropping trailing notes ("Not shown above", completeness, budget)
|
|
10860
11367
|
// over dropping a whole file's source — notes are recoverable, source isn't.
|
|
@@ -10869,8 +11376,10 @@ class ToolHandler {
|
|
|
10869
11376
|
if (trimmed.length <= hardCeiling)
|
|
10870
11377
|
break;
|
|
10871
11378
|
const at = trimmed.lastIndexOf(marker);
|
|
10872
|
-
if (at > hardCeiling * 0.4)
|
|
11379
|
+
if (at > hardCeiling * 0.4) {
|
|
10873
11380
|
trimmed = trimmed.slice(0, at).replace(/\n+$/, '');
|
|
11381
|
+
sourceCutoff = Math.min(sourceCutoff, trimmed.length);
|
|
11382
|
+
}
|
|
10874
11383
|
}
|
|
10875
11384
|
if (trimmed.length > hardCeiling) {
|
|
10876
11385
|
// Cut at a FILE-SECTION boundary so we drop whole trailing file-sections
|
|
@@ -10899,6 +11408,7 @@ class ToolHandler {
|
|
|
10899
11408
|
boundary = lastSection > hardCeiling * 0.5 ? lastSection : cut.lastIndexOf('\n');
|
|
10900
11409
|
}
|
|
10901
11410
|
const safe = boundary > 0 ? cut.slice(0, boundary) : cut;
|
|
11411
|
+
sourceCutoff = Math.min(sourceCutoff, safe.length);
|
|
10902
11412
|
finalText = safe + '\n\n... (output truncated to budget; the source above is complete and verbatim — treat it as already Read. For uncovered files/symbols, run another homegraph_explore with their exact names — not grep/read/node for symbols already shown.)';
|
|
10903
11413
|
}
|
|
10904
11414
|
else {
|
|
@@ -10949,14 +11459,85 @@ class ToolHandler {
|
|
|
10949
11459
|
});
|
|
10950
11460
|
sourceBytes += emitted.bytes;
|
|
10951
11461
|
}
|
|
11462
|
+
// Do not promote unseen subgraph hits or a large parent's unshown header.
|
|
11463
|
+
// The declaration's starting line must be in a surviving, fresh source span.
|
|
11464
|
+
const rootIds = new Set(subgraph.roots);
|
|
11465
|
+
for (const file of literalSource.files) {
|
|
11466
|
+
if (!finalText.includes(literalSource.text))
|
|
11467
|
+
break;
|
|
11468
|
+
emittedFiles.push(file);
|
|
11469
|
+
sourceBytes += file.bytes;
|
|
11470
|
+
}
|
|
11471
|
+
const locatedNodes = plan?.source === 'llm' ? [...(finalText.includes(literalSource.text) ? literalSource.nodes : []), ...emittedFiles.flatMap((file) => staleRendered.includes(file.path) || flow.text.length + (sourceEndByFile.get(file.path) ?? Infinity) > sourceCutoff
|
|
11472
|
+
? [] : (fileGroups.get(file.path)?.nodes ?? []).filter((node) => !['file', 'import', 'export', 'parameter'].includes(node.kind)
|
|
11473
|
+
&& (freshRangesByFile.get(file.path) ?? []).some((range) => node.startLine >= range.start && node.startLine <= range.end)))]
|
|
11474
|
+
.sort((a, b) => Number(rootIds.has(b.id)) - Number(rootIds.has(a.id)))
|
|
11475
|
+
.filter((node, i, nodes) => nodes.findIndex((other) => other.id === node.id) === i)
|
|
11476
|
+
.slice(0, 32).map((node) => ({ id: node.id, name: node.name, qualifiedName: node.qualifiedName,
|
|
11477
|
+
filePath: node.filePath, startLine: node.startLine })) : undefined;
|
|
10952
11478
|
return this.exploreResult(finalText, {
|
|
10953
11479
|
projectRoot,
|
|
10954
11480
|
query,
|
|
10955
11481
|
files: emittedFiles,
|
|
10956
11482
|
sourceBytes,
|
|
11483
|
+
evidenceStatus: sourceBytes > 0 ? (anyFileTrimmed || subgraph.confidence === 'low' || sourceCutoff < output.length
|
|
11484
|
+
|| (plan?.source === 'llm' && !(locatedNodes?.length)) ? 'partial' : 'complete')
|
|
11485
|
+
: filesIncluded > 0 && /HarmonyOS SDK API/.test(finalText) ? 'sdk-only' : 'empty',
|
|
10957
11486
|
responseBytes: finalText.length,
|
|
11487
|
+
...(locatedNodes ? { locatedNodes } : {}),
|
|
10958
11488
|
});
|
|
10959
11489
|
}
|
|
11490
|
+
/** Render literal witnesses before graph-heavy sections, including unindexed UI. */
|
|
11491
|
+
renderLiteralSource(cg, subgraph) {
|
|
11492
|
+
const sections = [];
|
|
11493
|
+
const files = [];
|
|
11494
|
+
const nodes = [];
|
|
11495
|
+
const seen = new Set();
|
|
11496
|
+
let chars = 0;
|
|
11497
|
+
for (const hit of subgraph.literalEvidence?.hits ?? []) {
|
|
11498
|
+
if (files.length >= 2 || seen.has(hit.filePath))
|
|
11499
|
+
continue;
|
|
11500
|
+
const absolute = (0, utils_1.validatePathWithinRoot)(cg.getProjectRoot(), hit.filePath);
|
|
11501
|
+
if (!absolute)
|
|
11502
|
+
continue;
|
|
11503
|
+
let source;
|
|
11504
|
+
try {
|
|
11505
|
+
source = (0, fs_1.readFileSync)(absolute, 'utf8');
|
|
11506
|
+
}
|
|
11507
|
+
catch {
|
|
11508
|
+
continue;
|
|
11509
|
+
}
|
|
11510
|
+
const lines = source.split('\n');
|
|
11511
|
+
// Never serve an earlier witness against a changed file.
|
|
11512
|
+
if (lines.slice(hit.startLine - 1, hit.endLine).join('\n') !== hit.text)
|
|
11513
|
+
continue;
|
|
11514
|
+
const containing = this.isFileStaleOnDisk(cg, hit.filePath, source) ? undefined
|
|
11515
|
+
: (0, evidence_rendering_1.canonicalSourceDeclarations)(cg.getNodesInFile(hit.filePath))
|
|
11516
|
+
.filter(node => !['file', 'import', 'export', 'parameter'].includes(node.kind)
|
|
11517
|
+
&& !node.name.startsWith('%') && node.name !== 'constructor'
|
|
11518
|
+
&& node.startLine <= hit.line && node.endLine >= hit.line)
|
|
11519
|
+
.sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
|
|
11520
|
+
const ranges = [{ start: hit.startLine, end: hit.endLine }];
|
|
11521
|
+
if (containing && containing.startLine < hit.startLine) {
|
|
11522
|
+
ranges.unshift({ start: containing.startLine, end: Math.min(containing.startLine + 2, hit.startLine - 1) });
|
|
11523
|
+
}
|
|
11524
|
+
const body = ranges.map(range => lines.slice(range.start - 1, range.end)
|
|
11525
|
+
.map((line, index) => `${range.start + index}\t${line}`).join('\n')).join('\n... (gap) ...\n');
|
|
11526
|
+
const resource = hit.resource
|
|
11527
|
+
? `\nResource: \`${hit.resource.filePath}:${hit.resource.line}\` — ${JSON.stringify(hit.resource.value)} → \`${hit.resource.key}\`.` : '';
|
|
11528
|
+
const section = `**Literal source witness: \`${hit.filePath}:${hit.line}\`**${resource}\n\n\`\`\`\n${body}\n\`\`\``;
|
|
11529
|
+
if (chars + section.length > 3000)
|
|
11530
|
+
continue;
|
|
11531
|
+
sections.push(section);
|
|
11532
|
+
chars += section.length;
|
|
11533
|
+
seen.add(hit.filePath);
|
|
11534
|
+
files.push({ path: hit.filePath, ranges, bytes: body.length, fingerprint: (0, explore_dedup_1.fileFingerprint)(source) });
|
|
11535
|
+
if (containing)
|
|
11536
|
+
nodes.push({ id: containing.id, name: containing.name,
|
|
11537
|
+
qualifiedName: containing.qualifiedName, filePath: containing.filePath, startLine: containing.startLine });
|
|
11538
|
+
}
|
|
11539
|
+
return { text: sections.join('\n\n'), files, nodes };
|
|
11540
|
+
}
|
|
10960
11541
|
/**
|
|
10961
11542
|
* An explore response plus the record of what it emitted (CG-17). The record
|
|
10962
11543
|
* rides the result only as far as {@link execute}, which files it into the
|
|
@@ -10967,7 +11548,8 @@ class ToolHandler {
|
|
|
10967
11548
|
const result = this.textResult(text);
|
|
10968
11549
|
result[explore_session_state_1.EXPLORE_EMISSION_KEY] = {
|
|
10969
11550
|
...emission,
|
|
10970
|
-
|
|
11551
|
+
evidenceStatus: emission.evidenceStatus ?? (emission.sourceBytes > 0 ? (meta.partial ? 'partial' : 'complete') : 'partial'),
|
|
11552
|
+
partial: emission.partial ?? (emission.evidenceStatus && emission.evidenceStatus !== 'complete' ? true : meta.partial),
|
|
10971
11553
|
nextAnchor: emission.nextAnchor ?? meta.nextAnchor,
|
|
10972
11554
|
};
|
|
10973
11555
|
return result;
|
|
@@ -10993,7 +11575,8 @@ class ToolHandler {
|
|
|
10993
11575
|
files: [],
|
|
10994
11576
|
sourceBytes: 0,
|
|
10995
11577
|
responseBytes: text.length,
|
|
10996
|
-
partial:
|
|
11578
|
+
partial: true,
|
|
11579
|
+
evidenceStatus: /HarmonyOS SDK API|ohos-sdk:/.test(text) ? 'sdk-only' : /No relevant code|No matching evidence|No survey ran/.test(text) ? 'empty' : 'partial',
|
|
10997
11580
|
nextAnchor: meta.nextAnchor,
|
|
10998
11581
|
};
|
|
10999
11582
|
return result;
|
|
@@ -11028,7 +11611,7 @@ class ToolHandler {
|
|
|
11028
11611
|
const symbol = this.validateString(args.symbol, 'symbol');
|
|
11029
11612
|
if (typeof symbol !== 'string')
|
|
11030
11613
|
return symbol;
|
|
11031
|
-
let matches = this.findSymbolMatches(cg, symbol);
|
|
11614
|
+
let matches = (0, evidence_rendering_1.canonicalSourceDeclarations)(this.findSymbolMatches(cg, symbol));
|
|
11032
11615
|
if (matches.length === 0) {
|
|
11033
11616
|
return this.textResult(`Symbol "${symbol}" not found in the codebase`);
|
|
11034
11617
|
}
|
|
@@ -12404,7 +12987,7 @@ class ToolHandler {
|
|
|
12404
12987
|
return {
|
|
12405
12988
|
// Single choke point for every tool's text, so no section builder can ship
|
|
12406
12989
|
// a stop-searching directive on an output that declares itself partial.
|
|
12407
|
-
content: [{ type: 'text', text: reconcilePartialAnswerNow(text) }],
|
|
12990
|
+
content: [{ type: 'text', text: (0, evidence_rendering_1.neutralRetrievalGuidance)(reconcilePartialAnswerNow(text)) }],
|
|
12408
12991
|
};
|
|
12409
12992
|
}
|
|
12410
12993
|
/**
|