codebeacon 0.1.2__py3-none-any.whl

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 (59) hide show
  1. codebeacon/__init__.py +1 -0
  2. codebeacon/__main__.py +3 -0
  3. codebeacon/cache.py +136 -0
  4. codebeacon/cli.py +391 -0
  5. codebeacon/common/__init__.py +0 -0
  6. codebeacon/common/filters.py +170 -0
  7. codebeacon/common/symbols.py +121 -0
  8. codebeacon/common/types.py +98 -0
  9. codebeacon/config.py +144 -0
  10. codebeacon/contextmap/__init__.py +0 -0
  11. codebeacon/contextmap/generator.py +602 -0
  12. codebeacon/discover/__init__.py +0 -0
  13. codebeacon/discover/detector.py +388 -0
  14. codebeacon/discover/scanner.py +192 -0
  15. codebeacon/export/__init__.py +0 -0
  16. codebeacon/export/mcp.py +515 -0
  17. codebeacon/export/obsidian.py +812 -0
  18. codebeacon/extract/__init__.py +22 -0
  19. codebeacon/extract/base.py +372 -0
  20. codebeacon/extract/components.py +357 -0
  21. codebeacon/extract/dependencies.py +140 -0
  22. codebeacon/extract/entities.py +575 -0
  23. codebeacon/extract/queries/README.md +116 -0
  24. codebeacon/extract/queries/actix.scm +115 -0
  25. codebeacon/extract/queries/angular.scm +155 -0
  26. codebeacon/extract/queries/aspnet.scm +159 -0
  27. codebeacon/extract/queries/django.scm +122 -0
  28. codebeacon/extract/queries/express.scm +124 -0
  29. codebeacon/extract/queries/fastapi.scm +152 -0
  30. codebeacon/extract/queries/flask.scm +120 -0
  31. codebeacon/extract/queries/gin.scm +142 -0
  32. codebeacon/extract/queries/ktor.scm +144 -0
  33. codebeacon/extract/queries/laravel.scm +172 -0
  34. codebeacon/extract/queries/nestjs.scm +183 -0
  35. codebeacon/extract/queries/rails.scm +114 -0
  36. codebeacon/extract/queries/react.scm +111 -0
  37. codebeacon/extract/queries/spring_boot.scm +204 -0
  38. codebeacon/extract/queries/svelte.scm +73 -0
  39. codebeacon/extract/queries/vapor.scm +130 -0
  40. codebeacon/extract/queries/vue.scm +123 -0
  41. codebeacon/extract/routes.py +910 -0
  42. codebeacon/extract/semantic.py +280 -0
  43. codebeacon/extract/services.py +597 -0
  44. codebeacon/graph/__init__.py +1 -0
  45. codebeacon/graph/analyze.py +281 -0
  46. codebeacon/graph/build.py +320 -0
  47. codebeacon/graph/cluster.py +160 -0
  48. codebeacon/graph/enrich.py +206 -0
  49. codebeacon/skill/SKILL.md +127 -0
  50. codebeacon/wave.py +292 -0
  51. codebeacon/wiki/__init__.py +0 -0
  52. codebeacon/wiki/generator.py +376 -0
  53. codebeacon/wiki/index.py +95 -0
  54. codebeacon/wiki/templates.py +467 -0
  55. codebeacon-0.1.2.dist-info/METADATA +319 -0
  56. codebeacon-0.1.2.dist-info/RECORD +59 -0
  57. codebeacon-0.1.2.dist-info/WHEEL +4 -0
  58. codebeacon-0.1.2.dist-info/entry_points.txt +2 -0
  59. codebeacon-0.1.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,357 @@
