codebread 1.0.0__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.
codebread/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """CodeBread — slice open a codebase and see its internal structure."""
2
+
3
+ __version__ = "1.0.0"
codebread/analyzer.py ADDED
@@ -0,0 +1,84 @@
1
+ """Orchestrates: scan -> parse -> classify -> connect -> graph dict."""
2
+ from __future__ import annotations
3
+
4
+ import datetime
5
+ import os
6
+ import sys
7
+ from typing import Callable, Dict, Optional
8
+
9
+ from . import __version__
10
+ from .classifier import classify
11
+ from .connections import build_graph
12
+ from .parsers import parse_file
13
+ from .scanner import read_text, scan
14
+
15
+
16
+ def analyze(root: str,
17
+ progress: Optional[Callable[[str], None]] = None) -> Dict:
18
+ """Run the full pipeline on `root` and return the graph dict."""
19
+ say = progress or (lambda msg: None)
20
+ root = os.path.abspath(root)
21
+
22
+ say(f"Scanning {root} ...")
23
+ result = scan(root)
24
+ files_meta = result["files"]
25
+ warnings = result["warnings"]
26
+ say(f" found {len(files_meta)} scannable files")
27
+
28
+ parsed = []
29
+ unsupported = {}
30
+ for i, meta in enumerate(files_meta, 1):
31
+ text, err = read_text(meta["abspath"])
32
+ info = parse_file(meta["path"], text, meta["language"])
33
+ if err:
34
+ info.warnings.append(err)
35
+ info.layer = classify(info, text)
36
+ if info.parsed or info.language in ("html", "css"):
37
+ info.source = text if len(text) <= 300_000 else \
38
+ text[:300_000] + "\n… (truncated)"
39
+ parsed.append(info)
40
+ for w in info.warnings:
41
+ if w.startswith("Unsupported:"):
42
+ unsupported.setdefault(meta["language"], 0)
43
+ unsupported[meta["language"]] += 1
44
+ warnings.append({"path": meta["path"], "message": w})
45
+ if i % 50 == 0:
46
+ say(f" parsed {i}/{len(files_meta)} files")
47
+
48
+ for lang, count in sorted(unsupported.items()):
49
+ say(f" note: {count} file(s) in {lang} - no parser available, "
50
+ f"shown as unsupported in the UI")
51
+
52
+ say("Mapping connections ...")
53
+ _annotate_tree(result["tree"], {f.path: f for f in parsed})
54
+ graph = build_graph(parsed, result["tree"], warnings)
55
+ graph["meta"] = {
56
+ "root": root,
57
+ "name": os.path.basename(root) or root,
58
+ "scannedAt": datetime.datetime.now().isoformat(timespec="seconds"),
59
+ "version": __version__,
60
+ }
61
+ s = graph["stats"]
62
+ say(f"Done: {s['files']} files, {s['functions']} functions, "
63
+ f"{s['tables']} tables, {s['connections']} connections, "
64
+ f"{s['warnings']} warnings")
65
+ return graph
66
+
67
+
68
+ def _annotate_tree(node: Dict, by_path: Dict) -> None:
69
+ """Copy layer/warning info onto tree nodes for the sidebar."""
70
+ if node.get("type") == "file":
71
+ info = by_path.get(node.get("path"))
72
+ if info is not None:
73
+ node["layer"] = info.layer
74
+ node["nFunctions"] = len(info.functions)
75
+ if info.warnings and "warning" not in node:
76
+ node["warning"] = "parse"
77
+ return
78
+ layers = set()
79
+ for child in node.get("children", []):
80
+ _annotate_tree(child, by_path)
81
+ if child.get("layer") and child["layer"] not in ("unknown", None):
82
+ layers.add(child["layer"])
83
+ if len(layers) == 1:
84
+ node["layer"] = layers.pop()
@@ -0,0 +1,102 @@
1
+ """Score-based Frontend / Backend / Database / Config classifier.
2
+
3
+ Combines framework import signatures, folder naming conventions, file
4
+ extensions/patterns, and extracted facts (routes, DB models). Ambiguous
5
+ files stay "unknown" (shown as Unclassified) rather than guessing wrong.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import re
11
+ from typing import Dict
12
+
13
+ from .models import FileInfo
14
+
15
+ FRONTEND_IMPORTS = re.compile(
16
+ r"^(react|react-dom|next|vue|@vue|nuxt|@angular|svelte|solid-js|preact|"
17
+ r"jquery|axios|@tanstack|redux|zustand|styled-components|@mui|antd|"
18
+ r"tailwindcss|three|d3)($|/)", re.IGNORECASE)
19
+ BACKEND_IMPORTS = re.compile(
20
+ r"^(express|koa|fastify|hapi|@nestjs|flask|django|fastapi|starlette|"
21
+ r"bottle|tornado|sanic|aiohttp|gin|echo|fiber|laravel|symfony|rails|"
22
+ r"sinatra|spring|springframework|gorilla|net/http|http\.server|"
23
+ r"microsoft\.aspnetcore)($|/|\.)", re.IGNORECASE)
24
+ DB_IMPORTS = re.compile(
25
+ r"^(sqlalchemy|django\.db|peewee|tortoise|pymongo|motor|redis|psycopg2?|"
26
+ r"mysql|sqlite3|mongoose|prisma|@prisma|sequelize|typeorm|knex|pg|"
27
+ r"mongodb|gorm|database/sql|entityframework|dapper|activerecord)"
28
+ r"($|/|\.)", re.IGNORECASE)
29
+
30
+ FRONTEND_DIRS = {"client", "frontend", "front", "components", "pages", "views",
31
+ "ui", "www", "public", "static", "assets", "layouts", "hooks",
32
+ "styles", "templates", "screens", "widgets"}
33
+ BACKEND_DIRS = {"server", "backend", "back", "api", "routes", "controllers",
34
+ "middleware", "services", "handlers", "endpoints", "resolvers",
35
+ "views_api", "rest", "graphql", "app"}
36
+ DB_DIRS = {"models", "model", "db", "database", "migrations", "schema",
37
+ "schemas", "entities", "repositories", "orm", "prisma", "sql"}
38
+
39
+ FRONTEND_EXTS = {".jsx", ".tsx", ".vue", ".svelte", ".html", ".htm",
40
+ ".css", ".scss", ".sass", ".less"}
41
+
42
+
43
+ def classify(info: FileInfo, raw_text: str = "") -> str:
44
+ if info.language == "config":
45
+ return "config"
46
+ if info.language in ("markdown", "text"):
47
+ return "unknown"
48
+ if info.language == "sql":
49
+ return "database"
50
+
51
+ score: Dict[str, float] = {"frontend": 0.0, "backend": 0.0,
52
+ "database": 0.0}
53
+
54
+ ext = os.path.splitext(info.path)[1].lower()
55
+ if ext in FRONTEND_EXTS:
56
+ score["frontend"] += 2.5
57
+ if info.language in ("vue", "svelte"):
58
+ score["frontend"] += 2.0
59
+
60
+ parts = [p.lower() for p in info.path.split("/")[:-1]]
61
+ for p in parts:
62
+ if p in FRONTEND_DIRS:
63
+ score["frontend"] += 1.5
64
+ if p in BACKEND_DIRS:
65
+ score["backend"] += 1.5
66
+ if p in DB_DIRS:
67
+ score["database"] += 2.0
68
+
69
+ for imp in info.imports:
70
+ if FRONTEND_IMPORTS.match(imp):
71
+ score["frontend"] += 2.0
72
+ if BACKEND_IMPORTS.match(imp):
73
+ score["backend"] += 2.5
74
+ if DB_IMPORTS.match(imp):
75
+ score["database"] += 2.0
76
+
77
+ # extracted facts are strong signals
78
+ n_routes = len(info.routes) + sum(len(f.routes) for f in info.functions)
79
+ if n_routes:
80
+ score["backend"] += 2.0 + min(n_routes, 5) * 0.4
81
+ if info.tables:
82
+ score["database"] += 2.0 + min(len(info.tables), 5) * 0.6
83
+ n_api = len(info.api_calls) + sum(len(f.api_calls) for f in info.functions)
84
+ if n_api and score["backend"] < 1.0:
85
+ score["frontend"] += 1.0 # things that *call* APIs lean frontend
86
+ n_db = sum(len(f.db_refs) for f in info.functions)
87
+ if n_db:
88
+ score["database"] += min(n_db, 4) * 0.5
89
+ score["backend"] += 0.5
90
+
91
+ if raw_text and info.language in ("javascript", "typescript"):
92
+ if re.search(r"\bdocument\.|window\.|useState\(|useEffect\(",
93
+ raw_text):
94
+ score["frontend"] += 1.5
95
+ if re.search(r"process\.env|require\(['\"]fs['\"]\)|__dirname",
96
+ raw_text):
97
+ score["backend"] += 0.8
98
+
99
+ best = max(score, key=lambda k: score[k])
100
+ if score[best] < 1.5:
101
+ return "unknown"
102
+ return best
codebread/cli.py ADDED
@@ -0,0 +1,106 @@
1
+ """CodeBread command line interface."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import os
6
+ import sys
7
+
8
+ from . import __version__
9
+
10
+ BANNER = r"""
11
+ ___ _ ___ _
12
+ / __|___ __| |___| _ )_ _ ___ __ _ __| |
13
+ | (__/ _ \/ _` / -_) _ \ '_/ -_) _` / _` |
14
+ \___\___/\__,_\___|___/_| \___\__,_\__,_| v{v}
15
+ slice open a codebase and see how it's wired
16
+ """
17
+
18
+
19
+ def main(argv=None) -> int:
20
+ parser = argparse.ArgumentParser(
21
+ prog="codebread",
22
+ description="Interactive codebase analyzer & visualizer. Scans a "
23
+ "project, extracts every function/class, maps frontend "
24
+ "-> backend -> database connections, and opens an "
25
+ "interactive graph in your browser.")
26
+ parser.add_argument("--path", "-p",
27
+ help="root folder to scan (prompted if omitted)")
28
+ parser.add_argument("--load", metavar="GRAPH.json",
29
+ help="serve a previously exported graph JSON "
30
+ "instead of re-scanning")
31
+ parser.add_argument("--diff", nargs=2, metavar=("OLD.json", "NEW.json"),
32
+ help="compare two saved graph JSON exports and "
33
+ "print what changed, then exit")
34
+ parser.add_argument("--port", type=int, default=8137,
35
+ help="local server port (default: 8137)")
36
+ parser.add_argument("--json", metavar="OUT.json",
37
+ help="also export the full graph as JSON")
38
+ parser.add_argument("--html", metavar="OUT.html",
39
+ help="also export a self-contained static HTML file")
40
+ parser.add_argument("--no-open", action="store_true",
41
+ help="don't auto-open the browser")
42
+ parser.add_argument("--no-serve", action="store_true",
43
+ help="scan + export only, don't start the server")
44
+ parser.add_argument("--version", action="version",
45
+ version=f"codebread {__version__}")
46
+ args = parser.parse_args(argv)
47
+
48
+ print(BANNER.format(v=__version__))
49
+
50
+ from .export import export_html, export_json, load_json
51
+
52
+ if args.diff:
53
+ old_path, new_path = args.diff
54
+ for p in (old_path, new_path):
55
+ if not os.path.isfile(p):
56
+ print(f"error: {p} not found", file=sys.stderr)
57
+ return 2
58
+ from .diff import compute_diff, format_diff_report
59
+ report = format_diff_report(compute_diff(load_json(old_path), load_json(new_path)))
60
+ print(report)
61
+ return 0
62
+
63
+ if args.load:
64
+ if not os.path.isfile(args.load):
65
+ print(f"error: {args.load} not found", file=sys.stderr)
66
+ return 2
67
+ graph = load_json(args.load)
68
+ print(f"[codebread] Loaded saved graph: {args.load}")
69
+ else:
70
+ root = args.path
71
+ if not root:
72
+ try:
73
+ root = input("Path to the project to analyze: ").strip().strip('"')
74
+ except (EOFError, KeyboardInterrupt):
75
+ print()
76
+ return 1
77
+ if not root:
78
+ print("error: no path given", file=sys.stderr)
79
+ return 2
80
+ root = os.path.expanduser(root)
81
+ if not os.path.isdir(root):
82
+ print(f"error: not a folder: {root}", file=sys.stderr)
83
+ return 2
84
+
85
+ from .analyzer import analyze
86
+ graph = analyze(root, progress=lambda m: print(f"[codebread] {m}"))
87
+
88
+ if graph["stats"]["warnings"]:
89
+ print(f"[codebread] {graph['stats']['warnings']} warning(s) - "
90
+ f"they are shown as badges in the UI, nothing was hidden.")
91
+
92
+ if args.json:
93
+ p = export_json(graph, args.json)
94
+ print(f"[codebread] Graph JSON written to {p}")
95
+ if args.html:
96
+ p = export_html(graph, args.html)
97
+ print(f"[codebread] Static HTML written to {p}")
98
+
99
+ if not args.no_serve:
100
+ from .server import serve
101
+ serve(graph, port=args.port, open_browser=not args.no_open)
102
+ return 0
103
+
104
+
105
+ if __name__ == "__main__":
106
+ sys.exit(main())