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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
---
@@ -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 = (
- '
+
+
+
+
+
-
@@ -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 @@
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 @@
-
-
-
-
-
---