invarlock 0.2.0__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 (132) hide show
  1. invarlock/__init__.py +33 -0
  2. invarlock/__main__.py +10 -0
  3. invarlock/_data/runtime/profiles/ci_cpu.yaml +15 -0
  4. invarlock/_data/runtime/profiles/release.yaml +23 -0
  5. invarlock/_data/runtime/tiers.yaml +76 -0
  6. invarlock/adapters/__init__.py +102 -0
  7. invarlock/adapters/_capabilities.py +45 -0
  8. invarlock/adapters/auto.py +99 -0
  9. invarlock/adapters/base.py +530 -0
  10. invarlock/adapters/base_types.py +85 -0
  11. invarlock/adapters/hf_bert.py +852 -0
  12. invarlock/adapters/hf_gpt2.py +403 -0
  13. invarlock/adapters/hf_llama.py +485 -0
  14. invarlock/adapters/hf_mixin.py +383 -0
  15. invarlock/adapters/hf_onnx.py +112 -0
  16. invarlock/adapters/hf_t5.py +137 -0
  17. invarlock/adapters/py.typed +1 -0
  18. invarlock/assurance/__init__.py +43 -0
  19. invarlock/cli/__init__.py +8 -0
  20. invarlock/cli/__main__.py +8 -0
  21. invarlock/cli/_evidence.py +25 -0
  22. invarlock/cli/_json.py +75 -0
  23. invarlock/cli/adapter_auto.py +162 -0
  24. invarlock/cli/app.py +287 -0
  25. invarlock/cli/commands/__init__.py +26 -0
  26. invarlock/cli/commands/certify.py +403 -0
  27. invarlock/cli/commands/doctor.py +1358 -0
  28. invarlock/cli/commands/explain_gates.py +151 -0
  29. invarlock/cli/commands/export_html.py +100 -0
  30. invarlock/cli/commands/plugins.py +1331 -0
  31. invarlock/cli/commands/report.py +354 -0
  32. invarlock/cli/commands/run.py +4146 -0
  33. invarlock/cli/commands/verify.py +1040 -0
  34. invarlock/cli/config.py +396 -0
  35. invarlock/cli/constants.py +68 -0
  36. invarlock/cli/device.py +92 -0
  37. invarlock/cli/doctor_helpers.py +74 -0
  38. invarlock/cli/errors.py +6 -0
  39. invarlock/cli/overhead_utils.py +60 -0
  40. invarlock/cli/provenance.py +66 -0
  41. invarlock/cli/utils.py +41 -0
  42. invarlock/config.py +56 -0
  43. invarlock/core/__init__.py +62 -0
  44. invarlock/core/abi.py +15 -0
  45. invarlock/core/api.py +274 -0
  46. invarlock/core/auto_tuning.py +317 -0
  47. invarlock/core/bootstrap.py +226 -0
  48. invarlock/core/checkpoint.py +221 -0
  49. invarlock/core/contracts.py +73 -0
  50. invarlock/core/error_utils.py +64 -0
  51. invarlock/core/events.py +298 -0
  52. invarlock/core/exceptions.py +95 -0
  53. invarlock/core/registry.py +481 -0
  54. invarlock/core/retry.py +146 -0
  55. invarlock/core/runner.py +2041 -0
  56. invarlock/core/types.py +154 -0
  57. invarlock/edits/__init__.py +12 -0
  58. invarlock/edits/_edit_utils.py +249 -0
  59. invarlock/edits/_external_utils.py +268 -0
  60. invarlock/edits/noop.py +47 -0
  61. invarlock/edits/py.typed +1 -0
  62. invarlock/edits/quant_rtn.py +801 -0
  63. invarlock/edits/registry.py +166 -0
  64. invarlock/eval/__init__.py +23 -0
  65. invarlock/eval/bench.py +1207 -0
  66. invarlock/eval/bootstrap.py +50 -0
  67. invarlock/eval/data.py +2052 -0
  68. invarlock/eval/metrics.py +2167 -0
  69. invarlock/eval/primary_metric.py +767 -0
  70. invarlock/eval/probes/__init__.py +24 -0
  71. invarlock/eval/probes/fft.py +139 -0
  72. invarlock/eval/probes/mi.py +213 -0
  73. invarlock/eval/probes/post_attention.py +323 -0
  74. invarlock/eval/providers/base.py +67 -0
  75. invarlock/eval/providers/seq2seq.py +111 -0
  76. invarlock/eval/providers/text_lm.py +113 -0
  77. invarlock/eval/providers/vision_text.py +93 -0
  78. invarlock/eval/py.typed +1 -0
  79. invarlock/guards/__init__.py +18 -0
  80. invarlock/guards/_contracts.py +9 -0
  81. invarlock/guards/invariants.py +640 -0
  82. invarlock/guards/policies.py +805 -0
  83. invarlock/guards/py.typed +1 -0
  84. invarlock/guards/rmt.py +2097 -0
  85. invarlock/guards/spectral.py +1419 -0
  86. invarlock/guards/tier_config.py +354 -0
  87. invarlock/guards/variance.py +3298 -0
  88. invarlock/guards_ref/__init__.py +15 -0
  89. invarlock/guards_ref/rmt_ref.py +40 -0
  90. invarlock/guards_ref/spectral_ref.py +135 -0
  91. invarlock/guards_ref/variance_ref.py +60 -0
  92. invarlock/model_profile.py +353 -0
  93. invarlock/model_utils.py +221 -0
  94. invarlock/observability/__init__.py +10 -0
  95. invarlock/observability/alerting.py +535 -0
  96. invarlock/observability/core.py +546 -0
  97. invarlock/observability/exporters.py +565 -0
  98. invarlock/observability/health.py +588 -0
  99. invarlock/observability/metrics.py +457 -0
  100. invarlock/observability/py.typed +1 -0
  101. invarlock/observability/utils.py +553 -0
  102. invarlock/plugins/__init__.py +12 -0
  103. invarlock/plugins/hello_guard.py +33 -0
  104. invarlock/plugins/hf_awq_adapter.py +82 -0
  105. invarlock/plugins/hf_bnb_adapter.py +79 -0
  106. invarlock/plugins/hf_gptq_adapter.py +78 -0
  107. invarlock/plugins/py.typed +1 -0
  108. invarlock/py.typed +1 -0
  109. invarlock/reporting/__init__.py +7 -0
  110. invarlock/reporting/certificate.py +3221 -0
  111. invarlock/reporting/certificate_schema.py +244 -0
  112. invarlock/reporting/dataset_hashing.py +215 -0
  113. invarlock/reporting/guards_analysis.py +948 -0
  114. invarlock/reporting/html.py +32 -0
  115. invarlock/reporting/normalizer.py +235 -0
  116. invarlock/reporting/policy_utils.py +517 -0
  117. invarlock/reporting/primary_metric_utils.py +265 -0
  118. invarlock/reporting/render.py +1442 -0
  119. invarlock/reporting/report.py +903 -0
  120. invarlock/reporting/report_types.py +278 -0
  121. invarlock/reporting/utils.py +175 -0
  122. invarlock/reporting/validate.py +631 -0
  123. invarlock/security.py +176 -0
  124. invarlock/sparsity_utils.py +323 -0
  125. invarlock/utils/__init__.py +150 -0
  126. invarlock/utils/digest.py +45 -0
  127. invarlock-0.2.0.dist-info/METADATA +586 -0
  128. invarlock-0.2.0.dist-info/RECORD +132 -0
  129. invarlock-0.2.0.dist-info/WHEEL +5 -0
  130. invarlock-0.2.0.dist-info/entry_points.txt +20 -0
  131. invarlock-0.2.0.dist-info/licenses/LICENSE +201 -0
  132. invarlock-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,8 @@