1
+ """Frontend component extraction for React, Vue, Svelte, Angular.
2
+
3
+ Public API:
4
+ extract_components(file_path, framework, project_path="") -> list[ComponentInfo]
5
+
6
+ Extracts:
7
+ - React: uppercase function/arrow components, hooks, props
8
+ - Vue: defineComponent / SFC, composables, props
9
+ - Svelte: SFC files, exported props, runes
10
+ - Angular: @Component class, selector, templateUrl
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+
16
+ from codebeacon.common.types import ComponentInfo
17
+ from codebeacon.extract.base import (
18
+ extract_sfc_sections,
19
+ load_query_file,
20
+ node_text,
21
+ parse_file,
22
+ parse_sfc_script,
23
+ run_query,
24
+ )
25
+
26
+
27
+ # ── Framework → query file stem ───────────────────────────────────────────────
28
+
29
+ _FW_TO_QUERY: dict[str, str] = {
30
+ "react": "react",
31
+ "nextjs": "react",
32
+ "vue": "vue",
33
+ "nuxt": "vue",
34
+ "sveltekit": "svelte",
35
+ "angular": "angular",
36
+ }
37
+
38
+
39
+ # ── Public function ───────────────────────────────────────────────────────────
40
+
41
+ def extract_components(
42
+ file_path: str,
43
+ framework: str,
44
+ project_path: str = "",
45
+ ) -> list[ComponentInfo]:
46
+ """Extract frontend component declarations from *file_path*."""
47
+ fw = framework.lower()
48
+ query_name = _FW_TO_QUERY.get(fw)
49
+ if not query_name:
50
+ return []
51
+
52
+ query_src = load_query_file(query_name)
53
+ if not query_src:
54
+ return []
55
+
56
+ # SFC dispatch
57
+ ext = Path(file_path).suffix.lower()
58
+ if ext in (".vue", ".svelte"):
59
+ sfc = extract_sfc_sections(file_path)
60
+ if sfc is None:
61
+ return []
62
+ parsed = parse_sfc_script(sfc)
63
+ else:
64
+ parsed = parse_file(file_path)
65
+
66
+ if parsed is None:
67
+ return []
68
+ root, lang = parsed
69
+
70
+ from codebeacon.extract.base import is_grammar_allowed
71
+ if not is_grammar_allowed(query_name, lang):
72
+ return []
73
+
74
+ try:
75
+ matches = run_query(lang, query_src, root)
76
+ except Exception:
77
+ return []
78
+
79
+ _interpreters = {
80
+ "react": _interpret_react,
81
+ "vue": _interpret_vue,
82
+ "svelte": _interpret_svelte,
83
+ "angular": _interpret_angular,
84
+ }
85
+
86
+ interpreter = _interpreters.get(query_name)
87
+ if interpreter is None:
88
+ return []
89
+
90
+ try:
91
+ components = interpreter(file_path, matches, fw)
92
+ except Exception:
93
+ components = []
94
+
95
+ # For SFC files, ensure at least one component with the filename as name
96
+ if ext in (".vue", ".svelte") and not components:
97
+ stem = Path(file_path).stem
98
+ components = [ComponentInfo(
99
+ name=stem,
100
+ source_file=file_path,
101
+ line=1,
102
+ framework=fw,
103
+ )]
104
+
105
+ # Derive route info for page components
106
+ if project_path:
107
+ _annotate_page_routes(components, file_path, fw, project_path)
108
+
109
+ return components
110
+
111
+
112
+ # ── Helpers ───────────────────────────────────────────────────────────────────
113
+
114
+ def _annotate_page_routes(
115
+ components: list[ComponentInfo],
116
+ file_path: str,
117
+ framework: str,
118
+ project_path: str,
119
+ ) -> None:
120
+ """Mark components as page components and set route_path for file-system routed frameworks."""
121
+ try:
122
+ rel = Path(file_path).relative_to(Path(project_path))
123
+ except ValueError:
124
+ return
125
+
126
+ parts = rel.parts
127
+ is_page = False
128
+ route_path = ""
129
+
130
+ if framework in ("nextjs", "react"):
131
+ if parts and parts[0] == "pages":
132
+ is_page = True
133
+ elif len(parts) >= 2 and parts[0] == "app":
134
+ stem = Path(parts[-1]).stem
135
+ if stem in ("page", "layout", "route"):
136
+ is_page = True
137
+ elif framework == "nuxt":
138
+ if parts and parts[0] == "pages":
139
+ is_page = True
140
+ elif framework == "sveltekit":
141
+ if len(parts) >= 3 and parts[0] == "src" and parts[1] == "routes":
142
+ stem = Path(parts[-1]).stem
143
+ if stem.startswith("+"):
144
+ is_page = True
145
+
146
+ if is_page:
147
+ for comp in components:
148
+ comp.is_page = True
149
+
150
+
151
+ # ── Per-framework interpreters ────────────────────────────────────────────────
152
+
153
+ def _interpret_react(
154
+ file_path: str, matches: list, framework: str,
155
+ ) -> list[ComponentInfo]:
156
+ """React/Next.js: exported uppercase functions/arrows + hooks + props."""
157
+ components: dict[str, ComponentInfo] = {} # name → ComponentInfo
158
+ hooks: list[str] = []
159
+ props: list[str] = []
160
+ imports: list[str] = []
161
+
162
+ for _idx, caps in matches:
163
+ # Exported function component
164
+ for cap_key in ("component.func_name", "component.arrow_name", "component.memo_name"):
165
+ if cap_key in caps:
166
+ name = node_text(caps[cap_key][0])
167
+ if name and name not in components:
168
+ # Determine line from the parent export/declaration
169
+ parent_key = None
170
+ for k in ("component.export_func", "component.export_func_upper",
171
+ "component.export_default_func", "component.export_arrow",
172
+ "component.local_arrow", "component.hoc"):
173
+ if k in caps:
174
+ parent_key = k
175
+ break
176
+ line = caps[parent_key][0].start_point[0] + 1 if parent_key else 1
177
+ components[name] = ComponentInfo(
178
+ name=name,
179
+ source_file=file_path,
180
+ line=line,
181
+ framework=framework,
182
+ )
183
+
184
+ # Hooks
185
+ if "hook.name" in caps:
186
+ hook_name = node_text(caps["hook.name"][0])
187
+ if hook_name not in hooks:
188
+ hooks.append(hook_name)
189
+
190
+ # Props destructuring
191
+ if "prop.name" in caps:
192
+ for pn in caps["prop.name"]:
193
+ p = node_text(pn)
194
+ if p not in props:
195
+ props.append(p)
196
+
197
+ # Imports (for imported component tracking)
198
+ if "import.path" in caps:
199
+ path = node_text(caps["import.path"][0]).strip("'\"")
200
+ if path not in imports and not path.startswith("."):
201
+ imports.append(path)
202
+
203
+ # Assign file-level hooks/imports to all components.
204
+ # Props are assigned only to the first component (typically the main one).
205
+ comp_list = list(components.values())
206
+ for i, comp in enumerate(comp_list):
207
+ comp.hooks = hooks[:]
208
+ comp.imports = imports[:]
209
+ if i == 0:
210
+ comp.props = props[:]
211
+
212
+ return comp_list
213
+
214
+
215
+ def _interpret_vue(
216
+ file_path: str, matches: list, framework: str,
217
+ ) -> list[ComponentInfo]:
218
+ """Vue: defineComponent / export default class + composables + defineProps."""
219
+ name = ""
220
+ line = 1
221
+ props: list[str] = []
222
+ composables: list[str] = []
223
+ imports: list[str] = []
224
+
225
+ for _idx, caps in matches:
226
+ # defineComponent({ name: "..." })
227
+ if "component.name" in caps:
228
+ name = node_text(caps["component.name"][0])
229
+ if "component.define" in caps:
230
+ line = caps["component.define"][0].start_point[0] + 1
231
+
232
+ # export default class ComponentName
233
+ if "component.class_name" in caps:
234
+ name = node_text(caps["component.class_name"][0])
235
+ if "component.class" in caps:
236
+ line = caps["component.class"][0].start_point[0] + 1
237
+
238
+ # defineComponent without name (anonymous)
239
+ if "component.define_anon" in caps and not name:
240
+ name = Path(file_path).stem
241
+
242
+ # defineProps({ key: ... })
243
+ if "prop.name" in caps:
244
+ for pn in caps["prop.name"]:
245
+ p = node_text(pn)
246
+ if p not in props:
247
+ props.append(p)
248
+
249
+ # Composable usage (useX)
250
+ if "composable.name" in caps:
251
+ c = node_text(caps["composable.name"][0])
252
+ if c not in composables:
253
+ composables.append(c)
254
+
255
+ # Imports
256
+ if "import.path" in caps:
257
+ path = node_text(caps["import.path"][0]).strip("'\"")
258
+ if path not in imports:
259
+ imports.append(path)
260
+
261
+ if not name:
262
+ name = Path(file_path).stem
263
+
264
+ return [ComponentInfo(
265
+ name=name,
266
+ source_file=file_path,
267
+ line=line,
268
+ framework=framework,
269
+ props=props,
270
+ hooks=composables,
271
+ imports=imports,
272
+ )]
273
+
274
+
275
+ def _interpret_svelte(
276
+ file_path: str, matches: list, framework: str,
277
+ ) -> list[ComponentInfo]:
278
+ """Svelte: SFC with export let props, runes, stores."""
279
+ name = Path(file_path).stem
280
+ props: list[str] = []
281
+ hooks: list[str] = []
282
+ imports: list[str] = []
283
+
284
+ for _idx, caps in matches:
285
+ # export let prop (Svelte 4)
286
+ if "prop.name" in caps:
287
+ for pn in caps["prop.name"]:
288
+ p = node_text(pn)
289
+ if p not in props:
290
+ props.append(p)
291
+
292
+ # Svelte 5 runes ($state, $derived, etc.)
293
+ if "rune.name" in caps:
294
+ r = node_text(caps["rune.name"][0])
295
+ if r not in hooks:
296
+ hooks.append(r)
297
+
298
+ # Stores (writable, readable)
299
+ if "store.name" in caps:
300
+ s = node_text(caps["store.name"][0])
301
+ if s not in hooks:
302
+ hooks.append(s)
303
+
304
+ # Component name override
305
+ if "component.name" in caps:
306
+ name = node_text(caps["component.name"][0])
307
+
308
+ # Imports
309
+ if "import.path" in caps:
310
+ path = node_text(caps["import.path"][0]).strip("'\"")
311
+ if path not in imports:
312
+ imports.append(path)
313
+
314
+ return [ComponentInfo(
315
+ name=name,
316
+ source_file=file_path,
317
+ line=1,
318
+ framework=framework,
319
+ props=props,
320
+ hooks=hooks,
321
+ imports=imports,
322
+ )]
323
+
324
+
325
+ def _interpret_angular(
326
+ file_path: str, matches: list, framework: str,
327
+ ) -> list[ComponentInfo]:
328
+ """Angular: @Component({ selector, templateUrl }) class."""
329
+ components: list[ComponentInfo] = []
330
+ seen: set[str] = set()
331
+
332
+ for _idx, caps in matches:
333
+ if "component.class" in caps and "component.class_name" in caps:
334
+ name = node_text(caps["component.class_name"][0])
335
+ if name in seen:
336
+ continue
337
+ seen.add(name)
338
+ selector = node_text(caps["component.selector"][0]) if "component.selector" in caps else ""
339
+ node = caps["component.class"][0]
340
+ comp = ComponentInfo(
341
+ name=name,
342
+ source_file=file_path,
343
+ line=node.start_point[0] + 1,
344
+ framework="angular",
345
+ )
346
+ if selector:
347
+ comp.hooks.append(f"selector:{selector}")
348
+ components.append(comp)
349
+
350
+ # templateUrl capture (separate pattern)
351
+ if "component.template_url_decorator" in caps and "component.template_url" in caps:
352
+ template_url = node_text(caps["component.template_url"][0])
353
+ # Assign to last component in list
354
+ if components:
355
+ components[-1].imports.append(template_url)
356
+
357
+ return components
@@ -0,0 +1,140 @@
1
+ """Import / dependency graph extraction for all supported languages.
2
+
3
+ Public API:
4
+ extract_dependencies(file_path, framework) -> list[Edge]
5
+
6
+ Near-generic: every .scm query file uses `@import.path` captures.
7
+ This module runs any framework's query and collects all import.path captures,
8
+ returning Edge objects with relation="imports_from".
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from pathlib import Path
13
+
14
+ from codebeacon.common.types import Edge
15
+ from codebeacon.extract.base import (
16
+ extract_sfc_sections,
17
+ load_query_file,
18
+ node_text,
19
+ parse_file,
20
+ parse_sfc_script,
21
+ run_query,
22
+ )
23
+
24
+
25
+ # ── Framework → query file stem ───────────────────────────────────────────────
26
+
27
+ _FW_TO_QUERY: dict[str, str] = {
28
+ "spring-boot": "spring_boot",
29
+ "express": "express",
30
+ "koa": "express",
31
+ "fastify": "express",
32
+ "nestjs": "nestjs",
33
+ "nextjs": "react",
34
+ "react": "react",
35
+ "fastapi": "fastapi",
36
+ "django": "django",
37
+ "flask": "flask",
38
+ "gin": "gin",
39
+ "echo": "gin",
40
+ "fiber": "gin",
41
+ "go": "gin",
42
+ "rails": "rails",
43
+ "laravel": "laravel",
44
+ "aspnet": "aspnet",
45
+ "actix": "actix",
46
+ "axum": "actix",
47
+ "rust": "actix",
48
+ "vapor": "vapor",
49
+ "ktor": "ktor",
50
+ "vue": "vue",
51
+ "nuxt": "vue",
52
+ "sveltekit": "svelte",
53
+ "angular": "angular",
54
+ }
55
+
56
+
57
+ # ── Public function ───────────────────────────────────────────────────────────
58
+
59
+ def extract_dependencies(file_path: str, framework: str) -> list[Edge]:
60
+ """Extract import/require/use statements and return list[Edge] with relation='imports_from'.
61
+
62
+ Each Edge has:
63
+ - source: file_path (the file that contains the import)
64
+ - target: the imported path/module (raw string from the import statement)
65
+ - relation: "imports_from"
66
+ - confidence: "EXTRACTED"
67
+ - confidence_score: 1.0
68
+ - source_file: file_path
69
+ """
70
+ fw = framework.lower()
71
+ query_name = _FW_TO_QUERY.get(fw)
72
+ if not query_name:
73
+ return []
74
+
75
+ query_src = load_query_file(query_name)
76
+ if not query_src:
77
+ return []
78
+
79
+ # SFC dispatch
80
+ ext = Path(file_path).suffix.lower()
81
+ if ext in (".vue", ".svelte"):
82
+ sfc = extract_sfc_sections(file_path)
83
+ if sfc is None:
84
+ return []
85
+ parsed = parse_sfc_script(sfc)
86
+ else:
87
+ parsed = parse_file(file_path)
88
+
89
+ if parsed is None:
90
+ return []
91
+ root, lang = parsed
92
+
93
+ from codebeacon.extract.base import is_grammar_allowed
94
+ if not is_grammar_allowed(query_name, lang):
95
+ return []
96
+
97
+ try:
98
+ matches = run_query(lang, query_src, root)
99
+ except Exception:
100
+ return []
101
+
102
+ # Generic: collect all import.path captures across all patterns
103
+ edges: list[Edge] = []
104
+ seen: set[str] = set()
105
+
106
+ for _idx, caps in matches:
107
+ # All query files use @import.path for the imported module string
108
+ if "import.path" not in caps:
109
+ continue
110
+ for import_node in caps["import.path"]:
111
+ raw = node_text(import_node).strip("'\"` ")
112
+ if not raw or raw in seen:
113
+ continue
114
+ seen.add(raw)
115
+ edges.append(Edge(
116
+ source=file_path,
117
+ target=raw,
118
+ relation="imports_from",
119
+ confidence="EXTRACTED",
120
+ confidence_score=1.0,
121
+ source_file=file_path,
122
+ ))
123
+
124
+ # Vapor uses @import.name instead of @import.path
125
+ if "import.name" in caps:
126
+ for import_node in caps["import.name"]:
127
+ raw = node_text(import_node).strip()
128
+ if not raw or raw in seen:
129
+ continue
130
+ seen.add(raw)
131
+ edges.append(Edge(
132
+ source=file_path,
133
+ target=raw,
134
+ relation="imports_from",
135
+ confidence="EXTRACTED",
136
+ confidence_score=1.0,
137
+ source_file=file_path,
138
+ ))
139
+
140
+ return edges