feat(resolution): bridge Vuex string dispatch/commit to actions and mutations

Completes the Vue store dispatch family (the Pinia bridge was 8ea3205). Vuex
dispatches by a runtime STRING key — `dispatch('user/login')` /
`commit('SET_TOKEN')` / `this.$store.dispatch('app/toggleDevice')` — with no
static edge to the handler.

vuexDispatchEdges (callback-synthesizer.ts): the last `/` segment of the key is
the action/mutation name, the preceding segment is the namespace (≈ the module
file). Resolve the name to a function node IN A STORE FILE — the ≥2-signal
store-file gate excludes a same-named `api/` helper (`getInfo`/`login` collide in
practice) — disambiguated by the immediate namespace segment appearing in the
path (handles deep nesting like `d2admin/user/set`), or the same file for a root
local `commit('M')` inside an action. The .vue component is a dispatcher fallback
for top-level setup calls. Surfaces in explore as `dynamic: vuex dispatch`.

Also extracts the canonical Vuex MODULE shape `export default { namespaced,
actions: {…}, mutations: {…} }` (tree-sitter.ts: extractStoreCollectionMethods
off the export_statement, store-file gated) — its object-literal methods were
otherwise never nodes, so d2-admin's actions couldn't be bridged.

Validated 100% precision on three repos — vue-element-admin (55 edges),
vue-admin-template (12), d2-admin (63): 0 non-store targets, 0 namespace
mismatches (54/54 namespaced edges route to the correct module despite 6
colliding `load` actions in d2-admin), 0 on Redux controls (basetool/uwave —
non-string `dispatch()` correctly ignored). Suite green (1613); new
__tests__/vuex-dispatch-synthesizer.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-06-20 20:49:06 -05:00
co-authored by Claude Opus 4.8
parent 8ea32059b6
commit 80a1044d3d
5 changed files with 229 additions and 3 deletions
+35
View File
@@ -1066,6 +1066,24 @@ export class TreeSitterExtractor {
const parentId = this.nodeStack[this.nodeStack.length - 1];
if (parentId) this.emitReExportRefs(node, parentId);
}
// Vuex MODULE default export — `export default { namespaced, actions: {…},
// mutations: {…} }` (the canonical Vuex module shape). Object-literal methods
// aren't otherwise extracted, so scan the config's actions/mutations/getters
// collections and extract their methods as nodes. Store-file gated (the
// ≥2-signal heuristic) so a plain default-exported object is untouched; skip
// the subtree afterward (the collection methods are now handled).
else if (
nodeType === 'export_statement' &&
(this.language === 'typescript' || this.language === 'tsx' ||
this.language === 'javascript' || this.language === 'jsx') &&
this.looksLikeVueStoreFile()
) {
const exported = getChildByField(node, 'value');
if (exported && (exported.type === 'object' || exported.type === 'object_expression')) {
this.extractStoreCollectionMethods(exported);
skipChildren = true;
}
}
// Check for function calls
else if (this.extractor.callTypes.includes(nodeType)) {
this.extractCall(node);
@@ -2183,6 +2201,23 @@ export class TreeSitterExtractor {
return objects;
}
/** Extract the methods of a store-config object's `actions`/`mutations`/`getters`
* properties. Used for the canonical Vuex MODULE shape `export default {
* namespaced, actions: {…}, mutations: {…} }` — object-literal methods aren't
* otherwise extracted, so the actions/mutations would never be nodes. */
private extractStoreCollectionMethods(configObj: SyntaxNode): void {
for (let j = 0; j < configObj.namedChildCount; j++) {
const member = configObj.namedChild(j);
if (member?.type !== 'pair') continue;
const key = getChildByField(member, 'key');
if (!key || !VUE_STORE_COLLECTION_NAMES.has(getNodeText(key, this.source))) continue;
const value = getChildByField(member, 'value');
if (value && (value.type === 'object' || value.type === 'object_expression')) {
this.extractObjectLiteralFunctions(value);
}
}
}
/** The SETUP function of a Pinia setup store (`defineStore('id', () => {…})`)
* — an arrow/function arg with a block body. Returns null for the options form
* (`defineStore({…})`) and for any non-defineStore call. The setup body's local