Skip to content

Commit c643bd4

Browse files
committed
refactor(copy): centralize user-facing strings across CLI, report, and MCP
1 parent 2d5c742 commit c643bd4

63 files changed

Lines changed: 4031 additions & 2410 deletions

Some content is hidden

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

AGENTS.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,8 @@ Architecture is layered, but grounded in current code (not aspirational diagrams
410410

411411
- **CLI entry + orchestration surface** (`codeclone/main.py`, `codeclone/surfaces/cli/*`, `codeclone/ui_messages/*`)
412412
owns argument parsing, runtime/config resolution, summaries, report writes, and exit routing.
413+
User-facing copy lives in `ui_messages/` submodules (`help`, `labels`, `runtime`,
414+
`markers`, `formatters`, `controller`, `styling`).
413415
- **Config layer** (`codeclone/config/*`) is the single source of truth for option specs, parser construction,
414416
`pyproject.toml` loading, and CLI > pyproject > defaults resolution.
415417
- **Core orchestration** (`codeclone/core/*`) owns bootstrap → discovery → worker processing → project metrics →
@@ -509,7 +511,8 @@ Use this map to route changes to the right owner module.
509511
- `codeclone/surfaces/mcp/server.py` — optional MCP launcher/server wiring, transport config, and MCP tool/resource
510512
registration; keep dependency loading lazy so base installs/CI do not require MCP runtime packages.
511513
- `codeclone/surfaces/mcp/messages/*` — MCP user-facing copy (tool/resource descriptions, help topics, workflow and
512-
intent messages, parameter Field docs). Keep message policy centralized like `ui_messages/`.
514+
intent messages, parameter Field docs, patch-contract hints, verification copy, remediation shapes). Keep message
515+
policy centralized like `ui_messages/`.
513516
- `codeclone/audit/*` — audit event schema, validation, writer/reader; passive evidence only.
514517
- `codeclone/budget/*` — patch/token budget estimation shared by CLI and MCP surfaces.
515518
- `tests/test_mcp_service.py`, `tests/test_mcp_server.py` — MCP contract and integration tests; run these when
@@ -520,6 +523,9 @@ Use this map to route changes to the right owner module.
520523
- `codeclone/domain/*.py` — centralized domain taxonomies/IDs (families, categories, source scopes, risk/severity
521524
levels); use these constants in pipeline/report/UI instead of scattering raw literals.
522525
- `codeclone/ui_messages/*` — CLI text/marker/help constants and formatter helpers. Keep message policy centralized.
526+
- `codeclone/report/messages/*` — report-layer user copy (glossary, suggestions,
527+
explainability, overview, security, chrome, text/markdown/sarif projections,
528+
gate prefixes).
523529
- `docs/`, `mkdocs.yml`, `.github/workflows/docs.yml`, `scripts/build_docs_example_report.py` — docs-site source,
524530
publication workflow, and live sample-report generation; keep published docs aligned with code contracts.
525531
- `extensions/vscode-codeclone/*` — stable VS Code extension surface; keep it baseline-aware, triage-first,
@@ -758,8 +764,9 @@ These rules exist because of real incidents in this repo. They are non-negotiabl
758764
`codeclone/report/html/assets/*`) are imported, not duplicated locally inside
759765
`codeclone/report/html/sections/*`.
760766
If you need a helper that doesn't exist, add it to the shared module.
761-
- Glossary terms used in stat-card labels live in `codeclone/report/html/widgets/glossary.py`. Adding a
762-
new label without a glossary entry is a contract gap.
767+
- Glossary term definitions live in `codeclone/report/messages/glossary.py`;
768+
`codeclone/report/html/widgets/glossary.py` renders HTML tooltips from that
769+
catalog. Adding a new stat-card label without a glossary entry is a contract gap.
763770

764771
### Conflict avoidance
765772

