okstra 0.205.1 → 0.206.1

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 (233) hide show
  1. package/README.md +2 -2
  2. package/dist/commands/lifecycle/install.mjs +2 -1
  3. package/dist/commands/lifecycle/install.mjs.map +1 -1
  4. package/docs/architecture.md +16 -16
  5. package/docs/cli.md +4 -4
  6. package/docs/contributor-change-matrix.md +3 -2
  7. package/docs/performance-improvement-plan-v2.md +1 -1
  8. package/docs/project-structure-overview.md +45 -21
  9. package/package.json +2 -3
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/bin/okstra-spawn-followups.py +2 -2
  12. package/runtime/prompts/launch.template.md +1 -1
  13. package/runtime/prompts/lead/context-loader.md +1 -1
  14. package/runtime/prompts/lead/convergence.md +3 -3
  15. package/runtime/prompts/lead/okstra-lead-contract.md +14 -55
  16. package/runtime/prompts/lead/phase-routing.md +82 -0
  17. package/runtime/prompts/lead/report-writer.md +2 -2
  18. package/runtime/prompts/lead/team-contract.md +1 -1
  19. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  20. package/runtime/prompts/profiles/_common-contract.md +1 -1
  21. package/runtime/prompts/profiles/_coverage-critic.md +1 -1
  22. package/runtime/prompts/profiles/forbidden-actions.json +0 -94
  23. package/runtime/python/okstra_ctl/agent/prompt_cli/corrections.py +1 -1
  24. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +10 -1
  25. package/runtime/python/okstra_ctl/analysis_inputs.py +0 -39
  26. package/runtime/python/okstra_ctl/analysis_scope.py +31 -0
  27. package/runtime/python/okstra_ctl/asset_roots.py +19 -0
  28. package/runtime/python/okstra_ctl/consumers.py +12 -0
  29. package/runtime/python/okstra_ctl/contract_graph.py +75 -10
  30. package/runtime/python/okstra_ctl/dispatch_state.py +22 -0
  31. package/runtime/python/okstra_ctl/doctor.py +15 -4
  32. package/runtime/python/okstra_ctl/execution_mutation_audit.py +27 -1
  33. package/runtime/python/okstra_ctl/handoff.py +11 -466
  34. package/runtime/python/okstra_ctl/handoff_error.py +5 -0
  35. package/runtime/python/okstra_ctl/implementation_direction.py +9 -485
  36. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +8 -1
  37. package/runtime/python/okstra_ctl/next_phase.py +2 -2
  38. package/runtime/python/okstra_ctl/option_comparison.py +3 -165
  39. package/runtime/python/okstra_ctl/option_votes.py +3 -191
  40. package/runtime/python/okstra_ctl/paths.py +22 -6
  41. package/runtime/python/okstra_ctl/phases/__init__.py +4 -0
  42. package/runtime/python/okstra_ctl/phases/catalog.py +260 -0
  43. package/runtime/python/okstra_ctl/phases/change_impact_analysis/boundary.json +11 -0
  44. package/runtime/python/okstra_ctl/phases/change_impact_analysis/entry.py +39 -0
  45. package/runtime/python/okstra_ctl/{report_html/view_models/change_impact_analysis.py → phases/change_impact_analysis/report.py} +3 -3
  46. package/runtime/python/okstra_ctl/phases/change_impact_analysis/spec.md +26 -0
  47. package/runtime/python/okstra_ctl/phases/change_impact_analysis/validation.py +23 -0
  48. package/runtime/python/okstra_ctl/phases/error_analysis/__init__.py +1 -0
  49. package/runtime/python/okstra_ctl/phases/error_analysis/boundary.json +9 -0
  50. package/runtime/{prompts/profiles/error-analysis.md → python/okstra_ctl/phases/error_analysis/profile.md} +2 -2
  51. package/runtime/python/okstra_ctl/{report_html/view_models/error_analysis.py → phases/error_analysis/report.py} +9 -8
  52. package/runtime/{templates/reports → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis-input.template.md +1 -1
  53. package/{docs/task-process/error-analysis.md → runtime/python/okstra_ctl/phases/error_analysis/spec.md} +22 -7
  54. package/runtime/python/okstra_ctl/phases/error_analysis/validation.py +241 -0
  55. package/runtime/python/okstra_ctl/phases/feature_analysis/__init__.py +1 -0
  56. package/runtime/python/okstra_ctl/phases/feature_analysis/boundary.json +8 -0
  57. package/runtime/python/okstra_ctl/phases/feature_analysis/entry.py +63 -0
  58. package/runtime/python/okstra_ctl/{report_html/view_models/feature_analysis.py → phases/feature_analysis/report.py} +12 -5
  59. package/runtime/python/okstra_ctl/phases/feature_analysis/spec.md +22 -0
  60. package/runtime/python/okstra_ctl/phases/feature_analysis/validation.py +27 -0
  61. package/runtime/python/okstra_ctl/phases/feature_analysis/wizard.py +95 -0
  62. package/runtime/python/okstra_ctl/phases/final_verification/__init__.py +4 -0
  63. package/runtime/python/okstra_ctl/phases/final_verification/boundary.json +8 -0
  64. package/runtime/python/okstra_ctl/phases/final_verification/entry.py +166 -0
  65. package/runtime/{prompts/profiles/final-verification.md → python/okstra_ctl/phases/final_verification/profile.md} +5 -5
  66. package/runtime/python/okstra_ctl/{report_html/view_models/final_verification.py → phases/final_verification/report.py} +12 -3
  67. package/runtime/{templates/reports → python/okstra_ctl/phases/final_verification/report_assets}/final-verification-input.template.md +1 -1
  68. package/{docs/task-process/final-verification.md → runtime/python/okstra_ctl/phases/final_verification/spec.md} +42 -25
  69. package/runtime/python/okstra_ctl/phases/final_verification/target.py +296 -0
  70. package/runtime/python/okstra_ctl/phases/final_verification/validation.py +190 -0
  71. package/runtime/python/okstra_ctl/phases/final_verification/wizard.py +38 -0
  72. package/runtime/python/okstra_ctl/phases/implementation/__init__.py +1 -0
  73. package/runtime/python/okstra_ctl/phases/implementation/boundary.json +17 -0
  74. package/runtime/python/okstra_ctl/{implementation_stage.py → phases/implementation/entry.py} +22 -10
  75. package/runtime/{prompts/host-orchestration/implementation.md → python/okstra_ctl/phases/implementation/host-rules.md} +1 -1
  76. package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-deliverable.md +1 -1
  77. package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-executor.md +4 -3
  78. package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-verifier.md +4 -4
  79. package/runtime/{prompts/profiles/implementation.md → python/okstra_ctl/phases/implementation/profile.md} +5 -5
  80. package/runtime/python/okstra_ctl/{report_html/view_models/implementation.py → phases/implementation/report.py} +3 -3
  81. package/runtime/{templates/reports → python/okstra_ctl/phases/implementation/report_assets}/implementation-input.template.md +1 -1
  82. package/{docs/task-process/implementation.md → runtime/python/okstra_ctl/phases/implementation/spec.md} +20 -8
  83. package/runtime/python/okstra_ctl/phases/implementation/validation.py +205 -0
  84. package/runtime/python/okstra_ctl/phases/implementation/wizard.py +39 -0
  85. package/runtime/python/okstra_ctl/phases/implementation_option_selection/__init__.py +1 -0
  86. package/runtime/python/okstra_ctl/phases/implementation_option_selection/authoring.py +80 -0
  87. package/runtime/python/okstra_ctl/phases/implementation_option_selection/boundary.json +10 -0
  88. package/runtime/python/okstra_ctl/phases/implementation_option_selection/comparison.py +168 -0
  89. package/runtime/python/okstra_ctl/phases/implementation_option_selection/entry.py +27 -0
  90. package/runtime/{prompts/profiles/implementation-option-selection.md → python/okstra_ctl/phases/implementation_option_selection/profile.md} +2 -2
  91. package/runtime/python/okstra_ctl/{report_html/view_models/implementation_option_selection.py → phases/implementation_option_selection/report.py} +2 -2
  92. package/{docs/task-process/implementation-option-selection.md → runtime/python/okstra_ctl/phases/implementation_option_selection/spec.md} +20 -7
  93. package/runtime/python/okstra_ctl/{implementation_options.py → phases/implementation_option_selection/validation.py} +3 -3
  94. package/runtime/python/okstra_ctl/phases/implementation_option_selection/votes.py +194 -0
  95. package/runtime/python/okstra_ctl/phases/implementation_planning/__init__.py +1 -0
  96. package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +2345 -0
  97. package/runtime/python/okstra_ctl/phases/implementation_planning/boundary.json +12 -0
  98. package/runtime/python/okstra_ctl/phases/implementation_planning/entry.py +161 -0
  99. package/runtime/python/okstra_ctl/phases/implementation_planning/guidance.py +178 -0
  100. package/runtime/{prompts/lead → python/okstra_ctl/phases/implementation_planning/instructions}/plan-body-verification.md +61 -51
  101. package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +3295 -0
  102. package/runtime/{prompts/profiles/implementation-planning.md → python/okstra_ctl/phases/implementation_planning/profile.md} +74 -25
  103. package/runtime/python/okstra_ctl/phases/implementation_planning/report.py +237 -0
  104. package/runtime/{templates/reports → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning-input.template.md +2 -2
  105. package/{docs/task-process/implementation-planning.md → runtime/python/okstra_ctl/phases/implementation_planning/spec.md} +30 -6
  106. package/runtime/python/okstra_ctl/phases/implementation_planning/validation.py +597 -0
  107. package/runtime/python/okstra_ctl/phases/implementation_planning/wizard.py +166 -0
  108. package/runtime/python/okstra_ctl/phases/improvement_discovery/boundary.json +12 -0
  109. package/runtime/python/okstra_ctl/{improvement_lenses.py → phases/improvement_discovery/lenses.py} +1 -6
  110. package/runtime/{prompts/profiles/improvement-discovery.md → python/okstra_ctl/phases/improvement_discovery/profile.md} +5 -5
  111. package/runtime/python/okstra_ctl/{report_html/view_models/improvement_discovery.py → phases/improvement_discovery/report.py} +3 -3
  112. package/runtime/{templates/reports → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery-input.template.md +1 -2
  113. package/runtime/python/okstra_ctl/phases/improvement_discovery/spec.md +29 -0
  114. package/runtime/{validators/validate_improvement_report.py → python/okstra_ctl/phases/improvement_discovery/validation.py} +5 -14
  115. package/runtime/python/okstra_ctl/phases/project_analysis/__init__.py +1 -0
  116. package/runtime/python/okstra_ctl/phases/project_analysis/boundary.json +8 -0
  117. package/runtime/python/okstra_ctl/phases/project_analysis/entry.py +11 -0
  118. package/runtime/python/okstra_ctl/{report_html/view_models/project_analysis.py → phases/project_analysis/report.py} +3 -3
  119. package/runtime/python/okstra_ctl/phases/project_analysis/spec.md +33 -0
  120. package/runtime/python/okstra_ctl/phases/project_analysis/validation.py +55 -0
  121. package/runtime/python/okstra_ctl/phases/release_handoff/__init__.py +1 -0
  122. package/runtime/python/okstra_ctl/phases/release_handoff/boundary.json +17 -0
  123. package/runtime/python/okstra_ctl/phases/release_handoff/entry.py +147 -0
  124. package/runtime/python/okstra_ctl/phases/release_handoff/operations.py +446 -0
  125. package/runtime/{prompts/profiles/release-handoff.md → python/okstra_ctl/phases/release_handoff/profile.md} +3 -3
  126. package/runtime/python/okstra_ctl/{report_html/view_models/release_handoff.py → phases/release_handoff/report.py} +3 -3
  127. package/runtime/{templates/reports → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff-input.template.md +1 -1
  128. package/{docs/task-process/release-handoff.md → runtime/python/okstra_ctl/phases/release_handoff/spec.md} +22 -9
  129. package/runtime/python/okstra_ctl/phases/release_handoff/wizard.py +84 -0
  130. package/runtime/python/okstra_ctl/phases/requirements_discovery/__init__.py +1 -0
  131. package/runtime/python/okstra_ctl/phases/requirements_discovery/boundary.json +9 -0
  132. package/runtime/{prompts/profiles/requirements-discovery.md → python/okstra_ctl/phases/requirements_discovery/profile.md} +2 -3
  133. package/runtime/python/okstra_ctl/{report_html/view_models/requirements_discovery.py → phases/requirements_discovery/report.py} +3 -3
  134. package/{docs/task-process/requirements-discovery.md → runtime/python/okstra_ctl/phases/requirements_discovery/spec.md} +25 -6
  135. package/runtime/{validators/validate_fanout.py → python/okstra_ctl/phases/requirements_discovery/validation.py} +11 -12
  136. package/runtime/python/okstra_ctl/phases/technical_verification/__init__.py +1 -0
  137. package/runtime/python/okstra_ctl/phases/technical_verification/boundary.json +9 -0
  138. package/runtime/python/okstra_ctl/phases/technical_verification/entry.py +100 -0
  139. package/runtime/{prompts/profiles/technical-verification.md → python/okstra_ctl/phases/technical_verification/profile.md} +1 -1
  140. package/runtime/python/okstra_ctl/{report_html/view_models/technical_verification.py → phases/technical_verification/report.py} +2 -2
  141. package/runtime/python/okstra_ctl/phases/technical_verification/spec.md +37 -0
  142. package/runtime/python/okstra_ctl/phases/technical_verification/validation.py +90 -0
  143. package/runtime/python/okstra_ctl/plan_approval.py +70 -0
  144. package/runtime/python/okstra_ctl/plan_items_cli.py +2 -2130
  145. package/runtime/python/okstra_ctl/profile_show.py +9 -3
  146. package/runtime/python/okstra_ctl/render.py +9 -2
  147. package/runtime/python/okstra_ctl/render_final_report.py +3 -2
  148. package/runtime/python/okstra_ctl/report_assembly.py +14 -93
  149. package/runtime/python/okstra_ctl/report_html/context_links.py +1 -1
  150. package/runtime/python/okstra_ctl/report_html/render.py +3 -2
  151. package/runtime/python/okstra_ctl/report_html/router.py +9 -33
  152. package/runtime/python/okstra_ctl/report_projections.py +1 -36
  153. package/runtime/python/okstra_ctl/report_routing.py +23 -0
  154. package/runtime/python/okstra_ctl/report_synthesis_packet.py +4 -73
  155. package/runtime/python/okstra_ctl/report_template_loader.py +35 -0
  156. package/runtime/python/okstra_ctl/report_validation_identity.py +38 -0
  157. package/runtime/python/okstra_ctl/report_views.py +18 -2
  158. package/runtime/python/okstra_ctl/run.py +113 -503
  159. package/runtime/python/okstra_ctl/stage_map.py +13 -0
  160. package/runtime/python/okstra_ctl/stage_targets.py +9 -286
  161. package/runtime/python/okstra_ctl/technical_verification_facts.py +52 -0
  162. package/runtime/python/okstra_ctl/user_response.py +199 -4
  163. package/runtime/python/okstra_ctl/verification_target.py +1 -1
  164. package/runtime/python/okstra_ctl/wizard/__init__.py +31 -31
  165. package/runtime/python/okstra_ctl/wizard/api.py +18 -0
  166. package/runtime/python/okstra_ctl/wizard/outcome.py +3 -12
  167. package/runtime/python/okstra_ctl/wizard/registry.py +20 -12
  168. package/runtime/python/okstra_ctl/wizard/state.py +6 -2
  169. package/runtime/python/okstra_ctl/wizard/steps_analysis.py +0 -97
  170. package/runtime/python/okstra_ctl/wizard/steps_plan.py +27 -278
  171. package/runtime/python/okstra_ctl/wizard/steps_roles.py +2 -1
  172. package/runtime/python/okstra_ctl/work_categories.py +1 -1
  173. package/runtime/python/okstra_ctl/worker_prompt_contract.py +36 -0
  174. package/runtime/python/okstra_ctl/workflow.py +26 -143
  175. package/runtime/skills/okstra-brief-gen/SKILL.md +3 -3
  176. package/runtime/skills/okstra-run/SKILL.md +1 -1
  177. package/runtime/skills/okstra-user-response/SKILL.md +23 -4
  178. package/runtime/templates/reports/quick-input.template.md +1 -1
  179. package/runtime/templates/reports/task-brief.template.md +1 -1
  180. package/runtime/validators/validate-brief.py +2 -2
  181. package/runtime/validators/validate-run.py +287 -4091
  182. package/runtime/validators/validate_analysis_report.py +14 -126
  183. package/docs/task-process/README.md +0 -82
  184. package/docs/task-process/common-flow.md +0 -173
  185. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +0 -147
  186. package/runtime/python/okstra_ctl/technical_verification.py +0 -195
  187. /package/runtime/{prompts/profiles/change-impact-analysis.json → python/okstra_ctl/phases/change_impact_analysis/profile.json} +0 -0
  188. /package/runtime/{prompts/profiles/change-impact-analysis.md → python/okstra_ctl/phases/change_impact_analysis/profile.md} +0 -0
  189. /package/runtime/{templates/reports → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis-input.template.md +0 -0
  190. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis.template.html +0 -0
  191. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis.template.md +0 -0
  192. /package/runtime/{prompts/profiles/error-analysis.json → python/okstra_ctl/phases/error_analysis/profile.json} +0 -0
  193. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis.template.html +0 -0
  194. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis.template.md +0 -0
  195. /package/runtime/{prompts/profiles/feature-analysis.json → python/okstra_ctl/phases/feature_analysis/profile.json} +0 -0
  196. /package/runtime/{prompts/profiles/feature-analysis.md → python/okstra_ctl/phases/feature_analysis/profile.md} +0 -0
  197. /package/runtime/{templates/reports → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis-input.template.md +0 -0
  198. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis.template.html +0 -0
  199. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis.template.md +0 -0
  200. /package/runtime/{prompts/profiles/final-verification.json → python/okstra_ctl/phases/final_verification/profile.json} +0 -0
  201. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/final_verification/report_assets}/final-verification.template.html +0 -0
  202. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/final_verification/report_assets}/final-verification.template.md +0 -0
  203. /package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-diff-review.md +0 -0
  204. /package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-self-check.md +0 -0
  205. /package/runtime/{prompts/profiles/implementation.json → python/okstra_ctl/phases/implementation/profile.json} +0 -0
  206. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation/report_assets}/implementation.template.html +0 -0
  207. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation/report_assets}/implementation.template.md +0 -0
  208. /package/runtime/{prompts/profiles/implementation-option-selection.json → python/okstra_ctl/phases/implementation_option_selection/profile.json} +0 -0
  209. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation_option_selection/report_assets}/implementation-option-selection.template.html +0 -0
  210. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation_option_selection/report_assets}/implementation-option-selection.template.md +0 -0
  211. /package/runtime/{prompts/host-orchestration/implementation-planning.md → python/okstra_ctl/phases/implementation_planning/host-rules.md} +0 -0
  212. /package/runtime/{prompts/profiles/implementation-planning.json → python/okstra_ctl/phases/implementation_planning/profile.json} +0 -0
  213. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning.template.html +0 -0
  214. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning.template.md +0 -0
  215. /package/runtime/{prompts/profiles/improvement-discovery.json → python/okstra_ctl/phases/improvement_discovery/profile.json} +0 -0
  216. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery.template.html +0 -0
  217. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery.template.md +0 -0
  218. /package/runtime/{prompts/profiles/project-analysis.json → python/okstra_ctl/phases/project_analysis/profile.json} +0 -0
  219. /package/runtime/{prompts/profiles/project-analysis.md → python/okstra_ctl/phases/project_analysis/profile.md} +0 -0
  220. /package/runtime/{templates/reports → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis-input.template.md +0 -0
  221. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis.template.html +0 -0
  222. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis.template.md +0 -0
  223. /package/runtime/{prompts/profiles/release-handoff.json → python/okstra_ctl/phases/release_handoff/profile.json} +0 -0
  224. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff.template.html +0 -0
  225. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff.template.md +0 -0
  226. /package/runtime/python/okstra_ctl/{fanout.py → phases/requirements_discovery/fanout.py} +0 -0
  227. /package/runtime/{prompts/profiles/requirements-discovery.json → python/okstra_ctl/phases/requirements_discovery/profile.json} +0 -0
  228. /package/runtime/{templates/reports → python/okstra_ctl/phases/requirements_discovery/report_assets}/fan-out-unit.template.md +0 -0
  229. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/requirements_discovery/report_assets}/requirements-discovery.template.html +0 -0
  230. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/requirements_discovery/report_assets}/requirements-discovery.template.md +0 -0
  231. /package/runtime/{prompts/profiles/technical-verification.json → python/okstra_ctl/phases/technical_verification/profile.json} +0 -0
  232. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/technical_verification/report_assets}/technical-verification.template.html +0 -0
  233. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/technical_verification/report_assets}/technical-verification.template.md +0 -0
