clean-code-tools 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +66 -0
  2. package/configs/eslint.clean-code.recommended.mjs +211 -0
  3. package/configs/python.clean-code.pyproject.toml +143 -0
  4. package/data/clean-code-patterns.jsonl +264 -0
  5. package/data/vector-record.schema.json +77 -0
  6. package/docs/README.md +29 -0
  7. package/docs/eslint-custom-rules.md +74 -0
  8. package/docs/eslint-recommended-config.md +87 -0
  9. package/docs/fastmcp-local-server.md +104 -0
  10. package/docs/publishing.md +125 -0
  11. package/docs/python-lint-recommended-config.md +57 -0
  12. package/docs/python-pylint-custom-rules.md +77 -0
  13. package/docs/semantic-weaviate.md +80 -0
  14. package/docs/static-trigger-semantic-review.md +97 -0
  15. package/evals/clean-code-retrieval.jsonl +13 -0
  16. package/ops/dev/weaviate/README.md +34 -0
  17. package/ops/dev/weaviate/compose.yaml +34 -0
  18. package/ops/dev/weaviate/smoke.sh +28 -0
  19. package/package.json +96 -0
  20. package/pyproject.toml +303 -0
  21. package/sample-apps/README.md +40 -0
  22. package/sample-apps/python-app/pyproject.toml +113 -0
  23. package/sample-apps/python-app/src/clean_pricing.py +10 -0
  24. package/sample-apps/python-app/src/smelly_pricing.py +8 -0
  25. package/sample-apps/ts-backend/eslint.config.mjs +3 -0
  26. package/sample-apps/ts-backend/package.json +18 -0
  27. package/sample-apps/ts-backend/src/clean-handler.ts +19 -0
  28. package/sample-apps/ts-backend/src/smelly-handler.ts +29 -0
  29. package/sample-apps/ts-backend/tsconfig.json +9 -0
  30. package/sample-apps/ts-frontend/eslint.config.mjs +3 -0
  31. package/sample-apps/ts-frontend/package.json +18 -0
  32. package/sample-apps/ts-frontend/src/CleanWidget.tsx +18 -0
  33. package/sample-apps/ts-frontend/src/SmellyWidget.tsx +27 -0
  34. package/sample-apps/ts-frontend/tsconfig.json +10 -0
  35. package/scripts/_mcp_app.py +21 -0
  36. package/scripts/check_clean_code_review_candidates.py +302 -0
  37. package/scripts/check_fastmcp_server.py +106 -0
  38. package/scripts/check_packages.py +137 -0
  39. package/scripts/check_python_config.py +130 -0
  40. package/scripts/check_repo_python_lint.py +46 -0
  41. package/scripts/check_retrieval_evals.py +132 -0
  42. package/scripts/check_sample_apps.py +169 -0
  43. package/scripts/check_semantic_search_tooling.py +102 -0
  44. package/scripts/clean_code_eslint_triggers.py +272 -0
  45. package/scripts/clean_code_mcp_server.py +7 -0
  46. package/scripts/clean_code_python_triggers.py +318 -0
  47. package/scripts/clean_code_review_candidates.py +291 -0
  48. package/scripts/clean_code_review_io.py +36 -0
  49. package/scripts/clean_code_review_models.py +43 -0
  50. package/scripts/clean_code_semantic.py +27 -0
  51. package/scripts/set_package_versions.py +82 -0
  52. package/scripts/weaviate_ingest_clean_code.py +44 -0
  53. package/scripts/weaviate_search_clean_code.py +51 -0
  54. package/skills/clean-code-mcp-reviewer/SKILL.md +209 -0
  55. package/skills/clean-code-mcp-reviewer/evals/evals.json +30 -0
  56. package/src/js/eslint-plugin-clean-code.mjs +758 -0
  57. package/src/python/clean_code_tools_pylint/__init__.py +14 -0
  58. package/src/python/clean_code_tools_pylint/ast_checker.py +122 -0
  59. package/src/python/clean_code_tools_pylint/comments.py +83 -0
  60. package/src/python/clean_code_tools_pylint/helpers.py +196 -0
  61. package/src/python/mcp_server/__init__.py +1 -0
  62. package/src/python/mcp_server/corpus.py +160 -0
  63. package/src/python/mcp_server/markdown.py +126 -0
  64. package/src/python/mcp_server/models.py +73 -0
  65. package/src/python/mcp_server/ranking.py +125 -0
  66. package/src/python/mcp_server/ranking_scoring.py +232 -0
  67. package/src/python/mcp_server/semantic.py +192 -0
  68. package/src/python/mcp_server/server.py +235 -0
  69. package/src/python/mcp_server/server_payloads.py +83 -0
  70. package/src/python/mcp_server/text.py +104 -0
  71. package/src/python/mcp_server/utils/__init__.py +1 -0
  72. package/src/python/mcp_server/utils/httpx_loader.py +14 -0
  73. package/src/python/mcp_server/utils/increment.py +7 -0
  74. package/src/python/mcp_server/utils/sha256_text.py +8 -0
  75. package/src/python/mcp_server/utils/unique_strings.py +15 -0
  76. package/src/python/mcp_server/weaviate.py +182 -0
  77. package/uv.lock +2012 -0
