Skip to content

Commit f6fa294

Browse files
committed
feat(vba): model roadmap declaration nodes
1 parent 4a9a422 commit f6fa294

18 files changed

Lines changed: 511 additions & 103 deletions

‎.github/workflows/ci.yml‎

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,25 +29,17 @@ jobs:
2929
strategy:
3030
fail-fast: false
3131
matrix:
32-
# Match the engines field: >=20.0.0 <25.0.0. 22 is the active LTS,
33-
# 20 is the previous LTS we still promise. We don't add 24 here
34-
# because Node 24 changed sqlite bindings and we want the matrix
35-
# to reflect supported configurations, not chase the latest.
36-
node-version: [20, 22]
32+
# Match the engines field: >=22.5.0 <25.0.0. CodeGraph uses the
33+
# built-in node:sqlite API, which is only available from Node 22.5+.
34+
node-version: [22]
3735
# Linux is the gating runner — every PR must pass it. Windows is
3836
# advisory: certain tests have pre-existing Windows-only EPERM
3937
# failures documented in CLAUDE.md, so we don't block PRs on it.
4038
# continue-on-error handles the inverse: a Linux failure still
4139
# fails the PR, but a Windows-only failure shows up as a yellow
4240
# check rather than red.
4341
os: [ubuntu-latest, windows-latest]
44-
exclude:
45-
# Reduce CI minutes by running Windows only on the active LTS.
46-
# Windows on Node 20 would mostly test "did Windows break again"
47-
# without telling us anything Node 22 doesn't already.
48-
- os: windows-latest
49-
node-version: 20
50-
continue-on-error: ${{ matrix.os == 'windows-latest' || matrix.node-version == 20 }}
42+
continue-on-error: ${{ matrix.os == 'windows-latest' }}
5143
steps:
5244
- name: Checkout
5345
uses: actions/checkout@v4
@@ -74,4 +66,4 @@ jobs:
7466
run: pnpm test
7567

7668
- name: Run E2E regression (extract-vba-realfixtures)
77-
run: pnpm exec vitest run __tests__/extraction-vba-realfixtures.test.ts
69+
run: pnpm exec vitest run __tests__/extraction-vba-realfixtures.test.ts

‎CHANGELOG.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1010
## [Unreleased]
1111

1212

13+
## [1.3.5] - 2026-07-02
14+
15+
### New Features
16+
17+
- VBA event declarations, user-defined `Type` blocks, and Win32 `Declare` statements are now first-class graph nodes. `RaiseEvent` calls link back to their declared `event`, `WithEvents` declarations emit subscriber edges, `Type ... End Type` members are queryable, and API declarations now carry DLL / alias / PtrSafe metadata while remaining traceable from call sites.
18+
- Node metadata is now persisted in the SQLite schema so extractor annotations such as constant values, Declare DLL details, and Type member types survive full indexing and are visible through graph queries.
19+
20+
### Documentation
21+
22+
- The VBA / Access README section now documents the full node-kind surface for roadmap issues #25 and #26: enum, enum_member, constant, query, event, type, type_member, declare, form-layout, and form-instance-control.
23+
1324
## [1.3.4] - 2026-07-02
1425

1526
### New Features