@@ -24,7 +24,6 @@
24
24
  - state the reporter's rejection criteria, missing routing inputs, and the evidence boundary behind each recommendation
25
25
  - Primary focus areas:
26
26
  - classify the work as bugfix, feature, improvement, refactor, or ops
27
- - determine whether `error-analysis` or `implementation-option-selection` is the next safe step. Direct `implementation-planning` or `implementation` handoff is never a valid routing target — implementation requires direction selection followed by an approved `implementation-planning` report
28
27
  - capture the reporter's **rejection criteria** — the delivered outcome that would make this work wrong or unacceptable — as a routing input. Consume it from the brief's `Desired Outcome` / `Out of Scope` / `Source Material` when present; when it is absent AND it would change the classification (e.g. bugfix vs feature) or the next-phase choice, raise it as one `decision` clarification row with `Evidence checked: none — reporter intent`. Never infer it — this is a reporter-intent signal, the mirror of improvement-discovery's `Anti-goals`
29
28
  - record the rejection criteria in `requirementsDiscovery.rejectionCriteria` with the
30
29
  `source` that produced it. When it was absent and would not have changed the
@@ -41,11 +40,11 @@
41
40
  - A single-item / single-domain request is not fanned out (preserving the current single-routing behaviour).
42
41
  - When a request spans two or more domains, or has two or more independently-startable work items,
