codegraph-engine 3.0.0__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 (130) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/agent_brain.py +2212 -0
  4. codegraph/agent_capabilities.py +4058 -0
  5. codegraph/agent_rules.py +531 -0
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture.py +371 -0
  8. codegraph/async_queue/__init__.py +50 -0
  9. codegraph/async_queue/detectors/__init__.py +48 -0
  10. codegraph/async_queue/detectors/celery.py +160 -0
  11. codegraph/async_queue/detectors/kafka.py +168 -0
  12. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  13. codegraph/async_queue/detectors/redis.py +130 -0
  14. codegraph/async_queue/detectors/sqs.py +135 -0
  15. codegraph/async_queue/models.py +161 -0
  16. codegraph/async_queue/otel_models.py +74 -0
  17. codegraph/async_queue/otel_parser.py +337 -0
  18. codegraph/async_queue/reconciler.py +427 -0
  19. codegraph/async_queue/scanner.py +291 -0
  20. codegraph/async_queue/traversal.py +447 -0
  21. codegraph/audit.py +106 -0
  22. codegraph/binding_resolver.py +616 -0
  23. codegraph/branch_comparison.py +197 -0
  24. codegraph/cache.py +95 -0
  25. codegraph/change_impact.py +547 -0
  26. codegraph/cli.py +2436 -0
  27. codegraph/cli_output.py +205 -0
  28. codegraph/config.py +45 -0
  29. codegraph/constraints.py +238 -0
  30. codegraph/context.py +2182 -0
  31. codegraph/context_freshness.py +351 -0
  32. codegraph/database/__init__.py +72 -0
  33. codegraph/database/extractor.py +3651 -0
  34. codegraph/database/interrogation.py +1494 -0
  35. codegraph/database/models.py +310 -0
  36. codegraph/database/schema_drift.py +862 -0
  37. codegraph/epistemic.py +200 -0
  38. codegraph/errors.py +282 -0
  39. codegraph/evidence/__init__.py +15 -0
  40. codegraph/evidence/citations.py +418 -0
  41. codegraph/evidence_contract.py +486 -0
  42. codegraph/frameworks.py +960 -0
  43. codegraph/frameworks_nestjs.py +502 -0
  44. codegraph/frameworks_nextjs.py +337 -0
  45. codegraph/frameworks_react.py +457 -0
  46. codegraph/freshness.py +335 -0
  47. codegraph/frontend_html.py +109 -0
  48. codegraph/git.py +362 -0
  49. codegraph/git_state.py +545 -0
  50. codegraph/graph/__init__.py +46 -0
  51. codegraph/graph/models.py +56 -0
  52. codegraph/graph/traversal.py +1993 -0
  53. codegraph/indexing/__init__.py +24 -0
  54. codegraph/indexing/classifier.py +564 -0
  55. codegraph/indexing/indexer.py +2578 -0
  56. codegraph/indexing/models.py +427 -0
  57. codegraph/indexing/parser.py +3404 -0
  58. codegraph/indexing/pool.py +136 -0
  59. codegraph/indexing/scanner.py +211 -0
  60. codegraph/indexing/telemetry.py +326 -0
  61. codegraph/indexing/templates.py +418 -0
  62. codegraph/indexing/test_framework.py +116 -0
  63. codegraph/installer.py +1702 -0
  64. codegraph/interrogation.py +2355 -0
  65. codegraph/llm/__init__.py +3 -0
  66. codegraph/llm/base.py +15 -0
  67. codegraph/llm/context.py +20 -0
  68. codegraph/mcp/__init__.py +3 -0
  69. codegraph/mcp/server.py +1476 -0
  70. codegraph/mcp_diagnostics.py +882 -0
  71. codegraph/memory/__init__.py +3 -0
  72. codegraph/memory/store.py +46 -0
  73. codegraph/models.py +310 -0
  74. codegraph/monorepo.py +925 -0
  75. codegraph/observability.py +262 -0
  76. codegraph/optimizer.py +1014 -0
  77. codegraph/planner.py +417 -0
  78. codegraph/process_lifecycle.py +1153 -0
  79. codegraph/py.typed +1 -0
  80. codegraph/query_expansion.py +199 -0
  81. codegraph/ranking.py +408 -0
  82. codegraph/refactor/__init__.py +36 -0
  83. codegraph/refactor/models.py +114 -0
  84. codegraph/refactor/renamer.py +844 -0
  85. codegraph/refactor/transactions.py +414 -0
  86. codegraph/resolver.py +3968 -0
  87. codegraph/resources/__init__.py +46 -0
  88. codegraph/resources/cache.py +117 -0
  89. codegraph/resources/coalescer.py +83 -0
  90. codegraph/resources/debouncer.py +98 -0
  91. codegraph/resources/governor.py +219 -0
  92. codegraph/resources/monitor.py +102 -0
  93. codegraph/resources/policy.py +123 -0
  94. codegraph/retrieval_policy.py +320 -0
  95. codegraph/roblox.py +2174 -0
  96. codegraph/route_composer.py +363 -0
  97. codegraph/route_topology.py +172 -0
  98. codegraph/runtime/__init__.py +51 -0
  99. codegraph/runtime/collector.py +155 -0
  100. codegraph/runtime/db_watcher.py +191 -0
  101. codegraph/runtime/ingestor.py +692 -0
  102. codegraph/runtime/models.py +130 -0
  103. codegraph/runtime/port_inspector.py +304 -0
  104. codegraph/runtime/reconciliation.py +492 -0
  105. codegraph/runtime/runner.py +353 -0
  106. codegraph/runtime/trace_linker.py +175 -0
  107. codegraph/search/__init__.py +25 -0
  108. codegraph/search/hybrid.py +846 -0
  109. codegraph/search/semantic.py +28 -0
  110. codegraph/security/__init__.py +51 -0
  111. codegraph/security/paths.py +189 -0
  112. codegraph/security/redaction.py +633 -0
  113. codegraph/semantic_conflicts.py +270 -0
  114. codegraph/semantic_decorators.py +372 -0
  115. codegraph/structural_diff.py +542 -0
  116. codegraph/styles.py +144 -0
  117. codegraph/symbol_history.py +277 -0
  118. codegraph/target_resolver.py +348 -0
  119. codegraph/task.py +637 -0
  120. codegraph/tool_selection_eval.py +1158 -0
  121. codegraph/tsconfig.py +236 -0
  122. codegraph/ui/__init__.py +6 -0
  123. codegraph/ui/server.py +755 -0
  124. codegraph/watcher.py +323 -0
  125. codegraph_engine-3.0.0.dist-info/METADATA +868 -0
  126. codegraph_engine-3.0.0.dist-info/RECORD +130 -0
  127. codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
  128. codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
  129. codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
  130. codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,418 @@
