claude-dev-env 2.9.0 → 2.10.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.
Files changed (289) hide show
  1. package/CLAUDE.md +1 -1
  2. package/_shared/pr-loop/CLAUDE.md +1 -1
  3. package/_shared/pr-loop/audit-contract.md +17 -6
  4. package/_shared/pr-loop/audit-reply-template.md +4 -4
  5. package/_shared/pr-loop/code-rules-gate.md +3 -5
  6. package/_shared/pr-loop/fix-protocol.md +2 -3
  7. package/_shared/pr-loop/gh-payloads.md +1 -1
  8. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  9. package/_shared/pr-loop/scripts/README.md +1 -1
  10. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
  13. package/_shared/pr-loop/scripts/preflight.py +9 -4
  14. package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
  15. package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
  16. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
  17. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
  18. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
  19. package/_shared/pr-loop/state-schema.md +5 -14
  20. package/agents/CLAUDE.md +2 -2
  21. package/agents/clean-coder.md +58 -548
  22. package/agents/code-quality-agent.md +10 -2
  23. package/agents/code-verifier.md +1 -1
  24. package/agents/test_agent_frontmatter.py +32 -40
  25. package/audit-rubrics/CLAUDE.md +2 -1
  26. package/audit-rubrics/audit-categories.json +704 -0
  27. package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
  28. package/bin/CLAUDE.md +16 -5
  29. package/bin/ever-shipped-skills.mjs +2 -0
  30. package/bin/install-plan.mjs +402 -0
  31. package/bin/install-transaction.mjs +455 -0
  32. package/bin/install.mjs +593 -147
  33. package/bin/install.plan.test.mjs +194 -0
  34. package/bin/install.profile-root.test.mjs +154 -0
  35. package/bin/install.profiles.test.mjs +253 -0
  36. package/bin/install.settings-defaults.test.mjs +200 -0
  37. package/bin/install.transaction.test.mjs +400 -0
  38. package/bin/install.uninstall-transaction.test.mjs +418 -0
  39. package/bin/merge_managed_permissions.mjs +130 -0
  40. package/bin/resolve-install-root.mjs +181 -0
  41. package/bin/select-install-targets.mjs +401 -0
  42. package/commands/CLAUDE.md +0 -2
  43. package/docs/references/CLAUDE.md +1 -0
  44. package/docs/references/advisor-tool.md +1 -1
  45. package/docs/references/prose-style-enforcement.md +25 -0
  46. package/hooks/blocking/CLAUDE.md +6 -6
  47. package/hooks/blocking/_path_setup.py +9 -5
  48. package/hooks/blocking/code_rules_docstrings.py +124 -30
  49. package/hooks/blocking/code_rules_enforcer.py +161 -16
  50. package/hooks/blocking/code_rules_shared.py +40 -23
  51. package/hooks/blocking/config/CLAUDE.md +3 -5
  52. package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
  53. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
  54. package/hooks/blocking/eli11_reply_enforcer.py +70 -113
  55. package/hooks/blocking/hedging_language_blocker.py +103 -20
  56. package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
  57. package/hooks/blocking/intent_only_ending_blocker.py +6 -0
  58. package/hooks/blocking/plain_language_blocker.py +139 -20
  59. package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
  60. package/hooks/blocking/state_description_blocker.py +7 -1
  61. package/hooks/blocking/tdd_enforcer.py +8 -0
  62. package/hooks/blocking/test__path_setup.py +28 -0
  63. package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
  64. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
  65. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
  66. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
  67. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
  68. package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
  69. package/hooks/blocking/test_hedging_language_blocker.py +120 -1
  70. package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
  71. package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
  72. package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
  73. package/hooks/blocking/test_plain_language_blocker.py +129 -19
  74. package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
  75. package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
  76. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
  77. package/hooks/blocking/test_state_description_blocker.py +45 -2
  78. package/hooks/blocking/test_stop_dispatcher.py +11 -7
  79. package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
  80. package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
  81. package/hooks/hooks.json +15 -0
  82. package/hooks/hooks_constants/CLAUDE.md +14 -3
  83. package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
  84. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
  85. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
  86. package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
  87. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
  88. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
  89. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
  90. package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
  91. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
  92. package/hooks/hooks_constants/project_paths_reader.py +31 -4
  93. package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
  94. package/hooks/hooks_constants/pytest_invocation.py +354 -0
  95. package/hooks/hooks_constants/session_start_injector.py +163 -0
  96. package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
  97. package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
  98. package/hooks/hooks_constants/shell_command_segments.py +5 -0
  99. package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
  100. package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
  101. package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
  102. package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
  103. package/hooks/hooks_constants/test_session_start_injector.py +168 -0
  104. package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
  105. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
  106. package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
  107. package/hooks/observability/CLAUDE.md +2 -0
  108. package/hooks/observability/prose_matcher_advisory.py +237 -0
  109. package/hooks/observability/test_prose_matcher_advisory.py +143 -0
  110. package/hooks/session/CLAUDE.md +9 -1
  111. package/hooks/session/_path_setup.py +13 -0
  112. package/hooks/session/issue_tracker_session_starter.py +135 -0
  113. package/hooks/session/orchestrator_auto_starter.py +100 -0
  114. package/hooks/session/test__path_setup.py +28 -0
  115. package/hooks/session/test_issue_tracker_session_starter.py +104 -0
  116. package/hooks/session/test_orchestrator_auto_starter.py +99 -0
  117. package/hooks/session/test_working_style_prompt.py +47 -0
  118. package/hooks/session/untracked_repo_detector.py +1 -24
  119. package/hooks/session/working_style_prompt.py +36 -0
  120. package/hooks/validators/_path_setup.py +19 -0
  121. package/hooks/validators/run_all_validators.py +8 -13
  122. package/installable-surfaces.manifest.json +21 -0
  123. package/output-styles/CLAUDE.md +1 -3
  124. package/package.json +4 -2
  125. package/rules/CLAUDE.md +1 -0
  126. package/rules/durable-post-artifacts.md +2 -2
  127. package/rules/eli11-replies.md +6 -1
  128. package/rules/hedging-claims.md +4 -2
  129. package/rules/long-horizon-autonomy.md +3 -1
  130. package/rules/opus5-communication-contract.md +45 -0
  131. package/rules/plain-language.md +2 -2
  132. package/rules/research-mode.md +1 -1
  133. package/scripts/CLAUDE.md +11 -0
  134. package/scripts/Sync-RepoMain.ps1 +215 -0
  135. package/scripts/active_capability_references.py +218 -0
  136. package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
  137. package/scripts/claude_chain_runner.py +394 -6
  138. package/scripts/claude_chain_usage.py +1 -1
  139. package/scripts/codex_compat_materializer.py +105 -85
  140. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
  141. package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
  142. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
  143. package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
  144. package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
  145. package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
  146. package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
  147. package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
  148. package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
  149. package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
  150. package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
  151. package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
  152. package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
  153. package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
  154. package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
  155. package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
  156. package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
  157. package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
  158. package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
  159. package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
  160. package/scripts/spawn_grok_batch.py +3 -0
  161. package/scripts/test_active_capability_references.py +108 -0
  162. package/scripts/test_claude_chain_runner.py +414 -82
  163. package/scripts/test_claude_chain_usage.py +12 -12
  164. package/scripts/test_resolve_worker_spawn.py +2 -2
  165. package/scripts/test_verify_installable_package.py +208 -0
  166. package/scripts/tests/test_codex_compat_materializer.py +33 -0
  167. package/scripts/verify_installable_package.py +612 -0
  168. package/settings.json +10 -0
  169. package/skills/CLAUDE.md +2 -0
  170. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  171. package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
  172. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  173. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
  174. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
  175. package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
  176. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
  177. package/skills/autoconverge/reference/convergence.md +2 -1
  178. package/skills/autoconverge/reference/stop-conditions.md +5 -3
  179. package/skills/beat-sheet/SKILL.md +54 -0
  180. package/skills/beat-sheet/reference/visual-beats.md +29 -0
  181. package/skills/bugteam/CONSTRAINTS.md +4 -4
  182. package/skills/bugteam/EXAMPLES.md +1 -1
  183. package/skills/bugteam/reference/README.md +1 -1
  184. package/skills/e-code-review/SKILL.md +26 -5
  185. package/skills/e-code-review/reference/effort-evaluation.md +35 -0
  186. package/skills/e-code-review/reference/medium.md +15 -4
  187. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
  188. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
  189. package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
  190. package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
  191. package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
  192. package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
  193. package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
  194. package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
  195. package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
  196. package/skills/e-code-review/scripts/grok_code_review.py +16 -7
  197. package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
  198. package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
  199. package/skills/e-code-review/scripts/test_grok_code_review.py +77 -0
  200. package/skills/plan-to-pr/scripts/create_packet.py +4 -4
  201. package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
  202. package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
  203. package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
  204. package/skills/plan-to-pr/scripts/validate_run.py +4 -1
  205. package/skills/pr-converge/scripts/check_convergence.py +21 -19
  206. package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
  207. package/skills/pr-converge/scripts/conftest.py +35 -0
  208. package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
  209. package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
  210. package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
  211. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  212. package/skills/rebase/SKILL.md +15 -3
  213. package/skills/reviewer-gates/SKILL.md +2 -2
  214. package/skills/show/SKILL.md +51 -0
  215. package/skills/show/references/accessibility.md +7 -0
  216. package/skills/show/references/art.md +3 -0
  217. package/skills/show/references/charts.md +3 -0
  218. package/skills/show/references/core-design.md +14 -0
  219. package/skills/show/references/erds.md +3 -0
  220. package/skills/show/references/flowcharts.md +3 -0
  221. package/skills/show/references/host-and-html.md +3 -0
  222. package/skills/show/references/illustrative-diagrams.md +10 -0
  223. package/skills/show/references/interaction.md +3 -0
  224. package/skills/show/references/mockups.md +3 -0
  225. package/skills/show/references/quality-gates.md +7 -0
  226. package/skills/show/references/structural-diagrams.md +3 -0
  227. package/skills/show/references/subject-inventory.md +21 -0
  228. package/skills/show/references/svg-contract.md +22 -0
  229. package/skills/show/routing.yaml +30 -0
  230. package/skills/show/samples/pr1262-v2.svg +222 -0
  231. package/skills/show/scripts/README.md +6 -0
  232. package/skills/show/scripts/validate-artifact.py +91 -0
  233. package/skills/show/scripts/validate-package.py +18 -0
  234. package/skills/show/templates/html-widget.html +4 -0
  235. package/skills/show/templates/svg-base.svg +19 -0
  236. package/skills/show/tests/fixtures/css-var.svg +6 -0
  237. package/skills/show/tests/fixtures/dead-ref.svg +7 -0
  238. package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
  239. package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
  240. package/skills/show/tests/fixtures/invalid.svg +1 -0
  241. package/skills/show/tests/fixtures/large-canvas.svg +21 -0
  242. package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
  243. package/skills/show/tests/fixtures/valid.html +1 -0
  244. package/skills/show/tests/test_validate-artifact.py +74 -0
  245. package/skills/show/tests/test_validators.py +59 -0
  246. package/skills/show/workflows/create-visual.md +13 -0
  247. package/skills/show/workflows/review-visual.md +20 -0
  248. package/skills/split-pr/SKILL.md +85 -0
  249. package/skills/split-pr/reference/path-layers.md +16 -0
  250. package/skills/split-pr/reference/proposal-format.md +15 -0
  251. package/skills/split-pr/reference/split-further-loop.md +10 -0
  252. package/skills/split-pr/reference/splitting-principles.md +26 -0
  253. package/skills/split-pr/scripts/analyze_pr.py +279 -0
  254. package/skills/split-pr/scripts/categorize_files.py +106 -0
  255. package/skills/split-pr/scripts/config/__init__.py +1 -0
  256. package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
  257. package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
  258. package/skills/split-pr/scripts/config/packing_constants.py +61 -0
  259. package/skills/split-pr/scripts/config/plan_constants.py +49 -0
  260. package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
  261. package/skills/split-pr/scripts/execute_split_slices.py +82 -0
  262. package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
  263. package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
  264. package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
  265. package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
  266. package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
  267. package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
  268. package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
  269. package/skills/split-pr/scripts/split_pr_title.py +41 -0
  270. package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
  271. package/skills/split-pr/scripts/test_categorize_files.py +55 -0
  272. package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
  273. package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
  274. package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
  275. package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
  276. package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
  277. package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
  278. package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
  279. package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
  280. package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
  281. package/skills/split-pr/scripts/test_verify_plan.py +56 -0
  282. package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
  283. package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
  284. package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
  285. package/skills/split-pr/scripts/verify_plan.py +139 -0
  286. package/system-prompts/software-engineer.xml +11 -2
  287. package/commands/initialize.md +0 -90
  288. package/commands/stubcheck.md +0 -88
  289. package/output-styles/caveman-agent.md +0 -37
