homegraph 1.5.8 → 1.6.0

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 (40) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/extraction/grammars.d.ts +19 -0
  3. package/dist/extraction/grammars.js +44 -1
  4. package/dist/extraction/index.js +17 -12
  5. package/dist/extraction/languages/arkts.d.ts +40 -7
  6. package/dist/extraction/languages/arkts.js +290 -84
  7. package/dist/extraction/tree-sitter.js +12 -3
  8. package/dist/index.js +23 -12
  9. package/dist/mcp/arkts-evidence-packs.js +1 -0
  10. package/dist/mcp/daemon.d.ts +21 -3
  11. package/dist/mcp/daemon.js +60 -6
  12. package/dist/mcp/engine.d.ts +26 -0
  13. package/dist/mcp/engine.js +143 -7
  14. package/dist/mcp/index-availability.d.ts +40 -10
  15. package/dist/mcp/index-availability.js +89 -23
  16. package/dist/mcp/index.js +9 -0
  17. package/dist/mcp/indexable-root.d.ts +22 -0
  18. package/dist/mcp/indexable-root.js +140 -0
  19. package/dist/mcp/liveness-watchdog.d.ts +6 -1
  20. package/dist/mcp/liveness-watchdog.js +17 -5
  21. package/dist/mcp/locate-contract.d.ts +50 -0
  22. package/dist/mcp/locate-contract.js +146 -0
  23. package/dist/mcp/server-instructions.d.ts +2 -6
  24. package/dist/mcp/server-instructions.js +15 -5
  25. package/dist/mcp/session.js +15 -0
  26. package/dist/mcp/tools.d.ts +52 -0
  27. package/dist/mcp/tools.js +750 -103
  28. package/dist/project-map/index.d.ts +45 -0
  29. package/dist/project-map/index.js +373 -45
  30. package/dist/resolution/callback-synthesizer.js +251 -0
  31. package/dist/resolution/frameworks/arkts-entry.d.ts +35 -6
  32. package/dist/resolution/frameworks/arkts-entry.js +513 -30
  33. package/dist/resolution/index.js +25 -21
  34. package/dist/runtime-log.d.ts +52 -0
  35. package/dist/runtime-log.js +199 -0
  36. package/dist/search/query-plan-provider.js +3 -2
  37. package/dist/search/query-plan.js +21 -19
  38. package/dist/search/query-utils.d.ts +13 -0
  39. package/dist/search/query-utils.js +64 -0
  40. package/package.json +2 -2
@@ -1,44 +1,62 @@
1
1
  "use strict";
2
2
  /**
3
- * Product-facing index availability for MCP tool results (Spec 0032).
3
+ * Product-facing index availability for MCP tool results (Spec 0032 + 0035).
4
4
  *
5
- * Internal `build_phase` stays as-is; this layer maps it (+ write-lock / busy)
6
- * to the four states hosts and agents should see in tool text.
5
+ * Internal `build_phase` stays as-is; this layer maps it (+ write-lock / busy /
6
+ * pending dirty files) to the five states hosts and agents should see.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.BOUND_PROJECT_PATH_PIN_MARKER = exports.PROJECT_ROOT_HINT_MARKER = exports.PRODUCT_STATUS_GLOSSARY = void 0;
9
10
  exports.productIndexGuidance = productIndexGuidance;
11
+ exports.formatProductStatusLine = formatProductStatusLine;
10
12
  exports.resolveProductIndexState = resolveProductIndexState;
11
13
  exports.isSqliteBusyMessage = isSqliteBusyMessage;
12
- const EMPTY_LINES = [
13
- 'HomeGraph status=empty — project map is not ready yet.',
14
- 'Retry in a few seconds (or call `homegraph_project` once the fast build completes).',
15
- ];
16
- const FAST_LINES = [
17
- 'HomeGraph status=fast — module/file map is ready; full symbol index is still building.',
18
- 'Use `homegraph_project` for the module/file map now, then retry this tool once indexing finishes.',
19
- ];
20
- const SYNCING_LINES = [
21
- 'HomeGraph status=syncing — the index is being written (lock held or local index/sync in progress).',
22
- 'Retry this tool shortly; do not busy-loop.',
23
- ];
14
+ exports.textAlreadyHasProductStatus = textAlreadyHasProductStatus;
15
+ exports.formatProjectRootPathHint = formatProjectRootPathHint;
16
+ exports.textAlreadyHasProjectRootHint = textAlreadyHasProjectRootHint;
17
+ exports.formatBoundProjectPathPinNotice = formatBoundProjectPathPinNotice;
18
+ exports.textAlreadyHasBoundProjectPathPinNotice = textAlreadyHasBoundProjectPathPinNotice;
19
+ /** One-line glossary for MCP initialize / tool surface (Spec 0035). */
20
+ exports.PRODUCT_STATUS_GLOSSARY = 'status: empty=not ready · fast=map only (homegraph_project) · full=fresh · dirty=usable but listed paths outdated · syncing=write lock, retry';
21
+ const MAX_DIRTY_PATHS = 5;
22
+ /** Whole-response guidance when the tool cannot usefully answer yet. */
24
23
  function productIndexGuidance(state) {
25
24
  switch (state) {
26
25
  case 'empty':
27
- return EMPTY_LINES.join('\n');
26
+ return 'HomeGraph status=empty — not ready; retry shortly.';
28
27
  case 'fast':
29
- return FAST_LINES.join('\n');
28
+ return 'HomeGraph status=fast — map only; use homegraph_project; retry other tools later.';
30
29
  case 'syncing':
31
- return SYNCING_LINES.join('\n');
30
+ return 'HomeGraph status=syncing — locked; retry shortly, do not loop.';
31
+ }
32
+ }
33
+ /**
34
+ * Single status line for tool footers (and for guidance-only replies).
35
+ * `pendingPaths` only used when `state === 'dirty'`.
36
+ */
37
+ function formatProductStatusLine(state, opts) {
38
+ switch (state) {
39
+ case 'empty':
40
+ case 'fast':
41
+ case 'syncing':
42
+ return productIndexGuidance(state);
43
+ case 'full':
44
+ return 'HomeGraph status=full — complete and up to date.';
45
+ case 'dirty': {
46
+ const paths = opts?.pendingPaths ?? [];
47
+ if (paths.length === 0) {
48
+ return 'HomeGraph status=dirty — outdated: pending files';
49
+ }
50
+ const shown = paths.slice(0, MAX_DIRTY_PATHS);
51
+ const more = paths.length > MAX_DIRTY_PATHS ? `, …+${paths.length - MAX_DIRTY_PATHS}` : '';
52
+ return `HomeGraph status=dirty — outdated: ${shown.join(', ')}${more}`;
53
+ }
32
54
  }
33
55
  }
