@hupan56/wlkj 2.5.0 → 2.7.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.
Files changed (127) hide show
  1. package/bin/cli.js +289 -12
  2. package/package.json +1 -1
  3. package/templates/qoder/agents/insight-planning.md +67 -0
  4. package/templates/qoder/agents/insight-research.md +61 -0
  5. package/templates/qoder/agents/prd-quick.md +1 -0
  6. package/templates/qoder/agents/prd-reference.md +10 -2
  7. package/templates/qoder/commands/optional/wl-insight.md +275 -0
  8. package/templates/qoder/commands/{wl-report.md → optional/wl-report.md} +13 -5
  9. package/templates/qoder/commands/{wl-spec.md → optional/wl-spec.md} +1 -1
  10. package/templates/qoder/commands/{wl-status.md → optional/wl-status.md} +28 -2
  11. package/templates/qoder/commands/wl-code.md +10 -2
  12. package/templates/qoder/commands/wl-commit.md +1 -1
  13. package/templates/qoder/commands/wl-design-draw.md +78 -0
  14. package/templates/qoder/commands/wl-design-scan.md +108 -0
  15. package/templates/qoder/commands/wl-design-spec.md +154 -0
  16. package/templates/qoder/commands/wl-design.md +32 -0
  17. package/templates/qoder/commands/wl-init.md +24 -3
  18. package/templates/qoder/commands/wl-prd-full.md +226 -0
  19. package/templates/qoder/commands/wl-prd-quick.md +134 -0
  20. package/templates/qoder/commands/wl-prd-review.md +104 -0
  21. package/templates/qoder/commands/wl-prd.md +17 -288
  22. package/templates/qoder/commands/wl-search.md +66 -30
  23. package/templates/qoder/commands/wl-task.md +290 -59
  24. package/templates/qoder/commands/wl-test.md +92 -24
  25. package/templates/qoder/config.yaml +59 -15
  26. package/templates/qoder/hooks/inject-workflow-state.py +35 -9
  27. package/templates/qoder/hooks/session-start.py +144 -62
  28. package/templates/qoder/rules/wl-pipeline.md +216 -105
  29. package/templates/qoder/scripts/__pycache__/search_index.cpython-39.pyc +0 -0
  30. package/templates/qoder/scripts/archive_prd.py +377 -0
  31. package/templates/qoder/scripts/autotest.py +1715 -0
  32. package/templates/qoder/scripts/autotest_batch.py +224 -0
  33. package/templates/qoder/scripts/autotest_run.py +297 -0
  34. package/templates/qoder/scripts/benchmark.py +210 -209
  35. package/templates/qoder/scripts/build_style_index.py +444 -4
  36. package/templates/qoder/scripts/check_carriers.py +238 -0
  37. package/templates/qoder/scripts/check_mcp.py +298 -0
  38. package/templates/qoder/scripts/check_qoderwork_consistency.py +166 -0
  39. package/templates/qoder/scripts/common/developer.py +26 -19
  40. package/templates/qoder/scripts/common/events.py +46 -0
  41. package/templates/qoder/scripts/common/extract.py +419 -0
  42. package/templates/qoder/scripts/common/graph_traverse.py +533 -0
  43. package/templates/qoder/scripts/common/identity.py +6 -1
  44. package/templates/qoder/scripts/common/paths.py +89 -0
  45. package/templates/qoder/scripts/common/pip_install.py +144 -0
  46. package/templates/qoder/scripts/common/platform_guard.py +61 -0
  47. package/templates/qoder/scripts/common/search_engine.py +205 -205
  48. package/templates/qoder/scripts/common/terms.py +57 -0
  49. package/templates/qoder/scripts/common/ts_extract.py +536 -0
  50. package/templates/qoder/scripts/context_pack.py +73 -13
  51. package/templates/qoder/scripts/enrich_prompt.py +226 -0
  52. package/templates/qoder/scripts/eval_prd.py +318 -225
  53. package/templates/qoder/scripts/export.py +487 -487
  54. package/templates/qoder/scripts/extract_api_params.py +246 -0
  55. package/templates/qoder/scripts/extract_routes.py +54 -0
  56. package/templates/qoder/scripts/extract_routes_tree.py +78 -0
  57. package/templates/qoder/scripts/fill_prototype.py +707 -0
  58. package/templates/qoder/scripts/gen_design_doc.py +394 -0
  59. package/templates/qoder/scripts/git_sync.py +27 -15
  60. package/templates/qoder/scripts/init_doctor.py +306 -41
  61. package/templates/qoder/scripts/install_qoderwork.py +366 -9
  62. package/templates/qoder/scripts/kg.py +708 -0
  63. package/templates/qoder/scripts/kg_auto_login.py +196 -0
  64. package/templates/qoder/scripts/kg_build.py +612 -0
  65. package/templates/qoder/scripts/kg_build_db.py +327 -0
  66. package/templates/qoder/scripts/kg_duckdb.py +549 -0
  67. package/templates/qoder/scripts/kg_incremental.py +393 -0
  68. package/templates/qoder/scripts/kg_link_db.py +224 -0
  69. package/templates/qoder/scripts/kg_mcp_server.py +801 -0
  70. package/templates/qoder/scripts/kg_semantic.py +150 -0
  71. package/templates/qoder/scripts/kg_test_runner.py +241 -0
  72. package/templates/qoder/scripts/lanhu_stdio_wrapper.py +119 -0
  73. package/templates/qoder/scripts/learn.py +118 -39
  74. package/templates/qoder/scripts/learn_aggregate.py +201 -0
  75. package/templates/qoder/scripts/mcp_launcher.py +359 -0
  76. package/templates/qoder/scripts/mysql_mcp_server.py +396 -0
  77. package/templates/qoder/scripts/repo_root.py +106 -0
  78. package/templates/qoder/scripts/role.py +12 -0
  79. package/templates/qoder/scripts/run_weekly_update.bat +5 -0
  80. package/templates/qoder/scripts/run_weekly_update.sh +5 -0
  81. package/templates/qoder/scripts/search_index.py +307 -60
  82. package/templates/qoder/scripts/secure-ls.js +5640 -0
  83. package/templates/qoder/scripts/setup.py +706 -453
  84. package/templates/qoder/scripts/setup_lanhu.py +963 -0
  85. package/templates/qoder/scripts/status.py +250 -11
  86. package/templates/qoder/scripts/sync_carriers.py +259 -0
  87. package/templates/qoder/scripts/syncgate.py +5 -4
  88. package/templates/qoder/scripts/task.py +75 -0
  89. package/templates/qoder/scripts/team_sync.py +60 -4
  90. package/templates/qoder/scripts/workspace_init.py +1 -1
  91. package/templates/qoder/skills/design-import/SKILL.md +226 -0
  92. package/templates/qoder/skills/design-import/figma-workflow.md +81 -0
  93. package/templates/qoder/skills/design-review/SKILL.md +82 -25
  94. package/templates/qoder/skills/prd-generator/SKILL.md +185 -58
  95. package/templates/qoder/skills/prd-review/SKILL.md +18 -1
  96. package/templates/qoder/skills/prompt-enrich/SKILL.md +90 -0
  97. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -141
  98. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  99. package/templates/qoder/skills/spec-coder/SKILL.md +18 -1
  100. package/templates/qoder/skills/spec-generator/SKILL.md +18 -1
  101. package/templates/qoder/skills/test-generator/SKILL.md +15 -2
  102. package/templates/qoder/skills/wl-code/SKILL.md +55 -36
  103. package/templates/qoder/skills/wl-commit/SKILL.md +89 -76
  104. package/templates/qoder/skills/wl-design/SKILL.md +55 -0
  105. package/templates/qoder/skills/wl-init/SKILL.md +76 -67
  106. package/templates/qoder/skills/wl-insight/SKILL.md +201 -81
  107. package/templates/qoder/skills/wl-prd-full/SKILL.md +69 -0
  108. package/templates/qoder/skills/wl-prd-quick/SKILL.md +49 -0
  109. package/templates/qoder/skills/wl-prd-review/SKILL.md +34 -0
  110. package/templates/qoder/skills/wl-report/SKILL.md +131 -107
  111. package/templates/qoder/skills/wl-search/SKILL.md +141 -75
  112. package/templates/qoder/skills/wl-spec/SKILL.md +49 -39
  113. package/templates/qoder/skills/wl-status/SKILL.md +83 -61
  114. package/templates/qoder/skills/wl-task/SKILL.md +132 -58
  115. package/templates/qoder/skills/wl-test/SKILL.md +406 -40
  116. package/templates/qoder/templates/prd-full-template.md +2 -0
  117. package/templates/qoder/templates/prd-quick-template.md +1 -0
  118. package/templates/qoder/templates/prototype-app.html +13 -8
  119. package/templates/qoder/templates/prototype-web.html +376 -93
  120. package/templates/root/AGENTS.md +89 -34
  121. package/templates/root/requirements.txt +21 -0
  122. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +259 -259
  123. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -186
  124. package/templates/qoder/agents/prd-planning.md +0 -56
  125. package/templates/qoder/agents/prd-research.md +0 -33
  126. package/templates/qoder/commands/wl-insight.md +0 -51
  127. package/templates/qoder/skills/wl-prd/SKILL.md +0 -89
