claude-dev-env 2.31.0 → 2.32.0

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 (80) hide show
  1. package/bin/install.test.mjs +11 -3
  2. package/docs/CODE_RULES.md +2 -1
  3. package/hooks/blocking/bash_post_call_dispatcher.py +70 -0
  4. package/hooks/blocking/bash_pre_tool_use_dispatcher.py +8 -21
  5. package/hooks/blocking/code_rules_duplicate_body.py +280 -102
  6. package/hooks/blocking/code_rules_enforcer.py +32 -14
  7. package/hooks/blocking/code_rules_enforcer_test_support.py +39 -0
  8. package/hooks/blocking/code_rules_shared.py +64 -14
  9. package/hooks/blocking/pr_description_writer_gate.py +18 -25
  10. package/hooks/blocking/shell_substitution_blocker.py +175 -8
  11. package/hooks/blocking/stop_dispatcher.py +6 -9
  12. package/hooks/blocking/tdd_enforcer.py +11 -5
  13. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +7 -0
  14. package/hooks/blocking/tdd_enforcer_parts/content_hash_store.py +141 -23
  15. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_hash_store.py +114 -1
  16. package/hooks/blocking/test_bash_post_call_dispatcher.py +112 -0
  17. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +12 -1
  18. package/hooks/blocking/test_code_rules_enforcer_ephemeral_repository_root.py +118 -0
  19. package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +251 -0
  20. package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +22 -0
  21. package/hooks/blocking/test_pr_description_writer_gate.py +21 -3
  22. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +14 -2
  23. package/hooks/blocking/test_shell_substitution_blocker.py +105 -0
  24. package/hooks/blocking/test_state_description_blocker.py +84 -53
  25. package/hooks/blocking/test_tdd_enforcer.py +131 -1
  26. package/hooks/hooks.json +2 -2
  27. package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +22 -0
  28. package/hooks/hooks_constants/hosted_hook_runner.py +53 -0
  29. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +9 -2
  30. package/hooks/hooks_constants/pr_description_writer_gate_constants.py +34 -0
  31. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +10 -0
  32. package/hooks/hooks_constants/state_file_lock.py +76 -0
  33. package/hooks/hooks_constants/subprocess_budget_completeness_content.py +2 -2
  34. package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +27 -0
  35. package/hooks/hooks_constants/test_failure_recorder_constants.py +35 -0
  36. package/hooks/hooks_constants/test_hosted_hook_runner.py +55 -1
  37. package/hooks/hooks_constants/test_multi_edit_reconstruction.py +1 -0
  38. package/hooks/hooks_constants/test_subprocess_budget_completeness_content.py +33 -0
  39. package/hooks/observability/pr_description_writer_spawn_tracker.py +4 -12
  40. package/hooks/observability/session_file_edit_tracker.py +2 -55
  41. package/hooks/observability/test_failure_recorder.py +219 -0
  42. package/hooks/observability/test_pr_description_writer_spawn_tracker.py +6 -2
  43. package/hooks/observability/test_test_failure_recorder.py +173 -0
  44. package/hooks/validators/abbreviation_checks.py +3 -3
  45. package/hooks/validators/code_quality_checks.py +4 -4
  46. package/hooks/validators/config/baseline_identity_constants.py +12 -0
  47. package/hooks/validators/config/directory_exemption_constants.py +42 -8
  48. package/hooks/validators/config/system_temporary_roots_constants.py +15 -0
  49. package/hooks/validators/config/validator_base_constants.py +9 -0
  50. package/hooks/validators/fast_save_validators.py +23 -80
  51. package/hooks/validators/mypy_integration.py +10 -36
  52. package/hooks/validators/pr_reference_checks.py +2 -2
  53. package/hooks/validators/project_roots.py +58 -0
  54. package/hooks/validators/python_antipattern_checks.py +3 -3
  55. package/hooks/validators/python_style_checks.py +3 -2
  56. package/hooks/validators/react_checks.py +2 -9
  57. package/hooks/validators/run_all_validators.py +247 -148
  58. package/hooks/validators/security_checks.py +3 -3
  59. package/hooks/validators/system_temporary_roots.py +1 -1
  60. package/hooks/validators/test_directory_exemption_constants.py +30 -24
  61. package/hooks/validators/test_fast_save_validators.py +16 -16
  62. package/hooks/validators/test_hook_timing_harness.py +10 -1
  63. package/hooks/validators/test_run_all_validators.py +22 -0
  64. package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +249 -0
  65. package/hooks/validators/test_run_all_validators_pretooluse.py +161 -0
  66. package/hooks/validators/test_run_all_validators_reporting.py +66 -0
  67. package/hooks/validators/test_run_all_validators_repository_root.py +202 -0
  68. package/hooks/validators/test_validator_base.py +51 -2
  69. package/hooks/validators/todo_checks.py +2 -2
  70. package/hooks/validators/type_safety_checks.py +3 -3
  71. package/hooks/validators/useless_test_checks.py +3 -3
  72. package/hooks/validators/validator_base.py +73 -1
  73. package/package.json +1 -1
  74. package/rules/shell-invocation.md +1 -1
  75. package/rules/testing.md +4 -0
  76. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +19 -0
  77. package/scripts/dev_env_scripts_constants/shared_tree_constants.py +17 -0
  78. package/scripts/grok_headless_runner.py +15 -12
  79. package/scripts/shared_tree_paths.py +104 -0
  80. package/scripts/test_shared_tree_paths.py +173 -0