34
56
  /**
35
57
  * Resolve the product state from a live HomeGraph handle.
36
58
  *
37
- * - empty: fast map not ready (`none` / `building_fast`)
38
- * - fast: map ready, full index not done (`fast` / `indexing`)
39
- * - full: symbol index ready
40
- * - syncing: full (or empty-while-contended-init) and a writer holds the lock /
41
- * this process is indexing — reads may fail or see a moving target
59
+ * Priority: syncing > empty > fast > dirty > full
42
60
  */
43
61
  function resolveProductIndexState(cg) {
44
62
  let phase;
@@ -69,10 +87,58 @@ function resolveProductIndexState(cg) {
69
87
  catch {
70
88
  return 'syncing';
71
89
  }
90
+ let pendingCount = 0;
91
+ try {
92
+ pendingCount = cg.getPendingFiles?.()?.length ?? 0;
93
+ }
94
+ catch {
95
+ pendingCount = 0;
96
+ }
97
+ if (pendingCount > 0)
98
+ return 'dirty';
72
99
  return 'full';
73
100
  }
74
101
  /** True when an error message indicates SQLite writer contention. */
75
102
  function isSqliteBusyMessage(message) {
76
103
  return /SQLITE_BUSY|database is locked/i.test(message);
77
104
  }
105
+ /** True when text is already a pure (or leading) HomeGraph status line. */
106
+ function textAlreadyHasProductStatus(text) {
107
+ return /^\s*HomeGraph status=/m.test(text);
108
+ }
109
+ /** Marker line for Spec 0038 project-root path hint (idempotent prepend). */
110
+ exports.PROJECT_ROOT_HINT_MARKER = 'HomeGraph project root:';
111
+ /**
112
+ * Short preamble: absolute project root + how to join repo-relative paths (Spec 0038).
113
+ * `absRoot` should already be resolved (platform-native absolute path).
114
+ */
115
+ function formatProjectRootPathHint(absRoot) {
116
+ const root = absRoot.trim();
117
+ return (`${exports.PROJECT_ROOT_HINT_MARKER} \`${root}\`\n` +
118
+ 'Paths below are repo-relative to that root. Pass them to Read/Grep as-is, or join as `<root>/<relative>` (use `/`). Do not invent experiment/result directory prefixes.');
119
+ }
120
+ /** True when text already carries a Spec 0038 project-root hint. */
121
+ function textAlreadyHasProjectRootHint(text) {
122
+ return text.includes(exports.PROJECT_ROOT_HINT_MARKER);
123
+ }
124
+ /** Marker for Spec 0043 bound-root projectPath soft-pin notice (idempotent prepend). */
125
+ exports.BOUND_PROJECT_PATH_PIN_MARKER = 'This MCP session is bound to';
126
+ /**
127
+ * English preamble when a tool `projectPath` resolves to a different index root
128
+ * than the MCP session's default bound root (Spec 0043). Success-shaped — not isError.
129
+ */
130
+ function formatBoundProjectPathPinNotice(opts) {
131
+ const bound = opts.boundRoot.trim();
132
+ const requested = opts.requestedPath.trim();
133
+ const resolved = opts.resolvedRoot === null
134
+ ? 'no .homegraph/ found walking up from it'
135
+ : `resolves to \`${opts.resolvedRoot.trim()}\``;
136
+ return (`${exports.BOUND_PROJECT_PATH_PIN_MARKER} \`${bound}\`.\n` +
137
+ `Ignoring projectPath=\`${requested}\` (${resolved}).\n` +
138
+ 'Results below are from the bound project root only. Omit projectPath, or pass a path under that root.');
139
+ }
140
+ /** True when text already carries a Spec 0043 pin notice. */
141
+ function textAlreadyHasBoundProjectPathPinNotice(text) {
142
+ return text.includes(exports.BOUND_PROJECT_PATH_PIN_MARKER);
143
+ }
78
144
  //# sourceMappingURL=index-availability.js.map
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