@colbymchenry/codegraph 0.9.9 → 1.0.0

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 (47) hide show
  1. package/README.md +100 -17
  2. package/dist/bin/codegraph.d.ts +1 -0
  3. package/dist/db/migrations.d.ts +1 -1
  4. package/dist/db/queries.d.ts +43 -0
  5. package/dist/db/sqlite-adapter.d.ts +7 -0
  6. package/dist/directory.d.ts +34 -2
  7. package/dist/extraction/astro-extractor.d.ts +79 -0
  8. package/dist/extraction/extraction-version.d.ts +25 -0
  9. package/dist/extraction/function-ref.d.ts +118 -0
  10. package/dist/extraction/grammars.d.ts +7 -1
  11. package/dist/extraction/index.d.ts +34 -0
  12. package/dist/extraction/languages/c-cpp.d.ts +8 -0
  13. package/dist/extraction/languages/csharp.d.ts +22 -0
  14. package/dist/extraction/languages/r.d.ts +3 -0
  15. package/dist/extraction/languages/typescript.d.ts +13 -0
  16. package/dist/extraction/liquid-extractor.d.ts +7 -0
  17. package/dist/extraction/razor-extractor.d.ts +42 -0
  18. package/dist/extraction/tree-sitter-types.d.ts +33 -0
  19. package/dist/extraction/tree-sitter.d.ts +211 -0
  20. package/dist/extraction/vue-extractor.d.ts +15 -0
  21. package/dist/index.d.ts +34 -2
  22. package/dist/installer/instructions-template.d.ts +34 -11
  23. package/dist/installer/targets/opencode.d.ts +9 -1
  24. package/dist/installer/targets/shared.d.ts +14 -0
  25. package/dist/mcp/daemon.d.ts +60 -1
  26. package/dist/mcp/dynamic-boundaries.d.ts +41 -0
  27. package/dist/mcp/ppid-watchdog.d.ts +44 -0
  28. package/dist/mcp/proxy.d.ts +6 -0
  29. package/dist/mcp/server-instructions.d.ts +12 -1
  30. package/dist/mcp/session.d.ts +2 -0
  31. package/dist/mcp/stdin-teardown.d.ts +27 -0
  32. package/dist/mcp/tools.d.ts +71 -0
  33. package/dist/resolution/callback-synthesizer.d.ts +3 -3
  34. package/dist/resolution/frameworks/astro.d.ts +9 -0
  35. package/dist/resolution/frameworks/index.d.ts +1 -0
  36. package/dist/resolution/import-resolver.d.ts +10 -0
  37. package/dist/resolution/index.d.ts +80 -0
  38. package/dist/resolution/name-matcher.d.ts +61 -0
  39. package/dist/resolution/types.d.ts +27 -3
  40. package/dist/resolution/workspace-packages.d.ts +48 -0
  41. package/dist/search/query-utils.d.ts +17 -1
  42. package/dist/sync/watcher.d.ts +124 -32
  43. package/dist/telemetry/index.d.ts +146 -0
  44. package/dist/types.d.ts +17 -2
  45. package/dist/upgrade/index.d.ts +132 -0
  46. package/dist/utils.d.ts +30 -24
  47. package/package.json +7 -7
@@ -9,6 +9,36 @@ import { UnresolvedRef, ResolvedRef, ResolutionContext } from './types';
9
9
  * by matching the filename against file nodes.
10
10
  */
