@aroman22/codegraph-vba 1.5.1 → 1.6.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 (76) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/codegraph.d.ts +1 -1
  3. package/dist/bin/command-supervision.d.ts +38 -1
  4. package/dist/db/index.d.ts +84 -5
  5. package/dist/db/migrations.d.ts +1 -1
  6. package/dist/db/queries.d.ts +127 -4
  7. package/dist/db/wal-valve.d.ts +106 -0
  8. package/dist/directory.d.ts +9 -5
  9. package/dist/extraction/cfml-extractor.d.ts +107 -0
  10. package/dist/extraction/grammars.d.ts +25 -1
  11. package/dist/extraction/index.d.ts +47 -2
  12. package/dist/extraction/languages/arkts.d.ts +3 -0
  13. package/dist/extraction/languages/c-cpp.d.ts +98 -0
  14. package/dist/extraction/languages/cfquery.d.ts +12 -0
  15. package/dist/extraction/languages/cfscript.d.ts +3 -0
  16. package/dist/extraction/languages/cobol.d.ts +33 -0
  17. package/dist/extraction/languages/erlang.d.ts +3 -0
  18. package/dist/extraction/languages/nix.d.ts +3 -0
  19. package/dist/extraction/languages/solidity.d.ts +3 -0
  20. package/dist/extraction/languages/terraform.d.ts +3 -0
  21. package/dist/extraction/languages/vbnet.d.ts +11 -0
  22. package/dist/extraction/mybatis-extractor.d.ts +30 -10
  23. package/dist/extraction/parse-pool.d.ts +28 -1
  24. package/dist/extraction/tree-sitter-types.d.ts +20 -1
  25. package/dist/extraction/tree-sitter.d.ts +60 -1
  26. package/dist/extraction/vba/call-sweep.d.ts +3 -0
  27. package/dist/extraction/vba/calls.d.ts +55 -0
  28. package/dist/extraction/vba/constants.d.ts +42 -0
  29. package/dist/extraction/vba/context.d.ts +175 -0
  30. package/dist/extraction/vba/controls.d.ts +24 -0
  31. package/dist/extraction/vba/declarations.d.ts +3 -0
  32. package/dist/extraction/vba/dims.d.ts +3 -0
  33. package/dist/extraction/vba/docmd.d.ts +16 -0
  34. package/dist/extraction/vba/enums-consts.d.ts +15 -0
  35. package/dist/extraction/vba/implements.d.ts +3 -0
  36. package/dist/extraction/vba/procedures.d.ts +3 -0
  37. package/dist/extraction/vba/sql-wrapper.d.ts +17 -0
  38. package/dist/extraction/vba/tempvars.d.ts +10 -0
  39. package/dist/extraction/vba/text-utils.d.ts +73 -0
  40. package/dist/extraction/vba-extractor.d.ts +3 -951
  41. package/dist/extraction/vba-preprocess.d.ts +1 -1
  42. package/dist/index.d.ts +81 -1
  43. package/dist/installer/index.d.ts +42 -0
  44. package/dist/mcp/daemon.d.ts +35 -3
  45. package/dist/mcp/early-ppid.d.ts +26 -0
  46. package/dist/mcp/liveness-watchdog.d.ts +18 -1
  47. package/dist/mcp/query-pool.d.ts +14 -0
  48. package/dist/mcp/session.d.ts +14 -0
  49. package/dist/mcp/startup-handshake.d.ts +44 -0
  50. package/dist/mcp/tools.d.ts +22 -0
  51. package/dist/project-config.d.ts +44 -0
  52. package/dist/resolution/c-fnptr-synthesizer.d.ts +2 -1
  53. package/dist/resolution/callback-synthesizer.d.ts +1 -1
  54. package/dist/resolution/cooperative-yield.d.ts +32 -0
  55. package/dist/resolution/frameworks/cics.d.ts +20 -0
  56. package/dist/resolution/frameworks/terraform.d.ts +38 -0
  57. package/dist/resolution/goframe-synthesizer.d.ts +2 -1
  58. package/dist/resolution/import-resolver.d.ts +7 -0
  59. package/dist/resolution/index.d.ts +85 -2
  60. package/dist/resolution/name-matcher.d.ts +22 -3
  61. package/dist/resolution/strip-comments.d.ts +1 -1
  62. package/dist/resolution/types.d.ts +29 -0
  63. package/dist/resolution/workspace-packages.d.ts +10 -0
  64. package/dist/search/identifier-segments.d.ts +60 -0
  65. package/dist/sync/watcher.d.ts +10 -5
  66. package/dist/sync/worktree.d.ts +9 -0
  67. package/dist/types.d.ts +25 -2
  68. package/dist/upgrade/index.d.ts +32 -0
  69. package/dist/upgrade/remove-binary.d.ts +87 -0
  70. package/dist/upgrade/update-check.d.ts +92 -0
  71. package/npm-shim.js +32 -3
  72. package/package.json +7 -7
  73. package/dist/reasoning/config.d.ts +0 -45
  74. package/dist/reasoning/credentials.d.ts +0 -5
  75. package/dist/reasoning/login.d.ts +0 -21
  76. package/dist/reasoning/reasoner.d.ts +0 -43
package/README.md CHANGED
@@ -281,7 +281,7 @@ The reliable, universal payoff is **surgical context and speed**: CodeGraph coll
281
281
  | **Full-Text Search** | Find code by name instantly across your entire codebase, powered by FTS5 |
