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
@@ -19,6 +19,7 @@ import sys
19
19
  from pathlib import Path
20
20
 
21
21
  from .paths import find_asset_root
22
+ from .phases.catalog import PhaseAssetError, UnknownTaskType, profile_markdown, profile_sidecar
22
23
  from .role_requirements import RoleProfileError, load_role_profile
23
24
  from .run import PrepareError, _expand_profile_includes
24
25
 
@@ -28,7 +29,7 @@ _PROFILES_REL = ("prompts", "profiles")
28
29
  # `prompts/profiles/_implementation-executor.md`. Reading the list from the
29
30
  # profile body rather than hard-coding it here is deliberate: a list in code
30
31
  # goes stale the moment a profile adds a sidecar, and does so silently.
31
- _SIDECAR_RE = re.compile(r"`(prompts/profiles/_[\w-]+\.md)`")
32
+ _SIDECAR_RE = re.compile(r"`((?:prompts/profiles|scripts/okstra_ctl/phases/implementation/instructions)/_[\w-]+\.md)`")
32
33
 
33
34
 
34
35
  def workspace_root(start: Path | None = None) -> Path:
@@ -44,7 +45,12 @@ def workspace_root(start: Path | None = None) -> Path:
44
45
 
45
46
 
46
47
  def profile_path(root: Path, task_type: str) -> Path:
47
- path = root.joinpath(*_PROFILES_REL, f"{task_type}.md")
48
+ try:
49
+ path = profile_markdown(root, task_type)
50
+ except UnknownTaskType as exc:
51
+ raise PrepareError(f"unknown task-type: {task_type}") from exc
52
+ except PhaseAssetError as exc:
53
+ raise PrepareError(str(exc)) from exc
48
54
  if not path.is_file():
49
55
  raise PrepareError(f"unknown task-type: {task_type} (no {path})")
50
56
  return path
@@ -71,7 +77,7 @@ def sidecar_bodies(root: Path, profile_text: str) -> list[str]:
71
77
  if relative in seen:
72
78
  continue
73
79
  seen.add(relative)
74
- path = root / relative
80
+ path = profile_sidecar(root, relative)
75
81
  if not path.is_file():
76
82
  continue
77
83
  body = _expand_profile_includes(path)
@@ -17,6 +17,8 @@ session id 등) 를 덧붙여 전달한다.
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
+ from .phases.catalog import profile_sidecar
21
+
20
22
  import hashlib
21
23
  import json
22
24
  import re
@@ -2632,7 +2634,7 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
2632
2634
  home_prompts / "lead" / "convergence.md"
2633
2635
  )
2634
2636
  plan_body_verification_path = ctx.get("OKSTRA_PLAN_BODY_VERIFICATION_PATH") or str(
2635
- home_prompts / "lead" / "plan-body-verification.md"
2637
+ profile_sidecar(home_prompts.parent, "prompts/lead/plan-body-verification.md")
2636
2638
  )
