sumd 0.3.35__tar.gz → 0.3.37__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.35 → sumd-0.3.37}/CHANGELOG.md +18 -0
- {sumd-0.3.35 → sumd-0.3.37}/PKG-INFO +6 -6
- {sumd-0.3.35 → sumd-0.3.37}/README.md +5 -5
- {sumd-0.3.35 → sumd-0.3.37}/pyproject.toml +1 -1
- {sumd-0.3.35 → sumd-0.3.37}/sumd/__init__.py +1 -1
- {sumd-0.3.35 → sumd-0.3.37}/sumd/extractor.py +52 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/pipeline.py +9 -1
- {sumd-0.3.35 → sumd-0.3.37}/sumd/sections/__init__.py +3 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/sections/base.py +1 -0
- sumd-0.3.37/sumd/sections/swop.py +68 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/validator.py +2 -0
- {sumd-0.3.35 → sumd-0.3.37}/.gitignore +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/LICENSE +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/cli.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/generator.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/mcp_server.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/models.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/parser.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/renderer.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/sections/api_stubs.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/sections/architecture.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/sections/call_graph.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/sections/code_analysis.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/sections/configuration.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/sections/dependencies.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/sections/deployment.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/sections/environment.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/sections/extras.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/sections/interfaces.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/sections/metadata.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/sections/quality.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/sections/refactor_analysis.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/sections/source_snippets.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/sections/test_contracts.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/sections/workflows.py +0 -0
- {sumd-0.3.35 → sumd-0.3.37}/sumd/toon_parser.py +0 -0
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.37] - 2026-04-23
|
|
11
|
+
|
|
12
|
+
### Docs
|
|
13
|
+
- Update README.md
|
|
14
|
+
|
|
15
|
+
### Other
|
|
16
|
+
- Update sumd/extractor.py
|
|
17
|
+
- Update sumd/pipeline.py
|
|
18
|
+
- Update sumd/sections/__init__.py
|
|
19
|
+
- Update sumd/sections/base.py
|
|
20
|
+
- Update sumd/sections/swop.py
|
|
21
|
+
- Update sumd/validator.py
|
|
22
|
+
|
|
23
|
+
## [0.3.36] - 2026-04-23
|
|
24
|
+
|
|
25
|
+
### Docs
|
|
26
|
+
- Update README.md
|
|
27
|
+
|
|
10
28
|
## [0.3.35] - 2026-04-23
|
|
11
29
|
|
|
12
30
|
### Docs
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sumd
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.37
|
|
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 (67 commits)
|
|
48
|
+
- 👤 **Human dev:** ~$2387 (23.9h @ $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 (67 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$2387 (23.9h @ $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.37"
|
|
8
8
|
|
|
9
9
|
from sumd.models import SUMDDocument
|
|
10
10
|
from sumd.parser import (
|
|
@@ -877,6 +877,58 @@ def extract_source_snippets(proj_dir: Path, pkg_name: str) -> list[dict]:
|
|
|
877
877
|
return results
|
|
878
878
|
|
|
879
879
|
|
|
880
|
+
def extract_swop(proj_dir: Path) -> dict[str, Any]:
|
|
881
|
+
"""Extract SWOP manifest files from .swop/manifests/<context>/ directory.
|
|
882
|
+
|
|
883
|
+
Returns dict with structure:
|
|
884
|
+
{
|
|
885
|
+
"contexts": {
|
|
886
|
+
"<context_name>": {
|
|
887
|
+
"commands": {file, content},
|
|
888
|
+
"queries": {file, content},
|
|
889
|
+
"events": {file, content}
|
|
890
|
+
}
|
|
891
|
+
},
|
|
892
|
+
"sources": [list of file paths]
|
|
893
|
+
}
|
|
894
|
+
"""
|
|
895
|
+
swop_dir = proj_dir / ".swop" / "manifests"
|
|
896
|
+
if not swop_dir.is_dir():
|
|
897
|
+
return {}
|
|
898
|
+
|
|
899
|
+
contexts: dict[str, dict[str, Any]] = {}
|
|
900
|
+
sources: list[str] = []
|
|
901
|
+
|
|
902
|
+
for context_dir in sorted(swop_dir.iterdir()):
|
|
903
|
+
if not context_dir.is_dir():
|
|
904
|
+
continue
|
|
905
|
+
|
|
906
|
+
context_name = context_dir.name
|
|
907
|
+
context_data: dict[str, dict[str, str]] = {}
|
|
908
|
+
|
|
909
|
+
for manifest_type in ("commands.yml", "queries.yml", "events.yml"):
|
|
910
|
+
manifest_path = context_dir / manifest_type
|
|
911
|
+
if manifest_path.exists():
|
|
912
|
+
rel_path = f".swop/manifests/{context_name}/{manifest_type}"
|
|
913
|
+
try:
|
|
914
|
+
content = manifest_path.read_text(encoding="utf-8").rstrip()
|
|
915
|
+
context_data[manifest_type.replace(".yml", "")] = {
|
|
916
|
+
"file": rel_path,
|
|
917
|
+
"content": content,
|
|
918
|
+
}
|
|
919
|
+
sources.append(rel_path)
|
|
920
|
+
except Exception:
|
|
921
|
+
pass
|
|
922
|
+
|
|
923
|
+
if context_data:
|
|
924
|
+
contexts[context_name] = context_data
|
|
925
|
+
|
|
926
|
+
if not contexts:
|
|
927
|
+
return {}
|
|
928
|
+
|
|
929
|
+
return {"contexts": contexts, "sources": sources}
|
|
930
|
+
|
|
931
|
+
|
|
880
932
|
def extract_project_analysis(
|
|
881
933
|
proj_dir: Path, refactor: bool = False
|
|
882
934
|
) -> list[dict[str, str]]:
|
|
@@ -42,6 +42,7 @@ from sumd.extractor import (
|
|
|
42
42
|
extract_readme_title,
|
|
43
43
|
extract_requirements,
|
|
44
44
|
extract_source_snippets,
|
|
45
|
+
extract_swop,
|
|
45
46
|
extract_taskfile,
|
|
46
47
|
generate_map_toon,
|
|
47
48
|
required_tools_for_profile,
|
|
@@ -228,11 +229,15 @@ def _collect_sources(
|
|
|
228
229
|
pkg_json: dict,
|
|
229
230
|
modules: list,
|
|
230
231
|
project_analysis: list,
|
|
232
|
+
swop: dict,
|
|
231
233
|
) -> list[str]:
|
|
232
234
|
"""Build the list of source labels that contributed data to this SUMD."""
|
|
233
|
-
|
|
235
|
+
sources = _collect_pkg_sources(
|
|
234
236
|
pyproj, reqs, tasks, makefile, scenarios, openapi, doql, pyqual, goal, env_vars
|
|
235
237
|
) + _collect_infra_sources(dockerfile, compose, pkg_json, modules, project_analysis)
|
|
238
|
+
if swop.get("sources"):
|
|
239
|
+
sources.extend(swop["sources"])
|
|
240
|
+
return sources
|
|
236
241
|
|
|
237
242
|
|
|
238
243
|
def _inject_toc(content: str) -> str:
|
|
@@ -289,6 +294,7 @@ class RenderPipeline:
|
|
|
289
294
|
dockerfile = extract_dockerfile(proj_dir)
|
|
290
295
|
compose = extract_docker_compose(proj_dir)
|
|
291
296
|
pkg_json = extract_package_json(proj_dir)
|
|
297
|
+
swop = extract_swop(proj_dir)
|
|
292
298
|
|
|
293
299
|
# Auto-regenerate map.toon.yaml — pure-Python, always fast.
|
|
294
300
|
_refresh_map_toon(proj_dir)
|
|
@@ -320,6 +326,7 @@ class RenderPipeline:
|
|
|
320
326
|
pkg_json,
|
|
321
327
|
modules,
|
|
322
328
|
project_analysis,
|
|
329
|
+
swop,
|
|
323
330
|
)
|
|
324
331
|
|
|
325
332
|
return RenderContext(
|
|
@@ -348,6 +355,7 @@ class RenderPipeline:
|
|
|
348
355
|
pkg_json=pkg_json,
|
|
349
356
|
project_analysis=project_analysis,
|
|
350
357
|
source_snippets=source_snippets,
|
|
358
|
+
swop=swop,
|
|
351
359
|
raw_sources=self.raw_sources,
|
|
352
360
|
sources_used=sources_used,
|
|
353
361
|
title=title or name,
|
|
@@ -28,6 +28,7 @@ from sumd.sections.metadata import MetadataSection
|
|
|
28
28
|
from sumd.sections.quality import QualitySection
|
|
29
29
|
from sumd.sections.refactor_analysis import RefactorAnalysisSection
|
|
30
30
|
from sumd.sections.source_snippets import SourceSnippetsSection
|
|
31
|
+
from sumd.sections.swop import SwopSection
|
|
31
32
|
from sumd.sections.test_contracts import TestContractsSection
|
|
32
33
|
from sumd.sections.workflows import WorkflowsSection
|
|
33
34
|
|
|
@@ -36,6 +37,7 @@ from sumd.sections.workflows import WorkflowsSection
|
|
|
36
37
|
SECTION_REGISTRY: list[type] = [
|
|
37
38
|
MetadataSection,
|
|
38
39
|
ArchitectureSection,
|
|
40
|
+
SwopSection,
|
|
39
41
|
InterfacesSection,
|
|
40
42
|
WorkflowsSection,
|
|
41
43
|
QualitySection,
|
|
@@ -71,6 +73,7 @@ PROFILES: dict[str, set[str]] = {
|
|
|
71
73
|
"rich": {
|
|
72
74
|
"metadata",
|
|
73
75
|
"architecture",
|
|
76
|
+
"swop",
|
|
74
77
|
"interfaces",
|
|
75
78
|
"workflows",
|
|
76
79
|
"quality",
|
|
@@ -53,6 +53,7 @@ class RenderContext:
|
|
|
53
53
|
pkg_json: dict = field(default_factory=dict)
|
|
54
54
|
project_analysis: list = field(default_factory=list)
|
|
55
55
|
source_snippets: list = field(default_factory=list)
|
|
56
|
+
swop: dict = field(default_factory=dict)
|
|
56
57
|
|
|
57
58
|
# Rendering options
|
|
58
59
|
raw_sources: bool = True
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""sumd.sections.swop — SwopSection for SWOP manifest files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from sumd.sections.base import RenderContext, Section
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _render_swop_section(swop: dict, raw_sources: bool) -> list[str]:
|
|
9
|
+
"""Render SWOP manifest section."""
|
|
10
|
+
L: list[str] = []
|
|
11
|
+
a = L.append
|
|
12
|
+
|
|
13
|
+
if not swop.get("contexts"):
|
|
14
|
+
return L
|
|
15
|
+
|
|
16
|
+
contexts = swop["contexts"]
|
|
17
|
+
sources = swop.get("sources", [])
|
|
18
|
+
|
|
19
|
+
a("## SWOP")
|
|
20
|
+
a("")
|
|
21
|
+
a("SWOP - Bi-directional runtime reconciler and drift-aware state graph for full-stack systems.")
|
|
22
|
+
a("")
|
|
23
|
+
|
|
24
|
+
if raw_sources:
|
|
25
|
+
# Render raw YAML files
|
|
26
|
+
for context_name, context_data in sorted(contexts.items()):
|
|
27
|
+
a(f"### Context: `{context_name}`")
|
|
28
|
+
a("")
|
|
29
|
+
|
|
30
|
+
for manifest_type in ("commands", "queries", "events"):
|
|
31
|
+
if manifest_type in context_data:
|
|
32
|
+
manifest = context_data[manifest_type]
|
|
33
|
+
file_path = manifest["file"]
|
|
34
|
+
content = manifest["content"]
|
|
35
|
+
a(f"#### {manifest_type.capitalize()} (`{file_path}`)")
|
|
36
|
+
a("")
|
|
37
|
+
a(f"```yaml markpact:swop path={file_path}")
|
|
38
|
+
a(content)
|
|
39
|
+
a("```")
|
|
40
|
+
a("")
|
|
41
|
+
else:
|
|
42
|
+
# Render parsed/structured view
|
|
43
|
+
for context_name, context_data in sorted(contexts.items()):
|
|
44
|
+
a(f"### Context: `{context_name}`")
|
|
45
|
+
a("")
|
|
46
|
+
|
|
47
|
+
for manifest_type in ("commands", "queries", "events"):
|
|
48
|
+
if manifest_type in context_data:
|
|
49
|
+
file_path = context_data[manifest_type]["file"]
|
|
50
|
+
a(f"- **{manifest_type.capitalize()}**: `{file_path}`")
|
|
51
|
+
a("")
|
|
52
|
+
|
|
53
|
+
return L
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SwopSection:
|
|
57
|
+
name = "swop"
|
|
58
|
+
level = 2
|
|
59
|
+
profiles = frozenset({"rich"})
|
|
60
|
+
|
|
61
|
+
def should_render(self, ctx: RenderContext) -> bool:
|
|
62
|
+
return bool(ctx.swop.get("contexts"))
|
|
63
|
+
|
|
64
|
+
def render(self, ctx: RenderContext) -> list[str]:
|
|
65
|
+
return _render_swop_section(ctx.swop, ctx.raw_sources)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
assert isinstance(SwopSection(), Section)
|
|
@@ -122,6 +122,7 @@ _VALID_MARKPACT_KINDS = {
|
|
|
122
122
|
"taskfile",
|
|
123
123
|
"pyqual",
|
|
124
124
|
"analysis",
|
|
125
|
+
"swop",
|
|
125
126
|
}
|
|
126
127
|
_MARKPACT_REQUIRED_ATTRS = {
|
|
127
128
|
"file": "path",
|
|
@@ -131,6 +132,7 @@ _MARKPACT_REQUIRED_ATTRS = {
|
|
|
131
132
|
"taskfile": "path",
|
|
132
133
|
"pyqual": "path",
|
|
133
134
|
"analysis": "path",
|
|
135
|
+
"swop": "path",
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
|
|
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
|