homegraph 1.5.4 → 1.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +4 -1
- package/dist/arkui/migrate-semantics.d.ts +8 -0
- package/dist/arkui/migrate-semantics.d.ts.map +1 -1
- package/dist/arkui/migrate-semantics.js +24 -1
- package/dist/arkui/migrate-semantics.js.map +1 -1
- package/dist/arkui/migrate-snapshot.d.ts.map +1 -1
- package/dist/arkui/migrate-snapshot.js +11 -4
- package/dist/arkui/migrate-snapshot.js.map +1 -1
- package/dist/mcp/engine.d.ts.map +1 -1
- package/dist/mcp/engine.js +2 -14
- package/dist/mcp/engine.js.map +1 -1
- package/dist/mcp/explore-repeat-guard.d.ts.map +1 -1
- package/dist/mcp/explore-repeat-guard.js +37 -0
- package/dist/mcp/explore-repeat-guard.js.map +1 -1
- package/dist/mcp/memory-budget.d.ts +9 -22
- package/dist/mcp/memory-budget.d.ts.map +1 -1
- package/dist/mcp/memory-budget.js +9 -59
- package/dist/mcp/memory-budget.js.map +1 -1
- package/dist/mcp/server-instructions.d.ts +2 -2
- package/dist/mcp/server-instructions.d.ts.map +1 -1
- package/dist/mcp/server-instructions.js +4 -0
- package/dist/mcp/server-instructions.js.map +1 -1
- package/dist/mcp/tools.d.ts +44 -3
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +1314 -78
- package/dist/mcp/tools.js.map +1 -1
- package/dist/search/query-utils.d.ts +10 -0
- package/dist/search/query-utils.d.ts.map +1 -1
- package/dist/search/query-utils.js +58 -5
- package/dist/search/query-utils.js.map +1 -1
- package/dist/upgrade/index.d.ts +23 -12
- package/dist/upgrade/index.d.ts.map +1 -1
- package/dist/upgrade/index.js +42 -48
- package/dist/upgrade/index.js.map +1 -1
- package/dist/upgrade/update-check.d.ts +4 -4
- package/dist/upgrade/update-check.d.ts.map +1 -1
- package/dist/upgrade/update-check.js +6 -5
- package/dist/upgrade/update-check.js.map +1 -1
- package/package.json +1 -1
package/dist/mcp/tools.js
CHANGED
|
@@ -16,6 +16,10 @@ exports.formatStaleBanner = formatStaleBanner;
|
|
|
16
16
|
exports.formatStaleFooter = formatStaleFooter;
|
|
17
17
|
exports.formatDegradedBanner = formatDegradedBanner;
|
|
18
18
|
exports.getStaticTools = getStaticTools;
|
|
19
|
+
exports.extractBareUsageSymbols = extractBareUsageSymbols;
|
|
20
|
+
exports.queryAsBareSymbolInventory = queryAsBareSymbolInventory;
|
|
21
|
+
exports.hasPositiveAnswerNowDirective = hasPositiveAnswerNowDirective;
|
|
22
|
+
exports.reconcilePartialAnswerNow = reconcilePartialAnswerNow;
|
|
19
23
|
const query_pool_1 = require("./query-pool");
|
|
20
24
|
const memory_budget_1 = require("./memory-budget");
|
|
21
25
|
const directory_1 = require("../directory");
|
|
@@ -35,6 +39,7 @@ function __setLoadHomeGraphForTests(cls) {
|
|
|
35
39
|
loadHomeGraphForTests = cls;
|
|
36
40
|
}
|
|
37
41
|
const worktree_1 = require("../sync/worktree");
|
|
42
|
+
const update_check_1 = require("../upgrade/update-check");
|
|
38
43
|
const graph_sources_1 = require("../graph-sources");
|
|
39
44
|
const query_utils_1 = require("../search/query-utils");
|
|
40
45
|
const fs_1 = require("fs");
|
|
@@ -874,6 +879,60 @@ exports.tools = [
|
|
|
874
879
|
},
|
|
875
880
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
876
881
|
},
|
|
882
|
+
{
|
|
883
|
+
name: 'homegraph_usages',
|
|
884
|
+
description: 'Focused in-repo usage inventory for one named API, `.member`, ALL_CAPS constant, field, or mutex. ' +
|
|
885
|
+
'Required: `query`. Returns usage files/lines only; it does not build a general flow or source dump. ' +
|
|
886
|
+
'Use directly for a narrow where-used question; `homegraph_explore` auto-routes equivalent high-confidence queries here.',
|
|
887
|
+
inputSchema: {
|
|
888
|
+
type: 'object',
|
|
889
|
+
properties: {
|
|
890
|
+
query: {
|
|
891
|
+
type: 'string',
|
|
892
|
+
description: 'Required. Exact API/member/constant/field name, with or without where-used wording.',
|
|
893
|
+
},
|
|
894
|
+
projectPath: projectPathProperty,
|
|
895
|
+
},
|
|
896
|
+
required: ['query'],
|
|
897
|
+
},
|
|
898
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
name: 'homegraph_modules',
|
|
902
|
+
description: 'Focused dependency/cycle inventory for named path modules or `*common` / `*service` / `*component` / `*constants` modules. ' +
|
|
903
|
+
'Required: `query`. It does not build a general code flow or scan unrelated survey families. ' +
|
|
904
|
+
'`homegraph_explore` auto-routes equivalent high-confidence dependency questions here.',
|
|
905
|
+
inputSchema: {
|
|
906
|
+
type: 'object',
|
|
907
|
+
properties: {
|
|
908
|
+
query: {
|
|
909
|
+
type: 'string',
|
|
910
|
+
description: 'Required. Dependency/cycle question containing the exact module names or paths.',
|
|
911
|
+
},
|
|
912
|
+
projectPath: projectPathProperty,
|
|
913
|
+
},
|
|
914
|
+
required: ['query'],
|
|
915
|
+
},
|
|
916
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
name: 'homegraph_native',
|
|
920
|
+
description: 'Focused NAPI/native export inventory for one named path or Type. Required: `query`. ' +
|
|
921
|
+
'Returns indexed export descriptors/registration sites without a general domain file dump. ' +
|
|
922
|
+
'`homegraph_explore` auto-routes equivalent high-confidence NAPI/export questions here.',
|
|
923
|
+
inputSchema: {
|
|
924
|
+
type: 'object',
|
|
925
|
+
properties: {
|
|
926
|
+
query: {
|
|
927
|
+
type: 'string',
|
|
928
|
+
description: 'Required. NAPI/native export question containing the exact path or Type name.',
|
|
929
|
+
},
|
|
930
|
+
projectPath: projectPathProperty,
|
|
931
|
+
},
|
|
932
|
+
required: ['query'],
|
|
933
|
+
},
|
|
934
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
935
|
+
},
|
|
877
936
|
{
|
|
878
937
|
name: 'homegraph_status',
|
|
879
938
|
description: 'Index health check (files / nodes / edges). No required args when a default project is loaded. Skip unless debugging.',
|
|
@@ -1069,12 +1128,150 @@ function getStaticTools() {
|
|
|
1069
1128
|
const allow = new Set(raw.split(',').map(s => s.trim().replace(/^homegraph_/, '').replace(/^homegraph_/, '')).filter(Boolean));
|
|
1070
1129
|
return allow.size ? exports.tools.filter(t => allow.has(t.name.replace(/^homegraph_/, ''))) : exports.tools;
|
|
1071
1130
|
}
|
|
1131
|
+
/** Prose that reads like an identifier but never names a symbol worth scanning. */
|
|
1132
|
+
const BARE_USAGE_STOPWORDS = new Set([
|
|
1133
|
+
'class', 'method', 'function', 'interface', 'module', 'import', 'export', 'value',
|
|
1134
|
+
'file', 'files', 'code', 'usage', 'usages', 'where', 'which', 'what', 'when',
|
|
1135
|
+
'call', 'calls', 'called', 'caller', 'callers', 'used', 'uses', 'using',
|
|
1136
|
+
'true', 'false', 'null', 'undefined', 'this', 'return', 'const', 'type',
|
|
1137
|
+
'string', 'number', 'boolean', 'object', 'array', 'void', 'async', 'await',
|
|
1138
|
+
]);
|
|
1139
|
+
/** Identifier-shaped tokens from a bare symbol bag. */
|
|
1140
|
+
function extractBareUsageSymbols(query) {
|
|
1141
|
+
const out = [];
|
|
1142
|
+
const seen = new Set();
|
|
1143
|
+
const re = /\b([A-Z][A-Za-z0-9]{3,}(?:\.[A-Za-z_]\w*)?|[A-Z][A-Z0-9_]{3,}|[a-z]+[A-Z]\w{2,}(?:\.[A-Za-z_]\w*)?)\b/g;
|
|
1144
|
+
for (const m of query.matchAll(re)) {
|
|
1145
|
+
const tok = m[1];
|
|
1146
|
+
const key = tok.toLowerCase();
|
|
1147
|
+
if (seen.has(key) || BARE_USAGE_STOPWORDS.has(key))
|
|
1148
|
+
continue;
|
|
1149
|
+
seen.add(key);
|
|
1150
|
+
out.push(tok);
|
|
1151
|
+
}
|
|
1152
|
+
return out;
|
|
1153
|
+
}
|
|
1154
|
+
/** Wording that asks how something works — never a plain usage inventory. */
|
|
1155
|
+
const FLOW_INTENT_RE = /如何|怎[么样]|机制|流程|原理|为什么|为何|架构|生命周期|时序|调用链|关系|\b(how|why|flow|mechanism|lifecycle|architecture|sequence|pipeline|between|implement|implementation)\b/i;
|
|
1156
|
+
/** Action-shaped identifiers are probably call-flow anchors, not inventories. */
|
|
1157
|
+
const ACTION_SEGMENTS = new Set([
|
|
1158
|
+
'trigger', 'callback', 'handler', 'listener', 'observer', 'dispatch', 'emit',
|
|
1159
|
+
'notify', 'render', 'draw', 'init', 'start', 'stop', 'update', 'refresh',
|
|
1160
|
+
'subscribe', 'publish', 'invoke', 'execute', 'run', 'process', 'handle',
|
|
1161
|
+
'post', 'send', 'receive', 'load', 'save', 'open', 'close',
|
|
1162
|
+
]);
|
|
1163
|
+
function identifierSegments(token) {
|
|
1164
|
+
return token
|
|
1165
|
+
.replace(/[._]/g, ' ')
|
|
1166
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
1167
|
+
.toLowerCase()
|
|
1168
|
+
.split(/\s+/)
|
|
1169
|
+
.filter(Boolean);
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* True only for a short bag of ALL_CAPS constants. Type/method bags remain on
|
|
1173
|
+
* explore-flow because routing those to usages would discard call paths.
|
|
1174
|
+
*/
|
|
1175
|
+
function queryAsBareSymbolInventory(query) {
|
|
1176
|
+
if (process.env.HOMEGRAPH_EXPLORE_SHAPE_ROUTING === '0')
|
|
1177
|
+
return false;
|
|
1178
|
+
const tokens = String(query || '').split(/[\s,、,]+/).filter(Boolean);
|
|
1179
|
+
if (tokens.length === 0 || tokens.length > 6)
|
|
1180
|
+
return false;
|
|
1181
|
+
if (FLOW_INTENT_RE.test(query))
|
|
1182
|
+
return false;
|
|
1183
|
+
if (tokens.some((t) => identifierSegments(t).some((s) => ACTION_SEGMENTS.has(s))))
|
|
1184
|
+
return false;
|
|
1185
|
+
return tokens.every((t) => /^[A-Z][A-Z0-9_]{3,}$/.test(t));
|
|
1186
|
+
}
|
|
1072
1187
|
/**
|
|
1073
1188
|
* Tool handler that executes tools against a HomeGraph instance
|
|
1074
1189
|
*
|
|
1075
1190
|
* Supports cross-project queries via the projectPath parameter.
|
|
1076
1191
|
* Other projects are opened on-demand and cached for performance.
|
|
1077
1192
|
*/
|
|
1193
|
+
const ANSWER_NOW_TOKEN = /\bANSWER NOW\b/gi;
|
|
1194
|
+
/** Whether one ANSWER NOW occurrence is postponed/negated rather than a stop directive. */
|
|
1195
|
+
function isNegatedAnswerNowAt(text, index) {
|
|
1196
|
+
const prefix = text
|
|
1197
|
+
.slice(Math.max(0, index - 96), index)
|
|
1198
|
+
.replace(/[*_`]/g, '')
|
|
1199
|
+
.replace(/\s+/g, ' ');
|
|
1200
|
+
return /(?:\b(?:do|should|must|can)\s+not|\bcannot|\bnever|\bbefore|\buntil|\bnot)\s+(?:\S+\s+){0,3}$/i
|
|
1201
|
+
.test(prefix);
|
|
1202
|
+
}
|
|
1203
|
+
/** True only for a positive instruction to stop and answer, not "do not/before ANSWER NOW". */
|
|
1204
|
+
function hasPositiveAnswerNowDirective(text) {
|
|
1205
|
+
const answerNow = new RegExp(ANSWER_NOW_TOKEN.source, ANSWER_NOW_TOKEN.flags);
|
|
1206
|
+
for (let match = answerNow.exec(text); match; match = answerNow.exec(text)) {
|
|
1207
|
+
if (!isNegatedAnswerNowAt(text, match.index))
|
|
1208
|
+
return true;
|
|
1209
|
+
}
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1212
|
+
function stripPositiveAnswerNowFromLine(line) {
|
|
1213
|
+
let changed = false;
|
|
1214
|
+
let out = line.replace(/\*\*[^*\n]*\bANSWER NOW\b[^*\n]*\*\*/gi, (span, offset, source) => {
|
|
1215
|
+
const tokenOffset = span.search(/\bANSWER NOW\b/i);
|
|
1216
|
+
if (tokenOffset < 0 || isNegatedAnswerNowAt(source, offset + tokenOffset))
|
|
1217
|
+
return span;
|
|
1218
|
+
changed = true;
|
|
1219
|
+
return '';
|
|
1220
|
+
});
|
|
1221
|
+
out = out.replace(ANSWER_NOW_TOKEN, (token, offset, source) => {
|
|
1222
|
+
if (isNegatedAnswerNowAt(source, offset))
|
|
1223
|
+
return token;
|
|
1224
|
+
changed = true;
|
|
1225
|
+
return '';
|
|
1226
|
+
});
|
|
1227
|
+
if (!changed)
|
|
1228
|
+
return { text: line, changed: false };
|
|
1229
|
+
// A positive close often carries a matching search prohibition. Once the
|
|
1230
|
+
// whole response is Partial, that prohibition is contradictory too.
|
|
1231
|
+
out = out
|
|
1232
|
+
.replace(/\s*(?:[,;—-]\s*)?(?:and\s+)?do\s+\*{0,2}not\*{0,2}\s+(?:(?:re-)?(?:Read|Grep|Glob)|`?homegraph_(?:search|explore|node|callers|callees)`?)[^.;\n]*/gi, '')
|
|
1233
|
+
.replace(/\s*(?:[,;—-]\s*)?no\s+(?:glob|grep|read|search|explore)(?:\s*\/\s*(?:glob|grep|read|search|explore))*\s+needed\b[^.;\n]*/gi, '')
|
|
1234
|
+
.replace(/\(\s*[,;—-]*\s*\)/g, '')
|
|
1235
|
+
.replace(/\s{2,}/g, ' ')
|
|
1236
|
+
.replace(/\s+([.;,])/g, '$1')
|
|
1237
|
+
.replace(/^(>\s*)[\s.;,—-]+/, '$1')
|
|
1238
|
+
.trimEnd();
|
|
1239
|
+
return { text: out, changed: true };
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Drop stop-searching directives from an output that also declares itself partial.
|
|
1243
|
+
*
|
|
1244
|
+
* Section builders each append their own footer and cannot see their siblings, so
|
|
1245
|
+
* one output can carry both `**Partial locator**` ("these are anchors, keep
|
|
1246
|
+
* looking") and `**ANSWER NOW**` ("stop, do not Grep"). Measured on a 54-question
|
|
1247
|
+
* run: 21 such outputs across 17 of 48 questions. Whichever the agent obeys, the
|
|
1248
|
+
* other was wrong — it either answers from an inventory the tool just called
|
|
1249
|
+
* incomplete, or it keeps searching against an explicit prohibition.
|
|
1250
|
+
*
|
|
1251
|
+
* Partial wins: an under-informed agent that keeps looking recovers, while one
|
|
1252
|
+
* that stops on a partial answer cannot. Negated mentions (`do **not** ANSWER
|
|
1253
|
+
* NOW`) are the correct pairing and are preserved.
|
|
1254
|
+
*/
|
|
1255
|
+
function reconcilePartialAnswerNow(text) {
|
|
1256
|
+
if (!/\*\*Partial locator\*\*/i.test(text))
|
|
1257
|
+
return text;
|
|
1258
|
+
let changed = false;
|
|
1259
|
+
const out = text
|
|
1260
|
+
.split('\n')
|
|
1261
|
+
.map((line) => {
|
|
1262
|
+
if (!/ANSWER NOW/i.test(line))
|
|
1263
|
+
return line;
|
|
1264
|
+
const stripped = stripPositiveAnswerNowFromLine(line);
|
|
1265
|
+
if (stripped.changed)
|
|
1266
|
+
changed = true;
|
|
1267
|
+
return stripped.text;
|
|
1268
|
+
})
|
|
1269
|
+
.filter((line) => line.trim() !== '>')
|
|
1270
|
+
.join('\n');
|
|
1271
|
+
if (!changed)
|
|
1272
|
+
return text;
|
|
1273
|
+
return `${out}\n\n> This survey is a **Partial locator**: the sections above are anchors, not a closed answer. Continue with the narrower lookup named above, or a text search, before answering.`;
|
|
1274
|
+
}
|
|
1078
1275
|
class ToolHandler {
|
|
1079
1276
|
cg;
|
|
1080
1277
|
// Cache of opened HomeGraph instances for cross-project queries
|
|
@@ -1671,11 +1868,6 @@ class ToolHandler {
|
|
|
1671
1868
|
this.catchUpGate = null;
|
|
1672
1869
|
await this.awaitCatchUpGate(gate);
|
|
1673
1870
|
}
|
|
1674
|
-
// Hard process RSS ceiling — any tool/path that already pushed us over must
|
|
1675
|
-
// stop with success-shaped Partial (never OOM / multi-GB growth).
|
|
1676
|
-
if ((0, memory_budget_1.isOverRssBudget)()) {
|
|
1677
|
-
return (0, memory_budget_1.rssBudgetPartialResult)(toolName);
|
|
1678
|
-
}
|
|
1679
1871
|
// Honor the optional tool allowlist (HOMEGRAPH_MCP_TOOLS): a trimmed
|
|
1680
1872
|
// surface rejects ablated tools defensively even if a client cached them.
|
|
1681
1873
|
if (!this.isToolAllowed(toolName)) {
|
|
@@ -1772,9 +1964,12 @@ class ToolHandler {
|
|
|
1772
1964
|
try {
|
|
1773
1965
|
const cgFast = this.getHomeGraph(projectPath);
|
|
1774
1966
|
const rootFast = cgFast.getProjectRoot();
|
|
1775
|
-
const fast = (toolName === 'homegraph_explore'
|
|
1776
|
-
? this.
|
|
1967
|
+
const fast = (toolName === 'homegraph_explore'
|
|
1968
|
+
? this.trySpecializedExploreRoute(cgFast, q, rootFast)
|
|
1777
1969
|
: null)
|
|
1970
|
+
?? (toolName === 'homegraph_explore' || toolName === 'homegraph_search'
|
|
1971
|
+
? this.tryFastInventoryExplore(cgFast, q, rootFast)
|
|
1972
|
+
: null)
|
|
1778
1973
|
?? (toolName === 'homegraph_explore' || toolName === 'homegraph_search'
|
|
1779
1974
|
? this.tryLightMechanismExplore(cgFast, q, rootFast)
|
|
1780
1975
|
: null)
|
|
@@ -2052,6 +2247,9 @@ class ToolHandler {
|
|
|
2052
2247
|
case 'homegraph_impact': return await this.handleImpact(args);
|
|
2053
2248
|
case 'homegraph_diff_impact': return await this.handleDiffImpact(args);
|
|
2054
2249
|
case 'homegraph_explore': return await this.handleExplore(args);
|
|
2250
|
+
case 'homegraph_usages': return await this.handleSpecializedUsages(args);
|
|
2251
|
+
case 'homegraph_modules': return await this.handleSpecializedModules(args);
|
|
2252
|
+
case 'homegraph_native': return await this.handleSpecializedNative(args);
|
|
2055
2253
|
case 'homegraph_node': return await this.handleNode(args);
|
|
2056
2254
|
case 'homegraph_files': return await this.handleFiles(args);
|
|
2057
2255
|
case 'homegraph_arkui_migrate': return await this.handleArkuiMigrate(args);
|
|
@@ -2074,6 +2272,17 @@ class ToolHandler {
|
|
|
2074
2272
|
// getProjectRoot) must fall through to FTS search rather than error.
|
|
2075
2273
|
try {
|
|
2076
2274
|
const projectRoot = cg.getProjectRoot();
|
|
2275
|
+
// Bare PascalCase type search — agents almost always fan out callers next;
|
|
2276
|
+
// route to caller inventory instead of FTS → node×N.
|
|
2277
|
+
if ((0, query_utils_1.queryIsTypeNameFocus)(query)) {
|
|
2278
|
+
const types = (0, query_utils_1.extractTypeNamesFromQuery)(query);
|
|
2279
|
+
if (types.length === 1) {
|
|
2280
|
+
const callerQ = `${types[0]} 哪些方法被外部调用`;
|
|
2281
|
+
const callerRedirect = this.tryFastInventoryExplore(cg, callerQ, projectRoot);
|
|
2282
|
+
if (callerRedirect)
|
|
2283
|
+
return callerRedirect;
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2077
2286
|
if ((0, query_utils_1.queryShouldPreferExploreOverSearch)(query)) {
|
|
2078
2287
|
const exploreRedirect = this.tryFastInventoryExplore(cg, query, projectRoot)
|
|
2079
2288
|
?? this.tryLightMechanismExplore(cg, query, projectRoot)
|
|
@@ -2177,8 +2386,6 @@ class ToolHandler {
|
|
|
2177
2386
|
const callers = [];
|
|
2178
2387
|
const labels = new Map();
|
|
2179
2388
|
for (const node of defNodes) {
|
|
2180
|
-
if ((0, memory_budget_1.isOverRssBudget)())
|
|
2181
|
-
break;
|
|
2182
2389
|
for (const c of cg.getCallers(node.id)) {
|
|
2183
2390
|
if (!seen.has(c.node.id)) {
|
|
2184
2391
|
seen.add(c.node.id);
|
|
@@ -3604,14 +3811,13 @@ class ToolHandler {
|
|
|
3604
3811
|
/**
|
|
3605
3812
|
* Multi-module / circular dependency survey — lean cycle list, not an import flood.
|
|
3606
3813
|
*/
|
|
3607
|
-
|
|
3608
|
-
// Prefer leaf module tokens (`launchercommon`) over shared parents (`staticcommon`)
|
|
3609
|
-
// — parent matches flood unrelated windowscene cycles.
|
|
3814
|
+
extractFocusedModuleNames(query) {
|
|
3610
3815
|
const leafModules = new Set();
|
|
3611
3816
|
for (const p of (query.match(/\b[A-Za-z][\w]*(?:common|service|component|constants)\b/gi) ?? [])) {
|
|
3612
3817
|
const low = p.toLowerCase();
|
|
3613
|
-
if (!/^(?:static|feature|product|base|common)$/i.test(low))
|
|
3818
|
+
if (!/^(?:static|staticcommon|feature|product|base|basecommon|common)$/i.test(low)) {
|
|
3614
3819
|
leafModules.add(low);
|
|
3820
|
+
}
|
|
3615
3821
|
}
|
|
3616
3822
|
for (const seg of query.match(/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)+/g) ?? []) {
|
|
3617
3823
|
const leaf = seg.split(/[/\\]/).pop()?.toLowerCase();
|
|
@@ -3619,6 +3825,12 @@ class ToolHandler {
|
|
|
3619
3825
|
leafModules.add(leaf);
|
|
3620
3826
|
}
|
|
3621
3827
|
}
|
|
3828
|
+
return leafModules;
|
|
3829
|
+
}
|
|
3830
|
+
buildModuleDependencySurveySection(cg, query) {
|
|
3831
|
+
// Prefer leaf module tokens (`launchercommon`) over shared parents (`staticcommon`)
|
|
3832
|
+
// — parent matches flood unrelated windowscene cycles.
|
|
3833
|
+
const leafModules = this.extractFocusedModuleNames(query);
|
|
3622
3834
|
if (leafModules.size === 0)
|
|
3623
3835
|
return { section: '', hitCount: 0 };
|
|
3624
3836
|
const rel = (p) => p.replace(/\\/g, '/');
|
|
@@ -3671,6 +3883,155 @@ class ToolHandler {
|
|
|
3671
3883
|
lines.push('');
|
|
3672
3884
|
return { section: lines.join('\n'), hitCount: matchedCycles.length };
|
|
3673
3885
|
}
|
|
3886
|
+
/** Bounded manifest-only dependency graph for modules named in the query. */
|
|
3887
|
+
buildFocusedModuleManifestSection(projectRoot, query) {
|
|
3888
|
+
const moduleNames = this.extractFocusedModuleNames(query);
|
|
3889
|
+
if (moduleNames.size === 0)
|
|
3890
|
+
return { section: '', manifestCount: 0, cycleCount: 0 };
|
|
3891
|
+
const skipDirs = new Set([
|
|
3892
|
+
'node_modules', 'oh_modules', '.git', '.homegraph', '.hvigor', '.preview',
|
|
3893
|
+
'build', 'dist', 'out', 'test', 'ohosTest',
|
|
3894
|
+
]);
|
|
3895
|
+
const queue = [{ rel: '', depth: 0 }];
|
|
3896
|
+
const manifests = new Map();
|
|
3897
|
+
const linterFiles = [];
|
|
3898
|
+
const asksDetection = /构建系统|如何检测|怎么检测|检测方式|linter|build\s+system|how.{0,24}detect/i.test(query);
|
|
3899
|
+
let visited = 0;
|
|
3900
|
+
let cursor = 0;
|
|
3901
|
+
while (cursor < queue.length && visited < 8000) {
|
|
3902
|
+
const { rel, depth } = queue[cursor++];
|
|
3903
|
+
visited++;
|
|
3904
|
+
const abs = (0, path_1.resolve)(projectRoot, rel);
|
|
3905
|
+
let entries;
|
|
3906
|
+
try {
|
|
3907
|
+
entries = (0, fs_1.readdirSync)(abs, { withFileTypes: true });
|
|
3908
|
+
}
|
|
3909
|
+
catch {
|
|
3910
|
+
continue;
|
|
3911
|
+
}
|
|
3912
|
+
const leaf = rel.split('/').pop()?.toLowerCase() ?? '';
|
|
3913
|
+
if (moduleNames.has(leaf) && entries.some((entry) => entry.isFile() && entry.name === 'oh-package.json5')) {
|
|
3914
|
+
const paths = manifests.get(leaf) ?? [];
|
|
3915
|
+
if (paths.length < 3)
|
|
3916
|
+
paths.push(rel ? `${rel}/oh-package.json5` : 'oh-package.json5');
|
|
3917
|
+
manifests.set(leaf, paths);
|
|
3918
|
+
}
|
|
3919
|
+
for (const entry of entries) {
|
|
3920
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
3921
|
+
if (entry.isFile()) {
|
|
3922
|
+
if (/^(?:code-)?linter.*\.json5?$/i.test(entry.name) && linterFiles.length < 8) {
|
|
3923
|
+
linterFiles.push(childRel);
|
|
3924
|
+
}
|
|
3925
|
+
continue;
|
|
3926
|
+
}
|
|
3927
|
+
if (!entry.isDirectory() || depth >= 6)
|
|
3928
|
+
continue;
|
|
3929
|
+
if (entry.name.startsWith('.') || skipDirs.has(entry.name))
|
|
3930
|
+
continue;
|
|
3931
|
+
queue.push({ rel: childRel, depth: depth + 1 });
|
|
3932
|
+
}
|
|
3933
|
+
if (manifests.size === moduleNames.size && (!asksDetection || linterFiles.length > 0))
|
|
3934
|
+
break;
|
|
3935
|
+
}
|
|
3936
|
+
if (manifests.size === 0)
|
|
3937
|
+
return { section: '', manifestCount: 0, cycleCount: 0 };
|
|
3938
|
+
const normalizeDependency = (name) => {
|
|
3939
|
+
const noProtocol = name.replace(/^file:/i, '').replace(/\\/g, '/');
|
|
3940
|
+
return noProtocol.split('/').filter(Boolean).pop()?.toLowerCase() ?? '';
|
|
3941
|
+
};
|
|
3942
|
+
const graph = new Map();
|
|
3943
|
+
const rows = [];
|
|
3944
|
+
for (const moduleName of [...moduleNames].sort()) {
|
|
3945
|
+
const manifestPaths = manifests.get(moduleName) ?? [];
|
|
3946
|
+
if (manifestPaths.length === 0) {
|
|
3947
|
+
rows.push(`- \`${moduleName}\` — manifest not found within the bounded project scan`);
|
|
3948
|
+
graph.set(moduleName, new Set());
|
|
3949
|
+
continue;
|
|
3950
|
+
}
|
|
3951
|
+
const manifestRel = manifestPaths[0];
|
|
3952
|
+
let parsed;
|
|
3953
|
+
try {
|
|
3954
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
3955
|
+
parsed = require('jsonc-parser').parse((0, fs_1.readFileSync)((0, path_1.resolve)(projectRoot, manifestRel), 'utf-8'));
|
|
3956
|
+
}
|
|
3957
|
+
catch {
|
|
3958
|
+
rows.push(`- \`${moduleName}\` — could not parse \`${manifestRel}\``);
|
|
3959
|
+
graph.set(moduleName, new Set());
|
|
3960
|
+
continue;
|
|
3961
|
+
}
|
|
3962
|
+
const deps = parsed?.dependencies;
|
|
3963
|
+
const declared = deps && typeof deps === 'object' ? Object.entries(deps) : [];
|
|
3964
|
+
const namedDeps = new Set();
|
|
3965
|
+
for (const [depName, depValue] of declared) {
|
|
3966
|
+
const candidates = [normalizeDependency(depName)];
|
|
3967
|
+
if (typeof depValue === 'string')
|
|
3968
|
+
candidates.push(normalizeDependency(depValue));
|
|
3969
|
+
for (const candidate of candidates) {
|
|
3970
|
+
if (candidate !== moduleName && moduleNames.has(candidate))
|
|
3971
|
+
namedDeps.add(candidate);
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
graph.set(moduleName, namedDeps);
|
|
3975
|
+
const target = namedDeps.size > 0
|
|
3976
|
+
? [...namedDeps].sort().map((name) => `\`${name}\``).join(', ')
|
|
3977
|
+
: '(none among the named modules)';
|
|
3978
|
+
rows.push(`- \`${moduleName}\` → ${target} — \`${manifestRel}\` (${declared.length} declared dependencies total)`);
|
|
3979
|
+
}
|
|
3980
|
+
const cycles = new Set();
|
|
3981
|
+
const visit = (start, current, path, seen) => {
|
|
3982
|
+
for (const next of graph.get(current) ?? []) {
|
|
3983
|
+
if (next === start && path.length > 1) {
|
|
3984
|
+
const core = path;
|
|
3985
|
+
const rotations = core.map((_, index) => {
|
|
3986
|
+
const rotated = [...core.slice(index), ...core.slice(0, index)];
|
|
3987
|
+
return [...rotated, rotated[0]].join(' -> ');
|
|
3988
|
+
});
|
|
3989
|
+
cycles.add(rotations.sort()[0]);
|
|
3990
|
+
continue;
|
|
3991
|
+
}
|
|
3992
|
+
if (seen.has(next))
|
|
3993
|
+
continue;
|
|
3994
|
+
visit(start, next, [...path, next], new Set([...seen, next]));
|
|
3995
|
+
}
|
|
3996
|
+
};
|
|
3997
|
+
for (const moduleName of graph.keys())
|
|
3998
|
+
visit(moduleName, moduleName, [moduleName], new Set([moduleName]));
|
|
3999
|
+
const lines = [
|
|
4000
|
+
'**Named module manifest dependencies**', '', ...rows, '',
|
|
4001
|
+
cycles.size > 0
|
|
4002
|
+
? `Detected **${cycles.size}** cycle(s) among the named modules:`
|
|
4003
|
+
: 'Detected **no cycle** among the named modules.',
|
|
4004
|
+
];
|
|
4005
|
+
for (const cycle of cycles)
|
|
4006
|
+
lines.push(`- \`${cycle}\``);
|
|
4007
|
+
if (asksDetection) {
|
|
4008
|
+
lines.push('', '**Build / linter cycle checks**', '');
|
|
4009
|
+
let matched = 0;
|
|
4010
|
+
for (const rel of linterFiles) {
|
|
4011
|
+
let fileLines = [];
|
|
4012
|
+
try {
|
|
4013
|
+
fileLines = (0, fs_1.readFileSync)((0, path_1.resolve)(projectRoot, rel), 'utf-8').split('\n');
|
|
4014
|
+
}
|
|
4015
|
+
catch {
|
|
4016
|
+
continue;
|
|
4017
|
+
}
|
|
4018
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
4019
|
+
const line = fileLines[i] ?? '';
|
|
4020
|
+
if (!/no-cycle|circular|cycle/i.test(line))
|
|
4021
|
+
continue;
|
|
4022
|
+
lines.push(`- \`${rel}:${i + 1}\` — \`${line.trim().slice(0, 140)}\``);
|
|
4023
|
+
if (++matched >= 12)
|
|
4024
|
+
break;
|
|
4025
|
+
}
|
|
4026
|
+
if (matched >= 12)
|
|
4027
|
+
break;
|
|
4028
|
+
}
|
|
4029
|
+
if (matched === 0)
|
|
4030
|
+
lines.push('- No explicit cycle-check rule was found in the bounded linter-config scan.');
|
|
4031
|
+
}
|
|
4032
|
+
lines.push('');
|
|
4033
|
+
return { section: lines.join('\n'), manifestCount: manifests.size, cycleCount: cycles.size };
|
|
4034
|
+
}
|
|
3674
4035
|
/**
|
|
3675
4036
|
* Path-module / named-Type NAPI export surface — not a 50-file domain dump.
|
|
3676
4037
|
*/
|
|
@@ -4177,6 +4538,9 @@ class ToolHandler {
|
|
|
4177
4538
|
const eventFiles = new Set();
|
|
4178
4539
|
const enumMembers = [];
|
|
4179
4540
|
const seenMember = new Set();
|
|
4541
|
+
/** Exported `*Event` classes from a named Event module file (not enum members). */
|
|
4542
|
+
const eventClasses = [];
|
|
4543
|
+
const seenEventClass = new Set();
|
|
4180
4544
|
const considerEventFile = (fp, preferredName) => {
|
|
4181
4545
|
eventFiles.add(fp);
|
|
4182
4546
|
if (/Event$/i.test(preferredName))
|
|
@@ -4192,10 +4556,21 @@ class ToolHandler {
|
|
|
4192
4556
|
seenMember.add(key);
|
|
4193
4557
|
enumMembers.push({ name: sib.name, file: rel(sib.filePath), line: sib.startLine || 1 });
|
|
4194
4558
|
}
|
|
4195
|
-
//
|
|
4559
|
+
// Barrel / module files often export many `*Event` classes (not enums).
|
|
4196
4560
|
if ((sib.kind === 'enum' || sib.kind === 'class' || sib.kind === 'type_alias' || sib.kind === 'interface')
|
|
4197
4561
|
&& /Event$/i.test(sib.name)) {
|
|
4198
4562
|
eventNames.add(sib.name);
|
|
4563
|
+
if (sib.kind === 'class' || sib.kind === 'enum' || sib.kind === 'type_alias') {
|
|
4564
|
+
const ck = `${sib.name}:${rel(sib.filePath)}`;
|
|
4565
|
+
if (!seenEventClass.has(ck)) {
|
|
4566
|
+
seenEventClass.add(ck);
|
|
4567
|
+
eventClasses.push({
|
|
4568
|
+
name: sib.name,
|
|
4569
|
+
file: rel(sib.filePath),
|
|
4570
|
+
line: sib.startLine || 1,
|
|
4571
|
+
});
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4199
4574
|
}
|
|
4200
4575
|
}
|
|
4201
4576
|
}
|
|
@@ -4262,6 +4637,7 @@ class ToolHandler {
|
|
|
4262
4637
|
'**Event types → Manager / handler sites**',
|
|
4263
4638
|
'',
|
|
4264
4639
|
];
|
|
4640
|
+
const rankedClasses = [...eventClasses].sort((a, b) => a.name.localeCompare(b.name));
|
|
4265
4641
|
if (enumMembers.length > 0) {
|
|
4266
4642
|
lines.push('### Event enum members (indexed)');
|
|
4267
4643
|
for (const m of enumMembers.slice(0, 40)) {
|
|
@@ -4271,8 +4647,25 @@ class ToolHandler {
|
|
|
4271
4647
|
lines.push(`- … and ${enumMembers.length - 40} more`);
|
|
4272
4648
|
lines.push('');
|
|
4273
4649
|
}
|
|
4650
|
+
// Named `*Event` module files often export many Event classes (not enums).
|
|
4651
|
+
// Listing them is the answer to "有哪些事件类型" — don't force Grep for members.
|
|
4652
|
+
if (eventClasses.length > 0) {
|
|
4653
|
+
lines.push('### Event type classes (exported from Event module)');
|
|
4654
|
+
for (const c of rankedClasses.slice(0, 40)) {
|
|
4655
|
+
lines.push(`- \`${c.name}\` — \`${c.file}:${c.line}\``);
|
|
4656
|
+
}
|
|
4657
|
+
if (rankedClasses.length > 40) {
|
|
4658
|
+
lines.push(`- … and ${rankedClasses.length - 40} more Event class(es)`);
|
|
4659
|
+
}
|
|
4660
|
+
lines.push('');
|
|
4661
|
+
}
|
|
4274
4662
|
lines.push('### Event Type anchors');
|
|
4275
|
-
|
|
4663
|
+
// Prefer concrete Event classes over the barrel file stem when both exist.
|
|
4664
|
+
const anchorEvents = (eventClasses.length > 0
|
|
4665
|
+
? eventClasses.map((c) => c.name)
|
|
4666
|
+
: [...eventNames]).filter((n, i, arr) => arr.indexOf(n) === i)
|
|
4667
|
+
.sort((a, b) => a.localeCompare(b));
|
|
4668
|
+
const sortedEvents = anchorEvents;
|
|
4276
4669
|
for (const ev of sortedEvents.slice(0, 12)) {
|
|
4277
4670
|
let loc = '';
|
|
4278
4671
|
try {
|
|
@@ -4281,6 +4674,11 @@ class ToolHandler {
|
|
|
4281
4674
|
loc = ` — \`${rel(n.filePath)}:${n.startLine}\``;
|
|
4282
4675
|
}
|
|
4283
4676
|
catch { /* */ }
|
|
4677
|
+
if (!loc) {
|
|
4678
|
+
const fromClass = eventClasses.find((c) => c.name === ev);
|
|
4679
|
+
if (fromClass)
|
|
4680
|
+
loc = ` — \`${fromClass.file}:${fromClass.line}\``;
|
|
4681
|
+
}
|
|
4284
4682
|
if (!loc) {
|
|
4285
4683
|
for (const fp of eventFiles) {
|
|
4286
4684
|
const base = rel(fp).split('/').pop() || '';
|
|
@@ -4296,7 +4694,7 @@ class ToolHandler {
|
|
|
4296
4694
|
lines.push('');
|
|
4297
4695
|
// Search handlers by primary Event names (+ a few members), not every noisy sibling.
|
|
4298
4696
|
const searchKeys = [
|
|
4299
|
-
...sortedEvents.slice(0,
|
|
4697
|
+
...sortedEvents.slice(0, 12),
|
|
4300
4698
|
...enumMembers.slice(0, 12).map((m) => m.name),
|
|
4301
4699
|
];
|
|
4302
4700
|
const hits = new Map();
|
|
@@ -4355,13 +4753,112 @@ class ToolHandler {
|
|
|
4355
4753
|
}
|
|
4356
4754
|
}
|
|
4357
4755
|
const ranked = [...hits.values()].sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
|
|
4756
|
+
const consumerHits = [];
|
|
4757
|
+
const consumerSeen = new Set();
|
|
4758
|
+
const eventNamesForConsumers = rankedClasses.length > 0
|
|
4759
|
+
? rankedClasses.map((c) => c.name)
|
|
4760
|
+
: sortedEvents;
|
|
4761
|
+
const addConsumer = (file, line, snippet, event, score) => {
|
|
4762
|
+
const key = `${file}:${line}:${event}`;
|
|
4763
|
+
if (consumerSeen.has(key))
|
|
4764
|
+
return;
|
|
4765
|
+
consumerSeen.add(key);
|
|
4766
|
+
consumerHits.push({ file, line, snippet: snippet.slice(0, 120), event, score });
|
|
4767
|
+
};
|
|
4768
|
+
for (const ev of eventNamesForConsumers.slice(0, 24)) {
|
|
4769
|
+
let results = [];
|
|
4770
|
+
try {
|
|
4771
|
+
results = cg.searchNodes(ev, { limit: 36 });
|
|
4772
|
+
}
|
|
4773
|
+
catch {
|
|
4774
|
+
continue;
|
|
4775
|
+
}
|
|
4776
|
+
const esc = ev.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
4777
|
+
const onRe = new RegExp(`(?:InnerEventUtil|evtBus|eventMgr|EventMgr|focusMode\\w*EventMgr)\\.on\\s*\\(\\s*${esc}|\\.on\\s*\\(\\s*${esc}`);
|
|
4778
|
+
const seenFiles = new Set();
|
|
4779
|
+
for (const r of results) {
|
|
4780
|
+
const fp = rel(r.node.filePath);
|
|
4781
|
+
if ((0, query_utils_1.isTestFile)(fp) || eventFiles.has(r.node.filePath))
|
|
4782
|
+
continue;
|
|
4783
|
+
if (seenFiles.has(fp))
|
|
4784
|
+
continue;
|
|
4785
|
+
seenFiles.add(fp);
|
|
4786
|
+
const abs = (0, path_1.isAbsolute)(r.node.filePath)
|
|
4787
|
+
? r.node.filePath
|
|
4788
|
+
: (0, path_1.join)(projectRoot, r.node.filePath);
|
|
4789
|
+
let content = '';
|
|
4790
|
+
try {
|
|
4791
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
4792
|
+
}
|
|
4793
|
+
catch {
|
|
4794
|
+
continue;
|
|
4795
|
+
}
|
|
4796
|
+
const fileLines = content.split('\n');
|
|
4797
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
4798
|
+
const line = fileLines[i] ?? '';
|
|
4799
|
+
if (!onRe.test(line) && !line.includes(`produceOn(${ev}`) && !line.includes(`produceOn(${ev},`)) {
|
|
4800
|
+
continue;
|
|
4801
|
+
}
|
|
4802
|
+
let score = 4;
|
|
4803
|
+
if (/Controller|Manager|Handler|Vm$/i.test(fp))
|
|
4804
|
+
score += 6;
|
|
4805
|
+
if (/\.on\s*\(/.test(line))
|
|
4806
|
+
score += 4;
|
|
4807
|
+
if (/produceOn/.test(line))
|
|
4808
|
+
score += 3;
|
|
4809
|
+
addConsumer(fp, i + 1, line.trim(), ev, score);
|
|
4810
|
+
}
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4813
|
+
// DataShareManager produceOn registrations (producer side of dispatch).
|
|
4814
|
+
let mgrNodes = [];
|
|
4815
|
+
try {
|
|
4816
|
+
mgrNodes = cg.getNodesByName('DataShareManager').filter((n) => !(0, query_utils_1.isTestFile)(n.filePath));
|
|
4817
|
+
}
|
|
4818
|
+
catch {
|
|
4819
|
+
mgrNodes = [];
|
|
4820
|
+
}
|
|
4821
|
+
for (const n of mgrNodes.slice(0, 2)) {
|
|
4822
|
+
const fp = rel(n.filePath);
|
|
4823
|
+
const abs = (0, path_1.isAbsolute)(n.filePath) ? n.filePath : (0, path_1.join)(projectRoot, n.filePath);
|
|
4824
|
+
try {
|
|
4825
|
+
const fileLines = (0, fs_1.readFileSync)(abs, 'utf-8').split('\n');
|
|
4826
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
4827
|
+
const line = fileLines[i] ?? '';
|
|
4828
|
+
if (!/produceOn\s*\(/.test(line))
|
|
4829
|
+
continue;
|
|
4830
|
+
const evM = line.match(/produceOn\s*\(\s*([A-Za-z_][\w]*Event)/);
|
|
4831
|
+
const evName = evM?.[1] ?? 'produceOn';
|
|
4832
|
+
addConsumer(fp, i + 1, line.trim(), evName, 5);
|
|
4833
|
+
if (consumerHits.length >= 10)
|
|
4834
|
+
break;
|
|
4835
|
+
}
|
|
4836
|
+
}
|
|
4837
|
+
catch { /* optional */ }
|
|
4838
|
+
}
|
|
4839
|
+
consumerHits.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
|
|
4840
|
+
if (consumerHits.length > 0) {
|
|
4841
|
+
lines.push('### Event consumer registrations (`.on` / `produceOn`)');
|
|
4842
|
+
for (const h of consumerHits.slice(0, 16)) {
|
|
4843
|
+
lines.push(`- \`${h.file}:${h.line}\` ← \`${h.event}\` \`${h.snippet}\``);
|
|
4844
|
+
}
|
|
4845
|
+
if (consumerHits.length > 16) {
|
|
4846
|
+
lines.push(`- … and ${consumerHits.length - 16} more consumer site(s)`);
|
|
4847
|
+
}
|
|
4848
|
+
lines.push('');
|
|
4849
|
+
}
|
|
4358
4850
|
lines.push('### Manager / handler files referencing these events');
|
|
4359
4851
|
const memberCount = enumMembers.length;
|
|
4360
|
-
|
|
4361
|
-
|
|
4852
|
+
const eventClassCount = eventClasses.length;
|
|
4853
|
+
const dispatchAsk = /分发|分发给|DataShareMgr|dispatch|routed?\s+to/i.test(query);
|
|
4854
|
+
// Complete when: enum members + handlers, OR Event classes + consumer wiring
|
|
4855
|
+
// for dispatch asks, OR a tight Event Type set.
|
|
4362
4856
|
const complete = ranked.length > 0
|
|
4363
|
-
&& ((memberCount > 0 && memberCount <= 48)
|
|
4364
|
-
|| (
|
|
4857
|
+
&& ((memberCount > 0 && memberCount <= 48 && ranked.length >= 1)
|
|
4858
|
+
|| (eventClassCount >= 3 && eventClassCount <= 64
|
|
4859
|
+
&& (dispatchAsk ? consumerHits.length >= 2 : ranked.length >= 1))
|
|
4860
|
+
|| (sortedEvents.length > 0 && sortedEvents.length <= 4 && ranked.length >= 1 && ranked[0].score >= 5)
|
|
4861
|
+
|| (dispatchAsk && consumerHits.length >= 3));
|
|
4365
4862
|
if (ranked.length === 0) {
|
|
4366
4863
|
lines.push('- No Manager import/use sites indexed for these event types.');
|
|
4367
4864
|
lines.push('');
|
|
@@ -4379,8 +4876,16 @@ class ToolHandler {
|
|
|
4379
4876
|
lines.push(`- … and ${ranked.length - 24} more handler file(s)`);
|
|
4380
4877
|
lines.push('');
|
|
4381
4878
|
if (complete) {
|
|
4382
|
-
lines.push(
|
|
4383
|
-
|
|
4879
|
+
lines.push(eventClassCount > 0
|
|
4880
|
+
? '> Event→Manager inventory — **ANSWER NOW** from Event type classes + consumer `.on` / handler files above; '
|
|
4881
|
+
+ 'ONE narrow Grep only if a specific Manager→event branch is still missing.'
|
|
4882
|
+
: '> Event→Manager inventory — **ANSWER NOW** from enum members + handler files above; '
|
|
4883
|
+
+ 'ONE narrow Grep only if a specific `case` branch is still missing.');
|
|
4884
|
+
}
|
|
4885
|
+
else if (eventClassCount >= 3 && dispatchAsk && consumerHits.length === 0) {
|
|
4886
|
+
lines.push('> **Partial locator** — Event type list above is complete, but consumer `.on` / dispatch wiring '
|
|
4887
|
+
+ 'was not indexed. ONE Grep for `.on(EventName` or `produceOn` in `*Manager*` files — '
|
|
4888
|
+
+ 'do **not** repo-storm; do **not** ANSWER NOW as a full dispatch map.');
|
|
4384
4889
|
}
|
|
4385
4890
|
else {
|
|
4386
4891
|
lines.push('> **Partial locator** — handler files are anchors, but enum-member coverage may be incomplete. '
|
|
@@ -4390,10 +4895,10 @@ class ToolHandler {
|
|
|
4390
4895
|
lines.push('');
|
|
4391
4896
|
return {
|
|
4392
4897
|
section: lines.join('\n'),
|
|
4393
|
-
hitCount: ranked.length + memberCount,
|
|
4394
|
-
eventCount: Math.max(sortedEvents.length, memberCount > 0 ? 1 : 0),
|
|
4898
|
+
hitCount: ranked.length + memberCount + eventClassCount,
|
|
4899
|
+
eventCount: Math.max(sortedEvents.length, eventClassCount, memberCount > 0 ? 1 : 0),
|
|
4395
4900
|
handlerCount: ranked.length,
|
|
4396
|
-
memberCount,
|
|
4901
|
+
memberCount: memberCount + eventClassCount,
|
|
4397
4902
|
complete,
|
|
4398
4903
|
};
|
|
4399
4904
|
}
|
|
@@ -4493,19 +4998,61 @@ class ToolHandler {
|
|
|
4493
4998
|
lines.push('### Cross links (imports / callers)');
|
|
4494
4999
|
if (edges.length === 0) {
|
|
4495
5000
|
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
5001
|
}
|
|
4500
5002
|
else {
|
|
4501
5003
|
const uniq = [...new Set(edges)].slice(0, 40);
|
|
4502
5004
|
lines.push(...uniq);
|
|
5005
|
+
}
|
|
5006
|
+
lines.push('');
|
|
5007
|
+
// Text-scan `extends Base` when graph edges are missing — shared bases are
|
|
5008
|
+
// the usual answer to "这几个 Installer/Parser 的依赖关系".
|
|
5009
|
+
const extendsHits = [];
|
|
5010
|
+
const baseNames = new Set();
|
|
5011
|
+
let projectRoot = '';
|
|
5012
|
+
try {
|
|
5013
|
+
projectRoot = cg.getProjectRoot();
|
|
5014
|
+
}
|
|
5015
|
+
catch { /* */ }
|
|
5016
|
+
for (const a of locs) {
|
|
5017
|
+
let content = '';
|
|
5018
|
+
try {
|
|
5019
|
+
const full = (0, path_1.isAbsolute)(a.file) ? a.file : (projectRoot ? (0, path_1.join)(projectRoot, a.file) : '');
|
|
5020
|
+
if (full && (0, fs_1.existsSync)(full))
|
|
5021
|
+
content = (0, fs_1.readFileSync)(full, 'utf-8');
|
|
5022
|
+
}
|
|
5023
|
+
catch { /* */ }
|
|
5024
|
+
if (!content)
|
|
5025
|
+
continue;
|
|
5026
|
+
const m = content.match(new RegExp(`(?:export\\s+)?(?:abstract\\s+)?class\\s+${a.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s+extends\\s+([A-Za-z_][A-Za-z0-9_]*)`));
|
|
5027
|
+
if (!m?.[1])
|
|
5028
|
+
continue;
|
|
5029
|
+
const base = m[1];
|
|
5030
|
+
baseNames.add(base);
|
|
5031
|
+
extendsHits.push(`- \`${a.name}\` extends \`${base}\``);
|
|
5032
|
+
}
|
|
5033
|
+
if (extendsHits.length > 0) {
|
|
5034
|
+
lines.push('### Inheritance (source `extends`)');
|
|
5035
|
+
lines.push(...[...new Set(extendsHits)].slice(0, 20));
|
|
5036
|
+
if (baseNames.size > 0) {
|
|
5037
|
+
lines.push('');
|
|
5038
|
+
lines.push(`Shared / parent Type(s): ${[...baseNames].slice(0, 8).map((b) => `\`${b}\``).join(', ')}.`);
|
|
5039
|
+
}
|
|
4503
5040
|
lines.push('');
|
|
4504
|
-
|
|
4505
|
-
|
|
5041
|
+
}
|
|
5042
|
+
const hasGraph = edges.length > 0;
|
|
5043
|
+
const hasExtends = extendsHits.length > 0;
|
|
5044
|
+
if (!hasGraph && !hasExtends) {
|
|
5045
|
+
lines.push('> **Partial locator** — Type files above are anchors. '
|
|
5046
|
+
+ '`homegraph_node` / `homegraph_callees` one Type at a time; do **not** ANSWER NOW as a full graph.');
|
|
5047
|
+
}
|
|
5048
|
+
else {
|
|
5049
|
+
lines.push('> Multi-Type dependency inventory — **ANSWER NOW** from locations'
|
|
5050
|
+
+ (hasExtends ? ' + `extends` bases' : '')
|
|
5051
|
+
+ (hasGraph ? ' + import/caller edges' : '')
|
|
5052
|
+
+ ' above; only `homegraph_node` one Type if a body detail is still missing.');
|
|
4506
5053
|
}
|
|
4507
5054
|
lines.push('');
|
|
4508
|
-
return { section: lines.join('\n'), hitCount: locs.length + edges.length };
|
|
5055
|
+
return { section: lines.join('\n'), hitCount: locs.length + edges.length + extendsHits.length };
|
|
4509
5056
|
}
|
|
4510
5057
|
/**
|
|
4511
5058
|
* Main-thread fast path for inventory surveys — skips the worker queue.
|
|
@@ -4522,7 +5069,8 @@ class ToolHandler {
|
|
|
4522
5069
|
const cg = this.getHomeGraph(args.projectPath);
|
|
4523
5070
|
const projectRoot = cg.getProjectRoot();
|
|
4524
5071
|
if (toolName === 'homegraph_explore') {
|
|
4525
|
-
return this.
|
|
5072
|
+
return this.trySpecializedExploreRoute(cg, query, projectRoot)
|
|
5073
|
+
?? this.tryFastInventoryExplore(cg, query, projectRoot)
|
|
4526
5074
|
?? this.tryLightMechanismExplore(cg, query, projectRoot)
|
|
4527
5075
|
?? this.tryCompactLocalSymbolExplore(cg, query, projectRoot);
|
|
4528
5076
|
}
|
|
@@ -4537,6 +5085,233 @@ class ToolHandler {
|
|
|
4537
5085
|
}
|
|
4538
5086
|
return null;
|
|
4539
5087
|
}
|
|
5088
|
+
/** Conservative compatibility router: ambiguous mechanism queries stay broad. */
|
|
5089
|
+
specializedExploreRouteForQuery(query) {
|
|
5090
|
+
if ((0, query_utils_1.queryAsModuleDependencySurvey)(query))
|
|
5091
|
+
return 'modules';
|
|
5092
|
+
if ((0, query_utils_1.queryAsModuleExportSurvey)(query))
|
|
5093
|
+
return 'native';
|
|
5094
|
+
if ((0, query_utils_1.queryAsReturnValueConsumerSurvey)(query))
|
|
5095
|
+
return null;
|
|
5096
|
+
if ((0, query_utils_1.shouldBuildApiUsageSurvey)(query))
|
|
5097
|
+
return 'usages';
|
|
5098
|
+
if (queryAsBareSymbolInventory(query))
|
|
5099
|
+
return 'usages';
|
|
5100
|
+
if (!(0, query_utils_1.shouldBuildMemberSurvey)(query))
|
|
5101
|
+
return null;
|
|
5102
|
+
return /哪里(?:用|使用|调用)|哪些.{0,24}(?:用|使用|调用|依赖)|使用位置|引用位置|调用位置|\busages?\b|\bwhere\b.{0,36}\bused\b|\bwhich\b.{0,36}\b(?:files?|functions?|methods?)\b.{0,24}\b(?:use|call|depend)/i.test(query)
|
|
5103
|
+
? 'usages'
|
|
5104
|
+
: null;
|
|
5105
|
+
}
|
|
5106
|
+
/** Run exactly one bounded survey family and expose the selected route. */
|
|
5107
|
+
runSpecializedExploreRoute(route, cg, query, projectRoot) {
|
|
5108
|
+
let section = '';
|
|
5109
|
+
let status = 'no_indexed_evidence';
|
|
5110
|
+
let coverage = '';
|
|
5111
|
+
if (route === 'modules') {
|
|
5112
|
+
const manifestResult = this.buildFocusedModuleManifestSection(projectRoot, query);
|
|
5113
|
+
const graphResult = manifestResult.manifestCount === 0
|
|
5114
|
+
? this.buildModuleDependencySurveySection(cg, query)
|
|
5115
|
+
: { section: '', hitCount: 0 };
|
|
5116
|
+
section = manifestResult.section || graphResult.section;
|
|
5117
|
+
status = section ? 'complete' : 'no_indexed_evidence';
|
|
5118
|
+
coverage = manifestResult.manifestCount > 0
|
|
5119
|
+
? 'named module manifests plus bounded linter evidence only; no full-repo cycle walk or unrelated surveys were built'
|
|
5120
|
+
: 'named-module dependency/cycle graph only; no source bodies or unrelated surveys were built';
|
|
5121
|
+
}
|
|
5122
|
+
else if (route === 'native') {
|
|
5123
|
+
const result = this.buildModuleExportSurveySection(cg, query, projectRoot);
|
|
5124
|
+
section = result.section;
|
|
5125
|
+
status = result.hitCount > 0 ? 'complete' : 'no_indexed_evidence';
|
|
5126
|
+
coverage = 'named path/Type NAPI export registrations only; no domain file dump was built';
|
|
5127
|
+
}
|
|
5128
|
+
else {
|
|
5129
|
+
const apiUsage = (0, query_utils_1.shouldBuildApiUsageSurvey)(query)
|
|
5130
|
+
? this.buildApiUsageSection(cg, query, projectRoot)
|
|
5131
|
+
: { section: '', fileCount: 0 };
|
|
5132
|
+
const memberUsage = (0, query_utils_1.shouldBuildMemberSurvey)(query)
|
|
5133
|
+
&& !((0, query_utils_1.queryAsFieldUsageSurvey)(query) && apiUsage.fileCount > 0)
|
|
5134
|
+
? this.buildMemberSurveySection(cg, query, projectRoot)
|
|
5135
|
+
: '';
|
|
5136
|
+
let scanned = (0, query_utils_1.shouldBuildApiUsageSurvey)(query) || (0, query_utils_1.shouldBuildMemberSurvey)(query);
|
|
5137
|
+
section = [apiUsage.section, memberUsage].filter(Boolean).join('\n');
|
|
5138
|
+
if (!section) {
|
|
5139
|
+
const bare = this.buildBareSymbolUsageSection(cg, query, projectRoot);
|
|
5140
|
+
scanned = scanned || bare.scanned;
|
|
5141
|
+
section = bare.section;
|
|
5142
|
+
}
|
|
5143
|
+
status = section ? 'complete' : (scanned ? 'no_indexed_evidence' : 'not_surveyed');
|
|
5144
|
+
coverage = 'named API/member/constant/field usage sites only; no general call-flow or source dump was built';
|
|
5145
|
+
}
|
|
5146
|
+
const body = section
|
|
5147
|
+
|| (status === 'not_surveyed'
|
|
5148
|
+
? '- No survey ran: this query carries no symbol name to scan. Re-run naming the symbol(s), or use `homegraph_explore`.'
|
|
5149
|
+
: '- No matching evidence was found in the current index for this focused survey.');
|
|
5150
|
+
return this.textResult(this.truncateOutput([
|
|
5151
|
+
`**HomeGraph specialized route: ${route}**`,
|
|
5152
|
+
`Status: ${status}`,
|
|
5153
|
+
`Coverage: ${coverage}.`,
|
|
5154
|
+
'',
|
|
5155
|
+
body,
|
|
5156
|
+
].join('\n')));
|
|
5157
|
+
}
|
|
5158
|
+
/** Usage sites for a bare symbol bag, including non-call textual references. */
|
|
5159
|
+
buildBareSymbolUsageSection(cg, query, projectRoot) {
|
|
5160
|
+
const symbols = extractBareUsageSymbols(query);
|
|
5161
|
+
if (symbols.length === 0)
|
|
5162
|
+
return { section: '', scanned: false };
|
|
5163
|
+
const MAX_SYMBOLS = 4;
|
|
5164
|
+
const MAX_FILES_SCANNED = 80;
|
|
5165
|
+
const MAX_FILES_REPORTED = 14;
|
|
5166
|
+
const MAX_LINES_PER_FILE = 3;
|
|
5167
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
5168
|
+
const byFile = new Map();
|
|
5169
|
+
const seenFiles = new Set();
|
|
5170
|
+
let matchedFiles = 0;
|
|
5171
|
+
const matchers = symbols.slice(0, MAX_SYMBOLS).map((sym) => ({
|
|
5172
|
+
sym,
|
|
5173
|
+
re: new RegExp(`\\b${sym.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`),
|
|
5174
|
+
}));
|
|
5175
|
+
const scanFile = (fp) => {
|
|
5176
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, fp);
|
|
5177
|
+
if (!abs)
|
|
5178
|
+
return { rows: [], bytes: 0 };
|
|
5179
|
+
let content;
|
|
5180
|
+
try {
|
|
5181
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
5182
|
+
}
|
|
5183
|
+
catch {
|
|
5184
|
+
return { rows: [], bytes: 0 };
|
|
5185
|
+
}
|
|
5186
|
+
const bytes = content.length;
|
|
5187
|
+
const hit = matchers.filter((m) => m.re.test(content));
|
|
5188
|
+
if (hit.length === 0)
|
|
5189
|
+
return { rows: [], bytes };
|
|
5190
|
+
const fileLines = content.split('\n');
|
|
5191
|
+
const rows = [];
|
|
5192
|
+
for (let i = 0; i < fileLines.length && rows.length < MAX_LINES_PER_FILE; i++) {
|
|
5193
|
+
const text = (fileLines[i] ?? '').trim();
|
|
5194
|
+
if (!text)
|
|
5195
|
+
continue;
|
|
5196
|
+
const m = hit.find((x) => x.re.test(text));
|
|
5197
|
+
if (m)
|
|
5198
|
+
rows.push({ line: i + 1, text, symbol: m.sym });
|
|
5199
|
+
}
|
|
5200
|
+
return { rows, bytes };
|
|
5201
|
+
};
|
|
5202
|
+
const record = (fp, rows) => {
|
|
5203
|
+
if (rows.length === 0)
|
|
5204
|
+
return;
|
|
5205
|
+
matchedFiles++;
|
|
5206
|
+
if (byFile.size < MAX_FILES_REPORTED)
|
|
5207
|
+
byFile.set(fp, rows);
|
|
5208
|
+
};
|
|
5209
|
+
for (const { sym } of matchers) {
|
|
5210
|
+
let nodes = [];
|
|
5211
|
+
try {
|
|
5212
|
+
nodes = cg.searchNodes(sym, { limit: 60 });
|
|
5213
|
+
}
|
|
5214
|
+
catch {
|
|
5215
|
+
continue;
|
|
5216
|
+
}
|
|
5217
|
+
for (const r of nodes) {
|
|
5218
|
+
if (seenFiles.size >= MAX_FILES_SCANNED)
|
|
5219
|
+
break;
|
|
5220
|
+
const fp = rel(r.node.filePath);
|
|
5221
|
+
if ((0, query_utils_1.isTestFile)(fp) || seenFiles.has(fp))
|
|
5222
|
+
continue;
|
|
5223
|
+
seenFiles.add(fp);
|
|
5224
|
+
record(fp, scanFile(fp).rows);
|
|
5225
|
+
}
|
|
5226
|
+
}
|
|
5227
|
+
const SWEEP_DEADLINE_MS = 3000;
|
|
5228
|
+
const SWEEP_MAX_FILES = 20000;
|
|
5229
|
+
const SWEEP_MAX_BYTES = 256 * 1024 * 1024;
|
|
5230
|
+
let sweepRan = false;
|
|
5231
|
+
let sweepComplete = false;
|
|
5232
|
+
if (process.env.HOMEGRAPH_EXPLORE_USAGE_SWEEP !== '0') {
|
|
5233
|
+
let corpus = [];
|
|
5234
|
+
try {
|
|
5235
|
+
corpus = cg.getFiles();
|
|
5236
|
+
}
|
|
5237
|
+
catch {
|
|
5238
|
+
corpus = [];
|
|
5239
|
+
}
|
|
5240
|
+
if (corpus.length > 0) {
|
|
5241
|
+
sweepRan = true;
|
|
5242
|
+
sweepComplete = true;
|
|
5243
|
+
const started = Date.now();
|
|
5244
|
+
let swept = 0;
|
|
5245
|
+
let bytesRead = 0;
|
|
5246
|
+
for (const f of corpus) {
|
|
5247
|
+
if (swept >= SWEEP_MAX_FILES || bytesRead >= SWEEP_MAX_BYTES
|
|
5248
|
+
|| (swept % 64 === 0 && Date.now() - started > SWEEP_DEADLINE_MS)) {
|
|
5249
|
+
sweepComplete = false;
|
|
5250
|
+
break;
|
|
5251
|
+
}
|
|
5252
|
+
const fp = rel(String(f?.path ?? ''));
|
|
5253
|
+
if (!fp || (0, query_utils_1.isTestFile)(fp) || seenFiles.has(fp))
|
|
5254
|
+
continue;
|
|
5255
|
+
seenFiles.add(fp);
|
|
5256
|
+
swept++;
|
|
5257
|
+
const scan = scanFile(fp);
|
|
5258
|
+
bytesRead += scan.bytes;
|
|
5259
|
+
record(fp, scan.rows);
|
|
5260
|
+
}
|
|
5261
|
+
}
|
|
5262
|
+
}
|
|
5263
|
+
if (byFile.size === 0)
|
|
5264
|
+
return { section: '', scanned: true };
|
|
5265
|
+
const truncated = matchedFiles > byFile.size;
|
|
5266
|
+
const lines = [
|
|
5267
|
+
`**Symbol usage sites** — \`${symbols.slice(0, MAX_SYMBOLS).join('`, `')}\``, '',
|
|
5268
|
+
sweepRan && sweepComplete
|
|
5269
|
+
? '> Full in-repo text sweep of every indexed source file: includes non-call references with no `calls` edge.'
|
|
5270
|
+
: '> Scanned in-repo: includes non-call references with no `calls` edge.',
|
|
5271
|
+
'',
|
|
5272
|
+
];
|
|
5273
|
+
for (const [file, rows] of byFile) {
|
|
5274
|
+
lines.push(`- \`${file}\``);
|
|
5275
|
+
for (const row of rows)
|
|
5276
|
+
lines.push(` - ${row.line}: \`${row.text.slice(0, 120)}\``);
|
|
5277
|
+
}
|
|
5278
|
+
lines.push('');
|
|
5279
|
+
if (truncated) {
|
|
5280
|
+
lines.push(`> Showing ${byFile.size} of ${matchedFiles} matching file(s) — narrow the symbol to see the rest.`);
|
|
5281
|
+
}
|
|
5282
|
+
else if (sweepRan && sweepComplete) {
|
|
5283
|
+
lines.push(`> ${byFile.size} file(s) — every indexed source file was swept, so this is the complete in-repo list. **ANSWER NOW** from it: do not re-grep these names.`);
|
|
5284
|
+
}
|
|
5285
|
+
else {
|
|
5286
|
+
lines.push(`> ${byFile.size} indexed use site file(s); the text sweep did not complete, so files that merely reference these names may be missing.`);
|
|
5287
|
+
}
|
|
5288
|
+
return { section: lines.join('\n'), scanned: true };
|
|
5289
|
+
}
|
|
5290
|
+
trySpecializedExploreRoute(cg, query, projectRoot) {
|
|
5291
|
+
const route = this.specializedExploreRouteForQuery(query);
|
|
5292
|
+
return route ? this.runSpecializedExploreRoute(route, cg, query, projectRoot) : null;
|
|
5293
|
+
}
|
|
5294
|
+
async handleSpecializedUsages(args) {
|
|
5295
|
+
const query = this.validateString(args.query, 'query');
|
|
5296
|
+
if (typeof query !== 'string')
|
|
5297
|
+
return query;
|
|
5298
|
+
const cg = this.getHomeGraph(args.projectPath);
|
|
5299
|
+
return this.runSpecializedExploreRoute('usages', cg, query, cg.getProjectRoot());
|
|
5300
|
+
}
|
|
5301
|
+
async handleSpecializedModules(args) {
|
|
5302
|
+
const query = this.validateString(args.query, 'query');
|
|
5303
|
+
if (typeof query !== 'string')
|
|
5304
|
+
return query;
|
|
5305
|
+
const cg = this.getHomeGraph(args.projectPath);
|
|
5306
|
+
return this.runSpecializedExploreRoute('modules', cg, query, cg.getProjectRoot());
|
|
5307
|
+
}
|
|
5308
|
+
async handleSpecializedNative(args) {
|
|
5309
|
+
const query = this.validateString(args.query, 'query');
|
|
5310
|
+
if (typeof query !== 'string')
|
|
5311
|
+
return query;
|
|
5312
|
+
const cg = this.getHomeGraph(args.projectPath);
|
|
5313
|
+
return this.runSpecializedExploreRoute('native', cg, query, cg.getProjectRoot());
|
|
5314
|
+
}
|
|
4540
5315
|
/**
|
|
4541
5316
|
* Fast inventory-only explore — skips findRelevantContext for survey/caller/dependency queries.
|
|
4542
5317
|
*/
|
|
@@ -4706,6 +5481,11 @@ class ToolHandler {
|
|
|
4706
5481
|
: { section: '', hitCount: 0 };
|
|
4707
5482
|
if (hoverResult.section)
|
|
4708
5483
|
lines.push(hoverResult.section);
|
|
5484
|
+
const containerResult = (0, query_utils_1.queryAsContainerCompositionSurvey)(query)
|
|
5485
|
+
? this.buildContainerCompositionSection(cg, query, projectRoot)
|
|
5486
|
+
: { section: '', hitCount: 0, complete: false };
|
|
5487
|
+
if (containerResult.section)
|
|
5488
|
+
lines.push(containerResult.section);
|
|
4709
5489
|
const importInventoryFilter = (0, query_utils_1.hasImportInventoryFilter)(query);
|
|
4710
5490
|
const multiAnchor = (0, query_utils_1.queryNamesMultipleExploreAnchors)(query);
|
|
4711
5491
|
// Type / caller surveys: skip expensive named-symbol flow synthesize —
|
|
@@ -4758,7 +5538,7 @@ class ToolHandler {
|
|
|
4758
5538
|
|| callerSection || memberSection || configSection
|
|
4759
5539
|
|| systemCapResult.section || declarationResult.section || moduleDepResult.section
|
|
4760
5540
|
|| moduleExportResult.section || dtsWrapResult.section || controlSyncResult.section
|
|
4761
|
-
|| eventDispatchResult.section;
|
|
5541
|
+
|| eventDispatchResult.section || containerResult.section;
|
|
4762
5542
|
if (!hasAnySection)
|
|
4763
5543
|
return null;
|
|
4764
5544
|
// Data-source / API usage inventories are complete without source dumps —
|
|
@@ -4833,6 +5613,11 @@ class ToolHandler {
|
|
|
4833
5613
|
: 'Event→Manager survey — 0 event types / handlers indexed. **Partial locator** — do **not** ANSWER NOW; '
|
|
4834
5614
|
+ 'ONE Grep for enum members / `case` branches OK.');
|
|
4835
5615
|
}
|
|
5616
|
+
if (containerResult.section && (0, query_utils_1.queryAsContainerCompositionSurvey)(query)) {
|
|
5617
|
+
return finishCompact(containerResult.complete
|
|
5618
|
+
? `Container composition survey — **${containerResult.hitCount}** page/site(s). **ANSWER NOW** from injection snippets above; do not Grep \`LocalStorageLink\` again.`
|
|
5619
|
+
: `Container composition survey — **${containerResult.hitCount}** site(s). **Partial locator** — Read the top page file for full build() wiring.`);
|
|
5620
|
+
}
|
|
4836
5621
|
if (apiUsageResult.fileCount > 0 && !dataSourceResult.section) {
|
|
4837
5622
|
return finishCompact(`API usage survey — **${apiUsageResult.fileCount}** file(s). **ANSWER NOW** from the list above.`);
|
|
4838
5623
|
}
|
|
@@ -5069,16 +5854,21 @@ class ToolHandler {
|
|
|
5069
5854
|
const n = cg.getNode(id);
|
|
5070
5855
|
if (!n)
|
|
5071
5856
|
continue;
|
|
5072
|
-
|
|
5857
|
+
const line = n.kind === 'import' ? (0, query_utils_1.resolveImportLineFromNode)(n, projectRoot) : '';
|
|
5858
|
+
const blob = `${n.name}\n${n.filePath}\n${n.signature || ''}\n${line}`.toLowerCase();
|
|
5859
|
+
const hitsDistinctive = distinctive.some((t) => blob.includes(t.toLowerCase()));
|
|
5860
|
+
const hitsPathTok = domainPathTokens.some((t) => blob.includes(t));
|
|
5861
|
+
// Domain Manager/Service hits must still match the distinctive stem
|
|
5862
|
+
// (e.g. `xml`) — otherwise agent EN bags (`xml parser`) keep every
|
|
5863
|
+
// `*Parser*Manager` that only matched the verb noun.
|
|
5073
5864
|
if ((0, query_utils_1.isDomainRoleSymbol)(n.name, n.filePath, domainPathTokens)) {
|
|
5074
|
-
|
|
5865
|
+
if (hitsDistinctive || hitsPathTok)
|
|
5866
|
+
keep.add(id);
|
|
5075
5867
|
continue;
|
|
5076
5868
|
}
|
|
5077
|
-
|
|
5078
|
-
const blob = `${n.name}\n${n.filePath}\n${n.signature || ''}\n${line}`.toLowerCase();
|
|
5079
|
-
if (distinctive.some((t) => blob.includes(t.toLowerCase())))
|
|
5869
|
+
if (hitsDistinctive)
|
|
5080
5870
|
keep.add(id);
|
|
5081
|
-
if (
|
|
5871
|
+
if (hitsPathTok)
|
|
5082
5872
|
keep.add(id);
|
|
5083
5873
|
}
|
|
5084
5874
|
if (keep.size > 0) {
|
|
@@ -5134,12 +5924,15 @@ class ToolHandler {
|
|
|
5134
5924
|
: distinctiveDeps.length > 0
|
|
5135
5925
|
? distinctiveDeps
|
|
5136
5926
|
: (0, query_utils_1.extractDomainSearchTerms)(query)
|
|
5137
|
-
.filter((t) => /^[\x00-\x7F]+$/.test(t) && !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(t.toLowerCase()) && t.length >=
|
|
5927
|
+
.filter((t) => /^[\x00-\x7F]+$/.test(t) && !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(t.toLowerCase()) && t.length >= 3)).slice(0, 4).join(' ');
|
|
5138
5928
|
const importResult = importQuery
|
|
5139
5929
|
? this.buildImportSitesSection(cg, importQuery, projectRoot)
|
|
5140
5930
|
: { section: '', siteCount: 0, compactListing: false };
|
|
5141
5931
|
if (importResult.section) {
|
|
5142
|
-
|
|
5932
|
+
// Wider + path-diversified when a distinctive stem exists (e.g. xml→convertxml
|
|
5933
|
+
// usages span backup/ + util/ — first-N-only hid judge keypoints).
|
|
5934
|
+
const importBullets = distinctiveEarly.length > 0 ? 8 : 5;
|
|
5935
|
+
const trimmed = this.trimLightImportSection(importResult.section, importBullets);
|
|
5143
5936
|
if (trimmed)
|
|
5144
5937
|
lines.push(trimmed);
|
|
5145
5938
|
}
|
|
@@ -5184,7 +5977,7 @@ class ToolHandler {
|
|
|
5184
5977
|
lines.push('');
|
|
5185
5978
|
}
|
|
5186
5979
|
}
|
|
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 >=
|
|
5980
|
+
const distinctiveForScore = (0, query_utils_1.extractDomainSearchTerms)(query).filter((t) => /^[\x00-\x7F]+$/.test(t) && !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(t.toLowerCase()) && t.length >= 3);
|
|
5188
5981
|
const apiSeeds = (0, query_utils_1.extractMechanismEntrySeeds)(query)
|
|
5189
5982
|
.map((s) => s.toLowerCase())
|
|
5190
5983
|
.filter((s) => s.length >= 5 && !query_utils_1.GENERIC_VERB_ANCHOR_NOISE.has(s));
|
|
@@ -5206,6 +5999,12 @@ class ToolHandler {
|
|
|
5206
5999
|
if (t.length >= 4 && fpLc.includes(t))
|
|
5207
6000
|
score += 20;
|
|
5208
6001
|
}
|
|
6002
|
+
if ((0, query_utils_1.queryAsNativeRenderThreadSurvey)(query)) {
|
|
6003
|
+
if (/plugin_manager|egl_core|nativerender|xcomponent|render_service/i.test(fpLc))
|
|
6004
|
+
score += 90;
|
|
6005
|
+
if (/\.cpp$|\.h$|cmake/i.test(fpLc))
|
|
6006
|
+
score += 25;
|
|
6007
|
+
}
|
|
5209
6008
|
for (const n of nodes) {
|
|
5210
6009
|
const line = n.kind === 'import' ? (0, query_utils_1.resolveImportLineFromNode)(n, projectRoot) : '';
|
|
5211
6010
|
const blob = `${n.name}\n${n.signature || ''}\n${line}`.toLowerCase();
|
|
@@ -5301,10 +6100,32 @@ class ToolHandler {
|
|
|
5301
6100
|
return null;
|
|
5302
6101
|
// Soft-close when inventory + at least one digest exist — enough to answer
|
|
5303
6102
|
// with ONE narrow Grep; avoid Partial→re-explore churn.
|
|
6103
|
+
// Also close when distinctive-stem import evidence + digests exist but no
|
|
6104
|
+
// *Manager* survived filtering (xml/convertxml how-implemented bags).
|
|
5304
6105
|
const digestCoversNext = !!(nextMgr && sortedFiles.some(([fp]) => fp === nextMgr.filePath)
|
|
5305
6106
|
&& filesRendered > 0);
|
|
5306
|
-
const
|
|
5307
|
-
&&
|
|
6107
|
+
const importEvidenceClose = distinctiveEarly.length > 0
|
|
6108
|
+
&& importResult.siteCount >= 3
|
|
6109
|
+
&& filesRendered >= 1;
|
|
6110
|
+
const xmlStem = distinctiveEarly.some((t) => t.toLowerCase() === 'xml');
|
|
6111
|
+
const bodyText = lines.join('\n');
|
|
6112
|
+
const xmlMechanismClose = xmlStem
|
|
6113
|
+
&& filesRendered >= 1
|
|
6114
|
+
&& /convertxml|ConvertXML|fastConvertToJSObject/i.test(bodyText);
|
|
6115
|
+
const nativeRenderClose = (0, query_utils_1.queryAsNativeRenderThreadSurvey)(query)
|
|
6116
|
+
&& filesRendered >= 1
|
|
6117
|
+
&& (/plugin_manager|egl_core|OnSurfaceCreated|EGLCore|nativerender/i.test(bodyText)
|
|
6118
|
+
|| /\*\*CMake \/ link \+ GL libs\*\*/.test(bodyText));
|
|
6119
|
+
// Don't soft-close onto an antithetical Primary (Delete* while ask is install/parse).
|
|
6120
|
+
const nextContradictsPipeline = !!(nextMgr
|
|
6121
|
+
&& /(?:解析|安装|激活|下载|parse|install|activate|download)/i.test(query)
|
|
6122
|
+
&& (/^(?:Delete|Remove|Uninstall|Clear|Cancel)/i.test(nextMgr.name)
|
|
6123
|
+
|| /Delete|Uninstall|RemoveOnline|CancelRestore/i.test(nextMgr.name)));
|
|
6124
|
+
const softClose = !nextContradictsPipeline && ((hasStrongManager && filesRendered >= 1
|
|
6125
|
+
&& (digestCoversNext || managerHits.length >= 2 || topIsStrong))
|
|
6126
|
+
|| importEvidenceClose
|
|
6127
|
+
|| xmlMechanismClose
|
|
6128
|
+
|| nativeRenderClose);
|
|
5308
6129
|
const closedAnswer = softClose || mechanismClosed || (hasFlowPath && topIsStrong);
|
|
5309
6130
|
lines.push('---');
|
|
5310
6131
|
if (mechanismClosed || (hasFlowPath && topIsStrong)) {
|
|
@@ -5501,7 +6322,32 @@ class ToolHandler {
|
|
|
5501
6322
|
header.push(line);
|
|
5502
6323
|
}
|
|
5503
6324
|
}
|
|
5504
|
-
|
|
6325
|
+
// Round-robin across top path prefixes so one product/ folder cannot hide
|
|
6326
|
+
// other convertxml / kit usage sites (backup vs util vs launchercommon).
|
|
6327
|
+
const diversify = (items, max) => {
|
|
6328
|
+
const buckets = new Map();
|
|
6329
|
+
for (const line of items) {
|
|
6330
|
+
const pathMatch = line.match(/^- `([^`]+)`/);
|
|
6331
|
+
const fp = (pathMatch?.[1]?.split(':')[0] ?? '').replace(/\\/g, '/');
|
|
6332
|
+
const parts = fp.split('/').filter(Boolean);
|
|
6333
|
+
const key = parts.length >= 3 ? `${parts[0]}/${parts[1]}/${parts[2]}` : (parts.length >= 2 ? `${parts[0]}/${parts[1]}` : (parts[0] || fp || '_'));
|
|
6334
|
+
const list = buckets.get(key) ?? [];
|
|
6335
|
+
list.push(line);
|
|
6336
|
+
buckets.set(key, list);
|
|
6337
|
+
}
|
|
6338
|
+
const keys = [...buckets.keys()];
|
|
6339
|
+
const out = [];
|
|
6340
|
+
let i = 0;
|
|
6341
|
+
while (out.length < max && keys.some((k) => (buckets.get(k)?.length ?? 0) > 0)) {
|
|
6342
|
+
const k = keys[i % keys.length];
|
|
6343
|
+
const list = buckets.get(k);
|
|
6344
|
+
if (list && list.length > 0)
|
|
6345
|
+
out.push(list.shift());
|
|
6346
|
+
i++;
|
|
6347
|
+
}
|
|
6348
|
+
return out;
|
|
6349
|
+
};
|
|
6350
|
+
const chosen = diversify([...prod, ...test], maxBullets);
|
|
5505
6351
|
const out = [...header, ...chosen];
|
|
5506
6352
|
if (prod.length + test.length > maxBullets) {
|
|
5507
6353
|
out.push(`- … (light-mechanism: showing ${maxBullets} sites, tests deprioritized — **ANSWER NOW**, do not Grep the same import)`);
|
|
@@ -5593,6 +6439,7 @@ class ToolHandler {
|
|
|
5593
6439
|
const typeNameSet = new Set(typeNames.map((t) => t.toLowerCase()));
|
|
5594
6440
|
const bridge = (0, query_utils_1.queryNeedsCoNamedUseBridge)(query);
|
|
5595
6441
|
const memberFocus = (0, query_utils_1.queryHasNamedMemberFocus)(query);
|
|
6442
|
+
const uiConsequence = (0, query_utils_1.queryAsMemberUiConsequenceSurvey)(query);
|
|
5596
6443
|
const uiCluster = (0, query_utils_1.queryAsFocusedUiCluster)(query);
|
|
5597
6444
|
const typeLifecycle = (0, query_utils_1.queryAsTypeLifecycleSurvey)(query);
|
|
5598
6445
|
const componentSurface = (0, query_utils_1.queryAsComponentSurfaceSurvey)(query)
|
|
@@ -5852,29 +6699,43 @@ class ToolHandler {
|
|
|
5852
6699
|
const multiTypeThin = typeNames.length >= 2
|
|
5853
6700
|
&& !(0, query_utils_1.queryHasNamedMemberFocus)(query)
|
|
5854
6701
|
&& !(0, query_utils_1.queryAsNamedComponentAction)(query);
|
|
6702
|
+
let lifecycleStubComplete = false;
|
|
6703
|
+
let lifecycleStubSection = '';
|
|
6704
|
+
if (typeLifecycle && stubHeavy) {
|
|
6705
|
+
const stubPaths = [...fileNodes.keys()].filter(isStubPath);
|
|
6706
|
+
const lifecycleResult = this.buildSdkStubLifecycleSection(projectRoot, stubPaths, query);
|
|
6707
|
+
lifecycleStubSection = lifecycleResult.section;
|
|
6708
|
+
lifecycleStubComplete = lifecycleResult.complete;
|
|
6709
|
+
}
|
|
5855
6710
|
const lines = [
|
|
5856
6711
|
`**Exploration: ${query}**`,
|
|
5857
6712
|
'',
|
|
5858
|
-
|
|
5859
|
-
? '> **
|
|
5860
|
-
+ '
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
+ '
|
|
5865
|
-
:
|
|
5866
|
-
? '> **
|
|
5867
|
-
+ '
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
6713
|
+
lifecycleStubComplete
|
|
6714
|
+
? '> **Coarse locate complete — SDK lifecycle inventory below.** **ANSWER NOW** from state enum + callbacks; '
|
|
6715
|
+
+ 'do not Grep in-repo or re-explore the same stub names.'
|
|
6716
|
+
: stubHeavy
|
|
6717
|
+
? '> **Partial locator** — indexed hits are SDK / `.d.ts` stubs, not in-repo implementations. '
|
|
6718
|
+
+ 'Prefer `homegraph_search` / ONE explore for `*State*` / impl class names; '
|
|
6719
|
+
+ 'ONE narrow Grep for in-repo impl names is OK — do not ANSWER NOW from the stub alone.'
|
|
6720
|
+
: resourceProvenanceAsk
|
|
6721
|
+
? '> **Coarse locate — check Source digests for `$r` / Image / download.** '
|
|
6722
|
+
+ 'Treat visible bodies as already Read. ONE narrow Grep for `$r`/`http` only if digests lack load origin — '
|
|
6723
|
+
+ 'do **not** re-explore or fan out Read on the same Page/Dialog.'
|
|
6724
|
+
: multiTypeThin
|
|
6725
|
+
? '> **Partial locator** — multi-Type bag below is a coarse anchor set. '
|
|
6726
|
+
+ 'Pick ONE Type/`Type.member` for a tighter explore, or ONE narrow Grep for unindexed wiring — do **not** ANSWER NOW as a full cross-Type mechanism.'
|
|
6727
|
+
: '> **Coarse locate complete.** Treat Source + trail as already Read for these symbols. '
|
|
6728
|
+
+ 'Answer from them; do **not** re-Grep/Read/node the same symbols. '
|
|
6729
|
+
+ 'ONE narrow Grep is OK only for residual literals / unindexed wiring — not a repo-wide storm.',
|
|
5871
6730
|
'',
|
|
5872
6731
|
`Local-symbol focus: **${seedIds.size}** seed(s)` +
|
|
5873
6732
|
(neighborIds.size > 0 ? `, **${neighborIds.size}** caller/callee neighbor(s)` : '') +
|
|
5874
6733
|
' — compact explore (definition + edge trail, no related-file dump).',
|
|
5875
6734
|
'',
|
|
5876
6735
|
];
|
|
5877
|
-
if (
|
|
6736
|
+
if (lifecycleStubSection)
|
|
6737
|
+
lines.push(lifecycleStubSection);
|
|
6738
|
+
if (stubHeavy && implAsk && !lifecycleStubComplete) {
|
|
5878
6739
|
lines.push('> Stub-only compact — state/callback answers usually live in project `.ets` / `.ts` under the feature, not `@ohos.*.d.ts`.', '');
|
|
5879
6740
|
}
|
|
5880
6741
|
// Bare `BadgeManager`-style compact: surface real @ohos/@kit / sibling Managers.
|
|
@@ -5971,7 +6832,15 @@ class ToolHandler {
|
|
|
5971
6832
|
trail.push('');
|
|
5972
6833
|
lines.push(...trail);
|
|
5973
6834
|
}
|
|
5974
|
-
else if (!stubHeavy
|
|
6835
|
+
else if (!stubHeavy
|
|
6836
|
+
&& !resourceProvenanceAsk
|
|
6837
|
+
&& !multiTypeThin
|
|
6838
|
+
// UI Page/Dialog surfaces + member text-usage already answer without a
|
|
6839
|
+
// graph trail — flipping to Partial caused ThemeHome/isExpired fan-out
|
|
6840
|
+
// (163355 flat-but-worse-cost / score drops).
|
|
6841
|
+
&& !componentSurface
|
|
6842
|
+
&& !uiCluster
|
|
6843
|
+
&& !memberFocus) {
|
|
5975
6844
|
// No graph trail — coarse locate only; do not claim completeness.
|
|
5976
6845
|
const softIdx = lines.findIndex((l) => l.includes('**Coarse locate complete.**'));
|
|
5977
6846
|
if (softIdx >= 0) {
|
|
@@ -6107,7 +6976,10 @@ class ToolHandler {
|
|
|
6107
6976
|
}
|
|
6108
6977
|
lines.push('');
|
|
6109
6978
|
lines.push(memberFocus
|
|
6110
|
-
?
|
|
6979
|
+
? uiConsequence
|
|
6980
|
+
? '> UI-consequence ask: call/filter sites above show **how true/expired values affect UI** '
|
|
6981
|
+
+ '(often exclusion from render stacks). **ANSWER NOW** from them + Source — do not Read/Grep the same member.'
|
|
6982
|
+
: '> These lines are the in-repo **call/filter sites** for the named member. **ANSWER NOW** from them + Source — do not Grep/`homegraph_callers` the same member.'
|
|
6111
6983
|
: '> These lines are the in-repo use sites. Answer from them + Source; do not grep the same names again.');
|
|
6112
6984
|
lines.push('');
|
|
6113
6985
|
}
|
|
@@ -6216,7 +7088,7 @@ class ToolHandler {
|
|
|
6216
7088
|
.slice(0, maxFiles);
|
|
6217
7089
|
const digest = exploreLocatorDigestEnabled();
|
|
6218
7090
|
lines.push(digest ? '**Source digests**' : '**Source Code**', '');
|
|
6219
|
-
lines.push(stubHeavy
|
|
7091
|
+
lines.push(stubHeavy && !lifecycleStubComplete
|
|
6220
7092
|
? '> Stub digests only — treat as Partial locator. Do **not** ANSWER NOW from `.d.ts` alone; search in-repo impl next.'
|
|
6221
7093
|
: digest
|
|
6222
7094
|
? '> Short digests — treat as already Read for these symbols. Answer from anchors + digests; '
|
|
@@ -6391,26 +7263,255 @@ class ToolHandler {
|
|
|
6391
7263
|
}
|
|
6392
7264
|
if (rendered === 0)
|
|
6393
7265
|
return null;
|
|
7266
|
+
// If provenance digests already show $r / download / PixelMap, kill any leftover
|
|
7267
|
+
// Partial banner so the agent stops (163355 D17 Partial→Read fan-out).
|
|
7268
|
+
if (resourceProvenanceAsk) {
|
|
7269
|
+
const body = lines.join('\n');
|
|
7270
|
+
if (/\$r\b|PixelMap|copyFile|http[s]?:|download|getPreview|previewPixelMap/i.test(body)) {
|
|
7271
|
+
const softIdx = lines.findIndex((l) => l.includes('**Partial locator**'));
|
|
7272
|
+
if (softIdx >= 0) {
|
|
7273
|
+
lines[softIdx] =
|
|
7274
|
+
'> **Coarse locate complete.** Source digests already show load origin (`$r` / download / PixelMap). '
|
|
7275
|
+
+ '**ANSWER NOW** — do not Read/Grep the same Page/Dialog again.';
|
|
7276
|
+
}
|
|
7277
|
+
}
|
|
7278
|
+
}
|
|
6394
7279
|
lines.push('---');
|
|
6395
7280
|
lines.push('> **Compact local explore complete — ANSWER NOW.** ' +
|
|
6396
7281
|
'Do **not** Read/Grep/`homegraph_search`/`homegraph_explore`/`homegraph_node` for the same symbols.');
|
|
6397
7282
|
lines.push('');
|
|
6398
7283
|
return this.textResult(lines.join('\n'));
|
|
6399
7284
|
}
|
|
7285
|
+
/**
|
|
7286
|
+
* When a lifecycle Type only exists as SDK `.d.ts`, extract state enums +
|
|
7287
|
+
* callback registrations from the stub so agents stop grep-storming in-repo.
|
|
7288
|
+
*/
|
|
7289
|
+
buildSdkStubLifecycleSection(projectRoot, stubFilePaths, _query) {
|
|
7290
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
7291
|
+
const enums = [];
|
|
7292
|
+
const callbacks = [];
|
|
7293
|
+
for (const fp of stubFilePaths.slice(0, 4)) {
|
|
7294
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, fp);
|
|
7295
|
+
if (!abs || !(0, fs_1.existsSync)(abs))
|
|
7296
|
+
continue;
|
|
7297
|
+
let content = '';
|
|
7298
|
+
try {
|
|
7299
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
7300
|
+
}
|
|
7301
|
+
catch {
|
|
7302
|
+
continue;
|
|
7303
|
+
}
|
|
7304
|
+
const fileLines = content.split('\n');
|
|
7305
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
7306
|
+
const line = fileLines[i] ?? '';
|
|
7307
|
+
if (/^\s*enum\s+\w*State\w*\s*\{/.test(line)) {
|
|
7308
|
+
const members = [];
|
|
7309
|
+
for (let j = i + 1; j < Math.min(fileLines.length, i + 48); j++) {
|
|
7310
|
+
const l = fileLines[j] ?? '';
|
|
7311
|
+
if (/^\s*\}/.test(l))
|
|
7312
|
+
break;
|
|
7313
|
+
const m = l.match(/^\s*(\w+)\s*[,=]/);
|
|
7314
|
+
if (m?.[1])
|
|
7315
|
+
members.push(m[1]);
|
|
7316
|
+
}
|
|
7317
|
+
if (members.length > 0) {
|
|
7318
|
+
enums.push(`\`${line.trim().slice(0, 72)}\` → ${members.slice(0, 14).join(', ')}`);
|
|
7319
|
+
}
|
|
7320
|
+
}
|
|
7321
|
+
if (/on\s*\(\s*type\s*:\s*['"][\w]+['"]/.test(line)
|
|
7322
|
+
|| /Register the callback|Register.*Callback|move scene session|foreground|background|sessionStateChange/i.test(line)) {
|
|
7323
|
+
callbacks.push(`- \`${rel(fp)}:${i + 1}\` \`${line.trim().slice(0, 110)}\``);
|
|
7324
|
+
}
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
if (enums.length === 0 && callbacks.length < 3) {
|
|
7328
|
+
return { section: '', complete: false };
|
|
7329
|
+
}
|
|
7330
|
+
const lines = [
|
|
7331
|
+
'**SDK lifecycle surface (indexed stub)**',
|
|
7332
|
+
'',
|
|
7333
|
+
'> Lifecycle Type is stub-only — state enum + callbacks below are authoritative. **ANSWER NOW**; '
|
|
7334
|
+
+ 'do not Grep in-repo for the same SDK API names.',
|
|
7335
|
+
'',
|
|
7336
|
+
];
|
|
7337
|
+
if (enums.length > 0) {
|
|
7338
|
+
lines.push('**State enum(s)**', '');
|
|
7339
|
+
for (const e of enums.slice(0, 4))
|
|
7340
|
+
lines.push(`- ${e}`);
|
|
7341
|
+
lines.push('');
|
|
7342
|
+
}
|
|
7343
|
+
if (callbacks.length > 0) {
|
|
7344
|
+
lines.push('**Callbacks / transitions**', '');
|
|
7345
|
+
lines.push(...callbacks.slice(0, 22));
|
|
7346
|
+
if (callbacks.length > 22)
|
|
7347
|
+
lines.push(`- … and ${callbacks.length - 22} more`);
|
|
7348
|
+
lines.push('');
|
|
7349
|
+
}
|
|
7350
|
+
const complete = enums.length > 0 && callbacks.length >= 3;
|
|
7351
|
+
return { section: lines.join('\n'), complete: complete || callbacks.length >= 6 };
|
|
7352
|
+
}
|
|
7353
|
+
/**
|
|
7354
|
+
* Pages sharing a container component + how they inject differentiated
|
|
7355
|
+
* ViewModel / LocalStorage state into the shared shell.
|
|
7356
|
+
*/
|
|
7357
|
+
buildContainerCompositionSection(cg, query, projectRoot) {
|
|
7358
|
+
const types = (0, query_utils_1.extractTypeNamesFromQuery)(query).filter((t) => !(0, query_utils_1.isFrameworkUiDecoratorName)(t));
|
|
7359
|
+
const containerName = types.find((t) => /(?:Screen|Page|View|Container)$/i.test(t))
|
|
7360
|
+
?? types.find((t) => /Screen/i.test(t))
|
|
7361
|
+
?? types[0];
|
|
7362
|
+
const viewModelName = types.find((t) => /ViewModel$/i.test(t));
|
|
7363
|
+
if (!containerName)
|
|
7364
|
+
return { section: '', hitCount: 0, complete: false };
|
|
7365
|
+
const rel = (p) => p.replace(/\\/g, '/');
|
|
7366
|
+
const hits = [];
|
|
7367
|
+
const seen = new Set();
|
|
7368
|
+
const ingestFile = (fp, content, baseScore) => {
|
|
7369
|
+
if (seen.has(fp) || (0, query_utils_1.isTestFile)(fp))
|
|
7370
|
+
return;
|
|
7371
|
+
if (!content.includes(containerName))
|
|
7372
|
+
return;
|
|
7373
|
+
const useRe = new RegExp(`${containerName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*[({]`);
|
|
7374
|
+
if (!useRe.test(content) && !content.includes(`import { ${containerName}`))
|
|
7375
|
+
return;
|
|
7376
|
+
const fileLines = content.split('\n');
|
|
7377
|
+
const snippets = [];
|
|
7378
|
+
for (let i = 0; i < fileLines.length; i++) {
|
|
7379
|
+
const line = fileLines[i] ?? '';
|
|
7380
|
+
if (line.includes(containerName)
|
|
7381
|
+
|| /@LocalStorageLink|LocalStorageProp|screenSession/i.test(line)
|
|
7382
|
+
|| (viewModelName && line.includes(viewModelName))
|
|
7383
|
+
|| /new\s+\w+ViewModel/i.test(line)) {
|
|
7384
|
+
snippets.push(`L${i + 1}: ${line.trim().slice(0, 100)}`);
|
|
7385
|
+
if (snippets.length >= 4)
|
|
7386
|
+
break;
|
|
7387
|
+
}
|
|
7388
|
+
}
|
|
7389
|
+
if (snippets.length === 0)
|
|
7390
|
+
return;
|
|
7391
|
+
seen.add(fp);
|
|
7392
|
+
let score = baseScore;
|
|
7393
|
+
if (/@LocalStorageLink/i.test(content))
|
|
7394
|
+
score += 30;
|
|
7395
|
+
if (/CommonScbView|EntryView|DevView/i.test(fp))
|
|
7396
|
+
score += 20;
|
|
7397
|
+
if (viewModelName && content.includes(viewModelName))
|
|
7398
|
+
score += 15;
|
|
7399
|
+
hits.push({ file: fp, snippet: snippets.join(' / '), score });
|
|
7400
|
+
};
|
|
7401
|
+
let results = [];
|
|
7402
|
+
try {
|
|
7403
|
+
results = cg.searchNodes(containerName, { kinds: ['import', 'component', 'class', 'struct'], limit: 36 });
|
|
7404
|
+
}
|
|
7405
|
+
catch { /* */ }
|
|
7406
|
+
for (const r of results) {
|
|
7407
|
+
if ((0, arkts_1.isOhosApiFilePath)(r.node.filePath))
|
|
7408
|
+
continue;
|
|
7409
|
+
const fp = rel(r.node.filePath);
|
|
7410
|
+
if (!/\.(ets|tsx|jsx|vue|svelte)$/i.test(fp))
|
|
7411
|
+
continue;
|
|
7412
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
|
|
7413
|
+
if (!abs)
|
|
7414
|
+
continue;
|
|
7415
|
+
let content = '';
|
|
7416
|
+
try {
|
|
7417
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
7418
|
+
}
|
|
7419
|
+
catch {
|
|
7420
|
+
continue;
|
|
7421
|
+
}
|
|
7422
|
+
ingestFile(fp, content, 12);
|
|
7423
|
+
}
|
|
7424
|
+
if (hits.length < 4) {
|
|
7425
|
+
try {
|
|
7426
|
+
for (const f of cg.getFiles()) {
|
|
7427
|
+
if (hits.length >= 14)
|
|
7428
|
+
break;
|
|
7429
|
+
const fp = rel(f.path);
|
|
7430
|
+
if (!/\.ets$/i.test(fp))
|
|
7431
|
+
continue;
|
|
7432
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, f.path);
|
|
7433
|
+
if (!abs)
|
|
7434
|
+
continue;
|
|
7435
|
+
let content = '';
|
|
7436
|
+
try {
|
|
7437
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
7438
|
+
}
|
|
7439
|
+
catch {
|
|
7440
|
+
continue;
|
|
7441
|
+
}
|
|
7442
|
+
ingestFile(fp, content, 8);
|
|
7443
|
+
}
|
|
7444
|
+
}
|
|
7445
|
+
catch { /* optional */ }
|
|
7446
|
+
}
|
|
7447
|
+
hits.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
|
|
7448
|
+
if (hits.length === 0)
|
|
7449
|
+
return { section: '', hitCount: 0, complete: false };
|
|
7450
|
+
const lines = [
|
|
7451
|
+
'**Container composition / ViewModel injection survey**',
|
|
7452
|
+
'',
|
|
7453
|
+
`> Pages using \`${containerName}\`${viewModelName ? ` with \`${viewModelName}\`` : ''}. `
|
|
7454
|
+
+ '**ANSWER NOW** from injection snippets — do not fan out Read/callers per page.',
|
|
7455
|
+
'',
|
|
7456
|
+
];
|
|
7457
|
+
for (const h of hits.slice(0, 10)) {
|
|
7458
|
+
lines.push(`- \`${h.file}\` — ${h.snippet.slice(0, 220)}`);
|
|
7459
|
+
}
|
|
7460
|
+
if (hits.length > 10)
|
|
7461
|
+
lines.push(`- … and ${hits.length - 10} more`);
|
|
7462
|
+
lines.push('');
|
|
7463
|
+
const complete = hits.length >= 2
|
|
7464
|
+
&& hits.some((h) => /@LocalStorageLink|screenSession|ViewModel/i.test(h.snippet));
|
|
7465
|
+
return { section: lines.join('\n'), hitCount: Math.min(hits.length, 10), complete };
|
|
7466
|
+
}
|
|
6400
7467
|
/**
|
|
6401
7468
|
* Hover / pointer-handler inventory — onHover / Hover* symbols + short snippets.
|
|
6402
7469
|
* Used when the agent asks about hover effects without naming a Type yet.
|
|
6403
7470
|
*/
|
|
6404
7471
|
buildHoverHandlerSurveySection(cg, query, projectRoot) {
|
|
6405
7472
|
const topicBoost = /图标|icon|appicon|dock|launcher/i.test(query);
|
|
6406
|
-
const searchTerms =
|
|
7473
|
+
const searchTerms = topicBoost
|
|
7474
|
+
? [
|
|
7475
|
+
'onHover', 'AppIconCommonView', 'HoverAnimationUtil', 'AppIconHoverEvent',
|
|
7476
|
+
'setIconHoverEventIface', 'HoverAnimation', 'HoverEffect',
|
|
7477
|
+
]
|
|
7478
|
+
: ['onHover', 'HoverAnimation', 'HoverEffect', 'HoverConstants', 'hover'];
|
|
6407
7479
|
const seen = new Set();
|
|
6408
7480
|
const hits = [];
|
|
7481
|
+
const scoreHit = (r, term, snippet) => {
|
|
7482
|
+
const nameLc = r.node.name.toLowerCase();
|
|
7483
|
+
const pathLc = r.node.filePath.toLowerCase().replace(/\\/g, '/');
|
|
7484
|
+
const termLc = term.toLowerCase();
|
|
7485
|
+
let score = nameLc === 'onhover' || nameLc.startsWith('onhover') ? 40 : 10;
|
|
7486
|
+
if (/hover/.test(nameLc))
|
|
7487
|
+
score += 15;
|
|
7488
|
+
if (/onhover/.test(snippet.toLowerCase()))
|
|
7489
|
+
score += 35;
|
|
7490
|
+
if (/appiconcommonview|hoveranimationutil|appiconhover/i.test(nameLc))
|
|
7491
|
+
score += 45;
|
|
7492
|
+
if (/seticonhover|geticonhover/i.test(nameLc))
|
|
7493
|
+
score += 35;
|
|
7494
|
+
if (topicBoost && /launchercommon|launchericon|appicon|smartdock|appcenter/i.test(pathLc)) {
|
|
7495
|
+
score += 50;
|
|
7496
|
+
}
|
|
7497
|
+
if (topicBoost && /controlcenter|commonconstants|controlpanel/i.test(pathLc)) {
|
|
7498
|
+
score -= 45;
|
|
7499
|
+
}
|
|
7500
|
+
if (topicBoost && nameLc === 'hoverconstants' && !/launcher|appicon/i.test(pathLc)) {
|
|
7501
|
+
score -= 40;
|
|
7502
|
+
}
|
|
7503
|
+
if (topicBoost && /import/.test(snippet) && !/onhover|hoveranimation|appicon/i.test(snippet.toLowerCase())) {
|
|
7504
|
+
score -= 25;
|
|
7505
|
+
}
|
|
7506
|
+
if (termLc === 'onhover' && /component|struct/.test(r.node.kind))
|
|
7507
|
+
score += 10;
|
|
7508
|
+
return score;
|
|
7509
|
+
};
|
|
6409
7510
|
for (const term of searchTerms) {
|
|
6410
7511
|
let results = [];
|
|
6411
7512
|
try {
|
|
6412
7513
|
results = cg.searchNodes(term, {
|
|
6413
|
-
kinds: ['method', 'function', 'property', 'field', 'variable', 'constant', 'class', 'component'],
|
|
7514
|
+
kinds: ['method', 'function', 'property', 'field', 'variable', 'constant', 'class', 'component', 'struct'],
|
|
6414
7515
|
limit: 30,
|
|
6415
7516
|
});
|
|
6416
7517
|
}
|
|
@@ -6429,11 +7530,6 @@ class ToolHandler {
|
|
|
6429
7530
|
if (seen.has(key))
|
|
6430
7531
|
continue;
|
|
6431
7532
|
seen.add(key);
|
|
6432
|
-
let score = nameLc === 'onhover' || nameLc.startsWith('onhover') ? 40 : 10;
|
|
6433
|
-
if (/hover/.test(nameLc))
|
|
6434
|
-
score += 15;
|
|
6435
|
-
if (topicBoost && /icon|appicon|dock|launcher|smartdock|appcenter/i.test(pathLc))
|
|
6436
|
-
score += 25;
|
|
6437
7533
|
let snippet = (r.node.signature || r.node.name).slice(0, 120);
|
|
6438
7534
|
try {
|
|
6439
7535
|
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
|
|
@@ -6445,12 +7541,38 @@ class ToolHandler {
|
|
|
6445
7541
|
}
|
|
6446
7542
|
}
|
|
6447
7543
|
catch { /* keep signature */ }
|
|
7544
|
+
// App icon views often expose `.onHover` on a builder line — scan nearby.
|
|
7545
|
+
if (topicBoost && /appiconcommonview/i.test(pathLc)) {
|
|
7546
|
+
try {
|
|
7547
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, r.node.filePath);
|
|
7548
|
+
if (abs && (0, fs_1.existsSync)(abs)) {
|
|
7549
|
+
const lines = (0, fs_1.readFileSync)(abs, 'utf-8').split('\n');
|
|
7550
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7551
|
+
if (!/\.onHover\s*\(/.test(lines[i] ?? ''))
|
|
7552
|
+
continue;
|
|
7553
|
+
const hk = `${r.node.filePath}:${i + 1}:onHover`;
|
|
7554
|
+
if (seen.has(hk))
|
|
7555
|
+
continue;
|
|
7556
|
+
seen.add(hk);
|
|
7557
|
+
hits.push({
|
|
7558
|
+
file: r.node.filePath,
|
|
7559
|
+
line: i + 1,
|
|
7560
|
+
name: 'onHover',
|
|
7561
|
+
kind: 'property',
|
|
7562
|
+
score: 90,
|
|
7563
|
+
snippet: (lines[i] ?? '').trim().slice(0, 160),
|
|
7564
|
+
});
|
|
7565
|
+
}
|
|
7566
|
+
}
|
|
7567
|
+
}
|
|
7568
|
+
catch { /* optional */ }
|
|
7569
|
+
}
|
|
6448
7570
|
hits.push({
|
|
6449
7571
|
file: r.node.filePath,
|
|
6450
7572
|
line: r.node.startLine,
|
|
6451
7573
|
name: r.node.name,
|
|
6452
7574
|
kind: r.node.kind,
|
|
6453
|
-
score,
|
|
7575
|
+
score: scoreHit(r, term, snippet),
|
|
6454
7576
|
snippet,
|
|
6455
7577
|
});
|
|
6456
7578
|
}
|
|
@@ -6469,20 +7591,32 @@ class ToolHandler {
|
|
|
6469
7591
|
};
|
|
6470
7592
|
}
|
|
6471
7593
|
hits.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
|
|
7594
|
+
// Icon-hover asks: drop import-only control-center noise from the visible set.
|
|
7595
|
+
const filtered = topicBoost
|
|
7596
|
+
? hits.filter((h) => {
|
|
7597
|
+
const pl = h.file.toLowerCase();
|
|
7598
|
+
if (h.score >= 35)
|
|
7599
|
+
return true;
|
|
7600
|
+
if (/controlcenter|commonconstants/.test(pl) && h.name === 'HoverConstants')
|
|
7601
|
+
return false;
|
|
7602
|
+
return h.score > 0;
|
|
7603
|
+
})
|
|
7604
|
+
: hits;
|
|
7605
|
+
const displayHits = filtered.length > 0 ? filtered : hits;
|
|
6472
7606
|
const lines = [
|
|
6473
7607
|
'**Hover / pointer handler survey**',
|
|
6474
7608
|
'',
|
|
6475
7609
|
'> **ANSWER NOW** from these in-repo hover handlers + snippets. Do not Grep `onHover` again unless a named Type is still missing.',
|
|
6476
7610
|
'',
|
|
6477
7611
|
];
|
|
6478
|
-
const shown =
|
|
7612
|
+
const shown = displayHits.slice(0, 12);
|
|
6479
7613
|
for (const h of shown) {
|
|
6480
7614
|
lines.push(`- \`${h.name}\` (${h.kind}) — \`${h.file}:${h.line}\``);
|
|
6481
7615
|
if (h.snippet)
|
|
6482
7616
|
lines.push(` \`${h.snippet.slice(0, 100)}\``);
|
|
6483
7617
|
}
|
|
6484
|
-
if (
|
|
6485
|
-
lines.push(`- … and ${
|
|
7618
|
+
if (displayHits.length > shown.length)
|
|
7619
|
+
lines.push(`- … and ${displayHits.length - shown.length} more`);
|
|
6486
7620
|
lines.push('');
|
|
6487
7621
|
return { section: lines.join('\n'), hitCount: shown.length };
|
|
6488
7622
|
}
|
|
@@ -6633,8 +7767,8 @@ class ToolHandler {
|
|
|
6633
7767
|
lines.push(`- \`${sym}\` — imported in ${fileList}${more}`);
|
|
6634
7768
|
}
|
|
6635
7769
|
}
|
|
6636
|
-
// "
|
|
6637
|
-
if (
|
|
7770
|
+
// Install/deps asks (incl. agent EN "parameters dependencies") — oh-package lines.
|
|
7771
|
+
if ((0, query_utils_1.queryAsksKitInstallDeps)(query)) {
|
|
6638
7772
|
const depLines = [];
|
|
6639
7773
|
try {
|
|
6640
7774
|
for (const f of cg.getFiles()) {
|
|
@@ -7181,7 +8315,7 @@ class ToolHandler {
|
|
|
7181
8315
|
const rawSymbols = (0, query_utils_1.extractApiUsageTokens)(query).slice(0, 6);
|
|
7182
8316
|
if (rawSymbols.length === 0)
|
|
7183
8317
|
return { section: '', fileCount: 0 };
|
|
7184
|
-
// Expand Telephony → telephony / @ohos.telephony
|
|
8318
|
+
// Expand Telephony → telephony / @ohos.telephony / @kit.TelephonyKit call sites.
|
|
7185
8319
|
const symbols = [];
|
|
7186
8320
|
for (const sym of rawSymbols) {
|
|
7187
8321
|
symbols.push(sym);
|
|
@@ -7194,6 +8328,12 @@ class ToolHandler {
|
|
|
7194
8328
|
symbols.push(ohos);
|
|
7195
8329
|
}
|
|
7196
8330
|
}
|
|
8331
|
+
if (rawSymbols.some((s) => /^telephony$/i.test(s))) {
|
|
8332
|
+
for (const kit of ['@kit.TelephonyKit', '@hms.telephony.enhanced', '@hms.telephony.vsim', 'TelephonyKit']) {
|
|
8333
|
+
if (!symbols.includes(kit))
|
|
8334
|
+
symbols.push(kit);
|
|
8335
|
+
}
|
|
8336
|
+
}
|
|
7197
8337
|
const rel = (p) => p.replace(/\\/g, '/');
|
|
7198
8338
|
const hits = new Map();
|
|
7199
8339
|
const addHit = (file, line) => {
|
|
@@ -7207,8 +8347,54 @@ class ToolHandler {
|
|
|
7207
8347
|
prev.stub = prev.stub && stub;
|
|
7208
8348
|
hits.set(fp, prev);
|
|
7209
8349
|
};
|
|
7210
|
-
|
|
8350
|
+
if (rawSymbols.some((s) => /^telephony$/i.test(s))) {
|
|
8351
|
+
for (const kit of ['@kit.TelephonyKit', '@hms.telephony.enhanced', '@hms.telephony.vsim']) {
|
|
8352
|
+
let importHits = [];
|
|
8353
|
+
try {
|
|
8354
|
+
importHits = cg.searchNodes(kit.replace(/^@/, ''), { kinds: ['import'], limit: 40 });
|
|
8355
|
+
}
|
|
8356
|
+
catch {
|
|
8357
|
+
importHits = [];
|
|
8358
|
+
}
|
|
8359
|
+
for (const r of importHits) {
|
|
8360
|
+
const line = (0, query_utils_1.resolveImportLineFromNode)(r.node, projectRoot);
|
|
8361
|
+
if (line.includes(kit) || line.toLowerCase().includes(kit.toLowerCase())) {
|
|
8362
|
+
addHit(r.node.filePath, r.node.startLine);
|
|
8363
|
+
}
|
|
8364
|
+
}
|
|
8365
|
+
// FTS stem fallback (`TelephonyKit`, `telephony.vsim`, …).
|
|
8366
|
+
const stem = kit.split('.').pop() ?? kit;
|
|
8367
|
+
try {
|
|
8368
|
+
importHits = cg.searchNodes(stem, { kinds: ['import'], limit: 40 });
|
|
8369
|
+
}
|
|
8370
|
+
catch {
|
|
8371
|
+
importHits = [];
|
|
8372
|
+
}
|
|
8373
|
+
for (const r of importHits) {
|
|
8374
|
+
const line = (0, query_utils_1.resolveImportLineFromNode)(r.node, projectRoot);
|
|
8375
|
+
if (line.includes('@kit.TelephonyKit') || line.includes('@hms.telephony')) {
|
|
8376
|
+
addHit(r.node.filePath, r.node.startLine);
|
|
8377
|
+
}
|
|
8378
|
+
}
|
|
8379
|
+
}
|
|
8380
|
+
}
|
|
8381
|
+
for (const sym of symbols.slice(0, 8)) {
|
|
7211
8382
|
const symLc = sym.toLowerCase();
|
|
8383
|
+
if (sym.startsWith('@')) {
|
|
8384
|
+
let importNodes = [];
|
|
8385
|
+
try {
|
|
8386
|
+
importNodes = cg.searchNodes(sym.replace(/^@/, ''), { kinds: ['import'], limit: 28 });
|
|
8387
|
+
}
|
|
8388
|
+
catch {
|
|
8389
|
+
importNodes = [];
|
|
8390
|
+
}
|
|
8391
|
+
for (const r of importNodes) {
|
|
8392
|
+
const line = (0, query_utils_1.resolveImportLineFromNode)(r.node, projectRoot).toLowerCase();
|
|
8393
|
+
if (line.includes(symLc.replace(/^@/, '')) || line.includes(symLc)) {
|
|
8394
|
+
addHit(r.node.filePath, r.node.startLine);
|
|
8395
|
+
}
|
|
8396
|
+
}
|
|
8397
|
+
}
|
|
7212
8398
|
let nodes = [];
|
|
7213
8399
|
try {
|
|
7214
8400
|
nodes = cg.searchNodes(sym.replace(/^@/, ''), { limit: 40 });
|
|
@@ -7307,6 +8493,14 @@ class ToolHandler {
|
|
|
7307
8493
|
const rankedFiles = [...hits.entries()].sort((a, b) => {
|
|
7308
8494
|
if (a[1].stub !== b[1].stub)
|
|
7309
8495
|
return a[1].stub ? 1 : -1;
|
|
8496
|
+
const telephonyBoost = (fp) => rawSymbols.some((s) => /^telephony$/i.test(s))
|
|
8497
|
+
&& /@kit\.telephonykit|@hms\.telephony|telephonykit/i.test(fp.toLowerCase())
|
|
8498
|
+
? -1
|
|
8499
|
+
: 0;
|
|
8500
|
+
const ta = telephonyBoost(a[0]);
|
|
8501
|
+
const tb = telephonyBoost(b[0]);
|
|
8502
|
+
if (ta !== tb)
|
|
8503
|
+
return ta - tb;
|
|
7310
8504
|
return a[0].localeCompare(b[0]);
|
|
7311
8505
|
});
|
|
7312
8506
|
const projectHits = rankedFiles.filter(([, v]) => !v.stub);
|
|
@@ -7344,9 +8538,17 @@ class ToolHandler {
|
|
|
7344
8538
|
}
|
|
7345
8539
|
}
|
|
7346
8540
|
// Recompute display after call-site enrichment.
|
|
8541
|
+
const telephonyBoost = (fp) => rawSymbols.some((s) => /^telephony$/i.test(s))
|
|
8542
|
+
&& /@kit\.telephonykit|@hms\.telephony|telephonykit/i.test(fp.toLowerCase())
|
|
8543
|
+
? -1
|
|
8544
|
+
: 0;
|
|
7347
8545
|
const rankedFiles2 = [...hits.entries()].sort((a, b) => {
|
|
7348
8546
|
if (a[1].stub !== b[1].stub)
|
|
7349
8547
|
return a[1].stub ? 1 : -1;
|
|
8548
|
+
const ta = telephonyBoost(a[0]);
|
|
8549
|
+
const tb = telephonyBoost(b[0]);
|
|
8550
|
+
if (ta !== tb)
|
|
8551
|
+
return ta - tb;
|
|
7350
8552
|
return a[0].localeCompare(b[0]);
|
|
7351
8553
|
});
|
|
7352
8554
|
const projectHits2 = rankedFiles2.filter(([, v]) => !v.stub);
|
|
@@ -7400,6 +8602,34 @@ class ToolHandler {
|
|
|
7400
8602
|
lines.push(`- … and ${methodNames.size - 24} more`);
|
|
7401
8603
|
lines.push('');
|
|
7402
8604
|
}
|
|
8605
|
+
if (rawSymbols.some((s) => /^telephony$/i.test(s))) {
|
|
8606
|
+
const kitSurfaces = new Set();
|
|
8607
|
+
for (const [fp, info] of display2.slice(0, 40)) {
|
|
8608
|
+
const abs = (0, utils_1.validatePathWithinRoot)(projectRoot, fp);
|
|
8609
|
+
if (!abs || !(0, fs_1.existsSync)(abs))
|
|
8610
|
+
continue;
|
|
8611
|
+
let fileLines = [];
|
|
8612
|
+
try {
|
|
8613
|
+
fileLines = (0, fs_1.readFileSync)(abs, 'utf-8').split('\n');
|
|
8614
|
+
}
|
|
8615
|
+
catch {
|
|
8616
|
+
continue;
|
|
8617
|
+
}
|
|
8618
|
+
for (const ln of info.lines.slice(0, 8)) {
|
|
8619
|
+
const lineText = fileLines[ln - 1] ?? '';
|
|
8620
|
+
for (const m of lineText.matchAll(/@kit\.TelephonyKit|@hms\.telephony(?:\.\w+)?/g)) {
|
|
8621
|
+
if (m[0])
|
|
8622
|
+
kitSurfaces.add(m[0]);
|
|
8623
|
+
}
|
|
8624
|
+
}
|
|
8625
|
+
}
|
|
8626
|
+
if (kitSurfaces.size > 0) {
|
|
8627
|
+
lines.push('**@kit / @hms Telephony imports**');
|
|
8628
|
+
for (const k of [...kitSurfaces].sort())
|
|
8629
|
+
lines.push(`- \`${k}\``);
|
|
8630
|
+
lines.push('');
|
|
8631
|
+
}
|
|
8632
|
+
}
|
|
7403
8633
|
let shown = 0;
|
|
7404
8634
|
const isImportish = (s) => /^\s*import\b/.test(s) || /\bfrom\s+['"]@/.test(s);
|
|
7405
8635
|
const isLifetimeish = (s) => /\b(?:new|delete|free|reset)\b/i.test(s) || /=\s*new\b/.test(s);
|
|
@@ -10146,6 +11376,10 @@ class ToolHandler {
|
|
|
10146
11376
|
lines.push(`- ${p.path} (edited ${ageMs}ms ago, ${label})`);
|
|
10147
11377
|
}
|
|
10148
11378
|
}
|
|
11379
|
+
const updateNotice = (0, update_check_1.getUpdateNotice)();
|
|
11380
|
+
if (updateNotice) {
|
|
11381
|
+
lines.push('', '**Update available:**', updateNotice);
|
|
11382
|
+
}
|
|
10149
11383
|
return this.textResult(lines.join('\n'));
|
|
10150
11384
|
}
|
|
10151
11385
|
/**
|
|
@@ -11033,7 +12267,9 @@ class ToolHandler {
|
|
|
11033
12267
|
}
|
|
11034
12268
|
textResult(text) {
|
|
11035
12269
|
return {
|
|
11036
|
-
|
|
12270
|
+
// Single choke point for every tool's text, so no section builder can ship
|
|
12271
|
+
// a stop-searching directive on an output that declares itself partial.
|
|
12272
|
+
content: [{ type: 'text', text: reconcilePartialAnswerNow(text) }],
|
|
11037
12273
|
};
|
|
11038
12274
|
}
|
|
11039
12275
|
/**
|