Skip to content

Commit d653251

Browse files
committed
docs(mcp): document saved queries, test sequences and the ERD surface
server-instructions.ts is the single source of truth for agent-facing guidance (#529), and it described .bas/.cls modules, form and report layouts, event wiring and test manifests — but none of the saved-query, test-sequence or Access ERD extractors the fork already ships. An agent that is never told `query` nodes exist greps the .sql instead, which is the read-fallback this project's retrieval work exists to prevent. Add the three surfaces in the shape the existing entries use: the file shape that produces them, the node kinds and edges emitted, and the provenance tag to filter on. The new suite asserts nothing about wording. It runs the real extractors, collects the node kinds and synthesizedBy tags they emit, and requires the instructions to name each one, so renaming a tag in an extractor fails the build until the guidance follows. The ERD case uses the checked-in fixture, which carries both a plain and a LINKED table, so both tags are exercised rather than assumed. Refs #320
1 parent 66f219b commit d653251

3 files changed

Lines changed: 146 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1515
- Dysflow exports are no longer read twice. Every form and report layout, test manifest and test sequence was being parsed once by the VBA dispatcher and again by the Dysflow framework hook, which made a full index do double the work on those files and left duplicate pending references behind. Turning Dysflow expansion off with `vba.dysflowExport: false` is also honoured again in every case. (#314)
1616
- A test manifest or test sequence saved with a UTF-8 byte-order mark is read correctly again. Until now such a file was rejected outright, so every test it registered quietly vanished from the graph with nothing but a warning to show for it — and on Windows that mark is what PowerShell writes by default. (#316)
1717
- Saved Access queries now appear in the graph no matter what order Dysflow writes them. The `queries.json` manifest is what marks the `.sql` files beside it as saved queries, and a manifest arriving after its queries left them unindexed until the next manual sync. Editing only the manifest — renaming a query, adding an entry — now re-indexes too. (#318)
18+
- Agents are now told about three Access surfaces the index already carried but never advertised: saved queries and the tables they touch, Dysflow test sequences alongside the manifests, and the table-and-column structure from the Access ERD export. An agent that was not told these exist fell back to reading the files by hand. (#320)
1819

1920
## [1.17.1] - 2026-09-12
2021

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* The MCP `initialize` instructions must describe the VBA/Access surfaces the
3+
* fork actually indexes (issue #320).
4+
*
5+
* `src/mcp/server-instructions.ts` is the single source of truth for
6+
* agent-facing guidance (#529) — it is the first and only thing an agent is
7+
* told about what lives in this index. Prose there is cheap to write and easy
8+
* to let rot, so these tests do NOT assert on wording. They run the real
9+
* extractors, collect the node kinds and `synthesizedBy` tags those extractors
10+
* emit, and require the instructions to name each one. Rename a tag in an
11+
* extractor and this suite fails until the guidance follows.
12+
*/
13+
import { describe, it, expect } from 'vitest';
14+
import * as fs from 'node:fs';
15+
import * as path from 'node:path';
16+
import { SERVER_INSTRUCTIONS } from '../src/mcp/server-instructions';
17+
import { SqlQueryExtractor } from '../src/extraction/sql-query-extractor';
18+
import { VbaTestSequenceExtractor } from '../src/extraction/vba-test-sequence-extractor';
19+
import { VbaTestManifestExtractor } from '../src/extraction/vba-test-manifest-extractor';
20+
import {
21+
AccessErdExtractor,
22+
ERD_TABLE_SYNTHESIZED_BY,
23+
ERD_LINKED_TABLE_SYNTHESIZED_BY,
24+
} from '../src/extraction/access-erd-extractor';
25+
import { EXTERNAL_BACKEND_SYNTHESIZED_BY } from '../src/extraction/sql-table-scan';
26+
27+
/** Every `synthesizedBy` tag on the nodes, edges and refs of a result. */
28+
function tagsOf(result: {
29+
nodes: Array<{ metadata?: Record<string, unknown> }>;
30+
edges: Array<{ metadata?: Record<string, unknown> }>;
31+
unresolvedReferences: Array<{ metadata?: Record<string, unknown> }>;
32+
}): string[] {
33+
const out = new Set<string>();
34+
for (const item of [...result.nodes, ...result.edges, ...result.unresolvedReferences]) {
35+
const tag = item.metadata?.synthesizedBy;
36+
if (typeof tag === 'string') out.add(tag);
37+
}
38+
return [...out];
39+
}
40+
41+
describe('MCP server instructions — VBA/Access coverage (#320)', () => {
42+
it('names every tag and node kind a saved Access query emits', () => {
43+
const r = new SqlQueryExtractor(
44+
'queries/Q_Ventas.sql',
45+
'SELECT * FROM TbVentas INNER JOIN TbClientes ON 1 = 1;\n',
46+
).extract();
47+
48+
// Sanity: the fixture really does produce a query node with table refs, so
49+
// an extractor that silently stopped emitting them cannot pass this test.
50+
expect(r.nodes.some((n) => n.kind === 'query')).toBe(true);
51+
const tags = tagsOf(r);
52+
expect(tags).toContain('sql-query-table');
53+
54+
for (const tag of tags) {
55+
expect(SERVER_INSTRUCTIONS, `undocumented tag: ${tag}`).toContain(tag);
56+
}
57+
expect(SERVER_INSTRUCTIONS).toContain('query');
58+
expect(SERVER_INSTRUCTIONS).toContain('queries.json');
59+
expect(SERVER_INSTRUCTIONS).toContain(EXTERNAL_BACKEND_SYNTHESIZED_BY);
60+
});
61+
62+
it('names the test-sequence surface, not just the manifests', () => {
63+
const r = new VbaTestSequenceExtractor(
64+
'tests/sequences/cache.json',
65+
JSON.stringify({
66+
runnerPolicy: { tool: 'dysflow', sequential: true },
67+
procedures: ['Test_Cache_RunSlice'],
68+
}),
69+
).extract();
70+
71+
const tags = tagsOf(r);
72+
expect(tags).toContain('vba-test-sequence');
73+
for (const tag of tags) {
74+
expect(SERVER_INSTRUCTIONS, `undocumented tag: ${tag}`).toContain(tag);
75+
}
76+
77+
// The metadata an agent has to know exists to act on a sequence.
78+
const ref = r.unresolvedReferences[0];
79+
for (const key of Object.keys(ref?.metadata ?? {})) {
80+
if (key === 'synthesizedBy') continue;
81+
expect(SERVER_INSTRUCTIONS, `undocumented metadata key: ${key}`).toContain(key);
82+
}
83+
});
84+
85+
it('still names the manifest surface it already documented', () => {
86+
// Guard against a future edit trimming the paragraph the sequences one
87+
// now sits beside.
88+
const r = new VbaTestManifestExtractor(
89+
'tests/tests.vba.smoke.json',
90+
JSON.stringify({ tests: [{ procedure: 'Test_X_RunAll' }] }),
91+
).extract();
92+
for (const tag of tagsOf(r)) {
93+
expect(SERVER_INSTRUCTIONS, `undocumented tag: ${tag}`).toContain(tag);
94+
}
95+
});
96+
97+
it('names every tag the Access ERD export emits', () => {
98+
const fixture = path.join(
99+
__dirname,
100+
'fixtures',
101+
'access-erd',
102+
'ERD',
103+
'Estructura_Datos.md',
104+
);
105+
const r = new AccessErdExtractor(
106+
'ERD/Estructura_Datos.md',
107+
fs.readFileSync(fixture, 'utf-8'),
108+
).extract();
109+
110+
// The fixture carries both a plain and a LINKED table, so both tags are
111+
// exercised rather than assumed.
112+
const tags = tagsOf(r);
113+
expect(tags).toContain(ERD_TABLE_SYNTHESIZED_BY);
114+
expect(tags).toContain(ERD_LINKED_TABLE_SYNTHESIZED_BY);
115+
116+
for (const tag of tags) {
117+
expect(SERVER_INSTRUCTIONS, `undocumented tag: ${tag}`).toContain(tag);
118+
}
119+
// The node kinds an agent would have to ask for by name.
120+
expect(SERVER_INSTRUCTIONS).toContain('type_member');
121+
expect(SERVER_INSTRUCTIONS).toContain('ERD/');
122+
});
123+
});

‎src/mcp/server-instructions.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,28 @@ format):
114114
production symbol reaches its covering test atoms with the manifest and tags
115115
to run.
116116
Pass \`projectPath\` to a codegraph index that includes VBA files.
117+
- **Saved Access queries are first-class nodes — do not grep the \`.sql\`.** A
118+
Dysflow-exported \`queries/<Name>.sql\` whose directory also carries a
119+
\`queries.json\` manifest becomes a \`query\` node (language \`sql\`, named after
120+
the file) with a \`references\` edge to every table its \`FROM\` / \`JOIN\` /
121+
\`INTO\` / \`UPDATE\` names, tagged \`sql-query-table\`; a table that resolves to
122+
another backend file is tagged \`vba-external-backend\` instead. A \`.sql\` with
123+
no sibling \`queries.json\` is deliberately NOT indexed, so an ordinary SQL
124+
migration in a non-Access repo stays out of the graph.
125+
- **Dysflow test sequences sit beside the manifests.** A \`sequences/*.json\`
126+
emits a \`file\` node and one \`references\` edge per \`procedures[]\` entry to
127+
that \`Test_*\` procedure, tagged \`vba-test-sequence\` and carrying
128+
\`runnerPolicy\`, \`sequenceFile\` and \`procedureIndex\`. So \`getCallers\` of a
129+
test atom reaches both the manifest that registers it (\`vba-test-manifest\`)
130+
and the sequence that orders it, with the runner policy to honour.
131+
- **Table structure comes from the Access ERD export.** An \`ERD/*.md\` produced
132+
by the Access structure generator emits one \`class\` node per table (tagged
133+
\`access-erd-table\`), one \`type_member\` per field joined by \`contains\`, and a
134+
\`references\` edge from a linked table to the backend it really lives in
135+
(tagged \`vba-linked-table\`). This is the cheapest answer to "what columns
136+
does this table have" and to "which backend owns it" — ask codegraph before
137+
reading the export. The extractor requires the generated header, so a
138+
hand-written diagram in an \`ERD/\` folder contributes nothing.
117139
- **VBA unresolved refs carry syntactic shape** (v1.7+). \`unresolved_refs.reference_kind\` is no longer the literal string \`"references"\` — it reports what the syntactic shape actually was. Values: \`call\` (paren-form or statement-form call site), \`qualified-call\` (\`obj.Foo(...)\` with runtime receiver), \`property-get\` / \`property-set\` (\`Me.Name\`, \`obj.Prop = value\`), \`bang-get\` / \`bang-set\` (\`Me!SubCtl\`, \`obj!Field = value\`), \`unqualified-ident\` (bare identifier like \`HayErrorEnRiesgo\` in an \`If\` condition), \`member-with\` (\`.Member\` inside a \`With\` block), \`dao-query\` (\`DoCmd.OpenQuery "X"\` argument). The legacy value \`references\` is retained on any path the round did not reclassify, so older SQL filters that key on it keep working. To find real missing callees, filter \`WHERE reference_kind IN ('call','qualified-call','unqualified-ident','member-with','bang-get')\` — that set has <10% false positives (DAO-field accesses, form-property reads, and bang refs no longer pollute the bucket).
118140
- **Post-extraction stub resolver** (v1.7+). Edges with \`metadata.synthesizedBy='vba-name-resolution'\` start life pointing at a synthetic function node; the resolver at \`src/resolution/index.ts:resolveVbaCallStubs\` (invoked from \`indexAll\` and \`sync\`) walks them and repoints each \`target\` to the real \`nodes.id\` when one exists. Runtime-object calls (\`DAO.*\`, \`fso.*\`, \`ListBox.*\`, \`Collection.*\`, \`err.*\`, \`VBA.*\`, \`Application.*\`, \`Screen.*\`, \`DoCmd.*\`, \`CurrentDb.*\`, \`Forms\`, \`Reports\`, \`Debug\`, \`Modules\`, \`References\`, \`CommandBars\`, \`SysCmd\`, \`CreateObject\`, \`GetObject\`, \`Fields\`) are explicitly declined — they remain \`stub:true\` because they can never link to user code. Shadow user classes (e.g. a user class actually named \`DAO\` with an \`Execute\` method) are preserved and linked normally. Every stub edge carries \`metadata.repointDecision\` with one of \`reponted-to-real\` (linked to a real \`nodes.id\`), \`declined-runtime\` (runtime object — never user code, filter OUT), \`declined-ambiguous\` (multiple real candidates — investigate), or \`declined-not-found\` (genuinely missing callee — this is the actionable signal). Consumers detecting "missing callees" MUST filter on \`repointDecision='declined-not-found'\`, NOT on the raw \`stub=true\` count — the raw count is dominated by runtime-object noise. See \`docs/vba-stub-repoint-decision.md\` for the full contract.
119141
`;

0 commit comments

Comments
 (0)