claude-dev-env 2.32.0 → 2.33.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/.agents/agents/clean-coder.md +8 -7
- package/.agents/agents/code-quality-agent.md +4 -2
- package/.agents/agents/pr-description-writer.md +8 -3
- package/.agents/agents/reference/pr-description-illustrative-voice.md +6 -1
- package/.agents/agents/test_agent_frontmatter.py +0 -9
- package/.agents/agents/test_pr_description_writer_style.py +24 -1
- package/.agents/skills/e-simplify/SKILL.md +1 -15
- package/.agents/skills/grok-spawn/reference/worker-briefs.md +4 -0
- package/.agents/skills/pr-refinement/SKILL.md +2 -2
- package/.agents/skills/pr-shared-extraction/SKILL.md +2 -2
- package/.agents/skills/pr-title-description/SKILL.md +6 -2
- package/.agents/skills/review-tier/SKILL.md +1 -1
- package/.agents/skills/review-tier/reference/tier-policy.json +2 -2
- package/.agents/skills/review-tier/scripts/review_tier.py +31 -4
- package/.agents/skills/review-tier/scripts/test_generation.py +7 -19
- package/.agents/skills/review-tier/scripts/test_review_tier.py +11 -11
- package/AGENTS.md +83 -0
- package/_shared/pr-loop/code-rules-gate.md +5 -2
- package/_shared/pr-loop/scripts/code_rules_gate.py +7 -3
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +13 -1
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +7 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +47 -1
- package/audit-rubrics/audit-categories.json +2 -2
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -1
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +5 -3
- package/audit-rubrics/category_rubrics/category-l-behavior-equivalence.md +6 -3
- package/audit-rubrics/prompts/category-e-dead-code.md +6 -6
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +25 -19
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +28 -25
- package/docs/CODE_RULES.md +7 -8
- package/hooks/blocking/code_rules_comments.py +118 -59
- package/hooks/blocking/code_rules_enforcer.py +19 -7
- package/hooks/blocking/code_rules_type_escape.py +16 -23
- package/hooks/blocking/javascript_comment_scanner.py +99 -0
- package/hooks/blocking/test_code_rules_enforcer_any_type_ignore.py +24 -4
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +190 -3
- package/hooks/blocking/test_code_rules_enforcer_javascript_comments.py +44 -0
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +0 -1
- package/hooks/git-hooks/pull_request_handoff.py +1 -1
- package/hooks/git-hooks/test_post_commit.py +8 -0
- package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +3 -4
- package/hooks/hooks_constants/test_unused_module_import_constants.py +9 -1
- package/hooks/hooks_constants/unused_module_import_constants.py +1 -1
- package/hooks/validators/comment_checks.py +2 -2
- package/hooks/validators/fast_save_validators.py +0 -9
- package/hooks/validators/git_checks.py +0 -84
- package/hooks/validators/run_all_validators.py +2 -9
- package/hooks/validators/test_fast_save_validators.py +2 -8
- package/hooks/validators/test_git_checks.py +0 -249
- package/hooks/validators/test_todo_checks.py +1 -0
- package/hooks/validators/todo_checks.py +1 -1
- package/package.json +1 -1
- package/rules/git-workflow.md +0 -8
- package/scripts/check.ps1 +35 -1
- package/scripts/codex_compat_materializer.py +1 -0
- package/scripts/tests/test_rules.py +111 -0
- package/system-prompts/software-engineer.xml +6 -19
- package/.agents/skills-archived/split-pr/SKILL.md +0 -85
- package/.agents/skills-archived/split-pr/reference/path-layers.md +0 -16
- package/.agents/skills-archived/split-pr/reference/proposal-format.md +0 -15
- package/.agents/skills-archived/split-pr/reference/split-further-loop.md +0 -10
- package/.agents/skills-archived/split-pr/reference/splitting-principles.md +0 -26
- package/.agents/skills-archived/split-pr/scripts/analyze_pr.py +0 -279
- package/.agents/skills-archived/split-pr/scripts/categorize_files.py +0 -106
- package/.agents/skills-archived/split-pr/scripts/config/__init__.py +0 -1
- package/.agents/skills-archived/split-pr/scripts/config/dependency_constants.py +0 -14
- package/.agents/skills-archived/split-pr/scripts/config/git_operations_constants.py +0 -36
- package/.agents/skills-archived/split-pr/scripts/config/packing_constants.py +0 -61
- package/.agents/skills-archived/split-pr/scripts/config/plan_constants.py +0 -49
- package/.agents/skills-archived/split-pr/scripts/config/split_pr_constants.py +0 -110
- package/.agents/skills-archived/split-pr/scripts/execute_split_slices.py +0 -82
- package/.agents/skills-archived/split-pr/scripts/pack_files_into_slices.py +0 -212
- package/.agents/skills-archived/split-pr/scripts/split_pr_dependency_graph.py +0 -70
- package/.agents/skills-archived/split-pr/scripts/split_pr_git_operations.py +0 -184
- package/.agents/skills-archived/split-pr/scripts/split_pr_layer_order.py +0 -58
- package/.agents/skills-archived/split-pr/scripts/split_pr_paginate.py +0 -119
- package/.agents/skills-archived/split-pr/scripts/split_pr_process_runner.py +0 -52
- package/.agents/skills-archived/split-pr/scripts/split_pr_script_types.py +0 -126
- package/.agents/skills-archived/split-pr/scripts/split_pr_title.py +0 -41
- package/.agents/skills-archived/split-pr/scripts/test_analyze_pr.py +0 -228
- package/.agents/skills-archived/split-pr/scripts/test_categorize_files.py +0 -55
- package/.agents/skills-archived/split-pr/scripts/test_categorize_files_packing.py +0 -59
- package/.agents/skills-archived/split-pr/scripts/test_execute_split_slices.py +0 -99
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_dependency_graph.py +0 -47
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_git_operations.py +0 -125
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_layer_order.py +0 -36
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_paginate.py +0 -65
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_script_types.py +0 -73
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_title.py +0 -28
- package/.agents/skills-archived/split-pr/scripts/test_verify_dependency_graph.py +0 -46
- package/.agents/skills-archived/split-pr/scripts/test_verify_plan.py +0 -56
- package/.agents/skills-archived/split-pr/scripts/test_verify_plan_contract.py +0 -50
- package/.agents/skills-archived/split-pr/scripts/test_verify_plan_path_normalization.py +0 -45
- package/.agents/skills-archived/split-pr/scripts/verify_dependency_graph.py +0 -111
- package/.agents/skills-archived/split-pr/scripts/verify_plan.py +0 -139
|
@@ -14,6 +14,7 @@ a string still flag.
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import sys
|
|
17
|
+
import importlib
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from types import SimpleNamespace
|
|
19
20
|
|
|
@@ -31,12 +32,12 @@ from code_rules_comments import ( # noqa: E402
|
|
|
31
32
|
check_comments_python,
|
|
32
33
|
extract_comment_texts,
|
|
33
34
|
)
|
|
34
|
-
|
|
35
35
|
code_rules_enforcer = SimpleNamespace(
|
|
36
36
|
check_comment_changes=check_comment_changes,
|
|
37
37
|
check_comments_python=check_comments_python,
|
|
38
38
|
extract_comment_texts=extract_comment_texts,
|
|
39
39
|
)
|
|
40
|
+
code_rules_enforcer_module = importlib.import_module("code_rules_enforcer")
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
def test_python_check_should_not_flag_hex_color_literal() -> None:
|
|
@@ -121,7 +122,7 @@ def test_check_comment_changes_skips_removal_when_old_python_un_parseable() -> N
|
|
|
121
122
|
assert issues == []
|
|
122
123
|
|
|
123
124
|
|
|
124
|
-
def
|
|
125
|
+
def test_check_comment_changes_allows_comment_removal_without_advisory(
|
|
125
126
|
capsys: pytest.CaptureFixture[str],
|
|
126
127
|
) -> None:
|
|
127
128
|
old_content = 'x = 1 # existing comment\n'
|
|
@@ -129,7 +130,7 @@ def test_check_comment_changes_advises_without_blocking_on_removal_on_parseable_
|
|
|
129
130
|
issues = code_rules_enforcer.check_comment_changes(old_content, new_content, "foo.py")
|
|
130
131
|
assert not any("Existing comment removed" in each_issue for each_issue in issues)
|
|
131
132
|
captured = capsys.readouterr()
|
|
132
|
-
assert
|
|
133
|
+
assert captured.err == ""
|
|
133
134
|
|
|
134
135
|
|
|
135
136
|
def test_python_check_should_exempt_directive_without_space_after_hash() -> None:
|
|
@@ -194,3 +195,189 @@ def test_extract_should_classify_inline_shebang_lookalike_as_inline_comment() ->
|
|
|
194
195
|
inline, standalone = code_rules_enforcer.extract_comment_texts(content, "foo.py")
|
|
195
196
|
assert inline != set()
|
|
196
197
|
assert standalone == set()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_full_gate_blocks_python_inline_comment_in_test_file() -> None:
|
|
201
|
+
context = code_rules_enforcer_module._ValidationContext(
|
|
202
|
+
content="total = 1 # added\n",
|
|
203
|
+
old_content="total = 1\n",
|
|
204
|
+
effective_content="total = 1 # added\n",
|
|
205
|
+
file_path="test_totals.py",
|
|
206
|
+
all_changed_lines=None,
|
|
207
|
+
defer_scope_to_caller=False,
|
|
208
|
+
sibling_directory=None,
|
|
209
|
+
phase="edit",
|
|
210
|
+
)
|
|
211
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
212
|
+
assert any("Inline comment added" in each_issue for each_issue in issues)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_full_gate_blocks_javascript_inline_comment_in_test_file() -> None:
|
|
216
|
+
context = code_rules_enforcer_module._ValidationContext(
|
|
217
|
+
content="const total = 1; // added\n",
|
|
218
|
+
old_content="const total = 1;\n",
|
|
219
|
+
effective_content="const total = 1; // added\n",
|
|
220
|
+
file_path="totals.test.js",
|
|
221
|
+
all_changed_lines=None,
|
|
222
|
+
defer_scope_to_caller=False,
|
|
223
|
+
sibling_directory=None,
|
|
224
|
+
phase="edit",
|
|
225
|
+
)
|
|
226
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
227
|
+
assert any("Inline comment added" in each_issue for each_issue in issues)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_full_gate_blocks_javascript_inline_block_comment_in_test_file() -> None:
|
|
231
|
+
context = code_rules_enforcer_module._ValidationContext(
|
|
232
|
+
content="const total = 1; /* added */\n",
|
|
233
|
+
old_content="const total = 1;\n",
|
|
234
|
+
effective_content="const total = 1; /* added */\n",
|
|
235
|
+
file_path="totals.test.js",
|
|
236
|
+
all_changed_lines=None,
|
|
237
|
+
defer_scope_to_caller=False,
|
|
238
|
+
sibling_directory=None,
|
|
239
|
+
phase="edit",
|
|
240
|
+
)
|
|
241
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
242
|
+
assert any("Inline comment added" in each_issue for each_issue in issues)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_full_gate_blocks_javascript_directive_comment_in_test_file() -> None:
|
|
246
|
+
context = code_rules_enforcer_module._ValidationContext(
|
|
247
|
+
content="const total = 1; // eslint-disable-next-line\n",
|
|
248
|
+
old_content="const total = 1;\n",
|
|
249
|
+
effective_content="const total = 1; // eslint-disable-next-line\n",
|
|
250
|
+
file_path="totals.test.js",
|
|
251
|
+
all_changed_lines=None,
|
|
252
|
+
defer_scope_to_caller=False,
|
|
253
|
+
sibling_directory=None,
|
|
254
|
+
phase="edit",
|
|
255
|
+
)
|
|
256
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
257
|
+
assert any("Inline comment added" in each_issue for each_issue in issues)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_full_gate_blocks_python_directive_comment_in_test_file() -> None:
|
|
261
|
+
context = code_rules_enforcer_module._ValidationContext(
|
|
262
|
+
content="total = 1 # noqa\n",
|
|
263
|
+
old_content="total = 1\n",
|
|
264
|
+
effective_content="total = 1 # noqa\n",
|
|
265
|
+
file_path="test_totals.py",
|
|
266
|
+
all_changed_lines=None,
|
|
267
|
+
defer_scope_to_caller=False,
|
|
268
|
+
sibling_directory=None,
|
|
269
|
+
phase="edit",
|
|
270
|
+
)
|
|
271
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
272
|
+
assert any("comment added" in each_issue.lower() for each_issue in issues)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@pytest.mark.parametrize(
|
|
276
|
+
"each_marker", ("# TODO #999: track", "# FIXME #999: track", "# HACK #999: track", "# XXX #999: track", "# type: ignore[misc]")
|
|
277
|
+
)
|
|
278
|
+
def test_full_gate_blocks_changed_marker_comments_in_test_file(each_marker: str) -> None:
|
|
279
|
+
context = code_rules_enforcer_module._ValidationContext(
|
|
280
|
+
content=f"total = 1 {each_marker}\n",
|
|
281
|
+
old_content="total = 1\n",
|
|
282
|
+
effective_content=f"total = 1 {each_marker}\n",
|
|
283
|
+
file_path="test_totals.py",
|
|
284
|
+
all_changed_lines=None,
|
|
285
|
+
defer_scope_to_caller=False,
|
|
286
|
+
sibling_directory=None,
|
|
287
|
+
phase="edit",
|
|
288
|
+
)
|
|
289
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
290
|
+
assert any("comment added" in each_issue.lower() for each_issue in issues)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def test_full_gate_blocks_standalone_comment_in_test_file() -> None:
|
|
294
|
+
context = code_rules_enforcer_module._ValidationContext(
|
|
295
|
+
content="# added\ntotal = 1\n",
|
|
296
|
+
old_content="total = 1\n",
|
|
297
|
+
effective_content="# added\ntotal = 1\n",
|
|
298
|
+
file_path="test_totals.py",
|
|
299
|
+
all_changed_lines=None,
|
|
300
|
+
defer_scope_to_caller=False,
|
|
301
|
+
sibling_directory=None,
|
|
302
|
+
phase="edit",
|
|
303
|
+
)
|
|
304
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
305
|
+
assert any("Standalone comment added" in each_issue for each_issue in issues)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_full_gate_preserves_shebang_and_docstring_in_test_file() -> None:
|
|
309
|
+
context = code_rules_enforcer_module._ValidationContext(
|
|
310
|
+
content='#!/usr/bin/env python3\n"""Describe totals."""\ntotal = 1\n',
|
|
311
|
+
old_content="#!/usr/bin/env python3\n",
|
|
312
|
+
effective_content='#!/usr/bin/env python3\n"""Describe totals."""\ntotal = 1\n',
|
|
313
|
+
file_path="test_totals.py",
|
|
314
|
+
all_changed_lines=None,
|
|
315
|
+
defer_scope_to_caller=False,
|
|
316
|
+
sibling_directory=None,
|
|
317
|
+
phase="edit",
|
|
318
|
+
)
|
|
319
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
320
|
+
assert not any("comment added" in each_issue.lower() for each_issue in issues)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def test_full_gate_allows_existing_comment_removal_without_advisory(
|
|
324
|
+
capsys: pytest.CaptureFixture[str],
|
|
325
|
+
) -> None:
|
|
326
|
+
context = code_rules_enforcer_module._ValidationContext(
|
|
327
|
+
content="total = 1\n",
|
|
328
|
+
old_content="total = 1 # existing\n",
|
|
329
|
+
effective_content="total = 1\n",
|
|
330
|
+
file_path="test_totals.py",
|
|
331
|
+
all_changed_lines=None,
|
|
332
|
+
defer_scope_to_caller=False,
|
|
333
|
+
sibling_directory=None,
|
|
334
|
+
phase="edit",
|
|
335
|
+
)
|
|
336
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
337
|
+
assert not any("Existing comment removed" in each_issue for each_issue in issues)
|
|
338
|
+
assert capsys.readouterr().err == ""
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@pytest.mark.parametrize(
|
|
342
|
+
("file_path", "old_content", "new_content"),
|
|
343
|
+
(
|
|
344
|
+
("totals.py", "total = 1\n", "total = 2 # TODO #999: track\n"),
|
|
345
|
+
("test_totals.py", "total = 1\n", "total = 2 # TODO #999: track\n"),
|
|
346
|
+
("totals.js", "const total = 1;\n", "const total = 2; // eslint-disable-next-line\n"),
|
|
347
|
+
("totals.test.js", "const total = 1;\n", "const total = 2; // eslint-disable-next-line\n"),
|
|
348
|
+
),
|
|
349
|
+
)
|
|
350
|
+
def test_edit_lane_does_not_report_comment_policy(
|
|
351
|
+
file_path: str, old_content: str, new_content: str
|
|
352
|
+
) -> None:
|
|
353
|
+
issues = code_rules_enforcer_module.validate_content_for_edit_lane(
|
|
354
|
+
new_content, file_path, old_content
|
|
355
|
+
)
|
|
356
|
+
assert not any("comment" in each_issue.lower() for each_issue in issues)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@pytest.mark.parametrize(
|
|
360
|
+
("file_path", "old_content", "new_content"),
|
|
361
|
+
(
|
|
362
|
+
("totals.py", "total = 1 # TODO #999: track\n", "total = 2 # TODO #999: track\n"),
|
|
363
|
+
("totals.py", "# TODO #999: track\ntotal = 1\n", "# TODO #999: track\ntotal = 2\n"),
|
|
364
|
+
("totals.py", "total = 1 # noqa\n", "total = 2 # noqa\n"),
|
|
365
|
+
(
|
|
366
|
+
"totals.js",
|
|
367
|
+
"const total = 1; // eslint-disable-next-line\n",
|
|
368
|
+
"const total = 2; // eslint-disable-next-line\n",
|
|
369
|
+
),
|
|
370
|
+
),
|
|
371
|
+
)
|
|
372
|
+
def test_check_comment_changes_blocks_retained_comments_on_changed_code(
|
|
373
|
+
file_path: str, old_content: str, new_content: str
|
|
374
|
+
) -> None:
|
|
375
|
+
issues = code_rules_enforcer.check_comment_changes(old_content, new_content, file_path)
|
|
376
|
+
assert any("retained on changed code" in each_issue for each_issue in issues)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def test_check_comment_changes_accepts_distant_untouched_comment() -> None:
|
|
380
|
+
old_content = "# TODO #999: distant note\n\ntotal = 1\n"
|
|
381
|
+
new_content = "# TODO #999: distant note\n\ntotal = 2\n"
|
|
382
|
+
issues = code_rules_enforcer.check_comment_changes(old_content, new_content, "totals.py")
|
|
383
|
+
assert not any("retained on changed code" in each_issue for each_issue in issues)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Tests for JavaScript comment extraction and diff occurrence reporting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
_BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
10
|
+
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
11
|
+
if _BLOCKING_DIRECTORY not in sys.path:
|
|
12
|
+
sys.path.insert(0, _BLOCKING_DIRECTORY)
|
|
13
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
14
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
15
|
+
|
|
16
|
+
check_comment_changes = importlib.import_module(
|
|
17
|
+
"code_rules_comments"
|
|
18
|
+
).check_comment_changes
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_check_comment_changes_ignores_javascript_url_string() -> None:
|
|
22
|
+
issues = check_comment_changes(
|
|
23
|
+
"", 'const docsUrl = "https://example.com/guide";\n', "docs.js"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
assert issues == []
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_check_comment_changes_reports_javascript_comment_line() -> None:
|
|
30
|
+
issues = check_comment_changes(
|
|
31
|
+
"", 'const total = 1;\nconst next = 2; // added\n', "totals.js"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
assert any("Line 2: Inline comment added" in each_issue for each_issue in issues)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_check_comment_changes_reports_duplicate_identical_comment_occurrence() -> None:
|
|
38
|
+
issues = check_comment_changes(
|
|
39
|
+
"total = 1 # repeated\n",
|
|
40
|
+
"total = 1 # repeated\ntotal = 2 # repeated\n",
|
|
41
|
+
"totals.py",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
assert any("Line 2: Inline comment added" in each_issue for each_issue in issues)
|
|
@@ -74,7 +74,6 @@ def test_should_grandfather_unchanged_join_separator_in_wide_edit() -> None:
|
|
|
74
74
|
)
|
|
75
75
|
proposed_fragment = (
|
|
76
76
|
"def render(all_paths: list) -> str:\n"
|
|
77
|
-
" # keep the join separator the same\n"
|
|
78
77
|
" return ', '.join(str(each_path) for each_path in all_paths)\n"
|
|
79
78
|
)
|
|
80
79
|
issues = code_rules_enforcer.validate_content_for_full_gate(
|
|
@@ -28,5 +28,5 @@ def get_pull_request_url(repo_dir: Path) -> str | None:
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def build_pull_request_reminder(url: str) -> str:
|
|
31
|
-
prompt = "Review this pull request: <PR link>\n\nRead the PR and its full diff. Do not use the branch name, commit message, labels, current title, or a shallow summary.\n\
|
|
31
|
+
prompt = "Review this pull request: <PR link>\n\nRead the PR and its full diff. Do not use the branch name, commit message, labels, current title, or a shallow summary.\n\nBefore writing, inventory every independently observable behavior in the full diff. A behavior is independent when a user, caller, operator, automation, output, error, exit status, fallback, or side effect can observe it separately. Lead with the central behavior for readability, then give every other behavior its own short paragraph or bullet.\n\nFor each behavior, state the trigger or observer, the Before -> After result, the affected surface/caller when shared, and focused proof. Include important preserved behavior and fallback paths when they help a coder rule a regression in or out. Describe outcomes, not line-by-line implementation, and do not hide multiple behaviors under vague umbrella wording.\n\nWrite like the reader is smart but knows nothing about this code. Pretend they asked you to explain it like they are five and feel stupid. If a word needs another sentence to explain it, replace the word — or turn it into a tiny story.\n\nYour job is not a dry API blurb. Your job is an illustrative before/after a person can picture.\n\nGold voice for titles and “What this adds”:\n\nThe theme package has a phone icon somewhere. We need to find it.\n\nBefore: yell “anyone know how to find icons?” and wait for a hand.\n\nAfter: call the finder by its name tag: “icon finder, go.”\n\nThat’s all “operation id” is — the finder’s name tag.\n\nRules for that voice:\n\nStart from a concrete scene (what’s in someone’s hand, what they’re hunting for).\nPrefer Before / After when the change is “how you ask” or “how you look it up.”\nUse picture words: hand it, open, find, check, save, skip, stop, call by name, name tag.\nPrefer spoken results: “found it,” “missing,” “duplicated,” “couldn’t.”\nWhen the diff uses an abstract word (operation id, registry, envelope, adapter, digest, schema), do one of two things:\nDrop it and say what the person does with their hands, or\nKeep it once, then gloss it in the same breath with a name-tag / mailbox / checklist metaphor — like the “finder’s name tag” line above.\nNever leave jargon unexplained. Never write as if the reader already knows the jargon.\nAdd a tiny concrete example in parentheses when a name is abstract (like “the phone icon”).\nFew words. Small words. Clear on first read.\nReturn:\n\nRecommended title\n\nFew words. Small words. Clear on first read.\nSay what you hand it, what it does, and what you get back — or the Before/After of how you ask.\nPrefer picture words and spoken results.\nGood title: “Call the icon finder by its name tag instead of asking around”\nGood title: “Given a theme STP and an asset name, report whether that asset was found, missing, or duplicated”\nBad title: anything that only says operation id / registry / typed envelope / adapter / digest / schema with no picture.\nUse “Add” only if it helps. Do not force it.\nNo vague words like “improve,” “enhance,” or “update.”\nReturn one title only.\nWhy this title fits\n\n1–2 short sentences in the same voice.\nIf needed, one Before / After beat, then what you get back.\nIf this section is needed to decode the title, rewrite the title.\nEvidence\n\nKey files or functions that support the title.\nIf you cannot read the PR or full diff, say so and do not propose a title.\n\nThen write a short, paste-ready PR description.\n\nUse this structure:\n\nWhat this adds\n1–2 short paragraphs in that illustrative voice. Lead with a scene when you can (“The theme package has a phone icon somewhere…”). If the change is a new way to ask, use Before / After. Say what you give it, what it does with its hands, and what you get back. If an abstract machine-word must appear, gloss it immediately (“That’s all X is — …”). Describe the job people can see. Do not describe inner machinery. Mention a check only if it is central to the feature.\n\nWhy\nOne short paragraph. Same plain level. Say who needs this and what goes wrong without it (the “yell and wait for a hand” problem). If this is needed to decode the feature, rewrite “What this adds.”\n\nVerification\nList only proof from the PR, CI, or your own checks.\n\nReported in the PR\n\n…\nChecked by you / CI here\n\n…\nRules:\n\nFew words. Small words. Clear on first read. ELI5 I-am-stupid mark.\nDo not invent behavior, tests, or benefits.\nStrip branch names, hashes, draft notes, agent notes, and merge notes.\nNo vague words like “improve,” “enhance,” or “update.”\nIllustrative first. Abstract labels only if glossed in the same breath.\nUse the recommended title and paste-ready description to run `gh pr edit <PR link> --title ... --body-file ...`, then run `gh pr view <PR link> --json title,body` to confirm the saved title and body. Report the saved result."
|
|
32
32
|
return prompt.replace("<PR link>", url)
|
|
@@ -242,6 +242,14 @@ def test_pull_request_reminder_prints_url(
|
|
|
242
242
|
assert output.startswith("Review this pull request: https://github.com/example/repo/pull/1\n")
|
|
243
243
|
assert "<PR link>" not in output
|
|
244
244
|
assert "Do not use the branch name, commit message, labels, current title" in output
|
|
245
|
+
assert (
|
|
246
|
+
"Before writing, inventory every independently observable behavior in the full diff."
|
|
247
|
+
in output
|
|
248
|
+
and "Lead with the central behavior for readability, then give every other behavior its own short paragraph or bullet."
|
|
249
|
+
in output
|
|
250
|
+
and "For each behavior, state the trigger or observer, the Before -> After result" in output
|
|
251
|
+
and "OVERRIDE:" not in output
|
|
252
|
+
)
|
|
245
253
|
assert "gh pr edit https://github.com/example/repo/pull/1" in output
|
|
246
254
|
assert output.rstrip().endswith("Report the saved result.")
|
|
247
255
|
|
|
@@ -60,14 +60,13 @@ STALE_COMMENT_DENY_TEMPLATE: str = (
|
|
|
60
60
|
"The comment above the changed line in {file_path} names "
|
|
61
61
|
"'{orphaned_name}', which this edit removes from the line below. "
|
|
62
62
|
"The comment left in place reads: {contradicted_comment}. "
|
|
63
|
-
"
|
|
63
|
+
"Remove the comment in the same edit."
|
|
64
64
|
)
|
|
65
65
|
|
|
66
66
|
STALE_COMMENT_ADDITIONAL_CONTEXT: str = (
|
|
67
67
|
"A standalone comment directly above an edited line describes that "
|
|
68
|
-
"line. When the edit drops a name the comment carries,
|
|
69
|
-
"comment
|
|
70
|
-
"the same Edit call."
|
|
68
|
+
"line. When the edit drops a name the comment carries, remove the "
|
|
69
|
+
"comment inside the same Edit call."
|
|
71
70
|
)
|
|
72
71
|
|
|
73
72
|
STALE_COMMENT_SYSTEM_MESSAGE: str = (
|
|
@@ -9,7 +9,15 @@ _HOOKS_ROOT = Path(__file__).resolve().parent.parent
|
|
|
9
9
|
if str(_HOOKS_ROOT) not in sys.path:
|
|
10
10
|
sys.path.insert(0, str(_HOOKS_ROOT))
|
|
11
11
|
|
|
12
|
-
from hooks_constants.unused_module_import_constants import
|
|
12
|
+
from hooks_constants.unused_module_import_constants import (
|
|
13
|
+
UNUSED_IMPORT_GUIDANCE,
|
|
14
|
+
line_suppresses_unused_import_via_noqa,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_unused_import_guidance_uses_comment_free_side_effect_remediation() -> None:
|
|
19
|
+
assert "noqa" not in UNUSED_IMPORT_GUIDANCE
|
|
20
|
+
assert "importlib.import_module" in UNUSED_IMPORT_GUIDANCE
|
|
13
21
|
|
|
14
22
|
|
|
15
23
|
def test_line_suppresses_bare_noqa() -> None:
|
|
@@ -11,7 +11,7 @@ NOQA_DIRECTIVE_PATTERN: re.Pattern[str] = re.compile(
|
|
|
11
11
|
)
|
|
12
12
|
MAX_UNUSED_IMPORT_ISSUES: int = 25
|
|
13
13
|
UNUSED_IMPORT_GUIDANCE: str = (
|
|
14
|
-
"remove unused import; if
|
|
14
|
+
"remove unused import; if side effects are required, call importlib.import_module explicitly"
|
|
15
15
|
)
|
|
16
16
|
TYPE_CHECKING_IDENTIFIER: str = "TYPE_CHECKING"
|
|
17
17
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Comment detection
|
|
1
|
+
"""Comment detection helpers used by the opt-in diff-aware linter.
|
|
2
2
|
|
|
3
3
|
Implements check 26: No comments in code (self-documenting code principle).
|
|
4
4
|
|
|
@@ -12,7 +12,7 @@ Exceptions (NOT flagged):
|
|
|
12
12
|
- Docstrings (triple-quoted strings for function contracts)
|
|
13
13
|
|
|
14
14
|
Note: Docstrings documenting function Args/Returns/Raises are acceptable
|
|
15
|
-
per CODE_RULES.md.
|
|
15
|
+
per CODE_RULES.md. The save-path validator does not invoke these helpers.
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
import re
|
|
@@ -42,7 +42,6 @@ from .test_safety_checks import (
|
|
|
42
42
|
check_debug_guard_in_dev_scripts,
|
|
43
43
|
check_no_skip_decorators,
|
|
44
44
|
)
|
|
45
|
-
from .todo_checks import validate_file as validate_todo_file
|
|
46
45
|
from .type_safety_checks import validate_file as validate_type_safety_file
|
|
47
46
|
from .useless_test_checks import validate_file as validate_useless_test_file
|
|
48
47
|
from .validator_base import ValidatorResult
|
|
@@ -153,14 +152,6 @@ def _all_fast_save_validator_specifications() -> (
|
|
|
153
152
|
_violation_lines_from(validate_magic_literal_file),
|
|
154
153
|
NO_PYTHON_FILES_MESSAGE,
|
|
155
154
|
),
|
|
156
|
-
# Python code quality and TODO tracking.
|
|
157
|
-
_FastSaveValidatorSpecification(
|
|
158
|
-
"TODO Tracking",
|
|
159
|
-
"36",
|
|
160
|
-
_is_python_file,
|
|
161
|
-
_violation_lines_from(validate_todo_file),
|
|
162
|
-
NO_PYTHON_FILES_MESSAGE,
|
|
163
|
-
),
|
|
164
155
|
_FastSaveValidatorSpecification(
|
|
165
156
|
"Security",
|
|
166
157
|
"27,28,29",
|
|
@@ -6,12 +6,6 @@ import sys
|
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from typing import List
|
|
8
8
|
|
|
9
|
-
try:
|
|
10
|
-
from .validator_defaults import DEFAULT_BASE_BRANCH_WHEN_UNKNOWN
|
|
11
|
-
except ImportError:
|
|
12
|
-
from validator_defaults import DEFAULT_BASE_BRANCH_WHEN_UNKNOWN
|
|
13
|
-
|
|
14
|
-
|
|
15
9
|
SUBPROCESS_TIMEOUT_SECONDS = 30
|
|
16
10
|
|
|
17
11
|
|
|
@@ -38,83 +32,6 @@ def get_current_branch() -> str:
|
|
|
38
32
|
return ""
|
|
39
33
|
|
|
40
34
|
|
|
41
|
-
def check_single_commit_when_pr_exists() -> List[Violation]:
|
|
42
|
-
"""
|
|
43
|
-
Check that a PR branch has exactly 1 commit ahead of its base.
|
|
44
|
-
|
|
45
|
-
Returns empty list if:
|
|
46
|
-
- No PR exists for current branch
|
|
47
|
-
- gh CLI or git is not available
|
|
48
|
-
- gh/git times out
|
|
49
|
-
- Branch is exactly 1 commit ahead of base
|
|
50
|
-
- git rev-list output is non-numeric
|
|
51
|
-
|
|
52
|
-
Returns violation if:
|
|
53
|
-
- Branch is 0 commits ahead of base
|
|
54
|
-
- Branch is more than 1 commit ahead of base
|
|
55
|
-
"""
|
|
56
|
-
branch = get_current_branch()
|
|
57
|
-
if not branch:
|
|
58
|
-
return []
|
|
59
|
-
|
|
60
|
-
try:
|
|
61
|
-
pr_info = subprocess.run(
|
|
62
|
-
["gh", "pr", "list", "--head", branch, "--json", "baseRefName,number"],
|
|
63
|
-
capture_output=True,
|
|
64
|
-
text=True,
|
|
65
|
-
check=True,
|
|
66
|
-
timeout=SUBPROCESS_TIMEOUT_SECONDS,
|
|
67
|
-
)
|
|
68
|
-
except FileNotFoundError:
|
|
69
|
-
return []
|
|
70
|
-
except subprocess.CalledProcessError:
|
|
71
|
-
return []
|
|
72
|
-
except subprocess.TimeoutExpired:
|
|
73
|
-
return []
|
|
74
|
-
|
|
75
|
-
try:
|
|
76
|
-
pr_data = json.loads(pr_info.stdout)
|
|
77
|
-
except json.JSONDecodeError:
|
|
78
|
-
return []
|
|
79
|
-
|
|
80
|
-
if not pr_data:
|
|
81
|
-
return []
|
|
82
|
-
|
|
83
|
-
base_branch = pr_data[0].get("baseRefName", DEFAULT_BASE_BRANCH_WHEN_UNKNOWN)
|
|
84
|
-
|
|
85
|
-
try:
|
|
86
|
-
rev_list = subprocess.run(
|
|
87
|
-
["git", "rev-list", "--count", f"{base_branch}..HEAD"],
|
|
88
|
-
capture_output=True,
|
|
89
|
-
text=True,
|
|
90
|
-
check=True,
|
|
91
|
-
timeout=SUBPROCESS_TIMEOUT_SECONDS,
|
|
92
|
-
)
|
|
93
|
-
except FileNotFoundError:
|
|
94
|
-
return []
|
|
95
|
-
except subprocess.CalledProcessError:
|
|
96
|
-
return []
|
|
97
|
-
except subprocess.TimeoutExpired:
|
|
98
|
-
return []
|
|
99
|
-
|
|
100
|
-
commit_count_text = rev_list.stdout.strip()
|
|
101
|
-
try:
|
|
102
|
-
commit_count = int(commit_count_text)
|
|
103
|
-
except ValueError:
|
|
104
|
-
return []
|
|
105
|
-
|
|
106
|
-
if commit_count == 1:
|
|
107
|
-
return []
|
|
108
|
-
|
|
109
|
-
return [
|
|
110
|
-
Violation(
|
|
111
|
-
file="",
|
|
112
|
-
line=0,
|
|
113
|
-
message=f"Branch must have exactly 1 commit ahead of {base_branch}, found {commit_count} commits",
|
|
114
|
-
)
|
|
115
|
-
]
|
|
116
|
-
|
|
117
|
-
|
|
118
35
|
def check_draft_pr_state() -> List[Violation]:
|
|
119
36
|
"""
|
|
120
37
|
Check that PR is in draft state.
|
|
@@ -175,7 +92,6 @@ def main() -> None:
|
|
|
175
92
|
"""Run all git checks and exit with appropriate code."""
|
|
176
93
|
violations: List[Violation] = []
|
|
177
94
|
|
|
178
|
-
violations.extend(check_single_commit_when_pr_exists())
|
|
179
95
|
violations.extend(check_draft_pr_state())
|
|
180
96
|
|
|
181
97
|
if violations:
|
|
@@ -410,18 +410,12 @@ def run_git_checks() -> ValidatorResult:
|
|
|
410
410
|
|
|
411
411
|
|
|
412
412
|
def run_comment_checks(files: List[Path]) -> ValidatorResult:
|
|
413
|
-
"""
|
|
414
|
-
|
|
415
|
-
The hook compares old vs new content to block NEW comments and
|
|
416
|
-
print a stderr advisory when an existing comment is removed. This
|
|
417
|
-
standalone validator is disabled because it flags ALL comments in
|
|
418
|
-
existing files, which forces agents to remove them to pass validation.
|
|
419
|
-
"""
|
|
413
|
+
"""Report that comment policy runs through the explicit diff-aware linter."""
|
|
420
414
|
return ValidatorResult(
|
|
421
415
|
name="No Comments",
|
|
422
416
|
checks="26",
|
|
423
417
|
passed=True,
|
|
424
|
-
output="
|
|
418
|
+
output="Run code_rules_gate.py with --comment-policy",
|
|
425
419
|
)
|
|
426
420
|
|
|
427
421
|
|
|
@@ -1675,7 +1669,6 @@ def main() -> int:
|
|
|
1675
1669
|
("Security", lambda: run_security_checks(files)),
|
|
1676
1670
|
("Code Quality", lambda: run_code_quality_checks(files)),
|
|
1677
1671
|
("Python Anti-patterns", lambda: run_python_antipattern_checks(files)),
|
|
1678
|
-
("TODO Tracking", lambda: run_todo_checks(files)),
|
|
1679
1672
|
("Type Safety", lambda: run_type_safety_checks(files)),
|
|
1680
1673
|
]
|
|
1681
1674
|
|
|
@@ -9,7 +9,6 @@ from .fast_save_validators import run_fast_save_validators
|
|
|
9
9
|
from .magic_value_checks import validate_file as read_magic_literal_violations
|
|
10
10
|
from .react_checks import check_no_class_components
|
|
11
11
|
from .test_safety_checks import check_no_skip_decorators
|
|
12
|
-
from .todo_checks import validate_file as read_todo_violations
|
|
13
12
|
|
|
14
13
|
MAGIC_LITERAL_SOURCE = "def compute_total(unit_price: int) -> int:\n return unit_price * 199\n"
|
|
15
14
|
_UNTRACKED_TODO_MARKER = "#" + " TODO"
|
|
@@ -24,7 +23,6 @@ _ALL_EXPECTED_FAST_SAVE_VALIDATOR_NAMES = frozenset(
|
|
|
24
23
|
"Python Style",
|
|
25
24
|
"Abbreviations",
|
|
26
25
|
"Magic Values",
|
|
27
|
-
"TODO Tracking",
|
|
28
26
|
"Security",
|
|
29
27
|
"Code Quality",
|
|
30
28
|
"Python Anti-patterns",
|
|
@@ -103,16 +101,12 @@ def test_magic_values_outcome_matches_direct_validate_file_call(
|
|
|
103
101
|
assert magic_values_outcome.output == "\n".join(expected_lines)
|
|
104
102
|
|
|
105
103
|
|
|
106
|
-
def
|
|
104
|
+
def test_todo_comments_do_not_run_on_save_path(tmp_path: Path) -> None:
|
|
107
105
|
target_file = tmp_path / "legacy_module.py"
|
|
108
106
|
target_file.write_text(TODO_SOURCE, encoding="utf-8")
|
|
109
|
-
expected_lines = [str(each) for each in read_todo_violations(target_file)]
|
|
110
|
-
assert expected_lines != []
|
|
111
|
-
|
|
112
107
|
all_outcomes = run_fast_save_validators([target_file])
|
|
113
108
|
|
|
114
|
-
|
|
115
|
-
assert todo_outcome.output == "\n".join(expected_lines)
|
|
109
|
+
assert all(each.name != "TODO Tracking" for each in all_outcomes)
|
|
116
110
|
|
|
117
111
|
|
|
118
112
|
def test_abbreviations_outcome_reports_clean_for_a_clean_file(tmp_path: Path) -> None:
|