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.
- package/CLAUDE.md +16 -13
- package/_shared/pr-loop/audit-contract.md +24 -12
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/README.md +1 -0
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviewer_availability_constants.py +12 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +0 -2
- package/_shared/pr-loop/scripts/reviewer_availability.py +182 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +9 -33
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +159 -0
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +14 -4
- package/agents/clean-coder.md +3 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/bin/install.mjs +16 -8
- package/bin/install.test.mjs +17 -6
- package/commands/CLAUDE.md +0 -1
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -4
- package/hooks/blocking/code_rules_constants_config.py +164 -1
- package/hooks/blocking/code_rules_docstrings.py +428 -15
- package/hooks/blocking/code_rules_enforcer.py +33 -0
- package/hooks/blocking/code_rules_imports_logging.py +867 -1
- package/hooks/blocking/code_rules_magic_values.py +5 -0
- package/hooks/blocking/code_rules_naming_collection.py +152 -6
- package/hooks/blocking/code_rules_shared.py +34 -0
- package/hooks/blocking/code_rules_string_magic.py +68 -0
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
- package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
- package/hooks/blocking/reviewer_spawn_gate.py +182 -0
- package/hooks/blocking/stale_comment_reference_blocker.py +267 -0
- package/hooks/blocking/state_description_blocker.py +96 -5
- package/hooks/blocking/test_code_rules_config_duplicate_path_anchor.py +132 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +385 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +67 -0
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +40 -0
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +213 -0
- package/hooks/blocking/test_code_rules_enforcer_referenced_underscore_loop.py +169 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
- package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
- package/hooks/blocking/test_code_rules_js_bare_flag_return_directive.py +266 -0
- package/hooks/blocking/test_code_rules_js_sibling_return_object_key_drift.py +490 -0
- package/hooks/blocking/test_code_rules_logging_adjacent_literals.py +171 -0
- package/hooks/blocking/test_code_rules_magic_values.py +54 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
- package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +9 -3
- package/hooks/blocking/test_reviewer_spawn_gate.py +230 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
- package/hooks/blocking/test_stale_comment_reference_blocker.py +236 -0
- package/hooks/blocking/test_state_description_blocker.py +135 -0
- package/hooks/hooks.json +5 -0
- package/hooks/hooks_constants/CLAUDE.md +3 -4
- package/hooks/hooks_constants/blocking_check_limits.py +46 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
- package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +41 -0
- package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +76 -0
- package/hooks/hooks_constants/state_description_blocker_constants.py +8 -0
- package/hooks/validation/post_tool_use_dispatcher.py +1 -1
- package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
- package/hooks/workflow/CLAUDE.md +2 -8
- package/package.json +1 -1
- package/rules/CLAUDE.md +5 -2
- package/rules/claude-md-orphan-file.md +5 -0
- package/rules/docstring-prose-matches-implementation.md +10 -1
- package/rules/env-var-table-code-drift.md +5 -0
- package/rules/es-exe-file-search.md +17 -0
- package/rules/no-historical-clutter.md +12 -1
- package/rules/orphan-css-class.md +5 -0
- package/rules/package-inventory-stale-entry.md +10 -0
- package/rules/paired-test-coverage.md +5 -0
- package/rules/plain-illustrative-docstrings.md +40 -1
- package/rules/verify-before-asking.md +7 -0
- package/rules/verify-runtime-state.md +40 -0
- package/rules/windows-filesystem-safe.md +8 -0
- package/rules/workers-done-before-complete.md +33 -0
- package/rules/workflow-substitution-slots.md +5 -0
- package/skills/CLAUDE.md +7 -2
- package/skills/autoconverge/SKILL.md +48 -61
- package/skills/autoconverge/reference/closing-report.md +6 -6
- package/skills/autoconverge/reference/convergence.md +17 -15
- package/skills/autoconverge/reference/gotchas.md +6 -3
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
- package/skills/autoconverge/workflow/converge.contract.test.mjs +153 -10
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +55 -19
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
- package/skills/autoconverge/workflow/converge.mjs +167 -106
- package/skills/autoconverge/workflow/render_report.py +7 -11
- package/skills/bugteam/CLAUDE.md +1 -1
- package/skills/bugteam/PROMPTS.md +7 -6
- package/skills/bugteam/SKILL.md +25 -72
- package/skills/bugteam/reference/CLAUDE.md +1 -3
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/bugteam/reference/audit-and-teammates.md +1 -1
- package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/bugteam/reference/team-setup.md +8 -7
- package/skills/bugteam/scripts/CLAUDE.md +0 -6
- package/skills/bugteam/scripts/README.md +0 -4
- package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
- package/skills/code/SKILL.md +2 -0
- package/skills/copilot-review/CLAUDE.md +1 -1
- package/skills/copilot-review/SKILL.md +25 -23
- package/skills/everything-search/SKILL.md +5 -0
- package/skills/findbugs/CLAUDE.md +2 -2
- package/skills/findbugs/SKILL.md +22 -83
- package/skills/fixbugs/SKILL.md +2 -4
- package/skills/log-audit/CLAUDE.md +20 -0
- package/skills/log-audit/SKILL.md +68 -0
- package/skills/log-audit/reference/CLAUDE.md +9 -0
- package/skills/log-audit/reference/charter.md +52 -0
- package/skills/log-audit/scripts/CLAUDE.md +27 -0
- package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
- package/skills/log-audit/scripts/collect_log_window.py +199 -0
- package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
- package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
- package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
- package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
- package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
- package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
- package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
- package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
- package/skills/monitor-open-prs/SKILL.md +2 -2
- package/skills/post-audit-findings/SKILL.md +84 -0
- package/skills/pr-converge/CLAUDE.md +2 -0
- package/skills/pr-converge/SKILL.md +72 -59
- package/skills/pr-converge/reference/CLAUDE.md +1 -1
- package/skills/pr-converge/reference/convergence-gates.md +16 -19
- package/skills/pr-converge/reference/examples.md +5 -5
- package/skills/pr-converge/reference/fix-protocol.md +16 -43
- package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/pr-converge/reference/per-tick.md +24 -45
- package/skills/pr-converge/reference/state-schema.md +15 -0
- package/skills/pr-converge/scripts/README.md +3 -5
- package/skills/pr-fix-protocol/SKILL.md +70 -0
- package/skills/pr-loop-lifecycle/SKILL.md +73 -0
- package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
- package/skills/pr-scope-resolve/SKILL.md +48 -0
- package/skills/qbug/CLAUDE.md +4 -4
- package/skills/qbug/SKILL.md +46 -144
- package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
- package/skills/reviewer-gates/SKILL.md +96 -0
- package/skills/session-log/CLAUDE.md +7 -7
- package/skills/session-log/SKILL.md +27 -44
- package/skills/test_markdown_link_integrity.py +103 -0
- package/commands/doc-gist.md +0 -16
- package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
- package/hooks/blocking/conftest.py +0 -30
- package/hooks/blocking/md_path_exemptions.py +0 -224
- package/hooks/blocking/md_to_html_blocker.py +0 -155
- package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
- package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
- package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
- package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
- package/hooks/hooks_constants/html_companion_constants.py +0 -20
- package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
- package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
- package/hooks/workflow/doc_gist_auto_publish.py +0 -144
- package/hooks/workflow/md_to_html_companion.py +0 -358
- package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
- package/hooks/workflow/test_md_to_html_companion.py +0 -613
- package/skills/bugteam/reference/audit-contract.md +0 -163
- package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
- package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
- package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
- package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
- package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
- package/skills/doc-gist/CLAUDE.md +0 -25
- package/skills/doc-gist/SKILL.md +0 -97
- package/skills/doc-gist/references/CLAUDE.md +0 -9
- package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
- package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
- package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
- package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
- package/skills/doc-gist/references/examples/05-design-system.html +0 -629
- package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
- package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
- package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
- package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
- package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
- package/skills/doc-gist/references/examples/11-status-report.html +0 -528
- package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
- package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
- package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
- package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
- package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
- package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
- package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
- package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
- package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
- package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
- package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
- package/skills/doc-gist/references/examples/README.md +0 -5
- package/skills/doc-gist/scripts/CLAUDE.md +0 -27
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
- package/skills/doc-gist/scripts/gist_upload.py +0 -177
- package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
- /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""Tests for check_docstring_delegation_summary_enumeration_drift (O6).
|
|
2
|
+
|
|
3
|
+
A thin wrapper docstring that enumerates its actions and points at the home
|
|
4
|
+
of the real body drifts when the same-named function in that named neighbor
|
|
5
|
+
carries a summary enumeration omitting one of those actions. The gate fires
|
|
6
|
+
from both sides: saving the wrapper compares it against the named neighbor on
|
|
7
|
+
disk, and saving the delegated body compares it against every neighboring
|
|
8
|
+
wrapper docstring pointing at it.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import sys
|
|
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_delegation_summary_enumeration_drift(
|
|
33
|
+
content: str, file_path: str
|
|
34
|
+
) -> list[str]:
|
|
35
|
+
return code_rules_enforcer.check_docstring_delegation_summary_enumeration_drift(
|
|
36
|
+
content, file_path
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
|
|
41
|
+
return code_rules_enforcer.validate_content(content, file_path, old_content)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
|
|
45
|
+
TARGET_FLOW_STEM = "listing_edit_flow"
|
|
46
|
+
PROCESSOR_FILE_NAME = "portal_processor.py"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _drifted_processor_source() -> str:
|
|
50
|
+
return (
|
|
51
|
+
"class PortalProcessor:\n"
|
|
52
|
+
" async def _refresh_store_sections(self, page: object) -> bool:\n"
|
|
53
|
+
' """Apply App Info, Russia, review note, publication edits;'
|
|
54
|
+
" full doc on ``listing_edit_flow``.\"\"\"\n"
|
|
55
|
+
" return await _refresh_store_sections(self, page)\n"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _matching_processor_source() -> str:
|
|
60
|
+
return (
|
|
61
|
+
"class PortalProcessor:\n"
|
|
62
|
+
" async def _refresh_store_sections(self, page: object) -> bool:\n"
|
|
63
|
+
' """Apply Russia, review note, publication edits;'
|
|
64
|
+
" full doc on ``listing_edit_flow``.\"\"\"\n"
|
|
65
|
+
" return await _refresh_store_sections(self, page)\n"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _target_flow_source() -> str:
|
|
70
|
+
return (
|
|
71
|
+
"async def _refresh_store_sections(processor: object, page: object) -> bool:\n"
|
|
72
|
+
' """Apply Russia uncheck, review note, and Publication edits.\n'
|
|
73
|
+
"\n"
|
|
74
|
+
" The App Info edit runs earlier in the binary phase and never\n"
|
|
75
|
+
" repeats here.\n"
|
|
76
|
+
' """\n'
|
|
77
|
+
" return True\n"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _target_flow_plain_summary() -> str:
|
|
82
|
+
return (
|
|
83
|
+
"async def _refresh_store_sections(processor: object, page: object) -> bool:\n"
|
|
84
|
+
' """Drive one theme end to end."""\n'
|
|
85
|
+
" return True\n"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _create_target_flow(directory: Path, source: str) -> Path:
|
|
90
|
+
created_path = directory / (TARGET_FLOW_STEM + ".py")
|
|
91
|
+
created_path.write_text(source, encoding="utf-8")
|
|
92
|
+
return created_path
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_should_flag_listed_action_absent_from_the_named_neighbor(tmp_path: Path) -> None:
|
|
96
|
+
_create_target_flow(tmp_path, _target_flow_source())
|
|
97
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
98
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
99
|
+
_drifted_processor_source(), checked_path
|
|
100
|
+
)
|
|
101
|
+
assert any("App Info" in each for each in issues), (
|
|
102
|
+
f"Expected 'App Info' to be flagged, got: {issues!r}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_should_pass_matching_summaries(tmp_path: Path) -> None:
|
|
107
|
+
_create_target_flow(tmp_path, _target_flow_source())
|
|
108
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
109
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
110
|
+
_matching_processor_source(), checked_path
|
|
111
|
+
)
|
|
112
|
+
assert issues == [], f"Aligned summaries pass, got: {issues!r}"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_should_flag_saved_body_that_strands_a_pointing_neighbor(tmp_path: Path) -> None:
|
|
116
|
+
checked_path = tmp_path / PROCESSOR_FILE_NAME
|
|
117
|
+
checked_path.write_text(_drifted_processor_source(), encoding="utf-8")
|
|
118
|
+
created_path = str(tmp_path / (TARGET_FLOW_STEM + ".py"))
|
|
119
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
120
|
+
_target_flow_source(), created_path
|
|
121
|
+
)
|
|
122
|
+
assert any(
|
|
123
|
+
"App Info" in each and PROCESSOR_FILE_NAME in each for each in issues
|
|
124
|
+
), f"Expected the stranded caller to be flagged, got: {issues!r}"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_should_pass_plain_one_purpose_summary(tmp_path: Path) -> None:
|
|
128
|
+
_create_target_flow(tmp_path, _target_flow_plain_summary())
|
|
129
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
130
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
131
|
+
_drifted_processor_source(), checked_path
|
|
132
|
+
)
|
|
133
|
+
assert issues == [], (
|
|
134
|
+
f"A comma-free summary compares against nothing, got: {issues!r}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_should_pass_when_the_named_neighbor_does_not_exist(tmp_path: Path) -> None:
|
|
139
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
140
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
141
|
+
_drifted_processor_source(), checked_path
|
|
142
|
+
)
|
|
143
|
+
assert issues == [], f"Nothing on disk to compare against, got: {issues!r}"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_should_pass_single_listed_action(tmp_path: Path) -> None:
|
|
147
|
+
_create_target_flow(tmp_path, _target_flow_source())
|
|
148
|
+
solo_source = (
|
|
149
|
+
"class PortalProcessor:\n"
|
|
150
|
+
" async def _refresh_store_sections(self, page: object) -> bool:\n"
|
|
151
|
+
' """Apply the App Info edit;'
|
|
152
|
+
" full doc on ``listing_edit_flow``.\"\"\"\n"
|
|
153
|
+
" return await _refresh_store_sections(self, page)\n"
|
|
154
|
+
)
|
|
155
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
156
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
157
|
+
solo_source, checked_path
|
|
158
|
+
)
|
|
159
|
+
assert issues == [], f"A lone named action never fires, got: {issues!r}"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_should_skip_strict_test_paths(tmp_path: Path) -> None:
|
|
163
|
+
_create_target_flow(tmp_path, _target_flow_source())
|
|
164
|
+
skipped_path = str(tmp_path / "test_portal_processor.py")
|
|
165
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
166
|
+
_drifted_processor_source(), skipped_path
|
|
167
|
+
)
|
|
168
|
+
assert issues == [], f"Exempt, got: {issues!r}"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_should_skip_hook_infrastructure() -> None:
|
|
172
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
173
|
+
_drifted_processor_source(), HOOK_INFRASTRUCTURE_PATH
|
|
174
|
+
)
|
|
175
|
+
assert issues == [], f"Exempt, got: {issues!r}"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_should_handle_syntax_error_gracefully(tmp_path: Path) -> None:
|
|
179
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
180
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
181
|
+
"def fetch(\n", checked_path
|
|
182
|
+
)
|
|
183
|
+
assert issues == [], f"A syntax error yields no issues, got: {issues!r}"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_should_handle_unparseable_neighbor(tmp_path: Path) -> None:
|
|
187
|
+
_create_target_flow(tmp_path, "def broken(\n")
|
|
188
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
189
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
190
|
+
_drifted_processor_source(), checked_path
|
|
191
|
+
)
|
|
192
|
+
assert issues == [], f"Unparseable neighbors yield no issues, got: {issues!r}"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_validate_content_surfaces_the_drift(tmp_path: Path) -> None:
|
|
196
|
+
_create_target_flow(tmp_path, _target_flow_source())
|
|
197
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
198
|
+
issues = validate_content(_drifted_processor_source(), checked_path, old_content="")
|
|
199
|
+
matching_issues = [each for each in issues if "App Info" in each and "O6" in each]
|
|
200
|
+
assert matching_issues, (
|
|
201
|
+
f"Expected validate_content to surface the O6 finding, got: {issues!r}"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _drifted_processor_source_with_wrapped_summary() -> str:
|
|
206
|
+
return (
|
|
207
|
+
"class PortalProcessor:\n"
|
|
208
|
+
" async def _refresh_store_sections(self, page: object) -> bool:\n"
|
|
209
|
+
' """Apply App Info, Russia, review note, publication\n'
|
|
210
|
+
" edits; full doc on ``listing_edit_flow``.\"\"\"\n"
|
|
211
|
+
" return await _refresh_store_sections(self, page)\n"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_should_flag_listed_action_when_the_summary_wraps_a_physical_line(
|
|
216
|
+
tmp_path: Path,
|
|
217
|
+
) -> None:
|
|
218
|
+
_create_target_flow(tmp_path, _target_flow_source())
|
|
219
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
220
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
221
|
+
_drifted_processor_source_with_wrapped_summary(), checked_path
|
|
222
|
+
)
|
|
223
|
+
assert any("App Info" in each for each in issues), (
|
|
224
|
+
f"A summary wrapped onto a second physical line still names 'App Info', "
|
|
225
|
+
f"got: {issues!r}"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _drifted_processor_source_with_capitalized_and_uneven_whitespace() -> str:
|
|
230
|
+
return (
|
|
231
|
+
"class PortalProcessor:\n"
|
|
232
|
+
" async def _refresh_store_sections(self, page: object) -> bool:\n"
|
|
233
|
+
' """Apply App Info, Russia, review note, And publication'
|
|
234
|
+
" edits; full doc on ``listing_edit_flow``.\"\"\"\n"
|
|
235
|
+
" return await _refresh_store_sections(self, page)\n"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_should_flag_listed_action_when_the_conjunction_is_capitalized_with_uneven_whitespace(
|
|
240
|
+
tmp_path: Path,
|
|
241
|
+
) -> None:
|
|
242
|
+
_create_target_flow(tmp_path, _target_flow_source())
|
|
243
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
244
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
245
|
+
_drifted_processor_source_with_capitalized_and_uneven_whitespace(), checked_path
|
|
246
|
+
)
|
|
247
|
+
assert any("App Info" in each for each in issues), (
|
|
248
|
+
f"A capitalized 'And' with uneven whitespace still splits into its own "
|
|
249
|
+
f"listed entry, got: {issues!r}"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _write_filler_neighbor_modules(directory: Path, filler_count: int) -> None:
|
|
254
|
+
for each_index in range(filler_count):
|
|
255
|
+
filler_path = directory / f"filler_module_{each_index:03d}.py"
|
|
256
|
+
filler_path.write_text("PLACEHOLDER_VALUE = 1\n", encoding="utf-8")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_should_stop_scanning_neighbors_past_the_configured_limit(
|
|
260
|
+
tmp_path: Path,
|
|
261
|
+
) -> None:
|
|
262
|
+
neighbor_scan_limit = sys.modules["code_rules_docstrings"].NEIGHBOR_SCAN_FILE_LIMIT
|
|
263
|
+
_write_filler_neighbor_modules(tmp_path, filler_count=neighbor_scan_limit + 5)
|
|
264
|
+
stranded_wrapper_path = tmp_path / "zzz_stranded_processor.py"
|
|
265
|
+
stranded_wrapper_path.write_text(_drifted_processor_source(), encoding="utf-8")
|
|
266
|
+
created_path = str(tmp_path / (TARGET_FLOW_STEM + ".py"))
|
|
267
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
268
|
+
_target_flow_source(), created_path
|
|
269
|
+
)
|
|
270
|
+
assert issues == [], (
|
|
271
|
+
"The alphabetically-last wrapper sits past the neighbor scan limit, so "
|
|
272
|
+
f"the scan must stop before reaching it, got: {issues!r}"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_should_still_flag_a_stranded_wrapper_within_the_scan_limit(
|
|
277
|
+
tmp_path: Path,
|
|
278
|
+
) -> None:
|
|
279
|
+
neighbor_scan_limit = sys.modules["code_rules_docstrings"].NEIGHBOR_SCAN_FILE_LIMIT
|
|
280
|
+
stranded_wrapper_path = tmp_path / "aaa_stranded_processor.py"
|
|
281
|
+
stranded_wrapper_path.write_text(_drifted_processor_source(), encoding="utf-8")
|
|
282
|
+
_write_filler_neighbor_modules(tmp_path, filler_count=neighbor_scan_limit + 5)
|
|
283
|
+
created_path = str(tmp_path / (TARGET_FLOW_STEM + ".py"))
|
|
284
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
285
|
+
_target_flow_source(), created_path
|
|
286
|
+
)
|
|
287
|
+
assert any(
|
|
288
|
+
"App Info" in each and "aaa_stranded_processor.py" in each for each in issues
|
|
289
|
+
), (
|
|
290
|
+
"The alphabetically-first wrapper sits within the neighbor scan limit "
|
|
291
|
+
f"and stays flagged even with extra filler neighbors, got: {issues!r}"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _target_flow_source_with_nested_duplicate_name() -> str:
|
|
296
|
+
return (
|
|
297
|
+
"async def _refresh_store_sections(processor: object, page: object) -> bool:\n"
|
|
298
|
+
' """Apply Russia uncheck, review note, and Publication edits.\n'
|
|
299
|
+
"\n"
|
|
300
|
+
" The App Info edit runs earlier in the binary phase and never\n"
|
|
301
|
+
" repeats here.\n"
|
|
302
|
+
' """\n'
|
|
303
|
+
" return True\n"
|
|
304
|
+
"\n"
|
|
305
|
+
"\n"
|
|
306
|
+
"def _outer_helper() -> bool:\n"
|
|
307
|
+
" def _refresh_store_sections() -> bool:\n"
|
|
308
|
+
' """A same-named nested helper that must not shadow the'
|
|
309
|
+
' top-level entry."""\n'
|
|
310
|
+
" return True\n"
|
|
311
|
+
"\n"
|
|
312
|
+
" return _refresh_store_sections()\n"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def test_should_not_let_a_nested_duplicate_name_hide_the_top_level_entry(
|
|
317
|
+
tmp_path: Path,
|
|
318
|
+
) -> None:
|
|
319
|
+
_create_target_flow(tmp_path, _target_flow_source_with_nested_duplicate_name())
|
|
320
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
321
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
322
|
+
_drifted_processor_source(), checked_path
|
|
323
|
+
)
|
|
324
|
+
assert any("App Info" in each for each in issues), (
|
|
325
|
+
"A nested helper sharing the top-level function's name must not "
|
|
326
|
+
f"delete the top-level entry from comparison, got: {issues!r}"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _processor_source_with_nested_pointer_wrapper() -> str:
|
|
331
|
+
return (
|
|
332
|
+
"class PortalProcessor:\n"
|
|
333
|
+
" async def _refresh_store_sections(self, page: object) -> bool:\n"
|
|
334
|
+
' """Drive one theme end to end."""\n'
|
|
335
|
+
" return True\n"
|
|
336
|
+
"\n"
|
|
337
|
+
" def _run_nested_helper(self) -> bool:\n"
|
|
338
|
+
" async def _refresh_store_sections() -> bool:\n"
|
|
339
|
+
' """Apply App Info, Russia, review note, publication'
|
|
340
|
+
' edits; full doc on ``listing_edit_flow``.\"\"\"\n'
|
|
341
|
+
" return True\n"
|
|
342
|
+
" return True\n"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def test_should_not_flag_a_nested_wrapper_pointing_at_a_delegate(
|
|
347
|
+
tmp_path: Path,
|
|
348
|
+
) -> None:
|
|
349
|
+
_create_target_flow(tmp_path, _target_flow_source())
|
|
350
|
+
checked_path = str(tmp_path / PROCESSOR_FILE_NAME)
|
|
351
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
352
|
+
_processor_source_with_nested_pointer_wrapper(), checked_path
|
|
353
|
+
)
|
|
354
|
+
assert issues == [], (
|
|
355
|
+
"A function-local wrapper is not a top-level function or class "
|
|
356
|
+
f"method, so it must never be compared, got: {issues!r}"
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _neighbor_source_with_nested_pointer_wrapper() -> str:
|
|
361
|
+
return (
|
|
362
|
+
"def _run_nested_helper() -> bool:\n"
|
|
363
|
+
" async def _refresh_store_sections() -> bool:\n"
|
|
364
|
+
' """Apply App Info, Russia, review note, publication edits;'
|
|
365
|
+
' full doc on ``listing_edit_flow``.\"\"\"\n'
|
|
366
|
+
" return True\n"
|
|
367
|
+
" return True\n"
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def test_should_not_flag_a_nested_neighbor_wrapper_as_a_stranded_caller(
|
|
372
|
+
tmp_path: Path,
|
|
373
|
+
) -> None:
|
|
374
|
+
neighbor_path = tmp_path / "portal_helpers.py"
|
|
375
|
+
neighbor_path.write_text(
|
|
376
|
+
_neighbor_source_with_nested_pointer_wrapper(), encoding="utf-8"
|
|
377
|
+
)
|
|
378
|
+
created_path = str(tmp_path / (TARGET_FLOW_STEM + ".py"))
|
|
379
|
+
issues = check_docstring_delegation_summary_enumeration_drift(
|
|
380
|
+
_target_flow_source(), created_path
|
|
381
|
+
)
|
|
382
|
+
assert issues == [], (
|
|
383
|
+
"A function-local wrapper in a neighbor module is not a pointing "
|
|
384
|
+
f"caller, so it must never be treated as stranded, got: {issues!r}"
|
|
385
|
+
)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Behavioural checks for the show-don't-tell narrative gate.
|
|
2
|
+
|
|
3
|
+
A readable summary earns its place by painting a concrete scene: a sample
|
|
4
|
+
input, an annotated outcome, an OK/FLAG contrast a reader pictures at once. A
|
|
5
|
+
summary that runs many short sentences with no worked example tells without
|
|
6
|
+
showing. The gate flags that wall in a top-level, class, or callable summary,
|
|
7
|
+
and stays quiet the moment the narrative carries a '::' listing or a '>>>'
|
|
8
|
+
doctest.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from types import ModuleType
|
|
17
|
+
|
|
18
|
+
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
19
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
20
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load_enforcer_module() -> ModuleType:
|
|
24
|
+
enforcer_module_name = "code_rules_enforcer"
|
|
25
|
+
enforcer_path = Path(__file__).parent / (enforcer_module_name + ".py")
|
|
26
|
+
spec = importlib.util.spec_from_file_location(enforcer_module_name, enforcer_path)
|
|
27
|
+
assert spec is not None
|
|
28
|
+
assert spec.loader is not None
|
|
29
|
+
loaded_module = importlib.util.module_from_spec(spec)
|
|
30
|
+
spec.loader.exec_module(loaded_module)
|
|
31
|
+
return loaded_module
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def check_docstring_prose_wall_without_illustration(content: str, file_path: str) -> list[str]:
|
|
38
|
+
return code_rules_enforcer.check_docstring_prose_wall_without_illustration(content, file_path)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
|
|
42
|
+
return code_rules_enforcer.validate_content(content, file_path, old_content)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
PRODUCTION_FILE_PATH = "/project/src/tally.py"
|
|
46
|
+
SUITE_FILE_PATH = "/project/src/test_tally.py"
|
|
47
|
+
INFRA_PATH = "/home/user/.claude/hooks/blocking/example.py"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _fixture_top_level_wall() -> str:
|
|
51
|
+
return (
|
|
52
|
+
'"""Assemble the nightly voyage tally from the harbor scans.\n'
|
|
53
|
+
"\n"
|
|
54
|
+
"A scan names one vessel and where it dropped anchor.\n"
|
|
55
|
+
"The tally walks the scans in arrival order and keeps that order.\n"
|
|
56
|
+
"A calm voyage ends well for every vessel it carried.\n"
|
|
57
|
+
"A halted voyage marks the vessel it was near when the storm arrived.\n"
|
|
58
|
+
"A wrecked voyage marks the vessel that sank and stops the walk there.\n"
|
|
59
|
+
"The tally groups the vessels by their final port for the harbor.\n"
|
|
60
|
+
"The harbor reads the tally and sees every arrival at a glance.\n"
|
|
61
|
+
'"""\n'
|
|
62
|
+
"\n"
|
|
63
|
+
"def emit_tally() -> None:\n"
|
|
64
|
+
" return None\n"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _fixture_callable_wall() -> str:
|
|
69
|
+
return (
|
|
70
|
+
"def emit_tally() -> None:\n"
|
|
71
|
+
' """Post the voyage tally to the harbor board.\n'
|
|
72
|
+
"\n"
|
|
73
|
+
" A scan names one vessel and where it dropped anchor.\n"
|
|
74
|
+
" The tally walks the scans in arrival order and keeps that order.\n"
|
|
75
|
+
" A calm voyage ends well for every vessel it carried.\n"
|
|
76
|
+
" A halted voyage marks the vessel it was near when the storm arrived.\n"
|
|
77
|
+
" A wrecked voyage marks the vessel that sank and stops the walk there.\n"
|
|
78
|
+
" The tally groups the vessels by their final port for the reader.\n"
|
|
79
|
+
" The reader reads the whole tally at a single glance.\n"
|
|
80
|
+
' """\n'
|
|
81
|
+
" return None\n"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _fixture_wall_with_listing() -> str:
|
|
86
|
+
return (
|
|
87
|
+
"def emit_tally() -> None:\n"
|
|
88
|
+
' """Post the voyage tally to the harbor board.\n'
|
|
89
|
+
"\n"
|
|
90
|
+
" A scan names one vessel and where it dropped anchor::\n"
|
|
91
|
+
"\n"
|
|
92
|
+
" vessel 42 -- halted -> marked mid-voyage\n"
|
|
93
|
+
" OK: a calm voyage ends well for every vessel\n"
|
|
94
|
+
" FLAG: a wrecked voyage reads the same as a calm one\n"
|
|
95
|
+
"\n"
|
|
96
|
+
" The reader reads the whole tally at a single glance.\n"
|
|
97
|
+
' """\n'
|
|
98
|
+
" return None\n"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _fixture_wall_with_doctest() -> str:
|
|
103
|
+
return (
|
|
104
|
+
"def emit_tally() -> None:\n"
|
|
105
|
+
' """Post the voyage tally to the harbor board.\n'
|
|
106
|
+
"\n"
|
|
107
|
+
" A calm voyage ends well for every vessel it carried.\n"
|
|
108
|
+
" A halted voyage marks the vessel it was near when the storm arrived.\n"
|
|
109
|
+
" A wrecked voyage marks the vessel that sank and stops the walk there.\n"
|
|
110
|
+
"\n"
|
|
111
|
+
" >>> emit_tally()\n"
|
|
112
|
+
" vessel 42 halted\n"
|
|
113
|
+
" vessel 43 calm\n"
|
|
114
|
+
"\n"
|
|
115
|
+
" The reader reads the whole tally at a single glance.\n"
|
|
116
|
+
' """\n'
|
|
117
|
+
" return None\n"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _fixture_short_summary() -> str:
|
|
122
|
+
return (
|
|
123
|
+
"def emit_tally() -> None:\n"
|
|
124
|
+
' """Post the voyage tally to the harbor board.\n'
|
|
125
|
+
"\n"
|
|
126
|
+
" A scan names one vessel and where it dropped anchor.\n"
|
|
127
|
+
" A calm voyage ends well for every vessel it carried.\n"
|
|
128
|
+
" A halted voyage marks the vessel it was near when it stopped.\n"
|
|
129
|
+
" The reader reads the whole tally at a single glance.\n"
|
|
130
|
+
' """\n'
|
|
131
|
+
" return None\n"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_should_flag_prose_wall_with_no_illustration() -> None:
|
|
136
|
+
issues = check_docstring_prose_wall_without_illustration(
|
|
137
|
+
_fixture_top_level_wall(), PRODUCTION_FILE_PATH
|
|
138
|
+
)
|
|
139
|
+
assert any("worked example" in each for each in issues), (
|
|
140
|
+
f"A top-level wall must flag, got: {issues!r}"
|
|
141
|
+
)
|
|
142
|
+
assert any("module" in each for each in issues)
|
|
143
|
+
assert len(issues) == 1
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_should_flag_function_prose_wall() -> None:
|
|
147
|
+
issues = check_docstring_prose_wall_without_illustration(
|
|
148
|
+
_fixture_callable_wall(), PRODUCTION_FILE_PATH
|
|
149
|
+
)
|
|
150
|
+
assert any("emit_tally" in each for each in issues), (
|
|
151
|
+
f"A callable wall must flag, got: {issues!r}"
|
|
152
|
+
)
|
|
153
|
+
assert len(issues) == 1
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_should_not_flag_wall_with_literal_block() -> None:
|
|
157
|
+
issues = check_docstring_prose_wall_without_illustration(
|
|
158
|
+
_fixture_wall_with_listing(), PRODUCTION_FILE_PATH
|
|
159
|
+
)
|
|
160
|
+
assert issues == [], f"A '::' listing must pass, got: {issues!r}"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_should_not_flag_wall_with_doctest() -> None:
|
|
164
|
+
issues = check_docstring_prose_wall_without_illustration(
|
|
165
|
+
_fixture_wall_with_doctest(), PRODUCTION_FILE_PATH
|
|
166
|
+
)
|
|
167
|
+
assert issues == [], f"A '>>>' doctest must pass, got: {issues!r}"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_should_not_flag_short_narrative() -> None:
|
|
171
|
+
issues = check_docstring_prose_wall_without_illustration(
|
|
172
|
+
_fixture_short_summary(), PRODUCTION_FILE_PATH
|
|
173
|
+
)
|
|
174
|
+
assert issues == [], f"A summary at the sentence limit must pass, got: {issues!r}"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_should_skip_test_file() -> None:
|
|
178
|
+
issues = check_docstring_prose_wall_without_illustration(_fixture_top_level_wall(), SUITE_FILE_PATH)
|
|
179
|
+
assert issues == [], f"A suite path stays exempt, got: {issues!r}"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_should_skip_hook_infrastructure() -> None:
|
|
183
|
+
issues = check_docstring_prose_wall_without_illustration(
|
|
184
|
+
_fixture_top_level_wall(), INFRA_PATH
|
|
185
|
+
)
|
|
186
|
+
assert issues == [], f"An infra path stays exempt, got: {issues!r}"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_should_handle_syntax_error_gracefully() -> None:
|
|
190
|
+
issues = check_docstring_prose_wall_without_illustration("def broken(\n", PRODUCTION_FILE_PATH)
|
|
191
|
+
assert issues == [], f"A syntax error yields no issues, got: {issues!r}"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_validate_content_surfaces_prose_wall() -> None:
|
|
195
|
+
issues = validate_content(_fixture_top_level_wall(), PRODUCTION_FILE_PATH, old_content="")
|
|
196
|
+
matching_issues = [each for each in issues if "worked example" in each]
|
|
197
|
+
assert matching_issues, f"validate_content must surface the wall, got: {issues!r}"
|
|
@@ -252,6 +252,33 @@ def test_should_flag_spaced_double_hyphen_joiner() -> None:
|
|
|
252
252
|
assert len(issues) == 1
|
|
253
253
|
|
|
254
254
|
|
|
255
|
+
def _diagram_first_literal_block() -> str:
|
|
256
|
+
return (
|
|
257
|
+
"def emit_tally() -> None:\n"
|
|
258
|
+
' """Post the voyage tally so the reader sees how the run ended.\n'
|
|
259
|
+
"\n"
|
|
260
|
+
" A calm voyage versus a halted one::\n"
|
|
261
|
+
"\n"
|
|
262
|
+
" vessel 42 -- halted -> marked mid-voyage while it sailed\n"
|
|
263
|
+
" calm — every vessel it carried shows a clean recorded outcome\n"
|
|
264
|
+
" OK: a calm run and a sunk run read differently in the log\n"
|
|
265
|
+
" FLAG: a sunk run looks identical to a calm finished run here\n"
|
|
266
|
+
" The reader reads the final outcome at a glance.\n"
|
|
267
|
+
' """\n'
|
|
268
|
+
" return None\n"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_should_not_flag_diagram_first_literal_block() -> None:
|
|
273
|
+
issues = check_docstring_runon_sentence(
|
|
274
|
+
_diagram_first_literal_block(), PRODUCTION_FILE_PATH
|
|
275
|
+
)
|
|
276
|
+
assert issues == [], (
|
|
277
|
+
"A '::' listing's arrow, em-dash, and OK/FLAG rows sit outside the "
|
|
278
|
+
f"narrative and must not inflate the run-on count, got: {issues!r}"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
255
282
|
def test_docstring_names_every_inspected_section_header() -> None:
|
|
256
283
|
docstring = code_rules_enforcer.check_docstring_runon_sentence.__doc__
|
|
257
284
|
assert docstring is not None
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import importlib.util
|
|
5
|
+
|
|
6
|
+
ENFORCER_PATH = Path(__file__).resolve().parent / "code_rules_enforcer.py"
|
|
7
|
+
specification = importlib.util.spec_from_file_location("code_rules_enforcer", ENFORCER_PATH)
|
|
8
|
+
code_rules_enforcer = importlib.util.module_from_spec(specification)
|
|
9
|
+
specification.loader.exec_module(code_rules_enforcer)
|
|
10
|
+
|
|
11
|
+
PRODUCTION_FILE_PATH = "packages/app/services/foo.py"
|
|
12
|
+
TEST_FILE_PATH = "packages/app/tests/test_foo.py"
|
|
13
|
+
CONFIG_FILE_PATH = "packages/app/config/constants.py"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_should_flag_literal_delimiter_join_separator_in_function_body() -> None:
|
|
17
|
+
source = (
|
|
18
|
+
"def render(all_paths: list) -> str:\n"
|
|
19
|
+
" return ', '.join(str(each_path) for each_path in all_paths)\n"
|
|
20
|
+
)
|
|
21
|
+
issues = code_rules_enforcer.check_join_separator_string_magic(source, PRODUCTION_FILE_PATH)
|
|
22
|
+
assert any("join" in each_issue for each_issue in issues), (
|
|
23
|
+
f"Expected literal join separator flagged, got: {issues}"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_should_not_flag_named_constant_join_separator() -> None:
|
|
28
|
+
source = (
|
|
29
|
+
"from config.constants import JOIN_DELIMITER\n"
|
|
30
|
+
"\n"
|
|
31
|
+
"def render(all_paths: list) -> str:\n"
|
|
32
|
+
" return JOIN_DELIMITER.join(all_paths)\n"
|
|
33
|
+
)
|
|
34
|
+
issues = code_rules_enforcer.check_join_separator_string_magic(source, PRODUCTION_FILE_PATH)
|
|
35
|
+
assert issues == [], f"Named constant separator must pass, got: {issues}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_should_not_flag_empty_string_join_concatenation() -> None:
|
|
39
|
+
source = "def concatenate(all_parts: list) -> str:\n return ''.join(all_parts)\n"
|
|
40
|
+
issues = code_rules_enforcer.check_join_separator_string_magic(source, PRODUCTION_FILE_PATH)
|
|
41
|
+
assert issues == [], (
|
|
42
|
+
f"Empty-string join concatenation is idiomatic and must pass, got: {issues}"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_should_skip_join_separator_in_test_files() -> None:
|
|
47
|
+
source = "def test_render() -> None:\n assert ', '.join(['a', 'b']) == 'a, b'\n"
|
|
48
|
+
issues = code_rules_enforcer.check_join_separator_string_magic(source, TEST_FILE_PATH)
|
|
49
|
+
assert issues == [], f"Test files exempt, got: {issues}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_should_skip_join_separator_in_config_files() -> None:
|
|
53
|
+
source = "def build_message(all_names: list) -> str:\n return ', '.join(all_names)\n"
|
|
54
|
+
issues = code_rules_enforcer.check_join_separator_string_magic(source, CONFIG_FILE_PATH)
|
|
55
|
+
assert issues == [], f"Config files exempt, got: {issues}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_should_report_join_separator_line_number() -> None:
|
|
59
|
+
source = (
|
|
60
|
+
"def render(all_paths: list) -> str:\n"
|
|
61
|
+
" joined = '; '.join(all_paths)\n"
|
|
62
|
+
" return joined\n"
|
|
63
|
+
)
|
|
64
|
+
issues = code_rules_enforcer.check_join_separator_string_magic(source, PRODUCTION_FILE_PATH)
|
|
65
|
+
assert any("Line 2" in each_issue for each_issue in issues), (
|
|
66
|
+
f"Expected line 2 reported, got: {issues}"
|
|
67
|
+
)
|