claude-dev-env 1.78.0 → 1.80.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 (63) hide show
  1. package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  2. package/_shared/pr-loop/scripts/copilot_quota.py +360 -0
  3. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -0
  5. package/_shared/pr-loop/scripts/tests/CLAUDE.md +7 -0
  6. package/_shared/pr-loop/scripts/tests/fixtures/copilot_internal_user_jonecho.json +76 -0
  7. package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +242 -0
  8. package/_shared/pr-loop/scripts/tests/test_copilot_quota_constants.py +63 -0
  9. package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -0
  10. package/bin/install.mjs +1 -0
  11. package/bin/install.test.mjs +3 -2
  12. package/hooks/blocking/CLAUDE.md +3 -2
  13. package/hooks/blocking/code_rules_docstrings.py +609 -16
  14. package/hooks/blocking/code_rules_enforcer.py +41 -0
  15. package/hooks/blocking/code_rules_imports_logging.py +136 -1
  16. package/hooks/blocking/code_rules_naming_collection.py +76 -1
  17. package/hooks/blocking/code_rules_paired_test.py +240 -22
  18. package/hooks/blocking/code_rules_test_assertions.py +159 -1
  19. package/hooks/blocking/code_rules_unused_imports.py +7 -63
  20. package/hooks/blocking/env_var_table_code_drift_blocker.py +475 -0
  21. package/hooks/blocking/package_inventory_stale_blocker.py +54 -15
  22. package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +129 -0
  23. package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +198 -0
  24. package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +115 -0
  25. package/hooks/blocking/test_code_rules_enforcer_docstring_unreferenced_param.py +160 -0
  26. package/hooks/blocking/test_code_rules_enforcer_js_returns_object.py +72 -0
  27. package/hooks/blocking/test_code_rules_enforcer_module_docstring_data_schema_scope.py +82 -0
  28. package/hooks/blocking/test_code_rules_enforcer_paired_test.py +190 -0
  29. package/hooks/blocking/test_code_rules_enforcer_polarity_name_contradiction.py +76 -0
  30. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +96 -15
  31. package/hooks/blocking/test_code_rules_enforcer_vacuous_cleanup_assertion.py +132 -0
  32. package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +167 -0
  33. package/hooks/blocking/test_env_var_table_code_drift_blocker.py +94 -0
  34. package/hooks/blocking/test_package_inventory_stale_blocker.py +46 -0
  35. package/hooks/blocking/test_pre_tool_use_dispatcher.py +7 -7
  36. package/hooks/hooks_constants/CLAUDE.md +1 -0
  37. package/hooks/hooks_constants/blocking_check_limits.py +79 -0
  38. package/hooks/hooks_constants/code_rules_enforcer_constants.py +28 -0
  39. package/hooks/hooks_constants/env_var_table_code_drift_constants.py +64 -0
  40. package/hooks/hooks_constants/package_inventory_stale_blocker_constants.py +20 -9
  41. package/hooks/hooks_constants/paired_test_coverage_constants.py +11 -3
  42. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  43. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +93 -0
  44. package/hooks/hooks_constants/unused_module_import_constants.py +0 -1
  45. package/package.json +1 -1
  46. package/rules/CLAUDE.md +1 -0
  47. package/rules/docstring-prose-matches-implementation.md +57 -54
  48. package/rules/env-var-table-code-drift.md +24 -0
  49. package/rules/package-inventory-stale-entry.md +4 -4
  50. package/rules/paired-test-coverage.md +12 -5
  51. package/skills/autoconverge/SKILL.md +26 -5
  52. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
  53. package/skills/autoconverge/workflow/converge.contract.test.mjs +56 -120
  54. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +45 -5
  55. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +50 -46
  56. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +6 -6
  57. package/skills/autoconverge/workflow/converge.mjs +110 -228
  58. package/skills/autoconverge/workflow/converge.run-input.test.mjs +11 -0
  59. package/skills/autoconverge/workflow/converge_multi.mjs +23 -7
  60. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +28 -2
  61. package/skills/pr-converge/SKILL.md +28 -2
  62. package/skills/pr-converge/reference/convergence-gates.md +13 -1
  63. package/skills/pr-converge/reference/state-schema.md +11 -0
