commiter-cli 0.3.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.
Files changed (96) hide show
  1. commiter/__init__.py +3 -0
  2. commiter/adapters/__init__.py +0 -0
  3. commiter/adapters/base.py +96 -0
  4. commiter/adapters/django_rest.py +247 -0
  5. commiter/adapters/express.py +204 -0
  6. commiter/adapters/fastapi.py +170 -0
  7. commiter/adapters/flask.py +169 -0
  8. commiter/adapters/nextjs.py +180 -0
  9. commiter/adapters/prisma.py +76 -0
  10. commiter/adapters/raw_sql.py +191 -0
  11. commiter/adapters/react.py +129 -0
  12. commiter/adapters/sqlalchemy.py +99 -0
  13. commiter/adapters/supabase.py +68 -0
  14. commiter/auth.py +130 -0
  15. commiter/cli.py +667 -0
  16. commiter/correlator.py +208 -0
  17. commiter/extractors/__init__.py +0 -0
  18. commiter/extractors/api_calls.py +91 -0
  19. commiter/extractors/api_endpoints.py +354 -0
  20. commiter/extractors/backend_files.py +33 -0
  21. commiter/extractors/base.py +40 -0
  22. commiter/extractors/db_operations.py +69 -0
  23. commiter/extractors/dependencies.py +219 -0
  24. commiter/generic_resolver.py +204 -0
  25. commiter/handler_index.py +97 -0
  26. commiter/lib.py +63 -0
  27. commiter/middleware_index.py +350 -0
  28. commiter/models.py +117 -0
  29. commiter/parser.py +1283 -0
  30. commiter/prefix_index.py +211 -0
  31. commiter/report/__init__.py +0 -0
  32. commiter/report/ai.py +120 -0
  33. commiter/report/api_guide.py +217 -0
  34. commiter/report/architecture.py +930 -0
  35. commiter/report/console.py +254 -0
  36. commiter/report/json_output.py +122 -0
  37. commiter/report/markdown.py +163 -0
  38. commiter/scanner.py +383 -0
  39. commiter/type_index.py +304 -0
  40. commiter/uploader.py +46 -0
  41. commiter/utils/__init__.py +0 -0
  42. commiter/utils/env_reader.py +78 -0
  43. commiter/utils/file_classifier.py +187 -0
  44. commiter/utils/path_helpers.py +73 -0
  45. commiter/utils/tsconfig_resolver.py +281 -0
  46. commiter/wrapper_index.py +288 -0
  47. commiter_cli-0.3.0.dist-info/METADATA +14 -0
  48. commiter_cli-0.3.0.dist-info/RECORD +96 -0
  49. commiter_cli-0.3.0.dist-info/WHEEL +5 -0
  50. commiter_cli-0.3.0.dist-info/entry_points.txt +2 -0
  51. commiter_cli-0.3.0.dist-info/top_level.txt +2 -0
  52. tests/__init__.py +0 -0
  53. tests/fixtures/arch_backend/app.py +22 -0
  54. tests/fixtures/arch_backend/middleware/__init__.py +0 -0
  55. tests/fixtures/arch_backend/middleware/rate_limit.py +4 -0
  56. tests/fixtures/arch_backend/routes/__init__.py +0 -0
  57. tests/fixtures/arch_backend/routes/analytics.py +20 -0
  58. tests/fixtures/arch_backend/routes/auth.py +29 -0
  59. tests/fixtures/arch_backend/routes/projects.py +60 -0
  60. tests/fixtures/arch_backend/routes/users.py +55 -0
  61. tests/fixtures/arch_monorepo/apps/api/app.py +30 -0
  62. tests/fixtures/arch_monorepo/apps/api/middleware/__init__.py +0 -0
  63. tests/fixtures/arch_monorepo/apps/api/middleware/auth.py +17 -0
  64. tests/fixtures/arch_monorepo/apps/api/middleware/rate_limit.py +10 -0
  65. tests/fixtures/arch_monorepo/apps/api/routes/__init__.py +0 -0
  66. tests/fixtures/arch_monorepo/apps/api/routes/auth.py +46 -0
  67. tests/fixtures/arch_monorepo/apps/api/routes/invites.py +30 -0
  68. tests/fixtures/arch_monorepo/apps/api/routes/notifications.py +25 -0
  69. tests/fixtures/arch_monorepo/apps/api/routes/projects.py +80 -0
  70. tests/fixtures/arch_monorepo/apps/api/routes/tasks.py +91 -0
  71. tests/fixtures/arch_monorepo/apps/api/routes/users.py +48 -0
  72. tests/fixtures/arch_monorepo/apps/api/services/__init__.py +0 -0
  73. tests/fixtures/arch_monorepo/apps/api/services/email.py +11 -0
  74. tests/fixtures/backend_b/app.py +17 -0
  75. tests/fixtures/fastapi_app/app.py +48 -0
  76. tests/fixtures/fastapi_crossfile/routes.py +18 -0
  77. tests/fixtures/fastapi_crossfile/schemas.py +21 -0
  78. tests/fixtures/flask_app/app.py +33 -0
  79. tests/fixtures/flask_blueprint/app.py +7 -0
  80. tests/fixtures/flask_blueprint/routes/items.py +13 -0
  81. tests/fixtures/flask_blueprint/routes/users.py +20 -0
  82. tests/fixtures/middleware_test_flask/routes/public.py +8 -0
  83. tests/fixtures/middleware_test_flask/routes/users.py +26 -0
  84. tests/fixtures/python_deep_imports/app/__init__.py +0 -0
  85. tests/fixtures/python_deep_imports/app/api/__init__.py +0 -0
  86. tests/fixtures/python_deep_imports/app/api/health.py +11 -0
  87. tests/fixtures/python_deep_imports/app/api/v1/__init__.py +0 -0
  88. tests/fixtures/python_deep_imports/app/api/v1/items.py +18 -0
  89. tests/fixtures/python_deep_imports/app/api/v1/users.py +27 -0
  90. tests/fixtures/python_deep_imports/app/schemas/__init__.py +0 -0
  91. tests/fixtures/python_deep_imports/app/schemas/item.py +13 -0
  92. tests/fixtures/python_deep_imports/app/schemas/user.py +15 -0
  93. tests/fixtures/python_deep_imports/app/shared/__init__.py +0 -0
  94. tests/fixtures/python_deep_imports/app/shared/models.py +7 -0
  95. tests/fixtures/raw_sql_test/app.py +54 -0
  96. tests/test_architecture.py +757 -0
