jevcheck 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.
jevcheck/__init__.py ADDED
@@ -0,0 +1,57 @@
1
+ """jevcheck — pytest for Jev behavior."""
2
+
3
+ from jevcheck.answers import (
4
+ ChoiceAnswer,
5
+ JevResponse,
6
+ NoulAnswer,
7
+ ScoreAnswer,
8
+ Usage,
9
+ mapped_confidence,
10
+ )
11
+ from jevcheck.client import AUTH_ENV, JevClient
12
+ from jevcheck.compare import compare, record_answers
13
+ from jevcheck.contract import Case, Contract, FieldExpect, load_contract, load_replay, write_replay
14
+ from jevcheck.eval import CaseResult, EvalReport, Outcome, evaluate, evaluate_case
15
+ from jevcheck.gate import Action, Decision, Gate
16
+ from jevcheck.pinning import (
17
+ ModelIdentityError,
18
+ UnpinnedModelError,
19
+ is_unpinned,
20
+ require_pinned,
21
+ require_response_identity,
22
+ )
23
+
24
+ __version__ = "0.2.0"
25
+
26
+ __all__ = [
27
+ "AUTH_ENV",
28
+ "Action",
29
+ "Case",
30
+ "CaseResult",
31
+ "ChoiceAnswer",
32
+ "Contract",
33
+ "Decision",
34
+ "EvalReport",
35
+ "compare",
36
+ "FieldExpect",
37
+ "Gate",
38
+ "JevClient",
39
+ "JevResponse",
40
+ "ModelIdentityError",
41
+ "NoulAnswer",
42
+ "Outcome",
43
+ "ScoreAnswer",
44
+ "UnpinnedModelError",
45
+ "Usage",
46
+ "evaluate",
47
+ "evaluate_case",
48
+ "is_unpinned",
49
+ "load_contract",
50
+ "load_replay",
51
+ "record_answers",
52
+ "write_replay",
53
+ "mapped_confidence",
54
+ "require_pinned",
55
+ "require_response_identity",
56
+ "__version__",
57
+ ]
jevcheck/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from jevcheck.cli import main
2
+
3
+ raise SystemExit(main())
jevcheck/answers.py ADDED
@@ -0,0 +1,228 @@
1
+ """Verified System One answer models. Fields match docs/jev-api.md only."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import Annotated, Any, Literal, Mapping
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr, field_validator
9
+
10
+ from jevcheck.questions import JSONContent
11
+
12
+ # Probability maps must be finite, nonnegative, and sum to 1 within this
13
+ # absolute tolerance. JSON/IEEE floats will not hit 1.0 exactly for many
14
+ # hand-written distributions; 1e-6 is the agreed check, not a fixture-format
15
+ # change. Maps that miss this are rejected rather than silently renormalized.
16
+ PROBABILITY_SUM_TOLERANCE = 1e-6
17
+
18
+
19
+ class Usage(BaseModel):
20
+ model_config = ConfigDict(extra="ignore", frozen=True)
21
+
22
+ input_tokens: StrictInt | None = None
23
+ output_tokens: StrictInt | None = None
24
+
25
+
26
+ class ChoiceAnswer(BaseModel):
27
+ model_config = ConfigDict(extra="ignore", frozen=True)
28
+
29
+ type: Literal["choice"] = "choice"
30
+ choice: StrictStr
31
+ confidence: float = Field(ge=0.0, le=1.0)
32
+ probabilities: dict[str, float]
33
+
34
+ @field_validator("confidence")
35
+ @classmethod
36
+ def _finite_confidence(cls, value: float) -> float:
37
+ return _require_finite(value, "choice confidence")
38
+
39
+ @field_validator("probabilities")
40
+ @classmethod
41
+ def _valid_probabilities(cls, value: dict[str, float]) -> dict[str, float]:
42
+ return validate_probability_map(value, what="choice")
43
+
44
+
45
+ class NoulAnswer(BaseModel):
46
+ model_config = ConfigDict(extra="ignore", frozen=True)
47
+
48
+ type: Literal["noul"] = "noul"
49
+ noul: float = Field(ge=0.0, le=1.0)
50
+
51
+ @field_validator("noul")
52
+ @classmethod
53
+ def _finite_noul(cls, value: float) -> float:
54
+ return _require_finite(value, "noul")
55
+
56
+
57
+ class ScoreAnswer(BaseModel):
58
+ model_config = ConfigDict(extra="ignore", frozen=True)
59
+
60
+ type: Literal["score"] = "score"
61
+ score: float
62
+ confidence: float = Field(ge=0.0, le=1.0)
63
+ legend: dict[str, JSONContent]
64
+ probabilities: dict[str, float]
65
+
66
+ @field_validator("score", "confidence")
67
+ @classmethod
68
+ def _finite_score_fields(cls, value: float) -> float:
69
+ return _require_finite(value, "score field")
70
+
71
+ @field_validator("legend", "probabilities", mode="before")
72
+ @classmethod
73
+ def _stringify_int_keys(cls, value: Any) -> Any:
74
+ """SDK 0.7.0 score maps use integer keys; coerce to str without inventing fields."""
75
+ if isinstance(value, Mapping):
76
+ return {str(key): item for key, item in value.items()}
77
+ return value
78
+
79
+ @field_validator("legend")
80
+ @classmethod
81
+ def _valid_legend(cls, value: dict[str, JSONContent]) -> dict[str, JSONContent]:
82
+ if not value:
83
+ raise ValueError("score legend must be a nonempty mapping")
84
+ return value
85
+
86
+ @field_validator("probabilities")
87
+ @classmethod
88
+ def _valid_probabilities(cls, value: dict[str, float]) -> dict[str, float]:
89
+ return validate_probability_map(value, what="score")
90
+
91
+
92
+ Answer = Annotated[
93
+ NoulAnswer | ChoiceAnswer | ScoreAnswer,
94
+ Field(discriminator="type"),
95
+ ]
96
+
97
+
98
+ class JevResponse(BaseModel):
99
+ model_config = ConfigDict(extra="ignore", frozen=True)
100
+
101
+ model: StrictStr
102
+ usage: Usage = Field(default_factory=Usage)
103
+ answers: dict[str, Answer] = Field(default_factory=dict)
104
+
105
+ @field_validator("model")
106
+ @classmethod
107
+ def _nonempty_model(cls, value: str) -> str:
108
+ if not value.strip():
109
+ raise ValueError("response.model must be a nonempty string")
110
+ return value
111
+
112
+
113
+ def mapped_confidence(answer: NoulAnswer | ChoiceAnswer | ScoreAnswer) -> float:
114
+ """Scalar used for floors, regressions, and the optional Gate helper."""
115
+ if isinstance(answer, NoulAnswer):
116
+ return answer.noul
117
+ return answer.confidence
118
+
119
+
120
+ def adapt_answer(raw: Any) -> NoulAnswer | ChoiceAnswer | ScoreAnswer:
121
+ """Accept SDK models, namespaces, or verified dicts.
122
+
123
+ Missing choice or score probabilities are rejected (not replaced with ``{}``).
124
+ Score ``legend`` is required and nonempty, matching SDK-shaped responses.
125
+ Score ``legend`` / ``probabilities`` integer keys from typesafe-sdk 0.7.0
126
+ are stringified so they match this schema.
127
+ """
128
+ if isinstance(raw, (NoulAnswer, ChoiceAnswer, ScoreAnswer)):
129
+ return raw
130
+ data = _to_mapping(raw)
131
+ kind = data.get("type")
132
+ if kind == "noul" or ("noul" in data and "choice" not in data and "score" not in data):
133
+ return NoulAnswer.model_validate({"type": "noul", "noul": data["noul"]})
134
+ if kind == "choice" or "choice" in data:
135
+ payload: dict[str, Any] = {
136
+ "type": "choice",
137
+ "choice": data["choice"],
138
+ "confidence": data["confidence"],
139
+ }
140
+ if "probabilities" in data:
141
+ payload["probabilities"] = data["probabilities"]
142
+ return ChoiceAnswer.model_validate(payload)
143
+ if kind == "score" or "score" in data:
144
+ payload = {
145
+ "type": "score",
146
+ "score": data["score"],
147
+ "confidence": data["confidence"],
148
+ }
149
+ if "legend" in data:
150
+ payload["legend"] = data["legend"]
151
+ if "probabilities" in data:
152
+ payload["probabilities"] = data["probabilities"]
153
+ return ScoreAnswer.model_validate(payload)
154
+ raise ValueError(f"unsupported answer object: {raw!r}")
155
+
156
+
157
+ def adapt_response(raw: Any) -> JevResponse:
158
+ if isinstance(raw, JevResponse):
159
+ return raw
160
+ data = _to_mapping(raw)
161
+ model = data.get("model", _MISSING)
162
+ if model is _MISSING:
163
+ raise ValueError("response.model is required")
164
+ if model is None:
165
+ raise ValueError("response.model must not be null")
166
+ if not isinstance(model, str):
167
+ raise ValueError(f"response.model must be a string, not {type(model).__name__}")
168
+ answers_raw = data.get("answers") or {}
169
+ if not isinstance(answers_raw, Mapping):
170
+ raise TypeError("response.answers must be a mapping")
171
+ usage_raw = data.get("usage") or {}
172
+ return JevResponse(
173
+ model=model,
174
+ usage=Usage.model_validate(_to_mapping(usage_raw) if usage_raw is not None else {}),
175
+ answers={str(name): adapt_answer(answer) for name, answer in answers_raw.items()},
176
+ )
177
+
178
+
179
+ def validate_probability_map(value: Any, *, what: str) -> dict[str, float]:
180
+ if not isinstance(value, Mapping) or not value:
181
+ raise ValueError(f"{what} probabilities must be a nonempty mapping")
182
+ cleaned: dict[str, float] = {}
183
+ for key, raw in value.items():
184
+ name = str(key)
185
+ try:
186
+ number = float(raw)
187
+ except (TypeError, ValueError) as exc:
188
+ raise ValueError(f"{what} probability {name!r} must be a finite number") from exc
189
+ if not math.isfinite(number):
190
+ raise ValueError(f"{what} probability {name!r} must be finite")
191
+ if number < 0.0:
192
+ raise ValueError(f"{what} probability {name!r} must be >= 0")
193
+ cleaned[name] = number
194
+ total = math.fsum(cleaned.values())
195
+ if abs(total - 1.0) > PROBABILITY_SUM_TOLERANCE:
196
+ raise ValueError(
197
+ f"{what} probabilities must sum to 1 ± {PROBABILITY_SUM_TOLERANCE}; got {total}"
198
+ )
199
+ return cleaned
200
+
201
+
202
+ def _require_finite(value: float, what: str) -> float:
203
+ if not math.isfinite(value):
204
+ raise ValueError(f"{what} must be a finite number")
205
+ return value
206
+
207
+
208
+ def _to_mapping(raw: Any) -> dict[str, Any]:
209
+ if isinstance(raw, Mapping):
210
+ return dict(raw)
211
+ if hasattr(raw, "model_dump"):
212
+ dumped = raw.model_dump()
213
+ if isinstance(dumped, Mapping):
214
+ return dict(dumped)
215
+ skip = {"model_config", "model_fields", "model_computed_fields"}
216
+ if hasattr(raw, "__dict__"):
217
+ return {k: v for k, v in vars(raw).items() if not k.startswith("_") and k not in skip}
218
+ public = {
219
+ name: getattr(raw, name)
220
+ for name in dir(raw)
221
+ if not name.startswith("_") and not callable(getattr(raw, name, None))
222
+ }
223
+ if public:
224
+ return public
225
+ raise TypeError(f"cannot adapt {type(raw).__name__} to a mapping")
226
+
227
+
228
+ _MISSING = object()
jevcheck/cli.py ADDED
@@ -0,0 +1,356 @@
1
+ """jevcheck CLI — eval a candidate model against a pinned contract."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import re
8
+ import sys
9
+ from collections.abc import Sequence
10
+ from pathlib import Path
11
+
12
+ from typesafe_sdk import (
13
+ TypeSafeAPIError,
14
+ TypeSafeAPIResponseValidationError,
15
+ TypeSafeAuthenticationError,
16
+ TypeSafeError,
17
+ TypeSafeInternalServerError,
18
+ TypeSafeRateLimitError,
19
+ )
20
+
21
+ from jevcheck.answers import JevResponse
22
+ from jevcheck.client import AUTH_ENV, JevClient
23
+ from jevcheck.compare import compare, record_answers
24
+ from jevcheck.contract import Case, Contract, load_contract, load_replay, write_replay
25
+ from jevcheck.eval import Fetch, evaluate
26
+ from jevcheck.pinning import (
27
+ ModelIdentityError,
28
+ UnpinnedModelError,
29
+ format_resolved_model,
30
+ require_pinned,
31
+ )
32
+
33
+ # Exit 0: compatible. Exit 1: behavioral contract failure.
34
+ # Exit 2: usage / input / identity. Exit 3: operational (HTTP / invalid API body).
35
+ EXIT_OK = 0
36
+ EXIT_BREAKING = 1
37
+ EXIT_USAGE = 2
38
+ EXIT_OPS = 3
39
+
40
+
41
+ def main(argv: Sequence[str] | None = None) -> int:
42
+ parser = argparse.ArgumentParser(
43
+ prog="jevcheck",
44
+ description="Pin Jev production contracts and eval candidate model upgrades.",
45
+ )
46
+ sub = parser.add_subparsers(dest="command", required=True)
47
+
48
+ eval_cmd = sub.add_parser("eval", help="compare a candidate model to a contract")
49
+ eval_cmd.add_argument("contract", type=Path, help="JSON contract or JSONL fixtures")
50
+ eval_cmd.add_argument(
51
+ "--candidate-model",
52
+ required=True,
53
+ help="explicit candidate model (upgrades must be intentional)",
54
+ )
55
+ eval_cmd.add_argument(
56
+ "--baseline-model",
57
+ default=None,
58
+ help="override contract baseline_model (required for JSONL without _meta)",
59
+ )
60
+ eval_cmd.add_argument(
61
+ "--answers",
62
+ type=Path,
63
+ default=None,
64
+ help="replay file of mocked System One responses keyed by case id",
65
+ )
66
+ eval_cmd.add_argument(
67
+ "--allow-unpinned",
68
+ action="store_true",
69
+ help=(
70
+ "opt in to floating names such as jev-latest and jev-preview; "
71
+ "accept a concrete resolved response model (not another alias)"
72
+ ),
73
+ )
74
+
75
+ record_cmd = sub.add_parser(
76
+ "record",
77
+ help="fetch baseline answers and write a replay JSON (v0.2)",
78
+ )
79
+ record_cmd.add_argument("contract", type=Path, help="JSON contract or JSONL fixtures")
80
+ record_cmd.add_argument(
81
+ "--out",
82
+ type=Path,
83
+ required=True,
84
+ help="path to write a replay file usable by eval --answers / compare --from",
85
+ )
86
+ record_cmd.add_argument(
87
+ "--baseline-model",
88
+ default=None,
89
+ help="override contract baseline_model (required for JSONL without _meta)",
90
+ )
91
+ record_cmd.add_argument(
92
+ "--answers",
93
+ type=Path,
94
+ default=None,
95
+ help="replay file of mocked baseline responses (CI / tests; no live key)",
96
+ )
97
+ record_cmd.add_argument(
98
+ "--allow-unpinned",
99
+ action="store_true",
100
+ help=(
101
+ "opt in to floating names such as jev-latest and jev-preview; "
102
+ "accept a concrete resolved response model (not another alias)"
103
+ ),
104
+ )
105
+
106
+ compare_cmd = sub.add_parser(
107
+ "compare",
108
+ help="evaluate a candidate against recorded or live baseline answers (v0.2)",
109
+ )
110
+ compare_cmd.add_argument("contract", type=Path, help="JSON contract or JSONL fixtures")
111
+ source = compare_cmd.add_mutually_exclusive_group(required=True)
112
+ source.add_argument(
113
+ "--from",
114
+ dest="from_replay",
115
+ type=Path,
116
+ default=None,
117
+ help="recorded baseline replay JSON (output of jevcheck record)",
118
+ )
119
+ source.add_argument(
120
+ "--from-model",
121
+ dest="from_model",
122
+ default=None,
123
+ help="live baseline model (fetches both models when --answers is omitted)",
124
+ )
125
+ compare_cmd.add_argument(
126
+ "--to",
127
+ dest="to_model",
128
+ required=True,
129
+ help="explicit candidate model (same pinning rules as eval --candidate-model)",
130
+ )
131
+ compare_cmd.add_argument(
132
+ "--baseline-model",
133
+ default=None,
134
+ help="override contract baseline_model (required for JSONL without _meta)",
135
+ )
136
+ compare_cmd.add_argument(
137
+ "--answers",
138
+ type=Path,
139
+ default=None,
140
+ help="replay file of mocked candidate responses keyed by case id",
141
+ )
142
+ compare_cmd.add_argument(
143
+ "--allow-unpinned",
144
+ action="store_true",
145
+ help=(
146
+ "opt in to floating names such as jev-latest and jev-preview; "
147
+ "accept a concrete resolved response model (not another alias)"
148
+ ),
149
+ )
150
+
151
+ args = parser.parse_args(list(argv) if argv is not None else None)
152
+ handlers = {
153
+ "eval": _run_eval,
154
+ "record": _run_record,
155
+ "compare": _run_compare,
156
+ }
157
+ if args.command not in handlers:
158
+ parser.error("unknown command")
159
+
160
+ try:
161
+ return handlers[args.command](args)
162
+ except UnpinnedModelError as exc:
163
+ _print_error(exc)
164
+ return EXIT_USAGE
165
+ except ModelIdentityError as exc:
166
+ _print_error(exc)
167
+ return EXIT_USAGE
168
+ except TypeSafeAuthenticationError as exc:
169
+ _print_error(f"authentication failed ({exc.status}): {exc}")
170
+ return EXIT_OPS
171
+ except TypeSafeRateLimitError as exc:
172
+ _print_error(f"rate limited ({exc.status}): {exc}")
173
+ return EXIT_OPS
174
+ except TypeSafeInternalServerError as exc:
175
+ _print_error(f"server error ({exc.status}): {exc}")
176
+ return EXIT_OPS
177
+ except TypeSafeAPIResponseValidationError as exc:
178
+ _print_error(f"invalid API response: {exc}")
179
+ return EXIT_OPS
180
+ except TypeSafeAPIError as exc:
181
+ _print_error(f"API error ({exc.status}): {exc}")
182
+ return EXIT_OPS
183
+ except TypeSafeError as exc:
184
+ _print_error(exc)
185
+ return EXIT_USAGE
186
+ except (OSError, TypeError, ValueError, KeyError, RuntimeError) as exc:
187
+ _print_error(exc)
188
+ return EXIT_USAGE
189
+
190
+
191
+ def _run_eval(args: argparse.Namespace) -> int:
192
+ allow = bool(args.allow_unpinned)
193
+ candidate = require_pinned(args.candidate_model, allow_unpinned=allow)
194
+ contract: Contract = load_contract(
195
+ args.contract,
196
+ baseline_model=args.baseline_model,
197
+ allow_unpinned=allow,
198
+ )
199
+ fetch = _answers_or_live_fetch(
200
+ contract,
201
+ args.answers,
202
+ model=candidate,
203
+ allow=allow,
204
+ live_need="live eval",
205
+ )
206
+ report = evaluate(
207
+ contract,
208
+ fetch,
209
+ candidate_model=candidate,
210
+ allow_unpinned=allow,
211
+ )
212
+ sys.stdout.write(report.summary())
213
+ return EXIT_BREAKING if report.breaking else EXIT_OK
214
+
215
+
216
+ def _run_record(args: argparse.Namespace) -> int:
217
+ allow = bool(args.allow_unpinned)
218
+ contract = load_contract(
219
+ args.contract,
220
+ baseline_model=args.baseline_model,
221
+ allow_unpinned=allow,
222
+ )
223
+ baseline = require_pinned(contract.baseline_model, allow_unpinned=allow)
224
+ fetch = _answers_or_live_fetch(
225
+ contract,
226
+ args.answers,
227
+ model=baseline,
228
+ allow=allow,
229
+ live_need="live record",
230
+ )
231
+ recorded = record_answers(
232
+ contract,
233
+ fetch,
234
+ baseline_model=baseline,
235
+ allow_unpinned=allow,
236
+ )
237
+ write_replay(args.out, recorded)
238
+ title = contract.name or "contract"
239
+ resolved_models = list(dict.fromkeys(response.model for response in recorded.values()))
240
+ if len(resolved_models) == 1:
241
+ resolved = resolved_models[0]
242
+ elif resolved_models:
243
+ resolved = ", ".join(resolved_models)
244
+ else:
245
+ resolved = baseline
246
+ sys.stdout.write(
247
+ f"jevcheck record: {title}\n"
248
+ f"baseline: {format_resolved_model(baseline, resolved)}\n"
249
+ f"cases: {len(recorded)}\n"
250
+ f"wrote: {args.out}\n"
251
+ )
252
+ return EXIT_OK
253
+
254
+
255
+ def _run_compare(args: argparse.Namespace) -> int:
256
+ allow = bool(args.allow_unpinned)
257
+ if args.from_model and args.baseline_model:
258
+ from_pin = require_pinned(args.from_model, allow_unpinned=allow)
259
+ base_pin = require_pinned(args.baseline_model, allow_unpinned=allow)
260
+ if from_pin != base_pin:
261
+ raise ValueError("--from-model and --baseline-model disagree")
262
+ baseline_override = args.from_model or args.baseline_model
263
+ contract = load_contract(
264
+ args.contract,
265
+ baseline_model=baseline_override,
266
+ allow_unpinned=allow,
267
+ )
268
+ candidate = require_pinned(args.to_model, allow_unpinned=allow)
269
+ if args.from_replay is not None:
270
+ baseline = load_replay(args.from_replay)
271
+ else:
272
+ live_baseline = require_pinned(contract.baseline_model, allow_unpinned=allow)
273
+ baseline_fetch = _answers_or_live_fetch(
274
+ contract,
275
+ None,
276
+ model=live_baseline,
277
+ allow=allow,
278
+ live_need="live compare",
279
+ )
280
+ baseline = record_answers(
281
+ contract,
282
+ baseline_fetch,
283
+ baseline_model=live_baseline,
284
+ allow_unpinned=allow,
285
+ )
286
+ fetch = _answers_or_live_fetch(
287
+ contract,
288
+ args.answers,
289
+ model=candidate,
290
+ allow=allow,
291
+ live_need="live compare",
292
+ )
293
+ report = compare(
294
+ contract,
295
+ baseline,
296
+ fetch,
297
+ candidate_model=candidate,
298
+ allow_unpinned=allow,
299
+ )
300
+ sys.stdout.write(report.summary())
301
+ return EXIT_BREAKING if report.breaking else EXIT_OK
302
+
303
+
304
+ def _answers_or_live_fetch(
305
+ contract: Contract,
306
+ answers: Path | None,
307
+ *,
308
+ model: str,
309
+ allow: bool,
310
+ live_need: str,
311
+ ) -> Fetch:
312
+ if answers is not None:
313
+ replay = load_replay(answers)
314
+ missing = [case.id for case in contract.cases if case.id not in replay]
315
+ if missing:
316
+ raise KeyError(f"replay file missing cases: {missing}")
317
+
318
+ def fetch(case: Case) -> JevResponse:
319
+ return replay[case.id]
320
+
321
+ return fetch
322
+
323
+ client = JevClient(model=model, allow_unpinned=allow)
324
+
325
+ def fetch(case: Case) -> JevResponse:
326
+ return client.system_one(state=case.state, questions=case.questions, model=model)
327
+
328
+ if not client.api_key:
329
+ raise RuntimeError(f"{live_need} needs {AUTH_ENV} or a --answers replay file")
330
+ return fetch
331
+
332
+
333
+ def redact_secrets(text: str, *secrets: str | None) -> str:
334
+ """Remove known credentials from user-visible error text."""
335
+ redacted = text
336
+ for secret in secrets:
337
+ if secret:
338
+ redacted = redacted.replace(secret, "***")
339
+ env_key = os.environ.get(AUTH_ENV)
340
+ if env_key:
341
+ redacted = redacted.replace(env_key, "***")
342
+ redacted = re.sub(r"(?i)(bearer\s+)\S+", r"\1***", redacted)
343
+ redacted = re.sub(
344
+ r"(?i)(authorization\s*[:=]\s*)(\S+)",
345
+ r"\1***",
346
+ redacted,
347
+ )
348
+ return redacted
349
+
350
+
351
+ def _print_error(exc: object) -> None:
352
+ print(f"jevcheck: {redact_secrets(str(exc))}", file=sys.stderr)
353
+
354
+
355
+ if __name__ == "__main__":
356
+ raise SystemExit(main())