43
42
  publish each item as a `runs/requirements-discovery/fan-out/unit-<NNN>.md` packet.
44
- The packet follows the `templates/reports/fan-out-unit.template.md` format, and its frontmatter
43
+ The packet follows the `scripts/okstra_ctl/phases/requirements_discovery/report_assets/fan-out-unit.template.md` format, and its frontmatter
45
44
  `domain`(work-category 5-enum: bugfix / feature / refactor / ops / improvement),
46
45
  `depends-on`(an inline list of unit-ids within the same fan-out `[unit-001]`, or `[]` if none),
47
46
  `recommended-next-phase`(error-analysis | implementation-option-selection) are filled in.
48
- Each packet MUST carry a non-empty `## Requirement Provenance` section whose every bullet is `brief:EB-001` / `brief:PB-001` / `brief:EO-001` — an end-state id the brief declares — or `contract:<rule>`. When the brief pins ids, citing a heading is rejected: every brief carries the same generic headings, so a heading cannot say WHICH reporter line demanded this unit, and a fan-out unit becomes the brief for a whole downstream task. Only a brief authored before the end-state sections existed still takes the older `brief:<heading>` form, and there the heading must literally exist in it. `derived:` is not admissible in a packet — cross-packet derivation cannot be resolved from a single packet, so each unit anchors directly on the brief. A unit you cannot source that way is not a work item: raise it as a clarification row instead of publishing a packet for it. **Enforced:** `validators/validate_fanout.py` `_check_provenance`.
47
+ Each packet MUST carry a non-empty `## Requirement Provenance` section whose every bullet is `brief:EB-001` / `brief:PB-001` / `brief:EO-001` — an end-state id the brief declares — or `contract:<rule>`. When the brief pins ids, citing a heading is rejected: every brief carries the same generic headings, so a heading cannot say WHICH reporter line demanded this unit, and a fan-out unit becomes the brief for a whole downstream task. Only a brief authored before the end-state sections existed still takes the older `brief:<heading>` form, and there the heading must literally exist in it. `derived:` is not admissible in a packet — cross-packet derivation cannot be resolved from a single packet, so each unit anchors directly on the brief. A unit you cannot source that way is not a work item: raise it as a clarification row instead of publishing a packet for it. **Enforced:** `scripts/okstra_ctl/phases/requirements_discovery/validation.py` `_check_provenance`.
49
48
  - in `runs/requirements-discovery/fan-out/index.md`, list the packets in depends-on topological order
