From 4bbf3bc5ca59cd4efafc6eb89385d54584ba447b Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Mon, 6 Apr 2026 21:01:28 +0500 Subject: [PATCH 01/17] feat(report): add canonical analysis profiles and bump report schema to 2.4 --- CHANGELOG.md | 10 ++++ README.md | 16 ++++-- benchmarks/run_docker_benchmark.sh | 2 +- codeclone/_cli_meta.py | 24 +++++++++ codeclone/_html_report/_sections/_overview.py | 15 +++++- codeclone/cli.py | 6 +++ codeclone/contracts.py | 2 +- codeclone/mcp_service.py | 37 +++++++++++++- codeclone/report/json_contract.py | 29 ++++++++++- docs/README.md | 2 +- docs/architecture.md | 3 +- docs/book/08-report.md | 7 ++- docs/book/13-testing-as-spec.md | 2 +- docs/book/14-compatibility-and-versioning.md | 2 +- docs/book/20-mcp-interface.md | 6 +-- docs/book/appendix/b-schema-layouts.md | 22 +++++--- docs/mcp.md | 48 +++++++++--------- docs/vscode-extension.md | 4 +- extensions/vscode-codeclone/README.md | 6 +-- pyproject.toml | 2 +- .../golden_expected_cli_snapshot.json | 2 +- tests/test_html_report.py | 50 +++++++++++++++++++ tests/test_mcp_service.py | 48 ++++++++++++++++++ tests/test_report_contract_coverage.py | 16 ++++++ uv.lock | 2 +- 25 files changed, 306 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6a19f1..9c44738 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [2.0.0b5] + +### MCP, HTML, and docs + +- Bump canonical report schema to `2.4` for `meta.analysis_profile`. +- Surface the effective runtime analysis profile (`min_loc`, `min_stmt`, block, and segment thresholds) in canonical + report metadata, MCP summary/triage projections, and the HTML Executive Summary subtitle. +- Refresh branch metadata and client docs for the `2.0.0b5` line. +- Update the README repository health badge to `87 (B)`. + ## [2.0.0b4] ### MCP server diff --git a/README.md b/README.md index e1ce0db..9dfa029 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Tests Benchmark Python - codeclone 85 (B) + codeclone 87 (B) License

@@ -274,16 +274,24 @@ class Middleware: # codeclone: ignore[dead-code] Dynamic/runtime false positives are resolved via explicit inline suppressions, not via broad heuristics.
-Canonical JSON report shape (v2.3) +Canonical JSON report shape (v2.4) ```json { - "report_schema_version": "2.3", + "report_schema_version": "2.4", "meta": { - "codeclone_version": "2.0.0b4", + "codeclone_version": "2.0.0b5", "project_name": "...", "scan_root": ".", "report_mode": "full", + "analysis_profile": { + "min_loc": 10, + "min_stmt": 6, + "block_min_loc": 20, + "block_min_stmt": 8, + "segment_min_loc": 20, + "segment_min_stmt": 10 + }, "analysis_thresholds": { "design_findings": { "...": "..." diff --git a/benchmarks/run_docker_benchmark.sh b/benchmarks/run_docker_benchmark.sh index 7faf994..7a11fe7 100755 --- a/benchmarks/run_docker_benchmark.sh +++ b/benchmarks/run_docker_benchmark.sh @@ -2,7 +2,7 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -IMAGE_TAG="${IMAGE_TAG:-codeclone-benchmark:2.0.0b4}" +IMAGE_TAG="${IMAGE_TAG:-codeclone-benchmark:2.0.0b5}" OUT_DIR="${OUT_DIR:-$ROOT_DIR/.cache/benchmarks}" OUTPUT_BASENAME="${OUTPUT_BASENAME:-codeclone-benchmark.json}" CPUSET="${CPUSET:-0}" diff --git a/codeclone/_cli_meta.py b/codeclone/_cli_meta.py index f112d8d..ffa9245 100644 --- a/codeclone/_cli_meta.py +++ b/codeclone/_cli_meta.py @@ -23,6 +23,15 @@ from .metrics_baseline import MetricsBaseline +class AnalysisProfileMeta(TypedDict): + min_loc: int + min_stmt: int + block_min_loc: int + block_min_stmt: int + segment_min_loc: int + segment_min_stmt: int + + def _current_python_version() -> str: return f"{sys.version_info.major}.{sys.version_info.minor}" @@ -75,6 +84,7 @@ class ReportMeta(TypedDict): health_grade: str | None analysis_mode: str metrics_computed: list[str] + analysis_profile: AnalysisProfileMeta design_complexity_threshold: int design_coupling_threshold: int design_cohesion_threshold: int @@ -103,6 +113,12 @@ def _build_report_meta( health_grade: str | None, analysis_mode: str, metrics_computed: tuple[str, ...], + min_loc: int, + min_stmt: int, + block_min_loc: int, + block_min_stmt: int, + segment_min_loc: int, + segment_min_stmt: int, design_complexity_threshold: int = DEFAULT_REPORT_DESIGN_COMPLEXITY_THRESHOLD, design_coupling_threshold: int = DEFAULT_REPORT_DESIGN_COUPLING_THRESHOLD, design_cohesion_threshold: int = DEFAULT_REPORT_DESIGN_COHESION_THRESHOLD, @@ -149,6 +165,14 @@ def _build_report_meta( "health_grade": health_grade, "analysis_mode": analysis_mode, "metrics_computed": list(metrics_computed), + "analysis_profile": { + "min_loc": min_loc, + "min_stmt": min_stmt, + "block_min_loc": block_min_loc, + "block_min_stmt": block_min_stmt, + "segment_min_loc": segment_min_loc, + "segment_min_stmt": segment_min_stmt, + }, "design_complexity_threshold": design_complexity_threshold, "design_coupling_threshold": design_coupling_threshold, "design_cohesion_threshold": design_cohesion_threshold, diff --git a/codeclone/_html_report/_sections/_overview.py b/codeclone/_html_report/_sections/_overview.py index bc5433e..fb073d3 100644 --- a/codeclone/_html_report/_sections/_overview.py +++ b/codeclone/_html_report/_sections/_overview.py @@ -438,12 +438,25 @@ def _scan_scope_subtitle(ctx: ReportContext) -> str: classes = _as_int(code.get("classes")) callable_total = functions + methods - return ( + scope_summary = ( f"{_format_count(total_found)} files \u00b7 " f"{_format_count(parsed_lines)} lines \u00b7 " f"{_format_count(callable_total)} callables \u00b7 " f"{_format_count(classes)} classes" ) + analysis_profile = _as_mapping(ctx.meta.get("analysis_profile")) + if not analysis_profile: + return scope_summary + return ( + f"{scope_summary}. " + "Thresholds: " + f"func {_as_int(analysis_profile.get('min_loc'))}/" + f"{_as_int(analysis_profile.get('min_stmt'))} \u00b7 " + f"block {_as_int(analysis_profile.get('block_min_loc'))}/" + f"{_as_int(analysis_profile.get('block_min_stmt'))} \u00b7 " + f"seg {_as_int(analysis_profile.get('segment_min_loc'))}/" + f"{_as_int(analysis_profile.get('segment_min_stmt'))}" + ) def _directory_hotspot_bucket_body(bucket: str, payload: Mapping[str, object]) -> str: diff --git a/codeclone/cli.py b/codeclone/cli.py index 1ed5e51..5b91a25 100644 --- a/codeclone/cli.py +++ b/codeclone/cli.py @@ -1405,6 +1405,12 @@ def _prepare_run_inputs() -> tuple[ ), analysis_mode=("clones_only" if args.skip_metrics else "full"), metrics_computed=_metrics_computed(args), + min_loc=args.min_loc, + min_stmt=args.min_stmt, + block_min_loc=args.block_min_loc, + block_min_stmt=args.block_min_stmt, + segment_min_loc=args.segment_min_loc, + segment_min_stmt=args.segment_min_stmt, design_complexity_threshold=DEFAULT_REPORT_DESIGN_COMPLEXITY_THRESHOLD, design_coupling_threshold=DEFAULT_REPORT_DESIGN_COUPLING_THRESHOLD, design_cohesion_threshold=DEFAULT_REPORT_DESIGN_COHESION_THRESHOLD, diff --git a/codeclone/contracts.py b/codeclone/contracts.py index 6f8064d..089166f 100644 --- a/codeclone/contracts.py +++ b/codeclone/contracts.py @@ -13,7 +13,7 @@ BASELINE_FINGERPRINT_VERSION: Final = "1" CACHE_VERSION: Final = "2.3" -REPORT_SCHEMA_VERSION: Final = "2.3" +REPORT_SCHEMA_VERSION: Final = "2.4" METRICS_BASELINE_SCHEMA_VERSION: Final = "1.0" DEFAULT_COMPLEXITY_THRESHOLD: Final = 20 diff --git a/codeclone/mcp_service.py b/codeclone/mcp_service.py index 884ac10..4e60af3 100644 --- a/codeclone/mcp_service.py +++ b/codeclone/mcp_service.py @@ -1076,6 +1076,12 @@ def analyze_repository(self, request: MCPAnalysisRequest) -> dict[str, object]: ), analysis_mode=request.analysis_mode, metrics_computed=self._metrics_computed(request.analysis_mode), + min_loc=_as_int(args.min_loc, DEFAULT_MIN_LOC), + min_stmt=_as_int(args.min_stmt, DEFAULT_MIN_STMT), + block_min_loc=_as_int(args.block_min_loc, DEFAULT_BLOCK_MIN_LOC), + block_min_stmt=_as_int(args.block_min_stmt, DEFAULT_BLOCK_MIN_STMT), + segment_min_loc=_as_int(args.segment_min_loc, DEFAULT_SEGMENT_MIN_LOC), + segment_min_stmt=_as_int(args.segment_min_stmt, DEFAULT_SEGMENT_MIN_STMT), design_complexity_threshold=_as_int( getattr( args, @@ -1654,7 +1660,7 @@ def get_production_triage( for row in suggestion_rows if str(row.get("source_kind", "")) == SOURCE_KIND_PRODUCTION ] - return { + payload: dict[str, object] = { "run_id": self._short_run_id(record.run_id), "health": dict(self._summary_health_payload(summary)), "cache": dict(self._as_mapping(summary.get("cache"))), @@ -1685,6 +1691,10 @@ def get_production_triage( "items": production_suggestions[:suggestion_limit], }, } + analysis_profile = self._summary_analysis_profile_payload(summary) + if analysis_profile: + payload["analysis_profile"] = analysis_profile + return payload def get_help( self, @@ -3252,6 +3262,7 @@ def _changed_analysis_payload( "run_id": self._short_run_id(record.run_id), "changed_files": len(record.changed_paths), "health": health_payload, + "analysis_profile": self._summary_analysis_profile_payload(record.summary), "health_delta": ( _as_int(changed_projection.get("health_delta", 0), 0) if changed_projection.get("health_delta") is not None @@ -3916,6 +3927,7 @@ def _build_run_summary_payload( metrics = self._as_mapping(report_document.get("metrics")) metrics_summary = self._as_mapping(metrics.get("summary")) summary = self._as_mapping(findings.get("summary")) + analysis_profile = self._summary_analysis_profile_payload(meta) payload = { "run_id": run_id, "root": str(root_path), @@ -3971,6 +3983,8 @@ def _build_run_summary_payload( "warnings": list(warnings), "failures": list(failures), } + if analysis_profile: + payload["analysis_profile"] = analysis_profile payload["cache"] = self._summary_cache_payload(payload) payload["health"] = self._summary_health_payload(payload) return payload @@ -4010,8 +4024,29 @@ def _summary_payload( "warnings": list(self._as_sequence(summary.get("warnings"))), "failures": list(self._as_sequence(summary.get("failures"))), } + analysis_profile = self._summary_analysis_profile_payload(summary) + if analysis_profile: + payload["analysis_profile"] = analysis_profile return payload + def _summary_analysis_profile_payload( + self, + summary: Mapping[str, object], + ) -> dict[str, int]: + analysis_profile = self._as_mapping(summary.get("analysis_profile")) + if not analysis_profile: + return {} + keys = ( + "min_loc", + "min_stmt", + "block_min_loc", + "block_min_stmt", + "segment_min_loc", + "segment_min_stmt", + ) + payload = {key: _as_int(analysis_profile.get(key), -1) for key in keys} + return {key: value for key, value in payload.items() if value >= 0} + def _summary_baseline_payload( self, summary: Mapping[str, object], diff --git a/codeclone/report/json_contract.py b/codeclone/report/json_contract.py index f11b242..faac269 100644 --- a/codeclone/report/json_contract.py +++ b/codeclone/report/json_contract.py @@ -150,6 +150,29 @@ def _design_findings_thresholds_payload( } +def _analysis_profile_payload( + raw_meta: Mapping[str, object] | None, +) -> dict[str, int] | None: + meta = dict(raw_meta or {}) + nested = _as_mapping(meta.get("analysis_profile")) + if nested: + meta = dict(nested) + keys = ( + "min_loc", + "min_stmt", + "block_min_loc", + "block_min_stmt", + "segment_min_loc", + "segment_min_stmt", + ) + if any(key not in meta for key in keys): + return None + payload = {key: _as_int(meta.get(key), -1) for key in keys} + if any(value < 0 for value in payload.values()): + return None + return payload + + def _normalize_path(value: str) -> str: return value.replace("\\", "/").strip() @@ -977,7 +1000,7 @@ def _build_meta_payload( scan_root=scan_root, ) ) - return { + payload: dict[str, object] = { "codeclone_version": str(meta.get("codeclone_version", "")), "project_name": str(meta.get("project_name", "")), "scan_root": ".", @@ -1039,6 +1062,10 @@ def _build_meta_payload( "metrics_baseline_path_absolute": metrics_baseline_abs, }, } + analysis_profile = _analysis_profile_payload(meta) + if analysis_profile is not None: + payload["analysis_profile"] = analysis_profile + return payload def _clone_group_assessment( diff --git a/docs/README.md b/docs/README.md index 693cc9c..b8c2337 100644 --- a/docs/README.md +++ b/docs/README.md @@ -39,7 +39,7 @@ repository build: - [Core pipeline and invariants](book/05-core-pipeline.md) - [Baseline contract (schema v2.0)](book/06-baseline.md) - [Cache contract (schema v2.3)](book/07-cache.md) -- [Report contract (schema v2.3)](book/08-report.md) +- [Report contract (schema v2.4)](book/08-report.md) ## Interfaces diff --git a/docs/architecture.md b/docs/architecture.md index 21c9697..ebbc3cc 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -144,7 +144,7 @@ gating decisions. Detected findings can be rendered as: - interactive HTML (`--html`), -- canonical JSON (`--json`, schema `2.3`), +- canonical JSON (`--json`, schema `2.4`), - deterministic text projection (`--text`), - deterministic Markdown projection (`--md`), - deterministic SARIF projection (`--sarif`). @@ -158,6 +158,7 @@ Reporting uses a layered model: Provenance is carried through `meta` and includes: - runtime/context (`codeclone_version`, `python_version`, `python_tag`, `analysis_mode`, `report_mode`) +- analysis profile (`meta.analysis_profile`) - analysis thresholds (`meta.analysis_thresholds.design_findings`) - baseline status block (`meta.baseline.*`) - cache status block (`meta.cache.*`) diff --git a/docs/book/08-report.md b/docs/book/08-report.md index 9be84ba..fd4fb6f 100644 --- a/docs/book/08-report.md +++ b/docs/book/08-report.md @@ -2,7 +2,7 @@ ## Purpose -Define report contracts in `2.0.0b4`: canonical JSON (`report_schema_version=2.3`) +Define report contracts in `2.0.0b5`: canonical JSON (`report_schema_version=2.4`) plus deterministic TXT/Markdown/SARIF projections. ## Public surface @@ -16,7 +16,7 @@ plus deterministic TXT/Markdown/SARIF projections. ## Data model -JSON report top-level (v2.3): +JSON report top-level (v2.4): - `report_schema_version` - `meta` @@ -28,6 +28,9 @@ JSON report top-level (v2.3): Canonical provenance additions: +- `meta.analysis_profile` records the effective runtime clone, block, and + segment thresholds for that run (`min_loc`, `min_stmt`, `block_*`, + `segment_*`). - `meta.analysis_thresholds.design_findings` records the effective report-level thresholds used to materialize canonical design findings for that run (`complexity > N`, `coupling > N`, `cohesion >= N`). diff --git a/docs/book/13-testing-as-spec.md b/docs/book/13-testing-as-spec.md index 192b340..c150d7a 100644 --- a/docs/book/13-testing-as-spec.md +++ b/docs/book/13-testing-as-spec.md @@ -34,7 +34,7 @@ The following matrix is treated as executable contract: | Baseline schema/integrity/compat gates | `tests/test_baseline.py` | | Cache v2.3 fail-open + status mapping | `tests/test_cache.py`, `tests/test_cli_inprocess.py::test_cli_reports_cache_too_large_respects_max_size_flag` | | Exit code categories and markers | `tests/test_cli_unit.py`, `tests/test_cli_inprocess.py` | -| Report schema v2.3 canonical/derived/integrity + JSON/TXT/MD/SARIF projections | `tests/test_report.py`, `tests/test_report_contract_coverage.py`, `tests/test_report_branch_invariants.py` | +| Report schema v2.4 canonical/derived/integrity + JSON/TXT/MD/SARIF projections | `tests/test_report.py`, `tests/test_report_contract_coverage.py`, `tests/test_report_branch_invariants.py` | | HTML render-only explainability + escaping | `tests/test_html_report.py` | | Scanner traversal safety | `tests/test_scanner_extra.py`, `tests/test_security.py` | diff --git a/docs/book/14-compatibility-and-versioning.md b/docs/book/14-compatibility-and-versioning.md index 3cf3f6c..ecf88a0 100644 --- a/docs/book/14-compatibility-and-versioning.md +++ b/docs/book/14-compatibility-and-versioning.md @@ -21,7 +21,7 @@ Current contract versions: - `BASELINE_SCHEMA_VERSION = "2.0"` - `BASELINE_FINGERPRINT_VERSION = "1"` - `CACHE_VERSION = "2.3"` -- `REPORT_SCHEMA_VERSION = "2.3"` +- `REPORT_SCHEMA_VERSION = "2.4"` - `METRICS_BASELINE_SCHEMA_VERSION = "1.0"` (used only when metrics are stored in a dedicated metrics-baseline file instead of the default unified baseline) diff --git a/docs/book/20-mcp-interface.md b/docs/book/20-mcp-interface.md index 7fe2481..169c499 100644 --- a/docs/book/20-mcp-interface.md +++ b/docs/book/20-mcp-interface.md @@ -55,7 +55,7 @@ Current server characteristics: - `off` `refresh` is rejected in MCP because the server is read-only. - summary payload: - - `run_id`, `version`, `schema`, `mode` + - `run_id`, `version`, `schema`, `mode`, compact `analysis_profile` - `baseline`, `metrics_baseline`, `cache` - `cache.freshness` classifies summary cache reuse as `fresh`, `mixed`, or `reused` @@ -96,8 +96,8 @@ Current tool set (`21` tools): |--------------------------|-----------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------| | `analyze_repository` | absolute `root`, `analysis_mode`, thresholds, cache/baseline paths | Full analysis → compact summary; then `get_run_summary` or `get_production_triage` | | `analyze_changed_paths` | absolute `root`, `changed_paths` or `git_diff_ref`, `analysis_mode` | Diff-aware analysis → compact changed-files snapshot | -| `get_run_summary` | `run_id` | Cheapest run snapshot: health, findings, baseline, inventory | -| `get_production_triage` | `run_id`, `max_hotspots`, `max_suggestions` | Production-first view: health, hotspots, suggestions | +| `get_run_summary` | `run_id` | Cheapest run snapshot: health, findings, baseline, inventory, active thresholds | +| `get_production_triage` | `run_id`, `max_hotspots`, `max_suggestions` | Production-first view: health, hotspots, suggestions, active thresholds | | `help` | `topic`, `detail` | Semantic guide for workflow, analysis profile, baseline, suppressions, review state, changed-scope | | `compare_runs` | `run_id_before`, `run_id_after`, `focus` | Run-to-run delta: regressions, improvements, health change | | `evaluate_gates` | `run_id`, gate thresholds | Preview CI gating decisions | diff --git a/docs/book/appendix/b-schema-layouts.md b/docs/book/appendix/b-schema-layouts.md index 5224e9f..3c7e209 100644 --- a/docs/book/appendix/b-schema-layouts.md +++ b/docs/book/appendix/b-schema-layouts.md @@ -2,14 +2,14 @@ ## Purpose -Compact structural layouts for baseline/cache/report contracts in `2.0.0b4`. +Compact structural layouts for baseline/cache/report contracts in `2.0.0b5`. ## Baseline schema (`2.0`) ```json { "meta": { - "generator": { "name": "codeclone", "version": "2.0.0b4" }, + "generator": { "name": "codeclone", "version": "2.0.0b5" }, "schema_version": "2.0", "fingerprint_version": "1", "python_tag": "cp313", @@ -77,17 +77,25 @@ Notes: - `u` row decoder accepts both legacy 11-column rows and canonical 17-column rows (legacy rows map new structural fields to neutral defaults). -## Report schema (`2.3`) +## Report schema (`2.4`) ```json { - "report_schema_version": "2.3", + "report_schema_version": "2.4", "meta": { - "codeclone_version": "2.0.0b4", + "codeclone_version": "2.0.0b5", "project_name": "codeclone", "scan_root": ".", "analysis_mode": "full", "report_mode": "full", + "analysis_profile": { + "min_loc": 10, + "min_stmt": 6, + "block_min_loc": 20, + "block_min_stmt": 8, + "segment_min_loc": 20, + "segment_min_stmt": 10 + }, "analysis_thresholds": { "design_findings": { "complexity": { "metric": "cyclomatic_complexity", "operator": ">", "value": 20 }, @@ -266,7 +274,7 @@ Notes: ```text # CodeClone Report - Markdown schema: 1.0 -- Source report schema: 2.3 +- Source report schema: 2.4 ... ## Overview ## Inventory @@ -297,7 +305,7 @@ Notes: "tool": { "driver": { "name": "codeclone", - "version": "2.0.0b4", + "version": "2.0.0b5", "rules": [ { "id": "CCLONE001", diff --git a/docs/mcp.md b/docs/mcp.md index e598f45..f2e1855 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -81,34 +81,34 @@ Run retention is bounded: default `4`, max `10` (`--history-limit`). If a tool request omits `processes`, MCP defers process-count policy to the core CodeClone runtime. -Current `b4` MCP surface: `21` tools, `7` fixed resources, and `3` +Current `b5` MCP surface: `21` tools, `7` fixed resources, and `3` run-scoped URI templates. ## Tool surface -| Tool | Purpose | -|--------------------------|-----------------------------------------------------------------------------------------------------| -| `analyze_repository` | Full analysis → compact summary; use `get_run_summary` or `get_production_triage` as the first pass | -| `analyze_changed_paths` | Diff-aware analysis via `changed_paths` or `git_diff_ref`; compact changed-files snapshot | -| `get_run_summary` | Cheapest run snapshot: health, findings, baseline, inventory | -| `get_production_triage` | Production-first view: health, hotspots, suggestions; best first pass for noisy repos | -| `help` | Semantic guide for workflow, analysis profile, baseline, suppressions, review state, changed-scope | -| `compare_runs` | Run-to-run delta: regressions, improvements, health change | -| `list_findings` | Filtered, paginated findings; use after hotspots or `check_*` | -| `get_finding` | Single finding detail by id; defaults to `normal` detail level | -| `get_remediation` | Remediation payload for one finding | -| `list_hotspots` | Priority-ranked hotspot views; preferred before broad listing | -| `get_report_section` | Read report sections; `metrics_detail` is paginated with family/path filters | -| `evaluate_gates` | Preview CI gating decisions | -| `check_clones` | Clone findings only; narrower than `list_findings` | -| `check_complexity` | Complexity hotspots only | -| `check_coupling` | Coupling hotspots only | -| `check_cohesion` | Cohesion hotspots only | -| `check_dead_code` | Dead-code findings only | -| `generate_pr_summary` | PR-friendly markdown or JSON summary | -| `mark_finding_reviewed` | Session-local review marker (in-memory) | -| `list_reviewed_findings` | List reviewed findings for a run | -| `clear_session_runs` | Reset in-memory runs and session state | +| Tool | Purpose | +|--------------------------|----------------------------------------------------------------------------------------------------------| +| `analyze_repository` | Full analysis → compact summary; use `get_run_summary` or `get_production_triage` as the first pass | +| `analyze_changed_paths` | Diff-aware analysis via `changed_paths` or `git_diff_ref`; compact changed-files snapshot | +| `get_run_summary` | Cheapest run snapshot: health, findings, baseline, inventory, active thresholds | +| `get_production_triage` | Production-first view: health, hotspots, suggestions, active thresholds; best first pass for noisy repos | +| `help` | Semantic guide for workflow, analysis profile, baseline, suppressions, review state, changed-scope | +| `compare_runs` | Run-to-run delta: regressions, improvements, health change | +| `list_findings` | Filtered, paginated findings; use after hotspots or `check_*` | +| `get_finding` | Single finding detail by id; defaults to `normal` detail level | +| `get_remediation` | Remediation payload for one finding | +| `list_hotspots` | Priority-ranked hotspot views; preferred before broad listing | +| `get_report_section` | Read report sections; `metrics_detail` is paginated with family/path filters | +| `evaluate_gates` | Preview CI gating decisions | +| `check_clones` | Clone findings only; narrower than `list_findings` | +| `check_complexity` | Complexity hotspots only | +| `check_coupling` | Coupling hotspots only | +| `check_cohesion` | Cohesion hotspots only | +| `check_dead_code` | Dead-code findings only | +| `generate_pr_summary` | PR-friendly markdown or JSON summary | +| `mark_finding_reviewed` | Session-local review marker (in-memory) | +| `list_reviewed_findings` | List reviewed findings for a run | +| `clear_session_runs` | Reset in-memory runs and session state | > `check_*` tools query stored runs only. Call `analyze_repository` or > `analyze_changed_paths` first. diff --git a/docs/vscode-extension.md b/docs/vscode-extension.md index d932760..c99d5bf 100644 --- a/docs/vscode-extension.md +++ b/docs/vscode-extension.md @@ -35,13 +35,13 @@ to `PATH`. Recommended install for the preview extension: ```bash -uv tool install --pre "codeclone[mcp]" +uv tool install "codeclone[mcp]>=2.0.0b4" ``` If you want the launcher inside the current environment instead: ```bash -uv pip install --pre "codeclone[mcp]" +uv pip install "codeclone[mcp]>=2.0.0b4" ``` Verify the launcher: diff --git a/extensions/vscode-codeclone/README.md b/extensions/vscode-codeclone/README.md index a168eed..57b7c8e 100644 --- a/extensions/vscode-codeclone/README.md +++ b/extensions/vscode-codeclone/README.md @@ -9,7 +9,7 @@ creating a second truth model. The extension stays read-only with respect to repository state and uses the same canonical report semantics as the CLI, HTML report, and MCP server. -This extension is published as a preview while the `2.0.0b4` line is still in +This extension is published as a preview while the `2.0.0b5` line is still in beta. ## What it is for @@ -50,13 +50,13 @@ falling back to `PATH`. Recommended install for the preview extension: ```bash -uv tool install --pre "codeclone[mcp]" +uv tool install "codeclone[mcp]>=2.0.0b4" ``` If you want the launcher inside the current environment instead: ```bash -uv pip install --pre "codeclone[mcp]" +uv pip install "codeclone[mcp]>=2.0.0b4" ``` Verify the launcher: diff --git a/pyproject.toml b/pyproject.toml index 33bbe96..6bc0987 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "codeclone" -version = "2.0.0b4" +version = "2.0.0b5" description = "Structural code quality analysis for Python" readme = { file = "README.md", content-type = "text/markdown" } license = "MPL-2.0 AND MIT" diff --git a/tests/fixtures/golden_v2/pyproject_defaults/golden_expected_cli_snapshot.json b/tests/fixtures/golden_v2/pyproject_defaults/golden_expected_cli_snapshot.json index f17a537..17a35ca 100644 --- a/tests/fixtures/golden_v2/pyproject_defaults/golden_expected_cli_snapshot.json +++ b/tests/fixtures/golden_v2/pyproject_defaults/golden_expected_cli_snapshot.json @@ -2,7 +2,7 @@ "meta": { "python_tag": "cp313" }, - "report_schema_version": "2.3", + "report_schema_version": "2.4", "project_name": "pyproject_defaults", "scan_root": ".", "baseline_status": "missing", diff --git a/tests/test_html_report.py b/tests/test_html_report.py index 0e3c5b9..a7b9360 100644 --- a/tests/test_html_report.py +++ b/tests/test_html_report.py @@ -1828,6 +1828,56 @@ def test_html_report_renders_run_snapshot_from_canonical_inventory() -> None: assert "Scan scope" not in html +def test_html_report_executive_summary_includes_effective_analysis_profile() -> None: + report_document = build_report_document( + func_groups={}, + block_groups={}, + segment_groups={}, + meta={ + "scan_root": "/repo/project", + "project_name": "project", + "min_loc": 5, + "min_stmt": 2, + "block_min_loc": 8, + "block_min_stmt": 3, + "segment_min_loc": 13, + "segment_min_stmt": 4, + }, + metrics=_metrics_payload( + health_score=82, + health_grade="B", + complexity_max=12, + complexity_high_risk=0, + coupling_high_risk=0, + cohesion_low=0, + dep_cycles=[], + dep_max_depth=2, + dead_total=0, + dead_critical=0, + ), + inventory={ + "files": {"total_found": 1, "analyzed": 1, "cached": 0, "skipped": 0}, + "code": {"parsed_lines": 20, "functions": 1, "methods": 0, "classes": 0}, + "file_list": ["/repo/project/pkg/a.py"], + }, + ) + + html = build_html_report( + func_groups={}, + block_groups={}, + segment_groups={}, + report_meta=report_document["meta"], + metrics=report_document["metrics"], + report_document=report_document, + ) + + _assert_html_contains( + html, + "Executive Summary", + "Thresholds: func 5/2 · block 8/3 · seg 13/4", + ) + + def test_html_report_metrics_without_health_score_uses_info_overview() -> None: html = build_html_report( func_groups={}, diff --git a/tests/test_mcp_service.py b/tests/test_mcp_service.py index fd51d1c..f62c7fb 100644 --- a/tests/test_mcp_service.py +++ b/tests/test_mcp_service.py @@ -256,6 +256,18 @@ def test_mcp_service_analyze_repository_registers_latest_run(tmp_path: Path) -> assert len(str(summary["run_id"])) == 8 assert summary["mode"] == "full" assert summary["schema"] == REPORT_SCHEMA_VERSION + assert cast("dict[str, int]", summary["analysis_profile"]) == { + "min_loc": 10, + "min_stmt": 6, + "block_min_loc": 20, + "block_min_stmt": 8, + "segment_min_loc": 20, + "segment_min_stmt": 10, + } + assert cast("dict[str, int]", latest["analysis_profile"]) == cast( + "dict[str, int]", + summary["analysis_profile"], + ) def test_mcp_service_help_returns_bounded_semantic_guidance() -> None: @@ -417,6 +429,14 @@ def test_mcp_service_summary_inventory_is_compact_and_report_inventory_stays_can } assert "inventory" not in changed_summary assert cast(int, changed_summary["changed_files"]) == 1 + assert cast("dict[str, int]", changed_summary["analysis_profile"]) == { + "min_loc": 10, + "min_stmt": 6, + "block_min_loc": 20, + "block_min_stmt": 8, + "segment_min_loc": 20, + "segment_min_stmt": 10, + } assert isinstance( cast("dict[str, object]", report_inventory["file_registry"])["items"], list, @@ -517,6 +537,14 @@ def test_mcp_service_hotspot_resources_and_triage_are_production_first( for item in cast("list[dict[str, object]]", top_suggestions["items"]) ) assert latest_triage["run_id"] == summary["run_id"] + assert cast("dict[str, int]", triage["analysis_profile"]) == { + "min_loc": 10, + "min_stmt": 6, + "block_min_loc": 20, + "block_min_stmt": 8, + "segment_min_loc": 20, + "segment_min_stmt": 10, + } with pytest.raises( MCPServiceContractError, match="only as codeclone://latest/triage", @@ -680,9 +708,21 @@ def test_mcp_service_granular_checks_pr_summary_and_resources( "dict[str, object]", service.get_run_summary(run_id=run_id)["health"], ) + summary_analysis_profile = cast( + "dict[str, int]", + service.get_run_summary(run_id=run_id)["analysis_profile"], + ) summary_dimensions = cast("dict[str, object]", summary_health["dimensions"]) assert clones["check"] == "clones" assert cast(int, clones["total"]) >= 1 + assert summary_analysis_profile == { + "min_loc": 10, + "min_stmt": 6, + "block_min_loc": 20, + "block_min_stmt": 8, + "segment_min_loc": 20, + "segment_min_stmt": 10, + } complexity = service.check_complexity( run_id=run_id, @@ -811,6 +851,14 @@ def test_mcp_service_clones_only_health_is_marked_unavailable( assert summary["health"] == expected assert stored_summary["health"] == expected assert triage["health"] == expected + assert cast("dict[str, int]", stored_summary["analysis_profile"]) == { + "min_loc": 10, + "min_stmt": 6, + "block_min_loc": 20, + "block_min_stmt": 8, + "segment_min_loc": 20, + "segment_min_stmt": 10, + } assert latest_health == expected diff --git a/tests/test_report_contract_coverage.py b/tests/test_report_contract_coverage.py index 5fb1822..e961243 100644 --- a/tests/test_report_contract_coverage.py +++ b/tests/test_report_contract_coverage.py @@ -530,6 +530,12 @@ def _rich_report_document() -> dict[str, object]: "python_tag": "cp313", "analysis_mode": "full", "report_mode": "full", + "min_loc": 10, + "min_stmt": 6, + "block_min_loc": 20, + "block_min_stmt": 8, + "segment_min_loc": 20, + "segment_min_stmt": 10, "baseline_loaded": True, "baseline_status": "ok", "cache_used": True, @@ -579,6 +585,16 @@ def test_report_document_rich_invariants_and_renderers() -> None: cast("dict[str, object]", payload["meta"])["analysis_thresholds"], )["design_findings"], ) + assert cast( + "dict[str, int]", cast("dict[str, object]", payload["meta"])["analysis_profile"] + ) == { + "min_loc": 10, + "min_stmt": 6, + "block_min_loc": 20, + "block_min_stmt": 8, + "segment_min_loc": 20, + "segment_min_stmt": 10, + } assert design_thresholds["complexity"] == { "metric": "cyclomatic_complexity", "operator": ">", diff --git a/uv.lock b/uv.lock index a88d798..680a19c 100644 --- a/uv.lock +++ b/uv.lock @@ -278,7 +278,7 @@ wheels = [ [[package]] name = "codeclone" -version = "2.0.0b4" +version = "2.0.0b5" source = { editable = "." } dependencies = [ { name = "orjson" }, From 6953347eb9b29ea284b40cc57cc8b3d3027640a5 Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Mon, 6 Apr 2026 21:22:30 +0500 Subject: [PATCH 02/17] feat(mcp,vscode): clarify repository health and triage focus semantics - add compact MCP interpretation fields for health_scope, focus, and new_by_source_kind across summary, production triage, and changed-scope projections - make the VS Code extension explain repository-wide health, production focus, outside-focus debt, and new-finding source-kind attribution more clearly without widening the review flow - bump the preview VS Code extension to 0.2.2 and record the UX clarification pass in its changelog --- CHANGELOG.md | 2 + README.md | 2 + codeclone/mcp_service.py | 37 ++++++++++++++++++ docs/book/01-architecture-map.md | 3 ++ docs/book/14-compatibility-and-versioning.md | 3 +- docs/book/20-mcp-interface.md | 12 ++++-- docs/mcp.md | 3 ++ extensions/vscode-codeclone/CHANGELOG.md | 5 +++ extensions/vscode-codeclone/package-lock.json | 4 +- extensions/vscode-codeclone/package.json | 2 +- extensions/vscode-codeclone/src/extension.js | 32 +++++++++++++++- extensions/vscode-codeclone/src/formatters.js | 2 +- extensions/vscode-codeclone/src/renderers.js | 11 +++++- extensions/vscode-codeclone/src/support.js | 17 +++++++++ tests/test_mcp_service.py | 38 +++++++++++++++++++ 15 files changed, 162 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c44738..c46ec5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ - Bump canonical report schema to `2.4` for `meta.analysis_profile`. - Surface the effective runtime analysis profile (`min_loc`, `min_stmt`, block, and segment thresholds) in canonical report metadata, MCP summary/triage projections, and the HTML Executive Summary subtitle. +- Clarify MCP interpretation with compact `health_scope`, `focus`, and `new_by_source_kind` fields in summary/triage + projections. - Refresh branch metadata and client docs for the `2.0.0b5` line. - Update the README repository health badge to `87 (B)`. diff --git a/README.md b/README.md index 9dfa029..4e36d6c 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,8 @@ repos: Optional read-only MCP server for AI agents and IDE clients. 21 tools + 10 resources — never mutates source, baselines, or repo state. +Compact summary and triage payloads make scope explicit: repository-wide health, +current focus, and new-finding source-kind attribution. ```bash uv tool install --pre "codeclone[mcp]" # or: uv pip install --pre "codeclone[mcp]" diff --git a/codeclone/mcp_service.py b/codeclone/mcp_service.py index 4e60af3..ad67c14 100644 --- a/codeclone/mcp_service.py +++ b/codeclone/mcp_service.py @@ -157,9 +157,15 @@ "changed", "integrity", ] +HealthScope = Literal["repository"] +SummaryFocus = Literal["repository", "production", "changed_paths"] _LEGACY_CACHE_PATH = Path("~/.cache/codeclone/cache.json").expanduser() _REPORT_DUMMY_PATH = Path(".cache/codeclone/report.json") +_HEALTH_SCOPE_REPOSITORY: Final[HealthScope] = "repository" +_FOCUS_REPOSITORY: Final[SummaryFocus] = "repository" +_FOCUS_PRODUCTION: Final[SummaryFocus] = "production" +_FOCUS_CHANGED_PATHS: Final[SummaryFocus] = "changed_paths" _MCP_CONFIG_KEYS = frozenset( { "min_loc", @@ -1662,11 +1668,20 @@ def get_production_triage( ] payload: dict[str, object] = { "run_id": self._short_run_id(record.run_id), + "focus": _FOCUS_PRODUCTION, + "health_scope": _HEALTH_SCOPE_REPOSITORY, "health": dict(self._summary_health_payload(summary)), "cache": dict(self._as_mapping(summary.get("cache"))), "findings": { "total": len(findings), "by_source_kind": findings_breakdown, + "new_by_source_kind": dict( + self._as_mapping( + self._as_mapping(summary.get("findings")).get( + "new_by_source_kind" + ) + ) + ), "outside_focus": len(findings) - findings_breakdown[SOURCE_KIND_PRODUCTION], }, @@ -3228,6 +3243,11 @@ def _build_changed_projection( known_count = sum( 1 for item in items if str(item.get("novelty", "")) == "known" ) + new_by_source_kind = self._source_kind_breakdown( + item.get("source_kind") + for item in items + if str(item.get("novelty", "")) == "new" + ) health_delta = self._summary_health_delta(record.summary) return { "run_id": self._short_run_id(record.run_id), @@ -3235,6 +3255,7 @@ def _build_changed_projection( "total": len(items), "new": new_count, "known": known_count, + "new_by_source_kind": new_by_source_kind, "items": items, "health": dict(self._summary_health_payload(record.summary)), "health_delta": health_delta, @@ -3260,6 +3281,8 @@ def _changed_analysis_payload( ) return { "run_id": self._short_run_id(record.run_id), + "focus": _FOCUS_CHANGED_PATHS, + "health_scope": _HEALTH_SCOPE_REPOSITORY, "changed_files": len(record.changed_paths), "health": health_payload, "analysis_profile": self._summary_analysis_profile_payload(record.summary), @@ -3270,6 +3293,9 @@ def _changed_analysis_payload( ), "verdict": str(changed_projection.get("verdict", "stable")), "new_findings": _as_int(changed_projection.get("new", 0), 0), + "new_by_source_kind": dict( + self._as_mapping(changed_projection.get("new_by_source_kind")) + ), "resolved_findings": 0, "changed_findings": [], } @@ -4003,6 +4029,8 @@ def _summary_payload( and not summary.get("baseline") ): return { + "focus": _FOCUS_REPOSITORY, + "health_scope": _HEALTH_SCOPE_REPOSITORY, "inventory": self._summary_inventory_payload(inventory), "health": self._summary_health_payload(summary), } @@ -4011,6 +4039,8 @@ def _summary_payload( ) payload: dict[str, object] = { "run_id": self._short_run_id(resolved_run_id) if resolved_run_id else "", + "focus": _FOCUS_REPOSITORY, + "health_scope": _HEALTH_SCOPE_REPOSITORY, "version": str(summary.get("codeclone_version", __version__)), "schema": str(summary.get("report_schema_version", REPORT_SCHEMA_VERSION)), "mode": str(summary.get("analysis_mode", "")), @@ -4149,6 +4179,7 @@ def _summary_findings_payload( "known": 0, "by_family": {}, "production": 0, + "new_by_source_kind": self._source_kind_breakdown(()), } findings = self._base_findings(record) by_family: dict[str, int] = { @@ -4160,6 +4191,11 @@ def _summary_findings_payload( new_count = 0 known_count = 0 production_count = 0 + new_by_source_kind = self._source_kind_breakdown( + self._finding_source_kind(finding) + for finding in findings + if str(finding.get("novelty", "")).strip() == "new" + ) for finding in findings: family = str(finding.get("family", "")).strip() family_key = "clones" if family == FAMILY_CLONE else family @@ -4177,6 +4213,7 @@ def _summary_findings_payload( "known": known_count, "by_family": {key: value for key, value in by_family.items() if value > 0}, "production": production_count, + "new_by_source_kind": new_by_source_kind, } def _summary_diff_payload( diff --git a/docs/book/01-architecture-map.md b/docs/book/01-architecture-map.md index 65d2c04..7441142 100644 --- a/docs/book/01-architecture-map.md +++ b/docs/book/01-architecture-map.md @@ -68,6 +68,9 @@ Refs: - The same rule applies to summary cache convenience fields such as `freshness` and to production-first triage projections built from canonical hotlists/suggestions. +- The same rule also applies to compact interpretation hints such as + `health_scope`, `focus`, and `new_by_source_kind`: they clarify projection + meaning without introducing a second report truth. - MCP finding lists may also expose short run/finding ids and slimmer relative location projections, while keeping `get_finding(detail_level="full")` as the richer per-finding inspection path. diff --git a/docs/book/14-compatibility-and-versioning.md b/docs/book/14-compatibility-and-versioning.md index ecf88a0..2a20277 100644 --- a/docs/book/14-compatibility-and-versioning.md +++ b/docs/book/14-compatibility-and-versioning.md @@ -56,7 +56,8 @@ Version bump rules: short MCP ids, slim summary locations, or omitting `priority_factors` outside `detail_level="full"`. - Additive MCP-only convenience fields/projections such as - `cache.freshness` or production-first triage also do not change + `cache.freshness`, production-first triage, `health_scope`, `focus`, or + `new_by_source_kind` also do not change `report_schema_version` when they are derived from unchanged canonical report and summary data. - The same rule applies to bounded MCP semantic guidance such as diff --git a/docs/book/20-mcp-interface.md b/docs/book/20-mcp-interface.md index 169c499..6f230fa 100644 --- a/docs/book/20-mcp-interface.md +++ b/docs/book/20-mcp-interface.md @@ -56,17 +56,21 @@ Current server characteristics: `refresh` is rejected in MCP because the server is read-only. - summary payload: - `run_id`, `version`, `schema`, `mode`, compact `analysis_profile` + - `health_scope` explains what the health score covers + - `focus` explains the active summary/triage lens - `baseline`, `metrics_baseline`, `cache` - `cache.freshness` classifies summary cache reuse as `fresh`, `mixed`, or `reused` - flattened `inventory` (`files`, `lines`, `functions`, `classes`) - - flattened `findings` (`total`, `new`, `known`, `by_family`, `production`) + - flattened `findings` (`total`, `new`, `known`, `by_family`, `production`, + `new_by_source_kind`) - flattened `diff` (`new_clones`, `health_delta`) - `warnings`, `failures` - `analyze_changed_paths` is intentionally more compact than `get_run_summary`: - it returns `changed_files`, `health`, `health_delta`, `verdict`, - `new_findings`, `resolved_findings`, and an empty `changed_findings` - placeholder, while detailed changed payload stays in + it returns `changed_files`, `focus`, `health_scope`, `health`, + `health_delta`, `verdict`, `new_findings`, `new_by_source_kind`, + `resolved_findings`, and an empty `changed_findings` placeholder, while + detailed changed payload stays in `get_report_section(section="changed")` - workflow guidance: - the MCP surface is intentionally agent-guiding rather than list-first diff --git a/docs/mcp.md b/docs/mcp.md index f2e1855..88019e5 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -118,6 +118,9 @@ run-scoped URI templates. - `check_*` responses include only the relevant health dimension. - Finding responses use short MCP IDs and relative paths by default; `detail_level=full` restores the compatibility payload with URIs. +- Summary and triage projections keep interpretation compact: `health_scope` + explains what the health score covers, `focus` explains the active view, and + `new_by_source_kind` attributes new findings without widening the payload. - Run IDs are 8-char hex handles; finding IDs are short prefixed forms. Both accept the full canonical form as input. - `metrics_detail(family="overloaded_modules")` exposes the report-only diff --git a/extensions/vscode-codeclone/CHANGELOG.md b/extensions/vscode-codeclone/CHANGELOG.md index 6e0032b..1ea4e80 100644 --- a/extensions/vscode-codeclone/CHANGELOG.md +++ b/extensions/vscode-codeclone/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## 0.2.2 + +- surface repository-vs-focus semantics more clearly in triage and summary UX +- explain new findings by source kind without widening the review flow + ## 0.2.1 - refresh packaged extension metadata for prerelease validation diff --git a/extensions/vscode-codeclone/package-lock.json b/extensions/vscode-codeclone/package-lock.json index e0c0592..cfcab9f 100644 --- a/extensions/vscode-codeclone/package-lock.json +++ b/extensions/vscode-codeclone/package-lock.json @@ -1,12 +1,12 @@ { "name": "codeclone", - "version": "0.2.1", + "version": "0.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codeclone", - "version": "0.2.1", + "version": "0.2.2", "license": "MPL-2.0", "devDependencies": { "@types/node": "^25.5.2", diff --git a/extensions/vscode-codeclone/package.json b/extensions/vscode-codeclone/package.json index 24485b1..a3e8056 100644 --- a/extensions/vscode-codeclone/package.json +++ b/extensions/vscode-codeclone/package.json @@ -2,7 +2,7 @@ "name": "codeclone", "displayName": "CodeClone", "description": "Baseline-aware, triage-first structural review for Python, powered by CodeClone MCP.", - "version": "0.2.1", + "version": "0.2.2", "preview": true, "publisher": "orenlab", "license": "MPL-2.0", diff --git a/extensions/vscode-codeclone/src/extension.js b/extensions/vscode-codeclone/src/extension.js index f47f8cc..0426e3e 100644 --- a/extensions/vscode-codeclone/src/extension.js +++ b/extensions/vscode-codeclone/src/extension.js @@ -2302,7 +2302,10 @@ class CodeCloneController { { nodeType: "section", id: "overview.health", - label: "Structural Health", + label: + String(state.latestSummary.health_scope || "repository") === "repository" + ? "Repository Health" + : "Structural Health", description: baselineDrift.healthDelta !== null ? `${state.latestSummary.health.score}/${state.latestSummary.health.grade} · ${signedInteger( @@ -2354,6 +2357,10 @@ class CodeCloneController { if (node.id === "overview.health") { const dimensions = safeObject(state.latestSummary.health.dimensions); return [ + this.detailNode( + "Scope", + capitalize(String(state.latestSummary.health_scope || "repository")) + ), this.detailNode("Score", `${state.latestSummary.health.score}/${state.latestSummary.health.grade}`), this.detailNode("Clones", number(dimensions.clones)), this.detailNode("Complexity", number(dimensions.complexity)), @@ -2412,15 +2419,30 @@ class CodeCloneController { } if (node.id === "overview.triage") { const nextAction = this.describeNextBestAction(state); + const triageFindings = safeObject(state.latestTriage?.findings); + const summaryFindings = safeObject(state.latestSummary.findings); return [ this.detailNode("Next best action", nextAction.label), this.detailNode("Focus mode", focusModeSpec(this.hotspotFocusMode).label), + this.detailNode( + "Focus", + capitalize(String(state.latestTriage?.focus || "production").replace(/_/g, " ")) + ), + this.detailNode( + "Health scope", + capitalize(String(state.latestSummary.health_scope || "repository")) + ), this.detailNode( "Analysis depth", currentAnalysisSettings ? currentAnalysisSettings.label : "unknown" ), this.detailNode("New regressions", number(reviewCounts.new)), + this.detailNode( + "New by source kind", + formatSourceKindSummary(summaryFindings.new_by_source_kind) + ), this.detailNode("Production hotspots", number(reviewCounts.production)), + this.detailNode("Outside focus", number(triageFindings.outside_focus)), this.detailNode( "New clones", baselineDrift.newClones !== null @@ -2438,9 +2460,17 @@ class CodeCloneController { } if (node.id === "overview.changed") { return [ + this.detailNode( + "Focus", + capitalize(String(state.changedSummary.focus || "changed_paths").replace(/_/g, " ")) + ), this.detailNode("Changed files", number(state.changedSummary.changed_files)), this.detailNode("Verdict", String(state.changedSummary.verdict)), this.detailNode("New findings", number(state.changedSummary.new_findings)), + this.detailNode( + "New by source kind", + formatSourceKindSummary(state.changedSummary.new_by_source_kind) + ), this.detailNode("Resolved findings", number(state.changedSummary.resolved_findings)), this.detailNode( "Health delta", diff --git a/extensions/vscode-codeclone/src/formatters.js b/extensions/vscode-codeclone/src/formatters.js index ed3f35a..228f4cf 100644 --- a/extensions/vscode-codeclone/src/formatters.js +++ b/extensions/vscode-codeclone/src/formatters.js @@ -80,7 +80,7 @@ function formatSourceKindSummary(value) { .filter(([, count]) => typeof count === "number" && count > 0) .sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey)); if (entries.length === 0) { - return "No production findings by source kind."; + return "none"; } return entries .map(([key, count]) => `${capitalize(key)} ${count}`) diff --git a/extensions/vscode-codeclone/src/renderers.js b/extensions/vscode-codeclone/src/renderers.js index 32ba46c..aa8d1c1 100644 --- a/extensions/vscode-codeclone/src/renderers.js +++ b/extensions/vscode-codeclone/src/renderers.js @@ -199,6 +199,13 @@ function renderTriageMarkdown(state) { const triageFindings = safeObject(triage.findings); const topHotspots = safeObject(triage.top_hotspots); const topSuggestions = safeObject(triage.top_suggestions); + const focus = capitalize(String(triage.focus || "production").replace(/_/g, " ")); + const healthScope = capitalize( + String(summary.health_scope || triage.health_scope || "repository").replace( + /_/g, + " " + ) + ); const items = safeArray(topHotspots.items); const suggestions = safeArray(topSuggestions.items); const lines = [ @@ -206,8 +213,10 @@ function renderTriageMarkdown(state) { "", `- Run: \`${state.currentRunId || "n/a"}\``, `- Workspace: \`${state.folder.name}\``, - `- Health: ${health.score || 0}/${health.grade || "?"}`, + `- Health: ${health.score || 0}/${health.grade || "?"} · ${healthScope} scope`, + `- Focus: ${focus} · ${Number(triageFindings.outside_focus || 0)} outside focus`, `- Findings: ${findings.total || 0} total · ${findings.production || 0} production`, + `- New findings: ${formatSourceKindSummary(findings.new_by_source_kind)}`, `- Source kinds: ${formatSourceKindSummary(triageFindings.by_source_kind)}`, ]; if (items.length > 0) { diff --git a/extensions/vscode-codeclone/src/support.js b/extensions/vscode-codeclone/src/support.js index fd436d1..04c36a9 100644 --- a/extensions/vscode-codeclone/src/support.js +++ b/extensions/vscode-codeclone/src/support.js @@ -217,6 +217,18 @@ function isMinimumSupportedCodeCloneVersion( return comparison !== null && comparison >= 0; } +/** + * @typedef {{ + * command?: string, + * args?: string[], + * cwd?: string, + * source?: string, + * }} LaunchSpecLike + */ + +/** + * @param {LaunchSpecLike | null | undefined} spec + */ function launchSpecOrigin(spec) { const launchSpec = spec || {}; const command = String(launchSpec.command || "").trim() || "codeclone-mcp"; @@ -237,6 +249,11 @@ function launchSpecOrigin(spec) { } } +/** + * @param {string} reportedVersion + * @param {string} [minimum] + * @param {LaunchSpecLike | null | undefined} [launchSpec] + */ function unsupportedVersionMessage( reportedVersion, minimum = MINIMUM_SUPPORTED_CODECLONE_VERSION, diff --git a/tests/test_mcp_service.py b/tests/test_mcp_service.py index f62c7fb..6625a4c 100644 --- a/tests/test_mcp_service.py +++ b/tests/test_mcp_service.py @@ -254,6 +254,8 @@ def test_mcp_service_analyze_repository_registers_latest_run(tmp_path: Path) -> latest = service.get_run_summary() assert summary["run_id"] == latest["run_id"] assert len(str(summary["run_id"])) == 8 + assert summary["focus"] == "repository" + assert summary["health_scope"] == "repository" assert summary["mode"] == "full" assert summary["schema"] == REPORT_SCHEMA_VERSION assert cast("dict[str, int]", summary["analysis_profile"]) == { @@ -268,6 +270,13 @@ def test_mcp_service_analyze_repository_registers_latest_run(tmp_path: Path) -> "dict[str, int]", summary["analysis_profile"], ) + assert ( + cast("dict[str, object]", summary["findings"])["new_by_source_kind"] + == cast( + "dict[str, object]", + latest["findings"], + )["new_by_source_kind"] + ) def test_mcp_service_help_returns_bounded_semantic_guidance() -> None: @@ -428,6 +437,8 @@ def test_mcp_service_summary_inventory_is_compact_and_report_inventory_stays_can "classes", } assert "inventory" not in changed_summary + assert changed_summary["focus"] == "changed_paths" + assert changed_summary["health_scope"] == "repository" assert cast(int, changed_summary["changed_files"]) == 1 assert cast("dict[str, int]", changed_summary["analysis_profile"]) == { "min_loc": 10, @@ -437,6 +448,9 @@ def test_mcp_service_summary_inventory_is_compact_and_report_inventory_stays_can "segment_min_loc": 20, "segment_min_stmt": 10, } + assert sum( + cast("dict[str, int]", changed_summary["new_by_source_kind"]).values() + ) == cast(int, changed_summary["new_findings"]) assert isinstance( cast("dict[str, object]", report_inventory["file_registry"])["items"], list, @@ -517,9 +531,17 @@ def test_mcp_service_hotspot_resources_and_triage_are_production_first( production_items = cast("list[dict[str, object]]", production_hotspots["items"]) assert triage["run_id"] == summary["run_id"] + assert triage["focus"] == "production" + assert triage["health_scope"] == "repository" assert _mapping_child(triage, "cache")["freshness"] == "fresh" assert findings_breakdown["production"] >= 1 assert findings_breakdown["tests"] >= 1 + assert sum( + cast("dict[str, int]", triage_findings["new_by_source_kind"]).values() + ) == cast( + int, + cast("dict[str, object]", summary["findings"])["new"], + ) assert cast(int, triage_findings["outside_focus"]) >= 1 assert suggestions_breakdown["production"] >= 1 assert suggestions_breakdown["tests"] >= 1 @@ -2730,6 +2752,8 @@ def test_mcp_service_helper_branches_for_empty_gate_and_missing_remediation( service.get_report_section(run_id="helpers", section="findings") assert service._summary_payload({"inventory": {}}) == { + "focus": "repository", + "health_scope": "repository", "inventory": {}, "health": {"available": False, "reason": "unavailable"}, } @@ -3271,6 +3295,13 @@ def test_mcp_service_summary_and_metrics_detail_helper_fallbacks( "known": 0, "by_family": {}, "production": 0, + "new_by_source_kind": { + "production": 0, + "tests": 0, + "fixtures": 0, + "mixed": 0, + "other": 0, + }, } record = _dummy_run_record(tmp_path, "summary-helper") @@ -3292,6 +3323,13 @@ def test_mcp_service_summary_and_metrics_detail_helper_fallbacks( "known": 1, "by_family": {}, "production": 0, + "new_by_source_kind": { + "production": 0, + "tests": 0, + "fixtures": 0, + "mixed": 0, + "other": 0, + }, } metrics_payload = service._metrics_detail_payload( From 333ccc8e9459dfd4d409bdf938001716190d144d Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Tue, 7 Apr 2026 17:16:46 +0500 Subject: [PATCH 03/17] feat(mcp,vscode): clarify repository health and triage focus semantics - add compact MCP interpretation fields for health_scope, focus, and new_by_source_kind across summary, production triage, and changed-scope projections - make the VS Code extension explain repository-wide health, production focus, outside-focus debt, and new-finding source-kind attribution more clearly without widening the review flow - bump the preview VS Code extension to 0.2.2 and record the UX clarification pass in its changelog --- CHANGELOG.md | 4 + README.md | 3 +- codeclone/mcp_service.py | 18 ++++- docs/book/20-mcp-interface.md | 10 ++- docs/book/21-vscode-extension.md | 2 +- docs/book/23-codex-plugin.md | 4 +- docs/codex-plugin.md | 16 +++- docs/mcp.md | 2 + docs/vscode-extension.md | 2 +- .../claude-desktop-codeclone/manifest.json | 2 +- .../package-lock.json | 4 +- .../claude-desktop-codeclone/package.json | 2 +- extensions/vscode-codeclone/CHANGELOG.md | 5 ++ extensions/vscode-codeclone/README.md | 2 +- extensions/vscode-codeclone/package-lock.json | 4 +- extensions/vscode-codeclone/package.json | 2 +- extensions/vscode-codeclone/src/extension.js | 25 +++++- extensions/vscode-codeclone/src/formatters.js | 21 ++++- extensions/vscode-codeclone/src/renderers.js | 8 ++ .../vscode-codeclone/test/renderers.test.js | 80 +++++++++++++++++++ plugins/codeclone/.codex-plugin/plugin.json | 2 +- plugins/codeclone/.mcp.json | 6 +- plugins/codeclone/README.md | 23 ++++-- tests/test_codex_plugin.py | 21 ++--- tests/test_mcp_service.py | 37 +++++++++ uv.lock | 12 +-- 26 files changed, 269 insertions(+), 48 deletions(-) create mode 100644 extensions/vscode-codeclone/test/renderers.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index c46ec5a..f3094b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ report metadata, MCP summary/triage projections, and the HTML Executive Summary subtitle. - Clarify MCP interpretation with compact `health_scope`, `focus`, and `new_by_source_kind` fields in summary/triage projections. +- Make baseline mismatch handling more explicit in MCP and the VS Code client by surfacing baseline/runtime python tags + and whether comparison is proceeding without a valid baseline. +- Make the Codex plugin prefer workspace-local launchers before `PATH`, with Poetry environment fallback for + python-tag-safe MCP startup. - Refresh branch metadata and client docs for the `2.0.0b5` line. - Update the README repository health badge to `87 (B)`. diff --git a/README.md b/README.md index 4e36d6c..3a6a260 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,8 @@ repos: Optional read-only MCP server for AI agents and IDE clients. 21 tools + 10 resources — never mutates source, baselines, or repo state. Compact summary and triage payloads make scope explicit: repository-wide health, -current focus, and new-finding source-kind attribution. +current focus, new-finding source-kind attribution, and when comparison is +proceeding without a valid baseline. ```bash uv tool install --pre "codeclone[mcp]" # or: uv pip install --pre "codeclone[mcp]" diff --git a/codeclone/mcp_service.py b/codeclone/mcp_service.py index ad67c14..8f10d14 100644 --- a/codeclone/mcp_service.py +++ b/codeclone/mcp_service.py @@ -1670,6 +1670,7 @@ def get_production_triage( "run_id": self._short_run_id(record.run_id), "focus": _FOCUS_PRODUCTION, "health_scope": _HEALTH_SCOPE_REPOSITORY, + "baseline": dict(self._as_mapping(summary.get("baseline"))), "health": dict(self._summary_health_payload(summary)), "cache": dict(self._as_mapping(summary.get("cache"))), "findings": { @@ -3283,6 +3284,7 @@ def _changed_analysis_payload( "run_id": self._short_run_id(record.run_id), "focus": _FOCUS_CHANGED_PATHS, "health_scope": _HEALTH_SCOPE_REPOSITORY, + "baseline": dict(self._summary_baseline_payload(record.summary)), "changed_files": len(record.changed_paths), "health": health_payload, "analysis_profile": self._summary_analysis_profile_payload(record.summary), @@ -3959,6 +3961,7 @@ def _build_run_summary_payload( "root": str(root_path), "analysis_mode": request.analysis_mode, "codeclone_version": meta.get("codeclone_version", __version__), + "python_tag": str(meta.get("python_tag", "")), "report_schema_version": report_document.get( "report_schema_version", REPORT_SCHEMA_VERSION, @@ -3971,6 +3974,7 @@ def _build_run_summary_payload( "loaded": bool(meta_baseline.get("loaded", baseline_state.loaded)), "status": str(meta_baseline.get("status", baseline_state.status.value)), "trusted_for_diff": baseline_state.trusted_for_diff, + "python_tag": meta_baseline.get("python_tag"), }, "metrics_baseline": { "path": meta_metrics_baseline.get( @@ -4096,11 +4100,21 @@ def _summary_trusted_state_payload( key: str, ) -> dict[str, object]: baseline = self._as_mapping(summary.get(key)) - return { + trusted = bool(baseline.get("trusted_for_diff", False)) + payload: dict[str, object] = { "loaded": bool(baseline.get("loaded", False)), "status": str(baseline.get("status", "")), - "trusted": bool(baseline.get("trusted_for_diff", False)), + "trusted": trusted, } + if key == "baseline": + payload["compared_without_valid_baseline"] = not trusted + baseline_python_tag = baseline.get("python_tag") + runtime_python_tag = summary.get("python_tag") + if isinstance(baseline_python_tag, str) and baseline_python_tag.strip(): + payload["baseline_python_tag"] = baseline_python_tag + if isinstance(runtime_python_tag, str) and runtime_python_tag.strip(): + payload["runtime_python_tag"] = runtime_python_tag + return payload def _summary_cache_payload( self, diff --git a/docs/book/20-mcp-interface.md b/docs/book/20-mcp-interface.md index 6f230fa..5b56fab 100644 --- a/docs/book/20-mcp-interface.md +++ b/docs/book/20-mcp-interface.md @@ -59,6 +59,9 @@ Current server characteristics: - `health_scope` explains what the health score covers - `focus` explains the active summary/triage lens - `baseline`, `metrics_baseline`, `cache` + - untrusted baseline comparisons stay compact but explicit through + `baseline.compared_without_valid_baseline`, + `baseline.baseline_python_tag`, and `baseline.runtime_python_tag` - `cache.freshness` classifies summary cache reuse as `fresh`, `mixed`, or `reused` - flattened `inventory` (`files`, `lines`, `functions`, `classes`) @@ -67,9 +70,10 @@ Current server characteristics: - flattened `diff` (`new_clones`, `health_delta`) - `warnings`, `failures` - `analyze_changed_paths` is intentionally more compact than `get_run_summary`: - it returns `changed_files`, `focus`, `health_scope`, `health`, - `health_delta`, `verdict`, `new_findings`, `new_by_source_kind`, - `resolved_findings`, and an empty `changed_findings` placeholder, while + it returns `changed_files`, compact `baseline`, `focus`, `health_scope`, + `health`, `health_delta`, `verdict`, `new_findings`, + `new_by_source_kind`, `resolved_findings`, and an empty + `changed_findings` placeholder, while detailed changed payload stays in `get_report_section(section="changed")` - workflow guidance: diff --git a/docs/book/21-vscode-extension.md b/docs/book/21-vscode-extension.md index 7a766fa..02c1c26 100644 --- a/docs/book/21-vscode-extension.md +++ b/docs/book/21-vscode-extension.md @@ -123,7 +123,7 @@ The extension runs as a workspace extension and requires: - CodeClone `2.0.0b4` or newer In `auto` mode, launcher resolution prefers the current workspace virtualenv -before `PATH`. +before `PATH`. Runtime and version-mismatch messages identify that resolved launcher source. For this reason: diff --git a/docs/book/23-codex-plugin.md b/docs/book/23-codex-plugin.md index ebb8a70..5978c80 100644 --- a/docs/book/23-codex-plugin.md +++ b/docs/book/23-codex-plugin.md @@ -46,6 +46,7 @@ The plugin currently provides: The plugin surface is additive: - `.mcp.json` contributes a local stdio MCP server definition +- that launcher prefers a workspace `.venv`, then a Poetry env, then `PATH` - the skill contributes workflow guidance and starter prompts - `README.md` documents local usage and boundaries inside the repository tree - Codex remains free to use direct `mcp add` config alongside or instead of the @@ -65,7 +66,8 @@ The plugin does not rewrite user config or install CodeClone automatically. - **Repo-local clarity**: the plugin is meant to travel with the repository as a native Codex surface. - **Launcher honesty**: the plugin assumes `codeclone-mcp` is already - installable or configured in the local environment. + installable in the current workspace or reachable on `PATH`, and prefers the + workspace environment when one is present. ## Relationship to other interfaces diff --git a/docs/codex-plugin.md b/docs/codex-plugin.md index 46baa71..2688ad3 100644 --- a/docs/codex-plugin.md +++ b/docs/codex-plugin.md @@ -8,7 +8,7 @@ Repo-local discovery via `.agents/plugins/marketplace.json`. | File | Purpose | |------------------------------|----------------------------------------------------| | `.codex-plugin/plugin.json` | Plugin metadata, prompts, instructions | -| `.mcp.json` | Local `codeclone-mcp --transport stdio` definition | +| `.mcp.json` | Workspace-first MCP launcher definition | | `skills/codeclone-review/` | Conservative-first full review skill | | `skills/codeclone-hotspots/` | Quick hotspot discovery skill | | `assets/` | Plugin branding | @@ -16,8 +16,16 @@ Repo-local discovery via `.agents/plugins/marketplace.json`. ## Install ```bash -uv tool install --pre "codeclone[mcp]" # or: uv pip install --pre "codeclone[mcp]" -codeclone-mcp --help # verify +uv venv +uv pip install --python .venv/bin/python "codeclone[mcp]>=2.0.0b4" +.venv/bin/codeclone-mcp --help +``` + +Global fallback: + +```bash +uv tool install "codeclone[mcp]>=2.0.0b4" +codeclone-mcp --help ``` Manual MCP registration without the plugin: @@ -36,7 +44,7 @@ gets a local MCP definition and two skills. Does not mutate - if you already registered `codeclone-mcp` manually, keep only one setup path to avoid duplicate MCP surfaces -- the bundled `.mcp.json` assumes `codeclone-mcp` resolves on `PATH` +- the bundled `.mcp.json` prefers `.venv`, then a Poetry env, then `PATH` For the underlying interface contract, see: diff --git a/docs/mcp.md b/docs/mcp.md index 88019e5..90ce7f1 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -121,6 +121,8 @@ run-scoped URI templates. - Summary and triage projections keep interpretation compact: `health_scope` explains what the health score covers, `focus` explains the active view, and `new_by_source_kind` attributes new findings without widening the payload. +- When baseline comparison is untrusted, summary and triage also expose + `baseline.compared_without_valid_baseline` plus baseline/runtime python tags. - Run IDs are 8-char hex handles; finding IDs are short prefixed forms. Both accept the full canonical form as input. - `metrics_detail(family="overloaded_modules")` exposes the report-only diff --git a/docs/vscode-extension.md b/docs/vscode-extension.md index c99d5bf..f85f831 100644 --- a/docs/vscode-extension.md +++ b/docs/vscode-extension.md @@ -30,7 +30,7 @@ The extension needs a local `codeclone-mcp` launcher. Minimum supported CodeClone version: `2.0.0b4`. In `auto` mode, it checks the current workspace virtualenv before falling back -to `PATH`. +to `PATH`. Runtime and version-mismatch messages identify that resolved launcher source. Recommended install for the preview extension: diff --git a/extensions/claude-desktop-codeclone/manifest.json b/extensions/claude-desktop-codeclone/manifest.json index 4d89fa0..62787c1 100644 --- a/extensions/claude-desktop-codeclone/manifest.json +++ b/extensions/claude-desktop-codeclone/manifest.json @@ -2,7 +2,7 @@ "manifest_version": "0.3", "name": "codeclone", "display_name": "CodeClone", - "version": "2.0.0-b4.0", + "version": "2.0.0-b5.0", "description": "Baseline-aware structural review for Claude Desktop through a local CodeClone MCP launcher.", "long_description": "CodeClone for Claude Desktop wraps the local codeclone-mcp launcher as an MCP bundle. It keeps Claude on the same canonical MCP surface used by the CLI, HTML report, VS Code extension, and Codex plugin — read-only, baseline-aware, local stdio only.", "author": { diff --git a/extensions/claude-desktop-codeclone/package-lock.json b/extensions/claude-desktop-codeclone/package-lock.json index 27c142c..341c057 100644 --- a/extensions/claude-desktop-codeclone/package-lock.json +++ b/extensions/claude-desktop-codeclone/package-lock.json @@ -1,12 +1,12 @@ { "name": "@orenlab/codeclone-claude-desktop", - "version": "2.0.0-b4.0", + "version": "2.0.0-b5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@orenlab/codeclone-claude-desktop", - "version": "2.0.0-b4.0", + "version": "2.0.0-b5.0", "license": "MPL-2.0", "engines": { "node": ">=20.0.0" diff --git a/extensions/claude-desktop-codeclone/package.json b/extensions/claude-desktop-codeclone/package.json index 81d745d..234e434 100644 --- a/extensions/claude-desktop-codeclone/package.json +++ b/extensions/claude-desktop-codeclone/package.json @@ -1,6 +1,6 @@ { "name": "@orenlab/codeclone-claude-desktop", - "version": "2.0.0-b4.0", + "version": "2.0.0-b5.0", "private": true, "description": "Claude Desktop MCP bundle wrapper for the local CodeClone MCP launcher.", "license": "MPL-2.0", diff --git a/extensions/vscode-codeclone/CHANGELOG.md b/extensions/vscode-codeclone/CHANGELOG.md index 1ea4e80..525fb2c 100644 --- a/extensions/vscode-codeclone/CHANGELOG.md +++ b/extensions/vscode-codeclone/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## 0.2.3 + +- explain baseline mismatch runs more clearly with compact baseline/runtime tag context +- surface runtime source in the session view and alongside baseline-mismatch run details + ## 0.2.2 - surface repository-vs-focus semantics more clearly in triage and summary UX diff --git a/extensions/vscode-codeclone/README.md b/extensions/vscode-codeclone/README.md index 57b7c8e..a813e94 100644 --- a/extensions/vscode-codeclone/README.md +++ b/extensions/vscode-codeclone/README.md @@ -45,7 +45,7 @@ CodeClone for VS Code needs a local `codeclone-mcp` launcher. Minimum supported CodeClone version: `2.0.0b4`. In `auto` mode, the extension checks the current workspace virtualenv before -falling back to `PATH`. +falling back to `PATH`. Runtime and version-mismatch messages identify that resolved launcher source. Recommended install for the preview extension: diff --git a/extensions/vscode-codeclone/package-lock.json b/extensions/vscode-codeclone/package-lock.json index cfcab9f..10fc5af 100644 --- a/extensions/vscode-codeclone/package-lock.json +++ b/extensions/vscode-codeclone/package-lock.json @@ -1,12 +1,12 @@ { "name": "codeclone", - "version": "0.2.2", + "version": "0.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codeclone", - "version": "0.2.2", + "version": "0.2.3", "license": "MPL-2.0", "devDependencies": { "@types/node": "^25.5.2", diff --git a/extensions/vscode-codeclone/package.json b/extensions/vscode-codeclone/package.json index a3e8056..2994a39 100644 --- a/extensions/vscode-codeclone/package.json +++ b/extensions/vscode-codeclone/package.json @@ -2,7 +2,7 @@ "name": "codeclone", "displayName": "CodeClone", "description": "Baseline-aware, triage-first structural review for Python, powered by CodeClone MCP.", - "version": "0.2.2", + "version": "0.2.3", "preview": true, "publisher": "orenlab", "license": "MPL-2.0", diff --git a/extensions/vscode-codeclone/src/extension.js b/extensions/vscode-codeclone/src/extension.js index 0426e3e..0bf7c34 100644 --- a/extensions/vscode-codeclone/src/extension.js +++ b/extensions/vscode-codeclone/src/extension.js @@ -23,6 +23,7 @@ const { findingIcon, firstNormalizedLocation, focusModeSpec, + formatBaselineTags, formatBaselineState, formatBooleanWord, formatCacheSummary, @@ -76,6 +77,7 @@ const { STALE_REASON_EDITOR, STALE_REASON_WORKSPACE, isMinimumSupportedCodeCloneVersion, + launchSpecOrigin, resolveAnalysisSettings, sameAnalysisSettings, locationsNeedDetailHydration, @@ -1275,7 +1277,12 @@ class CodeCloneController { if (drift.healthDelta !== null) { parts.push(`${signedInteger(drift.healthDelta)} health`); } - return parts.length > 0 ? parts.join(" · ") : "baseline unavailable"; + if (parts.length > 0) { + return parts.join(" · "); + } + return safeObject(state?.latestSummary?.baseline).compared_without_valid_baseline + ? "comparing without valid baseline" + : "baseline unavailable"; } async inspectLocalHtmlReport(state) { @@ -2379,6 +2386,9 @@ class CodeCloneController { } if (node.id === "overview.run") { const inventory = safeObject(state.latestSummary.inventory); + const baseline = safeObject(state.latestSummary.baseline); + const baselineTags = formatBaselineTags(baseline); + const launch = this.connectionInfo.launchSpec; return [ this.detailNode("Workspace", state.folder.name), this.detailNode("Run ID", state.currentRunId), @@ -2408,7 +2418,14 @@ class CodeCloneController { this.detailNode("Parsed lines", number(inventory.lines)), this.detailNode("Callables", number(inventory.functions)), this.detailNode("Classes", number(inventory.classes)), - this.detailNode("Baseline", formatBaselineState(state.latestSummary.baseline)), + this.detailNode("Baseline", formatBaselineState(baseline)), + ...(baseline.compared_without_valid_baseline && + baselineTags !== "unknown" + ? [this.detailNode("Baseline tags", baselineTags)] + : []), + ...(baseline.compared_without_valid_baseline && launch + ? [this.detailNode("Runtime source", launchSpecOrigin(launch))] + : []), this.detailNode( "Metrics baseline", formatBaselineState(state.latestSummary.metrics_baseline) @@ -2581,6 +2598,10 @@ class CodeCloneController { this.connectionInfo.serverInfo ? this.connectionInfo.serverInfo.version : "unknown" ), this.detailNode("Available tools", number(this.connectionInfo.toolCount)), + this.detailNode( + "Runtime source", + launch ? launchSpecOrigin(launch) : "not started" + ), this.detailNode( "Launcher", launch ? `${launch.command} ${launch.args.join(" ")}`.trim() : "not started" diff --git a/extensions/vscode-codeclone/src/formatters.js b/extensions/vscode-codeclone/src/formatters.js index 228f4cf..7498884 100644 --- a/extensions/vscode-codeclone/src/formatters.js +++ b/extensions/vscode-codeclone/src/formatters.js @@ -61,7 +61,25 @@ function formatBooleanWord(value) { function formatBaselineState(payload) { const entry = safeObject(payload); const status = String(entry.status || "unknown"); - return entry.trusted ? `${status} · trusted` : `${status} · untrusted`; + const parts = [status, entry.trusted ? "trusted" : "untrusted"]; + if (entry.compared_without_valid_baseline) { + parts.push("comparing without valid baseline"); + } + return parts.join(" · "); +} + +function formatBaselineTags(payload) { + const entry = safeObject(payload); + const baselinePythonTag = String(entry.baseline_python_tag || "").trim(); + const runtimePythonTag = String(entry.runtime_python_tag || "").trim(); + const parts = []; + if (baselinePythonTag) { + parts.push(`baseline ${baselinePythonTag}`); + } + if (runtimePythonTag) { + parts.push(`runtime ${runtimePythonTag}`); + } + return parts.length > 0 ? parts.join(" · ") : "unknown"; } function formatCacheSummary(payload) { @@ -294,6 +312,7 @@ module.exports = { findingIcon, firstNormalizedLocation, focusModeSpec, + formatBaselineTags, formatBaselineState, formatBooleanWord, formatCacheSummary, diff --git a/extensions/vscode-codeclone/src/renderers.js b/extensions/vscode-codeclone/src/renderers.js index aa8d1c1..f7a4cc2 100644 --- a/extensions/vscode-codeclone/src/renderers.js +++ b/extensions/vscode-codeclone/src/renderers.js @@ -9,6 +9,8 @@ const { capitalize, compactDecimal, decimal, + formatBaselineTags, + formatBaselineState, formatKind, formatSeverity, formatSourceKindSummary, @@ -194,6 +196,7 @@ function renderRemediationMarkdown(payload) { function renderTriageMarkdown(state) { const summary = safeObject(state.latestSummary); const triage = safeObject(state.latestTriage); + const baseline = safeObject(summary.baseline); const health = safeObject(summary.health); const findings = safeObject(summary.findings); const triageFindings = safeObject(triage.findings); @@ -208,17 +211,22 @@ function renderTriageMarkdown(state) { ); const items = safeArray(topHotspots.items); const suggestions = safeArray(topSuggestions.items); + const baselineTags = formatBaselineTags(baseline); const lines = [ "# CodeClone Production Triage", "", `- Run: \`${state.currentRunId || "n/a"}\``, `- Workspace: \`${state.folder.name}\``, `- Health: ${health.score || 0}/${health.grade || "?"} · ${healthScope} scope`, + `- Baseline: ${formatBaselineState(baseline)}`, `- Focus: ${focus} · ${Number(triageFindings.outside_focus || 0)} outside focus`, `- Findings: ${findings.total || 0} total · ${findings.production || 0} production`, `- New findings: ${formatSourceKindSummary(findings.new_by_source_kind)}`, `- Source kinds: ${formatSourceKindSummary(triageFindings.by_source_kind)}`, ]; + if (baseline.compared_without_valid_baseline && baselineTags !== "unknown") { + lines.push(`- Baseline tags: ${baselineTags}`); + } if (items.length > 0) { lines.push( "", diff --git a/extensions/vscode-codeclone/test/renderers.test.js b/extensions/vscode-codeclone/test/renderers.test.js new file mode 100644 index 0000000..a81214a --- /dev/null +++ b/extensions/vscode-codeclone/test/renderers.test.js @@ -0,0 +1,80 @@ +"use strict"; + +const test = require("node:test"); +const assert = require("node:assert/strict"); +const Module = require("node:module"); + +const originalLoad = Module._load; +Module._load = function patchedLoad(request, parent, isMain) { + if (request === "vscode") { + return { + ThemeIcon: class ThemeIcon {}, + ThemeColor: class ThemeColor {}, + }; + } + return originalLoad.call(this, request, parent, isMain); +}; + +const { + formatBaselineState, + formatBaselineTags, +} = require("../src/formatters"); +const {renderTriageMarkdown} = require("../src/renderers"); + +Module._load = originalLoad; + +test("formatBaselineState explains comparison without a valid baseline", () => { + assert.equal( + formatBaselineState({ + status: "mismatch_python_version", + trusted: false, + compared_without_valid_baseline: true, + }), + "mismatch_python_version · untrusted · comparing without valid baseline" + ); + assert.equal( + formatBaselineTags({ + baseline_python_tag: "cp313", + runtime_python_tag: "cp314", + }), + "baseline cp313 · runtime cp314" + ); +}); + +test("renderTriageMarkdown surfaces baseline mismatch context compactly", () => { + const markdown = renderTriageMarkdown({ + currentRunId: "abcd1234", + folder: {name: "demo"}, + latestSummary: { + baseline: { + status: "mismatch_python_version", + trusted: false, + compared_without_valid_baseline: true, + baseline_python_tag: "cp313", + runtime_python_tag: "cp314", + }, + health_scope: "repository", + health: {score: 87, grade: "B"}, + findings: { + total: 4, + production: 1, + new_by_source_kind: {tests: 1}, + }, + }, + latestTriage: { + focus: "production", + findings: { + outside_focus: 3, + by_source_kind: {production: 1, tests: 3}, + }, + top_hotspots: {items: []}, + top_suggestions: {items: []}, + }, + }); + + assert.match( + markdown, + /Baseline: mismatch_python_version · untrusted · comparing without valid baseline/ + ); + assert.match(markdown, /Baseline tags: baseline cp313 · runtime cp314/); +}); diff --git a/plugins/codeclone/.codex-plugin/plugin.json b/plugins/codeclone/.codex-plugin/plugin.json index 400a6e2..03e6286 100644 --- a/plugins/codeclone/.codex-plugin/plugin.json +++ b/plugins/codeclone/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "codeclone", - "version": "2.0.0-b4.0", + "version": "2.0.0-b5.0", "description": "Baseline-aware structural code quality analysis for Codex through the local CodeClone MCP server.", "author": { "name": "Den Rozhnovskiy", diff --git a/plugins/codeclone/.mcp.json b/plugins/codeclone/.mcp.json index cbd8cc1..488a9cf 100644 --- a/plugins/codeclone/.mcp.json +++ b/plugins/codeclone/.mcp.json @@ -1,10 +1,10 @@ { "mcpServers": { "codeclone": { - "command": "codeclone-mcp", + "command": "sh", "args": [ - "--transport", - "stdio" + "-lc", + "if [ -x \"$PWD/.venv/bin/codeclone-mcp\" ]; then exec \"$PWD/.venv/bin/codeclone-mcp\" --transport stdio; fi; if command -v poetry >/dev/null 2>&1; then poetry_env=\"$(poetry env info -p 2>/dev/null || true)\"; if [ -n \"$poetry_env\" ] && [ -x \"$poetry_env/bin/codeclone-mcp\" ]; then exec \"$poetry_env/bin/codeclone-mcp\" --transport stdio; fi; fi; if command -v codeclone-mcp >/dev/null 2>&1; then exec codeclone-mcp --transport stdio; fi; echo 'codeclone-mcp not found: expected .venv/bin/codeclone-mcp, a Poetry env launcher, or a PATH entry' >&2; exit 1" ] } } diff --git a/plugins/codeclone/README.md b/plugins/codeclone/README.md index 25e9057..d6acec3 100644 --- a/plugins/codeclone/README.md +++ b/plugins/codeclone/README.md @@ -17,15 +17,28 @@ Read-only, baseline-aware, local stdio only. ## Install -`codeclone-mcp` must be on `PATH`: +The plugin prefers a workspace launcher first: + +1. `./.venv/bin/codeclone-mcp` +2. the current Poetry environment launcher +3. `codeclone-mcp` from `PATH` + +Recommended workspace-local setup: ```bash -uv tool install --pre "codeclone[mcp]" -codeclone-mcp --help # verify +uv venv +uv pip install --python .venv/bin/python "codeclone[mcp]>=2.0.0b4" +.venv/bin/codeclone-mcp --help ``` -If you want to keep the launcher inside an existing environment instead, use -`uv pip install --pre "codeclone[mcp]"`. +If your workspace uses Poetry, install CodeClone into that Poetry environment. + +Global fallback: + +```bash +uv tool install "codeclone[mcp]>=2.0.0b4" +codeclone-mcp --help +``` Codex discovers the plugin from `.agents/plugins/marketplace.json`. It does not rewrite `~/.codex/config.toml`. diff --git a/tests/test_codex_plugin.py b/tests/test_codex_plugin.py index 24f235f..efc510f 100644 --- a/tests/test_codex_plugin.py +++ b/tests/test_codex_plugin.py @@ -15,7 +15,7 @@ def test_codex_plugin_manifest_is_consistent() -> None: assert isinstance(manifest, dict) assert manifest["name"] == "codeclone" - assert manifest["version"] == "2.0.0-b4.0" + assert manifest["version"] == "2.0.0-b5.0" assert manifest["skills"] == "./skills/" assert manifest["mcpServers"] == "./.mcp.json" assert manifest["license"] == "MPL-2.0" @@ -70,14 +70,14 @@ def test_codex_plugin_marketplace_and_mcp_config_are_aligned() -> None: ] assert isinstance(mcp_config, dict) - assert mcp_config == { - "mcpServers": { - "codeclone": { - "command": "codeclone-mcp", - "args": ["--transport", "stdio"], - } - } - } + server = mcp_config["mcpServers"]["codeclone"] + assert server["command"] == "sh" + assert server["args"][0] == "-lc" + launcher = server["args"][1] + assert "$PWD/.venv/bin/codeclone-mcp" in launcher + assert "poetry env info -p" in launcher + assert "exec codeclone-mcp --transport stdio" in launcher + assert "PATH entry" in launcher def test_codex_plugin_skill_exists() -> None: @@ -118,6 +118,9 @@ def test_codex_plugin_readme_and_docs_exist() -> None: assert "# CodeClone for Codex" in readme_text assert "codex mcp add codeclone -- codeclone-mcp --transport stdio" in readme_text assert "does not rewrite `~/.codex/config.toml`" in readme_text + assert "The plugin prefers a workspace launcher first" in readme_text + assert "the current Poetry environment launcher" in readme_text + assert 'uv tool install "codeclone[mcp]>=2.0.0b4"' in readme_text assert (root / "docs" / "codex-plugin.md").is_file() assert (root / "docs" / "terms-of-use.md").is_file() diff --git a/tests/test_mcp_service.py b/tests/test_mcp_service.py index 6625a4c..723e924 100644 --- a/tests/test_mcp_service.py +++ b/tests/test_mcp_service.py @@ -19,6 +19,7 @@ from codeclone import mcp_service as mcp_service_mod from codeclone._cli_config import ConfigValidationError +from codeclone.baseline import Baseline, current_python_tag from codeclone.cache import Cache from codeclone.contracts import REPORT_SCHEMA_VERSION from codeclone.mcp_service import ( @@ -279,6 +280,42 @@ def test_mcp_service_analyze_repository_registers_latest_run(tmp_path: Path) -> ) +def test_mcp_service_summary_explains_untrusted_baseline_python_tag_mismatch( + tmp_path: Path, +) -> None: + _write_clone_fixture(tmp_path) + baseline = Baseline(tmp_path / "codeclone.baseline.json") + baseline.generator = "codeclone" + baseline.schema_version = "2.0" + baseline.fingerprint_version = "1" + baseline.python_tag = "cp313" if current_python_tag() != "cp313" else "cp314" + baseline.created_at = "2026-04-07T00:00:00Z" + baseline.save() + + service = CodeCloneMCPService(history_limit=4) + summary = service.analyze_repository( + MCPAnalysisRequest( + root=str(tmp_path), + respect_pyproject=False, + cache_policy="off", + ) + ) + + baseline_payload = cast("dict[str, object]", summary["baseline"]) + assert baseline_payload["status"] == "mismatch_python_version" + assert baseline_payload["trusted"] is False + assert baseline_payload["compared_without_valid_baseline"] is True + assert baseline_payload["baseline_python_tag"] == baseline.python_tag + assert baseline_payload["runtime_python_tag"] == current_python_tag() + assert any( + "Baseline python tag mismatch" in warning + for warning in cast("list[str]", summary["warnings"]) + ) + + triage = service.get_production_triage() + assert cast("dict[str, object]", triage["baseline"]) == baseline_payload + + def test_mcp_service_help_returns_bounded_semantic_guidance() -> None: service = CodeCloneMCPService(history_limit=4) diff --git a/uv.lock b/uv.lock index 680a19c..3b15243 100644 --- a/uv.lock +++ b/uv.lock @@ -1361,11 +1361,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.22" +version = "0.0.24" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/45/e23b5dc14ddb9918ae4a625379506b17b6f8fc56ca1d82db62462f59aea6/python_multipart-0.0.24.tar.gz", hash = "sha256:9574c97e1c026e00bc30340ef7c7d76739512ab4dfd428fec8c330fa6a5cc3c8", size = 37695, upload-time = "2026-04-05T20:49:13.829Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, + { url = "https://files.pythonhosted.org/packages/a3/73/89930efabd4da63cea44a3f438aeb753d600123570e6d6264e763617a9ce/python_multipart-0.0.24-py3-none-any.whl", hash = "sha256:9b110a98db707df01a53c194f0af075e736a770dc5058089650d70b4a182f950", size = 24420, upload-time = "2026-04-05T20:49:12.555Z" }, ] [[package]] @@ -1832,16 +1832,16 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.43.0" +version = "0.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/f2/368268300fb8af33743508d738ef7bb4d56afdb46c6d9c0fa3dd515df171/uvicorn-0.43.0.tar.gz", hash = "sha256:ab1652d2fb23abf124f36ccc399828558880def222c3cb3d98d24021520dc6e8", size = 85686, upload-time = "2026-04-03T18:37:48.984Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/df/0cf5b0c451602748fdc7a702d4667f6e209bf96aa6e3160d754234445f2a/uvicorn-0.43.0-py3-none-any.whl", hash = "sha256:46fac64f487fd968cd999e5e49efbbe64bd231b5bd8b4a0b482a23ebce499620", size = 68591, upload-time = "2026-04-03T18:37:47.64Z" }, + { url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" }, ] [[package]] From ebcd474359e18f8150ebbc224d50bcfe12251a7f Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Wed, 8 Apr 2026 21:42:43 +0500 Subject: [PATCH 04/17] feat(claude): prefer workspace runtimes and poetry envs before global launchers --- CHANGELOG.md | 5 +- docs/book/22-claude-desktop-bundle.md | 9 +- docs/claude-desktop-bundle.md | 29 +- extensions/claude-desktop-codeclone/README.md | 22 +- .../claude-desktop-codeclone/manifest.json | 13 +- .../package-lock.json | 4 +- .../claude-desktop-codeclone/package.json | 2 +- .../claude-desktop-codeclone/src/launcher.js | 378 +++++++++++++++++- .../test/fixtures/hang-stdio.js | 8 + .../test/launcher.test.js | 223 ++++++++++- 10 files changed, 654 insertions(+), 39 deletions(-) create mode 100644 extensions/claude-desktop-codeclone/test/fixtures/hang-stdio.js diff --git a/CHANGELOG.md b/CHANGELOG.md index f3094b7..288fa20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,11 @@ projections. - Make baseline mismatch handling more explicit in MCP and the VS Code client by surfacing baseline/runtime python tags and whether comparison is proceeding without a valid baseline. -- Make the Codex plugin prefer workspace-local launchers before `PATH`, with Poetry environment fallback for +- Make the Claude Desktop bundle and Codex plugin prefer workspace-local launchers before `PATH`, with Poetry environment fallback for python-tag-safe MCP startup. +- Add `workspace_root` user-config field to the Claude Desktop bundle: setting it to the project directory forces the + launcher to prefer `.venv` inside that path even when Claude Desktop starts with a different working directory + (fixes python-tag mismatch caused by system-wide interpreter fallback). - Refresh branch metadata and client docs for the `2.0.0b5` line. - Update the README repository health badge to `87 (B)`. diff --git a/docs/book/22-claude-desktop-bundle.md b/docs/book/22-claude-desktop-bundle.md index 8ba4458..63aead2 100644 --- a/docs/book/22-claude-desktop-bundle.md +++ b/docs/book/22-claude-desktop-bundle.md @@ -57,13 +57,14 @@ The wrapper: 5. proxies stdio until shutdown The wrapper may auto-discover a few common global install locations, but it is -primarily designed for: +now prefers: -- `codeclone-mcp` on `PATH` +- a workspace-local `.venv` +- the active Poetry environment for the current workspace +- user-local install locations and `PATH` - or an explicit launcher command in bundle settings -Repository-local virtual environments are intentionally outside the default -bundle contract. +This keeps the launcher closer to the active project Python when possible. ## Design rules diff --git a/docs/claude-desktop-bundle.md b/docs/claude-desktop-bundle.md index 1238337..c1be170 100644 --- a/docs/claude-desktop-bundle.md +++ b/docs/claude-desktop-bundle.md @@ -10,16 +10,30 @@ production-first structural review. ## Install +The bundle prefers the current workspace launcher first: + +1. `./.venv/bin/codeclone-mcp` +2. the current Poetry environment launcher +3. user-local install paths and `PATH` + +```bash +uv venv +uv pip install --python .venv/bin/python "codeclone[mcp]>=2.0.0b4" +.venv/bin/codeclone-mcp --help +``` + +Global fallback: + ```bash -uv tool install --pre "codeclone[mcp]" # or: uv pip install --pre "codeclone[mcp]" -codeclone-mcp --help # verify +uv tool install "codeclone[mcp]>=2.0.0b4" +codeclone-mcp --help ``` ## Bundle workflow 1. Build: `cd extensions/claude-desktop-codeclone && node scripts/build-mcpb.mjs` 2. Claude Desktop: **Settings → Extensions → Install Extension** → select `.mcpb` -3. If `codeclone-mcp` is not on `PATH`, set **CodeClone launcher command** in +3. If you want to bypass auto-discovery, set **CodeClone launcher command** in the bundle settings to an absolute path. ## Settings @@ -31,9 +45,9 @@ codeclone-mcp --help # verify ## Runtime model -Node wrapper launches `codeclone-mcp` via local `stdio`. Auto-discovers the -launcher in `~/.local/bin`, macOS `~/Library/Python/*/bin`, or Windows Python -paths. Falls back to `PATH`. +Node wrapper launches `codeclone-mcp` via local `stdio`. It prefers a +workspace-local `.venv`, then a Poetry environment, then user-local install +paths, then `PATH`. ## Privacy @@ -42,7 +56,8 @@ See [Privacy Policy](privacy-policy.md). ## Current limits -- expects a global or explicitly configured launcher +- expects either a workspace launcher, a user-local/global launcher, or an + explicitly configured absolute launcher path - local install surface, not a hosted service layer For the underlying MCP contract, see: diff --git a/extensions/claude-desktop-codeclone/README.md b/extensions/claude-desktop-codeclone/README.md index 1a3da68..26e08e3 100644 --- a/extensions/claude-desktop-codeclone/README.md +++ b/extensions/claude-desktop-codeclone/README.md @@ -8,15 +8,25 @@ Read-only, baseline-aware, local stdio only. ## Install +The bundle prefers the current workspace launcher first: + +1. `./.venv/bin/codeclone-mcp` +2. the current Poetry environment launcher +3. user-local install paths and `PATH` + +Recommended workspace-local setup: + ```bash -uv tool install --pre "codeclone[mcp]" -codeclone-mcp --help # verify launcher +uv venv +uv pip install --python .venv/bin/python "codeclone[mcp]>=2.0.0b4" +.venv/bin/codeclone-mcp --help ``` -If you want to keep the launcher inside an existing environment instead, use: +Global fallback: ```bash -uv pip install --pre "codeclone[mcp]" +uv tool install "codeclone[mcp]>=2.0.0b4" +codeclone-mcp --help ``` Build and install the bundle: @@ -29,8 +39,8 @@ node scripts/build-mcpb.mjs Then in Claude Desktop: **Settings → Extensions → Install Extension** → select the `.mcpb` from `dist/`. -If `codeclone-mcp` is not on `PATH`, set **CodeClone launcher command** in the -extension settings to an absolute path. +If you want to bypass auto-discovery entirely, set **CodeClone launcher +command** in the extension settings to an absolute path. ## Configuration diff --git a/extensions/claude-desktop-codeclone/manifest.json b/extensions/claude-desktop-codeclone/manifest.json index 62787c1..ebf0ccb 100644 --- a/extensions/claude-desktop-codeclone/manifest.json +++ b/extensions/claude-desktop-codeclone/manifest.json @@ -2,7 +2,7 @@ "manifest_version": "0.3", "name": "codeclone", "display_name": "CodeClone", - "version": "2.0.0-b5.0", + "version": "2.0.0-b5.1", "description": "Baseline-aware structural review for Claude Desktop through a local CodeClone MCP launcher.", "long_description": "CodeClone for Claude Desktop wraps the local codeclone-mcp launcher as an MCP bundle. It keeps Claude on the same canonical MCP surface used by the CLI, HTML report, VS Code extension, and Codex plugin — read-only, baseline-aware, local stdio only.", "author": { @@ -98,15 +98,22 @@ ], "env": { "CODECLONE_MCP_COMMAND": "${user_config.launcher_command}", - "CODECLONE_MCP_ARGS_JSON": "${user_config.launcher_args_json}" + "CODECLONE_MCP_ARGS_JSON": "${user_config.launcher_args_json}", + "CODECLONE_WORKSPACE_ROOT": "${user_config.workspace_root}" } } }, "user_config": { + "workspace_root": { + "type": "string", + "title": "Workspace root path", + "description": "Optional absolute path to the Python project root (the directory that contains .venv or venv). When set, the launcher prefers that virtual environment over user-local or system-wide installs. Useful when Claude Desktop starts outside the project directory.", + "required": false + }, "launcher_command": { "type": "string", "title": "CodeClone launcher command", - "description": "Optional absolute path or bare command name for codeclone-mcp. Leave empty to use auto-discovery and PATH.", + "description": "Optional absolute path or bare command name for codeclone-mcp. Leave empty to prefer workspace launchers, then user-local paths, then PATH.", "required": false }, "launcher_args_json": { diff --git a/extensions/claude-desktop-codeclone/package-lock.json b/extensions/claude-desktop-codeclone/package-lock.json index 341c057..9f156cb 100644 --- a/extensions/claude-desktop-codeclone/package-lock.json +++ b/extensions/claude-desktop-codeclone/package-lock.json @@ -1,12 +1,12 @@ { "name": "@orenlab/codeclone-claude-desktop", - "version": "2.0.0-b5.0", + "version": "2.0.0-b5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@orenlab/codeclone-claude-desktop", - "version": "2.0.0-b5.0", + "version": "2.0.0-b5.1", "license": "MPL-2.0", "engines": { "node": ">=20.0.0" diff --git a/extensions/claude-desktop-codeclone/package.json b/extensions/claude-desktop-codeclone/package.json index 234e434..2b97c37 100644 --- a/extensions/claude-desktop-codeclone/package.json +++ b/extensions/claude-desktop-codeclone/package.json @@ -1,6 +1,6 @@ { "name": "@orenlab/codeclone-claude-desktop", - "version": "2.0.0-b5.0", + "version": "2.0.0-b5.1", "private": true, "description": "Claude Desktop MCP bundle wrapper for the local CodeClone MCP launcher.", "license": "MPL-2.0", diff --git a/extensions/claude-desktop-codeclone/src/launcher.js b/extensions/claude-desktop-codeclone/src/launcher.js index d918e89..509f86a 100644 --- a/extensions/claude-desktop-codeclone/src/launcher.js +++ b/extensions/claude-desktop-codeclone/src/launcher.js @@ -1,9 +1,10 @@ "use strict"; const fs = require("node:fs/promises"); +const fsSync = require("node:fs"); const os = require("node:os"); const path = require("node:path"); -const {spawn} = require("node:child_process"); +const {spawn, spawnSync} = require("node:child_process"); const USER_CONFIG_PLACEHOLDER_RE = /^\$\{user_config\.[^}]+\}$/; const BLOCKED_ARGS = new Set([ @@ -19,10 +20,43 @@ const BLOCKED_ARGS = new Set([ * @typedef {{ * command: string, * args: string[], - * source: string + * source: string, + * cwd: string | null * }} LaunchSpec */ +const ANCESTOR_WALK_MAX_DEPTH = 8; + +// Bounded escalation on shutdown: after stdin closes or a shutdown signal is +// received, give the child SHUTDOWN_GRACE_MS to exit cleanly; if it is still +// alive, send SIGTERM; if it is still alive KILL_GRACE_MS after that, send +// SIGKILL. Keeps the MCP session from hanging forever when the child wedges. +// Both grace periods can be overridden via env for operator tuning and tests; +// values below 50 ms are clamped to 50 ms to avoid footguns. +const MIN_GRACE_MS = 50; + +/** + * @param {string | undefined} raw + * @param {number} fallback + * @returns {number} + */ +function parseGraceMs(raw, fallback) { + const parsed = Number(raw); + if (!Number.isFinite(parsed) || parsed <= 0) { + return fallback; + } + return Math.max(MIN_GRACE_MS, Math.floor(parsed)); +} + +const SHUTDOWN_GRACE_MS = parseGraceMs( + process.env.CODECLONE_MCP_SHUTDOWN_GRACE_MS, + 5000, +); +const KILL_GRACE_MS = parseGraceMs( + process.env.CODECLONE_MCP_KILL_GRACE_MS, + 2000, +); + /** * @param {string | undefined} value * @returns {string} @@ -73,7 +107,8 @@ function parseLauncherArgsJson(value) { */ function validateAdditionalArgs(args) { for (const arg of args) { - if (BLOCKED_ARGS.has(arg)) { + const head = arg.split("=", 1)[0]; + if (BLOCKED_ARGS.has(head)) { throw new Error( `Unsupported launcher argument ${arg}. This bundle always uses local stdio transport.`, ); @@ -171,26 +206,211 @@ async function candidateAutoCommands(env, platform) { return existing; } +/** + * @param {string} rootPath + * @param {NodeJS.Platform} platform + * @returns {string[]} + */ +function workspaceLocalLauncherCandidates(rootPath, platform) { + const root = String(rootPath || "").trim(); + if (!root) { + return []; + } + if (platform === "win32") { + return [ + path.join(root, ".venv", "Scripts", "codeclone-mcp.exe"), + path.join(root, ".venv", "Scripts", "codeclone-mcp.cmd"), + path.join(root, "venv", "Scripts", "codeclone-mcp.exe"), + path.join(root, "venv", "Scripts", "codeclone-mcp.cmd"), + ]; + } + return [ + path.join(root, ".venv", "bin", "codeclone-mcp"), + path.join(root, "venv", "bin", "codeclone-mcp"), + ]; +} + +/** + * @param {NodeJS.ProcessEnv} env + * @param {string} cwd + * @returns {string[]} + */ +function workspaceRoots(env, cwd) { + const configuredRoot = normalizeConfiguredValue(env.CODECLONE_WORKSPACE_ROOT); + return [ + ...new Set([ + configuredRoot, + String(cwd || "").trim(), + String(env.PWD || "").trim(), + ]), + ].filter(Boolean); +} + +/** + * Walk upward from a starting directory looking for a workspace-local launcher + * in an ancestor `.venv`/`venv` virtual environment. Returns the first + * ancestor directory that contains a matching launcher, or null. Bounded by + * ANCESTOR_WALK_MAX_DEPTH and by the filesystem root to keep startup cost low + * and deterministic. + * + * @param {string} start + * @param {NodeJS.Platform} platform + * @returns {Promise} + */ +async function findAncestorWorkspaceRoot(start, platform) { + const anchor = String(start || "").trim(); + if (!anchor) { + return null; + } + let current = path.resolve(anchor); + for (let depth = 0; depth < ANCESTOR_WALK_MAX_DEPTH; depth += 1) { + for (const candidate of workspaceLocalLauncherCandidates(current, platform)) { + if (await fileExists(candidate)) { + return current; + } + } + const parent = path.dirname(current); + if (!parent || parent === current) { + return null; + } + current = parent; + } + return null; +} + +/** + * @param {NodeJS.ProcessEnv} env + * @param {NodeJS.Platform} platform + * @param {string} cwd + * @returns {Promise<{command: string, root: string}[]>} + */ +async function candidateWorkspaceCommands(env, platform, cwd) { + const roots = workspaceRoots(env, cwd); + const directCandidates = roots.flatMap((root) => + workspaceLocalLauncherCandidates(root, platform).map((command) => ({ + command, + root, + })), + ); + + /** @type {{command: string, root: string}[]} */ + const existing = []; + /** @type {Set} */ + const seen = new Set(); + for (const candidate of directCandidates) { + if (seen.has(candidate.command)) { + continue; + } + if (await fileExists(candidate.command)) { + existing.push(candidate); + seen.add(candidate.command); + } + } + if (existing.length > 0) { + return existing; + } + + // Ancestor walk only triggers when no direct workspace match is found. + // This handles the common Claude Desktop case where the bundle is launched + // from an unrelated cwd but a parent of cwd/PWD is the real project root. + for (const root of roots) { + const ancestor = await findAncestorWorkspaceRoot(root, platform); + if (!ancestor) { + continue; + } + for (const command of workspaceLocalLauncherCandidates(ancestor, platform)) { + if (seen.has(command)) { + continue; + } + if (await fileExists(command)) { + existing.push({command, root: ancestor}); + seen.add(command); + } + } + } + return existing; +} + +/** + * @param {NodeJS.ProcessEnv} env + * @param {NodeJS.Platform} platform + * @param {string} cwd + * @returns {Promise<{command: string, root: string} | null>} + */ +async function resolvePoetryLauncher(env, platform, cwd) { + const executable = platform === "win32" ? "codeclone-mcp.exe" : "codeclone-mcp"; + for (const root of workspaceRoots(env, cwd)) { + if (!(await fileExists(path.join(root, "pyproject.toml")))) { + continue; + } + const poetryProbe = spawnSync("poetry", ["env", "info", "-p"], { + cwd: root, + env, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + windowsHide: true, + }); + const poetryRoot = String(poetryProbe.stdout || "").trim(); + if (!poetryRoot) { + continue; + } + const candidate = + platform === "win32" + ? path.join(poetryRoot, "Scripts", executable) + : path.join(poetryRoot, "bin", executable); + if (await fileExists(candidate)) { + return {command: candidate, root}; + } + } + return null; +} + /** * @param {{ * env?: NodeJS.ProcessEnv, - * platform?: NodeJS.Platform + * platform?: NodeJS.Platform, + * cwd?: string * }} [options] * @returns {Promise} */ async function resolveLaunchSpec(options = {}) { const env = options.env ?? process.env; const platform = options.platform ?? process.platform; + const cwd = options.cwd ?? process.cwd(); const configuredCommand = normalizeConfiguredValue(env.CODECLONE_MCP_COMMAND); const configuredArgs = parseLauncherArgsJson(env.CODECLONE_MCP_ARGS_JSON ?? ""); validateConfiguredCommand(configuredCommand); validateAdditionalArgs(configuredArgs); + const configuredRoot = + normalizeConfiguredValue(env.CODECLONE_WORKSPACE_ROOT) || null; + if (configuredCommand) { return { command: configuredCommand, args: [...configuredArgs, "--transport", "stdio"], source: "configured", + cwd: configuredRoot, + }; + } + + const workspaceCommands = await candidateWorkspaceCommands(env, platform, cwd); + if (workspaceCommands.length > 0) { + return { + command: workspaceCommands[0].command, + args: ["--transport", "stdio"], + source: "workspaceLocal", + cwd: workspaceCommands[0].root, + }; + } + + const poetryLauncher = await resolvePoetryLauncher(env, platform, cwd); + if (poetryLauncher) { + return { + command: poetryLauncher.command, + args: ["--transport", "stdio"], + source: "poetryEnv", + cwd: poetryLauncher.root, }; } @@ -200,6 +420,7 @@ async function resolveLaunchSpec(options = {}) { command: autoCommands[0], args: ["--transport", "stdio"], source: "auto", + cwd: configuredRoot, }; } @@ -207,16 +428,40 @@ async function resolveLaunchSpec(options = {}) { command: "codeclone-mcp", args: ["--transport", "stdio"], source: "path", + cwd: configuredRoot, }; } +/** + * Narrow the TOCTOU window between candidate selection and spawn by re-stating + * the resolved command and locking onto its realpath. Bare command names + * (resolved by the OS via PATH) are returned unchanged. Throws on missing or + * non-regular targets so the caller can surface the setup hint. + * + * @param {string} command + * @returns {string} + */ +function lockResolvedCommand(command) { + if (!path.isAbsolute(command)) { + return command; + } + const real = fsSync.realpathSync(command); + const stat = fsSync.statSync(real); + if (!stat.isFile()) { + throw Object.assign(new Error(`Resolved launcher is not a regular file: ${real}`), { + code: "ENOENT", + }); + } + return real; +} + /** * @returns {string} */ function buildSetupMessage() { return [ "CodeClone launcher not found.", - "Install a CodeClone build that includes the MCP extra, or point this bundle at a working codeclone-mcp launcher.", + "Install CodeClone with the MCP extra in the current workspace, Poetry environment, or PATH, or point this bundle at a working codeclone-mcp launcher.", "Or configure an absolute launcher path in the Claude Desktop bundle settings.", ].join("\n"); } @@ -231,6 +476,19 @@ function exitProxy(code) { process.exit(code); } +// Strip ANSI escape sequences and other C0/C1 control characters (except tab) +// from child stderr before forwarding. The child is trusted, but its output is +// surfaced to terminals and log viewers that may misrender control bytes. +const CONTROL_CHAR_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b[@-_]|[\x00-\x08\x0b-\x1f\x7f]/g; + +/** + * @param {string} value + * @returns {string} + */ +function sanitizeForLog(value) { + return value.replace(CONTROL_CHAR_RE, ""); +} + /** * @param {NodeJS.WritableStream} stream * @param {string} prefix @@ -243,7 +501,7 @@ function createPrefixedWriter(stream, prefix) { const parts = text.split(/\r?\n/); carry = parts.pop() ?? ""; for (const part of parts) { - stream.write(`${prefix}${part}\n`); + stream.write(`${prefix}${sanitizeForLog(part)}\n`); } }; } @@ -258,22 +516,83 @@ function attachChildLifecycle(child) { child.stdout.pipe(process.stdout); process.stdin.pipe(child.stdin); + /** @type {NodeJS.Timeout | null} */ + let sigTermTimer = null; + /** @type {NodeJS.Timeout | null} */ + let sigKillTimer = null; + + const clearShutdownTimers = () => { + if (sigTermTimer) { + clearTimeout(sigTermTimer); + sigTermTimer = null; + } + if (sigKillTimer) { + clearTimeout(sigKillTimer); + sigKillTimer = null; + } + }; + + const childIsAlive = () => child.exitCode === null && child.signalCode === null; + + const scheduleSigKill = () => { + if (sigKillTimer || !childIsAlive()) { + return; + } + sigKillTimer = setTimeout(() => { + if (childIsAlive()) { + try { + child.kill("SIGKILL"); + } catch { + // Child may have raced to exit; nothing to do. + } + } + }, KILL_GRACE_MS); + // Do not hold the event loop open on the timer alone. + if (typeof sigKillTimer.unref === "function") { + sigKillTimer.unref(); + } + }; + + const sendSigTerm = () => { + if (!childIsAlive()) { + return; + } + try { + child.kill("SIGTERM"); + } catch { + // Child raced to exit before the signal landed. + } + scheduleSigKill(); + }; + + const scheduleGracefulShutdown = () => { + if (sigTermTimer || !childIsAlive()) { + return; + } + sigTermTimer = setTimeout(sendSigTerm, SHUTDOWN_GRACE_MS); + if (typeof sigTermTimer.unref === "function") { + sigTermTimer.unref(); + } + }; + /** @type {NodeJS.Signals[]} */ const signals = ["SIGINT", "SIGTERM", "SIGHUP"]; const forwardSignal = () => { - if (!child.killed) { - child.kill("SIGTERM"); - } + sendSigTerm(); }; for (const signal of signals) { process.once(signal, forwardSignal); } process.stdin.on("end", () => { - child.stdin.end(); + if (!child.stdin.destroyed && child.stdin.writable) { + child.stdin.end(); + } + scheduleGracefulShutdown(); }); return () => { + clearShutdownTimers(); child.stdout.unpipe(process.stdout); process.stdin.unpipe(child.stdin); child.stderr.off("data", writeStderr); @@ -301,11 +620,36 @@ async function runProxy(options = {}) { return; } - const child = spawn(spec.command, spec.args, { + const spawnCwd = spec.cwd && spec.cwd.length > 0 ? spec.cwd : undefined; + const childEnv = {...process.env}; + if (spawnCwd && !normalizeConfiguredValue(childEnv.CODECLONE_WORKSPACE_ROOT)) { + childEnv.CODECLONE_WORKSPACE_ROOT = spawnCwd; + } + + /** @type {string} */ + let resolvedCommand; + try { + resolvedCommand = lockResolvedCommand(spec.command); + } catch (error) { + const detail = + error && typeof error === "object" && "code" in error && error.code === "ENOENT" + ? buildSetupMessage() + : String(error.message || error); + process.stderr.write(`[codeclone] ${sanitizeForLog(detail)}\n`); + process.exitCode = 2; + return; + } + + process.stderr.write( + `[codeclone] launcher source=${spec.source} command=${resolvedCommand} cwd=${spawnCwd ?? ""}\n`, + ); + + const child = spawn(resolvedCommand, spec.args, { stdio: ["pipe", "pipe", "pipe"], shell: false, windowsHide: true, - env: process.env, + env: childEnv, + cwd: spawnCwd, }); const detach = attachChildLifecycle(child); @@ -328,7 +672,11 @@ async function runProxy(options = {}) { finish(2); }); - child.on("exit", (code, signal) => { + // "close" fires after the child's stdio streams have been fully drained, + // so any final JSON-RPC response the child wrote right before exiting has + // already been piped out to our own stdout. Using "exit" here would race + // with the pipe and silently drop the last response. + child.on("close", (code, signal) => { if (signal) { process.stderr.write(`[codeclone] Launcher exited via ${signal}.\n`); finish(1); @@ -342,11 +690,15 @@ module.exports = { BLOCKED_ARGS, buildSetupMessage, candidateAutoCommands, + candidateWorkspaceCommands, exitProxy, normalizeConfiguredValue, parseLauncherArgsJson, resolveLaunchSpec, + resolvePoetryLauncher, runProxy, validateAdditionalArgs, validateConfiguredCommand, + workspaceLocalLauncherCandidates, + workspaceRoots, }; diff --git a/extensions/claude-desktop-codeclone/test/fixtures/hang-stdio.js b/extensions/claude-desktop-codeclone/test/fixtures/hang-stdio.js new file mode 100644 index 0000000..ebd1f95 --- /dev/null +++ b/extensions/claude-desktop-codeclone/test/fixtures/hang-stdio.js @@ -0,0 +1,8 @@ +"use strict"; + +// Intentionally hung stdio child used to exercise the launcher's shutdown +// escalation: it ignores SIGTERM and keeps the event loop alive so only a +// SIGKILL from the wrapper can bring it down. +process.on("SIGTERM", () => {}); +process.stdin.resume(); +setInterval(() => {}, 1 << 30); diff --git a/extensions/claude-desktop-codeclone/test/launcher.test.js b/extensions/claude-desktop-codeclone/test/launcher.test.js index bd954df..d321ef3 100644 --- a/extensions/claude-desktop-codeclone/test/launcher.test.js +++ b/extensions/claude-desktop-codeclone/test/launcher.test.js @@ -1,6 +1,8 @@ "use strict"; const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); const path = require("node:path"); const {spawn} = require("node:child_process"); const test = require("node:test"); @@ -12,13 +14,17 @@ const { normalizeConfiguredValue, parseLauncherArgsJson, resolveLaunchSpec, + resolvePoetryLauncher, validateAdditionalArgs, validateConfiguredCommand, + workspaceLocalLauncherCandidates, + workspaceRoots, } = require("../src/launcher"); const rootDir = path.resolve(__dirname, ".."); const serverEntry = path.join(rootDir, "server", "index.js"); const echoScript = path.join(__dirname, "fixtures", "echo-stdio.js"); +const hangScript = path.join(__dirname, "fixtures", "hang-stdio.js"); test("normalizeConfiguredValue strips empty and placeholder values", () => { assert.equal(normalizeConfiguredValue(""), ""); @@ -56,11 +62,27 @@ test("validateAdditionalArgs blocks transport reconfiguration", () => { ); }); +test("validateAdditionalArgs blocks the --flag=value bypass form", () => { + assert.throws( + () => validateAdditionalArgs(["--transport=streamable-http"]), + /always uses local stdio transport/, + ); + assert.throws( + () => validateAdditionalArgs(["--host=0.0.0.0"]), + /always uses local stdio transport/, + ); + assert.throws( + () => validateAdditionalArgs(["--allow-remote=true"]), + /always uses local stdio transport/, + ); +}); + test("resolveLaunchSpec uses explicit launcher config when present", async () => { const spec = await resolveLaunchSpec({ env: { CODECLONE_MCP_COMMAND: "/tmp/codeclone-mcp", CODECLONE_MCP_ARGS_JSON: '["--history-limit","4"]', + CODECLONE_WORKSPACE_ROOT: "/some/project", }, platform: "darwin", }); @@ -68,26 +90,174 @@ test("resolveLaunchSpec uses explicit launcher config when present", async () => command: "/tmp/codeclone-mcp", args: ["--history-limit", "4", "--transport", "stdio"], source: "configured", + cwd: "/some/project", }); }); test("resolveLaunchSpec falls back to PATH when nothing is configured", async () => { + const emptyWorkspace = fs.mkdtempSync( + path.join(os.tmpdir(), "codeclone-claude-empty-workspace-"), + ); const spec = await resolveLaunchSpec({ env: { HOME: "/tmp/codeclone-claude-no-home", }, platform: "linux", + cwd: emptyWorkspace, }); assert.deepEqual(spec, { command: "codeclone-mcp", args: ["--transport", "stdio"], source: "path", + cwd: null, }); }); +test("workspaceLocalLauncherCandidates prefer workspace virtual environments", () => { + assert.deepEqual(workspaceLocalLauncherCandidates("/repo", "linux"), [ + "/repo/.venv/bin/codeclone-mcp", + "/repo/venv/bin/codeclone-mcp", + ]); +}); + +test("workspaceRoots places CODECLONE_WORKSPACE_ROOT first, before cwd and PWD", () => { + const roots = workspaceRoots( + {CODECLONE_WORKSPACE_ROOT: "/configured", PWD: "/from-pwd"}, + "/from-cwd", + ); + assert.deepEqual(roots, ["/configured", "/from-cwd", "/from-pwd"]); +}); + +test("workspaceRoots ignores unset or placeholder CODECLONE_WORKSPACE_ROOT", () => { + assert.deepEqual( + workspaceRoots({CODECLONE_WORKSPACE_ROOT: "${user_config.workspace_root}"}, "/cwd"), + ["/cwd"], + ); + assert.deepEqual( + workspaceRoots({CODECLONE_WORKSPACE_ROOT: ""}, "/cwd"), + ["/cwd"], + ); + assert.deepEqual( + workspaceRoots({}, "/cwd"), + ["/cwd"], + ); +}); + +test("resolveLaunchSpec prefers a workspace-local launcher before PATH", async () => { + const workspaceRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "codeclone-claude-workspace-"), + ); + const launcherPath = path.join(workspaceRoot, ".venv", "bin", "codeclone-mcp"); + fs.mkdirSync(path.dirname(launcherPath), {recursive: true}); + fs.writeFileSync(launcherPath, "#!/bin/sh\nexit 0\n", "utf8"); + fs.chmodSync(launcherPath, 0o755); + + const spec = await resolveLaunchSpec({ + env: { + HOME: "/tmp/codeclone-claude-no-home", + }, + platform: "linux", + cwd: workspaceRoot, + }); + + assert.deepEqual(spec, { + command: launcherPath, + args: ["--transport", "stdio"], + source: "workspaceLocal", + cwd: workspaceRoot, + }); +}); + +test("resolveLaunchSpec walks ancestors of cwd to find a project .venv launcher", async () => { + const workspaceRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "codeclone-claude-ancestor-"), + ); + const launcherPath = path.join(workspaceRoot, ".venv", "bin", "codeclone-mcp"); + fs.mkdirSync(path.dirname(launcherPath), {recursive: true}); + fs.writeFileSync(launcherPath, "#!/bin/sh\nexit 0\n", "utf8"); + fs.chmodSync(launcherPath, 0o755); + + const subdir = path.join(workspaceRoot, "src", "deep", "nested"); + fs.mkdirSync(subdir, {recursive: true}); + + const spec = await resolveLaunchSpec({ + env: { + HOME: "/tmp/codeclone-claude-no-home", + }, + platform: "linux", + cwd: subdir, + }); + + assert.deepEqual(spec, { + command: launcherPath, + args: ["--transport", "stdio"], + source: "workspaceLocal", + cwd: workspaceRoot, + }); +}); + +test("resolveLaunchSpec uses CODECLONE_WORKSPACE_ROOT even when cwd is wrong", async () => { + const workspaceRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "codeclone-claude-wsroot-"), + ); + const launcherPath = path.join(workspaceRoot, ".venv", "bin", "codeclone-mcp"); + fs.mkdirSync(path.dirname(launcherPath), {recursive: true}); + fs.writeFileSync(launcherPath, "#!/bin/sh\nexit 0\n", "utf8"); + fs.chmodSync(launcherPath, 0o755); + + const spec = await resolveLaunchSpec({ + env: { + HOME: "/tmp/codeclone-claude-no-home", + CODECLONE_WORKSPACE_ROOT: workspaceRoot, + }, + platform: "linux", + cwd: os.tmpdir(), // wrong cwd — simulates Claude Desktop launching outside the project + }); + + assert.deepEqual(spec, { + command: launcherPath, + args: ["--transport", "stdio"], + source: "workspaceLocal", + cwd: workspaceRoot, + }); +}); + +test("resolvePoetryLauncher finds the launcher inside the active Poetry env", async () => { + const workspaceRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "codeclone-claude-poetry-"), + ); + const toolRoot = fs.mkdtempSync(path.join(os.tmpdir(), "codeclone-claude-tools-")); + const poetryEnvRoot = path.join(toolRoot, "poetry-env"); + const launcherPath = path.join(poetryEnvRoot, "bin", "codeclone-mcp"); + fs.writeFileSync(path.join(workspaceRoot, "pyproject.toml"), "[tool.poetry]\nname='demo'\n", "utf8"); + fs.mkdirSync(path.dirname(launcherPath), {recursive: true}); + fs.writeFileSync(launcherPath, "#!/bin/sh\nexit 0\n", "utf8"); + fs.chmodSync(launcherPath, 0o755); + + const poetryBin = path.join(toolRoot, "poetry"); + fs.writeFileSync( + poetryBin, + "#!/bin/sh\nprintf '%s\\n' \"$FAKE_POETRY_ENV\"\n", + "utf8", + ); + fs.chmodSync(poetryBin, 0o755); + + const resolved = await resolvePoetryLauncher( + { + ...process.env, + PATH: toolRoot, + FAKE_POETRY_ENV: poetryEnvRoot, + }, + "linux", + workspaceRoot, + ); + + assert.deepEqual(resolved, {command: launcherPath, root: workspaceRoot}); +}); + test("buildSetupMessage stays actionable and bounded", () => { const text = buildSetupMessage(); - assert.match(text, /includes the MCP extra/); + assert.match(text, /workspace, Poetry environment, or PATH/); assert.match(text, /absolute launcher path/); }); @@ -147,7 +317,7 @@ test("server proxy launches the configured stdio child", async () => { assert.equal(exitCode, 0); assert.equal(stdoutChunks.join(""), '{"jsonrpc":"2.0","id":1,"method":"ping"}\n'); - assert.equal(stderrChunks.join(""), ""); + assert.match(stderrChunks.join(""), /\[codeclone\] launcher source=configured/); }); test("server proxy prints a setup hint when the launcher is missing", async () => { @@ -178,6 +348,55 @@ test("server proxy prints a setup hint when the launcher is missing", async () = assert.match(stderr, /CodeClone launcher not found/); }); +test("server proxy escalates to SIGKILL when the child ignores stdin close and SIGTERM", async () => { + const child = spawn( + process.execPath, + [serverEntry], + { + cwd: rootDir, + env: { + ...process.env, + CODECLONE_MCP_COMMAND: process.execPath, + CODECLONE_MCP_ARGS_JSON: JSON.stringify([hangScript]), + CODECLONE_MCP_SHUTDOWN_GRACE_MS: "100", + CODECLONE_MCP_KILL_GRACE_MS: "100", + }, + stdio: ["pipe", "pipe", "pipe"], + }, + ); + + let stderr = ""; + child.stderr.on("data", (chunk) => { + stderr += String(chunk); + }); + + // Give the hang-stdio child a beat to register its SIGTERM handler and + // start its keep-alive interval before we close stdin. + await new Promise((resolve) => setTimeout(resolve, 50)); + child.stdin.end(); + + const {code, signal} = await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + child.kill("SIGKILL"); + reject(new Error("wrapper did not escalate to SIGKILL in time")); + }, 3000); + child.on("exit", (exitCode, exitSignal) => { + clearTimeout(timer); + resolve({code: exitCode, signal: exitSignal}); + }); + }); + + // The wrapper should have completed shutdown escalation on its own + // (SIGTERM → wait → SIGKILL) and reported the terminating signal in + // its diagnostic stderr. We accept either a non-zero exit code or a + // signal exit: what matters is that the wrapper did NOT hang. + assert.ok( + (typeof code === "number" && code !== 0) || signal, + `wrapper exited cleanly instead of escalating (code=${code}, signal=${signal})`, + ); + assert.match(stderr, /Launcher exited via SIGKILL/); +}); + test("server proxy exits promptly on launcher startup failure even if stdin stays open", async () => { const child = spawn( process.execPath, From f9be59124c94159349d9fee4120e23316d27ef80 Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Wed, 8 Apr 2026 22:22:51 +0500 Subject: [PATCH 05/17] fix(core,ci): harden git diff validation, make segment digests canonical, and align CI policy --- .github/workflows/codeclone.yml | 2 +- .github/workflows/tests.yml | 2 +- CHANGELOG.md | 6 ++++ codeclone/_git_diff.py | 44 ++++++++++++++++++++++++++++++ codeclone/cli.py | 15 +++++----- codeclone/mcp_service.py | 13 +++++---- codeclone/pipeline.py | 4 ++- docs/architecture.md | 3 +- docs/book/09-cli.md | 2 ++ docs/book/11-security-model.md | 7 +++-- docs/book/20-mcp-interface.md | 2 ++ docs/mcp.md | 2 ++ tests/test_cli_unit.py | 23 ++++++++++++++++ tests/test_core_branch_coverage.py | 16 +++++++++++ tests/test_mcp_service.py | 8 +++++- 15 files changed, 127 insertions(+), 22 deletions(-) create mode 100644 codeclone/_git_diff.py diff --git a/.github/workflows/codeclone.yml b/.github/workflows/codeclone.yml index 392d3e3..d0566e4 100644 --- a/.github/workflows/codeclone.yml +++ b/.github/workflows/codeclone.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6.0.2 with: fetch-depth: 0 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 70fdff5..d07ce3f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,7 +40,7 @@ jobs: - name: Run tests # Smoke CLI tests intentionally disable subprocess coverage collection # to avoid runner-specific flakiness while keeping parent-process coverage strict. - run: uv run pytest --cov=codeclone --cov-report=term-missing --cov-fail-under=98 + run: uv run pytest --cov=codeclone --cov-report=term-missing --cov-fail-under=99 - name: Verify baseline exists if: ${{ matrix.python-version == '3.13' }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 288fa20..27c5e10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,12 @@ - Add `workspace_root` user-config field to the Claude Desktop bundle: setting it to the project directory forces the launcher to prefer `.venv` inside that path even when Claude Desktop starts with a different working directory (fixes python-tag mismatch caused by system-wide interpreter fallback). +- Validate `git_diff_ref` inputs as safe single revision expressions in both + CLI and MCP before invoking `git diff`. +- Replace the segment-group raw digest `repr()` payload with canonical JSON + bytes for cross-version-safe determinism. +- Align the tests workflow coverage gate with the canonical `fail_under = 99` + policy and refresh the remaining `actions/checkout` pin in `codeclone.yml`. - Refresh branch metadata and client docs for the `2.0.0b5` line. - Update the README repository health badge to `87 (B)`. diff --git a/codeclone/_git_diff.py b/codeclone/_git_diff.py new file mode 100644 index 0000000..d67f413 --- /dev/null +++ b/codeclone/_git_diff.py @@ -0,0 +1,44 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Den Rozhnovskiy + +from __future__ import annotations + +import re +from typing import Final + +_SAFE_GIT_DIFF_REF_RE: Final[re.Pattern[str]] = re.compile( + r"^(?![-./])[A-Za-z0-9._/@{}^~+-]+$" +) + + +def validate_git_diff_ref(git_diff_ref: str) -> str: + """Validate a safe, single git revision expression for `git diff`. + + CodeClone intentionally accepts a conservative subset of git revision + syntax here: common branch names, tags, revision operators (`~`, `^`), + reflog selectors (`@{...}`), and dotted range expressions. Whitespace, + control characters, option-like prefixes, and unsupported punctuation are + rejected before any subprocess call. + """ + + if git_diff_ref != git_diff_ref.strip(): + raise ValueError( + "Invalid git diff ref " + f"{git_diff_ref!r}: surrounding whitespace is not allowed." + ) + if not git_diff_ref: + raise ValueError("Invalid git diff ref '': value must not be empty.") + if any(ch.isspace() or ord(ch) < 32 or ord(ch) == 127 for ch in git_diff_ref): + raise ValueError( + "Invalid git diff ref " + f"{git_diff_ref!r}: whitespace and control characters are not allowed." + ) + if not _SAFE_GIT_DIFF_REF_RE.fullmatch(git_diff_ref): + raise ValueError( + "Invalid git diff ref " + f"{git_diff_ref!r}: expected a safe revision expression." + ) + return git_diff_ref diff --git a/codeclone/cli.py b/codeclone/cli.py index 5b91a25..fda7175 100644 --- a/codeclone/cli.py +++ b/codeclone/cli.py @@ -92,6 +92,7 @@ _print_metrics, _print_summary, ) +from ._git_diff import validate_git_diff_ref from .baseline import Baseline from .cache import Cache, CacheStatus, build_segment_report_projection from .contracts import ( @@ -270,16 +271,14 @@ def _normalize_changed_paths( def _git_diff_changed_paths(*, root_path: Path, git_diff_ref: str) -> tuple[str, ...]: - if git_diff_ref.startswith("-"): - console.print( - ui.fmt_contract_error( - f"Invalid git diff ref '{git_diff_ref}': must not start with '-'." - ) - ) + try: + validated_ref = validate_git_diff_ref(git_diff_ref) + except ValueError as exc: + console.print(ui.fmt_contract_error(str(exc))) sys.exit(ExitCode.CONTRACT_ERROR) try: completed = subprocess.run( - ["git", "diff", "--name-only", git_diff_ref, "--"], + ["git", "diff", "--name-only", validated_ref, "--"], cwd=str(root_path), check=True, capture_output=True, @@ -294,7 +293,7 @@ def _git_diff_changed_paths(*, root_path: Path, git_diff_ref: str) -> tuple[str, console.print( ui.fmt_contract_error( "Unable to resolve changed files from git diff ref " - f"'{git_diff_ref}': {exc}" + f"'{validated_ref}': {exc}" ) ) sys.exit(ExitCode.CONTRACT_ERROR) diff --git a/codeclone/mcp_service.py b/codeclone/mcp_service.py index 8f10d14..46cf8b3 100644 --- a/codeclone/mcp_service.py +++ b/codeclone/mcp_service.py @@ -47,6 +47,7 @@ ) from ._coerce import as_float as _as_float from ._coerce import as_int as _as_int +from ._git_diff import validate_git_diff_ref from .baseline import Baseline from .cache import Cache, CacheStatus from .contracts import ( @@ -793,13 +794,13 @@ def _git_diff_lines_payload( root_path: Path, git_diff_ref: str, ) -> tuple[str, ...]: - if git_diff_ref.startswith("-"): - raise MCPGitDiffError( - f"Invalid git diff ref '{git_diff_ref}': must not start with '-'." - ) + try: + validated_ref = validate_git_diff_ref(git_diff_ref) + except ValueError as exc: + raise MCPGitDiffError(str(exc)) from exc try: completed = subprocess.run( - ["git", "diff", "--name-only", git_diff_ref, "--"], + ["git", "diff", "--name-only", validated_ref, "--"], cwd=root_path, check=True, capture_output=True, @@ -808,7 +809,7 @@ def _git_diff_lines_payload( ) except (OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc: raise MCPGitDiffError( - f"Unable to resolve changed paths from git diff ref '{git_diff_ref}'." + f"Unable to resolve changed paths from git diff ref '{validated_ref}'." ) from exc return tuple( sorted({line.strip() for line in completed.stdout.splitlines() if line.strip()}) diff --git a/codeclone/pipeline.py b/codeclone/pipeline.py index 832141a..97f29dd 100644 --- a/codeclone/pipeline.py +++ b/codeclone/pipeline.py @@ -13,6 +13,8 @@ from pathlib import Path from typing import TYPE_CHECKING, Literal, cast +import orjson + from ._coerce import as_int, as_str from .cache import ( Cache, @@ -257,7 +259,7 @@ def _segment_groups_digest(segment_groups: GroupMap) -> str: for item in items ] normalized_rows.append((group_key, tuple(normalized_items))) - payload = repr(tuple(normalized_rows)).encode("utf-8") + payload = orjson.dumps(tuple(normalized_rows), option=orjson.OPT_SORT_KEYS) return sha256(payload).hexdigest() diff --git a/docs/architecture.md b/docs/architecture.md index ebbc3cc..01da554 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -224,7 +224,8 @@ Security boundaries: - `cache_policy=refresh` rejected to preserve read-only semantics. - Review markers are session-local in-memory state, never persisted. - Run history bounded by `--history-limit` to prevent unbounded memory growth. -- `git_diff_ref` validated against strict regex to prevent injection. +- `git_diff_ref` validated as a safe single revision expression before any + `git diff` subprocess call. --- diff --git a/docs/book/09-cli.md b/docs/book/09-cli.md index 0d059cb..4a6df83 100644 --- a/docs/book/09-cli.md +++ b/docs/book/09-cli.md @@ -75,6 +75,8 @@ Refs: - `--changed-only` requires either `--diff-against` or `--paths-from-git-diff`. - `--diff-against` requires `--changed-only`. - `--diff-against` and `--paths-from-git-diff` are mutually exclusive. +- Git diff refs are validated as safe single revision expressions before + subprocess execution. - Browser-open failure after a successful HTML write is warning-only and does not change the process exit code. - Baseline update write failure is contract error. - In gating mode, unreadable source files are contract errors with higher priority than clone gating failure. diff --git a/docs/book/11-security-model.md b/docs/book/11-security-model.md index 4c15efb..548c817 100644 --- a/docs/book/11-security-model.md +++ b/docs/book/11-security-model.md @@ -33,8 +33,9 @@ Security-relevant input classes: - `cache_policy=refresh` is rejected — MCP cannot trigger cache invalidation. - Review markers (`mark_finding_reviewed`) are session-local in-memory state; they are never persisted to disk or leaked into baselines/reports. -- `git_diff_ref` parameter is validated against a strict regex to prevent - command injection via shell-interpreted git arguments. +- `git_diff_ref` is validated as a safe single revision expression before any + `git diff` subprocess call. Leading option-like prefixes, whitespace/control + characters, and unsupported punctuation are rejected. - Run history is bounded by `--history-limit` (default 10) to prevent unbounded memory growth. @@ -68,7 +69,7 @@ Refs: | HTML-injected payload in metadata/source | Escaped output | | `--allow-remote` not passed for HTTP | Transport rejected | | `cache_policy=refresh` requested | Policy rejected | -| `git_diff_ref` fails regex | Parameter rejected | +| `git_diff_ref` fails validation | Parameter rejected | ## Determinism / canonicalization diff --git a/docs/book/20-mcp-interface.md b/docs/book/20-mcp-interface.md index 5b56fab..834f8dc 100644 --- a/docs/book/20-mcp-interface.md +++ b/docs/book/20-mcp-interface.md @@ -228,6 +228,8 @@ state behind `codeclone://latest/...`. - `analyze_changed_paths` requires `changed_paths` or `git_diff_ref`. - `analyze_repository` and `analyze_changed_paths` require an absolute `root`; relative roots like `.` are rejected. +- `git_diff_ref` is validated as a safe single revision expression before + invoking `git diff`. - `changed_paths` is a structured `list[str]` of repo-relative paths, not a comma-separated string payload. - `analyze_changed_paths` may return the same `run_id` as a previous run when diff --git a/docs/mcp.md b/docs/mcp.md index 90ce7f1..190cba1 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -236,6 +236,8 @@ Separate accepted baseline debt from new regressions. - Prefer `list_hotspots` or narrow `check_*` tools before broad `list_findings`. - Use `get_finding` / `get_remediation` for one finding instead of raising `detail_level` on larger lists. +- Keep `git_diff_ref` to a safe single revision expression; option-like, + whitespace-containing, and punctuated shell-style inputs are rejected. - Pass an absolute `root` — MCP rejects relative roots like `.`. - Use `"production-only"` / `source_kind` filters to cut test/fixture noise. - Use `mark_finding_reviewed` + `exclude_reviewed=true` in long sessions. diff --git a/tests/test_cli_unit.py b/tests/test_cli_unit.py index 98317b6..0165a7a 100644 --- a/tests/test_cli_unit.py +++ b/tests/test_cli_unit.py @@ -498,6 +498,29 @@ def test_git_diff_changed_paths_rejects_option_like_ref(tmp_path: Path) -> None: assert exc.value.code == 2 +@pytest.mark.parametrize( + "git_diff_ref", + [ + "HEAD~1;rm", + "HEAD path", + "HEAD:path", + "../HEAD", + " HEAD", + ], +) +def test_git_diff_changed_paths_rejects_unsafe_ref_syntax( + tmp_path: Path, + git_diff_ref: str, +) -> None: + cli.console = cli._make_console(no_color=True) + with pytest.raises(SystemExit) as exc: + cli._git_diff_changed_paths( + root_path=tmp_path.resolve(), + git_diff_ref=git_diff_ref, + ) + assert exc.value.code == 2 + + def test_report_path_origins_ignores_unrelated_equals_tokens() -> None: assert cli._report_path_origins(("--unknown=value", "--json=out.json")) == { "html": None, diff --git a/tests/test_core_branch_coverage.py b/tests/test_core_branch_coverage.py index 888f8dc..b26fdb6 100644 --- a/tests/test_core_branch_coverage.py +++ b/tests/test_core_branch_coverage.py @@ -7,9 +7,11 @@ from __future__ import annotations from argparse import Namespace +from hashlib import sha256 from pathlib import Path from typing import cast +import orjson import pytest import codeclone.cli as cli @@ -501,6 +503,20 @@ def test_pipeline_analyze_uses_cached_segment_projection( }, } + expected_payload = orjson.dumps( + ( + ( + "seg-hash|pkg.a:f", + ( + ("/tmp/a.py", "pkg.a:f", 10, 15, 6, "seg-hash", "seg-sig"), + ("/tmp/a.py", "pkg.a:f", 20, 25, 6, "seg-hash", "seg-sig"), + ), + ), + ), + option=orjson.OPT_SORT_KEYS, + ) + assert digest == sha256(expected_payload).hexdigest() + def _must_not_run( _segment_groups: object, ) -> tuple[dict[str, list[dict[str, object]]], int]: diff --git a/tests/test_mcp_service.py b/tests/test_mcp_service.py index 723e924..a1bbf5c 100644 --- a/tests/test_mcp_service.py +++ b/tests/test_mcp_service.py @@ -1415,12 +1415,18 @@ def test_mcp_service_git_diff_and_helper_branch_edges( ) -> None: service = CodeCloneMCPService(history_limit=4) - with pytest.raises(MCPGitDiffError, match="must not start with '-'"): + with pytest.raises(MCPGitDiffError, match="Invalid git diff ref"): mcp_service_mod._git_diff_lines_payload( root_path=tmp_path, git_diff_ref="--cached", ) + with pytest.raises(MCPGitDiffError, match="safe revision expression"): + mcp_service_mod._git_diff_lines_payload( + root_path=tmp_path, + git_diff_ref="HEAD:path", + ) + assert service._normalize_relative_path("./.github/workflows/docs.yml") == ( ".github/workflows/docs.yml" ) From 7ef49d0b1a76d2fcbc35a4793a0cd9d53ea5f719 Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Thu, 9 Apr 2026 22:40:11 +0500 Subject: [PATCH 06/17] feat(metrics): add adoption and public API baselines with compact schema-aware storage --- CHANGELOG.md | 28 +- README.md | 29 +- codeclone.baseline.json | 13286 +++++++++++++++- codeclone/_cli_args.py | 46 + codeclone/_cli_baselines.py | 72 +- codeclone/_cli_config.py | 8 + codeclone/_cli_gating.py | 98 +- codeclone/_cli_runtime.py | 25 + codeclone/_cli_summary.py | 31 + codeclone/_html_css.py | 14 + codeclone/_html_report/_components.py | 2 + codeclone/_html_report/_icons.py | 10 + codeclone/_html_report/_sections/_overview.py | 148 + codeclone/baseline.py | 51 +- codeclone/cache.py | 768 +- codeclone/cli.py | 58 +- codeclone/contracts.py | 6 +- codeclone/extractor.py | 35 + codeclone/mcp_server.py | 14 + codeclone/mcp_service.py | 82 +- codeclone/metrics/_visibility.py | 154 + codeclone/metrics/adoption.py | 166 + codeclone/metrics/api_surface.py | 421 + codeclone/metrics_baseline.py | 532 +- codeclone/models.py | 85 + codeclone/pipeline.py | 908 +- codeclone/report/json_contract.py | 148 + codeclone/ui_messages.py | 74 + docs/README.md | 2 +- docs/architecture.md | 4 +- docs/book/06-baseline.md | 19 +- docs/book/08-report.md | 18 +- docs/book/09-cli.md | 63 +- docs/book/13-testing-as-spec.md | 2 +- docs/book/14-compatibility-and-versioning.md | 29 +- docs/book/15-metrics-and-quality-gates.md | 38 +- docs/book/20-mcp-interface.md | 11 +- docs/book/appendix/b-schema-layouts.md | 135 +- docs/mcp.md | 6 +- .../golden_expected_cli_snapshot.json | 2 +- tests/test_adoption.py | 197 + tests/test_api_surface.py | 405 + tests/test_baseline.py | 81 +- tests/test_cache.py | 81 + tests/test_cli_inprocess.py | 16 + tests/test_cli_unit.py | 179 +- tests/test_html_report.py | 74 + tests/test_mcp_service.py | 101 + tests/test_metrics_baseline.py | 421 +- tests/test_pipeline_metrics.py | 441 +- tests/test_report_contract_coverage.py | 214 +- uv.lock | 168 +- 52 files changed, 19590 insertions(+), 416 deletions(-) create mode 100644 codeclone/metrics/_visibility.py create mode 100644 codeclone/metrics/adoption.py create mode 100644 codeclone/metrics/api_surface.py create mode 100644 tests/test_adoption.py create mode 100644 tests/test_api_surface.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 27c5e10..17330e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,35 @@ ## [2.0.0b5] +### Contracts, metrics, and review surfaces + +- Bump canonical report schema to `2.5` for `metrics.families.coverage_adoption` + and `metrics.families.api_surface`. +- Bump clone baseline schema to `2.1` and standalone metrics-baseline schema to + `1.2` for compact `api_surface` wire payloads (`local_name` on disk, + reconstructed full qualnames in runtime) while keeping read-compatibility for + earlier `2.0` / `1.1` baseline files in the current b5 line. +- Add shared public/private visibility classification for public-symbol-aware + metrics, without changing clone/fingerprint semantics. +- Add canonical type/docstring adoption coverage: + parameter coverage, return coverage, public docstring coverage, and explicit + `Any` counts. +- Add opt-in public API surface inventory and baseline diff: + public symbol snapshots, added symbols, and breaking changes against a + trusted metrics baseline. +- Add new gates: + `--min-typing-coverage`, `--min-docstring-coverage`, + `--fail-on-typing-regression`, `--fail-on-docstring-regression`, + `--fail-on-api-break`. +- Surface adoption and API metrics compactly in MCP summaries/detail, the HTML + Overview tab, and canonical report payloads without adding a new HTML tab. +- Extend the normal CLI `Metrics` block with adoption coverage and public API + facts, while keeping the quiet compact metrics line unchanged. +- Make unified clone baselines preserve embedded metrics and optional + `api_surface` payloads safely across saves. + ### MCP, HTML, and docs -- Bump canonical report schema to `2.4` for `meta.analysis_profile`. - Surface the effective runtime analysis profile (`min_loc`, `min_stmt`, block, and segment thresholds) in canonical report metadata, MCP summary/triage projections, and the HTML Executive Summary subtitle. - Clarify MCP interpretation with compact `health_scope`, `focus`, and `new_by_source_kind` fields in summary/triage diff --git a/README.md b/README.md index 3a6a260..1426fe8 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,8 @@ Live sample report: - **Clone detection** — function (CFG fingerprint), block (statement windows), and segment (report-only) clones - **Structural findings** — duplicated branch families, clone guard/exit divergence and clone-cohort drift (report-only) - **Quality metrics** — cyclomatic complexity, coupling (`CBO`), cohesion (`LCOM4`), dependency cycles, dead code, - health score, and report-only `Overloaded Modules` profiling + health score, type/docstring adoption coverage, public API surface diff, and report-only `Overloaded Modules` + profiling - **Baseline governance** — separates accepted **legacy** debt from **new regressions** and lets CI fail **only** on what changed - **Reports** — interactive HTML, deterministic JSON/TXT plus Markdown and SARIF projections from one canonical report @@ -141,8 +142,18 @@ codeclone . --fail-cycles --fail-dead-code # Regression detection vs baseline codeclone . --fail-on-new-metrics + +# Adoption and API governance +codeclone . --min-typing-coverage 80 --min-docstring-coverage 60 +codeclone . --fail-on-typing-regression --fail-on-docstring-regression +codeclone . --api-surface --update-metrics-baseline +codeclone . --fail-on-api-break ``` +In normal full-mode CLI output, CodeClone now surfaces adoption coverage +(`params`, `returns`, `docstrings`, `Any`) in the main `Metrics` block, and it +adds a `Public API` line when `--api-surface` facts are collected. + ### Pre-commit ```yaml @@ -277,11 +288,11 @@ class Middleware: # codeclone: ignore[dead-code] Dynamic/runtime false positives are resolved via explicit inline suppressions, not via broad heuristics.
-Canonical JSON report shape (v2.4) +Canonical JSON report shape (v2.5) ```json { - "report_schema_version": "2.4", + "report_schema_version": "2.5", "meta": { "codeclone_version": "2.0.0b5", "project_name": "...", @@ -348,8 +359,16 @@ Dynamic/runtime false positives are resolved via explicit inline suppressions, n } }, "metrics": { - "summary": {}, - "families": {} + "summary": { + "...": "...", + "coverage_adoption": { "...": "..." }, + "api_surface": { "...": "..." } + }, + "families": { + "...": "...", + "coverage_adoption": { "...": "..." }, + "api_surface": { "...": "..." } + } }, "derived": { "suggestions": [], diff --git a/codeclone.baseline.json b/codeclone.baseline.json index a2d9b88..f09d76d 100644 --- a/codeclone.baseline.json +++ b/codeclone.baseline.json @@ -2,14 +2,15 @@ "meta": { "generator": { "name": "codeclone", - "version": "2.0.0b4" + "version": "2.0.0b5" }, - "schema_version": "2.0", + "schema_version": "2.1", "fingerprint_version": "1", "python_tag": "cp313", - "created_at": "2026-04-04T18:57:08Z", - "payload_sha256": "691c6cedd10e2a51d6038780f3ae9dffe763356dd2aba742b3980f131b79f217", - "metrics_payload_sha256": "07e216e9a158e4dc56ad2ee6ca8069896cd17d39bdb0b8a3745ebd98627d0d25" + "created_at": "2026-04-09T17:35:38Z", + "payload_sha256": "c0b4a5a4f5aa567069a48e896c36a1792a8dfa5bd306f130084d9ae2b1d4e42c", + "metrics_payload_sha256": "d8949db71b78a98ae69c7ed44bc9a51a516a78d6af96cf0949ddbb8bee4401b6", + "api_surface_payload_sha256": "72e5bbf17f0ddefe404d13a017010390994470d7e2546d1d80d20fdeb9feff81" }, "clones": { "functions": [ @@ -22,6 +23,7 @@ "b4b5893be87edf98955f047cbf25ca755dc753b4|8579659a9e8c9755a6d2f0b1d82dda8866fd243b|1912d2ee3c541cbf9e51f485348586afe1a00755|ee69aff0b7ea38927e5082ceef14115c805f6734", "b6ee70d0bd6ff4b593f127a137aed9ab41179145|cacc33d58f323481f65fed57873d1c840531859e|d60c0005a4c850c140378d1c82b81dde93a7ccab|d60c0005a4c850c140378d1c82b81dde93a7ccab", "cacc33d58f323481f65fed57873d1c840531859e|d60c0005a4c850c140378d1c82b81dde93a7ccab|d60c0005a4c850c140378d1c82b81dde93a7ccab|b4b5893be87edf98955f047cbf25ca755dc753b4", + "dd877bb74646fec86e10e1b27f5f0d538f1b8311|58eeda1867bae218607b5c1e688b99794bf59b2d|6e2ea78e278f92a5d09654ea6ceaa8a703cccdd7|6ad0cbb6a95fa366a456b907d5f7bfe1c1590c38", "ee69aff0b7ea38927e5082ceef14115c805f6734|fcd36b4275c94f1955fb55e1c1ca3c04c7c0bb26|3c1b5cf24b4dfcd8e5736b735bfd3850940100d5|3c1b5cf24b4dfcd8e5736b735bfd3850940100d5" ] }, @@ -36,6 +38,13278 @@ "dependency_max_depth": 10, "dead_code_items": [], "health_score": 87, - "health_grade": "B" + "health_grade": "B", + "typing_param_permille": 1000, + "typing_return_permille": 998, + "docstring_permille": 16, + "typing_any_count": 44 + }, + "api_surface": { + "modules": [ + { + "module": "codeclone._cli_gating", + "filepath": "codeclone/_cli_gating.py", + "all_declared": [ + "parse_metric_reason_entry", + "policy_context", + "print_gating_failure_block" + ], + "symbols": [ + { + "local_name": "parse_metric_reason_entry", + "kind": "function", + "start_line": 59, + "end_line": 121, + "params": [ + { + "name": "reason", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + } + ], + "returns_hash": "Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='str', ctx=Load())], ctx=Load()), ctx=Load())", + "exported_via": "all" + }, + { + "local_name": "policy_context", + "kind": "function", + "start_line": 124, + "end_line": 181, + "params": [ + { + "name": "args", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='_GatingArgs', ctx=Load())" + }, + { + "name": "gate_kind", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "all" + }, + { + "local_name": "print_gating_failure_block", + "kind": "function", + "start_line": 184, + "end_line": 197, + "params": [ + { + "name": "args", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='_GatingArgs', ctx=Load())" + }, + { + "name": "code", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "console", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='_PrinterLike', ctx=Load())" + }, + { + "name": "entries", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "BinOp(left=Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load()), Constant(value=Ellipsis)], ctx=Load()), ctx=Load()), op=BitOr(), right=Subscript(value=Name(id='list', ctx=Load()), slice=Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load()), ctx=Load()))" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "all" + } + ] + }, + { + "module": "codeclone.baseline", + "filepath": "codeclone/baseline.py", + "all_declared": [], + "symbols": [ + { + "local_name": "BASELINE_GENERATOR", + "kind": "constant", + "start_line": 37, + "end_line": 37, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "BASELINE_UNTRUSTED_STATUSES", + "kind": "constant", + "start_line": 57, + "end_line": 71, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "Baseline", + "kind": "class", + "start_line": 104, + "end_line": 430, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "Baseline.__init__", + "kind": "method", + "start_line": 118, + "end_line": 128, + "params": [ + { + "name": "path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Name(id='Path', ctx=Load()))" + } + ], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "Baseline.diff", + "kind": "method", + "start_line": 425, + "end_line": 430, + "params": [ + { + "name": "block_groups", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())" + }, + { + "name": "func_groups", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())" + } + ], + "returns_hash": "Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Subscript(value=Name(id='set', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load()), Subscript(value=Name(id='set', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "Baseline.from_groups", + "kind": "method", + "start_line": 404, + "end_line": 423, + "params": [ + { + "name": "block_groups", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())" + }, + { + "name": "fingerprint_version", + "kind": "pos_or_kw", + "has_default": true, + "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "func_groups", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())" + }, + { + "name": "generator_version", + "kind": "pos_or_kw", + "has_default": true, + "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "path", + "kind": "pos_or_kw", + "has_default": true, + "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Name(id='Path', ctx=Load()))" + }, + { + "name": "python_tag", + "kind": "pos_or_kw", + "has_default": true, + "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "schema_version", + "kind": "pos_or_kw", + "has_default": true, + "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Constant(value=None))" + } + ], + "returns_hash": "Name(id='Baseline', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "Baseline.load", + "kind": "method", + "start_line": 130, + "end_line": 234, + "params": [ + { + "name": "max_size_bytes", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Name(id='int', ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "preloaded_payload", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='Any', ctx=Load())], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "Baseline.save", + "kind": "method", + "start_line": 236, + "end_line": 298, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "Baseline.verify_compatibility", + "kind": "method", + "start_line": 300, + "end_line": 356, + "params": [ + { + "name": "current_python_tag", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "Baseline.verify_integrity", + "kind": "method", + "start_line": 358, + "end_line": 401, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "BaselineStatus", + "kind": "class", + "start_line": 42, + "end_line": 54, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "MAX_BASELINE_SIZE_BYTES", + "kind": "constant", + "start_line": 39, + "end_line": 39, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "coerce_baseline_status", + "kind": "function", + "start_line": 74, + "end_line": 84, + "params": [ + { + "name": "raw_status", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "BinOp(left=BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Name(id='BaselineStatus', ctx=Load())), op=BitOr(), right=Constant(value=None))" + } + ], + "returns_hash": "Name(id='BaselineStatus', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "current_python_tag", + "kind": "function", + "start_line": 650, + "end_line": 655, + "params": [], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + } + ] + }, + { + "module": "codeclone.cache", + "filepath": "codeclone/cache.py", + "all_declared": [], + "symbols": [ + { + "local_name": "AnalysisProfile", + "kind": "class", + "start_line": 234, + "end_line": 240, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ApiParamSpecDict", + "kind": "class", + "start_line": 175, + "end_line": 179, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "BlockDict", + "kind": "constant", + "start_line": 116, + "end_line": 116, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "Cache", + "kind": "class", + "start_line": 316, + "end_line": 836, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "Cache.__init__", + "kind": "method", + "start_line": 335, + "end_line": 374, + "params": [ + { + "name": "block_min_loc", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "block_min_stmt", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "max_size_bytes", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Name(id='int', ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "min_loc", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "min_stmt", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "root", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Name(id='Path', ctx=Load())), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "segment_min_loc", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "segment_min_stmt", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Name(id='Path', ctx=Load()))" + } + ], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "Cache.get_file_entry", + "kind": "method", + "start_line": 659, + "end_line": 732, + "params": [ + { + "name": "filepath", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + } + ], + "returns_hash": "BinOp(left=Name(id='CacheEntry', ctx=Load()), op=BitOr(), right=Constant(value=None))", + "exported_via": "name" + }, + { + "local_name": "Cache.load", + "kind": "method", + "start_line": 447, + "end_line": 494, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "Cache.put_file_entry", + "kind": "method", + "start_line": 734, + "end_line": 836, + "params": [ + { + "name": "file_metrics", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Name(id='FileMetrics', ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "source_stats", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Name(id='SourceStatsDict', ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "structural_findings", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Subscript(value=Name(id='list', ctx=Load()), slice=Name(id='StructuralFindingGroup', ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "blocks", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='list', ctx=Load()), slice=Name(id='BlockUnit', ctx=Load()), ctx=Load())" + }, + { + "name": "filepath", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "segments", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='list', ctx=Load()), slice=Name(id='SegmentUnit', ctx=Load()), ctx=Load())" + }, + { + "name": "stat_sig", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='FileStat', ctx=Load())" + }, + { + "name": "units", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='list', ctx=Load()), slice=Name(id='Unit', ctx=Load()), ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "Cache.save", + "kind": "method", + "start_line": 594, + "end_line": 635, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "CacheData", + "kind": "class", + "start_line": 243, + "end_line": 248, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "CacheEntry", + "kind": "class", + "start_line": 219, + "end_line": 231, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "CacheEntryBase", + "kind": "class", + "start_line": 212, + "end_line": 216, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "CacheStatus", + "kind": "class", + "start_line": 89, + "end_line": 100, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ClassMetricsDict", + "kind": "class", + "start_line": 133, + "end_line": 134, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ClassMetricsDictBase", + "kind": "class", + "start_line": 120, + "end_line": 130, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "DeadCandidateDict", + "kind": "class", + "start_line": 153, + "end_line": 154, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "DeadCandidateDictBase", + "kind": "class", + "start_line": 144, + "end_line": 150, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "FileStat", + "kind": "class", + "start_line": 103, + "end_line": 105, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "LEGACY_CACHE_SECRET_FILENAME", + "kind": "constant", + "start_line": 78, + "end_line": 78, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "MAX_CACHE_SIZE_BYTES", + "kind": "constant", + "start_line": 77, + "end_line": 77, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ModuleApiSurfaceDict", + "kind": "class", + "start_line": 192, + "end_line": 196, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ModuleDepDict", + "kind": "class", + "start_line": 137, + "end_line": 141, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ModuleDocstringCoverageDict", + "kind": "class", + "start_line": 168, + "end_line": 172, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ModuleTypingCoverageDict", + "kind": "class", + "start_line": 157, + "end_line": 165, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "PublicSymbolDict", + "kind": "class", + "start_line": 182, + "end_line": 189, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SegmentDict", + "kind": "constant", + "start_line": 117, + "end_line": 117, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SegmentReportProjection", + "kind": "constant", + "start_line": 70, + "end_line": 70, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SourceStatsDict", + "kind": "class", + "start_line": 108, + "end_line": 112, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "StructuralFindingGroupDict", + "kind": "class", + "start_line": 205, + "end_line": 209, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "StructuralFindingOccurrenceDict", + "kind": "class", + "start_line": 199, + "end_line": 202, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "UnitDict", + "kind": "constant", + "start_line": 115, + "end_line": 115, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "build_segment_report_projection", + "kind": "constant", + "start_line": 71, + "end_line": 71, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "file_stat_signature", + "kind": "function", + "start_line": 839, + "end_line": 844, + "params": [ + { + "name": "path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + } + ], + "returns_hash": "Name(id='FileStat', ctx=Load())", + "exported_via": "name" + } + ] + }, + { + "module": "codeclone.cli", + "filepath": "codeclone/cli.py", + "all_declared": [ + "ExitCode", + "MAX_FILE_SIZE", + "ProcessingResult", + "analyze", + "bootstrap", + "discover", + "gate", + "main", + "process", + "process_file", + "report" + ], + "symbols": [ + { + "local_name": "MAX_FILE_SIZE", + "kind": "constant", + "start_line": 139, + "end_line": 139, + "params": [], + "returns_hash": "", + "exported_via": "all" + }, + { + "local_name": "ProcessingResult", + "kind": "class", + "start_line": 178, + "end_line": 192, + "params": [], + "returns_hash": "", + "exported_via": "all" + }, + { + "local_name": "analyze", + "kind": "function", + "start_line": 466, + "end_line": 479, + "params": [ + { + "name": "boot", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='BootstrapResult', ctx=Load())" + }, + { + "name": "discovery", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='DiscoveryResult', ctx=Load())" + }, + { + "name": "processing", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='PipelineProcessingResult', ctx=Load())" + } + ], + "returns_hash": "Name(id='AnalysisResult', ctx=Load())", + "exported_via": "all" + }, + { + "local_name": "bootstrap", + "kind": "function", + "start_line": 422, + "end_line": 437, + "params": [ + { + "name": "args", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='Namespace', ctx=Load())" + }, + { + "name": "cache_path", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + }, + { + "name": "output_paths", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "BinOp(left=Name(id='PipelineOutputPaths', ctx=Load()), op=BitOr(), right=Name(id='OutputPaths', ctx=Load()))" + }, + { + "name": "root", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Name(id='BootstrapResult', ctx=Load())", + "exported_via": "all" + }, + { + "local_name": "discover", + "kind": "function", + "start_line": 440, + "end_line": 441, + "params": [ + { + "name": "boot", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='BootstrapResult', ctx=Load())" + }, + { + "name": "cache", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='Cache', ctx=Load())" + } + ], + "returns_hash": "Name(id='DiscoveryResult', ctx=Load())", + "exported_via": "all" + }, + { + "local_name": "gate", + "kind": "function", + "start_line": 512, + "end_line": 529, + "params": [ + { + "name": "analysis", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='AnalysisResult', ctx=Load())" + }, + { + "name": "boot", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='BootstrapResult', ctx=Load())" + }, + { + "name": "metrics_diff", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "BinOp(left=Name(id='MetricsDiff', ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "new_block", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='set', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "new_func", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='set', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + } + ], + "returns_hash": "Name(id='GatingResult', ctx=Load())", + "exported_via": "all" + }, + { + "local_name": "main", + "kind": "function", + "start_line": 1634, + "end_line": 1647, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "all" + }, + { + "local_name": "process", + "kind": "function", + "start_line": 444, + "end_line": 463, + "params": [ + { + "name": "boot", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='BootstrapResult', ctx=Load())" + }, + { + "name": "cache", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='Cache', ctx=Load())" + }, + { + "name": "discovery", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='DiscoveryResult', ctx=Load())" + }, + { + "name": "on_advance", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[List(ctx=Load()), Constant(value=None)], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "on_parallel_fallback", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[List(elts=[Name(id='Exception', ctx=Load())], ctx=Load()), Constant(value=None)], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "on_worker_error", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[List(elts=[Name(id='str', ctx=Load())], ctx=Load()), Constant(value=None)], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" + } + ], + "returns_hash": "Name(id='PipelineProcessingResult', ctx=Load())", + "exported_via": "all" + }, + { + "local_name": "process_file", + "kind": "function", + "start_line": 402, + "end_line": 419, + "params": [ + { + "name": "cfg", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='NormalizationConfig', ctx=Load())" + }, + { + "name": "collect_structural_findings", + "kind": "pos_or_kw", + "has_default": true, + "annotation_hash": "Name(id='bool', ctx=Load())" + }, + { + "name": "filepath", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "min_loc", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "min_stmt", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "root", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + } + ], + "returns_hash": "Name(id='ProcessingResult', ctx=Load())", + "exported_via": "all" + }, + { + "local_name": "report", + "kind": "function", + "start_line": 482, + "end_line": 509, + "params": [ + { + "name": "analysis", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='AnalysisResult', ctx=Load())" + }, + { + "name": "boot", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='BootstrapResult', ctx=Load())" + }, + { + "name": "discovery", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='DiscoveryResult', ctx=Load())" + }, + { + "name": "html_builder", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[Constant(value=Ellipsis), Name(id='str', ctx=Load())], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "include_report_document", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "Name(id='bool', ctx=Load())" + }, + { + "name": "metrics_diff", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Name(id='MetricsDiff', ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "new_block", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='set', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "new_func", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='set', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "processing", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='PipelineProcessingResult', ctx=Load())" + }, + { + "name": "report_meta", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())" + } + ], + "returns_hash": "Name(id='ReportArtifacts', ctx=Load())", + "exported_via": "all" + } + ] + }, + { + "module": "codeclone.contracts", + "filepath": "codeclone/contracts.py", + "all_declared": [], + "symbols": [ + { + "local_name": "BASELINE_FINGERPRINT_VERSION", + "kind": "constant", + "start_line": 13, + "end_line": 13, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "BASELINE_SCHEMA_VERSION", + "kind": "constant", + "start_line": 12, + "end_line": 12, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "CACHE_VERSION", + "kind": "constant", + "start_line": 15, + "end_line": 15, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "COHESION_RISK_MEDIUM_MAX", + "kind": "constant", + "start_line": 31, + "end_line": 31, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "COMPLEXITY_RISK_LOW_MAX", + "kind": "constant", + "start_line": 27, + "end_line": 27, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "COMPLEXITY_RISK_MEDIUM_MAX", + "kind": "constant", + "start_line": 28, + "end_line": 28, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "COUPLING_RISK_LOW_MAX", + "kind": "constant", + "start_line": 29, + "end_line": 29, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "COUPLING_RISK_MEDIUM_MAX", + "kind": "constant", + "start_line": 30, + "end_line": 30, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "DEFAULT_COHESION_THRESHOLD", + "kind": "constant", + "start_line": 21, + "end_line": 21, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "DEFAULT_COMPLEXITY_THRESHOLD", + "kind": "constant", + "start_line": 19, + "end_line": 19, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "DEFAULT_COUPLING_THRESHOLD", + "kind": "constant", + "start_line": 20, + "end_line": 20, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "DEFAULT_HEALTH_THRESHOLD", + "kind": "constant", + "start_line": 25, + "end_line": 25, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "DEFAULT_REPORT_DESIGN_COHESION_THRESHOLD", + "kind": "constant", + "start_line": 24, + "end_line": 24, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "DEFAULT_REPORT_DESIGN_COMPLEXITY_THRESHOLD", + "kind": "constant", + "start_line": 22, + "end_line": 22, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "DEFAULT_REPORT_DESIGN_COUPLING_THRESHOLD", + "kind": "constant", + "start_line": 23, + "end_line": 23, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "DOCS_URL", + "kind": "constant", + "start_line": 53, + "end_line": 53, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ExitCode", + "kind": "class", + "start_line": 44, + "end_line": 48, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HEALTH_WEIGHTS", + "kind": "constant", + "start_line": 33, + "end_line": 41, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ISSUES_URL", + "kind": "constant", + "start_line": 52, + "end_line": 52, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "METRICS_BASELINE_SCHEMA_VERSION", + "kind": "constant", + "start_line": 17, + "end_line": 17, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "REPORT_SCHEMA_VERSION", + "kind": "constant", + "start_line": 16, + "end_line": 16, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "REPOSITORY_URL", + "kind": "constant", + "start_line": 51, + "end_line": 51, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "cli_help_epilog", + "kind": "function", + "start_line": 56, + "end_line": 72, + "params": [], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + } + ] + }, + { + "module": "codeclone.metrics.adoption", + "filepath": "codeclone/metrics/adoption.py", + "all_declared": [ + "collect_module_adoption" + ], + "symbols": [ + { + "local_name": "collect_module_adoption", + "kind": "function", + "start_line": 25, + "end_line": 105, + "params": [ + { + "name": "collector", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='QualnameCollector', ctx=Load())" + }, + { + "name": "filepath", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "imported_names", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='frozenset', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "module_name", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "tree", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='ast', ctx=Load()), attr='Module', ctx=Load())" + } + ], + "returns_hash": "Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='ModuleTypingCoverage', ctx=Load()), Name(id='ModuleDocstringCoverage', ctx=Load())], ctx=Load()), ctx=Load())", + "exported_via": "all" + } + ] + }, + { + "module": "codeclone.metrics.api_surface", + "filepath": "codeclone/metrics/api_surface.py", + "all_declared": [ + "collect_module_api_surface", + "compare_api_surfaces" + ], + "symbols": [ + { + "local_name": "collect_module_api_surface", + "kind": "function", + "start_line": 34, + "end_line": 102, + "params": [ + { + "name": "collector", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='QualnameCollector', ctx=Load())" + }, + { + "name": "filepath", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "imported_names", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='frozenset', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "include_private_modules", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "Name(id='bool', ctx=Load())" + }, + { + "name": "module_name", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "tree", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='ast', ctx=Load()), attr='Module', ctx=Load())" + } + ], + "returns_hash": "BinOp(left=Name(id='ModuleApiSurface', ctx=Load()), op=BitOr(), right=Constant(value=None))", + "exported_via": "all" + }, + { + "local_name": "compare_api_surfaces", + "kind": "function", + "start_line": 209, + "end_line": 266, + "params": [ + { + "name": "baseline", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "BinOp(left=Name(id='ApiSurfaceSnapshot', ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "current", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "BinOp(left=Name(id='ApiSurfaceSnapshot', ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "strict_types", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='bool', ctx=Load())" + } + ], + "returns_hash": "Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Constant(value=Ellipsis)], ctx=Load()), ctx=Load()), Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='ApiBreakingChange', ctx=Load()), Constant(value=Ellipsis)], ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())", + "exported_via": "all" + } + ] + }, + { + "module": "codeclone.metrics_baseline", + "filepath": "codeclone/metrics_baseline.py", + "all_declared": [ + "BASELINE_SCHEMA_VERSION", + "MAX_METRICS_BASELINE_SIZE_BYTES", + "METRICS_BASELINE_GENERATOR", + "METRICS_BASELINE_SCHEMA_VERSION", + "METRICS_BASELINE_UNTRUSTED_STATUSES", + "MetricsBaseline", + "MetricsBaselineStatus", + "coerce_metrics_baseline_status", + "current_python_tag", + "snapshot_from_project_metrics" + ], + "symbols": [ + { + "local_name": "MAX_METRICS_BASELINE_SIZE_BYTES", + "kind": "constant", + "start_line": 42, + "end_line": 42, + "params": [], + "returns_hash": "", + "exported_via": "all" + }, + { + "local_name": "METRICS_BASELINE_GENERATOR", + "kind": "constant", + "start_line": 41, + "end_line": 41, + "params": [], + "returns_hash": "", + "exported_via": "all" + }, + { + "local_name": "METRICS_BASELINE_UNTRUSTED_STATUSES", + "kind": "constant", + "start_line": 59, + "end_line": 74, + "params": [], + "returns_hash": "", + "exported_via": "all" + }, + { + "local_name": "MetricsBaseline", + "kind": "class", + "start_line": 219, + "end_line": 637, + "params": [], + "returns_hash": "", + "exported_via": "all" + }, + { + "local_name": "MetricsBaseline.__init__", + "kind": "method", + "start_line": 235, + "end_line": 247, + "params": [ + { + "name": "path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Name(id='Path', ctx=Load()))" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "all" + }, + { + "local_name": "MetricsBaseline.diff", + "kind": "method", + "start_line": 566, + "end_line": 637, + "params": [ + { + "name": "current", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='ProjectMetrics', ctx=Load())" + } + ], + "returns_hash": "Name(id='MetricsDiff', ctx=Load())", + "exported_via": "all" + }, + { + "local_name": "MetricsBaseline.from_project_metrics", + "kind": "method", + "start_line": 538, + "end_line": 564, + "params": [ + { + "name": "generator_version", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "path", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Name(id='Path', ctx=Load()))" + }, + { + "name": "project_metrics", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='ProjectMetrics', ctx=Load())" + }, + { + "name": "python_tag", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "schema_version", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Constant(value=None))" + } + ], + "returns_hash": "Name(id='MetricsBaseline', ctx=Load())", + "exported_via": "all" + }, + { + "local_name": "MetricsBaseline.load", + "kind": "method", + "start_line": 249, + "end_line": 344, + "params": [ + { + "name": "max_size_bytes", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Name(id='int', ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "preloaded_payload", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "all" + }, + { + "local_name": "MetricsBaseline.save", + "kind": "method", + "start_line": 346, + "end_line": 437, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "all" + }, + { + "local_name": "MetricsBaseline.verify_compatibility", + "kind": "method", + "start_line": 439, + "end_line": 466, + "params": [ + { + "name": "runtime_python_tag", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "all" + }, + { + "local_name": "MetricsBaseline.verify_integrity", + "kind": "method", + "start_line": 468, + "end_line": 535, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "all" + }, + { + "local_name": "MetricsBaselineStatus", + "kind": "class", + "start_line": 45, + "end_line": 56, + "params": [], + "returns_hash": "", + "exported_via": "all" + }, + { + "local_name": "coerce_metrics_baseline_status", + "kind": "function", + "start_line": 110, + "end_line": 120, + "params": [ + { + "name": "raw_status", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "BinOp(left=BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Name(id='MetricsBaselineStatus', ctx=Load())), op=BitOr(), right=Constant(value=None))" + } + ], + "returns_hash": "Name(id='MetricsBaselineStatus', ctx=Load())", + "exported_via": "all" + }, + { + "local_name": "snapshot_from_project_metrics", + "kind": "function", + "start_line": 123, + "end_line": 153, + "params": [ + { + "name": "project_metrics", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='ProjectMetrics', ctx=Load())" + } + ], + "returns_hash": "Name(id='MetricsSnapshot', ctx=Load())", + "exported_via": "all" + } + ] + }, + { + "module": "codeclone.pipeline", + "filepath": "codeclone/pipeline.py", + "all_declared": [], + "symbols": [ + { + "local_name": "AnalysisResult", + "kind": "class", + "start_line": 190, + "end_line": 206, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "BootstrapResult", + "kind": "class", + "start_line": 110, + "end_line": 115, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "DEFAULT_BATCH_SIZE", + "kind": "constant", + "start_line": 91, + "end_line": 91, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "DEFAULT_RUNTIME_PROCESSES", + "kind": "constant", + "start_line": 94, + "end_line": 94, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "DiscoveryResult", + "kind": "class", + "start_line": 119, + "end_line": 143, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "FileProcessResult", + "kind": "class", + "start_line": 147, + "end_line": 161, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "GatingResult", + "kind": "class", + "start_line": 210, + "end_line": 212, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "MAX_FILE_SIZE", + "kind": "constant", + "start_line": 90, + "end_line": 90, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "MetricGateConfig", + "kind": "class", + "start_line": 226, + "end_line": 238, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "OutputPaths", + "kind": "class", + "start_line": 101, + "end_line": 106, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "PARALLEL_MIN_FILES_FLOOR", + "kind": "constant", + "start_line": 93, + "end_line": 93, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "PARALLEL_MIN_FILES_PER_WORKER", + "kind": "constant", + "start_line": 92, + "end_line": 92, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ProcessingResult", + "kind": "class", + "start_line": 165, + "end_line": 186, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ReportArtifacts", + "kind": "class", + "start_line": 216, + "end_line": 222, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "analyze", + "kind": "function", + "start_line": 1957, + "end_line": 2078, + "params": [ + { + "name": "boot", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='BootstrapResult', ctx=Load())" + }, + { + "name": "discovery", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='DiscoveryResult', ctx=Load())" + }, + { + "name": "processing", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='ProcessingResult', ctx=Load())" + } + ], + "returns_hash": "Name(id='AnalysisResult', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "bootstrap", + "kind": "function", + "start_line": 443, + "end_line": 456, + "params": [ + { + "name": "args", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='Namespace', ctx=Load())" + }, + { + "name": "cache_path", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + }, + { + "name": "output_paths", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='OutputPaths', ctx=Load())" + }, + { + "name": "root", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Name(id='BootstrapResult', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "build_metrics_report_payload", + "kind": "function", + "start_line": 1757, + "end_line": 1954, + "params": [ + { + "name": "class_metrics", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='ClassMetrics', ctx=Load()), ctx=Load())" + }, + { + "name": "module_deps", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='ModuleDep', ctx=Load()), ctx=Load())" + }, + { + "name": "project_metrics", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='ProjectMetrics', ctx=Load())" + }, + { + "name": "scan_root", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "source_stats_by_file", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='int', ctx=Load()), Name(id='int', ctx=Load()), Name(id='int', ctx=Load()), Name(id='int', ctx=Load())], ctx=Load()), ctx=Load()), ctx=Load())" + }, + { + "name": "suppressed_dead_code", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='DeadItem', ctx=Load()), ctx=Load())" + }, + { + "name": "units", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='GroupItemLike', ctx=Load()), ctx=Load())" + } + ], + "returns_hash": "Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "compute_project_metrics", + "kind": "function", + "start_line": 1342, + "end_line": 1500, + "params": [ + { + "name": "api_modules", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='ModuleApiSurface', ctx=Load()), ctx=Load())" + }, + { + "name": "block_clone_groups", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "class_metrics", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='ClassMetrics', ctx=Load()), ctx=Load())" + }, + { + "name": "dead_candidates", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='DeadCandidate', ctx=Load()), ctx=Load())" + }, + { + "name": "docstring_modules", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='ModuleDocstringCoverage', ctx=Load()), ctx=Load())" + }, + { + "name": "files_analyzed_or_cached", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "files_found", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "function_clone_groups", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "module_deps", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='ModuleDep', ctx=Load()), ctx=Load())" + }, + { + "name": "referenced_names", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='frozenset', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "referenced_qualnames", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='frozenset', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "skip_dead_code", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='bool', ctx=Load())" + }, + { + "name": "skip_dependencies", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='bool', ctx=Load())" + }, + { + "name": "typing_modules", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='ModuleTypingCoverage', ctx=Load()), ctx=Load())" + }, + { + "name": "units", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='GroupItemLike', ctx=Load()), ctx=Load())" + } + ], + "returns_hash": "Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='ProjectMetrics', ctx=Load()), Name(id='DepGraph', ctx=Load()), Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='DeadItem', ctx=Load()), Constant(value=Ellipsis)], ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "compute_suggestions", + "kind": "function", + "start_line": 1503, + "end_line": 1525, + "params": [ + { + "name": "block_group_facts", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='str', ctx=Load())], ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "block_groups", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='GroupItemLike', ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" + }, + { + "name": "class_metrics", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='ClassMetrics', ctx=Load()), ctx=Load())" + }, + { + "name": "func_groups", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='GroupItemLike', ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" + }, + { + "name": "project_metrics", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='ProjectMetrics', ctx=Load())" + }, + { + "name": "scan_root", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "segment_groups", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='GroupItemLike', ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" + }, + { + "name": "structural_findings", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='StructuralFindingGroup', ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "units", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='GroupItemLike', ctx=Load()), ctx=Load())" + } + ], + "returns_hash": "Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='Suggestion', ctx=Load()), Constant(value=Ellipsis)], ctx=Load()), ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "discover", + "kind": "function", + "start_line": 785, + "end_line": 926, + "params": [ + { + "name": "boot", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='BootstrapResult', ctx=Load())" + }, + { + "name": "cache", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='Cache', ctx=Load())" + } + ], + "returns_hash": "Name(id='DiscoveryResult', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "gate", + "kind": "function", + "start_line": 2448, + "end_line": 2500, + "params": [ + { + "name": "analysis", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='AnalysisResult', ctx=Load())" + }, + { + "name": "boot", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='BootstrapResult', ctx=Load())" + }, + { + "name": "metrics_diff", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "BinOp(left=Name(id='MetricsDiff', ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "new_block", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Collection', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "new_func", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Collection', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + } + ], + "returns_hash": "Name(id='GatingResult', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "metric_gate_reasons", + "kind": "function", + "start_line": 2251, + "end_line": 2274, + "params": [ + { + "name": "config", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='MetricGateConfig', ctx=Load())" + }, + { + "name": "metrics_diff", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "BinOp(left=Name(id='MetricsDiff', ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "project_metrics", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='ProjectMetrics', ctx=Load())" + } + ], + "returns_hash": "Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Constant(value=Ellipsis)], ctx=Load()), ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "process", + "kind": "function", + "start_line": 1032, + "end_line": 1329, + "params": [ + { + "name": "batch_size", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "boot", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='BootstrapResult', ctx=Load())" + }, + { + "name": "cache", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='Cache', ctx=Load())" + }, + { + "name": "discovery", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='DiscoveryResult', ctx=Load())" + }, + { + "name": "on_advance", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[List(ctx=Load()), Constant(value=None)], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "on_parallel_fallback", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[List(elts=[Name(id='Exception', ctx=Load())], ctx=Load()), Constant(value=None)], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "on_worker_error", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[List(elts=[Name(id='str', ctx=Load())], ctx=Load()), Constant(value=None)], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" + } + ], + "returns_hash": "Name(id='ProcessingResult', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "process_file", + "kind": "function", + "start_line": 929, + "end_line": 1029, + "params": [ + { + "name": "api_include_private_modules", + "kind": "pos_or_kw", + "has_default": true, + "annotation_hash": "Name(id='bool', ctx=Load())" + }, + { + "name": "block_min_loc", + "kind": "pos_or_kw", + "has_default": true, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "block_min_stmt", + "kind": "pos_or_kw", + "has_default": true, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "cfg", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='NormalizationConfig', ctx=Load())" + }, + { + "name": "collect_api_surface", + "kind": "pos_or_kw", + "has_default": true, + "annotation_hash": "Name(id='bool', ctx=Load())" + }, + { + "name": "collect_docstring_coverage", + "kind": "pos_or_kw", + "has_default": true, + "annotation_hash": "Name(id='bool', ctx=Load())" + }, + { + "name": "collect_structural_findings", + "kind": "pos_or_kw", + "has_default": true, + "annotation_hash": "Name(id='bool', ctx=Load())" + }, + { + "name": "collect_typing_coverage", + "kind": "pos_or_kw", + "has_default": true, + "annotation_hash": "Name(id='bool', ctx=Load())" + }, + { + "name": "filepath", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "min_loc", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "min_stmt", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "root", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "segment_min_loc", + "kind": "pos_or_kw", + "has_default": true, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "segment_min_stmt", + "kind": "pos_or_kw", + "has_default": true, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "Name(id='FileProcessResult', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "report", + "kind": "function", + "start_line": 2093, + "end_line": 2248, + "params": [ + { + "name": "analysis", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='AnalysisResult', ctx=Load())" + }, + { + "name": "boot", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='BootstrapResult', ctx=Load())" + }, + { + "name": "discovery", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='DiscoveryResult', ctx=Load())" + }, + { + "name": "html_builder", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[Constant(value=Ellipsis), Name(id='str', ctx=Load())], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "include_report_document", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "Name(id='bool', ctx=Load())" + }, + { + "name": "metrics_diff", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Name(id='object', ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "new_block", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Collection', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "new_func", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Collection', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "processing", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='ProcessingResult', ctx=Load())" + }, + { + "name": "report_meta", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())" + } + ], + "returns_hash": "Name(id='ReportArtifacts', ctx=Load())", + "exported_via": "name" + } + ] + }, + { + "module": "codeclone.ui_messages", + "filepath": "codeclone/ui_messages.py", + "all_declared": [], + "symbols": [ + { + "local_name": "ACTION_UPDATE_BASELINE", + "kind": "constant", + "start_line": 281, + "end_line": 281, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "BANNER_SUBTITLE", + "kind": "constant", + "start_line": 25, + "end_line": 25, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "CHANGED_SCOPE_TITLE", + "kind": "constant", + "start_line": 183, + "end_line": 183, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "CLI_LAYOUT_MAX_WIDTH", + "kind": "constant", + "start_line": 185, + "end_line": 185, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ERR_BASELINE_GATING_REQUIRES_TRUSTED", + "kind": "constant", + "start_line": 293, + "end_line": 295, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ERR_BASELINE_WRITE_FAILED", + "kind": "constant", + "start_line": 251, + "end_line": 253, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ERR_INVALID_BASELINE", + "kind": "constant", + "start_line": 276, + "end_line": 280, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ERR_INVALID_BASELINE_PATH", + "kind": "constant", + "start_line": 250, + "end_line": 250, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ERR_INVALID_OUTPUT_EXT", + "kind": "constant", + "start_line": 240, + "end_line": 243, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ERR_INVALID_OUTPUT_PATH", + "kind": "constant", + "start_line": 244, + "end_line": 246, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ERR_INVALID_ROOT_PATH", + "kind": "constant", + "start_line": 248, + "end_line": 248, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ERR_OPEN_HTML_REPORT_REQUIRES_HTML", + "kind": "constant", + "start_line": 257, + "end_line": 259, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ERR_REPORT_WRITE_FAILED", + "kind": "constant", + "start_line": 254, + "end_line": 256, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ERR_ROOT_NOT_FOUND", + "kind": "constant", + "start_line": 247, + "end_line": 247, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ERR_SCAN_FAILED", + "kind": "constant", + "start_line": 249, + "end_line": 249, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ERR_TIMESTAMPED_REPORT_PATHS_REQUIRES_REPORT", + "kind": "constant", + "start_line": 260, + "end_line": 263, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "ERR_UNREADABLE_SOURCE_IN_GATING", + "kind": "constant", + "start_line": 264, + "end_line": 267, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "FAIL_METRICS_TITLE", + "kind": "constant", + "start_line": 307, + "end_line": 307, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "FAIL_NEW_ACCEPT_COMMAND", + "kind": "constant", + "start_line": 304, + "end_line": 304, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "FAIL_NEW_ACCEPT_TITLE", + "kind": "constant", + "start_line": 303, + "end_line": 303, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "FAIL_NEW_BLOCK", + "kind": "constant", + "start_line": 301, + "end_line": 301, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "FAIL_NEW_DETAIL_BLOCK", + "kind": "constant", + "start_line": 306, + "end_line": 306, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "FAIL_NEW_DETAIL_FUNCTION", + "kind": "constant", + "start_line": 305, + "end_line": 305, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "FAIL_NEW_FUNCTION", + "kind": "constant", + "start_line": 300, + "end_line": 300, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "FAIL_NEW_REPORT_TITLE", + "kind": "constant", + "start_line": 302, + "end_line": 302, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "FAIL_NEW_SUMMARY_TITLE", + "kind": "constant", + "start_line": 299, + "end_line": 299, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "FAIL_NEW_TITLE", + "kind": "constant", + "start_line": 298, + "end_line": 298, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_API_SURFACE", + "kind": "constant", + "start_line": 103, + "end_line": 106, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_BASELINE", + "kind": "constant", + "start_line": 58, + "end_line": 61, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_CACHE_DIR_LEGACY", + "kind": "constant", + "start_line": 53, + "end_line": 55, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_CACHE_PATH", + "kind": "constant", + "start_line": 49, + "end_line": 52, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_CHANGED_ONLY", + "kind": "constant", + "start_line": 37, + "end_line": 40, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_CI", + "kind": "constant", + "start_line": 127, + "end_line": 132, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_COLOR", + "kind": "constant", + "start_line": 173, + "end_line": 173, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_DEBUG", + "kind": "constant", + "start_line": 176, + "end_line": 179, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_DIFF_AGAINST", + "kind": "constant", + "start_line": 41, + "end_line": 44, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_DOCSTRING_COVERAGE", + "kind": "constant", + "start_line": 99, + "end_line": 102, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_FAIL_COHESION", + "kind": "constant", + "start_line": 82, + "end_line": 85, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_FAIL_COMPLEXITY", + "kind": "constant", + "start_line": 73, + "end_line": 77, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_FAIL_COUPLING", + "kind": "constant", + "start_line": 78, + "end_line": 81, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_FAIL_CYCLES", + "kind": "constant", + "start_line": 86, + "end_line": 86, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_FAIL_DEAD_CODE", + "kind": "constant", + "start_line": 87, + "end_line": 87, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_FAIL_HEALTH", + "kind": "constant", + "start_line": 88, + "end_line": 91, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_FAIL_ON_API_BREAK", + "kind": "constant", + "start_line": 115, + "end_line": 118, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_FAIL_ON_DOCSTRING_REGRESSION", + "kind": "constant", + "start_line": 111, + "end_line": 114, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_FAIL_ON_NEW", + "kind": "constant", + "start_line": 65, + "end_line": 67, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_FAIL_ON_NEW_METRICS", + "kind": "constant", + "start_line": 92, + "end_line": 95, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_FAIL_ON_TYPING_REGRESSION", + "kind": "constant", + "start_line": 107, + "end_line": 110, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_FAIL_THRESHOLD", + "kind": "constant", + "start_line": 68, + "end_line": 72, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_HTML", + "kind": "constant", + "start_line": 143, + "end_line": 146, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_JSON", + "kind": "constant", + "start_line": 147, + "end_line": 150, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_MAX_BASELINE_SIZE_MB", + "kind": "constant", + "start_line": 56, + "end_line": 56, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_MAX_CACHE_SIZE_MB", + "kind": "constant", + "start_line": 57, + "end_line": 57, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_MD", + "kind": "constant", + "start_line": 151, + "end_line": 154, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_METRICS_BASELINE", + "kind": "constant", + "start_line": 136, + "end_line": 139, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_MIN_DOCSTRING_COVERAGE", + "kind": "constant", + "start_line": 123, + "end_line": 126, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_MIN_LOC", + "kind": "constant", + "start_line": 34, + "end_line": 34, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_MIN_STMT", + "kind": "constant", + "start_line": 35, + "end_line": 35, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_MIN_TYPING_COVERAGE", + "kind": "constant", + "start_line": 119, + "end_line": 122, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_NO_COLOR", + "kind": "constant", + "start_line": 172, + "end_line": 172, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_NO_PROGRESS", + "kind": "constant", + "start_line": 170, + "end_line": 170, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_OPEN_HTML_REPORT", + "kind": "constant", + "start_line": 163, + "end_line": 165, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_PATHS_FROM_GIT_DIFF", + "kind": "constant", + "start_line": 45, + "end_line": 48, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_PROCESSES", + "kind": "constant", + "start_line": 36, + "end_line": 36, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_PROGRESS", + "kind": "constant", + "start_line": 171, + "end_line": 171, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_QUIET", + "kind": "constant", + "start_line": 174, + "end_line": 174, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_ROOT", + "kind": "constant", + "start_line": 33, + "end_line": 33, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_SARIF", + "kind": "constant", + "start_line": 155, + "end_line": 158, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_SKIP_DEAD_CODE", + "kind": "constant", + "start_line": 141, + "end_line": 141, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_SKIP_DEPENDENCIES", + "kind": "constant", + "start_line": 142, + "end_line": 142, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_SKIP_METRICS", + "kind": "constant", + "start_line": 140, + "end_line": 140, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_TEXT", + "kind": "constant", + "start_line": 159, + "end_line": 162, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_TIMESTAMPED_REPORT_PATHS", + "kind": "constant", + "start_line": 166, + "end_line": 169, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_TYPING_COVERAGE", + "kind": "constant", + "start_line": 96, + "end_line": 98, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_UPDATE_BASELINE", + "kind": "constant", + "start_line": 62, + "end_line": 64, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_UPDATE_METRICS_BASELINE", + "kind": "constant", + "start_line": 133, + "end_line": 135, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_VERBOSE", + "kind": "constant", + "start_line": 175, + "end_line": 175, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "HELP_VERSION", + "kind": "constant", + "start_line": 32, + "end_line": 32, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "INFO_PROCESSING_CHANGED", + "kind": "constant", + "start_line": 226, + "end_line": 226, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "MARKER_CONTRACT_ERROR", + "kind": "constant", + "start_line": 27, + "end_line": 27, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "MARKER_INTERNAL_ERROR", + "kind": "constant", + "start_line": 28, + "end_line": 28, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "METRICS_TITLE", + "kind": "constant", + "start_line": 182, + "end_line": 182, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "REPORT_BLOCK_GROUP_DISPLAY_NAME_ASSERT_PATTERN", + "kind": "constant", + "start_line": 30, + "end_line": 30, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "STATUS_DISCOVERING", + "kind": "constant", + "start_line": 223, + "end_line": 223, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "STATUS_GROUPING", + "kind": "constant", + "start_line": 224, + "end_line": 224, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SUCCESS_BASELINE_UPDATED", + "kind": "constant", + "start_line": 296, + "end_line": 296, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SUMMARY_COMPACT", + "kind": "constant", + "start_line": 201, + "end_line": 204, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SUMMARY_COMPACT_CHANGED_SCOPE", + "kind": "constant", + "start_line": 214, + "end_line": 216, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SUMMARY_COMPACT_CLONES", + "kind": "constant", + "start_line": 205, + "end_line": 208, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SUMMARY_COMPACT_METRICS", + "kind": "constant", + "start_line": 209, + "end_line": 213, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SUMMARY_LABEL_BLOCK", + "kind": "constant", + "start_line": 196, + "end_line": 196, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SUMMARY_LABEL_CACHE_HITS", + "kind": "constant", + "start_line": 189, + "end_line": 189, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SUMMARY_LABEL_CLASSES_ANALYZED", + "kind": "constant", + "start_line": 194, + "end_line": 194, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SUMMARY_LABEL_FILES_ANALYZED", + "kind": "constant", + "start_line": 188, + "end_line": 188, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SUMMARY_LABEL_FILES_FOUND", + "kind": "constant", + "start_line": 187, + "end_line": 187, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SUMMARY_LABEL_FILES_SKIPPED", + "kind": "constant", + "start_line": 190, + "end_line": 190, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SUMMARY_LABEL_FUNCTION", + "kind": "constant", + "start_line": 195, + "end_line": 195, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SUMMARY_LABEL_FUNCTIONS_ANALYZED", + "kind": "constant", + "start_line": 192, + "end_line": 192, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SUMMARY_LABEL_LINES_ANALYZED", + "kind": "constant", + "start_line": 191, + "end_line": 191, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SUMMARY_LABEL_METHODS_ANALYZED", + "kind": "constant", + "start_line": 193, + "end_line": 193, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SUMMARY_LABEL_NEW_BASELINE", + "kind": "constant", + "start_line": 199, + "end_line": 199, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SUMMARY_LABEL_SEGMENT", + "kind": "constant", + "start_line": 197, + "end_line": 197, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SUMMARY_LABEL_SUPPRESSED", + "kind": "constant", + "start_line": 198, + "end_line": 198, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "SUMMARY_TITLE", + "kind": "constant", + "start_line": 181, + "end_line": 181, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "WARN_BASELINE_IGNORED", + "kind": "constant", + "start_line": 288, + "end_line": 292, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "WARN_BASELINE_MISSING", + "kind": "constant", + "start_line": 282, + "end_line": 287, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "WARN_BATCH_ITEM_FAILED", + "kind": "constant", + "start_line": 229, + "end_line": 229, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "WARN_CACHE_SAVE_FAILED", + "kind": "constant", + "start_line": 235, + "end_line": 235, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "WARN_FAILED_FILES_HEADER", + "kind": "constant", + "start_line": 234, + "end_line": 234, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "WARN_HTML_REPORT_OPEN_FAILED", + "kind": "constant", + "start_line": 236, + "end_line": 238, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "WARN_LEGACY_CACHE", + "kind": "constant", + "start_line": 269, + "end_line": 274, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "WARN_NEW_CLONES_WITHOUT_FAIL", + "kind": "constant", + "start_line": 309, + "end_line": 312, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "WARN_PARALLEL_FALLBACK", + "kind": "constant", + "start_line": 230, + "end_line": 233, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "WARN_SUMMARY_ACCOUNTING_MISMATCH", + "kind": "constant", + "start_line": 218, + "end_line": 221, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "WARN_WORKER_FAILED", + "kind": "constant", + "start_line": 228, + "end_line": 228, + "params": [], + "returns_hash": "", + "exported_via": "name" + }, + { + "local_name": "banner_title", + "kind": "function", + "start_line": 319, + "end_line": 323, + "params": [ + { + "name": "version", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_baseline_write_failed", + "kind": "function", + "start_line": 342, + "end_line": 343, + "params": [ + { + "name": "error", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='object', ctx=Load())" + }, + { + "name": "path", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_batch_item_failed", + "kind": "function", + "start_line": 366, + "end_line": 367, + "params": [ + { + "name": "error", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='object', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_cache_save_failed", + "kind": "function", + "start_line": 378, + "end_line": 379, + "params": [ + { + "name": "error", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='object', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_changed_scope_compact", + "kind": "function", + "start_line": 645, + "end_line": 657, + "params": [ + { + "name": "findings", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "known", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "new", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "paths", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_changed_scope_findings", + "kind": "function", + "start_line": 635, + "end_line": 642, + "params": [ + { + "name": "known", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "new", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "total", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_changed_scope_paths", + "kind": "function", + "start_line": 631, + "end_line": 632, + "params": [ + { + "name": "count", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_contract_error", + "kind": "function", + "start_line": 664, + "end_line": 665, + "params": [ + { + "name": "message", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_failed_files_header", + "kind": "function", + "start_line": 374, + "end_line": 375, + "params": [ + { + "name": "count", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_html_report_open_failed", + "kind": "function", + "start_line": 350, + "end_line": 351, + "params": [ + { + "name": "error", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='object', ctx=Load())" + }, + { + "name": "path", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_internal_error", + "kind": "function", + "start_line": 668, + "end_line": 710, + "params": [ + { + "name": "debug", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "Name(id='bool', ctx=Load())" + }, + { + "name": "issues_url", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "error", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='BaseException', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_invalid_baseline", + "kind": "function", + "start_line": 386, + "end_line": 387, + "params": [ + { + "name": "error", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='object', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_invalid_baseline_path", + "kind": "function", + "start_line": 338, + "end_line": 339, + "params": [ + { + "name": "error", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='object', ctx=Load())" + }, + { + "name": "path", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_invalid_output_extension", + "kind": "function", + "start_line": 326, + "end_line": 331, + "params": [ + { + "name": "expected_suffix", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "label", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "path", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_invalid_output_path", + "kind": "function", + "start_line": 334, + "end_line": 335, + "params": [ + { + "name": "error", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='object', ctx=Load())" + }, + { + "name": "label", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "path", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_legacy_cache_warning", + "kind": "function", + "start_line": 382, + "end_line": 383, + "params": [ + { + "name": "legacy_path", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + }, + { + "name": "new_path", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_metrics_adoption", + "kind": "function", + "start_line": 574, + "end_line": 587, + "params": [ + { + "name": "any_annotation_count", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "docstring_permille", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "param_permille", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "return_permille", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_metrics_api_surface", + "kind": "function", + "start_line": 590, + "end_line": 610, + "params": [ + { + "name": "added", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "breaking", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "modules", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "public_symbols", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_metrics_cc", + "kind": "function", + "start_line": 529, + "end_line": 535, + "params": [ + { + "name": "avg", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='float', ctx=Load())" + }, + { + "name": "high_risk", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "max_val", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_metrics_cohesion", + "kind": "function", + "start_line": 542, + "end_line": 543, + "params": [ + { + "name": "avg", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='float', ctx=Load())" + }, + { + "name": "max_val", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_metrics_coupling", + "kind": "function", + "start_line": 538, + "end_line": 539, + "params": [ + { + "name": "avg", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='float', ctx=Load())" + }, + { + "name": "max_val", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_metrics_cycles", + "kind": "function", + "start_line": 546, + "end_line": 551, + "params": [ + { + "name": "count", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_metrics_dead_code", + "kind": "function", + "start_line": 554, + "end_line": 567, + "params": [ + { + "name": "suppressed", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "count", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_metrics_health", + "kind": "function", + "start_line": 524, + "end_line": 526, + "params": [ + { + "name": "grade", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "total", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_metrics_overloaded_modules", + "kind": "function", + "start_line": 613, + "end_line": 628, + "params": [ + { + "name": "candidates", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "population_status", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "top_score", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='float', ctx=Load())" + }, + { + "name": "total", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_parallel_fallback", + "kind": "function", + "start_line": 370, + "end_line": 371, + "params": [ + { + "name": "error", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='object', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_path", + "kind": "function", + "start_line": 390, + "end_line": 391, + "params": [ + { + "name": "path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + }, + { + "name": "template", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_pipeline_done", + "kind": "function", + "start_line": 660, + "end_line": 661, + "params": [ + { + "name": "elapsed", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='float', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_processing_changed", + "kind": "function", + "start_line": 358, + "end_line": 359, + "params": [ + { + "name": "count", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_report_write_failed", + "kind": "function", + "start_line": 346, + "end_line": 347, + "params": [ + { + "name": "error", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='object', ctx=Load())" + }, + { + "name": "label", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "path", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_summary_clones", + "kind": "function", + "start_line": 507, + "end_line": 521, + "params": [ + { + "name": "block", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "func", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "new", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "segment", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "suppressed", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_summary_compact", + "kind": "function", + "start_line": 394, + "end_line": 399, + "params": [ + { + "name": "analyzed", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "cache_hits", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "found", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "skipped", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_summary_compact_clones", + "kind": "function", + "start_line": 402, + "end_line": 416, + "params": [ + { + "name": "block", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "function", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "new", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "segment", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "suppressed", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_summary_compact_metrics", + "kind": "function", + "start_line": 419, + "end_line": 445, + "params": [ + { + "name": "cbo_avg", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='float', ctx=Load())" + }, + { + "name": "cbo_max", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "cc_avg", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='float', ctx=Load())" + }, + { + "name": "cc_max", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "cycles", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "dead", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "grade", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "health", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "lcom_avg", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='float', ctx=Load())" + }, + { + "name": "lcom_max", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "overloaded_modules", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_summary_files", + "kind": "function", + "start_line": 481, + "end_line": 489, + "params": [ + { + "name": "analyzed", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "cached", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "found", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "skipped", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_summary_parsed", + "kind": "function", + "start_line": 492, + "end_line": 504, + "params": [ + { + "name": "classes", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "functions", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "lines", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "methods", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Constant(value=None))", + "exported_via": "name" + }, + { + "local_name": "fmt_unreadable_source_in_gating", + "kind": "function", + "start_line": 354, + "end_line": 355, + "params": [ + { + "name": "count", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "fmt_worker_failed", + "kind": "function", + "start_line": 362, + "end_line": 363, + "params": [ + { + "name": "error", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='object', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "version_output", + "kind": "function", + "start_line": 315, + "end_line": 316, + "params": [ + { + "name": "version", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + } + ] + }, + { + "module": "tests.test_adoption", + "filepath": "tests/test_adoption.py", + "all_declared": [], + "symbols": [ + { + "local_name": "test_adoption_helper_rows_and_any_helpers_cover_method_and_variants", + "kind": "function", + "start_line": 172, + "end_line": 197, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_build_module_visibility_supports_strict_dunder_all_for_private_modules", + "kind": "function", + "start_line": 36, + "end_line": 65, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_collect_module_adoption_counts_annotations_docstrings_and_any", + "kind": "function", + "start_line": 68, + "end_line": 119, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_visibility_helpers_cover_private_modules_and_declared_all_edges", + "kind": "function", + "start_line": 122, + "end_line": 169, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + } + ] + }, + { + "module": "tests.test_api_surface", + "filepath": "tests/test_api_surface.py", + "all_declared": [], + "symbols": [ + { + "local_name": "test_api_surface_helpers_cover_constant_symbols_and_break_variants", + "kind": "function", + "start_line": 267, + "end_line": 405, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_collect_module_api_surface_skips_private_or_empty_modules", + "kind": "function", + "start_line": 229, + "end_line": 264, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_collect_module_api_surface_skips_self_and_collects_public_symbols", + "kind": "function", + "start_line": 45, + "end_line": 96, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_compare_api_surfaces_reports_added_removed_and_signature_breaks", + "kind": "function", + "start_line": 99, + "end_line": 209, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + } + ] + }, + { + "module": "tests.test_baseline", + "filepath": "tests/test_baseline.py", + "all_declared": [], + "symbols": [ + { + "local_name": "test_baseline_atomic_write_json_cleans_up_temp_file_on_replace_failure", + "kind": "function", + "start_line": 810, + "end_line": 826, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_diff", + "kind": "function", + "start_line": 67, + "end_line": 73, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_from_groups_defaults", + "kind": "function", + "start_line": 715, + "end_line": 725, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_hash_canonical_determinism", + "kind": "function", + "start_line": 578, + "end_line": 591, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_id_format_validation", + "kind": "function", + "start_line": 365, + "end_line": 378, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_id_lists_must_be_sorted_and_unique", + "kind": "function", + "start_line": 343, + "end_line": 362, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_integrity_fails_on_block_addition_without_rehash", + "kind": "function", + "start_line": 492, + "end_line": 503, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_integrity_fails_on_clone_removal", + "kind": "function", + "start_line": 480, + "end_line": 489, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_load_extra_top_level_key", + "kind": "function", + "start_line": 248, + "end_line": 259, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_load_json_read_error", + "kind": "function", + "start_line": 829, + "end_line": 845, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_load_legacy_codeclone_version_alias", + "kind": "function", + "start_line": 879, + "end_line": 892, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_load_legacy_payload", + "kind": "function", + "start_line": 217, + "end_line": 226, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_load_meta_and_clones_must_be_objects", + "kind": "function", + "start_line": 262, + "end_line": 270, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_load_missing", + "kind": "function", + "start_line": 155, + "end_line": 159, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_load_missing_required_clone_fields", + "kind": "function", + "start_line": 285, + "end_line": 294, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_load_missing_required_meta_fields", + "kind": "function", + "start_line": 273, + "end_line": 282, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_load_missing_top_level_key", + "kind": "function", + "start_line": 239, + "end_line": 245, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_load_rejects_invalid_json_shapes", + "kind": "function", + "start_line": 203, + "end_line": 214, + "params": [ + { + "name": "error_match", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "expected_status", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "raw_payload", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_load_rejects_metrics_section_for_schema_v1", + "kind": "function", + "start_line": 971, + "end_line": 982, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_load_rejects_non_canonical_block_lists", + "kind": "function", + "start_line": 513, + "end_line": 525, + "params": [ + { + "name": "blocks", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='list', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_load_rejects_non_object_preloaded_payload", + "kind": "function", + "start_line": 229, + "end_line": 236, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_load_stat_error", + "kind": "function", + "start_line": 174, + "end_line": 192, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_load_too_large", + "kind": "function", + "start_line": 162, + "end_line": 171, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_load_unexpected_clone_fields", + "kind": "function", + "start_line": 297, + "end_line": 308, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_load_whitespace_and_key_order_do_not_break_integrity", + "kind": "function", + "start_line": 661, + "end_line": 694, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_optional_str_paths", + "kind": "function", + "start_line": 848, + "end_line": 860, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_parse_semver_three_parts", + "kind": "function", + "start_line": 947, + "end_line": 953, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_payload_fields_contract_invariant", + "kind": "function", + "start_line": 539, + "end_line": 575, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_payload_sha256_independent_of_created_at_and_generator_version", + "kind": "function", + "start_line": 594, + "end_line": 609, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_payload_sha256_independent_of_schema_version", + "kind": "function", + "start_line": 612, + "end_line": 619, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_require_sorted_unique_ids_non_string", + "kind": "function", + "start_line": 956, + "end_line": 968, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_require_utc_iso8601_z_rejects_invalid_calendar_date", + "kind": "function", + "start_line": 863, + "end_line": 876, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_roundtrip_v1", + "kind": "function", + "start_line": 91, + "end_line": 118, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_safe_stat_size_oserror", + "kind": "function", + "start_line": 792, + "end_line": 807, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_save_atomic", + "kind": "function", + "start_line": 145, + "end_line": 152, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_save_defensive_non_mapping_meta", + "kind": "function", + "start_line": 1120, + "end_line": 1148, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_save_ignores_non_string_non_mapping_generator", + "kind": "function", + "start_line": 1246, + "end_line": 1269, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_save_preserves_embedded_api_surface_and_hash", + "kind": "function", + "start_line": 1006, + "end_line": 1026, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_save_preserves_embedded_metrics_and_hash", + "kind": "function", + "start_line": 985, + "end_line": 1003, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_save_preserves_embedded_metrics_without_hash", + "kind": "function", + "start_line": 1029, + "end_line": 1047, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_save_skips_non_string_meta_updates", + "kind": "function", + "start_line": 1184, + "end_line": 1222, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_save_sorts_clone_lists_deterministically", + "kind": "function", + "start_line": 697, + "end_line": 712, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_save_syncs_generator_when_meta_uses_string", + "kind": "function", + "start_line": 1151, + "end_line": 1181, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_save_updates_runtime_meta_fields", + "kind": "function", + "start_line": 121, + "end_line": 142, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_schema_version_mutation_preserves_integrity_and_hash_on_save", + "kind": "function", + "start_line": 622, + "end_line": 646, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_type_matrix", + "kind": "function", + "start_line": 324, + "end_line": 340, + "params": [ + { + "name": "container", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "error_match", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "field", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + }, + { + "name": "value", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='object', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_verify_accepts_previous_minor_in_current_major", + "kind": "function", + "start_line": 417, + "end_line": 425, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_verify_compatibility_ignores_generator_version", + "kind": "function", + "start_line": 528, + "end_line": 536, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_verify_compatibility_missing_fields", + "kind": "function", + "start_line": 736, + "end_line": 746, + "params": [ + { + "name": "attr", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "match_text", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_verify_fingerprint_mismatch", + "kind": "function", + "start_line": 428, + "end_line": 437, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_verify_generator_mismatch", + "kind": "function", + "start_line": 381, + "end_line": 393, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_verify_integrity_ignores_created_at_and_generator_version", + "kind": "function", + "start_line": 649, + "end_line": 658, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_verify_integrity_mismatch", + "kind": "function", + "start_line": 465, + "end_line": 477, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_verify_integrity_missing", + "kind": "function", + "start_line": 450, + "end_line": 462, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_verify_integrity_missing_context_fields", + "kind": "function", + "start_line": 779, + "end_line": 789, + "params": [ + { + "name": "attr", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "match_text", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_verify_integrity_payload_non_hex", + "kind": "function", + "start_line": 760, + "end_line": 768, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_verify_integrity_payload_not_string", + "kind": "function", + "start_line": 749, + "end_line": 757, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_verify_python_tag_mismatch", + "kind": "function", + "start_line": 440, + "end_line": 447, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_baseline_verify_schema_incompatibilities", + "kind": "function", + "start_line": 405, + "end_line": 414, + "params": [ + { + "name": "error_match", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "schema_version", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_coerce_baseline_status", + "kind": "function", + "start_line": 85, + "end_line": 88, + "params": [ + { + "name": "expected", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='BaselineStatus', ctx=Load())" + }, + { + "name": "raw_status", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "BinOp(left=BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Name(id='BaselineStatus', ctx=Load())), op=BitOr(), right=Constant(value=None))" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_parse_generator_meta_object_top_level_fallback", + "kind": "function", + "start_line": 922, + "end_line": 932, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_parse_generator_meta_rejects_extra_generator_keys", + "kind": "function", + "start_line": 935, + "end_line": 944, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_parse_generator_meta_string_legacy_alias", + "kind": "function", + "start_line": 895, + "end_line": 905, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_parse_generator_meta_string_prefers_generator_version", + "kind": "function", + "start_line": 908, + "end_line": 919, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_preserve_embedded_metrics_variants", + "kind": "function", + "start_line": 1050, + "end_line": 1117, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + } + ] + }, + { + "module": "tests.test_cache", + "filepath": "tests/test_cache.py", + "all_declared": [], + "symbols": [ + { + "local_name": "test_as_str_dict_rejects_non_string_keys", + "kind": "function", + "start_line": 1184, + "end_line": 1185, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_entry_block_item_not_dict", + "kind": "function", + "start_line": 623, + "end_line": 634, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_entry_container_shape_rejects_invalid_source_stats", + "kind": "function", + "start_line": 1689, + "end_line": 1706, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_entry_invalid_block_field_type", + "kind": "function", + "start_line": 637, + "end_line": 657, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_entry_invalid_segment_field_type", + "kind": "function", + "start_line": 674, + "end_line": 695, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_entry_invalid_stat_types", + "kind": "function", + "start_line": 542, + "end_line": 553, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_entry_invalid_unit_field_type", + "kind": "function", + "start_line": 598, + "end_line": 620, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_entry_invalid_units_container_type", + "kind": "function", + "start_line": 570, + "end_line": 581, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_entry_not_dict", + "kind": "function", + "start_line": 751, + "end_line": 755, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_entry_rejects_invalid_metrics_sections", + "kind": "function", + "start_line": 1250, + "end_line": 1267, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_entry_segment_item_not_dict", + "kind": "function", + "start_line": 660, + "end_line": 671, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_entry_stat_not_dict", + "kind": "function", + "start_line": 556, + "end_line": 567, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_entry_unit_item_not_dict", + "kind": "function", + "start_line": 584, + "end_line": 595, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_entry_valid_deep_schema", + "kind": "function", + "start_line": 698, + "end_line": 739, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_entry_validation", + "kind": "function", + "start_line": 535, + "end_line": 539, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_helper_type_guards_and_wire_api_decoders_cover_invalid_inputs", + "kind": "function", + "start_line": 265, + "end_line": 343, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_helpers_cover_invalid_analysis_profile_and_source_stats_shapes", + "kind": "function", + "start_line": 1361, + "end_line": 1382, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_legacy_secret_check_oserror_sets_warning", + "kind": "function", + "start_line": 898, + "end_line": 913, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_legacy_secret_warning_combined_with_other_warning", + "kind": "function", + "start_line": 882, + "end_line": 895, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_legacy_secret_warning_on_init", + "kind": "function", + "start_line": 852, + "end_line": 861, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_legacy_secret_warning_preserved_after_successful_load", + "kind": "function", + "start_line": 864, + "end_line": 879, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_load_analysis_profile_mismatch", + "kind": "function", + "start_line": 1008, + "end_line": 1021, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_load_corrupted_json", + "kind": "function", + "start_line": 766, + "end_line": 774, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_load_fingerprint_version_mismatch", + "kind": "function", + "start_line": 990, + "end_line": 1005, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_load_invalid_analysis_profile_payload", + "kind": "function", + "start_line": 1052, + "end_line": 1073, + "params": [ + { + "name": "bad_analysis_profile", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='object', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_load_invalid_files_type", + "kind": "function", + "start_line": 825, + "end_line": 836, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_load_invalid_top_level_type", + "kind": "function", + "start_line": 916, + "end_line": 923, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_load_invalid_wire_file_entry", + "kind": "function", + "start_line": 1076, + "end_line": 1086, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_load_missing_analysis_profile_in_payload", + "kind": "function", + "start_line": 1024, + "end_line": 1042, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_load_missing_file", + "kind": "function", + "start_line": 742, + "end_line": 748, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_load_missing_payload_or_sig", + "kind": "function", + "start_line": 937, + "end_line": 945, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_load_missing_v_field", + "kind": "function", + "start_line": 926, + "end_line": 934, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_load_normalizes_stale_structural_findings", + "kind": "function", + "start_line": 113, + "end_line": 188, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_load_python_tag_mismatch", + "kind": "function", + "start_line": 972, + "end_line": 987, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_load_rejects_missing_required_payload_fields", + "kind": "function", + "start_line": 956, + "end_line": 969, + "params": [ + { + "name": "payload_factory", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[List(elts=[Name(id='Cache', ctx=Load())], ctx=Load()), Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_load_unreadable_read_graceful_ignore", + "kind": "function", + "start_line": 795, + "end_line": 814, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_load_unreadable_stat_graceful_ignore", + "kind": "function", + "start_line": 777, + "end_line": 792, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_roundtrip", + "kind": "function", + "start_line": 71, + "end_line": 89, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_roundtrip_preserves_empty_structural_findings", + "kind": "function", + "start_line": 92, + "end_line": 110, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_save_error", + "kind": "function", + "start_line": 839, + "end_line": 849, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_save_skips_none_entry_from_lookup", + "kind": "function", + "start_line": 1089, + "end_line": 1112, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_signature_mismatch_warns", + "kind": "function", + "start_line": 463, + "end_line": 480, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_signature_validation_ignores_json_whitespace", + "kind": "function", + "start_line": 395, + "end_line": 407, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_too_large_warns", + "kind": "function", + "start_line": 521, + "end_line": 532, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_type_predicates_reject_non_dict_variants", + "kind": "function", + "start_line": 1709, + "end_line": 1817, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_v13_missing_optional_sections_default_empty", + "kind": "function", + "start_line": 377, + "end_line": 392, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_v13_uses_relpaths_when_root_set", + "kind": "function", + "start_line": 353, + "end_line": 374, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_v_field_version_mismatch_warns", + "kind": "function", + "start_line": 503, + "end_line": 518, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + }, + { + "name": "version", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cache_version_mismatch_warns", + "kind": "function", + "start_line": 483, + "end_line": 499, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_canonicalize_cache_entry_skips_invalid_dead_candidate_suppression_shape", + "kind": "function", + "start_line": 1385, + "end_line": 1425, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_decode_optional_wire_coupled_classes_rejects_non_string_qualname", + "kind": "function", + "start_line": 1428, + "end_line": 1435, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_decode_wire_block_rejects_missing_block_hash", + "kind": "function", + "start_line": 1824, + "end_line": 1831, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_decode_wire_dead_candidate_rejects_invalid_rows", + "kind": "function", + "start_line": 1844, + "end_line": 1845, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_decode_wire_file_and_name_section_helpers_cover_valid_and_invalid", + "kind": "function", + "start_line": 410, + "end_line": 460, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_decode_wire_file_entry_accepts_metrics_sections", + "kind": "function", + "start_line": 1318, + "end_line": 1338, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_decode_wire_file_entry_invalid_variants", + "kind": "function", + "start_line": 1203, + "end_line": 1204, + "params": [ + { + "name": "entry", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='object', ctx=Load())" + }, + { + "name": "filepath", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_decode_wire_file_entry_optional_source_stats", + "kind": "function", + "start_line": 1341, + "end_line": 1358, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_decode_wire_file_entry_rejects_metrics_related_invalid_sections", + "kind": "function", + "start_line": 1270, + "end_line": 1315, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_decode_wire_file_entry_skips_empty_coupled_classes_mapping", + "kind": "function", + "start_line": 1438, + "end_line": 1448, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_decode_wire_int_fields_rejects_non_int_values", + "kind": "function", + "start_line": 1820, + "end_line": 1821, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_decode_wire_item_rejects_invalid_risk_fields", + "kind": "function", + "start_line": 1219, + "end_line": 1233, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_decode_wire_item_type_failures", + "kind": "function", + "start_line": 1207, + "end_line": 1216, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_decode_wire_metrics_items_and_deps_roundtrip_shape", + "kind": "function", + "start_line": 1451, + "end_line": 1496, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_decode_wire_segment_rejects_missing_segment_signature", + "kind": "function", + "start_line": 1834, + "end_line": 1841, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_encode_wire_file_entry_compacts_dead_candidate_filepaths", + "kind": "function", + "start_line": 1537, + "end_line": 1560, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_encode_wire_file_entry_encodes_dead_candidate_suppressions", + "kind": "function", + "start_line": 1563, + "end_line": 1587, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_encode_wire_file_entry_includes_optional_metrics_sections", + "kind": "function", + "start_line": 1499, + "end_line": 1534, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_encode_wire_file_entry_skips_empty_or_invalid_coupled_classes", + "kind": "function", + "start_line": 1590, + "end_line": 1631, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_file_stat_signature", + "kind": "function", + "start_line": 758, + "end_line": 763, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_get_file_entry_keeps_loaded_cache_clean_on_canonical_hit", + "kind": "function", + "start_line": 210, + "end_line": 222, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_get_file_entry_missing_after_fallback_returns_none", + "kind": "function", + "start_line": 346, + "end_line": 350, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_get_file_entry_sorts_coupled_classes_in_runtime_payload", + "kind": "function", + "start_line": 1634, + "end_line": 1686, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_get_file_entry_uses_wire_key_fallback", + "kind": "function", + "start_line": 191, + "end_line": 207, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_resolve_root_oserror_returns_none", + "kind": "function", + "start_line": 1236, + "end_line": 1247, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_runtime_filepath_from_wire_resolve_oserror", + "kind": "function", + "start_line": 1166, + "end_line": 1181, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_store_canonical_file_entry_marks_dirty_only_when_entry_changes", + "kind": "function", + "start_line": 225, + "end_line": 262, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_wire_filepath_outside_root_falls_back_to_runtime_path", + "kind": "function", + "start_line": 1115, + "end_line": 1122, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_wire_filepath_resolve_oserror_falls_back_to_runtime_path", + "kind": "function", + "start_line": 1125, + "end_line": 1142, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_wire_filepath_resolve_relative_success_path", + "kind": "function", + "start_line": 1145, + "end_line": 1163, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + } + ] + }, + { + "module": "tests.test_cli_inprocess", + "filepath": "tests/test_cli_inprocess.py", + "all_declared": [], + "symbols": [ + { + "local_name": "test_cli_baseline_fingerprint_and_python_mismatch_status_prefers_fingerprint", + "kind": "function", + "start_line": 2581, + "end_line": 2601, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_baseline_fingerprint_mismatch_fails", + "kind": "function", + "start_line": 2474, + "end_line": 2498, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_baseline_missing_fails_in_ci", + "kind": "function", + "start_line": 2392, + "end_line": 2413, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_baseline_missing_fields_fails", + "kind": "function", + "start_line": 2501, + "end_line": 2531, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_baseline_missing_warning", + "kind": "function", + "start_line": 2371, + "end_line": 2389, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_baseline_python_version_mismatch_fails", + "kind": "function", + "start_line": 2604, + "end_line": 2624, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_baseline_python_version_mismatch_warns", + "kind": "function", + "start_line": 2452, + "end_line": 2471, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_baseline_schema_and_fingerprint_mismatch_status_prefers_schema", + "kind": "function", + "start_line": 2557, + "end_line": 2578, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_baseline_schema_version_mismatch_fails", + "kind": "function", + "start_line": 2534, + "end_line": 2554, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_batch_result_none_no_progress", + "kind": "function", + "start_line": 3648, + "end_line": 3659, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_batch_result_none_progress", + "kind": "function", + "start_line": 3662, + "end_line": 3674, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_blocks_processing", + "kind": "function", + "start_line": 2787, + "end_line": 2795, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_cache_analysis_profile_compatibility", + "kind": "function", + "start_line": 2011, + "end_line": 2081, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "expected_cache_schema_version", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "expected_cache_status", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "expected_cache_used", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='bool', ctx=Load())" + }, + { + "name": "expected_functions_total", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "expected_warning", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "first_min_loc", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "first_min_stmt", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "second_min_loc", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "second_min_stmt", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_cache_dir_override_respected", + "kind": "function", + "start_line": 723, + "end_line": 734, + "params": [ + { + "name": "flag", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_cache_not_shared_between_projects", + "kind": "function", + "start_line": 784, + "end_line": 799, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_cache_save_warning", + "kind": "function", + "start_line": 2825, + "end_line": 2846, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_cache_save_warning_quiet", + "kind": "function", + "start_line": 2849, + "end_line": 2876, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_cache_status_string_fallback", + "kind": "function", + "start_line": 952, + "end_line": 1003, + "params": [ + { + "name": "expected_status", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "load_warning", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_cache_warning", + "kind": "function", + "start_line": 2798, + "end_line": 2822, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_ci_discovery_cache_hit", + "kind": "function", + "start_line": 3217, + "end_line": 3258, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_ci_preset_fails_on_new", + "kind": "function", + "start_line": 2746, + "end_line": 2784, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_contract_error_priority_over_gating_failure_for_unreadable_source", + "kind": "function", + "start_line": 3101, + "end_line": 3138, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_dead_code_suppression_is_stable_between_plain_and_json_runs", + "kind": "function", + "start_line": 3860, + "end_line": 3919, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "source", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "suppressed_count", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='int', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_default_cache_dir_per_root", + "kind": "function", + "start_line": 737, + "end_line": 781, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_default_cache_dir_uses_root", + "kind": "function", + "start_line": 709, + "end_line": 719, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_discovery_cache_hit", + "kind": "function", + "start_line": 2913, + "end_line": 2966, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_discovery_skip_oserror", + "kind": "function", + "start_line": 2970, + "end_line": 3004, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "extra_args", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='list', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_fail_on_new_default_report_path", + "kind": "function", + "start_line": 3611, + "end_line": 3645, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_fail_on_new_no_report_path", + "kind": "function", + "start_line": 3486, + "end_line": 3510, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_fail_on_new_prints_groups", + "kind": "function", + "start_line": 3459, + "end_line": 3483, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_fail_on_new_verbose_and_report_path", + "kind": "function", + "start_line": 3571, + "end_line": 3608, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_fail_on_new_verbose_single_kind", + "kind": "function", + "start_line": 3520, + "end_line": 3568, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "expect_block", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='bool', ctx=Load())" + }, + { + "name": "expect_func", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='bool', ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "new_block", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='set', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "new_func", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='set', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_failed_batch_item_no_progress", + "kind": "function", + "start_line": 3677, + "end_line": 3688, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_failed_batch_item_progress", + "kind": "function", + "start_line": 3691, + "end_line": 3703, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_failed_files_report", + "kind": "function", + "start_line": 3371, + "end_line": 3389, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_failed_files_report_single", + "kind": "function", + "start_line": 3392, + "end_line": 3410, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_invalid_baseline_fails_in_ci", + "kind": "function", + "start_line": 1819, + "end_line": 1835, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_invalid_baseline_path_error_contract", + "kind": "function", + "start_line": 2885, + "end_line": 2910, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_invalid_root", + "kind": "function", + "start_line": 2879, + "end_line": 2882, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_invalid_root_exception", + "kind": "function", + "start_line": 1069, + "end_line": 1076, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_legacy_baseline_fail_on_new_fails_fast_exit_2", + "kind": "function", + "start_line": 1549, + "end_line": 1574, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_legacy_baseline_normal_mode_ignored_and_exit_zero", + "kind": "function", + "start_line": 1512, + "end_line": 1546, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_legacy_cache_resolve_failure", + "kind": "function", + "start_line": 823, + "end_line": 855, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_main_fail_on_new", + "kind": "function", + "start_line": 2669, + "end_line": 2702, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_main_fail_on_new_includes_blocks", + "kind": "function", + "start_line": 2705, + "end_line": 2743, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_main_fail_threshold", + "kind": "function", + "start_line": 2637, + "end_line": 2666, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_main_no_progress_fallback", + "kind": "function", + "start_line": 1020, + "end_line": 1031, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_main_no_progress_fallback_quiet", + "kind": "function", + "start_line": 1034, + "end_line": 1056, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_main_no_progress_parallel", + "kind": "function", + "start_line": 674, + "end_line": 706, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_main_outputs", + "kind": "function", + "start_line": 1133, + "end_line": 1172, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_main_progress_fallback", + "kind": "function", + "start_line": 1006, + "end_line": 1017, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_main_progress_path", + "kind": "function", + "start_line": 1059, + "end_line": 1066, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_negative_size_limits_fail_fast", + "kind": "function", + "start_line": 2627, + "end_line": 2634, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_new_clones_warning", + "kind": "function", + "start_line": 2416, + "end_line": 2449, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_no_legacy_warning_when_legacy_missing", + "kind": "function", + "start_line": 879, + "end_line": 890, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_no_legacy_warning_when_paths_match", + "kind": "function", + "start_line": 893, + "end_line": 945, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_no_legacy_warning_with_cache_override", + "kind": "function", + "start_line": 858, + "end_line": 876, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_open_html_report_failure_warns_without_failing", + "kind": "function", + "start_line": 1191, + "end_line": 1207, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_open_html_report_opens_written_html", + "kind": "function", + "start_line": 1175, + "end_line": 1188, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_output_extension_validation", + "kind": "function", + "start_line": 2094, + "end_line": 2118, + "params": [ + { + "name": "bad_name", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "expected", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "flag", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "label", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_output_path_resolve_error_contract", + "kind": "function", + "start_line": 2121, + "end_line": 2145, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_outputs_quiet_no_print", + "kind": "function", + "start_line": 2181, + "end_line": 2213, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_report_flag_contract_errors", + "kind": "function", + "start_line": 1281, + "end_line": 1299, + "params": [ + { + "name": "argv", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='list', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "expected_message", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_report_meta_cache_path_resolve_oserror_fallback", + "kind": "function", + "start_line": 3179, + "end_line": 3214, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_report_write_error_is_contract_error", + "kind": "function", + "start_line": 2148, + "end_line": 2178, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_reports_cache_meta_when_cache_missing", + "kind": "function", + "start_line": 1948, + "end_line": 1970, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_reports_cache_too_large_respects_max_size_flag", + "kind": "function", + "start_line": 1916, + "end_line": 1945, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_reports_cache_used_false_on_warning", + "kind": "function", + "start_line": 1879, + "end_line": 1913, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "expected_message", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "expected_schema_version", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='object', ctx=Load())" + }, + { + "name": "expected_status", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "mutator", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[List(elts=[Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())], ctx=Load()), Constant(value=None)], ctx=Load()), ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_reports_include_audit_metadata_baseline_too_large", + "kind": "function", + "start_line": 1687, + "end_line": 1707, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_reports_include_audit_metadata_fingerprint_mismatch", + "kind": "function", + "start_line": 1387, + "end_line": 1410, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_reports_include_audit_metadata_generator_mismatch", + "kind": "function", + "start_line": 1604, + "end_line": 1623, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_reports_include_audit_metadata_integrity_failed", + "kind": "function", + "start_line": 1577, + "end_line": 1601, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_reports_include_audit_metadata_integrity_field_type_errors", + "kind": "function", + "start_line": 1638, + "end_line": 1659, + "params": [ + { + "name": "bad_value", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='object', ctx=Load())" + }, + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "expected_message", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "expected_status", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "field", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_reports_include_audit_metadata_integrity_missing", + "kind": "function", + "start_line": 1662, + "end_line": 1684, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_reports_include_audit_metadata_invalid_baseline", + "kind": "function", + "start_line": 1465, + "end_line": 1481, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_reports_include_audit_metadata_legacy_baseline", + "kind": "function", + "start_line": 1484, + "end_line": 1509, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_reports_include_audit_metadata_missing_baseline", + "kind": "function", + "start_line": 1367, + "end_line": 1384, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_reports_include_audit_metadata_ok", + "kind": "function", + "start_line": 1302, + "end_line": 1364, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_reports_include_audit_metadata_python_mismatch", + "kind": "function", + "start_line": 1439, + "end_line": 1462, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_reports_include_audit_metadata_schema_mismatch", + "kind": "function", + "start_line": 1413, + "end_line": 1436, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_reports_include_source_io_skipped_zero", + "kind": "function", + "start_line": 3078, + "end_line": 3098, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_scan_failed_is_internal_error", + "kind": "function", + "start_line": 3338, + "end_line": 3351, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_scan_oserror_is_contract_error", + "kind": "function", + "start_line": 3354, + "end_line": 3368, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_summary_cache_miss_metrics", + "kind": "function", + "start_line": 3261, + "end_line": 3279, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_summary_format_stable", + "kind": "function", + "start_line": 3282, + "end_line": 3307, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_summary_no_color_has_no_ansi", + "kind": "function", + "start_line": 3325, + "end_line": 3335, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_summary_with_api_surface_shows_public_api_line", + "kind": "function", + "start_line": 3310, + "end_line": 3322, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_timestamped_report_paths_apply_to_bare_report_flags", + "kind": "function", + "start_line": 1210, + "end_line": 1238, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_timestamped_report_paths_do_not_rewrite_explicit_paths", + "kind": "function", + "start_line": 1241, + "end_line": 1263, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_too_large_baseline_fails_in_ci", + "kind": "function", + "start_line": 1838, + "end_line": 1859, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_unexpected_grouping_failure_is_internal", + "kind": "function", + "start_line": 1093, + "end_line": 1108, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_unexpected_html_render_failure_is_internal", + "kind": "function", + "start_line": 1111, + "end_line": 1130, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_unexpected_root_resolution_failure_is_internal", + "kind": "function", + "start_line": 1079, + "end_line": 1090, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_unreadable_source_ci_shows_overflow_summary", + "kind": "function", + "start_line": 3141, + "end_line": 3176, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_unreadable_source_fails_in_ci_with_contract_error", + "kind": "function", + "start_line": 3041, + "end_line": 3075, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_unreadable_source_normal_mode_warns_and_continues", + "kind": "function", + "start_line": 3007, + "end_line": 3038, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_untrusted_baseline_fails_in_ci", + "kind": "function", + "start_line": 1791, + "end_line": 1816, + "params": [ + { + "name": "bad_value", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='object', ctx=Load())" + }, + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "expected_message", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "expected_status", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "field", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_untrusted_baseline_ignored_for_diff", + "kind": "function", + "start_line": 1710, + "end_line": 1770, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_update_baseline", + "kind": "function", + "start_line": 2241, + "end_line": 2274, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_update_baseline_report_meta_uses_updated_payload_hash", + "kind": "function", + "start_line": 2277, + "end_line": 2305, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_update_baseline_skips_version_check", + "kind": "function", + "start_line": 2216, + "end_line": 2238, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_update_baseline_with_invalid_existing_file", + "kind": "function", + "start_line": 2338, + "end_line": 2368, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_update_baseline_write_error_is_contract_error", + "kind": "function", + "start_line": 2308, + "end_line": 2335, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_warns_on_legacy_cache", + "kind": "function", + "start_line": 802, + "end_line": 820, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_worker_failed", + "kind": "function", + "start_line": 3413, + "end_line": 3430, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_worker_failed_progress_sequential", + "kind": "function", + "start_line": 3433, + "end_line": 3443, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_worker_failed_sequential_no_progress", + "kind": "function", + "start_line": 3446, + "end_line": 3456, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_parse_metric_reason_entry_contract", + "kind": "function", + "start_line": 3943, + "end_line": 3946, + "params": [ + { + "name": "expected", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='str', ctx=Load())], ctx=Load()), ctx=Load())" + }, + { + "name": "reason", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_structural_findings_do_not_affect_clone_counts", + "kind": "function", + "start_line": 3722, + "end_line": 3743, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_structural_findings_do_not_affect_exit_code", + "kind": "function", + "start_line": 3746, + "end_line": 3756, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_structural_findings_recomputed_when_cache_was_built_without_reports", + "kind": "function", + "start_line": 3759, + "end_line": 3818, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + } + ] + }, + { + "module": "tests.test_cli_unit", + "filepath": "tests/test_cli_unit.py", + "all_declared": [], + "symbols": [ + { + "local_name": "test_argument_parser_contract_error_marker_for_invalid_args", + "kind": "function", + "start_line": 358, + "end_line": 366, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_banner_title_without_root_returns_single_line", + "kind": "function", + "start_line": 923, + "end_line": 926, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_changed_clone_gate_from_report_filters_changed_scope", + "kind": "function", + "start_line": 563, + "end_line": 619, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_help_text_consistency", + "kind": "function", + "start_line": 178, + "end_line": 229, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_internal_error_debug_env_includes_traceback", + "kind": "function", + "start_line": 340, + "end_line": 355, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_internal_error_debug_flag_includes_traceback", + "kind": "function", + "start_line": 322, + "end_line": 337, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_internal_error_marker", + "kind": "function", + "start_line": 302, + "end_line": 319, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_module_main_guard", + "kind": "function", + "start_line": 152, + "end_line": 156, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_plain_console_status_context", + "kind": "function", + "start_line": 296, + "end_line": 299, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_cli_version_flag_no_side_effects", + "kind": "function", + "start_line": 159, + "end_line": 175, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_compact_summary_labels_use_machine_scannable_keys", + "kind": "function", + "start_line": 950, + "end_line": 971, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_configure_metrics_mode_forces_dependency_and_dead_code_when_gated", + "kind": "function", + "start_line": 1195, + "end_line": 1211, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_configure_metrics_mode_rejects_skip_metrics_with_metrics_flags", + "kind": "function", + "start_line": 1173, + "end_line": 1192, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_enforce_gating_drops_rewritten_threshold_when_changed_scope_is_within_limit", + "kind": "function", + "start_line": 730, + "end_line": 765, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_enforce_gating_rewrites_clone_threshold_for_changed_scope", + "kind": "function", + "start_line": 684, + "end_line": 727, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_git_diff_changed_paths_normalizes_subprocess_output", + "kind": "function", + "start_line": 483, + "end_line": 504, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_git_diff_changed_paths_rejects_option_like_ref", + "kind": "function", + "start_line": 521, + "end_line": 527, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_git_diff_changed_paths_rejects_unsafe_ref_syntax", + "kind": "function", + "start_line": 540, + "end_line": 550, + "params": [ + { + "name": "git_diff_ref", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_git_diff_changed_paths_reports_subprocess_errors", + "kind": "function", + "start_line": 507, + "end_line": 518, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_main_impl_ci_enables_fail_on_new_metrics_when_metrics_baseline_loaded", + "kind": "function", + "start_line": 1795, + "end_line": 1833, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_main_impl_debug_sets_env_and_handles_metrics_baseline_resolve_error", + "kind": "function", + "start_line": 1288, + "end_line": 1317, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_main_impl_exits_on_invalid_pyproject_config", + "kind": "function", + "start_line": 1273, + "end_line": 1285, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_main_impl_fail_on_new_metrics_handles_load_error", + "kind": "function", + "start_line": 1650, + "end_line": 1659, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_main_impl_fail_on_new_metrics_handles_verify_error", + "kind": "function", + "start_line": 1662, + "end_line": 1682, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_main_impl_fail_on_new_metrics_requires_existing_baseline", + "kind": "function", + "start_line": 1629, + "end_line": 1647, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_main_impl_prints_changed_scope_when_changed_projection_is_available", + "kind": "function", + "start_line": 768, + "end_line": 887, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_main_impl_prints_metric_gate_reasons_and_exits_gating_failure", + "kind": "function", + "start_line": 1503, + "end_line": 1545, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_main_impl_rejects_update_metrics_baseline_when_metrics_skipped", + "kind": "function", + "start_line": 1459, + "end_line": 1478, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_main_impl_skip_metrics_defensive_contract_guard", + "kind": "function", + "start_line": 1606, + "end_line": 1626, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_main_impl_unified_metrics_update_auto_enables_baseline_update", + "kind": "function", + "start_line": 1579, + "end_line": 1603, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_main_impl_update_metrics_baseline_requires_project_metrics", + "kind": "function", + "start_line": 1481, + "end_line": 1500, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_main_impl_update_metrics_baseline_separate_path_message_branch", + "kind": "function", + "start_line": 1764, + "end_line": 1792, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_main_impl_update_metrics_baseline_write_error_contract", + "kind": "function", + "start_line": 1732, + "end_line": 1761, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_main_impl_uses_configured_metrics_baseline_without_cli_flag", + "kind": "function", + "start_line": 1548, + "end_line": 1576, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_make_console_caps_width_to_layout_limit", + "kind": "function", + "start_line": 890, + "end_line": 920, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_runtime_requires_adoption_snapshot_for_regression_gates", + "kind": "function", + "start_line": 1685, + "end_line": 1706, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_runtime_requires_api_surface_snapshot_for_api_gate", + "kind": "function", + "start_line": 1709, + "end_line": 1729, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_computed_includes_api_surface_only_when_enabled", + "kind": "function", + "start_line": 1247, + "end_line": 1270, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_computed_respects_skip_switches", + "kind": "function", + "start_line": 1222, + "end_line": 1244, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_normalize_changed_paths_rejects_outside_root", + "kind": "function", + "start_line": 470, + "end_line": 480, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_normalize_changed_paths_relativizes_dedupes_and_sorts", + "kind": "function", + "start_line": 417, + "end_line": 429, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_normalize_changed_paths_reports_unresolvable_path", + "kind": "function", + "start_line": 452, + "end_line": 467, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_normalize_changed_paths_skips_empty_relative_results", + "kind": "function", + "start_line": 432, + "end_line": 449, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_open_html_report_in_browser_raises_without_handler", + "kind": "function", + "start_line": 269, + "end_line": 283, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_open_html_report_in_browser_succeeds_when_handler_exists", + "kind": "function", + "start_line": 286, + "end_line": 293, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_print_changed_scope_uses_compact_line_in_quiet_mode", + "kind": "function", + "start_line": 1078, + "end_line": 1093, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_print_changed_scope_uses_dedicated_block", + "kind": "function", + "start_line": 1057, + "end_line": 1075, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_print_metrics_in_normal_mode_includes_adoption_and_public_api", + "kind": "function", + "start_line": 1127, + "end_line": 1170, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_print_metrics_in_quiet_mode_includes_overloaded_modules", + "kind": "function", + "start_line": 1096, + "end_line": 1124, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_print_summary_invariant_warning", + "kind": "function", + "start_line": 929, + "end_line": 947, + "params": [ + { + "name": "capsys", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" + }, + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_print_verbose_clone_hashes_noop_on_empty", + "kind": "function", + "start_line": 1836, + "end_line": 1843, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_print_verbose_clone_hashes_prints_sorted_values", + "kind": "function", + "start_line": 1846, + "end_line": 1857, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_probe_metrics_baseline_section_for_non_object_payload", + "kind": "function", + "start_line": 1214, + "end_line": 1219, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_process_file_encoding_error", + "kind": "function", + "start_line": 96, + "end_line": 109, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_process_file_read_oserror", + "kind": "function", + "start_line": 112, + "end_line": 125, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_process_file_stat_error", + "kind": "function", + "start_line": 76, + "end_line": 93, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_process_file_success", + "kind": "function", + "start_line": 144, + "end_line": 149, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_process_file_unexpected_error", + "kind": "function", + "start_line": 128, + "end_line": 141, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_report_path_origins_distinguish_bare_and_explicit_flags", + "kind": "function", + "start_line": 232, + "end_line": 248, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_report_path_origins_ignores_unrelated_equals_tokens", + "kind": "function", + "start_line": 553, + "end_line": 560, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_report_path_origins_stops_at_double_dash", + "kind": "function", + "start_line": 251, + "end_line": 258, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_run_analysis_stages_prints_source_read_failures_when_failed_files_are_empty", + "kind": "function", + "start_line": 643, + "end_line": 681, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_run_analysis_stages_requires_rich_console_when_progress_ui_is_enabled", + "kind": "function", + "start_line": 622, + "end_line": 640, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_timestamped_report_path_appends_utc_slug", + "kind": "function", + "start_line": 261, + "end_line": 266, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_ui_summary_formatters_cover_optional_branches", + "kind": "function", + "start_line": 974, + "end_line": 1054, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_validate_changed_scope_args_promotes_paths_from_git_diff", + "kind": "function", + "start_line": 407, + "end_line": 414, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_validate_changed_scope_args_rejects_invalid_combinations", + "kind": "function", + "start_line": 398, + "end_line": 404, + "params": [ + { + "name": "args", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Namespace', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + } + ] + }, + { + "module": "tests.test_html_report", + "filepath": "tests/test_html_report.py", + "all_declared": [], + "symbols": [ + { + "local_name": "build_html_report", + "kind": "function", + "start_line": 68, + "end_line": 87, + "params": [ + { + "name": "block_group_facts", + "kind": "kw_only", + "has_default": true, + "annotation_hash": "BinOp(left=Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='str', ctx=Load())], ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" + }, + { + "name": "block_groups", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='list', ctx=Load()), slice=Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='Any', ctx=Load())], ctx=Load()), ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" + }, + { + "name": "func_groups", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='list', ctx=Load()), slice=Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='Any', ctx=Load())], ctx=Load()), ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" + }, + { + "name": "segment_groups", + "kind": "kw_only", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='list', ctx=Load()), slice=Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='Any', ctx=Load())], ctx=Load()), ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" + }, + { + "name": "kwargs", + "kind": "kwarg", + "has_default": false, + "annotation_hash": "Name(id='Any', ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + }, + { + "local_name": "test_file_cache_missing_file", + "kind": "function", + "start_line": 1160, + "end_line": 1164, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_file_cache_range_bounds", + "kind": "function", + "start_line": 1196, + "end_line": 1203, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_file_cache_reads_ranges", + "kind": "function", + "start_line": 1146, + "end_line": 1157, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_file_cache_unicode_fallback", + "kind": "function", + "start_line": 1188, + "end_line": 1193, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_and_json_group_order_consistent", + "kind": "function", + "start_line": 1070, + "end_line": 1116, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_bare_qualname_keeps_non_python_path_prefix", + "kind": "function", + "start_line": 2441, + "end_line": 2457, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_block_group_includes_assert_only_explanation", + "kind": "function", + "start_line": 672, + "end_line": 681, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_block_group_includes_match_basis_and_compact_key", + "kind": "function", + "start_line": 645, + "end_line": 669, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_block_group_n_way_compare_hint", + "kind": "function", + "start_line": 684, + "end_line": 693, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_blocks_without_explanation_meta", + "kind": "function", + "start_line": 755, + "end_line": 760, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_coupling_coupled_classes_expands_for_more_than_three", + "kind": "function", + "start_line": 2241, + "end_line": 2254, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_coupling_coupled_classes_inline_for_three_or_less", + "kind": "function", + "start_line": 2229, + "end_line": 2238, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_coupling_coupled_classes_truncates_long_labels", + "kind": "function", + "start_line": 2257, + "end_line": 2261, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_dependency_chain_columns_render_html", + "kind": "function", + "start_line": 2413, + "end_line": 2438, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_dependency_graph_handles_rootless_and_disconnected_nodes", + "kind": "function", + "start_line": 2264, + "end_line": 2297, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_dependency_graph_rootless_fallback_seed", + "kind": "function", + "start_line": 2300, + "end_line": 2322, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_dependency_hubs_deterministic_tie_order", + "kind": "function", + "start_line": 2374, + "end_line": 2410, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_deterministic_group_order", + "kind": "function", + "start_line": 1033, + "end_line": 1067, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_direct_path_skips_directory_hotspots_cluster", + "kind": "function", + "start_line": 2023, + "end_line": 2042, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_directory_hotspots_use_test_scope_roots", + "kind": "function", + "start_line": 2045, + "end_line": 2115, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_empty", + "kind": "function", + "start_line": 196, + "end_line": 202, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_escapes_control_chars_in_payload", + "kind": "function", + "start_line": 1119, + "end_line": 1143, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_escapes_meta_and_title", + "kind": "function", + "start_line": 975, + "end_line": 1006, + "params": [ + { + "name": "report_meta_factory", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[Constant(value=Ellipsis), Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_escapes_script_breakout_payload", + "kind": "function", + "start_line": 1009, + "end_line": 1030, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_executive_summary_includes_effective_analysis_profile", + "kind": "function", + "start_line": 1831, + "end_line": 1878, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_explanation_without_match_rule", + "kind": "function", + "start_line": 798, + "end_line": 809, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_exposes_scope_counter_hooks_for_clone_ui", + "kind": "function", + "start_line": 368, + "end_line": 411, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_finding_cards_expose_stable_anchor_ids", + "kind": "function", + "start_line": 573, + "end_line": 642, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_footer_links_present", + "kind": "function", + "start_line": 887, + "end_line": 892, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_generation", + "kind": "function", + "start_line": 214, + "end_line": 236, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_group_and_item_metadata_attrs", + "kind": "function", + "start_line": 239, + "end_line": 265, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_handles_root_only_baseline_path", + "kind": "function", + "start_line": 788, + "end_line": 795, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_includes_provenance_metadata", + "kind": "function", + "start_line": 895, + "end_line": 947, + "params": [ + { + "name": "report_meta_factory", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[Constant(value=Ellipsis), Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_metrics_bad_health_score_and_dead_code_ok_tone", + "kind": "function", + "start_line": 2118, + "end_line": 2139, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_metrics_bool_health_score_and_long_dependency_labels", + "kind": "function", + "start_line": 2142, + "end_line": 2172, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_metrics_object_health_score_uses_float_fallback", + "kind": "function", + "start_line": 2207, + "end_line": 2226, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_metrics_risk_branches", + "kind": "function", + "start_line": 1625, + "end_line": 1653, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_metrics_warn_branches_and_dependency_svg", + "kind": "function", + "start_line": 1599, + "end_line": 1622, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_metrics_without_health_score_uses_info_overview", + "kind": "function", + "start_line": 1955, + "end_line": 1980, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_missing_source_snippet_fallback", + "kind": "function", + "start_line": 1167, + "end_line": 1185, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_mobile_topbar_reflows_brand_block", + "kind": "function", + "start_line": 839, + "end_line": 849, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_n_way_group_without_compare_note", + "kind": "function", + "start_line": 812, + "end_line": 824, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_narrow_kpi_cards_keep_badges_inside_card", + "kind": "function", + "start_line": 852, + "end_line": 866, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_overview_includes_adoption_and_api_summary_cluster", + "kind": "function", + "start_line": 1881, + "end_line": 1952, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_overview_includes_hotspot_sections_without_quick_views", + "kind": "function", + "start_line": 2495, + "end_line": 2545, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_overview_uses_canonical_report_overview_hotlists", + "kind": "function", + "start_line": 2548, + "end_line": 2670, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_provenance_badges_cover_mismatch_and_untrusted_metrics", + "kind": "function", + "start_line": 2325, + "end_line": 2352, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_provenance_handles_non_boolean_baseline_loaded", + "kind": "function", + "start_line": 2355, + "end_line": 2371, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_provenance_summary_uses_card_like_badges", + "kind": "function", + "start_line": 950, + "end_line": 972, + "params": [ + { + "name": "report_meta_factory", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[Constant(value=Ellipsis), Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_pygments_fallback", + "kind": "function", + "start_line": 1314, + "end_line": 1326, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_renders_block_novelty_tabs_and_group_flags", + "kind": "function", + "start_line": 335, + "end_line": 365, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_renders_dead_code_split_with_suppressed_layer", + "kind": "function", + "start_line": 2175, + "end_line": 2204, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_renders_directory_hotspots_from_canonical_report", + "kind": "function", + "start_line": 1983, + "end_line": 2020, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_renders_novelty_tabs_and_group_flags", + "kind": "function", + "start_line": 268, + "end_line": 309, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_renders_overloaded_modules_from_legacy_god_modules_key", + "kind": "function", + "start_line": 1733, + "end_line": 1757, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_renders_overloaded_modules_in_quality_and_overview", + "kind": "function", + "start_line": 1656, + "end_line": 1730, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_renders_run_snapshot_from_canonical_inventory", + "kind": "function", + "start_line": 1760, + "end_line": 1828, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_renders_untrusted_baseline_novelty_note", + "kind": "function", + "start_line": 312, + "end_line": 332, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_requires_block_group_facts_argument", + "kind": "function", + "start_line": 205, + "end_line": 211, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_respects_sparse_core_block_facts", + "kind": "function", + "start_line": 763, + "end_line": 785, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_segments_section", + "kind": "function", + "start_line": 1329, + "end_line": 1356, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_single_item_group", + "kind": "function", + "start_line": 1359, + "end_line": 1379, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_structural_findings_tab_uses_normalized_groups", + "kind": "function", + "start_line": 414, + "end_line": 500, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_structural_findings_why_modal_renders_examples", + "kind": "function", + "start_line": 503, + "end_line": 570, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_suggestions_cards_split_facts_assessment_and_action", + "kind": "function", + "start_line": 2460, + "end_line": 2492, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_table_css_matches_rendered_column_classes", + "kind": "function", + "start_line": 869, + "end_line": 884, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_topbar_actions_present", + "kind": "function", + "start_line": 827, + "end_line": 836, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_uses_core_block_group_facts", + "kind": "function", + "start_line": 696, + "end_line": 718, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_uses_core_hint_and_pattern_labels", + "kind": "function", + "start_line": 721, + "end_line": 736, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_uses_core_hint_context_label", + "kind": "function", + "start_line": 739, + "end_line": 752, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_html_report_with_blocks", + "kind": "function", + "start_line": 1281, + "end_line": 1311, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_pygments_css", + "kind": "function", + "start_line": 1230, + "end_line": 1232, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_pygments_css_formatter_init_fails", + "kind": "function", + "start_line": 1415, + "end_line": 1424, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_pygments_css_get_style_defs_error", + "kind": "function", + "start_line": 1403, + "end_line": 1412, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_pygments_css_import_error", + "kind": "function", + "start_line": 1240, + "end_line": 1245, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_pygments_css_invalid_style", + "kind": "function", + "start_line": 1235, + "end_line": 1237, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_render_code_block_truncate", + "kind": "function", + "start_line": 1206, + "end_line": 1227, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_render_code_block_truncates_and_fallback", + "kind": "function", + "start_line": 1382, + "end_line": 1400, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_render_code_block_without_pygments_uses_escaped_fallback", + "kind": "function", + "start_line": 1261, + "end_line": 1278, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_try_pygments_missing", + "kind": "function", + "start_line": 1248, + "end_line": 1253, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_try_pygments_ok", + "kind": "function", + "start_line": 1256, + "end_line": 1258, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "to_json_report", + "kind": "function", + "start_line": 55, + "end_line": 65, + "params": [ + { + "name": "block_groups", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='list', ctx=Load()), slice=Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='Any', ctx=Load())], ctx=Load()), ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" + }, + { + "name": "func_groups", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='list', ctx=Load()), slice=Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='Any', ctx=Load())], ctx=Load()), ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" + }, + { + "name": "segment_groups", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='list', ctx=Load()), slice=Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='Any', ctx=Load())], ctx=Load()), ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" + } + ], + "returns_hash": "Name(id='str', ctx=Load())", + "exported_via": "name" + } + ] + }, + { + "module": "tests.test_metrics_baseline", + "filepath": "tests/test_metrics_baseline.py", + "all_declared": [], + "symbols": [ + { + "local_name": "test_api_surface_payload_hashes_are_order_independent", + "kind": "function", + "start_line": 412, + "end_line": 433, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_coerce_metrics_baseline_status_variants", + "kind": "function", + "start_line": 227, + "end_line": 240, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_atomic_write_json_cleans_up_temp_file_on_replace_failure", + "kind": "function", + "start_line": 449, + "end_line": 466, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_diff_tracks_adoption_and_api_surface_deltas", + "kind": "function", + "start_line": 551, + "end_line": 609, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_diff_without_snapshot_uses_default_snapshot", + "kind": "function", + "start_line": 539, + "end_line": 548, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_embedded_clone_payload_and_schema_resolution", + "kind": "function", + "start_line": 893, + "end_line": 963, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_field_parsers_and_cycle_parser", + "kind": "function", + "start_line": 795, + "end_line": 846, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_json_and_structure_validators", + "kind": "function", + "start_line": 775, + "end_line": 792, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_load_accepts_absolute_api_surface_filepaths", + "kind": "function", + "start_line": 742, + "end_line": 772, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_load_accepts_legacy_api_surface_qualnames", + "kind": "function", + "start_line": 698, + "end_line": 739, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_load_json_read_oserror_status", + "kind": "function", + "start_line": 974, + "end_line": 987, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_load_missing_file_is_noop", + "kind": "function", + "start_line": 243, + "end_line": 246, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_load_rejects_non_object_preloaded_payload", + "kind": "function", + "start_line": 308, + "end_line": 317, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_load_size_and_shape_validation", + "kind": "function", + "start_line": 291, + "end_line": 305, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_load_stat_error_after_exists_true", + "kind": "function", + "start_line": 320, + "end_line": 346, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_load_stat_errors", + "kind": "function", + "start_line": 249, + "end_line": 288, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + }, + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_load_tracks_adoption_snapshot_presence", + "kind": "function", + "start_line": 647, + "end_line": 669, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_parse_generator_variants", + "kind": "function", + "start_line": 849, + "end_line": 890, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_parse_snapshot_grade_validation", + "kind": "function", + "start_line": 966, + "end_line": 971, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_save_embedded_clone_baseline_preserves_api_surface", + "kind": "function", + "start_line": 672, + "end_line": 695, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_save_rejects_corrupted_existing_payload", + "kind": "function", + "start_line": 469, + "end_line": 479, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_save_relativizes_api_surface_filepaths", + "kind": "function", + "start_line": 394, + "end_line": 409, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_save_requires_snapshot", + "kind": "function", + "start_line": 349, + "end_line": 352, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_save_standalone_payload_sets_metadata", + "kind": "function", + "start_line": 355, + "end_line": 372, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_save_with_existing_plain_payload_rewrites_plain", + "kind": "function", + "start_line": 436, + "end_line": 446, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_save_writes_compact_api_surface_local_names", + "kind": "function", + "start_line": 375, + "end_line": 391, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_verify_accepts_previous_minor_versions", + "kind": "function", + "start_line": 522, + "end_line": 536, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_baseline_verify_compatibility_and_integrity_failures", + "kind": "function", + "start_line": 482, + "end_line": 519, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_snapshot_from_project_metrics_and_from_project_metrics_factory", + "kind": "function", + "start_line": 612, + "end_line": 644, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + } + ] + }, + { + "module": "tests.test_pipeline_metrics", + "filepath": "tests/test_pipeline_metrics.py", + "all_declared": [], + "symbols": [ + { + "local_name": "test_build_metrics_report_payload_includes_adoption_and_api_surface_families", + "kind": "function", + "start_line": 263, + "end_line": 335, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_build_metrics_report_payload_includes_suppressed_dead_code_items", + "kind": "function", + "start_line": 229, + "end_line": 260, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_build_overloaded_modules_payload_flags_project_relative_candidates", + "kind": "function", + "start_line": 389, + "end_line": 456, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_compute_project_metrics_respects_skip_flags", + "kind": "function", + "start_line": 189, + "end_line": 226, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_enrich_metrics_report_payload_adds_docstring_and_breaking_api_rows", + "kind": "function", + "start_line": 725, + "end_line": 784, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_load_cached_metrics_extended_decodes_adoption_and_api_surface", + "kind": "function", + "start_line": 638, + "end_line": 688, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_load_cached_metrics_ignores_referenced_names_from_test_files", + "kind": "function", + "start_line": 459, + "end_line": 478, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_load_cached_metrics_preserves_coupled_classes", + "kind": "function", + "start_line": 481, + "end_line": 508, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_load_cached_metrics_preserves_dead_candidate_suppressions", + "kind": "function", + "start_line": 511, + "end_line": 534, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metric_gate_reasons_collects_all_enabled_reasons", + "kind": "function", + "start_line": 691, + "end_line": 722, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metric_gate_reasons_include_adoption_and_api_surface_contracts", + "kind": "function", + "start_line": 856, + "end_line": 904, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metric_gate_reasons_new_metrics_optional_buckets_empty", + "kind": "function", + "start_line": 830, + "end_line": 853, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metric_gate_reasons_partial_new_metrics_paths", + "kind": "function", + "start_line": 804, + "end_line": 827, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metric_gate_reasons_skip_disabled_and_non_critical_paths", + "kind": "function", + "start_line": 787, + "end_line": 801, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_metrics_payload_includes_overloaded_modules_for_small_population", + "kind": "function", + "start_line": 338, + "end_line": 386, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_module_names_from_units_extracts_module_prefixes", + "kind": "function", + "start_line": 178, + "end_line": 186, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_pipeline_basic_helpers_and_sort_keys", + "kind": "function", + "start_line": 150, + "end_line": 175, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_pipeline_cache_decode_helpers_cover_invalid_and_valid_payloads", + "kind": "function", + "start_line": 537, + "end_line": 635, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + } + ] + }, + { + "module": "tests.test_report_contract_coverage", + "filepath": "tests/test_report_contract_coverage.py", + "all_declared": [], + "symbols": [ + { + "local_name": "test_build_report_document_suppressed_dead_code_accepts_empty_bindings", + "kind": "function", + "start_line": 2238, + "end_line": 2278, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_coerce_helper_numeric_branches", + "kind": "function", + "start_line": 1130, + "end_line": 1136, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_collect_paths_from_metrics_covers_all_metric_families_and_skips_missing", + "kind": "function", + "start_line": 2055, + "end_line": 2117, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_collect_report_file_list_deterministically_merges_all_sources", + "kind": "function", + "start_line": 2120, + "end_line": 2185, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_count_file_lines_aggregates_paths", + "kind": "function", + "start_line": 1139, + "end_line": 1144, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_derive_inventory_code_counts_uses_cached_line_scan_fallback", + "kind": "function", + "start_line": 1147, + "end_line": 1177, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_derived_module_branches", + "kind": "function", + "start_line": 1427, + "end_line": 1467, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_directory_hotspot_helpers_cover_fallback_paths", + "kind": "function", + "start_line": 939, + "end_line": 1010, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_directory_hotspots_collapses_test_scope_roots_for_overview", + "kind": "function", + "start_line": 831, + "end_line": 936, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_directory_hotspots_has_more_root_paths_and_stable_sort", + "kind": "function", + "start_line": 757, + "end_line": 828, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_json_contract_private_helper_edge_branches", + "kind": "function", + "start_line": 2188, + "end_line": 2235, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_json_contract_private_helpers_cover_edge_cases", + "kind": "function", + "start_line": 1034, + "end_line": 1127, + "params": [ + { + "name": "tmp_path", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Path', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_markdown_and_sarif_reuse_prebuilt_report_document", + "kind": "function", + "start_line": 1013, + "end_line": 1031, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_markdown_render_long_list_branches", + "kind": "function", + "start_line": 1180, + "end_line": 1218, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_overview_handles_non_mapping_metric_summaries", + "kind": "function", + "start_line": 1555, + "end_line": 1586, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_overview_health_snapshot_handles_non_mapping_dimensions", + "kind": "function", + "start_line": 1589, + "end_line": 1600, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_overview_module_branches", + "kind": "function", + "start_line": 1470, + "end_line": 1552, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_render_sarif_report_document_without_srcroot_keeps_relative_payload", + "kind": "function", + "start_line": 1997, + "end_line": 2052, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_report_contract_includes_canonical_adoption_and_api_surface_families", + "kind": "function", + "start_line": 1281, + "end_line": 1417, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_report_contract_includes_canonical_overloaded_modules_family", + "kind": "function", + "start_line": 1246, + "end_line": 1278, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_report_contract_renderers_include_overloaded_modules_section", + "kind": "function", + "start_line": 1233, + "end_line": 1243, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_report_document_design_thresholds_can_change_canonical_findings", + "kind": "function", + "start_line": 679, + "end_line": 754, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_report_document_rich_invariants_and_renderers", + "kind": "function", + "start_line": 573, + "end_line": 676, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_sarif_and_serialize_helpers_cover_missing_primary_path_and_no_empty_tail", + "kind": "function", + "start_line": 2442, + "end_line": 2594, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_sarif_helper_level_mapping", + "kind": "function", + "start_line": 1420, + "end_line": 1424, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_sarif_private_helper_branches", + "kind": "function", + "start_line": 1683, + "end_line": 1752, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_sarif_private_helper_edge_branches", + "kind": "function", + "start_line": 1963, + "end_line": 1994, + "params": [ + { + "name": "monkeypatch", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_sarif_private_helper_family_dispatches", + "kind": "function", + "start_line": 1755, + "end_line": 1960, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_serialize_private_helpers_cover_structural_and_suppression_paths", + "kind": "function", + "start_line": 2281, + "end_line": 2439, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_suggestion_finding_id_clone_branches", + "kind": "function", + "start_line": 1676, + "end_line": 1680, + "params": [ + { + "name": "expected_finding_id", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='str', ctx=Load())" + }, + { + "name": "suggestion", + "kind": "pos_or_kw", + "has_default": false, + "annotation_hash": "Name(id='Suggestion', ctx=Load())" + } + ], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + }, + { + "local_name": "test_suggestion_finding_id_fallback_branch", + "kind": "function", + "start_line": 1603, + "end_line": 1622, + "params": [], + "returns_hash": "Constant(value=None)", + "exported_via": "name" + } + ] + } + ] } } diff --git a/codeclone/_cli_args.py b/codeclone/_cli_args.py index 17a2a2f..53418ec 100644 --- a/codeclone/_cli_args.py +++ b/codeclone/_cli_args.py @@ -213,6 +213,23 @@ def build_parser(version: str) -> _ArgumentParser: flag="--ci", help_text=ui.HELP_CI, ) + _add_bool_optional_argument( + baselines_ci_group, + flag="--typing-coverage", + help_text=ui.HELP_TYPING_COVERAGE, + default=True, + ) + _add_bool_optional_argument( + baselines_ci_group, + flag="--docstring-coverage", + help_text=ui.HELP_DOCSTRING_COVERAGE, + default=True, + ) + _add_bool_optional_argument( + baselines_ci_group, + flag="--api-surface", + help_text=ui.HELP_API_SURFACE, + ) quality_group = ap.add_argument_group("Quality gates") _add_bool_optional_argument( @@ -278,6 +295,35 @@ def build_parser(version: str) -> _ArgumentParser: metavar="SCORE_MIN", help=ui.HELP_FAIL_HEALTH, ) + _add_bool_optional_argument( + quality_group, + flag="--fail-on-typing-regression", + help_text=ui.HELP_FAIL_ON_TYPING_REGRESSION, + ) + _add_bool_optional_argument( + quality_group, + flag="--fail-on-docstring-regression", + help_text=ui.HELP_FAIL_ON_DOCSTRING_REGRESSION, + ) + _add_bool_optional_argument( + quality_group, + flag="--fail-on-api-break", + help_text=ui.HELP_FAIL_ON_API_BREAK, + ) + quality_group.add_argument( + "--min-typing-coverage", + type=int, + default=-1, + metavar="PERCENT", + help=ui.HELP_MIN_TYPING_COVERAGE, + ) + quality_group.add_argument( + "--min-docstring-coverage", + type=int, + default=-1, + metavar="PERCENT", + help=ui.HELP_MIN_DOCSTRING_COVERAGE, + ) stages_group = ap.add_argument_group("Analysis stages") _add_bool_optional_argument( diff --git a/codeclone/_cli_baselines.py b/codeclone/_cli_baselines.py index f06839a..19120a9 100644 --- a/codeclone/_cli_baselines.py +++ b/codeclone/_cli_baselines.py @@ -59,6 +59,9 @@ class _BaselineArgs(Protocol): skip_metrics: bool update_metrics_baseline: bool fail_on_new_metrics: bool + fail_on_typing_regression: bool + fail_on_docstring_regression: bool + fail_on_api_break: bool ci: bool @@ -274,7 +277,13 @@ def _metrics_mode_short_circuit( ) -> bool: if not args.skip_metrics: return False - if args.update_metrics_baseline or args.fail_on_new_metrics: + if ( + args.update_metrics_baseline + or args.fail_on_new_metrics + or args.fail_on_typing_regression + or args.fail_on_docstring_regression + or args.fail_on_api_break + ): console.print( ui.fmt_contract_error( "Metrics baseline operations require metrics analysis. " @@ -294,12 +303,12 @@ def _load_metrics_baseline_for_diff( shared_baseline_payload: dict[str, object] | None = None, ) -> None: if not metrics_baseline_exists: - if args.fail_on_new_metrics and not args.update_metrics_baseline: + if _metrics_baseline_gate_requested(args) and not args.update_metrics_baseline: state.failure_code = ExitCode.CONTRACT_ERROR console.print( ui.fmt_contract_error( - "Metrics baseline file is required for --fail-on-new-metrics. " - "Run codeclone . --update-metrics-baseline first." + "Metrics baseline file is required for metrics baseline-aware " + "gates. Run codeclone . --update-metrics-baseline first." ) ) return @@ -334,6 +343,11 @@ def _load_metrics_baseline_for_diff( state.loaded = True state.status = MetricsBaselineStatus.OK state.trusted_for_diff = True + _enforce_metrics_gate_schema_requirements( + args=args, + state=state, + console=console, + ) def _apply_metrics_baseline_untrusted_policy( @@ -345,7 +359,7 @@ def _apply_metrics_baseline_untrusted_policy( return state.loaded = False state.trusted_for_diff = False - if args.fail_on_new_metrics and not args.update_metrics_baseline: + if _metrics_baseline_gate_requested(args) and not args.update_metrics_baseline: state.failure_code = ExitCode.CONTRACT_ERROR @@ -392,3 +406,51 @@ def _update_metrics_baseline_if_requested( state.loaded = True state.status = MetricsBaselineStatus.OK state.trusted_for_diff = True + + +def _metrics_baseline_gate_requested(args: _BaselineArgs) -> bool: + return bool( + args.fail_on_new_metrics + or args.fail_on_typing_regression + or args.fail_on_docstring_regression + or args.fail_on_api_break + ) + + +def _enforce_metrics_gate_schema_requirements( + *, + args: _BaselineArgs, + state: _MetricsBaselineRuntime, + console: _PrinterLike, +) -> None: + baseline = state.baseline + needs_adoption_snapshot = bool( + args.fail_on_typing_regression or args.fail_on_docstring_regression + ) + if needs_adoption_snapshot and not getattr( + baseline, "has_coverage_adoption_snapshot", False + ): + state.loaded = False + state.trusted_for_diff = False + state.status = MetricsBaselineStatus.MISMATCH_SCHEMA_VERSION + state.failure_code = ExitCode.CONTRACT_ERROR + console.print( + ui.fmt_contract_error( + "Typing/docstring regression gates require a metrics baseline " + "that includes coverage adoption data. Run codeclone . " + "--update-metrics-baseline first." + ) + ) + return + if args.fail_on_api_break and baseline.api_surface_snapshot is None: + state.loaded = False + state.trusted_for_diff = False + state.status = MetricsBaselineStatus.MISMATCH_SCHEMA_VERSION + state.failure_code = ExitCode.CONTRACT_ERROR + console.print( + ui.fmt_contract_error( + "API break gating requires a metrics baseline with public API " + "surface data. Run codeclone . --api-surface " + "--update-metrics-baseline first." + ) + ) diff --git a/codeclone/_cli_config.py b/codeclone/_cli_config.py index 22efec1..1a48584 100644 --- a/codeclone/_cli_config.py +++ b/codeclone/_cli_config.py @@ -50,6 +50,14 @@ class _ConfigKeySpec: "fail_dead_code": _ConfigKeySpec(bool), "fail_health": _ConfigKeySpec(int), "fail_on_new_metrics": _ConfigKeySpec(bool), + "typing_coverage": _ConfigKeySpec(bool), + "docstring_coverage": _ConfigKeySpec(bool), + "api_surface": _ConfigKeySpec(bool), + "fail_on_typing_regression": _ConfigKeySpec(bool), + "fail_on_docstring_regression": _ConfigKeySpec(bool), + "fail_on_api_break": _ConfigKeySpec(bool), + "min_typing_coverage": _ConfigKeySpec(int), + "min_docstring_coverage": _ConfigKeySpec(int), "update_metrics_baseline": _ConfigKeySpec(bool), "metrics_baseline": _ConfigKeySpec(str), "skip_metrics": _ConfigKeySpec(bool), diff --git a/codeclone/_cli_gating.py b/codeclone/_cli_gating.py index 96b96e2..90de2ca 100644 --- a/codeclone/_cli_gating.py +++ b/codeclone/_cli_gating.py @@ -18,12 +18,17 @@ class _GatingArgs(Protocol): ci: bool fail_on_new_metrics: bool + fail_on_typing_regression: bool + fail_on_docstring_regression: bool + fail_on_api_break: bool fail_complexity: int fail_coupling: int fail_cohesion: int fail_cycles: bool fail_dead_code: bool fail_health: int + min_typing_coverage: int + min_docstring_coverage: int fail_on_new: bool fail_threshold: int @@ -36,6 +41,21 @@ def _strip_terminal_period(text: str) -> str: return text[:-1] if text.endswith(".") else text +def _parse_two_part_metric_detail( + text: str, + *, + prefix: str, + right_label: str, +) -> str | None: + if not text.startswith(prefix): + return None + left_part, right_part = text[len(prefix) :].split(", ", maxsplit=1) + return ( + f"{left_part.rsplit('=', maxsplit=1)[1]} " + f"({right_label}={right_part.rsplit('=', maxsplit=1)[1]})" + ) + + def parse_metric_reason_entry(reason: str) -> tuple[str, str]: trimmed = _strip_terminal_period(reason) @@ -57,6 +77,19 @@ def tail(prefix: str) -> str: if trimmed.startswith("Health score regressed vs metrics baseline: delta="): return "health_delta", trimmed.rsplit("=", maxsplit=1)[1] + typing_detail = _parse_two_part_metric_detail( + trimmed, + prefix="Typing coverage regressed vs metrics baseline: ", + right_label="returns_delta", + ) + if typing_detail is not None: + return "typing_coverage_delta", typing_detail + if trimmed.startswith("Docstring coverage regressed vs metrics baseline: delta="): + return "docstring_coverage_delta", trimmed.rsplit("=", maxsplit=1)[1] + if trimmed.startswith("Public API breaking changes vs metrics baseline: "): + return "api_breaking_changes", tail( + "Public API breaking changes vs metrics baseline: " + ) if trimmed.startswith("Dependency cycles detected: "): return "dependency_cycles", tail("Dependency cycles detected: ").replace( @@ -73,15 +106,17 @@ def tail(prefix: str) -> str: ("Coupling threshold exceeded: ", "coupling_max"), ("Cohesion threshold exceeded: ", "cohesion_max"), ("Health score below threshold: ", "health_score"), + ("Typing coverage below threshold: ", "typing_coverage"), + ("Docstring coverage below threshold: ", "docstring_coverage"), ) for prefix, kind in threshold_prefixes: - if trimmed.startswith(prefix): - left_part, threshold_part = tail(prefix).split(", ") - return ( - kind, - f"{left_part.rsplit('=', maxsplit=1)[1]} " - f"(threshold={threshold_part.rsplit('=', maxsplit=1)[1]})", - ) + threshold_detail = _parse_two_part_metric_detail( + trimmed, + prefix=prefix, + right_label="threshold", + ) + if threshold_detail is not None: + return kind, threshold_detail return "detail", trimmed @@ -94,26 +129,49 @@ def policy_context(*, args: _GatingArgs, gate_kind: str) -> str: match gate_kind: case "metrics": parts = ( - "fail-on-new-metrics" if args.fail_on_new_metrics else None, - f"fail-complexity={args.fail_complexity}" - if args.fail_complexity >= 0 + "fail-on-new-metrics" + if bool(getattr(args, "fail_on_new_metrics", False)) + else None, + f"fail-complexity={getattr(args, 'fail_complexity', -1)}" + if int(getattr(args, "fail_complexity", -1)) >= 0 else None, - f"fail-coupling={args.fail_coupling}" - if args.fail_coupling >= 0 + f"fail-coupling={getattr(args, 'fail_coupling', -1)}" + if int(getattr(args, "fail_coupling", -1)) >= 0 else None, - f"fail-cohesion={args.fail_cohesion}" - if args.fail_cohesion >= 0 + f"fail-cohesion={getattr(args, 'fail_cohesion', -1)}" + if int(getattr(args, "fail_cohesion", -1)) >= 0 + else None, + "fail-cycles" if bool(getattr(args, "fail_cycles", False)) else None, + "fail-dead-code" + if bool(getattr(args, "fail_dead_code", False)) + else None, + f"fail-health={getattr(args, 'fail_health', -1)}" + if int(getattr(args, "fail_health", -1)) >= 0 + else None, + "fail-on-typing-regression" + if bool(getattr(args, "fail_on_typing_regression", False)) + else None, + "fail-on-docstring-regression" + if bool(getattr(args, "fail_on_docstring_regression", False)) + else None, + "fail-on-api-break" + if bool(getattr(args, "fail_on_api_break", False)) + else None, + f"min-typing-coverage={getattr(args, 'min_typing_coverage', -1)}" + if int(getattr(args, "min_typing_coverage", -1)) >= 0 + else None, + f"min-docstring-coverage={getattr(args, 'min_docstring_coverage', -1)}" + if int(getattr(args, "min_docstring_coverage", -1)) >= 0 else None, - "fail-cycles" if args.fail_cycles else None, - "fail-dead-code" if args.fail_dead_code else None, - f"fail-health={args.fail_health}" if args.fail_health >= 0 else None, ) case "new-clones": - parts = ("fail-on-new" if args.fail_on_new else None,) + parts = ( + "fail-on-new" if bool(getattr(args, "fail_on_new", False)) else None, + ) case "threshold": parts = ( - f"fail-threshold={args.fail_threshold}" - if args.fail_threshold >= 0 + f"fail-threshold={getattr(args, 'fail_threshold', -1)}" + if int(getattr(args, "fail_threshold", -1)) >= 0 else None, ) case _: diff --git a/codeclone/_cli_runtime.py b/codeclone/_cli_runtime.py index 616057b..f459082 100644 --- a/codeclone/_cli_runtime.py +++ b/codeclone/_cli_runtime.py @@ -34,6 +34,14 @@ class _RuntimeArgs(Protocol): fail_cohesion: int fail_health: int fail_on_new_metrics: bool + fail_on_typing_regression: bool + fail_on_docstring_regression: bool + fail_on_api_break: bool + min_typing_coverage: int + min_docstring_coverage: int + typing_coverage: bool + docstring_coverage: bool + api_surface: bool update_metrics_baseline: bool skip_metrics: bool fail_cycles: bool @@ -67,6 +75,10 @@ def validate_numeric_args(args: _RuntimeArgs) -> bool: or args.fail_coupling < -1 or args.fail_cohesion < -1 or args.fail_health < -1 + or args.min_typing_coverage < -1 + or args.min_typing_coverage > 100 + or args.min_docstring_coverage < -1 + or args.min_docstring_coverage > 100 ) ) @@ -80,6 +92,11 @@ def _metrics_flags_requested(args: _RuntimeArgs) -> bool: or args.fail_dead_code or args.fail_health >= 0 or args.fail_on_new_metrics + or args.fail_on_typing_regression + or args.fail_on_docstring_regression + or args.fail_on_api_break + or args.min_typing_coverage >= 0 + or args.min_docstring_coverage >= 0 or args.update_metrics_baseline ) @@ -117,6 +134,8 @@ def configure_metrics_mode( args.skip_dead_code = False if args.fail_cycles: args.skip_dependencies = False + if bool(getattr(args, "fail_on_api_break", False)) or args.update_metrics_baseline: + args.api_surface = True def resolve_cache_path( @@ -155,6 +174,12 @@ def metrics_computed(args: _RuntimeArgs) -> tuple[str, ...]: computed.append("dependencies") if not args.skip_dead_code: computed.append("dead_code") + if bool(getattr(args, "typing_coverage", True)) or bool( + getattr(args, "docstring_coverage", True) + ): + computed.append("coverage_adoption") + if bool(getattr(args, "api_surface", False)): + computed.append("api_surface") return tuple(computed) diff --git a/codeclone/_cli_summary.py b/codeclone/_cli_summary.py index 14de73c..ebc5ff7 100644 --- a/codeclone/_cli_summary.py +++ b/codeclone/_cli_summary.py @@ -30,6 +30,15 @@ class MetricsSnapshot: overloaded_modules_total: int = 0 overloaded_modules_population_status: str = "" overloaded_modules_top_score: float = 0.0 + adoption_param_permille: int | None = None + adoption_return_permille: int | None = None + adoption_docstring_permille: int | None = None + adoption_any_annotation_count: int = 0 + api_surface_enabled: bool = False + api_surface_modules: int = 0 + api_surface_public_symbols: int = 0 + api_surface_added: int = 0 + api_surface_breaking: int = 0 @dataclass(frozen=True, slots=True) @@ -165,6 +174,28 @@ def _print_metrics( suppressed=metrics.suppressed_dead_code_count, ) ) + if ( + metrics.adoption_param_permille is not None + and metrics.adoption_return_permille is not None + and metrics.adoption_docstring_permille is not None + ): + console.print( + ui.fmt_metrics_adoption( + param_permille=metrics.adoption_param_permille, + return_permille=metrics.adoption_return_permille, + docstring_permille=metrics.adoption_docstring_permille, + any_annotation_count=metrics.adoption_any_annotation_count, + ) + ) + if metrics.api_surface_enabled: + console.print( + ui.fmt_metrics_api_surface( + public_symbols=metrics.api_surface_public_symbols, + modules=metrics.api_surface_modules, + added=metrics.api_surface_added, + breaking=metrics.api_surface_breaking, + ) + ) console.print( ui.fmt_metrics_overloaded_modules( candidates=metrics.overloaded_modules_candidates, diff --git a/codeclone/_html_css.py b/codeclone/_html_css.py index 64c7325..6606143 100644 --- a/codeclone/_html_css.py +++ b/codeclone/_html_css.py @@ -650,6 +650,20 @@ padding-left:var(--sp-3);position:relative;line-height:1.5} .overview-summary-list li::before{content:"\\2022";position:absolute;left:0;color:var(--text-muted)} .overview-summary-value{font-size:.85rem;color:var(--text-muted)} +/* Compact stat grid used inside overview-summary-item cards (Adoption & API). */ +.overview-stat-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(84px,1fr)); + gap:var(--sp-3);align-items:end} +.overview-stat{display:flex;flex-direction:column;gap:2px;min-width:0} +.overview-stat-value{font-size:1.35rem;font-weight:700;color:var(--text-primary); + font-variant-numeric:tabular-nums;line-height:1.15} +.overview-stat-label{font-size:.68rem;font-weight:500;color:var(--text-muted); + text-transform:uppercase;letter-spacing:.04em} +.overview-stat-caption{margin-top:var(--sp-3);font-size:.72rem;color:var(--text-muted); + line-height:1.4} +.overview-stat-caption code{font-family:var(--font-mono);font-size:.68rem; + padding:1px 4px;border-radius:var(--radius-sm);background:var(--bg-raised); + color:var(--text-secondary)} +.overview-stat-row + .kpi-detail{margin-top:var(--sp-2)} .overview-fact-list{display:flex;flex-direction:column;gap:var(--sp-2);margin-top:var(--sp-3)} .overview-fact-row{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-3); font-size:.76rem;border-bottom:1px solid color-mix(in srgb,var(--border) 45%,transparent);padding-bottom:6px} diff --git a/codeclone/_html_report/_components.py b/codeclone/_html_report/_components.py index 3a87292..2056552 100644 --- a/codeclone/_html_report/_components.py +++ b/codeclone/_html_report/_components.py @@ -60,6 +60,8 @@ def overview_cluster_header(title: str, subtitle: str | None = None) -> str: "top candidates": ("quality", "summary-icon summary-icon--info"), "more candidates": ("quality", "summary-icon summary-icon--info"), "health profile": ("health-profile", "summary-icon summary-icon--info"), + "adoption coverage": ("coverage-adoption", "summary-icon summary-icon--info"), + "public api surface": ("api-surface", "summary-icon summary-icon--info"), } diff --git a/codeclone/_html_report/_icons.py b/codeclone/_html_report/_icons.py index 64c4cd7..864a8ab 100644 --- a/codeclone/_html_report/_icons.py +++ b/codeclone/_html_report/_icons.py @@ -173,6 +173,16 @@ def _svg_with_class(size: int, sw: str, body: str, *, class_name: str = "") -> s '' '', ), + "coverage-adoption": ( + "2", + '' + '', + ), + "api-surface": ( + "2", + '' + '', + ), } diff --git a/codeclone/_html_report/_sections/_overview.py b/codeclone/_html_report/_sections/_overview.py index fb073d3..fe061ab 100644 --- a/codeclone/_html_report/_sections/_overview.py +++ b/codeclone/_html_report/_sections/_overview.py @@ -423,6 +423,153 @@ def _mb(*pairs: tuple[str, object]) -> str: ) +def _format_permille_pct(value: object) -> str: + return f"{_as_int(value) / 10.0:.1f}%" + + +def _format_permille_delta(value: object) -> str: + delta = _as_int(value) + sign = "+" if delta > 0 else "" + return f"{sign}{delta / 10.0:.1f}pt" + + +def _overview_stat(value: str, label: str) -> str: + return ( + '
' + f'
{_escape_html(value)}
' + f'
{_escape_html(label)}
' + "
" + ) + + +def _overview_stat_row(*stats: tuple[str, str]) -> str: + return ( + '
' + + "".join(_overview_stat(value, label) for value, label in stats) + + "
" + ) + + +def _adoption_card_html(adoption_summary: Mapping[str, object]) -> str: + params_pct = _format_permille_pct(adoption_summary.get("param_permille")) + returns_pct = _format_permille_pct(adoption_summary.get("return_permille")) + docs_pct = _format_permille_pct(adoption_summary.get("docstring_permille")) + stats_html = _overview_stat_row( + (params_pct, "params"), + (returns_pct, "returns"), + (docs_pct, "docstrings"), + ) + + deltas_html = "" + if bool(adoption_summary.get("baseline_diff_available")): + deltas_html = _mb( + ( + "\u0394 params", + _format_permille_delta(adoption_summary.get("param_delta")), + ), + ( + "\u0394 returns", + _format_permille_delta(adoption_summary.get("return_delta")), + ), + ( + "\u0394 docs", + _format_permille_delta(adoption_summary.get("docstring_delta")), + ), + ) + if deltas_html: + deltas_html = f'
{deltas_html}
' + + any_count = _as_int(adoption_summary.get("typing_any_count")) + if any_count > 0: + noun = "symbol" if any_count == 1 else "symbols" + caption_html = ( + '
' + f"{_format_count(any_count)} {noun} typed as Any" + "
" + ) + else: + caption_html = ( + '
' + "No symbols fall back to Any." + "
" + ) + + return stats_html + deltas_html + caption_html + + +def _api_card_html(api_summary: Mapping[str, object]) -> str: + if not bool(api_summary.get("enabled")): + return ( + '
Disabled in this run.
' + '
' + "Enable with --api-surface to track public symbols." + "
" + ) + + symbols = _as_int(api_summary.get("public_symbols")) + modules = _as_int(api_summary.get("modules")) + stats_html = _overview_stat_row( + (_format_count(symbols), "public symbols"), + (_format_count(modules), "modules"), + ) + + chips_html = "" + if bool(api_summary.get("baseline_diff_available")): + breaking = _as_int(api_summary.get("breaking")) + added = _as_int(api_summary.get("added")) + chips_html = _mb(("breaking", breaking), ("added", added)) + if chips_html: + chips_html = f'
{chips_html}
' + + if bool(api_summary.get("strict_types")): + caption_html = ( + '
' + "Strict type check enabled for the public surface." + "
" + ) + else: + caption_html = "" + + return stats_html + chips_html + caption_html + + +def _adoption_and_api_section(ctx: ReportContext) -> str: + metrics_map = _as_mapping(getattr(ctx, "metrics_map", {})) + adoption = _as_mapping(metrics_map.get("coverage_adoption")) + adoption_summary = _as_mapping(adoption.get("summary")) + api_surface = _as_mapping(metrics_map.get("api_surface")) + api_summary = _as_mapping(api_surface.get("summary")) + if not adoption_summary and not api_summary: + return "" + + cards: list[str] = [] + if adoption_summary: + cards.append( + overview_summary_item_html( + label="Adoption coverage", + body_html=_adoption_card_html(adoption_summary), + ) + ) + if api_summary: + cards.append( + overview_summary_item_html( + label="Public API surface", + body_html=_api_card_html(api_summary), + ) + ) + + return ( + '
' + + overview_cluster_header( + "Adoption & API", + "Type/docstring adoption and public API surface are shown as facts, not style pressure.", + ) + + '
' + + "".join(cards) + + "
" + ) + + def _scan_scope_subtitle(ctx: ReportContext) -> str: """Build a subtitle string with scan-scope essentials for the Executive Summary header.""" inventory = _as_mapping(getattr(ctx, "inventory_map", {})) @@ -893,6 +1040,7 @@ def _baselined_detail( + "" + "" + executive + + _adoption_and_api_section(ctx) + _directory_hotspots_section(ctx) + _overloaded_modules_section(ctx) + _analytics_section(ctx) diff --git a/codeclone/baseline.py b/codeclone/baseline.py index 2a116d9..c16c08c 100644 --- a/codeclone/baseline.py +++ b/codeclone/baseline.py @@ -35,7 +35,7 @@ # and narrowed before entering compatibility/integrity checks. BASELINE_GENERATOR = "codeclone" -_BASELINE_SCHEMA_MAX_MINOR_BY_MAJOR = {1: 0, 2: 0} +_BASELINE_SCHEMA_MAX_MINOR_BY_MAJOR = {1: 0, 2: 1} MAX_BASELINE_SIZE_BYTES = 5 * 1024 * 1024 @@ -85,7 +85,7 @@ def coerce_baseline_status( _TOP_LEVEL_REQUIRED_KEYS = {"meta", "clones"} -_TOP_LEVEL_OPTIONAL_KEYS = {"metrics"} +_TOP_LEVEL_OPTIONAL_KEYS = {"metrics", "api_surface"} _TOP_LEVEL_ALLOWED_KEYS = _TOP_LEVEL_REQUIRED_KEYS | _TOP_LEVEL_OPTIONAL_KEYS _META_REQUIRED_KEYS = { "generator", @@ -245,15 +245,24 @@ def save(self) -> None: generator_version=self.generator_version, created_at=self.created_at, ) - preserved_metrics, preserved_metrics_hash = _preserve_embedded_metrics( - self.path - ) + ( + preserved_metrics, + preserved_metrics_hash, + preserved_api_surface, + preserved_api_surface_hash, + ) = _preserve_embedded_metrics(self.path) if preserved_metrics is not None: payload["metrics"] = preserved_metrics if preserved_metrics_hash is not None: meta_obj = payload.get("meta") if isinstance(meta_obj, dict): meta_obj["metrics_payload_sha256"] = preserved_metrics_hash + if preserved_api_surface_hash is not None: + meta_obj["api_surface_payload_sha256"] = ( + preserved_api_surface_hash + ) + if preserved_api_surface is not None: + payload["api_surface"] = preserved_api_surface _atomic_write_json(self.path, payload) meta_obj = payload.get("meta") @@ -499,21 +508,41 @@ def _is_legacy_baseline_payload(payload: dict[str, Any]) -> bool: return "functions" in payload and "blocks" in payload -def _preserve_embedded_metrics(path: Path) -> tuple[dict[str, Any] | None, str | None]: +def _preserve_embedded_metrics( + path: Path, +) -> tuple[dict[str, Any] | None, str | None, dict[str, Any] | None, str | None]: try: payload = _load_json_object(path) except BaselineValidationError: - return None, None + return None, None, None, None metrics_obj = payload.get("metrics") + api_surface_obj = payload.get("api_surface") + preserved_api_surface = ( + dict(api_surface_obj) if isinstance(api_surface_obj, dict) else None + ) if not isinstance(metrics_obj, dict): - return None, None + return None, None, preserved_api_surface, None meta_obj = payload.get("meta") if not isinstance(meta_obj, dict): - return dict(metrics_obj), None + return dict(metrics_obj), None, preserved_api_surface, None metrics_hash = meta_obj.get("metrics_payload_sha256") + api_surface_hash = meta_obj.get("api_surface_payload_sha256") + normalized_api_surface_hash = ( + api_surface_hash if isinstance(api_surface_hash, str) else None + ) if not isinstance(metrics_hash, str): - return dict(metrics_obj), None - return dict(metrics_obj), metrics_hash + return ( + dict(metrics_obj), + None, + preserved_api_surface, + normalized_api_surface_hash, + ) + return ( + dict(metrics_obj), + metrics_hash, + preserved_api_surface, + normalized_api_surface_hash, + ) def _parse_generator_meta( diff --git a/codeclone/cache.py b/codeclone/cache.py index 918b692..f7314e1 100644 --- a/codeclone/cache.py +++ b/codeclone/cache.py @@ -52,7 +52,10 @@ DeadCandidate, FileMetrics, FunctionGroupItem, + ModuleApiSurface, ModuleDep, + ModuleDocstringCoverage, + ModuleTypingCoverage, SegmentGroupItem, SegmentUnit, StructuralFindingGroup, @@ -151,6 +154,48 @@ class DeadCandidateDict(DeadCandidateDictBase, total=False): suppressed_rules: list[str] +class ModuleTypingCoverageDict(TypedDict): + module: str + filepath: str + callable_count: int + params_total: int + params_annotated: int + returns_total: int + returns_annotated: int + any_annotation_count: int + + +class ModuleDocstringCoverageDict(TypedDict): + module: str + filepath: str + public_symbol_total: int + public_symbol_documented: int + + +class ApiParamSpecDict(TypedDict): + name: str + kind: str + has_default: bool + annotation_hash: str + + +class PublicSymbolDict(TypedDict): + qualname: str + kind: str + start_line: int + end_line: int + params: list[ApiParamSpecDict] + returns_hash: str + exported_via: str + + +class ModuleApiSurfaceDict(TypedDict): + module: str + filepath: str + all_declared: list[str] + symbols: list[PublicSymbolDict] + + class StructuralFindingOccurrenceDict(TypedDict): qualname: str start: int @@ -180,6 +225,9 @@ class CacheEntry(CacheEntryBase, total=False): referenced_qualnames: list[str] import_names: list[str] class_names: list[str] + typing_coverage: ModuleTypingCoverageDict + docstring_coverage: ModuleDocstringCoverageDict + api_surface: ModuleApiSurfaceDict structural_findings: list[StructuralFindingGroupDict] @@ -639,47 +687,44 @@ def get_file_entry(self, filepath: str) -> CacheEntry | None: if stat is None or units is None or blocks is None or segments is None: return None - class_metrics_raw = _as_typed_class_metrics_list(entry.get("class_metrics", [])) - module_deps_raw = _as_typed_module_deps_list(entry.get("module_deps", [])) - dead_candidates_raw = _as_typed_dead_candidates_list( - entry.get("dead_candidates", []) - ) - referenced_names_raw = _as_typed_string_list(entry.get("referenced_names", [])) - referenced_qualnames_raw = _as_typed_string_list( - entry.get("referenced_qualnames", []) - ) - import_names_raw = _as_typed_string_list(entry.get("import_names", [])) - class_names_raw = _as_typed_string_list(entry.get("class_names", [])) - if ( - class_metrics_raw is None - or module_deps_raw is None - or dead_candidates_raw is None - or referenced_names_raw is None - or referenced_qualnames_raw is None - or import_names_raw is None - or class_names_raw is None - ): + optional_sections = _decode_optional_cache_sections(entry) + if optional_sections is None: return None - - entry_to_canonicalize: CacheEntry = CacheEntry( - stat=stat, - units=units, - blocks=blocks, - segments=segments, - class_metrics=class_metrics_raw, - module_deps=module_deps_raw, - dead_candidates=dead_candidates_raw, - referenced_names=referenced_names_raw, - referenced_qualnames=referenced_qualnames_raw, - import_names=import_names_raw, - class_names=class_names_raw, + ( + class_metrics_raw, + module_deps_raw, + dead_candidates_raw, + referenced_names_raw, + referenced_qualnames_raw, + import_names_raw, + class_names_raw, + typing_coverage_raw, + docstring_coverage_raw, + api_surface_raw, + source_stats, + structural_findings, + ) = optional_sections + + entry_to_canonicalize: CacheEntry = _attach_optional_cache_sections( + CacheEntry( + stat=stat, + units=units, + blocks=blocks, + segments=segments, + class_metrics=class_metrics_raw, + module_deps=module_deps_raw, + dead_candidates=dead_candidates_raw, + referenced_names=referenced_names_raw, + referenced_qualnames=referenced_qualnames_raw, + import_names=import_names_raw, + class_names=class_names_raw, + ), + typing_coverage=typing_coverage_raw, + docstring_coverage=docstring_coverage_raw, + api_surface=api_surface_raw, + source_stats=source_stats, + structural_findings=structural_findings, ) - source_stats = _as_source_stats_dict(entry.get("source_stats")) - if source_stats is not None: - entry_to_canonicalize["source_stats"] = source_stats - sf_raw = entry.get("structural_findings") - if isinstance(sf_raw, list): - entry_to_canonicalize["structural_findings"] = sf_raw canonical_entry = _canonicalize_cache_entry(entry_to_canonicalize) return self._store_canonical_file_entry( runtime_path=runtime_lookup_key, @@ -717,6 +762,9 @@ def put_file_entry( referenced_qualnames, import_names, class_names, + typing_coverage, + docstring_coverage, + api_surface, ) = _new_optional_metrics_payload() if file_metrics is not None: class_metrics_rows = [ @@ -734,6 +782,18 @@ def put_file_entry( referenced_qualnames = sorted(set(file_metrics.referenced_qualnames)) import_names = sorted(set(file_metrics.import_names)) class_names = sorted(set(file_metrics.class_names)) + typing_coverage = _typing_coverage_dict_from_model( + file_metrics.typing_coverage, + filepath=runtime_path, + ) + docstring_coverage = _docstring_coverage_dict_from_model( + file_metrics.docstring_coverage, + filepath=runtime_path, + ) + api_surface = _api_surface_dict_from_model( + file_metrics.api_surface, + filepath=runtime_path, + ) source_stats_payload = source_stats or SourceStatsDict( lines=0, @@ -755,6 +815,12 @@ def put_file_entry( import_names=import_names, class_names=class_names, ) + if typing_coverage is not None: + entry_dict["typing_coverage"] = typing_coverage + if docstring_coverage is not None: + entry_dict["docstring_coverage"] = docstring_coverage + if api_surface is not None: + entry_dict["api_surface"] = api_surface if structural_findings is not None: entry_dict["structural_findings"] = _normalize_cached_structural_groups( [ @@ -814,8 +880,11 @@ def _new_optional_metrics_payload() -> tuple[ list[str], list[str], list[str], + ModuleTypingCoverageDict | None, + ModuleDocstringCoverageDict | None, + ModuleApiSurfaceDict | None, ]: - return [], [], [], [], [], [], [] + return [], [], [], [], [], [], [], None, None, None def _unit_dict_from_model(unit: Unit, filepath: str) -> UnitDict: @@ -864,6 +933,74 @@ def _segment_dict_from_model(segment: SegmentUnit, filepath: str) -> SegmentDict ) +def _typing_coverage_dict_from_model( + coverage: ModuleTypingCoverage | None, + *, + filepath: str, +) -> ModuleTypingCoverageDict | None: + if coverage is None: + return None + return ModuleTypingCoverageDict( + module=coverage.module, + filepath=filepath, + callable_count=coverage.callable_count, + params_total=coverage.params_total, + params_annotated=coverage.params_annotated, + returns_total=coverage.returns_total, + returns_annotated=coverage.returns_annotated, + any_annotation_count=coverage.any_annotation_count, + ) + + +def _docstring_coverage_dict_from_model( + coverage: ModuleDocstringCoverage | None, + *, + filepath: str, +) -> ModuleDocstringCoverageDict | None: + if coverage is None: + return None + return ModuleDocstringCoverageDict( + module=coverage.module, + filepath=filepath, + public_symbol_total=coverage.public_symbol_total, + public_symbol_documented=coverage.public_symbol_documented, + ) + + +def _api_surface_dict_from_model( + surface: ModuleApiSurface | None, + *, + filepath: str, +) -> ModuleApiSurfaceDict | None: + if surface is None: + return None + return ModuleApiSurfaceDict( + module=surface.module, + filepath=filepath, + all_declared=list(surface.all_declared or ()), + symbols=[ + PublicSymbolDict( + qualname=symbol.qualname, + kind=symbol.kind, + start_line=symbol.start_line, + end_line=symbol.end_line, + params=[ + ApiParamSpecDict( + name=param.name, + kind=param.kind, + has_default=param.has_default, + annotation_hash=param.annotation_hash, + ) + for param in symbol.params + ], + returns_hash=symbol.returns_hash, + exported_via=symbol.exported_via, + ) + for symbol in surface.symbols + ], + ) + + def _class_metrics_dict_from_model( metric: ClassMetrics, filepath: str, @@ -1006,6 +1143,28 @@ def _as_typed_string_list(value: object) -> list[str] | None: return _as_typed_list(value, predicate=lambda item: isinstance(item, str)) +def _as_module_typing_coverage_dict( + value: object, +) -> ModuleTypingCoverageDict | None: + if not _is_module_typing_coverage_dict(value): + return None + return cast("ModuleTypingCoverageDict", value) + + +def _as_module_docstring_coverage_dict( + value: object, +) -> ModuleDocstringCoverageDict | None: + if not _is_module_docstring_coverage_dict(value): + return None + return cast("ModuleDocstringCoverageDict", value) + + +def _as_module_api_surface_dict(value: object) -> ModuleApiSurfaceDict | None: + if not _is_module_api_surface_dict(value): + return None + return cast("ModuleApiSurfaceDict", value) + + def _normalized_optional_string_list(value: object) -> list[str] | None: items = _as_typed_string_list(value) if not items: @@ -1042,7 +1201,108 @@ def _has_cache_entry_container_shape(entry: Mapping[str, object]) -> bool: "class_names", "structural_findings", ) - return all(isinstance(entry.get(key, []), list) for key in optional_list_keys) + if not all(isinstance(entry.get(key, []), list) for key in optional_list_keys): + return False + typing_coverage = entry.get("typing_coverage") + if typing_coverage is not None and not _is_module_typing_coverage_dict( + typing_coverage + ): + return False + docstring_coverage = entry.get("docstring_coverage") + if docstring_coverage is not None and not _is_module_docstring_coverage_dict( + docstring_coverage + ): + return False + api_surface = entry.get("api_surface") + return api_surface is None or _is_module_api_surface_dict(api_surface) + + +def _decode_optional_cache_sections( + entry: Mapping[str, object], +) -> ( + tuple[ + list[ClassMetricsDict], + list[ModuleDepDict], + list[DeadCandidateDict], + list[str], + list[str], + list[str], + list[str], + ModuleTypingCoverageDict | None, + ModuleDocstringCoverageDict | None, + ModuleApiSurfaceDict | None, + SourceStatsDict | None, + list[StructuralFindingGroupDict] | None, + ] + | None +): + class_metrics_raw = _as_typed_class_metrics_list(entry.get("class_metrics", [])) + module_deps_raw = _as_typed_module_deps_list(entry.get("module_deps", [])) + dead_candidates_raw = _as_typed_dead_candidates_list( + entry.get("dead_candidates", []) + ) + referenced_names_raw = _as_typed_string_list(entry.get("referenced_names", [])) + referenced_qualnames_raw = _as_typed_string_list( + entry.get("referenced_qualnames", []) + ) + import_names_raw = _as_typed_string_list(entry.get("import_names", [])) + class_names_raw = _as_typed_string_list(entry.get("class_names", [])) + if ( + class_metrics_raw is None + or module_deps_raw is None + or dead_candidates_raw is None + or referenced_names_raw is None + or referenced_qualnames_raw is None + or import_names_raw is None + or class_names_raw is None + ): + return None + typing_coverage_raw = _as_module_typing_coverage_dict(entry.get("typing_coverage")) + docstring_coverage_raw = _as_module_docstring_coverage_dict( + entry.get("docstring_coverage") + ) + api_surface_raw = _as_module_api_surface_dict(entry.get("api_surface")) + source_stats = _as_source_stats_dict(entry.get("source_stats")) + structural_findings = entry.get("structural_findings") + typed_structural_findings = ( + structural_findings if isinstance(structural_findings, list) else None + ) + return ( + class_metrics_raw, + module_deps_raw, + dead_candidates_raw, + referenced_names_raw, + referenced_qualnames_raw, + import_names_raw, + class_names_raw, + typing_coverage_raw, + docstring_coverage_raw, + api_surface_raw, + source_stats, + typed_structural_findings, + ) + + +def _attach_optional_cache_sections( + entry: CacheEntry, + *, + typing_coverage: ModuleTypingCoverageDict | None = None, + docstring_coverage: ModuleDocstringCoverageDict | None = None, + api_surface: ModuleApiSurfaceDict | None = None, + source_stats: SourceStatsDict | None = None, + structural_findings: list[StructuralFindingGroupDict] | None = None, +) -> CacheEntry: + if typing_coverage is not None: + entry["typing_coverage"] = typing_coverage + if docstring_coverage is not None: + entry["docstring_coverage"] = docstring_coverage + if api_surface is not None: + entry["api_surface"] = api_surface + if source_stats is not None: + entry["source_stats"] = source_stats + if structural_findings is not None: + entry["structural_findings"] = structural_findings + return entry def _canonicalize_cache_entry(entry: CacheEntry) -> CacheEntry: @@ -1110,6 +1370,66 @@ def _canonicalize_cache_entry(entry: CacheEntry) -> CacheEntry: "import_names": sorted(set(entry["import_names"])), "class_names": sorted(set(entry["class_names"])), } + typing_coverage = entry.get("typing_coverage") + if typing_coverage is not None: + result["typing_coverage"] = ModuleTypingCoverageDict( + module=typing_coverage["module"], + filepath=typing_coverage["filepath"], + callable_count=typing_coverage["callable_count"], + params_total=typing_coverage["params_total"], + params_annotated=typing_coverage["params_annotated"], + returns_total=typing_coverage["returns_total"], + returns_annotated=typing_coverage["returns_annotated"], + any_annotation_count=typing_coverage["any_annotation_count"], + ) + docstring_coverage = entry.get("docstring_coverage") + if docstring_coverage is not None: + result["docstring_coverage"] = ModuleDocstringCoverageDict( + module=docstring_coverage["module"], + filepath=docstring_coverage["filepath"], + public_symbol_total=docstring_coverage["public_symbol_total"], + public_symbol_documented=docstring_coverage["public_symbol_documented"], + ) + api_surface = entry.get("api_surface") + if api_surface is not None: + symbols = sorted( + api_surface["symbols"], + key=lambda item: ( + item["qualname"], + item["kind"], + item["start_line"], + item["end_line"], + ), + ) + normalized_symbols = [ + PublicSymbolDict( + qualname=symbol["qualname"], + kind=symbol["kind"], + start_line=symbol["start_line"], + end_line=symbol["end_line"], + params=sorted( + [ + ApiParamSpecDict( + name=param["name"], + kind=param["kind"], + has_default=param["has_default"], + annotation_hash=param["annotation_hash"], + ) + for param in symbol.get("params", []) + ], + key=lambda item: (item["kind"], item["name"]), + ), + returns_hash=symbol.get("returns_hash", ""), + exported_via=symbol.get("exported_via", "name"), + ) + for symbol in symbols + ] + result["api_surface"] = ModuleApiSurfaceDict( + module=api_surface["module"], + filepath=api_surface["filepath"], + all_declared=sorted(set(api_surface.get("all_declared", []))), + symbols=normalized_symbols, + ) sf = entry.get("structural_findings") if sf is not None: result["structural_findings"] = sf @@ -1200,11 +1520,8 @@ def _decode_optional_wire_source_stats( *, obj: dict[str, object], ) -> SourceStatsDict | None: - raw = obj.get("ss") - if raw is None: - return None - row = _as_list(raw) - if row is None or len(row) != 4: + row = _decode_optional_wire_row(obj=obj, key="ss", expected_len=4) + if row is None: return None counts = _decode_wire_int_fields(row, 0, 1, 2, 3) if counts is None: @@ -1263,6 +1580,21 @@ def _decode_optional_wire_items_for_filepath( return decoded_items +def _decode_optional_wire_row( + *, + obj: dict[str, object], + key: str, + expected_len: int, +) -> list[object] | None: + raw = obj.get(key) + if raw is None: + return None + row = _as_list(raw) + if row is None or len(row) != expected_len: + return None + return row + + def _decode_optional_wire_names( *, obj: dict[str, object], @@ -1335,6 +1667,12 @@ def _decode_wire_file_entry(value: object, filepath: str) -> CacheEntry | None: import_names, class_names, ) = name_sections + typing_coverage = _decode_optional_wire_typing_coverage(obj=obj, filepath=filepath) + docstring_coverage = _decode_optional_wire_docstring_coverage( + obj=obj, + filepath=filepath, + ) + api_surface = _decode_optional_wire_api_surface(obj=obj, filepath=filepath) coupled_classes_map = _decode_optional_wire_coupled_classes(obj=obj, key="cc") if coupled_classes_map is None: return None @@ -1349,27 +1687,30 @@ def _decode_wire_file_entry(value: object, filepath: str) -> CacheEntry | None: if structural_findings is None: return None - result = CacheEntry( - stat=stat, - units=units, - blocks=blocks, - segments=segments, - class_metrics=class_metrics, - module_deps=module_deps, - dead_candidates=dead_candidates, - referenced_names=referenced_names, - referenced_qualnames=referenced_qualnames, - import_names=import_names, - class_names=class_names, + return _attach_optional_cache_sections( + CacheEntry( + stat=stat, + units=units, + blocks=blocks, + segments=segments, + class_metrics=class_metrics, + module_deps=module_deps, + dead_candidates=dead_candidates, + referenced_names=referenced_names, + referenced_qualnames=referenced_qualnames, + import_names=import_names, + class_names=class_names, + ), + typing_coverage=typing_coverage, + docstring_coverage=docstring_coverage, + api_surface=api_surface, + source_stats=source_stats, + structural_findings=( + _normalize_cached_structural_groups(structural_findings, filepath=filepath) + if has_structural_findings + else None + ), ) - if source_stats is not None: - result["source_stats"] = source_stats - if has_structural_findings: - result["structural_findings"] = _normalize_cached_structural_groups( - structural_findings, - filepath=filepath, - ) - return result def _decode_wire_file_sections( @@ -1464,6 +1805,157 @@ def _decode_wire_name_sections( ) +def _decode_optional_wire_typing_coverage( + *, + obj: dict[str, object], + filepath: str, +) -> ModuleTypingCoverageDict | None: + module_and_ints = _decode_optional_wire_module_ints( + obj=obj, + key="tc", + expected_len=7, + int_indexes=(1, 2, 3, 4, 5, 6), + ) + if module_and_ints is None: + return None + module, ints = module_and_ints + ( + callable_count, + params_total, + params_annotated, + returns_total, + returns_annotated, + any_annotation_count, + ) = ints + return ModuleTypingCoverageDict( + module=module, + filepath=filepath, + callable_count=callable_count, + params_total=params_total, + params_annotated=params_annotated, + returns_total=returns_total, + returns_annotated=returns_annotated, + any_annotation_count=any_annotation_count, + ) + + +def _decode_optional_wire_docstring_coverage( + *, + obj: dict[str, object], + filepath: str, +) -> ModuleDocstringCoverageDict | None: + module_and_counts = _decode_optional_wire_module_ints( + obj=obj, + key="dg", + expected_len=3, + int_indexes=(1, 2), + ) + if module_and_counts is None: + return None + module, counts = module_and_counts + public_symbol_total, public_symbol_documented = counts + return ModuleDocstringCoverageDict( + module=module, + filepath=filepath, + public_symbol_total=public_symbol_total, + public_symbol_documented=public_symbol_documented, + ) + + +def _decode_optional_wire_api_surface( + *, + obj: dict[str, object], + filepath: str, +) -> ModuleApiSurfaceDict | None: + row = _decode_optional_wire_row(obj=obj, key="as", expected_len=3) + if row is None: + return None + module = _as_str(row[0]) + all_declared = _decode_optional_wire_names(obj={"ad": row[1]}, key="ad") + symbols_raw = _as_list(row[2]) + if module is None or all_declared is None or symbols_raw is None: + return None + symbols: list[PublicSymbolDict] = [] + for symbol_raw in symbols_raw: + decoded_symbol = _decode_wire_api_surface_symbol(symbol_raw) + if decoded_symbol is None: + return None + symbols.append(decoded_symbol) + return ModuleApiSurfaceDict( + module=module, + filepath=filepath, + all_declared=sorted(set(all_declared)), + symbols=symbols, + ) + + +def _decode_optional_wire_module_ints( + *, + obj: dict[str, object], + key: str, + expected_len: int, + int_indexes: tuple[int, ...], +) -> tuple[str, tuple[int, ...]] | None: + row = _decode_optional_wire_row(obj=obj, key=key, expected_len=expected_len) + if row is None: + return None + module = _as_str(row[0]) + ints = _decode_wire_int_fields(row, *int_indexes) + if module is None or ints is None: + return None + return module, ints + + +def _decode_wire_api_surface_symbol( + value: object, +) -> PublicSymbolDict | None: + symbol_row = _decode_wire_row(value, valid_lengths={7}) + if symbol_row is None: + return None + str_fields = _decode_wire_str_fields(symbol_row, 0, 1, 4, 5) + int_fields = _decode_wire_int_fields(symbol_row, 2, 3) + params_raw = _as_list(symbol_row[6]) + if str_fields is None or int_fields is None or params_raw is None: + return None + qualname, kind, exported_via, returns_hash = str_fields + start_line, end_line = int_fields + params: list[ApiParamSpecDict] = [] + for param_raw in params_raw: + decoded_param = _decode_wire_api_param_spec(param_raw) + if decoded_param is None: + return None + params.append(decoded_param) + return PublicSymbolDict( + qualname=qualname, + kind=kind, + start_line=start_line, + end_line=end_line, + params=params, + returns_hash=returns_hash, + exported_via=exported_via, + ) + + +def _decode_wire_api_param_spec( + value: object, +) -> ApiParamSpecDict | None: + param_row = _decode_wire_row(value, valid_lengths={4}) + if param_row is None: + return None + str_fields = _decode_wire_str_fields(param_row, 0, 1, 3) + int_fields = _decode_wire_int_fields(param_row, 2) + if str_fields is None or int_fields is None: + return None + name, param_kind, annotation_hash = str_fields + (has_default_raw,) = int_fields + return ApiParamSpecDict( + name=name, + kind=param_kind, + has_default=bool(has_default_raw), + annotation_hash=annotation_hash, + ) + + def _decode_wire_structural_findings_optional( obj: dict[str, object], ) -> list[StructuralFindingGroupDict] | None: @@ -1623,20 +2115,15 @@ def _decode_wire_class_metric_fields( def _decode_wire_structural_group(value: object) -> StructuralFindingGroupDict | None: - group_row = _as_list(value) - if group_row is None or len(group_row) != 4: + group_row = _decode_wire_row(value, valid_lengths={4}) + if group_row is None: return None - finding_kind = _as_str(group_row[0]) - finding_key = _as_str(group_row[1]) + str_fields = _decode_wire_str_fields(group_row, 0, 1) items_raw = _as_list(group_row[3]) signature = _decode_wire_structural_signature(group_row[2]) - if ( - finding_kind is None - or finding_key is None - or items_raw is None - or signature is None - ): + if str_fields is None or items_raw is None or signature is None: return None + finding_kind, finding_key = str_fields items: list[StructuralFindingOccurrenceDict] = [] for item_raw in items_raw: item = _decode_wire_structural_occurrence(item_raw) @@ -1828,25 +2315,18 @@ def _decode_wire_dead_candidate( row = _decode_wire_row(value, valid_lengths={5, 6}) if row is None: return None - qualname = _as_str(row[0]) - local_name = _as_str(row[1]) - start_line = _as_int(row[2]) - end_line = _as_int(row[3]) - kind = _as_str(row[4]) + str_fields = _decode_wire_str_fields(row, 0, 1, 4) + int_fields = _decode_wire_int_fields(row, 2, 3) suppressed_rules: list[str] | None = [] if len(row) == 6: raw_rules = _as_list(row[5]) if raw_rules is None or not all(isinstance(rule, str) for rule in raw_rules): return None suppressed_rules = sorted({str(rule) for rule in raw_rules if str(rule)}) - if ( - qualname is None - or local_name is None - or start_line is None - or end_line is None - or kind is None - ): + if str_fields is None or int_fields is None: return None + qualname, local_name, kind = str_fields + start_line, end_line = int_fields decoded = DeadCandidateDict( qualname=qualname, local_name=local_name, @@ -2038,6 +2518,50 @@ def _append_coupled_classes_row(metric: ClassMetricsDict) -> None: wire["in"] = sorted(set(entry["import_names"])) if entry["class_names"]: wire["cn"] = sorted(set(entry["class_names"])) + typing_coverage = entry.get("typing_coverage") + if typing_coverage is not None: + wire["tc"] = [ + typing_coverage["module"], + typing_coverage["callable_count"], + typing_coverage["params_total"], + typing_coverage["params_annotated"], + typing_coverage["returns_total"], + typing_coverage["returns_annotated"], + typing_coverage["any_annotation_count"], + ] + docstring_coverage = entry.get("docstring_coverage") + if docstring_coverage is not None: + wire["dg"] = [ + docstring_coverage["module"], + docstring_coverage["public_symbol_total"], + docstring_coverage["public_symbol_documented"], + ] + api_surface = entry.get("api_surface") + if api_surface is not None: + wire["as"] = [ + api_surface["module"], + sorted(set(api_surface.get("all_declared", []))), + [ + [ + symbol["qualname"], + symbol["kind"], + symbol["start_line"], + symbol["end_line"], + symbol.get("exported_via", "name"), + symbol.get("returns_hash", ""), + [ + [ + param["name"], + param["kind"], + 1 if param["has_default"] else 0, + param.get("annotation_hash", ""), + ] + for param in symbol.get("params", []) + ], + ] + for symbol in api_surface["symbols"] + ], + ] if "structural_findings" in entry: sf = entry.get("structural_findings", []) @@ -2133,6 +2657,74 @@ def _is_segment_dict(value: object) -> bool: return _has_typed_fields(value, string_keys=string_keys, int_keys=int_keys) +def _is_module_typing_coverage_dict(value: object) -> bool: + if not isinstance(value, dict): + return False + string_keys = ("module", "filepath") + int_keys = ( + "callable_count", + "params_total", + "params_annotated", + "returns_total", + "returns_annotated", + "any_annotation_count", + ) + return _has_typed_fields(value, string_keys=string_keys, int_keys=int_keys) + + +def _is_module_docstring_coverage_dict(value: object) -> bool: + if not isinstance(value, dict): + return False + string_keys = ("module", "filepath") + int_keys = ("public_symbol_total", "public_symbol_documented") + return _has_typed_fields(value, string_keys=string_keys, int_keys=int_keys) + + +def _is_api_param_spec_dict(value: object) -> bool: + if not isinstance(value, dict): + return False + return ( + isinstance(value.get("name"), str) + and isinstance(value.get("kind"), str) + and isinstance(value.get("has_default"), bool) + and isinstance(value.get("annotation_hash", ""), str) + ) + + +def _is_public_symbol_dict(value: object) -> bool: + if not isinstance(value, dict): + return False + if not _has_typed_fields( + value, + string_keys=("qualname", "kind", "exported_via"), + int_keys=("start_line", "end_line"), + ): + return False + params = value.get("params", []) + return ( + isinstance(value.get("returns_hash", ""), str) + and isinstance( + params, + list, + ) + and all(_is_api_param_spec_dict(item) for item in params) + ) + + +def _is_module_api_surface_dict(value: object) -> bool: + if not isinstance(value, dict): + return False + all_declared = value.get("all_declared", []) + symbols = value.get("symbols", []) + return ( + isinstance(value.get("module"), str) + and isinstance(value.get("filepath"), str) + and _is_string_list(all_declared) + and isinstance(symbols, list) + and all(_is_public_symbol_dict(item) for item in symbols) + ) + + def _is_class_metrics_dict(value: object) -> bool: if not isinstance(value, dict): return False diff --git a/codeclone/cli.py b/codeclone/cli.py index fda7175..13dd206 100644 --- a/codeclone/cli.py +++ b/codeclone/cli.py @@ -139,6 +139,7 @@ MAX_FILE_SIZE = 10 * 1024 * 1024 __all__ = [ "MAX_FILE_SIZE", + "ExitCode", "ProcessingResult", "analyze", "bootstrap", @@ -1196,7 +1197,8 @@ def _prepare_run_inputs() -> tuple[ console.print( ui.fmt_contract_error( "Size limits must be non-negative integers (MB), " - "threshold flags must be >= 0 or -1." + "threshold flags must be >= 0 or -1, and coverage thresholds " + "must be between 0 and 100." ) ) sys.exit(ExitCode.CONTRACT_ERROR) @@ -1343,6 +1345,11 @@ def _prepare_run_inputs() -> tuple[ or args.fail_dead_code or args.fail_health >= 0 or args.fail_on_new_metrics + or args.fail_on_typing_regression + or args.fail_on_docstring_regression + or args.fail_on_api_break + or args.min_typing_coverage >= 0 + or args.min_docstring_coverage >= 0 ) source_read_contract_failure = ( bool(processing_result.source_read_failures) @@ -1469,9 +1476,21 @@ def _prepare_run_inputs() -> tuple[ if analysis_result.project_metrics is not None: pm = analysis_result.project_metrics + metrics_payload_map = _as_mapping(analysis_result.metrics_payload) overloaded_modules_summary = _as_mapping( - _as_mapping(analysis_result.metrics_payload).get("overloaded_modules") - ).get("summary") + _as_mapping(metrics_payload_map.get("overloaded_modules")).get("summary") + ) + adoption_summary = _as_mapping( + _as_mapping(metrics_payload_map.get("coverage_adoption")).get("summary") + ) + api_surface_summary = _as_mapping( + _as_mapping(metrics_payload_map.get("api_surface")).get("summary") + ) + api_surface_diff_available = bool( + metrics_baseline_state.trusted_for_diff + and getattr(metrics_baseline_state.baseline, "api_surface_snapshot", None) + is not None + ) overloaded_modules_summary_map = _as_mapping(overloaded_modules_summary) _print_metrics( console=cast("_PrinterLike", console), @@ -1501,6 +1520,39 @@ def _prepare_run_inputs() -> tuple[ overloaded_modules_top_score=_coerce.as_float( overloaded_modules_summary_map.get("top_score") ), + adoption_param_permille=( + _as_int(adoption_summary.get("param_permille")) + if adoption_summary + else None + ), + adoption_return_permille=( + _as_int(adoption_summary.get("return_permille")) + if adoption_summary + else None + ), + adoption_docstring_permille=( + _as_int(adoption_summary.get("docstring_permille")) + if adoption_summary + else None + ), + adoption_any_annotation_count=_as_int( + adoption_summary.get("typing_any_count") + ), + api_surface_enabled=bool(api_surface_summary.get("enabled")), + api_surface_modules=_as_int(api_surface_summary.get("modules")), + api_surface_public_symbols=_as_int( + api_surface_summary.get("public_symbols") + ), + api_surface_added=( + len(metrics_diff.new_api_symbols) + if metrics_diff is not None and api_surface_diff_available + else 0 + ), + api_surface_breaking=( + len(metrics_diff.new_api_breaking_changes) + if metrics_diff is not None and api_surface_diff_available + else 0 + ), ), ) diff --git a/codeclone/contracts.py b/codeclone/contracts.py index 089166f..f6b98f7 100644 --- a/codeclone/contracts.py +++ b/codeclone/contracts.py @@ -9,12 +9,12 @@ from enum import IntEnum from typing import Final -BASELINE_SCHEMA_VERSION: Final = "2.0" +BASELINE_SCHEMA_VERSION: Final = "2.1" BASELINE_FINGERPRINT_VERSION: Final = "1" CACHE_VERSION: Final = "2.3" -REPORT_SCHEMA_VERSION: Final = "2.4" -METRICS_BASELINE_SCHEMA_VERSION: Final = "1.0" +REPORT_SCHEMA_VERSION: Final = "2.5" +METRICS_BASELINE_SCHEMA_VERSION: Final = "1.2" DEFAULT_COMPLEXITY_THRESHOLD: Final = 20 DEFAULT_COUPLING_THRESHOLD: Final = 10 diff --git a/codeclone/extractor.py b/codeclone/extractor.py index c2b51b7..0969a89 100644 --- a/codeclone/extractor.py +++ b/codeclone/extractor.py @@ -30,6 +30,8 @@ cyclomatic_complexity, risk_level, ) +from .metrics.adoption import collect_module_adoption +from .metrics.api_surface import collect_module_api_surface from .models import ( BlockUnit, ClassMetrics, @@ -926,6 +928,10 @@ def extract_units_and_stats_from_source( segment_min_loc: int = 20, segment_min_stmt: int = 10, collect_structural_findings: bool = True, + collect_typing_coverage: bool = True, + collect_docstring_coverage: bool = True, + collect_api_surface: bool = False, + api_include_private_modules: bool = False, ) -> tuple[ list[Unit], list[BlockUnit], @@ -938,6 +944,8 @@ def extract_units_and_stats_from_source( tree = _parse_with_limits(source, PARSE_TIMEOUT_SECONDS) except SyntaxError as e: raise ParseError(f"Failed to parse {filepath}: {e}") from e + if not isinstance(tree, ast.Module): + raise ParseError(f"Failed to parse {filepath}: expected module AST root") collector = _qualnames.QualnameCollector() collector.visit(tree) @@ -1099,6 +1107,30 @@ def extract_units_and_stats_from_source( ), ) ) + typing_coverage = None + docstring_coverage = None + if collect_typing_coverage or collect_docstring_coverage: + module_typing, module_docstrings = collect_module_adoption( + tree=tree, + module_name=module_name, + filepath=filepath, + collector=collector, + imported_names=import_names, + ) + if collect_typing_coverage: + typing_coverage = module_typing + if collect_docstring_coverage: + docstring_coverage = module_docstrings + api_surface = None + if collect_api_surface: + api_surface = collect_module_api_surface( + tree=tree, + module_name=module_name, + filepath=filepath, + collector=collector, + imported_names=import_names, + include_private_modules=api_include_private_modules, + ) return ( units, @@ -1118,6 +1150,9 @@ def extract_units_and_stats_from_source( import_names=import_names, class_names=class_names, referenced_qualnames=referenced_qualnames, + typing_coverage=typing_coverage, + docstring_coverage=docstring_coverage, + api_surface=api_surface, ), structural_findings, ) diff --git a/codeclone/mcp_server.py b/codeclone/mcp_server.py index f09b376..f577f74 100644 --- a/codeclone/mcp_server.py +++ b/codeclone/mcp_server.py @@ -158,6 +158,7 @@ def analyze_repository( block_min_stmt: int | None = None, segment_min_loc: int | None = None, segment_min_stmt: int | None = None, + api_surface: bool | None = None, complexity_threshold: int | None = None, coupling_threshold: int | None = None, cohesion_threshold: int | None = None, @@ -182,6 +183,7 @@ def analyze_repository( block_min_stmt=block_min_stmt, segment_min_loc=segment_min_loc, segment_min_stmt=segment_min_stmt, + api_surface=api_surface, complexity_threshold=complexity_threshold, coupling_threshold=coupling_threshold, cohesion_threshold=cohesion_threshold, @@ -222,6 +224,7 @@ def analyze_changed_paths( block_min_stmt: int | None = None, segment_min_loc: int | None = None, segment_min_stmt: int | None = None, + api_surface: bool | None = None, complexity_threshold: int | None = None, coupling_threshold: int | None = None, cohesion_threshold: int | None = None, @@ -246,6 +249,7 @@ def analyze_changed_paths( block_min_stmt=block_min_stmt, segment_min_loc=segment_min_loc, segment_min_stmt=segment_min_stmt, + api_surface=api_surface, complexity_threshold=complexity_threshold, coupling_threshold=coupling_threshold, cohesion_threshold=cohesion_threshold, @@ -334,6 +338,11 @@ def evaluate_gates( fail_dead_code: bool = False, fail_health: int = -1, fail_on_new_metrics: bool = False, + fail_on_typing_regression: bool = False, + fail_on_docstring_regression: bool = False, + fail_on_api_break: bool = False, + min_typing_coverage: int = -1, + min_docstring_coverage: int = -1, ) -> dict[str, object]: return service.evaluate_gates( MCPGateRequest( @@ -347,6 +356,11 @@ def evaluate_gates( fail_dead_code=fail_dead_code, fail_health=fail_health, fail_on_new_metrics=fail_on_new_metrics, + fail_on_typing_regression=fail_on_typing_regression, + fail_on_docstring_regression=fail_on_docstring_regression, + fail_on_api_break=fail_on_api_break, + min_typing_coverage=min_typing_coverage, + min_docstring_coverage=min_docstring_coverage, ) ) diff --git a/codeclone/mcp_service.py b/codeclone/mcp_service.py index 46cf8b3..71f1d82 100644 --- a/codeclone/mcp_service.py +++ b/codeclone/mcp_service.py @@ -141,8 +141,10 @@ "complexity", "coupling", "cohesion", + "coverage_adoption", "dependencies", "dead_code", + "api_surface", "god_modules", "overloaded_modules", "health", @@ -181,6 +183,9 @@ "baseline", "max_baseline_size_mb", "metrics_baseline", + "typing_coverage", + "docstring_coverage", + "api_surface", } ) _RESOURCE_SECTION_MAP: Final[dict[str, ReportSection]] = { @@ -291,8 +296,10 @@ "complexity", "coupling", "cohesion", + "coverage_adoption", "dependencies", "dead_code", + "api_surface", "god_modules", "overloaded_modules", "health", @@ -880,6 +887,7 @@ class MCPAnalysisRequest: block_min_stmt: int | None = None segment_min_loc: int | None = None segment_min_stmt: int | None = None + api_surface: bool | None = None complexity_threshold: int | None = None coupling_threshold: int | None = None cohesion_threshold: int | None = None @@ -903,6 +911,11 @@ class MCPGateRequest: fail_dead_code: bool = False fail_health: int = -1 fail_on_new_metrics: bool = False + fail_on_typing_regression: bool = False + fail_on_docstring_regression: bool = False + fail_on_api_break: bool = False + min_typing_coverage: int = -1 + min_docstring_coverage: int = -1 @dataclass(frozen=True, slots=True) @@ -1345,6 +1358,11 @@ def evaluate_gates(self, request: MCPGateRequest) -> dict[str, object]: "fail_dead_code": request.fail_dead_code, "fail_health": request.fail_health, "fail_on_new_metrics": request.fail_on_new_metrics, + "fail_on_typing_regression": request.fail_on_typing_regression, + "fail_on_docstring_regression": request.fail_on_docstring_regression, + "fail_on_api_break": request.fail_on_api_break, + "min_typing_coverage": request.min_typing_coverage, + "min_docstring_coverage": request.min_docstring_coverage, }, } with self._state_lock: @@ -1370,6 +1388,11 @@ def _evaluate_gate_snapshot( fail_dead_code=request.fail_dead_code, fail_health=request.fail_health, fail_on_new_metrics=request.fail_on_new_metrics, + fail_on_typing_regression=request.fail_on_typing_regression, + fail_on_docstring_regression=request.fail_on_docstring_regression, + fail_on_api_break=request.fail_on_api_break, + min_typing_coverage=request.min_typing_coverage, + min_docstring_coverage=request.min_docstring_coverage, ), ) reasons.extend(f"metric:{reason}" for reason in metric_reasons) @@ -3742,6 +3765,14 @@ def _build_args(self, *, root_path: Path, request: MCPAnalysisRequest) -> Namesp fail_dead_code=False, fail_health=-1, fail_on_new_metrics=False, + fail_on_typing_regression=False, + fail_on_docstring_regression=False, + fail_on_api_break=False, + min_typing_coverage=-1, + min_docstring_coverage=-1, + typing_coverage=True, + docstring_coverage=True, + api_surface=False, design_complexity_threshold=DEFAULT_REPORT_DESIGN_COMPLEXITY_THRESHOLD, design_coupling_threshold=DEFAULT_REPORT_DESIGN_COUPLING_THRESHOLD, design_cohesion_threshold=DEFAULT_REPORT_DESIGN_COHESION_THRESHOLD, @@ -3785,7 +3816,7 @@ def _build_args(self, *, root_path: Path, request: MCPAnalysisRequest) -> Namesp if not validate_numeric_args(args): raise MCPServiceContractError( "Numeric analysis settings must be non-negative and thresholds " - "must be >= -1." + "must be >= -1. Coverage thresholds must be between 0 and 100." ) return args @@ -3805,6 +3836,7 @@ def _apply_request_overrides( "block_min_stmt": request.block_min_stmt, "segment_min_loc": request.segment_min_loc, "segment_min_stmt": request.segment_min_stmt, + "api_surface": request.api_surface, "max_baseline_size_mb": request.max_baseline_size_mb, "max_cache_size_mb": request.max_cache_size_mb, "design_complexity_threshold": request.complexity_threshold, @@ -4245,6 +4277,26 @@ def _summary_diff_payload( and self._summary_health_payload(summary).get("available") is not False else None ), + "typing_param_permille_delta": _as_int( + metrics_diff.get("typing_param_permille_delta", 0), + 0, + ), + "typing_return_permille_delta": _as_int( + metrics_diff.get("typing_return_permille_delta", 0), + 0, + ), + "docstring_permille_delta": _as_int( + metrics_diff.get("docstring_permille_delta", 0), + 0, + ), + "api_breaking_changes": _as_int( + metrics_diff.get("api_breaking_changes", 0), + 0, + ), + "new_api_symbols": _as_int( + metrics_diff.get("new_api_symbols", 0), + 0, + ), } def _metrics_detail_payload( @@ -4365,6 +4417,34 @@ def _metrics_diff_payload( "new_cycles": len(new_cycles), "new_dead_code": len(new_dead_code), "health_delta": _as_int(health_delta, 0), + "typing_param_permille_delta": _as_int( + getattr(metrics_diff, "typing_param_permille_delta", 0), + 0, + ), + "typing_return_permille_delta": _as_int( + getattr(metrics_diff, "typing_return_permille_delta", 0), + 0, + ), + "docstring_permille_delta": _as_int( + getattr(metrics_diff, "docstring_permille_delta", 0), + 0, + ), + "api_breaking_changes": len( + tuple( + cast( + Sequence[object], + getattr(metrics_diff, "new_api_breaking_changes", ()), + ) + ) + ), + "new_api_symbols": len( + tuple( + cast( + Sequence[object], + getattr(metrics_diff, "new_api_symbols", ()), + ) + ) + ), } def _dict_list(self, value: object) -> list[dict[str, object]]: diff --git a/codeclone/metrics/_visibility.py b/codeclone/metrics/_visibility.py new file mode 100644 index 0000000..e01c0f9 --- /dev/null +++ b/codeclone/metrics/_visibility.py @@ -0,0 +1,154 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Den Rozhnovskiy + +from __future__ import annotations + +import ast +from dataclasses import dataclass +from typing import TYPE_CHECKING, Literal + +if TYPE_CHECKING: + from collections.abc import Iterable + + from ..qualnames import QualnameCollector + +__all__ = [ + "ModuleVisibility", + "build_module_visibility", + "is_public_method_name", + "is_public_module_name", +] + +_PUBLIC_METHOD_DUNDERS = frozenset( + {"__call__", "__enter__", "__exit__", "__init__", "__iter__"} +) + + +@dataclass(frozen=True, slots=True) +class ModuleVisibility: + module_name: str + exported_names: frozenset[str] + all_declared: tuple[str, ...] | None + is_public_module: bool + + @property + def strict_exports(self) -> bool: + return self.all_declared is not None + + def exported_via(self, name: str) -> Literal["all", "name"] | None: + if name not in self.exported_names: + return None + return "all" if self.strict_exports else "name" + + +def is_public_module_name(module_name: str) -> bool: + return not any(part.startswith("_") for part in module_name.split(".") if part) + + +def is_public_method_name(name: str) -> bool: + return not name.startswith("_") or name in _PUBLIC_METHOD_DUNDERS + + +def build_module_visibility( + *, + tree: ast.Module, + module_name: str, + collector: QualnameCollector, + imported_names: Iterable[str], + include_private_modules: bool = False, +) -> ModuleVisibility: + declared_all = _declared_dunder_all(tree) + public_module = include_private_modules or is_public_module_name(module_name) + top_level_names = _top_level_declared_names(tree=tree, collector=collector) + imported = frozenset(imported_names) + if declared_all is not None: + exported_names = frozenset( + name for name in declared_all if name and name in top_level_names + ) + elif public_module: + exported_names = frozenset( + name + for name in top_level_names + if not name.startswith("_") and name not in imported + ) + else: + exported_names = frozenset() + return ModuleVisibility( + module_name=module_name, + exported_names=exported_names, + all_declared=declared_all, + is_public_module=public_module, + ) + + +def _declared_dunder_all(tree: ast.Module) -> tuple[str, ...] | None: + for node in tree.body: + if not isinstance(node, (ast.Assign, ast.AnnAssign)): + continue + targets: list[ast.Name] = [] + value: ast.AST | None + if isinstance(node, ast.Assign): + targets = [ + target for target in node.targets if isinstance(target, ast.Name) + ] + value = node.value + else: + targets = [node.target] if isinstance(node.target, ast.Name) else [] + value = node.value + if not any(target.id == "__all__" for target in targets): + continue + rows = _literal_string_sequence(value) + if rows is not None: + return tuple(sorted(set(rows))) + return None + + +def _literal_string_sequence(node: ast.AST | None) -> tuple[str, ...] | None: + if not isinstance(node, (ast.List, ast.Tuple)): + return None + values: list[str] = [] + for item in node.elts: + if not isinstance(item, ast.Constant) or not isinstance(item.value, str): + return None + text = item.value.strip() + if text: + values.append(text) + return tuple(values) + + +def _top_level_declared_names( + *, + tree: ast.Module, + collector: QualnameCollector, +) -> frozenset[str]: + names = { + local_name + for local_name, _node in collector.units + if "." not in local_name and local_name + } + names.update( + class_qualname + for class_qualname, _node in collector.class_nodes + if "." not in class_qualname and class_qualname + ) + for node in tree.body: + if isinstance(node, ast.Assign): + for target in node.targets: + names.update(_assigned_names(target)) + elif isinstance(node, ast.AnnAssign): + names.update(_assigned_names(node.target)) + return frozenset(name for name in names if name and name != "__all__") + + +def _assigned_names(node: ast.AST) -> set[str]: + if isinstance(node, ast.Name): + return {node.id} + if isinstance(node, (ast.Tuple, ast.List)): + names: set[str] = set() + for item in node.elts: + names.update(_assigned_names(item)) + return names + return set() diff --git a/codeclone/metrics/adoption.py b/codeclone/metrics/adoption.py new file mode 100644 index 0000000..db28d07 --- /dev/null +++ b/codeclone/metrics/adoption.py @@ -0,0 +1,166 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Den Rozhnovskiy + +from __future__ import annotations + +import ast +from typing import TYPE_CHECKING + +from ..models import ModuleDocstringCoverage, ModuleTypingCoverage +from ._visibility import ( + ModuleVisibility, + build_module_visibility, + is_public_method_name, +) + +if TYPE_CHECKING: + from ..qualnames import FunctionNode, QualnameCollector + +__all__ = ["collect_module_adoption"] + + +def collect_module_adoption( + *, + tree: ast.Module, + module_name: str, + filepath: str, + collector: QualnameCollector, + imported_names: frozenset[str], +) -> tuple[ModuleTypingCoverage, ModuleDocstringCoverage]: + visibility = build_module_visibility( + tree=tree, + module_name=module_name, + collector=collector, + imported_names=imported_names, + ) + ( + public_symbol_total, + public_symbol_documented, + callable_count, + params_total, + params_annotated, + returns_total, + returns_annotated, + any_annotation_count, + ) = (0, 0, 0, 0, 0, 0, 0, 0) + + public_classes = { + class_qualname + for class_qualname, class_node in collector.class_nodes + if "." not in class_qualname + and visibility.exported_via(class_node.name) is not None + } + + for local_name, node in collector.units: + callable_count += 1 + param_rows = _function_param_rows(node=node, is_method="." in local_name) + params_total += len(param_rows) + params_annotated += sum( + 1 for _name, annotation in param_rows if annotation is not None + ) + returns_total += 1 + returns_annotated += 1 if node.returns is not None else 0 + any_annotation_count += sum( + 1 for _name, annotation in param_rows if _is_any_annotation(annotation) + ) + any_annotation_count += 1 if _is_any_annotation(node.returns) else 0 + + if _is_public_docstring_target( + local_name=local_name, + node=node, + visibility=visibility, + public_classes=public_classes, + ): + public_symbol_total += 1 + if ast.get_docstring(node, clean=False) is not None: + public_symbol_documented += 1 + + for class_qualname, class_node in collector.class_nodes: + if "." in class_qualname or visibility.exported_via(class_node.name) is None: + continue + public_symbol_total += 1 + if ast.get_docstring(class_node, clean=False) is not None: + public_symbol_documented += 1 + + return ( + ModuleTypingCoverage( + module=module_name, + filepath=filepath, + callable_count=callable_count, + params_total=params_total, + params_annotated=params_annotated, + returns_total=returns_total, + returns_annotated=returns_annotated, + any_annotation_count=any_annotation_count, + ), + ModuleDocstringCoverage( + module=module_name, + filepath=filepath, + public_symbol_total=public_symbol_total, + public_symbol_documented=public_symbol_documented, + ), + ) + + +def _function_param_rows( + *, + node: FunctionNode, + is_method: bool, +) -> tuple[tuple[str, ast.AST | None], ...]: + args = node.args + pos_args = [*args.posonlyargs, *args.args] + rows: list[tuple[str, ast.AST | None]] = [] + + for index, arg in enumerate(pos_args): + if is_method and index == 0 and arg.arg in {"self", "cls"}: + continue + rows.append((arg.arg, arg.annotation)) + if args.vararg is not None: + rows.append((args.vararg.arg, args.vararg.annotation)) + rows.extend((arg.arg, arg.annotation) for arg in args.kwonlyargs) + if args.kwarg is not None: + rows.append((args.kwarg.arg, args.kwarg.annotation)) + return tuple(rows) + + +def _is_public_docstring_target( + *, + local_name: str, + node: FunctionNode, + visibility: ModuleVisibility, + public_classes: set[str], +) -> bool: + if "." not in local_name: + return visibility.exported_via(node.name) is not None + class_name, _, method_name = local_name.partition(".") + return class_name in public_classes and is_public_method_name(method_name) + + +def _is_any_annotation(node: ast.AST | None) -> bool: + if node is None: + return False + if isinstance(node, ast.Name): + return node.id == "Any" + if isinstance(node, ast.Attribute): + return _attribute_name(node) in {"typing.Any", "typing_extensions.Any"} + if isinstance(node, ast.Subscript): + return _is_any_annotation(node.value) or _is_any_annotation(node.slice) + if isinstance(node, ast.Tuple): + return any(_is_any_annotation(item) for item in node.elts) + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr): + return _is_any_annotation(node.left) or _is_any_annotation(node.right) + return False + + +def _attribute_name(node: ast.AST) -> str | None: + if isinstance(node, ast.Name): + return node.id + if isinstance(node, ast.Attribute): + prefix = _attribute_name(node.value) + if prefix is None: + return None + return f"{prefix}.{node.attr}" + return None diff --git a/codeclone/metrics/api_surface.py b/codeclone/metrics/api_surface.py new file mode 100644 index 0000000..53bd96e --- /dev/null +++ b/codeclone/metrics/api_surface.py @@ -0,0 +1,421 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Den Rozhnovskiy + +from __future__ import annotations + +import ast +from typing import TYPE_CHECKING, Literal + +from ..models import ( + ApiBreakingChange, + ApiParamSpec, + ApiSurfaceSnapshot, + ModuleApiSurface, + PublicSymbol, +) +from ._visibility import ( + ModuleVisibility, + build_module_visibility, + is_public_method_name, +) + +if TYPE_CHECKING: + from ..qualnames import FunctionNode, QualnameCollector + +__all__ = [ + "collect_module_api_surface", + "compare_api_surfaces", +] + + +def collect_module_api_surface( + *, + tree: ast.Module, + module_name: str, + filepath: str, + collector: QualnameCollector, + imported_names: frozenset[str], + include_private_modules: bool = False, +) -> ModuleApiSurface | None: + visibility = build_module_visibility( + tree=tree, + module_name=module_name, + collector=collector, + imported_names=imported_names, + include_private_modules=include_private_modules, + ) + if not visibility.is_public_module and not visibility.exported_names: + return None + + symbols: list[PublicSymbol] = [] + public_classes = { + class_qualname + for class_qualname, class_node in collector.class_nodes + if "." not in class_qualname + and visibility.exported_via(class_node.name) is not None + } + + for local_name, node in collector.units: + symbol = _callable_api_symbol( + module_name=module_name, + local_name=local_name, + node=node, + visibility=visibility, + public_classes=public_classes, + ) + if symbol is not None: + symbols.append(symbol) + for class_qualname, class_node in collector.class_nodes: + symbol = _class_api_symbol( + module_name=module_name, + class_qualname=class_qualname, + class_node=class_node, + visibility=visibility, + ) + if symbol is not None: + symbols.append(symbol) + + for constant_name, start_line, end_line in _public_constant_rows( + tree=tree, + visibility=visibility, + ): + symbol = _constant_api_symbol( + module_name=module_name, + constant_name=constant_name, + start_line=start_line, + end_line=end_line, + visibility=visibility, + ) + if symbol is not None: + symbols.append(symbol) + + if not symbols: + return None + return ModuleApiSurface( + module=module_name, + filepath=filepath, + symbols=tuple(sorted(symbols, key=lambda item: item.qualname)), + all_declared=visibility.all_declared, + ) + + +def _callable_api_symbol( + *, + module_name: str, + local_name: str, + node: FunctionNode, + visibility: ModuleVisibility, + public_classes: set[str], +) -> PublicSymbol | None: + start_line = int(getattr(node, "lineno", 0)) + end_line = int(getattr(node, "end_lineno", 0)) + returns_hash = _annotation_hash(node.returns) + if "." not in local_name: + return _build_public_symbol( + module_name=module_name, + export_name=node.name, + local_name=local_name, + kind="function", + start_line=start_line, + end_line=end_line, + params=_parameter_specs(node=node, is_method=False), + returns_hash=returns_hash, + visibility=visibility, + ) + class_name, _, method_name = local_name.partition(".") + if class_name not in public_classes or not is_public_method_name(method_name): + return None + return _build_public_symbol( + module_name=module_name, + export_name=class_name, + local_name=local_name, + kind="method", + start_line=start_line, + end_line=end_line, + params=_parameter_specs(node=node, is_method=True), + returns_hash=returns_hash, + visibility=visibility, + ) + + +def _class_api_symbol( + *, + module_name: str, + class_qualname: str, + class_node: ast.ClassDef, + visibility: ModuleVisibility, +) -> PublicSymbol | None: + if "." in class_qualname: + return None + return _build_public_symbol( + module_name=module_name, + export_name=class_node.name, + local_name=class_qualname, + kind="class", + start_line=int(getattr(class_node, "lineno", 0)), + end_line=int(getattr(class_node, "end_lineno", 0)), + visibility=visibility, + ) + + +def _constant_api_symbol( + *, + module_name: str, + constant_name: str, + start_line: int, + end_line: int, + visibility: ModuleVisibility, +) -> PublicSymbol | None: + return _build_public_symbol( + module_name=module_name, + export_name=constant_name, + local_name=constant_name, + kind="constant", + start_line=start_line, + end_line=end_line, + visibility=visibility, + ) + + +def _build_public_symbol( + *, + module_name: str, + export_name: str, + local_name: str, + kind: Literal["function", "class", "method", "constant"], + start_line: int, + end_line: int, + visibility: ModuleVisibility, + params: tuple[ApiParamSpec, ...] = (), + returns_hash: str = "", +) -> PublicSymbol | None: + exported_via = visibility.exported_via(export_name) + if exported_via is None: + return None + return PublicSymbol( + qualname=f"{module_name}:{local_name}", + kind=kind, + start_line=start_line, + end_line=end_line, + params=params, + returns_hash=returns_hash, + exported_via=exported_via, + ) + + +def compare_api_surfaces( + *, + baseline: ApiSurfaceSnapshot | None, + current: ApiSurfaceSnapshot | None, + strict_types: bool, +) -> tuple[tuple[str, ...], tuple[ApiBreakingChange, ...]]: + baseline_symbols = _symbol_index(baseline) + current_symbols = _symbol_index(current) + added = tuple(sorted(set(current_symbols) - set(baseline_symbols))) + breaking_changes: list[ApiBreakingChange] = [] + + for qualname in sorted(baseline_symbols): + baseline_symbol = baseline_symbols[qualname] + current_symbol = current_symbols.get(qualname) + if current_symbol is None: + breaking_changes.append( + ApiBreakingChange( + qualname=qualname, + filepath=baseline_symbol[1].filepath, + start_line=baseline_symbol[0].start_line, + end_line=baseline_symbol[0].end_line, + symbol_kind=baseline_symbol[0].kind, + change_kind="removed", + detail="Removed from the public API surface.", + ) + ) + continue + detail = _signature_break_detail( + baseline_symbol=baseline_symbol[0], + current_symbol=current_symbol[0], + strict_types=strict_types, + ) + if detail is None: + continue + breaking_changes.append( + ApiBreakingChange( + qualname=qualname, + filepath=current_symbol[1].filepath, + start_line=current_symbol[0].start_line, + end_line=current_symbol[0].end_line, + symbol_kind=current_symbol[0].kind, + change_kind="signature_break", + detail=detail, + ) + ) + + return added, tuple( + sorted( + breaking_changes, + key=lambda item: ( + item.filepath, + item.start_line, + item.end_line, + item.qualname, + item.change_kind, + ), + ) + ) + + +def _symbol_index( + snapshot: ApiSurfaceSnapshot | None, +) -> dict[str, tuple[PublicSymbol, ModuleApiSurface]]: + if snapshot is None: + return {} + return { + symbol.qualname: (symbol, module) + for module in snapshot.modules + for symbol in module.symbols + } + + +def _parameter_specs( + *, + node: FunctionNode, + is_method: bool, +) -> tuple[ApiParamSpec, ...]: + args = node.args + rows: list[ApiParamSpec] = [] + positional = [*args.posonlyargs, *args.args] + posonly_count = len(args.posonlyargs) + defaults_offset = len(positional) - len(args.defaults) + for index, arg in enumerate(positional): + if _is_implicit_method_receiver( + is_method=is_method, + index=index, + name=arg.arg, + ): + continue + rows.append( + ApiParamSpec( + name=arg.arg, + kind="pos_only" if index < posonly_count else "pos_or_kw", + has_default=index >= defaults_offset, + annotation_hash=_annotation_hash(arg.annotation), + ) + ) + if args.vararg is not None: + rows.append( + ApiParamSpec( + name=args.vararg.arg, + kind="vararg", + has_default=False, + annotation_hash=_annotation_hash(args.vararg.annotation), + ) + ) + for arg, default in zip(args.kwonlyargs, args.kw_defaults, strict=True): + rows.append( + ApiParamSpec( + name=arg.arg, + kind="kw_only", + has_default=default is not None, + annotation_hash=_annotation_hash(arg.annotation), + ) + ) + if args.kwarg is not None: + rows.append( + ApiParamSpec( + name=args.kwarg.arg, + kind="kwarg", + has_default=False, + annotation_hash=_annotation_hash(args.kwarg.annotation), + ) + ) + return tuple(rows) + + +def _is_implicit_method_receiver(*, is_method: bool, index: int, name: str) -> bool: + return is_method and index == 0 and name in {"self", "cls"} + + +def _annotation_hash(node: ast.AST | None) -> str: + if node is None: + return "" + return ast.dump(node, include_attributes=False) + + +def _public_constant_rows( + *, + tree: ast.Module, + visibility: ModuleVisibility, +) -> tuple[tuple[str, int, int], ...]: + rows: list[tuple[str, int, int]] = [] + for node in tree.body: + if isinstance(node, ast.Assign): + rows.extend( + ( + target.id, + int(getattr(node, "lineno", 0)), + int(getattr(node, "end_lineno", 0)), + ) + for target in node.targets + if isinstance(target, ast.Name) + and visibility.exported_via(target.id) is not None + ) + elif isinstance(node, ast.AnnAssign): + target = node.target + if ( + isinstance(target, ast.Name) + and visibility.exported_via(target.id) is not None + ): + rows.append( + ( + target.id, + int(getattr(node, "lineno", 0)), + int(getattr(node, "end_lineno", 0)), + ) + ) + return tuple(sorted(set(rows))) + + +def _signature_break_detail( + *, + baseline_symbol: PublicSymbol, + current_symbol: PublicSymbol, + strict_types: bool, +) -> str | None: + if baseline_symbol.kind != current_symbol.kind: + return ( + "Changed public symbol kind from " + f"{baseline_symbol.kind} to {current_symbol.kind}." + ) + if baseline_symbol.kind not in {"function", "method"}: + return None + baseline_params = baseline_symbol.params + current_params = current_symbol.params + if len(current_params) != len(baseline_params): + return "Changed callable parameter count." + for baseline_param, current_param in zip( + baseline_params, current_params, strict=True + ): + if baseline_param.kind != current_param.kind: + return ( + f"Changed parameter kind for {baseline_param.name} " + f"from {baseline_param.kind} to {current_param.kind}." + ) + if ( + baseline_param.kind != "pos_only" + and baseline_param.name != current_param.name + ): + return ( + f"Renamed public parameter {baseline_param.name} " + f"to {current_param.name}." + ) + if baseline_param.has_default and not current_param.has_default: + return f"Parameter {baseline_param.name} became required." + if strict_types and ( + baseline_param.annotation_hash != current_param.annotation_hash + ): + return f"Changed type annotation for parameter {baseline_param.name}." + if strict_types and baseline_symbol.returns_hash != current_symbol.returns_hash: + return "Changed return annotation." + return None diff --git a/codeclone/metrics_baseline.py b/codeclone/metrics_baseline.py index d5b5ceb..22f18ce 100644 --- a/codeclone/metrics_baseline.py +++ b/codeclone/metrics_baseline.py @@ -21,9 +21,19 @@ from ._json_io import write_json_document_atomically as _write_json_document_atomically from ._schema_validation import validate_top_level_structure from .baseline import current_python_tag +from .cache_paths import runtime_filepath_from_wire, wire_filepath_from_runtime from .contracts import BASELINE_SCHEMA_VERSION, METRICS_BASELINE_SCHEMA_VERSION from .errors import BaselineValidationError -from .models import MetricsDiff, MetricsSnapshot, ProjectMetrics +from .metrics.api_surface import compare_api_surfaces +from .models import ( + ApiParamSpec, + ApiSurfaceSnapshot, + MetricsDiff, + MetricsSnapshot, + ModuleApiSurface, + ProjectMetrics, + PublicSymbol, +) if TYPE_CHECKING: from collections.abc import Mapping @@ -64,7 +74,9 @@ class MetricsBaselineStatus(str, Enum): ) _TOP_LEVEL_REQUIRED_KEYS = frozenset({"meta", "metrics"}) -_TOP_LEVEL_ALLOWED_KEYS = _TOP_LEVEL_REQUIRED_KEYS | frozenset({"clones"}) +_TOP_LEVEL_ALLOWED_KEYS = _TOP_LEVEL_REQUIRED_KEYS | frozenset( + {"clones", "api_surface"} +) _META_REQUIRED_KEYS = frozenset( {"generator", "schema_version", "python_tag", "created_at", "payload_sha256"} ) @@ -83,7 +95,16 @@ class MetricsBaselineStatus(str, Enum): "health_grade", } ) +_METRICS_OPTIONAL_KEYS = frozenset( + { + "typing_param_permille", + "typing_return_permille", + "docstring_permille", + "typing_any_count", + } +) _METRICS_PAYLOAD_SHA256_KEY = "metrics_payload_sha256" +_API_SURFACE_PAYLOAD_SHA256_KEY = "api_surface_payload_sha256" def coerce_metrics_baseline_status( @@ -116,15 +137,38 @@ def snapshot_from_project_metrics(project_metrics: ProjectMetrics) -> MetricsSna ), health_score=int(project_metrics.health.total), health_grade=project_metrics.health.grade, + typing_param_permille=_permille( + project_metrics.typing_param_annotated, + project_metrics.typing_param_total, + ), + typing_return_permille=_permille( + project_metrics.typing_return_annotated, + project_metrics.typing_return_total, + ), + docstring_permille=_permille( + project_metrics.docstring_public_documented, + project_metrics.docstring_public_total, + ), + typing_any_count=int(project_metrics.typing_any_count), ) +def _permille(numerator: int, denominator: int) -> int: + if denominator <= 0: + return 0 + return round((1000.0 * float(numerator)) / float(denominator)) + + def _canonical_json(payload: object) -> str: return orjson.dumps(payload, option=orjson.OPT_SORT_KEYS).decode("utf-8") -def _snapshot_payload(snapshot: MetricsSnapshot) -> dict[str, object]: - return { +def _snapshot_payload( + snapshot: MetricsSnapshot, + *, + include_adoption: bool = True, +) -> dict[str, object]: + payload: dict[str, object] = { "max_complexity": int(snapshot.max_complexity), "high_risk_functions": list(snapshot.high_risk_functions), "max_coupling": int(snapshot.max_coupling), @@ -137,10 +181,26 @@ def _snapshot_payload(snapshot: MetricsSnapshot) -> dict[str, object]: "health_score": int(snapshot.health_score), "health_grade": snapshot.health_grade, } + if include_adoption: + payload.update( + { + "typing_param_permille": int(snapshot.typing_param_permille), + "typing_return_permille": int(snapshot.typing_return_permille), + "docstring_permille": int(snapshot.docstring_permille), + "typing_any_count": int(snapshot.typing_any_count), + } + ) + return payload -def _compute_payload_sha256(snapshot: MetricsSnapshot) -> str: - canonical = _canonical_json(_snapshot_payload(snapshot)) +def _compute_payload_sha256( + snapshot: MetricsSnapshot, + *, + include_adoption: bool = True, +) -> str: + canonical = _canonical_json( + _snapshot_payload(snapshot, include_adoption=include_adoption) + ) return hashlib.sha256(canonical.encode("utf-8")).hexdigest() @@ -158,9 +218,12 @@ def _now_utc_z() -> str: class MetricsBaseline: __slots__ = ( + "api_surface_payload_sha256", + "api_surface_snapshot", "created_at", "generator_name", "generator_version", + "has_coverage_adoption_snapshot", "is_embedded_in_clone_baseline", "path", "payload_sha256", @@ -178,6 +241,9 @@ def __init__(self, path: str | Path) -> None: self.created_at: str | None = None self.payload_sha256: str | None = None self.snapshot: MetricsSnapshot | None = None + self.has_coverage_adoption_snapshot = False + self.api_surface_payload_sha256: str | None = None + self.api_surface_snapshot: ApiSurfaceSnapshot | None = None self.is_embedded_in_clone_baseline = False def load( @@ -244,13 +310,21 @@ def load( _validate_required_keys(meta_obj, _META_REQUIRED_KEYS, path=self.path) _validate_required_keys(metrics_obj, _METRICS_REQUIRED_KEYS, path=self.path) - _validate_exact_keys(metrics_obj, _METRICS_REQUIRED_KEYS, path=self.path) + _validate_exact_keys( + metrics_obj, + _METRICS_REQUIRED_KEYS | _METRICS_OPTIONAL_KEYS, + path=self.path, + ) generator_name, generator_version = _parse_generator(meta_obj, path=self.path) schema_version = _require_str(meta_obj, "schema_version", path=self.path) python_tag = _require_str(meta_obj, "python_tag", path=self.path) created_at = _require_str(meta_obj, "created_at", path=self.path) payload_sha256 = _extract_metrics_payload_sha256(meta_obj, path=self.path) + api_surface_payload_sha256 = _extract_optional_payload_sha256( + meta_obj, + key=_API_SURFACE_PAYLOAD_SHA256_KEY, + ) self.generator_name = generator_name self.generator_version = generator_version @@ -258,7 +332,16 @@ def load( self.python_tag = python_tag self.created_at = created_at self.payload_sha256 = payload_sha256 + self.api_surface_payload_sha256 = api_surface_payload_sha256 self.snapshot = _parse_snapshot(metrics_obj, path=self.path) + self.has_coverage_adoption_snapshot = _has_coverage_adoption_snapshot( + metrics_obj, + ) + self.api_surface_snapshot = _parse_api_surface_snapshot( + payload.get("api_surface"), + path=self.path, + root=self.path.parent, + ) def save(self) -> None: if self.snapshot is None: @@ -273,6 +356,8 @@ def save(self) -> None: generator_name=self.generator_name or METRICS_BASELINE_GENERATOR, generator_version=self.generator_version or __version__, created_at=self.created_at or _now_utc_z(), + api_surface_snapshot=self.api_surface_snapshot, + api_surface_root=self.path.parent, ) payload_meta = cast("Mapping[str, Any]", payload["meta"]) payload_metrics_hash = _require_str( @@ -280,6 +365,11 @@ def save(self) -> None: "payload_sha256", path=self.path, ) + payload_api_surface_hash = _optional_require_str( + payload_meta, + _API_SURFACE_PAYLOAD_SHA256_KEY, + path=self.path, + ) existing: dict[str, Any] | None = None try: if self.path.exists(): @@ -302,11 +392,18 @@ def save(self) -> None: merged_meta = dict(existing_meta) merged_meta["schema_version"] = merged_schema_version merged_meta[_METRICS_PAYLOAD_SHA256_KEY] = payload_metrics_hash + if payload_api_surface_hash is None: + merged_meta.pop(_API_SURFACE_PAYLOAD_SHA256_KEY, None) + else: + merged_meta[_API_SURFACE_PAYLOAD_SHA256_KEY] = payload_api_surface_hash merged_payload: dict[str, object] = { "meta": merged_meta, "clones": clones_obj, "metrics": payload["metrics"], } + api_surface_payload = payload.get("api_surface") + if api_surface_payload is not None: + merged_payload["api_surface"] = api_surface_payload self.path.parent.mkdir(parents=True, exist_ok=True) _atomic_write_json(self.path, merged_payload) self.is_embedded_in_clone_baseline = True @@ -316,6 +413,12 @@ def save(self) -> None: self.payload_sha256 = _require_str( merged_meta, _METRICS_PAYLOAD_SHA256_KEY, path=self.path ) + self.has_coverage_adoption_snapshot = True + self.api_surface_payload_sha256 = _optional_require_str( + merged_meta, + _API_SURFACE_PAYLOAD_SHA256_KEY, + path=self.path, + ) self.generator_name, self.generator_version = _parse_generator( merged_meta, path=self.path ) @@ -330,6 +433,8 @@ def save(self) -> None: self.python_tag = _require_str(payload_meta, "python_tag", path=self.path) self.created_at = _require_str(payload_meta, "created_at", path=self.path) self.payload_sha256 = payload_metrics_hash + self.has_coverage_adoption_snapshot = True + self.api_surface_payload_sha256 = payload_api_surface_hash def verify_compatibility(self, *, runtime_python_tag: str) -> None: if self.generator_name != METRICS_BASELINE_GENERATOR: @@ -342,7 +447,10 @@ def verify_compatibility(self, *, runtime_python_tag: str) -> None: if self.is_embedded_in_clone_baseline else METRICS_BASELINE_SCHEMA_VERSION ) - if self.schema_version != expected_schema: + if not _is_compatible_metrics_schema( + baseline_version=self.schema_version, + expected_version=expected_schema, + ): raise BaselineValidationError( "Metrics baseline schema version mismatch: " f"baseline={self.schema_version}, " @@ -373,12 +481,58 @@ def verify_integrity(self) -> None: "Metrics baseline integrity payload hash is missing.", status=MetricsBaselineStatus.INTEGRITY_MISSING, ) - expected = _compute_payload_sha256(self.snapshot) + expected = _compute_payload_sha256( + self.snapshot, + include_adoption=self.has_coverage_adoption_snapshot, + ) if not hmac.compare_digest(self.payload_sha256, expected): raise BaselineValidationError( "Metrics baseline integrity check failed: payload_sha256 mismatch.", status=MetricsBaselineStatus.INTEGRITY_FAILED, ) + if self.api_surface_snapshot is not None: + if ( + not isinstance(self.api_surface_payload_sha256, str) + or len(self.api_surface_payload_sha256) != 64 + ): + raise BaselineValidationError( + "Metrics baseline API surface integrity payload hash is missing.", + status=MetricsBaselineStatus.INTEGRITY_MISSING, + ) + expected_api = _compute_api_surface_payload_sha256( + self.api_surface_snapshot, + root=self.path.parent, + ) + legacy_absolute_expected_api = _compute_api_surface_payload_sha256( + self.api_surface_snapshot + ) + legacy_expected_api = _compute_legacy_api_surface_payload_sha256( + self.api_surface_snapshot, + root=self.path.parent, + ) + legacy_absolute_qualname_expected_api = ( + _compute_legacy_api_surface_payload_sha256(self.api_surface_snapshot) + ) + if not ( + hmac.compare_digest(self.api_surface_payload_sha256, expected_api) + or hmac.compare_digest( + self.api_surface_payload_sha256, + legacy_absolute_expected_api, + ) + or hmac.compare_digest( + self.api_surface_payload_sha256, + legacy_expected_api, + ) + or hmac.compare_digest( + self.api_surface_payload_sha256, + legacy_absolute_qualname_expected_api, + ) + ): + raise BaselineValidationError( + "Metrics baseline integrity check failed: " + "api_surface payload_sha256 mismatch.", + status=MetricsBaselineStatus.INTEGRITY_FAILED, + ) @staticmethod def from_project_metrics( @@ -397,6 +551,16 @@ def from_project_metrics( baseline.created_at = _now_utc_z() baseline.snapshot = snapshot_from_project_metrics(project_metrics) baseline.payload_sha256 = _compute_payload_sha256(baseline.snapshot) + baseline.has_coverage_adoption_snapshot = True + baseline.api_surface_snapshot = project_metrics.api_surface + baseline.api_surface_payload_sha256 = ( + _compute_api_surface_payload_sha256( + project_metrics.api_surface, + root=baseline.path.parent, + ) + if project_metrics.api_surface is not None + else None + ) return baseline def diff(self, current: ProjectMetrics) -> MetricsDiff: @@ -413,6 +577,10 @@ def diff(self, current: ProjectMetrics) -> MetricsDiff: dead_code_items=(), health_score=0, health_grade="F", + typing_param_permille=0, + typing_return_permille=0, + docstring_permille=0, + typing_any_count=0, ) else: snapshot = self.snapshot @@ -442,6 +610,11 @@ def diff(self, current: ProjectMetrics) -> MetricsDiff: set(current_snapshot.dead_code_items) - set(snapshot.dead_code_items) ) ) + added_api_symbols, api_breaking_changes = compare_api_surfaces( + baseline=self.api_surface_snapshot, + current=current.api_surface, + strict_types=False, + ) return MetricsDiff( new_high_risk_functions=new_high_risk_functions, @@ -449,7 +622,53 @@ def diff(self, current: ProjectMetrics) -> MetricsDiff: new_cycles=new_cycles, new_dead_code=new_dead_code, health_delta=current_snapshot.health_score - snapshot.health_score, + typing_param_permille_delta=( + current_snapshot.typing_param_permille - snapshot.typing_param_permille + ), + typing_return_permille_delta=( + current_snapshot.typing_return_permille + - snapshot.typing_return_permille + ), + docstring_permille_delta=( + current_snapshot.docstring_permille - snapshot.docstring_permille + ), + new_api_symbols=added_api_symbols, + new_api_breaking_changes=api_breaking_changes, + ) + + +def _is_compatible_metrics_schema( + *, + baseline_version: str | None, + expected_version: str, +) -> bool: + if baseline_version is None: + return False + baseline_major_minor = _parse_major_minor(baseline_version) + expected_major_minor = _parse_major_minor(expected_version) + if baseline_major_minor is None or expected_major_minor is None: + return baseline_version == expected_version + baseline_major, baseline_minor = baseline_major_minor + expected_major, expected_minor = expected_major_minor + return baseline_major == expected_major and baseline_minor <= expected_minor + + +def _has_coverage_adoption_snapshot(metrics_obj: Mapping[str, object]) -> bool: + return all( + key in metrics_obj + for key in ( + "typing_param_permille", + "typing_return_permille", + "docstring_permille", ) + ) + + +def _parse_major_minor(version: str) -> tuple[int, int] | None: + parts = version.split(".") + if len(parts) != 2 or not all(part.isdigit() for part in parts): + return None + return int(parts[0]), int(parts[1]) def _atomic_write_json(path: Path, payload: dict[str, object]) -> None: @@ -544,6 +763,15 @@ def _extract_metrics_payload_sha256( return _require_str(payload, "payload_sha256", path=path) +def _extract_optional_payload_sha256( + payload: Mapping[str, Any], + *, + key: str, +) -> str | None: + value = payload.get(key) + return value if isinstance(value, str) else None + + def _require_int(payload: Mapping[str, Any], key: str, *, path: Path) -> int: value = payload.get(key) if isinstance(value, bool): @@ -559,6 +787,23 @@ def _require_int(payload: Mapping[str, Any], key: str, *, path: Path) -> int: ) +def _optional_require_str( + payload: Mapping[str, Any], + key: str, + *, + path: Path, +) -> str | None: + value = payload.get(key) + if value is None: + return None + if isinstance(value, str): + return value + raise BaselineValidationError( + f"Invalid metrics baseline schema at {path}: {key!r} must be str", + status=MetricsBaselineStatus.INVALID_TYPE, + ) + + def _require_str_list(payload: Mapping[str, Any], key: str, *, path: Path) -> list[str]: value = payload.get(key) if not isinstance(value, list): @@ -743,9 +988,262 @@ def _parse_snapshot( ), health_score=_require_int(payload, "health_score", path=path), health_grade=cast("Literal['A', 'B', 'C', 'D', 'F']", grade), + typing_param_permille=_optional_int( + payload, + "typing_param_permille", + path=path, + ), + typing_return_permille=_optional_int( + payload, + "typing_return_permille", + path=path, + ), + docstring_permille=_optional_int(payload, "docstring_permille", path=path), + typing_any_count=_optional_int(payload, "typing_any_count", path=path), ) +def _optional_int(payload: Mapping[str, Any], key: str, *, path: Path) -> int: + value = payload.get(key) + if value is None: + return 0 + return _require_int(payload, key, path=path) + + +def _parse_api_surface_snapshot( + payload: object, + *, + path: Path, + root: Path | None = None, +) -> ApiSurfaceSnapshot | None: + if payload is None: + return None + if not isinstance(payload, dict): + raise BaselineValidationError( + f"Invalid metrics baseline schema at {path}: 'api_surface' must be object", + status=MetricsBaselineStatus.INVALID_TYPE, + ) + raw_modules = payload.get("modules", []) + if not isinstance(raw_modules, list): + raise BaselineValidationError( + f"Invalid metrics baseline schema at {path}: " + "'api_surface.modules' must be list", + status=MetricsBaselineStatus.INVALID_TYPE, + ) + modules: list[ModuleApiSurface] = [] + for raw_module in raw_modules: + if not isinstance(raw_module, dict): + raise BaselineValidationError( + f"Invalid metrics baseline schema at {path}: " + "api surface module must be object", + status=MetricsBaselineStatus.INVALID_TYPE, + ) + module = _require_str(raw_module, "module", path=path) + wire_filepath = _require_str(raw_module, "filepath", path=path) + filepath = runtime_filepath_from_wire(wire_filepath, root=root) + all_declared = _require_str_list_or_none(raw_module, "all_declared", path=path) + raw_symbols = raw_module.get("symbols", []) + if not isinstance(raw_symbols, list): + raise BaselineValidationError( + f"Invalid metrics baseline schema at {path}: " + "api surface symbols must be list", + status=MetricsBaselineStatus.INVALID_TYPE, + ) + symbols: list[PublicSymbol] = [] + for raw_symbol in raw_symbols: + if not isinstance(raw_symbol, dict): + raise BaselineValidationError( + f"Invalid metrics baseline schema at {path}: " + "api surface symbol must be object", + status=MetricsBaselineStatus.INVALID_TYPE, + ) + local_name = _optional_require_str(raw_symbol, "local_name", path=path) + legacy_qualname = _optional_require_str(raw_symbol, "qualname", path=path) + if local_name is None and legacy_qualname is None: + raise BaselineValidationError( + f"Invalid metrics baseline schema at {path}: " + "api surface symbol requires 'local_name' or 'qualname'", + status=MetricsBaselineStatus.MISSING_FIELDS, + ) + if local_name is None: + assert legacy_qualname is not None + qualname = legacy_qualname + else: + qualname = _compose_api_surface_qualname( + module=module, + local_name=local_name, + ) + kind = _require_str(raw_symbol, "kind", path=path) + exported_via = _require_str(raw_symbol, "exported_via", path=path) + params_raw = raw_symbol.get("params", []) + if not isinstance(params_raw, list): + raise BaselineValidationError( + f"Invalid metrics baseline schema at {path}: " + "api surface params must be list", + status=MetricsBaselineStatus.INVALID_TYPE, + ) + params: list[ApiParamSpec] = [] + for raw_param in params_raw: + if not isinstance(raw_param, dict): + raise BaselineValidationError( + f"Invalid metrics baseline schema at {path}: " + "api param must be object", + status=MetricsBaselineStatus.INVALID_TYPE, + ) + name = _require_str(raw_param, "name", path=path) + param_kind = _require_str(raw_param, "kind", path=path) + has_default = raw_param.get("has_default") + annotation_hash = _optional_require_str( + raw_param, + "annotation_hash", + path=path, + ) + if not isinstance(has_default, bool): + raise BaselineValidationError( + f"Invalid metrics baseline schema at {path}: " + "api param 'has_default' must be bool", + status=MetricsBaselineStatus.INVALID_TYPE, + ) + params.append( + ApiParamSpec( + name=name, + kind=cast( + ( + "Literal['pos_only', 'pos_or_kw', " + "'vararg', 'kw_only', 'kwarg']" + ), + param_kind, + ), + has_default=has_default, + annotation_hash=annotation_hash or "", + ) + ) + symbols.append( + PublicSymbol( + qualname=qualname, + kind=cast( + "Literal['function', 'class', 'method', 'constant']", + kind, + ), + start_line=_require_int(raw_symbol, "start_line", path=path), + end_line=_require_int(raw_symbol, "end_line", path=path), + params=tuple(params), + returns_hash=_optional_require_str( + raw_symbol, + "returns_hash", + path=path, + ) + or "", + exported_via=cast("Literal['all', 'name']", exported_via), + ) + ) + modules.append( + ModuleApiSurface( + module=module, + filepath=filepath, + symbols=tuple(sorted(symbols, key=lambda item: item.qualname)), + all_declared=tuple(all_declared) if all_declared is not None else None, + ) + ) + return ApiSurfaceSnapshot( + modules=tuple(sorted(modules, key=lambda item: (item.filepath, item.module))) + ) + + +def _require_str_list_or_none( + payload: Mapping[str, Any], + key: str, + *, + path: Path, +) -> list[str] | None: + value = payload.get(key) + if value is None: + return None + return _require_str_list(payload, key, path=path) + + +def _api_surface_snapshot_payload( + snapshot: ApiSurfaceSnapshot, + *, + root: Path | None = None, + legacy_qualname: bool = False, +) -> dict[str, object]: + return { + "modules": [ + { + "module": module.module, + "filepath": wire_filepath_from_runtime(module.filepath, root=root), + "all_declared": list(module.all_declared or ()), + "symbols": [ + { + ("qualname" if legacy_qualname else "local_name"): ( + symbol.qualname + if legacy_qualname + else _local_name_from_qualname( + module=module.module, + qualname=symbol.qualname, + ) + ), + "kind": symbol.kind, + "start_line": symbol.start_line, + "end_line": symbol.end_line, + "params": [ + { + "name": param.name, + "kind": param.kind, + "has_default": param.has_default, + "annotation_hash": param.annotation_hash, + } + for param in symbol.params + ], + "returns_hash": symbol.returns_hash, + "exported_via": symbol.exported_via, + } + for symbol in sorted( + module.symbols, + key=lambda item: item.qualname, + ) + ], + } + for module in sorted( + snapshot.modules, + key=lambda item: (item.filepath, item.module), + ) + ] + } + + +def _compute_api_surface_payload_sha256( + snapshot: ApiSurfaceSnapshot, + *, + root: Path | None = None, +) -> str: + canonical = _canonical_json(_api_surface_snapshot_payload(snapshot, root=root)) + return hashlib.sha256(canonical.encode("utf-8")).hexdigest() + + +def _compute_legacy_api_surface_payload_sha256( + snapshot: ApiSurfaceSnapshot, + *, + root: Path | None = None, +) -> str: + canonical = _canonical_json( + _api_surface_snapshot_payload(snapshot, root=root, legacy_qualname=True) + ) + return hashlib.sha256(canonical.encode("utf-8")).hexdigest() + + +def _compose_api_surface_qualname(*, module: str, local_name: str) -> str: + return f"{module}:{local_name}" + + +def _local_name_from_qualname(*, module: str, qualname: str) -> str: + prefix = f"{module}:" + if qualname.startswith(prefix): + return qualname[len(prefix) :] + return qualname + + def _build_payload( *, snapshot: MetricsSnapshot, @@ -754,9 +1252,11 @@ def _build_payload( generator_name: str, generator_version: str, created_at: str, + api_surface_snapshot: ApiSurfaceSnapshot | None = None, + api_surface_root: Path | None = None, ) -> dict[str, Any]: payload_sha256 = _compute_payload_sha256(snapshot) - return { + payload: dict[str, Any] = { "meta": { "generator": { "name": generator_name, @@ -769,6 +1269,18 @@ def _build_payload( }, "metrics": _snapshot_payload(snapshot), } + if api_surface_snapshot is not None: + payload["meta"][_API_SURFACE_PAYLOAD_SHA256_KEY] = ( + _compute_api_surface_payload_sha256( + api_surface_snapshot, + root=api_surface_root, + ) + ) + payload["api_surface"] = _api_surface_snapshot_payload( + api_surface_snapshot, + root=api_surface_root, + ) + return payload __all__ = [ diff --git a/codeclone/models.py b/codeclone/models.py index eaae21a..f34abf5 100644 --- a/codeclone/models.py +++ b/codeclone/models.py @@ -126,6 +126,9 @@ class FileMetrics: import_names: frozenset[str] class_names: frozenset[str] referenced_qualnames: frozenset[str] = field(default_factory=frozenset) + typing_coverage: ModuleTypingCoverage | None = None + docstring_coverage: ModuleDocstringCoverage | None = None + api_surface: ModuleApiSurface | None = None @dataclass(frozen=True, slots=True) @@ -200,6 +203,16 @@ class ProjectMetrics: dependency_longest_chains: tuple[tuple[str, ...], ...] dead_code: tuple[DeadItem, ...] health: HealthScore + typing_param_total: int = 0 + typing_param_annotated: int = 0 + typing_return_total: int = 0 + typing_return_annotated: int = 0 + typing_any_count: int = 0 + docstring_public_total: int = 0 + docstring_public_documented: int = 0 + typing_modules: tuple[ModuleTypingCoverage, ...] = () + docstring_modules: tuple[ModuleDocstringCoverage, ...] = () + api_surface: ApiSurfaceSnapshot | None = None @dataclass(frozen=True, slots=True) @@ -215,6 +228,10 @@ class MetricsSnapshot: dead_code_items: tuple[str, ...] health_score: int health_grade: Literal["A", "B", "C", "D", "F"] + typing_param_permille: int = 0 + typing_return_permille: int = 0 + docstring_permille: int = 0 + typing_any_count: int = 0 @dataclass(frozen=True, slots=True) @@ -224,6 +241,74 @@ class MetricsDiff: new_cycles: tuple[tuple[str, ...], ...] new_dead_code: tuple[str, ...] health_delta: int + typing_param_permille_delta: int = 0 + typing_return_permille_delta: int = 0 + docstring_permille_delta: int = 0 + new_api_symbols: tuple[str, ...] = () + new_api_breaking_changes: tuple[ApiBreakingChange, ...] = () + + +@dataclass(frozen=True, slots=True) +class ApiParamSpec: + name: str + kind: Literal["pos_only", "pos_or_kw", "vararg", "kw_only", "kwarg"] + has_default: bool + annotation_hash: str = "" + + +@dataclass(frozen=True, slots=True) +class PublicSymbol: + qualname: str + kind: Literal["function", "class", "method", "constant"] + start_line: int + end_line: int + params: tuple[ApiParamSpec, ...] = () + returns_hash: str = "" + exported_via: Literal["all", "name"] = "name" + + +@dataclass(frozen=True, slots=True) +class ModuleApiSurface: + module: str + filepath: str + symbols: tuple[PublicSymbol, ...] + all_declared: tuple[str, ...] | None = None + + +@dataclass(frozen=True, slots=True) +class ApiSurfaceSnapshot: + modules: tuple[ModuleApiSurface, ...] + + +@dataclass(frozen=True, slots=True) +class ApiBreakingChange: + qualname: str + filepath: str + start_line: int + end_line: int + symbol_kind: Literal["function", "class", "method", "constant"] + change_kind: Literal["removed", "signature_break"] + detail: str + + +@dataclass(frozen=True, slots=True) +class ModuleTypingCoverage: + module: str + filepath: str + callable_count: int + params_total: int + params_annotated: int + returns_total: int + returns_annotated: int + any_annotation_count: int + + +@dataclass(frozen=True, slots=True) +class ModuleDocstringCoverage: + module: str + filepath: str + public_symbol_total: int + public_symbol_documented: int GroupItem = dict[str, object] diff --git a/codeclone/pipeline.py b/codeclone/pipeline.py index 97f29dd..f87477b 100644 --- a/codeclone/pipeline.py +++ b/codeclone/pipeline.py @@ -6,7 +6,9 @@ from __future__ import annotations +import inspect import os +from collections.abc import Mapping from concurrent.futures import ProcessPoolExecutor, as_completed from dataclasses import dataclass from hashlib import sha256 @@ -17,12 +19,14 @@ from ._coerce import as_int, as_str from .cache import ( + ApiParamSpecDict, Cache, CacheEntry, ClassMetricsDict, DeadCandidateDict, FileStat, ModuleDepDict, + PublicSymbolDict, SegmentReportProjection, SourceStatsDict, StructuralFindingGroupDict, @@ -42,6 +46,9 @@ find_unused, ) from .models import ( + ApiBreakingChange, + ApiParamSpec, + ApiSurfaceSnapshot, BlockUnit, ClassMetrics, DeadCandidate, @@ -52,8 +59,12 @@ GroupItemLike, GroupMap, MetricsDiff, + ModuleApiSurface, ModuleDep, + ModuleDocstringCoverage, + ModuleTypingCoverage, ProjectMetrics, + PublicSymbol, SegmentUnit, StructuralFindingGroup, StructuralFindingOccurrence, @@ -120,6 +131,9 @@ class DiscoveryResult: files_to_process: tuple[str, ...] skipped_warnings: tuple[str, ...] cached_referenced_qualnames: frozenset[str] = frozenset() + cached_typing_modules: tuple[ModuleTypingCoverage, ...] = () + cached_docstring_modules: tuple[ModuleDocstringCoverage, ...] = () + cached_api_modules: tuple[ModuleApiSurface, ...] = () cached_structural_findings: tuple[StructuralFindingGroup, ...] = () cached_segment_report_projection: SegmentReportProjection | None = None cached_lines: int = 0 @@ -165,6 +179,9 @@ class ProcessingResult: failed_files: tuple[str, ...] source_read_failures: tuple[str, ...] referenced_qualnames: frozenset[str] = frozenset() + typing_modules: tuple[ModuleTypingCoverage, ...] = () + docstring_modules: tuple[ModuleDocstringCoverage, ...] = () + api_modules: tuple[ModuleApiSurface, ...] = () structural_findings: tuple[StructuralFindingGroup, ...] = () source_stats_by_file: tuple[tuple[str, int, int, int, int], ...] = () @@ -214,6 +231,11 @@ class MetricGateConfig: fail_dead_code: bool fail_health: int fail_on_new_metrics: bool + fail_on_typing_regression: bool = False + fail_on_docstring_regression: bool = False + fail_on_api_break: bool = False + min_typing_coverage: int = -1 + min_docstring_coverage: int = -1 def _as_sorted_str_tuple(value: object) -> tuple[str, ...]: @@ -381,10 +403,13 @@ def _new_discovery_buffers() -> tuple[ list[DeadCandidate], set[str], set[str], + list[ModuleTypingCoverage], + list[ModuleDocstringCoverage], + list[ModuleApiSurface], list[str], list[str], ]: - return [], [], [], [], [], [], set(), set(), [], [] + return [], [], [], [], [], [], set(), set(), [], [], [], [], [] def _decode_cached_structural_finding_group( @@ -483,7 +508,185 @@ def _usable_cached_source_stats( return _cache_entry_source_stats(entry) -def _load_cached_metrics( +def _cache_dict_module_fields( + value: object, +) -> tuple[Mapping[str, object], str, str] | None: + if not isinstance(value, dict): + return None + row = cast("Mapping[str, object]", value) + module = row.get("module") + filepath = row.get("filepath") + if not isinstance(module, str) or not isinstance(filepath, str): + return None + return row, module, filepath + + +def _cache_dict_int_fields( + row: Mapping[str, object], + *keys: str, +) -> tuple[int, ...] | None: + values: list[int] = [] + for key in keys: + value = row.get(key) + if not isinstance(value, int): + return None + values.append(value) + return tuple(values) + + +def _typing_coverage_from_cache_dict( + value: object, +) -> ModuleTypingCoverage | None: + row_info = _cache_dict_module_fields(value) + if row_info is None: + return None + row, module, filepath = row_info + int_fields = _cache_dict_int_fields( + row, + "callable_count", + "params_total", + "params_annotated", + "returns_total", + "returns_annotated", + "any_annotation_count", + ) + if int_fields is None: + return None + ( + callable_count, + params_total, + params_annotated, + returns_total, + returns_annotated, + any_annotation_count, + ) = int_fields + return ModuleTypingCoverage( + module=module, + filepath=filepath, + callable_count=callable_count, + params_total=params_total, + params_annotated=params_annotated, + returns_total=returns_total, + returns_annotated=returns_annotated, + any_annotation_count=any_annotation_count, + ) + + +def _docstring_coverage_from_cache_dict( + value: object, +) -> ModuleDocstringCoverage | None: + row_info = _cache_dict_module_fields(value) + if row_info is None: + return None + row, module, filepath = row_info + totals = _cache_dict_int_fields( + row, + "public_symbol_total", + "public_symbol_documented", + ) + if totals is None: + return None + public_symbol_total, public_symbol_documented = totals + return ModuleDocstringCoverage( + module=module, + filepath=filepath, + public_symbol_total=public_symbol_total, + public_symbol_documented=public_symbol_documented, + ) + + +def _api_param_spec_from_cache_dict(value: ApiParamSpecDict) -> ApiParamSpec | None: + name = value.get("name") + kind = value.get("kind") + has_default = value.get("has_default") + annotation_hash = value.get("annotation_hash", "") + if ( + not isinstance(name, str) + or not isinstance(kind, str) + or not isinstance(has_default, bool) + or not isinstance(annotation_hash, str) + ): + return None + return ApiParamSpec( + name=name, + kind=cast( + "Literal['pos_only', 'pos_or_kw', 'vararg', 'kw_only', 'kwarg']", + kind, + ), + has_default=has_default, + annotation_hash=annotation_hash, + ) + + +def _public_symbol_from_cache_dict( + value: PublicSymbolDict, +) -> PublicSymbol | None: + qualname = value.get("qualname") + kind = value.get("kind") + start_line = value.get("start_line") + end_line = value.get("end_line") + exported_via = value.get("exported_via", "name") + returns_hash = value.get("returns_hash", "") + params_raw = value.get("params", []) + if ( + not isinstance(qualname, str) + or not isinstance(kind, str) + or not isinstance(start_line, int) + or not isinstance(end_line, int) + or not isinstance(exported_via, str) + or not isinstance(returns_hash, str) + or not isinstance(params_raw, list) + ): + return None + params = [] + for param in params_raw: + if not isinstance(param, dict): + return None + parsed = _api_param_spec_from_cache_dict(param) + if parsed is None: + return None + params.append(parsed) + return PublicSymbol( + qualname=qualname, + kind=cast("Literal['function', 'class', 'method', 'constant']", kind), + start_line=start_line, + end_line=end_line, + params=tuple(params), + returns_hash=returns_hash, + exported_via=cast("Literal['all', 'name']", exported_via), + ) + + +def _api_surface_from_cache_dict(value: object) -> ModuleApiSurface | None: + row_info = _cache_dict_module_fields(value) + if row_info is None: + return None + row, module, filepath = row_info + all_declared_raw = row.get("all_declared", []) + symbols_raw = row.get("symbols", []) + if ( + not isinstance(all_declared_raw, list) + or not isinstance(symbols_raw, list) + or not all(isinstance(item, str) for item in all_declared_raw) + ): + return None + symbols: list[PublicSymbol] = [] + for item in symbols_raw: + if not isinstance(item, dict): + return None + parsed = _public_symbol_from_cache_dict(cast("PublicSymbolDict", item)) + if parsed is None: + return None + symbols.append(parsed) + return ModuleApiSurface( + module=module, + filepath=filepath, + all_declared=tuple(sorted(set(all_declared_raw))) or None, + symbols=tuple(sorted(symbols, key=lambda item: item.qualname)), + ) + + +def _load_cached_metrics_extended( entry: CacheEntry, *, filepath: str, @@ -493,6 +696,9 @@ def _load_cached_metrics( tuple[DeadCandidate, ...], frozenset[str], frozenset[str], + ModuleTypingCoverage | None, + ModuleDocstringCoverage | None, + ModuleApiSurface | None, ]: class_metrics_rows: list[ClassMetricsDict] = entry.get("class_metrics", []) class_metrics = tuple( @@ -559,12 +765,20 @@ def _load_cached_metrics( if is_test_filepath(filepath) else frozenset(entry.get("referenced_qualnames", [])) ) + typing_coverage = _typing_coverage_from_cache_dict(entry.get("typing_coverage")) + docstring_coverage = _docstring_coverage_from_cache_dict( + entry.get("docstring_coverage") + ) + api_surface = _api_surface_from_cache_dict(entry.get("api_surface")) return ( class_metrics, module_deps, dead_candidates, referenced_names, referenced_qualnames, + typing_coverage, + docstring_coverage, + api_surface, ) @@ -586,6 +800,9 @@ def discover(*, boot: BootstrapResult, cache: Cache) -> DiscoveryResult: cached_dead_candidates, cached_referenced_names, cached_referenced_qualnames, + cached_typing_modules, + cached_docstring_modules, + cached_api_modules, files_to_process, skipped_warnings, ) = _new_discovery_buffers() @@ -642,12 +859,21 @@ def discover(*, boot: BootstrapResult, cache: Cache) -> DiscoveryResult: dead_candidates, referenced_names, referenced_qualnames, - ) = _load_cached_metrics(cached, filepath=filepath) + typing_coverage, + docstring_coverage, + api_surface, + ) = _load_cached_metrics_extended(cached, filepath=filepath) cached_class_metrics.extend(class_metrics) cached_module_deps.extend(module_deps) cached_dead_candidates.extend(dead_candidates) cached_referenced_names.update(referenced_names) cached_referenced_qualnames.update(referenced_qualnames) + if typing_coverage is not None: + cached_typing_modules.append(typing_coverage) + if docstring_coverage is not None: + cached_docstring_modules.append(docstring_coverage) + if api_surface is not None: + cached_api_modules.append(api_surface) if collect_structural_findings: cached_sf.extend( _decode_cached_structural_finding_group(group_dict, filepath) @@ -674,6 +900,18 @@ def discover(*, boot: BootstrapResult, cache: Cache) -> DiscoveryResult: ), cached_referenced_names=frozenset(cached_referenced_names), cached_referenced_qualnames=frozenset(cached_referenced_qualnames), + cached_typing_modules=tuple( + sorted(cached_typing_modules, key=lambda item: (item.filepath, item.module)) + ), + cached_docstring_modules=tuple( + sorted( + cached_docstring_modules, + key=lambda item: (item.filepath, item.module), + ) + ), + cached_api_modules=tuple( + sorted(cached_api_modules, key=lambda item: (item.filepath, item.module)) + ), files_to_process=tuple(files_to_process), skipped_warnings=tuple(sorted(skipped_warnings)), cached_structural_findings=tuple(cached_sf), @@ -695,6 +933,10 @@ def process_file( min_loc: int, min_stmt: int, collect_structural_findings: bool = True, + collect_typing_coverage: bool = True, + collect_docstring_coverage: bool = True, + collect_api_surface: bool = False, + api_include_private_modules: bool = False, block_min_loc: int = 20, block_min_stmt: int = 8, segment_min_loc: int = 20, @@ -757,6 +999,10 @@ def process_file( segment_min_loc=segment_min_loc, segment_min_stmt=segment_min_stmt, collect_structural_findings=collect_structural_findings, + collect_typing_coverage=collect_typing_coverage, + collect_docstring_coverage=collect_docstring_coverage, + collect_api_surface=collect_api_surface, + api_include_private_modules=api_include_private_modules, ) ) @@ -804,6 +1050,9 @@ def process( dead_candidates=discovery.cached_dead_candidates, referenced_names=discovery.cached_referenced_names, referenced_qualnames=discovery.cached_referenced_qualnames, + typing_modules=discovery.cached_typing_modules, + docstring_modules=discovery.cached_docstring_modules, + api_modules=discovery.cached_api_modules, files_analyzed=0, files_skipped=discovery.files_skipped, analyzed_lines=0, @@ -825,6 +1074,26 @@ def process( all_dead_candidates: list[DeadCandidate] = list(discovery.cached_dead_candidates) all_referenced_names: set[str] = set(discovery.cached_referenced_names) all_referenced_qualnames: set[str] = set(discovery.cached_referenced_qualnames) + all_typing_modules: list[ModuleTypingCoverage] = list( + discovery.cached_typing_modules + ) + all_docstring_modules: list[ModuleDocstringCoverage] = list( + discovery.cached_docstring_modules + ) + all_api_modules: list[ModuleApiSurface] = list(discovery.cached_api_modules) + collect_structural_findings = _should_collect_structural_findings(boot.output_paths) + collect_typing_coverage = not boot.args.skip_metrics and bool( + getattr(boot.args, "typing_coverage", True) + ) + collect_docstring_coverage = not boot.args.skip_metrics and bool( + getattr(boot.args, "docstring_coverage", True) + ) + collect_api_surface = not boot.args.skip_metrics and bool( + getattr(boot.args, "api_surface", False) + ) + api_include_private_modules = bool( + getattr(boot.args, "api_include_private_modules", False) + ) files_analyzed = 0 files_skipped = discovery.files_skipped @@ -930,6 +1199,12 @@ def _accept_result(result: FileProcessResult) -> None: all_referenced_qualnames.update( result.file_metrics.referenced_qualnames ) + if result.file_metrics.typing_coverage is not None: + all_typing_modules.append(result.file_metrics.typing_coverage) + if result.file_metrics.docstring_coverage is not None: + all_docstring_modules.append(result.file_metrics.docstring_coverage) + if result.file_metrics.api_surface is not None: + all_api_modules.append(result.file_metrics.api_surface) return files_skipped += 1 @@ -940,23 +1215,50 @@ def _accept_result(result: FileProcessResult) -> None: def _run_sequential(files: Sequence[str]) -> None: for filepath in files: - _accept_result( - process_file( - filepath, - root_str, - boot.config, - min_loc, - min_stmt, - collect_structural_findings, - block_min_loc, - block_min_stmt, - segment_min_loc, - segment_min_stmt, - ) - ) + _accept_result(_invoke_process_file(filepath)) if on_advance is not None: on_advance() + def _invoke_process_file(filepath: str) -> FileProcessResult: + optional_kwargs: dict[str, object] = { + "collect_structural_findings": collect_structural_findings, + "collect_typing_coverage": collect_typing_coverage, + "collect_docstring_coverage": collect_docstring_coverage, + "collect_api_surface": collect_api_surface, + "api_include_private_modules": api_include_private_modules, + "block_min_loc": block_min_loc, + "block_min_stmt": block_min_stmt, + "segment_min_loc": segment_min_loc, + "segment_min_stmt": segment_min_stmt, + } + try: + signature = inspect.signature(process_file) + except (TypeError, ValueError): + supported_kwargs = optional_kwargs + else: + parameters = tuple(signature.parameters.values()) + if any( + parameter.kind == inspect.Parameter.VAR_KEYWORD + for parameter in parameters + ): + supported_kwargs = optional_kwargs + else: + supported_names = {parameter.name for parameter in parameters} + supported_kwargs = { + key: value + for key, value in optional_kwargs.items() + if key in supported_names + } + process_callable = cast("Callable[..., FileProcessResult]", process_file) + return process_callable( + filepath, + root_str, + boot.config, + min_loc, + min_stmt, + **supported_kwargs, + ) + if _should_use_parallel(len(files_to_process), processes): try: with ProcessPoolExecutor(max_workers=processes) as executor: @@ -964,17 +1266,8 @@ def _run_sequential(files: Sequence[str]) -> None: batch = files_to_process[idx : idx + batch_size] futures = [ executor.submit( - process_file, + _invoke_process_file, filepath, - root_str, - boot.config, - min_loc, - min_stmt, - collect_structural_findings, - block_min_loc, - block_min_stmt, - segment_min_loc, - segment_min_stmt, ) for filepath in batch ] @@ -1011,6 +1304,15 @@ def _run_sequential(files: Sequence[str]) -> None: ), referenced_names=frozenset(all_referenced_names), referenced_qualnames=frozenset(all_referenced_qualnames), + typing_modules=tuple( + sorted(all_typing_modules, key=lambda item: (item.filepath, item.module)) + ), + docstring_modules=tuple( + sorted(all_docstring_modules, key=lambda item: (item.filepath, item.module)) + ), + api_modules=tuple( + sorted(all_api_modules, key=lambda item: (item.filepath, item.module)) + ), files_analyzed=files_analyzed, files_skipped=files_skipped, analyzed_lines=analyzed_lines, @@ -1045,6 +1347,9 @@ def compute_project_metrics( dead_candidates: Sequence[DeadCandidate], referenced_names: frozenset[str], referenced_qualnames: frozenset[str], + typing_modules: Sequence[ModuleTypingCoverage] = (), + docstring_modules: Sequence[ModuleDocstringCoverage] = (), + api_modules: Sequence[ModuleApiSurface] = (), files_found: int, files_analyzed_or_cached: int, function_clone_groups: int, @@ -1126,6 +1431,23 @@ def compute_project_metrics( referenced_qualnames=referenced_qualnames, ) + typing_rows = tuple( + sorted(typing_modules, key=lambda item: (item.filepath, item.module)) + ) + docstring_rows = tuple( + sorted(docstring_modules, key=lambda item: (item.filepath, item.module)) + ) + api_rows = tuple(sorted(api_modules, key=lambda item: (item.filepath, item.module))) + typing_param_total = sum(item.params_total for item in typing_rows) + typing_param_annotated = sum(item.params_annotated for item in typing_rows) + typing_return_total = sum(item.returns_total for item in typing_rows) + typing_return_annotated = sum(item.returns_annotated for item in typing_rows) + typing_any_count = sum(item.any_annotation_count for item in typing_rows) + docstring_public_total = sum(item.public_symbol_total for item in docstring_rows) + docstring_public_documented = sum( + item.public_symbol_documented for item in docstring_rows + ) + health = compute_health( HealthInputs( files_found=files_found, @@ -1164,6 +1486,16 @@ def compute_project_metrics( dependency_longest_chains=dep_graph.longest_chains, dead_code=dead_items, health=health, + typing_param_total=typing_param_total, + typing_param_annotated=typing_param_annotated, + typing_return_total=typing_return_total, + typing_return_annotated=typing_return_annotated, + typing_any_count=typing_any_count, + docstring_public_total=docstring_public_total, + docstring_public_documented=docstring_public_documented, + typing_modules=typing_rows, + docstring_modules=docstring_rows, + api_surface=ApiSurfaceSnapshot(modules=api_rows) if api_rows else None, ) return project_metrics, dep_graph, dead_items @@ -1193,6 +1525,235 @@ def compute_suggestions( ) +def _permille(numerator: int, denominator: int) -> int: + if denominator <= 0: + return 0 + return round((1000.0 * float(numerator)) / float(denominator)) + + +def _coverage_adoption_rows( + project_metrics: ProjectMetrics, +) -> list[dict[str, object]]: + docstring_by_module = { + (item.filepath, item.module): item for item in project_metrics.docstring_modules + } + rows: list[dict[str, object]] = [] + seen_keys: set[tuple[str, str]] = set() + for typing_item in project_metrics.typing_modules: + key = (typing_item.filepath, typing_item.module) + seen_keys.add(key) + docstring_item = docstring_by_module.get(key) + doc_total = docstring_item.public_symbol_total if docstring_item else 0 + doc_documented = ( + docstring_item.public_symbol_documented if docstring_item else 0 + ) + rows.append( + { + "module": typing_item.module, + "filepath": typing_item.filepath, + "callable_count": typing_item.callable_count, + "params_total": typing_item.params_total, + "params_annotated": typing_item.params_annotated, + "param_permille": _permille( + typing_item.params_annotated, + typing_item.params_total, + ), + "returns_total": typing_item.returns_total, + "returns_annotated": typing_item.returns_annotated, + "return_permille": _permille( + typing_item.returns_annotated, + typing_item.returns_total, + ), + "any_annotation_count": typing_item.any_annotation_count, + "public_symbol_total": doc_total, + "public_symbol_documented": doc_documented, + "docstring_permille": _permille(doc_documented, doc_total), + } + ) + for docstring_item in project_metrics.docstring_modules: + key = (docstring_item.filepath, docstring_item.module) + if key in seen_keys: + continue + rows.append( + { + "module": docstring_item.module, + "filepath": docstring_item.filepath, + "callable_count": 0, + "params_total": 0, + "params_annotated": 0, + "param_permille": 0, + "returns_total": 0, + "returns_annotated": 0, + "return_permille": 0, + "any_annotation_count": 0, + "public_symbol_total": docstring_item.public_symbol_total, + "public_symbol_documented": docstring_item.public_symbol_documented, + "docstring_permille": _permille( + docstring_item.public_symbol_documented, + docstring_item.public_symbol_total, + ), + } + ) + return sorted( + rows, + key=lambda item: ( + _as_int(item.get("param_permille")), + _as_int(item.get("docstring_permille")), + _as_int(item.get("return_permille")), + _as_str(item.get("module")), + ), + ) + + +def _api_surface_summary( + api_surface: ApiSurfaceSnapshot | None, +) -> dict[str, object]: + modules = api_surface.modules if api_surface is not None else () + return { + "enabled": api_surface is not None, + "modules": len(modules), + "public_symbols": sum(len(module.symbols) for module in modules), + "added": 0, + "breaking": 0, + "strict_types": False, + } + + +def _api_surface_rows( + api_surface: ApiSurfaceSnapshot | None, +) -> list[dict[str, object]]: + if api_surface is None: + return [] + rows: list[dict[str, object]] = [] + for module in api_surface.modules: + rows.extend( + { + "record_kind": "symbol", + "module": module.module, + "filepath": module.filepath, + "qualname": symbol.qualname, + "start_line": symbol.start_line, + "end_line": symbol.end_line, + "symbol_kind": symbol.kind, + "exported_via": symbol.exported_via, + "params_total": len(symbol.params), + "params": [ + { + "name": param.name, + "kind": param.kind, + "has_default": param.has_default, + "annotated": bool(param.annotation_hash), + } + for param in symbol.params + ], + "returns_annotated": bool(symbol.returns_hash), + } + for symbol in module.symbols + ) + return sorted( + rows, + key=lambda item: ( + _as_str(item.get("filepath")), + _as_int(item.get("start_line")), + _as_int(item.get("end_line")), + _as_str(item.get("qualname")), + _as_str(item.get("record_kind")), + ), + ) + + +def _breaking_api_surface_rows( + changes: Sequence[object], +) -> list[dict[str, object]]: + rows: list[dict[str, object]] = [] + for change in changes: + if not isinstance(change, ApiBreakingChange): + continue + module_name, _, _local_name = change.qualname.partition(":") + rows.append( + { + "record_kind": "breaking_change", + "module": module_name, + "filepath": change.filepath, + "qualname": change.qualname, + "start_line": change.start_line, + "end_line": change.end_line, + "symbol_kind": change.symbol_kind, + "change_kind": change.change_kind, + "detail": change.detail, + } + ) + return sorted( + rows, + key=lambda item: ( + _as_str(item.get("filepath")), + _as_int(item.get("start_line")), + _as_int(item.get("end_line")), + _as_str(item.get("qualname")), + _as_str(item.get("change_kind")), + ), + ) + + +def _enrich_metrics_report_payload( + *, + metrics_payload: Mapping[str, object], + metrics_diff: MetricsDiff | None, +) -> dict[str, object]: + enriched = { + key: (dict(value) if isinstance(value, Mapping) else value) + for key, value in metrics_payload.items() + } + coverage_adoption = dict( + cast("Mapping[str, object]", enriched.get("coverage_adoption", {})) + ) + coverage_summary = dict( + cast("Mapping[str, object]", coverage_adoption.get("summary", {})) + ) + if coverage_summary: + coverage_summary["baseline_diff_available"] = metrics_diff is not None + coverage_summary["param_delta"] = ( + int(metrics_diff.typing_param_permille_delta) + if metrics_diff is not None + else 0 + ) + coverage_summary["return_delta"] = ( + int(metrics_diff.typing_return_permille_delta) + if metrics_diff is not None + else 0 + ) + coverage_summary["docstring_delta"] = ( + int(metrics_diff.docstring_permille_delta) + if metrics_diff is not None + else 0 + ) + coverage_adoption["summary"] = coverage_summary + enriched["coverage_adoption"] = coverage_adoption + + api_surface = dict(cast("Mapping[str, object]", enriched.get("api_surface", {}))) + api_summary = dict(cast("Mapping[str, object]", api_surface.get("summary", {}))) + api_items = list(cast("Sequence[object]", api_surface.get("items", ()))) + if api_summary: + api_summary["baseline_diff_available"] = metrics_diff is not None + api_summary["added"] = ( + len(metrics_diff.new_api_symbols) if metrics_diff is not None else 0 + ) + api_summary["breaking"] = ( + len(metrics_diff.new_api_breaking_changes) + if metrics_diff is not None + else 0 + ) + api_surface["summary"] = api_summary + if metrics_diff is not None and metrics_diff.new_api_breaking_changes: + api_items.extend( + _breaking_api_surface_rows(metrics_diff.new_api_breaking_changes) + ) + api_surface["items"] = api_items + if api_surface: + enriched["api_surface"] = api_surface + return enriched + + def build_metrics_report_payload( *, scan_root: str = "", @@ -1256,6 +1817,9 @@ def build_metrics_report_payload( ] active_dead_items = tuple(project_metrics.dead_code) suppressed_dead_items = tuple(suppressed_dead_code) + coverage_adoption_rows = _coverage_adoption_rows(project_metrics) + api_surface_summary = _api_surface_summary(project_metrics.api_surface) + api_surface_items = _api_surface_rows(project_metrics.api_surface) def _serialize_dead_item( item: DeadItem, @@ -1351,6 +1915,35 @@ def _serialize_dead_item( "grade": project_metrics.health.grade, "dimensions": dict(project_metrics.health.dimensions), }, + "coverage_adoption": { + "summary": { + "modules": len(coverage_adoption_rows), + "params_total": project_metrics.typing_param_total, + "params_annotated": project_metrics.typing_param_annotated, + "param_permille": _permille( + project_metrics.typing_param_annotated, + project_metrics.typing_param_total, + ), + "returns_total": project_metrics.typing_return_total, + "returns_annotated": project_metrics.typing_return_annotated, + "return_permille": _permille( + project_metrics.typing_return_annotated, + project_metrics.typing_return_total, + ), + "public_symbol_total": project_metrics.docstring_public_total, + "public_symbol_documented": project_metrics.docstring_public_documented, + "docstring_permille": _permille( + project_metrics.docstring_public_documented, + project_metrics.docstring_public_total, + ), + "typing_any_count": project_metrics.typing_any_count, + }, + "items": coverage_adoption_rows, + }, + "api_surface": { + "summary": dict(api_surface_summary), + "items": api_surface_items, + }, "overloaded_modules": build_overloaded_modules_payload( scan_root=scan_root, source_stats_by_file=source_stats_by_file, @@ -1428,6 +2021,9 @@ def analyze( dead_candidates=processing.dead_candidates, referenced_names=processing.referenced_names, referenced_qualnames=processing.referenced_qualnames, + typing_modules=processing.typing_modules, + docstring_modules=processing.docstring_modules, + api_modules=processing.api_modules, files_found=discovery.files_found, files_analyzed_or_cached=files_analyzed_or_cached, function_clone_groups=func_clones_count, @@ -1548,6 +2144,14 @@ def report( ) if needs_report_document: + metrics_for_report = ( + _enrich_metrics_report_payload( + metrics_payload=analysis.metrics_payload, + metrics_diff=cast("MetricsDiff | None", metrics_diff), + ) + if analysis.metrics_payload is not None + else None + ) report_document = build_report_document( func_groups=analysis.func_groups, block_groups=analysis.block_groups_report, @@ -1558,12 +2162,20 @@ def report( new_function_group_keys=new_func, new_block_group_keys=new_block, new_segment_group_keys=set(analysis.segment_groups.keys()), - metrics=analysis.metrics_payload, + metrics=metrics_for_report, suggestions=analysis.suggestions, structural_findings=sf, ) if boot.output_paths.html and html_builder is not None: + metrics_for_html = ( + _enrich_metrics_report_payload( + metrics_payload=analysis.metrics_payload, + metrics_diff=cast("MetricsDiff | None", metrics_diff), + ) + if analysis.metrics_payload is not None + else None + ) contents["html"] = html_builder( func_groups=analysis.func_groups, block_groups=analysis.block_groups_report, @@ -1572,7 +2184,7 @@ def report( new_function_group_keys=new_func, new_block_group_keys=new_block, report_meta=report_meta, - metrics=analysis.metrics_payload, + metrics=metrics_for_html, suggestions=analysis.suggestions, structural_findings=sf, report_document=report_document, @@ -1643,81 +2255,194 @@ def metric_gate_reasons( config: MetricGateConfig, ) -> tuple[str, ...]: reasons: list[str] = [] + _append_threshold_metric_reasons( + reasons=reasons, + project_metrics=project_metrics, + config=config, + ) + _append_new_metric_diff_reasons( + reasons=reasons, + metrics_diff=metrics_diff, + config=config, + ) + _append_adoption_metric_reasons( + reasons=reasons, + metrics_diff=metrics_diff, + project_metrics=project_metrics, + config=config, + ) + return tuple(reasons) - if ( - config.fail_complexity >= 0 - and project_metrics.complexity_max > config.fail_complexity - ): - reasons.append( + +def _append_threshold_metric_reasons( + *, + reasons: list[str], + project_metrics: ProjectMetrics, + config: MetricGateConfig, +) -> None: + threshold_rows = ( + ( + config.fail_complexity >= 0 + and project_metrics.complexity_max > config.fail_complexity, "Complexity threshold exceeded: " f"max CC={project_metrics.complexity_max}, " - f"threshold={config.fail_complexity}." - ) - if ( - config.fail_coupling >= 0 - and project_metrics.coupling_max > config.fail_coupling - ): - reasons.append( + f"threshold={config.fail_complexity}.", + ), + ( + config.fail_coupling >= 0 + and project_metrics.coupling_max > config.fail_coupling, "Coupling threshold exceeded: " f"max CBO={project_metrics.coupling_max}, " - f"threshold={config.fail_coupling}." - ) - if ( - config.fail_cohesion >= 0 - and project_metrics.cohesion_max > config.fail_cohesion - ): - reasons.append( + f"threshold={config.fail_coupling}.", + ), + ( + config.fail_cohesion >= 0 + and project_metrics.cohesion_max > config.fail_cohesion, "Cohesion threshold exceeded: " f"max LCOM4={project_metrics.cohesion_max}, " - f"threshold={config.fail_cohesion}." - ) + f"threshold={config.fail_cohesion}.", + ), + ( + config.fail_health >= 0 + and project_metrics.health.total < config.fail_health, + "Health score below threshold: " + f"score={project_metrics.health.total}, threshold={config.fail_health}.", + ), + ) + reasons.extend(message for triggered, message in threshold_rows if triggered) if config.fail_cycles and project_metrics.dependency_cycles: reasons.append( "Dependency cycles detected: " f"{len(project_metrics.dependency_cycles)} cycle(s)." ) - if config.fail_dead_code: - high_conf_dead = [ - item for item in project_metrics.dead_code if item.confidence == "high" - ] - if high_conf_dead: - reasons.append( - f"Dead code detected (high confidence): {len(high_conf_dead)} item(s)." - ) - if config.fail_health >= 0 and project_metrics.health.total < config.fail_health: + high_conf_dead = _high_confidence_dead_code_count(project_metrics.dead_code) + if config.fail_dead_code and high_conf_dead > 0: reasons.append( - "Health score below threshold: " - f"score={project_metrics.health.total}, threshold={config.fail_health}." + f"Dead code detected (high confidence): {high_conf_dead} item(s)." ) - if config.fail_on_new_metrics and metrics_diff is not None: - if metrics_diff.new_high_risk_functions: - reasons.append( - "New high-risk functions vs metrics baseline: " - f"{len(metrics_diff.new_high_risk_functions)}." - ) - if metrics_diff.new_high_coupling_classes: - reasons.append( - "New high-coupling classes vs metrics baseline: " - f"{len(metrics_diff.new_high_coupling_classes)}." - ) - if metrics_diff.new_cycles: - reasons.append( - "New dependency cycles vs metrics baseline: " - f"{len(metrics_diff.new_cycles)}." - ) - if metrics_diff.new_dead_code: - reasons.append( - "New dead code items vs metrics baseline: " - f"{len(metrics_diff.new_dead_code)}." - ) - if metrics_diff.health_delta < 0: + +def _append_new_metric_diff_reasons( + *, + reasons: list[str], + metrics_diff: MetricsDiff | None, + config: MetricGateConfig, +) -> None: + if not config.fail_on_new_metrics or metrics_diff is None: + return + if metrics_diff.new_high_risk_functions: + reasons.append( + "New high-risk functions vs metrics baseline: " + f"{len(metrics_diff.new_high_risk_functions)}." + ) + if metrics_diff.new_high_coupling_classes: + reasons.append( + "New high-coupling classes vs metrics baseline: " + f"{len(metrics_diff.new_high_coupling_classes)}." + ) + if metrics_diff.new_cycles: + reasons.append( + "New dependency cycles vs metrics baseline: " + f"{len(metrics_diff.new_cycles)}." + ) + if metrics_diff.new_dead_code: + reasons.append( + "New dead code items vs metrics baseline: " + f"{len(metrics_diff.new_dead_code)}." + ) + if metrics_diff.health_delta < 0: + reasons.append( + "Health score regressed vs metrics baseline: " + f"delta={metrics_diff.health_delta}." + ) + + +def _append_metric_gate_reason( + *, + reasons: list[str], + enabled: bool, + triggered: bool, + message: str, +) -> None: + if enabled and triggered: + reasons.append(message) + + +def _append_adoption_metric_reasons( + *, + reasons: list[str], + metrics_diff: MetricsDiff | None, + project_metrics: ProjectMetrics, + config: MetricGateConfig, +) -> None: + typing_percent = ( + _permille( + project_metrics.typing_param_annotated, + project_metrics.typing_param_total, + ) + / 10.0 + ) + docstring_percent = ( + _permille( + project_metrics.docstring_public_documented, + project_metrics.docstring_public_total, + ) + / 10.0 + ) + if config.min_typing_coverage >= 0 and typing_percent < float( + config.min_typing_coverage + ): + reasons.append( + "Typing coverage below threshold: " + f"coverage={typing_percent:.1f}%, threshold={config.min_typing_coverage}%." + ) + if config.min_docstring_coverage >= 0 and docstring_percent < float( + config.min_docstring_coverage + ): + reasons.append( + "Docstring coverage below threshold: " + "coverage=" + f"{docstring_percent:.1f}%, " + f"threshold={config.min_docstring_coverage}%." + ) + if metrics_diff is None: + return + if config.fail_on_typing_regression: + typing_delta = int(getattr(metrics_diff, "typing_param_permille_delta", 0)) + return_delta = int(getattr(metrics_diff, "typing_return_permille_delta", 0)) + if typing_delta < 0 or return_delta < 0: reasons.append( - "Health score regressed vs metrics baseline: " - f"delta={metrics_diff.health_delta}." + "Typing coverage regressed vs metrics baseline: " + f"params_delta={typing_delta}, returns_delta={return_delta}." ) + docstring_delta = int(getattr(metrics_diff, "docstring_permille_delta", 0)) + _append_metric_gate_reason( + reasons=reasons, + enabled=config.fail_on_docstring_regression, + triggered=docstring_delta < 0, + message=( + "Docstring coverage regressed vs metrics baseline: " + f"delta={docstring_delta}." + ), + ) + api_breaking = tuple( + cast( + "Sequence[object]", + getattr(metrics_diff, "new_api_breaking_changes", ()), + ) + ) + _append_metric_gate_reason( + reasons=reasons, + enabled=config.fail_on_api_break, + triggered=bool(api_breaking), + message=( + f"Public API breaking changes vs metrics baseline: {len(api_breaking)}." + ), + ) - return tuple(reasons) + +def _high_confidence_dead_code_count(items: Sequence[DeadItem]) -> int: + return sum(1 for item in items if item.confidence == "high") def gate( @@ -1742,6 +2467,17 @@ def gate( fail_dead_code=boot.args.fail_dead_code, fail_health=boot.args.fail_health, fail_on_new_metrics=boot.args.fail_on_new_metrics, + fail_on_typing_regression=bool( + getattr(boot.args, "fail_on_typing_regression", False) + ), + fail_on_docstring_regression=bool( + getattr(boot.args, "fail_on_docstring_regression", False) + ), + fail_on_api_break=bool(getattr(boot.args, "fail_on_api_break", False)), + min_typing_coverage=int(getattr(boot.args, "min_typing_coverage", -1)), + min_docstring_coverage=int( + getattr(boot.args, "min_docstring_coverage", -1) + ), ), ) reasons.extend(f"metric:{reason}" for reason in metric_reasons) diff --git a/codeclone/report/json_contract.py b/codeclone/report/json_contract.py index faac269..4e09395 100644 --- a/codeclone/report/json_contract.py +++ b/codeclone/report/json_contract.py @@ -102,6 +102,8 @@ ] _OVERLOADED_MODULES_FAMILY = "overloaded_modules" +_COVERAGE_ADOPTION_FAMILY = "coverage_adoption" +_API_SURFACE_FAMILY = "api_surface" def _optional_str(value: object) -> str | None: @@ -376,6 +378,18 @@ def _collect_paths_from_metrics(metrics: Mapping[str, object]) -> set[str]: filepath = _optional_str(item_map.get("filepath")) if filepath is not None: paths.add(filepath) + coverage_adoption = _as_mapping(metrics.get(_COVERAGE_ADOPTION_FAMILY)) + for item in _as_sequence(coverage_adoption.get("items")): + item_map = _as_mapping(item) + filepath = _optional_str(item_map.get("filepath")) + if filepath is not None: + paths.add(filepath) + api_surface = _as_mapping(metrics.get(_API_SURFACE_FAMILY)) + for item in _as_sequence(api_surface.get("items")): + item_map = _as_mapping(item) + filepath = _optional_str(item_map.get("filepath")) + if filepath is not None: + paths.add(filepath) return paths @@ -723,6 +737,82 @@ def _normalize_suppressed_by( cohesion_summary = _as_mapping(cohesion.get("summary")) dead_code_summary = _as_mapping(dead_code.get("summary")) overloaded_modules_summary = _as_mapping(overloaded_modules.get("summary")) + coverage_adoption = _as_mapping(metrics_map.get(_COVERAGE_ADOPTION_FAMILY)) + coverage_adoption_summary = _as_mapping(coverage_adoption.get("summary")) + coverage_adoption_items = sorted( + ( + { + "module": str(item_map.get("module", "")).strip(), + "relative_path": _contract_path( + item_map.get("filepath", ""), + scan_root=scan_root, + )[0] + or "", + "callable_count": _as_int(item_map.get("callable_count")), + "params_total": _as_int(item_map.get("params_total")), + "params_annotated": _as_int(item_map.get("params_annotated")), + "param_permille": _as_int(item_map.get("param_permille")), + "returns_total": _as_int(item_map.get("returns_total")), + "returns_annotated": _as_int(item_map.get("returns_annotated")), + "return_permille": _as_int(item_map.get("return_permille")), + "any_annotation_count": _as_int(item_map.get("any_annotation_count")), + "public_symbol_total": _as_int(item_map.get("public_symbol_total")), + "public_symbol_documented": _as_int( + item_map.get("public_symbol_documented") + ), + "docstring_permille": _as_int(item_map.get("docstring_permille")), + } + for item in _as_sequence(coverage_adoption.get("items")) + for item_map in (_as_mapping(item),) + ), + key=lambda item: ( + item["relative_path"], + item["module"], + ), + ) + api_surface = _as_mapping(metrics_map.get(_API_SURFACE_FAMILY)) + api_surface_summary = _as_mapping(api_surface.get("summary")) + api_surface_items = sorted( + ( + { + "record_kind": str(item_map.get("record_kind", "symbol")), + "module": str(item_map.get("module", "")).strip(), + "relative_path": _contract_path( + item_map.get("filepath", ""), + scan_root=scan_root, + )[0] + or "", + "qualname": str(item_map.get("qualname", "")), + "start_line": _as_int(item_map.get("start_line")), + "end_line": _as_int(item_map.get("end_line")), + "symbol_kind": str(item_map.get("symbol_kind", "")), + "exported_via": _optional_str(item_map.get("exported_via")), + "params_total": _as_int(item_map.get("params_total")), + "params": [ + { + "name": str(param_map.get("name", "")), + "kind": str(param_map.get("kind", "")), + "has_default": bool(param_map.get("has_default")), + "annotated": bool(param_map.get("annotated")), + } + for param in _as_sequence(item_map.get("params")) + for param_map in (_as_mapping(param),) + ], + "returns_annotated": bool(item_map.get("returns_annotated")), + "change_kind": _optional_str(item_map.get("change_kind")), + "detail": _optional_str(item_map.get("detail")), + } + for item in _as_sequence(api_surface.get("items")) + for item_map in (_as_mapping(item),) + ), + key=lambda item: ( + item["relative_path"], + item["start_line"], + item["end_line"], + item["qualname"], + item["record_kind"], + ), + ) dead_high_confidence = sum( 1 for item in dead_items @@ -798,6 +888,64 @@ def _normalize_suppressed_by( "items": [], "items_truncated": False, }, + _COVERAGE_ADOPTION_FAMILY: { + "summary": { + "modules": len(coverage_adoption_items), + "params_total": _as_int(coverage_adoption_summary.get("params_total")), + "params_annotated": _as_int( + coverage_adoption_summary.get("params_annotated") + ), + "param_permille": _as_int( + coverage_adoption_summary.get("param_permille") + ), + "baseline_diff_available": bool( + coverage_adoption_summary.get("baseline_diff_available") + ), + "param_delta": _as_int(coverage_adoption_summary.get("param_delta")), + "returns_total": _as_int( + coverage_adoption_summary.get("returns_total") + ), + "returns_annotated": _as_int( + coverage_adoption_summary.get("returns_annotated") + ), + "return_permille": _as_int( + coverage_adoption_summary.get("return_permille") + ), + "return_delta": _as_int(coverage_adoption_summary.get("return_delta")), + "public_symbol_total": _as_int( + coverage_adoption_summary.get("public_symbol_total") + ), + "public_symbol_documented": _as_int( + coverage_adoption_summary.get("public_symbol_documented") + ), + "docstring_permille": _as_int( + coverage_adoption_summary.get("docstring_permille") + ), + "docstring_delta": _as_int( + coverage_adoption_summary.get("docstring_delta") + ), + "typing_any_count": _as_int( + coverage_adoption_summary.get("typing_any_count") + ), + }, + "items": coverage_adoption_items, + "items_truncated": False, + }, + _API_SURFACE_FAMILY: { + "summary": { + "enabled": bool(api_surface_summary.get("enabled")), + "baseline_diff_available": bool( + api_surface_summary.get("baseline_diff_available") + ), + "modules": _as_int(api_surface_summary.get("modules")), + "public_symbols": _as_int(api_surface_summary.get("public_symbols")), + "added": _as_int(api_surface_summary.get("added")), + "breaking": _as_int(api_surface_summary.get("breaking")), + "strict_types": bool(api_surface_summary.get("strict_types")), + }, + "items": api_surface_items, + "items_truncated": False, + }, _OVERLOADED_MODULES_FAMILY: { "summary": { "total": len(overloaded_module_items), diff --git a/codeclone/ui_messages.py b/codeclone/ui_messages.py index f19ed48..63d013a 100644 --- a/codeclone/ui_messages.py +++ b/codeclone/ui_messages.py @@ -93,6 +93,37 @@ "Exit with code 3 if new metrics violations appear relative to the\n" "metrics baseline." ) +HELP_TYPING_COVERAGE = ( + "Collect typing adoption coverage facts in full analysis mode.\nEnabled by default." +) +HELP_DOCSTRING_COVERAGE = ( + "Collect public docstring adoption coverage facts in full analysis mode.\n" + "Enabled by default." +) +HELP_API_SURFACE = ( + "Collect public API surface facts for baseline-aware compatibility review.\n" + "Disabled by default." +) +HELP_FAIL_ON_TYPING_REGRESSION = ( + "Exit with code 3 if typing adoption coverage regresses relative to the\n" + "metrics baseline." +) +HELP_FAIL_ON_DOCSTRING_REGRESSION = ( + "Exit with code 3 if public docstring coverage regresses relative to the\n" + "metrics baseline." +) +HELP_FAIL_ON_API_BREAK = ( + "Exit with code 3 if public API removals or signature breaks are detected\n" + "relative to the metrics baseline." +) +HELP_MIN_TYPING_COVERAGE = ( + "Exit with code 3 if parameter typing coverage falls below the threshold.\n" + "Threshold is a whole percent from 0 to 100." +) +HELP_MIN_DOCSTRING_COVERAGE = ( + "Exit with code 3 if public docstring coverage falls below the threshold.\n" + "Threshold is a whole percent from 0 to 100." +) HELP_CI = ( "Enable CI preset.\n" "Equivalent to: --fail-on-new --no-color --quiet.\n" @@ -536,6 +567,49 @@ def fmt_metrics_dead_code(count: int, *, suppressed: int = 0) -> str: ) +def _format_permille_pct(value: int) -> str: + return f"{value / 10.0:.1f}%" + + +def fmt_metrics_adoption( + *, + param_permille: int, + return_permille: int, + docstring_permille: int, + any_annotation_count: int, +) -> str: + parts = [ + f"params {_format_permille_pct(param_permille)}", + f"returns {_format_permille_pct(return_permille)}", + f"docstrings {_format_permille_pct(docstring_permille)}", + f"Any {_v(any_annotation_count)}", + ] + return f" {'Adoption':<{_L}}{' · '.join(parts)}" + + +def fmt_metrics_api_surface( + *, + public_symbols: int, + modules: int, + added: int, + breaking: int, +) -> str: + parts = [ + f"{_v(public_symbols, 'bold cyan')} symbols", + f"{_v(modules, 'bold cyan')} modules", + ] + if breaking > 0 or added > 0: + parts.append( + " / ".join( + [ + f"{_v(breaking, 'bold red')} breaking", + f"{_v(added, 'bold cyan')} added", + ] + ) + ) + return f" {'Public API':<{_L}}{' · '.join(parts)}" + + def fmt_metrics_overloaded_modules( *, candidates: int, diff --git a/docs/README.md b/docs/README.md index b8c2337..a5df1f1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -39,7 +39,7 @@ repository build: - [Core pipeline and invariants](book/05-core-pipeline.md) - [Baseline contract (schema v2.0)](book/06-baseline.md) - [Cache contract (schema v2.3)](book/07-cache.md) -- [Report contract (schema v2.4)](book/08-report.md) +- [Report contract (schema v2.5)](book/08-report.md) ## Interfaces diff --git a/docs/architecture.md b/docs/architecture.md index 01da554..5c96b06 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -144,7 +144,7 @@ gating decisions. Detected findings can be rendered as: - interactive HTML (`--html`), -- canonical JSON (`--json`, schema `2.4`), +- canonical JSON (`--json`, schema `2.5`), - deterministic text projection (`--text`), - deterministic Markdown projection (`--md`), - deterministic SARIF projection (`--sarif`). @@ -234,7 +234,7 @@ Security boundaries: Baseline comparison allows CI to fail **only on new clones**, enabling gradual architectural improvement. -Baseline files use a stable v2 contract (schema `2.0`, with compatibility +Baseline files use a stable v2 contract (current schema `2.1`, with compatibility support for major `1` legacy schema checks where applicable). Compatibility is checked by `schema_version`, `fingerprint_version`, `python_tag`, and `generator.name`, not package patch/minor version. diff --git a/docs/book/06-baseline.md b/docs/book/06-baseline.md index 46c7b6a..79d37e5 100644 --- a/docs/book/06-baseline.md +++ b/docs/book/06-baseline.md @@ -2,7 +2,7 @@ ## Purpose -Specify baseline schema v2, trust/compatibility checks, integrity hashing, and +Specify baseline schema v2.1, trust/compatibility checks, integrity hashing, and runtime behavior. ## Public surface @@ -17,7 +17,7 @@ runtime behavior. Canonical baseline shape: - Required top-level keys: `meta`, `clones` -- Optional top-level key: `metrics` (unified baseline flow) +- Optional top-level keys: `metrics`, `api_surface` (unified baseline flow) - `meta` required keys: `generator`, `schema_version`, `fingerprint_version`, `python_tag`, `created_at`, `payload_sha256` @@ -42,11 +42,22 @@ Compatibility gates (`verify_compatibility`): - `python_tag == current_python_tag()` - integrity verified via `payload_sha256` +Current runtime policy: + +- New clone baseline saves write schema `2.1`. +- Runtime still accepts `2.0` and `2.1` within baseline major `2`. + Embedded metrics contract: - Top-level `metrics` is allowed only for baseline schema `>= 2.0`. -- Clone baseline save preserves existing embedded `metrics` payload and - `meta.metrics_payload_sha256`. +- Clone baseline save preserves existing embedded `metrics` payload, + optional `api_surface` payload, and the corresponding + `meta.metrics_payload_sha256` / `meta.api_surface_payload_sha256` values. +- Embedded `api_surface` snapshots use a compact wire format: each symbol stores + `local_name` relative to its containing `module`, and each module row stores + `filepath` relative to the baseline directory when possible. Runtime + reconstructs canonical full qualnames and runtime filepaths in memory before + diffing. - The default runtime flow is unified: clone baseline and metrics baseline usually share the same `codeclone.baseline.json` file unless the metrics path is explicitly overridden. diff --git a/docs/book/08-report.md b/docs/book/08-report.md index fd4fb6f..60eb5b1 100644 --- a/docs/book/08-report.md +++ b/docs/book/08-report.md @@ -2,7 +2,7 @@ ## Purpose -Define report contracts in `2.0.0b5`: canonical JSON (`report_schema_version=2.4`) +Define report contracts in `2.0.0b5`: canonical JSON (`report_schema_version=2.5`) plus deterministic TXT/Markdown/SARIF projections. ## Public surface @@ -16,7 +16,7 @@ plus deterministic TXT/Markdown/SARIF projections. ## Data model -JSON report top-level (v2.4): +JSON report top-level (v2.5): - `report_schema_version` - `meta` @@ -39,6 +39,12 @@ Canonical report-only metrics additions: - `metrics.families.overloaded_modules` records project-relative module hotspot profiles and candidate classification for `Overloaded Modules` +- `metrics.families.coverage_adoption` records parameter coverage, return + coverage, public docstring coverage, and `Any` usage counts, plus compact + baseline deltas when a trusted metrics baseline is available +- `metrics.families.api_surface` records the current public symbol inventory + and compact baseline diff facts (`added`, `breaking`) when + `--api-surface` is enabled - the family is canonical report truth, but it does **not** participate in findings totals, health, gates, baseline NEW/KNOWN semantics, or SARIF in `b4` @@ -50,6 +56,14 @@ Canonical report-only metrics additions: - the layer may later become scoring only after validation and explicit health-model documentation updates +Coverage/API role split: + +- `coverage_adoption` is a canonical metrics family, not a style linter. It + reports observable adoption facts only. +- `api_surface` is a canonical metrics/gating family, not a second finding + engine. It reports public API inventory plus baseline-diff facts when the + run opted into API collection. + Canonical vs non-canonical split: - Canonical: `report_schema_version`, `meta`, `inventory`, `findings`, `metrics` diff --git a/docs/book/09-cli.md b/docs/book/09-cli.md index 4a6df83..fb0f467 100644 --- a/docs/book/09-cli.md +++ b/docs/book/09-cli.md @@ -27,8 +27,29 @@ Summary metrics: - function/block/segment groups - suppressed segment groups - dead-code active/suppressed status in metrics line +- adoption coverage in the normal `Metrics` block: + parameter typing, return typing, public docstrings, and `Any` count +- public API surface in the normal `Metrics` block when `api_surface` was + collected: symbol/module counts plus added/breaking deltas when a trusted + metrics baseline is available - new vs baseline +Metrics-related CLI gates: + +- threshold gates: + `--fail-complexity`, `--fail-coupling`, `--fail-cohesion`, `--fail-health` +- coverage threshold gates: + `--min-typing-coverage`, `--min-docstring-coverage` +- baseline-aware delta gates: + `--fail-on-new-metrics`, + `--fail-on-typing-regression`, + `--fail-on-docstring-regression`, + `--fail-on-api-break` +- update mode: + `--update-metrics-baseline` +- opt-in metrics family: + `--api-surface` + Refs: - `codeclone/_cli_summary.py:_print_summary` @@ -58,6 +79,19 @@ Refs: - `N found (M suppressed)` when active dead-code items exist - `✔ clean` when both active and suppressed are zero - `✔ clean (M suppressed)` when active is zero but suppressed > 0 +- The normal rich `Metrics` block includes: + - `Adoption` when adoption coverage facts were computed + - `Public API` when `api_surface` facts were computed +- Quiet compact metrics output stays on the existing fixed one-line summary and + does not expand adoption/API detail. +- Typing/docstring adoption metrics are computed by default in full mode. +- `--api-surface` is opt-in in normal runs, but runtime auto-enables it when + `--fail-on-api-break` or `--update-metrics-baseline` needs a public API + snapshot. +- `--fail-on-typing-regression` / `--fail-on-docstring-regression` require a + metrics baseline that already contains adoption coverage data. +- `--fail-on-api-break` requires a metrics baseline that already contains + `api_surface` data. Refs: @@ -91,20 +125,21 @@ Refs: ## Failure modes -| Condition | User-facing category | Exit | -|----------------------------------------------|----------------------|------| -| Invalid CLI flag | contract | 2 | -| Invalid output extension/path | contract | 2 | -| `--open-html-report` without `--html` | contract | 2 | -| `--timestamped-report-paths` without reports | contract | 2 | -| `--changed-only` without diff source | contract | 2 | -| `--diff-against` without `--changed-only` | contract | 2 | -| `--diff-against` + `--paths-from-git-diff` | contract | 2 | -| Baseline untrusted in CI/gating | contract | 2 | -| Unreadable source in CI/gating | contract | 2 | -| New clones with `--fail-on-new` | gating | 3 | -| Threshold exceeded | gating | 3 | -| Unexpected exception | internal | 5 | +| Condition | User-facing category | Exit | +|---------------------------------------------------------------------------|----------------------|------| +| Invalid CLI flag | contract | 2 | +| Invalid output extension/path | contract | 2 | +| `--open-html-report` without `--html` | contract | 2 | +| `--timestamped-report-paths` without reports | contract | 2 | +| `--changed-only` without diff source | contract | 2 | +| `--diff-against` without `--changed-only` | contract | 2 | +| `--diff-against` + `--paths-from-git-diff` | contract | 2 | +| Baseline untrusted in CI/gating | contract | 2 | +| Coverage/API regression gate without required metrics-baseline capability | contract | 2 | +| Unreadable source in CI/gating | contract | 2 | +| New clones with `--fail-on-new` | gating | 3 | +| Threshold exceeded | gating | 3 | +| Unexpected exception | internal | 5 | ## Determinism / canonicalization diff --git a/docs/book/13-testing-as-spec.md b/docs/book/13-testing-as-spec.md index c150d7a..a5fbcd2 100644 --- a/docs/book/13-testing-as-spec.md +++ b/docs/book/13-testing-as-spec.md @@ -34,7 +34,7 @@ The following matrix is treated as executable contract: | Baseline schema/integrity/compat gates | `tests/test_baseline.py` | | Cache v2.3 fail-open + status mapping | `tests/test_cache.py`, `tests/test_cli_inprocess.py::test_cli_reports_cache_too_large_respects_max_size_flag` | | Exit code categories and markers | `tests/test_cli_unit.py`, `tests/test_cli_inprocess.py` | -| Report schema v2.4 canonical/derived/integrity + JSON/TXT/MD/SARIF projections | `tests/test_report.py`, `tests/test_report_contract_coverage.py`, `tests/test_report_branch_invariants.py` | +| Report schema v2.5 canonical/derived/integrity + JSON/TXT/MD/SARIF projections | `tests/test_report.py`, `tests/test_report_contract_coverage.py`, `tests/test_report_branch_invariants.py` | | HTML render-only explainability + escaping | `tests/test_html_report.py` | | Scanner traversal safety | `tests/test_scanner_extra.py`, `tests/test_security.py` | diff --git a/docs/book/14-compatibility-and-versioning.md b/docs/book/14-compatibility-and-versioning.md index 2a20277..55ba2db 100644 --- a/docs/book/14-compatibility-and-versioning.md +++ b/docs/book/14-compatibility-and-versioning.md @@ -18,11 +18,11 @@ compatibility is enforced. Current contract versions: -- `BASELINE_SCHEMA_VERSION = "2.0"` +- `BASELINE_SCHEMA_VERSION = "2.1"` - `BASELINE_FINGERPRINT_VERSION = "1"` - `CACHE_VERSION = "2.3"` -- `REPORT_SCHEMA_VERSION = "2.4"` -- `METRICS_BASELINE_SCHEMA_VERSION = "1.0"` (used only when metrics are stored +- `REPORT_SCHEMA_VERSION = "2.5"` +- `METRICS_BASELINE_SCHEMA_VERSION = "1.2"` (used only when metrics are stored in a dedicated metrics-baseline file instead of the default unified baseline) Refs: @@ -79,8 +79,27 @@ Version bump rules: Baseline compatibility rules: - Runtime accepts baseline schema majors `1` and `2` with supported minors. -- Runtime writes current schema (`2.0`) on new/updated baseline saves. +- Runtime writes current schema (`2.1`) on new/updated baseline saves. - Embedded top-level `metrics` is valid only for baseline schema `>= 2.0`. +- Unified clone baselines may also embed top-level `api_surface` when metrics + baseline data is stored in the same file. +- Embedded and standalone `api_surface` snapshots now use compact symbol wire + layout (`local_name` relative to `module`, `filepath` relative to the + baseline directory when possible) while runtime reconstructs full canonical + qualnames and runtime filepaths before comparison. This is a schema change + for baseline `2.1` / metrics-baseline `1.2`, not a silent serialization + detail. +- Capability-sensitive metrics gates (for example adoption regression or API + break gating) must check for the required embedded data, not only the clone + baseline schema version. + +Metrics-baseline compatibility rules: + +- Runtime writes standalone metrics-baseline schema `1.2`. +- Runtime accepts standalone metrics-baseline `1.1` and `1.2`. +- When metrics are embedded into the unified clone baseline, the embedded + metrics section follows the clone baseline schema compatibility window + instead (`2.0` and `2.1` in the current runtime). Baseline regeneration rules: @@ -149,7 +168,7 @@ Refs: ## Locked by tests -- `tests/test_baseline.py::test_baseline_verify_schema_incompatibilities[schema_too_new]` +- `tests/test_baseline.py::test_baseline_verify_schema_incompatibilities` - `tests/test_baseline.py::test_baseline_verify_schema_incompatibilities[schema_major_mismatch]` - `tests/test_baseline.py::test_baseline_verify_fingerprint_mismatch` - `tests/test_cache.py::test_cache_v_field_version_mismatch_warns` diff --git a/docs/book/15-metrics-and-quality-gates.md b/docs/book/15-metrics-and-quality-gates.md index b3f3a9b..521e61a 100644 --- a/docs/book/15-metrics-and-quality-gates.md +++ b/docs/book/15-metrics-and-quality-gates.md @@ -18,12 +18,19 @@ Metrics gate inputs: - threshold gates: `--fail-complexity`, `--fail-coupling`, `--fail-cohesion`, `--fail-health` +- adoption threshold gates: + `--min-typing-coverage`, `--min-docstring-coverage` - boolean structural gates: `--fail-cycles`, `--fail-dead-code` -- delta gate: - `--fail-on-new-metrics` +- baseline-aware delta gates: + `--fail-on-new-metrics`, + `--fail-on-typing-regression`, + `--fail-on-docstring-regression`, + `--fail-on-api-break` - baseline update: `--update-metrics-baseline` +- opt-in metrics family: + `--api-surface` Modes: @@ -53,6 +60,13 @@ Refs: `skip_dead_code=true`, `skip_dependencies=true`. - `--fail-dead-code` forces dead-code analysis on (even if metrics are skipped). - `--fail-cycles` forces dependency analysis on (even if metrics are skipped). +- Type/docstring adoption metrics are computed by default in full mode. +- `--api-surface` is opt-in in normal runs, but runtime auto-enables it when + `--fail-on-api-break` or `--update-metrics-baseline` needs a public API + snapshot. +- In the normal CLI `Metrics` block, adoption coverage is shown whenever metrics + are computed, and the public API surface line appears when `api_surface` + facts were collected. - `--update-baseline` in full mode implies metrics-baseline update in the same run. - If metrics baseline path equals clone baseline path and clone baseline file is @@ -60,6 +74,10 @@ Refs: embedded metrics can be written safely. - `--fail-on-new-metrics` requires trusted metrics baseline unless baseline is being updated in the same run. +- `--fail-on-typing-regression` / `--fail-on-docstring-regression` require a + metrics baseline that already contains adoption coverage data. +- `--fail-on-api-break` requires a metrics baseline that already contains + `api_surface` data. - In CI mode, if metrics baseline was loaded and trusted, runtime enables `fail_on_new_metrics=true`. @@ -74,7 +92,8 @@ Refs: - Metrics diff is computed only when: metrics were computed and metrics baseline is trusted. - Metric gate reasons are emitted in deterministic order: - threshold checks -> cycles/dead/health -> NEW-vs-baseline diffs. + threshold checks -> cycles/dead/health -> NEW-vs-baseline diffs -> + adoption/API baseline diffs. - Metric gate reasons are namespaced as `metric:*` in gate output. Refs: @@ -84,12 +103,13 @@ Refs: ## Failure modes -| Condition | Behavior | -|------------------------------------------------------------|--------------------------| -| `--skip-metrics` with metrics flags | Contract error, exit `2` | -| `--fail-on-new-metrics` without trusted baseline | Contract error, exit `2` | -| `--update-metrics-baseline` when metrics were not computed | Contract error, exit `2` | -| Threshold breach or NEW-vs-baseline metric regressions | Gating failure, exit `3` | +| Condition | Behavior | +|-------------------------------------------------------------|--------------------------| +| `--skip-metrics` with metrics flags | Contract error, exit `2` | +| `--fail-on-new-metrics` without trusted baseline | Contract error, exit `2` | +| Coverage/API regression gate without required baseline data | Contract error, exit `2` | +| `--update-metrics-baseline` when metrics were not computed | Contract error, exit `2` | +| Threshold breach or NEW-vs-baseline metric regressions | Gating failure, exit `3` | ## Determinism / canonicalization diff --git a/docs/book/20-mcp-interface.md b/docs/book/20-mcp-interface.md index 834f8dc..4427caf 100644 --- a/docs/book/20-mcp-interface.md +++ b/docs/book/20-mcp-interface.md @@ -67,7 +67,9 @@ Current server characteristics: - flattened `inventory` (`files`, `lines`, `functions`, `classes`) - flattened `findings` (`total`, `new`, `known`, `by_family`, `production`, `new_by_source_kind`) - - flattened `diff` (`new_clones`, `health_delta`) + - flattened `diff` (`new_clones`, `health_delta`, + `typing_param_permille_delta`, `typing_return_permille_delta`, + `docstring_permille_delta`, `api_breaking_changes`, `new_api_symbols`) - `warnings`, `failures` - `analyze_changed_paths` is intentionally more compact than `get_run_summary`: it returns `changed_files`, compact `baseline`, `focus`, `health_scope`, @@ -102,8 +104,8 @@ Current tool set (`21` tools): | Tool | Key parameters | Purpose | |--------------------------|-----------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------| -| `analyze_repository` | absolute `root`, `analysis_mode`, thresholds, cache/baseline paths | Full analysis → compact summary; then `get_run_summary` or `get_production_triage` | -| `analyze_changed_paths` | absolute `root`, `changed_paths` or `git_diff_ref`, `analysis_mode` | Diff-aware analysis → compact changed-files snapshot | +| `analyze_repository` | absolute `root`, `analysis_mode`, thresholds, `api_surface`, cache/baseline paths | Full analysis → compact summary; then `get_run_summary` or `get_production_triage` | +| `analyze_changed_paths` | absolute `root`, `changed_paths` or `git_diff_ref`, `analysis_mode`, `api_surface` | Diff-aware analysis → compact changed-files snapshot | | `get_run_summary` | `run_id` | Cheapest run snapshot: health, findings, baseline, inventory, active thresholds | | `get_production_triage` | `run_id`, `max_hotspots`, `max_suggestions` | Production-first view: health, hotspots, suggestions, active thresholds | | `help` | `topic`, `detail` | Semantic guide for workflow, analysis profile, baseline, suppressions, review state, changed-scope | @@ -136,6 +138,9 @@ Recommended workflow: 4. `get_finding` → `get_remediation` 5. `generate_pr_summary(format="markdown")` +`metrics_detail` families currently include canonical health/quality families +plus `overloaded_modules`, `coverage_adoption`, and `api_surface`. + For analysis sensitivity, the intended model is: 1. start with repo defaults or `pyproject`-resolved thresholds diff --git a/docs/book/appendix/b-schema-layouts.md b/docs/book/appendix/b-schema-layouts.md index 3c7e209..86d3d13 100644 --- a/docs/book/appendix/b-schema-layouts.md +++ b/docs/book/appendix/b-schema-layouts.md @@ -4,24 +4,90 @@ Compact structural layouts for baseline/cache/report contracts in `2.0.0b5`. -## Baseline schema (`2.0`) +## Baseline schema (`2.1`) ```json { "meta": { "generator": { "name": "codeclone", "version": "2.0.0b5" }, - "schema_version": "2.0", + "schema_version": "2.1", "fingerprint_version": "1", "python_tag": "cp313", "created_at": "2026-03-11T00:00:00Z", "payload_sha256": "...", - "metrics_payload_sha256": "..." + "metrics_payload_sha256": "...", + "api_surface_payload_sha256": "..." }, "clones": { "functions": ["|"], "blocks": ["|||"] }, - "metrics": { "...": "optional embedded metrics snapshot" } + "metrics": { "...": "optional embedded metrics snapshot" }, + "api_surface": { "...": "optional embedded public API snapshot" } +} +``` + +Compact embedded `api_surface` symbol layout: + +```json +{ + "module": "pkg.mod", + "filepath": "pkg/mod.py", + "symbols": [ + { + "local_name": "PublicClass.method", + "kind": "method", + "start_line": 10, + "end_line": 14, + "params": [], + "returns_hash": "", + "exported_via": "name" + } + ] +} +``` + +Notes: + +- `local_name` is stored on disk to avoid repeating the containing module path. +- `filepath` is stored as a baseline-directory-relative wire path when + possible, rather than as a machine-local absolute path. +- Runtime reconstructs canonical full qualnames as `module:local_name` before + API-surface diffing and restores runtime filepaths from the wire path. + +## Standalone metrics-baseline schema (`1.2`) + +```json +{ + "meta": { + "generator": { "name": "codeclone", "version": "2.0.0b5" }, + "schema_version": "1.2", + "python_tag": "cp313", + "created_at": "2026-03-11T00:00:00Z", + "payload_sha256": "...", + "api_surface_payload_sha256": "..." + }, + "metrics": { "...": "metrics snapshot" }, + "api_surface": { + "modules": [ + { + "module": "pkg.mod", + "filepath": "pkg/mod.py", + "all_declared": [], + "symbols": [ + { + "local_name": "run", + "kind": "function", + "start_line": 10, + "end_line": 14, + "params": [], + "returns_hash": "", + "exported_via": "name" + } + ] + } + ] + } } ``` @@ -77,11 +143,11 @@ Notes: - `u` row decoder accepts both legacy 11-column rows and canonical 17-column rows (legacy rows map new structural fields to neutral defaults). -## Report schema (`2.4`) +## Report schema (`2.5`) ```json { - "report_schema_version": "2.4", + "report_schema_version": "2.5", "meta": { "codeclone_version": "2.0.0b5", "project_name": "codeclone", @@ -180,6 +246,27 @@ Notes: "population_status": "limited", "top_score": 0.0, "average_score": 0.0 + }, + "coverage_adoption": { + "modules": 0, + "params_total": 0, + "params_annotated": 0, + "param_permille": 0, + "returns_total": 0, + "returns_annotated": 0, + "return_permille": 0, + "public_symbol_total": 0, + "public_symbol_documented": 0, + "docstring_permille": 0, + "typing_any_count": 0 + }, + "api_surface": { + "enabled": false, + "modules": 0, + "public_symbols": 0, + "added": 0, + "breaking": 0, + "strict_types": false } }, "families": { @@ -215,6 +302,38 @@ Notes: }, "items": [] }, + "coverage_adoption": { + "summary": { + "modules": 0, + "params_total": 0, + "params_annotated": 0, + "param_permille": 0, + "baseline_diff_available": false, + "param_delta": 0, + "returns_total": 0, + "returns_annotated": 0, + "return_permille": 0, + "return_delta": 0, + "public_symbol_total": 0, + "public_symbol_documented": 0, + "docstring_permille": 0, + "docstring_delta": 0, + "typing_any_count": 0 + }, + "items": [] + }, + "api_surface": { + "summary": { + "enabled": false, + "baseline_diff_available": false, + "modules": 0, + "public_symbols": 0, + "added": 0, + "breaking": 0, + "strict_types": false + }, + "items": [] + }, "health": {} } }, @@ -274,7 +393,7 @@ Notes: ```text # CodeClone Report - Markdown schema: 1.0 -- Source report schema: 2.4 +- Source report schema: 2.5 ... ## Overview ## Inventory @@ -360,7 +479,7 @@ Notes: ], "properties": { "profileVersion": "1.0", - "reportSchemaVersion": "2.3" + "reportSchemaVersion": "2.5" }, "results": [ { diff --git a/docs/mcp.md b/docs/mcp.md index 190cba1..a438644 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -123,10 +123,14 @@ run-scoped URI templates. `new_by_source_kind` attributes new findings without widening the payload. - When baseline comparison is untrusted, summary and triage also expose `baseline.compared_without_valid_baseline` plus baseline/runtime python tags. +- Summary `diff` also carries compact adoption/API deltas: + `typing_param_permille_delta`, `typing_return_permille_delta`, + `docstring_permille_delta`, `api_breaking_changes`, and `new_api_symbols`. - Run IDs are 8-char hex handles; finding IDs are short prefixed forms. Both accept the full canonical form as input. - `metrics_detail(family="overloaded_modules")` exposes the report-only module-hotspot layer without turning it into findings or gate data. +- `metrics_detail` also accepts `coverage_adoption` and `api_surface`. - `help(topic=...)` is static: meaning, anti-patterns, next step, doc links. - Start with repo defaults or `pyproject`-resolved thresholds, then lower them only for an explicit higher-sensitivity exploratory pass. @@ -187,7 +191,7 @@ analyze_repository → get_production_triage ### Conservative first pass, then deeper review ``` -analyze_repository +analyze_repository(api_surface=true) # when you need API inventory/diff → help(topic="analysis_profile") when you need finer-grained local review → analyze_repository(min_loc=..., min_stmt=..., ...) as an explicit higher-sensitivity pass → compare_runs diff --git a/tests/fixtures/golden_v2/pyproject_defaults/golden_expected_cli_snapshot.json b/tests/fixtures/golden_v2/pyproject_defaults/golden_expected_cli_snapshot.json index 17a35ca..1417c3a 100644 --- a/tests/fixtures/golden_v2/pyproject_defaults/golden_expected_cli_snapshot.json +++ b/tests/fixtures/golden_v2/pyproject_defaults/golden_expected_cli_snapshot.json @@ -2,7 +2,7 @@ "meta": { "python_tag": "cp313" }, - "report_schema_version": "2.4", + "report_schema_version": "2.5", "project_name": "pyproject_defaults", "scan_root": ".", "baseline_status": "missing", diff --git a/tests/test_adoption.py b/tests/test_adoption.py new file mode 100644 index 0000000..76ed21b --- /dev/null +++ b/tests/test_adoption.py @@ -0,0 +1,197 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Den Rozhnovskiy + +from __future__ import annotations + +import ast + +from codeclone import extractor +from codeclone.metrics import _visibility as visibility_mod +from codeclone.metrics import adoption as adoption_mod +from codeclone.metrics._visibility import build_module_visibility +from codeclone.metrics.adoption import collect_module_adoption +from codeclone.qualnames import QualnameCollector + + +def _tree_collector_and_imports( + source: str, + *, + module_name: str, +) -> tuple[ast.Module, QualnameCollector, frozenset[str]]: + tree = ast.parse(source) + collector = QualnameCollector() + collector.visit(tree) + walk = extractor._collect_module_walk_data( + tree=tree, + module_name=module_name, + collector=collector, + collect_referenced_names=True, + ) + return tree, collector, walk.import_names + + +def test_build_module_visibility_supports_strict_dunder_all_for_private_modules() -> ( + None +): + tree, collector, import_names = _tree_collector_and_imports( + """ +__all__ = ["public_fn", "PublicClass"] + +def public_fn(): + return 1 + +def _private_fn(): + return 2 + +class PublicClass: + pass +""", + module_name="pkg._internal", + ) + visibility = build_module_visibility( + tree=tree, + module_name="pkg._internal", + collector=collector, + imported_names=import_names, + ) + + assert visibility.is_public_module is False + assert visibility.strict_exports is True + assert visibility.exported_names == frozenset({"PublicClass", "public_fn"}) + assert visibility.exported_via("public_fn") == "all" + assert visibility.exported_via("_private_fn") is None + + +def test_collect_module_adoption_counts_annotations_docstrings_and_any() -> None: + tree, collector, import_names = _tree_collector_and_imports( + """ +from typing import Any + +__all__ = ["public", "Public"] + +def public(a: int, b) -> Any: + \"\"\"Public function.\"\"\" + return b + +def _private(hidden): + return hidden + +class Public: + \"\"\"Public class.\"\"\" + + def method(self, item: Any) -> None: + \"\"\"Public method.\"\"\" + return None + + def _hidden(self, value: int) -> None: + return None +""", + module_name="pkg.mod", + ) + + typing_coverage, docstring_coverage = collect_module_adoption( + tree=tree, + module_name="pkg.mod", + filepath="pkg/mod.py", + collector=collector, + imported_names=import_names, + ) + + assert ( + typing_coverage.module, + typing_coverage.filepath, + typing_coverage.callable_count, + typing_coverage.params_total, + typing_coverage.params_annotated, + typing_coverage.returns_total, + typing_coverage.returns_annotated, + typing_coverage.any_annotation_count, + ) == ("pkg.mod", "pkg/mod.py", 4, 5, 3, 4, 3, 2) + + assert ( + docstring_coverage.module, + docstring_coverage.filepath, + docstring_coverage.public_symbol_total, + docstring_coverage.public_symbol_documented, + ) == ("pkg.mod", "pkg/mod.py", 3, 3) + + +def test_visibility_helpers_cover_private_modules_and_declared_all_edges() -> None: + tree, collector, import_names = _tree_collector_and_imports( + """ +items: list[str] = [] +_private = 1 +""", + module_name="pkg._internal", + ) + visibility = build_module_visibility( + tree=tree, + module_name="pkg._internal", + collector=collector, + imported_names=import_names, + ) + assert visibility.exported_names == frozenset() + + strict_tree = ast.parse( + """ +__all__: list[str] = ["Public", "CONST", ""] +(foo, [bar, baz]) = (1, [2, 3]) +CONST: int = 1 + +class Public: + pass +""" + ) + strict_collector = QualnameCollector() + strict_collector.visit(strict_tree) + declared_all = visibility_mod._declared_dunder_all(strict_tree) + top_level_names = visibility_mod._top_level_declared_names( + tree=strict_tree, + collector=strict_collector, + ) + + assert declared_all == ("CONST", "Public") + assert {"foo", "bar", "baz", "CONST", "Public"}.issubset(top_level_names) + assert visibility_mod._literal_string_sequence(ast.Constant(value="x")) is None + assert ( + visibility_mod._literal_string_sequence(ast.List(elts=[ast.Constant(value=1)])) + is None + ) + assign = ast.parse("(left, [right, tail]) = values").body[0] + assert isinstance(assign, ast.Assign) + assert visibility_mod._assigned_names(assign.targets[0]) == { + "left", + "right", + "tail", + } + + +def test_adoption_helper_rows_and_any_helpers_cover_method_and_variants() -> None: + function = ast.parse( + """ +def method( + self, + a: int, + /, + b, + *args: typing.Any, + c: int, + **kwargs: typing.Any, +) -> typing.Any | None: + return None +""" + ).body[0] + assert isinstance(function, (ast.FunctionDef, ast.AsyncFunctionDef)) + + rows = adoption_mod._function_param_rows(node=function, is_method=True) + assert [name for name, _annotation in rows] == ["a", "b", "args", "c", "kwargs"] + + typing_any = ast.parse("typing.Any", mode="eval").body + union_any = ast.parse("typing.Any | int", mode="eval").body + assert adoption_mod._is_any_annotation(typing_any) is True + assert adoption_mod._is_any_annotation(union_any) is True + assert adoption_mod._attribute_name(typing_any) == "typing.Any" + assert adoption_mod._attribute_name(ast.Constant(value=1)) is None diff --git a/tests/test_api_surface.py b/tests/test_api_surface.py new file mode 100644 index 0000000..8190a7b --- /dev/null +++ b/tests/test_api_surface.py @@ -0,0 +1,405 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Den Rozhnovskiy + +from __future__ import annotations + +import ast +from typing import Literal, cast + +from codeclone import extractor +from codeclone.metrics import api_surface as api_surface_mod +from codeclone.metrics._visibility import ModuleVisibility +from codeclone.metrics.api_surface import ( + collect_module_api_surface, + compare_api_surfaces, +) +from codeclone.models import ( + ApiParamSpec, + ApiSurfaceSnapshot, + ModuleApiSurface, + PublicSymbol, +) +from codeclone.qualnames import QualnameCollector + + +def _tree_collector_and_imports( + source: str, + *, + module_name: str, +) -> tuple[ast.Module, QualnameCollector, frozenset[str]]: + tree = ast.parse(source) + collector = QualnameCollector() + collector.visit(tree) + walk = extractor._collect_module_walk_data( + tree=tree, + module_name=module_name, + collector=collector, + collect_referenced_names=True, + ) + return tree, collector, walk.import_names + + +def test_collect_module_api_surface_skips_self_and_collects_public_symbols() -> None: + tree, collector, import_names = _tree_collector_and_imports( + """ +__all__ = ["run", "Public", "VALUE"] + +def run(value: int, *, enabled: bool = True) -> int: + return value + +class Public: + def __init__(self, dep, *, lazy: bool = False) -> None: + self.dep = dep + + def method(self, item: str) -> None: + return None + + def _hidden(self, value: int) -> None: + return None + +VALUE = 1 +""", + module_name="pkg.mod", + ) + + surface = collect_module_api_surface( + tree=tree, + module_name="pkg.mod", + filepath="pkg/mod.py", + collector=collector, + imported_names=import_names, + ) + + assert surface is not None + assert surface.module == "pkg.mod" + assert [symbol.qualname for symbol in surface.symbols] == [ + "pkg.mod:Public", + "pkg.mod:Public.__init__", + "pkg.mod:Public.method", + "pkg.mod:VALUE", + "pkg.mod:run", + ] + init_symbol = next( + symbol + for symbol in surface.symbols + if symbol.qualname == "pkg.mod:Public.__init__" + ) + method_symbol = next( + symbol + for symbol in surface.symbols + if symbol.qualname == "pkg.mod:Public.method" + ) + assert [param.name for param in init_symbol.params] == ["dep", "lazy"] + assert [param.name for param in method_symbol.params] == ["item"] + + +def test_compare_api_surfaces_reports_added_removed_and_signature_breaks() -> None: + baseline = ApiSurfaceSnapshot( + modules=( + ModuleApiSurface( + module="pkg.mod", + filepath="pkg/mod.py", + symbols=( + PublicSymbol( + qualname="pkg.mod:run", + kind="function", + start_line=1, + end_line=3, + params=( + ApiParamSpec( + name="value", + kind="pos_or_kw", + has_default=False, + ), + ApiParamSpec( + name="limit", + kind="pos_or_kw", + has_default=True, + ), + ), + ), + PublicSymbol( + qualname="pkg.mod:gone", + kind="function", + start_line=5, + end_line=6, + ), + PublicSymbol( + qualname="pkg.mod:Public.method", + kind="method", + start_line=10, + end_line=12, + params=( + ApiParamSpec( + name="enabled", + kind="kw_only", + has_default=True, + ), + ), + ), + ), + ), + ) + ) + current = ApiSurfaceSnapshot( + modules=( + ModuleApiSurface( + module="pkg.mod", + filepath="pkg/mod.py", + symbols=( + PublicSymbol( + qualname="pkg.mod:added", + kind="function", + start_line=20, + end_line=21, + ), + PublicSymbol( + qualname="pkg.mod:run", + kind="function", + start_line=1, + end_line=3, + params=( + ApiParamSpec( + name="value", + kind="pos_or_kw", + has_default=False, + ), + ApiParamSpec( + name="amount", + kind="pos_or_kw", + has_default=True, + ), + ), + ), + PublicSymbol( + qualname="pkg.mod:Public.method", + kind="method", + start_line=10, + end_line=12, + params=( + ApiParamSpec( + name="enabled", + kind="kw_only", + has_default=False, + ), + ), + ), + ), + ), + ) + ) + + added, breaking = compare_api_surfaces( + baseline=baseline, + current=current, + strict_types=False, + ) + + assert added == ("pkg.mod:added",) + assert [(item.qualname, item.change_kind) for item in breaking] == [ + ("pkg.mod:run", "signature_break"), + ("pkg.mod:gone", "removed"), + ("pkg.mod:Public.method", "signature_break"), + ] + assert breaking[0].detail == "Renamed public parameter limit to amount." + assert breaking[1].detail == "Removed from the public API surface." + assert breaking[2].detail == "Parameter enabled became required." + + +def _public_symbol( + qualname: str, + kind: Literal["function", "class", "method", "constant"], + *, + params: tuple[ApiParamSpec, ...] = (), + returns_hash: str = "", +) -> PublicSymbol: + return PublicSymbol( + qualname=qualname, + kind=kind, + start_line=1, + end_line=1, + params=params, + returns_hash=returns_hash, + ) + + +def test_collect_module_api_surface_skips_private_or_empty_modules() -> None: + private_tree, private_collector, private_imports = _tree_collector_and_imports( + """ +def hidden(): + return 1 +""", + module_name="pkg._internal", + ) + assert ( + collect_module_api_surface( + tree=private_tree, + module_name="pkg._internal", + filepath="pkg/_internal.py", + collector=private_collector, + imported_names=private_imports, + ) + is None + ) + + empty_tree, empty_collector, empty_imports = _tree_collector_and_imports( + """ +def _hidden(): + return 1 +""", + module_name="pkg.public", + ) + assert ( + collect_module_api_surface( + tree=empty_tree, + module_name="pkg.public", + filepath="pkg/public.py", + collector=empty_collector, + imported_names=empty_imports, + ) + is None + ) + + +def test_api_surface_helpers_cover_constant_symbols_and_break_variants() -> None: + visibility = ModuleVisibility( + module_name="pkg.mod", + exported_names=frozenset({"CONST", "Public"}), + all_declared=("CONST", "Public"), + is_public_module=True, + ) + annassign_tree = ast.parse("CONST: int = 1") + constant_rows = api_surface_mod._public_constant_rows( + tree=annassign_tree, + visibility=visibility, + ) + assert constant_rows == (("CONST", 1, 1),) + assert ( + api_surface_mod._build_public_symbol( + module_name="pkg.mod", + export_name="missing", + local_name="missing", + kind="constant", + start_line=1, + end_line=1, + visibility=visibility, + ) + is None + ) + outer_class = ast.parse( + """ +class Outer: + class Inner: + pass +""" + ).body[0] + assert isinstance(outer_class, ast.ClassDef) + nested_class = outer_class.body[0] + assert isinstance(nested_class, ast.ClassDef) + assert ( + api_surface_mod._class_api_symbol( + module_name="pkg.mod", + class_qualname="Outer.Inner", + class_node=nested_class, + visibility=visibility, + ) + is None + ) + + method = ast.parse( + """ +def run(self, a: int, /, b, *args: str, c: int, **kwargs: bytes) -> int: + return 1 +""" + ).body[0] + assert isinstance(method, (ast.FunctionDef, ast.AsyncFunctionDef)) + params = api_surface_mod._parameter_specs(node=method, is_method=True) + assert [param.name for param in params] == ["a", "b", "args", "c", "kwargs"] + + class_before = _public_symbol("pkg.mod:Thing", "class") + class_after = _public_symbol("pkg.mod:Thing", "constant") + assert ( + api_surface_mod._signature_break_detail( + baseline_symbol=class_before, + current_symbol=class_after, + strict_types=False, + ) + == "Changed public symbol kind from class to constant." + ) + assert ( + api_surface_mod._signature_break_detail( + baseline_symbol=class_before, + current_symbol=_public_symbol("pkg.mod:Thing", "class"), + strict_types=False, + ) + is None + ) + + baseline_param = _public_symbol( + "pkg.mod:run", + "function", + params=(ApiParamSpec(name="value", kind="kw_only", has_default=False),), + ) + current_param_kind = _public_symbol( + "pkg.mod:run", + "function", + params=(ApiParamSpec(name="value", kind="pos_or_kw", has_default=False),), + ) + current_param_type = _public_symbol( + "pkg.mod:run", + "function", + params=( + ApiParamSpec( + name="value", + kind="kw_only", + has_default=False, + annotation_hash="str", + ), + ), + ) + baseline_typed = _public_symbol( + "pkg.mod:run", + "function", + params=( + ApiParamSpec( + name="value", + kind="kw_only", + has_default=False, + annotation_hash="int", + ), + ), + returns_hash="int", + ) + current_return_type = _public_symbol( + "pkg.mod:run", + "function", + params=baseline_typed.params, + returns_hash="str", + ) + assert "Changed parameter kind" in cast( + str, + api_surface_mod._signature_break_detail( + baseline_symbol=baseline_param, + current_symbol=current_param_kind, + strict_types=False, + ), + ) + assert "Changed type annotation" in cast( + str, + api_surface_mod._signature_break_detail( + baseline_symbol=baseline_typed, + current_symbol=current_param_type, + strict_types=True, + ), + ) + assert ( + api_surface_mod._signature_break_detail( + baseline_symbol=baseline_typed, + current_symbol=current_return_type, + strict_types=True, + ) + == "Changed return annotation." + ) diff --git a/tests/test_baseline.py b/tests/test_baseline.py index f56ab1f..859b8c1 100644 --- a/tests/test_baseline.py +++ b/tests/test_baseline.py @@ -397,9 +397,10 @@ def test_baseline_verify_generator_mismatch(tmp_path: Path) -> None: ("schema_version", "error_match"), [ ("1.1", "newer than supported"), + ("2.2", "newer than supported"), ("3.0", "schema version mismatch"), ], - ids=["schema_too_new", "schema_major_mismatch"], + ids=["schema_minor_too_new_v1", "schema_minor_too_new_v2", "schema_major_mismatch"], ) def test_baseline_verify_schema_incompatibilities( tmp_path: Path, schema_version: str, error_match: str @@ -413,6 +414,17 @@ def test_baseline_verify_schema_incompatibilities( assert exc.value.status == "mismatch_schema_version" +def test_baseline_verify_accepts_previous_minor_in_current_major( + tmp_path: Path, +) -> None: + baseline_path = tmp_path / "baseline.json" + _write_payload(baseline_path, _trusted_payload(schema_version="2.0")) + baseline = Baseline(baseline_path) + baseline.load() + baseline.verify_compatibility(current_python_tag=_python_tag()) + assert baseline.schema_version == "2.0" + + def test_baseline_verify_fingerprint_mismatch(tmp_path: Path) -> None: baseline_path = tmp_path / "baseline.json" _write_payload(baseline_path, _trusted_payload(fingerprint_version="2")) @@ -991,6 +1003,29 @@ def test_baseline_save_preserves_embedded_metrics_and_hash(tmp_path: Path) -> No assert saved_meta["metrics_payload_sha256"] == "f" * 64 +def test_baseline_save_preserves_embedded_api_surface_and_hash(tmp_path: Path) -> None: + baseline_path = tmp_path / "baseline.json" + payload = _trusted_payload() + assert isinstance(payload, dict) + payload["metrics"] = {"health_score": 70} + payload["api_surface"] = {"modules": [{"module": "pkg.mod", "symbols": []}]} + meta = payload.get("meta") + assert isinstance(meta, dict) + meta["metrics_payload_sha256"] = "f" * 64 + meta["api_surface_payload_sha256"] = "a" * 64 + _write_payload(baseline_path, payload) + + baseline = Baseline(baseline_path) + baseline.load() + baseline.save() + + saved = json.loads(baseline_path.read_text("utf-8")) + saved_meta = saved.get("meta") + assert isinstance(saved_meta, dict) + assert saved["api_surface"] == {"modules": [{"module": "pkg.mod", "symbols": []}]} + assert saved_meta["api_surface_payload_sha256"] == "a" * 64 + + def test_baseline_save_preserves_embedded_metrics_without_hash(tmp_path: Path) -> None: baseline_path = tmp_path / "baseline.json" payload = _trusted_payload() @@ -1015,7 +1050,7 @@ def test_baseline_save_preserves_embedded_metrics_without_hash(tmp_path: Path) - def test_preserve_embedded_metrics_variants(tmp_path: Path) -> None: path = tmp_path / "baseline.json" _write_payload(path, {"meta": {}, "clones": {"functions": [], "blocks": []}}) - assert baseline_mod._preserve_embedded_metrics(path) == (None, None) + assert baseline_mod._preserve_embedded_metrics(path) == (None, None, None, None) _write_payload( path, @@ -1025,7 +1060,12 @@ def test_preserve_embedded_metrics_variants(tmp_path: Path) -> None: "metrics": {"x": 1}, }, ) - assert baseline_mod._preserve_embedded_metrics(path) == ({"x": 1}, None) + assert baseline_mod._preserve_embedded_metrics(path) == ( + {"x": 1}, + None, + None, + None, + ) _write_payload( path, @@ -1035,7 +1075,12 @@ def test_preserve_embedded_metrics_variants(tmp_path: Path) -> None: "metrics": {"x": 2}, }, ) - assert baseline_mod._preserve_embedded_metrics(path) == ({"x": 2}, None) + assert baseline_mod._preserve_embedded_metrics(path) == ( + {"x": 2}, + None, + None, + None, + ) _write_payload( path, @@ -1045,7 +1090,31 @@ def test_preserve_embedded_metrics_variants(tmp_path: Path) -> None: "metrics": {"x": 3}, }, ) - assert baseline_mod._preserve_embedded_metrics(path) == ({"x": 3}, "a" * 64) + assert baseline_mod._preserve_embedded_metrics(path) == ( + {"x": 3}, + "a" * 64, + None, + None, + ) + + _write_payload( + path, + { + "meta": { + "metrics_payload_sha256": "a" * 64, + "api_surface_payload_sha256": "b" * 64, + }, + "clones": {"functions": [], "blocks": []}, + "metrics": {"x": 3}, + "api_surface": {"modules": [{"module": "pkg.mod"}]}, + }, + ) + assert baseline_mod._preserve_embedded_metrics(path) == ( + {"x": 3}, + "a" * 64, + {"modules": [{"module": "pkg.mod"}]}, + "b" * 64, + ) def test_baseline_save_defensive_non_mapping_meta( @@ -1068,7 +1137,7 @@ def _payload(**_kwargs: object) -> dict[str, object]: monkeypatch.setattr( baseline_mod, "_preserve_embedded_metrics", - lambda _path: ({"health_score": 1}, "a" * 64), + lambda _path: ({"health_score": 1}, "a" * 64, None, None), ) baseline.save() diff --git a/tests/test_cache.py b/tests/test_cache.py index 788709e..49ddcdf 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -262,6 +262,87 @@ def test_store_canonical_file_entry_marks_dirty_only_when_entry_changes( assert cache._dirty is True +def test_cache_helper_type_guards_and_wire_api_decoders_cover_invalid_inputs() -> None: + assert cache_mod._as_module_typing_coverage_dict({"module": "pkg"}) is None + assert cache_mod._as_module_docstring_coverage_dict({"module": "pkg"}) is None + assert cache_mod._as_module_api_surface_dict({"module": "pkg"}) is None + assert ( + cache_mod._has_cache_entry_container_shape( + { + "stat": {"mtime_ns": 1, "size": 1}, + "units": [], + "blocks": [], + "segments": [], + "typing_coverage": {"module": "pkg"}, + } + ) + is False + ) + assert ( + cache_mod._has_cache_entry_container_shape( + { + "stat": {"mtime_ns": 1, "size": 1}, + "units": [], + "blocks": [], + "segments": [], + "docstring_coverage": {"module": "pkg"}, + } + ) + is False + ) + assert ( + cache_mod._has_cache_entry_container_shape( + { + "stat": {"mtime_ns": 1, "size": 1}, + "units": [], + "blocks": [], + "segments": [], + "api_surface": {"module": "pkg"}, + } + ) + is False + ) + assert ( + cache_mod._decode_optional_wire_api_surface( + obj={"as": ["pkg.mod", ["run"], [None]]}, + filepath="pkg/mod.py", + ) + is None + ) + assert ( + cache_mod._decode_optional_wire_module_ints( + obj={"tc": ["pkg.mod", "bad"]}, + key="tc", + expected_len=2, + int_indexes=(1,), + ) + is None + ) + assert cache_mod._decode_wire_api_surface_symbol(["pkg.mod:run"]) is None + assert ( + cache_mod._decode_wire_api_surface_symbol( + ["pkg.mod:run", "function", 1, 2, "name", "", [None]] + ) + is None + ) + assert cache_mod._decode_wire_api_param_spec(["value"]) is None + assert cache_mod._is_api_param_spec_dict([]) is False + assert cache_mod._is_public_symbol_dict([]) is False + assert ( + cache_mod._is_public_symbol_dict( + { + "qualname": "pkg.mod:run", + "kind": "function", + "exported_via": "name", + "start_line": 1, + "end_line": 2, + "params": "bad", + } + ) + is False + ) + + def test_get_file_entry_missing_after_fallback_returns_none(tmp_path: Path) -> None: root = tmp_path / "project" root.mkdir() diff --git a/tests/test_cli_inprocess.py b/tests/test_cli_inprocess.py index 20c084e..022c726 100644 --- a/tests/test_cli_inprocess.py +++ b/tests/test_cli_inprocess.py @@ -3292,6 +3292,7 @@ def test_cli_summary_format_stable( assert "Summary" in out assert out.count("Summary") == 1 assert "Metrics" in out + assert "Adoption" in out assert "Overloaded" in out assert "callables" in out assert "Files parsed" not in out @@ -3306,6 +3307,21 @@ def test_cli_summary_format_stable( assert _summary_metric(out, "New vs baseline") >= 0 +def test_cli_summary_with_api_surface_shows_public_api_line( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + src = tmp_path / "a.py" + src.write_text("def f(value: int) -> int:\n return value\n", "utf-8") + _patch_parallel(monkeypatch) + _run_main(monkeypatch, [str(tmp_path), "--no-progress", "--api-surface"]) + out = capsys.readouterr().out + assert "Public API" in out + assert "symbols" in out + assert "modules" in out + + def test_cli_summary_no_color_has_no_ansi( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, diff --git a/tests/test_cli_unit.py b/tests/test_cli_unit.py index 0165a7a..3ccb414 100644 --- a/tests/test_cli_unit.py +++ b/tests/test_cli_unit.py @@ -17,6 +17,7 @@ import pytest +import codeclone._cli_baselines as cli_baselines_mod import codeclone._cli_meta as cli_meta_mod import codeclone._cli_reports as cli_reports import codeclone._cli_summary as cli_summary @@ -34,6 +35,7 @@ from codeclone.errors import BaselineValidationError from codeclone.models import HealthScore, ProjectMetrics from codeclone.normalize import NormalizationConfig +from tests._assertions import assert_contains_all class _RecordingPrinter: @@ -44,6 +46,33 @@ def print(self, *objects: object, **kwargs: object) -> None: self.lines.append(" ".join(str(obj) for obj in objects)) +def _metrics_baseline_runtime_for_gate_checks() -> ( + cli_baselines_mod._MetricsBaselineRuntime +): + runtime = cli_baselines_mod._MetricsBaselineRuntime( + baseline=metrics_baseline_mod.MetricsBaseline("metrics.json") + ) + runtime.loaded = True + runtime.trusted_for_diff = True + return runtime + + +def _assert_metrics_gate_schema_contract_error( + runtime: cli_baselines_mod._MetricsBaselineRuntime, + printer: _RecordingPrinter, + *, + expected_fragment: str, +) -> None: + assert runtime.loaded is False + assert runtime.trusted_for_diff is False + assert ( + runtime.status + == metrics_baseline_mod.MetricsBaselineStatus.MISMATCH_SCHEMA_VERSION + ) + assert runtime.failure_code == cli.ExitCode.CONTRACT_ERROR + assert any(expected_fragment in line for line in printer.lines) + + def test_process_file_stat_error( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: @@ -987,6 +1016,26 @@ def test_ui_summary_formatters_cover_optional_branches() -> None: ) assert "12 ranked" in limited_overloaded_modules assert "report-only; limited population" in limited_overloaded_modules + adoption = ui.fmt_metrics_adoption( + param_permille=750, + return_permille=500, + docstring_permille=667, + any_annotation_count=1, + ) + assert_contains_all( + adoption, + "params 75.0%", + "returns 50.0%", + "docstrings 66.7%", + "Any 1", + ) + api_surface = ui.fmt_metrics_api_surface( + public_symbols=3, + modules=2, + breaking=1, + added=4, + ) + assert_contains_all(api_surface, "symbols", "modules", "breaking", "added") changed_paths = ui.fmt_changed_scope_paths(count=45) assert "45" in changed_paths assert "from git diff" in changed_paths @@ -1041,11 +1090,7 @@ def test_print_changed_scope_uses_compact_line_in_quiet_mode( ), ) out = capsys.readouterr().out - assert "Changed" in out - assert "paths=45" in out - assert "findings=7" in out - assert "new=2" in out - assert "known=5" in out + assert_contains_all(out, "Changed", "paths=45", "findings=7", "new=2", "known=5") def test_print_metrics_in_quiet_mode_includes_overloaded_modules( @@ -1075,6 +1120,54 @@ def test_print_metrics_in_quiet_mode_includes_overloaded_modules( ) out = capsys.readouterr().out assert "overloaded_modules=3" in out + assert "Adoption" not in out + assert "Public API" not in out + + +def test_print_metrics_in_normal_mode_includes_adoption_and_public_api( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + monkeypatch.setattr(cli, "console", cli._make_console(no_color=True)) + cli_summary._print_metrics( + console=cast("cli_summary._Printer", cli.console), + quiet=False, + metrics=cli_summary.MetricsSnapshot( + complexity_avg=2.8, + complexity_max=20, + high_risk_count=0, + coupling_avg=0.5, + coupling_max=9, + cohesion_avg=1.2, + cohesion_max=4, + cycles_count=0, + dead_code_count=0, + health_total=85, + health_grade="B", + adoption_param_permille=750, + adoption_return_permille=500, + adoption_docstring_permille=667, + adoption_any_annotation_count=1, + api_surface_enabled=True, + api_surface_modules=2, + api_surface_public_symbols=3, + api_surface_added=4, + api_surface_breaking=1, + overloaded_modules_candidates=3, + overloaded_modules_total=158, + overloaded_modules_population_status="ok", + overloaded_modules_top_score=0.98, + ), + ) + out = capsys.readouterr().out + assert_contains_all( + out, + "Adoption", + "params 75.0%", + "docstrings 66.7%", + "Public API", + "3 symbols", + "1 breaking", + ) def test_configure_metrics_mode_rejects_skip_metrics_with_metrics_flags( @@ -1133,7 +1226,7 @@ def test_metrics_computed_respects_skip_switches() -> None: skip_dependencies=True, skip_dead_code=True, ) - ) == ("complexity", "coupling", "cohesion", "health") + ) == ("complexity", "coupling", "cohesion", "health", "coverage_adoption") assert cli._metrics_computed( Namespace( skip_metrics=False, @@ -1147,6 +1240,33 @@ def test_metrics_computed_respects_skip_switches() -> None: "health", "dependencies", "dead_code", + "coverage_adoption", + ) + + +def test_metrics_computed_includes_api_surface_only_when_enabled() -> None: + assert cli._metrics_computed( + Namespace( + skip_metrics=False, + skip_dependencies=True, + skip_dead_code=True, + api_surface=False, + ) + ) == ("complexity", "coupling", "cohesion", "health", "coverage_adoption") + assert cli._metrics_computed( + Namespace( + skip_metrics=False, + skip_dependencies=True, + skip_dead_code=True, + api_surface=True, + ) + ) == ( + "complexity", + "coupling", + "cohesion", + "health", + "coverage_adoption", + "api_surface", ) @@ -1562,6 +1682,53 @@ def _raise_verify(self: object, *, runtime_python_tag: str) -> None: _assert_main_impl_exit_code(monkeypatch, argv, expected_code=2) +def test_metrics_baseline_runtime_requires_adoption_snapshot_for_regression_gates() -> ( + None +): + runtime = _metrics_baseline_runtime_for_gate_checks() + runtime.baseline.has_coverage_adoption_snapshot = False + printer = _RecordingPrinter() + + cli_baselines_mod._enforce_metrics_gate_schema_requirements( + args=SimpleNamespace( + fail_on_typing_regression=True, + fail_on_docstring_regression=False, + fail_on_api_break=False, + ), + state=runtime, + console=printer, + ) + + _assert_metrics_gate_schema_contract_error( + runtime, + printer, + expected_fragment="coverage adoption data", + ) + + +def test_metrics_baseline_runtime_requires_api_surface_snapshot_for_api_gate() -> None: + runtime = _metrics_baseline_runtime_for_gate_checks() + runtime.baseline.has_coverage_adoption_snapshot = True + runtime.baseline.api_surface_snapshot = None + printer = _RecordingPrinter() + + cli_baselines_mod._enforce_metrics_gate_schema_requirements( + args=SimpleNamespace( + fail_on_typing_regression=False, + fail_on_docstring_regression=False, + fail_on_api_break=True, + ), + state=runtime, + console=printer, + ) + + _assert_metrics_gate_schema_contract_error( + runtime, + printer, + expected_fragment="public API surface data", + ) + + def test_main_impl_update_metrics_baseline_write_error_contract( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: diff --git a/tests/test_html_report.py b/tests/test_html_report.py index a7b9360..5620513 100644 --- a/tests/test_html_report.py +++ b/tests/test_html_report.py @@ -1878,6 +1878,80 @@ def test_html_report_executive_summary_includes_effective_analysis_profile() -> ) +def test_html_report_overview_includes_adoption_and_api_summary_cluster() -> None: + metrics = _metrics_payload( + health_score=82, + health_grade="B", + complexity_max=12, + complexity_high_risk=0, + coupling_high_risk=0, + cohesion_low=0, + dep_cycles=[], + dep_max_depth=2, + dead_total=0, + dead_critical=0, + ) + metrics["coverage_adoption"] = { + "summary": { + "params_total": 4, + "params_annotated": 3, + "param_permille": 750, + "baseline_diff_available": True, + "param_delta": 125, + "returns_total": 2, + "returns_annotated": 1, + "return_permille": 500, + "return_delta": 250, + "public_symbol_total": 3, + "public_symbol_documented": 2, + "docstring_permille": 667, + "docstring_delta": 167, + "typing_any_count": 1, + }, + "items": [], + } + metrics["api_surface"] = { + "summary": { + "enabled": True, + "baseline_diff_available": True, + "modules": 1, + "public_symbols": 2, + "added": 1, + "breaking": 1, + "strict_types": False, + }, + "items": [], + } + + html = _render_metrics_html(metrics) + + _assert_html_contains( + html, + "Adoption & API", + "Adoption coverage", + '
75.0%
', + '
params
', + '
50.0%
', + '
returns
', + '
66.7%
', + '
docstrings
', + "Δ params", + "+12.5pt", + "Δ returns", + "+25.0pt", + "Δ docs", + "+16.7pt", + "1 symbol typed as Any", + "Public API surface", + '
2
', + '
public symbols
', + '
1
', + '
modules
', + "breaking", + "added", + ) + + def test_html_report_metrics_without_health_score_uses_info_overview() -> None: html = build_html_report( func_groups={}, diff --git a/tests/test_mcp_service.py b/tests/test_mcp_service.py index a1bbf5c..9609d37 100644 --- a/tests/test_mcp_service.py +++ b/tests/test_mcp_service.py @@ -1368,6 +1368,11 @@ def test_mcp_service_helper_filters_and_metrics_payload() -> None: "new_cycles": 1, "new_dead_code": 1, "health_delta": -3, + "typing_param_permille_delta": 0, + "typing_return_permille_delta": 0, + "docstring_permille_delta": 0, + "new_api_symbols": 0, + "api_breaking_changes": 0, } assert service._metrics_diff_payload(None) is None @@ -3470,6 +3475,102 @@ def test_mcp_service_summary_and_metrics_detail_helper_fallbacks( }, ], } + coverage_adoption_payload = service._metrics_detail_payload( + metrics={ + "summary": {}, + "families": { + "coverage_adoption": { + "items": [ + { + "relative_path": "pkg/mod.py", + "module": "pkg.mod", + "param_permille": 750, + "docstring_permille": 667, + } + ] + } + }, + }, + family="coverage_adoption", + path=None, + offset=0, + limit=5, + ) + assert coverage_adoption_payload == { + "family": "coverage_adoption", + "path": None, + "offset": 0, + "limit": 5, + "returned": 1, + "total": 1, + "has_more": False, + "items": [ + { + "path": "pkg/mod.py", + "module": "pkg.mod", + "param_permille": 750, + "docstring_permille": 667, + } + ], + } + api_surface_payload = service._metrics_detail_payload( + metrics={ + "summary": {}, + "families": { + "api_surface": { + "items": [ + { + "relative_path": "pkg/mod.py", + "module": "pkg.mod", + "qualname": "pkg.mod:run", + "record_kind": "symbol", + "symbol_kind": "function", + "params_total": 1, + }, + { + "relative_path": "pkg/mod.py", + "module": "pkg.mod", + "qualname": "pkg.mod:old", + "record_kind": "breaking_change", + "change_kind": "removed", + "detail": "Removed from the public API surface.", + }, + ] + } + }, + }, + family="api_surface", + path=None, + offset=0, + limit=5, + ) + assert api_surface_payload == { + "family": "api_surface", + "path": None, + "offset": 0, + "limit": 5, + "returned": 2, + "total": 2, + "has_more": False, + "items": [ + { + "path": "pkg/mod.py", + "module": "pkg.mod", + "qualname": "pkg.mod:run", + "record_kind": "symbol", + "symbol_kind": "function", + "params_total": 1, + }, + { + "path": "pkg/mod.py", + "module": "pkg.mod", + "qualname": "pkg.mod:old", + "record_kind": "breaking_change", + "change_kind": "removed", + "detail": "Removed from the public API surface.", + }, + ], + } assert service._compact_metrics_item( {"qualname": "pkg.mod:run", "score": 10, "skip": None} ) == {"qualname": "pkg.mod:run", "score": 10} diff --git a/tests/test_metrics_baseline.py b/tests/test_metrics_baseline.py index 862fc19..e22884b 100644 --- a/tests/test_metrics_baseline.py +++ b/tests/test_metrics_baseline.py @@ -8,19 +8,25 @@ import json import os +from dataclasses import replace from pathlib import Path from typing import Any, cast import pytest +import codeclone.baseline as baseline_mod import codeclone.metrics_baseline as mb_mod from codeclone.errors import BaselineValidationError from codeclone.metrics_baseline import MetricsBaseline, MetricsBaselineStatus from codeclone.models import ( + ApiParamSpec, + ApiSurfaceSnapshot, DeadItem, HealthScore, MetricsSnapshot, + ModuleApiSurface, ProjectMetrics, + PublicSymbol, ) @@ -79,6 +85,106 @@ def _project_metrics() -> ProjectMetrics: ) +def _api_surface_snapshot(*, include_added: bool = False) -> ApiSurfaceSnapshot: + symbols = [ + PublicSymbol( + qualname="pkg.mod:run", + kind="function", + start_line=10, + end_line=14, + params=( + ApiParamSpec( + name="value", + kind="pos_or_kw", + has_default=False, + ), + ), + ), + PublicSymbol( + qualname="pkg.mod:stable", + kind="function", + start_line=20, + end_line=22, + ), + ] + if include_added: + symbols.append( + PublicSymbol( + qualname="pkg.mod:added", + kind="function", + start_line=30, + end_line=32, + ) + ) + return ApiSurfaceSnapshot( + modules=( + ModuleApiSurface( + module="pkg.mod", + filepath="pkg/mod.py", + symbols=tuple(symbols), + ), + ) + ) + + +def _api_surface_snapshot_with_filepath(filepath: str) -> ApiSurfaceSnapshot: + snapshot = _api_surface_snapshot(include_added=True) + module = snapshot.modules[0] + return ApiSurfaceSnapshot( + modules=( + ModuleApiSurface( + module=module.module, + filepath=filepath, + symbols=module.symbols, + all_declared=module.all_declared, + ), + ) + ) + + +def _save_metrics_baseline_with_api_surface( + path: Path, + *, + api_surface: ApiSurfaceSnapshot, +) -> dict[str, object]: + baseline = MetricsBaseline.from_project_metrics( + project_metrics=_project_metrics_with_adoption_and_api( + api_surface=api_surface, + ), + path=path, + ) + baseline.save() + return cast(dict[str, object], json.loads(path.read_text("utf-8"))) + + +def _repo_metrics_baseline_path_and_abs_filepath(tmp_path: Path) -> tuple[Path, str]: + repo_root = tmp_path / "repo" + repo_root.mkdir() + path = repo_root / "metrics-baseline.json" + absolute_filepath = str((repo_root / "pkg" / "mod.py").resolve()) + return path, absolute_filepath + + +def _project_metrics_with_adoption_and_api( + *, + typing_param_annotated: int = 3, + typing_return_annotated: int = 2, + docstring_public_documented: int = 2, + api_surface: ApiSurfaceSnapshot | None = None, +) -> ProjectMetrics: + return replace( + _project_metrics(), + typing_param_total=4, + typing_param_annotated=typing_param_annotated, + typing_return_total=2, + typing_return_annotated=typing_return_annotated, + typing_any_count=1, + docstring_public_total=3, + docstring_public_documented=docstring_public_documented, + api_surface=api_surface, + ) + + def _write_json(path: Path, payload: object) -> None: path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), "utf-8") @@ -98,6 +204,26 @@ def _valid_payload( ) +def _ready_metrics_baseline( + path: Path, + *, + schema_version: str, + generator_name: str = mb_mod.METRICS_BASELINE_GENERATOR, + python_tag: str | None = None, + embedded: bool = False, + has_adoption: bool = True, +) -> MetricsBaseline: + baseline = MetricsBaseline(path) + baseline.snapshot = _snapshot() + baseline.payload_sha256 = mb_mod._compute_payload_sha256(_snapshot()) + baseline.has_coverage_adoption_snapshot = has_adoption + baseline.generator_name = generator_name + baseline.schema_version = schema_version + baseline.python_tag = python_tag or mb_mod.current_python_tag() + baseline.is_embedded_in_clone_baseline = embedded + return baseline + + def test_coerce_metrics_baseline_status_variants() -> None: assert ( mb_mod.coerce_metrics_baseline_status(MetricsBaselineStatus.OK) @@ -246,6 +372,67 @@ def test_metrics_baseline_save_standalone_payload_sets_metadata(tmp_path: Path) assert isinstance(baseline.payload_sha256, str) +def test_metrics_baseline_save_writes_compact_api_surface_local_names( + tmp_path: Path, +) -> None: + path = tmp_path / "metrics-baseline.json" + payload = _save_metrics_baseline_with_api_surface( + path, + api_surface=_api_surface_snapshot(include_added=True), + ) + api_surface = cast(dict[str, object], payload["api_surface"]) + modules = cast(list[dict[str, object]], api_surface["modules"]) + symbols = cast(list[dict[str, object]], modules[0]["symbols"]) + + assert "local_name" in symbols[0] + assert "qualname" not in symbols[0] + assert symbols[0]["local_name"] == "added" + assert symbols[1]["local_name"] == "run" + assert symbols[2]["local_name"] == "stable" + + +def test_metrics_baseline_save_relativizes_api_surface_filepaths( + tmp_path: Path, +) -> None: + path, absolute_filepath = _repo_metrics_baseline_path_and_abs_filepath(tmp_path) + payload = _save_metrics_baseline_with_api_surface( + path, + api_surface=_api_surface_snapshot_with_filepath(absolute_filepath), + ) + api_surface = cast(dict[str, object], payload["api_surface"]) + modules = cast(list[dict[str, object]], api_surface["modules"]) + assert modules[0]["filepath"] == "pkg/mod.py" + + reloaded = MetricsBaseline(path) + reloaded.load() + assert reloaded.api_surface_snapshot is not None + assert reloaded.api_surface_snapshot.modules[0].filepath == absolute_filepath + + +def test_api_surface_payload_hashes_are_order_independent() -> None: + symbols = _api_surface_snapshot(include_added=True).modules[0].symbols + reordered = ApiSurfaceSnapshot( + modules=( + ModuleApiSurface( + module="pkg.mod", + filepath="pkg/mod.py", + symbols=(symbols[2], symbols[0], symbols[1]), + ), + ) + ) + + assert mb_mod._compute_api_surface_payload_sha256( + reordered + ) == mb_mod._compute_api_surface_payload_sha256( + _api_surface_snapshot(include_added=True) + ) + assert mb_mod._compute_legacy_api_surface_payload_sha256( + reordered + ) == mb_mod._compute_legacy_api_surface_payload_sha256( + _api_surface_snapshot(include_added=True) + ) + + def test_metrics_baseline_save_with_existing_plain_payload_rewrites_plain( tmp_path: Path, ) -> None: @@ -295,12 +482,11 @@ def test_metrics_baseline_save_rejects_corrupted_existing_payload( def test_metrics_baseline_verify_compatibility_and_integrity_failures( tmp_path: Path, ) -> None: - baseline = MetricsBaseline(tmp_path / "metrics-baseline.json") - baseline.snapshot = _snapshot() - baseline.payload_sha256 = mb_mod._compute_payload_sha256(_snapshot()) - baseline.generator_name = "other" - baseline.schema_version = mb_mod.METRICS_BASELINE_SCHEMA_VERSION - baseline.python_tag = mb_mod.current_python_tag() + baseline = _ready_metrics_baseline( + tmp_path / "metrics-baseline.json", + schema_version=mb_mod.METRICS_BASELINE_SCHEMA_VERSION, + generator_name="other", + ) with pytest.raises(BaselineValidationError, match="generator mismatch"): baseline.verify_compatibility(runtime_python_tag=mb_mod.current_python_tag()) @@ -333,6 +519,23 @@ def test_metrics_baseline_verify_compatibility_and_integrity_failures( baseline.verify_integrity() +def test_metrics_baseline_verify_accepts_previous_minor_versions( + tmp_path: Path, +) -> None: + standalone = _ready_metrics_baseline( + tmp_path / "metrics-baseline.json", + schema_version="1.1", + ) + standalone.verify_compatibility(runtime_python_tag=mb_mod.current_python_tag()) + + embedded = _ready_metrics_baseline( + tmp_path / "codeclone.baseline.json", + schema_version="2.0", + embedded=True, + ) + embedded.verify_compatibility(runtime_python_tag=mb_mod.current_python_tag()) + + def test_metrics_baseline_diff_without_snapshot_uses_default_snapshot( tmp_path: Path, ) -> None: @@ -345,18 +548,89 @@ def test_metrics_baseline_diff_without_snapshot_uses_default_snapshot( assert diff.health_delta == 70 +def test_metrics_baseline_diff_tracks_adoption_and_api_surface_deltas( + tmp_path: Path, +) -> None: + baseline = MetricsBaseline.from_project_metrics( + project_metrics=_project_metrics_with_adoption_and_api( + typing_param_annotated=2, + typing_return_annotated=1, + docstring_public_documented=1, + api_surface=_api_surface_snapshot(include_added=False), + ), + path=tmp_path / "metrics-baseline.json", + ) + + current = _project_metrics_with_adoption_and_api( + typing_param_annotated=3, + typing_return_annotated=2, + docstring_public_documented=2, + api_surface=ApiSurfaceSnapshot( + modules=( + ModuleApiSurface( + module="pkg.mod", + filepath="pkg/mod.py", + symbols=( + PublicSymbol( + qualname="pkg.mod:run", + kind="function", + start_line=10, + end_line=14, + params=( + ApiParamSpec( + name="renamed", + kind="pos_or_kw", + has_default=False, + ), + ), + ), + PublicSymbol( + qualname="pkg.mod:added", + kind="function", + start_line=30, + end_line=32, + ), + ), + ), + ) + ), + ) + + diff = baseline.diff(current) + assert diff.typing_param_permille_delta == 250 + assert diff.typing_return_permille_delta == 500 + assert diff.docstring_permille_delta == 334 + assert diff.new_api_symbols == ("pkg.mod:added",) + assert [ + (item.qualname, item.change_kind) for item in diff.new_api_breaking_changes + ] == [ + ("pkg.mod:run", "signature_break"), + ("pkg.mod:stable", "removed"), + ] + + def test_snapshot_from_project_metrics_and_from_project_metrics_factory( tmp_path: Path, ) -> None: - snapshot = mb_mod.snapshot_from_project_metrics(_project_metrics()) + snapshot = mb_mod.snapshot_from_project_metrics( + _project_metrics_with_adoption_and_api( + api_surface=_api_surface_snapshot(include_added=False), + ) + ) assert snapshot.high_risk_functions == ("pkg.mod:hot",) assert snapshot.high_coupling_classes == ("pkg.mod:Service",) assert snapshot.low_cohesion_classes == ("pkg.mod:Service",) assert snapshot.dependency_cycles == (("pkg.a", "pkg.b"),) assert snapshot.dead_code_items == ("pkg.mod:unused",) + assert snapshot.typing_param_permille == 750 + assert snapshot.typing_return_permille == 1000 + assert snapshot.docstring_permille == 667 + assert snapshot.typing_any_count == 1 baseline = MetricsBaseline.from_project_metrics( - project_metrics=_project_metrics(), + project_metrics=_project_metrics_with_adoption_and_api( + api_surface=_api_surface_snapshot(include_added=False), + ), path=tmp_path / "metrics-baseline.json", generator_version="2.0.0", ) @@ -364,7 +638,138 @@ def test_snapshot_from_project_metrics_and_from_project_metrics_factory( assert baseline.generator_version == "2.0.0" assert baseline.schema_version == mb_mod.METRICS_BASELINE_SCHEMA_VERSION assert baseline.snapshot is not None + assert baseline.has_coverage_adoption_snapshot is True assert isinstance(baseline.payload_sha256, str) + assert isinstance(baseline.api_surface_payload_sha256, str) + assert baseline.api_surface_snapshot is not None + + +def test_metrics_baseline_load_tracks_adoption_snapshot_presence( + tmp_path: Path, +) -> None: + path = tmp_path / "metrics-baseline.json" + payload = _valid_payload() + meta = cast(dict[str, object], payload["meta"]) + metrics = cast(dict[str, object], payload["metrics"]) + metrics.pop("typing_param_permille") + metrics.pop("typing_return_permille") + metrics.pop("docstring_permille") + metrics.pop("typing_any_count") + meta["payload_sha256"] = mb_mod._compute_payload_sha256( + _snapshot(), + include_adoption=False, + ) + _write_json(path, payload) + + baseline = MetricsBaseline(path) + baseline.load() + + assert baseline.snapshot is not None + assert baseline.has_coverage_adoption_snapshot is False + baseline.verify_integrity() + + +def test_metrics_baseline_save_embedded_clone_baseline_preserves_api_surface( + tmp_path: Path, +) -> None: + path = tmp_path / "codeclone.baseline.json" + baseline_mod.Baseline.from_groups({}, {}, path=path).save() + + baseline = MetricsBaseline.from_project_metrics( + project_metrics=_project_metrics_with_adoption_and_api( + api_surface=_api_surface_snapshot(include_added=True), + ), + path=path, + ) + baseline.save() + + payload = json.loads(path.read_text("utf-8")) + meta = cast(dict[str, object], payload["meta"]) + assert "api_surface" in payload + assert "api_surface_payload_sha256" in meta + + reloaded = MetricsBaseline(path) + reloaded.load() + assert reloaded.is_embedded_in_clone_baseline is True + assert reloaded.has_coverage_adoption_snapshot is True + assert reloaded.api_surface_snapshot is not None + + +def test_metrics_baseline_load_accepts_legacy_api_surface_qualnames( + tmp_path: Path, +) -> None: + path = tmp_path / "metrics-baseline.json" + snapshot = _snapshot() + api_surface_snapshot = _api_surface_snapshot(include_added=True) + payload = mb_mod._build_payload( + snapshot=snapshot, + schema_version=mb_mod.METRICS_BASELINE_SCHEMA_VERSION, + python_tag=mb_mod.current_python_tag(), + generator_name=mb_mod.METRICS_BASELINE_GENERATOR, + generator_version="2.0.0", + created_at="2026-03-06T00:00:00Z", + api_surface_snapshot=api_surface_snapshot, + ) + api_surface = cast(dict[str, object], payload["api_surface"]) + modules = cast(list[dict[str, object]], api_surface["modules"]) + symbols = cast(list[dict[str, object]], modules[0]["symbols"]) + for symbol in symbols: + local_name = cast(str, symbol.pop("local_name")) + symbol["qualname"] = f"pkg.mod:{local_name}" + meta = cast(dict[str, object], payload["meta"]) + meta["api_surface_payload_sha256"] = ( + mb_mod._compute_legacy_api_surface_payload_sha256( + api_surface_snapshot, + root=path.parent, + ) + ) + _write_json(path, payload) + + baseline = MetricsBaseline(path) + baseline.load() + + assert baseline.api_surface_snapshot is not None + assert [ + item.qualname for item in baseline.api_surface_snapshot.modules[0].symbols + ] == [ + "pkg.mod:added", + "pkg.mod:run", + "pkg.mod:stable", + ] + baseline.verify_integrity() + + +def test_metrics_baseline_load_accepts_absolute_api_surface_filepaths( + tmp_path: Path, +) -> None: + path, absolute_filepath = _repo_metrics_baseline_path_and_abs_filepath(tmp_path) + snapshot = _snapshot() + api_surface_snapshot = _api_surface_snapshot_with_filepath(absolute_filepath) + payload = mb_mod._build_payload( + snapshot=snapshot, + schema_version=mb_mod.METRICS_BASELINE_SCHEMA_VERSION, + python_tag=mb_mod.current_python_tag(), + generator_name=mb_mod.METRICS_BASELINE_GENERATOR, + generator_version="2.0.0", + created_at="2026-03-06T00:00:00Z", + api_surface_snapshot=api_surface_snapshot, + api_surface_root=path.parent, + ) + api_surface = cast(dict[str, object], payload["api_surface"]) + modules = cast(list[dict[str, object]], api_surface["modules"]) + modules[0]["filepath"] = absolute_filepath + meta = cast(dict[str, object], payload["meta"]) + meta["api_surface_payload_sha256"] = mb_mod._compute_api_surface_payload_sha256( + api_surface_snapshot + ) + _write_json(path, payload) + + baseline = MetricsBaseline(path) + baseline.load() + + assert baseline.api_surface_snapshot is not None + assert baseline.api_surface_snapshot.modules[0].filepath == absolute_filepath + baseline.verify_integrity() def test_metrics_baseline_json_and_structure_validators(tmp_path: Path) -> None: diff --git a/tests/test_pipeline_metrics.py b/tests/test_pipeline_metrics.py index 324b3ad..40a69d3 100644 --- a/tests/test_pipeline_metrics.py +++ b/tests/test_pipeline_metrics.py @@ -6,27 +6,50 @@ from __future__ import annotations -from codeclone.cache import CacheEntry +from dataclasses import replace +from typing import cast + +from codeclone.cache import ( + ApiParamSpecDict, + CacheEntry, + ModuleApiSurfaceDict, + PublicSymbolDict, +) from codeclone.metrics import build_overloaded_modules_payload from codeclone.models import ( + ApiBreakingChange, + ApiParamSpec, + ApiSurfaceSnapshot, ClassMetrics, DeadCandidate, DeadItem, HealthScore, MetricsDiff, + ModuleApiSurface, ModuleDep, + ModuleDocstringCoverage, + ModuleTypingCoverage, ProjectMetrics, + PublicSymbol, ) from codeclone.pipeline import ( MetricGateConfig, + _api_param_spec_from_cache_dict, + _api_surface_from_cache_dict, _as_int, _as_sorted_str_tuple, _as_str, + _cache_dict_int_fields, + _cache_dict_module_fields, _class_metric_sort_key, - _load_cached_metrics, + _docstring_coverage_from_cache_dict, + _enrich_metrics_report_payload, + _load_cached_metrics_extended, _module_dep_sort_key, _module_names_from_units, + _public_symbol_from_cache_dict, _should_use_parallel, + _typing_coverage_from_cache_dict, build_metrics_report_payload, compute_project_metrics, metric_gate_reasons, @@ -66,6 +89,64 @@ def _project_metrics(*, dead_confidence: str = "high") -> ProjectMetrics: ) +def _project_metrics_with_adoption_and_api() -> ProjectMetrics: + return replace( + _project_metrics(), + typing_param_total=4, + typing_param_annotated=3, + typing_return_total=2, + typing_return_annotated=1, + typing_any_count=1, + docstring_public_total=3, + docstring_public_documented=2, + typing_modules=( + ModuleTypingCoverage( + module="pkg.mod", + filepath="pkg/mod.py", + callable_count=2, + params_total=4, + params_annotated=3, + returns_total=2, + returns_annotated=1, + any_annotation_count=1, + ), + ), + docstring_modules=( + ModuleDocstringCoverage( + module="pkg.mod", + filepath="pkg/mod.py", + public_symbol_total=3, + public_symbol_documented=2, + ), + ), + api_surface=ApiSurfaceSnapshot( + modules=( + ModuleApiSurface( + module="pkg.mod", + filepath="pkg/mod.py", + symbols=( + PublicSymbol( + qualname="pkg.mod:run", + kind="function", + start_line=10, + end_line=12, + params=( + ApiParamSpec( + name="value", + kind="pos_or_kw", + has_default=False, + annotation_hash="int", + ), + ), + returns_hash="int", + ), + ), + ), + ) + ), + ) + + def test_pipeline_basic_helpers_and_sort_keys() -> None: assert _as_int(True) == 1 assert _as_int("15") == 15 @@ -179,6 +260,81 @@ def test_build_metrics_report_payload_includes_suppressed_dead_code_items() -> N ] +def test_build_metrics_report_payload_includes_adoption_and_api_surface_families() -> ( + None +): + payload = build_metrics_report_payload( + project_metrics=_project_metrics_with_adoption_and_api(), + units=(), + class_metrics=(), + suppressed_dead_code=(), + ) + + coverage_adoption = cast(dict[str, object], payload["coverage_adoption"]) + assert coverage_adoption["summary"] == { + "modules": 1, + "params_total": 4, + "params_annotated": 3, + "param_permille": 750, + "returns_total": 2, + "returns_annotated": 1, + "return_permille": 500, + "public_symbol_total": 3, + "public_symbol_documented": 2, + "docstring_permille": 667, + "typing_any_count": 1, + } + assert coverage_adoption["items"] == [ + { + "module": "pkg.mod", + "filepath": "pkg/mod.py", + "callable_count": 2, + "params_total": 4, + "params_annotated": 3, + "param_permille": 750, + "returns_total": 2, + "returns_annotated": 1, + "return_permille": 500, + "any_annotation_count": 1, + "public_symbol_total": 3, + "public_symbol_documented": 2, + "docstring_permille": 667, + } + ] + + api_surface = cast(dict[str, object], payload["api_surface"]) + assert api_surface["summary"] == { + "enabled": True, + "modules": 1, + "public_symbols": 1, + "added": 0, + "breaking": 0, + "strict_types": False, + } + assert api_surface["items"] == [ + { + "record_kind": "symbol", + "module": "pkg.mod", + "filepath": "pkg/mod.py", + "qualname": "pkg.mod:run", + "start_line": 10, + "end_line": 12, + "symbol_kind": "function", + "exported_via": "name", + "params_total": 1, + "params": [ + { + "name": "value", + "kind": "pos_or_kw", + "has_default": False, + "annotated": True, + } + ], + "returns_annotated": True, + } + ] + + def test_metrics_payload_includes_overloaded_modules_for_small_population() -> None: payload = build_metrics_report_payload( scan_root="/repo", @@ -308,11 +464,11 @@ def test_load_cached_metrics_ignores_referenced_names_from_test_files() -> None: "segments": [], "referenced_names": ["orphan", "helper"], } - _, _, _, test_names, test_qualnames = _load_cached_metrics( + _, _, _, test_names, test_qualnames, *_ = _load_cached_metrics_extended( entry, filepath="pkg/tests/test_mod.py", ) - _, _, _, regular_names, regular_qualnames = _load_cached_metrics( + _, _, _, regular_names, regular_qualnames, *_ = _load_cached_metrics_extended( entry, filepath="pkg/mod.py", ) @@ -344,7 +500,10 @@ def test_load_cached_metrics_preserves_coupled_classes() -> None: } ], } - class_metrics, _, _, _, _ = _load_cached_metrics(entry, filepath="pkg/mod.py") + class_metrics, _, _, _, _, *_ = _load_cached_metrics_extended( + entry, + filepath="pkg/mod.py", + ) assert len(class_metrics) == 1 assert class_metrics[0].coupled_classes == ("TypeA", "TypeB") @@ -367,11 +526,168 @@ def test_load_cached_metrics_preserves_dead_candidate_suppressions() -> None: } ], } - _, _, dead_candidates, _, _ = _load_cached_metrics(entry, filepath="pkg/mod.py") + _, _, dead_candidates, _, _, *_ = _load_cached_metrics_extended( + entry, + filepath="pkg/mod.py", + ) assert len(dead_candidates) == 1 assert dead_candidates[0].suppressed_rules == ("dead-code",) +def test_pipeline_cache_decode_helpers_cover_invalid_and_valid_payloads() -> None: + assert _cache_dict_module_fields(1) is None + assert _cache_dict_module_fields({"module": "pkg.mod"}) is None + assert _cache_dict_int_fields({"count": "x"}, "count") is None + assert ( + _typing_coverage_from_cache_dict( + { + "module": "pkg.mod", + "filepath": "pkg/mod.py", + "callable_count": "bad", + } + ) + is None + ) + assert ( + _docstring_coverage_from_cache_dict( + { + "module": "pkg.mod", + "filepath": "pkg/mod.py", + "public_symbol_total": 1, + "public_symbol_documented": "bad", + } + ) + is None + ) + assert ( + _api_param_spec_from_cache_dict( + cast( + "ApiParamSpecDict", + { + "name": "value", + "kind": "pos_or_kw", + "has_default": "bad", + "annotation_hash": "", + }, + ) + ) + is None + ) + assert ( + _public_symbol_from_cache_dict( + cast( + "PublicSymbolDict", + { + "qualname": "pkg.mod:run", + "kind": "function", + "start_line": 1, + "end_line": 2, + "exported_via": "name", + "returns_hash": "", + "params": ["bad"], + }, + ) + ) + is None + ) + assert ( + _api_surface_from_cache_dict( + cast( + "ModuleApiSurfaceDict", + { + "module": "pkg.mod", + "filepath": "pkg/mod.py", + "all_declared": ["run"], + "symbols": ["bad"], + }, + ) + ) + is None + ) + + valid_surface = _api_surface_from_cache_dict( + { + "module": "pkg.mod", + "filepath": "pkg/mod.py", + "all_declared": ["run", "run"], + "symbols": [ + { + "qualname": "pkg.mod:run", + "kind": "function", + "start_line": 10, + "end_line": 12, + "exported_via": "name", + "returns_hash": "int", + "params": [ + { + "name": "value", + "kind": "pos_or_kw", + "has_default": False, + "annotation_hash": "int", + } + ], + } + ], + } + ) + assert valid_surface is not None + assert valid_surface.all_declared == ("run",) + assert valid_surface.symbols[0].params[0].annotation_hash == "int" + + +def test_load_cached_metrics_extended_decodes_adoption_and_api_surface() -> None: + entry: CacheEntry = { + "stat": {"mtime_ns": 1, "size": 1}, + "units": [], + "blocks": [], + "segments": [], + "typing_coverage": { + "module": "pkg.mod", + "filepath": "pkg/mod.py", + "callable_count": 2, + "params_total": 4, + "params_annotated": 3, + "returns_total": 2, + "returns_annotated": 1, + "any_annotation_count": 1, + }, + "docstring_coverage": { + "module": "pkg.mod", + "filepath": "pkg/mod.py", + "public_symbol_total": 3, + "public_symbol_documented": 2, + }, + "api_surface": { + "module": "pkg.mod", + "filepath": "pkg/mod.py", + "all_declared": ["run"], + "symbols": [ + { + "qualname": "pkg.mod:run", + "kind": "function", + "start_line": 10, + "end_line": 12, + "exported_via": "name", + "returns_hash": "int", + "params": [], + } + ], + }, + } + *_, typing_coverage, docstring_coverage, api_surface = ( + _load_cached_metrics_extended( + entry, + filepath="pkg/mod.py", + ) + ) + assert typing_coverage is not None + assert docstring_coverage is not None + assert api_surface is not None + assert typing_coverage.any_annotation_count == 1 + assert docstring_coverage.public_symbol_documented == 2 + assert api_surface.symbols[0].qualname == "pkg.mod:run" + + def test_metric_gate_reasons_collects_all_enabled_reasons() -> None: reasons = metric_gate_reasons( project_metrics=_project_metrics(dead_confidence="high"), @@ -406,6 +722,68 @@ def test_metric_gate_reasons_collects_all_enabled_reasons() -> None: assert any(reason.startswith("Health score regressed") for reason in reasons) +def test_enrich_metrics_report_payload_adds_docstring_and_breaking_api_rows() -> None: + metrics_diff = MetricsDiff( + new_high_risk_functions=(), + new_high_coupling_classes=(), + new_cycles=(), + new_dead_code=(), + health_delta=0, + typing_param_permille_delta=-25, + typing_return_permille_delta=0, + docstring_permille_delta=10, + new_api_symbols=("pkg.mod:added",), + new_api_breaking_changes=cast( + "tuple[ApiBreakingChange, ...]", + ( + ApiBreakingChange( + qualname="pkg.mod:old", + filepath="pkg/mod.py", + start_line=20, + end_line=21, + symbol_kind="function", + change_kind="removed", + detail="Removed from the public API surface.", + ), + "ignored", + ), + ), + ) + base_payload = build_metrics_report_payload( + project_metrics=replace( + _project_metrics_with_adoption_and_api(), + typing_modules=(), + docstring_modules=( + ModuleDocstringCoverage( + module="pkg.docs", + filepath="pkg/docs.py", + public_symbol_total=2, + public_symbol_documented=1, + ), + ), + ), + units=(), + class_metrics=(), + suppressed_dead_code=(), + ) + payload = _enrich_metrics_report_payload( + metrics_payload=base_payload, + metrics_diff=metrics_diff, + ) + + coverage_adoption = cast(dict[str, object], payload["coverage_adoption"]) + adoption_items = cast(list[dict[str, object]], coverage_adoption["items"]) + api_surface = cast(dict[str, object], payload["api_surface"]) + api_summary = cast(dict[str, object], api_surface["summary"]) + api_items = cast(list[dict[str, object]], api_surface["items"]) + + assert any(item["module"] == "pkg.docs" for item in adoption_items) + assert api_summary["baseline_diff_available"] is True + assert api_summary["added"] == 1 + assert api_summary["breaking"] == 2 + assert any(item.get("record_kind") == "breaking_change" for item in api_items) + + def test_metric_gate_reasons_skip_disabled_and_non_critical_paths() -> None: reasons = metric_gate_reasons( project_metrics=_project_metrics(dead_confidence="medium"), @@ -473,3 +851,54 @@ def test_metric_gate_reasons_new_metrics_optional_buckets_empty() -> None: "New dependency cycles vs metrics baseline: 1.", "Health score regressed vs metrics baseline: delta=-2.", ) + + +def test_metric_gate_reasons_include_adoption_and_api_surface_contracts() -> None: + reasons = metric_gate_reasons( + project_metrics=_project_metrics(dead_confidence="medium"), + metrics_diff=MetricsDiff( + new_high_risk_functions=(), + new_high_coupling_classes=(), + new_cycles=(), + new_dead_code=(), + health_delta=0, + typing_param_permille_delta=-125, + typing_return_permille_delta=-250, + docstring_permille_delta=-333, + new_api_breaking_changes=( + ApiBreakingChange( + qualname="pkg.mod:run", + filepath="pkg/mod.py", + start_line=10, + end_line=12, + symbol_kind="function", + change_kind="signature_break", + detail="Parameter value became required.", + ), + ), + ), + config=MetricGateConfig( + fail_complexity=-1, + fail_coupling=-1, + fail_cohesion=-1, + fail_cycles=False, + fail_dead_code=False, + fail_health=-1, + fail_on_new_metrics=False, + fail_on_typing_regression=True, + fail_on_docstring_regression=True, + fail_on_api_break=True, + min_typing_coverage=80, + min_docstring_coverage=70, + ), + ) + assert reasons == ( + "Typing coverage below threshold: coverage=0.0%, threshold=80%.", + "Docstring coverage below threshold: coverage=0.0%, threshold=70%.", + ( + "Typing coverage regressed vs metrics baseline: " + "params_delta=-125, returns_delta=-250." + ), + "Docstring coverage regressed vs metrics baseline: delta=-333.", + "Public API breaking changes vs metrics baseline: 1.", + ) diff --git a/tests/test_report_contract_coverage.py b/tests/test_report_contract_coverage.py index e961243..8d086db 100644 --- a/tests/test_report_contract_coverage.py +++ b/tests/test_report_contract_coverage.py @@ -1218,6 +1218,18 @@ def test_markdown_render_long_list_branches() -> None: assert "... and 2 more item(s)" in markdown +def _metric_family_payload( + payload: dict[str, object], + family: str, +) -> tuple[dict[str, object], dict[str, object], list[dict[str, object]]]: + metrics = cast(dict[str, object], payload["metrics"]) + summary = cast(dict[str, object], metrics["summary"]) + families = cast(dict[str, object], metrics["families"]) + family_payload = cast(dict[str, object], families[family]) + items = cast(list[dict[str, object]], family_payload["items"]) + return summary, family_payload, items + + def test_report_contract_renderers_include_overloaded_modules_section() -> None: payload = _rich_report_document() @@ -1234,10 +1246,10 @@ def test_report_contract_renderers_include_overloaded_modules_section() -> None: def test_report_contract_includes_canonical_overloaded_modules_family() -> None: payload = _rich_report_document() - metrics = cast(dict[str, object], payload["metrics"]) - summary = cast(dict[str, object], metrics["summary"]) - families = cast(dict[str, object], metrics["families"]) - overloaded_modules = cast(dict[str, object], families["overloaded_modules"]) + summary, overloaded_modules, overloaded_items = _metric_family_payload( + payload, + "overloaded_modules", + ) overloaded_summary = cast(dict[str, object], overloaded_modules["summary"]) assert summary["overloaded_modules"] == overloaded_summary @@ -1249,10 +1261,16 @@ def test_report_contract_includes_canonical_overloaded_modules_family() -> None: "average_score": 0.58, "candidate_score_cutoff": 0.91, } - first = cast(list[dict[str, object]], overloaded_modules["items"])[0] - assert first["module"] == "codeclone.alpha" - assert first["relative_path"] == "codeclone/alpha.py" - assert first["candidate_status"] == "candidate" + first = overloaded_items[0] + assert ( + first["module"], + first["relative_path"], + first["candidate_status"], + ) == ( + "codeclone.alpha", + "codeclone/alpha.py", + "candidate", + ) assert first["candidate_reasons"] == [ "size_pressure", "dependency_pressure", @@ -1260,6 +1278,145 @@ def test_report_contract_includes_canonical_overloaded_modules_family() -> None: ] +def test_report_contract_includes_canonical_adoption_and_api_surface_families() -> None: + payload = build_report_document( + func_groups={}, + block_groups={}, + segment_groups={}, + meta={"scan_root": "/repo"}, + metrics={ + "coverage_adoption": { + "summary": { + "params_total": 4, + "params_annotated": 3, + "param_permille": 750, + "baseline_diff_available": True, + "param_delta": 125, + "returns_total": 2, + "returns_annotated": 1, + "return_permille": 500, + "return_delta": 250, + "public_symbol_total": 3, + "public_symbol_documented": 2, + "docstring_permille": 667, + "docstring_delta": 167, + "typing_any_count": 1, + }, + "items": [ + { + "module": "pkg.mod", + "filepath": "/repo/pkg/mod.py", + "callable_count": 2, + "params_total": 4, + "params_annotated": 3, + "param_permille": 750, + "returns_total": 2, + "returns_annotated": 1, + "return_permille": 500, + "any_annotation_count": 1, + "public_symbol_total": 3, + "public_symbol_documented": 2, + "docstring_permille": 667, + } + ], + }, + "api_surface": { + "summary": { + "enabled": True, + "baseline_diff_available": True, + "modules": 1, + "public_symbols": 2, + "added": 1, + "breaking": 1, + "strict_types": False, + }, + "items": [ + { + "record_kind": "symbol", + "module": "pkg.mod", + "filepath": "/repo/pkg/mod.py", + "qualname": "pkg.mod:run", + "start_line": 10, + "end_line": 12, + "symbol_kind": "function", + "exported_via": "name", + "params_total": 1, + "params": [ + { + "name": "value", + "kind": "pos_or_kw", + "has_default": False, + "annotated": True, + } + ], + "returns_annotated": True, + }, + { + "record_kind": "breaking_change", + "module": "pkg.mod", + "filepath": "/repo/pkg/mod.py", + "qualname": "pkg.mod:old", + "start_line": 20, + "end_line": 21, + "symbol_kind": "function", + "change_kind": "removed", + "detail": "Removed from the public API surface.", + }, + ], + }, + }, + ) + + summary, adoption, adoption_items = _metric_family_payload( + payload, + "coverage_adoption", + ) + adoption_summary = cast(dict[str, object], adoption["summary"]) + assert summary["coverage_adoption"] == adoption_summary + assert adoption_summary == { + "modules": 1, + "params_total": 4, + "params_annotated": 3, + "param_permille": 750, + "baseline_diff_available": True, + "param_delta": 125, + "returns_total": 2, + "returns_annotated": 1, + "return_permille": 500, + "return_delta": 250, + "public_symbol_total": 3, + "public_symbol_documented": 2, + "docstring_permille": 667, + "docstring_delta": 167, + "typing_any_count": 1, + } + adoption_item = adoption_items[0] + assert ( + adoption_item["module"], + adoption_item["relative_path"], + adoption_item["docstring_permille"], + ) == ("pkg.mod", "pkg/mod.py", 667) + + _, api_surface, api_items = _metric_family_payload(payload, "api_surface") + api_summary = cast(dict[str, object], api_surface["summary"]) + assert summary["api_surface"] == api_summary + assert api_summary == { + "enabled": True, + "baseline_diff_available": True, + "modules": 1, + "public_symbols": 2, + "added": 1, + "breaking": 1, + "strict_types": False, + } + assert ( + api_items[0]["record_kind"], + api_items[0]["relative_path"], + api_items[1]["record_kind"], + api_items[1]["change_kind"], + ) == ("symbol", "pkg/mod.py", "breaking_change", "removed") + + def test_sarif_helper_level_mapping() -> None: assert _severity_to_level("critical") == "error" assert _severity_to_level("warning") == "warning" @@ -1614,14 +1771,25 @@ def test_sarif_private_helper_family_dispatches() -> None: design_complexity = _sarif_rule_spec({"family": "design", "category": "complexity"}) design_coupling = _sarif_rule_spec({"family": "design", "category": "coupling"}) design_dependency = _sarif_rule_spec({"family": "design", "category": "dependency"}) - assert clone_function.rule_id == "CCLONE001" - assert clone_block.rule_id == "CCLONE002" - assert structural_guard.rule_id == "CSTRUCT002" - assert structural_drift.rule_id == "CSTRUCT003" - assert design_cohesion.rule_id == "CDESIGN001" - assert design_complexity.rule_id == "CDESIGN002" - assert design_coupling.rule_id == "CDESIGN003" - assert design_dependency.rule_id == "CDESIGN004" + assert ( + clone_function.rule_id, + clone_block.rule_id, + structural_guard.rule_id, + structural_drift.rule_id, + design_cohesion.rule_id, + design_complexity.rule_id, + design_coupling.rule_id, + design_dependency.rule_id, + ) == ( + "CCLONE001", + "CCLONE002", + "CSTRUCT002", + "CSTRUCT003", + "CDESIGN001", + "CDESIGN002", + "CDESIGN003", + "CDESIGN004", + ) assert ( _sarif_result_message( @@ -1923,9 +2091,23 @@ def test_collect_paths_from_metrics_covers_all_metric_families_and_skips_missing {"filepath": ""}, ] }, + "coverage_adoption": { + "items": [ + {"filepath": "/repo/adoption.py"}, + {"filepath": None}, + ] + }, + "api_surface": { + "items": [ + {"filepath": "/repo/api.py"}, + {"filepath": ""}, + ] + }, } assert _collect_paths_from_metrics(metrics) == { + "/repo/adoption.py", + "/repo/api.py", "/repo/complexity.py", "/repo/coupling.py", "/repo/cohesion.py", diff --git a/uv.lock b/uv.lock index 3b15243..9a67aa4 100644 --- a/uv.lock +++ b/uv.lock @@ -447,62 +447,62 @@ toml = [ [[package]] name = "cryptography" -version = "46.0.6" +version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" }, - { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" }, - { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" }, - { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" }, - { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" }, - { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" }, - { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" }, - { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" }, - { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" }, - { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" }, - { url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" }, - { url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147, upload-time = "2026-03-25T23:33:48.249Z" }, - { url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" }, - { url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" }, - { url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" }, - { url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" }, - { url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" }, - { url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" }, - { url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" }, - { url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511, upload-time = "2026-03-25T23:34:09.892Z" }, - { url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692, upload-time = "2026-03-25T23:34:11.613Z" }, - { url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" }, - { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" }, - { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" }, - { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" }, - { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" }, - { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" }, - { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" }, - { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" }, - { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" }, - { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" }, - { url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" }, - { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" }, - { url = "https://files.pythonhosted.org/packages/2e/84/7ccff00ced5bac74b775ce0beb7d1be4e8637536b522b5df9b73ada42da2/cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead", size = 3475444, upload-time = "2026-03-25T23:34:38.944Z" }, - { url = "https://files.pythonhosted.org/packages/bc/1f/4c926f50df7749f000f20eede0c896769509895e2648db5da0ed55db711d/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8", size = 4218227, upload-time = "2026-03-25T23:34:40.871Z" }, - { url = "https://files.pythonhosted.org/packages/c6/65/707be3ffbd5f786028665c3223e86e11c4cda86023adbc56bd72b1b6bab5/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0", size = 4381399, upload-time = "2026-03-25T23:34:42.609Z" }, - { url = "https://files.pythonhosted.org/packages/f3/6d/73557ed0ef7d73d04d9aba745d2c8e95218213687ee5e76b7d236a5030fc/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b", size = 4217595, upload-time = "2026-03-25T23:34:44.205Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c5/e1594c4eec66a567c3ac4400008108a415808be2ce13dcb9a9045c92f1a0/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a", size = 4380912, upload-time = "2026-03-25T23:34:46.328Z" }, - { url = "https://files.pythonhosted.org/packages/1a/89/843b53614b47f97fe1abc13f9a86efa5ec9e275292c457af1d4a60dc80e0/cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e", size = 3409955, upload-time = "2026-03-25T23:34:48.465Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, + { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, ] [[package]] @@ -864,11 +864,11 @@ wheels = [ [[package]] name = "more-itertools" -version = "11.0.1" +version = "11.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/24/e0acc4bf54cba50c1d432c70a72a3df96db4a321b2c4c68432a60759044f/more_itertools-11.0.1.tar.gz", hash = "sha256:fefaf25b7ab08f0b45fa9f1892cae93b9fc0089ef034d39213bce15f1cc9e199", size = 144739, upload-time = "2026-04-02T16:17:45.061Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/f7/139d22fef48ac78127d18e01d80cf1be40236ae489769d17f35c3d425293/more_itertools-11.0.2.tar.gz", hash = "sha256:392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804", size = 144659, upload-time = "2026-04-09T15:01:33.297Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/f4/5e52c7319b8087acef603ed6e50dc325c02eaa999355414830468611f13c/more_itertools-11.0.1-py3-none-any.whl", hash = "sha256:eaf287826069452a8f61026c597eae2428b2d1ba2859083abbf240b46842ce6d", size = 72182, upload-time = "2026-04-02T16:17:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl", hash = "sha256:6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4", size = 71939, upload-time = "2026-04-09T15:01:32.21Z" }, ] [[package]] @@ -1082,11 +1082,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.9.4" +version = "4.9.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, ] [[package]] @@ -1307,7 +1307,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.2" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1318,9 +1318,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] @@ -1339,15 +1339,15 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.2.1" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/88/815e53084c5079a59df912825a279f41dd2e0df82281770eadc732f5352c/python_discovery-1.2.1.tar.gz", hash = "sha256:180c4d114bff1c32462537eac5d6a332b768242b76b69c0259c7d14b1b680c9e", size = 58457, upload-time = "2026-03-26T22:30:44.496Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl", hash = "sha256:b6a957b24c1cd79252484d3566d1b49527581d46e789aaf43181005e56201502", size = 31674, upload-time = "2026-03-26T22:30:43.396Z" }, + { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, ] [[package]] @@ -1664,27 +1664,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e6/97/e9f1ca355108ef7194e38c812ef40ba98c7208f47b13ad78d023caa583da/ruff-0.15.9.tar.gz", hash = "sha256:29cbb1255a9797903f6dde5ba0188c707907ff44a9006eb273b5a17bfa0739a2", size = 4617361, upload-time = "2026-04-02T18:17:20.829Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/1f/9cdfd0ac4b9d1e5a6cf09bedabdf0b56306ab5e333c85c87281273e7b041/ruff-0.15.9-py3-none-linux_armv6l.whl", hash = "sha256:6efbe303983441c51975c243e26dff328aca11f94b70992f35b093c2e71801e1", size = 10511206, upload-time = "2026-04-02T18:16:41.574Z" }, - { url = "https://files.pythonhosted.org/packages/3d/f6/32bfe3e9c136b35f02e489778d94384118bb80fd92c6d92e7ccd97db12ce/ruff-0.15.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4965bac6ac9ea86772f4e23587746f0b7a395eccabb823eb8bfacc3fa06069f7", size = 10923307, upload-time = "2026-04-02T18:17:08.645Z" }, - { url = "https://files.pythonhosted.org/packages/ca/25/de55f52ab5535d12e7aaba1de37a84be6179fb20bddcbe71ec091b4a3243/ruff-0.15.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf05aad70ca5b5a0a4b0e080df3a6b699803916d88f006efd1f5b46302daab8", size = 10316722, upload-time = "2026-04-02T18:16:44.206Z" }, - { url = "https://files.pythonhosted.org/packages/48/11/690d75f3fd6278fe55fff7c9eb429c92d207e14b25d1cae4064a32677029/ruff-0.15.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9439a342adb8725f32f92732e2bafb6d5246bd7a5021101166b223d312e8fc59", size = 10623674, upload-time = "2026-04-02T18:16:50.951Z" }, - { url = "https://files.pythonhosted.org/packages/bd/ec/176f6987be248fc5404199255522f57af1b4a5a1b57727e942479fec98ad/ruff-0.15.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5e6faf9d97c8edc43877c3f406f47446fc48c40e1442d58cfcdaba2acea745", size = 10351516, upload-time = "2026-04-02T18:16:57.206Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fc/51cffbd2b3f240accc380171d51446a32aa2ea43a40d4a45ada67368fbd2/ruff-0.15.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b34a9766aeec27a222373d0b055722900fbc0582b24f39661aa96f3fe6ad901", size = 11150202, upload-time = "2026-04-02T18:17:06.452Z" }, - { url = "https://files.pythonhosted.org/packages/d6/d4/25292a6dfc125f6b6528fe6af31f5e996e19bf73ca8e3ce6eb7fa5b95885/ruff-0.15.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89dd695bc72ae76ff484ae54b7e8b0f6b50f49046e198355e44ea656e521fef9", size = 11988891, upload-time = "2026-04-02T18:17:18.575Z" }, - { url = "https://files.pythonhosted.org/packages/13/e1/1eebcb885c10e19f969dcb93d8413dfee8172578709d7ee933640f5e7147/ruff-0.15.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce187224ef1de1bd225bc9a152ac7102a6171107f026e81f317e4257052916d5", size = 11480576, upload-time = "2026-04-02T18:16:52.986Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6b/a1548ac378a78332a4c3dcf4a134c2475a36d2a22ddfa272acd574140b50/ruff-0.15.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0c7c341f68adb01c488c3b7d4b49aa8ea97409eae6462d860a79cf55f431b6", size = 11254525, upload-time = "2026-04-02T18:17:02.041Z" }, - { url = "https://files.pythonhosted.org/packages/42/aa/4bb3af8e61acd9b1281db2ab77e8b2c3c5e5599bf2a29d4a942f1c62b8d6/ruff-0.15.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:55cc15eee27dc0eebdfcb0d185a6153420efbedc15eb1d38fe5e685657b0f840", size = 11204072, upload-time = "2026-04-02T18:17:13.581Z" }, - { url = "https://files.pythonhosted.org/packages/69/48/d550dc2aa6e423ea0bcc1d0ff0699325ffe8a811e2dba156bd80750b86dc/ruff-0.15.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6537f6eed5cda688c81073d46ffdfb962a5f29ecb6f7e770b2dc920598997ed", size = 10594998, upload-time = "2026-04-02T18:16:46.369Z" }, - { url = "https://files.pythonhosted.org/packages/63/47/321167e17f5344ed5ec6b0aa2cff64efef5f9e985af8f5622cfa6536043f/ruff-0.15.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6d3fcbca7388b066139c523bda744c822258ebdcfbba7d24410c3f454cc9af71", size = 10359769, upload-time = "2026-04-02T18:17:10.994Z" }, - { url = "https://files.pythonhosted.org/packages/67/5e/074f00b9785d1d2c6f8c22a21e023d0c2c1817838cfca4c8243200a1fa87/ruff-0.15.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:058d8e99e1bfe79d8a0def0b481c56059ee6716214f7e425d8e737e412d69677", size = 10850236, upload-time = "2026-04-02T18:16:48.749Z" }, - { url = "https://files.pythonhosted.org/packages/76/37/804c4135a2a2caf042925d30d5f68181bdbd4461fd0d7739da28305df593/ruff-0.15.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8e1ddb11dbd61d5983fa2d7d6370ef3eb210951e443cace19594c01c72abab4c", size = 11358343, upload-time = "2026-04-02T18:16:55.068Z" }, - { url = "https://files.pythonhosted.org/packages/88/3d/1364fcde8656962782aa9ea93c92d98682b1ecec2f184e625a965ad3b4a6/ruff-0.15.9-py3-none-win32.whl", hash = "sha256:bde6ff36eaf72b700f32b7196088970bf8fdb2b917b7accd8c371bfc0fd573ec", size = 10583382, upload-time = "2026-04-02T18:17:04.261Z" }, - { url = "https://files.pythonhosted.org/packages/4c/56/5c7084299bd2cacaa07ae63a91c6f4ba66edc08bf28f356b24f6b717c799/ruff-0.15.9-py3-none-win_amd64.whl", hash = "sha256:45a70921b80e1c10cf0b734ef09421f71b5aa11d27404edc89d7e8a69505e43d", size = 11744969, upload-time = "2026-04-02T18:16:59.611Z" }, - { url = "https://files.pythonhosted.org/packages/03/36/76704c4f312257d6dbaae3c959add2a622f63fcca9d864659ce6d8d97d3d/ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53", size = 11005870, upload-time = "2026-04-02T18:17:15.773Z" }, +version = "0.15.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" }, + { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" }, + { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" }, + { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" }, + { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" }, + { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" }, + { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" }, + { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" }, + { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" }, + { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" }, + { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" }, + { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" }, + { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" }, ] [[package]] From b88cc115160b07268c9ec07ef4b862274edf9909 Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Mon, 13 Apr 2026 18:11:39 +0500 Subject: [PATCH 07/17] feat: add coverage join and golden-fixture clone exclusions - join external Cobertura coverage.xml into current-run metrics using stdlib XML parsing - surface coverage join facts consistently in CLI, reports, MCP, SARIF, and HTML - distinguish measured coverage hotspots from coverage scope gaps in canonical findings - add project-level golden_fixture_paths policy and carry excluded clone groups as suppressed report facts - surface suppressed golden fixtures in Clones UI/CLI without affecting health or gates - fix cached segment projection branch behavior and benchmark/cache regressions - preserve cached public API parameter order and keep warm/cold API diffs stable - bump canonical report schema and refresh docs, MCP contracts, and regression tests --- CHANGELOG.md | 83 +- README.md | 21 +- benchmarks/__init__.py | 5 + benchmarks/run_benchmark.py | 40 + codeclone.baseline.json | 13300 +--------------- codeclone/_cli_args.py | 20 + codeclone/_cli_baselines.py | 5 + codeclone/_cli_config.py | 30 +- codeclone/_cli_gating.py | 15 + codeclone/_cli_runtime.py | 11 +- codeclone/_cli_summary.py | 53 + codeclone/_html_badges.py | 63 +- codeclone/_html_css.py | 68 +- codeclone/_html_js.py | 37 + codeclone/_html_report/_assemble.py | 10 + codeclone/_html_report/_components.py | 1 + codeclone/_html_report/_glossary.py | 41 + codeclone/_html_report/_sections/_clones.py | 98 +- codeclone/_html_report/_sections/_coupling.py | 35 +- .../_html_report/_sections/_coverage_join.py | 254 + codeclone/_html_report/_sections/_overview.py | 145 +- codeclone/_html_report/_tables.py | 3 +- codeclone/cache.py | 21 +- codeclone/cli.py | 73 +- codeclone/contracts.py | 4 +- codeclone/domain/findings.py | 8 + codeclone/golden_fixtures.py | 178 + codeclone/mcp_server.py | 21 +- codeclone/mcp_service.py | 148 +- codeclone/metrics/__init__.py | 3 + codeclone/metrics/coverage_join.py | 331 + codeclone/metrics_baseline.py | 48 +- codeclone/models.py | 39 + codeclone/paths.py | 44 +- codeclone/pipeline.py | 399 +- codeclone/report/derived.py | 40 +- codeclone/report/json_contract.py | 348 +- codeclone/report/markdown.py | 98 +- codeclone/report/overview.py | 16 + codeclone/report/sarif.py | 37 + codeclone/report/serialize.py | 125 +- codeclone/ui_messages.py | 129 +- docs/README.md | 6 +- docs/architecture.md | 2 +- docs/book/04-config-and-defaults.md | 98 + docs/book/05-core-pipeline.md | 29 +- docs/book/06-baseline.md | 2 + docs/book/07-cache.md | 6 +- docs/book/08-report.md | 37 +- docs/book/09-cli.md | 31 +- docs/book/10-html-render.md | 11 + docs/book/13-testing-as-spec.md | 23 +- docs/book/14-compatibility-and-versioning.md | 14 +- docs/book/15-metrics-and-quality-gates.md | 42 +- docs/book/20-mcp-interface.md | 87 +- docs/book/appendix/b-schema-layouts.md | 61 +- docs/mcp.md | 29 +- docs/sarif.md | 6 + pyproject.toml | 16 + tests/_assertions.py | 10 + tests/_ast_metrics_helpers.py | 29 + .../golden_expected_cli_snapshot.json | 2 +- tests/test_adoption.py | 25 +- tests/test_api_surface.py | 26 +- tests/test_benchmark.py | 80 + tests/test_cache.py | 56 + tests/test_cli_config.py | 31 + tests/test_cli_inprocess.py | 239 +- tests/test_cli_unit.py | 258 +- tests/test_coverage_join.py | 294 + tests/test_golden_fixtures.py | 140 + tests/test_html_report.py | 406 +- tests/test_html_report_helpers.py | 12 +- tests/test_mcp_service.py | 195 + tests/test_metrics_baseline.py | 90 +- tests/test_pipeline_metrics.py | 203 + tests/test_pipeline_process.py | 15 + tests/test_report.py | 122 + tests/test_report_contract_coverage.py | 227 +- uv.lock | 278 +- 80 files changed, 5757 insertions(+), 13929 deletions(-) create mode 100644 benchmarks/__init__.py create mode 100644 codeclone/_html_report/_sections/_coverage_join.py create mode 100644 codeclone/golden_fixtures.py create mode 100644 codeclone/metrics/coverage_join.py create mode 100644 tests/_ast_metrics_helpers.py create mode 100644 tests/test_benchmark.py create mode 100644 tests/test_coverage_join.py create mode 100644 tests/test_golden_fixtures.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 17330e9..78974cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,56 +2,43 @@ ## [2.0.0b5] +Expands the canonical contract with adoption, API-surface, and coverage-join layers; clarifies run interpretation +across MCP/HTML/clients; tightens MCP launcher/runtime behavior. + ### Contracts, metrics, and review surfaces -- Bump canonical report schema to `2.5` for `metrics.families.coverage_adoption` - and `metrics.families.api_surface`. -- Bump clone baseline schema to `2.1` and standalone metrics-baseline schema to - `1.2` for compact `api_surface` wire payloads (`local_name` on disk, - reconstructed full qualnames in runtime) while keeping read-compatibility for - earlier `2.0` / `1.1` baseline files in the current b5 line. -- Add shared public/private visibility classification for public-symbol-aware - metrics, without changing clone/fingerprint semantics. -- Add canonical type/docstring adoption coverage: - parameter coverage, return coverage, public docstring coverage, and explicit - `Any` counts. -- Add opt-in public API surface inventory and baseline diff: - public symbol snapshots, added symbols, and breaking changes against a - trusted metrics baseline. -- Add new gates: - `--min-typing-coverage`, `--min-docstring-coverage`, - `--fail-on-typing-regression`, `--fail-on-docstring-regression`, - `--fail-on-api-break`. -- Surface adoption and API metrics compactly in MCP summaries/detail, the HTML - Overview tab, and canonical report payloads without adding a new HTML tab. -- Extend the normal CLI `Metrics` block with adoption coverage and public API - facts, while keeping the quiet compact metrics line unchanged. -- Make unified clone baselines preserve embedded metrics and optional - `api_surface` payloads safely across saves. - -### MCP, HTML, and docs - -- Surface the effective runtime analysis profile (`min_loc`, `min_stmt`, block, and segment thresholds) in canonical - report metadata, MCP summary/triage projections, and the HTML Executive Summary subtitle. -- Clarify MCP interpretation with compact `health_scope`, `focus`, and `new_by_source_kind` fields in summary/triage - projections. -- Make baseline mismatch handling more explicit in MCP and the VS Code client by surfacing baseline/runtime python tags - and whether comparison is proceeding without a valid baseline. -- Make the Claude Desktop bundle and Codex plugin prefer workspace-local launchers before `PATH`, with Poetry environment fallback for - python-tag-safe MCP startup. -- Add `workspace_root` user-config field to the Claude Desktop bundle: setting it to the project directory forces the - launcher to prefer `.venv` inside that path even when Claude Desktop starts with a different working directory - (fixes python-tag mismatch caused by system-wide interpreter fallback). -- Validate `git_diff_ref` inputs as safe single revision expressions in both - CLI and MCP before invoking `git diff`. -- Replace the segment-group raw digest `repr()` payload with canonical JSON - bytes for cross-version-safe determinism. -- Align the tests workflow coverage gate with the canonical `fail_under = 99` - policy and refresh the remaining `actions/checkout` pin in `codeclone.yml`. -- Refresh branch metadata and client docs for the `2.0.0b5` line. -- Update the README repository health badge to `87 (B)`. - -## [2.0.0b4] +- Report schema `2.8`: add `coverage_adoption`, `api_surface`, `coverage_join`, and optional + `clones.suppressed.*` (for `golden_fixture_paths`); separate coverage hotspots vs scope gaps. +- Baselines: clone `2.1`, metrics `1.2`; compact `api_surface` payload (`local_name` on disk, qualnames at runtime); + read-compatible with `2.0` / `1.1`. +- Add public/private visibility classification for public-symbol metrics (no clone/fingerprint changes). +- Add annotation/docstring adoption coverage: parameter, return, public docstrings, explicit `Any`. +- Add opt-in API surface inventory + baseline diff (snapshots, additions, breaking changes). +- Add coverage join (`--coverage`): per-function facts + findings for below-threshold or missing-in-scope functions; + current-run only (not baseline truth, no fingerprint impact). +- Add `golden_fixture_paths`: exclude matching clone groups from health/gates while keeping suppressed facts. +- Add gates: `--min-typing-coverage`, `--min-docstring-coverage`, `--fail-on-typing-regression`, + `--fail-on-docstring-regression`, `--fail-on-api-break`, `--fail-on-untested-hotspots`, `--coverage-min`. +- Surface adoption/API/coverage-join in MCP, CLI Metrics, report payloads, and HTML (Overview + Quality subtab). +- Preserve embedded metrics and optional `api_surface` in unified baselines. +- Cache `2.4`: drop stale API-surface entries; preserve parameter order; align warm/cold API diffs. + +### MCP, HTML, and client interpretation + +- Surface effective analysis profile in report meta, MCP summary/triage, and HTML subtitle. +- Add `health_scope`, `focus`, `new_by_source_kind` to MCP summary/triage. +- Make baseline mismatch explicit (python tags + no-valid-baseline signal). +- Prefer workspace-local launchers over `PATH` (Poetry fallback). +- Add `workspace_root` to force project `.venv` selection. + +### Safety and maintenance + +- Validate `git_diff_ref` as safe single-revision expressions. +- Replace segment digest `repr()` with canonical JSON bytes (determinism). +- Align CI coverage gate (`fail_under = 99`) and refresh `actions/checkout` pin. +- Refresh branch metadata/docs for `2.0.0b5`; update README badge to `89 (B)`. + +## [2.0.0b4] - 2026-04-05 ### MCP server diff --git a/README.md b/README.md index 1426fe8..57c4328 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Tests Benchmark Python - codeclone 87 (B) + codeclone 89 (B) License

@@ -43,8 +43,8 @@ Live sample report: - **Clone detection** — function (CFG fingerprint), block (statement windows), and segment (report-only) clones - **Structural findings** — duplicated branch families, clone guard/exit divergence and clone-cohort drift (report-only) - **Quality metrics** — cyclomatic complexity, coupling (`CBO`), cohesion (`LCOM4`), dependency cycles, dead code, - health score, type/docstring adoption coverage, public API surface diff, and report-only `Overloaded Modules` - profiling + health score, type/docstring adoption coverage, current-run Cobertura coverage join, public API surface diff, and + report-only `Overloaded Modules` profiling - **Baseline governance** — separates accepted **legacy** debt from **new regressions** and lets CI fail **only** on what changed - **Reports** — interactive HTML, deterministic JSON/TXT plus Markdown and SARIF projections from one canonical report @@ -148,11 +148,17 @@ codeclone . --min-typing-coverage 80 --min-docstring-coverage 60 codeclone . --fail-on-typing-regression --fail-on-docstring-regression codeclone . --api-surface --update-metrics-baseline codeclone . --fail-on-api-break + +# Current-run Cobertura hotspot gate +codeclone . --coverage coverage.xml --fail-on-untested-hotspots --coverage-min 50 ``` In normal full-mode CLI output, CodeClone now surfaces adoption coverage (`params`, `returns`, `docstrings`, `Any`) in the main `Metrics` block, and it -adds a `Public API` line when `--api-surface` facts are collected. +adds a `Public API` line when `--api-surface` facts are collected. Passing +`--coverage FILE` adds a `Coverage` line from external Cobertura XML, surfaces +joined details under HTML `Quality -> Coverage Join` and MCP/report +`coverage_join`, and does not update the clone baseline. ### Pre-commit @@ -208,6 +214,7 @@ CodeClone can load project-level configuration from `pyproject.toml`: min_loc = 10 min_stmt = 6 baseline = "codeclone.baseline.json" +golden_fixture_paths = ["tests/fixtures/golden_*"] skip_metrics = false quiet = false html_out = ".cache/codeclone/report.html" @@ -288,11 +295,11 @@ class Middleware: # codeclone: ignore[dead-code] Dynamic/runtime false positives are resolved via explicit inline suppressions, not via broad heuristics.
-Canonical JSON report shape (v2.5) +Canonical JSON report shape (v2.8) ```json { - "report_schema_version": "2.5", + "report_schema_version": "2.8", "meta": { "codeclone_version": "2.0.0b5", "project_name": "...", @@ -362,11 +369,13 @@ Dynamic/runtime false positives are resolved via explicit inline suppressions, n "summary": { "...": "...", "coverage_adoption": { "...": "..." }, + "coverage_join": { "...": "..." }, "api_surface": { "...": "..." } }, "families": { "...": "...", "coverage_adoption": { "...": "..." }, + "coverage_join": { "...": "..." }, "api_surface": { "...": "..." } } }, diff --git a/benchmarks/__init__.py b/benchmarks/__init__.py new file mode 100644 index 0000000..9135843 --- /dev/null +++ b/benchmarks/__init__.py @@ -0,0 +1,5 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Den Rozhnovskiy diff --git a/benchmarks/run_benchmark.py b/benchmarks/run_benchmark.py index ba12356..4431188 100755 --- a/benchmarks/run_benchmark.py +++ b/benchmarks/run_benchmark.py @@ -176,6 +176,45 @@ def _run_cli_once( ) +def _validate_inventory_sample( + *, + scenario: Scenario, + measurement: RunMeasurement, +) -> None: + if measurement.files_found <= 0: + raise RuntimeError( + f"scenario {scenario.name} produced an empty inventory sample; " + "benchmark target is invalid" + ) + if measurement.files_skipped > 0: + raise RuntimeError( + f"scenario {scenario.name} skipped {measurement.files_skipped} files; " + "benchmark run is invalid" + ) + if scenario.mode == "cold": + if measurement.files_cached != 0: + raise RuntimeError( + f"cold scenario {scenario.name} unexpectedly used cache: " + f"cached={measurement.files_cached}" + ) + if measurement.files_analyzed <= 0: + raise RuntimeError( + f"cold scenario {scenario.name} analyzed no files: " + f"found={measurement.files_found} analyzed={measurement.files_analyzed}" + ) + return + if measurement.files_cached <= 0: + raise RuntimeError( + f"warm scenario {scenario.name} did not use cache: " + f"cached={measurement.files_cached}" + ) + if measurement.files_analyzed != 0: + raise RuntimeError( + f"warm scenario {scenario.name} analyzed files unexpectedly: " + f"analyzed={measurement.files_analyzed}" + ) + + def _scenario_result( *, scenario: Scenario, @@ -230,6 +269,7 @@ def _scenario_result( report_path=scenario_dir / f"run-report-{idx}.json", extra_args=scenario.extra_args, ) + _validate_inventory_sample(scenario=scenario, measurement=measurement) measurements.append(measurement) digests = sorted({m.digest for m in measurements}) diff --git a/codeclone.baseline.json b/codeclone.baseline.json index f09d76d..b4656f8 100644 --- a/codeclone.baseline.json +++ b/codeclone.baseline.json @@ -7,25 +7,13 @@ "schema_version": "2.1", "fingerprint_version": "1", "python_tag": "cp313", - "created_at": "2026-04-09T17:35:38Z", - "payload_sha256": "c0b4a5a4f5aa567069a48e896c36a1792a8dfa5bd306f130084d9ae2b1d4e42c", - "metrics_payload_sha256": "d8949db71b78a98ae69c7ed44bc9a51a516a78d6af96cf0949ddbb8bee4401b6", - "api_surface_payload_sha256": "72e5bbf17f0ddefe404d13a017010390994470d7e2546d1d80d20fdeb9feff81" + "created_at": "2026-04-13T13:10:37Z", + "payload_sha256": "07a383c1d0974593c83ac30430aec9b99d89fe50f640a9b3b433658e0bd029e8", + "metrics_payload_sha256": "122ee5d2d3dc2d4e9553b1d440c0314515dcb60cc79ada264b13c39c6ba18e04" }, "clones": { - "functions": [ - "efc8465229b381a3a50502d59d9539c0be3efe86|20-49" - ], - "blocks": [ - "3c1b5cf24b4dfcd8e5736b735bfd3850940100d5|3c1b5cf24b4dfcd8e5736b735bfd3850940100d5|3c1b5cf24b4dfcd8e5736b735bfd3850940100d5|3c1b5cf24b4dfcd8e5736b735bfd3850940100d5", - "3c1b5cf24b4dfcd8e5736b735bfd3850940100d5|3c1b5cf24b4dfcd8e5736b735bfd3850940100d5|3c1b5cf24b4dfcd8e5736b735bfd3850940100d5|cb4fcbc1b2a65ec1346898fc0d660335e25d7cbc", - "8579659a9e8c9755a6d2f0b1d82dda8866fd243b|1912d2ee3c541cbf9e51f485348586afe1a00755|ee69aff0b7ea38927e5082ceef14115c805f6734|ee69aff0b7ea38927e5082ceef14115c805f6734", - "b4b5893be87edf98955f047cbf25ca755dc753b4|8579659a9e8c9755a6d2f0b1d82dda8866fd243b|1912d2ee3c541cbf9e51f485348586afe1a00755|ee69aff0b7ea38927e5082ceef14115c805f6734", - "b6ee70d0bd6ff4b593f127a137aed9ab41179145|cacc33d58f323481f65fed57873d1c840531859e|d60c0005a4c850c140378d1c82b81dde93a7ccab|d60c0005a4c850c140378d1c82b81dde93a7ccab", - "cacc33d58f323481f65fed57873d1c840531859e|d60c0005a4c850c140378d1c82b81dde93a7ccab|d60c0005a4c850c140378d1c82b81dde93a7ccab|b4b5893be87edf98955f047cbf25ca755dc753b4", - "dd877bb74646fec86e10e1b27f5f0d538f1b8311|58eeda1867bae218607b5c1e688b99794bf59b2d|6e2ea78e278f92a5d09654ea6ceaa8a703cccdd7|6ad0cbb6a95fa366a456b907d5f7bfe1c1590c38", - "ee69aff0b7ea38927e5082ceef14115c805f6734|fcd36b4275c94f1955fb55e1c1ca3c04c7c0bb26|3c1b5cf24b4dfcd8e5736b735bfd3850940100d5|3c1b5cf24b4dfcd8e5736b735bfd3850940100d5" - ] + "functions": [], + "blocks": [] }, "metrics": { "max_complexity": 20, @@ -35,13281 +23,9 @@ "max_cohesion": 3, "low_cohesion_classes": [], "dependency_cycles": [], - "dependency_max_depth": 10, + "dependency_max_depth": 11, "dead_code_items": [], - "health_score": 87, - "health_grade": "B", - "typing_param_permille": 1000, - "typing_return_permille": 998, - "docstring_permille": 16, - "typing_any_count": 44 - }, - "api_surface": { - "modules": [ - { - "module": "codeclone._cli_gating", - "filepath": "codeclone/_cli_gating.py", - "all_declared": [ - "parse_metric_reason_entry", - "policy_context", - "print_gating_failure_block" - ], - "symbols": [ - { - "local_name": "parse_metric_reason_entry", - "kind": "function", - "start_line": 59, - "end_line": 121, - "params": [ - { - "name": "reason", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - } - ], - "returns_hash": "Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='str', ctx=Load())], ctx=Load()), ctx=Load())", - "exported_via": "all" - }, - { - "local_name": "policy_context", - "kind": "function", - "start_line": 124, - "end_line": 181, - "params": [ - { - "name": "args", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='_GatingArgs', ctx=Load())" - }, - { - "name": "gate_kind", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "all" - }, - { - "local_name": "print_gating_failure_block", - "kind": "function", - "start_line": 184, - "end_line": 197, - "params": [ - { - "name": "args", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='_GatingArgs', ctx=Load())" - }, - { - "name": "code", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "console", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='_PrinterLike', ctx=Load())" - }, - { - "name": "entries", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "BinOp(left=Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load()), Constant(value=Ellipsis)], ctx=Load()), ctx=Load()), op=BitOr(), right=Subscript(value=Name(id='list', ctx=Load()), slice=Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load()), ctx=Load()))" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "all" - } - ] - }, - { - "module": "codeclone.baseline", - "filepath": "codeclone/baseline.py", - "all_declared": [], - "symbols": [ - { - "local_name": "BASELINE_GENERATOR", - "kind": "constant", - "start_line": 37, - "end_line": 37, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "BASELINE_UNTRUSTED_STATUSES", - "kind": "constant", - "start_line": 57, - "end_line": 71, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "Baseline", - "kind": "class", - "start_line": 104, - "end_line": 430, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "Baseline.__init__", - "kind": "method", - "start_line": 118, - "end_line": 128, - "params": [ - { - "name": "path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Name(id='Path', ctx=Load()))" - } - ], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "Baseline.diff", - "kind": "method", - "start_line": 425, - "end_line": 430, - "params": [ - { - "name": "block_groups", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())" - }, - { - "name": "func_groups", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())" - } - ], - "returns_hash": "Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Subscript(value=Name(id='set', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load()), Subscript(value=Name(id='set', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "Baseline.from_groups", - "kind": "method", - "start_line": 404, - "end_line": 423, - "params": [ - { - "name": "block_groups", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())" - }, - { - "name": "fingerprint_version", - "kind": "pos_or_kw", - "has_default": true, - "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "func_groups", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())" - }, - { - "name": "generator_version", - "kind": "pos_or_kw", - "has_default": true, - "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "path", - "kind": "pos_or_kw", - "has_default": true, - "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Name(id='Path', ctx=Load()))" - }, - { - "name": "python_tag", - "kind": "pos_or_kw", - "has_default": true, - "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "schema_version", - "kind": "pos_or_kw", - "has_default": true, - "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Constant(value=None))" - } - ], - "returns_hash": "Name(id='Baseline', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "Baseline.load", - "kind": "method", - "start_line": 130, - "end_line": 234, - "params": [ - { - "name": "max_size_bytes", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Name(id='int', ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "preloaded_payload", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='Any', ctx=Load())], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "Baseline.save", - "kind": "method", - "start_line": 236, - "end_line": 298, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "Baseline.verify_compatibility", - "kind": "method", - "start_line": 300, - "end_line": 356, - "params": [ - { - "name": "current_python_tag", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "Baseline.verify_integrity", - "kind": "method", - "start_line": 358, - "end_line": 401, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "BaselineStatus", - "kind": "class", - "start_line": 42, - "end_line": 54, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "MAX_BASELINE_SIZE_BYTES", - "kind": "constant", - "start_line": 39, - "end_line": 39, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "coerce_baseline_status", - "kind": "function", - "start_line": 74, - "end_line": 84, - "params": [ - { - "name": "raw_status", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "BinOp(left=BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Name(id='BaselineStatus', ctx=Load())), op=BitOr(), right=Constant(value=None))" - } - ], - "returns_hash": "Name(id='BaselineStatus', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "current_python_tag", - "kind": "function", - "start_line": 650, - "end_line": 655, - "params": [], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - } - ] - }, - { - "module": "codeclone.cache", - "filepath": "codeclone/cache.py", - "all_declared": [], - "symbols": [ - { - "local_name": "AnalysisProfile", - "kind": "class", - "start_line": 234, - "end_line": 240, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ApiParamSpecDict", - "kind": "class", - "start_line": 175, - "end_line": 179, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "BlockDict", - "kind": "constant", - "start_line": 116, - "end_line": 116, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "Cache", - "kind": "class", - "start_line": 316, - "end_line": 836, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "Cache.__init__", - "kind": "method", - "start_line": 335, - "end_line": 374, - "params": [ - { - "name": "block_min_loc", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "block_min_stmt", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "max_size_bytes", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Name(id='int', ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "min_loc", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "min_stmt", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "root", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Name(id='Path', ctx=Load())), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "segment_min_loc", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "segment_min_stmt", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Name(id='Path', ctx=Load()))" - } - ], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "Cache.get_file_entry", - "kind": "method", - "start_line": 659, - "end_line": 732, - "params": [ - { - "name": "filepath", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - } - ], - "returns_hash": "BinOp(left=Name(id='CacheEntry', ctx=Load()), op=BitOr(), right=Constant(value=None))", - "exported_via": "name" - }, - { - "local_name": "Cache.load", - "kind": "method", - "start_line": 447, - "end_line": 494, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "Cache.put_file_entry", - "kind": "method", - "start_line": 734, - "end_line": 836, - "params": [ - { - "name": "file_metrics", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Name(id='FileMetrics', ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "source_stats", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Name(id='SourceStatsDict', ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "structural_findings", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Subscript(value=Name(id='list', ctx=Load()), slice=Name(id='StructuralFindingGroup', ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "blocks", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='list', ctx=Load()), slice=Name(id='BlockUnit', ctx=Load()), ctx=Load())" - }, - { - "name": "filepath", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "segments", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='list', ctx=Load()), slice=Name(id='SegmentUnit', ctx=Load()), ctx=Load())" - }, - { - "name": "stat_sig", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='FileStat', ctx=Load())" - }, - { - "name": "units", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='list', ctx=Load()), slice=Name(id='Unit', ctx=Load()), ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "Cache.save", - "kind": "method", - "start_line": 594, - "end_line": 635, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "CacheData", - "kind": "class", - "start_line": 243, - "end_line": 248, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "CacheEntry", - "kind": "class", - "start_line": 219, - "end_line": 231, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "CacheEntryBase", - "kind": "class", - "start_line": 212, - "end_line": 216, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "CacheStatus", - "kind": "class", - "start_line": 89, - "end_line": 100, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ClassMetricsDict", - "kind": "class", - "start_line": 133, - "end_line": 134, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ClassMetricsDictBase", - "kind": "class", - "start_line": 120, - "end_line": 130, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "DeadCandidateDict", - "kind": "class", - "start_line": 153, - "end_line": 154, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "DeadCandidateDictBase", - "kind": "class", - "start_line": 144, - "end_line": 150, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "FileStat", - "kind": "class", - "start_line": 103, - "end_line": 105, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "LEGACY_CACHE_SECRET_FILENAME", - "kind": "constant", - "start_line": 78, - "end_line": 78, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "MAX_CACHE_SIZE_BYTES", - "kind": "constant", - "start_line": 77, - "end_line": 77, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ModuleApiSurfaceDict", - "kind": "class", - "start_line": 192, - "end_line": 196, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ModuleDepDict", - "kind": "class", - "start_line": 137, - "end_line": 141, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ModuleDocstringCoverageDict", - "kind": "class", - "start_line": 168, - "end_line": 172, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ModuleTypingCoverageDict", - "kind": "class", - "start_line": 157, - "end_line": 165, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "PublicSymbolDict", - "kind": "class", - "start_line": 182, - "end_line": 189, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SegmentDict", - "kind": "constant", - "start_line": 117, - "end_line": 117, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SegmentReportProjection", - "kind": "constant", - "start_line": 70, - "end_line": 70, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SourceStatsDict", - "kind": "class", - "start_line": 108, - "end_line": 112, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "StructuralFindingGroupDict", - "kind": "class", - "start_line": 205, - "end_line": 209, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "StructuralFindingOccurrenceDict", - "kind": "class", - "start_line": 199, - "end_line": 202, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "UnitDict", - "kind": "constant", - "start_line": 115, - "end_line": 115, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "build_segment_report_projection", - "kind": "constant", - "start_line": 71, - "end_line": 71, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "file_stat_signature", - "kind": "function", - "start_line": 839, - "end_line": 844, - "params": [ - { - "name": "path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - } - ], - "returns_hash": "Name(id='FileStat', ctx=Load())", - "exported_via": "name" - } - ] - }, - { - "module": "codeclone.cli", - "filepath": "codeclone/cli.py", - "all_declared": [ - "ExitCode", - "MAX_FILE_SIZE", - "ProcessingResult", - "analyze", - "bootstrap", - "discover", - "gate", - "main", - "process", - "process_file", - "report" - ], - "symbols": [ - { - "local_name": "MAX_FILE_SIZE", - "kind": "constant", - "start_line": 139, - "end_line": 139, - "params": [], - "returns_hash": "", - "exported_via": "all" - }, - { - "local_name": "ProcessingResult", - "kind": "class", - "start_line": 178, - "end_line": 192, - "params": [], - "returns_hash": "", - "exported_via": "all" - }, - { - "local_name": "analyze", - "kind": "function", - "start_line": 466, - "end_line": 479, - "params": [ - { - "name": "boot", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='BootstrapResult', ctx=Load())" - }, - { - "name": "discovery", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='DiscoveryResult', ctx=Load())" - }, - { - "name": "processing", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='PipelineProcessingResult', ctx=Load())" - } - ], - "returns_hash": "Name(id='AnalysisResult', ctx=Load())", - "exported_via": "all" - }, - { - "local_name": "bootstrap", - "kind": "function", - "start_line": 422, - "end_line": 437, - "params": [ - { - "name": "args", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='Namespace', ctx=Load())" - }, - { - "name": "cache_path", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - }, - { - "name": "output_paths", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "BinOp(left=Name(id='PipelineOutputPaths', ctx=Load()), op=BitOr(), right=Name(id='OutputPaths', ctx=Load()))" - }, - { - "name": "root", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Name(id='BootstrapResult', ctx=Load())", - "exported_via": "all" - }, - { - "local_name": "discover", - "kind": "function", - "start_line": 440, - "end_line": 441, - "params": [ - { - "name": "boot", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='BootstrapResult', ctx=Load())" - }, - { - "name": "cache", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='Cache', ctx=Load())" - } - ], - "returns_hash": "Name(id='DiscoveryResult', ctx=Load())", - "exported_via": "all" - }, - { - "local_name": "gate", - "kind": "function", - "start_line": 512, - "end_line": 529, - "params": [ - { - "name": "analysis", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='AnalysisResult', ctx=Load())" - }, - { - "name": "boot", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='BootstrapResult', ctx=Load())" - }, - { - "name": "metrics_diff", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "BinOp(left=Name(id='MetricsDiff', ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "new_block", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='set', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "new_func", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='set', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - } - ], - "returns_hash": "Name(id='GatingResult', ctx=Load())", - "exported_via": "all" - }, - { - "local_name": "main", - "kind": "function", - "start_line": 1634, - "end_line": 1647, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "all" - }, - { - "local_name": "process", - "kind": "function", - "start_line": 444, - "end_line": 463, - "params": [ - { - "name": "boot", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='BootstrapResult', ctx=Load())" - }, - { - "name": "cache", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='Cache', ctx=Load())" - }, - { - "name": "discovery", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='DiscoveryResult', ctx=Load())" - }, - { - "name": "on_advance", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[List(ctx=Load()), Constant(value=None)], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "on_parallel_fallback", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[List(elts=[Name(id='Exception', ctx=Load())], ctx=Load()), Constant(value=None)], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "on_worker_error", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[List(elts=[Name(id='str', ctx=Load())], ctx=Load()), Constant(value=None)], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" - } - ], - "returns_hash": "Name(id='PipelineProcessingResult', ctx=Load())", - "exported_via": "all" - }, - { - "local_name": "process_file", - "kind": "function", - "start_line": 402, - "end_line": 419, - "params": [ - { - "name": "cfg", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='NormalizationConfig', ctx=Load())" - }, - { - "name": "collect_structural_findings", - "kind": "pos_or_kw", - "has_default": true, - "annotation_hash": "Name(id='bool', ctx=Load())" - }, - { - "name": "filepath", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "min_loc", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "min_stmt", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "root", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - } - ], - "returns_hash": "Name(id='ProcessingResult', ctx=Load())", - "exported_via": "all" - }, - { - "local_name": "report", - "kind": "function", - "start_line": 482, - "end_line": 509, - "params": [ - { - "name": "analysis", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='AnalysisResult', ctx=Load())" - }, - { - "name": "boot", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='BootstrapResult', ctx=Load())" - }, - { - "name": "discovery", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='DiscoveryResult', ctx=Load())" - }, - { - "name": "html_builder", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[Constant(value=Ellipsis), Name(id='str', ctx=Load())], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "include_report_document", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "Name(id='bool', ctx=Load())" - }, - { - "name": "metrics_diff", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Name(id='MetricsDiff', ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "new_block", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='set', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "new_func", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='set', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "processing", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='PipelineProcessingResult', ctx=Load())" - }, - { - "name": "report_meta", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())" - } - ], - "returns_hash": "Name(id='ReportArtifacts', ctx=Load())", - "exported_via": "all" - } - ] - }, - { - "module": "codeclone.contracts", - "filepath": "codeclone/contracts.py", - "all_declared": [], - "symbols": [ - { - "local_name": "BASELINE_FINGERPRINT_VERSION", - "kind": "constant", - "start_line": 13, - "end_line": 13, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "BASELINE_SCHEMA_VERSION", - "kind": "constant", - "start_line": 12, - "end_line": 12, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "CACHE_VERSION", - "kind": "constant", - "start_line": 15, - "end_line": 15, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "COHESION_RISK_MEDIUM_MAX", - "kind": "constant", - "start_line": 31, - "end_line": 31, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "COMPLEXITY_RISK_LOW_MAX", - "kind": "constant", - "start_line": 27, - "end_line": 27, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "COMPLEXITY_RISK_MEDIUM_MAX", - "kind": "constant", - "start_line": 28, - "end_line": 28, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "COUPLING_RISK_LOW_MAX", - "kind": "constant", - "start_line": 29, - "end_line": 29, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "COUPLING_RISK_MEDIUM_MAX", - "kind": "constant", - "start_line": 30, - "end_line": 30, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "DEFAULT_COHESION_THRESHOLD", - "kind": "constant", - "start_line": 21, - "end_line": 21, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "DEFAULT_COMPLEXITY_THRESHOLD", - "kind": "constant", - "start_line": 19, - "end_line": 19, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "DEFAULT_COUPLING_THRESHOLD", - "kind": "constant", - "start_line": 20, - "end_line": 20, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "DEFAULT_HEALTH_THRESHOLD", - "kind": "constant", - "start_line": 25, - "end_line": 25, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "DEFAULT_REPORT_DESIGN_COHESION_THRESHOLD", - "kind": "constant", - "start_line": 24, - "end_line": 24, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "DEFAULT_REPORT_DESIGN_COMPLEXITY_THRESHOLD", - "kind": "constant", - "start_line": 22, - "end_line": 22, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "DEFAULT_REPORT_DESIGN_COUPLING_THRESHOLD", - "kind": "constant", - "start_line": 23, - "end_line": 23, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "DOCS_URL", - "kind": "constant", - "start_line": 53, - "end_line": 53, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ExitCode", - "kind": "class", - "start_line": 44, - "end_line": 48, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HEALTH_WEIGHTS", - "kind": "constant", - "start_line": 33, - "end_line": 41, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ISSUES_URL", - "kind": "constant", - "start_line": 52, - "end_line": 52, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "METRICS_BASELINE_SCHEMA_VERSION", - "kind": "constant", - "start_line": 17, - "end_line": 17, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "REPORT_SCHEMA_VERSION", - "kind": "constant", - "start_line": 16, - "end_line": 16, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "REPOSITORY_URL", - "kind": "constant", - "start_line": 51, - "end_line": 51, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "cli_help_epilog", - "kind": "function", - "start_line": 56, - "end_line": 72, - "params": [], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - } - ] - }, - { - "module": "codeclone.metrics.adoption", - "filepath": "codeclone/metrics/adoption.py", - "all_declared": [ - "collect_module_adoption" - ], - "symbols": [ - { - "local_name": "collect_module_adoption", - "kind": "function", - "start_line": 25, - "end_line": 105, - "params": [ - { - "name": "collector", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='QualnameCollector', ctx=Load())" - }, - { - "name": "filepath", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "imported_names", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='frozenset', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "module_name", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "tree", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='ast', ctx=Load()), attr='Module', ctx=Load())" - } - ], - "returns_hash": "Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='ModuleTypingCoverage', ctx=Load()), Name(id='ModuleDocstringCoverage', ctx=Load())], ctx=Load()), ctx=Load())", - "exported_via": "all" - } - ] - }, - { - "module": "codeclone.metrics.api_surface", - "filepath": "codeclone/metrics/api_surface.py", - "all_declared": [ - "collect_module_api_surface", - "compare_api_surfaces" - ], - "symbols": [ - { - "local_name": "collect_module_api_surface", - "kind": "function", - "start_line": 34, - "end_line": 102, - "params": [ - { - "name": "collector", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='QualnameCollector', ctx=Load())" - }, - { - "name": "filepath", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "imported_names", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='frozenset', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "include_private_modules", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "Name(id='bool', ctx=Load())" - }, - { - "name": "module_name", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "tree", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='ast', ctx=Load()), attr='Module', ctx=Load())" - } - ], - "returns_hash": "BinOp(left=Name(id='ModuleApiSurface', ctx=Load()), op=BitOr(), right=Constant(value=None))", - "exported_via": "all" - }, - { - "local_name": "compare_api_surfaces", - "kind": "function", - "start_line": 209, - "end_line": 266, - "params": [ - { - "name": "baseline", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "BinOp(left=Name(id='ApiSurfaceSnapshot', ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "current", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "BinOp(left=Name(id='ApiSurfaceSnapshot', ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "strict_types", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='bool', ctx=Load())" - } - ], - "returns_hash": "Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Constant(value=Ellipsis)], ctx=Load()), ctx=Load()), Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='ApiBreakingChange', ctx=Load()), Constant(value=Ellipsis)], ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())", - "exported_via": "all" - } - ] - }, - { - "module": "codeclone.metrics_baseline", - "filepath": "codeclone/metrics_baseline.py", - "all_declared": [ - "BASELINE_SCHEMA_VERSION", - "MAX_METRICS_BASELINE_SIZE_BYTES", - "METRICS_BASELINE_GENERATOR", - "METRICS_BASELINE_SCHEMA_VERSION", - "METRICS_BASELINE_UNTRUSTED_STATUSES", - "MetricsBaseline", - "MetricsBaselineStatus", - "coerce_metrics_baseline_status", - "current_python_tag", - "snapshot_from_project_metrics" - ], - "symbols": [ - { - "local_name": "MAX_METRICS_BASELINE_SIZE_BYTES", - "kind": "constant", - "start_line": 42, - "end_line": 42, - "params": [], - "returns_hash": "", - "exported_via": "all" - }, - { - "local_name": "METRICS_BASELINE_GENERATOR", - "kind": "constant", - "start_line": 41, - "end_line": 41, - "params": [], - "returns_hash": "", - "exported_via": "all" - }, - { - "local_name": "METRICS_BASELINE_UNTRUSTED_STATUSES", - "kind": "constant", - "start_line": 59, - "end_line": 74, - "params": [], - "returns_hash": "", - "exported_via": "all" - }, - { - "local_name": "MetricsBaseline", - "kind": "class", - "start_line": 219, - "end_line": 637, - "params": [], - "returns_hash": "", - "exported_via": "all" - }, - { - "local_name": "MetricsBaseline.__init__", - "kind": "method", - "start_line": 235, - "end_line": 247, - "params": [ - { - "name": "path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Name(id='Path', ctx=Load()))" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "all" - }, - { - "local_name": "MetricsBaseline.diff", - "kind": "method", - "start_line": 566, - "end_line": 637, - "params": [ - { - "name": "current", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='ProjectMetrics', ctx=Load())" - } - ], - "returns_hash": "Name(id='MetricsDiff', ctx=Load())", - "exported_via": "all" - }, - { - "local_name": "MetricsBaseline.from_project_metrics", - "kind": "method", - "start_line": 538, - "end_line": 564, - "params": [ - { - "name": "generator_version", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "path", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Name(id='Path', ctx=Load()))" - }, - { - "name": "project_metrics", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='ProjectMetrics', ctx=Load())" - }, - { - "name": "python_tag", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "schema_version", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Constant(value=None))" - } - ], - "returns_hash": "Name(id='MetricsBaseline', ctx=Load())", - "exported_via": "all" - }, - { - "local_name": "MetricsBaseline.load", - "kind": "method", - "start_line": 249, - "end_line": 344, - "params": [ - { - "name": "max_size_bytes", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Name(id='int', ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "preloaded_payload", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "all" - }, - { - "local_name": "MetricsBaseline.save", - "kind": "method", - "start_line": 346, - "end_line": 437, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "all" - }, - { - "local_name": "MetricsBaseline.verify_compatibility", - "kind": "method", - "start_line": 439, - "end_line": 466, - "params": [ - { - "name": "runtime_python_tag", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "all" - }, - { - "local_name": "MetricsBaseline.verify_integrity", - "kind": "method", - "start_line": 468, - "end_line": 535, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "all" - }, - { - "local_name": "MetricsBaselineStatus", - "kind": "class", - "start_line": 45, - "end_line": 56, - "params": [], - "returns_hash": "", - "exported_via": "all" - }, - { - "local_name": "coerce_metrics_baseline_status", - "kind": "function", - "start_line": 110, - "end_line": 120, - "params": [ - { - "name": "raw_status", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "BinOp(left=BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Name(id='MetricsBaselineStatus', ctx=Load())), op=BitOr(), right=Constant(value=None))" - } - ], - "returns_hash": "Name(id='MetricsBaselineStatus', ctx=Load())", - "exported_via": "all" - }, - { - "local_name": "snapshot_from_project_metrics", - "kind": "function", - "start_line": 123, - "end_line": 153, - "params": [ - { - "name": "project_metrics", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='ProjectMetrics', ctx=Load())" - } - ], - "returns_hash": "Name(id='MetricsSnapshot', ctx=Load())", - "exported_via": "all" - } - ] - }, - { - "module": "codeclone.pipeline", - "filepath": "codeclone/pipeline.py", - "all_declared": [], - "symbols": [ - { - "local_name": "AnalysisResult", - "kind": "class", - "start_line": 190, - "end_line": 206, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "BootstrapResult", - "kind": "class", - "start_line": 110, - "end_line": 115, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "DEFAULT_BATCH_SIZE", - "kind": "constant", - "start_line": 91, - "end_line": 91, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "DEFAULT_RUNTIME_PROCESSES", - "kind": "constant", - "start_line": 94, - "end_line": 94, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "DiscoveryResult", - "kind": "class", - "start_line": 119, - "end_line": 143, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "FileProcessResult", - "kind": "class", - "start_line": 147, - "end_line": 161, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "GatingResult", - "kind": "class", - "start_line": 210, - "end_line": 212, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "MAX_FILE_SIZE", - "kind": "constant", - "start_line": 90, - "end_line": 90, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "MetricGateConfig", - "kind": "class", - "start_line": 226, - "end_line": 238, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "OutputPaths", - "kind": "class", - "start_line": 101, - "end_line": 106, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "PARALLEL_MIN_FILES_FLOOR", - "kind": "constant", - "start_line": 93, - "end_line": 93, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "PARALLEL_MIN_FILES_PER_WORKER", - "kind": "constant", - "start_line": 92, - "end_line": 92, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ProcessingResult", - "kind": "class", - "start_line": 165, - "end_line": 186, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ReportArtifacts", - "kind": "class", - "start_line": 216, - "end_line": 222, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "analyze", - "kind": "function", - "start_line": 1957, - "end_line": 2078, - "params": [ - { - "name": "boot", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='BootstrapResult', ctx=Load())" - }, - { - "name": "discovery", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='DiscoveryResult', ctx=Load())" - }, - { - "name": "processing", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='ProcessingResult', ctx=Load())" - } - ], - "returns_hash": "Name(id='AnalysisResult', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "bootstrap", - "kind": "function", - "start_line": 443, - "end_line": 456, - "params": [ - { - "name": "args", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='Namespace', ctx=Load())" - }, - { - "name": "cache_path", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - }, - { - "name": "output_paths", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='OutputPaths', ctx=Load())" - }, - { - "name": "root", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Name(id='BootstrapResult', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "build_metrics_report_payload", - "kind": "function", - "start_line": 1757, - "end_line": 1954, - "params": [ - { - "name": "class_metrics", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='ClassMetrics', ctx=Load()), ctx=Load())" - }, - { - "name": "module_deps", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='ModuleDep', ctx=Load()), ctx=Load())" - }, - { - "name": "project_metrics", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='ProjectMetrics', ctx=Load())" - }, - { - "name": "scan_root", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "source_stats_by_file", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='int', ctx=Load()), Name(id='int', ctx=Load()), Name(id='int', ctx=Load()), Name(id='int', ctx=Load())], ctx=Load()), ctx=Load()), ctx=Load())" - }, - { - "name": "suppressed_dead_code", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='DeadItem', ctx=Load()), ctx=Load())" - }, - { - "name": "units", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='GroupItemLike', ctx=Load()), ctx=Load())" - } - ], - "returns_hash": "Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "compute_project_metrics", - "kind": "function", - "start_line": 1342, - "end_line": 1500, - "params": [ - { - "name": "api_modules", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='ModuleApiSurface', ctx=Load()), ctx=Load())" - }, - { - "name": "block_clone_groups", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "class_metrics", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='ClassMetrics', ctx=Load()), ctx=Load())" - }, - { - "name": "dead_candidates", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='DeadCandidate', ctx=Load()), ctx=Load())" - }, - { - "name": "docstring_modules", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='ModuleDocstringCoverage', ctx=Load()), ctx=Load())" - }, - { - "name": "files_analyzed_or_cached", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "files_found", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "function_clone_groups", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "module_deps", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='ModuleDep', ctx=Load()), ctx=Load())" - }, - { - "name": "referenced_names", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='frozenset', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "referenced_qualnames", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='frozenset', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "skip_dead_code", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='bool', ctx=Load())" - }, - { - "name": "skip_dependencies", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='bool', ctx=Load())" - }, - { - "name": "typing_modules", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='ModuleTypingCoverage', ctx=Load()), ctx=Load())" - }, - { - "name": "units", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='GroupItemLike', ctx=Load()), ctx=Load())" - } - ], - "returns_hash": "Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='ProjectMetrics', ctx=Load()), Name(id='DepGraph', ctx=Load()), Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='DeadItem', ctx=Load()), Constant(value=Ellipsis)], ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "compute_suggestions", - "kind": "function", - "start_line": 1503, - "end_line": 1525, - "params": [ - { - "name": "block_group_facts", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='str', ctx=Load())], ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "block_groups", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='GroupItemLike', ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" - }, - { - "name": "class_metrics", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='ClassMetrics', ctx=Load()), ctx=Load())" - }, - { - "name": "func_groups", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='GroupItemLike', ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" - }, - { - "name": "project_metrics", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='ProjectMetrics', ctx=Load())" - }, - { - "name": "scan_root", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "segment_groups", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='GroupItemLike', ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" - }, - { - "name": "structural_findings", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='StructuralFindingGroup', ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "units", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Sequence', ctx=Load()), slice=Name(id='GroupItemLike', ctx=Load()), ctx=Load())" - } - ], - "returns_hash": "Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='Suggestion', ctx=Load()), Constant(value=Ellipsis)], ctx=Load()), ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "discover", - "kind": "function", - "start_line": 785, - "end_line": 926, - "params": [ - { - "name": "boot", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='BootstrapResult', ctx=Load())" - }, - { - "name": "cache", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='Cache', ctx=Load())" - } - ], - "returns_hash": "Name(id='DiscoveryResult', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "gate", - "kind": "function", - "start_line": 2448, - "end_line": 2500, - "params": [ - { - "name": "analysis", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='AnalysisResult', ctx=Load())" - }, - { - "name": "boot", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='BootstrapResult', ctx=Load())" - }, - { - "name": "metrics_diff", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "BinOp(left=Name(id='MetricsDiff', ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "new_block", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Collection', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "new_func", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Collection', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - } - ], - "returns_hash": "Name(id='GatingResult', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "metric_gate_reasons", - "kind": "function", - "start_line": 2251, - "end_line": 2274, - "params": [ - { - "name": "config", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='MetricGateConfig', ctx=Load())" - }, - { - "name": "metrics_diff", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "BinOp(left=Name(id='MetricsDiff', ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "project_metrics", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='ProjectMetrics', ctx=Load())" - } - ], - "returns_hash": "Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Constant(value=Ellipsis)], ctx=Load()), ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "process", - "kind": "function", - "start_line": 1032, - "end_line": 1329, - "params": [ - { - "name": "batch_size", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "boot", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='BootstrapResult', ctx=Load())" - }, - { - "name": "cache", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='Cache', ctx=Load())" - }, - { - "name": "discovery", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='DiscoveryResult', ctx=Load())" - }, - { - "name": "on_advance", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[List(ctx=Load()), Constant(value=None)], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "on_parallel_fallback", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[List(elts=[Name(id='Exception', ctx=Load())], ctx=Load()), Constant(value=None)], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "on_worker_error", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[List(elts=[Name(id='str', ctx=Load())], ctx=Load()), Constant(value=None)], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" - } - ], - "returns_hash": "Name(id='ProcessingResult', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "process_file", - "kind": "function", - "start_line": 929, - "end_line": 1029, - "params": [ - { - "name": "api_include_private_modules", - "kind": "pos_or_kw", - "has_default": true, - "annotation_hash": "Name(id='bool', ctx=Load())" - }, - { - "name": "block_min_loc", - "kind": "pos_or_kw", - "has_default": true, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "block_min_stmt", - "kind": "pos_or_kw", - "has_default": true, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "cfg", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='NormalizationConfig', ctx=Load())" - }, - { - "name": "collect_api_surface", - "kind": "pos_or_kw", - "has_default": true, - "annotation_hash": "Name(id='bool', ctx=Load())" - }, - { - "name": "collect_docstring_coverage", - "kind": "pos_or_kw", - "has_default": true, - "annotation_hash": "Name(id='bool', ctx=Load())" - }, - { - "name": "collect_structural_findings", - "kind": "pos_or_kw", - "has_default": true, - "annotation_hash": "Name(id='bool', ctx=Load())" - }, - { - "name": "collect_typing_coverage", - "kind": "pos_or_kw", - "has_default": true, - "annotation_hash": "Name(id='bool', ctx=Load())" - }, - { - "name": "filepath", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "min_loc", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "min_stmt", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "root", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "segment_min_loc", - "kind": "pos_or_kw", - "has_default": true, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "segment_min_stmt", - "kind": "pos_or_kw", - "has_default": true, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "Name(id='FileProcessResult', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "report", - "kind": "function", - "start_line": 2093, - "end_line": 2248, - "params": [ - { - "name": "analysis", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='AnalysisResult', ctx=Load())" - }, - { - "name": "boot", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='BootstrapResult', ctx=Load())" - }, - { - "name": "discovery", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='DiscoveryResult', ctx=Load())" - }, - { - "name": "html_builder", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[Constant(value=Ellipsis), Name(id='str', ctx=Load())], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "include_report_document", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "Name(id='bool', ctx=Load())" - }, - { - "name": "metrics_diff", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Name(id='object', ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "new_block", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Collection', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "new_func", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Collection', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "processing", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='ProcessingResult', ctx=Load())" - }, - { - "name": "report_meta", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Mapping', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())" - } - ], - "returns_hash": "Name(id='ReportArtifacts', ctx=Load())", - "exported_via": "name" - } - ] - }, - { - "module": "codeclone.ui_messages", - "filepath": "codeclone/ui_messages.py", - "all_declared": [], - "symbols": [ - { - "local_name": "ACTION_UPDATE_BASELINE", - "kind": "constant", - "start_line": 281, - "end_line": 281, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "BANNER_SUBTITLE", - "kind": "constant", - "start_line": 25, - "end_line": 25, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "CHANGED_SCOPE_TITLE", - "kind": "constant", - "start_line": 183, - "end_line": 183, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "CLI_LAYOUT_MAX_WIDTH", - "kind": "constant", - "start_line": 185, - "end_line": 185, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ERR_BASELINE_GATING_REQUIRES_TRUSTED", - "kind": "constant", - "start_line": 293, - "end_line": 295, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ERR_BASELINE_WRITE_FAILED", - "kind": "constant", - "start_line": 251, - "end_line": 253, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ERR_INVALID_BASELINE", - "kind": "constant", - "start_line": 276, - "end_line": 280, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ERR_INVALID_BASELINE_PATH", - "kind": "constant", - "start_line": 250, - "end_line": 250, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ERR_INVALID_OUTPUT_EXT", - "kind": "constant", - "start_line": 240, - "end_line": 243, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ERR_INVALID_OUTPUT_PATH", - "kind": "constant", - "start_line": 244, - "end_line": 246, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ERR_INVALID_ROOT_PATH", - "kind": "constant", - "start_line": 248, - "end_line": 248, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ERR_OPEN_HTML_REPORT_REQUIRES_HTML", - "kind": "constant", - "start_line": 257, - "end_line": 259, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ERR_REPORT_WRITE_FAILED", - "kind": "constant", - "start_line": 254, - "end_line": 256, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ERR_ROOT_NOT_FOUND", - "kind": "constant", - "start_line": 247, - "end_line": 247, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ERR_SCAN_FAILED", - "kind": "constant", - "start_line": 249, - "end_line": 249, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ERR_TIMESTAMPED_REPORT_PATHS_REQUIRES_REPORT", - "kind": "constant", - "start_line": 260, - "end_line": 263, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "ERR_UNREADABLE_SOURCE_IN_GATING", - "kind": "constant", - "start_line": 264, - "end_line": 267, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "FAIL_METRICS_TITLE", - "kind": "constant", - "start_line": 307, - "end_line": 307, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "FAIL_NEW_ACCEPT_COMMAND", - "kind": "constant", - "start_line": 304, - "end_line": 304, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "FAIL_NEW_ACCEPT_TITLE", - "kind": "constant", - "start_line": 303, - "end_line": 303, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "FAIL_NEW_BLOCK", - "kind": "constant", - "start_line": 301, - "end_line": 301, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "FAIL_NEW_DETAIL_BLOCK", - "kind": "constant", - "start_line": 306, - "end_line": 306, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "FAIL_NEW_DETAIL_FUNCTION", - "kind": "constant", - "start_line": 305, - "end_line": 305, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "FAIL_NEW_FUNCTION", - "kind": "constant", - "start_line": 300, - "end_line": 300, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "FAIL_NEW_REPORT_TITLE", - "kind": "constant", - "start_line": 302, - "end_line": 302, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "FAIL_NEW_SUMMARY_TITLE", - "kind": "constant", - "start_line": 299, - "end_line": 299, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "FAIL_NEW_TITLE", - "kind": "constant", - "start_line": 298, - "end_line": 298, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_API_SURFACE", - "kind": "constant", - "start_line": 103, - "end_line": 106, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_BASELINE", - "kind": "constant", - "start_line": 58, - "end_line": 61, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_CACHE_DIR_LEGACY", - "kind": "constant", - "start_line": 53, - "end_line": 55, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_CACHE_PATH", - "kind": "constant", - "start_line": 49, - "end_line": 52, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_CHANGED_ONLY", - "kind": "constant", - "start_line": 37, - "end_line": 40, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_CI", - "kind": "constant", - "start_line": 127, - "end_line": 132, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_COLOR", - "kind": "constant", - "start_line": 173, - "end_line": 173, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_DEBUG", - "kind": "constant", - "start_line": 176, - "end_line": 179, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_DIFF_AGAINST", - "kind": "constant", - "start_line": 41, - "end_line": 44, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_DOCSTRING_COVERAGE", - "kind": "constant", - "start_line": 99, - "end_line": 102, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_FAIL_COHESION", - "kind": "constant", - "start_line": 82, - "end_line": 85, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_FAIL_COMPLEXITY", - "kind": "constant", - "start_line": 73, - "end_line": 77, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_FAIL_COUPLING", - "kind": "constant", - "start_line": 78, - "end_line": 81, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_FAIL_CYCLES", - "kind": "constant", - "start_line": 86, - "end_line": 86, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_FAIL_DEAD_CODE", - "kind": "constant", - "start_line": 87, - "end_line": 87, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_FAIL_HEALTH", - "kind": "constant", - "start_line": 88, - "end_line": 91, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_FAIL_ON_API_BREAK", - "kind": "constant", - "start_line": 115, - "end_line": 118, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_FAIL_ON_DOCSTRING_REGRESSION", - "kind": "constant", - "start_line": 111, - "end_line": 114, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_FAIL_ON_NEW", - "kind": "constant", - "start_line": 65, - "end_line": 67, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_FAIL_ON_NEW_METRICS", - "kind": "constant", - "start_line": 92, - "end_line": 95, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_FAIL_ON_TYPING_REGRESSION", - "kind": "constant", - "start_line": 107, - "end_line": 110, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_FAIL_THRESHOLD", - "kind": "constant", - "start_line": 68, - "end_line": 72, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_HTML", - "kind": "constant", - "start_line": 143, - "end_line": 146, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_JSON", - "kind": "constant", - "start_line": 147, - "end_line": 150, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_MAX_BASELINE_SIZE_MB", - "kind": "constant", - "start_line": 56, - "end_line": 56, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_MAX_CACHE_SIZE_MB", - "kind": "constant", - "start_line": 57, - "end_line": 57, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_MD", - "kind": "constant", - "start_line": 151, - "end_line": 154, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_METRICS_BASELINE", - "kind": "constant", - "start_line": 136, - "end_line": 139, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_MIN_DOCSTRING_COVERAGE", - "kind": "constant", - "start_line": 123, - "end_line": 126, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_MIN_LOC", - "kind": "constant", - "start_line": 34, - "end_line": 34, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_MIN_STMT", - "kind": "constant", - "start_line": 35, - "end_line": 35, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_MIN_TYPING_COVERAGE", - "kind": "constant", - "start_line": 119, - "end_line": 122, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_NO_COLOR", - "kind": "constant", - "start_line": 172, - "end_line": 172, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_NO_PROGRESS", - "kind": "constant", - "start_line": 170, - "end_line": 170, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_OPEN_HTML_REPORT", - "kind": "constant", - "start_line": 163, - "end_line": 165, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_PATHS_FROM_GIT_DIFF", - "kind": "constant", - "start_line": 45, - "end_line": 48, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_PROCESSES", - "kind": "constant", - "start_line": 36, - "end_line": 36, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_PROGRESS", - "kind": "constant", - "start_line": 171, - "end_line": 171, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_QUIET", - "kind": "constant", - "start_line": 174, - "end_line": 174, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_ROOT", - "kind": "constant", - "start_line": 33, - "end_line": 33, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_SARIF", - "kind": "constant", - "start_line": 155, - "end_line": 158, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_SKIP_DEAD_CODE", - "kind": "constant", - "start_line": 141, - "end_line": 141, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_SKIP_DEPENDENCIES", - "kind": "constant", - "start_line": 142, - "end_line": 142, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_SKIP_METRICS", - "kind": "constant", - "start_line": 140, - "end_line": 140, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_TEXT", - "kind": "constant", - "start_line": 159, - "end_line": 162, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_TIMESTAMPED_REPORT_PATHS", - "kind": "constant", - "start_line": 166, - "end_line": 169, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_TYPING_COVERAGE", - "kind": "constant", - "start_line": 96, - "end_line": 98, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_UPDATE_BASELINE", - "kind": "constant", - "start_line": 62, - "end_line": 64, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_UPDATE_METRICS_BASELINE", - "kind": "constant", - "start_line": 133, - "end_line": 135, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_VERBOSE", - "kind": "constant", - "start_line": 175, - "end_line": 175, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "HELP_VERSION", - "kind": "constant", - "start_line": 32, - "end_line": 32, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "INFO_PROCESSING_CHANGED", - "kind": "constant", - "start_line": 226, - "end_line": 226, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "MARKER_CONTRACT_ERROR", - "kind": "constant", - "start_line": 27, - "end_line": 27, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "MARKER_INTERNAL_ERROR", - "kind": "constant", - "start_line": 28, - "end_line": 28, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "METRICS_TITLE", - "kind": "constant", - "start_line": 182, - "end_line": 182, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "REPORT_BLOCK_GROUP_DISPLAY_NAME_ASSERT_PATTERN", - "kind": "constant", - "start_line": 30, - "end_line": 30, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "STATUS_DISCOVERING", - "kind": "constant", - "start_line": 223, - "end_line": 223, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "STATUS_GROUPING", - "kind": "constant", - "start_line": 224, - "end_line": 224, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SUCCESS_BASELINE_UPDATED", - "kind": "constant", - "start_line": 296, - "end_line": 296, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SUMMARY_COMPACT", - "kind": "constant", - "start_line": 201, - "end_line": 204, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SUMMARY_COMPACT_CHANGED_SCOPE", - "kind": "constant", - "start_line": 214, - "end_line": 216, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SUMMARY_COMPACT_CLONES", - "kind": "constant", - "start_line": 205, - "end_line": 208, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SUMMARY_COMPACT_METRICS", - "kind": "constant", - "start_line": 209, - "end_line": 213, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SUMMARY_LABEL_BLOCK", - "kind": "constant", - "start_line": 196, - "end_line": 196, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SUMMARY_LABEL_CACHE_HITS", - "kind": "constant", - "start_line": 189, - "end_line": 189, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SUMMARY_LABEL_CLASSES_ANALYZED", - "kind": "constant", - "start_line": 194, - "end_line": 194, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SUMMARY_LABEL_FILES_ANALYZED", - "kind": "constant", - "start_line": 188, - "end_line": 188, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SUMMARY_LABEL_FILES_FOUND", - "kind": "constant", - "start_line": 187, - "end_line": 187, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SUMMARY_LABEL_FILES_SKIPPED", - "kind": "constant", - "start_line": 190, - "end_line": 190, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SUMMARY_LABEL_FUNCTION", - "kind": "constant", - "start_line": 195, - "end_line": 195, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SUMMARY_LABEL_FUNCTIONS_ANALYZED", - "kind": "constant", - "start_line": 192, - "end_line": 192, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SUMMARY_LABEL_LINES_ANALYZED", - "kind": "constant", - "start_line": 191, - "end_line": 191, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SUMMARY_LABEL_METHODS_ANALYZED", - "kind": "constant", - "start_line": 193, - "end_line": 193, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SUMMARY_LABEL_NEW_BASELINE", - "kind": "constant", - "start_line": 199, - "end_line": 199, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SUMMARY_LABEL_SEGMENT", - "kind": "constant", - "start_line": 197, - "end_line": 197, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SUMMARY_LABEL_SUPPRESSED", - "kind": "constant", - "start_line": 198, - "end_line": 198, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "SUMMARY_TITLE", - "kind": "constant", - "start_line": 181, - "end_line": 181, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "WARN_BASELINE_IGNORED", - "kind": "constant", - "start_line": 288, - "end_line": 292, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "WARN_BASELINE_MISSING", - "kind": "constant", - "start_line": 282, - "end_line": 287, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "WARN_BATCH_ITEM_FAILED", - "kind": "constant", - "start_line": 229, - "end_line": 229, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "WARN_CACHE_SAVE_FAILED", - "kind": "constant", - "start_line": 235, - "end_line": 235, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "WARN_FAILED_FILES_HEADER", - "kind": "constant", - "start_line": 234, - "end_line": 234, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "WARN_HTML_REPORT_OPEN_FAILED", - "kind": "constant", - "start_line": 236, - "end_line": 238, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "WARN_LEGACY_CACHE", - "kind": "constant", - "start_line": 269, - "end_line": 274, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "WARN_NEW_CLONES_WITHOUT_FAIL", - "kind": "constant", - "start_line": 309, - "end_line": 312, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "WARN_PARALLEL_FALLBACK", - "kind": "constant", - "start_line": 230, - "end_line": 233, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "WARN_SUMMARY_ACCOUNTING_MISMATCH", - "kind": "constant", - "start_line": 218, - "end_line": 221, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "WARN_WORKER_FAILED", - "kind": "constant", - "start_line": 228, - "end_line": 228, - "params": [], - "returns_hash": "", - "exported_via": "name" - }, - { - "local_name": "banner_title", - "kind": "function", - "start_line": 319, - "end_line": 323, - "params": [ - { - "name": "version", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_baseline_write_failed", - "kind": "function", - "start_line": 342, - "end_line": 343, - "params": [ - { - "name": "error", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='object', ctx=Load())" - }, - { - "name": "path", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_batch_item_failed", - "kind": "function", - "start_line": 366, - "end_line": 367, - "params": [ - { - "name": "error", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='object', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_cache_save_failed", - "kind": "function", - "start_line": 378, - "end_line": 379, - "params": [ - { - "name": "error", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='object', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_changed_scope_compact", - "kind": "function", - "start_line": 645, - "end_line": 657, - "params": [ - { - "name": "findings", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "known", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "new", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "paths", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_changed_scope_findings", - "kind": "function", - "start_line": 635, - "end_line": 642, - "params": [ - { - "name": "known", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "new", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "total", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_changed_scope_paths", - "kind": "function", - "start_line": 631, - "end_line": 632, - "params": [ - { - "name": "count", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_contract_error", - "kind": "function", - "start_line": 664, - "end_line": 665, - "params": [ - { - "name": "message", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_failed_files_header", - "kind": "function", - "start_line": 374, - "end_line": 375, - "params": [ - { - "name": "count", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_html_report_open_failed", - "kind": "function", - "start_line": 350, - "end_line": 351, - "params": [ - { - "name": "error", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='object', ctx=Load())" - }, - { - "name": "path", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_internal_error", - "kind": "function", - "start_line": 668, - "end_line": 710, - "params": [ - { - "name": "debug", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "Name(id='bool', ctx=Load())" - }, - { - "name": "issues_url", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "error", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='BaseException', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_invalid_baseline", - "kind": "function", - "start_line": 386, - "end_line": 387, - "params": [ - { - "name": "error", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='object', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_invalid_baseline_path", - "kind": "function", - "start_line": 338, - "end_line": 339, - "params": [ - { - "name": "error", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='object', ctx=Load())" - }, - { - "name": "path", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_invalid_output_extension", - "kind": "function", - "start_line": 326, - "end_line": 331, - "params": [ - { - "name": "expected_suffix", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "label", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "path", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_invalid_output_path", - "kind": "function", - "start_line": 334, - "end_line": 335, - "params": [ - { - "name": "error", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='object', ctx=Load())" - }, - { - "name": "label", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "path", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_legacy_cache_warning", - "kind": "function", - "start_line": 382, - "end_line": 383, - "params": [ - { - "name": "legacy_path", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - }, - { - "name": "new_path", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_metrics_adoption", - "kind": "function", - "start_line": 574, - "end_line": 587, - "params": [ - { - "name": "any_annotation_count", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "docstring_permille", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "param_permille", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "return_permille", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_metrics_api_surface", - "kind": "function", - "start_line": 590, - "end_line": 610, - "params": [ - { - "name": "added", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "breaking", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "modules", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "public_symbols", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_metrics_cc", - "kind": "function", - "start_line": 529, - "end_line": 535, - "params": [ - { - "name": "avg", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='float', ctx=Load())" - }, - { - "name": "high_risk", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "max_val", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_metrics_cohesion", - "kind": "function", - "start_line": 542, - "end_line": 543, - "params": [ - { - "name": "avg", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='float', ctx=Load())" - }, - { - "name": "max_val", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_metrics_coupling", - "kind": "function", - "start_line": 538, - "end_line": 539, - "params": [ - { - "name": "avg", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='float', ctx=Load())" - }, - { - "name": "max_val", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_metrics_cycles", - "kind": "function", - "start_line": 546, - "end_line": 551, - "params": [ - { - "name": "count", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_metrics_dead_code", - "kind": "function", - "start_line": 554, - "end_line": 567, - "params": [ - { - "name": "suppressed", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "count", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_metrics_health", - "kind": "function", - "start_line": 524, - "end_line": 526, - "params": [ - { - "name": "grade", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "total", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_metrics_overloaded_modules", - "kind": "function", - "start_line": 613, - "end_line": 628, - "params": [ - { - "name": "candidates", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "population_status", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "top_score", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='float', ctx=Load())" - }, - { - "name": "total", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_parallel_fallback", - "kind": "function", - "start_line": 370, - "end_line": 371, - "params": [ - { - "name": "error", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='object', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_path", - "kind": "function", - "start_line": 390, - "end_line": 391, - "params": [ - { - "name": "path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - }, - { - "name": "template", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_pipeline_done", - "kind": "function", - "start_line": 660, - "end_line": 661, - "params": [ - { - "name": "elapsed", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='float', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_processing_changed", - "kind": "function", - "start_line": 358, - "end_line": 359, - "params": [ - { - "name": "count", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_report_write_failed", - "kind": "function", - "start_line": 346, - "end_line": 347, - "params": [ - { - "name": "error", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='object', ctx=Load())" - }, - { - "name": "label", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "path", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_summary_clones", - "kind": "function", - "start_line": 507, - "end_line": 521, - "params": [ - { - "name": "block", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "func", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "new", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "segment", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "suppressed", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_summary_compact", - "kind": "function", - "start_line": 394, - "end_line": 399, - "params": [ - { - "name": "analyzed", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "cache_hits", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "found", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "skipped", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_summary_compact_clones", - "kind": "function", - "start_line": 402, - "end_line": 416, - "params": [ - { - "name": "block", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "function", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "new", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "segment", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "suppressed", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_summary_compact_metrics", - "kind": "function", - "start_line": 419, - "end_line": 445, - "params": [ - { - "name": "cbo_avg", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='float', ctx=Load())" - }, - { - "name": "cbo_max", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "cc_avg", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='float', ctx=Load())" - }, - { - "name": "cc_max", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "cycles", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "dead", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "grade", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "health", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "lcom_avg", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='float', ctx=Load())" - }, - { - "name": "lcom_max", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "overloaded_modules", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_summary_files", - "kind": "function", - "start_line": 481, - "end_line": 489, - "params": [ - { - "name": "analyzed", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "cached", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "found", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "skipped", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_summary_parsed", - "kind": "function", - "start_line": 492, - "end_line": 504, - "params": [ - { - "name": "classes", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "functions", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "lines", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "methods", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Constant(value=None))", - "exported_via": "name" - }, - { - "local_name": "fmt_unreadable_source_in_gating", - "kind": "function", - "start_line": 354, - "end_line": 355, - "params": [ - { - "name": "count", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "fmt_worker_failed", - "kind": "function", - "start_line": 362, - "end_line": 363, - "params": [ - { - "name": "error", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='object', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "version_output", - "kind": "function", - "start_line": 315, - "end_line": 316, - "params": [ - { - "name": "version", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - } - ] - }, - { - "module": "tests.test_adoption", - "filepath": "tests/test_adoption.py", - "all_declared": [], - "symbols": [ - { - "local_name": "test_adoption_helper_rows_and_any_helpers_cover_method_and_variants", - "kind": "function", - "start_line": 172, - "end_line": 197, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_build_module_visibility_supports_strict_dunder_all_for_private_modules", - "kind": "function", - "start_line": 36, - "end_line": 65, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_collect_module_adoption_counts_annotations_docstrings_and_any", - "kind": "function", - "start_line": 68, - "end_line": 119, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_visibility_helpers_cover_private_modules_and_declared_all_edges", - "kind": "function", - "start_line": 122, - "end_line": 169, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - } - ] - }, - { - "module": "tests.test_api_surface", - "filepath": "tests/test_api_surface.py", - "all_declared": [], - "symbols": [ - { - "local_name": "test_api_surface_helpers_cover_constant_symbols_and_break_variants", - "kind": "function", - "start_line": 267, - "end_line": 405, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_collect_module_api_surface_skips_private_or_empty_modules", - "kind": "function", - "start_line": 229, - "end_line": 264, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_collect_module_api_surface_skips_self_and_collects_public_symbols", - "kind": "function", - "start_line": 45, - "end_line": 96, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_compare_api_surfaces_reports_added_removed_and_signature_breaks", - "kind": "function", - "start_line": 99, - "end_line": 209, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - } - ] - }, - { - "module": "tests.test_baseline", - "filepath": "tests/test_baseline.py", - "all_declared": [], - "symbols": [ - { - "local_name": "test_baseline_atomic_write_json_cleans_up_temp_file_on_replace_failure", - "kind": "function", - "start_line": 810, - "end_line": 826, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_diff", - "kind": "function", - "start_line": 67, - "end_line": 73, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_from_groups_defaults", - "kind": "function", - "start_line": 715, - "end_line": 725, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_hash_canonical_determinism", - "kind": "function", - "start_line": 578, - "end_line": 591, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_id_format_validation", - "kind": "function", - "start_line": 365, - "end_line": 378, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_id_lists_must_be_sorted_and_unique", - "kind": "function", - "start_line": 343, - "end_line": 362, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_integrity_fails_on_block_addition_without_rehash", - "kind": "function", - "start_line": 492, - "end_line": 503, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_integrity_fails_on_clone_removal", - "kind": "function", - "start_line": 480, - "end_line": 489, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_load_extra_top_level_key", - "kind": "function", - "start_line": 248, - "end_line": 259, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_load_json_read_error", - "kind": "function", - "start_line": 829, - "end_line": 845, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_load_legacy_codeclone_version_alias", - "kind": "function", - "start_line": 879, - "end_line": 892, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_load_legacy_payload", - "kind": "function", - "start_line": 217, - "end_line": 226, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_load_meta_and_clones_must_be_objects", - "kind": "function", - "start_line": 262, - "end_line": 270, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_load_missing", - "kind": "function", - "start_line": 155, - "end_line": 159, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_load_missing_required_clone_fields", - "kind": "function", - "start_line": 285, - "end_line": 294, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_load_missing_required_meta_fields", - "kind": "function", - "start_line": 273, - "end_line": 282, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_load_missing_top_level_key", - "kind": "function", - "start_line": 239, - "end_line": 245, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_load_rejects_invalid_json_shapes", - "kind": "function", - "start_line": 203, - "end_line": 214, - "params": [ - { - "name": "error_match", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "expected_status", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "raw_payload", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_load_rejects_metrics_section_for_schema_v1", - "kind": "function", - "start_line": 971, - "end_line": 982, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_load_rejects_non_canonical_block_lists", - "kind": "function", - "start_line": 513, - "end_line": 525, - "params": [ - { - "name": "blocks", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='list', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_load_rejects_non_object_preloaded_payload", - "kind": "function", - "start_line": 229, - "end_line": 236, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_load_stat_error", - "kind": "function", - "start_line": 174, - "end_line": 192, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_load_too_large", - "kind": "function", - "start_line": 162, - "end_line": 171, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_load_unexpected_clone_fields", - "kind": "function", - "start_line": 297, - "end_line": 308, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_load_whitespace_and_key_order_do_not_break_integrity", - "kind": "function", - "start_line": 661, - "end_line": 694, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_optional_str_paths", - "kind": "function", - "start_line": 848, - "end_line": 860, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_parse_semver_three_parts", - "kind": "function", - "start_line": 947, - "end_line": 953, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_payload_fields_contract_invariant", - "kind": "function", - "start_line": 539, - "end_line": 575, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_payload_sha256_independent_of_created_at_and_generator_version", - "kind": "function", - "start_line": 594, - "end_line": 609, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_payload_sha256_independent_of_schema_version", - "kind": "function", - "start_line": 612, - "end_line": 619, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_require_sorted_unique_ids_non_string", - "kind": "function", - "start_line": 956, - "end_line": 968, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_require_utc_iso8601_z_rejects_invalid_calendar_date", - "kind": "function", - "start_line": 863, - "end_line": 876, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_roundtrip_v1", - "kind": "function", - "start_line": 91, - "end_line": 118, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_safe_stat_size_oserror", - "kind": "function", - "start_line": 792, - "end_line": 807, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_save_atomic", - "kind": "function", - "start_line": 145, - "end_line": 152, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_save_defensive_non_mapping_meta", - "kind": "function", - "start_line": 1120, - "end_line": 1148, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_save_ignores_non_string_non_mapping_generator", - "kind": "function", - "start_line": 1246, - "end_line": 1269, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_save_preserves_embedded_api_surface_and_hash", - "kind": "function", - "start_line": 1006, - "end_line": 1026, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_save_preserves_embedded_metrics_and_hash", - "kind": "function", - "start_line": 985, - "end_line": 1003, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_save_preserves_embedded_metrics_without_hash", - "kind": "function", - "start_line": 1029, - "end_line": 1047, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_save_skips_non_string_meta_updates", - "kind": "function", - "start_line": 1184, - "end_line": 1222, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_save_sorts_clone_lists_deterministically", - "kind": "function", - "start_line": 697, - "end_line": 712, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_save_syncs_generator_when_meta_uses_string", - "kind": "function", - "start_line": 1151, - "end_line": 1181, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_save_updates_runtime_meta_fields", - "kind": "function", - "start_line": 121, - "end_line": 142, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_schema_version_mutation_preserves_integrity_and_hash_on_save", - "kind": "function", - "start_line": 622, - "end_line": 646, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_type_matrix", - "kind": "function", - "start_line": 324, - "end_line": 340, - "params": [ - { - "name": "container", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "error_match", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "field", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - }, - { - "name": "value", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='object', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_verify_accepts_previous_minor_in_current_major", - "kind": "function", - "start_line": 417, - "end_line": 425, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_verify_compatibility_ignores_generator_version", - "kind": "function", - "start_line": 528, - "end_line": 536, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_verify_compatibility_missing_fields", - "kind": "function", - "start_line": 736, - "end_line": 746, - "params": [ - { - "name": "attr", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "match_text", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_verify_fingerprint_mismatch", - "kind": "function", - "start_line": 428, - "end_line": 437, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_verify_generator_mismatch", - "kind": "function", - "start_line": 381, - "end_line": 393, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_verify_integrity_ignores_created_at_and_generator_version", - "kind": "function", - "start_line": 649, - "end_line": 658, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_verify_integrity_mismatch", - "kind": "function", - "start_line": 465, - "end_line": 477, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_verify_integrity_missing", - "kind": "function", - "start_line": 450, - "end_line": 462, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_verify_integrity_missing_context_fields", - "kind": "function", - "start_line": 779, - "end_line": 789, - "params": [ - { - "name": "attr", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "match_text", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_verify_integrity_payload_non_hex", - "kind": "function", - "start_line": 760, - "end_line": 768, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_verify_integrity_payload_not_string", - "kind": "function", - "start_line": 749, - "end_line": 757, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_verify_python_tag_mismatch", - "kind": "function", - "start_line": 440, - "end_line": 447, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_baseline_verify_schema_incompatibilities", - "kind": "function", - "start_line": 405, - "end_line": 414, - "params": [ - { - "name": "error_match", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "schema_version", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_coerce_baseline_status", - "kind": "function", - "start_line": 85, - "end_line": 88, - "params": [ - { - "name": "expected", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='BaselineStatus', ctx=Load())" - }, - { - "name": "raw_status", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "BinOp(left=BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Name(id='BaselineStatus', ctx=Load())), op=BitOr(), right=Constant(value=None))" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_parse_generator_meta_object_top_level_fallback", - "kind": "function", - "start_line": 922, - "end_line": 932, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_parse_generator_meta_rejects_extra_generator_keys", - "kind": "function", - "start_line": 935, - "end_line": 944, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_parse_generator_meta_string_legacy_alias", - "kind": "function", - "start_line": 895, - "end_line": 905, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_parse_generator_meta_string_prefers_generator_version", - "kind": "function", - "start_line": 908, - "end_line": 919, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_preserve_embedded_metrics_variants", - "kind": "function", - "start_line": 1050, - "end_line": 1117, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - } - ] - }, - { - "module": "tests.test_cache", - "filepath": "tests/test_cache.py", - "all_declared": [], - "symbols": [ - { - "local_name": "test_as_str_dict_rejects_non_string_keys", - "kind": "function", - "start_line": 1184, - "end_line": 1185, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_entry_block_item_not_dict", - "kind": "function", - "start_line": 623, - "end_line": 634, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_entry_container_shape_rejects_invalid_source_stats", - "kind": "function", - "start_line": 1689, - "end_line": 1706, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_entry_invalid_block_field_type", - "kind": "function", - "start_line": 637, - "end_line": 657, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_entry_invalid_segment_field_type", - "kind": "function", - "start_line": 674, - "end_line": 695, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_entry_invalid_stat_types", - "kind": "function", - "start_line": 542, - "end_line": 553, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_entry_invalid_unit_field_type", - "kind": "function", - "start_line": 598, - "end_line": 620, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_entry_invalid_units_container_type", - "kind": "function", - "start_line": 570, - "end_line": 581, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_entry_not_dict", - "kind": "function", - "start_line": 751, - "end_line": 755, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_entry_rejects_invalid_metrics_sections", - "kind": "function", - "start_line": 1250, - "end_line": 1267, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_entry_segment_item_not_dict", - "kind": "function", - "start_line": 660, - "end_line": 671, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_entry_stat_not_dict", - "kind": "function", - "start_line": 556, - "end_line": 567, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_entry_unit_item_not_dict", - "kind": "function", - "start_line": 584, - "end_line": 595, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_entry_valid_deep_schema", - "kind": "function", - "start_line": 698, - "end_line": 739, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_entry_validation", - "kind": "function", - "start_line": 535, - "end_line": 539, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_helper_type_guards_and_wire_api_decoders_cover_invalid_inputs", - "kind": "function", - "start_line": 265, - "end_line": 343, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_helpers_cover_invalid_analysis_profile_and_source_stats_shapes", - "kind": "function", - "start_line": 1361, - "end_line": 1382, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_legacy_secret_check_oserror_sets_warning", - "kind": "function", - "start_line": 898, - "end_line": 913, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_legacy_secret_warning_combined_with_other_warning", - "kind": "function", - "start_line": 882, - "end_line": 895, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_legacy_secret_warning_on_init", - "kind": "function", - "start_line": 852, - "end_line": 861, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_legacy_secret_warning_preserved_after_successful_load", - "kind": "function", - "start_line": 864, - "end_line": 879, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_load_analysis_profile_mismatch", - "kind": "function", - "start_line": 1008, - "end_line": 1021, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_load_corrupted_json", - "kind": "function", - "start_line": 766, - "end_line": 774, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_load_fingerprint_version_mismatch", - "kind": "function", - "start_line": 990, - "end_line": 1005, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_load_invalid_analysis_profile_payload", - "kind": "function", - "start_line": 1052, - "end_line": 1073, - "params": [ - { - "name": "bad_analysis_profile", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='object', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_load_invalid_files_type", - "kind": "function", - "start_line": 825, - "end_line": 836, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_load_invalid_top_level_type", - "kind": "function", - "start_line": 916, - "end_line": 923, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_load_invalid_wire_file_entry", - "kind": "function", - "start_line": 1076, - "end_line": 1086, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_load_missing_analysis_profile_in_payload", - "kind": "function", - "start_line": 1024, - "end_line": 1042, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_load_missing_file", - "kind": "function", - "start_line": 742, - "end_line": 748, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_load_missing_payload_or_sig", - "kind": "function", - "start_line": 937, - "end_line": 945, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_load_missing_v_field", - "kind": "function", - "start_line": 926, - "end_line": 934, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_load_normalizes_stale_structural_findings", - "kind": "function", - "start_line": 113, - "end_line": 188, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_load_python_tag_mismatch", - "kind": "function", - "start_line": 972, - "end_line": 987, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_load_rejects_missing_required_payload_fields", - "kind": "function", - "start_line": 956, - "end_line": 969, - "params": [ - { - "name": "payload_factory", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[List(elts=[Name(id='Cache', ctx=Load())], ctx=Load()), Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_load_unreadable_read_graceful_ignore", - "kind": "function", - "start_line": 795, - "end_line": 814, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_load_unreadable_stat_graceful_ignore", - "kind": "function", - "start_line": 777, - "end_line": 792, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_roundtrip", - "kind": "function", - "start_line": 71, - "end_line": 89, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_roundtrip_preserves_empty_structural_findings", - "kind": "function", - "start_line": 92, - "end_line": 110, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_save_error", - "kind": "function", - "start_line": 839, - "end_line": 849, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_save_skips_none_entry_from_lookup", - "kind": "function", - "start_line": 1089, - "end_line": 1112, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_signature_mismatch_warns", - "kind": "function", - "start_line": 463, - "end_line": 480, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_signature_validation_ignores_json_whitespace", - "kind": "function", - "start_line": 395, - "end_line": 407, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_too_large_warns", - "kind": "function", - "start_line": 521, - "end_line": 532, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_type_predicates_reject_non_dict_variants", - "kind": "function", - "start_line": 1709, - "end_line": 1817, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_v13_missing_optional_sections_default_empty", - "kind": "function", - "start_line": 377, - "end_line": 392, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_v13_uses_relpaths_when_root_set", - "kind": "function", - "start_line": 353, - "end_line": 374, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_v_field_version_mismatch_warns", - "kind": "function", - "start_line": 503, - "end_line": 518, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - }, - { - "name": "version", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cache_version_mismatch_warns", - "kind": "function", - "start_line": 483, - "end_line": 499, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_canonicalize_cache_entry_skips_invalid_dead_candidate_suppression_shape", - "kind": "function", - "start_line": 1385, - "end_line": 1425, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_decode_optional_wire_coupled_classes_rejects_non_string_qualname", - "kind": "function", - "start_line": 1428, - "end_line": 1435, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_decode_wire_block_rejects_missing_block_hash", - "kind": "function", - "start_line": 1824, - "end_line": 1831, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_decode_wire_dead_candidate_rejects_invalid_rows", - "kind": "function", - "start_line": 1844, - "end_line": 1845, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_decode_wire_file_and_name_section_helpers_cover_valid_and_invalid", - "kind": "function", - "start_line": 410, - "end_line": 460, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_decode_wire_file_entry_accepts_metrics_sections", - "kind": "function", - "start_line": 1318, - "end_line": 1338, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_decode_wire_file_entry_invalid_variants", - "kind": "function", - "start_line": 1203, - "end_line": 1204, - "params": [ - { - "name": "entry", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='object', ctx=Load())" - }, - { - "name": "filepath", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_decode_wire_file_entry_optional_source_stats", - "kind": "function", - "start_line": 1341, - "end_line": 1358, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_decode_wire_file_entry_rejects_metrics_related_invalid_sections", - "kind": "function", - "start_line": 1270, - "end_line": 1315, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_decode_wire_file_entry_skips_empty_coupled_classes_mapping", - "kind": "function", - "start_line": 1438, - "end_line": 1448, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_decode_wire_int_fields_rejects_non_int_values", - "kind": "function", - "start_line": 1820, - "end_line": 1821, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_decode_wire_item_rejects_invalid_risk_fields", - "kind": "function", - "start_line": 1219, - "end_line": 1233, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_decode_wire_item_type_failures", - "kind": "function", - "start_line": 1207, - "end_line": 1216, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_decode_wire_metrics_items_and_deps_roundtrip_shape", - "kind": "function", - "start_line": 1451, - "end_line": 1496, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_decode_wire_segment_rejects_missing_segment_signature", - "kind": "function", - "start_line": 1834, - "end_line": 1841, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_encode_wire_file_entry_compacts_dead_candidate_filepaths", - "kind": "function", - "start_line": 1537, - "end_line": 1560, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_encode_wire_file_entry_encodes_dead_candidate_suppressions", - "kind": "function", - "start_line": 1563, - "end_line": 1587, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_encode_wire_file_entry_includes_optional_metrics_sections", - "kind": "function", - "start_line": 1499, - "end_line": 1534, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_encode_wire_file_entry_skips_empty_or_invalid_coupled_classes", - "kind": "function", - "start_line": 1590, - "end_line": 1631, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_file_stat_signature", - "kind": "function", - "start_line": 758, - "end_line": 763, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_get_file_entry_keeps_loaded_cache_clean_on_canonical_hit", - "kind": "function", - "start_line": 210, - "end_line": 222, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_get_file_entry_missing_after_fallback_returns_none", - "kind": "function", - "start_line": 346, - "end_line": 350, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_get_file_entry_sorts_coupled_classes_in_runtime_payload", - "kind": "function", - "start_line": 1634, - "end_line": 1686, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_get_file_entry_uses_wire_key_fallback", - "kind": "function", - "start_line": 191, - "end_line": 207, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_resolve_root_oserror_returns_none", - "kind": "function", - "start_line": 1236, - "end_line": 1247, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_runtime_filepath_from_wire_resolve_oserror", - "kind": "function", - "start_line": 1166, - "end_line": 1181, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_store_canonical_file_entry_marks_dirty_only_when_entry_changes", - "kind": "function", - "start_line": 225, - "end_line": 262, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_wire_filepath_outside_root_falls_back_to_runtime_path", - "kind": "function", - "start_line": 1115, - "end_line": 1122, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_wire_filepath_resolve_oserror_falls_back_to_runtime_path", - "kind": "function", - "start_line": 1125, - "end_line": 1142, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_wire_filepath_resolve_relative_success_path", - "kind": "function", - "start_line": 1145, - "end_line": 1163, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - } - ] - }, - { - "module": "tests.test_cli_inprocess", - "filepath": "tests/test_cli_inprocess.py", - "all_declared": [], - "symbols": [ - { - "local_name": "test_cli_baseline_fingerprint_and_python_mismatch_status_prefers_fingerprint", - "kind": "function", - "start_line": 2581, - "end_line": 2601, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_baseline_fingerprint_mismatch_fails", - "kind": "function", - "start_line": 2474, - "end_line": 2498, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_baseline_missing_fails_in_ci", - "kind": "function", - "start_line": 2392, - "end_line": 2413, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_baseline_missing_fields_fails", - "kind": "function", - "start_line": 2501, - "end_line": 2531, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_baseline_missing_warning", - "kind": "function", - "start_line": 2371, - "end_line": 2389, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_baseline_python_version_mismatch_fails", - "kind": "function", - "start_line": 2604, - "end_line": 2624, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_baseline_python_version_mismatch_warns", - "kind": "function", - "start_line": 2452, - "end_line": 2471, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_baseline_schema_and_fingerprint_mismatch_status_prefers_schema", - "kind": "function", - "start_line": 2557, - "end_line": 2578, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_baseline_schema_version_mismatch_fails", - "kind": "function", - "start_line": 2534, - "end_line": 2554, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_batch_result_none_no_progress", - "kind": "function", - "start_line": 3648, - "end_line": 3659, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_batch_result_none_progress", - "kind": "function", - "start_line": 3662, - "end_line": 3674, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_blocks_processing", - "kind": "function", - "start_line": 2787, - "end_line": 2795, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_cache_analysis_profile_compatibility", - "kind": "function", - "start_line": 2011, - "end_line": 2081, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "expected_cache_schema_version", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "expected_cache_status", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "expected_cache_used", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='bool', ctx=Load())" - }, - { - "name": "expected_functions_total", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "expected_warning", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "first_min_loc", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "first_min_stmt", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "second_min_loc", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "second_min_stmt", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_cache_dir_override_respected", - "kind": "function", - "start_line": 723, - "end_line": 734, - "params": [ - { - "name": "flag", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_cache_not_shared_between_projects", - "kind": "function", - "start_line": 784, - "end_line": 799, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_cache_save_warning", - "kind": "function", - "start_line": 2825, - "end_line": 2846, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_cache_save_warning_quiet", - "kind": "function", - "start_line": 2849, - "end_line": 2876, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_cache_status_string_fallback", - "kind": "function", - "start_line": 952, - "end_line": 1003, - "params": [ - { - "name": "expected_status", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "load_warning", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "BinOp(left=Name(id='str', ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_cache_warning", - "kind": "function", - "start_line": 2798, - "end_line": 2822, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_ci_discovery_cache_hit", - "kind": "function", - "start_line": 3217, - "end_line": 3258, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_ci_preset_fails_on_new", - "kind": "function", - "start_line": 2746, - "end_line": 2784, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_contract_error_priority_over_gating_failure_for_unreadable_source", - "kind": "function", - "start_line": 3101, - "end_line": 3138, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_dead_code_suppression_is_stable_between_plain_and_json_runs", - "kind": "function", - "start_line": 3860, - "end_line": 3919, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "source", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "suppressed_count", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='int', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_default_cache_dir_per_root", - "kind": "function", - "start_line": 737, - "end_line": 781, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_default_cache_dir_uses_root", - "kind": "function", - "start_line": 709, - "end_line": 719, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_discovery_cache_hit", - "kind": "function", - "start_line": 2913, - "end_line": 2966, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_discovery_skip_oserror", - "kind": "function", - "start_line": 2970, - "end_line": 3004, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "extra_args", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='list', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_fail_on_new_default_report_path", - "kind": "function", - "start_line": 3611, - "end_line": 3645, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_fail_on_new_no_report_path", - "kind": "function", - "start_line": 3486, - "end_line": 3510, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_fail_on_new_prints_groups", - "kind": "function", - "start_line": 3459, - "end_line": 3483, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_fail_on_new_verbose_and_report_path", - "kind": "function", - "start_line": 3571, - "end_line": 3608, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_fail_on_new_verbose_single_kind", - "kind": "function", - "start_line": 3520, - "end_line": 3568, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "expect_block", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='bool', ctx=Load())" - }, - { - "name": "expect_func", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='bool', ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "new_block", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='set', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "new_func", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='set', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_failed_batch_item_no_progress", - "kind": "function", - "start_line": 3677, - "end_line": 3688, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_failed_batch_item_progress", - "kind": "function", - "start_line": 3691, - "end_line": 3703, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_failed_files_report", - "kind": "function", - "start_line": 3371, - "end_line": 3389, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_failed_files_report_single", - "kind": "function", - "start_line": 3392, - "end_line": 3410, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_invalid_baseline_fails_in_ci", - "kind": "function", - "start_line": 1819, - "end_line": 1835, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_invalid_baseline_path_error_contract", - "kind": "function", - "start_line": 2885, - "end_line": 2910, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_invalid_root", - "kind": "function", - "start_line": 2879, - "end_line": 2882, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_invalid_root_exception", - "kind": "function", - "start_line": 1069, - "end_line": 1076, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_legacy_baseline_fail_on_new_fails_fast_exit_2", - "kind": "function", - "start_line": 1549, - "end_line": 1574, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_legacy_baseline_normal_mode_ignored_and_exit_zero", - "kind": "function", - "start_line": 1512, - "end_line": 1546, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_legacy_cache_resolve_failure", - "kind": "function", - "start_line": 823, - "end_line": 855, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_main_fail_on_new", - "kind": "function", - "start_line": 2669, - "end_line": 2702, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_main_fail_on_new_includes_blocks", - "kind": "function", - "start_line": 2705, - "end_line": 2743, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_main_fail_threshold", - "kind": "function", - "start_line": 2637, - "end_line": 2666, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_main_no_progress_fallback", - "kind": "function", - "start_line": 1020, - "end_line": 1031, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_main_no_progress_fallback_quiet", - "kind": "function", - "start_line": 1034, - "end_line": 1056, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_main_no_progress_parallel", - "kind": "function", - "start_line": 674, - "end_line": 706, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_main_outputs", - "kind": "function", - "start_line": 1133, - "end_line": 1172, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_main_progress_fallback", - "kind": "function", - "start_line": 1006, - "end_line": 1017, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_main_progress_path", - "kind": "function", - "start_line": 1059, - "end_line": 1066, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_negative_size_limits_fail_fast", - "kind": "function", - "start_line": 2627, - "end_line": 2634, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_new_clones_warning", - "kind": "function", - "start_line": 2416, - "end_line": 2449, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_no_legacy_warning_when_legacy_missing", - "kind": "function", - "start_line": 879, - "end_line": 890, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_no_legacy_warning_when_paths_match", - "kind": "function", - "start_line": 893, - "end_line": 945, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_no_legacy_warning_with_cache_override", - "kind": "function", - "start_line": 858, - "end_line": 876, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_open_html_report_failure_warns_without_failing", - "kind": "function", - "start_line": 1191, - "end_line": 1207, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_open_html_report_opens_written_html", - "kind": "function", - "start_line": 1175, - "end_line": 1188, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_output_extension_validation", - "kind": "function", - "start_line": 2094, - "end_line": 2118, - "params": [ - { - "name": "bad_name", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "expected", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "flag", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "label", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_output_path_resolve_error_contract", - "kind": "function", - "start_line": 2121, - "end_line": 2145, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_outputs_quiet_no_print", - "kind": "function", - "start_line": 2181, - "end_line": 2213, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_report_flag_contract_errors", - "kind": "function", - "start_line": 1281, - "end_line": 1299, - "params": [ - { - "name": "argv", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='list', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "expected_message", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_report_meta_cache_path_resolve_oserror_fallback", - "kind": "function", - "start_line": 3179, - "end_line": 3214, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_report_write_error_is_contract_error", - "kind": "function", - "start_line": 2148, - "end_line": 2178, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_reports_cache_meta_when_cache_missing", - "kind": "function", - "start_line": 1948, - "end_line": 1970, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_reports_cache_too_large_respects_max_size_flag", - "kind": "function", - "start_line": 1916, - "end_line": 1945, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_reports_cache_used_false_on_warning", - "kind": "function", - "start_line": 1879, - "end_line": 1913, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "expected_message", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "expected_schema_version", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='object', ctx=Load())" - }, - { - "name": "expected_status", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "mutator", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[List(elts=[Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())], ctx=Load()), Constant(value=None)], ctx=Load()), ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_reports_include_audit_metadata_baseline_too_large", - "kind": "function", - "start_line": 1687, - "end_line": 1707, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_reports_include_audit_metadata_fingerprint_mismatch", - "kind": "function", - "start_line": 1387, - "end_line": 1410, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_reports_include_audit_metadata_generator_mismatch", - "kind": "function", - "start_line": 1604, - "end_line": 1623, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_reports_include_audit_metadata_integrity_failed", - "kind": "function", - "start_line": 1577, - "end_line": 1601, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_reports_include_audit_metadata_integrity_field_type_errors", - "kind": "function", - "start_line": 1638, - "end_line": 1659, - "params": [ - { - "name": "bad_value", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='object', ctx=Load())" - }, - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "expected_message", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "expected_status", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "field", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_reports_include_audit_metadata_integrity_missing", - "kind": "function", - "start_line": 1662, - "end_line": 1684, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_reports_include_audit_metadata_invalid_baseline", - "kind": "function", - "start_line": 1465, - "end_line": 1481, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_reports_include_audit_metadata_legacy_baseline", - "kind": "function", - "start_line": 1484, - "end_line": 1509, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_reports_include_audit_metadata_missing_baseline", - "kind": "function", - "start_line": 1367, - "end_line": 1384, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_reports_include_audit_metadata_ok", - "kind": "function", - "start_line": 1302, - "end_line": 1364, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_reports_include_audit_metadata_python_mismatch", - "kind": "function", - "start_line": 1439, - "end_line": 1462, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_reports_include_audit_metadata_schema_mismatch", - "kind": "function", - "start_line": 1413, - "end_line": 1436, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_reports_include_source_io_skipped_zero", - "kind": "function", - "start_line": 3078, - "end_line": 3098, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_scan_failed_is_internal_error", - "kind": "function", - "start_line": 3338, - "end_line": 3351, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_scan_oserror_is_contract_error", - "kind": "function", - "start_line": 3354, - "end_line": 3368, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_summary_cache_miss_metrics", - "kind": "function", - "start_line": 3261, - "end_line": 3279, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_summary_format_stable", - "kind": "function", - "start_line": 3282, - "end_line": 3307, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_summary_no_color_has_no_ansi", - "kind": "function", - "start_line": 3325, - "end_line": 3335, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_summary_with_api_surface_shows_public_api_line", - "kind": "function", - "start_line": 3310, - "end_line": 3322, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_timestamped_report_paths_apply_to_bare_report_flags", - "kind": "function", - "start_line": 1210, - "end_line": 1238, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_timestamped_report_paths_do_not_rewrite_explicit_paths", - "kind": "function", - "start_line": 1241, - "end_line": 1263, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_too_large_baseline_fails_in_ci", - "kind": "function", - "start_line": 1838, - "end_line": 1859, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_unexpected_grouping_failure_is_internal", - "kind": "function", - "start_line": 1093, - "end_line": 1108, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_unexpected_html_render_failure_is_internal", - "kind": "function", - "start_line": 1111, - "end_line": 1130, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_unexpected_root_resolution_failure_is_internal", - "kind": "function", - "start_line": 1079, - "end_line": 1090, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_unreadable_source_ci_shows_overflow_summary", - "kind": "function", - "start_line": 3141, - "end_line": 3176, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_unreadable_source_fails_in_ci_with_contract_error", - "kind": "function", - "start_line": 3041, - "end_line": 3075, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_unreadable_source_normal_mode_warns_and_continues", - "kind": "function", - "start_line": 3007, - "end_line": 3038, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_untrusted_baseline_fails_in_ci", - "kind": "function", - "start_line": 1791, - "end_line": 1816, - "params": [ - { - "name": "bad_value", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='object', ctx=Load())" - }, - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "expected_message", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "expected_status", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "field", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_untrusted_baseline_ignored_for_diff", - "kind": "function", - "start_line": 1710, - "end_line": 1770, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_update_baseline", - "kind": "function", - "start_line": 2241, - "end_line": 2274, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_update_baseline_report_meta_uses_updated_payload_hash", - "kind": "function", - "start_line": 2277, - "end_line": 2305, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_update_baseline_skips_version_check", - "kind": "function", - "start_line": 2216, - "end_line": 2238, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_update_baseline_with_invalid_existing_file", - "kind": "function", - "start_line": 2338, - "end_line": 2368, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_update_baseline_write_error_is_contract_error", - "kind": "function", - "start_line": 2308, - "end_line": 2335, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_warns_on_legacy_cache", - "kind": "function", - "start_line": 802, - "end_line": 820, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_worker_failed", - "kind": "function", - "start_line": 3413, - "end_line": 3430, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_worker_failed_progress_sequential", - "kind": "function", - "start_line": 3433, - "end_line": 3443, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_worker_failed_sequential_no_progress", - "kind": "function", - "start_line": 3446, - "end_line": 3456, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_parse_metric_reason_entry_contract", - "kind": "function", - "start_line": 3943, - "end_line": 3946, - "params": [ - { - "name": "expected", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='tuple', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='str', ctx=Load())], ctx=Load()), ctx=Load())" - }, - { - "name": "reason", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_structural_findings_do_not_affect_clone_counts", - "kind": "function", - "start_line": 3722, - "end_line": 3743, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_structural_findings_do_not_affect_exit_code", - "kind": "function", - "start_line": 3746, - "end_line": 3756, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_structural_findings_recomputed_when_cache_was_built_without_reports", - "kind": "function", - "start_line": 3759, - "end_line": 3818, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - } - ] - }, - { - "module": "tests.test_cli_unit", - "filepath": "tests/test_cli_unit.py", - "all_declared": [], - "symbols": [ - { - "local_name": "test_argument_parser_contract_error_marker_for_invalid_args", - "kind": "function", - "start_line": 358, - "end_line": 366, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_banner_title_without_root_returns_single_line", - "kind": "function", - "start_line": 923, - "end_line": 926, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_changed_clone_gate_from_report_filters_changed_scope", - "kind": "function", - "start_line": 563, - "end_line": 619, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_help_text_consistency", - "kind": "function", - "start_line": 178, - "end_line": 229, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_internal_error_debug_env_includes_traceback", - "kind": "function", - "start_line": 340, - "end_line": 355, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_internal_error_debug_flag_includes_traceback", - "kind": "function", - "start_line": 322, - "end_line": 337, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_internal_error_marker", - "kind": "function", - "start_line": 302, - "end_line": 319, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_module_main_guard", - "kind": "function", - "start_line": 152, - "end_line": 156, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_plain_console_status_context", - "kind": "function", - "start_line": 296, - "end_line": 299, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_cli_version_flag_no_side_effects", - "kind": "function", - "start_line": 159, - "end_line": 175, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_compact_summary_labels_use_machine_scannable_keys", - "kind": "function", - "start_line": 950, - "end_line": 971, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_configure_metrics_mode_forces_dependency_and_dead_code_when_gated", - "kind": "function", - "start_line": 1195, - "end_line": 1211, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_configure_metrics_mode_rejects_skip_metrics_with_metrics_flags", - "kind": "function", - "start_line": 1173, - "end_line": 1192, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_enforce_gating_drops_rewritten_threshold_when_changed_scope_is_within_limit", - "kind": "function", - "start_line": 730, - "end_line": 765, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_enforce_gating_rewrites_clone_threshold_for_changed_scope", - "kind": "function", - "start_line": 684, - "end_line": 727, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_git_diff_changed_paths_normalizes_subprocess_output", - "kind": "function", - "start_line": 483, - "end_line": 504, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_git_diff_changed_paths_rejects_option_like_ref", - "kind": "function", - "start_line": 521, - "end_line": 527, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_git_diff_changed_paths_rejects_unsafe_ref_syntax", - "kind": "function", - "start_line": 540, - "end_line": 550, - "params": [ - { - "name": "git_diff_ref", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_git_diff_changed_paths_reports_subprocess_errors", - "kind": "function", - "start_line": 507, - "end_line": 518, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_main_impl_ci_enables_fail_on_new_metrics_when_metrics_baseline_loaded", - "kind": "function", - "start_line": 1795, - "end_line": 1833, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_main_impl_debug_sets_env_and_handles_metrics_baseline_resolve_error", - "kind": "function", - "start_line": 1288, - "end_line": 1317, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_main_impl_exits_on_invalid_pyproject_config", - "kind": "function", - "start_line": 1273, - "end_line": 1285, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_main_impl_fail_on_new_metrics_handles_load_error", - "kind": "function", - "start_line": 1650, - "end_line": 1659, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_main_impl_fail_on_new_metrics_handles_verify_error", - "kind": "function", - "start_line": 1662, - "end_line": 1682, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_main_impl_fail_on_new_metrics_requires_existing_baseline", - "kind": "function", - "start_line": 1629, - "end_line": 1647, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_main_impl_prints_changed_scope_when_changed_projection_is_available", - "kind": "function", - "start_line": 768, - "end_line": 887, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_main_impl_prints_metric_gate_reasons_and_exits_gating_failure", - "kind": "function", - "start_line": 1503, - "end_line": 1545, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_main_impl_rejects_update_metrics_baseline_when_metrics_skipped", - "kind": "function", - "start_line": 1459, - "end_line": 1478, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_main_impl_skip_metrics_defensive_contract_guard", - "kind": "function", - "start_line": 1606, - "end_line": 1626, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_main_impl_unified_metrics_update_auto_enables_baseline_update", - "kind": "function", - "start_line": 1579, - "end_line": 1603, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_main_impl_update_metrics_baseline_requires_project_metrics", - "kind": "function", - "start_line": 1481, - "end_line": 1500, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_main_impl_update_metrics_baseline_separate_path_message_branch", - "kind": "function", - "start_line": 1764, - "end_line": 1792, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_main_impl_update_metrics_baseline_write_error_contract", - "kind": "function", - "start_line": 1732, - "end_line": 1761, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_main_impl_uses_configured_metrics_baseline_without_cli_flag", - "kind": "function", - "start_line": 1548, - "end_line": 1576, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_make_console_caps_width_to_layout_limit", - "kind": "function", - "start_line": 890, - "end_line": 920, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_runtime_requires_adoption_snapshot_for_regression_gates", - "kind": "function", - "start_line": 1685, - "end_line": 1706, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_runtime_requires_api_surface_snapshot_for_api_gate", - "kind": "function", - "start_line": 1709, - "end_line": 1729, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_computed_includes_api_surface_only_when_enabled", - "kind": "function", - "start_line": 1247, - "end_line": 1270, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_computed_respects_skip_switches", - "kind": "function", - "start_line": 1222, - "end_line": 1244, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_normalize_changed_paths_rejects_outside_root", - "kind": "function", - "start_line": 470, - "end_line": 480, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_normalize_changed_paths_relativizes_dedupes_and_sorts", - "kind": "function", - "start_line": 417, - "end_line": 429, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_normalize_changed_paths_reports_unresolvable_path", - "kind": "function", - "start_line": 452, - "end_line": 467, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_normalize_changed_paths_skips_empty_relative_results", - "kind": "function", - "start_line": 432, - "end_line": 449, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_open_html_report_in_browser_raises_without_handler", - "kind": "function", - "start_line": 269, - "end_line": 283, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_open_html_report_in_browser_succeeds_when_handler_exists", - "kind": "function", - "start_line": 286, - "end_line": 293, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_print_changed_scope_uses_compact_line_in_quiet_mode", - "kind": "function", - "start_line": 1078, - "end_line": 1093, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_print_changed_scope_uses_dedicated_block", - "kind": "function", - "start_line": 1057, - "end_line": 1075, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_print_metrics_in_normal_mode_includes_adoption_and_public_api", - "kind": "function", - "start_line": 1127, - "end_line": 1170, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_print_metrics_in_quiet_mode_includes_overloaded_modules", - "kind": "function", - "start_line": 1096, - "end_line": 1124, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_print_summary_invariant_warning", - "kind": "function", - "start_line": 929, - "end_line": 947, - "params": [ - { - "name": "capsys", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Attribute(value=Name(id='pytest', ctx=Load()), attr='CaptureFixture', ctx=Load()), slice=Name(id='str', ctx=Load()), ctx=Load())" - }, - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_print_verbose_clone_hashes_noop_on_empty", - "kind": "function", - "start_line": 1836, - "end_line": 1843, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_print_verbose_clone_hashes_prints_sorted_values", - "kind": "function", - "start_line": 1846, - "end_line": 1857, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_probe_metrics_baseline_section_for_non_object_payload", - "kind": "function", - "start_line": 1214, - "end_line": 1219, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_process_file_encoding_error", - "kind": "function", - "start_line": 96, - "end_line": 109, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_process_file_read_oserror", - "kind": "function", - "start_line": 112, - "end_line": 125, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_process_file_stat_error", - "kind": "function", - "start_line": 76, - "end_line": 93, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_process_file_success", - "kind": "function", - "start_line": 144, - "end_line": 149, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_process_file_unexpected_error", - "kind": "function", - "start_line": 128, - "end_line": 141, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_report_path_origins_distinguish_bare_and_explicit_flags", - "kind": "function", - "start_line": 232, - "end_line": 248, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_report_path_origins_ignores_unrelated_equals_tokens", - "kind": "function", - "start_line": 553, - "end_line": 560, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_report_path_origins_stops_at_double_dash", - "kind": "function", - "start_line": 251, - "end_line": 258, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_run_analysis_stages_prints_source_read_failures_when_failed_files_are_empty", - "kind": "function", - "start_line": 643, - "end_line": 681, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_run_analysis_stages_requires_rich_console_when_progress_ui_is_enabled", - "kind": "function", - "start_line": 622, - "end_line": 640, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_timestamped_report_path_appends_utc_slug", - "kind": "function", - "start_line": 261, - "end_line": 266, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_ui_summary_formatters_cover_optional_branches", - "kind": "function", - "start_line": 974, - "end_line": 1054, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_validate_changed_scope_args_promotes_paths_from_git_diff", - "kind": "function", - "start_line": 407, - "end_line": 414, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_validate_changed_scope_args_rejects_invalid_combinations", - "kind": "function", - "start_line": 398, - "end_line": 404, - "params": [ - { - "name": "args", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Namespace', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - } - ] - }, - { - "module": "tests.test_html_report", - "filepath": "tests/test_html_report.py", - "all_declared": [], - "symbols": [ - { - "local_name": "build_html_report", - "kind": "function", - "start_line": 68, - "end_line": 87, - "params": [ - { - "name": "block_group_facts", - "kind": "kw_only", - "has_default": true, - "annotation_hash": "BinOp(left=Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='str', ctx=Load())], ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load()), op=BitOr(), right=Constant(value=None))" - }, - { - "name": "block_groups", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='list', ctx=Load()), slice=Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='Any', ctx=Load())], ctx=Load()), ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" - }, - { - "name": "func_groups", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='list', ctx=Load()), slice=Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='Any', ctx=Load())], ctx=Load()), ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" - }, - { - "name": "segment_groups", - "kind": "kw_only", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='list', ctx=Load()), slice=Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='Any', ctx=Load())], ctx=Load()), ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" - }, - { - "name": "kwargs", - "kind": "kwarg", - "has_default": false, - "annotation_hash": "Name(id='Any', ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - }, - { - "local_name": "test_file_cache_missing_file", - "kind": "function", - "start_line": 1160, - "end_line": 1164, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_file_cache_range_bounds", - "kind": "function", - "start_line": 1196, - "end_line": 1203, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_file_cache_reads_ranges", - "kind": "function", - "start_line": 1146, - "end_line": 1157, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_file_cache_unicode_fallback", - "kind": "function", - "start_line": 1188, - "end_line": 1193, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_and_json_group_order_consistent", - "kind": "function", - "start_line": 1070, - "end_line": 1116, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_bare_qualname_keeps_non_python_path_prefix", - "kind": "function", - "start_line": 2441, - "end_line": 2457, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_block_group_includes_assert_only_explanation", - "kind": "function", - "start_line": 672, - "end_line": 681, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_block_group_includes_match_basis_and_compact_key", - "kind": "function", - "start_line": 645, - "end_line": 669, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_block_group_n_way_compare_hint", - "kind": "function", - "start_line": 684, - "end_line": 693, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_blocks_without_explanation_meta", - "kind": "function", - "start_line": 755, - "end_line": 760, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_coupling_coupled_classes_expands_for_more_than_three", - "kind": "function", - "start_line": 2241, - "end_line": 2254, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_coupling_coupled_classes_inline_for_three_or_less", - "kind": "function", - "start_line": 2229, - "end_line": 2238, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_coupling_coupled_classes_truncates_long_labels", - "kind": "function", - "start_line": 2257, - "end_line": 2261, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_dependency_chain_columns_render_html", - "kind": "function", - "start_line": 2413, - "end_line": 2438, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_dependency_graph_handles_rootless_and_disconnected_nodes", - "kind": "function", - "start_line": 2264, - "end_line": 2297, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_dependency_graph_rootless_fallback_seed", - "kind": "function", - "start_line": 2300, - "end_line": 2322, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_dependency_hubs_deterministic_tie_order", - "kind": "function", - "start_line": 2374, - "end_line": 2410, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_deterministic_group_order", - "kind": "function", - "start_line": 1033, - "end_line": 1067, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_direct_path_skips_directory_hotspots_cluster", - "kind": "function", - "start_line": 2023, - "end_line": 2042, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_directory_hotspots_use_test_scope_roots", - "kind": "function", - "start_line": 2045, - "end_line": 2115, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_empty", - "kind": "function", - "start_line": 196, - "end_line": 202, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_escapes_control_chars_in_payload", - "kind": "function", - "start_line": 1119, - "end_line": 1143, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_escapes_meta_and_title", - "kind": "function", - "start_line": 975, - "end_line": 1006, - "params": [ - { - "name": "report_meta_factory", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[Constant(value=Ellipsis), Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_escapes_script_breakout_payload", - "kind": "function", - "start_line": 1009, - "end_line": 1030, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_executive_summary_includes_effective_analysis_profile", - "kind": "function", - "start_line": 1831, - "end_line": 1878, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_explanation_without_match_rule", - "kind": "function", - "start_line": 798, - "end_line": 809, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_exposes_scope_counter_hooks_for_clone_ui", - "kind": "function", - "start_line": 368, - "end_line": 411, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_finding_cards_expose_stable_anchor_ids", - "kind": "function", - "start_line": 573, - "end_line": 642, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_footer_links_present", - "kind": "function", - "start_line": 887, - "end_line": 892, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_generation", - "kind": "function", - "start_line": 214, - "end_line": 236, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_group_and_item_metadata_attrs", - "kind": "function", - "start_line": 239, - "end_line": 265, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_handles_root_only_baseline_path", - "kind": "function", - "start_line": 788, - "end_line": 795, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_includes_provenance_metadata", - "kind": "function", - "start_line": 895, - "end_line": 947, - "params": [ - { - "name": "report_meta_factory", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[Constant(value=Ellipsis), Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_metrics_bad_health_score_and_dead_code_ok_tone", - "kind": "function", - "start_line": 2118, - "end_line": 2139, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_metrics_bool_health_score_and_long_dependency_labels", - "kind": "function", - "start_line": 2142, - "end_line": 2172, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_metrics_object_health_score_uses_float_fallback", - "kind": "function", - "start_line": 2207, - "end_line": 2226, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_metrics_risk_branches", - "kind": "function", - "start_line": 1625, - "end_line": 1653, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_metrics_warn_branches_and_dependency_svg", - "kind": "function", - "start_line": 1599, - "end_line": 1622, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_metrics_without_health_score_uses_info_overview", - "kind": "function", - "start_line": 1955, - "end_line": 1980, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_missing_source_snippet_fallback", - "kind": "function", - "start_line": 1167, - "end_line": 1185, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_mobile_topbar_reflows_brand_block", - "kind": "function", - "start_line": 839, - "end_line": 849, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_n_way_group_without_compare_note", - "kind": "function", - "start_line": 812, - "end_line": 824, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_narrow_kpi_cards_keep_badges_inside_card", - "kind": "function", - "start_line": 852, - "end_line": 866, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_overview_includes_adoption_and_api_summary_cluster", - "kind": "function", - "start_line": 1881, - "end_line": 1952, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_overview_includes_hotspot_sections_without_quick_views", - "kind": "function", - "start_line": 2495, - "end_line": 2545, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_overview_uses_canonical_report_overview_hotlists", - "kind": "function", - "start_line": 2548, - "end_line": 2670, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_provenance_badges_cover_mismatch_and_untrusted_metrics", - "kind": "function", - "start_line": 2325, - "end_line": 2352, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_provenance_handles_non_boolean_baseline_loaded", - "kind": "function", - "start_line": 2355, - "end_line": 2371, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_provenance_summary_uses_card_like_badges", - "kind": "function", - "start_line": 950, - "end_line": 972, - "params": [ - { - "name": "report_meta_factory", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='Callable', ctx=Load()), slice=Tuple(elts=[Constant(value=Ellipsis), Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='object', ctx=Load())], ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_pygments_fallback", - "kind": "function", - "start_line": 1314, - "end_line": 1326, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_renders_block_novelty_tabs_and_group_flags", - "kind": "function", - "start_line": 335, - "end_line": 365, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_renders_dead_code_split_with_suppressed_layer", - "kind": "function", - "start_line": 2175, - "end_line": 2204, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_renders_directory_hotspots_from_canonical_report", - "kind": "function", - "start_line": 1983, - "end_line": 2020, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_renders_novelty_tabs_and_group_flags", - "kind": "function", - "start_line": 268, - "end_line": 309, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_renders_overloaded_modules_from_legacy_god_modules_key", - "kind": "function", - "start_line": 1733, - "end_line": 1757, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_renders_overloaded_modules_in_quality_and_overview", - "kind": "function", - "start_line": 1656, - "end_line": 1730, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_renders_run_snapshot_from_canonical_inventory", - "kind": "function", - "start_line": 1760, - "end_line": 1828, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_renders_untrusted_baseline_novelty_note", - "kind": "function", - "start_line": 312, - "end_line": 332, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_requires_block_group_facts_argument", - "kind": "function", - "start_line": 205, - "end_line": 211, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_respects_sparse_core_block_facts", - "kind": "function", - "start_line": 763, - "end_line": 785, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_segments_section", - "kind": "function", - "start_line": 1329, - "end_line": 1356, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_single_item_group", - "kind": "function", - "start_line": 1359, - "end_line": 1379, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_structural_findings_tab_uses_normalized_groups", - "kind": "function", - "start_line": 414, - "end_line": 500, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_structural_findings_why_modal_renders_examples", - "kind": "function", - "start_line": 503, - "end_line": 570, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_suggestions_cards_split_facts_assessment_and_action", - "kind": "function", - "start_line": 2460, - "end_line": 2492, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_table_css_matches_rendered_column_classes", - "kind": "function", - "start_line": 869, - "end_line": 884, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_topbar_actions_present", - "kind": "function", - "start_line": 827, - "end_line": 836, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_uses_core_block_group_facts", - "kind": "function", - "start_line": 696, - "end_line": 718, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_uses_core_hint_and_pattern_labels", - "kind": "function", - "start_line": 721, - "end_line": 736, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_uses_core_hint_context_label", - "kind": "function", - "start_line": 739, - "end_line": 752, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_html_report_with_blocks", - "kind": "function", - "start_line": 1281, - "end_line": 1311, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_pygments_css", - "kind": "function", - "start_line": 1230, - "end_line": 1232, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_pygments_css_formatter_init_fails", - "kind": "function", - "start_line": 1415, - "end_line": 1424, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_pygments_css_get_style_defs_error", - "kind": "function", - "start_line": 1403, - "end_line": 1412, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_pygments_css_import_error", - "kind": "function", - "start_line": 1240, - "end_line": 1245, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_pygments_css_invalid_style", - "kind": "function", - "start_line": 1235, - "end_line": 1237, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_render_code_block_truncate", - "kind": "function", - "start_line": 1206, - "end_line": 1227, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_render_code_block_truncates_and_fallback", - "kind": "function", - "start_line": 1382, - "end_line": 1400, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_render_code_block_without_pygments_uses_escaped_fallback", - "kind": "function", - "start_line": 1261, - "end_line": 1278, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_try_pygments_missing", - "kind": "function", - "start_line": 1248, - "end_line": 1253, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_try_pygments_ok", - "kind": "function", - "start_line": 1256, - "end_line": 1258, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "to_json_report", - "kind": "function", - "start_line": 55, - "end_line": 65, - "params": [ - { - "name": "block_groups", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='list', ctx=Load()), slice=Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='Any', ctx=Load())], ctx=Load()), ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" - }, - { - "name": "func_groups", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='list', ctx=Load()), slice=Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='Any', ctx=Load())], ctx=Load()), ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" - }, - { - "name": "segment_groups", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Subscript(value=Name(id='list', ctx=Load()), slice=Subscript(value=Name(id='dict', ctx=Load()), slice=Tuple(elts=[Name(id='str', ctx=Load()), Name(id='Any', ctx=Load())], ctx=Load()), ctx=Load()), ctx=Load())], ctx=Load()), ctx=Load())" - } - ], - "returns_hash": "Name(id='str', ctx=Load())", - "exported_via": "name" - } - ] - }, - { - "module": "tests.test_metrics_baseline", - "filepath": "tests/test_metrics_baseline.py", - "all_declared": [], - "symbols": [ - { - "local_name": "test_api_surface_payload_hashes_are_order_independent", - "kind": "function", - "start_line": 412, - "end_line": 433, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_coerce_metrics_baseline_status_variants", - "kind": "function", - "start_line": 227, - "end_line": 240, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_atomic_write_json_cleans_up_temp_file_on_replace_failure", - "kind": "function", - "start_line": 449, - "end_line": 466, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_diff_tracks_adoption_and_api_surface_deltas", - "kind": "function", - "start_line": 551, - "end_line": 609, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_diff_without_snapshot_uses_default_snapshot", - "kind": "function", - "start_line": 539, - "end_line": 548, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_embedded_clone_payload_and_schema_resolution", - "kind": "function", - "start_line": 893, - "end_line": 963, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_field_parsers_and_cycle_parser", - "kind": "function", - "start_line": 795, - "end_line": 846, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_json_and_structure_validators", - "kind": "function", - "start_line": 775, - "end_line": 792, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_load_accepts_absolute_api_surface_filepaths", - "kind": "function", - "start_line": 742, - "end_line": 772, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_load_accepts_legacy_api_surface_qualnames", - "kind": "function", - "start_line": 698, - "end_line": 739, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_load_json_read_oserror_status", - "kind": "function", - "start_line": 974, - "end_line": 987, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_load_missing_file_is_noop", - "kind": "function", - "start_line": 243, - "end_line": 246, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_load_rejects_non_object_preloaded_payload", - "kind": "function", - "start_line": 308, - "end_line": 317, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_load_size_and_shape_validation", - "kind": "function", - "start_line": 291, - "end_line": 305, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_load_stat_error_after_exists_true", - "kind": "function", - "start_line": 320, - "end_line": 346, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_load_stat_errors", - "kind": "function", - "start_line": 249, - "end_line": 288, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - }, - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_load_tracks_adoption_snapshot_presence", - "kind": "function", - "start_line": 647, - "end_line": 669, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_parse_generator_variants", - "kind": "function", - "start_line": 849, - "end_line": 890, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_parse_snapshot_grade_validation", - "kind": "function", - "start_line": 966, - "end_line": 971, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_save_embedded_clone_baseline_preserves_api_surface", - "kind": "function", - "start_line": 672, - "end_line": 695, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_save_rejects_corrupted_existing_payload", - "kind": "function", - "start_line": 469, - "end_line": 479, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_save_relativizes_api_surface_filepaths", - "kind": "function", - "start_line": 394, - "end_line": 409, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_save_requires_snapshot", - "kind": "function", - "start_line": 349, - "end_line": 352, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_save_standalone_payload_sets_metadata", - "kind": "function", - "start_line": 355, - "end_line": 372, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_save_with_existing_plain_payload_rewrites_plain", - "kind": "function", - "start_line": 436, - "end_line": 446, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_save_writes_compact_api_surface_local_names", - "kind": "function", - "start_line": 375, - "end_line": 391, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_verify_accepts_previous_minor_versions", - "kind": "function", - "start_line": 522, - "end_line": 536, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_baseline_verify_compatibility_and_integrity_failures", - "kind": "function", - "start_line": 482, - "end_line": 519, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_snapshot_from_project_metrics_and_from_project_metrics_factory", - "kind": "function", - "start_line": 612, - "end_line": 644, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - } - ] - }, - { - "module": "tests.test_pipeline_metrics", - "filepath": "tests/test_pipeline_metrics.py", - "all_declared": [], - "symbols": [ - { - "local_name": "test_build_metrics_report_payload_includes_adoption_and_api_surface_families", - "kind": "function", - "start_line": 263, - "end_line": 335, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_build_metrics_report_payload_includes_suppressed_dead_code_items", - "kind": "function", - "start_line": 229, - "end_line": 260, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_build_overloaded_modules_payload_flags_project_relative_candidates", - "kind": "function", - "start_line": 389, - "end_line": 456, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_compute_project_metrics_respects_skip_flags", - "kind": "function", - "start_line": 189, - "end_line": 226, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_enrich_metrics_report_payload_adds_docstring_and_breaking_api_rows", - "kind": "function", - "start_line": 725, - "end_line": 784, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_load_cached_metrics_extended_decodes_adoption_and_api_surface", - "kind": "function", - "start_line": 638, - "end_line": 688, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_load_cached_metrics_ignores_referenced_names_from_test_files", - "kind": "function", - "start_line": 459, - "end_line": 478, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_load_cached_metrics_preserves_coupled_classes", - "kind": "function", - "start_line": 481, - "end_line": 508, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_load_cached_metrics_preserves_dead_candidate_suppressions", - "kind": "function", - "start_line": 511, - "end_line": 534, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metric_gate_reasons_collects_all_enabled_reasons", - "kind": "function", - "start_line": 691, - "end_line": 722, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metric_gate_reasons_include_adoption_and_api_surface_contracts", - "kind": "function", - "start_line": 856, - "end_line": 904, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metric_gate_reasons_new_metrics_optional_buckets_empty", - "kind": "function", - "start_line": 830, - "end_line": 853, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metric_gate_reasons_partial_new_metrics_paths", - "kind": "function", - "start_line": 804, - "end_line": 827, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metric_gate_reasons_skip_disabled_and_non_critical_paths", - "kind": "function", - "start_line": 787, - "end_line": 801, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_metrics_payload_includes_overloaded_modules_for_small_population", - "kind": "function", - "start_line": 338, - "end_line": 386, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_module_names_from_units_extracts_module_prefixes", - "kind": "function", - "start_line": 178, - "end_line": 186, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_pipeline_basic_helpers_and_sort_keys", - "kind": "function", - "start_line": 150, - "end_line": 175, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_pipeline_cache_decode_helpers_cover_invalid_and_valid_payloads", - "kind": "function", - "start_line": 537, - "end_line": 635, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - } - ] - }, - { - "module": "tests.test_report_contract_coverage", - "filepath": "tests/test_report_contract_coverage.py", - "all_declared": [], - "symbols": [ - { - "local_name": "test_build_report_document_suppressed_dead_code_accepts_empty_bindings", - "kind": "function", - "start_line": 2238, - "end_line": 2278, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_coerce_helper_numeric_branches", - "kind": "function", - "start_line": 1130, - "end_line": 1136, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_collect_paths_from_metrics_covers_all_metric_families_and_skips_missing", - "kind": "function", - "start_line": 2055, - "end_line": 2117, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_collect_report_file_list_deterministically_merges_all_sources", - "kind": "function", - "start_line": 2120, - "end_line": 2185, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_count_file_lines_aggregates_paths", - "kind": "function", - "start_line": 1139, - "end_line": 1144, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_derive_inventory_code_counts_uses_cached_line_scan_fallback", - "kind": "function", - "start_line": 1147, - "end_line": 1177, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_derived_module_branches", - "kind": "function", - "start_line": 1427, - "end_line": 1467, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_directory_hotspot_helpers_cover_fallback_paths", - "kind": "function", - "start_line": 939, - "end_line": 1010, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_directory_hotspots_collapses_test_scope_roots_for_overview", - "kind": "function", - "start_line": 831, - "end_line": 936, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_directory_hotspots_has_more_root_paths_and_stable_sort", - "kind": "function", - "start_line": 757, - "end_line": 828, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_json_contract_private_helper_edge_branches", - "kind": "function", - "start_line": 2188, - "end_line": 2235, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_json_contract_private_helpers_cover_edge_cases", - "kind": "function", - "start_line": 1034, - "end_line": 1127, - "params": [ - { - "name": "tmp_path", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Path', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_markdown_and_sarif_reuse_prebuilt_report_document", - "kind": "function", - "start_line": 1013, - "end_line": 1031, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_markdown_render_long_list_branches", - "kind": "function", - "start_line": 1180, - "end_line": 1218, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_overview_handles_non_mapping_metric_summaries", - "kind": "function", - "start_line": 1555, - "end_line": 1586, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_overview_health_snapshot_handles_non_mapping_dimensions", - "kind": "function", - "start_line": 1589, - "end_line": 1600, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_overview_module_branches", - "kind": "function", - "start_line": 1470, - "end_line": 1552, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_render_sarif_report_document_without_srcroot_keeps_relative_payload", - "kind": "function", - "start_line": 1997, - "end_line": 2052, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_report_contract_includes_canonical_adoption_and_api_surface_families", - "kind": "function", - "start_line": 1281, - "end_line": 1417, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_report_contract_includes_canonical_overloaded_modules_family", - "kind": "function", - "start_line": 1246, - "end_line": 1278, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_report_contract_renderers_include_overloaded_modules_section", - "kind": "function", - "start_line": 1233, - "end_line": 1243, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_report_document_design_thresholds_can_change_canonical_findings", - "kind": "function", - "start_line": 679, - "end_line": 754, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_report_document_rich_invariants_and_renderers", - "kind": "function", - "start_line": 573, - "end_line": 676, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_sarif_and_serialize_helpers_cover_missing_primary_path_and_no_empty_tail", - "kind": "function", - "start_line": 2442, - "end_line": 2594, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_sarif_helper_level_mapping", - "kind": "function", - "start_line": 1420, - "end_line": 1424, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_sarif_private_helper_branches", - "kind": "function", - "start_line": 1683, - "end_line": 1752, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_sarif_private_helper_edge_branches", - "kind": "function", - "start_line": 1963, - "end_line": 1994, - "params": [ - { - "name": "monkeypatch", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Attribute(value=Name(id='pytest', ctx=Load()), attr='MonkeyPatch', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_sarif_private_helper_family_dispatches", - "kind": "function", - "start_line": 1755, - "end_line": 1960, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_serialize_private_helpers_cover_structural_and_suppression_paths", - "kind": "function", - "start_line": 2281, - "end_line": 2439, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_suggestion_finding_id_clone_branches", - "kind": "function", - "start_line": 1676, - "end_line": 1680, - "params": [ - { - "name": "expected_finding_id", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='str', ctx=Load())" - }, - { - "name": "suggestion", - "kind": "pos_or_kw", - "has_default": false, - "annotation_hash": "Name(id='Suggestion', ctx=Load())" - } - ], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - }, - { - "local_name": "test_suggestion_finding_id_fallback_branch", - "kind": "function", - "start_line": 1603, - "end_line": 1622, - "params": [], - "returns_hash": "Constant(value=None)", - "exported_via": "name" - } - ] - } - ] + "health_score": 89, + "health_grade": "B" } } diff --git a/codeclone/_cli_args.py b/codeclone/_cli_args.py index 53418ec..9fd6193 100644 --- a/codeclone/_cli_args.py +++ b/codeclone/_cli_args.py @@ -126,6 +126,7 @@ def build_parser(version: str) -> _ArgumentParser: block_min_stmt=DEFAULT_BLOCK_MIN_STMT, segment_min_loc=DEFAULT_SEGMENT_MIN_LOC, segment_min_stmt=DEFAULT_SEGMENT_MIN_STMT, + golden_fixture_paths=(), ) analysis_group.add_argument( "--processes", @@ -230,6 +231,13 @@ def build_parser(version: str) -> _ArgumentParser: flag="--api-surface", help_text=ui.HELP_API_SURFACE, ) + baselines_ci_group.add_argument( + "--coverage", + dest="coverage_xml", + metavar="FILE", + default=None, + help=ui.HELP_COVERAGE, + ) quality_group = ap.add_argument_group("Quality gates") _add_bool_optional_argument( @@ -310,6 +318,11 @@ def build_parser(version: str) -> _ArgumentParser: flag="--fail-on-api-break", help_text=ui.HELP_FAIL_ON_API_BREAK, ) + _add_bool_optional_argument( + quality_group, + flag="--fail-on-untested-hotspots", + help_text=ui.HELP_FAIL_ON_UNTESTED_HOTSPOTS, + ) quality_group.add_argument( "--min-typing-coverage", type=int, @@ -324,6 +337,13 @@ def build_parser(version: str) -> _ArgumentParser: metavar="PERCENT", help=ui.HELP_MIN_DOCSTRING_COVERAGE, ) + quality_group.add_argument( + "--coverage-min", + type=int, + default=50, + metavar="PERCENT", + help=ui.HELP_COVERAGE_MIN, + ) stages_group = ap.add_argument_group("Analysis stages") _add_bool_optional_argument( diff --git a/codeclone/_cli_baselines.py b/codeclone/_cli_baselines.py index 19120a9..8ae4baf 100644 --- a/codeclone/_cli_baselines.py +++ b/codeclone/_cli_baselines.py @@ -62,6 +62,9 @@ class _BaselineArgs(Protocol): fail_on_typing_regression: bool fail_on_docstring_regression: bool fail_on_api_break: bool + typing_coverage: bool + docstring_coverage: bool + api_surface: bool ci: bool @@ -385,6 +388,8 @@ def _update_metrics_baseline_if_requested( new_metrics_baseline = MetricsBaseline.from_project_metrics( project_metrics=project_metrics, path=metrics_baseline_path, + include_adoption=args.typing_coverage or args.docstring_coverage, + include_api_surface=args.api_surface, ) try: new_metrics_baseline.save() diff --git a/codeclone/_cli_config.py b/codeclone/_cli_config.py index 1a48584..5246663 100644 --- a/codeclone/_cli_config.py +++ b/codeclone/_cli_config.py @@ -12,6 +12,11 @@ from pathlib import Path from typing import TYPE_CHECKING, Final +from .golden_fixtures import ( + GoldenFixturePatternError, + normalize_golden_fixture_patterns, +) + if TYPE_CHECKING: import argparse from collections.abc import Mapping, Sequence @@ -25,6 +30,7 @@ class ConfigValidationError(ValueError): class _ConfigKeySpec: expected_type: type[object] allow_none: bool = False + expected_name: str | None = None _CONFIG_KEY_SPECS: Final[dict[str, _ConfigKeySpec]] = { @@ -53,16 +59,20 @@ class _ConfigKeySpec: "typing_coverage": _ConfigKeySpec(bool), "docstring_coverage": _ConfigKeySpec(bool), "api_surface": _ConfigKeySpec(bool), + "coverage_xml": _ConfigKeySpec(str, allow_none=True), "fail_on_typing_regression": _ConfigKeySpec(bool), "fail_on_docstring_regression": _ConfigKeySpec(bool), "fail_on_api_break": _ConfigKeySpec(bool), + "fail_on_untested_hotspots": _ConfigKeySpec(bool), "min_typing_coverage": _ConfigKeySpec(int), "min_docstring_coverage": _ConfigKeySpec(int), + "coverage_min": _ConfigKeySpec(int), "update_metrics_baseline": _ConfigKeySpec(bool), "metrics_baseline": _ConfigKeySpec(str), "skip_metrics": _ConfigKeySpec(bool), "skip_dead_code": _ConfigKeySpec(bool), "skip_dependencies": _ConfigKeySpec(bool), + "golden_fixture_paths": _ConfigKeySpec(list, expected_name="list[str]"), "html_out": _ConfigKeySpec(str, allow_none=True), "json_out": _ConfigKeySpec(str, allow_none=True), "md_out": _ConfigKeySpec(str, allow_none=True), @@ -79,6 +89,7 @@ class _ConfigKeySpec: "cache_path", "baseline", "metrics_baseline", + "coverage_xml", "html_out", "json_out", "md_out", @@ -187,7 +198,7 @@ def _validate_config_value(*, key: str, value: object) -> object: return None raise ConfigValidationError( "Invalid value type for tool.codeclone." - f"{key}: expected {spec.expected_type.__name__}" + f"{key}: expected {spec.expected_name or spec.expected_type.__name__}" ) expected_type = spec.expected_type @@ -215,6 +226,8 @@ def _validate_config_value(*, key: str, value: object) -> object: expected_type=str, expected_name="str", ) + if expected_type is list: + return _validated_string_list(key=key, value=value) raise ConfigValidationError(f"Unsupported config key spec for tool.codeclone.{key}") @@ -236,6 +249,21 @@ def _validated_config_instance( ) +def _validated_string_list(*, key: str, value: object) -> tuple[str, ...]: + if not isinstance(value, list): + raise ConfigValidationError( + f"Invalid value type for tool.codeclone.{key}: expected list[str]" + ) + if not all(isinstance(item, str) for item in value): + raise ConfigValidationError( + f"Invalid value type for tool.codeclone.{key}: expected list[str]" + ) + try: + return normalize_golden_fixture_patterns(value) + except GoldenFixturePatternError as exc: + raise ConfigValidationError(str(exc)) from exc + + def _load_toml(path: Path) -> object: if sys.version_info >= (3, 11): import tomllib diff --git a/codeclone/_cli_gating.py b/codeclone/_cli_gating.py index 90de2ca..20ec538 100644 --- a/codeclone/_cli_gating.py +++ b/codeclone/_cli_gating.py @@ -21,6 +21,7 @@ class _GatingArgs(Protocol): fail_on_typing_regression: bool fail_on_docstring_regression: bool fail_on_api_break: bool + fail_on_untested_hotspots: bool fail_complexity: int fail_coupling: int fail_cohesion: int @@ -29,6 +30,7 @@ class _GatingArgs(Protocol): fail_health: int min_typing_coverage: int min_docstring_coverage: int + coverage_min: int fail_on_new: bool fail_threshold: int @@ -90,6 +92,13 @@ def tail(prefix: str) -> str: return "api_breaking_changes", tail( "Public API breaking changes vs metrics baseline: " ) + coverage_detail = _parse_two_part_metric_detail( + trimmed, + prefix="Coverage hotspots detected: ", + right_label="threshold", + ) + if coverage_detail is not None: + return "coverage_hotspots", coverage_detail if trimmed.startswith("Dependency cycles detected: "): return "dependency_cycles", tail("Dependency cycles detected: ").replace( @@ -157,12 +166,18 @@ def policy_context(*, args: _GatingArgs, gate_kind: str) -> str: "fail-on-api-break" if bool(getattr(args, "fail_on_api_break", False)) else None, + "fail-on-untested-hotspots" + if bool(getattr(args, "fail_on_untested_hotspots", False)) + else None, f"min-typing-coverage={getattr(args, 'min_typing_coverage', -1)}" if int(getattr(args, "min_typing_coverage", -1)) >= 0 else None, f"min-docstring-coverage={getattr(args, 'min_docstring_coverage', -1)}" if int(getattr(args, "min_docstring_coverage", -1)) >= 0 else None, + f"coverage-min={getattr(args, 'coverage_min', -1)}" + if bool(getattr(args, "fail_on_untested_hotspots", False)) + else None, ) case "new-clones": parts = ( diff --git a/codeclone/_cli_runtime.py b/codeclone/_cli_runtime.py index f459082..dd6a787 100644 --- a/codeclone/_cli_runtime.py +++ b/codeclone/_cli_runtime.py @@ -26,6 +26,7 @@ class _RuntimeArgs(Protocol): cache_path: str | None + coverage_xml: str | None max_baseline_size_mb: int max_cache_size_mb: int fail_threshold: int @@ -37,8 +38,10 @@ class _RuntimeArgs(Protocol): fail_on_typing_regression: bool fail_on_docstring_regression: bool fail_on_api_break: bool + fail_on_untested_hotspots: bool min_typing_coverage: int min_docstring_coverage: int + coverage_min: int typing_coverage: bool docstring_coverage: bool api_surface: bool @@ -79,6 +82,8 @@ def validate_numeric_args(args: _RuntimeArgs) -> bool: or args.min_typing_coverage > 100 or args.min_docstring_coverage < -1 or args.min_docstring_coverage > 100 + or args.coverage_min < 0 + or args.coverage_min > 100 ) ) @@ -95,9 +100,11 @@ def _metrics_flags_requested(args: _RuntimeArgs) -> bool: or args.fail_on_typing_regression or args.fail_on_docstring_regression or args.fail_on_api_break + or args.fail_on_untested_hotspots or args.min_typing_coverage >= 0 or args.min_docstring_coverage >= 0 or args.update_metrics_baseline + or bool(getattr(args, "coverage_xml", None)) ) @@ -134,7 +141,7 @@ def configure_metrics_mode( args.skip_dead_code = False if args.fail_cycles: args.skip_dependencies = False - if bool(getattr(args, "fail_on_api_break", False)) or args.update_metrics_baseline: + if bool(getattr(args, "fail_on_api_break", False)): args.api_surface = True @@ -180,6 +187,8 @@ def metrics_computed(args: _RuntimeArgs) -> tuple[str, ...]: computed.append("coverage_adoption") if bool(getattr(args, "api_surface", False)): computed.append("api_surface") + if bool(getattr(args, "coverage_xml", None)): + computed.append("coverage_join") return tuple(computed) diff --git a/codeclone/_cli_summary.py b/codeclone/_cli_summary.py index ebc5ff7..5b849a8 100644 --- a/codeclone/_cli_summary.py +++ b/codeclone/_cli_summary.py @@ -39,6 +39,12 @@ class MetricsSnapshot: api_surface_public_symbols: int = 0 api_surface_added: int = 0 api_surface_breaking: int = 0 + coverage_join_status: str = "" + coverage_join_overall_permille: int = 0 + coverage_join_coverage_hotspots: int = 0 + coverage_join_scope_gap_hotspots: int = 0 + coverage_join_threshold_percent: int = 0 + coverage_join_source_label: str = "" @dataclass(frozen=True, slots=True) @@ -68,6 +74,7 @@ def _print_summary( func_clones_count: int, block_clones_count: int, segment_clones_count: int, + suppressed_golden_fixture_groups: int, suppressed_segment_groups: int, new_clones_count: int, ) -> None: @@ -88,6 +95,7 @@ def _print_summary( block=block_clones_count, segment=segment_clones_count, suppressed=suppressed_segment_groups, + fixture_excluded=suppressed_golden_fixture_groups, new=new_clones_count, ) ) @@ -118,6 +126,7 @@ def _print_summary( block=block_clones_count, segment=segment_clones_count, suppressed=suppressed_segment_groups, + fixture_excluded=suppressed_golden_fixture_groups, new=new_clones_count, ) ) @@ -148,6 +157,39 @@ def _print_metrics( overloaded_modules=metrics.overloaded_modules_candidates, ) ) + if ( + metrics.adoption_param_permille is not None + and metrics.adoption_return_permille is not None + and metrics.adoption_docstring_permille is not None + ): + console.print( + ui.fmt_summary_compact_adoption( + param_permille=metrics.adoption_param_permille, + return_permille=metrics.adoption_return_permille, + docstring_permille=metrics.adoption_docstring_permille, + any_annotation_count=metrics.adoption_any_annotation_count, + ) + ) + if metrics.api_surface_enabled: + console.print( + ui.fmt_summary_compact_api_surface( + public_symbols=metrics.api_surface_public_symbols, + modules=metrics.api_surface_modules, + added=metrics.api_surface_added, + breaking=metrics.api_surface_breaking, + ) + ) + if metrics.coverage_join_status: + console.print( + ui.fmt_summary_compact_coverage_join( + status=metrics.coverage_join_status, + overall_permille=metrics.coverage_join_overall_permille, + coverage_hotspots=metrics.coverage_join_coverage_hotspots, + scope_gap_hotspots=metrics.coverage_join_scope_gap_hotspots, + threshold_percent=metrics.coverage_join_threshold_percent, + source_label=metrics.coverage_join_source_label, + ) + ) else: from rich.rule import Rule @@ -196,6 +238,17 @@ def _print_metrics( breaking=metrics.api_surface_breaking, ) ) + if metrics.coverage_join_status: + console.print( + ui.fmt_metrics_coverage_join( + status=metrics.coverage_join_status, + overall_permille=metrics.coverage_join_overall_permille, + coverage_hotspots=metrics.coverage_join_coverage_hotspots, + scope_gap_hotspots=metrics.coverage_join_scope_gap_hotspots, + threshold_percent=metrics.coverage_join_threshold_percent, + source_label=metrics.coverage_join_source_label, + ) + ) console.print( ui.fmt_metrics_overloaded_modules( candidates=metrics.overloaded_modules_candidates, diff --git a/codeclone/_html_badges.py b/codeclone/_html_badges.py index 2ea9ee4..eac9bb0 100644 --- a/codeclone/_html_badges.py +++ b/codeclone/_html_badges.py @@ -34,12 +34,15 @@ __all__ = [ "CHECK_CIRCLE_SVG", + "INFO_CIRCLE_SVG", + "_micro_badges", "_quality_badge_html", "_render_chain_flow", "_short_label", "_source_kind_badge_html", "_stat_card", "_tab_empty", + "_tab_empty_info", ] _EFFORT_CSS: dict[str, str] = { @@ -57,6 +60,27 @@ "" ) +INFO_CIRCLE_SVG = ( + '' + '' + '' + '' + "" +) + + +def _micro_badges(*pairs: tuple[str, object]) -> str: + """Render compact label:value micro-badge pairs for stat card details.""" + return "".join( + f'' + f'{_escape_html(str(value))}' + f'{_escape_html(label)}' + for label, value in pairs + if value is not None and str(value) != "n/a" + ) + def _quality_badge_html(text: str) -> str: """Render a risk / severity / effort value as a styled badge.""" @@ -85,14 +109,47 @@ def _source_kind_badge_html(source_kind: str) -> str: ) -def _tab_empty(message: str) -> str: +def _tab_empty( + message: str, + *, + description: str | None = "Nothing to report - keep up the good work.", +) -> str: + desc_html = ( + f'
{_escape_html(description)}
' + if description + else "" + ) return ( '
' f"{CHECK_CIRCLE_SVG}" f'
{_escape_html(message)}
' - '
' - "Nothing to report - keep up the good work." + f"{desc_html}" "
" + ) + + +def _tab_empty_info( + message: str, + *, + description: str | None = None, + detail_html: str | None = None, +) -> str: + if detail_html: + desc_block = ( + f'
{detail_html}
' + ) + elif description: + desc_block = ( + f'
' + f"{_escape_html(description)}
" + ) + else: + desc_block = "" + return ( + '
' + f"{INFO_CIRCLE_SVG}" + f'
{_escape_html(message)}
' + f"{desc_block}" "
" ) diff --git a/codeclone/_html_css.py b/codeclone/_html_css.py index 6606143..5c3a7d8 100644 --- a/codeclone/_html_css.py +++ b/codeclone/_html_css.py @@ -277,9 +277,9 @@ _INSIGHT = """\ .insight-banner{padding:var(--sp-3) var(--sp-4);border-radius:var(--radius-md); margin-bottom:var(--sp-4);border-left:3px solid var(--border);background:none} -.insight-question{font-size:.72rem;font-weight:500;color:var(--text-muted); - text-transform:uppercase;letter-spacing:.04em;margin-bottom:2px} -.insight-answer{font-size:.82rem;color:var(--text-muted);line-height:1.5} +.insight-question{font-size:.78rem;font-weight:500;color:var(--text-muted); + text-transform:uppercase;letter-spacing:.03em;margin-bottom:2px} +.insight-answer{font-size:.82rem;color:var(--text-secondary);line-height:1.5} .insight-ok{border-left-color:var(--success);background:var(--success-muted)} .insight-warn{border-left-color:var(--warning);background:var(--warning-muted)} @@ -313,7 +313,8 @@ vertical-align:top} .table tr:last-child td{border-bottom:none} .table tr:hover td{background:var(--bg-raised)} -.table .col-name{font-weight:500;color:var(--text-primary)} +.table .col-name{font-weight:500;color:var(--text-primary);max-width:360px;overflow:hidden; + text-overflow:ellipsis;white-space:nowrap} .table .col-file,.table .col-path{color:var(--text-muted);max-width:240px;overflow:hidden; text-overflow:ellipsis;white-space:nowrap} .table .col-number,.table .col-num{font-variant-numeric:tabular-nums;text-align:right;white-space:nowrap} @@ -471,10 +472,10 @@ .overview-kpi-cards{display:grid;grid-template-columns:repeat(4,minmax(0,1fr)); gap:var(--sp-3);min-width:0} .overview-kpi-grid--with-health .meta-item{min-width:0} -.overview-kpi-grid--with-health .meta-item{min-height:108px} +.overview-kpi-grid--with-health .meta-item{min-height:0} .overview-kpi-cards .meta-item{display:grid;grid-template-rows:auto 1fr auto; - align-items:start;padding:var(--sp-3) var(--sp-4);gap:var(--sp-2);min-height:122px} -.overview-kpi-cards .meta-item .meta-label{font-size:.72rem;min-height:18px} + align-items:start;padding:var(--sp-3) var(--sp-4);gap:var(--sp-2);min-height:0} +.overview-kpi-cards .meta-item .meta-label{font-size:.75rem;min-height:18px} .overview-kpi-cards .meta-item .meta-value{display:flex;align-items:center; font-size:1.55rem;line-height:1;padding:var(--sp-1) 0} .overview-kpi-cards .kpi-detail{margin-top:0;gap:4px;align-self:end} @@ -581,6 +582,7 @@ .meta-item .meta-value--warn{color:var(--warning)} .meta-item .meta-value--muted{color:var(--text-muted)} .kpi-detail{display:flex;flex-wrap:wrap;gap:3px;margin-top:2px} +.kpi-detail code{font-size:.78rem} .kpi-micro{display:inline-flex;align-items:center;gap:2px;font-size:.62rem; padding:1px 5px;border-radius:var(--radius-sm);background:var(--bg-raised); white-space:nowrap;line-height:1.3} @@ -597,12 +599,11 @@ color:var(--text-muted);cursor:help;position:relative;border:1.5px solid var(--border); opacity:.5;transition:opacity var(--dur-fast) var(--ease)} .kpi-help:hover{opacity:1} -.kpi-help:hover::after{content:attr(data-tip);position:absolute;top:calc(100% + 6px);left:50%; - transform:translateX(-50%);background:var(--bg-overlay);color:var(--text-primary); +.kpi-tooltip{position:fixed;z-index:9999;pointer-events:none; + background:var(--bg-overlay);color:var(--text-primary); padding:var(--sp-2) var(--sp-3);border-radius:var(--radius-md);font-size:.75rem;font-weight:400; white-space:normal;width:max-content;max-width:240px;line-height:1.4; - box-shadow:var(--shadow-md);z-index:100;pointer-events:none; - border:1px solid var(--border)} + box-shadow:var(--shadow-md);border:1px solid var(--border)} /* Tone variants */ .meta-item.tone-ok{border-left:3px solid var(--success)} @@ -610,8 +611,8 @@ .meta-item.tone-risk{border-left:3px solid var(--error)} /* Clusters */ -.overview-cluster{margin-bottom:var(--sp-4)} -.overview-cluster-header{margin-bottom:var(--sp-2)} +.overview-cluster{margin-bottom:var(--sp-5)} +.overview-cluster-header{margin-bottom:var(--sp-3)} .overview-cluster-copy{font-size:.82rem;color:var(--text-muted);margin-top:2px} .overview-cluster-empty{display:flex;flex-direction:column;align-items:center;gap:var(--sp-2); padding:var(--sp-5);text-align:center;color:var(--text-muted);font-size:.85rem} @@ -639,8 +640,8 @@ .overview-summary-item{background:var(--bg-surface);border:1px solid var(--border); border-radius:var(--radius-lg);padding:var(--sp-4)} .overview-summary-label{display:flex;align-items:center;gap:var(--sp-2); - font-size:.72rem;font-weight:700;text-transform:uppercase; - letter-spacing:.06em;color:var(--text-muted);margin-bottom:var(--sp-3); + font-size:.82rem;font-weight:700;text-transform:none; + letter-spacing:normal;color:var(--text-secondary);margin-bottom:var(--sp-3); padding-bottom:var(--sp-2);border-bottom:1px solid var(--border)} .summary-icon{flex-shrink:0;opacity:.6} .summary-icon--risk{color:var(--warning)} @@ -650,7 +651,7 @@ padding-left:var(--sp-3);position:relative;line-height:1.5} .overview-summary-list li::before{content:"\\2022";position:absolute;left:0;color:var(--text-muted)} .overview-summary-value{font-size:.85rem;color:var(--text-muted)} -/* Compact stat grid used inside overview-summary-item cards (Adoption & API). */ +/* Compact stat grid used inside overview-summary-item cards (Coverage Join). */ .overview-stat-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(84px,1fr)); gap:var(--sp-3);align-items:end} .overview-stat{display:flex;flex-direction:column;gap:2px;min-width:0} @@ -664,13 +665,17 @@ padding:1px 4px;border-radius:var(--radius-sm);background:var(--bg-raised); color:var(--text-secondary)} .overview-stat-row + .kpi-detail{margin-top:var(--sp-2)} -.overview-fact-list{display:flex;flex-direction:column;gap:var(--sp-2);margin-top:var(--sp-3)} +/* Fact-list: compact label ··· value rows inside overview-summary-item cards. */ +.overview-fact-list{display:flex;flex-direction:column;gap:var(--sp-2)} .overview-fact-row{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-3); - font-size:.76rem;border-bottom:1px solid color-mix(in srgb,var(--border) 45%,transparent);padding-bottom:6px} -.overview-fact-row:last-child{border-bottom:none;padding-bottom:0} + font-size:.8rem;padding-bottom:6px} +.overview-fact-row:last-child{padding-bottom:0} .overview-fact-label{color:var(--text-muted)} -.overview-fact-value{color:var(--text-secondary);font-weight:600;font-variant-numeric:tabular-nums; - text-align:right} +.overview-fact-value{display:inline-flex;align-items:baseline;gap:6px; + color:var(--text-primary);font-weight:600;font-variant-numeric:tabular-nums;text-align:right} +.overview-fact-delta{font-size:.68rem;font-weight:400;color:var(--text-muted)} +.overview-fact-value--warn{color:var(--warning)} +.overview-fact-value--good{color:var(--success)} /* Source breakdown bars */ .breakdown-list{display:flex;flex-direction:column;gap:var(--sp-2)} .breakdown-row{display:grid;grid-template-columns:6.5rem 2rem 1fr;align-items:center;gap:var(--sp-2)} @@ -720,19 +725,21 @@ /* Health radar chart */ .health-radar{display:flex;justify-content:center;padding:var(--sp-3) 0} .health-radar svg{width:100%;max-width:520px;height:auto;overflow:visible} -.health-radar text{font-size:9px;font-family:var(--font-sans);fill:var(--text-muted)} +.health-radar text{font-size:10px;font-family:var(--font-sans);fill:var(--text-muted)} .health-radar .radar-score{font-weight:600;font-variant-numeric:tabular-nums;fill:var(--text-secondary)} .health-radar .radar-label--weak{fill:var(--error)} .health-radar .radar-label--weak .radar-score{fill:var(--error)} +.health-radar-legend{font-size:.75rem;color:var(--text-muted);text-align:center; + margin-top:var(--sp-2);max-width:520px;margin-left:auto;margin-right:auto} /* Findings by family bars */ .families-list{display:flex;flex-direction:column;gap:var(--sp-2)} .families-row{display:grid;grid-template-columns:5.5rem 2rem 1fr auto;align-items:center;gap:var(--sp-2)} -.families-row--muted{opacity:.55} +.families-row--muted{opacity:.65} .families-label{font-size:.75rem;font-weight:500;color:var(--text-secondary);text-align:right} .families-count{font-size:.8rem;font-weight:600;font-variant-numeric:tabular-nums; color:var(--text-primary);text-align:right} .breakdown-bar-track{display:flex} -.breakdown-bar-fill--baselined{opacity:.35} +.breakdown-bar-fill--baselined{opacity:.5} .breakdown-bar-fill--new{border-radius:0 3px 3px 0} .families-delta{font-size:.65rem;font-weight:600;font-variant-numeric:tabular-nums;white-space:nowrap} .families-delta--ok{color:var(--success)} @@ -744,12 +751,12 @@ # --------------------------------------------------------------------------- _DEPENDENCIES = """\ -.dep-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); +.stat-cards,.dep-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:var(--sp-2);margin-bottom:var(--sp-4)} -.dep-stats .meta-item{display:grid;grid-template-rows:auto 1fr auto;min-height:100px} -.dep-stats .meta-item .meta-label{font-size:.72rem;min-height:18px} -.dep-stats .meta-item .meta-value{display:flex;align-items:center} -.dep-stats .kpi-detail{margin-top:0;align-self:end} +.stat-cards .meta-item,.dep-stats .meta-item{display:grid;grid-template-rows:auto 1fr auto;min-height:100px} +.stat-cards .meta-item .meta-label,.dep-stats .meta-item .meta-label{font-size:.72rem;min-height:18px} +.stat-cards .meta-item .meta-value,.dep-stats .meta-item .meta-value{display:flex;align-items:center} +.stat-cards .kpi-detail,.dep-stats .kpi-detail{margin-top:0;align-self:end} .dep-graph-wrap{overflow:hidden;margin-bottom:var(--sp-4);border:1px solid var(--border); border-radius:var(--radius-lg);background:var(--bg-surface);padding:var(--sp-4)} .dep-graph-svg{width:100%;height:auto;max-height:520px} @@ -1033,6 +1040,9 @@ .tab-empty-icon{color:var(--text-muted);opacity:.4;margin-bottom:var(--sp-3);width:48px;height:48px} .tab-empty-title{font-size:1rem;font-weight:600;color:var(--text-primary);margin-bottom:var(--sp-1)} .tab-empty-desc{font-size:.85rem;color:var(--text-muted);max-width:320px} +.tab-empty-desc-detail{text-align:left;max-width:520px;font-size:.8rem;word-break:break-word} +.tab-empty-reason{display:block;margin-top:var(--sp-1);font-size:.75rem;color:var(--text-muted); + opacity:.7;word-break:break-all;font-family:var(--font-mono, monospace)} """ # --------------------------------------------------------------------------- diff --git a/codeclone/_html_js.py b/codeclone/_html_js.py index 0d07299..968d009 100644 --- a/codeclone/_html_js.py +++ b/codeclone/_html_js.py @@ -670,6 +670,42 @@ })(); """ +# --------------------------------------------------------------------------- +# Tooltips (fixed-position, escapes overflow containers) +# --------------------------------------------------------------------------- + +_TOOLTIPS = """\ +(function initTooltips(){ + let tip=null; + function show(e){ + const el=e.target; + const text=el.getAttribute('data-tip'); + if(!text)return; + tip=document.createElement('div'); + tip.className='kpi-tooltip'; + tip.textContent=text; + document.body.appendChild(tip); + const r=el.getBoundingClientRect(); + const tw=tip.offsetWidth; + const th=tip.offsetHeight; + let left=r.left+r.width/2-tw/2; + let top=r.bottom+6; + if(left<4)left=4; + if(left+tw>window.innerWidth-4)left=window.innerWidth-tw-4; + if(top+th>window.innerHeight-4){top=r.top-th-6} + tip.style.left=left+'px'; + tip.style.top=top+'px'; + } + function hide(){if(tip){tip.remove();tip=null}} + document.addEventListener('mouseenter',function(e){ + if(e.target.matches('.kpi-help[data-tip]'))show(e); + },true); + document.addEventListener('mouseleave',function(e){ + if(e.target.matches('.kpi-help[data-tip]'))hide(); + },true); +})(); +""" + # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- @@ -692,6 +728,7 @@ _SCOPE_COUNTERS, _LAZY_HIGHLIGHT, _IDE_LINKS, + _TOOLTIPS, ) diff --git a/codeclone/_html_report/_assemble.py b/codeclone/_html_report/_assemble.py index 8884d7f..7d598c3 100644 --- a/codeclone/_html_report/_assemble.py +++ b/codeclone/_html_report/_assemble.py @@ -107,6 +107,15 @@ def build_html_report( structural_count = len( tuple(normalize_structural_findings(ctx.structural_findings)) ) + coverage_join_summary = _as_mapping( + _as_mapping(ctx.metrics_map.get("coverage_join")).get("summary") + ) + coverage_review_items = ( + _as_int(coverage_join_summary.get("coverage_hotspots")) + + _as_int(coverage_join_summary.get("scope_gap_hotspots")) + if str(coverage_join_summary.get("status", "")).strip() == "ok" + else 0 + ) quality_issues = ( _as_int(_as_mapping(ctx.complexity_map.get("summary")).get("high_risk")) + _as_int(_as_mapping(ctx.coupling_map.get("summary")).get("high_risk")) @@ -114,6 +123,7 @@ def build_html_report( + _as_int( _as_mapping(ctx.overloaded_modules_map.get("summary")).get("candidates") ) + + coverage_review_items ) def _tab_badge(count: int) -> str: diff --git a/codeclone/_html_report/_components.py b/codeclone/_html_report/_components.py index 2056552..ca583d8 100644 --- a/codeclone/_html_report/_components.py +++ b/codeclone/_html_report/_components.py @@ -62,6 +62,7 @@ def overview_cluster_header(title: str, subtitle: str | None = None) -> str: "health profile": ("health-profile", "summary-icon summary-icon--info"), "adoption coverage": ("coverage-adoption", "summary-icon summary-icon--info"), "public api surface": ("api-surface", "summary-icon summary-icon--info"), + "coverage join": ("quality", "summary-icon summary-icon--info"), } diff --git a/codeclone/_html_report/_glossary.py b/codeclone/_html_report/_glossary.py index 70b7428..e48d4f0 100644 --- a/codeclone/_html_report/_glossary.py +++ b/codeclone/_html_report/_glossary.py @@ -48,6 +48,47 @@ "edges": "Total number of import relationships between modules", "max depth": "Longest chain of transitive imports", "cycles": "Number of circular import dependencies detected", + # Complexity stat cards + "high-risk functions": ( + "Functions with cyclomatic complexity above the high-risk threshold" + ), + "max cc": "Highest cyclomatic complexity value among all analyzed functions", + "avg cc": "Average cyclomatic complexity across all analyzed functions", + "deep nesting": ( + "Functions with nesting depth exceeding recommended threshold (> 4)" + ), + # Coupling stat cards + "high-coupling classes": "Classes with CBO above the high-risk threshold", + "max cbo": "Highest Coupling Between Objects value among all classes", + "avg cbo": "Average CBO across all analyzed classes", + "medium risk": "Items at medium risk level — worth reviewing but not critical", + # Cohesion stat cards + "low-cohesion classes": ( + "Classes with LCOM4 > 1, indicating multiple responsibilities" + ), + "max lcom4": "Highest Lack of Cohesion value among all classes", + "high risk": "Items at high risk level requiring attention", + # Overloaded module stat cards + "overloaded": ( + "Modules exceeding acceptable thresholds for size, complexity, or coupling" + ), + "critical": "Items with critical status requiring immediate attention", + "max score": "Highest overload score among all modules", + "avg loc": "Average lines of code per module", + # Dead code stat cards + "candidates": "Total dead code candidates detected by static analysis", + "high confidence": "Dead code items detected with high or critical confidence", + "suppressed": "Dead code candidates excluded by suppression rules", + "hit rate": "Percentage of high-confidence items among all candidates", + # Clone stat cards + "clone groups": "Distinct duplication patterns, each containing 2+ code fragments", + "instances": "Total duplicated code fragments across all groups", + "new groups": "Clone groups not present in the previous baseline", + "high spread": "Clone groups spanning multiple files", + # Suggestion stat cards + "total suggestions": "Total actionable improvement suggestions generated", + "warning": "Suggestions with warning severity worth reviewing", + "easy wins": "Actionable suggestions with low estimated effort", } diff --git a/codeclone/_html_report/_sections/_clones.py b/codeclone/_html_report/_sections/_clones.py index f60e363..480bf46 100644 --- a/codeclone/_html_report/_sections/_clones.py +++ b/codeclone/_html_report/_sections/_clones.py @@ -28,6 +28,7 @@ from ...report.suggestions import classify_clone_type from .._components import Tone, insight_block from .._icons import ICONS +from .._tables import render_rows_table from .._tabs import render_split_tabs if TYPE_CHECKING: @@ -35,8 +36,15 @@ from .._context import ReportContext _as_int = _coerce.as_int +_as_mapping = _coerce.as_mapping +_as_sequence = _coerce.as_sequence _HEX_SET = frozenset("0123456789abcdefABCDEF") +_SUPPRESSED_KIND_LABELS = { + "function": "Function", + "block": "Block", + "segment": "Segment", +} def _looks_like_hash(text: str) -> bool: @@ -169,6 +177,77 @@ def _render_group_explanation(meta: Mapping[str, object]) -> str: return f'
{"".join(parts)}{note}
' +def _flatten_suppressed_clone_groups( + ctx: ReportContext, +) -> tuple[Mapping[str, object], ...]: + findings = _as_mapping(ctx.report_document.get("findings")) + groups = _as_mapping(findings.get("groups")) + clones = _as_mapping(groups.get("clones")) + suppressed = _as_mapping(clones.get("suppressed")) + flattened: list[Mapping[str, object]] = [] + for bucket_key in ("functions", "blocks", "segments"): + for group in _as_sequence(suppressed.get(bucket_key)): + group_mapping = _as_mapping(group) + if group_mapping: + flattened.append(group_mapping) + return tuple(flattened) + + +def _suppressed_group_label( + group: Mapping[str, object], + ctx: ReportContext, +) -> tuple[str, str]: + items = _as_sequence(group.get("items")) + first_item = _as_mapping(items[0]) if items else {} + filepath = str(first_item.get("filepath", "")) + qualname = str(first_item.get("qualname", "")) + label = ctx.bare_qualname(qualname, filepath) or ctx.relative_path(filepath) + if not label: + label = str(group.get("id", "")) + return label, filepath + + +def _render_suppressed_clone_panel( + ctx: ReportContext, + groups: Sequence[Mapping[str, object]], +) -> str: + rows: list[tuple[str, str, str, str, str, str, str]] = [] + for group in groups[:200]: + label, filepath = _suppressed_group_label(group, ctx) + matched_patterns = ", ".join( + str(pattern).strip() + for pattern in _as_sequence(group.get("matched_patterns")) + if str(pattern).strip() + ) + suppression_rule = str(group.get("suppression_rule", "")).strip() + suppression_source = str(group.get("suppression_source", "")).strip() + rule_text = suppression_rule + if suppression_source: + rule_text = ( + f"{rule_text}@{suppression_source}" if rule_text else suppression_source + ) + rows.append( + ( + _SUPPRESSED_KIND_LABELS.get( + str(group.get("clone_kind", "")).strip().lower(), + "Clone", + ), + label, + filepath, + str(group.get("clone_type", "")), + str(group.get("count", "")), + rule_text, + matched_patterns or "-", + ) + ) + return render_rows_table( + headers=("Kind", "Group", "File", "Type", "Occurrences", "Rule", "Pattern"), + rows=rows, + empty_message="No suppressed clone groups.", + ctx=ctx, + ) + + def _render_section_toolbar( section_id: str, section_title: str, @@ -554,8 +633,11 @@ def render_clones_panel(ctx: ReportContext) -> tuple[str, bool, int, int]: Returns ``(panel_html, novelty_enabled, total_new, total_known)``. """ + suppressed_clone_groups = _flatten_suppressed_clone_groups(ctx) + suppressed_total = len(suppressed_clone_groups) + # Empty state - if not ctx.has_any_clones: + if not ctx.has_any_clones and suppressed_total == 0: empty = ( '
' f'
{ICONS["check"]}
' @@ -630,6 +712,15 @@ def render_clones_panel(ctx: ReportContext) -> tuple[str, bool, int, int]: sub_tabs.append( ("segments", "Segments", len(ctx.segment_sorted), segment_section) ) + if suppressed_total > 0: + sub_tabs.append( + ( + "suppressed", + "Suppressed", + suppressed_total, + _render_suppressed_clone_panel(ctx, suppressed_clone_groups), + ) + ) panel = global_novelty_html + render_split_tabs( group_id="clones", tabs=sub_tabs, emit_clone_counters=True @@ -643,6 +734,11 @@ def render_clones_panel(ctx: ReportContext) -> tuple[str, bool, int, int]: ) else: clones_answer = f"{ctx.clone_groups_total} groups and {ctx.clone_instances_total} instances." + if suppressed_total > 0: + clones_answer += ( + f" {suppressed_total} suppressed golden-fixture groups are excluded " + "from active review." + ) clones_tone: Tone = "warn" if ctx.clone_groups_total > 0 else "ok" panel = ( insight_block( diff --git a/codeclone/_html_report/_sections/_coupling.py b/codeclone/_html_report/_sections/_coupling.py index 8b43683..6be91ac 100644 --- a/codeclone/_html_report/_sections/_coupling.py +++ b/codeclone/_html_report/_sections/_coupling.py @@ -15,6 +15,11 @@ from .._components import Tone, insight_block from .._tables import render_rows_table from .._tabs import render_split_tabs +from ._coverage_join import ( + coverage_join_quality_count, + coverage_join_quality_summary, + render_coverage_join_panel, +) if TYPE_CHECKING: from collections.abc import Mapping @@ -54,11 +59,16 @@ def render_quality_panel(ctx: ReportContext) -> str: cohesion_summary = _as_mapping(ctx.cohesion_map.get("summary")) complexity_summary = _as_mapping(ctx.complexity_map.get("summary")) overloaded_modules_summary = _as_mapping(ctx.overloaded_modules_map.get("summary")) + coverage_join_summary = coverage_join_quality_summary(ctx) coupling_high_risk = _as_int(coupling_summary.get("high_risk")) cohesion_low = _as_int(cohesion_summary.get("low_cohesion")) complexity_high_risk = _as_int(complexity_summary.get("high_risk")) overloaded_module_candidates = _as_int(overloaded_modules_summary.get("candidates")) + coverage_review_items = coverage_join_quality_count(ctx) + coverage_hotspots = _as_int(coverage_join_summary.get("coverage_hotspots")) + coverage_scope_gaps = _as_int(coverage_join_summary.get("scope_gap_hotspots")) + coverage_join_status = str(coverage_join_summary.get("status", "")).strip() cc_max = _as_int(complexity_summary.get("max")) # Insight @@ -77,11 +87,24 @@ def render_quality_panel(ctx: ReportContext) -> str: f"max CBO {coupling_summary.get('max', 'n/a')}; " f"max LCOM4 {cohesion_summary.get('max', 'n/a')}." ) + if coverage_join_summary: + if coverage_join_status == "ok": + answer += ( + f" Coverage hotspots: {coverage_hotspots}; " + f"scope gaps: {coverage_scope_gaps}." + ) + else: + answer += " Coverage join unavailable." if overloaded_module_candidates > 0 or ( coupling_high_risk > 0 and cohesion_low > 0 ): tone = "risk" - elif coupling_high_risk > 0 or cohesion_low > 0 or complexity_high_risk > 0: + elif ( + coupling_high_risk > 0 + or cohesion_low > 0 + or complexity_high_risk > 0 + or coverage_review_items > 0 + ): tone = "warn" else: tone = "ok" @@ -197,6 +220,16 @@ def render_quality_panel(ctx: ReportContext) -> str: gm_panel, ), ] + coverage_join_panel = render_coverage_join_panel(ctx) + if coverage_join_panel: + sub_tabs.append( + ( + "coverage-join", + "Coverage Join", + coverage_review_items, + coverage_join_panel, + ) + ) return insight_block( question="Are there quality hotspots in the codebase?", diff --git a/codeclone/_html_report/_sections/_coverage_join.py b/codeclone/_html_report/_sections/_coverage_join.py new file mode 100644 index 0000000..d6c65f2 --- /dev/null +++ b/codeclone/_html_report/_sections/_coverage_join.py @@ -0,0 +1,254 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Den Rozhnovskiy + +"""Coverage Join HTML helpers for Quality tab rendering.""" + +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING + +from ... import _coerce +from ..._html_badges import _stat_card, _tab_empty_info +from ..._html_escape import _escape_html +from .._glossary import glossary_tip +from .._tables import render_rows_table + +if TYPE_CHECKING: + from collections.abc import Mapping + + from .._context import ReportContext + +_as_int = _coerce.as_int +_as_mapping = _coerce.as_mapping +_as_sequence = _coerce.as_sequence + + +def coverage_join_quality_count(ctx: ReportContext) -> int: + coverage_summary = _coverage_join_summary(ctx) + if str(coverage_summary.get("status", "")).strip() != "ok": + return 0 + return _as_int(coverage_summary.get("coverage_hotspots")) + _as_int( + coverage_summary.get("scope_gap_hotspots") + ) + + +def coverage_join_quality_summary(ctx: ReportContext) -> dict[str, object]: + return dict(_coverage_join_summary(ctx)) + + +def render_coverage_join_panel(ctx: ReportContext) -> str: + metrics_map = _as_mapping(getattr(ctx, "metrics_map", {})) + coverage_join = _as_mapping(metrics_map.get("coverage_join")) + coverage_summary = _as_mapping(coverage_join.get("summary")) + if not coverage_summary: + return "" + + status = str(coverage_summary.get("status", "")).strip() + if status != "ok": + source = _source_label(str(coverage_summary.get("source", "")).strip()) + invalid_reason_val = coverage_summary.get("invalid_reason") + invalid_reason = ( + invalid_reason_val.strip() if isinstance(invalid_reason_val, str) else "" + ) + detail_parts: list[str] = [] + if source: + detail_parts.append(f"Source: {_escape_html(source)}") + if invalid_reason: + detail_parts.append( + f'{_escape_html(invalid_reason)}' + ) + return _tab_empty_info( + "Coverage Join is unavailable for this run.", + detail_html="
".join(detail_parts) if detail_parts else None, + ) + + cards = [ + _status_card(coverage_summary), + _overall_coverage_card(coverage_summary), + _coverage_hotspots_card(coverage_summary), + _scope_gaps_card(coverage_summary), + _measured_units_card(coverage_summary), + ] + + return ( + f'
{"".join(cards)}
' + + '

Coverage review items

' + + render_rows_table( + headers=("Function", "Location", "CC", "Status", "Coverage", "Risk"), + rows=_coverage_join_table_rows(ctx, coverage_join), + empty_message=_coverage_join_empty_message(), + empty_description=_coverage_join_empty_description(), + raw_html_headers=("Location",), + ctx=ctx, + ) + ) + + +def _coverage_join_summary(ctx: ReportContext) -> Mapping[str, object]: + metrics_map = _as_mapping(getattr(ctx, "metrics_map", {})) + coverage_join = _as_mapping(metrics_map.get("coverage_join")) + return _as_mapping(coverage_join.get("summary")) + + +def _status_card(coverage_summary: Mapping[str, object]) -> str: + source = str(coverage_summary.get("source", "")).strip() + return _stat_card( + "Status", + "Joined", + detail=_micro_badges(("source", _source_label(source))) if source else "", + value_tone="good", + css_class="meta-item", + glossary_tip_fn=glossary_tip, + ) + + +def _overall_coverage_card(coverage_summary: Mapping[str, object]) -> str: + review_items = _as_int(coverage_summary.get("coverage_hotspots")) + _as_int( + coverage_summary.get("scope_gap_hotspots") + ) + return _stat_card( + "Overall coverage", + _format_permille_pct(coverage_summary.get("overall_permille")), + detail=_micro_badges( + ("covered", _as_int(coverage_summary.get("overall_covered_lines"))), + ("executable", _as_int(coverage_summary.get("overall_executable_lines"))), + ), + value_tone="warn" if review_items > 0 else "good", + css_class="meta-item", + glossary_tip_fn=glossary_tip, + ) + + +def _coverage_hotspots_card(coverage_summary: Mapping[str, object]) -> str: + hotspots = _as_int(coverage_summary.get("coverage_hotspots")) + threshold = _as_int(coverage_summary.get("hotspot_threshold_percent")) + return _stat_card( + "Coverage hotspots", + hotspots, + detail=_micro_badges(("threshold", f"< {threshold}%")), + value_tone="bad" if hotspots > 0 else "good", + css_class="meta-item", + glossary_tip_fn=glossary_tip, + ) + + +def _scope_gaps_card(coverage_summary: Mapping[str, object]) -> str: + scope_gaps = _as_int(coverage_summary.get("scope_gap_hotspots")) + return _stat_card( + "Scope gaps", + scope_gaps, + detail=_micro_badges( + ( + "not mapped", + _as_int(coverage_summary.get("missing_from_report_units")), + ), + ), + value_tone="warn" if scope_gaps > 0 else "good", + css_class="meta-item", + glossary_tip_fn=glossary_tip, + ) + + +def _measured_units_card(coverage_summary: Mapping[str, object]) -> str: + return _stat_card( + "Measured units", + _as_int(coverage_summary.get("measured_units")), + detail=_micro_badges(("units", _as_int(coverage_summary.get("units")))), + css_class="meta-item", + glossary_tip_fn=glossary_tip, + ) + + +def _coverage_join_table_rows( + ctx: ReportContext, + coverage_family: Mapping[str, object], +) -> list[tuple[str, str, str, str, str, str]]: + review_items = [ + _as_mapping(item) + for item in _as_sequence(coverage_family.get("items")) + if bool(_as_mapping(item).get("coverage_review_item")) + or bool(_as_mapping(item).get("coverage_hotspot")) + or bool(_as_mapping(item).get("scope_gap_hotspot")) + ] + return [ + ( + str(item.get("qualname", "")).strip() or "(unknown)", + _location_cell_html(ctx, item), + str(_as_int(item.get("cyclomatic_complexity"))), + _status_cell_label(item), + _coverage_cell_label(item), + str(item.get("risk", "low")).strip() or "low", + ) + for item in review_items[:50] + ] + + +def _coverage_join_empty_message() -> str: + return "No medium/high-risk functions need joined-coverage follow-up." + + +def _coverage_join_empty_description() -> str: + return ( + "No risky functions were below threshold or missing from the supplied " + "coverage.xml." + ) + + +def _location_cell_html(ctx: ReportContext, item: Mapping[str, object]) -> str: + relative_path = str(item.get("relative_path", "")).strip() + start_line = _as_int(item.get("start_line")) + end_line = _as_int(item.get("end_line")) + line_label = ( + f"{relative_path}:{start_line}" + if start_line > 0 + else (relative_path or "(unknown)") + ) + if end_line > start_line > 0: + line_label = f"{relative_path}:{start_line}-{end_line}" + file_target = ( + f"{ctx.scan_root.rstrip('/')}/{relative_path}" + if ctx.scan_root and relative_path + else relative_path + ) + return ( + f'' + f"{_escape_html(line_label)}" + ) + + +def _status_cell_label(item: Mapping[str, object]) -> str: + if bool(item.get("scope_gap_hotspot")): + return "not in coverage.xml" + if bool(item.get("coverage_hotspot")): + return "below threshold" + return str(item.get("coverage_status", "")).replace("_", " ").strip() or "n/a" + + +def _coverage_cell_label(item: Mapping[str, object]) -> str: + if bool(item.get("scope_gap_hotspot")): + return "n/a" + return _format_permille_pct(item.get("coverage_permille")) + + +def _micro_badges(*pairs: tuple[str, object]) -> str: + return "".join( + f'' + f'{_escape_html(str(value))}' + f'{_escape_html(label)}' + for label, value in pairs + if value is not None and str(value) != "n/a" + ) + + +def _format_permille_pct(value: object) -> str: + return f"{_as_int(value) / 10.0:.1f}%" + + +def _source_label(source: str) -> str: + name = Path(source).name + return name or source diff --git a/codeclone/_html_report/_sections/_overview.py b/codeclone/_html_report/_sections/_overview.py index fe061ab..b0483e1 100644 --- a/codeclone/_html_report/_sections/_overview.py +++ b/codeclone/_html_report/_sections/_overview.py @@ -59,6 +59,7 @@ "cohesion": "cohesion", "coupling": "coupling", "dead_code": "dead code", + "coverage": "coverage", "dependency": "dependency", } @@ -433,104 +434,98 @@ def _format_permille_delta(value: object) -> str: return f"{sign}{delta / 10.0:.1f}pt" -def _overview_stat(value: str, label: str) -> str: - return ( - '
' - f'
{_escape_html(value)}
' - f'
{_escape_html(label)}
' - "
" +def _fact_row( + label: str, + value: str, + *, + delta: str | None = None, + value_cls: str = "", +) -> str: + cls = f" overview-fact-value--{value_cls}" if value_cls else "" + delta_html = ( + f' {_escape_html(delta)}' + if delta + else "" ) - - -def _overview_stat_row(*stats: tuple[str, str]) -> str: return ( - '
' - + "".join(_overview_stat(value, label) for value, label in stats) - + "
" + '
' + f'{_escape_html(label)}' + f'' + f"{_escape_html(value)}{delta_html}" + "
" ) def _adoption_card_html(adoption_summary: Mapping[str, object]) -> str: - params_pct = _format_permille_pct(adoption_summary.get("param_permille")) - returns_pct = _format_permille_pct(adoption_summary.get("return_permille")) - docs_pct = _format_permille_pct(adoption_summary.get("docstring_permille")) - stats_html = _overview_stat_row( - (params_pct, "params"), - (returns_pct, "returns"), - (docs_pct, "docstrings"), - ) + has_baseline = bool(adoption_summary.get("baseline_diff_available")) - deltas_html = "" - if bool(adoption_summary.get("baseline_diff_available")): - deltas_html = _mb( - ( - "\u0394 params", - _format_permille_delta(adoption_summary.get("param_delta")), - ), - ( - "\u0394 returns", - _format_permille_delta(adoption_summary.get("return_delta")), - ), - ( - "\u0394 docs", - _format_permille_delta(adoption_summary.get("docstring_delta")), - ), - ) - if deltas_html: - deltas_html = f'
{deltas_html}
' + def _delta_or_none(key: str) -> str | None: + if not has_baseline: + return None + return _format_permille_delta(adoption_summary.get(key)) + + rows = [ + _fact_row( + "Param annotations", + _format_permille_pct(adoption_summary.get("param_permille")), + delta=_delta_or_none("param_delta"), + ), + _fact_row( + "Return annotations", + _format_permille_pct(adoption_summary.get("return_permille")), + delta=_delta_or_none("return_delta"), + ), + _fact_row( + "Docstrings", + _format_permille_pct(adoption_summary.get("docstring_permille")), + delta=_delta_or_none("docstring_delta"), + ), + ] any_count = _as_int(adoption_summary.get("typing_any_count")) - if any_count > 0: - noun = "symbol" if any_count == 1 else "symbols" - caption_html = ( - '
' - f"{_format_count(any_count)} {noun} typed as Any" - "
" - ) - else: - caption_html = ( - '
' - "No symbols fall back to Any." - "
" + rows.append( + _fact_row( + "Typed as Any", + _format_count(any_count), + value_cls="good" if any_count == 0 else "warn", ) + ) - return stats_html + deltas_html + caption_html + return '
' + "".join(rows) + "
" def _api_card_html(api_summary: Mapping[str, object]) -> str: if not bool(api_summary.get("enabled")): return ( '
Disabled in this run.
' - '
' - "Enable with --api-surface to track public symbols." - "
" + '
' + + _fact_row("Enable via", "--api-surface") + + "
" ) symbols = _as_int(api_summary.get("public_symbols")) modules = _as_int(api_summary.get("modules")) - stats_html = _overview_stat_row( - (_format_count(symbols), "public symbols"), - (_format_count(modules), "modules"), - ) + rows = [ + _fact_row("Public symbols", _format_count(symbols)), + _fact_row("Modules", _format_count(modules)), + ] - chips_html = "" if bool(api_summary.get("baseline_diff_available")): breaking = _as_int(api_summary.get("breaking")) added = _as_int(api_summary.get("added")) - chips_html = _mb(("breaking", breaking), ("added", added)) - if chips_html: - chips_html = f'
{chips_html}
' + rows.append( + _fact_row( + "Breaking changes", + _format_count(breaking), + value_cls="warn" if breaking > 0 else "good", + ) + ) + rows.append(_fact_row("Added symbols", _format_count(added))) if bool(api_summary.get("strict_types")): - caption_html = ( - '
' - "Strict type check enabled for the public surface." - "
" - ) - else: - caption_html = "" + rows.append(_fact_row("Strict mode", "enabled", value_cls="good")) - return stats_html + chips_html + caption_html + return '
' + "".join(rows) + "
" def _adoption_and_api_section(ctx: ReportContext) -> str: @@ -1055,6 +1050,12 @@ def _analytics_section(ctx: ReportContext) -> str: return "" radar_html = _health_radar_svg(dimensions) + radar_legend = ( + '
' + "Higher values indicate better code health." + " Red labels highlight dimensions below 60." + "
" + ) return ( '
' @@ -1063,6 +1064,8 @@ def _analytics_section(ctx: ReportContext) -> str: "Dimension scores across all quality axes.", ) + '
' - + overview_summary_item_html(label="Health profile", body_html=radar_html) + + overview_summary_item_html( + label="Health profile", body_html=radar_html + radar_legend + ) + "
" ) diff --git a/codeclone/_html_report/_tables.py b/codeclone/_html_report/_tables.py index d1b6331..7f633f2 100644 --- a/codeclone/_html_report/_tables.py +++ b/codeclone/_html_report/_tables.py @@ -72,12 +72,13 @@ def render_rows_table( headers: Sequence[str], rows: Sequence[Sequence[str]], empty_message: str, + empty_description: str | None = "Nothing to report - keep up the good work.", raw_html_headers: Collection[str] = (), ctx: ReportContext | None = None, ) -> str: """Render a data table with badges, tooltips, and col sizing.""" if not rows: - return _tab_empty(empty_message) + return _tab_empty(empty_message, description=empty_description) lower_headers = [h.lower() for h in headers] raw_html_set = {h.lower() for h in raw_html_headers} diff --git a/codeclone/cache.py b/codeclone/cache.py index f7314e1..b078d8b 100644 --- a/codeclone/cache.py +++ b/codeclone/cache.py @@ -1407,18 +1407,15 @@ def _canonicalize_cache_entry(entry: CacheEntry) -> CacheEntry: kind=symbol["kind"], start_line=symbol["start_line"], end_line=symbol["end_line"], - params=sorted( - [ - ApiParamSpecDict( - name=param["name"], - kind=param["kind"], - has_default=param["has_default"], - annotation_hash=param["annotation_hash"], - ) - for param in symbol.get("params", []) - ], - key=lambda item: (item["kind"], item["name"]), - ), + params=[ + ApiParamSpecDict( + name=param["name"], + kind=param["kind"], + has_default=param["has_default"], + annotation_hash=param["annotation_hash"], + ) + for param in symbol.get("params", []) + ], returns_hash=symbol.get("returns_hash", ""), exported_via=symbol.get("exported_via", "name"), ) diff --git a/codeclone/cli.py b/codeclone/cli.py index 13dd206..ba81092 100644 --- a/codeclone/cli.py +++ b/codeclone/cli.py @@ -490,6 +490,8 @@ def report( new_block: set[str], html_builder: Callable[..., str] | None = None, metrics_diff: MetricsDiff | None = None, + coverage_adoption_diff_available: bool = False, + api_surface_diff_available: bool = False, include_report_document: bool = False, ) -> ReportArtifacts: return cast( @@ -504,6 +506,8 @@ def report( new_block=new_block, html_builder=html_builder, metrics_diff=metrics_diff, + coverage_adoption_diff_available=coverage_adoption_diff_available, + api_surface_diff_available=api_surface_diff_available, include_report_document=include_report_document, ), ) @@ -955,6 +959,14 @@ def _require_rich_console( except CacheError as exc: console.print(ui.fmt_cache_save_failed(exc)) + coverage_join = getattr(analysis_result, "coverage_join", None) + if ( + coverage_join is not None + and coverage_join.status != "ok" + and coverage_join.invalid_reason + ): + console.print(ui.fmt_coverage_join_ignored(coverage_join.invalid_reason)) + return discovery_result, processing_result, analysis_result @@ -1015,6 +1027,24 @@ def _enforce_gating( ) sys.exit(metrics_baseline_failure_code) + if bool(getattr(args, "fail_on_untested_hotspots", False)): + if analysis.coverage_join is None: + console.print( + ui.fmt_contract_error( + "--fail-on-untested-hotspots requires --coverage." + ) + ) + sys.exit(ExitCode.CONTRACT_ERROR) + if analysis.coverage_join.status != "ok": + detail = analysis.coverage_join.invalid_reason or "invalid coverage input" + console.print( + ui.fmt_contract_error( + "Coverage gating requires a valid Cobertura XML input.\n" + f"Reason: {detail}" + ) + ) + sys.exit(ExitCode.CONTRACT_ERROR) + gate_result = gate( boot=boot, analysis=analysis, @@ -1443,6 +1473,19 @@ def _prepare_run_inputs() -> tuple[ metrics_diff = metrics_baseline_state.baseline.diff( analysis_result.project_metrics ) + coverage_adoption_diff_available = bool( + metrics_baseline_state.trusted_for_diff + and getattr( + metrics_baseline_state.baseline, + "has_coverage_adoption_snapshot", + False, + ) + ) + api_surface_diff_available = bool( + metrics_baseline_state.trusted_for_diff + and getattr(metrics_baseline_state.baseline, "api_surface_snapshot", None) + is not None + ) _print_summary( console=cast("_PrinterLike", console), @@ -1470,6 +1513,9 @@ def _prepare_run_inputs() -> tuple[ func_clones_count=analysis_result.func_clones_count, block_clones_count=analysis_result.block_clones_count, segment_clones_count=analysis_result.segment_clones_count, + suppressed_golden_fixture_groups=len( + getattr(analysis_result, "suppressed_clone_groups", ()) + ), suppressed_segment_groups=analysis_result.suppressed_segment_groups, new_clones_count=new_clones_count, ) @@ -1486,12 +1532,11 @@ def _prepare_run_inputs() -> tuple[ api_surface_summary = _as_mapping( _as_mapping(metrics_payload_map.get("api_surface")).get("summary") ) - api_surface_diff_available = bool( - metrics_baseline_state.trusted_for_diff - and getattr(metrics_baseline_state.baseline, "api_surface_snapshot", None) - is not None + coverage_join_summary = _as_mapping( + _as_mapping(metrics_payload_map.get("coverage_join")).get("summary") ) overloaded_modules_summary_map = _as_mapping(overloaded_modules_summary) + coverage_join_source = str(coverage_join_summary.get("source", "")).strip() _print_metrics( console=cast("_PrinterLike", console), quiet=args.quiet, @@ -1553,6 +1598,24 @@ def _prepare_run_inputs() -> tuple[ if metrics_diff is not None and api_surface_diff_available else 0 ), + coverage_join_status=str( + coverage_join_summary.get("status", "") + ).strip(), + coverage_join_overall_permille=_as_int( + coverage_join_summary.get("overall_permille") + ), + coverage_join_coverage_hotspots=_as_int( + coverage_join_summary.get("coverage_hotspots") + ), + coverage_join_scope_gap_hotspots=_as_int( + coverage_join_summary.get("scope_gap_hotspots") + ), + coverage_join_threshold_percent=_as_int( + coverage_join_summary.get("hotspot_threshold_percent") + ), + coverage_join_source_label=( + Path(coverage_join_source).name if coverage_join_source else "" + ), ), ) @@ -1566,6 +1629,8 @@ def _prepare_run_inputs() -> tuple[ new_block=new_block, html_builder=build_html_report, metrics_diff=metrics_diff, + coverage_adoption_diff_available=coverage_adoption_diff_available, + api_surface_diff_available=api_surface_diff_available, include_report_document=bool(changed_paths), ) changed_clone_gate = ( diff --git a/codeclone/contracts.py b/codeclone/contracts.py index f6b98f7..06e9517 100644 --- a/codeclone/contracts.py +++ b/codeclone/contracts.py @@ -12,8 +12,8 @@ BASELINE_SCHEMA_VERSION: Final = "2.1" BASELINE_FINGERPRINT_VERSION: Final = "1" -CACHE_VERSION: Final = "2.3" -REPORT_SCHEMA_VERSION: Final = "2.5" +CACHE_VERSION: Final = "2.4" +REPORT_SCHEMA_VERSION: Final = "2.8" METRICS_BASELINE_SCHEMA_VERSION: Final = "1.2" DEFAULT_COMPLEXITY_THRESHOLD: Final = 20 diff --git a/codeclone/domain/findings.py b/codeclone/domain/findings.py index 66f4851..686e44f 100644 --- a/codeclone/domain/findings.py +++ b/codeclone/domain/findings.py @@ -34,12 +34,16 @@ CATEGORY_COHESION: Final = "cohesion" CATEGORY_DEAD_CODE: Final = "dead_code" CATEGORY_DEPENDENCY: Final = "dependency" +CATEGORY_COVERAGE: Final = "coverage" FINDING_KIND_CLONE_GROUP: Final = "clone_group" FINDING_KIND_UNUSED_SYMBOL: Final = "unused_symbol" FINDING_KIND_CLASS_HOTSPOT: Final = "class_hotspot" FINDING_KIND_FUNCTION_HOTSPOT: Final = "function_hotspot" FINDING_KIND_CYCLE: Final = "cycle" +FINDING_KIND_UNTESTED_HOTSPOT: Final = "untested_hotspot" +FINDING_KIND_COVERAGE_HOTSPOT: Final = "coverage_hotspot" +FINDING_KIND_COVERAGE_SCOPE_GAP: Final = "coverage_scope_gap" STRUCTURAL_KIND_DUPLICATED_BRANCHES: Final = "duplicated_branches" STRUCTURAL_KIND_CLONE_GUARD_EXIT_DIVERGENCE: Final = "clone_guard_exit_divergence" @@ -50,6 +54,7 @@ "CATEGORY_COHESION", "CATEGORY_COMPLEXITY", "CATEGORY_COUPLING", + "CATEGORY_COVERAGE", "CATEGORY_DEAD_CODE", "CATEGORY_DEPENDENCY", "CATEGORY_STRUCTURAL", @@ -66,8 +71,11 @@ "FAMILY_STRUCTURAL", "FINDING_KIND_CLASS_HOTSPOT", "FINDING_KIND_CLONE_GROUP", + "FINDING_KIND_COVERAGE_HOTSPOT", + "FINDING_KIND_COVERAGE_SCOPE_GAP", "FINDING_KIND_CYCLE", "FINDING_KIND_FUNCTION_HOTSPOT", + "FINDING_KIND_UNTESTED_HOTSPOT", "FINDING_KIND_UNUSED_SYMBOL", "STRUCTURAL_KIND_CLONE_COHORT_DRIFT", "STRUCTURAL_KIND_CLONE_GUARD_EXIT_DIVERGENCE", diff --git a/codeclone/golden_fixtures.py b/codeclone/golden_fixtures.py new file mode 100644 index 0000000..3b6fe47 --- /dev/null +++ b/codeclone/golden_fixtures.py @@ -0,0 +1,178 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Den Rozhnovskiy + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from pathlib import PurePosixPath +from typing import Literal + +from .domain.source_scope import SOURCE_KIND_FIXTURES, SOURCE_KIND_TESTS +from .models import ( + GroupItem, + GroupItemLike, + GroupMap, + GroupMapLike, + SuppressedCloneGroup, +) +from .paths import classify_source_kind, normalize_repo_path, relative_repo_path + +CloneGroupKind = Literal["function", "block", "segment"] + +GOLDEN_FIXTURE_SUPPRESSION_RULE = "golden_fixture" +GOLDEN_FIXTURE_SUPPRESSION_SOURCE = "project_config" + +_ALLOWED_SOURCE_KINDS = frozenset({SOURCE_KIND_TESTS, SOURCE_KIND_FIXTURES}) + + +class GoldenFixturePatternError(ValueError): + """Raised when golden_fixture_paths contains an invalid pattern.""" + + +@dataclass(frozen=True, slots=True) +class GoldenFixtureGroupSplit: + active_groups: GroupMap + suppressed_groups: GroupMap + matched_patterns: dict[str, tuple[str, ...]] + + +def normalize_golden_fixture_patterns(patterns: Sequence[str]) -> tuple[str, ...]: + normalized: list[str] = [] + seen: set[str] = set() + for raw_pattern in patterns: + pattern = normalize_repo_path(str(raw_pattern)) + while pattern.startswith("./"): + pattern = pattern[2:] + pattern = pattern.rstrip("/") + if not pattern: + raise GoldenFixturePatternError( + "tool.codeclone.golden_fixture_paths entries must be non-empty" + ) + pure_pattern = PurePosixPath(pattern) + if pure_pattern.is_absolute(): + raise GoldenFixturePatternError( + "tool.codeclone.golden_fixture_paths entries must be repo-relative" + ) + if any(part == ".." for part in pure_pattern.parts): + raise GoldenFixturePatternError( + "tool.codeclone.golden_fixture_paths entries must not contain '..'" + ) + source_kind = classify_source_kind(pattern) + if source_kind not in _ALLOWED_SOURCE_KINDS: + raise GoldenFixturePatternError( + "tool.codeclone.golden_fixture_paths entries must target tests/ or " + "tests/fixtures/ paths" + ) + if pattern not in seen: + normalized.append(pattern) + seen.add(pattern) + return tuple(normalized) + + +def path_matches_golden_fixture_pattern(relative_path: str, pattern: str) -> bool: + normalized_path = normalize_repo_path(relative_path).lstrip("./") + if not normalized_path: + return False + candidate = PurePosixPath(normalized_path) + candidates = [candidate, *candidate.parents[:-1]] + return any(path.match(pattern) for path in candidates) + + +def split_clone_groups_for_golden_fixtures( + *, + groups: GroupMapLike, + kind: CloneGroupKind, + golden_fixture_paths: Sequence[str], + scan_root: str = "", +) -> GoldenFixtureGroupSplit: + active: GroupMap = {} + suppressed: GroupMap = {} + matched_patterns: dict[str, tuple[str, ...]] = {} + if not golden_fixture_paths: + for group_key in sorted(groups): + active[group_key] = [_copy_group_item(item) for item in groups[group_key]] + return GoldenFixtureGroupSplit( + active_groups=active, + suppressed_groups=suppressed, + matched_patterns=matched_patterns, + ) + + for group_key in sorted(groups): + copied_items = [_copy_group_item(item) for item in groups[group_key]] + group_patterns = _matched_patterns_for_group( + copied_items, + patterns=golden_fixture_paths, + scan_root=scan_root, + ) + if group_patterns: + suppressed[group_key] = copied_items + matched_patterns[group_key] = group_patterns + else: + active[group_key] = copied_items + return GoldenFixtureGroupSplit( + active_groups=active, + suppressed_groups=suppressed, + matched_patterns=matched_patterns, + ) + + +def build_suppressed_clone_groups( + *, + kind: CloneGroupKind, + groups: GroupMapLike, + matched_patterns: Mapping[str, Sequence[str]], +) -> tuple[SuppressedCloneGroup, ...]: + suppressed_groups: list[SuppressedCloneGroup] = [] + for group_key in sorted(groups): + patterns = tuple( + str(pattern).strip() + for pattern in matched_patterns.get(group_key, ()) + if str(pattern).strip() + ) + if not patterns: + continue + suppressed_groups.append( + SuppressedCloneGroup( + kind=kind, + group_key=group_key, + items=tuple(_copy_group_item(item) for item in groups[group_key]), + matched_patterns=patterns, + suppression_rule=GOLDEN_FIXTURE_SUPPRESSION_RULE, + suppression_source=GOLDEN_FIXTURE_SUPPRESSION_SOURCE, + ) + ) + return tuple(suppressed_groups) + + +def _copy_group_item(item: GroupItemLike) -> GroupItem: + return {str(key): value for key, value in item.items()} + + +def _matched_patterns_for_group( + items: Sequence[GroupItemLike], + *, + patterns: Sequence[str], + scan_root: str, +) -> tuple[str, ...]: + matched: set[str] = set() + for item in items: + filepath = str(item.get("filepath", "")).strip() + if not filepath: + return () + source_kind = classify_source_kind(filepath, scan_root=scan_root) + if source_kind not in _ALLOWED_SOURCE_KINDS: + return () + relative_path = relative_repo_path(filepath, scan_root=scan_root) + item_matches = tuple( + pattern + for pattern in patterns + if path_matches_golden_fixture_pattern(relative_path, pattern) + ) + if not item_matches: + return () + matched.update(item_matches) + return tuple(sorted(matched)) diff --git a/codeclone/mcp_server.py b/codeclone/mcp_server.py index f577f74..ee7a6fc 100644 --- a/codeclone/mcp_server.py +++ b/codeclone/mcp_server.py @@ -39,9 +39,10 @@ "them only for an explicit higher-sensitivity follow-up when needed. Use " "get_report_section(section='metrics_detail', family=..., limit=...) for " "bounded metrics drill-down, and prefer generate_pr_summary(format='markdown') " - "unless machine JSON is required. Pass an absolute repository root to " - "analysis tools. This server never updates baselines and never mutates " - "source files." + "unless machine JSON is required. Coverage join accepts external Cobertura " + "XML as a current-run signal and does not become baseline truth. Pass an " + "absolute repository root to analysis tools. This server never updates " + "baselines and never mutates source files." ) _MCP_INSTALL_HINT = ( "CodeClone MCP support requires the optional 'mcp' extra. " @@ -159,6 +160,8 @@ def analyze_repository( segment_min_loc: int | None = None, segment_min_stmt: int | None = None, api_surface: bool | None = None, + coverage_xml: str | None = None, + coverage_min: int | None = None, complexity_threshold: int | None = None, coupling_threshold: int | None = None, cohesion_threshold: int | None = None, @@ -184,6 +187,8 @@ def analyze_repository( segment_min_loc=segment_min_loc, segment_min_stmt=segment_min_stmt, api_surface=api_surface, + coverage_xml=coverage_xml, + coverage_min=coverage_min, complexity_threshold=complexity_threshold, coupling_threshold=coupling_threshold, cohesion_threshold=cohesion_threshold, @@ -225,6 +230,8 @@ def analyze_changed_paths( segment_min_loc: int | None = None, segment_min_stmt: int | None = None, api_surface: bool | None = None, + coverage_xml: str | None = None, + coverage_min: int | None = None, complexity_threshold: int | None = None, coupling_threshold: int | None = None, cohesion_threshold: int | None = None, @@ -250,6 +257,8 @@ def analyze_changed_paths( segment_min_loc=segment_min_loc, segment_min_stmt=segment_min_stmt, api_surface=api_surface, + coverage_xml=coverage_xml, + coverage_min=coverage_min, complexity_threshold=complexity_threshold, coupling_threshold=coupling_threshold, cohesion_threshold=cohesion_threshold, @@ -304,7 +313,7 @@ def get_production_triage( "canonical doc links. Use this when workflow or contract meaning " "is unclear. This is bounded guidance, not a full manual. " "Supported topics: workflow, analysis_profile, suppressions, " - "baseline, latest_runs, review_state, changed_scope." + "baseline, coverage, latest_runs, review_state, changed_scope." ), annotations=read_only_tool, structured_output=True, @@ -341,8 +350,10 @@ def evaluate_gates( fail_on_typing_regression: bool = False, fail_on_docstring_regression: bool = False, fail_on_api_break: bool = False, + fail_on_untested_hotspots: bool = False, min_typing_coverage: int = -1, min_docstring_coverage: int = -1, + coverage_min: int = 50, ) -> dict[str, object]: return service.evaluate_gates( MCPGateRequest( @@ -359,8 +370,10 @@ def evaluate_gates( fail_on_typing_regression=fail_on_typing_regression, fail_on_docstring_regression=fail_on_docstring_regression, fail_on_api_break=fail_on_api_break, + fail_on_untested_hotspots=fail_on_untested_hotspots, min_typing_coverage=min_typing_coverage, min_docstring_coverage=min_docstring_coverage, + coverage_min=coverage_min, ) ) diff --git a/codeclone/mcp_service.py b/codeclone/mcp_service.py index 71f1d82..6a22906 100644 --- a/codeclone/mcp_service.py +++ b/codeclone/mcp_service.py @@ -92,7 +92,7 @@ SOURCE_KIND_PRODUCTION, SOURCE_KIND_TESTS, ) -from .models import MetricsDiff, ProjectMetrics, Suggestion +from .models import CoverageJoinResult, MetricsDiff, ProjectMetrics, Suggestion from .pipeline import ( GatingResult, MetricGateConfig, @@ -132,6 +132,7 @@ "analysis_profile", "suppressions", "baseline", + "coverage", "latest_runs", "review_state", "changed_scope", @@ -142,6 +143,7 @@ "coupling", "cohesion", "coverage_adoption", + "coverage_join", "dependencies", "dead_code", "api_surface", @@ -186,6 +188,9 @@ "typing_coverage", "docstring_coverage", "api_surface", + "coverage_xml", + "coverage_min", + "golden_fixture_paths", } ) _RESOURCE_SECTION_MAP: Final[dict[str, ReportSection]] = { @@ -236,6 +241,7 @@ "analysis_profile", "suppressions", "baseline", + "coverage", "latest_runs", "review_state", "changed_scope", @@ -297,6 +303,7 @@ "coupling", "cohesion", "coverage_adoption", + "coverage_join", "dependencies", "dead_code", "api_surface", @@ -336,6 +343,14 @@ class MCPHelpTopicSpec: "Config and defaults", f"{_MCP_BOOK_URL}04-config-and-defaults/", ) +_REPORT_DOC_LINK: Final[tuple[str, str]] = ( + "Report contract", + f"{_MCP_BOOK_URL}08-report/", +) +_CLI_DOC_LINK: Final[tuple[str, str]] = ( + "CLI contract", + f"{_MCP_BOOK_URL}09-cli/", +) _PIPELINE_DOC_LINK: Final[tuple[str, str]] = ( "Core pipeline", f"{_MCP_BOOK_URL}05-core-pipeline/", @@ -540,6 +555,49 @@ class MCPHelpTopicSpec: "Assuming an untrusted baseline is only cosmetic in CI contexts.", ), ), + "coverage": MCPHelpTopicSpec( + summary=( + "Coverage join is an external current-run signal: CodeClone reads " + "an existing Cobertura XML report and joins line hits to risky " + "function spans." + ), + key_points=( + "Use Cobertura XML such as `coverage xml` output from coverage.py.", + "Coverage join does not become baseline truth and does not affect health.", + ( + "Coverage hotspot gating is current-run only and focuses on " + "medium/high-risk functions measured below the configured " + "threshold." + ), + ( + "Functions missing from the supplied coverage.xml are surfaced " + "as scope gaps, not labeled as untested." + ), + "Use metrics_detail(family='coverage_join') for bounded drill-down.", + ), + recommended_tools=( + "analyze_repository", + "analyze_changed_paths", + "get_run_summary", + "get_report_section", + "evaluate_gates", + ), + doc_links=( + _MCP_INTERFACE_DOC_LINK, + _CLI_DOC_LINK, + _REPORT_DOC_LINK, + ), + warnings=( + "Coverage join is only as accurate as the external XML path mapping.", + "It does not infer branch coverage and does not execute tests.", + "Use fail-on-untested-hotspots only with a valid joined coverage input.", + ), + anti_patterns=( + "Treating missing coverage XML as zero coverage without stating it.", + "Reading coverage join as a baseline-aware trend signal.", + "Assuming dynamic runtime dispatch is visible through a static line join.", + ), + ), "latest_runs": MCPHelpTopicSpec( summary=( "latest/* resources point to the most recent analysis run in the " @@ -888,6 +946,8 @@ class MCPAnalysisRequest: segment_min_loc: int | None = None segment_min_stmt: int | None = None api_surface: bool | None = None + coverage_xml: str | None = None + coverage_min: int | None = None complexity_threshold: int | None = None coupling_threshold: int | None = None cohesion_threshold: int | None = None @@ -914,8 +974,10 @@ class MCPGateRequest: fail_on_typing_regression: bool = False fail_on_docstring_regression: bool = False fail_on_api_break: bool = False + fail_on_untested_hotspots: bool = False min_typing_coverage: int = -1 min_docstring_coverage: int = -1 + coverage_min: int = 50 @dataclass(frozen=True, slots=True) @@ -933,6 +995,7 @@ class MCPRunRecord: func_clones_count: int block_clones_count: int project_metrics: ProjectMetrics | None + coverage_join: CoverageJoinResult | None suggestions: tuple[Suggestion, ...] new_func: frozenset[str] new_block: frozenset[str] @@ -1206,6 +1269,7 @@ def analyze_repository(self, request: MCPAnalysisRequest) -> dict[str, object]: func_clones_count=analysis_result.func_clones_count, block_clones_count=analysis_result.block_clones_count, project_metrics=analysis_result.project_metrics, + coverage_join=analysis_result.coverage_join, suggestions=analysis_result.suggestions, new_func=frozenset(new_func), new_block=frozenset(new_block), @@ -1231,6 +1295,7 @@ def analyze_repository(self, request: MCPAnalysisRequest) -> dict[str, object]: func_clones_count=analysis_result.func_clones_count, block_clones_count=analysis_result.block_clones_count, project_metrics=analysis_result.project_metrics, + coverage_join=analysis_result.coverage_join, suggestions=analysis_result.suggestions, new_func=frozenset(new_func), new_block=frozenset(new_block), @@ -1361,8 +1426,10 @@ def evaluate_gates(self, request: MCPGateRequest) -> dict[str, object]: "fail_on_typing_regression": request.fail_on_typing_regression, "fail_on_docstring_regression": request.fail_on_docstring_regression, "fail_on_api_break": request.fail_on_api_break, + "fail_on_untested_hotspots": request.fail_on_untested_hotspots, "min_typing_coverage": request.min_typing_coverage, "min_docstring_coverage": request.min_docstring_coverage, + "coverage_min": request.coverage_min, }, } with self._state_lock: @@ -1376,9 +1443,21 @@ def _evaluate_gate_snapshot( request: MCPGateRequest, ) -> GatingResult: reasons: list[str] = [] + if request.fail_on_untested_hotspots: + if record.coverage_join is None: + raise MCPServiceContractError( + "Coverage gating requires a run created with coverage_xml." + ) + if record.coverage_join.status != "ok": + detail = record.coverage_join.invalid_reason or "invalid coverage input" + raise MCPServiceContractError( + "Coverage gating requires a valid Cobertura XML input. " + f"Reason: {detail}" + ) if record.project_metrics is not None: metric_reasons = metric_gate_reasons( project_metrics=record.project_metrics, + coverage_join=record.coverage_join, metrics_diff=record.metrics_diff, config=MetricGateConfig( fail_complexity=request.fail_complexity, @@ -1391,8 +1470,10 @@ def _evaluate_gate_snapshot( fail_on_typing_regression=request.fail_on_typing_regression, fail_on_docstring_regression=request.fail_on_docstring_regression, fail_on_api_break=request.fail_on_api_break, + fail_on_untested_hotspots=request.fail_on_untested_hotspots, min_typing_coverage=request.min_typing_coverage, min_docstring_coverage=request.min_docstring_coverage, + coverage_min=request.coverage_min, ), ) reasons.extend(f"metric:{reason}" for reason in metric_reasons) @@ -1734,6 +1815,9 @@ def get_production_triage( analysis_profile = self._summary_analysis_profile_payload(summary) if analysis_profile: payload["analysis_profile"] = analysis_profile + coverage_join = self._summary_coverage_join_payload(record) + if coverage_join: + payload["coverage_join"] = coverage_join return payload def get_help( @@ -3324,6 +3408,7 @@ def _changed_analysis_payload( ), "resolved_findings": 0, "changed_findings": [], + "coverage_join": self._summary_coverage_join_payload(record), } def _augment_summary_with_changed( @@ -3681,6 +3766,11 @@ def _validate_analysis_request(self, request: MCPAnalysisRequest) -> None: "cache_policy='refresh' is not supported by the read-only " "CodeClone MCP server. Use 'reuse' or 'off'." ) + if request.analysis_mode == "clones_only" and request.coverage_xml is not None: + raise MCPServiceContractError( + "coverage_xml requires analysis_mode='full' because coverage join " + "depends on metrics-enabled analysis." + ) @staticmethod def _validate_choice( @@ -3773,6 +3863,9 @@ def _build_args(self, *, root_path: Path, request: MCPAnalysisRequest) -> Namesp typing_coverage=True, docstring_coverage=True, api_surface=False, + coverage_xml=None, + fail_on_untested_hotspots=False, + coverage_min=50, design_complexity_threshold=DEFAULT_REPORT_DESIGN_COMPLEXITY_THRESHOLD, design_coupling_threshold=DEFAULT_REPORT_DESIGN_COUPLING_THRESHOLD, design_cohesion_threshold=DEFAULT_REPORT_DESIGN_COHESION_THRESHOLD, @@ -3781,6 +3874,7 @@ def _build_args(self, *, root_path: Path, request: MCPAnalysisRequest) -> Namesp skip_metrics=False, skip_dead_code=False, skip_dependencies=False, + golden_fixture_paths=(), html_out=None, json_out=None, md_out=None, @@ -3837,6 +3931,7 @@ def _apply_request_overrides( "segment_min_loc": request.segment_min_loc, "segment_min_stmt": request.segment_min_stmt, "api_surface": request.api_surface, + "coverage_min": request.coverage_min, "max_baseline_size_mb": request.max_baseline_size_mb, "max_cache_size_mb": request.max_cache_size_mb, "design_complexity_threshold": request.complexity_threshold, @@ -3859,6 +3954,10 @@ def _apply_request_overrides( args.cache_path = str( self._resolve_optional_path(request.cache_path, root_path) ) + if request.coverage_xml is not None: + args.coverage_xml = str( + self._resolve_optional_path(request.coverage_xml, root_path) + ) @staticmethod def _resolve_optional_path(value: str, root_path: Path) -> Path: @@ -4094,6 +4193,10 @@ def _summary_payload( analysis_profile = self._summary_analysis_profile_payload(summary) if analysis_profile: payload["analysis_profile"] = analysis_profile + if record is not None: + coverage_join = self._summary_coverage_join_payload(record) + if coverage_join: + payload["coverage_join"] = coverage_join return payload def _summary_analysis_profile_payload( @@ -4299,6 +4402,40 @@ def _summary_diff_payload( ), } + def _summary_coverage_join_payload( + self, + record: MCPRunRecord, + ) -> dict[str, object]: + metrics = self._as_mapping(record.report_document.get("metrics")) + families = self._as_mapping(metrics.get("families")) + coverage_join = self._as_mapping(families.get("coverage_join")) + summary = self._as_mapping(coverage_join.get("summary")) + if not summary: + return {} + payload: dict[str, object] = { + "status": str(summary.get("status", "")).strip(), + "overall_permille": _as_int(summary.get("overall_permille", 0), 0), + "coverage_hotspots": _as_int(summary.get("coverage_hotspots", 0), 0), + "scope_gap_hotspots": _as_int(summary.get("scope_gap_hotspots", 0), 0), + "hotspot_threshold_percent": _as_int( + summary.get("hotspot_threshold_percent", 0), + 0, + ), + } + source_value = summary.get("source") + source = source_value.strip() if isinstance(source_value, str) else "" + if source: + payload["source"] = source + invalid_reason_value = summary.get("invalid_reason") + invalid_reason = ( + invalid_reason_value.strip() + if isinstance(invalid_reason_value, str) + else "" + ) + if invalid_reason: + payload["invalid_reason"] = invalid_reason + return payload + def _metrics_detail_payload( self, *, @@ -4437,14 +4574,7 @@ def _metrics_diff_payload( ) ) ), - "new_api_symbols": len( - tuple( - cast( - Sequence[object], - getattr(metrics_diff, "new_api_symbols", ()), - ) - ) - ), + "new_api_symbols": len(tuple(getattr(metrics_diff, "new_api_symbols", ()))), } def _dict_list(self, value: object) -> list[dict[str, object]]: diff --git a/codeclone/metrics/__init__.py b/codeclone/metrics/__init__.py index 14ea398..0551b7d 100644 --- a/codeclone/metrics/__init__.py +++ b/codeclone/metrics/__init__.py @@ -9,6 +9,7 @@ from .cohesion import cohesion_risk, compute_lcom4 from .complexity import cyclomatic_complexity, nesting_depth, risk_level from .coupling import compute_cbo, coupling_risk +from .coverage_join import CoverageJoinParseError, build_coverage_join from .dead_code import find_suppressed_unused, find_unused from .dependencies import ( build_dep_graph, @@ -21,7 +22,9 @@ from .overloaded_modules import build_overloaded_modules_payload __all__ = [ + "CoverageJoinParseError", "HealthInputs", + "build_coverage_join", "build_dep_graph", "build_import_graph", "build_overloaded_modules_payload", diff --git a/codeclone/metrics/coverage_join.py b/codeclone/metrics/coverage_join.py new file mode 100644 index 0000000..08c8278 --- /dev/null +++ b/codeclone/metrics/coverage_join.py @@ -0,0 +1,331 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Den Rozhnovskiy + +from __future__ import annotations + +from collections import defaultdict +from collections.abc import Sequence +from dataclasses import dataclass +from pathlib import Path +from typing import Literal, cast +from xml.etree import ElementTree + +from .._coerce import as_int, as_str +from ..models import CoverageJoinResult, GroupItemLike, UnitCoverageFact + +__all__ = [ + "CoverageJoinParseError", + "build_coverage_join", +] + +_Risk = Literal["low", "medium", "high"] +_CoverageStatus = Literal["measured", "missing_from_report", "no_executable_lines"] + +_MEASURED_STATUS: _CoverageStatus = "measured" +_MISSING_FROM_REPORT_STATUS: _CoverageStatus = "missing_from_report" +_NO_EXECUTABLE_LINES_STATUS: _CoverageStatus = "no_executable_lines" +_HOTSPOT_RISKS: frozenset[_Risk] = frozenset({"medium", "high"}) + + +class CoverageJoinParseError(ValueError): + """Raised when a Cobertura XML payload cannot be parsed safely.""" + + +@dataclass(frozen=True, slots=True) +class _CoverageFileLines: + executable_lines: frozenset[int] + covered_lines: frozenset[int] + + +@dataclass(frozen=True, slots=True) +class _CoverageReport: + files: dict[str, _CoverageFileLines] + + +def _permille(numerator: int, denominator: int) -> int: + if denominator <= 0: + return 0 + return round((1000.0 * float(numerator)) / float(denominator)) + + +def _local_tag_name(tag: object) -> str: + if not isinstance(tag, str): + return "" + _, _, local_name = tag.rpartition("}") + return local_name or tag + + +def _normalized_relpath_text(value: str) -> str: + return value.replace("\\", "/").strip() + + +def _resolved_path(candidate: Path) -> Path: + try: + return candidate.expanduser().resolve(strict=False) + except OSError: + return candidate.expanduser().absolute() + + +def _resolved_coverage_sources( + *, + root_element: ElementTree.Element, + root_path: Path, +) -> tuple[Path, ...]: + resolved: list[Path] = [] + seen: set[str] = set() + for element in root_element.iter(): + text = _normalized_relpath_text(element.text or "") + if _local_tag_name(element.tag) != "source" or not text: + continue + source_path = Path(text) + if not source_path.is_absolute(): + source_path = root_path / source_path + candidate = _resolved_path(source_path) + key = str(candidate) + if key not in seen: + resolved.append(candidate) + seen.add(key) + fallback = _resolved_path(root_path) + if str(fallback) not in seen: + resolved.insert(0, fallback) + return tuple(resolved) + + +def _resolve_report_filename( + *, + filename: str, + root_path: Path, + source_roots: Sequence[Path], +) -> str | None: + normalized_filename = _normalized_relpath_text(filename) + if not normalized_filename: + return None + raw_path = Path(normalized_filename) + candidates: list[Path] = [] + if raw_path.is_absolute(): + candidates.append(raw_path) + else: + candidates.append(root_path / raw_path) + candidates.extend(source_root / raw_path for source_root in source_roots) + + unique_candidates: list[Path] = [] + seen_candidates: set[str] = set() + for candidate in candidates: + resolved = _resolved_path(candidate) + key = str(resolved) + if key not in seen_candidates: + unique_candidates.append(resolved) + seen_candidates.add(key) + + under_root_existing: list[Path] = [] + under_root_fallback: list[Path] = [] + for candidate in unique_candidates: + try: + candidate.relative_to(root_path) + except ValueError: + continue + if candidate.exists(): + under_root_existing.append(candidate) + under_root_fallback.append(candidate) + + if under_root_existing: + return str(sorted(under_root_existing)[0]) + if under_root_fallback: + return str(under_root_fallback[0]) + return None + + +def _iter_cobertura_class_elements( + root_element: ElementTree.Element, +) -> Sequence[ElementTree.Element]: + return tuple( + element + for element in root_element.iter() + if _local_tag_name(element.tag) == "class" + ) + + +def _iter_cobertura_line_hits( + class_element: ElementTree.Element, +) -> Sequence[tuple[int, int]]: + rows: list[tuple[int, int]] = [] + for line_element in class_element.iter(): + if _local_tag_name(line_element.tag) == "line": + line_number = as_int(line_element.attrib.get("number"), -1) + hits = as_int(line_element.attrib.get("hits"), -1) + if line_number > 0 and hits >= 0: + rows.append((line_number, hits)) + return tuple(rows) + + +def _parse_coverage_report( + *, + coverage_xml: Path, + root_path: Path, +) -> _CoverageReport: + try: + tree = ElementTree.parse(coverage_xml) + except (ElementTree.ParseError, OSError) as exc: + raise CoverageJoinParseError( + f"Invalid Cobertura XML at {coverage_xml}: {exc}" + ) from exc + + root_element = tree.getroot() + source_roots = _resolved_coverage_sources( + root_element=root_element, root_path=root_path + ) + file_lines: dict[str, dict[str, set[int]]] = defaultdict( + lambda: {"executable": set(), "covered": set()} + ) + + for element in _iter_cobertura_class_elements(root_element): + filename = element.attrib.get("filename", "") + resolved_filename = _resolve_report_filename( + filename=filename, + root_path=root_path, + source_roots=source_roots, + ) + if resolved_filename is not None: + target = file_lines[resolved_filename] + for line_number, hits in _iter_cobertura_line_hits(element): + target["executable"].add(line_number) + if hits > 0: + target["covered"].add(line_number) + + return _CoverageReport( + files={ + filepath: _CoverageFileLines( + executable_lines=frozenset(sorted(lines["executable"])), + covered_lines=frozenset(sorted(lines["covered"])), + ) + for filepath, lines in sorted(file_lines.items()) + } + ) + + +def _unit_sort_key(item: GroupItemLike) -> tuple[str, int, int, str]: + return ( + as_str(item.get("filepath")), + as_int(item.get("start_line")), + as_int(item.get("end_line")), + as_str(item.get("qualname")), + ) + + +def _resolve_unit_path(filepath: str) -> str: + return str(_resolved_path(Path(filepath))) + + +def _risk_level(value: object) -> _Risk: + risk = as_str(value, "low") + if risk in {"low", "medium", "high"}: + return cast(_Risk, risk) + return "low" + + +def _unit_coverage_fact( + *, + unit: GroupItemLike, + coverage_file: _CoverageFileLines | None, +) -> UnitCoverageFact: + filepath = as_str(unit.get("filepath")) + start_line = as_int(unit.get("start_line")) + end_line = as_int(unit.get("end_line")) + coverage_status: _CoverageStatus + if coverage_file is None: + executable_lines = 0 + covered_lines = 0 + coverage_permille = 0 + coverage_status = _MISSING_FROM_REPORT_STATUS + else: + executable_lines = sum( + 1 + for line_number in coverage_file.executable_lines + if start_line <= line_number <= end_line + ) + covered_lines = sum( + 1 + for line_number in coverage_file.covered_lines + if start_line <= line_number <= end_line + ) + coverage_permille = _permille(covered_lines, executable_lines) + coverage_status = ( + _MEASURED_STATUS if executable_lines > 0 else _NO_EXECUTABLE_LINES_STATUS + ) + return UnitCoverageFact( + qualname=as_str(unit.get("qualname")), + filepath=filepath, + start_line=start_line, + end_line=end_line, + cyclomatic_complexity=as_int(unit.get("cyclomatic_complexity"), 1), + risk=_risk_level(unit.get("risk")), + executable_lines=executable_lines, + covered_lines=covered_lines, + coverage_permille=coverage_permille, + coverage_status=coverage_status, + ) + + +def _is_coverage_hotspot( + *, + fact: UnitCoverageFact, + hotspot_threshold_percent: int, +) -> bool: + if fact.risk not in _HOTSPOT_RISKS: + return False + if fact.coverage_status != _MEASURED_STATUS: + return False + return (fact.coverage_permille / 10.0) < float(hotspot_threshold_percent) + + +def _is_scope_gap_hotspot(*, fact: UnitCoverageFact) -> bool: + return ( + fact.risk in _HOTSPOT_RISKS + and fact.coverage_status == _MISSING_FROM_REPORT_STATUS + ) + + +def build_coverage_join( + *, + coverage_xml: Path, + root_path: Path, + units: Sequence[GroupItemLike], + hotspot_threshold_percent: int, +) -> CoverageJoinResult: + report = _parse_coverage_report(coverage_xml=coverage_xml, root_path=root_path) + facts = tuple( + _unit_coverage_fact( + unit=unit, + coverage_file=report.files.get( + _resolve_unit_path(as_str(unit.get("filepath"))) + ), + ) + for unit in sorted(units, key=_unit_sort_key) + ) + measured_units = sum( + 1 for fact in facts if fact.coverage_status == _MEASURED_STATUS + ) + overall_executable_lines = sum(fact.executable_lines for fact in facts) + overall_covered_lines = sum(fact.covered_lines for fact in facts) + return CoverageJoinResult( + coverage_xml=str(_resolved_path(coverage_xml)), + status="ok", + hotspot_threshold_percent=hotspot_threshold_percent, + files=len(report.files), + measured_units=measured_units, + overall_executable_lines=overall_executable_lines, + overall_covered_lines=overall_covered_lines, + coverage_hotspots=sum( + 1 + for fact in facts + if _is_coverage_hotspot( + fact=fact, + hotspot_threshold_percent=hotspot_threshold_percent, + ) + ), + scope_gap_hotspots=sum(1 for fact in facts if _is_scope_gap_hotspot(fact=fact)), + units=facts, + ) diff --git a/codeclone/metrics_baseline.py b/codeclone/metrics_baseline.py index 22f18ce..ed4197e 100644 --- a/codeclone/metrics_baseline.py +++ b/codeclone/metrics_baseline.py @@ -26,6 +26,7 @@ from .errors import BaselineValidationError from .metrics.api_surface import compare_api_surfaces from .models import ( + ApiBreakingChange, ApiParamSpec, ApiSurfaceSnapshot, MetricsDiff, @@ -356,6 +357,7 @@ def save(self) -> None: generator_name=self.generator_name or METRICS_BASELINE_GENERATOR, generator_version=self.generator_version or __version__, created_at=self.created_at or _now_utc_z(), + include_adoption=self.has_coverage_adoption_snapshot, api_surface_snapshot=self.api_surface_snapshot, api_surface_root=self.path.parent, ) @@ -413,7 +415,6 @@ def save(self) -> None: self.payload_sha256 = _require_str( merged_meta, _METRICS_PAYLOAD_SHA256_KEY, path=self.path ) - self.has_coverage_adoption_snapshot = True self.api_surface_payload_sha256 = _optional_require_str( merged_meta, _API_SURFACE_PAYLOAD_SHA256_KEY, @@ -433,7 +434,6 @@ def save(self) -> None: self.python_tag = _require_str(payload_meta, "python_tag", path=self.path) self.created_at = _require_str(payload_meta, "created_at", path=self.path) self.payload_sha256 = payload_metrics_hash - self.has_coverage_adoption_snapshot = True self.api_surface_payload_sha256 = payload_api_surface_hash def verify_compatibility(self, *, runtime_python_tag: str) -> None: @@ -542,6 +542,8 @@ def from_project_metrics( schema_version: str | None = None, python_tag: str | None = None, generator_version: str | None = None, + include_adoption: bool = True, + include_api_surface: bool = True, ) -> MetricsBaseline: baseline = MetricsBaseline(path) baseline.generator_name = METRICS_BASELINE_GENERATOR @@ -550,15 +552,20 @@ def from_project_metrics( baseline.python_tag = python_tag or current_python_tag() baseline.created_at = _now_utc_z() baseline.snapshot = snapshot_from_project_metrics(project_metrics) - baseline.payload_sha256 = _compute_payload_sha256(baseline.snapshot) - baseline.has_coverage_adoption_snapshot = True - baseline.api_surface_snapshot = project_metrics.api_surface + baseline.payload_sha256 = _compute_payload_sha256( + baseline.snapshot, + include_adoption=include_adoption, + ) + baseline.has_coverage_adoption_snapshot = include_adoption + baseline.api_surface_snapshot = ( + project_metrics.api_surface if include_api_surface else None + ) baseline.api_surface_payload_sha256 = ( _compute_api_surface_payload_sha256( - project_metrics.api_surface, + baseline.api_surface_snapshot, root=baseline.path.parent, ) - if project_metrics.api_surface is not None + if baseline.api_surface_snapshot is not None else None ) return baseline @@ -610,11 +617,17 @@ def diff(self, current: ProjectMetrics) -> MetricsDiff: set(current_snapshot.dead_code_items) - set(snapshot.dead_code_items) ) ) - added_api_symbols, api_breaking_changes = compare_api_surfaces( - baseline=self.api_surface_snapshot, - current=current.api_surface, - strict_types=False, - ) + added_api_symbols: tuple[str, ...] + api_breaking_changes: tuple[ApiBreakingChange, ...] + if self.api_surface_snapshot is None: + added_api_symbols = () + api_breaking_changes = () + else: + added_api_symbols, api_breaking_changes = compare_api_surfaces( + baseline=self.api_surface_snapshot, + current=current.api_surface, + strict_types=False, + ) return MetricsDiff( new_high_risk_functions=new_high_risk_functions, @@ -1252,10 +1265,14 @@ def _build_payload( generator_name: str, generator_version: str, created_at: str, + include_adoption: bool = True, api_surface_snapshot: ApiSurfaceSnapshot | None = None, api_surface_root: Path | None = None, ) -> dict[str, Any]: - payload_sha256 = _compute_payload_sha256(snapshot) + payload_sha256 = _compute_payload_sha256( + snapshot, + include_adoption=include_adoption, + ) payload: dict[str, Any] = { "meta": { "generator": { @@ -1267,7 +1284,10 @@ def _build_payload( "created_at": created_at, "payload_sha256": payload_sha256, }, - "metrics": _snapshot_payload(snapshot), + "metrics": _snapshot_payload( + snapshot, + include_adoption=include_adoption, + ), } if api_surface_snapshot is not None: payload["meta"][_API_SURFACE_PAYLOAD_SHA256_KEY] = ( diff --git a/codeclone/models.py b/codeclone/models.py index f34abf5..4814fc1 100644 --- a/codeclone/models.py +++ b/codeclone/models.py @@ -311,6 +311,45 @@ class ModuleDocstringCoverage: public_symbol_documented: int +@dataclass(frozen=True, slots=True) +class UnitCoverageFact: + qualname: str + filepath: str + start_line: int + end_line: int + cyclomatic_complexity: int + risk: Literal["low", "medium", "high"] + executable_lines: int + covered_lines: int + coverage_permille: int + coverage_status: Literal["measured", "missing_from_report", "no_executable_lines"] + + +@dataclass(frozen=True, slots=True) +class CoverageJoinResult: + coverage_xml: str + status: Literal["ok", "invalid"] + hotspot_threshold_percent: int + files: int = 0 + measured_units: int = 0 + overall_executable_lines: int = 0 + overall_covered_lines: int = 0 + coverage_hotspots: int = 0 + scope_gap_hotspots: int = 0 + units: tuple[UnitCoverageFact, ...] = () + invalid_reason: str | None = None + + +@dataclass(frozen=True, slots=True) +class SuppressedCloneGroup: + kind: Literal["function", "block", "segment"] + group_key: str + items: tuple[GroupItem, ...] + matched_patterns: tuple[str, ...] = () + suppression_rule: str = "" + suppression_source: str = "" + + GroupItem = dict[str, object] GroupItemLike = Mapping[str, object] GroupItemsLike = Sequence[GroupItemLike] diff --git a/codeclone/paths.py b/codeclone/paths.py index c9a33a6..d93428f 100644 --- a/codeclone/paths.py +++ b/codeclone/paths.py @@ -8,12 +8,52 @@ from pathlib import Path +from .domain.source_scope import ( + SOURCE_KIND_FIXTURES, + SOURCE_KIND_OTHER, + SOURCE_KIND_PRODUCTION, + SOURCE_KIND_TESTS, +) + _TEST_FILE_NAMES = {"conftest.py"} +def normalize_repo_path(value: str) -> str: + return value.replace("\\", "/").strip() + + +def relative_repo_path(filepath: str, *, scan_root: str = "") -> str: + normalized_path = normalize_repo_path(filepath) + normalized_root = normalize_repo_path(scan_root).rstrip("/") + if not normalized_path: + return normalized_path + if not normalized_root: + return normalized_path + prefix = f"{normalized_root}/" + if normalized_path.startswith(prefix): + return normalized_path[len(prefix) :] + if normalized_path == normalized_root: + return normalized_path.rsplit("/", maxsplit=1)[-1] + return normalized_path + + +def classify_source_kind(filepath: str, *, scan_root: str = "") -> str: + rel = relative_repo_path(filepath, scan_root=scan_root) + parts = [part for part in rel.lower().split("/") if part and part != "."] + if not parts: + return SOURCE_KIND_OTHER + for idx, part in enumerate(parts): + if part != SOURCE_KIND_TESTS: + continue + if idx + 1 < len(parts) and parts[idx + 1] == SOURCE_KIND_FIXTURES: + return SOURCE_KIND_FIXTURES + return SOURCE_KIND_TESTS + return SOURCE_KIND_PRODUCTION + + def is_test_filepath(filepath: str) -> bool: - normalized = filepath.lower().replace("\\", "/") - if "/tests/" in normalized or "/test/" in normalized: + source_kind = classify_source_kind(filepath) + if source_kind in {SOURCE_KIND_TESTS, SOURCE_KIND_FIXTURES}: return True filename = Path(filepath).name.lower() return filename in _TEST_FILE_NAMES or filename.startswith("test_") diff --git a/codeclone/pipeline.py b/codeclone/pipeline.py index f87477b..0f8dc7e 100644 --- a/codeclone/pipeline.py +++ b/codeclone/pipeline.py @@ -36,9 +36,15 @@ from .domain.findings import CATEGORY_COHESION, CATEGORY_COMPLEXITY, CATEGORY_COUPLING from .domain.quality import CONFIDENCE_HIGH, RISK_HIGH, RISK_LOW from .extractor import extract_units_and_stats_from_source +from .golden_fixtures import ( + build_suppressed_clone_groups, + split_clone_groups_for_golden_fixtures, +) from .grouping import build_block_groups, build_groups, build_segment_groups from .metrics import ( + CoverageJoinParseError, HealthInputs, + build_coverage_join, build_dep_graph, build_overloaded_modules_payload, compute_health, @@ -51,6 +57,7 @@ ApiSurfaceSnapshot, BlockUnit, ClassMetrics, + CoverageJoinResult, DeadCandidate, DeadItem, DepGraph, @@ -69,6 +76,7 @@ StructuralFindingGroup, StructuralFindingOccurrence, Suggestion, + SuppressedCloneGroup, Unit, ) from .normalize import NormalizationConfig @@ -202,6 +210,8 @@ class AnalysisResult: metrics_payload: dict[str, object] | None suggestions: tuple[Suggestion, ...] segment_groups_raw_digest: str + suppressed_clone_groups: tuple[SuppressedCloneGroup, ...] = () + coverage_join: CoverageJoinResult | None = None suppressed_dead_code_items: int = 0 structural_findings: tuple[StructuralFindingGroup, ...] = () @@ -234,8 +244,10 @@ class MetricGateConfig: fail_on_typing_regression: bool = False fail_on_docstring_regression: bool = False fail_on_api_break: bool = False + fail_on_untested_hotspots: bool = False min_typing_coverage: int = -1 min_docstring_coverage: int = -1 + coverage_min: int = 50 def _as_sorted_str_tuple(value: object) -> tuple[str, ...]: @@ -456,6 +468,18 @@ def bootstrap( ) +def _resolve_optional_runtime_path(value: object, *, root: Path) -> Path | None: + text = str(value).strip() if value is not None else "" + if not text: + return None + candidate = Path(text).expanduser() + resolved = candidate if candidate.is_absolute() else root / candidate + try: + return resolved.resolve() + except OSError: + return resolved.absolute() + + def _cache_entry_has_metrics(entry: CacheEntry) -> bool: metric_keys = ( "class_metrics", @@ -1029,6 +1053,62 @@ def process_file( ) +def _invoke_process_file( + filepath: str, + root: str, + cfg: NormalizationConfig, + min_loc: int, + min_stmt: int, + *, + collect_structural_findings: bool, + collect_typing_coverage: bool, + collect_docstring_coverage: bool, + collect_api_surface: bool, + api_include_private_modules: bool, + block_min_loc: int, + block_min_stmt: int, + segment_min_loc: int, + segment_min_stmt: int, +) -> FileProcessResult: + optional_kwargs: dict[str, object] = { + "collect_structural_findings": collect_structural_findings, + "collect_typing_coverage": collect_typing_coverage, + "collect_docstring_coverage": collect_docstring_coverage, + "collect_api_surface": collect_api_surface, + "api_include_private_modules": api_include_private_modules, + "block_min_loc": block_min_loc, + "block_min_stmt": block_min_stmt, + "segment_min_loc": segment_min_loc, + "segment_min_stmt": segment_min_stmt, + } + try: + signature = inspect.signature(process_file) + except (TypeError, ValueError): + supported_kwargs = optional_kwargs + else: + parameters = tuple(signature.parameters.values()) + if any( + parameter.kind == inspect.Parameter.VAR_KEYWORD for parameter in parameters + ): + supported_kwargs = optional_kwargs + else: + supported_names = {parameter.name for parameter in parameters} + supported_kwargs = { + key: value + for key, value in optional_kwargs.items() + if key in supported_names + } + process_callable = cast("Callable[..., FileProcessResult]", process_file) + return process_callable( + filepath, + root, + cfg, + min_loc, + min_stmt, + **supported_kwargs, + ) + + def process( *, boot: BootstrapResult, @@ -1215,50 +1295,27 @@ def _accept_result(result: FileProcessResult) -> None: def _run_sequential(files: Sequence[str]) -> None: for filepath in files: - _accept_result(_invoke_process_file(filepath)) + _accept_result( + _invoke_process_file( + filepath, + root_str, + boot.config, + min_loc, + min_stmt, + collect_structural_findings=collect_structural_findings, + collect_typing_coverage=collect_typing_coverage, + collect_docstring_coverage=collect_docstring_coverage, + collect_api_surface=collect_api_surface, + api_include_private_modules=api_include_private_modules, + block_min_loc=block_min_loc, + block_min_stmt=block_min_stmt, + segment_min_loc=segment_min_loc, + segment_min_stmt=segment_min_stmt, + ) + ) if on_advance is not None: on_advance() - def _invoke_process_file(filepath: str) -> FileProcessResult: - optional_kwargs: dict[str, object] = { - "collect_structural_findings": collect_structural_findings, - "collect_typing_coverage": collect_typing_coverage, - "collect_docstring_coverage": collect_docstring_coverage, - "collect_api_surface": collect_api_surface, - "api_include_private_modules": api_include_private_modules, - "block_min_loc": block_min_loc, - "block_min_stmt": block_min_stmt, - "segment_min_loc": segment_min_loc, - "segment_min_stmt": segment_min_stmt, - } - try: - signature = inspect.signature(process_file) - except (TypeError, ValueError): - supported_kwargs = optional_kwargs - else: - parameters = tuple(signature.parameters.values()) - if any( - parameter.kind == inspect.Parameter.VAR_KEYWORD - for parameter in parameters - ): - supported_kwargs = optional_kwargs - else: - supported_names = {parameter.name for parameter in parameters} - supported_kwargs = { - key: value - for key, value in optional_kwargs.items() - if key in supported_names - } - process_callable = cast("Callable[..., FileProcessResult]", process_file) - return process_callable( - filepath, - root_str, - boot.config, - min_loc, - min_stmt, - **supported_kwargs, - ) - if _should_use_parallel(len(files_to_process), processes): try: with ProcessPoolExecutor(max_workers=processes) as executor: @@ -1268,6 +1325,19 @@ def _invoke_process_file(filepath: str) -> FileProcessResult: executor.submit( _invoke_process_file, filepath, + root_str, + boot.config, + min_loc, + min_stmt, + collect_structural_findings=collect_structural_findings, + collect_typing_coverage=collect_typing_coverage, + collect_docstring_coverage=collect_docstring_coverage, + collect_api_surface=collect_api_surface, + api_include_private_modules=api_include_private_modules, + block_min_loc=block_min_loc, + block_min_stmt=block_min_stmt, + segment_min_loc=segment_min_loc, + segment_min_stmt=segment_min_stmt, ) for filepath in batch ] @@ -1531,6 +1601,91 @@ def _permille(numerator: int, denominator: int) -> int: return round((1000.0 * float(numerator)) / float(denominator)) +def _coverage_join_summary( + coverage_join: CoverageJoinResult | None, +) -> dict[str, object]: + if coverage_join is None: + return {} + return { + "status": coverage_join.status, + "source": coverage_join.coverage_xml, + "files": coverage_join.files, + "units": len(coverage_join.units), + "measured_units": coverage_join.measured_units, + "overall_executable_lines": coverage_join.overall_executable_lines, + "overall_covered_lines": coverage_join.overall_covered_lines, + "overall_permille": _permille( + coverage_join.overall_covered_lines, + coverage_join.overall_executable_lines, + ), + "missing_from_report_units": sum( + 1 + for fact in coverage_join.units + if fact.coverage_status == "missing_from_report" + ), + "coverage_hotspots": coverage_join.coverage_hotspots, + "scope_gap_hotspots": coverage_join.scope_gap_hotspots, + "hotspot_threshold_percent": coverage_join.hotspot_threshold_percent, + "invalid_reason": coverage_join.invalid_reason, + } + + +def _coverage_join_rows( + coverage_join: CoverageJoinResult | None, +) -> list[dict[str, object]]: + if coverage_join is None or coverage_join.status != "ok": + return [] + return sorted( + ( + { + "qualname": fact.qualname, + "filepath": fact.filepath, + "start_line": fact.start_line, + "end_line": fact.end_line, + "cyclomatic_complexity": fact.cyclomatic_complexity, + "risk": fact.risk, + "executable_lines": fact.executable_lines, + "covered_lines": fact.covered_lines, + "coverage_permille": fact.coverage_permille, + "coverage_status": fact.coverage_status, + "coverage_hotspot": ( + fact.risk in {"medium", "high"} + and fact.coverage_status == "measured" + and (fact.coverage_permille / 10.0) + < float(coverage_join.hotspot_threshold_percent) + ), + "scope_gap_hotspot": ( + fact.risk in {"medium", "high"} + and fact.coverage_status == "missing_from_report" + ), + "coverage_review_item": ( + ( + fact.risk in {"medium", "high"} + and fact.coverage_status == "measured" + and (fact.coverage_permille / 10.0) + < float(coverage_join.hotspot_threshold_percent) + ) + or ( + fact.risk in {"medium", "high"} + and fact.coverage_status == "missing_from_report" + ) + ), + } + for fact in coverage_join.units + ), + key=lambda item: ( + 0 if bool(item.get("coverage_hotspot")) else 1, + 0 if bool(item.get("scope_gap_hotspot")) else 1, + {"high": 0, "medium": 1, "low": 2}.get(_as_str(item.get("risk")), 3), + _as_int(item.get("coverage_permille"), 0), + -_as_int(item.get("cyclomatic_complexity"), 0), + _as_str(item.get("filepath")), + _as_int(item.get("start_line")), + _as_str(item.get("qualname")), + ), + ) + + def _coverage_adoption_rows( project_metrics: ProjectMetrics, ) -> list[dict[str, object]]: @@ -1699,6 +1854,8 @@ def _enrich_metrics_report_payload( *, metrics_payload: Mapping[str, object], metrics_diff: MetricsDiff | None, + coverage_adoption_diff_available: bool, + api_surface_diff_available: bool, ) -> dict[str, object]: enriched = { key: (dict(value) if isinstance(value, Mapping) else value) @@ -1711,20 +1868,20 @@ def _enrich_metrics_report_payload( cast("Mapping[str, object]", coverage_adoption.get("summary", {})) ) if coverage_summary: - coverage_summary["baseline_diff_available"] = metrics_diff is not None + coverage_summary["baseline_diff_available"] = coverage_adoption_diff_available coverage_summary["param_delta"] = ( int(metrics_diff.typing_param_permille_delta) - if metrics_diff is not None + if metrics_diff is not None and coverage_adoption_diff_available else 0 ) coverage_summary["return_delta"] = ( int(metrics_diff.typing_return_permille_delta) - if metrics_diff is not None + if metrics_diff is not None and coverage_adoption_diff_available else 0 ) coverage_summary["docstring_delta"] = ( int(metrics_diff.docstring_permille_delta) - if metrics_diff is not None + if metrics_diff is not None and coverage_adoption_diff_available else 0 ) coverage_adoption["summary"] = coverage_summary @@ -1734,17 +1891,23 @@ def _enrich_metrics_report_payload( api_summary = dict(cast("Mapping[str, object]", api_surface.get("summary", {}))) api_items = list(cast("Sequence[object]", api_surface.get("items", ()))) if api_summary: - api_summary["baseline_diff_available"] = metrics_diff is not None + api_summary["baseline_diff_available"] = api_surface_diff_available api_summary["added"] = ( - len(metrics_diff.new_api_symbols) if metrics_diff is not None else 0 + len(metrics_diff.new_api_symbols) + if metrics_diff is not None and api_surface_diff_available + else 0 ) api_summary["breaking"] = ( len(metrics_diff.new_api_breaking_changes) - if metrics_diff is not None + if metrics_diff is not None and api_surface_diff_available else 0 ) api_surface["summary"] = api_summary - if metrics_diff is not None and metrics_diff.new_api_breaking_changes: + if ( + metrics_diff is not None + and api_surface_diff_available + and metrics_diff.new_api_breaking_changes + ): api_items.extend( _breaking_api_surface_rows(metrics_diff.new_api_breaking_changes) ) @@ -1758,6 +1921,7 @@ def build_metrics_report_payload( *, scan_root: str = "", project_metrics: ProjectMetrics, + coverage_join: CoverageJoinResult | None = None, units: Sequence[GroupItemLike], class_metrics: Sequence[ClassMetrics], module_deps: Sequence[ModuleDep] = (), @@ -1820,6 +1984,8 @@ def build_metrics_report_payload( coverage_adoption_rows = _coverage_adoption_rows(project_metrics) api_surface_summary = _api_surface_summary(project_metrics.api_surface) api_surface_items = _api_surface_rows(project_metrics.api_surface) + coverage_join_summary = _coverage_join_summary(coverage_join) + coverage_join_items = _coverage_join_rows(coverage_join) def _serialize_dead_item( item: DeadItem, @@ -1843,7 +2009,7 @@ def _serialize_dead_item( ] return payload - return { + payload = { CATEGORY_COMPLEXITY: { "functions": complexity_rows, "summary": { @@ -1952,6 +2118,12 @@ def _serialize_dead_item( module_deps=module_deps, ), } + if coverage_join is not None: + payload["coverage_join"] = { + "summary": dict(coverage_join_summary), + "items": coverage_join_items, + } + return payload def analyze( @@ -1960,9 +2132,34 @@ def analyze( discovery: DiscoveryResult, processing: ProcessingResult, ) -> AnalysisResult: - func_groups = build_groups(processing.units) - block_groups = build_block_groups(processing.blocks) - segment_groups_raw = build_segment_groups(processing.segments) + golden_fixture_paths = tuple( + str(pattern).strip() + for pattern in getattr(boot.args, "golden_fixture_paths", ()) + if str(pattern).strip() + ) + + func_split = split_clone_groups_for_golden_fixtures( + groups=build_groups(processing.units), + kind="function", + golden_fixture_paths=golden_fixture_paths, + scan_root=str(boot.root), + ) + block_split = split_clone_groups_for_golden_fixtures( + groups=build_block_groups(processing.blocks), + kind="block", + golden_fixture_paths=golden_fixture_paths, + scan_root=str(boot.root), + ) + segment_split = split_clone_groups_for_golden_fixtures( + groups=build_segment_groups(processing.segments), + kind="segment", + golden_fixture_paths=golden_fixture_paths, + scan_root=str(boot.root), + ) + + func_groups = func_split.active_groups + block_groups = block_split.active_groups + segment_groups_raw = segment_split.active_groups segment_groups_raw_digest = _segment_groups_digest(segment_groups_raw) cached_projection = discovery.cached_segment_report_projection if ( @@ -1992,7 +2189,38 @@ def analyze( ) block_groups_report = prepare_block_report_groups(block_groups) - block_group_facts = build_block_group_facts(block_groups_report) + suppressed_block_groups_report = prepare_block_report_groups( + block_split.suppressed_groups + ) + if segment_split.suppressed_groups: + suppressed_segment_groups_report, _ = prepare_segment_report_groups( + segment_split.suppressed_groups + ) + else: + suppressed_segment_groups_report = {} + suppressed_clone_groups = ( + *build_suppressed_clone_groups( + kind="function", + groups=func_split.suppressed_groups, + matched_patterns=func_split.matched_patterns, + ), + *build_suppressed_clone_groups( + kind="block", + groups=suppressed_block_groups_report, + matched_patterns=block_split.matched_patterns, + ), + *build_suppressed_clone_groups( + kind="segment", + groups=suppressed_segment_groups_report, + matched_patterns=segment_split.matched_patterns, + ), + ) + block_group_facts = build_block_group_facts( + { + **block_groups_report, + **suppressed_block_groups_report, + } + ) func_clones_count = len(func_groups) block_clones_count = len(block_groups) @@ -2003,6 +2231,7 @@ def analyze( metrics_payload: dict[str, object] | None = None suggestions: tuple[Suggestion, ...] = () suppressed_dead_items: tuple[DeadItem, ...] = () + coverage_join: CoverageJoinResult | None = None cohort_structural_findings: tuple[StructuralFindingGroup, ...] = () if _should_collect_structural_findings(boot.output_paths): cohort_structural_findings = build_clone_cohort_structural_findings( @@ -2048,9 +2277,33 @@ def analyze( structural_findings=combined_structural_findings, scan_root=str(boot.root), ) + coverage_xml_path = _resolve_optional_runtime_path( + getattr(boot.args, "coverage_xml", None), + root=boot.root, + ) + if coverage_xml_path is not None: + try: + coverage_join = build_coverage_join( + coverage_xml=coverage_xml_path, + root_path=boot.root, + units=processing.units, + hotspot_threshold_percent=int( + getattr(boot.args, "coverage_min", 50) + ), + ) + except CoverageJoinParseError as exc: + coverage_join = CoverageJoinResult( + coverage_xml=str(coverage_xml_path), + status="invalid", + hotspot_threshold_percent=int( + getattr(boot.args, "coverage_min", 50) + ), + invalid_reason=str(exc), + ) metrics_payload = build_metrics_report_payload( scan_root=str(boot.root), project_metrics=project_metrics, + coverage_join=coverage_join, units=processing.units, class_metrics=processing.class_metrics, module_deps=processing.module_deps, @@ -2063,6 +2316,7 @@ def analyze( block_groups=block_groups, block_groups_report=block_groups_report, segment_groups=segment_groups, + suppressed_clone_groups=tuple(suppressed_clone_groups), suppressed_segment_groups=suppressed_segment_groups, block_group_facts=block_group_facts, func_clones_count=func_clones_count, @@ -2073,6 +2327,7 @@ def analyze( metrics_payload=metrics_payload, suggestions=suggestions, segment_groups_raw_digest=segment_groups_raw_digest, + coverage_join=coverage_join, suppressed_dead_code_items=len(suppressed_dead_items), structural_findings=combined_structural_findings, ) @@ -2101,6 +2356,8 @@ def report( new_block: Collection[str], html_builder: Callable[..., str] | None = None, metrics_diff: object | None = None, + coverage_adoption_diff_available: bool = False, + api_surface_diff_available: bool = False, include_report_document: bool = False, ) -> ReportArtifacts: contents: dict[str, str | None] = { @@ -2148,6 +2405,8 @@ def report( _enrich_metrics_report_payload( metrics_payload=analysis.metrics_payload, metrics_diff=cast("MetricsDiff | None", metrics_diff), + coverage_adoption_diff_available=coverage_adoption_diff_available, + api_surface_diff_available=api_surface_diff_available, ) if analysis.metrics_payload is not None else None @@ -2156,6 +2415,7 @@ def report( func_groups=analysis.func_groups, block_groups=analysis.block_groups_report, segment_groups=analysis.segment_groups, + suppressed_clone_groups=analysis.suppressed_clone_groups, meta=report_meta, inventory=report_inventory, block_facts=analysis.block_group_facts, @@ -2172,6 +2432,8 @@ def report( _enrich_metrics_report_payload( metrics_payload=analysis.metrics_payload, metrics_diff=cast("MetricsDiff | None", metrics_diff), + coverage_adoption_diff_available=coverage_adoption_diff_available, + api_surface_diff_available=api_surface_diff_available, ) if analysis.metrics_payload is not None else None @@ -2251,6 +2513,7 @@ def _render_projection_artifact( def metric_gate_reasons( *, project_metrics: ProjectMetrics, + coverage_join: CoverageJoinResult | None, metrics_diff: MetricsDiff | None, config: MetricGateConfig, ) -> tuple[str, ...]: @@ -2271,6 +2534,11 @@ def metric_gate_reasons( project_metrics=project_metrics, config=config, ) + _append_coverage_join_reasons( + reasons=reasons, + coverage_join=coverage_join, + config=config, + ) return tuple(reasons) @@ -2441,6 +2709,24 @@ def _append_adoption_metric_reasons( ) +def _append_coverage_join_reasons( + *, + reasons: list[str], + coverage_join: CoverageJoinResult | None, + config: MetricGateConfig, +) -> None: + if not config.fail_on_untested_hotspots or coverage_join is None: + return + if coverage_join.status != "ok": + return + if coverage_join.coverage_hotspots > 0: + reasons.append( + "Coverage hotspots detected: " + f"hotspots={coverage_join.coverage_hotspots}, " + f"threshold={config.coverage_min}%." + ) + + def _high_confidence_dead_code_count(items: Sequence[DeadItem]) -> int: return sum(1 for item in items if item.confidence == "high") @@ -2458,6 +2744,7 @@ def gate( if analysis.project_metrics is not None: metric_reasons = metric_gate_reasons( project_metrics=analysis.project_metrics, + coverage_join=analysis.coverage_join, metrics_diff=metrics_diff, config=MetricGateConfig( fail_complexity=boot.args.fail_complexity, @@ -2474,10 +2761,14 @@ def gate( getattr(boot.args, "fail_on_docstring_regression", False) ), fail_on_api_break=bool(getattr(boot.args, "fail_on_api_break", False)), + fail_on_untested_hotspots=bool( + getattr(boot.args, "fail_on_untested_hotspots", False) + ), min_typing_coverage=int(getattr(boot.args, "min_typing_coverage", -1)), min_docstring_coverage=int( getattr(boot.args, "min_docstring_coverage", -1) ), + coverage_min=int(getattr(boot.args, "coverage_min", 50)), ), ) reasons.extend(f"metric:{reason}" for reason in metric_reasons) diff --git a/codeclone/report/derived.py b/codeclone/report/derived.py index a3de937..6873a08 100644 --- a/codeclone/report/derived.py +++ b/codeclone/report/derived.py @@ -25,6 +25,12 @@ SOURCE_KIND_ORDER as _SOURCE_KIND_ORDER, ) from ..models import ReportLocation, SourceKind, StructuralFindingOccurrence +from ..paths import ( + classify_source_kind as _classify_source_kind, +) +from ..paths import ( + relative_repo_path as _relative_repo_path, +) if TYPE_CHECKING: from collections.abc import Iterable, Mapping, Sequence @@ -56,37 +62,19 @@ } -def _normalize_path(value: str) -> str: - return value.replace("\\", "/").strip() - - def relative_report_path(filepath: str, *, scan_root: str = "") -> str: - normalized_path = _normalize_path(filepath) - normalized_root = _normalize_path(scan_root).rstrip("/") - if not normalized_path: - return normalized_path - if not normalized_root: - return normalized_path - prefix = f"{normalized_root}/" - if normalized_path.startswith(prefix): - return normalized_path[len(prefix) :] - if normalized_path == normalized_root: - return normalized_path.rsplit("/", maxsplit=1)[-1] - return normalized_path + return _relative_repo_path(filepath, scan_root=scan_root) def classify_source_kind(filepath: str, *, scan_root: str = "") -> SourceKind: - rel = relative_report_path(filepath, scan_root=scan_root) - parts = [part for part in rel.lower().split("/") if part and part != "."] - if not parts: - return SOURCE_KIND_OTHER - for idx, part in enumerate(parts): - if part != SOURCE_KIND_TESTS: - continue - if idx + 1 < len(parts) and parts[idx + 1] == SOURCE_KIND_FIXTURES: - return SOURCE_KIND_FIXTURES + normalized = _classify_source_kind(filepath, scan_root=scan_root) + if normalized == SOURCE_KIND_PRODUCTION: + return SOURCE_KIND_PRODUCTION + if normalized == SOURCE_KIND_TESTS: return SOURCE_KIND_TESTS - return SOURCE_KIND_PRODUCTION + if normalized == SOURCE_KIND_FIXTURES: + return SOURCE_KIND_FIXTURES + return SOURCE_KIND_OTHER def source_kind_breakdown( diff --git a/codeclone/report/json_contract.py b/codeclone/report/json_contract.py index 4e09395..decfeb6 100644 --- a/codeclone/report/json_contract.py +++ b/codeclone/report/json_contract.py @@ -27,6 +27,7 @@ CATEGORY_COHESION, CATEGORY_COMPLEXITY, CATEGORY_COUPLING, + CATEGORY_COVERAGE, CATEGORY_DEAD_CODE, CATEGORY_DEPENDENCY, CLONE_KIND_BLOCK, @@ -39,6 +40,8 @@ FAMILY_DEAD_CODE, FAMILY_DESIGN, FAMILY_STRUCTURAL, + FINDING_KIND_COVERAGE_HOTSPOT, + FINDING_KIND_COVERAGE_SCOPE_GAP, ) from ..domain.quality import ( CONFIDENCE_HIGH, @@ -91,6 +94,7 @@ SourceKind, StructuralFindingGroup, Suggestion, + SuppressedCloneGroup, ) __all__ = [ @@ -104,6 +108,7 @@ _OVERLOADED_MODULES_FAMILY = "overloaded_modules" _COVERAGE_ADOPTION_FAMILY = "coverage_adoption" _API_SURFACE_FAMILY = "api_surface" +_COVERAGE_JOIN_FAMILY = "coverage_join" def _optional_str(value: object) -> str | None: @@ -390,6 +395,12 @@ def _collect_paths_from_metrics(metrics: Mapping[str, object]) -> set[str]: filepath = _optional_str(item_map.get("filepath")) if filepath is not None: paths.add(filepath) + coverage_join = _as_mapping(metrics.get(_COVERAGE_JOIN_FAMILY)) + for item in _as_sequence(coverage_join.get("items")): + item_map = _as_mapping(item) + filepath = _optional_str(item_map.get("filepath")) + if filepath is not None: + paths.add(filepath) return paths @@ -399,6 +410,7 @@ def _collect_report_file_list( func_groups: GroupMapLike, block_groups: GroupMapLike, segment_groups: GroupMapLike, + suppressed_clone_groups: Sequence[SuppressedCloneGroup] | None = None, metrics: Mapping[str, object] | None, structural_findings: Sequence[StructuralFindingGroup] | None, ) -> list[str]: @@ -414,11 +426,16 @@ def _collect_report_file_list( filepath = _optional_str(item.get("filepath")) if filepath is not None: files.add(filepath) + for suppressed_group in suppressed_clone_groups or (): + for item in suppressed_group.items: + filepath = _optional_str(item.get("filepath")) + if filepath is not None: + files.add(filepath) if metrics is not None: files.update(_collect_paths_from_metrics(metrics)) if structural_findings: - for group in normalize_structural_findings(structural_findings): - for occurrence in group.items: + for structural_group in normalize_structural_findings(structural_findings): + for occurrence in structural_group.items: filepath = _optional_str(occurrence.file_path) if filepath is not None: files.add(filepath) @@ -813,6 +830,45 @@ def _normalize_suppressed_by( item["record_kind"], ), ) + coverage_join = _as_mapping(metrics_map.get(_COVERAGE_JOIN_FAMILY)) + coverage_join_summary = _as_mapping(coverage_join.get("summary")) + coverage_join_items = sorted( + ( + { + "relative_path": _contract_path( + item_map.get("filepath", ""), + scan_root=scan_root, + )[0] + or "", + "qualname": str(item_map.get("qualname", "")).strip(), + "start_line": _as_int(item_map.get("start_line")), + "end_line": _as_int(item_map.get("end_line")), + "cyclomatic_complexity": _as_int( + item_map.get("cyclomatic_complexity"), + 1, + ), + "risk": str(item_map.get("risk", RISK_LOW)).strip() or RISK_LOW, + "executable_lines": _as_int(item_map.get("executable_lines")), + "covered_lines": _as_int(item_map.get("covered_lines")), + "coverage_permille": _as_int(item_map.get("coverage_permille")), + "coverage_status": str(item_map.get("coverage_status", "")).strip(), + "coverage_hotspot": bool(item_map.get("coverage_hotspot")), + "scope_gap_hotspot": bool(item_map.get("scope_gap_hotspot")), + } + for item in _as_sequence(coverage_join.get("items")) + for item_map in (_as_mapping(item),) + ), + key=lambda item: ( + 0 if bool(item["coverage_hotspot"]) else 1, + 0 if bool(item["scope_gap_hotspot"]) else 1, + {"high": 0, "medium": 1, "low": 2}.get(str(item["risk"]), 3), + _as_int(item["coverage_permille"]), + -_as_int(item["cyclomatic_complexity"]), + item["relative_path"], + _as_int(item["start_line"]), + item["qualname"], + ), + ) dead_high_confidence = sum( 1 for item in dead_items @@ -1004,6 +1060,45 @@ def _normalize_suppressed_by( "items_truncated": False, }, } + if coverage_join_summary or coverage_join_items or coverage_join: + normalized[_COVERAGE_JOIN_FAMILY] = { + "summary": { + "status": str(coverage_join_summary.get("status", "")), + "source": _contract_path( + coverage_join_summary.get("source", ""), + scan_root=scan_root, + )[0], + "files": _as_int(coverage_join_summary.get("files")), + "units": _as_int(coverage_join_summary.get("units")), + "measured_units": _as_int(coverage_join_summary.get("measured_units")), + "overall_executable_lines": _as_int( + coverage_join_summary.get("overall_executable_lines") + ), + "overall_covered_lines": _as_int( + coverage_join_summary.get("overall_covered_lines") + ), + "overall_permille": _as_int( + coverage_join_summary.get("overall_permille") + ), + "missing_from_report_units": _as_int( + coverage_join_summary.get("missing_from_report_units") + ), + "coverage_hotspots": _as_int( + coverage_join_summary.get("coverage_hotspots") + ), + "scope_gap_hotspots": _as_int( + coverage_join_summary.get("scope_gap_hotspots") + ), + "hotspot_threshold_percent": _as_int( + coverage_join_summary.get("hotspot_threshold_percent") + ), + "invalid_reason": _optional_str( + coverage_join_summary.get("invalid_reason") + ), + }, + "items": coverage_join_items, + "items_truncated": False, + } return normalized @@ -1392,6 +1487,83 @@ def _build_clone_groups( return encoded_groups +def _build_suppressed_clone_groups( + *, + groups: Sequence[SuppressedCloneGroup] | None, + block_facts: Mapping[str, Mapping[str, str]], + scan_root: str, +) -> dict[str, list[dict[str, object]]]: + buckets: dict[str, list[dict[str, object]]] = { + CLONE_KIND_FUNCTION: [], + CLONE_KIND_BLOCK: [], + CLONE_KIND_SEGMENT: [], + } + for group in groups or (): + items = group.items + clone_type = classify_clone_type(items=items, kind=group.kind) + severity, priority = _clone_group_assessment( + count=len(items), + clone_type=clone_type, + ) + locations = tuple( + report_location_from_group_item(item, scan_root=scan_root) for item in items + ) + source_scope = _source_scope_from_locations( + [ + { + "source_kind": location.source_kind, + } + for location in locations + ] + ) + spread_files, spread_functions = group_spread(locations) + rows = sorted( + [ + _clone_item_payload( + item, + kind=group.kind, + scan_root=scan_root, + ) + for item in items + ], + key=_item_sort_key, + ) + facts, display_facts = _build_clone_group_facts( + group_key=group.group_key, + kind=group.kind, + items=items, + block_facts=block_facts, + ) + encoded: dict[str, object] = { + "id": clone_group_id(group.kind, group.group_key), + "family": FAMILY_CLONE, + "category": group.kind, + "kind": "clone_group", + "severity": severity, + "confidence": CONFIDENCE_HIGH, + "priority": priority, + "clone_kind": group.kind, + "clone_type": clone_type, + "count": len(items), + "source_scope": source_scope, + "spread": { + "files": spread_files, + "functions": spread_functions, + }, + "items": rows, + "facts": facts, + "suppression_rule": group.suppression_rule, + "suppression_source": group.suppression_source, + "matched_patterns": list(group.matched_patterns), + } + if display_facts: + encoded["display_facts"] = display_facts + buckets[group.kind].append(encoded) + for bucket in buckets.values(): + bucket.sort(key=lambda group: (-_as_int(group.get("count")), str(group["id"]))) + return buckets + + def _structural_group_assessment( *, finding_kind: str, @@ -1878,6 +2050,80 @@ def _dependency_design_group( } +def _coverage_design_group( + item_map: Mapping[str, object], + *, + threshold_percent: int, + scan_root: str, +) -> dict[str, object] | None: + coverage_hotspot = bool(item_map.get("coverage_hotspot")) + scope_gap_hotspot = bool(item_map.get("scope_gap_hotspot")) + if not coverage_hotspot and not scope_gap_hotspot: + return None + qualname = str(item_map.get("qualname", "")).strip() + filepath = str(item_map.get("relative_path", "")).strip() + if not filepath: + return None + start_line = _as_int(item_map.get("start_line")) + end_line = _as_int(item_map.get("end_line")) + subject_key = qualname or f"{filepath}:{start_line}:{end_line}" + risk = str(item_map.get("risk", RISK_LOW)).strip() or RISK_LOW + coverage_status = str(item_map.get("coverage_status", "")).strip() + coverage_permille = _as_int(item_map.get("coverage_permille")) + covered_lines = _as_int(item_map.get("covered_lines")) + executable_lines = _as_int(item_map.get("executable_lines")) + complexity = _as_int(item_map.get("cyclomatic_complexity"), 1) + severity = SEVERITY_CRITICAL if risk == "high" else SEVERITY_WARNING + if scope_gap_hotspot: + kind = FINDING_KIND_COVERAGE_SCOPE_GAP + detail = "The supplied coverage.xml did not map to this function's file." + else: + kind = FINDING_KIND_COVERAGE_HOTSPOT + detail = "Joined line coverage is below the configured hotspot threshold." + return { + "id": design_group_id(CATEGORY_COVERAGE, subject_key), + "family": FAMILY_DESIGN, + "category": CATEGORY_COVERAGE, + "kind": kind, + "severity": severity, + "confidence": CONFIDENCE_HIGH, + "priority": _priority(severity, EFFORT_MODERATE), + "count": 1, + "source_scope": _single_location_source_scope( + filepath, + scan_root=scan_root, + ), + "spread": {"files": 1, "functions": 1}, + "items": [ + { + "relative_path": filepath, + "qualname": qualname, + "start_line": start_line, + "end_line": end_line, + "risk": risk, + "cyclomatic_complexity": complexity, + "coverage_permille": coverage_permille, + "coverage_status": coverage_status, + "covered_lines": covered_lines, + "executable_lines": executable_lines, + "coverage_hotspot": coverage_hotspot, + "scope_gap_hotspot": scope_gap_hotspot, + } + ], + "facts": { + "coverage_permille": coverage_permille, + "hotspot_threshold_percent": threshold_percent, + "coverage_status": coverage_status, + "covered_lines": covered_lines, + "executable_lines": executable_lines, + "cyclomatic_complexity": complexity, + "coverage_hotspot": coverage_hotspot, + "scope_gap_hotspot": scope_gap_hotspot, + "detail": detail, + }, + } + + def _build_design_groups( metrics_payload: Mapping[str, object], *, @@ -1898,6 +2144,11 @@ def _build_design_groups( _as_mapping(thresholds.get(CATEGORY_COHESION)).get("value"), default=DEFAULT_REPORT_DESIGN_COHESION_THRESHOLD, ) + coverage_join = _as_mapping(families.get(_COVERAGE_JOIN_FAMILY)) + coverage_threshold = _as_int( + _as_mapping(coverage_join.get("summary")).get("hotspot_threshold_percent"), + 50, + ) groups: list[dict[str, object]] = [] complexity = _as_mapping(families.get(CATEGORY_COMPLEXITY)) @@ -1936,6 +2187,15 @@ def _build_design_groups( if group is not None: groups.append(group) + for item in _as_sequence(coverage_join.get("items")): + group = _coverage_design_group( + _as_mapping(item), + threshold_percent=coverage_threshold, + scan_root=scan_root, + ) + if group is not None: + groups.append(group) + groups.sort(key=lambda group: (-_as_float(group["priority"]), str(group["id"]))) return groups @@ -1948,6 +2208,7 @@ def _findings_summary( structural_groups: Sequence[Mapping[str, object]], dead_code_groups: Sequence[Mapping[str, object]], design_groups: Sequence[Mapping[str, object]], + suppressed_clone_groups: Mapping[str, Sequence[Mapping[str, object]]] | None = None, dead_code_suppressed: int = 0, ) -> dict[str, object]: flat_groups = [ @@ -1979,6 +2240,42 @@ def _findings_summary( if impact_scope in source_scope_counts: source_scope_counts[impact_scope] += 1 clone_groups = [*clone_functions, *clone_blocks, *clone_segments] + clone_suppressed_map = _as_mapping(suppressed_clone_groups) + suppressed_functions = len(_as_sequence(clone_suppressed_map.get("function"))) + suppressed_blocks = len(_as_sequence(clone_suppressed_map.get("block"))) + suppressed_segments = len(_as_sequence(clone_suppressed_map.get("segment"))) + suppressed_clone_total = ( + suppressed_functions + suppressed_blocks + suppressed_segments + ) + clones_summary: dict[str, object] = { + "functions": len(clone_functions), + "blocks": len(clone_blocks), + "segments": len(clone_segments), + CLONE_NOVELTY_NEW: sum( + 1 + for group in clone_groups + if str(group.get("novelty", "")) == CLONE_NOVELTY_NEW + ), + CLONE_NOVELTY_KNOWN: sum( + 1 + for group in clone_groups + if str(group.get("novelty", "")) == CLONE_NOVELTY_KNOWN + ), + } + if suppressed_clone_total > 0: + clones_summary.update( + { + "suppressed": suppressed_clone_total, + "suppressed_functions": suppressed_functions, + "suppressed_blocks": suppressed_blocks, + "suppressed_segments": suppressed_segments, + } + ) + suppressed_summary = { + FAMILY_DEAD_CODE: max(0, dead_code_suppressed), + } + if suppressed_clone_total > 0: + suppressed_summary[FAMILY_CLONES] = suppressed_clone_total return { "total": len(flat_groups), "families": { @@ -1989,24 +2286,8 @@ def _findings_summary( }, "severity": severity_counts, "impact_scope": source_scope_counts, - "clones": { - "functions": len(clone_functions), - "blocks": len(clone_blocks), - "segments": len(clone_segments), - CLONE_NOVELTY_NEW: sum( - 1 - for group in clone_groups - if str(group.get("novelty", "")) == CLONE_NOVELTY_NEW - ), - CLONE_NOVELTY_KNOWN: sum( - 1 - for group in clone_groups - if str(group.get("novelty", "")) == CLONE_NOVELTY_KNOWN - ), - }, - "suppressed": { - FAMILY_DEAD_CODE: max(0, dead_code_suppressed), - }, + "clones": clones_summary, + "suppressed": suppressed_summary, } @@ -2389,6 +2670,7 @@ def _build_findings_payload( new_function_group_keys: Collection[str] | None, new_block_group_keys: Collection[str] | None, new_segment_group_keys: Collection[str] | None, + suppressed_clone_groups: Sequence[SuppressedCloneGroup] | None, design_thresholds: Mapping[str, object] | None, scan_root: str, ) -> dict[str, object]: @@ -2439,6 +2721,22 @@ def _build_findings_payload( design_thresholds=design_thresholds, scan_root=scan_root, ) + suppressed_clone_payload = _build_suppressed_clone_groups( + groups=suppressed_clone_groups, + block_facts=block_facts, + scan_root=scan_root, + ) + clone_groups_payload: dict[str, object] = { + "functions": clone_functions, + "blocks": clone_blocks, + "segments": clone_segments, + } + if any(suppressed_clone_payload.values()): + clone_groups_payload["suppressed"] = { + "functions": suppressed_clone_payload[CLONE_KIND_FUNCTION], + "blocks": suppressed_clone_payload[CLONE_KIND_BLOCK], + "segments": suppressed_clone_payload[CLONE_KIND_SEGMENT], + } return { "summary": _findings_summary( clone_functions=clone_functions, @@ -2447,14 +2745,11 @@ def _build_findings_payload( structural_groups=structural_groups, dead_code_groups=dead_code_groups, design_groups=design_groups, + suppressed_clone_groups=suppressed_clone_payload, dead_code_suppressed=dead_code_suppressed, ), "groups": { - FAMILY_CLONES: { - "functions": clone_functions, - "blocks": clone_blocks, - "segments": clone_segments, - }, + FAMILY_CLONES: clone_groups_payload, FAMILY_STRUCTURAL: { "groups": structural_groups, }, @@ -2554,6 +2849,7 @@ def build_report_document( new_function_group_keys: Collection[str] | None = None, new_block_group_keys: Collection[str] | None = None, new_segment_group_keys: Collection[str] | None = None, + suppressed_clone_groups: Sequence[SuppressedCloneGroup] | None = None, metrics: Mapping[str, object] | None = None, suggestions: Sequence[Suggestion] | None = None, structural_findings: Sequence[StructuralFindingGroup] | None = None, @@ -2570,6 +2866,7 @@ def build_report_document( func_groups=func_groups, block_groups=block_groups, segment_groups=segment_groups, + suppressed_clone_groups=suppressed_clone_groups, metrics=metrics, structural_findings=structural_findings, ) @@ -2590,6 +2887,7 @@ def build_report_document( new_function_group_keys=new_function_group_keys, new_block_group_keys=new_block_group_keys, new_segment_group_keys=new_segment_group_keys, + suppressed_clone_groups=suppressed_clone_groups, design_thresholds=design_thresholds, scan_root=scan_root, ) diff --git a/codeclone/report/markdown.py b/codeclone/report/markdown.py index 9c5fc0a..6ad5c2e 100644 --- a/codeclone/report/markdown.py +++ b/codeclone/report/markdown.py @@ -15,7 +15,7 @@ from .json_contract import build_report_document if TYPE_CHECKING: - from ..models import StructuralFindingGroup, Suggestion + from ..models import StructuralFindingGroup, Suggestion, SuppressedCloneGroup from .types import GroupMapLike MARKDOWN_SCHEMA_VERSION = "1.0" @@ -43,6 +43,7 @@ ("complexity", "Complexity", 3), ("coupling", "Coupling", 3), ("cohesion", "Cohesion", 3), + ("coverage-join", "Coverage Join", 3), ("overloaded-modules", "Overloaded Modules", 3), ("dependencies", "Dependencies", 3), ("dead-code-metrics", "Dead Code", 3), @@ -183,6 +184,59 @@ def _append_findings_section( lines.append("") +def _append_suppressed_clone_findings( + lines: list[str], + *, + groups: Sequence[object], +) -> None: + finding_rows = [_as_mapping(group) for group in groups] + if not finding_rows: + lines.append("_None._") + lines.append("") + return + for group in finding_rows: + lines.append("#### Suppressed clone group") + lines.append("") + _append_kv_bullets( + lines, + ( + ("Finding ID", f"`{_text(group.get('id'))}`"), + ("Category", group.get("category")), + ("Clone Type", group.get("clone_type")), + ("Severity", group.get("severity")), + ("Scope", _source_scope_text(_as_mapping(group.get("source_scope")))), + ("Spread", _spread_text(_as_mapping(group.get("spread")))), + ("Occurrences", group.get("count")), + ("Suppression Rule", group.get("suppression_rule")), + ("Suppression Source", group.get("suppression_source")), + ( + "Matched Patterns", + ", ".join( + str(item).strip() + for item in _as_sequence(group.get("matched_patterns")) + if str(item).strip() + ) + or "(none)", + ), + ), + ) + facts = _as_mapping(group.get("facts")) + display_facts = _as_mapping(group.get("display_facts")) + if facts or display_facts: + _append_facts_block(lines, title="Facts", facts=facts) + _append_facts_block(lines, title="Presentation facts", facts=display_facts) + lines.append("") + items = list(map(_as_mapping, _as_sequence(group.get("items")))) + lines.append("- Locations:") + visible_items = items[:_MAX_FINDING_LOCATIONS] + lines.extend(f" - {_location_text(item)}" for item in visible_items) + if len(items) > len(visible_items): + lines.append( + f" - ... and {len(items) - len(visible_items)} more occurrence(s)" + ) + lines.append("") + + def _append_metric_items( lines: list[str], *, @@ -215,6 +269,7 @@ def render_markdown_report_document(payload: Mapping[str, object]) -> str: findings_summary = _as_mapping(findings.get("summary")) findings_groups = _as_mapping(findings.get("groups")) clone_groups = _as_mapping(findings_groups.get("clones")) + suppressed_clone_groups = _as_mapping(clone_groups.get("suppressed")) overview = _as_mapping(derived.get("overview")) hotlists = _as_mapping(derived.get("hotlists")) suggestions = _as_sequence(derived.get("suggestions")) @@ -390,6 +445,17 @@ def render_markdown_report_document(payload: Mapping[str, object]) -> str: *_as_sequence(clone_groups.get("segments")), ], ) + if suppressed_clone_groups: + lines.append("#### Suppressed Golden Fixture Clone Groups") + lines.append("") + _append_suppressed_clone_findings( + lines, + groups=[ + *_as_sequence(suppressed_clone_groups.get("functions")), + *_as_sequence(suppressed_clone_groups.get("blocks")), + *_as_sequence(suppressed_clone_groups.get("segments")), + ], + ) _append_anchor(lines, *_anchor("structural-findings")) _append_findings_section( @@ -434,6 +500,30 @@ def render_markdown_report_document(payload: Mapping[str, object]) -> str: ("total", "average", "max", "low_cohesion"), ("lcom4", "method_count", "instance_var_count", "risk"), ), + ( + "coverage-join", + "Coverage Join", + ( + "status", + "source", + "units", + "measured_units", + "overall_permille", + "coverage_hotspots", + "scope_gap_hotspots", + "hotspot_threshold_percent", + ), + ( + "coverage_status", + "risk", + "coverage_permille", + "cyclomatic_complexity", + "covered_lines", + "executable_lines", + "coverage_hotspot", + "scope_gap_hotspot", + ), + ), ( "overloaded-modules", "Overloaded Modules", @@ -474,9 +564,13 @@ def render_markdown_report_document(payload: Mapping[str, object]) -> str: "overloaded_modules" if anchor_id == "overloaded-modules" else anchor_id ) ) + if family_key == "coverage-join": + family_key = "coverage_join" family_payload = _as_mapping(metrics_families.get(family_key)) if not family_payload and family_key == "overloaded_modules": family_payload = _as_mapping(metrics_families.get("god_modules")) + if not family_payload and family_key == "coverage_join": + continue family_summary_map = _as_mapping(family_payload.get("summary")) _append_anchor(lines, anchor_id, title, 3) _append_kv_bullets( @@ -542,6 +636,7 @@ def to_markdown_report( new_function_group_keys: Collection[str] | None = None, new_block_group_keys: Collection[str] | None = None, new_segment_group_keys: Collection[str] | None = None, + suppressed_clone_groups: Sequence[SuppressedCloneGroup] | None = None, metrics: Mapping[str, object] | None = None, suggestions: Collection[Suggestion] | None = None, structural_findings: Sequence[StructuralFindingGroup] | None = None, @@ -556,6 +651,7 @@ def to_markdown_report( new_function_group_keys=new_function_group_keys, new_block_group_keys=new_block_group_keys, new_segment_group_keys=new_segment_group_keys, + suppressed_clone_groups=suppressed_clone_groups, metrics=metrics, suggestions=tuple(suggestions or ()), structural_findings=tuple(structural_findings or ()), diff --git a/codeclone/report/overview.py b/codeclone/report/overview.py index cc0efda..c8c4a3a 100644 --- a/codeclone/report/overview.py +++ b/codeclone/report/overview.py @@ -18,6 +18,7 @@ CATEGORY_COHESION, CATEGORY_COMPLEXITY, CATEGORY_COUPLING, + CATEGORY_COVERAGE, CATEGORY_DEAD_CODE, CATEGORY_DEPENDENCY, CLONE_KIND_BLOCK, @@ -119,6 +120,7 @@ def _flatten_findings(findings: Mapping[str, object]) -> list[Mapping[str, objec CATEGORY_COMPLEXITY, CATEGORY_COUPLING, CATEGORY_COHESION, + CATEGORY_COVERAGE, CATEGORY_DEPENDENCY, ) @@ -155,6 +157,7 @@ def _directory_kind_breakdown_key(group: Mapping[str, object]) -> str | None: CATEGORY_COMPLEXITY, CATEGORY_COUPLING, CATEGORY_COHESION, + CATEGORY_COVERAGE, CATEGORY_DEPENDENCY, }: return category @@ -504,6 +507,19 @@ def serialize_finding_group_card(group: Mapping[str, object]) -> dict[str, objec _as_int(group.get("count")), ) summary = f"{cycle_length} modules participate in this cycle" + elif category == CATEGORY_COVERAGE: + kind = str(group.get("kind", "")).strip() + coverage_status = str(facts.get("coverage_status", "")).strip() + threshold = _as_int(facts.get("hotspot_threshold_percent")) + if kind == "coverage_scope_gap" or coverage_status == "missing_from_report": + title = "Include risky function in coverage input" + summary = "coverage.xml did not include this function's file" + else: + title = "Increase coverage for risky function" + summary = ( + f"coverage={_as_int(facts.get('coverage_permille')) / 10.0:.1f}%, " + f"threshold={threshold}%" + ) return { "title": title, diff --git a/codeclone/report/sarif.py b/codeclone/report/sarif.py index e316553..ec2177d 100644 --- a/codeclone/report/sarif.py +++ b/codeclone/report/sarif.py @@ -23,6 +23,7 @@ CATEGORY_COHESION, CATEGORY_COMPLEXITY, CATEGORY_COUPLING, + CATEGORY_COVERAGE, CATEGORY_DEPENDENCY, CLONE_KIND_BLOCK, CLONE_KIND_FUNCTION, @@ -33,6 +34,8 @@ FAMILY_STRUCTURAL, FINDING_KIND_CLASS_HOTSPOT, FINDING_KIND_CLONE_GROUP, + FINDING_KIND_COVERAGE_HOTSPOT, + FINDING_KIND_COVERAGE_SCOPE_GAP, FINDING_KIND_CYCLE, FINDING_KIND_FUNCTION_HOTSPOT, FINDING_KIND_UNUSED_SYMBOL, @@ -346,6 +349,28 @@ def _design_rule_spec(category: str, kind: str) -> _RuleSpec: kind or FINDING_KIND_CLASS_HOTSPOT, CONFIDENCE_HIGH, ) + if category == CATEGORY_COVERAGE: + if kind == FINDING_KIND_COVERAGE_SCOPE_GAP: + return _RuleSpec( + "CDESIGN006", + "Coverage scope gap", + "A medium/high-risk function is outside the supplied joined " + "coverage scope.", + SEVERITY_WARNING, + FAMILY_DESIGN, + kind, + CONFIDENCE_HIGH, + ) + return _RuleSpec( + "CDESIGN005", + "Coverage hotspot", + "A medium/high-risk function falls below the configured joined " + "coverage threshold.", + SEVERITY_WARNING, + FAMILY_DESIGN, + kind or FINDING_KIND_COVERAGE_HOTSPOT, + CONFIDENCE_HIGH, + ) return _RuleSpec( "CDESIGN004", "Dependency cycle", @@ -452,6 +477,13 @@ def _design_result_message( fact_key, label, metric_label = spec value = _as_int(facts.get(fact_key)) return f"{label} ({metric_label}={value}): {qualname}." + if category == CATEGORY_COVERAGE: + coverage_status = _text(facts.get("coverage_status")) + threshold = _as_int(facts.get("hotspot_threshold_percent")) + if coverage_status == "missing_from_report": + return f"Coverage scope gap (not in coverage.xml): {qualname}." + coverage_pct = _as_int(facts.get("coverage_permille")) / 10.0 + return f"Coverage hotspot ({coverage_pct:.1f}% < {threshold}%): {qualname}." modules = [_text(item.get("module")) for item in items if _text(item.get("module"))] return f"Dependency cycle ({len(modules)} modules): {' -> '.join(modules)}." @@ -658,6 +690,11 @@ def _design_result_properties( "cyclomatic_complexity", "nesting_depth", "cycle_length", + "coverage_permille", + "covered_lines", + "executable_lines", + "hotspot_threshold_percent", + "coverage_status", ): if key in facts: props[key] = facts[key] diff --git a/codeclone/report/serialize.py b/codeclone/report/serialize.py index 3cd4a9e..80c37d4 100644 --- a/codeclone/report/serialize.py +++ b/codeclone/report/serialize.py @@ -160,6 +160,54 @@ def _append_clone_section( lines.pop() +def _append_suppressed_clone_section( + lines: list[str], + *, + title: str, + groups: Sequence[object], + metric_name: str, +) -> None: + section_groups = [_as_mapping(group) for group in groups] + lines.append(f"{title} (groups={len(section_groups)})") + if not section_groups: + lines.append("(none)") + return + for idx, group in enumerate(section_groups, start=1): + lines.append(f"=== Suppressed clone group #{idx} ===") + lines.append( + "id=" + f"{format_meta_text_value(group.get('id'))} " + f"clone_type={format_meta_text_value(group.get('clone_type'))} " + f"severity={format_meta_text_value(group.get('severity'))} " + f"count={format_meta_text_value(group.get('count'))} " + f"spread={_spread_text(_as_mapping(group.get('spread')))} " + f"scope={_scope_text(_as_mapping(group.get('source_scope')))} " + "suppressed_by=" + f"{format_meta_text_value(group.get('suppression_rule'))}" + "@" + f"{format_meta_text_value(group.get('suppression_source'))} " + "matched_patterns=" + f"{format_meta_text_value(group.get('matched_patterns'))}" + ) + facts = _as_mapping(group.get("facts")) + if facts: + lines.append( + "facts: " + + _format_key_values( + facts, + tuple(sorted(str(key) for key in facts)), + skip_empty=True, + ) + ) + lines.extend( + _location_line(item, metric_name=metric_name) + for item in map(_as_mapping, _as_sequence(group.get("items"))) + ) + lines.append("") + if lines[-1] == "": + lines.pop() + + def _append_structural_findings(lines: list[str], groups: Sequence[object]) -> None: structural_groups = [_as_mapping(group) for group in groups] lines.append(f"STRUCTURAL FINDINGS (groups={len(structural_groups)})") @@ -479,7 +527,14 @@ def render_text_report_document(payload: Mapping[str, object]) -> str: digest = _as_mapping(integrity.get("digest")) findings_groups = _as_mapping(findings.get("groups")) clone_groups = _as_mapping(findings_groups.get("clones")) + suppressed_clone_groups = _as_mapping(clone_groups.get("suppressed")) runtime_meta = _as_mapping(meta_payload.get("runtime")) + clone_summary_keys: list[str] = ["functions", "blocks", "segments", "new", "known"] + if "suppressed" in findings_clones: + clone_summary_keys.append("suppressed") + suppressed_summary_keys: list[str] = ["dead_code"] + if "clones" in findings_suppressed: + suppressed_summary_keys.append("clones") lines = [ "REPORT METADATA", @@ -581,12 +636,12 @@ def render_text_report_document(payload: Mapping[str, object]) -> str: "Clones: " + _format_key_values( findings_clones, - ("functions", "blocks", "segments", "new", "known"), + tuple(clone_summary_keys), ), "Suppressed: " + _format_key_values( findings_suppressed, - ("dead_code",), + tuple(suppressed_summary_keys), ), "", "METRICS SUMMARY", @@ -596,18 +651,32 @@ def render_text_report_document(payload: Mapping[str, object]) -> str: "complexity", "coupling", "cohesion", + "coverage_join", "overloaded_modules", "dependencies", "dead_code", "health", ): family_summary = _as_mapping(metrics_summary.get(family_name)) + if family_name == "coverage_join" and not family_summary: + continue keys: Sequence[str] match family_name: case "complexity" | "coupling": keys = ("total", "average", "max", "high_risk") case "cohesion": keys = ("total", "average", "max", "low_cohesion") + case "coverage_join": + keys = ( + "status", + "source", + "units", + "measured_units", + "overall_permille", + "coverage_hotspots", + "scope_gap_hotspots", + "hotspot_threshold_percent", + ) case "dependencies": keys = ("modules", "edges", "cycles", "max_depth") case "overloaded_modules": @@ -624,6 +693,36 @@ def render_text_report_document(payload: Mapping[str, object]) -> str: keys = ("score", "grade") lines.append(f"{family_name}: {_format_key_values(family_summary, keys)}") + coverage_join_family = _as_mapping(metrics_families.get("coverage_join")) + coverage_join_items = _as_sequence(coverage_join_family.get("items")) + if coverage_join_family: + lines.extend( + [ + "", + "COVERAGE JOIN (top 10)", + ] + ) + if not coverage_join_items: + lines.append("(none)") + else: + lines.extend( + "- " + + _format_key_values( + item, + ( + "relative_path", + "qualname", + "coverage_status", + "risk", + "coverage_permille", + "cyclomatic_complexity", + "coverage_hotspot", + "scope_gap_hotspot", + ), + ) + for item in map(_as_mapping, coverage_join_items[:10]) + ) + overloaded_modules_family = _as_mapping(metrics_families.get("overloaded_modules")) if not overloaded_modules_family: overloaded_modules_family = _as_mapping(metrics_families.get("god_modules")) @@ -710,6 +809,28 @@ def render_text_report_document(payload: Mapping[str, object]) -> str: novelty="known", metric_name="size", ) + if suppressed_clone_groups: + lines.append("") + _append_suppressed_clone_section( + lines, + title="SUPPRESSED FUNCTION CLONES", + groups=_as_sequence(suppressed_clone_groups.get("functions")), + metric_name="loc", + ) + lines.append("") + _append_suppressed_clone_section( + lines, + title="SUPPRESSED BLOCK CLONES", + groups=_as_sequence(suppressed_clone_groups.get("blocks")), + metric_name="size", + ) + lines.append("") + _append_suppressed_clone_section( + lines, + title="SUPPRESSED SEGMENT CLONES", + groups=_as_sequence(suppressed_clone_groups.get("segments")), + metric_name="size", + ) lines.append("") _append_structural_findings( lines, diff --git a/codeclone/ui_messages.py b/codeclone/ui_messages.py index 63d013a..d811b31 100644 --- a/codeclone/ui_messages.py +++ b/codeclone/ui_messages.py @@ -104,6 +104,10 @@ "Collect public API surface facts for baseline-aware compatibility review.\n" "Disabled by default." ) +HELP_COVERAGE = ( + "Join external Cobertura XML line coverage to function spans.\n" + "Pass a `coverage xml` report path." +) HELP_FAIL_ON_TYPING_REGRESSION = ( "Exit with code 3 if typing adoption coverage regresses relative to the\n" "metrics baseline." @@ -116,6 +120,10 @@ "Exit with code 3 if public API removals or signature breaks are detected\n" "relative to the metrics baseline." ) +HELP_FAIL_ON_UNTESTED_HOTSPOTS = ( + "Exit with code 3 if medium/high-risk functions measured by Coverage Join\n" + "fall below the joined coverage threshold.\nRequires --coverage." +) HELP_MIN_TYPING_COVERAGE = ( "Exit with code 3 if parameter typing coverage falls below the threshold.\n" "Threshold is a whole percent from 0 to 100." @@ -124,6 +132,10 @@ "Exit with code 3 if public docstring coverage falls below the threshold.\n" "Threshold is a whole percent from 0 to 100." ) +HELP_COVERAGE_MIN = ( + "Coverage threshold for untested hotspot detection.\n" + "Threshold is a whole percent from 0 to 100.\nDefault: 50." +) HELP_CI = ( "Enable CI preset.\n" "Equivalent to: --fail-on-new --no-color --quiet.\n" @@ -236,6 +248,7 @@ WARN_HTML_REPORT_OPEN_FAILED = ( "[warning]Failed to open HTML report in browser: {path} ({error}).[/warning]" ) +WARN_COVERAGE_JOIN_IGNORED = "[warning]Coverage join ignored: {error}[/warning]" ERR_INVALID_OUTPUT_EXT = ( "[error]Invalid {label} output extension: {path} " @@ -351,6 +364,10 @@ def fmt_html_report_open_failed(*, path: Path, error: object) -> str: return WARN_HTML_REPORT_OPEN_FAILED.format(path=path, error=error) +def fmt_coverage_join_ignored(error: object) -> str: + return WARN_COVERAGE_JOIN_IGNORED.format(error=error) + + def fmt_unreadable_source_in_gating(*, count: int) -> str: return ERR_UNREADABLE_SOURCE_IN_GATING.format(count=count) @@ -405,15 +422,19 @@ def fmt_summary_compact_clones( block: int, segment: int, suppressed: int, + fixture_excluded: int, new: int, ) -> str: - return SUMMARY_COMPACT_CLONES.format( - function=function, - block=block, - segment=segment, - suppressed=suppressed, - new=new, - ) + parts = [ + f"Clones func={function}", + f"block={block}", + f"seg={segment}", + f"suppressed={suppressed}", + ] + if fixture_excluded > 0: + parts.append(f"fixtures={fixture_excluded}") + parts.append(f"new={new}") + return " ".join(parts) def fmt_summary_compact_metrics( @@ -445,6 +466,63 @@ def fmt_summary_compact_metrics( ) +def fmt_summary_compact_adoption( + *, + param_permille: int, + return_permille: int, + docstring_permille: int, + any_annotation_count: int, +) -> str: + return ( + "Adoption" + f" params={_format_permille_pct(param_permille)}" + f" returns={_format_permille_pct(return_permille)}" + f" docstrings={_format_permille_pct(docstring_permille)}" + f" any={any_annotation_count}" + ) + + +def fmt_summary_compact_api_surface( + *, + public_symbols: int, + modules: int, + added: int, + breaking: int, +) -> str: + return ( + "Public API" + f" symbols={public_symbols}" + f" modules={modules}" + f" breaking={breaking}" + f" added={added}" + ) + + +def fmt_summary_compact_coverage_join( + *, + status: str, + overall_permille: int, + coverage_hotspots: int, + scope_gap_hotspots: int, + threshold_percent: int, + source_label: str, +) -> str: + parts = [f"Coverage status={status or 'unknown'}"] + if status == "ok": + parts.extend( + [ + f"overall={_format_permille_pct(overall_permille)}", + f"coverage_hotspots={coverage_hotspots}", + f"threshold={threshold_percent}", + ] + ) + if scope_gap_hotspots > 0: + parts.append(f"scope_gaps={scope_gap_hotspots}") + if source_label: + parts.append(f"source={source_label}") + return " ".join(parts) + + _HEALTH_GRADE_STYLE: dict[str, str] = { HEALTH_GRADE_A: "bold green", HEALTH_GRADE_B: "green", @@ -505,7 +583,13 @@ def fmt_summary_parsed( def fmt_summary_clones( - *, func: int, block: int, segment: int, suppressed: int, new: int + *, + func: int, + block: int, + segment: int, + suppressed: int, + fixture_excluded: int, + new: int, ) -> str: clone_parts = [ f"{_v(func, 'bold yellow')} func", @@ -516,8 +600,10 @@ def fmt_summary_clones( main = " \u00b7 ".join(clone_parts) quals = [ f"{_v(suppressed, 'yellow')} suppressed", - f"{_v(new, 'bold red')} new", ] + if fixture_excluded > 0: + quals.append(f"{_v(fixture_excluded, 'yellow')} fixtures") + quals.append(f"{_v(new, 'bold red')} new") return f" {'Clones':<{_L}}{main} ({', '.join(quals)})" @@ -610,6 +696,31 @@ def fmt_metrics_api_surface( return f" {'Public API':<{_L}}{' · '.join(parts)}" +def fmt_metrics_coverage_join( + *, + status: str, + overall_permille: int, + coverage_hotspots: int, + scope_gap_hotspots: int, + threshold_percent: int, + source_label: str, +) -> str: + if status != "ok": + parts = ["join unavailable"] + if source_label: + parts.append(source_label) + return f" {'Coverage':<{_L}}[yellow]{' · '.join(parts)}[/yellow]" + parts = [ + f"{_format_permille_pct(overall_permille)} overall", + f"{_v(coverage_hotspots, 'bold red')} hotspots < {threshold_percent}%", + ] + if scope_gap_hotspots > 0: + parts.append(f"{_v(scope_gap_hotspots, 'bold yellow')} scope gaps") + if source_label: + parts.append(source_label) + return f" {'Coverage':<{_L}}{' · '.join(parts)}" + + def fmt_metrics_overloaded_modules( *, candidates: int, diff --git a/docs/README.md b/docs/README.md index a5df1f1..843577b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -37,9 +37,9 @@ repository build: - [Exit codes and failure policy](book/03-contracts-exit-codes.md) - [Config and defaults](book/04-config-and-defaults.md) - [Core pipeline and invariants](book/05-core-pipeline.md) -- [Baseline contract (schema v2.0)](book/06-baseline.md) -- [Cache contract (schema v2.3)](book/07-cache.md) -- [Report contract (schema v2.5)](book/08-report.md) +- [Baseline contract (schema v2.1)](book/06-baseline.md) +- [Cache contract (schema v2.4)](book/07-cache.md) +- [Report contract (schema v2.8)](book/08-report.md) ## Interfaces diff --git a/docs/architecture.md b/docs/architecture.md index 5c96b06..43fab28 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -144,7 +144,7 @@ gating decisions. Detected findings can be rendered as: - interactive HTML (`--html`), -- canonical JSON (`--json`, schema `2.5`), +- canonical JSON (`--json`, schema `2.8`), - deterministic text projection (`--text`), - deterministic Markdown projection (`--md`), - deterministic SARIF projection (`--sarif`). diff --git a/docs/book/04-config-and-defaults.md b/docs/book/04-config-and-defaults.md index a83593c..934d7f5 100644 --- a/docs/book/04-config-and-defaults.md +++ b/docs/book/04-config-and-defaults.md @@ -32,6 +32,7 @@ Key defaults: - `--baseline=codeclone.baseline.json` - `--max-baseline-size-mb=5` - `--max-cache-size-mb=50` +- `--coverage-min=50` - default cache path (when no cache flag is given): `/.cache/codeclone/cache.json` - `--metrics-baseline=codeclone.baseline.json` (same default path as `--baseline`) - bare reporting flags use default report paths: @@ -59,12 +60,109 @@ skip_metrics = true quiet = true ``` +Supported `[tool.codeclone]` keys in the current line: + +Analysis: + +| Key | Type | Default | Meaning | +|------------------------|---------------|--------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| +| `min_loc` | `int` | `10` | Minimum function LOC for clone admission | +| `min_stmt` | `int` | `6` | Minimum function statement count for clone admission | +| `block_min_loc` | `int` | `20` | Minimum function LOC for block-window analysis | +| `block_min_stmt` | `int` | `8` | Minimum function statements for block-window analysis | +| `segment_min_loc` | `int` | `20` | Minimum function LOC for segment analysis | +| `segment_min_stmt` | `int` | `10` | Minimum function statements for segment analysis | +| `processes` | `int` | `4` | Worker process count | +| `cache_path` | `str \| null` | `/.cache/codeclone/cache.json` | Cache file path | +| `max_cache_size_mb` | `int` | `50` | Maximum accepted cache size before fail-open ignore | +| `skip_metrics` | `bool` | `false*` | Skip full metrics mode when allowed | +| `skip_dead_code` | `bool` | `false` | Skip dead-code analysis | +| `skip_dependencies` | `bool` | `false` | Skip dependency analysis | +| `golden_fixture_paths` | `list[str]` | `[]` | Exclude clone groups fully contained in matching golden test fixtures from health/gates/active findings; keep them as suppressed report facts | + +Baseline and CI: + +| Key | Type | Default | Meaning | +|---------------------------|--------|---------------------------|-------------------------------------------| +| `baseline` | `str` | `codeclone.baseline.json` | Clone baseline path | +| `max_baseline_size_mb` | `int` | `5` | Maximum accepted baseline size | +| `update_baseline` | `bool` | `false` | Rewrite unified baseline from current run | +| `metrics_baseline` | `str` | `codeclone.baseline.json` | Dedicated metrics-baseline path override | +| `update_metrics_baseline` | `bool` | `false` | Rewrite metrics baseline from current run | +| `ci` | `bool` | `false` | Enable CI preset behavior | + +Quality gates and metric collection: + +| Key | Type | Default | Meaning | +|--------------------------------|---------------|---------|-------------------------------------------------------------------------------------| +| `fail_on_new` | `bool` | `false` | Fail when new clone groups appear | +| `fail_threshold` | `int` | `-1` | Fail when clone count exceeds threshold | +| `fail_complexity` | `int` | `-1` | Fail when max cyclomatic complexity exceeds threshold | +| `fail_coupling` | `int` | `-1` | Fail when max CBO exceeds threshold | +| `fail_cohesion` | `int` | `-1` | Fail when max LCOM4 exceeds threshold | +| `fail_cycles` | `bool` | `false` | Fail when dependency cycles are present | +| `fail_dead_code` | `bool` | `false` | Fail when high-confidence dead code is present | +| `fail_health` | `int` | `-1` | Fail when health score drops below threshold | +| `fail_on_new_metrics` | `bool` | `false` | Fail on new metric hotspots vs trusted metrics baseline | +| `typing_coverage` | `bool` | `true` | Collect typing adoption facts | +| `docstring_coverage` | `bool` | `true` | Collect public docstring adoption facts | +| `api_surface` | `bool` | `false` | Collect public API inventory/diff facts | +| `coverage_xml` | `str \| null` | `null` | Join external Cobertura XML to current-run function spans | +| `coverage_min` | `int` | `50` | Coverage threshold for joined measured coverage hotspots | +| `min_typing_coverage` | `int` | `-1` | Minimum allowed typing coverage percent | +| `min_docstring_coverage` | `int` | `-1` | Minimum allowed docstring coverage percent | +| `fail_on_typing_regression` | `bool` | `false` | Fail on typing coverage regression vs metrics baseline | +| `fail_on_docstring_regression` | `bool` | `false` | Fail on docstring coverage regression vs metrics baseline | +| `fail_on_api_break` | `bool` | `false` | Fail on public API breaking changes vs metrics baseline | +| `fail_on_untested_hotspots` | `bool` | `false` | Fail when medium/high-risk functions measured by Coverage Join fall below threshold | + +Report outputs and local UX: + +| Key | Type | Default | Meaning | +|---------------|---------------|---------|--------------------------------| +| `html_out` | `str \| null` | `null` | HTML report output path | +| `json_out` | `str \| null` | `null` | JSON report output path | +| `md_out` | `str \| null` | `null` | Markdown report output path | +| `sarif_out` | `str \| null` | `null` | SARIF report output path | +| `text_out` | `str \| null` | `null` | Plain-text report output path | +| `no_progress` | `bool` | `false` | Disable progress UI | +| `no_color` | `bool` | `false` | Disable colored CLI output | +| `quiet` | `bool` | `false` | Use compact CLI output | +| `verbose` | `bool` | `false` | Enable more verbose CLI output | +| `debug` | `bool` | `false` | Enable debug diagnostics | + +This is the exact accepted key set from `codeclone/_cli_config.py`; unknown +keys are contract errors. + +Notes: + +- `skip_metrics=false*`: parser default is `false`, but runtime may auto-enable + it when no metrics work is requested and no metrics baseline exists. +- Report output keys default to `null`; bare CLI flags still write to the + deterministic `.cache/codeclone/report.*` paths listed above. + CLI always has precedence when option is explicitly provided, including boolean overrides via `--foo/--no-foo` (e.g. `--no-skip-metrics`). Path values loaded from `pyproject.toml` are normalized relative to resolved scan root when provided as relative paths. +`golden_fixture_paths` is different: + +- entries are repo-relative glob patterns, not filesystem paths +- they are not normalized to absolute paths +- they must target `tests/` or `tests/fixtures/` +- a clone group is excluded only when every occurrence matches the configured + golden-fixture scope + +Current-run coverage join config: + +- `coverage_xml` may be set in `pyproject.toml`; relative paths resolve from + the scan root like other configured paths. +- `coverage_min` and `fail_on_untested_hotspots` follow the same precedence + rules as CLI flags. +- Coverage join remains current-run only and does not persist to baseline. + Metrics baseline path selection contract: - If `--metrics-baseline` is explicitly set, that path is used. diff --git a/docs/book/05-core-pipeline.md b/docs/book/05-core-pipeline.md index 1640dd1..57c3a3c 100644 --- a/docs/book/05-core-pipeline.md +++ b/docs/book/05-core-pipeline.md @@ -30,6 +30,8 @@ Stages: 5. Report-layer post-processing: - merge block windows to maximal regions - merge/suppress segment report groups + - optionally split out clone groups fully contained in configured + `golden_fixture_paths` 6. Structural report findings: - duplicated branch families from per-function AST structure facts - clone cohort drift families built from existing function groups (no rescan) @@ -42,20 +44,28 @@ Stages: - seven dimension scores: clones, complexity, coupling, cohesion, dead code, dependencies, coverage - weighted blend → composite score (0–100) and grade (A–F) -9. Design finding extraction: - - threshold-aware findings for complexity, coupling, cohesion - - thresholds recorded in `meta.analysis_thresholds.design_findings` -10. Suggestion generation: +9. Suggestion generation: - advisory cards from clone groups, structural findings, metric violations - deterministic priority sort, never gates CI -11. Derived overview and hotlists: +10. Current-run coverage join (optional): + - when `--coverage` is present, join external Cobertura XML to discovered + function spans + - invalid XML becomes `coverage_join.status="invalid"` for that run rather + than mutating baseline state +11. Design finding extraction: + - threshold-aware findings for complexity, coupling, cohesion + - coverage `coverage_hotspot` / `coverage_scope_gap` findings from valid + coverage-join rows only + - thresholds recorded in `meta.analysis_thresholds.design_findings` +12. Derived overview and hotlists: - overview families, top risks, source breakdown, health snapshot - directory hotspots by category (`derived.overview.directory_hotspots`) - hotlists: most actionable, highest spread, production/test-fixture hotspots -12. Gate evaluation: +13. Gate evaluation: - clone-baseline diff (NEW vs KNOWN) - metric threshold gates (`--fail-complexity`, `--fail-coupling`, etc.) - metric regression gates (`--fail-on-new-metrics`) + - coverage hotspot gate (`--fail-on-untested-hotspots`) - gate reasons emitted in deterministic order Refs: @@ -65,6 +75,7 @@ Refs: - `codeclone/report/blocks.py:prepare_block_report_groups` - `codeclone/report/segments.py:prepare_segment_report_groups` - `codeclone/metrics/health.py:compute_health` +- `codeclone/metrics/coverage_join.py:build_coverage_join` - `codeclone/report/json_contract.py:_build_design_groups` - `codeclone/report/suggestions.py:generate_suggestions` - `codeclone/report/overview.py:build_directory_hotspots` @@ -76,6 +87,12 @@ Refs: - Report-layer transformations do not change function/block grouping keys used for baseline diff. - Segment groups are report-only and do not participate in baseline diff/gating. - Structural findings are report-only and do not participate in baseline diff/gating. +- `golden_fixture_paths` is a project-level clone exclusion policy, not a + fingerprint/baseline rule: + - it applies only to clone groups fully contained in matching + `tests/` / `tests/fixtures/` paths + - excluded groups do not affect health, clone gates, or suggestions + - excluded groups remain observable as suppressed canonical report facts - Dead-code liveness references from test paths are excluded at extraction/cache-load boundaries for both local-name references and canonical qualname references. diff --git a/docs/book/06-baseline.md b/docs/book/06-baseline.md index 79d37e5..b21c51e 100644 --- a/docs/book/06-baseline.md +++ b/docs/book/06-baseline.md @@ -61,6 +61,8 @@ Embedded metrics contract: - The default runtime flow is unified: clone baseline and metrics baseline usually share the same `codeclone.baseline.json` file unless the metrics path is explicitly overridden. +- In unified rewrite mode, disabled optional metric surfaces are omitted from + the rewritten embedded payload instead of being preserved as stale baggage. Integrity payload includes only: diff --git a/docs/book/07-cache.md b/docs/book/07-cache.md index 1e2fe51..f61b815 100644 --- a/docs/book/07-cache.md +++ b/docs/book/07-cache.md @@ -2,7 +2,7 @@ ## Purpose -Define cache schema v2.3, integrity verification, and fail-open behavior. +Define cache schema v2.4, integrity verification, and fail-open behavior. ## Public surface @@ -13,7 +13,7 @@ Define cache schema v2.3, integrity verification, and fail-open behavior. ## Data model -On-disk schema (`v == "2.3"`): +On-disk schema (`v == "2.4"`): - Top-level: `v`, `payload`, `sig` - `payload` keys: `py`, `fp`, `ap`, `files`, optional `sr` @@ -73,6 +73,8 @@ Refs: - Cache save writes canonical JSON and atomically replaces target file. - Empty sections (`u`, `b`, `s`) are omitted from written wire entries. - `rn`/`rq` are serialized as sorted unique arrays and omitted when empty. +- Cached public-API symbol payloads preserve declared parameter order; cache + canonicalization must not reorder callable signatures. - `ss` is written when source stats are available and is required for full cache-hit accounting in discovery stage. - Legacy secret file `.cache_secret` is never used for trust; warning only. diff --git a/docs/book/08-report.md b/docs/book/08-report.md index 60eb5b1..2dbc6a0 100644 --- a/docs/book/08-report.md +++ b/docs/book/08-report.md @@ -2,7 +2,7 @@ ## Purpose -Define report contracts in `2.0.0b5`: canonical JSON (`report_schema_version=2.5`) +Define report contracts in `2.0.0b5`: canonical JSON (`report_schema_version=2.8`) plus deterministic TXT/Markdown/SARIF projections. ## Public surface @@ -16,7 +16,7 @@ plus deterministic TXT/Markdown/SARIF projections. ## Data model -JSON report top-level (v2.5): +JSON report top-level (v2.8): - `report_schema_version` - `meta` @@ -45,9 +45,19 @@ Canonical report-only metrics additions: - `metrics.families.api_surface` records the current public symbol inventory and compact baseline diff facts (`added`, `breaking`) when `--api-surface` is enabled -- the family is canonical report truth, but it does **not** participate in - findings totals, health, gates, baseline NEW/KNOWN semantics, or SARIF in - `b4` +- `metrics.families.coverage_join` records an optional current-run join between + external Cobertura line coverage and CodeClone function spans. Its summary + carries `status`, `source`, unit/line counts, `overall_permille`, + `missing_from_report_units`, `coverage_hotspots`, `scope_gap_hotspots`, + `hotspot_threshold_percent`, and optional `invalid_reason`; the same compact + summary is mirrored in `metrics.summary.coverage_join`; its items carry + per-function joined coverage facts, including `coverage_status`, + `coverage_hotspot`, and `scope_gap_hotspot`. +- coverage join facts are canonical report truth for that run, but they are + **not** baseline truth and do not update `codeclone.baseline.json` +- adoption/API/coverage-join metrics do **not** participate in clone baseline + NEW/KNOWN semantics; coverage join also does not participate in health scoring + and gates only when explicitly requested - `Overloaded Modules` is a report-only experimental layer rather than a second complexity metric: - complexity reports local control-flow hotspots in functions and methods @@ -60,6 +70,12 @@ Coverage/API role split: - `coverage_adoption` is a canonical metrics family, not a style linter. It reports observable adoption facts only. +- `coverage_join` is a canonical current-run signal over an external Cobertura + XML file. It reports joined line facts and may materialize + `design` findings with `category="coverage"` and kinds + `coverage_hotspot` (measured below threshold) or `coverage_scope_gap` + (outside the supplied coverage scope); it does not infer branch coverage or + execute tests. - `api_surface` is a canonical metrics/gating family, not a second finding engine. It reports public API inventory plus baseline-diff facts when the run opted into API collection. @@ -89,10 +105,14 @@ Derived projection layer: Finding families: - `findings.groups.clones.{functions,blocks,segments}` +- optional `findings.groups.clones.suppressed.{functions,blocks,segments}` for + clone groups excluded by project policy such as `golden_fixture_paths` - `findings.groups.structural.groups` - `findings.groups.dead_code.groups` - `findings.groups.design.groups` - `findings.summary.suppressed.dead_code` (suppressed counter, non-active findings) +- optional `findings.summary.suppressed.clones` plus clone-summary suppressed + counters when clone groups were excluded from active findings Important role split: @@ -132,6 +152,10 @@ Per-group common axes (family-specific fields may extend): - Design findings are built once in the canonical report using the effective threshold policy recorded in `meta.analysis_thresholds.design_findings`; MCP and HTML must not re-synthesize them post-hoc from raw metric rows. +- Coverage design findings are built from canonical `coverage_join` rows only + when a valid join is present. Invalid coverage input is represented as + `metrics.families.coverage_join.summary.status="invalid"` with no hotspot + item rows. - HTML overview cards are materialized from canonical findings plus `derived.overview` + `derived.hotlists`; pre-expanded overview card payloads are not part of the report contract. @@ -160,6 +184,9 @@ Per-group common axes (family-specific fields may extend): - Dead-code suppressed candidates are carried only under metrics (`metrics.families.dead_code.suppressed_items`) and never promoted to active `findings.groups.dead_code`. +- Clone groups excluded by `golden_fixture_paths` are carried only under + `findings.groups.clones.suppressed.*`; they do not contribute to active + findings totals, health scoring, clone gating, or suggestion generation. - A lower score after upgrade may reflect a broader health model, not only worse code. Report renderers may surface the score, but health-model expansion is documented separately in [15-health-score.md](15-health-score.md) diff --git a/docs/book/09-cli.md b/docs/book/09-cli.md index fb0f467..e79df83 100644 --- a/docs/book/09-cli.md +++ b/docs/book/09-cli.md @@ -25,6 +25,7 @@ Summary metrics: - files found/analyzed/cache hits/skipped - structural counters: analyzed lines/functions/methods/classes - function/block/segment groups +- excluded golden-fixture clone groups (when configured) - suppressed segment groups - dead-code active/suppressed status in metrics line - adoption coverage in the normal `Metrics` block: @@ -32,6 +33,9 @@ Summary metrics: - public API surface in the normal `Metrics` block when `api_surface` was collected: symbol/module counts plus added/breaking deltas when a trusted metrics baseline is available +- coverage join in the normal `Metrics` block when `--coverage FILE` was + provided: joined Cobertura overall line coverage, untested hotspot count, and + threshold/source context - new vs baseline Metrics-related CLI gates: @@ -45,10 +49,16 @@ Metrics-related CLI gates: `--fail-on-typing-regression`, `--fail-on-docstring-regression`, `--fail-on-api-break` +- external coverage join gate: + `--coverage FILE`, `--coverage-min PERCENT`, + `--fail-on-untested-hotspots` - update mode: `--update-metrics-baseline` - opt-in metrics family: `--api-surface` +- In unified baseline mode, `--update-baseline` rewrites embedded metric + surfaces from the current enabled config; disabled optional surfaces are + dropped. Refs: @@ -82,8 +92,12 @@ Refs: - The normal rich `Metrics` block includes: - `Adoption` when adoption coverage facts were computed - `Public API` when `api_surface` facts were computed + - `Coverage` when Cobertura coverage was joined with `--coverage` - Quiet compact metrics output stays on the existing fixed one-line summary and - does not expand adoption/API detail. + does not expand adoption/API/coverage-join detail. +- When `golden_fixture_paths` excludes clone groups from active review, CLI + keeps that count inside the `Clones` summary line (`fixtures=N`) instead of + adding a separate summary row. - Typing/docstring adoption metrics are computed by default in full mode. - `--api-surface` is opt-in in normal runs, but runtime auto-enables it when `--fail-on-api-break` or `--update-metrics-baseline` needs a public API @@ -92,6 +106,17 @@ Refs: metrics baseline that already contains adoption coverage data. - `--fail-on-api-break` requires a metrics baseline that already contains `api_surface` data. +- `--coverage` is a current-run external Cobertura input. It does not update or + compare against `codeclone.baseline.json`. +- Invalid Cobertura XML is warning-only in normal runs: CLI prints + `Coverage join ignored`, keeps exit `0`, and shows `Coverage` as unavailable + in the normal `Metrics` block. It becomes a contract error only when + `--fail-on-untested-hotspots` requires a valid join. +- `--fail-on-untested-hotspots` requires `--coverage` and a valid Cobertura XML + input. It exits `3` when medium/high-risk functions measured by Coverage Join + fall below `--coverage-min` (default `50`). Functions outside the supplied + `coverage.xml` scope are surfaced separately and do not trigger this gate. + The flag name is retained for CLI compatibility. Refs: @@ -136,9 +161,13 @@ Refs: | `--diff-against` + `--paths-from-git-diff` | contract | 2 | | Baseline untrusted in CI/gating | contract | 2 | | Coverage/API regression gate without required metrics-baseline capability | contract | 2 | +| `--fail-on-untested-hotspots` without `--coverage` | contract | 2 | +| Invalid Cobertura XML without hotspot gating | warning only | 0 | +| Invalid Cobertura XML for coverage hotspot gating | contract | 2 | | Unreadable source in CI/gating | contract | 2 | | New clones with `--fail-on-new` | gating | 3 | | Threshold exceeded | gating | 3 | +| Coverage hotspots with `--fail-on-untested-hotspots` | gating | 3 | | Unexpected exception | internal | 5 | ## Determinism / canonicalization diff --git a/docs/book/10-html-render.md b/docs/book/10-html-render.md index d11bc02..42f7530 100644 --- a/docs/book/10-html-render.md +++ b/docs/book/10-html-render.md @@ -47,8 +47,19 @@ Refs: - Hotspots by Directory: render-only view over `derived.overview.directory_hotspots` - Health Profile: full-width radar chart of dimension scores - Get Badge modal: grade-only / score+grade variants with shields.io embed +- Quality UI is also a report projection: + - deterministic subtabs for complexity, coupling, cohesion, overloaded + modules, and `Coverage Join` when canonical join facts exist + - `Coverage Join` uses the same stat-card and table patterns as other + quality surfaces; it separates measured coverage hotspots from coverage + scope gaps, and invalid joins render a factual unavailable state instead + of a success-style empty message - Dead-code UI is a single top-level `Dead Code` tab with deterministic split sub-tabs: `Active` and `Suppressed`. +- Clones UI may append a `Suppressed` sub-tab when canonical report data + includes `findings.groups.clones.suppressed.*`; those rows are factual + projections of policy-excluded clone groups such as `golden_fixture_paths` + and do not become active clone findings. - IDE deep links: - An IDE picker in the topbar lets users choose their IDE. The selection is persisted in `localStorage` (key `codeclone-ide`). diff --git a/docs/book/13-testing-as-spec.md b/docs/book/13-testing-as-spec.md index a5fbcd2..482c942 100644 --- a/docs/book/13-testing-as-spec.md +++ b/docs/book/13-testing-as-spec.md @@ -11,9 +11,13 @@ Contract tests are concentrated in: - `tests/test_baseline.py` - `tests/test_cache.py` - `tests/test_report.py` +- `tests/test_report_contract_coverage.py` - `tests/test_cli_inprocess.py` - `tests/test_cli_unit.py` +- `tests/test_coverage_join.py` +- `tests/test_golden_fixtures.py` - `tests/test_html_report.py` +- `tests/test_mcp_service.py` - `tests/test_detector_golden.py` - `tests/test_golden_v2.py` @@ -29,14 +33,16 @@ Test classes by role: The following matrix is treated as executable contract: -| Contract | Tests | -|--------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------| -| Baseline schema/integrity/compat gates | `tests/test_baseline.py` | -| Cache v2.3 fail-open + status mapping | `tests/test_cache.py`, `tests/test_cli_inprocess.py::test_cli_reports_cache_too_large_respects_max_size_flag` | -| Exit code categories and markers | `tests/test_cli_unit.py`, `tests/test_cli_inprocess.py` | -| Report schema v2.5 canonical/derived/integrity + JSON/TXT/MD/SARIF projections | `tests/test_report.py`, `tests/test_report_contract_coverage.py`, `tests/test_report_branch_invariants.py` | -| HTML render-only explainability + escaping | `tests/test_html_report.py` | -| Scanner traversal safety | `tests/test_scanner_extra.py`, `tests/test_security.py` | +| Contract | Tests | +|--------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| +| Baseline schema/integrity/compat gates | `tests/test_baseline.py` | +| Cache v2.4 fail-open + status mapping + API signature order preservation | `tests/test_cache.py`, `tests/test_cli_inprocess.py::test_cli_reports_cache_too_large_respects_max_size_flag`, `tests/test_cli_inprocess.py::test_cli_public_api_breaking_count_stable_across_warm_cache` | +| Exit code categories and markers | `tests/test_cli_unit.py`, `tests/test_cli_inprocess.py` | +| Report schema v2.8 canonical/derived/integrity + JSON/TXT/MD/SARIF projections | `tests/test_report.py`, `tests/test_report_contract_coverage.py`, `tests/test_report_branch_invariants.py` | +| HTML render-only explainability + escaping | `tests/test_html_report.py` | +| Current-run Cobertura coverage join parsing, gating, and projections | `tests/test_coverage_join.py`, `tests/test_pipeline_metrics.py`, `tests/test_cli_unit.py`, `tests/test_mcp_service.py`, `tests/test_html_report.py` | +| Golden fixture clone exclusion policy | `tests/test_golden_fixtures.py`, `tests/test_cli_inprocess.py::test_cli_pyproject_golden_fixture_paths_exclude_fixture_clone_groups`, `tests/test_report.py::test_report_json_clone_groups_can_include_suppressed_golden_fixture_bucket` | +| Scanner traversal safety | `tests/test_scanner_extra.py`, `tests/test_security.py` | ## Invariants (MUST) @@ -73,6 +79,7 @@ Refs: - `tests/test_baseline.py::test_baseline_payload_fields_contract_invariant` - `tests/test_cache.py::test_cache_v13_missing_optional_sections_default_empty` - `tests/test_report.py::test_report_json_compact_v21_contract` +- `tests/test_coverage_join.py::test_build_coverage_join_maps_cobertura_lines_to_function_spans` - `tests/test_cli_inprocess.py::test_cli_contract_error_priority_over_gating_failure_for_unreadable_source` - `tests/test_html_report.py::test_html_and_json_group_order_consistent` - `tests/test_detector_golden.py::test_detector_output_matches_golden_fixture` diff --git a/docs/book/14-compatibility-and-versioning.md b/docs/book/14-compatibility-and-versioning.md index 55ba2db..5e85b84 100644 --- a/docs/book/14-compatibility-and-versioning.md +++ b/docs/book/14-compatibility-and-versioning.md @@ -20,8 +20,8 @@ Current contract versions: - `BASELINE_SCHEMA_VERSION = "2.1"` - `BASELINE_FINGERPRINT_VERSION = "1"` -- `CACHE_VERSION = "2.3"` -- `REPORT_SCHEMA_VERSION = "2.5"` +- `CACHE_VERSION = "2.4"` +- `REPORT_SCHEMA_VERSION = "2.8"` - `METRICS_BASELINE_SCHEMA_VERSION = "1.2"` (used only when metrics are stored in a dedicated metrics-baseline file instead of the default unified baseline) @@ -69,9 +69,13 @@ Version bump rules: `report_schema_version` because they alter canonical report semantics and integrity payload. - The same is true for additive canonical metrics families such as - `metrics.families.overloaded_modules`: even though the layer is report-only and does - not affect health/gates/findings, it still changes canonical report schema - and integrity payload, so it requires a report-schema bump. + `metrics.families.overloaded_modules`, `coverage_adoption`, `api_surface`, + or `coverage_join`: even when the layer is report-only or current-run only, + it still changes canonical report schema and integrity payload, so it + requires a report-schema bump. +- The same rule applies to new canonical suppressed-finding buckets such as + `findings.groups.clones.suppressed.*`: even though they are non-active + review facts, they still change canonical report shape and integrity payload. - CodeClone does not currently define a separate health-model version constant. Health-score semantics are package-versioned and must be documented in the Health Score chapter and release notes when they change. diff --git a/docs/book/15-metrics-and-quality-gates.md b/docs/book/15-metrics-and-quality-gates.md index 521e61a..61b830c 100644 --- a/docs/book/15-metrics-and-quality-gates.md +++ b/docs/book/15-metrics-and-quality-gates.md @@ -20,6 +20,9 @@ Metrics gate inputs: `--fail-complexity`, `--fail-coupling`, `--fail-cohesion`, `--fail-health` - adoption threshold gates: `--min-typing-coverage`, `--min-docstring-coverage` +- external Cobertura coverage join: + `--coverage FILE`, `--coverage-min PERCENT`, + `--fail-on-untested-hotspots` - boolean structural gates: `--fail-cycles`, `--fail-dead-code` - baseline-aware delta gates: @@ -54,6 +57,10 @@ Refs: - `--skip-metrics` is incompatible with metrics gating/update flags and is a contract error. +- `golden_fixture_paths` is a separate project-level clone policy: + clone groups fully contained in matching `tests/` / `tests/fixtures/` paths + are excluded before health/gate/suggestion evaluation, but remain visible as + suppressed report facts. - If metrics are not explicitly requested and no metrics baseline exists, runtime auto-enables clone-only mode (`skip_metrics=true`). - In clone-only mode: @@ -61,12 +68,21 @@ Refs: - `--fail-dead-code` forces dead-code analysis on (even if metrics are skipped). - `--fail-cycles` forces dependency analysis on (even if metrics are skipped). - Type/docstring adoption metrics are computed by default in full mode. +- `--coverage` joins an external Cobertura XML file to current-run function + spans with stdlib XML parsing only. This signal is not metrics-baseline truth, + is not written to `codeclone.baseline.json`, and does not affect fingerprint + or clone identity semantics. +- Invalid Cobertura XML downgrades to a current-run + `coverage_join.status="invalid"` signal in normal analysis. It does not fail + the run or update any baseline; only `--fail-on-untested-hotspots` upgrades + invalid input into a contract error. - `--api-surface` is opt-in in normal runs, but runtime auto-enables it when `--fail-on-api-break` or `--update-metrics-baseline` needs a public API snapshot. - In the normal CLI `Metrics` block, adoption coverage is shown whenever metrics are computed, and the public API surface line appears when `api_surface` - facts were collected. + facts were collected. A coverage line appears when `--coverage` produced a + joined coverage summary. - `--update-baseline` in full mode implies metrics-baseline update in the same run. - If metrics baseline path equals clone baseline path and clone baseline file is @@ -78,6 +94,11 @@ Refs: metrics baseline that already contains adoption coverage data. - `--fail-on-api-break` requires a metrics baseline that already contains `api_surface` data. +- `--fail-on-untested-hotspots` requires `--coverage` and a valid Cobertura XML + input. It evaluates current-run `coverage_join` facts only for measured + medium/high-risk functions below the configured threshold; scope gaps are + surfaced separately and do not require or update a metrics baseline. The + flag name is retained for CLI compatibility. - In CI mode, if metrics baseline was loaded and trusted, runtime enables `fail_on_new_metrics=true`. @@ -93,7 +114,7 @@ Refs: metrics were computed and metrics baseline is trusted. - Metric gate reasons are emitted in deterministic order: threshold checks -> cycles/dead/health -> NEW-vs-baseline diffs -> - adoption/API baseline diffs. + adoption/API baseline diffs -> coverage-join hotspot gate. - Metric gate reasons are namespaced as `metric:*` in gate output. Refs: @@ -103,13 +124,16 @@ Refs: ## Failure modes -| Condition | Behavior | -|-------------------------------------------------------------|--------------------------| -| `--skip-metrics` with metrics flags | Contract error, exit `2` | -| `--fail-on-new-metrics` without trusted baseline | Contract error, exit `2` | -| Coverage/API regression gate without required baseline data | Contract error, exit `2` | -| `--update-metrics-baseline` when metrics were not computed | Contract error, exit `2` | -| Threshold breach or NEW-vs-baseline metric regressions | Gating failure, exit `3` | +| Condition | Behavior | +|-------------------------------------------------------------|--------------------------------------| +| `--skip-metrics` with metrics flags | Contract error, exit `2` | +| `--fail-on-new-metrics` without trusted baseline | Contract error, exit `2` | +| Coverage/API regression gate without required baseline data | Contract error, exit `2` | +| Invalid Cobertura XML without hotspot gate | Current-run invalid signal, exit `0` | +| Coverage hotspot gate without valid `--coverage` input | Contract error, exit `2` | +| `--update-metrics-baseline` when metrics were not computed | Contract error, exit `2` | +| Threshold breach or NEW-vs-baseline metric regressions | Gating failure, exit `3` | +| Coverage hotspots from current-run coverage join | Gating failure, exit `3` | ## Determinism / canonicalization diff --git a/docs/book/20-mcp-interface.md b/docs/book/20-mcp-interface.md index 4427caf..2c750df 100644 --- a/docs/book/20-mcp-interface.md +++ b/docs/book/20-mcp-interface.md @@ -70,6 +70,9 @@ Current server characteristics: - flattened `diff` (`new_clones`, `health_delta`, `typing_param_permille_delta`, `typing_return_permille_delta`, `docstring_permille_delta`, `api_breaking_changes`, `new_api_symbols`) + - optional `coverage_join` when an analysis request included + `coverage_xml` (`status`, `overall_permille`, `coverage_hotspots`, + `scope_gap_hotspots`, `hotspot_threshold_percent`) - `warnings`, `failures` - `analyze_changed_paths` is intentionally more compact than `get_run_summary`: it returns `changed_files`, compact `baseline`, `focus`, `health_scope`, @@ -102,29 +105,29 @@ produced by the report contract. Current tool set (`21` tools): -| Tool | Key parameters | Purpose | -|--------------------------|-----------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------| -| `analyze_repository` | absolute `root`, `analysis_mode`, thresholds, `api_surface`, cache/baseline paths | Full analysis → compact summary; then `get_run_summary` or `get_production_triage` | -| `analyze_changed_paths` | absolute `root`, `changed_paths` or `git_diff_ref`, `analysis_mode`, `api_surface` | Diff-aware analysis → compact changed-files snapshot | -| `get_run_summary` | `run_id` | Cheapest run snapshot: health, findings, baseline, inventory, active thresholds | -| `get_production_triage` | `run_id`, `max_hotspots`, `max_suggestions` | Production-first view: health, hotspots, suggestions, active thresholds | -| `help` | `topic`, `detail` | Semantic guide for workflow, analysis profile, baseline, suppressions, review state, changed-scope | -| `compare_runs` | `run_id_before`, `run_id_after`, `focus` | Run-to-run delta: regressions, improvements, health change | -| `evaluate_gates` | `run_id`, gate thresholds | Preview CI gating decisions | -| `get_report_section` | `run_id`, `section`, `family`, `path`, `offset`, `limit` | Read report sections; `metrics_detail` is paginated with family/path filters | -| `list_findings` | `family`, `severity`, `novelty`, `sort_by`, `detail_level`, `changed_paths`, pagination | Filtered, paginated findings; use after hotspots or `check_*` | -| `get_finding` | `finding_id`, `run_id`, `detail_level` | Single finding detail by id; defaults to `normal` | -| `get_remediation` | `finding_id`, `run_id`, `detail_level` | Remediation payload for one finding | -| `list_hotspots` | `kind`, `run_id`, `detail_level`, `changed_paths`, `limit` | Priority-ranked hotspot views; preferred before broad listing | -| `check_clones` | `run_id`, `root`, `path`, `clone_type`, `source_kind`, `detail_level` | Clone findings only; `health.dimensions` includes only `clones` | -| `check_complexity` | `run_id`, `root`, `path`, `min_complexity`, `detail_level` | Complexity hotspots only | -| `check_coupling` | `run_id`, `root`, `path`, `detail_level` | Coupling hotspots only | -| `check_cohesion` | `run_id`, `root`, `path`, `detail_level` | Cohesion hotspots only | -| `check_dead_code` | `run_id`, `root`, `path`, `min_severity`, `detail_level` | Dead-code findings only | -| `generate_pr_summary` | `run_id`, `changed_paths`, `git_diff_ref`, `format` | PR-friendly markdown or JSON summary | -| `mark_finding_reviewed` | `finding_id`, `run_id`, `note` | Session-local review marker (in-memory) | -| `list_reviewed_findings` | `run_id` | List reviewed findings for a run | -| `clear_session_runs` | none | Reset in-memory runs and session state | +| Tool | Key parameters | Purpose | +|--------------------------|----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------| +| `analyze_repository` | absolute `root`, `analysis_mode`, thresholds, `api_surface`, `coverage_xml`, cache/baseline paths | Full analysis → compact summary; then `get_run_summary` or `get_production_triage` | +| `analyze_changed_paths` | absolute `root`, `changed_paths` or `git_diff_ref`, `analysis_mode`, `api_surface`, `coverage_xml` | Diff-aware analysis → compact changed-files snapshot | +| `get_run_summary` | `run_id` | Cheapest run snapshot: health, findings, baseline, inventory, active thresholds | +| `get_production_triage` | `run_id`, `max_hotspots`, `max_suggestions` | Production-first view: health, hotspots, suggestions, active thresholds | +| `help` | `topic`, `detail` | Semantic guide for workflow, analysis profile, baseline, coverage, suppressions, review state, changed-scope | +| `compare_runs` | `run_id_before`, `run_id_after`, `focus` | Run-to-run delta: regressions, improvements, health change | +| `evaluate_gates` | `run_id`, gate thresholds, `fail_on_untested_hotspots`, `coverage_min` | Preview CI gating decisions | +| `get_report_section` | `run_id`, `section`, `family`, `path`, `offset`, `limit` | Read report sections; `metrics_detail` is paginated with family/path filters | +| `list_findings` | `family`, `severity`, `novelty`, `sort_by`, `detail_level`, `changed_paths`, pagination | Filtered, paginated findings; use after hotspots or `check_*` | +| `get_finding` | `finding_id`, `run_id`, `detail_level` | Single finding detail by id; defaults to `normal` | +| `get_remediation` | `finding_id`, `run_id`, `detail_level` | Remediation payload for one finding | +| `list_hotspots` | `kind`, `run_id`, `detail_level`, `changed_paths`, `limit` | Priority-ranked hotspot views; preferred before broad listing | +| `check_clones` | `run_id`, `root`, `path`, `clone_type`, `source_kind`, `detail_level` | Clone findings only; `health.dimensions` includes only `clones` | +| `check_complexity` | `run_id`, `root`, `path`, `min_complexity`, `detail_level` | Complexity hotspots only | +| `check_coupling` | `run_id`, `root`, `path`, `detail_level` | Coupling hotspots only | +| `check_cohesion` | `run_id`, `root`, `path`, `detail_level` | Cohesion hotspots only | +| `check_dead_code` | `run_id`, `root`, `path`, `min_severity`, `detail_level` | Dead-code findings only | +| `generate_pr_summary` | `run_id`, `changed_paths`, `git_diff_ref`, `format` | PR-friendly markdown or JSON summary | +| `mark_finding_reviewed` | `finding_id`, `run_id`, `note` | Session-local review marker (in-memory) | +| `list_reviewed_findings` | `run_id` | List reviewed findings for a run | +| `clear_session_runs` | none | Reset in-memory runs and session state | All tools are read-only except `mark_finding_reviewed` and `clear_session_runs` (session-local, in-memory). `check_*` tools query stored runs — call @@ -139,7 +142,8 @@ Recommended workflow: 5. `generate_pr_summary(format="markdown")` `metrics_detail` families currently include canonical health/quality families -plus `overloaded_modules`, `coverage_adoption`, and `api_surface`. +plus `overloaded_modules`, `coverage_adoption`, `coverage_join`, and +`api_surface`. For analysis sensitivity, the intended model is: @@ -200,6 +204,17 @@ state behind `codeclone://latest/...`. - baseline trust semantics - cache semantics - canonical report contract +- `coverage_xml` is resolved relative to the absolute root when it is not + already absolute. It is a current-run Cobertura input only; MCP must never + write it to baseline/cache/report artifacts or treat it as baseline truth. +- When `respect_pyproject=true`, MCP also respects `golden_fixture_paths`. + Clone groups excluded by that policy are omitted from active clone/gate + projections but remain available in the canonical report under the optional + `findings.groups.clones.suppressed.*` bucket. +- Invalid Cobertura XML during `analyze_*` does not fail analysis; the stored + run carries `coverage_join.status="invalid"` plus `invalid_reason`. + `evaluate_gates(fail_on_untested_hotspots=true)` on that run is a contract + error because hotspot gating requires a valid join. - Inline MCP design-threshold parameters (`complexity_threshold`, `coupling_threshold`, `cohesion_threshold`) define the canonical design finding universe of that run and are recorded in @@ -217,6 +232,10 @@ state behind `codeclone://latest/...`. - `metrics_detail(family="overloaded_modules")` exposes the canonical report-only module-hotspot layer, but does not promote it into findings, hotlists, or gate semantics. +- `metrics_detail(family="coverage_join")` exposes the canonical current-run + coverage join summary/items, including measured coverage hotspots and + coverage scope gaps. `evaluate_gates(fail_on_untested_hotspots=true)` + requires a stored run created with valid `coverage_xml`. - `get_remediation` is a deterministic MCP projection over existing suggestions/explainability data, not a second remediation engine. - `analysis_mode="clones_only"` must mirror the same metric/dependency @@ -276,18 +295,22 @@ state behind `codeclone://latest/...`. it returns `mixed` when run-to-run finding deltas and `health_delta` disagree. - `analysis_mode="clones_only"` keeps clone findings fully usable, but MCP surfaces mark `health` as unavailable instead of fabricating zeroed metrics. +- `coverage_xml` requires `analysis_mode="full"` because coverage join depends + on function-span metrics. - `codeclone://latest/triage` is a latest-only resource; run-specific triage is available via the tool, not via a `codeclone://runs/{run_id}/...` resource URI. ## Failure modes -| Condition | Behavior | -|--------------------------------------------|---------------------------------------------------| -| `mcp` extra not installed | `codeclone-mcp` prints install hint and exits `2` | -| Invalid root path / invalid numeric config | service raises contract error | -| Requested run missing | service raises run-not-found error | -| Requested finding missing | service raises finding-not-found error | -| Unsupported report section/resource suffix | service raises contract error | +| Condition | Behavior | +|---------------------------------------------------|---------------------------------------------------| +| `mcp` extra not installed | `codeclone-mcp` prints install hint and exits `2` | +| Invalid root path / invalid numeric config | service raises contract error | +| `coverage_xml` with `analysis_mode="clones_only"` | service raises contract error | +| Coverage hotspot gate without valid coverage join | service raises contract error | +| Requested run missing | service raises run-not-found error | +| Requested finding missing | service raises finding-not-found error | +| Unsupported report section/resource suffix | service raises contract error | ## Determinism / canonicalization @@ -297,6 +320,8 @@ state behind `codeclone://latest/...`. - No MCP-only heuristics may change analysis or gating semantics. - MCP must not re-synthesize design findings from raw metrics after the run; threshold-aware design findings belong to the canonical report document. +- Coverage join ordering and hotspot gates are inherited from canonical + `metrics.families.coverage_join` facts. ## Locked by tests diff --git a/docs/book/appendix/b-schema-layouts.md b/docs/book/appendix/b-schema-layouts.md index 86d3d13..54b0b4c 100644 --- a/docs/book/appendix/b-schema-layouts.md +++ b/docs/book/appendix/b-schema-layouts.md @@ -91,11 +91,11 @@ Notes: } ``` -## Cache schema (`2.3`) +## Cache schema (`2.4`) ```json { - "v": "2.3", + "v": "2.4", "payload": { "py": "cp313", "fp": "1", @@ -140,14 +140,16 @@ Notes: - `ss` stores per-file source stats and is required for full cache-hit accounting in discovery. - `rn`/`rq` are optional and decode to empty arrays when absent. +- Cached public-API symbol payloads preserve declaration order for `params`; + canonicalization must not rewrite callable signature order. - `u` row decoder accepts both legacy 11-column rows and canonical 17-column rows (legacy rows map new structural fields to neutral defaults). -## Report schema (`2.5`) +## Report schema (`2.8`) ```json { - "report_schema_version": "2.5", + "report_schema_version": "2.8", "meta": { "codeclone_version": "2.0.0b5", "project_name": "codeclone", @@ -199,14 +201,24 @@ Notes: "summary": { "...": "...", "suppressed": { - "dead_code": 0 + "dead_code": 0, + "clones": 1 } }, "groups": { "clones": { "functions": [], "blocks": [], - "segments": [] + "segments": [], + "suppressed": { + "functions": [ + { + "...": "..." + } + ], + "blocks": [], + "segments": [] + } }, "structural": { "groups": [ @@ -260,6 +272,21 @@ Notes: "docstring_permille": 0, "typing_any_count": 0 }, + "coverage_join": { + "status": "ok", + "source": "coverage.xml", + "files": 0, + "units": 0, + "measured_units": 0, + "overall_executable_lines": 0, + "overall_covered_lines": 0, + "overall_permille": 0, + "missing_from_report_units": 0, + "coverage_hotspots": 0, + "scope_gap_hotspots": 0, + "hotspot_threshold_percent": 50, + "invalid_reason": null + }, "api_surface": { "enabled": false, "modules": 0, @@ -322,6 +349,24 @@ Notes: }, "items": [] }, + "coverage_join": { + "summary": { + "status": "ok", + "source": "coverage.xml", + "files": 0, + "units": 0, + "measured_units": 0, + "overall_executable_lines": 0, + "overall_covered_lines": 0, + "overall_permille": 0, + "missing_from_report_units": 0, + "coverage_hotspots": 0, + "scope_gap_hotspots": 0, + "hotspot_threshold_percent": 50, + "invalid_reason": null + }, + "items": [] + }, "api_surface": { "summary": { "enabled": false, @@ -393,7 +438,7 @@ Notes: ```text # CodeClone Report - Markdown schema: 1.0 -- Source report schema: 2.5 +- Source report schema: 2.8 ... ## Overview ## Inventory @@ -479,7 +524,7 @@ Notes: ], "properties": { "profileVersion": "1.0", - "reportSchemaVersion": "2.5" + "reportSchemaVersion": "2.8" }, "results": [ { diff --git a/docs/mcp.md b/docs/mcp.md index a438644..ac7d5b4 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -126,11 +126,23 @@ run-scoped URI templates. - Summary `diff` also carries compact adoption/API deltas: `typing_param_permille_delta`, `typing_return_permille_delta`, `docstring_permille_delta`, `api_breaking_changes`, and `new_api_symbols`. +- When `analyze_repository` or `analyze_changed_paths` receives + `coverage_xml`, summaries include compact `coverage_join` facts. The XML path + may be absolute or relative to the analysis root, and the join remains a + current-run signal rather than baseline truth. +- When `respect_pyproject=true`, MCP also applies `golden_fixture_paths`. + Fully matching golden-fixture clone groups are excluded from active clone and + gate projections but remain visible in the canonical report under the + optional `findings.groups.clones.suppressed.*` bucket. +- Invalid Cobertura XML does not fail `analyze_*`; summaries expose + `coverage_join.status="invalid"` plus `invalid_reason`. Coverage hotspot gate + preview still requires a valid join. - Run IDs are 8-char hex handles; finding IDs are short prefixed forms. Both accept the full canonical form as input. - `metrics_detail(family="overloaded_modules")` exposes the report-only module-hotspot layer without turning it into findings or gate data. -- `metrics_detail` also accepts `coverage_adoption` and `api_surface`. +- `metrics_detail` also accepts `coverage_adoption`, `coverage_join`, and + `api_surface`. - `help(topic=...)` is static: meaning, anti-patterns, next step, doc links. - Start with repo defaults or `pyproject`-resolved thresholds, then lower them only for an explicit higher-sensitivity exploratory pass. @@ -178,7 +190,7 @@ analyze_repository → get_run_summary or get_production_triage ### Semantic uncertainty recovery ``` -help(topic="workflow" | "analysis_profile" | "baseline" | "suppressions" | "latest_runs" | "review_state" | "changed_scope") +help(topic="workflow" | "analysis_profile" | "baseline" | "coverage" | "suppressions" | "latest_runs" | "review_state" | "changed_scope") ``` ### Full repository review @@ -197,6 +209,17 @@ analyze_repository(api_surface=true) # when you need API inventory/diff → compare_runs ``` +### Coverage hotspot review + +``` +analyze_repository(coverage_xml="coverage.xml") +→ metrics_detail(family="coverage_join") +→ evaluate_gates(fail_on_untested_hotspots=true, coverage_min=50) + +Coverage Join in MCP separates measured `coverage_hotspots` from +`scope_gap_hotspots` (functions outside the supplied `coverage.xml` scope). +``` + ### Changed-files review (PR / patch) ``` @@ -243,6 +266,8 @@ Separate accepted baseline debt from new regressions. - Keep `git_diff_ref` to a safe single revision expression; option-like, whitespace-containing, and punctuated shell-style inputs are rejected. - Pass an absolute `root` — MCP rejects relative roots like `.`. +- Use `coverage_xml` only with `analysis_mode="full"`; clones-only analysis does + not collect the function-span facts needed for coverage join. - Use `"production-only"` / `source_kind` filters to cut test/fixture noise. - Use `mark_finding_reviewed` + `exclude_reviewed=true` in long sessions. diff --git a/docs/sarif.md b/docs/sarif.md index 3f3b7b1..2bb2518 100644 --- a/docs/sarif.md +++ b/docs/sarif.md @@ -68,6 +68,12 @@ For clone results, CodeClone also carries novelty-aware metadata when known: This improves usefulness in IDE/code-scanning flows that distinguish new vs known findings. +Coverage join can materialize `coverage` / `coverage_hotspot` and +`coverage_scope_gap` design findings when the canonical report already +contains valid `metrics.families.coverage_join` facts. SARIF projects those +findings like other design findings; it does not parse Cobertura XML or create +coverage-specific analysis truth. + ## Rule metadata Rule records are intentionally richer than a minimal SARIF export. diff --git a/pyproject.toml b/pyproject.toml index 6bc0987..c2abe99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,6 +100,22 @@ addopts = "-ra" branch = true source = ["codeclone"] +[tool.codeclone] +baseline = "codeclone.baseline.json" +min_loc = 6 +min_stmt = 4 +fail_on_new = true +fail_cycles = true +fail_dead_code = true +fail_health = 87 +fail_on_new_metrics = true +typing_coverage = false +docstring_coverage = false +api_surface = false +golden_fixture_paths = ["tests/fixtures/golden_*"] +min_typing_coverage = 99 + + [tool.coverage.report] show_missing = true fail_under = 99 diff --git a/tests/_assertions.py b/tests/_assertions.py index 1f4dd4a..8447d62 100644 --- a/tests/_assertions.py +++ b/tests/_assertions.py @@ -14,6 +14,11 @@ def assert_contains_all(text: str, *needles: str) -> None: assert needle in text +def assert_contains_none(text: str, *needles: str) -> None: + for needle in needles: + assert needle not in text + + def assert_mapping_entries( mapping: Mapping[str, object], /, @@ -23,6 +28,11 @@ def assert_mapping_entries( assert mapping[key] == value +def assert_missing_keys(mapping: Mapping[str, object], /, *keys: str) -> None: + for key in keys: + assert key not in mapping + + def snapshot_python_tag(snapshot: Mapping[str, object]) -> str: meta = snapshot.get("meta", {}) assert isinstance(meta, dict) diff --git a/tests/_ast_metrics_helpers.py b/tests/_ast_metrics_helpers.py new file mode 100644 index 0000000..cd75a99 --- /dev/null +++ b/tests/_ast_metrics_helpers.py @@ -0,0 +1,29 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Den Rozhnovskiy + +from __future__ import annotations + +import ast + +from codeclone import extractor +from codeclone.qualnames import QualnameCollector + + +def tree_collector_and_imports( + source: str, + *, + module_name: str, +) -> tuple[ast.Module, QualnameCollector, frozenset[str]]: + tree = ast.parse(source) + collector = QualnameCollector() + collector.visit(tree) + walk = extractor._collect_module_walk_data( + tree=tree, + module_name=module_name, + collector=collector, + collect_referenced_names=True, + ) + return tree, collector, walk.import_names diff --git a/tests/fixtures/golden_v2/pyproject_defaults/golden_expected_cli_snapshot.json b/tests/fixtures/golden_v2/pyproject_defaults/golden_expected_cli_snapshot.json index 1417c3a..ab5236f 100644 --- a/tests/fixtures/golden_v2/pyproject_defaults/golden_expected_cli_snapshot.json +++ b/tests/fixtures/golden_v2/pyproject_defaults/golden_expected_cli_snapshot.json @@ -2,7 +2,7 @@ "meta": { "python_tag": "cp313" }, - "report_schema_version": "2.5", + "report_schema_version": "2.8", "project_name": "pyproject_defaults", "scan_root": ".", "baseline_status": "missing", diff --git a/tests/test_adoption.py b/tests/test_adoption.py index 76ed21b..a4852f9 100644 --- a/tests/test_adoption.py +++ b/tests/test_adoption.py @@ -8,35 +8,18 @@ import ast -from codeclone import extractor from codeclone.metrics import _visibility as visibility_mod from codeclone.metrics import adoption as adoption_mod from codeclone.metrics._visibility import build_module_visibility from codeclone.metrics.adoption import collect_module_adoption from codeclone.qualnames import QualnameCollector - - -def _tree_collector_and_imports( - source: str, - *, - module_name: str, -) -> tuple[ast.Module, QualnameCollector, frozenset[str]]: - tree = ast.parse(source) - collector = QualnameCollector() - collector.visit(tree) - walk = extractor._collect_module_walk_data( - tree=tree, - module_name=module_name, - collector=collector, - collect_referenced_names=True, - ) - return tree, collector, walk.import_names +from tests._ast_metrics_helpers import tree_collector_and_imports def test_build_module_visibility_supports_strict_dunder_all_for_private_modules() -> ( None ): - tree, collector, import_names = _tree_collector_and_imports( + tree, collector, import_names = tree_collector_and_imports( """ __all__ = ["public_fn", "PublicClass"] @@ -66,7 +49,7 @@ class PublicClass: def test_collect_module_adoption_counts_annotations_docstrings_and_any() -> None: - tree, collector, import_names = _tree_collector_and_imports( + tree, collector, import_names = tree_collector_and_imports( """ from typing import Any @@ -120,7 +103,7 @@ def _hidden(self, value: int) -> None: def test_visibility_helpers_cover_private_modules_and_declared_all_edges() -> None: - tree, collector, import_names = _tree_collector_and_imports( + tree, collector, import_names = tree_collector_and_imports( """ items: list[str] = [] _private = 1 diff --git a/tests/test_api_surface.py b/tests/test_api_surface.py index 8190a7b..7cf1054 100644 --- a/tests/test_api_surface.py +++ b/tests/test_api_surface.py @@ -9,7 +9,6 @@ import ast from typing import Literal, cast -from codeclone import extractor from codeclone.metrics import api_surface as api_surface_mod from codeclone.metrics._visibility import ModuleVisibility from codeclone.metrics.api_surface import ( @@ -22,28 +21,11 @@ ModuleApiSurface, PublicSymbol, ) -from codeclone.qualnames import QualnameCollector - - -def _tree_collector_and_imports( - source: str, - *, - module_name: str, -) -> tuple[ast.Module, QualnameCollector, frozenset[str]]: - tree = ast.parse(source) - collector = QualnameCollector() - collector.visit(tree) - walk = extractor._collect_module_walk_data( - tree=tree, - module_name=module_name, - collector=collector, - collect_referenced_names=True, - ) - return tree, collector, walk.import_names +from tests._ast_metrics_helpers import tree_collector_and_imports def test_collect_module_api_surface_skips_self_and_collects_public_symbols() -> None: - tree, collector, import_names = _tree_collector_and_imports( + tree, collector, import_names = tree_collector_and_imports( """ __all__ = ["run", "Public", "VALUE"] @@ -227,7 +209,7 @@ def _public_symbol( def test_collect_module_api_surface_skips_private_or_empty_modules() -> None: - private_tree, private_collector, private_imports = _tree_collector_and_imports( + private_tree, private_collector, private_imports = tree_collector_and_imports( """ def hidden(): return 1 @@ -245,7 +227,7 @@ def hidden(): is None ) - empty_tree, empty_collector, empty_imports = _tree_collector_and_imports( + empty_tree, empty_collector, empty_imports = tree_collector_and_imports( """ def _hidden(): return 1 diff --git a/tests/test_benchmark.py b/tests/test_benchmark.py new file mode 100644 index 0000000..1963edc --- /dev/null +++ b/tests/test_benchmark.py @@ -0,0 +1,80 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Den Rozhnovskiy + +from __future__ import annotations + +import pytest + +from benchmarks.run_benchmark import ( + RunMeasurement, + Scenario, + _validate_inventory_sample, +) + + +def _measurement( + *, + found: int, + analyzed: int, + cached: int, + skipped: int = 0, +) -> RunMeasurement: + return RunMeasurement( + elapsed_seconds=0.1, + digest="digest", + files_found=found, + files_analyzed=analyzed, + files_cached=cached, + files_skipped=skipped, + ) + + +def test_benchmark_inventory_validation_accepts_valid_cold_and_warm_samples() -> None: + _validate_inventory_sample( + scenario=Scenario(name="cold_full", mode="cold", extra_args=()), + measurement=_measurement(found=10, analyzed=10, cached=0), + ) + _validate_inventory_sample( + scenario=Scenario(name="warm_full", mode="warm", extra_args=()), + measurement=_measurement(found=10, analyzed=0, cached=10), + ) + + +@pytest.mark.parametrize( + ("scenario", "measurement", "message"), + ( + ( + Scenario(name="cold_full", mode="cold", extra_args=()), + _measurement(found=10, analyzed=0, cached=0, skipped=10), + "skipped 10 files", + ), + ( + Scenario(name="cold_full", mode="cold", extra_args=()), + _measurement(found=10, analyzed=9, cached=1), + "unexpectedly used cache", + ), + ( + Scenario(name="warm_full", mode="warm", extra_args=()), + _measurement(found=10, analyzed=10, cached=0), + "did not use cache", + ), + ( + Scenario(name="warm_full", mode="warm", extra_args=()), + _measurement(found=10, analyzed=1, cached=9), + "analyzed files unexpectedly", + ), + ), +) +def test_benchmark_inventory_validation_rejects_invalid_samples( + scenario: Scenario, + measurement: RunMeasurement, + message: str, +) -> None: + with pytest.raises(RuntimeError, match=message): + _validate_inventory_sample( + scenario=scenario, + measurement=measurement, + ) diff --git a/tests/test_cache.py b/tests/test_cache.py index 49ddcdf..82674a3 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -21,6 +21,7 @@ from codeclone.cache_paths import runtime_filepath_from_wire, wire_filepath_from_runtime from codeclone.errors import CacheError from codeclone.extractor import Unit +from codeclone.models import ApiParamSpec, FileMetrics, ModuleApiSurface, PublicSymbol def _make_unit(filepath: str) -> Unit: @@ -110,6 +111,61 @@ def test_cache_roundtrip_preserves_empty_structural_findings(tmp_path: Path) -> assert entry["structural_findings"] == [] +def test_cache_roundtrip_preserves_api_surface_parameter_order( + tmp_path: Path, +) -> None: + cache_path = tmp_path / "cache.json" + cache = Cache(cache_path) + cache.put_file_entry( + "x.py", + {"mtime_ns": 1, "size": 10}, + [], + [], + [], + file_metrics=FileMetrics( + class_metrics=(), + module_deps=(), + dead_candidates=(), + referenced_names=frozenset(), + import_names=frozenset(), + class_names=frozenset(), + api_surface=ModuleApiSurface( + module="pkg.mod", + filepath="x.py", + all_declared=("run",), + symbols=( + PublicSymbol( + qualname="pkg.mod:run", + kind="function", + start_line=1, + end_line=2, + params=( + ApiParamSpec( + name="beta", + kind="pos_or_kw", + has_default=False, + ), + ApiParamSpec( + name="alpha", + kind="pos_or_kw", + has_default=False, + ), + ), + ), + ), + ), + ), + ) + cache.save() + + loaded = Cache(cache_path) + loaded.load() + entry = loaded.get_file_entry("x.py") + assert entry is not None + params = entry["api_surface"]["symbols"][0]["params"] + assert [param["name"] for param in params] == ["beta", "alpha"] + + def test_cache_load_normalizes_stale_structural_findings(tmp_path: Path) -> None: cache_path = tmp_path / "cache.json" cache = Cache(cache_path) diff --git a/tests/test_cli_config.py b/tests/test_cli_config.py index ecc60a0..14e6a9c 100644 --- a/tests/test_cli_config.py +++ b/tests/test_cli_config.py @@ -148,6 +148,11 @@ def test_apply_pyproject_config_overrides_respects_explicit_cli_flags() -> None: ("min_loc", 10, 10), ("baseline", "codeclone.baseline.json", "codeclone.baseline.json"), ("cache_path", None, None), + ( + "golden_fixture_paths", + ["tests/fixtures/golden_*", "tests/fixtures/golden_*"], + ("tests/fixtures/golden_*",), + ), ], ) def test_validate_config_value_accepts_expected_types( @@ -163,6 +168,8 @@ def test_validate_config_value_accepts_expected_types( ("update_baseline", "yes", "expected bool"), ("min_loc", True, "expected int"), ("baseline", 1, "expected str"), + ("golden_fixture_paths", "tests/fixtures/golden_*", "expected list\\[str\\]"), + ("golden_fixture_paths", ["pkg/*"], "must target tests/"), ], ) def test_validate_config_value_rejects_invalid_types( @@ -214,6 +221,30 @@ def test_normalize_path_config_value_behaviour(tmp_path: Path) -> None: ) == "/tmp/absolute-cache.json" ) + patterns = ("tests/fixtures/golden_*",) + assert ( + cfg_mod._normalize_path_config_value( + key="golden_fixture_paths", + value=patterns, + root_path=tmp_path, + ) + == patterns + ) + + +def test_load_pyproject_config_accepts_golden_fixture_paths(tmp_path: Path) -> None: + _write_pyproject( + tmp_path / "pyproject.toml", + """ +[tool.codeclone] +golden_fixture_paths = [ + "./tests/fixtures/golden_*", + "tests/fixtures/golden_*", +] +""".strip(), + ) + loaded = cfg_mod.load_pyproject_config(tmp_path) + assert loaded["golden_fixture_paths"] == ("tests/fixtures/golden_*",) def test_load_toml_py310_missing_tomli_raises( diff --git a/tests/test_cli_inprocess.py b/tests/test_cli_inprocess.py index 022c726..a9e715a 100644 --- a/tests/test_cli_inprocess.py +++ b/tests/test_cli_inprocess.py @@ -31,7 +31,11 @@ ) from codeclone.errors import CacheError from codeclone.models import Unit -from tests._assertions import assert_contains_all, assert_mapping_entries +from tests._assertions import ( + assert_contains_all, + assert_mapping_entries, + assert_missing_keys, +) from tests._report_access import ( report_clone_groups as _report_clone_groups, ) @@ -228,6 +232,19 @@ def f2(): ) +def _write_duplicate_function_module(directory: Path, filename: str) -> Path: + return _write_python_module( + directory, + filename, + """ +def duplicated(): + value = 1 + return value +""".strip() + + "\n", + ) + + def _prepare_basic_project(root: Path) -> Path: root.mkdir() return _write_python_module(root, "a.py") @@ -2305,6 +2322,70 @@ def test_cli_update_baseline_report_meta_uses_updated_payload_hash( assert baseline_meta["payload_sha256_verified"] is True +def test_cli_update_baseline_rewrites_embedded_metrics_to_enabled_surfaces_only( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + _write_python_module( + tmp_path, + "a.py", + """ +def public(value: int) -> int: + return value +""", + ) + baseline = tmp_path / "codeclone.baseline.json" + + _run_parallel_main( + monkeypatch, + [ + str(tmp_path), + "--baseline", + str(baseline), + "--update-baseline", + "--api-surface", + "--no-progress", + ], + ) + initial_payload = json.loads(baseline.read_text("utf-8")) + assert "api_surface" in initial_payload + assert "typing_param_permille" in initial_payload["metrics"] + + (tmp_path / "pyproject.toml").write_text( + """ +[tool.codeclone] +baseline = "codeclone.baseline.json" +api_surface = false +typing_coverage = false +docstring_coverage = false +""".strip() + + "\n", + "utf-8", + ) + + _run_parallel_main( + monkeypatch, + [ + str(tmp_path), + "--update-baseline", + "--no-progress", + ], + ) + + payload = json.loads(baseline.read_text("utf-8")) + meta = cast(dict[str, object], payload["meta"]) + metrics = cast(dict[str, object], payload["metrics"]) + assert_missing_keys(payload, "api_surface") + assert_missing_keys(meta, "api_surface_payload_sha256") + assert_missing_keys( + metrics, + "typing_param_permille", + "typing_return_permille", + "docstring_permille", + "typing_any_count", + ) + + def test_cli_update_baseline_write_error_is_contract_error( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -3322,6 +3403,162 @@ def test_cli_summary_with_api_surface_shows_public_api_line( assert "modules" in out +def test_cli_ci_summary_includes_adoption_and_public_api_lines( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + src = tmp_path / "a.py" + metrics_baseline_path = tmp_path / "metrics-baseline.json" + src.write_text("def f(value: int) -> int:\n return value\n", "utf-8") + baseline_path = _write_baseline( + tmp_path / "baseline.json", + python_version=f"{sys.version_info.major}.{sys.version_info.minor}", + ) + _patch_parallel(monkeypatch) + _run_main( + monkeypatch, + [ + str(tmp_path), + "--no-progress", + "--api-surface", + "--metrics-baseline", + str(metrics_baseline_path), + "--update-metrics-baseline", + ], + ) + _ = capsys.readouterr() + _run_main( + monkeypatch, + [ + str(tmp_path), + "--ci", + "--baseline", + str(baseline_path), + "--metrics-baseline", + str(metrics_baseline_path), + "--api-surface", + ], + ) + out = capsys.readouterr().out + assert "Adoption" in out + assert "Public API" in out + assert "symbols=" in out + assert "docstrings=" in out + + +def test_cli_pyproject_golden_fixture_paths_exclude_fixture_clone_groups( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + fixtures_dir = tmp_path / "tests" / "fixtures" / "golden_project" + fixtures_dir.mkdir(parents=True) + _write_duplicate_function_module(fixtures_dir, "a.py") + _write_duplicate_function_module(fixtures_dir, "b.py") + report_path = tmp_path / "report.json" + (tmp_path / "pyproject.toml").write_text( + """ +[tool.codeclone] +min_loc = 1 +min_stmt = 1 +fail_on_new = true +skip_metrics = true +golden_fixture_paths = ["tests/fixtures/golden_*"] +""".strip() + + "\n", + "utf-8", + ) + + _run_parallel_main( + monkeypatch, + [ + str(tmp_path), + "--no-progress", + "--json", + str(report_path), + ], + ) + + payload = json.loads(report_path.read_text("utf-8")) + clone_groups = cast( + "dict[str, object]", + cast("dict[str, object]", payload["findings"])["groups"], + )["clones"] + clone_groups_map = cast("dict[str, object]", clone_groups) + assert clone_groups_map["functions"] == [] + suppressed = cast("dict[str, object]", clone_groups_map["suppressed"]) + suppressed_functions = cast("list[dict[str, object]]", suppressed["functions"]) + assert len(suppressed_functions) == 1 + assert suppressed_functions[0]["suppression_rule"] == "golden_fixture" + assert ( + cast("dict[str, int]", payload["findings"]["summary"]["clones"])["suppressed"] + == 1 + ) + + +def test_cli_public_api_breaking_count_stable_across_warm_cache( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + src = tmp_path / "pkg.py" + metrics_baseline_path = tmp_path / "metrics-baseline.json" + cache_path = tmp_path / "cache.json" + src.write_text( + "def run(alpha: int, beta: int) -> int:\n return alpha + beta\n", + "utf-8", + ) + _patch_parallel(monkeypatch) + _run_main( + monkeypatch, + [ + str(tmp_path), + "--no-progress", + "--api-surface", + "--metrics-baseline", + str(metrics_baseline_path), + "--update-metrics-baseline", + ], + ) + _ = capsys.readouterr() + + src.write_text( + "def run(beta: int, alpha: int) -> int:\n return alpha + beta\n", + "utf-8", + ) + + _run_main( + monkeypatch, + [ + str(tmp_path), + "--no-progress", + "--api-surface", + "--metrics-baseline", + str(metrics_baseline_path), + "--cache-path", + str(cache_path), + ], + ) + cold_out = capsys.readouterr().out + + _run_main( + monkeypatch, + [ + str(tmp_path), + "--no-progress", + "--api-surface", + "--metrics-baseline", + str(metrics_baseline_path), + "--cache-path", + str(cache_path), + ], + ) + warm_out = capsys.readouterr().out + + assert "1 breaking" in cold_out + assert "1 breaking" in warm_out + + def test_cli_summary_no_color_has_no_ansi( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, diff --git a/tests/test_cli_unit.py b/tests/test_cli_unit.py index 3ccb414..a239a1b 100644 --- a/tests/test_cli_unit.py +++ b/tests/test_cli_unit.py @@ -940,6 +940,7 @@ def test_print_summary_invariant_warning( func_clones_count=0, block_clones_count=0, segment_clones_count=0, + suppressed_golden_fixture_groups=0, suppressed_segment_groups=0, new_clones_count=0, ) @@ -969,6 +970,51 @@ def test_compact_summary_labels_use_machine_scannable_keys() -> None: == "Metrics cc=2.8/21 cbo=0.6/8 lcom4=1.2/4" " cycles=0 dead_code=1 health=85(B) overloaded_modules=3" ) + assert ( + ui.fmt_summary_compact_adoption( + param_permille=750, + return_permille=500, + docstring_permille=667, + any_annotation_count=1, + ) + == "Adoption params=75.0% returns=50.0% docstrings=66.7% any=1" + ) + assert ( + ui.fmt_summary_compact_api_surface( + public_symbols=3, + modules=2, + breaking=1, + added=4, + ) + == "Public API symbols=3 modules=2 breaking=1 added=4" + ) + assert ( + ui.fmt_summary_compact_clones( + function=1, + block=2, + segment=0, + suppressed=3, + fixture_excluded=2, + new=4, + ) + == "Clones func=1 block=2 seg=0 suppressed=3 fixtures=2 new=4" + ) + assert ( + ui.fmt_summary_compact_coverage_join( + status="ok", + overall_permille=735, + coverage_hotspots=2, + scope_gap_hotspots=1, + threshold_percent=50, + source_label="coverage.xml", + ) + == "Coverage status=ok overall=73.5% coverage_hotspots=2" + " threshold=50 scope_gaps=1 source=coverage.xml" + ) + assert ( + ui.fmt_coverage_join_ignored("bad xml") + == "[warning]Coverage join ignored: bad xml[/warning]" + ) def test_ui_summary_formatters_cover_optional_branches() -> None: @@ -986,9 +1032,11 @@ def test_ui_summary_formatters_cover_optional_branches() -> None: block=2, segment=3, suppressed=1, + fixture_excluded=2, new=0, ) assert "[bold yellow]3[/bold yellow] seg" in clones + assert "[yellow]2[/yellow] fixtures" in clones assert "5 detected" in ui.fmt_metrics_cycles(5) dead_with_suppressed = ui.fmt_metrics_dead_code(447, suppressed=9) @@ -1036,6 +1084,34 @@ def test_ui_summary_formatters_cover_optional_branches() -> None: added=4, ) assert_contains_all(api_surface, "symbols", "modules", "breaking", "added") + coverage_join = ui.fmt_metrics_coverage_join( + status="ok", + overall_permille=735, + coverage_hotspots=2, + scope_gap_hotspots=1, + threshold_percent=50, + source_label="coverage.xml", + ) + assert_contains_all( + coverage_join, + "73.5% overall", + "[bold red]2[/bold red] hotspots < 50%", + "[bold yellow]1[/bold yellow] scope gaps", + "coverage.xml", + ) + coverage_join_unavailable = ui.fmt_metrics_coverage_join( + status="invalid", + overall_permille=0, + coverage_hotspots=0, + scope_gap_hotspots=0, + threshold_percent=50, + source_label="coverage.xml", + ) + assert_contains_all( + coverage_join_unavailable, + "join unavailable", + "coverage.xml", + ) changed_paths = ui.fmt_changed_scope_paths(count=45) assert "45" in changed_paths assert "from git diff" in changed_paths @@ -1124,7 +1200,60 @@ def test_print_metrics_in_quiet_mode_includes_overloaded_modules( assert "Public API" not in out -def test_print_metrics_in_normal_mode_includes_adoption_and_public_api( +def test_print_metrics_in_quiet_mode_includes_adoption_public_api_and_coverage( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + monkeypatch.setattr(cli, "console", cli._make_console(no_color=True)) + cli_summary._print_metrics( + console=cast("cli_summary._Printer", cli.console), + quiet=True, + metrics=cli_summary.MetricsSnapshot( + complexity_avg=2.8, + complexity_max=20, + high_risk_count=0, + coupling_avg=0.5, + coupling_max=9, + cohesion_avg=1.2, + cohesion_max=4, + cycles_count=0, + dead_code_count=0, + health_total=85, + health_grade="B", + adoption_param_permille=750, + adoption_return_permille=500, + adoption_docstring_permille=667, + adoption_any_annotation_count=1, + api_surface_enabled=True, + api_surface_modules=2, + api_surface_public_symbols=3, + api_surface_added=4, + api_surface_breaking=1, + coverage_join_status="ok", + coverage_join_overall_permille=735, + coverage_join_coverage_hotspots=2, + coverage_join_scope_gap_hotspots=1, + coverage_join_threshold_percent=50, + coverage_join_source_label="coverage.xml", + overloaded_modules_candidates=3, + overloaded_modules_total=158, + overloaded_modules_population_status="ok", + overloaded_modules_top_score=0.98, + ), + ) + out = capsys.readouterr().out + assert_contains_all( + out, + "Adoption", + "params=75.0%", + "Public API", + "breaking=1", + "Coverage", + "coverage_hotspots=2", + "source=coverage.xml", + ) + + +def test_print_metrics_in_normal_mode_includes_adoption_public_api_and_coverage( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: monkeypatch.setattr(cli, "console", cli._make_console(no_color=True)) @@ -1152,6 +1281,12 @@ def test_print_metrics_in_normal_mode_includes_adoption_and_public_api( api_surface_public_symbols=3, api_surface_added=4, api_surface_breaking=1, + coverage_join_status="ok", + coverage_join_overall_permille=735, + coverage_join_coverage_hotspots=2, + coverage_join_scope_gap_hotspots=1, + coverage_join_threshold_percent=50, + coverage_join_source_label="coverage.xml", overloaded_modules_candidates=3, overloaded_modules_total=158, overloaded_modules_population_status="ok", @@ -1167,6 +1302,9 @@ def test_print_metrics_in_normal_mode_includes_adoption_and_public_api( "Public API", "3 symbols", "1 breaking", + "Coverage", + "73.5% overall", + "2 hotspots < 50%", ) @@ -1211,6 +1349,36 @@ def test_configure_metrics_mode_forces_dependency_and_dead_code_when_gated() -> assert args.skip_dependencies is False +def test_configure_metrics_mode_does_not_force_api_surface_for_baseline_update() -> ( + None +): + args = Namespace( + skip_metrics=False, + fail_complexity=-1, + fail_coupling=-1, + fail_cohesion=-1, + fail_cycles=False, + fail_dead_code=False, + fail_health=-1, + fail_on_new_metrics=False, + fail_on_typing_regression=False, + fail_on_docstring_regression=False, + fail_on_api_break=False, + fail_on_untested_hotspots=False, + min_typing_coverage=-1, + min_docstring_coverage=-1, + update_metrics_baseline=True, + skip_dead_code=False, + skip_dependencies=False, + api_surface=False, + coverage_xml=None, + ) + + cli._configure_metrics_mode(args=args, metrics_baseline_exists=True) + + assert args.api_surface is False + + def test_probe_metrics_baseline_section_for_non_object_payload(tmp_path: Path) -> None: path = tmp_path / "baseline.json" path.write_text("[]", "utf-8") @@ -1270,6 +1438,94 @@ def test_metrics_computed_includes_api_surface_only_when_enabled() -> None: ) +def test_metrics_computed_includes_coverage_join_only_with_xml() -> None: + assert cli._metrics_computed( + Namespace( + skip_metrics=False, + skip_dependencies=True, + skip_dead_code=True, + api_surface=False, + coverage_xml=None, + ) + ) == ("complexity", "coupling", "cohesion", "health", "coverage_adoption") + assert cli._metrics_computed( + Namespace( + skip_metrics=False, + skip_dependencies=True, + skip_dead_code=True, + api_surface=False, + coverage_xml="coverage.xml", + ) + ) == ( + "complexity", + "coupling", + "cohesion", + "health", + "coverage_adoption", + "coverage_join", + ) + + +def test_enforce_gating_requires_coverage_input_for_hotspot_gate( + monkeypatch: pytest.MonkeyPatch, +) -> None: + cli.console = cli._make_console(no_color=True) + monkeypatch.setattr(cli, "gate", lambda **_kwargs: pipeline.GatingResult(0, ())) + with pytest.raises(SystemExit) as exc: + cli._enforce_gating( + args=Namespace( + fail_on_untested_hotspots=True, + fail_threshold=-1, + verbose=False, + ), + boot=cast("pipeline.BootstrapResult", object()), + analysis=cast(Any, SimpleNamespace(coverage_join=None)), + processing=cast(Any, Namespace(source_read_failures=[])), + source_read_contract_failure=False, + baseline_failure_code=None, + metrics_baseline_failure_code=None, + new_func=set(), + new_block=set(), + metrics_diff=None, + html_report_path=None, + ) + assert exc.value.code == 2 + + +def test_enforce_gating_requires_valid_coverage_input_for_hotspot_gate( + monkeypatch: pytest.MonkeyPatch, +) -> None: + cli.console = cli._make_console(no_color=True) + monkeypatch.setattr(cli, "gate", lambda **_kwargs: pipeline.GatingResult(0, ())) + with pytest.raises(SystemExit) as exc: + cli._enforce_gating( + args=Namespace( + fail_on_untested_hotspots=True, + fail_threshold=-1, + verbose=False, + ), + boot=cast("pipeline.BootstrapResult", object()), + analysis=cast( + Any, + SimpleNamespace( + coverage_join=SimpleNamespace( + status="invalid", + invalid_reason="broken xml", + ) + ), + ), + processing=cast(Any, Namespace(source_read_failures=[])), + source_read_contract_failure=False, + baseline_failure_code=None, + metrics_baseline_failure_code=None, + new_func=set(), + new_block=set(), + metrics_diff=None, + html_report_path=None, + ) + assert exc.value.code == 2 + + def test_main_impl_exits_on_invalid_pyproject_config( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: diff --git a/tests/test_coverage_join.py b/tests/test_coverage_join.py new file mode 100644 index 0000000..ceaa424 --- /dev/null +++ b/tests/test_coverage_join.py @@ -0,0 +1,294 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Den Rozhnovskiy + +from pathlib import Path +from xml.etree import ElementTree + +import pytest + +from codeclone.metrics.coverage_join import ( + CoverageJoinParseError, + _iter_cobertura_class_elements, + _iter_cobertura_line_hits, + _local_tag_name, + _resolve_report_filename, + _resolved_coverage_sources, + build_coverage_join, +) + + +def test_build_coverage_join_maps_cobertura_lines_to_function_spans( + tmp_path: Path, +) -> None: + source = tmp_path / "pkg" / "mod.py" + source.parent.mkdir() + source.write_text( + "\n".join( + ( + "def hot(value):", + " if value:", + " return value", + "", + "def covered():", + " return 1", + "", + "def no_lines():", + " return 2", + ) + ) + + "\n", + encoding="utf-8", + ) + coverage_xml = tmp_path / "coverage.xml" + coverage_xml.write_text( + """ + + + . + + + + + + + + + + + + + + + + +""", + encoding="utf-8", + ) + missing_source = tmp_path / "pkg" / "missing.py" + + result = build_coverage_join( + coverage_xml=coverage_xml, + root_path=tmp_path, + hotspot_threshold_percent=60, + units=( + { + "qualname": "pkg.mod:covered", + "filepath": str(source), + "start_line": 5, + "end_line": 6, + "cyclomatic_complexity": 1, + "risk": "medium", + }, + { + "qualname": "pkg.mod:no_lines", + "filepath": str(source), + "start_line": 8, + "end_line": 9, + "cyclomatic_complexity": 1, + "risk": "high", + }, + { + "qualname": "pkg.missing:lost", + "filepath": str(missing_source), + "start_line": 1, + "end_line": 2, + "cyclomatic_complexity": 8, + "risk": "high", + }, + { + "qualname": "pkg.mod:hot", + "filepath": str(source), + "start_line": 1, + "end_line": 3, + "cyclomatic_complexity": 12, + "risk": "high", + }, + ), + ) + + assert result.status == "ok" + assert result.coverage_xml == str(coverage_xml.resolve()) + assert result.files == 1 + assert result.measured_units == 2 + assert result.overall_executable_lines == 4 + assert result.overall_covered_lines == 3 + assert result.coverage_hotspots == 1 + assert result.scope_gap_hotspots == 1 + assert [fact.qualname for fact in result.units] == [ + "pkg.missing:lost", + "pkg.mod:hot", + "pkg.mod:covered", + "pkg.mod:no_lines", + ] + + missing, hot, covered, no_lines = result.units + assert missing.coverage_status == "missing_from_report" + assert missing.coverage_permille == 0 + assert hot.coverage_status == "measured" + assert hot.executable_lines == 2 + assert hot.covered_lines == 1 + assert hot.coverage_permille == 500 + assert covered.coverage_status == "measured" + assert covered.coverage_permille == 1000 + assert no_lines.coverage_status == "no_executable_lines" + assert no_lines.coverage_permille == 0 + + +def test_build_coverage_join_rejects_invalid_cobertura_xml(tmp_path: Path) -> None: + coverage_xml = tmp_path / "coverage.xml" + coverage_xml.write_text("", encoding="utf-8") + + with pytest.raises(CoverageJoinParseError, match="Invalid Cobertura XML"): + build_coverage_join( + coverage_xml=coverage_xml, + root_path=tmp_path, + hotspot_threshold_percent=50, + units=(), + ) + + +def test_coverage_join_resolves_sources_and_filenames(tmp_path: Path) -> None: + root_element = ElementTree.fromstring( + """ + + src + src + + pkg + +""" + ) + source_roots = _resolved_coverage_sources( + root_element=root_element, + root_path=tmp_path, + ) + expected_roots = ( + tmp_path.resolve(), + (tmp_path / "src").resolve(), + (tmp_path / "pkg").resolve(), + ) + + assert source_roots == expected_roots + assert _local_tag_name(123) == "" + assert _local_tag_name("{urn:test}source") == "source" + + existing = tmp_path / "pkg" / "mod.py" + existing.parent.mkdir() + existing.write_text("def run():\n return 1\n", encoding="utf-8") + + assert _resolve_report_filename( + filename="mod.py", + root_path=tmp_path, + source_roots=(tmp_path / "pkg",), + ) == str(existing.resolve()) + assert _resolve_report_filename( + filename="missing.py", + root_path=tmp_path, + source_roots=(), + ) == str((tmp_path / "missing.py").resolve()) + assert ( + _resolve_report_filename( + filename="", + root_path=tmp_path, + source_roots=(), + ) + is None + ) + assert ( + _resolve_report_filename( + filename=str(tmp_path.parent / "outside.py"), + root_path=tmp_path, + source_roots=(), + ) + is None + ) + + +def test_coverage_join_path_resolution_fallbacks( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + def _raise_os_error( + _self: Path, + *_args: object, + **_kwargs: object, + ) -> Path: + raise OSError("path resolution failed") + + monkeypatch.setattr(Path, "resolve", _raise_os_error) + root_element = ElementTree.fromstring( + f""" + + {tmp_path} + +""" + ) + + assert _resolved_coverage_sources( + root_element=root_element, + root_path=tmp_path, + ) == (tmp_path.absolute(),) + + +def test_coverage_join_filters_cobertura_elements_and_unknown_risk( + tmp_path: Path, +) -> None: + source = tmp_path / "pkg" / "mod.py" + source.parent.mkdir() + source.write_text("def run():\n return 1\n", encoding="utf-8") + coverage_xml = tmp_path / "coverage.xml" + coverage_xml.write_text( + """ + + + + + + + + + + + + + + + + +""", + encoding="utf-8", + ) + + root_element = ElementTree.parse(coverage_xml).getroot() + classes = _iter_cobertura_class_elements(root_element) + + assert [item.attrib["name"] for item in classes] == ["empty", "mod"] + assert _iter_cobertura_line_hits(classes[1]) == ((1, 0), (2, 1)) + + result = build_coverage_join( + coverage_xml=coverage_xml, + root_path=tmp_path, + hotspot_threshold_percent=50, + units=( + { + "qualname": "pkg.mod:run", + "filepath": str(source), + "start_line": 1, + "end_line": 2, + "cyclomatic_complexity": 1, + "risk": "dynamic", + }, + ), + ) + + fact = result.units[0] + assert (fact.risk, fact.coverage_status, fact.coverage_permille) == ( + "low", + "measured", + 500, + ) + assert result.coverage_hotspots == 0 + assert result.scope_gap_hotspots == 0 diff --git a/tests/test_golden_fixtures.py b/tests/test_golden_fixtures.py new file mode 100644 index 0000000..398e52d --- /dev/null +++ b/tests/test_golden_fixtures.py @@ -0,0 +1,140 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Den Rozhnovskiy + +from __future__ import annotations + +import pytest + +from codeclone.golden_fixtures import ( + GoldenFixturePatternError, + build_suppressed_clone_groups, + normalize_golden_fixture_patterns, + path_matches_golden_fixture_pattern, + split_clone_groups_for_golden_fixtures, +) + + +def test_normalize_golden_fixture_patterns_rejects_non_test_scope() -> None: + with pytest.raises(GoldenFixturePatternError, match="must target tests/"): + normalize_golden_fixture_patterns(["pkg/golden_*"]) + + +@pytest.mark.parametrize( + ("pattern", "message"), + [ + ("", "must be non-empty"), + ("/tmp/golden_*", "must be repo-relative"), + ("tests/../fixtures/golden_*", "must not contain '..'"), + ], +) +def test_normalize_golden_fixture_patterns_rejects_invalid_entries( + pattern: str, + message: str, +) -> None: + with pytest.raises(GoldenFixturePatternError, match=message): + normalize_golden_fixture_patterns([pattern]) + + +def test_path_matches_golden_fixture_pattern_matches_directory_subtrees() -> None: + assert path_matches_golden_fixture_pattern( + "tests/fixtures/golden_project/alpha.py", + "tests/fixtures/golden_*", + ) + assert not path_matches_golden_fixture_pattern( + "tests/helpers/golden_project/alpha.py", + "tests/fixtures/golden_*", + ) + + +def test_path_matches_golden_fixture_pattern_rejects_empty_relative_path() -> None: + assert not path_matches_golden_fixture_pattern("", "tests/fixtures/golden_*") + + +def test_split_clone_groups_for_golden_fixtures_requires_full_group_match() -> None: + split = split_clone_groups_for_golden_fixtures( + groups={ + "golden": [ + {"filepath": "/repo/tests/fixtures/golden_project/a.py"}, + {"filepath": "/repo/tests/fixtures/golden_project/b.py"}, + ], + "mixed": [ + {"filepath": "/repo/tests/fixtures/golden_project/c.py"}, + {"filepath": "/repo/pkg/mod.py"}, + ], + }, + kind="function", + golden_fixture_paths=("tests/fixtures/golden_*",), + scan_root="/repo", + ) + + assert set(split.active_groups) == {"mixed"} + assert set(split.suppressed_groups) == {"golden"} + assert split.matched_patterns == { + "golden": ("tests/fixtures/golden_*",), + } + + +def test_split_clone_groups_for_golden_fixtures_keeps_missing_or_unmatched_items() -> ( + None +): + split = split_clone_groups_for_golden_fixtures( + groups={ + "missing": [ + {"filepath": ""}, + {"filepath": "/repo/tests/fixtures/golden_project/b.py"}, + ], + "unmatched": [ + {"filepath": "/repo/tests/golden_project/a.py"}, + {"filepath": "/repo/tests/golden_project/b.py"}, + ], + }, + kind="function", + golden_fixture_paths=("tests/fixtures/golden_*",), + scan_root="/repo", + ) + + assert set(split.active_groups) == {"missing", "unmatched"} + assert split.suppressed_groups == {} + assert split.matched_patterns == {} + + +def test_build_suppressed_clone_groups_carries_rule_and_patterns() -> None: + suppressed = build_suppressed_clone_groups( + kind="function", + groups={ + "golden": [ + { + "filepath": "/repo/tests/fixtures/golden_project/a.py", + "qualname": "tests.fixtures.golden_project.a:run", + } + ] + }, + matched_patterns={"golden": ("tests/fixtures/golden_*",)}, + ) + + assert len(suppressed) == 1 + group = suppressed[0] + assert group.group_key == "golden" + assert group.matched_patterns == ("tests/fixtures/golden_*",) + assert group.suppression_rule == "golden_fixture" + assert group.suppression_source == "project_config" + + +def test_build_suppressed_clone_groups_skips_blank_pattern_bindings() -> None: + suppressed = build_suppressed_clone_groups( + kind="function", + groups={ + "golden": [ + { + "filepath": "/repo/tests/fixtures/golden_project/a.py", + "qualname": "tests.fixtures.golden_project.a:run", + } + ] + }, + matched_patterns={"golden": ("", " ")}, + ) + + assert suppressed == () diff --git a/tests/test_html_report.py b/tests/test_html_report.py index 5620513..4fb9351 100644 --- a/tests/test_html_report.py +++ b/tests/test_html_report.py @@ -12,6 +12,7 @@ import pytest +from codeclone._html_badges import _tab_empty_info from codeclone.contracts import ( CACHE_VERSION, DOCS_URL, @@ -33,6 +34,7 @@ StructuralFindingGroup, StructuralFindingOccurrence, Suggestion, + SuppressedCloneGroup, ) from codeclone.report import build_block_group_facts from codeclone.report.json_contract import ( @@ -41,6 +43,7 @@ structural_group_id, ) from codeclone.report.serialize import render_json_report_document +from tests._assertions import assert_contains_all from tests._report_fixtures import ( REPEATED_ASSERT_SOURCE, repeated_block_group_key, @@ -1356,6 +1359,66 @@ def test_html_report_segments_section(tmp_path: Path) -> None: assert "Segment clones" in html +def test_html_report_clone_tab_renders_suppressed_golden_fixture_groups( + tmp_path: Path, +) -> None: + fixture_file = tmp_path / "tests" / "fixtures" / "golden_project" / "alpha.py" + fixture_file_2 = tmp_path / "tests" / "fixtures" / "golden_project" / "beta.py" + fixture_file.parent.mkdir(parents=True, exist_ok=True) + fixture_file.write_text("def run():\n return 1\n", "utf-8") + fixture_file_2.write_text("def run():\n return 2\n", "utf-8") + + suppressed_group = SuppressedCloneGroup( + kind="function", + group_key="tests.fixtures.golden.alpha:run", + items=( + { + "qualname": "tests.fixtures.golden.alpha:run", + "filepath": str(fixture_file), + "start_line": 1, + "end_line": 2, + "loc": 2, + "stmt_count": 1, + "fingerprint": "fp-run", + "loc_bucket": "0-19", + }, + { + "qualname": "tests.fixtures.golden.beta:run", + "filepath": str(fixture_file_2), + "start_line": 1, + "end_line": 2, + "loc": 2, + "stmt_count": 1, + "fingerprint": "fp-run", + "loc_bucket": "0-19", + }, + ), + matched_patterns=("tests/fixtures/golden_*",), + suppression_rule="golden_fixture", + suppression_source="project_config", + ) + report_document = build_report_document( + func_groups={}, + block_groups={}, + segment_groups={}, + meta={"scan_root": str(tmp_path)}, + suppressed_clone_groups=(suppressed_group,), + ) + + html = build_html_report( + func_groups={}, + block_groups={}, + segment_groups={}, + report_meta={"scan_root": str(tmp_path)}, + report_document=report_document, + ) + + assert "Suppressed" in html + assert "golden_fixture@project_config" in html + assert "tests/fixtures/golden_*" in html + assert "No code clones detected" not in html + + def test_html_report_single_item_group(tmp_path: Path) -> None: f = tmp_path / "a.py" f.write_text("def f():\n x = 1\n", "utf-8") @@ -1641,15 +1704,15 @@ def test_html_report_metrics_risk_branches() -> None: dead_critical=2, ), ) - assert "insight-risk" in html - assert 'stroke="var(--error)"' in html - assert "Cycles: 1; max dependency depth: 4." in html - assert "5 candidates total; 2 high-confidence items; 0 suppressed." in html - assert '' f'
' - f'' - f'" + f'" + f'" "
" ) # -- Footer -- version = str(ctx.meta.get("codeclone_version", __version__)) + _report_schema = ctx.report_schema_version + _baseline_schema = _meta_pick( + ctx.meta.get("baseline_schema_version"), + ctx.baseline_meta.get("schema_version"), + ) + _cache_schema = _meta_pick( + ctx.meta.get("cache_schema_version"), + ctx.cache_meta.get("schema_version"), + ) + _schema_parts: list[str] = [] + if _report_schema: + _schema_parts.append(f"Report schema {_escape_html(str(_report_schema))}") + if _baseline_schema: + _schema_parts.append(f"Baseline schema {_escape_html(str(_baseline_schema))}") + if _cache_schema: + _schema_parts.append(f"Cache schema {_escape_html(str(_cache_schema))}") + _schema_line = ( + f'' + if _schema_parts + else "" + ) footer_html = ( '
' + '" + f"{_schema_line}" "
" ) diff --git a/codeclone/_html_report/_components.py b/codeclone/_html_report/_components.py index ca583d8..7a9fcae 100644 --- a/codeclone/_html_report/_components.py +++ b/codeclone/_html_report/_components.py @@ -12,7 +12,7 @@ from typing import Literal from .._coerce import as_int as _as_int -from .._html_badges import _source_kind_badge_html +from .._html_badges import _inline_empty, _source_kind_badge_html from .._html_escape import _escape_html from ._icons import section_icon_html @@ -89,7 +89,7 @@ def overview_source_breakdown_html(breakdown: Mapping[str, object]) -> str: ) rows = [(kind, count) for kind, count in sorted_items if count > 0] if not rows: - return '
n/a
' + return _inline_empty("No source data available", tone="neutral") total = sum(c for _, c in rows) parts: list[str] = [] diff --git a/codeclone/_html_report/_icons.py b/codeclone/_html_report/_icons.py index 864a8ab..87b68c2 100644 --- a/codeclone/_html_report/_icons.py +++ b/codeclone/_html_report/_icons.py @@ -55,10 +55,25 @@ def _svg_with_class(size: int, sw: str, body: str, *, class_name: str = "") -> s "2.5", '', ), - "theme": _svg( + "theme_moon": _svg_with_class( 16, "2", '', + class_name="theme-icon theme-icon-moon", + ), + "theme_sun": _svg_with_class( + 16, + "2", + '' + '' + '' + '' + '' + '' + '' + '' + '', + class_name="theme-icon theme-icon-sun", ), "check": _svg( 48, diff --git a/codeclone/_html_report/_sections/_clones.py b/codeclone/_html_report/_sections/_clones.py index ef417d3..65ab657 100644 --- a/codeclone/_html_report/_sections/_clones.py +++ b/codeclone/_html_report/_sections/_clones.py @@ -254,44 +254,77 @@ def _render_section_toolbar( section_title: str, group_count: int, ) -> str: - return ( - f'" ) + normalized_groups = normalize_structural_findings(groups) + if not normalized_groups: + return intro + _tab_empty("No structural findings detected.") resolved_file_cache = file_cache if file_cache is not None else _FileCache() why_templates: list[str] = [] diff --git a/codeclone/cache.py b/codeclone/cache.py index b078d8b..282cf66 100644 --- a/codeclone/cache.py +++ b/codeclone/cache.py @@ -238,6 +238,7 @@ class AnalysisProfile(TypedDict): block_min_stmt: int segment_min_loc: int segment_min_stmt: int + collect_api_surface: bool class CacheData(TypedDict): @@ -344,6 +345,7 @@ def __init__( block_min_stmt: int = 8, segment_min_loc: int = 20, segment_min_stmt: int = 10, + collect_api_surface: bool = False, ): self.path = Path(path) self.root = _resolve_root(root) @@ -355,6 +357,7 @@ def __init__( "block_min_stmt": block_min_stmt, "segment_min_loc": segment_min_loc, "segment_min_stmt": segment_min_stmt, + "collect_api_surface": collect_api_surface, } self.data: CacheData = _empty_cache_data( version=self._CACHE_VERSION, @@ -557,9 +560,13 @@ def _load_and_validate(self, raw_obj: object) -> CacheData | None: return self._reject_cache_load( "Cache analysis profile mismatch " f"(found min_loc={analysis_profile['min_loc']}, " - f"min_stmt={analysis_profile['min_stmt']}; " + f"min_stmt={analysis_profile['min_stmt']}, " + "collect_api_surface=" + f"{str(analysis_profile['collect_api_surface']).lower()}; " f"expected min_loc={self.analysis_profile['min_loc']}, " - f"min_stmt={self.analysis_profile['min_stmt']}); " + f"min_stmt={self.analysis_profile['min_stmt']}, " + "collect_api_surface=" + f"{str(self.analysis_profile['collect_api_surface']).lower()}); " "ignoring cache.", status=CacheStatus.ANALYSIS_PROFILE_MISMATCH, schema_version=version, @@ -1482,6 +1489,10 @@ def _as_analysis_profile(value: object) -> AnalysisProfile | None: block_min_stmt = _as_int(obj.get("block_min_stmt")) segment_min_loc = _as_int(obj.get("segment_min_loc")) segment_min_stmt = _as_int(obj.get("segment_min_stmt")) + collect_api_surface_raw = obj.get("collect_api_surface", False) + collect_api_surface = ( + collect_api_surface_raw if isinstance(collect_api_surface_raw, bool) else None + ) if ( min_loc is None or min_stmt is None @@ -1489,6 +1500,7 @@ def _as_analysis_profile(value: object) -> AnalysisProfile | None: or block_min_stmt is None or segment_min_loc is None or segment_min_stmt is None + or collect_api_surface is None ): return None @@ -1499,6 +1511,7 @@ def _as_analysis_profile(value: object) -> AnalysisProfile | None: block_min_stmt=block_min_stmt, segment_min_loc=segment_min_loc, segment_min_stmt=segment_min_stmt, + collect_api_surface=collect_api_surface, ) diff --git a/codeclone/cli.py b/codeclone/cli.py index d7cab46..09ac8c5 100644 --- a/codeclone/cli.py +++ b/codeclone/cli.py @@ -1372,6 +1372,7 @@ def _prepare_run_inputs() -> tuple[ block_min_stmt=args.block_min_stmt, segment_min_loc=args.segment_min_loc, segment_min_stmt=args.segment_min_stmt, + collect_api_surface=bool(args.api_surface), ) cache.load() if cache.load_warning: diff --git a/codeclone/contracts.py b/codeclone/contracts.py index 06e9517..70a76ee 100644 --- a/codeclone/contracts.py +++ b/codeclone/contracts.py @@ -12,7 +12,7 @@ BASELINE_SCHEMA_VERSION: Final = "2.1" BASELINE_FINGERPRINT_VERSION: Final = "1" -CACHE_VERSION: Final = "2.4" +CACHE_VERSION: Final = "2.5" REPORT_SCHEMA_VERSION: Final = "2.8" METRICS_BASELINE_SCHEMA_VERSION: Final = "1.2" diff --git a/codeclone/mcp_service.py b/codeclone/mcp_service.py index 6c994b0..5088694 100644 --- a/codeclone/mcp_service.py +++ b/codeclone/mcp_service.py @@ -4026,6 +4026,7 @@ def _build_cache( args.segment_min_stmt, DEFAULT_SEGMENT_MIN_STMT, ), + collect_api_surface=bool(getattr(args, "api_surface", False)), ) if policy != "off": cache.load() diff --git a/codeclone/templates.py b/codeclone/templates.py index bc3d493..a13cb31 100644 --- a/codeclone/templates.py +++ b/codeclone/templates.py @@ -16,7 +16,13 @@ FONT_CSS_URL = ( "https://fonts.googleapis.com/css2?" - "family=Inter:wght@400;500;600;700&" + # Inter Variable — single file, full weight axis (100..900), smoother + # rendering than static cuts. Used for body text AND display (KPI numbers, + # headings). Google Fonts' Inter Tight subset drops the `zero` OT feature, + # so we stick to a single Inter family and apply display weight/tracking + # via CSS instead of a sibling family. + "family=Inter:wght@100..900&" + # JetBrains Mono — code/monospace surfaces. "family=JetBrains+Mono:wght@400;500&" "display=swap" ) diff --git a/docs/README.md b/docs/README.md index 33b0f6d..2d04be9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -38,7 +38,7 @@ repository build: - [Config and defaults](book/04-config-and-defaults.md) - [Core pipeline and invariants](book/05-core-pipeline.md) - [Baseline contract (schema v2.1)](book/06-baseline.md) -- [Cache contract (schema v2.4)](book/07-cache.md) +- [Cache contract (schema v2.5)](book/07-cache.md) - [Report contract (schema v2.8)](book/08-report.md) ## Interfaces diff --git a/docs/book/07-cache.md b/docs/book/07-cache.md index f61b815..1e3e268 100644 --- a/docs/book/07-cache.md +++ b/docs/book/07-cache.md @@ -2,7 +2,7 @@ ## Purpose -Define cache schema v2.4, integrity verification, and fail-open behavior. +Define cache schema v2.5, integrity verification, and fail-open behavior. ## Public surface @@ -13,7 +13,7 @@ Define cache schema v2.4, integrity verification, and fail-open behavior. ## Data model -On-disk schema (`v == "2.4"`): +On-disk schema (`v == "2.5"`): - Top-level: `v`, `payload`, `sig` - `payload` keys: `py`, `fp`, `ap`, `files`, optional `sr` @@ -21,6 +21,7 @@ On-disk schema (`v == "2.4"`): - `min_loc`, `min_stmt` - `block_min_loc`, `block_min_stmt` - `segment_min_loc`, `segment_min_stmt` + - `collect_api_surface` - `files` map stores compact per-file entries: - `st`: `[mtime_ns, size]` - `ss`: `[lines, functions, methods, classes]` (source stats snapshot) @@ -54,9 +55,9 @@ Refs: - version `v == CACHE_VERSION` - `payload.py == current_python_tag()` - `payload.fp == BASELINE_FINGERPRINT_VERSION` - - `payload.ap` matches the current six-threshold analysis profile + - `payload.ap` matches the current analysis profile (`min_loc`, `min_stmt`, `block_min_loc`, `block_min_stmt`, - `segment_min_loc`, `segment_min_stmt`) + `segment_min_loc`, `segment_min_stmt`, `collect_api_surface`) - `sig` equals deterministic hash of canonical payload - Cache schema must also be bumped when cached analysis semantics change in a way that could leave syntactically valid but semantically stale per-file diff --git a/docs/book/13-testing-as-spec.md b/docs/book/13-testing-as-spec.md index 482c942..c2b03b2 100644 --- a/docs/book/13-testing-as-spec.md +++ b/docs/book/13-testing-as-spec.md @@ -36,7 +36,7 @@ The following matrix is treated as executable contract: | Contract | Tests | |--------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| | Baseline schema/integrity/compat gates | `tests/test_baseline.py` | -| Cache v2.4 fail-open + status mapping + API signature order preservation | `tests/test_cache.py`, `tests/test_cli_inprocess.py::test_cli_reports_cache_too_large_respects_max_size_flag`, `tests/test_cli_inprocess.py::test_cli_public_api_breaking_count_stable_across_warm_cache` | +| Cache v2.5 fail-open + status mapping + API-surface-aware reuse + API signature order preservation | `tests/test_cache.py`, `tests/test_cli_inprocess.py::test_cli_reports_cache_too_large_respects_max_size_flag`, `tests/test_cli_inprocess.py::test_cli_public_api_breaking_count_stable_across_warm_cache`, `tests/test_cli_inprocess.py::test_cli_api_surface_ignores_non_api_warm_cache` | | Exit code categories and markers | `tests/test_cli_unit.py`, `tests/test_cli_inprocess.py` | | Report schema v2.8 canonical/derived/integrity + JSON/TXT/MD/SARIF projections | `tests/test_report.py`, `tests/test_report_contract_coverage.py`, `tests/test_report_branch_invariants.py` | | HTML render-only explainability + escaping | `tests/test_html_report.py` | diff --git a/docs/book/14-compatibility-and-versioning.md b/docs/book/14-compatibility-and-versioning.md index 5e85b84..a68bdd2 100644 --- a/docs/book/14-compatibility-and-versioning.md +++ b/docs/book/14-compatibility-and-versioning.md @@ -20,7 +20,7 @@ Current contract versions: - `BASELINE_SCHEMA_VERSION = "2.1"` - `BASELINE_FINGERPRINT_VERSION = "1"` -- `CACHE_VERSION = "2.4"` +- `CACHE_VERSION = "2.5"` - `REPORT_SCHEMA_VERSION = "2.8"` - `METRICS_BASELINE_SCHEMA_VERSION = "1.2"` (used only when metrics are stored in a dedicated metrics-baseline file instead of the default unified baseline) diff --git a/docs/book/appendix/b-schema-layouts.md b/docs/book/appendix/b-schema-layouts.md index 54b0b4c..8d0a25e 100644 --- a/docs/book/appendix/b-schema-layouts.md +++ b/docs/book/appendix/b-schema-layouts.md @@ -91,11 +91,11 @@ Notes: } ``` -## Cache schema (`2.4`) +## Cache schema (`2.5`) ```json { - "v": "2.4", + "v": "2.5", "payload": { "py": "cp313", "fp": "1", @@ -105,7 +105,8 @@ Notes: "block_min_loc": 20, "block_min_stmt": 8, "segment_min_loc": 20, - "segment_min_stmt": 10 + "segment_min_stmt": 10, + "collect_api_surface": false }, "files": { "codeclone/cache.py": { diff --git a/tests/test_cache.py b/tests/test_cache.py index 82674a3..b449f48 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -1077,6 +1077,26 @@ def test_cache_load_analysis_profile_mismatch(tmp_path: Path) -> None: assert loaded.cache_schema_version == Cache._CACHE_VERSION +def test_cache_load_analysis_profile_mismatch_collect_api_surface( + tmp_path: Path, +) -> None: + cache_path = tmp_path / "cache.json" + cache = Cache(cache_path, collect_api_surface=False) + cache.put_file_entry("x.py", {"mtime_ns": 1, "size": 10}, [], [], []) + cache.save() + + loaded = Cache(cache_path, collect_api_surface=True) + loaded.load() + + assert loaded.load_warning is not None + assert "analysis profile mismatch" in loaded.load_warning + assert "collect_api_surface=false" in loaded.load_warning + assert "collect_api_surface=true" in loaded.load_warning + assert loaded.data["files"] == {} + assert loaded.load_status == CacheStatus.ANALYSIS_PROFILE_MISMATCH + assert loaded.cache_schema_version == Cache._CACHE_VERSION + + def test_cache_load_missing_analysis_profile_in_payload(tmp_path: Path) -> None: cache_path = tmp_path / "cache.json" cache = Cache(cache_path) diff --git a/tests/test_cli_inprocess.py b/tests/test_cli_inprocess.py index ab70584..c7f2f30 100644 --- a/tests/test_cli_inprocess.py +++ b/tests/test_cli_inprocess.py @@ -608,6 +608,18 @@ def _prepare_source_and_baseline(tmp_path: Path) -> tuple[Path, Path]: return src, baseline_path +def _prepare_api_surface_cache_case( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + *, + source: str, +) -> tuple[Path, Path, Path]: + src = tmp_path / "pkg.py" + src.write_text(source, "utf-8") + _patch_parallel(monkeypatch) + return src, tmp_path / "metrics-baseline.json", tmp_path / "cache.json" + + def _run_json_report( *, tmp_path: Path, @@ -3527,14 +3539,11 @@ def test_cli_public_api_breaking_count_stable_across_warm_cache( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ) -> None: - src = tmp_path / "pkg.py" - metrics_baseline_path = tmp_path / "metrics-baseline.json" - cache_path = tmp_path / "cache.json" - src.write_text( - "def run(alpha: int, beta: int) -> int:\n return alpha + beta\n", - "utf-8", + src, metrics_baseline_path, cache_path = _prepare_api_surface_cache_case( + tmp_path, + monkeypatch, + source="def run(alpha: int, beta: int) -> int:\n return alpha + beta\n", ) - _patch_parallel(monkeypatch) _run_main( monkeypatch, [ @@ -3585,6 +3594,55 @@ def test_cli_public_api_breaking_count_stable_across_warm_cache( assert "1 breaking" in warm_out +def test_cli_api_surface_ignores_non_api_warm_cache( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + _, _, cache_path = _prepare_api_surface_cache_case( + tmp_path, + monkeypatch, + source="def run(value: int) -> int:\n return value\n", + ) + report_path = tmp_path / "report.json" + _run_main( + monkeypatch, + [ + str(tmp_path), + "--no-progress", + "--cache-path", + str(cache_path), + ], + ) + _ = capsys.readouterr() + + _run_main( + monkeypatch, + [ + str(tmp_path), + "--no-progress", + "--api-surface", + "--cache-path", + str(cache_path), + "--json", + str(report_path), + ], + ) + out = capsys.readouterr().out + payload = json.loads(report_path.read_text("utf-8")) + api_surface_summary = cast( + "dict[str, object]", + cast("dict[str, object]", payload["metrics"])["summary"], + )["api_surface"] + + assert _summary_metric(out, "analyzed") == 1 + assert _summary_metric(out, "from cache") == 0 + assert "Public API" in out + assert cast("dict[str, object]", api_surface_summary)["enabled"] is True + assert cast("dict[str, object]", api_surface_summary)["public_symbols"] == 1 + assert cast("dict[str, object]", api_surface_summary)["modules"] == 1 + + def test_cli_summary_no_color_has_no_ansi( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, diff --git a/tests/test_html_report.py b/tests/test_html_report.py index 4fb9351..45dfd3b 100644 --- a/tests/test_html_report.py +++ b/tests/test_html_report.py @@ -869,6 +869,18 @@ def test_html_report_narrow_kpi_cards_keep_badges_inside_card() -> None: ) +def test_html_report_mobile_directory_hotspots_wrap_inside_summary_cards() -> None: + html = build_html_report(func_groups={}, block_groups={}, segment_groups={}) + _assert_html_contains( + html, + "@media(max-width:768px){", + ".dir-hotspot-head{flex-wrap:wrap;align-items:flex-start}", + ".dir-hotspot-detail{flex-wrap:wrap;align-items:flex-start}", + ".dir-hotspot-bar-track{width:min(148px,42%);min-width:96px}", + ".dir-hotspot-meta{width:100%}", + ) + + def test_html_report_table_css_matches_rendered_column_classes() -> None: html = build_html_report(func_groups={}, block_groups={}, segment_groups={}) _assert_html_contains( @@ -2708,6 +2720,41 @@ def test_html_report_provenance_badges_cover_mismatch_and_untrusted_metrics() -> ) +def test_html_report_provenance_table_values_use_unified_badges() -> None: + html = build_html_report( + func_groups={}, + block_groups={}, + segment_groups={}, + report_meta={ + "python_tag": "cp313", + "baseline_python_tag": "cp312", + "baseline_loaded": False, + "baseline_status": "missing", + "baseline_payload_sha256_verified": False, + "metrics_baseline_loaded": True, + "metrics_baseline_status": "missing", + "metrics_baseline_payload_sha256_verified": False, + "cache_status": "ok", + "cache_used": True, + }, + ) + _assert_html_contains( + html, + 'class="prov-badge prov-badge--amber prov-badge--inline"', + 'class="prov-badge prov-badge--red prov-badge--inline"', + 'class="prov-badge prov-badge--green prov-badge--inline"', + 'missing', + 'not loaded', + 'unverified', + 'ok', + 'hit', + 'runtime cp313', + ) + assert 'class="meta-status' not in html + assert 'class="meta-bool' not in html + assert 'class="prov-match' not in html + + def test_html_report_provenance_handles_non_boolean_baseline_loaded() -> None: html = build_html_report( func_groups={}, @@ -2727,6 +2774,160 @@ def test_html_report_provenance_handles_non_boolean_baseline_loaded() -> None: assert 'Baseline' not in html +def test_html_report_footer_uses_report_issue_link_text() -> None: + html = build_html_report( + func_groups={}, + block_groups={}, + segment_groups={}, + ) + _assert_html_contains(html, ">Docs · ", ">Report Issue") + assert ">Issues" not in html + + +def test_html_report_uses_numeric_font_for_overview_card_values() -> None: + html = build_html_report( + func_groups={}, + block_groups={}, + segment_groups={}, + ) + numeric_font_stack = ( + '--font-numeric:"JetBrains Mono",ui-monospace,' + 'SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;' + ) + _assert_html_contains( + html, + numeric_font_stack, + ".health-ring-score{font-family:var(--font-numeric);", + ".meta-item .meta-value{font-family:var(--font-numeric);", + ".overview-stat-value{font-family:var(--font-numeric);", + ) + + +def test_html_report_uses_jetbrains_mono_for_stat_card_content() -> None: + html = build_html_report( + func_groups={}, + block_groups={}, + segment_groups={}, + ) + _assert_html_contains( + html, + ".meta-item{padding:var(--sp-3) var(--sp-4);", + "font-family:var(--font-mono)}", + ".kpi-micro{display:inline-flex;align-items:center;gap:3px;", + "font-family:inherit}", + ".kpi-micro-val{font-family:inherit;font-weight:500;", + ".overview-summary-item{background:var(--bg-surface);", + "border:1px solid color-mix(in srgb,var(--border) 78%,transparent);", + "padding:var(--sp-4)}", + ".overview-summary-label{display:flex;align-items:center;gap:var(--sp-2);", + ("border-bottom:1px solid color-mix(in srgb,var(--border) 58%,transparent);"), + "font-family:var(--font-display)}", + ( + ".overview-summary-item > :not(.overview-summary-label)" + "{font-family:var(--font-mono)}" + ), + ) + + +def test_html_report_uses_jetbrains_mono_for_health_radar_labels() -> None: + html = build_html_report( + func_groups={}, + block_groups={}, + segment_groups={}, + ) + _assert_html_contains( + html, + ".health-radar text{font-size:10.5px;font-family:var(--font-mono);", + ".health-radar .radar-score{font-weight:600;font-variant-numeric:tabular-nums;", + ) + + +def test_html_report_empty_states_use_ui_font_stack() -> None: + html = build_html_report( + func_groups={}, + block_groups={}, + segment_groups={}, + ) + _assert_html_contains( + html, + ".tab-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;", + "font-family:var(--font-sans)}", + ".tab-empty-title{font-size:1rem;font-weight:600;color:var(--text-primary);margin-bottom:var(--sp-1);", + "font-family:var(--font-display)}", + ".tab-empty-desc{font-size:.85rem;color:var(--text-muted);max-width:320px;font-family:var(--font-sans)}", + ".inline-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;", + "font-family:var(--font-sans)}", + ) + + +def test_html_report_uses_shared_card_micro_interactions() -> None: + html = build_html_report( + func_groups={}, + block_groups={}, + segment_groups={}, + ) + _assert_html_contains( + html, + ".meta-item,.overview-row,.overview-summary-item,.group,.suggestion-card,.sf-card,.prov-section{", + "--card-hover-accent:var(--accent-primary);", + "@media (hover:hover) and (pointer:fine){", + "transform:translateY(-2px);", + ( + "border-color:color-mix(in oklch,var(--card-hover-accent) " + "22%,var(--border-strong));" + ), + "@media (prefers-reduced-motion:reduce){", + "transform:none}", + ) + + +def test_html_report_dead_code_cards_do_not_render_negative_active_count() -> None: + html = build_html_report( + func_groups={}, + block_groups={}, + segment_groups={}, + report_meta={"scan_root": "/outside/project"}, + metrics=_metrics_payload( + health_score=90, + health_grade="A", + complexity_max=1, + complexity_high_risk=0, + coupling_high_risk=0, + cohesion_low=0, + dep_cycles=[], + dep_max_depth=0, + dead_total=0, + dead_critical=0, + dead_suppressed=1, + ), + ) + _assert_html_contains( + html, + '0active', + ) + assert ( + '-1active' + not in html + ) + + +def test_html_report_findings_empty_state_keeps_intro_banner() -> None: + html = build_html_report( + func_groups={}, + block_groups={}, + segment_groups={}, + ) + _assert_html_contains( + html, + "What are structural findings?", + ( + "Repeated non-overlapping branch-body shapes detected inside " + "individual functions." + ), + "No structural findings detected.", + ) + + def test_html_report_dependency_hubs_deterministic_tie_order() -> None: html = _render_metrics_html( _dependency_metrics_payload( diff --git a/tests/test_html_report_helpers.py b/tests/test_html_report_helpers.py index 65c06b2..86ab5ad 100644 --- a/tests/test_html_report_helpers.py +++ b/tests/test_html_report_helpers.py @@ -570,11 +570,11 @@ def test_render_meta_panel_covers_status_tones_and_runtime_mismatch() -> None: ), ) ) - assert "meta-status--err" in meta_html - assert ">FAILED<" in meta_html - assert "meta-status--neutral" in meta_html - assert ">stale<" in meta_html - assert "prov-match--mismatch" in meta_html - assert "differs from runtime (cp313)" in meta_html + assert 'class="prov-badge prov-badge--red prov-badge--inline"' in meta_html + assert 'class="prov-badge prov-badge--neutral prov-badge--inline"' in meta_html + assert 'class="prov-badge prov-badge--amber prov-badge--inline"' in meta_html + assert 'FAILED' in meta_html + assert 'stale' in meta_html + assert 'runtime cp313' in meta_html assert 'verified' in meta_html assert 'Metrics baseline' in meta_html From 38ec861eb56890e5a42c6fa842f1947aef76c5fa Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Thu, 16 Apr 2026 16:31:33 +0500 Subject: [PATCH 14/17] chore(docs): refresh README.md --- README.md | 129 ++++++++++++++++++++++++------------------------------ 1 file changed, 58 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index bbc9edb..0284773 100644 --- a/README.md +++ b/README.md @@ -41,20 +41,17 @@ Live sample report: ## Features - **Clone detection** — function (CFG fingerprint), block (statement windows), and segment (report-only) clones -- **Structural findings** — duplicated branch families, clone guard/exit divergence and clone-cohort drift (report-only) -- **Quality metrics** — cyclomatic complexity, coupling (`CBO`), cohesion (`LCOM4`), dependency cycles, dead code, - health score, type/docstring adoption coverage, current-run Cobertura coverage join, public API surface diff, and - report-only `Overloaded Modules` profiling -- **Baseline governance** — separates accepted **legacy** debt from **new regressions** and lets CI fail **only** on - what changed -- **Reports** — interactive HTML, deterministic JSON/TXT plus Markdown and SARIF projections from one canonical report -- **MCP server** — optional read-only surface for AI agents and IDEs, designed as a budget-aware guided control - surface for agentic development -- **VS Code extension** — preview native client for CodeClone MCP with triage-first structural review, factual - `Coverage Join` overview support, and bounded in-IDE help topics -- **Native client surfaces** — preview Claude Desktop bundle and Codex plugin over the same canonical MCP contract +- **Structural findings** — duplicated branch families, clone guard/exit divergence, and clone-cohort drift +- **Quality metrics** — cyclomatic complexity, coupling (CBO), cohesion (LCOM4), dependency cycles, dead code, + health score, and overloaded-module profiling +- **Adoption & API** — type/docstring annotation coverage, public API surface inventory and baseline diff +- **Coverage Join** — fuse external Cobertura XML into the current run to surface coverage hotspots and scope gaps +- **Baseline governance** — separates accepted **legacy** debt from **new regressions**; CI fails only on what changed +- **Reports** — interactive HTML, JSON, Markdown, SARIF, and text from one canonical report +- **MCP server** — optional read-only surface for AI agents and IDEs +- **IDE & agent clients** — VS Code extension, Claude Desktop bundle, and Codex plugin over the same MCP contract - **CI-first** — deterministic output, stable ordering, exit code contract, pre-commit support -- **Fast** — incremental caching, parallel processing, warm-run optimization, and reproducible benchmark coverage +- **Fast** — incremental caching, parallel processing, warm-run optimization ## Quick Start @@ -150,16 +147,12 @@ codeclone . --fail-on-typing-regression --fail-on-docstring-regression codeclone . --api-surface --update-metrics-baseline codeclone . --fail-on-api-break -# Current-run Cobertura hotspot gate +# Coverage Join — fuse external Cobertura XML into the review codeclone . --coverage coverage.xml --fail-on-untested-hotspots --coverage-min 50 ``` -In normal full-mode CLI output, CodeClone now surfaces adoption coverage -(`params`, `returns`, `docstrings`, `Any`) in the main `Metrics` block, and it -adds a `Public API` line when `--api-surface` facts are collected. Passing -`--coverage FILE` adds a `Coverage` line from external Cobertura XML, surfaces -joined details under HTML `Quality -> Coverage Join` and MCP/report -`coverage_join`, and does not update the clone baseline. +Gate details: +[Metrics and quality gates](https://orenlab.github.io/codeclone/book/15-metrics-and-quality-gates/) ### Pre-commit @@ -179,10 +172,7 @@ repos: ## MCP Server Optional read-only MCP server for AI agents and IDE clients. -21 tools + 10 resources — never mutates source, baselines, or repo state. -Compact summary and triage payloads make scope explicit: repository-wide health, -current focus, new-finding source-kind attribution, and when comparison is -proceeding without a valid baseline. +Never mutates source, baselines, or repo state. ```bash uv tool install --pre "codeclone[mcp]" # or: uv pip install --pre "codeclone[mcp]" @@ -191,9 +181,7 @@ codeclone-mcp --transport stdio # local (Claude Code, Codex, Copilot, codeclone-mcp --transport streamable-http # remote / HTTP-only clients ``` -Docs: -[MCP usage guide](https://orenlab.github.io/codeclone/mcp/) -· +[MCP usage guide](https://orenlab.github.io/codeclone/mcp/) · [MCP interface contract](https://orenlab.github.io/codeclone/book/20-mcp-interface/) ### Native Client Surfaces @@ -206,6 +194,10 @@ Docs: All three are thin wrappers over the same `codeclone-mcp` contract — no second analysis engine. +[VS Code extension docs](https://orenlab.github.io/codeclone/book/21-vscode-extension/) · +[Claude Desktop docs](https://orenlab.github.io/codeclone/book/22-claude-desktop-bundle/) · +[Codex plugin docs](https://orenlab.github.io/codeclone/book/23-codex-plugin/) + ## Configuration CodeClone can load project-level configuration from `pyproject.toml`: @@ -231,6 +223,9 @@ segment_min_stmt = 10 Precedence: CLI flags > `pyproject.toml` > built-in defaults. +Config reference: +[Config and defaults](https://orenlab.github.io/codeclone/book/04-config-and-defaults/) + ## Baseline Workflow Baselines capture the current duplication state. Once committed, they become the CI reference point. @@ -253,6 +248,8 @@ Full contract: [Baseline contract](https://orenlab.github.io/codeclone/book/06-b Contract errors (`2`) take precedence over gating failures (`3`). +Full policy: [Exit codes and failure policy](https://orenlab.github.io/codeclone/book/03-contracts-exit-codes/) + ## Reports | Format | Flag | Default path | @@ -263,37 +260,12 @@ Contract errors (`2`) take precedence over gating failures (`3`). | SARIF | `--sarif` | `.cache/codeclone/report.sarif` | | Text | `--text` | `.cache/codeclone/report.txt` | -All report formats are rendered from one canonical JSON report document. - -- `--open-html-report` opens the generated HTML report in the default browser and requires `--html`. -- `--timestamped-report-paths` appends a UTC timestamp to default report filenames for bare report flags such as - `--html` or `--json`. Explicit report paths are not rewritten. - -The docs site also includes live example HTML/JSON/SARIF reports generated from the current `codeclone` repository. - -Structural findings include: - -- `duplicated_branches` -- `clone_guard_exit_divergence` -- `clone_cohort_drift` - -### Inline Suppressions - -CodeClone keeps dead-code detection deterministic and static by default. When a symbol is intentionally -invoked through runtime dynamics (for example framework callbacks, plugin loading, or reflection), suppress -the known false positive explicitly at the declaration site: +All formats are rendered from one canonical JSON report. +`--open-html-report` opens the HTML in the default browser. +`--timestamped-report-paths` appends a UTC timestamp to default filenames. -```python -# codeclone: ignore[dead-code] -def handle_exception(exc: Exception) -> None: - ... - - -class Middleware: # codeclone: ignore[dead-code] - ... -``` - -Dynamic/runtime false positives are resolved via explicit inline suppressions, not via broad heuristics. +Report contract: [Report contract](https://orenlab.github.io/codeclone/book/08-report/) · +[HTML render](https://orenlab.github.io/codeclone/book/10-html-render/)
Canonical JSON report shape (v2.8) @@ -410,11 +382,29 @@ Dynamic/runtime false positives are resolved via explicit inline suppressions, n } ``` -Canonical contract: [Report contract](https://orenlab.github.io/codeclone/book/08-report/) and -[Dead-code contract](https://orenlab.github.io/codeclone/book/16-dead-code-contract/) +Full contract: [Report contract](https://orenlab.github.io/codeclone/book/08-report/)
+## Inline Suppressions + +When a symbol is invoked through runtime dynamics (framework callbacks, plugin loading, reflection), +suppress the known false positive at the declaration site: + +```python +# codeclone: ignore[dead-code] +def handle_exception(exc: Exception) -> None: + ... + + +class Middleware: # codeclone: ignore[dead-code] + ... +``` + +Suppression contract: +[Inline suppressions](https://orenlab.github.io/codeclone/book/19-inline-suppressions/) · +[Dead-code contract](https://orenlab.github.io/codeclone/book/16-dead-code-contract/) + ## How It Works 1. **Parse** — Python source to AST @@ -430,18 +420,14 @@ CFG semantics: [CFG semantics](https://orenlab.github.io/codeclone/cfg/) ## Documentation -| Topic | Link | -|----------------------------|-----------------------------------------------------------------------------------------------------| -| Contract book (start here) | [Contracts and guarantees](https://orenlab.github.io/codeclone/book/00-intro/) | -| Exit codes | [Exit codes and failure policy](https://orenlab.github.io/codeclone/book/03-contracts-exit-codes/) | -| Configuration | [Config and defaults](https://orenlab.github.io/codeclone/book/04-config-and-defaults/) | -| Baseline contract | [Baseline contract](https://orenlab.github.io/codeclone/book/06-baseline/) | -| Cache contract | [Cache contract](https://orenlab.github.io/codeclone/book/07-cache/) | -| Report contract | [Report contract](https://orenlab.github.io/codeclone/book/08-report/) | -| Metrics & quality gates | [Metrics and quality gates](https://orenlab.github.io/codeclone/book/15-metrics-and-quality-gates/) | -| Dead code | [Dead-code contract](https://orenlab.github.io/codeclone/book/16-dead-code-contract/) | -| Docker benchmark contract | [Benchmarking contract](https://orenlab.github.io/codeclone/book/18-benchmarking/) | -| Determinism | [Determinism policy](https://orenlab.github.io/codeclone/book/12-determinism/) | +Full docs and contract book: [orenlab.github.io/codeclone](https://orenlab.github.io/codeclone/) + +Quick links: +[Baseline](https://orenlab.github.io/codeclone/book/06-baseline/) · +[Report](https://orenlab.github.io/codeclone/book/08-report/) · +[Metrics & gates](https://orenlab.github.io/codeclone/book/15-metrics-and-quality-gates/) · +[MCP](https://orenlab.github.io/codeclone/book/20-mcp-interface/) · +[CLI](https://orenlab.github.io/codeclone/book/09-cli/) ## Benchmarking Notes @@ -476,6 +462,7 @@ Versions released before this change remain under their original license terms. ## Links +- **Docs:** - **Issues:** - **PyPI:** - **Licenses:** [MPL-2.0](LICENSE) · [MIT docs](LICENSE-docs) From 68403260705ac4efa7570cea42155b304f7952ab Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Thu, 16 Apr 2026 17:57:53 +0500 Subject: [PATCH 15/17] feat(mcp): add compact threshold context for empty design checks - include threshold_context in empty complexity, coupling, and cohesion check payloads - report measured_units and highest_below_threshold from canonical metric families - distinguish run finding thresholds from explicit requested_min filters - document the compact MCP response hint for agent-oriented triage - cover empty-check and requested-min cases with focused MCP tests --- codeclone/mcp_service.py | 138 +++++++++++++++++++++++++++++++- docs/book/20-mcp-interface.md | 4 + docs/mcp.md | 4 + tests/test_mcp_service.py | 143 +++++++++++++++++++++++++++++++++- 4 files changed, 285 insertions(+), 4 deletions(-) diff --git a/codeclone/mcp_service.py b/codeclone/mcp_service.py index 5088694..cbed02b 100644 --- a/codeclone/mcp_service.py +++ b/codeclone/mcp_service.py @@ -295,6 +295,26 @@ "complexity": "complexity", "clones": "clones", } +_DESIGN_CHECK_CONTEXT: Final[dict[str, dict[str, object]]] = { + "complexity": { + "category": CATEGORY_COMPLEXITY, + "metric": "cyclomatic_complexity", + "operator": ">", + "default_threshold": DEFAULT_REPORT_DESIGN_COMPLEXITY_THRESHOLD, + }, + "coupling": { + "category": CATEGORY_COUPLING, + "metric": "cbo", + "operator": ">", + "default_threshold": DEFAULT_REPORT_DESIGN_COUPLING_THRESHOLD, + }, + "cohesion": { + "category": CATEGORY_COHESION, + "metric": "lcom4", + "operator": ">=", + "default_threshold": DEFAULT_REPORT_DESIGN_COHESION_THRESHOLD, + }, +} _VALID_METRICS_DETAIL_FAMILIES = frozenset( { "complexity", @@ -2045,6 +2065,13 @@ def check_complexity( detail_level=validated_detail, max_results=max_results, path=path, + threshold_context=self._design_threshold_context( + record=record, + check="complexity", + path=path, + items=findings, + requested_min=min_complexity, + ), ) def check_clones( @@ -2163,6 +2190,12 @@ def _check_design_metric( detail_level=validated_detail, max_results=max_results, path=path, + threshold_context=self._design_threshold_context( + record=record, + check=check, + path=path, + items=findings, + ), ) def check_dead_code( @@ -3615,6 +3648,7 @@ def _granular_payload( detail_level: DetailLevel, max_results: int, path: str | None, + threshold_context: Mapping[str, object] | None = None, ) -> dict[str, object]: bounded_items = [dict(item) for item in items[: max(1, max_results)]] full_health = dict(self._as_mapping(record.summary.get("health"))) @@ -3625,7 +3659,7 @@ def _granular_payload( if relevant_dimension and relevant_dimension in dimensions else dict(dimensions) ) - return { + payload: dict[str, object] = { "run_id": self._short_run_id(record.run_id), "check": check, "detail_level": detail_level, @@ -3639,6 +3673,108 @@ def _granular_payload( }, "items": bounded_items, } + if threshold_context: + payload["threshold_context"] = dict(threshold_context) + return payload + + def _design_threshold_context( + self, + *, + record: MCPRunRecord, + check: str, + path: str | None, + items: Sequence[Mapping[str, object]], + requested_min: int | None = None, + ) -> dict[str, object] | None: + if items: + return None + spec = _DESIGN_CHECK_CONTEXT.get(check) + if spec is None: + return None + category = str(spec["category"]) + metric = str(spec["metric"]) + operator = str(spec["operator"]) + normalized_path = self._normalize_relative_path(path or "") + metrics = self._as_mapping(record.report_document.get("metrics")) + families = self._as_mapping(metrics.get("families")) + family = self._as_mapping(families.get(category)) + metric_items = [ + self._as_mapping(item) + for item in self._as_sequence(family.get("items")) + if not normalized_path + or self._metric_item_matches_path( + self._as_mapping(item), + normalized_path, + ) + ] + if not metric_items: + return None + values = [_as_int(item.get(metric), 0) for item in metric_items] + finding_threshold = self._design_finding_threshold( + record=record, + check=check, + ) + threshold = finding_threshold + threshold_kind = "finding_threshold" + if requested_min is not None and requested_min > finding_threshold: + threshold = requested_min + threshold_kind = "requested_min" + highest_below = self._highest_below_threshold( + values=values, + operator=operator, + threshold=threshold, + ) + payload: dict[str, object] = { + "metric": metric, + "threshold": threshold, + "threshold_kind": threshold_kind, + "measured_units": len(metric_items), + } + if threshold_kind != "finding_threshold": + payload["finding_threshold"] = finding_threshold + if highest_below is not None: + payload["highest_below_threshold"] = highest_below + return payload + + def _design_finding_threshold( + self, + *, + record: MCPRunRecord, + check: str, + ) -> int: + spec = _DESIGN_CHECK_CONTEXT[check] + category = str(spec["category"]) + default_threshold = _as_int(spec["default_threshold"]) + findings = self._as_mapping(record.report_document.get("findings")) + thresholds = self._as_mapping( + self._as_mapping(findings.get("thresholds")).get("design_findings") + ) + threshold_payload = self._as_mapping(thresholds.get(category)) + if threshold_payload: + return _as_int(threshold_payload.get("value"), default_threshold) + request_value = { + "complexity": record.request.complexity_threshold, + "coupling": record.request.coupling_threshold, + "cohesion": record.request.cohesion_threshold, + }.get(check) + return _as_int(request_value, default_threshold) + + @staticmethod + def _highest_below_threshold( + *, + values: Sequence[int], + operator: str, + threshold: int, + ) -> int | None: + if operator == ">": + below = [value for value in values if value <= threshold] + elif operator == ">=": + below = [value for value in values if value < threshold] + else: + return None + if not below: + return None + return max(below) @staticmethod def _normalized_source_kind(value: object) -> str: diff --git a/docs/book/20-mcp-interface.md b/docs/book/20-mcp-interface.md index 2c750df..2401070 100644 --- a/docs/book/20-mcp-interface.md +++ b/docs/book/20-mcp-interface.md @@ -96,6 +96,10 @@ Current server characteristics: locations plus remediation - `detail_level="full"` keeps the compatibility-oriented payload, including `priority_factors`, `items`, and per-location `uri` + - empty design `check_*` responses may include a compact + `threshold_context` (`metric`, `threshold`, `measured_units`, + `highest_below_threshold`) so agents can tell whether the run is truly + quiet or just below the active threshold The MCP layer does not introduce a separate analysis engine. It calls the current CodeClone pipeline and reuses the canonical report document already diff --git a/docs/mcp.md b/docs/mcp.md index ac7d5b4..2031949 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -116,6 +116,10 @@ run-scoped URI templates. **Payload conventions:** - `check_*` responses include only the relevant health dimension. +- Empty design `check_*` responses may also include a compact + `threshold_context` (`metric`, `threshold`, `measured_units`, + `highest_below_threshold`) to show whether the run is genuinely quiet or + simply below the active threshold. - Finding responses use short MCP IDs and relative paths by default; `detail_level=full` restores the compatibility payload with URIs. - Summary and triage projections keep interpretation compact: `health_scope` diff --git a/tests/test_mcp_service.py b/tests/test_mcp_service.py index b9aa294..d497c94 100644 --- a/tests/test_mcp_service.py +++ b/tests/test_mcp_service.py @@ -2435,9 +2435,9 @@ def _patched_get_finding( request=MCPAnalysisRequest( root=str(tmp_path), respect_pyproject=False, - complexity_threshold=1, - coupling_threshold=1, - cohesion_threshold=1, + complexity_threshold=5, + coupling_threshold=5, + cohesion_threshold=4, ), comparison_settings=(), report_document={ @@ -2514,6 +2514,143 @@ def _patched_get_finding( if str(finding.get("family", "")) == "design" ] assert design_findings == [] + service._runs.register(fake_design_record) + empty_complexity = service.check_complexity( + run_id="design", + path="pkg/quality.py", + detail_level="summary", + ) + requested_complexity = service.check_complexity( + run_id="design", + path="pkg/quality.py", + min_complexity=8, + detail_level="summary", + ) + empty_coupling = service.check_coupling( + run_id="design", + path="pkg/quality.py", + detail_level="summary", + ) + empty_cohesion = service.check_cohesion( + run_id="design", + path="pkg/quality.py", + detail_level="summary", + ) + assert empty_complexity["total"] == 0 + assert empty_complexity["threshold_context"] == { + "metric": "cyclomatic_complexity", + "threshold": 5, + "threshold_kind": "finding_threshold", + "measured_units": 1, + "highest_below_threshold": 3, + } + assert requested_complexity["threshold_context"] == { + "metric": "cyclomatic_complexity", + "threshold": 8, + "threshold_kind": "requested_min", + "finding_threshold": 5, + "measured_units": 1, + "highest_below_threshold": 3, + } + assert empty_coupling["threshold_context"] == { + "metric": "cbo", + "threshold": 5, + "threshold_kind": "finding_threshold", + "measured_units": 1, + "highest_below_threshold": 2, + } + assert empty_cohesion["threshold_context"] == { + "metric": "lcom4", + "threshold": 4, + "threshold_kind": "finding_threshold", + "measured_units": 1, + "highest_below_threshold": 2, + } + assert ( + service._design_threshold_context( + record=fake_design_record, + check="complexity", + path="pkg/quality.py", + items=({"id": "existing"},), + ) + is None + ) + assert ( + service._design_threshold_context( + record=fake_design_record, + check="unknown", + path="pkg/quality.py", + items=(), + ) + is None + ) + thresholded_report_document = dict(fake_design_record.report_document) + thresholded_findings = dict( + cast("dict[str, object]", thresholded_report_document["findings"]) + ) + thresholded_findings["thresholds"] = { + "design_findings": { + "complexity": { + "metric": "cyclomatic_complexity", + "operator": ">", + "value": 6, + } + } + } + thresholded_report_document["findings"] = thresholded_findings + thresholded_record = replace( + fake_design_record, + report_document=thresholded_report_document, + ) + assert ( + service._design_finding_threshold( + record=thresholded_record, + check="complexity", + ) + == 6 + ) + no_below_report_document = dict(fake_design_record.report_document) + no_below_metrics = dict( + cast("dict[str, object]", no_below_report_document["metrics"]) + ) + no_below_families = dict(cast("dict[str, object]", no_below_metrics["families"])) + no_below_families["complexity"] = { + "items": [ + { + "qualname": "pkg.quality:very_hot", + "relative_path": "pkg/quality.py", + "start_line": 10, + "end_line": 20, + "cyclomatic_complexity": 9, + "nesting_depth": 2, + "risk": "high", + } + ] + } + no_below_metrics["families"] = no_below_families + no_below_report_document["metrics"] = no_below_metrics + no_below_record = replace( + fake_design_record, + report_document=no_below_report_document, + ) + assert service._design_threshold_context( + record=no_below_record, + check="complexity", + path="pkg/quality.py", + items=(), + ) == { + "metric": "cyclomatic_complexity", + "threshold": 5, + "threshold_kind": "finding_threshold", + "measured_units": 1, + } + assert ( + service._highest_below_threshold(values=(9,), operator=">", threshold=5) is None + ) + assert ( + service._highest_below_threshold(values=(1, 2), operator="!=", threshold=5) + is None + ) detail_payload = service._project_finding_detail( fake_design_record, { From 5f39b3f3b7dd99f723dd4bd0258b3be2652fb367 Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Thu, 16 Apr 2026 18:24:05 +0500 Subject: [PATCH 16/17] chore(deps): update project deps and pin actual version --- pyproject.toml | 14 +-- uv.lock | 262 ++++++++++++++++++++++++------------------------- 2 files changed, 137 insertions(+), 139 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b197f45..fa7abdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ requires-python = ">=3.10" dependencies = [ "orjson>=3.11.8", "pygments>=2.20.0", - "rich>=14.3.2", + "rich>=15.0.0", "tomli>=2.0.1; python_version < '3.11'", ] @@ -63,15 +63,15 @@ Documentation = "https://orenlab.github.io/codeclone/" [project.optional-dependencies] mcp = [ - "mcp>=1.26.0,<2", + "mcp>=1.27.0,<2", ] dev = [ - "pytest>=9.0.0", + "pytest>=9.0.3", "pytest-cov>=7.1.0", - "build>=1.4.1", - "twine>=5.0.0", - "mypy>=1.19.1", - "ruff>=0.15.8", + "build>=1.4.3", + "twine>=6.2.0", + "mypy>=1.20.1", + "ruff>=0.15.10", "pre-commit>=4.5.1", ] diff --git a/uv.lock b/uv.lock index 989eaa5..1e7ac3a 100644 --- a/uv.lock +++ b/uv.lock @@ -303,18 +303,18 @@ mcp = [ [package.metadata] requires-dist = [ - { name = "build", marker = "extra == 'dev'", specifier = ">=1.4.1" }, - { name = "mcp", marker = "extra == 'mcp'", specifier = ">=1.26.0,<2" }, - { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.19.1" }, + { name = "build", marker = "extra == 'dev'", specifier = ">=1.4.3" }, + { name = "mcp", marker = "extra == 'mcp'", specifier = ">=1.27.0,<2" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.20.1" }, { name = "orjson", specifier = ">=3.11.8" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.5.1" }, { name = "pygments", specifier = ">=2.20.0" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.3" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.1.0" }, - { name = "rich", specifier = ">=14.3.2" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.8" }, + { name = "rich", specifier = ">=15.0.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.10" }, { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0.1" }, - { name = "twine", marker = "extra == 'dev'", specifier = ">=5.0.0" }, + { name = "twine", marker = "extra == 'dev'", specifier = ">=6.2.0" }, ] provides-extras = ["mcp", "dev"] @@ -537,11 +537,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.25.2" +version = "3.28.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/17/6e8890271880903e3538660a21d63a6c1fea969ac71d0d6b608b78727fa9/filelock-3.28.0.tar.gz", hash = "sha256:4ed1010aae813c4ee8d9c660e4792475ee60c4a0ba76073ceaf862bd317e3ca6", size = 56474, upload-time = "2026-04-14T22:54:33.625Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, + { url = "https://files.pythonhosted.org/packages/3b/21/2f728888c45033d34a417bfcd248ea2564c9e08ab1bfd301377cf05d5586/filelock-3.28.0-py3-none-any.whl", hash = "sha256:de9af6712788e7171df1b28b15eba2446c69721433fa427a9bee07b17820a9db", size = 39189, upload-time = "2026-04-14T22:54:32.037Z" }, ] [[package]] @@ -1064,11 +1064,11 @@ wheels = [ [[package]] name = "packaging" -version = "26.0" +version = "26.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, ] [[package]] @@ -1125,7 +1125,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.5" +version = "2.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -1133,127 +1133,125 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/6b/1353beb3d1cd5cf61cdec5b6f87a9872399de3bc5cae0b7ce07ff4de2ab0/pydantic-2.13.1.tar.gz", hash = "sha256:a0f829b279ddd1e39291133fe2539d2aa46cc6b150c1706a270ff0879e3774d2", size = 843746, upload-time = "2026-04-15T14:57:19.398Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/81/5a/2225f4c176dbfed0d809e848b50ef08f70e61daa667b7fa14b0d311ae44d/pydantic-2.13.1-py3-none-any.whl", hash = "sha256:9557ecc2806faaf6037f85b1fbd963d01e30511c48085f0d573650fdeaad378a", size = 471917, upload-time = "2026-04-15T14:57:17.277Z" }, ] [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, - { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, - { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, - { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, - { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, - { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, - { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, - { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, - { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, - { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, - { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, - { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, - { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, - { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, - { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, - { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, - { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, - { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, - { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/a1/93/f97a86a7eb28faa1d038af2fd5d6166418b4433659108a4c311b57128b2d/pydantic_core-2.46.1.tar.gz", hash = "sha256:d408153772d9f298098fb5d620f045bdf0f017af0d5cb6e309ef8c205540caa4", size = 471230, upload-time = "2026-04-15T14:49:34.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/a0/07f275411355b567b994e565bc5ea9dbf522978060c18e3b7edf646c0fc2/pydantic_core-2.46.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:84eb5414871fd0293c38d2075802f95030ff11a92cf2189942bf76fd181af77b", size = 2123782, upload-time = "2026-04-15T14:52:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/ab/71/d027c7de46df5b9287ed6f0ef02346c84d61348326253a4f13695d54d66f/pydantic_core-2.46.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5c75fb25db086bf504c55730442e471c12bc9bfae817dd359b1a36bc93049d34", size = 1948561, upload-time = "2026-04-15T14:53:12.07Z" }, + { url = "https://files.pythonhosted.org/packages/77/74/cba894bea0d51a3b2dcada9eb3af9c4cfaa271bf21123372dc82ccef029f/pydantic_core-2.46.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dc09f0221425453fd9f73fd70bba15817d25b95858282702d7305a08d37306", size = 1974387, upload-time = "2026-04-15T14:50:14.048Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ad/cc122887d6f20ac5d997928b0bf3016ac9c7bae07dce089333aa0c2e868b/pydantic_core-2.46.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:139fd6722abc5e6513aa0a27b06ebeb997838c5b179cf5e83862ace45f281c56", size = 2054868, upload-time = "2026-04-15T14:49:51.912Z" }, + { url = "https://files.pythonhosted.org/packages/9f/09/22049b22d65a67253cbdced88dbce0e97162f35cc433917df37df794ede8/pydantic_core-2.46.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba723fd8ef6011af71f92ed54adb604e7699d172f4273e4b46f1cfb8ee8d72fd", size = 2228717, upload-time = "2026-04-15T14:49:27.384Z" }, + { url = "https://files.pythonhosted.org/packages/e6/98/b35a8a187cf977462668b5064c606e290c88c2561e053883d86193ab9c51/pydantic_core-2.46.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:828410e082555e55da9bbb5e6c17617386fe1415c4d42765a90d372ed9cce813", size = 2298261, upload-time = "2026-04-15T14:52:20.463Z" }, + { url = "https://files.pythonhosted.org/packages/98/ae/46f8d693caefc09d8e2d3f19a6b4f2252cf6542f0b555759f2b5ec2b4ca5/pydantic_core-2.46.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb5cd53264c9906c163a71b489e9ac71b0ae13a2dd0241e6129f4df38ba1c814", size = 2094496, upload-time = "2026-04-15T14:49:59.711Z" }, + { url = "https://files.pythonhosted.org/packages/ee/40/7e4013639d316d2cb67dae288c768d49cc4a7a4b16ef869e486880db1a1f/pydantic_core-2.46.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:4530a6594883d9d4a9c7ef68464ef6b4a88d839e3531c089a3942c78bffe0a66", size = 2144795, upload-time = "2026-04-15T14:52:44.731Z" }, + { url = "https://files.pythonhosted.org/packages/0d/87/c00f6450059804faf30f568009c8c98e72e6802c1ccd8b562da57953ad81/pydantic_core-2.46.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ed1c71f60abbf9c9a440dc8fc6b1180c45dcab3a5e311250de99744a0166bc95", size = 2173108, upload-time = "2026-04-15T14:51:37.806Z" }, + { url = "https://files.pythonhosted.org/packages/46/15/7a8fb06c109a07dbc1f5f272b2da1290c8a25f5900a579086e433049fc1a/pydantic_core-2.46.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:254253491f1b8e3ba18c15fe924bb9b175f1a48413b74e8f0c67b8f51b6f726b", size = 2185687, upload-time = "2026-04-15T14:51:33.125Z" }, + { url = "https://files.pythonhosted.org/packages/d9/38/c52ead78febf23d32db898c7022173c674226cf3c8ee1645220ab9516931/pydantic_core-2.46.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:dfcf6485ac38698a5b45f37467b8eb2f4f8e3edd5790e2579c5d52fdfffb2e3d", size = 2326273, upload-time = "2026-04-15T14:51:10.614Z" }, + { url = "https://files.pythonhosted.org/packages/1e/af/cb5ea2336e9938b3a0536ce4bfed4a342285caa8a6b8ff449a7bc2f179ec/pydantic_core-2.46.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:592b39150ab5b5a2cb2eb885097ee4c2e4d54e3b902f6ae32528f7e6e42c00fc", size = 2368428, upload-time = "2026-04-15T14:49:25.804Z" }, + { url = "https://files.pythonhosted.org/packages/a2/99/adcfbcbd96556120e7d795aab4fd77f5104a49051929c3805a9d736ec48f/pydantic_core-2.46.1-cp310-cp310-win32.whl", hash = "sha256:eb37b1369ad39ec046a36dc81ffd76870766bda2073f57448bbcb1fd3e4c5ad0", size = 1993405, upload-time = "2026-04-15T14:50:51.082Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ff/2767be513a250293f80748740ce73b0f0677711fc791b1afab3499734dd2/pydantic_core-2.46.1-cp310-cp310-win_amd64.whl", hash = "sha256:c330dab8254d422880177436a5892ac6d9337afff9fe383fb1f8c6caedb685e1", size = 2068177, upload-time = "2026-04-15T14:52:29.899Z" }, + { url = "https://files.pythonhosted.org/packages/37/96/d83d23fc3c822326d808b8c0457d4f7afb1552e741a7c2378a974c522c63/pydantic_core-2.46.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f0f84431981c6ae217ebb96c3eca8212f6f5edf116f62f62cc6c7d72971f826c", size = 2121938, upload-time = "2026-04-15T14:49:21.568Z" }, + { url = "https://files.pythonhosted.org/packages/11/44/94b1251825560f5d90e25ebcd457c4772e1f3e1a378f438c040fe2148f3e/pydantic_core-2.46.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a05f60b36549f59ab585924410187276ec17a94bae939273a213cea252c8471e", size = 1946541, upload-time = "2026-04-15T14:49:57.925Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8f/79aff4c8bd6fb49001ffe4747c775c0f066add9da13dec180eb0023ada34/pydantic_core-2.46.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2c93fd1693afdfae7b2897f7530ed3f180d9fc92ee105df3ebdff24d5061cc8", size = 1973067, upload-time = "2026-04-15T14:51:14.765Z" }, + { url = "https://files.pythonhosted.org/packages/56/01/826ab3afb1d43cbfdc2aa592bff0f1f6f4b90f5a801478ba07bde74e706f/pydantic_core-2.46.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c19983759394c702a776f42f33df8d7bb7883aefaa44a69ba86356a9fd67367", size = 2053146, upload-time = "2026-04-15T14:51:48.847Z" }, + { url = "https://files.pythonhosted.org/packages/6c/32/be20ec48ccbd85cac3f8d96ca0a0f87d5c14fbf1eb438da0ac733f2546f2/pydantic_core-2.46.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e8debf586d7d800a718194417497db5126d4f4302885a2dff721e9df3f4851c", size = 2227393, upload-time = "2026-04-15T14:51:53.218Z" }, + { url = "https://files.pythonhosted.org/packages/b5/8e/1fae21c887f363ed1a5cf9f267027700c796b7435313c21723cd3e8aeeb3/pydantic_core-2.46.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54160da754d63da7780b76e5743d44f026b9daffc6b8c9696a756368c0a298c9", size = 2296193, upload-time = "2026-04-15T14:50:31.065Z" }, + { url = "https://files.pythonhosted.org/packages/0a/29/e5637b539458ffb60ba9c204fc16c52ea36828427fa667e4f9c7d83cfea9/pydantic_core-2.46.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74cee962c8b4df9a9b0bb63582e51986127ee2316f0c49143b2996f4b201bd9c", size = 2092156, upload-time = "2026-04-15T14:52:37.227Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fa/3a453934af019c72652fb75489c504ae689de632fa2e037fec3195cd6948/pydantic_core-2.46.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:0ba3462872a678ebe21b15bd78eff40298b43ea50c26f230ec535c00cf93ec7e", size = 2142845, upload-time = "2026-04-15T14:51:04.847Z" }, + { url = "https://files.pythonhosted.org/packages/36/c2/71b56fa10a80b98036f4bf0fbb912833f8e9c61b15e66c236fadaf54c27c/pydantic_core-2.46.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b718873a966d91514c5252775f568985401b54a220919ab22b19a6c4edd8c053", size = 2170756, upload-time = "2026-04-15T14:50:17.16Z" }, + { url = "https://files.pythonhosted.org/packages/e1/da/a4c761dc8d982e2c53f991c0c36d37f6fe308e149bf0a101c25b0750a893/pydantic_core-2.46.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cb1310a9fd722da8cceec1fb59875e1c86bee37f0d8a9c667220f00ee722cc8f", size = 2183579, upload-time = "2026-04-15T14:51:20.888Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d4/b0a6c00622e4afd9a807b8bb05ba8f1a0b69ca068ac138d9d36700fe767b/pydantic_core-2.46.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:98e3ede76eb4b9db8e7b5efea07a3f3315135485794a5df91e3adf56c4d573b6", size = 2324516, upload-time = "2026-04-15T14:52:32.521Z" }, + { url = "https://files.pythonhosted.org/packages/45/f1/a4bace0c98b0774b02de99233882c48d94b399ba4394dd5e209665d05062/pydantic_core-2.46.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:780b8f24ff286e21fd010247011a68ea902c34b1eee7d775b598bc28f5f28ab6", size = 2367084, upload-time = "2026-04-15T14:50:37.832Z" }, + { url = "https://files.pythonhosted.org/packages/3a/54/ae827a3976b136d1c9a9a56c2299a8053605a69facaa0c7354ba167305eb/pydantic_core-2.46.1-cp311-cp311-win32.whl", hash = "sha256:1d452f4cad0f39a94414ca68cda7cc55ff4c3801b5ab0bc99818284a3d39f889", size = 1992061, upload-time = "2026-04-15T14:51:44.704Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/d85de69e0fdfafc0e87d88bd5d0c157a5443efaaef24eed152a8a8f8dfb6/pydantic_core-2.46.1-cp311-cp311-win_amd64.whl", hash = "sha256:f463fd6a67138d70200d2627676e9efbb0cee26d98a5d3042a35aa20f95ec129", size = 2065497, upload-time = "2026-04-15T14:51:17.077Z" }, + { url = "https://files.pythonhosted.org/packages/46/a7/9eb3b1038db630e1550924e81d1211b0dd70ac3740901fd95f30f5497990/pydantic_core-2.46.1-cp311-cp311-win_arm64.whl", hash = "sha256:155aec0a117140e86775eec113b574c1c299358bfd99467b2ea7b2ea26db2614", size = 2045914, upload-time = "2026-04-15T14:51:24.782Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fb/caaa8ee23861c170f07dbd58fc2be3a2c02a32637693cbb23eef02e84808/pydantic_core-2.46.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae8c8c5eb4c796944f3166f2f0dab6c761c2c2cc5bd20e5f692128be8600b9a4", size = 2119472, upload-time = "2026-04-15T14:49:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/fa/61/bcffaa52894489ff89e5e1cdde67429914bf083c0db7296bef153020f786/pydantic_core-2.46.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:daba6f5f5b986aa0682623a1a4f8d1ecb0ec00ce09cfa9ca71a3b742bc383e3a", size = 1951230, upload-time = "2026-04-15T14:52:27.646Z" }, + { url = "https://files.pythonhosted.org/packages/f8/95/80d2f43a2a1a1e3220fd329d614aa5a39e0a75d24353a3aaf226e605f1c2/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0265f3a2460539ecc97817a80c7a23c458dd84191229b655522a2674f701f14e", size = 1976394, upload-time = "2026-04-15T14:50:32.742Z" }, + { url = "https://files.pythonhosted.org/packages/8d/31/2c5b1a207926b5fc1961a2d11da940129bc3841c36cc4df03014195b2966/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb16c0156c4b4e94aa3719138cc43c53d30ff21126b6a3af63786dcc0757b56e", size = 2068455, upload-time = "2026-04-15T14:50:01.286Z" }, + { url = "https://files.pythonhosted.org/packages/7d/36/c6aa07274359a51ac62895895325ce90107e811c6cea39d2617a99ef10d7/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b42d80fad8e4b283e1e4138f1142f0d038c46d137aad2f9824ad9086080dd41", size = 2239049, upload-time = "2026-04-15T14:53:02.216Z" }, + { url = "https://files.pythonhosted.org/packages/0a/3f/77cdd0db8bddc714842dfd93f737c863751cf02001c993341504f6b0cd53/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cced85896d5b795293bc36b7e2fb0347a36c828551b50cbba510510d928548c", size = 2318681, upload-time = "2026-04-15T14:50:04.539Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a3/09d929a40e6727274b0b500ad06e1b3f35d4f4665ae1c8ba65acbb17e9b5/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a641cb1e74b44c418adaf9f5f450670dbec53511f030d8cde8d8accb66edc363", size = 2096527, upload-time = "2026-04-15T14:53:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/89/ae/544c3a82456ebc254a9fcbe2715bab76c70acf9d291aaea24391147943e4/pydantic_core-2.46.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:191e7a122ab14eb12415fe3f92610fc06c7f1d2b4b9101d24d490d447ac92506", size = 2170407, upload-time = "2026-04-15T14:51:27.138Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ce/0dfd881c7af4c522f47b325707bd9a2cdcf4f40e4f2fd30df0e9a3e8d393/pydantic_core-2.46.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fe4ff660f7938b5d92f21529ce331b011aa35e481ab64b7cd03f52384e544bb", size = 2188578, upload-time = "2026-04-15T14:50:39.655Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e9/980ea2a6d5114dd1a62ecc5f56feb3d34555f33bd11043f042e5f7f0724a/pydantic_core-2.46.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:18fcea085b3adc3868d8d19606da52d7a52d8bccd8e28652b0778dbe5e6a6660", size = 2188959, upload-time = "2026-04-15T14:52:42.243Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f1/595e0f50f4bfc56cde2fe558f2b0978f29f2865da894c6226231e17464a5/pydantic_core-2.46.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e8e589e7c9466e022d79e13c5764c2239b2e5a7993ba727822b021234f89b56b", size = 2339973, upload-time = "2026-04-15T14:52:10.642Z" }, + { url = "https://files.pythonhosted.org/packages/49/44/be9f979a6ab6b8c36865ccd92c3a38a760c66055e1f384665f35525134c4/pydantic_core-2.46.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f78eb3d4027963bdc9baccd177f02a98bf8714bc51fe17153d8b51218918b5bc", size = 2385228, upload-time = "2026-04-15T14:51:00.77Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d4/c826cd711787d240219f01d0d3ca116cb55516b8b95277820aa9c85e1882/pydantic_core-2.46.1-cp312-cp312-win32.whl", hash = "sha256:54fe30c20cab03844dc63bdc6ddca67f74a2eb8482df69c1e5f68396856241be", size = 1978828, upload-time = "2026-04-15T14:50:29.362Z" }, + { url = "https://files.pythonhosted.org/packages/22/05/8a1fcf8181be4c7a9cfc34e5fbf2d9c3866edc9dfd3c48d5401806e0a523/pydantic_core-2.46.1-cp312-cp312-win_amd64.whl", hash = "sha256:aea4e22ed4c53f2774221435e39969a54d2e783f4aee902cdd6c8011415de893", size = 2070015, upload-time = "2026-04-15T14:49:47.301Z" }, + { url = "https://files.pythonhosted.org/packages/61/d5/fea36ad2882b99c174ef4ffbc7ea6523f6abe26060fbc1f77d6441670232/pydantic_core-2.46.1-cp312-cp312-win_arm64.whl", hash = "sha256:f76fb49c34b4d66aa6e552ce9e852ea97a3a06301a9f01ae82f23e449e3a55f8", size = 2030176, upload-time = "2026-04-15T14:50:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d2/bda39bad2f426cb5078e6ad28076614d3926704196efe0d7a2a19a99025d/pydantic_core-2.46.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:cdc8a5762a9c4b9d86e204d555444e3227507c92daba06259ee66595834de47a", size = 2119092, upload-time = "2026-04-15T14:49:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/69631e64d69cb3481494b2bddefe0ddd07771209f74e9106d066f9138c2a/pydantic_core-2.46.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ba381dfe9c85692c566ecb60fa5a77a697a2a8eebe274ec5e4d6ec15fafad799", size = 1951400, upload-time = "2026-04-15T14:51:06.588Z" }, + { url = "https://files.pythonhosted.org/packages/53/1c/21cb3db6ae997df31be8e91f213081f72ffa641cb45c89b8a1986832b1f9/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1593d8de98207466dc070118322fef68307a0cc6a5625e7b386f6fdae57f9ab6", size = 1976864, upload-time = "2026-04-15T14:50:54.804Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/05c819f734318ce5a6ca24da300d93696c105af4adb90494ee571303afd8/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8262c74a1af5b0fdf795f5537f7145785a63f9fbf9e15405f547440c30017ed8", size = 2066669, upload-time = "2026-04-15T14:51:42.346Z" }, + { url = "https://files.pythonhosted.org/packages/cb/23/fadddf1c7f2f517f58731aea9b35c914e6005250f08dac9b8e53904cdbaa/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b88949a24182e83fbbb3f7ca9b7858d0d37b735700ea91081434b7d37b3b444", size = 2238737, upload-time = "2026-04-15T14:50:45.558Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/0cd4f95cb0359c8b1ec71e89c3777e7932c8dfeb9cd54740289f310aaead/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8f3708cd55537aeaf3fd0ea55df0d68d0da51dcb07cbc8508745b34acc4c6e0", size = 2316258, upload-time = "2026-04-15T14:51:08.471Z" }, + { url = "https://files.pythonhosted.org/packages/0c/40/6fc24c3766a19c222a0d60d652b78f0283339d4cd4c173fab06b7ee76571/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f79292435fff1d4f0c18d9cfaf214025cc88e4f5104bfaed53f173621da1c743", size = 2097474, upload-time = "2026-04-15T14:49:56.543Z" }, + { url = "https://files.pythonhosted.org/packages/4b/af/f39795d1ce549e35d0841382b9c616ae211caffb88863147369a8d74fba9/pydantic_core-2.46.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:a2e607aeb59cf4575bb364470288db3b9a1f0e7415d053a322e3e154c1a0802e", size = 2168383, upload-time = "2026-04-15T14:51:29.269Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/0d563f74582795779df6cc270c3fc220f49f4daf7860d74a5a6cda8491ff/pydantic_core-2.46.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec5ca190b75878a9f6ae1fc8f5eb678497934475aef3d93204c9fa01e97370b6", size = 2186182, upload-time = "2026-04-15T14:50:19.097Z" }, + { url = "https://files.pythonhosted.org/packages/5c/07/1c10d5ce312fc4cf86d1e50bdcdbb8ef248409597b099cab1b4bb3a093f7/pydantic_core-2.46.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:1f80535259dcdd517d7b8ca588d5ca24b4f337228e583bebedf7a3adcdf5f721", size = 2187859, upload-time = "2026-04-15T14:49:22.974Z" }, + { url = "https://files.pythonhosted.org/packages/92/01/e1f62d4cb39f0913dbf5c95b9b119ef30ddba9493dff8c2b012f0cdd67dc/pydantic_core-2.46.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:24820b3c82c43df61eca30147e42853e6c127d8b868afdc0c162df829e011eb4", size = 2338372, upload-time = "2026-04-15T14:49:53.316Z" }, + { url = "https://files.pythonhosted.org/packages/44/ed/218dfeea6127fb1781a6ceca241ec6edf00e8a8933ff331af2215975a534/pydantic_core-2.46.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f12794b1dd8ac9fb66619e0b3a0427189f5d5638e55a3de1385121a9b7bf9b39", size = 2384039, upload-time = "2026-04-15T14:53:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1e/011e763cd059238249fbd5780e0f8d0b04b47f86c8925e22784f3e5fc977/pydantic_core-2.46.1-cp313-cp313-win32.whl", hash = "sha256:9bc09aed935cdf50f09e908923f9efbcca54e9244bd14a5a0e2a6c8d2c21b4e9", size = 1977943, upload-time = "2026-04-15T14:52:17.969Z" }, + { url = "https://files.pythonhosted.org/packages/8c/06/b559a490d3ed106e9b1777b8d5c8112dd8d31716243cd662616f66c1f8ea/pydantic_core-2.46.1-cp313-cp313-win_amd64.whl", hash = "sha256:fac2d6c8615b8b42bee14677861ba09d56ee076ba4a65cfb9c3c3d0cc89042f2", size = 2068729, upload-time = "2026-04-15T14:53:07.288Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/32a198946e2e19508532aa9da02a61419eb15bd2d96bab57f810f2713e31/pydantic_core-2.46.1-cp313-cp313-win_arm64.whl", hash = "sha256:f978329f12ace9f3cb814a5e44d98bbeced2e36f633132bafa06d2d71332e33e", size = 2029550, upload-time = "2026-04-15T14:52:22.707Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2b/6793fe89ab66cb2d3d6e5768044eab80bba1d0fae8fd904d0a1574712e17/pydantic_core-2.46.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9917cb61effac7ec0f448ef491ec7584526d2193be84ff981e85cbf18b68c42a", size = 2118110, upload-time = "2026-04-15T14:50:52.947Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/e9a905ddfcc2fd7bd862b340c02be6ab1f827922822d425513635d0ac774/pydantic_core-2.46.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e749679ca9f8a9d0bff95fb7f6b57bb53f2207fa42ffcc1ec86de7e0029ab89", size = 1948645, upload-time = "2026-04-15T14:51:55.577Z" }, + { url = "https://files.pythonhosted.org/packages/15/23/26e67f86ed62ac9d6f7f3091ee5220bf14b5ac36fb811851d601365ef896/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2ecacee70941e233a2dad23f7796a06f86cc10cc2fbd1c97c7dd5b5a79ffa4f", size = 1977576, upload-time = "2026-04-15T14:49:37.58Z" }, + { url = "https://files.pythonhosted.org/packages/b8/78/813c13c0de323d4de54ee2e6fdd69a0271c09ac8dd65a8a000931aa487a5/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:647d0a2475b8ed471962eed92fa69145b864942f9c6daa10f95ac70676637ae7", size = 2060358, upload-time = "2026-04-15T14:51:40.087Z" }, + { url = "https://files.pythonhosted.org/packages/09/5e/4caf2a15149271fbd2b4d968899a450853c800b85152abcf54b11531417f/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac9cde61965b0697fce6e6cc372df9e1ad93734828aac36e9c1c42a22ad02897", size = 2235980, upload-time = "2026-04-15T14:50:34.535Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c1/a2cdabb5da6f5cb63a3558bcafffc20f790fa14ccffbefbfb1370fadc93f/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a2eb0864085f8b641fb3f54a2fb35c58aff24b175b80bc8a945050fcde03204", size = 2316800, upload-time = "2026-04-15T14:52:46.999Z" }, + { url = "https://files.pythonhosted.org/packages/76/fd/19d711e4e9331f9d77f222bffc202bf30ea0d74f6419046376bb82f244c8/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b83ce9fede4bc4fb649281d9857f06d30198b8f70168f18b987518d713111572", size = 2101762, upload-time = "2026-04-15T14:49:24.278Z" }, + { url = "https://files.pythonhosted.org/packages/dc/64/ce95625448e1a4e219390a2923fd594f3fa368599c6b42ac71a5df7238c9/pydantic_core-2.46.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:cb33192753c60f269d2f4a1db8253c95b0df6e04f2989631a8cc1b0f4f6e2e92", size = 2167737, upload-time = "2026-04-15T14:50:41.637Z" }, + { url = "https://files.pythonhosted.org/packages/ad/31/413572d03ca3e73b408f00f54418b91a8be6401451bc791eaeff210328e5/pydantic_core-2.46.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:96611d51f953f87e1ae97637c01ee596a08b7f494ea00a5afb67ea6547b9f53b", size = 2185658, upload-time = "2026-04-15T14:51:46.799Z" }, + { url = "https://files.pythonhosted.org/packages/36/09/e4f581353bdf3f0c7de8a8b27afd14fc761da29d78146376315a6fedc487/pydantic_core-2.46.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9b176fa55f9107db5e6c86099aa5bfd934f1d3ba6a8b43f714ddeebaed3f42b7", size = 2184154, upload-time = "2026-04-15T14:52:49.629Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a4/d0d52849933f5a4bf1ad9d8da612792f96469b37e286a269e3ee9c60bbb1/pydantic_core-2.46.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:79a59f63a4ce4f3330e27e6f3ce281dd1099453b637350e97d7cf24c207cd120", size = 2332379, upload-time = "2026-04-15T14:49:55.009Z" }, + { url = "https://files.pythonhosted.org/packages/30/93/25bfb08fdbef419f73290e573899ce938a327628c34e8f3a4bafeea30126/pydantic_core-2.46.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:f200fce071808a385a314b7343f5e3688d7c45746be3d64dc71ee2d3e2a13268", size = 2377964, upload-time = "2026-04-15T14:51:59.649Z" }, + { url = "https://files.pythonhosted.org/packages/15/36/b777766ff83fef1cf97473d64764cd44f38e0d8c269ed06faace9ae17666/pydantic_core-2.46.1-cp314-cp314-win32.whl", hash = "sha256:3a07eccc0559fb9acc26d55b16bf8ebecd7f237c74a9e2c5741367db4e6d8aff", size = 1976450, upload-time = "2026-04-15T14:51:57.665Z" }, + { url = "https://files.pythonhosted.org/packages/7b/4b/4cd19d2437acfc18ca166db5a2067040334991eb862c4ecf2db098c91fbf/pydantic_core-2.46.1-cp314-cp314-win_amd64.whl", hash = "sha256:1706d270309ac7d071ffe393988c471363705feb3d009186e55d17786ada9622", size = 2067750, upload-time = "2026-04-15T14:49:38.941Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a0/490751c0ef8f5b27aae81731859aed1508e72c1a9b5774c6034269db773b/pydantic_core-2.46.1-cp314-cp314-win_arm64.whl", hash = "sha256:22d4e7457ade8af06528012f382bc994a97cc2ce6e119305a70b3deff1e409d6", size = 2021109, upload-time = "2026-04-15T14:50:27.728Z" }, + { url = "https://files.pythonhosted.org/packages/36/3a/2a018968245fffd25d5f1972714121ad309ff2de19d80019ad93494844f9/pydantic_core-2.46.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:607ff9db0b7e2012e7eef78465e69f9a0d7d1c3e7c6a84cf0c4011db0fcc3feb", size = 2111548, upload-time = "2026-04-15T14:52:08.273Z" }, + { url = "https://files.pythonhosted.org/packages/77/5b/4103b6192213217e874e764e5467d2ff10d8873c1147d01fa432ac281880/pydantic_core-2.46.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8cda3eacaea13bd02a1bea7e457cc9fc30b91c5a91245cef9b215140f80dd78c", size = 1926745, upload-time = "2026-04-15T14:50:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/c3/70/602a667cf4be4bec6c3334512b12ae4ea79ce9bfe41dc51be1fd34434453/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9493279cdc7997fe19e5ed9b41f30cbc3806bd4722adb402fedb6f6d41bd72a", size = 1965922, upload-time = "2026-04-15T14:51:12.555Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/06a89ce5323e755b7d2812189f9706b87aaebe49b34d247b380502f7992c/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3644e5e10059999202355b6c6616e624909e23773717d8f76deb8a6e2a72328c", size = 2043221, upload-time = "2026-04-15T14:51:18.995Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6e/b1d9ad907d9d76964903903349fd2e33c87db4b993cc44713edcad0fc488/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ad6c9de57683e26c92730991960c0c3571b8053263b042de2d3e105930b2767", size = 2243655, upload-time = "2026-04-15T14:50:10.718Z" }, + { url = "https://files.pythonhosted.org/packages/ef/73/787abfaad51174641abb04c8aa125322279b40ad7ce23c495f5a69f76554/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:557ebaa27c7617e7088002318c679a8ce685fa048523417cd1ca52b7f516d955", size = 2295976, upload-time = "2026-04-15T14:53:09.694Z" }, + { url = "https://files.pythonhosted.org/packages/56/0b/b7c5a631b6d5153d4a1ea4923b139aea256dc3bd99c8e6c7b312c7733146/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cd37e39b22b796ba0298fe81e9421dd7b65f97acfbb0fb19b33ffdda7b9a7b4", size = 2103439, upload-time = "2026-04-15T14:50:08.32Z" }, + { url = "https://files.pythonhosted.org/packages/2a/3f/952ee470df69e5674cdec1cbde22331adf643b5cc2ff79f4292d80146ee4/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:6689443b59714992e67d62505cdd2f952d6cf1c14cc9fd9aeec6719befc6f23b", size = 2132871, upload-time = "2026-04-15T14:50:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8b/1dea3b1e683c60c77a60f710215f90f486755962aa8939dbcb7c0f975ac3/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f32c41ca1e3456b5dd691827b7c1433c12d5f0058cc186afbb3615bc07d97b8", size = 2168658, upload-time = "2026-04-15T14:52:24.897Z" }, + { url = "https://files.pythonhosted.org/packages/67/97/32ae283810910d274d5ba9f48f856f5f2f612410b78b249f302d297816f5/pydantic_core-2.46.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:88cd1355578852db83954dc36e4f58f299646916da976147c20cf6892ba5dc43", size = 2171184, upload-time = "2026-04-15T14:52:34.854Z" }, + { url = "https://files.pythonhosted.org/packages/a2/57/c9a855527fe56c2072070640221f53095b0b19eaf651f3c77643c9cabbe3/pydantic_core-2.46.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:a170fefdb068279a473cc9d34848b85e61d68bfcc2668415b172c5dfc6f213bf", size = 2316573, upload-time = "2026-04-15T14:52:12.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/14c39ffc7399819c5448007c7bcb4e6da5669850cfb7dcbb727594290b48/pydantic_core-2.46.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:556a63ff1006934dba4eed7ea31b58274c227e29298ec398e4275eda4b905e95", size = 2378340, upload-time = "2026-04-15T14:51:02.619Z" }, + { url = "https://files.pythonhosted.org/packages/01/55/a37461fbb29c053ea4e62cfc5c2d56425cb5efbef8316e63f6d84ae45718/pydantic_core-2.46.1-cp314-cp314t-win32.whl", hash = "sha256:3b146d8336a995f7d7da6d36e4a779b7e7dff2719ac00a1eb8bd3ded00bec87b", size = 1960843, upload-time = "2026-04-15T14:52:06.103Z" }, + { url = "https://files.pythonhosted.org/packages/22/d7/97e1221197d17a27f768363f87ec061519eeeed15bbd315d2e9d1429ff03/pydantic_core-2.46.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f1bc856c958e6fe9ec071e210afe6feb695f2e2e81fd8d2b102f558d364c4c17", size = 2048696, upload-time = "2026-04-15T14:52:52.154Z" }, + { url = "https://files.pythonhosted.org/packages/19/d5/4eac95255c7d35094b46a32ec1e4d80eac94729c694726ee1d69948bd5f0/pydantic_core-2.46.1-cp314-cp314t-win_arm64.whl", hash = "sha256:21a5bfd8a1aa4de60494cdf66b0c912b1495f26a8899896040021fbd6038d989", size = 2022343, upload-time = "2026-04-15T14:49:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/44/4b/1952d38a091aa7572c13460db4439d5610a524a1a533fb131e17d8eff9c2/pydantic_core-2.46.1-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c56887c0ffa05318128a80303c95066a9d819e5e66d75ff24311d9e0a58d6930", size = 2123089, upload-time = "2026-04-15T14:50:20.658Z" }, + { url = "https://files.pythonhosted.org/packages/90/06/f3623aa98e2d7cb4ed0ae0b164c5d8a1b86e5aca01744eba980eefcd5da4/pydantic_core-2.46.1-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:614b24b875c1072631065fa85e195b40700586afecb0b27767602007920dacf8", size = 1945481, upload-time = "2026-04-15T14:50:56.945Z" }, + { url = "https://files.pythonhosted.org/packages/69/f9/a9224203b8426893e22db2cf0da27cd930ad7d76e0a611ebd707e5e6c916/pydantic_core-2.46.1-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6382f6967c48519b6194e9e1e579e5898598b682556260eeaf05910400d827e", size = 1986294, upload-time = "2026-04-15T14:49:31.839Z" }, + { url = "https://files.pythonhosted.org/packages/96/29/954d2174db68b9f14292cef3ae8a05a25255735909adfcf45ca768023713/pydantic_core-2.46.1-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93cb8aa6c93fb833bb53f3a2841fbea6b4dc077453cd5b30c0634af3dee69369", size = 2144185, upload-time = "2026-04-15T14:52:39.449Z" }, + { url = "https://files.pythonhosted.org/packages/f4/97/95de673a1356a88b2efdaa120eb6af357a81555c35f6809a7a1423ff7aef/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:5f9107a24a4bc00293434dfa95cf8968751ad0dd703b26ea83a75a56f7326041", size = 2107564, upload-time = "2026-04-15T14:50:49.14Z" }, + { url = "https://files.pythonhosted.org/packages/00/fc/a7c16d85211ea9accddc693b7d049f20b0c06440d9264d1e1c074394ee6c/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:2b1801ba99876984d0a03362782819238141c4d0f3f67f69093663691332fc35", size = 1939925, upload-time = "2026-04-15T14:50:36.188Z" }, + { url = "https://files.pythonhosted.org/packages/2e/23/87841169d77820ddabeb81d82002c95dcb82163846666d74f5bdeeaec750/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7fd82a91a20ed6d54fa8c91e7a98255b1ff45bf09b051bfe7fe04eb411e232e", size = 1995313, upload-time = "2026-04-15T14:50:22.538Z" }, + { url = "https://files.pythonhosted.org/packages/ea/96/b46609359a354fa9cd336fc5d93334f1c358b756cc81e4b397347a88fa6f/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f135bf07c92c93def97008bc4496d16934da9efefd7204e5f22a2c92523cb1f", size = 2151197, upload-time = "2026-04-15T14:51:22.925Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/3d1d2999ad8e78b124c752e4fc583ecd98f3bea7cc42045add2fb6e31b62/pydantic_core-2.46.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b44b44537efbff2df9567cd6ba51b554d6c009260a021ab25629c81e066f1683", size = 2121103, upload-time = "2026-04-15T14:52:59.537Z" }, + { url = "https://files.pythonhosted.org/packages/de/08/50a56632994007c7a58c86f782accccbe2f3bb7ca80f462533e26424cd18/pydantic_core-2.46.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f9ca3af687cc6a5c89aeaa00323222fcbceb4c3cdc78efdac86f46028160c04", size = 1952464, upload-time = "2026-04-15T14:52:04.001Z" }, + { url = "https://files.pythonhosted.org/packages/75/0b/3cf631e33a55b1788add3e42ac921744bd1f39279082a027b4ef6f48bd32/pydantic_core-2.46.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2678a4cbc205f00a44542dca19d15c11ccddd7440fd9df0e322e2cae55bb67a", size = 2138504, upload-time = "2026-04-15T14:52:01.812Z" }, + { url = "https://files.pythonhosted.org/packages/fa/69/f96f3dfc939450b9aeb80d3fe1943e7bc0614b14e9447d84f48d65153e0c/pydantic_core-2.46.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e5a98cbb03a8a7983b0fb954e0af5e7016587f612e6332c6a4453f413f1d1851", size = 2165467, upload-time = "2026-04-15T14:52:15.455Z" }, + { url = "https://files.pythonhosted.org/packages/a8/22/bb61cccddc2ce85b179cd81a580a1746e880870060fbf4bf6024dab7e8aa/pydantic_core-2.46.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:b2f098b08860bd149e090ad232f27fffb5ecf1bfd9377015445c8e17355ec2d1", size = 2183882, upload-time = "2026-04-15T14:51:50.868Z" }, + { url = "https://files.pythonhosted.org/packages/0e/01/b9039da255c5fd3a7fd85344fda8861c847ad6d8fdd115580fa4505b2022/pydantic_core-2.46.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d2623606145b55a96efdd181b015c0356804116b2f14d3c2af4832fe4f45ed5f", size = 2323011, upload-time = "2026-04-15T14:49:40.32Z" }, + { url = "https://files.pythonhosted.org/packages/24/b1/f426b20cb72d0235718ccc4de3bc6d6c0d0c2a91a3fd2f32ae11b624bcc9/pydantic_core-2.46.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:420f515c42aaec607ff720867b300235bd393abd709b26b190ceacb57a9bfc17", size = 2365696, upload-time = "2026-04-15T14:49:41.936Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d2/d2b0025246481aa2ce6db8ba196e29b92063343ac76e675b3a1fa478ed4d/pydantic_core-2.46.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:375cfdd2a1049910c82ba2ff24f948e93599a529e0fdb066d747975ca31fc663", size = 2190970, upload-time = "2026-04-15T14:49:33.111Z" }, ] [[package]] @@ -1846,7 +1844,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "21.2.1" +version = "21.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -1855,16 +1853,16 @@ dependencies = [ { name = "python-discovery" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/c5/aff062c66b42e2183201a7ace10c6b2e959a9a16525c8e8ca8e59410d27a/virtualenv-21.2.1.tar.gz", hash = "sha256:b66ffe81301766c0d5e2208fc3576652c59d44e7b731fc5f5ed701c9b537fa78", size = 5844770, upload-time = "2026-04-09T18:47:11.482Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/98/3a7e644e19cb26133488caff231be390579860bbbb3da35913c49a1d0a46/virtualenv-21.2.4.tar.gz", hash = "sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada", size = 5850742, upload-time = "2026-04-14T22:15:31.438Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/0e/f083a76cb590e60dff3868779558eefefb8dfb7c9ed020babc7aa014ccbf/virtualenv-21.2.1-py3-none-any.whl", hash = "sha256:bd16b49c53562b28cf1a3ad2f36edb805ad71301dee70ddc449e5c88a9f919a2", size = 5828326, upload-time = "2026-04-09T18:47:09.331Z" }, + { url = "https://files.pythonhosted.org/packages/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac", size = 5831232, upload-time = "2026-04-14T22:15:29.342Z" }, ] [[package]] name = "zipp" -version = "3.23.0" +version = "3.23.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, ] From 5ffac60bb48fba07879c48683d22868b4f07a6a5 Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Thu, 16 Apr 2026 18:30:51 +0500 Subject: [PATCH 17/17] chore(release): finalize changelog for 2.0.0b5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e796e2..db54f6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [2.0.0b5] +## [2.0.0b5] - 2026-04-16 Expands the canonical contract with adoption, API-surface, and coverage-join layers; clarifies run interpretation across MCP/HTML/clients; tightens MCP launcher/runtime behavior.