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
@@ -0,0 +1,321 @@
1
+ """Dead config-dataclass field check: cross-module scan for ``*Config`` @dataclass fields.
2
+
3
+ A config ``@dataclass`` (any class whose name ends in ``"Config"``) is defined
4
+ in one module but constructed and consumed in others, so the per-file dead-field
5
+ check in ``code_rules_dead_dataclass_field`` cannot judge its fields — it skips
6
+ any class that is not constructed in the same file. This check resolves the
7
+ enclosing package tree — the scan root — and flags a ``*Config`` dataclass field
8
+ whose name appears as an attribute read (``obj.field``) in no production module
9
+ anywhere under that root.
10
+
11
+ The scan is deliberately conservative to keep false positives near zero:
12
+
13
+ - Only ``@dataclass`` classes whose name ends in ``"Config"`` participate; other
14
+ dataclasses are covered by the per-file check.
15
+ - Test and migration files are exempt as write destinations, so a field added to
16
+ a config dataclass inside a test is never flagged.
17
+ - Production modules under the scan root are scanned for attribute reads; test
18
+ and migration modules are deliberately excluded so a field read only by test
19
+ code is still flagged as dead-in-production.
20
+ - Field reads are collected as ``ast.Attribute.attr`` values (``obj.field``),
21
+ augmented-assignment targets (``cfg.field += 1`` reads ``field`` before
22
+ writing it), string literals (covers ``getattr(obj, "field")``),
23
+ keyword-argument names (covers ``ThemeUpdateConfig(debug_port=1)`` and
24
+ ``replace(cfg, debug_port=1)``), and match-pattern keyword attribute names
25
+ (``case Config(field=found)``). Plain ``ast.Name`` references are excluded — a
26
+ local variable named ``debug_port`` is not a read of ``config.debug_port``.
27
+ - A production module that reflectively reads a whole instance — a bare or
28
+ ``dataclasses``-qualified call to ``asdict``, ``astuple``, ``fields``,
29
+ ``replace``, or ``vars``, or a read of ``obj.__dict__`` — consumes every field
30
+ at once without naming any single field, so the check is suppressed for the
31
+ whole tree (returns ``[]``).
32
+ - A scan root whose total file count exceeds the configured cap cannot prove any
33
+ field dead, so the check returns ``[]`` on a cap hit.
34
+ - A field read only by a module outside the resolved scan root is treated as dead
35
+ — the same conservative scoping the dead-module-constant check accepts.
36
+
37
+ Unlike the per-file dead-dataclass-field check, this cross-module check does NOT
38
+ suppress on a dataclass-dunder whole-instance read — instance comparison
39
+ (``cfg == other``), set or dict membership, formatted-string conversion
40
+ (``f"{cfg}"``), or whole-instance stringification
41
+ (``str(cfg)``/``repr(cfg)``/``format(cfg)``). Those syntactic forms are not bound
42
+ to a config instance, and tree-wide one incidental match anywhere would disable
43
+ the check on any realistic package. The consequence is a documented, rare
44
+ limitation: a ``*Config`` field read ONLY via whole-instance dunder comparison or
45
+ stringification, and never read directly anywhere in production, may be flagged.
46
+ The augmented-assignment read mechanism (``cfg.field += 1`` reads ``field``
47
+ before writing it) is precise and remains a counted read.
48
+ """
49
+
50
+ import ast
51
+ import os
52
+ import sys
53
+ from pathlib import Path
54
+
55
+ _blocking_directory = str(Path(__file__).resolve().parent)
56
+ _hooks_directory = str(Path(__file__).resolve().parent.parent)
57
+ if _blocking_directory not in sys.path:
58
+ sys.path.insert(0, _blocking_directory)
59
+ if _hooks_directory not in sys.path:
60
+ sys.path.insert(0, _hooks_directory)
61
+
62
+ from code_rules_dead_dataclass_field import ( # noqa: E402
63
+ _augmented_assignment_attribute_names,
64
+ _dataclass_field_definitions,
65
+ _is_dataclass,
66
+ )
67
+ from code_rules_dead_module_constant import ( # noqa: E402
68
+ _scan_root_for_constants_module,
69
+ )
70
+ from code_rules_shared import ( # noqa: E402
71
+ is_migration_file,
72
+ is_test_file,
73
+ )
74
+
75
+ from hooks_constants.dead_config_field_constants import ( # noqa: E402
76
+ ALL_REFLECTIVE_FIELD_CONSUMER_NAMES,
77
+ CONFIG_CLASS_NAME_SUFFIX,
78
+ DATACLASSES_MODULE_NAME,
79
+ DEAD_CONFIG_FIELD_GUIDANCE,
80
+ MAX_DEAD_CONFIG_FIELD_ISSUES,
81
+ MAX_SCAN_ROOT_FILE_COUNT,
82
+ PYTHON_SOURCE_SUFFIX,
83
+ WHOLE_INSTANCE_DICT_ATTRIBUTE_NAME,
84
+ )
85
+
86
+
87
+ def _is_config_dataclass(class_node: ast.ClassDef) -> bool:
88
+ """Return whether a class is a @dataclass whose name ends in ``"Config"``.
89
+
90
+ Args:
91
+ class_node: The class definition node to test.
92
+
93
+ Returns:
94
+ True when the class carries a ``@dataclass`` decorator and its name ends
95
+ in ``"Config"``.
96
+ """
97
+ return _is_dataclass(class_node) and class_node.name.endswith(CONFIG_CLASS_NAME_SUFFIX)
98
+
99
+
100
+ def _reads_whole_instance_reflectively(tree: ast.Module) -> bool:
101
+ """Return whether a module consumes a whole instance via a reflective read.
102
+
103
+ Detects a bare call to any reflective whole-instance consumer (``asdict``,
104
+ ``astuple``, ``fields``, ``replace``, ``vars`` imported from ``dataclasses``),
105
+ a ``dataclasses``-qualified call to the same consumers
106
+ (``dataclasses.asdict(cfg)``, ``dataclasses.replace(cfg, ...)``), and a read
107
+ of the ``__dict__`` attribute. The method-call form must be ``dataclasses``-
108
+ qualified — an unrelated ``"text".replace(...)`` or ``frame.fields(...)`` on
109
+ another object does not match. Each matched form reads every field of an
110
+ instance at once without naming any single field, so a module that uses one
111
+ cannot prove a config field unread.
112
+
113
+ Args:
114
+ tree: The parsed module to inspect.
115
+
116
+ Returns:
117
+ True when the module makes a bare or ``dataclasses``-qualified call to a
118
+ reflective whole-instance consumer, or reads ``obj.__dict__``.
119
+ """
120
+ for each_node in ast.walk(tree):
121
+ if isinstance(each_node, ast.Attribute):
122
+ if each_node.attr == WHOLE_INSTANCE_DICT_ATTRIBUTE_NAME:
123
+ return True
124
+ continue
125
+ if not isinstance(each_node, ast.Call):
126
+ continue
127
+ function_node = each_node.func
128
+ if isinstance(function_node, ast.Name) and function_node.id in ALL_REFLECTIVE_FIELD_CONSUMER_NAMES:
129
+ return True
130
+ if (
131
+ isinstance(function_node, ast.Attribute)
132
+ and isinstance(function_node.value, ast.Name)
133
+ and function_node.value.id == DATACLASSES_MODULE_NAME
134
+ and function_node.attr in ALL_REFLECTIVE_FIELD_CONSUMER_NAMES
135
+ ):
136
+ return True
137
+ return False
138
+
139
+
140
+ def _attribute_read_names_in_source(source: str) -> tuple[set[str], bool]:
141
+ """Return attribute names read in a module's source and a suppression flag.
142
+
143
+ Collects attribute names via five mechanisms: ``ast.Attribute.attr`` values
144
+ in Load context, augmented-assignment targets (so ``cfg.debug_port += 1``
145
+ contributes ``"debug_port"`` because ``+=`` reads the attribute before
146
+ writing it), string literals (so ``getattr(obj, "field")`` contributes
147
+ ``"field"``), keyword-argument names (so ``ThemeUpdateConfig(debug_port=1)``
148
+ and ``replace(cfg, debug_port=1)`` each contribute ``"debug_port"``), and
149
+ ``ast.MatchClass.kwd_attrs`` names (so ``case Config(field=x)`` contributes
150
+ ``"field"``). The boolean reports whether the module suppresses the
151
+ dead-field check, which it does only when it reflectively reads a whole
152
+ instance — a bare or ``dataclasses``-qualified ``asdict``/``astuple``/
153
+ ``fields``/``replace``/``vars`` call, or an ``obj.__dict__`` read — because
154
+ that pattern reads every field at once without naming any single field, so
155
+ the caller treats it as "cannot prove any field dead". A ``SyntaxError``
156
+ contributes no names and no suppression.
157
+
158
+ Args:
159
+ source: The full text of a ``.py`` module.
160
+
161
+ Returns:
162
+ A (read_names, suppresses_dead_field_check) pair. The name set is every
163
+ attribute name the module reads via any of the five mechanisms above;
164
+ suppresses_dead_field_check is True only when a reflective whole-instance
165
+ read is present.
166
+ """
167
+ try:
168
+ tree = ast.parse(source)
169
+ except SyntaxError:
170
+ return set(), False
171
+ all_read_names: set[str] = _augmented_assignment_attribute_names(tree)
172
+ for each_node in ast.walk(tree):
173
+ if isinstance(each_node, ast.Attribute) and isinstance(each_node.ctx, ast.Load):
174
+ all_read_names.add(each_node.attr)
175
+ elif isinstance(each_node, ast.Constant) and isinstance(each_node.value, str):
176
+ all_read_names.add(each_node.value)
177
+ elif isinstance(each_node, ast.MatchClass):
178
+ all_read_names.update(each_node.kwd_attrs)
179
+ elif isinstance(each_node, ast.keyword) and each_node.arg is not None:
180
+ all_read_names.add(each_node.arg)
181
+ suppresses_dead_field_check = _reads_whole_instance_reflectively(tree)
182
+ return all_read_names, suppresses_dead_field_check
183
+
184
+
185
+ def _all_production_read_names_under_root(
186
+ scan_root: Path,
187
+ written_path: Path,
188
+ written_content: str,
189
+ ) -> tuple[set[str], bool, bool]:
190
+ """Return read names, a cap-hit flag, and a suppression flag for the tree.
191
+
192
+ Scans every production ``.py`` module under ``scan_root`` (excluding test and
193
+ migration files) for attribute reads. The written module's post-edit content
194
+ replaces its on-disk text so the current edit is included. Scanning stops at
195
+ the configured file cap. A module that reflectively reads a whole instance —
196
+ a bare or ``dataclasses``-qualified ``asdict``/``astuple``/``fields``/
197
+ ``replace``/``vars`` call, or an ``obj.__dict__`` read — sets the suppression
198
+ flag, signalling the caller that no field can be proven dead.
199
+
200
+ Args:
201
+ scan_root: The directory tree to scan.
202
+ written_path: The resolved path of the module being written.
203
+ written_content: The post-edit text of the written module.
204
+
205
+ Returns:
206
+ A (read_names, cap_was_hit, suppresses_dead_field_check) triple. The name
207
+ set is the union of attribute reads across every scanned production
208
+ module; cap_was_hit is True when the scan stopped at the configured file
209
+ cap before finishing the tree; suppresses_dead_field_check is True when
210
+ any scanned module reflectively reads a whole instance.
211
+ """
212
+ all_read_names, suppresses_dead_field_check = _attribute_read_names_in_source(written_content)
213
+ written_path_key = os.path.normcase(str(written_path))
214
+ scanned_file_count = 1
215
+ for each_path in scan_root.rglob("*" + PYTHON_SOURCE_SUFFIX):
216
+ if not each_path.is_file():
217
+ continue
218
+ if os.path.normcase(str(each_path.resolve())) == written_path_key:
219
+ continue
220
+ if is_test_file(str(each_path)):
221
+ continue
222
+ if is_migration_file(str(each_path)):
223
+ continue
224
+ scanned_file_count += 1
225
+ if scanned_file_count > MAX_SCAN_ROOT_FILE_COUNT:
226
+ return all_read_names, True, suppresses_dead_field_check
227
+ try:
228
+ sibling_source = each_path.read_text(encoding="utf-8")
229
+ except (OSError, UnicodeDecodeError):
230
+ continue
231
+ sibling_read_names, sibling_suppresses_dead_field_check = _attribute_read_names_in_source(
232
+ sibling_source
233
+ )
234
+ all_read_names |= sibling_read_names
235
+ suppresses_dead_field_check = (
236
+ suppresses_dead_field_check or sibling_suppresses_dead_field_check
237
+ )
238
+ return all_read_names, False, suppresses_dead_field_check
239
+
240
+
241
+ def check_dead_config_dataclass_fields(
242
+ content: str, file_path: str, full_file_content: str | None = None
243
+ ) -> list[str]:
244
+ """Flag a ``*Config`` @dataclass field read by no production module in the package tree.
245
+
246
+ Runs a cross-module scan restricted to ``@dataclass`` classes whose name ends
247
+ in ``"Config"``. For each such config dataclass in the written file, every
248
+ instance field whose name does not appear as an attribute read (``obj.field``),
249
+ augmented-assignment target (``cfg.field += 1``), string literal,
250
+ keyword-argument name (constructor or ``replace`` keyword), or match-pattern
251
+ keyword attribute in any production module under the enclosing scan root is
252
+ flagged as dead. When any production module under the scan root reflectively
253
+ reads a whole instance — a bare or ``dataclasses``-qualified call to
254
+ ``asdict``, ``astuple``, ``fields``, ``replace``, or ``vars``, or a read of
255
+ ``obj.__dict__`` — the check is suppressed for the whole tree and returns
256
+ ``[]``, since that pattern reads every field at once without naming any single
257
+ field. Test and
258
+ migration files are exempt as write destinations; production modules under the
259
+ scan root are scanned while test and migration modules in the tree are excluded
260
+ so fields read only by test code are still flagged as dead-in-production.
261
+ Whole-file analysis runs against ``full_file_content`` when supplied so an Edit
262
+ fragment is judged against the reconstructed post-edit file. A scan root
263
+ exceeding the file cap returns ``[]`` (cannot prove dead). The scan root is
264
+ resolved the same way as the dead-module-constant check: a ``config/`` module's
265
+ root is its parent directory, a module in a package directory's root is the
266
+ package's parent, and a top-level module's root is its enclosing directory.
267
+
268
+ Args:
269
+ content: The new content under validation (Edit fragment or whole file).
270
+ file_path: The destination path, used for the test/migration exemptions
271
+ and scan-root resolution.
272
+ full_file_content: The reconstructed post-edit whole-file content for an
273
+ Edit, or None for a Write where ``content`` is already the whole file.
274
+
275
+ Returns:
276
+ One violation message per dead config dataclass field, capped at the
277
+ configured maximum. Returns an empty list when the file is exempt, no
278
+ qualifying config dataclass is found, the scan root exceeds the file cap,
279
+ or a SyntaxError prevents parsing.
280
+ """
281
+ if is_test_file(file_path):
282
+ return []
283
+ if is_migration_file(file_path):
284
+ return []
285
+ effective_content = content if full_file_content is None else full_file_content
286
+ try:
287
+ tree = ast.parse(effective_content)
288
+ except SyntaxError:
289
+ return []
290
+ all_config_classes = [
291
+ each_node
292
+ for each_node in ast.walk(tree)
293
+ if isinstance(each_node, ast.ClassDef) and _is_config_dataclass(each_node)
294
+ ]
295
+ if not all_config_classes:
296
+ return []
297
+ scan_root = _scan_root_for_constants_module(file_path)
298
+ written_path = Path(file_path).resolve()
299
+ all_read_names, cap_was_hit, suppresses_dead_field_check = (
300
+ _all_production_read_names_under_root(
301
+ scan_root,
302
+ written_path,
303
+ effective_content,
304
+ )
305
+ )
306
+ if cap_was_hit:
307
+ return []
308
+ if suppresses_dead_field_check:
309
+ return []
310
+ all_issues: list[str] = []
311
+ for each_class in all_config_classes:
312
+ for each_field_name, each_field_line in _dataclass_field_definitions(each_class):
313
+ if each_field_name in all_read_names:
314
+ continue
315
+ all_issues.append(
316
+ f"Line {each_field_line}: config dataclass field {each_field_name!r}"
317
+ f" on {each_class.name} - {DEAD_CONFIG_FIELD_GUIDANCE}"
318
+ )
319
+ if len(all_issues) >= MAX_DEAD_CONFIG_FIELD_ISSUES:
320
+ return all_issues
321
+ return all_issues
@@ -33,6 +33,7 @@ from code_rules_annotations_length import ( # noqa: E402
33
33
  check_known_pytest_fixture_annotations,
34
34
  check_parameter_annotations,
35
35
  check_return_annotations,
36
+ check_unused_known_pytest_fixture_parameters,
36
37
  )
