claude-dev-env 1.38.1 → 1.40.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 (282) hide show
  1. package/CLAUDE.md +10 -36
  2. package/_shared/pr-loop/audit-reply-template.md +147 -0
  3. package/_shared/pr-loop/fix-protocol.md +25 -4
  4. package/_shared/pr-loop/gh-payloads.md +37 -50
  5. package/_shared/pr-loop/scripts/code_rules_gate.py +0 -60
  6. package/_shared/pr-loop/scripts/config/post_audit_thread_constants.py +199 -0
  7. package/_shared/pr-loop/scripts/config/reviews_disabled_constants.py +8 -0
  8. package/_shared/pr-loop/scripts/post_audit_thread.py +1242 -0
  9. package/_shared/pr-loop/scripts/preflight.py +129 -2
  10. package/_shared/pr-loop/scripts/reviews_disabled.py +59 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -19
  12. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +1116 -0
  13. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +127 -0
  14. package/_shared/pr-loop/scripts/tests/test_preflight.py +41 -0
  15. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +36 -0
  16. package/_shared/pr-loop/state-schema.md +1 -1
  17. package/agents/clean-coder.md +2 -2
  18. package/agents/pr-description-writer.md +150 -52
  19. package/bin/install.mjs +6 -7
  20. package/bin/install.test.mjs +8 -0
  21. package/commands/doc-gist.md +16 -0
  22. package/commands/plan.md +0 -2
  23. package/commands/review-plan.md +1 -1
  24. package/docs/CODE_RULES.md +122 -2
  25. package/docs/PR_DESCRIPTION_GUIDE.md +127 -64
  26. package/hooks/blocking/bot_mention_comment_blocker.py +75 -0
  27. package/hooks/blocking/code_rules_enforcer.py +1143 -129
  28. package/hooks/blocking/convergence_gate_blocker.py +130 -0
  29. package/hooks/blocking/destructive_command_blocker.py +74 -0
  30. package/hooks/blocking/gh_body_arg_blocker.py +30 -0
  31. package/hooks/blocking/md_to_html_blocker.py +119 -0
  32. package/hooks/blocking/pr_description_enforcer.py +57 -22
  33. package/hooks/blocking/test_bot_mention_comment_blocker.py +131 -0
  34. package/hooks/blocking/test_code_rules_enforcer.py +21 -0
  35. package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +70 -0
  36. package/hooks/blocking/test_code_rules_enforcer_any_imports_and_cast.py +92 -0
  37. package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +143 -0
  38. package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +152 -0
  39. package/hooks/blocking/test_code_rules_enforcer_bare_except.py +120 -0
  40. package/hooks/blocking/test_code_rules_enforcer_boundary_types.py +175 -0
  41. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  42. package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +50 -0
  43. package/hooks/blocking/test_code_rules_enforcer_docstring_format.py +255 -0
  44. package/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py +130 -0
  45. package/hooks/blocking/test_code_rules_enforcer_stub_implementations.py +141 -0
  46. package/hooks/blocking/test_code_rules_enforcer_test_branching.py +143 -0
  47. package/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py +169 -0
  48. package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +99 -0
  49. package/hooks/blocking/test_code_rules_enforcer_typed_dict_pairs.py +141 -0
  50. package/hooks/blocking/test_convergence_gate_blocker.py +63 -0
  51. package/hooks/blocking/test_destructive_command_blocker.py +146 -0
  52. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +102 -0
  53. package/hooks/blocking/test_gh_body_arg_blocker.py +45 -0
  54. package/hooks/blocking/test_md_to_html_blocker.py +317 -0
  55. package/hooks/blocking/test_pr_description_enforcer.py +69 -8
  56. package/hooks/config/any_type_config.py +7 -0
  57. package/hooks/config/banned_identifiers_constants.py +11 -0
  58. package/hooks/config/blocking_check_limits.py +38 -0
  59. package/hooks/config/bot_mention_comment_blocker_constants.py +20 -0
  60. package/hooks/config/code_rules_enforcer_constants.py +53 -0
  61. package/hooks/config/convergence_branch_constants.py +9 -0
  62. package/hooks/config/doc_gist_auto_publish_constants.py +18 -0
  63. package/hooks/config/html_companion_constants.py +20 -0
  64. package/hooks/config/inline_tuple_string_magic_constants.py +22 -0
  65. package/hooks/config/pr_description_enforcer_constants.py +14 -0
  66. package/hooks/config/test_banned_identifiers_constants.py +17 -0
  67. package/hooks/hooks.json +28 -20
  68. package/hooks/pyproject.toml +69 -0
  69. package/hooks/validators/mypy_integration.py +47 -1
  70. package/hooks/validators/run_all_validators.py +3 -3
  71. package/hooks/validators/test_mypy_integration.py +50 -1
  72. package/hooks/workflow/doc_gist_auto_publish.py +144 -0
  73. package/hooks/workflow/md_to_html_companion.py +365 -0
  74. package/hooks/workflow/test_doc_gist_auto_publish.py +117 -0
  75. package/hooks/workflow/test_md_to_html_companion.py +452 -0
  76. package/package.json +1 -1
  77. package/rules/gh-body-file.md +2 -0
  78. package/scripts/Install-SweepEmptyDirs.ps1 +111 -0
  79. package/scripts/check.ps1 +106 -0
  80. package/scripts/config/timing.py +11 -0
  81. package/scripts/sweep_empty_dirs.py +138 -0
  82. package/scripts/sync_to_cursor/rules.py +1 -1
  83. package/scripts/test_sweep_empty_dirs.py +183 -0
  84. package/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +323 -0
  85. package/skills/_shared/pr-loop/scripts/_cli_utils.py +22 -0
  86. package/skills/_shared/pr-loop/scripts/_path_resolver.py +165 -0
  87. package/skills/_shared/pr-loop/scripts/_xml_utils.py +20 -0
  88. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +182 -0
  89. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +185 -0
  90. package/skills/_shared/pr-loop/scripts/config/__init__.py +0 -0
  91. package/skills/_shared/pr-loop/scripts/config/path_resolver_constants.py +78 -0
  92. package/skills/_shared/pr-loop/scripts/init_loop_state.py +135 -0
  93. package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +175 -0
  94. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +182 -0
  95. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +206 -0
  96. package/skills/bugteam/CONSTRAINTS.md +21 -22
  97. package/skills/bugteam/EXAMPLES.md +3 -3
  98. package/skills/bugteam/PROMPTS.md +227 -67
  99. package/skills/bugteam/SKILL.md +132 -455
  100. package/skills/bugteam/reference/README.md +1 -1
  101. package/skills/bugteam/reference/audit-and-teammates.md +112 -39
  102. package/skills/bugteam/reference/audit-contract.md +4 -22
  103. package/skills/bugteam/reference/copilot-gap-analysis.md +8 -5
  104. package/skills/bugteam/reference/design-rationale.md +2 -2
  105. package/skills/bugteam/reference/github-pr-reviews.md +50 -57
  106. package/skills/bugteam/reference/obstacles/audit-assign-ids.md +13 -0
  107. package/skills/bugteam/reference/obstacles/audit-capture-excerpts.md +13 -0
  108. package/skills/bugteam/reference/obstacles/audit-walk-categories.md +13 -0
  109. package/skills/bugteam/reference/obstacles/audit-write-xml.md +13 -0
  110. package/skills/bugteam/reference/obstacles/fix-append-summary.md +13 -0
  111. package/skills/bugteam/reference/obstacles/fix-apply-fixes.md +13 -0
  112. package/skills/bugteam/reference/obstacles/fix-git-add-commit.md +13 -0
  113. package/skills/bugteam/reference/obstacles/fix-git-push.md +13 -0
  114. package/skills/bugteam/reference/obstacles/fix-post-reply.md +13 -0
  115. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +13 -0
  116. package/skills/bugteam/reference/obstacles/fix-py-compile.md +13 -0
  117. package/skills/bugteam/reference/obstacles/fix-read-files.md +13 -0
  118. package/skills/bugteam/reference/obstacles/fix-resolve-thread.md +13 -0
  119. package/skills/bugteam/reference/obstacles/fix-test-suite.md +13 -0
  120. package/skills/bugteam/reference/obstacles/fix-violation-count.md +13 -0
  121. package/skills/bugteam/reference/obstacles/fix-write-xml.md +13 -0
  122. package/skills/bugteam/reference/team-setup.md +111 -9
  123. package/skills/bugteam/reference/teardown-publish-permissions.md +39 -8
  124. package/skills/bugteam/scripts/README.md +60 -0
  125. package/skills/bugteam/scripts/_claude_permissions_common.py +358 -0
  126. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +976 -0
  127. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +375 -0
  128. package/skills/bugteam/scripts/bugteam_preflight.py +328 -0
  129. package/skills/bugteam/scripts/config/bugteam_code_rules_gate_constants.py +25 -0
  130. package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +26 -0
  131. package/skills/bugteam/scripts/config/bugteam_preflight_constants.py +35 -0
  132. package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +20 -0
  133. package/skills/bugteam/scripts/config/probe_code_rules_enforcer_check_constants.py +12 -0
  134. package/skills/bugteam/scripts/config/windows_safe_rmtree_constants.py +7 -0
  135. package/skills/bugteam/scripts/grant_project_claude_permissions.py +175 -0
  136. package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +107 -0
  137. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +220 -0
  138. package/skills/bugteam/scripts/test__claude_permissions_common.py +112 -0
  139. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +400 -0
  140. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +384 -0
  141. package/skills/bugteam/scripts/test_bugteam_preflight.py +309 -0
  142. package/skills/bugteam/scripts/test_claude_permissions_common.py +195 -0
  143. package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +55 -0
  144. package/skills/bugteam/scripts/test_probe_code_rules_enforcer_check.py +76 -0
  145. package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +55 -0
  146. package/skills/bugteam/scripts/test_windows_safe_rmtree.py +108 -0
  147. package/skills/bugteam/scripts/windows_safe_rmtree.py +100 -0
  148. package/skills/bugteam/test_skill_additions.py +1 -11
  149. package/skills/code/SKILL.md +176 -0
  150. package/skills/copilot-review/SKILL.md +16 -0
  151. package/skills/doc-gist/SKILL.md +99 -0
  152. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +453 -0
  153. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +515 -0
  154. package/skills/doc-gist/references/examples/03-code-review-pr.html +638 -0
  155. package/skills/doc-gist/references/examples/04-code-understanding.html +491 -0
  156. package/skills/doc-gist/references/examples/05-design-system.html +629 -0
  157. package/skills/doc-gist/references/examples/06-component-variants.html +605 -0
  158. package/skills/doc-gist/references/examples/07-prototype-animation.html +455 -0
  159. package/skills/doc-gist/references/examples/08-prototype-interaction.html +396 -0
  160. package/skills/doc-gist/references/examples/09-slide-deck.html +592 -0
  161. package/skills/doc-gist/references/examples/10-svg-illustrations.html +492 -0
  162. package/skills/doc-gist/references/examples/11-status-report.html +528 -0
  163. package/skills/doc-gist/references/examples/12-incident-report.html +596 -0
  164. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +395 -0
  165. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +381 -0
  166. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +368 -0
  167. package/skills/doc-gist/references/examples/16-implementation-plan.html +702 -0
  168. package/skills/doc-gist/references/examples/17-pr-writeup.html +595 -0
  169. package/skills/doc-gist/references/examples/18-editor-triage-board.html +573 -0
  170. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +663 -0
  171. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +722 -0
  172. package/skills/doc-gist/references/examples/README.md +5 -0
  173. package/skills/doc-gist/scripts/config/__init__.py +0 -0
  174. package/skills/doc-gist/scripts/config/gist_upload_constants.py +16 -0
  175. package/skills/doc-gist/scripts/gist_upload.py +177 -0
  176. package/skills/doc-gist/scripts/test_gist_upload.py +51 -0
  177. package/skills/findbugs/SKILL.md +96 -2
  178. package/skills/monitor-open-prs/SKILL.md +14 -32
  179. package/skills/monitor-open-prs/test_skill_contract.py +0 -11
  180. package/skills/pr-consistency-audit/SKILL.md +112 -0
  181. package/skills/pr-consistency-audit/reference/detection-rules.md +96 -0
  182. package/skills/pr-consistency-audit/reference/illustrations.md +78 -0
  183. package/skills/pr-converge/SKILL.md +229 -23
  184. package/skills/pr-converge/config/__init__.py +0 -0
  185. package/skills/pr-converge/config/constants.py +63 -0
  186. package/skills/pr-converge/reference/convergence-gates.md +138 -44
  187. package/skills/pr-converge/reference/examples.md +43 -11
  188. package/skills/pr-converge/reference/fix-protocol.md +6 -5
  189. package/skills/pr-converge/reference/ground-rules.md +5 -3
  190. package/skills/pr-converge/reference/multi-pr-orchestration.md +44 -19
  191. package/skills/pr-converge/reference/obstacles/fix-post-replies.md +13 -0
  192. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +13 -0
  193. package/skills/pr-converge/reference/obstacles/fix-push.md +13 -0
  194. package/skills/pr-converge/reference/obstacles/fix-read-filelines.md +13 -0
  195. package/skills/pr-converge/reference/obstacles/fix-reset-state.md +13 -0
  196. package/skills/pr-converge/reference/obstacles/fix-resolve-threads.md +13 -0
  197. package/skills/pr-converge/reference/obstacles/fix-spawn-clean-coder.md +13 -0
  198. package/skills/pr-converge/reference/obstacles/fix-stage-commit.md +13 -0
  199. package/skills/pr-converge/reference/obstacles/fix-trigger-bugbot.md +13 -0
  200. package/skills/pr-converge/reference/obstacles/fix-write-test.md +13 -0
  201. package/skills/pr-converge/reference/per-tick.md +107 -31
  202. package/skills/pr-converge/reference/state-schema.md +22 -1
  203. package/skills/pr-converge/reference/stop-conditions.md +9 -7
  204. package/skills/pr-converge/scripts/README.md +34 -46
  205. package/skills/pr-converge/scripts/check_bugbot_ci.py +279 -0
  206. package/skills/pr-converge/scripts/check_convergence.py +497 -0
  207. package/skills/pr-converge/scripts/check_pending_reviews.py +154 -0
  208. package/skills/pr-converge/scripts/config/pr_converge_constants.py +118 -0
  209. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +134 -0
  210. package/skills/pr-converge/scripts/post_fix_reply.py +168 -0
  211. package/skills/pr-converge/scripts/test_check_bugbot_ci.py +312 -0
  212. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -12
  213. package/skills/qbug/SKILL.md +157 -27
  214. package/skills/session-log/SKILL.md +216 -114
  215. package/skills/session-tidy/SKILL.md +1 -1
  216. package/skills/skill-builder/SKILL.md +138 -56
  217. package/skills/skill-builder/references/delegation-map.md +72 -113
  218. package/skills/skill-builder/references/progressive-disclosure.md +122 -0
  219. package/skills/skill-builder/references/self-audit-checklist.md +92 -0
  220. package/skills/skill-builder/references/skill-types.md +228 -0
  221. package/skills/skill-builder/references/thariq-x-post-skills.json +33 -0
  222. package/skills/skill-builder/templates/gap-analysis.md +15 -8
  223. package/skills/skill-builder/workflows/improve-skill.md +86 -57
  224. package/skills/skill-builder/workflows/new-skill.md +80 -168
  225. package/skills/skill-builder/workflows/polish-skill.md +78 -54
  226. package/skills/structure-prompt/SKILL.md +50 -0
  227. package/skills/structure-prompt/reference/adversarial-tuning.md +62 -0
  228. package/skills/structure-prompt/reference/block-classification.md +27 -0
  229. package/skills/structure-prompt/reference/canonical-case.md +48 -0
  230. package/skills/structure-prompt/reference/citation-depth.md +70 -0
  231. package/skills/structure-prompt/reference/cleanup.md +33 -0
  232. package/skills/structure-prompt/reference/constraints.md +33 -0
  233. package/skills/structure-prompt/reference/directives.md +37 -0
  234. package/skills/structure-prompt/reference/examples.md +72 -0
  235. package/skills/structure-prompt/reference/instantiation.md +51 -0
  236. package/skills/structure-prompt/reference/output-contract.md +72 -0
  237. package/skills/structure-prompt/reference/per-category.md +23 -0
  238. package/skills/structure-prompt/reference/persona.md +38 -0
  239. package/skills/structure-prompt/reference/research.md +33 -0
  240. package/skills/structure-prompt/reference/structure.md +28 -0
  241. package/agents/code-standards-agent.md +0 -93
  242. package/agents/groq-coder.md +0 -113
  243. package/agents/plan-executor.md +0 -226
  244. package/agents/project-docs-analyzer.md +0 -53
  245. package/agents/project-structure-organizer-agent.md +0 -72
  246. package/agents/skill-to-agent-converter.md +0 -370
  247. package/agents/skill-writer-agent.md +0 -470
  248. package/agents/user-docs-writer.md +0 -67
  249. package/agents/workflow-visual-documenter.md +0 -82
  250. package/commands/readability-review.md +0 -20
  251. package/hooks/mypy.ini +0 -2
  252. package/hooks/notification/attention_needed_notify.py +0 -71
  253. package/hooks/notification/claude_notification_handler.py +0 -67
  254. package/hooks/notification/notification_utils.py +0 -267
  255. package/hooks/notification/subagent_complete_notify.py +0 -381
  256. package/hooks/notification/test_attention_needed_notify.py +0 -47
  257. package/hooks/notification/test_claude_notification_handler.py +0 -54
  258. package/hooks/notification/test_notification_utils.py +0 -91
  259. package/hooks/notification/test_subagent_complete_notify.py +0 -79
  260. package/scripts/config/groq_bugteam_config.py +0 -230
  261. package/scripts/config/test_groq_bugteam_config.py +0 -83
  262. package/scripts/config/test_spec_implementer_prompt.py +0 -32
  263. package/scripts/groq_bugteam.README.md +0 -131
  264. package/scripts/groq_bugteam.py +0 -647
  265. package/scripts/groq_bugteam_dotenv.py +0 -40
  266. package/scripts/groq_bugteam_spec.py +0 -226
  267. package/scripts/test_groq_bugteam.py +0 -529
  268. package/scripts/test_groq_bugteam_apply_fix_from_spec.py +0 -426
  269. package/scripts/test_groq_bugteam_dotenv.py +0 -66
  270. package/scripts/test_groq_bugteam_spec.py +0 -338
  271. package/skills/bugteam/SKILL_EVALS.md +0 -309
  272. package/skills/dream/SKILL.md +0 -118
  273. package/skills/ingest/SKILL.md +0 -40
  274. package/skills/npm-creator/SKILL.md +0 -187
  275. package/skills/readability-review/SKILL.md +0 -127
  276. package/skills/resume-review/SKILL.md +0 -261
  277. package/skills/rule-audit/SKILL.md +0 -307
  278. package/skills/rule-creator/SKILL.md +0 -150
  279. package/skills/searching-obsidian-vault/SKILL.md +0 -131
  280. package/skills/skill-writer/REFERENCE.md +0 -284
  281. package/skills/skill-writer/SKILL.md +0 -222
  282. package/skills/tdd-team/SKILL.md +0 -128
