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
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.buildArktsEvidencePacks = buildArktsEvidencePacks;
|
|
37
|
+
const arkts_1 = require("../extraction/languages/arkts");
|
|
38
|
+
const implementation_context_1 = require("./implementation-context");
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const request_contract_1 = require("../search/request-contract");
|
|
41
|
+
const request_evidence_1 = require("./request-evidence");
|
|
4
42
|
const node_crypto_1 = require("node:crypto");
|
|
5
43
|
const node_fs_1 = require("node:fs");
|
|
6
44
|
const utils_1 = require("../utils");
|
|
@@ -8,7 +46,7 @@ const evidence_rendering_1 = require("./evidence-rendering");
|
|
|
8
46
|
const explore_dedup_1 = require("./explore-dedup");
|
|
9
47
|
const evidence_paths_1 = require("../graph/evidence-paths");
|
|
10
48
|
const LIMITS = { nodes: 36, expand: 12, edges: 48, files: 8, fileBytes: 512 * 1024, bytes: 2 * 1024 * 1024 };
|
|
11
|
-
const
|
|
49
|
+
const BASE_DECLARATIONS = new Set(['class', 'struct', 'component', 'interface', 'method', 'function', 'property', 'field', 'constant', 'variable', 'enum']);
|
|
12
50
|
const CONTAINERS = new Set(['class', 'struct', 'component', 'interface', 'enum']);
|
|
13
51
|
const RELATIONS = new Set(['calls', 'references', 'extends', 'implements', 'instantiates']);
|
|
14
52
|
const inline = (s) => s.replace(/[`\r\n]/g, ' ');
|
|
@@ -18,19 +56,36 @@ const local = (n) => !n.filePath.startsWith('ohos-sdk:') && !/\.d\.(?:ts|ets)$/i
|
|
|
18
56
|
// ArkAnalyzer's virtual entry/implicit constructor has no declaration on disk.
|
|
19
57
|
// Explicit constructors with a real range remain available through the normal renderer.
|
|
20
58
|
const artifact = (n) => n.filePath.includes('@dummy') || n.name.startsWith('@dummy')
|
|
59
|
+
|| /^%AM\d+/i.test(n.name)
|
|
21
60
|
|| (n.name === 'constructor' && n.startLine === 1 && n.endLine === 1);
|
|
22
61
|
/** Consume already-located nodes. No text search, new planner call or recursive retrieval. */
|
|
23
62
|
function buildArktsEvidencePacks(graph, options) {
|
|
63
|
+
const contextEnabled = (0, implementation_context_1.implementationContextEnabled)();
|
|
64
|
+
const DECLARATIONS = contextEnabled ? new Set([...BASE_DECLARATIONS, 'type_alias', 'import', 'export', 'route', 'file']) : BASE_DECLARATIONS;
|
|
24
65
|
if (!options.nodes.some(n => local(n) && /\.ets$/i.test(n.filePath) && DECLARATIONS.has(n.kind)))
|
|
25
66
|
return null;
|
|
26
67
|
const search = options.pathSearch;
|
|
68
|
+
const targetChecks = (0, request_contract_1.accuracyTargetsEnabled)();
|
|
69
|
+
const behaviorChecks = (0, request_contract_1.accuracyCoverageEnabled)();
|
|
70
|
+
const inspector = options.requestContract && (targetChecks || behaviorChecks)
|
|
71
|
+
? (0, request_evidence_1.createRequestEvidenceInspector)(options.requestContract) : undefined;
|
|
72
|
+
const requestSources = new Map();
|
|
73
|
+
const requestUnit = (s) => {
|
|
74
|
+
let unit = requestSources.get(s);
|
|
75
|
+
if (!unit) {
|
|
76
|
+
unit = { filePath: s.node.filePath, start: s.start, source: s.source };
|
|
77
|
+
requestSources.set(s, unit);
|
|
78
|
+
}
|
|
79
|
+
return unit;
|
|
80
|
+
};
|
|
27
81
|
const requiredIds = search ? new Set([...search.goal.anchorIds, ...search.paths.flatMap(p => p.nodeIds)]) : options.focusIds;
|
|
28
82
|
const inputNodes = search && requiredIds.size ? [...requiredIds].flatMap(id => { const n = graph.getNode(id); return n ? [n] : []; }) : options.nodes;
|
|
29
83
|
const all = (0, evidence_rendering_1.canonicalSourceDeclarations)(inputNodes.filter(n => DECLARATIONS.has(n.kind) && local(n) && !artifact(n)));
|
|
30
84
|
if (!all.length)
|
|
31
85
|
return null;
|
|
86
|
+
const inputOrder = new Map(all.map((node, index) => [node.id, index]));
|
|
32
87
|
const candidates = all.sort((a, b) => Number(requiredIds.has(b.id)) - Number(requiredIds.has(a.id))
|
|
33
|
-
|| a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine).slice(0, LIMITS.nodes);
|
|
88
|
+
|| (inspector && targetChecks ? inputOrder.get(a.id) - inputOrder.get(b.id) : a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine)).slice(0, LIMITS.nodes);
|
|
34
89
|
const focus = candidates.filter(n => requiredIds.has(n.id));
|
|
35
90
|
// A named owning type provides scope for its named members, not another
|
|
36
91
|
// callable endpoint. Do not invent a missing Type→method call obligation.
|
|
@@ -45,11 +100,11 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
45
100
|
let bytesRead = 0;
|
|
46
101
|
const root = (0, node_fs_1.realpathSync)(options.projectRoot);
|
|
47
102
|
const maxFiles = Math.max(1, Math.min(LIMITS.files, options.maxFiles ?? LIMITS.files));
|
|
48
|
-
const readSource = (node) => {
|
|
103
|
+
const readSource = (node, snapshot = false) => {
|
|
49
104
|
const previous = nodeSources.get(node.id);
|
|
50
105
|
if (previous)
|
|
51
106
|
return previous;
|
|
52
|
-
if (!local(node) || artifact(node) || !DECLARATIONS.has(node.kind)) {
|
|
107
|
+
if ((!local(node) && !(contextEnabled && (snapshot || node.kind === 'route' || (!node.filePath.startsWith('ohos-sdk:') && /\.d\.(?:ts|ets)$/.test(node.filePath))))) || artifact(node) || !DECLARATIONS.has(node.kind)) {
|
|
53
108
|
const failure = { gap: { target: label(node), reason: 'unavailable', nextAnchor: location(node) } };
|
|
54
109
|
nodeSources.set(node.id, failure);
|
|
55
110
|
return failure;
|
|
@@ -89,7 +144,7 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
89
144
|
const content = buffer.subarray(0, bytes).toString('utf8');
|
|
90
145
|
const indexed = graph.getFile(node.filePath);
|
|
91
146
|
// Hash equality, not mtime: edits can retain timestamps and invalidate ranges.
|
|
92
|
-
if (bytes !== size || !indexed || indexed.contentHash !== (0, node_crypto_1.createHash)('sha256').update(content).digest('hex'))
|
|
147
|
+
if (bytes !== size || (!snapshot && (!indexed || indexed.contentHash !== (0, node_crypto_1.createHash)('sha256').update(content).digest('hex'))))
|
|
93
148
|
file.reason = 'stale';
|
|
94
149
|
else
|
|
95
150
|
file = { content, lines: content.split('\n'), fingerprint: (0, explore_dedup_1.fileFingerprint)(content) };
|
|
@@ -101,6 +156,14 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
101
156
|
}
|
|
102
157
|
fileCache.set(node.filePath, file);
|
|
103
158
|
}
|
|
159
|
+
if ((snapshot || node.kind === 'file') && file.lines)
|
|
160
|
+
node = { ...node, endLine: file.lines.length };
|
|
161
|
+
if (node.kind === 'import' && file.lines) {
|
|
162
|
+
const head = file.lines.slice(node.startLine - 1, node.startLine + 20).join('\n');
|
|
163
|
+
const importText = head.match(/^\s*import\s+[\s\S]*?\bfrom\s*['"][^'"\r\n]+['"][^\S\r\n]*;?/);
|
|
164
|
+
if (importText)
|
|
165
|
+
node = { ...node, endLine: node.startLine + importText[0].split('\n').length - 1 };
|
|
166
|
+
}
|
|
104
167
|
const reason = file.reason ?? (!Number.isInteger(node.startLine) || !Number.isInteger(node.endLine)
|
|
105
168
|
|| node.startLine < 1 || node.endLine < node.startLine || node.endLine > file.lines.length
|
|
106
169
|
|| !file.lines.slice(node.startLine - 1, node.endLine).join('\n').trim() ? 'invalid_range' : undefined);
|
|
@@ -116,13 +179,13 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
116
179
|
const id = `${node.filePath}:${start}-${node.endLine}`;
|
|
117
180
|
if (!sources.has(id))
|
|
118
181
|
sources.set(id, { id, node, start, end: node.endLine,
|
|
119
|
-
source: file.lines.slice(start - 1, node.endLine).join('\n'), fingerprint: file.fingerprint });
|
|
182
|
+
source: file.lines.slice(start - 1, node.endLine).join('\n'), fingerprint: file.fingerprint, ...(snapshot ? { snapshot: true } : {}) });
|
|
120
183
|
const result = { id };
|
|
121
184
|
nodeSources.set(node.id, result);
|
|
122
185
|
return result;
|
|
123
186
|
};
|
|
124
187
|
const dependencies = (nodes) => {
|
|
125
|
-
const resolved = nodes.map(readSource);
|
|
188
|
+
const resolved = nodes.map(n => readSource(n));
|
|
126
189
|
return { sources: [...new Set(resolved.flatMap(s => s.id ? [s.id] : []))],
|
|
127
190
|
gaps: resolved.flatMap(s => s.gap ? [s.gap] : []) };
|
|
128
191
|
};
|
|
@@ -133,6 +196,104 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
133
196
|
if (all.length > candidates.length)
|
|
134
197
|
globalGaps.push({ target: `${all.length - candidates.length} additional candidates`,
|
|
135
198
|
reason: 'scope_limit', nextAnchor: location(all[candidates.length]) });
|
|
199
|
+
const configNode = (file) => ({ id: `context-config:${file}`, kind: 'constant', name: file, qualifiedName: file,
|
|
200
|
+
filePath: file, startLine: 1, endLine: 1, startColumn: 0, endColumn: 0, updatedAt: 0, language: 'yaml' });
|
|
201
|
+
const manifestCache = new Map();
|
|
202
|
+
const nearestManifest = (file) => {
|
|
203
|
+
if (manifestCache.has(file))
|
|
204
|
+
return manifestCache.get(file);
|
|
205
|
+
let dir = path.posix.dirname(file.replace(/\\/g, '/'));
|
|
206
|
+
let found;
|
|
207
|
+
for (let depth = 0; depth < 12; depth++) {
|
|
208
|
+
const manifest = path.posix.join(dir, 'oh-package.json5');
|
|
209
|
+
const absolute = (0, utils_1.validatePathWithinRoot)(root, manifest);
|
|
210
|
+
try {
|
|
211
|
+
if (absolute && (0, node_fs_1.statSync)(absolute).isFile()) {
|
|
212
|
+
found = configNode(manifest);
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch { /* absent */ }
|
|
217
|
+
if (dir === '.')
|
|
218
|
+
break;
|
|
219
|
+
dir = path.posix.dirname(dir);
|
|
220
|
+
}
|
|
221
|
+
manifestCache.set(file, found);
|
|
222
|
+
return found;
|
|
223
|
+
};
|
|
224
|
+
const configValue = (node) => {
|
|
225
|
+
const r = readSource(node, true);
|
|
226
|
+
if (!r.id)
|
|
227
|
+
return undefined;
|
|
228
|
+
try {
|
|
229
|
+
const value = (0, arkts_1.parseJson5Minimal)(sources.get(r.id).source);
|
|
230
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
const moduleBinding = (importer, module) => {
|
|
237
|
+
const manifest = nearestManifest(importer);
|
|
238
|
+
if (!manifest)
|
|
239
|
+
return undefined;
|
|
240
|
+
const config = configValue(manifest);
|
|
241
|
+
const deps = config?.dependencies;
|
|
242
|
+
if (!deps || typeof deps !== 'object' || Array.isArray(deps))
|
|
243
|
+
return undefined;
|
|
244
|
+
const dep = deps[module];
|
|
245
|
+
if (typeof dep !== 'string' || !/^file:\.{1,2}\//.test(dep))
|
|
246
|
+
return undefined;
|
|
247
|
+
const depRoot = path.posix.normalize(path.posix.join(path.posix.dirname(manifest.filePath), dep.slice(5)));
|
|
248
|
+
if (depRoot === '..' || depRoot.startsWith('../') || path.posix.isAbsolute(depRoot))
|
|
249
|
+
return undefined;
|
|
250
|
+
const targetManifest = configNode(path.posix.join(depRoot, 'oh-package.json5'));
|
|
251
|
+
const target = configValue(targetManifest);
|
|
252
|
+
if (!target)
|
|
253
|
+
return undefined;
|
|
254
|
+
const entry = typeof target.main === 'string' ? path.posix.join(depRoot, target.main) : depRoot;
|
|
255
|
+
const relativePath = './' + path.posix.relative(path.posix.dirname(importer), entry);
|
|
256
|
+
return { relativePath, witnesses: [manifest, targetManifest] };
|
|
257
|
+
};
|
|
258
|
+
const contextGroups = contextEnabled ? (0, implementation_context_1.collectImplementationContext)(graph, focus.length ? focus : candidates, node => { const r = readSource(node); return r.id ? sources.get(r.id)?.source : undefined; }, options.sdkModule, moduleBinding) : [];
|
|
259
|
+
for (const group of contextGroups) {
|
|
260
|
+
const deps = dependencies(group.nodes);
|
|
261
|
+
packs.push({ id: group.id, label: group.label, priority: 130, ...deps,
|
|
262
|
+
context: { label: group.label, notes: [...group.notes, ...group.missing.map(m => `Unresolved: ${m}`)], ...(group.sdk ? { sdk: group.sdk } : {}) } });
|
|
263
|
+
for (const missing of group.missing)
|
|
264
|
+
globalGaps.push({ target: missing, reason: 'unindexed_connection', nextAnchor: missing });
|
|
265
|
+
}
|
|
266
|
+
// Only ancestor manifests of the located code; no workspace traversal or credential files.
|
|
267
|
+
const configs = new Set();
|
|
268
|
+
for (const node of contextEnabled && contextGroups.some(g => g.id.startsWith('impl-import:')) ? (focus.length ? focus : candidates).slice(0, 3) : []) {
|
|
269
|
+
let dir = path.posix.dirname(node.filePath.replace(/\\/g, '/'));
|
|
270
|
+
for (let depth = 0; depth < 12; depth++) {
|
|
271
|
+
const manifest = path.posix.join(dir, 'oh-package.json5');
|
|
272
|
+
const absolute = (0, utils_1.validatePathWithinRoot)(root, manifest);
|
|
273
|
+
try {
|
|
274
|
+
if (absolute && (0, node_fs_1.statSync)(absolute).isFile()) {
|
|
275
|
+
configs.add(manifest);
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch { /* missing ancestor manifest */ }
|
|
280
|
+
if (dir === '.')
|
|
281
|
+
break;
|
|
282
|
+
dir = path.posix.dirname(dir);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (configs.size)
|
|
286
|
+
configs.add('build-profile.json5');
|
|
287
|
+
for (const config of configs) {
|
|
288
|
+
const node = { id: `context-config:${config}`, kind: 'constant', name: config, qualifiedName: config,
|
|
289
|
+
filePath: config, startLine: 1, endLine: 1, startColumn: 0, endColumn: 0, updatedAt: Date.now(), language: 'yaml' };
|
|
290
|
+
const dep = readSource(node, true);
|
|
291
|
+
if (dep.id)
|
|
292
|
+
packs.push({ id: node.id, label: `module/version configuration ${config}`, priority: 125, sources: [dep.id], gaps: [],
|
|
293
|
+
context: { label: `module/version configuration ${config}`, notes: ['Current configuration snapshot; declared dependencies do not prove installation or export availability.'] } });
|
|
294
|
+
else if (dep.gap && dep.gap.reason !== 'unavailable')
|
|
295
|
+
globalGaps.push(dep.gap);
|
|
296
|
+
}
|
|
136
297
|
const candidateIds = new Set(candidates.map(n => n.id));
|
|
137
298
|
const edges = [];
|
|
138
299
|
const edgeKeys = new Set();
|
|
@@ -157,6 +318,16 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
157
318
|
edges.push(edge);
|
|
158
319
|
}
|
|
159
320
|
}
|
|
321
|
+
for (const group of contextGroups)
|
|
322
|
+
for (const edge of group.edges) {
|
|
323
|
+
if (edge.kind === 'imports')
|
|
324
|
+
continue;
|
|
325
|
+
const key = (0, evidence_paths_1.evidenceEdgeKey)(edge);
|
|
326
|
+
if (!edgeKeys.has(key)) {
|
|
327
|
+
edgeKeys.add(key);
|
|
328
|
+
edges.push(edge);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
160
331
|
if (search) {
|
|
161
332
|
const fallbackNeighbors = search.missing.length || search.goal.anchorIds.length < 2 ? search.neighbors : [];
|
|
162
333
|
for (const edge of [...search.paths.flatMap(p => p.edges), ...fallbackNeighbors]) {
|
|
@@ -193,7 +364,7 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
193
364
|
for (const [i, edge] of edges.entries()) {
|
|
194
365
|
const from = graph.getNode(edge.source);
|
|
195
366
|
const to = graph.getNode(edge.target);
|
|
196
|
-
if (!from || !to || !local(from) || !local(to)) {
|
|
367
|
+
if (!from || !to || (!local(from) && from.kind !== 'route') || (!local(to) && to.kind !== 'route')) {
|
|
197
368
|
globalGaps.push({ target: `${from ? label(from) : edge.source} ${edge.kind} ${to ? label(to) : edge.target}`,
|
|
198
369
|
reason: 'unavailable', nextAnchor: from ? location(from) : edge.source });
|
|
199
370
|
continue;
|
|
@@ -266,6 +437,19 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
266
437
|
globalGaps.push({ target: `connecting path in the bounded graph slice: ${label(relationFocus[0])} → ${label(disconnected[0])}`,
|
|
267
438
|
reason: 'unindexed_connection', nextAnchor: location(disconnected[0]) });
|
|
268
439
|
}
|
|
440
|
+
// Rank already-read, hash-verified declarations by joint targets in the same file.
|
|
441
|
+
// Never discard a dependent helper merely because it has no UI label.
|
|
442
|
+
if (inspector && targetChecks) {
|
|
443
|
+
const byFile = new Map();
|
|
444
|
+
for (const unit of sources.values()) {
|
|
445
|
+
const list = byFile.get(unit.node.filePath) ?? [];
|
|
446
|
+
list.push(requestUnit(unit));
|
|
447
|
+
byFile.set(unit.node.filePath, list);
|
|
448
|
+
}
|
|
449
|
+
const scores = new Map([...byFile].map(([file, units]) => [file, inspector.score(units)]));
|
|
450
|
+
for (const pack of packs)
|
|
451
|
+
pack.priority += 30 * Math.max(0, ...pack.sources.map(id => scores.get(sources.get(id).node.filePath) ?? 0));
|
|
452
|
+
}
|
|
269
453
|
const selected = [];
|
|
270
454
|
const rejectedGap = (p) => p.gaps.length ? p.gaps : [{ target: p.label, reason: 'budget',
|
|
271
455
|
nextAnchor: sources.get(p.sources[0] ?? '') ? location(sources.get(p.sources[0]).node) : p.label }];
|
|
@@ -280,6 +464,12 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
280
464
|
return units.filter(s => !units.some(other => other.id !== s.id && other.node.filePath === s.node.filePath
|
|
281
465
|
&& other.start <= s.start && other.end >= s.end)).sort((a, b) => a.node.filePath.localeCompare(b.node.filePath) || a.start - b.start);
|
|
282
466
|
};
|
|
467
|
+
const controlCache = new Map();
|
|
468
|
+
const controlsFor = (chosen) => unitsFor(chosen).flatMap(unit => {
|
|
469
|
+
if (!controlCache.has(unit))
|
|
470
|
+
controlCache.set(unit, (0, request_evidence_1.inspectControlEvidence)([requestUnit(unit)]));
|
|
471
|
+
return controlCache.get(unit);
|
|
472
|
+
}).slice(0, 8);
|
|
283
473
|
const relationsFor = (chosen) => [...new Map(chosen.flatMap(p => p.path?.relations ?? (p.relation ? [p.relation] : []))
|
|
284
474
|
.map(r => [JSON.stringify(r), r])).values()];
|
|
285
475
|
const stopFor = (chosen) => search?.stopReason === 'supported'
|
|
@@ -290,6 +480,16 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
290
480
|
'Complete declarations preserve internal branches. Enclosing callers and runtime order/value flow are not proven. Retrieval is not task completion.'];
|
|
291
481
|
if (search)
|
|
292
482
|
lines.push(`Path goal: ${search.goal.kind}; search direction: ${search.goal.direction}; stop: ${stopFor(chosen)}. Static paths do not prove runtime behavior.`);
|
|
483
|
+
if (inspector)
|
|
484
|
+
lines.push(...(0, request_evidence_1.renderRequestEvidence)(inspector.inspect(unitsFor(chosen).map(requestUnit)), targetChecks, behaviorChecks));
|
|
485
|
+
if ((0, implementation_context_1.controlEvidenceEnabled)())
|
|
486
|
+
lines.push(...(0, request_evidence_1.renderControlEvidence)(controlsFor(chosen)));
|
|
487
|
+
for (const pack of chosen.filter(p => p.context)) {
|
|
488
|
+
lines.push(`**Implementation context: ${inline(pack.context.label)}**`, ...pack.context.notes.map(n => `- ${inline(n)}`));
|
|
489
|
+
const sdk = pack.context.sdk;
|
|
490
|
+
if (sdk)
|
|
491
|
+
lines.push(`Indexed SDK signatures for ${inline(sdk.module)}; database version ${inline(sdk.version)}. Not source bodies or device validation.`, ...sdk.nodes.map(n => `- ${inline(n.filePath)}:${n.startLine} ${inline(n.signature || n.name)}${n.docstring ? ` — ${inline(n.docstring).slice(0, 500)}${n.docstring.length > 500 ? ' [documentation excerpt; remaining constraints not shown]' : ''}` : ''}`));
|
|
492
|
+
}
|
|
293
493
|
const chosenPaths = chosen.filter(p => p.path);
|
|
294
494
|
if (chosenPaths.length)
|
|
295
495
|
lines.push('**Directed paths (all source dependencies provided)**', ...chosenPaths.map(p => `- ${inline(p.label)}`));
|
|
@@ -303,7 +503,7 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
303
503
|
for (const unit of unitsFor(chosen)) {
|
|
304
504
|
// Fence longer than any source backtick run: source-like guidance cannot escape.
|
|
305
505
|
const fence = '`'.repeat(Math.max(3, ...Array.from(unit.source.matchAll(/`+/g), m => m[0].length + 1)));
|
|
306
|
-
lines.push(`### ${inline(unit.node.filePath)}:${unit.start}-${unit.end} — complete declaration; file-fingerprint=${unit.fingerprint}`, fence + unit.node.language, unit.source.split('\n').map((line, i) => `${unit.start + i}\t${line}`).join('\n'), fence);
|
|
506
|
+
lines.push(`### ${inline(unit.node.filePath)}:${unit.start}-${unit.end} — ${unit.snapshot ? 'current configuration snapshot' : unit.node.kind === 'file' ? 'complete indexed file' : 'complete declaration'}; file-fingerprint=${unit.fingerprint}`, fence + unit.node.language, unit.source.split('\n').map((line, i) => `${unit.start + i}\t${line}`).join('\n'), fence);
|
|
307
507
|
}
|
|
308
508
|
if (gaps.length) {
|
|
309
509
|
lines.push('**Gaps — inspect only the missing evidence needed for the task**', ...gaps.slice(0, 6).map(g => `- ${g.reason}: ${inline(g.target)}. Next anchor: ${inline(g.nextAnchor)}.`));
|
|
@@ -340,11 +540,20 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
340
540
|
const locatedNodes = candidates.filter(n => units.some(s => s.node.filePath === n.filePath && s.start <= n.startLine && s.end >= n.endLine))
|
|
341
541
|
.map(n => ({ id: n.id, name: n.name, qualifiedName: n.qualifiedName, filePath: n.filePath, startLine: n.startLine }));
|
|
342
542
|
const status = !units.length ? 'empty' : gaps.length ? 'partial' : 'complete';
|
|
543
|
+
const checkedRequest = inspector?.inspect(units.map(requestUnit));
|
|
544
|
+
const requestEvidence = checkedRequest ? { ...checkedRequest, targets: targetChecks ? checkedRequest.targets : [],
|
|
545
|
+
behaviors: behaviorChecks ? checkedRequest.behaviors : [] } : undefined;
|
|
343
546
|
return { text, emission: { projectRoot: options.projectRoot, query: options.query, files,
|
|
344
547
|
sourceBytes: files.reduce((sum, f) => sum + f.bytes, 0), responseBytes: text.length, locatedNodes,
|
|
345
548
|
evidenceStatus: status, partial: status !== 'complete', coveredObligations: selected.map(p => p.label),
|
|
346
549
|
uncoveredObligations: gaps.map(g => `${g.reason}: ${g.target}`), nextAnchor: gaps[0]?.nextAnchor },
|
|
347
|
-
metadata: {
|
|
550
|
+
metadata: { ...((0, implementation_context_1.controlEvidenceEnabled)() ? { controls: controlsFor(selected) } : {}),
|
|
551
|
+
...(contextEnabled ? { implementationContext: selected.flatMap(p => p.context ? [{ ...p.context,
|
|
552
|
+
...(p.context.sdk ? { sdk: { ...p.context.sdk, nodes: p.context.sdk.nodes.map(n => ({
|
|
553
|
+
id: n.id, name: n.name, qualifiedName: n.qualifiedName, kind: n.kind, filePath: n.filePath,
|
|
554
|
+
startLine: n.startLine, endLine: n.endLine, startColumn: n.startColumn, endColumn: n.endColumn,
|
|
555
|
+
language: n.language, updatedAt: n.updatedAt, signature: n.signature,
|
|
556
|
+
})) } } : {}) }] : []) } : {}), ...(requestEvidence ? { requestEvidence } : {}), version: search ? 2 : 1, scope: 'bounded_static_evidence', status, selectedPacks: selected.map(p => p.id), gaps,
|
|
348
557
|
relations: relationsFor(selected), ...(search ? { pathSearch: { goal: search.goal, stopReason: stopFor(selected),
|
|
349
558
|
limitsHit: search.limitsHit, stats: search.stats, paths: search.paths.map(p => ({ id: p.id, nodeIds: p.nodeIds, cost: p.cost,
|
|
350
559
|
evidence: selected.some(c => c.path?.id === p.id) ? 'provided' : 'partial' })) } } : {}) } };
|
package/dist/mcp/daemon.d.ts
CHANGED
|
@@ -20,9 +20,12 @@
|
|
|
20
20
|
* so a SIGKILL'd host still reaps its proxy promptly; the proxy's socket
|
|
21
21
|
* close then decrements the daemon's refcount.
|
|
22
22
|
* - When the last client disconnects the daemon lingers for
|
|
23
|
-
* `HOMEGRAPH_DAEMON_IDLE_TIMEOUT_MS` (default
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* `HOMEGRAPH_DAEMON_IDLE_TIMEOUT_MS` (default **60s**, Spec 0047) so a
|
|
24
|
+
* quick reconnect does not repay startup — but **only after** any in-flight
|
|
25
|
+
* auto-init/full index finishes (`building_fast` / `indexing`). While a
|
|
26
|
+
* client is connected (refcount > 0) the daemon never idle-exits. This is
|
|
27
|
+
* what keeps a single-agent session from leaking a daemon forever (#277)
|
|
28
|
+
* without killing a half-built graph when the IDE just closed.
|
|
26
29
|
*
|
|
27
30
|
* What this file owns:
|
|
28
31
|
* - Listening on the daemon socket and spawning per-connection sessions.
|
|
@@ -41,6 +44,21 @@
|
|
|
41
44
|
*/
|
|
42
45
|
import * as net from 'net';
|
|
43
46
|
import { DaemonLockInfo } from './daemon-paths';
|
|
47
|
+
/**
|
|
48
|
+
* Spec 0047: phases that block daemon idle-exit when clients=0.
|
|
49
|
+
* Routine watch/sync is NOT included.
|
|
50
|
+
*/
|
|
51
|
+
export declare function isBuildPhaseBlockingIdle(phase: string | null | undefined): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Decide idle-timer action when the linger timer fires (refcount already 0).
|
|
54
|
+
* Pure helper for unit tests.
|
|
55
|
+
*/
|
|
56
|
+
export declare function decideIdleExitAction(opts: {
|
|
57
|
+
clientCount: number;
|
|
58
|
+
buildBlocking: boolean;
|
|
59
|
+
}): 'exit' | 'rearm-idle' | 'poll-build';
|
|
60
|
+
/** Default idle linger after last client (exported for tests). */
|
|
61
|
+
export declare function defaultDaemonIdleTimeoutMs(): number;
|
|
44
62
|
/**
|
|
45
63
|
* Finalize daemon shutdown. On POSIX, exit immediately — it's clean and fast.
|
|
46
64
|
* On Windows, do NOT force an exit while watchers may still be closing (that
|
package/dist/mcp/daemon.js
CHANGED
|
@@ -21,9 +21,12 @@
|
|
|
21
21
|
* so a SIGKILL'd host still reaps its proxy promptly; the proxy's socket
|
|
22
22
|
* close then decrements the daemon's refcount.
|
|
23
23
|
* - When the last client disconnects the daemon lingers for
|
|
24
|
-
* `HOMEGRAPH_DAEMON_IDLE_TIMEOUT_MS` (default
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* `HOMEGRAPH_DAEMON_IDLE_TIMEOUT_MS` (default **60s**, Spec 0047) so a
|
|
25
|
+
* quick reconnect does not repay startup — but **only after** any in-flight
|
|
26
|
+
* auto-init/full index finishes (`building_fast` / `indexing`). While a
|
|
27
|
+
* client is connected (refcount > 0) the daemon never idle-exits. This is
|
|
28
|
+
* what keeps a single-agent session from leaking a daemon forever (#277)
|
|
29
|
+
* without killing a half-built graph when the IDE just closed.
|
|
27
30
|
*
|
|
28
31
|
* What this file owns:
|
|
29
32
|
* - Listening on the daemon socket and spawning per-connection sessions.
|
|
@@ -75,6 +78,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
75
78
|
})();
|
|
76
79
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
77
80
|
exports.MAX_HELLO_LINE_BYTES = exports.Daemon = void 0;
|
|
81
|
+
exports.isBuildPhaseBlockingIdle = isBuildPhaseBlockingIdle;
|
|
82
|
+
exports.decideIdleExitAction = decideIdleExitAction;
|
|
83
|
+
exports.defaultDaemonIdleTimeoutMs = defaultDaemonIdleTimeoutMs;
|
|
78
84
|
exports.finalizeDaemonExit = finalizeDaemonExit;
|
|
79
85
|
exports.tryAcquireDaemonLock = tryAcquireDaemonLock;
|
|
80
86
|
exports.acquireLockViaExclusiveOpen = acquireLockViaExclusiveOpen;
|
|
@@ -92,8 +98,32 @@ const transport_1 = require("./transport");
|
|
|
92
98
|
const daemon_paths_1 = require("./daemon-paths");
|
|
93
99
|
const version_1 = require("./version");
|
|
94
100
|
const daemon_registry_1 = require("./daemon-registry");
|
|
95
|
-
/**
|
|
96
|
-
const
|
|
101
|
+
/** How often to re-check build phase while deferring idle exit (Spec 0047). */
|
|
102
|
+
const BUILD_IDLE_POLL_MS = 2_000;
|
|
103
|
+
/** Default idle linger after the last client disconnects (Spec 0047: 60s). */
|
|
104
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 60_000;
|
|
105
|
+
/**
|
|
106
|
+
* Spec 0047: phases that block daemon idle-exit when clients=0.
|
|
107
|
+
* Routine watch/sync is NOT included.
|
|
108
|
+
*/
|
|
109
|
+
function isBuildPhaseBlockingIdle(phase) {
|
|
110
|
+
return phase === 'building_fast' || phase === 'indexing';
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Decide idle-timer action when the linger timer fires (refcount already 0).
|
|
114
|
+
* Pure helper for unit tests.
|
|
115
|
+
*/
|
|
116
|
+
function decideIdleExitAction(opts) {
|
|
117
|
+
if (opts.clientCount > 0)
|
|
118
|
+
return 'rearm-idle';
|
|
119
|
+
if (opts.buildBlocking)
|
|
120
|
+
return 'poll-build';
|
|
121
|
+
return 'exit';
|
|
122
|
+
}
|
|
123
|
+
/** Default idle linger after last client (exported for tests). */
|
|
124
|
+
function defaultDaemonIdleTimeoutMs() {
|
|
125
|
+
return DEFAULT_IDLE_TIMEOUT_MS;
|
|
126
|
+
}
|
|
97
127
|
/**
|
|
98
128
|
* Hard ceiling on how long the daemon stays up with clients connected but no
|
|
99
129
|
* inbound traffic. A backstop (#692): if a client's socket-close is never
|
|
@@ -372,15 +402,39 @@ class Daemon {
|
|
|
372
402
|
return;
|
|
373
403
|
if (this.idleTimeoutMs <= 0)
|
|
374
404
|
return; // 0 = never idle-exit
|
|
405
|
+
// Spec 0047: if the last window closed mid-index, wait for build to finish
|
|
406
|
+
// before starting the idle linger — never exit while clients>0 either.
|
|
407
|
+
if (this.clients.size === 0 && this.engine.isIndexBuildInProgress()) {
|
|
408
|
+
this.idleTimer = setTimeout(() => {
|
|
409
|
+
this.idleTimer = null;
|
|
410
|
+
if (this.clients.size > 0)
|
|
411
|
+
return; // reconnect disarmed the need
|
|
412
|
+
if (this.engine.isIndexBuildInProgress()) {
|
|
413
|
+
this.armIdleTimer(); // keep polling until build settles
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
this.armIdleTimer(); // build done → arm full idle linger
|
|
417
|
+
}, BUILD_IDLE_POLL_MS);
|
|
418
|
+
this.idleTimer.unref?.();
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
375
421
|
this.idleTimer = setTimeout(() => {
|
|
376
422
|
this.idleTimer = null;
|
|
377
423
|
// Last-second sanity check: if a connection landed between the timer
|
|
378
424
|
// firing and now, don't exit. (setImmediate-ordering is the only way
|
|
379
425
|
// this races; cheap to defend against.)
|
|
380
|
-
|
|
426
|
+
const action = decideIdleExitAction({
|
|
427
|
+
clientCount: this.clients.size,
|
|
428
|
+
buildBlocking: this.engine.isIndexBuildInProgress(),
|
|
429
|
+
});
|
|
430
|
+
if (action === 'rearm-idle') {
|
|
381
431
|
this.armIdleTimer();
|
|
382
432
|
return;
|
|
383
433
|
}
|
|
434
|
+
if (action === 'poll-build') {
|
|
435
|
+
this.armIdleTimer(); // switches to build-poll branch above
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
384
438
|
void this.stop('idle timeout');
|
|
385
439
|
}, this.idleTimeoutMs);
|
|
386
440
|
// Don't keep the event loop alive just for this — the net.Server keeps the
|
package/dist/mcp/engine.d.ts
CHANGED
|
@@ -48,6 +48,13 @@ export declare class MCPEngine {
|
|
|
48
48
|
private opts;
|
|
49
49
|
private closed;
|
|
50
50
|
private queryPool;
|
|
51
|
+
/**
|
|
52
|
+
* Spec 0049: auto-init deferred because the root was empty. Probe until
|
|
53
|
+
* indexable, then run the normal create-DB path once.
|
|
54
|
+
*/
|
|
55
|
+
private deferredAutoInitRoot;
|
|
56
|
+
private deferProbeTimer;
|
|
57
|
+
private deferAutoInitPromise;
|
|
51
58
|
constructor(opts?: MCPEngineOptions);
|
|
52
59
|
/**
|
|
53
60
|
* Start the worker-thread query pool once a default project is open (daemon
|
|
@@ -67,8 +74,21 @@ export declare class MCPEngine {
|
|
|
67
74
|
getProjectPath(): string | null;
|
|
68
75
|
/** Shared ToolHandler — sessions delegate tool dispatch through this. */
|
|
69
76
|
getToolHandler(): ToolHandler;
|
|
77
|
+
/**
|
|
78
|
+
* Spec 0047: true while auto-init / full symbol build is in flight.
|
|
79
|
+
* Used by the daemon idle timer — never idle-exit mid-build when clients=0.
|
|
80
|
+
* Does not include routine watch/sync increments.
|
|
81
|
+
*/
|
|
82
|
+
isIndexBuildInProgress(): boolean;
|
|
70
83
|
/** Whether the default project's HomeGraph is open. */
|
|
71
84
|
hasDefaultHomeGraph(): boolean;
|
|
85
|
+
/** Spec 0049: true while auto-init is waiting for an empty root to grow sources. */
|
|
86
|
+
isAutoInitDeferred(): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Spec 0049: on tool call (or timer), re-check the deferred root and start
|
|
89
|
+
* the normal auto-init once it becomes indexable. Idempotent.
|
|
90
|
+
*/
|
|
91
|
+
kickDeferredAutoInit(): Promise<boolean>;
|
|
72
92
|
/**
|
|
73
93
|
* Walk up from `searchFrom` to find the nearest `.homegraph/` and open it.
|
|
74
94
|
* Idempotent: concurrent callers share one in-flight init; subsequent
|
|
@@ -100,6 +120,11 @@ export declare class MCPEngine {
|
|
|
100
120
|
* @returns true if the engine is fully wired to a new (or racing) index
|
|
101
121
|
*/
|
|
102
122
|
private tryAutoInit;
|
|
123
|
+
/** Spec 0049: create DB + fast map + background full (shared by first hit and kick). */
|
|
124
|
+
private runAutoInitCreate;
|
|
125
|
+
/** Spec 0049: remember empty root and (optionally) poll until indexable. */
|
|
126
|
+
private armDeferProbe;
|
|
127
|
+
private clearDeferProbe;
|
|
103
128
|
/**
|
|
104
129
|
* Open an index another process just created (Spec 0032 cold-start race).
|
|
105
130
|
* Brief retries cover the window between mkdir and db file create.
|
|
@@ -143,6 +168,7 @@ export declare class MCPEngine {
|
|
|
143
168
|
* Range: 1s … 30min. Used by product hosts (e.g. DevEco Code = 5 minutes).
|
|
144
169
|
*/
|
|
145
170
|
export declare function parseFixedWindowEnv(raw: string | undefined): number | undefined;
|
|
171
|
+
export { isIndexableRoot, parseDeferProbeMs } from './indexable-root';
|
|
146
172
|
/**
|
|
147
173
|
* Parse and clamp the HOMEGRAPH_WATCH_DEBOUNCE_MS env override.
|
|
148
174
|
*
|