claude-dev-env 1.83.0 → 1.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_shared/pr-loop/audit-contract.md +24 -12
- 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 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +8 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +85 -39
- 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_reviews_disabled.py +14 -0
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +94 -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 +0 -4
- package/hooks/blocking/claude_md_orphan_file_blocker.py +1 -1
- package/hooks/blocking/code_rules_constants_config.py +5 -0
- package/hooks/blocking/code_rules_docstrings.py +133 -19
- package/hooks/blocking/code_rules_enforcer.py +4 -0
- package/hooks/blocking/code_rules_imports_logging.py +10 -6
- package/hooks/blocking/code_rules_magic_values.py +5 -0
- package/hooks/blocking/code_rules_naming_collection.py +15 -8
- package/hooks/blocking/code_rules_paired_test.py +3 -2
- package/hooks/blocking/code_rules_shared.py +34 -0
- package/hooks/blocking/code_rules_string_magic.py +1 -1
- package/hooks/blocking/code_rules_unused_imports.py +2 -2
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +29 -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_naive_datetime.py +24 -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_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_shared_stdin_adoption.py +5 -30
- package/hooks/hooks_constants/CLAUDE.md +0 -3
- package/hooks/hooks_constants/blocking_check_limits.py +4 -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/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 +1 -1
- package/rules/docstring-prose-matches-implementation.md +1 -1
- package/rules/env-var-table-code-drift.md +1 -1
- package/rules/package-inventory-stale-entry.md +1 -1
- package/rules/paired-test-coverage.md +1 -1
- package/rules/plain-illustrative-docstrings.md +35 -1
- package/skills/CLAUDE.md +6 -1
- package/skills/_shared/pr-loop/CLAUDE.md +1 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +45 -0
- package/skills/_shared/pr-loop/scripts/test_write_handoff.py +201 -0
- package/skills/_shared/pr-loop/scripts/write_handoff.py +309 -0
- package/skills/autoconverge/SKILL.md +82 -58
- 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 +87 -13
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
- package/skills/autoconverge/workflow/converge.mjs +71 -74
- 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/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 +99 -60
- 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 +27 -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
|
@@ -53,6 +53,8 @@ from hooks_constants.blocking_check_limits import ( # noqa: E402
|
|
|
53
53
|
ALL_ZIPFILE_WRITE_MODE_VALUES,
|
|
54
54
|
DOCSTRING_FALLBACK_BRANCH_MINIMUM_ROUTE_COUNT,
|
|
55
55
|
DOCSTRING_LARGE_ZIP_FILE_EXCEPTION_NAME,
|
|
56
|
+
DOCSTRING_NARRATIVE_LINE_JOIN_SEPARATOR,
|
|
57
|
+
DOCSTRING_NARRATIVE_PROSE_LINE_LIMIT,
|
|
56
58
|
DOCSTRING_REFERENCE_MARKER_WINDOW,
|
|
57
59
|
DOCSTRING_RUNON_SENTENCE_BOUNDARY_PATTERN,
|
|
58
60
|
DOCSTRING_RUNON_SENTENCE_WORD_LIMIT,
|
|
@@ -72,6 +74,7 @@ from hooks_constants.blocking_check_limits import ( # noqa: E402
|
|
|
72
74
|
MAX_DOCSTRING_MARK_GLYPH_ENUMERATION_ISSUES,
|
|
73
75
|
MAX_DOCSTRING_NO_CONSUMER_CLAIM_ISSUES,
|
|
74
76
|
MAX_DOCSTRING_NO_NETWORK_CLAIM_ISSUES,
|
|
77
|
+
MAX_DOCSTRING_PROSE_WALL_ISSUES,
|
|
75
78
|
MAX_DOCSTRING_RAISES_LARGEZIPFILE_ISSUES,
|
|
76
79
|
MAX_DOCSTRING_RETURNS_PLURAL_CARDINALITY_ISSUES,
|
|
77
80
|
MAX_DOCSTRING_RUNON_SENTENCE_ISSUES,
|
|
@@ -90,6 +93,7 @@ from hooks_constants.blocking_check_limits import ( # noqa: E402
|
|
|
90
93
|
MINIMUM_NAMED_MARKS_FOR_PROSE_ENUMERATION,
|
|
91
94
|
MINIMUM_PUBLIC_CHECKS_FOR_MODULE_DOCSTRING_ROSTER,
|
|
92
95
|
MINIMUM_PUBLIC_METHODS_FOR_CLASS_DOCSTRING_BREADTH,
|
|
96
|
+
MINIMUM_RANGE_ARGS_FOR_SPAN,
|
|
93
97
|
MINIMUM_SIBLING_OCCURRENCES_FOR_SHARED_TOKEN,
|
|
94
98
|
MINIMUM_TOKENS_FOR_DISPATCH_CALLEE,
|
|
95
99
|
MINIMUM_TUPLE_MEMBERS_FOR_DOCSTRING_ENUMERATION,
|
|
@@ -2016,13 +2020,14 @@ def _documentable_docstrings_with_line(parsed_tree: ast.Module) -> list[tuple[in
|
|
|
2016
2020
|
def check_docstring_cardinal_count_matches_constant_family(
|
|
2017
2021
|
content: str, file_path: str
|
|
2018
2022
|
) -> list[str]:
|
|
2019
|
-
"""Flag a docstring cardinal count that
|
|
2023
|
+
"""Flag a docstring cardinal count that differs from a referenced constant family.
|
|
2020
2024
|
|
|
2021
2025
|
The drift this catches: a docstring states a cardinal count of an outcome
|
|
2022
2026
|
family (``Covers the four outcome branches: ...``) and enumerates that
|
|
2023
|
-
family's members in prose, while the module references
|
|
2024
|
-
same ``UPPER_SNAKE`` constant family
|
|
2025
|
-
|
|
2027
|
+
family's members in prose, while the module references a member set of the
|
|
2028
|
+
same ``UPPER_SNAKE`` constant family whose size differs from the stated
|
|
2029
|
+
count — above it and below it both trip. The prose
|
|
2030
|
+
misdescribes the code — a reader trusts the count and the enumeration to
|
|
2026
2031
|
be the full set, but the module imports and exercises an outcome the summary
|
|
2027
2032
|
omits. The check binds only when the docstring states a cardinal beside an
|
|
2028
2033
|
outcome noun, names two or more members of one referenced family, leaves at
|
|
@@ -2035,7 +2040,8 @@ def check_docstring_cardinal_count_matches_constant_family(
|
|
|
2035
2040
|
|
|
2036
2041
|
Args:
|
|
2037
2042
|
content: The source text to inspect.
|
|
2038
|
-
file_path:
|
|
2043
|
+
file_path: Accepted for the uniform check interface; the body does not
|
|
2044
|
+
read it.
|
|
2039
2045
|
|
|
2040
2046
|
Returns:
|
|
2041
2047
|
One issue per docstring whose cardinal count and enumeration omit a
|
|
@@ -2470,12 +2476,12 @@ def _argument_prose_scopes_a_single_line(argument_prose: str) -> bool:
|
|
|
2470
2476
|
)
|
|
2471
2477
|
|
|
2472
2478
|
|
|
2473
|
-
def
|
|
2479
|
+
def _call_node_builds_explicit_start_range(call_node: ast.Call) -> bool:
|
|
2474
2480
|
callee = call_node.func
|
|
2475
2481
|
return (
|
|
2476
2482
|
isinstance(callee, ast.Name)
|
|
2477
2483
|
and callee.id == "range"
|
|
2478
|
-
and len(call_node.args) >=
|
|
2484
|
+
and len(call_node.args) >= MINIMUM_RANGE_ARGS_FOR_SPAN
|
|
2479
2485
|
)
|
|
2480
2486
|
|
|
2481
2487
|
|
|
@@ -2487,7 +2493,7 @@ def _function_body_scopes_a_span_by_intersection(
|
|
|
2487
2493
|
for each_node in ast.walk(function_node):
|
|
2488
2494
|
if not isinstance(each_node, ast.Call):
|
|
2489
2495
|
continue
|
|
2490
|
-
if
|
|
2496
|
+
if _call_node_builds_explicit_start_range(each_node):
|
|
2491
2497
|
builds_range_span = True
|
|
2492
2498
|
if _call_callee_name(each_node) in ALL_DOCSTRING_SPAN_RANGE_BODY_CALLEE_NAMES:
|
|
2493
2499
|
calls_span_scoper = True
|
|
@@ -2507,11 +2513,12 @@ def check_docstring_args_single_line_scope_vs_span(content: str, file_path: str)
|
|
|
2507
2513
|
is the deterministic slice of Category O6 docstring-vs-implementation drift
|
|
2508
2514
|
for an Args single-line scope claim disagreeing with a span-intersection body.
|
|
2509
2515
|
|
|
2510
|
-
An entry is left alone when its prose says "any line of" / "any line in"
|
|
2511
|
-
|
|
2512
|
-
span
|
|
2513
|
-
|
|
2514
|
-
|
|
2516
|
+
An entry is left alone when its prose says "any line of" / "any line in" /
|
|
2517
|
+
"any of its lines" / "any span line" about its span, since that wording
|
|
2518
|
+
matches the span body. The body is judged a span-intersection scoper only
|
|
2519
|
+
when it both builds an explicit-start ``range(...)`` (two or more
|
|
2520
|
+
arguments) and calls a known span scoper, so a body that scopes by a
|
|
2521
|
+
single line never trips the check. Hook infrastructure is in scope here — the import-sort gate
|
|
2515
2522
|
that carries this drift class is itself a hook — and test files are exempt.
|
|
2516
2523
|
|
|
2517
2524
|
Args:
|
|
@@ -2562,14 +2569,71 @@ def _is_narrative_cut_header(stripped_line: str) -> bool:
|
|
|
2562
2569
|
)
|
|
2563
2570
|
|
|
2564
2571
|
|
|
2565
|
-
def
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2572
|
+
def _docstring_narrative_partition(docstring_text: str) -> tuple[list[str], bool]:
|
|
2573
|
+
"""Split a docstring narrative into its prose lines and whether it shows an example.
|
|
2574
|
+
|
|
2575
|
+
Walk the narrative up to the first ``Args:``/``Returns:`` cut header and sort
|
|
2576
|
+
every line into one of two buckets::
|
|
2577
|
+
|
|
2578
|
+
Render the report so the reader sees how it ended. <- prose line
|
|
2579
|
+
A finished run versus an interrupted one:: <- prose line (opener)
|
|
2580
|
+
theme_42 -- interrupted -> marked in-flight -- indented example body
|
|
2581
|
+
OK: a clean run reads differently in the log -- indented example body
|
|
2582
|
+
The reader reads the final outcome at a glance. <- prose line
|
|
2583
|
+
|
|
2584
|
+
A ``>>>`` doctest region and a ``::`` block with an indented body both count as
|
|
2585
|
+
an illustration, so their lines never join the prose bucket. The caller reads
|
|
2586
|
+
the prose count to size the wall and the flag to know whether an example is present.
|
|
2587
|
+
"""
|
|
2588
|
+
prose_lines: list[str] = []
|
|
2589
|
+
has_illustration = False
|
|
2590
|
+
all_lines = docstring_text.splitlines()
|
|
2591
|
+
total_lines = len(all_lines)
|
|
2592
|
+
line_index = 0
|
|
2593
|
+
while line_index < total_lines:
|
|
2594
|
+
raw_line = all_lines[line_index]
|
|
2595
|
+
stripped_line = raw_line.strip()
|
|
2569
2596
|
if _is_narrative_cut_header(stripped_line):
|
|
2570
2597
|
break
|
|
2571
|
-
|
|
2572
|
-
|
|
2598
|
+
if stripped_line.startswith(">>>"):
|
|
2599
|
+
has_illustration = True
|
|
2600
|
+
line_index += 1
|
|
2601
|
+
while line_index < total_lines:
|
|
2602
|
+
inner_stripped = all_lines[line_index].strip()
|
|
2603
|
+
if not inner_stripped or _is_narrative_cut_header(inner_stripped):
|
|
2604
|
+
break
|
|
2605
|
+
line_index += 1
|
|
2606
|
+
continue
|
|
2607
|
+
if stripped_line.endswith("::"):
|
|
2608
|
+
opener_indent = len(raw_line) - len(raw_line.lstrip())
|
|
2609
|
+
probe_index = line_index + 1
|
|
2610
|
+
has_indented_body = False
|
|
2611
|
+
while probe_index < total_lines:
|
|
2612
|
+
probe_raw = all_lines[probe_index]
|
|
2613
|
+
probe_stripped = probe_raw.strip()
|
|
2614
|
+
if not probe_stripped:
|
|
2615
|
+
probe_index += 1
|
|
2616
|
+
continue
|
|
2617
|
+
probe_indent = len(probe_raw) - len(probe_raw.lstrip())
|
|
2618
|
+
if probe_indent > opener_indent:
|
|
2619
|
+
has_indented_body = True
|
|
2620
|
+
probe_index += 1
|
|
2621
|
+
continue
|
|
2622
|
+
break
|
|
2623
|
+
if has_indented_body:
|
|
2624
|
+
has_illustration = True
|
|
2625
|
+
prose_lines.append(stripped_line)
|
|
2626
|
+
line_index = probe_index
|
|
2627
|
+
continue
|
|
2628
|
+
if stripped_line:
|
|
2629
|
+
prose_lines.append(stripped_line)
|
|
2630
|
+
line_index += 1
|
|
2631
|
+
return prose_lines, has_illustration
|
|
2632
|
+
|
|
2633
|
+
|
|
2634
|
+
def _docstring_narrative_text(docstring_text: str) -> str:
|
|
2635
|
+
prose_lines, _has_illustration = _docstring_narrative_partition(docstring_text)
|
|
2636
|
+
return DOCSTRING_NARRATIVE_LINE_JOIN_SEPARATOR.join(prose_lines)
|
|
2573
2637
|
|
|
2574
2638
|
|
|
2575
2639
|
def _sentence_word_count(sentence_text: str) -> int:
|
|
@@ -2675,6 +2739,56 @@ def check_docstring_runon_sentence(content: str, file_path: str) -> list[str]:
|
|
|
2675
2739
|
return issues[:MAX_DOCSTRING_RUNON_SENTENCE_ISSUES]
|
|
2676
2740
|
|
|
2677
2741
|
|
|
2742
|
+
def check_docstring_prose_wall_without_illustration(content: str, file_path: str) -> list[str]:
|
|
2743
|
+
"""Flag a summary that tells for many sentences and shows nothing.
|
|
2744
|
+
|
|
2745
|
+
A reader trusts the opening to paint a scene. A run of short sentences with
|
|
2746
|
+
no worked example leaves the reader piecing that scene together alone::
|
|
2747
|
+
|
|
2748
|
+
A calm voyage ends well for every vessel. <- one more line
|
|
2749
|
+
A halted voyage marks the vessel it neared. <- and another
|
|
2750
|
+
... more like these, no worked example ... flag: a wall, no scene
|
|
2751
|
+
A calm voyage versus a halted one:: ok: a worked example
|
|
2752
|
+
a lone vessel -- halted -> marked mid-voyage
|
|
2753
|
+
|
|
2754
|
+
Past the prose-line limit with no ``::`` listing and no ``>>>`` doctest, this
|
|
2755
|
+
fires. A narrative that shows a worked example, or one at the limit, passes.
|
|
2756
|
+
|
|
2757
|
+
Args:
|
|
2758
|
+
content: The source text to inspect.
|
|
2759
|
+
file_path: The path the source will be written to, used for exemptions.
|
|
2760
|
+
|
|
2761
|
+
Returns:
|
|
2762
|
+
One issue per summary that runs a wall of sentences with no worked
|
|
2763
|
+
example, capped at the issue limit for the rule.
|
|
2764
|
+
"""
|
|
2765
|
+
if is_test_file(file_path) or is_hook_infrastructure(file_path):
|
|
2766
|
+
return []
|
|
2767
|
+
try:
|
|
2768
|
+
parsed_tree = ast.parse(content)
|
|
2769
|
+
except SyntaxError:
|
|
2770
|
+
return []
|
|
2771
|
+
issues: list[str] = []
|
|
2772
|
+
for each_line_number, each_label, each_docstring in _documentable_docstring_targets(
|
|
2773
|
+
parsed_tree
|
|
2774
|
+
):
|
|
2775
|
+
prose_lines, has_illustration = _docstring_narrative_partition(each_docstring)
|
|
2776
|
+
if has_illustration:
|
|
2777
|
+
continue
|
|
2778
|
+
prose_line_count = len(prose_lines)
|
|
2779
|
+
if prose_line_count <= DOCSTRING_NARRATIVE_PROSE_LINE_LIMIT:
|
|
2780
|
+
continue
|
|
2781
|
+
issues.append(
|
|
2782
|
+
f"Line {each_line_number}: {each_label} summary runs {prose_line_count} "
|
|
2783
|
+
"narrative lines with no worked example - show, don't tell: swap the wall for a "
|
|
2784
|
+
"'::' listing (a sample input, an annotated outcome, ok/flag contrast rows) and "
|
|
2785
|
+
"keep the narrative to a few short lines (plain-illustrative-docstrings)"
|
|
2786
|
+
)
|
|
2787
|
+
if len(issues) >= MAX_DOCSTRING_PROSE_WALL_ISSUES:
|
|
2788
|
+
break
|
|
2789
|
+
return issues[:MAX_DOCSTRING_PROSE_WALL_ISSUES]
|
|
2790
|
+
|
|
2791
|
+
|
|
2678
2792
|
def _raises_section_text(docstring_text: str) -> str:
|
|
2679
2793
|
docstring_lines = docstring_text.splitlines()
|
|
2680
2794
|
raises_section_lines: list[str] = []
|
|
@@ -87,6 +87,7 @@ from code_rules_docstrings import ( # noqa: E402
|
|
|
87
87
|
check_docstring_no_consumer_claim,
|
|
88
88
|
check_docstring_no_inline_literal_claim,
|
|
89
89
|
check_docstring_no_network_claim_with_metadata_access,
|
|
90
|
+
check_docstring_prose_wall_without_illustration,
|
|
90
91
|
check_docstring_punctuation_mark_enumeration_coverage,
|
|
91
92
|
check_docstring_raises_unraisable_largezipfile,
|
|
92
93
|
check_docstring_returns_plural_cardinality,
|
|
@@ -351,6 +352,9 @@ def validate_content(
|
|
|
351
352
|
check_class_docstring_names_public_methods(effective_content, file_path)
|
|
352
353
|
)
|
|
353
354
|
all_issues.extend(check_docstring_runon_sentence(effective_content, file_path))
|
|
355
|
+
all_issues.extend(
|
|
356
|
+
check_docstring_prose_wall_without_illustration(effective_content, file_path)
|
|
357
|
+
)
|
|
354
358
|
all_issues.extend(
|
|
355
359
|
check_module_docstring_names_public_checks(effective_content, file_path)
|
|
356
360
|
)
|
|
@@ -244,7 +244,8 @@ def check_import_block_sorted(
|
|
|
244
244
|
give ruff the filename it needs for config discovery.
|
|
245
245
|
all_changed_lines: Post-edit line numbers the current edit touched, or
|
|
246
246
|
None to treat the whole file as in scope. When provided, a finding
|
|
247
|
-
blocks only when its block
|
|
247
|
+
blocks only when any line of its block span is among the changed
|
|
248
|
+
lines.
|
|
248
249
|
defer_scope_to_caller: When True, return every finding so the commit/push
|
|
249
250
|
gate's ``Line N:`` partitioning scopes by added line.
|
|
250
251
|
|
|
@@ -813,9 +814,9 @@ def _naive_datetime_remediation(constructor_name: str) -> str:
|
|
|
813
814
|
"""Return the fix guidance for a specific naive datetime constructor.
|
|
814
815
|
|
|
815
816
|
``utcnow`` and ``utcfromtimestamp`` accept no ``tz`` argument, so each points
|
|
816
|
-
at its aware replacement (``
|
|
817
|
-
|
|
818
|
-
only needs a ``tz=`` argument.
|
|
817
|
+
at its aware replacement (``now(tz=...)`` and ``fromtimestamp(..., tz=...)``
|
|
818
|
+
on the same datetime class, whichever import form the call used);
|
|
819
|
+
``fromtimestamp`` keeps its shape and only needs a ``tz=`` argument.
|
|
819
820
|
|
|
820
821
|
Args:
|
|
821
822
|
constructor_name: The naive constructor the call used.
|
|
@@ -824,9 +825,12 @@ def _naive_datetime_remediation(constructor_name: str) -> str:
|
|
|
824
825
|
The remediation clause to embed in the emitted issue.
|
|
825
826
|
"""
|
|
826
827
|
if constructor_name == NAIVE_DATETIME_UTCNOW_CONSTRUCTOR:
|
|
827
|
-
return "
|
|
828
|
+
return "replace utcnow() with now(tz=timezone.utc) on the same datetime class"
|
|
828
829
|
if constructor_name == NAIVE_DATETIME_UTCFROMTIMESTAMP_CONSTRUCTOR:
|
|
829
|
-
return
|
|
830
|
+
return (
|
|
831
|
+
"replace utcfromtimestamp(...) with fromtimestamp(..., tz=timezone.utc) "
|
|
832
|
+
"on the same datetime class"
|
|
833
|
+
)
|
|
830
834
|
return "pass tz= (e.g. tz=timezone.utc)"
|
|
831
835
|
|
|
832
836
|
|
|
@@ -17,6 +17,7 @@ from code_rules_path_utils import ( # noqa: E402
|
|
|
17
17
|
)
|
|
18
18
|
from code_rules_shared import ( # noqa: E402
|
|
19
19
|
_extract_fstring_literal_parts,
|
|
20
|
+
docstring_line_numbers,
|
|
20
21
|
is_test_file,
|
|
21
22
|
)
|
|
22
23
|
|
|
@@ -156,6 +157,7 @@ def check_magic_values(content: str, file_path: str) -> list[str]:
|
|
|
156
157
|
|
|
157
158
|
issues = []
|
|
158
159
|
lines = content.split("\n")
|
|
160
|
+
all_docstring_line_numbers = docstring_line_numbers(content)
|
|
159
161
|
is_inside_function = False
|
|
160
162
|
|
|
161
163
|
number_pattern = re.compile(r"(?<![.\w])(\d+\.?\d*)(?![.\w])")
|
|
@@ -167,6 +169,9 @@ def check_magic_values(content: str, file_path: str) -> list[str]:
|
|
|
167
169
|
if not stripped:
|
|
168
170
|
continue
|
|
169
171
|
|
|
172
|
+
if each_line_number in all_docstring_line_numbers:
|
|
173
|
+
continue
|
|
174
|
+
|
|
170
175
|
if re.match(r"^(async\s+)?def\s+\w+", stripped):
|
|
171
176
|
is_inside_function = True
|
|
172
177
|
continue
|
|
@@ -434,14 +434,21 @@ def _contradicting_polarity_pair(target_name: str, called_name: str) -> tuple[st
|
|
|
434
434
|
|
|
435
435
|
|
|
436
436
|
def check_polarity_name_contradiction(content: str, file_path: str) -> list[str]:
|
|
437
|
-
"""Flag
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
437
|
+
"""Flag an assignment whose target and callee assert opposite polarity.
|
|
438
|
+
|
|
439
|
+
::
|
|
440
|
+
|
|
441
|
+
is_inside_allowed = _point_hits_any_forbidden(...)
|
|
442
|
+
^^^^^^^ ^^^^^^^^^
|
|
443
|
+
allowed vs. forbidden ⚠ the two names clash
|
|
444
|
+
ok: is_inside_allowed = _point_inside_allowed_region(...)
|
|
445
|
+
flag: is_inside_allowed = _point_hits_any_forbidden(...)
|
|
446
|
+
|
|
447
|
+
The target token and the callee token contradict each other, so the reader
|
|
448
|
+
cannot tell which name states the truth. Any single-name assignment from a
|
|
449
|
+
call is inspected — collections and counts included, not only booleans.
|
|
450
|
+
Rename the callee to a neutral form the two names agree on at every call
|
|
451
|
+
site.
|
|
445
452
|
"""
|
|
446
453
|
if is_test_file(file_path):
|
|
447
454
|
return []
|
|
@@ -164,8 +164,9 @@ def _stem_matched_test_path(module_path: Path) -> Path | None:
|
|
|
164
164
|
|
|
165
165
|
Returns:
|
|
166
166
|
The path of the first existing ``test_<stem>.py`` or ``<stem>_test.py``
|
|
167
|
-
file
|
|
168
|
-
None when the module has no dedicated test
|
|
167
|
+
file beside the module, or ``test_<stem>.py`` under an ancestor
|
|
168
|
+
``tests`` directory — or None when the module has no dedicated test
|
|
169
|
+
file.
|
|
169
170
|
"""
|
|
170
171
|
module_directory = module_path.parent
|
|
171
172
|
module_stem = module_path.stem
|
|
@@ -273,6 +273,40 @@ def is_migration_file(file_path: str) -> bool:
|
|
|
273
273
|
return any(pattern.replace("\\", "/") in path_lower for pattern in ALL_MIGRATION_PATH_PATTERNS)
|
|
274
274
|
|
|
275
275
|
|
|
276
|
+
def docstring_line_numbers(content: str) -> set[int]:
|
|
277
|
+
"""Return every source line that sits inside a string-literal statement.
|
|
278
|
+
|
|
279
|
+
A diagram-first docstring often draws its point across several lines, and
|
|
280
|
+
those lines can hold a bare number (a row marker) or a contrast row
|
|
281
|
+
(``flag: is_ok = do_thing(...)``). Those are prose, not code, so a
|
|
282
|
+
line-based lint check that reads them as literals fires a false positive.
|
|
283
|
+
This walks the parsed source and collects the line numbers spanned by every
|
|
284
|
+
string-literal expression statement — module, class, and function
|
|
285
|
+
docstrings, plus any bare string statement — so those checks can skip them.
|
|
286
|
+
Unparseable source yields an empty set.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
content: The Python source to scan.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
The 1-indexed line numbers that fall inside a string-literal statement.
|
|
293
|
+
"""
|
|
294
|
+
try:
|
|
295
|
+
parsed_tree = ast.parse(content)
|
|
296
|
+
except SyntaxError:
|
|
297
|
+
return set()
|
|
298
|
+
all_docstring_line_numbers: set[int] = set()
|
|
299
|
+
for each_node in ast.walk(parsed_tree):
|
|
300
|
+
if not (isinstance(each_node, ast.Expr) and _statement_is_docstring(each_node)):
|
|
301
|
+
continue
|
|
302
|
+
end_line_number = each_node.end_lineno
|
|
303
|
+
if end_line_number is None:
|
|
304
|
+
continue
|
|
305
|
+
for each_line_number in range(each_node.lineno, end_line_number + 1):
|
|
306
|
+
all_docstring_line_numbers.add(each_line_number)
|
|
307
|
+
return all_docstring_line_numbers
|
|
308
|
+
|
|
309
|
+
|
|
276
310
|
def _build_parent_map(module_tree: ast.Module) -> dict[int, ast.AST]:
|
|
277
311
|
"""Map child node id() to its parent node for ancestor walking."""
|
|
278
312
|
parent_by_child_id: dict[int, ast.AST] = {}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Bare string-literal magic, inline literal-collection, inline tuple string-magic, and whitespace-indentation magic checks."""
|
|
1
|
+
"""Bare string-literal magic, join-separator string magic, inline literal-collection, inline tuple string-magic, and whitespace-indentation magic checks."""
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
4
|
import re
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Unused module-level import check and its import-range
|
|
1
|
+
"""Unused module-level import check and its import-range helpers."""
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
4
|
import sys
|
|
@@ -143,7 +143,7 @@ def check_unused_module_level_imports(
|
|
|
143
143
|
|
|
144
144
|
When ``full_file_content`` is provided, ``content`` is treated as an Edit
|
|
145
145
|
fragment containing the imports being added or replaced, while the
|
|
146
|
-
``__all__``
|
|
146
|
+
``__all__`` gate detection and reference scanning run
|
|
147
147
|
against ``full_file_content`` (the post-edit file as it will look once the
|
|
148
148
|
Edit applies). This prevents false-positive flags on imports added in the
|
|
149
149
|
same Edit as their consumers.
|
|
@@ -72,10 +72,10 @@ def path_is_exempt(file_path: str) -> bool:
|
|
|
72
72
|
Returns:
|
|
73
73
|
True when the path's basename is the canonical shared-helper home, an
|
|
74
74
|
rmtree-blocker hook source, one of the existing in-repo definition sites
|
|
75
|
-
(session_env_cleanup.py,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
(session_env_cleanup.py, teardown_worktrees.py), or a test file. A
|
|
76
|
+
definition there is intentional. Basename equality (not substring
|
|
77
|
+
containment) prevents a sibling whose name merely contains an exempt
|
|
78
|
+
fragment from bypassing the block.
|
|
79
79
|
"""
|
|
80
80
|
normalized_path = file_path.replace("\\", "/")
|
|
81
81
|
file_name = normalized_path.rsplit("/", 1)[-1]
|
|
@@ -75,6 +75,35 @@ def test_flags_anchor_line_scope_against_span_intersection_body() -> None:
|
|
|
75
75
|
assert "all_changed_lines" in issues[0]
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def test_flags_single_line_scope_against_stepped_range_span_body() -> None:
|
|
79
|
+
content = (
|
|
80
|
+
"def check_import_block_sorted(\n"
|
|
81
|
+
" content: str, file_path: str, all_changed_lines: set[int] | None\n"
|
|
82
|
+
") -> list[str]:\n"
|
|
83
|
+
' """Flag an unsorted import block scoped to the changed lines.\n'
|
|
84
|
+
"\n"
|
|
85
|
+
" Args:\n"
|
|
86
|
+
" content: The full file content the write would leave on disk.\n"
|
|
87
|
+
" file_path: The destination path used to gate by extension.\n"
|
|
88
|
+
" all_changed_lines: Post-edit line numbers, or None. When provided,\n"
|
|
89
|
+
" a finding blocks only when its block-anchor line is among the\n"
|
|
90
|
+
" changed lines.\n"
|
|
91
|
+
"\n"
|
|
92
|
+
" Returns:\n"
|
|
93
|
+
" One issue string per detected finding.\n"
|
|
94
|
+
' """\n'
|
|
95
|
+
" span_range = range(line_number, block_end_line_number + 1, 1)\n"
|
|
96
|
+
" all_violations.append((span_range, message))\n"
|
|
97
|
+
" return _scope_violations_to_changed_lines(\n"
|
|
98
|
+
" all_violations, all_changed_lines, defer_scope_to_caller\n"
|
|
99
|
+
" )\n"
|
|
100
|
+
)
|
|
101
|
+
issues = check_docstring_args_single_line_scope_vs_span(content, PRODUCTION_FILE_PATH)
|
|
102
|
+
assert len(issues) == 1
|
|
103
|
+
assert "check_import_block_sorted" in issues[0]
|
|
104
|
+
assert "all_changed_lines" in issues[0]
|
|
105
|
+
|
|
106
|
+
|
|
78
107
|
def test_flags_the_line_is_among_phrasing() -> None:
|
|
79
108
|
content = (
|
|
80
109
|
"def check_block(\n"
|