codeui-python 0.1.0__py3-none-any.whl → 0.1.1__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 (49) hide show
  1. codeui/__init__.py +1 -1
  2. codeui/agent/context.py +19 -3
  3. codeui/analysis/dead_code.py +13 -2
  4. codeui/analysis/undefined.py +70 -0
  5. codeui/analysis/unresolved_import.py +34 -4
  6. codeui/cli/main.py +101 -7
  7. codeui/core/cache.py +54 -0
  8. codeui/core/graph.py +233 -3
  9. codeui/core/override.py +1 -0
  10. codeui/core/resolver.py +51 -37
  11. codeui/errors.py +10 -0
  12. codeui/lang/base.py +5 -2
  13. codeui/lang/cpp.py +271 -0
  14. codeui/lang/csharp.py +262 -0
  15. codeui/lang/dart.py +278 -0
  16. codeui/lang/generic.py +6 -4
  17. codeui/lang/go.py +5 -2
  18. codeui/lang/java.py +320 -0
  19. codeui/lang/kotlin.py +303 -0
  20. codeui/lang/php.py +275 -0
  21. codeui/lang/python.py +49 -7
  22. codeui/lang/registry.py +22 -2
  23. codeui/lang/ruby.py +270 -0
  24. codeui/lang/rust.py +5 -2
  25. codeui/lang/scala.py +290 -0
  26. codeui/lang/swift.py +276 -0
  27. codeui/lang/ts.py +99 -8
  28. codeui/report/sarif_emitter.py +2 -1
  29. codeui/server/server.py +156 -85
  30. codeui/server/static/css/styles.css +1609 -0
  31. codeui/server/static/index.html +199 -0
  32. codeui/server/static/js/api.js +485 -0
  33. codeui/server/static/js/app.js +1044 -0
  34. codeui/server/static/js/graph_canvas.js +1485 -0
  35. codeui/server/static/js/minimap.js +383 -0
  36. codeui/server/static/js/sidebar.js +268 -0
  37. codeui/server/static/js/state.js +83 -0
  38. codeui/server/static/js/subgraph_window.js +1208 -0
  39. codeui/server/static/js/theme.js +332 -0
  40. codeui/tracer/__init__.py +4 -1
  41. codeui/tracer/tracer.py +109 -118
  42. codeui_python-0.1.1.dist-info/METADATA +171 -0
  43. codeui_python-0.1.1.dist-info/RECORD +71 -0
  44. {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/licenses/LICENSE +1 -1
  45. codeui_python-0.1.0.dist-info/METADATA +0 -191
  46. codeui_python-0.1.0.dist-info/RECORD +0 -52
  47. {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/WHEEL +0 -0
  48. {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/entry_points.txt +0 -0
  49. {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/top_level.txt +0 -0
codeui/__init__.py CHANGED
@@ -4,7 +4,7 @@ from codeui.core.graph import Graph
4
4
  from codeui.core.override import Override, OverrideKind, OverrideStore
5
5
  from codeui.core.repo import get_library_repo_url
6
6
 
7
- __version__ = "0.1.0"
7
+ __version__ = "0.1.1"
8
8
 
9
9
  __all__ = [
10
10
  "Location",
codeui/agent/context.py CHANGED
@@ -110,7 +110,9 @@ class ContextBuilder:
110
110
 
111
111
  relevant_findings = [
112
112
  f.to_dict() for f in self.graph.get_findings()
113
- if f.symbol_id in {s.id for s in syms} or f.location.file_id == target_sym.location.file_id
113
+ if (f.symbol_id and f.symbol_id in {s.id for s in syms}) or (
114
+ f.location and target_sym.location and f.location.file_id == target_sym.location.file_id
115
+ )
114
116
  ]
115
117
 
116
118
  manifest = {
@@ -147,7 +149,7 @@ class ContextBuilder:
147
149
  bodies[s.id] = f"{cm} {file_id}:{s.location.start_line if s.location else 1}\n{s.signature or s.name}"
148
150
  relevant_findings = [
149
151
  f.to_dict() for f in self.graph.get_findings()
150
- if f.location.file_id == file_id
152
+ if f.location and f.location.file_id == file_id
151
153
  ]
152
154
  return ContextBundle(
153
155
  symbol_bodies=bodies,
@@ -189,6 +191,18 @@ class ContextBuilder:
189
191
  manifest={"query": task.query, "symbols": task.target_symbols, "files": task.target_files},
190
192
  )
191
193
 
194
+ def build_context(self, task: TaskSpec, depth: int = 1) -> ContextBundle:
195
+ """Alias for for_task to build context bundle.
196
+ Example:
197
+ >>> g = Graph()
198
+ >>> builder = ContextBuilder(g)
199
+ >>> spec = TaskSpec("Fix issue")
200
+ >>> bundle = builder.build_context(spec)
201
+ >>> "query" in bundle.manifest
202
+ True
203
+ """
204
+ return self.for_task(task, depth=depth)
205
+
192
206
  def for_defect(self, finding_id: str) -> ContextBundle:
193
207
  """Build context bundle targeting a specific defect finding.
194
208
  Example:
@@ -204,4 +218,6 @@ class ContextBuilder:
204
218
  f = matching[0]
205
219
  if f.symbol_id:
206
220
  return self.for_symbol(f.symbol_id, depth=1)
207
- return self.for_file(f.location.file_id, depth=1)
221
+ if f.location and f.location.file_id:
222
+ return self.for_file(f.location.file_id, depth=1)
223
+ return ContextBundle({}, {}, [], [], {"finding_id": finding_id, "status": "no_location"})
@@ -3,7 +3,7 @@ from pathlib import Path
3
3
  from typing import ClassVar, Iterable, List, Set
4
4
  from codeui.analysis.base import AnalysisContext, Analyzer
5
5
  from codeui.core.graph import Graph
6
- from codeui.core.ir import EdgeKind, Finding, Location, Severity, SymbolKind, Visibility
6
+ from codeui.core.ir import EdgeKind, Finding, LayerKind, Location, Severity, SymbolKind, Visibility
7
7
 
8
8
  class DeadCodeAnalyzer(Analyzer):
9
9
  """Detects unused functions, unused private symbols, and orphan files.
@@ -22,7 +22,10 @@ class DeadCodeAnalyzer(Analyzer):
22
22
  "__init__", "__post_init__", "__str__", "__repr__", "__eq__", "__hash__",
23
23
  "__iter__", "__getitem__", "__setitem__", "__delitem__", "__len__",
24
24
  "__contains__", "__enter__", "__exit__", "__call__", "__await__",
25
- "constructor", "init", "dispose", "render", "setup", "teardown", "setUp", "tearDown"
25
+ "constructor", "init", "dispose", "render", "build", "initialize",
26
+ "setup", "teardown", "setUp", "tearDown", "beforeEach", "afterEach",
27
+ "beforeAll", "afterAll", "onCreate", "onDestroy", "onStart", "onStop",
28
+ "onResume", "onPause", "componentDidMount", "componentWillUnmount"
26
29
  }
27
30
 
28
31
  def run(self, graph: Graph, ctx: AnalysisContext) -> Iterable[Finding]:
@@ -43,6 +46,14 @@ class DeadCodeAnalyzer(Analyzer):
43
46
  referenced_targets.add(raw_target.split(".")[-1])
44
47
 
45
48
  for sym in graph.get_all_symbols():
49
+ if sym.location and sym.location.file_id:
50
+ cls = graph.get_file_classification(sym.location.file_id)
51
+ if cls.layer in (LayerKind.TEST, LayerKind.CONFIG):
52
+ continue
53
+ file_lower = sym.location.file_id.replace("\\", "/").lower()
54
+ if any(p in file_lower for p in ("/tests/", "/test/", "/benches/", "/bench/", "/examples/", "/example/", "/fixtures/")):
55
+ continue
56
+
46
57
  if sym.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD, SymbolKind.CLASS):
47
58
  if sym.name in self.ENTRY_POINTS or any(sym.name.startswith(p) for p in self.ENTRY_POINTS):
48
59
  continue
@@ -76,10 +76,80 @@ class UndefinedSymbolAnalyzer(Analyzer):
76
76
  if "." in target_name:
77
77
  continue
78
78
 
79
+ local_names = set()
80
+ try:
81
+ caller_sym = graph.get_symbol(edge.source_id)
82
+ if caller_sym and caller_sym.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD):
83
+ file_path = ctx.root_dir / caller_sym.location.file_id
84
+ if file_path.exists():
85
+ import ast
86
+ source = file_path.read_text(encoding="utf-8")
87
+ tree = ast.parse(source)
88
+ for node in ast.walk(tree):
89
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.lineno == caller_sym.location.start_line:
90
+ for arg in getattr(node.args, "posonlyargs", []) + getattr(node.args, "args", []) + getattr(node.args, "kwonlyargs", []):
91
+ if hasattr(arg, "arg"):
92
+ local_names.add(arg.arg)
93
+ if getattr(node.args, "vararg", None):
94
+ local_names.add(node.args.vararg.arg)
95
+ if getattr(node.args, "kwarg", None):
96
+ local_names.add(node.args.kwarg.arg)
97
+
98
+ for subnode in ast.walk(node):
99
+ if isinstance(subnode, ast.Assign):
100
+ for target in subnode.targets:
101
+ if isinstance(target, ast.Name):
102
+ local_names.add(target.id)
103
+ elif isinstance(target, (ast.Tuple, ast.List)):
104
+ def _ext_names(t):
105
+ res = set()
106
+ if isinstance(t, ast.Name): res.add(t.id)
107
+ elif isinstance(t, (ast.Tuple, ast.List)):
108
+ for e in t.elts: res.update(_ext_names(e))
109
+ return res
110
+ local_names.update(_ext_names(target))
111
+ elif isinstance(subnode, ast.AnnAssign):
112
+ if isinstance(subnode.target, ast.Name):
113
+ local_names.add(subnode.target.id)
114
+ elif isinstance(subnode, (ast.For, ast.AsyncFor)):
115
+ def _ext_names(t):
116
+ res = set()
117
+ if isinstance(t, ast.Name): res.add(t.id)
118
+ elif isinstance(t, (ast.Tuple, ast.List)):
119
+ for e in t.elts: res.update(_ext_names(e))
120
+ return res
121
+ local_names.update(_ext_names(subnode.target))
122
+ elif isinstance(subnode, (ast.With, ast.AsyncWith)):
123
+ for item in subnode.items:
124
+ if item.optional_vars:
125
+ def _ext_names(t):
126
+ res = set()
127
+ if isinstance(t, ast.Name): res.add(t.id)
128
+ elif isinstance(t, (ast.Tuple, ast.List)):
129
+ for e in t.elts: res.update(_ext_names(e))
130
+ return res
131
+ local_names.update(_ext_names(item.optional_vars))
132
+ elif isinstance(subnode, ast.ExceptHandler):
133
+ if subnode.name:
134
+ local_names.add(subnode.name)
135
+ elif isinstance(subnode, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
136
+ for gen in subnode.generators:
137
+ def _ext_names(t):
138
+ res = set()
139
+ if isinstance(t, ast.Name): res.add(t.id)
140
+ elif isinstance(t, (ast.Tuple, ast.List)):
141
+ for e in t.elts: res.update(_ext_names(e))
142
+ return res
143
+ local_names.update(_ext_names(gen.target))
144
+ break
145
+ except Exception:
146
+ pass
147
+
79
148
  if (
80
149
  not graph.has_symbol(target_id)
81
150
  and target_name not in all_symbol_names
82
151
  and target_name not in file_imports.get(src_file, set())
152
+ and target_name not in local_names
83
153
  and not target_name.startswith("_")
84
154
  ):
85
155
  loc = edge.location or Location(edge.source_id, 1, 0, 1, 0)
@@ -58,8 +58,20 @@ class UnresolvedImportAnalyzer(Analyzer):
58
58
  known_deps.add(stem)
59
59
  known_deps.add(p)
60
60
 
61
+ top_level_items = set()
61
62
  if ctx and ctx.root_dir:
62
63
  root = ctx.root_dir
64
+ try:
65
+ for item in root.iterdir():
66
+ top_level_items.add(item.name)
67
+ top_level_items.add(item.stem)
68
+ if item.name == "src" and item.is_dir():
69
+ for subitem in item.iterdir():
70
+ top_level_items.add(subitem.name)
71
+ top_level_items.add(subitem.stem)
72
+ except Exception:
73
+ pass
74
+
63
75
  for manifest_name in ("package.json", "requirements.txt", "pyproject.toml", "setup.py", "Pipfile", "go.mod", "Cargo.toml"):
64
76
  mpath = root / manifest_name
65
77
  if mpath.exists():
@@ -104,17 +116,35 @@ class UnresolvedImportAnalyzer(Analyzer):
104
116
  or any(raw_mod in f or base_mod in f for f in all_files)
105
117
  )
106
118
 
119
+ is_internal = False
120
+ if base_mod in top_level_items:
121
+ is_internal = True
122
+ else:
123
+ for f in all_files:
124
+ f_clean = f.replace("\\", "/")
125
+ if f_clean.startswith(base_mod + "/") or f_clean == base_mod or ("/" + base_mod + "/") in f_clean:
126
+ is_internal = True
127
+ break
128
+
129
+ if not is_internal and base_mod != "@" and base_mod != "":
130
+ is_known = True
131
+
107
132
  if not is_known and raw_mod.startswith("@/"):
108
133
  as_src = "src/" + raw_mod[2:]
109
134
  as_root = raw_mod[2:]
110
135
  if any(as_src in f or as_root in f for f in all_files):
111
136
  is_known = True
112
137
 
113
- if not is_known and hasattr(ctx, "resolve_path"):
138
+ if not is_known:
114
139
  src_file = edge.source_id.split("::")[0]
115
- resolved = ctx.resolve_path(src_file, raw_mod)
116
- if resolved:
117
- is_known = True
140
+ if hasattr(ctx, "resolve_path"):
141
+ resolved = ctx.resolve_path(src_file, raw_mod)
142
+ if resolved:
143
+ is_known = True
144
+ elif hasattr(ctx, "resolve_context") and ctx.resolve_context and hasattr(ctx.resolve_context, "resolve_path"):
145
+ resolved = ctx.resolve_context.resolve_path(src_file, raw_mod)
146
+ if resolved:
147
+ is_known = True
118
148
 
119
149
  if not is_known and hasattr(ctx, "alias_mappings"):
120
150
  for alias, target in getattr(ctx, "alias_mappings", {}).items():
codeui/cli/main.py CHANGED
@@ -27,6 +27,7 @@ def scan_project(
27
27
  project_path: Union[str, Path],
28
28
  entry_points: Optional[Sequence[str]] = None,
29
29
  layer_rules: Optional[Dict[str, Any]] = None,
30
+ clean_cache: bool = False,
30
31
  ) -> Graph:
31
32
  """Scan a project directory or remote Git repository and build the universal graph.
32
33
  Example:
@@ -46,9 +47,12 @@ def scan_project(
46
47
  graph = Graph()
47
48
  registry = LanguageRegistry()
48
49
  ctx = ResolveContext(resolved_project_path)
50
+ ctx.set_supported_extensions(registry.get_all_extensions())
49
51
  cache_db_path = resolved_project_path / ".codeui" / "cache.db"
50
52
  try:
51
53
  cache = AnalysisCache(cache_db_path)
54
+ if clean_cache:
55
+ cache.clear()
52
56
  except Exception:
53
57
  cache = None
54
58
 
@@ -96,6 +100,13 @@ def scan_project(
96
100
  continue
97
101
  all_files.append(p)
98
102
 
103
+ if cache:
104
+ valid_rel_paths = {
105
+ str(p.relative_to(resolved_project_path)).replace("\\", "/")
106
+ for p in all_files
107
+ }
108
+ cache.prune_stale(valid_rel_paths)
109
+
99
110
  parsed_files: Set[str] = set()
100
111
  raw_edges: List[Edge] = []
101
112
  for p in all_files:
@@ -196,6 +207,56 @@ def scan_project(
196
207
  if temp_clone_obj:
197
208
  temp_clone_obj.cleanup()
198
209
 
210
+ def export_pages(
211
+ project_path: Union[str, Path] = ".",
212
+ out_dir: Union[str, Path] = "dist",
213
+ entry_points: Optional[Sequence[str]] = None,
214
+ clean_cache: bool = False,
215
+ ) -> Path:
216
+ """Export static code graph and report artifacts for standalone browser preview.
217
+ Example:
218
+ >>> import tempfile
219
+ >>> with tempfile.TemporaryDirectory() as td:
220
+ ... out = export_pages(Path("."), Path(td) / "dist")
221
+ ... out.exists()
222
+ True
223
+ """
224
+ resolved_proj = Path(project_path).resolve()
225
+ resolved_out = Path(out_dir).resolve()
226
+ resolved_out.mkdir(parents=True, exist_ok=True)
227
+
228
+ g = scan_project(resolved_proj, entry_points=entry_points, clean_cache=clean_cache)
229
+
230
+ (resolved_out / "graph.json").write_text(JSONEmitter().emit(g), encoding="utf-8")
231
+
232
+ ctx = AnalysisContext(resolved_proj)
233
+ findings = [f.to_dict() for f in AnalysisRunner().run_all(g, ctx)]
234
+ (resolved_out / "defects.json").write_text(
235
+ json.dumps({"findings": findings, "count": len(findings)}, indent=2),
236
+ encoding="utf-8",
237
+ )
238
+
239
+ static_src = Path(__file__).resolve().parent.parent / "server" / "static"
240
+ if static_src.exists():
241
+ static_out = resolved_out / "static"
242
+ static_out.mkdir(parents=True, exist_ok=True)
243
+ for item in static_src.rglob("*"):
244
+ if item.is_file() and item.name != "index.html":
245
+ rel = item.relative_to(static_src)
246
+ dest = static_out / rel
247
+ dest.parent.mkdir(parents=True, exist_ok=True)
248
+ dest.write_bytes(item.read_bytes())
249
+
250
+ index_src = static_src / "index.html"
251
+ if index_src.exists():
252
+ html_text = index_src.read_text(encoding="utf-8")
253
+ html_text = html_text.replace('href="/static/', 'href="static/')
254
+ html_text = html_text.replace('src="/static/', 'src="static/')
255
+ (resolved_out / "index.html").write_text(html_text, encoding="utf-8")
256
+
257
+ (resolved_out / ".nojekyll").touch(exist_ok=True)
258
+ return resolved_out
259
+
199
260
  def main(args: Optional[List[str]] = None) -> int:
200
261
  """Main CLI entrypoint.
201
262
  Example:
@@ -209,16 +270,25 @@ def main(args: Optional[List[str]] = None) -> int:
209
270
  parser = argparse.ArgumentParser(prog="codeui", description="Universal code-intelligence engine", parents=[parent_parser])
210
271
  subparsers = parser.add_subparsers(dest="command")
211
272
 
212
- subparsers.add_parser("init", parents=[parent_parser], help="Initialize .codeui configuration")
273
+ init_p = subparsers.add_parser("init", parents=[parent_parser], help="Initialize .codeui workspace directory")
274
+ init_p.add_argument("project", nargs="?", default=".", help="Project path")
213
275
 
214
276
  scan_p = subparsers.add_parser("scan", parents=[parent_parser], help="Scan project and build code graph")
215
277
  scan_p.add_argument("project", nargs="?", default=".", help="Project path")
216
278
  scan_p.add_argument("--entry", action="append", help="Explicit application entry point(s)")
217
279
  scan_p.add_argument("--out", default=".codeui", help="Output cache directory")
280
+ scan_p.add_argument("--clean-cache", action="store_true", help="Clear stale cache before scanning")
281
+
282
+ pages_p = subparsers.add_parser("pages", parents=[parent_parser], help="Export static interactive graph and report artifacts")
283
+ pages_p.add_argument("project", nargs="?", default=".", help="Project path")
284
+ pages_p.add_argument("--out", default="dist", help="Output directory for static artifacts (default: dist)")
285
+ pages_p.add_argument("--entry", action="append", help="Explicit application entry point(s)")
286
+ pages_p.add_argument("--clean-cache", action="store_true", help="Clear stale cache before exporting")
218
287
 
219
288
  serve_p = subparsers.add_parser("serve", parents=[parent_parser], help="Start localhost web UI server")
220
289
  serve_p.add_argument("project", nargs="?", default=".", help="Project path")
221
290
  serve_p.add_argument("--entry", action="append", help="Explicit application entry point(s)")
291
+ serve_p.add_argument("--host", default="127.0.0.1", help="Host interface to bind to (e.g. 127.0.0.1 or 0.0.0.0)")
222
292
  serve_p.add_argument("--port", type=int, default=3000, help="Port number")
223
293
 
224
294
  trace_p = subparsers.add_parser("trace", parents=[parent_parser], help="Trace symbol logic")
@@ -266,15 +336,16 @@ def main(args: Optional[List[str]] = None) -> int:
266
336
  parsed = parser.parse_args(args)
267
337
 
268
338
  if parsed.command == "init":
269
- Path(".codeui").mkdir(exist_ok=True)
339
+ p = Path(parsed.project).resolve()
340
+ (p / ".codeui").mkdir(parents=True, exist_ok=True)
270
341
  if parsed.json:
271
- print(json.dumps({"status": "initialized", "dir": ".codeui"}))
342
+ print(json.dumps({"status": "initialized", "dir": str(p / ".codeui")}))
272
343
  else:
273
- print("Initialized .codeui workspace directory")
344
+ print(f"Initialized .codeui workspace directory in {p}")
274
345
  return 0
275
346
  elif parsed.command == "scan":
276
347
  p = Path(parsed.project).resolve()
277
- g = scan_project(p, entry_points=parsed.entry)
348
+ g = scan_project(p, entry_points=parsed.entry, clean_cache=parsed.clean_cache)
278
349
  out_dir = Path(parsed.out)
279
350
  out_dir.mkdir(parents=True, exist_ok=True)
280
351
  (out_dir / "graph.json").write_text(JSONEmitter().emit(g), encoding="utf-8")
@@ -283,12 +354,35 @@ def main(args: Optional[List[str]] = None) -> int:
283
354
  else:
284
355
  print(f"Scanned {len(g.get_all_files())} files, {len(g.get_all_symbols())} symbols, {len(g.get_findings())} defects.")
285
356
  return 0
357
+ elif parsed.command == "pages":
358
+ out_path = export_pages(
359
+ project_path=parsed.project,
360
+ out_dir=parsed.out,
361
+ entry_points=parsed.entry,
362
+ clean_cache=parsed.clean_cache,
363
+ )
364
+ if parsed.json:
365
+ print(json.dumps({"status": "exported", "out_dir": str(out_path)}))
366
+ else:
367
+ print(f"Exported static graph and report artifacts to '{out_path}'.")
368
+ return 0
286
369
  elif parsed.command == "serve":
287
370
  p = Path(parsed.project).resolve()
288
371
  g = scan_project(p, entry_points=parsed.entry)
289
372
  store = OverrideStore()
290
- server = create_server(g, store, p, port=parsed.port, rescan_fn=scan_project, entry_points=parsed.entry)
291
- print(f"codeui UI server running on http://127.0.0.1:{parsed.port}")
373
+ try:
374
+ server = create_server(g, store, p, host=parsed.host, port=parsed.port, rescan_fn=scan_project, entry_points=parsed.entry)
375
+ except OSError as e:
376
+ if "already in use" in str(e).lower() or getattr(e, "errno", 0) == 98:
377
+ try:
378
+ import subprocess
379
+ subprocess.run(["fuser", "-k", f"{parsed.port}/tcp"], capture_output=True)
380
+ except Exception:
381
+ pass
382
+ server = create_server(g, store, p, host=parsed.host, port=parsed.port, rescan_fn=scan_project, entry_points=parsed.entry)
383
+ else:
384
+ raise
385
+ print(f"codeui UI server running on http://{parsed.host}:{parsed.port}")
292
386
  try:
293
387
  server.serve_forever()
294
388
  except KeyboardInterrupt:
codeui/core/cache.py CHANGED
@@ -115,6 +115,37 @@ class AnalysisCache:
115
115
  result.append(sym)
116
116
  return result
117
117
 
118
+ def clear(self) -> None:
119
+ """Clear all entries from the cache database.
120
+ Example:
121
+ >>> cache = AnalysisCache(Path(".codeui/test_cache.db"))
122
+ >>> cache.clear()
123
+ >>> cache.close()
124
+ """
125
+ with self.conn:
126
+ self.conn.execute("DELETE FROM file_cache")
127
+
128
+ def prune_stale(self, existing_file_paths: Any) -> int:
129
+ """Remove cached entries for files that no longer exist on disk.
130
+ Example:
131
+ >>> cache = AnalysisCache(Path(".codeui/test_cache.db"))
132
+ >>> cache.prune_stale(["a.py"])
133
+ 0
134
+ >>> cache.close()
135
+ """
136
+ valid_set = set(existing_file_paths)
137
+ cursor = self.conn.cursor()
138
+ cursor.execute("SELECT file_path FROM file_cache")
139
+ cached_paths = [row[0] for row in cursor.fetchall()]
140
+ stale_paths = [p for p in cached_paths if p not in valid_set]
141
+ if stale_paths:
142
+ with self.conn:
143
+ self.conn.executemany(
144
+ "DELETE FROM file_cache WHERE file_path = ?",
145
+ [(p,) for p in stale_paths],
146
+ )
147
+ return len(stale_paths)
148
+
118
149
  def close(self) -> None:
119
150
  """Close SQLite database connection.
120
151
  Example:
@@ -133,3 +164,26 @@ def compute_content_hash(content: str) -> str:
133
164
  64
134
165
  """
135
166
  return hashlib.sha256(content.encode("utf-8")).hexdigest()
167
+
168
+ def remove_stale_cache(cache_path: Path) -> bool:
169
+ """Dynamically remove stale cache file or directory.
170
+ Example:
171
+ >>> p = Path(".codeui/stale_test.db")
172
+ >>> remove_stale_cache(p)
173
+ False
174
+ """
175
+ path = Path(cache_path)
176
+ if path.is_file():
177
+ try:
178
+ path.unlink(missing_ok=True)
179
+ return True
180
+ except OSError:
181
+ return False
182
+ if path.is_dir():
183
+ import shutil
184
+ try:
185
+ shutil.rmtree(path, ignore_errors=True)
186
+ return True
187
+ except OSError:
188
+ return False
189
+ return False