Skip to content

Commit 0c70546

Browse files
committed
fix(cache,html): invalidate stale api-surface cache and unify report badges
- make cache analysis-profile compatibility aware of api-surface collection - bump cache schema to 2.5 and propagate collect_api_surface through CLI and MCP cache setup - add regressions for warm-cache api-surface runs and profile mismatch handling - unify provenance modal chips under one badge UI and remove mixed status/bool badge styles - polish report cards, typography, mobile wrapping, and hover treatment - add Findings intro banner and fix dead-code suppressed summary count - sync cache contract docs, AGENTS version table, changelog, and HTML tests
1 parent 9acac52 commit 0c70546

21 files changed

Lines changed: 503 additions & 182 deletions

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ from another doc.** Current values (verified at write time):
144144
|-----------------------------------|------------------------------|---------------|
145145
| `BASELINE_SCHEMA_VERSION` | `codeclone/contracts.py` | `2.1` |
146146
| `BASELINE_FINGERPRINT_VERSION` | `codeclone/contracts.py` | `1` |
147-
| `CACHE_VERSION` | `codeclone/contracts.py` | `2.4` |
147+
| `CACHE_VERSION` | `codeclone/contracts.py` | `2.5` |
148148
| `REPORT_SCHEMA_VERSION` | `codeclone/contracts.py` | `2.8` |
149149
| `METRICS_BASELINE_SCHEMA_VERSION` | `codeclone/contracts.py` | `1.2` |
150150

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ across MCP/HTML/clients; tightens MCP launcher/runtime behavior.
2121
`--fail-on-docstring-regression`, `--fail-on-api-break`, `--fail-on-untested-hotspots`, `--coverage-min`.
2222
- Surface adoption/API/coverage-join in MCP, CLI Metrics, report payloads, and HTML (Overview + Quality subtab).
2323
- Preserve embedded metrics and optional `api_surface` in unified baselines.
24-
- Cache `2.4`: drop stale API-surface entries; preserve parameter order; align warm/cold API diffs.
24+
- Cache `2.5`: make analysis-profile compatibility API-surface-aware; invalidate stale non-API warm caches; preserve parameter order; align warm/cold API diffs.
2525

2626
### MCP, HTML, and client interpretation
2727

codeclone/_html_css.py

Lines changed: 144 additions & 110 deletions
Large diffs are not rendered by default.

codeclone/_html_report/_assemble.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ def _tab_badge(count: int) -> str:
280280
f'<a href="{REPOSITORY_URL}" target="_blank" rel="noopener">CodeClone</a> '
281281
f'<span class="muted">v{_escape_html(version)}</span> · '
282282
f'<a href="{DOCS_URL}" target="_blank" rel="noopener">Docs</a> · '
283-
f'<a href="{ISSUES_URL}" target="_blank" rel="noopener">Issues</a>'
283+
f'<a href="{ISSUES_URL}" target="_blank" rel="noopener">Report Issue</a>'
284284
"</div>"
285285
f"{_schema_line}"
286286
"</footer>"

codeclone/_html_report/_sections/_dead_code.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def render_dead_code_panel(ctx: ReportContext) -> str:
110110
_stat_card(
111111
"Candidates",
112112
dead_total,
113-
detail=_micro_badges(("active", dead_total - dead_suppressed_total)),
113+
detail=_micro_badges(("active", dead_total)),
114114
value_tone="warn" if dead_total > 0 else "good",
115115
glossary_tip_fn=glossary_tip,
116116
),

codeclone/_html_report/_sections/_meta.py

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,19 @@ def _truncate_middle(value: str, head: int, tail: int) -> str:
6363
return f"{value[:head]}\u2026{value[-tail:]}"
6464

6565

66-
def _prov_badge_html(label: str, value: str, color: str) -> str:
66+
def _prov_badge_html(label: str | None, value: str, color: str) -> str:
67+
classes = ["prov-badge", f"prov-badge--{color}"]
68+
if label is None:
69+
classes.append("prov-badge--inline")
70+
label_html = (
71+
f'<span class="prov-badge-lbl">{_escape_html(label)}</span>'
72+
if label is not None
73+
else ""
74+
)
6775
return (
68-
f'<span class="prov-badge prov-badge--{color}">'
76+
f'<span class="{" ".join(classes)}">'
6977
f'<span class="prov-badge-val">{_escape_html(value)}</span>'
70-
f'<span class="prov-badge-lbl">{_escape_html(label)}</span>'
78+
f"{label_html}"
7179
"</span>"
7280
)
7381

