claude-dev-env 1.60.0 → 1.62.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 (41) hide show
  1. package/CLAUDE.md +12 -0
  2. package/audit-rubrics/category_rubrics/category-f-silent-failures.md +1 -1
  3. package/audit-rubrics/prompts/category-e-dead-code.md +17 -4
  4. package/audit-rubrics/prompts/category-f-silent-failures.md +1 -0
  5. package/bin/install.mjs +1 -1
  6. package/docs/CODE_RULES.md +2 -2
  7. package/hooks/blocking/code_rules_annotations_length.py +189 -10
  8. package/hooks/blocking/code_rules_dead_config_field.py +321 -0
  9. package/hooks/blocking/code_rules_enforcer.py +14 -0
  10. package/hooks/blocking/code_rules_orphan_css_class.py +196 -0
  11. package/hooks/blocking/config/verified_commit_constants.py +15 -2
  12. package/hooks/blocking/destructive_command_blocker.py +483 -61
  13. package/hooks/blocking/test_code_rules_enforcer_annotations.py +240 -0
  14. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
  15. package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +432 -0
  16. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +82 -0
  17. package/hooks/blocking/test_code_rules_enforcer_orphan_css_class.py +196 -0
  18. package/hooks/blocking/test_destructive_command_blocker.py +213 -0
  19. package/hooks/blocking/test_verification_verdict_store.py +212 -0
  20. package/hooks/blocking/test_verified_commit_gate.py +159 -0
  21. package/hooks/blocking/test_verifier_verdict_minter.py +74 -95
  22. package/hooks/blocking/verification_verdict_store.py +240 -0
  23. package/hooks/blocking/verified_commit_gate.py +31 -9
  24. package/hooks/blocking/verifier_verdict_minter.py +46 -124
  25. package/hooks/hooks_constants/code_rules_enforcer_constants.py +6 -0
  26. package/hooks/hooks_constants/dead_config_field_constants.py +39 -0
  27. package/hooks/hooks_constants/destructive_command_segment_constants.py +15 -0
  28. package/hooks/hooks_constants/orphan_css_class_constants.py +40 -0
  29. package/hooks/validation/mypy_validator.py +59 -7
  30. package/hooks/validation/test_mypy_validator.py +94 -0
  31. package/package.json +1 -1
  32. package/rules/orphan-css-class.md +23 -0
  33. package/skills/autoconverge/reference/gotchas.md +11 -0
  34. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +5 -1
  35. package/skills/autoconverge/workflow/converge.contract.test.mjs +202 -13
  36. package/skills/autoconverge/workflow/converge.mjs +392 -51
  37. package/skills/autoconverge/workflow/test_render_report.py +55 -0
  38. package/skills/doc-gist/SKILL.md +3 -2
  39. package/skills/doc-gist/references/examples/21-decision-signoff.html +546 -0
  40. package/skills/doc-gist/references/examples/README.md +2 -2
  41. package/skills/task-build/SKILL.md +31 -0
@@ -320,3 +320,243 @@ def test_should_flag_undefaulted_fixture_before_defaulted_one() -> None:
320
320
  )
321
321
 
322
322
 