50
49
  as a numbered list (`1. unit-001`) (a generated view; explicitly do not hand-edit). The depends-on graph
51
50
  must be a DAG — `validate_fanout` rejects a cycle as a validation failure, so if a cycle appears,
@@ -1,9 +1,9 @@
1
1
  """Human-first requirements-discovery view model."""
2
2
  from __future__ import annotations
3
3
 
4
- from ..common import evidence_index
5
- from ..models import HumanReportView, VisualEdge, VisualNode
6
- from ..visualizations import decision_flow_figure
4
+ from okstra_ctl.report_html.common import evidence_index
5
+ from okstra_ctl.report_html.models import HumanReportView, VisualEdge, VisualNode
6
+ from okstra_ctl.report_html.visualizations import decision_flow_figure
7
7
 
8
8
  # Record fields this template anchors as `id-<row id>` (see
9
9
  # `HumanReportView.anchored_fields`); ids elsewhere land in the ledger.
@@ -1,5 +1,16 @@
1
1
  # requirements-discovery process
2
2
 
3
+ ## Guarantees
4
+
5
+ | ID | Guarantee | Enforcement |
6
+ |---|---|---|
7
+ | RD-1 | Optional fan-out absence is accepted | `scripts/okstra_ctl/phases/requirements_discovery/validation.py::validate_fanout` |
8
+ | RD-2 | Each packet cites an admissible brief or contract source | `scripts/okstra_ctl/phases/requirements_discovery/validation.py::_check_provenance` |
9
+ | RD-3 | Dependency cycles and unknown units are rejected | `scripts/okstra_ctl/phases/requirements_discovery/fanout.py::topological_order` |
10
+ | RD-4 | The index lists every packet in dependency order | `scripts/okstra_ctl/phases/requirements_discovery/validation.py::validate_fanout` |
11
+ | RD-5 | Packet destinations use the common routing contract | `scripts/okstra_ctl/report_routing.py::fanout_routing_errors` |
12
+ | RD-6 | Human intent and terminology are resolved from inspected evidence | unenforced — worker instructions and lead review; no generic validator proves intent semantics |
13
+
3
14
  ## Index