11
11
  export declare function matchByFilePath(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null;
12
+ export declare function sameLanguageFamily(a: string, b: string): boolean;
13
+ /**
14
+ * True when `lang` belongs to a known multi-language family (jvm/apple/web/c).
15
+ * Languages not listed (php, python, go, ruby, rust, dart, …) and config
16
+ * formats (yaml/xml/blade) form their own singleton families and return
17
+ * `false` — used to leave config↔code framework bridges (whose config side is
18
+ * never a known programming-language family) out of the cross-family gate.
19
+ */
20
+ export declare function isKnownLanguageFamily(lang: string): boolean;
21
+ /**
22
+ * True when `a` and `b` are two DIFFERENT *known* language families — the
23
+ * signature of a coincidental cross-language name collision (a TS `import
24
+ * React` matching a Swift `import React`, a C++ `#include "X.h"` matching a
25
+ * same-named ObjC header on another platform). The both-*known* test is
26
+ * deliberately weaker than {@link sameLanguageFamily}'s negation: a
27
+ * single-file-component language that carries its own tag (`vue`/`svelte`)
28
+ * importing a `.ts` module, or any singleton-family language (php/go/ruby/…),
29
+ * returns `false` here and is left alone.
30
+ */
31
+ export declare function crossesKnownFamily(a: string, b: string): boolean;
32
+ /**
33
+ * Resolve a function-as-value reference (#756) — a function name used as a
34
+ * callback/function-pointer value (`register(handler)`, `o->cb = handler`,
35
+ * `{ .cb = handler }`, `signal(SIGINT, handler)`). The ONLY strategy allowed
36
+ * for `function_ref` refs: exact name, function/method targets only, same
37
+ * language family, same-file first, and cross-file only when the match is
38
+ * UNIQUE. No fuzzy fallback, no qualified-name walking — a wrong callback
39
+ * edge is worse than none.
40
+ */
41
+ export declare function matchFunctionRef(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null;
12
42
  /**
13
43
  * Try to resolve a reference by exact name match
14
44
  */
@@ -17,6 +47,37 @@ export declare function matchByExactName(ref: UnresolvedRef, context: Resolution
17
47
  * Try to resolve by qualified name
18
48
  */
19
49
  export declare function matchByQualifiedName(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null;
50
+ /**
51
+ * Resolve a C++ chained call whose receiver is itself a call — encoded by the
52
+ * extractor as `<innerCallee>().<method>` (#645). The receiver's type is what
53
+ * the inner call returns; the outer method is then resolved and VALIDATED on it
54
+ * (resolveMethodOnType requires `cls::method` to exist), so a wrong inference
55
+ * produces no edge rather than a wrong one.
56
+ */
57
+ export declare function matchCppCallChain(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null;
58
+ /**
59
+ * Resolve a `::`-scoped factory chain whose receiver is a scoped/static call —
60
+ * PHP `Cls::for($x)->method()` (#608, the per-credential Laravel client idiom) or
61
+ * Rust `Foo::new().bar()` (an associated-function call) — both encoded by the
62
+ * extractor as `Cls::factory().method`. The receiver's type is what `Cls::factory`
63
+ * returns: a `self` marker (PHP `: self`/`: static`, Rust `-> Self`) resolves to
64
+ * the factory's own type, a concrete return type to that type. The outer method is
65
+ * then resolved and VALIDATED on it (resolveMethodOnType requires the method to
66
+ * exist on the type or a supertype it conforms to), so a wrong inference yields no
67
+ * edge rather than a wrong one. Shared by the `::`-receiver languages (PHP, Rust).
68
+ */
69
+ export declare function matchScopedCallChain(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null;
70
+ /**
71
+ * Resolve a dotted chained call whose receiver is a static factory / fluent call —
72
+ * `Foo.getInstance().bar()`, encoded by the extractor as `Foo.getInstance().bar`
73
+ * (#645/#608 mechanism). The receiver's type is what `Foo.getInstance` returns
74
+ * (its declared return type); the outer method is then resolved and VALIDATED on
75
+ * it (resolveMethodOnType requires `Type::method` to exist), so a wrong inference
76
+ * yields no edge rather than a wrong one (e.g. a same-named `bar()` on an
77
+ * unrelated class is never matched). Shared by the dot-notation languages
78
+ * (Java, Kotlin, C#, Swift) — same receiver shape, same `Class::method` qualified names.
79
+ */
80
+ export declare function matchDottedCallChain(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null;
20
81
  /**
21
82
  * Try to resolve by method name on a class/object
22
83
  */
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Types for the reference resolution system.
5
5
  */
6
- import { EdgeKind, Language, Node } from '../types';
6
+ import { Language, Node, ReferenceKind } from '../types';
7
7
  /**
8
8
  * An unresolved reference from extraction
9
9
  */
@@ -13,7 +13,7 @@ export interface UnresolvedRef {
13
13
  /** The name being referenced */
14
14
  referenceName: string;
15
15
  /** Type of reference */
16
- referenceKind: EdgeKind;
16
+ referenceKind: ReferenceKind;
17
17
  /** Line where reference occurs */
18
18
  line: number;
19
19
  /** Column where reference occurs */
@@ -36,7 +36,7 @@ export interface ResolvedRef {
36
36
  /** Confidence score (0-1) */
37
37
  confidence: number;
38
38
  /** How it was resolved */
39
- resolvedBy: 'exact-match' | 'import' | 'qualified-name' | 'framework' | 'fuzzy' | 'instance-method' | 'file-path';
39
+ resolvedBy: 'exact-match' | 'import' | 'qualified-name' | 'framework' | 'fuzzy' | 'instance-method' | 'file-path' | 'function-ref';
40
40
  }
41
41
  /**
42
42
  * Result of resolution attempt
@@ -76,6 +76,23 @@ export interface ResolutionContext {
76
76
  getAllFiles(): string[];
77
77
  /** Get nodes by lowercase name (O(1) lookup for fuzzy matching) */
78
78
  getNodesByLowerName(lowerName: string): Node[];
79
+ /**
80
+ * Direct supertypes of the type named `typeName` (same language): the classes
81
+ * it extends and the interfaces / protocols / traits it implements/conforms to,
82
+ * by simple name. Backed by the resolved `implements`/`extends` edges, so it is
83
+ * EMPTY during the first resolution pass (edges aren't built yet) and populated
84
+ * afterward — the conformance pass uses it to resolve a chained method defined
85
+ * on a supertype the receiver type conforms to (e.g. a protocol-extension
86
+ * method). Optional so external/test contexts compile without it.
87
+ */
88
+ getSupertypes?(typeName: string, language: Language): string[];
89
+ /**
90
+ * Look up a node by its id. Lets matchers derive the FROM-symbol's
91
+ * enclosing-class scope (Swift implicit-self method scoping, `this.X`
92
+ * member resolution). Optional so external/test contexts compile
93
+ * without it.
94
+ */
95
+ getNodeById?(id: string): Node | null;
79
96
  /** Get cached import mappings for a file */
80
97
  getImportMappings(filePath: string, language: Language): ImportMapping[];
81
98
  /**
@@ -94,6 +111,13 @@ export interface ResolutionContext {
94
111
  * cross-package imports from third-party packages.
95
112
  */
96
113
  getGoModule?(): import('./go-module').GoModule | null;
114
+ /**
115
+ * Monorepo workspace member packages, keyed by declared package name.
116
+ * Returns `null` for single-package repos (no `workspaces` field).
117
+ * Lets the resolver treat `@scope/ui/sub` as a local import into the
118
+ * member's directory instead of an external npm package (#629).
119
+ */
120
+ getWorkspacePackages?(): import('./workspace-packages').WorkspacePackages | null;
97
121
  /**
98
122
  * Re-exports declared by a file (`export { x } from './other'`,
99
123
  * `export * from './other'`). Empty array when the file has none.
@@ -0,0 +1,48 @@
1
+ /**
2
+ * JS/TS workspace (monorepo) package resolution.
3
+ *
4
+ * npm / yarn / bun read member packages from the root `package.json`
5
+ * `workspaces` field; pnpm from `pnpm-workspace.yaml`. A cross-package
6
+ * import like `@scope/ui/widgets` is LOCAL to the monorepo, but to a
7
+ * single-package resolver it looks exactly like a third-party npm
8
+ * specifier — so `isExternalImport` flags it external and the
9
+ * consumer↔definition edge is never created. For component barrels
10
+ * (`export { default as X } from './x.svelte'`) that surfaces as a false
11
+ * `0 callers` on a live component (issue #629).
12
+ *
13
+ * This module maps each member package's declared `name` to its
14
+ * directory so the resolver can rewrite `@scope/ui/widgets` →
15
+ * `packages/ui/widgets` and then run normal extension/index resolution.
16
+ *
17
+ * Scope deliberately small for v1 (mirrors path-aliases.ts):
18
+ * - reads `workspaces` (array OR `{ packages: [...] }`) from package.json,
19
+ * plus a minimal `pnpm-workspace.yaml` `packages:` list
20
+ * - expands one level of `*` / `**` globs (`packages/*`, `apps/*`)
21
+ * - subpath resolution is directory-based (`@scope/ui/sub` → `<ui>/sub`);
22
+ * it does NOT yet honour a member's `exports` map or `main` field
23
+ * - returns null when the project declares no workspaces, so single-
24
+ * package repos pay nothing and see no behaviour change.
25
+ */
26
+ export interface WorkspacePackages {
27
+ /** Member package `name` → directory relative to projectRoot (posix). */
28
+ byName: Map<string, string>;
29
+ }
30
+ /**
31
+ * Load workspace member packages for `projectRoot`. Returns `null` when
32
+ * the project declares no workspaces (the common single-package case) —
33
+ * callers then skip all workspace logic.
34
+ *
35
+ * Cheap to call repeatedly only via the resolver's per-instance cache;
36
+ * this function itself touches the filesystem, so the resolver memoises it
37
+ * the same way it does {@link loadProjectAliases} / {@link loadGoModule}.
38
+ */
39
+ export declare function loadWorkspacePackages(projectRoot: string): WorkspacePackages | null;
40
+ /**
41
+ * Rewrite a bare workspace import to a path relative to projectRoot,
42
+ * WITHOUT an extension — the caller applies the language's extension/index
43
+ * resolution. `@scope/ui/widgets` → `packages/ui/widgets`; the bare package
44
+ * name `@scope/ui` → its directory. Returns `null` when no member package
45
+ * name matches.
46
+ */
47
+ export declare function resolveWorkspaceImport(importPath: string, ws: WorkspacePackages): string | null;
48
+ //# sourceMappingURL=workspace-packages.d.ts.map
@@ -4,6 +4,22 @@
4
4
  * Shared module for search term extraction and scoring.
5
5
  */
6
6
  import { Node } from '../types';
7
+ /** Normalize a name to a comparable token: lowercase, alphanumerics only. */
8
+ export declare function normalizeNameToken(raw: string): string;
9
+ /**
10
+ * Tokens that name the PROJECT as a whole — its `go.mod` module, `package.json`
11
+ * name, or repo root directory — rather than any specific symbol. A user
12
+ * naturally puts the project name in a query as context ("MyApp backend
13
+ * routes"), but it carries no discriminative signal: when it's also a substring
14
+ * of a symbol or path on one stack (a `MyAppFrontend/` dir, a `MyAppApp` class)
15
+ * it lexically inflates that stack and buries the rest (#720).
16
+ *
17
+ * Returned normalized (lowercase, alphanumerics only) so a query word can be
18
+ * compared by its normalized form. Only names ≥5 chars are kept — short ones
19
+ * (`api`, `app`, `core`, `web`) collide with real query terms too often to
20
+ * safely down-weight.
21
+ */
22
+ export declare function deriveProjectNameTokens(projectRoot: string): Set<string>;
7
23
  /**
8
24
  * Common stop words to filter from search queries.
9
25
  * Includes generic English + code-specific noise words.
@@ -34,7 +50,7 @@ export declare function extractSearchTerms(query: string, options?: {
34
50
  * Score path relevance to a query
35
51
  * Higher score = more relevant path
36
52
  */
37
- export declare function scorePathRelevance(filePath: string, query: string): number;
53
+ export declare function scorePathRelevance(filePath: string, query: string, projectNameTokens?: Set<string>): number;
38
54
  /**
39
55
  * Check if a file path looks like a test file
40
56
  */
@@ -4,14 +4,31 @@
4
4
  * Watches the project directory for file changes and triggers debounced sync
5
5
  * operations to keep the code graph up-to-date.
6
6
  *
7
- * Uses chokidar, whose `ignored` callback filters directories BEFORE they are
8
- * watched so we never register inotify watches on excluded trees like
9
- * node_modules/, dist/, .git/ (fixes #276: recursive fs.watch exhausted the
10
- * kernel watch budget on large repos). The ignore decision reuses the indexer's
11
- * `buildDefaultIgnore` (built-in default-ignore dirs + the project's .gitignore)
12
- * so the watcher watches exactly the set the indexer indexes — in particular,
13
- * node_modules/build/cache dirs are excluded even when the repo has no
14
- * .gitignore (#407), which a .gitignore-only filter would miss.
7
+ * Uses Node's built-in `fs.watch` directly (no third-party watcher, no native
8
+ * addon) with a per-platform strategy chosen to keep the open-descriptor /
9
+ * kernel-watch cost BOUNDED rather than growing with the number of files:
10
+ *
11
+ * - macOS / Windows: a SINGLE recursive `fs.watch(root, {recursive:true})`.
12
+ * libuv maps this to one FSEvents stream (macOS) / one
13
+ * ReadDirectoryChangesW handle (Windows), so it costs O(1) descriptors no
14
+ * matter how large the tree. This is the fix for the macOS file-table
15
+ * exhaustion (#644 / #496 / #555 / #628): the previous watcher held one
16
+ * open fd PER WATCHED FILE on macOS (tens of thousands of REG fds), which
17
+ * exhausted `kern.maxfiles` and crashed unrelated processes system-wide.
18
+ *
19
+ * - Linux: recursive `fs.watch` is unsupported, so we watch each (non-ignored)
20
+ * DIRECTORY with one inotify watch — O(directories), NOT O(files). New
21
+ * directories are picked up dynamically and an overall watch cap bounds
22
+ * inotify usage on pathological monorepos (#579). A single inotify watch on
23
+ * a directory already reports create/modify/delete for its children, so
24
+ * per-file watches are never needed.
25
+ *
26
+ * Excluded trees (node_modules/, dist/, .git/, …) are filtered via the
27
+ * indexer's `buildScopeIgnore` (built-in default-ignore dirs + the project's
28
+ * .gitignore) — on Linux they're never descended into (so they cost no watch),
29
+ * and on macOS/Windows the single recursive stream still covers them but their
30
+ * events are dropped before any sync is scheduled. Either way the watcher's
31
+ * scope matches the indexer's (#276 / #407).
15
32
  */
16
33
  /**
17
34
  * Options for the file watcher
@@ -34,6 +51,15 @@ export interface WatchOptions {
34
51
  * Callback when a sync errors (for logging/diagnostics).
35
52
  */
36
53
  onSyncError?: (error: Error) => void;
54
+ /**
55
+ * Test-only. When true, `start()` installs NO OS-level fs.watch — the
56
+ * watcher is "inert" and only the {@link __emitWatchEventForTests} /
57
+ * {@link FileWatcher.ingestEventForTests} seam drives its pipeline. This
58
+ * restores the deterministic, OS-free behavior the unit tests need (real
59
+ * FSEvents/inotify delivery races under parallel vitest). Production never
60
+ * sets it.
61
+ */
62
+ inertForTests?: boolean;
37
63
  }
38
64
  /**
39
65
  * Thrown by a `syncFn` to signal that the underlying sync couldn't acquire
@@ -69,8 +95,9 @@ export interface PendingFile {
69
95
  * debounced sync operations via a provided callback.
70
96
  *
71
97
  * Design goals:
72
- * - Minimal resource usage (chokidar filters excluded directories before
73
- * registering an inotify watch see module docs / #276)
98
+ * - Bounded resource usage: O(1) descriptors on macOS/Windows (one recursive
99
+ * watch), O(directories) inotify watches on Linux never O(files), which
100
+ * was the system-crashing fd leak on macOS (#644/#496/#555/#628).
74
101
  * - Debounced to avoid thrashing on rapid saves
75
102
  * - Filters to supported source files by extension
76
103
  * - Ignores .codegraph/ and .git/ regardless of .gitignore
@@ -78,11 +105,18 @@ export interface PendingFile {
78
105
  * without blocking on a sync (issue #403)
79
106
  */
80
107
  export declare class FileWatcher {
81
- private watcher;
108
+ /** macOS/Windows: the single recursive watcher. Null on Linux. */
109
+ private recursiveWatcher;
110
+ /** Linux: one watcher per watched directory (keyed by absolute path). */
111
+ private dirWatchers;
112
+ /** Set once the per-directory watch cap is hit, so we log only once. */
113
+ private dirCapWarned;
114
+ /** Test-only inert mode: started, but with no OS watcher installed. */
115
+ private inert;
82
116
  private debounceTimer;
83
117
  /**
84
118
  * Files seen by the watcher since the last successful sync — populated on
85
- * every chokidar event, cleared at the start of a sync, and re-populated by
119
+ * every change event, cleared at the start of a sync, and re-populated by
86
120
  * events that arrive mid-sync (or restored on sync failure). Keyed by the
87
121
  * same project-relative POSIX path the rest of the codebase uses, so a
88
122
  * caller can intersect tool-response file paths against this map cheaply.
@@ -98,17 +132,18 @@ export declare class FileWatcher {
98
132
  private syncing;
99
133
  private stopped;
100
134
  /**
101
- * False until chokidar fires its `ready` event. Gates `pendingFiles`
102
- * insertion so the initial crawl's `add` events (one per pre-existing
103
- * source file) don't pollute the per-file staleness signal. The events
104
- * still flow into `scheduleSync()` to preserve the previous "initial
105
- * scan triggers a reconciling sync" behavior.
135
+ * True once the initial watch set is established. Unlike the previous
136
+ * chokidar implementation there is no asynchronous initial "crawl" emitting
137
+ * an `add` per existing file `fs.watch` only reports changes from the
138
+ * moment it's installed so this flips to true synchronously at the end of
139
+ * `start()`. The startup reconcile against on-disk state is handled
140
+ * separately by the engine's catch-up sync, not by the watcher.
106
141
  */
107
- private chokidarReady;
142
+ private ready;
108
143
  /**
109
- * Callbacks that resolve when chokidar fires `ready`. Used by tests (and
110
- * any production caller that cares about a clean baseline) to deterministically
111
- * gate on the end of the initial scan instead of guessing at a sleep duration.
144
+ * Callbacks that resolve when the watch set is established. Used by tests
145
+ * (and any production caller that cares about a clean baseline) to
146
+ * deterministically gate on watcher readiness.
112
147
  */
113
148
  private readyWaiters;
114
149
  private ignoreMatcher;
@@ -117,6 +152,7 @@ export declare class FileWatcher {
117
152
  private readonly syncFn;
118
153
  private readonly onSyncComplete?;
119
154
  private readonly onSyncError?;
155
+ private readonly inertForTests;
120
156
  constructor(projectRoot: string, syncFn: () => Promise<{
121
157
  filesChanged: number;
122
158
  durationMs: number;
@@ -126,32 +162,79 @@ export declare class FileWatcher {
126
162
  * Returns true if watching started successfully, false otherwise.
127
163
  */
128
164
  start(): boolean;
165
+ /**
166
+ * macOS/Windows: one recursive watcher for the whole tree. O(1) descriptors.
167
+ * `filename` arrives relative to the project root (with subdirectories), so
168
+ * it maps straight to a project-relative path.
169
+ */
170
+ private startRecursive;
171
+ /**
172
+ * Linux: walk the (non-ignored) tree and watch each directory. One inotify
173
+ * watch per directory reports create/modify/delete for that directory's
174
+ * direct children, so we never watch individual files.
175
+ */
176
+ private startPerDirectory;
177
+ /**
178
+ * Add an inotify watch for `dir` and recurse into its non-ignored
179
+ * subdirectories. When `markExisting` is true (a directory that appeared
180
+ * AFTER startup), the source files already inside it are recorded as pending
181
+ * — this closes the `mkdir + write` race where files created before the new
182
+ * directory's watch is installed would otherwise be missed until the next
183
+ * full sync. The initial startup walk passes false (the engine's catch-up
184
+ * sync owns the baseline).
185
+ */
186
+ private watchTree;
187
+ /**
188
+ * Linux per-directory event handler. `filename` is relative to `dir`. A new
189
+ * sub-directory is picked up by extending the watch tree; everything else is
190
+ * routed through the shared change handler.
191
+ */
192
+ private handleDirEvent;
193
+ /**
194
+ * Shared change handler for both watch strategies. `rel` is a
195
+ * project-relative POSIX path. Applies the ignore + source-file filters and,
196
+ * for a real source change, records it as pending (#403) and schedules a
197
+ * debounced sync.
198
+ *
199
+ * The recursive (macOS/Windows) watcher reports events for ignored trees too
200
+ * (one stream covers the whole repo), so the ignore check here is load-bearing
201
+ * — it drops node_modules/dist/.git churn before any sync is scheduled.
202
+ */
203
+ private handleChange;
204
+ /** Close and forget the watch for a directory that errored/was removed. */
205
+ private unwatchDir;
129
206
  /** Our own dirs are always ignored, regardless of .gitignore. */
130
207
  private isAlwaysIgnored;
131
208
  /**
132
- * chokidar `ignored` predicate — true for any path that should NOT be watched.
133
- * Uses chokidar's provided `stats` to decide directory-vs-file so a dir-only
134
- * rule like `build/` matches, without an extra `statSync` per path.
209
+ * True for any directory that should NOT be watched (used while building the
210
+ * Linux per-directory watch tree). Tests the directory form of the path so a
211
+ * dir-only ignore rule like `build/` matches.
135
212
  */
136
- private shouldIgnore;
213
+ private shouldIgnoreDir;
137
214
  /**
138
215
  * Stop watching for file changes.
139
216
  */
140
217
  stop(): void;
218
+ /**
219
+ * @internal Test-only: feed a synthetic project-relative change through the
220
+ * same filter → pendingFiles → debounced-sync path a real fs.watch event
221
+ * takes. Lets the watcher / staleness-banner suites stay deterministic
222
+ * instead of racing on OS watch-delivery latency. See
223
+ * {@link __emitWatchEventForTests}.
224
+ */
225
+ ingestEventForTests(relPath: string): void;
141
226
  /**
142
227
  * Whether the watcher is currently active.
143
228
  */
144
229
  isActive(): boolean;
145
230
  /**
146
- * Resolves once chokidar has fired its `ready` event (or immediately if
147
- * it has already done so). Useful for tests that need a deterministic
148
- * boundary before asserting on `pendingFiles` — guessing a sleep duration
149
- * is flaky under load because chokidar can take longer than expected to
150
- * finish its initial crawl on slow filesystems / parallel test runs.
231
+ * Resolves once the watch set has been installed (or immediately if it
232
+ * already has). Useful for tests that need a deterministic boundary before
233
+ * asserting on `pendingFiles`.
151
234
  *
152
235
  * Production callers don't need this: `pendingFiles` is read continuously,
153
- * the staleness banner is always correct (empty or populated), and the
154
- * initial-scan window is a small one-time startup cost.
236
+ * the staleness banner is always correct (empty or populated), and there is
237
+ * no asynchronous initial-scan window with `fs.watch`.
155
238
  */
156
239
  waitUntilReady(timeoutMs?: number): Promise<void>;
157
240
  /**
@@ -188,4 +271,13 @@ export declare class FileWatcher {
188
271
  */
189
272
  getPendingFiles(): PendingFile[];
190
273
  }
274
+ /**
275
+ * Test-only: synthesize a source-file change for the live watcher running at
276
+ * `projectRoot`, exercising the real filter → pendingFiles → debounced-sync
277
+ * logic without depending on fs.watch delivery timing (which races under
278
+ * parallel vitest). `relPath` is project-relative POSIX (e.g. "src/foo.ts").
279
+ * Returns false if no live watcher is registered for that root (e.g. outside a
280
+ * test runtime, where the registry is intentionally not populated).
281
+ */
282
+ export declare function __emitWatchEventForTests(projectRoot: string, relPath: string): boolean;
191
283
  //# sourceMappingURL=watcher.d.ts.map
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Anonymous usage telemetry — client side.
3
+ *
4
+ * The contract for what may be collected lives in docs/design/telemetry.md
5
+ * (and user-facing TELEMETRY.md); the ingest endpoint that enforces it is
6
+ * public at telemetry-worker/. This module honors four invariants:
7
+ *
8
+ * 1. Zero hot-path cost: recording is an in-memory increment. Disk writes are
9
+ * a tiny synchronous append at process exit (works under `process.exit()`,
10
+ * where `beforeExit` never fires); network sends happen opportunistically
11
+ * (startup of long-running commands, daemon interval, bounded await at the
12
+ * end of install/init) and are fire-and-forget everywhere else.
13
+ * 2. Zero stdout: stdio is the MCP protocol channel. Notices and debug output
14
+ * go to stderr only.
15
+ * 3. Off is off: when disabled, nothing is recorded, nothing is sent, and no
16
+ * socket is opened — there is no "opted out" ping. Turning telemetry off
17
+ * also deletes any buffered, unsent data.
18
+ * 4. Fail silent: offline, endpoint down, disk full — every failure mode is
19
+ * silence, never a retry loop, never an error surfaced to the user/agent.
20
+ *
21
+ * Usage counts aggregate locally into per-day rollups; only *completed* (UTC)
22
+ * days are sent, so volume scales with active machines, not with tool calls.
23
+ */
24
+ export declare const TELEMETRY_ENDPOINT = "https://telemetry.getcodegraph.com/v1/events";
25
+ export declare const TELEMETRY_DOCS = "https://github.com/colbymchenry/codegraph/blob/main/TELEMETRY.md";
26
+ export type UsageKind = 'mcp_tool' | 'cli_command';
27
+ export type LifecycleEvent = 'install' | 'index' | 'uninstall';
28
+ /** Coarse buckets — exact counts are deliberately not collected. */
29
+ export declare function bucketFileCount(n: number): '<100' | '100-1k' | '1k-10k' | '10k+';
30
+ export declare function bucketDuration(ms: number): '<10s' | '10-60s' | '1-5m' | '5m+';
31
+ /** Collapse a backend identifier (e.g. `node-sqlite`) to the schema's enum. */
32
+ export declare function backendKind(backend: string): 'native' | 'wasm';
33
+ /**
34
+ * Shared "a full index completed" event (CLI init/index + installer local
35
+ * init): language names and coarse buckets only — never paths, file names,
36
+ * or exact counts. Structurally typed so callers don't need engine imports.
37
+ */
38
+ export declare function recordIndexEvent(cg: {
39
+ getStats(): {
40
+ filesByLanguage: Record<string, number>;
41
+ };
42
+ getBackend(): string;
43
+ }, result: {
44
+ filesIndexed: number;
45
+ durationMs: number;
46
+ }): void;
47
+ export interface ClientInfo {
48
+ name?: string;
49
+ version?: string;
50
+ }
51
+ export interface TelemetryStatus {
52
+ enabled: boolean;
53
+ /** What decided the current state — mirrors the precedence order. */
54
+ decidedBy: 'DO_NOT_TRACK' | 'CODEGRAPH_TELEMETRY' | 'config' | 'default';
55
+ machineId: string | null;
56
+ configPath: string;
57
+ }
58
+ export interface TelemetryOptions {
59
+ /** Global state dir; defaults to ~/.codegraph. Tests inject a temp dir. */
60
+ dir?: string;
61
+ fetchImpl?: typeof globalThis.fetch;
62
+ now?: () => Date;
63
+ env?: NodeJS.ProcessEnv;
64
+ stderr?: (line: string) => void;
65
+ /** Tests opt out so short-lived instances don't pile onto process 'exit'. */
66
+ installExitHook?: boolean;
67
+ }
68
+ export declare class Telemetry {
69
+ private readonly dir;
70
+ private readonly fetchImpl;
71
+ private readonly now;
72
+ private readonly env;
73
+ private readonly writeStderr;
74
+ private counts;
75
+ private events;
76
+ private readonly installExitHook;
77
+ private exitHookInstalled;
78
+ private configCache;
79
+ private intervalHandle;
80
+ constructor(opts?: TelemetryOptions);
81
+ get configPath(): string;
82
+ get queuePath(): string;
83
+ /**
84
+ * Resolution order (first match wins) — keep in sync with TELEMETRY.md:
85
+ * DO_NOT_TRACK=1 > CODEGRAPH_TELEMETRY=0|1 > stored config > default on.
86
+ */
87
+ getStatus(): TelemetryStatus;
88
+ isEnabled(): boolean;
89
+ /**
90
+ * Persist an explicit user choice (installer toggle or `codegraph
91
+ * telemetry on|off`). Turning telemetry off also deletes any buffered,
92
+ * unsent data — off means off.
93
+ */
94
+ setEnabled(enabled: boolean, source: 'installer' | 'cli'): void;
95
+ /** True once any consent decision (or the first-run notice) is on disk. */
96
+ hasStoredChoice(): boolean;
97
+ /** In-memory increment — safe on the MCP tool-call hot path. */
98
+ recordUsage(kind: UsageKind, name: string, ok: boolean, client?: ClientInfo): void;
99
+ /** install / index / uninstall — buffered like everything else. */
100
+ recordLifecycle(event: LifecycleEvent, props: Record<string, unknown>): void;
101
+ /**
102
+ * Fire-and-forget send of everything sendable. Never throws, never logs
103
+ * above debug. Safe to call at startup of long-running commands.
104
+ */
105
+ maybeFlush(): void;
106
+ /**
107
+ * Drain in-memory state to the buffer, then send completed-day rollups and
108
+ * lifecycle events. Bounded by `timeoutMs`; leftovers stay buffered for the
109
+ * next process. Awaited only where latency is invisible (install/init).
110
+ */
111
+ flushNow(timeoutMs?: number): Promise<void>;
112
+ /**
113
+ * Periodic flush for long-lived processes (MCP daemon / serve). Unref'd so
114
+ * it never keeps the process alive.
115
+ */
116
+ startInterval(everyMs?: number): void;
117
+ stopInterval(): void;
118
+ private utcDay;
119
+ private readConfig;
120
+ private writeConfig;
121
+ /**
122
+ * Default-on consent is gated by a one-time stderr notice (interactive
123
+ * installs record their choice explicitly and never reach this).
124
+ */
125
+ private firstRunNotice;
126
+ /**
127
+ * Synchronous, tiny, exit-safe: drain in-memory deltas to the JSONL queue.
128
+ * Runs on `process.on('exit')`, so it must never be async or slow.
129
+ */
130
+ persistSync(): void;
131
+ private appendLines;
132
+ /**
133
+ * Atomically claim the queue for sending (rename). Concurrent processes
134
+ * can't double-send; a crash mid-send leaves a claim file that
135
+ * `recoverStaleClaims` merges back after an hour.
136
+ */
137
+ private claimQueue;
138
+ private recoverStaleClaims;
139
+ /** Returns the lines that did NOT make it out (to be re-queued). */
140
+ private send;
141
+ private packageVersion;
142
+ private ensureExitHook;
143
+ private debug;
144
+ }
145
+ export declare function getTelemetry(): Telemetry;
146
+ //# sourceMappingURL=index.d.ts.map
package/dist/types.d.ts CHANGED
@@ -20,7 +20,7 @@ export type EdgeKind = 'contains' | 'calls' | 'imports' | 'exports' | 'extends'
20
20
  * Supported programming languages. See NODE_KINDS for why this is a
21
21
  * runtime-iterable const array.
22
22
  */
23
- export declare const LANGUAGES: readonly ["typescript", "javascript", "tsx", "jsx", "python", "go", "rust", "java", "c", "cpp", "csharp", "php", "ruby", "swift", "kotlin", "dart", "svelte", "vue", "liquid", "pascal", "scala", "lua", "luau", "objc", "yaml", "twig", "xml", "properties", "unknown"];
23
+ export declare const LANGUAGES: readonly ["typescript", "javascript", "tsx", "jsx", "python", "go", "rust", "java", "c", "cpp", "csharp", "razor", "php", "ruby", "swift", "kotlin", "dart", "svelte", "vue", "astro", "liquid", "pascal", "scala", "lua", "luau", "objc", "r", "yaml", "twig", "xml", "properties", "unknown"];
24
24
  export type Language = (typeof LANGUAGES)[number];
25
25
  /**
26
26
  * A node in the knowledge graph representing a code symbol
@@ -64,6 +64,14 @@ export interface Node {
64
64
  decorators?: string[];
65
65
  /** Generic type parameters */
66
66
  typeParameters?: string[];
67
+ /**
68
+ * Normalized return/result type name for a function/method (the bare class
69
+ * name, smart-pointer pointee unwrapped). Captured for C/C++ so resolution
70
+ * can infer a chained receiver's type from what the inner call returns —
71
+ * `Foo::instance().bar()` resolves `bar` on `Foo` (issue #645). Undefined for
72
+ * languages/symbols where it isn't captured.
73
+ */
74
+ returnType?: string;
67
75
  /** When the node was last updated */
68
76
  updatedAt: number;
69
77
  }
@@ -139,6 +147,13 @@ export interface ExtractionError {
139
147
  /** Error code for categorization */
140
148
  code?: string;
141
149
  }
150
+ /**
151
+ * Kinds an unresolved reference can carry. `function_ref` is internal-only —
152
+ * a function name used as a VALUE (callback registration, #756). It never
153
+ * becomes an edge kind: resolution maps it to a `references` edge targeting
154
+ * function/method nodes only (see `matchFunctionRef`).
155
+ */
156
+ export type ReferenceKind = EdgeKind | 'function_ref';
142
157
  /**
143
158
  * A reference that couldn't be resolved during extraction
144
159
  */
@@ -148,7 +163,7 @@ export interface UnresolvedReference {
148
163
  /** Name being referenced */
149
164
  referenceName: string;
150
165
  /** Type of reference (call, type, import, etc.) */
151
- referenceKind: EdgeKind;
166
+ referenceKind: ReferenceKind;
152
167
  /** Location of the reference */
153
168
  line: number;
154
169
  column: number;