CHANGELOG.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,15 @@ review receipts, and workflow consolidation tools.
3333
- CLI controller query modes: `--blast-radius` and `--patch-verify`.
3434
- Audit trail events for intent lifecycle and token budget tracking.
3535
- MCP tool JSON schemas now include concise per-parameter descriptions.
36-
- MCP message catalog at `codeclone/surfaces/mcp/messages/*` for tool/resource
37-
descriptions, help topics, workflow/intent copy, and parameter Field docs
38-
(mirrors CLI `ui_messages/`).
36+
- Split monolithic `codeclone/ui_messages/__init__.py` into focused modules
37+
(`help`, `labels`, `runtime`, `markers`, `formatters`, `controller`, `styling`;
38+
stable names re-exported from `__init__.py`).
39+
- `codeclone/report/messages/glossary.py` for HTML tooltip definitions.
40+
- `codeclone/report/messages/` modules for suggestions, explainability, overview,
41+
security, chrome, projections, gate reason prefixes, plus `markdown.py` and
42+
`sarif.py` for Markdown/SARIF projection copy.
43+
- MCP `messages/patch_contract`, `verification`, `remediation`, `errors`,
44+
`receipt`, `claims`, and `blast_radius` modules.
3945
- `help(detail="compact")` includes `anti_patterns`; `trust_boundaries` help topic.
4046
- Analyze responses include a single `next_tool` hint; summary adds
4147
`security_surfaces.note` for report-only inventory semantics.
@@ -48,6 +54,16 @@ review receipts, and workflow consolidation tools.
4854
- MCP long UI strings moved out of `server.py`, `_session_shared.py`, and session
4955
mixins into `codeclone/surfaces/mcp/messages/*`; `_tool_param_docs.py` re-exports
5056
`messages/params.py`.
57+
- Controller CLI screen copy centralized in `ui_messages/controller.py`.
58+
- HTML glossary terms moved to `report/messages/glossary.py`.
59+
- Report suggestion/explainability copy moved to `report/messages/*`; gate reason
60+
prefixes shared between `gates/evaluator.py` and `gates/reasons.py`.
61+
- Text, Markdown, and SARIF renderers and HTML overview/structural/security/chrome
62+
sections consume `report/messages/*` instead of inline copy.
63+
- MCP tool/resource `title=` strings centralized in `messages/tools.py` and
64+
`messages/resources.py`; validation errors use `messages/errors.py`.
65+
- MCP receipt, claim-guard, and blast-radius user copy moved to
66+
`surfaces/mcp/messages/*`.
5167
- `pydantic` is now a base dependency (MCP schema export today; validation
5268
migration planned separately).
5369
- MCP always loads `golden_fixture_paths` from pyproject governance keys even when

codeclone/report/gates/evaluator.py

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from ...utils.coerce import as_int as _as_int
1616
from ...utils.coerce import as_mapping as _as_mapping
1717
from ...utils.coerce import as_sequence as _as_sequence
18+
from ..messages import gates as gate_msgs
1819

1920
if TYPE_CHECKING:
2021
from ...models import CoverageJoinResult, ProjectMetrics
@@ -271,9 +272,9 @@ def _complexity_threshold_reason(
271272
) -> tuple[str, ...]:
272273
return _reason_if(
273274
0 <= config.fail_complexity < state.complexity_max,
274-
"Complexity threshold exceeded: "
275-
f"max CC={state.complexity_max}, "
276-
f"threshold={config.fail_complexity}.",
275+
gate_msgs.GATE_REASON_COMPLEXITY_THRESHOLD
276+
+ f"max CC={state.complexity_max}, "
277+
+ f"threshold={config.fail_complexity}.",
277278
)
278279

279280

@@ -284,9 +285,9 @@ def _coupling_threshold_reason(
284285
) -> tuple[str, ...]:
285286
return _reason_if(
286287
0 <= config.fail_coupling < state.coupling_max,
287-
"Coupling threshold exceeded: "
288-
f"max CBO={state.coupling_max}, "
289-
f"threshold={config.fail_coupling}.",
288+
gate_msgs.GATE_REASON_COUPLING_THRESHOLD
289+
+ f"max CBO={state.coupling_max}, "
290+
+ f"threshold={config.fail_coupling}.",
290291
)
291292

292293

@@ -297,9 +298,9 @@ def _cohesion_threshold_reason(
297298
) -> tuple[str, ...]:
298299
return _reason_if(
299300
0 <= config.fail_cohesion < state.cohesion_max,
300-
"Cohesion threshold exceeded: "
301-
f"max LCOM4={state.cohesion_max}, "
302-
f"threshold={config.fail_cohesion}.",
301+
gate_msgs.GATE_REASON_COHESION_THRESHOLD
302+
+ f"max LCOM4={state.cohesion_max}, "
303+
+ f"threshold={config.fail_cohesion}.",
303304
)
304305

305306

