homegraph 1.6.1 → 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 +22 -0
- 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 +5 -0
- package/dist/db/queries.js +17 -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/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/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/locate-contract.js +1 -1
- package/dist/mcp/server-instructions.d.ts +2 -2
- package/dist/mcp/server-instructions.js +10 -10
- package/dist/mcp/session.js +3 -2
- package/dist/mcp/tools.d.ts +21 -2
- package/dist/mcp/tools.js +109 -50
- 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/sync/watcher.d.ts +30 -0
- package/dist/sync/watcher.js +45 -0
- package/package.json +1 -1
package/dist/resolution/index.js
CHANGED
|
@@ -57,6 +57,8 @@ const workspace_packages_1 = require("./workspace-packages");
|
|
|
57
57
|
const errors_1 = require("../errors");
|
|
58
58
|
const lru_cache_1 = require("./lru-cache");
|
|
59
59
|
const grammars_1 = require("../extraction/grammars");
|
|
60
|
+
const cpp_macro_visibility_1 = require("./cpp-macro-visibility");
|
|
61
|
+
const c_feature_variants_1 = require("./c-feature-variants");
|
|
60
62
|
/** Node kinds that can declare supertypes (extends/implements). */
|
|
61
63
|
const SUPERTYPE_BEARING_KINDS = new Set([
|
|
62
64
|
'class', 'struct', 'interface', 'trait', 'protocol', 'enum',
|
|
@@ -412,6 +414,8 @@ class ReferenceResolver {
|
|
|
412
414
|
if (this.context) {
|
|
413
415
|
(0, import_resolver_1.clearImportResolverMemos)(this.context);
|
|
414
416
|
(0, name_matcher_1.clearNameMatcherMemos)(this.context);
|
|
417
|
+
(0, cpp_macro_visibility_1.clearCppMacroVisibility)(this.context);
|
|
418
|
+
(0, c_feature_variants_1.clearCFeatureVariantMemos)(this.context);
|
|
415
419
|
}
|
|
416
420
|
}
|
|
417
421
|
/** `readFile` through the LRU content cache (null = read failed, also cached). */
|
|
@@ -852,6 +856,13 @@ class ReferenceResolver {
|
|
|
852
856
|
if (this.isBuiltInOrExternal(ref)) {
|
|
853
857
|
return null;
|
|
854
858
|
}
|
|
859
|
+
// A C/C++ call that is a visible function-like macro is not a call.
|
|
860
|
+
// The include walk that decides this is bounded (Spec 0054).
|
|
861
|
+
if ((ref.language === 'c' || ref.language === 'cpp')
|
|
862
|
+
&& ref.referenceKind === 'calls'
|
|
863
|
+
&& (0, cpp_macro_visibility_1.isVisibleCppMacro)(ref, this.context)) {
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
855
866
|
// CFML component paths in inheritance (#1152): `extends="coldbox.system.web.
|
|
856
867
|
// Controller"` names the supertype by its dot-separated path (or `extends=
|
|
857
868
|
// "../base"` by relative file path) — the graph indexes the class under its
|
|
@@ -24,6 +24,7 @@ exports.matchMethodCall = matchMethodCall;
|
|
|
24
24
|
exports.matchFuzzy = matchFuzzy;
|
|
25
25
|
exports.dumpNameMatcherProfile = dumpNameMatcherProfile;
|
|
26
26
|
exports.matchReference = matchReference;
|
|
27
|
+
const c_feature_variants_1 = require("./c-feature-variants");
|
|
27
28
|
/**
|
|
28
29
|
* Ceiling on how many same-named definitions a FUZZY name-match strategy will
|
|
29
30
|
* score. A name defined more times than this is "ubiquitous" — a method/symbol
|
|
@@ -361,6 +362,49 @@ function isLexicallyReachable(candidate, ref, context) {
|
|
|
361
362
|
return (ref.filePath === candidate.filePath &&
|
|
362
363
|
containers.some((p) => ref.line >= p.startLine && ref.line <= p.endLine));
|
|
363
364
|
}
|
|
365
|
+
/** Per-context memo: node id → file-scope `static` (C linkage). */
|
|
366
|
+
const C_STATIC_MEMO = new WeakMap();
|
|
367
|
+
/**
|
|
368
|
+
* A C function whose declaration line (or the line above it) is `static`.
|
|
369
|
+
* Another translation unit cannot call it. Fail open when the source is missing.
|
|
370
|
+
*/
|
|
371
|
+
function isStaticCFunction(candidate, context) {
|
|
372
|
+
let memo = C_STATIC_MEMO.get(context);
|
|
373
|
+
if (!memo) {
|
|
374
|
+
memo = new Map();
|
|
375
|
+
C_STATIC_MEMO.set(context, memo);
|
|
376
|
+
}
|
|
377
|
+
const hit = memo.get(candidate.id);
|
|
378
|
+
if (hit !== undefined)
|
|
379
|
+
return hit;
|
|
380
|
+
const lines = context.getFileLines?.(candidate.filePath) ?? context.readFile(candidate.filePath)?.split('\n') ?? [];
|
|
381
|
+
const head = [lines[candidate.startLine - 2] ?? '', lines[candidate.startLine - 1] ?? ''].join('\n');
|
|
382
|
+
const isStatic = /(^|[\s;}])static\s/.test(head);
|
|
383
|
+
memo.set(candidate.id, isStatic);
|
|
384
|
+
return isStatic;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Among same-named C/C++ function definitions, drop another file's `static`
|
|
388
|
+
* when a definition with external linkage remains, and drop a definition that
|
|
389
|
+
* exists only under a feature flag nothing in the project sets (Spec 0056).
|
|
390
|
+
* Same-file definitions stay. Any other language, or a non-call, is unchanged.
|
|
391
|
+
*/
|
|
392
|
+
function narrowCCallTargets(ref, candidates, context) {
|
|
393
|
+
if ((ref.language !== 'c' && ref.language !== 'cpp') || ref.referenceKind !== 'calls')
|
|
394
|
+
return candidates;
|
|
395
|
+
const funcs = candidates.filter((n) => n.kind === 'function');
|
|
396
|
+
if (funcs.length < 2)
|
|
397
|
+
return candidates;
|
|
398
|
+
const rest = candidates.filter((n) => n.kind !== 'function');
|
|
399
|
+
const visible = funcs.filter((n) => n.filePath === ref.filePath || !isStaticCFunction(n, context));
|
|
400
|
+
let pool = visible.length > 0 ? visible : funcs;
|
|
401
|
+
if (ref.language === 'c' && !(0, c_feature_variants_1.isOffByDefault)(ref.filePath, ref.line, context)) {
|
|
402
|
+
const plain = pool.filter((n) => !(0, c_feature_variants_1.isCFeatureVariant)(n, ref, context));
|
|
403
|
+
if (plain.length > 0 && plain.length < pool.length)
|
|
404
|
+
pool = plain;
|
|
405
|
+
}
|
|
406
|
+
return pool.length === funcs.length ? candidates : [...pool, ...rest];
|
|
407
|
+
}
|
|
364
408
|
/**
|
|
365
409
|
* Try to resolve a reference by exact name match
|
|
366
410
|
*/
|
|
@@ -399,8 +443,9 @@ function matchByExactName(ref, context) {
|
|
|
399
443
|
if (candidates.length > AMBIGUOUS_NAME_CEILING) {
|
|
400
444
|
return null;
|
|
401
445
|
}
|
|
402
|
-
// Multiple matches - try to narrow down
|
|
403
|
-
|
|
446
|
+
// Multiple matches - try to narrow down. C/C++ linkage runs first so a
|
|
447
|
+
// nearer file's `static` copy cannot beat the definition the call can see.
|
|
448
|
+
const bestMatch = findBestMatch(ref, narrowCCallTargets(ref, candidates, context), context);
|
|
404
449
|
if (bestMatch) {
|
|
405
450
|
// Lower confidence when the match is from a distant/unrelated module
|
|
406
451
|
const proximity = computePathProximity(ref.filePath, bestMatch.filePath);
|
|
@@ -1065,6 +1110,8 @@ function getInferScanStates(context) {
|
|
|
1065
1110
|
/** Drop the per-context scan states (see ReferenceResolver.clearCaches). */
|
|
1066
1111
|
function clearNameMatcherMemos(context) {
|
|
1067
1112
|
INFER_SCAN_STATES.delete(context);
|
|
1113
|
+
C_STATIC_MEMO.delete(context);
|
|
1114
|
+
(0, c_feature_variants_1.clearCFeatureVariantMemos)(context);
|
|
1068
1115
|
}
|
|
1069
1116
|
function memoPatterns(key, build) {
|
|
1070
1117
|
const hit = PATTERN_MEMO.get(key);
|
|
@@ -91,6 +91,12 @@ export interface ResolutionContext {
|
|
|
91
91
|
* callers fall back to splitting `readFile` themselves.
|
|
92
92
|
*/
|
|
93
93
|
getFileLines?(filePath: string): string[] | null;
|
|
94
|
+
/**
|
|
95
|
+
* Whether `filePath` contains `text`, without reading the whole file when
|
|
96
|
+
* the host can answer cheaper. Optional; C feature-flag scans fall back to
|
|
97
|
+
* `readFile` (Spec 0056).
|
|
98
|
+
*/
|
|
99
|
+
fileContains?(filePath: string, text: string): boolean;
|
|
94
100
|
/**
|
|
95
101
|
* The method-definition nodes matching `typeName::methodName` in `language` —
|
|
96
102
|
* exactly `resolveMethodOnType`'s kind/language/qualifiedName-suffix filter,
|
|
@@ -249,6 +255,8 @@ export interface ImportMapping {
|
|
|
249
255
|
* `export * from './other'`. Used by the resolver to chase
|
|
250
256
|
* symbols through barrel files.
|
|
251
257
|
*/
|
|
258
|
+
/** A `#define` recorded as a constant node's signature (C/C++ macro visibility). */
|
|
259
|
+
export declare const CPP_DEFINE_SIGNATURE: RegExp;
|
|
252
260
|
export type ReExport = {
|
|
253
261
|
kind: 'named';
|
|
254
262
|
/** Name as exported by THIS file. */
|
package/dist/resolution/types.js
CHANGED
|
@@ -5,4 +5,12 @@
|
|
|
5
5
|
* Types for the reference resolution system.
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.CPP_DEFINE_SIGNATURE = void 0;
|
|
9
|
+
/**
|
|
10
|
+
* Re-export from a file: `export { x } from './other'` or
|
|
11
|
+
* `export * from './other'`. Used by the resolver to chase
|
|
12
|
+
* symbols through barrel files.
|
|
13
|
+
*/
|
|
14
|
+
/** A `#define` recorded as a constant node's signature (C/C++ macro visibility). */
|
|
15
|
+
exports.CPP_DEFINE_SIGNATURE = /^\s*#\s*define\b/;
|
|
8
16
|
//# sourceMappingURL=types.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",
|