sumd 0.3.24__tar.gz → 0.3.26__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 (34) hide show
  1. {sumd-0.3.24 → sumd-0.3.26}/CHANGELOG.md +26 -0
  2. {sumd-0.3.24 → sumd-0.3.26}/PKG-INFO +7 -7
  3. {sumd-0.3.24 → sumd-0.3.26}/README.md +6 -6
  4. {sumd-0.3.24 → sumd-0.3.26}/pyproject.toml +1 -1
  5. {sumd-0.3.24 → sumd-0.3.26}/sumd/__init__.py +4 -2
  6. {sumd-0.3.24 → sumd-0.3.26}/sumd/cli.py +78 -54
  7. sumd-0.3.26/sumd/parser.py +233 -0
  8. sumd-0.3.24/sumd/parser.py → sumd-0.3.26/sumd/validator.py +2 -221
  9. {sumd-0.3.24 → sumd-0.3.26}/.gitignore +0 -0
  10. {sumd-0.3.24 → sumd-0.3.26}/LICENSE +0 -0
  11. {sumd-0.3.24 → sumd-0.3.26}/sumd/extractor.py +0 -0
  12. {sumd-0.3.24 → sumd-0.3.26}/sumd/generator.py +0 -0
  13. {sumd-0.3.24 → sumd-0.3.26}/sumd/mcp_server.py +0 -0
  14. {sumd-0.3.24 → sumd-0.3.26}/sumd/pipeline.py +0 -0
  15. {sumd-0.3.24 → sumd-0.3.26}/sumd/renderer.py +0 -0
  16. {sumd-0.3.24 → sumd-0.3.26}/sumd/sections/__init__.py +0 -0
  17. {sumd-0.3.24 → sumd-0.3.26}/sumd/sections/api_stubs.py +0 -0
  18. {sumd-0.3.24 → sumd-0.3.26}/sumd/sections/architecture.py +0 -0
  19. {sumd-0.3.24 → sumd-0.3.26}/sumd/sections/base.py +0 -0
  20. {sumd-0.3.24 → sumd-0.3.26}/sumd/sections/call_graph.py +0 -0
  21. {sumd-0.3.24 → sumd-0.3.26}/sumd/sections/code_analysis.py +0 -0
  22. {sumd-0.3.24 → sumd-0.3.26}/sumd/sections/configuration.py +0 -0
  23. {sumd-0.3.24 → sumd-0.3.26}/sumd/sections/dependencies.py +0 -0
  24. {sumd-0.3.24 → sumd-0.3.26}/sumd/sections/deployment.py +0 -0
  25. {sumd-0.3.24 → sumd-0.3.26}/sumd/sections/environment.py +0 -0
  26. {sumd-0.3.24 → sumd-0.3.26}/sumd/sections/extras.py +0 -0
  27. {sumd-0.3.24 → sumd-0.3.26}/sumd/sections/interfaces.py +0 -0
  28. {sumd-0.3.24 → sumd-0.3.26}/sumd/sections/metadata.py +0 -0
  29. {sumd-0.3.24 → sumd-0.3.26}/sumd/sections/quality.py +0 -0
  30. {sumd-0.3.24 → sumd-0.3.26}/sumd/sections/refactor_analysis.py +0 -0
  31. {sumd-0.3.24 → sumd-0.3.26}/sumd/sections/source_snippets.py +0 -0
  32. {sumd-0.3.24 → sumd-0.3.26}/sumd/sections/test_contracts.py +0 -0
  33. {sumd-0.3.24 → sumd-0.3.26}/sumd/sections/workflows.py +0 -0
  34. {sumd-0.3.24 → sumd-0.3.26}/sumd/toon_parser.py +0 -0
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.26] - 2026-04-23
11
+
12
+ ### Docs
13
+ - Update README.md
14
+ - Update docs/README.md
15
+ - Update project/README.md
16
+ - Update project/context.md
17
+
18
+ ### Other
19
+ - Update project/analysis.toon.yaml
20
+ - Update project/calls.mmd
21
+ - Update project/calls.png
22
+ - Update project/calls.toon.yaml
23
+ - Update project/calls.yaml
24
+ - Update project/compact_flow.mmd
25
+ - Update project/compact_flow.png
26
+ - Update project/duplication.toon.yaml
27
+ - Update project/evolution.toon.yaml
28
+ - Update project/flow.mmd
29
+ - ... and 11 more files
30
+
31
+ ## [0.3.25] - 2026-04-22
32
+
33
+ ### Docs
34
+ - Update README.md
35
+
10
36
  ## [0.3.24] - 2026-04-22