@@ -0,0 +1,254 @@
1
+ """Rich terminal output for repository documentation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.console import Console
6
+ from rich.table import Table
7
+ from rich.panel import Panel
8
+ from rich.text import Text
9
+ from rich.columns import Columns
10
+ from rich.tree import Tree as RichTree
11
+
12
+ from commiter.models import RepoDocumentation
13
+
14
+
15
+ def print_report(docs: list[RepoDocumentation], console: Console | None = None,
16
+ sections: dict[str, str | None] | None = None) -> None:
17
+ """Print a formatted summary of all scanned repositories to the terminal."""
18
+ if console is None:
19
+ console = Console()
20
+
21
+ for doc in docs:
22
+ _print_repo(doc, console, sections)
23
+ console.print()
24
+
25
+
26
+ def _show_section(sections: dict[str, str | None] | None, key: str) -> bool:
27
+ """Check if a section should be shown based on the sections config."""
28
+ if sections is None:
29
+ return True
30
+ return sections.get(key) is not None
31
+
32
+
33
+ def _print_repo(doc: RepoDocumentation, console: Console,
34
+ sections: dict[str, str | None] | None = None) -> None:
35
+ """Print a single repository's documentation."""
36
+ # Header
37
+ header = Text(f" {doc.repo_name}", style="bold cyan")
38
+ console.print(Panel(header, title="Repository", border_style="cyan"))
39
+
40
+ # Overview
41
+ overview = Table(show_header=False, box=None, padding=(0, 2))
42
+ overview.add_column("Key", style="bold")
43
+ overview.add_column("Value")
44
+ overview.add_row("Path", doc.repo_path)
45
+ overview.add_row("Languages", ", ".join(doc.languages) if doc.languages else "none detected")
46
+ overview.add_row("Frameworks", ", ".join(doc.frameworks) if doc.frameworks else "none detected")
47
+ overview.add_row("Files scanned", str(len(doc.file_classifications)))
48
+
49
+ # Summary counts
50
+ counts = []
51
+ if doc.endpoints:
52
+ counts.append(f"{len(doc.endpoints)} endpoints")
53
+ if doc.api_calls:
54
+ counts.append(f"{len(doc.api_calls)} API calls")
55
+ if doc.db_operations:
56
+ counts.append(f"{len(doc.db_operations)} DB operations")
57
+ if doc.dependencies:
58
+ counts.append(f"{len(doc.dependencies)} dependencies")
59
+ if counts:
60
+ overview.add_row("Summary", ", ".join(counts))
61
+
62
+ console.print(overview)
63
+ console.print()
64
+
65
+ # Dependencies
66
+ if doc.dependencies and _show_section(sections, "deps"):
67
+ _print_dependencies(doc, console)
68
+
69
+ # API Endpoints
70
+ if doc.endpoints and _show_section(sections, "endpoints"):
71
+ _print_endpoints(doc, console)
72
+
73
+ # API Calls (frontend)
74
+ if doc.api_calls and _show_section(sections, "calls"):
75
+ _print_api_calls(doc, console)
76
+
77
+ # DB Operations
78
+ if doc.db_operations and _show_section(sections, "db"):
79
+ _print_db_operations(doc, console)
80
+
81
+ # Shared DB relationships
82
+ shared_db = [r for r in doc.service_relationships if r.connection_type == "shared_db"]
83
+ if shared_db and _show_section(sections, "db"):
84
+ _print_shared_db(shared_db, console)
85
+
86
+
87
+ def _print_dependencies(doc: RepoDocumentation, console: Console) -> None:
88
+ """Print dependency table."""
89
+ table = Table(title="Dependencies", title_style="bold magenta")
90
+ table.add_column("Package", style="green")
91
+ table.add_column("Version")
92
+ table.add_column("Source")
93
+ table.add_column("Dev?", justify="center")
94
+
95
+ runtime = [d for d in doc.dependencies if not d.dev_only]
96
+ dev = [d for d in doc.dependencies if d.dev_only]
97
+
98
+ for dep in sorted(runtime, key=lambda d: d.name.lower()):
99
+ source_short = dep.source_file.split("/")[-1] if "/" in dep.source_file else dep.source_file
100
+ table.add_row(dep.name, dep.version_constraint, source_short, "")
101
+
102
+ for dep in sorted(dev, key=lambda d: d.name.lower()):
103
+ source_short = dep.source_file.split("/")[-1] if "/" in dep.source_file else dep.source_file
104
+ table.add_row(dep.name, dep.version_constraint, source_short, "yes")
105
+
106
+ console.print(table)
107
+ console.print()
108
+
109
+
110
+ def _print_endpoints(doc: RepoDocumentation, console: Console) -> None:
111
+ """Print API endpoints — compact table + detailed cards."""
112
+ # Summary table with just the essentials
113
+ table = Table(title="API Endpoints", title_style="bold magenta")
114
+ table.add_column("Method", style="bold yellow", no_wrap=True)
115
+ table.add_column("Route", style="green")
116
+ table.add_column("Handler")
117
+ table.add_column("File", style="dim")
118
+
119
+ for ep in doc.endpoints:
120
+ rel_path = _rel_path(ep.file_path, doc.repo_name)
121
+ table.add_row(ep.http_method.upper(), ep.route_pattern, ep.handler_name, f"{rel_path}:{ep.line}")
122
+
123
+ console.print(table)
124
+ console.print()
125
+
126
+ # Detailed cards for endpoints that have interesting details
127
+ has_details = any(
128
+ ep.parameters or ep.request_body_fields or ep.middleware or
129
+ ep.auth_decorators or ep.db_tables or ep.request_body_type
130
+ for ep in doc.endpoints
131
+ )
132
+
133
+ if has_details:
134
+ console.print(Text(" Endpoint Details", style="bold magenta"))
135
+ console.print()
136
+
137
+ for ep in doc.endpoints:
138
+ details = _build_endpoint_details(ep)
139
+ if not details:
140
+ continue
141
+
142
+ method_style = {
143
+ "GET": "green", "POST": "yellow", "PUT": "blue",
144
+ "DELETE": "red", "PATCH": "cyan",
145
+ }.get(ep.http_method.upper(), "white")
146
+
147
+ title = f"[bold {method_style}]{ep.http_method.upper()}[/bold {method_style}] [green]{ep.route_pattern}[/green]"
148
+ panel = Panel(
149
+ "\n".join(details),
150
+ title=title,
151
+ title_align="left",
152
+ border_style="dim",
153
+ padding=(0, 2),
154
+ )
155
+ console.print(panel)
156
+
157
+
158
+ def _build_endpoint_details(ep) -> list[str]:
159
+ """Build detail lines for an endpoint panel."""
160
+ details = []
161
+
162
+ rel_path = ep.file_path.split("/")[-1] if "/" in ep.file_path else ep.file_path
163
+ details.append(f"[bold]Handler:[/bold] {ep.handler_name} [dim]({rel_path}:{ep.line})[/dim]")
164
+
165
+ if ep.parameters:
166
+ params = ", ".join(f"[cyan]{p.name}[/cyan] ({p.source.value})" for p in ep.parameters)
167
+ details.append(f"[bold]Params:[/bold] {params}")
168
+
169
+ if ep.request_body_type:
170
+ details.append(f"[bold]Body Type:[/bold] [cyan]{ep.request_body_type}[/cyan]")
171
+
172
+ if ep.request_body_fields:
173
+ fields = ", ".join(f"[cyan]{f}[/cyan]" for f in ep.request_body_fields)
174
+ details.append(f"[bold]Body:[/bold] {fields}")
175
+
176
+ if ep.response_fields:
177
+ fields = ", ".join(f"[cyan]{f}[/cyan]" for f in ep.response_fields[:6])
178
+ suffix = f" (+{len(ep.response_fields) - 6} more)" if len(ep.response_fields) > 6 else ""
179
+ details.append(f"[bold]Response:[/bold] {fields}{suffix}")
180
+
181
+ if ep.auth_decorators:
182
+ auth = ", ".join(f"[yellow]{a}[/yellow]" for a in ep.auth_decorators)
183
+ details.append(f"[bold]Auth:[/bold] {auth}")
184
+
185
+ if ep.middleware:
186
+ mw = ", ".join(f"[magenta]{m}[/magenta]" for m in ep.middleware)
187
+ details.append(f"[bold]Middleware:[/bold] {mw}")
188
+
189
+ if ep.db_tables:
190
+ tables = ", ".join(f"[blue]{t}[/blue]" for t in ep.db_tables)
191
+ details.append(f"[bold]DB Tables:[/bold] {tables}")
192
+
193
+ # Only return details if there's more than just the handler line
194
+ if len(details) <= 1:
195
+ return []
196
+
197
+ return details
198
+
199
+
200
+ def _print_api_calls(doc: RepoDocumentation, console: Console) -> None:
201
+ """Print frontend API calls table."""
202
+ table = Table(title="Frontend API Calls", title_style="bold magenta")
203
+ table.add_column("Method", style="bold yellow", no_wrap=True)
204
+ table.add_column("URL", style="green")
205
+ table.add_column("Component/Page")
206
+ table.add_column("Traced From", style="dim")
207
+
208
+ for call in doc.api_calls:
209
+ traced = call.traced_from or "-"
210
+ table.add_row(call.http_method.upper(), call.url_pattern, call.component_or_page, traced)
211
+
212
+ console.print(table)
213
+ console.print()
214
+
215
+
216
+ def _print_db_operations(doc: RepoDocumentation, console: Console) -> None:
217
+ """Print database operations table."""
218
+ table = Table(title="Database Operations", title_style="bold magenta")
219
+ table.add_column("Operation", style="bold yellow", no_wrap=True)
220
+ table.add_column("Table", style="green")
221
+ table.add_column("ORM")
222
+ table.add_column("File", style="dim")
223
+ table.add_column("Filters", style="dim")
224
+
225
+ for op in doc.db_operations:
226
+ rel_path = _rel_path(op.file_path, doc.repo_name)
227
+ filters = ", ".join(op.filters) if op.filters else "-"
228
+ table.add_row(op.operation_type.upper(), op.table_name, op.orm_library, f"{rel_path}:{op.line}", filters)
229
+
230
+ console.print(table)
231
+ console.print()
232
+
233
+
234
+ def _print_shared_db(relationships: list, console: Console) -> None:
235
+ """Print shared database tables between repos."""
236
+ table = Table(title="Shared Database Tables", title_style="bold magenta")
237
+ table.add_column("Table", style="green")
238
+ table.add_column("Used In", style="cyan")
239
+ table.add_column("Also Used In", style="cyan")
240
+ table.add_column("Confidence", justify="center")
241
+
242
+ for rel in relationships:
243
+ table_name = rel.target_endpoint.replace("table:", "")
244
+ table.add_row(table_name, f"{rel.source_repo}/", f"{rel.target_repo}/", f"{rel.confidence:.0%}")
245
+
246
+ console.print(table)
247
+ console.print()
248
+
249
+
250
+ def _rel_path(file_path: str, repo_name: str) -> str:
251
+ """Make a file path relative for display."""
252
+ if repo_name + "/" in file_path:
253
+ return file_path.split(repo_name + "/", 1)[-1]
254
+ return file_path
@@ -0,0 +1,122 @@
1
+ """Generate structured JSON output from scanned repositories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import asdict
7
+
8
+ from commiter.models import RepoDocumentation
9
+
10
+
11
+ def generate_json(docs: list[RepoDocumentation], indent: int = 2,
12
+ sections: dict[str, str | None] | None = None) -> str:
13
+ """Generate a JSON string from the documentation results."""
14
+ output = []
15
+ for doc in docs:
16
+ repo_data = _serialize_repo(doc, sections)
17
+ output.append(repo_data)
18
+
19
+ return json.dumps(output, indent=indent, default=str)
20
+
21
+
22
+ def _show(sections: dict | None, key: str) -> bool:
23
+ if sections is None:
24
+ return True
25
+ return sections.get(key) is not None
26
+
27
+
28
+ def _serialize_repo(doc: RepoDocumentation, sections: dict | None = None) -> dict:
29
+ """Serialize a RepoDocumentation to a JSON-friendly dict."""
30
+ result = {
31
+ "repo_name": doc.repo_name,
32
+ "repo_path": doc.repo_path,
33
+ "languages": doc.languages,
34
+ "frameworks": doc.frameworks,
35
+ "summary": {
36
+ "total_files": len(doc.file_classifications),
37
+ "total_endpoints": len(doc.endpoints),
38
+ "total_api_calls": len(doc.api_calls),
39
+ "total_dependencies": len(doc.dependencies),
40
+ "total_db_operations": len(doc.db_operations),
41
+ "total_relationships": len(doc.service_relationships),
42
+ },
43
+ }
44
+
45
+ if _show(sections, "endpoints"):
46
+ result["endpoints"] = [
47
+ {
48
+ "http_method": ep.http_method,
49
+ "route_pattern": ep.route_pattern,
50
+ "handler_name": ep.handler_name,
51
+ "framework": ep.framework,
52
+ "file_path": ep.file_path,
53
+ "line": ep.line,
54
+ "parameters": [
55
+ {"name": p.name, "source": p.source.value, "type_hint": p.type_hint, "required": p.required}
56
+ for p in ep.parameters
57
+ ],
58
+ "request_body_fields": ep.request_body_fields,
59
+ "response_fields": ep.response_fields,
60
+ "auth_decorators": ep.auth_decorators,
61
+ "db_tables": ep.db_tables,
62
+ "request_body_type": ep.request_body_type,
63
+ "response_type": ep.response_type,
64
+ "middleware": ep.middleware,
65
+ }
66
+ for ep in doc.endpoints
67
+ ]
68
+
69
+ if _show(sections, "calls"):
70
+ result["api_calls"] = [
71
+ {
72
+ "http_method": call.http_method,
73
+ "url_pattern": call.url_pattern,
74
+ "component_or_page": call.component_or_page,
75
+ "client_library": call.client_library,
76
+ "file_path": call.file_path,
77
+ "line": call.line,
78
+ "traced_from": call.traced_from,
79
+ "response_type": call.response_type,
80
+ "body_type": call.body_type,
81
+ }
82
+ for call in doc.api_calls
83
+ ]
84
+
85
+ if _show(sections, "deps"):
86
+ result["dependencies"] = {
87
+ "runtime": [
88
+ {"name": d.name, "version": d.version_constraint, "source": d.source_file}
89
+ for d in doc.dependencies if not d.dev_only
90
+ ],
91
+ "dev": [
92
+ {"name": d.name, "version": d.version_constraint, "source": d.source_file}
93
+ for d in doc.dependencies if d.dev_only
94
+ ],
95
+ }
96
+
97
+ if _show(sections, "db"):
98
+ result["db_operations"] = [
99
+ {
100
+ "operation_type": op.operation_type,
101
+ "table_name": op.table_name,
102
+ "orm_library": op.orm_library,
103
+ "file_path": op.file_path,
104
+ "line": op.line,
105
+ "filters": op.filters,
106
+ }
107
+ for op in doc.db_operations
108
+ ]
109
+
110
+ result["service_relationships"] = [
111
+ {
112
+ "source_repo": rel.source_repo,
113
+ "target_repo": rel.target_repo,
114
+ "connection_type": rel.connection_type,
115
+ "source_file": rel.source_file,
116
+ "target_endpoint": rel.target_endpoint,
117
+ "confidence": rel.confidence,
118
+ }
119
+ for rel in doc.service_relationships
120
+ ]
121
+
122
+ return result
@@ -0,0 +1,163 @@
1
+ """Generate Markdown documentation from scanned repositories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from commiter.models import RepoDocumentation
8
+
9
+
10
+ def generate_markdown(docs: list[RepoDocumentation],
11
+ sections: dict[str, str | None] | None = None) -> str:
12
+ """Generate a single Markdown document covering all scanned repos."""
13
+ lines: list[str] = []
14
+
15
+ lines.append("# Repository Documentation\n")
16
+ lines.append(f"*Generated by commiter — {len(docs)} repository(ies) scanned.*\n")
17
+ lines.append("---\n")
18
+
19
+ for doc in docs:
20
+ lines.extend(_repo_section(doc, sections))
21
+ lines.append("---\n")
22
+
23
+ return "\n".join(lines)
24
+
25
+
26
+ def _show(sections: dict | None, key: str) -> bool:
27
+ if sections is None:
28
+ return True
29
+ return sections.get(key) is not None
30
+
31
+
32
+ def _repo_section(doc: RepoDocumentation,
33
+ sections: dict[str, str | None] | None = None) -> list[str]:
34
+ lines: list[str] = []
35
+
36
+ lines.append(f"## {doc.repo_name}\n")
37
+
38
+ # Overview table
39
+ lines.append("| Property | Value |")
40
+ lines.append("|----------|-------|")
41
+ lines.append(f"| Path | `{doc.repo_path}` |")
42
+ lines.append(f"| Languages | {', '.join(doc.languages) if doc.languages else 'none detected'} |")
43
+ lines.append(f"| Frameworks | {', '.join(doc.frameworks) if doc.frameworks else 'none detected'} |")
44
+ lines.append(f"| Files scanned | {len(doc.file_classifications)} |")
45
+ lines.append("")
46
+
47
+ # API Endpoints
48
+ if doc.endpoints and _show(sections, "endpoints"):
49
+ lines.extend(_endpoints_section(doc))
50
+
51
+ # Frontend API Calls
52
+ if doc.api_calls and _show(sections, "calls"):
53
+ lines.extend(_api_calls_section(doc))
54
+
55
+ # Dependencies
56
+ if doc.dependencies and _show(sections, "deps"):
57
+ lines.extend(_dependencies_section(doc))
58
+
59
+ # DB Operations
60
+ if doc.db_operations and _show(sections, "db"):
61
+ lines.extend(_db_operations_section(doc))
62
+
63
+ return lines
64
+
65
+
66
+ def _endpoints_section(doc: RepoDocumentation) -> list[str]:
67
+ lines: list[str] = []
68
+ lines.append("### API Endpoints\n")
69
+ lines.append("| Method | Route | Handler | File | Auth |")
70
+ lines.append("|--------|-------|---------|------|------|")
71
+
72
+ for ep in doc.endpoints:
73
+ rel = _rel_path(ep.file_path, doc.repo_name)
74
+ auth = ", ".join(ep.auth_decorators) if ep.auth_decorators else "-"
75
+ lines.append(f"| `{ep.http_method}` | `{ep.route_pattern}` | `{ep.handler_name}` | `{rel}:{ep.line}` | {auth} |")
76
+
77
+ lines.append("")
78
+
79
+ # Endpoint details
80
+ for ep in doc.endpoints:
81
+ lines.append(f"#### `{ep.http_method} {ep.route_pattern}`\n")
82
+ lines.append(f"- **Handler**: `{ep.handler_name}` ({ep.framework})")
83
+ if ep.parameters:
84
+ params_str = ", ".join(f"`{p.name}` ({p.source.value})" for p in ep.parameters)
85
+ lines.append(f"- **Parameters**: {params_str}")
86
+ if ep.request_body_fields:
87
+ lines.append(f"- **Request body**: {', '.join(f'`{f}`' for f in ep.request_body_fields)}")
88
+ if ep.response_fields:
89
+ lines.append(f"- **Response fields**: {', '.join(f'`{f}`' for f in ep.response_fields)}")
90
+ if ep.auth_decorators:
91
+ lines.append(f"- **Auth**: {', '.join(f'`{a}`' for a in ep.auth_decorators)}")
92
+ if ep.request_body_type:
93
+ lines.append(f"- **Request body type**: `{ep.request_body_type}`")
94
+ if ep.response_type:
95
+ lines.append(f"- **Response type**: `{ep.response_type}`")
96
+ if ep.middleware:
97
+ lines.append(f"- **Middleware**: {', '.join(f'`{m}`' for m in ep.middleware)}")
98
+ if ep.db_tables:
99
+ lines.append(f"- **DB tables**: {', '.join(f'`{t}`' for t in ep.db_tables)}")
100
+ lines.append("")
101
+
102
+ return lines
103
+
104
+
105
+ def _api_calls_section(doc: RepoDocumentation) -> list[str]:
106
+ lines: list[str] = []
107
+ lines.append("### Frontend API Calls\n")
108
+ lines.append("| Method | URL | Component/Page | Library | Traced From |")
109
+ lines.append("|--------|-----|----------------|---------|-------------|")
110
+
111
+ for call in doc.api_calls:
112
+ traced = call.traced_from or "-"
113
+ lines.append(f"| `{call.http_method}` | `{call.url_pattern}` | `{call.component_or_page}` | {call.client_library} | {traced} |")
114
+
115
+ lines.append("")
116
+ return lines
117
+
118
+
119
+ def _dependencies_section(doc: RepoDocumentation) -> list[str]:
120
+ lines: list[str] = []
121
+ runtime = [d for d in doc.dependencies if not d.dev_only]
122
+ dev = [d for d in doc.dependencies if d.dev_only]
123
+
124
+ if runtime:
125
+ lines.append("### Dependencies (Runtime)\n")
126
+ lines.append("| Package | Version | Source |")
127
+ lines.append("|---------|---------|--------|")
128
+ for dep in sorted(runtime, key=lambda d: d.name.lower()):
129
+ source = Path(dep.source_file).name
130
+ lines.append(f"| `{dep.name}` | `{dep.version_constraint}` | {source} |")
131
+ lines.append("")
132
+
133
+ if dev:
134
+ lines.append("### Dependencies (Dev)\n")
135
+ lines.append("| Package | Version | Source |")
136
+ lines.append("|---------|---------|--------|")
137
+ for dep in sorted(dev, key=lambda d: d.name.lower()):
138
+ source = Path(dep.source_file).name
139
+ lines.append(f"| `{dep.name}` | `{dep.version_constraint}` | {source} |")
140
+ lines.append("")
141
+
142
+ return lines
143
+
144
+
145
+ def _db_operations_section(doc: RepoDocumentation) -> list[str]:
146
+ lines: list[str] = []
147
+ lines.append("### Database Operations\n")
148
+ lines.append("| Operation | Table | ORM | File |")
149
+ lines.append("|-----------|-------|-----|------|")
150
+
151
+ for op in doc.db_operations:
152
+ rel = _rel_path(op.file_path, doc.repo_name)
153
+ lines.append(f"| `{op.operation_type}` | `{op.table_name}` | {op.orm_library} | `{rel}:{op.line}` |")
154
+
155
+ lines.append("")
156
+ return lines
157
+
158
+
159
+ def _rel_path(file_path: str, repo_name: str) -> str:
160
+ """Make a file path relative to the repo for display."""
161
+ if repo_name + "/" in file_path:
162
+ return file_path.split(repo_name + "/", 1)[-1]
163
+ return file_path