Skip to content

Commit f913133

Browse files
authored
Create controller.py
1 parent a0e3802 commit f913133

1 file changed

Lines changed: 207 additions & 0 deletions

File tree

src/stamp/engine/controller.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# File: src/stamp/engine/controller.py
2+
3+
from __future__ import annotations
4+
5+
import os
6+
from typing import Dict, Any, List, Optional, Tuple
7+
8+
from .loader import Loader
9+
from .parser import Parser
10+
from .validator import Validator
11+
from .normalizer import Normalizer
12+
from .reporter import Reporter
13+
14+
15+
class Controller:
16+
"""
17+
Controller
18+
----------
19+
The central orchestration layer for Stamp.
20+
21+
Implements the full workflow as defined in Stamp-Spec.md §3, §4, §5:
22+
23+
Loader → Parser → Validator → Normalizer → Reporter
24+
25+
Responsibilities:
26+
- Resolve paths from CLI
27+
- Load file contents
28+
- Parse metadata block
29+
- Validate metadata + classify errors
30+
- Apply normalization when allowed
31+
- Rewrite file if repairable
32+
- Generate machine-readable report
33+
- Return exit codes and validation statuses
34+
35+
This module is intentionally stateless and deterministic.
36+
"""
37+
38+
def __init__(self, schema: Dict[str, Any]):
39+
self.loader = Loader()
40+
self.parser = Parser()
41+
self.validator = Validator(schema)
42+
self.normalizer = Normalizer()
43+
self.reporter = Reporter()
44+
45+
# ----------------------------------------------------------------------
46+
# PUBLIC ENTRYPOINTS
47+
# ----------------------------------------------------------------------
48+
49+
def process_file(
50+
self,
51+
path: str,
52+
fix: bool = False,
53+
output_dir: Optional[str] = None,
54+
) -> Tuple[Dict[str, Any], int]:
55+
"""
56+
Process a single file.
57+
58+
Arguments:
59+
path — filesystem path to Markdown file
60+
fix — whether Stamp is allowed to auto-correct
61+
output_dir — alternate write location for normalized output
62+
63+
Returns:
64+
(report_dict, exit_code)
65+
"""
66+
67+
# ----------------------------------------------------------
68+
# 1. Load file
69+
# ----------------------------------------------------------
70+
original_content, loader_errors = self.loader.load_file(path)
71+
72+
# If loader errors → fatal
73+
if loader_errors:
74+
validation = self.validator.validate(
75+
metadata={},
76+
body=original_content,
77+
had_metadata=False,
78+
loader_errors=loader_errors,
79+
)
80+
report = self.reporter.generate_report(
81+
path=path,
82+
original_content=original_content,
83+
rewritten_content=None,
84+
validation=validation,
85+
corrections=None,
86+
)
87+
return report, 2
88+
89+
# ----------------------------------------------------------
90+
# 2. Parse YAML frontmatter
91+
# ----------------------------------------------------------
92+
metadata, body, had_metadata = self.parser.extract_metadata_block(
93+
original_content
94+
)
95+
96+
# ----------------------------------------------------------
97+
# 3. Validate metadata
98+
# ----------------------------------------------------------
99+
validation = self.validator.validate(
100+
metadata,
101+
body,
102+
had_metadata,
103+
loader_errors,
104+
)
105+
106+
# Exit code logic based on Stamp-Spec.md §5.3
107+
if validation.fatal_errors:
108+
# Fatal → do not rewrite
109+
report = self.reporter.generate_report(
110+
path=path,
111+
original_content=original_content,
112+
rewritten_content=None,
113+
validation=validation,
114+
corrections=None,
115+
)
116+
return report, 2
117+
118+
if validation.repairable_errors and not fix:
119+
# Repairable but in CHECK mode → do not rewrite, return repairable exit code
120+
report = self.reporter.generate_report(
121+
path=path,
122+
original_content=original_content,
123+
rewritten_content=None,
124+
validation=validation,
125+
corrections=validation.repairable_errors,
126+
)
127+
return report, 1
128+
129+
# ----------------------------------------------------------
130+
# 4. Apply normalization (only if fix=True)
131+
# ----------------------------------------------------------
132+
if validation.repairable_errors and fix:
133+
normalized_yaml, body_out, corrections = self.normalizer.normalize(
134+
validation.metadata,
135+
validation.body,
136+
)
137+
rewritten = self._rebuild_document(normalized_yaml, body_out)
138+
else:
139+
# No modifications required
140+
rewritten = original_content
141+
corrections = []
142+
143+
# ----------------------------------------------------------
144+
# 5. Write output if fix=True or if output_dir provided
145+
# ----------------------------------------------------------
146+
rewritten_path = None
147+
148+
if (fix and validation.repairable_errors) or output_dir:
149+
rewritten_path = self._write_output(
150+
path=path,
151+
content=rewritten,
152+
output_dir=output_dir,
153+
)
154+
155+
# ----------------------------------------------------------
156+
# 6. Create final report
157+
# ----------------------------------------------------------
158+
report = self.reporter.generate_report(
159+
path=rewritten_path or path,
160+
original_content=original_content,
161+
rewritten_content=rewritten if rewritten != original_content else None,
162+
validation=validation,
163+
corrections=corrections,
164+
)
165+
166+
# Exit code:
167+
# - 0 = pass/no repairs needed
168+
# - 1 = repairs applied
169+
# - 2 = fatal errors
170+
exit_code = 1 if corrections else 0
171+
172+
return report, exit_code
173+
174+
# ----------------------------------------------------------------------
175+
# INTERNAL HELPERS
176+
# ----------------------------------------------------------------------
177+
178+
def _rebuild_document(self, yaml_text: str, body: str) -> str:
179+
"""
180+
Reconstruct the document after normalization:
181+
182+
---
183+
<yaml_text>
184+
---
185+
<body>
186+
187+
Body is left unchanged.
188+
"""
189+
return f"---\n{yaml_text}---\n{body}"
190+
191+
# ----------------------------------------------------------------------
192+
193+
def _write_output(self, path: str, content: str, output_dir: Optional[str]) -> str:
194+
"""
195+
Write normalized content either in-place or into a designated output directory.
196+
Returns the path written to.
197+
"""
198+
if output_dir:
199+
os.makedirs(output_dir, exist_ok=True)
200+
out_path = os.path.join(output_dir, os.path.basename(path))
201+
else:
202+
out_path = path
203+
204+
with open(out_path, "w", encoding="utf-8") as fh:
205+
fh.write(content)
206+
207+
return out_path

0 commit comments

Comments
 (0)