claude-dev-env 2.27.0 → 2.28.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 (109) hide show
  1. package/.agents/skills/hitl/SKILL.md +45 -0
  2. package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
  3. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +16 -29
  4. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
  5. package/bin/ever-shipped-skills.mjs +1 -0
  6. package/docs/CODE_RULES.md +3 -3
  7. package/docs/agent-spawn-protocol.md +1 -1
  8. package/docs/references/prose-style-enforcement.md +5 -9
  9. package/hooks/blocking/AGENTS.md +3 -13
  10. package/hooks/blocking/code_rules_docstrings.py +10 -2138
  11. package/hooks/blocking/code_rules_enforcer.py +0 -121
  12. package/hooks/blocking/code_rules_imports_logging.py +1 -236
  13. package/hooks/blocking/code_rules_shared.py +23 -0
  14. package/hooks/blocking/code_rules_test_layout.py +8 -8
  15. package/hooks/blocking/config/prose_style_enforcement_constants.py +4 -2
  16. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +5 -1
  17. package/hooks/blocking/precommit_code_rules_gate.py +8 -43
  18. package/hooks/blocking/state_description_blocker.py +1 -7
  19. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  20. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +0 -8
  21. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +11 -112
  22. package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +0 -1
  23. package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +1 -18
  24. package/hooks/blocking/test_code_rules_shared.py +12 -0
  25. package/hooks/blocking/test_precommit_code_rules_gate.py +32 -179
  26. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -1
  27. package/hooks/blocking/test_state_description_blocker.py +6 -4
  28. package/hooks/blocking/test_stop_dispatcher.py +5 -29
  29. package/hooks/hooks_constants/AGENTS.md +1 -6
  30. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -1
  31. package/hooks/hooks_constants/code_rules_enforcer_constants.py +3 -0
  32. package/hooks/hooks_constants/messages.py +0 -2
  33. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +3 -17
  34. package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -2
  35. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -1
  36. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +7 -0
  37. package/hooks/hooks_constants/test_messages.py +5 -3
  38. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -2
  39. package/package.json +1 -1
  40. package/rules/AGENTS.md +3 -6
  41. package/rules/claims-as-quotes.md +0 -10
  42. package/rules/code-standards.md +6 -6
  43. package/rules/explore-thoroughly.md +0 -1
  44. package/rules/failure-blast-radius.md +0 -8
  45. package/rules/falsify-before-green.md +0 -8
  46. package/rules/file-global-constants.md +2 -2
  47. package/rules/filesystem-search.md +1 -1
  48. package/rules/git-workflow.md +1 -9
  49. package/rules/hedging-claims.md +2 -6
  50. package/rules/long-horizon-autonomy.md +1 -1
  51. package/rules/measurement-denominators.md +0 -9
  52. package/rules/research-mode.md +0 -6
  53. package/rules/verify-before-asking.md +0 -5
  54. package/rules/verify-runtime-state.md +0 -5
  55. package/scripts/codex_compat_materializer.py +99 -24
  56. package/scripts/sync_to_cursor/rules.py +0 -10
  57. package/scripts/tests/test_codex_compat_materializer.py +70 -19
  58. package/scripts/tests/test_engine.py +0 -1
  59. package/scripts/tests/test_rules.py +0 -1
  60. package/scripts/tests/test_sync_to_cursor.py +0 -1
  61. package/system-prompts/software-engineer.xml +3 -3
  62. package/hooks/blocking/code_rules_dead_argparse_argument.py +0 -554
  63. package/hooks/blocking/code_rules_dead_config_field.py +0 -568
  64. package/hooks/blocking/code_rules_dead_dataclass_field.py +0 -348
  65. package/hooks/blocking/code_rules_dead_module_constant.py +0 -757
  66. package/hooks/blocking/code_rules_dead_split_branch.py +0 -225
  67. package/hooks/blocking/code_rules_mock_completeness.py +0 -295
  68. package/hooks/blocking/code_rules_scope_binding.py +0 -151
  69. package/hooks/blocking/code_rules_unused_imports.py +0 -197
  70. package/hooks/blocking/hedging_language_blocker.py +0 -221
  71. package/hooks/blocking/intent_only_ending_blocker.py +0 -148
  72. package/hooks/blocking/test_code_rules_enforcer_dead_argparse_argument.py +0 -534
  73. package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +0 -846
  74. package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +0 -507
  75. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +0 -679
  76. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +0 -133
  77. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +0 -103
  78. package/hooks/blocking/test_code_rules_enforcer_dead_split_branch.py +0 -105
  79. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +0 -425
  80. package/hooks/blocking/test_code_rules_enforcer_docstring_cardinal_family.py +0 -176
  81. package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +0 -385
  82. package/hooks/blocking/test_code_rules_enforcer_docstring_fallback_branch.py +0 -398
  83. package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +0 -129
  84. package/hooks/blocking/test_code_rules_enforcer_docstring_inline_literal_claim.py +0 -93
  85. package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +0 -198
  86. package/hooks/blocking/test_code_rules_enforcer_docstring_mark_glyph_enumeration.py +0 -262
  87. package/hooks/blocking/test_code_rules_enforcer_docstring_no_consumer.py +0 -93
  88. package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +0 -115
  89. package/hooks/blocking/test_code_rules_enforcer_docstring_raises_largezipfile.py +0 -226
  90. package/hooks/blocking/test_code_rules_enforcer_docstring_returns_plural_cardinality.py +0 -207
  91. package/hooks/blocking/test_code_rules_enforcer_docstring_step_dispatch.py +0 -262
  92. package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +0 -164
  93. package/hooks/blocking/test_code_rules_enforcer_docstring_unguarded_payload.py +0 -188
  94. package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +0 -157
  95. package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +0 -303
  96. package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +0 -111
  97. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +0 -656
  98. package/hooks/blocking/test_hedging_language_blocker.py +0 -261
  99. package/hooks/blocking/test_intent_only_ending_blocker.py +0 -209
  100. package/hooks/hooks_constants/dead_argparse_argument_constants.py +0 -28
  101. package/hooks/hooks_constants/dead_config_field_constants.py +0 -39
  102. package/hooks/hooks_constants/dead_dataclass_field_constants.py +0 -25
  103. package/hooks/hooks_constants/dead_module_constant_constants.py +0 -30
  104. package/hooks/hooks_constants/hedging_uncertainty_constants.py +0 -42
  105. package/hooks/hooks_constants/test_dispatcher_constants_docstrings.py +0 -44
  106. package/rules/conservative-action.md +0 -17
  107. package/rules/context7.md +0 -8
  108. package/rules/few-words.md +0 -3
  109. package/rules/parallel-tools.md +0 -23
