claude-dev-env 1.82.0 → 1.83.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 (67) hide show
  1. package/CLAUDE.md +16 -13
  2. package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  3. package/_shared/pr-loop/scripts/README.md +1 -0
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviewer_availability_constants.py +12 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +0 -2
  7. package/_shared/pr-loop/scripts/reviewer_availability.py +182 -0
  8. package/_shared/pr-loop/scripts/terminology_sweep.py +9 -33
  9. package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
  10. package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +159 -0
  11. package/_shared/pr-loop/scripts/tests/test_reviewer_availability_constants.py +36 -0
  12. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +14 -4
  13. package/hooks/blocking/CLAUDE.md +2 -0
  14. package/hooks/blocking/code_rules_constants_config.py +159 -1
  15. package/hooks/blocking/code_rules_docstrings.py +312 -9
  16. package/hooks/blocking/code_rules_enforcer.py +29 -0
  17. package/hooks/blocking/code_rules_imports_logging.py +867 -1
  18. package/hooks/blocking/code_rules_naming_collection.py +141 -0
  19. package/hooks/blocking/code_rules_string_magic.py +68 -0
  20. package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
  21. package/hooks/blocking/reviewer_spawn_gate.py +182 -0
  22. package/hooks/blocking/stale_comment_reference_blocker.py +267 -0
  23. package/hooks/blocking/state_description_blocker.py +96 -5
  24. package/hooks/blocking/test_code_rules_config_duplicate_path_anchor.py +132 -0
  25. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +2 -0
  26. package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +385 -0
  27. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +67 -0
  28. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +40 -0
  29. package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +213 -0
  30. package/hooks/blocking/test_code_rules_enforcer_referenced_underscore_loop.py +169 -0
  31. package/hooks/blocking/test_code_rules_js_bare_flag_return_directive.py +266 -0
  32. package/hooks/blocking/test_code_rules_js_sibling_return_object_key_drift.py +490 -0
  33. package/hooks/blocking/test_code_rules_logging_adjacent_literals.py +171 -0
  34. package/hooks/blocking/test_pre_tool_use_dispatcher.py +9 -3
  35. package/hooks/blocking/test_reviewer_spawn_gate.py +230 -0
  36. package/hooks/blocking/test_stale_comment_reference_blocker.py +236 -0
  37. package/hooks/blocking/test_state_description_blocker.py +135 -0
  38. package/hooks/hooks.json +5 -0
  39. package/hooks/hooks_constants/CLAUDE.md +3 -1
  40. package/hooks/hooks_constants/blocking_check_limits.py +43 -0
  41. package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -0
  42. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  43. package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +41 -0
  44. package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +76 -0
  45. package/hooks/hooks_constants/state_description_blocker_constants.py +8 -0
  46. package/package.json +1 -1
  47. package/rules/CLAUDE.md +4 -1
  48. package/rules/claude-md-orphan-file.md +5 -0
  49. package/rules/docstring-prose-matches-implementation.md +10 -1
  50. package/rules/env-var-table-code-drift.md +5 -0
  51. package/rules/es-exe-file-search.md +17 -0
  52. package/rules/no-historical-clutter.md +12 -1
  53. package/rules/orphan-css-class.md +5 -0
  54. package/rules/package-inventory-stale-entry.md +10 -0
  55. package/rules/paired-test-coverage.md +5 -0
  56. package/rules/plain-illustrative-docstrings.md +5 -0
  57. package/rules/verify-before-asking.md +7 -0
  58. package/rules/verify-runtime-state.md +40 -0
  59. package/rules/windows-filesystem-safe.md +8 -0
  60. package/rules/workers-done-before-complete.md +33 -0
  61. package/rules/workflow-substitution-slots.md +5 -0
  62. package/skills/CLAUDE.md +1 -1
  63. package/skills/autoconverge/SKILL.md +10 -4
  64. package/skills/autoconverge/workflow/converge.contract.test.mjs +69 -0
  65. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +54 -18
  66. package/skills/autoconverge/workflow/converge.mjs +97 -33
  67. package/skills/everything-search/SKILL.md +5 -0
@@ -1,4 +1,9 @@
1
- """Constants-outside-config checks and the file-global constant use-count check."""
1
+ """Constants-outside-config checks and the file-global constant use-count check.
2
+
3
+ Also carries check_config_duplicate_path_anchor, which flags a config module
4
+ that rebuilds a directory a sibling module in the same package already
5
+ anchors from its own location.
6
+ """
2
7
 
3
8
  import ast
4
9
  import re
@@ -24,6 +29,9 @@ from code_rules_shared import ( # noqa: E402
24
29
  is_workflow_registry_file,
25
30
  )
26
31
 
32
+ from hooks_constants.blocking_check_limits import ( # noqa: E402
33
+ MAX_CONFIG_DUPLICATE_PATH_ANCHOR_ISSUES,
34
+ )
27
35
  from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
28
36
  ALL_PYTHON_EXTENSIONS,