‎README.md‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,13 +386,31 @@ The two are **sibling tools**: Dysflow owns the Access binary round-trip (sync,
386386
| **`Implements IFoo`** | `.cls` declares `Implements IFoo` | — | Emits an `implements` edge from the class to `IFoo` |
387387
| **`Dim x As Foo.Bar`** | `.bas`/`.cls` qualified type reference | — | `references` edge to `Foo` with `synthesizedBy: 'vba-name-resolution'`; silent when unresolvable |
388388
| **`WithEvents m_X As Form_Foo`** | `.cls` listener declaration | — | `references` edge to `Form_Foo` with `synthesizedBy: 'vba-withevents'` — closes the event-driven form flow |
389+
| **`Event Foo(...)` / `RaiseEvent Foo(...)`** | `.cls` custom event declaration + raise site | — | `event` node plus `raises-event` edges from the raising procedure; `WithEvents` also emits `subscribes-event` edges with the listener variable name |
390+
| **`Type T ... End Type`** | `.bas`/`.cls` user-defined type declaration | — | `type` node plus `type_member` child nodes linked by `type-member` edges and member type metadata |
391+
| **`Declare PtrSafe Function X Lib "dll"`** | `.bas`/`.cls` Win32/API declaration | — | `declare` node with DLL, alias, kind, and PtrSafe metadata; VBA call sites still emit `calls` edges to it |
392+
| **`Enum` / `Const` domain dictionaries** | `.bas`/`.cls` enum blocks and module constants | — | `enum`, `enum_member`, and `constant` nodes linked to their module/class; constant string values are preserved in metadata for local resolution |
393+
| **Saved Access QueryDefs** | `queries/<Name>.sql` | — | `query` node per Dysflow-exported `.sql` file with `references` edges to tables named by `FROM` / `JOIN` / `INTO` / `UPDATE` |
389394
| **`New Clase(...)`** | `.bas`/`.cls` instantiation | — | `references` edge with `synthesizedBy: 'vba-new-binding'` |
390395
| **SQL in VBA strings** | `.bas`/`.cls` SQL inside `DoCmd.RunSQL` / `CurrentDb.OpenRecordset` / `CurrentDb.Execute` / `db.Execute` | — | Table names extracted from `FROM`/`INTO`/`UPDATE <table>` → `references` edges with `synthesizedBy: 'vba-sql-table'` |
391396

392397
**Hard invariants** enforced by the extractor and verified by tests:
393398

394399
- **`.cls` is the canonical source for form code.** `.form.txt` emits **zero** `function` / `sub` / `class` nodes — only the form-level `module` node and `property` nodes per control. Dysflow overwrites `.form.txt`'s embedded code on the next import, so emitting code from there would be both wrong and ephemeral.
395-
- **A `.bas` with only `Public Const` declarations** (no Subs, Functions, Properties, Implements, or Dim) emits the `file` node only — no `module` / `class` per REQ-CODE-10.
400+
- **Option-only files stay silent.** A `.bas` containing only `Option ...` directives emits zero symbol nodes; a `.bas` with only `Enum`, `Const`, `Event`, `Type`, or `Declare` declarations DOES emit its module node because those declarations are real graph symbols.
401+
402+
**VBA / Access node kinds added by the fork:**
403+
404+
| Node kind | Meaning |
405+
|---|---|
406+
| `enum` / `enum_member` | VBA `Enum` block and its members |
407+
| `constant` | VBA `Const` declaration; string values are kept in metadata when available |
408+
| `query` | Dysflow-exported saved Access query (`queries/<Name>.sql`) |
409+
| `event` | VBA custom `Event` declaration |
410+
| `type` / `type_member` | VBA user-defined `Type ... End Type` and each declared member |
411+
| `declare` | Win32/API `Declare` / `Declare PtrSafe` statement |
412+
| `form-layout` | `.form.txt` / `.report.txt` form/report container |
413+
| `form-instance-control` | Access control instance from form/report UI text |
396414

397415
**Scope:** Dysflow-managed projects only (Dysflow's `.form.txt` / `.report.txt` SaveAsText format). Legacy `.frm` / `.dsr` Access binary formats are not in scope.
398416

‎__tests__/db-vba-call-stub-queries.test.ts‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@
55
* load-bearing e2e coverage lives in `extraction-vba-realfixtures.test.ts`
66
* per the Windows CI VBA regression subset requirement.
77
*
8-
* `nodes` has no `metadata` column (only `edges` does — see schema.sql), so
98
* `getVbaCallStubs()` finds candidate stub nodes via a JOIN against
10-
* `edges.metadata` (which DOES persist) rather than a `nodes.metadata` LIKE
11-
* prefilter — see the design-deviation note in
12-
* `src/db/queries.ts::getVbaCallStubs`.
9+
* `edges.metadata` rather than `nodes.metadata`: the stub flag is a
10+
* relationship fact about an unresolved call edge, not an intrinsic property
11+
* of the target symbol.
1312
*/
1413
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
1514
import * as fs from 'fs';
@@ -124,6 +123,28 @@ describe('VBA call-stub DB primitives', () => {
124123
});
125124
});
126125