@@ -365,45 +373,34 @@ def render_meta_panel(ctx: ReportContext) -> str:
365373
_STATUS_LABELS = frozenset(
366374
{"Baseline status", "Metrics baseline status", "Cache status"}
367375
)
376+
_prov_badge = _prov_badge_html
368377

369378
runtime_python_tag = str(python_tag_value or "").strip()
370379

371380
def _val_html(label: str, value: object) -> str:
372381
if label in _BOOL_LABELS and isinstance(value, bool):
373382
true_text, false_text = _BOOL_LABELS[label]
374-
if value:
375-
return (
376-
'<span class="meta-bool meta-bool-true">'
377-
f'<span class="meta-bool-icon">\u2713</span>'
378-
f'<span class="meta-bool-text">{true_text}</span>'
379-
"</span>"
380-
)
381-
return (
382-
'<span class="meta-bool meta-bool-false">'
383-
f'<span class="meta-bool-icon">\u2717</span>'
384-
f'<span class="meta-bool-text">{false_text}</span>'
385-
"</span>"
383+
return _prov_badge(
384+
None,
385+
true_text if value else false_text,
386+
"green" if value else "red",
386387
)
387388
if label in _STATUS_LABELS and isinstance(value, str) and value.strip():
388389
raw = value.strip()
389390
key = raw.lower()
390391
if key == "ok":
391-
tone = "ok"
392+
color = "green"
392393
text = "ok"
393394
elif key in {"error", "failed", "fail"}:
394-
tone = "err"
395+
color = "red"
395396
text = raw
396397
elif key in {"missing", "absent", "none"}:
397-
tone = "warn"
398+
color = "amber"
398399
text = raw
399400
else:
400-
tone = "neutral"
401+
color = "neutral"
401402
text = raw
402-
return (
403-
f'<span class="meta-status meta-status--{tone}">'
404-
f'<span class="meta-status-dot"></span>'
405-
f"{_escape_html(text)}</span>"
406-
)
403+
return _prov_badge(None, text, color)
407404
# Long path/hash values: middle-truncate with copy button + full title
408405
if (
409406
isinstance(value, str)
@@ -437,18 +434,9 @@ def _val_html(label: str, value: object) -> str:
437434
):
438435
text = _escape_html(value)
439436
if value.strip() == runtime_python_tag:
440-
badge = (
441-
'<span class="prov-match prov-match--ok" '
442-
'title="Matches runtime Python tag">'
443-
"\u2713 matches runtime</span>"
444-
)
437+
badge = _prov_badge(None, "matches runtime", "green")
445438
else:
446-
badge = (
447-
'<span class="prov-match prov-match--mismatch" '
448-
f'title="Runtime is {runtime_python_tag}">'
449-
f"\u26a0 differs from runtime ({_escape_html(runtime_python_tag)})"
450-
"</span>"
451-
)
439+
badge = _prov_badge(None, f"runtime {runtime_python_tag}", "amber")
452440
return f"{text} {badge}"
453441
return _escape_html(_meta_display(value))
454442

@@ -505,8 +493,6 @@ def _section_html(title: str, rows: list[tuple[str, object]]) -> str:
505493
_section_html(st, rows) for st, rows in meta_sections if rows
506494
)
507495

508-
_prov_badge = _prov_badge_html
509-
510496
badges: list[str] = []
511497
if _bl_verified is True:
512498
badges.append(_prov_badge("Baseline", "verified", "green"))

