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.
- package/CHANGELOG.md +27 -0
- package/README.md +64 -14
- package/dist/db/queries.d.ts +4 -0
- package/dist/db/queries.js +15 -0
- package/dist/extraction/grammars.d.ts +19 -0
- package/dist/extraction/grammars.js +44 -1
- package/dist/graph/evidence-paths.d.ts +2 -0
- package/dist/graph/evidence-paths.js +20 -3
- package/dist/mcp/arkts-evidence-packs.d.ts +17 -0
- package/dist/mcp/arkts-evidence-packs.js +219 -10
- package/dist/mcp/daemon.d.ts +21 -3
- package/dist/mcp/daemon.js +60 -6
- package/dist/mcp/engine.d.ts +26 -0
- package/dist/mcp/engine.js +143 -7
- package/dist/mcp/evidence-audit.d.ts +3 -0
- package/dist/mcp/evidence-audit.js +40 -0
- package/dist/mcp/implementation-context.d.ts +35 -0
- package/dist/mcp/implementation-context.js +232 -0
- package/dist/mcp/index-availability.d.ts +22 -0
- package/dist/mcp/index-availability.js +40 -1
- package/dist/mcp/index.js +9 -0
- package/dist/mcp/indexable-root.d.ts +22 -0
- package/dist/mcp/indexable-root.js +140 -0
- package/dist/mcp/liveness-watchdog.d.ts +6 -1
- package/dist/mcp/liveness-watchdog.js +17 -5
- package/dist/mcp/locate-contract.d.ts +50 -0
- package/dist/mcp/locate-contract.js +146 -0
- package/dist/mcp/query-cache.d.ts +1 -15
- package/dist/mcp/query-cache.js +20 -11
- package/dist/mcp/request-evidence.d.ts +69 -0
- package/dist/mcp/request-evidence.js +297 -0
- package/dist/mcp/server-instructions.d.ts +2 -2
- package/dist/mcp/server-instructions.js +12 -9
- package/dist/mcp/session.js +15 -0
- package/dist/mcp/tools.d.ts +67 -12
- package/dist/mcp/tools.js +841 -134
- package/dist/project-map/index.d.ts +45 -0
- package/dist/project-map/index.js +371 -3
- package/dist/resolution/callback-synthesizer.js +251 -0
- package/dist/resolution/frameworks/arkts-entry.d.ts +35 -6
- package/dist/resolution/frameworks/arkts-entry.js +513 -30
- package/dist/resolution/index.js +25 -21
- package/dist/runtime-log.d.ts +52 -0
- package/dist/runtime-log.js +199 -0
- package/dist/search/literal-evidence.js +11 -3
- package/dist/search/query-plan-provider.js +15 -6
- package/dist/search/query-plan.d.ts +3 -1
- package/dist/search/query-plan.js +25 -16
- package/dist/search/query-utils.d.ts +13 -0
- package/dist/search/query-utils.js +64 -0
- package/dist/search/request-contract.d.ts +29 -0
- package/dist/search/request-contract.js +76 -0
- 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.
|
|
77
|
-
* `
|
|
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
|
-
|
|
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 =
|
|
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
|
/**
|
package/dist/mcp/query-cache.js
CHANGED
|
@@ -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 =
|
|
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
|