37
38
  from code_rules_banned_identifiers import ( # noqa: E402
38
39
  check_banned_identifiers,
@@ -51,6 +52,9 @@ from code_rules_constants_config import ( # noqa: E402
51
52
  check_constants_outside_config_advisory,
52
53
  check_file_global_constants_use_count,
53
54
  )
55
+ from code_rules_dead_config_field import ( # noqa: E402
56
+ check_dead_config_dataclass_fields,
57
+ )
54
58
  from code_rules_dead_dataclass_field import ( # noqa: E402
55
59
  check_dead_dataclass_fields,
56
60
  )
@@ -89,6 +93,9 @@ from code_rules_optional_params import ( # noqa: E402
89
93
  check_duplicated_format_patterns,
90
94
  check_unused_optional_parameters,
91
95
  )
96
+ from code_rules_orphan_css_class import ( # noqa: E402
97
+ check_orphan_css_classes,
98
+ )
92
99
  from code_rules_paths_syspath import ( # noqa: E402
93
100
  check_hardcoded_user_paths,
94
101
  check_sys_path_insert_deduplication_guard,
@@ -275,12 +282,18 @@ def validate_content(
275
282
  all_issues.extend(
276
283
  check_dead_dataclass_fields(content, file_path, full_file_content)
277
284
  )
285
+ all_issues.extend(
286
+ check_dead_config_dataclass_fields(content, file_path, full_file_content)
287
+ )
278
288
  all_issues.extend(
279
289
  check_dead_module_constants(content, file_path, full_file_content)
280
290
  )
281
291
  all_issues.extend(check_library_print(content, file_path))
282
292
  all_issues.extend(check_parameter_annotations(content, file_path))
283
293
  all_issues.extend(check_known_pytest_fixture_annotations(content, file_path))
294
+ all_issues.extend(
295
+ check_unused_known_pytest_fixture_parameters(content, file_path)
296
+ )
284
297
  all_issues.extend(check_return_annotations(content, file_path))
285
298
  all_issues.extend(
286
299
  check_function_length(
@@ -294,6 +307,7 @@ def validate_content(
294
307
  all_issues.extend(check_inline_literal_collections(content, file_path))
295
308
  all_issues.extend(check_inline_tuple_string_magic(content, file_path))
296
309
  all_issues.extend(check_string_literal_magic(content, file_path))
310
+ all_issues.extend(check_orphan_css_classes(effective_content, file_path))
297
311
  check_incomplete_mocks(content, file_path)
298
312
  check_duplicated_format_patterns(content, file_path)
299
313
  advise_cross_skill_duplicate_helper(effective_content, file_path)
@@ -0,0 +1,196 @@
1
+ """Orphan-CSS-class check: class attributes in markup with no matching selector."""
2
+
3
+ import ast
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ _blocking_directory = str(Path(__file__).resolve().parent)
8
+ _hooks_directory = str(Path(__file__).resolve().parent.parent)
9
+ if _blocking_directory not in sys.path:
10
+ sys.path.insert(0, _blocking_directory)
11
+ if _hooks_directory not in sys.path:
12
+ sys.path.insert(0, _hooks_directory)
13
+
14
+ from code_rules_shared import ( # noqa: E402
15
+ is_test_file,
16
+ )
17
+
18
+ from hooks_constants.orphan_css_class_constants import ( # noqa: E402
19
+ CLASS_ATTRIBUTE_PATTERN,
20
+ CSS_CLASS_SELECTOR_PATTERN,
21
+ MAX_ORPHAN_CSS_CLASS_ISSUES,
22
+ MAX_SIBLING_MODULES_SCANNED,
23
+ ORPHAN_CSS_CLASS_MESSAGE_SUFFIX,
24
+ PYTHON_MODULE_GLOB,
25
+ STYLE_BLOCK_PATTERN,
26
+ )
27
+
28
+
29
+ def _string_literals_with_lines(tree: ast.Module) -> list[tuple[str, int]]:
30
+ """Return every string-constant value in the tree paired with its line number.
31
+
32
+ Args:
33
+ tree: The parsed module to walk.
34
+
35
+ Returns:
36
+ A list of ``(string_value, line_number)`` pairs, one per string constant.
37
+ """
38
+ literals: list[tuple[str, int]] = []
39
+ for each_node in ast.walk(tree):
40
+ if isinstance(each_node, ast.Constant) and isinstance(each_node.value, str):
41
+ literals.append((each_node.value, each_node.lineno))
42
+ return literals
43
+
44
+
45
+ def _class_names_in_attribute(attribute_text: str) -> list[str]:
46
+ """Return the individual class names in a single ``class="..."`` attribute.
47
+
48
+ Args:
49
+ attribute_text: The whitespace-separated class list from one attribute.
50
+
51
+ Returns:
52
+ Each non-empty class token, in order.
53
+ """
54
+ return [each_token for each_token in attribute_text.split() if each_token]
55
+
56
+
57
+ def _class_references_with_lines(
58
+ all_string_literals: list[tuple[str, int]],
59
+ ) -> list[tuple[str, int]]:
60
+ """Return every class name referenced in a ``class="..."`` attribute.
61
+
62
+ Args:
63
+ all_string_literals: The ``(literal_text, line_number)`` constants to scan.
64
+
65
+ Returns:
66
+ A list of ``(class_name, line_number)`` pairs, one per referenced class.
67
+ """
68
+ references: list[tuple[str, int]] = []
69
+ for each_text, each_line in all_string_literals:
70
+ for each_match in CLASS_ATTRIBUTE_PATTERN.finditer(each_text):
71
+ for each_class_name in _class_names_in_attribute(each_match.group(1)):
72
+ references.append((each_class_name, each_line))
73
+ return references
74
+
75
+
76
+ def _defined_class_selectors(all_string_literals: list[tuple[str, int]]) -> set[str]:
77
+ """Return every CSS class name defined by a selector inside a ``<style>`` block.
78
+
79
+ Args:
80
+ all_string_literals: The ``(literal_text, line_number)`` constants to scan.
81
+
82
+ Returns:
83
+ The set of class names that carry a matching ``.<class>`` selector.
84
+ """
85
+ defined: set[str] = set()
86
+ for each_text, _ in all_string_literals:
87
+ for each_style_match in STYLE_BLOCK_PATTERN.finditer(each_text):
88
+ for each_selector in CSS_CLASS_SELECTOR_PATTERN.finditer(
89
+ each_style_match.group(1)
90
+ ):
91
+ defined.add(each_selector.group(1))
92
+ return defined
93
+
94
+
95
+ def _sibling_module_paths(file_path: str) -> list[Path]:
96
+ """Return the importable sibling Python modules near *file_path*.
97
+
98
+ Scans the file's own directory and its immediate child directories, since a
99
+ markup module commonly imports its ``<style>`` constant from a companion
100
+ package directory beside it. The scan is bounded so a large tree never
101
+ stalls a write.
102
+
103
+ Args:
104
+ file_path: The absolute path of the file under validation.
105
+
106
+ Returns:
107
+ The sibling ``.py`` paths to read for cross-module selector resolution,
108
+ excluding the file itself, capped at the scan budget.
109
+ """
110
+ target = Path(file_path)
111
+ base_directory = target.parent
112
+ if not base_directory.is_dir():
113
+ return []
114
+ siblings: list[Path] = []
115
+ for each_path in sorted(base_directory.rglob(PYTHON_MODULE_GLOB)):
116
+ if each_path.resolve() == target.resolve():
117
+ continue
118
+ siblings.append(each_path)
119
+ if len(siblings) >= MAX_SIBLING_MODULES_SCANNED:
120
+ break
121
+ return siblings
122
+
123
+
124
+ def _selectors_from_sibling_modules(file_path: str) -> set[str]:
125
+ """Return CSS class selectors defined in ``<style>`` blocks of sibling modules.
126
+
127
+ Args:
128
+ file_path: The absolute path of the file under validation.
129
+
130
+ Returns:
131
+ The union of class names whose selectors appear in any readable sibling
132
+ module's string literals.
133
+ """
134
+ selectors: set[str] = set()
135
+ for each_sibling in _sibling_module_paths(file_path):
136
+ try:
137
+ sibling_source = each_sibling.read_text(encoding="utf-8")
138
+ except (OSError, UnicodeDecodeError):
139
+ continue
140
+ try:
141
+ sibling_tree = ast.parse(sibling_source)
142
+ except SyntaxError:
143
+ continue
144
+ selectors |= _defined_class_selectors(_string_literals_with_lines(sibling_tree))
145
+ return selectors
146
+
147
+
148
+ def check_orphan_css_classes(content: str, file_path: str) -> list[str]:
149
+ """Flag ``class="..."`` markup whose class has no matching CSS selector.
150
+
151
+ A module that emits HTML names each class it references with a matching
152
+ ``.<class>`` selector, either in a ``<style>`` block in the same file or in
153
+ a companion module beside it. A referenced class with no selector anywhere
154
+ is a dead attribute (or a missing rule), so this flags it. The check only
155
+ fires for a file that itself emits markup, and only after a ``<style>``
156
+ block exists in the file or a sibling — a file with markup but no style
157
+ source nearby is left alone, since its stylesheet lives outside the scan.
158
+ Test files are exempt, since a fixture may carry intentional orphan markup.
159
+
160
+ Args:
161
+ content: The new or whole-file content being written.
162
+ file_path: The destination path of the write or edit.
163
+
164
+ Returns:
165
+ One issue per orphan class reference, capped at the issue budget.
166
+ """
167
+ if is_test_file(file_path):
168
+ return []
169
+ try:
170
+ tree = ast.parse(content)
171
+ except SyntaxError:
172
+ return []
173
+ all_string_literals = _string_literals_with_lines(tree)
174
+ class_references = _class_references_with_lines(all_string_literals)
175
+ if not class_references:
176
+ return []
177
+ defined_selectors = _defined_class_selectors(all_string_literals)
178
+ defined_selectors |= _selectors_from_sibling_modules(file_path)
179
+ if not defined_selectors:
180
+ return []
181
+ issues: list[str] = []
182
+ reported_classes: set[str] = set()
183
+ for each_class_name, each_line in class_references:
184
+ if each_class_name in defined_selectors:
185
+ continue
186
+ if each_class_name in reported_classes:
187
+ continue
188
+ reported_classes.add(each_class_name)
189
+ issues.append(
190
+ f"Line {each_line}: CSS class {each_class_name!r} used in markup"
191
+ f" has no matching '.{each_class_name}' selector - "
192
+ f"{ORPHAN_CSS_CLASS_MESSAGE_SUFFIX}"
193
+ )
194
+ if len(issues) >= MAX_ORPHAN_CSS_CLASS_ISSUES:
195
+ break
196
+ return issues
@@ -47,6 +47,20 @@ WRITE_CALL_REGION_PATTERN = (
47
47
  )
48
48
  VERDICT_KEY_ALL_PASS = "all_pass"
49
49
  VERDICT_KEY_MANIFEST_SHA256 = "manifest_sha256"
50
+ VERDICT_KEY_FINDINGS = "findings"
51
+ SUBAGENTS_DIRECTORY_NAME = "subagents"
52
+ AGENT_TRANSCRIPT_GLOB = "agent-*.jsonl"
53
+ AGENT_META_SIDECAR_SUFFIX = ".meta.json"
54
+ AGENT_META_TYPE_KEY = "agentType"
55
+ TRANSCRIPT_ENTRY_TYPE_KEY = "type"
56
+ TRANSCRIPT_ASSISTANT_ENTRY_TYPE = "assistant"
57
+ TRANSCRIPT_MESSAGE_KEY = "message"
58
+ TRANSCRIPT_CONTENT_KEY = "content"
59
+ TRANSCRIPT_CONTENT_TYPE_KEY = "type"
60
+ TRANSCRIPT_TEXT_CONTENT_TYPE = "text"
61
+ TRANSCRIPT_TEXT_KEY = "text"
62
+ VERDICT_FENCE_PATTERN = r"```verdict\s*\n(.*?)```"
63
+ MANIFEST_HASH_CLI_FLAG = "--manifest-hash"
50
64
  DOCS_ONLY_EXTENSIONS = frozenset(
51
65
  {".md", ".txt", ".rst", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico"}
52
66
  )
@@ -82,9 +96,8 @@ COMMAND_AFTER_DIRECTORY_CHANGE_PATTERN = r"[;&|\n][\s]*\S"
82
96
  OPTION_WITH_VALUE_STEP = 2
83
97
  ALL_GATED_TOOL_NAMES = ("Bash", "PowerShell")
84
98
  HASH_PREVIEW_LENGTH = 16
99
+ VERIFICATION_BYPASS_MARKER = "# verify-skip"
85
100
  MINTING_AGENT_TYPE = "code-verifier"
86
- SPAWN_LOOKUP_ATTEMPT_COUNT = 3
87
- SPAWN_LOOKUP_RETRY_DELAY_SECONDS = 0.1
88
101
  VERDICT_DIRECTORY_GUARD_MESSAGE = (
89
102
  "BLOCKED: [VERDICT_DIRECTORY_GUARD] Shell access to the verification "
90
103
  "verdict directory (~/.claude/verification/) is denied. Only the "