codeclone/_html_report/_sections/_structural.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -488,10 +488,6 @@ def build_structural_findings_html_panel(
488488
context_lines: int = 3,
489489
max_snippet_lines: int = 220,
490490
) -> str:
491-
normalized_groups = normalize_structural_findings(groups)
492-
if not normalized_groups:
493-
return _tab_empty("No structural findings detected.")
494-
495491
intro = (
496492
'<div class="insight-banner insight-info">'
497493
'<div class="insight-question">What are structural findings?</div>'
@@ -500,6 +496,9 @@ def build_structural_findings_html_panel(
500496
"refactoring hints and do not affect clone detection or CI verdicts.</div>"
501497
"</div>"
502498
)
499+
normalized_groups = normalize_structural_findings(groups)
500+
if not normalized_groups:
501+
return intro + _tab_empty("No structural findings detected.")
503502

504503
resolved_file_cache = file_cache if file_cache is not None else _FileCache()
505504
why_templates: list[str] = []

codeclone/cache.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ class AnalysisProfile(TypedDict):
238238
block_min_stmt: int
239239
segment_min_loc: int
240240
segment_min_stmt: int
241+
collect_api_surface: bool
241242

242243

243244
class CacheData(TypedDict):
@@ -344,6 +345,7 @@ def __init__(
344345
block_min_stmt: int = 8,
345346
segment_min_loc: int = 20,
346347
segment_min_stmt: int = 10,
348+
collect_api_surface: bool = False,
347349
):
348350
self.path = Path(path)
349351
self.root = _resolve_root(root)
@@ -355,6 +357,7 @@ def __init__(
355357
"block_min_stmt": block_min_stmt,
356358
"segment_min_loc": segment_min_loc,
357359
"segment_min_stmt": segment_min_stmt,
360+
"collect_api_surface": collect_api_surface,
358361
}
359362
self.data: CacheData = _empty_cache_data(
360363
version=self._CACHE_VERSION,
@@ -557,9 +560,13 @@ def _load_and_validate(self, raw_obj: object) -> CacheData | None:
557560
return self._reject_cache_load(
558561
"Cache analysis profile mismatch "
559562
f"(found min_loc={analysis_profile['min_loc']}, "
560-
f"min_stmt={analysis_profile['min_stmt']}; "
563+
f"min_stmt={analysis_profile['min_stmt']}, "
564+
"collect_api_surface="
565+
f"{str(analysis_profile['collect_api_surface']).lower()}; "
561566
f"expected min_loc={self.analysis_profile['min_loc']}, "
562-
f"min_stmt={self.analysis_profile['min_stmt']}); "
567+
f"min_stmt={self.analysis_profile['min_stmt']}, "
568+
"collect_api_surface="
569+
f"{str(self.analysis_profile['collect_api_surface']).lower()}); "
563570
"ignoring cache.",
564571
status=CacheStatus.ANALYSIS_PROFILE_MISMATCH,
565572
schema_version=version,
@@ -1482,13 +1489,18 @@ def _as_analysis_profile(value: object) -> AnalysisProfile | None:
14821489
block_min_stmt = _as_int(obj.get("block_min_stmt"))
14831490
segment_min_loc = _as_int(obj.get("segment_min_loc"))
14841491
segment_min_stmt = _as_int(obj.get("segment_min_stmt"))
1492+
collect_api_surface_raw = obj.get("collect_api_surface", False)
1493+
collect_api_surface = (
1494+
collect_api_surface_raw if isinstance(collect_api_surface_raw, bool) else None
1495+
)
14851496
if (
14861497
min_loc is None
14871498
or min_stmt is None
14881499
or block_min_loc is None
14891500
or block_min_stmt is None
14901501
or segment_min_loc is None
14911502
or segment_min_stmt is None
1503+
or collect_api_surface is None
14921504
):
14931505
return None
14941506

@@ -1499,6 +1511,7 @@ def _as_analysis_profile(value: object) -> AnalysisProfile | None:
14991511
block_min_stmt=block_min_stmt,
15001512
segment_min_loc=segment_min_loc,
15011513
segment_min_stmt=segment_min_stmt,
1514+
collect_api_surface=collect_api_surface,
15021515
)
15031516

15041517

codeclone/cli.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,6 +1372,7 @@ def _prepare_run_inputs() -> tuple[
13721372
block_min_stmt=args.block_min_stmt,
13731373
segment_min_loc=args.segment_min_loc,
13741374
segment_min_stmt=args.segment_min_stmt,
1375+
collect_api_surface=bool(args.api_surface),
13751376
)
13761377
cache.load()
13771378
if cache.load_warning:

codeclone/contracts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
BASELINE_SCHEMA_VERSION: Final = "2.1"
1313
BASELINE_FINGERPRINT_VERSION: Final = "1"
1414

15-
CACHE_VERSION: Final = "2.4"
15+
CACHE_VERSION: Final = "2.5"
1616
REPORT_SCHEMA_VERSION: Final = "2.8"
1717
METRICS_BASELINE_SCHEMA_VERSION: Final = "1.2"
1818

0 commit comments

Comments
 (0)