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.
Files changed (46) hide show
  1. {sumd-0.3.30 → sumd-0.3.32}/CHANGELOG.md +27 -0
  2. {sumd-0.3.30 → sumd-0.3.32}/PKG-INFO +6 -6
  3. {sumd-0.3.30 → sumd-0.3.32}/README.md +5 -5
  4. {sumd-0.3.30 → sumd-0.3.32}/pyproject.toml +1 -1
  5. {sumd-0.3.30 → sumd-0.3.32}/sumd/__init__.py +1 -1
  6. sumd-0.3.32/sumd/sections/api_stubs.py +76 -0
  7. sumd-0.3.32/sumd/sections/call_graph.py +156 -0
  8. sumd-0.3.32/sumd/sections/code_analysis.py +68 -0
  9. sumd-0.3.32/sumd/sections/configuration.py +44 -0
  10. sumd-0.3.32/sumd/sections/dependencies.py +97 -0
  11. sumd-0.3.32/sumd/sections/deployment.py +111 -0
  12. sumd-0.3.32/sumd/sections/extras.py +72 -0
  13. sumd-0.3.32/sumd/sections/quality.py +82 -0
  14. sumd-0.3.32/sumd/sections/source_snippets.py +69 -0
  15. sumd-0.3.32/sumd/sections/test_contracts.py +78 -0
  16. sumd-0.3.32/sumd/sections/workflows.py +86 -0
  17. sumd-0.3.30/sumd/sections/api_stubs.py +0 -25
  18. sumd-0.3.30/sumd/sections/call_graph.py +0 -27
  19. sumd-0.3.30/sumd/sections/code_analysis.py +0 -25
  20. sumd-0.3.30/sumd/sections/configuration.py +0 -21
  21. sumd-0.3.30/sumd/sections/dependencies.py +0 -26
  22. sumd-0.3.30/sumd/sections/deployment.py +0 -23
  23. sumd-0.3.30/sumd/sections/extras.py +0 -21
  24. sumd-0.3.30/sumd/sections/quality.py +0 -21
  25. sumd-0.3.30/sumd/sections/source_snippets.py +0 -26
  26. sumd-0.3.30/sumd/sections/test_contracts.py +0 -25
  27. sumd-0.3.30/sumd/sections/workflows.py +0 -21
  28. {sumd-0.3.30 → sumd-0.3.32}/.gitignore +0 -0
  29. {sumd-0.3.30 → sumd-0.3.32}/LICENSE +0 -0
  30. {sumd-0.3.30 → sumd-0.3.32}/sumd/cli.py +0 -0
  31. {sumd-0.3.30 → sumd-0.3.32}/sumd/extractor.py +0 -0
  32. {sumd-0.3.30 → sumd-0.3.32}/sumd/generator.py +0 -0
  33. {sumd-0.3.30 → sumd-0.3.32}/sumd/mcp_server.py +0 -0
  34. {sumd-0.3.30 → sumd-0.3.32}/sumd/models.py +0 -0
  35. {sumd-0.3.30 → sumd-0.3.32}/sumd/parser.py +0 -0
  36. {sumd-0.3.30 → sumd-0.3.32}/sumd/pipeline.py +0 -0
  37. {sumd-0.3.30 → sumd-0.3.32}/sumd/renderer.py +0 -0
  38. {sumd-0.3.30 → sumd-0.3.32}/sumd/sections/__init__.py +0 -0
  39. {sumd-0.3.30 → sumd-0.3.32}/sumd/sections/architecture.py +0 -0
  40. {sumd-0.3.30 → sumd-0.3.32}/sumd/sections/base.py +0 -0
  41. {sumd-0.3.30 → sumd-0.3.32}/sumd/sections/environment.py +0 -0
  42. {sumd-0.3.30 → sumd-0.3.32}/sumd/sections/interfaces.py +0 -0
  43. {sumd-0.3.30 → sumd-0.3.32}/sumd/sections/metadata.py +0 -0
  44. {sumd-0.3.30 → sumd-0.3.32}/sumd/sections/refactor_analysis.py +0 -0
  45. {sumd-0.3.30 → sumd-0.3.32}/sumd/toon_parser.py +0 -0
  46. {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.30
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
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.3.30-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
45
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-20.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
44
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.3.32-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
45
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-21.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
46
46
 
47
- - 🤖 **LLM usage:** $7.5000 (60 commits)
48
- - 👤 **Human dev:** ~$2071 (20.7h @ $100/h, 30min dedup)
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
- ![Version](https://img.shields.io/badge/version-0.3.30-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
54
+ ![Version](https://img.shields.io/badge/version-0.3.32-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
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
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.3.30-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-20.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.3.32-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-21.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.5000 (60 commits)
10
- - 👤 **Human dev:** ~$2071 (20.7h @ $100/h, 30min dedup)
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
- ![Version](https://img.shields.io/badge/version-0.3.30-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
16
+ ![Version](https://img.shields.io/badge/version-0.3.32-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
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 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sumd"
7
- version = "0.3.30"
7
+ version = "0.3.32"
8
8
  description = "SUMD - Structured Unified Markdown Descriptor for AI-aware project documentation"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -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.30"
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)