okstra 0.206.0 → 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 (210) hide show
  1. package/README.md +2 -2
  2. package/dist/commands/lifecycle/install.mjs +1 -1
  3. package/dist/commands/lifecycle/install.mjs.map +1 -1
  4. package/docs/architecture.md +12 -12
  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 +39 -18
  9. package/package.json +1 -1
  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 +13 -54
  16. package/runtime/prompts/lead/phase-routing.md +64 -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/dispatch_state.py +22 -0
  30. package/runtime/python/okstra_ctl/doctor.py +2 -1
  31. package/runtime/python/okstra_ctl/execution_mutation_audit.py +27 -1
  32. package/runtime/python/okstra_ctl/handoff.py +11 -466
  33. package/runtime/python/okstra_ctl/handoff_error.py +5 -0
  34. package/runtime/python/okstra_ctl/implementation_direction.py +0 -477
  35. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +8 -1
  36. package/runtime/python/okstra_ctl/option_comparison.py +3 -165
  37. package/runtime/python/okstra_ctl/option_votes.py +3 -191
  38. package/runtime/python/okstra_ctl/paths.py +8 -6
  39. package/runtime/python/okstra_ctl/phases/catalog.py +56 -12
  40. package/runtime/python/okstra_ctl/phases/change_impact_analysis/boundary.json +11 -0
  41. package/runtime/python/okstra_ctl/phases/change_impact_analysis/entry.py +39 -0
  42. package/runtime/python/okstra_ctl/{report_html/view_models/change_impact_analysis.py → phases/change_impact_analysis/report.py} +3 -3
  43. package/runtime/python/okstra_ctl/phases/change_impact_analysis/spec.md +26 -0
  44. package/runtime/python/okstra_ctl/phases/change_impact_analysis/validation.py +23 -0
  45. package/runtime/python/okstra_ctl/phases/error_analysis/__init__.py +1 -0
  46. package/runtime/python/okstra_ctl/phases/error_analysis/boundary.json +9 -0
  47. package/runtime/{prompts/profiles/error-analysis.md → python/okstra_ctl/phases/error_analysis/profile.md} +2 -2
  48. package/runtime/python/okstra_ctl/{report_html/view_models/error_analysis.py → phases/error_analysis/report.py} +9 -8
  49. package/runtime/{templates/reports → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis-input.template.md +1 -1
  50. package/runtime/python/okstra_ctl/phases/error_analysis/spec.md +118 -0
  51. package/runtime/python/okstra_ctl/phases/error_analysis/validation.py +241 -0
  52. package/runtime/python/okstra_ctl/phases/feature_analysis/__init__.py +1 -0
  53. package/runtime/python/okstra_ctl/phases/feature_analysis/boundary.json +8 -0
  54. package/runtime/python/okstra_ctl/phases/feature_analysis/entry.py +63 -0
  55. package/runtime/python/okstra_ctl/{report_html/view_models/feature_analysis.py → phases/feature_analysis/report.py} +12 -5
  56. package/runtime/python/okstra_ctl/phases/feature_analysis/spec.md +22 -0
  57. package/runtime/python/okstra_ctl/phases/feature_analysis/validation.py +27 -0
  58. package/runtime/python/okstra_ctl/phases/feature_analysis/wizard.py +95 -0
  59. package/runtime/python/okstra_ctl/phases/final_verification/boundary.json +8 -0
  60. package/runtime/python/okstra_ctl/phases/final_verification/profile.md +1 -1
  61. package/runtime/{templates/reports → python/okstra_ctl/phases/final_verification/report_assets}/final-verification-input.template.md +1 -1
  62. package/runtime/python/okstra_ctl/phases/final_verification/spec.md +1 -1
  63. package/runtime/python/okstra_ctl/phases/implementation/__init__.py +1 -0
  64. package/runtime/python/okstra_ctl/phases/implementation/boundary.json +17 -0
  65. package/runtime/python/okstra_ctl/{implementation_stage.py → phases/implementation/entry.py} +22 -10
  66. package/runtime/{prompts/host-orchestration/implementation.md → python/okstra_ctl/phases/implementation/host-rules.md} +1 -1
  67. package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-deliverable.md +1 -1
  68. package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-executor.md +4 -3
  69. package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-verifier.md +4 -4
  70. package/runtime/{prompts/profiles/implementation.md → python/okstra_ctl/phases/implementation/profile.md} +5 -5
  71. package/runtime/python/okstra_ctl/{report_html/view_models/implementation.py → phases/implementation/report.py} +3 -3
  72. package/runtime/{templates/reports → python/okstra_ctl/phases/implementation/report_assets}/implementation-input.template.md +1 -1
  73. package/runtime/python/okstra_ctl/phases/implementation/spec.md +238 -0
  74. package/runtime/python/okstra_ctl/phases/implementation/validation.py +205 -0
  75. package/runtime/python/okstra_ctl/phases/implementation/wizard.py +39 -0
  76. package/runtime/python/okstra_ctl/phases/implementation_option_selection/__init__.py +1 -0
  77. package/runtime/python/okstra_ctl/phases/implementation_option_selection/authoring.py +80 -0
  78. package/runtime/python/okstra_ctl/phases/implementation_option_selection/boundary.json +10 -0
  79. package/runtime/python/okstra_ctl/phases/implementation_option_selection/comparison.py +168 -0
  80. package/runtime/python/okstra_ctl/phases/implementation_option_selection/entry.py +27 -0
  81. package/runtime/{prompts/profiles/implementation-option-selection.md → python/okstra_ctl/phases/implementation_option_selection/profile.md} +2 -2
  82. package/runtime/python/okstra_ctl/{report_html/view_models/implementation_option_selection.py → phases/implementation_option_selection/report.py} +2 -2
  83. package/runtime/python/okstra_ctl/phases/implementation_option_selection/spec.md +83 -0
  84. package/runtime/python/okstra_ctl/{implementation_options.py → phases/implementation_option_selection/validation.py} +3 -3
  85. package/runtime/python/okstra_ctl/phases/implementation_option_selection/votes.py +194 -0
  86. package/runtime/python/okstra_ctl/phases/implementation_planning/__init__.py +1 -0
  87. package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +2345 -0
  88. package/runtime/python/okstra_ctl/phases/implementation_planning/boundary.json +12 -0
  89. package/runtime/python/okstra_ctl/phases/implementation_planning/entry.py +161 -0
  90. package/runtime/python/okstra_ctl/phases/implementation_planning/guidance.py +178 -0
  91. package/runtime/{prompts/lead → python/okstra_ctl/phases/implementation_planning/instructions}/plan-body-verification.md +61 -51
  92. package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +3295 -0
  93. package/runtime/{prompts/profiles/implementation-planning.md → python/okstra_ctl/phases/implementation_planning/profile.md} +74 -25
  94. package/runtime/python/okstra_ctl/phases/implementation_planning/report.py +237 -0
  95. package/runtime/{templates/reports → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning-input.template.md +2 -2
  96. package/runtime/python/okstra_ctl/phases/implementation_planning/spec.md +204 -0
  97. package/runtime/python/okstra_ctl/phases/implementation_planning/validation.py +597 -0
  98. package/runtime/python/okstra_ctl/phases/implementation_planning/wizard.py +166 -0
  99. package/runtime/python/okstra_ctl/phases/improvement_discovery/boundary.json +12 -0
  100. package/runtime/python/okstra_ctl/{improvement_lenses.py → phases/improvement_discovery/lenses.py} +1 -6
  101. package/runtime/{prompts/profiles/improvement-discovery.md → python/okstra_ctl/phases/improvement_discovery/profile.md} +5 -5
  102. package/runtime/python/okstra_ctl/{report_html/view_models/improvement_discovery.py → phases/improvement_discovery/report.py} +3 -3
  103. package/runtime/{templates/reports → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery-input.template.md +1 -2
  104. package/runtime/python/okstra_ctl/phases/improvement_discovery/spec.md +29 -0
  105. package/runtime/{validators/validate_improvement_report.py → python/okstra_ctl/phases/improvement_discovery/validation.py} +5 -14
  106. package/runtime/python/okstra_ctl/phases/project_analysis/__init__.py +1 -0
  107. package/runtime/python/okstra_ctl/phases/project_analysis/boundary.json +8 -0
  108. package/runtime/python/okstra_ctl/phases/project_analysis/entry.py +11 -0
  109. package/runtime/python/okstra_ctl/{report_html/view_models/project_analysis.py → phases/project_analysis/report.py} +3 -3
  110. package/runtime/python/okstra_ctl/phases/project_analysis/spec.md +33 -0
  111. package/runtime/python/okstra_ctl/phases/project_analysis/validation.py +55 -0
  112. package/runtime/python/okstra_ctl/phases/release_handoff/__init__.py +1 -0
  113. package/runtime/python/okstra_ctl/phases/release_handoff/boundary.json +17 -0
  114. package/runtime/python/okstra_ctl/phases/release_handoff/entry.py +147 -0
  115. package/runtime/python/okstra_ctl/phases/release_handoff/operations.py +446 -0
  116. package/runtime/{prompts/profiles/release-handoff.md → python/okstra_ctl/phases/release_handoff/profile.md} +3 -3
  117. package/runtime/python/okstra_ctl/{report_html/view_models/release_handoff.py → phases/release_handoff/report.py} +3 -3
  118. package/runtime/{templates/reports → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff-input.template.md +1 -1
  119. package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +233 -0
  120. package/runtime/python/okstra_ctl/phases/release_handoff/wizard.py +84 -0
  121. package/runtime/python/okstra_ctl/phases/requirements_discovery/__init__.py +1 -0
  122. package/runtime/python/okstra_ctl/phases/requirements_discovery/boundary.json +9 -0
  123. package/runtime/{prompts/profiles/requirements-discovery.md → python/okstra_ctl/phases/requirements_discovery/profile.md} +2 -3
  124. package/runtime/python/okstra_ctl/{report_html/view_models/requirements_discovery.py → phases/requirements_discovery/report.py} +3 -3
  125. package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +132 -0
  126. package/runtime/{validators/validate_fanout.py → python/okstra_ctl/phases/requirements_discovery/validation.py} +11 -12
  127. package/runtime/python/okstra_ctl/phases/technical_verification/__init__.py +1 -0
  128. package/runtime/python/okstra_ctl/phases/technical_verification/boundary.json +9 -0
  129. package/runtime/python/okstra_ctl/phases/technical_verification/entry.py +100 -0
  130. package/runtime/{prompts/profiles/technical-verification.md → python/okstra_ctl/phases/technical_verification/profile.md} +1 -1
  131. package/runtime/python/okstra_ctl/{report_html/view_models/technical_verification.py → phases/technical_verification/report.py} +2 -2
  132. package/runtime/python/okstra_ctl/phases/technical_verification/spec.md +37 -0
  133. package/runtime/python/okstra_ctl/phases/technical_verification/validation.py +90 -0
  134. package/runtime/python/okstra_ctl/plan_approval.py +70 -0
  135. package/runtime/python/okstra_ctl/plan_items_cli.py +2 -2130
  136. package/runtime/python/okstra_ctl/profile_show.py +3 -3
  137. package/runtime/python/okstra_ctl/render.py +9 -2
  138. package/runtime/python/okstra_ctl/report_assembly.py +11 -90
  139. package/runtime/python/okstra_ctl/report_html/context_links.py +1 -1
  140. package/runtime/python/okstra_ctl/report_projections.py +1 -36
  141. package/runtime/python/okstra_ctl/report_routing.py +23 -0
  142. package/runtime/python/okstra_ctl/report_synthesis_packet.py +4 -73
  143. package/runtime/python/okstra_ctl/report_validation_identity.py +38 -0
  144. package/runtime/python/okstra_ctl/report_views.py +1 -1
  145. package/runtime/python/okstra_ctl/run.py +67 -349
  146. package/runtime/python/okstra_ctl/stage_map.py +13 -0
  147. package/runtime/python/okstra_ctl/technical_verification_facts.py +52 -0
  148. package/runtime/python/okstra_ctl/wizard/__init__.py +31 -31
  149. package/runtime/python/okstra_ctl/wizard/api.py +18 -0
  150. package/runtime/python/okstra_ctl/wizard/outcome.py +3 -12
  151. package/runtime/python/okstra_ctl/wizard/registry.py +20 -12
  152. package/runtime/python/okstra_ctl/wizard/steps_analysis.py +0 -97
  153. package/runtime/python/okstra_ctl/wizard/steps_plan.py +10 -263
  154. package/runtime/python/okstra_ctl/wizard/steps_roles.py +2 -1
  155. package/runtime/python/okstra_ctl/work_categories.py +1 -1
  156. package/runtime/python/okstra_ctl/worker_prompt_contract.py +36 -0
  157. package/runtime/python/okstra_ctl/workflow.py +26 -143
  158. package/runtime/skills/okstra-brief-gen/SKILL.md +3 -3
  159. package/runtime/skills/okstra-run/SKILL.md +1 -1
  160. package/runtime/templates/reports/quick-input.template.md +1 -1
  161. package/runtime/templates/reports/task-brief.template.md +1 -1
  162. package/runtime/validators/validate-brief.py +2 -2
  163. package/runtime/validators/validate-run.py +299 -3940
  164. package/runtime/validators/validate_analysis_report.py +14 -126
  165. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +0 -147
  166. package/runtime/python/okstra_ctl/technical_verification.py +0 -195
  167. /package/runtime/{prompts/profiles/change-impact-analysis.json → python/okstra_ctl/phases/change_impact_analysis/profile.json} +0 -0
  168. /package/runtime/{prompts/profiles/change-impact-analysis.md → python/okstra_ctl/phases/change_impact_analysis/profile.md} +0 -0
  169. /package/runtime/{templates/reports → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis-input.template.md +0 -0
  170. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis.template.html +0 -0
  171. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/change_impact_analysis/report_assets}/change-impact-analysis.template.md +0 -0
  172. /package/runtime/{prompts/profiles/error-analysis.json → python/okstra_ctl/phases/error_analysis/profile.json} +0 -0
  173. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis.template.html +0 -0
  174. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/error_analysis/report_assets}/error-analysis.template.md +0 -0
  175. /package/runtime/{prompts/profiles/feature-analysis.json → python/okstra_ctl/phases/feature_analysis/profile.json} +0 -0
  176. /package/runtime/{prompts/profiles/feature-analysis.md → python/okstra_ctl/phases/feature_analysis/profile.md} +0 -0
  177. /package/runtime/{templates/reports → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis-input.template.md +0 -0
  178. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis.template.html +0 -0
  179. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/feature_analysis/report_assets}/feature-analysis.template.md +0 -0
  180. /package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-diff-review.md +0 -0
  181. /package/runtime/{prompts/profiles → python/okstra_ctl/phases/implementation/instructions}/_implementation-self-check.md +0 -0
  182. /package/runtime/{prompts/profiles/implementation.json → python/okstra_ctl/phases/implementation/profile.json} +0 -0
  183. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation/report_assets}/implementation.template.html +0 -0
  184. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation/report_assets}/implementation.template.md +0 -0
  185. /package/runtime/{prompts/profiles/implementation-option-selection.json → python/okstra_ctl/phases/implementation_option_selection/profile.json} +0 -0
  186. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation_option_selection/report_assets}/implementation-option-selection.template.html +0 -0
  187. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation_option_selection/report_assets}/implementation-option-selection.template.md +0 -0
  188. /package/runtime/{prompts/host-orchestration/implementation-planning.md → python/okstra_ctl/phases/implementation_planning/host-rules.md} +0 -0
  189. /package/runtime/{prompts/profiles/implementation-planning.json → python/okstra_ctl/phases/implementation_planning/profile.json} +0 -0
  190. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning.template.html +0 -0
  191. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/implementation_planning/report_assets}/implementation-planning.template.md +0 -0
  192. /package/runtime/{prompts/profiles/improvement-discovery.json → python/okstra_ctl/phases/improvement_discovery/profile.json} +0 -0
  193. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery.template.html +0 -0
  194. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/improvement_discovery/report_assets}/improvement-discovery.template.md +0 -0
  195. /package/runtime/{prompts/profiles/project-analysis.json → python/okstra_ctl/phases/project_analysis/profile.json} +0 -0
  196. /package/runtime/{prompts/profiles/project-analysis.md → python/okstra_ctl/phases/project_analysis/profile.md} +0 -0
  197. /package/runtime/{templates/reports → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis-input.template.md +0 -0
  198. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis.template.html +0 -0
  199. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/project_analysis/report_assets}/project-analysis.template.md +0 -0
  200. /package/runtime/{prompts/profiles/release-handoff.json → python/okstra_ctl/phases/release_handoff/profile.json} +0 -0
  201. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff.template.html +0 -0
  202. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/release_handoff/report_assets}/release-handoff.template.md +0 -0
  203. /package/runtime/python/okstra_ctl/{fanout.py → phases/requirements_discovery/fanout.py} +0 -0
  204. /package/runtime/{prompts/profiles/requirements-discovery.json → python/okstra_ctl/phases/requirements_discovery/profile.json} +0 -0
  205. /package/runtime/{templates/reports → python/okstra_ctl/phases/requirements_discovery/report_assets}/fan-out-unit.template.md +0 -0
  206. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/requirements_discovery/report_assets}/requirements-discovery.template.html +0 -0
  207. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/requirements_discovery/report_assets}/requirements-discovery.template.md +0 -0
  208. /package/runtime/{prompts/profiles/technical-verification.json → python/okstra_ctl/phases/technical_verification/profile.json} +0 -0
  209. /package/runtime/{templates/reports/html/tasks → python/okstra_ctl/phases/technical_verification/report_assets}/technical-verification.template.html +0 -0
  210. /package/runtime/{templates/reports/md/tasks → python/okstra_ctl/phases/technical_verification/report_assets}/technical-verification.template.md +0 -0
