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,615 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import json
5
+ from pathlib import Path
6
+ import subprocess
7
+ from typing import Any
8
+
9
+ from agentic_project_kit.workspace import load_workspace
10
+
11
+ DEFAULT_DP5_STAGE_RECORD_PATH = Path(
12
+ "docs/architecture/evidence/dpa/assessment/DP5_STRICT_STAGE_RECORD_20260801.json"
13
+ )
14
+ DP5_STAGE_MODEL = "dpa-dp5-stage-adoption-v1"
15
+ VALID_STATUS = "VALID_DP5_STAGE_RECORD"
16
+ ACCEPTED_OBSERVE_STATUS = "DP5_OBSERVE_STAGE_ADOPTED"
17
+ ACCEPTED_OBSERVE_TOKEN = "DPA_DP5_OBSERVE_STAGE_AUTHORIZED"
18
+ ACCEPTED_WARN_STATUS = "DP5_WARN_STAGE_ADOPTED"
19
+ ACCEPTED_WARN_TOKEN = "DPA_DP5_WARN_STAGE_AUTHORIZED"
20
+ ACCEPTED_BLOCK_NEW_STATUS = "DP5_BLOCK_NEW_STAGE_ADOPTED"
21
+ ACCEPTED_BLOCK_NEW_TOKEN = "DPA_DP5_BLOCK_NEW_STAGE_AUTHORIZED"
22
+ ACCEPTED_STRICT_STATUS = "DP5_STRICT_STAGE_ADOPTED"
23
+ ACCEPTED_STRICT_TOKEN = "DPA_DP5_STRICT_STAGE_AUTHORIZED"
24
+ EVIDENCE_OUTPUT_ROOT_PARTS = ("evidence", "dpa", "assessment")
25
+ STAGE_SEQUENCE = ("observe", "warn", "block-new", "strict")
26
+ SUPPORTED_STAGES = ("observe", "warn", "block-new", "strict")
27
+ STAGE_CONTRACTS = {
28
+ "observe": {
29
+ "status": ACCEPTED_OBSERVE_STATUS,
30
+ "decision_token": ACCEPTED_OBSERVE_TOKEN,
31
+ "gate_set_id": "DPA_DP5_OBSERVE_GATE_SET_V1",
32
+ "stage_behavior": "observe-only",
33
+ "stage_decision": "record_only_no_new_blocking",
34
+ "rollback_stage": "pre-dp5",
35
+ },
36
+ "warn": {
37
+ "status": ACCEPTED_WARN_STATUS,
38
+ "decision_token": ACCEPTED_WARN_TOKEN,
39
+ "gate_set_id": "DPA_DP5_WARN_GATE_SET_V1",
40
+ "stage_behavior": "warn-only",
41
+ "stage_decision": "warn_without_blocking_compatibility",
42
+ "rollback_stage": "observe",
43
+ },
44
+ "block-new": {
45
+ "status": ACCEPTED_BLOCK_NEW_STATUS,
46
+ "decision_token": ACCEPTED_BLOCK_NEW_TOKEN,
47
+ "gate_set_id": "DPA_DP5_BLOCK_NEW_GATE_SET_V1",
48
+ "stage_behavior": "block-new",
49
+ "stage_decision": "block_new_nonconforming_projection_states",
50
+ "rollback_stage": "warn",
51
+ },
52
+ "strict": {
53
+ "status": ACCEPTED_STRICT_STATUS,
54
+ "decision_token": ACCEPTED_STRICT_TOKEN,
55
+ "gate_set_id": "DPA_DP5_STRICT_GATE_SET_V1",
56
+ "stage_behavior": "strict",
57
+ "stage_decision": "block_all_configured_noncompliant_states",
58
+ "rollback_stage": "block-new",
59
+ },
60
+ }
61
+
62
+ NON_STAGE_FALSE_CLAIM_FIELDS = (
63
+ "kit_wide_dpa_conformance_claimed",
64
+ "production_mutation_performed",
65
+ "generated_outputs_manually_patched",
66
+ "stable_dpa_claimed",
67
+ )
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class Dp5StageFinding:
72
+ code: str
73
+ message: str
74
+ path: str
75
+
76
+ def as_dict(self) -> dict[str, str]:
77
+ return {"code": self.code, "message": self.message, "path": self.path}
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class Dp5StageAdoptionResult:
82
+ root: str
83
+ record_path: str
84
+ record_present: bool
85
+ record_validation_ref: str
86
+ current_validation_ref: str
87
+ record_status: str
88
+ decision_token: str
89
+ active_stage: str
90
+ findings: tuple[Dp5StageFinding, ...]
91
+
92
+ @property
93
+ def ok(self) -> bool:
94
+ return self.result_status == VALID_STATUS
95
+
96
+ @property
97
+ def result_status(self) -> str:
98
+ if not self.record_present:
99
+ return "MISSING_DP5_STAGE_RECORD"
100
+ if self.findings:
101
+ return "INVALID_DP5_STAGE_RECORD"
102
+ return VALID_STATUS
103
+
104
+ def stage_accepted(self, stage: str) -> bool:
105
+ return self.ok and _stage_is_active_or_past(self.active_stage, stage)
106
+
107
+ def as_dict(self) -> dict[str, Any]:
108
+ claims = _false_claims()
109
+ for stage in STAGE_SEQUENCE:
110
+ claims[_stage_claim_field(stage)] = self.stage_accepted(stage)
111
+ return {
112
+ "schema_version": 1,
113
+ "kind": "dpa_dp5_stage_check",
114
+ "stage_model": DP5_STAGE_MODEL,
115
+ "result_status": self.result_status,
116
+ "record_path": self.record_path,
117
+ "record_present": self.record_present,
118
+ "record_validation_ref": self.record_validation_ref,
119
+ "current_validation_ref": self.current_validation_ref,
120
+ "record_status": self.record_status,
121
+ "decision_token": self.decision_token,
122
+ "active_stage": self.active_stage,
123
+ "stage_status": {
124
+ stage: "ADOPTED" if self.stage_accepted(stage) else "BLOCKED"
125
+ for stage in STAGE_SEQUENCE
126
+ },
127
+ "finding_count": len(self.findings),
128
+ "findings": [finding.as_dict() for finding in self.findings],
129
+ "claims": claims,
130
+ }
131
+
132
+
133
+ def evaluate_dp5_stage_record(
134
+ root: Path | str = ".",
135
+ *,
136
+ record_path: Path | str = DEFAULT_DP5_STAGE_RECORD_PATH,
137
+ validation_ref: str | None = None,
138
+ ) -> Dp5StageAdoptionResult:
139
+ base = Path(root).resolve()
140
+ path = _resolve_under_root(base, record_path)
141
+ display_path = _display_path(path, base)
142
+ current_ref = validation_ref or _git_head(base)
143
+
144
+ if not path.exists():
145
+ return Dp5StageAdoptionResult(
146
+ root=base.as_posix(),
147
+ record_path=display_path,
148
+ record_present=False,
149
+ record_validation_ref="",
150
+ current_validation_ref=current_ref,
151
+ record_status="MISSING",
152
+ decision_token="",
153
+ active_stage="none",
154
+ findings=(
155
+ Dp5StageFinding(
156
+ code="dp5-stage-record-missing",
157
+ message="Record an exact DP5 stage authorization before adopting a lifecycle-gate stage.",
158
+ path=display_path,
159
+ ),
160
+ ),
161
+ )
162
+
163
+ try:
164
+ data = json.loads(path.read_text(encoding="utf-8"))
165
+ except json.JSONDecodeError as exc:
166
+ return _invalid_result(base, display_path, current_ref, f"record JSON is invalid: {exc}")
167
+ if not isinstance(data, dict):
168
+ return _invalid_result(base, display_path, current_ref, "record root must be a JSON object")
169
+
170
+ findings: list[Dp5StageFinding] = []
171
+ _validate_header(data, findings, display_path, root=base)
172
+ _validate_stage_scope(data, findings, display_path, root=base)
173
+ _validate_gate_set(data, findings, display_path, root=base)
174
+ _validate_findings_mapping(data, findings, display_path)
175
+ _validate_rollback(data, findings, display_path, root=base)
176
+ _validate_claims(data, findings, display_path)
177
+
178
+ return Dp5StageAdoptionResult(
179
+ root=base.as_posix(),
180
+ record_path=display_path,
181
+ record_present=True,
182
+ record_validation_ref=str(data.get("validation_ref", "")),
183
+ current_validation_ref=current_ref,
184
+ record_status=str(data.get("status", "")),
185
+ decision_token=str(data.get("decision_token", "")),
186
+ active_stage=str(data.get("stage", "none")),
187
+ findings=tuple(findings),
188
+ )
189
+
190
+
191
+ def render_dp5_stage_check(result: Dp5StageAdoptionResult) -> str:
192
+ payload = result.as_dict()
193
+ lines = [
194
+ "DPA_DP5_STAGE_CHECK",
195
+ f"STATUS={payload['result_status']}",
196
+ f"RECORD={payload['record_path']}",
197
+ f"RECORD_VALIDATION_REF={payload['record_validation_ref']}",
198
+ f"CURRENT_VALIDATION_REF={payload['current_validation_ref']}",
199
+ f"RECORD_STATUS={payload['record_status']}",
200
+ f"DECISION_TOKEN={payload['decision_token']}",
201
+ f"ACTIVE_STAGE={payload['active_stage']}",
202
+ f"FINDINGS={payload['finding_count']}",
203
+ ]
204
+ for stage, status in payload["stage_status"].items():
205
+ lines.append(f"DP5_STAGE={stage}|status={status}")
206
+ for finding in result.findings:
207
+ lines.append(f"FINDING={finding.code}|path={finding.path}|{finding.message}")
208
+ return "\n".join(lines) + "\n"
209
+
210
+
211
+ def write_dp5_stage_check_json(
212
+ result: Dp5StageAdoptionResult,
213
+ root: Path | str,
214
+ output: Path | str,
215
+ *,
216
+ execute: bool,
217
+ ) -> dict[str, Any]:
218
+ base = Path(root).resolve()
219
+ output_path = _resolve_under_root(base, output)
220
+ relative = output_path.relative_to(base)
221
+ evidence_root = _evidence_output_root(base)
222
+ if evidence_root not in (output_path, *output_path.parents):
223
+ return {
224
+ "result_status": "BLOCK",
225
+ "reason": "output_outside_dpa_assessment_evidence_root",
226
+ "output_path": relative.as_posix(),
227
+ "written": False,
228
+ }
229
+ rendered = json.dumps(result.as_dict(), indent=2, sort_keys=True) + "\n"
230
+ changed = True
231
+ if output_path.exists():
232
+ changed = output_path.read_text(encoding="utf-8") != rendered
233
+ if execute:
234
+ output_path.parent.mkdir(parents=True, exist_ok=True)
235
+ output_path.write_text(rendered, encoding="utf-8")
236
+ return {
237
+ "result_status": "PASS",
238
+ "output_path": relative.as_posix(),
239
+ "changed": changed,
240
+ "written": bool(execute),
241
+ }
242
+
243
+
244
+ def _invalid_result(
245
+ root: Path,
246
+ display_path: str,
247
+ current_ref: str,
248
+ message: str,
249
+ ) -> Dp5StageAdoptionResult:
250
+ return Dp5StageAdoptionResult(
251
+ root=root.as_posix(),
252
+ record_path=display_path,
253
+ record_present=True,
254
+ record_validation_ref="",
255
+ current_validation_ref=current_ref,
256
+ record_status="INVALID",
257
+ decision_token="",
258
+ active_stage="none",
259
+ findings=(
260
+ Dp5StageFinding(
261
+ code="dp5-stage-record-invalid-json",
262
+ message=message,
263
+ path=display_path,
264
+ ),
265
+ ),
266
+ )
267
+
268
+
269
+ def _validate_header(
270
+ data: dict[str, Any],
271
+ findings: list[Dp5StageFinding],
272
+ path: str,
273
+ *,
274
+ root: Path,
275
+ ) -> None:
276
+ if data.get("schema_version") != 1:
277
+ _finding(findings, "schema-version-invalid", "schema_version must be 1", path)
278
+ if data.get("kind") != "dpa_dp5_stage_record":
279
+ _finding(findings, "kind-invalid", "kind must be dpa_dp5_stage_record", path)
280
+ stage = str(data.get("stage", ""))
281
+ contract = STAGE_CONTRACTS.get(stage)
282
+ if contract is None:
283
+ _finding(
284
+ findings,
285
+ "unsupported-stage",
286
+ f"this bounded slice only supports DP5 stages: {', '.join(SUPPORTED_STAGES)}",
287
+ path,
288
+ )
289
+ else:
290
+ if data.get("status") != contract["status"]:
291
+ _finding(findings, "status-not-adopted", f"status must be {contract['status']}", path)
292
+ if data.get("decision_token") != contract["decision_token"]:
293
+ _finding(
294
+ findings,
295
+ "decision-token-invalid",
296
+ f"decision_token must be {contract['decision_token']}",
297
+ path,
298
+ )
299
+ validation_ref = str(data.get("validation_ref", ""))
300
+ if not validation_ref:
301
+ _finding(findings, "validation-ref-missing", "validation_ref must record an exact ref", path)
302
+ elif _git_ref_exists(root, validation_ref) is False:
303
+ _finding(findings, "validation-ref-unresolvable", f"validation_ref is not a known commit: {validation_ref}", path)
304
+ if not str(data.get("maintainer", "")).strip():
305
+ _finding(findings, "maintainer-missing", "maintainer authorization text must be recorded", path)
306
+
307
+
308
+ def _validate_stage_scope(
309
+ data: dict[str, Any],
310
+ findings: list[Dp5StageFinding],
311
+ path: str,
312
+ *,
313
+ root: Path,
314
+ ) -> None:
315
+ stage = str(data.get("stage", ""))
316
+ scope = _mapping(data.get("target_scope"))
317
+ if scope.get("id") != "DPA_POST_DP2_DP3_DP4_ACCEPTED_SCOPE":
318
+ _finding(findings, "target-scope-id-invalid", "target_scope.id must name the accepted DP3/DP4 scope", path)
319
+ if scope.get("enforcement_stage") != stage:
320
+ _finding(
321
+ findings,
322
+ "target-scope-stage-invalid",
323
+ f"target_scope.enforcement_stage must be {stage}",
324
+ path,
325
+ )
326
+ if stage == "warn":
327
+ previous = str(scope.get("previous_stage_record", "")).strip()
328
+ if not previous:
329
+ _finding(findings, "previous-stage-record-missing", "warn scope must name previous_stage_record", path)
330
+ elif not (root / previous).exists():
331
+ _finding(findings, "previous-stage-record-missing", f"missing previous stage record: {previous}", path)
332
+ if stage == "block-new":
333
+ previous = str(scope.get("previous_stage_record", "")).strip()
334
+ if not previous:
335
+ _finding(findings, "previous-stage-record-missing", "block-new scope must name previous_stage_record", path)
336
+ elif not (root / previous).exists():
337
+ _finding(findings, "previous-stage-record-missing", f"missing previous stage record: {previous}", path)
338
+ baseline = str(scope.get("baseline_assessment", "")).strip()
339
+ if not baseline:
340
+ _finding(findings, "baseline-assessment-missing", "block-new scope must name baseline_assessment", path)
341
+ elif not (root / baseline).exists():
342
+ _finding(findings, "baseline-assessment-missing", f"missing baseline assessment: {baseline}", path)
343
+ if stage == "strict":
344
+ previous = str(scope.get("previous_stage_record", "")).strip()
345
+ if not previous:
346
+ _finding(findings, "previous-stage-record-missing", "strict scope must name previous_stage_record", path)
347
+ elif not (root / previous).exists():
348
+ _finding(findings, "previous-stage-record-missing", f"missing previous stage record: {previous}", path)
349
+ _require_existing_paths(scope.get("evidence"), root, findings, path, "target-scope-evidence-missing")
350
+
351
+
352
+ def _validate_gate_set(
353
+ data: dict[str, Any],
354
+ findings: list[Dp5StageFinding],
355
+ path: str,
356
+ *,
357
+ root: Path,
358
+ ) -> None:
359
+ stage = str(data.get("stage", ""))
360
+ contract = STAGE_CONTRACTS.get(stage)
361
+ gate_set = _mapping(data.get("gate_set"))
362
+ if contract is not None and gate_set.get("id") != contract["gate_set_id"]:
363
+ _finding(findings, "gate-set-id-invalid", f"gate_set.id must be {contract['gate_set_id']}", path)
364
+ if contract is not None and gate_set.get("stage_behavior") != contract["stage_behavior"]:
365
+ _finding(
366
+ findings,
367
+ "gate-set-stage-behavior-invalid",
368
+ f"gate_set.stage_behavior must be {contract['stage_behavior']}",
369
+ path,
370
+ )
371
+ if gate_set.get("blocks_unrelated_work") is not False:
372
+ _finding(findings, "gate-set-overblocks", "DP5 observe/warn gate set must not block unrelated work", path)
373
+ if stage == "warn":
374
+ if gate_set.get("warns_on_noncompliance") is not True:
375
+ _finding(findings, "gate-set-warning-disabled", "warn gate set must enable noncompliance warnings", path)
376
+ if not _string_list(gate_set.get("warning_surfaces")):
377
+ _finding(findings, "gate-set-warning-surfaces-missing", "warn gate set must list warning_surfaces", path)
378
+ if stage == "block-new":
379
+ if gate_set.get("blocks_new_noncompliance") is not True:
380
+ _finding(
381
+ findings,
382
+ "gate-set-block-new-disabled",
383
+ "block-new gate set must enable new noncompliance blocking",
384
+ path,
385
+ )
386
+ _require_existing_paths(
387
+ gate_set.get("baseline_evidence"),
388
+ root,
389
+ findings,
390
+ path,
391
+ "gate-set-baseline-evidence-missing",
392
+ )
393
+ if stage == "strict":
394
+ if gate_set.get("blocks_all_configured_noncompliance") is not True:
395
+ _finding(
396
+ findings,
397
+ "gate-set-strict-disabled",
398
+ "strict gate set must block all configured noncompliance",
399
+ path,
400
+ )
401
+ if gate_set.get("strict_scope_limited") is not True:
402
+ _finding(
403
+ findings,
404
+ "gate-set-strict-scope-unbounded",
405
+ "strict gate set must declare strict_scope_limited=true",
406
+ path,
407
+ )
408
+ commands = _list_of_mappings(gate_set.get("commands"))
409
+ if not commands:
410
+ _finding(findings, "gate-set-commands-missing", "gate_set.commands must not be empty", path)
411
+ for index, command in enumerate(commands):
412
+ if not str(command.get("command", "")).strip():
413
+ _finding(findings, "gate-set-command-missing", f"gate_set.commands[{index}] needs command", path)
414
+ _require_existing_paths(command.get("evidence"), root, findings, path, "gate-set-command-evidence-missing")
415
+
416
+
417
+ def _validate_findings_mapping(
418
+ data: dict[str, Any],
419
+ findings: list[Dp5StageFinding],
420
+ path: str,
421
+ ) -> None:
422
+ stage = str(data.get("stage", ""))
423
+ contract = STAGE_CONTRACTS.get(stage)
424
+ mapping = _mapping(data.get("findings_mapping"))
425
+ required = _mapping(mapping.get("unknown_mutation_safety_finding"))
426
+ if required.get("disposition") != "fail_closed_for_mutation_safety":
427
+ _finding(
428
+ findings,
429
+ "unknown-mutation-safety-disposition-invalid",
430
+ "unknown mutation-safety findings must fail closed",
431
+ path,
432
+ )
433
+ if contract is not None and mapping.get("stage_decision") != contract["stage_decision"]:
434
+ _finding(
435
+ findings,
436
+ "stage-decision-invalid",
437
+ f"{stage} mapping must use {contract['stage_decision']}",
438
+ path,
439
+ )
440
+ if stage == "warn":
441
+ observed = _mapping(mapping.get("observed_noncompliance"))
442
+ if observed.get("disposition") != "emit_warning_without_blocking_unrelated_work":
443
+ _finding(
444
+ findings,
445
+ "observed-noncompliance-disposition-invalid",
446
+ "warn mapping must emit warnings without blocking unrelated work",
447
+ path,
448
+ )
449
+ if stage == "block-new":
450
+ observed = _mapping(mapping.get("observed_noncompliance"))
451
+ if observed.get("disposition") != "block_new_nonconformance_with_baseline_allowlist":
452
+ _finding(
453
+ findings,
454
+ "observed-noncompliance-disposition-invalid",
455
+ "block-new mapping must block new nonconformance against the accepted baseline",
456
+ path,
457
+ )
458
+ if stage == "strict":
459
+ observed = _mapping(mapping.get("observed_noncompliance"))
460
+ if observed.get("disposition") != "block_all_configured_noncompliance":
461
+ _finding(
462
+ findings,
463
+ "observed-noncompliance-disposition-invalid",
464
+ "strict mapping must block all configured noncompliance in the accepted scope",
465
+ path,
466
+ )
467
+
468
+
469
+ def _validate_rollback(
470
+ data: dict[str, Any],
471
+ findings: list[Dp5StageFinding],
472
+ path: str,
473
+ *,
474
+ root: Path,
475
+ ) -> None:
476
+ stage = str(data.get("stage", ""))
477
+ contract = STAGE_CONTRACTS.get(stage)
478
+ rollback = _mapping(data.get("rollback"))
479
+ if contract is not None and rollback.get("less_strict_stage") != contract["rollback_stage"]:
480
+ _finding(
481
+ findings,
482
+ "rollback-stage-invalid",
483
+ f"rollback.less_strict_stage must be {contract['rollback_stage']}",
484
+ path,
485
+ )
486
+ if rollback.get("tested_or_adjudicated") is not True:
487
+ _finding(findings, "rollback-not-proven", "rollback must be tested or explicitly adjudicated", path)
488
+ _require_existing_paths(rollback.get("evidence"), root, findings, path, "rollback-evidence-missing")
489
+
490
+
491
+ def _validate_claims(
492
+ data: dict[str, Any],
493
+ findings: list[Dp5StageFinding],
494
+ path: str,
495
+ ) -> None:
496
+ active_stage = str(data.get("stage", ""))
497
+ claims = _mapping(data.get("claims"))
498
+ for stage in STAGE_SEQUENCE:
499
+ field = _stage_claim_field(stage)
500
+ expected = _stage_is_active_or_past(active_stage, stage)
501
+ if claims.get(field) is not expected:
502
+ _finding(findings, "stage-claim-invalid", f"claims.{field} must be {str(expected).lower()}", path)
503
+ for field in NON_STAGE_FALSE_CLAIM_FIELDS:
504
+ if claims.get(field) is not False:
505
+ _finding(findings, "false-claim-invalid", f"claims.{field} must be false", path)
506
+
507
+
508
+ def _require_existing_paths(
509
+ value: Any,
510
+ root: Path,
511
+ findings: list[Dp5StageFinding],
512
+ path: str,
513
+ code: str,
514
+ ) -> None:
515
+ paths = _string_list(value)
516
+ if not paths:
517
+ _finding(findings, code, "evidence paths must be a non-empty string list", path)
518
+ return
519
+ for evidence_path in paths:
520
+ if not (root / evidence_path).exists():
521
+ _finding(findings, code, f"missing evidence path: {evidence_path}", path)
522
+
523
+
524
+ def _list_of_mappings(value: Any) -> list[dict[str, Any]]:
525
+ if not isinstance(value, list):
526
+ return []
527
+ return [item for item in value if isinstance(item, dict)]
528
+
529
+
530
+ def _string_list(value: Any) -> list[str]:
531
+ if not isinstance(value, list):
532
+ return []
533
+ return [item for item in value if isinstance(item, str) and item]
534
+
535
+
536
+ def _mapping(value: Any) -> dict[str, Any]:
537
+ return value if isinstance(value, dict) else {}
538
+
539
+
540
+ def _finding(findings: list[Dp5StageFinding], code: str, message: str, path: str) -> None:
541
+ findings.append(Dp5StageFinding(code=code, message=message, path=path))
542
+
543
+
544
+ def _false_claims() -> dict[str, bool]:
545
+ return {
546
+ "observe_stage_active": False,
547
+ "warn_stage_active": False,
548
+ "block_new_stage_active": False,
549
+ "strict_stage_active": False,
550
+ "kit_wide_dpa_conformance_claimed": False,
551
+ "production_mutation_performed": False,
552
+ "generated_outputs_manually_patched": False,
553
+ "stable_dpa_claimed": False,
554
+ }
555
+
556
+
557
+ def _stage_claim_field(stage: str) -> str:
558
+ if stage == "block-new":
559
+ return "block_new_stage_active"
560
+ return f"{stage}_stage_active"
561
+
562
+
563
+ def _stage_is_active_or_past(active_stage: str, stage: str) -> bool:
564
+ try:
565
+ return STAGE_SEQUENCE.index(stage) <= STAGE_SEQUENCE.index(active_stage)
566
+ except ValueError:
567
+ return False
568
+
569
+
570
+ def _resolve_under_root(root: Path, path: Path | str) -> Path:
571
+ candidate = Path(path)
572
+ if candidate.is_absolute():
573
+ return candidate
574
+ return root / candidate
575
+
576
+
577
+ def _display_path(path: Path, root: Path) -> str:
578
+ try:
579
+ return path.relative_to(root).as_posix()
580
+ except ValueError:
581
+ return path.as_posix()
582
+
583
+
584
+ def _evidence_output_root(root: Path) -> Path:
585
+ ws = load_workspace(root, suppress_legacy_profile_warning=True)
586
+ return ws.architecture_file(Path(*EVIDENCE_OUTPUT_ROOT_PARTS)).resolve()
587
+
588
+
589
+ def _git_head(root: Path) -> str:
590
+ try:
591
+ completed = subprocess.run(
592
+ ["git", "rev-parse", "HEAD"],
593
+ cwd=root,
594
+ check=True,
595
+ text=True,
596
+ stdout=subprocess.PIPE,
597
+ stderr=subprocess.PIPE,
598
+ )
599
+ except (OSError, subprocess.CalledProcessError):
600
+ return "UNKNOWN"
601
+ return completed.stdout.strip() or "UNKNOWN"
602
+
603
+
604
+ def _git_ref_exists(root: Path, ref: str) -> bool | None:
605
+ if not (root / ".git").exists():
606
+ return None
607
+ completed = subprocess.run(
608
+ ["git", "rev-parse", "--verify", f"{ref}^{{commit}}"],
609
+ cwd=root,
610
+ check=False,
611
+ text=True,
612
+ stdout=subprocess.PIPE,
613
+ stderr=subprocess.PIPE,
614
+ )
615
+ return completed.returncode == 0