@@ -45,6 +45,7 @@ from .test_safety_checks import (
45
45
  from .todo_checks import validate_file as validate_todo_file
46
46
  from .type_safety_checks import validate_file as validate_type_safety_file
47
47
  from .useless_test_checks import validate_file as validate_useless_test_file
48
+ from .validator_base import ValidatorResult
48
49
 
49
50
  FilePredicate = Callable[[Path], bool]
50
51
  FileViolationLineCollector = Callable[[Path], "list[str]"]
@@ -76,21 +77,6 @@ def _violation_lines_from(raw_validate_file: _RawFileValidator) -> FileViolation
76
77
  return _collect
77
78
 
78
79
 
79
- @dataclass(frozen=True)
80
- class FastSaveCheckOutcome:
81
- """One save-path check's finding.
82
-
83
- The caller wraps this into its own reporting type. This module never
84
- imports that type back, so no circular import exists between the two
85
- modules.
86
- """
87
-
88
- display_name: str
89
- checks: str
90
- is_clean: bool
91
- violation_report: str
92
-
93
-
94
80
  def _is_python_file(file_path: Path) -> bool:
95
81
  return file_path.suffix == PYTHON_SOURCE_SUFFIX
96
82
 
@@ -122,15 +108,10 @@ def _collect_test_safety_violation_lines(file_path: Path) -> "list[str]":
122
108
 
123
109
 
124
110
  def _collect_react_violation_lines(file_path: Path) -> "list[str]":
125
- """Adapt react_checks's whole-batch ``check_no_class_components`` to one file.
126
-
127
- react_checks.Violation carries no ``__str__`` override, so this formats
128
- each violation the same way react_checks's own ``main`` prints one.
129
- """
130
- all_violations = check_no_class_components([str(file_path)])
111
+ """Adapt react_checks's whole-batch ``check_no_class_components`` to one file."""
131
112
  return [
132
- f"{each_violation.file}:{each_violation.line}: {each_violation.message}"
133
- for each_violation in all_violations
113
+ str(each_violation)
114
+ for each_violation in check_no_class_components([str(file_path)])
134
115
  ]
135
116
 
136
117
 
@@ -145,11 +126,12 @@ class _FastSaveValidatorSpecification:
145
126
  no_matching_files_message: str
146
127
 
147
128
 
148
- def _python_naming_and_syntax_specifications() -> (
149
- "list[_FastSaveValidatorSpecification]"
129
+ def _all_fast_save_validator_specifications() -> (
130
+ "tuple[_FastSaveValidatorSpecification, ...]"
150
131
  ):
151
- """Return the save-path checks for Python naming, style, and constants."""
152
- return [
132
+ """Return the save-path roster: every non-Mypy, non-Ruff file-scoped check."""
133
+ return (
134
+ # Python naming, style, and constants.
153
135
  _FastSaveValidatorSpecification(
154
136
  "Python Style",
155
137
  "1,2,3,4",
@@ -171,14 +153,7 @@ def _python_naming_and_syntax_specifications() -> (
171
153
  _violation_lines_from(validate_magic_literal_file),
172
154
  NO_PYTHON_FILES_MESSAGE,
173
155
  ),
174
- ]
175
-
176
-
177
- def _python_quality_and_tracking_specifications() -> (
178
- "list[_FastSaveValidatorSpecification]"
179
- ):
180
- """Return the save-path checks for Python code quality and TODO tracking."""
181
- return [
156
+ # Python code quality and TODO tracking.
182
157
  _FastSaveValidatorSpecification(
183
158
  "TODO Tracking",
184
159
  "36",
@@ -200,14 +175,7 @@ def _python_quality_and_tracking_specifications() -> (
200
175
  _violation_lines_from(validate_code_quality_file),
201
176
  NO_PYTHON_FILES_MESSAGE,
202
177
  ),
203
- ]
204
-
205
-
206
- def _python_safety_and_typing_specifications() -> (
207
- "list[_FastSaveValidatorSpecification]"
208
- ):
209
- """Return the save-path checks for Python anti-patterns and typing."""
210
- return [
178
+ # Python anti-patterns and typing.
211
179
  _FastSaveValidatorSpecification(
212
180
  "Python Anti-patterns",
213
181
  "33,34,35",
@@ -222,12 +190,7 @@ def _python_safety_and_typing_specifications() -> (
222
190
  _violation_lines_from(validate_type_safety_file),
223
191
  NO_PYTHON_FILES_MESSAGE,
224
192
  ),
225
- ]
226
-
227
-
228
- def _test_scoped_fast_save_specifications() -> "list[_FastSaveValidatorSpecification]":
229
- """Return the save-path checks scoped to a ``test_*.py``-shaped file."""
230
- return [
193
+ # Scoped to a test_*.py-shaped file.
231
194
  _FastSaveValidatorSpecification(
232
195
  "Test Safety",
233
196
  "11,21",
@@ -242,14 +205,7 @@ def _test_scoped_fast_save_specifications() -> "list[_FastSaveValidatorSpecifica
242
205
  _violation_lines_from(validate_useless_test_file),
243
206
  NO_TEST_FILES_MESSAGE,
244
207
  ),
245
- ]
246
-
247
-
248
- def _cross_language_fast_save_specifications() -> (
249
- "list[_FastSaveValidatorSpecification]"
250
- ):
251
- """Return the save-path checks that read a non-Python or multi-language file."""
252
- return [
208
+ # Non-Python or multi-language files.
253
209
  _FastSaveValidatorSpecification(
254
210
  "React",
255
211
  "17",
@@ -264,39 +220,26 @@ def _cross_language_fast_save_specifications() -> (
264
220
  _violation_lines_from(validate_pr_reference_file),
265
221
  NO_CODE_REFERENCE_FILES_MESSAGE,
266
222
  ),
267
- ]
268
-
269
-
270
- def _all_fast_save_validator_specifications() -> (
271
- "list[_FastSaveValidatorSpecification]"
272
- ):
273
- """Return the save-path roster: every non-Mypy, non-Ruff file-scoped check."""
274
- return [
275
- *_python_naming_and_syntax_specifications(),
276
- *_python_quality_and_tracking_specifications(),
277
- *_python_safety_and_typing_specifications(),
278
- *_test_scoped_fast_save_specifications(),
279
- *_cross_language_fast_save_specifications(),
280
- ]
223
+ )
281
224
 
282
225
 
283
226
  def _fast_save_outcome(
284
227
  specification: _FastSaveValidatorSpecification,
285
228
  is_clean: bool,
286
229
  violation_report: str,
287
- ) -> FastSaveCheckOutcome:
288
- """Build one outcome from a specification's display name and check numbers."""
289
- return FastSaveCheckOutcome(
290
- display_name=specification.display_name,
230
+ ) -> ValidatorResult:
231
+ """Build one result from a specification's display name and check numbers."""
232
+ return ValidatorResult(
233
+ name=specification.display_name,
291
234
  checks=specification.checks,
292
- is_clean=is_clean,
293
- violation_report=violation_report,
235
+ passed=is_clean,
236
+ output=violation_report,
294
237
  )
295
238
 
296
239
 
297
240
  def _run_specification(
298
241
  specification: _FastSaveValidatorSpecification, files: "list[Path]"
299
- ) -> FastSaveCheckOutcome:
242
+ ) -> ValidatorResult:
300
243
  """Run one validator's violation collector in-process over its matching files."""
301
244
  matching_files = [
302
245
  each_file for each_file in files if specification.applies_to_file(each_file)
@@ -316,14 +259,14 @@ def _run_specification(
316
259
  return _fast_save_outcome(specification, False, violation_report)
317
260
 
318
261
 
319
- def run_fast_save_validators(files: "list[Path]") -> "list[FastSaveCheckOutcome]":
262
+ def run_fast_save_validators(files: "list[Path]") -> "list[ValidatorResult]":
320
263
  """Run every save-path validator in-process, skipping Mypy and Ruff.
321
264
 
322
265
  Args:
323
266
  files: The files under validation -- one reconstructed file in gate mode.
324
267
 
325
268
  Returns:
326
- One outcome per in-process validator, in roster order. The caller adds
269
+ One result per in-process validator, in roster order. The caller adds
327
270
  Ruff and never adds Mypy.
328
271
  """
329
272
  return [
@@ -13,28 +13,26 @@ _validators_directory = str(Path(__file__).resolve().parent)
13
13
  _hooks_directory = str(Path(__file__).resolve().parent.parent)
14
14
 
15
15
  try:
16
+ from project_roots import enclosing_project_root
16
17
  from pyproject_config_discovery import (
17
- ancestor_directories,
18
+ ancestor_directories, # noqa: F401 re-exported for validation/mypy_validator
18
19
  find_pyproject_configuring_tool,
19
20
  )
20
- from system_temporary_roots import enclosing_system_temporary_root
21
21
  except ModuleNotFoundError:
22
22
  if _validators_directory not in sys.path:
23
23
  sys.path.insert(0, _validators_directory)
24
+ from project_roots import enclosing_project_root
24
25
  from pyproject_config_discovery import (
25
- ancestor_directories,
26
+ ancestor_directories, # noqa: F401 re-exported for validation/mypy_validator
26
27
  find_pyproject_configuring_tool,
27
28
  )
28
- from system_temporary_roots import enclosing_system_temporary_root
29
29
 
30
30
  try:
31
31
  from hooks_constants.mypy_integration_constants import (
32
32
  FOLLOW_IMPORTS_FLAG,
33
33
  FOLLOW_IMPORTS_SKIP_VALUE,
34
- GIT_DIRECTORY_NAME,
35
34
  MYPY_DETACHED_SUBPROCESS_TIMEOUT_SECONDS,
36
35
  MYPY_DETACHED_TIMEOUT_SKIP_MESSAGE,
37
- PYPROJECT_FILENAME,
38
36
  PYTHON_SOURCE_SUFFIX,
39
37
  )
40
38
  from hooks_constants.pyproject_config_discovery_constants import MYPY_TOOL_TABLE_NAME
@@ -44,10 +42,8 @@ except ModuleNotFoundError:
44
42
  from hooks_constants.mypy_integration_constants import (
45
43
  FOLLOW_IMPORTS_FLAG,
46
44
  FOLLOW_IMPORTS_SKIP_VALUE,
47
- GIT_DIRECTORY_NAME,
48
45
  MYPY_DETACHED_SUBPROCESS_TIMEOUT_SECONDS,
49
46
  MYPY_DETACHED_TIMEOUT_SKIP_MESSAGE,
50
- PYPROJECT_FILENAME,
51
47
  PYTHON_SOURCE_SUFFIX,
52
48
  )
53
49
  from hooks_constants.pyproject_config_discovery_constants import MYPY_TOOL_TABLE_NAME
@@ -97,41 +93,19 @@ def find_pyproject_with_mypy_config(starting_file: Path) -> Path | None:
97
93
 
98
94
 
99
95
  def find_module_resolution_root(starting_file: Path) -> Path | None:
100
- """Return the nearest ancestor directory that roots a project, else None.
96
+ """Return the project root mypy resolves this file's first-party imports against.
101
97
 
102
- A project root is the first ancestor holding a ``.git`` entry or a
103
- ``pyproject.toml``. Mypy resolves a first-party import against its working
104
- directory, so anchoring there binds ``config.*`` to the target file's own
105
- project and keeps a foreign ``config`` in the caller's directory out of scope.
106
-
107
- The walk does not climb out of the system temp directory. A PreToolUse
108
- staging copy lives under ``%TEMP%``, and a ``.git`` in the user home above
109
- that temp root is not this file's project.
110
-
111
- ::
112
-
113
- target_repo/.git + target_repo/tools/x.py -> target_repo
114
- /tmp/detached/x.py (home .git above temp) -> None
115
- flag: walk past %TEMP% into ~/.git -> mypy cwd=home, hook timeout
98
+ Mypy resolves a first-party import against its working directory, so
99
+ anchoring there binds ``config.*`` to the target file's own project and
100
+ keeps a foreign ``config`` in the caller's directory out of scope.
116
101
 
117
102
  Args:
118
103
  starting_file: The file (or directory) the walk begins from.
119
104
 
120
105
  Returns:
121
- The nearest ancestor Path that holds ``.git`` or ``pyproject.toml``,
122
- or ``None`` when no such ancestor exists inside the walk limit.
106
+ The project root, or None when the file sits inside none.
123
107
  """
124
- git_entry_name = GIT_DIRECTORY_NAME
125
- pyproject_filename = PYPROJECT_FILENAME
126
- enclosing_temporary_root = enclosing_system_temporary_root(starting_file)
127
- for each_candidate_directory in ancestor_directories(starting_file):
128
- has_git_entry = (each_candidate_directory / git_entry_name).exists()
129
- has_pyproject = (each_candidate_directory / pyproject_filename).is_file()
130
- if has_git_entry or has_pyproject:
131
- return each_candidate_directory
132
- if each_candidate_directory == enclosing_temporary_root:
133
- return None
134
- return None
108
+ return enclosing_project_root(starting_file)
135
109
 
136
110
 
137
111
  def _first_module_resolution_root(all_py_files: list[str]) -> Path | None:
@@ -9,7 +9,7 @@ import sys
9
9
  from pathlib import Path
10
10
  from typing import List
11
11
 
12
- from .validator_base import Violation
12
+ from .validator_base import Violation, source_text
13
13
 
14
14
 
15
15
  PR_PATTERN = re.compile(r"#.*\bPR\s*#?\d+", re.IGNORECASE)
@@ -42,7 +42,7 @@ def check_pr_references(source: str, filename: str) -> List[Violation]:
42
42
  def validate_file(file_path: Path) -> List[Violation]:
43
43
  filename = str(file_path)
44
44
  try:
45
- source = file_path.read_text(encoding="utf-8")
45
+ source = source_text(file_path)
46
46
  except Exception as error:
47
47
  return [Violation(filename, 0, f"Error reading file: {error}")]
48
48
 
@@ -0,0 +1,58 @@
1
+ """Resolve the project root that contains a path, if any.
2
+
3
+ A project root is the nearest ancestor holding a ``.git`` entry or a
4
+ ``pyproject.toml``. Callers need that anchor for unrelated reasons: binding a
5
+ first-party import, deciding which directory segments a project owns. One
6
+ module answers it rather than each re-coding the walk.
7
+
8
+ ::
9
+
10
+ target_repo/.git + target_repo/tools/x.py -> target_repo
11
+ /tmp/detached/x.py (home .git above temp) -> None
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ _validators_directory = str(Path(__file__).resolve().parent)
20
+ _hooks_directory = str(Path(__file__).resolve().parent.parent)
21
+ for each_directory in (_validators_directory, _hooks_directory):
22
+ if each_directory not in sys.path:
23
+ sys.path.insert(0, each_directory)
24
+
25
+ from pyproject_config_discovery import ancestor_directories # noqa: E402
26
+ from system_temporary_roots import ( # noqa: E402
27
+ enclosing_system_temporary_root,
28
+ )
29
+
30
+ from hooks_constants.mypy_integration_constants import ( # noqa: E402
31
+ GIT_DIRECTORY_NAME,
32
+ PYPROJECT_FILENAME,
33
+ )
34
+
35
+
36
+ def enclosing_project_root(starting_file: Path) -> Path | None:
37
+ """Return the nearest ancestor directory that roots a project, else None.
38
+
39
+ The walk stops at the system temp root. A staging copy lives under a temp
40
+ directory, and a ``.git`` in the user home above that root roots no
41
+ project of the staged file's.
42
+
43
+ Args:
44
+ starting_file: The file (or directory) the walk begins from.
45
+
46
+ Returns:
47
+ The nearest ancestor holding ``.git`` or ``pyproject.toml``, or None
48
+ when the walk reaches the temp root or runs out of ancestors.
49
+ """
50
+ enclosing_temporary_root = enclosing_system_temporary_root(starting_file)
51
+ for each_candidate_directory in ancestor_directories(starting_file):
52
+ has_git_entry = (each_candidate_directory / GIT_DIRECTORY_NAME).exists()
53
+ has_pyproject = (each_candidate_directory / PYPROJECT_FILENAME).is_file()
54
+ if has_git_entry or has_pyproject:
55
+ return each_candidate_directory
56
+ if each_candidate_directory == enclosing_temporary_root:
57
+ return None
58
+ return None
@@ -11,7 +11,7 @@ import sys
11
11
  from pathlib import Path
12
12
  from typing import List
13
13
 
14
- from .validator_base import Violation
14
+ from .validator_base import Violation, source_text, syntax_tree
15
15
 
16
16
  _hooks_directory = str(Path(__file__).resolve().parent.parent)
17
17
 
@@ -90,8 +90,8 @@ def validate_file(file_path: Path) -> List[Violation]:
90
90
  filename = str(file_path)
91
91
 
92
92
  try:
93
- source = file_path.read_text(encoding="utf-8")
94
- tree = ast.parse(source)
93
+ source = source_text(file_path)
94
+ tree = syntax_tree(source)
95
95
  except Exception as error:
96
96
  return [Violation(filename, 0, f"Error: {error}")]
97
97
 
@@ -21,6 +21,7 @@ from hooks_constants.python_style_checks_constants import ( # noqa: E402
21
21
  EXPECTED_BLANK_LINES_BETWEEN_FUNCTIONS,
22
22
  MINIMUM_ARGUMENT_COUNT,
23
23
  )
24
+ from validators.validator_base import source_text, syntax_tree # noqa: E402
24
25
  from validators.python_style_helpers import ( # noqa: E402
25
26
  FunctionNode,
26
27
  blank_line_for_source,
@@ -331,12 +332,12 @@ def validate_file(file_path: Path) -> list[Violation]:
331
332
  violations: list[Violation] = []
332
333
  filename = str(file_path)
333
334
  try:
334
- source = file_path.read_text(encoding="utf-8")
335
+ source = source_text(file_path)
335
336
  except Exception as error:
336
337
  violations.append(Violation(filename, 0, f"Error reading file: {error}"))
337
338
  return violations
338
339
  try:
339
- tree = ast.parse(source)
340
+ tree = syntax_tree(source)
340
341
  except SyntaxError as error:
341
342
  violations.append(
342
343
  Violation(filename, error.lineno or 0, f"Syntax error: {error.msg}")
@@ -7,16 +7,9 @@ Validates React-specific code standards:
7
7
 
8
8
  import re
9
9
  import sys
10
- from dataclasses import dataclass
11
10
  from pathlib import Path
12
11
 
13
-
14
- @dataclass(frozen=True)
15
- class Violation:
16
- """Represents a validation violation."""
17
- file: str
18
- line: int
19
- message: str
12
+ from .validator_base import Violation
20
13
 
21
14
 
22
15
  CLASS_COMPONENT_PATTERN = re.compile(
@@ -81,7 +74,7 @@ def main() -> int:
81
74
  violations = check_no_class_components(file_paths)
82
75
 
83
76
  for violation in violations:
84
- print(f"{violation.file}:{violation.line}: {violation.message}")
77
+ print(violation)
85
78
 
86
79
  return 1 if violations else 0
87
80