claude-dev-env 2.31.1 → 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/bin/install.test.mjs +11 -3
- package/docs/CODE_RULES.md +8 -9
- package/hooks/blocking/bash_post_call_dispatcher.py +70 -0
- package/hooks/blocking/bash_pre_tool_use_dispatcher.py +8 -21
- package/hooks/blocking/code_rules_comments.py +118 -59
- package/hooks/blocking/code_rules_duplicate_body.py +13 -7
- package/hooks/blocking/code_rules_enforcer.py +19 -7
- package/hooks/blocking/code_rules_enforcer_test_support.py +39 -0
- package/hooks/blocking/code_rules_type_escape.py +16 -23
- package/hooks/blocking/javascript_comment_scanner.py +99 -0
- package/hooks/blocking/shell_substitution_blocker.py +175 -8
- package/hooks/blocking/stop_dispatcher.py +6 -9
- package/hooks/blocking/tdd_enforcer.py +3 -1
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +7 -0
- package/hooks/blocking/tdd_enforcer_parts/content_hash_store.py +141 -23
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_hash_store.py +114 -1
- package/hooks/blocking/test_bash_post_call_dispatcher.py +112 -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/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +8 -1
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +7 -2
- package/hooks/blocking/test_shell_substitution_blocker.py +105 -0
- package/hooks/blocking/test_state_description_blocker.py +56 -71
- package/hooks/blocking/test_tdd_enforcer.py +67 -1
- package/hooks/git-hooks/pull_request_handoff.py +1 -1
- package/hooks/git-hooks/test_post_commit.py +8 -0
- package/hooks/hooks.json +2 -2
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +22 -0
- package/hooks/hooks_constants/hosted_hook_runner.py +53 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +9 -2
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +10 -0
- package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +3 -4
- package/hooks/hooks_constants/state_file_lock.py +76 -0
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +27 -0
- package/hooks/hooks_constants/test_failure_recorder_constants.py +35 -0
- package/hooks/hooks_constants/test_hosted_hook_runner.py +55 -1
- 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/observability/session_file_edit_tracker.py +2 -55
- package/hooks/observability/test_failure_recorder.py +219 -0
- package/hooks/observability/test_test_failure_recorder.py +173 -0
- package/hooks/validators/comment_checks.py +2 -2
- package/hooks/validators/config/directory_exemption_constants.py +0 -9
- package/hooks/validators/config/system_temporary_roots_constants.py +15 -0
- package/hooks/validators/fast_save_validators.py +0 -9
- package/hooks/validators/git_checks.py +0 -84
- package/hooks/validators/mypy_integration.py +10 -36
- package/hooks/validators/project_roots.py +58 -0
- package/hooks/validators/run_all_validators.py +89 -42
- package/hooks/validators/system_temporary_roots.py +1 -1
- package/hooks/validators/test_fast_save_validators.py +2 -8
- package/hooks/validators/test_git_checks.py +0 -249
- package/hooks/validators/test_run_all_validators.py +22 -0
- package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +249 -0
- package/hooks/validators/test_run_all_validators_reporting.py +66 -0
- 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/rules/shell-invocation.md +1 -1
- package/rules/testing.md +4 -0
- package/scripts/check.ps1 +35 -1
- package/scripts/codex_compat_materializer.py +1 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +9 -5
- package/scripts/dev_env_scripts_constants/shared_tree_constants.py +17 -0
- package/scripts/shared_tree_paths.py +59 -25
- package/scripts/test_shared_tree_paths.py +30 -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
|
@@ -141,3 +141,33 @@ def test_missing_module_returns_unresolved_candidate(tmp_path: Path) -> None:
|
|
|
141
141
|
assert not (
|
|
142
142
|
expected_scripts_directory / PROCESS_TREE_KILL_MODULE_FILENAME
|
|
143
143
|
).is_file()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_generalized_resolver_serves_a_deeper_anchor(tmp_path: Path) -> None:
|
|
147
|
+
"""A caller three levels under the root resolves its own sub-package.
|
|
148
|
+
|
|
149
|
+
This is the shape the pr-loop and codex-review scripts carry, which the
|
|
150
|
+
process-tree wrapper's own anchor of one level cannot reach.
|
|
151
|
+
"""
|
|
152
|
+
installed_root = tmp_path / "root"
|
|
153
|
+
module_directory = installed_root / "_shared" / "pr-loop" / "scripts"
|
|
154
|
+
module_directory.mkdir(parents=True)
|
|
155
|
+
module_file = str(module_directory / "run_codex_review.py")
|
|
156
|
+
expected_scripts_directory = (
|
|
157
|
+
installed_root
|
|
158
|
+
/ SHARED_PACKAGE_DIRECTORY_NAME
|
|
159
|
+
/ PROCESS_TREE_DIRECTORY_NAME
|
|
160
|
+
/ SCRIPTS_DIRECTORY_NAME
|
|
161
|
+
)
|
|
162
|
+
expected_scripts_directory.mkdir(parents=True)
|
|
163
|
+
(expected_scripts_directory / PROCESS_TREE_KILL_MODULE_FILENAME).write_text(
|
|
164
|
+
"", encoding="utf-8"
|
|
165
|
+
)
|
|
166
|
+
resolved_scripts_directory = shared_tree_paths.resolve_shared_scripts_directory(
|
|
167
|
+
module_file,
|
|
168
|
+
{},
|
|
169
|
+
PROCESS_TREE_DIRECTORY_NAME,
|
|
170
|
+
PROCESS_TREE_KILL_MODULE_FILENAME,
|
|
171
|
+
3,
|
|
172
|
+
)
|
|
173
|
+
assert resolved_scripts_directory == expected_scripts_directory
|
|
@@ -76,3 +76,114 @@ def test_every_shipped_claude_rule_maps_to_an_mdc() -> None:
|
|
|
76
76
|
if each_rule_file.name in _SKIPPED_RULE_FILE_NAMES:
|
|
77
77
|
continue
|
|
78
78
|
assert each_rule_file.name in output_name_by_rule_file, each_rule_file.name
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _comment_policy_phrases() -> tuple[str, ...]:
|
|
82
|
+
return (
|
|
83
|
+
"when a change touches code that an existing comment describes or is attached to",
|
|
84
|
+
"leave comments tied to untouched code unchanged",
|
|
85
|
+
"keep comment cleanup inside the requested task",
|
|
86
|
+
"production and tests follow one rule",
|
|
87
|
+
"changed directive, todo, fixme, hack, xxx, and type-ignore comments are removed rather than added or justified",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _comment_policy_surfaces(package_root: Path) -> tuple[Path, ...]:
|
|
92
|
+
return (
|
|
93
|
+
package_root.parent.parent / "AGENTS.md",
|
|
94
|
+
package_root / "AGENTS.md",
|
|
95
|
+
package_root / "docs" / "CODE_RULES.md",
|
|
96
|
+
package_root / "system-prompts" / "software-engineer.xml",
|
|
97
|
+
package_root / ".agents" / "agents" / "clean-coder.md",
|
|
98
|
+
package_root / ".agents" / "agents" / "code-quality-agent.md",
|
|
99
|
+
package_root / "_shared" / "pr-loop" / "code-rules-gate.md",
|
|
100
|
+
package_root / "audit-rubrics" / "category_rubrics" / "category-j-code-rules-compliance.md",
|
|
101
|
+
package_root / "audit-rubrics" / "prompts" / "category-j-code-rules-compliance.md",
|
|
102
|
+
package_root / "audit-rubrics" / "category_rubrics" / "category-l-behavior-equivalence.md",
|
|
103
|
+
package_root / "audit-rubrics" / "prompts" / "category-l-behavior-equivalence.md",
|
|
104
|
+
package_root / ".agents" / "skills" / "grok-spawn" / "reference" / "worker-briefs.md",
|
|
105
|
+
package_root.parent.parent / ".github" / "copilot-instructions.md",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _worker_policy_phrases() -> tuple[str, ...]:
|
|
110
|
+
return (
|
|
111
|
+
"do not add code comments.",
|
|
112
|
+
"preserve existing comments.",
|
|
113
|
+
"docstrings remain allowed.",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _worker_policy_surfaces(package_root: Path) -> tuple[Path, ...]:
|
|
118
|
+
return (
|
|
119
|
+
package_root.parent.parent / "AGENTS.md",
|
|
120
|
+
package_root / "AGENTS.md",
|
|
121
|
+
package_root / "docs" / "CODE_RULES.md",
|
|
122
|
+
package_root / "system-prompts" / "software-engineer.xml",
|
|
123
|
+
package_root / ".agents" / "agents" / "clean-coder.md",
|
|
124
|
+
package_root / ".agents" / "skills" / "grok-spawn" / "reference" / "worker-briefs.md",
|
|
125
|
+
package_root.parent.parent / ".github" / "copilot-instructions.md",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_comment_guidance_reaches_installed_instruction_surfaces() -> None:
|
|
130
|
+
expected_phrases = _comment_policy_phrases()
|
|
131
|
+
all_surface_paths = _comment_policy_surfaces(_PACKAGE_ROOT)
|
|
132
|
+
|
|
133
|
+
for each_surface_path in all_surface_paths:
|
|
134
|
+
surface_text = each_surface_path.read_text(encoding="utf-8").lower()
|
|
135
|
+
for each_phrase in expected_phrases:
|
|
136
|
+
assert each_phrase in surface_text, each_surface_path
|
|
137
|
+
for each_surface_path in _worker_policy_surfaces(_PACKAGE_ROOT):
|
|
138
|
+
surface_text = each_surface_path.read_text(encoding="utf-8").lower()
|
|
139
|
+
for each_phrase in _worker_policy_phrases():
|
|
140
|
+
assert each_phrase in surface_text, each_surface_path
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _category_l_policy_texts() -> tuple[str, str, str]:
|
|
144
|
+
category_l_rubric = (
|
|
145
|
+
_PACKAGE_ROOT
|
|
146
|
+
/ "audit-rubrics"
|
|
147
|
+
/ "category_rubrics"
|
|
148
|
+
/ "category-l-behavior-equivalence.md"
|
|
149
|
+
)
|
|
150
|
+
category_l_prompt = (
|
|
151
|
+
_PACKAGE_ROOT
|
|
152
|
+
/ "audit-rubrics"
|
|
153
|
+
/ "prompts"
|
|
154
|
+
/ "category-l-behavior-equivalence.md"
|
|
155
|
+
)
|
|
156
|
+
audit_categories = _PACKAGE_ROOT / "audit-rubrics" / "audit-categories.json"
|
|
157
|
+
return tuple(
|
|
158
|
+
each_path.read_text(encoding="utf-8").lower()
|
|
159
|
+
for each_path in (category_l_rubric, category_l_prompt, audit_categories)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _comment_policy_summary_texts() -> tuple[str, str, str, str]:
|
|
164
|
+
clean_coder = _PACKAGE_ROOT / ".agents" / "agents" / "clean-coder.md"
|
|
165
|
+
category_j = (
|
|
166
|
+
_PACKAGE_ROOT
|
|
167
|
+
/ "audit-rubrics"
|
|
168
|
+
/ "category_rubrics"
|
|
169
|
+
/ "category-j-code-rules-compliance.md"
|
|
170
|
+
)
|
|
171
|
+
code_quality = _PACKAGE_ROOT / ".agents" / "agents" / "code-quality-agent.md"
|
|
172
|
+
gate = _PACKAGE_ROOT / "_shared" / "pr-loop" / "code-rules-gate.md"
|
|
173
|
+
return tuple(
|
|
174
|
+
each_path.read_text(encoding="utf-8").lower()
|
|
175
|
+
for each_path in (clean_coder, category_j, code_quality, gate)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_comment_policy_uses_changed_comment_names_and_type_directives() -> None:
|
|
180
|
+
category_l_rubric, category_l_prompt, audit_categories = _category_l_policy_texts()
|
|
181
|
+
clean_coder, category_j, code_quality, gate = _comment_policy_summary_texts()
|
|
182
|
+
assert "| l7 | changed-comment handling" in category_l_rubric
|
|
183
|
+
assert "**l7. changed-comment handling**" in category_l_prompt
|
|
184
|
+
assert '"axis_name": "changed-comment handling"' in audit_categories
|
|
185
|
+
assert "no type-ignore directives" in clean_coder
|
|
186
|
+
assert "no `# type: ignore` directives" in category_j
|
|
187
|
+
assert "production and test code" in code_quality
|
|
188
|
+
assert "comments tied to touched code are removed" in gate
|
|
189
|
+
assert "comments tied to untouched code remain unchanged" in gate
|
|
@@ -115,8 +115,7 @@
|
|
|
115
115
|
Types — complete coverage required:
|
|
116
116
|
- Every parameter and return value carries an explicit type hint
|
|
117
117
|
- Replace Any with the most specific type available
|
|
118
|
-
-
|
|
119
|
-
trailing `#` justification of at least five characters (AGENTS Types)
|
|
118
|
+
- Avoid `# type: ignore`; remove it and use a typed boundary or real type (AGENTS Types)
|
|
120
119
|
|
|
121
120
|
Configuration — one source of truth:
|
|
122
121
|
- Every constant lives in config/ only
|
|
@@ -124,10 +123,12 @@
|
|
|
124
123
|
- UPPER_SNAKE names belong in config/ only
|
|
125
124
|
|
|
126
125
|
Comments — preservation, add-time blocking:
|
|
127
|
-
-
|
|
128
|
-
-
|
|
126
|
+
- Preserve existing comments.
|
|
127
|
+
- Do not add code comments.
|
|
128
|
+
- When a change touches code that an existing comment describes or is attached to, remove that comment in the same change and carry its meaning through clear names and structure. Leave comments tied to untouched code unchanged. Keep comment cleanup inside the requested task.
|
|
129
|
+
- Production and tests follow one rule. Changed directive, TODO, FIXME, HACK, XXX, and type-ignore comments are removed rather than added or justified.
|
|
129
130
|
- Express new explanations through self-documenting names
|
|
130
|
-
- Docstrings
|
|
131
|
+
- Docstrings remain allowed.
|
|
131
132
|
|
|
132
133
|
Style — enforced at write time:
|
|
133
134
|
- Imports belong at the top of the file
|
|
@@ -197,11 +198,6 @@
|
|
|
197
198
|
</scope_discipline>
|
|
198
199
|
|
|
199
200
|
<git_workflow>
|
|
200
|
-
Maintain one commit per review stage. The initial feature is one commit. Each subsequent
|
|
201
|
-
review round adds exactly one commit. Each round's fixes belong in a single, dedicated
|
|
202
|
-
commit; keep the initial feature commit and each review-fix commit separate to preserve
|
|
203
|
-
the full review history.
|
|
204
|
-
|
|
205
201
|
Keep working documents and image files out of the repository. Files matching these patterns
|
|
206
202
|
belong outside the repo:
|
|
207
203
|
- docs/plans/*.md — working planning documents
|
|
@@ -212,13 +208,6 @@
|
|
|
212
208
|
When multiple features need shared infrastructure, extract the shared infrastructure into
|
|
213
209
|
its own PR, get it reviewed and merged, then launch parallel feature PRs that import it.
|
|
214
210
|
|
|
215
|
-
When running as a GitHub Action (detected via the GITHUB_ACTIONS=true environment variable),
|
|
216
|
-
commit and push each logical change as it completes rather than batching the full session
|
|
217
|
-
into a single commit at the end. Each incremental commit carries a focused message
|
|
218
|
-
describing only that change. This gives PR observers real-time progress and lets them
|
|
219
|
-
intervene mid-run. The "one commit per review stage" rule above applies to interactive
|
|
220
|
-
local sessions; in the GitHub Actions context the entire run is the review stage, and
|
|
221
|
-
incremental commits within it are the expected shape.
|
|
222
211
|
</git_workflow>
|
|
223
212
|
|
|
224
213
|
<agent_workflow>
|
|
@@ -251,8 +240,6 @@
|
|
|
251
240
|
|
|
252
241
|
Reply to each comment inline on the PR after the fix is in place.
|
|
253
242
|
|
|
254
|
-
Produce one review-fix commit per review round. Each round gets exactly one commit covering
|
|
255
|
-
all fixes for that round.
|
|
256
243
|
</code_review_response>
|
|
257
244
|
|
|
258
245
|
<documentation>
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: split-pr
|
|
3
|
-
description: >-
|
|
4
|
-
Source-owned PR analyzer for the hand-written line metric and 200/600 split
|
|
5
|
-
gates. Triggers: /split-pr analyze, split analysis, hand-written lines,
|
|
6
|
-
200/600 PR split.
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# split-pr (analyzer)
|
|
10
|
-
|
|
11
|
-
**Source-owned analyzer** for the locked hand-written line metric and the 200 /
|
|
12
|
-
600 split gates. Reports hand-written churn separately from excluded churn
|
|
13
|
-
(generated, vendor, minified, lockfile). File count informs judgment and is
|
|
14
|
-
never a hard gate.
|
|
15
|
-
|
|
16
|
-
## Gotchas
|
|
17
|
-
|
|
18
|
-
- **Hand-written lines are additions plus deletions** on non-excluded paths.
|
|
19
|
-
- **200 hand-written lines** require a recorded `split_analysis` document.
|
|
20
|
-
- **600 hand-written lines** default to multiple PR slices (`default_split`).
|
|
21
|
-
- **Atomic exception** at 600+ needs a recorded unsplittable reason and a
|
|
22
|
-
standing Fable verdict token — never stay one PR silently.
|
|
23
|
-
- **File count is context only.** It never blocks analysis.
|
|
24
|
-
|
|
25
|
-
## When this applies
|
|
26
|
-
|
|
27
|
-
- User asks for a split analysis or hand-written line report on a PR or file list.
|
|
28
|
-
- A PR approaches or exceeds the 200 / 600 hand-written line surfaces.
|
|
29
|
-
|
|
30
|
-
## Process
|
|
31
|
-
|
|
32
|
-
1. Run `python scripts/analyze_pr.py --files-json <path> --pretty` (or `--pr N`).
|
|
33
|
-
2. Read `hand_written_lines`, `excluded_churn_lines`, `requires_split_analysis`,
|
|
34
|
-
`default_split`, and optional `atomic_exception`.
|
|
35
|
-
3. At 200+ without a prior analysis artifact, record the analyzer JSON before
|
|
36
|
-
shipping further commits on that surface.
|
|
37
|
-
4. At 600+ with `default_split=true`, plan multiple PR slices unless
|
|
38
|
-
`atomic_exception` carries a Fable-approved reason.
|
|
39
|
-
|
|
40
|
-
## File index
|
|
41
|
-
|
|
42
|
-
| File | Purpose |
|
|
43
|
-
|---|---|
|
|
44
|
-
| `SKILL.md` | Analyzer contract and 200/600 gates |
|
|
45
|
-
| `scripts/analyze_pr.py` | Emit analysis JSON from gh or offline files |
|
|
46
|
-
| `scripts/categorize_files.py` | Path churn class (hand-written vs excluded) |
|
|
47
|
-
| `scripts/split_pr_script_types.py` | Canonical split-plan build and path-assignment validation |
|
|
48
|
-
| `scripts/split_pr_title.py` | Collapse titles to exactly one conventional prefix |
|
|
49
|
-
| `scripts/split_pr_layer_order.py` | Deterministic config→other layer sort |
|
|
50
|
-
| `scripts/split_pr_paginate.py` | Paginated PR file intake via gh api --slurp |
|
|
51
|
-
| `scripts/pack_files_into_slices.py` | Pack annotated files into ≤200 hand-written-line slices by layer |
|
|
52
|
-
| `scripts/config/packing_constants.py` | Review budget, hard cap, path→layer markers for packing |
|
|
53
|
-
| `scripts/test_categorize_files_packing.py` | Budget packing and layer inference tests |
|
|
54
|
-
| `scripts/verify_plan.py` | Coverage verification: unique path assignment, path normalization, title contract |
|
|
55
|
-
| `scripts/test_verify_plan.py` | Full-coverage and gap rejection tests |
|
|
56
|
-
| `scripts/test_verify_plan_contract.py` | source_commit and title-normalization contract tests |
|
|
57
|
-
| `scripts/test_verify_plan_path_normalization.py` | Unsafe-path fail-closed tests |
|
|
58
|
-
| `scripts/split_pr_dependency_graph.py` | Deterministic layer-rank dependency graph over slices |
|
|
59
|
-
| `scripts/config/dependency_constants.py` | Graph key names and layer ranks |
|
|
60
|
-
| `scripts/test_split_pr_dependency_graph.py` | Graph order, duplicate-id, unknown-layer tests |
|
|
61
|
-
| `scripts/verify_dependency_graph.py` | CLI + shape/order checks for a dependency graph document |
|
|
62
|
-
| `scripts/test_verify_dependency_graph.py` | Rebuild-order accept/reject and CLI exit tests |
|
|
63
|
-
| `scripts/config/split_pr_constants.py` | Thresholds and excluded path markers |
|
|
64
|
-
| `scripts/config/plan_constants.py` | Plan schema keys, layer order, title-prefix tokens |
|
|
65
|
-
| `scripts/test_analyze_pr.py` | Boundary tests at 199/200/599/600 |
|
|
66
|
-
| `scripts/test_categorize_files.py` | Exclusion classification tests |
|
|
67
|
-
| `scripts/test_split_pr_script_types.py` | Plan schema and full path assignment |
|
|
68
|
-
| `scripts/test_split_pr_title.py` | One-prefix title normalization |
|
|
69
|
-
| `scripts/test_split_pr_layer_order.py` | Layer order ranking |
|
|
70
|
-
| `scripts/test_split_pr_paginate.py` | Slurp page flattening |
|
|
71
|
-
| `scripts/split_pr_process_runner.py` | Captured subprocess runner for Git ops |
|
|
72
|
-
| `scripts/split_pr_git_operations.py` | Safe local slice commit + restore via git_operations_constants |
|
|
73
|
-
| `scripts/execute_split_slices.py` | Materialize full plan in topo order |
|
|
74
|
-
| `scripts/config/git_operations_constants.py` | Git argv tokens and error strings |
|
|
75
|
-
| `scripts/test_split_pr_git_operations.py` | Dirty-tree, path staging, restore tests |
|
|
76
|
-
| `scripts/test_execute_split_slices.py` | Plan walk order and failure restore |
|
|
77
|
-
| `reference/proposal-format.md` | Human-readable plan field contract |
|
|
78
|
-
| `reference/splitting-principles.md` | Vertical-slice invariants |
|
|
79
|
-
| `reference/path-layers.md` | Layer hints (context only) |
|
|
80
|
-
| `reference/split-further-loop.md` | Re-split loop until leaves fit review |
|
|
81
|
-
|
|
82
|
-
## Folder map
|
|
83
|
-
|
|
84
|
-
- `scripts/` — analyzer, categorize helpers, plan verifier, plan contract, constants, tests
|
|
85
|
-
- `reference/` — vertical-slice principles, layer hints, plan field contract
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# Path layers — context only
|
|
2
|
-
|
|
3
|
-
Path-layer labels (database, contracts, backend, frontend, tests, config) are
|
|
4
|
-
**judgment aids**, not hard gates. Vertical slice assignment overrides layer
|
|
5
|
-
buckets when tests must travel with the modules they cover.
|
|
6
|
-
|
|
7
|
-
| Layer hint | Typical paths |
|
|
8
|
-
|---|---|
|
|
9
|
-
| database | `**/migrations/**`, `**/schema.prisma` |
|
|
10
|
-
| contracts | `**/api/**/types*`, OpenAPI specs |
|
|
11
|
-
| backend | `**/services/**`, `**/handlers/**` |
|
|
12
|
-
| frontend | `**/components/**`, `**/*.tsx` |
|
|
13
|
-
| tests | `**/test_*.py`, `**/*_test.py`, `**/*.test.*` |
|
|
14
|
-
| config | lockfiles, `package.json`, CI workflows |
|
|
15
|
-
|
|
16
|
-
File count and layer count inform review; they never alone block a plan.
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# Split-plan proposal format
|
|
2
|
-
|
|
3
|
-
Machine-readable plan documents use `schema_version` `1` with:
|
|
4
|
-
|
|
5
|
-
| Field | Meaning |
|
|
6
|
-
|---|---|
|
|
7
|
-
| `source_commit` | Exact SHA the changed-file list was taken from |
|
|
8
|
-
| `all_changed_paths` | Full path set for that commit |
|
|
9
|
-
| `all_slices` | Ordered slices after layer sort |
|
|
10
|
-
|
|
11
|
-
Each slice carries `id`, `title` (exactly one conventional prefix), `layer`
|
|
12
|
-
(one of `config` / `backend` / `frontend` / `tests` / `docs` / `other`), and
|
|
13
|
-
`all_paths`. Every path in `all_changed_paths` is assigned to exactly one
|
|
14
|
-
slice. Titles are normalized with `split_pr_title.normalize_split_title`.
|
|
15
|
-
Layer tokens follow `ALL_LAYER_ORDER` in `packages/claude-dev-env/.agents/skills-archived/split-pr/scripts/config/plan_constants.py`.
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
# Split-further loop
|
|
2
|
-
|
|
3
|
-
After the first vertical plan lands as draft slices, re-check each leaf PR:
|
|
4
|
-
|
|
5
|
-
1. Run the hand-written line analyzer (`scripts/analyze_pr.py`).
|
|
6
|
-
2. If hand-written lines ≥ 200, record a split-analysis artifact.
|
|
7
|
-
3. If hand-written lines ≥ 600 and no atomic exception, propose a further
|
|
8
|
-
vertical split (tests still co-located with behavior).
|
|
9
|
-
4. Stop when every leaf fits review or carries a Fable-approved atomic
|
|
10
|
-
exception.
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# Splitting principles — vertical slices
|
|
2
|
-
|
|
3
|
-
A split plan groups changed paths into **semantic vertical slices**, not layer
|
|
4
|
-
silos. Each slice tells one reviewable story and keeps related tests beside the
|
|
5
|
-
behavior they cover.
|
|
6
|
-
|
|
7
|
-
## Invariants
|
|
8
|
-
|
|
9
|
-
1. **Test co-location.** A behavior change and its related tests share one
|
|
10
|
-
slice. Tests do not land in a later dedicated-test slice by default.
|
|
11
|
-
2. **One story per slice.** Each slice has a single review focus (one vertical
|
|
12
|
-
capability or one coherent subsystem story).
|
|
13
|
-
3. **Unique path ownership.** Every changed path appears in exactly one slice.
|
|
14
|
-
4. **Green intermediate.** Each stacked intermediate must collect and run the
|
|
15
|
-
tests that belong to that slice and its bases.
|
|
16
|
-
5. **Preparatory refactors first.** Substantial refactors that do not change
|
|
17
|
-
product behavior occupy earlier preparatory slices, not mixed with behavior.
|
|
18
|
-
6. **Dependency bases encode merge order.** Slice N+1 bases on slice N when it
|
|
19
|
-
depends on N's definitions.
|
|
20
|
-
|
|
21
|
-
## Fable gates
|
|
22
|
-
|
|
23
|
-
Standing Fable approval is required for:
|
|
24
|
-
|
|
25
|
-
- a semantic re-bucket that moves paths across slices after the first plan
|
|
26
|
-
- an oversized atomic exception that keeps an unsplittable surface as one PR
|
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Analyze PR churn with the hand-written line metric and 200/600 gates.
|
|
3
|
-
|
|
4
|
-
::
|
|
5
|
-
|
|
6
|
-
python analyze_pr.py --files-json files.json --pretty
|
|
7
|
-
{"hand_written_lines": 200, "requires_split_analysis": true, ...}
|
|
8
|
-
|
|
9
|
-
Uses paginated ``gh api .../pulls/N/files`` unless ``--files-json`` is offline.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
import argparse
|
|
15
|
-
import json
|
|
16
|
-
import subprocess
|
|
17
|
-
import sys
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
|
|
20
|
-
from categorize_files import annotate_files, sum_churn_by_class
|
|
21
|
-
from config.split_pr_constants import (
|
|
22
|
-
CHURN_CLASS_HAND_WRITTEN,
|
|
23
|
-
DEFAULT_SPLIT_HAND_WRITTEN_LINE_THRESHOLD,
|
|
24
|
-
EXIT_CODE_FAILURE,
|
|
25
|
-
EXIT_CODE_SUCCESS,
|
|
26
|
-
FILE_KEY_ADDITIONS,
|
|
27
|
-
FILE_KEY_CHANGED_LINES,
|
|
28
|
-
FILE_KEY_CHURN_CLASS,
|
|
29
|
-
FILE_KEY_DELETIONS,
|
|
30
|
-
FILE_KEY_PATH,
|
|
31
|
-
GH_API_SUBCOMMAND,
|
|
32
|
-
GH_COMMAND,
|
|
33
|
-
GH_FILE_ADDITIONS,
|
|
34
|
-
GH_FILE_DELETIONS,
|
|
35
|
-
GH_FILE_FILENAME,
|
|
36
|
-
GH_FILE_PATH,
|
|
37
|
-
GH_JSON_FLAG,
|
|
38
|
-
GH_NAME_WITH_OWNER_FIELD,
|
|
39
|
-
GH_PAGINATE_FLAG,
|
|
40
|
-
GH_PULLS_FILES_PATH_TEMPLATE,
|
|
41
|
-
GH_REPO_SUBCOMMAND,
|
|
42
|
-
GH_SLURP_FLAG,
|
|
43
|
-
GH_VIEW,
|
|
44
|
-
JSON_INDENT_SPACES,
|
|
45
|
-
PAYLOAD_KEY_ALL_FILES,
|
|
46
|
-
PAYLOAD_KEY_ATOMIC_EXCEPTION,
|
|
47
|
-
PAYLOAD_KEY_DEFAULT_SPLIT,
|
|
48
|
-
PAYLOAD_KEY_ERROR,
|
|
49
|
-
PAYLOAD_KEY_EXCLUDED_CHURN_LINES,
|
|
50
|
-
PAYLOAD_KEY_FABLE_VERDICT,
|
|
51
|
-
PAYLOAD_KEY_FILE_COUNT,
|
|
52
|
-
PAYLOAD_KEY_HAND_WRITTEN_LINES,
|
|
53
|
-
PAYLOAD_KEY_REASON,
|
|
54
|
-
PAYLOAD_KEY_REQUIRES_SPLIT_ANALYSIS,
|
|
55
|
-
SPLIT_ANALYSIS_HAND_WRITTEN_LINE_THRESHOLD,
|
|
56
|
-
UTF8_ENCODING,
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
JsonObject = dict[str, object]
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def build_analysis_from_files(
|
|
63
|
-
all_file_records: list[JsonObject],
|
|
64
|
-
*,
|
|
65
|
-
atomic_exception_reason: str | None = None,
|
|
66
|
-
fable_verdict: str | None = None,
|
|
67
|
-
) -> JsonObject:
|
|
68
|
-
"""Build the hand-written line analysis document.
|
|
69
|
-
|
|
70
|
-
Args:
|
|
71
|
-
all_file_records: Raw path/additions/deletions maps.
|
|
72
|
-
atomic_exception_reason: When set at 600+, records an atomic exception.
|
|
73
|
-
fable_verdict: Standing Fable verdict token required with the exception.
|
|
74
|
-
|
|
75
|
-
Returns:
|
|
76
|
-
Analysis payload with thresholds and optional atomic_exception.
|
|
77
|
-
|
|
78
|
-
Raises:
|
|
79
|
-
ValueError: When an atomic exception is set without a Fable verdict.
|
|
80
|
-
"""
|
|
81
|
-
all_annotated = annotate_files(all_file_records)
|
|
82
|
-
hand_written_lines = sum_churn_by_class(
|
|
83
|
-
all_annotated, churn_class=CHURN_CLASS_HAND_WRITTEN
|
|
84
|
-
)
|
|
85
|
-
excluded_churn_lines = 0
|
|
86
|
-
for each_record in all_annotated:
|
|
87
|
-
if each_record.get(FILE_KEY_CHURN_CLASS) != CHURN_CLASS_HAND_WRITTEN:
|
|
88
|
-
excluded_churn_lines += int(
|
|
89
|
-
each_record.get(FILE_KEY_CHANGED_LINES, 0) or 0
|
|
90
|
-
)
|
|
91
|
-
file_count = len(all_annotated)
|
|
92
|
-
requires_split_analysis = (
|
|
93
|
-
hand_written_lines >= SPLIT_ANALYSIS_HAND_WRITTEN_LINE_THRESHOLD
|
|
94
|
-
)
|
|
95
|
-
default_split = hand_written_lines >= DEFAULT_SPLIT_HAND_WRITTEN_LINE_THRESHOLD
|
|
96
|
-
analysis: JsonObject = {
|
|
97
|
-
PAYLOAD_KEY_HAND_WRITTEN_LINES: hand_written_lines,
|
|
98
|
-
PAYLOAD_KEY_EXCLUDED_CHURN_LINES: excluded_churn_lines,
|
|
99
|
-
PAYLOAD_KEY_FILE_COUNT: file_count,
|
|
100
|
-
PAYLOAD_KEY_REQUIRES_SPLIT_ANALYSIS: requires_split_analysis,
|
|
101
|
-
PAYLOAD_KEY_DEFAULT_SPLIT: default_split,
|
|
102
|
-
PAYLOAD_KEY_ALL_FILES: all_annotated,
|
|
103
|
-
PAYLOAD_KEY_ATOMIC_EXCEPTION: None,
|
|
104
|
-
}
|
|
105
|
-
if default_split and atomic_exception_reason:
|
|
106
|
-
if not fable_verdict:
|
|
107
|
-
raise ValueError(
|
|
108
|
-
"atomic exception at 600+ hand-written lines requires a "
|
|
109
|
-
"standing Fable verdict"
|
|
110
|
-
)
|
|
111
|
-
analysis[PAYLOAD_KEY_ATOMIC_EXCEPTION] = {
|
|
112
|
-
PAYLOAD_KEY_REASON: atomic_exception_reason,
|
|
113
|
-
PAYLOAD_KEY_FABLE_VERDICT: fable_verdict,
|
|
114
|
-
}
|
|
115
|
-
analysis[PAYLOAD_KEY_DEFAULT_SPLIT] = False
|
|
116
|
-
return analysis
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def _file_records_from_gh(raw_files: object) -> list[JsonObject]:
|
|
120
|
-
if not isinstance(raw_files, list):
|
|
121
|
-
return []
|
|
122
|
-
all_file_records: list[JsonObject] = []
|
|
123
|
-
for each_file in raw_files:
|
|
124
|
-
if not isinstance(each_file, dict):
|
|
125
|
-
continue
|
|
126
|
-
path = each_file.get(GH_FILE_PATH) or each_file.get(GH_FILE_FILENAME)
|
|
127
|
-
if not path:
|
|
128
|
-
continue
|
|
129
|
-
all_file_records.append(
|
|
130
|
-
{
|
|
131
|
-
FILE_KEY_PATH: str(path),
|
|
132
|
-
FILE_KEY_ADDITIONS: int(each_file.get(GH_FILE_ADDITIONS, 0) or 0),
|
|
133
|
-
FILE_KEY_DELETIONS: int(each_file.get(GH_FILE_DELETIONS, 0) or 0),
|
|
134
|
-
}
|
|
135
|
-
)
|
|
136
|
-
return all_file_records
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def _flatten_paginated_file_pages(payload: object) -> list[object]:
|
|
140
|
-
if not isinstance(payload, list):
|
|
141
|
-
return []
|
|
142
|
-
all_files: list[object] = []
|
|
143
|
-
for each_page in payload:
|
|
144
|
-
if isinstance(each_page, list):
|
|
145
|
-
all_files.extend(each_page)
|
|
146
|
-
elif isinstance(each_page, dict):
|
|
147
|
-
all_files.append(each_page)
|
|
148
|
-
return all_files
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def _load_files_json(files_json_path: Path) -> list[JsonObject]:
|
|
152
|
-
payload = json.loads(files_json_path.read_text(encoding=UTF8_ENCODING))
|
|
153
|
-
if not isinstance(payload, list):
|
|
154
|
-
raise ValueError("files-json must be a JSON array")
|
|
155
|
-
return _file_records_from_gh(payload)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def _resolve_repo_name(repo: str | None) -> str:
|
|
159
|
-
if repo:
|
|
160
|
-
return repo
|
|
161
|
-
completed = subprocess.run(
|
|
162
|
-
[
|
|
163
|
-
GH_COMMAND,
|
|
164
|
-
GH_REPO_SUBCOMMAND,
|
|
165
|
-
GH_VIEW,
|
|
166
|
-
GH_JSON_FLAG,
|
|
167
|
-
GH_NAME_WITH_OWNER_FIELD,
|
|
168
|
-
],
|
|
169
|
-
capture_output=True,
|
|
170
|
-
text=True,
|
|
171
|
-
check=False,
|
|
172
|
-
encoding=UTF8_ENCODING,
|
|
173
|
-
)
|
|
174
|
-
if completed.returncode != EXIT_CODE_SUCCESS:
|
|
175
|
-
raise RuntimeError(
|
|
176
|
-
completed.stderr.strip()
|
|
177
|
-
or "gh repo view failed; pass --repo owner/name"
|
|
178
|
-
)
|
|
179
|
-
payload = json.loads(completed.stdout)
|
|
180
|
-
name_with_owner = payload.get(GH_NAME_WITH_OWNER_FIELD)
|
|
181
|
-
if not name_with_owner:
|
|
182
|
-
raise RuntimeError("gh repo view returned no nameWithOwner; pass --repo")
|
|
183
|
-
return str(name_with_owner)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def _fetch_pr_files(pr_number: int, repo: str | None) -> list[JsonObject]:
|
|
187
|
-
resolved_repo = _resolve_repo_name(repo)
|
|
188
|
-
api_path = GH_PULLS_FILES_PATH_TEMPLATE.format(
|
|
189
|
-
repo=resolved_repo,
|
|
190
|
-
pr_number=pr_number,
|
|
191
|
-
)
|
|
192
|
-
all_command = [
|
|
193
|
-
GH_COMMAND,
|
|
194
|
-
GH_API_SUBCOMMAND,
|
|
195
|
-
api_path,
|
|
196
|
-
GH_PAGINATE_FLAG,
|
|
197
|
-
GH_SLURP_FLAG,
|
|
198
|
-
]
|
|
199
|
-
completed = subprocess.run(
|
|
200
|
-
all_command,
|
|
201
|
-
capture_output=True,
|
|
202
|
-
text=True,
|
|
203
|
-
check=False,
|
|
204
|
-
encoding=UTF8_ENCODING,
|
|
205
|
-
)
|
|
206
|
-
if completed.returncode != EXIT_CODE_SUCCESS:
|
|
207
|
-
raise RuntimeError(completed.stderr.strip() or "gh api pulls files failed")
|
|
208
|
-
payload = json.loads(completed.stdout)
|
|
209
|
-
return _file_records_from_gh(_flatten_paginated_file_pages(payload))
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
def _analysis_for_cli(
|
|
213
|
-
all_file_records: list[JsonObject],
|
|
214
|
-
exception_reason: str | None,
|
|
215
|
-
standing_verdict: str | None,
|
|
216
|
-
) -> JsonObject:
|
|
217
|
-
"""Map CLI flag values onto the analysis builder kwargs."""
|
|
218
|
-
return build_analysis_from_files(
|
|
219
|
-
all_file_records,
|
|
220
|
-
atomic_exception_reason=exception_reason,
|
|
221
|
-
fable_verdict=standing_verdict,
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def main() -> int:
|
|
226
|
-
"""CLI entry: emit analysis JSON to stdout.
|
|
227
|
-
|
|
228
|
-
Returns:
|
|
229
|
-
Process exit code.
|
|
230
|
-
|
|
231
|
-
Raises:
|
|
232
|
-
Does not raise; converts analysis failures into a JSON error payload.
|
|
233
|
-
"""
|
|
234
|
-
parser = argparse.ArgumentParser(description="Hand-written line PR analyzer")
|
|
235
|
-
parser.add_argument("--pr", type=int, default=0, help="PR number for gh fetch")
|
|
236
|
-
parser.add_argument("--repo", default=None, help="owner/name for gh")
|
|
237
|
-
parser.add_argument(
|
|
238
|
-
"--files-json",
|
|
239
|
-
type=Path,
|
|
240
|
-
default=None,
|
|
241
|
-
help="Offline files payload (tests); skips gh",
|
|
242
|
-
)
|
|
243
|
-
parser.add_argument(
|
|
244
|
-
"--atomic-exception-reason",
|
|
245
|
-
default=None,
|
|
246
|
-
help="Unsplittable reason when keeping one PR at 600+",
|
|
247
|
-
)
|
|
248
|
-
parser.add_argument(
|
|
249
|
-
"--fable-verdict",
|
|
250
|
-
default=None,
|
|
251
|
-
help="Standing Fable verdict token for the atomic exception",
|
|
252
|
-
)
|
|
253
|
-
parser.add_argument("--pretty", action="store_true", help="Indent JSON output")
|
|
254
|
-
parsed = parser.parse_args()
|
|
255
|
-
try:
|
|
256
|
-
if parsed.files_json is not None:
|
|
257
|
-
all_file_records = _load_files_json(parsed.files_json)
|
|
258
|
-
elif parsed.pr > 0:
|
|
259
|
-
all_file_records = _fetch_pr_files(parsed.pr, parsed.repo)
|
|
260
|
-
else:
|
|
261
|
-
raise ValueError("provide --pr or --files-json")
|
|
262
|
-
analysis = _analysis_for_cli(
|
|
263
|
-
all_file_records,
|
|
264
|
-
parsed.atomic_exception_reason,
|
|
265
|
-
parsed.fable_verdict,
|
|
266
|
-
)
|
|
267
|
-
except (OSError, ValueError, RuntimeError, json.JSONDecodeError) as error:
|
|
268
|
-
print(
|
|
269
|
-
json.dumps({PAYLOAD_KEY_ERROR: str(error)}),
|
|
270
|
-
file=sys.stdout,
|
|
271
|
-
)
|
|
272
|
-
return EXIT_CODE_FAILURE
|
|
273
|
-
indent = JSON_INDENT_SPACES if parsed.pretty else None
|
|
274
|
-
print(json.dumps(analysis, indent=indent))
|
|
275
|
-
return EXIT_CODE_SUCCESS
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if __name__ == "__main__":
|
|
279
|
-
raise SystemExit(main())
|