11
37
 
12
38
  ### Docs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sumd
3
- Version: 0.3.24
3
+ Version: 0.3.26
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.24-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-19.5h-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.26-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-19.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
46
46
 
47
- - 🤖 **LLM usage:** $7.5000 (54 commits)
48
- - 👤 **Human dev:** ~$1946 (19.5h @ $100/h, 30min dedup)
47
+ - 🤖 **LLM usage:** $7.5000 (56 commits)
48
+ - 👤 **Human dev:** ~$1971 (19.7h @ $100/h, 30min dedup)
49
49
 
50
- Generated on 2026-04-22 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
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.24-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.26-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.24-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-19.5h-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.26-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-19.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.5000 (54 commits)
10
- - 👤 **Human dev:** ~$1946 (19.5h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $7.5000 (56 commits)
10
+ - 👤 **Human dev:** ~$1971 (19.7h @ $100/h, 30min dedup)
11
11
 
12
- Generated on 2026-04-22 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
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.24-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.26-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.24"
7
+ version = "0.3.26"
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.24"
7
+ __version__ = "0.3.26"
8
8
 
9
9
  from sumd.parser import (
10
10
  SUMDDocument,
@@ -12,10 +12,12 @@ from sumd.parser import (
12
12
  parse,
13
13
  parse_file,
14
14
  validate,
15
+ )
16
+ from sumd.validator import (
17
+ CodeBlockIssue,
15
18
  validate_codeblocks,
16
19
  validate_markdown,
17
20
  validate_sumd_file,
18
- CodeBlockIssue,
19
21
  )
20
22
  from sumd.generator import generate_sumd_content
21
23
 
@@ -872,6 +872,78 @@ def _echo_scan_result(proj_dir: Path, doc, sources: list, cb_warnings: list) ->
872
872
  )
873
873
 
874
874
 
875
+ def _maybe_generate_doql(proj_dir: Path, fix: bool) -> None:
876
+ """Generate app.doql.less BEFORE rendering SUMD so it gets included.
877
+
878
+ Pull name/version from the most specific manifest available and
879
+ pick language-appropriate defaults for the boilerplate.
880
+ """
881
+ project_type = _detect_project_type(proj_dir)
882
+ pyproj = extract_pyproject(proj_dir)
883
+ pkg_json = extract_package_json(proj_dir) if project_type == "node" else {}
884
+ project_name = (
885
+ pyproj.get("name")
886
+ or pkg_json.get("name")
887
+ or proj_dir.name
888
+ )
889
+ version = (
890
+ pyproj.get("version")
891
+ or pkg_json.get("version")
892
+ or "0.1.0"
893
+ )
894
+ doql_path = _generate_doql_less(
895
+ proj_dir, project_name, version,
896
+ force=fix, project_type=project_type,
897
+ )
898
+ if doql_path:
899
+ click.echo(f" 📝 Generated {doql_path.name} ({project_type})")
900
+
901
+
902
+ def _finalize_scan(
903
+ proj_dir: Path,
904
+ doc,
905
+ sources: list,
906
+ cb_warnings: list,
907
+ export_json: bool,
908
+ run_analyze: bool,
909
+ tool_list: list[str],
910
+ doql_sync: bool,
911
+ sumd_path: Path,
912
+ ) -> dict:
913
+ """Run post-scan actions: export JSON, run analysis, DOQL sync."""
914
+ _echo_scan_result(proj_dir, doc, sources, cb_warnings)
915
+
916
+ if export_json:
917
+ _export_sumd_json(proj_dir, doc)
918
+
919
+ if run_analyze:
920
+ click.echo(" 🔬 Running analysis...")
921
+ _run_analysis_tools(proj_dir, tool_list)
922
+ click.echo(f" ✅ Analysis complete → {proj_dir / 'project'}/")
923
+
924
+ if doql_sync and ((proj_dir / "app.doql.less").exists() or (proj_dir / "app.doql.css").exists()):
925
+ click.echo(" ⚙️ Syncing DOQL...")
926
+ r = subprocess.run(
927
+ ["doql", "sync"],
928
+ cwd=str(proj_dir),
929
+ capture_output=True,
930
+ text=True,
931
+ )
932
+ if r.returncode == 0:
933
+ click.echo(" ✅ DOQL sync complete")
934
+ else:
935
+ click.echo(f" ⚠️ DOQL sync failed: {r.stderr.strip() or r.stdout.strip()}", err=True)
936
+
937
+ return {
938
+ "status": "OK",
939
+ "project_name": doc.project_name,
940
+ "sections": len(doc.sections),
941
+ "sources": sources,
942
+ "path": str(sumd_path),
943
+ "warnings": [c.message for c in cb_warnings],
944
+ }
945
+
946
+
875
947
  def _scan_one_project(
876
948
  proj_dir: Path,
877
949
  fix: bool,
@@ -896,29 +968,8 @@ def _scan_one_project(
896
968
  return {"status": "SKIP", "path": str(sumd_path)}
897
969
 
898
970
  try:
899
- # Generate app.doql.less BEFORE rendering SUMD so it gets included.
900
- # Pull name/version from the most specific manifest available and
901
- # pick language-appropriate defaults for the boilerplate.
902
971
  if generate_doql:
903
- project_type = _detect_project_type(proj_dir)
904
- pyproj = extract_pyproject(proj_dir)
905
- pkg_json = extract_package_json(proj_dir) if project_type == "node" else {}
906
- project_name = (
907
- pyproj.get("name")
908
- or pkg_json.get("name")
909
- or proj_dir.name
910
- )
911
- version = (
912
- pyproj.get("version")
913
- or pkg_json.get("version")
914
- or "0.1.0"
915
- )
916
- doql_path = _generate_doql_less(
917
- proj_dir, project_name, version,
918
- force=fix, project_type=project_type,
919
- )
920
- if doql_path:
921
- click.echo(f" 📝 Generated {doql_path.name} ({project_type})")
972
+ _maybe_generate_doql(proj_dir, fix)
922
973
 
923
974
  doc, md_issues, cb_errors, cb_warnings, sources = _render_write_validate(
924
975
  proj_dir, sumd_path, raw, profile
@@ -933,37 +984,10 @@ def _scan_one_project(
933
984
  click.echo(f" \u2193 {e}")
934
985
  return {"status": "INVALID", "errors": all_errors, "path": str(sumd_path)}
935
986
 
936
- _echo_scan_result(proj_dir, doc, sources, cb_warnings)
937
-
938
- if export_json:
939
- _export_sumd_json(proj_dir, doc)
940
-
941
- if run_analyze:
942
- click.echo(" \U0001f52c Running analysis...")
943
- _run_analysis_tools(proj_dir, tool_list)
944
- click.echo(f" \u2705 Analysis complete \u2192 {proj_dir / 'project'}/")
945
-
946
- if doql_sync and ((proj_dir / "app.doql.less").exists() or (proj_dir / "app.doql.css").exists()):
947
- click.echo(" \u2699\ufe0f Syncing DOQL...")
948
- r = subprocess.run(
949
- ["doql", "sync"],
950
- cwd=str(proj_dir),
951
- capture_output=True,
952
- text=True,
953
- )
954
- if r.returncode == 0:
955
- click.echo(" \u2705 DOQL sync complete")
956
- else:
957
- click.echo(f" \u26a0\ufe0f DOQL sync failed: {r.stderr.strip() or r.stdout.strip()}", err=True)
958
-
959
- return {
960
- "status": "OK",
961
- "project_name": doc.project_name,
962
- "sections": len(doc.sections),
963
- "sources": sources,
964
- "path": str(sumd_path),
965
- "warnings": [c.message for c in cb_warnings],
966
- }
987
+ return _finalize_scan(
988
+ proj_dir, doc, sources, cb_warnings,
989
+ export_json, run_analyze, tool_list, doql_sync, sumd_path,
990
+ )
967
991
 
968
992
  except Exception as exc:
969
993
  dash = "\u2013"
@@ -975,7 +999,7 @@ def _scan_one_project(
975
999
  @click.argument("workspace", type=click.Path(exists=True, path_type=Path), default=".")
976
1000
  @click.option(
977
1001
  "--export-json/--no-export-json",
978
- default=True,
1002
+ default=False,
979
1003
  help="Also export sumd.json per project",
980
1004
  )
981
1005
  @click.option(
@@ -0,0 +1,233 @@
1
+ """SUMD Parser - Parse and validate SUMD markdown documents."""
2
+
3
+ import re
4
+ from dataclasses import dataclass, field
5
+ from enum import Enum
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List, Optional
8
+
9
+
10
+ class SectionType(Enum):
11
+ """SUMD section types."""
12
+
13
+ PROJECT_HEADER = "project_header"
14
+ METADATA = "metadata"
15
+ INTENT = "intent"
16
+ ARCHITECTURE = "architecture"
17
+ INTERFACES = "interfaces"
18
+ WORKFLOWS = "workflows"
19
+ CONFIGURATION = "configuration"
20
+ DEPENDENCIES = "dependencies"
21
+ DEPLOYMENT = "deployment"
22
+ UNKNOWN = "unknown"
23
+
24
+
25
+ @dataclass
26
+ class Section:
27
+ """Represents a SUMD section."""
28
+
29
+ name: str
30
+ type: SectionType
31
+ content: str
32
+ level: int
33
+ subsections: List["Section"] = field(default_factory=list)
34
+ metadata: Dict[str, Any] = field(default_factory=dict)
35
+
36
+
37
+ @dataclass
38
+ class SUMDDocument:
39
+ """Represents a parsed SUMD document."""
40
+
41
+ project_name: str
42
+ description: str
43
+ sections: List[Section] = field(default_factory=list)
44
+ metadata: Dict[str, Any] = field(default_factory=dict)
45
+ raw_content: str = ""
46
+
47
+
48
+ class SUMDParser:
49
+ """Parser for SUMD markdown documents."""
50
+
51
+ SECTION_MAPPING = {
52
+ "metadata": SectionType.METADATA,
53
+ "intent": SectionType.INTENT,
54
+ "architecture": SectionType.ARCHITECTURE,
55
+ "interfaces": SectionType.INTERFACES,
56
+ "workflows": SectionType.WORKFLOWS,
57
+ "configuration": SectionType.CONFIGURATION,
58
+ "dependencies": SectionType.DEPENDENCIES,
59
+ "deployment": SectionType.DEPLOYMENT,
60
+ }
61
+
62
+ def __init__(self):
63
+ self.current_document: Optional[SUMDDocument] = None
64
+
65
+ def parse(self, content: str) -> SUMDDocument:
66
+ """Parse a SUMD markdown document.
67
+
68
+ Args:
69
+ content: The markdown content to parse
70
+
71
+ Returns:
72
+ SUMDDocument: Parsed document structure
73
+ """
74
+ self.current_document = SUMDDocument(
75
+ project_name="", description="", raw_content=content
76
+ )
77
+
78
+ lines = content.split("\n")
79
+ self._parse_header(lines)
80
+ self._parse_sections(lines)
81
+
82
+ return self.current_document
83
+
84
+ def parse_file(self, path: Path) -> SUMDDocument:
85
+ """Parse a SUMD file.
86
+
87
+ Args:
88
+ path: Path to the SUMD markdown file
89
+
90
+ Returns:
91
+ SUMDDocument: Parsed document structure
92
+ """
93
+ content = path.read_text(encoding="utf-8")
94
+ return self.parse(content)
95
+
96
+ def _parse_header(self, lines: List[str]) -> None:
97
+ """Parse the project header (H1).
98
+
99
+ Args:
100
+ lines: List of document lines
101
+ """
102
+ if not lines:
103
+ return
104
+
105
+ # Find H1 header
106
+ for i, line in enumerate(lines):
107
+ if line.startswith("# ") and not line.startswith("##"):
108
+ header_content = line[2:].strip()
109
+ # Split into name and description
110
+ parts = header_content.split(" - ", 1)
111
+ self.current_document.project_name = parts[0].strip()
112
+ if len(parts) > 1:
113
+ self.current_document.description = parts[1].strip()
114
+ else:
115
+ # Look for description on next lines
116
+ for j in range(i + 1, min(i + 3, len(lines))):
117
+ if lines[j].strip() and not lines[j].startswith("#"):
118
+ self.current_document.description = lines[j].strip()
119
+ break
120
+ break
121
+
122
+ def _parse_sections(self, lines: List[str]) -> None:
123
+ """Parse all sections in the document.
124
+
125
+ Args:
126
+ lines: List of document lines
127
+ """
128
+ sections = []
129
+ current_section = None
130
+ current_content = []
131
+
132
+ for line in lines:
133
+ if line.startswith("##"):
134
+ # Save previous section
135
+ if current_section:
136
+ current_section.content = "\n".join(current_content).strip()
137
+ sections.append(current_section)
138
+ current_content = []
139
+
140
+ # Start new section
141
+ section_name = line[2:].strip().lower()
142
+ section_type = self.SECTION_MAPPING.get(
143
+ section_name, SectionType.UNKNOWN
144
+ )
145
+ current_section = Section(
146
+ name=section_name, type=section_type, content="", level=2
147
+ )
148
+ elif current_section:
149
+ current_content.append(line)
150
+
151
+ # Save last section
152
+ if current_section:
153
+ current_section.content = "\n".join(current_content).strip()
154
+ sections.append(current_section)
155
+
156
+ self.current_document.sections = sections
157
+
158
+ def validate(self, document: SUMDDocument) -> List[str]:
159
+ """Validate a SUMD document against the specification.
160
+
161
+ Args:
162
+ document: The document to validate
163
+
164
+ Returns:
165
+ List of validation errors (empty if valid)
166
+ """
167
+ errors = []
168
+
169
+ # Check required sections
170
+ section_types = {section.type for section in document.sections}
171
+
172
+ if SectionType.INTENT not in section_types:
173
+ errors.append("Missing required section: Intent")
174
+
175
+ if SectionType.ARCHITECTURE not in section_types:
176
+ errors.append("Missing required section: Architecture")
177
+
178
+ if SectionType.INTERFACES not in section_types:
179
+ errors.append("Missing required section: Interfaces")
180
+
181
+ # Check project name
182
+ if not document.project_name:
183
+ errors.append("Missing project name in header")
184
+
185
+ return errors
186
+
187
+
188
+ def parse(content: str) -> SUMDDocument:
189
+ """Parse a SUMD markdown document.
190
+
191
+ Args:
192
+ content: The markdown content to parse
193
+
194
+ Returns:
195
+ SUMDDocument: Parsed document structure
196
+ """
197
+ parser = SUMDParser()
198
+ return parser.parse(content)
199
+
200
+
201
+ def parse_file(path: Path) -> SUMDDocument:
202
+ """Parse a SUMD file.
203
+
204
+ Args:
205
+ path: Path to the SUMD markdown file
206
+
207
+ Returns:
208
+ SUMDDocument: Parsed document structure
209
+ """
210
+ parser = SUMDParser()
211
+ return parser.parse_file(path)
212
+
213
+
214
+ def validate(document: SUMDDocument) -> List[str]:
215
+ """Validate a SUMD document.
216
+
217
+ Args:
218
+ document: The document to validate
219
+
220
+ Returns:
221
+ List of validation errors (empty if valid)
222
+ """
223
+ parser = SUMDParser()
224
+ return parser.validate(document)
225
+
226
+
227
+ # Backward-compatible re-exports from validator module
228
+ from sumd.validator import (
229
+ CodeBlockIssue,
230
+ validate_codeblocks,
231
+ validate_markdown,
232
+ validate_sumd_file,
233
+ )
@@ -1,227 +1,8 @@
1
- """SUMD Parser - Parse and validate SUMD markdown documents."""
1
+ """SUMD Validator validate markdown structure and fenced codeblocks."""
2
2
 
3
3
  import re
4
- from dataclasses import dataclass, field
5
- from enum import Enum
4
+ from dataclasses import dataclass
6
5
  from pathlib import Path
7
- from typing import Any, Dict, List, Optional
8
-
9
-
10
- class SectionType(Enum):
11
- """SUMD section types."""
12
-
13
- PROJECT_HEADER = "project_header"
14
- METADATA = "metadata"
15
- INTENT = "intent"
16
- ARCHITECTURE = "architecture"
17
- INTERFACES = "interfaces"
18
- WORKFLOWS = "workflows"
19
- CONFIGURATION = "configuration"
20
- DEPENDENCIES = "dependencies"
21
- DEPLOYMENT = "deployment"
22
- UNKNOWN = "unknown"
23
-
24
-
25
- @dataclass
26
- class Section:
27
- """Represents a SUMD section."""
28
-
29
- name: str
30
- type: SectionType
31
- content: str
32
- level: int
33
- subsections: List["Section"] = field(default_factory=list)
34
- metadata: Dict[str, Any] = field(default_factory=dict)
35
-
36
-
37
- @dataclass
38
- class SUMDDocument:
39
- """Represents a parsed SUMD document."""
40
-
41
- project_name: str
42
- description: str
43
- sections: List[Section] = field(default_factory=list)
44
- metadata: Dict[str, Any] = field(default_factory=dict)
45
- raw_content: str = ""
46
-
47
-
48
- class SUMDParser:
49
- """Parser for SUMD markdown documents."""
50
-
51
- SECTION_MAPPING = {
52
- "metadata": SectionType.METADATA,
53
- "intent": SectionType.INTENT,
54
- "architecture": SectionType.ARCHITECTURE,
55
- "interfaces": SectionType.INTERFACES,
56
- "workflows": SectionType.WORKFLOWS,
57
- "configuration": SectionType.CONFIGURATION,
58
- "dependencies": SectionType.DEPENDENCIES,
59
- "deployment": SectionType.DEPLOYMENT,
60
- }
61
-
62
- def __init__(self):
63
- self.current_document: Optional[SUMDDocument] = None
64
-
65
- def parse(self, content: str) -> SUMDDocument:
66
- """Parse a SUMD markdown document.
67
-
68
- Args:
69
- content: The markdown content to parse
70
-
71
- Returns:
72
- SUMDDocument: Parsed document structure
73
- """
74
- self.current_document = SUMDDocument(
75
- project_name="", description="", raw_content=content
76
- )
77
-
78
- lines = content.split("\n")
79
- self._parse_header(lines)
80
- self._parse_sections(lines)
81
-
82
- return self.current_document
83
-
84
- def parse_file(self, path: Path) -> SUMDDocument:
85
- """Parse a SUMD file.
86
-
87
- Args:
88
- path: Path to the SUMD markdown file
89
-
90
- Returns:
91
- SUMDDocument: Parsed document structure
92
- """
93
- content = path.read_text(encoding="utf-8")
94
- return self.parse(content)
95
-
96
- def _parse_header(self, lines: List[str]) -> None:
97
- """Parse the project header (H1).
98
-
99
- Args:
100
- lines: List of document lines
101
- """
102
- if not lines:
103
- return
104
-
105
- # Find H1 header
106
- for i, line in enumerate(lines):
107
- if line.startswith("# ") and not line.startswith("##"):
108
- header_content = line[2:].strip()
109
- # Split into name and description
110
- parts = header_content.split(" - ", 1)
111
- self.current_document.project_name = parts[0].strip()
112
- if len(parts) > 1:
113
- self.current_document.description = parts[1].strip()
114
- else:
115
- # Look for description on next lines
116
- for j in range(i + 1, min(i + 3, len(lines))):
117
- if lines[j].strip() and not lines[j].startswith("#"):
118
- self.current_document.description = lines[j].strip()
119
- break
120
- break
121
-
122
- def _parse_sections(self, lines: List[str]) -> None:
123
- """Parse all sections in the document.
124
-
125
- Args:
126
- lines: List of document lines
127
- """
128
- sections = []
129
- current_section = None
130
- current_content = []
131
-
132
- for line in lines:
133
- if line.startswith("##"):
134
- # Save previous section
135
- if current_section:
136
- current_section.content = "\n".join(current_content).strip()
137
- sections.append(current_section)
138
- current_content = []
139
-
140
- # Start new section
141
- section_name = line[2:].strip().lower()
142
- section_type = self.SECTION_MAPPING.get(
143
- section_name, SectionType.UNKNOWN
144
- )
145
- current_section = Section(
146
- name=section_name, type=section_type, content="", level=2
147
- )
148
- elif current_section:
149
- current_content.append(line)
150
-
151
- # Save last section
152
- if current_section:
153
- current_section.content = "\n".join(current_content).strip()
154
- sections.append(current_section)
155
-
156
- self.current_document.sections = sections
157
-
158
- def validate(self, document: SUMDDocument) -> List[str]:
159
- """Validate a SUMD document against the specification.
160
-
161
- Args:
162
- document: The document to validate
163
-
164
- Returns:
165
- List of validation errors (empty if valid)
166
- """
167
- errors = []
168
-
169
- # Check required sections
170
- section_types = {section.type for section in document.sections}
171
-
172
- if SectionType.INTENT not in section_types:
173
- errors.append("Missing required section: Intent")
174
-
175
- if SectionType.ARCHITECTURE not in section_types:
176
- errors.append("Missing required section: Architecture")
177
-
178
- if SectionType.INTERFACES not in section_types:
179
- errors.append("Missing required section: Interfaces")
180
-
181
- # Check project name
182
- if not document.project_name:
183
- errors.append("Missing project name in header")
184
-
185
- return errors
186
-
187
-
188
- def parse(content: str) -> SUMDDocument:
189
- """Parse a SUMD markdown document.
190
-
191
- Args:
192
- content: The markdown content to parse
193
-
194
- Returns:
195
- SUMDDocument: Parsed document structure
196
- """
197
- parser = SUMDParser()
198
- return parser.parse(content)
199
-
200
-
201
- def parse_file(path: Path) -> SUMDDocument:
202
- """Parse a SUMD file.
203
-
204
- Args:
205
- path: Path to the SUMD markdown file
206
-
207
- Returns:
208
- SUMDDocument: Parsed document structure
209
- """
210
- parser = SUMDParser()
211
- return parser.parse_file(path)
212
-
213
-
214
- def validate(document: SUMDDocument) -> List[str]:
215
- """Validate a SUMD document.
216
-
217
- Args:
218
- document: The document to validate
219
-
220
- Returns:
221
- List of validation errors (empty if valid)
222
- """
223
- parser = SUMDParser()
224
- return parser.validate(document)
225
6
 
226
7
 
227
8
  # ---------------------------------------------------------------------------
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