From 04d72bf2168ed09849905815a5fbc486b2008d39 Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 1 May 2026 13:19:43 +0500 Subject: [PATCH 1/7] release: stabilize v2 surfaces and GitHub Action - move package/docs/extension/plugin metadata to stable 2.0.0 defaults - refresh CodeClone branding across docs, HTML report, VS Code, Claude Desktop, and Codex plugin assets - polish the composite GitHub Action install policy, helper structure, and PR review summary - add/update tests for action defaults, extension support, and plugin metadata --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/ISSUE_TEMPLATE/cfg_semantics.yml | 2 +- .github/ISSUE_TEMPLATE/false_positive.yml | 2 +- .github/ISSUE_TEMPLATE/mcp_server.yml | 2 +- .github/actions/codeclone/README.md | 35 +- .github/actions/codeclone/_action_impl.py | 592 +++++++++++++++--- .github/actions/codeclone/action.yml | 4 +- .../actions/codeclone/render_pr_comment.py | 107 +++- .github/actions/codeclone/run_codeclone.py | 49 +- AGENTS.md | 14 +- CHANGELOG.md | 14 + README.md | 190 +++--- codeclone/report/html/widgets/icons.py | 18 +- docs/README-pypi.md | 169 +++++ docs/README.md | 4 +- docs/assets/codeclone-wordmark-dark.svg | 25 +- docs/assets/codeclone-wordmark.svg | 25 +- docs/book/04-config-and-defaults.md | 2 +- docs/book/08-report.md | 2 +- docs/book/20-mcp-interface.md | 14 +- docs/book/21-vscode-extension.md | 4 +- docs/book/appendix/b-schema-layouts.md | 10 +- docs/claude-desktop-bundle.md | 4 +- docs/codex-plugin.md | 4 +- docs/mcp.md | 22 +- docs/terms-of-use.md | 2 +- docs/vscode-extension.md | 11 +- extensions/claude-desktop-codeclone/README.md | 4 +- .../claude-desktop-codeclone/manifest.json | 4 +- .../claude-desktop-codeclone/media/icon.png | Bin 1815 -> 2079 bytes .../package-lock.json | 4 +- .../claude-desktop-codeclone/package.json | 2 +- extensions/vscode-codeclone/CHANGELOG.md | 5 + extensions/vscode-codeclone/README.md | 10 +- .../vscode-codeclone/media/icon-source.svg | 35 +- extensions/vscode-codeclone/media/icon.png | Bin 1815 -> 2079 bytes extensions/vscode-codeclone/package-lock.json | 4 +- extensions/vscode-codeclone/package.json | 3 +- extensions/vscode-codeclone/src/constants.js | 2 +- extensions/vscode-codeclone/src/renderers.js | 2 +- extensions/vscode-codeclone/src/support.js | 5 +- .../test/runArtifacts.test.js | 4 +- .../vscode-codeclone/test/support.test.js | 15 +- mkdocs.yml | 2 + plugins/codeclone/.codex-plugin/plugin.json | 4 +- plugins/codeclone/README.md | 4 +- plugins/codeclone/assets/icon.png | Bin 1815 -> 2079 bytes plugins/codeclone/assets/logo.png | Bin 1815 -> 2079 bytes pyproject.toml | 6 +- tests/test_codex_plugin.py | 4 +- tests/test_github_action_helpers.py | 83 ++- uv.lock | 4 +- 52 files changed, 1099 insertions(+), 436 deletions(-) create mode 100644 docs/README-pypi.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 80e478f..94841e9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -16,7 +16,7 @@ body: attributes: label: CodeClone version description: Output of `codeclone --version` - placeholder: "2.0.0b4" + placeholder: "2.0.0" validations: required: true diff --git a/.github/ISSUE_TEMPLATE/cfg_semantics.yml b/.github/ISSUE_TEMPLATE/cfg_semantics.yml index a070429..527f05c 100644 --- a/.github/ISSUE_TEMPLATE/cfg_semantics.yml +++ b/.github/ISSUE_TEMPLATE/cfg_semantics.yml @@ -15,7 +15,7 @@ body: id: version attributes: label: CodeClone version - placeholder: "2.0.0b4" + placeholder: "2.0.0" - type: textarea id: scenario diff --git a/.github/ISSUE_TEMPLATE/false_positive.yml b/.github/ISSUE_TEMPLATE/false_positive.yml index acaa38a..22c3ab2 100644 --- a/.github/ISSUE_TEMPLATE/false_positive.yml +++ b/.github/ISSUE_TEMPLATE/false_positive.yml @@ -15,7 +15,7 @@ body: id: version attributes: label: CodeClone version - placeholder: "2.0.0b4" + placeholder: "2.0.0" validations: required: true diff --git a/.github/ISSUE_TEMPLATE/mcp_server.yml b/.github/ISSUE_TEMPLATE/mcp_server.yml index e897c51..f4bd362 100644 --- a/.github/ISSUE_TEMPLATE/mcp_server.yml +++ b/.github/ISSUE_TEMPLATE/mcp_server.yml @@ -17,7 +17,7 @@ body: attributes: label: CodeClone version description: Output of `codeclone --version` - placeholder: "2.0.0b4" + placeholder: "2.0.0" validations: required: true diff --git a/.github/actions/codeclone/README.md b/.github/actions/codeclone/README.md index aa05a22..e2f09d2 100644 --- a/.github/actions/codeclone/README.md +++ b/.github/actions/codeclone/README.md @@ -30,13 +30,19 @@ source under test. Remote consumers still install from PyPI. ## Basic usage ```yaml -- uses: orenlab/codeclone/.github/actions/codeclone@main +- uses: orenlab/codeclone/.github/actions/codeclone@v2 with: fail-on-new: "true" ``` -For released references, prefer pinning to a major version tag such as `@v2` -or to an immutable commit SHA. +For strict reproducibility, pin the full release tag: + +```yaml +- uses: orenlab/codeclone/.github/actions/codeclone@v2.0.0 +``` + +For long-lived workflows, `@v2` follows the latest compatible 2.x action +metadata. ## PR workflow example @@ -61,7 +67,7 @@ jobs: with: fetch-depth: 0 - - uses: orenlab/codeclone/.github/actions/codeclone@main + - uses: orenlab/codeclone/.github/actions/codeclone@v2 with: fail-on-new: "true" fail-health: "60" @@ -74,7 +80,7 @@ jobs: | Input | Default | Purpose | |-------------------------|---------------------------------|-------------------------------------------------------------------------------------------------------------------| | `python-version` | `3.14` | Python version used to run the action | -| `package-version` | `""` | CodeClone version from PyPI for remote installs; ignored when the action runs from the checked-out CodeClone repo | +| `package-version` | `2.0.0` | CodeClone version from PyPI for remote installs; ignored when the action runs from the checked-out CodeClone repo | | `path` | `.` | Project root to analyze | | `json-path` | `.cache/codeclone/report.json` | JSON report output path | | `sarif` | `true` | Generate SARIF and try to upload it | @@ -136,26 +142,27 @@ Notes: - if you only want gating and JSON output, you can disable `sarif` and `pr-comment` -## Stable vs prerelease installs +## Install policy + +Released action tags pin the PyPI package version in action metadata. For +example, `@v2.0.0` installs `codeclone==2.0.0` unless you override +`package-version`. -Stable: +Explicit prerelease or smoke-test override: ```yaml with: - package-version: "" + package-version: "" ``` -Explicit prerelease: +Local/self-repo validation: ```yaml -with: - package-version: "2.0.0b4" +- uses: ./.github/actions/codeclone ``` -Local/self-repo validation: - - `uses: ./.github/actions/codeclone` installs CodeClone from the checked-out - repository source, so beta branches and unreleased commits do not depend on + repository source, so release branches and unreleased commits do not depend on PyPI publication. ## Notes and limitations diff --git a/.github/actions/codeclone/_action_impl.py b/.github/actions/codeclone/_action_impl.py index b4d52b9..9cfa729 100644 --- a/.github/actions/codeclone/_action_impl.py +++ b/.github/actions/codeclone/_action_impl.py @@ -4,6 +4,17 @@ # SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Den Rozhnovskiy +"""GitHub Action helpers for running CodeClone and rendering PR feedback. + +This module is intentionally small and dependency-free. It builds the CodeClone +CLI invocation from action inputs, executes the analyzer, writes GitHub Actions +outputs, and renders a compact Markdown review comment from the canonical JSON +report. + +Public functions and dataclasses are used by the action entrypoint and should +remain stable. +""" + from __future__ import annotations import json @@ -14,10 +25,13 @@ from typing import Literal COMMENT_MARKER = "" +DEFAULT_CODECLONE_PACKAGE_VERSION = "2.0.0" @dataclass(frozen=True, slots=True) class ActionInputs: + """Normalized GitHub Action inputs used to build a CodeClone invocation.""" + path: str json_path: str sarif: bool @@ -39,6 +53,8 @@ class ActionInputs: @dataclass(frozen=True, slots=True) class RunResult: + """Result of a CodeClone CLI execution inside the action runtime.""" + exit_code: int json_path: str json_exists: bool @@ -48,15 +64,51 @@ class RunResult: @dataclass(frozen=True, slots=True) class InstallTarget: + """Resolved package requirement used by the action installer.""" + requirement: str - source: Literal["repo", "pypi-version", "pypi-latest"] + source: Literal["repo", "pypi-version", "pypi-default"] + + +@dataclass(frozen=True, slots=True) +class _PrCommentContext: + """Typed internal view over canonical report fields used by PR rendering.""" + + clone_summary: dict[str, object] + families: dict[str, object] + complexity: dict[str, object] + coupling: dict[str, object] + cohesion: dict[str, object] + dependencies: dict[str, object] + dead_code: dict[str, object] + overloaded_modules: dict[str, object] + coverage_join: dict[str, object] + security_surfaces: dict[str, object] + api_surface: dict[str, object] + health_score: int + health_grade: str + baseline_status: str + cache_label: str + codeclone_version: str def parse_bool(value: str) -> bool: + """Parse GitHub Action boolean input values. + + GitHub Action inputs arrive as strings. CodeClone action booleans are true + only when the normalized value is exactly ``"true"``. + """ + return value.strip().lower() == "true" def parse_optional_int(value: str) -> int | None: + """Parse optional integer action input values. + + Empty strings and ``-1`` are treated as unset values because GitHub Action + inputs do not have native nullable integer types. + """ + normalized = value.strip() if normalized in {"", "-1"}: return None @@ -64,43 +116,39 @@ def parse_optional_int(value: str) -> int | None: def build_codeclone_args(inputs: ActionInputs) -> list[str]: - args = [inputs.path, "--json", inputs.json_path] - if inputs.sarif: - args.extend(["--sarif", inputs.sarif_path]) - if inputs.no_progress: - args.append("--no-progress") - if inputs.fail_on_new: - args.append("--fail-on-new") - if inputs.fail_on_new_metrics: - args.append("--fail-on-new-metrics") - if inputs.fail_threshold is not None: - args.extend(["--fail-threshold", str(inputs.fail_threshold)]) - if inputs.fail_complexity is not None: - args.extend(["--fail-complexity", str(inputs.fail_complexity)]) - if inputs.fail_coupling is not None: - args.extend(["--fail-coupling", str(inputs.fail_coupling)]) - if inputs.fail_cohesion is not None: - args.extend(["--fail-cohesion", str(inputs.fail_cohesion)]) - if inputs.fail_cycles: - args.append("--fail-cycles") - if inputs.fail_dead_code: - args.append("--fail-dead-code") - if inputs.fail_health is not None: - args.extend(["--fail-health", str(inputs.fail_health)]) - if inputs.baseline_path.strip(): - args.extend(["--baseline", inputs.baseline_path]) - if inputs.metrics_baseline_path.strip(): - args.extend(["--metrics-baseline", inputs.metrics_baseline_path]) - if inputs.extra_args.strip(): - args.extend(shlex.split(inputs.extra_args)) + """Build CodeClone CLI arguments from normalized action inputs. + + The returned list intentionally excludes the executable name. Extra + arguments are parsed with :mod:`shlex` so quoted values behave like shell + arguments without invoking a shell. + """ + + args: list[str] = [inputs.path, "--json", inputs.json_path] + + for value, flag in _valued_codeclone_options(inputs): + if value is not None: + args.extend([flag, str(value)]) + + for enabled, flag in _boolean_codeclone_flags(inputs): + if enabled: + args.append(flag) + + extra_args = inputs.extra_args.strip() + if extra_args: + args.extend(shlex.split(extra_args)) + return args def ensure_parent_dir(path_text: str) -> None: + """Create the parent directory for an output path when needed.""" + Path(path_text).parent.mkdir(parents=True, exist_ok=True) def write_outputs(path: str, values: dict[str, str]) -> None: + """Append GitHub Action output values to ``GITHUB_OUTPUT``.""" + with open(path, "a", encoding="utf-8") as handle: for key, value in values.items(): handle.write(f"{key}={value}\n") @@ -113,8 +161,17 @@ def resolve_install_target( workspace: str, package_version: str, ) -> InstallTarget: + """Resolve whether the action should install CodeClone from repo or PyPI. + + When the action itself is executed from the same checkout as the workspace, + installing from the repository keeps local action smoke tests honest. + Otherwise the action installs either the explicitly requested PyPI version + or the stable default package version. + """ + action_root = Path(action_path).resolve().parents[2] workspace_root = Path(workspace).resolve() + if action_root == workspace_root: return InstallTarget(requirement=str(action_root), source="repo") @@ -124,112 +181,100 @@ def resolve_install_target( requirement=f"codeclone=={normalized_version}", source="pypi-version", ) - return InstallTarget(requirement="codeclone", source="pypi-latest") + + return InstallTarget( + requirement=f"codeclone=={DEFAULT_CODECLONE_PACKAGE_VERSION}", + source="pypi-default", + ) def run_codeclone(inputs: ActionInputs) -> RunResult: + """Run CodeClone and return output artifact status. + + The action treats analyzer timeouts as internal execution errors and maps + them to CodeClone's internal-error exit code ``5``. + """ + ensure_parent_dir(inputs.json_path) if inputs.sarif: ensure_parent_dir(inputs.sarif_path) + argv = ["codeclone", *build_codeclone_args(inputs)] + try: - completed = subprocess.run(argv, check=False, timeout=600) + completed = subprocess.run(argv, check=False, timeout=600, shell=False) except subprocess.TimeoutExpired: print("::error::CodeClone analysis timed out after 10 minutes") - return RunResult( - exit_code=5, - json_path=inputs.json_path, - json_exists=Path(inputs.json_path).exists(), - sarif_path=inputs.sarif_path, - sarif_exists=inputs.sarif and Path(inputs.sarif_path).exists(), - ) - return RunResult( - exit_code=completed.returncode, - json_path=inputs.json_path, - json_exists=Path(inputs.json_path).exists(), - sarif_path=inputs.sarif_path, - sarif_exists=inputs.sarif and Path(inputs.sarif_path).exists(), - ) - - -def _mapping(value: object) -> dict[str, object]: - return value if isinstance(value, dict) else {} - + return _run_result_from_paths(exit_code=5, inputs=inputs) -def _int(value: object, default: int = 0) -> int: - return value if isinstance(value, int) else default - - -def _str(value: object, default: str = "") -> str: - return value if isinstance(value, str) else default + return _run_result_from_paths(exit_code=completed.returncode, inputs=inputs) def render_pr_comment(report: dict[str, object], *, exit_code: int) -> str: - meta = _mapping(report.get("meta")) - findings = _mapping(report.get("findings")) - findings_summary = _mapping(findings.get("summary")) - clone_summary = _mapping(findings_summary.get("clones")) - families = _mapping(findings_summary.get("families")) - metrics = _mapping(report.get("metrics")) - metrics_summary = _mapping(metrics.get("summary")) - health = _mapping(metrics_summary.get("health")) - baseline = _mapping(meta.get("baseline")) - cache = _mapping(meta.get("cache")) - - health_score = _int(health.get("score"), default=-1) - health_grade = _str(health.get("grade"), default="?") - baseline_status = _str(baseline.get("status"), default="unknown") - cache_used = bool(cache.get("used")) - codeclone_version = _str(meta.get("codeclone_version"), default="?") + """Render a compact Markdown PR review comment from a canonical report.""" + + ctx = _build_pr_comment_context(report) + rows = _build_pr_comment_rows(ctx) + focus = _review_focus( + exit_code=exit_code, + clone_summary=ctx.clone_summary, + dependencies=ctx.dependencies, + coverage_join=ctx.coverage_join, + security_surfaces=ctx.security_surfaces, + overloaded_modules=ctx.overloaded_modules, + ) - status_icon = "white_check_mark" - status_label = "Passed" - if exit_code == 3: - status_icon = "x" - status_label = "Failed (gating)" - elif exit_code != 0: - status_icon = "warning" - status_label = "Error" + status_icon, status_label = _status_label(exit_code) lines = [ COMMENT_MARKER, - "## :microscope: CodeClone Report", + "## CodeClone Review", "", - "| Metric | Value |", - "|--------|-------|", - f"| Health | **{health_score}/100 ({health_grade})** |", - f"| Status | :{status_icon}: {status_label} |", - f"| Baseline | `{baseline_status}` |", - f"| Cache | `{'used' if cache_used else 'not used'}` |", - f"| Version | `{codeclone_version}` |", + ( + f"**{status_icon} {status_label}** · " + f"Health **{ctx.health_score}/100 ({ctx.health_grade})** · " + f"Baseline `{ctx.baseline_status}` · " + f"Cache `{ctx.cache_label}` · " + f"CodeClone `{ctx.codeclone_version}`" + ), "", - "### Findings", - "```text", - _clone_summary_line(clone_summary=clone_summary, families=families), - f"Structural: {_int(families.get('structural'))}", - f"Dead code: {_int(families.get('dead_code'))}", - f"Design: {_int(families.get('design'))}", - "```", + "### Review snapshot", + "| Area | Signal | Review note |", + "|------|--------|-------------|", + *[ + f"| {_table_cell(area)} | {_table_cell(signal)} | {_table_cell(note)} |" + for area, signal, note in rows + ], "", - ":robot: Generated by " + "### Review focus", + *[f"- {item}" for item in focus], + "", + "Security Surfaces are report-only capability inventory, " + "not vulnerability claims. Generated by " 'CodeClone', ] return "\n".join(lines) def write_step_summary(path: str, body: str) -> None: + """Append Markdown content to ``GITHUB_STEP_SUMMARY``.""" + with open(path, "a", encoding="utf-8") as handle: handle.write(body) handle.write("\n") def load_report(path: str) -> dict[str, object]: + """Load a CodeClone JSON report and return an empty mapping on bad shape.""" + with open(path, encoding="utf-8") as handle: loaded = json.load(handle) return loaded if isinstance(loaded, dict) else {} def build_inputs_from_env(env: dict[str, str]) -> ActionInputs: + """Build normalized action inputs from the GitHub Actions environment.""" + return ActionInputs( path=env["INPUT_PATH"], json_path=env["INPUT_JSON_PATH"], @@ -251,13 +296,358 @@ def build_inputs_from_env(env: dict[str, str]) -> ActionInputs: ) -def _clone_summary_line( +def _valued_codeclone_options( + inputs: ActionInputs, +) -> tuple[tuple[object | None, str], ...]: + """Return valued CLI options in deterministic output order.""" + + return ( + (inputs.sarif_path if inputs.sarif else None, "--sarif"), + (inputs.fail_threshold, "--fail-threshold"), + (inputs.fail_complexity, "--fail-complexity"), + (inputs.fail_coupling, "--fail-coupling"), + (inputs.fail_cohesion, "--fail-cohesion"), + (inputs.fail_health, "--fail-health"), + (inputs.baseline_path.strip() or None, "--baseline"), + (inputs.metrics_baseline_path.strip() or None, "--metrics-baseline"), + ) + + +def _boolean_codeclone_flags(inputs: ActionInputs) -> tuple[tuple[bool, str], ...]: + """Return boolean CLI flags in deterministic output order.""" + + return ( + (inputs.no_progress, "--no-progress"), + (inputs.fail_on_new, "--fail-on-new"), + (inputs.fail_on_new_metrics, "--fail-on-new-metrics"), + (inputs.fail_cycles, "--fail-cycles"), + (inputs.fail_dead_code, "--fail-dead-code"), + ) + + +def _run_result_from_paths(*, exit_code: int, inputs: ActionInputs) -> RunResult: + """Build a run result from expected output paths.""" + + json_path = Path(inputs.json_path) + sarif_path = Path(inputs.sarif_path) + + return RunResult( + exit_code=exit_code, + json_path=inputs.json_path, + json_exists=json_path.exists(), + sarif_path=inputs.sarif_path, + sarif_exists=inputs.sarif and sarif_path.exists(), + ) + + +def _mapping(value: object) -> dict[str, object]: + """Return ``value`` when it is a JSON object, otherwise an empty mapping.""" + + return value if isinstance(value, dict) else {} + + +def _int(value: object, default: int = 0) -> int: + """Return an integer JSON value or a default.""" + + return value if isinstance(value, int) else default + + +def _str(value: object, default: str = "") -> str: + """Return a string JSON value or a default.""" + + return value if isinstance(value, str) else default + + +def _float(value: object, default: float = 0.0) -> float: + """Return a numeric JSON value as float or a default.""" + + if isinstance(value, int | float): + return float(value) + return default + + +def _one_decimal(value: object) -> str: + """Format a numeric JSON value with one decimal place.""" + + return f"{_float(value):.1f}" + + +def _percent_from_permille(value: object) -> str: + """Format a permille JSON value as a percentage string.""" + + return f"{_float(value) / 10.0:.1f}%" + + +def _table_cell(value: object) -> str: + """Escape Markdown table cell separators and newlines.""" + + text = str(value) + return text.replace("|", "\\|").replace("\n", " ") + + +def _status_label(exit_code: int) -> tuple[str, str]: + """Map CodeClone exit codes to PR comment status labels.""" + + if exit_code == 0: + return ":white_check_mark:", "Passed" + if exit_code == 3: + return ":x:", "Failed (gating)" + if exit_code == 2: + return ":warning:", "Contract error" + return ":warning:", "Error" + + +def _build_pr_comment_context(report: dict[str, object]) -> _PrCommentContext: + """Extract the report fields needed for PR comment rendering.""" + + meta = _mapping(report.get("meta")) + findings = _mapping(report.get("findings")) + findings_summary = _mapping(findings.get("summary")) + metrics = _mapping(report.get("metrics")) + metrics_summary = _mapping(metrics.get("summary")) + + health = _mapping(metrics_summary.get("health")) + baseline = _mapping(meta.get("baseline")) + cache = _mapping(meta.get("cache")) + + return _PrCommentContext( + clone_summary=_mapping(findings_summary.get("clones")), + families=_mapping(findings_summary.get("families")), + complexity=_mapping(metrics_summary.get("complexity")), + coupling=_mapping(metrics_summary.get("coupling")), + cohesion=_mapping(metrics_summary.get("cohesion")), + dependencies=_mapping(metrics_summary.get("dependencies")), + dead_code=_mapping(metrics_summary.get("dead_code")), + overloaded_modules=_mapping(metrics_summary.get("overloaded_modules")), + coverage_join=_mapping(metrics_summary.get("coverage_join")), + security_surfaces=_mapping(metrics_summary.get("security_surfaces")), + api_surface=_mapping(metrics_summary.get("api_surface")), + health_score=_int(health.get("score"), default=-1), + health_grade=_str(health.get("grade"), default="?"), + baseline_status=_str(baseline.get("status"), default="unknown"), + cache_label="hit" if bool(cache.get("used")) else "miss", + codeclone_version=_str(meta.get("codeclone_version"), default="?"), + ) + + +def _build_pr_comment_rows(ctx: _PrCommentContext) -> list[tuple[str, str, str]]: + """Build the fixed PR comment review snapshot rows.""" + + coverage_signal, coverage_note = _format_coverage_join_row(ctx.coverage_join) + security_signal, security_note = _format_security_surfaces_row( + ctx.security_surfaces + ) + api_signal, api_note = _format_api_surface_row(ctx.api_surface) + + return [ + ( + "Clones", + _clone_signal_line( + clone_summary=ctx.clone_summary, + families=ctx.families, + ), + ( + "review new groups before merge" + if _int(ctx.clone_summary.get("new")) + else "no new clone debt reported" + ), + ), + ( + "Quality", + _format_quality_signal( + complexity=ctx.complexity, + coupling=ctx.coupling, + cohesion=ctx.cohesion, + overloaded_modules=ctx.overloaded_modules, + ), + "structural metric snapshot", + ), + ( + "Dependencies", + _format_dependencies_signal(ctx.dependencies), + ( + "acyclic" + if _int(ctx.dependencies.get("cycles")) == 0 + else "cycle review needed" + ), + ), + ("Coverage Join", coverage_signal, coverage_note), + ("Security Surfaces", security_signal, security_note), + ("API Surface", api_signal, api_note), + ( + "Dead code", + _format_dead_code_signal(ctx.dead_code), + ( + "clean" + if _int(ctx.dead_code.get("high_confidence")) == 0 + else "review candidates" + ), + ), + ] + + +def _format_quality_signal( + *, + complexity: dict[str, object], + coupling: dict[str, object], + cohesion: dict[str, object], + overloaded_modules: dict[str, object], +) -> str: + """Format the quality row signal.""" + + return ( + f"CC max {_int(complexity.get('max'))}, " + f"CBO max {_int(coupling.get('max'))}, " + f"LCOM4 max {_int(cohesion.get('max'))}, " + f"overloaded {_int(overloaded_modules.get('candidates'))}" + ) + + +def _format_dependencies_signal(dependencies: dict[str, object]) -> str: + """Format the dependency profile row signal.""" + + return ( + f"avg {_one_decimal(dependencies.get('avg_depth'))}, " + f"p95 {_int(dependencies.get('p95_depth'))}, " + f"max {_int(dependencies.get('max_depth'))}, " + f"cycles {_int(dependencies.get('cycles'))}" + ) + + +def _format_coverage_join_row(coverage_join: dict[str, object]) -> tuple[str, str]: + """Format the Coverage Join review row.""" + + if not coverage_join: + return "not joined", "no coverage.xml facts in this report" + + coverage_status = _str(coverage_join.get("status"), default="") + signal = ( + f"{_percent_from_permille(coverage_join.get('overall_permille'))} overall, " + f"{_int(coverage_join.get('coverage_hotspots'))} hotspots, " + f"{_int(coverage_join.get('scope_gap_hotspots'))} scope gaps" + ) + note = ( + "joined with coverage.xml" + if coverage_status == "ok" + else f"not joined: {_str(coverage_join.get('invalid_reason'), 'unknown')}" + ) + return signal, note + + +def _format_security_surfaces_row( + security_surfaces: dict[str, object], +) -> tuple[str, str]: + """Format the Security Surfaces review row.""" + + security_items = _int(security_surfaces.get("items")) + signal = ( + f"{security_items} surfaces, " + f"{_int(security_surfaces.get('category_count'))} categories, " + f"{_int(security_surfaces.get('production'))} production" + ) + note = ( + "report-only boundary inventory" + if security_items + else "no security surfaces reported" + ) + return signal, note + + +def _format_api_surface_row(api_surface: dict[str, object]) -> tuple[str, str]: + """Format the API Surface review row.""" + + api_enabled = bool(api_surface.get("enabled")) + signal = ( + f"{_int(api_surface.get('public_symbols'))} symbols, " + f"{_int(api_surface.get('modules'))} modules" + if api_enabled + else "disabled" + ) + note = ( + f"{_int(api_surface.get('breaking'))} breaking, " + f"{_int(api_surface.get('added'))} added" + if api_enabled + else "not part of this run" + ) + return signal, note + + +def _format_dead_code_signal(dead_code: dict[str, object]) -> str: + """Format the dead-code row signal.""" + + return ( + f"{_int(dead_code.get('high_confidence'))} high-confidence, " + f"{_int(dead_code.get('suppressed'))} suppressed" + ) + + +def _review_focus( + *, + exit_code: int, + clone_summary: dict[str, object], + dependencies: dict[str, object], + coverage_join: dict[str, object], + security_surfaces: dict[str, object], + overloaded_modules: dict[str, object], +) -> list[str]: + """Build focused follow-up suggestions for the PR comment.""" + + items: list[str] = [] + + if exit_code == 3: + items.append("CI gates failed; start with rows marked as gating-sensitive.") + elif exit_code == 2: + items.append( + "Contract error; check baseline/config trust before reviewing metrics." + ) + + new_clones = _int(clone_summary.get("new")) + if new_clones: + items.append(f"Review {new_clones} new clone group(s) before merge.") + + cycles = _int(dependencies.get("cycles")) + if cycles: + items.append( + f"Inspect {cycles} dependency cycle(s); cycles are hard structural risk." + ) + + coverage_hotspots = _int(coverage_join.get("coverage_hotspots")) + scope_gaps = _int(coverage_join.get("scope_gap_hotspots")) + if coverage_hotspots or scope_gaps: + items.append( + f"Use Coverage Join for {coverage_hotspots} coverage hotspot(s) " + f"and {scope_gaps} scope gap(s)." + ) + + production_surfaces = _int(security_surfaces.get("production")) + if production_surfaces: + items.append( + f"Treat {production_surfaces} production security surface(s) as " + "review-first boundary code when touched." + ) + + overloaded = _int(overloaded_modules.get("candidates")) + if overloaded: + items.append( + f"Review {overloaded} overloaded module candidate(s) " + "when they intersect this PR." + ) + + if not items: + items.append("No focused review pressure reported by the canonical summary.") + + return items + + +def _clone_signal_line( *, clone_summary: dict[str, object], families: dict[str, object], ) -> str: + """Format the clone summary row signal.""" + return ( - f"Clones: {_int(families.get('clones'))} " - f"({_int(clone_summary.get('new'))} new, " - f"{_int(clone_summary.get('known'))} known)" + f"{_int(families.get('clones'))} total, " + f"{_int(clone_summary.get('new'))} new, " + f"{_int(clone_summary.get('known'))} known" ) diff --git a/.github/actions/codeclone/action.yml b/.github/actions/codeclone/action.yml index 2d0d1f1..f397473 100644 --- a/.github/actions/codeclone/action.yml +++ b/.github/actions/codeclone/action.yml @@ -7,7 +7,7 @@ author: OrenLab branding: icon: copy - color: blue + color: purple inputs: python-version: @@ -18,7 +18,7 @@ inputs: package-version: description: "CodeClone version from PyPI for remote installs (ignored when the action runs from the checked-out CodeClone repo)" required: false - default: "" + default: "2.0.0" path: description: "Project root" diff --git a/.github/actions/codeclone/render_pr_comment.py b/.github/actions/codeclone/render_pr_comment.py index f08668e..79ec0be 100644 --- a/.github/actions/codeclone/render_pr_comment.py +++ b/.github/actions/codeclone/render_pr_comment.py @@ -4,9 +4,19 @@ # SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Den Rozhnovskiy +"""Render the CodeClone GitHub Action PR comment from a JSON report. + +This entrypoint is intentionally small: it reads action/runtime paths from the +environment, renders the Markdown comment from the canonical JSON report, writes +the comment body file, and exposes GitHub Action outputs for later workflow +steps. +""" + from __future__ import annotations import os +from dataclasses import dataclass +from pathlib import Path from _action_impl import ( load_report, @@ -16,43 +26,76 @@ ) +@dataclass(frozen=True, slots=True) +class _CommentRuntime: + """Environment-derived runtime paths for PR comment rendering.""" + + report_path: Path + output_path: Path + exit_code: int + github_output: str | None + step_summary: str | None + + def main() -> int: - report_path = os.environ["REPORT_PATH"] - output_path = os.environ["COMMENT_OUTPUT_PATH"] - exit_code = int(os.environ["ANALYSIS_EXIT_CODE"]) - - if not os.path.exists(report_path): - github_output = os.environ.get("GITHUB_OUTPUT") - if github_output: - write_outputs( - github_output, - { - "comment-exists": "false", - "comment-body-path": output_path, - }, - ) + """Render a PR comment when a CodeClone report exists.""" + + runtime = _comment_runtime_from_env(os.environ) + + if not runtime.report_path.exists(): + _write_comment_outputs(runtime, comment_exists=False) return 0 - body = render_pr_comment(load_report(report_path), exit_code=exit_code) - with open(output_path, "w", encoding="utf-8") as handle: - handle.write(body) - handle.write("\n") - - step_summary = os.environ.get("GITHUB_STEP_SUMMARY") - if step_summary: - write_step_summary(step_summary, body) - - github_output = os.environ.get("GITHUB_OUTPUT") - if github_output: - write_outputs( - github_output, - { - "comment-exists": "true", - "comment-body-path": output_path, - }, - ) + body = render_pr_comment( + load_report(str(runtime.report_path)), + exit_code=runtime.exit_code, + ) + _write_comment_body(runtime.output_path, body) + + if runtime.step_summary: + write_step_summary(runtime.step_summary, body) + + _write_comment_outputs(runtime, comment_exists=True) return 0 +def _comment_runtime_from_env(env: os._Environ[str]) -> _CommentRuntime: + """Build comment-rendering runtime from GitHub Action environment values.""" + + return _CommentRuntime( + report_path=Path(env["REPORT_PATH"]), + output_path=Path(env["COMMENT_OUTPUT_PATH"]), + exit_code=int(env["ANALYSIS_EXIT_CODE"]), + github_output=env.get("GITHUB_OUTPUT"), + step_summary=env.get("GITHUB_STEP_SUMMARY"), + ) + + +def _write_comment_body(path: Path, body: str) -> None: + """Write the rendered Markdown comment body.""" + + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(f"{body}\n", encoding="utf-8") + + +def _write_comment_outputs( + runtime: _CommentRuntime, + *, + comment_exists: bool, +) -> None: + """Expose PR comment metadata through ``GITHUB_OUTPUT`` when available.""" + + if not runtime.github_output: + return + + write_outputs( + runtime.github_output, + { + "comment-exists": "true" if comment_exists else "false", + "comment-body-path": str(runtime.output_path), + }, + ) + + if __name__ == "__main__": raise SystemExit(main()) diff --git a/.github/actions/codeclone/run_codeclone.py b/.github/actions/codeclone/run_codeclone.py index b253289..81339a2 100644 --- a/.github/actions/codeclone/run_codeclone.py +++ b/.github/actions/codeclone/run_codeclone.py @@ -4,29 +4,52 @@ # SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Den Rozhnovskiy +"""Run CodeClone inside the GitHub Action runtime. + +This entrypoint normalizes GitHub Action inputs from the environment, executes +CodeClone, and exposes artifact paths plus analyzer exit status through +GITHUB_OUTPUT. The process itself returns 0 so later workflow steps can +decide how to handle the analyzer result. +""" + from __future__ import annotations import os -from _action_impl import build_inputs_from_env, run_codeclone, write_outputs +from _action_impl import RunResult, build_inputs_from_env, run_codeclone, write_outputs def main() -> int: + """Run CodeClone and publish action outputs.""" + result = run_codeclone(build_inputs_from_env(dict(os.environ))) - github_output = os.environ.get("GITHUB_OUTPUT") - if github_output: - write_outputs( - github_output, - { - "exit-code": str(result.exit_code), - "json-path": result.json_path, - "json-exists": str(result.json_exists).lower(), - "sarif-path": result.sarif_path, - "sarif-exists": str(result.sarif_exists).lower(), - }, - ) + _write_run_outputs(github_output=os.environ.get("GITHUB_OUTPUT"), result=result) return 0 +def _write_run_outputs(*, github_output: str | None, result: RunResult) -> None: + """Expose CodeClone run metadata through GITHUB_OUTPUT when available.""" + + if not github_output: + return + + write_outputs( + github_output, + { + "exit-code": str(result.exit_code), + "json-path": result.json_path, + "json-exists": _bool_output(result.json_exists), + "sarif-path": result.sarif_path, + "sarif-exists": _bool_output(result.sarif_exists), + }, + ) + + +def _bool_output(value: bool) -> str: + """Format a boolean value for GitHub Action outputs.""" + + return str(value).lower() + + if __name__ == "__main__": raise SystemExit(main()) diff --git a/AGENTS.md b/AGENTS.md index a645b79..8f4190f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,10 +61,10 @@ Key artifacts: - `.cache/codeclone/cache.json` — analysis cache (integrity-checked) - `.cache/codeclone/report.html|report.json|report.md|report.sarif|report.txt` — reports - `codeclone-mcp` — optional read-only MCP server (install via `codeclone[mcp]`) -- `extensions/vscode-codeclone/` — preview VS Code extension as a native, read-only IDE client over `codeclone-mcp` -- `extensions/claude-desktop-codeclone/` — preview Claude Desktop `.mcpb` bundle as a local install wrapper over +- `extensions/vscode-codeclone/` — stable VS Code extension as a native, read-only IDE client over `codeclone-mcp` +- `extensions/claude-desktop-codeclone/` — stable Claude Desktop `.mcpb` bundle as a local install wrapper over `codeclone-mcp` -- `plugins/codeclone/` + `.agents/plugins/marketplace.json` — preview Codex plugin as a native local discovery layer +- `plugins/codeclone/` + `.agents/plugins/marketplace.json` — stable Codex plugin as a native local discovery layer over `codeclone-mcp`, with a bundled CodeClone review skill - MCP runs are in-memory only; review markers are session-local and must never leak into baseline/cache/report artifacts @@ -109,7 +109,7 @@ smoke: ```bash cd extensions/vscode-codeclone -vsce package --pre-release --out /tmp/codeclone.vsix +vsce package --out /tmp/codeclone.vsix ``` If you touched the Claude Desktop bundle surface, also run: @@ -463,11 +463,11 @@ Use this map to route changes to the right owner module. - `codeclone/ui_messages/*` — CLI text/marker/help constants and formatter helpers. Keep message policy centralized. - `docs/`, `mkdocs.yml`, `.github/workflows/docs.yml`, `scripts/build_docs_example_report.py` — docs-site source, publication workflow, and live sample-report generation; keep published docs aligned with code contracts. -- `extensions/vscode-codeclone/*` — preview VS Code extension surface; keep it baseline-aware, triage-first, +- `extensions/vscode-codeclone/*` — stable VS Code extension surface; keep it baseline-aware, triage-first, source-first, and faithful to MCP/canonical report semantics rather than building a second analyzer or report model. -- `extensions/claude-desktop-codeclone/*` — preview Claude Desktop bundle surface; keep it local-stdio-only, +- `extensions/claude-desktop-codeclone/*` — stable Claude Desktop bundle surface; keep it local-stdio-only, launcher-focused, and faithful to `codeclone-mcp` rather than re-implementing MCP semantics in the bundle layer. -- `plugins/codeclone/*`, `.agents/plugins/marketplace.json` — preview Codex plugin surface; keep it Codex-native, +- `plugins/codeclone/*`, `.agents/plugins/marketplace.json` — stable Codex plugin surface; keep it Codex-native, conservative-first, skills-guided, and faithful to `codeclone-mcp` rather than inventing plugin-only analysis logic. - `tests/` — executable specification: architecture rules, contracts, goldens, invariants, regressions. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7665c44..cd46c27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [2.0.0] - 2026-04-30 + +`2.0.0` promotes the completed 2.0 release line to the stable public contract. + +### Release + +- Mark the Python package as stable (`2.0.0`) while keeping the established baseline, cache, report, and metrics + baseline schemas unchanged. +- Make stable install guidance the default across README, docs, MCP guides, and local integration surfaces; prerelease + installs remain available only as explicit version pins. +- Align VS Code, Claude Desktop, and Codex integration metadata with the final CodeClone 2.0 MCP package. +- Preserve the 2.0 behavior set: canonical package layout, adaptive dependency depth profiling, Coverage Join, + report-only Security Surfaces, read-only MCP, and native IDE/agent projections. + ## [2.0.0b7] - 2026-04-28 `2.0.0b7` is a beta hotfix for packaging-only issues found after the `2.0.0b6` publish. diff --git a/README.md b/README.md index 047d97e..58775df 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@
+ -
-

