sumd 0.3.28__tar.gz → 0.3.30__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 (37) hide show
  1. {sumd-0.3.28 → sumd-0.3.30}/CHANGELOG.md +31 -0
  2. {sumd-0.3.28 → sumd-0.3.30}/PKG-INFO +32 -21
  3. {sumd-0.3.28 → sumd-0.3.30}/README.md +14 -3
  4. {sumd-0.3.28 → sumd-0.3.30}/pyproject.toml +18 -18
  5. {sumd-0.3.28 → sumd-0.3.30}/sumd/__init__.py +1 -1
  6. {sumd-0.3.28 → sumd-0.3.30}/sumd/pipeline.py +120 -4
  7. sumd-0.3.30/sumd/sections/architecture.py +156 -0
  8. sumd-0.3.30/sumd/sections/interfaces.py +157 -0
  9. sumd-0.3.28/sumd/sections/architecture.py +0 -23
  10. sumd-0.3.28/sumd/sections/interfaces.py +0 -23
  11. {sumd-0.3.28 → sumd-0.3.30}/.gitignore +0 -0
  12. {sumd-0.3.28 → sumd-0.3.30}/LICENSE +0 -0
  13. {sumd-0.3.28 → sumd-0.3.30}/sumd/cli.py +0 -0
  14. {sumd-0.3.28 → sumd-0.3.30}/sumd/extractor.py +0 -0
  15. {sumd-0.3.28 → sumd-0.3.30}/sumd/generator.py +0 -0
  16. {sumd-0.3.28 → sumd-0.3.30}/sumd/mcp_server.py +0 -0
  17. {sumd-0.3.28 → sumd-0.3.30}/sumd/models.py +0 -0
  18. {sumd-0.3.28 → sumd-0.3.30}/sumd/parser.py +0 -0
  19. {sumd-0.3.28 → sumd-0.3.30}/sumd/renderer.py +0 -0
  20. {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/__init__.py +0 -0
  21. {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/api_stubs.py +0 -0
  22. {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/base.py +0 -0
  23. {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/call_graph.py +0 -0
  24. {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/code_analysis.py +0 -0
  25. {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/configuration.py +0 -0
  26. {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/dependencies.py +0 -0
  27. {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/deployment.py +0 -0
  28. {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/environment.py +0 -0
  29. {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/extras.py +0 -0
  30. {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/metadata.py +0 -0
  31. {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/quality.py +0 -0
  32. {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/refactor_analysis.py +0 -0
  33. {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/source_snippets.py +0 -0
  34. {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/test_contracts.py +0 -0
  35. {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/workflows.py +0 -0
  36. {sumd-0.3.28 → sumd-0.3.30}/sumd/toon_parser.py +0 -0
  37. {sumd-0.3.28 → sumd-0.3.30}/sumd/validator.py +0 -0
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.30] - 2026-04-23
11
+
12
+ ### Docs
13
+ - Update README.md
14
+ - Update SUMD.md
15
+ - Update SUMR.md
16
+ - Update docs/README.md
17
+ - Update docs/USAGE.md
18
+ - Update examples/SUMD.md
19
+ - Update examples/SUMR.md
20
+ - Update project/README.md
21
+ - Update project/context.md
22
+
23
+ ### Other
24
+ - Update app.doql.less
25
+ - Update examples/app.doql.less
26
+ - Update examples/project/map.toon.yaml
27
+ - Update project.sh
28
+ - Update project/analysis.toon.yaml
29
+ - Update project/calls.mmd
30
+ - Update project/calls.png
31
+ - Update project/calls.toon.yaml
32
+ - Update project/calls.yaml
33
+ - Update project/compact_flow.mmd
34
+ - ... and 13 more files
35
+
36
+ ## [0.3.29] - 2026-04-23
37
+
38
+ ### Docs
39
+ - Update README.md
40
+
10
41
  ## [0.3.28] - 2026-04-23
11
42
 
12
43
  ### Docs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sumd
3
- Version: 0.3.28
3
+ Version: 0.3.30
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
@@ -15,25 +15,25 @@ Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Requires-Python: >=3.10
18
- Requires-Dist: click>=8.0
19
- Requires-Dist: costs>=0.1.20
20
- Requires-Dist: goal>=2.1.0
21
- Requires-Dist: pfix>=0.1.60
22
- Requires-Dist: pyyaml>=6.0
23
- Requires-Dist: toml>=0.10.0
18
+ Requires-Dist: click>=8.3.3
19
+ Requires-Dist: costs>=0.1.50
20
+ Requires-Dist: goal>=2.1.190
21
+ Requires-Dist: pfix>=0.1.72
22
+ Requires-Dist: pyyaml>=6.0.3
23
+ Requires-Dist: toml>=0.10.2
24
24
  Provides-Extra: dev
25
- Requires-Dist: build; extra == 'dev'
26
- Requires-Dist: costs>=0.1.20; extra == 'dev'
27
- Requires-Dist: goal>=2.1.0; extra == 'dev'
28
- Requires-Dist: mcp>=1.0.0; extra == 'dev'
29
- Requires-Dist: pfix>=0.1.60; extra == 'dev'
30
- Requires-Dist: pyqual>=0.1; extra == 'dev'
31
- Requires-Dist: pytest-cov>=4.0; extra == 'dev'
32
- Requires-Dist: pytest>=7.0; extra == 'dev'
33
- Requires-Dist: ruff>=0.1; extra == 'dev'
34
- Requires-Dist: twine; extra == 'dev'
25
+ Requires-Dist: build>=1.4.4; extra == 'dev'
26
+ Requires-Dist: costs>=0.1.50; extra == 'dev'
27
+ Requires-Dist: goal>=2.1.190; extra == 'dev'
28
+ Requires-Dist: mcp>=1.27.0; extra == 'dev'
29
+ Requires-Dist: pfix>=0.1.72; extra == 'dev'
30
+ Requires-Dist: pyqual>=0.1.143; extra == 'dev'
31
+ Requires-Dist: pytest-cov>=7.1.0; extra == 'dev'
32
+ Requires-Dist: pytest>=9.0.3; extra == 'dev'
33
+ Requires-Dist: ruff>=0.15.11; extra == 'dev'
34
+ Requires-Dist: twine>=6.2.0; extra == 'dev'
35
35
  Provides-Extra: mcp
36
- Requires-Dist: mcp>=1.0.0; extra == 'mcp'
36
+ Requires-Dist: mcp>=1.27.0; extra == 'mcp'
37
37
  Description-Content-Type: text/markdown
38
38
 
39
39
  # SUMD
@@ -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.28-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
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
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)
46
46
 
47
- - 🤖 **LLM usage:** $7.5000 (58 commits)
47
+ - 🤖 **LLM usage:** $7.5000 (60 commits)
48
48
  - 👤 **Human dev:** ~$2071 (20.7h @ $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.28-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.30-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.
@@ -255,6 +255,17 @@ SUMD is part of a three-layer system:
255
255
 
256
256
  SUMD can refresh `app.doql.less` metadata and optionally trigger DOQL's cache-aware rebuild.
257
257
 
258
+ ### Generating DOQL from source
259
+
260
+ For a rich, reverse-engineered `app.doql.less` (entities, interfaces, dependencies extracted from actual code), run `doql adopt` **before** `sumd`:
261
+
262
+ ```bash
263
+ doql adopt . --format less --force # generate/update app.doql.less from source
264
+ sumd . --fix # consume it into SUMD.md
265
+ ```
266
+
267
+ `sumd` alone generates only a minimal boilerplate. Use `doql` when you need a full declarative architecture extracted from the codebase.
268
+
258
269
  ### `app.doql.less` refresh behaviour
259
270
 
260
271
  When `sumd scan . --fix` runs (or the shorthand `sumd .`):
@@ -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.28-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
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
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)
8
8
 
9
- - 🤖 **LLM usage:** $7.5000 (58 commits)
9
+ - 🤖 **LLM usage:** $7.5000 (60 commits)
10
10
  - 👤 **Human dev:** ~$2071 (20.7h @ $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.28-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.30-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.
@@ -217,6 +217,17 @@ SUMD is part of a three-layer system:
217
217
 
218
218
  SUMD can refresh `app.doql.less` metadata and optionally trigger DOQL's cache-aware rebuild.
219
219
 
220
+ ### Generating DOQL from source
221
+
222
+ For a rich, reverse-engineered `app.doql.less` (entities, interfaces, dependencies extracted from actual code), run `doql adopt` **before** `sumd`:
223
+
224
+ ```bash
225
+ doql adopt . --format less --force # generate/update app.doql.less from source
226
+ sumd . --fix # consume it into SUMD.md
227
+ ```
228
+
229
+ `sumd` alone generates only a minimal boilerplate. Use `doql` when you need a full declarative architecture extracted from the codebase.
230
+
220
231
  ### `app.doql.less` refresh behaviour
221
232
 
222
233
  When `sumd scan . --fix` runs (or the shorthand `sumd .`):
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sumd"
7
- version = "0.3.28"
7
+ version = "0.3.30"
8
8
  description = "SUMD - Structured Unified Markdown Descriptor for AI-aware project documentation"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -14,12 +14,12 @@ authors = [
14
14
  ]
15
15
  keywords = ["sumd", "markdown", "documentation", "llm", "project-descriptor"]
16
16
  dependencies = [
17
- "click>=8.0",
18
- "pyyaml>=6.0",
19
- "toml>=0.10.0",
20
- "goal>=2.1.0",
21
- "costs>=0.1.20",
22
- "pfix>=0.1.60",
17
+ "click>=8.3.3",
18
+ "pyyaml>=6.0.3",
19
+ "toml>=0.10.2",
20
+ "goal>=2.1.190",
21
+ "costs>=0.1.50",
22
+ "pfix>=0.1.72",
23
23
  ]
24
24
  classifiers = [
25
25
  "Development Status :: 3 - Alpha",
@@ -32,19 +32,19 @@ classifiers = [
32
32
 
33
33
  [project.optional-dependencies]
34
34
  mcp = [
35
- "mcp>=1.0.0",
35
+ "mcp>=1.27.0",
36
36
  ]
37
37
  dev = [
38
- "pytest>=7.0",
39
- "pytest-cov>=4.0",
40
- "ruff>=0.1",
41
- "build",
42
- "twine",
43
- "pyqual>=0.1",
44
- "goal>=2.1.0",
45
- "costs>=0.1.20",
46
- "pfix>=0.1.60",
47
- "mcp>=1.0.0",
38
+ "pytest>=9.0.3",
39
+ "pytest-cov>=7.1.0",
40
+ "ruff>=0.15.11",
41
+ "build>=1.4.4",
42
+ "twine>=6.2.0",
43
+ "pyqual>=0.1.143",
44
+ "goal>=2.1.190",
45
+ "costs>=0.1.50",
46
+ "pfix>=0.1.72",
47
+ "mcp>=1.27.0",
48
48
  ]
49
49
 
50
50
  [tool.pytest.ini_options]
@@ -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.28"
7
+ __version__ = "0.3.30"
8
8
 
9
9
  from sumd.models import SUMDDocument
10
10
  from sumd.parser import (
@@ -46,10 +46,6 @@ from sumd.extractor import (
46
46
  generate_map_toon,
47
47
  required_tools_for_profile,
48
48
  )
49
- from sumd.renderer import (
50
- _collect_sources,
51
- _inject_toc,
52
- )
53
49
  from sumd.sections import PROFILES, SECTION_REGISTRY
54
50
  from sumd.sections.base import RenderContext
55
51
  from sumd.toon_parser import extract_testql_scenarios
@@ -138,6 +134,126 @@ def _refresh_analysis_files(proj_dir: Path, profile: str) -> None:
138
134
  pass # non-fatal
139
135
 
140
136
 
137
+ # ---------------------------------------------------------------------------
138
+ # Source collection helpers (moved from renderer.py)
139
+ # ---------------------------------------------------------------------------
140
+
141
+
142
+ def _collect_tool_sources(
143
+ pyproj: dict, reqs: list, tasks: list, makefile: list, scenarios: list
144
+ ) -> list[str]:
145
+ """Collect source labels for file-based tool inputs."""
146
+ sources: list[str] = []
147
+ if pyproj:
148
+ sources.append("pyproject.toml")
149
+ if reqs:
150
+ sources.extend(r["file"] for r in reqs)
151
+ if tasks:
152
+ sources.append("Taskfile.yml")
153
+ if makefile:
154
+ sources.append("Makefile")
155
+ if scenarios:
156
+ sources.append(f"testql({len(scenarios)})")
157
+ return sources
158
+
159
+
160
+ def _doql_sources(doql: dict) -> list[str]:
161
+ """Return doql source labels if any doql content is present."""
162
+ if doql.get("app") or doql.get("workflows") or doql.get("entities"):
163
+ return doql.get("sources", ["app.doql.less"])
164
+ return []
165
+
166
+
167
+ def _collect_pkg_sources(
168
+ pyproj: dict,
169
+ reqs: list,
170
+ tasks: list,
171
+ makefile: list,
172
+ scenarios: list,
173
+ openapi: dict,
174
+ doql: dict,
175
+ pyqual: dict,
176
+ goal: dict,
177
+ env_vars: list,
178
+ ) -> list[str]:
179
+ """Collect source labels for code/pipeline sources."""
180
+ sources = _collect_tool_sources(pyproj, reqs, tasks, makefile, scenarios)
181
+ if openapi.get("endpoints"):
182
+ sources.append(f"openapi({len(openapi['endpoints'])} ep)")
183
+ sources.extend(_doql_sources(doql))
184
+ if pyqual.get("stages"):
185
+ sources.append("pyqual.yaml")
186
+ if goal.get("name"):
187
+ sources.append("goal.yaml")
188
+ if env_vars:
189
+ sources.append(".env.example")
190
+ return sources
191
+
192
+
193
+ def _collect_infra_sources(
194
+ dockerfile: dict,
195
+ compose: dict,
196
+ pkg_json: dict,
197
+ modules: list,
198
+ project_analysis: list,
199
+ ) -> list[str]:
200
+ """Collect source labels for infra/module sources."""
201
+ sources: list[str] = []
202
+ if dockerfile:
203
+ sources.append("Dockerfile")
204
+ if compose.get("services"):
205
+ sources.append(compose.get("file", "docker-compose.yml"))
206
+ if pkg_json.get("name"):
207
+ sources.append("package.json")
208
+ if modules:
209
+ sources.append(f"src({len(modules)} mod)")
210
+ if project_analysis:
211
+ sources.append(f"project/({len(project_analysis)} analysis files)")
212
+ return sources
213
+
214
+
215
+ def _collect_sources(
216
+ pyproj: dict,
217
+ reqs: list,
218
+ tasks: list,
219
+ makefile: list,
220
+ scenarios: list,
221
+ openapi: dict,
222
+ doql: dict,
223
+ pyqual: dict,
224
+ goal: dict,
225
+ env_vars: list,
226
+ dockerfile: dict,
227
+ compose: dict,
228
+ pkg_json: dict,
229
+ modules: list,
230
+ project_analysis: list,
231
+ ) -> list[str]:
232
+ """Build the list of source labels that contributed data to this SUMD."""
233
+ return _collect_pkg_sources(
234
+ pyproj, reqs, tasks, makefile, scenarios, openapi, doql, pyqual, goal, env_vars
235
+ ) + _collect_infra_sources(dockerfile, compose, pkg_json, modules, project_analysis)
236
+
237
+
238
+ def _inject_toc(content: str) -> str:
239
+ """Inject a ## Contents TOC block before ## Metadata."""
240
+ import re
241
+
242
+ h2_sections = re.findall(r"^## (.+)$", content, re.MULTILINE)
243
+ if not h2_sections:
244
+ return content
245
+ toc_lines = ["## Contents", ""]
246
+ for sec in h2_sections:
247
+ anchor = re.sub(r"[^\w\s-]", "", sec.lower()).strip()
248
+ anchor = re.sub(r"\s+", "-", anchor)
249
+ toc_lines.append(f"- [{sec}](#{anchor})")
250
+ toc_lines.append("")
251
+ toc_block = "\n".join(toc_lines)
252
+ return re.sub(
253
+ r"(\n## Metadata\n)", f"\n{toc_block}\n## Metadata\n", content, count=1
254
+ )
255
+
256
+
141
257
  class RenderPipeline:
142
258
  """Collect project data → build sections → render → inject TOC.
143
259
 
@@ -0,0 +1,156 @@
1
+ """sumd.sections.architecture — ArchitectureSection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from sumd.sections.base import RenderContext, Section
8
+
9
+
10
+ # ---------------------------------------------------------------------------
11
+ # Private renderers (moved from renderer.py)
12
+ # ---------------------------------------------------------------------------
13
+
14
+
15
+ def _render_architecture_doql_section(
16
+ doql: dict, proj_dir: Path, raw_sources: bool, L: list[str]
17
+ ) -> None:
18
+ a = L.append
19
+ doql_sources = ", ".join(f"`{s}`" for s in doql.get("sources", ["app.doql.less"]))
20
+ a(f"### DOQL Application Declaration ({doql_sources})")
21
+ a("")
22
+ if raw_sources:
23
+ for fname in doql.get("sources", ["app.doql.less"]):
24
+ fpath = proj_dir / fname
25
+ if fpath.exists():
26
+ lang = "less" if fname.endswith(".less") else "css"
27
+ a(f"```{lang} markpact:doql path={fname}")
28
+ a(fpath.read_text(encoding="utf-8").rstrip())
29
+ a("```")
30
+ a("")
31
+ else:
32
+ _render_architecture_doql_parsed(doql, L)
33
+
34
+
35
+ def _render_architecture_modules(modules: list[str], name: str, L: list[str]) -> None:
36
+ a = L.append
37
+ a("### Source Modules")
38
+ a("")
39
+ for mod in modules:
40
+ a(f"- `{name}.{mod}`")
41
+ a("")
42
+
43
+
44
+ def _render_doql_app(doql: dict, L: list[str]) -> None:
45
+ if not doql.get("app"):
46
+ return
47
+ a = L.append
48
+ a("```less")
49
+ a("app {")
50
+ for k, v in doql["app"].items():
51
+ a(f" {k}: {v};")
52
+ a("}")
53
+ a("```")
54
+ a("")
55
+
56
+
57
+ def _render_doql_entities(doql: dict, L: list[str]) -> None:
58
+ if not doql.get("entities"):
59
+ return
60
+ a = L.append
61
+ a("### DOQL Data Model (`entity`)")
62
+ a("")
63
+ for ent in doql["entities"]:
64
+ attrs_str = ""
65
+ if ent.get("attrs"):
66
+ attrs_str = " — " + ", ".join(
67
+ f"`{k}: {v}`" for k, v in ent["attrs"].items()
68
+ )
69
+ page_str = f" page=`{ent['page']}`" if ent.get("page") else ""
70
+ a(f"- `entity[{ent['name']}]`{page_str}{attrs_str}")
71
+ a("")
72
+
73
+
74
+ def _render_doql_interfaces(doql: dict, L: list[str]) -> None:
75
+ if not doql.get("interfaces"):
76
+ return
77
+ a = L.append
78
+ a("### DOQL Interfaces")
79
+ a("")
80
+ for iface in list(doql["interfaces"]):
81
+ sel = iface.get("selector", "")
82
+ attrs = ", ".join(
83
+ f"{k}: {v}" for k, v in iface.items() if k not in ("selector", "page")
84
+ )
85
+ page_str = f" page=`{iface['page']}`" if iface.get("page") else ""
86
+ a(f"- `interface[{sel}]`{page_str} — {attrs}")
87
+ a("")
88
+
89
+
90
+ def _render_doql_integrations(doql: dict, L: list[str]) -> None:
91
+ if not doql.get("integrations"):
92
+ return
93
+ a = L.append
94
+ a("### DOQL Integrations")
95
+ a("")
96
+ for intg in list(doql["integrations"]):
97
+ sel = intg.get("selector", "")
98
+ attrs = ", ".join(f"{k}: {v}" for k, v in intg.items() if k != "selector")
99
+ a(f"- `integration[{sel}]` — {attrs}")
100
+ a("")
101
+
102
+
103
+ def _render_architecture_doql_parsed(doql: dict, L: list[str]) -> None:
104
+ """Render parsed DOQL blocks into L (mutates in place)."""
105
+ _render_doql_app(doql, L)
106
+ _render_doql_entities(doql, L)
107
+ _render_doql_interfaces(doql, L)
108
+ _render_doql_integrations(doql, L)
109
+
110
+
111
+ def _render_architecture(
112
+ doql: dict, modules: list[str], name: str, proj_dir: Path, raw_sources: bool
113
+ ) -> list[str]:
114
+ L: list[str] = []
115
+ a = L.append
116
+ a("## Architecture")
117
+ a("")
118
+ a("```")
119
+ a(
120
+ "SUMD (description) → DOQL/source (code) → taskfile (automation) → testql (verification)"
121
+ )
122
+ a("```")
123
+ a("")
124
+ if (
125
+ doql.get("app")
126
+ or doql.get("entities")
127
+ or doql.get("interfaces")
128
+ or doql.get("workflows")
129
+ ):
130
+ _render_architecture_doql_section(doql, proj_dir, raw_sources, L)
131
+ if modules:
132
+ _render_architecture_modules(modules, name, L)
133
+ return L
134
+
135
+
136
+ # ---------------------------------------------------------------------------
137
+ # Section class
138
+ # ---------------------------------------------------------------------------
139
+
140
+
141
+ class ArchitectureSection:
142
+ name = "architecture"
143
+ level = 2
144
+ profiles = frozenset({"minimal", "light", "rich"})
145
+
146
+ def should_render(self, ctx: RenderContext) -> bool:
147
+ return True
148
+
149
+ def render(self, ctx: RenderContext) -> list[str]:
150
+ return _render_architecture(
151
+ ctx.doql, ctx.modules, ctx.name, ctx.proj_dir, ctx.raw_sources
152
+ )
153
+
154
+
155
+ assert isinstance(ArchitectureSection(), Section)
156
+
@@ -0,0 +1,157 @@
1
+ """sumd.sections.interfaces — InterfacesSection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from sumd.sections.base import RenderContext, Section
8
+
9
+
10
+ # ---------------------------------------------------------------------------
11
+ # Private renderers (moved from renderer.py)
12
+ # ---------------------------------------------------------------------------
13
+
14
+
15
+ def _render_interfaces_openapi(
16
+ openapi: dict, proj_dir: Path, raw_sources: bool, L: list[str]
17
+ ) -> None:
18
+ """Render REST API section into L (mutates in place)."""
19
+ a = L.append
20
+ a("### REST API (from `openapi.yaml`)")
21
+ a("")
22
+ if raw_sources:
23
+ op_path = proj_dir / "openapi.yaml"
24
+ if op_path.exists():
25
+ a("```yaml markpact:openapi path=openapi.yaml")
26
+ a(op_path.read_text(encoding="utf-8").rstrip())
27
+ a("```")
28
+ a("")
29
+ else:
30
+ a("| Method | Path | OperationId | Summary |")
31
+ a("|--------|------|-------------|---------|")
32
+ for ep in openapi["endpoints"]:
33
+ summary = ep.get("summary", "").replace("|", "\\|")
34
+ a(
35
+ f"| `{ep['method']}` | `{ep['path']}` | `{ep['operationId']}` | {summary} |"
36
+ )
37
+ a("")
38
+ if openapi.get("schemas"):
39
+ a("**Schemas**: " + ", ".join(f"`{s}`" for s in openapi["schemas"]))
40
+ a("")
41
+
42
+
43
+ def _render_testql_raw(scenarios: list, proj_dir: Path, L: list[str]) -> None:
44
+ a = L.append
45
+ seen_scenario_files: set[str] = set()
46
+ for sc in scenarios:
47
+ rel = sc.get("rel_path", sc["file"])
48
+ fpath = proj_dir / rel
49
+ if not fpath.exists() or rel in seen_scenario_files:
50
+ continue
51
+ seen_scenario_files.add(rel)
52
+ a(f"#### `{rel}`")
53
+ a("")
54
+ a(f"```toon markpact:testql path={rel}")
55
+ a(fpath.read_text(encoding="utf-8").rstrip())
56
+ a("```")
57
+ a("")
58
+
59
+
60
+ def _render_testql_endpoint(ep: dict, L: list[str]) -> None:
61
+ """Render one endpoint line for a testql scenario."""
62
+ op = f" — `{ep['operationId']}`" if ep.get("operationId") else ""
63
+ sm = f": {ep['summary']}" if ep.get("summary") else ""
64
+ L.append(f" - `{ep['method']} {ep['path']}` → `{ep['status']}`{op}{sm}")
65
+
66
+
67
+ def _render_testql_extras(sc: dict, L: list[str]) -> None:
68
+ """Render performance / navigate / gui blocks for a testql scenario."""
69
+ a = L.append
70
+ if sc.get("performance"):
71
+ a("- **performance**:")
72
+ for p in sc["performance"]:
73
+ a(f" - `{p['metric']} < {p['threshold']}`")
74
+ if sc.get("navigate"):
75
+ a("- **navigate**: " + ", ".join(f"`{u}`" for u in sc["navigate"]))
76
+ if sc.get("gui"):
77
+ a("- **gui actions**:")
78
+ for g in sc["gui"]:
79
+ a(f" - `{g['action']} {g['selector']}`")
80
+
81
+
82
+ def _render_testql_one_structured(sc: dict, L: list[str]) -> None:
83
+ a = L.append
84
+ a(f"#### `{sc['file']}`")
85
+ a("")
86
+ a(f"- **name**: {sc['name']}")
87
+ a(f"- **type**: `{sc['type']}`")
88
+ if sc["detectors"]:
89
+ a(f"- **detectors**: {sc['detectors']}")
90
+ for k, v in sc["config"].items():
91
+ a(f"- **{k}**: `{v}`")
92
+ if sc["endpoints"]:
93
+ a("- **endpoints**:")
94
+ for ep in sc["endpoints"]:
95
+ _render_testql_endpoint(ep, L)
96
+ if sc["asserts"]:
97
+ a("- **asserts**:")
98
+ for ass in sc["asserts"]:
99
+ a(f" - `{ass['field']} {ass['op']} {ass['expected']}`")
100
+ _render_testql_extras(sc, L)
101
+ a("")
102
+
103
+
104
+ def _render_interfaces_testql(
105
+ scenarios: list, proj_dir: Path, raw_sources: bool, L: list[str]
106
+ ) -> None:
107
+ """Render testql scenarios section into L (mutates in place)."""
108
+ a = L.append
109
+ a("### testql Scenarios")
110
+ a("")
111
+ if raw_sources:
112
+ _render_testql_raw(scenarios, proj_dir, L)
113
+ else:
114
+ for sc in scenarios:
115
+ _render_testql_one_structured(sc, L)
116
+
117
+
118
+ def _render_interfaces(
119
+ scripts: list, openapi: dict, scenarios: list, proj_dir: Path, raw_sources: bool
120
+ ) -> list[str]:
121
+ L: list[str] = []
122
+ a = L.append
123
+ a("## Interfaces")
124
+ a("")
125
+ if scripts:
126
+ a("### CLI Entry Points")
127
+ a("")
128
+ for s in scripts:
129
+ a(f"- `{s}`")
130
+ a("")
131
+ if openapi.get("endpoints"):
132
+ _render_interfaces_openapi(openapi, proj_dir, raw_sources, L)
133
+ if scenarios:
134
+ _render_interfaces_testql(scenarios, proj_dir, raw_sources, L)
135
+ return L
136
+
137
+
138
+ # ---------------------------------------------------------------------------
139
+ # Section class
140
+ # ---------------------------------------------------------------------------
141
+
142
+
143
+ class InterfacesSection:
144
+ name = "interfaces"
145
+ level = 2
146
+ profiles = frozenset({"light", "rich"})
147
+
148
+ def should_render(self, ctx: RenderContext) -> bool:
149
+ return bool(ctx.scripts or ctx.openapi.get("endpoints") or ctx.scenarios)
150
+
151
+ def render(self, ctx: RenderContext) -> list[str]:
152
+ return _render_interfaces(
153
+ ctx.scripts, ctx.openapi, ctx.scenarios, ctx.proj_dir, ctx.raw_sources
154
+ )
155
+
156
+
157
+ assert isinstance(InterfacesSection(), Section)
@@ -1,23 +0,0 @@
1
- """sumd.sections.architecture — ArchitectureSection."""
2
-
3
- from __future__ import annotations
4
-
5
- from sumd.renderer import _render_architecture
6
- from sumd.sections.base import RenderContext, Section
7
-
8
-
9
- class ArchitectureSection:
10
- name = "architecture"
11
- level = 2
12
- profiles = frozenset({"minimal", "light", "rich"})
13
-
14
- def should_render(self, ctx: RenderContext) -> bool:
15
- return True
16
-
17
- def render(self, ctx: RenderContext) -> list[str]:
18
- return _render_architecture(
19
- ctx.doql, ctx.modules, ctx.name, ctx.proj_dir, ctx.raw_sources
20
- )
21
-
22
-
23
- assert isinstance(ArchitectureSection(), Section)
@@ -1,23 +0,0 @@
1
- """sumd.sections.interfaces — InterfacesSection."""
2
-
3
- from __future__ import annotations
4
-
5
- from sumd.renderer import _render_interfaces
6
- from sumd.sections.base import RenderContext, Section
7
-
8
-
9
- class InterfacesSection:
10
- name = "interfaces"
11
- level = 2
12
- profiles = frozenset({"light", "rich"})
13
-
14
- def should_render(self, ctx: RenderContext) -> bool:
15
- return bool(ctx.scripts or ctx.openapi.get("endpoints") or ctx.scenarios)
16
-
17
- def render(self, ctx: RenderContext) -> list[str]:
18
- return _render_interfaces(
19
- ctx.scripts, ctx.openapi, ctx.scenarios, ctx.proj_dir, ctx.raw_sources
20
- )
21
-
22
-
23
- assert isinstance(InterfacesSection(), Section)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes