okstra 0.206.0 → 0.207.0

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 (259) hide show
  1. package/README.md +3 -3
  2. package/dist/cli-registry.mjs +7 -1
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/dist/commands/lifecycle/install.mjs +1 -1
  5. package/dist/commands/lifecycle/install.mjs.map +1 -1
  6. package/docs/architecture/storage-model.md +1 -0
  7. package/docs/architecture.md +40 -16
  8. package/docs/cli.md +17 -15
  9. package/docs/contributor-change-matrix.md +3 -2
  10. package/docs/performance-improvement-plan-v2.md +1 -1
  11. package/docs/project-structure-overview.md +43 -20
  12. package/package.json +1 -1
  13. package/runtime/BUILD.json +2 -2
  14. package/runtime/agents/operations/code-review.json +1 -1
  15. package/runtime/bin/lib/okstra/usage.sh +3 -3
  16. package/runtime/bin/okstra-compact-reminder.sh +1 -1
  17. package/runtime/bin/okstra-spawn-followups.py +2 -2
  18. package/runtime/prompts/duties/direction-selection-worker.json +1 -1
  19. package/runtime/prompts/launch.template.md +2 -2
  20. package/runtime/prompts/lead/adapters/cmux.md +4 -3
  21. package/runtime/prompts/lead/context-loader.md +1 -1
  22. package/runtime/prompts/lead/convergence.md +44 -12
  23. package/runtime/prompts/lead/okstra-lead-contract.md +44 -73
  24. package/runtime/prompts/lead/phase-routing.md +64 -0
  25. package/runtime/prompts/lead/report-writer.md +10 -8
  26. package/runtime/prompts/lead/team-contract.md +1 -1
  27. package/runtime/prompts/profiles/_clarification-recommendation.md +4 -4
  28. package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
  29. package/runtime/prompts/profiles/_common-contract.md +2 -2
  30. package/runtime/prompts/profiles/_coverage-critic.md +1 -1
  31. package/runtime/prompts/profiles/forbidden-actions.json +0 -94
  32. package/runtime/prompts/wizard/prompts.ko.json +2 -1
  33. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -1
  34. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +5 -5
  35. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -1
  36. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +3 -2
  37. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +1 -1
  38. package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +1 -1
  39. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +17 -26
  40. package/runtime/python/okstra_ctl/agent/prompt_cli/batch.py +183 -0
  41. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +60 -10
  42. package/runtime/python/okstra_ctl/agent/prompt_cli/corrections.py +1 -1
  43. package/runtime/python/okstra_ctl/agent/prompt_cli/jobs.py +21 -4
  44. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +10 -1
  45. package/runtime/python/okstra_ctl/analysis_inputs.py +0 -39
  46. package/runtime/python/okstra_ctl/analysis_scope.py +31 -0
  47. package/runtime/python/okstra_ctl/approval_decisions.py +32 -2
  48. package/runtime/python/okstra_ctl/asset_roots.py +19 -0
  49. package/runtime/python/okstra_ctl/assignment_resolver.py +8 -0
  50. package/runtime/python/okstra_ctl/blocking_checks.py +7 -0
  51. package/runtime/python/okstra_ctl/code_review_target.py +92 -6
  52. package/runtime/python/okstra_ctl/consumers.py +12 -0
  53. package/runtime/python/okstra_ctl/dispatch_checkpoints.py +121 -0
  54. package/runtime/python/okstra_ctl/dispatch_core.py +54 -32
  55. package/runtime/python/okstra_ctl/dispatch_state.py +34 -5
  56. package/runtime/python/okstra_ctl/doctor.py +2 -1
  57. package/runtime/python/okstra_ctl/domain/provider.py +5 -0
  58. package/runtime/python/okstra_ctl/domain/worker_presentation.py +21 -2
  59. package/runtime/python/okstra_ctl/domain/write_policy.py +2 -1
  60. package/runtime/python/okstra_ctl/execution_mutation_audit.py +46 -9
  61. package/runtime/python/okstra_ctl/handoff.py +11 -466
  62. package/runtime/python/okstra_ctl/handoff_error.py +5 -0
  63. package/runtime/python/okstra_ctl/implementation_direction.py +0 -477
  64. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +13 -1
  65. package/runtime/python/okstra_ctl/lead_progress.py +33 -1
  66. package/runtime/python/okstra_ctl/manager_view.py +26 -19
  67. package/runtime/python/okstra_ctl/model_io/lines.py +21 -4
  68. package/runtime/python/okstra_ctl/models.py +4 -1
  69. package/runtime/python/okstra_ctl/operation_invocation.py +11 -2
  70. package/runtime/python/okstra_ctl/option_comparison.py +3 -165
  71. package/runtime/python/okstra_ctl/option_votes.py +3 -191
  72. package/runtime/python/okstra_ctl/paths.py +8 -6
  73. package/runtime/python/okstra_ctl/phases/catalog.py +56 -12
  74. package/runtime/python/okstra_ctl/phases/change_impact_analysis/boundary.json +11 -0
  75. package/runtime/python/okstra_ctl/phases/change_impact_analysis/entry.py +39 -0
  76. package/runtime/python/okstra_ctl/{report_html/view_models/change_impact_analysis.py → phases/change_impact_analysis/report.py} +3 -3
  77. package/runtime/python/okstra_ctl/phases/change_impact_analysis/spec.md +26 -0
  78. package/runtime/python/okstra_ctl/phases/change_impact_analysis/validation.py +23 -0
  79. package/runtime/python/okstra_ctl/phases/error_analysis/__init__.py +1 -0
  80. package/runtime/python/okstra_ctl/phases/error_analysis/boundary.json +9 -0
  81. package/runtime/{prompts/profiles/error-analysis.md → python/okstra_ctl/phases/error_analysis/profile.md} +2 -2
  82. package/runtime/python/okstra_ctl/{report_html/view_models/error_analysis.py → phases/error_analysis/report.py} +9 -8
  83. package/runtime/{templates/reports → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis-input.template.md +1 -1
  84. package/runtime/python/okstra_ctl/phases/error_analysis/spec.md +118 -0
  85. package/runtime/python/okstra_ctl/phases/error_analysis/validation.py +241 -0
  86. package/runtime/python/okstra_ctl/phases/feature_analysis/__init__.py +1 -0
  87. package/runtime/python/okstra_ctl/phases/feature_analysis/boundary.json +8 -0
  88. package/runtime/python/okstra_ctl/phases/feature_analysis/entry.py +63 -0
  89. package/runtime/python/okstra_ctl/{report_html/view_models/feature_analysis.py → phases/feature_analysis/report.py} +12 -5
  90. package/runtime/python/okstra_ctl/phases/feature_analysis/spec.md +22 -0
  91. package/runtime/python/okstra_ctl/phases/feature_analysis/validation.py +27 -0
  92. package/runtime/python/okstra_ctl/phases/feature_analysis/wizard.py +95 -0
  93. package/runtime/python/okstra_ctl/phases/final_verification/boundary.json +8 -0
  94. package/runtime/python/okstra_ctl/phases/final_verification/profile.md +2 -2
  95. package/runtime/{templates/reports → python/okstra_ctl/phases/final_verification/report_assets}/final-verification-input.template.md +1 -1
  96. package/runtime/python/okstra_ctl/phases/final_verification/spec.md +1 -1
  97. package/runtime/python/okstra_ctl/phases/implementation/__init__.py +1 -0
  98. package/runtime/python/okstra_ctl/phases/implementation/boundary.json +17 -0
  99. package/runtime/python/okstra_ctl/{implementation_stage.py → phases/implementation/entry.py} +22 -10
  100. package/runtime/{prompts/host-orchestration/implementation.md → python/okstra_ctl/phases/implementation/host-rules.md} +1 -1
  101. package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-deliverable.md +1 -1
  102. package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-executor.md +4 -3
  103. package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-verifier.md +18 -7
  104. package/runtime/{prompts/profiles/implementation.md → python/okstra_ctl/phases/implementation/profile.md} +5 -5
  105. package/runtime/python/okstra_ctl/{report_html/view_models/implementation.py → phases/implementation/report.py} +3 -3
  106. package/runtime/{templates/reports → python/okstra_ctl/phases/implementation/report_assets}/implementation-input.template.md +1 -1
  107. package/runtime/python/okstra_ctl/phases/implementation/spec.md +238 -0
  108. package/runtime/python/okstra_ctl/phases/implementation/validation.py +205 -0
  109. package/runtime/python/okstra_ctl/phases/implementation/wizard.py +39 -0
  110. package/runtime/python/okstra_ctl/phases/implementation_option_selection/__init__.py +1 -0
  111. package/runtime/python/okstra_ctl/phases/implementation_option_selection/authoring.py +80 -0
  112. package/runtime/python/okstra_ctl/phases/implementation_option_selection/boundary.json +10 -0
  113. package/runtime/python/okstra_ctl/phases/implementation_option_selection/comparison.py +168 -0
  114. package/runtime/python/okstra_ctl/phases/implementation_option_selection/entry.py +27 -0
  115. package/runtime/{prompts/profiles/implementation-option-selection.md → python/okstra_ctl/phases/implementation_option_selection/profile.md} +3 -3
  116. package/runtime/python/okstra_ctl/{report_html/view_models/implementation_option_selection.py → phases/implementation_option_selection/report.py} +2 -2
  117. package/runtime/python/okstra_ctl/phases/implementation_option_selection/spec.md +83 -0
  118. package/runtime/python/okstra_ctl/{implementation_options.py → phases/implementation_option_selection/validation.py} +3 -3
  119. package/runtime/python/okstra_ctl/phases/implementation_option_selection/votes.py +194 -0
  120. package/runtime/python/okstra_ctl/phases/implementation_planning/__init__.py +1 -0
  121. package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +2345 -0
  122. package/runtime/python/okstra_ctl/phases/implementation_planning/boundary.json +12 -0
  123. package/runtime/python/okstra_ctl/phases/implementation_planning/entry.py +161 -0
  124. package/runtime/python/okstra_ctl/phases/implementation_planning/guidance.py +178 -0
  125. package/runtime/{prompts/lead → python/okstra_ctl/phases/implementation_planning/instructions}/plan-body-verification.md +61 -51
  126. package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +3295 -0
  127. package/runtime/{prompts/profiles/implementation-planning.md → python/okstra_ctl/phases/implementation_planning/profile.md} +74 -25
  128. package/runtime/python/okstra_ctl/phases/implementation_planning/report.py +237 -0
  129. package/runtime/{templates/reports → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning-input.template.md +2 -2
  130. package/runtime/python/okstra_ctl/phases/implementation_planning/spec.md +204 -0
  131. package/runtime/python/okstra_ctl/phases/implementation_planning/validation.py +597 -0
  132. package/runtime/python/okstra_ctl/phases/implementation_planning/wizard.py +166 -0
  133. package/runtime/python/okstra_ctl/phases/improvement_discovery/boundary.json +12 -0
  134. package/runtime/python/okstra_ctl/{improvement_lenses.py → phases/improvement_discovery/lenses.py} +1 -6
  135. package/runtime/{prompts/profiles/improvement-discovery.md → python/okstra_ctl/phases/improvement_discovery/profile.md} +5 -5
  136. package/runtime/python/okstra_ctl/{report_html/view_models/improvement_discovery.py → phases/improvement_discovery/report.py} +3 -3
  137. package/runtime/{templates/reports → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery-input.template.md +1 -2
  138. package/runtime/python/okstra_ctl/phases/improvement_discovery/spec.md +29 -0
  139. package/runtime/{validators/validate_improvement_report.py → python/okstra_ctl/phases/improvement_discovery/validation.py} +5 -14
  140. package/runtime/python/okstra_ctl/phases/project_analysis/__init__.py +1 -0
  141. package/runtime/python/okstra_ctl/phases/project_analysis/boundary.json +8 -0
  142. package/runtime/python/okstra_ctl/phases/project_analysis/entry.py +11 -0
  143. package/runtime/python/okstra_ctl/{report_html/view_models/project_analysis.py → phases/project_analysis/report.py} +3 -3
  144. package/runtime/python/okstra_ctl/phases/project_analysis/spec.md +33 -0
  145. package/runtime/python/okstra_ctl/phases/project_analysis/validation.py +55 -0
  146. package/runtime/python/okstra_ctl/phases/release_handoff/__init__.py +1 -0
  147. package/runtime/python/okstra_ctl/phases/release_handoff/boundary.json +17 -0
  148. package/runtime/python/okstra_ctl/phases/release_handoff/entry.py +147 -0
  149. package/runtime/python/okstra_ctl/phases/release_handoff/operations.py +446 -0
  150. package/runtime/{prompts/profiles/release-handoff.md → python/okstra_ctl/phases/release_handoff/profile.md} +3 -3
  151. package/runtime/python/okstra_ctl/{report_html/view_models/release_handoff.py → phases/release_handoff/report.py} +3 -3
  152. package/runtime/{templates/reports → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff-input.template.md +1 -1
  153. package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +233 -0
  154. package/runtime/python/okstra_ctl/phases/release_handoff/wizard.py +84 -0
  155. package/runtime/python/okstra_ctl/phases/requirements_discovery/__init__.py +1 -0
  156. package/runtime/python/okstra_ctl/phases/requirements_discovery/boundary.json +9 -0
  157. package/runtime/{prompts/profiles/requirements-discovery.md → python/okstra_ctl/phases/requirements_discovery/profile.md} +2 -3
  158. package/runtime/python/okstra_ctl/{report_html/view_models/requirements_discovery.py → phases/requirements_discovery/report.py} +3 -3
  159. package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +132 -0
  160. package/runtime/{validators/validate_fanout.py → python/okstra_ctl/phases/requirements_discovery/validation.py} +11 -12
  161. package/runtime/python/okstra_ctl/phases/technical_verification/__init__.py +1 -0
  162. package/runtime/python/okstra_ctl/phases/technical_verification/boundary.json +9 -0
  163. package/runtime/python/okstra_ctl/phases/technical_verification/entry.py +100 -0
  164. package/runtime/{prompts/profiles/technical-verification.md → python/okstra_ctl/phases/technical_verification/profile.md} +2 -2
  165. package/runtime/python/okstra_ctl/{report_html/view_models/technical_verification.py → phases/technical_verification/report.py} +2 -2
  166. package/runtime/python/okstra_ctl/phases/technical_verification/spec.md +37 -0
  167. package/runtime/python/okstra_ctl/phases/technical_verification/validation.py +90 -0
  168. package/runtime/python/okstra_ctl/plan_approval.py +70 -0
  169. package/runtime/python/okstra_ctl/plan_items_cli.py +2 -2130
  170. package/runtime/python/okstra_ctl/process_group.py +118 -0
  171. package/runtime/python/okstra_ctl/profile_show.py +3 -3
  172. package/runtime/python/okstra_ctl/render.py +15 -4
  173. package/runtime/python/okstra_ctl/report_assembly.py +28 -92
  174. package/runtime/python/okstra_ctl/report_finalize.py +106 -2
  175. package/runtime/python/okstra_ctl/report_html/context_links.py +1 -1
  176. package/runtime/python/okstra_ctl/report_projections.py +1 -36
  177. package/runtime/python/okstra_ctl/report_routing.py +23 -0
  178. package/runtime/python/okstra_ctl/report_synthesis_packet.py +4 -73
  179. package/runtime/python/okstra_ctl/report_validation_identity.py +38 -0
  180. package/runtime/python/okstra_ctl/report_views.py +1 -1
  181. package/runtime/python/okstra_ctl/run.py +68 -350
  182. package/runtime/python/okstra_ctl/run_artifact_prune.py +200 -0
  183. package/runtime/python/okstra_ctl/stage_map.py +13 -0
  184. package/runtime/python/okstra_ctl/team.py +108 -9
  185. package/runtime/python/okstra_ctl/technical_verification_facts.py +52 -0
  186. package/runtime/python/okstra_ctl/wizard/__init__.py +31 -31
  187. package/runtime/python/okstra_ctl/wizard/api.py +18 -0
  188. package/runtime/python/okstra_ctl/wizard/outcome.py +3 -12
  189. package/runtime/python/okstra_ctl/wizard/registry.py +20 -12
  190. package/runtime/python/okstra_ctl/wizard/steps_analysis.py +0 -97
  191. package/runtime/python/okstra_ctl/wizard/steps_options.py +8 -0
  192. package/runtime/python/okstra_ctl/wizard/steps_plan.py +10 -263
  193. package/runtime/python/okstra_ctl/wizard/steps_roles.py +2 -1
  194. package/runtime/python/okstra_ctl/work_categories.py +1 -1
  195. package/runtime/python/okstra_ctl/worker_dispatch.py +44 -3
  196. package/runtime/python/okstra_ctl/worker_prompt_contract.py +36 -0
  197. package/runtime/python/okstra_ctl/worker_prompt_policy.py +19 -0
  198. package/runtime/python/okstra_ctl/worker_runner.py +21 -3
  199. package/runtime/python/okstra_ctl/workflow.py +26 -143
  200. package/runtime/python/okstra_ctl/write_policy.py +57 -7
  201. package/runtime/python/okstra_project/dirs.py +14 -0
  202. package/runtime/python/okstra_project/resolver.py +2 -1
  203. package/runtime/schemas/execution-manifest-v2.schema.json +2 -1
  204. package/runtime/skills/okstra-brief-gen/SKILL.md +3 -3
  205. package/runtime/skills/okstra-code-review/SKILL.md +70 -32
  206. package/runtime/skills/okstra-code-review/references/review-calibration.md +26 -6
  207. package/runtime/skills/okstra-run/SKILL.md +3 -3
  208. package/runtime/templates/manager/view.template.html +18 -1
  209. package/runtime/templates/reports/quick-input.template.md +1 -1
  210. package/runtime/templates/reports/task-brief.template.md +1 -1
  211. package/runtime/validators/validate-brief.py +2 -2
  212. package/runtime/validators/validate-run.py +299 -3940
  213. package/runtime/validators/validate_analysis_report.py +14 -126
  214. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +0 -147
  215. package/runtime/python/okstra_ctl/technical_verification.py +0 -195
  216. /package/runtime/{prompts/profiles/change-impact-analysis.json → python/okstra_ctl/phases/change_impact_analysis/profile.json} +0 -0
  217. /package/runtime/{prompts/profiles/change-impact-analysis.md → python/okstra_ctl/phases/change_impact_analysis/profile.md} +0 -0
  218. /package/runtime/{templates/reports → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis-input.template.md +0 -0
  219. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis.template.html +0 -0
  220. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis.template.md +0 -0
  221. /package/runtime/{prompts/profiles/error-analysis.json → python/okstra_ctl/phases/error_analysis/profile.json} +0 -0
  222. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis.template.html +0 -0
  223. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis.template.md +0 -0
  224. /package/runtime/{prompts/profiles/feature-analysis.json → python/okstra_ctl/phases/feature_analysis/profile.json} +0 -0
  225. /package/runtime/{prompts/profiles/feature-analysis.md → python/okstra_ctl/phases/feature_analysis/profile.md} +0 -0
  226. /package/runtime/{templates/reports → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis-input.template.md +0 -0
  227. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis.template.html +0 -0
  228. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis.template.md +0 -0
  229. /package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-diff-review.md +0 -0
  230. /package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-self-check.md +0 -0
  231. /package/runtime/{prompts/profiles/implementation.json → python/okstra_ctl/phases/implementation/profile.json} +0 -0
  232. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation/report_assets}/implementation.template.html +0 -0
  233. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation/report_assets}/implementation.template.md +0 -0
  234. /package/runtime/{prompts/profiles/implementation-option-selection.json → python/okstra_ctl/phases/implementation_option_selection/profile.json} +0 -0
  235. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation_option_selection/report_assets}/implementation-option-selection.template.html +0 -0
  236. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation_option_selection/report_assets}/implementation-option-selection.template.md +0 -0
  237. /package/runtime/{prompts/host-orchestration/implementation-planning.md → python/okstra_ctl/phases/implementation_planning/host-rules.md} +0 -0
  238. /package/runtime/{prompts/profiles/implementation-planning.json → python/okstra_ctl/phases/implementation_planning/profile.json} +0 -0
  239. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning.template.html +0 -0
  240. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning.template.md +0 -0
  241. /package/runtime/{prompts/profiles/improvement-discovery.json → python/okstra_ctl/phases/improvement_discovery/profile.json} +0 -0
  242. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery.template.html +0 -0
  243. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery.template.md +0 -0
  244. /package/runtime/{prompts/profiles/project-analysis.json → python/okstra_ctl/phases/project_analysis/profile.json} +0 -0
  245. /package/runtime/{prompts/profiles/project-analysis.md → python/okstra_ctl/phases/project_analysis/profile.md} +0 -0
  246. /package/runtime/{templates/reports → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis-input.template.md +0 -0
  247. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis.template.html +0 -0
  248. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis.template.md +0 -0
  249. /package/runtime/{prompts/profiles/release-handoff.json → python/okstra_ctl/phases/release_handoff/profile.json} +0 -0
  250. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff.template.html +0 -0
  251. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff.template.md +0 -0
  252. /package/runtime/python/okstra_ctl/{fanout.py → phases/requirements_discovery/fanout.py} +0 -0
  253. /package/runtime/{prompts/profiles/requirements-discovery.json → python/okstra_ctl/phases/requirements_discovery/profile.json} +0 -0
  254. /package/runtime/{templates/reports → python/okstra_ctl/phases/requirements_discovery/report_assets}/fan-out-unit.template.md +0 -0
  255. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/requirements_discovery/report_assets}/requirements-discovery.template.html +0 -0
  256. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/requirements_discovery/report_assets}/requirements-discovery.template.md +0 -0
  257. /package/runtime/{prompts/profiles/technical-verification.json → python/okstra_ctl/phases/technical_verification/profile.json} +0 -0
  258. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/technical_verification/report_assets}/technical-verification.template.html +0 -0
  259. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/technical_verification/report_assets}/technical-verification.template.md +0 -0
@@ -0,0 +1,168 @@
1
+ """구현 방향 비교 자료 — 선택 리포트 기록 하나를 채팅용 사실 묶음으로 투영한다.
2
+
3
+ 사용자가 방향을 고르기 전에 비교 자료를 청하면 리드는 점수표·투표표·충족
4
+ 요약을 보여 준다. 그 숫자는 전부 `implementationOptionSelection` 기록에 있고
5
+ `weightedScore` 는 발행 시점에 재계산 값과 대조된다
6
+ (`implementation_options.py` `_validate_option_scores`). 리드가 JSON 을 손으로
7
+ 읽고 가중 점수를 다시 계산하던 자리를 이 명령이 대신한다. 서술(도입 문장,
8
+ 방향별 요지)은 리드가 `narrative` 와 각 방향의 근거 필드로 쓴다.
9
+
10
+ 읽기 전용이다. 어떤 파일도 쓰지 않는다.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import sys
17
+ from collections.abc import Mapping, Sequence
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from okstra_ctl import next_phase
22
+ from okstra_ctl.final_report_paths import final_report_data_path
23
+ from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
24
+
25
+
26
+ class OptionComparisonError(ValueError):
27
+ """비교 자료를 만들 수 없는 기록."""
28
+
29
+
30
+ def _rows(value: object) -> list[Mapping[str, Any]]:
31
+ if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
32
+ return []
33
+ return [row for row in value if isinstance(row, Mapping)]
34
+
35
+
36
+ def _text(value: object) -> str:
37
+ return value.strip() if isinstance(value, str) else ""
38
+
39
+
40
+ def _count(value: object) -> int:
41
+ return len(value) if isinstance(value, Sequence) and not isinstance(value, str) else 0
42
+
43
+
44
+ def _option(
45
+ option: Mapping[str, Any], recommended_id: str
46
+ ) -> dict[str, Any]:
47
+ coverage = option.get("coverageSummary")
48
+ coverage = coverage if isinstance(coverage, Mapping) else {}
49
+ scores = _rows(option.get("criterionScores"))
50
+ return {
51
+ "id": _text(option.get("id")),
52
+ "name": _text(option.get("name")),
53
+ "recommended": _text(option.get("id")) == recommended_id,
54
+ "goal": _text(option.get("goal")),
55
+ "coreMechanism": _text(option.get("coreMechanism")),
56
+ # 발행 검증이 재계산 값과 같음을 보장한 기록값이다. 다시 계산하지 않는다.
57
+ "weightedScore": option.get("weightedScore"),
58
+ "scores": {_text(row.get("criterion")): row.get("score") for row in scores},
59
+ "scoreRationales": {
60
+ _text(row.get("criterion")): _text(row.get("rationale")) for row in scores
61
+ },
62
+ "coverage": {
63
+ "verdict": _text(coverage.get("coverageVerdict")),
64
+ "coveredCount": coverage.get("coveredCount"),
65
+ "totalCount": coverage.get("totalCount"),
66
+ "requirementIds": [
67
+ _text(row.get("requirementId"))
68
+ for row in _rows(option.get("requirementCoverage"))
69
+ if _text(row.get("requirementId"))
70
+ ],
71
+ },
72
+ "safetyBlockers": _count(option.get("safetyBlockers")),
73
+ "unresolvedFeasibilityFacts": _count(option.get("unresolvedFeasibilityFacts")),
74
+ "votes": {
75
+ _text(vote.get("worker")): {
76
+ "verdict": _text(vote.get("verdict")),
77
+ "rationale": _text(vote.get("rationale")),
78
+ "counterevidence": _text(vote.get("counterevidence")),
79
+ }
80
+ for vote in _rows(option.get("feasibilityVotes"))
81
+ if _text(vote.get("worker"))
82
+ },
83
+ }
84
+
85
+
86
+ def build_comparison(report_data: Mapping[str, Any]) -> dict[str, Any]:
87
+ """선택 리포트 기록 → 비교 자료 사실 묶음."""
88
+ selection = report_data.get("implementationOptionSelection")
89
+ if not isinstance(selection, Mapping):
90
+ raise OptionComparisonError(
91
+ "report has no implementationOptionSelection block; "
92
+ "pass an implementation-option-selection report record"
93
+ )
94
+ recommended_id = _text(selection.get("recommendedOptionId"))
95
+ options = [
96
+ _option(row, recommended_id) for row in _rows(selection.get("rankedOptions"))
97
+ ]
98
+ if not options:
99
+ raise OptionComparisonError("report ranks no direction; there is nothing to compare")
100
+ # 투표 열 순서는 처음 등장한 순서다 — 방향마다 같은 분석자 집합이 투표한다.
101
+ workers: list[str] = []
102
+ for option in options:
103
+ for worker in option["votes"]:
104
+ if worker not in workers:
105
+ workers.append(worker)
106
+ narrative = selection.get("userNarrative")
107
+ narrative = narrative if isinstance(narrative, Mapping) else {}
108
+ return {
109
+ "mode": _text(selection.get("mode")),
110
+ "routing": _text(selection.get("routing")),
111
+ "recommendedOptionId": recommended_id,
112
+ "criteria": [
113
+ {"criterion": _text(row.get("criterion")), "weight": row.get("weight")}
114
+ for row in _rows(selection.get("evaluationCriteria"))
115
+ ],
116
+ "workers": workers,
117
+ "options": options,
118
+ "narrative": {
119
+ key: _text(block.get("text"))
120
+ for key, block in narrative.items()
121
+ if isinstance(block, Mapping) and _text(block.get("text"))
122
+ },
123
+ # "남은 절차" 문단의 근거. 다음 phase 포인터와 같은 문장이다.
124
+ "nextStep": next_phase.project(report_data)["rationale"],
125
+ }
126
+
127
+
128
+ def _load(report: Path) -> Mapping[str, Any]:
129
+ data_path = final_report_data_path(report)
130
+ try:
131
+ data = load_owned_object(data_path, artifact="implementation option data")
132
+ except (OSError, JsonBoundaryError) as exc:
133
+ raise OptionComparisonError(f"cannot read report record {data_path}: {exc}") from exc
134
+ return data
135
+
136
+
137
+ def build_parser() -> argparse.ArgumentParser:
138
+ parser = argparse.ArgumentParser(
139
+ prog="okstra option-comparison",
140
+ description=(
141
+ "Print the comparison facts of an implementation-option-selection "
142
+ "report as JSON: criteria and weights, per-direction scores and "
143
+ "recorded weighted score, requirement coverage, blocker counts, "
144
+ "per-worker feasibility votes, the report narrative, and the next step."
145
+ ),
146
+ )
147
+ parser.add_argument(
148
+ "--report",
149
+ required=True,
150
+ help="final-report-implementation-option-selection-<seq>.data.json (a .md sibling is accepted)",
151
+ )
152
+ return parser
153
+
154
+
155
+ def main(argv: list[str] | None = None) -> int:
156
+ args = build_parser().parse_args(argv)
157
+ try:
158
+ payload = build_comparison(_load(Path(args.report)))
159
+ except OptionComparisonError as exc:
160
+ print(f"option-comparison: {exc}", file=sys.stderr)
161
+ return 2
162
+ json.dump(payload, sys.stdout, ensure_ascii=False, indent=2)
163
+ sys.stdout.write("\n")
164
+ return 0
165
+
166
+
167
+ if __name__ == "__main__":
168
+ sys.exit(main())
@@ -0,0 +1,27 @@
1
+ """방향 비교가 필요한 요구사항과 독립 분석자 수를 확인한다."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+ from typing import Sequence
6
+
7
+ from okstra_ctl.prepare_error import PrepareError
8
+ from okstra_ctl.scope_provenance import brief_end_state_id_sequence
9
+
10
+
11
+ def validate_brief_requirements(brief_path: Path) -> None:
12
+ if not brief_end_state_id_sequence(brief_path):
13
+ raise PrepareError(
14
+ "regenerate the brief with stable end-state IDs before starting "
15
+ "implementation-option-selection"
16
+ )
17
+
18
+
19
+ def validate_analyser_roster(
20
+ workers: Sequence[str]
21
+ ) -> None:
22
+ """Require independent analysis from three workers before option selection."""
23
+ analyser_count = sum(worker != "report-writer" for worker in workers)
24
+ if analyser_count < 3:
25
+ raise PrepareError(
26
+ "implementation-option-selection requires at least 3 analyser workers"
27
+ )
@@ -14,7 +14,7 @@
14
14
  - Apply the shared reporter-confirmation precondition exactly as written. Unresolved `intent-check:` and `conversion-block:` rows use `Blocks=next-phase`.
15
15
  - Treat each stable brief end-state ID as a required evaluation target. A missing ID is a preparation failure; do not invent a replacement requirement.
16
16
  - Worker direction-selection procedure:
17
- - In `candidate-comparison` mode, produce candidate, supporting and contradicting evidence, criterion scores, and requirement mappings. For every candidate, state one feasibility verdict — `feasible`, `not-feasible`, or `uncertain` — with a one-sentence rationale and the strongest counterevidence, each citing inspected evidence. The report's `feasibilityVotes` row for this worker is built from that statement, so a candidate without one leaves the writer nothing but another worker's words.
17
+ - In `candidate-comparison` mode, produce candidate, supporting and contradicting evidence, criterion scores, and requirement mappings. For every candidate, state one feasibility verdict — `feasible`, `not-feasible`, or `uncertain` — with a rationale and the strongest counterevidence, each citing inspected evidence. Write the rationale as long as the argument needs: say why each cited finding supports the verdict, not only which findings do. The report's `feasibilityVotes` row for this worker is built from that statement, so a candidate without one leaves the writer nothing but another worker's words.
18
18
  - In `candidate-comparison` mode only, submit at most three candidates. A candidate must be feasible from inspected evidence, not from an assumed future change.
19
19
  - In `preselected-validation` mode, receive one preselected direction from the lead and validate its evidence, counterevidence, criterion scores, and requirement mappings, and state the same feasibility verdict with its rationale and counterevidence. The worker must not generate new candidates.
20
20
  - Do not produce detailed file lists, stage maps, execution commands, or a plan approval request.
@@ -29,7 +29,7 @@
29
29
  - Map every displayed candidate or preselected direction to the stable brief end-state IDs it satisfies, preserves, or leaves unresolved.
30
30
  - **Close the vote gaps before you conclude `blocked`.** Round 1 runs the designers in parallel, so each one votes only on the candidates it proposed and the merged set ends up with a different hole per analyser. A candidate that is otherwise sound then fails the every-analyser clause and drops out of the ranking — three such candidates blocked a run whose comparison had in fact converged (2026-09-10, dev-10629-4: IO-001, IO-002 and IO-003 each held two `feasible` votes and each was missing a different designer). Run `okstra option-votes gaps --task-manifest <taskManifestPath> --narrative <report writer narrative>` before assembly. For each analyser it names, dispatch one vote-completion assignment asking for that analyser's own feasibility verdict, rationale, and counterevidence on the named candidate — no new candidate, so the run stays in `candidate-comparison` mode. The command reports no gap when a vote cannot settle the block (safety blockers, unresolved feasibility facts, too few feasible verdicts); that is the honest `blocked`.
31
31
  - Clarification request policy (phase-specific addenda — shared policy is in `_common-contract.md`):
32
- - Classify every `unresolvedFeasibilityFacts` entry with `resolutionKind`: `user-decision` requires nonempty `clarificationRefs` naming the actual C-NNN records for that fact; `technical-verification` requires source review, design evidence, or experiments and does not require a user question. Preserve `fact`, `whyItMatters`, and `evidence`. Reuse answered questions and their recorded dispositions; never reopen one merely to save the report. An unrelated open question does not satisfy a missing reference. Historical unclassified entries remain readable, but blocked report reassembly requires the writer to classify them explicitly. Enforced by `validate_blocked_answer_channel` in `scripts/okstra_ctl/implementation_options.py`, called by report assembly and `validate-run.py`. Saving preserves `routing: blocked` and does not make a candidate valid. In `humanSummary.actions`, `verdictCard.nextStep`, and selection guidance, distinguish pending user decisions from remaining technical verification; after the pilot route is answered, carry the selected route and remove the old answer-waiting instruction.
32
+ - Classify every `unresolvedFeasibilityFacts` entry with `resolutionKind`: `user-decision` requires nonempty `clarificationRefs` naming the actual C-NNN records for that fact; `technical-verification` requires source review, design evidence, or experiments and does not require a user question. Preserve `fact`, `whyItMatters`, and `evidence`. Reuse answered questions and their recorded dispositions; never reopen one merely to save the report. An unrelated open question does not satisfy a missing reference. Historical unclassified entries remain readable, but blocked report reassembly requires the writer to classify them explicitly. Enforced by `validate_blocked_answer_channel` in `scripts/okstra_ctl/phases/implementation_option_selection/validation.py`, called by report assembly and `validate-run.py`. Saving preserves `routing: blocked` and does not make a candidate valid. In `humanSummary.actions`, `verdictCard.nextStep`, and selection guidance, distinguish pending user decisions from remaining technical verification; after the pilot route is answered, carry the selected route and remove the old answer-waiting instruction.
33
33
  {{INCLUDE:_clarification-recommendation.md}}
34
34
  - Cross-verification mode:
35
35
  - Phase 5.5 convergence runs in adversarial mode (`convergence.adversarial=true`).
@@ -40,6 +40,6 @@
40
40
 
41
41
  ## Technical evidence loop
42
42
 
43
- When no candidate is valid and explicit eligible technical facts remain, route to `technical-verification` to collect experimental evidence. Keep `rankedOptions` empty and `recommendedOptionId` null. `validate_blocked_answer_channel` rejects this route while user decisions remain unresolved or no safe, explicitly classified technical fact is available. Historical blocked reports can be supplied explicitly without rewriting their verdict.
43
+ Record candidate validity, unresolved user decisions, and explicitly classified technical facts. The lead chooses the next phase under `## implementation-option-selection` in `prompts/lead/phase-routing.md`.
44
44
 
45
45
  When `--clarification-response` carries a technical-verification report, read its source comparison, experiment plans, results and logs. Check that the tested environment matches current code, then author fresh feasibility votes. Do not directly convert a supported result into a feasible candidate, remove unrelated uncertainties or treat a failed or unrun probe as compatibility proof. Preserve the user's pilot scope and any deferred production rollout.
@@ -1,8 +1,8 @@
1
1
  """Human-first implementation-direction comparison view."""
2
2
  from __future__ import annotations
