@colbymchenry/codegraph 1.1.1 → 1.1.3
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 +12 -0
- package/dist/bin/command-supervision.d.ts +12 -0
- package/dist/db/migrations.d.ts +1 -1
- package/dist/directory.d.ts +32 -0
- package/dist/extraction/index.d.ts +13 -1
- package/dist/extraction/languages/c-cpp.d.ts +16 -0
- package/dist/extraction/parse-pool.d.ts +126 -0
- package/dist/extraction/tree-sitter.d.ts +21 -0
- package/dist/mcp/daemon-paths.d.ts +30 -3
- package/dist/mcp/daemon.d.ts +48 -0
- package/dist/mcp/engine.d.ts +17 -0
- package/dist/mcp/ppid-watchdog.d.ts +18 -0
- package/dist/mcp/query-pool.d.ts +94 -0
- package/dist/mcp/query-worker.d.ts +24 -0
- package/dist/mcp/tools.d.ts +57 -0
- package/dist/project-config.d.ts +20 -0
- package/dist/resolution/c-fnptr-synthesizer.d.ts +0 -28
- package/dist/sync/worktree.d.ts +9 -0
- package/dist/types.d.ts +6 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -606,6 +606,18 @@ add a negation — `!vendor/`. The defaults apply uniformly, so committing a
|
|
|
606
606
|
dependency or build directory doesn't force it into the graph; the `.gitignore`
|
|
607
607
|
negation is the explicit opt-in.
|
|
608
608
|
|
|
609
|
+
`.gitignore` can't drop a directory you've **committed**, though. For a vendored
|
|
610
|
+
theme or SDK that's checked into the repo (e.g. a Metronic theme under
|
|
611
|
+
`static/`), list it under `exclude` in `codegraph.json` — gitignore-style
|
|
612
|
+
patterns, matched against repo-root-relative paths, honored on index, sync, and
|
|
613
|
+
watch:
|
|
614
|
+
|
|
615
|
+
```json
|
|
616
|
+
{
|
|
617
|
+
"exclude": ["static/", "**/vendor/**"]
|
|
618
|
+
}
|
|
619
|
+
```
|
|
620
|
+
|
|
609
621
|
### Custom file extensions
|
|
610
622
|
|
|
611
623
|
If your project uses a non-standard extension for a [supported
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface CommandSupervision {
|
|
2
|
+
/** Tear down both watchdogs. Idempotent; call when the command finishes. */
|
|
3
|
+
stop(): void;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Install the liveness + PPID watchdogs for the duration of a CLI command.
|
|
7
|
+
* `label` is used in the shutdown notice (e.g. `"index"`). Returns a handle
|
|
8
|
+
* whose `stop()` must be called when the command completes so neither watchdog
|
|
9
|
+
* outlives it.
|
|
10
|
+
*/
|
|
11
|
+
export declare function installCommandSupervision(label: string): CommandSupervision;
|
|
12
|
+
//# sourceMappingURL=command-supervision.d.ts.map
|
package/dist/db/migrations.d.ts
CHANGED
package/dist/directory.d.ts
CHANGED
|
@@ -85,6 +85,38 @@ export declare function findIndexedSubprojectRoots(root: string, opts?: {
|
|
|
85
85
|
maxDepth?: number;
|
|
86
86
|
max?: number;
|
|
87
87
|
}): string[];
|
|
88
|
+
/**
|
|
89
|
+
* Does `prompt` contain an explicit structural keyword (English or CJK)? A
|
|
90
|
+
* keyword is a strong, self-contained signal, so the front-load hook fires on it
|
|
91
|
+
* directly — no graph check needed. (A *code-token* match, by contrast, is only
|
|
92
|
+
* a candidate the hook verifies against the graph first; see {@link extractCodeTokens}.)
|
|
93
|
+
*/
|
|
94
|
+
export declare function hasStructuralKeyword(prompt: string): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Identifier-shaped tokens in `prompt` — camelCase / PascalCase-with-inner-cap,
|
|
97
|
+
* snake_case, a `name(` call, or the two sides of an `a.b` member access. Naming
|
|
98
|
+
* a symbol is a code question whatever the surrounding human language, and these
|
|
99
|
+
* shapes almost never occur in ordinary prose, so they catch the common
|
|
100
|
+
* "<symbol> 的调用链?" / "where is <symbol> 定義" prompts no keyword list would.
|
|
101
|
+
*
|
|
102
|
+
* These are *candidates*, not a verdict: a tech brand like `JavaScript` or
|
|
103
|
+
* `GitHub` is identifier-shaped too, so the front-load hook checks each token
|
|
104
|
+
* against the actual index ({@link getNodesByName}) and only fires when one is a
|
|
105
|
+
* real symbol here — otherwise a brand-name prompt would inject ~16KB of
|
|
106
|
+
* low-relevance context (issue #994 follow-up). A doc/data filename ("README.md")
|
|
107
|
+
* is excluded from the member-access form since it's a file reference, not a symbol.
|
|
108
|
+
*/
|
|
109
|
+
export declare function extractCodeTokens(prompt: string): string[];
|
|
110
|
+
/**
|
|
111
|
+
* Cheap, graph-free candidate gate for the front-load hook: could `prompt` be a
|
|
112
|
+
* structural / flow / impact / "where-how" question worth front-loading context
|
|
113
|
+
* for? True on an explicit keyword (English or CJK, issue #994) OR an
|
|
114
|
+
* identifier-shaped token. A keyword is sufficient to fire on its own; a
|
|
115
|
+
* token-only match is only a candidate the hook then verifies against the graph
|
|
116
|
+
* (a brand name like `JavaScript` is token-shaped but isn't a symbol). Every
|
|
117
|
+
* non-candidate prompt ("fix this typo", in any language) stays a zero-cost no-op.
|
|
118
|
+
*/
|
|
119
|
+
export declare function isStructuralPrompt(prompt: string): boolean;
|
|
88
120
|
/**
|
|
89
121
|
* What the front-load hook should do for a prompt issued from a directory.
|
|
90
122
|
*/
|
|
@@ -66,12 +66,24 @@ export declare function buildDefaultIgnore(rootDir: string): Ignore;
|
|
|
66
66
|
*/
|
|
67
67
|
export declare class ScopeIgnore {
|
|
68
68
|
private rootMatcher;
|
|
69
|
+
/**
|
|
70
|
+
* Project `codegraph.json` `exclude` patterns (#999), matched against the
|
|
71
|
+
* full root-relative path. Wins over everything else — an explicit user
|
|
72
|
+
* exclude applies even to tracked files and even inside embedded repos.
|
|
73
|
+
*/
|
|
74
|
+
private exclude;
|
|
69
75
|
private embedded;
|
|
70
76
|
private defaults;
|
|
71
77
|
constructor(rootMatcher: Ignore, embedded: Array<{
|
|
72
78
|
root: string;
|
|
73
79
|
matcher: Ignore;
|
|
74
|
-
}
|
|
80
|
+
}>,
|
|
81
|
+
/**
|
|
82
|
+
* Project `codegraph.json` `exclude` patterns (#999), matched against the
|
|
83
|
+
* full root-relative path. Wins over everything else — an explicit user
|
|
84
|
+
* exclude applies even to tracked files and even inside embedded repos.
|
|
85
|
+
*/
|
|
86
|
+
exclude?: Ignore | null);
|
|
75
87
|
ignores(rel: string): boolean;
|
|
76
88
|
}
|
|
77
89
|
/**
|
|
@@ -7,6 +7,22 @@ import type { LanguageExtractor } from '../tree-sitter-types';
|
|
|
7
7
|
* template args. Returns undefined for primitives / void / `auto` / empty.
|
|
8
8
|
*/
|
|
9
9
|
export declare function normalizeCppReturnType(raw: string): string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Strip C++ template arguments from a base-type reference name so it matches the
|
|
12
|
+
* bare class/struct the template was DEFINED as. `template<typename T> class
|
|
13
|
+
* Base { … }` is indexed as a node named `Base`, but a derived class
|
|
14
|
+
* `class D : public Base<int>` records its base as the full `Base<int>` (and
|
|
15
|
+
* `class Q : public ns::Tpl<int>` as `ns::Tpl<int>`) — neither name-matches
|
|
16
|
+
* `Base` / `ns::Tpl`, so the `extends` edge never resolves and the derived class
|
|
17
|
+
* looks like it inherits from nothing (#1043).
|
|
18
|
+
*
|
|
19
|
+
* Removes every balanced `<…>` group regardless of nesting or position, so
|
|
20
|
+
* `Base<int>` → `Base`, `ns::Tpl<Foo<int>>` → `ns::Tpl`, and the rare
|
|
21
|
+
* `Outer<int>::Inner` → `Outer::Inner`. The remaining qualified head is exactly
|
|
22
|
+
* what the non-templated base case already produces, so resolution treats them
|
|
23
|
+
* identically. A name with no template args passes through unchanged.
|
|
24
|
+
*/
|
|
25
|
+
export declare function stripCppTemplateArgs(name: string): string;
|
|
10
26
|
export declare const cExtractor: LanguageExtractor;
|
|
11
27
|
export declare const cppExtractor: LanguageExtractor;
|
|
12
28
|
//# sourceMappingURL=c-cpp.d.ts.map
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse worker pool — runs tree-sitter parsing across N worker threads so a full
|
|
3
|
+
* `codegraph index` uses every core instead of pinning one.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: `ExtractionOrchestrator.indexAll()` already reads files in
|
|
6
|
+
* parallel, but it parsed them through a SINGLE worker thread, so on an
|
|
7
|
+
* N-core machine indexing a large repo used one core and left the rest idle
|
|
8
|
+
* (issue #1015, the parse-time half of #320). Spreading the parse calls across a
|
|
9
|
+
* pool of workers — each its own tree-sitter WASM heap — restores multi-core
|
|
10
|
+
* throughput. SQLite storage stays on the main thread (it isn't thread-safe), so
|
|
11
|
+
* only the CPU-bound parse step is parallelised; results are stored as they
|
|
12
|
+
* arrive, in whatever order they finish.
|
|
13
|
+
*
|
|
14
|
+
* Design mirrors {@link ../mcp/query-pool} (idle-list dispatch, lazy growth,
|
|
15
|
+
* throttled cold-starts, crash recovery), with parse-specific behaviour:
|
|
16
|
+
* - per-worker recycle: WASM linear memory grows but never shrinks, so each
|
|
17
|
+
* worker is torn down and replaced after `recycleInterval` parses to reclaim
|
|
18
|
+
* its heap — the same reason the old single worker recycled.
|
|
19
|
+
* - reject, don't retry: a parse that crashes or times out its worker REJECTS
|
|
20
|
+
* (with a message the orchestrator's retry pass recognises) rather than being
|
|
21
|
+
* silently requeued — the orchestrator owns the smarter two-stage retry
|
|
22
|
+
* (fresh worker, then comment-stripped) on a clean WASM heap.
|
|
23
|
+
* - a size-1 pool reproduces the old single-worker path exactly, which is the
|
|
24
|
+
* conservative rollback: set `CODEGRAPH_PARSE_WORKERS=1`.
|
|
25
|
+
*
|
|
26
|
+
* Memory: peak scales with pool size (≈ size × a worker's pre-recycle heap), so
|
|
27
|
+
* the default is capped and the env var lets constrained machines dial it down.
|
|
28
|
+
*/
|
|
29
|
+
import type { Language, ExtractionResult } from '../types';
|
|
30
|
+
/**
|
|
31
|
+
* Minimal worker surface the pool drives — satisfied by a real `worker_threads`
|
|
32
|
+
* Worker. Abstracted so tests can inject a fake worker and exercise the pool's
|
|
33
|
+
* queue / growth / recycle / crash-recovery logic without spawning threads or a
|
|
34
|
+
* built `dist/`.
|
|
35
|
+
*/
|
|
36
|
+
export interface ParsePoolWorker {
|
|
37
|
+
postMessage(msg: unknown): void;
|
|
38
|
+
terminate(): Promise<number> | void;
|
|
39
|
+
on(event: 'message', cb: (m: unknown) => void): void;
|
|
40
|
+
on(event: 'error', cb: (e: Error) => void): void;
|
|
41
|
+
on(event: 'exit', cb: (code: number) => void): void;
|
|
42
|
+
}
|
|
43
|
+
/** A single file to parse. `language` is resolved on the main thread (it holds
|
|
44
|
+
* the project's codegraph.json extension overrides) and handed to the worker. */
|
|
45
|
+
export interface ParseTask {
|
|
46
|
+
filePath: string;
|
|
47
|
+
content: string;
|
|
48
|
+
language: Language;
|
|
49
|
+
frameworkNames?: string[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the pool size from the `CODEGRAPH_PARSE_WORKERS` override and the
|
|
53
|
+
* machine's core count.
|
|
54
|
+
* - explicit `0` or `1` → 1 worker (the old single-worker path; the rollback).
|
|
55
|
+
* - explicit `N` → N, clamped to [1, 16].
|
|
56
|
+
* - unset / blank / non-numeric → `clamp(cores - 1, 1, 8)` (leave a core for
|
|
57
|
+
* the main thread + UI; never zero — parsing always needs a worker).
|
|
58
|
+
*/
|
|
59
|
+
export declare function resolveParsePoolSize(envVal: string | undefined, cpuCount: number): number;
|
|
60
|
+
export interface ParseWorkerPoolOptions {
|
|
61
|
+
/** Languages to load grammars for in every worker at spawn. */
|
|
62
|
+
languages: Language[];
|
|
63
|
+
/** Number of worker threads (≥1). Clamp the resolved value before passing. */
|
|
64
|
+
size: number;
|
|
65
|
+
/** Compiled `parse-worker.js` path. Required unless `createWorker` is given. */
|
|
66
|
+
workerScriptPath?: string;
|
|
67
|
+
/** Parses per worker before recycle. Default 250. */
|
|
68
|
+
recycleInterval?: number;
|
|
69
|
+
/** Base per-parse timeout (ms); scaled by file size per parse. Default 10s. */
|
|
70
|
+
parseTimeoutMs?: number;
|
|
71
|
+
/** Worker factory (tests inject a fake). Defaults to a real `worker_threads` Worker. */
|
|
72
|
+
createWorker?: () => ParsePoolWorker;
|
|
73
|
+
/** Optional verbose logger (the orchestrator's `[worker] …` logger). */
|
|
74
|
+
log?: (msg: string) => void;
|
|
75
|
+
}
|
|
76
|
+
export declare class ParseWorkerPool {
|
|
77
|
+
private idle;
|
|
78
|
+
private queue;
|
|
79
|
+
private inflight;
|
|
80
|
+
private workers;
|
|
81
|
+
private pending;
|
|
82
|
+
private parseCounts;
|
|
83
|
+
private nextId;
|
|
84
|
+
private totalCrashes;
|
|
85
|
+
private destroyed;
|
|
86
|
+
private readonly languages;
|
|
87
|
+
private readonly maxSize;
|
|
88
|
+
private readonly recycleInterval;
|
|
89
|
+
private readonly parseTimeoutMs;
|
|
90
|
+
private readonly createWorker;
|
|
91
|
+
private readonly log;
|
|
92
|
+
constructor(opts: ParseWorkerPoolOptions);
|
|
93
|
+
/** Pool size cap (for logging). */
|
|
94
|
+
get size(): number;
|
|
95
|
+
/** Live worker count (for tests). */
|
|
96
|
+
get liveWorkers(): number;
|
|
97
|
+
/** False once the crash budget is exhausted (or after destroy). */
|
|
98
|
+
get healthy(): boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Parse one file on the pool. Resolves with the extraction result, or REJECTS
|
|
101
|
+
* if the parse times out or its worker crashes — the caller records the error
|
|
102
|
+
* and (for worker-exit/OOM rejections) re-attempts in its retry pass.
|
|
103
|
+
*/
|
|
104
|
+
requestParse(task: ParseTask): Promise<ExtractionResult>;
|
|
105
|
+
private spawnOne;
|
|
106
|
+
private onMessage;
|
|
107
|
+
/** A worker died (crash hook / OOM exit / spawn error). Reject its in-flight
|
|
108
|
+
* parse so the caller's retry pass can re-attempt it, then respawn. */
|
|
109
|
+
private onWorkerGone;
|
|
110
|
+
/** Tear down a worker that has hit its recycle threshold and replace it. Not a
|
|
111
|
+
* crash, so it doesn't count against the budget. */
|
|
112
|
+
private recycle;
|
|
113
|
+
private removeWorker;
|
|
114
|
+
private dispatch;
|
|
115
|
+
private onTimeout;
|
|
116
|
+
private drain;
|
|
117
|
+
private settle;
|
|
118
|
+
/**
|
|
119
|
+
* Recycle every idle worker now (fresh WASM heaps). The orchestrator calls
|
|
120
|
+
* this before its retry pass so crash-on-memory files get the cleanest heap.
|
|
121
|
+
*/
|
|
122
|
+
recycleAll(): void;
|
|
123
|
+
/** Terminate all workers and reject any outstanding parses. */
|
|
124
|
+
destroy(): Promise<void>;
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=parse-pool.d.ts.map
|
|
@@ -438,6 +438,27 @@ export declare class TreeSitterExtractor {
|
|
|
438
438
|
* arguments (`new Foo(bar())`) get their own `calls` references.
|
|
439
439
|
*/
|
|
440
440
|
private extractInstantiation;
|
|
441
|
+
/**
|
|
442
|
+
* Is this C++ `declaration` a stack/direct-initialization object construction
|
|
443
|
+
* that invokes a constructor — `Calculator calc(0)` (direct-init) or
|
|
444
|
+
* `Widget w{1, 2}` (brace-init) — as opposed to a plain variable or a
|
|
445
|
+
* function declaration? Used to emit an `instantiates` edge for the
|
|
446
|
+
* call-less construction syntax (#1035); heap `new T(...)` is handled
|
|
447
|
+
* separately by INSTANTIATION_KINDS.
|
|
448
|
+
*
|
|
449
|
+
* Two signals, both required:
|
|
450
|
+
* - the `type` field is a class-like NAMED type (`type_identifier`,
|
|
451
|
+
* `template_type`, or `qualified_identifier`). Primitives (`int x(0)`),
|
|
452
|
+
* `auto` (`placeholder_type_specifier` — that form always carries a real
|
|
453
|
+
* `call_expression`, already handled), and sized specifiers are excluded —
|
|
454
|
+
* they construct no class; and
|
|
455
|
+
* - a declarator carries constructor arguments: an `init_declarator` whose
|
|
456
|
+
* `value` is an `argument_list` (`(args)`) or `initializer_list` (`{args}`).
|
|
457
|
+
* This skips default construction `Calculator c;` (no value) and the
|
|
458
|
+
* most-vexing-parse `Calculator c();` (a bodyless `function_declarator`,
|
|
459
|
+
* a function decl — not a construction).
|
|
460
|
+
*/
|
|
461
|
+
private isCppStackConstruction;
|
|
441
462
|
/**
|
|
442
463
|
* Static-member / value-read pass. A type/enum/class used only via a member
|
|
443
464
|
* VALUE — `Enum.value`, `Type.CONST`, `Colors.red`, `Foo::BAR` — recorded no
|
|
@@ -16,11 +16,38 @@
|
|
|
16
16
|
* an absolute-path hash under `os.tmpdir()`. The pidfile always stays in the
|
|
17
17
|
* project (it doesn't have a length limit) — and acts as the authoritative
|
|
18
18
|
* pointer to the socket path the daemon chose.
|
|
19
|
+
*
|
|
20
|
+
* Second special-case (#997, #974): some filesystems can't host an AF_UNIX node
|
|
21
|
+
* AT ALL — ExFAT/FAT external volumes, certain network mounts, WSL2 DrvFs — so
|
|
22
|
+
* `listen()` throws ENOTSUP/EACCES regardless of path length. We can't cheaply
|
|
23
|
+
* tell those apart from a normal volume up front, so instead of guessing we
|
|
24
|
+
* expose an ORDERED candidate list (`getDaemonSocketCandidates`): the in-project
|
|
25
|
+
* path first, the deterministic tmpdir path as the fallback of last resort. The
|
|
26
|
+
* daemon binds the first that works (relocating past a capability error); the
|
|
27
|
+
* proxy connects the first that answers. Both walk the SAME list, so they still
|
|
28
|
+
* converge on whichever the daemon bound with zero coordination.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Ordered socket / named-pipe path candidates the daemon should try to bind (and
|
|
32
|
+
* the proxy should try to connect) for `projectRoot`, most-preferred first.
|
|
33
|
+
* Deterministic given a project root, so independent processes converge without
|
|
34
|
+
* coordination — even when the preferred candidate is unusable and both fall
|
|
35
|
+
* through to the same fallback.
|
|
36
|
+
*
|
|
37
|
+
* - Windows: a single named pipe (lives in the kernel pipe namespace, not on
|
|
38
|
+
* the project FS, so neither the length nor the ExFAT hazard applies).
|
|
39
|
+
* - Short in-project path: `[ .codegraph/daemon.sock , <tmpdir> ]` — try the
|
|
40
|
+
* project first, fall back to tmpdir if its FS can't host a socket (#997).
|
|
41
|
+
* - Long in-project path (deep monorepos, Bazel out dirs): `[ <tmpdir> ]` only
|
|
42
|
+
* — bind would throw ENAMETOOLONG, so we skip straight to tmpdir.
|
|
19
43
|
*/
|
|
44
|
+
export declare function getDaemonSocketCandidates(projectRoot: string): string[];
|
|
20
45
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
46
|
+
* The PREFERRED (primary) socket path — candidate 0. Use this only where a
|
|
47
|
+
* single representative path is wanted (the lockfile's informational
|
|
48
|
+
* `socketPath` field, status display). For binding/connecting, walk the full
|
|
49
|
+
* {@link getDaemonSocketCandidates} list — the daemon may bind a fallback when
|
|
50
|
+
* candidate 0 is unusable.
|
|
24
51
|
*/
|
|
25
52
|
export declare function getDaemonSocketPath(projectRoot: string): string;
|
|
26
53
|
/** Absolute path to the daemon pid lockfile for `projectRoot`. */
|
package/dist/mcp/daemon.d.ts
CHANGED
|
@@ -39,7 +39,18 @@
|
|
|
39
39
|
* - The decision of *whether* to run as daemon at all — that's `MCPServer`.
|
|
40
40
|
* - The MCP protocol state machine — that's `./session.ts`.
|
|
41
41
|
*/
|
|
42
|
+
import * as net from 'net';
|
|
42
43
|
import { DaemonLockInfo } from './daemon-paths';
|
|
44
|
+
/**
|
|
45
|
+
* Finalize daemon shutdown. On POSIX, exit immediately — it's clean and fast.
|
|
46
|
+
* On Windows, do NOT force an exit while watchers may still be closing (that
|
|
47
|
+
* trips the libuv assertion above); instead mark success and let the loop drain
|
|
48
|
+
* to a natural exit, with an UNREF'd backstop that force-exits only if a stray
|
|
49
|
+
* handle would otherwise hang shutdown. Pure and platform-injected so both
|
|
50
|
+
* branches are unit-testable off-Windows. Returns the backstop timer (Windows)
|
|
51
|
+
* so callers/tests can clear it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function finalizeDaemonExit(platform: NodeJS.Platform, exit: (code: number) => void): NodeJS.Timeout | null;
|
|
43
54
|
/** Bytes/parse-window for an oversized hello line — bounded against a malicious peer. */
|
|
44
55
|
declare const MAX_HELLO_LINE_BYTES = 4096;
|
|
45
56
|
/**
|
|
@@ -177,8 +188,29 @@ export type AcquireResult = {
|
|
|
177
188
|
* the pidfile becomes visible in one step already containing a full record.
|
|
178
189
|
* Whoever links first wins; everyone else gets EEXIST and reads a complete file.
|
|
179
190
|
* There is no empty-file window at all.
|
|
191
|
+
*
|
|
192
|
+
* Filesystems without hard links (#997): ExFAT/FAT external volumes and some
|
|
193
|
+
* network mounts can't `link()` at all — it throws ENOTSUP/EPERM, which would
|
|
194
|
+
* otherwise kill the daemon before it ever reaches the socket bind. There we
|
|
195
|
+
* fall back to an O_EXCL create (`acquireLockViaExclusiveOpen`): still exclusive
|
|
196
|
+
* ("first writer wins"), but the full record is written through the fd in a
|
|
197
|
+
* second step, so the empty-file window the link approach removed is reopened —
|
|
198
|
+
* only on these filesystems, only for the microseconds between create and write
|
|
199
|
+
* (far narrower than the original bug, which the file watcher's startup latency
|
|
200
|
+
* widened). The race's worst case is two daemons briefly; on a single external
|
|
201
|
+
* drive that's strictly better than the daemon never starting at all.
|
|
180
202
|
*/
|
|
181
203
|
export declare function tryAcquireDaemonLock(projectRoot: string): AcquireResult;
|
|
204
|
+
/**
|
|
205
|
+
* Exclusive-create the pidfile (O_CREAT|O_EXCL via the `wx` flag) and write the
|
|
206
|
+
* full record through the same fd — the hard-link-free fallback used by
|
|
207
|
+
* {@link tryAcquireDaemonLock} on filesystems without `link()`. Returns true if
|
|
208
|
+
* we created it (acquired the lock), false on EEXIST (another candidate holds
|
|
209
|
+
* it). Any other error propagates. Still exclusive, so "first writer wins" holds
|
|
210
|
+
* exactly as the link path does; the only difference is the brief empty-file
|
|
211
|
+
* window between create and write. Exported for testing.
|
|
212
|
+
*/
|
|
213
|
+
export declare function acquireLockViaExclusiveOpen(pidPath: string, info: DaemonLockInfo): boolean;
|
|
182
214
|
/**
|
|
183
215
|
* Remove a stale pidfile, but only if it still names a dead process. Re-reads
|
|
184
216
|
* the file immediately before unlinking so we never delete a lock that a live
|
|
@@ -197,6 +229,22 @@ export declare function clearStaleDaemonLock(pidPath: string, expectedDeadPid?:
|
|
|
197
229
|
* mistake a live daemon for a dead one and clear its lock.
|
|
198
230
|
*/
|
|
199
231
|
export declare function isProcessAlive(pid: number): boolean;
|
|
232
|
+
/**
|
|
233
|
+
* Bind the first usable socket from an ordered candidate list, relocating past
|
|
234
|
+
* any path that fails to bind for a non-conflict reason (see {@link
|
|
235
|
+
* SOCKET_BIND_CONFLICT_CODE}). The injected `listen` does the real
|
|
236
|
+
* `net.Server.listen` (and stale-socket clear); abstracted so the relocation
|
|
237
|
+
* policy is unit-testable without a real unsupported filesystem. Returns the
|
|
238
|
+
* server plus the path actually bound. An EADDRINUSE, or any error on the LAST
|
|
239
|
+
* candidate, propagates — the caller releases the lockfile and falls back to
|
|
240
|
+
* direct mode (#974). Exported for testing.
|
|
241
|
+
*/
|
|
242
|
+
export declare function bindFirstUsableSocket(candidates: string[], listen: (socketPath: string) => Promise<net.Server>, opts?: {
|
|
243
|
+
onRelocate?: (from: string, to: string, code: string) => void;
|
|
244
|
+
}): Promise<{
|
|
245
|
+
server: net.Server;
|
|
246
|
+
socketPath: string;
|
|
247
|
+
}>;
|
|
200
248
|
/**
|
|
201
249
|
* Parse one client-hello line. Returns the peer pids if `line` is a well-formed
|
|
202
250
|
* client-hello (carries the `codegraph_client` marker), or null otherwise — in
|
package/dist/mcp/engine.d.ts
CHANGED
|
@@ -17,6 +17,15 @@ export interface MCPEngineOptions {
|
|
|
17
17
|
* cheap. Honors {@link watchDisabledReason} regardless.
|
|
18
18
|
*/
|
|
19
19
|
watch?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Whether to off-load read-tool dispatch to a worker-thread pool. Only the
|
|
22
|
+
* SHARED daemon wants this — it serves many concurrent clients on one event
|
|
23
|
+
* loop, so without a pool concurrent explores serialize and starve the MCP
|
|
24
|
+
* transport. Direct mode (one stdio client, no concurrency) leaves it off so a
|
|
25
|
+
* single call never pays a worker round-trip. `CODEGRAPH_QUERY_POOL_SIZE=0`
|
|
26
|
+
* disables it even in daemon mode.
|
|
27
|
+
*/
|
|
28
|
+
queryPool?: boolean;
|
|
20
29
|
}
|
|
21
30
|
/**
|
|
22
31
|
* Shared MCP engine. Thread-safe in the sense that multiple sessions can
|
|
@@ -32,7 +41,15 @@ export declare class MCPEngine {
|
|
|
32
41
|
private watcherStarted;
|
|
33
42
|
private opts;
|
|
34
43
|
private closed;
|
|
44
|
+
private queryPool;
|
|
35
45
|
constructor(opts?: MCPEngineOptions);
|
|
46
|
+
/**
|
|
47
|
+
* Start the worker-thread query pool once a default project is open (daemon
|
|
48
|
+
* mode only; honors `CODEGRAPH_QUERY_POOL_SIZE`). Idempotent and best-effort:
|
|
49
|
+
* if workers can't spawn on this platform the ToolHandler keeps serving reads
|
|
50
|
+
* in-process, so the pool can only help, never break, tool calls.
|
|
51
|
+
*/
|
|
52
|
+
private maybeStartPool;
|
|
36
53
|
/**
|
|
37
54
|
* Convenience for {@link MCPServer} compatibility: pre-seed an explicit
|
|
38
55
|
* project path (from the `--path` CLI flag) without yet opening it. This
|
|
@@ -41,4 +41,22 @@ export interface SupervisionState {
|
|
|
41
41
|
* supervisor and should shut down, or null while it is still supervised.
|
|
42
42
|
*/
|
|
43
43
|
export declare function supervisionLostReason(state: SupervisionState): string | null;
|
|
44
|
+
/** Default PPID poll cadence (ms). Shared by the MCP server and CLI commands. */
|
|
45
|
+
export declare const DEFAULT_PPID_POLL_MS = 5000;
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the PPID watchdog poll interval from an env override
|
|
48
|
+
* (`CODEGRAPH_PPID_POLL_MS`). A value of `0` disables the watchdog entirely
|
|
49
|
+
* (escape hatch for embedded scenarios where the parent legitimately re-parents
|
|
50
|
+
* the process on purpose). Anything non-numeric or negative falls back to the
|
|
51
|
+
* default.
|
|
52
|
+
*/
|
|
53
|
+
export declare function parsePpidPollMs(raw: string | undefined): number;
|
|
54
|
+
/**
|
|
55
|
+
* Parse the host PID propagated across the `--liftoff-only` re-exec
|
|
56
|
+
* (`CODEGRAPH_HOST_PPID`). Returns a positive integer PID, or null when
|
|
57
|
+
* unset/invalid — the direct-launch path, where the watchdog falls back to
|
|
58
|
+
* `process.ppid` divergence. PIDs of 0/1 are rejected (0 = unknown, 1 = init,
|
|
59
|
+
* i.e. already orphaned), so the watchdog doesn't latch onto init.
|
|
60
|
+
*/
|
|
61
|
+
export declare function parseHostPpid(raw: string | undefined): number | null;
|
|
44
62
|
//# sourceMappingURL=ppid-watchdog.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query pool — runs CPU-heavy read-tool calls on a pool of worker threads so
|
|
3
|
+
* the shared daemon's main event loop stays free for the MCP transport.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: see {@link ./query-worker}. One daemon, one event loop, one
|
|
6
|
+
* synchronous SQLite connection serializes every concurrent `codegraph_explore`
|
|
7
|
+
* AND starves the transport (a 10-way wave delivered 0 transport heartbeats in
|
|
8
|
+
* 25s — responses can't flush until the whole batch drains, so clients time
|
|
9
|
+
* out). Spreading the dispatch across worker threads (each its own WAL read
|
|
10
|
+
* connection) restores true multi-core parallelism and an idle main loop.
|
|
11
|
+
*
|
|
12
|
+
* Properties:
|
|
13
|
+
* - lazy growth: one warm worker on construct, grows to `size` on demand, so a
|
|
14
|
+
* single-agent session pays for one connection and a 10-subagent burst grows
|
|
15
|
+
* to the core budget.
|
|
16
|
+
* - crash recovery: a dead worker is respawned and its in-flight call retried
|
|
17
|
+
* once; a poison call that keeps crashing fails gracefully (never wedges the
|
|
18
|
+
* pool). A crash budget trips a circuit breaker (`healthy` → false) so the
|
|
19
|
+
* caller falls back to in-process dispatch instead of thrashing respawns.
|
|
20
|
+
* - graceful backstop: a call that can't be served within `softTimeoutMs`
|
|
21
|
+
* resolves with SUCCESS-shaped "busy, retry" guidance — never `isError`, so
|
|
22
|
+
* a momentary overload can't teach the agent to abandon codegraph — instead
|
|
23
|
+
* of hanging past the client's hard timeout.
|
|
24
|
+
*/
|
|
25
|
+
import type { ToolResult } from './tools';
|
|
26
|
+
/**
|
|
27
|
+
* Minimal worker surface the pool drives — satisfied by a real `worker_threads`
|
|
28
|
+
* Worker. Abstracted so tests can inject a fake worker and exercise the pool's
|
|
29
|
+
* queue / growth / crash-recovery / backstop logic without spawning threads or
|
|
30
|
+
* needing a built `dist/`.
|
|
31
|
+
*/
|
|
32
|
+
export interface PoolWorker {
|
|
33
|
+
postMessage(msg: unknown): void;
|
|
34
|
+
terminate(): Promise<number> | void;
|
|
35
|
+
on(event: 'message', cb: (m: unknown) => void): void;
|
|
36
|
+
on(event: 'error', cb: (e: Error) => void): void;
|
|
37
|
+
on(event: 'exit', cb: (code: number) => void): void;
|
|
38
|
+
}
|
|
39
|
+
export interface QueryPoolOptions {
|
|
40
|
+
/** Default project root each worker opens at spawn. */
|
|
41
|
+
root: string;
|
|
42
|
+
/** Max worker threads. Defaults to `clamp(cores-1, 1, 16)`. */
|
|
43
|
+
size?: number;
|
|
44
|
+
/** Linger before a queued call gets busy-guidance. Default 45s. */
|
|
45
|
+
softTimeoutMs?: number;
|
|
46
|
+
/** Retries for an in-flight call whose worker crashed. Default 1. */
|
|
47
|
+
maxRetries?: number;
|
|
48
|
+
/** Worker factory (tests inject a fake). Defaults to a real `worker_threads` Worker. */
|
|
49
|
+
createWorker?: () => PoolWorker;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the pool size from the `CODEGRAPH_QUERY_POOL_SIZE` override and the
|
|
53
|
+
* machine's core count. `0` (or a negative) explicitly disables the pool (the
|
|
54
|
+
* caller serves in-process — today's behavior). Unset → `clamp(cores-1, 1, 16)`:
|
|
55
|
+
* leave a core for the main loop + OS, but never zero, since even one worker
|
|
56
|
+
* frees the transport and lets responses flush incrementally.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolvePoolSize(envVal: string | undefined, cpuCount: number): number;
|
|
59
|
+
export declare class QueryPool {
|
|
60
|
+
private idle;
|
|
61
|
+
private queue;
|
|
62
|
+
private inflight;
|
|
63
|
+
private workers;
|
|
64
|
+
private pendingWorkers;
|
|
65
|
+
private nextId;
|
|
66
|
+
private totalCrashes;
|
|
67
|
+
private destroyed;
|
|
68
|
+
private readonly root;
|
|
69
|
+
private readonly maxSize;
|
|
70
|
+
private readonly softTimeoutMs;
|
|
71
|
+
private readonly maxRetries;
|
|
72
|
+
private readonly createWorker;
|
|
73
|
+
constructor(opts: QueryPoolOptions);
|
|
74
|
+
/** Pool size cap (for logging/status). */
|
|
75
|
+
get size(): number;
|
|
76
|
+
/** Live worker count (for tests/status). */
|
|
77
|
+
get liveWorkers(): number;
|
|
78
|
+
/**
|
|
79
|
+
* False once the crash budget is exhausted (or after destroy). The ToolHandler
|
|
80
|
+
* checks this and falls back to in-process dispatch — a broken worker platform
|
|
81
|
+
* degrades to today's behavior instead of failing tool calls.
|
|
82
|
+
*/
|
|
83
|
+
get healthy(): boolean;
|
|
84
|
+
private spawnOne;
|
|
85
|
+
private onMessage;
|
|
86
|
+
private onWorkerGone;
|
|
87
|
+
private drain;
|
|
88
|
+
private settle;
|
|
89
|
+
/** Run a read tool on the pool. Always resolves (never rejects). */
|
|
90
|
+
run(toolName: string, args: Record<string, unknown>): Promise<ToolResult>;
|
|
91
|
+
/** Terminate all workers and answer any outstanding calls gracefully. */
|
|
92
|
+
destroy(): Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=query-pool.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query worker thread — issue: concurrent MCP tool calls starve the daemon.
|
|
3
|
+
*
|
|
4
|
+
* The shared daemon serves every session on ONE event loop with synchronous
|
|
5
|
+
* `node:sqlite`. `codegraph_explore` is CPU-heavy (FTS + RWR/personalized-
|
|
6
|
+
* PageRank + impact + output building) stitched together by microtask `await`s,
|
|
7
|
+
* so N concurrent explores keep the microtask queue continuously full and
|
|
8
|
+
* starve the macrotask phases — timers AND socket I/O. The transport freezes:
|
|
9
|
+
* no response flushes, no request is read, until the whole batch drains. With
|
|
10
|
+
* ~10 subagents that routinely exceeds the MCP client's request timeout.
|
|
11
|
+
*
|
|
12
|
+
* This worker moves the heavy read-tool dispatch OFF the daemon's main loop.
|
|
13
|
+
* Each worker owns its OWN read connection (node:sqlite WAL allows N concurrent
|
|
14
|
+
* readers across connections — verified: a worker reader sees the main writer's
|
|
15
|
+
* committed catch-up/watcher writes), so {@link QueryPool} runs N tool calls in
|
|
16
|
+
* true parallel up to core count while the main loop stays free for the MCP
|
|
17
|
+
* transport. The worker runs {@link ToolHandler.executeReadTool} — validation +
|
|
18
|
+
* dispatch + error classification — and returns the raw {@link ToolResult}; the
|
|
19
|
+
* MAIN thread keeps the catch-up gate, the watcher-state notices (staleness /
|
|
20
|
+
* worktree), `codegraph_status`, and telemetry, none of which a watcher-less
|
|
21
|
+
* read connection can answer.
|
|
22
|
+
*/
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=query-worker.d.ts.map
|
package/dist/mcp/tools.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Defines the tools exposed by the CodeGraph MCP server.
|
|
5
5
|
*/
|
|
6
6
|
import type CodeGraph from '../index';
|
|
7
|
+
import type { QueryPool } from './query-pool';
|
|
7
8
|
import type { PendingFile } from '../sync';
|
|
8
9
|
/**
|
|
9
10
|
* An expected, recoverable "codegraph can't serve this" condition — most
|
|
@@ -113,6 +114,33 @@ export interface ToolDefinition {
|
|
|
113
114
|
properties: Record<string, PropertySchema>;
|
|
114
115
|
required?: string[];
|
|
115
116
|
};
|
|
117
|
+
/** Behavioral hints for clients (see {@link ToolAnnotations}). */
|
|
118
|
+
annotations?: ToolAnnotations;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* MCP ToolAnnotations — behavioral hints a client MAY use to decide how, or
|
|
122
|
+
* whether, to run a tool (introduced in the 2025-03-26 spec, carried in
|
|
123
|
+
* 2025-06-18). They are advisory and never to be trusted for security, but
|
|
124
|
+
* clients gate on them: Cursor's Ask mode, for one, refuses any MCP tool that
|
|
125
|
+
* doesn't advertise `readOnlyHint: true` (issue #1018).
|
|
126
|
+
*
|
|
127
|
+
* The field is purely additive — a client that predates annotations ignores it
|
|
128
|
+
* — so codegraph advertises these even though `initialize` still negotiates the
|
|
129
|
+
* 2024-11-05 protocol version.
|
|
130
|
+
*
|
|
131
|
+
* https://modelcontextprotocol.io/specification/2025-06-18/schema#toolannotations
|
|
132
|
+
*/
|
|
133
|
+
export interface ToolAnnotations {
|
|
134
|
+
/** Human-readable title for the tool. */
|
|
135
|
+
title?: string;
|
|
136
|
+
/** If true, the tool does not modify its environment. Default (unset): false. */
|
|
137
|
+
readOnlyHint?: boolean;
|
|
138
|
+
/** Meaningful only when NOT read-only: may the tool perform destructive updates? */
|
|
139
|
+
destructiveHint?: boolean;
|
|
140
|
+
/** If true, repeat calls with the same arguments have no additional effect. */
|
|
141
|
+
idempotentHint?: boolean;
|
|
142
|
+
/** If true, the tool interacts with an open world of external entities. */
|
|
143
|
+
openWorldHint?: boolean;
|
|
116
144
|
}
|
|
117
145
|
interface PropertySchema {
|
|
118
146
|
type: string;
|
|
@@ -159,7 +187,15 @@ export declare class ToolHandler {
|
|
|
159
187
|
private defaultProjectHint;
|
|
160
188
|
private worktreeMismatchCache;
|
|
161
189
|
private catchUpGate;
|
|
190
|
+
private queryPool;
|
|
162
191
|
constructor(cg: CodeGraph | null);
|
|
192
|
+
/**
|
|
193
|
+
* Engine-only: attach (or detach with null) the worker-thread query pool. The
|
|
194
|
+
* shared daemon sets this once its default project is open; the workers each
|
|
195
|
+
* hold their own WAL read connection and run {@link executeReadTool}. A
|
|
196
|
+
* worker's own ToolHandler never has a pool, so there is no nested off-loading.
|
|
197
|
+
*/
|
|
198
|
+
setQueryPool(pool: QueryPool | null): void;
|
|
163
199
|
/**
|
|
164
200
|
* Update the default CodeGraph instance (e.g. after lazy initialization)
|
|
165
201
|
*/
|
|
@@ -286,6 +322,27 @@ export declare class ToolHandler {
|
|
|
286
322
|
* Execute a tool by name
|
|
287
323
|
*/
|
|
288
324
|
execute(toolName: string, args: Record<string, unknown>): Promise<ToolResult>;
|
|
325
|
+
/**
|
|
326
|
+
* Run a single read tool to completion and return its raw {@link ToolResult},
|
|
327
|
+
* classifying expected failures the same way {@link execute}'s catch does so
|
|
328
|
+
* the SHAPE is identical whether dispatch runs in-process or on a worker:
|
|
329
|
+
* NotIndexed → success-shaped guidance, PathRefusal → clean error, anything
|
|
330
|
+
* else → internal-error-with-retry. Never throws.
|
|
331
|
+
*
|
|
332
|
+
* This is the worker thread's entry point (see {@link ./query-worker}) and the
|
|
333
|
+
* in-process fallback for {@link execute}. It deliberately does NOT run the
|
|
334
|
+
* catch-up gate or the staleness/worktree notices — those need the daemon's
|
|
335
|
+
* watched main instance and stay on the main thread. Cross-cutting allowlist +
|
|
336
|
+
* path validation already ran in {@link execute} before routing here.
|
|
337
|
+
*/
|
|
338
|
+
executeReadTool(toolName: string, args: Record<string, unknown>): Promise<ToolResult>;
|
|
339
|
+
/**
|
|
340
|
+
* Pure dispatch over the read tools — the switch, with no gate, no notices, no
|
|
341
|
+
* allowlist/validation (the caller owns those). `codegraph_status` is handled
|
|
342
|
+
* on the main thread in {@link execute} and never reaches here. May throw
|
|
343
|
+
* NotIndexed/PathRefusal, which {@link executeReadTool} classifies.
|
|
344
|
+
*/
|
|
345
|
+
private dispatchTool;
|
|
289
346
|
/**
|
|
290
347
|
* Handle codegraph_search
|
|
291
348
|
*/
|
package/dist/project-config.d.ts
CHANGED
|
@@ -12,6 +12,17 @@ export interface ProjectConfig {
|
|
|
12
12
|
* are never discovered or indexed (#970, #976).
|
|
13
13
|
*/
|
|
14
14
|
includeIgnored?: string[];
|
|
15
|
+
/**
|
|
16
|
+
* Gitignore-style patterns for paths to keep OUT of the index — even when
|
|
17
|
+
* they are git-TRACKED, which `.gitignore` cannot do (#999). The escape hatch
|
|
18
|
+
* for a committed vendor/theme/SDK directory (e.g. a checked-in Metronic theme
|
|
19
|
+
* under `static/`) that bloats the graph and slows indexing but isn't really
|
|
20
|
+
* your code. Matched against project-root-relative paths, so a directory like
|
|
21
|
+
* `"static/"`, a double-star vendor glob, or `"assets/theme"` all work.
|
|
22
|
+
* Absent/empty (the default) excludes nothing beyond the built-in defaults
|
|
23
|
+
* and your `.gitignore`.
|
|
24
|
+
*/
|
|
25
|
+
exclude?: string[];
|
|
15
26
|
}
|
|
16
27
|
/**
|
|
17
28
|
* Load the validated extension overrides for a project, mtime-cached.
|
|
@@ -31,6 +42,15 @@ export declare function loadExtensionOverrides(rootDir: string): Record<string,
|
|
|
31
42
|
* are never discovered or indexed (#970, #976).
|
|
32
43
|
*/
|
|
33
44
|
export declare function loadIncludeIgnoredPatterns(rootDir: string): string[];
|
|
45
|
+
/**
|
|
46
|
+
* Load the validated `exclude` patterns for a project, mtime-cached.
|
|
47
|
+
*
|
|
48
|
+
* These name paths to keep OUT of the index even when git-tracked — the escape
|
|
49
|
+
* hatch for a committed vendor/theme/SDK directory `.gitignore` can't drop
|
|
50
|
+
* (#999). An empty result — the zero-config default — excludes nothing beyond
|
|
51
|
+
* the built-in defaults and the project's `.gitignore`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function loadExcludePatterns(rootDir: string): string[];
|
|
34
54
|
/** Test/maintenance hook: forget cached config (e.g. after rewriting it in a test). */
|
|
35
55
|
export declare function clearProjectConfigCache(): void;
|
|
36
56
|
//# sourceMappingURL=project-config.d.ts.map
|
|
@@ -1,31 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* C/C++ function-pointer dispatch synthesis (#932).
|
|
3
|
-
*
|
|
4
|
-
* C/C++ polymorphism is the function pointer: a struct carries a fn-pointer
|
|
5
|
-
* field (`int (*fn)(int)`, or a fn-pointer-typedef field `hook_func func`),
|
|
6
|
-
* concrete functions are *registered* into it through a table
|
|
7
|
-
* (`static struct cmd cmds[] = {{"add", cmd_add}, …}`, a designated
|
|
8
|
-
* `.fn = cmd_add`, or `x->fn = cmd_add`), and the dispatcher calls through it
|
|
9
|
-
* indirectly (`p->fn(argv)`). Static extraction captures neither the
|
|
10
|
-
* registration→field binding nor the indirect call, so the dispatcher→handler
|
|
11
|
-
* edge is missing and `git`'s `run_builtin` looks like it calls nothing, the
|
|
12
|
-
* hooks in `hook_demo.c` are unreachable, etc.
|
|
13
|
-
*
|
|
14
|
-
* This bridges it, keyed by **(struct type, fn-pointer field)**:
|
|
15
|
-
* • registrations — a function bound to `S.field` via a positional
|
|
16
|
-
* initializer (matched by field index), a designated `.field = fn`, or a
|
|
17
|
-
* direct `x.field = fn` / `x->field = fn` assignment;
|
|
18
|
-
* • dispatch — `recv->field(…)` / `recv.field(…)` where `recv` resolves to a
|
|
19
|
-
* value of struct type `S` (from the enclosing function's params / locals),
|
|
20
|
-
* falling back to the field name when it is unique to one struct;
|
|
21
|
-
* • field←field propagation — `a->f = b->g` merges `B.g`'s handlers into
|
|
22
|
-
* `A.f`, so a generic single-slot hook that is reassigned from a registry
|
|
23
|
-
* (the `hook_demo.c` shape: `h->func = found->fn`) still resolves.
|
|
24
|
-
*
|
|
25
|
-
* Whole-graph pass after base resolution; all edges are `provenance:'heuristic'`
|
|
26
|
-
* (`synthesizedBy:'fn-pointer-dispatch'`). High precision via the (type, field)
|
|
27
|
-
* key + a real-function gate; a project with no fn-pointer dispatch is a no-op.
|
|
28
|
-
*/
|
|
29
1
|
import type { Edge } from '../types';
|
|
30
2
|
import type { QueryBuilder } from '../db/queries';
|
|
31
3
|
import type { ResolutionContext } from './types';
|
package/dist/sync/worktree.d.ts
CHANGED
|
@@ -25,6 +25,15 @@
|
|
|
25
25
|
* is exactly the distinction this module relies on.
|
|
26
26
|
*/
|
|
27
27
|
export declare function gitWorktreeRoot(dir: string): string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Absolute, symlink-resolved git **common** directory for `dir` — the shared
|
|
30
|
+
* `.git` that all worktrees of one repository point at. Linked worktrees of the
|
|
31
|
+
* same repo report the SAME common dir; a submodule or an embedded clone is a
|
|
32
|
+
* DIFFERENT repository and reports its own (`…/.git/modules/<name>` or its own
|
|
33
|
+
* `.git`). That distinction is what separates a genuine "borrowed worktree"
|
|
34
|
+
* from a nested repo the parent index already covers. Null when not a repo.
|
|
35
|
+
*/
|
|
36
|
+
export declare function gitCommonDir(dir: string): string | null;
|
|
28
37
|
export interface WorktreeIndexMismatch {
|
|
29
38
|
/** The git working tree the command was run from. */
|
|
30
39
|
worktreeRoot: string;
|
package/dist/types.d.ts
CHANGED
|
@@ -235,7 +235,12 @@ export interface SearchOptions {
|
|
|
235
235
|
export interface SearchResult {
|
|
236
236
|
/** Matching node */
|
|
237
237
|
node: Node;
|
|
238
|
-
/**
|
|
238
|
+
/**
|
|
239
|
+
* Relevance score for relative ranking only — higher is more relevant.
|
|
240
|
+
* NOT normalized and NOT a 0-1 fraction: the FTS path returns an unbounded
|
|
241
|
+
* BM25 magnitude (often in the tens or hundreds), while the fuzzy/exact
|
|
242
|
+
* paths return ~0-1. Use it to order results, not as an absolute percentage.
|
|
243
|
+
*/
|
|
239
244
|
score: number;
|
|
240
245
|
/** Matched text snippets for highlighting */
|
|
241
246
|
highlights?: string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colbymchenry/codegraph",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
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.
|
|
19
|
-
"@colbymchenry/codegraph-darwin-x64": "1.1.
|
|
20
|
-
"@colbymchenry/codegraph-linux-arm64": "1.1.
|
|
21
|
-
"@colbymchenry/codegraph-linux-x64": "1.1.
|
|
22
|
-
"@colbymchenry/codegraph-win32-arm64": "1.1.
|
|
23
|
-
"@colbymchenry/codegraph-win32-x64": "1.1.
|
|
18
|
+
"@colbymchenry/codegraph-darwin-arm64": "1.1.3",
|
|
19
|
+
"@colbymchenry/codegraph-darwin-x64": "1.1.3",
|
|
20
|
+
"@colbymchenry/codegraph-linux-arm64": "1.1.3",
|
|
21
|
+
"@colbymchenry/codegraph-linux-x64": "1.1.3",
|
|
22
|
+
"@colbymchenry/codegraph-win32-arm64": "1.1.3",
|
|
23
|
+
"@colbymchenry/codegraph-win32-x64": "1.1.3"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"npm-shim.js",
|