invarlock 0.3.6__py3-none-any.whl → 0.3.7__py3-none-any.whl

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 (55) hide show
  1. invarlock/__init__.py +2 -2
  2. invarlock/adapters/__init__.py +10 -14
  3. invarlock/adapters/auto.py +35 -40
  4. invarlock/adapters/capabilities.py +2 -2
  5. invarlock/adapters/hf_causal.py +418 -0
  6. invarlock/adapters/{hf_onnx.py → hf_causal_onnx.py} +3 -3
  7. invarlock/adapters/hf_mixin.py +25 -4
  8. invarlock/adapters/{hf_bert.py → hf_mlm.py} +4 -11
  9. invarlock/adapters/{hf_t5.py → hf_seq2seq.py} +9 -9
  10. invarlock/cli/adapter_auto.py +31 -21
  11. invarlock/cli/app.py +73 -2
  12. invarlock/cli/commands/certify.py +600 -59
  13. invarlock/cli/commands/doctor.py +8 -10
  14. invarlock/cli/commands/plugins.py +13 -9
  15. invarlock/cli/commands/report.py +233 -69
  16. invarlock/cli/commands/run.py +907 -183
  17. invarlock/cli/commands/verify.py +76 -11
  18. invarlock/cli/config.py +1 -1
  19. invarlock/cli/doctor_helpers.py +4 -5
  20. invarlock/cli/output.py +193 -0
  21. invarlock/cli/provenance.py +1 -1
  22. invarlock/core/bootstrap.py +1 -1
  23. invarlock/core/registry.py +9 -11
  24. invarlock/core/runner.py +111 -25
  25. invarlock/edits/quant_rtn.py +65 -37
  26. invarlock/eval/bench.py +3 -3
  27. invarlock/eval/data.py +68 -23
  28. invarlock/eval/metrics.py +59 -1
  29. invarlock/eval/tasks/__init__.py +12 -0
  30. invarlock/eval/tasks/classification.py +48 -0
  31. invarlock/eval/tasks/qa.py +36 -0
  32. invarlock/eval/tasks/text_generation.py +102 -0
  33. invarlock/guards/invariants.py +19 -10
  34. invarlock/guards/rmt.py +2 -2
  35. invarlock/guards/variance.py +2 -2
  36. invarlock/model_profile.py +48 -27
  37. invarlock/observability/health.py +6 -6
  38. invarlock/observability/metrics.py +108 -0
  39. invarlock/reporting/certificate.py +159 -9
  40. invarlock/reporting/certificate_schema.py +1 -1
  41. invarlock/reporting/guards_analysis.py +154 -4
  42. invarlock/reporting/html.py +55 -5
  43. invarlock/reporting/normalizer.py +7 -0
  44. invarlock/reporting/render.py +791 -431
  45. invarlock/reporting/report.py +39 -3
  46. invarlock/reporting/report_types.py +6 -1
  47. invarlock/reporting/telemetry.py +86 -0
  48. {invarlock-0.3.6.dist-info → invarlock-0.3.7.dist-info}/METADATA +23 -9
  49. {invarlock-0.3.6.dist-info → invarlock-0.3.7.dist-info}/RECORD +53 -48
  50. {invarlock-0.3.6.dist-info → invarlock-0.3.7.dist-info}/WHEEL +1 -1
  51. {invarlock-0.3.6.dist-info → invarlock-0.3.7.dist-info}/entry_points.txt +5 -3
  52. invarlock/adapters/hf_gpt2.py +0 -404
  53. invarlock/adapters/hf_llama.py +0 -487
  54. {invarlock-0.3.6.dist-info → invarlock-0.3.7.dist-info}/licenses/LICENSE +0 -0
  55. {invarlock-0.3.6.dist-info → invarlock-0.3.7.dist-info}/top_level.txt +0 -0
