claude-dev-env 2.9.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +2 -2
- package/_shared/advisor/CLAUDE.md +3 -2
- package/_shared/advisor/advisor-protocol.md +74 -108
- package/_shared/advisor/reference/advisor-block.md +37 -0
- package/_shared/advisor/reference/cli-chain.md +45 -0
- package/_shared/advisor/reference/consult-format.md +41 -0
- package/_shared/advisor/reference/lifecycle.md +21 -0
- package/_shared/advisor/reference/sol-rung.md +31 -0
- package/_shared/advisor/reference/spawn-walk-log.md +31 -0
- package/_shared/advisor/reference/third-party-bind.md +30 -0
- package/_shared/advisor/reference/warm-up.md +33 -0
- package/_shared/advisor/scripts/codex_sol_advisor.py +449 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +21 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +19 -17
- package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +28 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +32 -9
- package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +474 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +79 -0
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +39 -17
- package/_shared/advisor/scripts/tier_model_ids.py +24 -0
- package/_shared/pr-loop/CLAUDE.md +1 -1
- package/_shared/pr-loop/audit-contract.md +17 -6
- package/_shared/pr-loop/audit-reply-template.md +4 -4
- package/_shared/pr-loop/code-rules-gate.md +3 -5
- package/_shared/pr-loop/fix-protocol.md +2 -3
- package/_shared/pr-loop/gh-payloads.md +1 -1
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/README.md +1 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
- package/_shared/pr-loop/scripts/preflight.py +9 -4
- package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
- package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
- package/_shared/pr-loop/state-schema.md +5 -14
- package/agents/CLAUDE.md +2 -2
- package/agents/clean-coder.md +58 -548
- package/agents/code-quality-agent.md +10 -2
- package/agents/code-verifier.md +1 -1
- package/agents/test_agent_frontmatter.py +32 -40
- package/audit-rubrics/CLAUDE.md +2 -1
- package/audit-rubrics/audit-categories.json +704 -0
- package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
- package/bin/CLAUDE.md +16 -5
- package/bin/ever-shipped-skills.mjs +2 -0
- package/bin/install-plan.mjs +402 -0
- package/bin/install-transaction.mjs +455 -0
- package/bin/install.mjs +593 -147
- package/bin/install.plan.test.mjs +194 -0
- package/bin/install.profile-root.test.mjs +154 -0
- package/bin/install.profiles.test.mjs +253 -0
- package/bin/install.settings-defaults.test.mjs +200 -0
- package/bin/install.transaction.test.mjs +400 -0
- package/bin/install.uninstall-transaction.test.mjs +418 -0
- package/bin/merge_managed_permissions.mjs +130 -0
- package/bin/resolve-install-root.mjs +181 -0
- package/bin/select-install-targets.mjs +401 -0
- package/commands/CLAUDE.md +0 -2
- package/docs/references/CLAUDE.md +3 -1
- package/docs/references/advisor-tool.md +25 -7
- package/docs/references/prose-style-enforcement.md +25 -0
- package/docs/references/team-advisor-skill.md +3 -3
- package/docs/references/weak-executor-advisor.md +91 -0
- package/hooks/blocking/CLAUDE.md +6 -6
- package/hooks/blocking/_path_setup.py +9 -5
- package/hooks/blocking/code_rules_docstrings.py +124 -30
- package/hooks/blocking/code_rules_enforcer.py +161 -16
- package/hooks/blocking/code_rules_shared.py +40 -23
- package/hooks/blocking/config/CLAUDE.md +3 -5
- package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
- package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
- package/hooks/blocking/eli11_reply_enforcer.py +70 -113
- package/hooks/blocking/hedging_language_blocker.py +103 -20
- package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
- package/hooks/blocking/intent_only_ending_blocker.py +6 -0
- package/hooks/blocking/plain_language_blocker.py +139 -20
- package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
- package/hooks/blocking/state_description_blocker.py +7 -1
- package/hooks/blocking/tdd_enforcer.py +8 -0
- package/hooks/blocking/test__path_setup.py +28 -0
- package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
- package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
- package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
- package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
- package/hooks/blocking/test_fable_spawn_gate.py +18 -11
- package/hooks/blocking/test_hedging_language_blocker.py +120 -1
- package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
- package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
- package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
- package/hooks/blocking/test_plain_language_blocker.py +129 -19
- package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
- package/hooks/blocking/test_state_description_blocker.py +45 -2
- package/hooks/blocking/test_stop_dispatcher.py +11 -7
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
- package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
- package/hooks/hooks.json +15 -0
- package/hooks/hooks_constants/CLAUDE.md +14 -3
- package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
- package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
- package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
- package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
- package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
- package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/project_paths_reader.py +31 -4
- package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
- package/hooks/hooks_constants/pytest_invocation.py +354 -0
- package/hooks/hooks_constants/session_start_injector.py +163 -0
- package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
- package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
- package/hooks/hooks_constants/shell_command_segments.py +5 -0
- package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
- package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
- package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
- package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
- package/hooks/hooks_constants/test_session_start_injector.py +168 -0
- package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
- package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
- package/hooks/observability/CLAUDE.md +2 -0
- package/hooks/observability/prose_matcher_advisory.py +237 -0
- package/hooks/observability/test_prose_matcher_advisory.py +143 -0
- package/hooks/session/CLAUDE.md +9 -1
- package/hooks/session/_path_setup.py +13 -0
- package/hooks/session/issue_tracker_session_starter.py +135 -0
- package/hooks/session/orchestrator_auto_starter.py +100 -0
- package/hooks/session/test__path_setup.py +28 -0
- package/hooks/session/test_issue_tracker_session_starter.py +104 -0
- package/hooks/session/test_orchestrator_auto_starter.py +99 -0
- package/hooks/session/test_working_style_prompt.py +47 -0
- package/hooks/session/untracked_repo_detector.py +1 -24
- package/hooks/session/working_style_prompt.py +36 -0
- package/hooks/validators/_path_setup.py +19 -0
- package/hooks/validators/run_all_validators.py +8 -13
- package/installable-surfaces.manifest.json +21 -0
- package/output-styles/CLAUDE.md +1 -3
- package/package.json +4 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/durable-post-artifacts.md +2 -2
- package/rules/eli11-replies.md +6 -1
- package/rules/hedging-claims.md +4 -2
- package/rules/long-horizon-autonomy.md +3 -1
- package/rules/opus5-communication-contract.md +45 -0
- package/rules/plain-language.md +2 -2
- package/rules/research-mode.md +1 -1
- package/scripts/CLAUDE.md +11 -0
- package/scripts/Sync-RepoMain.ps1 +215 -0
- package/scripts/active_capability_references.py +218 -0
- package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
- package/scripts/claude_chain_runner.py +394 -6
- package/scripts/claude_chain_usage.py +1 -1
- package/scripts/codex_compat_materializer.py +105 -85
- package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
- package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
- package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
- package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
- package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
- package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
- package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
- package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
- package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
- package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
- package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
- package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
- package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
- package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
- package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
- package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
- package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
- package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
- package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
- package/scripts/spawn_grok_batch.py +3 -0
- package/scripts/test_active_capability_references.py +108 -0
- package/scripts/test_claude_chain_runner.py +414 -82
- package/scripts/test_claude_chain_usage.py +12 -12
- package/scripts/test_resolve_worker_spawn.py +2 -2
- package/scripts/test_verify_installable_package.py +208 -0
- package/scripts/tests/test_codex_compat_materializer.py +33 -0
- package/scripts/verify_installable_package.py +612 -0
- package/settings.json +10 -0
- package/skills/CLAUDE.md +2 -0
- package/skills/_shared/advisor/CLAUDE.md +1 -1
- package/skills/_shared/advisor/scripts/README.md +2 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
- package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
- package/skills/autoconverge/reference/convergence.md +2 -1
- package/skills/autoconverge/reference/stop-conditions.md +5 -3
- package/skills/beat-sheet/SKILL.md +54 -0
- package/skills/beat-sheet/reference/visual-beats.md +29 -0
- package/skills/bugteam/CONSTRAINTS.md +4 -4
- package/skills/bugteam/EXAMPLES.md +1 -1
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/e-code-review/SKILL.md +26 -5
- package/skills/e-code-review/reference/effort-evaluation.md +35 -0
- package/skills/e-code-review/reference/medium.md +15 -4
- package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
- package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
- package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
- package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
- package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
- package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
- package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
- package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
- package/skills/e-code-review/scripts/grok_code_review.py +16 -7
- package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
- package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
- package/skills/e-code-review/scripts/test_grok_code_review.py +77 -0
- package/skills/grokify/SKILL.md +1 -1
- package/skills/grokify/templates/handoff-template.md +2 -2
- package/skills/orchestrator/SKILL.md +5 -4
- package/skills/plan-to-pr/scripts/create_packet.py +4 -4
- package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
- package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
- package/skills/plan-to-pr/scripts/validate_run.py +4 -1
- package/skills/pr-converge/scripts/check_convergence.py +21 -19
- package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
- package/skills/pr-converge/scripts/conftest.py +35 -0
- package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
- package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
- package/skills/rebase/SKILL.md +15 -3
- package/skills/reviewer-gates/SKILL.md +2 -2
- package/skills/show/SKILL.md +51 -0
- package/skills/show/references/accessibility.md +7 -0
- package/skills/show/references/art.md +3 -0
- package/skills/show/references/charts.md +3 -0
- package/skills/show/references/core-design.md +14 -0
- package/skills/show/references/erds.md +3 -0
- package/skills/show/references/flowcharts.md +3 -0
- package/skills/show/references/host-and-html.md +3 -0
- package/skills/show/references/illustrative-diagrams.md +10 -0
- package/skills/show/references/interaction.md +3 -0
- package/skills/show/references/mockups.md +3 -0
- package/skills/show/references/quality-gates.md +7 -0
- package/skills/show/references/structural-diagrams.md +3 -0
- package/skills/show/references/subject-inventory.md +21 -0
- package/skills/show/references/svg-contract.md +22 -0
- package/skills/show/routing.yaml +30 -0
- package/skills/show/samples/pr1262-v2.svg +222 -0
- package/skills/show/scripts/README.md +6 -0
- package/skills/show/scripts/validate-artifact.py +91 -0
- package/skills/show/scripts/validate-package.py +18 -0
- package/skills/show/templates/html-widget.html +4 -0
- package/skills/show/templates/svg-base.svg +19 -0
- package/skills/show/tests/fixtures/css-var.svg +6 -0
- package/skills/show/tests/fixtures/dead-ref.svg +7 -0
- package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
- package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
- package/skills/show/tests/fixtures/invalid.svg +1 -0
- package/skills/show/tests/fixtures/large-canvas.svg +21 -0
- package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
- package/skills/show/tests/fixtures/valid.html +1 -0
- package/skills/show/tests/test_validate-artifact.py +74 -0
- package/skills/show/tests/test_validators.py +59 -0
- package/skills/show/workflows/create-visual.md +13 -0
- package/skills/show/workflows/review-visual.md +20 -0
- package/skills/split-pr/SKILL.md +85 -0
- package/skills/split-pr/reference/path-layers.md +16 -0
- package/skills/split-pr/reference/proposal-format.md +15 -0
- package/skills/split-pr/reference/split-further-loop.md +10 -0
- package/skills/split-pr/reference/splitting-principles.md +26 -0
- package/skills/split-pr/scripts/analyze_pr.py +279 -0
- package/skills/split-pr/scripts/categorize_files.py +106 -0
- package/skills/split-pr/scripts/config/__init__.py +1 -0
- package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
- package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
- package/skills/split-pr/scripts/config/packing_constants.py +61 -0
- package/skills/split-pr/scripts/config/plan_constants.py +49 -0
- package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
- package/skills/split-pr/scripts/execute_split_slices.py +82 -0
- package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
- package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
- package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
- package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
- package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
- package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
- package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
- package/skills/split-pr/scripts/split_pr_title.py +41 -0
- package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
- package/skills/split-pr/scripts/test_categorize_files.py +55 -0
- package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
- package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
- package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
- package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
- package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
- package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
- package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
- package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
- package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
- package/skills/split-pr/scripts/test_verify_plan.py +56 -0
- package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
- package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
- package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
- package/skills/split-pr/scripts/verify_plan.py +139 -0
- package/skills/team-advisor/SKILL.md +7 -4
- package/skills/team-advisor/reference/advisor-docs-review.md +207 -0
- package/system-prompts/software-engineer.xml +11 -2
- package/commands/initialize.md +0 -90
- package/commands/stubcheck.md +0 -88
- package/output-styles/caveman-agent.md +0 -37
|
@@ -0,0 +1,61 @@
|
|
|
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"
|
|
@@ -0,0 +1,49 @@
|
|
|
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"
|
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
)
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
|
@@ -0,0 +1,212 @@
|
|
|
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
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Build a deterministic dependency graph over split-plan slices.
|
|
2
|
+
|
|
3
|
+
::
|
|
4
|
+
|
|
5
|
+
graph = build_dependency_graph(all_slices)
|
|
6
|
+
# edges: lower-rank layers must land before higher-rank layers
|
|
7
|
+
|
|
8
|
+
Empty input, empty or duplicate ids, and unknown layers raise ValueError.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from config.dependency_constants import (
|
|
14
|
+
EDGE_KEY_FROM,
|
|
15
|
+
EDGE_KEY_TO,
|
|
16
|
+
GRAPH_KEY_EDGES,
|
|
17
|
+
GRAPH_KEY_NODES,
|
|
18
|
+
GRAPH_KEY_ORDER,
|
|
19
|
+
ALL_LAYER_RANK_BY_NAME,
|
|
20
|
+
)
|
|
21
|
+
from config.plan_constants import SLICE_KEY_ID, SLICE_KEY_LAYER
|
|
22
|
+
from split_pr_script_types import JsonObject
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_dependency_graph(all_slices: list[JsonObject]) -> JsonObject:
|
|
26
|
+
"""Return nodes, layer-order edges, and a stable topological order.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
all_slices: Slice maps with ``id`` and ``layer``.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Graph document with nodes, edges, and topological_order.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
ValueError: When input is empty, a slice id is empty or duplicated,
|
|
36
|
+
or a layer is unknown.
|
|
37
|
+
"""
|
|
38
|
+
if not all_slices:
|
|
39
|
+
raise ValueError("all_slices must be non-empty")
|
|
40
|
+
all_nodes: list[str] = []
|
|
41
|
+
all_layer_by_id: dict[str, str] = {}
|
|
42
|
+
for each_slice in all_slices:
|
|
43
|
+
slice_id = str(each_slice.get(SLICE_KEY_ID, "") or "")
|
|
44
|
+
if not slice_id:
|
|
45
|
+
raise ValueError("every slice needs a non-empty id")
|
|
46
|
+
if slice_id in all_layer_by_id:
|
|
47
|
+
raise ValueError(f"duplicate slice id: {slice_id}")
|
|
48
|
+
layer = str(each_slice.get(SLICE_KEY_LAYER, "other") or "other")
|
|
49
|
+
if layer not in ALL_LAYER_RANK_BY_NAME:
|
|
50
|
+
raise ValueError(f"unknown layer {layer!r} on slice {slice_id}")
|
|
51
|
+
all_nodes.append(slice_id)
|
|
52
|
+
all_layer_by_id[slice_id] = layer
|
|
53
|
+
all_edges: list[JsonObject] = []
|
|
54
|
+
for each_from in all_nodes:
|
|
55
|
+
for each_to in all_nodes:
|
|
56
|
+
if each_from == each_to:
|
|
57
|
+
continue
|
|
58
|
+
from_rank = ALL_LAYER_RANK_BY_NAME[all_layer_by_id[each_from]]
|
|
59
|
+
to_rank = ALL_LAYER_RANK_BY_NAME[all_layer_by_id[each_to]]
|
|
60
|
+
if from_rank < to_rank:
|
|
61
|
+
all_edges.append({EDGE_KEY_FROM: each_from, EDGE_KEY_TO: each_to})
|
|
62
|
+
all_order = sorted(
|
|
63
|
+
all_nodes,
|
|
64
|
+
key=lambda each_id: (ALL_LAYER_RANK_BY_NAME[all_layer_by_id[each_id]], each_id),
|
|
65
|
+
)
|
|
66
|
+
return {
|
|
67
|
+
GRAPH_KEY_NODES: all_nodes,
|
|
68
|
+
GRAPH_KEY_EDGES: all_edges,
|
|
69
|
+
GRAPH_KEY_ORDER: all_order,
|
|
70
|
+
}
|