323
+ def test_should_flag_unused_known_fixture_parameter_in_test_file() -> None:
324
+ source = (
325
+ "from pathlib import Path\n"
326
+ "def test_omits_config(tmp_path: Path) -> None:\n"
327
+ " command = build_command('module.py', None)\n"
328
+ " assert command[-1] == 'module.py'\n"
329
+ )
330
+ issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
331
+ source, TEST_FILE_PATH
332
+ )
333
+ assert any(
334
+ "tmp_path" in each_issue for each_issue in issues
335
+ ), f"Expected unused tmp_path fixture parameter flagged, got: {issues}"
336
+
337
+
338
+ def test_should_not_flag_used_known_fixture_parameter() -> None:
339
+ source = (
340
+ "from pathlib import Path\n"
341
+ "def test_includes_config(tmp_path: Path) -> None:\n"
342
+ " config_file = tmp_path / 'pyproject.toml'\n"
343
+ " assert config_file.name == 'pyproject.toml'\n"
344
+ )
345
+ issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
346
+ source, TEST_FILE_PATH
347
+ )
348
+ assert issues == [], (
349
+ f"A referenced fixture parameter must not be flagged, got: {issues}"
350
+ )
351
+
352
+
353
+ def test_should_not_flag_unused_ordinary_test_parameter() -> None:
354
+ source = "def test_thing(some_value):\n return 1\n"
355
+ issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
356
+ source, TEST_FILE_PATH
357
+ )
358
+ assert issues == [], (
359
+ f"Only known pytest fixtures are checked, not arbitrary params, got: {issues}"
360
+ )
361
+
362
+
363
+ def test_should_not_flag_unused_fixture_outside_test_files() -> None:
364
+ source = "def build(tmp_path):\n return 1\n"
365
+ issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
366
+ source, PRODUCTION_FILE_PATH
367
+ )
368
+ assert issues == [], (
369
+ f"This check applies to test files only, got: {issues}"
370
+ )
371
+
372
+
373
+ def test_should_flag_unused_monkeypatch_fixture_parameter() -> None:
374
+ source = (
375
+ "import pytest\n"
376
+ "def test_env(monkeypatch: pytest.MonkeyPatch) -> None:\n"
377
+ " assert build_command('m.py', None)[-1] == 'm.py'\n"
378
+ )
379
+ issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
380
+ source, TEST_FILE_PATH
381
+ )
382
+ assert any(
383
+ "monkeypatch" in each_issue for each_issue in issues
384
+ ), f"Expected unused monkeypatch fixture parameter flagged, got: {issues}"
385
+
386
+
387
+ def test_should_count_attribute_access_as_fixture_use() -> None:
388
+ source = (
389
+ "import pytest\n"
390
+ "def test_env(monkeypatch: pytest.MonkeyPatch) -> None:\n"
391
+ " monkeypatch.setenv('A', 'B')\n"
392
+ )
393
+ issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
394
+ source, TEST_FILE_PATH
395
+ )
396
+ assert issues == [], (
397
+ f"Attribute access on the fixture counts as a use, got: {issues}"
398
+ )
399
+
400
+
401
+ def test_should_count_nested_function_reference_as_fixture_use() -> None:
402
+ source = (
403
+ "from pathlib import Path\n"
404
+ "def test_board(tmp_path: Path) -> None:\n"
405
+ " def inner() -> Path:\n"
406
+ " return tmp_path\n"
407
+ " assert inner().name\n"
408
+ )
409
+ issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
410
+ source, TEST_FILE_PATH
411
+ )
412
+ assert issues == [], (
413
+ f"A reference inside a nested function counts as a use, got: {issues}"
414
+ )
415
+
416
+
417
+ def test_should_not_flag_unused_fixture_in_decorated_fixture_function() -> None:
418
+ source = (
419
+ "import pytest\n"
420
+ "from pathlib import Path\n"
421
+ "@pytest.fixture\n"
422
+ "def board(tmp_path: Path) -> int:\n"
423
+ " return 1\n"
424
+ )
425
+ issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
426
+ source, TEST_FILE_PATH
427
+ )
428
+ assert issues == [], (
429
+ f"A fixture composing another fixture by injection alone is intentional "
430
+ f"and must not be flagged, got: {issues}"
431
+ )
432
+
433
+
434
+ def test_should_count_comprehension_reference_as_fixture_use() -> None:
435
+ source = (
436
+ "import pytest\n"
437
+ "def test_log_lines(caplog: pytest.LogCaptureFixture) -> None:\n"
438
+ " messages = [each_record.message for each_record in caplog.records]\n"
439
+ " assert messages == []\n"
440
+ )
441
+ issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
442
+ source, TEST_FILE_PATH
443
+ )
444
+ assert issues == [], (
445
+ f"A reference inside a comprehension counts as a use, got: {issues}"
446
+ )
447
+
448
+
449
+ def test_should_not_flag_unused_fixture_in_decorated_test_named_function() -> None:
450
+ source = (
451
+ "import pytest\n"
452
+ "from pathlib import Path\n"
453
+ "@pytest.fixture\n"
454
+ "def test_board(tmp_path: Path) -> int:\n"
455
+ " return 1\n"
456
+ )
457
+ issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
458
+ source, TEST_FILE_PATH
459
+ )
460
+ assert issues == [], (
461
+ f"A @pytest.fixture-decorated function named test_* is a fixture, not a "
462
+ f"test, and must not be flagged, got: {issues}"
463
+ )
464
+
465
+
466
+ def test_should_not_flag_fixture_param_on_nested_test_named_helper() -> None:
467
+ source = (
468
+ "from pathlib import Path\n"
469
+ "def test_outer(tmp_path: Path) -> None:\n"
470
+ " assert tmp_path\n"
471
+ " def test_inner(tmp_path) -> None:\n"
472
+ " return None\n"
473
+ " test_inner(tmp_path)\n"
474
+ )
475
+ issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
476
+ source, TEST_FILE_PATH
477
+ )
478
+ assert issues == [], (
479
+ f"A fixture-named parameter on a function nested inside a test body is a "
480
+ f"local helper argument pytest never injects, got: {issues}"
481
+ )
482
+
483
+
484
+ def test_should_count_augmented_assignment_as_fixture_reference() -> None:
485
+ source = "def test_aug(request) -> None:\n request += 1\n"
486
+ issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
487
+ source, TEST_FILE_PATH
488
+ )
489
+ assert issues == [], (
490
+ f"An augmented assignment to the fixture references it, got: {issues}"
491
+ )
492
+
493
+
494
+ def test_should_count_del_as_fixture_reference() -> None:
495
+ source = "def test_d(tmp_path: Path) -> None:\n del tmp_path\n"
496
+ issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
497
+ source, TEST_FILE_PATH
498
+ )
499
+ assert issues == [], (
500
+ f"A del of the fixture references it, got: {issues}"
501
+ )
502
+
503
+
504
+ def test_should_flag_unused_fixture_parameter_on_class_method() -> None:
505
+ source = (
506
+ "from pathlib import Path\n"
507
+ "class TestThing:\n"
508
+ " def test_method(self, tmp_path: Path) -> None:\n"
509
+ " assert 1 == 1\n"
510
+ )
511
+ issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
512
+ source, TEST_FILE_PATH
513
+ )
514
+ assert any(
515
+ "tmp_path" in each_issue for each_issue in issues
516
+ ), f"An unused fixture on a class-body test method must be flagged, got: {issues}"
517
+
518
+
519
+ def test_should_flag_unused_fixture_parameter_on_nested_class_method() -> None:
520
+ source = (
521
+ "from pathlib import Path\n"
522
+ "class TestOuter:\n"
523
+ " class TestInner:\n"
524
+ " def test_method(self, tmp_path: Path) -> None:\n"
525
+ " assert 1 == 1\n"
526
+ )
527
+ issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
528
+ source, TEST_FILE_PATH
529
+ )
530
+ assert any(
531
+ "tmp_path" in each_issue for each_issue in issues
532
+ ), f"An unused fixture on a nested-class test method must be flagged, got: {issues}"
533
+
534
+
535
+ def test_should_flag_fixture_when_body_only_plain_assigns_it() -> None:
536
+ source = "def test_x(tmp_path: Path) -> None:\n tmp_path = 1\n"
537
+ issues = code_rules_enforcer.check_unused_known_pytest_fixture_parameters(
538
+ source, TEST_FILE_PATH
539
+ )
540
+ assert any(
541
+ "tmp_path" in each_issue for each_issue in issues
542
+ ), f"A plain Store target is not a reference, so tmp_path is unused, got: {issues}"
543
+
544
+
545
+ def test_annotation_check_skips_fixture_param_on_function_nested_in_test() -> None:
546
+ source = (
547
+ "from pathlib import Path\n"
548
+ "def test_outer(tmp_path: Path) -> None:\n"
549
+ " def inner(tmp_path) -> None:\n"
550
+ " return None\n"
551
+ " inner(tmp_path)\n"
552
+ )
553
+ issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
554
+ source, TEST_FILE_PATH
555
+ )
556
+ assert issues == [], (
557
+ f"A fixture-named param on a function nested in a test body is not an "
558
+ f"injection site, and the correctly annotated outer param is fine, "
559
+ f"got: {issues}"
560
+ )
561
+
562
+
@@ -77,6 +77,7 @@ KNOWN_UNCAPPED_CHECKS_PENDING_REVIEW: frozenset[str] = frozenset(
77
77
  "check_return_annotations",
78
78
  "check_skip_decorators_in_tests",
79
79
  "check_string_literal_magic",
80
+ "check_unused_known_pytest_fixture_parameters",
80
81
  "check_unused_optional_parameters",
81
82
  }
