code-lens-cli 0.7.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.
seer/repo/render.py ADDED
@@ -0,0 +1,470 @@
1
+ """Markdown emitters for seer.repo.
2
+
3
+ Each ``render_*_markdown`` function takes a plain dict (the shape produced
4
+ by :mod:`seer.repo.profile`, and later :mod:`seer.repo.connections` /
5
+ :mod:`seer.repo.graph`) and returns a string.
6
+
7
+ The matching JSON envelopes are produced by callers via
8
+ :func:`seer.cli._output.emit_result` with ``json_mode=True``; render.py
9
+ does not duplicate that.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any
15
+
16
+ from seer.cli._errors import EXIT_ENV_ERROR, EXIT_INTERNAL, EXIT_USER_ERROR, SeerError
17
+
18
+ _KIND_LABEL = {
19
+ EXIT_USER_ERROR: "user error",
20
+ EXIT_ENV_ERROR: "environment error",
21
+ EXIT_INTERNAL: "internal bug",
22
+ }
23
+
24
+
25
+ def _section_break(lines: list[str]) -> None:
26
+ """Emit a blank line + horizontal rule before a new section heading.
27
+
28
+ Gives top-level ``##`` sections a strong visual anchor — readers (human
29
+ or LLM) can scan the report and immediately see section boundaries even
30
+ when individual sections contain dense prose-filled tables.
31
+ """
32
+ lines.append("")
33
+ lines.append("---")
34
+
35
+
36
+ def _append_skill_table(lines: list[str], skills: list[dict]) -> None:
37
+ """Append the vendored-skills table section to *lines*."""
38
+ _section_break(lines)
39
+ lines.append(f"## Vendored skills ({len(skills)})")
40
+ lines.append("| Skill | Source | Version |")
41
+ lines.append("|---|---|---|")
42
+ for s in skills:
43
+ lines.append(f"| {s.get('name', '')} | {s.get('source', '')} | {s.get('version', '')} |")
44
+
45
+
46
+ def _append_citation_table(lines: list[str], citations: list[dict]) -> None:
47
+ """Append the citations table section to *lines*."""
48
+ _section_break(lines)
49
+ lines.append(f"## Citations ({len(citations)})")
50
+ lines.append("| Local | Source repo | SHA |")
51
+ lines.append("|---|---|---|")
52
+ for c in citations:
53
+ lines.append(f"| {c.get('local', '')} | {c.get('source_repo', '')} | {c.get('sha', '')} |")
54
+
55
+
56
+ def _changelog_line(entry: dict) -> str:
57
+ """Format a single changelog entry as a markdown list item."""
58
+ v = entry.get("version", "")
59
+ d = entry.get("date", "")
60
+ s = entry.get("summary", "")
61
+ date_suffix = f" ({d})" if d else ""
62
+ return f"- **{v}**{date_suffix} — {s}"
63
+
64
+
65
+ def _append_changelog(lines: list[str], changelog: list[dict]) -> None:
66
+ """Append the recent-changelog section to *lines*."""
67
+ _section_break(lines)
68
+ lines.append("## Recent changelog")
69
+ for entry in changelog:
70
+ lines.append(_changelog_line(entry))
71
+
72
+
73
+ def _append_deps_runtime(lines: list[str], deps: list[str]) -> None:
74
+ """Append the `## Runtime dependencies` block."""
75
+ _section_break(lines)
76
+ lines.append(f"## Runtime dependencies ({len(deps)})")
77
+ for d in deps:
78
+ lines.append(f"- {d}")
79
+
80
+
81
+ def _append_tree_node(lines: list[str], node: dict[str, Any], indent: int) -> None:
82
+ """Render one ``package_tree`` node and recurse into its subpackages."""
83
+ pad = " " * indent
84
+ lines.append(f"{pad}- **{node.get('name', '')}/**")
85
+ modules = node.get("modules") or []
86
+ if modules:
87
+ module_pad = " " * (indent + 1)
88
+ joined = ", ".join(f"`{m}`" for m in modules)
89
+ lines.append(f"{module_pad}- {joined}")
90
+ for sub in node.get("subpackages") or []:
91
+ _append_tree_node(lines, sub, indent + 1)
92
+
93
+
94
+ def _append_package_layout(
95
+ lines: list[str], layout: list[str], tree: list[dict[str, Any]] | None
96
+ ) -> None:
97
+ """Append the `## Package layout` block — nested tree if available, flat list otherwise."""
98
+ _section_break(lines)
99
+ lines.append("## Package layout")
100
+ if tree:
101
+ for node in tree:
102
+ _append_tree_node(lines, node, indent=0)
103
+ return
104
+ for item in layout:
105
+ lines.append(f"- {item}")
106
+
107
+
108
+ def _append_build_test(lines: list[str], build_test: dict) -> None:
109
+ """Append the `## Build & test` block."""
110
+ _section_break(lines)
111
+ lines.append("## Build & test")
112
+ for k, v in build_test.items():
113
+ lines.append(f"- {k}: {v}")
114
+
115
+
116
+ def _append_ci_workflows(lines: list[str], workflows: list[dict]) -> None:
117
+ """Append the `## CI workflows` table section."""
118
+ _section_break(lines)
119
+ lines.append("## CI workflows")
120
+ lines.append("| File | Name |")
121
+ lines.append("|---|---|")
122
+ for wf in workflows:
123
+ lines.append(f"| {wf.get('file', '')} | {wf.get('name', '')} |")
124
+
125
+
126
+ def _append_publish_target(lines: list[str], pt: dict) -> None:
127
+ """Append the `## Publish target` block."""
128
+ _section_break(lines)
129
+ lines.append("## Publish target")
130
+ lines.append(f"- kind: {pt.get('kind', '')}")
131
+ lines.append(f"- workflow: {pt.get('workflow', '')}")
132
+ lines.append(f"- trigger: {pt.get('trigger', '')}")
133
+
134
+
135
+ def _append_git_remote(lines: list[str], gr: dict) -> None:
136
+ """Append the `## Git remote` block."""
137
+ _section_break(lines)
138
+ lines.append("## Git remote")
139
+ for k, v in gr.items():
140
+ lines.append(f"- {k}: {v}")
141
+
142
+
143
+ def _append_module_summaries(lines: list[str], summaries: list[dict]) -> None:
144
+ """Append the `## Module summaries` table section."""
145
+ _section_break(lines)
146
+ lines.append("## Module summaries")
147
+ lines.append("| Module | Summary |")
148
+ lines.append("|---|---|")
149
+ for entry in summaries:
150
+ lines.append(f"| {entry.get('module', '')} | {entry.get('summary', '')} |")
151
+
152
+
153
+ def _render_build_test_section(lines: list[str], profile: dict[str, Any]) -> None:
154
+ """Render the build_test section if present and non-None."""
155
+ bt = profile.get("build_test")
156
+ if bt:
157
+ _append_build_test(lines, bt)
158
+
159
+
160
+ def _render_publish_target_section(lines: list[str], profile: dict[str, Any]) -> None:
161
+ """Render the publish_target section if present and non-None."""
162
+ pt = profile.get("publish_target")
163
+ if pt:
164
+ _append_publish_target(lines, pt)
165
+
166
+
167
+ def _render_git_remote_section(lines: list[str], profile: dict[str, Any]) -> None:
168
+ """Render the git_remote section if present and non-None."""
169
+ gr = profile.get("git_remote")
170
+ if gr:
171
+ _append_git_remote(lines, gr)
172
+
173
+
174
+ def _append_project_status(lines: list[str], status: str) -> None:
175
+ """Append the `## Project status` block."""
176
+ _section_break(lines)
177
+ lines.append("## Project status")
178
+ lines.append(status)
179
+
180
+
181
+ def _append_extra(lines: list[str], extra: dict[str, Any]) -> None:
182
+ """Append the `## Extra` key/value block."""
183
+ _section_break(lines)
184
+ lines.append("## Extra")
185
+ for k, v in extra.items():
186
+ lines.append(f"- **{k}:** {v}")
187
+
188
+
189
+ def _append_github_state(lines: list[str], gs: dict[str, Any]) -> None:
190
+ """Append the `## GitHub` block for github_state data."""
191
+ _section_break(lines)
192
+ lines.append("## GitHub")
193
+ release = gs.get("latest_release")
194
+ if release:
195
+ tag = release.get("tag", "")
196
+ pub = release.get("published_at", "")
197
+ lines.append(f"- latest_release: {tag} ({pub})")
198
+ else:
199
+ lines.append("- latest_release: none")
200
+ lines.append(f"- open_issues: {gs.get('open_issues', '')}")
201
+ lines.append(f"- default_branch: {gs.get('default_branch', '')}")
202
+ lines.append(f"- ci_status_on_default: {gs.get('ci_status_on_default', '')}")
203
+
204
+
205
+ def _append_pypi_state(lines: list[str], ps: dict[str, Any]) -> None:
206
+ """Append the `## PyPI` block for pypi_state data."""
207
+ _section_break(lines)
208
+ lines.append("## PyPI")
209
+ lines.append(f"- latest_version: {ps.get('latest_version', '')}")
210
+ lines.append(f"- released_at: {ps.get('released_at', '')}")
211
+
212
+
213
+ def _render_package_layout_section(lines: list[str], profile: dict[str, Any]) -> None:
214
+ """Render the package-layout section from either the nested tree or the flat list."""
215
+ layout = profile.get("package_layout") or []
216
+ tree = profile.get("package_tree") or []
217
+ if layout or tree:
218
+ _append_package_layout(lines, layout, tree)
219
+
220
+
221
+ # Each entry renders one shallow-profile section, in display order. Most entries
222
+ # are a ``(profile-key, appender)`` pair — append iff the value is truthy.
223
+ # The package-layout slot uses a custom thunk because it pulls from two keys.
224
+ _SHALLOW_RENDERERS: list[Any] = [
225
+ ("deps_runtime", _append_deps_runtime),
226
+ _render_package_layout_section,
227
+ _render_build_test_section,
228
+ ("ci_workflows", _append_ci_workflows),
229
+ _render_publish_target_section,
230
+ _render_git_remote_section,
231
+ ("module_summaries", _append_module_summaries),
232
+ ("vendored_skills", _append_skill_table),
233
+ ("citations", _append_citation_table),
234
+ ("changelog_recent", _append_changelog),
235
+ ("claude_md_status", _append_project_status),
236
+ ("extra", _append_extra),
237
+ ("github_state", _append_github_state),
238
+ ("pypi_state", _append_pypi_state),
239
+ ]
240
+
241
+
242
+ def _append_shallow_sections(lines: list[str], profile: dict[str, Any]) -> None:
243
+ """Append every populated shallow-profile section to *lines*, in display order."""
244
+ for entry in _SHALLOW_RENDERERS:
245
+ if callable(entry):
246
+ entry(lines, profile)
247
+ continue
248
+ key, append = entry
249
+ if value := profile.get(key):
250
+ append(lines, value)
251
+
252
+
253
+ def _append_deep_sections(lines: list[str], profile: dict[str, Any]) -> None:
254
+ """Append deep-only sections (readme intro, CLAUDE.md content, commits)."""
255
+ readme = profile.get("readme_intro") or ""
256
+ if readme:
257
+ _section_break(lines)
258
+ lines.append("## Readme intro")
259
+ lines.append(readme)
260
+
261
+ md_sections = profile.get("claude_md_sections") or ""
262
+ if md_sections:
263
+ _section_break(lines)
264
+ lines.append(md_sections)
265
+
266
+ commits = profile.get("commits_recent") or []
267
+ if commits:
268
+ _section_break(lines)
269
+ lines.append("## Recent commits")
270
+ for c in commits:
271
+ lines.append(f"- {c}")
272
+
273
+
274
+ def render_profile_markdown(profile: dict[str, Any]) -> str:
275
+ """Render a profile dict (shallow or deep) as a markdown report."""
276
+ lines: list[str] = []
277
+ name = profile.get("name") or "(unknown)"
278
+ lines.append(f"# {name}")
279
+
280
+ version = profile.get("version") or ""
281
+ if version:
282
+ lines.append(f"- **Version:** {version}")
283
+
284
+ manifest = profile.get("manifest")
285
+ language = profile.get("language") or "unknown"
286
+ if manifest:
287
+ lines.append(f"- **Manifest:** {manifest} ({language})")
288
+ else:
289
+ lines.append(f"- **Manifest:** none ({language})")
290
+
291
+ path = profile.get("path") or ""
292
+ if path:
293
+ lines.append(f"- **Path:** {path}")
294
+
295
+ entry_points = profile.get("entry_points") or {}
296
+ if entry_points:
297
+ _section_break(lines)
298
+ lines.append("## Entry points")
299
+ for k, v in entry_points.items():
300
+ lines.append(f"- `{k}` → `{v}`")
301
+
302
+ _append_shallow_sections(lines, profile)
303
+ _append_deep_sections(lines, profile)
304
+
305
+ return "\n".join(lines) + "\n"
306
+
307
+
308
+ def render_error_markdown(err: SeerError) -> str:
309
+ """Render a :class:`SeerError` as a markdown error block (for stderr)."""
310
+ label = _KIND_LABEL.get(err.code, "error")
311
+ lines: list[str] = []
312
+ lines.append(f"**Error:** {err.message}")
313
+ if err.reason:
314
+ lines.append("")
315
+ lines.append(f"**Reason:** {err.reason}")
316
+ if err.remediation:
317
+ lines.append("")
318
+ lines.append(f"**Remediation:** {err.remediation}")
319
+ lines.append("")
320
+ lines.append(f"Exit code: {err.code} ({label})")
321
+ return "\n".join(lines) + "\n"
322
+
323
+
324
+ def _append_edge_section(
325
+ lines: list[str],
326
+ label: str,
327
+ es: list[dict[str, str]],
328
+ nodes_by_id: dict[str, Any],
329
+ ) -> None:
330
+ """Append one typed edge group (imports / citations / vendored) to *lines*."""
331
+ lines.append("")
332
+ lines.append(f"## {label} ({len(es)})")
333
+ for edge in es:
334
+ target = edge["to"]
335
+ node = nodes_by_id.get(target, {})
336
+ loc = node.get("path")
337
+ tag = f"({loc})" if loc else "(external)"
338
+ spec = edge.get("spec") or ""
339
+ spec_suffix = f" {spec}" if spec else ""
340
+ lines.append(f"- {target} {tag}{spec_suffix}")
341
+
342
+
343
+ def _append_walk_errors(lines: list[str], errors: list[dict[str, str]]) -> None:
344
+ """Append the walk-errors section to *lines*."""
345
+ lines.append("")
346
+ lines.append(f"## Errors during walk ({len(errors)})")
347
+ for err in errors:
348
+ lines.append("")
349
+ lines.append(f"**{err.get('node', '')}**")
350
+ if err.get("reason"):
351
+ lines.append(f"- Reason: {err['reason']}")
352
+ if err.get("remediation"):
353
+ lines.append(f"- Remediation: {err['remediation']}")
354
+
355
+
356
+ def _append_node_list(lines: list[str], nodes: list[dict[str, Any]]) -> None:
357
+ """Append internal repo list items to *lines*."""
358
+ for node in nodes:
359
+ v = node.get("version") or ""
360
+ v_suffix = f" — {v}" if v else ""
361
+ lines.append(f"- **{node['id']}** ({node.get('path', '')}){v_suffix}")
362
+
363
+
364
+ def _append_external_list(lines: list[str], nodes: list[dict[str, Any]]) -> None:
365
+ """Append external target list items to *lines*."""
366
+ for node in nodes:
367
+ lines.append(f"- {node['id']}")
368
+
369
+
370
+ def _append_graph_edges(
371
+ lines: list[str],
372
+ by_type: dict[str, list[dict[str, str]]],
373
+ ) -> None:
374
+ """Append typed edge sections to *lines*."""
375
+ for kind, label in [
376
+ ("import", "Import edges"),
377
+ ("cite", "Citation edges"),
378
+ ("vendor", "Vendor edges"),
379
+ ]:
380
+ es = by_type.get(kind)
381
+ if not es:
382
+ continue
383
+ lines.append("")
384
+ lines.append(f"## {label} ({len(es)})")
385
+ for edge in es:
386
+ spec = edge.get("spec") or ""
387
+ spec_suffix = f" {spec}" if spec else ""
388
+ lines.append(f"- {edge['from']} → {edge['to']}{spec_suffix}")
389
+
390
+
391
+ def render_graph_markdown(graph: dict[str, Any]) -> str:
392
+ """Render a workspace-graph dict as a markdown report including mermaid."""
393
+ lines: list[str] = []
394
+ roots = graph.get("roots") or []
395
+ lines.append("# Workspace graph")
396
+ if roots:
397
+ lines.append("Roots: " + ", ".join(roots))
398
+
399
+ nodes = graph.get("nodes") or []
400
+ internal = [n for n in nodes if not n.get("external")]
401
+ external = [n for n in nodes if n.get("external")]
402
+
403
+ if internal:
404
+ lines.append("")
405
+ lines.append(f"## Repos ({len(internal)})")
406
+ _append_node_list(lines, internal)
407
+
408
+ if external:
409
+ lines.append("")
410
+ lines.append(f"## External targets ({len(external)})")
411
+ _append_external_list(lines, external)
412
+
413
+ edges = graph.get("edges") or []
414
+ by_type: dict[str, list[dict[str, str]]] = {}
415
+ for edge in edges:
416
+ by_type.setdefault(edge["type"], []).append(edge)
417
+ _append_graph_edges(lines, by_type)
418
+
419
+ walk_errors = graph.get("walk_errors") or []
420
+ if walk_errors:
421
+ _append_walk_errors(lines, walk_errors)
422
+
423
+ mermaid = graph.get("mermaid") or ""
424
+ if mermaid:
425
+ lines.append("")
426
+ lines.append("## Mermaid")
427
+ lines.append("```mermaid")
428
+ lines.append(mermaid.rstrip())
429
+ lines.append("```")
430
+
431
+ return "\n".join(lines) + "\n"
432
+
433
+
434
+ def render_connections_markdown(walk: dict[str, Any]) -> str:
435
+ """Render a connections-walk dict as a markdown report."""
436
+ name = walk.get("seed_name") or "(unknown)"
437
+ depth = walk.get("depth")
438
+ lines: list[str] = []
439
+ lines.append(f"# {name} — connections (depth {depth})")
440
+ seed_path = walk.get("seed")
441
+ if seed_path:
442
+ lines.append(f"Seed: {seed_path}")
443
+
444
+ edges = walk.get("edges") or []
445
+ nodes_by_id: dict[str, Any] = {n["id"]: n for n in (walk.get("nodes") or [])}
446
+
447
+ by_type: dict[str, list[dict[str, str]]] = {}
448
+ for edge in edges:
449
+ by_type.setdefault(edge["type"], []).append(edge)
450
+
451
+ for kind, label in [
452
+ ("import", "Imports"),
453
+ ("cite", "Citations"),
454
+ ("vendor", "Vendored skills"),
455
+ ]:
456
+ edge_group = by_type.get(kind)
457
+ if edge_group:
458
+ _append_edge_section(lines, label, edge_group, nodes_by_id)
459
+
460
+ errors = walk.get("walk_errors") or []
461
+ if errors:
462
+ _append_walk_errors(lines, errors)
463
+
464
+ internal = sum(1 for n in nodes_by_id.values() if not n.get("external"))
465
+ external = sum(1 for n in nodes_by_id.values() if n.get("external"))
466
+ lines.append("")
467
+ lines.append("## Summary")
468
+ lines.append(f"{internal} internal node(s), {external} external; {len(edges)} edge(s) total.")
469
+
470
+ return "\n".join(lines) + "\n"