From bc99a7c3ffcbdb821d6537efa54aa7a3205b55a7 Mon Sep 17 00:00:00 2001 From: Kayhan Date: Sat, 10 Oct 2026 08:26:24 -0700 Subject: [PATCH 1/2] 0.3.11: a coverage problem says so at every door - A `coverage_map` shard that exists and cannot be parsed is exit 2 at every command and fails the plugin session. It was a warning, and the gate then exited 1 for an I/O failure. A missing shard behaves as before. - A map prefix loses only a leading `./`. `.lstrip("./")` turned `.tools/a` into `tools/a`, which matched nothing. - A TypeScript report that matches none of the scanned files says so on stderr and in every stdout format, with its own counts. Co-Authored-By: Claude Opus 5.5 --- README.md | 10 +- src/riskratchet/cli.py | 5 +- src/riskratchet/config.py | 4 +- src/riskratchet/coverage.py | 35 +++-- src/riskratchet/models.py | 49 +++++-- src/riskratchet/typescript_engine.py | 8 +- tests/test_coverage.py | 56 ++++++-- tests/test_pytest_plugin.py | 20 +++ tests/test_setup_errors.py | 19 ++- tests/test_typescript_coverage_overlap.py | 159 ++++++++++++++++++++++ 10 files changed, 313 insertions(+), 52 deletions(-) create mode 100644 tests/test_typescript_coverage_overlap.py diff --git a/README.md b/README.md index fea202e..ae4e490 100644 --- a/README.md +++ b/README.md @@ -1048,7 +1048,15 @@ A shard's `files` keys can be relative to the repository root (`src/m.py`), which is what `pytest --cov` writes when it runs from the package directory. The repository-relative spelling is tried first. If a coverage report matches none of the scanned files, the run says `coverage matched 0 of N scanned -files` on stderr, in the summary, and on the `Baseline:` line of `check`. +files` on stderr, in the summary, and on the `Baseline:` line of `check`. Since +0.3.11 a TypeScript report gets the same note, spelled `TypeScript coverage +matched 0 of N scanned files`, and the two counts are kept apart. + +A shard that exists and cannot be parsed is exit `2` at every command and fails +the pytest plugin's session (since 0.3.11; before that it was a warning, and its +prefix scored as uncovered). `allow_missing_coverage` covers a shard that is +missing, never one that is malformed. A map prefix may start with a dot +(`.tools/a`); a leading `./` is ignored. `--coverage FILE` on the command line replaces the configured map for that run (since 0.3.10; before that the flag was ignored whenever a map was configured). diff --git a/src/riskratchet/cli.py b/src/riskratchet/cli.py index aa448a5..6fa494d 100644 --- a/src/riskratchet/cli.py +++ b/src/riskratchet/cli.py @@ -2824,9 +2824,8 @@ def _coverage_shard_warn(path: Path, message: str) -> None: """Report a `--coverage-map` shard that could not be loaded. `_ensure_coverage_map_exists` promises "treating as no coverage" for a - missing shard, but the loader used to raise anyway. The warning now fires - where the shard is actually dropped, so a missing *and* a malformed shard - get the same one message with the same remediation. + missing shard, and this fires where the shard is actually dropped. Since 0.3.11 + only a missing shard reaches it: a malformed one raises and is exit 2. """ typer.secho( _format_setup_error( diff --git a/src/riskratchet/config.py b/src/riskratchet/config.py index 5e2b202..66e5cd6 100644 --- a/src/riskratchet/config.py +++ b/src/riskratchet/config.py @@ -903,9 +903,9 @@ def _ensure_coverage_map_exists( ) -> None: """Fail when a coverage-map path is missing and the policy forbids it. - When `allow_missing`, this is a no-op: the loader skips unusable shards and + When `allow_missing`, this is a no-op: the loader skips a missing shard and warns from `cli._coverage_shard_warn`, which fires where the shard is - actually dropped and covers malformed files too — not just absent ones. + actually dropped. A malformed shard is never allowed: it raises in the loader. Warning here as well would double-report the same shard. """ if allow_missing: diff --git a/src/riskratchet/coverage.py b/src/riskratchet/coverage.py index 147cb97..c616b2f 100644 --- a/src/riskratchet/coverage.py +++ b/src/riskratchet/coverage.py @@ -151,29 +151,29 @@ def load_coverage_map( ) -> MultiCoverageData: """Load one CoverageData per prefix and wrap them in a MultiCoverageData. - A shard that is missing or malformed is skipped and reported through - `on_error(path, message)` rather than aborting the whole run — the caller - (`config._ensure_coverage_map_exists`) may have already promised the user - "treating as no coverage", and a raise here would break that promise. This - mirrors `typescript_coverage.load_istanbul_coverage_files`, so both backends - degrade the same way. Without `on_error`, unusable shards are skipped - silently. + A missing shard is skipped and reported through `on_error(path, message)` rather than + aborting the run: the caller (`config._ensure_coverage_map_exists`) has either + already refused it or promised the user "treating as no coverage". A malformed shard + raises `ValueError`, which the entry points turn into a setup error. Without + `on_error`, a missing shard is skipped silently. """ loaded = ((prefix, _load_shard(path, on_error)) for prefix, path in coverage_map.items()) return MultiCoverageData.from_map({prefix: data for prefix, data in loaded if data is not None}) def _load_shard(path: Path, on_error: Any) -> CoverageData | None: - """Load one coverage shard, or report why it is unusable and return None.""" + """Load one coverage shard, or report that it is missing and return None. + + A shard that exists and cannot be parsed raises `ValueError` (0.3.11), as a single + `--coverage` file always has. It used to be skipped with a warning: every function + under its prefix scored 0%, and the gate exited 1 for what is an I/O failure. + """ try: return load_coverage(path) except FileNotFoundError: - message = "file not found" - except ValueError as exc: - message = str(exc) - if on_error is not None: - on_error(path, message) - return None + if on_error is not None: + on_error(path, "file not found") + return None def _prefix_relative_hit(data: CoverageData, prefix: str, normalized: str) -> dict[str, Any] | None: @@ -183,7 +183,12 @@ def _prefix_relative_hit(data: CoverageData, prefix: str, normalized: str) -> di def _normalize_prefix(raw: str) -> str: - return raw.replace("\\", "/").strip().lstrip("./").rstrip("/") + """`./packages/a/` -> `packages/a`. Only a leading `./` goes: `.lstrip("./")` strips + characters, so until 0.3.11 `.tools/a` became `tools/a` and matched nothing.""" + prefix = raw.replace("\\", "/").strip() + while prefix.startswith("./"): + prefix = prefix[2:] + return "" if prefix == "." else prefix.rstrip("/") def coverage_for_span( diff --git a/src/riskratchet/models.py b/src/riskratchet/models.py index 9093e9f..f4e4bb9 100644 --- a/src/riskratchet/models.py +++ b/src/riskratchet/models.py @@ -63,16 +63,17 @@ def label(self) -> str: } -def coverage_overlap_note(checked: int, unmatched: int) -> str | None: +def coverage_overlap_note(checked: int, unmatched: int, *, label: str = "coverage") -> str | None: """`coverage matched 0 of 12 scanned files`, or None when at least one file matched. The one spelling every renderer and the stderr warning share (0.3.10). A count, never a path, so it is safe under redaction. Only the all-or-nothing case is said: a partial - match is the ordinary state of a project with untested modules. + match is the ordinary state of a project with untested modules. `label` names the + backend's report (0.3.11), so a TypeScript report that matched nothing says which. """ if not checked or unmatched < checked: return None - return f"coverage matched 0 of {checked} scanned file{'' if checked == 1 else 's'}" + return f"{label} matched 0 of {checked} scanned file{'' if checked == 1 else 's'}" def unscored_breakdown(counts: dict[UnscoredCause, int]) -> str: @@ -235,19 +236,43 @@ class RiskReport: coverage_checked_files: int = 0 coverage_unmatched_files: int = 0 + # The same pair for TypeScript files and the Istanbul/LCOV reports (0.3.11). Kept apart + # from the Python pair: summed, one backend matching everything would hide the other + # matching nothing. + ts_coverage_checked_files: int = 0 + ts_coverage_unmatched_files: int = 0 + + def _coverage_notes(self) -> tuple[str | None, str | None]: + return ( + coverage_overlap_note(self.coverage_checked_files, self.coverage_unmatched_files), + coverage_overlap_note( + self.ts_coverage_checked_files, + self.ts_coverage_unmatched_files, + label="TypeScript coverage", + ), + ) + def coverage_note(self) -> str | None: - return coverage_overlap_note(self.coverage_checked_files, self.coverage_unmatched_files) + notes = [note for note in self._coverage_notes() if note is not None] + return "; ".join(notes) or None def coverage_warning(self) -> str | None: """The sentence the CLI and the pytest plugin both print when coverage matched nothing.""" - note = self.coverage_note() - if note is None: - return None - return ( - f"{note}, so no function has coverage data this run and a change in test coverage " - "cannot move a score. The report's `files` keys name other paths; " - "`riskratchet doctor` shows the overlap." - ) + python, typescript = self._coverage_notes() + sentences = [] + if python is not None: + sentences.append( + f"{python}, so no function has coverage data this run and a change in test coverage " + "cannot move a score. The report's `files` keys name other paths; " + "`riskratchet doctor` shows the overlap." + ) + if typescript is not None: + sentences.append( + f"{typescript}, so no TypeScript function has coverage data this run and a change " + "in test coverage cannot move its score. The report names other paths; " + "`riskratchet doctor` shows the overlap." + ) + return " ".join(sentences) or None def by_id(self) -> dict[FunctionId, FunctionRisk]: return {fn.id: fn for fn in self.functions} diff --git a/src/riskratchet/typescript_engine.py b/src/riskratchet/typescript_engine.py index 6ea3dfd..5c6f72b 100644 --- a/src/riskratchet/typescript_engine.py +++ b/src/riskratchet/typescript_engine.py @@ -113,6 +113,7 @@ def analyze_typescript( file_line_counts: dict[str, int] = {} file_fn_counts: dict[str, int] = {} unmeasured_files = 0 + measurable_files = 0 skipped = _SkippedFiles(root_path, on_error) for path in files: rel = relative_posix(path, root_path) @@ -124,6 +125,7 @@ def analyze_typescript( file_fn_counts[rel] = len(found) if has_coverage: file_cov = coverage.lookup(rel) + measurable_files += bool(found) if file_cov is None and found: unmeasured_files += 1 found = _enrich_coverage(found, file_cov, rel, on_warning) @@ -205,6 +207,8 @@ def analyze_typescript( skipped_generated_files=len(skipped.generated), unscored_functions=tuple(unscored_functions), unscored_files=skipped.unscored(), + ts_coverage_checked_files=measurable_files, + ts_coverage_unmatched_files=unmeasured_files, scoring=ScoringInputs( model=SCORING_MODEL_VERSION, weights=resolved_weights, @@ -257,9 +261,11 @@ def merge_reports(python: RiskReport, typescript: RiskReport) -> RiskReport: skipped_generated_files=python.skipped_generated_files + typescript.skipped_generated_files, unscored_functions=python.unscored_functions + typescript.unscored_functions, unscored_files=python.unscored_files + typescript.unscored_files, - # Python's own: the TypeScript loader refuses a report that matches nothing. + # Each backend's own pair, never summed: see `RiskReport.ts_coverage_checked_files`. coverage_checked_files=python.coverage_checked_files, coverage_unmatched_files=python.coverage_unmatched_files, + ts_coverage_checked_files=typescript.ts_coverage_checked_files, + ts_coverage_unmatched_files=typescript.ts_coverage_unmatched_files, # One value, not a merge: `build_report` hands both backends the same weights, churn # window and root, so the two `ScoringInputs` are equal by construction. Falling back # to the TypeScript one keeps a hand-built Python report (tests, downstream callers) diff --git a/tests/test_coverage.py b/tests/test_coverage.py index 1d5628f..afb149e 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -194,7 +194,11 @@ def test_load_coverage_map_skips_missing_shard_with_callback(tmp_path: Path) -> assert errors == [(tmp_path / "absent.json", "file not found")] -def test_load_coverage_map_skips_unreadable_shard(tmp_path: Path) -> None: +def test_load_coverage_map_raises_for_a_malformed_shard(tmp_path: Path) -> None: + """0.3.11: a shard that exists and cannot be parsed is an error, as one `--coverage` file is. + + It used to be skipped through `on_error`, so its prefix scored 0% and the gate exited 1. + """ good = _write( tmp_path, {"files": {"pkg/foo.py": {"executed_lines": [1], "missing_lines": []}}}, @@ -202,28 +206,56 @@ def test_load_coverage_map_skips_unreadable_shard(tmp_path: Path) -> None: junk = tmp_path / "junk.json" junk.write_text("not json", encoding="utf-8") errors: list[tuple[Path, str]] = [] + with pytest.raises(ValueError, match="could not read coverage file"): + load_coverage_map( + {"pkg": good, "bad": junk}, + on_error=lambda path, message: errors.append((path, message)), + ) + assert errors == [] + + +def test_load_coverage_map_skips_a_missing_shard(tmp_path: Path) -> None: + good = _write( + tmp_path, + {"files": {"pkg/foo.py": {"executed_lines": [1], "missing_lines": []}}}, + ) + gone = tmp_path / "absent.json" + errors: list[tuple[Path, str]] = [] multi = load_coverage_map( - {"pkg": good, "bad": junk}, + {"pkg": good, "gone": gone}, on_error=lambda path, message: errors.append((path, message)), ) assert multi.lookup("pkg/foo.py") is not None - assert len(errors) == 1 - assert errors[0][0] == junk - assert "could not read" in errors[0][1] + assert errors == [(gone, "file not found")] -def test_load_coverage_map_without_callback_skips_silently(tmp_path: Path) -> None: - """`on_error` is optional: unusable shards are simply absent.""" - junk = tmp_path / "junk.json" - junk.write_text("not json", encoding="utf-8") +def test_load_coverage_map_without_callback_skips_a_missing_shard_silently(tmp_path: Path) -> None: + """`on_error` is optional: a missing shard is simply absent.""" + multi = load_coverage_map({"gone": tmp_path / "absent.json"}) - multi = load_coverage_map({"bad": junk, "gone": tmp_path / "absent.json"}) - - assert multi.lookup("bad/foo.py") is None + assert multi.lookup("gone/foo.py") is None assert multi.prefixes == () +@pytest.mark.parametrize( + ("prefix", "path"), + [ + (".tools/a", ".tools/a/src/m.py"), + ("./packages/a", "packages/a/src/m.py"), + ("././packages/a/", "packages/a/src/m.py"), + (".", "src/m.py"), + ], +) +def test_a_map_prefix_loses_only_a_leading_dot_slash(tmp_path: Path, prefix: str, path: str) -> None: + """0.3.11: `.lstrip("./")` strips characters, so `.tools/a` became `tools/a` and matched nothing.""" + shard = _write(tmp_path, {"files": {"src/m.py": {"executed_lines": [1], "missing_lines": []}}}) + + multi = load_coverage_map({prefix: shard}) + + assert multi.lookup(path) is not None + + # --- 0.3.4: every JSON loader that raises must reject a non-object root -------- _RAISING_LOADERS = { diff --git a/tests/test_pytest_plugin.py b/tests/test_pytest_plugin.py index d0d2c3f..6e2b06f 100644 --- a/tests/test_pytest_plugin.py +++ b/tests/test_pytest_plugin.py @@ -742,6 +742,26 @@ def test_the_plugin_fails_the_session_for_a_missing_shard(pytester: pytest.Pytes assert "allow_missing_coverage = true" in text +def test_the_plugin_fails_the_session_for_a_malformed_shard(pytester: pytest.Pytester) -> None: + """0.3.11: it was a warning, and `allow_missing_coverage` does not make it one again.""" + _map_project(pytester) + (pytester.path / "cov-b.json").write_text("[]", encoding="utf-8") + pyproject = pytester.path / "pyproject.toml" + pyproject.write_text( + pyproject.read_text(encoding="utf-8").replace( + 'paths = ["packages"]\n', 'paths = ["packages"]\nallow_missing_coverage = true\n' + ), + encoding="utf-8", + ) + + result = pytester.runpytest_subprocess("--riskratchet", "-p", "no:cacheprovider") + + assert result.ret == 1, result.stdout.str() + text = _collapsed(result.stdout.str()) + assert "cov-b.json must be a JSON object" in text + assert "treating that prefix as no coverage" not in text + + def test_the_plugin_tolerates_a_missing_shard_when_config_allows_it(pytester: pytest.Pytester) -> None: _map_project(pytester) (pytester.path / "cov-b.json").unlink() diff --git a/tests/test_setup_errors.py b/tests/test_setup_errors.py index a271137..e4f7cde 100644 --- a/tests/test_setup_errors.py +++ b/tests/test_setup_errors.py @@ -386,22 +386,29 @@ def test_scan_with_missing_coverage_map_shard_continues( assert "pytest --cov" in result.stderr -def test_scan_with_malformed_coverage_map_shard_continues( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch +@pytest.mark.parametrize("command", ["scan", "baseline", "check", "diff", "explain"]) +def test_a_malformed_coverage_map_shard_is_exit_2( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, command: str ) -> None: + """0.3.11: it was a warning. Every function under the prefix then scored 0%, so `check` + exited 1 for an I/O failure, while the same bytes passed as `--coverage` were exit 2. + `--allow-missing-coverage` does not cover it: the shard is there, and it is corrupt.""" monkeypatch.chdir(tmp_path) src = _project(tmp_path) + _seed_baseline(src) junk = tmp_path / "junk.json" junk.write_text("not json", encoding="utf-8") + target = ["src/m.py::trivial"] if command == "explain" else [str(src)] + allow = ["--allow-missing-coverage"] if command in {"baseline", "check", "diff"} else [] result = runner.invoke( app, - ["scan", str(src), "--coverage-map", f"src={junk}", "--no-auto-cov", "--no-git"], + [command, *target, "--coverage-map", f"src={junk}", "--no-auto-cov", "--no-git", *allow], ) - assert result.exit_code == 0, result.output - assert "coverage-map shard unusable" in result.stderr - assert "could not read" in result.stderr + assert result.exit_code == 2, (result.output, result.stderr) + assert "could not read coverage file" in result.stderr + assert "junk.json" in result.stderr def test_strict_missing_coverage_map_shard_still_fails( diff --git a/tests/test_typescript_coverage_overlap.py b/tests/test_typescript_coverage_overlap.py new file mode 100644 index 0000000..1152ffb --- /dev/null +++ b/tests/test_typescript_coverage_overlap.py @@ -0,0 +1,159 @@ +"""0.3.11: an Istanbul report that matches no scanned TypeScript file says so. + +0.3.10 added the note for Python and counted Python files only. A TypeScript report keyed +to some other tree printed one stderr line, and every stdout format stayed silent. +""" + +from __future__ import annotations + +import json +from typing import TYPE_CHECKING + +import pytest +from typer.testing import CliRunner + +from riskratchet.cli import app +from riskratchet.models import RiskReport + +pytest.importorskip("tree_sitter") +pytest.importorskip("tree_sitter_typescript") + +if TYPE_CHECKING: + from pathlib import Path + +runner = CliRunner() + +_TS = ( + "export function f(x: number, y: number): number {\n" + " if (x > 1) {\n" + " if (y > 2) { return 1; }\n" + " return 2;\n" + " }\n" + " return y;\n" + "}\n" +) +_PY = "def g(x):\n if x:\n return 1\n return 0\n" +_NOTE = "TypeScript coverage matched 0 of 2 scanned files" + + +def _istanbul(*paths: str) -> str: + statement = {"start": {"line": 2, "column": 0}, "end": {"line": 2, "column": 5}} + return json.dumps( + { + path: { + "path": path, + "statementMap": {"0": statement}, + "s": {"0": 1}, + "fnMap": {}, + "f": {}, + "branchMap": {}, + "b": {}, + } + for path in paths + } + ) + + +def _project(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, *covered: str, python: bool = False) -> None: + """Two TypeScript files and an Istanbul report that names `covered`.""" + (tmp_path / "src").mkdir() + for name in ("a.ts", "b.ts"): + (tmp_path / "src" / name).write_text(_TS, encoding="utf-8") + (tmp_path / "cov.json").write_text(_istanbul(*covered), encoding="utf-8") + config = '[tool.riskratchet]\npaths = ["src"]\nauto_coverage = false\ntypescript = true\nts_coverage = ["cov.json"]\n' + if python: + (tmp_path / "src" / "g.py").write_text(_PY, encoding="utf-8") + (tmp_path / "coverage.json").write_text( + json.dumps({"files": {"src/g.py": {"executed_lines": [1, 2, 3, 4], "missing_lines": []}}}), + encoding="utf-8", + ) + config += 'coverage = "coverage.json"\n' + (tmp_path / "pyproject.toml").write_text(config, encoding="utf-8") + monkeypatch.chdir(tmp_path) + + +def _run(*args: str) -> tuple[int, str, str]: + result = runner.invoke(app, [*args, "--no-git"]) + return result.exit_code, " ".join(result.stdout.split()), result.stderr + + +def test_scan_says_so_in_every_format_and_once_on_stderr( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _project(tmp_path, monkeypatch, "/elsewhere/other.ts") + + for fmt in ("table", "markdown"): + code, out, err = _run("scan", "--format", fmt) + assert code == 0, (fmt, out, err) + assert _NOTE in out, fmt + assert err.count(f"warning: {_NOTE}") == 1, fmt + + +def test_check_says_so_on_the_baseline_line_of_every_format( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _project(tmp_path, monkeypatch, "/elsewhere/other.ts") + assert _run("baseline")[0] == 0 + + for fmt in ("table", "markdown", "pr-comment"): + code, out, err = _run("check", "--format", fmt) + assert code == 0, (fmt, out, err) + assert f"0 not seen this run · {_NOTE}" in out, fmt + assert err.count(f"warning: {_NOTE}") == 1, fmt + + +def test_a_partial_match_says_nothing(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + _project(tmp_path, monkeypatch, "src/a.ts") + assert _run("baseline")[0] == 0 + + for args in (("scan",), ("check",), ("check", "--format", "pr-comment")): + _, out, err = _run(*args) + assert "matched 0 of" not in out + err, args + + +def test_python_that_matches_does_not_hide_typescript_that_does_not( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """The two counts are kept apart: summed, 1 match of 3 files would read as a partial match.""" + _project(tmp_path, monkeypatch, "/elsewhere/other.ts", python=True) + + _, out, err = _run("scan") + + assert _NOTE in out + assert "warning: coverage matched 0 of" not in err + assert "no TypeScript function has coverage data" in err + + +def test_the_note_names_no_path_under_private_comment( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _project(tmp_path, monkeypatch, "/elsewhere/other.ts") + assert _run("baseline")[0] == 0 + + result = runner.invoke( + app, + ["check", "--no-git", "--format", "pr-comment", "--private-comment", "--redact-salt", "s"], + ) + + assert _NOTE in result.stdout + assert "src/a.ts" not in result.stdout + warning = next(line for line in result.stderr.splitlines() if "matched 0 of" in line) + assert "src/" not in warning + assert "elsewhere" not in warning + + +def test_both_notes_join_when_both_backends_match_nothing() -> None: + report = RiskReport( + functions=(), + files=(), + coverage_checked_files=1, + coverage_unmatched_files=1, + ts_coverage_checked_files=2, + ts_coverage_unmatched_files=2, + ) + + assert report.coverage_note() == f"coverage matched 0 of 1 scanned file; {_NOTE}" + warning = report.coverage_warning() + assert warning is not None + assert "no function has coverage data" in warning + assert "no TypeScript function has coverage data" in warning From f9bf5df8c48aeb361ae712e5b5e8e71ccec851ae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 15:27:51 +0000 Subject: [PATCH 2/2] chore: regenerate riskratchet baseline (CI) [riskratchet-baseline-bypass] --- .riskratchet.json | 107 ++++++++++++++++++++++++++-------------------- 1 file changed, 61 insertions(+), 46 deletions(-) diff --git a/.riskratchet.json b/.riskratchet.json index b0f5425..d5e95c4 100644 --- a/.riskratchet.json +++ b/.riskratchet.json @@ -1308,16 +1308,16 @@ { "path": "src/riskratchet/cli.py", "qualname": "_coverage_shard_warn", - "score": 0.0, + "score": 1.0, "components": { "coverage_gap": 0.0, "structural_complexity": 0.0, "branch_gap": 0.0, - "churn": 0.0, + "churn": 10.0, "public_surface": 0.0, "sprawl": 0.0 }, - "fingerprint": "2aa7f411111c648447f5a8967e93ad4448b69f8217952d846fb25c6e4ab8c473", + "fingerprint": "95d33bfa2d51c73c915725b4eb38e6c2730dbb8388c341e0cda94c86ab1ecee9", "signature": "c99319a04055c73049538aeb84ed9b75f9d4fbcf8b6ba1ff8dba13dbc6dea409" }, { @@ -1713,12 +1713,12 @@ { "path": "src/riskratchet/cli.py", "qualname": "_refuse_to_erase_baseline", - "score": 15.7794, + "score": 14.7794, "components": { "coverage_gap": 11.7647, "structural_complexity": 30.0, "branch_gap": 25.0, - "churn": 10.0, + "churn": 0.0, "public_surface": 0.0, "sprawl": 0.0 }, @@ -2463,16 +2463,16 @@ { "path": "src/riskratchet/config.py", "qualname": "_ensure_coverage_map_exists", - "score": 6.75, + "score": 7.75, "components": { "coverage_gap": 0.0, "structural_complexity": 15.0, "branch_gap": 0.0, - "churn": 30.0, + "churn": 40.0, "public_surface": 0.0, "sprawl": 0.0 }, - "fingerprint": "6114959e064e94959ed43477231786d545aa5b378e28dc36f7958414332c49fd", + "fingerprint": "297b2aa0d68c4b8e02dde0dedb2a565566501a5beeae87140d6a9b261b2caceb", "signature": "b1fba04050efd6503aeeee1a669eb2ea6c70436c1037d9561473f28b0565866f" }, { @@ -3393,31 +3393,31 @@ { "path": "src/riskratchet/coverage.py", "qualname": "_load_shard", - "score": 3.75, + "score": 3.5, "components": { "coverage_gap": 0.0, - "structural_complexity": 15.0, + "structural_complexity": 10.0, "branch_gap": 0.0, - "churn": 0.0, + "churn": 10.0, "public_surface": 0.0, "sprawl": 0.0 }, - "fingerprint": "d99eb8e4ebfa79e64d8a8ff38f41af1229b3e374634092df50fb1e0908a515fc", + "fingerprint": "74aeb5bcad3f8cb4f1711e54f27874130d3d994a5b6d361362a6f47d08c7cad2", "signature": "dc2f0467108086c90825696912265ee5f7b655ea8a0e49c7c46f14fca847833b" }, { "path": "src/riskratchet/coverage.py", "qualname": "_normalize_prefix", - "score": 0.0, + "score": 3.5, "components": { "coverage_gap": 0.0, - "structural_complexity": 0.0, + "structural_complexity": 10.0, "branch_gap": 0.0, - "churn": 0.0, + "churn": 10.0, "public_surface": 0.0, "sprawl": 0.0 }, - "fingerprint": "f6958cf5fad1700ba1a921ea241f776a4d931b136792e583a903e01f9c1f9bdd", + "fingerprint": "9ff3dad7435012675c809388686bf95aafa821b89a8dca97c453096ecddcc96c", "signature": "62ac03e10cd3962aa87f9531c0fd55e82235fe51240924a9fe27b17947059b60" }, { @@ -3483,16 +3483,16 @@ { "path": "src/riskratchet/coverage.py", "qualname": "load_coverage_map", - "score": 4.75, + "score": 5.75, "components": { "coverage_gap": 0.0, "structural_complexity": 15.0, "branch_gap": 0.0, - "churn": 10.0, + "churn": 20.0, "public_surface": 0.0, "sprawl": 0.0 }, - "fingerprint": "4624e3ec5b2a555c401eacb5af145d1e4e7ae7d749573a06f0d81e5d8486cfee", + "fingerprint": "e5461326668bc069d468deb2efbc818720311ddcdb69efdefc31c248d848adc2", "signature": "26ee06065d4e5d55da515327c8614918a80bc2ce9beaceff6d3b533697613aa4" }, { @@ -4905,6 +4905,21 @@ "fingerprint": "172cb54f4a892e5a463118992d083b15ddfbb3082e6f9ccb97f488316c1db4bd", "signature": "6a58e1b8d468378ebe884ffaaf98089832bc3483a4137411de77d1a859566c4e" }, + { + "path": "src/riskratchet/models.py", + "qualname": "RiskReport._coverage_notes", + "score": 2.0, + "components": { + "coverage_gap": 0.0, + "structural_complexity": 0.0, + "branch_gap": 0.0, + "churn": 20.0, + "public_surface": 0.0, + "sprawl": 0.0 + }, + "fingerprint": "8b0f3bd37f1b42d1b95d720a2861003dc62d6c9053fd191996a4c343ba3ffaab", + "signature": "d7434a5f52456b5389cdac6a411940fb2b8edbbd8bd0446a10c8ae67136c016a" + }, { "path": "src/riskratchet/models.py", "qualname": "RiskReport.by_id", @@ -4923,31 +4938,31 @@ { "path": "src/riskratchet/models.py", "qualname": "RiskReport.coverage_note", - "score": 1.0, + "score": 4.75, "components": { "coverage_gap": 0.0, - "structural_complexity": 0.0, + "structural_complexity": 15.0, "branch_gap": 0.0, "churn": 10.0, "public_surface": 0.0, "sprawl": 0.0 }, - "fingerprint": "77d5be7a920518e80ffb22e3c00c78c2c82d91f654795fc67c1849e683f265e6", + "fingerprint": "fdf4ef941c3e7c505b174628e5c727a852f84c13b5a30da0ded85dba71023e26", "signature": "1deb5b29adb74cf2ccf731cfa88f74c5108cffaf43790caed20f3ea53f477885" }, { "path": "src/riskratchet/models.py", "qualname": "RiskReport.coverage_warning", - "score": 2.25, + "score": 4.75, "components": { "coverage_gap": 0.0, - "structural_complexity": 5.0, + "structural_complexity": 15.0, "branch_gap": 0.0, "churn": 10.0, "public_surface": 0.0, "sprawl": 0.0 }, - "fingerprint": "0d1f6dfe91975bbf98695410dcfdbbccf0bce3e6cd137d4e99c253119b99dd7f", + "fingerprint": "d1332c558bde2e40ac616e3c1bed8d53e77321265f1a4a035e9b212367b8ea10", "signature": "1deb5b29adb74cf2ccf731cfa88f74c5108cffaf43790caed20f3ea53f477885" }, { @@ -4983,17 +4998,17 @@ { "path": "src/riskratchet/models.py", "qualname": "coverage_overlap_note", - "score": 5.75, + "score": 6.75, "components": { "coverage_gap": 0.0, "structural_complexity": 15.0, "branch_gap": 0.0, - "churn": 20.0, + "churn": 30.0, "public_surface": 0.0, "sprawl": 0.0 }, - "fingerprint": "44ee2cd6f2e697e18139093369833df6985027ecffafef6f2ab59c39956eab57", - "signature": "ae0f4dc31ef62f0e98f52c1bf085ee236ed3c1e34c18d5aa6c3e70551614b383" + "fingerprint": "ba64c550c39cecb76d280706a461a642b4a7672e285139beeae1bb31e9d54274", + "signature": "9a74b959ccf2be7769306cad8692eb45fabdc4043293878e7acaefc0caf9f707" }, { "path": "src/riskratchet/models.py", @@ -8178,12 +8193,12 @@ { "path": "src/riskratchet/typescript_engine.py", "qualname": "_SkippedFiles.__init__", - "score": 3.0, + "score": 1.0, "components": { "coverage_gap": 0.0, "structural_complexity": 0.0, "branch_gap": 0.0, - "churn": 30.0, + "churn": 10.0, "public_surface": 0.0, "sprawl": 0.0 }, @@ -8193,12 +8208,12 @@ { "path": "src/riskratchet/typescript_engine.py", "qualname": "_SkippedFiles.on_error", - "score": 9.75, + "score": 10.75, "components": { "coverage_gap": 0.0, "structural_complexity": 5.0, "branch_gap": 50.0, - "churn": 10.0, + "churn": 20.0, "public_surface": 0.0, "sprawl": 0.0 }, @@ -8223,12 +8238,12 @@ { "path": "src/riskratchet/typescript_engine.py", "qualname": "_SkippedFiles.unscored", - "score": 5.5, + "score": 4.5, "components": { "coverage_gap": 0.0, "structural_complexity": 10.0, "branch_gap": 0.0, - "churn": 30.0, + "churn": 20.0, "public_surface": 0.0, "sprawl": 0.0 }, @@ -8253,12 +8268,12 @@ { "path": "src/riskratchet/typescript_engine.py", "qualname": "_enrich_coverage", - "score": 11.75, + "score": 10.75, "components": { "coverage_gap": 0.0, "structural_complexity": 35.0, "branch_gap": 0.0, - "churn": 30.0, + "churn": 20.0, "public_surface": 0.0, "sprawl": 0.0 }, @@ -8313,12 +8328,12 @@ { "path": "src/riskratchet/typescript_engine.py", "qualname": "_resolve_coverage", - "score": 7.0, + "score": 8.0, "components": { "coverage_gap": 0.0, "structural_complexity": 20.0, "branch_gap": 0.0, - "churn": 20.0, + "churn": 30.0, "public_surface": 0.0, "sprawl": 0.0 }, @@ -8343,16 +8358,16 @@ { "path": "src/riskratchet/typescript_engine.py", "qualname": "analyze_typescript", - "score": 42.1533, + "score": 43.4886, "components": { - "coverage_gap": 11.3208, + "coverage_gap": 10.9091, "structural_complexity": 85.0, "branch_gap": 12.5, - "churn": 60.0, - "public_surface": 11.3208, - "sprawl": 85.0 + "churn": 70.0, + "public_surface": 10.9091, + "sprawl": 90.0 }, - "fingerprint": "15d40bb25477a8d54b48642579ca9bd5a850daa2131aef72c8b7ed08e7044011", + "fingerprint": "008332bdfcdf4335e30422a076aa1e4bf3841991accd92798903488b21bbf2b4", "signature": "1c986107dd94984c9dfc2250ba74d54131c386aee3f70adbd0dd343e0f828f7d" }, { @@ -8367,7 +8382,7 @@ "public_surface": 0.0, "sprawl": 0.0 }, - "fingerprint": "697a80bc3ae984f5759f7f2082f592951f7c63d8fa54ea646a4370f7e9cbab55", + "fingerprint": "b363756dea16a8651a48ae665a72a28f886caafbf96b0970d1af788acaef62a0", "signature": "845cb928a25a12ec65c6e82db31b23193dc93079d4c256270ef7e81a12bc8fdf" }, {