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.
Files changed (59) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +64 -14
  3. package/dist/bin/homegraph.js +2 -2
  4. package/dist/db/index.js +1 -0
  5. package/dist/db/migrations.d.ts +6 -1
  6. package/dist/db/migrations.js +26 -1
  7. package/dist/db/queries.d.ts +9 -0
  8. package/dist/db/queries.js +32 -7
  9. package/dist/db/schema.sql +4 -0
  10. package/dist/extraction/grammars.js +2 -1
  11. package/dist/extraction/languages/c-cpp.d.ts +8 -0
  12. package/dist/extraction/languages/c-cpp.js +44 -0
  13. package/dist/extraction/wasm-runtime-flags.d.ts +4 -0
  14. package/dist/extraction/wasm-runtime-flags.js +9 -0
  15. package/dist/graph/evidence-paths.d.ts +2 -0
  16. package/dist/graph/evidence-paths.js +20 -3
  17. package/dist/index.js +7 -1
  18. package/dist/installer/targets/icode.d.ts +43 -0
  19. package/dist/installer/targets/icode.js +631 -0
  20. package/dist/installer/targets/registry.js +2 -0
  21. package/dist/installer/targets/types.d.ts +1 -1
  22. package/dist/mcp/arkts-evidence-packs.d.ts +17 -0
  23. package/dist/mcp/arkts-evidence-packs.js +218 -10
  24. package/dist/mcp/evidence-audit.d.ts +3 -0
  25. package/dist/mcp/evidence-audit.js +40 -0
  26. package/dist/mcp/evidence-rendering.js +1 -1
  27. package/dist/mcp/explore-repeat-guard.d.ts +19 -5
  28. package/dist/mcp/explore-repeat-guard.js +35 -14
  29. package/dist/mcp/explore-session-state.d.ts +5 -0
  30. package/dist/mcp/explore-session-state.js +1 -0
  31. package/dist/mcp/implementation-context.d.ts +35 -0
  32. package/dist/mcp/implementation-context.js +232 -0
  33. package/dist/mcp/locate-contract.js +1 -1
  34. package/dist/mcp/query-cache.d.ts +1 -15
  35. package/dist/mcp/query-cache.js +20 -11
  36. package/dist/mcp/request-evidence.d.ts +69 -0
  37. package/dist/mcp/request-evidence.js +297 -0
  38. package/dist/mcp/server-instructions.d.ts +2 -2
  39. package/dist/mcp/server-instructions.js +15 -15
  40. package/dist/mcp/session.js +3 -2
  41. package/dist/mcp/tools.d.ts +39 -12
  42. package/dist/mcp/tools.js +237 -121
  43. package/dist/resolution/c-feature-variants.d.ts +40 -0
  44. package/dist/resolution/c-feature-variants.js +272 -0
  45. package/dist/resolution/cpp-macro-visibility.d.ts +21 -0
  46. package/dist/resolution/cpp-macro-visibility.js +393 -0
  47. package/dist/resolution/index.js +11 -0
  48. package/dist/resolution/name-matcher.js +49 -2
  49. package/dist/resolution/types.d.ts +8 -0
  50. package/dist/resolution/types.js +8 -0
  51. package/dist/search/literal-evidence.js +11 -3
  52. package/dist/search/query-plan-provider.js +13 -5
  53. package/dist/search/query-plan.d.ts +3 -1
  54. package/dist/search/query-plan.js +11 -4
  55. package/dist/search/request-contract.d.ts +29 -0
  56. package/dist/search/request-contract.js +76 -0
  57. package/dist/sync/watcher.d.ts +30 -0
  58. package/dist/sync/watcher.js +45 -0
  59. 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
- * Common projectPath property for cross-project queries
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: '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).',
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 tools
928
+ * All HomeGraph MCP tool definitions (handlers stay registered).
913
929
  *
914
- * Prefer the smallest tool that answers: callers/node for one named symbol,
915
- * explore for multi-file flows. Skip HomeGraph entirely for topic file-lists,
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: 'Optional graph evidence for a concrete unresolved cross-symbol mechanism in THIS repo. Required: `query`. ' +
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
- 'Use ordinary bash/search/read for paths, symbols, literal strings and local changes; continue editing when that evidence suffices. ' +
1187
- 'Do not call for routine pre-edit orientation or merely because implementation is difficult. ' +
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
- 'Partial/busy → at most one focused recovery for the named gap; budgets are ceilings, not required calls. ' +
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
- 'use homegraph_usages for a narrow import/usage inventory, not SDK catalogs.',
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 `.codegraph/` indexes live only in sub-projects
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, or already requires it, is returned untouched;
1408
- * explore's `['query']` becomes `['query', 'projectPath']`, and a tool with no
1409
- * `required` list (status/files) gains `['projectPath']`.
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
- if (!tool.inputSchema.properties.projectPath)
1432
+ const prop = tool.inputSchema.properties.projectPath;
1433
+ if (!prop)
1414
1434
  return tool;