282
282
  | **Impact Analysis** | Trace callers, callees, and the full impact radius of any symbol before making changes |
283
283
  | **Always Fresh** | File watcher uses native OS events (FSEvents/inotify/ReadDirectoryChangesW) with debounced auto-sync — the graph stays current as you code, zero config |
284
- | **20+ Languages** | TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Objective-C, Swift, Kotlin, Scala, Dart, Lua, Luau, R, Svelte, Vue, Astro, Liquid, Pascal/Delphi, **VBA / Access** |
284
+ | **30+ Languages** | TypeScript, JavaScript, ArkTS, Python, Go, Rust, Java, C#, VB.NET, PHP, Ruby, C, C++, CUDA, Objective-C, Metal, Swift, Kotlin, Scala, Dart, Lua, Luau, R, Nix, Erlang, CFML, COBOL, Solidity, Terraform/OpenTofu, Svelte, Vue, Astro, Liquid, Pascal/Delphi, **VBA / Access** |
285
285
  | **Framework-aware Routes** | Recognizes web-framework routing files and links URL patterns to their handlers across 17 frameworks |
286
286
  | **Mixed iOS / React Native / Expo** | Closes cross-language flows that static parsing misses: Swift ↔ ObjC bridging, React Native legacy bridge + TurboModules + Fabric view components, native → JS event emitters, Expo Modules |
287
287
  | **VBA / Access + Dysflow** | Indexes the Dysflow-exported source tree (`.bas`/`.cls` + `.form.txt`/`.report.txt`) of Microsoft Access projects so agents can navigate VBA code the same way they navigate TypeScript or Python |
@@ -22,5 +22,5 @@
22
22
  * codegraph affected [files] Find test files affected by changes
23
23
  * codegraph upgrade [version] Update CodeGraph to the latest release
24
24
  */
25
- export {};
25
+ import '../mcp/early-ppid';
26
26
  //# sourceMappingURL=codegraph.d.ts.map