@@ -310,8 +311,8 @@ def _health_threshold_reason(
310311
) -> tuple[str, ...]:
311312
return _reason_if(
312313
config.fail_health >= 0 and state.health_score < config.fail_health,
313-
"Health score below threshold: "
314-
f"score={state.health_score}, threshold={config.fail_health}.",
314+
gate_msgs.GATE_REASON_HEALTH_THRESHOLD
315+
+ f"score={state.health_score}, threshold={config.fail_health}.",
315316
)
316317

317318

@@ -322,7 +323,7 @@ def _dependency_cycles_reason(
322323
) -> tuple[str, ...]:
323324
return _reason_if(
324325
config.fail_cycles and state.dependency_cycles > 0,
325-
f"Dependency cycles detected: {state.dependency_cycles} cycle(s).",
326+
f"{gate_msgs.GATE_REASON_CYCLES_DETECTED}{state.dependency_cycles}{gate_msgs.GATE_SUFFIX_CYCLES}.",
326327
)
327328

328329

@@ -333,7 +334,7 @@ def _dead_code_high_confidence_reason(
333334
) -> tuple[str, ...]:
334335
return _reason_if(
335336
config.fail_dead_code and state.dead_high_confidence > 0,
336-
f"Dead code detected (high confidence): {state.dead_high_confidence} item(s).",
337+
f"{gate_msgs.GATE_REASON_DEAD_CODE_DETECTED}{state.dead_high_confidence}{gate_msgs.GATE_SUFFIX_ITEMS}.",
337338
)
338339

339340

@@ -344,8 +345,7 @@ def _new_high_risk_functions_reason(
344345
) -> tuple[str, ...]:
345346
return _reason_if(
346347
config.fail_on_new_metrics and state.diff_new_high_risk_functions > 0,
347-
"New high-risk functions vs metrics baseline: "
348-
f"{state.diff_new_high_risk_functions}.",
348+
f"{gate_msgs.GATE_REASON_NEW_HIGH_RISK_FUNCTIONS}{state.diff_new_high_risk_functions}.",
349349
)
350350

351351

@@ -356,8 +356,7 @@ def _new_high_coupling_classes_reason(
356356
) -> tuple[str, ...]:
357357
return _reason_if(
358358
config.fail_on_new_metrics and state.diff_new_high_coupling_classes > 0,
359-
"New high-coupling classes vs metrics baseline: "
360-
f"{state.diff_new_high_coupling_classes}.",
359+
f"{gate_msgs.GATE_REASON_NEW_HIGH_COUPLING}{state.diff_new_high_coupling_classes}.",
361360
)
362361

363362

@@ -368,7 +367,7 @@ def _new_dependency_cycles_reason(
368367
) -> tuple[str, ...]:
369368
return _reason_if(
370369
config.fail_on_new_metrics and state.diff_new_cycles > 0,
371-
f"New dependency cycles vs metrics baseline: {state.diff_new_cycles}.",
370+
f"{gate_msgs.GATE_REASON_NEW_CYCLES}{state.diff_new_cycles}.",
372371
)
373372

374373

@@ -379,7 +378,7 @@ def _new_dead_code_reason(
379378
) -> tuple[str, ...]:
380379
return _reason_if(
381380
config.fail_on_new_metrics and state.diff_new_dead_code > 0,
382-
f"New dead code items vs metrics baseline: {state.diff_new_dead_code}.",
381+
f"{gate_msgs.GATE_REASON_NEW_DEAD_CODE}{state.diff_new_dead_code}.",
383382
)
384383

385384

@@ -390,7 +389,7 @@ def _health_regression_reason(
390389
) -> tuple[str, ...]:
391390
return _reason_if(
392391
config.fail_on_new_metrics and state.diff_health_delta < 0,
393-
f"Health score regressed vs metrics baseline: delta={state.diff_health_delta}.",
392+
f"{gate_msgs.GATE_REASON_HEALTH_REGRESSION}{state.diff_health_delta}.",
394393
)
395394

396395

