claude-dev-env 1.82.0 → 1.84.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 (217) hide show
  1. package/CLAUDE.md +16 -13
  2. package/_shared/pr-loop/audit-contract.md +24 -12
  3. package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  4. package/_shared/pr-loop/scripts/README.md +1 -0
  5. package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
  6. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  8. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviewer_availability_constants.py +12 -0
  9. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +0 -2
  10. package/_shared/pr-loop/scripts/reviewer_availability.py +182 -0
  11. package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
  12. package/_shared/pr-loop/scripts/terminology_sweep.py +9 -33
  13. package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
  14. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
  15. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
  16. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
  17. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
  18. package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +159 -0
  19. package/_shared/pr-loop/scripts/tests/test_reviewer_availability_constants.py +36 -0
  20. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
  21. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +14 -4
  22. package/agents/clean-coder.md +3 -0
  23. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  24. package/bin/install.mjs +16 -8
  25. package/bin/install.test.mjs +17 -6
  26. package/commands/CLAUDE.md +0 -1
  27. package/docs/CODE_RULES.md +1 -1
  28. package/hooks/blocking/CLAUDE.md +2 -4
  29. package/hooks/blocking/code_rules_constants_config.py +164 -1
  30. package/hooks/blocking/code_rules_docstrings.py +428 -15
  31. package/hooks/blocking/code_rules_enforcer.py +33 -0
  32. package/hooks/blocking/code_rules_imports_logging.py +867 -1
  33. package/hooks/blocking/code_rules_magic_values.py +5 -0
  34. package/hooks/blocking/code_rules_naming_collection.py +152 -6
  35. package/hooks/blocking/code_rules_shared.py +34 -0
  36. package/hooks/blocking/code_rules_string_magic.py +68 -0
  37. package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
  38. package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
  39. package/hooks/blocking/reviewer_spawn_gate.py +182 -0
  40. package/hooks/blocking/stale_comment_reference_blocker.py +267 -0
  41. package/hooks/blocking/state_description_blocker.py +96 -5
  42. package/hooks/blocking/test_code_rules_config_duplicate_path_anchor.py +132 -0
  43. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +2 -0
  44. package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +385 -0
  45. package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
  46. package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
  47. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +67 -0
  48. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +40 -0
  49. package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +213 -0
  50. package/hooks/blocking/test_code_rules_enforcer_referenced_underscore_loop.py +169 -0
  51. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
  52. package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
  53. package/hooks/blocking/test_code_rules_js_bare_flag_return_directive.py +266 -0
  54. package/hooks/blocking/test_code_rules_js_sibling_return_object_key_drift.py +490 -0
  55. package/hooks/blocking/test_code_rules_logging_adjacent_literals.py +171 -0
  56. package/hooks/blocking/test_code_rules_magic_values.py +54 -0
  57. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
  58. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
  59. package/hooks/blocking/test_pre_tool_use_dispatcher.py +9 -3
  60. package/hooks/blocking/test_reviewer_spawn_gate.py +230 -0
  61. package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
  62. package/hooks/blocking/test_stale_comment_reference_blocker.py +236 -0
  63. package/hooks/blocking/test_state_description_blocker.py +135 -0
  64. package/hooks/hooks.json +5 -0
  65. package/hooks/hooks_constants/CLAUDE.md +3 -4
  66. package/hooks/hooks_constants/blocking_check_limits.py +46 -0
  67. package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -0
  68. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  69. package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
  70. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
  71. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  72. package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +41 -0
  73. package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +76 -0
  74. package/hooks/hooks_constants/state_description_blocker_constants.py +8 -0
  75. package/hooks/validation/post_tool_use_dispatcher.py +1 -1
  76. package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
  77. package/hooks/workflow/CLAUDE.md +2 -8
  78. package/package.json +1 -1
  79. package/rules/CLAUDE.md +5 -2
  80. package/rules/claude-md-orphan-file.md +5 -0
  81. package/rules/docstring-prose-matches-implementation.md +10 -1
  82. package/rules/env-var-table-code-drift.md +5 -0
  83. package/rules/es-exe-file-search.md +17 -0
  84. package/rules/no-historical-clutter.md +12 -1
  85. package/rules/orphan-css-class.md +5 -0
  86. package/rules/package-inventory-stale-entry.md +10 -0
  87. package/rules/paired-test-coverage.md +5 -0
  88. package/rules/plain-illustrative-docstrings.md +40 -1
  89. package/rules/verify-before-asking.md +7 -0
  90. package/rules/verify-runtime-state.md +40 -0
  91. package/rules/windows-filesystem-safe.md +8 -0
  92. package/rules/workers-done-before-complete.md +33 -0
  93. package/rules/workflow-substitution-slots.md +5 -0
  94. package/skills/CLAUDE.md +7 -2
  95. package/skills/autoconverge/SKILL.md +48 -61
  96. package/skills/autoconverge/reference/closing-report.md +6 -6
  97. package/skills/autoconverge/reference/convergence.md +17 -15
  98. package/skills/autoconverge/reference/gotchas.md +6 -3
  99. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
  100. package/skills/autoconverge/workflow/converge.contract.test.mjs +153 -10
  101. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +55 -19
  102. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
  103. package/skills/autoconverge/workflow/converge.mjs +167 -106
  104. package/skills/autoconverge/workflow/render_report.py +7 -11
  105. package/skills/bugteam/CLAUDE.md +1 -1
  106. package/skills/bugteam/PROMPTS.md +7 -6
  107. package/skills/bugteam/SKILL.md +25 -72
  108. package/skills/bugteam/reference/CLAUDE.md +1 -3
  109. package/skills/bugteam/reference/README.md +1 -1
  110. package/skills/bugteam/reference/audit-and-teammates.md +1 -1
  111. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
  112. package/skills/bugteam/reference/team-setup.md +8 -7
  113. package/skills/bugteam/scripts/CLAUDE.md +0 -6
  114. package/skills/bugteam/scripts/README.md +0 -4
  115. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
  116. package/skills/code/SKILL.md +2 -0
  117. package/skills/copilot-review/CLAUDE.md +1 -1
  118. package/skills/copilot-review/SKILL.md +25 -23
  119. package/skills/everything-search/SKILL.md +5 -0
  120. package/skills/findbugs/CLAUDE.md +2 -2
  121. package/skills/findbugs/SKILL.md +22 -83
  122. package/skills/fixbugs/SKILL.md +2 -4
  123. package/skills/log-audit/CLAUDE.md +20 -0
  124. package/skills/log-audit/SKILL.md +68 -0
  125. package/skills/log-audit/reference/CLAUDE.md +9 -0
  126. package/skills/log-audit/reference/charter.md +52 -0
  127. package/skills/log-audit/scripts/CLAUDE.md +27 -0
  128. package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
  129. package/skills/log-audit/scripts/collect_log_window.py +199 -0
  130. package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
  131. package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
  132. package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
  133. package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
  134. package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
  135. package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
  136. package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
  137. package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
  138. package/skills/monitor-open-prs/SKILL.md +2 -2
  139. package/skills/post-audit-findings/SKILL.md +84 -0
  140. package/skills/pr-converge/CLAUDE.md +2 -0
  141. package/skills/pr-converge/SKILL.md +72 -59
  142. package/skills/pr-converge/reference/CLAUDE.md +1 -1
  143. package/skills/pr-converge/reference/convergence-gates.md +16 -19
  144. package/skills/pr-converge/reference/examples.md +5 -5
  145. package/skills/pr-converge/reference/fix-protocol.md +16 -43
  146. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
  147. package/skills/pr-converge/reference/per-tick.md +24 -45
  148. package/skills/pr-converge/reference/state-schema.md +15 -0
  149. package/skills/pr-converge/scripts/README.md +3 -5
  150. package/skills/pr-fix-protocol/SKILL.md +70 -0
  151. package/skills/pr-loop-lifecycle/SKILL.md +73 -0
  152. package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
  153. package/skills/pr-scope-resolve/SKILL.md +48 -0
  154. package/skills/qbug/CLAUDE.md +4 -4
  155. package/skills/qbug/SKILL.md +46 -144
  156. package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
  157. package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
  158. package/skills/reviewer-gates/SKILL.md +96 -0
  159. package/skills/session-log/CLAUDE.md +7 -7
  160. package/skills/session-log/SKILL.md +27 -44
  161. package/skills/test_markdown_link_integrity.py +103 -0
  162. package/commands/doc-gist.md +0 -16
  163. package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
  164. package/hooks/blocking/conftest.py +0 -30
  165. package/hooks/blocking/md_path_exemptions.py +0 -224
  166. package/hooks/blocking/md_to_html_blocker.py +0 -155
  167. package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
  168. package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
  169. package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
  170. package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
  171. package/hooks/hooks_constants/html_companion_constants.py +0 -20
  172. package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
  173. package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
  174. package/hooks/workflow/doc_gist_auto_publish.py +0 -144
  175. package/hooks/workflow/md_to_html_companion.py +0 -358
  176. package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
  177. package/hooks/workflow/test_md_to_html_companion.py +0 -613
  178. package/skills/bugteam/reference/audit-contract.md +0 -163
  179. package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
  180. package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
  181. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
  182. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
  183. package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
  184. package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
  185. package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
  186. package/skills/doc-gist/CLAUDE.md +0 -25
  187. package/skills/doc-gist/SKILL.md +0 -97
  188. package/skills/doc-gist/references/CLAUDE.md +0 -9
  189. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
  190. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
  191. package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
  192. package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
  193. package/skills/doc-gist/references/examples/05-design-system.html +0 -629
  194. package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
  195. package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
  196. package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
  197. package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
  198. package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
  199. package/skills/doc-gist/references/examples/11-status-report.html +0 -528
  200. package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
  201. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
  202. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
  203. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
  204. package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
  205. package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
  206. package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
  207. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
  208. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
  209. package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
  210. package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
  211. package/skills/doc-gist/references/examples/README.md +0 -5
  212. package/skills/doc-gist/scripts/CLAUDE.md +0 -27
  213. package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
  214. package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
  215. package/skills/doc-gist/scripts/gist_upload.py +0 -177
  216. package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
  217. /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