4
15
 
5
16
  - [1. Purpose](#1-purpose)
@@ -105,9 +116,17 @@ Non-goals are source edit, plan authoring, build, and deployment.
105
116
 
106
117
  ## 6. Code reviewed
107
118
 
108
- - [`skills/okstra-run/SKILL.md`](../../skills/okstra-run/SKILL.md)
109
- - [`scripts/okstra_ctl/wizard/`](../../scripts/okstra_ctl/wizard/)
110
- - [`scripts/okstra_ctl/run.py`](../../scripts/okstra_ctl/run.py)
111
- - [`scripts/okstra_ctl/workflow.py`](../../scripts/okstra_ctl/workflow.py)
112
- - [`prompts/profiles/requirements-discovery.md`](../../prompts/profiles/requirements-discovery.md)
113
- - [`prompts/lead/okstra-lead-contract.md`](../../prompts/lead/okstra-lead-contract.md)
119
+ - [`skills/okstra-run/SKILL.md`](../../../../skills/okstra-run/SKILL.md)
120
+ - [`scripts/okstra_ctl/wizard/`](../../wizard)
121
+ - [`scripts/okstra_ctl/run.py`](../../run.py)
122
+ - [`scripts/okstra_ctl/workflow.py`](../../workflow.py)
123
+ - [`scripts/okstra_ctl/phases/requirements_discovery/profile.md`](profile.md)
124
+ - [`prompts/lead/okstra-lead-contract.md`](../../../../prompts/lead/okstra-lead-contract.md)
125
+
126
+ ## Phase ownership and validation
127
+
128
+ `profile.md` owns request discovery and decomposition instructions; `profile.json` owns roles. `validation.py` owns fan-out packet identity, work category, requirement provenance, dependency and index checks. `fanout.py` orders dependencies and rejects cycles and unknown units. `report.py` and `report_assets/` own the report body and fan-out packet template.
129
+
130
+ Common scope-provenance helpers continue to define brief end-state identifiers and citation grammar. Shared schemas remain in `schemas/final-report-v2.0.schema.json` and `schemas/final-report-v3.0.schema.json` under `requirementsDiscovery`. The common lead's `prompts/lead/phase-routing.md` owns destination selection; `report_routing.py` checks packet destinations.
131
+
132
+ `tests/test_fanout.py` checks real packet files, missing and invalid provenance, dependency cycles, missing dependencies and index order. The common validator calls `validate_requirements_discovery_fanout` only for this task type, preserving the optional fan-out behavior and the `requirements-discovery:` error prefix. Source inspection and human-intent decisions remain worker instructions rather than machine-enforced semantic claims.
@@ -6,25 +6,19 @@ and a `fan-out/` directory exists under the run dir.
6
6
  from __future__ import annotations
7
7
 
8
8
  import re
9
- import sys
10
9
  from dataclasses import dataclass, field
11
10
  from pathlib import Path
12
11
 
13
- _VALIDATORS_DIR = Path(__file__).resolve().parent
14
- for _ssot_dir in (_VALIDATORS_DIR.parent / "scripts", _VALIDATORS_DIR.parent / "python"):
15
- if _ssot_dir.is_dir() and str(_ssot_dir) not in sys.path:
16
- sys.path.insert(0, str(_ssot_dir))
17
-
18
- from okstra_ctl.work_categories import WORK_CATEGORIES # noqa: E402
19
- from okstra_ctl.fanout import topological_order, CycleError # noqa: E402
20
- from okstra_ctl.scope_provenance import ( # noqa: E402
12
+ from okstra_ctl.report_routing import fanout_routing_errors
13
+ from okstra_ctl.work_categories import WORK_CATEGORIES
14
+ from .fanout import topological_order, CycleError
15
+ from okstra_ctl.scope_provenance import (
21
16
  brief_citation_problem,
22
17
  brief_end_state_ids,
23
18
  brief_headings,
24
19
  parse_source,
25
20
  )
26
21
 
27
- _NEXT_PHASES = ("error-analysis", "implementation-planning")
28
22
  _UNIT_RE = re.compile(r"unit-\d{3}")
29
23
  # index.md 번호목록 항목에서만 unit-NNN 을 추출 — 내러티브 문장 중복 방지
30
24
  _INDEX_ENTRY_RE = re.compile(r"^\s*\d+\.\s+(unit-\d{3})\b", re.MULTILINE)
@@ -138,8 +132,7 @@ def validate_fanout(run_dir: Path, brief_path: Path | None = None) -> Validation
138
132
  errors.append(f"{pkt.name}: unit-id {uid!r} != filename stem {pkt.stem!r}")
139
133
  if fm.get("domain") not in WORK_CATEGORIES:
140
134
  errors.append(f"{pkt.name}: domain {fm.get('domain')!r} not in {WORK_CATEGORIES}")
141
- if fm.get("recommended-next-phase") not in _NEXT_PHASES:
142
- errors.append(f"{pkt.name}: recommended-next-phase not in {_NEXT_PHASES}")
135
+ errors.extend(fanout_routing_errors(pkt.name, fm.get("recommended-next-phase")))
143
136
  units[pkt.stem] = _parse_deps(fm.get("depends-on", ""))
144
137
 
145
138
  if not units:
@@ -167,3 +160,9 @@ def validate_fanout(run_dir: Path, brief_path: Path | None = None) -> Validation
167
160
  errors.append(f"index.md order {listed} is not the topological order {order}")
168
161
 
169
162
  return ValidationResult(ok=not errors, errors=errors)
163
+
164
+
165
+ def validate_requirements_discovery_fanout(run_dir, failures, brief_path=None) -> None:
166
+ """단계 산출물 오류를 공통 실행 검증의 오류 목록에 같은 접두로 전달한다."""
167
+ result = validate_fanout(Path(run_dir), brief_path)
168
+ failures.extend(f"requirements-discovery: {error}" for error in result.errors)
@@ -0,0 +1 @@
1
+ """기술 검증 단계의 입력과 관측 증거 정책."""
@@ -0,0 +1,9 @@
1
+ {
2
+ "allowed": " - falsifiable experiment plans for the frozen unresolved facts\n - dependency installation, source experiments, tests and builds only in this run's experiment copies\n - command logs, observed signals and per-fact supported/refuted/inconclusive/not-run results\n - return to implementation-option-selection with evidence, without adoption or plan approval",
3
+ "forbidden": [
4
+ "source edits or installs in the project checkout, task worktree, or another worker's experiment copy",
5
+ "production credentials, remote writes, deployments, migrations, publishing, commits or merging experiments into product branches",
6
+ "marking candidates feasible, selecting a direction, approving a plan, or declaring task acceptance",
7
+ "starting another lifecycle phase inside this run; return the evidence to implementation-option-selection"
8
+ ]
9
+ }
@@ -0,0 +1,100 @@
1
+ """미확정 사실의 시험 입력과 관측 결과를 연결하며 도입 승인은 만들지 않는다."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import re
7
+ from pathlib import Path
8
+ from typing import TypedDict
9
+
10
+ from okstra_ctl.final_report_schema import load_schema_for_data, validate
11
+ from okstra_ctl.implementation_direction import validate_task_artifact_path
12
+ from okstra_ctl.json_boundary import load_owned_object_snapshot, serialize_owned_object
13
+ from okstra_ctl.technical_verification_facts import (
14
+ TechnicalVerificationError,
15
+ VerificationFact,
16
+ technical_verification_facts,
17
+ )
18
+
19
+
20
+ class VerificationInput(TypedDict):
21
+ sourceReport: str
22
+ sourceDataSha256: str
23
+ scope: str
24
+ facts: list[VerificationFact]
25
+
26
+
27
+ def resolve_technical_verification_input(
28
+ report: Path, project_root: Path, task_root: Path, task_key: str
29
+ ) -> VerificationInput:
30
+ """같은 작업의 후보 비교 기록을 시험 입력으로 고정한다."""
31
+ report = report if report.is_absolute() else project_root / report
32
+ validate_task_artifact_path(report, task_root, "technical verification source")
33
+ expected_parent = task_root / "runs/implementation-option-selection/reports"
34
+ if report.parent != expected_parent or not re.fullmatch(
35
+ r"final-report-implementation-option-selection-\d{3,}\.data\.json", report.name
36
+ ):
37
+ raise TechnicalVerificationError(
38
+ "source must be this task's option-selection record"
39
+ )
40
+ snapshot = load_owned_object_snapshot(
41
+ report, artifact="technical verification source"
42
+ )
43
+ data = snapshot.value
44
+ errors = validate(data, load_schema_for_data(data))
45
+ if errors:
46
+ raise TechnicalVerificationError("invalid source report: " + "; ".join(errors))
47
+ if data["header"]["taskKey"] != task_key:
48
+ raise TechnicalVerificationError(
49
+ "source taskKey does not match verification task"
50
+ )
51
+ return {
52
+ "sourceReport": report.relative_to(project_root).as_posix(),
53
+ "sourceDataSha256": hashlib.sha256(snapshot.raw_bytes).hexdigest(),
54
+ "scope": "technical-evidence-only",
55
+ "facts": technical_verification_facts(data),
56
+ }
57
+
58
+
59
+ def write_technical_verification_input(
60
+ payload: VerificationInput, run_root: Path, seq: str
61
+ ) -> Path:
62
+ """실행 순번별 입력을 저장해 후속 실행이 이전 시험 범위를 덮지 않게 한다."""
63
+ path = run_root / "state" / f"technical-verification-input-{seq}.json"
64
+ serialized = serialize_owned_object(path, payload, artifact="technical verification input")
65
+ path.parent.mkdir(parents=True, exist_ok=True)
66
+ with path.open("x", encoding="utf-8") as output:
67
+ output.write(serialized)
68
+ return path
69
+
70
+
71
+ def prepare_technical_verification_input(
72
+ source_path: str, project_root: Path, task_root: Path, task_key: str
73
+ ) -> VerificationInput:
74
+ """공통 준비가 부수효과를 시작하기 전에 필수 입력과 오류 형식을 고정한다."""
75
+ from okstra_ctl.prepare_error import PrepareError
76
+
77
+ if not source_path:
78
+ raise PrepareError("technical-verification requires --clarification-response pointing to this task's option-selection record")
79
+ try:
80
+ return resolve_technical_verification_input(
81
+ Path(source_path), project_root, task_root, task_key,
82
+ )
83
+ except ValueError as exc:
84
+ raise PrepareError(str(exc)) from exc
85
+
86
+
87
+ def materialize_technical_input_packet(payload_json: str, run_root: Path, seq: str) -> str:
88
+ """입력 저장과 패킷 안내를 함께 구성해 실행 순번별 증거 위치를 보존한다."""
89
+ import json
90
+
91
+ verification_path = write_technical_verification_input(
92
+ json.loads(payload_json), run_root, seq,
93
+ )
94
+ return (
95
+ "\n## Technical Verification Input\n\n"
96
+ f"- Frozen input: `{verification_path}`\n"
97
+ f"- Experiment copies only: `{run_root}/experiments/{seq}/<worker-id>/`\n"
98
+ "- Read the input before planning experiments; copy each fact identity verbatim into results.\n"
99
+ + "\n```json\n" + payload_json + "\n```\n"
100
+ )
@@ -22,7 +22,7 @@ Copy boundaries and pre-execution plans are worker instructions reviewed during
22
22
 
23
23
  Copy `sourceReport`, `sourceDataSha256` and `scope` from the frozen input into `technicalVerification`. Emit exactly one check per fact, preserving its `id`, `candidateId`, `factIndex` and `fact`. Each check records its hypothesis, procedure, signals, command evidence, observation, status and limitations. Use project-relative evidence paths. Mark contested results inconclusive.
24
24
 
25
- Route only to `implementation-option-selection`, with one matching phase-continuation follow-up. The next comparison consumes this report through `--clarification-response` and authors fresh feasibility votes. Do not edit the source comparison, clear unrelated uncertainties, rank candidates, approve implementation, merge code or expand the user's pilot scope.
25
+ Do not edit the source comparison, clear unrelated uncertainties, rank candidates, approve implementation, merge code or expand the user's pilot scope.
26
26
 
27
27
  `validate_technical_verification_report` checks frozen-input identity and evidence before publication and during run validation. The report schema restricts the routing target and requires `frontmatter.approved=false`.
28
28
  {{INCLUDE:_coverage-critic.md}}
@@ -2,8 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from ..common import evidence_index
6
- from ..models import HumanReportView
5
+ from okstra_ctl.report_html.common import evidence_index
6
+ from okstra_ctl.report_html.models import HumanReportView
7
7
 
8
8
 
9
9
  def build_technical_verification_view(data: dict) -> HumanReportView:
@@ -0,0 +1,37 @@
1
+ # Technical verification phase
2
+
3
+ ## Purpose and boundaries
4
+
5
+ This phase collects experimental evidence for unresolved technical facts. It does not rank candidates, select a direction, approve implementation or modify the source comparison. The lead chooses the continuation using `prompts/lead/phase-routing.md`.
6
+
7
+ ## Input and preparation
8
+
9
+ The existing `--clarification-response` argument identifies this task's option-selection data record. `entry.prepare_technical_verification_input` checks the required argument before run creation. `entry.resolve_technical_verification_input` checks ownership, the canonical record name, schema and task key, then freezes the source-byte SHA-256 and eligible fact identities. Shared `technical_verification_facts.py` retains candidate safety and unresolved user-decision rules for both producer and consumer phases.
10
+
11
+ `entry.materialize_technical_input_packet` writes `state/technical-verification-input-<seq>.json` without replacing an existing snapshot and appends the input and experiment directory to the existing analysis packet. Artifact names, CLI inputs and preparation order are unchanged.
12
+
13
+ ## Execution and output
14
+
15
+ `profile.md` owns experiment preparation, isolated source copies, command evidence, convergence and fact classification. `profile.json` owns role requirements. Worker write boundaries remain in the common invocation write contract.
16
+
17
+ The report's `technicalVerification` block preserves `sourceReport`, `sourceDataSha256`, `scope` and each fact's identity. Checks record hypothesis, procedure, confirming and rejecting signals, observations, limitations and commands. Status values are `supported`, `refuted`, `inconclusive` and `not-run`.
18
+
19
+ Report schemas remain common: `schemas/final-report-v2.0.schema.json` and `schemas/final-report-v3.0.schema.json`, including the `technicalVerification` definition and approval/continuation constraints. `report.py` and `report_assets/` own the phase body; the common report engine owns publication and page layout.
20
+
21
+ ## Validation and failure behavior
22
+
23
+ `validation.validate_technical_verification_report` checks frozen-input metadata, exactly one check per input fact, unchanged fact identity and execution evidence. Observed checks need commands; supported checks cannot cite a nonzero exit code. Log and working-directory evidence stays inside the matching run's experiment sequence and rejects symlink escapes. `not-run` may omit commands.
24
+
25
+ Both `report_assembly.py` and `validators/validate-run.py` call this validation and the common `report_routing.technical_verification_routing_errors` check. Publication rejects invalid evidence before replacing the report. These checks do not prove that a declared command produced a log or sandbox arbitrary shell commands. Copy isolation and pre-execution plan quality remain worker instructions reviewed during convergence.
26
+
27
+ ## Tests
28
+
29
+ `tests/test_technical_verification.py` covers input ownership, unsafe/unclassified sources, source preservation, exclusive snapshot creation, fact completeness, failed or missing commands, logs, sequence and symlink boundaries, full preparation and schema-v2/v3 report rendering. `tests/report/test_report_assembly.py::test_technical_assembly_requires_evidence_before_replacing_record` checks publication preserves an existing record when evidence fails.
30
+
31
+ ## Guarantees
32
+
33
+ | ID | Guarantee | Enforcement |
34
+ |---|---|---|
35
+ | TV-1 | Report checks preserve the frozen input metadata and cover every input fact exactly once. | `scripts/okstra_ctl/phases/technical_verification/validation.py::validate_technical_verification_report` |
36
+ | TV-2 | Observed results require execution evidence within the matching experiment sequence. | `scripts/okstra_ctl/phases/technical_verification/validation.py::_check_execution_evidence` |
37
+ | TV-3 | The continuation targets implementation option selection. | `scripts/okstra_ctl/report_routing.py::technical_verification_routing_errors` |
@@ -0,0 +1,90 @@
1
+ """관측 판정과 실행별 시험 증거의 일치 검사."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from collections.abc import Mapping
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from okstra_ctl.implementation_direction import validate_task_artifact_path
10
+ from okstra_ctl.json_boundary import load_owned_object_snapshot
11
+ from okstra_ctl.technical_verification_facts import TechnicalVerificationError
12
+
13
+
14
+ def _check_execution_evidence(
15
+ check: Mapping[str, Any], project_root: Path, run_root: Path, seq: str
16
+ ) -> list[str]:
17
+ """관측 판정은 실행 기록과 실행별 시험 디렉터리를 요구한다."""
18
+ errors = []
19
+ commands = check.get("commands") or []
20
+ if check.get("status") != "not-run" and not commands:
21
+ errors.append(f"{check['id']}: an observed result requires command evidence")
22
+ for command in commands:
23
+ try:
24
+ log = project_root / command["logPath"]
25
+ experiment_root = run_root / "experiments" / seq
26
+ validate_task_artifact_path(log, experiment_root, "verification log")
27
+ if not log.read_text(encoding="utf-8").strip():
28
+ errors.append(f"{check['id']}: verification log is empty")
29
+ cwd = project_root / command["cwd"]
30
+ cwd.resolve(strict=True).relative_to(experiment_root.resolve(strict=True))
31
+ relative = cwd.relative_to(experiment_root)
32
+ current = run_root
33
+ for part in ("experiments", seq, *relative.parts):
34
+ current = current / part
35
+ if current.is_symlink():
36
+ raise TechnicalVerificationError(
37
+ "experiment cwd contains a symlink"
38
+ )
39
+ if not cwd.is_dir() or cwd.is_symlink():
40
+ errors.append(f"{check['id']}: experiment cwd must be a directory")
41
+ except (OSError, ValueError) as exc:
42
+ errors.append(f"{check['id']}: invalid execution evidence: {exc}")
43
+ if check.get("status") == "supported" and command.get("exitCode") != 0:
44
+ errors.append(f"{check['id']}: a supported result has a failed command")
45
+ return errors
46
+
47
+
48
+ def validate_technical_verification_report(
49
+ data: Mapping[str, Any], report_path: Path, project_root: Path
50
+ ) -> list[str]:
51
+ """발행·최종 검증에서 같은 사실 집합과 실제 시험 산출물을 대조한다."""
52
+ if (data.get("header") or {}).get("taskType") != "technical-verification":
53
+ return []
54
+ match = re.fullmatch(
55
+ r"final-report-technical-verification-(\d{3,})\.data\.json", report_path.name
56
+ )
57
+ if match is None:
58
+ return ["technical verification report must use its canonical record path"]
59
+ run_root = report_path.parent.parent
60
+ path = run_root / "state" / f"technical-verification-input-{match[1]}.json"
61
+ try:
62
+ validate_task_artifact_path(path, run_root, "technical verification input")
63
+ source = load_owned_object_snapshot(
64
+ path, artifact="technical verification input"
65
+ ).value
66
+ except (OSError, ValueError) as exc:
67
+ return [str(exc)]
68
+ block = data.get("technicalVerification") or {}
69
+ errors = []
70
+ for key in ("sourceReport", "sourceDataSha256", "scope"):
71
+ if block.get(key) != source.get(key):
72
+ errors.append(f"technicalVerification.{key} must match the run input")
73
+ expected = {fact["id"]: fact for fact in source["facts"]}
74
+ checks = block.get("checks") or []
75
+ ids = [check.get("id") for check in checks]
76
+ if len(ids) != len(set(ids)) or set(ids) != set(expected):
77
+ errors.append(
78
+ "technicalVerification.checks must cover each input fact exactly once"
79
+ )
80
+ for check in checks:
81
+ fact = expected.get(check.get("id"))
82
+ if fact is None:
83
+ continue
84
+ for key in ("candidateId", "factIndex", "fact"):
85
+ if check.get(key) != fact[key]:
86
+ errors.append(f"{check['id']}: {key} must preserve the input fact")
87
+ errors.extend(
88
+ _check_execution_evidence(check, project_root, run_root, match[1])
89
+ )
90
+ return errors
@@ -0,0 +1,70 @@
1
+ """계획 승인 상태를 보고서와 사용자 응답에 동일하게 제공한다."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Mapping, Any
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class PlanApprovalState:
11
+ option_names: tuple[str, ...]
12
+ recommended_option: str
13
+ disabled_reason: str
14
+ blocker_ids: tuple[str, ...]
15
+ show_option_selector: bool = True
16
+ reentry_command: str = ""
17
+
18
+
19
+ def resolve_recommended_option(rec_name: str, names: tuple[str, ...]) -> str:
20
+ if rec_name in names:
21
+ return rec_name
22
+ matches = [
23
+ name
24
+ for name in names
25
+ if rec_name and (name.startswith(rec_name) or rec_name.startswith(name))
26
+ ]
27
+ return matches[0] if len(matches) == 1 else names[0]
28
+
29
+
30
+ def plan_approval_state(data: dict) -> PlanApprovalState | None:
31
+ planning = data.get("implementationPlanning")
32
+ if not isinstance(planning, dict):
33
+ return None
34
+ blockers = tuple(
35
+ row["id"]
36
+ for row in data.get("clarificationItems", [])
37
+ if row.get("blocks") == "approval" and row.get("status") in {"open", "answered"}
38
+ )
39
+ reason = f"{len(blockers)} approval blocker(s) unresolved" if blockers else ""
40
+ if planning.get("planningContract") == "selected-direction":
41
+ if planning.get("outcome") == "direction-invalidated" and not reason:
42
+ reason = "selected direction invalidated"
43
+ task_key = (data.get("header") or {}).get("taskKey", "")
44
+ reentry_command = (
45
+ f"/okstra-run task-key={task_key} task-type=implementation-option-selection"
46
+ if planning.get("outcome") == "direction-invalidated" and task_key
47
+ else ""
48
+ )
49
+ return PlanApprovalState((), "", reason, blockers, False, reentry_command)
50
+ names = tuple(
51
+ row["name"]
52
+ for row in planning.get("optionCandidates", [])
53
+ if isinstance(row, dict) and row.get("name")
54
+ )
55
+ if not names:
56
+ return None
57
+ recommended = planning.get("recommendedOption", {}).get("name", "")
58
+ return PlanApprovalState(
59
+ names,
60
+ resolve_recommended_option(recommended, names),
61
+ reason,
62
+ blockers,
63
+ )
64
+
65
+
66
+ def plan_is_approved(report_data: Mapping[str, Any] | None) -> bool:
67
+ if not isinstance(report_data, Mapping):
68
+ return False
69
+ frontmatter = report_data.get("frontmatter")
70
+ return isinstance(frontmatter, Mapping) and frontmatter.get("approved") is True