2637
2639
  report_writer_path = ctx.get("OKSTRA_REPORT_WRITER_PATH") or str(
2638
2640
  home_prompts / "lead" / "report-writer.md"
@@ -2971,7 +2973,12 @@ def render_template_with_ctx(template_path: str, output_path: str, ctx: dict) ->
2971
2973
  Optional defaults (VALIDATION_STATUS etc.) should be filled by calling
2972
2974
  `apply_lead_prompt_defaults(ctx)` in the same setup step.
2973
2975
  """
2974
- template = Path(template_path).read_text(encoding="utf-8")
2976
+ from .phases.catalog import migrated_report_template
2977
+
2978
+ source = Path(template_path)
2979
+ if source.parent.name == "reports" and source.parent.parent.name == "templates":
2980
+ source = migrated_report_template(source.parent.parent.parent, source.name) or source
2981
+ template = source.read_text(encoding="utf-8")
2975
2982
 
2976
2983
  fm_ctx = dict(ctx)
2977
2984
  fm_ctx.setdefault("DOC_TYPE", _doc_type_from_template_path(template_path))
@@ -42,9 +42,10 @@ from typing import Any
42
42
  # registers ``markupsafe`` and ``jinja2`` aliases in ``sys.modules`` so
43
43
  # downstream ``from jinja2 import ...`` resolves to the vendored copy.
44
44
  import okstra_vendor # noqa: F401 — side effect: sys.modules aliases
45
- from jinja2 import ChainableUndefined, Environment, FileSystemLoader
45
+ from jinja2 import ChainableUndefined, Environment
46
46
 
47
47
  from okstra_ctl.clarification_items import USER_INPUT_BLOCKS, progress_blocking_ids
48
+ from okstra_ctl.report_template_loader import report_template_loader
48
49
  from okstra_ctl.final_report_schema import (
49
50
  SchemaError,
50
51
  load_schema_for_data,
@@ -523,7 +524,7 @@ def _build_environment(template_dir: Path) -> Environment:
523
524
  # the renderer can't tell "the writer forgot this required field"
524
525
  # from "this field is intentionally absent on this task-type".
525
526
  env = Environment(
526
- loader=FileSystemLoader(str(template_dir)),
527
+ loader=report_template_loader(template_dir),
527
528
  undefined=ChainableUndefined,
528
529
  trim_blocks=True,
529
530
  lstrip_blocks=True,
@@ -2,9 +2,10 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import copy
5
- import json
6
5
  import os
7
6
  import tempfile
7
+ from okstra_ctl.phases.implementation_planning.validation import validate_selected_direction_plan
8
+
8
9
  from dataclasses import dataclass
9
10
  from pathlib import Path
10
11
  from typing import Any, Callable, Mapping, Sequence
@@ -25,22 +26,25 @@ from .report_synthesis_packet import (
25
26
  )
26
27
  from .conformance import strip_stage_declaration_label
27
28
  from .design_prep import DesignPrepError, materialize_design_prep_requests
28
- from .implementation_options import validate_blocked_answer_channel
29
- from .technical_verification import validate_technical_verification_report
29
+ from .phases.implementation_option_selection.validation import validate_blocked_answer_channel
30
+ from .phases.technical_verification.validation import validate_technical_verification_report
31
+ from .report_routing import technical_verification_routing_errors
30
32
  from .implementation_direction import (
31
33
  load_selected_direction_snapshot,
32
- validate_selected_direction_plan,
33
34
  )
34
35
  from .paths import task_dir
36
+ from .phases.final_verification.report import verification_scope
35
37
  from .report_projections import (
36
38
  ReportProjectionError,
37
39
  project_convergence,
38
- project_design,
39
40
  project_execution,
40
41
  project_token_usage,
41
42
  )
42
- from .scope_provenance import parse_source
43
- from .verification_target import read_verification_target
43
+ from .phases.implementation_planning.report import (
44
+ attach_design_snapshot, _sync_human_summary_with_plan_body_gate,
45
+ _fill_end_state_coverage,
46
+ )
47
+
44
48
  from .write_policy import planned_path_declaration_errors
45
49
 
46
50
 
@@ -355,8 +359,7 @@ def _verification_scope(project_root: Path, manifest: Mapping[str, Any]) -> str:
355
359
  relative = str(manifest.get("verificationTargetPath") or "").strip()
356
360
  if not relative:
357
361
  return ""
358
- target = read_verification_target(project_root, relative)
359
- return str((target or {}).get("scope") or "").strip()
362
+ return verification_scope(project_root, relative)
360
363
 
361
364
 
362
365
  def _clarification_carry_in(
@@ -689,15 +692,9 @@ def _attach_machine_planning(
689
692
  if design_row is None:
690
693
  return
691
694
  try:
692
- design = project_design(planning, _read_json(design_row))
695
+ attach_design_snapshot(planning, _read_json(design_row))
693
696
  except ReportProjectionError as exc:
694
697
  _fail(design_row.owner, design_row.path, "$", exc)
695
- planning["designPreparation"] = design["designPreparation"]
696
- stages = {row.get("stage"): row for row in planning.get("stages") or [] if isinstance(row, dict)}
697
- for coverage in design["stageCoverage"]:
698
- stage = stages.get(coverage.get("stage"))
699
- if stage is not None:
700
- stage["designSurfaceCoverage"] = coverage.get("rows") or []
701
698
  verification_row = inputs.get("plan-body-verification")
702
699
  if verification_row is None:
703
700
  return
@@ -717,83 +714,6 @@ def _attach_machine_planning(
717
714
  _sync_human_summary_with_plan_body_gate(data, projection)
718
715
 
719
716
 
720
- def _sync_human_summary_with_plan_body_gate(
721
- data: dict[str, Any], projection: Mapping[str, Any],
722
- ) -> None:
723
- """게이트가 통과했는데 내러티브가 unseeded 로 남으면 사람용 칸을 맞춘다.
724
-
725
- report-writer 는 PBV 전에 쓰고, 조립이 게이트를 덮어씌운다. blockers 와
726
- nextStep 이 그때의 문장을 그대로 두면 통과한 계획이 아직 검증 전으로 보인다.
727
- """
728
- gate = str(projection.get("gateResult") or "")
729
- if gate not in {"passed", "passed-with-dissent"}:
730
- return
731
- summary = data.get("humanSummary")
732
- if isinstance(summary, dict) and isinstance(summary.get("blockers"), list):
733
- summary["blockers"] = [
734
- row for row in summary["blockers"]
735
- if "unseeded" not in str(row).lower()
736
- ]
737
- card = data.get("verdictCard")
738
- if not isinstance(card, dict):
739
- return
740
- next_step = str(card.get("nextStep") or "")
741
- lowered = next_step.lower()
742
- if "unseeded" not in lowered and "seed" not in lowered:
743
- return
744
- next_step = (
745
- "Plan-body verification passed. Do not start implementation "
746
- "until `frontmatter.approved` is true."
747
- )
748
- card["nextStep"] = next_step
749
- final = data.get("finalVerdict")
750
- if isinstance(final, dict):
751
- final["nextStep"] = next_step
752
-
753
-
754
- def _fill_end_state_coverage(data: dict[str, Any]) -> None:
755
- """writer 가 못 쓴 endStateCoverage 를 requirementCoverage brief:EB-001 에서 채운다.
756
-
757
- 내러티브 스키마가 이 칸을 열어 주면 writer 가 쓸 수 있다. 비어 있으면
758
- validate-run 이 brief id 마다 한 행을 요구하므로 조립이 채운다.
759
- """
760
- existing = data.get("endStateCoverage")
761
- if isinstance(existing, list) and existing:
762
- return
763
- planning = data.get("implementationPlanning")
764
- if not isinstance(planning, dict):
765
- return
766
- derived = _end_state_rows_from_requirement_coverage(planning)
767
- if derived:
768
- data["endStateCoverage"] = derived
769
-
770
-
771
- def _end_state_rows_from_requirement_coverage(
772
- planning: Mapping[str, Any],
773
- ) -> list[dict[str, Any]]:
774
- rows: list[dict[str, Any]] = []
775
- seen: set[str] = set()
776
- for item in planning.get("requirementCoverage") or []:
777
- if not isinstance(item, Mapping):
778
- continue
779
- ref = parse_source(str(item.get("source") or ""))
780
- if not ref.is_end_state_id or ref.value in seen:
781
- continue
782
- seen.add(ref.value)
783
- status = str(item.get("status") or "")
784
- row: dict[str, Any] = {
785
- "id": ref.value,
786
- "disposition": "addressed" if status == "covered" else "deferred",
787
- "coveredBy": str(item.get("id") or item.get("coveredBy") or ""),
788
- }
789
- if row["disposition"] != "addressed":
790
- row["rationale"] = (
791
- f"requirementCoverage `{item.get('id')}` status is `{status or 'empty'}`."
792
- )
793
- rows.append(row)
794
- return rows
795
-
796
-
797
717
  def _schema_owner(error: str) -> str:
798
718
  field = error.split(" ", 1)[0]
799
719
  owners = {
@@ -897,6 +817,7 @@ def assemble_report(
897
817
  target = Path(value)
898
818
  target = target if target.is_absolute() else project_root / target
899
819
  verification_errors = validate_technical_verification_report(data, target, project_root)
820
+ verification_errors.extend(technical_verification_routing_errors(data))
900
821
  if verification_errors:
901
822
  raise ReportAssemblyError(tuple(
902
823
  AssemblyIssue("report-writer", str(target), "technicalVerification", error)
@@ -247,7 +247,7 @@ def approved_plan_links(
247
247
  except JsonBoundaryError:
248
248
  return {}
249
249
  from .common import anchor_index
250
- from .view_models.implementation_planning import ANCHORED_FIELDS
250
+ from okstra_ctl.phases.implementation_planning.report import ANCHORED_FIELDS
251
251
 
252
252
  page = html_view_path(record)
253
253
  href = Path(os.path.relpath(page, data_path.resolve().parent)).as_posix()
@@ -7,7 +7,7 @@ import sys
7
7
  from pathlib import Path
8
8
 
9
9
  import okstra_vendor # noqa: F401 # registers vendored dependency aliases
10
- from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape
10
+ from jinja2 import Environment, StrictUndefined, select_autoescape
11
11
 
12
12
  from ..final_report_paths import final_report_data_path, translation_sidecar_path
13
13
  from ..final_report_schema import load_schema_for_data, validate
@@ -16,6 +16,7 @@ from ..report_translation import overlay
16
16
  from ..report_view_artifacts import user_responses_dir_for_report
17
17
  from ..usage_cells import format_duration_ms
18
18
  from ..json_boundary import load_owned_object
19
+ from ..report_template_loader import report_template_loader
19
20
  from .common import anchor_index, worker_finding_links
20
21
  from .context_links import (
21
22
  approved_plan_links,
@@ -145,7 +146,7 @@ def render_v2_html_view(
145
146
  route = resolve_html_route(run_meta.task_type)
146
147
  view = route.view_builder(data)
147
148
  root = _templates_root(templates_root)
148
- env = Environment(loader=FileSystemLoader(str(root)), autoescape=select_autoescape(("html",)), undefined=StrictUndefined)
149
+ env = Environment(loader=report_template_loader(root), autoescape=select_autoescape(("html",)), undefined=StrictUndefined)
149
150
  env.policies["json.dumps_kwargs"] = {"sort_keys": True, "ensure_ascii": False}
150
151
  # Binding the index here is what lets a template cite an id without
151
152
  # threading the index through every macro and call site. The record's
@@ -1,46 +1,22 @@
1
1
  """Fail-closed task-type routing for schema-v2 HTML views."""
2
2
  from __future__ import annotations
3
3
 
4
+ import importlib
5
+
6
+ from ..phases.catalog import UnknownTaskType, view_builder_import
4
7
  from ..report_contract import html_template_for
5
8
  from .models import HtmlRoute
6
- from .view_models.change_impact_analysis import build_change_impact_analysis_view
7
- from .view_models.error_analysis import build_error_analysis_view
8
- from .view_models.technical_verification import build_technical_verification_view
9
- from .view_models.feature_analysis import build_feature_analysis_view
10
- from .view_models.final_verification import build_final_verification_view
11
- from .view_models.improvement_discovery import build_improvement_discovery_view
12
- from .view_models.implementation_option_selection import (
13
- build_implementation_option_selection_view,
14
- )
15
- from .view_models.implementation import build_implementation_view
16
- from .view_models.implementation_planning import build_implementation_planning_view
17
- from .view_models.project_analysis import build_project_analysis_view
18
- from .view_models.release_handoff import build_release_handoff_view
19
- from .view_models.requirements_discovery import build_requirements_discovery_view
20
9
 
21
10
 
22
11
  class HtmlRenderError(ValueError):
23
12
  """Raised when a v2 task has no dedicated human view."""
24
13
 
25
14
 
26
- VIEW_BUILDERS = {
27
- "technical-verification": build_technical_verification_view,
28
- "requirements-discovery": build_requirements_discovery_view,
29
- "improvement-discovery": build_improvement_discovery_view,
30
- "error-analysis": build_error_analysis_view,
31
- "implementation-option-selection": build_implementation_option_selection_view,
32
- "project-analysis": build_project_analysis_view,
33
- "feature-analysis": build_feature_analysis_view,
34
- "change-impact-analysis": build_change_impact_analysis_view,
35
- "implementation-planning": build_implementation_planning_view,
36
- "implementation": build_implementation_view,
37
- "final-verification": build_final_verification_view,
38
- "release-handoff": build_release_handoff_view,
39
- }
40
-
41
-
42
15
  def resolve_html_route(task_type: str) -> HtmlRoute:
43
- builder = VIEW_BUILDERS.get(task_type)
44
- if builder is None:
45
- raise HtmlRenderError(f"no v2 html view builder for {task_type}")
16
+ """선택한 작업 유형의 표시 함수만 적재한다."""
17
+ try:
18
+ module_name, function_name = view_builder_import(task_type)
19
+ except UnknownTaskType as exc:
20
+ raise HtmlRenderError(f"no v2 html view builder for {task_type}") from exc
21
+ builder = getattr(importlib.import_module(module_name), function_name)
46
22
  return HtmlRoute(template_name=html_template_for(task_type), view_builder=builder)
@@ -4,10 +4,8 @@ from __future__ import annotations
4
4
  from okstra_token_usage.blocks import accounting_workers
5
5
 
6
6
  import json
7
- from copy import deepcopy
8
- from typing import Any, Mapping, Sequence
7
+ from typing import Any, Mapping
9
8
 
10
- from .design_surfaces import DesignSurfaceTrigger, detect_design_surfaces
11
9
  from .report_contract import execution_roles_from_manifest
12
10
  from .usage_cells import duration_ms_from_bounds
13
11
 
@@ -220,39 +218,6 @@ def project_token_usage(team_state: Mapping[str, Any]) -> dict[str, Any]:
220
218
  }
221
219
 
222
220
 
223
- def _trigger_rows(trigger: DesignSurfaceTrigger) -> list[dict[str, Any]]:
224
- return [
225
- {"step": item.step, "field": item.field, "match": item.match}
226
- for item in trigger.evidence
227
- ]
228
-
229
-
230
- def project_design(
231
- planning: Mapping[str, Any], detector_snapshot: Mapping[str, Any],
232
- ) -> dict[str, Any]:
233
- """탐지 결과가 실제 계획 표면과 일치할 때만 설계 블록을 반환한다."""
234
- triggers = detect_design_surfaces(planning)
235
- expected = {(row.stage, row.kind): _trigger_rows(row) for row in triggers}
236
- coverage = detector_snapshot.get("stageCoverage")
237
- coverage = coverage if isinstance(coverage, list) else []
238
- actual: dict[tuple[int, str], list[dict[str, Any]]] = {}
239
- for stage in coverage:
240
- if not isinstance(stage, Mapping):
241
- raise ReportProjectionError("owner=design-surface-detector invalid stageCoverage row")
242
- for row in stage.get("rows") or []:
243
- if isinstance(row, Mapping):
244
- actual[(int(stage.get("stage") or 0), str(row.get("kind") or ""))] = list(row.get("triggerEvidence") or [])
245
- if expected != actual:
246
- raise ReportProjectionError("owner=design-surface-detector trigger coverage mismatch")
247
- preparation = detector_snapshot.get("designPreparation")
248
- if not isinstance(preparation, Mapping):
249
- raise ReportProjectionError("owner=design-surface-detector missing designPreparation")
250
- return {
251
- "designPreparation": deepcopy(dict(preparation)),
252
- "stageCoverage": deepcopy(coverage),
253
- }
254
-
255
-
256
221
  def _round_history(state: Mapping[str, Any]) -> dict[str, Any]:
257
222
  if not (state.get("config") or {}).get("enabled", True):
258
223
  return {"disabled": True}
@@ -0,0 +1,23 @@
1
+ """보고서 생산 단계와 분리된 공통 이동 적합성 검사."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Mapping
5
+ from typing import Any
6
+
7
+
8
+ def technical_verification_routing_errors(data: Mapping[str, Any]) -> list[str]:
9
+ """기술 검증의 후속 목적지가 후보 비교인지 검사하며 목적지를 추론하지 않는다."""
10
+ if (data.get("header") or {}).get("taskType") != "technical-verification":
11
+ return []
12
+ block = data.get("technicalVerification") or {}
13
+ if (block.get("routing") or {}).get("nextTaskType") != "implementation-option-selection":
14
+ return ["technical verification returns only to implementation-option-selection"]
15
+ return []
16
+
17
+
18
+ def fanout_routing_errors(packet_name: str, target: str | None) -> list[str]:
19
+ """작업 분해 패킷의 후속 단계가 허용된 값인지 검사한다."""
20
+ allowed = ("error-analysis", "implementation-option-selection")
21
+ if target not in allowed:
22
+ return [f"{packet_name}: recommended-next-phase not in {allowed}"]
23
+ return []
@@ -15,22 +15,14 @@ from okstra_token_usage.blocks import accounting_workers
15
15
 
16
16
  from .analysis_packet import reference_source_extracts
17
17
  from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
18
- from .implementation_options import (
19
- CANDIDATE_COMPARISON_ROUTING,
20
- EVALUATION_CRITERIA,
21
- MAX_CRITERION_VALUE,
22
- MAX_RANKED_OPTIONS,
23
- MAX_RAW_CANDIDATES_PER_ANALYSER,
24
- MIN_CRITERION_VALUE,
25
- MIN_FEASIBLE_VOTES,
26
- NO_VALID_OPTIONS_ROUTING,
18
+ from .phases.implementation_option_selection.authoring import (
19
+ requirement_instructions as selection_requirement_instructions,
27
20
  )
28
21
  from .report_narrative import writer_owned_data, writer_owned_schema
29
22
  from .schema_excerpt import build_schema_excerpt
30
23
  from .stage_ledger import build_stage_ledger
31
24
  from .scope_provenance import brief_end_state_id_sequence
32
25
 
33
- from .exact_coverage import COVERAGE_VERDICT_PRECEDENCE
34
26
  from .final_report_schema import follow_up_task_rules, task_block_rules, verdict_token_rule
35
27
  from .report_contract import TASK_TYPE_DATA_PROPERTY
36
28
  from .report_markdown import humanise
@@ -251,70 +243,9 @@ class ReportSynthesisPacket:
251
243
  return self._planning_requirement_instructions()
252
244
  if self.task_type != "implementation-option-selection":
253
245
  return []
254
- criteria = ", ".join(f"`{item}`" for item in EVALUATION_CRITERIA)
255
- requirement_ids = ", ".join(
256
- f"`{item}`" for item in self.original_requirement_ids
257
- )
258
- analysers = ", ".join(
259
- f"`{item}`" for item in self.participating_analysers
246
+ return selection_requirement_instructions(
247
+ self.original_requirement_ids, self.participating_analysers,
260
248
  )
261
- return [
262
- f"Original requirement ids, in order: {requirement_ids}.",
263
- f"Participating analysers: {analysers}.",
264
- f"Evaluation criteria, in exact order: {criteria}.",
265
- "Every `rankedOptions` and `candidateAudit` row must contain one "
266
- "`requirementCoverage` row per original requirement id and one "
267
- "`criterionScores` row per evaluation criterion. Weights and scores "
268
- f"are integers from {MIN_CRITERION_VALUE} through {MAX_CRITERION_VALUE}; "
269
- "`weightedScore` is the recalculated weighted mean.",
270
- "`proposedBy` names exactly one participating analyser id, never a "
271
- "list. Counting `rankedOptions` and `candidateAudit` together, each "
272
- f"analyser may propose at most {MAX_RAW_CANDIDATES_PER_ANALYSER} raw "
273
- "candidates and the run may hold at most "
274
- f"{MAX_RAW_CANDIDATES_PER_ANALYSER} times the analyser count.",
275
- "`coverageSummary` is recalculated from the row's `requirementCoverage` "
276
- "statuses and `scopeCommitments`, and every field must equal the "
277
- "recalculation: `totalCount` = number of original requirement ids; "
278
- "`coveredCount` = rows with status `covered`; `coveragePercent` = "
279
- "round(coveredCount / totalCount * 100, 2); `unmappedCommitments` = "
280
- "commitment ids whose `requirementIds` is empty or names an id outside "
281
- "the original set; `scopePrecisionPercent` = round((commitments - "
282
- "unmapped) / commitments * 100, 2); `contradictedRequirements` = "
283
- "original ids with status `contradicted`, in original order; "
284
- "`coverageVerdict` = the first that applies of "
285
- + ", ".join(f"`{verdict}`" for verdict in COVERAGE_VERDICT_PRECEDENCE)
286
- + " (any contradicted; coveredCount below totalCount; any unmapped; "
287
- "otherwise).",
288
- "A ranked option is valid only when every participating analyser "
289
- "supplied one feasibility vote, at least "
290
- f"{MIN_FEASIBLE_VOTES} votes are `feasible`, and both `safetyBlockers` "
291
- "and `unresolvedFeasibilityFacts` are empty.",
292
- ("Classify each `unresolvedFeasibilityFacts` entry with `resolutionKind`: "
293
- "`user-decision` requires nonempty `clarificationRefs` linking that fact "
294
- "to actual C-NNN records, including answered records; `technical-verification` "
295
- "requires no user question. Preserve the fact, whyItMatters, and evidence. "
296
- "Historical unclassified facts remain readable but require classification "
297
- "before blocked report reassembly. Saving does not lift blocked routing."),
298
- ("After a linked decision is answered, preserve its disposition and selected "
299
- "value. Update humanSummary.actions, verdictCard.nextStep, and selection "
300
- "guidance to name remaining technical verification separately from unanswered "
301
- "user decisions; never reopen an answered question to permit saving."),
302
- "Each `feasibilityVotes` row states that analyser's own verdict, "
303
- "rationale, and strongest counterevidence as its result gives them. "
304
- "Two non-`uncertain` votes with identical rationale and "
305
- "counterevidence are rejected as one worker's text copied under "
306
- "another's name.",
307
- f"Rank at most {MAX_RANKED_OPTIONS} valid options by descending "
308
- "`requirement-fit`, `correctness-risk`, `architecture-fit`, "
309
- "`weightedScore`, then id. With valid options set routing to "
310
- f"`{CANDIDATE_COMPARISON_ROUTING}`; without a valid option use "
311
- f"`{NO_VALID_OPTIONS_ROUTING}`.",
312
- "In `candidate-comparison`, keep `preselectedDirection` null and do "
313
- "not route directly to `implementation-planning`. In "
314
- "`preselected-validation`, emit exactly one validated option, an empty "
315
- "`candidateAudit`, a cited `preselectedDirection`, and routing "
316
- "`implementation-planning`.",
317
- ]
318
249
 
319
250
  def _planning_requirement_instructions(self) -> list[str]:
320
251
  """계획 저작 계약에 브리프 요구사항 id 순서를 박아 넣는다.
@@ -0,0 +1,35 @@
1
+ """보고서 템플릿 로더.
2
+
3
+ 이전된 단계의 본문은 단계 ``report_assets/`` 에 있고, 공통 레이아웃과
4
+ 매크로는 ``templates/reports`` 에 남는다. 논리 이름은 그대로다.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ import okstra_vendor # noqa: F401 # registers vendored dependency aliases
11
+ from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound
12
+
13
+ from .phases.catalog import migrated_report_template
14
+
15
+
16
+ class ReportTemplateLoader(BaseLoader):
17
+ """논리 템플릿 이름을 단계 정본 또는 공통 템플릿 트리에서 연다."""
18
+
19
+ def __init__(self, templates_root: Path) -> None:
20
+ self._root = templates_root
21
+ self._fallback = FileSystemLoader(str(templates_root))
22
+
23
+ def get_source(self, environment, template):
24
+ physical = migrated_report_template(self._root.parent.parent, template)
25
+ if physical is None:
26
+ return self._fallback.get_source(environment, template)
27
+ if not physical.is_file():
28
+ raise TemplateNotFound(template)
29
+ source = physical.read_text(encoding="utf-8")
30
+ mtime = physical.stat().st_mtime
31
+ return source, str(physical), lambda: physical.stat().st_mtime == mtime
32
+
33
+
34
+ def report_template_loader(templates_root: Path) -> ReportTemplateLoader:
35
+ return ReportTemplateLoader(templates_root)
@@ -0,0 +1,38 @@
1
+ """보고서 검증 시 파일 이름에서 실행 식별자를 읽는다."""
2
+
3
+ from pathlib import Path
4
+ import re
5
+
6
+ _REPORT_BASENAME_SEQ_RE = re.compile(r"-(?P<seq>\d{3})(?:\.data)?\.(?:md|json)$")
7
+
8
+
9
+ _REPORT_BASENAME_TASK_TYPE_RE = re.compile(
10
+ r"^final-report-(?P<task_type>[a-z][a-z-]*?)-\d{3}(?:\.data)?\.(?:md|json)$"
11
+ )
12
+
13
+
14
+ def _report_run_seq(report_path: Path) -> str | None:
15
+ """This run's seq, read off `final-report-<task-type>-<seq>.md`. ``None``
16
+ when the name does not carry one, so callers fall back to not filtering
17
+ rather than silently checking nothing."""
18
+ match = _REPORT_BASENAME_SEQ_RE.search(report_path.name)
19
+ return match.group("seq") if match else None
20
+
21
+
22
+ def _report_task_type(report_path: Path) -> str:
23
+ """This run's task type, read off `final-report-<task-type>-<seq>`.
24
+
25
+ The report's own `header.taskType` is the first source, but it is not
26
+ always reachable. `report_narrative._allowed_top_level()` has no `header` —
27
+ it is not a writer-owned block — so a gate scored from a narrative, which is
28
+ the only input a report-contract-3.0 run has before assembly, carries no
29
+ task type at all. Globbing with an empty one matched nothing and reported
30
+ every verdict as unbacked under a `runs//worker-results/` path.
31
+
32
+ The filename carries it in every caller: the full-run path passes the report
33
+ itself, and `_report_path_for_state` builds the same canonical name from the
34
+ state file. Returns `""` when the name does not carry one, so a caller can
35
+ tell "not resolvable" from a real task type.
36
+ """
37
+ match = _REPORT_BASENAME_TASK_TYPE_RE.match(report_path.name)
38
+ return match.group("task_type") if match else ""
@@ -30,9 +30,10 @@ import hashlib
30
30
  import html
31
31
  import json
32
32
  import re
33
+ from collections.abc import Mapping
33
34
  from dataclasses import dataclass
34
35
  from pathlib import Path
35
- from typing import Iterable, Optional
36
+ from typing import Any, Iterable, Optional
36
37
 
37
38
  from .user_response_values import ( # noqa: F401 — 재노출(값 타입은 leaf 소유)
38
39
  UserPlanDecision,
@@ -40,7 +41,7 @@ from .user_response_values import ( # noqa: F401 — 재노출(값 타입은 le
40
41
  )
41
42
  from .final_report_paths import final_report_data_path
42
43
  from .json_boundary import JsonBoundaryError, load_owned_object
43
- from .report_html.view_models.implementation_planning import (
44
+ from .plan_approval import (
44
45
  PlanApprovalState,
45
46
  plan_approval_state,
46
47
  resolve_recommended_option,
@@ -1164,6 +1165,21 @@ def normalize_direction_selection_identity(
1164
1165
  return normalized_id, normalized_name
1165
1166
 
1166
1167
 
1168
+ def direction_candidate_ineligibility(option: Mapping[str, Any]) -> str:
1169
+ """Why a ranked candidate cannot become the planning direction, or ``""``.
1170
+
1171
+ The planning gate (`implementation_direction._selected_option`) and the
1172
+ in-session picker (`user_response.set_direction_selection`) share this rule,
1173
+ so a candidate the picker accepts is one the gate accepts.
1174
+ """
1175
+ coverage = option.get("coverageSummary")
1176
+ if not isinstance(coverage, Mapping) or coverage.get("coverageVerdict") != "exact":
1177
+ return "selected candidate must have exact coverage"
1178
+ if option.get("safetyBlockers") or option.get("unresolvedFeasibilityFacts"):
1179
+ return "selected candidate has a safety blocker"
1180
+ return ""
1181
+
1182
+
1167
1183
  def _quoted_sidecar_field(label: str, value: str) -> str:
1168
1184
  cleaned = value.strip()
1169
1185
  if not cleaned: