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.
- {sumd-0.3.28 → sumd-0.3.30}/CHANGELOG.md +31 -0
- {sumd-0.3.28 → sumd-0.3.30}/PKG-INFO +32 -21
- {sumd-0.3.28 → sumd-0.3.30}/README.md +14 -3
- {sumd-0.3.28 → sumd-0.3.30}/pyproject.toml +18 -18
- {sumd-0.3.28 → sumd-0.3.30}/sumd/__init__.py +1 -1
- {sumd-0.3.28 → sumd-0.3.30}/sumd/pipeline.py +120 -4
- sumd-0.3.30/sumd/sections/architecture.py +156 -0
- sumd-0.3.30/sumd/sections/interfaces.py +157 -0
- sumd-0.3.28/sumd/sections/architecture.py +0 -23
- sumd-0.3.28/sumd/sections/interfaces.py +0 -23
- {sumd-0.3.28 → sumd-0.3.30}/.gitignore +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/LICENSE +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/cli.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/extractor.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/generator.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/mcp_server.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/models.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/parser.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/renderer.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/__init__.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/api_stubs.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/base.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/call_graph.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/code_analysis.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/configuration.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/dependencies.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/deployment.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/environment.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/extras.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/metadata.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/quality.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/refactor_analysis.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/source_snippets.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/test_contracts.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/sections/workflows.py +0 -0
- {sumd-0.3.28 → sumd-0.3.30}/sumd/toon_parser.py +0 -0
- {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.
|
|
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.
|
|
19
|
-
Requires-Dist: costs>=0.1.
|
|
20
|
-
Requires-Dist: goal>=2.1.
|
|
21
|
-
Requires-Dist: pfix>=0.1.
|
|
22
|
-
Requires-Dist: pyyaml>=6.0
|
|
23
|
-
Requires-Dist: toml>=0.10.
|
|
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.
|
|
27
|
-
Requires-Dist: goal>=2.1.
|
|
28
|
-
Requires-Dist: mcp>=1.
|
|
29
|
-
Requires-Dist: pfix>=0.1.
|
|
30
|
-
Requires-Dist: pyqual>=0.1; extra == 'dev'
|
|
31
|
-
Requires-Dist: pytest-cov>=
|
|
32
|
-
Requires-Dist: pytest>=
|
|
33
|
-
Requires-Dist: ruff>=0.
|
|
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.
|
|
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
|
-
    
|
|
45
45
|
  
|
|
46
46
|
|
|
47
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
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
|
-
  
|
|
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
|
-
    
|
|
7
7
|
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $7.5000 (
|
|
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
|
-
  
|
|
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.
|
|
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.
|
|
18
|
-
"pyyaml>=6.0",
|
|
19
|
-
"toml>=0.10.
|
|
20
|
-
"goal>=2.1.
|
|
21
|
-
"costs>=0.1.
|
|
22
|
-
"pfix>=0.1.
|
|
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.
|
|
35
|
+
"mcp>=1.27.0",
|
|
36
36
|
]
|
|
37
37
|
dev = [
|
|
38
|
-
"pytest>=
|
|
39
|
-
"pytest-cov>=
|
|
40
|
-
"ruff>=0.
|
|
41
|
-
"build",
|
|
42
|
-
"twine",
|
|
43
|
-
"pyqual>=0.1",
|
|
44
|
-
"goal>=2.1.
|
|
45
|
-
"costs>=0.1.
|
|
46
|
-
"pfix>=0.1.
|
|
47
|
-
"mcp>=1.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|