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
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
"""Classify changed files as hand-written or excluded churn.
|
|
2
|
-
|
|
3
|
-
::
|
|
4
|
-
|
|
5
|
-
classify_path_churn("src/app.py") # hand_written
|
|
6
|
-
classify_path_churn("package-lock.json") # lockfile
|
|
7
|
-
classify_path_churn("vendor/lib.js") # vendor
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
|
|
14
|
-
from config.split_pr_constants import (
|
|
15
|
-
ALL_GENERATED_PATH_MARKERS,
|
|
16
|
-
ALL_LOCKFILE_NAMES,
|
|
17
|
-
ALL_MINIFIED_SUFFIXES,
|
|
18
|
-
ALL_VENDOR_PATH_MARKERS,
|
|
19
|
-
CHURN_CLASS_GENERATED,
|
|
20
|
-
CHURN_CLASS_HAND_WRITTEN,
|
|
21
|
-
CHURN_CLASS_LOCKFILE,
|
|
22
|
-
CHURN_CLASS_MINIFIED,
|
|
23
|
-
CHURN_CLASS_VENDOR,
|
|
24
|
-
FILE_KEY_ADDITIONS,
|
|
25
|
-
FILE_KEY_CHANGED_LINES,
|
|
26
|
-
FILE_KEY_CHURN_CLASS,
|
|
27
|
-
FILE_KEY_DELETIONS,
|
|
28
|
-
FILE_KEY_PATH,
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
JsonObject = dict[str, object]
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def classify_path_churn(file_path: str) -> str:
|
|
35
|
-
"""Return the churn class for a repository-relative path.
|
|
36
|
-
|
|
37
|
-
Args:
|
|
38
|
-
file_path: Path relative to the repository root.
|
|
39
|
-
|
|
40
|
-
Returns:
|
|
41
|
-
One of hand_written, generated, vendor, minified, or lockfile.
|
|
42
|
-
"""
|
|
43
|
-
normalized = file_path.replace("\\", "/")
|
|
44
|
-
basename = Path(normalized).name
|
|
45
|
-
if basename in ALL_LOCKFILE_NAMES:
|
|
46
|
-
return CHURN_CLASS_LOCKFILE
|
|
47
|
-
lowered = normalized.lower()
|
|
48
|
-
for each_suffix in ALL_MINIFIED_SUFFIXES:
|
|
49
|
-
if lowered.endswith(each_suffix):
|
|
50
|
-
return CHURN_CLASS_MINIFIED
|
|
51
|
-
path_with_slashes = f"/{lowered}/"
|
|
52
|
-
for each_marker in ALL_VENDOR_PATH_MARKERS:
|
|
53
|
-
if each_marker in path_with_slashes:
|
|
54
|
-
return CHURN_CLASS_VENDOR
|
|
55
|
-
for each_marker in ALL_GENERATED_PATH_MARKERS:
|
|
56
|
-
if each_marker in lowered or each_marker.strip("/") in lowered.split("/"):
|
|
57
|
-
return CHURN_CLASS_GENERATED
|
|
58
|
-
return CHURN_CLASS_HAND_WRITTEN
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def annotate_files(all_file_records: list[JsonObject]) -> list[JsonObject]:
|
|
62
|
-
"""Annotate each file record with churn class and changed line count.
|
|
63
|
-
|
|
64
|
-
Args:
|
|
65
|
-
all_file_records: Maps with path, additions, deletions.
|
|
66
|
-
|
|
67
|
-
Returns:
|
|
68
|
-
Annotated records (new list; inputs not mutated).
|
|
69
|
-
"""
|
|
70
|
-
all_annotated: list[JsonObject] = []
|
|
71
|
-
for each_record in all_file_records:
|
|
72
|
-
path = str(each_record.get(FILE_KEY_PATH, "") or "")
|
|
73
|
-
additions = int(each_record.get(FILE_KEY_ADDITIONS, 0) or 0)
|
|
74
|
-
deletions = int(each_record.get(FILE_KEY_DELETIONS, 0) or 0)
|
|
75
|
-
changed_lines = max(0, additions) + max(0, deletions)
|
|
76
|
-
all_annotated.append(
|
|
77
|
-
{
|
|
78
|
-
FILE_KEY_PATH: path,
|
|
79
|
-
FILE_KEY_ADDITIONS: max(0, additions),
|
|
80
|
-
FILE_KEY_DELETIONS: max(0, deletions),
|
|
81
|
-
FILE_KEY_CHANGED_LINES: changed_lines,
|
|
82
|
-
FILE_KEY_CHURN_CLASS: classify_path_churn(path),
|
|
83
|
-
}
|
|
84
|
-
)
|
|
85
|
-
return all_annotated
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def sum_churn_by_class(
|
|
89
|
-
all_annotated: list[JsonObject],
|
|
90
|
-
*,
|
|
91
|
-
churn_class: str,
|
|
92
|
-
) -> int:
|
|
93
|
-
"""Sum changed lines for records matching ``churn_class``.
|
|
94
|
-
|
|
95
|
-
Args:
|
|
96
|
-
all_annotated: Annotated file records.
|
|
97
|
-
churn_class: Target class name.
|
|
98
|
-
|
|
99
|
-
Returns:
|
|
100
|
-
Total changed lines in that class.
|
|
101
|
-
"""
|
|
102
|
-
total = 0
|
|
103
|
-
for each_record in all_annotated:
|
|
104
|
-
if each_record.get(FILE_KEY_CHURN_CLASS) == churn_class:
|
|
105
|
-
total += int(each_record.get(FILE_KEY_CHANGED_LINES, 0) or 0)
|
|
106
|
-
return total
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""Config package for split-pr analyzer constants."""
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
"""Constants for deterministic split-slice dependency graphs."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from config.plan_constants import ALL_LAYER_ORDER
|
|
6
|
-
|
|
7
|
-
GRAPH_KEY_NODES: str = "nodes"
|
|
8
|
-
GRAPH_KEY_EDGES: str = "edges"
|
|
9
|
-
GRAPH_KEY_ORDER: str = "topological_order"
|
|
10
|
-
EDGE_KEY_FROM: str = "from"
|
|
11
|
-
EDGE_KEY_TO: str = "to"
|
|
12
|
-
ALL_LAYER_RANK_BY_NAME: dict[str, int] = {
|
|
13
|
-
each_layer: each_index for each_index, each_layer in enumerate(ALL_LAYER_ORDER)
|
|
14
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
"""Constants for safe local Git slice materialization."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
GIT_COMMAND: str = "git"
|
|
6
|
-
ALL_GIT_STATUS_PORCELAIN: tuple[str, ...] = ("status", "--porcelain")
|
|
7
|
-
ALL_GIT_REV_PARSE_HEAD: tuple[str, ...] = ("rev-parse", "HEAD")
|
|
8
|
-
GIT_CHECKOUT: str = "checkout"
|
|
9
|
-
GIT_ADD: str = "add"
|
|
10
|
-
GIT_COMMIT: str = "commit"
|
|
11
|
-
GIT_COMMIT_MESSAGE_FLAG: str = "-m"
|
|
12
|
-
ALL_GIT_RESTORE_STAGED_WORKTREE: tuple[str, ...] = (
|
|
13
|
-
"restore",
|
|
14
|
-
"--staged",
|
|
15
|
-
"--worktree",
|
|
16
|
-
".",
|
|
17
|
-
)
|
|
18
|
-
ALL_GIT_CLEAN_FORCE_DIR: tuple[str, ...] = ("clean", "-fd")
|
|
19
|
-
RECORD_KEY_COMMIT_SHA: str = "commit_sha"
|
|
20
|
-
RECORD_KEY_BASE_SHA: str = "base_sha"
|
|
21
|
-
RECORD_KEY_ALL_PATHS: str = "all_paths"
|
|
22
|
-
RECORD_KEY_EXIT_CODE: str = "exit_code"
|
|
23
|
-
ERROR_DIRTY_TREE: str = "working tree is dirty; refuse materialization"
|
|
24
|
-
ERROR_COMMIT_FAILED: str = "git commit failed for slice"
|
|
25
|
-
ERROR_CHECKOUT_FAILED: str = "git checkout restore failed"
|
|
26
|
-
ERROR_ADD_FAILED: str = "git add failed for slice paths"
|
|
27
|
-
ERROR_EMPTY_SLICE_PATHS: str = "slice path list is empty"
|
|
28
|
-
ERROR_REV_PARSE_FAILED: str = "rev-parse HEAD failed"
|
|
29
|
-
ERROR_STATUS_FAILED: str = "git status failed"
|
|
30
|
-
ERROR_RESTORE_FAILED: str = "git restore failed"
|
|
31
|
-
ERROR_CLEAN_FAILED: str = "git clean failed"
|
|
32
|
-
ERROR_BASE_MISMATCH_TEMPLATE: str = (
|
|
33
|
-
"HEAD {head_sha} does not match expected base {expected_base_sha}"
|
|
34
|
-
)
|
|
35
|
-
GIT_FORCE_FLAG: str = "--force"
|
|
36
|
-
GIT_PATHSPEC_SEPARATOR: str = "--"
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
"""Constants for review-budget packing of categorized files."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from config.plan_constants import ALL_LAYER_ORDER
|
|
6
|
-
from config.split_pr_constants import (
|
|
7
|
-
DEFAULT_SPLIT_HAND_WRITTEN_LINE_THRESHOLD,
|
|
8
|
-
SPLIT_ANALYSIS_HAND_WRITTEN_LINE_THRESHOLD,
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
REVIEW_BUDGET_HAND_WRITTEN_LINES: int = SPLIT_ANALYSIS_HAND_WRITTEN_LINE_THRESHOLD
|
|
12
|
-
HARD_CAP_HAND_WRITTEN_LINES: int = DEFAULT_SPLIT_HAND_WRITTEN_LINE_THRESHOLD
|
|
13
|
-
SLICE_ID_TEMPLATE: str = "{index:02d}-{layer}"
|
|
14
|
-
SLICE_TITLE_TEMPLATE: str = "chore: {layer} slice {index}"
|
|
15
|
-
(
|
|
16
|
-
LAYER_CONFIG,
|
|
17
|
-
LAYER_BACKEND,
|
|
18
|
-
LAYER_FRONTEND,
|
|
19
|
-
LAYER_TESTS,
|
|
20
|
-
LAYER_DOCS,
|
|
21
|
-
LAYER_OTHER,
|
|
22
|
-
) = ALL_LAYER_ORDER
|
|
23
|
-
ALL_TEST_PATH_MARKERS: tuple[str, ...] = (
|
|
24
|
-
"/tests/",
|
|
25
|
-
"/test/",
|
|
26
|
-
"test_",
|
|
27
|
-
"_test.",
|
|
28
|
-
".test.",
|
|
29
|
-
".spec.",
|
|
30
|
-
)
|
|
31
|
-
ALL_DOC_SUFFIXES: tuple[str, ...] = (".md", ".rst", ".txt")
|
|
32
|
-
ALL_CONFIG_BASENAMES: frozenset[str] = frozenset(
|
|
33
|
-
{
|
|
34
|
-
"package.json",
|
|
35
|
-
"pyproject.toml",
|
|
36
|
-
"setup.cfg",
|
|
37
|
-
"tsconfig.json",
|
|
38
|
-
".eslintrc",
|
|
39
|
-
"ruff.toml",
|
|
40
|
-
}
|
|
41
|
-
)
|
|
42
|
-
ALL_CONFIG_PATH_MARKERS: tuple[str, ...] = ("/config/", "/.github/")
|
|
43
|
-
ALL_FRONTEND_SUFFIXES: tuple[str, ...] = (
|
|
44
|
-
".tsx",
|
|
45
|
-
".jsx",
|
|
46
|
-
".css",
|
|
47
|
-
".scss",
|
|
48
|
-
".vue",
|
|
49
|
-
".svelte",
|
|
50
|
-
)
|
|
51
|
-
ALL_BACKEND_SUFFIXES: tuple[str, ...] = (
|
|
52
|
-
".py",
|
|
53
|
-
".mjs",
|
|
54
|
-
".js",
|
|
55
|
-
".ts",
|
|
56
|
-
".go",
|
|
57
|
-
".rs",
|
|
58
|
-
)
|
|
59
|
-
EXCLUDED_CHURN_LAYER_LABEL: str = "excluded-churn"
|
|
60
|
-
EMPTY_HAND_WRITTEN_TITLE: str = "chore: empty hand-written set"
|
|
61
|
-
EMPTY_HAND_WRITTEN_SLICE_ID: str = "01-other"
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
"""Constants for split-plan records, layer order, and title normalization."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
PLAN_SCHEMA_VERSION: int = 1
|
|
6
|
-
PLAN_KEY_SCHEMA_VERSION: str = "schema_version"
|
|
7
|
-
PLAN_KEY_SOURCE_COMMIT: str = "source_commit"
|
|
8
|
-
PLAN_KEY_ALL_SLICES: str = "all_slices"
|
|
9
|
-
PLAN_KEY_ALL_CHANGED_PATHS: str = "all_changed_paths"
|
|
10
|
-
SLICE_KEY_ID: str = "id"
|
|
11
|
-
SLICE_KEY_TITLE: str = "title"
|
|
12
|
-
SLICE_KEY_LAYER: str = "layer"
|
|
13
|
-
SLICE_KEY_ALL_PATHS: str = "all_paths"
|
|
14
|
-
|
|
15
|
-
ALL_LAYER_ORDER: tuple[str, ...] = (
|
|
16
|
-
"config",
|
|
17
|
-
"backend",
|
|
18
|
-
"frontend",
|
|
19
|
-
"tests",
|
|
20
|
-
"docs",
|
|
21
|
-
"other",
|
|
22
|
-
)
|
|
23
|
-
ALL_LAYER_RANK_BY_NAME: dict[str, int] = {
|
|
24
|
-
each_layer: each_index for each_index, each_layer in enumerate(ALL_LAYER_ORDER)
|
|
25
|
-
}
|
|
26
|
-
UNKNOWN_LAYER_RANK: int = len(ALL_LAYER_ORDER)
|
|
27
|
-
|
|
28
|
-
CONVENTIONAL_PREFIX_PATTERN: str = (
|
|
29
|
-
r"^(?P<prefix>feat|fix|docs|chore|refactor|test|ci|perf|build|style|revert)"
|
|
30
|
-
r"(?:\([^)]+\))?!?:\s*"
|
|
31
|
-
)
|
|
32
|
-
TITLE_PREFIX_SEPARATOR: str = ": "
|
|
33
|
-
DEFAULT_TITLE_PREFIX: str = "chore"
|
|
34
|
-
DEFAULT_SLICE_LAYER: str = "other"
|
|
35
|
-
DEFAULT_EMPTY_TITLE_REMAINDER: str = "split slice"
|
|
36
|
-
UTF8_ENCODING: str = "utf-8"
|
|
37
|
-
GH_COMMAND: str = "gh"
|
|
38
|
-
GH_API: str = "api"
|
|
39
|
-
GH_PAGINATE_FLAG: str = "--paginate"
|
|
40
|
-
GH_SLURP_FLAG: str = "--slurp"
|
|
41
|
-
GH_PULLS_FILES_PATH_TEMPLATE: str = "repos/{owner}/{repo}/pulls/{pr_number}/files"
|
|
42
|
-
GH_REST_FILE_FILENAME: str = "filename"
|
|
43
|
-
FILE_KEY_PATH: str = "path"
|
|
44
|
-
FILE_KEY_ADDITIONS: str = "additions"
|
|
45
|
-
FILE_KEY_DELETIONS: str = "deletions"
|
|
46
|
-
FILE_KEY_SHA: str = "sha"
|
|
47
|
-
EXIT_CODE_SUCCESS: int = 0
|
|
48
|
-
PATH_SEPARATOR: str = "/"
|
|
49
|
-
VERIFY_PAYLOAD_KEY_OK: str = "ok"
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
"""Thresholds and path markers for the hand-written line PR analyzer.
|
|
2
|
-
|
|
3
|
-
::
|
|
4
|
-
|
|
5
|
-
hand_written_lines >= 200 -> requires_split_analysis
|
|
6
|
-
hand_written_lines >= 600 -> default_split (unless atomic_exception)
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
SPLIT_ANALYSIS_HAND_WRITTEN_LINE_THRESHOLD: int = 200
|
|
12
|
-
"""Hand-written lines that require a recorded split analysis."""
|
|
13
|
-
|
|
14
|
-
DEFAULT_SPLIT_HAND_WRITTEN_LINE_THRESHOLD: int = 600
|
|
15
|
-
"""Hand-written lines that default to multiple PR slices."""
|
|
16
|
-
|
|
17
|
-
EXIT_CODE_SUCCESS: int = 0
|
|
18
|
-
EXIT_CODE_FAILURE: int = 1
|
|
19
|
-
|
|
20
|
-
JSON_INDENT_SPACES: int = 2
|
|
21
|
-
UTF8_ENCODING: str = "utf-8"
|
|
22
|
-
|
|
23
|
-
CHURN_CLASS_HAND_WRITTEN: str = "hand_written"
|
|
24
|
-
CHURN_CLASS_GENERATED: str = "generated"
|
|
25
|
-
CHURN_CLASS_VENDOR: str = "vendor"
|
|
26
|
-
CHURN_CLASS_MINIFIED: str = "minified"
|
|
27
|
-
CHURN_CLASS_LOCKFILE: str = "lockfile"
|
|
28
|
-
|
|
29
|
-
ALL_GENERATED_PATH_MARKERS: tuple[str, ...] = (
|
|
30
|
-
"/generated/",
|
|
31
|
-
".generated.",
|
|
32
|
-
"/dist/",
|
|
33
|
-
"/build/",
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
ALL_VENDOR_PATH_MARKERS: tuple[str, ...] = (
|
|
37
|
-
"/vendor/",
|
|
38
|
-
"/third_party/",
|
|
39
|
-
"/node_modules/",
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
ALL_MINIFIED_SUFFIXES: tuple[str, ...] = (
|
|
43
|
-
".min.js",
|
|
44
|
-
".min.css",
|
|
45
|
-
".min.mjs",
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
ALL_LOCKFILE_NAMES: frozenset[str] = frozenset(
|
|
49
|
-
{
|
|
50
|
-
"package-lock.json",
|
|
51
|
-
"yarn.lock",
|
|
52
|
-
"pnpm-lock.yaml",
|
|
53
|
-
"poetry.lock",
|
|
54
|
-
"Cargo.lock",
|
|
55
|
-
"Pipfile.lock",
|
|
56
|
-
"composer.lock",
|
|
57
|
-
"Gemfile.lock",
|
|
58
|
-
}
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
PAYLOAD_KEY_HAND_WRITTEN_LINES: str = "hand_written_lines"
|
|
62
|
-
PAYLOAD_KEY_EXCLUDED_CHURN_LINES: str = "excluded_churn_lines"
|
|
63
|
-
PAYLOAD_KEY_FILE_COUNT: str = "file_count"
|
|
64
|
-
PAYLOAD_KEY_REQUIRES_SPLIT_ANALYSIS: str = "requires_split_analysis"
|
|
65
|
-
PAYLOAD_KEY_DEFAULT_SPLIT: str = "default_split"
|
|
66
|
-
PAYLOAD_KEY_ATOMIC_EXCEPTION: str = "atomic_exception"
|
|
67
|
-
PAYLOAD_KEY_ALL_FILES: str = "all_files"
|
|
68
|
-
PAYLOAD_KEY_ERROR: str = "error"
|
|
69
|
-
PAYLOAD_KEY_FABLE_VERDICT: str = "fable_verdict"
|
|
70
|
-
PAYLOAD_KEY_REASON: str = "reason"
|
|
71
|
-
|
|
72
|
-
FILE_KEY_PATH: str = "path"
|
|
73
|
-
FILE_KEY_ADDITIONS: str = "additions"
|
|
74
|
-
FILE_KEY_DELETIONS: str = "deletions"
|
|
75
|
-
FILE_KEY_CHURN_CLASS: str = "churn_class"
|
|
76
|
-
FILE_KEY_CHANGED_LINES: str = "changed_lines"
|
|
77
|
-
|
|
78
|
-
GH_COMMAND: str = "gh"
|
|
79
|
-
GH_API_SUBCOMMAND: str = "api"
|
|
80
|
-
GH_REPO_SUBCOMMAND: str = "repo"
|
|
81
|
-
GH_VIEW: str = "view"
|
|
82
|
-
GH_JSON_FLAG: str = "--json"
|
|
83
|
-
GH_PAGINATE_FLAG: str = "--paginate"
|
|
84
|
-
GH_SLURP_FLAG: str = "--slurp"
|
|
85
|
-
GH_NAME_WITH_OWNER_FIELD: str = "nameWithOwner"
|
|
86
|
-
GH_PULLS_FILES_PATH_TEMPLATE: str = "repos/{repo}/pulls/{pr_number}/files"
|
|
87
|
-
GH_FILE_PATH: str = "path"
|
|
88
|
-
GH_FILE_FILENAME: str = "filename"
|
|
89
|
-
GH_FILE_ADDITIONS: str = "additions"
|
|
90
|
-
GH_FILE_DELETIONS: str = "deletions"
|
|
91
|
-
|
|
92
|
-
PLAN_KEY_SLICES: str = "slices"
|
|
93
|
-
PLAN_KEY_PATHS: str = "paths"
|
|
94
|
-
PLAN_KEY_STORY: str = "story"
|
|
95
|
-
PLAN_KEY_IS_PREPARATORY_REFACTOR: str = "is_preparatory_refactor"
|
|
96
|
-
PLAN_KEY_IS_VALID: str = "is_valid"
|
|
97
|
-
PLAN_KEY_VIOLATIONS: str = "violations"
|
|
98
|
-
|
|
99
|
-
VIOLATION_MISSING_PATH: str = "path_missing_from_all_slices"
|
|
100
|
-
VIOLATION_DUPLICATE_PATH: str = "path_in_multiple_slices"
|
|
101
|
-
VIOLATION_TEST_WITHOUT_BEHAVIOR: str = "test_slice_missing_related_behavior"
|
|
102
|
-
VIOLATION_EMPTY_STORY: str = "slice_missing_story"
|
|
103
|
-
|
|
104
|
-
ALL_TEST_PATH_MARKERS: tuple[str, ...] = (
|
|
105
|
-
"/test_",
|
|
106
|
-
"_test.py",
|
|
107
|
-
".test.",
|
|
108
|
-
".spec.",
|
|
109
|
-
"/tests/",
|
|
110
|
-
)
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
"""Materialize an entire split plan as ordered local commits.
|
|
2
|
-
|
|
3
|
-
::
|
|
4
|
-
|
|
5
|
-
all_commit_records = materialize_plan_locally(repo, plan)
|
|
6
|
-
# one commit per slice in dependency topological order
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
|
|
13
|
-
from config.dependency_constants import GRAPH_KEY_ORDER
|
|
14
|
-
from config.git_operations_constants import RECORD_KEY_COMMIT_SHA
|
|
15
|
-
from config.plan_constants import (
|
|
16
|
-
PLAN_KEY_ALL_SLICES,
|
|
17
|
-
PLAN_KEY_SOURCE_COMMIT,
|
|
18
|
-
SLICE_KEY_ALL_PATHS,
|
|
19
|
-
SLICE_KEY_ID,
|
|
20
|
-
SLICE_KEY_TITLE,
|
|
21
|
-
)
|
|
22
|
-
from split_pr_dependency_graph import build_dependency_graph
|
|
23
|
-
from split_pr_git_operations import materialize_slice_commit, restore_repository_state
|
|
24
|
-
from split_pr_process_runner import ProcessRunner, run_process
|
|
25
|
-
from split_pr_script_types import JsonObject, validate_split_plan
|
|
26
|
-
from verify_plan import verify_split_plan_coverage
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def materialize_plan_locally(
|
|
30
|
-
repository_path: Path,
|
|
31
|
-
all_plan: JsonObject,
|
|
32
|
-
run: ProcessRunner = run_process,
|
|
33
|
-
) -> list[JsonObject]:
|
|
34
|
-
"""Validate the plan, then commit each slice in topological layer order.
|
|
35
|
-
|
|
36
|
-
Args:
|
|
37
|
-
repository_path: Git repository root.
|
|
38
|
-
all_plan: Split-plan document (schema v1).
|
|
39
|
-
run: Process runner for git invocations.
|
|
40
|
-
|
|
41
|
-
Returns:
|
|
42
|
-
List of per-slice materialization records in commit order.
|
|
43
|
-
|
|
44
|
-
Raises:
|
|
45
|
-
ValueError: When the plan fails coverage or graph validation.
|
|
46
|
-
RuntimeError: When any git step fails; repository is restored to
|
|
47
|
-
the plan source_commit on failure.
|
|
48
|
-
"""
|
|
49
|
-
verify_split_plan_coverage(all_plan)
|
|
50
|
-
validate_split_plan(all_plan)
|
|
51
|
-
source_commit = str(all_plan[PLAN_KEY_SOURCE_COMMIT])
|
|
52
|
-
all_slices = all_plan[PLAN_KEY_ALL_SLICES]
|
|
53
|
-
assert isinstance(all_slices, list)
|
|
54
|
-
graph = build_dependency_graph(all_slices)
|
|
55
|
-
order = graph[GRAPH_KEY_ORDER]
|
|
56
|
-
assert isinstance(order, list)
|
|
57
|
-
slice_by_id = {
|
|
58
|
-
str(each[SLICE_KEY_ID]): each
|
|
59
|
-
for each in all_slices
|
|
60
|
-
if isinstance(each, dict)
|
|
61
|
-
}
|
|
62
|
-
all_commit_records: list[JsonObject] = []
|
|
63
|
-
current_base = source_commit
|
|
64
|
-
try:
|
|
65
|
-
for each_slice_id in order:
|
|
66
|
-
each_slice = slice_by_id[str(each_slice_id)]
|
|
67
|
-
all_paths = each_slice[SLICE_KEY_ALL_PATHS]
|
|
68
|
-
assert isinstance(all_paths, list)
|
|
69
|
-
title = str(each_slice[SLICE_KEY_TITLE])
|
|
70
|
-
commit_record = materialize_slice_commit(
|
|
71
|
-
repository_path=repository_path,
|
|
72
|
-
all_slice_paths=[str(each) for each in all_paths],
|
|
73
|
-
commit_message=title,
|
|
74
|
-
expected_base_sha=current_base,
|
|
75
|
-
run=run,
|
|
76
|
-
)
|
|
77
|
-
all_commit_records.append(commit_record)
|
|
78
|
-
current_base = str(commit_record[RECORD_KEY_COMMIT_SHA])
|
|
79
|
-
except RuntimeError:
|
|
80
|
-
restore_repository_state(repository_path, source_commit, run)
|
|
81
|
-
raise
|
|
82
|
-
return all_commit_records
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
"""Pack categorized files into review-budgeted split slices.
|
|
2
|
-
|
|
3
|
-
::
|
|
4
|
-
|
|
5
|
-
plan = pack_files_into_slices(
|
|
6
|
-
source_commit="abc",
|
|
7
|
-
all_file_records=[{"path": "a.py", "additions": 50, "deletions": 0}],
|
|
8
|
-
)
|
|
9
|
-
# multi-file slices stay under the 200 hand-written budget;
|
|
10
|
-
# a single file may reach the 600 hard cap alone
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
|
|
17
|
-
from categorize_files import annotate_files
|
|
18
|
-
from config.packing_constants import (
|
|
19
|
-
ALL_BACKEND_SUFFIXES,
|
|
20
|
-
ALL_CONFIG_BASENAMES,
|
|
21
|
-
ALL_CONFIG_PATH_MARKERS,
|
|
22
|
-
ALL_DOC_SUFFIXES,
|
|
23
|
-
ALL_FRONTEND_SUFFIXES,
|
|
24
|
-
ALL_TEST_PATH_MARKERS,
|
|
25
|
-
EMPTY_HAND_WRITTEN_SLICE_ID,
|
|
26
|
-
EMPTY_HAND_WRITTEN_TITLE,
|
|
27
|
-
EXCLUDED_CHURN_LAYER_LABEL,
|
|
28
|
-
HARD_CAP_HAND_WRITTEN_LINES,
|
|
29
|
-
LAYER_BACKEND,
|
|
30
|
-
LAYER_CONFIG,
|
|
31
|
-
LAYER_DOCS,
|
|
32
|
-
LAYER_FRONTEND,
|
|
33
|
-
LAYER_OTHER,
|
|
34
|
-
LAYER_TESTS,
|
|
35
|
-
REVIEW_BUDGET_HAND_WRITTEN_LINES,
|
|
36
|
-
SLICE_ID_TEMPLATE,
|
|
37
|
-
SLICE_TITLE_TEMPLATE,
|
|
38
|
-
)
|
|
39
|
-
from config.plan_constants import (
|
|
40
|
-
SLICE_KEY_ALL_PATHS,
|
|
41
|
-
SLICE_KEY_ID,
|
|
42
|
-
SLICE_KEY_LAYER,
|
|
43
|
-
SLICE_KEY_TITLE,
|
|
44
|
-
)
|
|
45
|
-
from config.split_pr_constants import (
|
|
46
|
-
CHURN_CLASS_HAND_WRITTEN,
|
|
47
|
-
FILE_KEY_CHANGED_LINES,
|
|
48
|
-
FILE_KEY_CHURN_CLASS,
|
|
49
|
-
FILE_KEY_PATH,
|
|
50
|
-
)
|
|
51
|
-
from split_pr_script_types import JsonObject, build_split_plan, validate_split_plan
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def infer_path_layer(file_path: str) -> str:
|
|
55
|
-
"""Map a path to a packing layer token.
|
|
56
|
-
|
|
57
|
-
Args:
|
|
58
|
-
file_path: Repository-relative path.
|
|
59
|
-
|
|
60
|
-
Returns:
|
|
61
|
-
One of config/backend/frontend/tests/docs/other.
|
|
62
|
-
"""
|
|
63
|
-
normalized = file_path.replace("\\", "/").lower()
|
|
64
|
-
basename = Path(normalized).name
|
|
65
|
-
if basename in ALL_CONFIG_BASENAMES:
|
|
66
|
-
return LAYER_CONFIG
|
|
67
|
-
for each_marker in ALL_CONFIG_PATH_MARKERS:
|
|
68
|
-
if each_marker in f"/{normalized}/" or normalized.startswith(
|
|
69
|
-
each_marker.strip("/")
|
|
70
|
-
):
|
|
71
|
-
return LAYER_CONFIG
|
|
72
|
-
for each_marker in ALL_TEST_PATH_MARKERS:
|
|
73
|
-
if each_marker in normalized:
|
|
74
|
-
return LAYER_TESTS
|
|
75
|
-
for each_suffix in ALL_DOC_SUFFIXES:
|
|
76
|
-
if normalized.endswith(each_suffix):
|
|
77
|
-
return LAYER_DOCS
|
|
78
|
-
for each_suffix in ALL_FRONTEND_SUFFIXES:
|
|
79
|
-
if normalized.endswith(each_suffix):
|
|
80
|
-
return LAYER_FRONTEND
|
|
81
|
-
for each_suffix in ALL_BACKEND_SUFFIXES:
|
|
82
|
-
if normalized.endswith(each_suffix):
|
|
83
|
-
return LAYER_BACKEND
|
|
84
|
-
return LAYER_OTHER
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _slice_document(
|
|
88
|
-
slice_index: int,
|
|
89
|
-
layer_name: str,
|
|
90
|
-
all_paths: list[str],
|
|
91
|
-
title_layer: str | None = None,
|
|
92
|
-
) -> JsonObject:
|
|
93
|
-
title_token = title_layer if title_layer is not None else layer_name
|
|
94
|
-
slice_id = SLICE_ID_TEMPLATE.format(index=slice_index, layer=layer_name)
|
|
95
|
-
slice_title = SLICE_TITLE_TEMPLATE.format(layer=title_token, index=slice_index)
|
|
96
|
-
return {
|
|
97
|
-
SLICE_KEY_ID: slice_id,
|
|
98
|
-
SLICE_KEY_TITLE: slice_title,
|
|
99
|
-
SLICE_KEY_LAYER: layer_name,
|
|
100
|
-
SLICE_KEY_ALL_PATHS: list(all_paths),
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def _pack_hand_written_slices(
|
|
105
|
-
all_hand_written: list[JsonObject],
|
|
106
|
-
review_budget: int,
|
|
107
|
-
) -> list[JsonObject]:
|
|
108
|
-
all_slices: list[JsonObject] = []
|
|
109
|
-
open_layer: str | None = None
|
|
110
|
-
open_paths: list[str] = []
|
|
111
|
-
open_lines = 0
|
|
112
|
-
slice_index = 1
|
|
113
|
-
|
|
114
|
-
def flush_open() -> None:
|
|
115
|
-
nonlocal open_layer, open_paths, open_lines, slice_index
|
|
116
|
-
if open_layer is None or not open_paths:
|
|
117
|
-
open_layer = None
|
|
118
|
-
open_paths = []
|
|
119
|
-
open_lines = 0
|
|
120
|
-
return
|
|
121
|
-
all_slices.append(_slice_document(slice_index, open_layer, open_paths))
|
|
122
|
-
slice_index += 1
|
|
123
|
-
open_layer = None
|
|
124
|
-
open_paths = []
|
|
125
|
-
open_lines = 0
|
|
126
|
-
|
|
127
|
-
all_sorted = sorted(
|
|
128
|
-
all_hand_written,
|
|
129
|
-
key=lambda each: (
|
|
130
|
-
infer_path_layer(str(each[FILE_KEY_PATH])),
|
|
131
|
-
str(each[FILE_KEY_PATH]),
|
|
132
|
-
),
|
|
133
|
-
)
|
|
134
|
-
for each_file in all_sorted:
|
|
135
|
-
path = str(each_file[FILE_KEY_PATH])
|
|
136
|
-
layer = infer_path_layer(path)
|
|
137
|
-
lines = int(each_file.get(FILE_KEY_CHANGED_LINES, 0) or 0)
|
|
138
|
-
if lines > HARD_CAP_HAND_WRITTEN_LINES:
|
|
139
|
-
raise ValueError(
|
|
140
|
-
f"file {path} has {lines} hand-written lines above hard cap "
|
|
141
|
-
f"{HARD_CAP_HAND_WRITTEN_LINES}"
|
|
142
|
-
)
|
|
143
|
-
if open_layer is not None and (
|
|
144
|
-
layer != open_layer or open_lines + lines > review_budget
|
|
145
|
-
):
|
|
146
|
-
flush_open()
|
|
147
|
-
if open_layer is None:
|
|
148
|
-
open_layer = layer
|
|
149
|
-
open_paths = [path]
|
|
150
|
-
open_lines = lines
|
|
151
|
-
else:
|
|
152
|
-
open_paths.append(path)
|
|
153
|
-
open_lines += lines
|
|
154
|
-
flush_open()
|
|
155
|
-
return all_slices
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def pack_files_into_slices(
|
|
159
|
-
source_commit: str,
|
|
160
|
-
all_file_records: list[JsonObject],
|
|
161
|
-
review_budget: int = REVIEW_BUDGET_HAND_WRITTEN_LINES,
|
|
162
|
-
) -> JsonObject:
|
|
163
|
-
"""Annotate, layer, and pack files into budgeted slices, then build a plan.
|
|
164
|
-
|
|
165
|
-
Args:
|
|
166
|
-
source_commit: Exact commit the file list was taken from.
|
|
167
|
-
all_file_records: Path/additions/deletions maps.
|
|
168
|
-
review_budget: Max hand-written lines per slice (default 200).
|
|
169
|
-
|
|
170
|
-
Returns:
|
|
171
|
-
Validated split-plan document.
|
|
172
|
-
|
|
173
|
-
Raises:
|
|
174
|
-
ValueError: When a single file exceeds the hard 600-line cap alone,
|
|
175
|
-
or plan validation fails.
|
|
176
|
-
"""
|
|
177
|
-
if review_budget < 1:
|
|
178
|
-
raise ValueError("review_budget must be positive")
|
|
179
|
-
all_annotated = annotate_files(all_file_records)
|
|
180
|
-
all_changed_paths = [str(each[FILE_KEY_PATH]) for each in all_annotated]
|
|
181
|
-
all_hand_written = [
|
|
182
|
-
each
|
|
183
|
-
for each in all_annotated
|
|
184
|
-
if each.get(FILE_KEY_CHURN_CLASS) == CHURN_CLASS_HAND_WRITTEN
|
|
185
|
-
]
|
|
186
|
-
all_excluded = [
|
|
187
|
-
each
|
|
188
|
-
for each in all_annotated
|
|
189
|
-
if each.get(FILE_KEY_CHURN_CLASS) != CHURN_CLASS_HAND_WRITTEN
|
|
190
|
-
]
|
|
191
|
-
all_slices = _pack_hand_written_slices(all_hand_written, review_budget)
|
|
192
|
-
if all_excluded:
|
|
193
|
-
all_slices.append(
|
|
194
|
-
_slice_document(
|
|
195
|
-
len(all_slices) + 1,
|
|
196
|
-
LAYER_OTHER,
|
|
197
|
-
[str(each[FILE_KEY_PATH]) for each in all_excluded],
|
|
198
|
-
title_layer=EXCLUDED_CHURN_LAYER_LABEL,
|
|
199
|
-
)
|
|
200
|
-
)
|
|
201
|
-
if not all_slices and all_changed_paths:
|
|
202
|
-
all_slices.append(
|
|
203
|
-
{
|
|
204
|
-
SLICE_KEY_ID: EMPTY_HAND_WRITTEN_SLICE_ID,
|
|
205
|
-
SLICE_KEY_TITLE: EMPTY_HAND_WRITTEN_TITLE,
|
|
206
|
-
SLICE_KEY_LAYER: LAYER_OTHER,
|
|
207
|
-
SLICE_KEY_ALL_PATHS: list(all_changed_paths),
|
|
208
|
-
}
|
|
209
|
-
)
|
|
210
|
-
plan = build_split_plan(source_commit, all_changed_paths, all_slices)
|
|
211
|
-
validate_split_plan(plan)
|
|
212
|
-
return plan
|