@@ -72,11 +72,15 @@ from code_rules_docstrings import ( # noqa: E402
72
72
  check_docstring_args_match_signature,
73
73
  check_docstring_args_single_line_scope_vs_span,
74
74
  check_docstring_cardinal_count_matches_constant_family,
75
+ check_docstring_documents_unreferenced_parameter,
75
76
  check_docstring_fallback_branch_coverage,
77
+ check_docstring_field_runmode_outcome,
76
78
  check_docstring_format,
79
+ check_docstring_length_constant_superlative_vs_exact_gate,
77
80
  check_docstring_names_undefined_constant,
78
81
  check_docstring_no_consumer_claim,
79
82
  check_docstring_no_inline_literal_claim,
83
+ check_docstring_no_network_claim_with_metadata_access,
80
84
  check_docstring_punctuation_mark_enumeration_coverage,
81
85
  check_docstring_raises_unraisable_largezipfile,
82
86
  check_docstring_returns_plural_cardinality,
@@ -85,6 +89,7 @@ from code_rules_docstrings import ( # noqa: E402
85
89
  check_docstring_tuple_enumeration_match,
86
90
  check_docstring_unguarded_malformed_payload_claim,
87
91
  check_module_docstring_names_public_checks,
92
+ check_module_docstring_scope_omits_data_schema_constants,
88
93
  )
89
94
  from code_rules_duplicate_body import ( # noqa: E402
90
95
  advise_cross_skill_duplicate_helper,
@@ -97,6 +102,7 @@ from code_rules_imports_logging import ( # noqa: E402
97
102
  check_import_block_sorted,
98
103
  check_imports_at_top,
99
104
  check_js_resume_task_enumeration_coverage,
105
+ check_js_returns_object_schemaless_branch,
100
106
  check_library_print,
101
107
  check_logging_fstrings,
102
108
  check_logging_printf_tokens,
@@ -112,6 +118,7 @@ from code_rules_mock_completeness import ( # noqa: E402
112
118
  from code_rules_naming_collection import ( # noqa: E402
113
119
  check_collection_prefix,
114
120
  check_loop_variable_naming,
121
+ check_polarity_name_contradiction,
115
122
  check_stuttering_collection_prefix,
116
123
  )
117
124
  from code_rules_optional_params import ( # noqa: E402
@@ -123,6 +130,7 @@ from code_rules_orphan_css_class import ( # noqa: E402
123
130
  )
124
131
  from code_rules_paired_test import ( # noqa: E402
125
132
  check_public_function_missing_paired_test,
133
+ check_test_file_omits_module_public_function,
126
134
  )
127
135
  from code_rules_paths_syspath import ( # noqa: E402
128
136
  check_hardcoded_user_paths,
@@ -147,6 +155,7 @@ from code_rules_test_assertions import ( # noqa: E402
147
155
  check_flag_gated_scenario_test_naming,
148
156
  check_skip_decorators_in_tests,
149
157
  check_stale_test_name_target,
158
+ check_vacuous_cleanup_assertion_tests,
150
159
  )
151
160
  from code_rules_test_branching_except import ( # noqa: E402
152
161
  check_bare_except,
@@ -294,8 +303,16 @@ def validate_content(
294
303
  all_issues.extend(check_boundary_types(effective_content, file_path))
295
304
  all_issues.extend(check_docstring_format(effective_content, file_path))
296
305
  all_issues.extend(check_docstring_args_match_signature(effective_content, file_path))
306
+ all_issues.extend(
307
+ check_docstring_documents_unreferenced_parameter(effective_content, file_path)
308
+ )
297
309
  all_issues.extend(check_docstring_fallback_branch_coverage(effective_content, file_path))
298
310
  all_issues.extend(check_docstring_no_consumer_claim(effective_content, file_path))
311
+ all_issues.extend(
312
+ check_docstring_no_network_claim_with_metadata_access(
313
+ effective_content, file_path
314
+ )
315
+ )
299
316
  all_issues.extend(
300
317
  check_docstring_unguarded_malformed_payload_claim(
301
318
  effective_content, file_path
@@ -311,6 +328,11 @@ def validate_content(
311
328
  all_issues.extend(
312
329
  check_module_docstring_names_public_checks(effective_content, file_path)
313
330
  )
331
+ all_issues.extend(
332
+ check_module_docstring_scope_omits_data_schema_constants(
333
+ effective_content, file_path
334
+ )
335
+ )
314
336
  all_issues.extend(
315
337
  check_docstring_tuple_enumeration_match(effective_content, file_path)
316
338
  )
@@ -327,6 +349,14 @@ def validate_content(
327
349
  all_issues.extend(
328
350
  check_docstring_returns_plural_cardinality(effective_content, file_path)
329
351
  )
352
+ all_issues.extend(
353
+ check_docstring_length_constant_superlative_vs_exact_gate(
354
+ effective_content, file_path
355
+ )
356
+ )
357
+ all_issues.extend(
358
+ check_docstring_field_runmode_outcome(effective_content, file_path)
359
+ )
330
360
  all_issues.extend(
331
361
  check_docstring_raises_unraisable_largezipfile(effective_content, file_path)
332
362
  )
@@ -368,6 +398,7 @@ def validate_content(
368
398
  )
369
399
  all_issues.extend(check_existence_check_tests(content, file_path))
370
400
  all_issues.extend(check_constant_equality_tests(content, file_path))
401
+ all_issues.extend(check_vacuous_cleanup_assertion_tests(content, file_path))
371
402
  all_issues.extend(check_stale_test_name_target(content, file_path))
372
403
  check_flag_gated_scenario_test_naming(content, file_path)
373
404
  all_issues.extend(check_unused_optional_parameters(content, file_path))
@@ -414,7 +445,14 @@ def validate_content(
414
445
  defer_scope_to_caller,
415
446
  )
416
447
  )
448
+ all_issues.extend(
449
+ check_test_file_omits_module_public_function(
450
+ effective_content,
451
+ file_path,
452
+ )
453
+ )
417
454
  all_issues.extend(check_loop_variable_naming(content, file_path))
455
+ all_issues.extend(check_polarity_name_contradiction(content, file_path))
418
456
  all_issues.extend(check_inline_literal_collections(content, file_path))
419
457
  all_issues.extend(check_inline_tuple_string_magic(content, file_path))
420
458
  all_issues.extend(check_string_literal_magic(content, file_path))
@@ -431,6 +469,9 @@ def validate_content(
431
469
  all_issues.extend(
432
470
  check_js_resume_task_enumeration_coverage(content, file_path)
433
471
  )
472
+ all_issues.extend(
473
+ check_js_returns_object_schemaless_branch(content, file_path)
474
+ )
434
475
 
435
476
  if extension in ALL_CODE_EXTENSIONS:
436
477
  advise_file_line_count(content, file_path)
@@ -1,4 +1,4 @@
1
- """Imports-at-top, import-block-sorted, logging f-string, win32gui None, E2E spec naming, JS resume-task enumeration coverage, file-length advisory, and library-print checks."""
1
+ """Imports-at-top, import-block-sorted, logging f-string, win32gui None, E2E spec naming, JS resume-task enumeration coverage, JS returns-object schema-less branch, file-length advisory, and library-print checks."""
2
2
 
3
3
  import ast
4
4
  import json
@@ -34,6 +34,7 @@ from hooks_constants.blocking_check_limits import ( # noqa: E402
34
34
  MAX_E2E_TEST_NAMING_ISSUES,
35
35
  MAX_IMPORT_BLOCK_SORT_ISSUES,
36
36
  MAX_JS_RESUME_TASK_ENUMERATION_ISSUES,
37
+ MAX_JS_RETURNS_OBJECT_SCHEMALESS_ISSUES,
37
38
  MAX_LOGGING_FSTRING_ISSUES,
38
39
  MAX_LOGGING_PRINTF_TOKEN_ISSUES,
39
40
  MAX_WINDOWS_API_NONE_ISSUES,
@@ -55,17 +56,21 @@ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
55
56
  ENUMERATION_LEADING_CONJUNCTION_PATTERN,
56
57
  ENUMERATION_LIST_ITEM_SEPARATOR_PATTERN,
57
58
  ENUMERATION_TASK_ITEM_PATTERN,
59
+ FUNCTION_WITH_JSDOC_PATTERN,
58
60
  HYPHENATED_TASK_ITEM_PATTERN,
59
61
  JAVASCRIPT_BLOCK_COMMENT_CLOSER,
60
62
  JAVASCRIPT_BLOCK_COMMENT_OPENER,
61
63
  JAVASCRIPT_LINE_COMMENT_OPENER,
62
64
  JAVASCRIPT_REGEX_DELIMITER,
63
65
  JAVASCRIPT_STRING_ESCAPE_CHARACTER,
66
+ JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN,
64
67
  LOGGING_FSTRING_PATTERN,
65
68
  LOGGING_PRINTF_TOKEN_PATTERN,
66
69
  MINIMUM_FORMAT_LOGGER_ARGUMENT_COUNT,
67
70
  NOT_INSIDE_TYPE_CHECKING_BLOCK,
68
71
  RESUME_TASK_ENUMERATION_PATTERN,
72
+ RETURN_CALL_OPENING_PARENTHESIS_PATTERN,
73
+ SCHEMA_OPTIONS_PROPERTY_KEY_PATTERN,
69
74
  SPAWN_AGENT_WITH_JSDOC_PATTERN,
70
75
  TASK_DISPATCH_NAME_PATTERN,
71
76
  TRIPLE_DOUBLE_QUOTE_DELIMITER,
@@ -919,6 +924,136 @@ def check_js_resume_task_enumeration_coverage(
919
924
  return issues[:MAX_JS_RESUME_TASK_ENUMERATION_ISSUES]
920
925
 
921
926
 
927
+ def _parameter_list_end_index(blanked_content: str, after_open_parenthesis_index: int) -> int:
928
+ """Return the index just past the ``)`` that closes an opened parameter list.
929
+
930
+ ``after_open_parenthesis_index`` sits just past the opening ``(``, so the scan
931
+ starts one level deep and walks paren-balanced. A nested ``(`` inside the list
932
+ — a default-value call, a destructure default — does not close the list early.
933
+ """
934
+ depth = 1
935
+ for each_position in range(after_open_parenthesis_index, len(blanked_content)):
936
+ character = blanked_content[each_position]
937
+ if character == "(":
938
+ depth += 1
939
+ elif character == ")":
940
+ depth -= 1
941
+ if depth == 0:
942
+ return each_position + 1
943
+ return len(blanked_content)
944
+
945
+
946
+ def _javascript_function_body(content: str, header_end_index: int) -> str | None:
947
+ """Return the brace-balanced body of the function whose header ends at the index.
948
+
949
+ ``header_end_index`` sits just past the opening ``(`` of the parameter list, so
950
+ the scan advances past the paren-balanced parameter list before finding the body
951
+ brace. A destructured parameter (``function f({ ... }) { ... }``) opens a brace
952
+ inside that list; the body brace is the first ``{`` after the list closes, not
953
+ the destructure brace.
954
+ """
955
+ body_search_index = _parameter_list_end_index(
956
+ _blank_non_code_regions(content), header_end_index
957
+ )
958
+ bounded_content = content[: _next_top_level_function_index(content, body_search_index)]
959
+ return _balanced_brace_body(bounded_content, body_search_index)
960
+
961
+
962
+ def _balanced_parenthesis_span(blanked_content: str, opening_index: int) -> str:
963
+ """Return the paren-balanced slice of blanked source starting at an opening ``(``."""
964
+ depth = 0
965
+ for each_position in range(opening_index, len(blanked_content)):
966
+ character = blanked_content[each_position]
967
+ if character == "(":
968
+ depth += 1
969
+ elif character == ")":
970
+ depth -= 1
971
+ if depth == 0:
972
+ return blanked_content[opening_index : each_position + 1]
973
+ return blanked_content[opening_index:]
974
+
975
+
976
+ def _mixed_schema_return_callees(function_body: str) -> set[str]:
977
+ """Return callees a function returns both with and without a schema options object.
978
+
979
+ The body is blanked so a brace or keyword inside a prompt string never counts.
980
+ Each ``return <callee>(...)`` whose argument list carries an object literal is
981
+ grouped by whether that argument list also carries a ``schema`` key. A callee
982
+ that appears on both sides marks a function that returns a schema object in one
983
+ branch and a schema-less transcript in another.
984
+ """
985
+ blanked_body = _blank_non_code_regions(function_body)
986
+ schema_bearing_callees: set[str] = set()
987
+ schema_less_callees: set[str] = set()
988
+ for each_return_match in RETURN_CALL_OPENING_PARENTHESIS_PATTERN.finditer(blanked_body):
989
+ argument_span = _balanced_parenthesis_span(blanked_body, each_return_match.end() - 1)
990
+ if "{" not in argument_span:
991
+ continue
992
+ callee_name = each_return_match.group("callee")
993
+ if SCHEMA_OPTIONS_PROPERTY_KEY_PATTERN.search(argument_span) is not None:
994
+ schema_bearing_callees.add(callee_name)
995
+ else:
996
+ schema_less_callees.add(callee_name)
997
+ return schema_bearing_callees & schema_less_callees
998
+
999
+
1000
+ def check_js_returns_object_schemaless_branch(content: str, file_path: str) -> list[str]:
1001
+ """Flag a JSDoc @returns Promise<object> whose branch returns a transcript string.
1002
+
1003
+ Picture a dispatcher that spawns an agent and returns whatever the agent
1004
+ resolves to. Most branches pass a ``schema`` in the agent options, so the agent
1005
+ resolves to a structured object — and the JSDoc reads
1006
+ ``@returns {Promise<object>}``. But one branch spawns the same agent with no
1007
+ schema, so that branch resolves to a plain transcript string. A caller who
1008
+ trusts the object contract and reads a field off the result gets undefined on
1009
+ that branch.
1010
+
1011
+ The check reads each ``function`` declaration that carries a JSDoc block. When
1012
+ the JSDoc promises a ``Promise<object>`` and the body returns one agent helper
1013
+ both with a ``schema`` options object and without one, the schema-less branch
1014
+ contradicts the object claim and the function is flagged. A function whose every
1015
+ return passes a schema, one that returns plain object literals, and one whose
1016
+ ``@returns`` already admits a string are all left alone. This is the JS/.mjs
1017
+ slice of Category O6 docstring-prose-vs-implementation drift; the Python
1018
+ enforcer's AST docstring checks never inspect JavaScript source.
1019
+
1020
+ Args:
1021
+ content: The source text to inspect.
1022
+ file_path: The path the source will be written to, used for exemptions.
1023
+
1024
+ Returns:
1025
+ One issue per function whose object return-type claim a schema-less branch
1026
+ contradicts, capped at the module limit.
1027
+ """
1028
+ if is_test_file(file_path) or is_hook_infrastructure(file_path):
1029
+ return []
1030
+ if get_file_extension(file_path) not in ALL_JAVASCRIPT_EXTENSIONS:
1031
+ return []
1032
+ issues: list[str] = []
1033
+ for each_match in FUNCTION_WITH_JSDOC_PATTERN.finditer(content):
1034
+ if JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN.search(each_match.group("jsdoc")) is None:
1035
+ continue
1036
+ function_body = _javascript_function_body(content, each_match.end())
1037
+ if function_body is None:
1038
+ continue
1039
+ mixed_callees = _mixed_schema_return_callees(function_body)
1040
+ if not mixed_callees:
1041
+ continue
1042
+ function_name = each_match.group("name")
1043
+ line_number = content.count("\n", 0, each_match.start("name")) + 1
1044
+ joined_callees = ", ".join(sorted(mixed_callees))
1045
+ issues.append(
1046
+ f"Line {line_number}: {function_name}() JSDoc @returns a Promise<object> but a "
1047
+ f"branch returns {joined_callees}(...) with a schema-less options object — that "
1048
+ "branch resolves to a transcript string, not the structured object the @returns "
1049
+ "claims; widen the @returns type to admit the string transcript, or pass a schema "
1050
+ "(Category O6 docstring-vs-implementation drift)"
1051
+ )
1052
+ if len(issues) >= MAX_JS_RETURNS_OBJECT_SCHEMALESS_ISSUES:
1053
+ break
1054
+ return issues[:MAX_JS_RETURNS_OBJECT_SCHEMALESS_ISSUES]
1055
+
1056
+
922
1057
  def _is_cli_entry_point(file_path: str) -> bool:
923
1058
  path_lower = file_path.lower().replace("\\", "/")
924
1059
  return any(marker.replace("\\", "/") in path_lower for marker in ALL_CLI_FILE_PATH_MARKERS)
@@ -1,6 +1,7 @@
1
- """Collection-prefix, stuttering-prefix, and loop-variable naming checks."""
1
+ """Collection-prefix, stuttering-prefix, loop-variable, and polarity-name-contradiction naming checks."""
2
2
 
3
3
  import ast
4
+ import re
4
5
  import sys
5
6
  from pathlib import Path
6
7
 
@@ -11,6 +12,7 @@ if _blocking_directory not in sys.path:
11
12
  if _hooks_directory not in sys.path:
12
13
  sys.path.insert(0, _hooks_directory)
13
14
 
15
+ from code_rules_boolean_mustcheck import _called_terminal_name # noqa: E402
14
16
  from code_rules_shared import ( # noqa: E402
15
17
  _collect_annotated_arguments,
16
18
  _collect_target_names,
@@ -22,11 +24,13 @@ from code_rules_shared import ( # noqa: E402
22
24
  from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
23
25
  ALL_COLLECTION_TYPE_NAMES,
24
26
  ALL_LOOP_INDEX_LETTER_EXEMPTIONS,
27
+ ALL_POLARITY_ANTONYM_TOKEN_PAIRS,
25
28
  ALL_SUBSCRIPT_ONLY_COLLECTION_TYPE_NAMES,
26
29
  ALL_UNION_TYPING_NAMES,
27
30
  BARE_EACH_TOKEN,
28
31
  COLLECTION_BY_NAME_PATTERN,
29
32
  EACH_PREFIX,
33
+ POLARITY_TOKEN_BOUNDARY_PATTERN,
30
34
  UPPER_SNAKE_CONSTANT_PATTERN,
31
35
  )
32
36
  from hooks_constants.stuttering_check_config import ( # noqa: E402
@@ -262,3 +266,74 @@ def check_loop_variable_naming(content: str, file_path: str) -> list[str]:
262
266
  f"Line {each_name_node.lineno}: loop variable {target_name!r} - prefix with each_ (CODE_RULES §5)"
263
267
  )
264
268
  return issues
269
+
270
+
271
+ def _name_carries_token(name: str, token: str) -> bool:
272
+ """True when name carries token as a whole underscore-delimited word."""
273
+ return re.search(POLARITY_TOKEN_BOUNDARY_PATTERN % re.escape(token), name) is not None
274
+
275
+
276
+ def _contradicting_polarity_pair(target_name: str, called_name: str) -> tuple[str, str] | None:
277
+ """The (target_token, called_token) antonym pair when target and callee assert opposite polarity."""
278
+ target_lower = target_name.lower()
279
+ called_lower = called_name.lower()
280
+ for each_positive_token, each_negative_token in ALL_POLARITY_ANTONYM_TOKEN_PAIRS:
281
+ if _name_carries_token(target_lower, each_positive_token) and _name_carries_token(
282
+ called_lower, each_negative_token
283
+ ):
284
+ return each_positive_token, each_negative_token
285
+ if _name_carries_token(target_lower, each_negative_token) and _name_carries_token(
286
+ called_lower, each_positive_token
287
+ ):
288
+ return each_negative_token, each_positive_token
289
+ return None
290
+
291
+
292
+ def check_polarity_name_contradiction(content: str, file_path: str) -> list[str]:
293
+ """Flag a boolean assignment whose target and callee assert opposite polarity.
294
+
295
+ Catches the self-contradicting statement ``is_inside_allowed =
296
+ _point_hits_any_forbidden(...)`` — a target name carrying one polarity token
297
+ (``allowed``) assigned directly from a single call whose callee name carries
298
+ the antonym token (``forbidden``). One side lies about the behavior, so the
299
+ reader cannot trust either name. Rename the callee to a neutral form that
300
+ reads truthfully at every call site.
301
+ """
302
+ if is_test_file(file_path):
303
+ return []
304
+ if is_workflow_registry_file(file_path) or is_migration_file(file_path):
305
+ return []
306
+ try:
307
+ tree = ast.parse(content)
308
+ except SyntaxError:
309
+ return []
310
+ issues: list[str] = []
311
+ for each_node in ast.walk(tree):
312
+ target_name: str | None = None
313
+ value_node: ast.expr | None = None
314
+ target_line = 0
315
+ if isinstance(each_node, ast.Assign) and len(each_node.targets) == 1 and isinstance(
316
+ each_node.targets[0], ast.Name
317
+ ):
318
+ target_name = each_node.targets[0].id
319
+ value_node = each_node.value
320
+ target_line = each_node.lineno
321
+ elif isinstance(each_node, ast.AnnAssign) and isinstance(each_node.target, ast.Name):
322
+ target_name = each_node.target.id
323
+ value_node = each_node.value
324
+ target_line = each_node.lineno
325
+ if target_name is None or not isinstance(value_node, ast.Call):
326
+ continue
327
+ called_name = _called_terminal_name(value_node)
328
+ if called_name is None:
329
+ continue
330
+ contradicting_pair = _contradicting_polarity_pair(target_name, called_name)
331
+ if contradicting_pair is None:
332
+ continue
333
+ target_token, called_token = contradicting_pair
334
+ issues.append(
335
+ f"Line {target_line}: {target_name!r} (says {target_token!r}) is assigned from"
336
+ f" {called_name!r} (says {called_token!r}) - the name contradicts the value; rename the"
337
+ f" callee to a neutral form so each call site reads truthfully (CODE_RULES §5)"
338
+ )
339
+ return issues