Skip to content

Commit a472e97

Browse files
authored
v2.0.0b1 (#13)
* feat: complete spec 2.0.0 architecture and UX updates * feat(cli iu): improvements to the cli UI, adding pre-commit hooks, updating documentation * chore(docs): update logo size * feat(core): UI improvements, updating report schemas; adding new formats (md, sarif); multiple hot path optimizations and other improvements * feat(core): Comprehensive benchmark for codeclone has been added, the documentation has been updated, and the crash of tests in CI has been fixed * fix(tests): stabilize unreadable-source CLI assertion across runners by checking combined stdout/stderr output; fix README.md; fix matrics error in GAW * fix(ci): isolate unreadable-source CLI tests with per-test cache path and run benchmark container under host uid:gid to prevent bind-mount write permission failures * fix(tests): deflake unreadable-source CLI check by asserting JSON source_io_skipped contract instead of unstable console warning text * chore(docs): update README.md and update package deps * fix(perf): harden scanner root filtering and optimize report snippet/explain paths * docs(changelog): add 1.4.4 backport section under 2.0.0b1 * feat(core): ship clone_guard_exit_divergence + clone_cohort_drift, optimize cache/report pipeline, harden deterministic contracts, and align docs/tests for 2.0.0b1 * fix(detect): treat module-level PEP 562 hooks (__getattr__/__dir__) as non-actionable dead-code candidates and update tests/docs * feat(detect): add declaration-scoped # noqa: codeclone[dead-code] suppressions (parser, symbol binding, final filtering) with tests and docs; update html-report UI * fix(report): propagate dead-code noqa suppressions end-to-end (detection→metrics→CLI/JSON/TXT/MD/HTML), exclude suppressed from health * chore(docs): update docs * refactor(domain): centralize report/pipeline taxonomies and unify coercion helpers/tests * refactor(domain): centralize finding taxonomies across sarif/structural/ui and enforce domain-layer boundaries * refactor(report): modularize HTML rendering, finalize codeclone suppressions, and clean dead code * fix(suppressions): bind multiline inline ignores to decorated declaration headers consistently * fix(suppressions): bind multiline inline ignores to decorated declaration headers consistently * refactor(html): refresh report layouts, findings cards, and metric delta badges * feat(cli): add browser-open HTML reports and timestamped default report paths * test(cli): make HTML open warning assertion robust across wrapped console output * refactor(report): materialize overview insights, streamline cache/extractor internals, and sync docs * feat(core): improve report UX, strengthen cache/report contracts, and publish project docs * chore(docs): update AGENTS.md * perf(core): add lazy suppression/report fast-paths and streamline extractor binding * chore(release): finalize beta packaging metadata, PyPI README links, and v2.0.0b1 notes
1 parent 424dfb3 commit a472e97

File tree

192 files changed

+44044
-9189
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

192 files changed

+44044
-9189
lines changed

.dockerignore

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.git
2+
.cache
3+
.venv
4+
.pytest_cache
5+
.mypy_cache
6+
.ruff_cache
7+
.idea
8+
__pycache__/
9+
*.pyc
10+
*.pyo
11+
*.pyd
12+
.coverage
13+
build/
14+
dist/
15+
*.egg-info/
16+
.uv-cache
17+
docs
18+
codeclone.egg-info
19+
.pre-commit-config.yaml
20+
uv.lock

.github/actions/codeclone/action.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: CodeClone
22
description: >
3-
AST-based Python code clone detector focused on architectural duplication
4-
and CI-friendly baseline enforcement.
3+
Structural code quality analysis for Python with
4+
CI-friendly baseline enforcement.
55
66
author: OrenLab
77

.github/workflows/benchmark.yml

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
name: benchmark
2+
run-name: benchmark • ${{ github.event_name }} • ${{ github.ref_name }}
3+
4+
on:
5+
push:
6+
branches: [ "feat/2.0.0" ]
7+
pull_request:
8+
branches: [ "feat/2.0.0" ]
9+
workflow_dispatch:
10+
inputs:
11+
profile:
12+
description: Benchmark profile
13+
required: true
14+
default: smoke
15+
type: choice
16+
options:
17+
- smoke
18+
- extended
19+
20+
permissions:
21+
contents: read
22+
23+
concurrency:
24+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
25+
cancel-in-progress: true
26+
27+
jobs:
28+
benchmark:
29+
name: >-
30+
bench • ${{ matrix.label }}
31+
runs-on: ${{ matrix.os }}
32+
timeout-minutes: ${{ matrix.timeout_minutes }}
33+
34+
strategy:
35+
fail-fast: false
36+
matrix:
37+
include:
38+
# default profile for push / PR
39+
- profile: smoke
40+
label: linux-smoke
41+
os: ubuntu-latest
42+
runs: 12
43+
warmups: 3
44+
cpus: "1.0"
45+
memory: "2g"
46+
timeout_minutes: 45
47+
48+
# extended profile for manual runs
49+
- profile: extended
50+
label: linux-extended
51+
os: ubuntu-latest
52+
runs: 16
53+
warmups: 4
54+
cpus: "1.0"
55+
memory: "2g"
56+
timeout_minutes: 50
57+
58+
- profile: extended
59+
label: macos-extended
60+
os: macos-latest
61+
runs: 12
62+
warmups: 3
63+
cpus: ""
64+
memory: ""
65+
timeout_minutes: 60
66+
67+
steps:
68+
- name: Resolve run profile gate
69+
shell: bash
70+
run: |
71+
enabled=0
72+
if [ "${{ github.event_name }}" != "workflow_dispatch" ]; then
73+
if [ "${{ matrix.profile }}" = "smoke" ]; then
74+
enabled=1
75+
fi
76+
else
77+
if [ "${{ matrix.profile }}" = "${{ inputs.profile }}" ]; then
78+
enabled=1
79+
fi
80+
fi
81+
echo "BENCH_ENABLED=$enabled" >> "$GITHUB_ENV"
82+
83+
- name: Checkout
84+
if: env.BENCH_ENABLED == '1'
85+
uses: actions/checkout@v6.0.2
86+
87+
- name: Set up Python (macOS local benchmark)
88+
if: env.BENCH_ENABLED == '1' && runner.os == 'macOS'
89+
uses: actions/setup-python@v6.2.0
90+
with:
91+
python-version: "3.13"
92+
allow-prereleases: true
93+
94+
- name: Set up uv (macOS local benchmark)
95+
if: env.BENCH_ENABLED == '1' && runner.os == 'macOS'
96+
uses: astral-sh/setup-uv@v5
97+
with:
98+
enable-cache: true
99+
100+
- name: Install dependencies (macOS local benchmark)
101+
if: env.BENCH_ENABLED == '1' && runner.os == 'macOS'
102+
run: uv sync --all-extras --dev
103+
104+
- name: Set benchmark output path
105+
if: env.BENCH_ENABLED == '1'
106+
shell: bash
107+
run: |
108+
mkdir -p .cache/benchmarks
109+
echo "BENCH_JSON=.cache/benchmarks/codeclone-benchmark-${{ matrix.label }}.json" >> "$GITHUB_ENV"
110+
111+
- name: Build and run Docker benchmark (Linux)
112+
if: env.BENCH_ENABLED == '1' && runner.os == 'Linux'
113+
env:
114+
RUNS: ${{ matrix.runs }}
115+
WARMUPS: ${{ matrix.warmups }}
116+
CPUS: ${{ matrix.cpus }}
117+
MEMORY: ${{ matrix.memory }}
118+
run: |
119+
./benchmarks/run_docker_benchmark.sh
120+
cp .cache/benchmarks/codeclone-benchmark.json "$BENCH_JSON"
121+
122+
- name: Run local benchmark (macOS)
123+
if: env.BENCH_ENABLED == '1' && runner.os == 'macOS'
124+
run: |
125+
uv run python benchmarks/run_benchmark.py \
126+
--target . \
127+
--runs "${{ matrix.runs }}" \
128+
--warmups "${{ matrix.warmups }}" \
129+
--tmp-dir "/tmp/codeclone-bench-${{ matrix.label }}" \
130+
--output "$BENCH_JSON"
131+
132+
- name: Print benchmark summary
133+
if: env.BENCH_ENABLED == '1'
134+
shell: bash
135+
run: |
136+
python - <<'PY'
137+
import json
138+
import os
139+
from pathlib import Path
140+
141+
report_path = Path(os.environ["BENCH_JSON"])
142+
if not report_path.exists():
143+
print(f"benchmark report not found: {report_path}")
144+
raise SystemExit(1)
145+
146+
payload = json.loads(report_path.read_text(encoding="utf-8"))
147+
scenarios = payload.get("scenarios", [])
148+
comparisons = payload.get("comparisons", {})
149+
150+
print("CodeClone benchmark summary")
151+
print(f"label={os.environ.get('RUNNER_OS','unknown').lower()} / {os.environ.get('GITHUB_JOB','benchmark')}")
152+
for scenario in scenarios:
153+
name = str(scenario.get("name", "unknown"))
154+
stats = scenario.get("stats_seconds", {})
155+
median = float(stats.get("median", 0.0))
156+
p95 = float(stats.get("p95", 0.0))
157+
stdev = float(stats.get("stdev", 0.0))
158+
digest = str(scenario.get("digest", ""))
159+
print(
160+
f"- {name:16s} median={median:.4f}s "
161+
f"p95={p95:.4f}s stdev={stdev:.4f}s digest={digest}"
162+
)
163+
164+
if comparisons:
165+
print("ratios:")
166+
for key, value in sorted(comparisons.items()):
167+
print(f"- {key}={float(value):.3f}x")
168+
169+
summary_file = os.environ.get("GITHUB_STEP_SUMMARY")
170+
if not summary_file:
171+
raise SystemExit(0)
172+
173+
lines = [
174+
f"## CodeClone benchmark — {os.environ.get('RUNNER_OS', 'unknown')} / ${{ matrix.label }}",
175+
"",
176+
f"- Tool: `{payload['tool']['name']} {payload['tool']['version']}`",
177+
f"- Target: `{payload['config']['target']}`",
178+
f"- Runs: `{payload['config']['runs']}`",
179+
f"- Warmups: `{payload['config']['warmups']}`",
180+
f"- Generated: `{payload['generated_at_utc']}`",
181+
"",
182+
"### Scenarios",
183+
"",
184+
"| Scenario | Median (s) | p95 (s) | Stdev (s) | Deterministic | Digest |",
185+
"|---|---:|---:|---:|:---:|---|",
186+
]
187+
188+
for scenario in scenarios:
189+
stats = scenario.get("stats_seconds", {})
190+
lines.append(
191+
"| "
192+
f"{scenario.get('name', '')} | "
193+
f"{float(stats.get('median', 0.0)):.4f} | "
194+
f"{float(stats.get('p95', 0.0)):.4f} | "
195+
f"{float(stats.get('stdev', 0.0)):.4f} | "
196+
f"{'yes' if bool(scenario.get('deterministic')) else 'no'} | "
197+
f"{scenario.get('digest', '')} |"
198+
)
199+
200+
if comparisons:
201+
lines.extend(
202+
[
203+
"",
204+
"### Ratios",
205+
"",
206+
"| Metric | Value |",
207+
"|---|---:|",
208+
]
209+
)
210+
for key, value in sorted(comparisons.items()):
211+
lines.append(f"| {key} | {float(value):.3f}x |")
212+
213+
with Path(summary_file).open("a", encoding="utf-8") as fh:
214+
fh.write("\n".join(lines) + "\n")
215+
PY
216+
217+
- name: Skip non-selected profile
218+
if: env.BENCH_ENABLED != '1'
219+
run: echo "Skipping matrix profile '${{ matrix.profile }}' for event '${{ github.event_name }}'"
220+
221+
- name: Upload benchmark artifact
222+
if: env.BENCH_ENABLED == '1'
223+
uses: actions/upload-artifact@v4
224+
with:
225+
name: codeclone-benchmark-${{ matrix.label }}
226+
path: ${{ env.BENCH_JSON }}
227+
if-no-files-found: error

.github/workflows/docs.yml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
name: docs
2+
run-name: docs • ${{ github.event_name }} • ${{ github.ref_name }}
3+
4+
on:
5+
push:
6+
branches: [ "main" ]
7+
pull_request:
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
concurrency:
14+
group: docs-${{ github.ref }}
15+
cancel-in-progress: true
16+
17+
jobs:
18+
build:
19+
runs-on: ubuntu-latest
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@v6.0.2
23+
24+
- name: Set up Python
25+
uses: actions/setup-python@v6.2.0
26+
with:
27+
python-version: "3.13"
28+
allow-prereleases: true
29+
30+
- name: Set up uv
31+
uses: astral-sh/setup-uv@v5
32+
with:
33+
enable-cache: true
34+
35+
- name: Install project dependencies
36+
run: uv sync --dev
37+
38+
- name: Configure GitHub Pages
39+
uses: actions/configure-pages@v5
40+
41+
- name: Build docs site
42+
run: uv run --with mkdocs --with mkdocs-material mkdocs build --strict
43+
44+
- name: Generate sample report artifacts
45+
run: uv run python scripts/build_docs_example_report.py --output-dir site/examples/report/live
46+
47+
- name: Upload docs artifact
48+
if: ${{ github.event_name != 'push' || github.ref != 'refs/heads/main' }}
49+
uses: actions/upload-artifact@v7
50+
with:
51+
name: codeclone-docs-site
52+
path: site
53+
if-no-files-found: error
54+
55+
- name: Upload GitHub Pages artifact
56+
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
57+
uses: actions/upload-pages-artifact@v4
58+
with:
59+
path: site
60+
61+
deploy:
62+
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
63+
runs-on: ubuntu-latest
64+
needs: build
65+
permissions:
66+
contents: read
67+
pages: write
68+
id-token: write
69+
environment:
70+
name: github-pages
71+
url: ${{ steps.deployment.outputs.page_url }}
72+
steps:
73+
- name: Deploy to GitHub Pages
74+
id: deployment
75+
uses: actions/deploy-pages@v4

.gitignore

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ __pycache__/
1818
.coverage
1919
.coverage.*
2020
htmlcov/
21+
site/
2122

2223
# Tool caches
2324
.cache/
@@ -33,5 +34,7 @@ htmlcov/
3334

3435
# Logs
3536
*.log
36-
37-
.claude
37+
/.claude/
38+
/docs/SPEC-2.0.0.md
39+
/.uv-cache/
40+
/package-lock.json

0 commit comments

Comments
 (0)