- Structural code quality analysis for Python -

+

Structural code quality analysis for Python

+ +

+ PyPI + Status + Downloads + Tests + Benchmark + Python + codeclone 90 (A) + License +

+ +

+ VS Code + VS Code Installs + Discussions +

-

- PyPI - Downloads - Tests - Benchmark - Python - codeclone 90 (A) - License -

+ --- @@ -42,8 +49,8 @@ Live sample report: [orenlab.github.io/codeclone/examples/report/](https://orenlab.github.io/codeclone/examples/report/) > [!NOTE] -> This README and docs site track the in-development `v2.0.x` line from `main`. -> For the latest stable CodeClone documentation (`v1.4.4`), see the +> This README and docs site document the CodeClone `2.0` release line. +> For the previous `1.4.x` line, see the > [`v1.4.4` README](https://github.com/orenlab/codeclone/blob/v1.4.4/README.md) > and the > [`v1.4.4` docs tree](https://github.com/orenlab/codeclone/tree/v1.4.4/docs). @@ -66,7 +73,7 @@ Live sample report: ## Quick Start ```bash -uv tool install codeclone # use --pre for beta +uv tool install codeclone codeclone . # analyze codeclone . --html # HTML report @@ -112,17 +119,22 @@ codeclone . --ci
What --ci enables -The --ci preset equals --fail-on-new --no-color --quiet. + +The `--ci` preset equals `--fail-on-new --no-color --quiet`. When a trusted metrics baseline is loaded, CI mode also enables ---fail-on-new-metrics. +`--fail-on-new-metrics`.
+> [!TIP] +> Run `codeclone . --update-baseline` once after install to establish your CI reference point. +> Commit the baseline file — it becomes the contract CI enforces on every push. + ### GitHub Action CodeClone also ships a composite GitHub Action for PR and CI workflows: ```yaml -- uses: orenlab/codeclone/.github/actions/codeclone@main +- uses: orenlab/codeclone/.github/actions/codeclone@v2 with: fail-on-new: "true" sarif: "true" @@ -185,9 +197,9 @@ Triage-first MCP server for AI agents and IDE clients, built on the same canonic contract: never mutates source, baselines, or repo state. ```bash -uv tool install --pre "codeclone[mcp]" +uv tool install "codeclone[mcp]" # or -uv pip install --pre "codeclone[mcp]" +uv pip install "codeclone[mcp]" # local stdio clients codeclone-mcp --transport stdio @@ -196,6 +208,11 @@ codeclone-mcp --transport stdio codeclone-mcp --transport streamable-http ``` +> [!WARNING] +> Analysis tools require an absolute repository root. Relative roots such as `.` are rejected. +> Keep `stdio` as the default transport for local IDE and agent clients; HTTP exposure beyond +> loopback requires explicit `--allow-remote`. + [MCP usage guide](https://orenlab.github.io/codeclone/mcp/) · [MCP interface contract](https://orenlab.github.io/codeclone/book/20-mcp-interface/) @@ -289,7 +306,7 @@ Report contract: [Report contract](https://orenlab.github.io/codeclone/book/08-r { "report_schema_version": "2.10", "meta": { - "codeclone_version": "2.0.0b6", + "codeclone_version": "2.0.0", "project_name": "...", "scan_root": ".", "report_mode": "full", @@ -301,82 +318,41 @@ Report contract: [Report contract](https://orenlab.github.io/codeclone/book/08-r "segment_min_loc": 20, "segment_min_stmt": 10 }, - "analysis_thresholds": { - "design_findings": { - "...": "..." - } - }, - "baseline": { - "...": "..." - }, - "cache": { - "...": "..." - }, - "metrics_baseline": { - "...": "..." - }, + "analysis_thresholds": { "design_findings": { "...": "..." } }, + "baseline": { "...": "..." }, + "cache": { "...": "..." }, + "metrics_baseline": { "...": "..." }, "runtime": { "analysis_started_at_utc": "...", "report_generated_at_utc": "..." } }, "inventory": { - "files": { - "...": "..." - }, - "code": { - "...": "..." - }, - "file_registry": { - "encoding": "relative_path", - "items": [] - } + "files": { "...": "..." }, + "code": { "...": "..." }, + "file_registry": { "encoding": "relative_path", "items": [] } }, "findings": { - "summary": { - "...": "..." - }, + "summary": { "...": "..." }, "groups": { - "clones": { - "functions": [], - "blocks": [], - "segments": [] - }, - "structural": { - "groups": [] - }, - "dead_code": { - "groups": [] - }, - "design": { - "groups": [] - } + "clones": { "functions": [], "blocks": [], "segments": [] }, + "structural": { "groups": [] }, + "dead_code": { "groups": [] }, + "design": { "groups": [] } } }, "metrics": { "summary": { "...": "...", - "coverage_adoption": { - "...": "..." - }, - "coverage_join": { - "...": "..." - }, - "api_surface": { - "...": "..." - } + "coverage_adoption": { "...": "..." }, + "coverage_join": { "...": "..." }, + "api_surface": { "...": "..." } }, "families": { "...": "...", - "coverage_adoption": { - "...": "..." - }, - "coverage_join": { - "...": "..." - }, - "api_surface": { - "...": "..." - } + "coverage_adoption": { "...": "..." }, + "coverage_join": { "...": "..." }, + "api_surface": { "...": "..." } } }, "derived": { @@ -396,15 +372,8 @@ Report contract: [Report contract](https://orenlab.github.io/codeclone/book/08-r } }, "integrity": { - "canonicalization": { - "version": "1", - "scope": "canonical_only" - }, - "digest": { - "algorithm": "sha256", - "verified": true, - "value": "..." - } + "canonicalization": { "version": "1", "scope": "canonical_only" }, + "digest": { "algorithm": "sha256", "verified": true, "value": "..." } } } ``` @@ -434,13 +403,39 @@ Suppression contract: ## How It Works -1. **Parse** — Python source to AST -2. **Normalize** — canonical structure (robust to renaming, formatting) -3. **CFG** — per-function control flow graph -4. **Fingerprint** — stable hash computation -5. **Group** — function, block, and segment clone groups -6. **Metrics** — complexity, coupling, cohesion, dependencies, dead code, health -7. **Gate** — baseline comparison, threshold checks +
+Pipeline overview + +``` +Python source + │ + ▼ + Parse ──────── AST per file + │ + ▼ + Normalize ───── canonical structure (rename/format-resistant) + │ + ▼ + CFG ─────────── per-function control flow graph + │ + ▼ + Fingerprint ──── stable hash per function / block / segment + │ + ▼ + Group ────────── clone groups + structural findings + │ + ▼ + Metrics ─────── complexity · coupling · cohesion · dependencies + dead code · adoption · security surfaces · health + │ + ▼ + Gate ────────── baseline diff · threshold checks · CI exit codes + │ + ▼ + Report ─────── HTML · JSON · Markdown · SARIF · text +``` + +
Architecture: [Architecture narrative](https://orenlab.github.io/codeclone/architecture/) · CFG semantics: [CFG semantics](https://orenlab.github.io/codeclone/cfg/) @@ -491,5 +486,6 @@ Versions released before this change remain under their original license terms. - **Docs:** - **Issues:** +- **Discussions:** - **PyPI:** - **Licenses:** [MPL-2.0](https://github.com/orenlab/codeclone/blob/main/LICENSE) · [MIT docs](https://github.com/orenlab/codeclone/blob/main/LICENSE-MIT) · [Scope map](https://github.com/orenlab/codeclone/blob/main/LICENSES.md) diff --git a/codeclone/report/html/widgets/icons.py b/codeclone/report/html/widgets/icons.py index 87b68c2..12dde5c 100644 --- a/codeclone/report/html/widgets/icons.py +++ b/codeclone/report/html/widgets/icons.py @@ -27,15 +27,15 @@ def _svg_with_class(size: int, sw: str, body: str, *, class_name: str = "") -> s BRAND_LOGO = ( - '

+ + + + CodeClone + +

+ +

+ Structural code quality analysis for Python +

+ +

+ PyPI + Tests + Benchmark + Python +

+ +CodeClone provides deterministic structural code quality analysis for Python. +It detects architectural duplication, computes quality metrics, and enforces +CI gates with baseline-aware governance: known technical debt stays accepted, +new regressions stay visible. + +The same analysis pipeline powers CLI reports, CI checks, the MCP server, and +native IDE/agent clients. + +- Documentation: +- Live sample report: +- Source: +- Issues: + +## Features + +- Clone detection: function, block, and report-only segment clones. +- Structural findings: duplicated branch families, clone guard/exit divergence, + and clone-cohort drift. +- Quality metrics: complexity, coupling, cohesion, dependency cycles, adaptive + dependency depth, dead code, health score, and overloaded-module profiling. +- Coverage Join: combines Cobertura XML with CodeClone units to surface + coverage hotspots and scope gaps. +- Security Surfaces: report-only inventory of security-relevant boundaries and + sensitive capabilities. It does not claim vulnerabilities. +- Baseline governance: separates accepted legacy debt from new regressions. +- Reports: HTML, JSON, Markdown, SARIF, and text from one report payload. +- MCP control surface: read-only agent/IDE interface over the same pipeline. +- Native clients: VS Code extension, Claude Desktop bundle, and Codex plugin. + +## Quick Start + +```bash +uv tool install codeclone + +codeclone . # analyze +codeclone . --html # write HTML report +codeclone . --html --open-html-report +codeclone . --json --md --sarif --text +codeclone . --ci # CI mode +``` + +Run without installing: + +```bash +uvx codeclone@latest . +``` + +## CI Workflow + +```bash +# 1. Generate and commit the baseline +codeclone . --update-baseline + +# 2. Enforce it in CI +codeclone . --ci +``` + +`--ci` enables baseline-aware gating and exits with deterministic status codes: + +| Code | Meaning | +|------|---------| +| `0` | Success | +| `2` | Contract error, such as an untrusted baseline or invalid config | +| `3` | Gating failure, such as new clones or failed metric thresholds | +| `5` | Internal error | + +## Reports + +```bash +codeclone . --html +codeclone . --json +codeclone . --md +codeclone . --sarif +codeclone . --text +``` + +All report formats are rendered from the same deterministic report payload. +The HTML report is intended for human review; JSON, SARIF, Markdown, and text +are intended for automation and CI surfaces. + +Report contract: + + +## MCP and Native Clients + +Install the optional MCP runtime when you want CodeClone in AI agents or IDEs: + +```bash +uv tool install "codeclone[mcp]" + +codeclone-mcp --transport stdio +``` + +The MCP server is read-only by contract. It does not mutate source files, +baselines, cache, or repository state. + +Client surfaces: + +| Surface | Link | +|---------|------| +| VS Code extension | | +| Claude Desktop bundle | | +| Codex plugin | | + +MCP docs: + + +## Configuration + +CodeClone reads project configuration from `pyproject.toml`: + +```toml +[tool.codeclone] +baseline = "codeclone.baseline.json" +min_loc = 10 +min_stmt = 6 +block_min_loc = 20 +block_min_stmt = 8 +fail_on_new = true +fail_cycles = true +fail_dead_code = true +fail_health = 80 +``` + +Precedence is deterministic: + +```text +CLI flags > pyproject.toml > built-in defaults +``` + +Config reference: + + +## License + +- Code: MPL-2.0 (`LICENSE`) +- Documentation and docs-site content: MIT (`LICENSE-MIT`) + +License scope map: + diff --git a/docs/README.md b/docs/README.md index 47fc996..7e56b44 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,8 +4,8 @@ This site is built with MkDocs and published to [orenlab.github.io/codeclone](https://orenlab.github.io/codeclone/). !!! note "Version Notice" - This site currently documents the in-development `v2.0.x` line from `main`. - For the latest stable CodeClone documentation (`v1.4.4`), see the + This site documents the CodeClone `2.0` release line. + For the previous `1.4.x` line, see the [`v1.4.4` README](https://github.com/orenlab/codeclone/blob/v1.4.4/README.md) and the [`v1.4.4` docs tree](https://github.com/orenlab/codeclone/tree/v1.4.4/docs). diff --git a/docs/assets/codeclone-wordmark-dark.svg b/docs/assets/codeclone-wordmark-dark.svg index 6716ada..10ef92d 100644 --- a/docs/assets/codeclone-wordmark-dark.svg +++ b/docs/assets/codeclone-wordmark-dark.svg @@ -1,17 +1,12 @@ - - - - - - - CodeClone + + + + + + Code + + Clone diff --git a/docs/assets/codeclone-wordmark.svg b/docs/assets/codeclone-wordmark.svg index d2f8c96..5c45a96 100644 --- a/docs/assets/codeclone-wordmark.svg +++ b/docs/assets/codeclone-wordmark.svg @@ -1,17 +1,12 @@ - - - - - - - CodeClone + + + + + + Code + + Clone diff --git a/docs/book/04-config-and-defaults.md b/docs/book/04-config-and-defaults.md index 9365533..6197453 100644 --- a/docs/book/04-config-and-defaults.md +++ b/docs/book/04-config-and-defaults.md @@ -187,7 +187,7 @@ Dependency depth config note: CLI or `pyproject.toml` option. - Dependency depth now uses an internal adaptive profile based on `avg_depth`, `p95_depth`, and `max_depth` for the internal module graph. -- There is no user-facing knob to tune that model in `2.0.0b6`. +- There is no user-facing knob to tune that model in `2.0.0`. Metrics baseline path selection contract: diff --git a/docs/book/08-report.md b/docs/book/08-report.md index 82b1cf5..814dc22 100644 --- a/docs/book/08-report.md +++ b/docs/book/08-report.md @@ -2,7 +2,7 @@ ## Purpose -Define the canonical report contract in `2.0.0b6`: report schema `2.10` plus +Define the canonical report contract in `2.0.0`: report schema `2.10` plus deterministic text/Markdown/SARIF/HTML projections. ## Public surface diff --git a/docs/book/20-mcp-interface.md b/docs/book/20-mcp-interface.md index a1775a6..a54070b 100644 --- a/docs/book/20-mcp-interface.md +++ b/docs/book/20-mcp-interface.md @@ -2,15 +2,15 @@ ## Purpose -Define the current public MCP surface in the `2.0` beta line. +Define the current public MCP surface in the CodeClone `2.0` release line. The MCP layer is optional, read-only, and built on the same canonical pipeline/report contracts as the CLI. It does not create a second analysis engine or a second persistence model. !!! note "Read-only integration contract" -MCP surfaces the same canonical report and run state as the CLI and HTML -report. It must not mutate source, baseline, cache, or report artifacts. + MCP surfaces the same canonical report and run state as the CLI and HTML + report. It must not mutate source, baseline, cache, or report artifacts. ## Public surface @@ -45,9 +45,9 @@ Current server characteristics: or `off` !!! warning "Absolute roots and remote exposure" -Analysis tools require an absolute repository root, and HTTP exposure -beyond loopback is intentionally explicit. Keep `stdio` as the default for -local IDE and agent clients. + Analysis tools require an absolute repository root, and HTTP exposure + beyond loopback is intentionally explicit. Keep `stdio` as the default for + local IDE and agent clients. ## Tools @@ -66,7 +66,7 @@ second, then drill into one finding or one hotspot family. | `get_production_triage` | `run_id`, `max_hotspots`, `max_suggestions` | Production-first first-pass view over one stored run. | | `help` | `topic`, `detail` | Bounded workflow/contract guidance for supported MCP topics. | | `compare_runs` | `run_id_before`, `run_id_after`, `focus` | Run-to-run delta view over findings and health; returns `incomparable` when roots/settings differ. | -| `evaluate_gates` | `run_id`, gate flags, threshold overrides, `coverage_min` | Preview CI/gating decisions against a stored run without mutating process or repo state. | +| `evaluate_gates` | `run_id`, gate flags, threshold overrides, `coverage_min` | Evaluate CI/gating decisions against a stored run without mutating process or repo state. | ### Report and finding projection tools diff --git a/docs/book/21-vscode-extension.md b/docs/book/21-vscode-extension.md index 14b2c5c..44d72f2 100644 --- a/docs/book/21-vscode-extension.md +++ b/docs/book/21-vscode-extension.md @@ -135,7 +135,7 @@ The extension runs as a workspace extension and requires: - local filesystem access - local git access for changed-files review - a local `codeclone-mcp` launcher, or an explicitly configured launcher -- CodeClone `2.0.0b4` or newer +- CodeClone `2.0.0` or newer In `auto` mode, launcher resolution prefers the current workspace virtualenv before `PATH`. Runtime and version-mismatch messages identify that resolved launcher source. @@ -181,7 +181,7 @@ For this reason: ## Non-guarantees -- Exact view grouping and copy may evolve between beta releases. +- Exact view grouping and copy may evolve between extension releases. - Internal client-side caching and view-model shaping may evolve as long as the extension remains faithful to MCP and canonical report semantics. - Explorer decoration styling, review-loop polish, and other non-contract UI diff --git a/docs/book/appendix/b-schema-layouts.md b/docs/book/appendix/b-schema-layouts.md index 7595832..155e594 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.0b6`. +Compact structural layouts for baseline/cache/report contracts in `2.0.0`. ## Baseline schema (`2.1`) ```json { "meta": { - "generator": { "name": "codeclone", "version": "2.0.0b6" }, + "generator": { "name": "codeclone", "version": "2.0.0" }, "schema_version": "2.1", "fingerprint_version": "1", "python_tag": "cp314", @@ -60,7 +60,7 @@ Notes: ```json { "meta": { - "generator": { "name": "codeclone", "version": "2.0.0b6" }, + "generator": { "name": "codeclone", "version": "2.0.0" }, "schema_version": "1.2", "python_tag": "cp314", "created_at": "2026-03-11T00:00:00Z", @@ -153,7 +153,7 @@ Notes: { "report_schema_version": "2.10", "meta": { - "codeclone_version": "2.0.0b6", + "codeclone_version": "2.0.0", "project_name": "codeclone", "scan_root": ".", "analysis_mode": "full", @@ -503,7 +503,7 @@ Notes: "tool": { "driver": { "name": "codeclone", - "version": "2.0.0b6", + "version": "2.0.0", "rules": [ { "id": "CCLONE001", diff --git a/docs/claude-desktop-bundle.md b/docs/claude-desktop-bundle.md index c742616..0887363 100644 --- a/docs/claude-desktop-bundle.md +++ b/docs/claude-desktop-bundle.md @@ -23,14 +23,14 @@ The bundle prefers the current workspace launcher first: ```bash uv venv -uv pip install --python .venv/bin/python --pre "codeclone[mcp]" +uv pip install --python .venv/bin/python "codeclone[mcp]" .venv/bin/codeclone-mcp --help ``` Global fallback: ```bash -uv tool install --pre "codeclone[mcp]" +uv tool install "codeclone[mcp]" codeclone-mcp --help ``` diff --git a/docs/codex-plugin.md b/docs/codex-plugin.md index 6f6d5e8..0150418 100644 --- a/docs/codex-plugin.md +++ b/docs/codex-plugin.md @@ -18,14 +18,14 @@ Repo-local discovery via `.agents/plugins/marketplace.json`. ```bash uv venv -uv pip install --python .venv/bin/python --pre "codeclone[mcp]" +uv pip install --python .venv/bin/python "codeclone[mcp]" .venv/bin/codeclone-mcp --help ``` Global fallback: ```bash -uv tool install --pre "codeclone[mcp]" +uv tool install "codeclone[mcp]" codeclone-mcp --help ``` diff --git a/docs/mcp.md b/docs/mcp.md index 06a76b8..bdeda02 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -17,13 +17,13 @@ Works with any MCP-capable client regardless of backend model. === "Standalone tool" ```bash title="Install the MCP launcher as a standalone tool" - uv tool install --pre "codeclone[mcp]" + uv tool install "codeclone[mcp]" ``` === "Existing environment" ```bash title="Install the MCP extra into the current environment" - uv pip install --pre "codeclone[mcp]" + uv pip install "codeclone[mcp]" ``` ## Quick client setup @@ -103,7 +103,7 @@ 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 `b6` MCP surface: `21` tools, `7` fixed resources, and `3` +Current CodeClone `2.0` MCP surface: `21` tools, `7` fixed resources, and `3` run-scoped URI templates. ## Tool surface @@ -121,7 +121,7 @@ run-scoped URI templates. | `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 | +| `evaluate_gates` | Evaluate CI gating decisions | | `check_clones` | Clone findings only; narrower than `list_findings` | | `check_complexity` | Complexity hotspots only | | `check_coupling` | Coupling hotspots only | @@ -344,13 +344,13 @@ If `codeclone-mcp` is not on `PATH`, use an absolute path to the launcher. ## Troubleshooting -| Problem | Fix | -|-----------------------------------------------------------|-------------------------------------------------------------------------------------| -| `CodeClone MCP support requires the optional 'mcp' extra` | `uv tool install --pre "codeclone[mcp]"` or `uv pip install --pre "codeclone[mcp]"` | -| Client cannot find `codeclone-mcp` | `uv tool install --pre "codeclone[mcp]"` or use an absolute launcher path | -| Client only accepts remote MCP | Use `streamable-http` transport | -| Agent reads stale results | Call `analyze_repository` again; `latest` always points to the most recent run | -| `changed_paths` rejected | Pass a `list[str]` of repo-relative paths, not a comma-separated string | +| Problem | Fix | +|-----------------------------------------------------------|--------------------------------------------------------------------------------| +| `CodeClone MCP support requires the optional 'mcp' extra` | `uv tool install "codeclone[mcp]"` or `uv pip install "codeclone[mcp]"` | +| Client cannot find `codeclone-mcp` | `uv tool install "codeclone[mcp]"` or use an absolute launcher path | +| Client only accepts remote MCP | Use `streamable-http` transport | +| Agent reads stale results | Call `analyze_repository` again; `latest` always points to the most recent run | +| `changed_paths` rejected | Pass a `list[str]` of repo-relative paths, not a comma-separated string | ## See also diff --git a/docs/terms-of-use.md b/docs/terms-of-use.md index 82604b4..34b08b1 100644 --- a/docs/terms-of-use.md +++ b/docs/terms-of-use.md @@ -39,7 +39,7 @@ you build and secure that deployment separately. ## Support and updates -CodeClone integrations may evolve during the `2.0.x` beta line. Published docs, +CodeClone integrations may evolve during the `2.x` release line. Published docs, tests, and changelog entries define the intended contract surface for each release. diff --git a/docs/vscode-extension.md b/docs/vscode-extension.md index d488d7a..edcd050 100644 --- a/docs/vscode-extension.md +++ b/docs/vscode-extension.md @@ -1,7 +1,6 @@ # VS Code Extension -CodeClone ships a preview VS Code extension in -`extensions/vscode-codeclone/`. +CodeClone ships a stable VS Code extension in `extensions/vscode-codeclone/`. It is a native IDE surface over `codeclone-mcp` and is designed for baseline-aware, triage-first structural review inside the editor. @@ -29,21 +28,21 @@ It does not create a second truth model and it does not mutate the repository. The extension needs a local `codeclone-mcp` launcher. -Minimum supported CodeClone version: `2.0.0b4`. +Minimum supported CodeClone version: `2.0.0`. In `auto` mode, it checks the current workspace virtualenv before falling back to `PATH`. Runtime and version-mismatch messages identify that resolved launcher source. -Recommended install for the preview extension: +Recommended install: ```bash -uv tool install --pre "codeclone[mcp]" +uv tool install "codeclone[mcp]" ``` If you want the launcher inside the current environment instead: ```bash -uv pip install --pre "codeclone[mcp]" +uv pip install "codeclone[mcp]" ``` Verify the launcher: diff --git a/extensions/claude-desktop-codeclone/README.md b/extensions/claude-desktop-codeclone/README.md index 37ee769..83da9e9 100644 --- a/extensions/claude-desktop-codeclone/README.md +++ b/extensions/claude-desktop-codeclone/README.md @@ -20,14 +20,14 @@ Recommended workspace-local setup: ```bash uv venv -uv pip install --python .venv/bin/python --pre "codeclone[mcp]" +uv pip install --python .venv/bin/python "codeclone[mcp]" .venv/bin/codeclone-mcp --help ``` Global fallback: ```bash -uv tool install --pre "codeclone[mcp]" +uv tool install "codeclone[mcp]" codeclone-mcp --help ``` diff --git a/extensions/claude-desktop-codeclone/manifest.json b/extensions/claude-desktop-codeclone/manifest.json index ccdf403..ae51e65 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-b6.0", + "version": "2.0.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": { @@ -64,7 +64,7 @@ }, { "name": "evaluate_gates", - "description": "Preview CI gating decisions for the current run." + "description": "Evaluate CI gating decisions for the current run." }, { "name": "generate_pr_summary", diff --git a/extensions/claude-desktop-codeclone/media/icon.png b/extensions/claude-desktop-codeclone/media/icon.png index b8bf9fdb445d927f98f6955c78b5263dc70f6716..31388ba302274bfebc1e05666adc73d42c3227b9 100644 GIT binary patch literal 2079 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 VhMdH{!4~h(u;7TG+n=y<{sW6`1sebW diff --git a/extensions/claude-desktop-codeclone/package-lock.json b/extensions/claude-desktop-codeclone/package-lock.json index 0e6d72b..4056196 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-b6.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@orenlab/codeclone-claude-desktop", - "version": "2.0.0-b6.0", + "version": "2.0.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 5abfc93..dd3847c 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-b6.0", + "version": "2.0.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 4afa1af..ed9708d 100644 --- a/extensions/vscode-codeclone/CHANGELOG.md +++ b/extensions/vscode-codeclone/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## 0.2.6 + +- align setup guidance with the stable CodeClone `2.0.0` MCP package +- require CodeClone `2.0.0` or newer for the final 2.0 release line + ## 0.2.5 - pin the packaging toolchain to `@vscode/vsce@2.25.0` to remove the vulnerable transitive `uuid<14` chain from the diff --git a/extensions/vscode-codeclone/README.md b/extensions/vscode-codeclone/README.md index d585d95..4774cda 100644 --- a/extensions/vscode-codeclone/README.md +++ b/extensions/vscode-codeclone/README.md @@ -9,8 +9,6 @@ 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 for the current `2.0.x` beta line. - ## What it is for CodeClone inside VS Code is designed for: @@ -42,21 +40,21 @@ report inside the sidebar. CodeClone for VS Code needs a local `codeclone-mcp` launcher. -Minimum supported CodeClone version: `2.0.0b4`. +Minimum supported CodeClone version: `2.0.0`. In `auto` mode, the extension checks the current workspace virtualenv before falling back to `PATH`. Runtime and version-mismatch messages identify that resolved launcher source. -Recommended install for the preview extension: +Recommended install: ```bash -uv tool install --pre "codeclone[mcp]" +uv tool install "codeclone[mcp]" ``` If you want the launcher inside the current environment instead: ```bash -uv pip install --pre "codeclone[mcp]" +uv pip install "codeclone[mcp]" ``` Verify the launcher: diff --git a/extensions/vscode-codeclone/media/icon-source.svg b/extensions/vscode-codeclone/media/icon-source.svg index dd4d4d2..a813208 100644 --- a/extensions/vscode-codeclone/media/icon-source.svg +++ b/extensions/vscode-codeclone/media/icon-source.svg @@ -1,34 +1,17 @@ - - - + + + diff --git a/extensions/vscode-codeclone/media/icon.png b/extensions/vscode-codeclone/media/icon.png index b8bf9fdb445d927f98f6955c78b5263dc70f6716..31388ba302274bfebc1e05666adc73d42c3227b9 100644 GIT binary patch literal 2079 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 VhMdH{!4~h(u;7TG+n=y<{sW6`1sebW diff --git a/extensions/vscode-codeclone/package-lock.json b/extensions/vscode-codeclone/package-lock.json index 87a0aa7..cbc2d73 100644 --- a/extensions/vscode-codeclone/package-lock.json +++ b/extensions/vscode-codeclone/package-lock.json @@ -1,12 +1,12 @@ { "name": "codeclone", - "version": "0.2.5", + "version": "0.2.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codeclone", - "version": "0.2.5", + "version": "0.2.6", "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 8fe0aaa..722efdc 100644 --- a/extensions/vscode-codeclone/package.json +++ b/extensions/vscode-codeclone/package.json @@ -2,8 +2,7 @@ "name": "codeclone", "displayName": "CodeClone", "description": "Baseline-aware, triage-first structural review for Python, powered by CodeClone MCP.", - "version": "0.2.5", - "preview": true, + "version": "0.2.6", "publisher": "orenlab", "license": "MPL-2.0", "repository": { diff --git a/extensions/vscode-codeclone/src/constants.js b/extensions/vscode-codeclone/src/constants.js index 675e033..749d524 100644 --- a/extensions/vscode-codeclone/src/constants.js +++ b/extensions/vscode-codeclone/src/constants.js @@ -11,7 +11,7 @@ const HELP_TOPICS = [ ]; const OPTIONAL_HELP_TOPICS = [ - {topic: "coverage", minimumVersion: "2.0.0b5"}, + {topic: "coverage", minimumVersion: "2.0.0"}, ]; const KNOWN_HELP_TOPICS = [ diff --git a/extensions/vscode-codeclone/src/renderers.js b/extensions/vscode-codeclone/src/renderers.js index 1ff42a2..e3aeb08 100644 --- a/extensions/vscode-codeclone/src/renderers.js +++ b/extensions/vscode-codeclone/src/renderers.js @@ -69,7 +69,7 @@ function renderSetupMarkdown() { "", `Minimum supported CodeClone version: \`${MINIMUM_SUPPORTED_CODECLONE_VERSION}\``, "", - "## Recommended install for the preview extension", + "## Recommended install", "", "```bash", PREVIEW_INSTALL_COMMAND, diff --git a/extensions/vscode-codeclone/src/support.js b/extensions/vscode-codeclone/src/support.js index f7117d3..04a9468 100644 --- a/extensions/vscode-codeclone/src/support.js +++ b/extensions/vscode-codeclone/src/support.js @@ -7,9 +7,8 @@ const STALE_REASON_WORKSPACE = "workspace changed after this run"; const ANALYSIS_PROFILE_DEFAULTS = "defaults"; const ANALYSIS_PROFILE_DEEPER_REVIEW = "deeperReview"; const ANALYSIS_PROFILE_CUSTOM = "custom"; -const MINIMUM_SUPPORTED_CODECLONE_VERSION = "2.0.0b4"; -const PREVIEW_INSTALL_COMMAND = - 'uv tool install --pre "codeclone[mcp]"'; +const MINIMUM_SUPPORTED_CODECLONE_VERSION = "2.0.0"; +const PREVIEW_INSTALL_COMMAND = 'uv tool install "codeclone[mcp]"'; const ANALYSIS_PROFILE_IDS = new Set([ ANALYSIS_PROFILE_DEFAULTS, ANALYSIS_PROFILE_DEEPER_REVIEW, diff --git a/extensions/vscode-codeclone/test/runArtifacts.test.js b/extensions/vscode-codeclone/test/runArtifacts.test.js index b5ab482..21c6e97 100644 --- a/extensions/vscode-codeclone/test/runArtifacts.test.js +++ b/extensions/vscode-codeclone/test/runArtifacts.test.js @@ -51,14 +51,14 @@ test("loadRunArtifacts starts MCP reads and git snapshot together", async () => assert.ok(resolveTriage); assert.ok(resolveMetrics); assert.ok(resolveReviewed); - resolveSummary({version: "2.0.0b6"}); + resolveSummary({version: "2.0.0"}); resolveTriage({hotspots: []}); resolveMetrics({summary: {health: {score: 90}}}); resolveReviewed({items: [{id: "f1"}]}); resolveGitSnapshot({head: "abc123"}); assert.deepEqual(await promise, { - summary: {version: "2.0.0b6"}, + summary: {version: "2.0.0"}, triage: {hotspots: []}, metricsSummary: {health: {score: 90}}, reviewedItems: [{id: "f1"}], diff --git a/extensions/vscode-codeclone/test/support.test.js b/extensions/vscode-codeclone/test/support.test.js index 5ca4f77..e3e5e69 100644 --- a/extensions/vscode-codeclone/test/support.test.js +++ b/extensions/vscode-codeclone/test/support.test.js @@ -146,13 +146,13 @@ test("launchSpecOrigin makes launcher provenance explicit", () => { test("unsupportedVersionMessage includes launcher provenance and next step", () => { assert.equal( - unsupportedVersionMessage("1.27.0", "2.0.0b4", { + unsupportedVersionMessage("1.27.0", "2.0.0", { command: "/workspace/repo/.venv/bin/codeclone-mcp", args: [], cwd: "/workspace/repo", source: "workspaceLocal", }), - "The local CodeClone MCP server is not supported. It reported version 1.27.0; this extension requires CodeClone >= 2.0.0b4. The extension resolved workspace-local launcher (/workspace/repo/.venv/bin/codeclone-mcp). Update that environment or set codeclone.mcp.command to a newer launcher." + "The local CodeClone MCP server is not supported. It reported version 1.27.0; this extension requires CodeClone >= 2.0.0. The extension resolved workspace-local launcher (/workspace/repo/.venv/bin/codeclone-mcp). Update that environment or set codeclone.mcp.command to a newer launcher." ); }); @@ -323,13 +323,10 @@ test("compareCodeCloneVersions keeps beta, rc, and final ordering", () => { }); test("minimum supported CodeClone version and install command stay aligned", () => { - assert.equal(MINIMUM_SUPPORTED_CODECLONE_VERSION, "2.0.0b4"); - assert.equal(isMinimumSupportedCodeCloneVersion("2.0.0b4"), true); + assert.equal(MINIMUM_SUPPORTED_CODECLONE_VERSION, "2.0.0"); + assert.equal(isMinimumSupportedCodeCloneVersion("2.0.0"), true); assert.equal(isMinimumSupportedCodeCloneVersion("2.0.1"), true); - assert.equal(isMinimumSupportedCodeCloneVersion("2.0.0b3"), false); + assert.equal(isMinimumSupportedCodeCloneVersion("2.0.0rc2"), false); assert.equal(isMinimumSupportedCodeCloneVersion("1.27.0"), false); - assert.equal( - PREVIEW_INSTALL_COMMAND, - 'uv tool install --pre "codeclone[mcp]"' - ); + assert.equal(PREVIEW_INSTALL_COMMAND, 'uv tool install "codeclone[mcp]"'); }); diff --git a/mkdocs.yml b/mkdocs.yml index a941452..7f77407 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,6 +6,8 @@ repo_name: orenlab/codeclone docs_dir: docs edit_uri: blob/main/docs/ strict: true +exclude_docs: | + README-pypi.md theme: name: material diff --git a/plugins/codeclone/.codex-plugin/plugin.json b/plugins/codeclone/.codex-plugin/plugin.json index 2f737fa..0d19993 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-b6.0", + "version": "2.0.0", "description": "Baseline-aware structural code quality analysis for Codex through the local CodeClone MCP server.", "author": { "name": "Den Rozhnovskiy", @@ -41,7 +41,7 @@ "Run a changed-files CodeClone review for my current diff.", "Check CodeClone health and explain what to fix first." ], - "brandColor": "#58A6FF", + "brandColor": "#6366f1", "composerIcon": "./assets/icon.png", "logo": "./assets/logo.png" } diff --git a/plugins/codeclone/README.md b/plugins/codeclone/README.md index ba96331..aa03b9c 100644 --- a/plugins/codeclone/README.md +++ b/plugins/codeclone/README.md @@ -36,7 +36,7 @@ Recommended workspace-local setup: ```bash uv venv -uv pip install --python .venv/bin/python --pre "codeclone[mcp]" +uv pip install --python .venv/bin/python "codeclone[mcp]" .venv/bin/codeclone-mcp --help ``` @@ -45,7 +45,7 @@ If your workspace uses Poetry, install CodeClone into that Poetry environment. Global fallback: ```bash -uv tool install --pre "codeclone[mcp]" +uv tool install "codeclone[mcp]" codeclone-mcp --help ``` diff --git a/plugins/codeclone/assets/icon.png b/plugins/codeclone/assets/icon.png index b8bf9fdb445d927f98f6955c78b5263dc70f6716..31388ba302274bfebc1e05666adc73d42c3227b9 100644 GIT binary patch literal 2079 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 VhMdH{!4~h(u;7TG+n=y<{sW6`1sebW diff --git a/plugins/codeclone/assets/logo.png b/plugins/codeclone/assets/logo.png index b8bf9fdb445d927f98f6955c78b5263dc70f6716..31388ba302274bfebc1e05666adc73d42c3227b9 100644 GIT binary patch literal 2079 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 VhMdH{!4~h(u;7TG+n=y<{sW6`1sebW diff --git a/pyproject.toml b/pyproject.toml index a8b11a2..eff1f0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,9 +4,9 @@ build-backend = "setuptools.build_meta" [project] name = "codeclone" -version = "2.0.0b7" +version = "2.0.0" description = "Structural code quality analysis for Python" -readme = { file = "README.md", content-type = "text/markdown" } +readme = { file = "docs/README-pypi.md", content-type = "text/markdown" } license = "MPL-2.0 AND MIT" license-files = ["LICENSE", "LICENSE-MIT"] @@ -39,7 +39,7 @@ keywords = [ ] classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Topic :: Software Development :: Quality Assurance", "Topic :: Software Development :: Testing", diff --git a/tests/test_codex_plugin.py b/tests/test_codex_plugin.py index a525cee..ebd5a31 100644 --- a/tests/test_codex_plugin.py +++ b/tests/test_codex_plugin.py @@ -17,7 +17,7 @@ def test_codex_plugin_manifest_is_consistent() -> None: assert isinstance(manifest, dict) assert manifest["name"] == plugin_root.name assert manifest["name"] == "codeclone" - assert manifest["version"] == "2.0.0-b6.0" + assert manifest["version"] == "2.0.0" assert manifest["skills"] == "./skills/" assert manifest["mcpServers"] == "./.mcp.json" assert manifest["license"] == "MPL-2.0" @@ -135,7 +135,7 @@ def test_codex_plugin_readme_and_docs_exist() -> None: assert "The plugin prefers a workspace launcher first" in readme_text assert "the current Poetry environment launcher" in readme_text assert "without relying on `sh -lc`" in readme_text - assert 'uv tool install --pre "codeclone[mcp]"' in readme_text + assert 'uv tool install "codeclone[mcp]"' 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_github_action_helpers.py b/tests/test_github_action_helpers.py index 4e34a53..0198265 100644 --- a/tests/test_github_action_helpers.py +++ b/tests/test_github_action_helpers.py @@ -7,6 +7,7 @@ from __future__ import annotations import importlib.util +import re import sys from pathlib import Path from types import ModuleType @@ -100,7 +101,7 @@ def test_render_pr_comment_uses_canonical_report_summary() -> None: action_impl = _load_action_impl() report = { "meta": { - "codeclone_version": "2.0.0b4", + "codeclone_version": "2.0.0", "baseline": {"status": "ok"}, "cache": {"used": True}, }, @@ -123,7 +124,36 @@ def test_render_pr_comment_uses_canonical_report_summary() -> None: "health": { "score": 81, "grade": "B", - } + }, + "complexity": {"max": 20, "high_risk": 0}, + "coupling": {"max": 10, "high_risk": 0}, + "cohesion": {"max": 3, "low_cohesion": 0}, + "dependencies": { + "avg_depth": 4.0, + "p95_depth": 13, + "max_depth": 16, + "cycles": 0, + }, + "dead_code": {"high_confidence": 0, "suppressed": 2}, + "overloaded_modules": {"candidates": 13}, + "coverage_join": { + "status": "ok", + "overall_permille": 994, + "coverage_hotspots": 1, + "scope_gap_hotspots": 2, + }, + "security_surfaces": { + "items": 58, + "category_count": 4, + "production": 28, + }, + "api_surface": { + "enabled": True, + "public_symbols": 2119, + "modules": 208, + "breaking": 0, + "added": 0, + }, } }, } @@ -134,14 +164,19 @@ def test_render_pr_comment_uses_canonical_report_summary() -> None: body, ( "", - "CodeClone Report", + "CodeClone Review", + "Review snapshot", "**81/100 (B)**", - ":x: Failed (gating)", - "Clones: 8 (1 new, 7 known)", - "Structural: 15", - "Dead code: 0", - "Design: 3", - "`2.0.0b4`", + "**:x: Failed (gating)**", + "8 total, 1 new, 7 known", + "CC max 20, CBO max 10, LCOM4 max 3, overloaded 13", + "avg 4.0, p95 13, max 16, cycles 0", + "99.4% overall, 1 hotspots, 2 scope gaps", + "58 surfaces, 4 categories, 28 production", + "2119 symbols, 208 modules", + "CI gates failed; start with rows marked as gating-sensitive.", + "Security Surfaces are report-only capability inventory", + "`2.0.0`", ), ) @@ -156,7 +191,7 @@ def test_resolve_install_target_uses_repo_source_for_local_action_checkout( target = _resolve_install_target( action_path=action_path, workspace=repo_root, - package_version="2.0.0b4", + package_version="2.0.0", ) assert target.source == "repo" @@ -173,9 +208,9 @@ def test_resolve_install_target_uses_pypi_for_remote_checkout(tmp_path: Path) -> pinned = _resolve_install_target( action_path=action_path, workspace=workspace_root, - package_version="2.0.0b4", + package_version="2.0.0", ) - latest = _resolve_install_target( + default = _resolve_install_target( action_path=action_path, workspace=workspace_root, package_version="", @@ -184,11 +219,25 @@ def test_resolve_install_target_uses_pypi_for_remote_checkout(tmp_path: Path) -> assert ( pinned.source, pinned.requirement, - latest.source, - latest.requirement, + default.source, + default.requirement, ) == ( "pypi-version", - "codeclone==2.0.0b4", - "pypi-latest", - "codeclone", + "codeclone==2.0.0", + "pypi-default", + "codeclone==2.0.0", ) + + +def test_action_default_package_version_tracks_release_version() -> None: + action_impl = _load_action_impl() + action_metadata = Path(".github/actions/codeclone/action.yml").read_text( + encoding="utf-8" + ) + pyproject = Path("pyproject.toml").read_text(encoding="utf-8") + version_match = re.search(r'^version = "([^"]+)"$', pyproject, re.MULTILINE) + assert version_match is not None + version = version_match.group(1) + + assert version == action_impl.DEFAULT_CODECLONE_PACKAGE_VERSION + assert f'default: "{version}"' in action_metadata diff --git a/uv.lock b/uv.lock index f352091..92df669 100644 --- a/uv.lock +++ b/uv.lock @@ -282,7 +282,7 @@ wheels = [ [[package]] name = "codeclone" -version = "2.0.0b7" +version = "2.0.0" source = { editable = "." } dependencies = [ { name = "orjson" }, @@ -631,7 +631,7 @@ name = "importlib-metadata" version = "9.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "zipp", marker = "python_full_version < '3.15'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } wheels = [ From d16abcec8cdaf842f73a8c1e91f38569f074ba32 Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 1 May 2026 13:23:00 +0500 Subject: [PATCH 2/7] chore(docs): re-organization of badges in the README --- README.md | 99 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 75 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 58775df..3cb86b2 100644 --- a/README.md +++ b/README.md @@ -22,17 +22,19 @@ PyPI Status Downloads - Tests - Benchmark Python codeclone 90 (A) License

+

+ Tests + Benchmark +

+

VS Code VS Code Installs - Discussions

@@ -318,41 +320,82 @@ Report contract: [Report contract](https://orenlab.github.io/codeclone/book/08-r "segment_min_loc": 20, "segment_min_stmt": 10 }, - "analysis_thresholds": { "design_findings": { "...": "..." } }, - "baseline": { "...": "..." }, - "cache": { "...": "..." }, - "metrics_baseline": { "...": "..." }, + "analysis_thresholds": { + "design_findings": { + "...": "..." + } + }, + "baseline": { + "...": "..." + }, + "cache": { + "...": "..." + }, + "metrics_baseline": { + "...": "..." + }, "runtime": { "analysis_started_at_utc": "...", "report_generated_at_utc": "..." } }, "inventory": { - "files": { "...": "..." }, - "code": { "...": "..." }, - "file_registry": { "encoding": "relative_path", "items": [] } + "files": { + "...": "..." + }, + "code": { + "...": "..." + }, + "file_registry": { + "encoding": "relative_path", + "items": [] + } }, "findings": { - "summary": { "...": "..." }, + "summary": { + "...": "..." + }, "groups": { - "clones": { "functions": [], "blocks": [], "segments": [] }, - "structural": { "groups": [] }, - "dead_code": { "groups": [] }, - "design": { "groups": [] } + "clones": { + "functions": [], + "blocks": [], + "segments": [] + }, + "structural": { + "groups": [] + }, + "dead_code": { + "groups": [] + }, + "design": { + "groups": [] + } } }, "metrics": { "summary": { "...": "...", - "coverage_adoption": { "...": "..." }, - "coverage_join": { "...": "..." }, - "api_surface": { "...": "..." } + "coverage_adoption": { + "...": "..." + }, + "coverage_join": { + "...": "..." + }, + "api_surface": { + "...": "..." + } }, "families": { "...": "...", - "coverage_adoption": { "...": "..." }, - "coverage_join": { "...": "..." }, - "api_surface": { "...": "..." } + "coverage_adoption": { + "...": "..." + }, + "coverage_join": { + "...": "..." + }, + "api_surface": { + "...": "..." + } } }, "derived": { @@ -372,8 +415,15 @@ Report contract: [Report contract](https://orenlab.github.io/codeclone/book/08-r } }, "integrity": { - "canonicalization": { "version": "1", "scope": "canonical_only" }, - "digest": { "algorithm": "sha256", "verified": true, "value": "..." } + "canonicalization": { + "version": "1", + "scope": "canonical_only" + }, + "digest": { + "algorithm": "sha256", + "verified": true, + "value": "..." + } } } ``` @@ -488,4 +538,5 @@ Versions released before this change remain under their original license terms. - **Issues:** - **Discussions:** - **PyPI:** -- **Licenses:** [MPL-2.0](https://github.com/orenlab/codeclone/blob/main/LICENSE) · [MIT docs](https://github.com/orenlab/codeclone/blob/main/LICENSE-MIT) · [Scope map](https://github.com/orenlab/codeclone/blob/main/LICENSES.md) +- **Licenses: + ** [MPL-2.0](https://github.com/orenlab/codeclone/blob/main/LICENSE) · [MIT docs](https://github.com/orenlab/codeclone/blob/main/LICENSE-MIT) · [Scope map](https://github.com/orenlab/codeclone/blob/main/LICENSES.md) From e09ce18eccdeb3b9cebdbd44abc5db90ac685deb Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 1 May 2026 13:37:14 +0500 Subject: [PATCH 3/7] chore(docs): refine CodeClone wordmark assets - adjust light/dark wordmark viewBox and text spacing - simplify SVG text layout for cleaner rendering --- docs/assets/codeclone-wordmark-dark.svg | 10 ++++------ docs/assets/codeclone-wordmark.svg | 10 ++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/docs/assets/codeclone-wordmark-dark.svg b/docs/assets/codeclone-wordmark-dark.svg index 10ef92d..9c8e589 100644 --- a/docs/assets/codeclone-wordmark-dark.svg +++ b/docs/assets/codeclone-wordmark-dark.svg @@ -1,12 +1,10 @@ - + Code - - Clone - + dominant-baseline="central">Code + Clone diff --git a/docs/assets/codeclone-wordmark.svg b/docs/assets/codeclone-wordmark.svg index 5c45a96..98003ec 100644 --- a/docs/assets/codeclone-wordmark.svg +++ b/docs/assets/codeclone-wordmark.svg @@ -1,12 +1,10 @@ - + Code - - Clone - + dominant-baseline="central">Code + Clone From d0c09e9c5a01d3498f91fe2d1e9771a84aaf3c5a Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 1 May 2026 13:40:37 +0500 Subject: [PATCH 4/7] chore(docs): refine CodeClone wordmark assets - adjust light/dark wordmark viewBox and text spacing - simplify SVG text layout for cleaner rendering --- docs/assets/codeclone-wordmark-dark.svg | 2 +- docs/assets/codeclone-wordmark.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/assets/codeclone-wordmark-dark.svg b/docs/assets/codeclone-wordmark-dark.svg index 9c8e589..d50ebc8 100644 --- a/docs/assets/codeclone-wordmark-dark.svg +++ b/docs/assets/codeclone-wordmark-dark.svg @@ -1,4 +1,4 @@ - + diff --git a/docs/assets/codeclone-wordmark.svg b/docs/assets/codeclone-wordmark.svg index 98003ec..a7371f7 100644 --- a/docs/assets/codeclone-wordmark.svg +++ b/docs/assets/codeclone-wordmark.svg @@ -1,4 +1,4 @@ - + From fdd17849efce9845057bf5dce9ec1967bc22dd79 Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 1 May 2026 13:43:05 +0500 Subject: [PATCH 5/7] chore(docs): refine CodeClone wordmark assets - adjust light/dark wordmark viewBox and text spacing - simplify SVG text layout for cleaner rendering --- docs/assets/codeclone-wordmark-dark.svg | 10 +++++----- docs/assets/codeclone-wordmark.svg | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/assets/codeclone-wordmark-dark.svg b/docs/assets/codeclone-wordmark-dark.svg index d50ebc8..0ee0ba8 100644 --- a/docs/assets/codeclone-wordmark-dark.svg +++ b/docs/assets/codeclone-wordmark-dark.svg @@ -1,10 +1,10 @@ - + - Code - Clone + + CodeClone + diff --git a/docs/assets/codeclone-wordmark.svg b/docs/assets/codeclone-wordmark.svg index a7371f7..4d234c1 100644 --- a/docs/assets/codeclone-wordmark.svg +++ b/docs/assets/codeclone-wordmark.svg @@ -1,10 +1,10 @@ - + - Code - Clone + + CodeClone + From f65a1865dc90f9f0c446e3b901cb940d34497693 Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 1 May 2026 13:45:26 +0500 Subject: [PATCH 6/7] chore(docs): re-organization of badges in the README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3cb86b2..97fecf5 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ CodeClone From 6898b8dea2871c216e4d340f1925a39ee0dcd906 Mon Sep 17 00:00:00 2001 From: Den Rozhnovskiy Date: Fri, 1 May 2026 13:50:01 +0500 Subject: [PATCH 7/7] chore(docs): re-organization of badges in the README --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index 97fecf5..4272b1b 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,6 @@ Benchmark

-

- VS Code - VS Code Installs -

- ---