@colbymchenry/codegraph 1.1.5 → 1.2.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.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## 🎉 1.0 Released!
6
6
 
7
- Already installed? Run `codegraph upgrade` to update in place.
7
+ Already installed? Run `codegraph upgrade`
8
8
 
9
9
  Follow [@getcodegraph](https://x.com/getcodegraph) on X for updates.
10
10
 
@@ -119,4 +119,24 @@ export declare const DATABASE_FILENAME = "codegraph.db";
119
119
  * Get the default database path for a project
120
120
  */
121
121
  export declare function getDatabasePath(projectRoot: string): string;
122
+ /**
123
+ * Delete a database file and its WAL sidecars (`-wal`/`-shm`).
124
+ *
125
+ * This is how a FULL re-index discards an existing database — rather than
126
+ * opening the old graph and DELETE-ing every row. On a large or pre-fix
127
+ * poisoned index (e.g. an old graph that scanned an ignored gitlink corpus into
128
+ * ~1.6M nodes with a multi-GB WAL, #1065) the per-row `nodes_fts` delete-trigger
129
+ * churn blocks the main thread long enough to trip the #850 liveness watchdog
130
+ * before indexing even starts, so the rebuild could never recover the bad state
131
+ * (#1067). Unlinking is O(1) regardless of DB size and also reclaims the disk
132
+ * the bloated WAL would otherwise keep.
133
+ *
134
+ * POSIX removes the directory entry even while another process (a daemon/MCP
135
+ * server) still holds the file open; that holder heals via `reopenIfReplaced`
136
+ * (#925). On Windows a live holder can make the unlink fail with EBUSY/EPERM —
137
+ * that is thrown for the caller to surface ("stop the other process and retry").
138
+ * The `-wal`/`-shm` sidecars are best-effort: SQLite recreates them on the next
139
+ * open, so a leftover sidecar is harmless.
140
+ */
141
+ export declare function removeDatabaseFiles(dbPath: string): void;
122
142
  //# sourceMappingURL=index.d.ts.map
@@ -47,5 +47,22 @@ export declare const cExtractor: LanguageExtractor;
47
47
  * so C's heavier use of `struct TAG var;` never reaches it.
48
48
  */
49
49
  export declare function blankCppExportMacros(source: string): string;
50
+ export declare function blankCppInlineMacros(source: string): string;
51
+ /**
52
+ * Universal fallback (any macro, no list) for a C/C++ function name still mangled
53
+ * because a macro we don't blank sat in front of the return type: `MACRO Ret
54
+ * name(…)` / `Ret MACRO name(…)` misparse so the return type is glued onto the
55
+ * name ("Ret name", "char_t* to_str(double v)"). Recover the real identifier —
56
+ * the token immediately before the parameter list (or the last token). This runs
57
+ * AFTER the curated pre-parse blank, so it only ever sees the residual tail that
58
+ * blanking didn't already fix cleanly (which also recovers the return type).
59
+ *
60
+ * Safe by construction: only touches an ALREADY-mangled name — one with an
61
+ * internal space that isn't a legit `operator …`/destructor — so a well-formed
62
+ * name is returned unchanged. Guarded against the two ways it could mis-pick:
63
+ * the `Ret (name)` parenthesized-name idiom (left as-is, ambiguous), and a token
64
+ * that is a bare primitive/keyword rather than a real identifier.
65
+ */
66
+ export declare function recoverMangledCppName(name: string): string;
50
67
  export declare const cppExtractor: LanguageExtractor;
51
68
  //# sourceMappingURL=c-cpp.d.ts.map
@@ -114,6 +114,15 @@ export interface LanguageExtractor {
114
114
  returnField?: string;
115
115
  /** Override symbol name extraction (e.g. ObjC multi-part selectors). */
116
116
  resolveName?: (node: SyntaxNode, source: string) => string | undefined;
117
+ /**
118
+ * Post-process an already-extracted name to recover a real identifier from a
119
+ * name still mangled by a macro the pre-parse didn't blank (C/C++:
120
+ * `MACRO Ret name(` misparses to the name "Ret name"). Applied to every name
121
+ * this extractor produces, so it MUST be a no-op on a well-formed name — only
122
+ * C/C++ set it, because a mangled name there is unambiguous (an internal space),
123
+ * whereas e.g. Kotlin/Scala backtick identifiers legitimately contain spaces.
124
+ */
125
+ recoverMangledName?: (name: string) => string;
117
126
  /** Extract property name when the generic name walk fails (e.g. ObjC @property). */
118
127
  extractPropertyName?: (node: SyntaxNode, source: string) => string | null;
119
128
  /** Extract signature from node */
@@ -140,6 +149,14 @@ export interface LanguageExtractor {
140
149
  extraClassNodeTypes?: string[];
141
150
  /** Whether methods can be top-level without enclosing class (Go: true) */
142
151
  methodsAreTopLevel?: boolean;
152
+ /**
153
+ * Skip a bodiless class node as a forward declaration / elaborated type,
154
+ * mirroring the bodiless-struct/enum skip. Set only for languages where a
155
+ * bodiless `class` specifier is NOT a complete definition — C/C++
156
+ * (`class Foo;` is a forward decl). Leave unset for languages where a
157
+ * bodiless class IS complete (Kotlin `class Empty`, Scala `case object`). (#1093)
158
+ */
159
+ skipBodilessClass?: boolean;
143
160
  /** NodeKind to use for interface-like declarations (Rust: 'trait'). Default: 'interface' */
144
161
  interfaceKind?: NodeKind;
145
162
  /**
package/dist/index.d.ts CHANGED
@@ -110,6 +110,23 @@ export declare class CodeGraph {
110
110
  * @returns A CodeGraph instance
111
111
  */
112
112
  static open(projectRoot: string, options?: OpenOptions): Promise<CodeGraph>;
113
+ /**
114
+ * Rebuild the project's database from scratch and return a fresh, empty
115
+ * instance — the "same result as a fresh init" semantics that `codegraph
116
+ * index` documents.
117
+ *
118
+ * Unlike `open()` followed by `clear()`, this DISCARDS the existing
119
+ * `.codegraph/codegraph.db` (and its `-wal`/`-shm` sidecars) before
120
+ * re-initializing, instead of opening the old database and DELETE-ing every
121
+ * row. On a large or pre-fix poisoned index — e.g. an old graph that scanned
122
+ * an ignored gitlink corpus (#1065) into ~1.6M nodes with a multi-GB WAL —
123
+ * the per-row `nodes_fts` delete-trigger churn blocks the main thread long
124
+ * enough to trip the #850 liveness watchdog before indexing even starts, so a
125
+ * full re-index could never recover the bad state (#1067). Discarding the
126
+ * files is O(1) regardless of size, reclaims the disk, and sidesteps opening
127
+ * (and running migrations against) the poisoned database entirely.
128
+ */
129
+ static recreate(projectRoot: string): Promise<CodeGraph>;
113
130
  /**
114
131
  * Open synchronously (without sync)
115
132
  */
@@ -11,5 +11,5 @@ import type { ResolutionContext } from './types';
11
11
  * Sidekiq Worker.perform_async → #perform + Laravel event(new X) → listener handle).
12
12
  * Returns the count added. Never throws into indexing — callers wrap in try/catch.
13
13
  */
14
- export declare function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionContext): number;
14
+ export declare function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionContext): Promise<number>;
15
15
  //# sourceMappingURL=callback-synthesizer.d.ts.map
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Cooperative yielding for long synchronous resolution spans.
3
+ *
4
+ * Reference resolution and callback-edge synthesis run on the indexer's MAIN
5
+ * thread — unlike parsing, which is off-thread in the parse worker. The #850
6
+ * liveness watchdog (armed on `index`/`init` since #999) SIGKILLs the process
7
+ * when that thread doesn't turn its event loop for the timeout window (default
8
+ * 60s), because its heartbeat is a `setInterval` on that same thread. On a large
9
+ * repo, resolving refs + synthesizing dynamic-dispatch edges legitimately runs
10
+ * for minutes, so a span that never yields starves the heartbeat and the
11
+ * watchdog kills a VALID, in-progress index — the exact symptom of #1091 (the
12
+ * progress bar freezes at wherever it last rendered — 88% / 100% — then the
13
+ * process is killed).
14
+ *
15
+ * `createYielder` returns a `maybeYield()` that yields (via `setImmediate`) only
16
+ * once more than `budgetMs` of wall-clock has elapsed since the last yield, so
17
+ * fast repos pay essentially nothing while slow ones give the heartbeat a
18
+ * regular window to fire. Call it at natural boundaries in a long loop (between
19
+ * batches, between synthesis passes).
20
+ *
21
+ * This does NOT weaken the watchdog. A genuinely wedged loop — an infinite or
22
+ * non-terminating span, the case the watchdog exists to catch — never reaches a
23
+ * yield point, so the heartbeat still stops and the SIGKILL still fires. We only
24
+ * stop killing work that is demonstrably making progress.
25
+ */
26
+ /** Yield when more than `budgetMs` of wall-clock has passed since the last yield. */
27
+ export type MaybeYield = () => Promise<void>;
28
+ /** Default budget: well under the watchdog's minimum heartbeat cadence (~1s), so
29
+ * a heartbeat byte always has a chance to land between yields. */
30
+ export declare const DEFAULT_YIELD_BUDGET_MS = 250;
31
+ export declare function createYielder(budgetMs?: number): MaybeYield;
32
+ //# sourceMappingURL=cooperative-yield.d.ts.map
@@ -103,7 +103,16 @@ export declare class ReferenceResolver {
103
103
  * (re-resolving an already-resolved ref is a no-op since it's been deleted).
104
104
  * Returns the number of newly-created edges.
105
105
  */
106
- resolveChainedCallsViaConformance(): number;
106
+ resolveChainedCallsViaConformance(): Promise<number>;
107
+ /**
108
+ * Resolve one batch in smaller sub-chunks, yielding to the event loop between
109
+ * them so the #850 liveness heartbeat can fire on a slow/dense batch (#1091).
110
+ * Behaviourally identical to a single `resolveAll(batch)`: `warmCaches()` is
111
+ * idempotent (guarded) and `resolveOne` is independent per ref, so splitting
112
+ * and re-merging changes only timing, never which edges get created. Falls
113
+ * through to a plain `resolveAll` when the batch is already small.
114
+ */
115
+ private resolveBatchYielding;
107
116
  /**
108
117
  * Resolve and persist in batches to keep memory bounded.
109
118
  * Processes unresolved references in chunks, persisting edges and cleaning
@@ -172,7 +181,7 @@ export declare class ReferenceResolver {
172
181
  * Mirrors resolveChainedCallsViaConformance's lifecycle. Returns the number
173
182
  * of newly-created edges.
174
183
  */
175
- resolveDeferredThisMemberRefs(): number;
184
+ resolveDeferredThisMemberRefs(): Promise<number>;
176
185
  private gateLanguage;
177
186
  /**
178
187
  * Drop a FRAMEWORK-strategy resolution that crosses two *known* language
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Handles symbol name matching for reference resolution.
5
5
  */
6
+ import { Node } from '../types';
6
7
  import { UnresolvedRef, ResolvedRef, ResolutionContext } from './types';
7
8
  /**
8
9
  * Try to resolve a path-like reference (e.g., "snippets/drawer-menu.liquid")
@@ -47,6 +48,27 @@ export declare function matchByExactName(ref: UnresolvedRef, context: Resolution
47
48
  * Try to resolve by qualified name
48
49
  */
49
50
  export declare function matchByQualifiedName(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null;
51
+ /**
52
+ * When a symbol name is ambiguous across files, prefer the candidate(s) declared
53
+ * in the call site's own file, keeping the rest in their original order (#1079).
54
+ * A same-file definition is the strongest language-agnostic signal for which of
55
+ * several same-named symbols a call means; without it, resolution collapses onto
56
+ * whichever was indexed first, so a call in `b/svc` wrongly targets `a/svc`.
57
+ * No-op when there are <2 candidates or none share the call site's file.
58
+ */
59
+ export declare function preferCallSiteFile(nodes: Node[], callSiteFile: string): Node[];
60
+ export declare function resolveMethodOnType(typeName: string, methodName: string, ref: UnresolvedRef, context: ResolutionContext, confidence: number, resolvedBy: ResolvedRef['resolvedBy'],
61
+ /**
62
+ * Optional FQN that identifies WHICH class declaration `typeName`
63
+ * refers to in the caller's file. When multiple candidates share
64
+ * the same qualifiedName (`FooConverter::convert` in both
65
+ * `dao/converter/` and `service/converter/`), the FQN's
66
+ * file-path-suffix picks the right one — the disambiguation
67
+ * signal Java imports carry but the call site doesn't (#314).
68
+ */
69
+ preferredFqn?: string,
70
+ /** Recursion guard for the supertype/conformance walk. */
71
+ depth?: number): ResolvedRef | null;
50
72
  /**
51
73
  * Resolve a C++ chained call whose receiver is itself a call — encoded by the
52
74
  * extractor as `<innerCallee>().<method>` (#645). The receiver's type is what
package/npm-shim.js CHANGED
@@ -45,7 +45,7 @@ async function main() {
45
45
  // Happy path: the npm-installed optional dependency. Fall back to a download
46
46
  // when the registry didn't deliver it.
47
47
  var resolved = resolveInstalledBundle() || (await selfHealBundle());
48
- var res = childProcess.spawnSync(resolved.command, resolved.args, { stdio: 'inherit' });
48
+ var res = childProcess.spawnSync(resolved.command, resolved.args, { stdio: 'inherit', windowsHide: true });
49
49
  if (res.error) {
50
50
  process.stderr.write('codegraph: ' + res.error.message + '\n');
51
51
  process.exit(1);
@@ -105,7 +105,7 @@ async function selfHealBundle() {
105
105
  // Already downloaded by a previous run? Use it even when downloads are
106
106
  // disabled — CODEGRAPH_NO_DOWNLOAD blocks fetching, not a cached bundle.
107
107
  var cached = launcherIn(dest);
108
- if (cached) return cached;
108
+ if (cached) { pruneOldBundles(bundlesDir, dest); return cached; }
109
109
 
110
110
  if (process.env.CODEGRAPH_NO_DOWNLOAD) {
111
111
  fail('the network fallback is disabled (CODEGRAPH_NO_DOWNLOAD is set).');
@@ -149,6 +149,7 @@ async function selfHealBundle() {
149
149
 
150
150
  var ready = launcherIn(dest);
151
151
  if (!ready) fail('downloaded bundle is missing its launcher under ' + dest + '.');
152
+ pruneOldBundles(bundlesDir, dest);
152
153
  process.stderr.write('codegraph: bundle ready.\n');
153
154
  return ready;
154
155
  }
@@ -221,7 +222,7 @@ function extract(archive, destDir) {
221
222
  var args = isWindows
222
223
  ? ['-xf', archive, '-C', destDir, '--strip-components=1']
223
224
  : ['-xzf', archive, '-C', destDir, '--strip-components=1'];
224
- var res = childProcess.spawnSync('tar', args, { stdio: 'ignore' });
225
+ var res = childProcess.spawnSync('tar', args, { stdio: 'ignore', windowsHide: true });
225
226
  if (res.error) throw new Error('tar unavailable: ' + res.error.message);
226
227
  if (res.status !== 0) throw new Error('tar exited ' + res.status);
227
228
  }
@@ -230,6 +231,27 @@ function rmrf(p) {
230
231
  try { fs.rmSync(p, { recursive: true, force: true }); } catch (e) { /* best effort */ }
231
232
  }
232
233
 
234
+ // Drop sibling bundles for OTHER versions of this same platform target, keeping
235
+ // only keepDir. The self-heal cache otherwise accumulates a full ~50 MB bundle
236
+ // per version forever (issue #1074). Best-effort: a locked/busy dir (a
237
+ // concurrent run still mapping an older node.exe on Windows) just stays — rmrf
238
+ // already swallows its own errors, and the readdir is guarded — so cleanup can
239
+ // never break a working command. Only this target's "<target>-<version>" dirs
240
+ // are touched; other platforms' bundles and the ".dl-*" staging dirs are left
241
+ // alone.
242
+ function pruneOldBundles(bundlesDir, keepDir) {
243
+ var keep = path.basename(keepDir);
244
+ try {
245
+ var names = fs.readdirSync(bundlesDir);
246
+ for (var i = 0; i < names.length; i++) {
247
+ var name = names[i];
248
+ if (name === keep) continue;
249
+ if (name.indexOf(target + '-') !== 0) continue;
250
+ rmrf(path.join(bundlesDir, name));
251
+ }
252
+ } catch (e) { /* best effort — never break a working run over cleanup */ }
253
+ }
254
+
233
255
  function fail(reason) {
234
256
  process.stderr.write(
235
257
  'codegraph: no prebuilt bundle for ' + target + '.\n' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colbymchenry/codegraph",
3
- "version": "1.1.5",
3
+ "version": "1.2.0",
4
4
  "description": "Local-first code intelligence for AI agents (MCP). Self-contained — bundles its own runtime.",
5
5
  "bin": {
6
6
  "codegraph": "npm-shim.js"
@@ -15,12 +15,12 @@
15
15
  "./package.json": "./package.json"
16
16
  },
17
17
  "optionalDependencies": {
18
- "@colbymchenry/codegraph-darwin-arm64": "1.1.5",
19
- "@colbymchenry/codegraph-darwin-x64": "1.1.5",
20
- "@colbymchenry/codegraph-linux-arm64": "1.1.5",
21
- "@colbymchenry/codegraph-linux-x64": "1.1.5",
22
- "@colbymchenry/codegraph-win32-arm64": "1.1.5",
23
- "@colbymchenry/codegraph-win32-x64": "1.1.5"
18
+ "@colbymchenry/codegraph-darwin-arm64": "1.2.0",
19
+ "@colbymchenry/codegraph-darwin-x64": "1.2.0",
20
+ "@colbymchenry/codegraph-linux-arm64": "1.2.0",
21
+ "@colbymchenry/codegraph-linux-x64": "1.2.0",
22
+ "@colbymchenry/codegraph-win32-arm64": "1.2.0",
23
+ "@colbymchenry/codegraph-win32-x64": "1.2.0"
24
24
  },
25
25
  "files": [
26
26
  "npm-shim.js",