@@ -0,0 +1,40 @@
1
+ # Sample Apps
2
+
3
+ These apps exercise the clean-code lint presets against realistic small examples.
4
+
5
+ ## Python
6
+
7
+ ```bash
8
+ python -m pip install "ruff>=0.15.0" "pylint>=4.0.0"
9
+ cd sample-apps/python-app
10
+ ruff check src/clean_pricing.py
11
+ pylint --rcfile=pyproject.toml src/clean_pricing.py
12
+ ruff check src/smelly_pricing.py
13
+ pylint --rcfile=pyproject.toml src/smelly_pricing.py
14
+ ```
15
+
16
+ The clean file should pass. The smelly file should report TODO tracking, commented-out code, unused arguments, and too many arguments.
17
+
18
+ ## TypeScript Backend
19
+
20
+ ```bash
21
+ cd sample-apps/ts-backend
22
+ bun install
23
+ bun run lint:clean
24
+ bun run lint:smelly
25
+ ```
26
+
27
+ The clean handler should pass. The smelly handler should report untracked TODOs, commented-out code, boolean flag arguments, policy literals, train-wreck navigation, and null usage.
28
+
29
+ ## TypeScript Frontend
30
+
31
+ ```bash
32
+ cd sample-apps/ts-frontend
33
+ bun install
34
+ bun run lint:clean
35
+ bun run lint:smelly
36
+ ```
37
+
38
+ The clean widget should pass. The smelly widget should report untracked TODOs, commented-out code, train-wreck navigation, policy literals, output argument mutation, null usage, and boolean flag arguments.
39
+
40
+ The TypeScript samples import the preset through `clean-code-tools/configs/eslint.clean-code.recommended.mjs`, so they exercise the package export path rather than a repository-relative config path.
@@ -0,0 +1,113 @@
1
+ [tool.ruff]
2
+ target-version = "py311"
3
+ line-length = 100
4
+ extend-exclude = [
5
+ ".venv",
6
+ "__pycache__",
7
+ "build",
8
+ "dist",
9
+ ]
10
+
11
+ [tool.ruff.lint]
12
+ select = [
13
+ "E",
14
+ "F",
15
+ "I",
16
+ "UP",
17
+ "B",
18
+ "C4",
19
+ "SIM",
20
+ "RET",
21
+ "ARG",
22
+ "ERA",
23
+ "TD",
24
+ "PLR2004",
25
+ "RUF",
26
+ ]
27
+ ignore = [
28
+ "E501",
29
+ "TD001",
30
+ ]
31
+ fixable = [
32
+ "E",
33
+ "F",
34
+ "I",
35
+ "UP",
36
+ "B",
37
+ "C4",
38
+ "SIM",
39
+ "RET",
40
+ "ARG",
41
+ "RUF",
42
+ ]
43
+ unfixable = [
44
+ "ERA",
45
+ "TD",
46
+ "PLR2004",
47
+ ]
48
+
49
+ [tool.ruff.lint.per-file-ignores]
50
+ "tests/**/*.py" = [
51
+ "ARG001",
52
+ "ARG002",
53
+ "PLR2004",
54
+ "S101",
55
+ ]
56
+
57
+ [tool.pylint.main]
58
+ py-version = "3.11"
59
+ jobs = 0
60
+ recursive = true
61
+ load-plugins = [
62
+ "clean_code_tools_pylint",
63
+ ]
64
+
65
+ [tool.pylint."messages control"]
66
+ disable = [
67
+ "all",
68
+ ]
69
+ enable = [
70
+ "cyclic-import",
71
+ "clean-code-boolean-flag-argument",
72
+ "clean-code-business-policy-literal",
73
+ "clean-code-commented-out-code",
74
+ "clean-code-noisy-comment",
75
+ "clean-code-output-argument-mutation",
76
+ "clean-code-redundant-comment",
77
+ "clean-code-todo-format",
78
+ "clean-code-train-wreck",
79
+ "duplicate-code",
80
+ "import-error",
81
+ "too-many-ancestors",
82
+ "too-many-arguments",
83
+ "too-many-branches",
84
+ "too-many-instance-attributes",
85
+ "too-many-lines",
86
+ "too-many-locals",
87
+ "too-many-nested-blocks",
88
+ "too-many-public-methods",
89
+ "too-many-return-statements",
90
+ "too-many-statements",
91
+ "too-many-boolean-expressions",
92
+ "too-few-public-methods",
93
+ ]
94
+
95
+ [tool.pylint.design]
96
+ max-args = 5
97
+ max-attributes = 7
98
+ max-bool-expr = 5
99
+ max-branches = 12
100
+ max-locals = 15
101
+ max-nested-blocks = 4
102
+ max-public-methods = 12
103
+ max-returns = 6
104
+ max-statements = 40
105
+ max-parents = 7
106
+ min-public-methods = 1
107
+
108
+ [tool.pylint.format]
109
+ max-line-length = 100
110
+ max-module-lines = 300
111
+
112
+ [tool.pylint.reports]
113
+ score = false
@@ -0,0 +1,10 @@
1
+ MAX_RETRY_ATTEMPTS = 5
2
+ PENDING_STATUS = "pending"
3
+
4
+
5
+ def can_retry_payment(status: str, failed_attempts: int) -> bool:
6
+ return status == PENDING_STATUS and failed_attempts < MAX_RETRY_ATTEMPTS
7
+
8
+
9
+ def calculate_total(subtotal_cents: int, tax_cents: int) -> int:
10
+ return subtotal_cents + tax_cents
@@ -0,0 +1,8 @@
1
+ # TODO clean this up
2
+ # old_total = calculate_total(order)
3
+
4
+
5
+ def calculate_total(order, include_tax, dry_run, retry, verbose, mode):
6
+ if order.status == "pending":
7
+ return 5
8
+ return 0
@@ -0,0 +1,3 @@
1
+ import cleanCode from "clean-code-tools/configs/eslint.clean-code.recommended.mjs";
2
+
3
+ export default cleanCode;
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "sample-ts-backend",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "lint:clean": "eslint src/clean-handler.ts --max-warnings 0",
7
+ "lint:smelly": "eslint src/smelly-handler.ts"
8
+ },
9
+ "devDependencies": {
10
+ "@eslint/js": "^10.0.0",
11
+ "clean-code-tools": "file:../..",
12
+ "eslint": "^10.4.0",
13
+ "eslint-plugin-sonarjs": "^4.0.0",
14
+ "eslint-plugin-unicorn": "^69.0.0",
15
+ "typescript": "^5.0.0",
16
+ "typescript-eslint": "^8.0.0"
17
+ }
18
+ }
@@ -0,0 +1,19 @@
1
+ const MAX_LOGIN_ATTEMPTS = 5;
2
+ const PAYMENT_FAILED_STATUS = "payment_failed";
3
+
4
+ interface Payment {
5
+ readonly failedAttempts: number;
6
+ readonly status: string;
7
+ }
8
+
9
+ function canRetryPayment(payment: Payment): boolean {
10
+ return payment.status === PAYMENT_FAILED_STATUS && payment.failedAttempts < MAX_LOGIN_ATTEMPTS;
11
+ }
12
+
13
+ export function planPaymentRetry(payment: Payment): string {
14
+ if (canRetryPayment(payment)) {
15
+ return "retry";
16
+ }
17
+
18
+ return "skip";
19
+ }
@@ -0,0 +1,29 @@
1
+ // TODO clean this up
2
+ // await publishReceipt(receipt);
3
+
4
+ type Payment = {
5
+ status: string;
6
+ customer: {
7
+ account: {
8
+ billing: {
9
+ email: string;
10
+ };
11
+ };
12
+ };
13
+ };
14
+
15
+ function sendPaymentEmail(payment: Payment, dryRun: boolean): null {
16
+ if (payment.status === "payment_failed") {
17
+ sendEmail(payment.customer.account.billing.email, true);
18
+ }
19
+
20
+ return null;
21
+ }
22
+
23
+ function sendEmail(email: string, urgent: boolean): void {
24
+ if (urgent) {
25
+ console.log(email);
26
+ }
27
+ }
28
+
29
+ sendPaymentEmail({ status: "payment_failed", customer: { account: { billing: { email: "a@b.test" } } } }, false);
@@ -0,0 +1,9 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "ESNext",
4
+ "moduleResolution": "Bundler",
5
+ "strict": true,
6
+ "target": "ES2022"
7
+ },
8
+ "include": ["src/**/*.ts"]
9
+ }
@@ -0,0 +1,3 @@
1
+ import cleanCode from "clean-code-tools/configs/eslint.clean-code.recommended.mjs";
2
+
3
+ export default cleanCode;
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "sample-ts-frontend",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "lint:clean": "eslint src/CleanWidget.tsx --max-warnings 0",
7
+ "lint:smelly": "eslint src/SmellyWidget.tsx"
8
+ },
9
+ "devDependencies": {
10
+ "@eslint/js": "^10.0.0",
11
+ "clean-code-tools": "file:../..",
12
+ "eslint": "^10.4.0",
13
+ "eslint-plugin-sonarjs": "^4.0.0",
14
+ "eslint-plugin-unicorn": "^69.0.0",
15
+ "typescript": "^5.0.0",
16
+ "typescript-eslint": "^8.0.0"
17
+ }
18
+ }
@@ -0,0 +1,18 @@
1
+ const MAX_VISIBLE_ITEMS = 5;
2
+
3
+ interface DashboardWidgetProps {
4
+ readonly items: readonly string[];
5
+ readonly shouldShowEmptyState: boolean;
6
+ }
7
+
8
+ function visibleItems(items: readonly string[]): readonly string[] {
9
+ return items.slice(0, MAX_VISIBLE_ITEMS);
10
+ }
11
+
12
+ export function DashboardWidget({ items, shouldShowEmptyState }: DashboardWidgetProps): string {
13
+ if (items.length === 0 && shouldShowEmptyState) {
14
+ return "No items";
15
+ }
16
+
17
+ return visibleItems(items).join(", ");
18
+ }
@@ -0,0 +1,27 @@
1
+ // TODO fix widget
2
+ // return <OldWidget />;
3
+
4
+ type DashboardWidgetProps = {
5
+ items: string[];
6
+ user: {
7
+ account: {
8
+ plan: {
9
+ status: string;
10
+ };
11
+ };
12
+ };
13
+ };
14
+
15
+ function DashboardWidget(props: DashboardWidgetProps, compact: boolean): null {
16
+ if (props.user.account.plan.status === "active") {
17
+ props.items.push("bonus");
18
+ }
19
+
20
+ if (compact) {
21
+ return null;
22
+ }
23
+
24
+ return null;
25
+ }
26
+
27
+ DashboardWidget({ items: [], user: { account: { plan: { status: "active" } } } }, true);
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "jsx": "react-jsx",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "target": "ES2022"
8
+ },
9
+ "include": ["src/**/*.tsx"]
10
+ }
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import importlib
5
+ import sys
6
+ from pathlib import Path
7
+ from types import ModuleType
8
+
9
+ ROOT = Path(__file__).resolve().parents[1]
10
+ PYTHON_SRC = ROOT / "src" / "python"
11
+
12
+ if str(PYTHON_SRC) not in sys.path:
13
+ sys.path.insert(0, str(PYTHON_SRC))
14
+
15
+
16
+ def load_semantic_module() -> ModuleType:
17
+ return importlib.import_module("mcp_server.semantic")
18
+
19
+
20
+ def load_server_module() -> ModuleType:
21
+ return importlib.import_module("mcp_server.server")
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ import tomllib
6
+ from pathlib import Path
7
+
8
+ from clean_code_eslint_triggers import ESLINT_TRIGGERS
9
+ from clean_code_python_triggers import PYLINT_TRIGGERS, RUFF_TRIGGERS
10
+ from clean_code_review_candidates import (
11
+ candidate_payload,
12
+ eslint_candidates,
13
+ markdown_payload,
14
+ merge_candidates,
15
+ pylint_candidates,
16
+ ruff_candidates,
17
+ )
18
+
19
+ ROOT = Path(__file__).resolve().parents[1]
20
+ ESLINT_CONFIG = ROOT / "configs/eslint.clean-code.recommended.mjs"
21
+ PYTHON_CONFIG = ROOT / "configs/python.clean-code.pyproject.toml"
22
+ REPO_PYTHON_CONFIG = ROOT / "pyproject.toml"
23
+
24
+ EXPECTED_ESLINT_TRIGGERS = {
25
+ "@typescript-eslint/consistent-type-imports",
26
+ "@typescript-eslint/naming-convention",
27
+ "@typescript-eslint/no-confusing-void-expression",
28
+ "@typescript-eslint/no-magic-numbers",
29
+ "@typescript-eslint/no-unnecessary-condition",
30
+ "@typescript-eslint/no-unnecessary-type-assertion",
31
+ "@typescript-eslint/no-unused-vars",
32
+ "@typescript-eslint/prefer-nullish-coalescing",
33
+ "@typescript-eslint/strict-boolean-expressions",
34
+ "@typescript-eslint/switch-exhaustiveness-check",
35
+ "clean-code/no-boolean-flag-arguments",
36
+ "clean-code/no-business-policy-literals",
37
+ "clean-code/no-commented-out-code",
38
+ "clean-code/no-noisy-comments",
39
+ "clean-code/no-output-argument-mutation",
40
+ "clean-code/no-redundant-comment",
41
+ "clean-code/no-train-wrecks",
42
+ "clean-code/todo-format",
43
+ "complexity",
44
+ "max-depth",
45
+ "max-lines",
46
+ "max-lines-per-function",
47
+ "max-params",
48
+ "no-empty",
49
+ "no-negated-condition",
50
+ "no-nested-ternary",
51
+ "no-restricted-syntax",
52
+ "no-useless-return",
53
+ "sonarjs/cognitive-complexity",
54
+ "sonarjs/no-dead-store",
55
+ "sonarjs/no-duplicate-string",
56
+ "sonarjs/no-duplicated-branches",
57
+ "sonarjs/no-identical-conditions",
58
+ "sonarjs/no-identical-functions",
59
+ "sonarjs/no-inverted-boolean-check",
60
+ "unicorn/explicit-length-check",
61
+ "unicorn/no-negated-condition",
62
+ "unicorn/no-null",
63
+ }
64
+
65
+ ESLINT_SEMANTIC_EXCLUSIONS = {
66
+ "@typescript-eslint/no-floating-promises",
67
+ "@typescript-eslint/no-misused-promises",
68
+ "eqeqeq",
69
+ "spaced-comment",
70
+ }
71
+
72
+ EXPECTED_PYLINT_TRIGGERS = {
73
+ "clean-code-boolean-flag-argument",
74
+ "clean-code-business-policy-literal",
75
+ "clean-code-commented-out-code",
76
+ "clean-code-noisy-comment",
77
+ "clean-code-output-argument-mutation",
78
+ "clean-code-redundant-comment",
79
+ "clean-code-todo-format",
80
+ "clean-code-train-wreck",
81
+ "cyclic-import",
82
+ "duplicate-code",
83
+ "too-few-public-methods",
84
+ "too-many-ancestors",
85
+ "too-many-arguments",
86
+ "too-many-boolean-expressions",
87
+ "too-many-branches",
88
+ "too-many-instance-attributes",
89
+ "too-many-lines",
90
+ "too-many-locals",
91
+ "too-many-nested-blocks",
92
+ "too-many-public-methods",
93
+ "too-many-return-statements",
94
+ "too-many-statements",
95
+ }
96
+
97
+ PYLINT_SEMANTIC_EXCLUSIONS = {
98
+ "import-error",
99
+ }
100
+
101
+ EXPECTED_RUFF_TRIGGERS = {
102
+ "ARG001",
103
+ "ARG002",
104
+ "ERA001",
105
+ "F401",
106
+ "F841",
107
+ "PLR0911",
108
+ "PLR0912",
109
+ "PLR0913",
110
+ "PLR0914",
111
+ "PLR0915",
112
+ "PLR0916",
113
+ "PLR1702",
114
+ "PLR2004",
115
+ "RET505",
116
+ "RET506",
117
+ "RET507",
118
+ "RET508",
119
+ "SIM102",
120
+ "SIM103",
121
+ "SIM108",
122
+ "TD002",
123
+ "TD003",
124
+ }
125
+
126
+ RUFF_CURATED_SELECT_PREFIXES = {
127
+ "ARG",
128
+ "ERA",
129
+ "F",
130
+ "PLR2004",
131
+ "RET",
132
+ "SIM",
133
+ "TD",
134
+ }
135
+
136
+
137
+ def enabled_eslint_rules() -> set[str]:
138
+ source = ESLINT_CONFIG.read_text()
139
+ rule_pattern = re.compile(
140
+ r'^\s*(?:"(?P<quoted>[^"]+)"|(?P<bare>[A-Za-z][\w/-]*)):\s*'
141
+ r'(?:(?P<string>"(?:warn|error)")|\[\s*"(?P<array>warn|error)")',
142
+ re.MULTILINE,
143
+ )
144
+ return {
145
+ match.group("quoted") or match.group("bare")
146
+ for match in rule_pattern.finditer(source)
147
+ }
148
+
149
+
150
+ def python_lint_config(path: Path = PYTHON_CONFIG) -> dict:
151
+ return tomllib.loads(path.read_text())
152
+
153
+
154
+ def enabled_pylint_rules() -> set[str]:
155
+ return set(python_lint_config()["tool"]["pylint"]["messages control"]["enable"])
156
+
157
+
158
+ def selected_ruff_prefixes() -> set[str]:
159
+ return set(python_lint_config()["tool"]["ruff"]["lint"]["select"])
160
+
161
+
162
+ def repo_selected_ruff_prefixes() -> set[str]:
163
+ return set(python_lint_config(REPO_PYTHON_CONFIG)["tool"]["ruff"]["lint"]["select"])
164
+
165
+
166
+ def all_selected_ruff_prefixes() -> set[str]:
167
+ return selected_ruff_prefixes() | repo_selected_ruff_prefixes()
168
+
169
+
170
+ def ruff_code_is_selected(code: str, selected_prefixes: set[str]) -> bool:
171
+ return any(code.startswith(prefix) for prefix in selected_prefixes)
172
+
173
+
174
+ def main() -> None:
175
+ assert set(ESLINT_TRIGGERS) == EXPECTED_ESLINT_TRIGGERS
176
+ assert set(PYLINT_TRIGGERS) == EXPECTED_PYLINT_TRIGGERS
177
+ assert set(RUFF_TRIGGERS) == EXPECTED_RUFF_TRIGGERS
178
+ assert enabled_eslint_rules() <= set(ESLINT_TRIGGERS) | ESLINT_SEMANTIC_EXCLUSIONS
179
+ assert enabled_pylint_rules() <= set(PYLINT_TRIGGERS) | PYLINT_SEMANTIC_EXCLUSIONS
180
+ assert selected_ruff_prefixes() >= RUFF_CURATED_SELECT_PREFIXES
181
+ assert all(ruff_code_is_selected(code, all_selected_ruff_prefixes()) for code in RUFF_TRIGGERS)
182
+
183
+ eslint_results = [
184
+ {
185
+ "filePath": "/repo/src/pricing.ts",
186
+ "messages": [
187
+ {
188
+ "ruleId": "max-lines-per-function",
189
+ "message": "Function has too many lines.",
190
+ "line": 12,
191
+ "column": 1,
192
+ },
193
+ {
194
+ "ruleId": "clean-code/no-boolean-flag-arguments",
195
+ "message": "Boolean literal selects behavior.",
196
+ "line": 44,
197
+ "column": 22,
198
+ },
199
+ {
200
+ "ruleId": "semi",
201
+ "message": "Formatting-only rules are not semantic triggers.",
202
+ "line": 45,
203
+ "column": 1,
204
+ },
205
+ ],
206
+ }
207
+ ]
208
+ pylint_results = [
209
+ {
210
+ "path": "sample-apps/python-app/src/smelly_pricing.py",
211
+ "obj": "calculate_total",
212
+ "line": 5,
213
+ "column": 0,
214
+ "symbol": "too-many-arguments",
215
+ "message": "Too many arguments (6/5)",
216
+ },
217
+ {
218
+ "path": "sample-apps/python-app/src/smelly_pricing.py",
219
+ "obj": "calculate_total",
220
+ "line": 7,
221
+ "column": 4,
222
+ "symbol": "clean-code-output-argument-mutation",
223
+ "message": "Avoid mutating parameter 'order' as an output argument.",
224
+ },
225
+ {
226
+ "path": "sample-apps/python-app/src/smelly_pricing.py",
227
+ "obj": "calculate_total",
228
+ "line": 5,
229
+ "column": 0,
230
+ "symbol": "missing-function-docstring",
231
+ "message": "Not part of semantic trigger handoff.",
232
+ },
233
+ ]
234
+ ruff_results = [
235
+ {
236
+ "filename": "sample-apps/python-app/src/smelly_pricing.py",
237
+ "location": {"row": 12, "column": 8},
238
+ "code": "PLR2004",
239
+ "message": "Magic value used in comparison.",
240
+ },
241
+ {
242
+ "filename": "sample-apps/python-app/src/smelly_pricing.py",
243
+ "location": {"row": 18, "column": 5},
244
+ "code": "TD003",
245
+ "message": "Missing issue link for this TODO.",
246
+ },
247
+ {
248
+ "filename": "sample-apps/python-app/src/smelly_pricing.py",
249
+ "location": {"row": 20, "column": 1},
250
+ "code": "E501",
251
+ "message": "Formatting-only rules are not semantic triggers.",
252
+ },
253
+ ]
254
+
255
+ candidates = merge_candidates(
256
+ [
257
+ *eslint_candidates(eslint_results),
258
+ *pylint_candidates(pylint_results),
259
+ *ruff_candidates(ruff_results),
260
+ ]
261
+ )
262
+ payload = candidate_payload(candidates)
263
+
264
+ assert payload["schema"] == "clean-code-review-candidates/v1"
265
+ assert payload["candidate_count"] == 6
266
+
267
+ assert {candidate["skill"] for candidate in payload["candidates"]} == {
268
+ "clean-code-mcp-reviewer"
269
+ }
270
+
271
+ typescript_candidates = [
272
+ candidate for candidate in payload["candidates"] if candidate["language"] == "typescript"
273
+ ]
274
+ python_candidates = [
275
+ candidate for candidate in payload["candidates"] if candidate["language"] == "python"
276
+ ]
277
+ assert len(typescript_candidates) == 2
278
+ assert {candidate["anchor"] for candidate in typescript_candidates} == {"line 12", "line 44"}
279
+ assert any(candidate["symbol"] == "calculate_total" for candidate in python_candidates)
280
+ assert any("too many arguments" in candidate["mcp_queries"][0] for candidate in python_candidates)
281
+ assert any(
282
+ trigger["tool"] == "pylint" and trigger["rule"] == "clean-code-output-argument-mutation"
283
+ for candidate in python_candidates
284
+ for trigger in candidate["triggers"]
285
+ )
286
+ assert any(
287
+ trigger["tool"] == "ruff" and trigger["rule"] == "TD003"
288
+ for candidate in python_candidates
289
+ for trigger in candidate["triggers"]
290
+ )
291
+
292
+ markdown = markdown_payload(candidates)
293
+ assert "Clean-Code Semantic Review Candidates" in markdown
294
+ assert "clean-code/no-boolean-flag-arguments" in markdown
295
+ assert "calculate_total" in markdown
296
+ assert "ruff/PLR2004" in markdown
297
+
298
+ print("clean_code_review_candidates_check=ok")
299
+
300
+
301
+ if __name__ == "__main__":
302
+ main()