1
+ """Module entry so you can `python -m invarlock.cli`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .app import main
6
+
7
+ if __name__ == "__main__": # pragma: no cover
8
+ main()
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ def maybe_dump_guard_evidence(target_dir: str | Path, payload: dict[str, Any]) -> None:
10
+ """Dump a small JSON blob of guard decision inputs when INVARLOCK_EVIDENCE_DEBUG=1.
11
+
12
+ Keeps payload tiny; callers should pre-filter arrays and redact large fields.
13
+ """
14
+ if os.getenv("INVARLOCK_EVIDENCE_DEBUG", "0") != "1":
15
+ return
16
+ try:
17
+ path = Path(target_dir)
18
+ path.mkdir(parents=True, exist_ok=True)
19
+ out = path / "guards_evidence.json"
20
+ out.write_text(
21
+ json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8"
22
+ )
23
+ except Exception:
24
+ # Never raise in evidence hook
25
+ pass
invarlock/cli/_json.py ADDED
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import asdict, is_dataclass
5
+ from datetime import UTC, datetime
6
+ from typing import Any
7
+
8
+ import typer
9
+
10
+ try:
11
+ from invarlock.core.exceptions import InvarlockError
12
+ except Exception: # pragma: no cover - optional in minimal environments
13
+ InvarlockError = None # type: ignore[assignment]
14
+
15
+
16
+ def _ts() -> str:
17
+ return datetime.now(UTC).isoformat()
18
+
19
+
20
+ def emit(payload: Any, exit_code: int) -> None:
21
+ """Emit a JSON payload with a stable envelope and exit.
22
+
23
+ - Adds `ts` (UTC ISO) and `component=cli` if absent
24
+ - Accepts dicts or dataclasses
25
+ - Exits with provided code via Typer
26
+ """
27
+ if is_dataclass(payload):
28
+ payload = asdict(payload) # type: ignore[assignment]
29
+ if isinstance(payload, dict):
30
+ payload.setdefault("ts", _ts())
31
+ payload.setdefault("component", "cli")
32
+ typer.echo(json.dumps(payload, sort_keys=True))
33
+ raise typer.Exit(exit_code)
34
+
35
+
36
+ def encode_error(exc: Exception) -> dict[str, Any]:
37
+ """Encode an exception as a structured error object for JSON envelopes.
38
+
39
+ Fields:
40
+ - code: error code if available (InvarlockError), else a generic tag
41
+ - category: exception class name
42
+ - recoverable: bool when available, else False
43
+ - context: attached details when available, else {}
44
+ """
45
+ try:
46
+ category = type(exc).__name__
47
+ except Exception:
48
+ category = "Exception"
49
+
50
+ # Default shape
51
+ out: dict[str, Any] = {
52
+ "code": "E_GENERIC",
53
+ "category": category,
54
+ "recoverable": False,
55
+ "context": {},
56
+ }
57
+
58
+ # InvarlockError dataclass provides code/details/recoverable
59
+ try:
60
+ if InvarlockError is not None and isinstance(exc, InvarlockError):
61
+ out["code"] = getattr(exc, "code", out["code"]) or out["code"]
62
+ out["recoverable"] = bool(getattr(exc, "recoverable", False))
63
+ details = getattr(exc, "details", None)
64
+ if isinstance(details, dict):
65
+ out["context"] = details
66
+ return out
67
+ except Exception:
68
+ # Fall back to generic encoding when anything unexpected happens
69
+ pass
70
+
71
+ # Heuristic: common schema/validation categories → generic schema code
72
+ if category in {"ValidationError", "ConfigError", "DataError"}:
73
+ out["code"] = "E_SCHEMA"
74
+
75
+ return out
@@ -0,0 +1,162 @@
1
+ """
2
+ Auto adapter resolution utilities.
3
+
4
+ These helpers map a model identifier (HF directory or Hub ID) to a
5
+ concrete built-in adapter name (hf_gpt2, hf_llama, hf_bert) without
6
+ adding a hard dependency on Transformers.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+
17
+ def _read_local_hf_config(model_id: str | os.PathLike[str]) -> dict[str, Any] | None:
18
+ """Read config.json from a local HF directory if present."""
19
+ try:
20
+ p = Path(model_id)
21
+ except Exception:
22
+ return None
23
+ cfg_path = p / "config.json"
24
+ if not cfg_path.exists():
25
+ return None
26
+ try:
27
+ with cfg_path.open("r", encoding="utf-8") as fh:
28
+ data = json.load(fh)
29
+ if isinstance(data, dict):
30
+ return data
31
+ except Exception:
32
+ return None
33
+ return None
34
+
35
+
36
+ def _detect_quant_family_from_cfg(cfg: dict[str, Any]) -> str | None:
37
+ """Detect quantization family from a HF config dict.
38
+
39
+ Returns one of: 'hf_gptq', 'hf_awq', 'hf_bnb' or None if not detected.
40
+ """
41
+ try:
42
+ q = cfg.get("quantization_config") or {}
43
+ if isinstance(q, dict):
44
+ method = str(q.get("quant_method", q.get("quant_method_full", ""))).lower()
45
+ if any(tok in method for tok in ("gptq",)):
46
+ return "hf_gptq"
47
+ if any(tok in method for tok in ("awq",)):
48
+ return "hf_awq"
49
+ # BitsAndBytes style
50
+ if any(
51
+ str(q.get(k, "")).lower() in {"true", "1"}
52
+ for k in ("load_in_4bit", "load_in_8bit")
53
+ ) or any("bitsandbytes" in str(v).lower() for v in q.values()):
54
+ return "hf_bnb"
55
+ except Exception:
56
+ return None
57
+ return None
58
+
59
+
60
+ def resolve_auto_adapter(
61
+ model_id: str | os.PathLike[str], default: str = "hf_gpt2"
62
+ ) -> str:
63
+ """Resolve an appropriate built-in adapter name for a model.
64
+
65
+ Heuristics:
66
+ - Prefer local config.json (no network). Inspect `model_type` and
67
+ `architectures` to classify LLaMA/Mistral vs BERT vs GPT-like.
68
+ - Fallback to simple name heuristics on the model_id string.
69
+ - Default to `hf_gpt2` when unsure.
70
+ """
71
+ cfg = _read_local_hf_config(model_id)
72
+ model_id_str = str(model_id)
73
+
74
+ def _from_cfg(c: dict[str, Any]) -> str | None:
75
+ # Prefer explicit quantization families first
76
+ fam = _detect_quant_family_from_cfg(c)
77
+ if fam:
78
+ return fam
79
+ mt = str(c.get("model_type", "")).lower()
80
+ archs = [str(a) for a in c.get("architectures", []) if isinstance(a, str)]
81
+ arch_blob = " ".join(archs)
82
+ if (
83
+ mt in {"llama", "mistral", "qwen", "yi"}
84
+ or "Llama" in arch_blob
85
+ or "Mistral" in arch_blob
86
+ ):
87
+ return "hf_llama"
88
+ # Treat masked-LM families as BERT-like
89
+ if (
90
+ mt in {"bert", "roberta", "distilbert", "albert", "deberta", "deberta-v2"}
91
+ or "MaskedLM" in arch_blob
92
+ ):
93
+ return "hf_bert"
94
+ # Generic causal LM
95
+ if "CausalLM" in arch_blob or mt in {
96
+ "gpt2",
97
+ "gpt_neox",
98
+ "opt",
99
+ "gptj",
100
+ "gptj8bit",
101
+ }:
102
+ return "hf_gpt2"
103
+ return None
104
+
105
+ # If local directory contains ONNX model files, prefer hf_onnx
106
+ try:
107
+ p = Path(model_id)
108
+ if p.exists() and p.is_dir():
109
+ # Common Optimum export names
110
+ onnx_files = [
111
+ "model.onnx",
112
+ "decoder_model.onnx",
113
+ "decoder_with_past_model.onnx",
114
+ "encoder_model.onnx",
115
+ ]
116
+ if any((p / fname).exists() for fname in onnx_files):
117
+ return "hf_onnx"
118
+ except Exception:
119
+ pass
120
+
121
+ if isinstance(cfg, dict):
122
+ resolved = _from_cfg(cfg)
123
+ if resolved:
124
+ return resolved
125
+
126
+ # String heuristics as last resort
127
+ lower_id = model_id_str.lower()
128
+ # Quantized repo heuristics
129
+ if any(k in lower_id for k in ["gptq", "-gptq", "_gptq"]):
130
+ return "hf_gptq"
131
+ if any(k in lower_id for k in ["awq", "-awq", "_awq"]):
132
+ return "hf_awq"
133
+ if any(
134
+ k in lower_id for k in ["bnb", "bitsandbytes", "-4bit", "-8bit", "4bit", "8bit"]
135
+ ):
136
+ return "hf_bnb"
137
+ if any(k in lower_id for k in ["llama", "mistral", "qwen", "yi"]):
138
+ return "hf_llama"
139
+ if any(k in lower_id for k in ["bert", "roberta", "albert", "deberta"]):
140
+ return "hf_bert"
141
+ return default
142
+
143
+
144
+ def apply_auto_adapter_if_needed(cfg: Any) -> Any:
145
+ """Mutate/clone a InvarLockConfig to resolve adapter:auto → concrete adapter.
146
+
147
+ Returns the same config object if no change is needed.
148
+ """
149
+ try:
150
+ adapter = str(getattr(cfg.model, "adapter", ""))
151
+ if adapter.strip().lower() not in {"auto", "hf_auto", "auto_hf"}:
152
+ return cfg
153
+ model_id = str(getattr(cfg.model, "id", ""))
154
+ resolved = resolve_auto_adapter(model_id)
155
+ data = cfg.model_dump()
156
+ data.setdefault("model", {})["adapter"] = resolved
157
+ return cfg.__class__(data) # re-wrap as InvarLockConfig
158
+ except Exception:
159
+ return cfg
160
+
161
+
162
+ __all__ = ["resolve_auto_adapter", "apply_auto_adapter_if_needed"]
invarlock/cli/app.py ADDED
@@ -0,0 +1,287 @@
1
+ """
2
+ InvarLock CLI Main Entry Point (unified namespace)
3
+ =============================================
4
+
5
+ Modern CLI with clean command interface using modular command structure.
6
+
7
+ Import guard: set `INVARLOCK_LIGHT_IMPORT=1` to avoid heavy plugin discovery and
8
+ third‑party imports during docs/tests. This keeps `import invarlock.cli.app` safe in
9
+ minimal environments.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+
16
+ import typer
17
+ from rich.console import Console
18
+ from typer.core import TyperGroup
19
+
20
+ from invarlock.security import enforce_default_security
21
+
22
+ # Lightweight import mode disables heavy side effects in some modules, but we no
23
+ # longer force plugin discovery off globally here; individual commands may gate
24
+ # discovery based on their own flags.
25
+ LIGHT_IMPORT = os.getenv("INVARLOCK_LIGHT_IMPORT", "").strip().lower() in {
26
+ "1",
27
+ "true",
28
+ "yes",
29
+ }
30
+
31
+
32
+ # Deterministic help ordering
33
+ class OrderedGroup(TyperGroup):
34
+ def list_commands(self, ctx): # type: ignore[override]
35
+ return [
36
+ "certify",
37
+ "report",
38
+ "verify",
39
+ "run",
40
+ "plugins",
41
+ "doctor",
42
+ "version",
43
+ ]
44
+
45
+
46
+ # Initialize CLI app
47
+ app = typer.Typer(
48
+ name="invarlock",
49
+ help=(
50
+ "InvarLock — certify model changes with deterministic pairing and safety gates.\n"
51
+ "Quick path: invarlock certify --baseline <MODEL> --subject <MODEL>\n"
52
+ "Hint: use --edit-config to run the built-in quant_rtn demo.\n"
53
+ "Tip: enable downloads with INVARLOCK_ALLOW_NETWORK=1 when fetching.\n"
54
+ "Exit codes:\n"
55
+ " 0=success\n"
56
+ " 1=generic failure\n"
57
+ " 2=schema invalid\n"
58
+ " 3=hard abort ([INVARLOCK:EXXX])."
59
+ ),
60
+ no_args_is_help=True,
61
+ cls=OrderedGroup,
62
+ )
63
+
64
+ console = Console()
65
+
66
+
67
+ @app.command()
68
+ def version():
69
+ """Show InvarLock version."""
70
+ # Prefer package metadata when available so CLI reflects wheel truth
71
+ try:
72
+ from importlib.metadata import version as _pkg_version
73
+
74
+ schema = None
75
+ try:
76
+ from invarlock.reporting.certificate import (
77
+ CERTIFICATE_SCHEMA_VERSION as _SCHEMA,
78
+ )
79
+
80
+ schema = _SCHEMA
81
+ except Exception:
82
+ schema = None
83
+ msg = f"InvarLock {_pkg_version('invarlock')}"
84
+ if schema:
85
+ msg += f" · schema={schema}"
86
+ console.print(msg)
87
+ return
88
+ except Exception:
89
+ pass
90
+ try:
91
+ from invarlock import __version__
92
+
93
+ console.print(f"InvarLock {__version__}")
94
+ except Exception:
95
+ console.print("InvarLock version unknown")
96
+
97
+
98
+ """Register command modules and groups in the desired help order.
99
+
100
+ Order: certify → report → run → plugins → doctor → version
101
+ """
102
+
103
+
104
+ @app.command(
105
+ name="certify",
106
+ help=(
107
+ "Certify a subject model against a baseline and generate a safety certificate. "
108
+ "Use when you have two model snapshots and want pass/fail gating."
109
+ ),
110
+ )
111
+ def _certify_lazy(
112
+ source: str = typer.Option(
113
+ ..., "--source", "--baseline", help="Baseline model dir or Hub ID"
114
+ ),
115
+ edited: str = typer.Option(
116
+ ..., "--edited", "--subject", help="Subject model dir or Hub ID"
117
+ ),
118
+ adapter: str = typer.Option(
119
+ "auto", "--adapter", help="Adapter name or 'auto' to resolve"
120
+ ),
121
+ device: str | None = typer.Option(
122
+ None, "--device", help="Device override for runs (auto|cuda|mps|cpu)"
123
+ ),
124
+ profile: str = typer.Option("ci", "--profile", help="Profile (ci|release)"),
125
+ tier: str = typer.Option("balanced", "--tier", help="Tier label for context"),
126
+ preset: str | None = typer.Option(
127
+ None,
128
+ "--preset",
129
+ help=(
130
+ "Universal preset path to use (defaults to causal or masked preset "
131
+ "based on adapter)"
132
+ ),
133
+ ),
134
+ out: str = typer.Option("runs", "--out", help="Base output directory"),
135
+ cert_out: str = typer.Option(
136
+ "reports/cert", "--cert-out", help="Certificate output directory"
137
+ ),
138
+ edit_config: str | None = typer.Option(
139
+ None, "--edit-config", help="Edit preset to apply a demo edit (quant_rtn)"
140
+ ),
141
+ ):
142
+ from .commands.certify import certify_command as _cert
143
+
144
+ return _cert(
145
+ source=source,
146
+ edited=edited,
147
+ adapter=adapter,
148
+ device=device,
149
+ profile=profile,
150
+ tier=tier,
151
+ preset=preset,
152
+ out=out,
153
+ cert_out=cert_out,
154
+ edit_config=edit_config,
155
+ )
156
+
157
+
158
+ def _register_subapps() -> None:
159
+ # Import sub-apps lazily to keep module import light and satisfy E402
160
+ from .commands.doctor import doctor_command as _doctor_cmd
161
+ from .commands.plugins import plugins_app as _plugins_app
162
+ from .commands.report import report_app as _report_app
163
+
164
+ app.add_typer(_report_app, name="report")
165
+ app.add_typer(_plugins_app, name="plugins")
166
+ app.command(name="doctor")(_doctor_cmd)
167
+
168
+
169
+ @app.command(
170
+ name="verify",
171
+ help=(
172
+ "Verify certificate JSON(s) against schema, pairing math, and gates. "
173
+ "Use --json for a single-line machine-readable envelope."
174
+ ),
175
+ )
176
+ def _verify_typed(
177
+ certificates: list[str] = typer.Argument(
178
+ ..., help="One or more certificate JSON files to verify."
179
+ ),
180
+ baseline: str | None = typer.Option(
181
+ None,
182
+ "--baseline",
183
+ help="Optional baseline certificate/report JSON to enforce provider parity.",
184
+ ),
185
+ tolerance: float = typer.Option(
186
+ 1e-9, "--tolerance", help="Tolerance for analysis-basis comparisons."
187
+ ),
188
+ profile: str | None = typer.Option(
189
+ "dev",
190
+ "--profile",
191
+ help="Execution profile affecting parity enforcement and exit codes (dev|ci|release).",
192
+ ),
193
+ json_out: bool = typer.Option(
194
+ False,
195
+ "--json",
196
+ help="Emit machine-readable JSON (suppresses human-readable output)",
197
+ ),
198
+ ):
199
+ from pathlib import Path as _Path
200
+
201
+ from .commands.verify import verify_command as _verify
202
+
203
+ cert_paths = [_Path(c) for c in certificates]
204
+ baseline_path = _Path(baseline) if isinstance(baseline, str) else None
205
+ return _verify(
206
+ certificates=cert_paths,
207
+ baseline=baseline_path,
208
+ tolerance=tolerance,
209
+ profile=profile,
210
+ json_out=json_out,
211
+ )
212
+
213
+
214
+ @app.command(
215
+ name="run",
216
+ help=(
217
+ "Execute an end-to-end run from a YAML config (edit + guards + reports). "
218
+ "Writes run artifacts and optionally a safety certificate."
219
+ ),
220
+ )
221
+ def _run_typed(
222
+ config: str = typer.Option(
223
+ ..., "--config", "-c", help="Path to YAML configuration file"
224
+ ),
225
+ device: str | None = typer.Option(
226
+ None, "--device", help="Device override (auto|cuda|mps|cpu)"
227
+ ),
228
+ profile: str | None = typer.Option(
229
+ None, "--profile", help="Profile to apply (ci|release)"
230
+ ),
231
+ out: str | None = typer.Option(None, "--out", help="Output directory override"),
232
+ edit: str | None = typer.Option(None, "--edit", help="Edit kind (quant|mixed)"),
233
+ tier: str | None = typer.Option(
234
+ None,
235
+ "--tier",
236
+ help="Auto-tuning tier override (conservative|balanced|aggressive)",
237
+ ),
238
+ probes: int | None = typer.Option(
239
+ None, "--probes", help="Number of micro-probes (0=deterministic, >0=adaptive)"
240
+ ),
241
+ until_pass: bool = typer.Option(
242
+ False, "--until-pass", help="Retry until certificate passes (max 3 attempts)"
243
+ ),
244
+ max_attempts: int = typer.Option(
245
+ 3, "--max-attempts", help="Maximum retry attempts for --until-pass mode"
246
+ ),
247
+ timeout: int | None = typer.Option(
248
+ None, "--timeout", help="Timeout in seconds for --until-pass mode"
249
+ ),
250
+ baseline: str | None = typer.Option(
251
+ None,
252
+ "--baseline",
253
+ help="Path to baseline report.json for certificate validation",
254
+ ),
255
+ no_cleanup: bool = typer.Option(
256
+ False, "--no-cleanup", help="Skip cleanup of temporary artifacts"
257
+ ),
258
+ ):
259
+ from .commands.run import run_command as _run
260
+
261
+ return _run(
262
+ config=config,
263
+ device=device,
264
+ profile=profile,
265
+ out=out,
266
+ edit=edit,
267
+ tier=tier,
268
+ probes=probes,
269
+ until_pass=until_pass,
270
+ max_attempts=max_attempts,
271
+ timeout=timeout,
272
+ baseline=baseline,
273
+ no_cleanup=no_cleanup,
274
+ )
275
+
276
+
277
+ _register_subapps()
278
+
279
+
280
+ def main() -> None:
281
+ """Main entry point for the InvarLock CLI."""
282
+ enforce_default_security()
283
+ app()
284
+
285
+
286
+ if __name__ == "__main__":
287
+ main()
@@ -0,0 +1,26 @@
1
+ """CLI command package.
2
+
3
+ Lightweight namespace re-exports for programmatic access in tests and tooling.
4
+ Import-time work is minimal; subcommands themselves may perform heavier imports
5
+ only when invoked.
6
+ """
7
+
8
+ from .certify import certify_command
9
+ from .doctor import doctor_command
10
+ from .explain_gates import explain_gates_command
11
+ from .export_html import export_html_command
12
+ from .plugins import plugins_command
13
+ from .report import report_command
14
+ from .run import run_command
15
+ from .verify import verify_command
16
+
17
+ __all__ = [
18
+ "certify_command",
19
+ "doctor_command",
20
+ "explain_gates_command",
21
+ "export_html_command",
22
+ "plugins_command",
23
+ "run_command",
24
+ "verify_command",
25
+ "report_command",
26
+ ]