homegraph 1.6.0 → 1.6.2
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 +31 -0
- package/README.md +64 -14
- package/dist/bin/homegraph.js +2 -2
- package/dist/db/index.js +1 -0
- package/dist/db/migrations.d.ts +6 -1
- package/dist/db/migrations.js +26 -1
- package/dist/db/queries.d.ts +9 -0
- package/dist/db/queries.js +32 -7
- package/dist/db/schema.sql +4 -0
- package/dist/extraction/grammars.js +2 -1
- package/dist/extraction/languages/c-cpp.d.ts +8 -0
- package/dist/extraction/languages/c-cpp.js +44 -0
- package/dist/extraction/wasm-runtime-flags.d.ts +4 -0
- package/dist/extraction/wasm-runtime-flags.js +9 -0
- package/dist/graph/evidence-paths.d.ts +2 -0
- package/dist/graph/evidence-paths.js +20 -3
- package/dist/index.js +7 -1
- package/dist/installer/targets/icode.d.ts +43 -0
- package/dist/installer/targets/icode.js +631 -0
- package/dist/installer/targets/registry.js +2 -0
- package/dist/installer/targets/types.d.ts +1 -1
- package/dist/mcp/arkts-evidence-packs.d.ts +17 -0
- package/dist/mcp/arkts-evidence-packs.js +218 -10
- package/dist/mcp/evidence-audit.d.ts +3 -0
- package/dist/mcp/evidence-audit.js +40 -0
- package/dist/mcp/evidence-rendering.js +1 -1
- package/dist/mcp/explore-repeat-guard.d.ts +19 -5
- package/dist/mcp/explore-repeat-guard.js +35 -14
- package/dist/mcp/explore-session-state.d.ts +5 -0
- package/dist/mcp/explore-session-state.js +1 -0
- package/dist/mcp/implementation-context.d.ts +35 -0
- package/dist/mcp/implementation-context.js +232 -0
- package/dist/mcp/locate-contract.js +1 -1
- 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 +15 -15
- package/dist/mcp/session.js +3 -2
- package/dist/mcp/tools.d.ts +39 -12
- package/dist/mcp/tools.js +237 -121
- package/dist/resolution/c-feature-variants.d.ts +40 -0
- package/dist/resolution/c-feature-variants.js +272 -0
- package/dist/resolution/cpp-macro-visibility.d.ts +21 -0
- package/dist/resolution/cpp-macro-visibility.js +393 -0
- package/dist/resolution/index.js +11 -0
- package/dist/resolution/name-matcher.js +49 -2
- package/dist/resolution/types.d.ts +8 -0
- package/dist/resolution/types.js +8 -0
- package/dist/search/literal-evidence.js +11 -3
- package/dist/search/query-plan-provider.js +13 -5
- package/dist/search/query-plan.d.ts +3 -1
- package/dist/search/query-plan.js +11 -4
- package/dist/search/request-contract.d.ts +29 -0
- package/dist/search/request-contract.js +76 -0
- package/dist/sync/watcher.d.ts +30 -0
- package/dist/sync/watcher.js +45 -0
- package/package.json +1 -1
|
@@ -17,7 +17,7 @@ export type Location = 'global' | 'local';
|
|
|
17
17
|
* lookup. New targets add a value here when they're added to the
|
|
18
18
|
* registry. Keep these short and lowercase.
|
|
19
19
|
*/
|
|
20
|
-
export type TargetId = 'claude' | 'cursor' | 'codex' | 'opencode' | 'deveco' | 'codebuddy' | 'hermes' | 'gemini' | 'antigravity' | 'kiro';
|
|
20
|
+
export type TargetId = 'claude' | 'cursor' | 'codex' | 'opencode' | 'deveco' | 'codebuddy' | 'hermes' | 'gemini' | 'antigravity' | 'kiro' | 'icode';
|
|
21
21
|
/**
|
|
22
22
|
* Result of `target.detect(location)`.
|
|
23
23
|
*
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { type SdkModule } from './implementation-context';
|
|
2
|
+
import { type RequestContract } from '../search/request-contract';
|
|
3
|
+
import { type RequestEvidence, type ControlEvidence } from './request-evidence';
|
|
1
4
|
import type HomeGraph from '../index';
|
|
2
5
|
import type { Edge, Node } from '../types';
|
|
3
6
|
import type { ExploreEmission } from './explore-session-state';
|
|
@@ -22,6 +25,15 @@ interface EvidencePack {
|
|
|
22
25
|
sources: string[];
|
|
23
26
|
priority: number;
|
|
24
27
|
gaps: EvidenceGap[];
|
|
28
|
+
context?: {
|
|
29
|
+
label: string;
|
|
30
|
+
notes: string[];
|
|
31
|
+
sdk?: {
|
|
32
|
+
module: string;
|
|
33
|
+
version: string;
|
|
34
|
+
nodes: Node[];
|
|
35
|
+
};
|
|
36
|
+
};
|
|
25
37
|
relation?: RelationEvidence;
|
|
26
38
|
path?: {
|
|
27
39
|
id: string;
|
|
@@ -36,6 +48,9 @@ export interface ArktsEvidenceResult {
|
|
|
36
48
|
version: 1 | 2;
|
|
37
49
|
scope: 'bounded_static_evidence';
|
|
38
50
|
status: 'complete' | 'partial' | 'empty';
|
|
51
|
+
requestEvidence?: RequestEvidence;
|
|
52
|
+
controls?: ControlEvidence[];
|
|
53
|
+
implementationContext?: Array<NonNullable<EvidencePack['context']>>;
|
|
39
54
|
selectedPacks: string[];
|
|
40
55
|
gaps: EvidenceGap[];
|
|
41
56
|
relations: NonNullable<EvidencePack['relation']>[];
|
|
@@ -62,6 +77,8 @@ export declare function buildArktsEvidencePacks(graph: Pick<HomeGraph, 'getNode'
|
|
|
62
77
|
maxChars: number;
|
|
63
78
|
maxFiles?: number;
|
|
64
79
|
pathSearch?: EvidencePathSearch;
|
|
80
|
+
requestContract?: RequestContract;
|
|
81
|
+
sdkModule?: (module: string) => SdkModule | undefined;
|
|
65
82
|
}): ArktsEvidenceResult | null;
|
|
66
83
|
export {};
|
|
67
84
|
//# sourceMappingURL=arkts-evidence-packs.d.ts.map
|
|
@@ -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, ' ');
|
|
@@ -22,16 +60,32 @@ const artifact = (n) => n.filePath.includes('@dummy') || n.name.startsWith('@dum
|
|
|
22
60
|
|| (n.name === 'constructor' && n.startLine === 1 && n.endLine === 1);
|
|
23
61
|
/** Consume already-located nodes. No text search, new planner call or recursive retrieval. */
|
|
24
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;
|
|
25
65
|
if (!options.nodes.some(n => local(n) && /\.ets$/i.test(n.filePath) && DECLARATIONS.has(n.kind)))
|
|
26
66
|
return null;
|
|
27
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
|
+
};
|
|
28
81
|
const requiredIds = search ? new Set([...search.goal.anchorIds, ...search.paths.flatMap(p => p.nodeIds)]) : options.focusIds;
|
|
29
82
|
const inputNodes = search && requiredIds.size ? [...requiredIds].flatMap(id => { const n = graph.getNode(id); return n ? [n] : []; }) : options.nodes;
|
|
30
83
|
const all = (0, evidence_rendering_1.canonicalSourceDeclarations)(inputNodes.filter(n => DECLARATIONS.has(n.kind) && local(n) && !artifact(n)));
|
|
31
84
|
if (!all.length)
|
|
32
85
|
return null;
|
|
86
|
+
const inputOrder = new Map(all.map((node, index) => [node.id, index]));
|
|
33
87
|
const candidates = all.sort((a, b) => Number(requiredIds.has(b.id)) - Number(requiredIds.has(a.id))
|
|
34
|
-
|| 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);
|
|
35
89
|
const focus = candidates.filter(n => requiredIds.has(n.id));
|
|
36
90
|
// A named owning type provides scope for its named members, not another
|
|
37
91
|
// callable endpoint. Do not invent a missing Type→method call obligation.
|
|
@@ -46,11 +100,11 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
46
100
|
let bytesRead = 0;
|
|
47
101
|
const root = (0, node_fs_1.realpathSync)(options.projectRoot);
|
|
48
102
|
const maxFiles = Math.max(1, Math.min(LIMITS.files, options.maxFiles ?? LIMITS.files));
|
|
49
|
-
const readSource = (node) => {
|
|
103
|
+
const readSource = (node, snapshot = false) => {
|
|
50
104
|
const previous = nodeSources.get(node.id);
|
|
51
105
|
if (previous)
|
|
52
106
|
return previous;
|
|
53
|
-
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)) {
|
|
54
108
|
const failure = { gap: { target: label(node), reason: 'unavailable', nextAnchor: location(node) } };
|
|
55
109
|
nodeSources.set(node.id, failure);
|
|
56
110
|
return failure;
|
|
@@ -90,7 +144,7 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
90
144
|
const content = buffer.subarray(0, bytes).toString('utf8');
|
|
91
145
|
const indexed = graph.getFile(node.filePath);
|
|
92
146
|
// Hash equality, not mtime: edits can retain timestamps and invalidate ranges.
|
|
93
|
-
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'))))
|
|
94
148
|
file.reason = 'stale';
|
|
95
149
|
else
|
|
96
150
|
file = { content, lines: content.split('\n'), fingerprint: (0, explore_dedup_1.fileFingerprint)(content) };
|
|
@@ -102,6 +156,14 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
102
156
|
}
|
|
103
157
|
fileCache.set(node.filePath, file);
|
|
104
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
|
+
}
|
|
105
167
|
const reason = file.reason ?? (!Number.isInteger(node.startLine) || !Number.isInteger(node.endLine)
|
|
106
168
|
|| node.startLine < 1 || node.endLine < node.startLine || node.endLine > file.lines.length
|
|
107
169
|
|| !file.lines.slice(node.startLine - 1, node.endLine).join('\n').trim() ? 'invalid_range' : undefined);
|
|
@@ -117,13 +179,13 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
117
179
|
const id = `${node.filePath}:${start}-${node.endLine}`;
|
|
118
180
|
if (!sources.has(id))
|
|
119
181
|
sources.set(id, { id, node, start, end: node.endLine,
|
|
120
|
-
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 } : {}) });
|
|
121
183
|
const result = { id };
|
|
122
184
|
nodeSources.set(node.id, result);
|
|
123
185
|
return result;
|
|
124
186
|
};
|
|
125
187
|
const dependencies = (nodes) => {
|
|
126
|
-
const resolved = nodes.map(readSource);
|
|
188
|
+
const resolved = nodes.map(n => readSource(n));
|
|
127
189
|
return { sources: [...new Set(resolved.flatMap(s => s.id ? [s.id] : []))],
|
|
128
190
|
gaps: resolved.flatMap(s => s.gap ? [s.gap] : []) };
|
|
129
191
|
};
|
|
@@ -134,6 +196,104 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
134
196
|
if (all.length > candidates.length)
|
|
135
197
|
globalGaps.push({ target: `${all.length - candidates.length} additional candidates`,
|
|
136
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
|
+
}
|
|
137
297
|
const candidateIds = new Set(candidates.map(n => n.id));
|
|
138
298
|
const edges = [];
|
|
139
299
|
const edgeKeys = new Set();
|
|
@@ -158,6 +318,16 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
158
318
|
edges.push(edge);
|
|
159
319
|
}
|
|
160
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
|
+
}
|
|
161
331
|
if (search) {
|
|
162
332
|
const fallbackNeighbors = search.missing.length || search.goal.anchorIds.length < 2 ? search.neighbors : [];
|
|
163
333
|
for (const edge of [...search.paths.flatMap(p => p.edges), ...fallbackNeighbors]) {
|
|
@@ -194,7 +364,7 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
194
364
|
for (const [i, edge] of edges.entries()) {
|
|
195
365
|
const from = graph.getNode(edge.source);
|
|
196
366
|
const to = graph.getNode(edge.target);
|
|
197
|
-
if (!from || !to || !local(from) || !local(to)) {
|
|
367
|
+
if (!from || !to || (!local(from) && from.kind !== 'route') || (!local(to) && to.kind !== 'route')) {
|
|
198
368
|
globalGaps.push({ target: `${from ? label(from) : edge.source} ${edge.kind} ${to ? label(to) : edge.target}`,
|
|
199
369
|
reason: 'unavailable', nextAnchor: from ? location(from) : edge.source });
|
|
200
370
|
continue;
|
|
@@ -267,6 +437,19 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
267
437
|
globalGaps.push({ target: `connecting path in the bounded graph slice: ${label(relationFocus[0])} → ${label(disconnected[0])}`,
|
|
268
438
|
reason: 'unindexed_connection', nextAnchor: location(disconnected[0]) });
|
|
269
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
|
+
}
|
|
270
453
|
const selected = [];
|
|
271
454
|
const rejectedGap = (p) => p.gaps.length ? p.gaps : [{ target: p.label, reason: 'budget',
|
|
272
455
|
nextAnchor: sources.get(p.sources[0] ?? '') ? location(sources.get(p.sources[0]).node) : p.label }];
|
|
@@ -281,6 +464,12 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
281
464
|
return units.filter(s => !units.some(other => other.id !== s.id && other.node.filePath === s.node.filePath
|
|
282
465
|
&& other.start <= s.start && other.end >= s.end)).sort((a, b) => a.node.filePath.localeCompare(b.node.filePath) || a.start - b.start);
|
|
283
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);
|
|
284
473
|
const relationsFor = (chosen) => [...new Map(chosen.flatMap(p => p.path?.relations ?? (p.relation ? [p.relation] : []))
|
|
285
474
|
.map(r => [JSON.stringify(r), r])).values()];
|
|
286
475
|
const stopFor = (chosen) => search?.stopReason === 'supported'
|
|
@@ -291,6 +480,16 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
291
480
|
'Complete declarations preserve internal branches. Enclosing callers and runtime order/value flow are not proven. Retrieval is not task completion.'];
|
|
292
481
|
if (search)
|
|
293
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
|
+
}
|
|
294
493
|
const chosenPaths = chosen.filter(p => p.path);
|
|
295
494
|
if (chosenPaths.length)
|
|
296
495
|
lines.push('**Directed paths (all source dependencies provided)**', ...chosenPaths.map(p => `- ${inline(p.label)}`));
|
|
@@ -304,7 +503,7 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
304
503
|
for (const unit of unitsFor(chosen)) {
|
|
305
504
|
// Fence longer than any source backtick run: source-like guidance cannot escape.
|
|
306
505
|
const fence = '`'.repeat(Math.max(3, ...Array.from(unit.source.matchAll(/`+/g), m => m[0].length + 1)));
|
|
307
|
-
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);
|
|
308
507
|
}
|
|
309
508
|
if (gaps.length) {
|
|
310
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)}.`));
|
|
@@ -341,11 +540,20 @@ function buildArktsEvidencePacks(graph, options) {
|
|
|
341
540
|
const locatedNodes = candidates.filter(n => units.some(s => s.node.filePath === n.filePath && s.start <= n.startLine && s.end >= n.endLine))
|
|
342
541
|
.map(n => ({ id: n.id, name: n.name, qualifiedName: n.qualifiedName, filePath: n.filePath, startLine: n.startLine }));
|
|
343
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;
|
|
344
546
|
return { text, emission: { projectRoot: options.projectRoot, query: options.query, files,
|
|
345
547
|
sourceBytes: files.reduce((sum, f) => sum + f.bytes, 0), responseBytes: text.length, locatedNodes,
|
|
346
548
|
evidenceStatus: status, partial: status !== 'complete', coveredObligations: selected.map(p => p.label),
|
|
347
549
|
uncoveredObligations: gaps.map(g => `${g.reason}: ${g.target}`), nextAnchor: gaps[0]?.nextAnchor },
|
|
348
|
-
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,
|
|
349
557
|
relations: relationsFor(selected), ...(search ? { pathSearch: { goal: search.goal, stopReason: stopFor(selected),
|
|
350
558
|
limitsHit: search.limitsHit, stats: search.stats, paths: search.paths.map(p => ({ id: p.id, nodeIds: p.nodeIds, cost: p.cost,
|
|
351
559
|
evidence: selected.some(c => c.path?.id === p.id) ? 'provided' : 'partial' })) } } : {}) } };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Opt-in local evidence only, never an answer cache. Logging cannot break retrieval. */
|
|
2
|
+
export declare function recordEvidencePack(projectRoot: string, input: unknown, output: unknown): 'written' | 'disabled' | 'limit' | 'unavailable';
|
|
3
|
+
//# sourceMappingURL=evidence-audit.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.recordEvidencePack = recordEvidencePack;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const utils_1 = require("../utils");
|
|
7
|
+
/** Opt-in local evidence only, never an answer cache. Logging cannot break retrieval. */
|
|
8
|
+
function recordEvidencePack(projectRoot, input, output) {
|
|
9
|
+
if (process.env.HOMEGRAPH_ARKTS_EVIDENCE_LOG !== '1')
|
|
10
|
+
return 'disabled';
|
|
11
|
+
let fd;
|
|
12
|
+
try {
|
|
13
|
+
const target = (0, utils_1.validatePathWithinRoot)(projectRoot, '.homegraph/evidence-packs.jsonl');
|
|
14
|
+
if (!target)
|
|
15
|
+
return 'unavailable';
|
|
16
|
+
const record = Buffer.from(JSON.stringify({ id: (0, node_crypto_1.randomUUID)(), time: new Date().toISOString(),
|
|
17
|
+
stage: 'arkts_pack_before_host_wrapping', version: 1, input, output }) + '\n');
|
|
18
|
+
fd = (0, node_fs_1.openSync)(target, node_fs_1.constants.O_WRONLY | node_fs_1.constants.O_CREAT | node_fs_1.constants.O_APPEND | node_fs_1.constants.O_NONBLOCK | (node_fs_1.constants.O_NOFOLLOW ?? 0), 0o600);
|
|
19
|
+
if (!(0, node_fs_1.fstatSync)(fd).isFile())
|
|
20
|
+
return 'unavailable';
|
|
21
|
+
if ((0, node_fs_1.fstatSync)(fd).size + record.length > 16 * 1024 * 1024)
|
|
22
|
+
return 'limit';
|
|
23
|
+
let at = 0;
|
|
24
|
+
while (at < record.length) {
|
|
25
|
+
const written = (0, node_fs_1.writeSync)(fd, record, at);
|
|
26
|
+
if (!written)
|
|
27
|
+
return 'unavailable';
|
|
28
|
+
at += written;
|
|
29
|
+
}
|
|
30
|
+
return 'written';
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return 'unavailable';
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
if (fd !== undefined)
|
|
37
|
+
(0, node_fs_1.closeSync)(fd);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=evidence-audit.js.map
|
|
@@ -52,7 +52,7 @@ function neutralRetrievalGuidance(text) {
|
|
|
52
52
|
return line
|
|
53
53
|
.replace(/\*{0,2}ANSWER NOW\*{0,2}/g, 'use the evidence shown')
|
|
54
54
|
.replace(/Flow \+ Source above are authoritative for this query\./g, 'Source coverage is limited to the displayed ranges.')
|
|
55
|
-
.replace(/Do \*\*not\*\* grep\/read
|
|
55
|
+
.replace(/Do \*\*not\*\* grep\/read\/(?:`homegraph_node`\/)?`homegraph_search`[^.]*\./gi, 'Inspect missing evidence as needed.')
|
|
56
56
|
.replace(/do\s+\*{0,2}not\*{0,2}\s+(?:re-)?(?:grep|read|glob)(?:\/[^.;]+)?[^.;]*[.;]/gi, 'Avoid duplicate reads of unchanged ranges;');
|
|
57
57
|
}).join('\n');
|
|
58
58
|
}
|
|
@@ -40,14 +40,28 @@ export declare function inferExplorePartialMeta(text: string): {
|
|
|
40
40
|
};
|
|
41
41
|
export interface ExploreRepeatDecision {
|
|
42
42
|
refuse: boolean;
|
|
43
|
-
/** Matched prior call when refusing for overlap. */
|
|
43
|
+
/** Matched prior call when refusing for overlap or cooldown. */
|
|
44
44
|
matched?: ExploreCallRecord;
|
|
45
|
-
reason: 'overlap' | 'call-budget' | '
|
|
45
|
+
reason: 'overlap' | 'call-budget' | 'cooldown' | 'next-anchor' | 'ok';
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* After `homegraph_explore` records a result (the response is about to be
|
|
49
|
+
* returned), refuse another explore on the same project until this elapses.
|
|
50
|
+
* There is no per-connection call ceiling — a later question on a long-lived
|
|
51
|
+
* MCP session must still be able to explore.
|
|
52
|
+
*
|
|
53
|
+
* Tool descriptions, initialize instructions, and explore footers must quote
|
|
54
|
+
* this same wait. `getExploreBudget` scales output size only.
|
|
55
|
+
*/
|
|
56
|
+
export declare const EXPLORE_COOLDOWN_MS = 10000;
|
|
57
|
+
/** Whole seconds shown to agents. Matches {@link EXPLORE_COOLDOWN_MS}. */
|
|
58
|
+
export declare function exploreCooldownSeconds(): number;
|
|
47
59
|
/**
|
|
48
60
|
* Whether this explore should short-refuse instead of re-running.
|
|
61
|
+
*
|
|
62
|
+
* `now` is injectable so tests can move past the cooldown without sleeping.
|
|
49
63
|
*/
|
|
50
|
-
export declare function decideExploreRepeat(prior: ExploreProjectState | null | undefined, query: string): ExploreRepeatDecision;
|
|
64
|
+
export declare function decideExploreRepeat(prior: ExploreProjectState | null | undefined, query: string, now?: number): ExploreRepeatDecision;
|
|
51
65
|
/** Short success-shaped refuse when a repeat explore would waste tokens. */
|
|
52
66
|
export declare function formatExploreRepeatRefuse(decision: ExploreRepeatDecision, query: string): string;
|
|
53
67
|
export interface DepthToolFuseDecision {
|
|
@@ -98,8 +112,8 @@ export declare function formatMechanismSoftCloseFooter(next?: DomainRoleAnchorCa
|
|
|
98
112
|
/** Honest Partial footer with a single Next anchor + ONE Grep. */
|
|
99
113
|
export declare function formatMechanismPartialFooter(nextCaption: string): string;
|
|
100
114
|
/**
|
|
101
|
-
* Appended when
|
|
102
|
-
*
|
|
115
|
+
* Appended when another Partial is emitted after one earlier Partial in this
|
|
116
|
+
* session. The wait is the return cooldown, not a connection-long stop.
|
|
103
117
|
*/
|
|
104
118
|
export declare function formatSecondPartialStopFooter(nextAnchor?: string): string;
|
|
105
119
|
/**
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Shape-driven only — no product nouns.
|
|
10
10
|
*/
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.EXPLORE_COOLDOWN_MS = void 0;
|
|
12
13
|
exports.exploreQueryTokens = exploreQueryTokens;
|
|
13
14
|
exports.queryTokenJaccard = queryTokenJaccard;
|
|
14
15
|
exports.queryTokenOverlapScore = queryTokenOverlapScore;
|
|
@@ -17,6 +18,7 @@ exports.novelAnchorsVsPriors = novelAnchorsVsPriors;
|
|
|
17
18
|
exports.isTightExploreFollowUp = isTightExploreFollowUp;
|
|
18
19
|
exports.extractNextAnchorFromText = extractNextAnchorFromText;
|
|
19
20
|
exports.inferExplorePartialMeta = inferExplorePartialMeta;
|
|
21
|
+
exports.exploreCooldownSeconds = exploreCooldownSeconds;
|
|
20
22
|
exports.decideExploreRepeat = decideExploreRepeat;
|
|
21
23
|
exports.formatExploreRepeatRefuse = formatExploreRepeatRefuse;
|
|
22
24
|
exports.latestCountedExplore = latestCountedExplore;
|
|
@@ -159,10 +161,19 @@ function inferExplorePartialMeta(text) {
|
|
|
159
161
|
};
|
|
160
162
|
}
|
|
161
163
|
/**
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
+
* After `homegraph_explore` records a result (the response is about to be
|
|
165
|
+
* returned), refuse another explore on the same project until this elapses.
|
|
166
|
+
* There is no per-connection call ceiling — a later question on a long-lived
|
|
167
|
+
* MCP session must still be able to explore.
|
|
168
|
+
*
|
|
169
|
+
* Tool descriptions, initialize instructions, and explore footers must quote
|
|
170
|
+
* this same wait. `getExploreBudget` scales output size only.
|
|
164
171
|
*/
|
|
165
|
-
|
|
172
|
+
exports.EXPLORE_COOLDOWN_MS = 10_000;
|
|
173
|
+
/** Whole seconds shown to agents. Matches {@link EXPLORE_COOLDOWN_MS}. */
|
|
174
|
+
function exploreCooldownSeconds() {
|
|
175
|
+
return Math.round(exports.EXPLORE_COOLDOWN_MS / 1000);
|
|
176
|
+
}
|
|
166
177
|
/**
|
|
167
178
|
* After a counted Partial explore, allow this many depth tools
|
|
168
179
|
* (`homegraph_node` / callers / callees) before refusing — stops Partial→node×N
|
|
@@ -172,20 +183,30 @@ const MAX_DEPTH_AFTER_PARTIAL = 1;
|
|
|
172
183
|
function isCountedCall(call) {
|
|
173
184
|
return call.evidenceStatus !== undefined || (call.responseBytes || 0) >= 400;
|
|
174
185
|
}
|
|
186
|
+
function latestReturnedCall(prior) {
|
|
187
|
+
for (let i = prior.calls.length - 1; i >= 0; i--) {
|
|
188
|
+
const call = prior.calls[i];
|
|
189
|
+
if (call && typeof call.returnedAt === 'number' && Number.isFinite(call.returnedAt))
|
|
190
|
+
return call;
|
|
191
|
+
}
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
175
194
|
/**
|
|
176
195
|
* Whether this explore should short-refuse instead of re-running.
|
|
196
|
+
*
|
|
197
|
+
* `now` is injectable so tests can move past the cooldown without sleeping.
|
|
177
198
|
*/
|
|
178
|
-
function decideExploreRepeat(prior, query) {
|
|
199
|
+
function decideExploreRepeat(prior, query, now = Date.now()) {
|
|
179
200
|
if (!prior || prior.calls.length === 0)
|
|
180
201
|
return { refuse: false, reason: 'ok' };
|
|
181
202
|
const counted = prior.calls.filter(isCountedCall);
|
|
182
203
|
const last = counted[counted.length - 1];
|
|
183
|
-
|
|
184
|
-
if (
|
|
204
|
+
const returned = latestReturnedCall(prior);
|
|
205
|
+
if (returned && now - returned.returnedAt < exports.EXPLORE_COOLDOWN_MS) {
|
|
185
206
|
return {
|
|
186
207
|
refuse: true,
|
|
187
|
-
matched:
|
|
188
|
-
reason: '
|
|
208
|
+
matched: returned,
|
|
209
|
+
reason: 'cooldown',
|
|
189
210
|
};
|
|
190
211
|
}
|
|
191
212
|
// Missing or SDK-only evidence is not a successfully answered query. Permit
|
|
@@ -217,8 +238,8 @@ function formatExploreRepeatRefuse(decision, query) {
|
|
|
217
238
|
: '';
|
|
218
239
|
let why;
|
|
219
240
|
switch (decision.reason) {
|
|
220
|
-
case '
|
|
221
|
-
why = `
|
|
241
|
+
case 'cooldown':
|
|
242
|
+
why = `The previous \`homegraph_explore\` result was returned less than **${exploreCooldownSeconds()}** seconds ago. Wait, reuse that result, or inspect with Read/Grep, then call explore again.`;
|
|
222
243
|
break;
|
|
223
244
|
case 'next-anchor':
|
|
224
245
|
why = `Prior explore was Partial and named a **Next anchor** — do not re-explore a paraphrase of "${priorQ}${priorQ.length >= 100 ? '…' : ''}".`;
|
|
@@ -415,8 +436,8 @@ function formatMechanismPartialFooter(nextCaption) {
|
|
|
415
436
|
+ 'Use targeted Read/Grep if the evidence remains missing; an inventory alone does not establish behavior.');
|
|
416
437
|
}
|
|
417
438
|
/**
|
|
418
|
-
* Appended when
|
|
419
|
-
*
|
|
439
|
+
* Appended when another Partial is emitted after one earlier Partial in this
|
|
440
|
+
* session. The wait is the return cooldown, not a connection-long stop.
|
|
420
441
|
*/
|
|
421
442
|
function formatSecondPartialStopFooter(nextAnchor) {
|
|
422
443
|
const next = nextAnchor
|
|
@@ -425,7 +446,7 @@ function formatSecondPartialStopFooter(nextAnchor) {
|
|
|
425
446
|
return [
|
|
426
447
|
'',
|
|
427
448
|
'---',
|
|
428
|
-
|
|
449
|
+
`> **Second Partial.** Wait ${exploreCooldownSeconds()} seconds after this result before calling \`homegraph_explore\` again.`
|
|
429
450
|
+ next
|
|
430
451
|
+ ' Evidence remains incomplete. Inspect only missing source with targeted Read or narrow Grep, '
|
|
431
452
|
+ 'then continue the requested edits and validation. Reuse unchanged source already returned.',
|
|
@@ -473,7 +494,7 @@ function formatPartialExploreGuidance(opts) {
|
|
|
473
494
|
`⚠️ **Partial result** — ${why}. This is NOT an error.`,
|
|
474
495
|
'A targeted Read or **ONE narrow Grep** may recover missing evidence using names already in the question.',
|
|
475
496
|
anchorLine,
|
|
476
|
-
'At most **one** focused
|
|
497
|
+
'At most **one** focused callers/callees recovery after Partial; reuse source already returned.',
|
|
477
498
|
'If a second explore is still Partial, its retrieval budget is exhausted. Inspect missing source directly and continue the requested work; do not infer task completion.',
|
|
478
499
|
].join('\n');
|
|
479
500
|
}
|
|
@@ -118,6 +118,11 @@ export interface ExploreEmission {
|
|
|
118
118
|
export interface ExploreCallRecord extends ExploreEmission {
|
|
119
119
|
/** 1-based call index within this session FOR THIS PROJECT. Survives eviction. */
|
|
120
120
|
index: number;
|
|
121
|
+
/**
|
|
122
|
+
* Epoch ms when this result was recorded and about to be returned.
|
|
123
|
+
* The explore cooldown measures from here, not from when the request arrived.
|
|
124
|
+
*/
|
|
125
|
+
returnedAt?: number;
|
|
121
126
|
}
|
|
122
127
|
/** Everything the session knows about one project. */
|
|
123
128
|
export interface ExploreProjectState {
|
|
@@ -215,6 +215,7 @@ class ExploreSessionState {
|
|
|
215
215
|
state.responseBytes += Math.max(0, emission.responseBytes || 0);
|
|
216
216
|
const record = {
|
|
217
217
|
index: state.callCount,
|
|
218
|
+
returnedAt: Date.now(),
|
|
218
219
|
projectRoot: emission.projectRoot,
|
|
219
220
|
query: typeof emission.query === 'string' ? emission.query : '',
|
|
220
221
|
files: this.boundFiles(emission.files),
|