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.
Files changed (59) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +64 -14
  3. package/dist/bin/homegraph.js +2 -2
  4. package/dist/db/index.js +1 -0
  5. package/dist/db/migrations.d.ts +6 -1
  6. package/dist/db/migrations.js +26 -1
  7. package/dist/db/queries.d.ts +9 -0
  8. package/dist/db/queries.js +32 -7
  9. package/dist/db/schema.sql +4 -0
  10. package/dist/extraction/grammars.js +2 -1
  11. package/dist/extraction/languages/c-cpp.d.ts +8 -0
  12. package/dist/extraction/languages/c-cpp.js +44 -0
  13. package/dist/extraction/wasm-runtime-flags.d.ts +4 -0
  14. package/dist/extraction/wasm-runtime-flags.js +9 -0
  15. package/dist/graph/evidence-paths.d.ts +2 -0
  16. package/dist/graph/evidence-paths.js +20 -3
  17. package/dist/index.js +7 -1
  18. package/dist/installer/targets/icode.d.ts +43 -0
  19. package/dist/installer/targets/icode.js +631 -0
  20. package/dist/installer/targets/registry.js +2 -0
  21. package/dist/installer/targets/types.d.ts +1 -1
  22. package/dist/mcp/arkts-evidence-packs.d.ts +17 -0
  23. package/dist/mcp/arkts-evidence-packs.js +218 -10
  24. package/dist/mcp/evidence-audit.d.ts +3 -0
  25. package/dist/mcp/evidence-audit.js +40 -0
  26. package/dist/mcp/evidence-rendering.js +1 -1
  27. package/dist/mcp/explore-repeat-guard.d.ts +19 -5
  28. package/dist/mcp/explore-repeat-guard.js +35 -14
  29. package/dist/mcp/explore-session-state.d.ts +5 -0
  30. package/dist/mcp/explore-session-state.js +1 -0
  31. package/dist/mcp/implementation-context.d.ts +35 -0
  32. package/dist/mcp/implementation-context.js +232 -0
  33. package/dist/mcp/locate-contract.js +1 -1
  34. package/dist/mcp/query-cache.d.ts +1 -15
  35. package/dist/mcp/query-cache.js +20 -11
  36. package/dist/mcp/request-evidence.d.ts +69 -0
  37. package/dist/mcp/request-evidence.js +297 -0
  38. package/dist/mcp/server-instructions.d.ts +2 -2
  39. package/dist/mcp/server-instructions.js +15 -15
  40. package/dist/mcp/session.js +3 -2
  41. package/dist/mcp/tools.d.ts +39 -12
  42. package/dist/mcp/tools.js +237 -121
  43. package/dist/resolution/c-feature-variants.d.ts +40 -0
  44. package/dist/resolution/c-feature-variants.js +272 -0
  45. package/dist/resolution/cpp-macro-visibility.d.ts +21 -0
  46. package/dist/resolution/cpp-macro-visibility.js +393 -0
  47. package/dist/resolution/index.js +11 -0
  48. package/dist/resolution/name-matcher.js +49 -2
  49. package/dist/resolution/types.d.ts +8 -0
  50. package/dist/resolution/types.js +8 -0
  51. package/dist/search/literal-evidence.js +11 -3
  52. package/dist/search/query-plan-provider.js +13 -5
  53. package/dist/search/query-plan.d.ts +3 -1
  54. package/dist/search/query-plan.js +11 -4
  55. package/dist/search/request-contract.d.ts +29 -0
  56. package/dist/search/request-contract.js +76 -0
  57. package/dist/sync/watcher.d.ts +30 -0
  58. package/dist/sync/watcher.js +45 -0
  59. 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
- exports.QUERY_PLAN_VERSION = 2;
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
- // Spec 0042: non-LLM retrieval must not concatenate taskContext into the FTS string.
300
- // Planner prose stays out of seeds; only typed slots (anchors/searchTerms/literalTexts) retrieve.
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
@@ -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.0",
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",