Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/actions/codeclone/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ source under test. Remote consumers still install from PyPI.
For strict reproducibility, pin the full release tag:

```yaml
- uses: orenlab/codeclone/.github/actions/codeclone@v2.0.0
- uses: orenlab/codeclone/.github/actions/codeclone@v2.0.1
```

For long-lived workflows, `@v2` follows the latest compatible 2.x action
Expand Down Expand Up @@ -80,7 +80,7 @@ jobs:
| Input | Default | Purpose |
|-------------------------|---------------------------------|-------------------------------------------------------------------------------------------------------------------|
| `python-version` | `3.14` | Python version used to run the action |
| `package-version` | `2.0.0` | CodeClone version from PyPI for remote installs; ignored when the action runs from the checked-out CodeClone repo |
| `package-version` | `2.0.1` | 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 |
Expand Down Expand Up @@ -145,7 +145,7 @@ Notes:
## 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
example, `@v2.0.1` installs `codeclone==2.0.1` unless you override
`package-version`.

Explicit prerelease or smoke-test override:
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/codeclone/_action_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from typing import Literal

COMMENT_MARKER = "<!-- codeclone-report -->"
DEFAULT_CODECLONE_PACKAGE_VERSION = "2.0.1b1"
DEFAULT_CODECLONE_PACKAGE_VERSION = "2.0.1"


@dataclass(frozen=True, slots=True)
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/codeclone/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: "2.0.1b1"
default: "2.0.1"

path:
description: "Project root"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ jobs:

