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.
Files changed (40) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/bin/homegraph.js +2 -2
  3. package/dist/db/index.js +1 -0
  4. package/dist/db/migrations.d.ts +6 -1
  5. package/dist/db/migrations.js +26 -1
  6. package/dist/db/queries.d.ts +5 -0
  7. package/dist/db/queries.js +17 -7
  8. package/dist/db/schema.sql +4 -0
  9. package/dist/extraction/grammars.js +2 -1
  10. package/dist/extraction/languages/c-cpp.d.ts +8 -0
  11. package/dist/extraction/languages/c-cpp.js +44 -0
  12. package/dist/extraction/wasm-runtime-flags.d.ts +4 -0
  13. package/dist/extraction/wasm-runtime-flags.js +9 -0
  14. package/dist/index.js +7 -1
  15. package/dist/installer/targets/icode.d.ts +43 -0
  16. package/dist/installer/targets/icode.js +631 -0
  17. package/dist/installer/targets/registry.js +2 -0
  18. package/dist/installer/targets/types.d.ts +1 -1
  19. package/dist/mcp/evidence-rendering.js +1 -1
  20. package/dist/mcp/explore-repeat-guard.d.ts +19 -5
  21. package/dist/mcp/explore-repeat-guard.js +35 -14
  22. package/dist/mcp/explore-session-state.d.ts +5 -0
  23. package/dist/mcp/explore-session-state.js +1 -0
  24. package/dist/mcp/locate-contract.js +1 -1
  25. package/dist/mcp/server-instructions.d.ts +2 -2
  26. package/dist/mcp/server-instructions.js +10 -10
  27. package/dist/mcp/session.js +3 -2
  28. package/dist/mcp/tools.d.ts +21 -2
  29. package/dist/mcp/tools.js +109 -50
  30. package/dist/resolution/c-feature-variants.d.ts +40 -0
  31. package/dist/resolution/c-feature-variants.js +272 -0
  32. package/dist/resolution/cpp-macro-visibility.d.ts +21 -0
  33. package/dist/resolution/cpp-macro-visibility.js +393 -0
  34. package/dist/resolution/index.js +11 -0
  35. package/dist/resolution/name-matcher.js +49 -2
  36. package/dist/resolution/types.d.ts +8 -0
  37. package/dist/resolution/types.js +8 -0
  38. package/dist/sync/watcher.d.ts +30 -0
  39. package/dist/sync/watcher.js +45 -0
  40. package/package.json +1 -1
@@ -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
- const bestMatch = findBestMatch(ref, candidates, context);
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. */
@@ -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
@@ -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
@@ -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.1",
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",