sumd 0.3.30__tar.gz → 0.3.32__tar.gz
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.
- {sumd-0.3.30 → sumd-0.3.32}/CHANGELOG.md +27 -0
- {sumd-0.3.30 → sumd-0.3.32}/PKG-INFO +6 -6
- {sumd-0.3.30 → sumd-0.3.32}/README.md +5 -5
- {sumd-0.3.30 → sumd-0.3.32}/pyproject.toml +1 -1
- {sumd-0.3.30 → sumd-0.3.32}/sumd/__init__.py +1 -1
- sumd-0.3.32/sumd/sections/api_stubs.py +76 -0
- sumd-0.3.32/sumd/sections/call_graph.py +156 -0
- sumd-0.3.32/sumd/sections/code_analysis.py +68 -0
- sumd-0.3.32/sumd/sections/configuration.py +44 -0
- sumd-0.3.32/sumd/sections/dependencies.py +97 -0
- sumd-0.3.32/sumd/sections/deployment.py +111 -0
- sumd-0.3.32/sumd/sections/extras.py +72 -0
- sumd-0.3.32/sumd/sections/quality.py +82 -0
- sumd-0.3.32/sumd/sections/source_snippets.py +69 -0
- sumd-0.3.32/sumd/sections/test_contracts.py +78 -0
- sumd-0.3.32/sumd/sections/workflows.py +86 -0
- sumd-0.3.30/sumd/sections/api_stubs.py +0 -25
- sumd-0.3.30/sumd/sections/call_graph.py +0 -27
- sumd-0.3.30/sumd/sections/code_analysis.py +0 -25
- sumd-0.3.30/sumd/sections/configuration.py +0 -21
- sumd-0.3.30/sumd/sections/dependencies.py +0 -26
- sumd-0.3.30/sumd/sections/deployment.py +0 -23
- sumd-0.3.30/sumd/sections/extras.py +0 -21
- sumd-0.3.30/sumd/sections/quality.py +0 -21
- sumd-0.3.30/sumd/sections/source_snippets.py +0 -26
- sumd-0.3.30/sumd/sections/test_contracts.py +0 -25
- sumd-0.3.30/sumd/sections/workflows.py +0 -21
- {sumd-0.3.30 → sumd-0.3.32}/.gitignore +0 -0
- {sumd-0.3.30 → sumd-0.3.32}/LICENSE +0 -0
- {sumd-0.3.30 → sumd-0.3.32}/sumd/cli.py +0 -0
- {sumd-0.3.30 → sumd-0.3.32}/sumd/extractor.py +0 -0
- {sumd-0.3.30 → sumd-0.3.32}/sumd/generator.py +0 -0
- {sumd-0.3.30 → sumd-0.3.32}/sumd/mcp_server.py +0 -0
- {sumd-0.3.30 → sumd-0.3.32}/sumd/models.py +0 -0
- {sumd-0.3.30 → sumd-0.3.32}/sumd/parser.py +0 -0
- {sumd-0.3.30 → sumd-0.3.32}/sumd/pipeline.py +0 -0
- {sumd-0.3.30 → sumd-0.3.32}/sumd/renderer.py +0 -0
- {sumd-0.3.30 → sumd-0.3.32}/sumd/sections/__init__.py +0 -0
- {sumd-0.3.30 → sumd-0.3.32}/sumd/sections/architecture.py +0 -0
- {sumd-0.3.30 → sumd-0.3.32}/sumd/sections/base.py +0 -0
- {sumd-0.3.30 → sumd-0.3.32}/sumd/sections/environment.py +0 -0
- {sumd-0.3.30 → sumd-0.3.32}/sumd/sections/interfaces.py +0 -0
- {sumd-0.3.30 → sumd-0.3.32}/sumd/sections/metadata.py +0 -0
- {sumd-0.3.30 → sumd-0.3.32}/sumd/sections/refactor_analysis.py +0 -0
- {sumd-0.3.30 → sumd-0.3.32}/sumd/toon_parser.py +0 -0
- {sumd-0.3.30 → sumd-0.3.32}/sumd/validator.py +0 -0
|
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.32] - 2026-04-23
|
|
11
|
+
|
|
12
|
+
### Docs
|
|
13
|
+
- Update README.md
|
|
14
|
+
|
|
15
|
+
### Other
|
|
16
|
+
- Update project.sh
|
|
17
|
+
- Update sumd/sections/api_stubs.py
|
|
18
|
+
- Update sumd/sections/call_graph.py
|
|
19
|
+
- Update sumd/sections/code_analysis.py
|
|
20
|
+
- Update sumd/sections/configuration.py
|
|
21
|
+
- Update sumd/sections/dependencies.py
|
|
22
|
+
- Update sumd/sections/deployment.py
|
|
23
|
+
- Update sumd/sections/extras.py
|
|
24
|
+
- Update sumd/sections/source_snippets.py
|
|
25
|
+
- Update sumd/sections/test_contracts.py
|
|
26
|
+
|
|
27
|
+
## [0.3.31] - 2026-04-23
|
|
28
|
+
|
|
29
|
+
### Docs
|
|
30
|
+
- Update README.md
|
|
31
|
+
|
|
32
|
+
### Other
|
|
33
|
+
- Update sumd/sections/interfaces.py
|
|
34
|
+
- Update sumd/sections/quality.py
|
|
35
|
+
- Update sumd/sections/workflows.py
|
|
36
|
+
|
|
10
37
|
## [0.3.30] - 2026-04-23
|
|
11
38
|
|
|
12
39
|
### Docs
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sumd
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.32
|
|
4
4
|
Summary: SUMD - Structured Unified Markdown Descriptor for AI-aware project documentation
|
|
5
5
|
Project-URL: Homepage, https://github.com/tom/statement
|
|
6
6
|
Project-URL: Repository, https://github.com/tom/statement
|
|
@@ -41,17 +41,17 @@ Description-Content-Type: text/markdown
|
|
|
41
41
|
|
|
42
42
|
## AI Cost Tracking
|
|
43
43
|
|
|
44
|
-
     