126+
describe('node metadata persistence', () => {
127+
it('round-trips node metadata through the nodes table', () => {
128+
q.insertNode({
129+
...makeVbaFunctionNode('decl', 'GetTickCount'),
130+
kind: 'declare',
131+
metadata: {
132+
dll: 'kernel32',
133+
declareKind: 'function',
134+
ptrSafe: true,
135+
},
136+
});
137+
138+
const node = q.getNodeById('decl');
139+
140+
expect(node?.metadata).toEqual({
141+
dll: 'kernel32',
142+
declareKind: 'function',
143+
ptrSafe: true,
144+
});
145+
});
146+
});
147+
127148
describe('repointEdgeTarget()', () => {
128149
it('updates target + metadata in place, leaves other columns untouched', () => {
129150
q.insertNodes([
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { VbaExtractor } from '../src/extraction/vba-extractor';
3+
4+
function extract(filePath: string, source: string) {
5+
return new VbaExtractor(filePath, source).extract();
6+
}
7+
8+
describe('VbaExtractor — roadmap #26 event declarations', () => {
9+
it('models Event declarations and RaiseEvent calls as first-class graph nodes and edges', () => {
10+
const src = [
11+
'Attribute VB_Name = "PedidoPublisher"',
12+
'Public Event PedidoGuardado(ByVal IdPedido As Long)',
13+
'',
14+
'Public Sub Guardar()',
15+
' RaiseEvent PedidoGuardado(42)',
16+
'End Sub',
17+
].join('\n');
18+
19+
const r = extract('src/classes/PedidoPublisher.cls', src);
20+
const event = r.nodes.find((n) => n.kind === 'event' && n.name === 'PedidoGuardado');
21+
const caller = r.nodes.find((n) => n.kind === 'function' && n.name === 'Guardar');
22+
23+
expect(event).toBeDefined();
24+
expect(event?.qualifiedName).toBe('PedidoPublisher.PedidoGuardado');
25+
expect(event?.visibility).toBe('public');
26+
expect(caller).toBeDefined();
27+
expect(r.edges).toContainEqual(
28+
expect.objectContaining({
29+
source: caller?.id,
30+
target: event?.id,
31+
kind: 'raises-event',
32+
metadata: expect.objectContaining({ eventName: 'PedidoGuardado' }),
33+
}),
34+
);
35+
});
36+
37+
it('models WithEvents declarations as subscriber edges with the variable name attached', () => {
38+
const r = extract(
39+
'src/classes/FormListener.cls',
40+
'Private WithEvents m_Form As Form_Pedido',
41+
);
42+
43+
const target = r.nodes.find((n) => n.name === 'Form_Pedido');
44+
expect(target).toBeDefined();
45+
expect(r.edges).toContainEqual(
46+
expect.objectContaining({
47+
target: target?.id,
48+
kind: 'subscribes-event',
49+
metadata: expect.objectContaining({
50+
synthesizedBy: 'vba-withevents',
51+
variableName: 'm_Form',
52+
}),
53+
}),
54+
);
55+
});
56+
});
57+
58+
describe('VbaExtractor — roadmap #26 Type declarations', () => {
59+
it('models Type declarations with type_member nodes and type-member edges', () => {
60+
const src = [
61+
'Attribute VB_Name = "Tipos"',
62+
'Private Type TPedido',
63+
' Id As Long',
64+
' Nombre As String',
65+
'End Type',
66+
].join('\n');
67+
68+
const r = extract('src/modules/Tipos.bas', src);
69+
const type = r.nodes.find((n) => n.kind === 'type' && n.name === 'TPedido');
70+
const members = r.nodes.filter((n) => n.kind === 'type_member');
71+
72+
expect(type).toBeDefined();
73+
expect(type?.qualifiedName).toBe('TPedido');
74+
expect(type?.visibility).toBe('private');
75+
expect(members.map((n) => n.name).sort()).toEqual(['Id', 'Nombre']);
76+
expect(members.find((n) => n.name === 'Id')?.metadata?.memberType).toBe('Long');
77+
expect(members.find((n) => n.name === 'Nombre')?.metadata?.memberType).toBe('String');
78+
for (const member of members) {
79+
expect(r.edges).toContainEqual(
80+
expect.objectContaining({
81+
source: type?.id,
82+
target: member.id,
83+
kind: 'type-member',
84+
}),
85+
);
86+
}
87+
});
88+
});
89+
90+
describe('VbaExtractor — roadmap #26 Declare statements', () => {
91+
it('models Win32 Declare statements as declare nodes and keeps calls traceable', () => {
92+
const src = [
93+
'Attribute VB_Name = "WinApi"',
94+
'Public Declare PtrSafe Function GetTickCount Lib "kernel32" () As Long',
95+
'',
96+
'Public Sub UseApi()',
97+
' GetTickCount',
98+
'End Sub',
99+
].join('\n');
100+
101+
const r = extract('src/modules/WinApi.bas', src);
102+
const declaration = r.nodes.find((n) => n.kind === 'declare' && n.name === 'GetTickCount');
103+
const caller = r.nodes.find((n) => n.kind === 'function' && n.name === 'UseApi');
104+
105+
expect(declaration).toBeDefined();
106+
expect(declaration?.metadata).toEqual(
107+
expect.objectContaining({
108+
dll: 'kernel32',
109+
declareKind: 'function',
110+
ptrSafe: true,
111+
}),
112+
);
113+
expect(r.edges).toContainEqual(
114+
expect.objectContaining({
115+
source: caller?.id,
116+
target: declaration?.id,
117+
kind: 'calls',
118+
}),
119+
);
120+
});
121+
});

‎__tests__/extraction-vba.test.ts‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1756,8 +1756,8 @@ describe('VbaExtractor — SQL variable accumulation across self-referential con
17561756
});
17571757

17581758

1759-
describe('VbaExtractor — API declarations and VBA conditional compilation', () => {
1760-
it('extracts Public Declare PtrSafe Sub as a single-line function node with metadata', () => {
1759+
describe('VbaExtractor � API declarations and VBA conditional compilation', () => {
1760+
it('extracts Public Declare PtrSafe Sub as a single-line declare node with metadata', () => {
17611761
const src = [
17621762
'Option Explicit',
17631763
'Public Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)',
@@ -1767,12 +1767,16 @@ describe('VbaExtractor
17671767
].join('\n');
17681768

17691769
const r = extract('src/modules/modApi.bas', src);
1770-
const sleep = r.nodes.find((n) => n.kind === 'function' && n.name === 'Sleep');
1770+
const sleep = r.nodes.find((n) => n.kind === 'declare' && n.name === 'Sleep');
17711771
expect(sleep).toBeDefined();
17721772
expect(sleep?.visibility).toBe('public');
17731773
expect(sleep?.startLine).toBe(2);
17741774
expect(sleep?.endLine).toBe(2);
1775-
expect(sleep?.metadata?.isDeclare).toBe(true);
1775+
expect(sleep?.metadata).toEqual(expect.objectContaining({
1776+
dll: 'kernel32',
1777+
declareKind: 'sub',
1778+
ptrSafe: true,
1779+
}));
17761780

17771781
const caller = r.nodes.find((n) => n.kind === 'function' && n.name === 'UseSleep');
17781782
const call = r.edges.find((e) => e.kind === 'calls' && e.source === caller?.id && e.target === sleep?.id);
@@ -1789,14 +1793,14 @@ describe('VbaExtractor
17891793
].join('\n');
17901794

17911795
const r = extract('src/modules/modApi.bas', src);
1792-
const declarations = r.nodes.filter((n) => n.kind === 'function' && n.name === 'GetTickCount');
1796+
const declarations = r.nodes.filter((n) => n.kind === 'declare' && n.name === 'GetTickCount');
17931797
expect(declarations).toHaveLength(1);
17941798
expect(declarations[0]?.startLine).toBe(2);
1795-
expect(declarations[0]?.metadata?.isDeclare).toBe(true);
1799+
expect(declarations[0]?.metadata?.ptrSafe).toBe(true);
17961800
});
17971801
});
17981802

1799-
describe('VbaExtractor — custom db variables and OpenForm constants', () => {
1803+
describe('VbaExtractor � custom db variables and OpenForm constants', () => {
18001804
it('extracts inline SQL executed through custom variables ending in db', () => {
18011805
const src = [
18021806
'Sub Q(p_db As Object)',

‎__tests__/foundation.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ describe('Database Connection', () => {
282282

283283
const version = db.getSchemaVersion();
284284
expect(version).not.toBeNull();
285-
expect(version?.version).toBe(5);
285+
expect(version?.version).toBe(6);
286286

287287
db.close();
288288
});

‎__tests__/node-version-check.test.ts‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@
77
*/
88

99
import { describe, it, expect } from 'vitest';
10-
import { buildNode25BlockBanner, buildNodeTooOldBanner, MIN_NODE_MAJOR } from '../src/bin/node-version-check';
10+
import {
11+
buildNode25BlockBanner,
12+
buildNodeTooOldBanner,
13+
MIN_NODE_MAJOR,
14+
MIN_NODE_MINOR,
15+
isBelowMinimumNodeVersion,
16+
} from '../src/bin/node-version-check';
1117

1218
describe('buildNode25BlockBanner', () => {
1319
it('embeds the reported Node version in the header', () => {
@@ -50,10 +56,17 @@ describe('buildNodeTooOldBanner', () => {
5056
});
5157

5258
it('states the supported floor matching MIN_NODE_MAJOR', () => {
53-
expect(MIN_NODE_MAJOR).toBe(20);
54-
expect(buildNodeTooOldBanner('18.0.0')).toContain(
55-
`requires Node.js ${MIN_NODE_MAJOR} or newer`
56-
);
59+
expect(MIN_NODE_MAJOR).toBe(22);
60+
expect(MIN_NODE_MINOR).toBe(5);
61+
expect(buildNodeTooOldBanner('20.0.0')).toContain('requires Node.js 22.5 or newer');
62+
expect(buildNodeTooOldBanner('22.4.0')).toContain('built-in node:sqlite');
63+
});
64+
65+
it('rejects Node 20 and Node 22 before node:sqlite became available', () => {
66+
expect(isBelowMinimumNodeVersion('20.19.0')).toBe(true);
67+
expect(isBelowMinimumNodeVersion('22.4.0')).toBe(true);
68+
expect(isBelowMinimumNodeVersion('22.5.0')).toBe(false);
69+
expect(isBelowMinimumNodeVersion('24.0.0')).toBe(false);
5770
});
5871

5972
it('points users to Node 22 LTS via nvm and Homebrew', () => {

‎__tests__/pr19-improvements.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ describe('Best-Candidate Resolution', () => {
299299
describe('Schema v2 Migration', () => {
300300
it.skipIf(!HAS_SQLITE)('should have correct current schema version', async () => {
301301
const { CURRENT_SCHEMA_VERSION } = await import('../src/db/migrations');
302-
expect(CURRENT_SCHEMA_VERSION).toBe(5);
302+
expect(CURRENT_SCHEMA_VERSION).toBe(6);
303303
});
304304

305305
it.skipIf(!HAS_SQLITE)('should have migration for version 2', async () => {

‎package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@aroman22/codegraph-vba",
3-
"version": "1.3.4",
3+
"version": "1.3.5",
44
"description": "Supercharge AI coding agents with semantic code intelligence — surgical context, fewer tool calls, faster answers. 100% local.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
@@ -51,7 +51,7 @@
5151
"vitest": "^2.1.9"
5252
},
5353
"engines": {
54-
"node": ">=20.0.0 <25.0.0"
54+
"node": ">=22.5.0 <25.0.0"
5555
},
5656
"packageManager": "pnpm@10.16.1"
5757
}

0 commit comments

Comments
 (0)