1415
1435
  const required = tool.inputSchema.required ?? [];
1416
- if (required.includes('projectPath'))
1417
- return tool;
1436
+ const nextRequired = required.includes('projectPath')
1437
+ ? required
1438
+ : [...required, 'projectPath'];
1418
1439
  return {
1419
1440
  ...tool,
1420
- inputSchema: { ...tool.inputSchema, required: [...required, 'projectPath'] },
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
- const raw = process.env.HOMEGRAPH_MCP_TOOLS ?? process.env.HOMEGRAPH_MCP_TOOLS;
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 the HOMEGRAPH_MCP_TOOLS
1701
- * env var (comma-separated short names, e.g. "explore,search,node").
1702
- * Unset/empty → every tool is exposed. Set → only the listed tools are
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
- const raw = process.env.HOMEGRAPH_MCP_TOOLS ?? process.env.HOMEGRAPH_MCP_TOOLS;
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 (if any). */
1787
+ /** Whether a tool name passes the HOMEGRAPH_MCP_TOOLS allowlist. */
1717
1788
  isToolAllowed(name) {
1718
1789
  const allow = this.toolAllowlist();
1719
- return !allow || allow.has(name.replace(/^homegraph_/, ''));
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
- // No explicit allowlist → expose every defined tool. An allowlist trims
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: on projects under TINY_REPO_FILE_THRESHOLD
1749
- // files, only expose the core trio (search, node, explore) — one
1750
- // below even the 4-tool default: at this scale callers, too, reduces
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
- // - 3-tool gate (search + context + trace): cost regressed on
1756
- // cobra/ky/sinatra. The agent fell back to raw Reads to cover
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
- // An explicit host selection overrides the default size-based surface.
1780
- // Otherwise small ArkTS repos silently lose requested specialized tools.
1781
- if (!allow && stats.fileCount < TINY_REPO_FILE_THRESHOLD) {
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 a
1812
- * call budget — there is nothing to budget over an unbuilt index.
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 `Budget: make at most ${getExploreBudget(fileCount)} calls for this project (${fileCount.toLocaleString()} files indexed).`;
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 or homegraph_node on a candidate; source for the sites above is included below.',
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 `homegraph_node` / callers. Do **not** ANSWER NOW from local cache helpers.');
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
- + '`homegraph_node` / ONE explore on the Event Type file, or ONE Grep for enum members / '
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
- + '`homegraph_node` the Event file or ONE Grep for handler/`case` branches — do **not** ANSWER NOW as a full dispatch map.');
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
- + '`homegraph_node` the Event Type / ONE Grep for members or `case` — do **not** repo-storm; do **not** ANSWER NOW as a full map.');
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
- + '`homegraph_node` / `homegraph_callees` one Type at a time; do **not** ANSWER NOW as a full graph.');
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; only `homegraph_node` one Type if a body detail is still missing.');
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 `homegraph_node` / callees.'
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 `homegraph_node` only if a body detail is still missing.',
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 queryPaths = process.env.HOMEGRAPH_ARKTS_QUERY_PATHS !== '0';
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) => cg.getQueryBuilder().getEvidenceEdges(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
- + 'At most one `homegraph_node` after Partial; prefer ONE narrow Grep for unlisted wiring — not a repo storm. '
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** call `homegraph_node`, Read, or Grep for symbols/files already shown. '
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`/`homegraph_node` for the same symbols.');
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
- const text = (0, explore_repeat_guard_1.formatExploreRepeatRefuse)(repeat, query);
9992
- return this.exploreResult(text, {
9993
- projectRoot,
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/`homegraph_node`.'
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 `homegraph_node` / search for symbols already shown. ' +
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\` (or \`homegraph_node\`) with its exact name — line-numbered source, cheaper and more complete than Read.`);
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** (budget ≤${callBudget} for ${stats.fileCount.toLocaleString()} files). ` +
11899
- 'Answer from Flow + Source above. Extra explore/`homegraph_node`/Read for the **same** symbols multiplies tokens — ' +
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/`homegraph_node`/`homegraph_search` for the same symbols or files (multiplies tokens). ' +
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
- for (const hit of subgraph.literalEvidence?.hits ?? []) {
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);