claude-dev-env 1.61.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.
@@ -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
+ )
@@ -6,6 +6,7 @@ decide what to gate.
6
6
  """
7
7
 
8
8
  import importlib.util
9
+ import io
9
10
  import json
10
11
  import os
11
12
  import pathlib
@@ -28,6 +29,7 @@ gate_module = importlib.util.module_from_spec(gate_spec)
28
29
  gate_spec.loader.exec_module(gate_module)
29
30
  gated_repo_directories = gate_module.gated_repo_directories
30
31
  deny_reason_for_directory = gate_module.deny_reason_for_directory
32
+ gate_main = gate_module.main
31
33
 
32
34
  store_spec = importlib.util.spec_from_file_location(
33
35
  "verification_verdict_store",
@@ -493,3 +495,33 @@ def test_no_verdict_of_either_kind_denies_the_commit(
493
495
  deny_reason = deny_reason_for_directory(str(work_dir), str(transcript_path))
494
496
  assert deny_reason is not None
495
497
  assert "VERIFIED_COMMIT_GATE" in deny_reason
498
+
499
+
500
+ def _run_gate_main(
501
+ monkeypatch: pytest.MonkeyPatch, command_text: str, work_dir: pathlib.Path
502
+ ) -> None:
503
+ payload_text = json.dumps(
504
+ {
505
+ "tool_name": "Bash",
506
+ "tool_input": {"command": command_text},
507
+ "cwd": str(work_dir),
508
+ "transcript_path": "",
509
+ }
510
+ )
511
+ monkeypatch.setattr(sys, "stdin", io.StringIO(payload_text))
512
+ gate_main()
513
+
514
+
515
+ def test_verification_bypass_marker_allows_an_otherwise_gated_commit(
516
+ monkeypatch: pytest.MonkeyPatch,
517
+ capsys: pytest.CaptureFixture[str],
518
+ tmp_path: pathlib.Path,
519
+ ) -> None:
520
+ fake_home = tmp_path / "home"
521
+ fake_home.mkdir()
522
+ _isolate_home(monkeypatch, fake_home)
523
+ work_dir = _make_gated_repo(tmp_path)
524
+ _run_gate_main(monkeypatch, "git commit -m x", work_dir)
525
+ assert "VERIFIED_COMMIT_GATE" in capsys.readouterr().out
526
+ _run_gate_main(monkeypatch, "git commit -m x # verify-skip", work_dir)
527
+ assert capsys.readouterr().out == ""
@@ -5,6 +5,8 @@ Fires on Bash and PowerShell tool calls. When the command carries a
5
5
  targets, computes the live change-surface manifest against the merge base,
6
6
  and allows the command only when one of these holds:
7
7
 
8
+ - the command carries the verification bypass marker (``# verify-skip``),
9
+ a manual on-the-fly override that skips the gate for that one command,
8
10
  - the repository has no resolvable upstream base — no ``origin/HEAD``, no
9
11
  configured tracking ref, and neither ``origin/main`` nor ``origin/master``
10
12
  (scratch repos with no remote branch are out of scope),
@@ -47,6 +49,7 @@ from config.verified_commit_constants import (
47
49
  OPTION_WITH_VALUE_STEP,
48
50
  REPO_DIRECTORY_OPTION,
49
51
  VALUE_TAKING_GIT_OPTIONS,
52
+ VERIFICATION_BYPASS_MARKER,
50
53
  WORK_TREE_OPTION,
51
54
  )
52
55
  from verification_verdict_store import (
@@ -504,7 +507,12 @@ def deny_reason_for_directory(target_directory: str, transcript_path: str) -> st
504
507
 
505
508
 
506
509
  def main() -> None:
507
- """Read the PreToolUse payload and deny unverified commit/push commands."""
510
+ """Read the PreToolUse payload and decide whether to allow the command.
511
+
512
+ Allows the command without a verdict when it carries the verification
513
+ bypass marker (``VERIFICATION_BYPASS_MARKER``), a manual on-the-fly
514
+ override; otherwise denies an unverified commit or push.
515
+ """
508
516
  try:
509
517
  pretooluse_payload = json.load(sys.stdin)
510
518
  except json.JSONDecodeError:
@@ -514,6 +522,8 @@ def main() -> None:
514
522
  command_text = pretooluse_payload.get("tool_input", {}).get("command", "")
515
523
  if not command_text:
516
524
  return
525
+ if VERIFICATION_BYPASS_MARKER in command_text:
526
+ return
517
527
  session_directory = pretooluse_payload.get("cwd", ".")
518
528
  transcript_path = pretooluse_payload.get("transcript_path", "")
519
529
  for each_target_directory in gated_repo_directories(command_text, session_directory):
@@ -0,0 +1,39 @@
1
+ """Constants for the dead config-dataclass field detector in ``code_rules_enforcer``.
2
+
3
+ Lives under the hooks-tree ``hooks_constants`` package so module-level
4
+ UPPER_SNAKE constants satisfy the CODE_RULES "constants live in config"
5
+ requirement and share a home with the other hook-tree configuration.
6
+ """
7
+
8
+ from hooks_constants.dead_dataclass_field_constants import (
9
+ ALL_REFLECTIVE_FIELD_CONSUMER_NAMES,
10
+ WHOLE_INSTANCE_DICT_ATTRIBUTE_NAME,
11
+ )
12
+ from hooks_constants.dead_module_constant_constants import (
13
+ CONFIG_DIRECTORY_SEGMENT,
14
+ DUNDER_INIT_FILENAME,
15
+ MAX_SCAN_ROOT_FILE_COUNT,
16
+ PYTHON_SOURCE_SUFFIX,
17
+ )
18
+
19
+ CONFIG_CLASS_NAME_SUFFIX: str = "Config"
20
+ DATACLASSES_MODULE_NAME: str = "dataclasses"
21
+ MAX_DEAD_CONFIG_FIELD_ISSUES: int = 25
22
+ DEAD_CONFIG_FIELD_GUIDANCE: str = (
23
+ "config dataclass field is defined but read by no production module in the"
24
+ " enclosing package tree - remove the dead field, or read it where the value"
25
+ " is needed (CODE_RULES §9.8)"
26
+ )
27
+
28
+ __all__ = [
29
+ "ALL_REFLECTIVE_FIELD_CONSUMER_NAMES",
30
+ "CONFIG_CLASS_NAME_SUFFIX",
31
+ "CONFIG_DIRECTORY_SEGMENT",
32
+ "DATACLASSES_MODULE_NAME",
33
+ "DEAD_CONFIG_FIELD_GUIDANCE",
34
+ "DUNDER_INIT_FILENAME",
35
+ "MAX_DEAD_CONFIG_FIELD_ISSUES",
36
+ "MAX_SCAN_ROOT_FILE_COUNT",
37
+ "PYTHON_SOURCE_SUFFIX",
38
+ "WHOLE_INSTANCE_DICT_ATTRIBUTE_NAME",
39
+ ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.61.0",
3
+ "version": "1.62.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -45,3 +45,14 @@ fails in a new way.
45
45
  - **`gh` token drift across accounts.** When a run touches more than one GitHub
46
46
  account, pin the token with `--user <login>`; `gh auth token` alone can return
47
47
  another account's token after a switch.
48
+
49
+ - **The verified-commit gate can block the fix from landing.** The fix lens
50
+ commits and pushes through the `verified_commit_gate` hook, which denies a
51
+ `git commit`/`git push` until a `code-verifier` verdict covers the branch
52
+ surface. A run can reach a clean fix yet fail to land it — the push stays
53
+ blocked when no verdict is minted for that surface. A manual override exists:
54
+ a trailing `# verify-skip` comment on the commit or push command skips the
55
+ gate for that one command. Autoconverge must never apply that override on its
56
+ own. When landing a fix needs it, stop and tell the user the verified-commit
57
+ gate is blocking the push and that going forward needs either a `# verify-skip`
58
+ bypass or a switch to `/pr-converge`, then let the user decide.
@@ -215,6 +215,11 @@ HTML_STYLE_BLOCK = """\
215
215
  .cause { background:#fff; border:1px solid #e2e8f0; border-radius:10px; padding:14px 18px; margin-top:16px; font-size:14px; color:#475569; }
216
216
  .cause b { color:#0f172a; }
217
217
 
218
+ .appendix { background:#fff; border:1px solid #e2e8f0; border-radius:10px; padding:14px 18px; margin-top:16px; font-size:13px; color:#475569; }
219
+ .appendix summary { font-weight:600; color:#0f172a; cursor:pointer; }
220
+ .appendix-body { margin-top:10px; }
221
+ .appendix-item { font-family:'JetBrains Mono',monospace; font-size:12px; color:#475569; padding:5px 0; border-top:1px solid #f1f5f9; }
222
+
218
223
  footer { margin-top:40px; padding-top:16px; border-top:1px solid #e2e8f0; color:#94a3b8; font-size:12px; }
219
224
  footer code { background:#e2e8f0; padding:1px 6px; border-radius:4px; font-family:'JetBrains Mono',monospace; }
220
225
  @media (max-width:680px){ .pf-grid,.term-grid{grid-template-columns:1fr;} }
@@ -43,6 +43,31 @@ def _render_cli(journal_path: Path, out_path: Path) -> subprocess.CompletedProce
43
43
  )
44
44
 
45
45
 
46
+ def test_rendered_report_defines_every_referenced_css_class(tmp_path: Path) -> None:
47
+ """Every class the rendered report markup references resolves to a CSS selector.
48
+
49
+ Renders the report from the findings fixture so the raw-findings appendix is
50
+ present, then asserts no class attribute names a style the stylesheet omits,
51
+ keeping the report markup and HTML_STYLE_BLOCK from drifting apart.
52
+ """
53
+ out_path = tmp_path / "report.html"
54
+ completed = _render_cli(FIXTURE_JOURNAL, out_path)
55
+ assert completed.returncode == 0, f"CLI failed:\n{completed.stderr}"
56
+ html_content = out_path.read_text(encoding="utf-8")
57
+
58
+ style_match = re.search(r"<style>(.*?)</style>", html_content, re.DOTALL)
59
+ assert style_match is not None
60
+ defined_classes = set(re.findall(r"\.([A-Za-z][\w-]*)", style_match.group(1)))
61
+ referenced_classes = {
62
+ each_name
63
+ for attribute_value in re.findall(r'class="([^"]*)"', html_content)
64
+ for each_name in attribute_value.split()
65
+ }
66
+
67
+ orphan_classes = referenced_classes - defined_classes
68
+ assert not orphan_classes, f"classes referenced but undefined: {sorted(orphan_classes)}"
69
+
70
+
46
71
  def _copy_run_tree_without_summary_entry(destination_root: Path) -> Path:
47
72
  """Copy the fixture run tree, dropping the convergence-summary workflowProgress entry.
48
73
 
@@ -58,7 +58,7 @@ Reads HTML from `--input <path>` or stdin (`--input -`), runs `gh gist create`,
58
58
 
59
59
  ## Designing fresh — the example gallery
60
60
 
61
- The skill ships [`references/examples/`](references/examples/) with all 20 of Thariq's html-effectiveness prototypes verbatim from [thariqs.github.io/html-effectiveness](https://thariqs.github.io/html-effectiveness/). They are *examples to learn from, not templates to fill.*
61
+ The skill ships [`references/examples/`](references/examples/) with 21 html-effectiveness examples: Thariq's 20 prototypes verbatim from [thariqs.github.io/html-effectiveness](https://thariqs.github.io/html-effectiveness/) (`01`–`20`), plus one original addition (`21-decision-signoff.html`). They are *examples to learn from, not templates to fill.*
62
62
 
63
63
  When the user requests an artifact, decide the shape that fits. Use the gallery for grounding:
64
64
 
@@ -84,6 +84,7 @@ When the user requests an artifact, decide the shape that fits. Use the gallery
84
84
  | Triage / kanban board (drag-drop) | `18-editor-triage-board.html` |
85
85
  | Feature flag toggles with deps | `19-editor-feature-flags.html` |
86
86
  | Live-updating template editor | `20-editor-prompt-tuner.html` |
87
+ | Decision / sign-off doc (accept-or-change each call, export digest) | `21-decision-signoff.html` |
87
88
 
88
89
  Read the matching example for the artifact you're designing. Crib palette, typography, spatial idioms, component patterns. **Adapt — do not copy.** A PR writeup for a hooks PR shouldn't look identical to one for a notification-queue PR. The gallery teaches what shapes work; the request decides which shape fits.
89
90
 
@@ -92,5 +93,5 @@ Read the matching example for the artifact you're designing. Crib palette, typog
92
93
  - `SKILL.md` — this file.
93
94
  - `skills/doc-gist/scripts/gist_upload.py` — transport: HTML in, gist + preview URLs out.
94
95
  - `skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py` — the URL prefixes and template strings.
95
- - `references/examples/` — Thariq's 20 html-effectiveness prototypes.
96
+ - `references/examples/` — Thariq's 20 html-effectiveness prototypes (`01`–`20`) plus one original addition (`21-decision-signoff.html`).
96
97
  - (PostToolUse hook lives in `packages/claude-dev-env/hooks/workflow/doc_gist_auto_publish.py` — wired into the plugin's `hooks.json`.)