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,549 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass, field
4
+ from datetime import datetime, timezone
5
+ import hashlib
6
+ import json
7
+ from pathlib import Path
8
+ import re
9
+ import subprocess
10
+ from typing import Any
11
+
12
+ from agentic_project_kit.dpa_readiness import DEFAULT_READINESS_PATH, evaluate_dpa_readiness
13
+ from agentic_project_kit.operational_handoff_projection import (
14
+ ensure_generated_operational_handoff_block,
15
+ render_current_operational_handoff_state,
16
+ )
17
+ from agentic_project_kit.safe_file_replace import safe_replace
18
+ from agentic_project_kit.workspace import LEGACY_DEFAULTS, load_workspace
19
+ from agentic_project_kit.workspace_lock import WorkspaceLockBusy, workspace_mutation_lock
20
+
21
+ DEFAULT_ACCEPTANCE_STATE_PATH = Path(LEGACY_DEFAULTS.agentic_root) / LEGACY_DEFAULTS.dpa_current_handoff_acceptance_state_file
22
+ DEFAULT_CURRENT_HANDOFF_FILENAME = "CURRENT_HANDOFF.md"
23
+
24
+ CONTRACT_ID = "DPA-CURRENT-HANDOFF-OPERATIONAL-STATE-v1"
25
+ GATE_SET_ID = "dpa-current-handoff-minimal-gate-set-v1"
26
+ RENDERER_ID = "agentic_project_kit.operational_handoff_projection"
27
+ RENDERER_SEMANTIC_VERSION = "1"
28
+ TARGET_SCOPE = "CURRENT_HANDOFF_OPERATIONAL_STATE_BLOCK"
29
+ WRITER_ID = "WRT-CH-001"
30
+
31
+ OPERATIONAL_REFRESH_MARKER_PATTERN = re.compile(
32
+ r"\n## Operational documentation refresh state after PR #\d+\n\n"
33
+ r"Current administrative handoff refresh state is `[^`]+` \(`[^`]*`\)\. "
34
+ r"Continue next only after this post-PR\d+ refresh is committed and merged; "
35
+ r"the next substantive slice must be created from fresh main\.\n?",
36
+ flags=re.MULTILINE,
37
+ )
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class DpaCurrentHandoffFinding:
42
+ code: str
43
+ severity: str
44
+ message: str
45
+ path: str = ""
46
+
47
+ def as_dict(self) -> dict[str, str]:
48
+ return asdict(self)
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class DpaCurrentHandoffPlan:
53
+ branch: str
54
+ validation_ref: str
55
+ target_path: str
56
+ source_path: str
57
+ acceptance_state_path: str
58
+ target_before_fingerprint: str
59
+ source_fingerprint: str
60
+ projected_payload_fingerprint: str
61
+ projected_complete_target_fingerprint: str
62
+ would_change_target: bool
63
+ prior_acceptance_status: str
64
+ freshness: str
65
+
66
+ def as_dict(self) -> dict[str, object]:
67
+ return asdict(self)
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class DpaCurrentHandoffResult:
72
+ result_status: str
73
+ execute: bool
74
+ initialized_acceptance: bool
75
+ wrote_target: bool
76
+ wrote_acceptance_state: bool
77
+ plan: DpaCurrentHandoffPlan
78
+ findings: tuple[DpaCurrentHandoffFinding, ...] = field(default_factory=tuple)
79
+ acceptance_state: dict[str, Any] | None = None
80
+
81
+ @property
82
+ def ok(self) -> bool:
83
+ return not any(item.severity == "BLOCKER" for item in self.findings)
84
+
85
+ def as_dict(self) -> dict[str, object]:
86
+ payload = {
87
+ "schema_version": 1,
88
+ "kind": "dpa_current_handoff_lifecycle_result",
89
+ "result_status": self.result_status,
90
+ "execute": self.execute,
91
+ "initialized_acceptance": self.initialized_acceptance,
92
+ "wrote_target": self.wrote_target,
93
+ "wrote_acceptance_state": self.wrote_acceptance_state,
94
+ "plan": self.plan.as_dict(),
95
+ "finding_count": len(self.findings),
96
+ "findings": [item.as_dict() for item in self.findings],
97
+ "claims": {
98
+ "acceptance_state_recorded": self.wrote_acceptance_state,
99
+ "production_mutation_performed": False,
100
+ "kit_conformance_claimed": False,
101
+ "generated_outputs_manually_patched": False,
102
+ },
103
+ }
104
+ if self.acceptance_state is not None:
105
+ payload["acceptance_state"] = self.acceptance_state
106
+ return payload
107
+
108
+
109
+ def _sha256_bytes(data: bytes) -> str:
110
+ return hashlib.sha256(data).hexdigest()
111
+
112
+
113
+ def _read_bytes(path: Path) -> bytes:
114
+ return path.read_bytes() if path.exists() else b""
115
+
116
+
117
+ def _read_text(path: Path) -> str:
118
+ return path.read_text(encoding="utf-8") if path.exists() else ""
119
+
120
+
121
+ def _repo_rel(root: Path, path: Path) -> str:
122
+ try:
123
+ return path.relative_to(root).as_posix()
124
+ except ValueError:
125
+ return path.as_posix()
126
+
127
+
128
+ def _resolve(root: Path, path: Path) -> Path:
129
+ return path if path.is_absolute() else root / path
130
+
131
+
132
+ def _git(root: Path, args: list[str]) -> str:
133
+ completed = subprocess.run(["git", *args], cwd=root, text=True, capture_output=True, check=False)
134
+ if completed.returncode != 0:
135
+ return "UNKNOWN"
136
+ return completed.stdout.strip() or "UNKNOWN"
137
+
138
+
139
+ def _load_acceptance_state(path: Path) -> dict[str, Any] | None:
140
+ if not path.exists():
141
+ return None
142
+ try:
143
+ loaded = json.loads(path.read_text(encoding="utf-8"))
144
+ except json.JSONDecodeError as exc:
145
+ raise ValueError(f"acceptance state is not valid JSON: {exc}") from exc
146
+ if not isinstance(loaded, dict):
147
+ raise ValueError("acceptance state must be a JSON object")
148
+ return loaded
149
+
150
+
151
+ def _write_json_atomic(path: Path, payload: dict[str, Any]) -> None:
152
+ path.parent.mkdir(parents=True, exist_ok=True)
153
+ tmp = path.with_name(path.name + ".tmp")
154
+ tmp.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
155
+ safe_replace(path, tmp)
156
+
157
+
158
+ def _write_text_atomic(path: Path, text: str) -> None:
159
+ tmp = path.with_name(path.name + ".tmp")
160
+ tmp.write_text(text, encoding="utf-8")
161
+ safe_replace(path, tmp)
162
+
163
+
164
+ def _render_target_text(root: Path, target_path: Path, source_path: Path) -> str:
165
+ current = _read_text(target_path).replace("\\n", "\n")
166
+ current = OPERATIONAL_REFRESH_MARKER_PATTERN.sub("", current).rstrip() + "\n"
167
+ return ensure_generated_operational_handoff_block(
168
+ current,
169
+ render_current_operational_handoff_state(root, path=source_path),
170
+ )
171
+
172
+
173
+ def _build_acceptance_state(
174
+ *,
175
+ plan: DpaCurrentHandoffPlan,
176
+ target_text: str,
177
+ branch: str,
178
+ validation_ref: str,
179
+ contract_id: str = CONTRACT_ID,
180
+ target_scope: str = TARGET_SCOPE,
181
+ writer_id: str = WRITER_ID,
182
+ renderer_id: str = RENDERER_ID,
183
+ renderer_semantic_version: str = RENDERER_SEMANTIC_VERSION,
184
+ ) -> dict[str, Any]:
185
+ now = datetime.now(timezone.utc).isoformat()
186
+ return {
187
+ "schema_version": 1,
188
+ "kind": "dpa_acceptance_state",
189
+ "status": "accepted",
190
+ "accepted_at": now,
191
+ "accepted_lifecycle_outcome": "accepted",
192
+ "accepted_gate_result": "pass",
193
+ "accepted_gate_set_id": GATE_SET_ID,
194
+ "branch": branch,
195
+ "validation_ref": validation_ref,
196
+ "contract_id": contract_id,
197
+ "target_scope": target_scope,
198
+ "writer_id": writer_id,
199
+ "renderer": {
200
+ "id": renderer_id,
201
+ "semantic_version": renderer_semantic_version,
202
+ },
203
+ "source": {
204
+ "path": plan.source_path,
205
+ "fingerprint": plan.source_fingerprint,
206
+ },
207
+ "target": {
208
+ "path": plan.target_path,
209
+ "complete_target_fingerprint": _sha256_bytes(target_text.encode("utf-8")),
210
+ "payload_fingerprint": plan.projected_payload_fingerprint,
211
+ "pre_write_fingerprint": plan.target_before_fingerprint,
212
+ },
213
+ "claims": {
214
+ "production_mutation_performed": False,
215
+ "kit_conformance_claimed": False,
216
+ "generated_outputs_manually_patched": False,
217
+ },
218
+ }
219
+
220
+
221
+ def _acceptance_finding(
222
+ code: str,
223
+ message: str,
224
+ *,
225
+ path: Path | None = None,
226
+ root: Path | None = None,
227
+ ) -> DpaCurrentHandoffFinding:
228
+ rel = _repo_rel(root, path) if path is not None and root is not None else ""
229
+ return DpaCurrentHandoffFinding(code=code, severity="BLOCKER", message=message, path=rel)
230
+
231
+
232
+ def evaluate_current_handoff_lifecycle(
233
+ root: Path | str = ".",
234
+ *,
235
+ target_path: Path | None = None,
236
+ acceptance_state_path: Path | None = None,
237
+ readiness_path: Path = DEFAULT_READINESS_PATH,
238
+ validation_ref: str | None = None,
239
+ execute: bool = False,
240
+ initialize_acceptance: bool = False,
241
+ require_dp2_authorized: bool = True,
242
+ ) -> DpaCurrentHandoffResult:
243
+ resolved_root = Path(root).resolve()
244
+ workspace = load_workspace(resolved_root)
245
+ target = _resolve(resolved_root, target_path) if target_path is not None else workspace.handoff_file(DEFAULT_CURRENT_HANDOFF_FILENAME)
246
+ source = workspace.operational_handoff_state_path()
247
+
248
+ try:
249
+ projected_text = _render_target_text(resolved_root, target, source)
250
+ except (OSError, ValueError) as exc:
251
+ projected_text = _read_text(target)
252
+ preflight_findings = (_acceptance_finding("render-failed", str(exc), path=target, root=resolved_root),)
253
+ else:
254
+ preflight_findings = ()
255
+
256
+ return evaluate_current_handoff_text_lifecycle(
257
+ resolved_root,
258
+ target_path=target,
259
+ acceptance_state_path=acceptance_state_path,
260
+ readiness_path=readiness_path,
261
+ validation_ref=validation_ref,
262
+ execute=execute,
263
+ initialize_acceptance=initialize_acceptance,
264
+ require_dp2_authorized=require_dp2_authorized,
265
+ projected_text=projected_text,
266
+ source_path=source,
267
+ writer_id=WRITER_ID,
268
+ renderer_id=RENDERER_ID,
269
+ renderer_semantic_version=RENDERER_SEMANTIC_VERSION,
270
+ contract_id=CONTRACT_ID,
271
+ target_scope=TARGET_SCOPE,
272
+ lock_command="dpa current-handoff-refresh",
273
+ preflight_findings=preflight_findings,
274
+ )
275
+
276
+
277
+ def evaluate_current_handoff_text_lifecycle(
278
+ root: Path | str = ".",
279
+ *,
280
+ projected_text: str,
281
+ writer_id: str,
282
+ renderer_id: str,
283
+ renderer_semantic_version: str = RENDERER_SEMANTIC_VERSION,
284
+ contract_id: str = CONTRACT_ID,
285
+ target_scope: str = TARGET_SCOPE,
286
+ source_path: Path | str | None = None,
287
+ source_fingerprint: str | None = None,
288
+ target_path: Path | None = None,
289
+ acceptance_state_path: Path | None = None,
290
+ readiness_path: Path = DEFAULT_READINESS_PATH,
291
+ validation_ref: str | None = None,
292
+ execute: bool = False,
293
+ initialize_acceptance: bool = False,
294
+ require_dp2_authorized: bool = True,
295
+ lock_command: str = "dpa current-handoff-lifecycle",
296
+ preflight_findings: tuple[DpaCurrentHandoffFinding, ...] = (),
297
+ ) -> DpaCurrentHandoffResult:
298
+ resolved_root = Path(root).resolve()
299
+ workspace = load_workspace(resolved_root)
300
+ target = _resolve(resolved_root, target_path) if target_path is not None else workspace.handoff_file(DEFAULT_CURRENT_HANDOFF_FILENAME)
301
+ acceptance_path = (
302
+ _resolve(resolved_root, acceptance_state_path)
303
+ if acceptance_state_path is not None
304
+ else workspace.dpa_current_handoff_acceptance_state_path()
305
+ )
306
+ target_rel = _repo_rel(resolved_root, target)
307
+ acceptance_rel = _repo_rel(resolved_root, acceptance_path)
308
+ source_file: Path | None = None
309
+ if source_fingerprint is None:
310
+ if source_path is None:
311
+ raise ValueError("source_path or source_fingerprint is required")
312
+ source_file = _resolve(resolved_root, Path(source_path))
313
+ source_rel = _repo_rel(resolved_root, source_file)
314
+ source_fingerprint = _sha256_bytes(_read_bytes(source_file))
315
+ else:
316
+ source_rel = str(source_path or "<declared-inputs>")
317
+
318
+ findings: list[DpaCurrentHandoffFinding] = list(preflight_findings)
319
+ branch = _git(resolved_root, ["branch", "--show-current"])
320
+ current_head = _git(resolved_root, ["rev-parse", "HEAD"])
321
+ effective_ref = validation_ref or current_head
322
+
323
+ if validation_ref and current_head != "UNKNOWN" and validation_ref != current_head:
324
+ findings.append(
325
+ _acceptance_finding(
326
+ "stale-validation-ref",
327
+ "validation_ref does not match the current repository HEAD",
328
+ path=target,
329
+ root=resolved_root,
330
+ )
331
+ )
332
+
333
+ if require_dp2_authorized:
334
+ readiness = evaluate_dpa_readiness(resolved_root, readiness_path=readiness_path)
335
+ if not readiness.ok or not readiness.dp2_ready:
336
+ findings.append(
337
+ _acceptance_finding(
338
+ "dp2-not-authorized",
339
+ "DPA readiness record does not structurally authorize DP2 for this target scope",
340
+ path=readiness_path,
341
+ root=resolved_root,
342
+ )
343
+ )
344
+
345
+ try:
346
+ previous_acceptance = _load_acceptance_state(acceptance_path)
347
+ except ValueError as exc:
348
+ previous_acceptance = None
349
+ findings.append(_acceptance_finding("acceptance-state-invalid", str(exc), path=acceptance_path, root=resolved_root))
350
+
351
+ target_bytes = _read_bytes(target)
352
+ target_before = _sha256_bytes(target_bytes)
353
+ projected_fingerprint = _sha256_bytes(projected_text.encode("utf-8"))
354
+ would_change = target_bytes != projected_text.encode("utf-8")
355
+
356
+ prior_status = "missing" if previous_acceptance is None else str(previous_acceptance.get("status") or "unknown")
357
+ freshness = "indeterminate"
358
+ if previous_acceptance is None:
359
+ if not initialize_acceptance:
360
+ findings.append(
361
+ _acceptance_finding(
362
+ "acceptance-state-missing",
363
+ "No DPA acceptance-state record exists; pass --initialize-acceptance for the first accepted CURRENT_HANDOFF projection",
364
+ path=acceptance_path,
365
+ root=resolved_root,
366
+ )
367
+ )
368
+ else:
369
+ accepted_target = str((previous_acceptance.get("target") or {}).get("complete_target_fingerprint") or "")
370
+ accepted_source = str((previous_acceptance.get("source") or {}).get("fingerprint") or "")
371
+ accepted_source_path = str((previous_acceptance.get("source") or {}).get("path") or "")
372
+ accepted_target_path = str((previous_acceptance.get("target") or {}).get("path") or "")
373
+ if accepted_target_path != target_rel:
374
+ findings.append(
375
+ _acceptance_finding(
376
+ "acceptance-state-scope-mismatch",
377
+ f"Acceptance-state target path is {accepted_target_path!r}, expected {target_rel!r}",
378
+ path=acceptance_path,
379
+ root=resolved_root,
380
+ )
381
+ )
382
+ elif accepted_target != target_before:
383
+ findings.append(
384
+ _acceptance_finding(
385
+ "target-drift",
386
+ "Current target bytes differ from the accepted complete-target fingerprint",
387
+ path=target,
388
+ root=resolved_root,
389
+ )
390
+ )
391
+ freshness = "stale"
392
+ elif would_change:
393
+ freshness = "stale"
394
+ elif accepted_source_path == source_rel and accepted_source == source_fingerprint:
395
+ freshness = "fresh"
396
+ else:
397
+ freshness = "fresh"
398
+
399
+ plan = DpaCurrentHandoffPlan(
400
+ branch=branch,
401
+ validation_ref=effective_ref,
402
+ target_path=target_rel,
403
+ source_path=source_rel,
404
+ acceptance_state_path=acceptance_rel,
405
+ target_before_fingerprint=target_before,
406
+ source_fingerprint=source_fingerprint,
407
+ projected_payload_fingerprint=projected_fingerprint,
408
+ projected_complete_target_fingerprint=projected_fingerprint,
409
+ would_change_target=would_change,
410
+ prior_acceptance_status=prior_status,
411
+ freshness=freshness,
412
+ )
413
+
414
+ if findings:
415
+ return DpaCurrentHandoffResult(
416
+ result_status="BLOCKED",
417
+ execute=execute,
418
+ initialized_acceptance=initialize_acceptance and previous_acceptance is None,
419
+ wrote_target=False,
420
+ wrote_acceptance_state=False,
421
+ plan=plan,
422
+ findings=tuple(findings),
423
+ )
424
+
425
+ if not execute:
426
+ status = "FRESH" if freshness == "fresh" else "WOULD_ACCEPT_INITIAL" if previous_acceptance is None else "WOULD_REFRESH"
427
+ return DpaCurrentHandoffResult(
428
+ result_status=status,
429
+ execute=False,
430
+ initialized_acceptance=initialize_acceptance and previous_acceptance is None,
431
+ wrote_target=False,
432
+ wrote_acceptance_state=False,
433
+ plan=plan,
434
+ )
435
+
436
+ try:
437
+ with workspace_mutation_lock(resolved_root, lock_command):
438
+ lock_head = _git(resolved_root, ["rev-parse", "HEAD"])
439
+ if current_head != "UNKNOWN" and lock_head != current_head:
440
+ findings.append(
441
+ _acceptance_finding(
442
+ "under-lock-head-changed",
443
+ "Repository HEAD changed between preflight and under-lock revalidation",
444
+ path=target,
445
+ root=resolved_root,
446
+ )
447
+ )
448
+ if _sha256_bytes(_read_bytes(target)) != target_before:
449
+ findings.append(
450
+ _acceptance_finding(
451
+ "under-lock-target-changed",
452
+ "Target bytes changed between preflight and under-lock revalidation",
453
+ path=target,
454
+ root=resolved_root,
455
+ )
456
+ )
457
+ if source_file is not None and _sha256_bytes(_read_bytes(source_file)) != source_fingerprint:
458
+ findings.append(
459
+ _acceptance_finding(
460
+ "under-lock-source-changed",
461
+ "Source bytes changed between preflight and under-lock revalidation",
462
+ path=source_file,
463
+ root=resolved_root,
464
+ )
465
+ )
466
+ if findings:
467
+ return DpaCurrentHandoffResult(
468
+ result_status="BLOCKED",
469
+ execute=True,
470
+ initialized_acceptance=initialize_acceptance and previous_acceptance is None,
471
+ wrote_target=False,
472
+ wrote_acceptance_state=False,
473
+ plan=plan,
474
+ findings=tuple(findings),
475
+ )
476
+
477
+ _write_text_atomic(target, projected_text)
478
+ if _sha256_bytes(_read_bytes(target)) != projected_fingerprint:
479
+ findings.append(
480
+ _acceptance_finding(
481
+ "post-write-verification-failed",
482
+ "Target bytes after Write do not match the projected output fingerprint",
483
+ path=target,
484
+ root=resolved_root,
485
+ )
486
+ )
487
+ return DpaCurrentHandoffResult(
488
+ result_status="BLOCKED",
489
+ execute=True,
490
+ initialized_acceptance=initialize_acceptance and previous_acceptance is None,
491
+ wrote_target=True,
492
+ wrote_acceptance_state=False,
493
+ plan=plan,
494
+ findings=tuple(findings),
495
+ )
496
+
497
+ acceptance = _build_acceptance_state(
498
+ plan=plan,
499
+ target_text=projected_text,
500
+ branch=branch,
501
+ validation_ref=effective_ref,
502
+ contract_id=contract_id,
503
+ target_scope=target_scope,
504
+ writer_id=writer_id,
505
+ renderer_id=renderer_id,
506
+ renderer_semantic_version=renderer_semantic_version,
507
+ )
508
+ _write_json_atomic(acceptance_path, acceptance)
509
+ except WorkspaceLockBusy as exc:
510
+ findings.append(_acceptance_finding("workspace-lock-busy", str(exc), path=target, root=resolved_root))
511
+ return DpaCurrentHandoffResult(
512
+ result_status="BLOCKED",
513
+ execute=True,
514
+ initialized_acceptance=initialize_acceptance and previous_acceptance is None,
515
+ wrote_target=False,
516
+ wrote_acceptance_state=False,
517
+ plan=plan,
518
+ findings=tuple(findings),
519
+ )
520
+
521
+ return DpaCurrentHandoffResult(
522
+ result_status="ACCEPTED",
523
+ execute=True,
524
+ initialized_acceptance=initialize_acceptance and previous_acceptance is None,
525
+ wrote_target=True,
526
+ wrote_acceptance_state=True,
527
+ plan=plan,
528
+ acceptance_state=acceptance,
529
+ )
530
+
531
+
532
+ def render_current_handoff_lifecycle_result(result: DpaCurrentHandoffResult) -> str:
533
+ lines = [
534
+ "DPA_CURRENT_HANDOFF_LIFECYCLE",
535
+ f"STATUS={result.result_status}",
536
+ f"TARGET={result.plan.target_path}",
537
+ f"ACCEPTANCE_STATE={result.plan.acceptance_state_path}",
538
+ f"VALIDATION_REF={result.plan.validation_ref}",
539
+ f"FRESHNESS={result.plan.freshness}",
540
+ f"WOULD_CHANGE_TARGET={str(result.plan.would_change_target).lower()}",
541
+ f"EXECUTE={str(result.execute).lower()}",
542
+ f"WROTE_TARGET={str(result.wrote_target).lower()}",
543
+ f"WROTE_ACCEPTANCE_STATE={str(result.wrote_acceptance_state).lower()}",
544
+ f"FINDINGS={len(result.findings)}",
545
+ ]
546
+ for finding in result.findings:
547
+ path = f" path={finding.path}" if finding.path else ""
548
+ lines.append(f"- {finding.severity} {finding.code}:{path} {finding.message}")
549
+ return "\n".join(lines) + "\n"