homegraph 1.5.10 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +64 -14
  3. package/dist/db/queries.d.ts +4 -0
  4. package/dist/db/queries.js +15 -0
  5. package/dist/extraction/grammars.d.ts +19 -0
  6. package/dist/extraction/grammars.js +44 -1
  7. package/dist/graph/evidence-paths.d.ts +2 -0
  8. package/dist/graph/evidence-paths.js +20 -3
  9. package/dist/mcp/arkts-evidence-packs.d.ts +17 -0
  10. package/dist/mcp/arkts-evidence-packs.js +219 -10
  11. package/dist/mcp/daemon.d.ts +21 -3
  12. package/dist/mcp/daemon.js +60 -6
  13. package/dist/mcp/engine.d.ts +26 -0
  14. package/dist/mcp/engine.js +143 -7
  15. package/dist/mcp/evidence-audit.d.ts +3 -0
  16. package/dist/mcp/evidence-audit.js +40 -0
  17. package/dist/mcp/implementation-context.d.ts +35 -0
  18. package/dist/mcp/implementation-context.js +232 -0
  19. package/dist/mcp/index-availability.d.ts +22 -0
  20. package/dist/mcp/index-availability.js +40 -1
  21. package/dist/mcp/index.js +9 -0
  22. package/dist/mcp/indexable-root.d.ts +22 -0
  23. package/dist/mcp/indexable-root.js +140 -0
  24. package/dist/mcp/liveness-watchdog.d.ts +6 -1
  25. package/dist/mcp/liveness-watchdog.js +17 -5
  26. package/dist/mcp/locate-contract.d.ts +50 -0
  27. package/dist/mcp/locate-contract.js +146 -0
  28. package/dist/mcp/query-cache.d.ts +1 -15
  29. package/dist/mcp/query-cache.js +20 -11
  30. package/dist/mcp/request-evidence.d.ts +69 -0
  31. package/dist/mcp/request-evidence.js +297 -0
  32. package/dist/mcp/server-instructions.d.ts +2 -2
  33. package/dist/mcp/server-instructions.js +12 -9
  34. package/dist/mcp/session.js +15 -0
  35. package/dist/mcp/tools.d.ts +67 -12
  36. package/dist/mcp/tools.js +841 -134
  37. package/dist/project-map/index.d.ts +45 -0
  38. package/dist/project-map/index.js +371 -3
  39. package/dist/resolution/callback-synthesizer.js +251 -0
  40. package/dist/resolution/frameworks/arkts-entry.d.ts +35 -6
  41. package/dist/resolution/frameworks/arkts-entry.js +513 -30
  42. package/dist/resolution/index.js +25 -21
  43. package/dist/runtime-log.d.ts +52 -0
  44. package/dist/runtime-log.js +199 -0
  45. package/dist/search/literal-evidence.js +11 -3
  46. package/dist/search/query-plan-provider.js +15 -6
  47. package/dist/search/query-plan.d.ts +3 -1
  48. package/dist/search/query-plan.js +25 -16
  49. package/dist/search/query-utils.d.ts +13 -0
  50. package/dist/search/query-utils.js +64 -0
  51. package/dist/search/request-contract.d.ts +29 -0
  52. package/dist/search/request-contract.js +76 -0
  53. package/package.json +1 -1
@@ -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 DECLARATIONS = new Set(['class', 'struct', 'component', 'interface', 'method', 'function', 'property', 'field', 'constant', 'variable', 'enum']);
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: { version: search ? 2 : 1, scope: 'bounded_static_evidence', status, selectedPacks: selected.map(p => p.id), gaps,
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' })) } } : {}) } };
@@ -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 300s) so back-to-back agent
24
- * runs in the same project don't repay startup, then exits cleanly. This is
25
- * what keeps a single-agent session from leaking a daemon forever (#277).
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
@@ -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 300s) so back-to-back agent
25
- * runs in the same project don't repay startup, then exits cleanly. This is
26
- * what keeps a single-agent session from leaking a daemon forever (#277).
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
- /** Default idle linger after the last client disconnects. */
96
- const DEFAULT_IDLE_TIMEOUT_MS = 300_000;
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
- if (this.clients.size > 0) {
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
@@ -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
  *