agentic-project-kit 1.0.1__py3-none-any.whl

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 (310) hide show
  1. agentic_project_kit/__init__.py +1 -0
  2. agentic_project_kit/absolute_path_portability_audit.py +241 -0
  3. agentic_project_kit/access_level_policy.py +14 -0
  4. agentic_project_kit/access_levels.py +22 -0
  5. agentic_project_kit/action_registry.py +195 -0
  6. agentic_project_kit/action_specs.py +286 -0
  7. agentic_project_kit/agent_command_runner.py +516 -0
  8. agentic_project_kit/audit_test_matrix.py +36 -0
  9. agentic_project_kit/audit_test_matrix_cli.py +43 -0
  10. agentic_project_kit/chat_bootloader.py +257 -0
  11. agentic_project_kit/chat_entrypoint_contract.py +297 -0
  12. agentic_project_kit/checks.py +511 -0
  13. agentic_project_kit/ci_readiness.py +211 -0
  14. agentic_project_kit/cli.py +147 -0
  15. agentic_project_kit/cli_commands/__init__.py +1 -0
  16. agentic_project_kit/cli_commands/absolute_path_audit.py +25 -0
  17. agentic_project_kit/cli_commands/actions.py +21 -0
  18. agentic_project_kit/cli_commands/artifact_gc.py +240 -0
  19. agentic_project_kit/cli_commands/boot.py +55 -0
  20. agentic_project_kit/cli_commands/chat.py +34 -0
  21. agentic_project_kit/cli_commands/checks.py +305 -0
  22. agentic_project_kit/cli_commands/cockpit.py +103 -0
  23. agentic_project_kit/cli_commands/command_taxonomy.py +25 -0
  24. agentic_project_kit/cli_commands/commands.py +129 -0
  25. agentic_project_kit/cli_commands/dev.py +23 -0
  26. agentic_project_kit/cli_commands/direction.py +92 -0
  27. agentic_project_kit/cli_commands/doc_currency_audit.py +25 -0
  28. agentic_project_kit/cli_commands/doc_registry.py +126 -0
  29. agentic_project_kit/cli_commands/docs.py +223 -0
  30. agentic_project_kit/cli_commands/dpa.py +1338 -0
  31. agentic_project_kit/cli_commands/evidence.py +251 -0
  32. agentic_project_kit/cli_commands/github.py +34 -0
  33. agentic_project_kit/cli_commands/governance.py +14 -0
  34. agentic_project_kit/cli_commands/gui.py +23 -0
  35. agentic_project_kit/cli_commands/gui_readiness_gate.py +27 -0
  36. agentic_project_kit/cli_commands/handoff.py +183 -0
  37. agentic_project_kit/cli_commands/human_workflows.py +430 -0
  38. agentic_project_kit/cli_commands/init.py +132 -0
  39. agentic_project_kit/cli_commands/instruction.py +75 -0
  40. agentic_project_kit/cli_commands/mutation_lock_audit.py +24 -0
  41. agentic_project_kit/cli_commands/ns_legacy_audit.py +25 -0
  42. agentic_project_kit/cli_commands/pass_already_done.py +66 -0
  43. agentic_project_kit/cli_commands/patch_failure_discipline_audit.py +26 -0
  44. agentic_project_kit/cli_commands/patch_preflight.py +37 -0
  45. agentic_project_kit/cli_commands/path_literal_audit.py +39 -0
  46. agentic_project_kit/cli_commands/patterns.py +40 -0
  47. agentic_project_kit/cli_commands/planning_docs_audit.py +25 -0
  48. agentic_project_kit/cli_commands/pr.py +133 -0
  49. agentic_project_kit/cli_commands/pr_hygiene.py +27 -0
  50. agentic_project_kit/cli_commands/profiles.py +26 -0
  51. agentic_project_kit/cli_commands/program_redundancy_audit.py +25 -0
  52. agentic_project_kit/cli_commands/project_direction.py +25 -0
  53. agentic_project_kit/cli_commands/release.py +392 -0
  54. agentic_project_kit/cli_commands/release_publish_orchestration.py +36 -0
  55. agentic_project_kit/cli_commands/remote_branch_hygiene.py +152 -0
  56. agentic_project_kit/cli_commands/remote_next.py +57 -0
  57. agentic_project_kit/cli_commands/removed_ns_commands.py +65 -0
  58. agentic_project_kit/cli_commands/rule_registry.py +129 -0
  59. agentic_project_kit/cli_commands/rules.py +222 -0
  60. agentic_project_kit/cli_commands/scaffold.py +46 -0
  61. agentic_project_kit/cli_commands/slice.py +21 -0
  62. agentic_project_kit/cli_commands/standard_gates_audit_suite.py +27 -0
  63. agentic_project_kit/cli_commands/state.py +51 -0
  64. agentic_project_kit/cli_commands/status_current_state_audit.py +30 -0
  65. agentic_project_kit/cli_commands/todo.py +53 -0
  66. agentic_project_kit/cli_commands/transfer.py +19 -0
  67. agentic_project_kit/cli_commands/transfer_context_flow.py +398 -0
  68. agentic_project_kit/cli_commands/transfer_context_helpers.py +398 -0
  69. agentic_project_kit/cli_commands/transfer_diagnostics.py +767 -0
  70. agentic_project_kit/cli_commands/transfer_evidence_flow.py +368 -0
  71. agentic_project_kit/cli_commands/transfer_handoff_flow.py +594 -0
  72. agentic_project_kit/cli_commands/transfer_order_contract.py +74 -0
  73. agentic_project_kit/cli_commands/transfer_post_merge_complete.py +442 -0
  74. agentic_project_kit/cli_commands/transfer_post_merge_settle.py +171 -0
  75. agentic_project_kit/cli_commands/transfer_pr_closeout_complete.py +124 -0
  76. agentic_project_kit/cli_commands/transfer_pr_create_flow.py +743 -0
  77. agentic_project_kit/cli_commands/transfer_pr_merge_flow.py +547 -0
  78. agentic_project_kit/cli_commands/transfer_repo_after_pr.py +160 -0
  79. agentic_project_kit/cli_commands/transfer_repo_pre_pr.py +664 -0
  80. agentic_project_kit/cli_commands/transfer_runner_flow.py +131 -0
  81. agentic_project_kit/cli_commands/transfer_runtime_initial.py +194 -0
  82. agentic_project_kit/cli_commands/transfer_shared.py +274 -0
  83. agentic_project_kit/cli_commands/validation.py +200 -0
  84. agentic_project_kit/cli_commands/version.py +27 -0
  85. agentic_project_kit/cli_commands/work_orders.py +219 -0
  86. agentic_project_kit/cli_commands/workflow.py +471 -0
  87. agentic_project_kit/cli_commands/workflow_guard.py +26 -0
  88. agentic_project_kit/cli_commands/workspace.py +252 -0
  89. agentic_project_kit/cockpit.py +608 -0
  90. agentic_project_kit/cockpit_readiness.py +67 -0
  91. agentic_project_kit/command_authority_audit.py +192 -0
  92. agentic_project_kit/command_inbox_check.py +103 -0
  93. agentic_project_kit/command_manifest.py +591 -0
  94. agentic_project_kit/command_queue_contract.py +161 -0
  95. agentic_project_kit/command_selector.py +245 -0
  96. agentic_project_kit/command_taxonomy.py +264 -0
  97. agentic_project_kit/commit_guard.py +67 -0
  98. agentic_project_kit/communication_artifact_gc.py +495 -0
  99. agentic_project_kit/communication_rule_context.py +437 -0
  100. agentic_project_kit/communication_state.py +115 -0
  101. agentic_project_kit/composed_wrapper_invariants.py +97 -0
  102. agentic_project_kit/composite_short_circuit.py +140 -0
  103. agentic_project_kit/contract.py +251 -0
  104. agentic_project_kit/control_state.py +83 -0
  105. agentic_project_kit/doc_currency_audit.py +536 -0
  106. agentic_project_kit/doc_lifecycle.py +1179 -0
  107. agentic_project_kit/doc_lifecycle_signals.py +446 -0
  108. agentic_project_kit/doc_lifecycle_sweep.py +639 -0
  109. agentic_project_kit/doc_mesh.py +491 -0
  110. agentic_project_kit/doctor.py +379 -0
  111. agentic_project_kit/documentation_registry.py +1122 -0
  112. agentic_project_kit/documentation_system_audit.py +310 -0
  113. agentic_project_kit/dpa_current_handoff_lifecycle.py +549 -0
  114. agentic_project_kit/dpa_dp2_decision_readiness.py +362 -0
  115. agentic_project_kit/dpa_dp3_dp4_adjudication.py +604 -0
  116. agentic_project_kit/dpa_dp5_block_new_gate.py +294 -0
  117. agentic_project_kit/dpa_dp5_stage_adoption.py +615 -0
  118. agentic_project_kit/dpa_dp5_strict_gate.py +187 -0
  119. agentic_project_kit/dpa_final_closeout.py +627 -0
  120. agentic_project_kit/dpa_fixture_evidence.py +680 -0
  121. agentic_project_kit/dpa_maintainer_record_check.py +711 -0
  122. agentic_project_kit/dpa_post_dp2_scope_assessment.py +734 -0
  123. agentic_project_kit/dpa_probe_002_readiness.py +509 -0
  124. agentic_project_kit/dpa_probe_003_readiness.py +417 -0
  125. agentic_project_kit/dpa_probe_004_readiness.py +437 -0
  126. agentic_project_kit/dpa_readiness.py +505 -0
  127. agentic_project_kit/dpa_readonly_probe_execution.py +500 -0
  128. agentic_project_kit/dpa_registry_contracts.py +312 -0
  129. agentic_project_kit/dpa_renderer_readiness.py +407 -0
  130. agentic_project_kit/dpa_repo_adoption_assessment.py +574 -0
  131. agentic_project_kit/dpa_stable_readiness.py +743 -0
  132. agentic_project_kit/dpa_successor_projection.py +168 -0
  133. agentic_project_kit/dpa_workspace_init_projection.py +93 -0
  134. agentic_project_kit/dpa_wrt_ch001_evidence.py +327 -0
  135. agentic_project_kit/entrypoint_slice_runner.py +153 -0
  136. agentic_project_kit/evidence_clean.py +152 -0
  137. agentic_project_kit/evidence_commit_paths.py +214 -0
  138. agentic_project_kit/evidence_finalize_log.py +204 -0
  139. agentic_project_kit/evidence_guard.py +114 -0
  140. agentic_project_kit/evidence_inspector.py +507 -0
  141. agentic_project_kit/evidence_state_contract.py +99 -0
  142. agentic_project_kit/execution_mode_state.py +148 -0
  143. agentic_project_kit/final_summary_contract.py +85 -0
  144. agentic_project_kit/finalize_guard.py +139 -0
  145. agentic_project_kit/gatekeeper_core.py +232 -0
  146. agentic_project_kit/github.py +25 -0
  147. agentic_project_kit/governance.py +59 -0
  148. agentic_project_kit/gui_action_execution.py +105 -0
  149. agentic_project_kit/gui_action_renderer.py +81 -0
  150. agentic_project_kit/gui_action_run_intent.py +98 -0
  151. agentic_project_kit/gui_action_views.py +195 -0
  152. agentic_project_kit/gui_activity_log.py +76 -0
  153. agentic_project_kit/gui_button_catalog.py +605 -0
  154. agentic_project_kit/gui_cockpit.py +365 -0
  155. agentic_project_kit/gui_cockpit_actions.py +695 -0
  156. agentic_project_kit/gui_cockpit_common.py +172 -0
  157. agentic_project_kit/gui_cockpit_header.py +501 -0
  158. agentic_project_kit/gui_cockpit_sidebar.py +387 -0
  159. agentic_project_kit/gui_cockpit_task.py +748 -0
  160. agentic_project_kit/gui_command_projection.py +355 -0
  161. agentic_project_kit/gui_communication_modes.py +130 -0
  162. agentic_project_kit/gui_dry_run.py +219 -0
  163. agentic_project_kit/gui_gatekeeper_status.py +259 -0
  164. agentic_project_kit/gui_layout_plan.py +148 -0
  165. agentic_project_kit/gui_open_folder.py +18 -0
  166. agentic_project_kit/gui_output_status_panel.py +61 -0
  167. agentic_project_kit/gui_panel_state.py +47 -0
  168. agentic_project_kit/gui_presenter.py +62 -0
  169. agentic_project_kit/gui_readiness_gate.py +206 -0
  170. agentic_project_kit/gui_release_flow.py +59 -0
  171. agentic_project_kit/gui_task_editor.py +1077 -0
  172. agentic_project_kit/gui_tk_widgets.py +177 -0
  173. agentic_project_kit/gui_tkinter_renderer.py +87 -0
  174. agentic_project_kit/gui_tkinter_shell.py +1176 -0
  175. agentic_project_kit/gui_transfer_contract.py +28 -0
  176. agentic_project_kit/gui_viewmodel.py +698 -0
  177. agentic_project_kit/gui_window_guard.py +46 -0
  178. agentic_project_kit/handoff_freshness.py +390 -0
  179. agentic_project_kit/handoff_prompt.py +180 -0
  180. agentic_project_kit/handoff_state.py +254 -0
  181. agentic_project_kit/instruction_lint.py +455 -0
  182. agentic_project_kit/llm_context_carriers.py +66 -0
  183. agentic_project_kit/llm_execution_context.py +285 -0
  184. agentic_project_kit/local_command_stack.py +133 -0
  185. agentic_project_kit/local_feature_gate.py +82 -0
  186. agentic_project_kit/local_garbage_collector.py +269 -0
  187. agentic_project_kit/models.py +18 -0
  188. agentic_project_kit/mutation_lock_audit.py +421 -0
  189. agentic_project_kit/next_turn_evidence.py +253 -0
  190. agentic_project_kit/next_turn_merge_if_green.py +459 -0
  191. agentic_project_kit/next_turn_pr_status.py +255 -0
  192. agentic_project_kit/next_turn_result.py +183 -0
  193. agentic_project_kit/next_turn_runner.py +159 -0
  194. agentic_project_kit/next_turn_slot.py +107 -0
  195. agentic_project_kit/next_turn_status.py +274 -0
  196. agentic_project_kit/ns_legacy_reference_audit.py +258 -0
  197. agentic_project_kit/ns_slice_runner.py +16 -0
  198. agentic_project_kit/operational_handoff_projection.py +124 -0
  199. agentic_project_kit/output_contract.py +80 -0
  200. agentic_project_kit/output_repair.py +61 -0
  201. agentic_project_kit/pass_already_done.py +216 -0
  202. agentic_project_kit/patch_artifact_preflight.py +147 -0
  203. agentic_project_kit/patch_cycle_workflow.py +440 -0
  204. agentic_project_kit/patch_failure_discipline_audit.py +223 -0
  205. agentic_project_kit/patch_preflight.py +215 -0
  206. agentic_project_kit/path_literal_audit.py +612 -0
  207. agentic_project_kit/patterns.py +99 -0
  208. agentic_project_kit/planning_docs_consolidation_audit.py +313 -0
  209. agentic_project_kit/portability_shell_gate.py +40 -0
  210. agentic_project_kit/post_merge_handoff_refresh.py +56 -0
  211. agentic_project_kit/post_release.py +252 -0
  212. agentic_project_kit/post_release_closeout.py +616 -0
  213. agentic_project_kit/pr_cleanup.py +172 -0
  214. agentic_project_kit/pr_closeout.py +50 -0
  215. agentic_project_kit/pr_closeout_complete.py +344 -0
  216. agentic_project_kit/pr_create_or_skip.py +71 -0
  217. agentic_project_kit/pr_hygiene.py +210 -0
  218. agentic_project_kit/program_redundancy_audit.py +251 -0
  219. agentic_project_kit/project_direction.py +840 -0
  220. agentic_project_kit/protected_change_planner.py +194 -0
  221. agentic_project_kit/release.py +401 -0
  222. agentic_project_kit/release_doi_safety.py +24 -0
  223. agentic_project_kit/release_gate_core.py +174 -0
  224. agentic_project_kit/release_metadata_authority_gate.py +328 -0
  225. agentic_project_kit/release_metadata_prep.py +43 -0
  226. agentic_project_kit/release_notes.py +823 -0
  227. agentic_project_kit/release_prep_core.py +204 -0
  228. agentic_project_kit/release_prepare.py +369 -0
  229. agentic_project_kit/release_process_guardrails.py +247 -0
  230. agentic_project_kit/release_publish_core.py +114 -0
  231. agentic_project_kit/release_publish_orchestration.py +469 -0
  232. agentic_project_kit/release_state.py +552 -0
  233. agentic_project_kit/release_verify_core.py +175 -0
  234. agentic_project_kit/remote_branch_hygiene.py +223 -0
  235. agentic_project_kit/remote_next.py +89 -0
  236. agentic_project_kit/remote_next_closeout.py +101 -0
  237. agentic_project_kit/remote_next_order_contract.py +391 -0
  238. agentic_project_kit/removed_ns_commands.py +458 -0
  239. agentic_project_kit/removed_source_audit.py +225 -0
  240. agentic_project_kit/rendering.py +13 -0
  241. agentic_project_kit/repair_report.py +79 -0
  242. agentic_project_kit/repo_identity.py +59 -0
  243. agentic_project_kit/result_report_classifier.py +57 -0
  244. agentic_project_kit/rule_ack.py +105 -0
  245. agentic_project_kit/rule_preservation.py +58 -0
  246. agentic_project_kit/rule_refresh.py +174 -0
  247. agentic_project_kit/rule_registry_registration.py +322 -0
  248. agentic_project_kit/rule_registry_report.py +206 -0
  249. agentic_project_kit/rule_registry_validator.py +704 -0
  250. agentic_project_kit/rule_snapshot.py +98 -0
  251. agentic_project_kit/rule_source_validator.py +133 -0
  252. agentic_project_kit/run_summary.py +68 -0
  253. agentic_project_kit/run_summary_renderer.py +238 -0
  254. agentic_project_kit/runtime_validator.py +68 -0
  255. agentic_project_kit/safe_file_replace.py +35 -0
  256. agentic_project_kit/safe_patch.py +47 -0
  257. agentic_project_kit/safe_push.py +218 -0
  258. agentic_project_kit/safe_remove_diagnostic.py +51 -0
  259. agentic_project_kit/scaffold.py +69 -0
  260. agentic_project_kit/site_claims.py +444 -0
  261. agentic_project_kit/site_generator.py +1140 -0
  262. agentic_project_kit/slice_gate.py +225 -0
  263. agentic_project_kit/slice_runner.py +75 -0
  264. agentic_project_kit/standard_gates_audit_suite.py +167 -0
  265. agentic_project_kit/state_freshness.py +135 -0
  266. agentic_project_kit/status_current_state_audit.py +762 -0
  267. agentic_project_kit/successor_handoff_package.py +1763 -0
  268. agentic_project_kit/templates.py +292 -0
  269. agentic_project_kit/terminal_block_guard.py +166 -0
  270. agentic_project_kit/terminal_logging.py +275 -0
  271. agentic_project_kit/todo.py +97 -0
  272. agentic_project_kit/transfer_closeout.py +136 -0
  273. agentic_project_kit/transfer_continue.py +215 -0
  274. agentic_project_kit/transfer_local_runner.py +101 -0
  275. agentic_project_kit/transfer_operation_monitor.py +236 -0
  276. agentic_project_kit/transfer_post_merge_lifecycle.py +356 -0
  277. agentic_project_kit/transfer_post_merge_settle.py +383 -0
  278. agentic_project_kit/transfer_pr_actions.py +164 -0
  279. agentic_project_kit/transfer_remote_next.py +959 -0
  280. agentic_project_kit/transfer_repo_actions.py +2253 -0
  281. agentic_project_kit/transfer_runner.py +347 -0
  282. agentic_project_kit/transfer_safety_context.py +291 -0
  283. agentic_project_kit/transfer_state.py +483 -0
  284. agentic_project_kit/transfer_uplink.py +471 -0
  285. agentic_project_kit/transfer_workflow_next.py +148 -0
  286. agentic_project_kit/typed_work_order_evidence.py +58 -0
  287. agentic_project_kit/typed_work_order_queue.py +166 -0
  288. agentic_project_kit/typed_work_order_runner.py +168 -0
  289. agentic_project_kit/work_cycle.py +317 -0
  290. agentic_project_kit/work_discard_changes.py +129 -0
  291. agentic_project_kit/work_order_patch.py +199 -0
  292. agentic_project_kit/work_order_runner.py +120 -0
  293. agentic_project_kit/work_order_uploader.py +220 -0
  294. agentic_project_kit/work_order_validator.py +185 -0
  295. agentic_project_kit/work_orders.py +246 -0
  296. agentic_project_kit/workflow_guard.py +262 -0
  297. agentic_project_kit/workflow_summary_runner.py +124 -0
  298. agentic_project_kit/workspace.py +665 -0
  299. agentic_project_kit/workspace_adopt.py +471 -0
  300. agentic_project_kit/workspace_dpa_intake.py +406 -0
  301. agentic_project_kit/workspace_init.py +487 -0
  302. agentic_project_kit/workspace_lock.py +129 -0
  303. agentic_project_kit/workspace_remove.py +381 -0
  304. agentic_project_kit/workspace_upgrade.py +324 -0
  305. agentic_project_kit/wrapper_live_status.py +104 -0
  306. agentic_project_kit-1.0.1.dist-info/METADATA +724 -0
  307. agentic_project_kit-1.0.1.dist-info/RECORD +310 -0
  308. agentic_project_kit-1.0.1.dist-info/WHEEL +4 -0
  309. agentic_project_kit-1.0.1.dist-info/entry_points.txt +3 -0
  310. agentic_project_kit-1.0.1.dist-info/licenses/LICENSE +10 -0
