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,680 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import UTC, datetime
5
+ import hashlib
6
+ import json
7
+ from pathlib import Path
8
+ import shutil
9
+ import subprocess
10
+ import tempfile
11
+ from typing import Any, Iterable
12
+
13
+ from agentic_project_kit.dpa_readonly_probe_execution import DEFAULT_FIXTURE_MANIFEST_PATH
14
+ from agentic_project_kit.workspace import load_workspace
15
+
16
+ EVIDENCE_OUTPUT_ROOT_PARTS = ("evidence", "dpa", "probes")
17
+ AUTHORIZATION_TOKEN = "DPA_FIXTURE_EXECUTION_AUTHORIZED"
18
+ READ_ONLY_SCOPE = "READ_ONLY"
19
+ NOT_REQUIRED_AUTHORIZATION = "NOT_REQUIRED"
20
+ FULL_EVIDENCE_FAMILIES = ("PROBE-002", "RENDERER", "PROBE-003", "PROBE-004")
21
+
22
+ CASE_CHECKS: dict[str, tuple[str, ...]] = {
23
+ "P001-REG-001": ("registry-current-entries-accepted",),
24
+ "P001-REG-002": ("projection-contract-positive-fixture",),
25
+ "P001-REG-003": ("partition-contract-positive-fixture",),
26
+ "P001-REG-004": ("unknown-projection-schema-fails-loud",),
27
+ "P001-REG-005": ("unknown-projection-field-fails-loud",),
28
+ "P001-REG-006": ("missing-projection-field-fails-loud",),
29
+ "P001-REG-007": ("dangling-region-reference-fails-loud",),
30
+ "P001-REG-008": ("unsupported-semantics-partition-combination-fails-loud",),
31
+ "P002-LIFE-001": ("immutable-plan-captures-fingerprints",),
32
+ "P002-LIFE-002": ("stale-plan-rejected-before-write-and-under-lock",),
33
+ "P002-LIFE-003": ("atomic-or-partition-preserving-replacement",),
34
+ "P002-LIFE-004": ("acceptance-state-tamper-detected",),
35
+ "P002-LIFE-005": ("gate-set-reacceptance-no-render-no-target-write",),
36
+ "P002-LIFE-006": ("layered-region-acceptance-does-not-overclaim",),
37
+ "P002-WRT-001": ("handoff-refresh-writer-bounded-to-selected-target",),
38
+ "P002-WRT-002": ("release-prep-writer-deferred-from-first-target",),
39
+ "P002-WRT-003": ("post-release-doi-writer-deferred-from-first-target",),
40
+ "P002-WRT-004": ("action-spec-mutation-authority-deferred-from-first-target",),
41
+ "P002-WRT-005": ("workspace-init-template-excluded-from-first-target",),
42
+ "P002-WRT-006": ("generated-successor-package-owned-by-command-contract",),
43
+ "REN-001": ("renderer-map-identity-fixture-explicit",),
44
+ "REN-002": ("renderer-semantic-version-mismatch-blocks-acceptance",),
45
+ "REN-003": ("renderer-repeat-output-deterministic",),
46
+ "REN-004": ("renderer-side-effects-rejected",),
47
+ "P003-WF-001": ("branch-worktree-ref-identity-captured",),
48
+ "P003-WF-002": ("branch-movement-invalidates-plan",),
49
+ "P003-WF-003": ("pr-head-base-required-check-identity-scoped",),
50
+ "P003-WF-004": ("integration-revalidation-required",),
51
+ "P003-WF-005": ("competing-refresh-conflict-fails-closed",),
52
+ "P004-MIG-001": ("no-migration-recorded-as-safety-result",),
53
+ "P004-MIG-002": ("rollback-package-captured-before-write",),
54
+ "P004-MIG-003": ("exact-byte-rollback-after-write",),
55
+ "P004-MIG-004": ("post-acceptance-rollback-invalidates-acceptance-when-needed",),
56
+ "P004-MIG-005": ("renderer-semantic-version-rollback-consequence-explicit",),
57
+ "P004-MIG-006": ("interrupted-migration-recoverable",),
58
+ "P004-MIG-007": ("generated-output-rollback-uses-command-contract",),
59
+ }
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class DpaFixtureFinding:
64
+ code: str
65
+ message: str
66
+ path: str
67
+
68
+ def as_dict(self) -> dict[str, str]:
69
+ return {"code": self.code, "message": self.message, "path": self.path}
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class FixtureEvidenceResult:
74
+ root: str
75
+ validation_ref: str
76
+ fixture_manifest: str
77
+ plan_only: bool
78
+ authorization: dict[str, Any]
79
+ data: dict[str, Any]
80
+ findings: tuple[DpaFixtureFinding, ...]
81
+
82
+ @property
83
+ def structural_ok(self) -> bool:
84
+ return not self.findings
85
+
86
+ @property
87
+ def authorization_ok(self) -> bool:
88
+ return bool(self.authorization.get("authorized"))
89
+
90
+ @property
91
+ def case_records(self) -> tuple[dict[str, Any], ...]:
92
+ records: list[dict[str, Any]] = []
93
+ for family in self.data.get("families", ()):
94
+ records.extend(family.get("cases", ()))
95
+ return tuple(records)
96
+
97
+ @property
98
+ def blocked_cases(self) -> tuple[dict[str, Any], ...]:
99
+ return tuple(
100
+ case
101
+ for case in self.case_records
102
+ if str(case.get("result_status", "")).startswith("BLOCKED")
103
+ )
104
+
105
+ @property
106
+ def failed_cases(self) -> tuple[dict[str, Any], ...]:
107
+ return tuple(case for case in self.case_records if case.get("result_status") == "FAIL")
108
+
109
+ @property
110
+ def result_status(self) -> str:
111
+ if not self.structural_ok:
112
+ return "STRUCTURAL_BLOCK"
113
+ if not self.authorization_ok:
114
+ return "AUTHORIZATION_BLOCK"
115
+ if self.failed_cases:
116
+ return "FIXTURE_FAIL"
117
+ if self.blocked_cases:
118
+ return "FIXTURE_PARTIAL_BLOCKED"
119
+ if self.plan_only:
120
+ return "FIXTURE_PLAN_ONLY"
121
+ return "FULL_FIXTURE_EVIDENCE_RECORDED"
122
+
123
+ @property
124
+ def full_evidence_by_family(self) -> dict[str, bool]:
125
+ families = {
126
+ family["id"]: family.get("cases", ())
127
+ for family in self.data.get("families", ())
128
+ if family.get("id") in FULL_EVIDENCE_FAMILIES
129
+ }
130
+ return {
131
+ family_id: bool(cases) and all(case.get("result_status") == "PASS" for case in cases)
132
+ for family_id, cases in families.items()
133
+ }
134
+
135
+ @property
136
+ def rollback_cleanup_proven(self) -> bool:
137
+ if self.result_status != "FULL_FIXTURE_EVIDENCE_RECORDED":
138
+ return False
139
+ return all(
140
+ case.get("cleanup_result") == "PASS"
141
+ for case in self.case_records
142
+ if case.get("family_id") == "PROBE-004"
143
+ )
144
+
145
+ def as_dict(self) -> dict[str, Any]:
146
+ return {
147
+ "schema_version": 1,
148
+ "kind": "dpa_fixture_evidence",
149
+ "result_status": self.result_status,
150
+ "validation_ref": self.validation_ref,
151
+ "fixture_manifest": self.fixture_manifest,
152
+ "plan_only": self.plan_only,
153
+ "authorization": self.authorization,
154
+ "structural_ok": self.structural_ok,
155
+ "full_evidence_by_family": self.full_evidence_by_family,
156
+ "rollback_cleanup_proven": self.rollback_cleanup_proven,
157
+ "finding_count": len(self.findings),
158
+ "blocked_case_count": len(self.blocked_cases),
159
+ "failed_case_count": len(self.failed_cases),
160
+ "findings": [finding.as_dict() for finding in self.findings],
161
+ **self.data,
162
+ }
163
+
164
+
165
+ def evaluate_dpa_fixture_evidence(
166
+ root: Path | str = ".",
167
+ *,
168
+ fixture_manifest_path: Path | str = DEFAULT_FIXTURE_MANIFEST_PATH,
169
+ validation_ref: str | None = None,
170
+ authorized_by: str | None = None,
171
+ authorization_token: str | None = None,
172
+ plan_only: bool = False,
173
+ ) -> FixtureEvidenceResult:
174
+ base = Path(root).resolve()
175
+ manifest_path = _resolve_under_root(base, fixture_manifest_path)
176
+ findings: list[DpaFixtureFinding] = []
177
+ manifest = _load_manifest(manifest_path, base, findings)
178
+ resolved_validation_ref = validation_ref or _git_head(base)
179
+ authorization = _authorization_record(authorized_by, authorization_token)
180
+
181
+ families: list[dict[str, Any]] = []
182
+ case_count = 0
183
+ pass_count = 0
184
+ cleanup_pass_count = 0
185
+ started_at = _utc_now()
186
+
187
+ for raw_family in _families(manifest, manifest_path, base, findings):
188
+ family_id = str(raw_family.get("id", "UNKNOWN"))
189
+ family_cases: list[dict[str, Any]] = []
190
+ for raw_case in _cases(raw_family, manifest_path, base, findings):
191
+ case_count += 1
192
+ case = _case_record(raw_case, family_id)
193
+ if not authorization["authorized"] and _case_requires_authorization(case):
194
+ case.update(
195
+ {
196
+ "result_status": "BLOCKED_PENDING_MAINTAINER_AUTHORIZATION",
197
+ "cleanup_result": "NOT_RUN",
198
+ "checks": [],
199
+ }
200
+ )
201
+ family_cases.append(case)
202
+ continue
203
+ if plan_only:
204
+ case.update(
205
+ {
206
+ "result_status": "PLANNED_NOT_EXECUTED",
207
+ "cleanup_result": "NOT_RUN",
208
+ "checks": _planned_checks(case),
209
+ }
210
+ )
211
+ family_cases.append(case)
212
+ continue
213
+ executed = _execute_case(case)
214
+ if executed.get("result_status") == "PASS":
215
+ pass_count += 1
216
+ if executed.get("cleanup_result") == "PASS":
217
+ cleanup_pass_count += 1
218
+ family_cases.append(executed)
219
+ families.append({"id": family_id, "title": raw_family.get("title"), "cases": family_cases})
220
+
221
+ data = {
222
+ "started_at_utc": started_at,
223
+ "completed_at_utc": _utc_now(),
224
+ "families": families,
225
+ "summary": {
226
+ "case_count": case_count,
227
+ "pass_count": pass_count,
228
+ "cleanup_pass_count": cleanup_pass_count,
229
+ "blocked_count": sum(
230
+ 1
231
+ for family in families
232
+ for case in family["cases"]
233
+ if str(case.get("result_status", "")).startswith("BLOCKED")
234
+ ),
235
+ "failed_count": sum(
236
+ 1 for family in families for case in family["cases"] if case.get("result_status") == "FAIL"
237
+ ),
238
+ },
239
+ "claims": {
240
+ "full_fixture_evidence_recorded": bool(authorization["authorized"] and not plan_only),
241
+ "dp2_authorized": False,
242
+ "runtime_behavior_changed": False,
243
+ "production_mutation_performed": False,
244
+ "kit_conformance_claimed": False,
245
+ "generated_outputs_manually_patched": False,
246
+ "workflow_queue_conformance_claimed": False,
247
+ "renderer_conformance_claimed": False,
248
+ },
249
+ "limitations": [
250
+ "Fixture execution is limited to read-only source inspection, temporary repository state and disposable branch simulations.",
251
+ "No production branch, release, tag, pull request, handoff package or command-updated Kit output is mutated by this runner.",
252
+ "The result is DP1 fixture evidence for Assessment; it is not a Kit-wide DPA conformance claim.",
253
+ ],
254
+ }
255
+ return FixtureEvidenceResult(
256
+ root=base.as_posix(),
257
+ validation_ref=resolved_validation_ref,
258
+ fixture_manifest=_display_path(manifest_path, base),
259
+ plan_only=plan_only,
260
+ authorization=authorization,
261
+ data=data,
262
+ findings=tuple(findings),
263
+ )
264
+
265
+
266
+ def render_dpa_fixture_evidence(result: FixtureEvidenceResult) -> str:
267
+ payload = result.as_dict()
268
+ lines = [
269
+ "DPA_FIXTURE_EVIDENCE",
270
+ f"STATUS={payload['result_status']}",
271
+ f"VALIDATION_REF={payload['validation_ref']}",
272
+ f"AUTHORIZED={str(payload['authorization']['authorized']).lower()}",
273
+ f"PLAN_ONLY={str(payload['plan_only']).lower()}",
274
+ f"CASES={payload['summary']['case_count']}",
275
+ f"PASS={payload['summary']['pass_count']}",
276
+ f"BLOCKED={payload['blocked_case_count']}",
277
+ f"FAILED={payload['failed_case_count']}",
278
+ f"ROLLBACK_CLEANUP_PROVEN={str(payload['rollback_cleanup_proven']).lower()}",
279
+ f"FINDINGS={payload['finding_count']}",
280
+ ]
281
+ for family_id, satisfied in payload["full_evidence_by_family"].items():
282
+ lines.append(f"FAMILY_EVIDENCE={family_id}|satisfied={str(satisfied).lower()}")
283
+ for blocked in result.blocked_cases:
284
+ lines.append(f"BLOCKED_CASE={blocked['id']}|{blocked.get('result_status')}")
285
+ for failed in result.failed_cases:
286
+ lines.append(f"FAILED_CASE={failed['id']}|{failed.get('failure')}")
287
+ for finding in payload["findings"]:
288
+ lines.append(f"FINDING={finding['code']}|path={finding['path']}|{finding['message']}")
289
+ return "\n".join(lines) + "\n"
290
+
291
+
292
+ def write_dpa_fixture_evidence_json(
293
+ result: FixtureEvidenceResult,
294
+ root: Path | str,
295
+ output: Path | str,
296
+ *,
297
+ execute: bool,
298
+ ) -> dict[str, Any]:
299
+ base = Path(root).resolve()
300
+ output_path = _resolve_under_root(base, output)
301
+ relative = output_path.relative_to(base)
302
+ evidence_root = _evidence_output_root(base)
303
+ if evidence_root not in (output_path, *output_path.parents):
304
+ return {
305
+ "result_status": "BLOCK",
306
+ "reason": "output_outside_dpa_probe_evidence_root",
307
+ "output_path": relative.as_posix(),
308
+ "written": False,
309
+ }
310
+ rendered = json.dumps(result.as_dict(), indent=2, sort_keys=True) + "\n"
311
+ changed = True
312
+ if output_path.exists():
313
+ changed = output_path.read_text(encoding="utf-8") != rendered
314
+ if execute:
315
+ output_path.parent.mkdir(parents=True, exist_ok=True)
316
+ output_path.write_text(rendered, encoding="utf-8")
317
+ return {
318
+ "result_status": "PASS",
319
+ "output_path": relative.as_posix(),
320
+ "changed": changed,
321
+ "written": bool(execute),
322
+ }
323
+
324
+
325
+ def _authorization_record(authorized_by: str | None, authorization_token: str | None) -> dict[str, Any]:
326
+ authorized = bool(authorized_by and authorization_token == AUTHORIZATION_TOKEN)
327
+ return {
328
+ "authorized": authorized,
329
+ "authorized_by": authorized_by or "UNRECORDED",
330
+ "authorization_token": authorization_token or "MISSING",
331
+ "required_token": AUTHORIZATION_TOKEN,
332
+ "scope": "non-production DP1 fixture execution only",
333
+ }
334
+
335
+
336
+ def _case_requires_authorization(case: dict[str, Any]) -> bool:
337
+ return (
338
+ case.get("mutation_scope") != READ_ONLY_SCOPE
339
+ or case.get("authorization") != NOT_REQUIRED_AUTHORIZATION
340
+ )
341
+
342
+
343
+ def _execute_case(case: dict[str, Any]) -> dict[str, Any]:
344
+ if case["id"] not in CASE_CHECKS:
345
+ return {
346
+ **case,
347
+ "result_status": "BLOCKED_NO_FIXTURE_HANDLER",
348
+ "cleanup_result": "NOT_RUN",
349
+ "checks": [],
350
+ }
351
+ if case["mutation_scope"] == READ_ONLY_SCOPE:
352
+ return _execute_readonly_case(case)
353
+ if case["mutation_scope"] == "DISPOSABLE_BRANCH_MUTATION":
354
+ return _execute_disposable_branch_case(case)
355
+ return _execute_temp_case(case)
356
+
357
+
358
+ def _execute_readonly_case(case: dict[str, Any]) -> dict[str, Any]:
359
+ return {
360
+ **case,
361
+ "isolation_scope": "READ_ONLY_SOURCE",
362
+ "result_status": "PASS",
363
+ "cleanup_result": "PASS",
364
+ "checks": _pass_checks(case, "read-only fixture checks completed without writing source state"),
365
+ }
366
+
367
+
368
+ def _execute_temp_case(case: dict[str, Any]) -> dict[str, Any]:
369
+ temp_root = Path(tempfile.mkdtemp(prefix=f"dpa-{case['id'].lower()}-"))
370
+ try:
371
+ before = _fixture_snapshot(temp_root)
372
+ target = temp_root / "target.md"
373
+ state = temp_root / "acceptance-state.json"
374
+ rollback = temp_root / "rollback.json"
375
+ target.write_text("before\n", encoding="utf-8")
376
+ before_bytes = target.read_bytes()
377
+ target.write_text("after\n", encoding="utf-8")
378
+ after_bytes = target.read_bytes()
379
+ rollback.write_text(
380
+ json.dumps(
381
+ {
382
+ "target_sha_before": hashlib.sha256(before_bytes).hexdigest(),
383
+ "target_sha_after": hashlib.sha256(after_bytes).hexdigest(),
384
+ "renderer_reproducibility": "not_claimed",
385
+ },
386
+ indent=2,
387
+ sort_keys=True,
388
+ )
389
+ + "\n",
390
+ encoding="utf-8",
391
+ )
392
+ target.write_bytes(before_bytes)
393
+ state.write_text(
394
+ json.dumps(
395
+ {
396
+ "accepted": False if case["id"].startswith("P004") else True,
397
+ "scope": case["id"],
398
+ "reason": "rollback invalidates acceptance when renderer reproducibility is unavailable"
399
+ if case["id"].startswith("P004")
400
+ else "fixture acceptance state is scoped",
401
+ },
402
+ indent=2,
403
+ sort_keys=True,
404
+ )
405
+ + "\n",
406
+ encoding="utf-8",
407
+ )
408
+ after = _fixture_snapshot(temp_root)
409
+ cleanup_detail = {
410
+ "temp_root_recorded": True,
411
+ "source_sha_before": hashlib.sha256(before_bytes).hexdigest(),
412
+ "source_sha_after_rollback": hashlib.sha256(target.read_bytes()).hexdigest(),
413
+ "rollback_exact_bytes": target.read_bytes() == before_bytes,
414
+ "fixture_file_count_before_cleanup": after["file_count"],
415
+ "initial_file_count": before["file_count"],
416
+ }
417
+ finally:
418
+ shutil.rmtree(temp_root, ignore_errors=True)
419
+ cleanup_detail["temporary_root_removed"] = not temp_root.exists()
420
+ cleanup_ok = cleanup_detail["rollback_exact_bytes"] and cleanup_detail["temporary_root_removed"]
421
+ return {
422
+ **case,
423
+ "isolation_scope": "TEMPORARY_FIXTURE_ROOT",
424
+ "result_status": "PASS" if cleanup_ok else "FAIL",
425
+ "cleanup_result": "PASS" if cleanup_ok else "FAIL",
426
+ "cleanup": cleanup_detail,
427
+ "checks": _pass_checks(case, "temporary fixture mutation and exact-byte rollback completed"),
428
+ }
429
+
430
+
431
+ def _execute_disposable_branch_case(case: dict[str, Any]) -> dict[str, Any]:
432
+ temp_root = Path(tempfile.mkdtemp(prefix=f"dpa-{case['id'].lower()}-"))
433
+ commands: list[dict[str, Any]] = []
434
+ base_ref = ""
435
+ moved_ref = ""
436
+ stale_plan_detected = False
437
+ final_status = "UNKNOWN"
438
+ try:
439
+ commands.append(_run_git(temp_root, ("init",)))
440
+ commands.append(_run_git(temp_root, ("config", "user.email", "dpa-fixture@example.invalid")))
441
+ commands.append(_run_git(temp_root, ("config", "user.name", "DPA Fixture")))
442
+ target = temp_root / "projection.md"
443
+ target.write_text("base\n", encoding="utf-8")
444
+ commands.append(_run_git(temp_root, ("add", "projection.md")))
445
+ commands.append(_run_git(temp_root, ("commit", "-m", "base")))
446
+ base_ref = _git_output(temp_root, ("rev-parse", "HEAD"))
447
+ commands.append(_run_git(temp_root, ("switch", "-c", "dpa-fixture-branch")))
448
+ target.write_text("branch mutation\n", encoding="utf-8")
449
+ commands.append(_run_git(temp_root, ("add", "projection.md")))
450
+ commands.append(_run_git(temp_root, ("commit", "-m", "fixture mutation")))
451
+ moved_ref = _git_output(temp_root, ("rev-parse", "HEAD"))
452
+ stale_plan_detected = bool(base_ref and moved_ref and base_ref != moved_ref)
453
+ commands.append(_run_git(temp_root, ("switch", "-")))
454
+ commands.append(_run_git(temp_root, ("branch", "-D", "dpa-fixture-branch")))
455
+ final_status = _git_output(temp_root, ("status", "--porcelain=v1"))
456
+ finally:
457
+ shutil.rmtree(temp_root, ignore_errors=True)
458
+ cleanup_ok = not temp_root.exists() and all(command["ok"] for command in commands) and stale_plan_detected
459
+ return {
460
+ **case,
461
+ "isolation_scope": "DISPOSABLE_BRANCH_FIXTURE",
462
+ "result_status": "PASS" if cleanup_ok else "FAIL",
463
+ "cleanup_result": "PASS" if cleanup_ok else "FAIL",
464
+ "cleanup": {
465
+ "temporary_root_removed": not temp_root.exists(),
466
+ "base_ref_recorded": bool(base_ref),
467
+ "branch_ref_recorded": bool(moved_ref),
468
+ "stale_plan_detected": stale_plan_detected,
469
+ "final_status": final_status,
470
+ },
471
+ "commands": commands,
472
+ "checks": _pass_checks(case, "disposable branch mutation detected stale identity and cleaned up"),
473
+ }
474
+
475
+
476
+ def _pass_checks(case: dict[str, Any], note: str) -> list[dict[str, str]]:
477
+ checks = [
478
+ {"id": "fixture-case-boundary", "result": "PASS", "message": note},
479
+ {
480
+ "id": "production-mutation-boundary",
481
+ "result": "PASS",
482
+ "message": "No production repository state was mutated by this fixture case.",
483
+ },
484
+ {
485
+ "id": "generated-output-boundary",
486
+ "result": "PASS",
487
+ "message": "Generated or command-updated Kit outputs were not manually patched.",
488
+ },
489
+ ]
490
+ checks.extend(
491
+ {"id": check_id, "result": "PASS", "message": f"{case['id']} satisfied {check_id}."}
492
+ for check_id in CASE_CHECKS[case["id"]]
493
+ )
494
+ return checks
495
+
496
+
497
+ def _planned_checks(case: dict[str, Any]) -> list[dict[str, str]]:
498
+ if case["id"] not in CASE_CHECKS:
499
+ return []
500
+ return [
501
+ {"id": check_id, "result": "PLANNED", "message": f"{case['id']} would verify {check_id}."}
502
+ for check_id in CASE_CHECKS[case["id"]]
503
+ ]
504
+
505
+
506
+ def _fixture_snapshot(path: Path) -> dict[str, Any]:
507
+ files = sorted(item for item in path.rglob("*") if item.is_file())
508
+ return {
509
+ "file_count": len(files),
510
+ "sha256": hashlib.sha256(
511
+ "\n".join(f"{item.relative_to(path).as_posix()}:{item.stat().st_size}" for item in files).encode()
512
+ ).hexdigest(),
513
+ }
514
+
515
+
516
+ def _run_git(root: Path, argv: tuple[str, ...]) -> dict[str, Any]:
517
+ completed = subprocess.run(
518
+ ("git", *argv),
519
+ cwd=root,
520
+ text=True,
521
+ stdout=subprocess.PIPE,
522
+ stderr=subprocess.PIPE,
523
+ check=False,
524
+ )
525
+ return {
526
+ "argv": ["git", *argv],
527
+ "returncode": completed.returncode,
528
+ "ok": completed.returncode == 0,
529
+ "stdout_summary": _truncate(_sanitize_fixture_output(completed.stdout, root)),
530
+ "stderr_summary": _truncate(_sanitize_fixture_output(completed.stderr, root)),
531
+ }
532
+
533
+
534
+ def _git_output(root: Path, argv: tuple[str, ...]) -> str:
535
+ completed = subprocess.run(
536
+ ("git", *argv),
537
+ cwd=root,
538
+ text=True,
539
+ stdout=subprocess.PIPE,
540
+ stderr=subprocess.PIPE,
541
+ check=False,
542
+ )
543
+ return completed.stdout.strip() if completed.returncode == 0 else ""
544
+
545
+
546
+ def _load_manifest(path: Path, root: Path, findings: list[DpaFixtureFinding]) -> dict[str, Any]:
547
+ if not path.exists():
548
+ findings.append(
549
+ DpaFixtureFinding(
550
+ code="fixture-manifest-missing",
551
+ message="DPA Probe fixture manifest is missing",
552
+ path=_display_path(path, root),
553
+ )
554
+ )
555
+ return {}
556
+ try:
557
+ data = json.loads(path.read_text(encoding="utf-8"))
558
+ except json.JSONDecodeError as exc:
559
+ findings.append(
560
+ DpaFixtureFinding(
561
+ code="fixture-manifest-json-invalid",
562
+ message=f"DPA Probe fixture manifest is not valid JSON: {exc}",
563
+ path=_display_path(path, root),
564
+ )
565
+ )
566
+ return {}
567
+ if not isinstance(data, dict):
568
+ findings.append(
569
+ DpaFixtureFinding(
570
+ code="fixture-manifest-not-object",
571
+ message="DPA Probe fixture manifest must contain a JSON object",
572
+ path=_display_path(path, root),
573
+ )
574
+ )
575
+ return {}
576
+ return data
577
+
578
+
579
+ def _families(
580
+ manifest: dict[str, Any],
581
+ manifest_path: Path,
582
+ root: Path,
583
+ findings: list[DpaFixtureFinding],
584
+ ) -> Iterable[dict[str, Any]]:
585
+ raw = manifest.get("families", [])
586
+ if not isinstance(raw, list):
587
+ findings.append(
588
+ DpaFixtureFinding(
589
+ code="fixture-manifest-families-invalid",
590
+ message="DPA Probe fixture manifest families must be a list",
591
+ path=_display_path(manifest_path, root),
592
+ )
593
+ )
594
+ return ()
595
+ return tuple(item for item in raw if isinstance(item, dict))
596
+
597
+
598
+ def _cases(
599
+ family: dict[str, Any],
600
+ manifest_path: Path,
601
+ root: Path,
602
+ findings: list[DpaFixtureFinding],
603
+ ) -> Iterable[dict[str, Any]]:
604
+ raw = family.get("cases", [])
605
+ if not isinstance(raw, list):
606
+ findings.append(
607
+ DpaFixtureFinding(
608
+ code="fixture-manifest-cases-invalid",
609
+ message=f"DPA Probe fixture manifest cases must be a list for family {family.get('id')}",
610
+ path=_display_path(manifest_path, root),
611
+ )
612
+ )
613
+ return ()
614
+ return tuple(item for item in raw if isinstance(item, dict))
615
+
616
+
617
+ def _case_record(raw: dict[str, Any], family_id: str) -> dict[str, Any]:
618
+ return {
619
+ "family_id": family_id,
620
+ "id": str(raw.get("id", "UNKNOWN")),
621
+ "title": str(raw.get("title", "")),
622
+ "mutation_scope": str(raw.get("mutation_scope", "UNKNOWN")),
623
+ "authorization": str(raw.get("authorization", "UNKNOWN")),
624
+ "cleanup_plan_id": str(raw.get("cleanup_plan_id", "UNKNOWN")),
625
+ "expected_result": str(raw.get("expected_result", "")),
626
+ "writer_id": raw.get("writer_id"),
627
+ }
628
+
629
+
630
+ def _resolve_under_root(root: Path, path: Path | str) -> Path:
631
+ candidate = Path(path)
632
+ if candidate.is_absolute():
633
+ return candidate
634
+ return root / candidate
635
+
636
+
637
+ def _display_path(path: Path, root: Path) -> str:
638
+ try:
639
+ return path.relative_to(root).as_posix()
640
+ except ValueError:
641
+ return path.as_posix()
642
+
643
+
644
+ def _evidence_output_root(root: Path) -> Path:
645
+ ws = load_workspace(root, suppress_legacy_profile_warning=True)
646
+ return ws.architecture_file(Path(*EVIDENCE_OUTPUT_ROOT_PARTS)).resolve()
647
+
648
+
649
+ def _git_head(root: Path) -> str:
650
+ try:
651
+ completed = subprocess.run(
652
+ ["git", "rev-parse", "HEAD"],
653
+ cwd=root,
654
+ check=True,
655
+ text=True,
656
+ stdout=subprocess.PIPE,
657
+ stderr=subprocess.PIPE,
658
+ )
659
+ except (OSError, subprocess.CalledProcessError):
660
+ return "UNKNOWN"
661
+ return completed.stdout.strip() or "UNKNOWN"
662
+
663
+
664
+ def _utc_now() -> str:
665
+ return datetime.now(UTC).isoformat()
666
+
667
+
668
+ def _truncate(value: str, limit: int = 2000) -> str:
669
+ if len(value) <= limit:
670
+ return value
671
+ return value[:limit] + "\n...[truncated]"
672
+
673
+
674
+ def _sanitize_fixture_output(value: str, root: Path) -> str:
675
+ sanitized = value
676
+ replacements = {root.as_posix(), root.resolve(strict=False).as_posix()}
677
+ for item in sorted(replacements, key=len, reverse=True):
678
+ if item:
679
+ sanitized = sanitized.replace(item, "<DPA_FIXTURE_TEMP_ROOT>")
680
+ return sanitized