@@ -403,8 +402,8 @@ def _typing_coverage_threshold_reason(
403402
return _reason_if(
404403
config.min_typing_coverage >= 0
405404
and typing_percent < float(config.min_typing_coverage),
406-
"Typing coverage below threshold: "
407-
f"coverage={typing_percent:.1f}%, threshold={config.min_typing_coverage}%.",
405+
gate_msgs.GATE_REASON_TYPING_THRESHOLD
406+
+ f"coverage={typing_percent:.1f}%, threshold={config.min_typing_coverage}%.",
408407
)
409408

410409

@@ -417,9 +416,9 @@ def _docstring_coverage_threshold_reason(
417416
return _reason_if(
418417
config.min_docstring_coverage >= 0
419418
and docstring_percent < float(config.min_docstring_coverage),
420-
"Docstring coverage below threshold: "
421-
f"coverage={docstring_percent:.1f}%, "
422-
f"threshold={config.min_docstring_coverage}%.",
419+
gate_msgs.GATE_REASON_DOCSTRING_THRESHOLD
420+
+ f"coverage={docstring_percent:.1f}%, "
421+
+ f"threshold={config.min_docstring_coverage}%.",
423422
)
424423

425424

@@ -434,9 +433,9 @@ def _typing_regression_reason(
434433
state.diff_typing_param_permille_delta < 0
435434
or state.diff_typing_return_permille_delta < 0
436435
),
437-
"Typing coverage regressed vs metrics baseline: "
438-
f"params_delta={state.diff_typing_param_permille_delta}, "
439-
f"returns_delta={state.diff_typing_return_permille_delta}.",
436+
gate_msgs.GATE_REASON_TYPING_REGRESSION
437+
+ f"params_delta={state.diff_typing_param_permille_delta}, "
438+
+ f"returns_delta={state.diff_typing_return_permille_delta}.",
440439
)
441440

442441

@@ -447,8 +446,7 @@ def _docstring_regression_reason(
447446
) -> tuple[str, ...]:
448447
return _reason_if(
449448
config.fail_on_docstring_regression and state.diff_docstring_permille_delta < 0,
450-
"Docstring coverage regressed vs metrics baseline: "
451-
f"delta={state.diff_docstring_permille_delta}.",
449+
f"{gate_msgs.GATE_REASON_DOCSTRING_REGRESSION}{state.diff_docstring_permille_delta}.",
452450
)
453451

454452

@@ -459,8 +457,7 @@ def _api_breaking_changes_reason(
459457
) -> tuple[str, ...]:
460458
return _reason_if(
461459
config.fail_on_api_break and state.api_breaking_changes > 0,
462-
"Public API breaking changes vs metrics baseline: "
463-
f"{state.api_breaking_changes}.",
460+
f"{gate_msgs.GATE_REASON_API_BREAKING}{state.api_breaking_changes}.",
464461
)
465462

466463

@@ -473,9 +470,9 @@ def _coverage_hotspots_reason(
473470
config.fail_on_untested_hotspots
474471
and state.coverage_join_status == "ok"
475472
and state.coverage_hotspots > 0,
476-
"Coverage hotspots detected: "
477-
f"hotspots={state.coverage_hotspots}, "
478-
f"threshold={config.coverage_min}%.",
473+
gate_msgs.GATE_REASON_COVERAGE_HOTSPOTS
474+
+ f"hotspots={state.coverage_hotspots}, "
475+
+ f"threshold={config.coverage_min}%.",
479476
)
480477

481478

codeclone/report/gates/reasons.py

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
from typing import Protocol
1010

11+
from ..messages import gates as gate_msgs
12+
1113
__all__ = [
1214
"parse_metric_reason_entry",
1315
"policy_context",
@@ -65,58 +67,53 @@ def tail(prefix: str) -> str:
6567
return trimmed[len(prefix) :]
6668

6769
simple_prefixes: tuple[tuple[str, str], ...] = (
68-
("New high-risk functions vs metrics baseline: ", "new_high_risk_functions"),
69-
(
70-
"New high-coupling classes vs metrics baseline: ",
71-
"new_high_coupling_classes",
72-
),
73-
("New dependency cycles vs metrics baseline: ", "new_dependency_cycles"),
74-
("New dead code items vs metrics baseline: ", "new_dead_code_items"),
70+
(gate_msgs.GATE_REASON_NEW_HIGH_RISK_FUNCTIONS, "new_high_risk_functions"),
71+
(gate_msgs.GATE_REASON_NEW_HIGH_COUPLING, "new_high_coupling_classes"),
72+
(gate_msgs.GATE_REASON_NEW_CYCLES, "new_dependency_cycles"),
73+
(gate_msgs.GATE_REASON_NEW_DEAD_CODE, "new_dead_code_items"),
7574
)
7675
for prefix, kind in simple_prefixes:
7776
if trimmed.startswith(prefix):
7877
return kind, tail(prefix)
7978

80-
if trimmed.startswith("Health score regressed vs metrics baseline: delta="):
79+
if trimmed.startswith(gate_msgs.GATE_REASON_HEALTH_REGRESSION):
8180
return "health_delta", trimmed.rsplit("=", maxsplit=1)[1]
8281
typing_detail = _parse_two_part_metric_detail(
8382
trimmed,
84-
prefix="Typing coverage regressed vs metrics baseline: ",
83+
prefix=gate_msgs.GATE_REASON_TYPING_REGRESSION,
8584
right_label="returns_delta",
8685
)
8786
if typing_detail is not None:
8887
return "typing_coverage_delta", typing_detail
89-
if trimmed.startswith("Docstring coverage regressed vs metrics baseline: delta="):
88+
if trimmed.startswith(gate_msgs.GATE_REASON_DOCSTRING_REGRESSION):
9089
return "docstring_coverage_delta", trimmed.rsplit("=", maxsplit=1)[1]
91-
if trimmed.startswith("Public API breaking changes vs metrics baseline: "):
92-
return "api_breaking_changes", tail(
93-
"Public API breaking changes vs metrics baseline: "
94-
)
90+
if trimmed.startswith(gate_msgs.GATE_REASON_API_BREAKING):
91+
return "api_breaking_changes", tail(gate_msgs.GATE_REASON_API_BREAKING)
9592
coverage_detail = _parse_two_part_metric_detail(
9693
trimmed,
97-
prefix="Coverage hotspots detected: ",
94+
prefix=gate_msgs.GATE_REASON_COVERAGE_HOTSPOTS,
9895
right_label="threshold",
9996
)
10097
if coverage_detail is not None:
10198
return "coverage_hotspots", coverage_detail
10299

103-
if trimmed.startswith("Dependency cycles detected: "):
104-
return "dependency_cycles", tail("Dependency cycles detected: ").replace(
105-
" cycle(s)", ""
100+
if trimmed.startswith(gate_msgs.GATE_REASON_CYCLES_DETECTED):
101+
return "dependency_cycles", tail(gate_msgs.GATE_REASON_CYCLES_DETECTED).replace(
102+
gate_msgs.GATE_SUFFIX_CYCLES, ""
106103
)
107104

108-
if trimmed.startswith("Dead code detected (high confidence): "):
105+
if trimmed.startswith(gate_msgs.GATE_REASON_DEAD_CODE_DETECTED):
109106
return "dead_code_items", tail(
110-
"Dead code detected (high confidence): "
111-
).replace(" item(s)", "")
107+
gate_msgs.GATE_REASON_DEAD_CODE_DETECTED
108+
).replace(gate_msgs.GATE_SUFFIX_ITEMS, "")
112109

113110
threshold_prefixes: tuple[tuple[str, str], ...] = (
114-
("Complexity threshold exceeded: ", "complexity_max"),
115-
("Coupling threshold exceeded: ", "coupling_max"),
116-
("Cohesion threshold exceeded: ", "cohesion_max"),
117-
("Health score below threshold: ", "health_score"),
118-
("Typing coverage below threshold: ", "typing_coverage"),
119-
("Docstring coverage below threshold: ", "docstring_coverage"),
111+
(gate_msgs.GATE_REASON_COMPLEXITY_THRESHOLD, "complexity_max"),
112+
(gate_msgs.GATE_REASON_COUPLING_THRESHOLD, "coupling_max"),
113+
(gate_msgs.GATE_REASON_COHESION_THRESHOLD, "cohesion_max"),
114+
(gate_msgs.GATE_REASON_HEALTH_THRESHOLD, "health_score"),
115+
(gate_msgs.GATE_REASON_TYPING_THRESHOLD, "typing_coverage"),
116+
(gate_msgs.GATE_REASON_DOCSTRING_THRESHOLD, "docstring_coverage"),
120117
)
121118
for prefix, kind in threshold_prefixes:
122119
threshold_detail = _parse_two_part_metric_detail(
@@ -203,7 +200,11 @@ def print_gating_failure_block(
203200
entries: tuple[tuple[str, object], ...] | list[tuple[str, object]],
204201
args: _GatingArgs,
205202
) -> None:
206-
console.print(f"\n\u2717 GATING FAILURE [{code}]", style="bold red", markup=False)
203+
console.print(
204+
f"\n\u2717 {gate_msgs.GATE_FAILURE_HEADER.format(code=code)}",
205+
style="bold red",
206+
markup=False,
207+
)
207208
normalized_entries = [("policy", policy_context(args=args, gate_kind=code))]
208209
normalized_entries.extend((key, str(value)) for key, value in entries)
209210
width = max(len(key) for key, _ in normalized_entries)

0 commit comments

Comments
 (0)