@@ -0,0 +1,252 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Sequence
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+ from pathlib import Path
7
+ from typing import Any
8
+ import json
9
+ import re
10
+ import urllib.parse
11
+ import urllib.request
12
+
13
+ from agentic_project_kit.documentation_registry import build_documentation_registry_summary
14
+ from agentic_project_kit.release import CommandResult, read_project_version, run_command
15
+
16
+ ZENODO_HTTP_TIMEOUT_SECONDS = 5
17
+
18
+
19
+ class PostReleaseStatus(str, Enum):
20
+ PASS = "PASS"
21
+ FAIL = "FAIL"
22
+ WARN = "WARN"
23
+ WAITING = "WAITING"
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class PostReleaseCheckResult:
28
+ name: str
29
+ status: PostReleaseStatus
30
+ detail: str
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class PostReleaseReport:
35
+ version: str
36
+ checks: tuple[PostReleaseCheckResult, ...]
37
+ registry_summary: dict[str, Any] | None = None
38
+
39
+ @property
40
+ def ok(self) -> bool:
41
+ return all(check.status != PostReleaseStatus.FAIL for check in self.checks)
42
+
43
+
44
+ HttpGetter = Callable[[str], tuple[int, str]]
45
+ CommandRunner = Callable[[Path, Sequence[str]], CommandResult]
46
+
47
+
48
+ def build_post_release_report(
49
+ project_root: Path,
50
+ version: str | None = None,
51
+ command_runner: CommandRunner | None = None,
52
+ http_getter: HttpGetter | None = None,
53
+ ) -> PostReleaseReport:
54
+ resolved_version = version or read_project_version(project_root)
55
+ resolved_command_runner = command_runner or run_command
56
+ resolved_http_getter = http_getter or urlopen_text
57
+
58
+ github_release = check_github_release_exists(project_root, resolved_version, resolved_command_runner)
59
+ concept_doi = read_citation_doi(project_root)
60
+ concept_doi_check = check_concept_doi(concept_doi)
61
+ zenodo_check = check_zenodo_version_record(resolved_version, concept_doi, resolved_http_getter)
62
+
63
+ return PostReleaseReport(
64
+ version=resolved_version,
65
+ checks=(github_release, concept_doi_check, zenodo_check),
66
+ registry_summary=_load_registry_summary(project_root),
67
+ )
68
+
69
+
70
+ def check_github_release_exists(
71
+ project_root: Path,
72
+ version: str,
73
+ command_runner: CommandRunner | None = None,
74
+ ) -> PostReleaseCheckResult:
75
+ resolved_command_runner = command_runner or run_command
76
+ tag = f"v{version}"
77
+ result = resolved_command_runner(project_root, ["gh", "release", "view", tag])
78
+ output = "\n".join(part for part in (result.stdout.strip(), result.stderr.strip()) if part)
79
+ if result.returncode == 0:
80
+ return PostReleaseCheckResult("GitHub release", PostReleaseStatus.PASS, f"GitHub release exists: {tag}")
81
+ if result.returncode == 127 or _looks_like_unavailable_github_cli(output):
82
+ return PostReleaseCheckResult("GitHub release", PostReleaseStatus.WARN, output or "gh release view failed")
83
+ if _looks_like_missing_github_release(output):
84
+ return PostReleaseCheckResult("GitHub release", PostReleaseStatus.FAIL, f"GitHub release is absent: {tag}")
85
+ return PostReleaseCheckResult("GitHub release", PostReleaseStatus.WARN, output or "gh release view failed")
86
+
87
+
88
+ def check_concept_doi(concept_doi: str | None) -> PostReleaseCheckResult:
89
+ if not concept_doi:
90
+ return PostReleaseCheckResult(
91
+ "Zenodo concept DOI",
92
+ PostReleaseStatus.WARN,
93
+ "no DOI found in CITATION.cff; Zenodo lookup skipped",
94
+ )
95
+ if not concept_doi.startswith("10.5281/zenodo."):
96
+ return PostReleaseCheckResult(
97
+ "Zenodo concept DOI",
98
+ PostReleaseStatus.WARN,
99
+ f"DOI does not look like a Zenodo DOI: {concept_doi}",
100
+ )
101
+ return PostReleaseCheckResult("Zenodo concept DOI", PostReleaseStatus.PASS, f"found DOI: {concept_doi}")
102
+
103
+
104
+ def check_zenodo_version_record(
105
+ version: str,
106
+ concept_doi: str | None,
107
+ http_getter: HttpGetter | None = None,
108
+ ) -> PostReleaseCheckResult:
109
+ if not concept_doi:
110
+ return PostReleaseCheckResult(
111
+ "Zenodo version DOI",
112
+ PostReleaseStatus.WARN,
113
+ "Zenodo lookup skipped because no concept DOI was found",
114
+ )
115
+
116
+ resolved_http_getter = http_getter or urlopen_text
117
+ url = build_zenodo_records_url(concept_doi)
118
+ try:
119
+ status_code, body = resolved_http_getter(url)
120
+ except OSError as exc:
121
+ return PostReleaseCheckResult("Zenodo version DOI", PostReleaseStatus.WARN, f"Zenodo lookup failed: {exc}")
122
+
123
+ if status_code != 200:
124
+ return PostReleaseCheckResult(
125
+ "Zenodo version DOI",
126
+ PostReleaseStatus.WARN,
127
+ f"Zenodo lookup returned HTTP {status_code}",
128
+ )
129
+
130
+ try:
131
+ payload = json.loads(body)
132
+ except json.JSONDecodeError as exc:
133
+ return PostReleaseCheckResult("Zenodo version DOI", PostReleaseStatus.WARN, f"Zenodo JSON parse failed: {exc}")
134
+
135
+ version_doi = find_version_doi(payload, version)
136
+ if version_doi:
137
+ return PostReleaseCheckResult(
138
+ "Zenodo version DOI",
139
+ PostReleaseStatus.PASS,
140
+ f"verified version DOI for v{version}: {version_doi}",
141
+ )
142
+
143
+ return PostReleaseCheckResult(
144
+ "Zenodo version DOI",
145
+ PostReleaseStatus.WAITING,
146
+ f"no verified Zenodo record found yet for v{version}; leave README/CITATION unchanged",
147
+ )
148
+
149
+
150
+ def build_zenodo_records_url(concept_doi: str) -> str:
151
+ query = f'conceptdoi:"{concept_doi}"'
152
+ return "https://zenodo.org/api/records?" + urllib.parse.urlencode(
153
+ {"q": query, "all_versions": "true", "sort": "mostrecent"}
154
+ )
155
+
156
+
157
+ def find_version_doi(payload: object, version: str) -> str | None:
158
+ hits = _extract_hits(payload)
159
+ wanted = {version, f"v{version}"}
160
+ for record in hits:
161
+ if not isinstance(record, dict):
162
+ continue
163
+ metadata = record.get("metadata")
164
+ if not isinstance(metadata, dict):
165
+ metadata = {}
166
+ record_version = str(metadata.get("version", "")).strip()
167
+ title = str(metadata.get("title", ""))
168
+ doi = str(record.get("doi") or metadata.get("doi") or "").strip()
169
+ if not doi:
170
+ continue
171
+ if record_version in wanted or _title_mentions_version(title, version):
172
+ return doi
173
+ return None
174
+
175
+
176
+ def read_citation_doi(project_root: Path) -> str | None:
177
+ citation = project_root / "CITATION.cff"
178
+ if not citation.exists():
179
+ return None
180
+ match = re.search(r"^doi:\s*['\"]?([^'\"\s]+)", citation.read_text(encoding="utf-8"), re.MULTILINE)
181
+ if not match:
182
+ return None
183
+ return match.group(1).strip()
184
+
185
+
186
+ def render_post_release_report(report: PostReleaseReport) -> str:
187
+ lines = [f"Post-release check for target v{report.version}", ""]
188
+ for check in report.checks:
189
+ lines.append(f"[{check.status.value}] {check.name}: {check.detail}")
190
+ if report.registry_summary is not None:
191
+ lines.extend(("", *_render_registry_summary_lines(report.registry_summary)))
192
+ lines.append("")
193
+ lines.append("Overall: PASS" if report.ok else "Overall: FAIL")
194
+ return "\n".join(lines) + "\n"
195
+
196
+
197
+ def urlopen_text(url: str) -> tuple[int, str]:
198
+ request = urllib.request.Request(url, headers={"User-Agent": "agentic-project-kit"})
199
+ with urllib.request.urlopen(request, timeout=ZENODO_HTTP_TIMEOUT_SECONDS) as response: # noqa: S310
200
+ status = getattr(response, "status", 200)
201
+ body = response.read().decode("utf-8")
202
+ return status, body
203
+
204
+
205
+ def _extract_hits(payload: object) -> list[object]:
206
+ if not isinstance(payload, dict):
207
+ return []
208
+ hits = payload.get("hits")
209
+ if isinstance(hits, dict):
210
+ records = hits.get("hits")
211
+ if isinstance(records, list):
212
+ return records
213
+ if isinstance(hits, list):
214
+ return hits
215
+ return []
216
+
217
+
218
+ def _title_mentions_version(title: str, version: str) -> bool:
219
+ normalized = title.lower()
220
+ return f"v{version}" in normalized or f" {version}" in normalized
221
+
222
+
223
+ def _looks_like_unavailable_github_cli(output: str) -> bool:
224
+ normalized = output.lower()
225
+ return any(fragment in normalized for fragment in ("could not run gh", "gh unavailable"))
226
+
227
+
228
+ def _looks_like_missing_github_release(output: str) -> bool:
229
+ normalized = output.lower()
230
+ return any(fragment in normalized for fragment in ("release not found", "http 404"))
231
+
232
+
233
+ def _load_registry_summary(project_root: Path) -> dict[str, Any] | None:
234
+ try:
235
+ return build_documentation_registry_summary(project_root)
236
+ except (OSError, ValueError):
237
+ return None
238
+
239
+
240
+ def _render_registry_summary_lines(summary: dict[str, Any]) -> list[str]:
241
+ lines = [
242
+ "Documentation registry:",
243
+ f"- registry: {summary['registry_path']}",
244
+ f"- version: {summary['version']}",
245
+ f"- documents: {summary['document_count']}",
246
+ f"- broad_migration_allowed: {summary['broad_migration_allowed']}",
247
+ ]
248
+ class_counts = summary.get("class_counts", {})
249
+ if isinstance(class_counts, dict):
250
+ for class_name, count in class_counts.items():
251
+ lines.append(f"- class:{class_name}: {count}")
252
+ return lines