@@ -0,0 +1,490 @@
1
+ """Tests for check_js_sibling_return_object_key_drift.
2
+
3
+ The check catches the JS/.mjs Category O return-shape drift PR #824 surfaced: a
4
+ workflow body whose success return carries a field (``allDeferredPrs``) while a
5
+ blocker early-return in the same scope omits it. The two return paths disagree on
6
+ shape, so an orchestrator reading the field off the blocker result gets undefined
7
+ where the documented contract and the sibling return both promise it. Early exits
8
+ that omit two or more fields — a genuinely smaller exit shape — pass.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import importlib.util
14
+ import pathlib
15
+ import sys
16
+
17
+ _HOOK_DIRECTORY = pathlib.Path(__file__).parent
18
+ if str(_HOOK_DIRECTORY) not in sys.path:
19
+ sys.path.insert(0, str(_HOOK_DIRECTORY))
20
+
21
+ _module_spec = importlib.util.spec_from_file_location(
22
+ "code_rules_imports_logging",
23
+ _HOOK_DIRECTORY / "code_rules_imports_logging.py",
24
+ )
25
+ assert _module_spec is not None
26
+ assert _module_spec.loader is not None
27
+ _imports_logging_module = importlib.util.module_from_spec(_module_spec)
28
+ _module_spec.loader.exec_module(_imports_logging_module)
29
+
30
+ check_js_sibling_return_object_key_drift = (
31
+ _imports_logging_module.check_js_sibling_return_object_key_drift
32
+ )
33
+
34
+ _MJS_PATH = "skills/autoconverge/workflow/converge_multi.mjs"
35
+
36
+ _SHIPPED_CONVERGE_MJS = (
37
+ _HOOK_DIRECTORY.parents[1] / "skills" / "autoconverge" / "workflow" / "converge.mjs"
38
+ )
39
+
40
+ _SHIPPED_INSTALL_MYPY_INI_MJS = (
41
+ _HOOK_DIRECTORY.parents[1] / "bin" / "install_mypy_ini.mjs"
42
+ )
43
+
44
+ _TYPESCRIPT_PATH = "src/example.ts"
45
+
46
+ _TYPESCRIPT_JSX_PATH = "src/example.tsx"
47
+
48
+
49
+ def _multi_workflow_source(blocker_return_body: str) -> str:
50
+ return (
51
+ "function classifyMultiInput(rawArgs) {\n"
52
+ " if (rawArgs == null) {\n"
53
+ " return { input: null, blocker: 'bad coordinates' }\n"
54
+ " }\n"
55
+ " return { input: rawArgs, blocker: null }\n"
56
+ "}\n"
57
+ "\n"
58
+ "const multiInput = classifyMultiInput(args)\n"
59
+ "if (multiInput.blocker) {\n"
60
+ f" {blocker_return_body}\n"
61
+ "}\n"
62
+ "const input = multiInput.input\n"
63
+ "\n"
64
+ "const childResults = await parallel(\n"
65
+ " input.prs.map((eachPr) => async () => {\n"
66
+ " return {\n"
67
+ " owner: eachPr.owner,\n"
68
+ " repo: eachPr.repo,\n"
69
+ " prNumber: eachPr.prNumber,\n"
70
+ " converged: true,\n"
71
+ " deferredPrs: [],\n"
72
+ " }\n"
73
+ " }),\n"
74
+ ")\n"
75
+ "\n"
76
+ "return {\n"
77
+ " converged: true,\n"
78
+ " prCount: input.prs.length,\n"
79
+ " convergedCount: input.prs.length,\n"
80
+ " results: childResults,\n"
81
+ " allDeferredPrs: [],\n"
82
+ " blocker: null,\n"
83
+ "}\n"
84
+ )
85
+
86
+
87
+ def _drifted_blocker_source() -> str:
88
+ return _multi_workflow_source(
89
+ "return { converged: false, prCount: 0, convergedCount: 0, results: [], "
90
+ "blocker: multiInput.blocker }"
91
+ )
92
+
93
+
94
+ def _consistent_blocker_source() -> str:
95
+ return _multi_workflow_source(
96
+ "return { converged: false, prCount: 0, convergedCount: 0, results: [], "
97
+ "allDeferredPrs: [], blocker: multiInput.blocker }"
98
+ )
99
+
100
+
101
+ def _three_field_gap_source() -> str:
102
+ return (
103
+ "const runInput = classifyRunInput(args)\n"
104
+ "if (runInput.blocker) {\n"
105
+ " return { converged: false, rounds: 0, finalSha: null, blocker: runInput.blocker }\n"
106
+ "}\n"
107
+ "\n"
108
+ "return {\n"
109
+ " converged: true,\n"
110
+ " rounds,\n"
111
+ " finalSha: head,\n"
112
+ " blocker: null,\n"
113
+ " standardsNote,\n"
114
+ " copilotNote,\n"
115
+ " reuseNote,\n"
116
+ "}\n"
117
+ )
118
+
119
+
120
+ def _cross_scope_subset_source() -> str:
121
+ return (
122
+ "function buildFull() {\n"
123
+ " return { alpha: 1, beta: 2, gamma: 3 }\n"
124
+ "}\n"
125
+ "\n"
126
+ "function buildPartial() {\n"
127
+ " return { alpha: 1, beta: 2 }\n"
128
+ "}\n"
129
+ )
130
+
131
+
132
+ def _discriminated_union_source() -> str:
133
+ return (
134
+ "function classifyOutcome(state) {\n"
135
+ " if (state == null) return { kind: 'retry' }\n"
136
+ " if (state.done === true) return { kind: 'ready', summary: state.summary }\n"
137
+ " return { kind: 'repair', failures: state.failures }\n"
138
+ "}\n"
139
+ )
140
+
141
+
142
+ def _action_keyed_union_source() -> str:
143
+ return (
144
+ "function configureTarget(target) {\n"
145
+ " if (alreadyConfigured(target)) {\n"
146
+ " return { action: 'already-configured', path: target }\n"
147
+ " }\n"
148
+ " if (existsSync(target)) {\n"
149
+ " return { action: 'skipped-existing', path: target, expectedLine: expectedPathLine }\n"
150
+ " }\n"
151
+ " writeFileSync(target)\n"
152
+ " return { action: 'created', path: target }\n"
153
+ "}\n"
154
+ )
155
+
156
+
157
+ def _two_typescript_functions_with_object_return_type() -> str:
158
+ return (
159
+ "function alpha(): { ok: boolean } {\n"
160
+ " return { a: 1, b: 2 }\n"
161
+ "}\n"
162
+ "function beta(): { ok: boolean } {\n"
163
+ " return { a: 1, b: 2, c: 3 }\n"
164
+ "}\n"
165
+ )
166
+
167
+
168
+ def _single_typescript_function_with_drift() -> str:
169
+ return (
170
+ "function classify(state): { converged: boolean } {\n"
171
+ " if (state == null) {\n"
172
+ " return { converged: false, rounds: 0, blocker: 'bad state' }\n"
173
+ " }\n"
174
+ " return { converged: true, rounds: 5, blocker: null, deferred: [] }\n"
175
+ "}\n"
176
+ )
177
+
178
+
179
+ def _two_typescript_functions_with_named_return_type() -> str:
180
+ return (
181
+ "function toSummary(): UserSummary {\n"
182
+ " return { count: 1, size: 2 }\n"
183
+ "}\n"
184
+ "function toDetail(): UserDetail {\n"
185
+ " return { count: 3, size: 4, extra: 5 }\n"
186
+ "}\n"
187
+ )
188
+
189
+
190
+ def _two_typescript_functions_with_primitive_return_type() -> str:
191
+ return (
192
+ "function toSummary(): void {\n"
193
+ " return { count: 1, size: 2 }\n"
194
+ "}\n"
195
+ "function toDetail(): void {\n"
196
+ " return { count: 3, size: 4, extra: 5 }\n"
197
+ "}\n"
198
+ )
199
+
200
+
201
+ def _single_typescript_function_named_return_type_with_drift() -> str:
202
+ return (
203
+ "function classify(state): ClassifyResult {\n"
204
+ " if (state == null) {\n"
205
+ " return { converged: false, rounds: 0, blocker: 'bad state' }\n"
206
+ " }\n"
207
+ " return { converged: true, rounds: 5, blocker: null, deferred: [] }\n"
208
+ "}\n"
209
+ )
210
+
211
+
212
+ def _two_typescript_functions_with_function_type_return() -> str:
213
+ return (
214
+ "function toSummary(): (x: number) => void {\n"
215
+ " return { count: 1, size: 2 }\n"
216
+ "}\n"
217
+ "function toDetail(): (x: number) => void {\n"
218
+ " return { count: 3, size: 4, extra: 5 }\n"
219
+ "}\n"
220
+ )
221
+
222
+
223
+ def _two_typescript_functions_with_mixed_union_return() -> str:
224
+ return (
225
+ "function alpha(): Promise<Foo> | Bar {\n"
226
+ " return { x: 1, y: 2 }\n"
227
+ "}\n"
228
+ "function beta(): Promise<Foo> | Bar {\n"
229
+ " return { x: 1, y: 2, z: 3 }\n"
230
+ "}\n"
231
+ )
232
+
233
+
234
+ def _single_typescript_function_function_type_return_with_drift() -> str:
235
+ return (
236
+ "function classify(state): (x: number) => void {\n"
237
+ " if (state == null) {\n"
238
+ " return { converged: false, rounds: 0, blocker: 'bad state' }\n"
239
+ " }\n"
240
+ " return { converged: true, rounds: 5, blocker: null, deferred: [] }\n"
241
+ "}\n"
242
+ )
243
+
244
+
245
+ def _two_typescript_functions_with_conditional_return_type() -> str:
246
+ return (
247
+ "function alpha(state: T): T extends U ? A : B {\n"
248
+ " return { a: 1, b: 2 }\n"
249
+ "}\n"
250
+ "function beta(state: T): T extends U ? A : B {\n"
251
+ " return { a: 1, b: 2, c: 3 }\n"
252
+ "}\n"
253
+ )
254
+
255
+
256
+ def _single_typescript_function_conditional_return_type_with_drift() -> str:
257
+ return (
258
+ "function classify(state: T): T extends U ? A : B {\n"
259
+ " if (state == null) {\n"
260
+ " return { converged: false, rounds: 0, blocker: 'bad state' }\n"
261
+ " }\n"
262
+ " return { converged: true, rounds: 5, blocker: null, deferred: [] }\n"
263
+ "}\n"
264
+ )
265
+
266
+
267
+ def _two_concise_body_arrows_in_one_scope_source() -> str:
268
+ return (
269
+ "function build() {\n"
270
+ " const toFull = () => ({ x: 1, y: 2, z: 3 })\n"
271
+ " const toPartial = () => ({ x: 1, y: 2 })\n"
272
+ " return toFull()\n"
273
+ "}\n"
274
+ )
275
+
276
+
277
+ def _for_await_drift_source() -> str:
278
+ return (
279
+ "async function drain(stream) {\n"
280
+ " for await (const eachChunk of stream) {\n"
281
+ " if (eachChunk.bad) {\n"
282
+ " return { converged: false, rounds: 0, blocker: 'bad' }\n"
283
+ " }\n"
284
+ " }\n"
285
+ " return { converged: true, rounds: 1, blocker: null, deferred: [] }\n"
286
+ "}\n"
287
+ )
288
+
289
+
290
+ def _mixed_quote_shared_discriminant_drift_source() -> str:
291
+ return (
292
+ "function classify(state) {\n"
293
+ " if (state == null) {\n"
294
+ ' return { action: "created", rounds: 0, blocker: \'bad\' }\n'
295
+ " }\n"
296
+ " return { action: 'created', rounds: 1, blocker: null, deferred: [] }\n"
297
+ "}\n"
298
+ )
299
+
300
+
301
+ def _mixed_quote_differing_discriminant_source() -> str:
302
+ return (
303
+ "function classify(state) {\n"
304
+ " if (state == null) {\n"
305
+ ' return { action: "skipped-existing", rounds: 0, blocker: \'bad\' }\n'
306
+ " }\n"
307
+ " return { action: 'created', rounds: 1, blocker: null, deferred: [] }\n"
308
+ "}\n"
309
+ )
310
+
311
+
312
+ def _trailing_comment_shared_discriminant_drift_source() -> str:
313
+ return (
314
+ "function classify(state) {\n"
315
+ " if (state == null) {\n"
316
+ " return { action: 'created' /* legacy tag */, rounds: 0, blocker: 'bad' }\n"
317
+ " }\n"
318
+ " return { action: 'created', rounds: 1, blocker: null, deferred: [] }\n"
319
+ "}\n"
320
+ )
321
+
322
+
323
+ def test_flags_blocker_return_that_omits_one_documented_field() -> None:
324
+ issues = check_js_sibling_return_object_key_drift(_drifted_blocker_source(), _MJS_PATH)
325
+ assert len(issues) == 1
326
+ assert "allDeferredPrs" in issues[0]
327
+
328
+
329
+ def test_accepts_when_blocker_return_carries_the_field() -> None:
330
+ issues = check_js_sibling_return_object_key_drift(_consistent_blocker_source(), _MJS_PATH)
331
+ assert issues == []
332
+
333
+
334
+ def test_accepts_early_exit_that_omits_more_than_one_field() -> None:
335
+ issues = check_js_sibling_return_object_key_drift(_three_field_gap_source(), _MJS_PATH)
336
+ assert issues == []
337
+
338
+
339
+ def test_scopes_subset_comparison_to_a_single_function() -> None:
340
+ issues = check_js_sibling_return_object_key_drift(_cross_scope_subset_source(), _MJS_PATH)
341
+ assert issues == []
342
+
343
+
344
+ def test_accepts_discriminated_union_returns() -> None:
345
+ issues = check_js_sibling_return_object_key_drift(_discriminated_union_source(), _MJS_PATH)
346
+ assert issues == []
347
+
348
+
349
+ def test_accepts_subset_by_one_with_string_discriminant() -> None:
350
+ issues = check_js_sibling_return_object_key_drift(_action_keyed_union_source(), _MJS_PATH)
351
+ assert issues == []
352
+
353
+
354
+ def test_accepts_candidate_whose_supersets_disagree_on_the_missing_key() -> None:
355
+ source = (
356
+ "function classifyRun(state) {\n"
357
+ " if (state == null) {\n"
358
+ " return { converged: false, rounds: 0, finalSha: null }\n"
359
+ " }\n"
360
+ " if (state.failed) {\n"
361
+ " return { converged: false, rounds: 0, finalSha: null, failureNote: state.note }\n"
362
+ " }\n"
363
+ " return { converged: true, rounds: state.rounds, finalSha: state.sha, "
364
+ "summary: state.summary }\n"
365
+ "}\n"
366
+ )
367
+ assert check_js_sibling_return_object_key_drift(source, _MJS_PATH) == []
368
+
369
+
370
+ def test_shipped_install_mypy_ini_mjs_passes_its_own_check() -> None:
371
+ shipped_source = _SHIPPED_INSTALL_MYPY_INI_MJS.read_text(encoding="utf-8")
372
+ issues = check_js_sibling_return_object_key_drift(
373
+ shipped_source, "bin/install_mypy_ini.mjs"
374
+ )
375
+ assert issues == []
376
+
377
+
378
+ def test_typescript_object_return_type_keeps_functions_in_separate_scopes() -> None:
379
+ source = _two_typescript_functions_with_object_return_type()
380
+ assert check_js_sibling_return_object_key_drift(source, _TYPESCRIPT_PATH) == []
381
+ assert check_js_sibling_return_object_key_drift(source, _TYPESCRIPT_JSX_PATH) == []
382
+
383
+
384
+ def test_typescript_object_return_type_still_flags_intra_function_drift() -> None:
385
+ issues = check_js_sibling_return_object_key_drift(
386
+ _single_typescript_function_with_drift(), _TYPESCRIPT_PATH
387
+ )
388
+ assert len(issues) == 1
389
+ assert "deferred" in issues[0]
390
+
391
+
392
+ def test_typescript_named_return_type_keeps_functions_in_separate_scopes() -> None:
393
+ source = _two_typescript_functions_with_named_return_type()
394
+ assert check_js_sibling_return_object_key_drift(source, _TYPESCRIPT_PATH) == []
395
+ assert check_js_sibling_return_object_key_drift(source, _TYPESCRIPT_JSX_PATH) == []
396
+
397
+
398
+ def test_typescript_primitive_return_type_keeps_functions_in_separate_scopes() -> None:
399
+ source = _two_typescript_functions_with_primitive_return_type()
400
+ assert check_js_sibling_return_object_key_drift(source, _TYPESCRIPT_PATH) == []
401
+
402
+
403
+ def test_typescript_named_return_type_still_flags_intra_function_drift() -> None:
404
+ issues = check_js_sibling_return_object_key_drift(
405
+ _single_typescript_function_named_return_type_with_drift(), _TYPESCRIPT_PATH
406
+ )
407
+ assert len(issues) == 1
408
+ assert "deferred" in issues[0]
409
+
410
+
411
+ def test_typescript_function_type_return_keeps_functions_in_separate_scopes() -> None:
412
+ source = _two_typescript_functions_with_function_type_return()
413
+ assert check_js_sibling_return_object_key_drift(source, _TYPESCRIPT_PATH) == []
414
+
415
+
416
+ def test_typescript_mixed_union_return_keeps_functions_in_separate_scopes() -> None:
417
+ source = _two_typescript_functions_with_mixed_union_return()
418
+ assert check_js_sibling_return_object_key_drift(source, _TYPESCRIPT_PATH) == []
419
+
420
+
421
+ def test_typescript_function_type_return_still_flags_intra_function_drift() -> None:
422
+ issues = check_js_sibling_return_object_key_drift(
423
+ _single_typescript_function_function_type_return_with_drift(), _TYPESCRIPT_PATH
424
+ )
425
+ assert len(issues) == 1
426
+ assert "deferred" in issues[0]
427
+
428
+
429
+ def test_typescript_conditional_return_type_keeps_functions_in_separate_scopes() -> None:
430
+ source = _two_typescript_functions_with_conditional_return_type()
431
+ assert check_js_sibling_return_object_key_drift(source, _TYPESCRIPT_PATH) == []
432
+
433
+
434
+ def test_typescript_conditional_return_type_still_flags_intra_function_drift() -> None:
435
+ issues = check_js_sibling_return_object_key_drift(
436
+ _single_typescript_function_conditional_return_type_with_drift(), _TYPESCRIPT_PATH
437
+ )
438
+ assert len(issues) == 1
439
+ assert "deferred" in issues[0]
440
+
441
+
442
+ def test_distinct_concise_body_arrows_in_one_scope_do_not_drift() -> None:
443
+ issues = check_js_sibling_return_object_key_drift(
444
+ _two_concise_body_arrows_in_one_scope_source(), _MJS_PATH
445
+ )
446
+ assert issues == []
447
+
448
+
449
+ def test_for_await_block_shares_its_functions_scope() -> None:
450
+ issues = check_js_sibling_return_object_key_drift(_for_await_drift_source(), _MJS_PATH)
451
+ assert len(issues) == 1
452
+ assert "deferred" in issues[0]
453
+
454
+
455
+ def test_mixed_quote_shared_discriminant_does_not_suppress_drift() -> None:
456
+ issues = check_js_sibling_return_object_key_drift(
457
+ _mixed_quote_shared_discriminant_drift_source(), _MJS_PATH
458
+ )
459
+ assert len(issues) == 1
460
+ assert "deferred" in issues[0]
461
+
462
+
463
+ def test_mixed_quote_differing_discriminant_still_passes_as_union() -> None:
464
+ issues = check_js_sibling_return_object_key_drift(
465
+ _mixed_quote_differing_discriminant_source(), _MJS_PATH
466
+ )
467
+ assert issues == []
468
+
469
+
470
+ def test_trailing_comment_after_shared_discriminant_does_not_suppress_drift() -> None:
471
+ issues = check_js_sibling_return_object_key_drift(
472
+ _trailing_comment_shared_discriminant_drift_source(), _MJS_PATH
473
+ )
474
+ assert len(issues) == 1
475
+ assert "deferred" in issues[0]
476
+
477
+
478
+ def test_skips_python_files() -> None:
479
+ issues = check_js_sibling_return_object_key_drift(
480
+ _drifted_blocker_source(), "workflow/converge_multi.py"
481
+ )
482
+ assert issues == []
483
+
484
+
485
+ def test_shipped_converge_mjs_passes_its_own_check() -> None:
486
+ shipped_source = _SHIPPED_CONVERGE_MJS.read_text(encoding="utf-8")
487
+ issues = check_js_sibling_return_object_key_drift(
488
+ shipped_source, str(_SHIPPED_CONVERGE_MJS)
489
+ )
490
+ assert issues == []
@@ -0,0 +1,171 @@
1
+ """Tests for check_logging_adjacent_string_literals."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ from pathlib import Path
7
+ from types import ModuleType
8
+
9
+ ENFORCER_FILENAME = "code_rules_enforcer.py"
10
+ ENFORCER_MODULE_NAME = "code_rules_enforcer_adjacent_literals_tests"
11
+ PRODUCTION_FILE_PATH = "shared_utils/web_automation/sample.py"
12
+ TEST_FILE_PATH = "shared_utils/web_automation/tests/test_sample.py"
13
+
14
+
15
+ def load_enforcer_module() -> ModuleType:
16
+ loader_path = Path(__file__).parent / ENFORCER_FILENAME
17
+ module_spec = importlib.util.spec_from_file_location(ENFORCER_MODULE_NAME, loader_path)
18
+ assert module_spec is not None
19
+ assert module_spec.loader is not None
20
+ loaded_module = importlib.util.module_from_spec(module_spec)
21
+ module_spec.loader.exec_module(loaded_module)
22
+ return loaded_module
23
+
24
+
25
+ enforcer = load_enforcer_module()
26
+
27
+
28
+ def test_should_flag_adjacent_literals_in_attribute_logger_call() -> None:
29
+ source = 'logger.info("[Batch]" " Failed %s", job_name)\n'
30
+ issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
31
+ assert len(issues) == 1
32
+ assert "adjacent string literals" in issues[0]
33
+
34
+
35
+ def test_should_flag_adjacent_literals_in_log_helper_call() -> None:
36
+ source = 'log_debug("[Batch]" " Completed {}", job_name)\n'
37
+ issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
38
+ assert len(issues) == 1
39
+
40
+
41
+ def test_should_flag_adjacent_literals_on_underscore_prefixed_logger() -> None:
42
+ source = '_logger.info("[Batch]" " Failed %s", job_name)\n'
43
+ issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
44
+ assert len(issues) == 1
45
+ assert "adjacent string literals" in issues[0]
46
+
47
+
48
+ def test_should_flag_adjacent_single_quoted_literals() -> None:
49
+ source = "logger.warning('[Batch]' ' Skipped %s', job_name)\n"
50
+ issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
51
+ assert len(issues) == 1
52
+
53
+
54
+ def test_should_flag_zero_whitespace_adjacent_literals() -> None:
55
+ source = 'log_info("[Batch]""Failed %s", job_name)\n'
56
+ issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
57
+ assert len(issues) == 1
58
+ assert "adjacent string literals" in issues[0]
59
+
60
+
61
+ def test_should_flag_adjacent_literals_with_prefixed_first_literal_bytes() -> None:
62
+ source = 'log_info(b"[Batch]" b" Failed %s", job_name)\n'
63
+ issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
64
+ assert len(issues) == 1
65
+ assert "adjacent string literals" in issues[0]
66
+
67
+
68
+ def test_should_flag_zero_whitespace_adjacent_literals_with_prefixed_first_literal() -> None:
69
+ source = 'log_info(r"[Batch]""Failed %s", job_name)\n'
70
+ issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
71
+ assert len(issues) == 1
72
+ assert "adjacent string literals" in issues[0]
73
+
74
+
75
+ def test_should_allow_single_triple_double_quoted_literal_log_call() -> None:
76
+ source = 'log_info("""message""")\n'
77
+ issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
78
+ assert issues == []
79
+
80
+
81
+ def test_should_allow_single_triple_single_quoted_literal_log_call() -> None:
82
+ source = "log_debug('''message''')\n"
83
+ issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
84
+ assert issues == []
85
+
86
+
87
+ def test_should_allow_prefixed_triple_quoted_literal_log_call() -> None:
88
+ source = 'log_info(r"""message""")\n'
89
+ issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
90
+ assert issues == []
91
+
92
+
93
+ def test_should_allow_single_literal_log_call() -> None:
94
+ source = 'logger.info("[Batch] Starting %s (row %d)", job_name, attempt_index)\n'
95
+ issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
96
+ assert issues == []
97
+
98
+
99
+ def test_should_allow_comma_separated_string_arguments() -> None:
100
+ source = 'logger.info("delivered %s %s", message_id, "queued")\n'
101
+ issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
102
+ assert issues == []
103
+
104
+
105
+ def test_should_allow_explicit_plus_concatenation() -> None:
106
+ source = 'logger.info("[Batch] " + suffix_pattern, job_name)\n'
107
+ issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
108
+ assert issues == []
109
+
110
+
111
+ def test_should_allow_adjacent_literals_outside_log_calls() -> None:
112
+ source = 'banner_text = "[Batch]" " Failed %s"\n'
113
+ issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
114
+ assert issues == []
115
+
116
+
117
+ def test_should_exempt_test_files() -> None:
118
+ source = 'logger.info("[Batch]" " Failed %s", job_name)\n'
119
+ issues = enforcer.check_logging_adjacent_string_literals(source, TEST_FILE_PATH)
120
+ assert issues == []
121
+
122
+
123
+ def test_should_skip_non_python_files() -> None:
124
+ source = 'logger.info("[Batch]" " Failed %s", job_name)\n'
125
+ issues = enforcer.check_logging_adjacent_string_literals(source, "notes.md")
126
+ assert issues == []
127
+
128
+
129
+ def test_should_allow_adjacent_literals_in_second_statement_on_same_line() -> None:
130
+ source = 'logger.info("done"); banner = "a" "b"\n'
131
+ issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
132
+ assert issues == []
133
+
134
+
135
+ def test_should_flag_adjacent_literals_in_log_call_preceded_by_other_statement() -> None:
136
+ source = 'attempt_index = 1; logger.info("[Batch]" " Failed %s", job_name)\n'
137
+ issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
138
+ assert len(issues) == 1
139
+ assert "adjacent string literals" in issues[0]
140
+
141
+
142
+ def test_should_allow_adjacent_literal_example_inside_docstring() -> None:
143
+ source = (
144
+ 'def emit_batch_result() -> None:\n'
145
+ ' """Example: logger.info("[Batch]" " Failed %s", job_name).\n'
146
+ ' """\n'
147
+ " logger.info(\"[Batch] Starting %s\", job_name)\n"
148
+ )
149
+ issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
150
+ assert issues == []
151
+
152
+
153
+ def test_should_allow_adjacent_literal_example_inside_comment() -> None:
154
+ source = (
155
+ '# example: logger.info("[Batch]" " Failed %s", job_name)\n'
156
+ 'logger.info("[Batch] Starting %s", job_name)\n'
157
+ )
158
+ issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
159
+ assert issues == []
160
+
161
+
162
+ def test_should_still_flag_real_call_alongside_docstring_example() -> None:
163
+ source = (
164
+ 'def emit_batch_result() -> None:\n'
165
+ ' """Example: logger.info("[Batch]" " Failed %s", job_name).\n'
166
+ ' """\n'
167
+ ' logger.info("[Batch]" " Failed %s", job_name)\n'
168
+ )
169
+ issues = enforcer.check_logging_adjacent_string_literals(source, PRODUCTION_FILE_PATH)
170
+ assert len(issues) == 1
171
+ assert "adjacent string literals" in issues[0]