1
+ """Deterministic HTML, Jinja2, and Web Template Parser.
2
+
3
+ Extracts:
4
+ 1. DOM elements with IDs or semantic anchors into Symbol records.
5
+ 2. Inline JavaScript event handlers (onclick="fn()", @click="fn") into CallRef and Symbol records.
6
+ 3. Form submission actions (<form action="/api/..." method="POST">) into BindingRef records.
7
+ 4. Script tag imports (<script src="...">) and stylesheet links into ImportRef records.
8
+ 5. Embedded <script> JavaScript blocks parsed with exact file line offsets.
9
+ 6. Jinja/Django/EJS template directives ({% block %}, {% extends %}, {% include %}).
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import hashlib
14
+ import re
15
+ from html.parser import HTMLParser
16
+
17
+ from codegraph.indexing.models import (
18
+ BindingRef,
19
+ CallRef,
20
+ ImportRef,
21
+ InheritanceRef,
22
+ Symbol,
23
+ normalize_module,
24
+ )
25
+ from codegraph.indexing.parser import PARSER_VERSION, ParseResult, _parse_js_ts
26
+
27
+ # Regex for extracting JS function calls in inline handlers like:
28
+ # onclick="quickToggleStock(123)", onsubmit="return handleForm(this, event)"
29
+ _JS_CALL_PATTERN = re.compile(r"\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(")
30
+
31
+ # Regex for Jinja2 / Django / EJS template directives
32
+ _JINJA_EXTENDS = re.compile(r"""\{%\s*extends\s+['"]([^'"]+)['"]\s*%\}""")
33
+ _JINJA_INCLUDE = re.compile(r"""\{%\s*include\s+['"]([^'"]+)['"]\s*%\}""")
34
+ _JINJA_BLOCK = re.compile(r"""\{%\s*block\s+([a-zA-Z0-9_]+)\s*%\}""")
35
+ _JINJA_URL_FOR = re.compile(r"""\{\{\s*url_for\s*\(\s*['"]([^'"]+)['"]""")
36
+ _DJANGO_URL = re.compile(r"""\{%\s*url\s+['"]([^'"]+)['"]""")
37
+
38
+ # HTMX HTTP request attribute mappings
39
+ _HTMX_VERBS = {
40
+ "hx-get": "GET",
41
+ "hx-post": "POST",
42
+ "hx-put": "PUT",
43
+ "hx-delete": "DELETE",
44
+ "hx-patch": "PATCH",
45
+ "data-hx-get": "GET",
46
+ "data-hx-post": "POST",
47
+ "data-hx-put": "PUT",
48
+ "data-hx-delete": "DELETE",
49
+ "data-hx-patch": "PATCH",
50
+ }
51
+
52
+
53
+ class _TemplateHTMLParser(HTMLParser):
54
+ def __init__(self, file_path: str, module_name: str) -> None:
55
+ super().__init__()
56
+ self.file_path = file_path
57
+ self.module_name = module_name
58
+ self.symbols: list[Symbol] = []
59
+ self.calls: list[CallRef] = []
60
+ self.imports: list[ImportRef] = []
61
+ self.bindings: list[BindingRef] = []
62
+ self.script_blocks: list[tuple[int, str]] = [] # (start_line, content)
63
+ self._in_script = False
64
+ self._script_start_line = 1
65
+ self._script_buf: list[str] = []
66
+
67
+ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
68
+ line_num, _ = self.getpos()
69
+ attr_dict: dict[str, str] = {k.lower(): (v or "") for k, v in attrs if k}
70
+
71
+ if tag.lower() == "script":
72
+ src = attr_dict.get("src")
73
+ if src:
74
+ self.imports.append(
75
+ ImportRef(
76
+ source_file=self.file_path,
77
+ module=src,
78
+ line=line_num,
79
+ import_type="script_src",
80
+ )
81
+ )
82
+ else:
83
+ self._in_script = True
84
+ self._script_start_line = line_num
85
+ self._script_buf = []
86
+
87
+ if tag.lower() == "link" and attr_dict.get("rel") == "stylesheet":
88
+ href = attr_dict.get("href")
89
+ if href:
90
+ self.imports.append(
91
+ ImportRef(
92
+ source_file=self.file_path,
93
+ module=href,
94
+ line=line_num,
95
+ import_type="stylesheet",
96
+ )
97
+ )
98
+
99
+ # 1. DOM Elements with ID
100
+ elem_id = attr_dict.get("id")
101
+ if elem_id:
102
+ sym_name = elem_id.strip()
103
+ self.symbols.append(
104
+ Symbol(
105
+ name=sym_name,
106
+ qualified_name=f"{tag}#{sym_name}",
107
+ kind="html_element",
108
+ language="html",
109
+ file_path=self.file_path,
110
+ start_line=line_num,
111
+ end_line=line_num,
112
+ scope=tag,
113
+ )
114
+ )
115
+
116
+ # 2. Inline JavaScript / Alpine.js Event Handlers (onclick, onsubmit, @click, x-on:click, x-init, etc.)
117
+ for attr_k, attr_v in attr_dict.items():
118
+ is_handler = (
119
+ attr_k.startswith("on")
120
+ or attr_k.startswith("@")
121
+ or attr_k.startswith("v-on:")
122
+ or attr_k.startswith("x-on:")
123
+ or attr_k in ("x-init", "x-effect")
124
+ or (attr_k.startswith("(") and attr_k.endswith(")"))
125
+ )
126
+ if not is_handler or not attr_v:
127
+ continue
128
+
129
+ if attr_k.startswith(("@", "x-on:", "x-data", "x-init", "x-show")):
130
+ caller_label = f"{tag}#{elem_id}" if elem_id else tag
131
+ self.bindings.append(
132
+ BindingRef(
133
+ target_name=attr_k,
134
+ file_path=self.file_path,
135
+ line=line_num,
136
+ expr_kind="ALPINE_EVENT",
137
+ source_expr=attr_v,
138
+ base_expr=caller_label,
139
+ )
140
+ )
141
+
142
+ matches = _JS_CALL_PATTERN.findall(attr_v)
143
+ found_fns: list[str] = list(matches)
144
+
145
+ # If attribute value is a bare identifier / method reference (e.g. @click="handleClick" or onclick="handleClick")
146
+ val_clean = attr_v.strip()
147
+ if not found_fns and re.match(r"^[a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)?$", val_clean):
148
+ found_fns.append(val_clean.split(".")[-1])
149
+
150
+ for fn_name in found_fns:
151
+ if fn_name in ("return", "if", "for", "while", "this"):
152
+ continue
153
+
154
+ caller_label = f"{tag}#{elem_id}" if elem_id else tag
155
+ self.calls.append(
156
+ CallRef(
157
+ callee=fn_name,
158
+ source_file=self.file_path,
159
+ line=line_num,
160
+ end_line=line_num,
161
+ caller_symbol=caller_label,
162
+ caller_canonical_id=f"{self.module_name}.{caller_label}",
163
+ confidence="HIGH",
164
+ )
165
+ )
166
+
167
+ self.symbols.append(
168
+ Symbol(
169
+ name=fn_name,
170
+ qualified_name=f"{caller_label}[{attr_k}]->{fn_name}",
171
+ kind="event_handler",
172
+ language="html",
173
+ file_path=self.file_path,
174
+ start_line=line_num,
175
+ end_line=line_num,
176
+ scope=caller_label,
177
+ )
178
+ )
179
+
180
+ # 3. Form submissions (<form action="..." method="...">)
181
+ if tag.lower() == "form":
182
+ action = attr_dict.get("action", "").strip()
183
+ method = attr_dict.get("method", "GET").strip().upper()
184
+ if action:
185
+ self.bindings.append(
186
+ BindingRef(
187
+ target_name=action,
188
+ file_path=self.file_path,
189
+ line=line_num,
190
+ expr_kind="HTML_FORM_ACTION",
191
+ base_expr=method,
192
+ )
193
+ )
194
+
195
+ # 4. Modern Python Stack: HTMX attributes (hx-get, hx-post, hx-delete, hx-put, hx-patch, etc.)
196
+ for hx_attr, http_method in _HTMX_VERBS.items():
197
+ if hx_attr in attr_dict and attr_dict[hx_attr]:
198
+ raw_endpoint = attr_dict[hx_attr].strip()
199
+ caller_label = f"{tag}#{elem_id}" if elem_id else tag
200
+ target_sel = attr_dict.get("hx-target") or attr_dict.get("data-hx-target") or ""
201
+ trigger = attr_dict.get("hx-trigger") or attr_dict.get("data-hx-trigger") or ""
202
+ swap = attr_dict.get("hx-swap") or attr_dict.get("data-hx-swap") or ""
203
+
204
+ resolved_endpoint = raw_endpoint
205
+ m_django = _DJANGO_URL.search(raw_endpoint)
206
+ if m_django:
207
+ resolved_endpoint = m_django.group(1)
208
+ else:
209
+ m_jinja = _JINJA_URL_FOR.search(raw_endpoint)
210
+ if m_jinja:
211
+ resolved_endpoint = m_jinja.group(1)
212
+
213
+ meta_entries = (
214
+ ("target", target_sel),
215
+ ("trigger", trigger),
216
+ ("swap", swap),
217
+ ("element", tag),
218
+ ("element_id", elem_id or ""),
219
+ ("raw_url", raw_endpoint),
220
+ )
221
+
222
+ self.bindings.append(
223
+ BindingRef(
224
+ target_name=resolved_endpoint,
225
+ file_path=self.file_path,
226
+ line=line_num,
227
+ expr_kind="HTMX_REQUEST",
228
+ base_expr=http_method,
229
+ source_expr=target_sel,
230
+ attr_name=trigger,
231
+ dict_entries=meta_entries,
232
+ )
233
+ )
234
+
235
+ self.calls.append(
236
+ CallRef(
237
+ callee=resolved_endpoint,
238
+ qualified_callee=f"{http_method} {resolved_endpoint}",
239
+ source_file=self.file_path,
240
+ line=line_num,
241
+ end_line=line_num,
242
+ caller_symbol=caller_label,
243
+ caller_canonical_id=f"{self.module_name}.{caller_label}",
244
+ confidence="HIGH",
245
+ )
246
+ )
247
+
248
+ self.symbols.append(
249
+ Symbol(
250
+ name=resolved_endpoint,
251
+ qualified_name=f"{caller_label}[{hx_attr}]->{http_method} {resolved_endpoint}",
252
+ kind="htmx_action",
253
+ language="html",
254
+ file_path=self.file_path,
255
+ start_line=line_num,
256
+ end_line=line_num,
257
+ scope=caller_label,
258
+ )
259
+ )
260
+
261
+ def handle_endtag(self, tag: str) -> None:
262
+ if tag.lower() == "script" and self._in_script:
263
+ self._in_script = False
264
+ content = "".join(self._script_buf)
265
+ if content.strip():
266
+ self.script_blocks.append((self._script_start_line, content))
267
+ self._script_buf = []
268
+
269
+ def handle_data(self, data: str) -> None:
270
+ if self._in_script:
271
+ self._script_buf.append(data)
272
+
273
+
274
+ def parse_html_template(content: str, file_path: str) -> ParseResult:
275
+ """Parse an HTML or web template file in a single deterministic pass."""
276
+ source_hash = hashlib.sha256(content.encode("utf-8", errors="replace")).hexdigest()
277
+ module_name = normalize_module(file_path, "html")
278
+
279
+ parser = _TemplateHTMLParser(file_path=file_path, module_name=module_name)
280
+ try:
281
+ parser.feed(content)
282
+ except Exception as e:
283
+ return ParseResult(
284
+ path=file_path,
285
+ language="html",
286
+ parser_version=PARSER_VERSION,
287
+ source_hash=source_hash,
288
+ parse_failed=True,
289
+ parse_error=f"HTML parse error: {e}",
290
+ )
291
+
292
+ symbols = list(parser.symbols)
293
+ calls = list(parser.calls)
294
+ imports = list(parser.imports)
295
+ inheritance: list[InheritanceRef] = []
296
+ bindings = list(parser.bindings)
297
+
298
+ # 4. Parse Jinja / Django directives line-by-line
299
+ lines = content.splitlines()
300
+ for idx, line in enumerate(lines, 1):
301
+ # {% extends "base.html" %}
302
+ ext_match = _JINJA_EXTENDS.search(line)
303
+ if ext_match:
304
+ inheritance.append(
305
+ InheritanceRef(
306
+ source_symbol=module_name,
307
+ base_name=ext_match.group(1),
308
+ relationship="EXTENDS",
309
+ source_file=file_path,
310
+ line=idx,
311
+ )
312
+ )
313
+
314
+ # {% include "nav.html" %}
315
+ inc_match = _JINJA_INCLUDE.search(line)
316
+ if inc_match:
317
+ imports.append(
318
+ ImportRef(
319
+ source_file=file_path,
320
+ module=inc_match.group(1),
321
+ line=idx,
322
+ import_type="template_include",
323
+ )
324
+ )
325
+
326
+ # {% block content %}
327
+ blk_match = _JINJA_BLOCK.search(line)
328
+ if blk_match:
329
+ blk_name = blk_match.group(1)
330
+ symbols.append(
331
+ Symbol(
332
+ name=blk_name,
333
+ qualified_name=f"block#{blk_name}",
334
+ kind="template_block",
335
+ language="html",
336
+ file_path=file_path,
337
+ start_line=idx,
338
+ end_line=idx,
339
+ scope=module_name,
340
+ )
341
+ )
342
+
343
+ # {{ url_for('user_bp.checkout') }}
344
+ url_match = _JINJA_URL_FOR.search(line)
345
+ if url_match:
346
+ target_endpoint = url_match.group(1)
347
+ calls.append(
348
+ CallRef(
349
+ callee=target_endpoint,
350
+ source_file=file_path,
351
+ line=idx,
352
+ end_line=idx,
353
+ caller_symbol=module_name,
354
+ caller_canonical_id=module_name,
355
+ confidence="HIGH",
356
+ )
357
+ )
358
+
359
+ # 5. Parse embedded <script> tags with JS parser
360
+ for start_line, script_code in parser.script_blocks:
361
+ try:
362
+ js_res = _parse_js_ts(script_code, "javascript", file_path)
363
+ # Offset line numbers by start_line - 1
364
+ line_offset = max(0, start_line - 1)
365
+ for s in js_res.symbols:
366
+ adj_start = s.start_line + line_offset
367
+ adj_end = s.end_line + line_offset
368
+ symbols.append(
369
+ Symbol(
370
+ name=s.name,
371
+ qualified_name=s.qualified_name,
372
+ kind=s.kind,
373
+ language="javascript",
374
+ file_path=file_path,
375
+ start_line=adj_start,
376
+ end_line=adj_end,
377
+ signature=s.signature,
378
+ scope=s.scope,
379
+ )
380
+ )
381
+ for c in js_res.calls:
382
+ calls.append(
383
+ CallRef(
384
+ callee=c.callee,
385
+ source_file=file_path,
386
+ line=c.line + line_offset,
387
+ end_line=c.end_line + line_offset,
388
+ caller_symbol=c.caller_symbol,
389
+ caller_canonical_id=c.caller_canonical_id,
390
+ confidence=c.confidence,
391
+ )
392
+ )
393
+ for imp in js_res.imports:
394
+ imports.append(
395
+ ImportRef(
396
+ source_file=file_path,
397
+ module=imp.module,
398
+ name=imp.name,
399
+ line=imp.line + line_offset,
400
+ import_type=imp.import_type,
401
+ )
402
+ )
403
+ except Exception:
404
+ # Tolerant fallback if inline script cannot be parsed
405
+ pass
406
+
407
+ return ParseResult(
408
+ path=file_path,
409
+ language="html",
410
+ parser_version=PARSER_VERSION,
411
+ source_hash=source_hash,
412
+ symbols=tuple(symbols),
413
+ imports=tuple(imports),
414
+ calls=tuple(calls),
415
+ inheritance=tuple(inheritance),
416
+ bindings=tuple(bindings),
417
+ parse_failed=False,
418
+ )
@@ -0,0 +1,116 @@
1
+ """Source-backed test framework detection.
2
+
3
+ Detects test frameworks (PYTEST, UNITTEST, JEST, VITEST, MOCHA, OTHER, UNKNOWN)
4
+ using concrete repository facts (imports, class inheritance, configuration files).
5
+ Does not guess or assume tests exist merely because 'test' appears in a path.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import sqlite3
11
+ from enum import StrEnum
12
+ from pathlib import Path
13
+
14
+
15
+ class TestFramework(StrEnum):
16
+ PYTEST = "PYTEST"
17
+ UNITTEST = "UNITTEST"
18
+ JEST = "JEST"
19
+ VITEST = "VITEST"
20
+ MOCHA = "MOCHA"
21
+ OTHER = "OTHER"
22
+ UNKNOWN = "UNKNOWN"
23
+
24
+
25
+ def detect_test_framework(
26
+ con: sqlite3.Connection,
27
+ repository: Path,
28
+ ) -> tuple[TestFramework, bool, str]:
29
+ """Detect test framework and whether tests are present in the repository.
30
+
31
+ Returns:
32
+ (framework, tests_present, evidence)
33
+ """
34
+ root = repository.resolve()
35
+
36
+ # 1. Query files for test files
37
+ test_rows = []
38
+ try:
39
+ test_rows = con.execute(
40
+ "SELECT path FROM files WHERE (category='TEST' OR path LIKE 'tests/%' "
41
+ "OR path LIKE 'test/%' OR path LIKE '%/test_%.py' OR path LIKE '%_test.py' "
42
+ "OR path LIKE '%.test.%' OR path LIKE '%.spec.%') AND status='ok'"
43
+ ).fetchall()
44
+ except sqlite3.OperationalError:
45
+ pass
46
+
47
+ test_paths = [r[0] for r in test_rows]
48
+
49
+ # If no test files in index, check filesystem as fallback
50
+ if not test_paths:
51
+ for pat in ("tests", "test"):
52
+ d = root / pat
53
+ if d.is_dir() and any(d.iterdir()):
54
+ test_paths.append(pat)
55
+ break
56
+
57
+ if not test_paths:
58
+ return (TestFramework.UNKNOWN, False, "No test files or directories detected")
59
+
60
+ # 2. Check Python imports from test files
61
+ try:
62
+ imp_rows = con.execute(
63
+ "SELECT DISTINCT module FROM imports WHERE module IN ('pytest', 'unittest') "
64
+ "OR module LIKE 'pytest.%' OR module LIKE 'unittest.%'"
65
+ ).fetchall()
66
+ imp_modules = {r[0] for r in imp_rows}
67
+ if any(m.startswith("pytest") for m in imp_modules):
68
+ return (TestFramework.PYTEST, True, "Source evidence: imports pytest")
69
+ if any(m.startswith("unittest") for m in imp_modules):
70
+ return (TestFramework.UNITTEST, True, "Source evidence: imports unittest")
71
+ except sqlite3.OperationalError:
72
+ pass
73
+
74
+ # 3. Check Python configuration files
75
+ if (root / "pytest.ini").is_file() or (root / "conftest.py").is_file():
76
+ return (TestFramework.PYTEST, True, "Configuration evidence: pytest.ini or conftest.py present")
77
+
78
+ pyproject = root / "pyproject.toml"
79
+ if pyproject.is_file():
80
+ try:
81
+ content = pyproject.read_text(encoding="utf-8", errors="replace")
82
+ if "tool.pytest" in content or "pytest" in content:
83
+ return (TestFramework.PYTEST, True, "Configuration evidence: pyproject.toml defines pytest")
84
+ except OSError:
85
+ pass
86
+
87
+ # 4. Check JS/TS package.json
88
+ pkg_json = root / "package.json"
89
+ if pkg_json.is_file():
90
+ try:
91
+ data = json.loads(pkg_json.read_text(encoding="utf-8", errors="replace"))
92
+ dev_deps = data.get("devDependencies", {})
93
+ deps = data.get("dependencies", {})
94
+ scripts = data.get("scripts", {})
95
+ all_deps = {**deps, **dev_deps}
96
+ scripts_str = " ".join(str(v) for v in scripts.values())
97
+
98
+ if "vitest" in all_deps or "vitest" in scripts_str:
99
+ return (TestFramework.VITEST, True, "Package evidence: vitest declared in package.json")
100
+ if "jest" in all_deps or "jest" in scripts_str:
101
+ return (TestFramework.JEST, True, "Package evidence: jest declared in package.json")
102
+ if "mocha" in all_deps or "mocha" in scripts_str:
103
+ return (TestFramework.MOCHA, True, "Package evidence: mocha declared in package.json")
104
+ except (OSError, json.JSONDecodeError):
105
+ pass
106
+
107
+ # 5. Check config files for JS test runners
108
+ for f in root.glob("jest.config.*"):
109
+ if f.is_file():
110
+ return (TestFramework.JEST, True, f"Configuration evidence: {f.name} present")
111
+ for f in root.glob("vitest.config.*"):
112
+ if f.is_file():
113
+ return (TestFramework.VITEST, True, f"Configuration evidence: {f.name} present")
114
+
115
+ # 6. Fallback if test files exist but specific runner not identified
116
+ return (TestFramework.OTHER, True, f"Test files present ({len(test_paths)} test files) with unspecified runner")