@@ -0,0 +1,419 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Shared extraction utilities for the QODER knowledge graph.
4
+
5
+ Centralizes regex-based extraction that was previously copy-pasted across
6
+ build_style_index.py and git_sync.py. The headline feature is the **call-chain
7
+ extractor**: given a project's source tree, it builds a table mapping each
8
+ clickable UI element (button @click + label) to the backend API endpoint it
9
+ triggers, so test automation and impact analysis can answer:
10
+
11
+ - "点击'新增'按钮会调哪个接口?" (test automation / QoderWork)
12
+ - "改这个接口会影响哪些页面/按钮?" (dev impact analysis)
13
+
14
+ Two passes:
15
+ 1. ``build_api_fn_table`` — scan ``src/api/**/*.ts``, parse ``enum Api``
16
+ + ``export function`` → {fnName: {url, verb, file}}. ~100% reliable.
17
+ 2. ``trace_clicks`` — scan ``src/views/**/*.vue``, resolve each
18
+ ``@click="handleX"`` to an api function via 4 patterns (direct / drawer
19
+ onConfirm / Modal.confirm onOk / router.push deferred).
20
+
21
+ Confidence is tagged per edge (high/medium/low/deferred) so consumers know
22
+ which traces are statically certain vs need runtime confirmation.
23
+ """
24
+ import os
25
+ import re
26
+
27
+
28
+ # ── Reusable extraction regexes (previously duplicated) ──────────────
29
+
30
+ # Table column: title + field (VxeGrid/Vben schema). Used by build_style_index.
31
+ RE_TABLE_COL_TF = re.compile(
32
+ r"title:\s*['\"]([^'\"]+)['\"]\s*,\s*field:\s*['\"]([^'\"]+)['\"]")
33
+ RE_TABLE_COL_FT = re.compile(
34
+ r"field:\s*['\"]([^'\"]+)['\"]\s*,\s*title:\s*['\"]([^'\"]+)['\"]")
35
+
36
+ # Form field: fieldName + label
37
+ RE_FORM_FIELD = re.compile(
38
+ r"fieldName:\s*['\"]([^'\"]+)['\"]\s*,\s*label:\s*['\"]([^'\"]+)['\"]")
39
+
40
+ # Ant Design Vue component imports
41
+ RE_ANT_IMPORTS = re.compile(
42
+ r"import\s+\{([^}]+)\}\s+from\s+['\"]ant-design-vue['\"]")
43
+
44
+
45
+ # ── Call-chain extraction ────────────────────────────────────────────
46
+
47
+ # enum Api { key = '/url', ... } — the URL constant block
48
+ RE_ENUM_API = re.compile(r"enum\s+Api\s*\{([^}]*)\}", re.S)
49
+ RE_ENUM_ENTRY = re.compile(r"(\w+)\s*=\s*['\"`]([^'\"`]+)['\"`]")
50
+
51
+ # export function fnName(...) { return requestClient.<verb>(<urlExpr>, ...) }
52
+ # urlExpr can be Api.key (symbolic) or a template literal `${Api.key}/...`
53
+ # export function fnName(...) { ... }
54
+ # Also matches: export const fnName = (async)? (...) => { (arrow function consts,
55
+ # used for commonExport wrappers like `export const exportFactoryList = async (p) => ...`)
56
+ RE_EXPORT_FN = re.compile(
57
+ r"(?:export\s+(?:async\s+)?function\s+(\w+)\s*\(|"
58
+ r"export\s+const\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{?)", re.M)
59
+ RE_REQUEST_CALL = re.compile(
60
+ r"requestClient\.(\w+)(?:<[^)]*?>)?\s*\(\s*(Api\.\w+|`[^`]*`|'[^']*'|\"[^\"]*\")")
61
+
62
+ # commonExport(Api.key, ...) / commonDownloadExcel(fn, ...) — wrapper that
63
+ # delegates to requestClient. commonExport takes a URL; commonDownloadExcel
64
+ # takes an api *function* as first arg.
65
+ RE_COMMON_EXPORT = re.compile(r"commonExport\s*\(\s*(Api\.\w+)")
66
+ RE_COMMON_DOWNLOAD = re.compile(r"commonDownloadExcel\s*\(\s*(\w+)")
67
+
68
+ # Vue template: @click="handler" or @click.stop="handler(...)"
69
+ RE_CLICK = re.compile(r"@click(?:\.\w+)?\s*=\s*['\"]([^'\"]+)['\"]")
70
+
71
+ # Button text between tags (after @click line, take nearest text node)
72
+ # We grab the text right after the clickable element opening tag.
73
+ RE_BTN_TEXT_AFTER_CLICK = re.compile(
74
+ r"@click(?:\.\w+)?\s*=\s*['\"][^'\"]*['\"][^>]*>\s*([^\n<]{1,20})", re.S)
75
+
76
+ # import { fn1, fn2 } from '#/api/...' — api imports in a Vue SFC
77
+ RE_API_IMPORT = re.compile(
78
+ r"import\s+\{([^}]+)\}\s+from\s+['\"](?:#|@)/(?:api|@/api)/([^'\"]+)['\"]")
79
+
80
+ # connectedComponent: localImport (drawer/modal wiring)
81
+ RE_CONNECTED_COMP = re.compile(r"connectedComponent:\s*(\w+)")
82
+
83
+ # import localName from './xxx.vue' (local default import for drawer)
84
+ RE_LOCAL_VUE_IMPORT = re.compile(
85
+ r"import\s+(\w+)\s+from\s+['\"]([^'\"]+\.vue)['\"]")
86
+
87
+ # function handlerName(...) { ... } — capture full function body (greedy to next function/export/</script)
88
+ RE_FN_BODY = re.compile(
89
+ r"(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*\{", re.M)
90
+
91
+ # modalApi.open() / drawerApi.open() — drawer trigger
92
+ RE_DRAWER_OPEN = re.compile(r"(\w*[Aa]pi)\??\.open\s*\(\s*\)")
93
+
94
+ # Modal.confirm({ onOk: ... }) — delete confirmation
95
+ RE_MODAL_CONFIRM = re.compile(r"Modal\.confirm\s*\(\s*\{", re.S)
96
+
97
+ # useVbenDrawer({ connectedComponent: X }) — drawer declaration
98
+ RE_USE_DRAWER = re.compile(
99
+ r"useVbenDrawer\s*\(\s*\{[^}]*connectedComponent:\s*(\w+)", re.S)
100
+
101
+ # router.push({ path: '...' })
102
+ RE_ROUTER_PUSH = re.compile(r"router\.push\s*\(")
103
+
104
+
105
+ def parse_api_enum_block(content):
106
+ """Extract {enumKey: '/url'} from an ``enum Api { ... }`` block.
107
+
108
+ Handles enum values containing template literals with ${VAR} — the nested
109
+ braces would break a naive [^}]* regex, so we use brace-depth matching.
110
+ """
111
+ urls = {}
112
+ # Find "enum Api {" then manually match braces to find the closing }
113
+ for m in re.finditer(r'enum\s+Api\s*\{', content):
114
+ start = m.end()
115
+ depth = 1
116
+ i = start
117
+ while i < len(content) and depth > 0:
118
+ if content[i] == '{':
119
+ depth += 1
120
+ elif content[i] == '}':
121
+ depth -= 1
122
+ i += 1
123
+ block = content[start:i-1] if depth == 0 else content[start:]
124
+ for em in RE_ENUM_ENTRY.finditer(block):
125
+ urls[em.group(1)] = em.group(2)
126
+ return urls
127
+
128
+
129
+ def build_api_fn_table(api_dir):
130
+ """Scan ``src/api/**/*.ts`` and build {fnName: {url, verb, file}}.
131
+
132
+ Resolves symbolic ``Api.key`` references via the file's ``enum Api`` block.
133
+ Handles ``commonExport(Api.key)`` wrappers. Returns dict + the enum map
134
+ per file (for cross-file resolution).
135
+ """
136
+ table = {}
137
+ if not api_dir or not os.path.isdir(api_dir):
138
+ return table
139
+
140
+ for root, dirs, files in os.walk(api_dir):
141
+ dirs[:] = [d for d in dirs if d not in ('node_modules', 'dist', '__pycache__')]
142
+ for f in files:
143
+ if not f.endswith('.ts'):
144
+ continue
145
+ fpath = os.path.join(root, f)
146
+ try:
147
+ content = open(fpath, encoding='utf-8', errors='ignore').read()
148
+ except Exception:
149
+ continue
150
+ urls = parse_api_enum_block(content)
151
+
152
+ # For each exported function, find requestClient call inside its
153
+ # EXACT body (brace-matched, not a fixed-length snippet — a snippet
154
+ # would bleed into the next function and mis-resolve the Api key).
155
+ for fm in RE_EXPORT_FN.finditer(content):
156
+ fn_name = fm.group(1) or fm.group(2) # function name or const name
157
+ if not fn_name:
158
+ continue
159
+ body = _get_fn_body(content, fn_name, max_len=500)
160
+ if not body:
161
+ continue
162
+ entry = None
163
+ # Direct requestClient.<verb>(Api.key or 'literal')
164
+ rm = RE_REQUEST_CALL.search(body)
165
+ if rm:
166
+ verb = rm.group(1)
167
+ url_expr = rm.group(2)
168
+ url = _resolve_url_expr(url_expr, urls)
169
+ if url:
170
+ entry = {'url': url, 'verb': verb, 'file': fpath.replace('\\', '/')}
171
+ # commonExport(Api.key) wrapper
172
+ if not entry:
173
+ cm = RE_COMMON_EXPORT.search(body)
174
+ if cm:
175
+ url = _resolve_url_expr(cm.group(1), urls)
176
+ if url:
177
+ entry = {'url': url, 'verb': 'post', 'file': fpath.replace('\\', '/')}
178
+ if entry:
179
+ table[fn_name] = entry
180
+ return table
181
+
182
+
183
+ def _resolve_url_expr(expr, urls):
184
+ """Resolve ``Api.key`` or template literal to a concrete URL string."""
185
+ expr = expr.strip()
186
+ if expr.startswith('Api.'):
187
+ key = expr[4:]
188
+ return urls.get(key)
189
+ # Template literal `${Api.key}/xxx` or plain string
190
+ if expr.startswith('`') or expr.startswith("'") or expr.startswith('"'):
191
+ inner = expr.strip('`\'"')
192
+ # Replace ${Api.key} placeholders
193
+ def repl(m):
194
+ return urls.get(m.group(1), m.group(0))
195
+ inner = re.sub(r"\$\{Api\.(\w+)\}", repl, inner)
196
+ # If still has Api. references, try direct
197
+ inner = re.sub(r"Api\.(\w+)", lambda m: urls.get(m.group(1), m.group(0)), inner)
198
+ return inner if '/' in inner else None
199
+ return None
200
+
201
+
202
+ def _get_fn_body(content, fn_name, max_len=800):
203
+ """Get the body text of a named function (best-effort, ~max_len chars).
204
+
205
+ Handles both ``function fnName() {`` and ``const fnName = () => {`` forms.
206
+ """
207
+ # Try: function fnName( OR const fnName = (async) (...) => {
208
+ for pat in [
209
+ re.compile(r"(?:async\s+)?function\s+" + re.escape(fn_name) + r"\s*\([^)]*\)\s*\{"),
210
+ re.compile(r"const\s+" + re.escape(fn_name) + r"\s*=\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{?"),
211
+ ]:
212
+ m = pat.search(content)
213
+ if m:
214
+ break
215
+ else:
216
+ return ''
217
+ start = m.end()
218
+ # If arrow fn without explicit {, grab the expression line instead
219
+ if '=>' in m.group(0) and '{' not in m.group(0):
220
+ return content[start:start + max_len]
221
+ # Simple brace matching
222
+ depth = 1
223
+ i = start
224
+ end = min(len(content), start + max_len)
225
+ while i < end and depth > 0:
226
+ if content[i] == '{':
227
+ depth += 1
228
+ elif content[i] == '}':
229
+ depth -= 1
230
+ i += 1
231
+ return content[start:i]
232
+
233
+
234
+ def _find_api_calls_in_text(text, api_table, api_imports):
235
+ """Return list of (api_fn, endpoint, verb) found in a text snippet."""
236
+ hits = []
237
+ for fn in api_imports:
238
+ if fn in api_table and re.search(r'\b' + re.escape(fn) + r'\b', text):
239
+ info = api_table[fn]
240
+ hits.append((fn, info['url'], info.get('verb', '?')))
241
+ # commonDownloadExcel(fnName, ...) — first arg is an api function
242
+ for dm in RE_COMMON_DOWNLOAD.finditer(text):
243
+ fn = dm.group(1)
244
+ if fn in api_table:
245
+ info = api_table[fn]
246
+ hits.append((fn, info['url'], info.get('verb', '?')))
247
+ return hits
248
+
249
+
250
+ def _resolve_drawer_api(content, drawer_var):
251
+ """Given a drawer/modal api var name, find its connectedComponent import path."""
252
+ # useVbenDrawer({ connectedComponent: CompName }) declared as [CompName, drawerVar]
253
+ # Pattern: const [CompName, drawerVar] = useVbenDrawer({ connectedComponent: compImport })
254
+ pat = re.compile(
255
+ r"const\s+\[\s*(\w+)\s*,\s*" + re.escape(drawer_var) + r"\s*\]\s*=\s*useVbenDrawer",
256
+ re.S)
257
+ m = pat.search(content)
258
+ if not m:
259
+ # Fallback: find connectedComponent near the drawer var
260
+ for cm in RE_CONNECTED_COMP.finditer(content):
261
+ comp = cm.group(1)
262
+ # Verify this comp's drawer var matches by proximity (within 200 chars)
263
+ if drawer_var in content[max(0, cm.start()-200):cm.start()+200]:
264
+ m = type('x', (), {'group': lambda self, i: comp})()
265
+ break
266
+ if not m:
267
+ return None
268
+ comp_name = m.group(1) if hasattr(m, 'group') else None
269
+ if not comp_name:
270
+ return None
271
+ # Find import localName from './xxx.vue' matching comp_name
272
+ for im in RE_LOCAL_VUE_IMPORT.finditer(content):
273
+ if im.group(1) == comp_name:
274
+ return im.group(2)
275
+ return None
276
+
277
+
278
+ def trace_clicks(vue_path, vue_content, api_table):
279
+ """Extract click→api traces from a single Vue SFC.
280
+
281
+ Returns list of trace dicts:
282
+ {vue_file, button, handler, api_fn, endpoint, verb, confidence}
283
+ confidence: 'high' (direct), 'medium' (drawer 1-hop), 'low' (modal confirm),
284
+ 'deferred' (router.push).
285
+ """
286
+ traces = []
287
+ dirname = os.path.dirname(vue_path)
288
+
289
+ # 1. Collect api imports in this SFC
290
+ api_imports = set()
291
+ for im in RE_API_IMPORT.finditer(vue_content):
292
+ for name in im.group(1).split(','):
293
+ name = name.strip().split(' as ')[0].strip()
294
+ if name and name in api_table:
295
+ api_imports.add(name)
296
+
297
+ # 2. Find all @click handlers + nearest button text
298
+ clicks = list(RE_CLICK.finditer(vue_content))
299
+ if not clicks:
300
+ return traces
301
+
302
+ # Build drawer map: drawerVar → connected component .vue path.
303
+ # Pattern: const [Comp, drawerVar] = useVbenDrawer({ connectedComponent: compImport })
304
+ # Note: Comp name may differ from compImport (e.g. [FactoryModal, modalApi] with
305
+ # connectedComponent: factoryModal). So we link drawerVar → compImport → import path.
306
+ drawer_map = {}
307
+ # Find all useVbenDrawer declarations and capture the drawer var + connectedComponent
308
+ for dm in re.finditer(
309
+ r"const\s+\[\s*\w+\s*,\s*(\w+)\s*\]\s*=\s*useVbenDrawer\s*\(\s*\{([^}]*)\}",
310
+ vue_content, re.S):
311
+ drawer_var = dm.group(1)
312
+ decl_body = dm.group(2)
313
+ cm = re.search(r"connectedComponent:\s*(\w+)", decl_body)
314
+ if not cm:
315
+ continue
316
+ comp_import_name = cm.group(1)
317
+ # Resolve comp_import_name → './xxx.vue' via local imports
318
+ for im in RE_LOCAL_VUE_IMPORT.finditer(vue_content):
319
+ if im.group(1) == comp_import_name:
320
+ drawer_map[drawer_var] = im.group(2)
321
+ break
322
+
323
+ # 3. For each click, resolve to api
324
+ for i, cm in enumerate(clicks):
325
+ handler_expr = cm.group(1).strip()
326
+ # Button text: text right after the element, or previous text node
327
+ btn_text = ''
328
+ tm = RE_BTN_TEXT_AFTER_CLICK.search(vue_content, cm.start())
329
+ if tm:
330
+ btn_text = tm.group(1).strip()[:12]
331
+ # Strip Vue interpolation {{ }} from button text (e.g. "{{ item.name }}")
332
+ btn_text = re.sub(r'\{\{[^}]*\}\}', '', btn_text).strip()[:12]
333
+
334
+ # Skip inline expressions that aren't function calls:
335
+ # $emit, item.remove(), splice, push — pure frontend, no API
336
+ if handler_expr.startswith('$') or '.splice(' in handler_expr or '.push(' in handler_expr:
337
+ continue
338
+
339
+ # Get handler function name (strip args/calls)
340
+ handler_name = handler_expr.split('(')[0].split('.')[0].strip()
341
+ if not handler_name or handler_name in ('undefined', '', 'true', 'false'):
342
+ continue
343
+ # Skip single-word inline handlers (add, remove, item, emit) that are
344
+ # likely data/methods shorthand, not named handler functions
345
+ if handler_name in ('item', 'emit', 'row', 'data') and '.' not in handler_expr:
346
+ continue
347
+
348
+ fn_body = _get_fn_body(vue_content, handler_name)
349
+
350
+ trace = {
351
+ 'vue_file': vue_path.replace('\\', '/'),
352
+ 'button': btn_text,
353
+ 'handler': handler_name,
354
+ 'api_fn': None, 'endpoint': None, 'verb': None,
355
+ 'confidence': None,
356
+ }
357
+
358
+ # Pattern B: direct api call in handler body
359
+ hits = _find_api_calls_in_text(fn_body, api_table, api_imports)
360
+ if hits:
361
+ fn, url, verb = hits[0]
362
+ trace.update(api_fn=fn, endpoint=url, verb=verb, confidence='high')
363
+ traces.append(trace)
364
+ continue
365
+
366
+ # Pattern D: drawer/modal open → resolve connected component
367
+ open_m = RE_DRAWER_OPEN.search(fn_body)
368
+ if open_m:
369
+ drawer_var = open_m.group(1)
370
+ comp_rel = drawer_map.get(drawer_var)
371
+ if comp_rel:
372
+ comp_path = os.path.normpath(os.path.join(dirname, comp_rel))
373
+ if os.path.isfile(comp_path):
374
+ try:
375
+ comp_content = open(comp_path, encoding='utf-8', errors='ignore').read()
376
+ comp_api_imports = set()
377
+ for im in RE_API_IMPORT.finditer(comp_content):
378
+ for name in im.group(1).split(','):
379
+ name = name.strip().split(' as ')[0].strip()
380
+ if name in api_table:
381
+ comp_api_imports.add(name)
382
+ # Find onConfirm/handleConfirm body in the component
383
+ for confirm_name in ('handleConfirm', 'onConfirm', 'handleSubmit', 'onOk'):
384
+ cbody = _get_fn_body(comp_content, confirm_name)
385
+ if cbody:
386
+ chits = _find_api_calls_in_text(cbody, api_table, comp_api_imports)
387
+ if chits:
388
+ fn, url, verb = chits[0]
389
+ trace.update(api_fn=fn, endpoint=url, verb=verb,
390
+ confidence='medium')
391
+ break
392
+ except Exception:
393
+ pass
394
+ if trace['endpoint']:
395
+ traces.append(trace)
396
+ continue
397
+
398
+ # Modal.confirm({ onOk }) pattern
399
+ mc_m = RE_MODAL_CONFIRM.search(fn_body)
400
+ if mc_m:
401
+ onok_body = fn_body[mc_m.end():]
402
+ hits = _find_api_calls_in_text(onok_body[:400], api_table, api_imports)
403
+ if hits:
404
+ fn, url, verb = hits[0]
405
+ trace.update(api_fn=fn, endpoint=url, verb=verb, confidence='low')
406
+ traces.append(trace)
407
+ continue
408
+
409
+ # router.push — deferred
410
+ if RE_ROUTER_PUSH.search(fn_body):
411
+ trace['confidence'] = 'deferred'
412
+ traces.append(trace)
413
+ continue
414
+
415
+ # Unresolved — record as low confidence (handler exists but no api found)
416
+ trace['confidence'] = 'unresolved'
417
+ traces.append(trace)
418
+
419
+ return traces