@@ -1,3 +1,34 @@
1
+ /**
2
+ * Process supervision for long-running CLI commands (`index` / `init --index`).
3
+ *
4
+ * Indexing a large repo can run for a while on the main thread, and #999
5
+ * surfaced two ways that goes wrong when nothing is watching it:
6
+ *
7
+ * 1. **Orphaned worker.** `index` runs in a child re-exec'd with
8
+ * `--liftoff-only` (the WASM-flag relaunch). Its parent blocks in
9
+ * `spawnSync`, so when the parent shim is killed it cannot forward the
10
+ * signal — the child keeps running, now orphaned, pinning a core. The PPID
11
+ * watchdog (#277) notices the parent/host went away and exits the child.
12
+ * 2. **Wedged indexer.** The `#850` main-thread liveness watchdog — which
13
+ * SIGKILLs a process whose event loop stops turning — was wired only into
14
+ * the MCP `serve` path, so a wedged `index`/`init` was never auto-killed.
15
+ *
16
+ * Both reuse the exact mechanisms `serve` already uses; this just makes them
17
+ * available to a one-shot command. Best-effort and self-disabling: a missing
18
+ * watchdog never blocks the command from running. Both honour the same env
19
+ * switches as `serve` (`CODEGRAPH_NO_WATCHDOG`, `CODEGRAPH_PPID_POLL_MS=0`).
20
+ *
21
+ * Unlike the daemon — whose main thread only does fast, bounded work — the
22
+ * `index`/`init` path runs reference resolution and dynamic-edge synthesis
23
+ * SYNCHRONOUSLY on this thread, and on a large repo that is legitimately many
24
+ * seconds of work. So those spans yield cooperatively to the event loop
25
+ * (`src/resolution/cooperative-yield.ts`) to keep the heartbeat alive; without
26
+ * that the watchdog would SIGKILL a valid, in-progress index (#1091). The
27
+ * distinction it must preserve — kill a TRUE wedge, spare slow-but-progressing
28
+ * work — is exactly what cooperative yielding buys: a genuinely stuck span never
29
+ * reaches its next yield, so it still trips the timeout.
30
+ */
31
+ import { WatchdogOptions } from '../mcp/liveness-watchdog';
1
32
  export interface CommandSupervision {
2
33
  /** Tear down both watchdogs. Idempotent; call when the command finishes. */
3
34
  stop(): void;
@@ -7,6 +38,12 @@ export interface CommandSupervision {
7
38
  * `label` is used in the shutdown notice (e.g. `"index"`). Returns a handle
8
39
  * whose `stop()` must be called when the command completes so neither watchdog
9
40
  * outlives it.
41
+ *
42
+ * Pass `watchdog.progressPaths` (the project's SQLite DB + `-wal`) so the
43
+ * liveness watchdog can tell a slow-but-progressing store on degraded storage
44
+ * (files advancing) from a true wedge (they aren't) — one long synchronous
45
+ * SQLite statement on a 150-IOPS disk otherwise gets a healthy index
46
+ * SIGKILLed (#1231).
10
47
  */
11
- export declare function installCommandSupervision(label: string): CommandSupervision;
48
+ export declare function installCommandSupervision(label: string, watchdog?: WatchdogOptions): CommandSupervision;
12
49
  //# sourceMappingURL=command-supervision.d.ts.map
@@ -68,13 +68,51 @@ export declare class DatabaseConnection {
68
68
  * Get database file size in bytes
69
69
  */
70
70
  getSize(): number;
71
+ /**
72
+ * Size of the `-wal` sidecar file in bytes. 0 when it doesn't exist (non-WAL
73
+ * journal mode, in-memory DB, or no write since the last checkpoint+reset).
74
+ */
75
+ getWalSizeBytes(): number;
76
+ /** Current `wal_autocheckpoint` interval in pages (0 = disabled). */
77
+ getWalAutocheckpoint(): number;
78
+ /**
79
+ * Set the connection's `wal_autocheckpoint` interval (pages; 0 disables).
80
+ * Bulk indexing defers checkpoints entirely (#1231): the default 1000-page
81
+ * auto-checkpoint re-writes hot B-tree/FTS pages into the main DB file over
82
+ * and over — measured at ~95% of ALL disk I/O during a bulk index, and the
83
+ * difference between 45s and 19+ minutes on HDD-class storage. During
84
+ * deferral a {@link WalCheckpointValve} bounds WAL growth off-thread.
85
+ */
86
+ setWalAutocheckpoint(pages: number): void;
87
+ /**
88
+ * `PRAGMA wal_checkpoint(PASSIVE)` on a worker thread with its own
89
+ * connection. PASSIVE never blocks the writer, and running it off-thread
90
+ * means the main thread — and the #850 watchdog heartbeat — keep turning
91
+ * even when the backfill is minutes of I/O on slow storage (a synchronous
92
+ * checkpoint that exceeds the watchdog's 60s window gets a healthy index
93
+ * SIGKILLed — observed in the #1231 repro).
94
+ *
95
+ * Returns SQLite's checkpoint result row — `log === checkpointed` with
96
+ * `busy === 0` means the ENTIRE WAL was backfilled, so the writer's next
97
+ * commit restarts the WAL from the top and the file stops growing. The
98
+ * WAL valve needs that signal because a WAL file's SIZE never shrinks:
99
+ * after the first wrap, raw file size says nothing about the un-backfilled
100
+ * backlog. Best-effort: returns null on any failure (including worker
101
+ * threads being unavailable — a potentially minutes-long checkpoint must
102
+ * never run inline on the main thread).
103
+ */
104
+ checkpointWalPassive(): Promise<{
105
+ busy: number;
106
+ log: number;
107
+ checkpointed: number;
108
+ } | null>;
71
109
  /**
72
110
  * Optimize database (vacuum and analyze)
73
111
  */
74
112
  optimize(): void;
75
113
  /**
76
- * Lightweight, non-blocking maintenance to run after bulk writes
77
- * (indexAll, sync). Two operations:
114
+ * Lightweight maintenance to run after bulk writes (indexAll, sync).
115
+ * Two operations:
78
116
  *
79
117
  * - `PRAGMA optimize` — incremental ANALYZE; SQLite only re-analyzes
80
118
  * tables whose row counts changed materially since the last
@@ -86,10 +124,31 @@ export declare class DatabaseConnection {
86
124
  * unboundedly between automatic checkpoints (auto-fires at 1000
87
125
  * pages by default; large indexAll runs blow past that).
88
126
  *
89
- * Both operations are silently swallowed on failure they're a
90
- * best-effort optimization, never load-bearing for correctness.
127
+ * Runs on a WORKER THREAD with its own connection: on a multi-GB index
128
+ * these pragmas are minutes of synchronous IO (a 95k-file kernel index
129
+ * left a 593MB WAL whose checkpoint alone blew the #850 watchdog's 60s
130
+ * window and got a COMPLETED index SIGKILLed at the finish line). WAL
131
+ * checkpointing from a second connection is standard SQLite; `PRAGMA
132
+ * optimize` persists its statistics in sqlite_stat tables, so the main
133
+ * connection benefits the same. The main thread just awaits a message,
134
+ * so the event loop — and the watchdog heartbeat — keep turning.
135
+ *
136
+ * Everything is silently swallowed on failure — best-effort
137
+ * optimization, never load-bearing for correctness. If worker threads
138
+ * are unavailable, falls back to a bounded in-line `PRAGMA optimize`
139
+ * and SKIPS the checkpoint (the final close() checkpoints after the
140
+ * CLI has already disarmed its watchdog).
141
+ */
142
+ runMaintenance(): Promise<void>;
143
+ /**
144
+ * Run pragmas on a worker thread against its own connection to this DB
145
+ * (shared machinery for {@link runMaintenance} and
146
+ * {@link checkpointWalPassive}). Each pragma is individually best-effort;
147
+ * the whole call is best-effort. `inlineFallback` (if any) runs on THIS
148
+ * connection only when worker threads are unavailable — keep it to pragmas
149
+ * that are safe to run synchronously on the main thread.
91
150
  */
92
- runMaintenance(): void;
151
+ private runPragmasOffThread;
93
152
  /**
94
153
  * Close the database connection
95
154
  */
@@ -119,4 +178,24 @@ export declare const DATABASE_FILENAME = "codegraph.db";
119
178
  * Get the default database path for a project
120
179
  */
121
180
  export declare function getDatabasePath(projectRoot: string): string;
181
+ /**
182
+ * Delete a database file and its WAL sidecars (`-wal`/`-shm`).
183
+ *
184
+ * This is how a FULL re-index discards an existing database — rather than
185
+ * opening the old graph and DELETE-ing every row. On a large or pre-fix
186
+ * poisoned index (e.g. an old graph that scanned an ignored gitlink corpus into
187
+ * ~1.6M nodes with a multi-GB WAL, #1065) the per-row `nodes_fts` delete-trigger
188
+ * churn blocks the main thread long enough to trip the #850 liveness watchdog
189
+ * before indexing even starts, so the rebuild could never recover the bad state
190
+ * (#1067). Unlinking is O(1) regardless of DB size and also reclaims the disk
191
+ * the bloated WAL would otherwise keep.
192
+ *
193
+ * POSIX removes the directory entry even while another process (a daemon/MCP
194
+ * server) still holds the file open; that holder heals via `reopenIfReplaced`
195
+ * (#925). On Windows a live holder can make the unlink fail with EBUSY/EPERM —
196
+ * that is thrown for the caller to surface ("stop the other process and retry").
197
+ * The `-wal`/`-shm` sidecars are best-effort: SQLite recreates them on the next
198
+ * open, so a leftover sidecar is harmless.
199
+ */
200
+ export declare function removeDatabaseFiles(dbPath: string): void;
122
201
  //# sourceMappingURL=index.d.ts.map
@@ -7,7 +7,7 @@ import { SqliteDatabase } from './sqlite-adapter';
7
7
  /**
8
8
  * Current schema version
9
9
  */
10
- export declare const CURRENT_SCHEMA_VERSION = 6;
10
+ export declare const CURRENT_SCHEMA_VERSION = 9;
11
11
  /**
12
12
  * Migration definition
13
13
  */
@@ -4,7 +4,7 @@
4
4
  * Prepared statements for CRUD operations on the knowledge graph.
5
5
  */
6
6
  import { SqliteDatabase } from './sqlite-adapter';
7
- import { Node, Edge, FileRecord, UnresolvedReference, NodeKind, EdgeKind, GraphStats, SearchOptions, SearchResult } from '../types';
7
+ import { Node, Edge, FileRecord, UnresolvedReference, NodeKind, EdgeKind, Language, GraphStats, SearchOptions, SearchResult } from '../types';
8
8
  /**
9
9
  * Query builder for the knowledge graph database
10
10
  */
@@ -14,6 +14,8 @@ export declare class QueryBuilder {
14
14
  private nodeCache;
15
15
  private readonly maxCacheSize;
16
16
  private stmts;
17
+ private segmentedNames;
18
+ private static readonly MAX_SEGMENTED_NAMES;
17
19
  constructor(db: SqliteDatabase);
18
20
  /** Set the normalized project-name tokens used to down-weight non-discriminative
19
21
  * query words in path scoring (#720). Called once when the project opens. */
@@ -24,6 +26,12 @@ export declare class QueryBuilder {
24
26
  * Insert a new node
25
27
  */
26
28
  insertNode(node: Node): void;
29
+ /** Which node kinds contribute their name to the segment vocabulary — the
30
+ * single gate shared by insertNode, updateNode, and the rebuild page query
31
+ * (getDistinctNodeNames), so the write paths can't drift apart. */
32
+ private isSegmentableKind;
33
+ /** Write `name`'s segments into name_segment_vocab (idempotent). */
34
+ private insertNameSegments;
27
35
  /**
28
36
  * Insert multiple nodes in a transaction
29
37
  */
@@ -40,6 +48,44 @@ export declare class QueryBuilder {
40
48
  * Delete all nodes for a file
41
49
  */
42
50
  deleteNodesByFile(filePath: string): void;
51
+ /** Wipe the segment vocabulary. A full index calls this at its start; the
52
+ * node write path repopulates it as files (re-)index, so the end state is
53
+ * exactly the current names with no orphan rows. */
54
+ clearNameSegmentVocab(): void;
55
+ /** True when the vocab has no rows — an index built before the table existed.
56
+ * `sync` uses this to heal such databases (see rebuildNameSegmentVocabFrom). */
57
+ isNameSegmentVocabEmpty(): boolean;
58
+ /** One page of distinct segmentable node names, for batched vocab rebuilds
59
+ * (file basenames and import specifiers are excluded from the vocab — see
60
+ * insertNode). */
61
+ getDistinctNodeNames(limit: number, offset: number): string[];
62
+ /** Insert segments for a batch of names in one transaction (vocab heal path). */
63
+ insertNameSegmentsBatch(names: string[]): void;
64
+ /**
65
+ * Names whose segments cover at least `minWords` distinct PROMPT WORDS —
66
+ * the co-occurrence probe behind the prompt hook's medium tier: the words
67
+ * "state" and "machine" both being segments of `OrderStateMachine` is strong
68
+ * evidence the prompt names that symbol in prose. Ordered by coverage.
69
+ *
70
+ * Takes (segment variant → original word) pairs and folds variants back to
71
+ * their word INSIDE the SQL: a name matching both `service` and `services`
72
+ * counts ONE word, not two. Counting raw variants let plural-variant pairs
73
+ * of a single word tie with genuine two-word matches and — because ORDER
74
+ * BY/LIMIT run here, before any JS-side re-check — crowd a real match past
75
+ * the LIMIT on vocab-heavy repos (#1146).
76
+ */
77
+ getSegmentCoOccurrence(variants: Array<{
78
+ segment: string;
79
+ word: string;
80
+ }>, minWords: number, limit: number): Array<{
81
+ name: string;
82
+ matches: number;
83
+ }>;
84
+ /** How many distinct names each segment appears in — the rarity signal that
85
+ * separates a discriminative word ("checkout") from a ubiquitous one ("state"). */
86
+ getSegmentNameCounts(segments: string[]): Map<string, number>;
87
+ /** Names containing the given segment (rare-single-word tier). */
88
+ getNamesForSegment(segment: string, limit: number): string[];
43
89
  /**
44
90
  * Get a node by ID
45
91
  */
@@ -152,10 +198,32 @@ export declare class QueryBuilder {
152
198
  * Get all nodes in the database
153
199
  */
154
200
  getAllNodes(): Node[];
201
+ /**
202
+ * Stream nodes of one language whose `decorators` JSON array contains
203
+ * `decorator`. The LIKE on the JSON text is a cheap index-free pre-filter
204
+ * (a decorator name can appear as a substring of another), so callers must
205
+ * still exact-check `node.decorators.includes(decorator)`. Exists so the
206
+ * kotlin expect/actual synthesizer never materializes the whole node table
207
+ * the way `getAllNodes().filter(...)` did — that array alone OOM'd Node's
208
+ * default heap on a 2M-node graph (#1212).
209
+ */
210
+ iterateNodesByLanguageWithDecorator(language: Language, decorator: string): IterableIterator<Node>;
211
+ /**
212
+ * Distinct languages present in the files table. One indexed aggregate —
213
+ * lets the dynamic-edge synthesizers skip passes for languages the project
214
+ * doesn't contain at all (a Kotlin pass has no work on a pure-C repo), so
215
+ * their cost is zero rather than a full-graph scan that finds nothing (#1212).
216
+ */
217
+ getDistinctFileLanguages(): Set<string>;
155
218
  /**
156
219
  * Get nodes by exact name match (uses idx_nodes_name index)
157
220
  */
158
221
  getNodesByName(name: string): Node[];
222
+ /**
223
+ * Nodes whose name starts with `prefix`, by index range scan (a LIKE would
224
+ * skip idx_nodes_name under SQLite's default case-insensitive LIKE).
225
+ */
226
+ getNodesByNamePrefix(prefix: string, limit?: number): Node[];
159
227
  /**
160
228
  * Get nodes by exact qualified name match (uses idx_nodes_qualified_name index)
161
229
  */
@@ -251,6 +319,17 @@ export declare class QueryBuilder {
251
319
  * about the target symbol.
252
320
  */
253
321
  getVbaCallStubs(): Node[];
322
+ /**
323
+ * Find VBA reference-stub candidate nodes for `resolveVbaReferenceStubs`
324
+ * (issue #78).
325
+ *
326
+ * A VBA reference-stub is a `class` node whose `metadata.synthesizedBy`
327
+ * starts with `vba-` and is targeted by a `references` edge from a VBA
328
+ * module. These synthetic nodes are created by the extractor for
329
+ * cross-file type references (`Dim x As MyEnum`) but may not have been
330
+ * resolved to the real type node during the main resolution pass.
331
+ */
332
+ getVbaReferenceStubs(): Node[];
254
333
  /**
255
334
  * Repoint an edge's `target` + `metadata` in place, leaving all other
256
335
  * columns (source, kind, line, col, provenance) untouched. Used by
@@ -315,6 +394,8 @@ export declare class QueryBuilder {
315
394
  getCrossFileIncomingEdgesWithTarget(filePath: string): Array<Edge & {
316
395
  targetName: string;
317
396
  targetKind: NodeKind;
397
+ sourceFilePath: string;
398
+ sourceLanguage: Language;
318
399
  }>;
319
400
  /**
320
401
  * Insert or update a file record
@@ -362,12 +443,18 @@ export declare class QueryBuilder {
362
443
  */
363
444
  getUnresolvedReferences(): UnresolvedReference[];
364
445
  /**
365
- * Get the count of unresolved references without loading them into memory
446
+ * Get the count of PENDING (never-attempted) references without loading
447
+ * them into memory. Rows marked status='failed' — attempted by a completed
448
+ * pass, no match — are excluded: they are not outstanding work, only retry
449
+ * candidates for the #1240 sweep, so they must not trip the #1187 orphan
450
+ * sweep or the `status` pending-refs warning.
366
451
  */
367
452
  getUnresolvedReferencesCount(): number;
368
453
  /**
369
- * Get a batch of unresolved references using LIMIT/OFFSET pagination.
370
- * Used to process references in bounded memory chunks.
454
+ * Get a batch of PENDING unresolved references using LIMIT/OFFSET
455
+ * pagination. Used to process references in bounded memory chunks; failed
456
+ * rows are excluded so the batched drain loop terminates once every row
457
+ * has been attempted.
371
458
  */
372
459
  getUnresolvedReferencesBatch(offset: number, limit: number): UnresolvedReference[];
373
460
  /**
@@ -378,6 +465,13 @@ export declare class QueryBuilder {
378
465
  * Get all distinct node names (lightweight — just name strings for pre-filtering)
379
466
  */
380
467
  getAllNodeNames(): string[];
468
+ /**
469
+ * Stream the distinct node names one row at a time — the incremental
470
+ * counterpart to {@link getAllNodeNames} for callers that need to yield
471
+ * to the event loop mid-scan (resolver cache warm-up on multi-million-node
472
+ * indexes). Fresh statement per call: the iterator holds an open cursor.
473
+ */
474
+ iterateNodeNames(): IterableIterator<string>;
381
475
  /**
382
476
  * Get unresolved references scoped to specific file paths.
383
477
  * Uses the idx_unresolved_file_path index for efficient lookup.
@@ -400,6 +494,35 @@ export declare class QueryBuilder {
400
494
  referenceName: string;
401
495
  referenceKind: string;
402
496
  }>): void;
497
+ /**
498
+ * Mark refs a completed resolution pass could not resolve as status='failed'
499
+ * instead of deleting them (#1240). Failed rows are invisible to the pending
500
+ * count/batch readers (so drain loops and the #1187 orphan sweep still
501
+ * terminate) but stay queryable by name_tail so a later sync can retry them
502
+ * when a changed file introduces a symbol that could satisfy them. name_tail
503
+ * is (re)written here so rows inserted before the v8 migration get their
504
+ * tail the first time they're attempted.
505
+ */
506
+ markReferencesFailed(refs: Array<{
507
+ fromNodeId: string;
508
+ referenceName: string;
509
+ referenceKind: string;
510
+ }>): void;
511
+ /**
512
+ * Failed refs whose name tail matches one of the given symbol names — the
513
+ * candidates a sync should retry after files carrying those names changed
514
+ * (#1240). Names matching more than `perNameCeiling` failed refs are
515
+ * skipped entirely: at that population a name is external/builtin noise
516
+ * (`get`, `map`, …) that one new definition won't resolve — the same
517
+ * rationale as resolution's AMBIGUOUS_NAME_CEILING (#999) — and retrying an
518
+ * arbitrary subset would be both wasted work and incoherent coverage.
519
+ */
520
+ getRetryableFailedReferences(names: string[], perNameCeiling?: number): UnresolvedReference[];
521
+ /**
522
+ * Distinct node names present in the given files — the symbol names a sync
523
+ * pass uses to look up retryable failed refs after those files changed.
524
+ */
525
+ getNodeNamesByFiles(filePaths: string[]): string[];
403
526
  /**
404
527
  * Lightweight (nodes, edges) count snapshot. Used around an index/sync
405
528
  * run to compute true additions across extraction + resolution +
@@ -0,0 +1,106 @@
1
+ /**
2
+ * WAL checkpoint valve — bounds WAL growth while auto-checkpointing is
3
+ * deferred during a bulk index (#1231).
4
+ *
5
+ * Why deferral: SQLite's default `wal_autocheckpoint` (1000 pages) re-writes
6
+ * hot B-tree/FTS pages into the main DB file over and over during a bulk
7
+ * index — measured at ~95% of ALL disk I/O, and the difference between 45s
8
+ * and 19+ minutes on HDD-class storage (150 random IOPS). Deferring
9
+ * checkpoints turns the store into pure sequential WAL appends; each backfill
10
+ * pass writes distinct pages once, in page order (≈ sequential).
11
+ *
12
+ * Why a valve: unbounded deferral is its own failure mode, both measured in
13
+ * the #1231 repro. The WAL duplicates hot pages per COMMIT, so it grows far
14
+ * faster than the DB (5.9GB WAL for a ~340MB DB on a 3.3k-file index) —
15
+ * filling the disk, and poisoning every subsequent read that must page
16
+ * through it (the first resolution-phase read blocked the main thread >60s
17
+ * and the #850 liveness watchdog killed the healthy index). The valve
18
+ * watches WAL growth on a timer and, past a soft threshold, backfills with
19
+ * `PRAGMA wal_checkpoint(PASSIVE)` on a worker-thread connection — PASSIVE
20
+ * never blocks the writer, and off-thread means the main thread (and the
21
+ * watchdog heartbeat) keep turning regardless of how long a backfill takes.
22
+ *
23
+ * The load-bearing subtlety: a WAL file's SIZE never shrinks. After a full
24
+ * backfill, the writer's next commit RESTARTS the WAL from the top and the
25
+ * frames recycle inside the same file — so raw size says nothing about the
26
+ * un-backfilled backlog, and a size-triggered valve degenerates into firing
27
+ * (and pausing the writer) forever once the file passes its threshold
28
+ * (measured: guava crawled at ~9min per 160 files). Instead the valve
29
+ * tracks `sizeAtLastFullBackfill` — refreshed whenever a checkpoint reports
30
+ * `log === checkpointed` (everything backfilled) — and triggers on GROWTH
31
+ * beyond that baseline, which only happens when genuinely un-backfilled
32
+ * frames push past the file's high-water mark.
33
+ *
34
+ * Backpressure: if the writer outruns the checkpointer past a hard cap of
35
+ * growth (2× soft), {@link backpressure} pauses the writer (at a safe,
36
+ * between-transactions boundary) until a FULL backfill lands. One in-flight
37
+ * pass is not enough: on a disk saturated by the writer, every concurrent
38
+ * PASSIVE pass is already stale by the time it finishes (the writer appended
39
+ * past its snapshot), so neither SQLite's WAL wrap nor the baseline ever
40
+ * trigger and the WAL grows without bound (measured: 5.9GB on guava at 150
41
+ * IOPS, then a >60s read stall and a watchdog kill). With the writer parked,
42
+ * the next pass covers everything, the WAL wraps on the following commit,
43
+ * and the pause is the disk's honest catch-up cost — the correct terminal
44
+ * mode when hardware genuinely can't keep up with the append rate.
45
+ */
46
+ import type { DatabaseConnection } from './index';
47
+ /**
48
+ * Resolve the valve's soft threshold from the `CODEGRAPH_WAL_VALVE_MB`
49
+ * override; non-numeric / non-positive values fall back to the default.
50
+ */
51
+ export declare function resolveWalValveMb(envVal: string | undefined): number;
52
+ export declare class WalCheckpointValve {
53
+ private readonly db;
54
+ private readonly intervalMs;
55
+ private readonly log;
56
+ private timer;
57
+ private inflight;
58
+ /** Writer pause in progress (hard cap breached): passes loop until a full backfill. */
59
+ private pause;
60
+ /**
61
+ * WAL file size observed when a checkpoint last reported the ENTIRE WAL
62
+ * backfilled. Growth is measured against this baseline — see the header
63
+ * comment for why absolute size cannot be used.
64
+ */
65
+ private sizeAtLastFullBackfill;
66
+ private readonly softBytes;
67
+ private readonly hardBytes;
68
+ constructor(db: DatabaseConnection, softMb?: number, intervalMs?: number, log?: (msg: string) => void);
69
+ private mb;
70
+ /** Un-backfilled growth estimate: bytes the WAL has grown past the last full backfill. */
71
+ private growthBytes;
72
+ /** Begin watching the WAL. Idempotent; the timer never holds the loop open. */
73
+ start(): void;
74
+ /** Stop watching. Any in-flight checkpoint keeps running — await drain(). */
75
+ stop(): void;
76
+ /** One poll: fire an off-thread passive checkpoint when growth passes the soft threshold. */
77
+ check(): void;
78
+ /**
79
+ * Writer-side backstop, called at a between-transactions boundary. Returns
80
+ * null (no wait) while growth is under the hard cap; past it, returns a
81
+ * promise that resolves only once a FULL backfill has landed — see the
82
+ * header comment for why a single pass is not enough on a saturated disk.
83
+ */
84
+ backpressure(): Promise<void> | null;
85
+ /** Await any in-flight checkpoint and writer pause. */
86
+ drain(): Promise<void>;
87
+ /**
88
+ * Phase-boundary fold: backfill the ENTIRE WAL now (off-thread, awaited).
89
+ * Called between bulk phases — e.g. after parsing, before resolution's
90
+ * first reads — so the next phase never pages a bulk-write-sized WAL on
91
+ * the main thread (the post-parse read against a multi-GB WAL is what
92
+ * blew the #850 watchdog's 60s window in the #1231 repro). The await
93
+ * keeps the event loop (and the watchdog heartbeat) turning.
94
+ */
95
+ foldNow(): Promise<void>;
96
+ /**
97
+ * With the writer parked on the returned promise, loop passive passes until
98
+ * one reports the entire WAL backfilled (typically the second: the first
99
+ * drains the pass that was already running against a stale snapshot). Gives
100
+ * up after a bounded number of passes — e.g. a reader pinning the WAL —
101
+ * because unbounded WAL growth degrades; a wedged writer never recovers.
102
+ */
103
+ private backfillFully;
104
+ private fire;
105
+ }
106
+ //# sourceMappingURL=wal-valve.d.ts.map
@@ -86,10 +86,14 @@ export declare function findIndexedSubprojectRoots(root: string, opts?: {
86
86
  max?: number;
87
87
  }): string[];
88
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}.)
89
+ * Does `prompt` contain an explicit structural keyword? A keyword is a strong,
90
+ * self-contained signal, so the front-load hook fires on it directly — no graph
91
+ * check needed. (A *code-token* match, by contrast, is only a candidate the
92
+ * hook verifies against the graph first; see {@link extractCodeTokens}.)
93
+ * Coverage is multilingual (#994, #1126): the ~29 languages with the largest
94
+ * developer populations, across Latin, Cyrillic, Greek, CJK, Hangul, Arabic,
95
+ * Hebrew, Thai, and Devanagari scripts. Languages beyond the keyword table
96
+ * still fire through the language-agnostic code-token path.
93
97
  */
94
98
  export declare function hasStructuralKeyword(prompt: string): boolean;
95
99
  /**
@@ -110,7 +114,7 @@ export declare function extractCodeTokens(prompt: string): string[];
110
114
  /**
111
115
  * Cheap, graph-free candidate gate for the front-load hook: could `prompt` be a
112
116
  * structural / flow / impact / "where-how" question worth front-loading context
113
- * for? True on an explicit keyword (English or CJK, issue #994) OR an
117
+ * for? True on an explicit keyword in any covered language (#994, #1126) OR an
114
118
  * identifier-shaped token. A keyword is sufficient to fire on its own; a
115
119
  * token-only match is only a candidate the hook then verifies against the graph
116
120
  * (a brand name like `JavaScript` is token-shaped but isn't a symbol). Every
@@ -0,0 +1,107 @@
1
+ import { ExtractionResult, Language } from '../types';
2
+ /**
3
+ * CfmlExtractor - Extracts code relationships from CFML source (.cfc/.cfm).
4
+ *
5
+ * tree-sitter-cfml splits CFML into two related grammars: `cfml` (tag-based —
6
+ * `<cfcomponent>`/`<cffunction>`/HTML) and `cfscript` (modern bare-script
7
+ * `component { ... }` syntax). The `cfml` grammar's own injections.scm treats
8
+ * bare-script content as an opaque blob meant to be re-parsed by `cfscript` —
9
+ * that re-parsing only happens at the editor/highlighting layer, not in the
10
+ * raw AST, so this extractor replicates it: a file whose first real token
11
+ * isn't `<` is delegated wholesale to the cfscript grammar (the dominant
12
+ * modern style); otherwise the file is walked tag-by-tag with the cfml
13
+ * grammar, delegating any `<cfscript>` tag bodies the same way.
14
+ */
15
+ export declare class CfmlExtractor {
16
+ private filePath;
17
+ private source;
18
+ private language;
19
+ private nodes;
20
+ private edges;
21
+ private unresolvedReferences;
22
+ private errors;
23
+ /** `language` is the file's detected language — `'cfml'` for `.cfc`/`.cfm`, `'cfscript'` for `.cfs`. Both dialect-switch internally; this only controls the language tag stamped onto emitted nodes/refs. */
24
+ constructor(filePath: string, source: string, language?: Language);
25
+ extract(): ExtractionResult;
26
+ /** Modern bare-script `.cfc`/`.cfm`: delegate the whole file to the cfscript grammar. */
27
+ private extractBareScript;
28
+ /** Legacy tag-based CFML: walk `<cfcomponent>`/`<cffunction>`, delegating `<cfscript>` bodies. */
29
+ private extractTagBased;
30
+ /** Build the file's own `kind:'file'` node, spanning the whole source. Tag-based files need this explicitly — unlike `extractBareScript` (which delegates the whole file to `TreeSitterExtractor` and inherits its file node), `extractTagBased` walks the tree itself and has no other source of one. */
31
+ private createFileNode;
32
+ /**
33
+ * Walks `program`'s named children with a single forward cursor (not an
34
+ * index loop) — `extractComponent` consumes a variable run of FOLLOWING
35
+ * siblings as the component body (see its doc comment), so this must
36
+ * resume from whatever it last consumed rather than revisiting those same
37
+ * cffunction/cfscript siblings a second time as bogus top-level symbols.
38
+ */
39
+ private walkProgram;
40
+ /**
41
+ * `<cfcomponent extends="Base" implements="IFoo,IBar">...</cfcomponent>`.
42
+ * The grammar's implicit-end-tag scanner means component body content
43
+ * (cffunction tags, cfscript tags, etc.) appears as the open tag's FOLLOWING
44
+ * siblings in `program`, not nested children — walk forward to the matching
45
+ * cf_component_close_tag.
46
+ */
47
+ private extractComponent;
48
+ /**
49
+ * `<cffunction name="..." access="..." returntype="...">...</cffunction>`.
50
+ * `parentClassId` decides `method` vs top-level `function`; `containerId` is
51
+ * the `contains`-edge target (the class when inside one, otherwise the file
52
+ * node for a bare top-level cffunction) — kept separate so a top-level
53
+ * function still gets a containment edge without being misclassified as a
54
+ * method of the file. A method's qualifiedName is scoped under
55
+ * `parentClassName` (`TagService::save`, the same `Class::member` shape the
56
+ * generic extractor produces) so type-validated method resolution can match.
57
+ */
58
+ private extractFunctionTag;
59
+ /**
60
+ * Recursively delegates any `cf_script_tag`/`cf_query_tag` found within
61
+ * `node`'s subtree — e.g. a `<cfscript>`/`<cfquery>` nested inside
62
+ * `<cfif>`/`<cfloop>`/`<cftry>` control-flow tags, which (unlike
63
+ * `<cfcomponent>`'s body — see the implicit-end-tag note on `extractComponent`)
64
+ * ARE normal children, just possibly several levels deep, so a direct-children
65
+ * check misses them. Does not descend into a nested `cf_function_tag` — that
66
+ * has its own scope and is walked separately. `parentClassName` rides along
67
+ * so a `<cfscript>` at component scope classifies its functions as methods
68
+ * scoped under the component.
69
+ */
70
+ private delegateNestedTags;
71
+ /**
72
+ * Delegate a `<cfscript>...</cfscript>` tag body to the cfscript grammar.
73
+ * With `parentClassName` set (the block sits at component scope), functions
74
+ * declared at the script's top level are the component's methods
75
+ * (`<cfcomponent><cfscript>function configure(){}` — the standard ColdBox
76
+ * ModuleConfig shape): they're re-kinded `function` → `method`, and every
77
+ * merged symbol's qualifiedName is prefixed with the component scope
78
+ * (`configure` → `ModuleConfig::configure`) so type-validated method
79
+ * resolution can match them. Functions nested inside another function
80
+ * (closures) keep kind `function`.
81
+ */
82
+ private delegateScriptTag;
83
+ /**
84
+ * Delegate a `<cfquery>...</cfquery>` tag's SQL body to the `cfquery` grammar.
85
+ * `#hash#` expressions inside the SQL (e.g. `#getCurrentUser().getId()#` in a
86
+ * WHERE clause) are real CFML calls/references — tree-sitter-cfml's `cfquery`
87
+ * grammar parses them structurally (same `call_expression`/`member_expression`
88
+ * shape as cfscript), so without this delegation they're silently dropped as
89
+ * opaque SQL text. The grammar models no other symbols, so only call/reference
90
+ * extraction is relevant here — unlike `delegateScriptTag`, there are no nodes
91
+ * or contains-edges to merge.
92
+ */
93
+ private delegateQueryTag;
94
+ /** Read a `cf_attribute`'s value by name from a tag node's direct `cf_attribute`/`cf_tag_attributes` children. */
95
+ private tagAttr;
96
+ private componentNameFromPath;
97
+ }
98
+ /**
99
+ * Sniff whether CFML source is bare-script (`component { ... }`, modern style)
100
+ * vs tag-based (`<cfcomponent>`, `<cfif>`, HTML). Skips a leading UTF-8 BOM
101
+ * (endemic in CFML's Windows-editor history — 17% of ColdBox's files carry
102
+ * one; both grammars parse fine with it once routed correctly), whitespace,
103
+ * and `//`/`/* *\/` comments to find the first real token; tag-based files
104
+ * start with `<`, script-based files don't.
105
+ */
106
+ export declare function isBareScriptCfml(source: string): boolean;
107
+ //# sourceMappingURL=cfml-extractor.d.ts.map