Problem
VbaExtractor (after Issue #83) is a "thin orchestrator" that owns 5 create*Classifier() factory functions in src/extraction/vba/:
createEventsTypesDeclaresClassifier() → declarations.ts
createImplementsClassifier() → implements.ts
createDimsClassifier() → dims.ts
createEnumsConstsClassifier() → enums-consts.ts
createCallsAndSqlClassifier(lines) → call-sweep.ts
Each is a function that returns a VbaClassifier object (defined in src/extraction/vba/context.ts) with a classifyLine(line, index, ctx) method. The actual rule definitions (regexes, gating conditions, emit logic) are scattered as private functions and consts inside the respective files.
Adding a new rule (e.g., for a new VBA construct) currently requires: edit a *Classifier file, add a regex constant, add a check inside classifyLine, add a count++ somewhere. The discipline is implicit — there's no central registry of "what does this extractor actually look for".
The JSDoc in src/extraction/vba-extractor.ts:21-31 even hints at the direction:
"This class is now a THIN ORCHESTRATOR that owns a SINGLE per-line walk. Preprocessed source is split into a readonly string[] ONCE…"
…and a 2026-06-28 design doc in openspec/changes/archive/2026-06-28-vba-extractor/design.md:79-87 lists 9 rules (R1-R9) in a clean table format. That table never made it into code as a structure — it stayed as documentation.
Goal
Promote the implicit rules table to an explicit one. Each rule lives as a typed object with a stable id, pattern(s), and an emit callback. The orchestrator drives the table; the rule bodies are testable in isolation.
Why now
- The codebase is at a clean state (FORMS-1..7 done, god-file split done, single-walk done). The next refactor is the rule table.
- The existing 5 classifiers stay; this is a structural change that fits inside them.
- Adding a new rule becomes: write one rule object, drop it in the table. No more "which classifier should I edit?".
- The compliance bar for new VBA features (e.g.,
With block member calls, Set x = New, RaiseEvent, etc.) is much lower.
Approach
Define VbaExtractionRule<T> in a new src/extraction/vba/rules.ts:
interface VbaExtractionRule<T = unknown> {
readonly id: string; // 'procedure', 'dim', 'implements', 'sql-in-strings', ...
readonly description: string;
readonly pattern: RegExp | RegExp[];
readonly requires?: 'class' | 'module' | 'inside-procedure' | string;
readonly scan?: 'masked' | 'unmasked' | 'both'; // masked = callScanLine, unmasked = original
readonly emit: (match: RegExpMatchArray, ctx: VbaExtractorContext, line: string, lineNum: number) => T | null;
readonly count?: (result: T) => number; // how many symbols this rule emitted (for hasAnySymbols)
}
Then src/extraction/vba/procedures.ts (and the other 4 files) export a RULES: VbaExtractionRule[] constant. The orchestrator iterates the table, dispatches each line to each rule, and the rule's emit returns the node/edge/unresolved-ref to be appended.
Compatibility shim: the existing VbaClassifier interface stays (it's used by createCallsAndSqlClassifier which carries inter-line state — procedure stack, with stack, sqlVariables). The 5 classifier files become "rule + inter-line state container" — the rules they own are exposed as a RULES array, and the classifier's classifyLine orchestrates the in-rule dispatches.
Acceptance criteria
Out of scope
Related
Problem
VbaExtractor(after Issue #83) is a "thin orchestrator" that owns 5create*Classifier()factory functions insrc/extraction/vba/:createEventsTypesDeclaresClassifier()→declarations.tscreateImplementsClassifier()→implements.tscreateDimsClassifier()→dims.tscreateEnumsConstsClassifier()→enums-consts.tscreateCallsAndSqlClassifier(lines)→call-sweep.tsEach is a function that returns a
VbaClassifierobject (defined insrc/extraction/vba/context.ts) with aclassifyLine(line, index, ctx)method. The actual rule definitions (regexes, gating conditions, emit logic) are scattered as private functions andconsts inside the respective files.Adding a new rule (e.g., for a new VBA construct) currently requires: edit a
*Classifierfile, add a regex constant, add a check insideclassifyLine, add acount++somewhere. The discipline is implicit — there's no central registry of "what does this extractor actually look for".The JSDoc in
src/extraction/vba-extractor.ts:21-31even hints at the direction:…and a 2026-06-28 design doc in
openspec/changes/archive/2026-06-28-vba-extractor/design.md:79-87lists 9 rules (R1-R9) in a clean table format. That table never made it into code as a structure — it stayed as documentation.Goal
Promote the implicit rules table to an explicit one. Each rule lives as a typed object with a stable id, pattern(s), and an emit callback. The orchestrator drives the table; the rule bodies are testable in isolation.
Why now
Withblock member calls,Set x = New,RaiseEvent, etc.) is much lower.Approach
Define
VbaExtractionRule<T>in a newsrc/extraction/vba/rules.ts:Then
src/extraction/vba/procedures.ts(and the other 4 files) export aRULES: VbaExtractionRule[]constant. The orchestrator iterates the table, dispatches each line to each rule, and the rule'semitreturns the node/edge/unresolved-ref to be appended.Compatibility shim: the existing
VbaClassifierinterface stays (it's used bycreateCallsAndSqlClassifierwhich carries inter-line state — procedure stack, with stack, sqlVariables). The 5 classifier files become "rule + inter-line state container" — the rules they own are exposed as aRULESarray, and the classifier'sclassifyLineorchestrates the in-rule dispatches.Acceptance criteria
src/extraction/vba/rules.tswith theVbaExtractionRuleinterfaceRULES: VbaExtractionRule[]constant (rules extracted from inline code)vba-extractor.ts) imports each classifier'sRULESand validates the table is non-emptyemitWithNewKindreturning apropertynode forProperty Set Foo) requires touching 1 file: the new rule's*.tsand a 1-line addition to the classifier'sRULESarray__tests__/extraction-vba-rule-table.test.tsvalidates the table shape (every rule has a unique id, a non-empty pattern, anemitfunction)Out of scope
VbaClassifierinterface entirely (the inter-line state machines — procedure stack, with stack, sqlVariables — need a class, not a pure rule). The shim is a deliberate bridge.Related
openspec/changes/archive/2026-06-28-vba-extractor/design.md:79-87(R1-R9)LanguageExtractorshape used by tree-sitter-based languages (src/extraction/languages/typescript.tsis the canonical example: a typed config object with hooks)