3
3
 
4
- from ..common import evidence_index, scoped_anchor_map
5
- from ..models import HumanReportView
4
+ from okstra_ctl.report_html.common import evidence_index, scoped_anchor_map
5
+ from okstra_ctl.report_html.models import HumanReportView
6
6
 
7
7
  # Record fields this template anchors as `id-<row id>` (see
8
8
  # `HumanReportView.anchored_fields`); ids elsewhere land in the ledger.
@@ -0,0 +1,83 @@
1
+ # implementation-option-selection process
2
+
3
+ This directory owns candidate evaluation, vote completion, comparison projection, preparation rules, and report presentation. Selected-direction snapshots, user answers, exact coverage, and technical verification facts remain shared contracts.
4
+
5
+ ## Guarantees
6
+
7
+ | ID | Guarantee | Enforcement |
8
+ |---|---|---|
9
+ | IOS-1 | Candidate scores, coverage, votes and ranking are recomputed | `scripts/okstra_ctl/phases/implementation_option_selection/validation.py::validate_implementation_option_selection` |
10
+ | IOS-2 | Blocked facts retain their user decision or technical evidence channel | `scripts/okstra_ctl/phases/implementation_option_selection/validation.py::validate_blocked_answer_channel` |
11
+ | IOS-3 | Comparison requires at least three independent analysers | `scripts/okstra_ctl/phases/implementation_option_selection/entry.py::validate_analyser_roster` |
12
+ | IOS-4 | Candidate fingerprints deduplicate equivalent mechanisms | `scripts/okstra_ctl/phases/implementation_option_selection/tests/test_implementation_options.py::test_duplicate_candidates_share_a_fingerprint` |
13
+
14
+ Report schemas remain in `schemas/final-report-v2.0.schema.json` and `schemas/final-report-v3.0.schema.json`, under `implementationOptionSelection`. Next-phase decisions belong to `prompts/lead/phase-routing.md`.
15
+
16
+ ## Index
17
+
18
+ - [1. Purpose](#1-purpose)
19
+ - [2. Execution modes](#2-execution-modes)
20
+ - [3. Prepare gates](#3-prepare-gates)
21
+ - [4. Candidate validation and ranking](#4-candidate-validation-and-ranking)
22
+ - [5. Direction confirmation and planning handoff](#5-direction-confirmation-and-planning-handoff)
23
+ - [6. Forbidden actions](#6-forbidden-actions)
24
+ - [7. Verified code](#7-verified-code)
25
+
26
+ ## 1. Purpose
27
+
28
+ `implementation-option-selection` is the read-only lifecycle phase between cause analysis and detailed planning. It decides which implementation mechanism and architecture boundary planning may realize. It does not name the exact file list, split stages, or prescribe test commands.
29
+
30
+ Direction confirmation and detailed plan approval are independent user decisions. Confirming a direction permits planning to begin. It does not approve the plan or permit implementation.
31
+
32
+ ## 2. Execution modes
33
+
34
+ | Mode | Input | Output |
35
+ |---|---|---|
36
+ | `candidate-comparison` | Requirement ledger, cause evidence, code evidence, independently proposed raw candidates | At most three ranked valid directions and a separate user selection |
37
+ | `preselected-validation` | A direction already fixed by upstream evidence or an explicit user instruction | One normalized and validated direction, or `blocked`; no alternative is generated |
38
+
39
+ The normal analyser roster contains at least three analyser workers plus the report writer. Each analyser may propose at most three raw candidates. All analysers reassess the merged candidate set before ranking.
40
+
41
+ ## 3. Prepare gates
42
+
43
+ Prepare rejects the phase when the brief has no stable `EB-NNN`, `PB-NNN`, or `EO-NNN` requirement IDs. External Gates are not part of that denominator. Prepare also rejects a roster with fewer than three analysers.
44
+
45
+ The phase reuses the task-key worktree and may inspect the code and prior task artifacts. It does not obtain a writable implementation-stage worktree.
46
+
47
+ ## 4. Candidate validation and ranking
48
+
49
+ Every displayed candidate has all of the following properties:
50
+
51
+ - `coveragePercent == 100`
52
+ - `scopePrecisionPercent == 100`
53
+ - `coverageVerdict == exact`
54
+ - no `unmappedCommitments`
55
+ - no `contradictedRequirements`
56
+ - supporting code or upstream evidence
57
+ - at least two feasibility votes
58
+ - no safety blocker or unresolved implementation-critical external fact
59
+
60
+ The final report can display one, two, or three valid candidates. Rejected candidates remain in `candidateAudit` with their rejection reasons and cannot be selected. If no candidate is valid, the report uses `blocked` and planning cannot start.
61
+
62
+ Ranking uses eight fixed criteria with per-run weights: requirement fit, architecture fit, change locality, implementation complexity, correctness risk, reversibility, verification cost, and rollout cost. Safety and exact-coverage failures override the weighted score.
63
+
64
+ ## 5. Direction confirmation and planning handoff
65
+
66
+ Comparison mode records a `DIRECTION SELECTION` block in the user-response sidecar, either through the HTML report's Export or in-session through `/okstra-user-response` (`okstra user-response direction`). Prepare validates the selected ID against the displayed candidates and binds the response to the report's sibling data JSON through its SHA-256 digest.
67
+
68
+ A new planning run receives the selection report through `--selected-direction`. Prepare normalizes the validated choice into `instruction-set/selected-direction.json`. Planning cites that snapshot through `selectedDirectionRef` and writes `approved: false` until the user separately approves the detailed plan.
69
+
70
+ If planning proves that the mechanism or architecture boundary cannot satisfy exact coverage, it emits `direction-invalidated` and routes back to `implementation-option-selection`. It never picks the next ranked direction automatically.
71
+
72
+ ## 6. Forbidden actions
73
+
74
+ This phase does not edit source code, run builds or tests, execute migrations, deploy, or call a write API. Candidate details do not contain exact file lists, stage maps, or test commands. Those details belong to `implementation-planning` after direction confirmation.
75
+
76
+ ## 7. Verified code
77
+
78
+ - [`prompts/profiles/implementation-option-selection.md`](profile.md)
79
+ - [`prompts/duties/direction-selection-worker.json`](../../../../prompts/duties/direction-selection-worker.json)
80
+ - [`scripts/okstra_ctl/phases/implementation_option_selection/validation.py`](validation.py)
81
+ - [`scripts/okstra_ctl/implementation_direction.py`](../../../../scripts/okstra_ctl/implementation_direction.py)
82
+ - [`scripts/okstra_ctl/exact_coverage.py`](../../../../scripts/okstra_ctl/exact_coverage.py)
83
+ - [`validators/validate-run.py`](../../../../validators/validate-run.py)
@@ -10,14 +10,14 @@ from dataclasses import dataclass
10
10
  from collections.abc import Mapping, Sequence
11
11
  from typing import Any
12
12
 
13
- from .clarification_items.dispositions import (
13
+ from okstra_ctl.clarification_items.dispositions import (
14
14
  USER_INPUT_BLOCKS,
15
15
  clarification_disposition,
16
16
  incorporated_clarification_ids,
17
17
  row_blocks_progress,
18
18
  )
19
- from .exact_coverage import ExactCoverageError, calculate_exact_coverage
20
- from .technical_verification import TechnicalVerificationError, technical_verification_facts
19
+ from okstra_ctl.exact_coverage import ExactCoverageError, calculate_exact_coverage
20
+ from okstra_ctl.technical_verification_facts import TechnicalVerificationError, technical_verification_facts
21
21
 
22
22
 
23
23
  EVALUATION_CRITERIA = (
@@ -0,0 +1,194 @@
1
+ """전원 투표만 모자란 구현 후보와, 그 표를 받아야 할 분석자를 낸다.
2
+
3
+ `implementation-option-selection` 의 1라운드는 설계자들이 병렬로 돌아 서로의
4
+ 후보를 보지 못한다. 그래서 자기가 낸 후보에만 실현 가능성 표를 남기고, 병합된
5
+ 집합에는 분석자마다 다른 구멍이 생긴다. 유효성 규칙은 전원 투표를 요구하므로
6
+ (`implementation_options._validate_option_feasibility`) 그런 후보는 순위표에
7
+ 오르지 못하고, 남는 후보가 하나도 없으면 run 이 `routing: blocked` 로 끝난다 —
8
+ 실측(2026-09-10, dev-10629-4): 설계자 3명 로스터에서 IO-001·IO-002·IO-003 이
9
+ 각각 `feasible` 2표를 받고도 빠진 분석자가 하나씩 달라 전부 탈락했다.
10
+
11
+ 거절은 그 상태를 알려 줄 뿐 메우지 못한다. 이 명령이 앞으로 가는 길이다:
12
+ 표만 모자란 후보를 세고, 어느 분석자에게 어떤 후보를 부쳐야 하는지 말한다.
13
+ 재검증(reverify) 라운드는 주장을 반박하는 라운드이지 후보에 표를 남기는
14
+ 라운드가 아니므로, 그 구멍을 메우는 디스패치는 리드가 이 목록을 보고 연다.
15
+
16
+ 읽는 자리는 두 가지다. 조립 전이면 작성자 서사(`--narrative`), 이미 발행된
17
+ run 이면 리포트 레코드(`--report`). 로스터는 task-manifest 의
18
+ `recommendedWorkers` 에서 `report-writer` 를 뺀 것이고, 그것이 검증기가
19
+ `participating analysers` 로 쓰는 값과 같은 정의다(`validators/validate-run.py`).
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import json
25
+ import sys
26
+ from collections.abc import Mapping
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ from okstra_ctl.final_report_schema import load_schema_version
31
+ from okstra_ctl.phases.implementation_option_selection.validation import VoteGap, vote_gaps
32
+ from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
33
+ from okstra_ctl.report_contract import CURRENT_REPORT_SCHEMA_VERSION
34
+ from okstra_ctl.report_narrative import parse_narrative_structure
35
+
36
+
37
+ class OptionVotesError(ValueError):
38
+ """투표 구멍을 셀 입력이 없거나 읽히지 않는다."""
39
+
40
+
41
+ def _record_from_report(path: Path) -> dict[str, Any]:
42
+ try:
43
+ return load_owned_object(path, artifact="final-report data.json")
44
+ except (JsonBoundaryError, OSError) as exc:
45
+ raise OptionVotesError(f"report record is unreadable: {exc}") from exc
46
+
47
+
48
+ def _record_from_narrative(path: Path) -> dict[str, Any]:
49
+ """작성자 서사를 레코드 모양으로 읽는다.
50
+
51
+ 값 결함은 무시한다 — 교정 원장이 고칠 자리이고, 표 구멍을 세는 데에는
52
+ 후보 id 와 `feasibilityVotes` 만 있으면 된다. 여기서 서사 전체를 거절하면
53
+ 아직 교정 중인 run 은 이 명령을 쓸 수 없다.
54
+ """
55
+ try:
56
+ markdown = path.read_text(encoding="utf-8")
57
+ except OSError as exc:
58
+ raise OptionVotesError(f"narrative is unreadable: {exc}") from exc
59
+ schema = load_schema_version(CURRENT_REPORT_SCHEMA_VERSION)
60
+ record, _defects = parse_narrative_structure(markdown, schema)
61
+ return record
62
+
63
+
64
+ def participating_analysers(manifest_path: Path) -> tuple[str, ...]:
65
+ try:
66
+ manifest = load_owned_object(manifest_path, artifact="task-manifest")
67
+ except (JsonBoundaryError, OSError) as exc:
68
+ raise OptionVotesError(f"task manifest is unreadable: {exc}") from exc
69
+ roster = manifest.get("recommendedWorkers")
70
+ if not isinstance(roster, list):
71
+ raise OptionVotesError("task manifest has no recommendedWorkers roster")
72
+ return tuple(
73
+ str(worker) for worker in roster if str(worker) != "report-writer"
74
+ )
75
+
76
+
77
+ def _selection(record: Mapping[str, Any]) -> Mapping[str, Any]:
78
+ selection = record.get("implementationOptionSelection")
79
+ if not isinstance(selection, Mapping):
80
+ raise OptionVotesError(
81
+ "the source has no implementationOptionSelection block — "
82
+ "this command reads an implementation-option-selection run"
83
+ )
84
+ return selection
85
+
86
+
87
+ def _dispatch_lines(gaps: list[VoteGap]) -> list[str]:
88
+ """분석자별로 부칠 후보 목록. 디스패치 단위가 분석자이기 때문이다."""
89
+ by_analyser: dict[str, list[str]] = {}
90
+ for gap in gaps:
91
+ for analyser in gap.missing:
92
+ by_analyser.setdefault(analyser, []).append(gap.option_id)
93
+ return [
94
+ f" {analyser}: {', '.join(options)}"
95
+ for analyser, options in sorted(by_analyser.items())
96
+ ]
97
+
98
+
99
+ def _render(gaps: list[VoteGap]) -> str:
100
+ if not gaps:
101
+ return (
102
+ "No candidate is short of votes alone. A blocked run here is "
103
+ "blocked by something a vote cannot settle — safety blockers, "
104
+ "unresolved feasibility facts, or too few feasible verdicts."
105
+ )
106
+ lines = [
107
+ f"{len(gaps)} candidate(s) need only the missing feasibility votes:",
108
+ "",
109
+ ]
110
+ lines += [
111
+ f" {gap.option_id}: {gap.feasible_votes} feasible so far, "
112
+ f"missing {', '.join(gap.missing)}"
113
+ for gap in gaps
114
+ ]
115
+ lines += ["", "Dispatch one vote-completion assignment per analyser:"]
116
+ lines += _dispatch_lines(gaps)
117
+ lines += [
118
+ "",
119
+ "Each assignment asks that analyser for its own feasibility verdict, "
120
+ "rationale, and counterevidence on the named candidate — nothing else. "
121
+ "It generates no candidate, so the run stays in `candidate-comparison` "
122
+ "mode; `preselected-validation` is a whole-run mode that would collapse "
123
+ "the comparison to one direction.",
124
+ ]
125
+ return "\n".join(lines)
126
+
127
+
128
+ def _gaps(args: argparse.Namespace) -> int:
129
+ if bool(args.report) == bool(args.narrative):
130
+ raise OptionVotesError("pass exactly one of --report or --narrative")
131
+ record = (
132
+ _record_from_report(args.report)
133
+ if args.report
134
+ else _record_from_narrative(args.narrative)
135
+ )
136
+ gaps = vote_gaps(
137
+ _selection(record), participating_analysers(args.task_manifest)
138
+ )
139
+ if args.json:
140
+ print(json.dumps(
141
+ {
142
+ "gaps": [
143
+ {
144
+ "optionId": gap.option_id,
145
+ "missing": list(gap.missing),
146
+ "feasibleVotes": gap.feasible_votes,
147
+ }
148
+ for gap in gaps
149
+ ]
150
+ },
151
+ ensure_ascii=False,
152
+ indent=2,
153
+ ))
154
+ else:
155
+ print(_render(gaps))
156
+ return 0
157
+
158
+
159
+ _CLI_DESCRIPTION = (
160
+ "Report the implementation candidates that only lack feasibility votes, "
161
+ "and which analyser owes each one."
162
+ )
163
+
164
+
165
+ def _parser() -> argparse.ArgumentParser:
166
+ parser = argparse.ArgumentParser(
167
+ description=_CLI_DESCRIPTION, prog="okstra option-votes"
168
+ )
169
+ subparsers = parser.add_subparsers(dest="command", required=True)
170
+ gaps_parser = subparsers.add_parser("gaps")
171
+ gaps_parser.add_argument(
172
+ "--task-manifest", type=Path, required=True,
173
+ help="the task's task-manifest.json — its roster names the analysers")
174
+ gaps_parser.add_argument(
175
+ "--report", type=Path,
176
+ help="a published final-report `.data.json`")
177
+ gaps_parser.add_argument(
178
+ "--narrative", type=Path,
179
+ help="the report writer's narrative markdown, before assembly")
180
+ gaps_parser.add_argument("--json", action="store_true")
181
+ return parser
182
+
183
+
184
+ def main(argv: list[str] | None = None) -> int:
185
+ args = _parser().parse_args(argv)
186
+ try:
187
+ return _gaps(args)
188
+ except OptionVotesError as exc:
189
+ print(f"okstra option-votes: {exc}", file=sys.stderr)
190
+ return 1
191
+
192
+
193
+ if __name__ == "__main__":
194
+ raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1 @@
1
+ """구현 계획 단계의 전용 정책과 자원."""