@@ -1042,8 +1042,7 @@ def doctor_command(
1042
1042
  module = str(info.get("module") or "")
1043
1043
  support = (
1044
1044
  "auto"
1045
- if module.startswith("invarlock.adapters")
1046
- and n in {"hf_causal_auto", "hf_mlm_auto"}
1045
+ if module.startswith("invarlock.adapters") and n in {"hf_auto"}
1047
1046
  else (
1048
1047
  "core"
1049
1048
  if module.startswith("invarlock.adapters")
@@ -1058,11 +1057,10 @@ def doctor_command(
1058
1057
 
1059
1058
  # Heuristic backend mapping without heavy imports
1060
1059
  if n in {
1061
- "hf_gpt2",
1062
- "hf_bert",
1063
- "hf_llama",
1064
- "hf_causal_auto",
1065
- "hf_mlm_auto",
1060
+ "hf_causal",
1061
+ "hf_mlm",
1062
+ "hf_seq2seq",
1063
+ "hf_auto",
1066
1064
  }:
1067
1065
  # Transformers-based
1068
1066
  backend = "transformers"
@@ -1097,8 +1095,8 @@ def doctor_command(
1097
1095
  }.get(n)
1098
1096
  if hint:
1099
1097
  enable = f"pip install '{hint}'"
1100
- # Special-case: hf_onnx is a core adapter but requires Optimum/ONNXRuntime
1101
- if n == "hf_onnx":
1098
+ # Special-case: ONNX causal adapter is core but requires Optimum/ONNXRuntime
1099
+ if n == "hf_causal_onnx":
1102
1100
  backend = backend or "onnxruntime"
1103
1101
  present = (
1104
1102
  importlib.util.find_spec("optimum.onnxruntime") is not None
@@ -1322,7 +1320,7 @@ def doctor_command(
1322
1320
  if "optimum" in str(e).lower():
1323
1321
  if not json_out:
1324
1322
  console.print(
1325
- " [yellow]⚠️ Optional Optimum/ONNXRuntime missing; hf_onnx will be shown as needs_extra[/yellow]"
1323
+ " [yellow]⚠️ Optional Optimum/ONNXRuntime missing; hf_causal_onnx will be shown as needs_extra[/yellow]"
1326
1324
  )
1327
1325
  # Do not mark overall health as failed for optional extras
1328
1326
  else:
@@ -201,9 +201,9 @@ def plugins_command(
201
201
  entry = info.get("entry_point")
202
202
  # Classify support level independent of origin
203
203
  if module.startswith("invarlock.adapters"):
204
- if n in {"hf_causal_auto", "hf_mlm_auto"}:
204
+ if n in {"hf_auto"}:
205
205
  support = "auto"
206
- elif n in {"hf_onnx"}:
206
+ elif n in {"hf_causal_onnx"}:
207
207
  # ONNX relies on optional extras (optimum + onnxruntime)
208
208
  support = "optional"
209
209
  else:
@@ -236,7 +236,7 @@ def plugins_command(
236
236
  if backend_name in {"auto-gptq", "autoawq"} and not is_linux:
237
237
  status = "unsupported"
238
238
  enable = "Linux-only"
239
- # Extras completeness for certain adapters (e.g., hf_onnx needs optimum + onnxruntime)
239
+ # Extras completeness for certain adapters (e.g., hf_causal_onnx needs optimum + onnxruntime)
240
240
  try:
241
241
  extras_status = _check_plugin_extras(n, "adapters")
242
242
  except Exception:
@@ -883,10 +883,14 @@ def _check_plugin_extras(plugin_name: str, plugin_type: str) -> str:
883
883
  "variance": {"packages": [], "extra": ""},
884
884
  "rmt": {"packages": [], "extra": ""},
885
885
  # Adapter plugins (baked-in only)
886
- "hf_gpt2": {"packages": ["transformers"], "extra": "invarlock[adapters]"},
887
- "hf_bert": {"packages": ["transformers"], "extra": "invarlock[adapters]"},
888
- "hf_llama": {"packages": ["transformers"], "extra": "invarlock[adapters]"},
889
- "hf_onnx": {"packages": ["optimum", "onnxruntime"], "extra": "invarlock[onnx]"},
886
+ "hf_causal": {"packages": ["transformers"], "extra": "invarlock[adapters]"},
887
+ "hf_mlm": {"packages": ["transformers"], "extra": "invarlock[adapters]"},
888
+ "hf_seq2seq": {"packages": ["transformers"], "extra": "invarlock[adapters]"},
889
+ "hf_auto": {"packages": ["transformers"], "extra": "invarlock[adapters]"},
890
+ "hf_causal_onnx": {
891
+ "packages": ["optimum", "onnxruntime"],
892
+ "extra": "invarlock[onnx]",
893
+ },
890
894
  # Optional adapter plugins
891
895
  "hf_gptq": {"packages": ["auto_gptq"], "extra": "invarlock[gptq]"},
892
896
  "hf_awq": {"packages": ["autoawq"], "extra": "invarlock[awq]"},
@@ -971,7 +975,7 @@ def _resolve_uninstall_targets(target: str) -> list[str]:
971
975
  "bitsandbytes": ["bitsandbytes"],
972
976
  # ONNX/Optimum family
973
977
  "onnx": ["onnxruntime"],
974
- "hf_onnx": ["onnxruntime"],
978
+ "hf_causal_onnx": ["onnxruntime"],
975
979
  "optimum": ["optimum"],
976
980
  }
977
981
  return mapping.get(name, [])
@@ -1010,7 +1014,7 @@ def _resolve_install_targets(target: str) -> list[str]:
1010
1014
  "transformers": ["invarlock[adapters]"],
1011
1015
  # ONNX/Optimum
1012
1016
  "onnx": ["invarlock[onnx]"],
1013
- "hf_onnx": ["invarlock[onnx]"],
1017
+ "hf_causal_onnx": ["invarlock[onnx]"],
1014
1018
  "optimum": ["invarlock[onnx]"],
1015
1019
  # Direct packages passthrough
1016
1020
  "bitsandbytes": ["bitsandbytes"],
@@ -8,16 +8,84 @@ Provides the `invarlock report` group with:
8
8
  """
9
9
 
10
10
  import json
11
+ import math
11
12
  from pathlib import Path
13
+ from typing import Any
12
14
 
13
15
  import typer
14
16
  from rich.console import Console
15
17
 
18
+ from invarlock.cli.output import print_event, resolve_output_style
16
19
  from invarlock.reporting import certificate as certificate_lib
17
20
  from invarlock.reporting import report as report_lib
18
21
 
19
22
  console = Console()
20
23
 
24
+ SECTION_WIDTH = 67
25
+ KV_LABEL_WIDTH = 16
26
+ GATE_LABEL_WIDTH = 32
27
+ ARTIFACT_LABEL_WIDTH = 18
28
+
29
+
30
+ def _print_section_header(console: Console, title: str) -> None:
31
+ bar = "═" * SECTION_WIDTH
32
+ console.print(bar)
33
+ console.print(title)
34
+ console.print(bar)
35
+
36
+
37
+ def _format_kv_line(label: str, value: str, *, width: int = KV_LABEL_WIDTH) -> str:
38
+ return f" {label:<{width}}: {value}"
39
+
40
+
41
+ def _format_status(ok: bool) -> str:
42
+ return "PASS" if ok else "FAIL"
43
+
44
+
45
+ def _fmt_metric_value(value: Any) -> str:
46
+ try:
47
+ val = float(value)
48
+ except (TypeError, ValueError):
49
+ return "N/A"
50
+ if not math.isfinite(val):
51
+ return "N/A"
52
+ return f"{val:.3f}"
53
+
54
+
55
+ def _fmt_ci_range(ci: Any) -> str:
56
+ if isinstance(ci, (list, tuple)) and len(ci) == 2:
57
+ try:
58
+ lo = float(ci[0])
59
+ hi = float(ci[1])
60
+ except (TypeError, ValueError):
61
+ return "N/A"
62
+ if math.isfinite(lo) and math.isfinite(hi):
63
+ return f"{lo:.3f}–{hi:.3f}"
64
+ return "N/A"
65
+
66
+
67
+ def _artifact_entries(
68
+ saved_files: dict[str, str], output_dir: str
69
+ ) -> list[tuple[str, str]]:
70
+ order = [
71
+ ("cert", "Certificate (JSON)"),
72
+ ("cert_md", "Certificate (MD)"),
73
+ ("json", "JSON"),
74
+ ("markdown", "Markdown"),
75
+ ("html", "HTML"),
76
+ ]
77
+ entries: list[tuple[str, str]] = [("Output", output_dir)]
78
+ used: set[str] = set()
79
+ for key, label in order:
80
+ if key in saved_files:
81
+ entries.append((label, str(saved_files[key])))
82
+ used.add(key)
83
+ for key in sorted(saved_files.keys()):
84
+ if key in used:
85
+ continue
86
+ entries.append((key.upper(), str(saved_files[key])))
87
+ return entries
88
+
21
89
 
22
90
  # Group with callback so `invarlock report` still generates reports
23
91
  report_app = typer.Typer(
@@ -33,6 +101,8 @@ def _generate_reports(
33
101
  compare: str | None = None,
34
102
  baseline: str | None = None,
35
103
  output: str | None = None,
104
+ style: str = "audit",
105
+ no_color: bool = False,
36
106
  ) -> None:
37
107
  # This callback runs only when invoked without subcommand (default Click behavior)
38
108
  try:
@@ -55,21 +125,34 @@ def _generate_reports(
55
125
  compare = _coerce_option(compare)
56
126
  baseline = _coerce_option(baseline)
57
127
  output = _coerce_option(output)
128
+ style = _coerce_option(style, "audit")
129
+ no_color = bool(_coerce_option(no_color, False))
130
+
131
+ output_style = resolve_output_style(
132
+ style=str(style),
133
+ profile="ci",
134
+ progress=False,
135
+ timing=False,
136
+ no_color=no_color,
137
+ )
138
+
139
+ def _event(tag: str, message: str, *, emoji: str | None = None) -> None:
140
+ print_event(console, tag, message, style=output_style, emoji=emoji)
58
141
 
59
142
  # Load primary report
60
- console.print(f"📊 Loading run report: {run}")
143
+ _event("DATA", f"Loading run report: {run}", emoji="📊")
61
144
  primary_report = _load_run_report(run)
62
145
 
63
146
  # Load comparison report if specified
64
147
  compare_report = None
65
148
  if compare:
66
- console.print(f"📊 Loading comparison report: {compare}")
149
+ _event("DATA", f"Loading comparison report: {compare}", emoji="📊")
67
150
  compare_report = _load_run_report(compare)
68
151
 
69
152
  # Load baseline report if specified
70
153
  baseline_report = None
71
154
  if baseline:
72
- console.print(f"📊 Loading baseline report: {baseline}")
155
+ _event("DATA", f"Loading baseline report: {baseline}", emoji="📊")
73
156
  baseline_report = _load_run_report(baseline)
74
157
 
75
158
  # Determine output directory
@@ -88,17 +171,20 @@ def _generate_reports(
88
171
  # Validate certificate requirements
89
172
  if "cert" in formats:
90
173
  if baseline_report is None:
91
- console.print(
92
- "[red]❌ Certificate format requires --baseline parameter[/red]"
93
- )
94
- console.print(
95
- "Use: invarlock report --run <run_dir> --format cert --baseline <baseline_run_dir>"
174
+ _event("FAIL", "Certificate format requires --baseline", emoji="❌")
175
+ _event(
176
+ "INFO",
177
+ "Use: invarlock report --run <run_dir> --format cert --baseline <baseline_run_dir>",
96
178
  )
97
179
  raise typer.Exit(1)
98
- console.print("📜 Generating safety certificate with baseline comparison")
180
+ _event(
181
+ "EXEC",
182
+ "Generating evaluation certificate with baseline comparison",
183
+ emoji="📜",
184
+ )
99
185
 
100
186
  # Generate reports
101
- console.print(f"📝 Generating reports in formats: {formats}")
187
+ _event("EXEC", f"Generating reports in formats: {formats}", emoji="📝")
102
188
  saved_files = report_lib.save_report(
103
189
  primary_report,
104
190
  output_dir,
@@ -109,40 +195,8 @@ def _generate_reports(
109
195
  )
110
196
 
111
197
  # Show results
112
- console.print("[green]✅ Reports generated successfully![/green]")
113
- console.print(f"📁 Output directory: {output_dir}")
114
-
115
- for fmt, file_path in saved_files.items():
116
- if fmt == "cert":
117
- console.print(f" 📜 CERTIFICATE (JSON): {file_path}")
118
- elif fmt == "cert_md":
119
- console.print(f" 📜 CERTIFICATE (MD): {file_path}")
120
- else:
121
- console.print(f" 📄 {fmt.upper()}: {file_path}")
122
-
123
- # Show key metrics (PM-first). Avoid PPL-first wording.
124
- console.print("\n📈 Key Metrics:")
125
- console.print(f" Model: {primary_report['meta']['model_id']}")
126
- console.print(f" Edit: {primary_report['edit']['name']}")
127
- pm = (primary_report.get("metrics", {}) or {}).get("primary_metric", {})
128
- if isinstance(pm, dict) and pm:
129
- kind = str(pm.get("kind") or "primary")
130
- console.print(f" Primary Metric: {kind}")
131
- final = pm.get("final")
132
- if isinstance(final, int | float):
133
- console.print(f" point (final): {final:.3f}")
134
- dci = pm.get("display_ci")
135
- if isinstance(dci, tuple | list) and len(dci) == 2:
136
- try:
137
- lo, hi = float(dci[0]), float(dci[1])
138
- console.print(f" CI: {lo:.3f}–{hi:.3f}")
139
- except Exception:
140
- pass
141
- ratio = pm.get("ratio_vs_baseline")
142
- if isinstance(ratio, int | float):
143
- console.print(f" ratio vs baseline: {ratio:.3f}")
144
-
145
- # Show certificate validation if generated
198
+ _event("PASS", "Reports generated successfully.", emoji="✅")
199
+
146
200
  if "cert" in formats and baseline_report:
147
201
  try:
148
202
  certificate = certificate_lib.make_certificate(
@@ -155,36 +209,105 @@ def _generate_reports(
155
209
 
156
210
  block = _console_block(certificate)
157
211
  overall_pass = bool(block.get("overall_pass"))
212
+ status_text = _format_status(overall_pass)
158
213
 
159
- console.print("\n📜 Certificate Validation:")
160
- status_emoji = "✅" if overall_pass else "❌"
161
- console.print(
162
- f" Overall Status: {status_emoji} {'PASS' if overall_pass else 'FAIL'}"
163
- )
214
+ console.print("")
215
+ _print_section_header(console, "CERTIFICATE SUMMARY")
216
+ console.print(_format_kv_line("Status", status_text))
164
217
 
218
+ schema_version = certificate.get("schema_version")
219
+ if schema_version:
220
+ console.print(
221
+ _format_kv_line("Schema Version", str(schema_version))
222
+ )
223
+
224
+ run_id = certificate.get("run_id") or (
225
+ (primary_report.get("meta", {}) or {}).get("run_id")
226
+ )
227
+ if run_id:
228
+ console.print(_format_kv_line("Run ID", str(run_id)))
229
+
230
+ model_id = (primary_report.get("meta", {}) or {}).get("model_id")
231
+ edit_name = (primary_report.get("edit", {}) or {}).get("name")
232
+ if model_id:
233
+ console.print(_format_kv_line("Model", str(model_id)))
234
+ if edit_name:
235
+ console.print(_format_kv_line("Edit", str(edit_name)))
236
+
237
+ pm = (primary_report.get("metrics", {}) or {}).get("primary_metric", {})
238
+ console.print(" PRIMARY METRIC")
239
+ pm_entries: list[tuple[str, str]] = []
240
+ if isinstance(pm, dict) and pm:
241
+ kind = str(pm.get("kind") or "primary")
242
+ pm_entries.append(("Kind", kind))
243
+ preview = pm.get("preview")
244
+ if preview is not None:
245
+ pm_entries.append(("Preview", _fmt_metric_value(preview)))
246
+ final = pm.get("final")
247
+ if final is not None:
248
+ pm_entries.append(("Final", _fmt_metric_value(final)))
249
+ ratio = pm.get("ratio_vs_baseline")
250
+ if ratio is not None:
251
+ pm_entries.append(("Ratio", _fmt_metric_value(ratio)))
252
+ dci = pm.get("display_ci")
253
+ if dci is not None:
254
+ pm_entries.append(("CI", _fmt_ci_range(dci)))
255
+ if not pm_entries:
256
+ pm_entries.append(("Status", "Unavailable"))
257
+ for idx, (label, value) in enumerate(pm_entries):
258
+ branch = "└─" if idx == len(pm_entries) - 1 else "├─"
259
+ console.print(f" {branch} {label:<14} {value}")
260
+
261
+ console.print(" VALIDATION GATES")
165
262
  rows = block.get("rows", [])
166
263
  if isinstance(rows, list) and rows:
167
- for row in rows:
168
- try:
169
- label = row.get("label")
170
- status = row.get("status")
171
- if label and status:
172
- console.print(f" {label}: {status}")
173
- except Exception:
174
- continue
264
+ for idx, row in enumerate(rows):
265
+ label = str(row.get("label") or "Unknown")
266
+ ok = bool(row.get("ok"))
267
+ status = _format_status(ok)
268
+ mark = "✓" if ok else "✗"
269
+ branch = "└─" if idx == len(rows) - 1 else "├─"
270
+ console.print(
271
+ f" {branch} {label:<{GATE_LABEL_WIDTH}} {mark} {status}"
272
+ )
273
+ else:
274
+ console.print(f" └─ {'No validation rows':<{GATE_LABEL_WIDTH}} -")
275
+
276
+ console.print(" ARTIFACTS")
277
+ entries = _artifact_entries(saved_files, str(output_dir))
278
+ for idx, (label, value) in enumerate(entries):
279
+ branch = "└─" if idx == len(entries) - 1 else "├─"
280
+ console.print(f" {branch} {label:<{ARTIFACT_LABEL_WIDTH}} {value}")
281
+ console.print("═" * SECTION_WIDTH)
175
282
 
176
283
  # In CLI report flow, do not hard-exit on validation failure; just display status.
177
284
  # CI gating should be handled by dedicated verify commands.
178
285
 
179
286
  except Exception as e:
180
- console.print(
181
- f" [yellow]⚠️ Certificate validation error: {e}[/yellow]"
182
- )
287
+ _event("WARN", f"Certificate validation error: {e}", emoji="⚠️")
183
288
  # Exit non-zero on certificate generation error
184
289
  raise typer.Exit(1) from e
290
+ else:
291
+ console.print(_format_kv_line("Output", str(output_dir)))
292
+ for label, value in _artifact_entries(saved_files, str(output_dir))[1:]:
293
+ console.print(
294
+ _format_kv_line(label, str(value), width=ARTIFACT_LABEL_WIDTH)
295
+ )
185
296
 
186
297
  except Exception as e:
187
- console.print(f"[red]❌ Report generation failed: {e}[/red]")
298
+ print_event(
299
+ console,
300
+ "FAIL",
301
+ f"Report generation failed: {e}",
302
+ style=resolve_output_style(
303
+ style="audit",
304
+ profile="ci",
305
+ progress=False,
306
+ timing=False,
307
+ no_color=False,
308
+ ),
309
+ emoji="❌",
310
+ )
188
311
  raise typer.Exit(1) from e
189
312
 
190
313
 
@@ -206,15 +329,37 @@ def report_callback(
206
329
  help="Path to baseline run for certificate generation (required for cert format)",
207
330
  ),
208
331
  output: str | None = typer.Option(None, "--output", "-o", help="Output directory"),
332
+ style: str = typer.Option("audit", "--style", help="Output style (audit|friendly)"),
333
+ no_color: bool = typer.Option(
334
+ False, "--no-color", help="Disable ANSI colors (respects NO_COLOR=1)"
335
+ ),
209
336
  ):
210
337
  """Generate a report from a run (default callback)."""
211
338
  if getattr(ctx, "resilient_parsing", False) or ctx.invoked_subcommand is not None:
212
339
  return
213
340
  if not run:
214
- console.print("[red]❌ --run is required when no subcommand is provided[/red]")
341
+ print_event(
342
+ console,
343
+ "FAIL",
344
+ "--run is required when no subcommand is provided",
345
+ style=resolve_output_style(
346
+ style=str(style),
347
+ profile="ci",
348
+ progress=False,
349
+ timing=False,
350
+ no_color=no_color,
351
+ ),
352
+ emoji="❌",
353
+ )
215
354
  raise typer.Exit(2)
216
355
  return _generate_reports(
217
- run=run, format=format, compare=compare, baseline=baseline, output=output
356
+ run=run,
357
+ format=format,
358
+ compare=compare,
359
+ baseline=baseline,
360
+ output=output,
361
+ style=style,
362
+ no_color=no_color,
218
363
  )
219
364
 
220
365
 
@@ -225,9 +370,17 @@ def report_command(
225
370
  compare: str | None = None,
226
371
  baseline: str | None = None,
227
372
  output: str | None = None,
373
+ style: str = "audit",
374
+ no_color: bool = False,
228
375
  ):
229
376
  return _generate_reports(
230
- run=run, format=format, compare=compare, baseline=baseline, output=output
377
+ run=run,
378
+ format=format,
379
+ compare=compare,
380
+ baseline=baseline,
381
+ output=output,
382
+ style=style,
383
+ no_color=no_color,
231
384
  )
232
385
 
233
386
 
@@ -326,11 +479,22 @@ def report_validate(
326
479
  ),
327
480
  ):
328
481
  """Validate a certificate JSON against the current schema (v1)."""
482
+ output_style = resolve_output_style(
483
+ style="audit",
484
+ profile="ci",
485
+ progress=False,
486
+ timing=False,
487
+ no_color=False,
488
+ )
489
+
490
+ def _event(tag: str, message: str, *, emoji: str | None = None) -> None:
491
+ print_event(console, tag, message, style=output_style, emoji=emoji)
492
+
329
493
  p = Path(report)
330
494
  try:
331
495
  payload = json.loads(p.read_text(encoding="utf-8"))
332
496
  except Exception as exc: # noqa: BLE001
333
- console.print(f"[red]❌ Failed to read input JSON: {exc}[/red]")
497
+ _event("FAIL", f"Failed to read input JSON: {exc}", emoji="❌")
334
498
  raise typer.Exit(1) from exc
335
499
 
336
500
  try:
@@ -338,16 +502,16 @@ def report_validate(
338
502
 
339
503
  ok = validate_certificate(payload)
340
504
  if not ok:
341
- console.print("[red]❌ Certificate schema validation failed[/red]")
505
+ _event("FAIL", "Certificate schema validation failed", emoji="❌")
342
506
  raise typer.Exit(2)
343
- console.print(" Certificate schema is valid")
507
+ _event("PASS", "Certificate schema is valid", emoji="✅")
344
508
  except ValueError as exc:
345
- console.print(f"[red]❌ Certificate validation error: {exc}[/red]")
509
+ _event("FAIL", f"Certificate validation error: {exc}", emoji="❌")
346
510
  raise typer.Exit(2) from exc
347
511
  except typer.Exit:
348
512
  raise
349
513
  except Exception as exc: # noqa: BLE001
350
- console.print(f"[red]❌ Validation failed: {exc}[/red]")
514
+ _event("FAIL", f"Validation failed: {exc}", emoji="❌")
351
515
  raise typer.Exit(1) from exc
352
516
 
353
517