29
37
  FILE_GLOBAL_UPPER_SNAKE_PATTERN,
@@ -253,3 +261,153 @@ def check_file_global_constants_use_count(content: str, file_path: str) -> list[
253
261
  )
254
262
 
255
263
  return issues
264
+
265
+
266
+ def _references_dunder_file(node: ast.AST) -> bool:
267
+ """Return True when the expression tree reads ``__file__``."""
268
+ return any(
269
+ isinstance(each_node, ast.Name) and each_node.id == "__file__"
270
+ for each_node in ast.walk(node)
271
+ )
272
+
273
+
274
+ def _file_anchor_up_count(node: ast.AST) -> Optional[int]:
275
+ """Return how many directory levels a ``__file__`` anchor climbs, else None.
276
+
277
+ A ``parents[N]`` subscript on a ``__file__``-rooted expression climbs
278
+ ``N + 1`` levels; a chain of ``.parent`` attributes climbs one level per
279
+ link. Any other expression returns None.
280
+ """
281
+ if isinstance(node, ast.Subscript):
282
+ subscripted = node.value
283
+ if (
284
+ isinstance(subscripted, ast.Attribute)
285
+ and subscripted.attr == "parents"
286
+ and isinstance(node.slice, ast.Constant)
287
+ and isinstance(node.slice.value, int)
288
+ and _references_dunder_file(subscripted.value)
289
+ ):
290
+ return node.slice.value + 1
291
+ parent_step_count = 0
292
+ current_node = node
293
+ while isinstance(current_node, ast.Attribute) and current_node.attr == "parent":
294
+ parent_step_count += 1
295
+ current_node = current_node.value
296
+ if parent_step_count and _references_dunder_file(current_node):
297
+ return parent_step_count
298
+ return None
299
+
300
+
301
+ def _anchored_join_signature(node: ast.AST) -> Optional[tuple[int, str]]:
302
+ """Return (up_count, first literal segment) for an anchored ``/`` join, else None.
303
+
304
+ Flattens a left-leaning ``/`` chain, requires the left-most operand to be
305
+ a ``__file__`` anchor, and requires the first joined segment to be a
306
+ string literal.
307
+ """
308
+ if not isinstance(node, ast.BinOp) or not isinstance(node.op, ast.Div):
309
+ return None
310
+ all_segments: list[str] = []
311
+ current_node: ast.expr = node
312
+ while isinstance(current_node, ast.BinOp) and isinstance(current_node.op, ast.Div):
313
+ right_operand = current_node.right
314
+ if isinstance(right_operand, ast.Constant) and isinstance(right_operand.value, str):
315
+ all_segments.insert(0, right_operand.value)
316
+ else:
317
+ all_segments.insert(0, "")
318
+ current_node = current_node.left
319
+ anchor_up_count = _file_anchor_up_count(current_node)
320
+ if anchor_up_count is None or not all_segments or not all_segments[0]:
321
+ return None
322
+ return anchor_up_count, all_segments[0]
323
+
324
+
325
+ def _module_anchor_signatures(module_tree: ast.Module) -> dict[tuple[int, str], int]:
326
+ """Return line numbers keyed by anchored-join signature for top-level assignments."""
327
+ signatures_by_key: dict[tuple[int, str], int] = {}
328
+ for each_statement in module_tree.body:
329
+ if isinstance(each_statement, ast.Assign):
330
+ assigned_expression = each_statement.value
331
+ elif isinstance(each_statement, ast.AnnAssign) and each_statement.value is not None:
332
+ assigned_expression = each_statement.value
333
+ else:
334
+ continue
335
+ for each_node in ast.walk(assigned_expression):
336
+ signature = _anchored_join_signature(each_node)
337
+ if signature is not None:
338
+ signatures_by_key.setdefault(signature, each_statement.lineno)
339
+ return signatures_by_key
340
+
341
+
342
+ def _config_anchor_check_applies(file_path: str) -> bool:
343
+ """Return whether the duplicate-path-anchor check should inspect this file."""
344
+ return (
345
+ is_config_file(file_path)
346
+ and not is_test_file(file_path)
347
+ and get_file_extension(file_path) in ALL_PYTHON_EXTENSIONS
348
+ )
349
+
350
+
351
+ def _sibling_anchor_owner_names(config_directory: Path, written_name: str) -> dict[tuple[int, str], str]:
352
+ """Return the sibling config module name that first builds each anchor signature."""
353
+ owner_name_by_signature: dict[tuple[int, str], str] = {}
354
+ for each_sibling_path in sorted(config_directory.glob("*.py")):
355
+ if each_sibling_path.name == written_name or is_test_file(str(each_sibling_path)):
356
+ continue
357
+ try:
358
+ sibling_tree = ast.parse(each_sibling_path.read_text(encoding="utf-8"))
359
+ except (OSError, SyntaxError, UnicodeDecodeError):
360
+ continue
361
+ for each_signature in _module_anchor_signatures(sibling_tree):
362
+ owner_name_by_signature.setdefault(each_signature, each_sibling_path.name)
363
+ return owner_name_by_signature
364
+
365
+
366
+ def check_config_duplicate_path_anchor(content: str, file_path: str) -> list[str]:
367
+ """Flag a config module re-anchoring a path a sibling config module already builds.
368
+
369
+ Two config modules in the same ``config/`` directory that each anchor
370
+ ``Path(__file__)`` the same number of levels up and join the same first
371
+ literal segment are two sources of truth for one directory: a rename in
372
+ one module silently leaves the other pointing at the old folder. The
373
+ check fires on a config-module write whose module-level assignment joins
374
+ a ``__file__`` anchor (``parents[N]`` or a ``.parent`` chain) with a
375
+ literal segment that a sibling module in the same directory also joins at
376
+ the same depth. Compose one shared base constant and build the other path
377
+ from it. Test files, non-Python files, and modules outside a config
378
+ directory are exempt; an unreadable or unparseable sibling is skipped.
379
+
380
+ Args:
381
+ content: The Python source under validation.
382
+ file_path: The destination path, used to locate sibling config modules.
383
+
384
+ Returns:
385
+ One issue line per duplicated anchor, capped at the configured maximum.
386
+ """
387
+ if not _config_anchor_check_applies(file_path):
388
+ return []
389
+ try:
390
+ module_tree = ast.parse(content)
391
+ except SyntaxError:
392
+ return []
393
+ written_signatures = _module_anchor_signatures(module_tree)
394
+ if not written_signatures:
395
+ return []
396
+ config_directory = Path(file_path).parent
397
+ if not config_directory.is_dir():
398
+ return []
399
+ owner_name_by_signature = _sibling_anchor_owner_names(config_directory, Path(file_path).name)
400
+ issues: list[str] = []
401
+ for each_signature, each_line_number in sorted(written_signatures.items(), key=lambda pair: pair[1]):
402
+ owner_name = owner_name_by_signature.get(each_signature)
403
+ if owner_name is None:
404
+ continue
405
+ anchor_up_count, first_segment = each_signature
406
+ issues.append(
407
+ f"Line {each_line_number}: joins {first_segment} onto the same base, "
408
+ f"{anchor_up_count} levels above this file, that {owner_name} "
409
+ "already builds - define the base once and compose both paths from it"
410
+ )
411
+ if len(issues) >= MAX_CONFIG_DUPLICATE_PATH_ANCHOR_ISSUES:
412
+ break
413
+ return issues
@@ -25,6 +25,7 @@ from hooks_constants.blocking_check_limits import ( # noqa: E402
25
25
  ALL_ABSENT_TYPE_CHECKING_GATE_DOCSTRING_PHRASES,
26
26
  ALL_DATA_SCHEMA_CONSTANT_NAME_MARKERS,
27
27
  ALL_DATA_SCHEMA_DOCSTRING_ACKNOWLEDGEMENT_PHRASES,
28
+ ALL_DELEGATION_ENUMERATION_STOP_TOKENS,
28
29
  ALL_DOCSTRING_EXCLUSIVE_SCOPE_PHRASES,
29
30
  ALL_DOCSTRING_EXEMPT_DECORATOR_NAMES,
30
31
  ALL_DOCSTRING_FILE_REFERENCE_SUFFIXES,
@@ -63,6 +64,7 @@ from hooks_constants.blocking_check_limits import ( # noqa: E402
63
64
  MAX_DOCSTRING_ARGS_SIGNATURE_ISSUES,
64
65
  MAX_DOCSTRING_ARGS_SPAN_SCOPE_ISSUES,
65
66
  MAX_DOCSTRING_CARDINAL_FAMILY_ISSUES,
67
+ MAX_DOCSTRING_DELEGATION_ENUMERATION_ISSUES,
66
68
  MAX_DOCSTRING_FALLBACK_BRANCH_ISSUES,
67
69
  MAX_DOCSTRING_FIELD_RUNMODE_OUTCOME_ISSUES,
68
70
  MAX_DOCSTRING_FORMAT_ISSUES,
@@ -82,16 +84,21 @@ from hooks_constants.blocking_check_limits import ( # noqa: E402
82
84
  MAX_MODULE_DOCSTRING_CHECK_ROSTER_ISSUES,
83
85
  MAX_MODULE_DOCSTRING_DATA_SCHEMA_SCOPE_ISSUES,
84
86
  MINIMUM_CONSTANT_FAMILY_MEMBERS_FOR_CARDINAL_CHECK,
87
+ MINIMUM_DELEGATION_ENUMERATION_ITEMS,
85
88
  MINIMUM_DOCSTRING_FAMILY_OVERLAP_FOR_CARDINAL_CHECK,
86
89
  MINIMUM_NAMED_LINEAR_STEPS_FOR_DISPATCH_CHECK,
87
90
  MINIMUM_NAMED_MARKS_FOR_PROSE_ENUMERATION,
88
91
  MINIMUM_PUBLIC_CHECKS_FOR_MODULE_DOCSTRING_ROSTER,
89
92
  MINIMUM_PUBLIC_METHODS_FOR_CLASS_DOCSTRING_BREADTH,
93
+ MINIMUM_SIBLING_OCCURRENCES_FOR_SHARED_TOKEN,
90
94
  MINIMUM_TOKENS_FOR_DISPATCH_CALLEE,
91
95
  MINIMUM_TUPLE_MEMBERS_FOR_DOCSTRING_ENUMERATION,
92
96
  MODULE_DOCSTRING_DATA_SCHEMA_CONSTANT_SAMPLE_LIMIT,
97
+ NEIGHBOR_SCAN_FILE_LIMIT,
98
+ POINTER_TO_DELEGATE_PATTERN,
93
99
  PYTHON_MODULE_FILE_SUFFIX,
94
100
  SINGLE_DICT_KEY_COUNT_FOR_PLURAL_CARDINALITY_DRIFT,
101
+ TOKEN_WORD_PATTERN,
95
102
  TYPE_CHECKING_IDENTIFIER_MARKER,
96
103
  WORD_BOUNDARY_REGEX,
97
104
  ZIPFILE_ALLOW_ZIP64_KEYWORD,
@@ -1128,14 +1135,48 @@ def _distinctive_name_tokens(check_name: str) -> list[str]:
1128
1135
  ]
1129
1136
 
1130
1137
 
1131
- def _docstring_mentions_check(docstring_text: str, check_name: str) -> bool:
1138
+ def _shared_sibling_name_tokens(all_check_names: list[str]) -> frozenset[str]:
1139
+ token_occurrence_count: dict[str, int] = {}
1140
+ for each_check_name in all_check_names:
1141
+ for each_token in set(each_token.lower() for each_token in _name_tokens(each_check_name)):
1142
+ token_occurrence_count[each_token] = token_occurrence_count.get(each_token, 0) + 1
1143
+ return frozenset(
1144
+ each_token
1145
+ for each_token, each_count in token_occurrence_count.items()
1146
+ if each_count >= MINIMUM_SIBLING_OCCURRENCES_FOR_SHARED_TOKEN
1147
+ )
1148
+
1149
+
1150
+ def _module_distinctive_name_tokens(
1151
+ check_name: str, all_shared_sibling_tokens: frozenset[str]
1152
+ ) -> list[str]:
1153
+ return [
1154
+ each_token
1155
+ for each_token in _distinctive_name_tokens(check_name)
1156
+ if each_token.lower() not in all_shared_sibling_tokens
1157
+ ]
1158
+
1159
+
1160
+ def _token_present_in_text(token: str, lowered_text: str) -> bool:
1161
+ lowered_token = token.lower()
1162
+ if lowered_token in lowered_text:
1163
+ return True
1164
+ return lowered_token.endswith("s") and lowered_token[:-1] in lowered_text
1165
+
1166
+
1167
+ def _docstring_mentions_check(
1168
+ docstring_text: str, check_name: str, all_shared_sibling_tokens: frozenset[str]
1169
+ ) -> bool:
1132
1170
  lowered_docstring = docstring_text.lower()
1133
1171
  if check_name.lower() in lowered_docstring:
1134
1172
  return True
1135
- distinctive_tokens = _distinctive_name_tokens(check_name)
1136
- if not distinctive_tokens:
1173
+ distinguishing_tokens = _module_distinctive_name_tokens(check_name, all_shared_sibling_tokens)
1174
+ if not distinguishing_tokens:
1137
1175
  return True
1138
- return any(each_token.lower() in lowered_docstring for each_token in distinctive_tokens)
1176
+ return any(
1177
+ _token_present_in_text(each_token, lowered_docstring)
1178
+ for each_token in distinguishing_tokens
1179
+ )
1139
1180
 
1140
1181
 
1141
1182
  def check_module_docstring_names_public_checks(content: str, file_path: str) -> list[str]:
@@ -1146,10 +1187,17 @@ def check_module_docstring_names_public_checks(content: str, file_path: str) ->
1146
1187
  roster. When the module grows a public ``check_*`` entry point the summary
1147
1188
  never names, the enumeration under-describes the module — the
1148
1189
  docstring-prose-vs-implementation drift the repo flags as Category O6/O8.
1149
- A check counts as named when the full ``check_*`` name, or any distinctive
1150
- (non-generic) underscore-separated token of it, appears in the summary;
1151
- generic tokens (``check``, ``test``, ``tests``) do not count. A module with
1152
- two or more public checks and any check the summary never names is reported
1190
+ A check counts as named when the full ``check_*`` name, or a
1191
+ module-distinctive underscore-separated token of it, appears in the summary
1192
+ (matched allowing a trailing-``s`` plural). A token is module-distinctive
1193
+ when it is non-generic (generic tokens ``check``, ``test``, ``tests`` never
1194
+ count) and it is not shared by two or more of the module's check names — a
1195
+ token such as ``string`` or ``magic`` that recurs across sibling checks is
1196
+ no evidence that any one check is named, so a check whose only
1197
+ summary-present tokens are shared ones is reported. A check with no
1198
+ module-distinctive token is treated as named, since the summary cannot single
1199
+ it out. A module with two or more public checks and any check the summary
1200
+ never names is reported
1153
1201
  so the summary names the full roster. Modules with a multi-paragraph
1154
1202
  docstring body are left to the audit lane, since their prose can carry the
1155
1203
  roster without naming each check by name. This check covers hook
@@ -1175,9 +1223,10 @@ def check_module_docstring_names_public_checks(content: str, file_path: str) ->
1175
1223
  public_check_names = _module_public_check_names(parsed_tree)
1176
1224
  if len(public_check_names) < MINIMUM_PUBLIC_CHECKS_FOR_MODULE_DOCSTRING_ROSTER:
1177
1225
  return []
1226
+ all_shared_sibling_tokens = _shared_sibling_name_tokens(public_check_names)
1178
1227
  issues: list[str] = []
1179
1228
  for each_name in public_check_names:
1180
- if _docstring_mentions_check(module_docstring, each_name):
1229
+ if _docstring_mentions_check(module_docstring, each_name, all_shared_sibling_tokens):
1181
1230
  continue
1182
1231
  issues.append(
1183
1232
  f"Line 1: module docstring omits public check {each_name}() — name every "
@@ -2874,3 +2923,257 @@ def check_docstring_raises_unraisable_largezipfile(
2874
2923
  if len(issues) >= MAX_DOCSTRING_RAISES_LARGEZIPFILE_ISSUES:
2875
2924
  break
2876
2925
  return issues[:MAX_DOCSTRING_RAISES_LARGEZIPFILE_ISSUES]
2926
+
2927
+
2928
+ def _leading_summary_line(raw_docstring: str) -> str:
2929
+ summary_paragraph = raw_docstring.strip().split("\n\n", maxsplit=1)[0]
2930
+ return " ".join(summary_paragraph.split())
2931
+
2932
+
2933
+ def _pointer_target_stem(raw_docstring: str) -> str:
2934
+ marker_match = POINTER_TO_DELEGATE_PATTERN.search(raw_docstring)
2935
+ return marker_match.group(1) if marker_match is not None else ""
2936
+
2937
+
2938
+ def _listed_entries(wrapper_summary: str) -> list[str]:
2939
+ marker_match = POINTER_TO_DELEGATE_PATTERN.search(wrapper_summary)
2940
+ if marker_match is None:
2941
+ return []
2942
+ listed_text = wrapper_summary[: marker_match.start()].strip().rstrip(";,").strip()
2943
+ normalized_text = re.sub(r"\s+and\s+", ", ", listed_text, flags=re.IGNORECASE)
2944
+ return [
2945
+ each_entry.strip()
2946
+ for each_entry in normalized_text.split(",")
2947
+ if each_entry.strip()
2948
+ ]
2949
+
2950
+
2951
+ def _words_of_entry(raw_entry: str) -> list[str]:
2952
+ return [
2953
+ each_token.lower()
2954
+ for each_token in TOKEN_WORD_PATTERN.findall(raw_entry)
2955
+ if each_token.lower() not in ALL_DELEGATION_ENUMERATION_STOP_TOKENS
2956
+ ]
2957
+
2958
+
2959
+ def _matches_any_word(delegate_summary: str, all_distinct_words: list[str]) -> bool:
2960
+ lowered_summary = delegate_summary.lower()
2961
+ return any(
2962
+ re.search(
2963
+ WORD_BOUNDARY_REGEX + re.escape(each_token) + WORD_BOUNDARY_REGEX,
2964
+ lowered_summary,
2965
+ )
2966
+ is not None
2967
+ for each_token in all_distinct_words
2968
+ )
2969
+
2970
+
2971
+ def _absent_entries(wrapper_summary: str, delegate_summary: str) -> list[str]:
2972
+ all_entries = _listed_entries(wrapper_summary)
2973
+ if len(all_entries) < MINIMUM_DELEGATION_ENUMERATION_ITEMS:
2974
+ return []
2975
+ if "," not in delegate_summary:
2976
+ return []
2977
+ absent_entry_texts: list[str] = []
2978
+ for each_entry in all_entries:
2979
+ distinct_words = _words_of_entry(each_entry)
2980
+ if not distinct_words:
2981
+ continue
2982
+ if _matches_any_word(delegate_summary, distinct_words):
2983
+ continue
2984
+ absent_entry_texts.append(each_entry)
2985
+ return absent_entry_texts
2986
+
2987
+
2988
+ def _top_level_definitions_and_methods(
2989
+ parsed_tree: ast.Module,
2990
+ ) -> list[ast.FunctionDef | ast.AsyncFunctionDef]:
2991
+ all_definitions_and_methods: list[ast.FunctionDef | ast.AsyncFunctionDef] = []
2992
+ for each_node in parsed_tree.body:
2993
+ if isinstance(each_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
2994
+ all_definitions_and_methods.append(each_node)
2995
+ elif isinstance(each_node, ast.ClassDef):
2996
+ all_definitions_and_methods.extend(
2997
+ each_member
2998
+ for each_member in each_node.body
2999
+ if isinstance(each_member, (ast.FunctionDef, ast.AsyncFunctionDef))
3000
+ )
3001
+ return all_definitions_and_methods
3002
+
3003
+
3004
+ def _entries_by_function_name(parsed_tree: ast.Module) -> dict[str, tuple[int, str]]:
3005
+ entries_by_name: dict[str, tuple[int, str]] = {}
3006
+ ambiguous_names: set[str] = set()
3007
+ for each_definition in _top_level_definitions_and_methods(parsed_tree):
3008
+ if each_definition.name in entries_by_name:
3009
+ ambiguous_names.add(each_definition.name)
3010
+ continue
3011
+ entries_by_name[each_definition.name] = (
3012
+ each_definition.lineno,
3013
+ ast.get_docstring(each_definition) or "",
3014
+ )
3015
+ for each_ambiguous_name in ambiguous_names:
3016
+ del entries_by_name[each_ambiguous_name]
3017
+ return entries_by_name
3018
+
3019
+
3020
+ def _source_text_or_empty(source_path: Path) -> str:
3021
+ try:
3022
+ return source_path.read_text(encoding="utf-8")
3023
+ except (OSError, UnicodeDecodeError):
3024
+ return ""
3025
+
3026
+
3027
+ def _parsed_module_or_none(source_text: str) -> ast.Module | None:
3028
+ if not source_text:
3029
+ return None
3030
+ try:
3031
+ return ast.parse(source_text)
3032
+ except SyntaxError:
3033
+ return None
3034
+
3035
+
3036
+ def _outbound_pointer_issues(parsed_tree: ast.Module, file_path: str) -> list[str]:
3037
+ parent_directory = Path(file_path).parent
3038
+ issues: list[str] = []
3039
+ delegate_entries_by_stem: dict[str, dict[str, tuple[int, str]]] = {}
3040
+ for each_node in _top_level_definitions_and_methods(parsed_tree):
3041
+ wrapper_docstring = ast.get_docstring(each_node) or ""
3042
+ wrapper_summary = _leading_summary_line(wrapper_docstring)
3043
+ target_stem = _pointer_target_stem(wrapper_summary)
3044
+ if not target_stem:
3045
+ continue
3046
+ if target_stem not in delegate_entries_by_stem:
3047
+ delegate_path = parent_directory / (target_stem + PYTHON_MODULE_FILE_SUFFIX)
3048
+ delegate_tree = _parsed_module_or_none(_source_text_or_empty(delegate_path))
3049
+ delegate_entries_by_stem[target_stem] = (
3050
+ _entries_by_function_name(delegate_tree)
3051
+ if delegate_tree is not None
3052
+ else {}
3053
+ )
3054
+ delegate_entry = delegate_entries_by_stem[target_stem].get(each_node.name)
3055
+ if delegate_entry is None:
3056
+ continue
3057
+ delegate_summary = _leading_summary_line(delegate_entry[1])
3058
+ for each_entry in _absent_entries(wrapper_summary, delegate_summary):
3059
+ issues.append(
3060
+ f"Line {each_node.lineno}: {each_node.name}() names "
3061
+ f"'{each_entry}' in its summary, but the summary of the "
3062
+ f"same-named function in {target_stem} omits it - align the "
3063
+ "two summaries (Category O6 docstring-vs-implementation drift)"
3064
+ )
3065
+ return issues
3066
+
3067
+
3068
+ def _scannable_neighbor_paths(written_path: Path) -> list[Path]:
3069
+ try:
3070
+ all_neighbor_paths = sorted(
3071
+ written_path.parent.glob("*" + PYTHON_MODULE_FILE_SUFFIX)
3072
+ )
3073
+ except OSError:
3074
+ return []
3075
+ scannable_paths: list[Path] = []
3076
+ for each_path in all_neighbor_paths:
3077
+ if each_path.name == written_path.name:
3078
+ continue
3079
+ if is_strict_test_file(str(each_path)):
3080
+ continue
3081
+ scannable_paths.append(each_path)
3082
+ if len(scannable_paths) >= NEIGHBOR_SCAN_FILE_LIMIT:
3083
+ break
3084
+ return scannable_paths
3085
+
3086
+
3087
+ def _inbound_pointer_issues(
3088
+ neighbor_tree: ast.Module,
3089
+ neighbor_name: str,
3090
+ delegate_stem: str,
3091
+ all_entries_by_name: dict[str, tuple[int, str]],
3092
+ ) -> list[str]:
3093
+ issues: list[str] = []
3094
+ for each_node in _top_level_definitions_and_methods(neighbor_tree):
3095
+ wrapper_docstring = ast.get_docstring(each_node) or ""
3096
+ wrapper_summary = _leading_summary_line(wrapper_docstring)
3097
+ if _pointer_target_stem(wrapper_summary) != delegate_stem:
3098
+ continue
3099
+ delegate_entry = all_entries_by_name.get(each_node.name)
3100
+ if delegate_entry is None:
3101
+ continue
3102
+ delegate_summary = _leading_summary_line(delegate_entry[1])
3103
+ for each_entry in _absent_entries(wrapper_summary, delegate_summary):
3104
+ issues.append(
3105
+ f"Line {delegate_entry[0]}: {each_node.name}() summary omits "
3106
+ f"'{each_entry}', named by the pointing wrapper docstring in "
3107
+ f"{neighbor_name} - reword that wrapper docstring in the same "
3108
+ "change (Category O6 docstring-vs-implementation drift)"
3109
+ )
3110
+ return issues
3111
+
3112
+
3113
+ def _neighbor_scan_issues(parsed_tree: ast.Module, file_path: str) -> list[str]:
3114
+ written_path = Path(file_path)
3115
+ all_entries_by_name = _entries_by_function_name(parsed_tree)
3116
+ if not all_entries_by_name:
3117
+ return []
3118
+ issues: list[str] = []
3119
+ for each_neighbor in _scannable_neighbor_paths(written_path):
3120
+ neighbor_source = _source_text_or_empty(each_neighbor)
3121
+ if POINTER_TO_DELEGATE_PATTERN.search(neighbor_source) is None:
3122
+ continue
3123
+ neighbor_tree = _parsed_module_or_none(neighbor_source)
3124
+ if neighbor_tree is None:
3125
+ continue
3126
+ issues.extend(
3127
+ _inbound_pointer_issues(
3128
+ neighbor_tree,
3129
+ each_neighbor.name,
3130
+ written_path.stem,
3131
+ all_entries_by_name,
3132
+ )
3133
+ )
3134
+ return issues
3135
+
3136
+
3137
+ def check_docstring_delegation_summary_enumeration_drift(
3138
+ content: str, file_path: str
3139
+ ) -> list[str]:
3140
+ """Flag a delegating wrapper summary that its delegation target contradicts.
3141
+
3142
+ The drift this catches: a thin wrapper method whose docstring summary
3143
+ enumerates the actions it stands for and points at the home of the real
3144
+ body ("Apply App Info, Russia, review note, publication edits; full doc
3145
+ on `listing_edit_flow`"), while the same-named function in that named
3146
+ sibling file carries a summary enumeration that omits one of those
3147
+ actions. An edit that moves one action out of the delegated body and
3148
+ rewords only the delegated docstring leaves the wrapper claiming work the
3149
+ body skips. The gate fires from both sides. On a wrapper save, it resolves
3150
+ the named sibling file beside the saved one, finds the same-named
3151
+ function, and compares the two summary enumerations. On a delegated-body
3152
+ save, it scans neighboring .py files for a wrapper docstring pointing at
3153
+ the saved stem and runs the same comparison, so the save that removes the
3154
+ action hears about the stranded wrapper docstring. A listed action counts
3155
+ as named when any of its distinctive words appears at a word boundary in
3156
+ the delegated summary. The comparison binds when the wrapper enumerates
3157
+ two or more actions and the delegated summary itself holds a comma, so a
3158
+ plain one-purpose delegated summary never gets compared. This covers the
3159
+ deterministic delegating-wrapper slice of Category O6
3160
+ docstring-prose-vs-implementation drift.
3161
+
3162
+ Args:
3163
+ content: The source text to inspect.
3164
+ file_path: The path the source will be written to, used for exemptions
3165
+ and to resolve the delegated or wrapping file beside it.
3166
+
3167
+ Returns:
3168
+ One issue per listed wrapper action the paired delegated summary
3169
+ omits, capped at the module limit.
3170
+ """
3171
+ if is_strict_test_file(file_path) or is_hook_infrastructure(file_path):
3172
+ return []
3173
+ try:
3174
+ parsed_tree = ast.parse(content)
3175
+ except SyntaxError:
3176
+ return []
3177
+ issues = _outbound_pointer_issues(parsed_tree, file_path)
3178
+ issues.extend(_neighbor_scan_issues(parsed_tree, file_path))
3179
+ return issues[:MAX_DOCSTRING_DELEGATION_ENUMERATION_ISSUES]
@@ -51,6 +51,7 @@ from code_rules_comments import ( # noqa: E402
51
51
  check_comment_changes,
52
52
  )
53
53
  from code_rules_constants_config import ( # noqa: E402
54
+ check_config_duplicate_path_anchor,
54
55
  check_constants_outside_config,
55
56
  check_constants_outside_config_advisory,
56
57
  check_file_global_constants_use_count,
@@ -75,6 +76,7 @@ from code_rules_docstrings import ( # noqa: E402
75
76
  check_docstring_args_match_signature,
76
77
  check_docstring_args_single_line_scope_vs_span,
77
78
  check_docstring_cardinal_count_matches_constant_family,
79
+ check_docstring_delegation_summary_enumeration_drift,
78
80
  check_docstring_documents_unreferenced_parameter,
79
81
  check_docstring_fallback_branch_coverage,
80
82
  check_docstring_field_runmode_outcome,
@@ -105,11 +107,15 @@ from code_rules_imports_logging import ( # noqa: E402
105
107
  check_e2e_test_naming,
106
108
  check_import_block_sorted,
107
109
  check_imports_at_top,
110
+ check_js_bare_flag_return_directive,
108
111
  check_js_resume_task_enumeration_coverage,
109
112
  check_js_returns_object_schemaless_branch,
113
+ check_js_sibling_return_object_key_drift,
110
114
  check_library_print,
115
+ check_logging_adjacent_string_literals,
111
116
  check_logging_fstrings,
112
117
  check_logging_printf_tokens,
118
+ check_naive_datetime_construction,
113
119
  check_windows_api_none,
114
120
  )
115
121
  from code_rules_js_conventions import ( # noqa: E402
@@ -127,6 +133,7 @@ from code_rules_naming_collection import ( # noqa: E402
127
133
  check_collection_prefix,
128
134
  check_loop_variable_naming,
129
135
  check_polarity_name_contradiction,
136
+ check_referenced_underscore_loop_variable,
130
137
  check_stuttering_collection_prefix,
131
138
  )
132
139
  from code_rules_optional_params import ( # noqa: E402
@@ -154,6 +161,7 @@ from code_rules_shared import ( # noqa: E402
154
161
  from code_rules_string_magic import ( # noqa: E402
155
162
  check_inline_literal_collections,
156
163
  check_inline_tuple_string_magic,
164
+ check_join_separator_string_magic,
157
165
  check_string_literal_magic,
158
166
  check_whitespace_indentation_magic,
159
167
  )
@@ -268,10 +276,13 @@ def validate_content(
268
276
  )
269
277
  all_issues.extend(check_logging_fstrings(content))
270
278
  all_issues.extend(check_logging_printf_tokens(content, file_path))
279
+ all_issues.extend(check_logging_adjacent_string_literals(content, file_path))
271
280
  all_issues.extend(check_windows_api_none(content))
281
+ all_issues.extend(check_naive_datetime_construction(content, file_path))
272
282
  all_issues.extend(check_magic_values(content, file_path))
273
283
  all_issues.extend(check_fstring_structural_literals(content, file_path))
274
284
  all_issues.extend(check_constants_outside_config(content, file_path))
285
+ all_issues.extend(check_config_duplicate_path_anchor(content, file_path))
275
286
  all_issues.extend(check_constants_outside_config_advisory(content, file_path))
276
287
  all_issues.extend(check_file_global_constants_use_count(content, file_path))
277
288
  all_issues.extend(
@@ -389,6 +400,11 @@ def validate_content(
389
400
  all_issues.extend(
390
401
  check_docstring_args_single_line_scope_vs_span(effective_content, file_path)
391
402
  )
403
+ all_issues.extend(
404
+ check_docstring_delegation_summary_enumeration_drift(
405
+ effective_content, file_path
406
+ )
407
+ )
392
408
  all_issues.extend(
393
409
  check_boolean_naming(
394
410
  effective_content,
@@ -477,9 +493,11 @@ def validate_content(
477
493
  )
478
494
  )
479
495
  all_issues.extend(check_loop_variable_naming(content, file_path))
496
+ all_issues.extend(check_referenced_underscore_loop_variable(content, file_path))
480
497
  all_issues.extend(check_polarity_name_contradiction(content, file_path))
481
498
  all_issues.extend(check_inline_literal_collections(content, file_path))
482
499
  all_issues.extend(check_inline_tuple_string_magic(content, file_path))
500
+ all_issues.extend(check_join_separator_string_magic(content, file_path))
483
501
  all_issues.extend(check_string_literal_magic(content, file_path))
484
502
  all_issues.extend(check_whitespace_indentation_magic(content, file_path))
485
503
  all_issues.extend(check_orphan_css_classes(effective_content, file_path))
@@ -513,6 +531,17 @@ def validate_content(
513
531
  all_issues.extend(
514
532
  check_js_returns_object_schemaless_branch(content, file_path)
515
533
  )
534
+ all_issues.extend(
535
+ check_js_sibling_return_object_key_drift(content, file_path)
536
+ )
537
+ all_issues.extend(
538
+ check_js_bare_flag_return_directive(
539
+ effective_content,
540
+ file_path,
541
+ all_changed_lines,
542
+ defer_scope_to_caller,
543
+ )
544
+ )
516
545
 
517
546
  if extension in ALL_CODE_EXTENSIONS:
518
547
  advise_file_line_count(content, file_path)