homegraph 1.6.0 → 1.6.2
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 +31 -0
- package/README.md +64 -14
- package/dist/bin/homegraph.js +2 -2
- package/dist/db/index.js +1 -0
- package/dist/db/migrations.d.ts +6 -1
- package/dist/db/migrations.js +26 -1
- package/dist/db/queries.d.ts +9 -0
- package/dist/db/queries.js +32 -7
- package/dist/db/schema.sql +4 -0
- package/dist/extraction/grammars.js +2 -1
- package/dist/extraction/languages/c-cpp.d.ts +8 -0
- package/dist/extraction/languages/c-cpp.js +44 -0
- package/dist/extraction/wasm-runtime-flags.d.ts +4 -0
- package/dist/extraction/wasm-runtime-flags.js +9 -0
- package/dist/graph/evidence-paths.d.ts +2 -0
- package/dist/graph/evidence-paths.js +20 -3
- package/dist/index.js +7 -1
- package/dist/installer/targets/icode.d.ts +43 -0
- package/dist/installer/targets/icode.js +631 -0
- package/dist/installer/targets/registry.js +2 -0
- package/dist/installer/targets/types.d.ts +1 -1
- package/dist/mcp/arkts-evidence-packs.d.ts +17 -0
- package/dist/mcp/arkts-evidence-packs.js +218 -10
- package/dist/mcp/evidence-audit.d.ts +3 -0
- package/dist/mcp/evidence-audit.js +40 -0
- package/dist/mcp/evidence-rendering.js +1 -1
- package/dist/mcp/explore-repeat-guard.d.ts +19 -5
- package/dist/mcp/explore-repeat-guard.js +35 -14
- package/dist/mcp/explore-session-state.d.ts +5 -0
- package/dist/mcp/explore-session-state.js +1 -0
- package/dist/mcp/implementation-context.d.ts +35 -0
- package/dist/mcp/implementation-context.js +232 -0
- package/dist/mcp/locate-contract.js +1 -1
- package/dist/mcp/query-cache.d.ts +1 -15
- package/dist/mcp/query-cache.js +20 -11
- package/dist/mcp/request-evidence.d.ts +69 -0
- package/dist/mcp/request-evidence.js +297 -0
- package/dist/mcp/server-instructions.d.ts +2 -2
- package/dist/mcp/server-instructions.js +15 -15
- package/dist/mcp/session.js +3 -2
- package/dist/mcp/tools.d.ts +39 -12
- package/dist/mcp/tools.js +237 -121
- package/dist/resolution/c-feature-variants.d.ts +40 -0
- package/dist/resolution/c-feature-variants.js +272 -0
- package/dist/resolution/cpp-macro-visibility.d.ts +21 -0
- package/dist/resolution/cpp-macro-visibility.js +393 -0
- package/dist/resolution/index.js +11 -0
- package/dist/resolution/name-matcher.js +49 -2
- package/dist/resolution/types.d.ts +8 -0
- package/dist/resolution/types.js +8 -0
- package/dist/search/literal-evidence.js +11 -3
- package/dist/search/query-plan-provider.js +13 -5
- package/dist/search/query-plan.d.ts +3 -1
- package/dist/search/query-plan.js +11 -4
- package/dist/search/request-contract.d.ts +29 -0
- package/dist/search/request-contract.js +76 -0
- package/dist/sync/watcher.d.ts +30 -0
- package/dist/sync/watcher.js +45 -0
- package/package.json +1 -1
package/dist/mcp/tools.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* MCP Tool Definitions
|
|
4
|
-
*
|
|
5
|
-
* Defines the tools exposed by the HomeGraph MCP server.
|
|
6
|
-
*/
|
|
7
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.ToolHandler = exports.tools = exports.PathRefusalError = exports.GraphSourcesDisabledError = exports.NotIndexedError = void 0;
|
|
3
|
+
exports.ToolHandler = exports.DEFAULT_MCP_TOOL_SHORT_NAMES = exports.tools = exports.PathRefusalError = exports.GraphSourcesDisabledError = exports.NotIndexedError = void 0;
|
|
9
4
|
exports.__setLoadHomeGraphForTests = __setLoadHomeGraphForTests;
|
|
10
5
|
exports.normalizeQuerySpelling = normalizeQuerySpelling;
|
|
11
6
|
exports.getExploreBudget = getExploreBudget;
|
|
7
|
+
exports.formatExploreCallBudgetNote = formatExploreCallBudgetNote;
|
|
12
8
|
exports.getExploreOutputBudget = getExploreOutputBudget;
|
|
13
9
|
exports.tightenExploreBudgetForQuery = tightenExploreBudgetForQuery;
|
|
14
10
|
exports.exploreLocatorDigestEnabled = exploreLocatorDigestEnabled;
|
|
@@ -20,11 +16,17 @@ exports.formatHarmonySeamNotes = formatHarmonySeamNotes;
|
|
|
20
16
|
exports.formatStaleBanner = formatStaleBanner;
|
|
21
17
|
exports.formatStaleFooter = formatStaleFooter;
|
|
22
18
|
exports.formatDegradedBanner = formatDegradedBanner;
|
|
19
|
+
exports.resolveMcpToolAllowlist = resolveMcpToolAllowlist;
|
|
20
|
+
exports.shortMcpToolNamesEnabled = shortMcpToolNamesEnabled;
|
|
21
|
+
exports.canonicalizeMcpToolName = canonicalizeMcpToolName;
|
|
23
22
|
exports.getStaticTools = getStaticTools;
|
|
24
23
|
exports.extractBareUsageSymbols = extractBareUsageSymbols;
|
|
25
24
|
exports.queryAsBareSymbolInventory = queryAsBareSymbolInventory;
|
|
26
25
|
exports.hasPositiveAnswerNowDirective = hasPositiveAnswerNowDirective;
|
|
27
26
|
exports.reconcilePartialAnswerNow = reconcilePartialAnswerNow;
|
|
27
|
+
const evidence_audit_1 = require("./evidence-audit");
|
|
28
|
+
const implementation_context_1 = require("./implementation-context");
|
|
29
|
+
const request_contract_1 = require("../search/request-contract");
|
|
28
30
|
const query_pool_1 = require("./query-pool");
|
|
29
31
|
const source_slice_identity_1 = require("./source-slice-identity");
|
|
30
32
|
const evidence_rendering_1 = require("./evidence-rendering");
|
|
@@ -310,6 +312,14 @@ function getExploreBudget(fileCount) {
|
|
|
310
312
|
return 4;
|
|
311
313
|
return 5;
|
|
312
314
|
}
|
|
315
|
+
/**
|
|
316
|
+
* Cooldown shown on `homegraph_explore` when the project is indexed.
|
|
317
|
+
* Matches {@link exploreCooldownSeconds}. Output tiers stay on {@link getExploreBudget}.
|
|
318
|
+
*/
|
|
319
|
+
function formatExploreCallBudgetNote(fileCount) {
|
|
320
|
+
const files = Math.max(0, fileCount).toLocaleString();
|
|
321
|
+
return `Wait ${(0, explore_repeat_guard_1.exploreCooldownSeconds)()} seconds after homegraph_explore returns before calling it again (${files} files indexed).`;
|
|
322
|
+
}
|
|
313
323
|
function getExploreOutputBudget(fileCount) {
|
|
314
324
|
// Tiered budget, scaled to project size. The budget is a CEILING (relevance
|
|
315
325
|
// still gates WHAT is included), and it MUST stay under the agent's INLINE
|
|
@@ -885,11 +895,17 @@ function formatDegradedBanner(reason) {
|
|
|
885
895
|
(reason ? `\n Reason: ${reason}` : ''));
|
|
886
896
|
}
|
|
887
897
|
/**
|
|
888
|
-
*
|
|
898
|
+
* projectPath property descriptions — kept in sync with whether the field is
|
|
899
|
+
* in `inputSchema.required`. The optional text is the module-level default
|
|
900
|
+
* (session has a bound root). {@link withRequiredProjectPath} swaps in the
|
|
901
|
+
* required text when there is no default project, so schema and prose agree.
|
|
889
902
|
*/
|
|
903
|
+
const PROJECT_PATH_DESC_OPTIONAL = 'Absolute path to the project to query (or any directory inside it) — homegraph uses the nearest .homegraph/ index at or above that path. Omit to use this session\'s default project. Pass it to query a second codebase, or when the server root has no index of its own (e.g. a monorepo where only sub-projects are indexed, so there is no default project).';
|
|
904
|
+
const PROJECT_PATH_DESC_REQUIRED = 'Required. Absolute path to the project to query (or any directory inside it) — homegraph uses the nearest .homegraph/ index at or above that path. This session has no default project, so every call must pass projectPath (e.g. a monorepo sub-project with its own .homegraph/, or any already-indexed folder).';
|
|
905
|
+
/** Common projectPath property for cross-project queries (optional / default-project sessions). */
|
|
890
906
|
const projectPathProperty = {
|
|
891
907
|
type: 'string',
|
|
892
|
-
description:
|
|
908
|
+
description: PROJECT_PATH_DESC_OPTIONAL,
|
|
893
909
|
};
|
|
894
910
|
/**
|
|
895
911
|
* EVERY homegraph tool is query-only: it reads the pre-built index and never
|
|
@@ -909,10 +925,11 @@ const READ_ONLY_ANNOTATIONS = {
|
|
|
909
925
|
openWorldHint: false,
|
|
910
926
|
};
|
|
911
927
|
/**
|
|
912
|
-
* All HomeGraph MCP
|
|
928
|
+
* All HomeGraph MCP tool definitions (handlers stay registered).
|
|
913
929
|
*
|
|
914
|
-
*
|
|
915
|
-
* explore
|
|
930
|
+
* Default tools/list is the slim pair in `DEFAULT_MCP_TOOL_SHORT_NAMES`
|
|
931
|
+
* (explore / project). Opt into the full catalog with
|
|
932
|
+
* `HOMEGRAPH_MCP_TOOLS=all`. Skip HomeGraph entirely for topic file-lists,
|
|
916
933
|
* concept compares, SDK catalogs, and literal greps.
|
|
917
934
|
*
|
|
918
935
|
* All tools support cross-project queries via the optional `projectPath` parameter.
|
|
@@ -1181,16 +1198,15 @@ exports.tools = [
|
|
|
1181
1198
|
},
|
|
1182
1199
|
{
|
|
1183
1200
|
name: 'homegraph_explore',
|
|
1184
|
-
description: '
|
|
1201
|
+
description: 'Default locator when the task touches existing code in THIS repo. Required: `query`. ' +
|
|
1202
|
+
'Open alone (no parallel grep/glob/read in the same step) with feature/component/symbol keywords; `taskContext` = the requirement. ' +
|
|
1185
1203
|
'For module/route-profile **paths** and engineering overview use homegraph_project first (not this tool). ' +
|
|
1186
|
-
'
|
|
1187
|
-
'
|
|
1188
|
-
'For a missing usage, dependency/cycle or native-registration relation, use ' +
|
|
1189
|
-
'homegraph_usages, homegraph_modules, or homegraph_native instead. ' +
|
|
1204
|
+
'Do NOT skip because the index might still be building — call anyway. ' +
|
|
1205
|
+
'Located ArkTS code may include render scope, imported types, module configuration, indexed SDK signatures and control-state gaps. ' +
|
|
1190
1206
|
'Returns call paths and compact line-numbered source (Harmony route_map queries may lead with Registration sources; form/shortcuts queries may lead with Capability profiles; element/string.json literals may lead with Resource hits + optional bound .ets anchors; Seam notes may flag stubs). ArkTS symbol evidence uses complete declarations and bounded directed paths with intermediate source dependencies; explicit Gaps and stop reasons name omitted or unverified evidence. Qualify ambiguous symbols by owning type or file. State the missing relation with known anchors, requested action, scope and constraints; taskContext can carry the full task. ' +
|
|
1191
1207
|
'Reuse unchanged complete ranges; refresh missing, edited or truncated evidence. ' +
|
|
1192
1208
|
'No new evidence → change to a targeted source inspection, not a paraphrased explore. ' +
|
|
1193
|
-
|
|
1209
|
+
`After this tool returns, wait ${(0, explore_repeat_guard_1.exploreCooldownSeconds)()} seconds before calling it again. ` +
|
|
1194
1210
|
'Retrieval completion is not task completion: continue implementation and required validation.',
|
|
1195
1211
|
inputSchema: {
|
|
1196
1212
|
type: 'object',
|
|
@@ -1199,7 +1215,7 @@ exports.tools = [
|
|
|
1199
1215
|
type: 'string',
|
|
1200
1216
|
description: 'Required. For pre-edit orientation or how/mechanism: pass the user task or domain keywords (page/module/feature words). ' +
|
|
1201
1217
|
'For named flows, include Type / Type.member / component names. For @kit mechanism/flow, include module/export tokens; ' +
|
|
1202
|
-
'
|
|
1218
|
+
'not SDK feature catalogs.',
|
|
1203
1219
|
},
|
|
1204
1220
|
taskContext: {
|
|
1205
1221
|
type: 'string',
|
|
@@ -1391,11 +1407,12 @@ exports.tools = [
|
|
|
1391
1407
|
},
|
|
1392
1408
|
];
|
|
1393
1409
|
/**
|
|
1394
|
-
* Return `defs` with `projectPath` marked `required` in each tool's inputSchema
|
|
1410
|
+
* Return `defs` with `projectPath` marked `required` in each tool's inputSchema
|
|
1411
|
+
* and with a matching property description (no "Omit…" wording).
|
|
1395
1412
|
*
|
|
1396
1413
|
* Used for the NO-DEFAULT-PROJECT tool surface (issue #993): when the MCP server
|
|
1397
1414
|
* has no default project to fall back to — a gateway server started outside any
|
|
1398
|
-
* repo, or a monorepo root whose `.
|
|
1415
|
+
* repo, or a monorepo root whose `.homegraph/` indexes live only in sub-projects
|
|
1399
1416
|
* — every call MUST carry an explicit `projectPath`, so the schema should say so.
|
|
1400
1417
|
* A `required` field is a HIGH-salience channel (MCP clients surface and often
|
|
1401
1418
|
* validate it), unlike the instructions text the reporter found too weak to stop
|
|
@@ -1404,23 +1421,90 @@ exports.tools = [
|
|
|
1404
1421
|
*
|
|
1405
1422
|
* Pure: clones each tool's schema rather than mutating the shared module-level
|
|
1406
1423
|
* `tools` array (reused by every session and the static surface). A tool that
|
|
1407
|
-
* doesn't expose projectPath
|
|
1408
|
-
*
|
|
1409
|
-
*
|
|
1424
|
+
* doesn't expose projectPath is returned untouched; explore's `['query']`
|
|
1425
|
+
* becomes `['query', 'projectPath']`, and a tool with no `required` list
|
|
1426
|
+
* (status/files) gains `['projectPath']`. The property description is always
|
|
1427
|
+
* swapped to {@link PROJECT_PATH_DESC_REQUIRED} so it cannot contradict
|
|
1428
|
+
* `required`.
|
|
1410
1429
|
*/
|
|
1411
1430
|
function withRequiredProjectPath(defs) {
|
|
1412
1431
|
return defs.map((tool) => {
|
|
1413
|
-
|
|
1432
|
+
const prop = tool.inputSchema.properties.projectPath;
|
|
1433
|
+
if (!prop)
|
|
1414
1434
|
return tool;
|
|
1415
1435
|
const required = tool.inputSchema.required ?? [];
|
|
1416
|
-
|
|
1417
|
-
|
|
1436
|
+
const nextRequired = required.includes('projectPath')
|
|
1437
|
+
? required
|
|
1438
|
+
: [...required, 'projectPath'];
|
|
1418
1439
|
return {
|
|
1419
1440
|
...tool,
|
|
1420
|
-
inputSchema: {
|
|
1441
|
+
inputSchema: {
|
|
1442
|
+
...tool.inputSchema,
|
|
1443
|
+
properties: {
|
|
1444
|
+
...tool.inputSchema.properties,
|
|
1445
|
+
projectPath: { ...prop, description: PROJECT_PATH_DESC_REQUIRED },
|
|
1446
|
+
},
|
|
1447
|
+
required: nextRequired,
|
|
1448
|
+
},
|
|
1421
1449
|
};
|
|
1422
1450
|
});
|
|
1423
1451
|
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Default MCP tools/list surface (product slim). Handlers for other tools remain
|
|
1454
|
+
* in-tree; restore the full catalog with `HOMEGRAPH_MCP_TOOLS=all` (or `*`), or
|
|
1455
|
+
* name a comma list (e.g. `explore,node,search,arkui_migrate`).
|
|
1456
|
+
*/
|
|
1457
|
+
exports.DEFAULT_MCP_TOOL_SHORT_NAMES = ['explore', 'project'];
|
|
1458
|
+
/** Resolve the exposed-tool allowlist from env (default = product slim pair). */
|
|
1459
|
+
function resolveMcpToolAllowlist(raw = process.env.HOMEGRAPH_MCP_TOOLS) {
|
|
1460
|
+
if (!raw || !raw.trim()) {
|
|
1461
|
+
return new Set(exports.DEFAULT_MCP_TOOL_SHORT_NAMES);
|
|
1462
|
+
}
|
|
1463
|
+
const trimmed = raw.trim();
|
|
1464
|
+
if (trimmed === '*' || /^all$/i.test(trimmed))
|
|
1465
|
+
return 'all';
|
|
1466
|
+
const set = new Set(trimmed.split(',').map((s) => s.trim().replace(/^homegraph_/, '')).filter(Boolean));
|
|
1467
|
+
return set.size ? set : new Set(exports.DEFAULT_MCP_TOOL_SHORT_NAMES);
|
|
1468
|
+
}
|
|
1469
|
+
function filterToolsByAllowlist(defs, allow) {
|
|
1470
|
+
if (allow === 'all')
|
|
1471
|
+
return defs;
|
|
1472
|
+
return defs.filter((t) => allow.has(t.name.replace(/^homegraph_/, '')));
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* Product hosts that prefix MCP tools with the server name (e.g. DevEco Code
|
|
1476
|
+
* → `homegraph_` + tool name) set `HOMEGRAPH_MCP_SHORT_TOOL_NAMES=1` so
|
|
1477
|
+
* tools/list exposes `explore` / `project` instead of `homegraph_explore`.
|
|
1478
|
+
* After the host prefix, the model-visible name is still `homegraph_explore`.
|
|
1479
|
+
* Cursor / Claude leave this unset and keep the canonical `homegraph_*` names.
|
|
1480
|
+
*/
|
|
1481
|
+
function shortMcpToolNamesEnabled(raw = process.env.HOMEGRAPH_MCP_SHORT_TOOL_NAMES) {
|
|
1482
|
+
const v = (raw ?? '').trim().toLowerCase();
|
|
1483
|
+
return v === '1' || v === 'true' || v === 'yes';
|
|
1484
|
+
}
|
|
1485
|
+
const CANONICAL_TOOL_NAMES = new Set(exports.tools.map((t) => t.name));
|
|
1486
|
+
/**
|
|
1487
|
+
* Map a tools/call name to the canonical `homegraph_*` form.
|
|
1488
|
+
* Accepts both short (`explore`) and long (`homegraph_explore`) so hosts that
|
|
1489
|
+
* list short names (or still pass the long name) both work.
|
|
1490
|
+
*/
|
|
1491
|
+
function canonicalizeMcpToolName(name) {
|
|
1492
|
+
if (!name)
|
|
1493
|
+
return name;
|
|
1494
|
+
if (name.startsWith('homegraph_'))
|
|
1495
|
+
return name;
|
|
1496
|
+
const canonical = `homegraph_${name}`;
|
|
1497
|
+
return CANONICAL_TOOL_NAMES.has(canonical) ? canonical : name;
|
|
1498
|
+
}
|
|
1499
|
+
/** Clone defs with `homegraph_` stripped from each tool name (list surface only). */
|
|
1500
|
+
function withShortToolNames(defs) {
|
|
1501
|
+
if (!shortMcpToolNamesEnabled())
|
|
1502
|
+
return defs;
|
|
1503
|
+
return defs.map((t) => ({
|
|
1504
|
+
...t,
|
|
1505
|
+
name: t.name.replace(/^homegraph_/, ''),
|
|
1506
|
+
}));
|
|
1507
|
+
}
|
|
1424
1508
|
/**
|
|
1425
1509
|
* Allowlist-filtered tool definitions WITHOUT an engine — the static surface the
|
|
1426
1510
|
* proxy answers `tools/list` with before any project is open. Mirrors
|
|
@@ -1428,12 +1512,7 @@ function withRequiredProjectPath(defs) {
|
|
|
1428
1512
|
* note in a description only adds once `cg` is loaded; the schemas are static).
|
|
1429
1513
|
*/
|
|
1430
1514
|
function getStaticTools() {
|
|
1431
|
-
|
|
1432
|
-
if (!raw || !raw.trim()) {
|
|
1433
|
-
return exports.tools;
|
|
1434
|
-
}
|
|
1435
|
-
const allow = new Set(raw.split(',').map(s => s.trim().replace(/^homegraph_/, '').replace(/^homegraph_/, '')).filter(Boolean));
|
|
1436
|
-
return allow.size ? exports.tools.filter(t => allow.has(t.name.replace(/^homegraph_/, ''))) : exports.tools;
|
|
1515
|
+
return withShortToolNames(filterToolsByAllowlist(exports.tools, resolveMcpToolAllowlist()));
|
|
1437
1516
|
}
|
|
1438
1517
|
/** Prose that reads like an identifier but never names a symbol worth scanning. */
|
|
1439
1518
|
const BARE_USAGE_STOPWORDS = new Set([
|
|
@@ -1697,26 +1776,20 @@ class ToolHandler {
|
|
|
1697
1776
|
return this.cg !== null;
|
|
1698
1777
|
}
|
|
1699
1778
|
/**
|
|
1700
|
-
* Optional allowlist of exposed tools, parsed from
|
|
1701
|
-
*
|
|
1702
|
-
*
|
|
1703
|
-
* exposed. Lets an operator (or an A/B harness) trim the tool surface
|
|
1704
|
-
* without rebuilding the client config; the ablated tool is then truly
|
|
1705
|
-
* absent from ListTools rather than merely denied on call.
|
|
1779
|
+
* Optional allowlist of exposed tools, parsed from HOMEGRAPH_MCP_TOOLS.
|
|
1780
|
+
* Unset/empty → product slim default (`explore`, `project`).
|
|
1781
|
+
* `all` / `*` → full catalog. Comma list → only those short names.
|
|
1706
1782
|
* Matching is on the short form, so "node" and "homegraph_node" both work.
|
|
1707
1783
|
*/
|
|
1708
1784
|
toolAllowlist() {
|
|
1709
|
-
|
|
1710
|
-
if (!raw || !raw.trim())
|
|
1711
|
-
return null;
|
|
1712
|
-
const short = (s) => s.trim().replace(/^homegraph_/, '');
|
|
1713
|
-
const set = new Set(raw.split(',').map(short).filter(Boolean));
|
|
1714
|
-
return set.size ? set : null;
|
|
1785
|
+
return resolveMcpToolAllowlist();
|
|
1715
1786
|
}
|
|
1716
|
-
/** Whether a tool name passes the HOMEGRAPH_MCP_TOOLS allowlist
|
|
1787
|
+
/** Whether a tool name passes the HOMEGRAPH_MCP_TOOLS allowlist. */
|
|
1717
1788
|
isToolAllowed(name) {
|
|
1718
1789
|
const allow = this.toolAllowlist();
|
|
1719
|
-
|
|
1790
|
+
if (allow === 'all')
|
|
1791
|
+
return true;
|
|
1792
|
+
return allow.has(name.replace(/^homegraph_/, ''));
|
|
1720
1793
|
}
|
|
1721
1794
|
/**
|
|
1722
1795
|
* Get tool definitions with dynamic descriptions based on project size.
|
|
@@ -1726,11 +1799,7 @@ class ToolHandler {
|
|
|
1726
1799
|
*/
|
|
1727
1800
|
getTools() {
|
|
1728
1801
|
const allow = this.toolAllowlist();
|
|
1729
|
-
|
|
1730
|
-
// the surface to only the listed short names.
|
|
1731
|
-
let visible = allow
|
|
1732
|
-
? exports.tools.filter(t => allow.has(t.name.replace(/^homegraph_/, '')))
|
|
1733
|
-
: exports.tools;
|
|
1802
|
+
let visible = filterToolsByAllowlist(exports.tools, allow);
|
|
1734
1803
|
// No default project loaded → no-root-index case (#993): a gateway server
|
|
1735
1804
|
// started outside any repo, or a monorepo root whose indexes live in
|
|
1736
1805
|
// sub-projects. With nothing to fall back to, EVERY call needs an explicit
|
|
@@ -1742,32 +1811,16 @@ class ToolHandler {
|
|
|
1742
1811
|
// IS open we leave projectPath optional (below): a bare call falls back to
|
|
1743
1812
|
// it, exactly as in the common single-project launch.
|
|
1744
1813
|
if (!this.cg)
|
|
1745
|
-
return withRequiredProjectPath(visible);
|
|
1814
|
+
return withShortToolNames(withRequiredProjectPath(visible));
|
|
1746
1815
|
try {
|
|
1747
1816
|
const stats = this.cg.getStats();
|
|
1748
|
-
// Tiny-repo tool gating
|
|
1749
|
-
//
|
|
1750
|
-
//
|
|
1751
|
-
// to one grep. (Historical note: the audit below ran when context and
|
|
1752
|
-
// trace still existed; its "5 core tools" are today's trio.)
|
|
1817
|
+
// Tiny-repo tool gating applies only when the host asked for the full
|
|
1818
|
+
// catalog (`HOMEGRAPH_MCP_TOOLS=all`). The product default is already the
|
|
1819
|
+
// slim pair; an explicit comma allowlist is left untouched.
|
|
1753
1820
|
//
|
|
1754
|
-
// n=2 audits ruled out cutting below 5 tools
|
|
1755
|
-
//
|
|
1756
|
-
//
|
|
1757
|
-
// what homegraph_node + homegraph_explore would have answered.
|
|
1758
|
-
// - 1-tool gate (search only): catastrophic regression — express
|
|
1759
|
-
// went from -43% WIN to +107% LOSS. With only search, the agent
|
|
1760
|
-
// can't navigate the call graph structurally and reads everything.
|
|
1761
|
-
//
|
|
1762
|
-
// 5 is the empirical lower bound. Tools beyond search/context/
|
|
1763
|
-
// node/explore/trace pay overhead that the agent doesn't recoup
|
|
1764
|
-
// on tiny-repo flow questions.
|
|
1765
|
-
// ITER4: raise threshold 150 → 500 so single-file frameworks
|
|
1766
|
-
// (sinatra at 159, slim_framework around 200) also get the
|
|
1767
|
-
// 5-tool surface. The empirical 5-tool floor was set on <150
|
|
1768
|
-
// probes; iter3 measurement showed sinatra is structurally the
|
|
1769
|
-
// SAME problem as cobra (single-file WITHOUT-arm Read wins),
|
|
1770
|
-
// so it deserves the same gating.
|
|
1821
|
+
// Historical note: n=2 audits ruled out cutting below ~5 tools on the
|
|
1822
|
+
// *full* catalog for tiny OSS repos (search+node+explore+…). That trim
|
|
1823
|
+
// still matters when someone opts into `all` on a <500-file tree.
|
|
1771
1824
|
const TINY_REPO_FILE_THRESHOLD = 500;
|
|
1772
1825
|
const TINY_REPO_CORE_TOOLS = new Set([
|
|
1773
1826
|
'homegraph_explore',
|
|
@@ -1776,12 +1829,12 @@ class ToolHandler {
|
|
|
1776
1829
|
'homegraph_diff_impact',
|
|
1777
1830
|
'homegraph_project',
|
|
1778
1831
|
]);
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
if (
|
|
1832
|
+
const envRaw = (process.env.HOMEGRAPH_MCP_TOOLS ?? '').trim();
|
|
1833
|
+
const askedForFullCatalog = allow === 'all' && (/^all$/i.test(envRaw) || envRaw === '*');
|
|
1834
|
+
if (askedForFullCatalog && stats.fileCount < TINY_REPO_FILE_THRESHOLD) {
|
|
1782
1835
|
visible = visible.filter(t => TINY_REPO_CORE_TOOLS.has(t.name));
|
|
1783
1836
|
}
|
|
1784
|
-
return visible.map(tool => {
|
|
1837
|
+
return withShortToolNames(visible.map(tool => {
|
|
1785
1838
|
if (tool.name === 'homegraph_explore') {
|
|
1786
1839
|
return {
|
|
1787
1840
|
...tool,
|
|
@@ -1789,10 +1842,10 @@ class ToolHandler {
|
|
|
1789
1842
|
};
|
|
1790
1843
|
}
|
|
1791
1844
|
return tool;
|
|
1792
|
-
});
|
|
1845
|
+
}));
|
|
1793
1846
|
}
|
|
1794
1847
|
catch {
|
|
1795
|
-
return visible;
|
|
1848
|
+
return withShortToolNames(visible);
|
|
1796
1849
|
}
|
|
1797
1850
|
}
|
|
1798
1851
|
/**
|
|
@@ -1808,12 +1861,12 @@ class ToolHandler {
|
|
|
1808
1861
|
* guidance so the frozen description and the live call-time response tell
|
|
1809
1862
|
* the same story: failed build → say so (indexing is the user's call, so
|
|
1810
1863
|
* the agent relays it); still building → point at homegraph_project and
|
|
1811
|
-
* retry; genuinely empty → honest empty note. Only fileCount > 0 carries
|
|
1812
|
-
*
|
|
1864
|
+
* retry; genuinely empty → honest empty note. Only fileCount > 0 carries the
|
|
1865
|
+
* return cooldown — there is nothing to cool down over an unbuilt index.
|
|
1813
1866
|
*/
|
|
1814
1867
|
exploreSuffix(fileCount) {
|
|
1815
1868
|
if (fileCount > 0) {
|
|
1816
|
-
return
|
|
1869
|
+
return formatExploreCallBudgetNote(fileCount);
|
|
1817
1870
|
}
|
|
1818
1871
|
const cg = this.cg;
|
|
1819
1872
|
if (cg) {
|
|
@@ -2272,6 +2325,8 @@ class ToolHandler {
|
|
|
2272
2325
|
* it (the CLI does) and explore behaves exactly as before, untracked.
|
|
2273
2326
|
*/
|
|
2274
2327
|
async execute(toolName, args, sessionState) {
|
|
2328
|
+
// Short list names (`explore`) and canonical (`homegraph_explore`) both work.
|
|
2329
|
+
toolName = canonicalizeMcpToolName(toolName);
|
|
2275
2330
|
const requestStartedAt = Date.now();
|
|
2276
2331
|
const result = await this.executeCore(toolName, args, sessionState, requestStartedAt);
|
|
2277
2332
|
this.traceToolCall(toolName, args, result, requestStartedAt);
|
|
@@ -2401,7 +2456,8 @@ class ToolHandler {
|
|
|
2401
2456
|
// would re-serve the first call's full source and defeat CG-18 dedup.
|
|
2402
2457
|
const skipCacheForSession = toolName === 'homegraph_explore' && !!sessionState;
|
|
2403
2458
|
const requestPlan = readQueryPlan(args);
|
|
2404
|
-
const cacheEnabled = !skipCacheForSession && requestPlan?.source !== 'llm'
|
|
2459
|
+
const cacheEnabled = !skipCacheForSession && !(toolName === 'homegraph_explore' && process.env.HOMEGRAPH_ARKTS_EVIDENCE_LOG === '1') && requestPlan?.source !== 'llm'
|
|
2460
|
+
&& !(requestPlan?.requestContract && ((0, request_contract_1.accuracyTargetsEnabled)() || (0, request_contract_1.accuracyCoverageEnabled)()))
|
|
2405
2461
|
&& !requestPlan?.telemetry.fallbackReason && (0, query_cache_1.isMcpQueryCacheEnabled)() && (0, query_cache_1.isCacheableMcpTool)(toolName);
|
|
2406
2462
|
let cacheKey;
|
|
2407
2463
|
let cacheQueries;
|
|
@@ -2423,7 +2479,7 @@ class ToolHandler {
|
|
|
2423
2479
|
const cached = cacheIndex.getEntry(cacheQueries, cacheKey);
|
|
2424
2480
|
const packMeta = cached?._meta?.homegraphEvidencePacks;
|
|
2425
2481
|
const cachedFiles = cached?._meta?.homegraphEvidence?.files;
|
|
2426
|
-
if (cached && (!packMeta || (packMeta.status === 'complete'
|
|
2482
|
+
if (cached && (!packMeta || (packMeta.status === 'complete' && !packMeta.implementationContext?.length
|
|
2427
2483
|
&& this.areEvidenceFilesCurrent(cachedFiles ?? [], cacheCg.getProjectRoot())))) {
|
|
2428
2484
|
const diagnosed = this.withQueryPlanDiagnostics(cached, args, true);
|
|
2429
2485
|
const withWorktree = this.withWorktreeNotice(diagnosed, projectPath);
|
|
@@ -3920,7 +3976,7 @@ class ToolHandler {
|
|
|
3920
3976
|
'',
|
|
3921
3977
|
...notes,
|
|
3922
3978
|
'',
|
|
3923
|
-
'> These sites choose their call target at runtime (registry / bus / reflection) — the site shown IS where the flow continues. To follow it, run homegraph_explore
|
|
3979
|
+
'> These sites choose their call target at runtime (registry / bus / reflection) — the site shown IS where the flow continues. To follow it, run homegraph_explore on a candidate; source for the sites above is included below.',
|
|
3924
3980
|
'',
|
|
3925
3981
|
].join('\n');
|
|
3926
3982
|
}
|
|
@@ -5161,7 +5217,7 @@ class ToolHandler {
|
|
|
5161
5217
|
else {
|
|
5162
5218
|
lines.push('> **Partial locator** — related writers / sibling Managers above are anchors, not a full answer. ' +
|
|
5163
5219
|
'ONE tighter explore with a named Type, or ONE narrow Grep for `@ohos` if still missing — ' +
|
|
5164
|
-
'do **not** fan out
|
|
5220
|
+
'do **not** fan out callers. Do **not** ANSWER NOW from local cache helpers.');
|
|
5165
5221
|
}
|
|
5166
5222
|
lines.push('');
|
|
5167
5223
|
return { section: lines.join('\n'), edgeCount, sdkImportCount, strongCount };
|
|
@@ -5266,7 +5322,7 @@ class ToolHandler {
|
|
|
5266
5322
|
'**Event types → Manager / handler sites**',
|
|
5267
5323
|
'',
|
|
5268
5324
|
'> **Partial locator** — no event Type/enum exports indexed for the named anchors. '
|
|
5269
|
-
+ '
|
|
5325
|
+
+ 'ONE explore on the Event Type file, or ONE Grep for enum members / '
|
|
5270
5326
|
+ '`case` branches — not a repo-wide storm. Do **not** ANSWER NOW.',
|
|
5271
5327
|
'',
|
|
5272
5328
|
].join('\n'),
|
|
@@ -5507,7 +5563,7 @@ class ToolHandler {
|
|
|
5507
5563
|
lines.push('- No Manager import/use sites indexed for these event types.');
|
|
5508
5564
|
lines.push('');
|
|
5509
5565
|
lines.push('> **Partial locator** — event anchors above only. '
|
|
5510
|
-
+ '
|
|
5566
|
+
+ 'ONE Grep for handler/`case` branches — do **not** ANSWER NOW as a full dispatch map.');
|
|
5511
5567
|
}
|
|
5512
5568
|
else {
|
|
5513
5569
|
for (const h of ranked.slice(0, 24)) {
|
|
@@ -5533,7 +5589,7 @@ class ToolHandler {
|
|
|
5533
5589
|
}
|
|
5534
5590
|
else {
|
|
5535
5591
|
lines.push('> **Partial locator** — handler files are anchors, but enum-member coverage may be incomplete. '
|
|
5536
|
-
+ '
|
|
5592
|
+
+ 'ONE Grep for members or `case` — do **not** repo-storm; do **not** ANSWER NOW as a full map.');
|
|
5537
5593
|
}
|
|
5538
5594
|
}
|
|
5539
5595
|
lines.push('');
|
|
@@ -5687,13 +5743,13 @@ class ToolHandler {
|
|
|
5687
5743
|
const hasExtends = extendsHits.length > 0;
|
|
5688
5744
|
if (!hasGraph && !hasExtends) {
|
|
5689
5745
|
lines.push('> **Partial locator** — Type files above are anchors. '
|
|
5690
|
-
+ '`
|
|
5746
|
+
+ '`homegraph_callees` one Type at a time; do **not** ANSWER NOW as a full graph.');
|
|
5691
5747
|
}
|
|
5692
5748
|
else {
|
|
5693
5749
|
lines.push('> Multi-Type dependency inventory — **ANSWER NOW** from locations'
|
|
5694
5750
|
+ (hasExtends ? ' + `extends` bases' : '')
|
|
5695
5751
|
+ (hasGraph ? ' + import/caller edges' : '')
|
|
5696
|
-
+ ' above;
|
|
5752
|
+
+ ' above; one tighter explore if a body detail is still missing.');
|
|
5697
5753
|
}
|
|
5698
5754
|
lines.push('');
|
|
5699
5755
|
return { section: lines.join('\n'), hitCount: locs.length + edges.length + extendsHits.length };
|
|
@@ -5744,7 +5800,7 @@ class ToolHandler {
|
|
|
5744
5800
|
...prior, version: plan.version, source: plan.source, intent: plan.intent, route: plan.route,
|
|
5745
5801
|
confidence: plan.confidence,
|
|
5746
5802
|
plannerSeeds: { anchors: plan.anchors, searchTerms: plan.searchTerms, literalTexts: plan.literalTexts,
|
|
5747
|
-
sourceScope: plan.sourceScope, relation: plan.relation },
|
|
5803
|
+
sourceScope: plan.sourceScope, relation: plan.relation, requestContract: plan.requestContract },
|
|
5748
5804
|
hasTaskContext: !!plan.taskContext,
|
|
5749
5805
|
matchedFeatures: Object.entries(plan.features).filter(([, matched]) => matched).map(([name]) => name).slice(0, 12),
|
|
5750
5806
|
planningMs: plan.telemetry.durationMs, durationMs: Math.max(plan.telemetry.durationMs, Date.now() - started),
|
|
@@ -5769,6 +5825,10 @@ class ToolHandler {
|
|
|
5769
5825
|
if (plan.route === 'usages' || plan.route === 'modules' || plan.route === 'native') {
|
|
5770
5826
|
return this.runSpecializedExploreRoute(plan.route, cg, query, root, plan);
|
|
5771
5827
|
}
|
|
5828
|
+
// Constrained ArkTS retrieval needs the full, verified-source renderer.
|
|
5829
|
+
if (plan.requestContract && ((0, request_contract_1.accuracyTargetsEnabled)() || (0, request_contract_1.accuracyCoverageEnabled)())
|
|
5830
|
+
&& plan.sourceScope !== 'sdk' && cg.getFiles().some(f => /\.ets$/i.test(f.path)))
|
|
5831
|
+
return null;
|
|
5772
5832
|
// These legacy paths re-extract seeds from text and cannot consume bound
|
|
5773
5833
|
// node identity / step hints. Model general/flow plans use full explore;
|
|
5774
5834
|
// rule/default and specialized routes retain their existing fast behavior.
|
|
@@ -6269,9 +6329,9 @@ class ToolHandler {
|
|
|
6269
6329
|
soft
|
|
6270
6330
|
? `Named-Type dependency inventory — **${multi.hitCount}** locator hit(s). `
|
|
6271
6331
|
+ '**Partial locator** — locations above; ONE tighter explore with a named Type, '
|
|
6272
|
-
+ 'or ONE narrow Grep — do **not** fan out
|
|
6332
|
+
+ 'or ONE narrow Grep — do **not** fan out callees.'
|
|
6273
6333
|
: `Named-Type dependency inventory — **${multi.hitCount}** locator hit(s). `
|
|
6274
|
-
+ '**ANSWER NOW** from locations + cross links; one
|
|
6334
|
+
+ '**ANSWER NOW** from locations + cross links; one tighter explore only if a body detail is still missing.',
|
|
6275
6335
|
'',
|
|
6276
6336
|
multi.section,
|
|
6277
6337
|
].join('\n'));
|
|
@@ -6629,20 +6689,58 @@ class ToolHandler {
|
|
|
6629
6689
|
if (!nodes.some(n => /\.ets$/i.test(n.filePath) && !n.filePath.startsWith('ohos-sdk:')))
|
|
6630
6690
|
return null;
|
|
6631
6691
|
const budget = getExploreOutputBudget(cg.getStats().fileCount);
|
|
6632
|
-
const
|
|
6692
|
+
const constrained = !!plan?.requestContract && ((0, request_contract_1.accuracyTargetsEnabled)() || (0, request_contract_1.accuracyCoverageEnabled)());
|
|
6693
|
+
const queryPaths = (!constrained || plan?.intent === 'flow' || !!plan?.bindings?.length) && process.env.HOMEGRAPH_ARKTS_QUERY_PATHS !== '0';
|
|
6633
6694
|
if (queryPaths)
|
|
6634
6695
|
nodes = (0, evidence_paths_1.completeEvidencePathCandidates)(query, nodes, (name, limit) => cg.getQueryBuilder().getNodesByQualifiedNameExact(name, limit), plan);
|
|
6696
|
+
if ((0, implementation_context_1.implementationContextEnabled)()) {
|
|
6697
|
+
const boundIds = new Set(focusIds);
|
|
6698
|
+
nodes = nodes.map(n => {
|
|
6699
|
+
if (n.kind !== 'struct')
|
|
6700
|
+
return n;
|
|
6701
|
+
const component = cg.getNodesInFile(n.filePath).find(c => c.kind === 'component' && c.name === n.name && c.startLine === n.startLine);
|
|
6702
|
+
if (component && boundIds.has(n.id))
|
|
6703
|
+
boundIds.add(component.id);
|
|
6704
|
+
return component ?? n;
|
|
6705
|
+
});
|
|
6706
|
+
focusIds = boundIds;
|
|
6707
|
+
}
|
|
6635
6708
|
const pathSearch = !queryPaths ? undefined : (0, evidence_paths_1.searchEvidencePaths)({
|
|
6636
6709
|
getNode: id => cg.getNode(id),
|
|
6637
|
-
getEdges: (id, direction, kinds, limit, preferred) =>
|
|
6710
|
+
getEdges: (id, direction, kinds, limit, preferred) => {
|
|
6711
|
+
const read = (nodeId) => cg.getQueryBuilder().getEvidenceEdges(nodeId, direction, kinds, limit, preferred);
|
|
6712
|
+
if (!(0, implementation_context_1.implementationContextEnabled)())
|
|
6713
|
+
return read(id);
|
|
6714
|
+
const owner = cg.getNode(id);
|
|
6715
|
+
const aliases = owner && ['struct', 'component'].includes(owner.kind)
|
|
6716
|
+
? cg.getNodesInFile(owner.filePath).filter(n => ['struct', 'component'].includes(n.kind) && n.name === owner.name
|
|
6717
|
+
&& n.startLine === owner.startLine && n.endLine === owner.endLine).slice(0, 2) : [];
|
|
6718
|
+
const canonical = (nodeId) => {
|
|
6719
|
+
const n = cg.getNode(nodeId);
|
|
6720
|
+
if (!n || n.kind !== 'struct')
|
|
6721
|
+
return nodeId;
|
|
6722
|
+
return cg.getNodesInFile(n.filePath).find(c => c.kind === 'component' && c.name === n.name
|
|
6723
|
+
&& c.startLine === n.startLine && c.endLine === n.endLine)?.id ?? nodeId;
|
|
6724
|
+
};
|
|
6725
|
+
// ArkAnalyzer stores struct/component roles for the same declaration. Project
|
|
6726
|
+
// their ownership edges to one identity; this is not a new runtime call.
|
|
6727
|
+
const edges = (aliases.length ? aliases.flatMap(n => read(n.id)) : read(id)).map(e => ({ ...e,
|
|
6728
|
+
source: canonical(e.source), target: canonical(e.target) })).filter(e => e.source !== e.target);
|
|
6729
|
+
return [...new Map(edges.map(e => [JSON.stringify([e.source, e.target, e.kind, e.metadata]), e])).values()].slice(0, limit);
|
|
6730
|
+
},
|
|
6638
6731
|
}, (0, evidence_paths_1.resolveEvidencePathGoal)(query, nodes, focusIds, plan));
|
|
6639
6732
|
const result = (0, arkts_evidence_packs_1.buildArktsEvidencePacks)(cg, { projectRoot, query, nodes, focusIds,
|
|
6640
|
-
maxChars: Math.min(budget.maxOutputChars, maxChars ?? budget.maxOutputChars), maxFiles, pathSearch
|
|
6733
|
+
maxChars: Math.min(budget.maxOutputChars, maxChars ?? budget.maxOutputChars), maxFiles, pathSearch, requestContract: plan?.requestContract,
|
|
6734
|
+
sdkModule: cg.getGraphSources() === 'project' || cg.getGraphSources() === 'none' ? undefined : module => {
|
|
6735
|
+
const nodes = cg.getQueryBuilder().getOhosApiModuleNodes(module);
|
|
6736
|
+
return nodes.length ? { nodes, version: cg.getOhosApiBinding()?.version ?? 'unknown' } : undefined;
|
|
6737
|
+
} });
|
|
6641
6738
|
if (!result)
|
|
6642
6739
|
return null;
|
|
6740
|
+
const audit = (0, evidence_audit_1.recordEvidencePack)(projectRoot, { query, requestContract: plan?.requestContract }, result);
|
|
6643
6741
|
// The pack renderer already supplies neutral guidance and exact source bytes.
|
|
6644
6742
|
return { content: [{ type: 'text', text: result.text }], [explore_session_state_1.EXPLORE_EMISSION_KEY]: result.emission,
|
|
6645
|
-
_meta: { homegraphEvidencePacks: result.metadata } };
|
|
6743
|
+
_meta: { homegraphEvidencePacks: result.metadata, ...(audit !== 'disabled' ? { homegraphEvidenceAudit: audit } : {}) } };
|
|
6646
6744
|
}
|
|
6647
6745
|
/** Render a compact symbol-bounded slice on the legacy mechanism path. */
|
|
6648
6746
|
renderLightMechanismSource(projectRoot, filePath, nodes, maxChars) {
|
|
@@ -6876,7 +6974,7 @@ class ToolHandler {
|
|
|
6876
6974
|
: '> **Partial locator** — Manager / domain inventory below are anchors, not a full mechanism closure. '
|
|
6877
6975
|
+ 'Pick **ONE** concrete `*Manager` / next-hop name from the list for a tighter explore — '
|
|
6878
6976
|
+ 'do **not** re-explore a paraphrase of the same keyword bag. '
|
|
6879
|
-
+ '
|
|
6977
|
+
+ 'Prefer ONE narrow Grep for unlisted wiring — not a repo storm. '
|
|
6880
6978
|
+ 'Do **not** ANSWER NOW yet.',
|
|
6881
6979
|
'',
|
|
6882
6980
|
`Mechanism anchors: **${seedIds.size}** symbol(s) — lightweight explore (seed + flow spine).`,
|
|
@@ -7099,7 +7197,7 @@ class ToolHandler {
|
|
|
7099
7197
|
lines.push('---');
|
|
7100
7198
|
if (mechanismClosed || (hasFlowPath && topIsStrong)) {
|
|
7101
7199
|
lines.push('> **Mechanism explore complete — ANSWER NOW** from the Manager inventory + spine + digests above. '
|
|
7102
|
-
+ 'Do **not**
|
|
7200
|
+
+ 'Do **not** Read or Grep for symbols/files already shown. '
|
|
7103
7201
|
+ 'Only one tighter `homegraph_explore` if a **named** Type essential to the answer is still missing.');
|
|
7104
7202
|
}
|
|
7105
7203
|
else if (softClose) {
|
|
@@ -8260,7 +8358,7 @@ class ToolHandler {
|
|
|
8260
8358
|
}
|
|
8261
8359
|
lines.push('---');
|
|
8262
8360
|
lines.push('> **Compact local explore complete — ANSWER NOW.** ' +
|
|
8263
|
-
'Do **not** Read/Grep/`homegraph_search`/`homegraph_explore
|
|
8361
|
+
'Do **not** Read/Grep/`homegraph_search`/`homegraph_explore` for the same symbols.');
|
|
8264
8362
|
lines.push('');
|
|
8265
8363
|
return this.textResult(lines.join('\n'));
|
|
8266
8364
|
}
|
|
@@ -9988,14 +10086,9 @@ class ToolHandler {
|
|
|
9988
10086
|
const sessionPrior = (0, explore_session_state_1.viewForProject)((0, explore_session_state_1.readExploreSessionView)(args), projectRoot);
|
|
9989
10087
|
const repeat = (0, explore_repeat_guard_1.decideExploreRepeat)(sessionPrior, query);
|
|
9990
10088
|
if (!plan && this.shouldRefuseRepeatedEvidence(repeat, projectRoot)) {
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
query,
|
|
9995
|
-
files: [],
|
|
9996
|
-
sourceBytes: 0,
|
|
9997
|
-
responseBytes: text.length,
|
|
9998
|
-
});
|
|
10089
|
+
// Do not record this refusal. A recorded return would restart the cooldown
|
|
10090
|
+
// and a client retrying inside the window would never get through.
|
|
10091
|
+
return this.textResult((0, explore_repeat_guard_1.formatExploreRepeatRefuse)(repeat, query));
|
|
9999
10092
|
}
|
|
10000
10093
|
// A planned step already tried fast paths once, before entering full explore.
|
|
10001
10094
|
const compactLocal = plan ? null : this.tryFastInventoryExplore(cg, query, projectRoot)
|
|
@@ -10034,7 +10127,7 @@ class ToolHandler {
|
|
|
10034
10127
|
: locateAnchors.length === 1
|
|
10035
10128
|
? `${locateAnchors[0]} ${query}`
|
|
10036
10129
|
: query;
|
|
10037
|
-
const wantHints = !!(plan && (plan.source === 'llm' || plan.literalTexts?.length || plan.anchors?.length))
|
|
10130
|
+
const wantHints = !!(plan && (plan.source === 'llm' || plan.literalTexts?.length || plan.anchors?.length || plan.requestContract))
|
|
10038
10131
|
|| exploreSourceScope !== 'all';
|
|
10039
10132
|
const subgraph = await cg.findRelevantContext(contextQuery, {
|
|
10040
10133
|
...contextOpts,
|
|
@@ -10043,7 +10136,8 @@ class ToolHandler {
|
|
|
10043
10136
|
...(plan?.anchors ?? []).filter((anchor) => !(plan?.bindings ?? []).some((node) => anchor === node.name || anchor === node.qualifiedName)),
|
|
10044
10137
|
...locateAnchors,
|
|
10045
10138
|
].slice(0, 16),
|
|
10046
|
-
searchTerms: plan?.searchTerms ?? [], literalTexts: plan?.literalTexts,
|
|
10139
|
+
searchTerms: plan?.searchTerms ?? [], literalTexts: [...new Set([...(plan?.literalTexts ?? []),
|
|
10140
|
+
...((0, request_contract_1.accuracyTargetsEnabled)() && !plan?.bindings?.length ? (0, request_contract_1.contractLiteralTexts)(plan?.requestContract) : [])])].slice(0, 8),
|
|
10047
10141
|
sourceScope: exploreSourceScope,
|
|
10048
10142
|
nodeIds: (plan?.bindings ?? []).map((node) => node.id),
|
|
10049
10143
|
} } : {}),
|
|
@@ -10078,6 +10172,26 @@ class ToolHandler {
|
|
|
10078
10172
|
}
|
|
10079
10173
|
}
|
|
10080
10174
|
}
|
|
10175
|
+
if (plan?.requestContract && ((0, request_contract_1.accuracyTargetsEnabled)() || (0, request_contract_1.accuracyCoverageEnabled)()) && plan.sourceScope !== 'sdk'
|
|
10176
|
+
// Resource values need the existing resource + reference renderer; a bare
|
|
10177
|
+
// declaration would silently drop the value-to-key witness.
|
|
10178
|
+
&& !subgraph.literalEvidence?.hits.some(hit => hit.kind === 'resource_reference')) {
|
|
10179
|
+
// One existing retrieval pass; raw literal hits affect candidate order only.
|
|
10180
|
+
// Actual target evidence is checked against hash-verified source in the packer.
|
|
10181
|
+
const hits = subgraph.literalEvidence?.hits ?? [];
|
|
10182
|
+
const fileHits = new Map();
|
|
10183
|
+
for (const hit of hits) {
|
|
10184
|
+
const labels = fileHits.get(hit.filePath) ?? new Set();
|
|
10185
|
+
labels.add(hit.literal);
|
|
10186
|
+
fileHits.set(hit.filePath, labels);
|
|
10187
|
+
}
|
|
10188
|
+
const candidates = [...subgraph.nodes.values()];
|
|
10189
|
+
if ((0, request_contract_1.accuracyTargetsEnabled)())
|
|
10190
|
+
candidates.sort((a, b) => (fileHits.get(b.filePath)?.size ?? 0) - (fileHits.get(a.filePath)?.size ?? 0));
|
|
10191
|
+
const packed = this.tryArktsEvidenceExplore(cg, query, projectRoot, candidates, new Set(plan.bindings?.length ? plan.bindings.map(b => b.id) : subgraph.roots), args._hgEvidenceMaxChars, maxFiles, plan);
|
|
10192
|
+
if (packed)
|
|
10193
|
+
return packed;
|
|
10194
|
+
}
|
|
10081
10195
|
const literalSource = this.renderLiteralSource(cg, subgraph);
|
|
10082
10196
|
if (subgraph.nodes.size === 0) {
|
|
10083
10197
|
// Spec 0044 §9: empty exact evidence → Miss (paths only), not a source dump.
|
|
@@ -11166,9 +11280,9 @@ class ToolHandler {
|
|
|
11166
11280
|
lines.push(exploreLocatorDigestEnabled()
|
|
11167
11281
|
? '> Spine / top-hit digests — treat as already Read. '
|
|
11168
11282
|
+ 'Answer from Flow + Anchors + digests when you can; ONE narrow Grep for residual unindexed wiring. '
|
|
11169
|
-
+ 'Do not re-dump these files via Read
|
|
11283
|
+
+ 'Do not re-dump these files via Read.'
|
|
11170
11284
|
: '> Line-numbered source — treat as already Read. Answer from Flow + Source below when you can; ' +
|
|
11171
|
-
'do not re-read/grep these files, and do not fan out
|
|
11285
|
+
'do not re-read/grep these files, and do not fan out search for symbols already shown. ' +
|
|
11172
11286
|
'Another `homegraph_explore` with tighter names only if a needed symbol is missing.');
|
|
11173
11287
|
lines.push('');
|
|
11174
11288
|
for (const [filePath, group] of filesToRender) {
|
|
@@ -11887,16 +12001,15 @@ class ToolHandler {
|
|
|
11887
12001
|
}
|
|
11888
12002
|
else if (anyFileTrimmed) {
|
|
11889
12003
|
lines.push('');
|
|
11890
|
-
lines.push(`> Some file sections were trimmed for size. For a specific symbol you still need, run another \`homegraph_explore\`
|
|
12004
|
+
lines.push(`> Some file sections were trimmed for size. For a specific symbol you still need, run another \`homegraph_explore\` with its exact name — line-numbered source, cheaper and more complete than Read.`);
|
|
11891
12005
|
}
|
|
11892
12006
|
// Add explore budget note based on project size
|
|
11893
12007
|
if (budget.includeBudgetNote) {
|
|
11894
12008
|
try {
|
|
11895
12009
|
const stats = cg.getStats();
|
|
11896
|
-
const callBudget = getExploreBudget(stats.fileCount);
|
|
11897
12010
|
lines.push('');
|
|
11898
|
-
lines.push(`> **Usually one explore is enough** (
|
|
11899
|
-
'Answer from Flow + Source above. Extra explore
|
|
12011
|
+
lines.push(`> **Usually one explore is enough** (wait ${(0, explore_repeat_guard_1.exploreCooldownSeconds)()}s after this result before another \`homegraph_explore\`; ${stats.fileCount.toLocaleString()} files). ` +
|
|
12012
|
+
'Answer from Flow + Source above. Extra explore/Read for the **same** symbols multiplies tokens — ' +
|
|
11900
12013
|
'only call again for a missing **named** symbol.');
|
|
11901
12014
|
}
|
|
11902
12015
|
catch {
|
|
@@ -11974,7 +12087,7 @@ class ToolHandler {
|
|
|
11974
12087
|
if (!finalText.includes('**Partial result**') && !finalText.includes('> **Explore complete**')) {
|
|
11975
12088
|
finalText +=
|
|
11976
12089
|
'\n\n> **Explore complete — ANSWER NOW.** Flow + Source above are authoritative for this query. ' +
|
|
11977
|
-
'Do **not** grep/read/`
|
|
12090
|
+
'Do **not** grep/read/`homegraph_search` for the same symbols or files (multiplies tokens). ' +
|
|
11978
12091
|
'Only call one tighter `homegraph_explore` if a named symbol essential to the answer is missing.';
|
|
11979
12092
|
}
|
|
11980
12093
|
// Curated header (#1046): substitute the sentinel with the count of files
|
|
@@ -12047,7 +12160,10 @@ class ToolHandler {
|
|
|
12047
12160
|
const nodes = [];
|
|
12048
12161
|
const seen = new Set();
|
|
12049
12162
|
let chars = 0;
|
|
12050
|
-
|
|
12163
|
+
// Preserve value-to-key provenance when a plain label in the same file
|
|
12164
|
+
// would otherwise consume its single source-witness slot first.
|
|
12165
|
+
const literalHits = [...(subgraph.literalEvidence?.hits ?? [])].sort((a, b) => Number(!!b.resource) - Number(!!a.resource));
|
|
12166
|
+
for (const hit of literalHits) {
|
|
12051
12167
|
if (files.length >= 2 || seen.has(hit.filePath))
|
|
12052
12168
|
continue;
|
|
12053
12169
|
const absolute = (0, utils_1.validatePathWithinRoot)(cg.getProjectRoot(), hit.filePath);
|