- name: Upload benchmark artifact
if: env.BENCH_ENABLED == '1'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: codeclone-benchmark-${{ matrix.label }}
path: ${{ env.BENCH_JSON }}
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ jobs:
run: uv run --with twine twine check dist/*

- name: Upload distributions
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: python-package-distributions
path: dist/
Expand All @@ -83,7 +83,7 @@ jobs:
id-token: write
steps:
- name: Download distributions
uses: actions/download-artifact@v5
uses: actions/download-artifact@v8
with:
name: python-package-distributions
path: dist/
Expand All @@ -107,7 +107,7 @@ jobs:
id-token: write
steps:
- name: Download distributions
uses: actions/download-artifact@v5
uses: actions/download-artifact@v8
with:
name: python-package-distributions
path: dist/
Expand Down
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Changelog

## [Unreleased]
## [2.0.1] - 2026-05-14

`2.0.1` is a focused stability release for dead-code precision and cache/report
contract parity after the 2.0 line.

### Dead code

Expand All @@ -13,6 +16,9 @@
- Treat `typing.Protocol` and `typing_extensions.Protocol` declarations, including
generic `Protocol[T]`, as type-only contracts so structural interfaces do not produce
false-positive dead-code findings.
- Show a one-time interactive CLI migration note for projects upgrading from
the 2.0.0 line when the refined reachability model may reduce dead-code
findings.
- Bump cache schema to `2.7` and report schema to `2.11` to carry reachability facts
for cold/warm parity and report explainability.

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Live sample report: [orenlab.github.io/codeclone/examples/report/](https://orenl
profile, dead code, health score, and overloaded-module profiling
- **Adoption & API** — type/docstring annotation coverage, public API surface inventory and baseline diff
- **Coverage Join** — fuse external Cobertura XML into the current run to surface coverage hotspots and scope gaps
- **Security Surfaces** — report-only inventory of security-relevant capability boundaries without vulnerability claims

**Surfaces & integrations**

Expand Down Expand Up @@ -318,7 +319,7 @@ Top-level keys: `report_schema_version`, `meta`, `inventory`, `findings`, `metri
{
"report_schema_version": "2.11",
"meta": {
"codeclone_version": "2.0.0",
"codeclone_version": "2.0.1",
"project_name": "...",
"scan_root": ".",
"...": "..."
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1.7

FROM python:3.14.3-slim-bookworm
FROM python:3.14.5-slim-bookworm

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
Expand Down
131 changes: 126 additions & 5 deletions codeclone/surfaces/cli/tips.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,24 @@
from pathlib import Path
from typing import TextIO

from packaging.version import InvalidVersion, Version

from ... import ui_messages as ui
from ...utils.json_io import read_json_object, write_json_document_atomically
from .attrs import bool_attr
from .types import PrinterLike

_VSCODE_EXTENSION_TIP_KEY = "vscode_extension"
_DEAD_CODE_REACHABILITY_MIGRATION_TIP_KEY = (
"dead_code_reachability_2_0_1_migration_shown"
)
_TIPS_SCHEMA_VERSION = 1
_VSCODE_EXTENSION_URL = (
"https://marketplace.visualstudio.com/items?itemName=orenlab.codeclone"
)
_DEAD_CODE_REACHABILITY_BASELINE_MIN = Version("2.0.0b1")
_DEAD_CODE_REACHABILITY_BASELINE_MAX = Version("2.0.0")
_DEAD_CODE_REACHABILITY_CURRENT_MIN = Version("2.0.1")
_CI_ENV_KEYS: tuple[str, ...] = (
"CI",
"GITHUB_ACTIONS",
Expand Down Expand Up @@ -91,6 +99,16 @@ def _tip_last_shown_version(state: Mapping[str, object], *, tip_key: str) -> str
return ""


def _tip_was_shown(state: Mapping[str, object], *, tip_key: str) -> bool:
tips = state.get("tips")
if not isinstance(tips, dict):
return False
entry = tips.get(tip_key)
if not isinstance(entry, dict):
return False
return entry.get("shown") is True


def _remember_tip_version(
*,
path: Path,
Expand All @@ -113,6 +131,60 @@ def _remember_tip_version(
)


def _remember_tip_shown(
*,
path: Path,
state: Mapping[str, object],
tip_key: str,
) -> None:
tips = state.get("tips")
updated_tips = dict(tips) if isinstance(tips, dict) else {}
updated_tips[tip_key] = {"shown": True}
write_json_document_atomically(
path,
{
"schema_version": _TIPS_SCHEMA_VERSION,
"tips": updated_tips,
},
sort_keys=True,
indent=True,
trailing_newline=True,
)


def _tip_context_allowed(
*,
args: object,
environ: Mapping[str, str],
stream: TextIO,
) -> bool:
if bool_attr(args, "quiet") or bool_attr(args, "ci"):
return False
if _is_ci_environment(environ):
return False
return _stream_is_tty(stream)


def _dead_code_reachability_migration_applies(
*,
baseline_generator_version: str | None,
codeclone_version: str,
) -> bool:
if not baseline_generator_version:
return False
try:
baseline_version = Version(baseline_generator_version)
current_version = Version(codeclone_version)
except InvalidVersion:
return False
return (
_DEAD_CODE_REACHABILITY_BASELINE_MIN
<= baseline_version
<= _DEAD_CODE_REACHABILITY_BASELINE_MAX
and current_version >= _DEAD_CODE_REACHABILITY_CURRENT_MIN
)


def maybe_print_vscode_extension_tip(
*,
args: object,
Expand All @@ -124,11 +196,11 @@ def maybe_print_vscode_extension_tip(
) -> bool:
effective_environ = os.environ if environ is None else environ
effective_stream = sys.stdout if stream is None else stream
if bool_attr(args, "quiet") or bool_attr(args, "ci"):
return False
if _is_ci_environment(effective_environ):
return False
if not _stream_is_tty(effective_stream):
if not _tip_context_allowed(
args=args,
environ=effective_environ,
stream=effective_stream,
):
return False
if not _is_vscode_environment(effective_environ):
return False
Expand All @@ -154,6 +226,55 @@ def maybe_print_vscode_extension_tip(
return True


def maybe_print_dead_code_reachability_migration_note(
*,
args: object,
console: PrinterLike,
codeclone_version: str,
cache_path: Path,
baseline_generator_version: str | None,
baseline_trusted_for_diff: bool,
environ: Mapping[str, str] | None = None,
stream: TextIO | None = None,
) -> bool:
if not baseline_trusted_for_diff:
return False
if not _dead_code_reachability_migration_applies(
baseline_generator_version=baseline_generator_version,
codeclone_version=codeclone_version,
):
return False

effective_environ = os.environ if environ is None else environ
effective_stream = sys.stdout if stream is None else stream
if not _tip_context_allowed(
args=args,
environ=effective_environ,
stream=effective_stream,
):
return False

state_path = _tips_state_path(cache_path)
state = _load_tips_state(state_path)
if _tip_was_shown(
state,
tip_key=_DEAD_CODE_REACHABILITY_MIGRATION_TIP_KEY,
):
return False

console.print(ui.fmt_dead_code_reachability_migration_note())
try:
_remember_tip_shown(
path=state_path,
state=state,
tip_key=_DEAD_CODE_REACHABILITY_MIGRATION_TIP_KEY,
)
except OSError:
return True
return True


__all__ = [
"maybe_print_dead_code_reachability_migration_note",
"maybe_print_vscode_extension_tip",
]
11 changes: 11 additions & 0 deletions codeclone/surfaces/cli/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@
resolve_changed_clone_gate = cli_post_run.resolve_changed_clone_gate
warn_new_clones_without_fail = cli_post_run.warn_new_clones_without_fail
maybe_print_vscode_extension_tip = cli_tips.maybe_print_vscode_extension_tip
maybe_print_dead_code_reachability_migration_note = (
cli_tips.maybe_print_dead_code_reachability_migration_note
)

_report_path_origins = cli_reports_output._report_path_origins
_resolve_output_paths = cli_reports_output._resolve_output_paths
Expand Down Expand Up @@ -522,6 +525,14 @@ def _main_impl() -> None:
notice_new_clones_count=notice_new_clones_count,
console=_console(),
)
maybe_print_dead_code_reachability_migration_note(
args=args,
console=_console(),
codeclone_version=__version__,
cache_path=cache_path,
baseline_generator_version=baseline_state.baseline.generator_version,
baseline_trusted_for_diff=baseline_state.trusted_for_diff,
)
maybe_print_vscode_extension_tip(
args=args,
console=_console(),
Expand Down
6 changes: 4 additions & 2 deletions codeclone/surfaces/mcp/_session_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
DEFAULT_SEGMENT_MIN_STMT,
)
from ...contracts import (
BASELINE_SCHEMA_VERSION,
DEFAULT_COVERAGE_MIN,
DEFAULT_JSON_REPORT_PATH,
DEFAULT_REPORT_DESIGN_COHESION_THRESHOLD,
Expand Down Expand Up @@ -539,8 +540,9 @@ class MCPHelpTopicSpec:
),
key_points=(
(
"Canonical baseline schema is v2.0 with meta and clone keys; "
"metrics may be embedded for unified flows."
f"Canonical baseline schema is v{BASELINE_SCHEMA_VERSION} "
"with meta and clone keys; metrics may be embedded for "
"unified flows."
),
(
"Compatibility depends on generator identity, supported "
Expand Down
12 changes: 11 additions & 1 deletion codeclone/ui_messages/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
HEALTH_GRADE_F,
)

BANNER_SUBTITLE = "Structural code analysis"
BANNER_SUBTITLE = "Structural review layer"

MARKER_CONTRACT_ERROR = "[error]CONTRACT ERROR:[/error]"
MARKER_INTERNAL_ERROR = "[error]INTERNAL ERROR:[/error]"
Expand Down Expand Up @@ -360,6 +360,12 @@
"navigation.\n"
"[dim]{url}[/dim]"
)
NOTE_DEAD_CODE_REACHABILITY_MIGRATION = (
"\n[dim]Note:[/dim] Dead-code reachability was refined in 2.0.1 for "
"common Python frameworks.\n"
"[dim]Fewer dead-code findings after upgrading from 2.0.0 are expected: "
"this usually means reduced false positives, not weaker detection.[/dim]"
)

_RICH_MARKUP_TAG_RE = re.compile(r"\[/?[a-zA-Z][a-zA-Z0-9_ .#:-]*]")

Expand Down Expand Up @@ -439,6 +445,10 @@ def fmt_vscode_extension_tip(*, url: str) -> str:
return TIP_VSCODE_EXTENSION.format(url=url)


def fmt_dead_code_reachability_migration_note() -> str:
return NOTE_DEAD_CODE_REACHABILITY_MIGRATION


def fmt_legacy_cache_warning(*, legacy_path: Path, new_path: Path) -> str:
return WARN_LEGACY_CACHE.format(legacy_path=legacy_path, new_path=new_path)

Expand Down
1 change: 1 addition & 0 deletions docs/README-pypi.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ deterministic facts.
- **Quality metrics** — cyclomatic complexity, coupling (CBO), cohesion (LCOM4), dependency cycles, adaptive depth profile, dead code, health score, and overloaded-module profiling
- **Adoption & API** — type/docstring annotation coverage, public API surface inventory and baseline diff
- **Coverage Join** — fuse external Cobertura XML into the current run to surface coverage hotspots and scope gaps
- **Security Surfaces** — report-only inventory of security-relevant capability boundaries without vulnerability claims

**Surfaces & integrations**
- **MCP control surface** — triage-first agent and IDE interface over the same canonical pipeline; read-only by contract
Expand Down
Loading
Loading