homegraph 1.5.10 → 1.6.1

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 (53) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +64 -14
  3. package/dist/db/queries.d.ts +4 -0
  4. package/dist/db/queries.js +15 -0
  5. package/dist/extraction/grammars.d.ts +19 -0
  6. package/dist/extraction/grammars.js +44 -1
  7. package/dist/graph/evidence-paths.d.ts +2 -0
  8. package/dist/graph/evidence-paths.js +20 -3
  9. package/dist/mcp/arkts-evidence-packs.d.ts +17 -0
  10. package/dist/mcp/arkts-evidence-packs.js +219 -10
  11. package/dist/mcp/daemon.d.ts +21 -3
  12. package/dist/mcp/daemon.js +60 -6
  13. package/dist/mcp/engine.d.ts +26 -0
  14. package/dist/mcp/engine.js +143 -7
  15. package/dist/mcp/evidence-audit.d.ts +3 -0
  16. package/dist/mcp/evidence-audit.js +40 -0
  17. package/dist/mcp/implementation-context.d.ts +35 -0
  18. package/dist/mcp/implementation-context.js +232 -0
  19. package/dist/mcp/index-availability.d.ts +22 -0
  20. package/dist/mcp/index-availability.js +40 -1
  21. package/dist/mcp/index.js +9 -0
  22. package/dist/mcp/indexable-root.d.ts +22 -0
  23. package/dist/mcp/indexable-root.js +140 -0
  24. package/dist/mcp/liveness-watchdog.d.ts +6 -1
  25. package/dist/mcp/liveness-watchdog.js +17 -5
  26. package/dist/mcp/locate-contract.d.ts +50 -0
  27. package/dist/mcp/locate-contract.js +146 -0
  28. package/dist/mcp/query-cache.d.ts +1 -15
  29. package/dist/mcp/query-cache.js +20 -11
  30. package/dist/mcp/request-evidence.d.ts +69 -0
  31. package/dist/mcp/request-evidence.js +297 -0
  32. package/dist/mcp/server-instructions.d.ts +2 -2
  33. package/dist/mcp/server-instructions.js +12 -9
  34. package/dist/mcp/session.js +15 -0
  35. package/dist/mcp/tools.d.ts +67 -12
  36. package/dist/mcp/tools.js +841 -134
  37. package/dist/project-map/index.d.ts +45 -0
  38. package/dist/project-map/index.js +371 -3
  39. package/dist/resolution/callback-synthesizer.js +251 -0
  40. package/dist/resolution/frameworks/arkts-entry.d.ts +35 -6
  41. package/dist/resolution/frameworks/arkts-entry.js +513 -30
  42. package/dist/resolution/index.js +25 -21
  43. package/dist/runtime-log.d.ts +52 -0
  44. package/dist/runtime-log.js +199 -0
  45. package/dist/search/literal-evidence.js +11 -3
  46. package/dist/search/query-plan-provider.js +15 -6
  47. package/dist/search/query-plan.d.ts +3 -1
  48. package/dist/search/query-plan.js +25 -16
  49. package/dist/search/query-utils.d.ts +13 -0
  50. package/dist/search/query-utils.js +64 -0
  51. package/dist/search/request-contract.d.ts +29 -0
  52. package/dist/search/request-contract.js +76 -0
  53. package/package.json +1 -1
package/dist/mcp/index.js CHANGED
@@ -73,6 +73,7 @@ const fs = __importStar(require("fs"));
73
73
  const path = __importStar(require("path"));
74
74
  const child_process_1 = require("child_process");
75
75
  const directory_1 = require("../directory");
76
+ const runtime_log_1 = require("../runtime-log");
76
77
  const transport_1 = require("./transport");
77
78
  const engine_1 = require("./engine");
78
79
  const session_1 = require("./session");
@@ -298,6 +299,7 @@ class MCPServer {
298
299
  // Runs until the host disconnects; the proxy installs its own watchdog and
299
300
  // falls back to an in-process engine if the daemon never comes up.
300
301
  this.mode = 'proxy';
302
+ (0, runtime_log_1.logLifecycle)('mcp.start', { projectRoot: root, mode: 'proxy' });
301
303
  await this.runProxyWithLocalHandshake(root);
302
304
  return;
303
305
  }
@@ -306,6 +308,7 @@ class MCPServer {
306
308
  // is still safe to recover from with a direct-mode session.
307
309
  const msg = err instanceof Error ? err.message : String(err);
308
310
  process.stderr.write(`[HomeGraph MCP] Proxy path failed (${msg}); falling back to direct mode.\n`);
311
+ (0, runtime_log_1.logLifecycleError)('mcp.proxy.fail', { projectRoot: root, msg });
309
312
  return this.startDirect('proxy path threw');
310
313
  }
311
314
  }
