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.
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/copilot_quota.py +360 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -0
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +7 -0
- package/_shared/pr-loop/scripts/tests/fixtures/copilot_internal_user_jonecho.json +76 -0
- package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +242 -0
- package/_shared/pr-loop/scripts/tests/test_copilot_quota_constants.py +63 -0
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -0
- package/bin/install.mjs +1 -0
- package/bin/install.test.mjs +3 -2
- package/hooks/blocking/CLAUDE.md +3 -2
- package/hooks/blocking/code_rules_docstrings.py +609 -16
- package/hooks/blocking/code_rules_enforcer.py +41 -0
- package/hooks/blocking/code_rules_imports_logging.py +136 -1
- package/hooks/blocking/code_rules_naming_collection.py +76 -1
- package/hooks/blocking/code_rules_paired_test.py +240 -22
- package/hooks/blocking/code_rules_test_assertions.py +159 -1
- package/hooks/blocking/code_rules_unused_imports.py +7 -63
- package/hooks/blocking/env_var_table_code_drift_blocker.py +475 -0
- package/hooks/blocking/package_inventory_stale_blocker.py +54 -15
- package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +129 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +198 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +115 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_unreferenced_param.py +160 -0
- package/hooks/blocking/test_code_rules_enforcer_js_returns_object.py +72 -0
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_data_schema_scope.py +82 -0
- package/hooks/blocking/test_code_rules_enforcer_paired_test.py +190 -0
- package/hooks/blocking/test_code_rules_enforcer_polarity_name_contradiction.py +76 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +96 -15
- package/hooks/blocking/test_code_rules_enforcer_vacuous_cleanup_assertion.py +132 -0
- package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +167 -0
- package/hooks/blocking/test_env_var_table_code_drift_blocker.py +94 -0
- package/hooks/blocking/test_package_inventory_stale_blocker.py +46 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +7 -7
- package/hooks/hooks_constants/CLAUDE.md +1 -0
- package/hooks/hooks_constants/blocking_check_limits.py +79 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +28 -0
- package/hooks/hooks_constants/env_var_table_code_drift_constants.py +64 -0
- package/hooks/hooks_constants/package_inventory_stale_blocker_constants.py +20 -9
- package/hooks/hooks_constants/paired_test_coverage_constants.py +11 -3
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +93 -0
- package/hooks/hooks_constants/unused_module_import_constants.py +0 -1
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +57 -54
- package/rules/env-var-table-code-drift.md +24 -0
- package/rules/package-inventory-stale-entry.md +4 -4
- package/rules/paired-test-coverage.md +12 -5
- package/skills/autoconverge/SKILL.md +26 -5
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
- package/skills/autoconverge/workflow/converge.contract.test.mjs +56 -120
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +45 -5
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +50 -46
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +6 -6
- package/skills/autoconverge/workflow/converge.mjs +110 -228
- package/skills/autoconverge/workflow/converge.run-input.test.mjs +11 -0
- package/skills/autoconverge/workflow/converge_multi.mjs +23 -7
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +28 -2
- package/skills/pr-converge/SKILL.md +28 -2
- package/skills/pr-converge/reference/convergence-gates.md +13 -1
- 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,
|
|
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
|