claude-dev-env 2.8.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 (323) hide show
  1. package/CLAUDE.md +8 -2
  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 +62 -562
  22. package/agents/code-quality-agent.md +10 -2
  23. package/agents/code-verifier.md +1 -1
  24. package/agents/test_agent_frontmatter.py +58 -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/CODE_RULES.md +4 -2
  44. package/docs/references/CLAUDE.md +3 -2
  45. package/docs/references/advisor-tool.md +44 -6
  46. package/docs/references/prose-style-enforcement.md +25 -0
  47. package/docs/references/team-advisor-skill.md +14 -8
  48. package/hooks/blocking/CLAUDE.md +6 -6
  49. package/hooks/blocking/_path_setup.py +9 -5
  50. package/hooks/blocking/code_rules_docstrings.py +124 -30
  51. package/hooks/blocking/code_rules_enforcer.py +161 -16
  52. package/hooks/blocking/code_rules_shared.py +40 -23
  53. package/hooks/blocking/config/CLAUDE.md +3 -5
  54. package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
  55. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
  56. package/hooks/blocking/eli11_reply_enforcer.py +70 -113
  57. package/hooks/blocking/hedging_language_blocker.py +103 -20
  58. package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
  59. package/hooks/blocking/intent_only_ending_blocker.py +6 -0
  60. package/hooks/blocking/plain_language_blocker.py +139 -20
  61. package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
  62. package/hooks/blocking/state_description_blocker.py +7 -1
  63. package/hooks/blocking/tdd_enforcer.py +8 -0
  64. package/hooks/blocking/test__path_setup.py +28 -0
  65. package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
  66. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
  67. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
  68. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
  69. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
  70. package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
  71. package/hooks/blocking/test_hedging_language_blocker.py +120 -1
  72. package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
  73. package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
  74. package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
  75. package/hooks/blocking/test_plain_language_blocker.py +129 -19
  76. package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
  77. package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
  78. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
  79. package/hooks/blocking/test_state_description_blocker.py +45 -2
  80. package/hooks/blocking/test_stop_dispatcher.py +11 -7
  81. package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
  82. package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
  83. package/hooks/hooks.json +15 -0
  84. package/hooks/hooks_constants/CLAUDE.md +14 -3
  85. package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
  86. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
  87. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  88. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
  89. package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
  90. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
  91. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
  92. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
  93. package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
  94. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
  95. package/hooks/hooks_constants/project_paths_reader.py +31 -4
  96. package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
  97. package/hooks/hooks_constants/pytest_invocation.py +354 -0
  98. package/hooks/hooks_constants/session_start_injector.py +163 -0
  99. package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
  100. package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
  101. package/hooks/hooks_constants/shell_command_segments.py +5 -0
  102. package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
  103. package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
  104. package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
  105. package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
  106. package/hooks/hooks_constants/test_session_start_injector.py +168 -0
  107. package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
  108. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
  109. package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
  110. package/hooks/observability/CLAUDE.md +2 -0
  111. package/hooks/observability/prose_matcher_advisory.py +237 -0
  112. package/hooks/observability/test_prose_matcher_advisory.py +143 -0
  113. package/hooks/session/CLAUDE.md +9 -1
  114. package/hooks/session/_path_setup.py +13 -0
  115. package/hooks/session/issue_tracker_session_starter.py +135 -0
  116. package/hooks/session/orchestrator_auto_starter.py +100 -0
  117. package/hooks/session/test__path_setup.py +28 -0
  118. package/hooks/session/test_issue_tracker_session_starter.py +104 -0
  119. package/hooks/session/test_orchestrator_auto_starter.py +99 -0
  120. package/hooks/session/test_working_style_prompt.py +47 -0
  121. package/hooks/session/untracked_repo_detector.py +1 -24
  122. package/hooks/session/working_style_prompt.py +36 -0
  123. package/hooks/validators/_path_setup.py +19 -0
  124. package/hooks/validators/run_all_validators.py +8 -13
  125. package/installable-surfaces.manifest.json +21 -0
  126. package/output-styles/CLAUDE.md +15 -0
  127. package/package.json +5 -2
  128. package/rules/CLAUDE.md +1 -0
  129. package/rules/code-standards.md +33 -7
  130. package/rules/durable-post-artifacts.md +2 -2
  131. package/rules/eli11-replies.md +7 -2
  132. package/rules/hedging-claims.md +4 -2
  133. package/rules/long-horizon-autonomy.md +3 -1
  134. package/rules/opus5-communication-contract.md +45 -0
  135. package/rules/plain-language.md +2 -2
  136. package/rules/research-mode.md +1 -1
  137. package/scripts/CLAUDE.md +13 -2
  138. package/scripts/Sync-RepoMain.ps1 +215 -0
  139. package/scripts/active_capability_references.py +218 -0
  140. package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
  141. package/scripts/claude_chain_runner.py +394 -6
  142. package/scripts/claude_chain_usage.py +1 -1
  143. package/scripts/codex_compat_materializer.py +105 -85
  144. package/scripts/dev_env_scripts_constants/CLAUDE.md +3 -1
  145. package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
  146. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
  147. package/scripts/dev_env_scripts_constants/grok_run_ledger_constants.py +50 -0
  148. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +104 -0
  149. package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
  150. package/scripts/grok_patch_artifacts.py +123 -0
  151. package/scripts/grok_run_ledger.py +318 -0
  152. package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
  153. package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
  154. package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
  155. package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
  156. package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
  157. package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
  158. package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
  159. package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
  160. package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
  161. package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
  162. package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
  163. package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
  164. package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
  165. package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
  166. package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
  167. package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
  168. package/scripts/spawn_grok_batch.py +556 -9
  169. package/scripts/test_active_capability_references.py +108 -0
  170. package/scripts/test_claude_chain_runner.py +414 -82
  171. package/scripts/test_claude_chain_usage.py +12 -12
  172. package/scripts/test_grok_patch_artifacts.py +82 -0
  173. package/scripts/test_grok_run_ledger.py +116 -0
  174. package/scripts/test_resolve_worker_spawn.py +2 -2
  175. package/scripts/test_spawn_grok_batch.py +295 -0
  176. package/scripts/test_verify_installable_package.py +208 -0
  177. package/scripts/tests/test_codex_compat_materializer.py +33 -0
  178. package/scripts/verify_installable_package.py +612 -0
  179. package/settings.json +10 -0
  180. package/skills/CLAUDE.md +6 -2
  181. package/skills/_shared/CLAUDE.md +37 -4
  182. package/skills/_shared/advisor/CLAUDE.md +9 -0
  183. package/skills/_shared/advisor/advisor-protocol.md +5 -0
  184. package/skills/_shared/advisor/scripts/README.md +9 -0
  185. package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
  186. package/skills/_shared/pr-loop/CLAUDE.md +18 -1
  187. package/skills/_shared/pr-loop/audit-contract.md +5 -0
  188. package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
  189. package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
  190. package/skills/_shared/pr-loop/fix-protocol.md +5 -0
  191. package/skills/_shared/pr-loop/gh-payloads.md +5 -0
  192. package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
  193. package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
  194. package/skills/_shared/pr-loop/scripts/CLAUDE.md +9 -1
  195. package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -0
  196. package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
  197. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  198. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
  199. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
  200. package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
  201. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
  202. package/skills/_shared/pr-loop/state-schema.md +5 -0
  203. package/skills/_shared/pr-loop/worker-spawn.md +5 -0
  204. package/skills/autoconverge/reference/convergence.md +2 -1
  205. package/skills/autoconverge/reference/stop-conditions.md +5 -3
  206. package/skills/beat-sheet/SKILL.md +54 -0
  207. package/skills/beat-sheet/reference/visual-beats.md +29 -0
  208. package/skills/bugteam/CONSTRAINTS.md +4 -4
  209. package/skills/bugteam/EXAMPLES.md +1 -1
  210. package/skills/bugteam/reference/README.md +1 -1
  211. package/skills/e-code-review/SKILL.md +29 -3
  212. package/skills/e-code-review/reference/effort-evaluation.md +35 -0
  213. package/skills/e-code-review/reference/medium.md +15 -4
  214. package/skills/e-code-review/reference/runner-selection.md +40 -0
  215. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
  216. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
  217. package/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +1 -0
  218. package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
  219. package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -0
  220. package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
  221. package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
  222. package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
  223. package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
  224. package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
  225. package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
  226. package/skills/e-code-review/scripts/grok_code_review.py +230 -0
  227. package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
  228. package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
  229. package/skills/e-code-review/scripts/test_grok_code_review.py +289 -0
  230. package/skills/grok-spawn/SKILL.md +5 -0
  231. package/skills/orchestrator/SKILL.md +5 -0
  232. package/skills/plan-to-pr/scripts/create_packet.py +4 -4
  233. package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
  234. package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
  235. package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
  236. package/skills/plan-to-pr/scripts/validate_run.py +4 -1
  237. package/skills/pr-converge/scripts/check_convergence.py +21 -19
  238. package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
  239. package/skills/pr-converge/scripts/conftest.py +35 -0
  240. package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
  241. package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
  242. package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
  243. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  244. package/skills/rebase/SKILL.md +15 -3
  245. package/skills/reviewer-gates/SKILL.md +2 -2
  246. package/skills/show/SKILL.md +51 -0
  247. package/skills/show/references/accessibility.md +7 -0
  248. package/skills/show/references/art.md +3 -0
  249. package/skills/show/references/charts.md +3 -0
  250. package/skills/show/references/core-design.md +14 -0
  251. package/skills/show/references/erds.md +3 -0
  252. package/skills/show/references/flowcharts.md +3 -0
  253. package/skills/show/references/host-and-html.md +3 -0
  254. package/skills/show/references/illustrative-diagrams.md +10 -0
  255. package/skills/show/references/interaction.md +3 -0
  256. package/skills/show/references/mockups.md +3 -0
  257. package/skills/show/references/quality-gates.md +7 -0
  258. package/skills/show/references/structural-diagrams.md +3 -0
  259. package/skills/show/references/subject-inventory.md +21 -0
  260. package/skills/show/references/svg-contract.md +22 -0
  261. package/skills/show/routing.yaml +30 -0
  262. package/skills/show/samples/pr1262-v2.svg +222 -0
  263. package/skills/show/scripts/README.md +6 -0
  264. package/skills/show/scripts/validate-artifact.py +91 -0
  265. package/skills/show/scripts/validate-package.py +18 -0
  266. package/skills/show/templates/html-widget.html +4 -0
  267. package/skills/show/templates/svg-base.svg +19 -0
  268. package/skills/show/tests/fixtures/css-var.svg +6 -0
  269. package/skills/show/tests/fixtures/dead-ref.svg +7 -0
  270. package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
  271. package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
  272. package/skills/show/tests/fixtures/invalid.svg +1 -0
  273. package/skills/show/tests/fixtures/large-canvas.svg +21 -0
  274. package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
  275. package/skills/show/tests/fixtures/valid.html +1 -0
  276. package/skills/show/tests/test_validate-artifact.py +74 -0
  277. package/skills/show/tests/test_validators.py +59 -0
  278. package/skills/show/workflows/create-visual.md +13 -0
  279. package/skills/show/workflows/review-visual.md +20 -0
  280. package/skills/split-pr/SKILL.md +85 -0
  281. package/skills/split-pr/reference/path-layers.md +16 -0
  282. package/skills/split-pr/reference/proposal-format.md +15 -0
  283. package/skills/split-pr/reference/split-further-loop.md +10 -0
  284. package/skills/split-pr/reference/splitting-principles.md +26 -0
  285. package/skills/split-pr/scripts/analyze_pr.py +279 -0
  286. package/skills/split-pr/scripts/categorize_files.py +106 -0
  287. package/skills/split-pr/scripts/config/__init__.py +1 -0
  288. package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
  289. package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
  290. package/skills/split-pr/scripts/config/packing_constants.py +61 -0
  291. package/skills/split-pr/scripts/config/plan_constants.py +49 -0
  292. package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
  293. package/skills/split-pr/scripts/execute_split_slices.py +82 -0
  294. package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
  295. package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
  296. package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
  297. package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
  298. package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
  299. package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
  300. package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
  301. package/skills/split-pr/scripts/split_pr_title.py +41 -0
  302. package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
  303. package/skills/split-pr/scripts/test_categorize_files.py +55 -0
  304. package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
  305. package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
  306. package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
  307. package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
  308. package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
  309. package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
  310. package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
  311. package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
  312. package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
  313. package/skills/split-pr/scripts/test_verify_plan.py +56 -0
  314. package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
  315. package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
  316. package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
  317. package/skills/split-pr/scripts/verify_plan.py +139 -0
  318. package/skills/task-build/reference/tool-routing.md +3 -0
  319. package/skills/team-advisor/SKILL.md +23 -44
  320. package/system-prompts/software-engineer.xml +17 -5
  321. package/commands/initialize.md +0 -90
  322. package/commands/stubcheck.md +0 -88
  323. package/skills/test_markdown_link_integrity.py +0 -107
@@ -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
+ }