|
|
45
|
+
  
|
|
46
46
|
|
|
47
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
48
|
-
- 👤 **Human dev:** ~$
|
|
47
|
+
- 🤖 **LLM usage:** $7.5000 (62 commits)
|
|
48
|
+
- 👤 **Human dev:** ~$2098 (21.0h @ $100/h, 30min dedup)
|
|
49
49
|
|
|
50
50
|
Generated on 2026-04-23 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
51
51
|
|
|
52
52
|
---
|
|
53
53
|
|
|
54
|
-
  
|
|
55
55
|
|
|
56
56
|
**SUMD** (Structured Unified Markdown Descriptor) is a semantic project descriptor format in Markdown.
|
|
57
57
|
It defines intent, structure, execution entry points, and the mental model of a system for both humans and LLMs.
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
     
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $7.5000 (62 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$2098 (21.0h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
12
|
Generated on 2026-04-23 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
  
|
|
17
17
|
|
|
18
18
|
**SUMD** (Structured Unified Markdown Descriptor) is a semantic project descriptor format in Markdown.
|
|
19
19
|
It defines intent, structure, execution entry points, and the mental model of a system for both humans and LLMs.
|
|
@@ -4,7 +4,7 @@ SUMD is a semantic project descriptor format in Markdown that defines intent,
|
|
|
4
4
|
structure, execution entry points, and mental model of a system for both humans and LLMs.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "0.3.
|
|
7
|
+
__version__ = "0.3.32"
|
|
8
8
|
|
|
9
9
|
from sumd.models import SUMDDocument
|
|
10
10
|
from sumd.parser import (
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""sumd.sections.api_stubs — ApiStubsSection.
|
|
2
|
+
|
|
3
|
+
Renders OpenAPI endpoints as Python-like typed stubs for LLM orientation.
|
|
4
|
+
LLM sees function signatures and HTTP method/path without reading full openapi.yaml.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from sumd.sections.base import RenderContext, Section
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# Private renderers (moved from renderer.py)
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _render_api_stubs(openapi: dict) -> list[str]:
|
|
18
|
+
"""Render OpenAPI endpoints as Python-like typed stubs for LLM orientation."""
|
|
19
|
+
endpoints = openapi.get("endpoints", [])
|
|
20
|
+
schemas = openapi.get("schemas", [])
|
|
21
|
+
if not endpoints:
|
|
22
|
+
return []
|
|
23
|
+
L: list[str] = []
|
|
24
|
+
a = L.append
|
|
25
|
+
a("## API Stubs")
|
|
26
|
+
a("")
|
|
27
|
+
title = openapi.get("title", "")
|
|
28
|
+
version = openapi.get("version", "")
|
|
29
|
+
if title:
|
|
30
|
+
a(f"*{title} v{version} — auto-generated stubs from `openapi.yaml`.*")
|
|
31
|
+
a("")
|
|
32
|
+
|
|
33
|
+
# Group by tag for structure
|
|
34
|
+
by_tag: dict[str, list[dict]] = {}
|
|
35
|
+
for ep in endpoints:
|
|
36
|
+
tag = ep["tags"][0] if ep.get("tags") else "default"
|
|
37
|
+
by_tag.setdefault(tag, []).append(ep)
|
|
38
|
+
|
|
39
|
+
a("```python markpact:openapi path=openapi.yaml")
|
|
40
|
+
for tag, eps in by_tag.items():
|
|
41
|
+
a(f"# {tag}")
|
|
42
|
+
for ep in eps:
|
|
43
|
+
op_id = (
|
|
44
|
+
ep.get("operationId")
|
|
45
|
+
or f"{ep['method'].lower()}_{ep['path'].replace('/', '_').strip('_')}"
|
|
46
|
+
)
|
|
47
|
+
summary = f" # {ep['summary']}" if ep.get("summary") else ""
|
|
48
|
+
a(f"def {op_id}() -> Response:{summary}")
|
|
49
|
+
a(f' "{ep["method"]} {ep["path"]}"')
|
|
50
|
+
a("")
|
|
51
|
+
a("```")
|
|
52
|
+
a("")
|
|
53
|
+
if schemas:
|
|
54
|
+
a("**Schemas**: " + ", ".join(f"`{s}`" for s in schemas))
|
|
55
|
+
a("")
|
|
56
|
+
return L
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# Section class
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ApiStubsSection:
|
|
65
|
+
name = "api_stubs"
|
|
66
|
+
level = 2
|
|
67
|
+
profiles = frozenset({"rich"})
|
|
68
|
+
|
|
69
|
+
def should_render(self, ctx: RenderContext) -> bool:
|
|
70
|
+
return bool(ctx.openapi.get("endpoints"))
|
|
71
|
+
|
|
72
|
+
def render(self, ctx: RenderContext) -> list[str]:
|
|
73
|
+
return _render_api_stubs(ctx.openapi)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
assert isinstance(ApiStubsSection(), Section)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""sumd.sections.call_graph — CallGraphSection.
|
|
2
|
+
|
|
3
|
+
Renders a summary of the call graph (HUBS table + degree stats) from
|
|
4
|
+
calls.toon.yaml in project/, followed by the full embed for LLM reference.
|
|
5
|
+
|
|
6
|
+
Replaces the raw dump that would otherwise appear in CodeAnalysis.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
from sumd.sections.base import RenderContext, Section
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Private renderers (moved from renderer.py)
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse_calls_header(lines: list[str]) -> dict:
|
|
22
|
+
"""Parse node/edge/module counts and CC average from header comments."""
|
|
23
|
+
result = {"nodes": 0, "edges": 0, "modules_count": 0, "cc_avg": 0.0}
|
|
24
|
+
for line in lines[:5]:
|
|
25
|
+
if line.startswith("# nodes:"):
|
|
26
|
+
m = re.search(
|
|
27
|
+
r"nodes:\s*(\d+).*edges:\s*(\d+).*modules:\s*(\d+)", line
|
|
28
|
+
)
|
|
29
|
+
if m:
|
|
30
|
+
result["nodes"] = int(m.group(1))
|
|
31
|
+
result["edges"] = int(m.group(2))
|
|
32
|
+
result["modules_count"] = int(m.group(3))
|
|
33
|
+
if line.startswith("# CC"):
|
|
34
|
+
m = re.search(r"CC\u0304=?\s*([\d.]+)", line)
|
|
35
|
+
if m:
|
|
36
|
+
result["cc_avg"] = float(m.group(1))
|
|
37
|
+
return result
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _parse_hub_stat_line(line: str) -> dict | None:
|
|
41
|
+
"""Extract CC/in/out/total metrics from a hub stats line, or return None."""
|
|
42
|
+
m = re.search(r"CC=(\d+)\s+in:(\d+)\s+out:(\d+)\s+total:(\d+)", line)
|
|
43
|
+
if not m:
|
|
44
|
+
return None
|
|
45
|
+
return {
|
|
46
|
+
"cc": int(m.group(1)),
|
|
47
|
+
"in": int(m.group(2)),
|
|
48
|
+
"out": int(m.group(3)),
|
|
49
|
+
"total": int(m.group(4)),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _process_in_hubs_line(line: str, hubs: list, current_hub: dict) -> dict:
|
|
54
|
+
"""Process a single indented line while inside the HUBS block."""
|
|
55
|
+
if line.startswith(" "):
|
|
56
|
+
stats = _parse_hub_stat_line(line)
|
|
57
|
+
if stats and current_hub:
|
|
58
|
+
current_hub.update(stats)
|
|
59
|
+
elif line.startswith(" "):
|
|
60
|
+
if current_hub:
|
|
61
|
+
hubs.append(current_hub)
|
|
62
|
+
current_hub = {"name": line.strip()}
|
|
63
|
+
return current_hub
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _parse_calls_hubs(lines: list[str]) -> list[dict]:
|
|
67
|
+
"""Parse HUBS section into list of hub dicts."""
|
|
68
|
+
hubs: list[dict] = []
|
|
69
|
+
in_hubs = False
|
|
70
|
+
current_hub: dict = {}
|
|
71
|
+
for line in lines:
|
|
72
|
+
if line.startswith("HUBS["):
|
|
73
|
+
in_hubs = True
|
|
74
|
+
continue
|
|
75
|
+
if in_hubs and line and not line.startswith(" "):
|
|
76
|
+
in_hubs = False
|
|
77
|
+
if in_hubs:
|
|
78
|
+
current_hub = _process_in_hubs_line(line, hubs, current_hub)
|
|
79
|
+
if current_hub:
|
|
80
|
+
hubs.append(current_hub)
|
|
81
|
+
return hubs
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _parse_calls_toon(content: str) -> dict:
|
|
85
|
+
"""Parse calls.toon.yaml text into structured dict for rendering."""
|
|
86
|
+
lines = content.splitlines()
|
|
87
|
+
return {
|
|
88
|
+
**_parse_calls_header(lines),
|
|
89
|
+
"hubs": _parse_calls_hubs(lines),
|
|
90
|
+
"modules": [],
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _render_call_graph(project_analysis: list) -> list[str]:
|
|
95
|
+
"""Render call graph summary from calls.toon.yaml in project_analysis."""
|
|
96
|
+
calls_entry = next(
|
|
97
|
+
(e for e in project_analysis if "calls.toon" in e.get("file", "")), None
|
|
98
|
+
)
|
|
99
|
+
if not calls_entry:
|
|
100
|
+
return []
|
|
101
|
+
|
|
102
|
+
data = _parse_calls_toon(calls_entry["content"])
|
|
103
|
+
if not data["hubs"]:
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
L: list[str] = []
|
|
107
|
+
a = L.append
|
|
108
|
+
a("## Call Graph")
|
|
109
|
+
a("")
|
|
110
|
+
a(
|
|
111
|
+
f"*{data['nodes']} nodes · {data['edges']} edges · {data['modules_count']} modules · CC\u0304={data['cc_avg']}*"
|
|
112
|
+
)
|
|
113
|
+
a("")
|
|
114
|
+
|
|
115
|
+
# Top hubs table (top 8 by total degree)
|
|
116
|
+
top_hubs = sorted(data["hubs"], key=lambda h: h.get("total", 0), reverse=True)[:8]
|
|
117
|
+
a("### Hubs (by degree)")
|
|
118
|
+
a("")
|
|
119
|
+
a("| Function | CC | in | out | total |")
|
|
120
|
+
a("|----------|----|----|-----|-------|")
|
|
121
|
+
for hub in top_hubs:
|
|
122
|
+
name = hub["name"].split(".")[-1] # short name
|
|
123
|
+
module = ".".join(hub["name"].split(".")[:-1])
|
|
124
|
+
cc_flag = " ⚠" if hub.get("cc", 0) >= 10 else ""
|
|
125
|
+
a(
|
|
126
|
+
f"| `{name}` *(in {module})* | {hub.get('cc', 0)}{cc_flag} | {hub.get('in', 0)} | {hub.get('out', 0)} | **{hub.get('total', 0)}** |"
|
|
127
|
+
)
|
|
128
|
+
a("")
|
|
129
|
+
|
|
130
|
+
# Full embed for LLM reference under markpact tag
|
|
131
|
+
rel = calls_entry["file"]
|
|
132
|
+
a(f"```toon markpact:analysis path={rel}")
|
|
133
|
+
a(calls_entry["content"].rstrip())
|
|
134
|
+
a("```")
|
|
135
|
+
a("")
|
|
136
|
+
return L
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# Section class
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class CallGraphSection:
|
|
145
|
+
name = "call_graph"
|
|
146
|
+
level = 2
|
|
147
|
+
profiles = frozenset({"rich", "refactor"})
|
|
148
|
+
|
|
149
|
+
def should_render(self, ctx: RenderContext) -> bool:
|
|
150
|
+
return any("calls.toon" in e.get("file", "") for e in ctx.project_analysis)
|
|
151
|
+
|
|
152
|
+
def render(self, ctx: RenderContext) -> list[str]:
|
|
153
|
+
return _render_call_graph(ctx.project_analysis)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
assert isinstance(CallGraphSection(), Section)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""sumd.sections.code_analysis — CodeAnalysisSection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from sumd.sections.base import RenderContext, Section
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# ---------------------------------------------------------------------------
|
|
9
|
+
# Private renderers (moved from renderer.py)
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _render_code_analysis(
|
|
14
|
+
project_analysis: list, skip_files: set[str] | None = None
|
|
15
|
+
) -> list[str]:
|
|
16
|
+
"""Render Code Analysis section, optionally skipping files handled by other sections."""
|
|
17
|
+
skip_files = skip_files or set()
|
|
18
|
+
entries = [
|
|
19
|
+
e
|
|
20
|
+
for e in project_analysis
|
|
21
|
+
if not any(s in e.get("file", "") for s in skip_files)
|
|
22
|
+
]
|
|
23
|
+
if not entries:
|
|
24
|
+
return []
|
|
25
|
+
L: list[str] = []
|
|
26
|
+
a = L.append
|
|
27
|
+
a("## Code Analysis")
|
|
28
|
+
a("")
|
|
29
|
+
for entry in entries:
|
|
30
|
+
a(f"### `{entry['file']}`")
|
|
31
|
+
a("")
|
|
32
|
+
lang = entry["lang"]
|
|
33
|
+
if lang == "markdown":
|
|
34
|
+
a(entry["content"])
|
|
35
|
+
elif lang == "text":
|
|
36
|
+
a(f"```text markpact:analysis path={entry['file']}")
|
|
37
|
+
a(entry["content"])
|
|
38
|
+
a("```")
|
|
39
|
+
else:
|
|
40
|
+
a(f"```{lang} markpact:analysis path={entry['file']}")
|
|
41
|
+
a(entry["content"])
|
|
42
|
+
a("```")
|
|
43
|
+
a("")
|
|
44
|
+
return L
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Section class
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CodeAnalysisSection:
|
|
53
|
+
name = "code_analysis"
|
|
54
|
+
level = 2
|
|
55
|
+
profiles = frozenset({"rich"})
|
|
56
|
+
|
|
57
|
+
def should_render(self, ctx: RenderContext) -> bool:
|
|
58
|
+
# Skip if ALL entries are calls.toon (handled by CallGraphSection)
|
|
59
|
+
non_calls = [
|
|
60
|
+
e for e in ctx.project_analysis if "calls.toon" not in e.get("file", "")
|
|
61
|
+
]
|
|
62
|
+
return bool(non_calls)
|
|
63
|
+
|
|
64
|
+
def render(self, ctx: RenderContext) -> list[str]:
|
|
65
|
+
return _render_code_analysis(ctx.project_analysis, skip_files={"calls.toon"})
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
assert isinstance(CodeAnalysisSection(), Section)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""sumd.sections.configuration — ConfigurationSection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from sumd.sections.base import RenderContext, Section
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# ---------------------------------------------------------------------------
|
|
9
|
+
# Private renderers (moved from renderer.py)
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _render_configuration_section(name: str, version: str) -> list[str]:
|
|
14
|
+
return [
|
|
15
|
+
"## Configuration",
|
|
16
|
+
"",
|
|
17
|
+
"```yaml",
|
|
18
|
+
"project:",
|
|
19
|
+
f" name: {name}",
|
|
20
|
+
f" version: {version}",
|
|
21
|
+
" env: local",
|
|
22
|
+
"```",
|
|
23
|
+
"",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Section class
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ConfigurationSection:
|
|
33
|
+
name = "configuration"
|
|
34
|
+
level = 2
|
|
35
|
+
profiles = frozenset({"light", "rich"})
|
|
36
|
+
|
|
37
|
+
def should_render(self, ctx: RenderContext) -> bool:
|
|
38
|
+
return True
|
|
39
|
+
|
|
40
|
+
def render(self, ctx: RenderContext) -> list[str]:
|
|
41
|
+
return _render_configuration_section(ctx.name, ctx.version)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
assert isinstance(ConfigurationSection(), Section)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""sumd.sections.dependencies — DependenciesSection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from sumd.sections.base import RenderContext, Section
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# ---------------------------------------------------------------------------
|
|
9
|
+
# Private renderers (moved from renderer.py)
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _render_deps_runtime(deps: list, node_deps: list, L: list[str]) -> None:
|
|
14
|
+
a = L.append
|
|
15
|
+
if deps:
|
|
16
|
+
a("### Runtime")
|
|
17
|
+
a("")
|
|
18
|
+
a("```text markpact:deps python")
|
|
19
|
+
for dep in deps:
|
|
20
|
+
a(dep)
|
|
21
|
+
a("```")
|
|
22
|
+
a("")
|
|
23
|
+
elif node_deps:
|
|
24
|
+
a("### Runtime (Node.js)")
|
|
25
|
+
a("")
|
|
26
|
+
a("```text markpact:deps node")
|
|
27
|
+
for dep in node_deps[:30]:
|
|
28
|
+
a(dep)
|
|
29
|
+
if len(node_deps) > 30:
|
|
30
|
+
a(f"# (+{len(node_deps) - 30} more)")
|
|
31
|
+
a("```")
|
|
32
|
+
a("")
|
|
33
|
+
else:
|
|
34
|
+
a("### Runtime")
|
|
35
|
+
a("")
|
|
36
|
+
a("*(see pyproject.toml)*")
|
|
37
|
+
a("")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _render_deps_dev(dev_deps: list, node_dev: list, L: list[str]) -> None:
|
|
41
|
+
a = L.append
|
|
42
|
+
if dev_deps:
|
|
43
|
+
a("### Development")
|
|
44
|
+
a("")
|
|
45
|
+
a("```text markpact:deps python scope=dev")
|
|
46
|
+
for dep in dev_deps:
|
|
47
|
+
a(dep)
|
|
48
|
+
a("```")
|
|
49
|
+
a("")
|
|
50
|
+
elif node_dev:
|
|
51
|
+
a("### Development (Node.js)")
|
|
52
|
+
a("")
|
|
53
|
+
a("```text markpact:deps node scope=dev")
|
|
54
|
+
for dep in node_dev[:20]:
|
|
55
|
+
a(dep)
|
|
56
|
+
if len(node_dev) > 20:
|
|
57
|
+
a(f"# (+{len(node_dev) - 20} more)")
|
|
58
|
+
a("```")
|
|
59
|
+
a("")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _render_dependencies(
|
|
63
|
+
deps: list, dev_deps: list, pkg_json: dict | None = None
|
|
64
|
+
) -> list[str]:
|
|
65
|
+
pkg_json = pkg_json or {}
|
|
66
|
+
L: list[str] = []
|
|
67
|
+
a = L.append
|
|
68
|
+
a("## Dependencies")
|
|
69
|
+
a("")
|
|
70
|
+
_render_deps_runtime(deps, pkg_json.get("dependencies", []), L)
|
|
71
|
+
_render_deps_dev(dev_deps, pkg_json.get("devDependencies", []), L)
|
|
72
|
+
return L
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# Section class
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class DependenciesSection:
|
|
81
|
+
name = "dependencies"
|
|
82
|
+
level = 2
|
|
83
|
+
profiles = frozenset({"light", "rich"})
|
|
84
|
+
|
|
85
|
+
def should_render(self, ctx: RenderContext) -> bool:
|
|
86
|
+
return bool(
|
|
87
|
+
ctx.deps
|
|
88
|
+
or ctx.dev_deps
|
|
89
|
+
or ctx.pkg_json.get("dependencies")
|
|
90
|
+
or ctx.pkg_json.get("devDependencies")
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def render(self, ctx: RenderContext) -> list[str]:
|
|
94
|
+
return _render_dependencies(ctx.deps, ctx.dev_deps, ctx.pkg_json)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
assert isinstance(DependenciesSection(), Section)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""sumd.sections.deployment — DeploymentSection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from sumd.sections.base import RenderContext, Section
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# ---------------------------------------------------------------------------
|
|
9
|
+
# Private renderers (moved from renderer.py)
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _render_deployment_install(pkg_json: dict, name: str, L: list[str]) -> None:
|
|
14
|
+
a = L.append
|
|
15
|
+
if pkg_json.get("name"):
|
|
16
|
+
a("```bash markpact:run")
|
|
17
|
+
a(f"npm install {pkg_json['name']}")
|
|
18
|
+
a("```")
|
|
19
|
+
else:
|
|
20
|
+
a("```bash markpact:run")
|
|
21
|
+
a(f"pip install {name}")
|
|
22
|
+
a("")
|
|
23
|
+
a("# development install")
|
|
24
|
+
a("pip install -e .[dev]")
|
|
25
|
+
a("```")
|
|
26
|
+
a("")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _render_deployment_reqs(reqs: list, L: list[str]) -> None:
|
|
30
|
+
if not reqs:
|
|
31
|
+
return
|
|
32
|
+
a = L.append
|
|
33
|
+
a("### Requirements Files")
|
|
34
|
+
a("")
|
|
35
|
+
for r in reqs:
|
|
36
|
+
a(f"#### `{r['file']}`")
|
|
37
|
+
a("")
|
|
38
|
+
for dep in r["deps"][:20]:
|
|
39
|
+
a(f"- `{dep}`")
|
|
40
|
+
if len(r["deps"]) > 20:
|
|
41
|
+
a(f"- *(+{len(r['deps']) - 20} more)*")
|
|
42
|
+
a("")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _render_dockerfile_info(dockerfile: dict, a) -> None:
|
|
46
|
+
"""Append Dockerfile section lines to output via *a*."""
|
|
47
|
+
a("### Docker")
|
|
48
|
+
a("")
|
|
49
|
+
a(f"- **base image**: `{dockerfile['from']}`")
|
|
50
|
+
if dockerfile["ports"]:
|
|
51
|
+
a(f"- **expose**: {', '.join(f'`{p}`' for p in dockerfile['ports'])}")
|
|
52
|
+
if dockerfile["entrypoint"]:
|
|
53
|
+
a(f"- **entrypoint**: `{dockerfile['entrypoint']}`")
|
|
54
|
+
if dockerfile["labels"]:
|
|
55
|
+
for k, v in dockerfile["labels"].items():
|
|
56
|
+
a(f"- **label** `{k}`: {v}")
|
|
57
|
+
a("")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _render_deployment_docker(dockerfile: dict, compose: dict, L: list[str]) -> None:
|
|
61
|
+
a = L.append
|
|
62
|
+
if dockerfile:
|
|
63
|
+
_render_dockerfile_info(dockerfile, a)
|
|
64
|
+
if compose.get("services"):
|
|
65
|
+
a(f"### Docker Compose (`{compose['file']}`)")
|
|
66
|
+
a("")
|
|
67
|
+
for svc in compose["services"]:
|
|
68
|
+
ports_str = (
|
|
69
|
+
", ".join(f"`{p}`" for p in svc["ports"]) if svc["ports"] else ""
|
|
70
|
+
)
|
|
71
|
+
image_str = f" image=`{svc['image']}`" if svc["image"] else ""
|
|
72
|
+
a(
|
|
73
|
+
f"- **{svc['name']}**{image_str}"
|
|
74
|
+
+ (f" ports: {ports_str}" if ports_str else "")
|
|
75
|
+
)
|
|
76
|
+
a("")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _render_deployment(
|
|
80
|
+
pkg_json: dict, name: str, reqs: list, dockerfile: dict, compose: dict
|
|
81
|
+
) -> list[str]:
|
|
82
|
+
L: list[str] = []
|
|
83
|
+
a = L.append
|
|
84
|
+
a("## Deployment")
|
|
85
|
+
a("")
|
|
86
|
+
_render_deployment_install(pkg_json, name, L)
|
|
87
|
+
_render_deployment_reqs(reqs, L)
|
|
88
|
+
_render_deployment_docker(dockerfile, compose, L)
|
|
89
|
+
return L
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# Section class
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class DeploymentSection:
|
|
98
|
+
name = "deployment"
|
|
99
|
+
level = 2
|
|
100
|
+
profiles = frozenset({"light", "rich"})
|
|
101
|
+
|
|
102
|
+
def should_render(self, ctx: RenderContext) -> bool:
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
def render(self, ctx: RenderContext) -> list[str]:
|
|
106
|
+
return _render_deployment(
|
|
107
|
+
ctx.pkg_json, ctx.name, ctx.reqs, ctx.dockerfile, ctx.compose
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
assert isinstance(DeploymentSection(), Section)
|