2424import sys
2525from pathlib import Path
2626from typing import Any
27+ from rich import print
2728
2829# ---------------------------------------------------------------------------
29- # Lazy imports — keep startup fast and give clear messages when deps missing.
30+ # function for colored output
3031# ---------------------------------------------------------------------------
3132
33+ def error (msg ):
34+ print (f"[red]{ msg } [/red]" , file = sys .stderr )
35+
36+ def success (msg ):
37+ print (f"[green]{ msg } [/green]" , file = sys .stderr )
38+
39+ def warn (msg ):
40+ print (f"[yellow]{ msg } [/yellow]" , file = sys .stderr )
41+
42+ def policy_violation (msg ):
43+ print (f"[bold red]{ msg } [/bold red]" , file = sys .stderr )
44+
45+ def passed_check (msg ):
46+ print (f"[green]✔ { msg } [/green]" , file = sys .stderr )
47+
48+ # ---------------------------------------------------------------------------
49+ # Lazy imports
50+ # ---------------------------------------------------------------------------
3251
3352def _import_yaml () -> Any :
3453 try :
3554 import yaml
36-
3755 return yaml
3856 except ImportError :
39- print ("ERROR: pyyaml is required — pip install pyyaml" , file = sys . stderr )
57+ error ("ERROR: pyyaml is required — pip install pyyaml" )
4058 sys .exit (2 )
4159
4260
@@ -49,35 +67,33 @@ def _load_file(path: Path) -> dict[str, Any]:
4967 elif path .suffix == ".json" :
5068 data = json .loads (text )
5169 else :
52- # Try YAML first, fall back to JSON
5370 yaml = _import_yaml ()
5471 try :
5572 data = yaml .safe_load (text )
5673 except Exception :
5774 data = json .loads (text )
75+
5876 if not isinstance (data , dict ):
5977 raise ValueError (f"Expected a mapping at top level, got { type (data ).__name__ } " )
6078 return data
6179
62-
6380# ============================================================================
6481# validate
6582# ============================================================================
6683
67-
6884def cmd_validate (args : argparse .Namespace ) -> int :
6985 """Validate a policy YAML/JSON file against the PolicyDocument schema."""
7086 from .schema import PolicyDocument # noqa: E402
7187
7288 path = Path (args .path )
7389 if not path .exists ():
74- print (f"ERROR: file not found: { path } " , file = sys . stderr )
90+ error (f"ERROR: file not found: { path } " )
7591 return 2
7692
7793 try :
7894 data = _load_file (path )
7995 except Exception as exc :
80- print (f"ERROR: failed to parse { path } : { exc } " , file = sys . stderr )
96+ error (f"ERROR: failed to parse { path } : { exc } " )
8197 return 2
8298
8399 # --- Optional JSON-Schema validation (best-effort) --------------------
@@ -89,32 +105,30 @@ def cmd_validate(args: argparse.Namespace) -> int:
89105 schema = json .loads (schema_path .read_text (encoding = "utf-8" ))
90106 jsonschema .validate (instance = data , schema = schema )
91107 except ImportError :
92- pass # jsonschema not installed — skip, rely on Pydantic
108+ pass
93109 except jsonschema .ValidationError as ve :
94- print (f"FAIL: { path } " )
95- print (f" JSON-Schema error: { ve .message } " )
110+ policy_violation (f"FAIL: { path } " )
111+ error (f" JSON-Schema error: { ve .message } " )
96112 if ve .absolute_path :
97- print (f" Location: { ' -> ' .join (str (p ) for p in ve .absolute_path )} " )
113+ warn (f" Location: { ' -> ' .join (str (p ) for p in ve .absolute_path )} " )
98114 return 1
99115
100116 # --- Pydantic validation (authoritative) ------------------------------
101117 try :
102118 PolicyDocument .model_validate (data )
103119 except Exception as exc :
104- print (f"FAIL: { path } " )
120+ policy_violation (f"FAIL: { path } " )
105121 for line in str (exc ).splitlines ():
106- print (f" { line } " )
122+ error (f" { line } " )
107123 return 1
108124
109- print (f"OK: { path } " )
125+ success (f"OK: { path } " )
110126 return 0
111127
112-
113128# ============================================================================
114129# test
115130# ============================================================================
116131
117-
118132def cmd_test (args : argparse .Namespace ) -> int :
119133 """Test a policy against a set of scenarios."""
120134 from .evaluator import PolicyEvaluator # noqa: E402
@@ -125,7 +139,7 @@ def cmd_test(args: argparse.Namespace) -> int:
125139
126140 for p in (policy_path , scenarios_path ):
127141 if not p .exists ():
128- print (f"ERROR: file not found: { p } " , file = sys . stderr )
142+ error (f"ERROR: file not found: { p } " )
129143 return 2
130144
131145 # Load the policy
@@ -135,19 +149,19 @@ def cmd_test(args: argparse.Namespace) -> int:
135149 try :
136150 doc = PolicyDocument .from_json (policy_path )
137151 except Exception :
138- print (f"ERROR: failed to load policy { policy_path } : { exc } " , file = sys . stderr )
152+ error (f"ERROR: failed to load policy { policy_path } : { exc } " )
139153 return 2
140154
141155 # Load scenarios
142156 try :
143157 scenarios_data = _load_file (scenarios_path )
144158 except Exception as exc :
145- print (f"ERROR: failed to parse scenarios { scenarios_path } : { exc } " , file = sys . stderr )
159+ error (f"ERROR: failed to parse scenarios { scenarios_path } : { exc } " )
146160 return 2
147161
148162 scenarios = scenarios_data .get ("scenarios" , [])
149163 if not scenarios :
150- print ("ERROR: no scenarios found in test file" , file = sys . stderr )
164+ error ("ERROR: no scenarios found in test file" )
151165 return 2
152166
153167 evaluator = PolicyEvaluator (policies = [doc ])
@@ -175,22 +189,20 @@ def cmd_test(args: argparse.Namespace) -> int:
175189
176190 if errors :
177191 failed += 1
178- print (f" FAIL: { name } " )
192+ policy_violation (f"FAIL: { name } " )
179193 for err in errors :
180- print (f" - { err } " )
194+ error (f" - { err } " )
181195 else :
182196 passed += 1
183- print ( f" PASS: { name } " )
197+ passed_check ( name )
184198
185- print (f"\n { passed } /{ total } scenarios passed." )
199+ success (f"\n { passed } /{ total } scenarios passed." )
186200 return 1 if failed > 0 else 0
187201
188-
189202# ============================================================================
190203# diff
191204# ============================================================================
192205
193-
194206def cmd_diff (args : argparse .Namespace ) -> int :
195207 """Show differences between two policy files."""
196208 from .schema import PolicyDocument # noqa: E402
@@ -200,14 +212,14 @@ def cmd_diff(args: argparse.Namespace) -> int:
200212
201213 for p in (path1 , path2 ):
202214 if not p .exists ():
203- print (f"ERROR: file not found: { p } " , file = sys . stderr )
215+ error (f"ERROR: file not found: { p } " )
204216 return 2
205217
206218 try :
207219 doc1 = PolicyDocument .model_validate (_load_file (path1 ))
208220 doc2 = PolicyDocument .model_validate (_load_file (path2 ))
209221 except Exception as exc :
210- print (f"ERROR: failed to load policies: { exc } " , file = sys . stderr )
222+ error (f"ERROR: failed to load policies: { exc } " )
211223 return 2
212224
213225 differences : list [str ] = []
@@ -271,20 +283,18 @@ def cmd_diff(args: argparse.Namespace) -> int:
271283 differences .append (f" rule '{ name } ' message changed" )
272284
273285 if differences :
274- print (f"Differences between { path1 } and { path2 } :" )
286+ success (f"Differences between { path1 } and { path2 } :" )
275287 for diff in differences :
276288 print (diff )
277289 return 1
278290 else :
279- print (f"No differences between { path1 } and { path2 } ." )
291+ success (f"No differences between { path1 } and { path2 } ." )
280292 return 0
281293
282-
283294# ============================================================================
284295# Main entry point
285296# ============================================================================
286297
287-
288298def main (argv : list [str ] | None = None ) -> int :
289299 """Parse arguments and dispatch to the appropriate subcommand."""
290300 parser = argparse .ArgumentParser (
@@ -293,22 +303,22 @@ def main(argv: list[str] | None = None) -> int:
293303 )
294304 subparsers = parser .add_subparsers (dest = "command" , help = "Available commands" )
295305
296- # -- validate ----------------------------------------------------------
306+ # -- validate
297307 p_validate = subparsers .add_parser (
298308 "validate" ,
299309 help = "Validate a policy YAML/JSON file against the schema." ,
300310 )
301311 p_validate .add_argument ("path" , help = "Path to the policy file to validate." )
302312
303- # -- test --------------------------------------------------------------
313+ # -- test
304314 p_test = subparsers .add_parser (
305315 "test" ,
306316 help = "Test a policy against a set of scenarios." ,
307317 )
308318 p_test .add_argument ("policy_path" , help = "Path to the policy file." )
309319 p_test .add_argument ("test_scenarios_path" , help = "Path to the test scenarios YAML." )
310320
311- # -- diff --------------------------------------------------------------
321+ # -- diff
312322 p_diff = subparsers .add_parser (
313323 "diff" ,
314324 help = "Show differences between two policy files." ,
0 commit comments