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.
- package/CHANGELOG.md +34 -0
- package/dist/extraction/grammars.d.ts +19 -0
- package/dist/extraction/grammars.js +44 -1
- package/dist/extraction/index.js +17 -12
- package/dist/extraction/languages/arkts.d.ts +40 -7
- package/dist/extraction/languages/arkts.js +290 -84
- package/dist/extraction/tree-sitter.js +12 -3
- package/dist/index.js +23 -12
- package/dist/mcp/arkts-evidence-packs.js +1 -0
- 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/index-availability.d.ts +40 -10
- package/dist/mcp/index-availability.js +89 -23
- 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/server-instructions.d.ts +2 -6
- package/dist/mcp/server-instructions.js +15 -5
- package/dist/mcp/session.js +15 -0
- package/dist/mcp/tools.d.ts +52 -0
- package/dist/mcp/tools.js +750 -103
- package/dist/project-map/index.d.ts +45 -0
- package/dist/project-map/index.js +373 -45
- 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/query-plan-provider.js +3 -2
- package/dist/search/query-plan.js +21 -19
- package/dist/search/query-utils.d.ts +13 -0
- package/dist/search/query-utils.js +64 -0
- 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
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
26
|
+
return 'HomeGraph status=empty — not ready; retry shortly.';
|
|
28
27
|
case 'fast':
|
|
29
|
-
return
|
|
28
|
+
return 'HomeGraph status=fast — map only; use homegraph_project; retry other tools later.';
|
|
30
29
|
case 'syncing':
|
|
31
|
-
return
|
|
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
|
-
*
|
|
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.
|
|
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
|