@@ -1,17 +1,16 @@
1
1
  """Human-first error-analysis view model."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import re
5
6
 
6
- from ..common import evidence_index
7
- from ..models import HumanReportView, VisualEdge, VisualNode
8
- from ..visualizations import cause_graph_figure
7
+ from okstra_ctl.report_html.common import evidence_index
8
+ from okstra_ctl.report_html.models import HumanReportView, VisualEdge, VisualNode
9
+ from okstra_ctl.report_html.visualizations import cause_graph_figure
9
10
 
10
11
  # Record fields this template anchors as `id-<row id>` (see
11
12
  # `HumanReportView.anchored_fields`); ids elsewhere land in the ledger.
12
- ANCHORED_FIELDS = (
13
- "errorAnalysis.causeCandidates",
14
- )
13
+ ANCHORED_FIELDS = ("errorAnalysis.causeCandidates",)
15
14
 
16
15
 
17
16
  _LEAD_CLAUSE = re.compile(r"\s[—–-]\s|(?<=[.。!?])\s")
@@ -43,7 +42,7 @@ def _cause_node(row: dict, leading_cause_id: str) -> VisualNode:
43
42
  "candidate",
44
43
  "leading" if leading else row["confidence"],
45
44
  "",
46
- note="Leading cause" if leading else f'Confidence {row["confidence"]}',
45
+ note="Leading cause" if leading else f"Confidence {row['confidence']}",
47
46
  )
48
47
 
49
48
 
@@ -79,7 +78,9 @@ def _cause_edges(rows: list[dict], symptom_id: str) -> tuple[VisualEdge, ...]:
79
78
 
80
79
 
81
80
  def _cause_figure(error: dict):
82
- symptom = VisualNode("symptom", "Observed failure", "effect", "risk", "", note="Symptom")
81
+ symptom = VisualNode(
82
+ "symptom", "Observed failure", "effect", "risk", "", note="Symptom"
83
+ )
83
84
  rows = error.get("causeCandidates", [])
84
85
  leading_cause_id = (error.get("routing") or {}).get("leadingCauseId") or ""
85
86
  causes = tuple(_cause_node(row, leading_cause_id) for row in rows)
@@ -80,7 +80,7 @@ taskType: "{{FM_TASK_TYPE}}"
80
80
 
81
81
  > Analysers MUST NOT expand the hypothesis space or evidence collection into items listed here. If an analyser believes an excluded item is causally linked to the symptom, it is reported as a recommended follow-up analysis task in the final report — never silently merged into the current root-cause narrative. If this section is left empty, analysers treat any cause beyond what `Symptom Summary` and `Reproduction Context` describe as out of scope by default.
82
82
 
83
- **Enforced (delivery only):** `tests/contract/test_scope_boundary_delivery.py` pins that a filled section reaches every analysis worker through `okstra_ctl.analysis_packet` — drop it from `CANONICAL_BRIEF_SECTIONS` and every run's exclusions vanish silently. The exclusions themselves are prose and are **not** machine-checked: only the codebase-scan `out-of-scope` frontmatter is a path list, and `validators/validate_improvement_report.py` checks that one.
83
+ **Enforced (delivery only):** `tests/contract/test_scope_boundary_delivery.py` pins that a filled section reaches every analysis worker through `okstra_ctl.analysis_packet` — drop it from `CANONICAL_BRIEF_SECTIONS` and every run's exclusions vanish silently. The exclusions themselves are prose and are **not** machine-checked: only the codebase-scan `out-of-scope` frontmatter is a path list, and `scripts/okstra_ctl/phases/improvement_discovery/validation.py` checks that one.
84
84
 
85
85
  ## Questions for Analysers
86
86
 
@@ -0,0 +1,118 @@
1
+ # error-analysis process
2
+
3
+ ## Guarantees
4
+
5
+ | ID | Guarantee | Enforcement |
6
+ |---|---|---|
7
+ | EA-1 | A blocked reproduction names its blocking reason; other statuses carry an empty reason | `scripts/okstra_ctl/phases/error_analysis/validation.py::_validate_reproduction` |
8
+ | EA-2 | Cause-chain links reference sibling candidates and reject self-links and cycles | `scripts/okstra_ctl/phases/error_analysis/tests/test_diagnosis.py::test_cause_chain_rejects_unresolvable_links` |
9
+ | EA-3 | A selected destination agrees with the cause reference, verdict directions, and continuation row | `scripts/okstra_ctl/phases/error_analysis/validation.py::_validate_error_analysis_consistency` |
10
+ | EA-4 | Common routing destinations match both supported report schemas | `scripts/okstra_ctl/phases/error_analysis/tests/test_diagnosis.py::test_error_routing_policy_matches_schema` |
11
+
12
+ ## Index
13
+
14
+ - [1. Purpose](#1-purpose)
15
+ - [2. okstra-run wizard flow](#2-okstra-run-wizard-flow)
16
+ - [3. prepare_task_bundle handling](#3-prepare_task_bundle-handling)
17
+ - [4. lead execution flow](#4-lead-execution-flow)
18
+ - [5. Deliverables and prohibitions](#5-deliverables-and-prohibitions)
19
+ - [6. Code reviewed](#6-code-reviewed)
20
+
21
+ ## 1. Purpose
22
+
23
+ `error-analysis` analyzes a reported error or incident to organize the symptom, trigger, root-cause candidate, reproduction gap, and validation path. It is not the phase that produces the fix itself or an implementation design.
24
+
25
+ ## 2. okstra-run wizard flow
26
+
27
+ ```mermaid
28
+ flowchart TD
29
+ Start[/okstra-run/] --> Common[common task identity flow]
30
+ Common --> Type[task-type = error-analysis]
31
+ Type --> Worktree{active worktree exists?}
32
+ Worktree -->|yes| RoleCount[role-count min..max<br/>omit uses recommended; skip if min==max]
33
+ Worktree -->|no| BaseRef[base-ref pick/text<br/>main recommended]
34
+ BaseRef --> RoleCount
35
+ RoleCount --> RoleModel[role-model provider/model per slot]
36
+ RoleModel --> RoleAdd[min=0 roles via role-add only<br/>default skip]
37
+ RoleAdd --> Extras[directive, related tasks, clarification]
38
+ Extras --> Confirm
39
+ Confirm --> Render[render-bundle --render-only]
40
+ ```
41
+
42
+ Launch selection uses role slots and model refs only: current-session lead is this session (listed on the confirmation summary), then each static role's count in `min..max` (default **recommended**; the count step is skipped when `min == max`), then one `provider/model` per slot. Roles with `min = 0` stay closed unless the user opens them with role-add (default skip). Duplicate model refs in the same role are rejected. There is no provider roster multi-pick and no `Use defaults / Customize` fork. Dynamic verifiers are not chosen at launch. `--workers` is a CLI compatibility input only, not a launch picker.
43
+
44
+ ## 3. prepare_task_bundle handling
45
+
46
+ ```mermaid
47
+ sequenceDiagram
48
+ participant Skill as okstra-run
49
+ participant Wizard as okstra_ctl.wizard
50
+ participant Run as prepare_task_bundle
51
+ participant WT as worktree/provision.py
52
+ participant Art as artifacts
53
+
54
+ Skill->>Wizard: task-type error-analysis selected
55
+ Wizard-->>Skill: workers/base-ref/model args
56
+ Skill->>Run: render-bundle --render-only
57
+ Run->>Run: canonical brief preflight
58
+ Run->>Run: validate brief/profile
59
+ Run->>Run: resolve worker roster
60
+ Run->>WT: provision/reuse worktree
61
+ Run->>Art: analysis-profile.md includes common contract
62
+ Run->>Art: task-manifest workflow next=validated report route
63
+ ```
64
+
65
+ For canonical briefs, preflight runs before worker resolution, worktree provisioning, or report creation. A brief whose `reporter-confirmations` status is `pending` stops at this point; legacy briefs keep the compatibility path.
66
+
67
+ The final report records its next phase in `errorAnalysis.routing.nextTaskType`. The common Phase Routing guide selects the destination from the recorded cause evidence. When report validation passes, Phase 7 projects `workflow.nextRecommendedPhase` from that one field (`scripts/okstra_ctl/next_phase.py::project`) — a `ready` pointer naming it. A report that leaves the field empty leaves the pointer `pending`; there is no static fallback that supplies a phase the report did not author.
68
+
69
+ ## 4. lead execution flow
70
+
71
+ ```mermaid
72
+ flowchart TD
73
+ Intake[Phase 1 intake] --> Prompts[Phase 2 worker prompts]
74
+ Prompts --> Team[Phase 3 TeamCreate]
75
+ Team --> Dispatch[Phase 4/5 dispatch analysers]
76
+ Dispatch --> Evidence[worker outputs<br/>root-cause hypotheses]
77
+ Evidence --> Conv[Phase 5.5 convergence<br/>default maxRounds = 2]
78
+ Conv --> Report[Phase 6 report-writer final report]
79
+ Report --> Persist[Phase 7 persist + validate]
80
+ ```
81
+
82
+ The workers analyze the symptom and evidence independently. A finding two distinct role executions derived on their own is recorded as full consensus at Round 0 in both modes — independent co-derivation is already cross-verification, so the adversarial burden of proof applies to single-source claims. Evidence-backed counter-evidence classifies a finding `contested` in the round it lands and takes it out of the verification queue; later agreement can neither erase it nor cost another round. The report-writer does not analyze during Phase 4/5 but writes the final report in Phase 6.
83
+
84
+ ## 5. Deliverables and prohibitions
85
+
86
+ ```mermaid
87
+ flowchart LR
88
+ Symptom[Symptom] --> Hyp[Root-cause candidates]
89
+ Hyp --> Gap[Reproduction gaps]
90
+ Gap --> Validate[Validation path]
91
+ Validate --> Next[Recommended next diagnostic/planning step]
92
+ Hyp -. forbidden .-> Fix[Code fix in this run]
93
+ ```
94
+
95
+ The expected final-report content is:
96
+
97
+ - evidence-backed cause analysis
98
+ - uncertainty boundary
99
+ - practical next diagnostic steps
100
+ - if there is blocking uncertainty, `## 1. Clarification Items`, usually `Blocks=next-phase`
101
+
102
+ For `error-analysis`, the structured `errorAnalysis` object is the source of truth for the verbatim symptom, reproduction status, `EA-NNN` cause candidates and their counter-evidence, the next diagnostic, and routing. Its shape is enforced by the final-report schema; `validators/validate-run.py::_validate_error_analysis_consistency` enforces the cross-field values — the routing target, a `leadingCauseId` naming a real cause candidate, the two `direction` fields agreeing with that target, and the single matching `phase-continuation` follow-up row. The common Phase Routing guide owns the conditions for selecting the next phase.
103
+
104
+ What is prohibited is source edit, refactor, fix attempt, implementation design artifact, and running build/migration/deploy. Deferring ambiguity that could be answered from code or logs to a user question is also a defect per the profile.
105
+
106
+ ## 6. Code reviewed
107
+
108
+ - [`prompts/profiles/error-analysis.md`](profile.md)
109
+ - [`templates/reports/error-analysis-input.template.md`](report_assets/error-analysis-input.template.md)
110
+ - [`scripts/okstra_ctl/workflow.py`](../../workflow.py)
111
+ - [`scripts/okstra_ctl/wizard/`](../../wizard)
112
+ - [`prompts/lead/okstra-lead-contract.md`](../../../../prompts/lead/okstra-lead-contract.md)
113
+
114
+ ## Phase ownership
115
+
116
+ `profile.md` and `profile.json` own the diagnostic procedure and worker requirements. `validation.py` owns reproduction, cause-chain, and authored routing consistency checks. `report.py` and `report_assets/` own the diagnosis display and input template. The common lead guide `prompts/lead/phase-routing.md` owns destination selection; common evidence reading, clarification contracts, and lifecycle projection remain outside this phase. The shared 2.0 and 3.0 final-report schemas retain `$defs.ErrorAnalysis`.
117
+
118
+ `tests/test_diagnosis.py` exercises diagnosis rules. The common contract suite retains workflow projection and final-report assembly tests. No additional entry or wizard module is needed: this phase uses the common brief preflight and worker-selection flow.
@@ -0,0 +1,241 @@
1
+ """오류 분석의 재현·원인 연결·선택된 이동 결과를 검증한다."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Mapping
6
+ from okstra_ctl.workflow import ERROR_ANALYSIS_ROUTING_DIRECTIONS
7
+
8
+
9
+ def _upstream_by_candidate(candidates: list[Any]) -> dict[str, list[str]]:
10
+ upstream: dict[str, list[str]] = {}
11
+ for candidate in candidates:
12
+ if not isinstance(candidate, Mapping):
13
+ continue
14
+ candidate_id = candidate.get("id")
15
+ declared = candidate.get("downstreamOf")
16
+ if isinstance(candidate_id, str) and isinstance(declared, list):
17
+ upstream[candidate_id] = [row for row in declared if isinstance(row, str)]
18
+ return upstream
19
+
20
+
21
+ def _chain_cycle(upstream: dict[str, list[str]]) -> list[str]:
22
+ """The first cycle reachable through `downstreamOf`, as the ids that form it.
23
+
24
+ A cycle is a diagnosis that says each step is caused by the next, so it
25
+ names no first cause. It also hangs the figure's layering, which relaxes
26
+ until depths settle.
27
+ """
28
+ settled: set[str] = set()
29
+ for start in sorted(upstream):
30
+ stack = [(start, False)]
31
+ on_path: list[str] = []
32
+ active: dict[str, int] = {}
33
+ while stack:
34
+ current, exiting = stack.pop()
35
+ if exiting:
36
+ active.pop(current)
37
+ on_path.pop()
38
+ settled.add(current)
39
+ continue
40
+ if current in active:
41
+ return on_path[active[current] :] + [current]
42
+ if current in settled or current not in upstream:
43
+ continue
44
+ active[current] = len(on_path)
45
+ on_path.append(current)
46
+ stack.append((current, True))
47
+ stack.extend((parent, False) for parent in upstream[current])
48
+ return []
49
+
50
+
51
+ def _validate_cause_chain(
52
+ candidates: list[Any], candidate_ids: set[str], failures: list[str]
53
+ ) -> None:
54
+ """`downstreamOf` must name a sibling candidate, and never itself."""
55
+ upstream = _upstream_by_candidate(candidates)
56
+ for candidate_id in sorted(upstream):
57
+ unknown = sorted(set(upstream[candidate_id]) - candidate_ids)
58
+ if unknown:
59
+ failures.append(
60
+ f"final-report data.json: {candidate_id}.downstreamOf names "
61
+ "unknown cause candidate(s): " + ", ".join(unknown) + "."
62
+ )
63
+ if candidate_id in upstream[candidate_id]:
64
+ failures.append(
65
+ f"final-report data.json: {candidate_id}.downstreamOf names itself."
66
+ )
67
+ cycle = _chain_cycle(
68
+ {
69
+ key: [row for row in value if row in candidate_ids]
70
+ for key, value in upstream.items()
71
+ }
72
+ )
73
+ if cycle:
74
+ failures.append(
75
+ "final-report data.json: cause candidates form a downstreamOf cycle: "
76
+ + " -> ".join(cycle)
77
+ + "."
78
+ )
79
+
80
+
81
+ def _validate_reproduction(
82
+ error_analysis: Mapping[str, Any], failures: list[str]
83
+ ) -> None:
84
+ reproduction_value = error_analysis.get("reproduction")
85
+ reproduction = reproduction_value if isinstance(reproduction_value, Mapping) else {}
86
+ reproduction_status = reproduction.get("status")
87
+ blocked_reason = reproduction.get("blockedReason")
88
+ if reproduction_status == "blocked-before-repro":
89
+ if not isinstance(blocked_reason, str) or not blocked_reason.strip():
90
+ failures.append(
91
+ "final-report data.json: blocked-before-repro requires a non-empty "
92
+ "errorAnalysis.reproduction.blockedReason."
93
+ )
94
+ elif blocked_reason != "":
95
+ failures.append(
96
+ "final-report data.json: errorAnalysis.reproduction.blockedReason "
97
+ "must be exactly empty unless status is blocked-before-repro."
98
+ )
99
+
100
+
101
+ def _validate_cause_candidates(candidates: list[Any], failures: list[str]) -> set[str]:
102
+ candidate_ids: list[str] = []
103
+ for candidate in candidates:
104
+ if not isinstance(candidate, Mapping):
105
+ continue
106
+ candidate_id = candidate.get("id")
107
+ if isinstance(candidate_id, str):
108
+ candidate_ids.append(candidate_id)
109
+ duplicate_ids = sorted(
110
+ candidate_id
111
+ for candidate_id in set(candidate_ids)
112
+ if candidate_ids.count(candidate_id) > 1
113
+ )
114
+ if duplicate_ids:
115
+ failures.append(
116
+ "final-report data.json: duplicate cause candidate id(s): "
117
+ + ", ".join(duplicate_ids)
118
+ + "."
119
+ )
120
+
121
+ _validate_cause_chain(candidates, set(candidate_ids), failures)
122
+
123
+ return set(candidate_ids)
124
+
125
+
126
+ def _validate_routing_cause(
127
+ error_analysis: Mapping[str, Any],
128
+ candidates: list[Any],
129
+ candidate_id_set: set[str],
130
+ failures: list[str],
131
+ ) -> Any:
132
+ routing_value = error_analysis.get("routing")
133
+ routing = routing_value if isinstance(routing_value, Mapping) else {}
134
+ target = routing.get("nextTaskType")
135
+ leading_cause_id = routing.get("leadingCauseId")
136
+ if isinstance(target, str) and target not in ERROR_ANALYSIS_ROUTING_DIRECTIONS:
137
+ failures.append(
138
+ "final-report data.json: errorAnalysis.routing has unsupported "
139
+ f"routing target `{target}`."
140
+ )
141
+ if target == "implementation-option-selection":
142
+ if not candidates:
143
+ failures.append(
144
+ "final-report data.json: implementation-option-selection routing requires "
145
+ "at least one cause candidate."
146
+ )
147
+ if (
148
+ not isinstance(leading_cause_id, str)
149
+ or leading_cause_id not in candidate_id_set
150
+ ):
151
+ failures.append(
152
+ "final-report data.json: implementation-option-selection routing "
153
+ "leadingCauseId must reference a cause candidate."
154
+ )
155
+ elif target == "error-analysis" and (
156
+ not isinstance(leading_cause_id, str)
157
+ or (leading_cause_id != "" and leading_cause_id not in candidate_id_set)
158
+ ):
159
+ failures.append(
160
+ "final-report data.json: error-analysis routing leadingCauseId must be "
161
+ "empty or reference a cause candidate."
162
+ )
163
+
164
+ return target
165
+
166
+
167
+ def _validate_verdict_directions(
168
+ data: Mapping[str, Any], target: Any, failures: list[str]
169
+ ) -> None:
170
+ expected_direction = ERROR_ANALYSIS_ROUTING_DIRECTIONS.get(target)
171
+ verdict_card_value = data.get("verdictCard")
172
+ verdict_card = verdict_card_value if isinstance(verdict_card_value, Mapping) else {}
173
+ final_verdict_value = data.get("finalVerdict")
174
+ final_verdict = (
175
+ final_verdict_value if isinstance(final_verdict_value, Mapping) else {}
176
+ )
177
+ if expected_direction:
178
+ for field_name, verdict in (
179
+ ("verdictCard", verdict_card),
180
+ ("finalVerdict", final_verdict),
181
+ ):
182
+ if verdict.get("direction") != expected_direction:
183
+ failures.append(
184
+ f"final-report data.json: {field_name}.direction must be "
185
+ f"`{expected_direction}` for `{target}` routing."
186
+ )
187
+
188
+
189
+ def _validate_phase_continuation(
190
+ data: Mapping[str, Any], target: Any, failures: list[str]
191
+ ) -> None:
192
+ follow_up_tasks_value = data.get("followUpTasks")
193
+ follow_up_tasks = (
194
+ follow_up_tasks_value if isinstance(follow_up_tasks_value, list) else []
195
+ )
196
+ continuations = [
197
+ row
198
+ for row in follow_up_tasks
199
+ if isinstance(row, Mapping) and row.get("origin") == "phase-continuation"
200
+ ]
201
+ if len(continuations) != 1:
202
+ failures.append(
203
+ "final-report data.json: followUpTasks must contain exactly one "
204
+ "phase-continuation row."
205
+ )
206
+ else:
207
+ continuation = continuations[0]
208
+ if continuation.get("suggestedTaskType") != target:
209
+ failures.append(
210
+ "final-report data.json: phase-continuation suggestedTaskType "
211
+ "must match the routing target."
212
+ )
213
+ # `newTaskId` 의 형식과 `priority` 값은 여기서 보지 않는다. 이 행은
214
+ # `okstra-spawn-followups.py` 의 `NON_SPAWNING_ORIGINS` 에 들어 있어
215
+ # 아무것도 스폰하지 않는 표식이고, 그래서 두 필드에는 소비자가 없다.
216
+ # 게다가 그 스크립트의 priority 기본값은 `P1` 인데 여기서는 `P0` 를
217
+ # 요구해 두 값이 정면으로 어긋났고, 두 규칙 중 어느 쪽도 report-writer
218
+ # 가 읽는 스키마·프로필·템플릿 어디에도 적혀 있지 않았다 — 작성자가
219
+ # 알 수 없는 규칙을 소비자 없는 필드에 걸어 두고 있었다.
220
+ if continuation.get("autoSpawn") != "no":
221
+ failures.append(
222
+ "final-report data.json: phase-continuation autoSpawn must be no."
223
+ )
224
+
225
+
226
+ def _validate_error_analysis_consistency(
227
+ data: Mapping[str, Any], failures: list[str]
228
+ ) -> None:
229
+ error_analysis_value = data.get("errorAnalysis")
230
+ error_analysis = (
231
+ error_analysis_value if isinstance(error_analysis_value, Mapping) else {}
232
+ )
233
+ _validate_reproduction(error_analysis, failures)
234
+ candidates_value = error_analysis.get("causeCandidates")
235
+ candidates = candidates_value if isinstance(candidates_value, list) else []
236
+ candidate_ids = _validate_cause_candidates(candidates, failures)
237
+ target = _validate_routing_cause(
238
+ error_analysis, candidates, candidate_ids, failures
239
+ )
240
+ _validate_verdict_directions(data, target, failures)
241
+ _validate_phase_continuation(data, target, failures)
@@ -0,0 +1 @@
1
+ """기능 분석 단계의 대상 해석과 보고서 정책."""
@@ -0,0 +1,8 @@
1
+ {
2
+ "allowed": " - confirmed target and normal, alternate, and failure flows\n - rules, state transitions, external calls, and test coverage scope\n",
3
+ "forbidden": [
4
+ "source or configuration edits",
5
+ "tests, builds, migrations, or deployments",
6
+ "starting any other lifecycle phase inside this run"
7
+ ]
8
+ }
@@ -0,0 +1,63 @@
1
+ """선택한 프로젝트 증거에서 기능 분석 대상을 해석한다."""
2
+
3
+ from __future__ import annotations
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Mapping, Sequence
7
+ from okstra_ctl.prepare_error import PrepareError
8
+ from okstra_ctl.analysis_inputs import (
9
+ AnalysisInputError,
10
+ AnalysisReportCandidate,
11
+ ResolvedEvidenceInput,
12
+ )
13
+
14
+ _FEATURE_ID_RE = re.compile(r"PF-\d{3}")
15
+
16
+
17
+ def resolve_analysis_target(
18
+ raw_target: str,
19
+ evidence_inputs: Sequence[ResolvedEvidenceInput],
20
+ candidates: Mapping[Path, AnalysisReportCandidate],
21
+ ) -> dict[str, object]:
22
+ """Normalize a feature-index identifier or a non-empty free-text target."""
23
+ target = raw_target.strip()
24
+ if not target:
25
+ raise AnalysisInputError("analysis target must not be empty")
26
+ if not _FEATURE_ID_RE.fullmatch(target):
27
+ return {"inputMode": "free-text", "requestedValue": target}
28
+ matches: list[dict[object, object]] = []
29
+ for evidence in evidence_inputs:
30
+ if evidence.relation != "project-context":
31
+ continue
32
+ candidate = candidates.get(evidence.report_path)
33
+ if candidate is None:
34
+ candidate = next(
35
+ (
36
+ value
37
+ for path, value in candidates.items()
38
+ if path.resolve() == evidence.report_path
39
+ ),
40
+ None,
41
+ )
42
+ if candidate is None:
43
+ continue
44
+ matches.extend(
45
+ feature
46
+ for feature in candidate.feature_index
47
+ if isinstance(feature, dict) and feature.get("id") == target
48
+ )
49
+ if len(matches) != 1:
50
+ raise AnalysisInputError(
51
+ f"exactly one project-context feature must match {target}"
52
+ )
53
+ return {
54
+ "inputMode": "feature-index",
55
+ "requestedValue": target,
56
+ "feature": dict(matches[0]),
57
+ }
58
+
59
+
60
+ def validate_feature_target_input(raw_target: str) -> None:
61
+ """실행 생성 전에 필수 대상 입력을 확인한다."""
62
+ if not raw_target.strip():
63
+ raise PrepareError("feature-analysis requires --analysis-target")
@@ -1,9 +1,10 @@
1
1
  """Human-first feature-analysis view model."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
- from ..common import analysis_review_ids, evidence_index
5
- from ..models import HumanReportView, VisualNode
6
- from ..visualizations import flow_figure
5
+ from okstra_ctl.report_html.common import analysis_review_ids, evidence_index
6
+ from okstra_ctl.report_html.models import HumanReportView, VisualNode
7
+ from okstra_ctl.report_html.visualizations import flow_figure
7
8
 
8
9
  # Record fields this template anchors as `id-<row id>` (see
9
10
  # `HumanReportView.anchored_fields`); ids elsewhere land in the ledger.
@@ -15,7 +16,11 @@ ANCHORED_FIELDS = (
15
16
  )
16
17
 
17
18
 
18
- _FLOW_KINDS = {"normal": "Normal path", "alternative": "Alternative path", "failure": "Failure path"}
19
+ _FLOW_KINDS = {
20
+ "normal": "Normal path",
21
+ "alternative": "Alternative path",
22
+ "failure": "Failure path",
23
+ }
19
24
 
20
25
 
21
26
  def _flow_nodes(feature: dict) -> tuple[VisualNode, ...]:
@@ -33,7 +38,9 @@ def _flow_nodes(feature: dict) -> tuple[VisualNode, ...]:
33
38
 
34
39
  def build_feature_analysis_view(data: dict) -> HumanReportView:
35
40
  feature = data["featureAnalysis"]
36
- figure = flow_figure(nodes=_flow_nodes(feature), edges=(), title="Feature behavior paths")
41
+ figure = flow_figure(
42
+ nodes=_flow_nodes(feature), edges=(), title="Feature behavior paths"
43
+ )
37
44
  context = {
38
45
  "humanSummary": data["humanSummary"],
39
46
  "feature": feature,
@@ -0,0 +1,22 @@
1
+ # Feature analysis phase
2
+
3
+ ## Purpose and boundaries
4
+
5
+ This phase describes a confirmed feature's normal, alternate and failure flows, domain rules, state changes, external interactions and test coverage. The profile guides read-only analysis without implementation alternatives or execution plans. Evidence report loading, user review authority, source snapshots and report publication remain common contracts.
6
+
7
+ ## Inputs and questions
8
+
9
+ `entry.resolve_analysis_target` trims free-text targets and resolves `PF-` identifiers against exactly one feature in the selected project-context evidence. `entry.validate_feature_target_input` rejects a missing target before provisioning. The wizard preserves project-evidence selection before feature-index selection or direct target text, including reuse and rewind behavior. `wizard.py` owns target questions; shared evidence questions stay in `wizard/steps_analysis.py`.
10
+
11
+ ## Reports and validation
12
+
13
+ `profile.md` and `profile.json` own instructions and worker roles. `report.py` and `report_assets/` own feature report content. `validation._validate_feature_semantics` checks that report target fields match the resolved target. The common analysis validator continues to enforce current-code evidence, included paths, worker completion and scope confirmation. Shared schemas retain the same versions and artifact names. The profile does not choose a next lifecycle phase; orchestration remains common.
14
+
15
+ ## Guarantees
16
+
17
+ | ID | Guarantee | Enforcement |
18
+ |---|---|---|
19
+ | FA-1 | A feature-index target resolves to exactly one selected project-context feature; free text cannot be empty. | `scripts/okstra_ctl/phases/feature_analysis/entry.py::resolve_analysis_target` |
20
+ | FA-2 | Report target identity matches the resolved analysis target. | `scripts/okstra_ctl/phases/feature_analysis/validation.py::_validate_feature_semantics` |
21
+ | FA-3 | Current-code evidence remains within the declared analysis scope. | `validators/validate_analysis_report.py::_validate_all_current_code_evidence` |
22
+ | FA-4 | Feature target selection preserves the wizard's rendered arguments. | `tests/domain/wizard/test_analysis_inputs.py::test_feature_target_from_index_reaches_render_args` |
@@ -0,0 +1,27 @@
1
+ """기능 분석 결과의 대상 일치 정책."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ EVIDENCE_COLLECTIONS = ("flows", "domainRules", "stateChanges", "externalInteractions")
7
+
8
+
9
+ def _validate_feature_semantics(data: dict, errors: list[str]) -> None:
10
+ analysis = data.get("featureAnalysis") or {}
11
+ common_target = ((data.get("analysisCommon") or {}).get("scope") or {}).get(
12
+ "resolvedTarget"
13
+ ) or {}
14
+ feature_target = analysis.get("target") or {}
15
+ for field in ("inputMode", "requestedValue"):
16
+ if feature_target.get(field) != common_target.get(field):
17
+ errors.append(
18
+ f"featureAnalysis.target.{field} must match "
19
+ f"analysisCommon.scope.resolvedTarget.{field}"
20
+ )
21
+ resolved_feature = common_target.get("feature") or {}
22
+ if common_target.get("inputMode") == "feature-index" and (
23
+ feature_target.get("featureId") != resolved_feature.get("id")
24
+ ):
25
+ errors.append(
26
+ "featureAnalysis.target.featureId must match the resolved feature id"
27
+ )