@@ -346,6 +349,11 @@ class MCPServer {
346
349
  if (reason && process.env.HOMEGRAPH_MCP_DEBUG) {
347
350
  process.stderr.write(`[HomeGraph MCP] Direct mode: ${reason}.\n`);
348
351
  }
352
+ (0, runtime_log_1.logLifecycle)('mcp.start', {
353
+ projectRoot: this.projectPath ?? undefined,
354
+ mode: 'direct',
355
+ reason,
356
+ });
349
357
  // Direct mode = one client. Do NOT start a query-pool worker: each worker
350
358
  // is a second V8 isolate + a second open of the project DB, and on large
351
359
  // indexes (hundreds of MB) that alone pushed process-tree RSS to ~5GB.
@@ -392,6 +400,7 @@ class MCPServer {
392
400
  */
393
401
  async startDaemonProcess() {
394
402
  const root = resolveDaemonRoot(this.projectPath) ?? this.projectPath ?? process.cwd();
403
+ (0, runtime_log_1.logLifecycle)('mcp.start', { projectRoot: root, mode: 'daemon' });
395
404
  for (let attempt = 0; attempt < TAKEOVER_MAX_RETRIES; attempt++) {
396
405
  const lock = (0, daemon_1.tryAcquireDaemonLock)(root);
397
406
  if (lock.kind === 'acquired') {
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Cheap “is this root worth creating a HomeGraph index?” probe (Spec 0049).
3
+ *
4
+ * Used by MCP auto-init to avoid writing `.homegraph/` into empty workspaces
5
+ * (e.g. DevEco Code task dirs before `devecocli create`).
6
+ */
7
+ /**
8
+ * True when `root` already looks like a project HomeGraph should index.
9
+ * Short-circuits on root `build-profile.json5`, else bounded BFS for any
10
+ * {@link isSourceFile} hit. Ignores `.homegraph*` and common build/vendor dirs.
11
+ */
12
+ export declare function isIndexableRoot(rootDir: string, opts?: {
13
+ maxDirs?: number;
14
+ maxDepth?: number;
15
+ }): boolean;
16
+ /**
17
+ * Parse `HOMEGRAPH_DEFER_PROBE_MS`. Default **60000**.
18
+ * `0` disables the timer (tool-call kick only). Invalid → default.
19
+ * Clamp: 0, or 1000ms … 10min.
20
+ */
21
+ export declare function parseDeferProbeMs(raw: string | undefined): number;
22
+ //# sourceMappingURL=indexable-root.d.ts.map
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ /**
3
+ * Cheap “is this root worth creating a HomeGraph index?” probe (Spec 0049).
4
+ *
5
+ * Used by MCP auto-init to avoid writing `.homegraph/` into empty workspaces
6
+ * (e.g. DevEco Code task dirs before `devecocli create`).
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.isIndexableRoot = isIndexableRoot;
43
+ exports.parseDeferProbeMs = parseDeferProbeMs;
44
+ const fs = __importStar(require("fs"));
45
+ const path = __importStar(require("path"));
46
+ const grammars_1 = require("../extraction/grammars");
47
+ const directory_1 = require("../directory");
48
+ const HARMONY_PROFILE = 'build-profile.json5';
49
+ /** Dirs skipped during the bounded source walk (names only). */
50
+ const SKIP_DIR_NAMES = new Set([
51
+ 'node_modules',
52
+ 'oh_modules',
53
+ '.git',
54
+ '.hvigor',
55
+ '.preview',
56
+ '.cxx',
57
+ 'dist',
58
+ 'build',
59
+ 'out',
60
+ 'coverage',
61
+ 'vendor',
62
+ 'Pods',
63
+ 'DerivedData',
64
+ '__pycache__',
65
+ '.venv',
66
+ 'venv',
67
+ 'target',
68
+ '.gradle',
69
+ 'obj',
70
+ ]);
71
+ const DEFAULT_MAX_DIRS = 4000;
72
+ const DEFAULT_MAX_DEPTH = 8;
73
+ /**
74
+ * True when `root` already looks like a project HomeGraph should index.
75
+ * Short-circuits on root `build-profile.json5`, else bounded BFS for any
76
+ * {@link isSourceFile} hit. Ignores `.homegraph*` and common build/vendor dirs.
77
+ */
78
+ function isIndexableRoot(rootDir, opts) {
79
+ const root = path.resolve(rootDir);
80
+ let st;
81
+ try {
82
+ st = fs.statSync(root);
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ if (!st.isDirectory())
88
+ return false;
89
+ if (fs.existsSync(path.join(root, HARMONY_PROFILE)))
90
+ return true;
91
+ const maxDirs = opts?.maxDirs ?? DEFAULT_MAX_DIRS;
92
+ const maxDepth = opts?.maxDepth ?? DEFAULT_MAX_DEPTH;
93
+ const queue = [{ dir: root, depth: 0 }];
94
+ let dirsSeen = 0;
95
+ while (queue.length > 0 && dirsSeen < maxDirs) {
96
+ const { dir, depth } = queue.shift();
97
+ dirsSeen += 1;
98
+ let entries;
99
+ try {
100
+ entries = fs.readdirSync(dir, { withFileTypes: true });
101
+ }
102
+ catch {
103
+ continue;
104
+ }
105
+ for (const ent of entries) {
106
+ const name = ent.name;
107
+ if (name === '.' || name === '..')
108
+ continue;
109
+ if ((0, directory_1.isHomeGraphDataDir)(name) || SKIP_DIR_NAMES.has(name))
110
+ continue;
111
+ const full = path.join(dir, name);
112
+ if (ent.isDirectory()) {
113
+ if (depth + 1 <= maxDepth)
114
+ queue.push({ dir: full, depth: depth + 1 });
115
+ continue;
116
+ }
117
+ if (ent.isFile() && (0, grammars_1.isSourceFile)(full))
118
+ return true;
119
+ }
120
+ }
121
+ return false;
122
+ }
123
+ /**
124
+ * Parse `HOMEGRAPH_DEFER_PROBE_MS`. Default **60000**.
125
+ * `0` disables the timer (tool-call kick only). Invalid → default.
126
+ * Clamp: 0, or 1000ms … 10min.
127
+ */
128
+ function parseDeferProbeMs(raw) {
129
+ if (raw === undefined || !raw.trim())
130
+ return 60_000;
131
+ const n = Number(raw);
132
+ if (!Number.isFinite(n) || !Number.isInteger(n))
133
+ return 60_000;
134
+ if (n === 0)
135
+ return 0;
136
+ if (n < 1000 || n > 10 * 60 * 1000)
137
+ return 60_000;
138
+ return n;
139
+ }
140
+ //# sourceMappingURL=indexable-root.js.map
@@ -7,6 +7,11 @@ export declare const DEFAULT_WATCHDOG_TIMEOUT_MS = 60000;
7
7
  * beyond any legitimate statement). 10× the 60s default ⇒ 10 minutes.
8
8
  */
9
9
  export declare const PROGRESS_CAP_MULTIPLIER = 10;
10
+ /**
11
+ * Spec 0047: liveness watchdog is opt-in.
12
+ * Armed only when `HOMEGRAPH_WATCHDOG` is truthy and `HOMEGRAPH_NO_WATCHDOG` is not.
13
+ */
14
+ export declare function isLivenessWatchdogEnabled(env?: NodeJS.ProcessEnv): boolean;
10
15
  /** Parse the timeout env, falling back to the default for missing/invalid values. */
11
16
  export declare function parseWatchdogTimeoutMs(raw: string | undefined, fallback?: number): number;
12
17
  /** Derive a heartbeat cadence that emits several beats inside the timeout window. */
@@ -42,7 +47,7 @@ export declare function installMainThreadWatchdog(options?: WatchdogOptions): Wa
42
47
  * resume function. Nested calls refcount; a `stop()` while suspended cancels
43
48
  * the pending re-arm.
44
49
  *
45
- * No-op when nothing is armed (tests, `HOMEGRAPH_NO_WATCHDOG`, library embeds).
50
+ * No-op when nothing is armed (tests, default-off Spec 0047, `HOMEGRAPH_NO_WATCHDOG`, library embeds).
46
51
  */
47
52
  export declare function suspendLivenessWatchdog(): () => void;
48
53
  /**
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.PROGRESS_CAP_MULTIPLIER = exports.DEFAULT_WATCHDOG_TIMEOUT_MS = void 0;
37
+ exports.isLivenessWatchdogEnabled = isLivenessWatchdogEnabled;
37
38
  exports.parseWatchdogTimeoutMs = parseWatchdogTimeoutMs;
38
39
  exports.deriveCheckIntervalMs = deriveCheckIntervalMs;
39
40
  exports.installMainThreadWatchdog = installMainThreadWatchdog;
@@ -73,8 +74,9 @@ exports.runWithoutLivenessWatchdog = runWithoutLivenessWatchdog;
73
74
  * (off-thread) and the daemon's indexing shells out to a child process, so the
74
75
  * daemon's main thread only ever does fast, bounded work. The default timeout
75
76
  * is ~300× the 5h #850 wedge shorter, yet far longer than any legitimate
76
- * main-thread block. Opt out with `HOMEGRAPH_NO_WATCHDOG=1`; tune with
77
- * `HOMEGRAPH_WATCHDOG_TIMEOUT_MS`.
77
+ * main-thread block. Spec 0047: **opt in** with `HOMEGRAPH_WATCHDOG=1` (default
78
+ * off for product / DevEco concurrent init); `HOMEGRAPH_NO_WATCHDOG=1` still
79
+ * forces off. Tune with `HOMEGRAPH_WATCHDOG_TIMEOUT_MS`.
78
80
  *
79
81
  * **Disk-progress deferral (`progressPaths`).** The CLI `index`/`init` path is
80
82
  * different: it runs the SQLite store on this thread, and one long synchronous
@@ -118,6 +120,15 @@ function isEnvTruthy(raw) {
118
120
  return false;
119
121
  return ['1', 'true', 'yes', 'on'].includes(raw.trim().toLowerCase());
120
122
  }
123
+ /**
124
+ * Spec 0047: liveness watchdog is opt-in.
125
+ * Armed only when `HOMEGRAPH_WATCHDOG` is truthy and `HOMEGRAPH_NO_WATCHDOG` is not.
126
+ */
127
+ function isLivenessWatchdogEnabled(env = process.env) {
128
+ if (isEnvTruthy(env.HOMEGRAPH_NO_WATCHDOG))
129
+ return false;
130
+ return isEnvTruthy(env.HOMEGRAPH_WATCHDOG);
131
+ }
121
132
  /** Parse the timeout env, falling back to the default for missing/invalid values. */
122
133
  function parseWatchdogTimeoutMs(raw, fallback = exports.DEFAULT_WATCHDOG_TIMEOUT_MS) {
123
134
  if (raw === undefined)
@@ -153,7 +164,7 @@ const capMs = Number(process.argv[3]);
153
164
  const progressPaths = process.argv.slice(4);
154
165
  const secs = Math.round(timeoutMs / 1000);
155
166
  function kill(extra) {
156
- try { fs.writeSync(2, Buffer.from('[HomeGraph] Main thread unresponsive for ~' + secs + 's' + (extra || '') + ' — killing the wedged process so a fresh one can start (#850). Disable with HOMEGRAPH_NO_WATCHDOG=1.\\n')); } catch (e) {}
167
+ try { fs.writeSync(2, Buffer.from('[HomeGraph] Main thread unresponsive for ~' + secs + 's' + (extra || '') + ' — killing the wedged process so a fresh one can start (#850). Disable with HOMEGRAPH_NO_WATCHDOG=1 (default off; enable with HOMEGRAPH_WATCHDOG=1).\\n')); } catch (e) {}
157
168
  try { process.kill(parentPid, 'SIGKILL'); } catch (e) {}
158
169
  process.exit(0);
159
170
  }
@@ -318,7 +329,8 @@ function rearmFromOptions(options) {
318
329
  * current generation.
319
330
  */
320
331
  function installMainThreadWatchdog(options = {}) {
321
- if (isEnvTruthy(process.env.HOMEGRAPH_NO_WATCHDOG))
332
+ // Spec 0047: default off — set HOMEGRAPH_WATCHDOG=1 to arm.
333
+ if (!isLivenessWatchdogEnabled())
322
334
  return null;
323
335
  // Replace any prior generation (including a mid-suspend pending re-arm).
324
336
  suspendDepth = 0;
@@ -336,7 +348,7 @@ function installMainThreadWatchdog(options = {}) {
336
348
  * resume function. Nested calls refcount; a `stop()` while suspended cancels
337
349
  * the pending re-arm.
338
350
  *
339
- * No-op when nothing is armed (tests, `HOMEGRAPH_NO_WATCHDOG`, library embeds).
351
+ * No-op when nothing is armed (tests, default-off Spec 0047, `HOMEGRAPH_NO_WATCHDOG`, library embeds).
340
352
  */
341
353
  function suspendLivenessWatchdog() {
342
354
  suspendDepth++;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Spec 0044 — explore locate contract helpers (Located / Miss / Partial,
3
+ * literal-first demotion of log/toast spines, filename≠declaration hint,
4
+ * depth-fuse exemption for already-located symbols).
5
+ */
6
+ import type { ExploreEmission, ExploreCallRecord, ExploreProjectState } from './explore-session-state';
7
+ export type LocateOutcome = 'located' | 'partial' | 'miss';
8
+ /** Marker strings (idempotent checks). */
9
+ export declare const LOCATED_MARKER = "**Located**";
10
+ export declare const MISS_MARKER = "**Miss**";
11
+ export declare const FILENAME_DECL_MISMATCH_MARKER = "Filename does not match primary declaration";
12
+ /** Query explicitly names a log/toast API — then those nodes may stay on the spine. */
13
+ export declare function queryNamesLogOrToast(query: string): boolean;
14
+ export declare function isLogOrToastSpineName(name: string): boolean;
15
+ /** Demote log/toast when the user did not ask for them. */
16
+ export declare function shouldDemoteLogToastSpine(name: string, query: string): boolean;
17
+ /**
18
+ * Classify locate outcome for explore (Spec 0044 §8–9).
19
+ * Exact anchor / literal witness beats fuzzy-only FTS.
20
+ */
21
+ export declare function classifyLocateOutcome(opts: {
22
+ hasExactAnchorHit: boolean;
23
+ hasLiteralWitness: boolean;
24
+ hasFullDeclarations: boolean;
25
+ missingStaticRelation: boolean;
26
+ budgetPartial: boolean;
27
+ }): LocateOutcome;
28
+ export declare function formatLocatedBanner(): string;
29
+ export declare function formatPartialBanner(): string;
30
+ /** Miss: paths only, no source bodies. */
31
+ export declare function formatMissBanner(candidatePaths: readonly string[]): string;
32
+ /**
33
+ * When file basename (sans extension) ≠ primary declaration name.
34
+ * Returns null when names match or inputs are empty.
35
+ */
36
+ export declare function formatFilenameDeclarationMismatch(filePath: string, primaryDeclName: string | null | undefined): string | null;
37
+ /** Names recorded on the latest counted explore emission. */
38
+ export declare function locatedNamesFromEmission(emission: ExploreEmission | ExploreCallRecord | null | undefined): string[];
39
+ export declare function symbolMatchesLocatedNames(symbol: string | undefined, locatedNames: readonly string[]): boolean;
40
+ /**
41
+ * Spec 0044 §10.2 — do not depth-fuse-refuse a symbol already present on the
42
+ * latest explore locate list (even after Partial spent its one generic drill).
43
+ */
44
+ export declare function shouldExemptDepthFuseForLocatedSymbol(prior: ExploreProjectState | null | undefined, symbol: string | undefined): boolean;
45
+ /** Strip CJK→ASCII synonym seeds when Miss-bound (Spec 0044 §9). */
46
+ export declare function shouldSuppressSynonymExpansion(opts: {
47
+ hasExactAnchorHit: boolean;
48
+ hasLiteralWitness: boolean;
49
+ }): boolean;
50
+ //# sourceMappingURL=locate-contract.d.ts.map
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ /**
3
+ * Spec 0044 — explore locate contract helpers (Located / Miss / Partial,
4
+ * literal-first demotion of log/toast spines, filename≠declaration hint,
5
+ * depth-fuse exemption for already-located symbols).
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.FILENAME_DECL_MISMATCH_MARKER = exports.MISS_MARKER = exports.LOCATED_MARKER = void 0;
9
+ exports.queryNamesLogOrToast = queryNamesLogOrToast;
10
+ exports.isLogOrToastSpineName = isLogOrToastSpineName;
11
+ exports.shouldDemoteLogToastSpine = shouldDemoteLogToastSpine;
12
+ exports.classifyLocateOutcome = classifyLocateOutcome;
13
+ exports.formatLocatedBanner = formatLocatedBanner;
14
+ exports.formatPartialBanner = formatPartialBanner;
15
+ exports.formatMissBanner = formatMissBanner;
16
+ exports.formatFilenameDeclarationMismatch = formatFilenameDeclarationMismatch;
17
+ exports.locatedNamesFromEmission = locatedNamesFromEmission;
18
+ exports.symbolMatchesLocatedNames = symbolMatchesLocatedNames;
19
+ exports.shouldExemptDepthFuseForLocatedSymbol = shouldExemptDepthFuseForLocatedSymbol;
20
+ exports.shouldSuppressSynonymExpansion = shouldSuppressSynonymExpansion;
21
+ const explore_session_state_1 = require("./explore-session-state");
22
+ const explore_repeat_guard_1 = require("./explore-repeat-guard");
23
+ /** Marker strings (idempotent checks). */
24
+ exports.LOCATED_MARKER = '**Located**';
25
+ exports.MISS_MARKER = '**Miss**';
26
+ exports.FILENAME_DECL_MISMATCH_MARKER = 'Filename does not match primary declaration';
27
+ const LOG_TOAST_NAME_RE = /^(?:Logger|Hilog|HiLog|Toast|promptAction|log(?:Info|Error|Warn|Debug|Fatal)?|hilog)$/i;
28
+ /** Query explicitly names a log/toast API — then those nodes may stay on the spine. */
29
+ function queryNamesLogOrToast(query) {
30
+ return /\b(?:Logger|hilog|HiLog|Toast|promptAction|logInfo|logError|logWarn|logDebug)\b/i.test(query)
31
+ || /日志|弹窗|吐司/.test(query);
32
+ }
33
+ function isLogOrToastSpineName(name) {
34
+ return LOG_TOAST_NAME_RE.test(name.trim());
35
+ }
36
+ /** Demote log/toast when the user did not ask for them. */
37
+ function shouldDemoteLogToastSpine(name, query) {
38
+ return isLogOrToastSpineName(name) && !queryNamesLogOrToast(query);
39
+ }
40
+ /**
41
+ * Classify locate outcome for explore (Spec 0044 §8–9).
42
+ * Exact anchor / literal witness beats fuzzy-only FTS.
43
+ */
44
+ function classifyLocateOutcome(opts) {
45
+ if (!opts.hasExactAnchorHit && !opts.hasLiteralWitness)
46
+ return 'miss';
47
+ if (opts.budgetPartial || opts.missingStaticRelation)
48
+ return 'partial';
49
+ if (opts.hasFullDeclarations && (opts.hasExactAnchorHit || opts.hasLiteralWitness)) {
50
+ return 'located';
51
+ }
52
+ return 'partial';
53
+ }
54
+ function formatLocatedBanner() {
55
+ return (`> ${exports.LOCATED_MARKER} — exact anchor declaration(s) are in this reply. `
56
+ + 'Reuse them; do **not** Grep the same symbol names again. '
57
+ + 'Runtime behavior may still need validation after edits.');
58
+ }
59
+ function formatPartialBanner() {
60
+ return ('> **Partial** — evidence is incomplete (missing relation or budget). '
61
+ + 'Next: `homegraph_node` / `homegraph_usages` / `homegraph_search` for the named gap — '
62
+ + 'do **not** Grep the same symbol names already shown.');
63
+ }
64
+ /** Miss: paths only, no source bodies. */
65
+ function formatMissBanner(candidatePaths) {
66
+ const paths = candidatePaths
67
+ .map((p) => p.replace(/\\/g, '/').trim())
68
+ .filter(Boolean)
69
+ .slice(0, 12);
70
+ const list = paths.length
71
+ ? paths.map((p) => `- \`${p}\``).join('\n')
72
+ : '- _(no fuzzy path candidates)_';
73
+ return [
74
+ `> ${exports.MISS_MARKER} — no exact in-repo anchor or literal witness. Source bodies omitted.`,
75
+ 'Fuzzy candidate paths (no source):',
76
+ list,
77
+ 'Next: `homegraph_search` with a more precise symbol, file basename, or quoted UI label.',
78
+ ].join('\n');
79
+ }
80
+ /**
81
+ * When file basename (sans extension) ≠ primary declaration name.
82
+ * Returns null when names match or inputs are empty.
83
+ */
84
+ function formatFilenameDeclarationMismatch(filePath, primaryDeclName) {
85
+ if (!primaryDeclName?.trim())
86
+ return null;
87
+ const base = filePath.replace(/\\/g, '/').split('/').pop() ?? '';
88
+ const stem = base.replace(/\.[^.]+$/, '');
89
+ if (!stem)
90
+ return null;
91
+ const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
92
+ if (norm(stem) === norm(primaryDeclName))
93
+ return null;
94
+ return (`> ⚠ ${exports.FILENAME_DECL_MISMATCH_MARKER}: file \`${stem}\` vs \`${primaryDeclName.trim()}\` — `
95
+ + 'prefer the declaration name for edits and follow-up queries.');
96
+ }
97
+ /** Names recorded on the latest counted explore emission. */
98
+ function locatedNamesFromEmission(emission) {
99
+ if (!emission?.locatedNodes?.length)
100
+ return [];
101
+ const out = new Set();
102
+ for (const n of emission.locatedNodes) {
103
+ if (n?.name)
104
+ out.add(n.name);
105
+ if (n?.qualifiedName) {
106
+ out.add(n.qualifiedName);
107
+ const leaf = n.qualifiedName.split(/[.::]/).pop();
108
+ if (leaf)
109
+ out.add(leaf);
110
+ }
111
+ }
112
+ return [...out];
113
+ }
114
+ function symbolMatchesLocatedNames(symbol, locatedNames) {
115
+ const raw = symbol?.trim();
116
+ if (!raw || locatedNames.length === 0)
117
+ return false;
118
+ const needle = raw.toLowerCase();
119
+ const leaf = needle.split(/[.::]/).pop() ?? needle;
120
+ for (const n of locatedNames) {
121
+ const ln = n.toLowerCase();
122
+ if (ln === needle || ln === leaf)
123
+ return true;
124
+ if (ln.endsWith(`.${leaf}`) || ln.endsWith(`::${leaf}`))
125
+ return true;
126
+ }
127
+ return false;
128
+ }
129
+ /**
130
+ * Spec 0044 §10.2 — do not depth-fuse-refuse a symbol already present on the
131
+ * latest explore locate list (even after Partial spent its one generic drill).
132
+ */
133
+ function shouldExemptDepthFuseForLocatedSymbol(prior, symbol) {
134
+ const last = (0, explore_repeat_guard_1.latestCountedExplore)(prior);
135
+ if (!last)
136
+ return false;
137
+ // Closed explores already skip the fuse; exemption matters for Partial/Miss.
138
+ if ((0, explore_session_state_1.inferExploreEvidenceStatus)(last) === 'complete')
139
+ return false;
140
+ return symbolMatchesLocatedNames(symbol, locatedNamesFromEmission(last));
141
+ }
142
+ /** Strip CJK→ASCII synonym seeds when Miss-bound (Spec 0044 §9). */
143
+ function shouldSuppressSynonymExpansion(opts) {
144
+ return !opts.hasExactAnchorHit && !opts.hasLiteralWitness;
145
+ }
146
+ //# sourceMappingURL=locate-contract.js.map
@@ -1,21 +1,7 @@
1
- /**
2
- * MCP query cache — memory key index + serialized tool responses in homegraph.db.
3
- *
4
- * A per-project in-memory Set of cache keys gives O(1) negative lookup so misses
5
- * skip the SQLite SELECT. Payloads stay in homegraph.db; the Set is warmed from
6
- * DB on first use after daemon restart when the index stamp is still valid.
7
- *
8
- * Invalidated wholesale when the index stamp or cache format version changes.
9
- * Per-call staleness/worktree notices are applied AFTER cache lookup so hits
10
- * stay fresh.
11
- *
12
- * The key Set lives on the daemon main thread only (cache read/write runs there
13
- * before/after worker-pool explore dispatch), so no cross-thread locking is needed.
14
- */
15
1
  import type { QueryBuilder } from '../db/queries';
16
2
  import type { ToolResult } from './tools';
17
3
  /** Bump when cache-key normalization or cached payload shape changes. */
18
- export declare const QUERY_CACHE_FORMAT_VERSION = 5;
4
+ export declare const QUERY_CACHE_FORMAT_VERSION = 7;
19
5
  export declare function isMcpQueryCacheEnabled(): boolean;
20
6
  export declare function isCacheableMcpTool(toolName: string): boolean;
21
7
  /**
@@ -1,4 +1,14 @@
1
1
  "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.McpQueryCacheIndex = exports.QUERY_CACHE_FORMAT_VERSION = void 0;
4
+ exports.isMcpQueryCacheEnabled = isMcpQueryCacheEnabled;
5
+ exports.isCacheableMcpTool = isCacheableMcpTool;
6
+ exports.getMcpQueryCacheIndex = getMcpQueryCacheIndex;
7
+ exports.resetMcpQueryCacheIndices = resetMcpQueryCacheIndices;
8
+ exports.normalizeExploreQueryTerms = normalizeExploreQueryTerms;
9
+ exports.buildMcpQueryCacheFingerprint = buildMcpQueryCacheFingerprint;
10
+ exports.buildMcpQueryCacheKey = buildMcpQueryCacheKey;
11
+ const request_contract_1 = require("../search/request-contract");
2
12
  /**
3
13
  * MCP query cache — memory key index + serialized tool responses in homegraph.db.
4
14
  *
@@ -13,15 +23,6 @@
13
23
  * The key Set lives on the daemon main thread only (cache read/write runs there
14
24
  * before/after worker-pool explore dispatch), so no cross-thread locking is needed.
15
25
  */
16
- Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.McpQueryCacheIndex = exports.QUERY_CACHE_FORMAT_VERSION = void 0;
18
- exports.isMcpQueryCacheEnabled = isMcpQueryCacheEnabled;
19
- exports.isCacheableMcpTool = isCacheableMcpTool;
20
- exports.getMcpQueryCacheIndex = getMcpQueryCacheIndex;
21
- exports.resetMcpQueryCacheIndices = resetMcpQueryCacheIndices;
22
- exports.normalizeExploreQueryTerms = normalizeExploreQueryTerms;
23
- exports.buildMcpQueryCacheFingerprint = buildMcpQueryCacheFingerprint;
24
- exports.buildMcpQueryCacheKey = buildMcpQueryCacheKey;
25
26
  const node_crypto_1 = require("node:crypto");
26
27
  const query_plan_1 = require("../search/query-plan");
27
28
  const PLAN_RELATIONS = new Set(query_plan_1.QUERY_RELATIONS);
@@ -35,7 +36,7 @@ function defaultExploreMaxFiles(fileCount) {
35
36
  return 8;
36
37
  }
37
38
  /** Bump when cache-key normalization or cached payload shape changes. */
38
- exports.QUERY_CACHE_FORMAT_VERSION = 5;
39
+ exports.QUERY_CACHE_FORMAT_VERSION = 7;
39
40
  const METADATA_INDEX_STAMP = 'query_cache_index_stamp';
40
41
  const METADATA_FORMAT_VERSION = 'query_cache_format_version';
41
42
  /** Tools that must stay live — never cached. */
@@ -146,7 +147,7 @@ function exploreEnvFingerprint() {
146
147
  const fullSource = process.env.HOMEGRAPH_EXPLORE_FULL_SOURCE === '1' ? '1' : '0';
147
148
  const evidencePacks = process.env.HOMEGRAPH_ARKTS_EVIDENCE_PACKS === '0' ? '0' : '1';
148
149
  const queryPaths = process.env.HOMEGRAPH_ARKTS_QUERY_PATHS === '0' ? '0' : '1';
149
- return `linums:${linums}|adaptive:${adaptive}|rankMultiterm:${rankMultiterm}|fullSource:${fullSource}|arktsEvidence:${evidencePacks}|arktsPaths:${queryPaths}`;
150
+ return `implementationContext:${process.env.HOMEGRAPH_ARKTS_IMPLEMENTATION_CONTEXT === '0' ? '0' : '1'}|controlEvidence:${process.env.HOMEGRAPH_ARKTS_CONTROL_EVIDENCE === '0' ? '0' : '1'}|linums:${linums}|adaptive:${adaptive}|rankMultiterm:${rankMultiterm}|fullSource:${fullSource}|arktsEvidence:${evidencePacks}|arktsPaths:${queryPaths}|accuracyTargets:${process.env.HOMEGRAPH_ACCURACY_TARGETS === '0' ? '0' : '1'}|accuracyCoverage:${process.env.HOMEGRAPH_ACCURACY_COVERAGE === '0' ? '0' : '1'}`;
150
151
  }
151
152
  function normalizeString(value) {
152
153
  if (typeof value !== 'string')
@@ -213,7 +214,15 @@ function queryPlanFingerprint(value) {
213
214
  if (!features || Object.keys(features).length > 64
214
215
  || !Object.values(features).every(v => typeof v === 'boolean'))
215
216
  return undefined;
217
+ let requestContract;
218
+ try {
219
+ requestContract = (0, request_contract_1.validateRequestContract)(plan.requestContract, originalQuery + '\n' + (query(plan.taskContext) ?? ''));
220
+ }
221
+ catch {
222
+ return undefined;
223
+ }
216
224
  return stableJson({
225
+ requestContract,
217
226
  version: plan.version, originalQuery, canonicalQuery, intent, route,
218
227
  taskContext: query(plan.taskContext),
219
228
  anchors, searchTerms, steps, features,
@@ -0,0 +1,69 @@
1
+ import type { RequestContract, RequestTarget } from '../search/request-contract';
2
+ export interface RequestSource {
3
+ filePath: string;
4
+ start: number;
5
+ source: string;
6
+ }
7
+ interface StringToken {
8
+ start: number;
9
+ end: number;
10
+ value: string;
11
+ }
12
+ interface Lexed {
13
+ code: string;
14
+ strings: StringToken[];
15
+ pairs: Map<number, number>;
16
+ valid: boolean;
17
+ }
18
+ export interface TargetEvidence {
19
+ id: string;
20
+ text: string;
21
+ presence: RequestTarget['presence'];
22
+ status: 'observed' | 'ambiguous' | 'not_observed' | 'requested_not_observed';
23
+ locations: string[];
24
+ note: string;
25
+ }
26
+ export interface BehaviorEvidence {
27
+ id: string;
28
+ text: string;
29
+ kind: string;
30
+ status: 'binding_observed' | 'binding_not_observed' | 'unknown';
31
+ locations: string[];
32
+ note: string;
33
+ }
34
+ export interface RequestEvidence {
35
+ scope: 'returned_source_only';
36
+ runtimeVerified: false;
37
+ targets: TargetEvidence[];
38
+ behaviors: BehaviorEvidence[];
39
+ }
40
+ /** Conservative lexical witness reader. It does not evaluate ArkTS or infer data flow.
41
+ * Mask comments, strings, templates and regular expressions before reading structure.
42
+ * Escaped/interpolated strings are deliberately not used as exact literal witnesses.
43
+ */
44
+ export declare function lexEvidenceSource(source: string): Lexed;
45
+ /** Reuse one analysis per verified source unit; no disk reads or model calls. */
46
+ export declare function createRequestEvidenceInspector(contract: RequestContract): {
47
+ score: (units: RequestSource[]) => number;
48
+ inspect: (units: RequestSource[]) => RequestEvidence;
49
+ };
50
+ export declare function renderRequestEvidence(evidence: RequestEvidence, targets: boolean, behaviors: boolean): string[];
51
+ export interface ControlEvidence {
52
+ location: string;
53
+ control: string;
54
+ labels: string[];
55
+ enabled: 'binding_observed' | 'binding_not_observed' | 'unknown';
56
+ expression?: string;
57
+ click?: string;
58
+ state: Array<{
59
+ expression: string;
60
+ writeLocations: string[];
61
+ }>;
62
+ }
63
+ /** An inventory of returned controls, not a natural-language target match or a UI test.
64
+ * Resource names stay literal: an icon called "undo" is not translated into a task label.
65
+ */
66
+ export declare function inspectControlEvidence(units: RequestSource[]): ControlEvidence[];
67
+ export declare function renderControlEvidence(rows: ControlEvidence[]): string[];
68
+ export {};
69
+ //# sourceMappingURL=request-evidence.d.ts.map