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
|
@@ -51,7 +51,8 @@ exports.compileQueryPlanStep = compileQueryPlanStep;
|
|
|
51
51
|
exports.planQuery = planQuery;
|
|
52
52
|
/** Versioned retrieval intent, not a replacement for the user's question or graph evidence. */
|
|
53
53
|
const shape = __importStar(require("./query-utils"));
|
|
54
|
-
|
|
54
|
+
const request_contract_1 = require("./request-contract");
|
|
55
|
+
exports.QUERY_PLAN_VERSION = 3;
|
|
55
56
|
exports.QUERY_RELATIONS = ['incoming_references', 'registration_sites', 'outgoing_calls', 'module_imports', 'module_cycles'];
|
|
56
57
|
exports.QUERY_SOURCE_SCOPES = ['local', 'sdk', 'all'];
|
|
57
58
|
/** One shared bound for provider input, transferred plans and cache identity. */
|
|
@@ -193,6 +194,7 @@ function buildRuleQueryPlan(query, originalTaskContext) {
|
|
|
193
194
|
return {
|
|
194
195
|
version: exports.QUERY_PLAN_VERSION,
|
|
195
196
|
originalQuery: query,
|
|
197
|
+
requestContract: (0, request_contract_1.ruleRequestContract)(canonicalQuery, literalTexts),
|
|
196
198
|
...(taskContext ? { taskContext } : {}),
|
|
197
199
|
canonicalQuery,
|
|
198
200
|
intent,
|
|
@@ -295,9 +297,14 @@ function compileQueryPlanStep(plan, step, resolvedAnchors = []) {
|
|
|
295
297
|
const relation = step.relation;
|
|
296
298
|
const stepIntent = intentForQueryRelation(downgradedOverview ? 'general' : step.intent, relation);
|
|
297
299
|
const query = downgradedOverview ? plan.originalQuery : step.query;
|
|
298
|
-
const literalTexts = step.literalTexts ?? (plan.steps.length === 1 ? plan.literalTexts : undefined) ?? []
|
|
299
|
-
|
|
300
|
-
|
|
300
|
+
const literalTexts = [...new Set([...(step.literalTexts ?? (plan.steps.length === 1 ? plan.literalTexts : undefined) ?? []),
|
|
301
|
+
// Independent discovery keeps user labels; dependent helpers retain identity without global UI seeds.
|
|
302
|
+
...(!step.dependsOn.length ? [...(plan.literalTexts ?? []), ...((0, request_contract_1.accuracyTargetsEnabled)() ? (0, request_contract_1.contractLiteralTexts)(plan.requestContract) : [])] : [])])].slice(0, 8);
|
|
303
|
+
// Scope and negations remain losslessly in originalQuery/taskContext for the
|
|
304
|
+
// executor and cache. Repeating that prose here erases the step's focus.
|
|
305
|
+
// A planner's English explanation is not source text. Only its typed slots
|
|
306
|
+
// become seeds; this prevents a verb such as "locate" matching an SDK method.
|
|
307
|
+
// Legacy/malformed seedless steps retain the USER's query, never planner prose.
|
|
301
308
|
const seeds = [...new Set([...anchors, ...(step.searchTerms ?? []), ...literalTexts])];
|
|
302
309
|
const retrievalQuery = plan.source === 'llm'
|
|
303
310
|
? seeds.join(' ') || plan.originalQuery : query;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Grounded request data, never repository facts or benchmark acceptance answers. */
|
|
2
|
+
export declare const TARGET_ROLES: readonly ["page", "literal", "symbol", "object"];
|
|
3
|
+
export declare const OBJECT_KINDS: readonly ["ui", "form", "native"];
|
|
4
|
+
export declare const BEHAVIOR_KINDS: readonly ["enabled", "visibility", "event", "state", "route", "runtime"];
|
|
5
|
+
export interface RequestTarget {
|
|
6
|
+
id: string;
|
|
7
|
+
text: string;
|
|
8
|
+
role: typeof TARGET_ROLES[number];
|
|
9
|
+
presence: 'existing' | 'requested';
|
|
10
|
+
objectKind?: typeof OBJECT_KINDS[number];
|
|
11
|
+
}
|
|
12
|
+
export interface BehaviorObligation {
|
|
13
|
+
id: string;
|
|
14
|
+
text: string;
|
|
15
|
+
kind: typeof BEHAVIOR_KINDS[number];
|
|
16
|
+
targetId?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface RequestContract {
|
|
19
|
+
targets: RequestTarget[];
|
|
20
|
+
obligations: BehaviorObligation[];
|
|
21
|
+
}
|
|
22
|
+
export declare const accuracyTargetsEnabled: () => boolean;
|
|
23
|
+
export declare const accuracyCoverageEnabled: () => boolean;
|
|
24
|
+
/** No repair of invented text, no unknown enum values, no generated identifiers. */
|
|
25
|
+
export declare function validateRequestContract(value: unknown, original: string): RequestContract | undefined;
|
|
26
|
+
/** Fallback deliberately cannot decide whether quoted text should already exist. */
|
|
27
|
+
export declare function ruleRequestContract(original: string, literals: string[]): RequestContract | undefined;
|
|
28
|
+
export declare function contractLiteralTexts(contract?: RequestContract): string[];
|
|
29
|
+
//# sourceMappingURL=request-contract.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.accuracyCoverageEnabled = exports.accuracyTargetsEnabled = exports.BEHAVIOR_KINDS = exports.OBJECT_KINDS = exports.TARGET_ROLES = void 0;
|
|
4
|
+
exports.validateRequestContract = validateRequestContract;
|
|
5
|
+
exports.ruleRequestContract = ruleRequestContract;
|
|
6
|
+
exports.contractLiteralTexts = contractLiteralTexts;
|
|
7
|
+
/** Grounded request data, never repository facts or benchmark acceptance answers. */
|
|
8
|
+
exports.TARGET_ROLES = ['page', 'literal', 'symbol', 'object'];
|
|
9
|
+
exports.OBJECT_KINDS = ['ui', 'form', 'native'];
|
|
10
|
+
exports.BEHAVIOR_KINDS = ['enabled', 'visibility', 'event', 'state', 'route', 'runtime'];
|
|
11
|
+
const accuracyTargetsEnabled = () => process.env.HOMEGRAPH_ACCURACY_TARGETS !== '0';
|
|
12
|
+
exports.accuracyTargetsEnabled = accuracyTargetsEnabled;
|
|
13
|
+
const accuracyCoverageEnabled = () => process.env.HOMEGRAPH_ACCURACY_COVERAGE !== '0';
|
|
14
|
+
exports.accuracyCoverageEnabled = accuracyCoverageEnabled;
|
|
15
|
+
/** No repair of invented text, no unknown enum values, no generated identifiers. */
|
|
16
|
+
function validateRequestContract(value, original) {
|
|
17
|
+
if (value === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
const fail = () => { throw new Error('invalid_request_contract'); };
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
21
|
+
return fail();
|
|
22
|
+
const v = value;
|
|
23
|
+
if (!Array.isArray(v.targets) || v.targets.length > 6 || !Array.isArray(v.obligations) || v.obligations.length > 6)
|
|
24
|
+
return fail();
|
|
25
|
+
if (Object.keys(v).some(key => !['targets', 'obligations'].includes(key)))
|
|
26
|
+
return fail();
|
|
27
|
+
const ids = new Set();
|
|
28
|
+
const record = (raw) => {
|
|
29
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
30
|
+
return fail();
|
|
31
|
+
const r = raw;
|
|
32
|
+
if (typeof r.id !== 'string' || !/^[A-Za-z][\w-]{0,15}$/.test(r.id) || ids.has(r.id)
|
|
33
|
+
|| typeof r.text !== 'string' || r.text.trim() !== r.text || r.text.length < 2 || r.text.length > 256
|
|
34
|
+
|| /[\u0000-\u001f\u007f]/.test(r.text) || !original.includes(r.text))
|
|
35
|
+
return fail();
|
|
36
|
+
ids.add(r.id);
|
|
37
|
+
return r;
|
|
38
|
+
};
|
|
39
|
+
const targets = v.targets.map(raw => {
|
|
40
|
+
const r = record(raw);
|
|
41
|
+
if (Object.keys(r).some(key => !['id', 'text', 'role', 'presence', 'objectKind'].includes(key)))
|
|
42
|
+
return fail();
|
|
43
|
+
if (!exports.TARGET_ROLES.includes(r.role) || !['existing', 'requested'].includes(r.presence)
|
|
44
|
+
|| (r.objectKind !== undefined && !exports.OBJECT_KINDS.includes(r.objectKind)))
|
|
45
|
+
return fail();
|
|
46
|
+
return { id: r.id, text: r.text, role: r.role,
|
|
47
|
+
presence: r.presence, ...(r.objectKind ? { objectKind: r.objectKind } : {}) };
|
|
48
|
+
});
|
|
49
|
+
const obligations = v.obligations.map(raw => {
|
|
50
|
+
const r = record(raw);
|
|
51
|
+
if (Object.keys(r).some(key => !['id', 'text', 'kind', 'targetId'].includes(key)))
|
|
52
|
+
return fail();
|
|
53
|
+
if (!exports.BEHAVIOR_KINDS.includes(r.kind)
|
|
54
|
+
|| (r.targetId !== undefined && !targets.some(t => t.id === r.targetId)))
|
|
55
|
+
return fail();
|
|
56
|
+
return { id: r.id, text: r.text, kind: r.kind,
|
|
57
|
+
...(r.targetId ? { targetId: r.targetId } : {}) };
|
|
58
|
+
});
|
|
59
|
+
return { targets, obligations };
|
|
60
|
+
}
|
|
61
|
+
/** Fallback deliberately cannot decide whether quoted text should already exist. */
|
|
62
|
+
function ruleRequestContract(original, literals) {
|
|
63
|
+
const targets = literals.slice(0, 6).map((text, i) => ({ id: `t${i + 1}`, text, role: 'literal', presence: 'requested' }));
|
|
64
|
+
// A small language-level behavior trigger, not a list of task names or expected answers.
|
|
65
|
+
const statements = original.split(/[\n。;;]/).map(s => s.trim()).filter(Boolean);
|
|
66
|
+
const obligations = statements.filter(s => /禁用|启用|\bdisabl(?:e|ed|ing)\b|\benabl(?:e|ed|ing)\b/i.test(s))
|
|
67
|
+
.slice(0, 6).map((s, i) => {
|
|
68
|
+
const named = targets.filter(t => s.includes(t.text));
|
|
69
|
+
return { id: `b${i + 1}`, text: s.slice(0, 256), kind: 'enabled', ...(named.length === 1 ? { targetId: named[0].id } : {}) };
|
|
70
|
+
});
|
|
71
|
+
return targets.length || obligations.length ? { targets, obligations } : undefined;
|
|
72
|
+
}
|
|
73
|
+
function contractLiteralTexts(contract) {
|
|
74
|
+
return (contract?.targets ?? []).filter(t => t.role === 'literal' || t.role === 'page').map(t => t.text);
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=request-contract.js.map
|
package/dist/sync/watcher.d.ts
CHANGED
|
@@ -88,7 +88,30 @@ export interface WatchOptions {
|
|
|
88
88
|
* sets it.
|
|
89
89
|
*/
|
|
90
90
|
inertForTests?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Indexed size and mtime for a project-relative path, or null when the
|
|
93
|
+
* file is not in the index. On Windows this drops watcher events whose
|
|
94
|
+
* size and modification time still match the index — NTFS last-access
|
|
95
|
+
* updates look like edits (Spec 0055). Absent means every event is kept.
|
|
96
|
+
*/
|
|
97
|
+
indexedFingerprint?: (relPath: string) => {
|
|
98
|
+
size: number;
|
|
99
|
+
modifiedAt: number;
|
|
100
|
+
} | null;
|
|
91
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* True when a watcher event is only an NTFS last-access update: Windows,
|
|
104
|
+
* the file is already indexed, and its size and modification time still
|
|
105
|
+
* match. A missing stat, a missing index row, or any other platform is a
|
|
106
|
+
* real change (fail open).
|
|
107
|
+
*/
|
|
108
|
+
export declare function isAccessTimeOnlyChange(platform: NodeJS.Platform, onDisk: {
|
|
109
|
+
size: number;
|
|
110
|
+
mtimeMs: number;
|
|
111
|
+
} | null, indexed: {
|
|
112
|
+
size: number;
|
|
113
|
+
modifiedAt: number;
|
|
114
|
+
} | null): boolean;
|
|
92
115
|
/**
|
|
93
116
|
* Thrown by a `syncFn` to signal that the underlying sync couldn't acquire
|
|
94
117
|
* the cross-process write lock (#449). The watcher treats this as "no
|
|
@@ -209,6 +232,7 @@ export declare class FileWatcher {
|
|
|
209
232
|
private readonly onSyncError?;
|
|
210
233
|
private readonly onDegraded?;
|
|
211
234
|
private readonly inertForTests;
|
|
235
|
+
private readonly indexedFingerprint?;
|
|
212
236
|
constructor(projectRoot: string, syncFn: (paths?: string[]) => Promise<{
|
|
213
237
|
filesChanged: number;
|
|
214
238
|
durationMs: number;
|
|
@@ -257,6 +281,12 @@ export declare class FileWatcher {
|
|
|
257
281
|
* — it drops node_modules/dist/.git churn before any sync is scheduled.
|
|
258
282
|
*/
|
|
259
283
|
private handleChange;
|
|
284
|
+
/**
|
|
285
|
+
* Windows ReadDirectoryChangesW reports a read as `change` when NTFS
|
|
286
|
+
* updates last-access time. Drop that event when size and mtime still
|
|
287
|
+
* match the index. Stat or lookup failure keeps the event.
|
|
288
|
+
*/
|
|
289
|
+
private isAccessTimeOnly;
|
|
260
290
|
/**
|
|
261
291
|
* A deleted DIRECTORY arrives as one event on the directory's own path —
|
|
262
292
|
* no source extension, so the source-file filter drops it, and the files
|
package/dist/sync/watcher.js
CHANGED
|
@@ -67,6 +67,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
67
67
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
68
68
|
exports.FileWatcher = exports.LockUnavailableError = void 0;
|
|
69
69
|
exports.__setFsWatchForTests = __setFsWatchForTests;
|
|
70
|
+
exports.isAccessTimeOnlyChange = isAccessTimeOnlyChange;
|
|
70
71
|
exports.__emitWatchEventForTests = __emitWatchEventForTests;
|
|
71
72
|
const fs = __importStar(require("fs"));
|
|
72
73
|
const path = __importStar(require("path"));
|
|
@@ -186,6 +187,18 @@ function maxDirWatches() {
|
|
|
186
187
|
*/
|
|
187
188
|
const liveWatchersForTests = new Map();
|
|
188
189
|
const IS_TEST_RUNTIME = !!(process.env.VITEST || process.env.NODE_ENV === 'test');
|
|
190
|
+
/**
|
|
191
|
+
* True when a watcher event is only an NTFS last-access update: Windows,
|
|
192
|
+
* the file is already indexed, and its size and modification time still
|
|
193
|
+
* match. A missing stat, a missing index row, or any other platform is a
|
|
194
|
+
* real change (fail open).
|
|
195
|
+
*/
|
|
196
|
+
function isAccessTimeOnlyChange(platform, onDisk, indexed) {
|
|
197
|
+
if (platform !== 'win32' || !onDisk || !indexed)
|
|
198
|
+
return false;
|
|
199
|
+
return onDisk.size === indexed.size
|
|
200
|
+
&& Math.floor(onDisk.mtimeMs) === Math.floor(indexed.modifiedAt);
|
|
201
|
+
}
|
|
189
202
|
/**
|
|
190
203
|
* Thrown by a `syncFn` to signal that the underlying sync couldn't acquire
|
|
191
204
|
* the cross-process write lock (#449). The watcher treats this as "no
|
|
@@ -296,6 +309,7 @@ class FileWatcher {
|
|
|
296
309
|
onSyncError;
|
|
297
310
|
onDegraded;
|
|
298
311
|
inertForTests;
|
|
312
|
+
indexedFingerprint;
|
|
299
313
|
constructor(projectRoot, syncFn, options = {}) {
|
|
300
314
|
this.projectRoot = projectRoot;
|
|
301
315
|
this.syncFn = syncFn;
|
|
@@ -305,6 +319,7 @@ class FileWatcher {
|
|
|
305
319
|
this.onSyncError = options.onSyncError;
|
|
306
320
|
this.onDegraded = options.onDegraded;
|
|
307
321
|
this.inertForTests = options.inertForTests ?? false;
|
|
322
|
+
this.indexedFingerprint = options.indexedFingerprint;
|
|
308
323
|
}
|
|
309
324
|
/**
|
|
310
325
|
* Start watching for file changes.
|
|
@@ -528,6 +543,10 @@ class FileWatcher {
|
|
|
528
543
|
this.maybeScheduleForRemovedDir(rel);
|
|
529
544
|
return;
|
|
530
545
|
}
|
|
546
|
+
if (this.isAccessTimeOnly(rel)) {
|
|
547
|
+
(0, errors_1.logDebug)('Ignoring Windows access-time watcher event', { file: rel });
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
531
550
|
(0, errors_1.logDebug)('File change detected', { file: rel });
|
|
532
551
|
if (this.ready) {
|
|
533
552
|
const now = Date.now();
|
|
@@ -539,6 +558,32 @@ class FileWatcher {
|
|
|
539
558
|
}
|
|
540
559
|
this.scheduleSync();
|
|
541
560
|
}
|
|
561
|
+
/**
|
|
562
|
+
* Windows ReadDirectoryChangesW reports a read as `change` when NTFS
|
|
563
|
+
* updates last-access time. Drop that event when size and mtime still
|
|
564
|
+
* match the index. Stat or lookup failure keeps the event.
|
|
565
|
+
*/
|
|
566
|
+
isAccessTimeOnly(rel) {
|
|
567
|
+
if (process.platform !== 'win32' || !this.indexedFingerprint)
|
|
568
|
+
return false;
|
|
569
|
+
let indexed;
|
|
570
|
+
try {
|
|
571
|
+
indexed = this.indexedFingerprint(rel);
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
let onDisk = null;
|
|
577
|
+
try {
|
|
578
|
+
const st = fs.statSync(path.join(this.projectRoot, rel));
|
|
579
|
+
if (st.isFile())
|
|
580
|
+
onDisk = { size: st.size, mtimeMs: st.mtimeMs };
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
onDisk = null;
|
|
584
|
+
}
|
|
585
|
+
return isAccessTimeOnlyChange(process.platform, onDisk, indexed);
|
|
586
|
+
}
|
|
542
587
|
/**
|
|
543
588
|
* A deleted DIRECTORY arrives as one event on the directory's own path —
|
|
544
589
|
* no source extension, so the source-file filter drops it, and the files
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homegraph",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.2",
|
|
4
4
|
"description": "Supercharge AI coding agents with semantic code intelligence — surgical context, fewer tool calls, faster answers. 100% local.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|