@aroman22/codegraph-vba 1.10.0 → 1.11.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.
@@ -354,6 +354,43 @@ export declare class QueryBuilder {
354
354
  * a second stub edge onto the same real target (F1 duplicate-collapse).
355
355
  */
356
356
  edgeExists(source: string, target: string, kind: EdgeKind): boolean;
357
+ /**
358
+ * Issue #150 — return every `references` edge whose metadata was
359
+ * stamped by the given `synthesizedBy` key. Used by the
360
+ * event-handler synthesis pass to find every `WithEvents` binding
361
+ * in the project. The `metadata LIKE` filter is a cheap index-free
362
+ * pre-filter (a `synthesizedBy` value can appear as a substring of
363
+ * another); callers must still exact-check the parsed JSON
364
+ * `metadata.synthesizedBy` field.
365
+ *
366
+ * Returns the raw rows (id, source, target, metadata JSON). Callers
367
+ * join against `getNodeById` to materialize the source/target Node
368
+ * values. The `references` edges this returns are the
369
+ * `resolveVbaReferenceStubs`-repointed ones (target is the real
370
+ * class node, not the synthetic stub), so the binding lookup
371
+ * survives the resolver.
372
+ */
373
+ getReferencesBySynthesizedBy(synthesizedBy: string): Array<{
374
+ id: number;
375
+ source: string;
376
+ target: string;
377
+ metadata: string | null;
378
+ }>;
379
+ /**
380
+ * Issue #150 — return every `raises-event` edge in the DB. Used by
381
+ * the event-handler synthesis pass to walk every `RaiseEvent`
382
+ * statement and connect it to its `m_<var>_<EventName>` handler
383
+ * Sub. Returns the raw rows; callers parse the metadata to pull
384
+ * the `eventName` for downstream edge construction.
385
+ */
386
+ getRaisesEventEdges(): Array<{
387
+ id: number;
388
+ source: string;
389
+ target: string;
390
+ line: number | null;
391
+ col: number | null;
392
+ metadata: string | null;
393
+ }>;
357
394
  /**
358
395
  * Find all edges where both source and target are in the given node set.
359
396
  * Useful for recovering inter-node connectivity after BFS.
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Dysflow-export framework resolver (issue #154).
3
+ *
4
+ * The 3 Dysflow-specific extractors ÔÇö form/report SaveAsText UI, test
5
+ * manifests, and test sequences ÔÇö used to be hard-coded into
6
+ * `extractFromSource` as a `detectedLanguage === 'vba' && <isFoo> (filePath)`
7
+ * ladder. They are now lifted into this `FrameworkResolver`-shaped module
8
+ * so they can be:
9
+ * 1. Reached through the standard framework registry (the same path every
10
+ * other framework ÔÇö Express, React, Spring ÔÇö flows through).
11
+ * 2. Opted out at the project level via `codegraph.json`'s
12
+ * `vba.dysflowExport: false` ÔÇö useful for projects that carry legacy
13
+ * `.form.txt`/`.report.txt` files (or test manifests from a different
14
+ * system) and want them tracked as just a `file` node instead of
15
+ * being expanded into the graph.
16
+ *
17
+ * Architecture: the underlying `VbaFormExtractor` / `VbaTestManifestExtractor`
18
+ * / `VbaTestSequenceExtractor` classes are the single source of truth for
19
+ * the per-file extraction logic. This module:
20
+ * - decides WHICH sub-extractor applies to a file (path shape);
21
+ * - delegates the actual `extract()` to that sub-extractor;
22
+ * - exposes the `FrameworkResolver` shape (`detect` / `extract` /
23
+ * `resolve` / `claimsReference`) so the rest of the codebase can treat
24
+ * "Dysflow export" as a regular framework on top of the base VBA
25
+ * language.
26
+ *
27
+ * Behavior contract: with `dysflowExport: true` (the default), the emitted
28
+ * nodes/edges/references are byte-identical to the pre-refactor paths in
29
+ * `tree-sitter.ts`. With `dysflowExport: false`, the framework's `detect()`
30
+ * returns `false` so it's never in the per-project detected list ÔÇö meaning
31
+ * its `extract()` is never called and the form/report/manifest/sequence
32
+ * files fall through to a `file`-only node via the language-specific
33
+ * dispatch in `tree-sitter.ts`.
34
+ */
35
+ import { FrameworkResolver } from '../../resolution/types';
36
+ export declare const dysflowExportResolver: FrameworkResolver;
37
+ //# sourceMappingURL=dysflow-export.d.ts.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Extraction-side framework resolvers.
3
+ *
4
+ * `src/extraction/frameworks/` is the home for `FrameworkResolver`s whose
5
+ * primary contribution is per-file extraction (nodes + references), as
6
+ * opposed to `src/resolution/frameworks/` whose resolvers are primarily
7
+ * reference resolvers. The two registries are kept separate so a future
8
+ * "extraction-only" framework (e.g. another format-specific generator)
9
+ * doesn't drag in the full resolution pipeline, and so the namespacing
10
+ * matches the layer the resolver actually plugs into.
11
+ *
12
+ * Currently only the Dysflow export resolver lives here ÔÇö the 3
13
+ * Dysflow-specific extractors (form/report, test manifest, test
14
+ * sequence) were lifted out of `tree-sitter.ts` into this `FrameworkResolver`
15
+ * shape so the project can opt out via `codegraph.json`
16
+ * (`vba.dysflowExport: false`, issue #154). New extraction-side frameworks
17
+ * (e.g. a future "terraform state" generator) should be added here.
18
+ */
19
+ export { dysflowExportResolver } from './dysflow-export';
20
+ //# sourceMappingURL=index.d.ts.map
@@ -48,6 +48,16 @@ export interface ParseTask {
48
48
  language: Language;
49
49
  frameworkNames?: string[];
50
50
  vbaTargets?: Record<string, boolean>;
51
+ /** Issue #152: per-file fanout cap for `RaiseEvent` edges (VBA only). */
52
+ maxRaiseFanout?: number;
53
+ /**
54
+ * Issue #154 — gate the 3 Dysflow-specific VBA sub-extractors. `true`
55
+ * (the default) keeps the pre-refactor behavior; `false` opts out so
56
+ * form/report/manifest/sequence files are tracked as just a `file`
57
+ * node. Threaded through the worker so the same flag is honored on
58
+ * either side of the pool boundary.
59
+ */
60
+ dysflowExport?: boolean;
51
61
  }
52
62
  /**
53
63
  * Resolve the pool size from the `CODEGRAPH_PARSE_WORKERS` override and the
@@ -701,6 +701,14 @@ export declare class TreeSitterExtractor {
701
701
  * If `frameworkNames` is provided, framework-specific extractors matching
702
702
  * those names and the file's language are run after the tree-sitter pass.
703
703
  * Their nodes/references/errors are merged into the returned result.
704
+ *
705
+ * `dysflowExport` (issue #154) gates the 3 Dysflow-specific VBA
706
+ * sub-extractors (form/report SaveAsText, test manifests, test sequences).
707
+ * `true` (the default) preserves the pre-refactor behavior; `false` opts
708
+ * out so `.form.txt`/`.report.txt`/manifest/sequence files are tracked as
709
+ * just a `file` node. Read from `codegraph.json` via
710
+ * `loadDysflowExportConfig(rootDir)` at the call site and threaded in here
711
+ * so this function stays project-config-agnostic.
704
712
  */
705
- export declare function extractFromSource(filePath: string, source: string, language?: Language, frameworkNames?: string[], vbaTargets?: Record<string, boolean>): ExtractionResult;
713
+ export declare function extractFromSource(filePath: string, source: string, language?: Language, frameworkNames?: string[], vbaTargets?: Record<string, boolean>, maxRaiseFanout?: number, dysflowExport?: boolean): ExtractionResult;
706
714
  //# sourceMappingURL=tree-sitter.d.ts.map
@@ -1,9 +1,52 @@
1
1
  import { VbaExtractorContext, VbaClassifier } from './context';
2
+ import { VbaExtractionRule } from './rules';
3
+ /**
4
+ * Issue #153: the declarative rule table for the calls/SQL concern.
5
+ *
6
+ * Of the call-sweep's full machinery, only the per-line *patterns*
7
+ * fit the declarative shape. The procedural scanners
8
+ * (`scanRaiseEvents`, `scanCallSites`, `scanMeControlReferences`,
9
+ * `scanSqlInLine`, `scanDoCmdOpenCalls`, `scanDoCmdOpenQuery`,
10
+ * `scanFormsBang`, `sweepTempVars`, statement/qualified call
11
+ * detection) walk the masked line scanning for call shapes that
12
+ * don't reduce to a single regex — they stay inside the factory's
13
+ * `classifyLine` (acknowledged in the Issue #153 spec: "The
14
+ * inter-line state machines (procedure stack, with stack,
15
+ * sqlVariables) NEED a class, not a pure rule.").
16
+ *
17
+ * Four rules, all driven by the masked line (the call site / Set
18
+ * patterns must not match inside string literals):
19
+ *
20
+ * - `set-new` — `Set <var> = New <Type>[.<Inner>]`; registers the
21
+ * receiver in `localVarTypeMap` and emits a
22
+ * `vba-set-new` `references` edge.
23
+ * - `set-call` — `Set <var> = <Factory>(...)`; types the receiver
24
+ * from a same-file function's project-class return
25
+ * type so a later `x.Method` resolves to the
26
+ * factory's class. Emits a `vba-factory-return`
27
+ * `references` edge.
28
+ * - `with-start` — `With <receiver>`; normalizes the receiver and
29
+ * pushes it onto `ctx.vbaWithStack` (replaces the
30
+ * pre-#153 per-factory closure variable).
31
+ * - `with-end` — `End With`; pops the matching `ctx.vbaWithStack`
32
+ * entry.
33
+ *
34
+ * The `set-new` rule's emit function is a "pure" (match, ctx, line)
35
+ * consumer — it reads/writes only `ctx` and the unmasked/ masked
36
+ * line, with no closure references. Same for the other three.
37
+ */
38
+ export declare const RULES: readonly VbaExtractionRule<unknown>[];
2
39
  /**
3
40
  * Issue #83: factory for the calls/SQL classifier. The factory takes the
4
41
  * pre-split `lines` array (so `trackSqlVariableAssignment` can do its
5
42
  * multi-line look-ahead for `&`-accumulate semantics) and closes over the
6
43
  * per-file state the legacy `sweepCallsAndSql` declared locally.
44
+ *
45
+ * Issue #153: the per-line pattern matching has been extracted into
46
+ * the `RULES` table. The factory body is now a thin shell that
47
+ * dispatches `RULES` and runs the procedural scanners (call sites,
48
+ * raise events, SQL, DoCmd, TempVars, etc.) that don't reduce to a
49
+ * single regex.
7
50
  */
8
51
  export declare function createCallsAndSqlClassifier(lines: readonly string[]): VbaClassifier;
9
52
  /**
@@ -143,8 +143,58 @@ export declare class VbaExtractorContext {
143
143
  * of every procedure whose `End` marker the sweep has not yet seen.
144
144
  */
145
145
  procStack: number[];
146
+ /**
147
+ * Issue #153: per-extraction state for the events/types/declares
148
+ * classifier's `Type <Name> ... End Type` block tracking. The
149
+ * `currentTypeBlock` is set when a `type-start` rule fires and
150
+ * cleared when the matching `type-end` rule fires. Lives on `ctx`
151
+ * (instead of the factory's closure) so the declarative RULES
152
+ * table's `emit` functions can read/write it without taking a
153
+ * closure-captured mutable reference. `null` means "outside any
154
+ * type block right now". Reset implicitly per-extraction (a fresh
155
+ * `VbaExtractorContext` is constructed every `extract()` call).
156
+ */
157
+ vbaDeclTypeBlock: {
158
+ id: string;
159
+ name: string;
160
+ } | null;
161
+ /**
162
+ * Issue #153: per-extraction state for the enum/consts classifier's
163
+ * `Enum <Name> ... End Enum` block tracking. Same shape and
164
+ * rationale as `vbaDeclTypeBlock`: lives on `ctx` so the
165
+ * declarative RULES table's `emit` functions can read/write it
166
+ * without taking a closure-captured mutable reference.
167
+ * `null` means "outside any enum block right now".
168
+ */
169
+ vbaEnumBlock: {
170
+ id: string;
171
+ name: string;
172
+ } | null;
173
+ /**
174
+ * Issue #153: per-extraction state for the calls/SQL classifier's
175
+ * `With <receiver> ... End With` block tracking. The with-receiver
176
+ * stack mirrors the textual nesting of `With` blocks inside a
177
+ * procedure body — the top of the stack is the active receiver
178
+ * for `.Member` and `.Member(args)` statement-form calls. Reset
179
+ * implicitly per-extraction (a fresh `VbaExtractorContext` is
180
+ * constructed every `extract()` call). Replaces the per-factory
181
+ * closure variable that lived in `sweepCallsAndSql` before the
182
+ * Issue #153 RULES-table refactor.
183
+ */
184
+ vbaWithStack: string[];
146
185
  /** Local event name (lowercase) → event node for `RaiseEvent` edge emission. */
147
186
  localEvents: Map<string, Node>;
187
+ /**
188
+ * Issue #152: per-event counter for `RaiseEvent <EventName>` sites. Keyed
189
+ * by event node id (so two different events that share a name across
190
+ * files don't collide — the gate is per-file). The count is incremented
191
+ * in `scanRaiseEvents` BEFORE the edge is pushed, so the count equals
192
+ * the number of raise sites regardless of the gate decision downstream.
193
+ * The orchestrator's fanout-gate pass reads this map to decide which
194
+ * event nodes to flag `metadata.highFanout: true` and which `raises-event`
195
+ * edges to drop.
196
+ */
197
+ raiseEventCounts: Map<string, number>;
148
198
  /**
149
199
  * Same-file function/property return types, keyed by lowercase proc name →
150
200
  * declared return type. ONLY non-primitive (project-class) return types are
@@ -156,7 +206,34 @@ export declare class VbaExtractorContext {
156
206
  * time) — that stays the resolver's frontier.
157
207
  */
158
208
  functionReturnTypes: Map<string, string>;
209
+ /**
210
+ * Issue #156: per-stage timings, lazily allocated ONLY when
211
+ * `CODEGRAPH_VBA_TIMING=1|2` is set. The default path (env var unset)
212
+ * keeps this `null` so `recordStage` is a single null-check and we
213
+ * pay zero Map-allocation cost in the hot path. Stage names are
214
+ * dot-separated: `preprocess.stripVbaComments`,
215
+ * `preprocess.cc.lexer+parser`, `walk.procedures`, `walk.main`,
216
+ * `classifier.<name>`. Counters are in milliseconds (float).
217
+ */
218
+ timings: Map<string, number> | null;
219
+ /**
220
+ * Issue #156: how many times each classifier's `classifyLine` was
221
+ * invoked. Only allocated together with `timings` so the default path
222
+ * pays no cost.
223
+ */
224
+ classifierInvokeCounts: Map<string, number> | null;
159
225
  constructor(filePath: string);
226
+ /**
227
+ * Issue #156: ensure the timings Maps exist. Called once per
228
+ * `extract()` by the orchestrator right after it reads the env var.
229
+ * No-op when already allocated.
230
+ */
231
+ ensureTimings(): void;
232
+ /**
233
+ * Issue #156: add `ms` to the stage named `name`. No-op when
234
+ * `timings` is null (default path).
235
+ */
236
+ recordStage(name: string, ms: number): void;
160
237
  /**
161
238
  * Emit a `contains` edge from the (lazily-created) module/class node to
162
239
  * `targetId`. Mirrors the pending-source pattern the procedure and
@@ -193,6 +270,27 @@ export declare class VbaExtractorContext {
193
270
  resolveReceiverType(receiverName: string): string;
194
271
  findOrCreateFunctionNodeId(proc: ProcInfo): string;
195
272
  findFunctionNodeByName(name: string): Node | undefined;
273
+ /**
274
+ * Issue #152: apply the per-file `RaiseEvent` fanout gate. For every
275
+ * event whose `raiseEventCounts` value exceeds `maxFanout`:
276
+ * 1. Stamp the event node's `metadata.highFanout = true` and
277
+ * `metadata.raiseCount = <count>` so consumers (codegraph_explore,
278
+ * vba-event-tracer) can recognize a high-fanout event.
279
+ * 2. Drop every `raises-event` edge targeting that event id from
280
+ * `edges`. The event node itself stays — declaration site, handler
281
+ * linkage via `subscribes-event`, and metadata are all preserved.
282
+ *
283
+ * Events with `<= maxFanout` raise sites are untouched (no flag, edges
284
+ * stay). When `maxFanout` is undefined, the gate is disabled and the
285
+ * method is a no-op. Mirrors the upstream `EVENT_FANOUT_CAP` discipline
286
+ * in `src/resolution/callback-synthesizer.ts:43` (this side suppresses
287
+ * the producer side, the upstream side suppresses the synthesized
288
+ * cross-file dispatcher/handler edges).
289
+ *
290
+ * Returns the number of `raises-event` edges dropped (zero when the
291
+ * gate is disabled or no event exceeds the cap — useful for tests).
292
+ */
293
+ applyRaiseFanoutGate(maxFanout: number | undefined): number;
196
294
  /**
197
295
  * Emit a `references` edge from the file's module/class node to a synthetic
198
296
  * node named `targetName`. Used by Dim, WithEvents, Set-New, and SQL sweeps.
@@ -206,8 +304,18 @@ export declare class VbaExtractorContext {
206
304
  * TempVars sweep already emits (`emitTempVarReference`). Omitted for
207
305
  * structural references (Dim/WithEvents/Set-New) where the direction is not
208
306
  * meaningful.
307
+ *
308
+ * `extras` (optional): additional metadata fields to merge into the
309
+ * emitted edge's `metadata` object. Used by the WithEvents sweep to
310
+ * stamp `variableName` (issue #150) so the post-extraction
311
+ * event-handler synthesis pass can find the `m_<var>_<event>`
312
+ * handler Sub even after `resolveVbaReferenceStubs` CASCADE-deletes
313
+ * the companion `subscribes-event` edge along with the synthetic
314
+ * class stub. Fields here are merged AFTER the standard
315
+ * `{ synthesizedBy [, access] }` shape, so callers can override
316
+ * (rarely useful) but not accidentally drop the stamp.
209
317
  */
210
- emitReference(targetName: string, lineNum: number, column: number, synthesizedBy: string, access?: 'read' | 'write'): void;
318
+ emitReference(targetName: string, lineNum: number, column: number, synthesizedBy: string, access?: 'read' | 'write', extras?: Record<string, unknown>): void;
211
319
  /**
212
320
  * Issue #52: shared lookup helper for `scanDoCmdOpenCalls` and
213
321
  * `scanDoCmdOpenQuery`. The current scope is the procedure whose
@@ -1,7 +1,43 @@
1
1
  import { VbaExtractorContext, VbaClassifier } from './context';
2
+ import { VbaExtractionRule } from './rules';
2
3
  /**
3
- * Issue #83: factory for the events/types/declares classifier. Closure
4
- * state: `currentType` (the open `Type ... End Type` block).
4
+ * Issue #153: the declarative rule table for the events/types/declares
5
+ * concern. Five rules, partitioned by where they fire:
6
+ *
7
+ * - `event-decl` (outside-type-block) — match an `Event <Name>` decl
8
+ * - `type-start` (outside-type-block) — match a `Type <Name>` header;
9
+ * sets `ctx.vbaDeclTypeBlock` on a successful match
10
+ * - `type-end` (inside-type-block) — match `End Type`; clears
11
+ * `ctx.vbaDeclTypeBlock` on a successful match
12
+ * - `type-member` (inside-type-block) — match `<Member> As <Type>`
13
+ * inside an open type block
14
+ * - `dll-declare` (outside-type-block) — match a `[Private|Public]
15
+ * Declare [PtrSafe] Sub|Function <Name> Lib "<dll>"
16
+ * [Alias "<x>"]` Win32 API declaration
17
+ *
18
+ * The `requires` field encodes the type-block precondition. The
19
+ * dispatcher (the factory's `classifyLine`) filters rules whose
20
+ * precondition does not hold, so an `End Type` line never
21
+ * accidentally re-enters the `event-decl` / `dll-declare` paths.
22
+ *
23
+ * Inter-line state lives on `ctx.vbaDeclTypeBlock` (replaces the
24
+ * pre-#153 factory closure variable) so the emit functions can
25
+ * read/write it without taking a mutable closure reference.
26
+ */
27
+ export declare const RULES: readonly VbaExtractionRule<unknown>[];
28
+ /**
29
+ * Issue #83: factory for the events/types/declares classifier.
30
+ *
31
+ * Inter-line state (`ctx.vbaDeclTypeBlock`) lives on `ctx` — the
32
+ * declarative RULES table's `emit` functions read/write it directly.
33
+ * The dispatcher below honours each rule's `requires` precondition
34
+ * so the per-line dispatch mirrors the legacy cascade exactly:
35
+ *
36
+ * if (currentType) {
37
+ * try type-end, then type-member; return early
38
+ * } else {
39
+ * try event-decl, type-start, dll-declare
40
+ * }
5
41
  */
6
42
  export declare function createEventsTypesDeclaresClassifier(): VbaClassifier;
7
43
  /**
@@ -1,6 +1,40 @@
1
1
  import { VbaExtractorContext, VbaClassifier } from './context';
2
+ import { VbaExtractionRule } from './rules';
2
3
  /**
3
- * Issue #83: factory for the Dim / WithEvents classifier. Stateless per-line.
4
+ * Issue #153: the declarative rule table for the Dim / WithEvents
5
+ * concern. Two rules:
6
+ *
7
+ * - `dim-decl` — match a `Dim|Private|Public|Global|Static
8
+ * <var> [As [New] <TypePart>[.<TypePart>]][, …]`
9
+ * declaration; populate `localVarTypeMap` and
10
+ * emit one `references` edge per non-primitive
11
+ * type (or per qualified outer type).
12
+ * - `withevents-decl`— match a `[Dim|Private|Public|Global|Static]
13
+ * WithEvents <var> As <FormType>` declaration;
14
+ * populate `localVarTypeMap` with `withEvents: true`,
15
+ * emit a `references` edge (synthesizedBy:
16
+ * `vba-withevents`) and a `subscribes-event` edge.
17
+ *
18
+ * The two rules are dispatched independently and BOTH can fire on the
19
+ * same line in principle (a `WithEvents` line does NOT match the
20
+ * `Dim|Private|...` prefix because the negative lookahead excludes it).
21
+ * The `dim-decl` rule's `count` hook reports the number of
22
+ * non-primitive `references` edges it emitted, so a multi-variable
23
+ * `Dim a As Foo, b As Bar` line adds 2 to the classifier count, and a
24
+ * bare `Dim x` line adds 0 (the bare-Dim path is a `localVarTypeMap`
25
+ * only side effect, no graph edges).
26
+ *
27
+ * No inter-line state — each rule is self-contained.
28
+ */
29
+ export declare const RULES: readonly VbaExtractionRule<unknown>[];
30
+ /**
31
+ * Issue #83: factory for the Dim / WithEvents classifier.
32
+ *
33
+ * The body walks the declarative `RULES` table (Issue #153). The two
34
+ * rules are independent: `dim-decl` handles typed declarations
35
+ * (REJECTED if the line is a `WithEvents` because the prefix
36
+ * negative-lookahead excludes `WithEvents`), `withevents-decl`
37
+ * handles WithEvents. No inter-line state.
4
38
  */
5
39
  export declare function createDimsClassifier(): VbaClassifier;
6
40
  /**
@@ -1,13 +1,53 @@
1
1
  import { VbaExtractorContext, VbaClassifier } from './context';
2
+ import { VbaExtractionRule } from './rules';
2
3
  /**
3
- * Issue #83: factory for the enum / const classifier. Closure state:
4
- * `currentEnum` (open `Enum ... End Enum` block).
4
+ * Issue #153: the declarative rule table for the enum / const concern.
5
+ * Six rules, partitioned by where they fire:
5
6
  *
6
- * Also resets + advances the SHARED `ctx.procStack`/`ctx.currentProcKey`
7
- * per line same protocol the pre-#83 sweep followed. The calls-sql
8
- * classifier runs AFTER this one in the per-line dispatch order and
9
- * applies the same protocol, so the end-of-line scope state is identical
10
- * to the legacy sequential-sweep behaviour.
7
+ * - `proc-start` (outside-enum-block) match a `Sub` /
8
+ * `Function` / `Property` header; push onto
9
+ * `ctx.procStack` and set `ctx.currentProcKey`
10
+ * so subsequent Const writes land in the
11
+ * per-proc resolution bucket.
12
+ * - `proc-end` (outside-enum-block) — match `End Sub` /
13
+ * `End Function` / `End Property`; pop the
14
+ * matching `procStack` entry.
15
+ * - `enum-start` (outside-enum-block) — match `[visibility]
16
+ * Enum <Name>`; emit an `enum` node + `contains`
17
+ * edge and mark `ctx.vbaEnumBlock`.
18
+ * - `enum-end` (inside-enum-block) — match `End Enum`; clear
19
+ * `ctx.vbaEnumBlock`.
20
+ * - `enum-member` (inside-enum-block) — match `<MemberName>
21
+ * [= <value>]`; emit an `enum_member` node + a
22
+ * `contains` edge from the open enum.
23
+ * - `const-decl` (outside-enum-block) — match `[visibility]
24
+ * Const <decls>`; always write the per-scope
25
+ * resolution bucket, and emit a `constant` node
26
+ * + `contains` edge ONLY when not inside a proc
27
+ * (proc-local Consts are not module symbols).
28
+ *
29
+ * The inter-line `ctx.procStack` / `ctx.currentProcKey` state is
30
+ * SHARED with the calls/SQL classifier (issue #52 protocol). The
31
+ * `ctx.vbaEnumBlock` state is local to this concern and lives on
32
+ * `ctx` for the same RULES-table-friendliness reason as
33
+ * `vbaDeclTypeBlock`.
34
+ */
35
+ export declare const RULES: readonly VbaExtractionRule<unknown>[];
36
+ /**
37
+ * Issue #83: factory for the enum / const classifier.
38
+ *
39
+ * The body walks the declarative `RULES` table (Issue #153) and
40
+ * honours each rule's `requires` precondition. The inter-line
41
+ * state — `ctx.procStack`, `ctx.currentProcKey`, and
42
+ * `ctx.vbaEnumBlock` — lives on `ctx` so the RULES table's `emit`
43
+ * functions can read/write it without taking a closure reference.
44
+ *
45
+ * Issue #52: the first invocation also resets the shared proc-stack
46
+ * + lookup key so leftover state from a previous `extract()` (only
47
+ * possible in tests that construct a fresh extractor and run twice)
48
+ * never leaks across sweeps. The walk below updates both every
49
+ * iteration; `sweepCallsAndSql` resets again at its own start, so
50
+ * the protocol stays consistent across both classifiers.
11
51
  */
12
52
  export declare function createEnumsConstsClassifier(): VbaClassifier;
13
53
  /**
@@ -0,0 +1,3 @@
1
+ import { QueryBuilder } from '../../db/queries';
2
+ export declare function synthesizeVbaEventHandlerEdges(queries: QueryBuilder): number;
3
+ //# sourceMappingURL=event-synth.d.ts.map
@@ -1,6 +1,22 @@
1
1
  import { VbaExtractorContext, VbaClassifier } from './context';
2
+ import { VbaExtractionRule } from './rules';
3
+ /**
4
+ * Issue #153: the declarative rule table for the implements concern.
5
+ * One rule — `implements` — matching a leading `Implements <Name>`
6
+ * declaration. The body has no inter-line state to track, so the
7
+ * orchestrator walks the table and bumps `this.count` on every
8
+ * non-null emit. Keeps the inline `classifyLine` below honest: any
9
+ * branch not represented in this table is dead code.
10
+ */
11
+ export declare const RULES: readonly VbaExtractionRule<unknown>[];
2
12
  /**
3
13
  * Issue #83: factory for the `Implements` classifier. Stateless per-line.
14
+ *
15
+ * The body walks the declarative `RULES` table — the inline regex
16
+ * match that lived here before #153 is now a single entry in
17
+ * `RULES`. This is the canonical pattern every classifier will
18
+ * converge on as part of the refactor; the legacy "one giant
19
+ * if/else cascade" is gone.
4
20
  */
5
21
  export declare function createImplementsClassifier(): VbaClassifier;
6
22
  /**
@@ -1,7 +1,27 @@
1
1
  import { VbaExtractorContext, ProcInfo, VbaClassifier } from './context';
2
+ import { VbaExtractionRule } from './rules';
3
+ /**
4
+ * Issue #153: the declarative rule table for the procedures concern.
5
+ * One rule — `procedure` — matches a `Sub` / `Function` /
6
+ * `Property Get|Let|Set` declaration and emits the function node,
7
+ * the `localProcs` / `functionNodeByName` / `functionNodeByStartLine`
8
+ * / `functionReturnTypes` registration, and (for form code-behind)
9
+ * the synthesized `event-handler` edge.
10
+ *
11
+ * The emit body is intentionally not split across multiple rules —
12
+ * these are 5+ small operations that all fire on the same declaration
13
+ * line. Splitting them would force the orchestrator to know they
14
+ * always co-occur, which is more coupling than the original cascade
15
+ * had. The pattern is the discriminating surface; everything past
16
+ * `match` is one emit.
17
+ */
18
+ export declare const RULES: readonly VbaExtractionRule<unknown>[];
2
19
  /**
3
20
  * Issue #83: factory for the procedures classifier. Closure state: none
4
21
  * beyond `count` (the per-concern accumulators live on `ctx`).
22
+ *
23
+ * The body walks the declarative `RULES` table (Issue #153). The
24
+ * inline cascade that used to live here is now one entry in `RULES`.
5
25
  */
6
26
  export declare function createProceduresClassifier(): VbaClassifier;
7
27
  /**
@@ -0,0 +1,125 @@
1
+ /**
2
+ * VbaExtractionRule — the declarative rule shape every VBA classifier
3
+ * exports as a `RULES: VbaExtractionRule[]` constant. Issue #153.
4
+ *
5
+ * Before this refactor the per-concern sweepers under `src/extraction/vba/`
6
+ * encoded their rules as inline `if (RE.test(line)) { ... }` branches
7
+ * inside `classifyLine`. Each branch was hard to enumerate, impossible
8
+ * to unit-test in isolation, and easy to silently drop when refactoring
9
+ * the body of a sweep. The rule table here promotes the pattern →
10
+ * emit-mapping to a typed object so:
11
+ *
12
+ * 1. Every rule has a stable `id` (used in error messages, logs, and
13
+ * as a stable handle for tools that consume the rule table).
14
+ * 2. The pattern is explicit (single RegExp or RegExp[]) — the
15
+ * dispatcher knows exactly what to test, no "branched control flow
16
+ * around the regex".
17
+ * 3. The emit function is the ONLY side effect for the rule — pure
18
+ * given (match, ctx, line, lineNum) → T | null. That makes the
19
+ * rule trivially testable in isolation.
20
+ * 4. The `count?` hook lets a rule report "N symbols emitted" without
21
+ * sharing an accumulator with the orchestrator (each rule owns
22
+ * its own count semantics).
23
+ *
24
+ * Inter-line state (procedure stack, With stack, SQL variables) STILL
25
+ * lives on `VbaExtractorContext` — the rule table is per-line. The
26
+ * `classifyLine` orchestrator inside each classifier is the "shell"
27
+ * that maintains the state machine and walks the rule table.
28
+ */
29
+ import type { VbaExtractorContext } from './context';
30
+ /**
31
+ * A single per-line rule the orchestrator dispatches.
32
+ *
33
+ * @typeParam T - The shape `emit` returns. Most rules return
34
+ * `void | null` because they push directly onto `ctx.nodes` /
35
+ * `ctx.edges` (the canonical VBA sweep idiom). Rules that
36
+ * synthesize a specific symbol can narrow `T` to the symbol's
37
+ * shape so `count?` and downstream consumers get a typed handle.
38
+ *
39
+ * Fields:
40
+ *
41
+ * - `id` Stable, human-readable identifier (e.g. `'implements'`,
42
+ * `'procedure'`, `'dim'`, `'sql-in-strings'`). The rule
43
+ * table's primary key — every id within one file must be
44
+ * unique. Used in error messages and in the test suite
45
+ * that pins the table's shape.
46
+ * - `description` One-line plain-English summary. Surfaced in tooling
47
+ * that introspects the table.
48
+ * - `pattern` The regex (or regex alternatives) the dispatcher tests
49
+ * against the per-line source. Either a single `RegExp`
50
+ * or a non-empty `RegExp[]` (a rule is matched when ANY
51
+ * of the alternatives matches). The dispatcher runs
52
+ * `.exec()` for single regexes and iterates for arrays.
53
+ * - `requires?` Optional structural gate. `'class'` means the rule
54
+ * only fires inside a `.cls` file; `'module'` only in
55
+ * `.bas`/`.frm`/`.dsr`; `'inside-procedure'` only when
56
+ * `ctx.procStack.length > 0`. The orchestrator is
57
+ * expected to honour this — keeping the gate declarative
58
+ * lets the rule own its own preconditions instead of
59
+ * scattering `if` checks into the rule body.
60
+ * - `scan?` `'masked'` = run on the string-literal-masked line
61
+ * (so call patterns inside `"..."` are ignored), the
62
+ * default. `'unmasked'` = run on the original line (so
63
+ * SQL patterns inside `"..."` are caught). `'both'` =
64
+ * dispatcher calls the rule twice, once with each line.
65
+ * - `emit` The per-match side effect. Receives the
66
+ * `RegExpExecArray` from the pattern, the shared
67
+ * `VbaExtractorContext`, the original (or masked) line,
68
+ * and the 1-based line number. Returns the symbol it
69
+ * emitted (used by `count?`) or `null` when the match
70
+ * didn't apply (the caller is expected to skip
71
+ * `count?` in that case).
72
+ * - `count?` Optional counter. When the rule emits exactly one
73
+ * symbol per successful match the orchestrator can
74
+ * treat `1` as the implicit count; the hook is for
75
+ * rules whose emit can fan out (e.g. a multi-variable
76
+ * `Dim a As Foo, b As Bar` line that produces two
77
+ * `references` edges from one match).
78
+ *
79
+ * The `count` parameter is typed as `unknown` to keep the
80
+ * `VbaExtractionRule<T>` shape covariant in `T` — narrowing `T`
81
+ * (e.g. to `{ edges: number }` for the dim-decl fan-out rule) does
82
+ * NOT then force `count` to take that narrower type, which would
83
+ * break the `VbaExtractionRule<unknown>[]` aggregate. Rule
84
+ * authors cast inside their `count` body.
85
+ */
86
+ export interface VbaExtractionRule<T = unknown> {
87
+ readonly id: string;
88
+ readonly description: string;
89
+ readonly pattern: RegExp | RegExp[];
90
+ readonly requires?: 'class' | 'module' | 'inside-procedure' | string;
91
+ readonly scan?: 'masked' | 'unmasked' | 'both';
92
+ readonly emit: (match: RegExpMatchArray, ctx: VbaExtractorContext, line: string, lineNum: number) => T | null;
93
+ readonly count?: (result: unknown) => number;
94
+ }
95
+ /**
96
+ * Helper to build a `VbaExtractionRule<T>` with a single RegExp.
97
+ * Most rules are 1-line declarations; this collapses the boilerplate
98
+ * to one place.
99
+ */
100
+ export declare function defineRule<T = unknown>(spec: Omit<VbaExtractionRule<T>, 'pattern'> & {
101
+ pattern: RegExp;
102
+ }): VbaExtractionRule<T>;
103
+ /**
104
+ * Helper to build a `VbaExtractionRule<T>` whose `pattern` is an array
105
+ * of alternative regexes. The dispatcher treats the rule as matched
106
+ * when ANY alternative matches; it runs the alternatives in order and
107
+ * uses the first match's `RegExpExecArray` for `emit`.
108
+ */
109
+ export declare function defineRuleAlternatives<T = unknown>(spec: Omit<VbaExtractionRule<T>, 'pattern'> & {
110
+ pattern: RegExp[];
111
+ }): VbaExtractionRule<T>;
112
+ /**
113
+ * Run a single rule's `pattern` against `line` and return the first
114
+ * match's `RegExpMatchArray`, or `null` when no alternative matches.
115
+ * `pattern` is `RegExp | RegExp[]` (an array means "match any of
116
+ * these alternatives"); this helper normalizes both into the same
117
+ * `RegExpMatchArray | null` shape the dispatcher's `emit` expects.
118
+ *
119
+ * Every classifier's `classifyLine` walks the declarative `RULES`
120
+ * table via this helper. Centralizing the RegExp/RegExp[] branching
121
+ * here keeps the per-classifier dispatcher loops trivial and avoids
122
+ * each classifier re-implementing the same boilerplate.
123
+ */
124
+ export declare function matchRule(pattern: RegExp | RegExp[], line: string): RegExpMatchArray | null;
125
+ //# sourceMappingURL=rules.d.ts.map
@@ -1,11 +1,87 @@
1
1
  import { ExtractionResult } from '../types';
2
+ import type { VbaExtractionRule } from './vba/rules';
3
+ /**
4
+ * Issue #152: per-file fanout cap for `RaiseEvent <EventName>` edges. An
5
+ * event raised from more than this many sites in a single file is
6
+ * flagged `metadata.highFanout: true` and ALL `raises-event` edges to it
7
+ * are dropped — the gate suppresses graph noise from events with generic
8
+ * names (`Change`, `Click`, `AfterUpdate`, …) that would otherwise
9
+ * produce hundreds of edges per file. Mirrors the upstream
10
+ * `EVENT_FANOUT_CAP` discipline in
11
+ * `src/resolution/callback-synthesizer.ts:43`. Configurable via
12
+ * `codegraph.json` → `vba.maxRaiseFanout`. Pass `Number.POSITIVE_INFINITY`
13
+ * (or any value `>=` the largest expected count) to disable the gate.
14
+ */
15
+ export declare const DEFAULT_MAX_RAISE_FANOUT = 50;
16
+ /**
17
+ * Issue #153: the orchestrator's aggregated view of every VBA
18
+ * classifier's declarative `RULES` table. Each entry is the
19
+ * `readonly VbaExtractionRule[]` constant exported by the
20
+ * corresponding classifier file. Exposed at module scope so:
21
+ *
22
+ * 1. Tests can pin the table shape and assert that the orchestrator
23
+ * still references each table (a deleted import would
24
+ * accidentally break a whole concern and the test would catch
25
+ * the missing reference).
26
+ * 2. Tooling can introspect every rule the orchestrator
27
+ * dispatches — useful for `codegraph stats` style UIs that
28
+ * show "what kinds of VBA symbols does codegraph know about".
29
+ * 3. The `validateVbaRuleTables` invariant runs on this aggregate
30
+ * so a refactor that empties one concern's `RULES` array
31
+ * (e.g. by deleting the `defineRule` calls during a partial
32
+ * migration) fails loudly at module load instead of silently
33
+ * dropping a whole concern.
34
+ *
35
+ * The keys are the FILE BASENAMES of the classifier modules (no
36
+ * `.ts` extension, kebab-case) — the same naming convention the
37
+ * per-classifier export comments use. Stable identifier for
38
+ * `validateVbaRuleTables` consumers.
39
+ */
40
+ export declare const VBA_RULE_TABLES: Readonly<Record<string, readonly VbaExtractionRule[]>>;
41
+ /**
42
+ * Issue #153: validate that every per-concern rule table the
43
+ * orchestrator dispatches is non-empty. Called once at module load
44
+ * (see the IIFE at the bottom of this file) so an accidentally-
45
+ * emptied `RULES` array fails loudly when the extractor is first
46
+ * imported, NOT at the first `extract()` call.
47
+ *
48
+ * The result is `{ ok, empty }`:
49
+ * - `ok: true` — every required table has at least one rule.
50
+ * - `ok: false` — at least one table is empty; `empty` lists the
51
+ * concern keys that need attention.
52
+ *
53
+ * The function accepts an optional `tables` argument so the unit
54
+ * test in `__tests__/extraction-vba-rule-table.test.ts` can verify
55
+ * the validator's contract independently of the live data.
56
+ */
57
+ export declare function validateVbaRuleTables(tables?: Readonly<Record<string, readonly VbaExtractionRule[]>>): {
58
+ ok: boolean;
59
+ empty: string[];
60
+ };
2
61
  export declare class VbaExtractor {
3
62
  private filePath;
4
63
  private source;
5
64
  private ctx;
6
65
  private vbaTargets?;
7
- constructor(filePath: string, source: string, vbaTargets?: Record<string, boolean>);
66
+ /**
67
+ * Issue #152: per-file fanout cap for `RaiseEvent <EventName>` edges.
68
+ * When a single event is raised more than this many times in one file,
69
+ * the event node is flagged `metadata.highFanout: true` and ALL
70
+ * `raises-event` edges to it are dropped. Pass `undefined` to disable
71
+ * the gate (the legacy, uncapped behaviour). The 50 default lives in
72
+ * the orchestrator: callers that don't pass a value get the default.
73
+ */
74
+ private maxRaiseFanout;
75
+ constructor(filePath: string, source: string, vbaTargets?: Record<string, boolean>, maxRaiseFanout?: number);
8
76
  extract(): ExtractionResult;
77
+ /**
78
+ * Issue #156: emit the per-file timing block (mode `1` or `2`) and
79
+ * — in mode `2` — append the running aggregate. No-op when
80
+ * `mode === 'off'`. In aggregate mode we flush after every extract()
81
+ * call so the line is visible per-file even without a graceful
82
+ * shutdown (crash-on-file-3 still shows files 1-3 in the aggregate).
83
+ */
84
+ private maybeEmitTimings;
9
85
  private result;
10
86
  private isFormOrReportFile;
11
87
  private createFileNode;
@@ -96,7 +96,17 @@ export declare function stripVbaComments(src: string): string;
96
96
  * downstream extraction keeps source-line parity. Unsupported/unsafe
97
97
  * expressions evaluate to false rather than throwing.
98
98
  */
99
- export declare function preprocessConditionalCompilation(src: string, customTargets?: Record<string, boolean>): string;
99
+ export declare function preprocessConditionalCompilation(src: string, customTargets?: Record<string, boolean>,
100
+ /**
101
+ * Issue #156: optional timing sink for the internal lexer+parser.
102
+ * When provided, the wall-clock spent inside `evaluateConditionalExpression`
103
+ * and `evaluateConstRhs` is accumulated under the key
104
+ * `preprocess.cc.lexer+parser` so the orchestrator can break the
105
+ * outer `preprocess.conditionalCompilation` stage into its outer +
106
+ * inner sub-totals. Undefined → no-op, zero cost (just the
107
+ * `if (timings)` null-check on every CC line).
108
+ */
109
+ timings?: Map<string, number> | null): string;
100
110
  export interface StringLiteralSpan {
101
111
  /** The literal content (no surrounding quotes; `""` collapsed to `"`). */
102
112
  text: string;
@@ -0,0 +1,38 @@
1
+ import { VbaExtractorContext, VbaClassifier } from './vba/context';
2
+ export type TimingMode = 'off' | 'per-file' | 'aggregate';
3
+ /**
4
+ * Read the timing mode from the env var. Reads are cached per call —
5
+ * the env var is consulted on every `extract()` but the result is just
6
+ * a number/string compare.
7
+ */
8
+ export declare function readTimingMode(): TimingMode;
9
+ /**
10
+ * Run `fn`, time the wall-clock, and record into `ctx.timings` under
11
+ * `stageName`. Returns the value `fn` returned. No-op when timings
12
+ * are disabled (the Map is null) — except the `performance.now()` pair,
13
+ * which is the irreducible cost of measuring anything.
14
+ *
15
+ * `performance.now()` is the right primitive here (monotonic, sub-ms
16
+ * resolution on every supported Node version). `Date.now()` would round
17
+ * to whole milliseconds and lose the per-stage signal on small files.
18
+ */
19
+ export declare function withStage<T>(ctx: VbaExtractorContext, stageName: string, fn: () => T): T;
20
+ /**
21
+ * Time a single `VbaClassifier.classifyLine` invocation. The accumulated
22
+ * time is reported under the bucket `classifier.<name>` so the
23
+ * aggregate stage totals match the per-classifier view.
24
+ */
25
+ export declare function withClassifier(ctx: VbaExtractorContext, classifier: VbaClassifier, line: string, index: number): void;
26
+ /**
27
+ * Per-file stderr block. Cheap string-build; the per-extract call cost
28
+ * is bounded by the small number of stages (~10).
29
+ */
30
+ export declare function emitPerFileTimings(ctx: VbaExtractorContext, filePath: string): void;
31
+ export declare function recordAggregateTimings(ctx: VbaExtractorContext, _filePath: string): void;
32
+ export declare function flushAggregate(): void;
33
+ /**
34
+ * Test-only: clear the aggregate between test cases so they do not
35
+ * pollute each other. Not exported in the public API.
36
+ */
37
+ export declare function _resetAggregateForTests(): void;
38
+ //# sourceMappingURL=vba-timing.d.ts.map
@@ -25,6 +25,27 @@ export interface ProjectConfig {
25
25
  exclude?: string[];
26
26
  vba?: {
27
27
  targets?: Record<string, boolean>;
28
+ /**
29
+ * Per-file fanout cap for `RaiseEvent <EventName>` edges (#152). When
30
+ * a single event (by name) is raised from more than this many sites
31
+ * in one file, the event node is flagged `metadata.highFanout: true`
32
+ * and ALL `raises-event` edges to it are dropped — the gate suppresses
33
+ * graph noise from events with generic names (`Change`, `Click`,
34
+ * `AfterUpdate`, …) that would otherwise produce hundreds of edges per
35
+ * file. Defaults to 50 when unset; the event node itself (declaration
36
+ * site, handler linkage via `subscribes-event`) is preserved.
37
+ */
38
+ maxRaiseFanout?: number;
39
+ /**
40
+ * Run the Dysflow-specific extractors (form/report SaveAsText, test
41
+ * manifests, test sequences) on top of the base VBA extractor. Set to
42
+ * `false` to opt out — useful for projects that carry legacy
43
+ * `.form.txt`/`.report.txt` files (or test-manifest JSON from a
44
+ * different system) and want them tracked as just a `file` node
45
+ * instead of being expanded into the graph (issue #154). Defaults to
46
+ * `true`.
47
+ */
48
+ dysflowExport?: boolean;
28
49
  };
29
50
  /**
30
51
  * Gitignore-style patterns for first-party source to force INTO the index even
@@ -43,7 +64,17 @@ export interface ProjectConfig {
43
64
  }
44
65
  export declare function loadVbaConfig(rootDir: string): {
45
66
  targets?: Record<string, boolean>;
67
+ maxRaiseFanout?: number;
68
+ dysflowExport?: boolean;
46
69
  };
70
+ /**
71
+ * Load the validated `vba.dysflowExport` flag for a project, mtime-cached
72
+ * (issue #154). Returns `true` when the flag is absent — the default keeps
73
+ * the legacy Dysflow-specific extractors running so existing users see no
74
+ * behavioral change. `false` opts out, so `.form.txt`/`.report.txt`/test
75
+ * manifest/sequence files are tracked as just a `file` node.
76
+ */
77
+ export declare function loadDysflowExportConfig(rootDir: string): boolean;
47
78
  /**
48
79
  * Load the validated extension overrides for a project, mtime-cached.
49
80
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aroman22/codegraph-vba",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "Local-first code intelligence for AI agents (MCP). Self-contained — bundles its own runtime.",
5
5
  "bin": {
6
6
  "codegraph-vba": "npm-shim.js"
@@ -15,12 +15,12 @@
15
15
  "./package.json": "./package.json"
16
16
  },
17
17
  "optionalDependencies": {
18
- "@aroman22/codegraph-vba-darwin-arm64": "1.10.0",
19
- "@aroman22/codegraph-vba-darwin-x64": "1.10.0",
20
- "@aroman22/codegraph-vba-linux-arm64": "1.10.0",
21
- "@aroman22/codegraph-vba-linux-x64": "1.10.0",
22
- "@aroman22/codegraph-vba-win32-arm64": "1.10.0",
23
- "@aroman22/codegraph-vba-win32-x64": "1.10.0"
18
+ "@aroman22/codegraph-vba-darwin-arm64": "1.11.0",
19
+ "@aroman22/codegraph-vba-darwin-x64": "1.11.0",
20
+ "@aroman22/codegraph-vba-linux-arm64": "1.11.0",
21
+ "@aroman22/codegraph-vba-linux-x64": "1.11.0",
22
+ "@aroman22/codegraph-vba-win32-arm64": "1.11.0",
23
+ "@aroman22/codegraph-vba-win32-x64": "1.11.0"
24
24
  },
25
25
  "files": [
26
26
  "npm-shim.js",