@@ -1,507 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import importlib.util
4
- from pathlib import Path
5
-
6
- ENFORCER_PATH = Path(__file__).resolve().parent / "code_rules_enforcer.py"
7
- specification = importlib.util.spec_from_file_location(
8
- "code_rules_enforcer", ENFORCER_PATH
9
- )
10
- assert specification is not None and specification.loader is not None
11
- code_rules_enforcer = importlib.util.module_from_spec(specification)
12
- specification.loader.exec_module(code_rules_enforcer)
13
-
14
- PRODUCTION_FILE_PATH = "packages/app/services/report.py"
15
- TEST_FILE_PATH = "packages/app/services/test_report.py"
16
- MIGRATION_FILE_PATH = "packages/app/migrations/0001_initial.py"
17
- WORKFLOW_FILE_PATH = "packages/app/skills/thing/workflow/render_report.py"
18
-
19
-
20
- def _check(source: str, file_path: str) -> list[str]:
21
- return code_rules_enforcer.check_dead_dataclass_fields(source, file_path)
22
-
23
-
24
- def test_should_flag_dataclass_field_assigned_but_never_read() -> None:
25
- source = (
26
- "from dataclasses import dataclass\n"
27
- "\n"
28
- "@dataclass\n"
29
- "class PrMetadata:\n"
30
- " number: int\n"
31
- " url: str\n"
32
- "\n"
33
- "def build() -> PrMetadata:\n"
34
- " return PrMetadata(number=1, url='x')\n"
35
- "\n"
36
- "def render(metadata: PrMetadata) -> int:\n"
37
- " return metadata.number\n"
38
- )
39
- issues = _check(source, PRODUCTION_FILE_PATH)
40
- assert any(
41
- "'url'" in each_issue and "PrMetadata" in each_issue for each_issue in issues
42
- ), f"Expected dead 'url' field flagged, got: {issues}"
43
- assert not any(
44
- "'number'" in each_issue for each_issue in issues
45
- ), f"Read field 'number' must not be flagged, got: {issues}"
46
-
47
-
48
- def test_should_not_flag_field_read_via_attribute_access() -> None:
49
- source = (
50
- "from dataclasses import dataclass\n"
51
- "\n"
52
- "@dataclass\n"
53
- "class PrMetadata:\n"
54
- " url: str\n"
55
- "\n"
56
- "def build() -> PrMetadata:\n"
57
- " return PrMetadata(url='x')\n"
58
- "\n"
59
- "def render(metadata: PrMetadata) -> str:\n"
60
- " return metadata.url\n"
61
- )
62
- assert _check(source, PRODUCTION_FILE_PATH) == []
63
-
64
-
65
- def test_should_not_flag_when_class_never_constructed_in_file() -> None:
66
- source = (
67
- "from dataclasses import dataclass\n"
68
- "\n"
69
- "@dataclass\n"
70
- "class PublicConfig:\n"
71
- " url: str\n"
72
- " number: int\n"
73
- )
74
- assert _check(source, PRODUCTION_FILE_PATH) == []
75
-
76
-
77
- def test_should_not_flag_non_dataclass_class() -> None:
78
- source = "class Plain:\n url: str\n\ndef build() -> Plain:\n return Plain()\n"
79
- assert _check(source, PRODUCTION_FILE_PATH) == []
80
-
81
-
82
- def test_should_not_flag_classvar_field() -> None:
83
- source = (
84
- "from dataclasses import dataclass\n"
85
- "from typing import ClassVar\n"
86
- "\n"
87
- "@dataclass\n"
88
- "class Counter:\n"
89
- " label: ClassVar[str] = 'count'\n"
90
- " total: int\n"
91
- "\n"
92
- "def build() -> Counter:\n"
93
- " return Counter(total=1)\n"
94
- "\n"
95
- "def read(counter: Counter) -> int:\n"
96
- " return counter.total\n"
97
- )
98
- assert _check(source, PRODUCTION_FILE_PATH) == []
99
-
100
-
101
- def test_should_not_flag_field_read_via_literal_getattr() -> None:
102
- source = (
103
- "from dataclasses import dataclass\n"
104
- "\n"
105
- "@dataclass\n"
106
- "class Row:\n"
107
- " url: str\n"
108
- "\n"
109
- "def build() -> Row:\n"
110
- " return Row(url='x')\n"
111
- "\n"
112
- "def read(row: Row) -> str:\n"
113
- " return getattr(row, 'url')\n"
114
- )
115
- assert _check(source, PRODUCTION_FILE_PATH) == []
116
-
117
-
118
- def test_should_suppress_check_when_non_literal_getattr_present() -> None:
119
- source = (
120
- "from dataclasses import dataclass\n"
121
- "\n"
122
- "@dataclass\n"
123
- "class Row:\n"
124
- " url: str\n"
125
- "\n"
126
- "def build() -> Row:\n"
127
- " return Row(url='x')\n"
128
- "\n"
129
- "def read(row: Row, field_name: str) -> str:\n"
130
- " return getattr(row, field_name)\n"
131
- )
132
- assert _check(source, PRODUCTION_FILE_PATH) == []
133
-
134
-
135
- def test_should_handle_called_dataclass_decorator() -> None:
136
- source = (
137
- "from dataclasses import dataclass\n"
138
- "\n"
139
- "@dataclass(frozen=True)\n"
140
- "class PrMetadata:\n"
141
- " url: str\n"
142
- "\n"
143
- "def build() -> PrMetadata:\n"
144
- " return PrMetadata(url='x')\n"
145
- )
146
- issues = _check(source, PRODUCTION_FILE_PATH)
147
- assert any(
148
- "'url'" in each_issue for each_issue in issues
149
- ), f"Expected dead field flagged on called-decorator dataclass, got: {issues}"
150
-
151
-
152
- def test_should_handle_dotted_dataclasses_decorator() -> None:
153
- source = (
154
- "import dataclasses\n"
155
- "\n"
156
- "@dataclasses.dataclass\n"
157
- "class PrMetadata:\n"
158
- " url: str\n"
159
- "\n"
160
- "def build() -> PrMetadata:\n"
161
- " return PrMetadata(url='x')\n"
162
- )
163
- issues = _check(source, PRODUCTION_FILE_PATH)
164
- assert any(
165
- "'url'" in each_issue for each_issue in issues
166
- ), f"Expected dead field flagged on dotted-decorator dataclass, got: {issues}"
167
-
168
-
169
- def test_should_be_silent_for_test_files() -> None:
170
- source = (
171
- "from dataclasses import dataclass\n"
172
- "\n"
173
- "@dataclass\n"
174
- "class PrMetadata:\n"
175
- " url: str\n"
176
- "\n"
177
- "def build() -> PrMetadata:\n"
178
- " return PrMetadata(url='x')\n"
179
- )
180
- assert _check(source, TEST_FILE_PATH) == []
181
-
182
-
183
- def test_should_be_silent_for_migration_files() -> None:
184
- source = (
185
- "from dataclasses import dataclass\n"
186
- "\n"
187
- "@dataclass\n"
188
- "class PrMetadata:\n"
189
- " url: str\n"
190
- "\n"
191
- "def build() -> PrMetadata:\n"
192
- " return PrMetadata(url='x')\n"
193
- )
194
- assert _check(source, MIGRATION_FILE_PATH) == []
195
-
196
-
197
- def test_should_flag_dead_field_in_workflow_path() -> None:
198
- source = (
199
- "from dataclasses import dataclass\n"
200
- "\n"
201
- "@dataclass\n"
202
- "class PrMetadata:\n"
203
- " url: str\n"
204
- "\n"
205
- "def build() -> PrMetadata:\n"
206
- " return PrMetadata(url='x')\n"
207
- )
208
- issues = _check(source, WORKFLOW_FILE_PATH)
209
- assert any(
210
- "'url'" in each_issue for each_issue in issues
211
- ), f"Workflow-path files are subject to dead-code detection, got: {issues}"
212
-
213
-
214
- def test_should_tolerate_syntax_error() -> None:
215
- assert _check("@dataclass\nclass Broken(:\n", PRODUCTION_FILE_PATH) == []
216
-
217
-
218
- def test_validate_content_runs_dead_field_check() -> None:
219
- source = (
220
- "from dataclasses import dataclass\n"
221
- "\n"
222
- "@dataclass\n"
223
- "class PrMetadata:\n"
224
- " url: str\n"
225
- "\n"
226
- "def build() -> PrMetadata:\n"
227
- " return PrMetadata(url='x')\n"
228
- )
229
- issues = code_rules_enforcer.validate_content(source, PRODUCTION_FILE_PATH)
230
- assert any(
231
- "'url'" in each_issue for each_issue in issues
232
- ), f"Expected the enforcer dispatch to surface the dead field, got: {issues}"
233
-
234
-
235
- def test_should_suppress_check_when_asdict_consumes_instance() -> None:
236
- source = (
237
- "from dataclasses import dataclass, asdict\n"
238
- "\n"
239
- "@dataclass\n"
240
- "class Row:\n"
241
- " url: str\n"
242
- "\n"
243
- "def build() -> dict:\n"
244
- " return asdict(Row(url='x'))\n"
245
- )
246
- assert _check(source, PRODUCTION_FILE_PATH) == []
247
-
248
-
249
- def test_should_suppress_check_when_astuple_consumes_instance() -> None:
250
- source = (
251
- "from dataclasses import dataclass, astuple\n"
252
- "\n"
253
- "@dataclass\n"
254
- "class Row:\n"
255
- " url: str\n"
256
- "\n"
257
- "def build() -> tuple:\n"
258
- " return astuple(Row(url='x'))\n"
259
- )
260
- assert _check(source, PRODUCTION_FILE_PATH) == []
261
-
262
-
263
- def test_should_suppress_check_when_vars_consumes_instance() -> None:
264
- source = (
265
- "from dataclasses import dataclass\n"
266
- "\n"
267
- "@dataclass\n"
268
- "class Row:\n"
269
- " url: str\n"
270
- "\n"
271
- "def build() -> dict:\n"
272
- " return vars(Row(url='x'))\n"
273
- )
274
- assert _check(source, PRODUCTION_FILE_PATH) == []
275
-
276
-
277
- def test_should_suppress_check_when_replace_consumes_instance() -> None:
278
- source = (
279
- "from dataclasses import dataclass, replace\n"
280
- "\n"
281
- "@dataclass(frozen=True)\n"
282
- "class Row:\n"
283
- " url: str\n"
284
- "\n"
285
- "def build() -> Row:\n"
286
- " return replace(Row(url='x'), url='y')\n"
287
- )
288
- assert _check(source, PRODUCTION_FILE_PATH) == []
289
-
290
-
291
- def test_should_suppress_check_when_dotted_replace_consumes_instance() -> None:
292
- source = (
293
- "import dataclasses\n"
294
- "\n"
295
- "@dataclasses.dataclass(frozen=True)\n"
296
- "class Row:\n"
297
- " url: str\n"
298
- "\n"
299
- "def build() -> Row:\n"
300
- " return dataclasses.replace(Row(url='x'), url='y')\n"
301
- )
302
- assert _check(source, PRODUCTION_FILE_PATH) == []
303
-
304
-
305
- def test_should_suppress_check_when_instance_dict_consumes_instance() -> None:
306
- source = (
307
- "from dataclasses import dataclass\n"
308
- "\n"
309
- "@dataclass\n"
310
- "class Row:\n"
311
- " url: str\n"
312
- "\n"
313
- "def build() -> dict:\n"
314
- " return Row(url='x').__dict__\n"
315
- )
316
- assert _check(source, PRODUCTION_FILE_PATH) == []
317
-
318
-
319
- def test_should_not_flag_field_read_via_multi_argument_attrgetter() -> None:
320
- source = (
321
- "from dataclasses import dataclass\n"
322
- "from operator import attrgetter\n"
323
- "\n"
324
- "@dataclass\n"
325
- "class Row:\n"
326
- " first: int\n"
327
- " second: int\n"
328
- "\n"
329
- "def build() -> tuple:\n"
330
- " return attrgetter('first', 'second')(Row(first=1, second=2))\n"
331
- )
332
- assert _check(source, PRODUCTION_FILE_PATH) == []
333
-
334
-
335
- def test_should_suppress_check_when_two_instances_compared_for_equality() -> None:
336
- source = (
337
- "from dataclasses import dataclass\n"
338
- "\n"
339
- "@dataclass\n"
340
- "class Row:\n"
341
- " url: str\n"
342
- "\n"
343
- "def are_same(left: Row, right: Row) -> bool:\n"
344
- " return Row(url='a') == Row(url='b')\n"
345
- )
346
- assert _check(source, PRODUCTION_FILE_PATH) == []
347
-
348
-
349
- def test_should_suppress_check_when_ordered_instances_compared() -> None:
350
- source = (
351
- "from dataclasses import dataclass\n"
352
- "\n"
353
- "@dataclass(order=True)\n"
354
- "class Row:\n"
355
- " priority: int\n"
356
- "\n"
357
- "def is_earlier() -> bool:\n"
358
- " return Row(priority=1) < Row(priority=2)\n"
359
- )
360
- assert _check(source, PRODUCTION_FILE_PATH) == []
361
-
362
-
363
- def test_should_suppress_check_when_instances_placed_in_set() -> None:
364
- source = (
365
- "from dataclasses import dataclass\n"
366
- "\n"
367
- "@dataclass(frozen=True)\n"
368
- "class Row:\n"
369
- " url: str\n"
370
- "\n"
371
- "def unique_rows() -> set:\n"
372
- " return {Row(url='a'), Row(url='b')}\n"
373
- )
374
- assert _check(source, PRODUCTION_FILE_PATH) == []
375
-
376
-
377
- def test_should_suppress_check_when_whole_instance_stringified() -> None:
378
- source = (
379
- "from dataclasses import dataclass\n"
380
- "\n"
381
- "@dataclass\n"
382
- "class Row:\n"
383
- " url: str\n"
384
- "\n"
385
- "def describe() -> str:\n"
386
- " return str(Row(url='x'))\n"
387
- )
388
- assert _check(source, PRODUCTION_FILE_PATH) == []
389
-
390
-
391
- def test_should_not_flag_field_read_via_match_class_pattern() -> None:
392
- source = (
393
- "from dataclasses import dataclass\n"
394
- "\n"
395
- "@dataclass\n"
396
- "class Row:\n"
397
- " url: str\n"
398
- "\n"
399
- "def read(row: Row) -> str:\n"
400
- " match row:\n"
401
- " case Row(url=found):\n"
402
- " return found\n"
403
- " return ''\n"
404
- "\n"
405
- "def build() -> Row:\n"
406
- " return Row(url='x')\n"
407
- )
408
- assert _check(source, PRODUCTION_FILE_PATH) == []
409
-
410
-
411
- def test_should_suppress_check_when_fields_reflection_consumes_instance() -> None:
412
- source = (
413
- "import dataclasses\n"
414
- "from dataclasses import dataclass\n"
415
- "\n"
416
- "@dataclass\n"
417
- "class Row:\n"
418
- " url: str\n"
419
- "\n"
420
- "def field_names() -> list:\n"
421
- " return [each_field.name for each_field in dataclasses.fields(Row(url='x'))]\n"
422
- )
423
- assert _check(source, PRODUCTION_FILE_PATH) == []
424
-
425
-
426
- def test_should_not_flag_field_read_via_augmented_assignment() -> None:
427
- source = (
428
- "from dataclasses import dataclass\n"
429
- "\n"
430
- "@dataclass\n"
431
- "class Row:\n"
432
- " counter: int\n"
433
- "\n"
434
- "def bump(row: Row) -> None:\n"
435
- " row.counter += 1\n"
436
- "\n"
437
- "def build() -> Row:\n"
438
- " return Row(counter=0)\n"
439
- )
440
- assert _check(source, PRODUCTION_FILE_PATH) == []
441
-
442
-
443
- def test_should_evaluate_full_file_content_when_supplied() -> None:
444
- fragment = " return metadata.number\n"
445
- full_file = (
446
- "from dataclasses import dataclass\n"
447
- "\n"
448
- "@dataclass\n"
449
- "class PrMetadata:\n"
450
- " number: int\n"
451
- " url: str\n"
452
- "\n"
453
- "def build() -> PrMetadata:\n"
454
- " return PrMetadata(number=1, url='x')\n"
455
- "\n"
456
- "def render(metadata: PrMetadata) -> int:\n"
457
- " return metadata.number\n"
458
- )
459
- issues = code_rules_enforcer.check_dead_dataclass_fields(
460
- fragment, PRODUCTION_FILE_PATH, full_file
461
- )
462
- assert any(
463
- "'url'" in each_issue for each_issue in issues
464
- ), f"Expected the reconstructed whole-file content to govern, got: {issues}"
465
- assert not any(
466
- "'number'" in each_issue for each_issue in issues
467
- ), f"Read field 'number' must not be flagged, got: {issues}"
468
-
469
-
470
- def test_should_not_flag_field_on_module_level_singleton() -> None:
471
- source = (
472
- "from dataclasses import dataclass\n"
473
- "\n"
474
- "@dataclass(frozen=True)\n"
475
- "class AppConfig:\n"
476
- " timeout: int\n"
477
- " retries: int\n"
478
- "\n"
479
- "SETTINGS = AppConfig(timeout=30, retries=3)\n"
480
- )
481
- assert _check(source, PRODUCTION_FILE_PATH) == []
482
-
483
-
484
- def test_should_flag_local_dataclass_but_not_module_level_singleton() -> None:
485
- source = (
486
- "from dataclasses import dataclass\n"
487
- "\n"
488
- "@dataclass(frozen=True)\n"
489
- "class AppConfig:\n"
490
- " timeout: int\n"
491
- "\n"
492
- "@dataclass\n"
493
- "class LocalRow:\n"
494
- " url: str\n"
495
- "\n"
496
- "SETTINGS = AppConfig(timeout=30)\n"
497
- "\n"
498
- "def build() -> LocalRow:\n"
499
- " return LocalRow(url='x')\n"
500
- )
501
- issues = _check(source, PRODUCTION_FILE_PATH)
502
- assert any(
503
- "'url'" in each_issue and "LocalRow" in each_issue for each_issue in issues
504
- ), f"Locally-constructed dead field must still be flagged, got: {issues}"
505
- assert not any(
506
- "AppConfig" in each_issue for each_issue in issues
507
- ), f"Module-level singleton fields must not be flagged, got: {issues}"