82
83
  )
@@ -0,0 +1,432 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import os
5
+ import shutil
6
+ import stat
7
+ import tempfile
8
+ from collections.abc import Callable, Iterator
9
+ from pathlib import Path
10
+
11
+ import pytest
12
+
13
+ ENFORCER_PATH = Path(__file__).resolve().parent / "code_rules_enforcer.py"
14
+ specification = importlib.util.spec_from_file_location("code_rules_enforcer", ENFORCER_PATH)
15
+ assert specification is not None and specification.loader is not None
16
+ code_rules_enforcer = importlib.util.module_from_spec(specification)
17
+ specification.loader.exec_module(code_rules_enforcer)
18
+
19
+ PRODUCTION_FILE_PATH = "packages/app/services/report.py"
20
+ TEST_FILE_PATH = "packages/app/services/test_report.py"
21
+ MIGRATION_FILE_PATH = "packages/app/migrations/0001_initial.py"
22
+
23
+ THEME_UPDATE_CONFIG_BODY = (
24
+ "from dataclasses import dataclass\n"
25
+ "\n"
26
+ "@dataclass\n"
27
+ "class ThemeUpdateConfig:\n"
28
+ " portal_url: str\n"
29
+ " debug_port: int\n"
30
+ " timeout_seconds: int\n"
31
+ )
32
+
33
+
34
+ def _strip_read_only_and_retry(
35
+ removal_function: Callable[[str], object],
36
+ target_path: str,
37
+ _exc_info: BaseException,
38
+ ) -> None:
39
+ try:
40
+ os.chmod(target_path, stat.S_IWRITE)
41
+ removal_function(target_path)
42
+ except OSError:
43
+ pass
44
+
45
+
46
+ @pytest.fixture
47
+ def neutral_root() -> Iterator[Path]:
48
+ """Yield a temp directory whose path carries no ``test_`` segment.
49
+
50
+ The enforcer's ``is_test_file`` keys on the full path string, and pytest's
51
+ own ``tmp_path`` directory name embeds the test name, which would make every
52
+ synthetic config path look like a test file. A neutral ``mkdtemp`` root
53
+ mirrors how a production config module path looks.
54
+ """
55
+ neutral_directory = Path(tempfile.mkdtemp(prefix="deadcfg-")).resolve()
56
+ try:
57
+ yield neutral_directory
58
+ finally:
59
+ shutil.rmtree(neutral_directory, onexc=_strip_read_only_and_retry)
60
+
61
+
62
+ def _check(source: str, file_path: str) -> list[str]:
63
+ return code_rules_enforcer.check_dead_config_dataclass_fields(source, file_path)
64
+
65
+
66
+ def _build_config_package(
67
+ workflow_directory: Path,
68
+ config_body: str,
69
+ consumer_body: str,
70
+ ) -> Path:
71
+ config_package = workflow_directory / "os_update_workflow"
72
+ config_package.mkdir(parents=True)
73
+ (config_package / "__init__.py").write_text("", encoding="utf-8")
74
+ config_path = config_package / "config.py"
75
+ config_path.write_text(config_body, encoding="utf-8")
76
+ (workflow_directory / "runner.py").write_text(consumer_body, encoding="utf-8")
77
+ return config_path
78
+
79
+
80
+ def test_flags_config_field_read_by_no_production_module(neutral_root: Path) -> None:
81
+ consumer_body = (
82
+ "from os_update_workflow.config import ThemeUpdateConfig\n"
83
+ "\n"
84
+ "def run(configuration: ThemeUpdateConfig) -> None:\n"
85
+ " print(configuration.portal_url)\n"
86
+ " print(configuration.timeout_seconds)\n"
87
+ )
88
+ config_path = _build_config_package(
89
+ neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
90
+ )
91
+ issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
92
+ assert any("'debug_port'" in each_issue for each_issue in issues), (
93
+ f"Expected dead 'debug_port' flagged, got: {issues}"
94
+ )
95
+ assert not any(
96
+ "'portal_url'" in each_issue or "'timeout_seconds'" in each_issue for each_issue in issues
97
+ ), f"Fields read in consumer must not be flagged, got: {issues}"
98
+
99
+
100
+ def test_does_not_flag_field_read_as_attribute_in_sibling_module(neutral_root: Path) -> None:
101
+ consumer_body = (
102
+ "from os_update_workflow.config import ThemeUpdateConfig\n"
103
+ "\n"
104
+ "def run(configuration: ThemeUpdateConfig) -> None:\n"
105
+ " print(configuration.portal_url)\n"
106
+ " print(configuration.debug_port)\n"
107
+ " print(configuration.timeout_seconds)\n"
108
+ )
109
+ config_path = _build_config_package(
110
+ neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
111
+ )
112
+ issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
113
+ assert issues == [], f"All fields are read in consumer, none must be flagged, got: {issues}"
114
+
115
+
116
+ def test_flags_field_read_only_by_test_module(neutral_root: Path) -> None:
117
+ workflow_directory = neutral_root / "workflow"
118
+ config_package = workflow_directory / "os_update_workflow"
119
+ config_package.mkdir(parents=True)
120
+ (config_package / "__init__.py").write_text("", encoding="utf-8")
121
+ config_path = config_package / "config.py"
122
+ config_path.write_text(THEME_UPDATE_CONFIG_BODY, encoding="utf-8")
123
+ test_body = (
124
+ "from os_update_workflow.config import ThemeUpdateConfig\n"
125
+ "\n"
126
+ "def test_debug_port_default() -> None:\n"
127
+ " cfg = ThemeUpdateConfig(portal_url='x', debug_port=9222, timeout_seconds=30)\n"
128
+ " assert cfg.debug_port == 9222\n"
129
+ " assert cfg.portal_url == 'x'\n"
130
+ " assert cfg.timeout_seconds == 30\n"
131
+ )
132
+ (workflow_directory / "test_config.py").write_text(test_body, encoding="utf-8")
133
+ issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
134
+ assert any("'debug_port'" in each_issue for each_issue in issues), (
135
+ f"Field read only by test code must still be flagged as dead-in-production, got: {issues}"
136
+ )
137
+
138
+
139
+ def test_ignores_non_config_named_dataclass(neutral_root: Path) -> None:
140
+ source = (
141
+ "from dataclasses import dataclass\n"
142
+ "\n"
143
+ "@dataclass\n"
144
+ "class ThemeMetadata:\n"
145
+ " title: str\n"
146
+ " debug_port: int\n"
147
+ )
148
+ workflow_directory = neutral_root / "workflow"
149
+ workflow_directory.mkdir(parents=True)
150
+ module_path = workflow_directory / "metadata.py"
151
+ module_path.write_text(source, encoding="utf-8")
152
+ issues = _check(source, str(module_path))
153
+ assert issues == [], (
154
+ f"Non-Config-named dataclasses are outside scope of this check, got: {issues}"
155
+ )
156
+
157
+
158
+ def test_does_not_flag_field_read_via_string_literal(neutral_root: Path) -> None:
159
+ consumer_body = (
160
+ "from os_update_workflow.config import ThemeUpdateConfig\n"
161
+ "\n"
162
+ "def read_field(configuration: ThemeUpdateConfig, field_name: str) -> object:\n"
163
+ " return getattr(configuration, 'debug_port')\n"
164
+ )
165
+ config_path = _build_config_package(
166
+ neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
167
+ )
168
+ issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
169
+ assert not any("'debug_port'" in each_issue for each_issue in issues), (
170
+ f"String literal getattr read must count as a field read, got: {issues}"
171
+ )
172
+
173
+
174
+ def test_does_not_flag_when_consumer_serializes_whole_instance_via_asdict(
175
+ neutral_root: Path,
176
+ ) -> None:
177
+ consumer_body = (
178
+ "from dataclasses import asdict\n"
179
+ "from os_update_workflow.config import ThemeUpdateConfig\n"
180
+ "\n"
181
+ "def serialize(configuration: ThemeUpdateConfig) -> dict[str, object]:\n"
182
+ " return asdict(configuration)\n"
183
+ )
184
+ config_path = _build_config_package(
185
+ neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
186
+ )
187
+ issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
188
+ assert issues == [], (
189
+ f"asdict reads every field at once, so no field may be flagged, got: {issues}"
190
+ )
191
+
192
+
193
+ def test_does_not_flag_when_consumer_reads_instance_dict(neutral_root: Path) -> None:
194
+ consumer_body = (
195
+ "from os_update_workflow.config import ThemeUpdateConfig\n"
196
+ "\n"
197
+ "def serialize(configuration: ThemeUpdateConfig) -> dict[str, object]:\n"
198
+ " return dict(configuration.__dict__)\n"
199
+ )
200
+ config_path = _build_config_package(
201
+ neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
202
+ )
203
+ issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
204
+ assert issues == [], (
205
+ f"__dict__ read consumes every field at once, so none may be flagged, got: {issues}"
206
+ )
207
+
208
+
209
+ def test_does_not_flag_field_used_only_as_replace_keyword(neutral_root: Path) -> None:
210
+ consumer_body = (
211
+ "from dataclasses import replace\n"
212
+ "from os_update_workflow.config import ThemeUpdateConfig\n"
213
+ "\n"
214
+ "def repoint(configuration: ThemeUpdateConfig) -> ThemeUpdateConfig:\n"
215
+ " return replace(configuration, debug_port=9999)\n"
216
+ )
217
+ config_path = _build_config_package(
218
+ neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
219
+ )
220
+ issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
221
+ assert not any("'debug_port'" in each_issue for each_issue in issues), (
222
+ f"replace keyword usage of debug_port must count as a read, got: {issues}"
223
+ )
224
+
225
+
226
+ def test_does_not_flag_field_used_only_as_constructor_keyword(neutral_root: Path) -> None:
227
+ consumer_body = (
228
+ "from os_update_workflow.config import ThemeUpdateConfig\n"
229
+ "\n"
230
+ "def build() -> ThemeUpdateConfig:\n"
231
+ " return ThemeUpdateConfig(portal_url='x', debug_port=1, timeout_seconds=99)\n"
232
+ )
233
+ config_path = _build_config_package(
234
+ neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
235
+ )
236
+ issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
237
+ assert issues == [], (
238
+ f"Constructor keyword arguments name every field, so none may be flagged, got: {issues}"
239
+ )
240
+
241
+
242
+ def test_returns_empty_list_at_file_cap(
243
+ neutral_root: Path, monkeypatch: pytest.MonkeyPatch
244
+ ) -> None:
245
+ monkeypatch.setattr("code_rules_dead_config_field.MAX_SCAN_ROOT_FILE_COUNT", 0)
246
+ config_path = _build_config_package(
247
+ neutral_root / "workflow",
248
+ THEME_UPDATE_CONFIG_BODY,
249
+ "def noop() -> None:\n pass\n",
250
+ )
251
+ issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
252
+ assert issues == [], f"File cap hit must return [] (cannot prove dead), got: {issues}"
253
+
254
+
255
+ def test_returns_empty_list_on_syntax_error(neutral_root: Path) -> None:
256
+ workflow_directory = neutral_root / "workflow"
257
+ workflow_directory.mkdir(parents=True)
258
+ broken_path = workflow_directory / "config.py"
259
+ broken_source = "@dataclass\nclass BrokenConfig(\n"
260
+ broken_path.write_text(broken_source, encoding="utf-8")
261
+ issues = _check(broken_source, str(broken_path))
262
+ assert issues == [], f"SyntaxError must return [], got: {issues}"
263
+
264
+
265
+ def test_is_skipped_on_test_file_destination() -> None:
266
+ issues = _check(THEME_UPDATE_CONFIG_BODY, TEST_FILE_PATH)
267
+ assert issues == [], f"Test file destinations are exempt, got: {issues}"
268
+
269
+
270
+ def test_is_skipped_on_migration_file_destination() -> None:
271
+ issues = _check(THEME_UPDATE_CONFIG_BODY, MIGRATION_FILE_PATH)
272
+ assert issues == [], f"Migration file destinations are exempt, got: {issues}"
273
+
274
+
275
+ def test_real_world_shape_theme_update_config_with_dead_debug_port(neutral_root: Path) -> None:
276
+ workflow_directory = neutral_root / "workflow"
277
+ config_package = workflow_directory / "os_update_workflow"
278
+ config_package.mkdir(parents=True)
279
+ (config_package / "__init__.py").write_text("", encoding="utf-8")
280
+ config_path = config_package / "config.py"
281
+ config_path.write_text(THEME_UPDATE_CONFIG_BODY, encoding="utf-8")
282
+ orchestrator_body = (
283
+ "from os_update_workflow.config import ThemeUpdateConfig\n"
284
+ "\n"
285
+ "def build_session(configuration: ThemeUpdateConfig) -> None:\n"
286
+ " url = configuration.portal_url\n"
287
+ " seconds = configuration.timeout_seconds\n"
288
+ " print(url, seconds)\n"
289
+ )
290
+ (workflow_directory / "orchestrator.py").write_text(orchestrator_body, encoding="utf-8")
291
+ issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
292
+ assert any("'debug_port'" in each_issue for each_issue in issues), (
293
+ f"debug_port field unused in production must be flagged, got: {issues}"
294
+ )
295
+ assert not any(
296
+ "'portal_url'" in each_issue or "'timeout_seconds'" in each_issue for each_issue in issues
297
+ ), f"Fields read in orchestrator must not be flagged, got: {issues}"
298
+
299
+
300
+ def test_does_not_flag_field_used_only_via_augmented_assignment(neutral_root: Path) -> None:
301
+ consumer_body = (
302
+ "from os_update_workflow.config import ThemeUpdateConfig\n"
303
+ "\n"
304
+ "def run(configuration: ThemeUpdateConfig) -> None:\n"
305
+ " print(configuration.portal_url)\n"
306
+ " print(configuration.timeout_seconds)\n"
307
+ " configuration.debug_port += 1\n"
308
+ )
309
+ config_path = _build_config_package(
310
+ neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
311
+ )
312
+ issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
313
+ assert not any("'debug_port'" in each_issue for each_issue in issues), (
314
+ f"Augmented assignment reads debug_port before writing, so it is not dead, got: {issues}"
315
+ )
316
+
317
+
318
+ def test_flags_field_read_only_via_whole_instance_comparison(neutral_root: Path) -> None:
319
+ """A field read ONLY via whole-instance comparison IS flagged (accepted limitation).
320
+
321
+ Unlike the per-file dead-dataclass-field check, this cross-module check does
322
+ not suppress on a dataclass-dunder whole-instance read: instance comparison
323
+ (``left == right``) is not bound to a config instance, and tree-wide one
324
+ incidental ``==`` anywhere would disable the check on any realistic package.
325
+ A ``*Config`` field whose only production read is this whole-instance
326
+ comparison, and that is never read directly, is therefore flagged — a
327
+ documented, rare limitation.
328
+ """
329
+ consumer_body = (
330
+ "from os_update_workflow.config import ThemeUpdateConfig\n"
331
+ "\n"
332
+ "def is_same(left: ThemeUpdateConfig, right: ThemeUpdateConfig) -> bool:\n"
333
+ " return left == right\n"
334
+ )
335
+ config_path = _build_config_package(
336
+ neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
337
+ )
338
+ issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
339
+ assert any("'debug_port'" in each_issue for each_issue in issues), (
340
+ f"Field read only via whole-instance comparison is flagged, got: {issues}"
341
+ )
342
+
343
+
344
+ def test_flags_field_read_only_via_whole_instance_stringification(neutral_root: Path) -> None:
345
+ """A field read ONLY via whole-instance stringification IS flagged (accepted limitation).
346
+
347
+ The cross-module check does not suppress on a formatted-string conversion of
348
+ a whole instance (``f'{configuration}'``): the f-string is not bound to a
349
+ config instance, and tree-wide one incidental f-string anywhere would disable
350
+ the check on any realistic package. A ``*Config`` field whose only production
351
+ read is this whole-instance stringification, and that is never read directly,
352
+ is therefore flagged — a documented, rare limitation.
353
+ """
354
+ consumer_body = (
355
+ "from os_update_workflow.config import ThemeUpdateConfig\n"
356
+ "\n"
357
+ "def describe(configuration: ThemeUpdateConfig) -> str:\n"
358
+ " return f'{configuration}'\n"
359
+ )
360
+ config_path = _build_config_package(
361
+ neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
362
+ )
363
+ issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
364
+ assert any("'debug_port'" in each_issue for each_issue in issues), (
365
+ f"Field read only via whole-instance stringification is flagged, got: {issues}"
366
+ )
367
+
368
+
369
+ def test_unrelated_string_method_does_not_suppress_so_dead_field_flagged(
370
+ neutral_root: Path,
371
+ ) -> None:
372
+ """An unrelated ``.replace(...)`` on a string must not suppress the tree.
373
+
374
+ ``"some text".replace("a", "b")`` is a string method, not a ``dataclasses``-
375
+ qualified reflective consumer, so it does not make the check treat the tree
376
+ as a whole-instance read. A genuinely dead ``debug_port`` is still flagged.
377
+ """
378
+ consumer_body = (
379
+ "from os_update_workflow.config import ThemeUpdateConfig\n"
380
+ "\n"
381
+ "def run(configuration: ThemeUpdateConfig) -> str:\n"
382
+ " print(configuration.portal_url)\n"
383
+ " print(configuration.timeout_seconds)\n"
384
+ " return 'some text'.replace('a', 'b')\n"
385
+ )
386
+ config_path = _build_config_package(
387
+ neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
388
+ )
389
+ issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
390
+ assert any("'debug_port'" in each_issue for each_issue in issues), (
391
+ f"Unrelated string .replace must not suppress, so dead debug_port is flagged, got: {issues}"
392
+ )
393
+
394
+
395
+ def test_dataclasses_qualified_replace_call_suppresses_so_no_field_flagged(
396
+ neutral_root: Path,
397
+ ) -> None:
398
+ """A ``dataclasses.replace(cfg, ...)`` call suppresses the whole tree.
399
+
400
+ A genuine ``dataclasses``-qualified reflective consumer reads every field at
401
+ once, so the check is suppressed for the whole tree and no field is flagged.
402
+ """
403
+ consumer_body = (
404
+ "import dataclasses\n"
405
+ "from os_update_workflow.config import ThemeUpdateConfig\n"
406
+ "\n"
407
+ "def repoint(configuration: ThemeUpdateConfig) -> ThemeUpdateConfig:\n"
408
+ " return dataclasses.replace(configuration, debug_port=9999)\n"
409
+ )
410
+ config_path = _build_config_package(
411
+ neutral_root / "workflow", THEME_UPDATE_CONFIG_BODY, consumer_body
412
+ )
413
+ issues = _check(THEME_UPDATE_CONFIG_BODY, str(config_path))
414
+ assert issues == [], (
415
+ f"dataclasses.replace reads every field at once, so none may be flagged, got: {issues}"
416
+ )
417
+
418
+
419
+ def test_validate_content_dispatch_runs_dead_config_field_check(neutral_root: Path) -> None:
420
+ workflow_directory = neutral_root / "workflow"
421
+ config_package = workflow_directory / "os_update_workflow"
422
+ config_package.mkdir(parents=True)
423
+ (config_package / "__init__.py").write_text("", encoding="utf-8")
424
+ config_path = config_package / "config.py"
425
+ config_path.write_text(THEME_UPDATE_CONFIG_BODY, encoding="utf-8")
426
+ (workflow_directory / "runner.py").write_text(
427
+ "def noop() -> None:\n pass\n", encoding="utf-8"
428
+ )
429
+ issues = code_rules_enforcer.validate_content(THEME_UPDATE_CONFIG_BODY, str(config_path))
430
+ assert any("'debug_port'" in each_issue for each_issue in issues), (
431
+ f"Expected the enforcer dispatch to surface the dead config field, got: {issues}"
432
+ )