8Turn measurements into a gate-callable verdict with a real exit code.
12- Resolve thresholds from explicit flags over an ``.octrc.json`` section.
13- Apply the don't-get-worse ratchet against a recorded baseline.
14- Name every breaching function as ``relpath:line qualname CC=n [CRAP=m]``.
15- Carry the verdict as both machine fields and compact ASCII lines.
21 L2 - verdict lifecycle
22 L3 - threshold and breach details
27- No active threshold means no verdict, so today's output is unchanged.
28- Exit 0 is clean, exit 2 is at least one breach, exit 3 is a named refusal.
29- A malformed threshold is refused by name and never silently ignored.
30- The module imports only the Python standard library and sibling measurement
31 modules, so it stays callable from a zero-dependency context.
35- oct.complexity.service (result rows and relative-path display)
36- oct.complexity.baseline (lazy; BaselineEntry type hints for the ratchet comparison)
42from __future__
import annotations
44from dataclasses
import dataclass
45from pathlib
import Path
46from typing
import TYPE_CHECKING, Mapping, Sequence
51 relative_display_path,
68SOURCE_CONFIG =
"octrc"
70CONFIG_SECTION =
"complexity"
74 """Raised when a gate-mode invocation cannot be honestly evaluated."""
77@dataclass(frozen=
True, slots=
True)
79 """The effective CC and CRAP limits plus where each one came from."""
81 max_cc: int |
None =
None
82 max_crap: float |
None =
None
83 max_cc_source: str |
None =
None
84 max_crap_source: str |
None =
None
88 """Return whether any threshold is in force for this invocation."""
92 """Return a compact ASCII description of the active limits."""
94 if self.
max_cc is not None:
95 parts.append(f
"max CC {self.max_cc}")
97 parts.append(f
"max CRAP {_number(self.max_crap)}")
98 return ", ".join(parts)
if parts
else "no thresholds"
101@dataclass(frozen=True, slots=True)
103 """One function that fails the active thresholds, with its numbers."""
110 metrics: tuple[str, ...]
112 baseline_complexity: int |
None =
None
113 baseline_crap: float |
None =
None
116 """Return a JSON-ready breach record."""
130 """Return the pinned ``relpath:line qualname CC=n [CRAP=m]`` line."""
131 line = f
"{self.file}:{self.lineno} {self.qualname} CC={self.complexity}"
132 if self.
crap is not None:
133 line = f
"{line} CRAP={self.crap:.2f}"
134 return f
"{line} ({self.reason})"
137@dataclass(frozen=True, slots=True)
139 """Everything a gate or an agent needs to branch on one measurement run."""
143 thresholds: Thresholds
144 breaches: tuple[Breach, ...]
145 baseline: str |
None =
None
146 baseline_functions: int |
None =
None
147 baseline_written: str |
None =
None
148 baseline_written_functions: int |
None =
None
151 """Return the machine-readable verdict envelope."""
155 "max_cc": self.thresholds.max_cc,
156 "max_crap": self.thresholds.max_crap,
157 "max_cc_source": self.thresholds.max_cc_source,
158 "max_crap_source": self.thresholds.max_crap_source,
164 "breaches": [breach.to_dict()
for breach
in self.
breaches],
169 flag_max_cc: int |
None,
170 flag_max_crap: float |
None,
173 """Resolve the effective thresholds, with flags overriding the config.
175 ``config`` is the whole ``.octrc.json`` document; only its optional
176 ``complexity`` object is read. A malformed value raises
177 :class:`GateRefusalError` rather than degrading to "no threshold",
178 because a silently inactive gate reads exactly like a passing one.
180 section = config.get(CONFIG_SECTION)
if isinstance(config, dict)
else None
181 if section
is not None and not isinstance(section, dict):
183 f
"octrc '{CONFIG_SECTION}' must be a JSON object, got "
184 f
"{type(section).__name__}"
186 settings = section
or {}
187 config_cc =
_coerce_limit(settings.get(
"max_cc"),
"max_cc", as_int=
True)
188 config_crap =
_coerce_limit(settings.get(
"max_crap"),
"max_crap", as_int=
False)
189 max_cc = flag_max_cc
if flag_max_cc
is not None else config_cc
190 max_crap = flag_max_crap
if flag_max_crap
is not None else config_crap
194 max_cc_source=
_source(flag_max_cc, config_cc),
195 max_crap_source=
_source(flag_max_crap, config_crap),
199def _source(flag_value: object, config_value: object) -> str |
None:
200 """Return which input supplied the effective limit, if any did."""
201 if flag_value
is not None:
203 if config_value
is not None:
212) -> int | float |
None:
213 """Validate one configured limit or refuse it by name."""
216 if isinstance(value, bool)
or not isinstance(value, (int, float)):
218 f
"octrc '{CONFIG_SECTION}.{key}' must be a positive number, got "
219 f
"{type(value).__name__}"
222 if not isinstance(value, int)
or value < 1:
224 f
"octrc '{CONFIG_SECTION}.{key}' must be an integer of at "
225 f
"least 1, got {value!r}"
230 f
"octrc '{CONFIG_SECTION}.{key}' must be greater than 0, got {value!r}"
236 results: Sequence[FunctionResult],
237 thresholds: Thresholds,
239 baseline: Mapping[str,
"BaselineEntry"] |
None =
None,
240) -> tuple[Breach, ...]:
241 """Return every function that fails the active thresholds, worst first.
243 Without a baseline, failing means exceeding a threshold. With one, the
244 ratchet applies: a function the baseline never saw fails on the threshold
245 alone, and a recorded function fails only when it is BOTH over the
246 threshold and strictly worse than its recorded value. Existing debt held
247 at or below its recorded value never fails - this is hygiene, not
250 ``results`` arrives already ordered worst-first by the service, so the
251 breach list names the most severe function first.
253 if not thresholds.active:
261 if baseline
is not None:
262 recorded = baseline.get(
263 function_identity(item.file, item.qualname, root)
265 if recorded
is not None:
271 file=relative_display_path(item.file, root),
273 qualname=item.qualname,
274 complexity=item.complexity,
277 reason=
_reason(baseline, recorded, metrics),
278 baseline_complexity=
None if recorded
is None else recorded.complexity,
279 baseline_crap=
None if recorded
is None else recorded.crap,
282 return tuple(breaches)
286 item: FunctionResult,
287 metrics: tuple[str, ...],
288 recorded:
"BaselineEntry",
290 """Keep only the over-threshold metrics that are worse than recorded.
292 A recorded ``crap`` of ``None`` means the baseline was written without
293 coverage data, so there is no accepted value to sit at or below. That
294 fails closed: an unrecorded metric is never grandfathered.
297 for metric
in metrics:
298 if metric ==
"cc" and item.complexity > recorded.complexity:
300 elif metric ==
"crap" and (
301 recorded.crap
is None
302 or (item.crap
is not None and item.crap > recorded.crap)
309 baseline: Mapping[str,
"BaselineEntry"] |
None,
310 recorded:
"BaselineEntry | None",
311 metrics: tuple[str, ...],
313 """Return the human-readable cause of one breach."""
315 return "over-threshold"
317 return "not in baseline"
320 parts.append(f
"worse than baseline CC={recorded.complexity}")
321 if "crap" in metrics:
323 "no baseline CRAP recorded"
324 if recorded.crap
is None
325 else f
"worse than baseline CRAP={recorded.crap:.2f}"
327 return "; ".join(parts)
331 item: FunctionResult,
332 thresholds: Thresholds,
334 """Return which active metrics this one function exceeds."""
336 if thresholds.max_cc
is not None and item.complexity > thresholds.max_cc:
339 thresholds.max_crap
is not None
340 and item.crap
is not None
341 and item.crap > thresholds.max_crap
343 metrics.append(
"crap")
344 return tuple(metrics)
348 thresholds: Thresholds,
349 breaches: Sequence[Breach],
350 baseline: str |
None =
None,
351 baseline_functions: int |
None =
None,
352 baseline_written: str |
None =
None,
353 baseline_written_functions: int |
None =
None,
355 """Return the verdict for one gate-mode invocation."""
356 breached = bool(breaches)
358 status=
"breached" if breached
else "clean",
359 exit_code=EXIT_BREACH
if breached
else EXIT_CLEAN,
360 thresholds=thresholds,
361 breaches=tuple(breaches),
363 baseline_functions=baseline_functions,
364 baseline_written=baseline_written,
365 baseline_written_functions=baseline_written_functions,
370 """Render the verdict as compact ASCII lines for terminal users."""
372 if verdict.baseline_written
is not None:
374 f
"Baseline written: {verdict.baseline_written} "
375 f
"({verdict.baseline_written_functions} function(s))."
377 if verdict.baseline
is not None:
379 f
"Baseline: {verdict.baseline} "
380 f
"({verdict.baseline_functions} function(s) recorded)."
383 return "\n".join(lines)
387 """Return the verdict's outcome lines, breaches included."""
388 if not verdict.thresholds.active:
389 return [
"No thresholds active; measurement only."]
390 limits = verdict.thresholds.summary()
391 if not verdict.breaches:
392 return [f
"No threshold breaches ({limits})."]
393 lines = [f
"Threshold breaches ({limits}):"]
394 lines.extend(f
" {breach.format_line()}" for breach
in verdict.breaches)
396 f
"{len(verdict.breaches)} function(s) over the active thresholds."
402 """Render a limit without a trailing ``.0`` on whole numbers."""
403 return str(int(value))
if float(value).is_integer()
else str(value)
dict[str, object] to_dict(self)
int baseline_written_functions
dict[str, object] to_dict(self)
tuple[str,...] _breached_metrics(FunctionResult item, Thresholds thresholds)
tuple[str,...] _worsened_metrics(FunctionResult item, tuple[str,...] metrics, "BaselineEntry" recorded)
str|None _source(object flag_value, object config_value)
Thresholds resolve_thresholds(int|None flag_max_cc, float|None flag_max_crap, dict|None config)
str _reason(Mapping[str, "BaselineEntry"]|None baseline, "BaselineEntry | None" recorded, tuple[str,...] metrics)
list[str] _render_outcome(Verdict verdict)
int|float|None _coerce_limit(object value, str key, bool as_int)
tuple[Breach,...] evaluate(Sequence[FunctionResult] results, Thresholds thresholds, str|Path root, Mapping[str, "BaselineEntry"]|None baseline=None)
str render_verdict(Verdict verdict)
Verdict build_verdict(Thresholds thresholds, Sequence[Breach] breaches, str|None baseline=None, int|None baseline_functions=None, str|None baseline_written=None, int|None baseline_written_functions=None)