@@ -0,0 +1,13 @@
1
+ # Create a visual
2
+
3
+ 1. Restate the visual goal in one sentence.
4
+ 2. Choose a route by intent, not by subject.
5
+ 3. Load shared references and the route references listed in `routing.yaml`.
6
+ 4. Build the subject inventory per `references/subject-inventory.md`.
7
+ 5. Sketch the smallest useful structure before writing markup.
8
+ 6. Keep labels short and put explanation in surrounding prose.
9
+ 7. Build from the route template.
10
+ 8. Run package and artifact validators.
11
+ 9. Inspect the rendered result for clipping, overlap, legibility, and contrast.
12
+
13
+ Use an overview plus detail visuals when the content exceeds the route limits.
@@ -0,0 +1,20 @@
1
+ # Review a visual
2
+
3
+ 1. Identify the route and expected output type.
4
+ 2. Check accessibility metadata and host compatibility.
5
+ 3. Walk the A–E checklist below in order. Accuracy outranks style, so a finding at A–D outranks a finding at E.
6
+ 4. Run `validate-artifact.py` and the relevant render inspection.
7
+ 5. Repair verified defects and rerun all checks.
8
+ 6. Report the remaining limitation when a module lacks dedicated guidance.
9
+
10
+ ## A–E checklist
11
+
12
+ - **A — instant readability.** A stranger gets the story in 10 seconds.
13
+ - **B — literalness.** Ideas are drawn as objects rather than written as labels.
14
+ - **C — visual hierarchy.** The most important object is the largest and strongest.
15
+ - **D — accuracy against the source.** Every arrow matches a citable `path:line`.
16
+ - **E — craft.** Layout bounds hold, geometry connects, references resolve, color meaning is consistent, and text reads at display scale.
17
+
18
+ ## Optional advisor audit
19
+
20
+ One pass, no loop. Hand one subagent the artifact plus the A–E checklist above, ask for ranked issues each with a fix, apply the fixes once, then run step 4 again.
@@ -0,0 +1,85 @@
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
@@ -0,0 +1,16 @@
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.
@@ -0,0 +1,15 @@
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/skills/split-pr/scripts/config/plan_constants.py`.
@@ -0,0 +1,10 @@
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.
@@ -0,0 +1,26 @@
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
@@ -0,0 +1,279 @@
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())
@@ -0,0 +1,106 @@
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
@@ -0,0 +1 @@
1
+ """Config package for split-pr analyzer constants."""
@@ -0,0 +1,14 @@
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
+ }
@@ -0,0 +1,36 @@
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 = "--"