@@ -0,0 +1,175 @@
1
+ """Tests for check_boundary_types — flags Any at module boundaries.
2
+
3
+ Per Plan 1c.boundary_type_check / Phase B6: a function signature or class
4
+ attribute typed with `Any` (directly or nested inside a generic) makes
5
+ no type promise to callers. Production code at boundaries should name
6
+ the concrete shape it accepts and returns. Local variables are exempt
7
+ (they are private to the function); `if TYPE_CHECKING:` blocks are
8
+ exempt (those imports never reach runtime); `protocols.py` and
9
+ `types.py` are exempt (interface declaration files).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import importlib.util
15
+ from pathlib import Path
16
+ from types import ModuleType
17
+
18
+
19
+ def _load_enforcer_module() -> ModuleType:
20
+ module_path = Path(__file__).parent / "code_rules_enforcer.py"
21
+ spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
22
+ assert spec is not None
23
+ assert spec.loader is not None
24
+ module = importlib.util.module_from_spec(spec)
25
+ spec.loader.exec_module(module)
26
+ return module
27
+
28
+
29
+ code_rules_enforcer = _load_enforcer_module()
30
+
31
+
32
+ def check_boundary_types(content: str, file_path: str) -> list[str]:
33
+ return code_rules_enforcer.check_boundary_types(content, file_path)
34
+
35
+
36
+ PRODUCTION_FILE_PATH = "/project/src/services.py"
37
+ PROTOCOLS_FILE_PATH = "/project/src/protocols.py"
38
+ TYPES_FILE_PATH = "/project/src/types.py"
39
+ TEST_FILE_PATH = "/project/src/test_services.py"
40
+ HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
41
+
42
+
43
+ def test_should_flag_any_as_direct_param_annotation() -> None:
44
+ source = (
45
+ "from typing import Any\n\ndef fetch(payload: Any) -> None:\n return None\n"
46
+ )
47
+ issues = check_boundary_types(source, PRODUCTION_FILE_PATH)
48
+ assert any("Any" in each for each in issues), (
49
+ f"Expected Any-in-signature flag, got: {issues!r}"
50
+ )
51
+
52
+
53
+ def test_should_flag_any_in_dict_param() -> None:
54
+ source = (
55
+ "from typing import Any\n"
56
+ "\n"
57
+ "def fetch(payload: dict[str, Any]) -> None:\n"
58
+ " return None\n"
59
+ )
60
+ issues = check_boundary_types(source, PRODUCTION_FILE_PATH)
61
+ assert any("Any" in each for each in issues), (
62
+ f"Expected dict[str, Any] flagged, got: {issues!r}"
63
+ )
64
+
65
+
66
+ def test_should_flag_any_as_return_type() -> None:
67
+ source = (
68
+ "from typing import Any, List\n\ndef fetch() -> List[Any]:\n return []\n"
69
+ )
70
+ issues = check_boundary_types(source, PRODUCTION_FILE_PATH)
71
+ assert any("Any" in each for each in issues), (
72
+ f"Expected List[Any] return flagged, got: {issues!r}"
73
+ )
74
+
75
+
76
+ def test_should_flag_any_nested_two_levels() -> None:
77
+ source = (
78
+ "from typing import Any\n"
79
+ "\n"
80
+ "def fetch(payload: dict[str, list[Any]]) -> None:\n"
81
+ " return None\n"
82
+ )
83
+ issues = check_boundary_types(source, PRODUCTION_FILE_PATH)
84
+ assert any("Any" in each for each in issues), (
85
+ f"Expected nested Any flagged, got: {issues!r}"
86
+ )
87
+
88
+
89
+ def test_should_flag_callable_returning_any() -> None:
90
+ source = (
91
+ "from typing import Any, Callable\n"
92
+ "\n"
93
+ "def fetch() -> Callable[..., Any]:\n"
94
+ " return lambda: 1\n"
95
+ )
96
+ issues = check_boundary_types(source, PRODUCTION_FILE_PATH)
97
+ assert any("Any" in each for each in issues), (
98
+ f"Expected Callable[..., Any] flagged, got: {issues!r}"
99
+ )
100
+
101
+
102
+ def test_should_flag_any_in_class_attribute_annotation() -> None:
103
+ source = "from typing import Any\n\nclass Cache:\n storage: dict[str, Any]\n"
104
+ issues = check_boundary_types(source, PRODUCTION_FILE_PATH)
105
+ assert any("Any" in each for each in issues), (
106
+ f"Expected class-attribute Any flagged, got: {issues!r}"
107
+ )
108
+
109
+
110
+ def test_should_not_flag_specific_dict_value_type() -> None:
111
+ source = "def fetch(payload: dict[str, int]) -> dict[str, str]:\n return {}\n"
112
+ issues = check_boundary_types(source, PRODUCTION_FILE_PATH)
113
+ assert issues == [], f"Specific types must not be flagged, got: {issues!r}"
114
+
115
+
116
+ def test_should_not_flag_local_variable_annotation() -> None:
117
+ source = (
118
+ "from typing import Any\n"
119
+ "\n"
120
+ "def fetch() -> None:\n"
121
+ " cache: dict[str, Any] = {}\n"
122
+ " return None\n"
123
+ )
124
+ issues = check_boundary_types(source, PRODUCTION_FILE_PATH)
125
+ assert issues == [], f"Local var annotations must not be flagged, got: {issues!r}"
126
+
127
+
128
+ def test_should_skip_protocols_file() -> None:
129
+ source = (
130
+ "from typing import Any, Protocol\n"
131
+ "\n"
132
+ "class Storage(Protocol):\n"
133
+ " def get(self, key: str) -> Any: ...\n"
134
+ )
135
+ issues = check_boundary_types(source, PROTOCOLS_FILE_PATH)
136
+ assert issues == [], f"protocols.py exempt, got: {issues!r}"
137
+
138
+
139
+ def test_should_skip_types_file() -> None:
140
+ source = (
141
+ "from typing import Any\n\ndef coerce(value: Any) -> Any:\n return value\n"
142
+ )
143
+ issues = check_boundary_types(source, TYPES_FILE_PATH)
144
+ assert issues == [], f"types.py exempt, got: {issues!r}"
145
+
146
+
147
+ def test_should_skip_test_file() -> None:
148
+ source = (
149
+ "from typing import Any\n\ndef fetch(payload: Any) -> None:\n return None\n"
150
+ )
151
+ issues = check_boundary_types(source, TEST_FILE_PATH)
152
+ assert issues == [], f"Test files exempt, got: {issues!r}"
153
+
154
+
155
+ def test_should_skip_hook_infrastructure() -> None:
156
+ source = (
157
+ "from typing import Any\n\ndef fetch(payload: Any) -> None:\n return None\n"
158
+ )
159
+ issues = check_boundary_types(source, HOOK_INFRASTRUCTURE_PATH)
160
+ assert issues == [], f"Hook infrastructure exempt, got: {issues!r}"
161
+
162
+
163
+ def test_should_handle_syntax_error_gracefully() -> None:
164
+ source = "def fetch(\n"
165
+ issues = check_boundary_types(source, PRODUCTION_FILE_PATH)
166
+ assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
167
+
168
+
169
+ def test_should_include_line_number() -> None:
170
+ source = (
171
+ "from typing import Any\n\ndef fetch(payload: Any) -> None:\n return None\n"
172
+ )
173
+ issues = check_boundary_types(source, PRODUCTION_FILE_PATH)
174
+ assert len(issues) >= 1
175
+ assert "Line 3" in issues[0], f"Issue must include line number, got: {issues[0]!r}"
@@ -67,7 +67,6 @@ KNOWN_UNCAPPED_CHECKS_PENDING_REVIEW: frozenset[str] = frozenset(
67
67
  "check_return_annotations",
68
68
  "check_skip_decorators_in_tests",
69
69
  "check_string_literal_magic",
70
- "check_type_escape_hatches",
71
70
  "check_unused_optional_parameters",
72
71
  "check_windows_api_none",
73
72
  }
@@ -76,6 +76,56 @@ def test_should_flag_pep604_union_dict_parameter() -> None:
76
76
  )
77
77
 
78
78
 
79
+ def test_should_not_flag_bare_dict_parameter() -> None:
80
+ source = "def consume(user_record: dict) -> None:\n return None\n"
81
+ issues = code_rules_enforcer.check_collection_prefix(source, PRODUCTION_FILE_PATH)
82
+ assert not any(
83
+ "Collection parameter user_record -" in each_issue for each_issue in issues
84
+ ), (
85
+ f"Bare dict parameter is a structured record, not a collection, got: {issues}"
86
+ )
87
+
88
+
89
+ def test_should_flag_subscripted_dict_parameter() -> None:
90
+ source = "def consume(price_lookup: dict[str, int]) -> None:\n return None\n"
91
+ issues = code_rules_enforcer.check_collection_prefix(source, PRODUCTION_FILE_PATH)
92
+ assert any(
93
+ "Collection parameter price_lookup -" in each_issue for each_issue in issues
94
+ ), (
95
+ f"Expected dict[K, V] parameter flagged, got: {issues}"
96
+ )
97
+
98
+
99
+ def test_should_not_flag_module_level_bare_dict_constant() -> None:
100
+ source = "MY_USER_RECORD: dict = {}\n"
101
+ issues = code_rules_enforcer.check_collection_prefix(source, PRODUCTION_FILE_PATH)
102
+ assert not any(
103
+ "Collection constant MY_USER_RECORD -" in each_issue for each_issue in issues
104
+ ), (
105
+ f"Bare dict module-level constant is a structured record, not a collection, got: {issues}"
106
+ )
107
+
108
+
109
+ def test_should_flag_module_level_subscripted_dict_constant_without_all_prefix() -> None:
110
+ source = "MY_PRICE_LOOKUP: dict[str, int] = {}\n"
111
+ issues = code_rules_enforcer.check_collection_prefix(source, PRODUCTION_FILE_PATH)
112
+ assert any(
113
+ "Collection constant MY_PRICE_LOOKUP -" in each_issue for each_issue in issues
114
+ ), (
115
+ f"Expected dict[K, V] module-level constant flagged, got: {issues}"
116
+ )
117
+
118
+
119
+ def test_should_not_flag_pep604_bare_dict_union_parameter() -> None:
120
+ source = "def consume(user_record: dict | None = None) -> None:\n return None\n"
121
+ issues = code_rules_enforcer.check_collection_prefix(source, PRODUCTION_FILE_PATH)
122
+ assert not any(
123
+ "Collection parameter user_record -" in each_issue for each_issue in issues
124
+ ), (
125
+ f"Bare dict | None parameter is a structured record, not a collection, got: {issues}"
126
+ )
127
+
128
+
79
129
  def test_should_not_flag_pep604_union_when_param_has_all_prefix() -> None:
80
130
  source = (
81
131
  "def consume(all_numbers: set[int] | None = None) -> None:\n return None\n"
@@ -0,0 +1,255 @@
1
+ """Tests for check_docstring_format — Google-style Args:/Returns:/Raises:.
2
+
3
+ Per Plan 1c.docstring_format_check / Phase B7: a public function whose
4
+ signature takes parameters, returns a non-None value, or raises an
5
+ exception must document those facts in Google-style sections so
6
+ callers can reason about the contract without reading the body.
7
+
8
+ Exemptions: private (`_foo`), dunder (`__init__`), `@property`,
9
+ functions ≤3 lines (trivial), abstract methods, test files.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import importlib.util
15
+ from pathlib import Path
16
+ from types import ModuleType
17
+
18
+
19
+ def _load_enforcer_module() -> ModuleType:
20
+ module_path = Path(__file__).parent / "code_rules_enforcer.py"
21
+ spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
22
+ assert spec is not None
23
+ assert spec.loader is not None
24
+ module = importlib.util.module_from_spec(spec)
25
+ spec.loader.exec_module(module)
26
+ return module
27
+
28
+
29
+ code_rules_enforcer = _load_enforcer_module()
30
+
31
+
32
+ def check_docstring_format(content: str, file_path: str) -> list[str]:
33
+ return code_rules_enforcer.check_docstring_format(content, file_path)
34
+
35
+
36
+ PRODUCTION_FILE_PATH = "/project/src/services.py"
37
+ TEST_FILE_PATH = "/project/src/test_services.py"
38
+ HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
39
+
40
+
41
+ def _function_with_param_no_docstring() -> str:
42
+ return (
43
+ "def fetch_user(user_id: int) -> str:\n"
44
+ " lookup = _registry.get(user_id)\n"
45
+ " if not lookup:\n"
46
+ " return ''\n"
47
+ " return lookup.name\n"
48
+ )
49
+
50
+
51
+ def test_should_flag_public_function_with_params_missing_args_section() -> None:
52
+ issues = check_docstring_format(
53
+ _function_with_param_no_docstring(), PRODUCTION_FILE_PATH
54
+ )
55
+ assert any("Args" in each for each in issues), (
56
+ f"Expected missing-Args flag, got: {issues!r}"
57
+ )
58
+
59
+
60
+ def test_should_flag_public_function_with_non_none_return_missing_returns_section() -> (
61
+ None
62
+ ):
63
+ source = (
64
+ "def fetch_user(user_id: int) -> str:\n"
65
+ ' """Look up a user by id.\n'
66
+ "\n"
67
+ " Args:\n"
68
+ " user_id: The user identifier.\n"
69
+ ' """\n'
70
+ " lookup = _registry.get(user_id)\n"
71
+ " if not lookup:\n"
72
+ " return ''\n"
73
+ " return lookup.name\n"
74
+ )
75
+ issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
76
+ assert any("Returns" in each or "Yields" in each for each in issues), (
77
+ f"Expected missing-Returns flag, got: {issues!r}"
78
+ )
79
+
80
+
81
+ def test_should_flag_public_function_with_raise_missing_raises_section() -> None:
82
+ source = (
83
+ "def fetch_user(user_id: int) -> str:\n"
84
+ ' """Look up a user by id.\n'
85
+ "\n"
86
+ " Args:\n"
87
+ " user_id: The user identifier.\n"
88
+ "\n"
89
+ " Returns:\n"
90
+ " The user name.\n"
91
+ ' """\n'
92
+ " lookup = _registry.get(user_id)\n"
93
+ " if not lookup:\n"
94
+ " raise LookupError('missing')\n"
95
+ " return lookup.name\n"
96
+ )
97
+ issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
98
+ assert any("Raises" in each for each in issues), (
99
+ f"Expected missing-Raises flag, got: {issues!r}"
100
+ )
101
+
102
+
103
+ def test_should_not_flag_function_with_complete_google_docstring() -> None:
104
+ source = (
105
+ "def fetch_user(user_id: int) -> str:\n"
106
+ ' """Look up a user by id.\n'
107
+ "\n"
108
+ " Args:\n"
109
+ " user_id: The user identifier.\n"
110
+ "\n"
111
+ " Returns:\n"
112
+ " The user name.\n"
113
+ "\n"
114
+ " Raises:\n"
115
+ " LookupError: When the user is missing.\n"
116
+ ' """\n'
117
+ " lookup = _registry.get(user_id)\n"
118
+ " if not lookup:\n"
119
+ " raise LookupError('missing')\n"
120
+ " return lookup.name\n"
121
+ )
122
+ issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
123
+ assert issues == [], f"Complete docstring must not be flagged, got: {issues!r}"
124
+
125
+
126
+ def test_should_not_require_returns_when_return_type_is_none() -> None:
127
+ source = (
128
+ "def store_user(user_id: int) -> None:\n"
129
+ ' """Persist a user record.\n'
130
+ "\n"
131
+ " Args:\n"
132
+ " user_id: The user identifier.\n"
133
+ ' """\n'
134
+ " if user_id < 0:\n"
135
+ " return\n"
136
+ " _registry[user_id] = True\n"
137
+ )
138
+ issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
139
+ assert issues == [], (
140
+ f"None-returning function must not require Returns:, got: {issues!r}"
141
+ )
142
+
143
+
144
+ def test_should_accept_yields_in_lieu_of_returns_for_generator() -> None:
145
+ source = (
146
+ 'def stream_users(batch_size: int) -> "Iterator[str]":\n'
147
+ ' """Stream user names lazily.\n'
148
+ "\n"
149
+ " Args:\n"
150
+ " batch_size: How many to read at a time.\n"
151
+ "\n"
152
+ " Yields:\n"
153
+ " Each user name in turn.\n"
154
+ ' """\n'
155
+ " for each in _registry.values():\n"
156
+ " yield each.name\n"
157
+ )
158
+ issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
159
+ assert issues == [], f"Yields: must satisfy Returns: requirement, got: {issues!r}"
160
+
161
+
162
+ def test_should_skip_private_function() -> None:
163
+ source = "def _internal_helper(value: int) -> int:\n return value * 2\n"
164
+ issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
165
+ assert issues == [], f"Private functions exempt, got: {issues!r}"
166
+
167
+
168
+ def test_should_skip_dunder_method() -> None:
169
+ source = (
170
+ "class Cache:\n"
171
+ " def __init__(self, capacity: int) -> None:\n"
172
+ " self._capacity = capacity\n"
173
+ " self._storage = {}\n"
174
+ " self._hits = 0\n"
175
+ " self._misses = 0\n"
176
+ )
177
+ issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
178
+ assert issues == [], f"Dunder methods exempt, got: {issues!r}"
179
+
180
+
181
+ def test_should_skip_property_method() -> None:
182
+ source = (
183
+ "class Cache:\n"
184
+ " @property\n"
185
+ " def capacity(self) -> int:\n"
186
+ " first_calculation = self._capacity\n"
187
+ " adjusted = first_calculation - self._reserved\n"
188
+ " return adjusted\n"
189
+ )
190
+ issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
191
+ assert issues == [], f"@property methods exempt, got: {issues!r}"
192
+
193
+
194
+ def test_should_skip_short_function() -> None:
195
+ source = "def double(value: int) -> int:\n return value * 2\n"
196
+ issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
197
+ assert issues == [], f"Functions <=3 lines exempt, got: {issues!r}"
198
+
199
+
200
+ def test_should_skip_abstract_method() -> None:
201
+ source = (
202
+ "from abc import abstractmethod\n"
203
+ "\n"
204
+ "class Repository:\n"
205
+ " @abstractmethod\n"
206
+ " def fetch(self, key: str) -> int:\n"
207
+ " ...\n"
208
+ )
209
+ issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
210
+ assert issues == [], f"@abstractmethod exempt, got: {issues!r}"
211
+
212
+
213
+ def test_should_skip_test_file() -> None:
214
+ issues = check_docstring_format(_function_with_param_no_docstring(), TEST_FILE_PATH)
215
+ assert issues == [], f"Test files exempt, got: {issues!r}"
216
+
217
+
218
+ def test_should_skip_hook_infrastructure() -> None:
219
+ issues = check_docstring_format(
220
+ _function_with_param_no_docstring(), HOOK_INFRASTRUCTURE_PATH
221
+ )
222
+ assert issues == [], f"Hook infrastructure exempt, got: {issues!r}"
223
+
224
+
225
+ def test_should_handle_syntax_error_gracefully() -> None:
226
+ issues = check_docstring_format("def fetch(\n", PRODUCTION_FILE_PATH)
227
+ assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
228
+
229
+
230
+ def test_should_skip_short_function_with_docstring() -> None:
231
+ source = (
232
+ "def double(value: int) -> int:\n"
233
+ ' """Multiply the value by two.\n'
234
+ "\n"
235
+ " The calculation uses left-shift internally.\n"
236
+ ' """\n'
237
+ " return value * 2\n"
238
+ )
239
+ issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
240
+ assert issues == [], (
241
+ f"Short function with docstring must be exempt, got: {issues!r}"
242
+ )
243
+
244
+
245
+ def test_should_not_count_self_or_cls_as_documentable_params() -> None:
246
+ source = (
247
+ "class Cache:\n"
248
+ " def reset(self) -> None:\n"
249
+ ' """Drop all cached entries."""\n'
250
+ " self._storage.clear()\n"
251
+ " self._hits = 0\n"
252
+ " self._misses = 0\n"
253
+ )
254
+ issues = check_docstring_format(source, PRODUCTION_FILE_PATH)
255
+ assert issues == [], f"self-only methods must not require Args:, got: {issues!r}"
@@ -0,0 +1,130 @@
1
+ """Unit tests for the inline-tuple snake_case-string-magic check in code_rules_enforcer.
2
+
3
+ These tests cover the gap surfaced during PR #419: a tuple literal whose first
4
+ element is a snake_case string (e.g. ``("kept", "Unknown status")``) inside a
5
+ function body slipped past the Write/Edit hook even though the commit-time
6
+ gate caught it.
7
+ """
8
+
9
+ import importlib.util
10
+ import pathlib
11
+ import sys
12
+
13
+ _HOOK_DIR = pathlib.Path(__file__).parent
14
+ if str(_HOOK_DIR) not in sys.path:
15
+ sys.path.insert(0, str(_HOOK_DIR))
16
+
17
+ hook_spec = importlib.util.spec_from_file_location(
18
+ "code_rules_enforcer",
19
+ _HOOK_DIR / "code_rules_enforcer.py",
20
+ )
21
+ assert hook_spec is not None
22
+ assert hook_spec.loader is not None
23
+ hook_module = importlib.util.module_from_spec(hook_spec)
24
+ hook_spec.loader.exec_module(hook_module)
25
+ check_inline_tuple_string_magic = hook_module.check_inline_tuple_string_magic
26
+ validate_content = hook_module.validate_content
27
+
28
+ PRODUCTION_FILE_PATH = "packages/app/services/loader.py"
29
+ TEST_FILE_PATH = "packages/app/services/test_loader.py"
30
+ CONFIG_FILE_PATH = "packages/app/config/labels.py"
31
+
32
+
33
+ def test_should_flag_inline_snake_case_tuple_pair_inside_function() -> None:
34
+ content = "def describe(glyph):\n return {'a': ('kept', 'Unknown status')}\n"
35
+ issues = check_inline_tuple_string_magic(content, PRODUCTION_FILE_PATH)
36
+ assert any("'kept'" in each_issue for each_issue in issues), (
37
+ f"Expected 'kept' tuple-pair flagged, got: {issues}"
38
+ )
39
+
40
+
41
+ def test_should_flag_inline_snake_case_tuple_inside_dict_value() -> None:
42
+ content = "def lookup():\n return {'STATUS_KEPT': ('kept', 'Patch unchanged')}\n"
43
+ issues = check_inline_tuple_string_magic(content, PRODUCTION_FILE_PATH)
44
+ assert any("'kept'" in each_issue for each_issue in issues), (
45
+ f"Expected nested tuple flagged, got: {issues}"
46
+ )
47
+
48
+
49
+ def test_should_flag_first_element_snake_case_with_underscore() -> None:
50
+ content = "def label():\n return ('unknown_status', 'placeholder')\n"
51
+ issues = check_inline_tuple_string_magic(content, PRODUCTION_FILE_PATH)
52
+ assert any("'unknown_status'" in each_issue for each_issue in issues), (
53
+ f"Expected snake_case-with-underscore flagged, got: {issues}"
54
+ )
55
+
56
+
57
+ def test_should_not_flag_tuple_outside_function_body() -> None:
58
+ content = "ALL_STATUS = ('kept', 'Unknown status')\n"
59
+ issues = check_inline_tuple_string_magic(content, PRODUCTION_FILE_PATH)
60
+ assert issues == [], (
61
+ f"Module-level constants are not in function bodies; must not flag, got: {issues}"
62
+ )
63
+
64
+
65
+ def test_should_skip_test_files() -> None:
66
+ content = "def test_thing():\n return ('kept', 'Unknown status')\n"
67
+ issues = check_inline_tuple_string_magic(content, TEST_FILE_PATH)
68
+ assert issues == [], f"Test files are exempt, got: {issues}"
69
+
70
+
71
+ def test_should_skip_config_files() -> None:
72
+ content = "def build():\n return ('kept', 'Unknown status')\n"
73
+ issues = check_inline_tuple_string_magic(content, CONFIG_FILE_PATH)
74
+ assert issues == [], f"Config files are exempt, got: {issues}"
75
+
76
+
77
+ def test_should_not_flag_tuple_with_non_snake_case_first_element() -> None:
78
+ content = "def render():\n return ('Title Case', 'Body')\n"
79
+ issues = check_inline_tuple_string_magic(content, PRODUCTION_FILE_PATH)
80
+ assert issues == [], (
81
+ f"Non-snake-case strings are not column/key-like; must not flag, got: {issues}"
82
+ )
83
+
84
+
85
+ def test_should_not_flag_short_string_first_element() -> None:
86
+ content = "def render():\n return ('ok', 'fine')\n"
87
+ issues = check_inline_tuple_string_magic(content, PRODUCTION_FILE_PATH)
88
+ assert issues == [], (
89
+ f"Strings shorter than minimum length are exempt, got: {issues}"
90
+ )
91
+
92
+
93
+ def test_should_not_flag_keyword_strings() -> None:
94
+ content = "def render():\n return ('true', 'false')\n"
95
+ issues = check_inline_tuple_string_magic(content, PRODUCTION_FILE_PATH)
96
+ assert issues == [], (
97
+ f"Keyword literals (true/false/none/null) are exempt, got: {issues}"
98
+ )
99
+
100
+
101
+ def test_should_not_flag_tuple_longer_than_pair() -> None:
102
+ content = "def render():\n return ('kept', 'Unknown status', 'extra')\n"
103
+ issues = check_inline_tuple_string_magic(content, PRODUCTION_FILE_PATH)
104
+ assert issues == [], f"Only two-element tuples are inspected, got: {issues}"
105
+
106
+
107
+ def test_should_dedupe_nested_function_tuples() -> None:
108
+ """Tuples inside nested FunctionDefs must produce one finding, not many.
109
+
110
+ Without deduplication the outer ast.walk enumerates every FunctionDef
111
+ including nested ones, then the inner walk visits each tuple via every
112
+ enclosing function. Must surface exactly one finding per tuple site.
113
+ """
114
+ content = (
115
+ "def outer():\n"
116
+ " def inner():\n"
117
+ ' x = ("some_column_name", 42)\n'
118
+ " return x\n"
119
+ " return inner\n"
120
+ )
121
+ issues = check_inline_tuple_string_magic(content, PRODUCTION_FILE_PATH)
122
+ assert len(issues) == 1, f"expected 1 finding, got {len(issues)}: {issues!r}"
123
+
124
+
125
+ def test_validate_content_wires_check_for_python_files() -> None:
126
+ content = "def describe(glyph):\n return {'a': ('kept', 'Unknown status')}\n"
127
+ issues = validate_content(content, PRODUCTION_FILE_PATH)
128
+ assert any("'kept'" in each_issue for each_issue in issues), (
129
+ f"validate_content must run the new check, got: {issues}"
130
+ )